scribbletune
Version:
Create music with JavaScript and Node.js!
917 lines (905 loc) • 27.9 kB
JavaScript
;
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
});