Skip to content

Instantly share code, notes, and snippets.

@keyboardr
Last active October 15, 2017 11:47
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save keyboardr/8f558f2796ba5840f7a8646f06731792 to your computer and use it in GitHub Desktop.
Save keyboardr/8f558f2796ba5840f7a8646f06731792 to your computer and use it in GitHub Desktop.
A enum to parse and represent a music key
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