diff --git a/Tone/core/context/BaseContext.ts b/Tone/core/context/BaseContext.ts index 1122aa08..b2872217 100644 --- a/Tone/core/context/BaseContext.ts +++ b/Tone/core/context/BaseContext.ts @@ -8,63 +8,90 @@ type Transport = import("../clock/Transport").Transport; type Listener = import("./Listener").Listener; // these are either not used in Tone.js or deprecated and not implemented. -export type ExcludedFromBaseAudioContext = "onstatechange" | "addEventListener" | "removeEventListener" | "listener" | "dispatchEvent" | "audioWorklet" | "destination" | "createScriptProcessor"; +export type ExcludedFromBaseAudioContext = + | "onstatechange" + | "addEventListener" + | "removeEventListener" + | "listener" + | "dispatchEvent" + | "audioWorklet" + | "destination" + | "createScriptProcessor"; // the subset of the BaseAudioContext which Tone.Context implements. -export type BaseAudioContextSubset = Omit; +export type BaseAudioContextSubset = Omit< + BaseAudioContext, + ExcludedFromBaseAudioContext +>; export type ContextLatencyHint = AudioContextLatencyCategory; -export abstract class BaseContext extends Emitter<"statechange" | "tick"> implements BaseAudioContextSubset { - +export abstract class BaseContext + extends Emitter<"statechange" | "tick"> + implements BaseAudioContextSubset { //--------------------------- // BASE AUDIO CONTEXT METHODS //--------------------------- - abstract createAnalyser(): AnalyserNode + abstract createAnalyser(): AnalyserNode; - abstract createOscillator(): OscillatorNode + abstract createOscillator(): OscillatorNode; - abstract createBufferSource(): AudioBufferSourceNode + abstract createBufferSource(): AudioBufferSourceNode; - abstract createBiquadFilter(): BiquadFilterNode + abstract createBiquadFilter(): BiquadFilterNode; - abstract createBuffer(_numberOfChannels: number, _length: number, _sampleRate: number): AudioBuffer + abstract createBuffer( + _numberOfChannels: number, + _length: number, + _sampleRate: number + ): AudioBuffer; - abstract createChannelMerger(_numberOfInputs?: number | undefined): ChannelMergerNode + abstract createChannelMerger( + _numberOfInputs?: number | undefined + ): ChannelMergerNode; - abstract createChannelSplitter(_numberOfOutputs?: number | undefined): ChannelSplitterNode + abstract createChannelSplitter( + _numberOfOutputs?: number | undefined + ): ChannelSplitterNode; - abstract createConstantSource(): ConstantSourceNode + abstract createConstantSource(): ConstantSourceNode; - abstract createConvolver(): ConvolverNode + abstract createConvolver(): ConvolverNode; - abstract createDelay(_maxDelayTime?: number | undefined): DelayNode + abstract createDelay(_maxDelayTime?: number | undefined): DelayNode; - abstract createDynamicsCompressor(): DynamicsCompressorNode + abstract createDynamicsCompressor(): DynamicsCompressorNode; - abstract createGain(): GainNode + abstract createGain(): GainNode; - abstract createIIRFilter(_feedForward: number[] | Float32Array, _feedback: number[] | Float32Array): IIRFilterNode + abstract createIIRFilter( + _feedForward: number[] | Float32Array, + _feedback: number[] | Float32Array + ): IIRFilterNode; - abstract createPanner(): PannerNode + abstract createPanner(): PannerNode; abstract createPeriodicWave( _real: number[] | Float32Array, _imag: number[] | Float32Array, - _constraints?: PeriodicWaveConstraints | undefined, - ): PeriodicWave + _constraints?: PeriodicWaveConstraints | undefined + ): PeriodicWave; - abstract createStereoPanner(): StereoPannerNode + abstract createStereoPanner(): StereoPannerNode; - abstract createWaveShaper(): WaveShaperNode + abstract createWaveShaper(): WaveShaperNode; - abstract createMediaStreamSource(_stream: MediaStream): MediaStreamAudioSourceNode - - abstract createMediaElementSource(_element: HTMLMediaElement): MediaElementAudioSourceNode - - abstract createMediaStreamDestination(): MediaStreamAudioDestinationNode + abstract createMediaStreamSource( + _stream: MediaStream + ): MediaStreamAudioSourceNode; - abstract decodeAudioData(_audioData: ArrayBuffer): Promise + abstract createMediaElementSource( + _element: HTMLMediaElement + ): MediaElementAudioSourceNode; + + abstract createMediaStreamDestination(): MediaStreamAudioDestinationNode; + + abstract decodeAudioData(_audioData: ArrayBuffer): Promise; //--------------------------- // TONE AUDIO CONTEXT METHODS @@ -73,45 +100,56 @@ export abstract class BaseContext extends Emitter<"statechange" | "tick"> implem abstract createAudioWorkletNode( _name: string, _options?: Partial - ): AudioWorkletNode + ): AudioWorkletNode; - abstract get rawContext(): AnyAudioContext + abstract get rawContext(): AnyAudioContext; - abstract async addAudioWorkletModule(_url: string, _name: string): Promise + abstract async addAudioWorkletModule( + _url: string, + _name: string + ): Promise; abstract lookAhead: number; abstract latencyHint: ContextLatencyHint | Seconds; - abstract resume(): Promise + abstract resume(): Promise; - abstract setTimeout(_fn: (...args: any[]) => void, _timeout: Seconds): number + abstract setTimeout( + _fn: (...args: any[]) => void, + _timeout: Seconds + ): number; - abstract clearTimeout(_id: number): this + abstract clearTimeout(_id: number): this; - abstract setInterval(_fn: (...args: any[]) => void, _interval: Seconds): number + abstract setInterval( + _fn: (...args: any[]) => void, + _interval: Seconds + ): number; - abstract clearInterval(_id: number): this + abstract clearInterval(_id: number): this; - abstract getConstant(_val: number): AudioBufferSourceNode + abstract getConstant(_val: number): AudioBufferSourceNode; - abstract get currentTime(): Seconds + abstract get currentTime(): Seconds; - abstract get state(): AudioContextState + abstract get state(): AudioContextState; - abstract get sampleRate(): number + abstract get sampleRate(): number; - abstract get listener(): Listener + abstract get listener(): Listener; - abstract get transport(): Transport + abstract get transport(): Transport; - abstract get draw(): Draw + abstract get draw(): Draw; - abstract get destination(): Destination + abstract get destination(): Destination; - abstract now(): Seconds + abstract now(): Seconds; - abstract immediate(): Seconds + abstract immediate(): Seconds; + + abstract toJSON(): Record; readonly isOffline: boolean = false; } diff --git a/Tone/core/context/Context.test.ts b/Tone/core/context/Context.test.ts index 45ff0fbb..4481cba7 100644 --- a/Tone/core/context/Context.test.ts +++ b/Tone/core/context/Context.test.ts @@ -12,7 +12,6 @@ import { Draw } from "../util/Draw"; import { connect } from "./ToneAudioNode"; describe("Context", () => { - it("creates and disposes the classes attached to the context", async () => { const ac = createAudioContext(); const context = new Context(ac); @@ -32,7 +31,6 @@ describe("Context", () => { }); context("AudioContext", () => { - it("extends the AudioContext methods", () => { const ctx = new Context(createAudioContext()); expect(ctx).to.have.property("createGain"); @@ -46,8 +44,15 @@ describe("Context", () => { return ctx.close(); }); + it("can be stringified", () => { + const ctx = new Context(createAudioContext()); + expect(JSON.stringify(ctx)).to.equal("{}"); + ctx.dispose(); + return ctx.close(); + }); + if (ONLINE_TESTING) { - it("clock is running", done => { + it("clock is running", (done) => { const interval = setInterval(() => { if (getContext().currentTime > 0.5) { clearInterval(interval); @@ -88,7 +93,6 @@ describe("Context", () => { }); context("state", () => { - it("can suspend and resume the state", async () => { const ac = createAudioContext(); const context = new Context(ac); @@ -105,14 +109,14 @@ describe("Context", () => { const ac = createAudioContext(); const context = new Context(ac); let triggerChange = false; - context.on("statechange", state => { + context.on("statechange", (state) => { if (!triggerChange) { triggerChange = true; expect(state).to.equal("running"); } }); await context.resume(); - await new Promise(done => setTimeout(() => done(), 10)); + await new Promise((done) => setTimeout(() => done(), 10)); expect(triggerChange).to.equal(true); context.dispose(); return ac.close(); @@ -120,9 +124,7 @@ describe("Context", () => { }); if (ONLINE_TESTING) { - context("clockSource", () => { - let ctx; beforeEach(() => { ctx = new Context(); @@ -138,14 +140,14 @@ describe("Context", () => { expect(ctx.clockSource).to.equal("worker"); }); - it("provides callback", done => { + it("provides callback", (done) => { expect(ctx.clockSource).to.equal("worker"); ctx.setTimeout(() => { done(); }, 0.1); }); - it("can be set to 'timeout'", done => { + it("can be set to 'timeout'", (done) => { ctx.clockSource = "timeout"; expect(ctx.clockSource).to.equal("timeout"); ctx.setTimeout(() => { @@ -153,7 +155,7 @@ describe("Context", () => { }, 0.1); }); - it("can be set to 'offline'", done => { + it("can be set to 'offline'", (done) => { ctx.clockSource = "offline"; expect(ctx.clockSource).to.equal("offline"); // provides no callback @@ -167,9 +169,7 @@ describe("Context", () => { }); } context("setTimeout", () => { - if (ONLINE_TESTING) { - let ctx; beforeEach(() => { ctx = new Context(); @@ -181,19 +181,19 @@ describe("Context", () => { return ctx.close(); }); - it("can set a timeout", done => { + it("can set a timeout", (done) => { ctx.setTimeout(() => { done(); }, 0.1); }); it("returns an id", () => { - expect(ctx.setTimeout(() => { }, 0.1)).to.be.a("number"); + expect(ctx.setTimeout(() => {}, 0.1)).to.be.a("number"); // try clearing a random ID, shouldn't cause any errors ctx.clearTimeout(-2); }); - it("timeout is not invoked when cancelled", done => { + it("timeout is not invoked when cancelled", (done) => { const id = ctx.setTimeout(() => { throw new Error("shouldn't be invoked"); }, 0.01); @@ -203,7 +203,7 @@ describe("Context", () => { }, 0.02); }); - it("order is maintained", done => { + it("order is maintained", (done) => { let wasInvoked = false; ctx.setTimeout(() => { expect(wasInvoked).to.equal(true); @@ -216,7 +216,7 @@ describe("Context", () => { } it("is invoked in the offline context", () => { - return Offline(context => { + return Offline((context) => { const transport = new Transport({ context }); transport.context.setTimeout(() => { expect(transport.now()).to.be.closeTo(0.01, 0.005); @@ -226,9 +226,7 @@ describe("Context", () => { }); context("setInterval", () => { - if (ONLINE_TESTING) { - let ctx; beforeEach(() => { ctx = new Context(); @@ -240,19 +238,19 @@ describe("Context", () => { return ctx.close(); }); - it("can set an interval", done => { + it("can set an interval", (done) => { ctx.setInterval(() => { done(); }, 0.1); }); it("returns an id", () => { - expect(ctx.setInterval(() => { }, 0.1)).to.be.a("number"); + expect(ctx.setInterval(() => {}, 0.1)).to.be.a("number"); // try clearing a random ID, shouldn't cause any errors ctx.clearInterval(-2); }); - it("timeout is not invoked when cancelled", done => { + it("timeout is not invoked when cancelled", (done) => { const id = ctx.setInterval(() => { throw new Error("shouldn't be invoked"); }, 0.01); @@ -262,7 +260,7 @@ describe("Context", () => { }, 0.02); }); - it("order is maintained", done => { + it("order is maintained", (done) => { let wasInvoked = false; ctx.setInterval(() => { expect(wasInvoked).to.equal(true); @@ -276,7 +274,7 @@ describe("Context", () => { it("is invoked in the offline context", () => { let invocationCount = 0; - return Offline(context => { + return Offline((context) => { context.setInterval(() => { invocationCount++; }, 0.01); @@ -287,10 +285,13 @@ describe("Context", () => { it("is invoked in with the right interval", () => { let numberOfInvocations = 0; - return Offline(context => { + return Offline((context) => { let intervalTime = context.now(); context.setInterval(() => { - expect(context.now() - intervalTime).to.be.closeTo(0.01, 0.005); + expect(context.now() - intervalTime).to.be.closeTo( + 0.01, + 0.005 + ); intervalTime = context.now(); numberOfInvocations++; }, 0.01); @@ -301,7 +302,6 @@ describe("Context", () => { }); context("get/set", () => { - let ctx; beforeEach(() => { ctx = new Context(); @@ -324,7 +324,7 @@ describe("Context", () => { }); it("gets a constant signal", () => { - return ConstantOutput(context => { + return ConstantOutput((context) => { const bufferSrc = context.getConstant(1); connect(bufferSrc, context.destination); }, 1); @@ -335,7 +335,6 @@ describe("Context", () => { const bufferB = ctx.getConstant(2); expect(bufferA).to.equal(bufferB); }); - }); context("Methods", () => { diff --git a/Tone/core/context/Context.ts b/Tone/core/context/Context.ts index fd8443b4..fb3f3457 100644 --- a/Tone/core/context/Context.ts +++ b/Tone/core/context/Context.ts @@ -4,7 +4,11 @@ import { isAudioContext } from "../util/AdvancedTypeCheck"; import { optionsFromArguments } from "../util/Defaults"; import { Timeline } from "../util/Timeline"; import { isDefined, isString } from "../util/TypeCheck"; -import { AnyAudioContext, createAudioContext, createAudioWorkletNode } from "./AudioContext"; +import { + AnyAudioContext, + createAudioContext, + createAudioWorkletNode, +} from "./AudioContext"; import { closeContext, initializeContext } from "./ContextInitialization"; import { BaseContext, ContextLatencyHint } from "./BaseContext"; import { assert } from "../util/Debug"; @@ -33,7 +37,6 @@ export interface ContextTimeoutEvent { * @category Core */ export class Context extends BaseContext { - readonly name: string = "Context"; /** @@ -107,7 +110,9 @@ export class Context extends BaseContext { constructor(options?: Partial); constructor() { super(); - const options = optionsFromArguments(Context.getDefaults(), arguments, ["context"]); + const options = optionsFromArguments(Context.getDefaults(), arguments, [ + "context", + ]); if (options.context) { this._context = options.context; @@ -117,7 +122,11 @@ export class Context extends BaseContext { }); } - this._ticker = new Ticker(this.emit.bind(this, "tick"), options.clockSource, options.updateInterval); + this._ticker = new Ticker( + this.emit.bind(this, "tick"), + options.clockSource, + options.updateInterval + ); this.on("tick", this._timeoutLoop.bind(this)); // fwd events from the context @@ -166,13 +175,21 @@ export class Context extends BaseContext { createBiquadFilter(): BiquadFilterNode { return this._context.createBiquadFilter(); } - createBuffer(numberOfChannels: number, length: number, sampleRate: number): AudioBuffer { + createBuffer( + numberOfChannels: number, + length: number, + sampleRate: number + ): AudioBuffer { return this._context.createBuffer(numberOfChannels, length, sampleRate); } - createChannelMerger(numberOfInputs?: number | undefined): ChannelMergerNode { + createChannelMerger( + numberOfInputs?: number | undefined + ): ChannelMergerNode { return this._context.createChannelMerger(numberOfInputs); } - createChannelSplitter(numberOfOutputs?: number | undefined): ChannelSplitterNode { + createChannelSplitter( + numberOfOutputs?: number | undefined + ): ChannelSplitterNode { return this._context.createChannelSplitter(numberOfOutputs); } createConstantSource(): ConstantSourceNode { @@ -190,7 +207,10 @@ export class Context extends BaseContext { createGain(): GainNode { return this._context.createGain(); } - createIIRFilter(feedForward: number[] | Float32Array, feedback: number[] | Float32Array): IIRFilterNode { + createIIRFilter( + feedForward: number[] | Float32Array, + feedback: number[] | Float32Array + ): IIRFilterNode { // @ts-ignore return this._context.createIIRFilter(feedForward, feedback); } @@ -200,7 +220,7 @@ export class Context extends BaseContext { createPeriodicWave( real: number[] | Float32Array, imag: number[] | Float32Array, - constraints?: PeriodicWaveConstraints | undefined, + constraints?: PeriodicWaveConstraints | undefined ): PeriodicWave { return this._context.createPeriodicWave(real, imag, constraints); } @@ -211,17 +231,28 @@ export class Context extends BaseContext { return this._context.createWaveShaper(); } createMediaStreamSource(stream: MediaStream): MediaStreamAudioSourceNode { - assert(isAudioContext(this._context), "Not available if OfflineAudioContext"); + assert( + isAudioContext(this._context), + "Not available if OfflineAudioContext" + ); const context = this._context as AudioContext; return context.createMediaStreamSource(stream); } - createMediaElementSource(element: HTMLMediaElement): MediaElementAudioSourceNode { - assert(isAudioContext(this._context), "Not available if OfflineAudioContext"); + createMediaElementSource( + element: HTMLMediaElement + ): MediaElementAudioSourceNode { + assert( + isAudioContext(this._context), + "Not available if OfflineAudioContext" + ); const context = this._context as AudioContext; return context.createMediaElementSource(element); } createMediaStreamDestination(): MediaStreamAudioDestinationNode { - assert(isAudioContext(this._context), "Not available if OfflineAudioContext"); + assert( + isAudioContext(this._context), + "Not available if OfflineAudioContext" + ); const context = this._context as AudioContext; return context.createMediaStreamDestination(); } @@ -256,7 +287,10 @@ export class Context extends BaseContext { return this._listener; } set listener(l) { - assert(!this._initialized, "The listener cannot be set after initialization."); + assert( + !this._initialized, + "The listener cannot be set after initialization." + ); this._listener = l; } @@ -268,7 +302,10 @@ export class Context extends BaseContext { return this._transport; } set transport(t: Transport) { - assert(!this._initialized, "The transport cannot be set after initialization."); + assert( + !this._initialized, + "The transport cannot be set after initialization." + ); this._transport = t; } @@ -292,7 +329,10 @@ export class Context extends BaseContext { return this._destination; } set destination(d: Destination) { - assert(!this._initialized, "The destination cannot be set after initialization."); + assert( + !this._initialized, + "The destination cannot be set after initialization." + ); this._destination = d; } @@ -303,11 +343,11 @@ export class Context extends BaseContext { /** * Maps a module name to promise of the addModule method */ - private _workletModules: Map> = new Map() + private _workletModules: Map> = new Map(); /** * Create an audio worklet node from a name and options. The module - * must first be loaded using [[addAudioWorkletModule]]. + * must first be loaded using [[addAudioWorkletModule]]. */ createAudioWorkletNode( name: string, @@ -322,9 +362,15 @@ export class Context extends BaseContext { * @param name The name of the module */ async addAudioWorkletModule(url: string, name: string): Promise { - assert(isDefined(this.rawContext.audioWorklet), "AudioWorkletNode is only available in a secure context (https or localhost)"); + assert( + isDefined(this.rawContext.audioWorklet), + "AudioWorkletNode is only available in a secure context (https or localhost)" + ); if (!this._workletModules.has(name)) { - this._workletModules.set(name, this.rawContext.audioWorklet.addModule(url)); + this._workletModules.set( + name, + this.rawContext.audioWorklet.addModule(url) + ); } await this._workletModules.get(name); } @@ -334,7 +380,7 @@ export class Context extends BaseContext { */ protected async workletsAreReady(): Promise { const promises: Promise[] = []; - this._workletModules.forEach(promise => promises.push(promise)); + this._workletModules.forEach((promise) => promises.push(promise)); await Promise.all(promises); } @@ -423,7 +469,7 @@ export class Context extends BaseContext { } /** - * The current audio context time without the [[lookAhead]]. + * The current audio context time without the [[lookAhead]]. * In most cases it is better to use [[now]] instead of [[immediate]] since * with [[now]] the [[lookAhead]] is applied equally to _all_ components including internal components, * to making sure that everything is scheduled in sync. Mixing [[now]] and [[immediate]] @@ -447,7 +493,7 @@ export class Context extends BaseContext { /** * Close the context. Once closed, the context can no longer be used and - * any AudioNodes created from the context will be silent. + * any AudioNodes created from the context will be silent. */ async close(): Promise { if (isAudioContext(this._context)) { @@ -459,13 +505,17 @@ export class Context extends BaseContext { } /** - * **Internal** Generate a looped buffer at some constant value. + * **Internal** Generate a looped buffer at some constant value. */ getConstant(val: number): AudioBufferSourceNode { if (this._constants.has(val)) { return this._constants.get(val) as AudioBufferSourceNode; } else { - const buffer = this._context.createBuffer(1, 128, this._context.sampleRate); + const buffer = this._context.createBuffer( + 1, + 128, + this._context.sampleRate + ); const arr = buffer.getChannelData(0); for (let i = 0; i < arr.length; i++) { arr[i] = val; @@ -488,7 +538,9 @@ export class Context extends BaseContext { super.dispose(); this._ticker.dispose(); this._timeouts.dispose(); - Object.keys(this._constants).map(val => this._constants[val].disconnect()); + Object.keys(this._constants).map((val) => + this._constants[val].disconnect() + ); return this; } @@ -536,7 +588,7 @@ export class Context extends BaseContext { * @param id The ID returned from setTimeout */ clearTimeout(id: number): this { - this._timeouts.forEach(event => { + this._timeouts.forEach((event) => { if (event.id === id) { this._timeouts.remove(event); } @@ -573,4 +625,13 @@ export class Context extends BaseContext { intervalFn(); return id; } + + /* + * This is a placeholder so that JSON.stringify does not throw an error + * This matches what JSON.stringify(audioContext) returns on a native + * audioContext instance. + */ + toJSON() { + return {}; + } } diff --git a/Tone/core/context/DummyContext.ts b/Tone/core/context/DummyContext.ts index 5ce49234..ef5396b5 100644 --- a/Tone/core/context/DummyContext.ts +++ b/Tone/core/context/DummyContext.ts @@ -8,7 +8,6 @@ type Transport = import("../clock/Transport").Transport; type Listener = import("./Listener").Listener; export class DummyContext extends BaseContext { - //--------------------------- // BASE AUDIO CONTEXT METHODS //--------------------------- @@ -28,15 +27,23 @@ export class DummyContext extends BaseContext { return {} as BiquadFilterNode; } - createBuffer(_numberOfChannels: number, _length: number, _sampleRate: number): AudioBuffer { + createBuffer( + _numberOfChannels: number, + _length: number, + _sampleRate: number + ): AudioBuffer { return {} as AudioBuffer; } - createChannelMerger(_numberOfInputs?: number | undefined): ChannelMergerNode { + createChannelMerger( + _numberOfInputs?: number | undefined + ): ChannelMergerNode { return {} as ChannelMergerNode; } - createChannelSplitter(_numberOfOutputs?: number | undefined): ChannelSplitterNode { + createChannelSplitter( + _numberOfOutputs?: number | undefined + ): ChannelSplitterNode { return {} as ChannelSplitterNode; } @@ -60,7 +67,10 @@ export class DummyContext extends BaseContext { return {} as GainNode; } - createIIRFilter(_feedForward: number[] | Float32Array, _feedback: number[] | Float32Array): IIRFilterNode { + createIIRFilter( + _feedForward: number[] | Float32Array, + _feedback: number[] | Float32Array + ): IIRFilterNode { return {} as IIRFilterNode; } @@ -71,7 +81,7 @@ export class DummyContext extends BaseContext { createPeriodicWave( _real: number[] | Float32Array, _imag: number[] | Float32Array, - _constraints?: PeriodicWaveConstraints | undefined, + _constraints?: PeriodicWaveConstraints | undefined ): PeriodicWave { return {} as PeriodicWave; } @@ -88,10 +98,12 @@ export class DummyContext extends BaseContext { return {} as MediaStreamAudioSourceNode; } - createMediaElementSource(_element: HTMLMediaElement): MediaElementAudioSourceNode { + createMediaElementSource( + _element: HTMLMediaElement + ): MediaElementAudioSourceNode { return {} as MediaElementAudioSourceNode; } - + createMediaStreamDestination(): MediaStreamAudioDestinationNode { return {} as MediaStreamAudioDestinationNode; } @@ -170,12 +182,12 @@ export class DummyContext extends BaseContext { get draw(): Draw { return {} as Draw; } - set draw(_d) { } + set draw(_d) {} get destination(): Destination { return {} as Destination; } - set destination(_d: Destination) { } + set destination(_d: Destination) {} now() { return 0; @@ -185,5 +197,9 @@ export class DummyContext extends BaseContext { return 0; } + toJSON() { + return {}; + } + readonly isOffline: boolean = false; }