tone 15.5.56 → 15.5.58
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/Tone/event/PatternGenerator.test.ts +18 -0
- package/Tone/event/PatternGenerator.ts +5 -0
- package/Tone/instrument/Sampler.test.ts +21 -0
- package/Tone/instrument/Sampler.ts +152 -115
- package/Tone/signal/ToneConstantSource.test.ts +23 -12
- package/Tone/signal/ToneConstantSource.ts +14 -4
- package/Tone/version.ts +1 -1
- package/build/Tone.js +1 -1
- package/build/Tone.js.map +1 -1
- package/build/esm/event/PatternGenerator.js +5 -0
- package/build/esm/event/PatternGenerator.js.map +1 -1
- package/build/esm/instrument/Sampler.d.ts +22 -0
- package/build/esm/instrument/Sampler.js +17 -0
- package/build/esm/instrument/Sampler.js.map +1 -1
- package/build/esm/signal/ToneConstantSource.d.ts +4 -0
- package/build/esm/signal/ToneConstantSource.js +13 -5
- package/build/esm/signal/ToneConstantSource.js.map +1 -1
- package/build/esm/version.js +1 -1
- package/package.json +2 -2
|
@@ -99,5 +99,23 @@ describe("PatternGenerator", () => {
|
|
|
99
99
|
currentIndex = nextIndex;
|
|
100
100
|
}
|
|
101
101
|
});
|
|
102
|
+
|
|
103
|
+
it("never randomly walks outside of the range of values", () => {
|
|
104
|
+
[1, 2, 3, 4].forEach((numValues) => {
|
|
105
|
+
const pattern = PatternGenerator(numValues, "randomWalk");
|
|
106
|
+
for (let i = 0; i < 100; i++) {
|
|
107
|
+
expect(pattern.next().value)
|
|
108
|
+
.to.be.at.least(0)
|
|
109
|
+
.and.at.most(numValues - 1);
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("randomly walks in place when there is only one value", () => {
|
|
115
|
+
const pattern = PatternGenerator(1, "randomWalk");
|
|
116
|
+
expect(getArrayValues(pattern, 10)).to.deep.equal([
|
|
117
|
+
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
118
|
+
]);
|
|
119
|
+
});
|
|
102
120
|
});
|
|
103
121
|
});
|
|
@@ -138,6 +138,11 @@ function* randomWalk(numValues: number): IterableIterator<number> {
|
|
|
138
138
|
// randomly choose a starting index
|
|
139
139
|
let index = Math.floor(Math.random() * numValues);
|
|
140
140
|
while (true) {
|
|
141
|
+
// with a single value there is nowhere to step to
|
|
142
|
+
if (numValues === 1) {
|
|
143
|
+
yield index;
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
141
146
|
if (index === 0) {
|
|
142
147
|
index++; // at bottom, so force upward step
|
|
143
148
|
} else if (index === numValues - 1) {
|
|
@@ -88,6 +88,27 @@ describe("Sampler", () => {
|
|
|
88
88
|
expect(buffer.isSilent()).to.be.false;
|
|
89
89
|
});
|
|
90
90
|
|
|
91
|
+
it("get() reports back the urls, baseUrl, onload and onerror it was constructed with", () => {
|
|
92
|
+
const onload = () => {};
|
|
93
|
+
const onerror = () => {};
|
|
94
|
+
const sampler = new Sampler({
|
|
95
|
+
urls: {
|
|
96
|
+
69: A4_buffer,
|
|
97
|
+
},
|
|
98
|
+
baseUrl: "./test/audio/",
|
|
99
|
+
onload,
|
|
100
|
+
onerror,
|
|
101
|
+
});
|
|
102
|
+
const options = sampler.get();
|
|
103
|
+
expect(options.urls).to.deep.equal({
|
|
104
|
+
69: A4_buffer,
|
|
105
|
+
});
|
|
106
|
+
expect(options.baseUrl).to.equal("./test/audio/");
|
|
107
|
+
expect(options.onload).to.equal(onload);
|
|
108
|
+
expect(options.onerror).to.equal(onerror);
|
|
109
|
+
sampler.dispose();
|
|
110
|
+
});
|
|
111
|
+
|
|
91
112
|
it("throws an error if there are no available notes to play", () => {
|
|
92
113
|
expect(() => {
|
|
93
114
|
const sampler = new Sampler();
|
|
@@ -34,9 +34,9 @@ export interface SamplerOptions extends InstrumentOptions {
|
|
|
34
34
|
baseUrl: string;
|
|
35
35
|
curve: ToneBufferSourceCurve;
|
|
36
36
|
urls: SamplesMap;
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
37
|
+
loop: boolean;
|
|
38
|
+
loopEnd: number;
|
|
39
|
+
loopStart: number;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
/**
|
|
@@ -73,14 +73,14 @@ export class Sampler extends Instrument<SamplerOptions> {
|
|
|
73
73
|
*/
|
|
74
74
|
private _activeSources: Map<MidiNote, ToneBufferSource[]> = new Map();
|
|
75
75
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
76
|
+
/**
|
|
77
|
+
* The list of all provided midi notes
|
|
78
|
+
*/
|
|
79
|
+
private _providedMidiNotes: MidiNote[] = [];
|
|
80
80
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
81
|
+
/**
|
|
82
|
+
* if the buffer should loop once its over
|
|
83
|
+
*/
|
|
84
84
|
private _loop: boolean;
|
|
85
85
|
|
|
86
86
|
/**
|
|
@@ -115,6 +115,26 @@ export class Sampler extends Instrument<SamplerOptions> {
|
|
|
115
115
|
*/
|
|
116
116
|
curve: ToneBufferSourceCurve;
|
|
117
117
|
|
|
118
|
+
/**
|
|
119
|
+
* The urls passed in the constructor, kept so `get()` can report them back.
|
|
120
|
+
*/
|
|
121
|
+
private _urls: SamplesMap;
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* The baseUrl passed in the constructor, kept so `get()` can report it back.
|
|
125
|
+
*/
|
|
126
|
+
private _baseUrl: string;
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* The onload callback passed in the constructor, kept so `get()` can report it back.
|
|
130
|
+
*/
|
|
131
|
+
private _onload: () => void;
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* The onerror callback passed in the constructor, kept so `get()` can report it back.
|
|
135
|
+
*/
|
|
136
|
+
private _onerror: (error: Error) => void;
|
|
137
|
+
|
|
118
138
|
/**
|
|
119
139
|
* @param samples An object of samples mapping either Midi Note Numbers or
|
|
120
140
|
* Scientific Pitch Notation to the url of that sample.
|
|
@@ -167,9 +187,13 @@ export class Sampler extends Instrument<SamplerOptions> {
|
|
|
167
187
|
this.attack = options.attack;
|
|
168
188
|
this.release = options.release;
|
|
169
189
|
this.curve = options.curve;
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
190
|
+
this._urls = options.urls;
|
|
191
|
+
this._baseUrl = options.baseUrl;
|
|
192
|
+
this._onload = options.onload;
|
|
193
|
+
this._onerror = options.onerror;
|
|
194
|
+
this._loop = options.loop;
|
|
195
|
+
this._loopStart = options.loopStart;
|
|
196
|
+
this._loopEnd = options.loopEnd;
|
|
173
197
|
|
|
174
198
|
// invoke the callback if it's already loaded
|
|
175
199
|
if (this._buffers.loaded) {
|
|
@@ -187,9 +211,23 @@ export class Sampler extends Instrument<SamplerOptions> {
|
|
|
187
211
|
onerror: noOp,
|
|
188
212
|
release: 0.1,
|
|
189
213
|
urls: {},
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
214
|
+
loop: false,
|
|
215
|
+
loopEnd: 0,
|
|
216
|
+
loopStart: 0,
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* urls, baseUrl, onload and onerror aren't stored as regular class
|
|
222
|
+
* properties (unlike attack/release/curve), so the base implementation
|
|
223
|
+
* can't find them via `Reflect.has` and they'd be silently missing here.
|
|
224
|
+
*/
|
|
225
|
+
get(): SamplerOptions {
|
|
226
|
+
return Object.assign(super.get(), {
|
|
227
|
+
urls: this._urls,
|
|
228
|
+
baseUrl: this._baseUrl,
|
|
229
|
+
onload: this._onload,
|
|
230
|
+
onerror: this._onerror,
|
|
193
231
|
});
|
|
194
232
|
}
|
|
195
233
|
|
|
@@ -226,7 +264,7 @@ export class Sampler extends Instrument<SamplerOptions> {
|
|
|
226
264
|
if (!Array.isArray(notes)) {
|
|
227
265
|
notes = [notes];
|
|
228
266
|
}
|
|
229
|
-
|
|
267
|
+
const offset = defaultArg(this._loopStart, 0);
|
|
230
268
|
notes.forEach((note) => {
|
|
231
269
|
const midiFloat = ftomf(
|
|
232
270
|
new FrequencyClass(this.context, note).toFrequency()
|
|
@@ -240,9 +278,9 @@ export class Sampler extends Instrument<SamplerOptions> {
|
|
|
240
278
|
const playbackRate = intervalToFrequencyRatio(
|
|
241
279
|
difference + remainder
|
|
242
280
|
);
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
281
|
+
const duration = this._loop
|
|
282
|
+
? undefined
|
|
283
|
+
: buffer.duration / playbackRate;
|
|
246
284
|
// play that note
|
|
247
285
|
const source = new ToneBufferSource({
|
|
248
286
|
url: buffer,
|
|
@@ -250,9 +288,9 @@ export class Sampler extends Instrument<SamplerOptions> {
|
|
|
250
288
|
curve: this.curve,
|
|
251
289
|
fadeIn: this.attack,
|
|
252
290
|
fadeOut: this.release,
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
291
|
+
loop: this._loop,
|
|
292
|
+
loopStart: this._loopStart,
|
|
293
|
+
loopEnd: this._loopEnd,
|
|
256
294
|
playbackRate,
|
|
257
295
|
}).connect(this.output);
|
|
258
296
|
source.start(time, offset, duration, velocity);
|
|
@@ -393,98 +431,97 @@ export class Sampler extends Instrument<SamplerOptions> {
|
|
|
393
431
|
return this._buffers.loaded;
|
|
394
432
|
}
|
|
395
433
|
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
434
|
+
/**
|
|
435
|
+
* Set the loop start and end. Will only loop if loop is set to true.
|
|
436
|
+
* @param loopStart The loop start time
|
|
437
|
+
* @param loopEnd The loop end time
|
|
438
|
+
* @example
|
|
439
|
+
* const sampler = new Tone.Sampler({
|
|
440
|
+
* urls: {
|
|
441
|
+
* A1: "https://tonejs.github.io/audio/berklee/guitar_chord4.mp3",
|
|
442
|
+
* },
|
|
443
|
+
* }).toDestination();
|
|
444
|
+
* // loop between the given points
|
|
445
|
+
* sampler.setLoopPoints(0.2, 0.3);
|
|
446
|
+
* sampler.loop = true;
|
|
447
|
+
*/
|
|
448
|
+
setLoopPoints(loopStart: Time, loopEnd: Time): this {
|
|
449
|
+
this.loopStart = loopStart;
|
|
450
|
+
this.loopEnd = loopEnd;
|
|
451
|
+
return this;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* If loop is true, the loop will start at this position.
|
|
456
|
+
*/
|
|
457
|
+
get loopStart(): Time {
|
|
458
|
+
return this._loopStart;
|
|
459
|
+
}
|
|
460
|
+
set loopStart(loopStart) {
|
|
461
|
+
this._loopStart = loopStart;
|
|
462
|
+
this._providedMidiNotes.forEach((midiNote) => {
|
|
463
|
+
const buffer = this._buffers.get(midiNote);
|
|
464
|
+
if (buffer.loaded) {
|
|
465
|
+
assertRange(this.toSeconds(loopStart), 0, buffer.duration);
|
|
466
|
+
}
|
|
467
|
+
});
|
|
468
|
+
// get the current sources
|
|
469
|
+
this._activeSources.forEach((sourceList) => {
|
|
470
|
+
sourceList.forEach((source) => {
|
|
471
|
+
source.loopStart = loopStart;
|
|
472
|
+
});
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* If loop is true, the loop will end at this position.
|
|
478
|
+
*/
|
|
479
|
+
get loopEnd(): Time {
|
|
480
|
+
return this._loopEnd;
|
|
481
|
+
}
|
|
482
|
+
set loopEnd(loopEnd) {
|
|
483
|
+
this._loopEnd = loopEnd;
|
|
484
|
+
this._providedMidiNotes.forEach((midiNote) => {
|
|
485
|
+
const buffer = this._buffers.get(midiNote);
|
|
486
|
+
if (buffer.loaded) {
|
|
487
|
+
assertRange(this.toSeconds(loopEnd), 0, buffer.duration);
|
|
488
|
+
}
|
|
489
|
+
});
|
|
490
|
+
// get the current sources
|
|
491
|
+
this._activeSources.forEach((sourceList) => {
|
|
492
|
+
sourceList.forEach((source) => {
|
|
493
|
+
source.loopEnd = loopEnd;
|
|
494
|
+
});
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* If the buffers should loop once they are over.
|
|
500
|
+
* @example
|
|
501
|
+
* const sampler = new Tone.Sampler({
|
|
502
|
+
* urls: {
|
|
503
|
+
* A4: "https://tonejs.github.io/audio/berklee/femalevoice_aa_A4.mp3",
|
|
504
|
+
* },
|
|
505
|
+
* }).toDestination();
|
|
506
|
+
* sampler.loop = true;
|
|
507
|
+
*/
|
|
508
|
+
get loop(): boolean {
|
|
509
|
+
return this._loop;
|
|
510
|
+
}
|
|
511
|
+
set loop(loop) {
|
|
512
|
+
// if no change, do nothing
|
|
513
|
+
if (this._loop === loop) {
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
this._loop = loop;
|
|
517
|
+
// set the loop of all of the sources
|
|
518
|
+
this._activeSources.forEach((sourceList) => {
|
|
519
|
+
sourceList.forEach((source) => {
|
|
520
|
+
source.loop = loop;
|
|
521
|
+
});
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
|
|
488
525
|
/**
|
|
489
526
|
* Clean up
|
|
490
527
|
*/
|
|
@@ -38,30 +38,30 @@ describe("ToneConstantSource", () => {
|
|
|
38
38
|
});
|
|
39
39
|
|
|
40
40
|
context("onended", () => {
|
|
41
|
-
it("invokes the onended callback in the online context", (
|
|
41
|
+
it("invokes the onended callback in the online context", async () => {
|
|
42
42
|
const source = new ToneConstantSource();
|
|
43
43
|
source.start();
|
|
44
44
|
source.stop("+0.3");
|
|
45
45
|
const now = source.now();
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
46
|
+
await new Promise<void>((resolve) => {
|
|
47
|
+
source.onended = () => resolve();
|
|
48
|
+
});
|
|
49
|
+
expect(source.now() - now).to.be.within(0.25, 0.6);
|
|
50
|
+
source.dispose();
|
|
51
51
|
});
|
|
52
52
|
|
|
53
|
-
it("invokes the onended callback only once in the online context", (
|
|
53
|
+
it("invokes the onended callback only once in the online context", async () => {
|
|
54
54
|
const source = new ToneConstantSource();
|
|
55
55
|
source.start();
|
|
56
56
|
source.stop("+0.1");
|
|
57
57
|
source.stop("+0.2");
|
|
58
58
|
source.stop("+0.3");
|
|
59
59
|
const now = source.now();
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
60
|
+
await new Promise<void>((resolve) => {
|
|
61
|
+
source.onended = () => resolve();
|
|
62
|
+
});
|
|
63
|
+
expect(source.now() - now).to.be.within(0.25, 0.6);
|
|
64
|
+
source.dispose();
|
|
65
65
|
});
|
|
66
66
|
|
|
67
67
|
it("invokes the onended callback in the offline context", async () => {
|
|
@@ -129,6 +129,17 @@ describe("ToneConstantSource", () => {
|
|
|
129
129
|
expect(buffer.getValueAtTime(0.2)).to.equal(0);
|
|
130
130
|
});
|
|
131
131
|
|
|
132
|
+
it("stops the native ConstantSourceNode once stop() takes effect", async () => {
|
|
133
|
+
const stopSpy = sinon.spy(ConstantSourceNode.prototype, "stop");
|
|
134
|
+
const source = new ToneConstantSource().start();
|
|
135
|
+
source.stop();
|
|
136
|
+
await new Promise<void>((resolve) => {
|
|
137
|
+
source.onended = () => resolve();
|
|
138
|
+
});
|
|
139
|
+
expect(stopSpy).to.have.been.calledOnce;
|
|
140
|
+
source.dispose();
|
|
141
|
+
});
|
|
142
|
+
|
|
132
143
|
it("clamps start time to the currentTime", () => {
|
|
133
144
|
const source = new ToneConstantSource();
|
|
134
145
|
source.start(0);
|
|
@@ -31,6 +31,11 @@ export class ToneConstantSource<
|
|
|
31
31
|
*/
|
|
32
32
|
private _source?: ConstantSourceNode;
|
|
33
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Whether the native source was started, so it can be stopped
|
|
36
|
+
*/
|
|
37
|
+
private _sourceStarted = false;
|
|
38
|
+
|
|
34
39
|
/**
|
|
35
40
|
* The offset of the signal generator
|
|
36
41
|
*/
|
|
@@ -86,6 +91,7 @@ export class ToneConstantSource<
|
|
|
86
91
|
this._stopTime !== -1 && this._stopTime < this._startTime;
|
|
87
92
|
if (isStarted && !isStoppedBeforeStart) {
|
|
88
93
|
this._source.start(this._startTime);
|
|
94
|
+
this._sourceStarted = true;
|
|
89
95
|
}
|
|
90
96
|
}
|
|
91
97
|
|
|
@@ -97,15 +103,19 @@ export class ToneConstantSource<
|
|
|
97
103
|
const computedTime = this.toSeconds(time);
|
|
98
104
|
this.log("start", computedTime);
|
|
99
105
|
this._startGain(computedTime);
|
|
100
|
-
this._source
|
|
106
|
+
if (this._source) {
|
|
107
|
+
this._source.start(computedTime);
|
|
108
|
+
this._sourceStarted = true;
|
|
109
|
+
}
|
|
101
110
|
return this;
|
|
102
111
|
}
|
|
103
112
|
|
|
104
113
|
protected _stopSource(time?: Seconds): void {
|
|
105
|
-
|
|
106
|
-
|
|
114
|
+
// state is already "stopped" here: OneShotSource defers this call
|
|
115
|
+
// until after _stopTime, so check the native node instead
|
|
116
|
+
if (this._sourceStarted) {
|
|
117
|
+
this._source?.stop(time);
|
|
107
118
|
}
|
|
108
|
-
this._source?.stop(time);
|
|
109
119
|
}
|
|
110
120
|
|
|
111
121
|
dispose(): this {
|
package/Tone/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const version: string = "15.5.
|
|
1
|
+
export const version: string = "15.5.58";
|