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.
- package/README.md +276 -22
- package/dist/browser.cjs +1183 -0
- package/dist/browser.cjs.map +1 -0
- package/dist/browser.js +1135 -1
- package/dist/browser.js.map +1 -1
- package/dist/cli.cjs +813 -0
- package/dist/{index.mjs → index.cjs} +225 -169
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +323 -0
- package/dist/index.d.ts +303 -350
- package/dist/index.js +524 -1
- package/dist/index.js.map +1 -1
- package/dist/scribbletune.global.js +1944 -0
- package/dist/scribbletune.global.js.map +1 -0
- package/package.json +32 -40
- package/dist/lib/arp.d.ts +0 -10
- package/dist/lib/browser-clip.d.ts +0 -14
- package/dist/lib/browser-index.d.ts +0 -7
- package/dist/lib/channel.d.ts +0 -61
- package/dist/lib/clip.d.ts +0 -2
- package/dist/lib/index.d.ts +0 -7
- package/dist/lib/midi.d.ts +0 -11
- package/dist/lib/progression.d.ts +0 -25
- package/dist/lib/session.d.ts +0 -14
- package/dist/lib/types/arp-params.d.ts +0 -6
- package/dist/lib/types/channel-params.d.ts +0 -92
- package/dist/lib/types/channel-pattern.d.ts +0 -24
- package/dist/lib/types/clip-params.d.ts +0 -104
- package/dist/lib/types/event-fn.d.ts +0 -7
- package/dist/lib/types/index.d.ts +0 -14
- package/dist/lib/types/note-object.d.ts +0 -6
- package/dist/lib/types/nvp.d.ts +0 -4
- package/dist/lib/types/play-params.d.ts +0 -15
- package/dist/lib/types/player-observer-fn.d.ts +0 -6
- package/dist/lib/types/progression-scale.d.ts +0 -2
- package/dist/lib/types/seq-fn.d.ts +0 -2
- package/dist/lib/types/sizzle-style.d.ts +0 -2
- package/dist/lib/types/synth-params.d.ts +0 -20
- package/dist/lib/types/tpd.d.ts +0 -15
- package/dist/lib/utils.d.ts +0 -56
- package/dist/scribbletune.js +0 -2
- 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
|
+
});
|