scribbletune 5.2.0 → 5.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +276 -22
  2. package/dist/browser.cjs +1183 -0
  3. package/dist/browser.cjs.map +1 -0
  4. package/dist/browser.js +1135 -1
  5. package/dist/browser.js.map +1 -1
  6. package/dist/cli.cjs +813 -0
  7. package/dist/{index.mjs → index.cjs} +225 -169
  8. package/dist/index.cjs.map +1 -0
  9. package/dist/index.d.cts +323 -0
  10. package/dist/index.d.ts +303 -350
  11. package/dist/index.js +524 -1
  12. package/dist/index.js.map +1 -1
  13. package/dist/scribbletune.global.js +1944 -0
  14. package/dist/scribbletune.global.js.map +1 -0
  15. package/package.json +32 -40
  16. package/dist/lib/arp.d.ts +0 -10
  17. package/dist/lib/browser-clip.d.ts +0 -14
  18. package/dist/lib/browser-index.d.ts +0 -7
  19. package/dist/lib/channel.d.ts +0 -61
  20. package/dist/lib/clip.d.ts +0 -2
  21. package/dist/lib/index.d.ts +0 -7
  22. package/dist/lib/midi.d.ts +0 -11
  23. package/dist/lib/progression.d.ts +0 -25
  24. package/dist/lib/session.d.ts +0 -14
  25. package/dist/lib/types/arp-params.d.ts +0 -6
  26. package/dist/lib/types/channel-params.d.ts +0 -92
  27. package/dist/lib/types/channel-pattern.d.ts +0 -24
  28. package/dist/lib/types/clip-params.d.ts +0 -104
  29. package/dist/lib/types/event-fn.d.ts +0 -7
  30. package/dist/lib/types/index.d.ts +0 -14
  31. package/dist/lib/types/note-object.d.ts +0 -6
  32. package/dist/lib/types/nvp.d.ts +0 -4
  33. package/dist/lib/types/play-params.d.ts +0 -15
  34. package/dist/lib/types/player-observer-fn.d.ts +0 -6
  35. package/dist/lib/types/progression-scale.d.ts +0 -2
  36. package/dist/lib/types/seq-fn.d.ts +0 -2
  37. package/dist/lib/types/sizzle-style.d.ts +0 -2
  38. package/dist/lib/types/synth-params.d.ts +0 -20
  39. package/dist/lib/types/tpd.d.ts +0 -15
  40. package/dist/lib/utils.d.ts +0 -56
  41. package/dist/scribbletune.js +0 -2
  42. package/dist/scribbletune.js.map +0 -1
package/dist/cli.cjs ADDED
@@ -0,0 +1,813 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __export = (target, all) => {
10
+ for (var name in all)
11
+ __defProp(target, name, { get: all[name], enumerable: true });
12
+ };
13
+ var __copyProps = (to, from, except, desc) => {
14
+ if (from && typeof from === "object" || typeof from === "function") {
15
+ for (let key of __getOwnPropNames(from))
16
+ if (!__hasOwnProp.call(to, key) && key !== except)
17
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
18
+ }
19
+ return to;
20
+ };
21
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
22
+ // If the importer is in node compatibility mode or this is not an ESM
23
+ // file that has been converted to a CommonJS file using a Babel-
24
+ // compatible transform (i.e. "__esModule" has not been set), then set
25
+ // "default" to the CommonJS "module.exports" for node compatibility.
26
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
27
+ mod
28
+ ));
29
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
30
+
31
+ // src/cli.ts
32
+ var cli_exports = {};
33
+ __export(cli_exports, {
34
+ runCli: () => runCli
35
+ });
36
+ module.exports = __toCommonJS(cli_exports);
37
+ var import_harmonics4 = require("harmonics");
38
+
39
+ // src/arp.ts
40
+ var import_harmonics2 = require("harmonics");
41
+
42
+ // src/utils.ts
43
+ var import_harmonics = require("harmonics");
44
+ var isNote = (str) => /^[a-gA-G](?:#|b)?\d$/.test(str);
45
+ var expandStr = (str) => {
46
+ str = JSON.stringify(str.split(""));
47
+ str = str.replace(/,"\[",/g, ", [");
48
+ str = str.replace(/"\[",/g, "[");
49
+ str = str.replace(/,"\]"/g, "]");
50
+ return JSON.parse(str);
51
+ };
52
+ var shuffle = (arr, fullShuffle = true) => {
53
+ const lastIndex = arr.length - 1;
54
+ arr.forEach((el, idx) => {
55
+ if (idx >= lastIndex) {
56
+ return;
57
+ }
58
+ const rnd = fullShuffle ? (
59
+ // Pick random number from idx+1 to lastIndex (Modified algorithm, (N-1)! combinations)
60
+ // Math.random -> [0, 1) -> [0, lastIndex-idx ) --floor-> [0, lastIndex-idx-1]
61
+ // rnd = [0, lastIndex-idx-1] + 1 + idx = [1 + idx, lastIndex]
62
+ // (Original algorithm would pick rnd = [idx, lastIndex], thus any element could arrive back into its slot)
63
+ Math.floor(Math.random() * (lastIndex - idx)) + 1 + idx
64
+ ) : (
65
+ // Pick random number from idx to lastIndex (Unmodified Richard Durstenfeld, N! combinations)
66
+ Math.floor(Math.random() * (lastIndex + 1 - idx)) + idx
67
+ );
68
+ arr[idx] = arr[rnd];
69
+ arr[rnd] = el;
70
+ });
71
+ return arr;
72
+ };
73
+ var pickOne = (arr) => arr[Math.floor(Math.random() * arr.length)];
74
+ var dice = () => !!Math.round(Math.random());
75
+ var errorHasMessage = (x) => {
76
+ return typeof x === "object" && x !== null && "message" in x && typeof x.message === "string";
77
+ };
78
+ var convertChordToNotes = (el) => {
79
+ let c1;
80
+ let c2;
81
+ let e1;
82
+ let e2;
83
+ try {
84
+ c1 = (0, import_harmonics.inlineChord)(el);
85
+ } catch (e) {
86
+ e1 = e;
87
+ }
88
+ try {
89
+ c2 = (0, import_harmonics.chord)(el.replace(/_/g, " "));
90
+ } catch (e) {
91
+ e2 = e;
92
+ }
93
+ if (!e1 && !e2) {
94
+ if (c1.toString() !== c2.toString()) {
95
+ throw new Error(`Chord ${el} cannot decode, guessing ${c1} or ${c2}`);
96
+ }
97
+ return c1;
98
+ }
99
+ if (!e1) {
100
+ return c1;
101
+ }
102
+ if (!e2) {
103
+ return c2;
104
+ }
105
+ return (0, import_harmonics.chord)(el);
106
+ };
107
+ var convertChordsToNotes = (el) => {
108
+ if (typeof el === "string" && isNote(el)) {
109
+ return [el];
110
+ }
111
+ if (Array.isArray(el)) {
112
+ el.forEach((n) => {
113
+ if (Array.isArray(n)) {
114
+ n.forEach((n1) => {
115
+ if (typeof n1 !== "string" || !isNote(n1)) {
116
+ throw new TypeError("array of arrays must comprise valid notes");
117
+ }
118
+ });
119
+ } else if (typeof n !== "string" || !isNote(n)) {
120
+ throw new TypeError("array must comprise valid notes");
121
+ }
122
+ });
123
+ return el;
124
+ }
125
+ if (!Array.isArray(el)) {
126
+ const c = convertChordToNotes(el);
127
+ if (c?.length) {
128
+ return c;
129
+ }
130
+ }
131
+ throw new Error(`Chord ${el} not found`);
132
+ };
133
+ var randomInt = (num = 1) => Math.round(Math.random() * num);
134
+
135
+ // src/arp.ts
136
+ var DEFAULT_OCTAVE = 4;
137
+ var fillArr = (arr, len) => {
138
+ const bumpOctave = (el) => {
139
+ if (!el) {
140
+ throw new Error("Empty element");
141
+ }
142
+ const note = el.replace(/\d/, "");
143
+ const oct = el.replace(/\D/g, "") || DEFAULT_OCTAVE;
144
+ if (!note) {
145
+ throw new Error("Incorrect note");
146
+ }
147
+ return note + (+oct + 1);
148
+ };
149
+ const arr1 = arr.map(bumpOctave);
150
+ const arr2 = arr1.map(bumpOctave);
151
+ const finalArr = [...arr, ...arr1, ...arr2];
152
+ return finalArr.slice(0, len);
153
+ };
154
+ var arp = (chordsOrParams) => {
155
+ let finalArr = [];
156
+ const params = {
157
+ count: 4,
158
+ order: "0123",
159
+ chords: ""
160
+ };
161
+ if (typeof chordsOrParams === "string") {
162
+ params.chords = chordsOrParams;
163
+ } else {
164
+ if (chordsOrParams.order?.match(/\D/g)) {
165
+ throw new TypeError("Invalid value for order");
166
+ }
167
+ if (chordsOrParams.count > 8 || chordsOrParams.count < 2) {
168
+ throw new TypeError("Invalid value for count");
169
+ }
170
+ if (chordsOrParams.count && !chordsOrParams.order) {
171
+ params.order = Array.from(Array(chordsOrParams.count).keys()).join("");
172
+ }
173
+ Object.assign(params, chordsOrParams);
174
+ }
175
+ if (typeof params.chords === "string") {
176
+ const chordsArr = params.chords.split(" ");
177
+ chordsArr.forEach((c, i) => {
178
+ try {
179
+ const filledArr = fillArr((0, import_harmonics2.inlineChord)(c), params.count);
180
+ const reorderedArr = params.order.split("").map((idx) => filledArr[Number(idx)]);
181
+ finalArr = [...finalArr, ...reorderedArr];
182
+ } catch (_e) {
183
+ throw new Error(
184
+ `Cannot decode chord ${i + 1} "${c}" in given "${params.chords}"`
185
+ );
186
+ }
187
+ });
188
+ } else if (Array.isArray(params.chords)) {
189
+ params.chords.forEach((c, i) => {
190
+ try {
191
+ const filledArr = fillArr(c, params.count);
192
+ const reorderedArr = params.order.split("").map((idx) => filledArr[Number(idx)]);
193
+ finalArr = [...finalArr, ...reorderedArr];
194
+ } catch (e) {
195
+ throw new Error(
196
+ `${errorHasMessage(e) ? e.message : e} in chord ${i + 1} "${c}"`
197
+ );
198
+ }
199
+ });
200
+ } else {
201
+ throw new TypeError("Invalid value for chords");
202
+ }
203
+ return finalArr;
204
+ };
205
+
206
+ // src/clip-utils.ts
207
+ var defaultParams = {
208
+ notes: ["C4"],
209
+ pattern: "x",
210
+ shuffle: false,
211
+ sizzle: false,
212
+ sizzleReps: 1,
213
+ arpegiate: false,
214
+ subdiv: "4n",
215
+ amp: 100,
216
+ accentLow: 70,
217
+ randomNotes: null,
218
+ offlineRendering: false
219
+ };
220
+ var validatePattern = (pattern) => {
221
+ if (/[^x\-_[\]R]/.test(pattern)) {
222
+ throw new TypeError(
223
+ `pattern can only comprise x - _ [ ] R, found ${pattern}`
224
+ );
225
+ }
226
+ };
227
+ var preprocessClipParams = (params, extraDefaults) => {
228
+ params = { ...defaultParams, ...extraDefaults, ...params || {} };
229
+ if (typeof params.notes === "string") {
230
+ params.notes = params.notes.replace(/\s{2,}/g, " ").split(" ");
231
+ }
232
+ params.notes = params.notes ? params.notes.map(convertChordsToNotes) : [];
233
+ validatePattern(params.pattern);
234
+ if (params.shuffle) {
235
+ params.notes = shuffle(params.notes);
236
+ }
237
+ if (params.randomNotes && typeof params.randomNotes === "string") {
238
+ params.randomNotes = params.randomNotes.replace(/\s{2,}/g, " ").split(/\s/);
239
+ }
240
+ if (params.randomNotes) {
241
+ params.randomNotes = params.randomNotes.map(
242
+ convertChordsToNotes
243
+ );
244
+ }
245
+ return params;
246
+ };
247
+
248
+ // src/clip.ts
249
+ var hdr = {
250
+ "1m": 2048,
251
+ "2m": 4096,
252
+ "3m": 6144,
253
+ "4m": 8192,
254
+ "1n": 512,
255
+ "2n": 256,
256
+ "4n": 128,
257
+ "8n": 64,
258
+ "16n": 32
259
+ };
260
+ var clip = (params) => {
261
+ params = preprocessClipParams(params);
262
+ const clipNotes = [];
263
+ let step = 0;
264
+ const recursivelyApplyPatternToNotes = (patternArr, length, parentNoteLength) => {
265
+ let totalLength = 0;
266
+ patternArr.forEach((char, idx) => {
267
+ if (typeof char === "string") {
268
+ let note = null;
269
+ if (char === "-") {
270
+ } else if (char === "R" && randomInt() && // Use 1/2 probability for R to pick from param.notes
271
+ params.randomNotes && params.randomNotes.length > 0) {
272
+ note = params.randomNotes[randomInt(params.randomNotes.length - 1)];
273
+ } else if (params.notes) {
274
+ note = params.notes[step];
275
+ }
276
+ if (char === "x" || char === "R") {
277
+ step++;
278
+ }
279
+ if (char === "x" || char === "-" || char === "R") {
280
+ clipNotes.push({
281
+ note,
282
+ length,
283
+ level: char === "R" && !params.randomNotes ? params.accentLow : params.amp
284
+ });
285
+ totalLength += length;
286
+ }
287
+ if (char === "_" && clipNotes.length) {
288
+ clipNotes[clipNotes.length - 1].length += length;
289
+ totalLength += length;
290
+ }
291
+ if (parentNoteLength && totalLength !== parentNoteLength && idx === patternArr.length - 1) {
292
+ const diff = Math.abs(
293
+ parentNoteLength - totalLength
294
+ );
295
+ const lastClipNote = clipNotes[clipNotes.length - 1];
296
+ if (lastClipNote.length > diff) {
297
+ lastClipNote.length = lastClipNote.length - diff;
298
+ } else {
299
+ lastClipNote.length = lastClipNote.length + diff;
300
+ }
301
+ }
302
+ if (step === params.notes?.length) {
303
+ step = 0;
304
+ }
305
+ }
306
+ if (Array.isArray(char)) {
307
+ let isTriplet = false;
308
+ if (char.length % 2 !== 0 || length % 2 !== 0) {
309
+ isTriplet = true;
310
+ }
311
+ recursivelyApplyPatternToNotes(
312
+ char,
313
+ Math.round(length / char.length),
314
+ isTriplet && length
315
+ );
316
+ totalLength += length;
317
+ }
318
+ });
319
+ };
320
+ recursivelyApplyPatternToNotes(
321
+ expandStr(params.pattern),
322
+ hdr[params.subdiv] || hdr["4n"],
323
+ false
324
+ );
325
+ if (params.sizzle) {
326
+ const volArr = [];
327
+ const style = params.sizzle === true ? "sin" : params.sizzle;
328
+ const beats = clipNotes.length;
329
+ const amp = params.amp;
330
+ const sizzleReps = params.sizzleReps;
331
+ const stepLevel = amp / (beats / sizzleReps);
332
+ if (style === "sin" || style === "cos") {
333
+ for (let i = 0; i < beats; i++) {
334
+ const level = Math[style](i * Math.PI / (beats / sizzleReps)) * amp;
335
+ volArr.push(Math.round(Math.abs(level)));
336
+ }
337
+ }
338
+ if (style === "rampUp") {
339
+ let level = 0;
340
+ for (let i = 0; i < beats; i++) {
341
+ if (i % (beats / sizzleReps) === 0) {
342
+ level = 0;
343
+ } else {
344
+ level = level + stepLevel;
345
+ }
346
+ volArr.push(Math.round(Math.abs(level)));
347
+ }
348
+ }
349
+ if (style === "rampDown") {
350
+ let level = amp;
351
+ for (let i = 0; i < beats; i++) {
352
+ if (i % (beats / sizzleReps) === 0) {
353
+ level = amp;
354
+ } else {
355
+ level = level - stepLevel;
356
+ }
357
+ volArr.push(Math.round(Math.abs(level)));
358
+ }
359
+ }
360
+ for (let i = 0; i < volArr.length; i++) {
361
+ clipNotes[i].level = volArr[i] ? volArr[i] : 1;
362
+ }
363
+ }
364
+ if (params.accent) {
365
+ if (/[^x-]/.test(params.accent)) {
366
+ throw new TypeError("Accent can only have x and - characters");
367
+ }
368
+ let a = 0;
369
+ for (const clipNote of clipNotes) {
370
+ let level = params.accent[a] === "x" ? params.amp : params.accentLow;
371
+ if (params.sizzle) {
372
+ level = (clipNote.level + level) / 2;
373
+ }
374
+ clipNote.level = Math.round(level);
375
+ a = a + 1;
376
+ if (a === params.accent.length) {
377
+ a = 0;
378
+ }
379
+ }
380
+ }
381
+ return clipNotes;
382
+ };
383
+
384
+ // src/midi.ts
385
+ var import_node_fs = __toESM(require("fs"), 1);
386
+ var import_midi = require("@scribbletune/midi");
387
+ var midi = (notes, fileName = "music.mid", bpm) => {
388
+ const file = createFileFromNotes(notes, bpm);
389
+ const bytes = file.toBytes();
390
+ if (fileName === null) {
391
+ return bytes;
392
+ }
393
+ if (!fileName.endsWith(".mid")) {
394
+ fileName = `${fileName}.mid`;
395
+ }
396
+ if (typeof window !== "undefined" && window.URL && typeof window.URL.createObjectURL === "function") {
397
+ return createDownloadLink(bytes, fileName);
398
+ }
399
+ import_node_fs.default.writeFileSync(fileName, bytes, "binary");
400
+ console.log(`MIDI file generated: ${fileName}.`);
401
+ };
402
+ var createDownloadLink = (b, fileName) => {
403
+ const bytes = new Uint8Array(b.length);
404
+ for (let i = 0; i < b.length; i++) {
405
+ const ascii = b.charCodeAt(i);
406
+ bytes[i] = ascii;
407
+ }
408
+ const blob = new Blob([bytes], { type: "audio/midi" });
409
+ const link = document.createElement("a");
410
+ link.href = typeof window !== "undefined" && typeof window.URL !== "undefined" && typeof window.URL.createObjectURL !== "undefined" && window.URL.createObjectURL(blob) || "";
411
+ link.download = fileName;
412
+ link.innerText = "Download MIDI file";
413
+ return link;
414
+ };
415
+ var createFileFromNotes = (notes, bpm) => {
416
+ const file = new import_midi.File();
417
+ const track = new import_midi.Track();
418
+ if (typeof bpm === "number") {
419
+ track.setTempo(bpm);
420
+ }
421
+ file.addTrack(track);
422
+ for (const noteObj of notes) {
423
+ const level = noteObj.level || 127;
424
+ if (noteObj.note) {
425
+ if (typeof noteObj.note === "string") {
426
+ track.noteOn(0, noteObj.note, noteObj.length, level);
427
+ track.noteOff(0, noteObj.note, noteObj.length, level);
428
+ } else {
429
+ track.addChord(0, noteObj.note, noteObj.length, level);
430
+ }
431
+ } else {
432
+ track.noteOff(0, "", noteObj.length);
433
+ }
434
+ }
435
+ return file;
436
+ };
437
+
438
+ // src/progression.ts
439
+ var import_harmonics3 = require("harmonics");
440
+ var getChordDegrees = (mode) => {
441
+ const theRomans = {
442
+ ionian: ["I", "ii", "iii", "IV", "V", "vi", "vii\xB0"],
443
+ dorian: ["i", "ii", "III", "IV", "v", "vi\xB0", "VII"],
444
+ phrygian: ["i", "II", "III", "iv", "v\xB0", "VI", "vii"],
445
+ lydian: ["I", "II", "iii", "iv\xB0", "V", "vi", "vii"],
446
+ mixolydian: ["I", "ii", "iii\xB0", "IV", "v", "vi", "VII"],
447
+ aeolian: ["i", "ii\xB0", "III", "iv", "v", "VI", "VII"],
448
+ locrian: ["i\xB0", "II", "iii", "iv", "V", "VI", "vii"],
449
+ "melodic minor": ["i", "ii", "III+", "IV", "V", "vi\xB0", "vii\xB0"],
450
+ "harmonic minor": ["i", "ii\xB0", "III+", "iv", "V", "VI", "vii\xB0"]
451
+ };
452
+ theRomans.major = theRomans.ionian;
453
+ theRomans.minor = theRomans.aeolian;
454
+ return theRomans[mode] || [];
455
+ };
456
+ var idxByDegree = {
457
+ i: 0,
458
+ ii: 1,
459
+ iii: 2,
460
+ iv: 3,
461
+ v: 4,
462
+ vi: 5,
463
+ vii: 6
464
+ };
465
+ var getChordName = (roman) => {
466
+ const str = roman.replace(/\W/g, "");
467
+ let prefix = "M";
468
+ if (str.toLowerCase() === str) {
469
+ prefix = "m";
470
+ }
471
+ if (roman.indexOf("\xB0") > -1) {
472
+ return `${prefix}7b5`;
473
+ }
474
+ if (roman.indexOf("+") > -1) {
475
+ return `${prefix}#5`;
476
+ }
477
+ if (roman.indexOf("7") > -1) {
478
+ return prefix === "M" ? "maj7" : "m7";
479
+ }
480
+ return prefix;
481
+ };
482
+ var getChordsByProgression = (noteOctaveScale, chordDegress) => {
483
+ const noteOctaveScaleArr = noteOctaveScale.split(" ");
484
+ if (!noteOctaveScaleArr[0].match(/\d/)) {
485
+ noteOctaveScaleArr[0] += "4";
486
+ noteOctaveScale = noteOctaveScaleArr.join(" ");
487
+ }
488
+ const mode = (0, import_harmonics3.scale)(noteOctaveScale);
489
+ const chordDegreesArr = chordDegress.replace(/\s*,+\s*/g, " ").split(" ");
490
+ const chordFamily = chordDegreesArr.map((roman) => {
491
+ const chordName = getChordName(roman);
492
+ const scaleId = idxByDegree[roman.replace(/\W|\d/g, "").toLowerCase()];
493
+ const note = mode[scaleId];
494
+ const oct = note.replace(/\D+/, "");
495
+ return `${note.replace(/\d/, "") + chordName}_${oct}`;
496
+ });
497
+ return chordFamily.toString().replace(/,/g, " ");
498
+ };
499
+ var getProgFactory = ({ T, P, D }) => {
500
+ return (count = 4) => {
501
+ const chords = [];
502
+ chords.push(pickOne(T));
503
+ let i = 1;
504
+ if (i < count - 1) {
505
+ chords.push(pickOne(P));
506
+ i++;
507
+ }
508
+ if (i < count - 1 && dice()) {
509
+ chords.push(pickOne(P));
510
+ i++;
511
+ }
512
+ if (i < count - 1) {
513
+ chords.push(pickOne(D));
514
+ i++;
515
+ }
516
+ if (i < count - 1) {
517
+ chords.push(pickOne(P));
518
+ i++;
519
+ }
520
+ if (i < count - 1) {
521
+ chords.push(pickOne(D));
522
+ i++;
523
+ }
524
+ if (i < count - 1 && dice()) {
525
+ chords.push(pickOne(P));
526
+ i++;
527
+ }
528
+ while (i < count) {
529
+ chords.push(pickOne(D));
530
+ i++;
531
+ }
532
+ return chords;
533
+ };
534
+ };
535
+ var M = getProgFactory({ T: ["I", "vi"], P: ["ii", "IV"], D: ["V"] });
536
+ var m = getProgFactory({ T: ["i", "VI"], P: ["ii", "iv"], D: ["V"] });
537
+ var progression = (scaleType, count = 4) => {
538
+ if (scaleType === "major" || scaleType === "M") {
539
+ return M(count);
540
+ }
541
+ if (scaleType === "minor" || scaleType === "m") {
542
+ return m(count);
543
+ }
544
+ return [];
545
+ };
546
+
547
+ // src/cli.ts
548
+ var HELP_TEXT = `Usage:
549
+ scribbletune --riff <root> <mode> <pattern> [octaveShift] [motif]
550
+ scribbletune --chord <root> <mode> <progression|random> <pattern> [subdiv]
551
+ scribbletune --arp <root> <mode> <progression|random> <pattern> [subdiv]
552
+
553
+ Examples:
554
+ scribbletune --riff C3 phrygian x-xRx_RR 0 AABC --sizzle sin 2 --outfile riff.mid
555
+ scribbletune --chord C3 major 1645 xxxx 1m --sizzle cos 1 --outfile chord.mid
556
+ scribbletune --chord C3 major CM-FM-Am-GM xxxx 1m
557
+ scribbletune --chord C3 major random xxxx 1m
558
+ scribbletune --arp C3 major 1736 xxxx 1m --sizzle cos 4
559
+
560
+ Options:
561
+ --outfile <name> Output MIDI filename (default: music.mid)
562
+ --bpm <number> Tempo in BPM
563
+ --subdiv <value> Note subdivision (e.g. 4n, 1m)
564
+ --sizzle [style] [n] Sizzle style: sin|cos|rampUp|rampDown and optional reps
565
+ --sizzle-reps <n> Repetitions for sizzle
566
+ --amp <0-127> Maximum note level
567
+ --accent <pattern> Accent pattern using x and -
568
+ --accent-low <0-127> Accent low level
569
+ --count <2-8> Arp note count (arp command only)
570
+ --order <digits> Arp order string (arp command only)
571
+ -h, --help Show this help
572
+ `;
573
+ var romanByDigit = (progDigits, mode) => {
574
+ const modeForDegrees = mode.toLowerCase();
575
+ const chordDegrees = getChordDegrees(modeForDegrees);
576
+ if (!chordDegrees.length) {
577
+ throw new TypeError(`Unsupported mode "${mode}" for progression digits`);
578
+ }
579
+ const romans = progDigits.split("").map((digit) => {
580
+ const idx = Number(digit) - 1;
581
+ if (idx < 0 || idx >= chordDegrees.length) {
582
+ throw new TypeError(`Invalid progression digit "${digit}" in "${progDigits}"`);
583
+ }
584
+ return chordDegrees[idx];
585
+ });
586
+ return { chordDegrees: romans.join(" "), raw: progDigits };
587
+ };
588
+ var setOctave = (note, octaveShift = 0) => {
589
+ const base = note.replace(/\d+/g, "");
590
+ const oct = Number(note.match(/\d+/)?.[0] || "4");
591
+ return `${base}${oct + octaveShift}`;
592
+ };
593
+ var parseProgression = (root, mode, progressionInput) => {
594
+ if (progressionInput === "random") {
595
+ const modeType = mode === "minor" || mode === "m" ? "minor" : "major";
596
+ const randomProg = progression(modeType, 4).join(" ");
597
+ return getChordsByProgression(`${root} ${mode}`, randomProg);
598
+ }
599
+ if (/^[1-7]+$/.test(progressionInput)) {
600
+ const converted = romanByDigit(progressionInput, mode);
601
+ return getChordsByProgression(`${root} ${mode}`, converted.chordDegrees);
602
+ }
603
+ if (/^[ivIV°+7\s,]+$/.test(progressionInput)) {
604
+ const normalized = progressionInput.replace(/\s*,+\s*/g, " ");
605
+ return getChordsByProgression(`${root} ${mode}`, normalized);
606
+ }
607
+ return progressionInput.replace(/-/g, " ");
608
+ };
609
+ var parseCliArgs = (argv) => {
610
+ if (argv.length === 0 || argv.includes("-h") || argv.includes("--help")) {
611
+ return null;
612
+ }
613
+ let commandArg = argv[0];
614
+ if (commandArg.startsWith("--")) {
615
+ commandArg = commandArg.slice(2);
616
+ }
617
+ if (commandArg !== "riff" && commandArg !== "chord" && commandArg !== "arp") {
618
+ throw new TypeError(
619
+ `First argument must be riff/chord/arp (or --riff/--chord/--arp), received "${argv[0]}"`
620
+ );
621
+ }
622
+ const positionals = [];
623
+ const options = {
624
+ command: commandArg,
625
+ positionals,
626
+ outfile: "music.mid"
627
+ };
628
+ let i = 1;
629
+ while (i < argv.length) {
630
+ const token = argv[i];
631
+ if (!token.startsWith("--")) {
632
+ positionals.push(token);
633
+ i++;
634
+ continue;
635
+ }
636
+ if (token === "--outfile") {
637
+ options.outfile = argv[i + 1];
638
+ i += 2;
639
+ continue;
640
+ }
641
+ if (token === "--bpm") {
642
+ options.bpm = Number(argv[i + 1]);
643
+ i += 2;
644
+ continue;
645
+ }
646
+ if (token === "--subdiv") {
647
+ options.subdiv = argv[i + 1];
648
+ i += 2;
649
+ continue;
650
+ }
651
+ if (token === "--sizzle") {
652
+ const styleOrNum = argv[i + 1];
653
+ const maybeNum = argv[i + 2];
654
+ if (!styleOrNum || styleOrNum.startsWith("--")) {
655
+ options.sizzle = true;
656
+ i += 1;
657
+ continue;
658
+ }
659
+ if (/^\d+$/.test(styleOrNum)) {
660
+ options.sizzle = true;
661
+ options.sizzleReps = Number(styleOrNum);
662
+ i += 2;
663
+ continue;
664
+ }
665
+ options.sizzle = styleOrNum;
666
+ if (maybeNum && /^\d+$/.test(maybeNum)) {
667
+ options.sizzleReps = Number(maybeNum);
668
+ i += 3;
669
+ } else {
670
+ i += 2;
671
+ }
672
+ continue;
673
+ }
674
+ if (token === "--sizzle-reps") {
675
+ options.sizzleReps = Number(argv[i + 1]);
676
+ i += 2;
677
+ continue;
678
+ }
679
+ if (token === "--amp") {
680
+ options.amp = Number(argv[i + 1]);
681
+ i += 2;
682
+ continue;
683
+ }
684
+ if (token === "--accent") {
685
+ options.accent = argv[i + 1];
686
+ i += 2;
687
+ continue;
688
+ }
689
+ if (token === "--accent-low") {
690
+ options.accentLow = Number(argv[i + 1]);
691
+ i += 2;
692
+ continue;
693
+ }
694
+ if (token === "--count") {
695
+ options.count = Number(argv[i + 1]);
696
+ i += 2;
697
+ continue;
698
+ }
699
+ if (token === "--order") {
700
+ options.order = argv[i + 1];
701
+ i += 2;
702
+ continue;
703
+ }
704
+ throw new TypeError(`Unknown option "${token}"`);
705
+ }
706
+ return options;
707
+ };
708
+ var baseClipParams = (parsed) => {
709
+ return {
710
+ sizzle: parsed.sizzle,
711
+ sizzleReps: parsed.sizzleReps,
712
+ amp: parsed.amp,
713
+ accent: parsed.accent,
714
+ accentLow: parsed.accentLow,
715
+ subdiv: parsed.subdiv
716
+ };
717
+ };
718
+ var makeRiff = (parsed) => {
719
+ const [root, mode, pattern, octaveShiftArg, motif] = parsed.positionals;
720
+ if (!root || !mode || !pattern) {
721
+ throw new TypeError(
722
+ "riff requires: <root> <mode> <pattern> [octaveShift] [motif]"
723
+ );
724
+ }
725
+ const octaveShift = Number(octaveShiftArg || "0");
726
+ const riffScale = (0, import_harmonics4.scale)(`${setOctave(root, octaveShift)} ${mode}`);
727
+ const riffNotes = motif && motif.length ? motif.toUpperCase().split("").map((letter) => {
728
+ const idx = letter.charCodeAt(0) - 65;
729
+ if (idx < 0) {
730
+ return riffScale[0];
731
+ }
732
+ return riffScale[idx % riffScale.length];
733
+ }) : riffScale;
734
+ return clip({
735
+ notes: riffNotes,
736
+ randomNotes: riffScale,
737
+ pattern,
738
+ ...baseClipParams(parsed)
739
+ });
740
+ };
741
+ var makeChord = (parsed) => {
742
+ const [root, mode, progressionInput, pattern, subdiv] = parsed.positionals;
743
+ if (!root || !mode || !progressionInput || !pattern) {
744
+ throw new TypeError(
745
+ "chord requires: <root> <mode> <progression|random> <pattern> [subdiv]"
746
+ );
747
+ }
748
+ const chords = parseProgression(root, mode, progressionInput);
749
+ return clip({
750
+ notes: chords,
751
+ pattern,
752
+ subdiv: parsed.subdiv || subdiv,
753
+ ...baseClipParams(parsed)
754
+ });
755
+ };
756
+ var makeArp = (parsed) => {
757
+ const [root, mode, progressionInput, pattern, subdiv] = parsed.positionals;
758
+ if (!root || !mode || !progressionInput || !pattern) {
759
+ throw new TypeError(
760
+ "arp requires: <root> <mode> <progression|random> <pattern> [subdiv]"
761
+ );
762
+ }
763
+ const chords = parseProgression(root, mode, progressionInput);
764
+ const arpNotes = arp({
765
+ chords,
766
+ count: parsed.count || 4,
767
+ order: parsed.order || "0123"
768
+ });
769
+ return clip({
770
+ notes: arpNotes,
771
+ pattern,
772
+ subdiv: parsed.subdiv || subdiv,
773
+ ...baseClipParams(parsed)
774
+ });
775
+ };
776
+ var runCli = (argv, deps) => {
777
+ const stdout = deps?.stdout || console.log;
778
+ const stderr = deps?.stderr || console.error;
779
+ const writeMidi = deps?.writeMidi || ((notes, fileName, bpm) => {
780
+ midi(notes, fileName, bpm);
781
+ });
782
+ try {
783
+ const parsed = parseCliArgs(argv);
784
+ if (!parsed) {
785
+ stdout(HELP_TEXT);
786
+ return 0;
787
+ }
788
+ let notes = [];
789
+ if (parsed.command === "riff") {
790
+ notes = makeRiff(parsed);
791
+ } else if (parsed.command === "chord") {
792
+ notes = makeChord(parsed);
793
+ } else {
794
+ notes = makeArp(parsed);
795
+ }
796
+ writeMidi(notes, parsed.outfile, parsed.bpm);
797
+ stdout(
798
+ `Generated ${parsed.command} clip (${notes.length} events) -> ${parsed.outfile}`
799
+ );
800
+ return 0;
801
+ } catch (e) {
802
+ stderr(e instanceof Error ? e.message : String(e));
803
+ stderr("Run with --help for usage");
804
+ return 1;
805
+ }
806
+ };
807
+ if (process.argv[1]?.includes("cli")) {
808
+ process.exit(runCli(process.argv.slice(2)));
809
+ }
810
+ // Annotate the CommonJS export names for ESM import in node:
811
+ 0 && (module.exports = {
812
+ runCli
813
+ });