scribbletune 5.0.0-alpha.2 → 5.0.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/{browser.js → dist/browser.js} +1 -1
- package/dist/browser.js.map +1 -0
- package/dist/index.d.ts +370 -0
- package/{index.js → dist/index.js} +1 -1
- package/dist/index.js.map +1 -0
- package/{max.js → dist/max.js} +1 -1
- package/dist/max.js.map +1 -0
- package/dist/scribbletune.js +1 -1
- package/dist/scribbletune.js.map +1 -1
- package/package.json +4 -1
- package/.editorconfig +0 -14
- package/.eslintignore +0 -4
- package/.eslintrc +0 -68
- package/.github/PULL_REQUEST_TEMPLATE.md +0 -13
- package/.nycrc.json +0 -21
- package/.travis.yml +0 -7
- package/.vscode/extensions.json +0 -17
- package/.vscode/launch.json +0 -64
- package/.vscode/settings.json +0 -44
- package/.vscode/tasks.json +0 -78
- package/CONTRIBUTING.md +0 -85
- package/browser.js.map +0 -1
- package/examples/arpeggiate.js +0 -87
- package/examples/bassline.js +0 -46
- package/examples/chords.js +0 -18
- package/examples/drummer.js +0 -46
- package/examples/hats.js +0 -11
- package/examples/kick.js +0 -11
- package/examples/progressions.js +0 -15
- package/examples/riff.js +0 -53
- package/examples/scale.js +0 -12
- package/examples/snare.js +0 -11
- package/examples/tempo.js +0 -20
- package/examples/tidal.js +0 -26
- package/index.d.ts +0 -62
- package/index.js.map +0 -1
- package/jest.config.js +0 -21
- package/max.js.map +0 -1
- package/runkit.js +0 -9
- package/src/arp.ts +0 -113
- package/src/browser-clip.ts +0 -240
- package/src/browser-index.ts +0 -26
- package/src/channel.ts +0 -631
- package/src/clip.ts +0 -249
- package/src/index.ts +0 -24
- package/src/max-index.ts +0 -24
- package/src/max.ts +0 -47
- package/src/midi.ts +0 -104
- package/src/progression.ts +0 -182
- package/src/session.ts +0 -104
- package/src/typings.d.ts +0 -297
- package/src/utils.ts +0 -203
- package/tests/arp.spec.ts +0 -42
- package/tests/browser-clip.spec.ts +0 -60
- package/tests/clip.spec.ts +0 -501
- package/tests/midi.spec.ts +0 -52
- package/tests/progression.spec.ts +0 -31
- package/tests/utils.spec.ts +0 -32
- package/tsconfig.json +0 -67
- package/webpack.config.js +0 -113
package/src/channel.ts
DELETED
|
@@ -1,631 +0,0 @@
|
|
|
1
|
-
import { clip, getNote, getDuration } from './browser-clip';
|
|
2
|
-
import { errorHasMessage, IIndexable } from './utils';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Get the next logical position to play in the session
|
|
6
|
-
* Tone has a build-in method `Tone.Transport.nextSubdivision('4n')`
|
|
7
|
-
* but I think it s better to round off as follows for live performance
|
|
8
|
-
*/
|
|
9
|
-
const getNextPos = (
|
|
10
|
-
clip: null | { align?: string; alignOffset?: string }
|
|
11
|
-
): number | string => {
|
|
12
|
-
// TODO: (soon) convert to using transportPosTicks (fewer computations)
|
|
13
|
-
const arr = Tone.Transport.position.split(':');
|
|
14
|
-
// If we are still around 0:0:0x, then set start position to 0
|
|
15
|
-
if (arr[0] === '0' && arr[1] === '0') {
|
|
16
|
-
return 0;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
// Else set it to the next bar
|
|
20
|
-
const transportPosTicks = Tone.Transport.ticks;
|
|
21
|
-
const align = clip?.align || '1m';
|
|
22
|
-
const alignOffset = clip?.alignOffset || '0';
|
|
23
|
-
const alignTicks: number = Tone.Ticks(align).toTicks();
|
|
24
|
-
const alignOffsetTicks: number = Tone.Ticks(alignOffset).toTicks();
|
|
25
|
-
const nextPosTicks = Tone.Ticks(
|
|
26
|
-
Math.floor(transportPosTicks / alignTicks + 1) * alignTicks +
|
|
27
|
-
alignOffsetTicks
|
|
28
|
-
);
|
|
29
|
-
// const nextPosBBS = nextPosTicks.toBarsBeatsSixteenths();
|
|
30
|
-
// return nextPosBBS; // Extraneous computations
|
|
31
|
-
return nextPosTicks;
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Channel
|
|
36
|
-
* A channel is made up of a Tone.js Player/Instrument, one or more
|
|
37
|
-
* Tone.js sequences (known as clips in Scribbletune)
|
|
38
|
-
* & optionally a set of effects (with or without presets)
|
|
39
|
-
*
|
|
40
|
-
* API:
|
|
41
|
-
* clips -> Get all clips for this channel
|
|
42
|
-
* addClip -> Add a new clip to the channel
|
|
43
|
-
* startClip -> Start a clip at the provided index
|
|
44
|
-
* stopClip -> Stop a clip at the provided index
|
|
45
|
-
* activeClipIdx -> Get the clip that is currently playing
|
|
46
|
-
*/
|
|
47
|
-
export class Channel {
|
|
48
|
-
idx: number | string;
|
|
49
|
-
name: string;
|
|
50
|
-
activePatternIdx: number;
|
|
51
|
-
channelClips: any;
|
|
52
|
-
clipNoteCount: number;
|
|
53
|
-
instrument: any;
|
|
54
|
-
external: any;
|
|
55
|
-
initializerTask: Promise<void>;
|
|
56
|
-
hasLoaded: boolean; // if (!this.hasLoaded) - don't play this channel. Either still loading, or (initOutputProducer() rejected,
|
|
57
|
-
hasFailed: boolean | Error;
|
|
58
|
-
private eventCbFn: EventFn | undefined;
|
|
59
|
-
private playerCbFn: playerObserverFnc | undefined;
|
|
60
|
-
private counterResetTask: number | undefined;
|
|
61
|
-
constructor(params: ChannelParams) {
|
|
62
|
-
this.idx = params.idx || 0;
|
|
63
|
-
this.name = params.name || 'ch ' + params.idx;
|
|
64
|
-
this.activePatternIdx = -1;
|
|
65
|
-
this.channelClips = [];
|
|
66
|
-
this.clipNoteCount = 0;
|
|
67
|
-
|
|
68
|
-
// Filter out unrequired params and create clip params object
|
|
69
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
70
|
-
const { clips, samples, sample, synth, ...params1 } = params;
|
|
71
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
72
|
-
const { external, sampler, buffer, ...params2 } = params1;
|
|
73
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
74
|
-
const { player, instrument, volume, ...params3 } = params2;
|
|
75
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
76
|
-
const { eventCb, playerCb, effects, ...params4 } = params3;
|
|
77
|
-
const { context = Tone.getContext(), ...originalParamsFiltered } = params4;
|
|
78
|
-
|
|
79
|
-
this.eventCbFn = eventCb;
|
|
80
|
-
this.playerCbFn = playerCb;
|
|
81
|
-
|
|
82
|
-
// Async section
|
|
83
|
-
this.hasLoaded = false;
|
|
84
|
-
this.hasFailed = false;
|
|
85
|
-
this.initializerTask = this.initOutputProducer(context, params).then(() => {
|
|
86
|
-
return this.initInstrument(context, params).then(() => {
|
|
87
|
-
return this.adjustInstrument(context, params).then(() => {
|
|
88
|
-
return this.initEffects(context, params);
|
|
89
|
-
});
|
|
90
|
-
});
|
|
91
|
-
});
|
|
92
|
-
// End Async section
|
|
93
|
-
|
|
94
|
-
// Sync section
|
|
95
|
-
let clipsFailed: { message: string } | false = false;
|
|
96
|
-
try {
|
|
97
|
-
params.clips.forEach((c: any, i: number) => {
|
|
98
|
-
try {
|
|
99
|
-
this.addClip({
|
|
100
|
-
...c,
|
|
101
|
-
...originalParamsFiltered,
|
|
102
|
-
});
|
|
103
|
-
} catch (e) {
|
|
104
|
-
// Annotate the error with Clip info
|
|
105
|
-
throw new Error(
|
|
106
|
-
`${errorHasMessage(e) ? e.message : e} in clip ${i + 1}`
|
|
107
|
-
);
|
|
108
|
-
}
|
|
109
|
-
}, this);
|
|
110
|
-
} catch (e) {
|
|
111
|
-
clipsFailed = e as { message: string }; // Stash the error
|
|
112
|
-
}
|
|
113
|
-
// End Sync section
|
|
114
|
-
|
|
115
|
-
// Reconcile sync section with async section
|
|
116
|
-
this.initializerTask
|
|
117
|
-
.then(() => {
|
|
118
|
-
if (clipsFailed) {
|
|
119
|
-
throw clipsFailed;
|
|
120
|
-
}
|
|
121
|
-
this.hasLoaded = true;
|
|
122
|
-
this.eventCb('loaded', {}); // Report async load completion.
|
|
123
|
-
})
|
|
124
|
-
.catch(e => {
|
|
125
|
-
this.hasFailed = e;
|
|
126
|
-
this.eventCb('error', { e }); // Report async errors.
|
|
127
|
-
});
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
static setTransportTempo(valueBpm: number): void {
|
|
131
|
-
Tone.Transport.bpm.value = valueBpm;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
static startTransport(): void {
|
|
135
|
-
Tone.start();
|
|
136
|
-
Tone.Transport.start();
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
static stopTransport(deleteEvents = true): void {
|
|
140
|
-
Tone.Transport.stop();
|
|
141
|
-
if (deleteEvents) {
|
|
142
|
-
// Delete all events in the Tone.Transport
|
|
143
|
-
Tone.Transport.cancel();
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
setVolume(volume: number): void {
|
|
148
|
-
// ? this.volume = volume;
|
|
149
|
-
|
|
150
|
-
// Change volume of the player
|
|
151
|
-
// if (this.player) {
|
|
152
|
-
// this.player.volume.value = volume;
|
|
153
|
-
// }
|
|
154
|
-
|
|
155
|
-
// Change volume of the sampler
|
|
156
|
-
// if (this.sampler) {
|
|
157
|
-
// this.sampler.volume.value = volume;
|
|
158
|
-
// }
|
|
159
|
-
|
|
160
|
-
// Change volume of the instrument
|
|
161
|
-
if (this.instrument) {
|
|
162
|
-
this.instrument.volume.value = volume;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
// Change volume of the external
|
|
166
|
-
if (this.external) {
|
|
167
|
-
this.external.setVolume?.(volume);
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
startClip(idx: number, position?: number | string): void {
|
|
172
|
-
const clip = this.channelClips[idx];
|
|
173
|
-
position = position || (position === 0 ? 0 : getNextPos(clip));
|
|
174
|
-
// Stop any other currently running clip
|
|
175
|
-
if (this.activePatternIdx > -1 && this.activePatternIdx !== idx) {
|
|
176
|
-
this.stopClip(this.activePatternIdx, position);
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
if (clip && clip.state !== 'started') {
|
|
180
|
-
// We need to schedule that for just before when clip?.start(position) events start coming.
|
|
181
|
-
this.counterResetTask = Tone.Transport.scheduleOnce(
|
|
182
|
-
(/* time: Tone.Seconds */) => {
|
|
183
|
-
this.clipNoteCount = 0;
|
|
184
|
-
},
|
|
185
|
-
position
|
|
186
|
-
);
|
|
187
|
-
|
|
188
|
-
this.activePatternIdx = idx;
|
|
189
|
-
// clip?.stop(position); // DEBUG: trying to clear out start/stop events
|
|
190
|
-
// clip?.clear(position); // DEBUG: trying to clear out events
|
|
191
|
-
clip?.start(position);
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
stopClip(idx: number, position?: number | string): void {
|
|
196
|
-
const clip = this.channelClips[idx];
|
|
197
|
-
position = position || (position === 0 ? 0 : getNextPos(clip));
|
|
198
|
-
clip?.stop(position);
|
|
199
|
-
if (idx === this.activePatternIdx) {
|
|
200
|
-
this.activePatternIdx = -1;
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
addClip(clipParams: ClipParams, idx?: number): void {
|
|
205
|
-
idx = idx || this.channelClips.length;
|
|
206
|
-
if (clipParams.pattern) {
|
|
207
|
-
this.channelClips[idx as number] = clip(
|
|
208
|
-
{
|
|
209
|
-
...clipParams,
|
|
210
|
-
},
|
|
211
|
-
this
|
|
212
|
-
);
|
|
213
|
-
// Pass certain clipParams into getNextPos()
|
|
214
|
-
['align', 'alignOffset'].forEach(key => {
|
|
215
|
-
if ((clipParams as IIndexable)[key]) {
|
|
216
|
-
this.channelClips[idx as number][key] = (clipParams as IIndexable)[
|
|
217
|
-
key
|
|
218
|
-
];
|
|
219
|
-
}
|
|
220
|
-
});
|
|
221
|
-
} else {
|
|
222
|
-
// Allow creation of empty clips
|
|
223
|
-
this.channelClips[idx as number] = null;
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
/**
|
|
228
|
-
* @param {Object} ClipParams clip parameters
|
|
229
|
-
* @return {Function} function that can be used as the callback in Tone.Sequence https://tonejs.github.io/docs/Sequence
|
|
230
|
-
*/
|
|
231
|
-
getSeqFn(params: ClipParams): SeqFn {
|
|
232
|
-
if (this.external) {
|
|
233
|
-
return (time: string, el: string) => {
|
|
234
|
-
if (el === 'x' || el === 'R') {
|
|
235
|
-
const counter = this.clipNoteCount;
|
|
236
|
-
if (this.hasLoaded) {
|
|
237
|
-
const note = getNote(el, params, counter)[0];
|
|
238
|
-
const duration = getDuration(params, counter);
|
|
239
|
-
const durSeconds = Tone.Time(duration).toSeconds();
|
|
240
|
-
this.playerCb({ note, duration, time, counter });
|
|
241
|
-
try {
|
|
242
|
-
this.external.triggerAttackRelease?.(note, durSeconds, time);
|
|
243
|
-
} catch (e) {
|
|
244
|
-
this.eventCb('error', { e }); // Report play errors.
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
this.clipNoteCount++;
|
|
248
|
-
}
|
|
249
|
-
};
|
|
250
|
-
} else if (this.instrument instanceof Tone.Player) {
|
|
251
|
-
return (time: string, el: string) => {
|
|
252
|
-
if (el === 'x' || el === 'R') {
|
|
253
|
-
const counter = this.clipNoteCount;
|
|
254
|
-
if (this.hasLoaded) {
|
|
255
|
-
this.playerCb({ note: '', duration: '', time, counter });
|
|
256
|
-
try {
|
|
257
|
-
this.instrument.start(time);
|
|
258
|
-
} catch (e) {
|
|
259
|
-
this.eventCb('error', { e }); // Report play errors.
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
this.clipNoteCount++;
|
|
263
|
-
}
|
|
264
|
-
};
|
|
265
|
-
} else if (
|
|
266
|
-
this.instrument instanceof Tone.PolySynth ||
|
|
267
|
-
this.instrument instanceof Tone.Sampler
|
|
268
|
-
) {
|
|
269
|
-
return (time: string, el: string) => {
|
|
270
|
-
if (el === 'x' || el === 'R') {
|
|
271
|
-
const counter = this.clipNoteCount;
|
|
272
|
-
if (this.hasLoaded) {
|
|
273
|
-
const note = getNote(el, params, counter);
|
|
274
|
-
const duration = getDuration(params, counter);
|
|
275
|
-
this.playerCb({ note, duration, time, counter });
|
|
276
|
-
try {
|
|
277
|
-
this.instrument.triggerAttackRelease(note, duration, time);
|
|
278
|
-
} catch (e) {
|
|
279
|
-
this.eventCb('error', { e }); // Report play errors.
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
this.clipNoteCount++;
|
|
283
|
-
}
|
|
284
|
-
};
|
|
285
|
-
} else if (this.instrument instanceof Tone.NoiseSynth) {
|
|
286
|
-
return (time: string, el: string) => {
|
|
287
|
-
if (el === 'x' || el === 'R') {
|
|
288
|
-
const counter = this.clipNoteCount;
|
|
289
|
-
if (this.hasLoaded) {
|
|
290
|
-
const duration = getDuration(params, counter);
|
|
291
|
-
this.playerCb({ note: '', duration, time, counter });
|
|
292
|
-
try {
|
|
293
|
-
this.instrument.triggerAttackRelease(duration, time);
|
|
294
|
-
} catch (e) {
|
|
295
|
-
this.eventCb('error', { e }); // Report play errors.
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
this.clipNoteCount++;
|
|
299
|
-
}
|
|
300
|
-
};
|
|
301
|
-
} else {
|
|
302
|
-
return (time: string, el: string) => {
|
|
303
|
-
if (el === 'x' || el === 'R') {
|
|
304
|
-
const counter = this.clipNoteCount;
|
|
305
|
-
if (this.hasLoaded) {
|
|
306
|
-
const note = getNote(el, params, counter)[0];
|
|
307
|
-
const duration = getDuration(params, counter);
|
|
308
|
-
this.playerCb({ note, duration, time, counter });
|
|
309
|
-
try {
|
|
310
|
-
this.instrument.triggerAttackRelease(note, duration, time);
|
|
311
|
-
} catch (e) {
|
|
312
|
-
this.eventCb('error', { e }); // Report play errors.
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
this.clipNoteCount++;
|
|
316
|
-
}
|
|
317
|
-
};
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
private eventCb(event: string, params: any): void {
|
|
322
|
-
if (typeof this.eventCbFn === 'function') {
|
|
323
|
-
params.channel = this;
|
|
324
|
-
this.eventCbFn(event, params);
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
private playerCb(params: any): void {
|
|
329
|
-
if (typeof this.playerCbFn === 'function') {
|
|
330
|
-
params.channel = this;
|
|
331
|
-
this.playerCbFn(params);
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
/**
|
|
336
|
-
* Check Tone.js object loaded state and either invoke `resolve` right away, or attach to and wait using Tone onload cb.
|
|
337
|
-
* It's an ugly hack that reaches into Tone's internal ._buffers or ._buffer to insert itself into .onload() callback.
|
|
338
|
-
* Tone has different ways to pull the onload callback from within to the API, so this implementation is very brittle.
|
|
339
|
-
* The sole reason for its existence is to handle async loaded state of Tone instruments that we allow to pass in from outside.
|
|
340
|
-
* If that option is eliminated, then this hacky function can be killed (or re-implemented via public onload API)
|
|
341
|
-
* @param toneObject Tone.js object (will work with non-Tone objects that have same loaded/onload properties)
|
|
342
|
-
* @param resolve onload callback
|
|
343
|
-
*/
|
|
344
|
-
private checkToneObjLoaded(toneObject: any, resolve: () => void) {
|
|
345
|
-
const skipRecursion = toneObject instanceof Tone.Sampler; // Sampler has a Map of ToneAudioBuffer, and our method to find inner .onload() does not work since there is no single one.
|
|
346
|
-
|
|
347
|
-
// eslint-disable-next-line no-prototype-builtins
|
|
348
|
-
if ('loaded' in toneObject) {
|
|
349
|
-
if (toneObject.loaded) {
|
|
350
|
-
resolve();
|
|
351
|
-
return;
|
|
352
|
-
}
|
|
353
|
-
if (skipRecursion) {
|
|
354
|
-
return;
|
|
355
|
-
}
|
|
356
|
-
// Try Recursion into inner objects:
|
|
357
|
-
let handled = false;
|
|
358
|
-
['buffer', '_buffer', '_buffers'].forEach(key => {
|
|
359
|
-
if (key in toneObject) {
|
|
360
|
-
this.checkToneObjLoaded(toneObject[key], resolve);
|
|
361
|
-
handled = true;
|
|
362
|
-
}
|
|
363
|
-
});
|
|
364
|
-
if (handled) {
|
|
365
|
-
return;
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
// Check object type if it has load/onload (and _buffers or _buffer), then call resolve()
|
|
370
|
-
// The list was created for Tone@14.8.0 by grepping and reviewing the source code.
|
|
371
|
-
// Known objecs to have:
|
|
372
|
-
const hasOnload =
|
|
373
|
-
toneObject instanceof Tone.ToneAudioBuffer ||
|
|
374
|
-
toneObject instanceof Tone.ToneBufferSource ||
|
|
375
|
-
// Falback for "future" objects
|
|
376
|
-
('loaded' in toneObject && 'onload' in toneObject);
|
|
377
|
-
|
|
378
|
-
if (!hasOnload) {
|
|
379
|
-
// console.log('resolve() for ch "%o" idx %o onload NOT FOUND', this.name, this.idx);
|
|
380
|
-
// This is not a good assumption. E.g. it does not work for Tone.ToneAudioBuffers
|
|
381
|
-
resolve();
|
|
382
|
-
} else {
|
|
383
|
-
const oldOnLoad = toneObject.onload;
|
|
384
|
-
toneObject.onload = () => {
|
|
385
|
-
if (oldOnLoad && typeof oldOnLoad === 'function') {
|
|
386
|
-
toneObject.onload = oldOnLoad;
|
|
387
|
-
oldOnLoad();
|
|
388
|
-
}
|
|
389
|
-
// console.log('resolve() for ch "%o" idx %o', this.name, this.idx);
|
|
390
|
-
resolve();
|
|
391
|
-
};
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
private recreateToneObjectInContext(
|
|
396
|
-
toneObject: any, // Tone.PolySynth | Tone.Player | Tone.Sampler | Tone['' | '']
|
|
397
|
-
context: any
|
|
398
|
-
): Promise<any> {
|
|
399
|
-
context = context || Tone.getContext();
|
|
400
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
401
|
-
return new Promise<any>((resolve, reject) => {
|
|
402
|
-
// Tone.PolySynth | Tone.Player | Tone.Sampler | Tone['' | '']
|
|
403
|
-
if (toneObject instanceof Tone.PolySynth) {
|
|
404
|
-
const newObj = Tone.PolySynth(Tone[toneObject._dummyVoice.name], {
|
|
405
|
-
...toneObject.get(),
|
|
406
|
-
context,
|
|
407
|
-
});
|
|
408
|
-
this.checkToneObjLoaded(newObj, () => resolve(newObj));
|
|
409
|
-
} else if (toneObject instanceof Tone.Player) {
|
|
410
|
-
const newObj = Tone.Player({
|
|
411
|
-
url: toneObject._buffer,
|
|
412
|
-
context,
|
|
413
|
-
onload: () => this.checkToneObjLoaded(newObj, () => resolve(newObj)),
|
|
414
|
-
});
|
|
415
|
-
} else if (toneObject instanceof Tone.Sampler) {
|
|
416
|
-
const { attack, curve, release, volume } = toneObject.get();
|
|
417
|
-
const paramsFromSampler = {
|
|
418
|
-
attack,
|
|
419
|
-
curve,
|
|
420
|
-
release,
|
|
421
|
-
volume,
|
|
422
|
-
};
|
|
423
|
-
const paramsFromBuffers = {
|
|
424
|
-
baseUrl: toneObject._buffers.baseUrl,
|
|
425
|
-
urls: Object.fromEntries(toneObject._buffers._buffers.entries()),
|
|
426
|
-
};
|
|
427
|
-
const newObj = Tone.Sampler({
|
|
428
|
-
...paramsFromSampler,
|
|
429
|
-
...paramsFromBuffers,
|
|
430
|
-
context,
|
|
431
|
-
onload: () => this.checkToneObjLoaded(newObj, () => resolve(newObj)),
|
|
432
|
-
});
|
|
433
|
-
} else {
|
|
434
|
-
const newObj = Tone[toneObject.name]({
|
|
435
|
-
...toneObject.get(),
|
|
436
|
-
context,
|
|
437
|
-
onload: () => this.checkToneObjLoaded(newObj, () => resolve(newObj)),
|
|
438
|
-
});
|
|
439
|
-
this.checkToneObjLoaded(newObj, () => resolve(newObj));
|
|
440
|
-
}
|
|
441
|
-
});
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
private initOutputProducer(
|
|
445
|
-
context: any,
|
|
446
|
-
params: ChannelParams
|
|
447
|
-
): Promise<void> {
|
|
448
|
-
context = context || Tone.getContext();
|
|
449
|
-
return new Promise<void>((resolve, reject) => {
|
|
450
|
-
/*
|
|
451
|
-
* 1. The params object can be used to pass a sample (sound source) OR a synth(Synth/FMSynth/AMSynth etc) or samples.
|
|
452
|
-
* Scribbletune will then create a Tone.js Player or Tone.js Instrument or Tone.js Sampler respectively
|
|
453
|
-
* 2. It can also be used to pass a Tone.js Player object or instrument that was created elsewhere
|
|
454
|
-
* (mostly by Scribbletune itself in the channel creation method)
|
|
455
|
-
**/
|
|
456
|
-
|
|
457
|
-
if (params.synth) {
|
|
458
|
-
if (params.instrument) {
|
|
459
|
-
throw new Error(
|
|
460
|
-
'Either synth or instrument can be provided, but not both.'
|
|
461
|
-
);
|
|
462
|
-
}
|
|
463
|
-
if ((params.synth as SynthParams).synth) {
|
|
464
|
-
const synthName = (params.synth as SynthParams).synth;
|
|
465
|
-
// const presetName = (params.synth as SynthParams).presetName; // Unused here
|
|
466
|
-
const preset = (params.synth as SynthParams).preset || {};
|
|
467
|
-
this.instrument = new Tone[synthName]({
|
|
468
|
-
...preset,
|
|
469
|
-
context,
|
|
470
|
-
// Use onload for cases when synthName calls out Tone.Sample/Player/Sampler.
|
|
471
|
-
// It could be a universal way to load Tone.js instruments.
|
|
472
|
-
onload: () => this.checkToneObjLoaded(this.instrument, resolve),
|
|
473
|
-
// This onload is ignored in all synths. Therefore we call checkToneObjLoaded() again below.
|
|
474
|
-
// It is safe to call resolve() multiple times for Promise<void>
|
|
475
|
-
});
|
|
476
|
-
this.checkToneObjLoaded(this.instrument, resolve);
|
|
477
|
-
} else {
|
|
478
|
-
this.instrument = params.synth; // TODO: This is dangerous by-reference assignment.
|
|
479
|
-
console.warn(
|
|
480
|
-
'The "synth" parameter with instrument will be deprecated in the future. Please use the "instrument" parameter instead.'
|
|
481
|
-
);
|
|
482
|
-
// params.synth describing the Tone[params.synth.synth] is allowed.
|
|
483
|
-
this.checkToneObjLoaded(this.instrument, resolve);
|
|
484
|
-
}
|
|
485
|
-
} else if (typeof params.instrument === 'string') {
|
|
486
|
-
this.instrument = new Tone[params.instrument]({ context });
|
|
487
|
-
this.checkToneObjLoaded(this.instrument, resolve);
|
|
488
|
-
} else if (params.instrument) {
|
|
489
|
-
this.instrument = params.instrument; // TODO: This is dangerous by-reference assignment. Tone.instrument has context that holds all other instruments. Client side params get polluted with circular references. If params come from e.g. react-ApolloClient data, Apollo tools crash on circular references.
|
|
490
|
-
this.checkToneObjLoaded(this.instrument, resolve);
|
|
491
|
-
} else if (params.sample || params.buffer) {
|
|
492
|
-
this.instrument = new Tone.Player({
|
|
493
|
-
url: params.sample || params.buffer,
|
|
494
|
-
context,
|
|
495
|
-
onload: () => this.checkToneObjLoaded(this.instrument, resolve),
|
|
496
|
-
});
|
|
497
|
-
} else if (params.samples) {
|
|
498
|
-
this.instrument = new Tone.Sampler({
|
|
499
|
-
urls: params.samples,
|
|
500
|
-
context,
|
|
501
|
-
onload: () => this.checkToneObjLoaded(this.instrument, resolve),
|
|
502
|
-
});
|
|
503
|
-
} else if (params.sampler) {
|
|
504
|
-
this.instrument = params.sampler; // TODO: This is dangerous by-reference assignment.
|
|
505
|
-
this.checkToneObjLoaded(this.instrument, resolve);
|
|
506
|
-
} else if (params.player) {
|
|
507
|
-
this.instrument = params.player; // TODO: This is dangerous by-reference assignment.
|
|
508
|
-
this.checkToneObjLoaded(this.instrument, resolve);
|
|
509
|
-
} else if (params.external) {
|
|
510
|
-
this.external = { ...params.external }; // Sanitize object by shallow clone
|
|
511
|
-
this.instrument = {
|
|
512
|
-
context,
|
|
513
|
-
volume: { value: 0 },
|
|
514
|
-
};
|
|
515
|
-
// Do not call! this.checkToneObjLoaded(this.instrument, resolve);
|
|
516
|
-
|
|
517
|
-
if (params.external.init) {
|
|
518
|
-
return params.external
|
|
519
|
-
.init(context.rawContext)
|
|
520
|
-
.then(() => {
|
|
521
|
-
resolve();
|
|
522
|
-
})
|
|
523
|
-
.catch((e: any) => {
|
|
524
|
-
reject(
|
|
525
|
-
new Error(
|
|
526
|
-
`${e.message} loading external output module of channel idx ${
|
|
527
|
-
this.idx
|
|
528
|
-
}, ${this.name ?? '(no name)'}`
|
|
529
|
-
)
|
|
530
|
-
);
|
|
531
|
-
});
|
|
532
|
-
} else {
|
|
533
|
-
resolve();
|
|
534
|
-
}
|
|
535
|
-
} else {
|
|
536
|
-
throw new Error(
|
|
537
|
-
'One of required synth|instrument|sample|sampler|samples|buffer|player|external is not provided!'
|
|
538
|
-
);
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
if (!this.instrument) {
|
|
542
|
-
throw new Error('Failed instantiating instrument from given params.');
|
|
543
|
-
}
|
|
544
|
-
});
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
private initInstrument(context: any, params: ChannelParams): Promise<void> {
|
|
548
|
-
context = context || Tone.getContext();
|
|
549
|
-
if (!params.external && this.instrument?.context !== context) {
|
|
550
|
-
return this.recreateToneObjectInContext(this.instrument, context).then(
|
|
551
|
-
newObj => {
|
|
552
|
-
this.instrument = newObj;
|
|
553
|
-
}
|
|
554
|
-
);
|
|
555
|
-
} else {
|
|
556
|
-
// Nothing to do
|
|
557
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
558
|
-
return new Promise<void>((resolve, reject) => {
|
|
559
|
-
resolve();
|
|
560
|
-
});
|
|
561
|
-
}
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
private adjustInstrument(context: any, params: ChannelParams): Promise<void> {
|
|
565
|
-
context = context || Tone.getContext();
|
|
566
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
567
|
-
return new Promise<void>((resolve, reject) => {
|
|
568
|
-
if (params.volume) {
|
|
569
|
-
// this.instrument.volume.value = params.volume;
|
|
570
|
-
this.setVolume(params.volume);
|
|
571
|
-
}
|
|
572
|
-
resolve();
|
|
573
|
-
});
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
private initEffects(context: any, params: ChannelParams): Promise<void> {
|
|
577
|
-
context = context || Tone.getContext();
|
|
578
|
-
|
|
579
|
-
const createEffect = (effect: any): Promise<any> => {
|
|
580
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
581
|
-
return new Promise<any>((resolve, reject) => {
|
|
582
|
-
if (typeof effect === 'string') {
|
|
583
|
-
resolve(new Tone[effect]({ context }));
|
|
584
|
-
} else if (effect.context !== context) {
|
|
585
|
-
return this.recreateToneObjectInContext(effect, context);
|
|
586
|
-
} else {
|
|
587
|
-
resolve(effect);
|
|
588
|
-
}
|
|
589
|
-
}).then(effectOut => {
|
|
590
|
-
return effectOut.toDestination();
|
|
591
|
-
});
|
|
592
|
-
};
|
|
593
|
-
|
|
594
|
-
const startEffect = (eff: any) => {
|
|
595
|
-
return typeof eff.start === 'function' ? eff.start() : eff;
|
|
596
|
-
};
|
|
597
|
-
|
|
598
|
-
const toArray = (someVal: any): any[] => {
|
|
599
|
-
if (!someVal) {
|
|
600
|
-
return [];
|
|
601
|
-
}
|
|
602
|
-
if (Array.isArray(someVal)) {
|
|
603
|
-
return someVal;
|
|
604
|
-
}
|
|
605
|
-
return [someVal];
|
|
606
|
-
};
|
|
607
|
-
|
|
608
|
-
const effectsIn = toArray(params.effects);
|
|
609
|
-
if (params.external) {
|
|
610
|
-
if (effectsIn.length !== 0) {
|
|
611
|
-
throw new Error('Effects cannot be used with external output');
|
|
612
|
-
}
|
|
613
|
-
return Promise.resolve();
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
// effects = params.effects.map(createEffect).map(startEffect);
|
|
617
|
-
return Promise.all(effectsIn.map(createEffect))
|
|
618
|
-
.then(results => results.map(startEffect))
|
|
619
|
-
.then(effects => {
|
|
620
|
-
this.instrument.chain(...effects).toDestination();
|
|
621
|
-
});
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
get clips(): any[] {
|
|
625
|
-
return this.channelClips;
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
get activeClipIdx(): number {
|
|
629
|
-
return this.activePatternIdx;
|
|
630
|
-
}
|
|
631
|
-
}
|