function notesBetween(a, b) { const [tonicA, tonicB] = [a[0], b[0]] const abs = tonicB.charCodeAt() - tonicA.charCodeAt(); if (abs < 0) { return abs + 7; } else { return abs; } } const ringHandler = { get(target, prop) { return target[prop % target.length] || Reflect.get(...arguments); } }; export const chromaticScale = new Proxy( ['C', 'C♯', 'D', 'D♯', 'E', 'F', 'F♯', 'G', 'G♯', 'A', 'A♯', 'B'], ringHandler); // todo: handle tonics with a flat ‘♭’, so we can deal with [BE]♭. function scaleFromIntervals(tonic, intervals) { const scaleIndex = chromaticScale.indexOf(tonic); if (scaleIndex < 0) { throw new Error(`tonic ${tonic} not found in scale ${chromaticScale.toString()}`); } let scale = [tonic]; let steps = 0; let lastBase = tonic[0]; for (let i = 0; i < intervals.length; i++) { steps += intervals[i]; const note = chromaticScale[scaleIndex + steps]; // don't display two base notes in a row by changing // accidentals. const delta = notesBetween(lastBase, note); if (delta === 0) { const nextBase = chromaticScale[scaleIndex + steps + 1][0]; scale.push(`${nextBase}♭`) } else if (delta === 1) { scale.push(`${note}`) } else { // we can only be two away, so take the sharp of the // previous note. const nextBase = chromaticScale[scaleIndex + steps - 1][0]; scale.push(`${nextBase}♯`) } lastBase = scale[scale.length-1]; } return new Proxy(scale, ringHandler); } class Scale { constructor(tonic, intervals) { this.scale = scaleFromIntervals(tonic, intervals); return new Proxy(this, { get(target, prop) { if (prop in target) { return target[prop]; } else { return target.scale[prop] || Reflect.get(...arguments); } } }); } get tonic() { return this.scale[0]; } get third() { return this.scale[2]; } get fifth() { return this.scale[4]; } get length() { return this.scale.length; } toString() { return `[${this.scale.join(", ")}]`; } } export function MajorScale(tonic) { console.debug(`MajorScale(${tonic})`); const intervals = [2, 2, 1, 2, 2, 2]; return new Scale(tonic, intervals); } export function MinorScale(tonic) { console.debug(`MinorScale(${tonic})`); const intervals = [2, 1, 2, 2, 1, 2]; return new Scale(tonic, intervals); }