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; } } export class Note { static natural = '♮'; static sharp = '♯'; static flat = '♭'; static range = ['A', 'B', 'C', 'D', 'E', 'F', 'G']; static get first() { return this.range.at(0); } static get last() { return this.range.at(-1); } static noSharps = ['B', 'E']; static get noFlats() { return this.noSharps.map(n => String.fromCharCode(n.charCodeAt()+1)) } static fromString(string) { const [root, accidental] = [string[0], string[1]]; switch (accidental) { case this.sharp: if (this.noSharps.includes(root)) { return new this(String.fromCharCode(root.charCodeAt()+1)) } else { return new this(root, true); } case this.flat: if (this.noFlats.includes(root)) { return new this(String.fromCharCode(root.charCodeAt()-1)); } else if (root === Note.first) { // i don't want to make a ring buffer that accepts // negative indices, ok??? return new this(Note.last, true); } else { const code = root.charCodeAt(); return new this(String.fromCharCode(code-1), true); } case undefined: case this.natural: return new this(root); default: throw new Error('wtf is that accidental', accidental); } } constructor(root, isSharp=false) { if (isSharp && Note.noSharps.includes(root)) { throw new Error(`bad note: ${root}${Note.sharp}`); } this.root = root; this.isSharp = isSharp; } toString() { return this.root + (this.isSharp ? Note.sharp : ''); } toAlternateString() { if (this.isSharp) { const next = (this.root === Note.last) ? Note.first : String.fromCharCode(this.root.charCodeAt()+1); if (Note.noSharps.includes(this.root)) { return next; } else { return next + Note.flat; } } else if (Note.noFlats.includes(this.root)) { const prev = (this.root === Note.first) ? Note.last : String.fromCharCode(this.root.charCodeAt()-1); return prev + Note.sharp; } else { return this.toString(); } } toRelativeString(other) { const alternate = this.toAlternateString(); const otherStr = other.toString(); const delta = otherStr.charCodeAt() - alternate.charCodeAt(); if (delta === -1 || delta === 1) { return alternate; } else { return this.toString(); } } } 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); }