scribbletune 5.1.0 → 5.2.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/dist/browser.js +1 -1
- package/dist/browser.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +515 -0
- package/dist/lib/arp.d.ts +10 -0
- package/dist/lib/browser-clip.d.ts +14 -0
- package/dist/lib/browser-index.d.ts +7 -0
- package/dist/lib/channel.d.ts +61 -0
- package/dist/lib/clip.d.ts +2 -0
- package/dist/lib/index.d.ts +7 -0
- package/dist/lib/midi.d.ts +11 -0
- package/dist/lib/progression.d.ts +25 -0
- package/dist/lib/session.d.ts +14 -0
- package/dist/lib/types/arp-params.d.ts +6 -0
- package/dist/lib/types/channel-params.d.ts +92 -0
- package/dist/lib/types/channel-pattern.d.ts +24 -0
- package/dist/lib/types/clip-params.d.ts +104 -0
- package/dist/lib/types/event-fn.d.ts +7 -0
- package/dist/lib/types/index.d.ts +14 -0
- package/dist/lib/types/note-object.d.ts +6 -0
- package/dist/lib/types/nvp.d.ts +4 -0
- package/dist/lib/types/play-params.d.ts +15 -0
- package/dist/lib/types/player-observer-fn.d.ts +6 -0
- package/dist/lib/types/progression-scale.d.ts +2 -0
- package/dist/lib/types/seq-fn.d.ts +2 -0
- package/dist/lib/types/sizzle-style.d.ts +2 -0
- package/dist/lib/types/synth-params.d.ts +20 -0
- package/dist/lib/types/tpd.d.ts +15 -0
- package/dist/lib/utils.d.ts +56 -0
- package/dist/scribbletune.js +1 -1
- package/dist/scribbletune.js.map +1 -1
- package/package.json +8 -10
- package/dist/max.js +0 -2
- package/dist/max.js.map +0 -1
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,515 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { scales, chords, scale as scale2, chord as chord2 } from "harmonics";
|
|
3
|
+
|
|
4
|
+
// src/utils.ts
|
|
5
|
+
import { chord, inlineChord } from "harmonics";
|
|
6
|
+
var isNote = (str) => /^[a-gA-G](?:#|b)?\d$/.test(str);
|
|
7
|
+
var expandStr = (str) => {
|
|
8
|
+
str = JSON.stringify(str.split(""));
|
|
9
|
+
str = str.replace(/,"\[",/g, ", [");
|
|
10
|
+
str = str.replace(/"\[",/g, "[");
|
|
11
|
+
str = str.replace(/,"\]"/g, "]");
|
|
12
|
+
return JSON.parse(str);
|
|
13
|
+
};
|
|
14
|
+
var shuffle = (arr, fullShuffle = true) => {
|
|
15
|
+
const lastIndex = arr.length - 1;
|
|
16
|
+
arr.forEach((el, idx) => {
|
|
17
|
+
if (idx >= lastIndex) {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
const rnd = fullShuffle ? (
|
|
21
|
+
// Pick random number from idx+1 to lastIndex (Modified algorithm, (N-1)! combinations)
|
|
22
|
+
// Math.random -> [0, 1) -> [0, lastIndex-idx ) --floor-> [0, lastIndex-idx-1]
|
|
23
|
+
// rnd = [0, lastIndex-idx-1] + 1 + idx = [1 + idx, lastIndex]
|
|
24
|
+
// (Original algorithm would pick rnd = [idx, lastIndex], thus any element could arrive back into its slot)
|
|
25
|
+
Math.floor(Math.random() * (lastIndex - idx)) + 1 + idx
|
|
26
|
+
) : (
|
|
27
|
+
// Pick random number from idx to lastIndex (Unmodified Richard Durstenfeld, N! combinations)
|
|
28
|
+
Math.floor(Math.random() * (lastIndex + 1 - idx)) + idx
|
|
29
|
+
);
|
|
30
|
+
arr[idx] = arr[rnd];
|
|
31
|
+
arr[rnd] = el;
|
|
32
|
+
});
|
|
33
|
+
return arr;
|
|
34
|
+
};
|
|
35
|
+
var pickOne = (arr) => arr.length > 1 ? arr[Math.round(Math.random())] : arr[0];
|
|
36
|
+
var dice = () => !!Math.round(Math.random());
|
|
37
|
+
var errorHasMessage = (x) => {
|
|
38
|
+
return typeof x.message === "string";
|
|
39
|
+
};
|
|
40
|
+
var convertChordToNotes = (el) => {
|
|
41
|
+
let c1;
|
|
42
|
+
let c2;
|
|
43
|
+
let e1;
|
|
44
|
+
let e2;
|
|
45
|
+
try {
|
|
46
|
+
c1 = inlineChord(el);
|
|
47
|
+
} catch (e) {
|
|
48
|
+
e1 = e;
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
c2 = chord(el.replace(/_/g, " "));
|
|
52
|
+
} catch (e) {
|
|
53
|
+
e2 = e;
|
|
54
|
+
}
|
|
55
|
+
if (!e1 && !e2) {
|
|
56
|
+
if (c1.toString() !== c2.toString()) {
|
|
57
|
+
throw new Error(`Chord ${el} cannot decode, guessing ${c1} or ${c2}`);
|
|
58
|
+
}
|
|
59
|
+
return c1;
|
|
60
|
+
}
|
|
61
|
+
if (!e1) {
|
|
62
|
+
return c1;
|
|
63
|
+
}
|
|
64
|
+
if (!e2) {
|
|
65
|
+
return c2;
|
|
66
|
+
}
|
|
67
|
+
return chord(el);
|
|
68
|
+
};
|
|
69
|
+
var convertChordsToNotes = (el) => {
|
|
70
|
+
if (typeof el === "string" && isNote(el)) {
|
|
71
|
+
return [el];
|
|
72
|
+
}
|
|
73
|
+
if (Array.isArray(el)) {
|
|
74
|
+
el.forEach((n) => {
|
|
75
|
+
if (Array.isArray(n)) {
|
|
76
|
+
n.forEach((n1) => {
|
|
77
|
+
if (typeof n1 !== "string" || !isNote(n1)) {
|
|
78
|
+
throw new TypeError("array of arrays must comprise valid notes");
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
} else if (typeof n !== "string" || !isNote(n)) {
|
|
82
|
+
throw new TypeError("array must comprise valid notes");
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
return el;
|
|
86
|
+
}
|
|
87
|
+
if (!Array.isArray(el)) {
|
|
88
|
+
const c = convertChordToNotes(el);
|
|
89
|
+
if (c && c.length) {
|
|
90
|
+
return c;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
throw new Error(`Chord ${el} not found`);
|
|
94
|
+
};
|
|
95
|
+
var randomInt = (num = 1) => Math.round(Math.random() * num);
|
|
96
|
+
|
|
97
|
+
// src/clip.ts
|
|
98
|
+
var getDefaultParams = () => ({
|
|
99
|
+
notes: ["C4"],
|
|
100
|
+
pattern: "x",
|
|
101
|
+
shuffle: false,
|
|
102
|
+
sizzle: false,
|
|
103
|
+
sizzleReps: 1,
|
|
104
|
+
arpegiate: false,
|
|
105
|
+
subdiv: "4n",
|
|
106
|
+
amp: 100,
|
|
107
|
+
accentLow: 70,
|
|
108
|
+
randomNotes: null,
|
|
109
|
+
offlineRendering: false
|
|
110
|
+
});
|
|
111
|
+
var hdr = {
|
|
112
|
+
"1m": 2048,
|
|
113
|
+
"2m": 4096,
|
|
114
|
+
"3m": 6144,
|
|
115
|
+
"4m": 8192,
|
|
116
|
+
"1n": 512,
|
|
117
|
+
"2n": 256,
|
|
118
|
+
"4n": 128,
|
|
119
|
+
"8n": 64,
|
|
120
|
+
"16n": 32
|
|
121
|
+
};
|
|
122
|
+
var clip = (params) => {
|
|
123
|
+
params = { ...getDefaultParams(), ...params || {} };
|
|
124
|
+
if (typeof params.notes === "string") {
|
|
125
|
+
params.notes = params.notes.replace(/\s{2,}/g, " ");
|
|
126
|
+
params.notes = params.notes.split(" ");
|
|
127
|
+
}
|
|
128
|
+
params.notes = params.notes ? params.notes.map(convertChordsToNotes) : [];
|
|
129
|
+
if (/[^x\-_[\]R]/.test(params.pattern)) {
|
|
130
|
+
throw new TypeError(
|
|
131
|
+
`pattern can only comprise x - _ [ ] R, found ${params.pattern}`
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
if (params.shuffle) {
|
|
135
|
+
params.notes = shuffle(params.notes);
|
|
136
|
+
}
|
|
137
|
+
if (params.randomNotes && typeof params.randomNotes === "string") {
|
|
138
|
+
params.randomNotes = params.randomNotes.replace(/\s{2,}/g, " ").split(/\s/);
|
|
139
|
+
}
|
|
140
|
+
if (params.randomNotes) {
|
|
141
|
+
params.randomNotes = params.randomNotes.map(
|
|
142
|
+
convertChordsToNotes
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
const clipNotes = [];
|
|
146
|
+
let step = 0;
|
|
147
|
+
const recursivelyApplyPatternToNotes = (patternArr, length, parentNoteLength) => {
|
|
148
|
+
let totalLength = 0;
|
|
149
|
+
patternArr.forEach((char, idx) => {
|
|
150
|
+
var _a;
|
|
151
|
+
if (typeof char === "string") {
|
|
152
|
+
let note = null;
|
|
153
|
+
if (char === "-") {
|
|
154
|
+
} else if (char === "R" && randomInt() && // Use 1/2 probability for R to pick from param.notes
|
|
155
|
+
params.randomNotes && params.randomNotes.length > 0) {
|
|
156
|
+
note = params.randomNotes[randomInt(params.randomNotes.length - 1)];
|
|
157
|
+
} else if (params.notes) {
|
|
158
|
+
note = params.notes[step];
|
|
159
|
+
}
|
|
160
|
+
if (char === "x" || char === "R") {
|
|
161
|
+
step++;
|
|
162
|
+
}
|
|
163
|
+
if (char === "x" || char === "-" || char === "R") {
|
|
164
|
+
clipNotes.push({
|
|
165
|
+
note,
|
|
166
|
+
length,
|
|
167
|
+
level: char === "R" && !params.randomNotes ? params.accentLow : params.amp
|
|
168
|
+
});
|
|
169
|
+
totalLength += length;
|
|
170
|
+
}
|
|
171
|
+
if (char === "_" && clipNotes.length) {
|
|
172
|
+
clipNotes[clipNotes.length - 1].length += length;
|
|
173
|
+
totalLength += length;
|
|
174
|
+
}
|
|
175
|
+
if (parentNoteLength && totalLength !== parentNoteLength && idx === patternArr.length - 1) {
|
|
176
|
+
const diff = Math.abs(
|
|
177
|
+
parentNoteLength - totalLength
|
|
178
|
+
);
|
|
179
|
+
const lastClipNote = clipNotes[clipNotes.length - 1];
|
|
180
|
+
if (lastClipNote.length > diff) {
|
|
181
|
+
lastClipNote.length = lastClipNote.length - diff;
|
|
182
|
+
} else {
|
|
183
|
+
lastClipNote.length = lastClipNote.length + diff;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
if (step === ((_a = params.notes) == null ? void 0 : _a.length)) {
|
|
187
|
+
step = 0;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
if (Array.isArray(char)) {
|
|
191
|
+
let isTriplet = false;
|
|
192
|
+
if (char.length % 2 !== 0 || length % 2 !== 0) {
|
|
193
|
+
isTriplet = true;
|
|
194
|
+
}
|
|
195
|
+
recursivelyApplyPatternToNotes(
|
|
196
|
+
char,
|
|
197
|
+
Math.round(length / char.length),
|
|
198
|
+
isTriplet && length
|
|
199
|
+
);
|
|
200
|
+
totalLength += length;
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
};
|
|
204
|
+
recursivelyApplyPatternToNotes(
|
|
205
|
+
expandStr(params.pattern),
|
|
206
|
+
hdr[params.subdiv] || hdr["4n"],
|
|
207
|
+
false
|
|
208
|
+
);
|
|
209
|
+
if (params.sizzle) {
|
|
210
|
+
const volArr = [];
|
|
211
|
+
const style = params.sizzle === true ? "sin" : params.sizzle;
|
|
212
|
+
const beats = clipNotes.length;
|
|
213
|
+
const amp = params.amp;
|
|
214
|
+
const sizzleReps = params.sizzleReps;
|
|
215
|
+
const stepLevel = amp / (beats / sizzleReps);
|
|
216
|
+
if (style === "sin" || style === "cos") {
|
|
217
|
+
for (let i = 0; i < beats; i++) {
|
|
218
|
+
const level = Math[style](i * Math.PI / (beats / sizzleReps)) * amp;
|
|
219
|
+
volArr.push(Math.round(Math.abs(level)));
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
if (style === "rampUp") {
|
|
223
|
+
let level = 0;
|
|
224
|
+
for (let i = 0; i < beats; i++) {
|
|
225
|
+
if (i % (beats / sizzleReps) === 0) {
|
|
226
|
+
level = 0;
|
|
227
|
+
} else {
|
|
228
|
+
level = level + stepLevel;
|
|
229
|
+
}
|
|
230
|
+
volArr.push(Math.round(Math.abs(level)));
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
if (style === "rampDown") {
|
|
234
|
+
let level = amp;
|
|
235
|
+
for (let i = 0; i < beats; i++) {
|
|
236
|
+
if (i % (beats / sizzleReps) === 0) {
|
|
237
|
+
level = amp;
|
|
238
|
+
} else {
|
|
239
|
+
level = level - stepLevel;
|
|
240
|
+
}
|
|
241
|
+
volArr.push(Math.round(Math.abs(level)));
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
for (let i = 0; i < volArr.length; i++) {
|
|
245
|
+
clipNotes[i].level = volArr[i] ? volArr[i] : 1;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
if (params.accent) {
|
|
249
|
+
if (/[^x-]/.test(params.accent)) {
|
|
250
|
+
throw new TypeError("Accent can only have x and - characters");
|
|
251
|
+
}
|
|
252
|
+
let a = 0;
|
|
253
|
+
for (const clipNote of clipNotes) {
|
|
254
|
+
let level = params.accent[a] === "x" ? params.amp : params.accentLow;
|
|
255
|
+
if (params.sizzle) {
|
|
256
|
+
level = (clipNote.level + level) / 2;
|
|
257
|
+
}
|
|
258
|
+
clipNote.level = Math.round(level);
|
|
259
|
+
a = a + 1;
|
|
260
|
+
if (a === params.accent.length) {
|
|
261
|
+
a = 0;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return clipNotes;
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
// src/progression.ts
|
|
269
|
+
import { scale } from "harmonics";
|
|
270
|
+
var getChordDegrees = (mode) => {
|
|
271
|
+
const theRomans = {
|
|
272
|
+
ionian: ["I", "ii", "iii", "IV", "V", "vi", "vii\xB0"],
|
|
273
|
+
dorian: ["i", "ii", "III", "IV", "v", "vi\xB0", "VII"],
|
|
274
|
+
phrygian: ["i", "II", "III", "iv", "v\xB0", "VI", "vii"],
|
|
275
|
+
lydian: ["I", "II", "iii", "iv\xB0", "V", "vi", "vii"],
|
|
276
|
+
mixolydian: ["I", "ii", "iii\xB0", "IV", "v", "vi", "VII"],
|
|
277
|
+
aeolian: ["i", "ii\xB0", "III", "iv", "v", "VI", "VII"],
|
|
278
|
+
locrian: ["i\xB0", "II", "iii", "iv", "V", "VI", "vii"],
|
|
279
|
+
"melodic minor": ["i", "ii", "III+", "IV", "V", "vi\xB0", "vii\xB0"],
|
|
280
|
+
"harmonic minor": ["i", "ii\xB0", "III+", "iv", "V", "VI", "vii\xB0"]
|
|
281
|
+
};
|
|
282
|
+
theRomans.major = theRomans.ionian;
|
|
283
|
+
theRomans.minor = theRomans.aeolian;
|
|
284
|
+
return theRomans[mode] || [];
|
|
285
|
+
};
|
|
286
|
+
var idxByDegree = {
|
|
287
|
+
i: 0,
|
|
288
|
+
ii: 1,
|
|
289
|
+
iii: 2,
|
|
290
|
+
iv: 3,
|
|
291
|
+
v: 4,
|
|
292
|
+
vi: 5,
|
|
293
|
+
vii: 6
|
|
294
|
+
};
|
|
295
|
+
var getChordName = (roman) => {
|
|
296
|
+
const str = roman.replace(/\W/g, "");
|
|
297
|
+
let prefix = "M";
|
|
298
|
+
if (str.toLowerCase() === str) {
|
|
299
|
+
prefix = "m";
|
|
300
|
+
}
|
|
301
|
+
if (roman.indexOf("\xB0") > -1) {
|
|
302
|
+
return prefix + "7b5";
|
|
303
|
+
}
|
|
304
|
+
if (roman.indexOf("+") > -1) {
|
|
305
|
+
return prefix + "#5";
|
|
306
|
+
}
|
|
307
|
+
if (roman.indexOf("7") > -1) {
|
|
308
|
+
return prefix === "M" ? "maj7" : "m7";
|
|
309
|
+
}
|
|
310
|
+
return prefix;
|
|
311
|
+
};
|
|
312
|
+
var getChordsByProgression = (noteOctaveScale, chordDegress) => {
|
|
313
|
+
const noteOctaveScaleArr = noteOctaveScale.split(" ");
|
|
314
|
+
if (!noteOctaveScaleArr[0].match(/\d/)) {
|
|
315
|
+
noteOctaveScaleArr[0] += "4";
|
|
316
|
+
noteOctaveScale = noteOctaveScaleArr.join(" ");
|
|
317
|
+
}
|
|
318
|
+
const mode = scale(noteOctaveScale);
|
|
319
|
+
const chordDegreesArr = chordDegress.replace(/\s*,+\s*/g, " ").split(" ");
|
|
320
|
+
const chordFamily = chordDegreesArr.map((roman) => {
|
|
321
|
+
const chordName = getChordName(roman);
|
|
322
|
+
const scaleId = idxByDegree[roman.replace(/\W|\d/g, "").toLowerCase()];
|
|
323
|
+
const note = mode[scaleId];
|
|
324
|
+
const oct = note.replace(/\D+/, "");
|
|
325
|
+
return note.replace(/\d/, "") + chordName + "_" + oct;
|
|
326
|
+
});
|
|
327
|
+
return chordFamily.toString().replace(/,/g, " ");
|
|
328
|
+
};
|
|
329
|
+
var getProgFactory = ({ T, P, D }) => {
|
|
330
|
+
return (count = 4) => {
|
|
331
|
+
const chords2 = [];
|
|
332
|
+
chords2.push(pickOne(T));
|
|
333
|
+
let i = 1;
|
|
334
|
+
if (i < count - 1) {
|
|
335
|
+
chords2.push(pickOne(P));
|
|
336
|
+
i++;
|
|
337
|
+
}
|
|
338
|
+
if (i < count - 1 && dice()) {
|
|
339
|
+
chords2.push(pickOne(P));
|
|
340
|
+
i++;
|
|
341
|
+
}
|
|
342
|
+
if (i < count - 1) {
|
|
343
|
+
chords2.push(pickOne(D));
|
|
344
|
+
i++;
|
|
345
|
+
}
|
|
346
|
+
if (i < count - 1) {
|
|
347
|
+
chords2.push(pickOne(P));
|
|
348
|
+
i++;
|
|
349
|
+
}
|
|
350
|
+
if (i < count - 1) {
|
|
351
|
+
chords2.push(pickOne(D));
|
|
352
|
+
i++;
|
|
353
|
+
}
|
|
354
|
+
if (i < count - 1 && dice()) {
|
|
355
|
+
chords2.push(pickOne(P));
|
|
356
|
+
i++;
|
|
357
|
+
}
|
|
358
|
+
while (i < count) {
|
|
359
|
+
chords2.push(pickOne(D));
|
|
360
|
+
i++;
|
|
361
|
+
}
|
|
362
|
+
return chords2;
|
|
363
|
+
};
|
|
364
|
+
};
|
|
365
|
+
var M = getProgFactory({ T: ["I", "vi"], P: ["ii", "IV"], D: ["V"] });
|
|
366
|
+
var m = getProgFactory({ T: ["i", "VI"], P: ["ii", "iv"], D: ["V"] });
|
|
367
|
+
var progression = (scaleType, count = 4) => {
|
|
368
|
+
if (scaleType === "major" || scaleType === "M") {
|
|
369
|
+
return M(count);
|
|
370
|
+
}
|
|
371
|
+
if (scaleType === "minor" || scaleType === "m") {
|
|
372
|
+
return m(count);
|
|
373
|
+
}
|
|
374
|
+
return [];
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
// src/arp.ts
|
|
378
|
+
import { inlineChord as inlineChord2 } from "harmonics";
|
|
379
|
+
var DEFAULT_OCTAVE = 4;
|
|
380
|
+
var fillArr = (arr, len) => {
|
|
381
|
+
const bumpOctave = (el) => {
|
|
382
|
+
if (!el) {
|
|
383
|
+
throw new Error("Empty element");
|
|
384
|
+
}
|
|
385
|
+
const note = el.replace(/\d/, "");
|
|
386
|
+
const oct = el.replace(/\D/g, "") || DEFAULT_OCTAVE;
|
|
387
|
+
if (!note) {
|
|
388
|
+
throw new Error("Incorrect note");
|
|
389
|
+
}
|
|
390
|
+
return note + (+oct + 1);
|
|
391
|
+
};
|
|
392
|
+
const arr1 = arr.map(bumpOctave);
|
|
393
|
+
const arr2 = arr1.map(bumpOctave);
|
|
394
|
+
const finalArr = [...arr, ...arr1, ...arr2];
|
|
395
|
+
return finalArr.slice(0, len);
|
|
396
|
+
};
|
|
397
|
+
var arp = (chordsOrParams) => {
|
|
398
|
+
let finalArr = [];
|
|
399
|
+
const params = {
|
|
400
|
+
count: 4,
|
|
401
|
+
order: "0123",
|
|
402
|
+
chords: ""
|
|
403
|
+
};
|
|
404
|
+
if (typeof chordsOrParams === "string") {
|
|
405
|
+
params.chords = chordsOrParams;
|
|
406
|
+
} else {
|
|
407
|
+
if (chordsOrParams.order && chordsOrParams.order.match(/\D/g)) {
|
|
408
|
+
throw new TypeError("Invalid value for order");
|
|
409
|
+
}
|
|
410
|
+
if (chordsOrParams.count > 8 || chordsOrParams.count < 2) {
|
|
411
|
+
throw new TypeError("Invalid value for count");
|
|
412
|
+
}
|
|
413
|
+
if (chordsOrParams.count && !chordsOrParams.order) {
|
|
414
|
+
params.order = Array.from(Array(chordsOrParams.count).keys()).join("");
|
|
415
|
+
}
|
|
416
|
+
Object.assign(params, chordsOrParams);
|
|
417
|
+
}
|
|
418
|
+
if (typeof params.chords === "string") {
|
|
419
|
+
const chordsArr = params.chords.split(" ");
|
|
420
|
+
chordsArr.forEach((c, i) => {
|
|
421
|
+
try {
|
|
422
|
+
const filledArr = fillArr(inlineChord2(c), params.count);
|
|
423
|
+
const reorderedArr = params.order.split("").map((idx) => filledArr[idx]);
|
|
424
|
+
finalArr = [...finalArr, ...reorderedArr];
|
|
425
|
+
} catch (e) {
|
|
426
|
+
throw new Error(
|
|
427
|
+
`Cannot decode chord ${i + 1} "${c}" in given "${params.chords}"`
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
});
|
|
431
|
+
} else if (Array.isArray(params.chords)) {
|
|
432
|
+
params.chords.forEach((c, i) => {
|
|
433
|
+
try {
|
|
434
|
+
const filledArr = fillArr(c, params.count);
|
|
435
|
+
const reorderedArr = params.order.split("").map((idx) => filledArr[idx]);
|
|
436
|
+
finalArr = [...finalArr, ...reorderedArr];
|
|
437
|
+
} catch (e) {
|
|
438
|
+
throw new Error(
|
|
439
|
+
`${errorHasMessage(e) ? e.message : e} in chord ${i + 1} "${c}"`
|
|
440
|
+
);
|
|
441
|
+
}
|
|
442
|
+
});
|
|
443
|
+
} else {
|
|
444
|
+
throw new TypeError("Invalid value for chords");
|
|
445
|
+
}
|
|
446
|
+
return finalArr;
|
|
447
|
+
};
|
|
448
|
+
|
|
449
|
+
// src/midi.ts
|
|
450
|
+
import fs from "fs";
|
|
451
|
+
import * as jsmidgen from "jsmidgen";
|
|
452
|
+
var midi = (notes, fileName = "music.mid", bpm) => {
|
|
453
|
+
const file = createFileFromNotes(notes, bpm);
|
|
454
|
+
const bytes = file.toBytes();
|
|
455
|
+
if (fileName === null) {
|
|
456
|
+
return bytes;
|
|
457
|
+
}
|
|
458
|
+
if (!fileName.endsWith(".mid")) {
|
|
459
|
+
fileName = fileName + ".mid";
|
|
460
|
+
}
|
|
461
|
+
if (typeof window !== "undefined" && window.URL && window.URL.createObjectURL) {
|
|
462
|
+
return createDownloadLink(bytes, fileName);
|
|
463
|
+
}
|
|
464
|
+
fs.writeFileSync(fileName, bytes, "binary");
|
|
465
|
+
console.log(`MIDI file generated: ${fileName}.`);
|
|
466
|
+
};
|
|
467
|
+
var createDownloadLink = (b, fileName) => {
|
|
468
|
+
const bytes = new Uint8Array(b.length);
|
|
469
|
+
for (let i = 0; i < b.length; i++) {
|
|
470
|
+
const ascii = b.charCodeAt(i);
|
|
471
|
+
bytes[i] = ascii;
|
|
472
|
+
}
|
|
473
|
+
const blob = new Blob([bytes], { type: "audio/midi" });
|
|
474
|
+
const link = document.createElement("a");
|
|
475
|
+
link.href = typeof window !== "undefined" && typeof window.URL !== "undefined" && typeof window.URL.createObjectURL !== "undefined" && window.URL.createObjectURL(blob) || "";
|
|
476
|
+
link.download = fileName;
|
|
477
|
+
link.innerText = "Download MIDI file";
|
|
478
|
+
return link;
|
|
479
|
+
};
|
|
480
|
+
var createFileFromNotes = (notes, bpm) => {
|
|
481
|
+
const file = new jsmidgen.File();
|
|
482
|
+
const track = new jsmidgen.Track();
|
|
483
|
+
if (typeof bpm === "number") {
|
|
484
|
+
track.setTempo(bpm);
|
|
485
|
+
}
|
|
486
|
+
file.addTrack(track);
|
|
487
|
+
for (const noteObj of notes) {
|
|
488
|
+
const level = noteObj.level || 127;
|
|
489
|
+
if (noteObj.note) {
|
|
490
|
+
if (typeof noteObj.note === "string") {
|
|
491
|
+
track.noteOn(0, noteObj.note, noteObj.length, level);
|
|
492
|
+
track.noteOff(0, noteObj.note, noteObj.length, level);
|
|
493
|
+
} else {
|
|
494
|
+
track.addChord(0, noteObj.note, noteObj.length, level);
|
|
495
|
+
}
|
|
496
|
+
} else {
|
|
497
|
+
track.noteOff(0, "", noteObj.length);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
return file;
|
|
501
|
+
};
|
|
502
|
+
export {
|
|
503
|
+
arp,
|
|
504
|
+
chord2 as chord,
|
|
505
|
+
chords,
|
|
506
|
+
clip,
|
|
507
|
+
getChordDegrees,
|
|
508
|
+
getChordsByProgression,
|
|
509
|
+
midi,
|
|
510
|
+
scale2 as mode,
|
|
511
|
+
scales as modes,
|
|
512
|
+
progression,
|
|
513
|
+
scale2 as scale,
|
|
514
|
+
scales
|
|
515
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ArpParams } from './types';
|
|
2
|
+
/**
|
|
3
|
+
*
|
|
4
|
+
* @param chordsOrParams a string that denotes space (comma?) separated chords to be used or an object with additional properties
|
|
5
|
+
* By default, if this is a string, the count of notes generated is 8 and the order is ascending.
|
|
6
|
+
* For instance arp('CM FM') will result in an array of notes [C4, E4, G4, F4, A4, C4, C5, E5]
|
|
7
|
+
* You can even provide Params as an object.
|
|
8
|
+
* For e.g. arp({count: 8, order: '10325476', chords: 'FM_4 Gm7b5_4 AbM_4 Bbm_4 Cm_5 DbM_5 EbM_5})
|
|
9
|
+
*/
|
|
10
|
+
export declare const arp: (chordsOrParams: string | ArpParams) => string[];
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Channel } from './channel';
|
|
2
|
+
import type { ClipParams } from './types';
|
|
3
|
+
export declare const getNote: (el: string, params: ClipParams, counter: number) => string | (string | string[])[];
|
|
4
|
+
export declare const getDuration: (params: ClipParams, counter: number) => string | number | undefined;
|
|
5
|
+
export declare const recursivelyApplyPatternToDurations: (patternArr: string[], length: number, durations?: number[]) => number[];
|
|
6
|
+
export declare const totalPatternDuration: (pattern: string, subdivOrLength: string | number) => number;
|
|
7
|
+
export declare const renderingDuration: (pattern: string, subdivOrLength: string | number, notes: string | (string | string[])[], randomNotes: undefined | null | string | (string | string[])[]) => number;
|
|
8
|
+
/**
|
|
9
|
+
* @param {Object}
|
|
10
|
+
* @return {Tone.js Sequence Object}
|
|
11
|
+
* Take a object literal that may have a Tone.js player OR instrument
|
|
12
|
+
* or simply a sample or synth with a pattern and return a Tone.js sequence
|
|
13
|
+
*/
|
|
14
|
+
export declare const clip: (params: ClipParams, channel: Channel) => any;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { scales, chords, scale, chord } from 'harmonics';
|
|
2
|
+
import { clip } from './browser-clip';
|
|
3
|
+
import { getChordDegrees, getChordsByProgression, progression } from './progression';
|
|
4
|
+
import { arp } from './arp';
|
|
5
|
+
import { Session } from './session';
|
|
6
|
+
export * from './types';
|
|
7
|
+
export { scale, scale as mode, scales, scales as modes, chord, chords, clip, getChordDegrees, getChordsByProgression, progression, arp, Session, };
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { ChannelParams, ClipParams, SeqFn } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Channel
|
|
4
|
+
* A channel is made up of a Tone.js Player/Instrument, one or more
|
|
5
|
+
* Tone.js sequences (known as clips in Scribbletune)
|
|
6
|
+
* & optionally a set of effects (with or without presets)
|
|
7
|
+
*
|
|
8
|
+
* API:
|
|
9
|
+
* clips -> Get all clips for this channel
|
|
10
|
+
* addClip -> Add a new clip to the channel
|
|
11
|
+
* startClip -> Start a clip at the provided index
|
|
12
|
+
* stopClip -> Stop a clip at the provided index
|
|
13
|
+
* activeClipIdx -> Get the clip that is currently playing
|
|
14
|
+
*/
|
|
15
|
+
export declare class Channel {
|
|
16
|
+
idx: number | string;
|
|
17
|
+
name: string;
|
|
18
|
+
activePatternIdx: number;
|
|
19
|
+
channelClips: any;
|
|
20
|
+
clipNoteCount: number;
|
|
21
|
+
instrument: any;
|
|
22
|
+
external: any;
|
|
23
|
+
initializerTask: Promise<void>;
|
|
24
|
+
hasLoaded: boolean;
|
|
25
|
+
hasFailed: boolean | Error;
|
|
26
|
+
private eventCbFn;
|
|
27
|
+
private playerCbFn;
|
|
28
|
+
private counterResetTask;
|
|
29
|
+
constructor(params: ChannelParams);
|
|
30
|
+
static setTransportTempo(valueBpm: number): void;
|
|
31
|
+
static startTransport(): void;
|
|
32
|
+
static stopTransport(deleteEvents?: boolean): void;
|
|
33
|
+
setVolume(volume: number): void;
|
|
34
|
+
startClip(idx: number, position?: number | string): void;
|
|
35
|
+
stopClip(idx: number, position?: number | string): void;
|
|
36
|
+
addClip(clipParams: ClipParams, idx?: number): void;
|
|
37
|
+
/**
|
|
38
|
+
* @param {Object} ClipParams clip parameters
|
|
39
|
+
* @return {Function} function that can be used as the callback in Tone.Sequence https://tonejs.github.io/docs/Sequence
|
|
40
|
+
*/
|
|
41
|
+
getSeqFn(params: ClipParams): SeqFn;
|
|
42
|
+
private eventCb;
|
|
43
|
+
private playerCb;
|
|
44
|
+
/**
|
|
45
|
+
* Check Tone.js object loaded state and either invoke `resolve` right away, or attach to and wait using Tone onload cb.
|
|
46
|
+
* It's an ugly hack that reaches into Tone's internal ._buffers or ._buffer to insert itself into .onload() callback.
|
|
47
|
+
* Tone has different ways to pull the onload callback from within to the API, so this implementation is very brittle.
|
|
48
|
+
* The sole reason for its existence is to handle async loaded state of Tone instruments that we allow to pass in from outside.
|
|
49
|
+
* If that option is eliminated, then this hacky function can be killed (or re-implemented via public onload API)
|
|
50
|
+
* @param toneObject Tone.js object (will work with non-Tone objects that have same loaded/onload properties)
|
|
51
|
+
* @param resolve onload callback
|
|
52
|
+
*/
|
|
53
|
+
private checkToneObjLoaded;
|
|
54
|
+
private recreateToneObjectInContext;
|
|
55
|
+
private initOutputProducer;
|
|
56
|
+
private initInstrument;
|
|
57
|
+
private adjustInstrument;
|
|
58
|
+
private initEffects;
|
|
59
|
+
get clips(): any[];
|
|
60
|
+
get activeClipIdx(): number;
|
|
61
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { scales, chords, scale, chord } from 'harmonics';
|
|
2
|
+
import { clip } from './clip';
|
|
3
|
+
import { getChordDegrees, getChordsByProgression, progression } from './progression';
|
|
4
|
+
import { arp } from './arp';
|
|
5
|
+
import { midi } from './midi';
|
|
6
|
+
export * from './types';
|
|
7
|
+
export { scale, scale as mode, scales, scales as modes, chord, chords, clip, getChordDegrees, getChordsByProgression, progression, arp, midi, };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { NoteObject } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Take an array of note objects to generate a MIDI file in the same location as this method is called
|
|
4
|
+
* @param {NoteObject[]} notes Notes are in the format: {note: ['c3'], level: 127, length: 64}
|
|
5
|
+
* @param {String | null} fileName If a filename is not provided, then `music.mid` is used by default
|
|
6
|
+
* If `null` is passed for `fileName`, bytes are returned instead of creating a file
|
|
7
|
+
* If this method is called from a browser then it will return a HTML link that you can append in your page
|
|
8
|
+
* This link will enable the generated MIDI as a downloadable file.
|
|
9
|
+
* @param {Number | null} bpm If a value is provided, the generated midi file will be set to this bpm value.
|
|
10
|
+
*/
|
|
11
|
+
export declare const midi: (notes: NoteObject[], fileName?: string | null, bpm?: number | undefined) => string | HTMLAnchorElement | undefined;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { ProgressionScale } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Get the chords that go with a given scale/mode
|
|
4
|
+
* This is useful only in case you want to check what chords work with a scale/mode
|
|
5
|
+
* so that you can come up with chord progressions
|
|
6
|
+
* @param {String} mode e.g. major
|
|
7
|
+
* @return {Array} e.g.['I', 'ii', 'iii', 'IV', 'V', 'vi', 'vii°']
|
|
8
|
+
*/
|
|
9
|
+
export declare const getChordDegrees: (mode: string) => string[];
|
|
10
|
+
/**
|
|
11
|
+
* Take the specified scale and degrees and return the chord names for them
|
|
12
|
+
* These can be used as the value for the `notes` param of the `clip` method
|
|
13
|
+
* @param {String} noteOctaveScale e.g. 'C4 major'
|
|
14
|
+
* @param {String} chordDegress e.g. 'I IV V IV'
|
|
15
|
+
* @return {String} e.g. 'CM FM GM FM'
|
|
16
|
+
*/
|
|
17
|
+
export declare const getChordsByProgression: (noteOctaveScale: string, chordDegress: string) => string;
|
|
18
|
+
/**
|
|
19
|
+
* Generate a chord progression based on basic music theory
|
|
20
|
+
* where we follow tonic to optionally predominant and then dominant
|
|
21
|
+
* and then randomly to predominant and continue this till we reach `count`
|
|
22
|
+
* @param scaleType e.g. M (for major chord progression), m (for minor chord progression)
|
|
23
|
+
* @param count e.g. 4
|
|
24
|
+
*/
|
|
25
|
+
export declare const progression: (scaleType: ProgressionScale, count?: number) => any[];
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Channel } from './channel';
|
|
2
|
+
import type { ChannelParams, PlayParams } from './types';
|
|
3
|
+
export declare class Session {
|
|
4
|
+
sessionChannels: Channel[];
|
|
5
|
+
constructor(arr: ChannelParams[]);
|
|
6
|
+
uniqueIdx(channels: Channel[], idx?: string | number): string | number;
|
|
7
|
+
createChannel(ch: ChannelParams): Channel;
|
|
8
|
+
get channels(): Channel[];
|
|
9
|
+
setTransportTempo(valueBpm: number): void;
|
|
10
|
+
startTransport(): void;
|
|
11
|
+
stopTransport(deleteEvents?: boolean): void;
|
|
12
|
+
startRow(idx: number): void;
|
|
13
|
+
play(params: PlayParams): void;
|
|
14
|
+
}
|