UNPKG

16.3 kBJavaScriptView Raw
1import { __awaiter } from "tslib";
2import { Ticker } from "../clock/Ticker.js";
3import { isAudioContext } from "../util/AdvancedTypeCheck.js";
4import { optionsFromArguments } from "../util/Defaults.js";
5import { Timeline } from "../util/Timeline.js";
6import { isDefined } from "../util/TypeCheck.js";
7import { createAudioContext, createAudioWorkletNode, } from "./AudioContext.js";
8import { closeContext, initializeContext } from "./ContextInitialization.js";
9import { BaseContext } from "./BaseContext.js";
10import { assert } from "../util/Debug.js";
11/**
12 * Wrapper around the native AudioContext.
13 * @category Core
14 */
15export class Context extends BaseContext {
16 constructor() {
17 var _a, _b;
18 super();
19 this.name = "Context";
20 /**
21 * An object containing all of the constants AudioBufferSourceNodes
22 */
23 this._constants = new Map();
24 /**
25 * All of the setTimeout events.
26 */
27 this._timeouts = new Timeline();
28 /**
29 * The timeout id counter
30 */
31 this._timeoutIds = 0;
32 /**
33 * Private indicator if the context has been initialized
34 */
35 this._initialized = false;
36 /**
37 * Private indicator if a close() has been called on the context, since close is async
38 */
39 this._closeStarted = false;
40 /**
41 * Indicates if the context is an OfflineAudioContext or an AudioContext
42 */
43 this.isOffline = false;
44 //--------------------------------------------
45 // AUDIO WORKLET
46 //--------------------------------------------
47 /**
48 * Maps a module name to promise of the addModule method
49 */
50 this._workletPromise = null;
51 const options = optionsFromArguments(Context.getDefaults(), arguments, [
52 "context",
53 ]);
54 if (options.context) {
55 this._context = options.context;
56 // custom context provided, latencyHint unknown (unless explicitly provided in options)
57 this._latencyHint = ((_a = arguments[0]) === null || _a === void 0 ? void 0 : _a.latencyHint) || "";
58 }
59 else {
60 this._context = createAudioContext({
61 latencyHint: options.latencyHint,
62 });
63 this._latencyHint = options.latencyHint;
64 }
65 this._ticker = new Ticker(this.emit.bind(this, "tick"), options.clockSource, options.updateInterval, this._context.sampleRate);
66 this.on("tick", this._timeoutLoop.bind(this));
67 // fwd events from the context
68 this._context.onstatechange = () => {
69 this.emit("statechange", this.state);
70 };
71 // if no custom updateInterval provided, updateInterval will be derived by lookAhead setter
72 this[((_b = arguments[0]) === null || _b === void 0 ? void 0 : _b.hasOwnProperty("updateInterval"))
73 ? "_lookAhead"
74 : "lookAhead"] = options.lookAhead;
75 }
76 static getDefaults() {
77 return {
78 clockSource: "worker",
79 latencyHint: "interactive",
80 lookAhead: 0.1,
81 updateInterval: 0.05,
82 };
83 }
84 /**
85 * Finish setting up the context. **You usually do not need to do this manually.**
86 */
87 initialize() {
88 if (!this._initialized) {
89 // add any additional modules
90 initializeContext(this);
91 this._initialized = true;
92 }
93 return this;
94 }
95 //---------------------------
96 // BASE AUDIO CONTEXT METHODS
97 //---------------------------
98 createAnalyser() {
99 return this._context.createAnalyser();
100 }
101 createOscillator() {
102 return this._context.createOscillator();
103 }
104 createBufferSource() {
105 return this._context.createBufferSource();
106 }
107 createBiquadFilter() {
108 return this._context.createBiquadFilter();
109 }
110 createBuffer(numberOfChannels, length, sampleRate) {
111 return this._context.createBuffer(numberOfChannels, length, sampleRate);
112 }
113 createChannelMerger(numberOfInputs) {
114 return this._context.createChannelMerger(numberOfInputs);
115 }
116 createChannelSplitter(numberOfOutputs) {
117 return this._context.createChannelSplitter(numberOfOutputs);
118 }
119 createConstantSource() {
120 return this._context.createConstantSource();
121 }
122 createConvolver() {
123 return this._context.createConvolver();
124 }
125 createDelay(maxDelayTime) {
126 return this._context.createDelay(maxDelayTime);
127 }
128 createDynamicsCompressor() {
129 return this._context.createDynamicsCompressor();
130 }
131 createGain() {
132 return this._context.createGain();
133 }
134 createIIRFilter(feedForward, feedback) {
135 // @ts-ignore
136 return this._context.createIIRFilter(feedForward, feedback);
137 }
138 createPanner() {
139 return this._context.createPanner();
140 }
141 createPeriodicWave(real, imag, constraints) {
142 return this._context.createPeriodicWave(real, imag, constraints);
143 }
144 createStereoPanner() {
145 return this._context.createStereoPanner();
146 }
147 createWaveShaper() {
148 return this._context.createWaveShaper();
149 }
150 createMediaStreamSource(stream) {
151 assert(isAudioContext(this._context), "Not available if OfflineAudioContext");
152 const context = this._context;
153 return context.createMediaStreamSource(stream);
154 }
155 createMediaElementSource(element) {
156 assert(isAudioContext(this._context), "Not available if OfflineAudioContext");
157 const context = this._context;
158 return context.createMediaElementSource(element);
159 }
160 createMediaStreamDestination() {
161 assert(isAudioContext(this._context), "Not available if OfflineAudioContext");
162 const context = this._context;
163 return context.createMediaStreamDestination();
164 }
165 decodeAudioData(audioData) {
166 return this._context.decodeAudioData(audioData);
167 }
168 /**
169 * The current time in seconds of the AudioContext.
170 */
171 get currentTime() {
172 return this._context.currentTime;
173 }
174 /**
175 * The current time in seconds of the AudioContext.
176 */
177 get state() {
178 return this._context.state;
179 }
180 /**
181 * The current time in seconds of the AudioContext.
182 */
183 get sampleRate() {
184 return this._context.sampleRate;
185 }
186 /**
187 * The listener
188 */
189 get listener() {
190 this.initialize();
191 return this._listener;
192 }
193 set listener(l) {
194 assert(!this._initialized, "The listener cannot be set after initialization.");
195 this._listener = l;
196 }
197 /**
198 * There is only one Transport per Context. It is created on initialization.
199 */
200 get transport() {
201 this.initialize();
202 return this._transport;
203 }
204 set transport(t) {
205 assert(!this._initialized, "The transport cannot be set after initialization.");
206 this._transport = t;
207 }
208 /**
209 * This is the Draw object for the context which is useful for synchronizing the draw frame with the Tone.js clock.
210 */
211 get draw() {
212 this.initialize();
213 return this._draw;
214 }
215 set draw(d) {
216 assert(!this._initialized, "Draw cannot be set after initialization.");
217 this._draw = d;
218 }
219 /**
220 * A reference to the Context's destination node.
221 */
222 get destination() {
223 this.initialize();
224 return this._destination;
225 }
226 set destination(d) {
227 assert(!this._initialized, "The destination cannot be set after initialization.");
228 this._destination = d;
229 }
230 /**
231 * Create an audio worklet node from a name and options. The module
232 * must first be loaded using {@link addAudioWorkletModule}.
233 */
234 createAudioWorkletNode(name, options) {
235 return createAudioWorkletNode(this.rawContext, name, options);
236 }
237 /**
238 * Add an AudioWorkletProcessor module
239 * @param url The url of the module
240 */
241 addAudioWorkletModule(url) {
242 return __awaiter(this, void 0, void 0, function* () {
243 assert(isDefined(this.rawContext.audioWorklet), "AudioWorkletNode is only available in a secure context (https or localhost)");
244 if (!this._workletPromise) {
245 this._workletPromise = this.rawContext.audioWorklet.addModule(url);
246 }
247 yield this._workletPromise;
248 });
249 }
250 /**
251 * Returns a promise which resolves when all of the worklets have been loaded on this context
252 */
253 workletsAreReady() {
254 return __awaiter(this, void 0, void 0, function* () {
255 (yield this._workletPromise) ? this._workletPromise : Promise.resolve();
256 });
257 }
258 //---------------------------
259 // TICKER
260 //---------------------------
261 /**
262 * How often the interval callback is invoked.
263 * This number corresponds to how responsive the scheduling
264 * can be. Setting to 0 will result in the lowest practial interval
265 * based on context properties. context.updateInterval + context.lookAhead
266 * gives you the total latency between scheduling an event and hearing it.
267 */
268 get updateInterval() {
269 return this._ticker.updateInterval;
270 }
271 set updateInterval(interval) {
272 this._ticker.updateInterval = interval;
273 }
274 /**
275 * What the source of the clock is, either "worker" (default),
276 * "timeout", or "offline" (none).
277 */
278 get clockSource() {
279 return this._ticker.type;
280 }
281 set clockSource(type) {
282 this._ticker.type = type;
283 }
284 /**
285 * The amount of time into the future events are scheduled. Giving Web Audio
286 * a short amount of time into the future to schedule events can reduce clicks and
287 * improve performance. This value can be set to 0 to get the lowest latency.
288 * Adjusting this value also affects the {@link updateInterval}.
289 */
290 get lookAhead() {
291 return this._lookAhead;
292 }
293 set lookAhead(time) {
294 this._lookAhead = time;
295 // if lookAhead is 0, default to .01 updateInterval
296 this.updateInterval = time ? time / 2 : 0.01;
297 }
298 /**
299 * The type of playback, which affects tradeoffs between audio
300 * output latency and responsiveness.
301 * In addition to setting the value in seconds, the latencyHint also
302 * accepts the strings "interactive" (prioritizes low latency),
303 * "playback" (prioritizes sustained playback), "balanced" (balances
304 * latency and performance).
305 * @example
306 * // prioritize sustained playback
307 * const context = new Tone.Context({ latencyHint: "playback" });
308 * // set this context as the global Context
309 * Tone.setContext(context);
310 * // the global context is gettable with Tone.getContext()
311 * console.log(Tone.getContext().latencyHint);
312 */
313 get latencyHint() {
314 return this._latencyHint;
315 }
316 /**
317 * The unwrapped AudioContext or OfflineAudioContext
318 */
319 get rawContext() {
320 return this._context;
321 }
322 /**
323 * The current audio context time plus a short {@link lookAhead}.
324 * @example
325 * setInterval(() => {
326 * console.log("now", Tone.now());
327 * }, 100);
328 */
329 now() {
330 return this._context.currentTime + this._lookAhead;
331 }
332 /**
333 * The current audio context time without the {@link lookAhead}.
334 * In most cases it is better to use {@link now} instead of {@link immediate} since
335 * with {@link now} the {@link lookAhead} is applied equally to _all_ components including internal components,
336 * to making sure that everything is scheduled in sync. Mixing {@link now} and {@link immediate}
337 * can cause some timing issues. If no lookAhead is desired, you can set the {@link lookAhead} to `0`.
338 */
339 immediate() {
340 return this._context.currentTime;
341 }
342 /**
343 * Starts the audio context from a suspended state. This is required
344 * to initially start the AudioContext.
345 * @see {@link start}
346 */
347 resume() {
348 if (isAudioContext(this._context)) {
349 return this._context.resume();
350 }
351 else {
352 return Promise.resolve();
353 }
354 }
355 /**
356 * Close the context. Once closed, the context can no longer be used and
357 * any AudioNodes created from the context will be silent.
358 */
359 close() {
360 return __awaiter(this, void 0, void 0, function* () {
361 if (isAudioContext(this._context) &&
362 this.state !== "closed" &&
363 !this._closeStarted) {
364 this._closeStarted = true;
365 yield this._context.close();
366 }
367 if (this._initialized) {
368 closeContext(this);
369 }
370 });
371 }
372 /**
373 * **Internal** Generate a looped buffer at some constant value.
374 */
375 getConstant(val) {
376 if (this._constants.has(val)) {
377 return this._constants.get(val);
378 }
379 else {
380 const buffer = this._context.createBuffer(1, 128, this._context.sampleRate);
381 const arr = buffer.getChannelData(0);
382 for (let i = 0; i < arr.length; i++) {
383 arr[i] = val;
384 }
385 const constant = this._context.createBufferSource();
386 constant.channelCount = 1;
387 constant.channelCountMode = "explicit";
388 constant.buffer = buffer;
389 constant.loop = true;
390 constant.start(0);
391 this._constants.set(val, constant);
392 return constant;
393 }
394 }
395 /**
396 * Clean up. Also closes the audio context.
397 */
398 dispose() {
399 super.dispose();
400 this._ticker.dispose();
401 this._timeouts.dispose();
402 Object.keys(this._constants).map((val) => this._constants[val].disconnect());
403 this.close();
404 return this;
405 }
406 //---------------------------
407 // TIMEOUTS
408 //---------------------------
409 /**
410 * The private loop which keeps track of the context scheduled timeouts
411 * Is invoked from the clock source
412 */
413 _timeoutLoop() {
414 const now = this.now();
415 let firstEvent = this._timeouts.peek();
416 while (this._timeouts.length && firstEvent && firstEvent.time <= now) {
417 // invoke the callback
418 firstEvent.callback();
419 // shift the first event off
420 this._timeouts.shift();
421 // get the next one
422 firstEvent = this._timeouts.peek();
423 }
424 }
425 /**
426 * A setTimeout which is guaranteed by the clock source.
427 * Also runs in the offline context.
428 * @param fn The callback to invoke
429 * @param timeout The timeout in seconds
430 * @returns ID to use when invoking Context.clearTimeout
431 */
432 setTimeout(fn, timeout) {
433 this._timeoutIds++;
434 const now = this.now();
435 this._timeouts.add({
436 callback: fn,
437 id: this._timeoutIds,
438 time: now + timeout,
439 });
440 return this._timeoutIds;
441 }
442 /**
443 * Clears a previously scheduled timeout with Tone.context.setTimeout
444 * @param id The ID returned from setTimeout
445 */
446 clearTimeout(id) {
447 this._timeouts.forEach((event) => {
448 if (event.id === id) {
449 this._timeouts.remove(event);
450 }
451 });
452 return this;
453 }
454 /**
455 * Clear the function scheduled by {@link setInterval}
456 */
457 clearInterval(id) {
458 return this.clearTimeout(id);
459 }
460 /**
461 * Adds a repeating event to the context's callback clock
462 */
463 setInterval(fn, interval) {
464 const id = ++this._timeoutIds;
465 const intervalFn = () => {
466 const now = this.now();
467 this._timeouts.add({
468 callback: () => {
469 // invoke the callback
470 fn();
471 // invoke the event to repeat it
472 intervalFn();
473 },
474 id,
475 time: now + interval,
476 });
477 };
478 // kick it off
479 intervalFn();
480 return id;
481 }
482}
483//# sourceMappingURL=Context.js.map
\No newline at end of file