scribbletune 5.4.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.
- package/README.md +276 -22
- package/dist/cli.cjs +813 -0
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -1,41 +1,295 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img width="64" src="https://scribbletune.com/images/scribbletune-logo.png" alt="Scribbletune">
|
|
3
|
+
</p>
|
|
2
4
|
|
|
3
|
-
|
|
4
|
-
[](https://npm.runkit.com/scribbletune)
|
|
5
|
+
<h1 align="center">Scribbletune</h1>
|
|
5
6
|
|
|
6
|
-
|
|
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
|
-
|
|
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
|
+
#### Command format
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
scribbletune --riff <root> <mode> <pattern> [octaveShift] [motif] [options]
|
|
46
|
+
scribbletune --chord <root> <mode> <progression|random> <pattern> [subdiv] [options]
|
|
47
|
+
scribbletune --arp <root> <mode> <progression|random> <pattern> [subdiv] [options]
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Common options:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
--outfile <file.mid> # default: music.mid
|
|
54
|
+
--bpm <number>
|
|
55
|
+
--subdiv <4n|8n|1m...>
|
|
56
|
+
--sizzle [sin|cos|rampUp|rampDown] [reps]
|
|
57
|
+
--sizzle-reps <number>
|
|
58
|
+
--amp <0-127>
|
|
59
|
+
--accent <x--x...>
|
|
60
|
+
--accent-low <0-127>
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
#### `--riff` examples
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
# Basic riff from scale
|
|
67
|
+
scribbletune --riff C3 phrygian x-xRx_RR --outfile riff.mid
|
|
68
|
+
|
|
69
|
+
# With octave shift and motif
|
|
70
|
+
scribbletune --riff C3 phrygian x-xRx_RR 0 AABC --sizzle sin 2 --outfile riff-aabc.mid
|
|
12
71
|
```
|
|
13
72
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
73
|
+
#### `--chord` examples
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
# Degree digits (resolved against root/mode)
|
|
77
|
+
scribbletune --chord C3 major 1645 xxxx 1m --sizzle cos 1 --outfile chords-1645.mid
|
|
78
|
+
|
|
79
|
+
# Roman numerals (space/comma separated)
|
|
80
|
+
scribbletune --chord C3 major "I IV vi V" xxxx 1m --outfile chords-roman.mid
|
|
81
|
+
|
|
82
|
+
# Random progression
|
|
83
|
+
scribbletune --chord C3 major random xxxx 1m --outfile chords-random.mid
|
|
84
|
+
|
|
85
|
+
# Explicit chord names (root/mode currently ignored for this style)
|
|
86
|
+
scribbletune --chord C3 major CM-FM-Am-GM xxxx 1m --outfile chords-explicit.mid
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
#### `--arp` examples
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
# Arp from degree progression
|
|
93
|
+
scribbletune --arp C3 major 1736 xxxx 1m --sizzle cos 4 --outfile arp-1736.mid
|
|
94
|
+
|
|
95
|
+
# Arp from explicit chords
|
|
96
|
+
scribbletune --arp C3 major CM-FM-Am-GM xxxx 1m --count 4 --order 0123 --outfile arp-explicit.mid
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Run `scribbletune --help` to see the latest CLI usage text.
|
|
100
|
+
|
|
101
|
+
### Generate a MIDI file (Node.js)
|
|
102
|
+
|
|
103
|
+
```js
|
|
104
|
+
import { scale, clip, midi } from 'scribbletune';
|
|
105
|
+
|
|
106
|
+
const notes = scale('C4 major');
|
|
107
|
+
const c = clip({ notes, pattern: 'x'.repeat(8) });
|
|
108
|
+
|
|
109
|
+
midi(c, 'c-major.mid');
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Run it with `node` and open the `.mid` file in Ableton Live, GarageBand, Logic, or any DAW.
|
|
113
|
+
|
|
114
|
+
### Play in the browser (with Tone.js)
|
|
115
|
+
|
|
116
|
+
Scribbletune's browser entry point adds `Session`, `Channel`, and live `clip()` support on top of [Tone.js](https://tonejs.github.io/).
|
|
117
|
+
|
|
118
|
+
```js
|
|
119
|
+
import { Session } from 'scribbletune/browser';
|
|
120
|
+
|
|
121
|
+
const session = new Session();
|
|
122
|
+
const channel = session.createChannel({
|
|
123
|
+
instrument: 'PolySynth',
|
|
124
|
+
clips: [
|
|
125
|
+
{ pattern: 'x-x-', notes: 'C4 E4 G4' },
|
|
126
|
+
{ pattern: '[-xx]', notes: 'C4 D#4' },
|
|
127
|
+
],
|
|
20
128
|
});
|
|
21
129
|
|
|
22
|
-
|
|
130
|
+
await Tone.start();
|
|
131
|
+
Tone.Transport.start();
|
|
132
|
+
channel.startClip(0);
|
|
23
133
|
```
|
|
24
134
|
|
|
135
|
+
### Standalone sample clip (no Session/Channel needed)
|
|
136
|
+
|
|
137
|
+
```js
|
|
138
|
+
import { clip } from 'scribbletune/browser';
|
|
25
139
|
|
|
26
|
-
|
|
140
|
+
await Tone.start();
|
|
141
|
+
Tone.Transport.start();
|
|
27
142
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
143
|
+
const kick = clip({
|
|
144
|
+
sample: 'https://scribbletune.com/sounds/kick.wav',
|
|
145
|
+
pattern: 'x-x-',
|
|
146
|
+
});
|
|
147
|
+
kick.start();
|
|
31
148
|
```
|
|
32
149
|
|
|
33
|
-
|
|
150
|
+
## Core concepts
|
|
34
151
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
152
|
+
### Pattern language
|
|
153
|
+
|
|
154
|
+
Scribbletune uses a simple string notation to describe rhythms:
|
|
155
|
+
|
|
156
|
+
| Char | Meaning |
|
|
157
|
+
|------|---------|
|
|
158
|
+
| `x` | Note on |
|
|
159
|
+
| `-` | Note off (rest) |
|
|
160
|
+
| `_` | Sustain previous note |
|
|
161
|
+
| `R` | Random note (from `randomNotes` pool) |
|
|
162
|
+
| `[]` | Subdivide (e.g. `[xx]` = two notes in one beat) |
|
|
163
|
+
|
|
164
|
+
```js
|
|
165
|
+
'x---x---x-x-x---' // basic kick pattern
|
|
166
|
+
'[xx][xx]x-x-' // hihat with subdivisions
|
|
167
|
+
'x___' // one long sustained note
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### Scales and chords
|
|
171
|
+
|
|
172
|
+
Powered by [harmonics](https://github.com/scribbletune/harmonics):
|
|
173
|
+
|
|
174
|
+
```js
|
|
175
|
+
import { scale, chord, scales, chords } from 'scribbletune';
|
|
176
|
+
|
|
177
|
+
scale('C4 major'); // ['C4', 'D4', 'E4', 'F4', 'G4', 'A4', 'B4']
|
|
178
|
+
chord('CM'); // ['C4', 'E4', 'G4']
|
|
179
|
+
scales(); // list all available scale names
|
|
180
|
+
chords(); // list all available chord names
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### Arpeggios
|
|
184
|
+
|
|
185
|
+
```js
|
|
186
|
+
import { arp } from 'scribbletune';
|
|
187
|
+
|
|
188
|
+
arp({ chords: 'CM FM', count: 4, order: '0123' });
|
|
189
|
+
// ['C4', 'E4', 'G4', 'C5', 'F4', 'A4', 'C5', 'F5']
|
|
38
190
|
```
|
|
39
|
-
This will provide the same API but augment it a bit to support browser based functionality.
|
|
40
191
|
|
|
41
|
-
|
|
192
|
+
### Chord progressions
|
|
193
|
+
|
|
194
|
+
```js
|
|
195
|
+
import { progression, getChordsByProgression } from 'scribbletune';
|
|
196
|
+
|
|
197
|
+
progression('M', 4); // e.g. ['I', 'ii', 'V', 'IV']
|
|
198
|
+
|
|
199
|
+
getChordsByProgression('C4 major', 'I IV V IV');
|
|
200
|
+
// 'CM_4 FM_4 GM_4 FM_4'
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
## Browser API
|
|
204
|
+
|
|
205
|
+
The browser entry point (`scribbletune/browser`) provides everything above plus:
|
|
206
|
+
|
|
207
|
+
### Session and Channel
|
|
208
|
+
|
|
209
|
+
```js
|
|
210
|
+
import { Session } from 'scribbletune/browser';
|
|
211
|
+
|
|
212
|
+
const session = new Session();
|
|
213
|
+
const drums = session.createChannel({
|
|
214
|
+
sample: 'https://scribbletune.com/sounds/kick.wav',
|
|
215
|
+
clips: [
|
|
216
|
+
{ pattern: 'x---x---' },
|
|
217
|
+
{ pattern: 'x-x-x-x-' },
|
|
218
|
+
],
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
const synth = session.createChannel({
|
|
222
|
+
instrument: 'PolySynth',
|
|
223
|
+
clips: [
|
|
224
|
+
{ pattern: 'x-x-', notes: 'C4 E4 G4' },
|
|
225
|
+
],
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
await Tone.start();
|
|
229
|
+
Tone.Transport.start();
|
|
230
|
+
|
|
231
|
+
// Start clips independently
|
|
232
|
+
drums.startClip(0);
|
|
233
|
+
synth.startClip(0);
|
|
234
|
+
|
|
235
|
+
// Switch patterns on the fly
|
|
236
|
+
drums.startClip(1);
|
|
237
|
+
|
|
238
|
+
// Or start a row across all channels
|
|
239
|
+
session.startRow(0);
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
### Channel options
|
|
243
|
+
|
|
244
|
+
Channels accept various sound sources:
|
|
245
|
+
|
|
246
|
+
```js
|
|
247
|
+
// Built-in Tone.js synth (by name)
|
|
248
|
+
{ instrument: 'PolySynth' }
|
|
249
|
+
|
|
250
|
+
// Pre-built Tone.js instrument
|
|
251
|
+
{ instrument: new Tone.FMSynth() }
|
|
252
|
+
|
|
253
|
+
// Audio sample URL
|
|
254
|
+
{ sample: 'https://example.com/kick.wav' }
|
|
255
|
+
|
|
256
|
+
// Multi-sample instrument
|
|
257
|
+
{ samples: { C3: 'piano-c3.wav', D3: 'piano-d3.wav' } }
|
|
258
|
+
|
|
259
|
+
// With effects
|
|
260
|
+
{ instrument: 'PolySynth', effects: ['Chorus', 'Reverb'] }
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
## API reference
|
|
264
|
+
|
|
265
|
+
| Export | Description |
|
|
266
|
+
|--------|-------------|
|
|
267
|
+
| `clip(params)` | Create a clip — returns note objects (Node.js) or a Tone.Sequence (browser) |
|
|
268
|
+
| `midi(clip, filename?)` | Export a clip to a MIDI file |
|
|
269
|
+
| `scale(name)` | Get notes of a scale, e.g. `'C4 minor'` |
|
|
270
|
+
| `chord(name)` | Get notes of a chord, e.g. `'CM'` |
|
|
271
|
+
| `scales()` | List all available scale names |
|
|
272
|
+
| `chords()` | List all available chord names |
|
|
273
|
+
| `arp(params)` | Generate arpeggiated note sequences |
|
|
274
|
+
| `progression(type, count)` | Generate a chord progression (`'M'` or `'m'`) |
|
|
275
|
+
| `getChordsByProgression(scale, degrees)` | Convert Roman numeral degrees to chord names |
|
|
276
|
+
| `getChordDegrees(mode)` | Get Roman numeral degrees for a mode |
|
|
277
|
+
| `Session` | _(browser only)_ Manage multiple channels and coordinate playback |
|
|
278
|
+
|
|
279
|
+
## Development
|
|
280
|
+
|
|
281
|
+
```bash
|
|
282
|
+
npm install # install dependencies
|
|
283
|
+
npm test # run tests
|
|
284
|
+
npm run build # build with tsup
|
|
285
|
+
npm run lint # check with biome
|
|
286
|
+
npm run dev # build in watch mode
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
## License
|
|
290
|
+
|
|
291
|
+
MIT
|
|
292
|
+
|
|
293
|
+
---
|
|
294
|
+
|
|
295
|
+
[scribbletune.com](https://scribbletune.com) | [Soundcloud](https://soundcloud.com/scribbletune)
|
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
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "scribbletune",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.5.0",
|
|
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",
|