UNPKG

scribbletune

Version:

Create music with JavaScript and Node.js!

917 lines (905 loc) 27.9 kB
#!/usr/bin/env node "use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/cli.ts var cli_exports = {}; __export(cli_exports, { runCli: () => runCli }); module.exports = __toCommonJS(cli_exports); var import_harmonics4 = require("harmonics"); // src/arp.ts var import_harmonics2 = require("harmonics"); // src/utils.ts var import_harmonics = require("harmonics"); var isNote = (str) => /^[a-gA-G](?:#|b)?\d$/.test(str); var expandStr = (str) => { str = JSON.stringify(str.split("")); str = str.replace(/,"\[",/g, ", ["); str = str.replace(/"\[",/g, "["); str = str.replace(/,"\]"/g, "]"); return JSON.parse(str); }; var shuffle = (arr, fullShuffle = true) => { const lastIndex = arr.length - 1; arr.forEach((el, idx) => { if (idx >= lastIndex) { return; } const rnd = fullShuffle ? ( // Pick random number from idx+1 to lastIndex (Modified algorithm, (N-1)! combinations) // Math.random -> [0, 1) -> [0, lastIndex-idx ) --floor-> [0, lastIndex-idx-1] // rnd = [0, lastIndex-idx-1] + 1 + idx = [1 + idx, lastIndex] // (Original algorithm would pick rnd = [idx, lastIndex], thus any element could arrive back into its slot) Math.floor(Math.random() * (lastIndex - idx)) + 1 + idx ) : ( // Pick random number from idx to lastIndex (Unmodified Richard Durstenfeld, N! combinations) Math.floor(Math.random() * (lastIndex + 1 - idx)) + idx ); arr[idx] = arr[rnd]; arr[rnd] = el; }); return arr; }; var pickOne = (arr) => arr[Math.floor(Math.random() * arr.length)]; var dice = () => !!Math.round(Math.random()); var errorHasMessage = (x) => { return typeof x === "object" && x !== null && "message" in x && typeof x.message === "string"; }; var convertChordToNotes = (el) => { let c1; let c2; let e1; let e2; try { c1 = (0, import_harmonics.inlineChord)(el); } catch (e) { e1 = e; } try { c2 = (0, import_harmonics.chord)(el.replace(/_/g, " ")); } catch (e) { e2 = e; } if (!e1 && !e2 && c1 && c2) { if (c1.toString() !== c2.toString()) { throw new Error(`Chord ${el} cannot decode, guessing ${c1} or ${c2}`); } return c1; } if (!e1 && c1) { return c1; } if (!e2 && c2) { return c2; } return (0, import_harmonics.chord)(el); }; var convertChordsToNotes = (el) => { if (typeof el === "string" && isNote(el)) { return [el]; } if (Array.isArray(el)) { el.forEach((n) => { if (Array.isArray(n)) { n.forEach((n1) => { if (typeof n1 !== "string" || !isNote(n1)) { throw new TypeError("array of arrays must comprise valid notes"); } }); } else if (typeof n !== "string" || !isNote(n)) { throw new TypeError("array must comprise valid notes"); } }); return el; } if (!Array.isArray(el)) { const c = convertChordToNotes(el); if (c?.length) { return c; } } throw new Error(`Chord ${el} not found`); }; var randomInt = (num = 1) => Math.round(Math.random() * num); // src/arp.ts var DEFAULT_OCTAVE = 4; var fillArr = (arr, len) => { const bumpOctave = (el) => { if (!el) { throw new Error("Empty element"); } const note = el.replace(/\d/, ""); const oct = el.replace(/\D/g, "") || DEFAULT_OCTAVE; if (!note) { throw new Error("Incorrect note"); } return note + (+oct + 1); }; const arr1 = arr.map(bumpOctave); const arr2 = arr1.map(bumpOctave); const finalArr = [...arr, ...arr1, ...arr2]; return finalArr.slice(0, len); }; var arp = (chordsOrParams) => { let finalArr = []; const params = { count: 4, order: "0123", chords: "" }; if (typeof chordsOrParams === "string") { params.chords = chordsOrParams; } else { if (chordsOrParams.order?.match(/\D/g)) { throw new TypeError("Invalid value for order"); } if (chordsOrParams.count > 8 || chordsOrParams.count < 2) { throw new TypeError("Invalid value for count"); } if (chordsOrParams.count && !chordsOrParams.order) { params.order = Array.from(Array(chordsOrParams.count).keys()).join(""); } Object.assign(params, chordsOrParams); } if (typeof params.chords === "string") { const chordsArr = params.chords.split(" "); chordsArr.forEach((c, i) => { try { const filledArr = fillArr((0, import_harmonics2.inlineChord)(c), params.count); const reorderedArr = params.order.split("").map((idx) => filledArr[Number(idx)]); finalArr = [...finalArr, ...reorderedArr]; } catch (_e) { throw new Error( `Cannot decode chord ${i + 1} "${c}" in given "${params.chords}"` ); } }); } else if (Array.isArray(params.chords)) { params.chords.forEach((c, i) => { try { const filledArr = fillArr(c, params.count); const reorderedArr = params.order.split("").map((idx) => filledArr[Number(idx)]); finalArr = [...finalArr, ...reorderedArr]; } catch (e) { throw new Error( `${errorHasMessage(e) ? e.message : e} in chord ${i + 1} "${c}"` ); } }); } else { throw new TypeError("Invalid value for chords"); } return finalArr; }; // src/clip-utils.ts var defaultParams = { notes: ["C4"], pattern: "x", shuffle: false, sizzle: false, sizzleReps: 1, arpegiate: false, subdiv: "4n", amp: 100, accentLow: 70, randomNotes: null, offlineRendering: false }; var validatePattern = (pattern) => { if (/[^x\-_[\]R]/.test(pattern)) { throw new TypeError( `pattern can only comprise x - _ [ ] R, found ${pattern}` ); } }; var preprocessClipParams = (params, extraDefaults) => { params = { ...defaultParams, ...extraDefaults, ...params || {} }; if (typeof params.notes === "string") { params.notes = params.notes.replace(/\s{2,}/g, " ").split(" "); } params.notes = params.notes ? params.notes.map(convertChordsToNotes) : []; validatePattern(params.pattern); if (params.shuffle) { params.notes = shuffle(params.notes); } if (params.randomNotes && typeof params.randomNotes === "string") { params.randomNotes = params.randomNotes.replace(/\s{2,}/g, " ").split(/\s/); } if (params.randomNotes) { params.randomNotes = params.randomNotes.map( convertChordsToNotes ); } return params; }; // src/clip.ts var hdr = { "1m": 2048, "2m": 4096, "3m": 6144, "4m": 8192, "1n": 512, "2n": 256, "4n": 128, "8n": 64, "16n": 32 }; var clip = (params) => { params = preprocessClipParams(params); const clipNotes = []; let step = 0; const recursivelyApplyPatternToNotes = (patternArr, length, parentNoteLength) => { let totalLength = 0; patternArr.forEach((char, idx) => { if (typeof char === "string") { let note = null; if (char === "-") { } else if (char === "R" && randomInt() && // Use 1/2 probability for R to pick from param.notes params.randomNotes && params.randomNotes.length > 0) { note = params.randomNotes[randomInt(params.randomNotes.length - 1)]; } else if (params.notes) { note = params.notes[step]; } if (char === "x" || char === "R") { step++; } if (char === "x" || char === "-" || char === "R") { clipNotes.push({ note, length, level: char === "R" && !params.randomNotes ? params.accentLow : params.amp }); totalLength += length; } if (char === "_" && clipNotes.length) { clipNotes[clipNotes.length - 1].length += length; totalLength += length; } if (parentNoteLength && totalLength !== parentNoteLength && idx === patternArr.length - 1) { const diff = Math.abs( parentNoteLength - totalLength ); const lastClipNote = clipNotes[clipNotes.length - 1]; if (lastClipNote.length > diff) { lastClipNote.length = lastClipNote.length - diff; } else { lastClipNote.length = lastClipNote.length + diff; } } if (step === params.notes?.length) { step = 0; } } if (Array.isArray(char)) { let isTriplet = false; if (char.length % 2 !== 0 || length % 2 !== 0) { isTriplet = true; } recursivelyApplyPatternToNotes( char, Math.round(length / char.length), isTriplet && length ); totalLength += length; } }); }; recursivelyApplyPatternToNotes( expandStr(params.pattern), hdr[params.subdiv] || hdr["4n"], false ); if (params.sizzle) { const volArr = []; const style = params.sizzle === true ? "sin" : params.sizzle; const beats = clipNotes.length; const amp = params.amp; const sizzleReps = params.sizzleReps; const stepLevel = amp / (beats / sizzleReps); if (style === "sin" || style === "cos") { for (let i = 0; i < beats; i++) { const level = Math[style](i * Math.PI / (beats / sizzleReps)) * amp; volArr.push(Math.round(Math.abs(level))); } } if (style === "rampUp") { let level = 0; for (let i = 0; i < beats; i++) { if (i % (beats / sizzleReps) === 0) { level = 0; } else { level = level + stepLevel; } volArr.push(Math.round(Math.abs(level))); } } if (style === "rampDown") { let level = amp; for (let i = 0; i < beats; i++) { if (i % (beats / sizzleReps) === 0) { level = amp; } else { level = level - stepLevel; } volArr.push(Math.round(Math.abs(level))); } } for (let i = 0; i < volArr.length; i++) { clipNotes[i].level = volArr[i] ? volArr[i] : 1; } } if (params.accent) { if (/[^x-]/.test(params.accent)) { throw new TypeError("Accent can only have x and - characters"); } let a = 0; for (const clipNote of clipNotes) { let level = params.accent[a] === "x" ? params.amp : params.accentLow; if (params.sizzle) { level = (clipNote.level + level) / 2; } clipNote.level = Math.round(level); a = a + 1; if (a === params.accent.length) { a = 0; } } } return clipNotes; }; // src/midi.ts var import_node_fs = __toESM(require("fs"), 1); var import_midi = require("@scribbletune/midi"); var midi = (notes, fileName = "music.mid", bpm) => { const file = createFileFromNotes(notes, bpm); const bytes = file.toBytes(); if (fileName === null) { return bytes; } if (!fileName.endsWith(".mid")) { fileName = `${fileName}.mid`; } if (typeof window !== "undefined" && window.URL && typeof window.URL.createObjectURL === "function") { return createDownloadLink(bytes, fileName); } import_node_fs.default.writeFileSync(fileName, bytes, "binary"); console.log(`MIDI file generated: ${fileName}.`); }; var createDownloadLink = (b, fileName) => { const bytes = new Uint8Array(b.length); for (let i = 0; i < b.length; i++) { const ascii = b.charCodeAt(i); bytes[i] = ascii; } const blob = new Blob([bytes], { type: "audio/midi" }); const link = document.createElement("a"); link.href = typeof window !== "undefined" && typeof window.URL !== "undefined" && typeof window.URL.createObjectURL !== "undefined" && window.URL.createObjectURL(blob) || ""; link.download = fileName; link.innerText = "Download MIDI file"; return link; }; var createFileFromNotes = (notes, bpm) => { const file = new import_midi.File(); const track = new import_midi.Track(); if (typeof bpm === "number") { track.setTempo(bpm); } file.addTrack(track); for (const noteObj of notes) { const level = noteObj.level || 127; if (noteObj.note) { if (typeof noteObj.note === "string") { track.noteOn(0, noteObj.note, noteObj.length, level); track.noteOff(0, noteObj.note, noteObj.length, level); } else { track.addChord(0, noteObj.note, noteObj.length, level); } } else { track.noteOff(0, "", noteObj.length); } } return file; }; // src/progression.ts var import_harmonics3 = require("harmonics"); var getChordDegrees = (mode) => { const theRomans = { ionian: ["I", "ii", "iii", "IV", "V", "vi", "vii\xB0"], dorian: ["i", "ii", "III", "IV", "v", "vi\xB0", "VII"], phrygian: ["i", "II", "III", "iv", "v\xB0", "VI", "vii"], lydian: ["I", "II", "iii", "iv\xB0", "V", "vi", "vii"], mixolydian: ["I", "ii", "iii\xB0", "IV", "v", "vi", "VII"], aeolian: ["i", "ii\xB0", "III", "iv", "v", "VI", "VII"], locrian: ["i\xB0", "II", "iii", "iv", "V", "VI", "vii"], "melodic minor": ["i", "ii", "III+", "IV", "V", "vi\xB0", "vii\xB0"], "harmonic minor": ["i", "ii\xB0", "III+", "iv", "V", "VI", "vii\xB0"] }; theRomans.major = theRomans.ionian; theRomans.minor = theRomans.aeolian; return theRomans[mode] || []; }; var idxByDegree = { i: 0, ii: 1, iii: 2, iv: 3, v: 4, vi: 5, vii: 6 }; var getChordName = (roman) => { const str = roman.replace(/\W/g, ""); let prefix = "M"; if (str.toLowerCase() === str) { prefix = "m"; } if (roman.indexOf("\xB0") > -1) { return `${prefix}7b5`; } if (roman.indexOf("+") > -1) { return `${prefix}#5`; } if (roman.indexOf("7") > -1) { return prefix === "M" ? "maj7" : "m7"; } return prefix; }; var getChordsByProgression = (noteOctaveScale, chordDegress) => { const noteOctaveScaleArr = noteOctaveScale.split(" "); if (!noteOctaveScaleArr[0].match(/\d/)) { noteOctaveScaleArr[0] += "4"; noteOctaveScale = noteOctaveScaleArr.join(" "); } const mode = (0, import_harmonics3.scale)(noteOctaveScale); const chordDegreesArr = chordDegress.replace(/\s*,+\s*/g, " ").split(" "); const chordFamily = chordDegreesArr.map((roman) => { const chordName = getChordName(roman); const scaleId = idxByDegree[roman.replace(/\W|\d/g, "").toLowerCase()]; const note = mode[scaleId]; const oct = note.replace(/\D+/, ""); return `${note.replace(/\d/, "") + chordName}_${oct}`; }); return chordFamily.toString().replace(/,/g, " "); }; var getProgFactory = ({ T, P, D }) => { return (count = 4) => { const chords = []; chords.push(pickOne(T)); let i = 1; if (i < count - 1) { chords.push(pickOne(P)); i++; } if (i < count - 1 && dice()) { chords.push(pickOne(P)); i++; } if (i < count - 1) { chords.push(pickOne(D)); i++; } if (i < count - 1) { chords.push(pickOne(P)); i++; } if (i < count - 1) { chords.push(pickOne(D)); i++; } if (i < count - 1 && dice()) { chords.push(pickOne(P)); i++; } while (i < count) { chords.push(pickOne(D)); i++; } return chords; }; }; var M = getProgFactory({ T: ["I", "vi"], P: ["ii", "IV"], D: ["V"] }); var m = getProgFactory({ T: ["i", "VI"], P: ["ii", "iv"], D: ["V"] }); var progression = (scaleType, count = 4) => { if (scaleType === "major" || scaleType === "M") { return M(count); } if (scaleType === "minor" || scaleType === "m") { return m(count); } return []; }; // src/cli.ts var HELP_TEXT = `Usage: scribbletune --riff <root> <mode> <pattern> <subdiv> scribbletune --chord <root> <mode> <pattern> <subdiv> <progression|random> scribbletune --arp <root> <mode> <pattern> <subdiv> <progression|random> Examples: scribbletune --riff C3 phrygian x-xRx_RR 8n --style AABC --sizzle sin 2 --outfile riff.mid scribbletune --chord C3 major xxxx 1m 1645 --sizzle cos 1 --outfile chord.mid scribbletune --chord C3 major xxxx 1m CM-FM-Am-GM scribbletune --chord C3 major xxxx 1m random scribbletune --arp C3 major xxxx 1m 1736 --sizzle cos 4 Options: --outfile <name> Output MIDI filename (default: music.mid) --bpm <number> Tempo in BPM --subdiv <value> Note subdivision (e.g. 4n, 1m) --sizzle [style] [n] Sizzle style: sin|cos|rampUp|rampDown and optional reps --sizzle-reps <n> Repetitions for sizzle --amp <0-127> Maximum note level --accent <pattern> Accent pattern using x and - --accent-low <0-127> Accent low level --count <2-8> Arp note count (arp command only) --order <digits> Arp order string (arp command only) --style <letters> Riff motif/style pattern (e.g. AABC) --fit-pattern Repeat pattern until it can consume all generated notes (default) --no-fit-pattern Disable automatic pattern fitting -h, --help Show this help `; var romanByDigit = (progDigits, mode) => { const modeForDegrees = mode.toLowerCase(); const chordDegrees = getChordDegrees(modeForDegrees); if (!chordDegrees.length) { throw new TypeError(`Unsupported mode "${mode}" for progression digits`); } const romans = progDigits.split("").map((digit) => { const idx = Number(digit) - 1; if (idx < 0 || idx >= chordDegrees.length) { throw new TypeError( `Invalid progression digit "${digit}" in "${progDigits}"` ); } return chordDegrees[idx]; }); return { chordDegrees: romans.join(" "), raw: progDigits }; }; var parseProgression = (root, mode, progressionInput) => { if (progressionInput === "random") { const modeType = mode === "minor" || mode === "m" ? "minor" : "major"; const randomProg = progression(modeType, 4).join(" "); return getChordsByProgression(`${root} ${mode}`, randomProg); } if (/^[1-7]+$/.test(progressionInput)) { const converted = romanByDigit(progressionInput, mode); return getChordsByProgression(`${root} ${mode}`, converted.chordDegrees); } if (/^[ivIV°+7\s,]+$/.test(progressionInput)) { const normalized = progressionInput.replace(/\s*,+\s*/g, " "); return getChordsByProgression(`${root} ${mode}`, normalized); } return progressionInput.replace(/-/g, " "); }; var normalizeArpOrder = (order) => { if (!/^\d+$/.test(order)) { throw new TypeError("Invalid value for --order"); } if (order.includes("0")) { return order; } return order.split("").map((ch) => { const n = Number(ch); if (n < 1) { throw new TypeError("Invalid value for --order"); } return String(n - 1); }).join(""); }; var expandPatternSyntax = (pattern) => { const jsRepeatQuoted = pattern.match(/^(['"])(.+)\1\.repeat\((\d+)\)$/); if (jsRepeatQuoted) { return jsRepeatQuoted[2].repeat(Number(jsRepeatQuoted[3])); } const jsRepeatUnquoted = pattern.match(/^(.+)\.repeat\((\d+)\)$/); if (jsRepeatUnquoted) { return jsRepeatUnquoted[1].repeat(Number(jsRepeatUnquoted[2])); } const prefixRepeat = pattern.match(/^(\d+)\((.+)\)$/); if (prefixRepeat) { return prefixRepeat[2].repeat(Number(prefixRepeat[1])); } const suffixRepeat = pattern.match(/^\((.+)\)(\d+)$/); if (suffixRepeat) { return suffixRepeat[1].repeat(Number(suffixRepeat[2])); } return pattern; }; var countPatternSteps = (pattern) => (pattern.match(/[xR]/g) || []).length; var fitPatternToNoteCount = (pattern, noteCount) => { const stepCount = countPatternSteps(pattern); if (!stepCount || stepCount >= noteCount) { return pattern; } const reps = Math.ceil(noteCount / stepCount); return pattern.repeat(reps); }; var resolvePattern = (rawPattern, noteCount, fitPattern = false) => { const expanded = expandPatternSyntax(rawPattern); return fitPattern ? fitPatternToNoteCount(expanded, noteCount) : expanded; }; var parseCliArgs = (argv) => { if (argv.length === 0 || argv.includes("-h") || argv.includes("--help")) { return null; } let commandArg = argv[0]; if (commandArg.startsWith("--")) { commandArg = commandArg.slice(2); } if (commandArg !== "riff" && commandArg !== "chord" && commandArg !== "arp") { throw new TypeError( `First argument must be riff/chord/arp (or --riff/--chord/--arp), received "${argv[0]}"` ); } const positionals = []; const options = { command: commandArg, positionals, outfile: "music.mid", fitPattern: true }; let i = 1; while (i < argv.length) { const token = argv[i]; if (!token.startsWith("--")) { positionals.push(token); i++; continue; } if (token === "--outfile") { options.outfile = argv[i + 1]; i += 2; continue; } if (token === "--bpm") { options.bpm = Number(argv[i + 1]); i += 2; continue; } if (token === "--subdiv") { options.subdiv = argv[i + 1]; i += 2; continue; } if (token === "--sizzle") { const styleOrNum = argv[i + 1]; const maybeNum = argv[i + 2]; if (!styleOrNum || styleOrNum.startsWith("--")) { options.sizzle = true; i += 1; continue; } if (/^\d+$/.test(styleOrNum)) { options.sizzle = true; options.sizzleReps = Number(styleOrNum); i += 2; continue; } options.sizzle = styleOrNum; if (maybeNum && /^\d+$/.test(maybeNum)) { options.sizzleReps = Number(maybeNum); i += 3; } else { i += 2; } continue; } if (token === "--sizzle-reps") { options.sizzleReps = Number(argv[i + 1]); i += 2; continue; } if (token === "--amp") { options.amp = Number(argv[i + 1]); i += 2; continue; } if (token === "--accent") { options.accent = argv[i + 1]; i += 2; continue; } if (token === "--accent-low") { options.accentLow = Number(argv[i + 1]); i += 2; continue; } if (token === "--count") { options.count = Number(argv[i + 1]); i += 2; continue; } if (token === "--order") { options.order = argv[i + 1]; i += 2; continue; } if (token === "--style") { options.style = argv[i + 1]; i += 2; continue; } if (token === "--fit-pattern") { options.fitPattern = true; i += 1; continue; } if (token === "--no-fit-pattern") { options.fitPattern = false; i += 1; continue; } throw new TypeError(`Unknown option "${token}"`); } return options; }; var baseClipParams = (parsed) => { const raw = { sizzle: parsed.sizzle, sizzleReps: parsed.sizzleReps, amp: parsed.amp, accent: parsed.accent, accentLow: parsed.accentLow }; return Object.fromEntries( Object.entries(raw).filter(([, v]) => v !== void 0) ); }; var makeRiff = (parsed) => { const [root, mode, pattern, subdiv] = parsed.positionals; const style = parsed.style; if (!root || !mode || !pattern || !subdiv) { throw new TypeError("riff requires: <root> <mode> <pattern> <subdiv>"); } const riffScale = (0, import_harmonics4.scale)(`${root} ${mode}`); if (style?.length) { const sectionPattern = expandPatternSyntax(pattern); const letters = style.toUpperCase().split(""); const sectionCache = {}; const combined = []; const clipParams = { ...baseClipParams(parsed), subdiv: parsed.subdiv || subdiv }; for (const letter of letters) { if (!sectionCache[letter]) { const idx = letter.charCodeAt(0) - 65; const note = riffScale[idx >= 0 ? idx % riffScale.length : 0]; sectionCache[letter] = clip({ notes: [note], randomNotes: riffScale, pattern: sectionPattern, ...clipParams }); } combined.push( ...sectionCache[letter].map((event) => ({ ...event, note: event.note ? [...event.note] : null })) ); } return combined; } const riffNotes = riffScale; const resolvedPattern = resolvePattern( pattern, riffNotes.length, parsed.fitPattern ); return clip({ notes: riffNotes, randomNotes: riffScale, pattern: resolvedPattern, ...baseClipParams(parsed), subdiv: parsed.subdiv || subdiv }); }; var makeChord = (parsed) => { const [root, mode, pattern, subdiv, progressionInput] = parsed.positionals; if (!root || !mode || !progressionInput || !pattern || !subdiv) { throw new TypeError( "chord requires: <root> <mode> <pattern> <subdiv> <progression|random>" ); } const chords = parseProgression(root, mode, progressionInput); const chordCount = chords.trim().split(/\s+/).length; const resolvedPattern = resolvePattern( pattern, chordCount, parsed.fitPattern ); return clip({ notes: chords, pattern: resolvedPattern, ...baseClipParams(parsed), subdiv: parsed.subdiv || subdiv }); }; var makeArp = (parsed) => { const [root, mode, pattern, subdiv, progressionInput] = parsed.positionals; if (!root || !mode || !progressionInput || !pattern || !subdiv) { throw new TypeError( "arp requires: <root> <mode> <pattern> <subdiv> <progression|random>" ); } const chords = parseProgression(root, mode, progressionInput); const arpNotes = arp({ chords, count: parsed.count || 4, order: parsed.order ? normalizeArpOrder(parsed.order) : "0123" }); const resolvedPattern = resolvePattern( pattern, arpNotes.length, parsed.fitPattern ); return clip({ notes: arpNotes, pattern: resolvedPattern, ...baseClipParams(parsed), subdiv: parsed.subdiv || subdiv }); }; var runCli = (argv, deps) => { const stdout = deps?.stdout || console.log; const stderr = deps?.stderr || console.error; const writeMidi = deps?.writeMidi || ((notes, fileName, bpm) => { midi(notes, fileName, bpm); }); try { const parsed = parseCliArgs(argv); if (!parsed) { stdout(HELP_TEXT); return 0; } let notes = []; if (parsed.command === "riff") { notes = makeRiff(parsed); } else if (parsed.command === "chord") { notes = makeChord(parsed); } else { notes = makeArp(parsed); } writeMidi(notes, parsed.outfile, parsed.bpm); stdout( `Generated ${parsed.command} clip (${notes.length} events) -> ${parsed.outfile}` ); return 0; } catch (e) { stderr(e instanceof Error ? e.message : String(e)); stderr("Run with --help for usage"); return 1; } }; if (process.argv[1]?.includes("cli")) { process.exit(runCli(process.argv.slice(2))); } // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { runCli });