1 | import { Volume } from "../component/channel/Volume.js";
|
2 | import "../core/context/Destination.js";
|
3 | import "../core/clock/Transport.js";
|
4 | import { Param } from "../core/context/Param.js";
|
5 | import {
|
6 | OutputNode,
|
7 | ToneAudioNode,
|
8 | ToneAudioNodeOptions,
|
9 | } from "../core/context/ToneAudioNode.js";
|
10 | import { Decibels, Seconds, Time } from "../core/type/Units.js";
|
11 | import { defaultArg } from "../core/util/Defaults.js";
|
12 | import { noOp, readOnly } from "../core/util/Interface.js";
|
13 | import {
|
14 | BasicPlaybackState,
|
15 | StateTimeline,
|
16 | StateTimelineEvent,
|
17 | } from "../core/util/StateTimeline.js";
|
18 | import { isDefined, isUndef } from "../core/util/TypeCheck.js";
|
19 | import { assert, assertContextRunning } from "../core/util/Debug.js";
|
20 | import { GT } from "../core/util/Math.js";
|
21 |
|
22 | type onStopCallback = (source: Source<any>) => void;
|
23 |
|
24 | export interface SourceOptions extends ToneAudioNodeOptions {
|
25 | volume: Decibels;
|
26 | mute: boolean;
|
27 | onstop: onStopCallback;
|
28 | }
|
29 |
|
30 |
|
31 |
|
32 |
|
33 |
|
34 |
|
35 |
|
36 |
|
37 |
|
38 |
|
39 |
|
40 |
|
41 |
|
42 |
|
43 |
|
44 |
|
45 |
|
46 |
|
47 | export abstract class Source<
|
48 | Options extends SourceOptions,
|
49 | > extends ToneAudioNode<Options> {
|
50 | |
51 |
|
52 |
|
53 | private _volume: Volume;
|
54 |
|
55 | |
56 |
|
57 |
|
58 | output: OutputNode;
|
59 |
|
60 | |
61 |
|
62 |
|
63 | input = undefined;
|
64 |
|
65 | |
66 |
|
67 |
|
68 |
|
69 |
|
70 |
|
71 | volume: Param<"decibels">;
|
72 |
|
73 | |
74 |
|
75 |
|
76 | onstop: onStopCallback;
|
77 |
|
78 | |
79 |
|
80 |
|
81 | protected _state: StateTimeline<{
|
82 | duration?: Seconds;
|
83 | offset?: Seconds;
|
84 | |
85 |
|
86 |
|
87 |
|
88 | implicitEnd?: boolean;
|
89 | }> = new StateTimeline("stopped");
|
90 |
|
91 | |
92 |
|
93 |
|
94 | protected _synced = false;
|
95 |
|
96 | |
97 |
|
98 |
|
99 | private _scheduled: number[] = [];
|
100 |
|
101 | |
102 |
|
103 |
|
104 | private _syncedStart: (time: Seconds, offset: Seconds) => void = noOp;
|
105 | private _syncedStop: (time: Seconds) => void = noOp;
|
106 |
|
107 | constructor(options: SourceOptions) {
|
108 | super(options);
|
109 | this._state.memory = 100;
|
110 | this._state.increasing = true;
|
111 |
|
112 | this._volume = this.output = new Volume({
|
113 | context: this.context,
|
114 | mute: options.mute,
|
115 | volume: options.volume,
|
116 | });
|
117 | this.volume = this._volume.volume;
|
118 | readOnly(this, "volume");
|
119 | this.onstop = options.onstop;
|
120 | }
|
121 |
|
122 | static getDefaults(): SourceOptions {
|
123 | return Object.assign(ToneAudioNode.getDefaults(), {
|
124 | mute: false,
|
125 | onstop: noOp,
|
126 | volume: 0,
|
127 | });
|
128 | }
|
129 |
|
130 | |
131 |
|
132 |
|
133 |
|
134 |
|
135 |
|
136 |
|
137 |
|
138 | get state(): BasicPlaybackState {
|
139 | if (this._synced) {
|
140 | if (this.context.transport.state === "started") {
|
141 | return this._state.getValueAtTime(
|
142 | this.context.transport.seconds
|
143 | ) as BasicPlaybackState;
|
144 | } else {
|
145 | return "stopped";
|
146 | }
|
147 | } else {
|
148 | return this._state.getValueAtTime(this.now()) as BasicPlaybackState;
|
149 | }
|
150 | }
|
151 |
|
152 | |
153 |
|
154 |
|
155 |
|
156 |
|
157 |
|
158 |
|
159 | get mute(): boolean {
|
160 | return this._volume.mute;
|
161 | }
|
162 | set mute(mute: boolean) {
|
163 | this._volume.mute = mute;
|
164 | }
|
165 |
|
166 |
|
167 | protected abstract _start(time: Time, offset?: Time, duration?: Time): void;
|
168 | protected abstract _stop(time: Time): void;
|
169 | protected abstract _restart(
|
170 | time: Seconds,
|
171 | offset?: Time,
|
172 | duration?: Time
|
173 | ): void;
|
174 |
|
175 | |
176 |
|
177 |
|
178 |
|
179 | private _clampToCurrentTime(time: Seconds): Seconds {
|
180 | if (this._synced) {
|
181 | return time;
|
182 | } else {
|
183 | return Math.max(time, this.context.currentTime);
|
184 | }
|
185 | }
|
186 |
|
187 | |
188 |
|
189 |
|
190 |
|
191 |
|
192 |
|
193 |
|
194 |
|
195 | start(time?: Time, offset?: Time, duration?: Time): this {
|
196 | let computedTime =
|
197 | isUndef(time) && this._synced
|
198 | ? this.context.transport.seconds
|
199 | : this.toSeconds(time);
|
200 | computedTime = this._clampToCurrentTime(computedTime);
|
201 |
|
202 | if (
|
203 | !this._synced &&
|
204 | this._state.getValueAtTime(computedTime) === "started"
|
205 | ) {
|
206 |
|
207 | assert(
|
208 | GT(
|
209 | computedTime,
|
210 | (this._state.get(computedTime) as StateTimelineEvent).time
|
211 | ),
|
212 | "Start time must be strictly greater than previous start time"
|
213 | );
|
214 | this._state.cancel(computedTime);
|
215 | this._state.setStateAtTime("started", computedTime);
|
216 | this.log("restart", computedTime);
|
217 | this.restart(computedTime, offset, duration);
|
218 | } else {
|
219 | this.log("start", computedTime);
|
220 | this._state.setStateAtTime("started", computedTime);
|
221 | if (this._synced) {
|
222 |
|
223 | const event = this._state.get(computedTime);
|
224 | if (event) {
|
225 | event.offset = this.toSeconds(defaultArg(offset, 0));
|
226 | event.duration = duration
|
227 | ? this.toSeconds(duration)
|
228 | : undefined;
|
229 | }
|
230 | const sched = this.context.transport.schedule((t) => {
|
231 | this._start(t, offset, duration);
|
232 | }, computedTime);
|
233 | this._scheduled.push(sched);
|
234 |
|
235 |
|
236 |
|
237 | if (
|
238 | this.context.transport.state === "started" &&
|
239 | this.context.transport.getSecondsAtTime(this.immediate()) >
|
240 | computedTime
|
241 | ) {
|
242 | this._syncedStart(
|
243 | this.now(),
|
244 | this.context.transport.seconds
|
245 | );
|
246 | }
|
247 | } else {
|
248 | assertContextRunning(this.context);
|
249 | this._start(computedTime, offset, duration);
|
250 | }
|
251 | }
|
252 | return this;
|
253 | }
|
254 |
|
255 | |
256 |
|
257 |
|
258 |
|
259 |
|
260 |
|
261 |
|
262 |
|
263 |
|
264 | stop(time?: Time): this {
|
265 | let computedTime =
|
266 | isUndef(time) && this._synced
|
267 | ? this.context.transport.seconds
|
268 | : this.toSeconds(time);
|
269 | computedTime = this._clampToCurrentTime(computedTime);
|
270 | if (
|
271 | this._state.getValueAtTime(computedTime) === "started" ||
|
272 | isDefined(this._state.getNextState("started", computedTime))
|
273 | ) {
|
274 | this.log("stop", computedTime);
|
275 | if (!this._synced) {
|
276 | this._stop(computedTime);
|
277 | } else {
|
278 | const sched = this.context.transport.schedule(
|
279 | this._stop.bind(this),
|
280 | computedTime
|
281 | );
|
282 | this._scheduled.push(sched);
|
283 | }
|
284 | this._state.cancel(computedTime);
|
285 | this._state.setStateAtTime("stopped", computedTime);
|
286 | }
|
287 | return this;
|
288 | }
|
289 |
|
290 | |
291 |
|
292 |
|
293 | restart(time?: Time, offset?: Time, duration?: Time): this {
|
294 | time = this.toSeconds(time);
|
295 | if (this._state.getValueAtTime(time) === "started") {
|
296 | this._state.cancel(time);
|
297 | this._restart(time, offset, duration);
|
298 | }
|
299 | return this;
|
300 | }
|
301 |
|
302 | |
303 |
|
304 |
|
305 |
|
306 |
|
307 |
|
308 |
|
309 |
|
310 |
|
311 |
|
312 |
|
313 |
|
314 |
|
315 |
|
316 |
|
317 | sync(): this {
|
318 | if (!this._synced) {
|
319 | this._synced = true;
|
320 | this._syncedStart = (time, offset) => {
|
321 | if (GT(offset, 0)) {
|
322 |
|
323 | const stateEvent = this._state.get(offset);
|
324 |
|
325 | if (
|
326 | stateEvent &&
|
327 | stateEvent.state === "started" &&
|
328 | stateEvent.time !== offset
|
329 | ) {
|
330 |
|
331 | const startOffset =
|
332 | offset - this.toSeconds(stateEvent.time);
|
333 | let duration: number | undefined;
|
334 | if (stateEvent.duration) {
|
335 | duration =
|
336 | this.toSeconds(stateEvent.duration) -
|
337 | startOffset;
|
338 | }
|
339 | this._start(
|
340 | time,
|
341 | this.toSeconds(stateEvent.offset) + startOffset,
|
342 | duration
|
343 | );
|
344 | }
|
345 | }
|
346 | };
|
347 | this._syncedStop = (time) => {
|
348 | const seconds = this.context.transport.getSecondsAtTime(
|
349 | Math.max(time - this.sampleTime, 0)
|
350 | );
|
351 | if (this._state.getValueAtTime(seconds) === "started") {
|
352 | this._stop(time);
|
353 | }
|
354 | };
|
355 | this.context.transport.on("start", this._syncedStart);
|
356 | this.context.transport.on("loopStart", this._syncedStart);
|
357 | this.context.transport.on("stop", this._syncedStop);
|
358 | this.context.transport.on("pause", this._syncedStop);
|
359 | this.context.transport.on("loopEnd", this._syncedStop);
|
360 | }
|
361 | return this;
|
362 | }
|
363 |
|
364 | |
365 |
|
366 |
|
367 |
|
368 | unsync(): this {
|
369 | if (this._synced) {
|
370 | this.context.transport.off("stop", this._syncedStop);
|
371 | this.context.transport.off("pause", this._syncedStop);
|
372 | this.context.transport.off("loopEnd", this._syncedStop);
|
373 | this.context.transport.off("start", this._syncedStart);
|
374 | this.context.transport.off("loopStart", this._syncedStart);
|
375 | }
|
376 | this._synced = false;
|
377 |
|
378 | this._scheduled.forEach((id) => this.context.transport.clear(id));
|
379 | this._scheduled = [];
|
380 | this._state.cancel(0);
|
381 |
|
382 | this._stop(0);
|
383 | return this;
|
384 | }
|
385 |
|
386 | |
387 |
|
388 |
|
389 | dispose(): this {
|
390 | super.dispose();
|
391 | this.onstop = noOp;
|
392 | this.unsync();
|
393 | this._volume.dispose();
|
394 | this._state.dispose();
|
395 | return this;
|
396 | }
|
397 | }
|