Last active
October 15, 2017 11:47
-
-
Save keyboardr/8f558f2796ba5840f7a8646f06731792 to your computer and use it in GitHub Desktop.
A enum to parse and represent a music key
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.keyboardr.bluejay.model; | |
import android.support.annotation.NonNull; | |
import android.support.annotation.Nullable; | |
import android.text.TextUtils; | |
import java.util.ArrayList; | |
import java.util.Arrays; | |
import java.util.Collections; | |
import java.util.Comparator; | |
import java.util.List; | |
/** | |
* Tools for parsing the key of a song | |
*/ | |
public enum MusicKey { | |
Ab(false, 4), A(false, 11), Bb(false, 6), B(false, 1), C(false, 8), Db(false, 3), D(false, 10), | |
Eb(false, 5), E(false, 12), F(false, 7), Gb(false, 2), G(false, 9), | |
Abm(true, 1), Am(true, 8), Bbm(true, 3), Bm(true, 10), Cm(true, 5), Dbm(true, 12), | |
Dm(true, 7), Ebm(true, 2), Em(true, 9), Fm(true, 4), Gbm(true, 11), Gm(true, 6); | |
public static final Comparator<MusicKey> TONIC_ORDER = new Comparator<MusicKey>() { | |
@Override | |
public int compare(MusicKey musicKey, MusicKey t1) { | |
return musicKey.tonic > t1.tonic || (!musicKey.isMinor && t1.isMinor) ? 1 | |
: musicKey.tonic < t1.tonic || (musicKey.isMinor && t1.isMinor) ? -1 : 0; | |
} | |
}; | |
public static final Comparator<MusicKey> CAMELOT_ORDER = new Comparator<MusicKey>() { | |
@Override | |
public int compare(MusicKey musicKey, MusicKey t1) { | |
return musicKey.camelotNumber > t1.camelotNumber || (!musicKey.isMinor && t1.isMinor) ? 1 | |
: musicKey.camelotNumber < t1.camelotNumber || (musicKey.isMinor && t1.isMinor) ? -1 : 0; | |
} | |
}; | |
private static final List<MusicKey> sCamelotList; | |
static { | |
sCamelotList = new ArrayList<>(24); | |
sCamelotList.addAll(Arrays.asList(MusicKey.values())); | |
Collections.sort(sCamelotList, new Comparator<MusicKey>() { | |
@Override | |
public int compare(MusicKey left, MusicKey right) { | |
if (left.isMinor && !right.isMinor) { | |
return -1; | |
} | |
if (!left.isMinor && right.isMinor) { | |
return 1; | |
} | |
return left.camelotNumber - right.camelotNumber; | |
} | |
}); | |
} | |
@Nullable | |
public static MusicKey fromString(@NonNull String name) { | |
name = name.trim(); | |
if (TextUtils.isEmpty(name)) { | |
return null; | |
} | |
if (Character.isDigit(name.charAt(0))) { | |
// Try parsing Camelot/OK notation | |
name = name.toUpperCase(); | |
boolean isMinor; | |
boolean isCamelot = false; | |
switch (name.charAt(name.length() - 1)) { | |
case 'A': | |
isCamelot = true; | |
// fallthrough | |
case 'M': | |
isMinor = true; | |
break; | |
case 'B': | |
isCamelot = true; | |
// fallthrough | |
case 'D': | |
isMinor = false; | |
break; | |
default: | |
return null; | |
} | |
int camelotNumber; | |
try { | |
camelotNumber = Integer.valueOf(name.substring(0, name.length() - 1)); | |
} catch (NumberFormatException e) { | |
return null; | |
} | |
if (camelotNumber > 12 || camelotNumber <= 0) { | |
return null; | |
} | |
int index = camelotNumber - 1; | |
if (!isCamelot) { | |
index += 7; | |
index %= 12; | |
} | |
if (isMinor) { | |
index += 12; | |
} | |
return sCamelotList.get(index); | |
} | |
// Try parsing sharp/flat notation | |
name = name.toLowerCase(); | |
boolean isMinor = name.charAt(name.length() - 1) == 'm'; | |
if (isMinor) { | |
name = name.substring(0, name.length() - 1); | |
} else if (name.endsWith("minor")) { | |
isMinor = true; | |
name = name.substring(0, name.length() - 5); | |
} | |
if (TextUtils.isEmpty(name)) { | |
return null; | |
} | |
int tonic; | |
switch (name.charAt(0)) { | |
case 'a': | |
tonic = 1; | |
break; | |
case 'b': | |
case 'h': | |
tonic = 3; | |
break; | |
case 'c': | |
tonic = 4; | |
break; | |
case 'd': | |
tonic = 6; | |
break; | |
case 'e': | |
tonic = 8; | |
break; | |
case 'f': | |
tonic = 9; | |
break; | |
case 'g': | |
tonic = 11; | |
break; | |
default: | |
return null; | |
} | |
name = name.substring(1); | |
loop: | |
while (name.length() > 0) { | |
switch (name.charAt(0)) { | |
case 'b': | |
case '\u266d': //♭ | |
tonic--; | |
break; | |
case '#': | |
tonic++; | |
break; | |
case 'x': | |
tonic += 2; | |
break; | |
case '\u266e': //♮ | |
break; | |
case '\uD834': | |
if (name.startsWith("\ud834\udd2a")) { // 𝄪 | |
tonic += 2; | |
name = name.substring(1); | |
break; | |
} else if (name.startsWith("\ud834\udd2b")) { // 𝄫 | |
tonic -= 2; | |
name = name.substring(1); | |
break; | |
} else break loop; | |
case 's': | |
if (name.startsWith("sharp")) { | |
tonic++; | |
name = name.substring(4); | |
break; | |
} else break loop; | |
case 'f': | |
if (name.startsWith("flat")) { | |
tonic--; | |
name = name.substring(3); | |
break; | |
} else break loop; | |
default: | |
if (Character.isWhitespace(name.charAt(0))) { | |
break; | |
} else break loop; | |
} | |
name = name.substring(1); | |
} | |
tonic %= 12; | |
return MusicKey.values()[isMinor ? tonic + 12 : tonic]; | |
} | |
public final boolean isMinor; | |
public final int camelotNumber; | |
private final int tonic; | |
private String sharpName; | |
private String flatName; | |
private MusicKey relative; | |
private MusicKey parallel; | |
MusicKey(boolean isMinor, int camelotNumber) { | |
this.isMinor = isMinor; | |
this.camelotNumber = camelotNumber; | |
this.tonic = ordinal() % 12; | |
} | |
@NonNull | |
public MusicKey getRelative() { | |
if (relative == null) { | |
for (int i = isMinor ? 0 : 12; i < 24; i++) { | |
MusicKey other = MusicKey.values()[i]; | |
if (other.camelotNumber == camelotNumber) { | |
relative = other; | |
other.relative = this; | |
break; | |
} | |
} | |
} | |
return relative; | |
} | |
public MusicKey getParallel() { | |
if (parallel == null) { | |
parallel = MusicKey.values()[(ordinal() + 12) % 24]; | |
parallel.parallel = this; | |
} | |
return parallel; | |
} | |
public String getCamelotName() { | |
return camelotNumber + (isMinor ? "A" : "B"); | |
} | |
public String getOKName() { | |
return getOkNumber() + (isMinor ? "m" : "d"); | |
} | |
private int getOkNumber() { | |
return (camelotNumber - 7) % 12; | |
} | |
public String getSharpName() { | |
if (sharpName == null) { | |
String name; | |
switch (tonic) { | |
case 0: | |
name = "G#"; | |
break; | |
case 1: | |
name = "A"; | |
break; | |
case 2: | |
name = "A#"; | |
break; | |
case 3: | |
name = "B"; | |
break; | |
case 4: | |
name = "C"; | |
break; | |
case 5: | |
name = "C#"; | |
break; | |
case 6: | |
name = "D"; | |
break; | |
case 7: | |
name = "D#"; | |
break; | |
case 8: | |
name = "E"; | |
break; | |
case 9: | |
name = "F"; | |
break; | |
case 10: | |
name = "F#"; | |
break; | |
case 11: | |
name = "G"; | |
break; | |
default: | |
throw new IllegalArgumentException("Unknown tonic: " + tonic); | |
} | |
if (isMinor) { | |
name += "m"; | |
} | |
sharpName = name; | |
} | |
return sharpName; | |
} | |
public String getFlatName() { | |
if (flatName == null) { | |
String name; | |
switch (tonic) { | |
case 0: | |
name = "A♭"; | |
break; | |
case 1: | |
name = "A"; | |
break; | |
case 2: | |
name = "B♭"; | |
break; | |
case 3: | |
name = "B"; | |
break; | |
case 4: | |
name = "C"; | |
break; | |
case 5: | |
name = "D♭"; | |
break; | |
case 6: | |
name = "D"; | |
break; | |
case 7: | |
name = "E♭"; | |
break; | |
case 8: | |
name = "E"; | |
break; | |
case 9: | |
name = "F"; | |
break; | |
case 10: | |
name = "G♭"; | |
break; | |
case 11: | |
name = "G"; | |
break; | |
default: | |
throw new IllegalArgumentException("Unknown tonic: " + tonic); | |
} | |
if (isMinor) { | |
name += "m"; | |
} | |
flatName = name; | |
} | |
return flatName; | |
} | |
public String getNaturalName() { | |
int sharps = getOkNumber() - 1; | |
if (sharps > 6) { | |
return getFlatName(); | |
} | |
if (sharps < 6) { | |
return getSharpName(); | |
} | |
return isMinor ? getFlatName() : getSharpName(); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment