1 | import { expect } from "chai";
|
2 | import { atTime, Offline } from "../../test/helper/Offline.js";
|
3 | import { ToneAudioBuffer } from "../core/context/ToneAudioBuffer.js";
|
4 | import { getContext } from "../core/Global.js";
|
5 | import { Player } from "./buffer/Player.js";
|
6 | import { Oscillator } from "./oscillator/Oscillator.js";
|
7 |
|
8 | describe("Source", () => {
|
9 | it("can be started and stopped", () => {
|
10 | const source = new Oscillator();
|
11 | source.start(0);
|
12 | source.stop(1);
|
13 | source.dispose();
|
14 | });
|
15 |
|
16 | it("can be constructed with an options object", () => {
|
17 | const source = new Oscillator({
|
18 | volume: -20,
|
19 | });
|
20 | expect(source.volume.value).to.be.closeTo(-20, 0.1);
|
21 | source.dispose();
|
22 | });
|
23 |
|
24 | it("can be muted in the constructor options", () => {
|
25 | const source = new Oscillator({
|
26 | mute: true,
|
27 | });
|
28 | expect(source.mute).to.be.true;
|
29 | source.dispose();
|
30 | });
|
31 |
|
32 | it("can set the volume", () => {
|
33 | const source = new Oscillator();
|
34 | source.volume.value = -8;
|
35 | expect(source.volume.value).to.be.closeTo(-8, 0.1);
|
36 | source.dispose();
|
37 | });
|
38 |
|
39 | it("can mute and unmute the source", () => {
|
40 | const source = new Oscillator();
|
41 | source.volume.value = -8;
|
42 | source.mute = true;
|
43 | expect(source.mute).to.be.true;
|
44 | expect(source.volume.value).to.equal(-Infinity);
|
45 | source.mute = false;
|
46 |
|
47 | expect(source.volume.value).to.be.closeTo(-8, 0.1);
|
48 | source.dispose();
|
49 | });
|
50 |
|
51 | it("can get and set values with an object", () => {
|
52 | const source = new Oscillator();
|
53 | source.set({ volume: -10 });
|
54 | expect(source.get().volume).to.be.closeTo(-10, 0.1);
|
55 | source.dispose();
|
56 | });
|
57 |
|
58 | it("is initally stopped", () => {
|
59 | const source = new Oscillator();
|
60 | expect(source.state).to.equal("stopped");
|
61 | source.dispose();
|
62 | });
|
63 |
|
64 | it("cannot be scheduled to stop/start twice in a row", () => {
|
65 | return Offline(() => {
|
66 | const source = new Oscillator();
|
67 | source.start(0).start(1);
|
68 | source.stop(2).stop(3);
|
69 | source.dispose();
|
70 | });
|
71 | });
|
72 |
|
73 | it("can be scheduled with multiple starts/stops", () => {
|
74 | return Offline(() => {
|
75 | const source = new Oscillator();
|
76 | source.start(0).stop(0.5).start(0.75).stop(1).start(1.25).stop(1.5);
|
77 | return [
|
78 | atTime(0.1, () => {
|
79 | expect(source.state).to.equal("started");
|
80 | }),
|
81 | atTime(0.5, () => {
|
82 | expect(source.state).to.equal("stopped");
|
83 | }),
|
84 | atTime(0.8, () => {
|
85 | expect(source.state).to.equal("started");
|
86 | }),
|
87 | atTime(1, () => {
|
88 | expect(source.state).to.equal("stopped");
|
89 | }),
|
90 | atTime(1.25, () => {
|
91 | expect(source.state).to.equal("started");
|
92 | }),
|
93 | atTime(1.6, () => {
|
94 | expect(source.state).to.equal("stopped");
|
95 | }),
|
96 | ];
|
97 | }, 2);
|
98 | });
|
99 |
|
100 | it("clamps start time to the currentTime", (done) => {
|
101 | const source = new Oscillator();
|
102 | expect(source.state).to.equal("stopped");
|
103 | source.start(0);
|
104 | setTimeout(() => {
|
105 | expect(source.state).to.equal("started");
|
106 | source.dispose();
|
107 | done();
|
108 | }, 10);
|
109 | });
|
110 |
|
111 | it("clamps stop time to the currentTime", (done) => {
|
112 | const source = new Oscillator();
|
113 | expect(source.state).to.equal("stopped");
|
114 | source.start(0);
|
115 | setTimeout(() => {
|
116 | expect(source.state).to.equal("started");
|
117 | source.stop(0);
|
118 | setTimeout(() => {
|
119 | expect(source.state).to.equal("stopped");
|
120 | source.dispose();
|
121 | done();
|
122 | }, 10);
|
123 | }, 10);
|
124 | });
|
125 |
|
126 | it("correctly returns the scheduled play state", () => {
|
127 | return Offline(() => {
|
128 | const source = new Oscillator();
|
129 | expect(source.state).to.equal("stopped");
|
130 | source.start(0).stop(0.5);
|
131 |
|
132 | return (time) => {
|
133 | if (time >= 0 && time < 0.5) {
|
134 | expect(source.state).to.equal("started");
|
135 | } else if (time > 0.5) {
|
136 | expect(source.state).to.equal("stopped");
|
137 | }
|
138 | };
|
139 | }, 0.6);
|
140 | });
|
141 |
|
142 | it("start needs to be greater than the previous start time", () => {
|
143 | return Offline(() => {
|
144 | const source = new Oscillator();
|
145 | source.start(0);
|
146 | expect(() => {
|
147 | source.start(0);
|
148 | }).to.throw(Error);
|
149 | source.dispose();
|
150 | });
|
151 | });
|
152 |
|
153 | context("sync", () => {
|
154 | const ramp = new Float32Array(getContext().sampleRate);
|
155 | ramp.forEach((val, index) => {
|
156 | ramp[index] = index / getContext().sampleRate;
|
157 | });
|
158 | const rampBuffer = ToneAudioBuffer.fromArray(ramp);
|
159 |
|
160 | it("can sync its start to the transport", () => {
|
161 | return Offline(({ transport }) => {
|
162 | const source = new Oscillator();
|
163 | source.sync().start(0);
|
164 | expect(source.state).to.equal("stopped");
|
165 | transport.start(source.now());
|
166 | expect(source.state).to.equal("started");
|
167 | source.dispose();
|
168 | transport.stop();
|
169 | });
|
170 | });
|
171 |
|
172 | it("calling sync multiple times has no affect", () => {
|
173 | return Offline(({ transport }) => {
|
174 | const source = new Oscillator();
|
175 | source.sync().sync().start(0);
|
176 | expect(source.state).to.equal("stopped");
|
177 | transport.start(source.now());
|
178 | expect(source.state).to.equal("started");
|
179 | source.dispose();
|
180 | transport.stop();
|
181 | });
|
182 | });
|
183 |
|
184 | it("can unsync after it was synced", () => {
|
185 | return Offline(({ transport }) => {
|
186 | const source = new Oscillator();
|
187 | source.sync().start(0);
|
188 | source.unsync();
|
189 | transport.start();
|
190 | expect(source.state).to.equal("stopped");
|
191 | });
|
192 | });
|
193 |
|
194 | it("calling unsync multiple times has no affect", () => {
|
195 | return Offline(({ transport }) => {
|
196 | const source = new Oscillator();
|
197 | source.sync().start(0);
|
198 | source.unsync().unsync();
|
199 | transport.start();
|
200 | expect(source.state).to.equal("stopped");
|
201 | });
|
202 | });
|
203 |
|
204 | it("can sync its stop to the transport", () => {
|
205 | return Offline(({ transport }) => {
|
206 | const source = new Oscillator();
|
207 | source.sync().start(0);
|
208 | expect(source.state).to.equal("stopped");
|
209 | transport.start(0).stop(0.4);
|
210 | expect(source.state).to.equal("started");
|
211 |
|
212 | return (time) => {
|
213 | if (time > 0.4) {
|
214 | expect(source.state).to.equal("stopped");
|
215 | }
|
216 | };
|
217 | }, 0.5);
|
218 | });
|
219 |
|
220 | it("can schedule multiple starts/stops", () => {
|
221 | return Offline(({ transport }) => {
|
222 | const source = new Oscillator();
|
223 | source.sync().start(0.1).stop(0.2).start(0.3);
|
224 | transport.start(0).stop(0.4);
|
225 | expect(source.state).to.equal("stopped");
|
226 |
|
227 | return (time) => {
|
228 | if (time > 0.1 && time < 0.19) {
|
229 | expect(source.state).to.equal("started");
|
230 | } else if (time > 0.2 && time < 0.29) {
|
231 | expect(source.state).to.equal("stopped");
|
232 | } else if (time > 0.3 && time < 0.39) {
|
233 | expect(source.state).to.equal("started");
|
234 | } else if (time > 0.4) {
|
235 | expect(source.state).to.equal("stopped");
|
236 | }
|
237 | };
|
238 | }, 0.6);
|
239 | });
|
240 |
|
241 | it.skip("can sync schedule multiple starts", () => {
|
242 | return Offline(({ transport }) => {
|
243 | const buff = ToneAudioBuffer.fromArray(
|
244 | new Float32Array(1024).map((v) => 1)
|
245 | );
|
246 | const source = new Player(buff);
|
247 | source.sync().start(0.1).start(0.3);
|
248 | transport.start(0);
|
249 | expect(source.state).to.equal("stopped");
|
250 |
|
251 | return [
|
252 | atTime(0.11, () => {
|
253 | expect(source.state).to.equal("started");
|
254 | }),
|
255 | atTime(0.31, () => {
|
256 | expect(source.state).to.equal("started");
|
257 | }),
|
258 | ];
|
259 | }, 0.6);
|
260 | });
|
261 |
|
262 | it("has correct offset when the transport is started with an offset", () => {
|
263 | return Offline(({ transport }) => {
|
264 | const source = new Oscillator();
|
265 | source.sync().start(0.3).stop(0.4);
|
266 | transport.start(0, 0.1);
|
267 | expect(source.state).to.equal("stopped");
|
268 |
|
269 | return (time) => {
|
270 | if (time > 0.21 && time < 0.29) {
|
271 | expect(source.state).to.equal("started");
|
272 | } else if (time > 0.31) {
|
273 | expect(source.state).to.equal("stopped");
|
274 | }
|
275 | };
|
276 | }, 0.5);
|
277 | });
|
278 |
|
279 | it("can start with an offset after the start time of the source", () => {
|
280 | return Offline(({ transport }) => {
|
281 | const source = new Oscillator();
|
282 | source.sync().start(0);
|
283 | transport.start(0, 0.1);
|
284 | expect(source.state).to.equal("started");
|
285 | source.dispose();
|
286 | }, 0.1);
|
287 | });
|
288 |
|
289 | it("can sync its start to the transport after a delay", () => {
|
290 | return Offline(({ transport }) => {
|
291 | const source = new Oscillator();
|
292 | source.sync().start(0.3);
|
293 | transport.start(0).stop(0.4);
|
294 | expect(source.state).to.equal("stopped");
|
295 |
|
296 | return (time) => {
|
297 | if (time > 0.3 && time < 0.39) {
|
298 | expect(source.state).to.equal("started");
|
299 | } else if (time > 0.4) {
|
300 | expect(source.state).to.equal("stopped");
|
301 | }
|
302 | };
|
303 | }, 0.6);
|
304 | });
|
305 |
|
306 | it("correct state when the transport position is changed", () => {
|
307 | return Offline(({ transport }) => {
|
308 | const source = new Oscillator();
|
309 | source.sync().start(0.3).stop(0.4);
|
310 | transport.start(0).stop(0.4);
|
311 | expect(source.state).to.equal("stopped");
|
312 | transport.seconds = 0.305;
|
313 | expect(source.state).to.equal("started");
|
314 | transport.seconds = 0.405;
|
315 | return atTime(0.01, () => {
|
316 | expect(source.state).to.equal("stopped");
|
317 | });
|
318 | }, 0.1);
|
319 | });
|
320 |
|
321 | it("gives the correct offset on time on start/stop events", () => {
|
322 | return Offline(({ transport }) => {
|
323 | const source = new Player(rampBuffer).toDestination();
|
324 | source.sync().start(0.2, 0.1).stop(0.3);
|
325 | transport.start(0.2);
|
326 | }, 0.7).then((output) => {
|
327 | expect(output.getValueAtTime(0.41)).to.be.closeTo(0.1, 0.01);
|
328 | expect(output.getValueAtTime(0.45)).to.be.closeTo(0.15, 0.001);
|
329 | expect(output.getValueAtTime(0.5)).to.be.equal(0);
|
330 | });
|
331 | });
|
332 |
|
333 | it("gives the correct offset on time on start/stop events when started with an offset", () => {
|
334 | return Offline(({ transport }) => {
|
335 | const source = new Player(rampBuffer).toDestination();
|
336 | source.sync().start(0.2, 0.1).stop(0.4);
|
337 | transport.start(0.2, 0.1);
|
338 | }, 0.7).then((output) => {
|
339 | expect(output.getValueAtTime(0.21)).to.be.closeTo(0.0, 0.01);
|
340 | expect(output.getValueAtTime(0.31)).to.be.closeTo(0.1, 0.01);
|
341 | expect(output.getValueAtTime(0.41)).to.be.closeTo(0.2, 0.01);
|
342 | expect(output.getValueAtTime(0.45)).to.be.closeTo(0.25, 0.01);
|
343 | expect(output.getValueAtTime(0.51)).to.be.equal(0);
|
344 | });
|
345 | });
|
346 |
|
347 | it("gives the correct offset on time on start/stop events invoked with an transport offset that's in the middle of the event", () => {
|
348 | return Offline(({ transport }) => {
|
349 | const source = new Player(rampBuffer).toDestination();
|
350 | source.sync().start(0.2, 0.1).stop(0.4);
|
351 | transport.start(0, 0.3);
|
352 | }, 0.7).then((output) => {
|
353 | expect(output.getValueAtTime(0.01)).to.be.closeTo(0.2, 0.01);
|
354 | expect(output.getValueAtTime(0.05)).to.be.closeTo(0.25, 0.01);
|
355 | expect(output.getValueAtTime(0.11)).to.be.equal(0);
|
356 | });
|
357 | });
|
358 |
|
359 | it("gives the correct duration when invoked with an transport offset that's in the middle of the event", () => {
|
360 | return Offline(({ transport }) => {
|
361 | const source = new Player(rampBuffer).toDestination();
|
362 | source.sync().start(0.2, 0.1, 0.3);
|
363 | transport.start(0, 0.3);
|
364 | }, 0.7).then((output) => {
|
365 | expect(output.getValueAtTime(0.01)).to.be.closeTo(0.2, 0.01);
|
366 | expect(output.getValueAtTime(0.1)).to.be.closeTo(0.3, 0.01);
|
367 | expect(output.getValueAtTime(0.199)).to.be.closeTo(0.4, 0.01);
|
368 | expect(output.getValueAtTime(0.31)).to.be.equal(0);
|
369 | });
|
370 | });
|
371 |
|
372 | it("stops at the right time when transport.stop is invoked before the scheduled stop", () => {
|
373 | return Offline(({ transport }) => {
|
374 | const source = new Player(rampBuffer).toDestination();
|
375 | source.sync().start(0.2).stop(0.4);
|
376 | transport.start(0).stop(0.3);
|
377 | }, 0.7).then((output) => {
|
378 | expect(output.getValueAtTime(0.2)).to.be.closeTo(0.0, 0.01);
|
379 | expect(output.getValueAtTime(0.25)).to.be.closeTo(0.05, 0.01);
|
380 | expect(output.getValueAtTime(0.31)).to.be.equal(0);
|
381 | });
|
382 | });
|
383 |
|
384 | it("invokes the right methods and offsets when the transport is seeked", () => {
|
385 | return Offline(({ transport }) => {
|
386 | const source = new Player(rampBuffer).toDestination();
|
387 | source.sync().start(0.2);
|
388 | transport.start(0, 0.3);
|
389 |
|
390 | return atTime(0.1, () => {
|
391 |
|
392 | transport.seconds = 0.1;
|
393 | });
|
394 | }, 0.7).then((output) => {
|
395 | expect(output.getValueAtTime(0.01)).to.be.closeTo(0.1, 0.01);
|
396 | expect(output.getValueAtTime(0.05)).to.be.closeTo(0.15, 0.01);
|
397 | expect(output.getValueAtTime(0.11)).to.be.closeTo(0.0, 0.01);
|
398 | expect(output.getValueAtTime(0.21)).to.be.closeTo(0.0, 0.01);
|
399 | expect(output.getValueAtTime(0.25)).to.be.closeTo(0.05, 0.01);
|
400 | expect(output.getValueAtTime(0.3)).to.be.closeTo(0.1, 0.01);
|
401 | });
|
402 | });
|
403 | });
|
404 | });
|