scribbletune 5.4.0 → 5.5.1

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 (3) hide show
  1. package/README.md +345 -22
  2. package/dist/cli.cjs +884 -0
  3. package/package.json +4 -1
package/README.md CHANGED
@@ -1,41 +1,364 @@
1
- ## <img width=2% src="https://scribbletune.com/images/scribbletune-logo.png"> SCRIBBLETUNE
1
+ <p align="center">
2
+ <img width="64" src="https://scribbletune.com/images/scribbletune-logo.png" alt="Scribbletune">
3
+ </p>
2
4
 
3
- [![Build Status](https://travis-ci.com/scribbletune/scribbletune.svg?branch=master)](http://travis-ci.com/scribbletune/scribbletune)
4
- [![Try scribbletune on RunKit](https://badge.runkitcdn.com/scribbletune.svg)](https://npm.runkit.com/scribbletune)
5
+ <h1 align="center">Scribbletune</h1>
5
6
 
6
- Use simple **JavaScript** `Strings` and `Arrays` to generate rhythms and musical patterns. Directly use the names of scales or chords in your code to get arrays which you can mash up using Array methods in ways you hadn't imagined before! Create clips of musical ideas and **export MIDI files** which you can import in _Ableton Live, Reason, GarageBand_ or any music creation software that accepts MIDI files.
7
+ <p align="center">
8
+ Create music with JavaScript. Use simple strings and arrays to craft rhythms, melodies, and chord progressions — then export MIDI files or play them live in the browser with <a href="https://tonejs.github.io/">Tone.js</a>.
9
+ </p>
7
10
 
8
- ### Install
11
+ <p align="center">
12
+ <a href="https://www.npmjs.com/package/scribbletune"><img src="https://img.shields.io/npm/v/scribbletune.svg" alt="npm version"></a>
13
+ <a href="https://www.npmjs.com/package/scribbletune"><img src="https://img.shields.io/npm/l/scribbletune.svg" alt="license"></a>
14
+ </p>
15
+
16
+ ---
17
+
18
+ ## Install
19
+
20
+ ```bash
21
+ npm install scribbletune
22
+ ```
23
+
24
+ ## Quick start
25
+
26
+ ### CLI
27
+
28
+ The package now ships a CLI binary: `scribbletune`.
29
+
30
+ Run modes:
9
31
 
10
32
  ```bash
33
+ # Global install
34
+ npm install -g scribbletune
35
+ scribbletune --help
36
+
37
+ # Local/project install
11
38
  npm install scribbletune
39
+ npx scribbletune --help
40
+ ```
41
+
42
+ Quick command examples:
43
+
44
+ ```bash
45
+ # Use built file directly from repo (before publish)
46
+ node dist/cli.cjs --help
47
+
48
+ # Use local package binary
49
+ npx scribbletune --help
50
+
51
+ # If installed globally
52
+ scribbletune --help
53
+ ```
54
+
55
+ #### Command format
56
+
57
+ ```bash
58
+ scribbletune --riff <root> <mode> <pattern> [octaveShift] [motif] [options]
59
+ scribbletune --chord <root> <mode> <progression|random> <pattern> [subdiv] [options]
60
+ scribbletune --arp <root> <mode> <progression|random> <pattern> [subdiv] [options]
61
+ ```
62
+
63
+ Progression input rules for `--chord` and `--arp`:
64
+
65
+ ```bash
66
+ 1645 # degree digits
67
+ "I IV vi V" # roman numerals (space separated)
68
+ I,IV,vi,V # roman numerals (comma separated)
69
+ random # generated progression
70
+ CM-FM-Am-GM # explicit chord names
71
+ ```
72
+
73
+ Notes:
74
+ - Hyphenated romans like `I-IV-vi-V` are not supported currently.
75
+ - For explicit chords (`CM-FM-Am-GM`), `root` and `mode` are currently ignored.
76
+
77
+ Common options:
78
+
79
+ ```bash
80
+ --outfile <file.mid> # default: music.mid
81
+ --bpm <number>
82
+ --subdiv <4n|8n|1m...>
83
+ --sizzle [sin|cos|rampUp|rampDown] [reps]
84
+ --sizzle-reps <number>
85
+ --amp <0-127>
86
+ --accent <x--x...>
87
+ --accent-low <0-127>
88
+ --fit-pattern # explicit enable (already enabled by default)
89
+ --no-fit-pattern # disable automatic pattern fitting
90
+ ```
91
+
92
+ Note: if your pattern uses `[` and `]` (for subdivisions), quote it in shell:
93
+
94
+ ```bash
95
+ scribbletune --arp C3 major 1 'x-x[xx]-x-[xx]' 16n
96
+ ```
97
+
98
+ Pattern helpers:
99
+
100
+ ```bash
101
+ x.repeat(4) # -> xxxx
102
+ 'x-x[xx]'.repeat(2)
103
+ 2(x-x[xx]) # prefix repeat shorthand
104
+ (x-x[xx])2 # suffix repeat shorthand
105
+ ```
106
+
107
+ #### `--riff` examples
108
+
109
+ ```bash
110
+ # Basic riff from scale
111
+ scribbletune --riff C3 phrygian x-xRx_RR --outfile riff.mid
112
+
113
+ # With octave shift and motif
114
+ scribbletune --riff C3 phrygian x-xRx_RR 0 AABC --sizzle sin 2 --outfile riff-aabc.mid
115
+
116
+ # Pattern with subdivisions (quote [] in shell)
117
+ scribbletune --riff C3 phrygian 'x-x[xx]-x-[xx]' 0 AABC --outfile riff-subdiv.mid
118
+ ```
119
+
120
+ #### `--chord` examples
121
+
122
+ ```bash
123
+ # Degree digits (resolved against root/mode)
124
+ scribbletune --chord C3 major 1645 xxxx 1m --sizzle cos 1 --outfile chords-1645.mid
125
+
126
+ # Roman numerals (space/comma separated)
127
+ scribbletune --chord C3 major "I IV vi V" xxxx 1m --outfile chords-roman.mid
128
+
129
+ # Random progression
130
+ scribbletune --chord C3 major random xxxx 1m --outfile chords-random.mid
131
+
132
+ # Explicit chord names (root/mode currently ignored for this style)
133
+ scribbletune --chord C3 major CM-FM-Am-GM xxxx 1m --outfile chords-explicit.mid
134
+
135
+ # Subdivisions in pattern
136
+ scribbletune --chord C3 major I,IV,vi,V 'x-x[xx]-x-[xx]' 8n --outfile chords-subdiv.mid
137
+ ```
138
+
139
+ #### `--arp` examples
140
+
141
+ ```bash
142
+ # Arp from degree progression
143
+ scribbletune --arp C3 major 1736 xxxx 1m --sizzle cos 4 --outfile arp-1736.mid
144
+
145
+ # Single degree "1" means tonic chord in the chosen key/mode
146
+ scribbletune --arp C3 major 1 xxxx 4n --outfile arp-degree-1.mid
147
+
148
+ # Arp from explicit chords
149
+ scribbletune --arp C3 major CM-FM-Am-GM xxxx 1m --count 4 --order 1234 --outfile arp-explicit.mid
150
+
151
+ # Custom note order inside each arpeggiated chord (one-based)
152
+ scribbletune --arp C3 major 1 xxxx 4n --order 2143 --outfile arp-order-2143.mid
153
+
154
+ # Same custom order using local dist build
155
+ node dist/cli.cjs --arp C3 major 1 xxxx 4n --order 2143 --outfile arp-order-local.mid
156
+
157
+ # Auto-fit is default (single x expands to full generated arp length)
158
+ scribbletune --arp C3 major 1736 x 4n --outfile arp-fit-default.mid
159
+
160
+ # Disable auto-fit if you want a short clip
161
+ scribbletune --arp C3 major 1736 x 4n --no-fit-pattern --outfile arp-no-fit.mid
162
+ ```
163
+
164
+ `--order` behavior:
165
+ - One-based order is supported (`1234`, `2143`) and is recommended.
166
+ - Zero-based order is also accepted for backward compatibility (`0123`, `1032`).
167
+
168
+ Run `scribbletune --help` to see the latest CLI usage text.
169
+
170
+ ### Generate a MIDI file (Node.js)
171
+
172
+ ```js
173
+ import { scale, clip, midi } from 'scribbletune';
174
+
175
+ const notes = scale('C4 major');
176
+ const c = clip({ notes, pattern: 'x'.repeat(8) });
177
+
178
+ midi(c, 'c-major.mid');
12
179
  ```
13
180
 
14
- ### Use it to create a MIDI clip by running a JS file from your terminal using node.js
15
- ```javascript
16
- const scribble = require('scribbletune');
17
- const clip = scribble.clip({
18
- notes: scribble.scale('C4 major'),
19
- pattern: 'x'.repeat(7) + '_'
181
+ Run it with `node` and open the `.mid` file in Ableton Live, GarageBand, Logic, or any DAW.
182
+
183
+ ### Play in the browser (with Tone.js)
184
+
185
+ Scribbletune's browser entry point adds `Session`, `Channel`, and live `clip()` support on top of [Tone.js](https://tonejs.github.io/).
186
+
187
+ ```js
188
+ import { Session } from 'scribbletune/browser';
189
+
190
+ const session = new Session();
191
+ const channel = session.createChannel({
192
+ instrument: 'PolySynth',
193
+ clips: [
194
+ { pattern: 'x-x-', notes: 'C4 E4 G4' },
195
+ { pattern: '[-xx]', notes: 'C4 D#4' },
196
+ ],
20
197
  });
21
198
 
22
- scribble.midi(clip, 'c-major.mid');
199
+ await Tone.start();
200
+ Tone.Transport.start();
201
+ channel.startClip(0);
23
202
  ```
24
203
 
204
+ ### Standalone sample clip (no Session/Channel needed)
25
205
 
26
- You can use Scribbletune even **in the browser** with Tone.js!. There are a couple of ways to do this. A quick and dirty way is to make sure to pull in [Tone.js](https://cdnjs.com/libraries/tone) followed by the [latest browser version of Scribbletune](https://cdnjs.com/libraries/scribbletune).
206
+ ```js
207
+ import { clip } from 'scribbletune/browser';
27
208
 
28
- ```html
29
- <script src="https://cdnjs.cloudflare.com/ajax/libs/tone/<LATEST-VERSION-FROM-CDNJS>/Tone.min.js"></script>
30
- <script src="https://cdnjs.cloudflare.com/ajax/libs/scribbletune/<LATEST-VERSION-FROM-CDNJS>/scribbletune.js"></script>
209
+ await Tone.start();
210
+ Tone.Transport.start();
211
+
212
+ const kick = clip({
213
+ sample: 'https://scribbletune.com/sounds/kick.wav',
214
+ pattern: 'x-x-',
215
+ });
216
+ kick.start();
31
217
  ```
32
218
 
33
- This will expose a global object called `scribble` which you can directly use to run the methods from Scribbletune in conjunction with Tone.js
219
+ ## Core concepts
220
+
221
+ ### Pattern language
222
+
223
+ Scribbletune uses a simple string notation to describe rhythms:
224
+
225
+ | Char | Meaning |
226
+ |------|---------|
227
+ | `x` | Note on |
228
+ | `-` | Note off (rest) |
229
+ | `_` | Sustain previous note |
230
+ | `R` | Random note (from `randomNotes` pool) |
231
+ | `[]` | Subdivide (e.g. `[xx]` = two notes in one beat) |
232
+
233
+ ```js
234
+ 'x---x---x-x-x---' // basic kick pattern
235
+ '[xx][xx]x-x-' // hihat with subdivisions
236
+ 'x___' // one long sustained note
237
+ ```
238
+
239
+ ### Scales and chords
240
+
241
+ Powered by [harmonics](https://github.com/scribbletune/harmonics):
242
+
243
+ ```js
244
+ import { scale, chord, scales, chords } from 'scribbletune';
245
+
246
+ scale('C4 major'); // ['C4', 'D4', 'E4', 'F4', 'G4', 'A4', 'B4']
247
+ chord('CM'); // ['C4', 'E4', 'G4']
248
+ scales(); // list all available scale names
249
+ chords(); // list all available chord names
250
+ ```
251
+
252
+ ### Arpeggios
253
+
254
+ ```js
255
+ import { arp } from 'scribbletune';
256
+
257
+ arp({ chords: 'CM FM', count: 4, order: '0123' });
258
+ // ['C4', 'E4', 'G4', 'C5', 'F4', 'A4', 'C5', 'F5']
259
+ ```
260
+
261
+ ### Chord progressions
262
+
263
+ ```js
264
+ import { progression, getChordsByProgression } from 'scribbletune';
265
+
266
+ progression('M', 4); // e.g. ['I', 'ii', 'V', 'IV']
34
267
 
35
- The recommended way for the browser, however, is to import it like this
36
- ```javascript
37
- const scribble = require('scribbletune/browser');
268
+ getChordsByProgression('C4 major', 'I IV V IV');
269
+ // 'CM_4 FM_4 GM_4 FM_4'
38
270
  ```
39
- This will provide the same API but augment it a bit to support browser based functionality.
40
271
 
41
- Visit [scribbletune.com](https://scribbletune.com) for documentation, tutorials and examples! Listen to music generated with Scribbletune on [Soundcloud](https://soundcloud.com/scribbletune).
272
+ ## Browser API
273
+
274
+ The browser entry point (`scribbletune/browser`) provides everything above plus:
275
+
276
+ ### Session and Channel
277
+
278
+ ```js
279
+ import { Session } from 'scribbletune/browser';
280
+
281
+ const session = new Session();
282
+ const drums = session.createChannel({
283
+ sample: 'https://scribbletune.com/sounds/kick.wav',
284
+ clips: [
285
+ { pattern: 'x---x---' },
286
+ { pattern: 'x-x-x-x-' },
287
+ ],
288
+ });
289
+
290
+ const synth = session.createChannel({
291
+ instrument: 'PolySynth',
292
+ clips: [
293
+ { pattern: 'x-x-', notes: 'C4 E4 G4' },
294
+ ],
295
+ });
296
+
297
+ await Tone.start();
298
+ Tone.Transport.start();
299
+
300
+ // Start clips independently
301
+ drums.startClip(0);
302
+ synth.startClip(0);
303
+
304
+ // Switch patterns on the fly
305
+ drums.startClip(1);
306
+
307
+ // Or start a row across all channels
308
+ session.startRow(0);
309
+ ```
310
+
311
+ ### Channel options
312
+
313
+ Channels accept various sound sources:
314
+
315
+ ```js
316
+ // Built-in Tone.js synth (by name)
317
+ { instrument: 'PolySynth' }
318
+
319
+ // Pre-built Tone.js instrument
320
+ { instrument: new Tone.FMSynth() }
321
+
322
+ // Audio sample URL
323
+ { sample: 'https://example.com/kick.wav' }
324
+
325
+ // Multi-sample instrument
326
+ { samples: { C3: 'piano-c3.wav', D3: 'piano-d3.wav' } }
327
+
328
+ // With effects
329
+ { instrument: 'PolySynth', effects: ['Chorus', 'Reverb'] }
330
+ ```
331
+
332
+ ## API reference
333
+
334
+ | Export | Description |
335
+ |--------|-------------|
336
+ | `clip(params)` | Create a clip — returns note objects (Node.js) or a Tone.Sequence (browser) |
337
+ | `midi(clip, filename?)` | Export a clip to a MIDI file |
338
+ | `scale(name)` | Get notes of a scale, e.g. `'C4 minor'` |
339
+ | `chord(name)` | Get notes of a chord, e.g. `'CM'` |
340
+ | `scales()` | List all available scale names |
341
+ | `chords()` | List all available chord names |
342
+ | `arp(params)` | Generate arpeggiated note sequences |
343
+ | `progression(type, count)` | Generate a chord progression (`'M'` or `'m'`) |
344
+ | `getChordsByProgression(scale, degrees)` | Convert Roman numeral degrees to chord names |
345
+ | `getChordDegrees(mode)` | Get Roman numeral degrees for a mode |
346
+ | `Session` | _(browser only)_ Manage multiple channels and coordinate playback |
347
+
348
+ ## Development
349
+
350
+ ```bash
351
+ npm install # install dependencies
352
+ npm test # run tests
353
+ npm run build # build with tsup
354
+ npm run lint # check with biome
355
+ npm run dev # build in watch mode
356
+ ```
357
+
358
+ ## License
359
+
360
+ MIT
361
+
362
+ ---
363
+
364
+ [scribbletune.com](https://scribbletune.com) | [Soundcloud](https://soundcloud.com/scribbletune)
package/dist/cli.cjs ADDED
@@ -0,0 +1,884 @@
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
+ --fit-pattern Repeat pattern until it can consume all generated notes (default)
572
+ --no-fit-pattern Disable automatic pattern fitting
573
+ -h, --help Show this help
574
+ `;
575
+ var romanByDigit = (progDigits, mode) => {
576
+ const modeForDegrees = mode.toLowerCase();
577
+ const chordDegrees = getChordDegrees(modeForDegrees);
578
+ if (!chordDegrees.length) {
579
+ throw new TypeError(`Unsupported mode "${mode}" for progression digits`);
580
+ }
581
+ const romans = progDigits.split("").map((digit) => {
582
+ const idx = Number(digit) - 1;
583
+ if (idx < 0 || idx >= chordDegrees.length) {
584
+ throw new TypeError(`Invalid progression digit "${digit}" in "${progDigits}"`);
585
+ }
586
+ return chordDegrees[idx];
587
+ });
588
+ return { chordDegrees: romans.join(" "), raw: progDigits };
589
+ };
590
+ var setOctave = (note, octaveShift = 0) => {
591
+ const base = note.replace(/\d+/g, "");
592
+ const oct = Number(note.match(/\d+/)?.[0] || "4");
593
+ return `${base}${oct + octaveShift}`;
594
+ };
595
+ var parseProgression = (root, mode, progressionInput) => {
596
+ if (progressionInput === "random") {
597
+ const modeType = mode === "minor" || mode === "m" ? "minor" : "major";
598
+ const randomProg = progression(modeType, 4).join(" ");
599
+ return getChordsByProgression(`${root} ${mode}`, randomProg);
600
+ }
601
+ if (/^[1-7]+$/.test(progressionInput)) {
602
+ const converted = romanByDigit(progressionInput, mode);
603
+ return getChordsByProgression(`${root} ${mode}`, converted.chordDegrees);
604
+ }
605
+ if (/^[ivIV°+7\s,]+$/.test(progressionInput)) {
606
+ const normalized = progressionInput.replace(/\s*,+\s*/g, " ");
607
+ return getChordsByProgression(`${root} ${mode}`, normalized);
608
+ }
609
+ return progressionInput.replace(/-/g, " ");
610
+ };
611
+ var normalizeArpOrder = (order) => {
612
+ if (!/^\d+$/.test(order)) {
613
+ throw new TypeError("Invalid value for --order");
614
+ }
615
+ if (order.includes("0")) {
616
+ return order;
617
+ }
618
+ return order.split("").map((ch) => {
619
+ const n = Number(ch);
620
+ if (n < 1) {
621
+ throw new TypeError("Invalid value for --order");
622
+ }
623
+ return String(n - 1);
624
+ }).join("");
625
+ };
626
+ var expandPatternSyntax = (pattern) => {
627
+ const jsRepeatQuoted = pattern.match(/^(['"])(.+)\1\.repeat\((\d+)\)$/);
628
+ if (jsRepeatQuoted) {
629
+ return jsRepeatQuoted[2].repeat(Number(jsRepeatQuoted[3]));
630
+ }
631
+ const jsRepeatUnquoted = pattern.match(/^(.+)\.repeat\((\d+)\)$/);
632
+ if (jsRepeatUnquoted) {
633
+ return jsRepeatUnquoted[1].repeat(Number(jsRepeatUnquoted[2]));
634
+ }
635
+ const prefixRepeat = pattern.match(/^(\d+)\((.+)\)$/);
636
+ if (prefixRepeat) {
637
+ return prefixRepeat[2].repeat(Number(prefixRepeat[1]));
638
+ }
639
+ const suffixRepeat = pattern.match(/^\((.+)\)(\d+)$/);
640
+ if (suffixRepeat) {
641
+ return suffixRepeat[1].repeat(Number(suffixRepeat[2]));
642
+ }
643
+ return pattern;
644
+ };
645
+ var countPatternSteps = (pattern) => (pattern.match(/[xR]/g) || []).length;
646
+ var fitPatternToNoteCount = (pattern, noteCount) => {
647
+ const stepCount = countPatternSteps(pattern);
648
+ if (!stepCount || stepCount >= noteCount) {
649
+ return pattern;
650
+ }
651
+ const reps = Math.ceil(noteCount / stepCount);
652
+ return pattern.repeat(reps);
653
+ };
654
+ var resolvePattern = (rawPattern, noteCount, fitPattern = false) => {
655
+ const expanded = expandPatternSyntax(rawPattern);
656
+ return fitPattern ? fitPatternToNoteCount(expanded, noteCount) : expanded;
657
+ };
658
+ var parseCliArgs = (argv) => {
659
+ if (argv.length === 0 || argv.includes("-h") || argv.includes("--help")) {
660
+ return null;
661
+ }
662
+ let commandArg = argv[0];
663
+ if (commandArg.startsWith("--")) {
664
+ commandArg = commandArg.slice(2);
665
+ }
666
+ if (commandArg !== "riff" && commandArg !== "chord" && commandArg !== "arp") {
667
+ throw new TypeError(
668
+ `First argument must be riff/chord/arp (or --riff/--chord/--arp), received "${argv[0]}"`
669
+ );
670
+ }
671
+ const positionals = [];
672
+ const options = {
673
+ command: commandArg,
674
+ positionals,
675
+ outfile: "music.mid",
676
+ fitPattern: true
677
+ };
678
+ let i = 1;
679
+ while (i < argv.length) {
680
+ const token = argv[i];
681
+ if (!token.startsWith("--")) {
682
+ positionals.push(token);
683
+ i++;
684
+ continue;
685
+ }
686
+ if (token === "--outfile") {
687
+ options.outfile = argv[i + 1];
688
+ i += 2;
689
+ continue;
690
+ }
691
+ if (token === "--bpm") {
692
+ options.bpm = Number(argv[i + 1]);
693
+ i += 2;
694
+ continue;
695
+ }
696
+ if (token === "--subdiv") {
697
+ options.subdiv = argv[i + 1];
698
+ i += 2;
699
+ continue;
700
+ }
701
+ if (token === "--sizzle") {
702
+ const styleOrNum = argv[i + 1];
703
+ const maybeNum = argv[i + 2];
704
+ if (!styleOrNum || styleOrNum.startsWith("--")) {
705
+ options.sizzle = true;
706
+ i += 1;
707
+ continue;
708
+ }
709
+ if (/^\d+$/.test(styleOrNum)) {
710
+ options.sizzle = true;
711
+ options.sizzleReps = Number(styleOrNum);
712
+ i += 2;
713
+ continue;
714
+ }
715
+ options.sizzle = styleOrNum;
716
+ if (maybeNum && /^\d+$/.test(maybeNum)) {
717
+ options.sizzleReps = Number(maybeNum);
718
+ i += 3;
719
+ } else {
720
+ i += 2;
721
+ }
722
+ continue;
723
+ }
724
+ if (token === "--sizzle-reps") {
725
+ options.sizzleReps = Number(argv[i + 1]);
726
+ i += 2;
727
+ continue;
728
+ }
729
+ if (token === "--amp") {
730
+ options.amp = Number(argv[i + 1]);
731
+ i += 2;
732
+ continue;
733
+ }
734
+ if (token === "--accent") {
735
+ options.accent = argv[i + 1];
736
+ i += 2;
737
+ continue;
738
+ }
739
+ if (token === "--accent-low") {
740
+ options.accentLow = Number(argv[i + 1]);
741
+ i += 2;
742
+ continue;
743
+ }
744
+ if (token === "--count") {
745
+ options.count = Number(argv[i + 1]);
746
+ i += 2;
747
+ continue;
748
+ }
749
+ if (token === "--order") {
750
+ options.order = argv[i + 1];
751
+ i += 2;
752
+ continue;
753
+ }
754
+ if (token === "--fit-pattern") {
755
+ options.fitPattern = true;
756
+ i += 1;
757
+ continue;
758
+ }
759
+ if (token === "--no-fit-pattern") {
760
+ options.fitPattern = false;
761
+ i += 1;
762
+ continue;
763
+ }
764
+ throw new TypeError(`Unknown option "${token}"`);
765
+ }
766
+ return options;
767
+ };
768
+ var baseClipParams = (parsed) => {
769
+ return {
770
+ sizzle: parsed.sizzle,
771
+ sizzleReps: parsed.sizzleReps,
772
+ amp: parsed.amp,
773
+ accent: parsed.accent,
774
+ accentLow: parsed.accentLow
775
+ };
776
+ };
777
+ var makeRiff = (parsed) => {
778
+ const [root, mode, pattern, octaveShiftArg, motif] = parsed.positionals;
779
+ if (!root || !mode || !pattern) {
780
+ throw new TypeError(
781
+ "riff requires: <root> <mode> <pattern> [octaveShift] [motif]"
782
+ );
783
+ }
784
+ const octaveShift = Number(octaveShiftArg || "0");
785
+ const riffScale = (0, import_harmonics4.scale)(`${setOctave(root, octaveShift)} ${mode}`);
786
+ const riffNotes = motif && motif.length ? motif.toUpperCase().split("").map((letter) => {
787
+ const idx = letter.charCodeAt(0) - 65;
788
+ if (idx < 0) {
789
+ return riffScale[0];
790
+ }
791
+ return riffScale[idx % riffScale.length];
792
+ }) : riffScale;
793
+ const resolvedPattern = resolvePattern(
794
+ pattern,
795
+ riffNotes.length,
796
+ parsed.fitPattern
797
+ );
798
+ return clip({
799
+ notes: riffNotes,
800
+ randomNotes: riffScale,
801
+ pattern: resolvedPattern,
802
+ ...baseClipParams(parsed)
803
+ });
804
+ };
805
+ var makeChord = (parsed) => {
806
+ const [root, mode, progressionInput, pattern, subdiv] = parsed.positionals;
807
+ if (!root || !mode || !progressionInput || !pattern) {
808
+ throw new TypeError(
809
+ "chord requires: <root> <mode> <progression|random> <pattern> [subdiv]"
810
+ );
811
+ }
812
+ const chords = parseProgression(root, mode, progressionInput);
813
+ const chordCount = chords.trim().split(/\s+/).length;
814
+ const resolvedPattern = resolvePattern(pattern, chordCount, parsed.fitPattern);
815
+ return clip({
816
+ notes: chords,
817
+ pattern: resolvedPattern,
818
+ ...baseClipParams(parsed),
819
+ subdiv: parsed.subdiv || subdiv
820
+ });
821
+ };
822
+ var makeArp = (parsed) => {
823
+ const [root, mode, progressionInput, pattern, subdiv] = parsed.positionals;
824
+ if (!root || !mode || !progressionInput || !pattern) {
825
+ throw new TypeError(
826
+ "arp requires: <root> <mode> <progression|random> <pattern> [subdiv]"
827
+ );
828
+ }
829
+ const chords = parseProgression(root, mode, progressionInput);
830
+ const arpNotes = arp({
831
+ chords,
832
+ count: parsed.count || 4,
833
+ order: parsed.order ? normalizeArpOrder(parsed.order) : "0123"
834
+ });
835
+ const resolvedPattern = resolvePattern(
836
+ pattern,
837
+ arpNotes.length,
838
+ parsed.fitPattern
839
+ );
840
+ return clip({
841
+ notes: arpNotes,
842
+ pattern: resolvedPattern,
843
+ ...baseClipParams(parsed),
844
+ subdiv: parsed.subdiv || subdiv
845
+ });
846
+ };
847
+ var runCli = (argv, deps) => {
848
+ const stdout = deps?.stdout || console.log;
849
+ const stderr = deps?.stderr || console.error;
850
+ const writeMidi = deps?.writeMidi || ((notes, fileName, bpm) => {
851
+ midi(notes, fileName, bpm);
852
+ });
853
+ try {
854
+ const parsed = parseCliArgs(argv);
855
+ if (!parsed) {
856
+ stdout(HELP_TEXT);
857
+ return 0;
858
+ }
859
+ let notes = [];
860
+ if (parsed.command === "riff") {
861
+ notes = makeRiff(parsed);
862
+ } else if (parsed.command === "chord") {
863
+ notes = makeChord(parsed);
864
+ } else {
865
+ notes = makeArp(parsed);
866
+ }
867
+ writeMidi(notes, parsed.outfile, parsed.bpm);
868
+ stdout(
869
+ `Generated ${parsed.command} clip (${notes.length} events) -> ${parsed.outfile}`
870
+ );
871
+ return 0;
872
+ } catch (e) {
873
+ stderr(e instanceof Error ? e.message : String(e));
874
+ stderr("Run with --help for usage");
875
+ return 1;
876
+ }
877
+ };
878
+ if (process.argv[1]?.includes("cli")) {
879
+ process.exit(runCli(process.argv.slice(2)));
880
+ }
881
+ // Annotate the CommonJS export names for ESM import in node:
882
+ 0 && (module.exports = {
883
+ runCli
884
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scribbletune",
3
- "version": "5.4.0",
3
+ "version": "5.5.1",
4
4
  "description": "Create music with JavaScript and Node.js!",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -9,6 +9,9 @@
9
9
  "dist"
10
10
  ],
11
11
  "module": "./dist/index.js",
12
+ "bin": {
13
+ "scribbletune": "./dist/cli.cjs"
14
+ },
12
15
  "exports": {
13
16
  ".": {
14
17
  "types": "./dist/index.d.ts",