UNPKG

6.21 kBPlain TextView Raw
1import { Gain } from "../core/context/Gain.js";
2import {
3 ToneAudioNode,
4 ToneAudioNodeOptions,
5} from "../core/context/ToneAudioNode.js";
6import { GainFactor, Seconds, Time } from "../core/type/Units.js";
7import { noOp } from "../core/util/Interface.js";
8import { assert } from "../core/util/Debug.js";
9import { BasicPlaybackState } from "../core/util/StateTimeline.js";
10
11export type OneShotSourceCurve = "linear" | "exponential";
12
13type onEndedCallback = (source: OneShotSource<any>) => void;
14
15export interface OneShotSourceOptions extends ToneAudioNodeOptions {
16 onended: onEndedCallback;
17 fadeIn: Time;
18 fadeOut: Time;
19 curve: OneShotSourceCurve;
20}
21
22/**
23 * Base class for fire-and-forget nodes
24 */
25export abstract class OneShotSource<
26 Options extends ToneAudioNodeOptions,
27> extends ToneAudioNode<Options> {
28 /**
29 * The callback to invoke after the
30 * source is done playing.
31 */
32 onended: onEndedCallback = noOp;
33
34 /**
35 * Sources do not have input nodes
36 */
37 input: undefined;
38
39 /**
40 * The start time
41 */
42 protected _startTime = -1;
43
44 /**
45 * The stop time
46 */
47 protected _stopTime = -1;
48
49 /**
50 * The id of the timeout
51 */
52 private _timeout = -1;
53
54 /**
55 * The public output node
56 */
57 output: Gain = new Gain({
58 context: this.context,
59 gain: 0,
60 });
61
62 /**
63 * The output gain node.
64 */
65 protected _gainNode = this.output;
66
67 /**
68 * The fadeIn time of the amplitude envelope.
69 */
70 protected _fadeIn: Time;
71
72 /**
73 * The fadeOut time of the amplitude envelope.
74 */
75 protected _fadeOut: Time;
76
77 /**
78 * The curve applied to the fades, either "linear" or "exponential"
79 */
80 protected _curve: OneShotSourceCurve;
81
82 constructor(options: OneShotSourceOptions) {
83 super(options);
84
85 this._fadeIn = options.fadeIn;
86 this._fadeOut = options.fadeOut;
87 this._curve = options.curve;
88 this.onended = options.onended;
89 }
90
91 static getDefaults(): OneShotSourceOptions {
92 return Object.assign(ToneAudioNode.getDefaults(), {
93 curve: "linear" as OneShotSourceCurve,
94 fadeIn: 0,
95 fadeOut: 0,
96 onended: noOp,
97 });
98 }
99
100 /**
101 * Stop the source node
102 */
103 protected abstract _stopSource(time: Seconds): void;
104
105 /**
106 * Start the source node at the given time
107 * @param time When to start the node
108 */
109 protected abstract start(time?: Time): this;
110 /**
111 * Start the source at the given time
112 * @param time When to start the source
113 */
114 protected _startGain(time: Seconds, gain: GainFactor = 1): this {
115 assert(
116 this._startTime === -1,
117 "Source cannot be started more than once"
118 );
119 // apply a fade in envelope
120 const fadeInTime = this.toSeconds(this._fadeIn);
121
122 // record the start time
123 this._startTime = time + fadeInTime;
124 this._startTime = Math.max(this._startTime, this.context.currentTime);
125
126 // schedule the envelope
127 if (fadeInTime > 0) {
128 this._gainNode.gain.setValueAtTime(0, time);
129 if (this._curve === "linear") {
130 this._gainNode.gain.linearRampToValueAtTime(
131 gain,
132 time + fadeInTime
133 );
134 } else {
135 this._gainNode.gain.exponentialApproachValueAtTime(
136 gain,
137 time,
138 fadeInTime
139 );
140 }
141 } else {
142 this._gainNode.gain.setValueAtTime(gain, time);
143 }
144 return this;
145 }
146
147 /**
148 * Stop the source node at the given time.
149 * @param time When to stop the source
150 */
151 stop(time?: Time): this {
152 this.log("stop", time);
153 this._stopGain(this.toSeconds(time));
154 return this;
155 }
156
157 /**
158 * Stop the source at the given time
159 * @param time When to stop the source
160 */
161 protected _stopGain(time: Seconds): this {
162 assert(this._startTime !== -1, "'start' must be called before 'stop'");
163 // cancel the previous stop
164 this.cancelStop();
165
166 // the fadeOut time
167 const fadeOutTime = this.toSeconds(this._fadeOut);
168
169 // schedule the stop callback
170 this._stopTime = this.toSeconds(time) + fadeOutTime;
171 this._stopTime = Math.max(this._stopTime, this.now());
172 if (fadeOutTime > 0) {
173 // start the fade out curve at the given time
174 if (this._curve === "linear") {
175 this._gainNode.gain.linearRampTo(0, fadeOutTime, time);
176 } else {
177 this._gainNode.gain.targetRampTo(0, fadeOutTime, time);
178 }
179 } else {
180 // stop any ongoing ramps, and set the value to 0
181 this._gainNode.gain.cancelAndHoldAtTime(time);
182 this._gainNode.gain.setValueAtTime(0, time);
183 }
184 this.context.clearTimeout(this._timeout);
185 this._timeout = this.context.setTimeout(() => {
186 // allow additional time for the exponential curve to fully decay
187 const additionalTail =
188 this._curve === "exponential" ? fadeOutTime * 2 : 0;
189 this._stopSource(this.now() + additionalTail);
190 this._onended();
191 }, this._stopTime - this.context.currentTime);
192 return this;
193 }
194
195 /**
196 * Invoke the onended callback
197 */
198 protected _onended(): void {
199 if (this.onended !== noOp) {
200 this.onended(this);
201 // overwrite onended to make sure it only is called once
202 this.onended = noOp;
203 // dispose when it's ended to free up for garbage collection only in the online context
204 if (!this.context.isOffline) {
205 const disposeCallback = () => this.dispose();
206 // @ts-ignore
207 if (typeof window.requestIdleCallback !== "undefined") {
208 // @ts-ignore
209 window.requestIdleCallback(disposeCallback);
210 } else {
211 setTimeout(disposeCallback, 1000);
212 }
213 }
214 }
215 }
216
217 /**
218 * Get the playback state at the given time
219 */
220 getStateAtTime = function (time: Time): BasicPlaybackState {
221 const computedTime = this.toSeconds(time);
222 if (
223 this._startTime !== -1 &&
224 computedTime >= this._startTime &&
225 (this._stopTime === -1 || computedTime <= this._stopTime)
226 ) {
227 return "started";
228 } else {
229 return "stopped";
230 }
231 };
232
233 /**
234 * Get the playback state at the current time
235 */
236 get state(): BasicPlaybackState {
237 return this.getStateAtTime(this.now());
238 }
239
240 /**
241 * Cancel a scheduled stop event
242 */
243 cancelStop(): this {
244 this.log("cancelStop");
245 assert(this._startTime !== -1, "Source is not started");
246 // cancel the stop envelope
247 this._gainNode.gain.cancelScheduledValues(
248 this._startTime + this.sampleTime
249 );
250 this.context.clearTimeout(this._timeout);
251 this._stopTime = -1;
252 return this;
253 }
254
255 dispose(): this {
256 super.dispose();
257 this._gainNode.dispose();
258 this.onended = noOp;
259 return this;
260 }
261}