UNPKG

vexflow

Version:

A JavaScript library for rendering music notation and guitar tablature.

444 lines (379 loc) 12.9 kB
// [VexFlow](https://vexflow.com) - Copyright (c) Mohit Muthanna 2010. // MIT License import { RuntimeError } from './util'; export interface NoteAccidental { note: number; accidental: AccidentalValue; } export interface NoteParts { root: string; accidental: string; } export interface KeyParts { root: string; accidental: string; type: string; } export type KeyValue = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11; export type RootValue = 0 | 1 | 2 | 3 | 4 | 5 | 6; export type AccidentalValue = -2 | -1 | 0 | 1 | 2; export interface Key { root_index: RootValue; int_val: KeyValue; } /** Music implements some standard music theory routines. */ export class Music { /** Number of an canonical notes (12). */ static get NUM_TONES(): number { return this.canonical_notes.length; } /** Names of root notes ('c', 'd',...) */ static get roots(): string[] { return ['c', 'd', 'e', 'f', 'g', 'a', 'b']; } /** Values of the root notes.*/ static get root_values(): KeyValue[] { return [0, 2, 4, 5, 7, 9, 11]; } /** Indices of the root notes.*/ static get root_indices(): Record<string, RootValue> { return { c: 0, d: 1, e: 2, f: 3, g: 4, a: 5, b: 6, }; } /** Names of canonical notes ('c', 'c#', 'd',...). */ static get canonical_notes(): string[] { return ['c', 'c#', 'd', 'd#', 'e', 'f', 'f#', 'g', 'g#', 'a', 'a#', 'b']; } /** Names of diatonic intervals ('unison', 'm2', 'M2',...). */ static get diatonic_intervals(): string[] { return ['unison', 'm2', 'M2', 'm3', 'M3', 'p4', 'dim5', 'p5', 'm6', 'M6', 'b7', 'M7', 'octave']; } /** NoteAccidental associated to diatonic intervals. */ static get diatonic_accidentals(): Record<string, NoteAccidental> { return { 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 }, }; } /** Semitones shift associated to intervals .*/ static get intervals(): Record<string, number> { return { 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, }; } /** Semitones shifts associated with scales. */ static get scales(): Record<string, number[]> { return { major: [2, 2, 1, 2, 2, 2, 1], minor: [2, 1, 2, 2, 1, 2, 2], ionian: [2, 2, 1, 2, 2, 2, 1], dorian: [2, 1, 2, 2, 2, 1, 2], phyrgian: [1, 2, 2, 2, 1, 2, 2], lydian: [2, 2, 2, 1, 2, 2, 1], mixolydian: [2, 2, 1, 2, 2, 1, 2], aeolian: [2, 1, 2, 2, 1, 2, 2], locrian: [1, 2, 2, 1, 2, 2, 2], }; } /** Scales associated with m (minor) and M (major). */ static get scaleTypes(): Record<string, number[]> { return { M: Music.scales.major, m: Music.scales.minor, }; } /** Accidentals abbreviations. */ static get accidentals(): string[] { return ['bb', 'b', 'n', '#', '##']; } /** Note values. */ static get noteValues(): Record<string, Key> { return { 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 }, }; } protected isValidNoteValue(note: number): boolean { return note >= 0 && note < Music.canonical_notes.length; } protected isValidIntervalValue(interval: number): boolean { return interval >= 0 && interval < Music.diatonic_intervals.length; } /** Return root and accidental associated to a note. */ getNoteParts(noteString: string): NoteParts { if (!noteString || noteString.length < 1) { throw new RuntimeError('BadArguments', 'Invalid note name: ' + noteString); } if (noteString.length > 3) { throw new RuntimeError('BadArguments', 'Invalid note name: ' + noteString); } const note = noteString.toLowerCase(); const regex = /^([cdefgab])(b|bb|n|#|##)?$/; const match = regex.exec(note); if (match !== null) { const root = match[1]; const accidental = match[2]; return { root, accidental, }; } else { throw new RuntimeError('BadArguments', 'Invalid note name: ' + noteString); } } /** Return root, accidental and type associated to a key. */ getKeyParts(keyString: string): KeyParts { if (!keyString || keyString.length < 1) { throw new RuntimeError('BadArguments', 'Invalid key: ' + keyString); } const key = keyString.toLowerCase(); // Support Major, Minor, Melodic Minor, and Harmonic Minor key types. const regex = /^([cdefgab])(b|#)?(mel|harm|m|M)?$/; const match = regex.exec(key); if (match !== null) { const root = match[1]; const accidental = match[2]; let type = match[3]; // Unspecified type implies major if (!type) type = 'M'; return { root, accidental, type, }; } else { throw new RuntimeError('BadArguments', `Invalid key: ${keyString}`); } } /** Note value associated to a note name. */ getNoteValue(noteString: string): number { const value = Music.noteValues[noteString]; if (value === undefined) { throw new RuntimeError('BadArguments', `Invalid note name: ${noteString}`); } return value.int_val; } /** Interval value associated to an interval name. */ getIntervalValue(intervalString: string): number { const value = Music.intervals[intervalString]; if (value === undefined) { throw new RuntimeError('BadArguments', `Invalid interval name: ${intervalString}`); } return value; } /** Canonical note name associated to a value. */ getCanonicalNoteName(noteValue: number): string { if (!this.isValidNoteValue(noteValue)) { throw new RuntimeError('BadArguments', `Invalid note value: ${noteValue}`); } return Music.canonical_notes[noteValue]; } /** Interval name associated to a value. */ getCanonicalIntervalName(intervalValue: number): string { if (!this.isValidIntervalValue(intervalValue)) { throw new RuntimeError('BadArguments', `Invalid interval value: ${intervalValue}`); } return Music.diatonic_intervals[intervalValue]; } /** * Given a note, interval, and interval direction, produce the relative note. */ getRelativeNoteValue(noteValue: number, intervalValue: number, direction: number = 1): number { if (direction !== 1 && direction !== -1) { throw new RuntimeError('BadArguments', `Invalid direction: ${direction}`); } let sum = (noteValue + direction * intervalValue) % Music.NUM_TONES; if (sum < 0) sum += Music.NUM_TONES; return sum; } /** * Given a root and note value, produce the relative note name. */ getRelativeNoteName(root: string, noteValue: number): string { const parts = this.getNoteParts(root); const rootValue = this.getNoteValue(parts.root); let interval = noteValue - rootValue; if (Math.abs(interval) > Music.NUM_TONES - 3) { let multiplier = 1; if (interval > 0) multiplier = -1; // Possibly wrap around. (Add +1 for modulo operator) const reverse_interval = ((noteValue + 1 + (rootValue + 1)) % Music.NUM_TONES) * multiplier; if (Math.abs(reverse_interval) > 2) { throw new RuntimeError('BadArguments', `Notes not related: ${root}, ${noteValue})`); } else { interval = reverse_interval; } } if (Math.abs(interval) > 2) { throw new RuntimeError('BadArguments', `Notes not related: ${root}, ${noteValue})`); } let relativeNoteName = parts.root; if (interval > 0) { for (let i = 1; i <= interval; ++i) { relativeNoteName += '#'; } } else if (interval < 0) { for (let 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(key: number, intervals: number[]): number[] { const tones = [key]; let nextNote = key; for (let i = 0; i < intervals.length; i++) { nextNote = this.getRelativeNoteValue(nextNote, intervals[i]); if (nextNote !== key) tones.push(nextNote); } return tones; } /** * Return the interval of a note, given a diatonic scale. * e.g., given the scale C, and the note E, returns M3. */ getIntervalBetween(note1: number, note2: number, direction: number = 1): number { if (direction !== 1 && direction !== -1) { throw new RuntimeError('BadArguments', `Invalid direction: ${direction}`); } if (!this.isValidNoteValue(note1) || !this.isValidNoteValue(note2)) { throw new RuntimeError('BadArguments', `Invalid notes: ${note1}, ${note2}`); } let difference = direction === 1 ? note2 - note1 : 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(keySignature: string): Record<string, string> { const keySigParts = this.getKeyParts(keySignature); if (!keySigParts.type) throw new RuntimeError('BadArguments', 'Unsupported key type: undefined'); const scaleName = Music.scaleTypes[keySigParts.type]; let keySigString = keySigParts.root; if (keySigParts.accidental) keySigString += keySigParts.accidental; if (!scaleName) throw new RuntimeError('BadArguments', 'Unsupported key type: ' + keySignature); const scale = this.getScaleTones(this.getNoteValue(keySigString), scaleName); const noteLocation = Music.root_indices[keySigParts.root]; const scaleMap = {} as Record<string, string>; for (let i = 0; i < Music.roots.length; ++i) { const index = (noteLocation + i) % Music.roots.length; const rootName = Music.roots[index]; let noteName = this.getRelativeNoteName(rootName, scale[i]); if (noteName.length === 1) { noteName += 'n'; } scaleMap[rootName] = noteName; } return scaleMap; } }