'use strict'; const { HID } = require('./hid'); const NOTE_NAMES = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B']; const NOTE_ALIASES = { C: 0, 'B#': 0, 'C#': 1, DB: 1, D: 2, 'D#': 3, EB: 3, E: 4, FB: 4, F: 5, 'E#': 5, 'F#': 6, GB: 6, G: 7, 'G#': 8, AB: 8, A: 9, 'A#': 10, BB: 10, B: 11, CB: 11, }; /** Interval sets, matching Live 12's `scale_intervals`. */ const SCALES = { major: [0, 2, 4, 5, 7, 9, 11], minor: [0, 2, 3, 5, 7, 8, 10], 'harmonic-minor': [0, 2, 3, 5, 7, 8, 11], 'melodic-minor': [0, 2, 3, 5, 7, 9, 11], dorian: [0, 2, 3, 5, 7, 9, 10], phrygian: [0, 1, 3, 5, 7, 8, 10], lydian: [0, 2, 4, 6, 7, 9, 11], mixolydian: [0, 2, 4, 5, 7, 9, 10], locrian: [0, 1, 3, 5, 6, 8, 10], 'major-pentatonic': [0, 2, 4, 7, 9], 'minor-pentatonic': [0, 3, 5, 7, 10], blues: [0, 3, 5, 6, 7, 10], 'whole-tone': [0, 2, 4, 6, 8, 10], chromatic: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11], }; /** * Ableton's computer MIDI keyboard, one octave laid out like a piano: * white keys on the home row, black keys on the row above, plus the octave C. */ const NOTE_KEYS = [ { key: 'a', hid: HID.a, pitchClass: 0, frameKey: 'note-a' }, { key: 'w', hid: HID.w, pitchClass: 1, frameKey: 'note-w' }, { key: 's', hid: HID.s, pitchClass: 2, frameKey: 'note-s' }, { key: 'e', hid: HID.e, pitchClass: 3, frameKey: 'note-e' }, { key: 'd', hid: HID.d, pitchClass: 4, frameKey: 'note-d' }, { key: 'f', hid: HID.f, pitchClass: 5, frameKey: 'note-f' }, { key: 't', hid: HID.t, pitchClass: 6, frameKey: 'note-t' }, { key: 'g', hid: HID.g, pitchClass: 7, frameKey: 'note-g' }, { key: 'y', hid: HID.y, pitchClass: 8, frameKey: 'note-y' }, { key: 'h', hid: HID.h, pitchClass: 9, frameKey: 'note-h' }, { key: 'u', hid: HID.u, pitchClass: 10, frameKey: 'note-u' }, { key: 'j', hid: HID.j, pitchClass: 11, frameKey: 'note-j' }, // The octave key: same pitch class as A, one octave up. { key: 'k', hid: HID.k, pitchClass: 0, frameKey: 'note-k', octaveUp: true }, ]; const NOTE_KEY_HIDS = NOTE_KEYS.map((k) => k.hid); /** "C", "f#", "Bb", "3" -> 0..11. Throws on garbage. */ function parseRoot(input) { if (typeof input === 'number') { if (!Number.isInteger(input) || input < 0 || input > 11) { throw new Error(`root must be an integer 0-11, got ${input}`); } return input; } const raw = String(input).trim(); if (/^\d+$/.test(raw)) return parseRoot(Number(raw)); const normalized = raw.toUpperCase().replace(/♯/g, '#').replace(/♭/g, 'B'); if (normalized in NOTE_ALIASES) return NOTE_ALIASES[normalized]; throw new Error(`unknown root note: ${input}`); } /** "major", "Harmonic Minor", "minor_pentatonic" -> interval array. */ function parseScale(input) { const normalized = String(input).trim().toLowerCase().replace(/[\s_]+/g, '-'); if (normalized in SCALES) return SCALES[normalized]; throw new Error( `unknown scale: ${input} (known: ${Object.keys(SCALES).join(', ')})` ); } /** "0,2,4,5,7,9,11" or [0,2,...] -> normalized, deduped, sorted interval array. */ function parseIntervals(input) { const list = Array.isArray(input) ? input : String(input).split(/[,\s]+/); const parsed = list .filter((v) => String(v).length > 0) .map((v) => { const n = Number(v); if (!Number.isInteger(n)) throw new Error(`interval is not an integer: ${v}`); return n; }); if (parsed.length === 0) throw new Error('interval list is empty'); return parsed; } /** * The core of the whole project: * lit = { (root + interval) % 12 } * Returns a sorted array of pitch classes. */ function pitchClassesFor(root, intervals) { const set = new Set(intervals.map((i) => (((root + i) % 12) + 12) % 12)); return [...set].sort((a, b) => a - b); } /** Pitch classes -> the QWERTY keys Ableton maps them to (includes 'k'). */ function keysFor(pitchClasses) { const set = new Set(pitchClasses); return NOTE_KEYS.filter((k) => set.has(k.pitchClass)).map((k) => k.key); } function noteName(pitchClass) { return NOTE_NAMES[(((pitchClass % 12) + 12) % 12)]; } module.exports = { NOTE_NAMES, NOTE_ALIASES, SCALES, NOTE_KEYS, NOTE_KEY_HIDS, parseRoot, parseScale, parseIntervals, pitchClassesFor, keysFor, noteName, };