const ringHandler = { get(target, prop) { return target[prop % target.length] || Reflect.get(...arguments); } }; export const chromaticScale = new Proxy( ['A', 'A♯', 'B', 'C', 'C♯', 'D', 'D♯', 'E', 'F', 'F♯', 'G', 'G♯'], ringHandler); function scaleFromIntervals(tonic, intervals) { const scaleIndex = chromaticScale.indexOf(tonic); if (scaleIndex < 0) { console.error('tonic not found in scale', tonic, chromaticScale); return undefined; } 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. if (note[0] === lastBase) { const nextBase = chromaticScale[scaleIndex + steps + 1][0]; scale.push(`${nextBase}♭`) lastBase = nextBase[0]; } else { scale.push(note); lastBase = note[0]; } } 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); }