VexFlow - Music Engraving for HTML5 Copyright Mohit Muthanna 2010
This class implements some standard music theory routines.
requires: vex.js (Vex) requires: flow.js (Vex.Flow)
/**
* @constructor
*/
Vex.Flow.Music = (function() {
function Music() {
this.init();
}
Music.NUM_TONES = 12;
Music.roots = [ "c", "d", "e", "f", "g", "a", "b" ];
Music.root_values = [ 0, 2, 4, 5, 7, 9, 11 ];
Music.root_indices = {
"c": 0,
"d": 1,
"e": 2,
"f": 3,
"g": 4,
"a": 5,
"b": 6
};
Music.canonical_notes = [
"c", "c#", "d", "d#",
"e", "f", "f#", "g",
"g#", "a", "a#", "b"
];
Music.diatonic_intervals = [
"unison", "m2", "M2", "m3", "M3",
"p4", "dim5", "p5", "m6", "M6",
"b7", "M7", "octave"
];
Music.diatonic_accidentals = {
"unison": {note: 0, accidental: 0},
"m2": {note: 1, accidental: -1},
"M2": {note: 1, accidental: 0},
"m3": {note: 2, accidental: -1},
"M3": {note: 2, accidental: 0},
"p4": {note: 3, accidental: 0},
"dim5": {note: 4, accidental: -1},
"p5": {note: 4, accidental: 0},
"m6": {note: 5, accidental: -1},
"M6": {note: 5, accidental: 0},
"b7": {note: 6, accidental: -1},
"M7": {note: 6, accidental: 0},
"octave": {note: 7, accidental: 0}
};
Music.intervals = {
"u": 0, "unison": 0,
"m2": 1, "b2": 1, "min2": 1, "S": 1, "H": 1,
"2": 2, "M2": 2, "maj2": 2, "T": 2, "W": 2,
"m3": 3, "b3": 3, "min3": 3,
"M3": 4, "3": 4, "maj3": 4,
"4": 5, "p4": 5,
"#4": 6, "b5": 6, "aug4": 6, "dim5": 6,
"5": 7, "p5": 7,
"#5": 8, "b6": 8, "aug5": 8,
"6": 9, "M6": 9, "maj6": 9,
"b7": 10, "m7": 10, "min7": 10, "dom7": 10,
"M7": 11, "maj7": 11,
"8": 12, "octave": 12
};
Music.scales = {
major: [2, 2, 1, 2, 2, 2, 1],
dorian: [2, 1, 2, 2, 2, 1, 2],
mixolydian: [2, 2, 1, 2, 2, 1, 2],
minor: [2, 1, 2, 2, 1, 2, 2]
};
Music.accidentals = [ "bb", "b", "n", "#", "##" ];
Music.noteValues = {
'c': { root_index: 0, int_val: 0 },
'cn': { root_index: 0, int_val: 0 },
'c#': { root_index: 0, int_val: 1 },
'c##': { root_index: 0, int_val: 2 },
'cb': { root_index: 0, int_val: 11 },
'cbb': { root_index: 0, int_val: 10 },
'd': { root_index: 1, int_val: 2 },
'dn': { root_index: 1, int_val: 2 },
'd#': { root_index: 1, int_val: 3 },
'd##': { root_index: 1, int_val: 4 },
'db': { root_index: 1, int_val: 1 },
'dbb': { root_index: 1, int_val: 0 },
'e': { root_index: 2, int_val: 4 },
'en': { root_index: 2, int_val: 4 },
'e#': { root_index: 2, int_val: 5 },
'e##': { root_index: 2, int_val: 6 },
'eb': { root_index: 2, int_val: 3 },
'ebb': { root_index: 2, int_val: 2 },
'f': { root_index: 3, int_val: 5 },
'fn': { root_index: 3, int_val: 5 },
'f#': { root_index: 3, int_val: 6 },
'f##': { root_index: 3, int_val: 7 },
'fb': { root_index: 3, int_val: 4 },
'fbb': { root_index: 3, int_val: 3 },
'g': { root_index: 4, int_val: 7 },
'gn': { root_index: 4, int_val: 7 },
'g#': { root_index: 4, int_val: 8 },
'g##': { root_index: 4, int_val: 9 },
'gb': { root_index: 4, int_val: 6 },
'gbb': { root_index: 4, int_val: 5 },
'a': { root_index: 5, int_val: 9 },
'an': { root_index: 5, int_val: 9 },
'a#': { root_index: 5, int_val: 10 },
'a##': { root_index: 5, int_val: 11 },
'ab': { root_index: 5, int_val: 8 },
'abb': { root_index: 5, int_val: 7 },
'b': { root_index: 6, int_val: 11 },
'bn': { root_index: 6, int_val: 11 },
'b#': { root_index: 6, int_val: 0 },
'b##': { root_index: 6, int_val: 1 },
'bb': { root_index: 6, int_val: 10 },
'bbb': { root_index: 6, int_val: 9 }
};
Music.prototype = {
init: function() {},
isValidNoteValue: function(note) {
if (note == null || note < 0 || note >= Vex.Flow.Music.NUM_TONES)
return false;
return true;
},
isValidIntervalValue: function(interval) {
return this.isValidNoteValue(interval);
},
getNoteParts: function(noteString) {
if (!noteString || noteString.length < 1)
throw new Vex.RERR("BadArguments", "Invalid note name: " + noteString);
if (noteString.length > 3)
throw new Vex.RERR("BadArguments", "Invalid note name: " + noteString);
var note = noteString.toLowerCase();
var regex = /^([cdefgab])(b|bb|n|#|##)?$/;
var match = regex.exec(note);
if (match != null) {
var root = match[1];
var accidental = match[2];
return {
'root': root,
'accidental': accidental
};
} else {
throw new Vex.RERR("BadArguments", "Invalid note name: " + noteString);
}
},
getKeyParts: function(keyString) {
if (!keyString || keyString.length < 1)
throw new Vex.RERR("BadArguments", "Invalid key: " + keyString);
var key = keyString.toLowerCase();Support Major, Minor, Melodic Minor, and Harmonic Minor key types.
var regex = /^([cdefgab])(b|#)?(mel|harm|m|M)?$/;
var match = regex.exec(key);
if (match != null) {
var root = match[1];
var accidental = match[2];
var type = match[3];Unspecified type implies major
if (!type) type = "M";
return {
'root': root,
'accidental': accidental,
'type': type
};
} else {
throw new Vex.RERR("BadArguments", "Invalid key: " + keyString);
}
},
getNoteValue: function(noteString) {
var value = Music.noteValues[noteString];
if (value == null)
throw new Vex.RERR("BadArguments", "Invalid note name: " + noteString);
return value.int_val;
},
getIntervalValue: function(intervalString) {
var value = Music.intervals[intervalString];
if (value == null)
throw new Vex.RERR("BadArguments",
"Invalid interval name: " + intervalString);
return value;
},
getCanonicalNoteName: function(noteValue) {
if (!this.isValidNoteValue(noteValue))
throw new Vex.RERR("BadArguments",
"Invalid note value: " + noteValue);
return Music.canonical_notes[noteValue];
},
getCanonicalIntervalName: function(intervalValue) {
if (!this.isValidIntervalValue(intervalValue))
throw new Vex.RERR("BadArguments",
"Invalid interval value: " + intervalValue);
return Music.diatonic_intervals[intervalValue];
},
/* Given a note, interval, and interval direction, product the
* relative note.
*/
getRelativeNoteValue: function(noteValue, intervalValue, direction) {
if (direction == null) direction = 1;
if (direction != 1 && direction != -1)
throw new Vex.RERR("BadArguments", "Invalid direction: " + direction);
var sum = (noteValue + (direction * intervalValue)) % Music.NUM_TONES;
if (sum < 0) sum += Music.NUM_TONES;
return sum;
},
getRelativeNoteName: function(root, noteValue) {
var parts = this.getNoteParts(root);
var rootValue = this.getNoteValue(parts.root);
var interval = noteValue - rootValue;
if (Math.abs(interval) > Music.NUM_TONES - 3) {
var multiplier = 1;
if (interval > 0 ) multiplier = -1;Possibly wrap around. (Add +1 for modulo operator)
var reverse_interval = (((noteValue + 1) + (rootValue + 1)) %
Music.NUM_TONES) * multiplier;
if (Math.abs(reverse_interval) > 2) {
throw new Vex.RERR("BadArguments", "Notes not related: " + root + ", " +
noteValue);
} else {
interval = reverse_interval;
}
}
if (Math.abs(interval) > 2)
throw new Vex.RERR("BadArguments", "Notes not related: " + root + ", " +
noteValue);
var relativeNoteName = parts.root;
var i;
if (interval > 0) {
for (i = 1; i <= interval; ++i)
relativeNoteName += "#";
} else if (interval < 0) {
for (i = -1; i >= interval; --i)
relativeNoteName += "b";
}
return relativeNoteName;
},
/* Return scale tones, given intervals. Each successive interval is
* relative to the previous one, e.g., Major Scale:
*
* TTSTTTS = [2,2,1,2,2,2,1]
*
* When used with key = 0, returns C scale (which is isomorphic to
* interval list).
*/
getScaleTones: function(key, intervals) {
var tones = [];
tones.push(key);
var nextNote = key;
for (var i = 0; i < intervals.length; ++i) {
nextNote = this.getRelativeNoteValue(nextNote,
intervals[i]);
if (nextNote != key) tones.push(nextNote);
}
return tones;
},
/* Returns the interval of a note, given a diatonic scale.
*
* E.g., Given the scale C, and the note E, returns M3
*/
getIntervalBetween: function(note1, note2, direction) {
if (direction == null) direction = 1;
if (direction != 1 && direction != -1)
throw new Vex.RERR("BadArguments", "Invalid direction: " + direction);
if (!this.isValidNoteValue(note1) || !this.isValidNoteValue(note2))
throw new Vex.RERR("BadArguments",
"Invalid notes: " + note1 + ", " + note2);
var difference;
if (direction == 1)
difference = note2 - note1;
else
difference = note1 - note2;
if (difference < 0) difference += Music.NUM_TONES;
return difference;
},Create a scale map that represents the pitch state for a
keySignature. For example, passing a G to keySignature would
return a scale map with every note naturalized except for F which
has an F# state.
createScaleMap: function(keySignature) {
var keySigParts = this.getKeyParts(keySignature);
var scaleName = Vex.Flow.KeyManager.scales[keySigParts.type];
var keySigString = keySigParts.root;
if (keySigParts.accidental) keySigString += keySigParts.accidental;
if (!scaleName) throw new Vex.RERR("BadArguments", "Unsupported key type: " + keySignature);
var scale = this.getScaleTones(this.getNoteValue(keySigString), scaleName);
var noteLocation = Vex.Flow.Music.root_indices[keySigParts.root];
var scaleMap = {};
for (var i = 0; i < Vex.Flow.Music.roots.length; ++i) {
var index = (noteLocation + i) % Vex.Flow.Music.roots.length;
var rootName = Vex.Flow.Music.roots[index];
var noteName = this.getRelativeNoteName(rootName, scale[i]);
if (noteName.length === 1) {
noteName += "n";
}
scaleMap[rootName] = noteName;
}
return scaleMap;
}
};
return Music;
}());