allow JSON.stringify(context)

This commit is contained in:
Jack Anderson 2020-12-31 17:53:38 -08:00
parent bfa600399f
commit 16859ff2e2
4 changed files with 227 additions and 113 deletions

View file

@ -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<BaseAudioContext, ExcludedFromBaseAudioContext>;
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<AudioBuffer>
abstract createMediaElementSource(
_element: HTMLMediaElement
): MediaElementAudioSourceNode;
abstract createMediaStreamDestination(): MediaStreamAudioDestinationNode;
abstract decodeAudioData(_audioData: ArrayBuffer): Promise<AudioBuffer>;
//---------------------------
// TONE AUDIO CONTEXT METHODS
@ -73,45 +100,56 @@ export abstract class BaseContext extends Emitter<"statechange" | "tick"> implem
abstract createAudioWorkletNode(
_name: string,
_options?: Partial<AudioWorkletNodeOptions>
): AudioWorkletNode
): AudioWorkletNode;
abstract get rawContext(): AnyAudioContext
abstract get rawContext(): AnyAudioContext;
abstract async addAudioWorkletModule(_url: string, _name: string): Promise<void>
abstract async addAudioWorkletModule(
_url: string,
_name: string
): Promise<void>;
abstract lookAhead: number;
abstract latencyHint: ContextLatencyHint | Seconds;
abstract resume(): Promise<void>
abstract resume(): Promise<void>;
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<string, any>;
readonly isOffline: boolean = false;
}

View file

@ -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", () => {

View file

@ -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<ContextOptions>);
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<string, Promise<void>> = new Map()
private _workletModules: Map<string, Promise<void>> = 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<void> {
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<void> {
const promises: Promise<void>[] = [];
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<void> {
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 {};
}
}

View file

@ -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;
}