prettier all of the files

This commit is contained in:
Yotam Mann 2024-05-03 11:09:28 -04:00
parent 2f3807c855
commit 3e73d88157
336 changed files with 7691 additions and 4067 deletions

View file

@ -4,7 +4,6 @@ import { Noise } from "../../source/Noise.js";
import { Analyser } from "./Analyser.js";
describe("Analyser", () => {
BasicTests(Analyser);
it("can get and set properties", () => {
@ -31,7 +30,7 @@ describe("Analyser", () => {
const anl = new Analyser("fft", 512);
const analysis = anl.getValue();
expect(analysis.length).to.equal(512);
analysis.forEach(val => {
analysis.forEach((val) => {
expect(val).is.lessThan(0);
});
anl.dispose();
@ -46,7 +45,7 @@ describe("Analyser", () => {
setTimeout(() => {
const analysis = anl.getValue();
expect(analysis.length).to.equal(256);
analysis.forEach(val => {
analysis.forEach((val) => {
expect(val).is.within(-1, 1);
});
anl.dispose();
@ -74,5 +73,4 @@ describe("Analyser", () => {
expect((anl.getValue()[0] as Float32Array).length).to.equal(512);
anl.dispose();
});
});

View file

@ -1,4 +1,9 @@
import { InputNode, OutputNode, ToneAudioNode, ToneAudioNodeOptions } from "../../core/context/ToneAudioNode.js";
import {
InputNode,
OutputNode,
ToneAudioNode,
ToneAudioNodeOptions,
} from "../../core/context/ToneAudioNode.js";
import { NormalRange, PowerOfTwo } from "../../core/type/Units.js";
import { optionsFromArguments } from "../../core/util/Defaults.js";
import { Split } from "../channel/Split.js";
@ -20,7 +25,6 @@ export interface AnalyserOptions extends ToneAudioNodeOptions {
* @category Component
*/
export class Analyser extends ToneAudioNode<AnalyserOptions> {
readonly name: string = "Analyser";
readonly input: InputNode;
@ -58,18 +62,30 @@ export class Analyser extends ToneAudioNode<AnalyserOptions> {
constructor(type?: AnalyserType, size?: number);
constructor(options?: Partial<AnalyserOptions>);
constructor() {
super(optionsFromArguments(Analyser.getDefaults(), arguments, ["type", "size"]));
const options = optionsFromArguments(Analyser.getDefaults(), arguments, ["type", "size"]);
super(
optionsFromArguments(Analyser.getDefaults(), arguments, [
"type",
"size",
])
);
const options = optionsFromArguments(
Analyser.getDefaults(),
arguments,
["type", "size"]
);
this.input = this.output = this._gain = new Gain({ context: this.context });
this.input =
this.output =
this._gain =
new Gain({ context: this.context });
this._split = new Split({
context: this.context,
channels: options.channels,
});
this.input.connect(this._split);
assertRange(options.channels, 1);
// create the analysers
for (let channel = 0; channel < options.channels; channel++) {
this._analysers[channel] = this.context.createAnalyser();
@ -141,7 +157,10 @@ export class Analyser extends ToneAudioNode<AnalyserOptions> {
return this._type;
}
set type(type: AnalyserType) {
assert(type === "waveform" || type === "fft", `Analyser: invalid type: ${type}`);
assert(
type === "waveform" || type === "fft",
`Analyser: invalid type: ${type}`
);
this._type = type;
}
@ -152,7 +171,7 @@ export class Analyser extends ToneAudioNode<AnalyserOptions> {
return this._analysers[0].smoothingTimeConstant;
}
set smoothing(val: NormalRange) {
this._analysers.forEach(a => a.smoothingTimeConstant = val);
this._analysers.forEach((a) => (a.smoothingTimeConstant = val));
}
/**
@ -160,7 +179,7 @@ export class Analyser extends ToneAudioNode<AnalyserOptions> {
*/
dispose(): this {
super.dispose();
this._analysers.forEach(a => a.disconnect());
this._analysers.forEach((a) => a.disconnect());
this._split.dispose();
this._gain.dispose();
return this;

View file

@ -6,11 +6,9 @@ import { Signal } from "../../signal/Signal.js";
import { DCMeter } from "./DCMeter.js";
describe("DCMeter", () => {
BasicTests(DCMeter);
context("DCMetering", () => {
it("passes the audio through", () => {
return PassAudio((input) => {
const meter = new DCMeter().toDestination();

View file

@ -4,7 +4,7 @@ import { MeterBase, MeterBaseOptions } from "./MeterBase.js";
export type DCMeterOptions = MeterBaseOptions;
/**
* DCMeter gets the raw value of the input signal at the current time.
* DCMeter gets the raw value of the input signal at the current time.
* @see {@link Meter}.
*
* @example
@ -18,7 +18,6 @@ export type DCMeterOptions = MeterBaseOptions;
* @category Component
*/
export class DCMeter extends MeterBase<DCMeterOptions> {
readonly name: string = "DCMeter";
constructor(options?: Partial<DCMeterOptions>);

View file

@ -5,7 +5,6 @@ import { Noise } from "../../source/Noise.js";
import { FFT } from "./FFT.js";
describe("FFT", () => {
BasicTests(FFT);
it("can get and set properties", () => {
@ -38,7 +37,10 @@ describe("FFT", () => {
it("can get the frequency values of each index of the return array", () => {
const fft = new FFT(32);
expect(fft.getFrequencyOfIndex(0)).to.be.closeTo(0, 1);
expect(fft.getFrequencyOfIndex(16)).to.be.closeTo(fft.context.sampleRate / 4, 1);
expect(fft.getFrequencyOfIndex(16)).to.be.closeTo(
fft.context.sampleRate / 4,
1
);
fft.dispose();
});
@ -51,7 +53,7 @@ describe("FFT", () => {
setTimeout(() => {
const analysis = fft.getValue();
expect(analysis.length).to.equal(256);
analysis.forEach(value => {
analysis.forEach((value) => {
expect(value).is.within(-Infinity, 0);
});
fft.dispose();
@ -71,7 +73,7 @@ describe("FFT", () => {
setTimeout(() => {
const analysis = fft.getValue();
analysis.forEach(value => {
analysis.forEach((value) => {
expect(value).is.within(0, 1);
});
fft.dispose();

View file

@ -17,7 +17,6 @@ export interface FFTOptions extends MeterBaseOptions {
* @category Component
*/
export class FFT extends MeterBase<FFTOptions> {
readonly name: string = "FFT";
/**
@ -34,7 +33,9 @@ export class FFT extends MeterBase<FFTOptions> {
constructor(options?: Partial<FFTOptions>);
constructor() {
super(optionsFromArguments(FFT.getDefaults(), arguments, ["size"]));
const options = optionsFromArguments(FFT.getDefaults(), arguments, ["size"]);
const options = optionsFromArguments(FFT.getDefaults(), arguments, [
"size",
]);
this.normalRange = options.normalRange;
this._analyser.type = "fft";
@ -55,7 +56,7 @@ export class FFT extends MeterBase<FFTOptions> {
*/
getValue(): Float32Array {
const values = this._analyser.getValue() as Float32Array;
return values.map(v => this.normalRange ? dbToGain(v) : v);
return values.map((v) => (this.normalRange ? dbToGain(v) : v));
}
/**
@ -87,7 +88,10 @@ export class FFT extends MeterBase<FFTOptions> {
* console.log([0, 1, 2, 3, 4].map(index => fft.getFrequencyOfIndex(index)));
*/
getFrequencyOfIndex(index: number): Hertz {
assert(0 <= index && index < this.size, `index must be greater than or equal to 0 and less than ${this.size}`);
return index * this.context.sampleRate / (this.size * 2);
assert(
0 <= index && index < this.size,
`index must be greater than or equal to 0 and less than ${this.size}`
);
return (index * this.context.sampleRate) / (this.size * 2);
}
}

View file

@ -6,11 +6,9 @@ import { PassAudio } from "../../../test/helper/PassAudio.js";
import { expect } from "chai";
describe("Follower", () => {
BasicTests(Follower);
context("Envelope Following", () => {
it("handles getter/setter as Object", () => {
const foll = new Follower();
const values = {
@ -108,4 +106,3 @@ describe("Follower", () => {
});
});
});

View file

@ -1,5 +1,10 @@
import { Time } from "../../core/type/Units.js";
import { InputNode, OutputNode, ToneAudioNode, ToneAudioNodeOptions } from "../../core/context/ToneAudioNode.js";
import {
InputNode,
OutputNode,
ToneAudioNode,
ToneAudioNodeOptions,
} from "../../core/context/ToneAudioNode.js";
import { optionsFromArguments } from "../../core/util/Defaults.js";
import { OnePoleFilter } from "../filter/OnePoleFilter.js";
import { Abs } from "../../signal/Abs.js";
@ -9,8 +14,8 @@ export interface FollowerOptions extends ToneAudioNodeOptions {
}
/**
* Follower is a simple envelope follower.
* It's implemented by applying a lowpass filter to the absolute value of the incoming signal.
* Follower is a simple envelope follower.
* It's implemented by applying a lowpass filter to the absolute value of the incoming signal.
* ```
* +-----+ +---------------+
* Input +--> Abs +----> OnePoleFilter +--> Output
@ -19,7 +24,6 @@ export interface FollowerOptions extends ToneAudioNodeOptions {
* @category Component
*/
export class Follower extends ToneAudioNode<FollowerOptions> {
readonly name: string = "Follower";
readonly input: InputNode;
@ -46,14 +50,22 @@ export class Follower extends ToneAudioNode<FollowerOptions> {
constructor(smoothing?: Time);
constructor(options?: Partial<FollowerOptions>);
constructor() {
super(optionsFromArguments(Follower.getDefaults(), arguments, ["smoothing"]));
const options = optionsFromArguments(Follower.getDefaults(), arguments, ["smoothing"]);
super(
optionsFromArguments(Follower.getDefaults(), arguments, [
"smoothing",
])
);
const options = optionsFromArguments(
Follower.getDefaults(),
arguments,
["smoothing"]
);
this._abs = this.input = new Abs({ context: this.context });
this._lowpass = this.output = new OnePoleFilter({
context: this.context,
frequency: 1 / this.toSeconds(options.smoothing),
type: "lowpass"
type: "lowpass",
});
this._abs.connect(this._lowpass);
this._smoothing = options.smoothing;
@ -61,12 +73,12 @@ export class Follower extends ToneAudioNode<FollowerOptions> {
static getDefaults(): FollowerOptions {
return Object.assign(ToneAudioNode.getDefaults(), {
smoothing: 0.05
smoothing: 0.05,
});
}
/**
* The amount of time it takes a value change to arrive at the updated value.
* The amount of time it takes a value change to arrive at the updated value.
*/
get smoothing(): Time {
return this._smoothing;

View file

@ -9,11 +9,9 @@ import { Panner } from "../channel/Panner.js";
import { Merge } from "../channel/Merge.js";
describe("Meter", () => {
BasicTests(Meter);
context("Metering", () => {
it("handles getter/setter as Object", () => {
const meter = new Meter();
const values = {
@ -60,9 +58,8 @@ describe("Meter", () => {
meter.dispose();
});
});
if (ONLINE_TESTING) {
it("can get the rms level of the incoming signal", (done) => {
const meter = new Meter();
const osc = new Oscillator().connect(meter).start();

View file

@ -15,8 +15,8 @@ export interface MeterOptions extends MeterBaseOptions {
* Meter gets the [RMS](https://en.wikipedia.org/wiki/Root_mean_square)
* of an input signal. It can also get the raw value of the input signal.
* Setting `normalRange` to `true` will covert the output to a range of
* 0-1. See an example using a graphical display
* [here](https://tonejs.github.io/examples/meter).
* 0-1. See an example using a graphical display
* [here](https://tonejs.github.io/examples/meter).
* @see {@link DCMeter}.
*
* @example
@ -30,7 +30,6 @@ export interface MeterOptions extends MeterBaseOptions {
* @category Component
*/
export class Meter extends MeterBase<MeterOptions> {
readonly name: string = "Meter";
/**
@ -56,18 +55,25 @@ export class Meter extends MeterBase<MeterOptions> {
constructor(smoothing?: NormalRange);
constructor(options?: Partial<MeterOptions>);
constructor() {
super(optionsFromArguments(Meter.getDefaults(), arguments, ["smoothing"]));
const options = optionsFromArguments(Meter.getDefaults(), arguments, ["smoothing"]);
super(
optionsFromArguments(Meter.getDefaults(), arguments, ["smoothing"])
);
const options = optionsFromArguments(Meter.getDefaults(), arguments, [
"smoothing",
]);
this.input = this.output = this._analyser = new Analyser({
context: this.context,
size: 256,
type: "waveform",
channels: options.channelCount,
});
this.input =
this.output =
this._analyser =
new Analyser({
context: this.context,
size: 256,
type: "waveform",
channels: options.channelCount,
});
this.smoothing = options.smoothing,
this.normalRange = options.normalRange;
(this.smoothing = options.smoothing),
(this.normalRange = options.normalRange);
this._rms = new Array(options.channelCount);
this._rms.fill(0);
}
@ -90,22 +96,30 @@ export class Meter extends MeterBase<MeterOptions> {
}
/**
* Get the current value of the incoming signal.
* Get the current value of the incoming signal.
* Output is in decibels when {@link normalRange} is `false`.
* If {@link channels} = 1, then the output is a single number
* representing the value of the input signal. When {@link channels} > 1,
* then each channel is returned as a value in a number array.
* then each channel is returned as a value in a number array.
*/
getValue(): number | number[] {
const aValues = this._analyser.getValue();
const channelValues = this.channels === 1 ? [aValues as Float32Array] : aValues as Float32Array[];
const channelValues =
this.channels === 1
? [aValues as Float32Array]
: (aValues as Float32Array[]);
const vals = channelValues.map((values, index) => {
const totalSquared = values.reduce((total, current) => total + current * current, 0);
const totalSquared = values.reduce(
(total, current) => total + current * current,
0
);
const rms = Math.sqrt(totalSquared / values.length);
// the rms can only fall at the rate of the smoothing
// but can jump up instantly
this._rms[index] = Math.max(rms, this._rms[index] * this.smoothing);
return this.normalRange ? this._rms[index] : gainToDb(this._rms[index]);
return this.normalRange
? this._rms[index]
: gainToDb(this._rms[index]);
});
if (this.channels === 1) {
return vals[0];

View file

@ -1,4 +1,9 @@
import { InputNode, OutputNode, ToneAudioNode, ToneAudioNodeOptions } from "../../core/context/ToneAudioNode.js";
import {
InputNode,
OutputNode,
ToneAudioNode,
ToneAudioNodeOptions,
} from "../../core/context/ToneAudioNode.js";
import { optionsFromArguments } from "../../core/util/Defaults.js";
import { Analyser } from "./Analyser.js";
@ -7,8 +12,9 @@ export type MeterBaseOptions = ToneAudioNodeOptions;
/**
* The base class for Metering classes.
*/
export class MeterBase<Options extends MeterBaseOptions> extends ToneAudioNode<Options> {
export class MeterBase<
Options extends MeterBaseOptions,
> extends ToneAudioNode<Options> {
readonly name: string = "MeterBase";
/**
@ -30,11 +36,14 @@ export class MeterBase<Options extends MeterBaseOptions> extends ToneAudioNode<O
constructor() {
super(optionsFromArguments(MeterBase.getDefaults(), arguments));
this.input = this.output = this._analyser = new Analyser({
context: this.context,
size: 256,
type: "waveform",
});
this.input =
this.output =
this._analyser =
new Analyser({
context: this.context,
size: 256,
type: "waveform",
});
}
dispose(): this {

View file

@ -5,7 +5,6 @@ import { Noise } from "../../source/Noise.js";
import { Waveform } from "./Waveform.js";
describe("Waveform", () => {
BasicTests(Waveform);
it("can get and set properties", () => {
@ -27,7 +26,6 @@ describe("Waveform", () => {
});
if (ONLINE_TESTING) {
it("can run waveform analysis", (done) => {
const noise = new Noise();
const anl = new Waveform(256);
@ -37,7 +35,7 @@ describe("Waveform", () => {
setTimeout(() => {
const analysis = anl.getValue();
expect(analysis.length).to.equal(256);
analysis.forEach(value => {
analysis.forEach((value) => {
expect(value).is.within(-1, 1);
});
anl.dispose();

View file

@ -14,7 +14,6 @@ export interface WaveformOptions extends MeterBaseOptions {
* @category Component
*/
export class Waveform extends MeterBase<WaveformOptions> {
readonly name: string = "Waveform";
/**
@ -23,8 +22,14 @@ export class Waveform extends MeterBase<WaveformOptions> {
constructor(size?: PowerOfTwo);
constructor(options?: Partial<WaveformOptions>);
constructor() {
super(optionsFromArguments(Waveform.getDefaults(), arguments, ["size"]));
const options = optionsFromArguments(Waveform.getDefaults(), arguments, ["size"]);
super(
optionsFromArguments(Waveform.getDefaults(), arguments, ["size"])
);
const options = optionsFromArguments(
Waveform.getDefaults(),
arguments,
["size"]
);
this._analyser.type = "waveform";
this.size = options.size;

View file

@ -6,11 +6,9 @@ import { Offline } from "../../../test/helper/Offline.js";
import { expect } from "chai";
describe("Channel", () => {
BasicTests(Channel);
context("Channel", () => {
it("can pass volume and panning into the constructor", () => {
const channel = new Channel(-10, -1);
expect(channel.pan.value).to.be.closeTo(-1, 0.01);
@ -23,7 +21,7 @@ describe("Channel", () => {
pan: 1,
volume: 6,
mute: false,
solo: true
solo: true,
});
expect(channel.pan.value).to.be.closeTo(1, 0.01);
expect(channel.volume.value).to.be.closeTo(6, 0.01);
@ -31,7 +29,7 @@ describe("Channel", () => {
expect(channel.solo).to.be.true;
channel.dispose();
});
it("passes the incoming signal through", () => {
return PassAudio((input) => {
const channel = new Channel().toDestination();
@ -64,7 +62,7 @@ describe("Channel", () => {
describe("bus", () => {
it("can connect two channels together by name", () => {
return PassAudio(input => {
return PassAudio((input) => {
const sendChannel = new Channel();
input.connect(sendChannel);
sendChannel.send("test");

View file

@ -1,5 +1,10 @@
import { AudioRange, Decibels } from "../../core/type/Units.js";
import { InputNode, OutputNode, ToneAudioNode, ToneAudioNodeOptions } from "../../core/context/ToneAudioNode.js";
import {
InputNode,
OutputNode,
ToneAudioNode,
ToneAudioNodeOptions,
} from "../../core/context/ToneAudioNode.js";
import { optionsFromArguments } from "../../core/util/Defaults.js";
import { Solo } from "./Solo.js";
import { PanVol } from "./PanVol.js";
@ -16,7 +21,7 @@ export interface ChannelOptions extends ToneAudioNodeOptions {
}
/**
* Channel provides a channel strip interface with volume, pan, solo and mute controls.
* Channel provides a channel strip interface with volume, pan, solo and mute controls.
* @see {@link PanVol} and {@link Solo}
* @example
* // pan the incoming signal left and drop the volume 12db
@ -24,7 +29,6 @@ export interface ChannelOptions extends ToneAudioNodeOptions {
* @category Component
*/
export class Channel extends ToneAudioNode<ChannelOptions> {
readonly name: string = "Channel";
readonly input: InputNode;
@ -59,8 +63,16 @@ export class Channel extends ToneAudioNode<ChannelOptions> {
constructor(volume?: Decibels, pan?: AudioRange);
constructor(options?: Partial<ChannelOptions>);
constructor() {
super(optionsFromArguments(Channel.getDefaults(), arguments, ["volume", "pan"]));
const options = optionsFromArguments(Channel.getDefaults(), arguments, ["volume", "pan"]);
super(
optionsFromArguments(Channel.getDefaults(), arguments, [
"volume",
"pan",
])
);
const options = optionsFromArguments(Channel.getDefaults(), arguments, [
"volume",
"pan",
]);
this._solo = this.input = new Solo({
solo: options.solo,
@ -71,7 +83,7 @@ export class Channel extends ToneAudioNode<ChannelOptions> {
pan: options.pan,
volume: options.volume,
mute: options.mute,
channelCount: options.channelCount
channelCount: options.channelCount,
});
this.pan = this._panVol.pan;
this.volume = this._panVol.volume;
@ -119,7 +131,7 @@ export class Channel extends ToneAudioNode<ChannelOptions> {
}
/**
* Store the send/receive channels by name.
* Store the send/receive channels by name.
*/
private static buses: Map<string, Gain> = new Map();
@ -137,11 +149,11 @@ export class Channel extends ToneAudioNode<ChannelOptions> {
/**
* Send audio to another channel using a string. `send` is a lot like
* {@link connect}, except it uses a string instead of an object. This can
* {@link connect}, except it uses a string instead of an object. This can
* be useful in large applications to decouple sections since {@link send}
* and {@link receive} can be invoked separately in order to connect an object
* @param name The channel name to send the audio
* @param volume The amount of the signal to send.
* @param volume The amount of the signal to send.
* Defaults to 0db, i.e. send the entire signal
* @returns Returns the gain node of this connection.
*/
@ -158,7 +170,7 @@ export class Channel extends ToneAudioNode<ChannelOptions> {
}
/**
* Receive audio from a channel which was connected with {@link send}.
* Receive audio from a channel which was connected with {@link send}.
* @param name The channel name to receive audio from.
*/
receive(name: string): this {

View file

@ -5,11 +5,9 @@ import { Signal } from "../../signal/Signal.js";
import { CrossFade } from "./CrossFade.js";
describe("CrossFade", () => {
BasicTests(CrossFade);
context("Fading", () => {
it("handles input and output connections", () => {
const comp = new CrossFade();
connectFrom().connect(comp.a);
@ -19,39 +17,51 @@ describe("CrossFade", () => {
});
it("pass 100% of input 0", () => {
return ConstantOutput(() => {
const crossFade = new CrossFade();
const drySignal = new Signal(10);
const wetSignal = new Signal(20);
drySignal.connect(crossFade.a);
wetSignal.connect(crossFade.b);
crossFade.fade.value = 0;
crossFade.toDestination();
}, 10, 0.05);
return ConstantOutput(
() => {
const crossFade = new CrossFade();
const drySignal = new Signal(10);
const wetSignal = new Signal(20);
drySignal.connect(crossFade.a);
wetSignal.connect(crossFade.b);
crossFade.fade.value = 0;
crossFade.toDestination();
},
10,
0.05
);
});
it("pass 100% of input 1", () => {
return ConstantOutput(() => {
const crossFade = new CrossFade();
const drySignal = new Signal(10);
const wetSignal = new Signal(20);
drySignal.connect(crossFade.a);
wetSignal.connect(crossFade.b);
crossFade.fade.value = 1;
crossFade.toDestination();
}, 20, 0.01);
return ConstantOutput(
() => {
const crossFade = new CrossFade();
const drySignal = new Signal(10);
const wetSignal = new Signal(20);
drySignal.connect(crossFade.a);
wetSignal.connect(crossFade.b);
crossFade.fade.value = 1;
crossFade.toDestination();
},
20,
0.01
);
});
it("can mix two signals", () => {
return ConstantOutput(() => {
const crossFade = new CrossFade();
const drySignal = new Signal(2);
const wetSignal = new Signal(1);
drySignal.connect(crossFade.a);
wetSignal.connect(crossFade.b);
crossFade.fade.value = 0.5;
crossFade.toDestination();
}, 2.12, 0.01);
return ConstantOutput(
() => {
const crossFade = new CrossFade();
const drySignal = new Signal(2);
const wetSignal = new Signal(1);
drySignal.connect(crossFade.a);
wetSignal.connect(crossFade.b);
crossFade.fade.value = 0.5;
crossFade.toDestination();
},
2.12,
0.01
);
});
});
});

View file

@ -1,5 +1,9 @@
import { Gain } from "../../core/context/Gain.js";
import { connect, ToneAudioNode, ToneAudioNodeOptions } from "../../core/context/ToneAudioNode.js";
import {
connect,
ToneAudioNode,
ToneAudioNodeOptions,
} from "../../core/context/ToneAudioNode.js";
import { NormalRange } from "../../core/type/Units.js";
import { optionsFromArguments } from "../../core/util/Defaults.js";
import { readOnly } from "../../core/util/Interface.js";
@ -37,7 +41,6 @@ interface CrossFadeOptions extends ToneAudioNodeOptions {
* @category Component
*/
export class CrossFade extends ToneAudioNode<CrossFadeOptions> {
readonly name: string = "CrossFade";
/**
@ -97,8 +100,18 @@ export class CrossFade extends ToneAudioNode<CrossFadeOptions> {
constructor(fade?: NormalRange);
constructor(options?: Partial<CrossFadeOptions>);
constructor() {
super(Object.assign(optionsFromArguments(CrossFade.getDefaults(), arguments, ["fade"])));
const options = optionsFromArguments(CrossFade.getDefaults(), arguments, ["fade"]);
super(
Object.assign(
optionsFromArguments(CrossFade.getDefaults(), arguments, [
"fade",
])
)
);
const options = optionsFromArguments(
CrossFade.getDefaults(),
arguments,
["fade"]
);
this.fade = new Signal({
context: this.context,

View file

@ -7,11 +7,9 @@ import { Signal } from "../../signal/Signal.js";
import { Merge } from "./Merge.js";
describe("Merge", () => {
BasicTests(Merge);
context("Merging", () => {
it("handles input and output connections", () => {
const merge = new Merge();
connectFrom().connect(merge);
@ -43,14 +41,18 @@ describe("Merge", () => {
});
it("merge two signal into one stereo signal", () => {
return Offline(() => {
const sigL = new Signal(1);
const sigR = new Signal(2);
const merger = new Merge();
sigL.connect(merger, 0, 0);
sigR.connect(merger, 0, 1);
merger.toDestination();
}, 0.1, 2).then(buffer => {
return Offline(
() => {
const sigL = new Signal(1);
const sigR = new Signal(2);
const merger = new Merge();
sigL.connect(merger, 0, 0);
sigR.connect(merger, 0, 1);
merger.toDestination();
},
0.1,
2
).then((buffer) => {
expect(buffer.toArray()[0][0]).to.be.closeTo(1, 0.001);
expect(buffer.toArray()[1][0]).to.be.closeTo(2, 0.001);
});

View file

@ -1,4 +1,7 @@
import { ToneAudioNode, ToneAudioNodeOptions } from "../../core/context/ToneAudioNode.js";
import {
ToneAudioNode,
ToneAudioNodeOptions,
} from "../../core/context/ToneAudioNode.js";
import { Positive } from "../../core/type/Units.js";
import { optionsFromArguments } from "../../core/util/Defaults.js";
@ -18,7 +21,6 @@ interface MergeOptions extends ToneAudioNodeOptions {
* @category Component
*/
export class Merge extends ToneAudioNode<MergeOptions> {
readonly name: string = "Merge";
/**
@ -42,10 +44,17 @@ export class Merge extends ToneAudioNode<MergeOptions> {
constructor(channels?: Positive);
constructor(options?: Partial<MergeOptions>);
constructor() {
super(optionsFromArguments(Merge.getDefaults(), arguments, ["channels"]));
const options = optionsFromArguments(Merge.getDefaults(), arguments, ["channels"]);
super(
optionsFromArguments(Merge.getDefaults(), arguments, ["channels"])
);
const options = optionsFromArguments(Merge.getDefaults(), arguments, [
"channels",
]);
this._merger = this.output = this.input = this.context.createChannelMerger(options.channels);
this._merger =
this.output =
this.input =
this.context.createChannelMerger(options.channels);
}
static getDefaults(): MergeOptions {

View file

@ -4,11 +4,9 @@ import { PassAudio } from "../../../test/helper/PassAudio.js";
import { connectFrom, connectTo } from "../../../test/helper/Connect.js";
describe("MidSideMerge", () => {
BasicTests(MidSideMerge);
context("Merging", () => {
it("handles inputs and outputs", () => {
const merge = new MidSideMerge();
merge.connect(connectTo());
@ -25,4 +23,3 @@ describe("MidSideMerge", () => {
});
});
});

View file

@ -1,4 +1,7 @@
import { ToneAudioNode, ToneAudioNodeOptions } from "../../core/context/ToneAudioNode.js";
import {
ToneAudioNode,
ToneAudioNodeOptions,
} from "../../core/context/ToneAudioNode.js";
import { Merge } from "./Merge.js";
import { Add } from "../../signal/Add.js";
import { Multiply } from "../../signal/Multiply.js";
@ -17,7 +20,6 @@ export type MidSideMergeOptions = ToneAudioNodeOptions;
* @category Component
*/
export class MidSideMerge extends ToneAudioNode<MidSideMergeOptions> {
readonly name: string = "MidSideMerge";
/**
@ -64,7 +66,7 @@ export class MidSideMerge extends ToneAudioNode<MidSideMergeOptions> {
* Multiply the left by sqrt(1/2)
*/
private _rightMult: Multiply;
constructor(options?: Partial<MidSideMergeOptions>);
constructor() {
super(optionsFromArguments(MidSideMerge.getDefaults(), arguments));
@ -72,13 +74,13 @@ export class MidSideMerge extends ToneAudioNode<MidSideMergeOptions> {
this.side = new Gain({ context: this.context });
this._left = new Add({ context: this.context });
this._leftMult = new Multiply({
context: this.context,
value: Math.SQRT1_2
context: this.context,
value: Math.SQRT1_2,
});
this._right = new Subtract({ context: this.context });
this._rightMult = new Multiply({
context: this.context,
value: Math.SQRT1_2
context: this.context,
value: Math.SQRT1_2,
});
this._merge = this.output = new Merge({ context: this.context });
@ -91,7 +93,7 @@ export class MidSideMerge extends ToneAudioNode<MidSideMergeOptions> {
this._leftMult.connect(this._merge, 0, 0);
this._rightMult.connect(this._merge, 0, 1);
}
dispose(): this {
super.dispose();
this.mid.dispose();

View file

@ -8,11 +8,9 @@ import { connectFrom, connectTo } from "../../../test/helper/Connect.js";
import { expect } from "chai";
describe("MidSideSplit", () => {
BasicTests(MidSideSplit);
context("Splitting", () => {
it("handles inputs and outputs", () => {
const split = new MidSideSplit();
connectFrom().connect(split);
@ -61,19 +59,26 @@ describe("MidSideSplit", () => {
});
it("can decompose and reconstruct a signal", () => {
return Offline(() => {
const midSideMerge = new MidSideMerge().toDestination();
const split = new MidSideSplit();
split.mid.connect(midSideMerge.mid);
split.side.connect(midSideMerge.side);
const merge = new Merge().connect(split);
new Signal(0.2).connect(merge, 0, 0);
new Signal(0.4).connect(merge, 0, 1);
}, 0.1, 2).then((buffer) => {
buffer.toArray()[0].forEach(l => expect(l).to.be.closeTo(0.2, 0.01));
buffer.toArray()[1].forEach(r => expect(r).to.be.closeTo(0.4, 0.01));
return Offline(
() => {
const midSideMerge = new MidSideMerge().toDestination();
const split = new MidSideSplit();
split.mid.connect(midSideMerge.mid);
split.side.connect(midSideMerge.side);
const merge = new Merge().connect(split);
new Signal(0.2).connect(merge, 0, 0);
new Signal(0.4).connect(merge, 0, 1);
},
0.1,
2
).then((buffer) => {
buffer
.toArray()[0]
.forEach((l) => expect(l).to.be.closeTo(0.2, 0.01));
buffer
.toArray()[1]
.forEach((r) => expect(r).to.be.closeTo(0.4, 0.01));
});
});
});
});

View file

@ -1,4 +1,7 @@
import { ToneAudioNode, ToneAudioNodeOptions } from "../../core/context/ToneAudioNode.js";
import {
ToneAudioNode,
ToneAudioNodeOptions,
} from "../../core/context/ToneAudioNode.js";
import { Split } from "./Split.js";
import { Add } from "../../signal/Add.js";
import { Multiply } from "../../signal/Multiply.js";
@ -17,7 +20,6 @@ export type MidSideSplitOptions = ToneAudioNodeOptions;
* @category Component
*/
export class MidSideSplit extends ToneAudioNode<MidSideSplitOptions> {
readonly name: string = "MidSideSplit";
readonly input: Split;
@ -37,7 +39,7 @@ export class MidSideSplit extends ToneAudioNode<MidSideSplitOptions> {
private _midAdd: Add;
/**
* Subtract left and right channels.
* Subtract left and right channels.
*/
private _sideSubtract: Subtract;
@ -50,14 +52,14 @@ export class MidSideSplit extends ToneAudioNode<MidSideSplitOptions> {
* The "side" output. `(Left-Right)/sqrt(2)`
*/
readonly side: ToneAudioNode;
constructor(options?: Partial<MidSideSplitOptions>);
constructor() {
super(optionsFromArguments(MidSideSplit.getDefaults(), arguments));
this._split = this.input = new Split({
channels: 2,
context: this.context
context: this.context,
});
this._midAdd = new Add({ context: this.context });
this.mid = new Multiply({

View file

@ -6,16 +6,18 @@ import { Signal } from "../../signal/Signal.js";
import { Mono } from "./Mono.js";
describe("Mono", () => {
BasicTests(Mono);
context("Mono", () => {
it("Makes a mono signal in both channels", () => {
return Offline(() => {
const mono = new Mono().toDestination();
const signal = new Signal(2).connect(mono);
}, 0.1, 2).then((buffer) => {
return Offline(
() => {
const mono = new Mono().toDestination();
const signal = new Signal(2).connect(mono);
},
0.1,
2
).then((buffer) => {
expect(buffer.toArray()[0][0]).to.equal(2);
expect(buffer.toArray()[1][0]).to.equal(2);
expect(buffer.toArray()[0][100]).to.equal(2);
@ -26,10 +28,14 @@ describe("Mono", () => {
});
it("Sums a stereo signal into a mono signal", () => {
return Offline(() => {
const mono = new Mono().toDestination();
const signal = StereoSignal(2, 2).connect(mono);
}, 0.1, 2).then((buffer) => {
return Offline(
() => {
const mono = new Mono().toDestination();
const signal = StereoSignal(2, 2).connect(mono);
},
0.1,
2
).then((buffer) => {
expect(buffer.toArray()[0][0]).to.equal(2);
expect(buffer.toArray()[1][0]).to.equal(2);
expect(buffer.toArray()[0][100]).to.equal(2);
@ -40,4 +46,3 @@ describe("Mono", () => {
});
});
});

View file

@ -1,5 +1,9 @@
import { Gain } from "../../core/context/Gain.js";
import { OutputNode, ToneAudioNode, ToneAudioNodeOptions } from "../../core/context/ToneAudioNode.js";
import {
OutputNode,
ToneAudioNode,
ToneAudioNodeOptions,
} from "../../core/context/ToneAudioNode.js";
import { optionsFromArguments } from "../../core/util/Defaults.js";
import { Merge } from "./Merge.js";
@ -12,7 +16,6 @@ export type MonoOptions = ToneAudioNodeOptions;
* @category Component
*/
export class Mono extends ToneAudioNode<MonoOptions> {
readonly name: string = "Mono";
/**
@ -32,7 +35,6 @@ export class Mono extends ToneAudioNode<MonoOptions> {
constructor(options?: Partial<MonoOptions>);
constructor() {
super(optionsFromArguments(Mono.getDefaults(), arguments));
this.input = new Gain({ context: this.context });

View file

@ -5,7 +5,6 @@ import { PassAudio } from "../../../test/helper/PassAudio.js";
import { MultibandSplit } from "./MultibandSplit.js";
describe("MultibandSplit", () => {
BasicTests(MultibandSplit);
it("handles input and output connections", () => {
@ -41,21 +40,21 @@ describe("MultibandSplit", () => {
});
it("passes the incoming signal through low", () => {
return PassAudio(input => {
return PassAudio((input) => {
const split = new MultibandSplit().low.toDestination();
input.connect(split);
});
});
it("passes the incoming signal through mid", () => {
return PassAudio(input => {
return PassAudio((input) => {
const split = new MultibandSplit().mid.toDestination();
input.connect(split);
});
});
it("passes the incoming signal through high", () => {
return PassAudio(input => {
return PassAudio((input) => {
const split = new MultibandSplit({
highFrequency: 10,
lowFrequency: 5,

View file

@ -1,5 +1,8 @@
import { Gain } from "../../core/context/Gain.js";
import { ToneAudioNode, ToneAudioNodeOptions } from "../../core/context/ToneAudioNode.js";
import {
ToneAudioNode,
ToneAudioNodeOptions,
} from "../../core/context/ToneAudioNode.js";
import { Frequency, Positive } from "../../core/type/Units.js";
import { optionsFromArguments } from "../../core/util/Defaults.js";
import { readOnly, writable } from "../../core/util/Interface.js";
@ -31,7 +34,6 @@ interface MultibandSplitOptions extends ToneAudioNodeOptions {
* @category Component
*/
export class MultibandSplit extends ToneAudioNode<MultibandSplitOptions> {
readonly name: string = "MultibandSplit";
/**
@ -104,8 +106,17 @@ export class MultibandSplit extends ToneAudioNode<MultibandSplitOptions> {
constructor(lowFrequency?: Frequency, highFrequency?: Frequency);
constructor(options?: Partial<MultibandSplitOptions>);
constructor() {
super(optionsFromArguments(MultibandSplit.getDefaults(), arguments, ["lowFrequency", "highFrequency"]));
const options = optionsFromArguments(MultibandSplit.getDefaults(), arguments, ["lowFrequency", "highFrequency"]);
super(
optionsFromArguments(MultibandSplit.getDefaults(), arguments, [
"lowFrequency",
"highFrequency",
])
);
const options = optionsFromArguments(
MultibandSplit.getDefaults(),
arguments,
["lowFrequency", "highFrequency"]
);
this.lowFrequency = new Signal({
context: this.context,
@ -162,5 +173,4 @@ export class MultibandSplit extends ToneAudioNode<MultibandSplitOptions> {
this.Q.dispose();
return this;
}
}

View file

@ -6,11 +6,9 @@ import { Signal } from "../../signal/Signal.js";
import { PanVol } from "./PanVol.js";
describe("PanVol", () => {
BasicTests(PanVol);
context("Pan and Volume", () => {
it("can be constructed with the panning and volume value", () => {
const panVol = new PanVol(0.3, -12);
expect(panVol.pan.value).to.be.closeTo(0.3, 0.001);
@ -53,6 +51,5 @@ describe("PanVol", () => {
expect(buffer.isSilent()).to.be.true;
});
});
});
});

View file

@ -1,6 +1,11 @@
import { readOnly } from "../../core/util/Interface.js";
import { Param } from "../../core/context/Param.js";
import { InputNode, OutputNode, ToneAudioNode, ToneAudioNodeOptions } from "../../core/context/ToneAudioNode.js";
import {
InputNode,
OutputNode,
ToneAudioNode,
ToneAudioNodeOptions,
} from "../../core/context/ToneAudioNode.js";
import { AudioRange, Decibels } from "../../core/type/Units.js";
import { optionsFromArguments } from "../../core/util/Defaults.js";
import { Panner } from "./Panner.js";
@ -22,7 +27,6 @@ export interface PanVolOptions extends ToneAudioNodeOptions {
* @category Component
*/
export class PanVol extends ToneAudioNode<PanVolOptions> {
readonly name: string = "PanVol";
readonly input: InputNode;
@ -57,9 +61,16 @@ export class PanVol extends ToneAudioNode<PanVolOptions> {
constructor(pan?: AudioRange, volume?: Decibels);
constructor(options?: Partial<PanVolOptions>);
constructor() {
super(optionsFromArguments(PanVol.getDefaults(), arguments, ["pan", "volume"]));
const options = optionsFromArguments(PanVol.getDefaults(), arguments, ["pan", "volume"]);
super(
optionsFromArguments(PanVol.getDefaults(), arguments, [
"pan",
"volume",
])
);
const options = optionsFromArguments(PanVol.getDefaults(), arguments, [
"pan",
"volume",
]);
this._panner = this.input = new Panner({
context: this.context,

View file

@ -6,11 +6,9 @@ import { Signal } from "../../signal/Signal.js";
import { Panner } from "./Panner.js";
describe("Panner", () => {
BasicTests(Panner);
context("Panning", () => {
it("can be constructed with the panning value", () => {
const panner = new Panner(0.3);
expect(panner.pan.value).to.be.closeTo(0.3, 0.001);
@ -33,10 +31,14 @@ describe("Panner", () => {
});
it("pans hard left when the pan is set to -1", () => {
return Offline(() => {
const panner = new Panner(-1).toDestination();
new Signal(1).connect(panner);
}, 0.1, 2).then((buffer) => {
return Offline(
() => {
const panner = new Panner(-1).toDestination();
new Signal(1).connect(panner);
},
0.1,
2
).then((buffer) => {
const l = buffer.toArray()[0];
const r = buffer.toArray()[1];
expect(l[0]).to.be.closeTo(1, 0.01);
@ -45,10 +47,14 @@ describe("Panner", () => {
});
it("pans hard right when the pan is set to 1", () => {
return Offline(() => {
const panner = new Panner(1).toDestination();
new Signal(1).connect(panner);
}, 0.1, 2).then((buffer) => {
return Offline(
() => {
const panner = new Panner(1).toDestination();
new Signal(1).connect(panner);
},
0.1,
2
).then((buffer) => {
const l = buffer.toArray()[0];
const r = buffer.toArray()[1];
expect(l[0]).to.be.closeTo(0, 0.01);
@ -57,10 +63,14 @@ describe("Panner", () => {
});
it("mixes the signal in equal power when panned center", () => {
return Offline(() => {
const panner = new Panner(0).toDestination();
new Signal(1).connect(panner);
}, 0.1, 2).then((buffer) => {
return Offline(
() => {
const panner = new Panner(0).toDestination();
new Signal(1).connect(panner);
},
0.1,
2
).then((buffer) => {
const l = buffer.toArray()[0];
const r = buffer.toArray()[1];
expect(l[0]).to.be.closeTo(0.707, 0.01);
@ -69,13 +79,17 @@ describe("Panner", () => {
});
it("can chain two panners when channelCount is 2", () => {
return Offline(() => {
const panner1 = new Panner({
channelCount: 2,
}).toDestination();
const panner0 = new Panner(-1).connect(panner1);
new Signal(1).connect(panner0);
}, 0.1, 2).then((buffer) => {
return Offline(
() => {
const panner1 = new Panner({
channelCount: 2,
}).toDestination();
const panner0 = new Panner(-1).connect(panner1);
new Signal(1).connect(panner0);
},
0.1,
2
).then((buffer) => {
const l = buffer.toArray()[0];
const r = buffer.toArray()[1];
expect(l[0]).to.be.closeTo(1, 0.01);

View file

@ -1,5 +1,8 @@
import { Param } from "../../core/context/Param.js";
import { ToneAudioNode, ToneAudioNodeOptions } from "../../core/context/ToneAudioNode.js";
import {
ToneAudioNode,
ToneAudioNodeOptions,
} from "../../core/context/ToneAudioNode.js";
import { AudioRange } from "../../core/type/Units.js";
import { optionsFromArguments } from "../../core/util/Defaults.js";
import { readOnly } from "../../core/util/Interface.js";
@ -21,7 +24,6 @@ interface TonePannerOptions extends ToneAudioNodeOptions {
* @category Component
*/
export class Panner extends ToneAudioNode<TonePannerOptions> {
readonly name: string = "Panner";
/**
@ -52,8 +54,14 @@ export class Panner extends ToneAudioNode<TonePannerOptions> {
*/
constructor(pan?: AudioRange);
constructor() {
super(Object.assign(optionsFromArguments(Panner.getDefaults(), arguments, ["pan"])));
const options = optionsFromArguments(Panner.getDefaults(), arguments, ["pan"]);
super(
Object.assign(
optionsFromArguments(Panner.getDefaults(), arguments, ["pan"])
)
);
const options = optionsFromArguments(Panner.getDefaults(), arguments, [
"pan",
]);
this.pan = new Param({
context: this.context,

View file

@ -4,7 +4,6 @@ import { PassAudio } from "../../../test/helper/PassAudio.js";
import { Panner3D } from "./Panner3D.js";
describe("Panner3D", () => {
BasicTests(Panner3D);
it("passes the incoming signal through", () => {

View file

@ -1,5 +1,8 @@
import { Param } from "../../core/context/Param.js";
import { ToneAudioNode, ToneAudioNodeOptions } from "../../core/context/ToneAudioNode.js";
import {
ToneAudioNode,
ToneAudioNodeOptions,
} from "../../core/context/ToneAudioNode.js";
import { Degrees, GainFactor } from "../../core/type/Units.js";
import { optionsFromArguments } from "../../core/util/Defaults.js";
import "../../core/context/Listener.js";
@ -26,7 +29,6 @@ export interface Panner3DOptions extends ToneAudioNodeOptions {
* @category Component
*/
export class Panner3D extends ToneAudioNode<Panner3DOptions> {
readonly name: string = "Panner3D";
/**
@ -52,9 +54,18 @@ export class Panner3D extends ToneAudioNode<Panner3DOptions> {
constructor(positionX: number, positionY: number, positionZ: number);
constructor(options?: Partial<Panner3DOptions>);
constructor() {
super(optionsFromArguments(Panner3D.getDefaults(), arguments, ["positionX", "positionY", "positionZ"]));
const options = optionsFromArguments(Panner3D.getDefaults(), arguments, ["positionX", "positionY", "positionZ"]);
super(
optionsFromArguments(Panner3D.getDefaults(), arguments, [
"positionX",
"positionY",
"positionZ",
])
);
const options = optionsFromArguments(
Panner3D.getDefaults(),
arguments,
["positionX", "positionY", "positionZ"]
);
this._panner = this.input = this.output = this.context.createPanner();
// set some values

View file

@ -6,9 +6,7 @@ import { ToneWithContext } from "../../core/context/ToneWithContext.js";
import { Synth } from "../../instrument/Synth.js";
describe("Recorder", () => {
context("basic", () => {
it("can be created and disposed", () => {
const rec = new Recorder();
rec.dispose();
@ -33,11 +31,13 @@ describe("Recorder", () => {
it("can set a different context", () => {
const testContext = new Context();
const rec = new Recorder({
context: testContext
context: testContext,
});
for (const member in rec) {
if (rec[member] instanceof ToneWithContext) {
expect(rec[member].context, `member: ${member}`).to.equal(testContext);
expect(rec[member].context, `member: ${member}`).to.equal(
testContext
);
}
}
testContext.dispose();
@ -47,11 +47,10 @@ describe("Recorder", () => {
});
function wait(time) {
return new Promise(done => setTimeout(done, time));
return new Promise((done) => setTimeout(done, time));
}
context("start/stop/pause", () => {
it("can be started", () => {
const rec = new Recorder();
rec.start();
@ -114,6 +113,5 @@ describe("Recorder", () => {
rec.dispose();
synth.dispose();
});
});
});

View file

@ -1,4 +1,7 @@
import { ToneAudioNode, ToneAudioNodeOptions } from "../../core/context/ToneAudioNode.js";
import {
ToneAudioNode,
ToneAudioNodeOptions,
} from "../../core/context/ToneAudioNode.js";
import { Gain } from "../../core/context/Gain.js";
import { assert } from "../../core/util/Debug.js";
import { theWindow } from "../../core/context/AudioContext.js";
@ -12,8 +15,8 @@ export interface RecorderOptions extends ToneAudioNodeOptions {
/**
* A wrapper around the MediaRecorder API. Unlike the rest of Tone.js, this module does not offer
* any sample-accurate scheduling because it is not a feature of the MediaRecorder API.
* This is only natively supported in Chrome and Firefox.
* For a cross-browser shim, install (audio-recorder-polyfill)[https://www.npmjs.com/package/audio-recorder-polyfill].
* This is only natively supported in Chrome and Firefox.
* For a cross-browser shim, install (audio-recorder-polyfill)[https://www.npmjs.com/package/audio-recorder-polyfill].
* @example
* const recorder = new Tone.Recorder();
* const synth = new Tone.Synth().connect(recorder);
@ -37,7 +40,6 @@ export interface RecorderOptions extends ToneAudioNodeOptions {
* @category Component
*/
export class Recorder extends ToneAudioNode<RecorderOptions> {
readonly name = "Recorder";
/**
@ -46,7 +48,7 @@ export class Recorder extends ToneAudioNode<RecorderOptions> {
private _recorder: MediaRecorder;
/**
* MediaRecorder requires
* MediaRecorder requires
*/
private _stream: MediaStreamAudioDestinationNode;
@ -55,12 +57,11 @@ export class Recorder extends ToneAudioNode<RecorderOptions> {
constructor(options?: Partial<RecorderOptions>);
constructor() {
super(optionsFromArguments(Recorder.getDefaults(), arguments));
const options = optionsFromArguments(Recorder.getDefaults(), arguments);
this.input = new Gain({
context: this.context
context: this.context,
});
assert(Recorder.supported, "Media Recorder API is not available");
@ -68,7 +69,7 @@ export class Recorder extends ToneAudioNode<RecorderOptions> {
this._stream = this.context.createMediaStreamDestination();
this.input.connect(this._stream);
this._recorder = new MediaRecorder(this._stream.stream, {
mimeType: options.mimeType
mimeType: options.mimeType,
});
}
@ -77,15 +78,15 @@ export class Recorder extends ToneAudioNode<RecorderOptions> {
}
/**
* The mime type is the format that the audio is encoded in. For Chrome
* that is typically webm encoded as "vorbis".
* The mime type is the format that the audio is encoded in. For Chrome
* that is typically webm encoded as "vorbis".
*/
get mimeType(): string {
return this._recorder.mimeType;
}
/**
* Test if your platform supports the Media Recorder API. If it's not available,
* Test if your platform supports the Media Recorder API. If it's not available,
* try installing this (polyfill)[https://www.npmjs.com/package/audio-recorder-polyfill].
*/
static get supported(): boolean {
@ -111,7 +112,7 @@ export class Recorder extends ToneAudioNode<RecorderOptions> {
*/
async start() {
assert(this.state !== "started", "Recorder is already started");
const startPromise = new Promise<void>(done => {
const startPromise = new Promise<void>((done) => {
const handleStart = () => {
this._recorder.removeEventListener("start", handleStart, false);
@ -131,9 +132,13 @@ export class Recorder extends ToneAudioNode<RecorderOptions> {
*/
async stop(): Promise<Blob> {
assert(this.state !== "stopped", "Recorder is not started");
const dataPromise: Promise<Blob> = new Promise(done => {
const dataPromise: Promise<Blob> = new Promise((done) => {
const handleData = (e: BlobEvent) => {
this._recorder.removeEventListener("dataavailable", handleData, false);
this._recorder.removeEventListener(
"dataavailable",
handleData,
false
);
done(e.data);
};

View file

@ -5,11 +5,9 @@ import { Signal } from "../../signal/Signal.js";
import { Solo } from "./Solo.js";
describe("Solo", () => {
BasicTests(Solo);
context("Soloing", () => {
it("can be soloed an unsoloed", () => {
const sol = new Solo();
sol.solo = true;
@ -81,58 +79,78 @@ describe("Solo", () => {
});
it("passes both signals when nothing is soloed", () => {
return ConstantOutput(() => {
const soloA = new Solo().toDestination();
const soloB = new Solo().toDestination();
new Signal(10).connect(soloA);
new Signal(20).connect(soloB);
}, 30, 0.01);
return ConstantOutput(
() => {
const soloA = new Solo().toDestination();
const soloB = new Solo().toDestination();
new Signal(10).connect(soloA);
new Signal(20).connect(soloB);
},
30,
0.01
);
});
it("passes one signal when it is soloed", () => {
return ConstantOutput(() => {
const soloA = new Solo().toDestination();
const soloB = new Solo().toDestination();
new Signal(10).connect(soloA);
new Signal(20).connect(soloB);
soloA.solo = true;
}, 10, 0.01);
return ConstantOutput(
() => {
const soloA = new Solo().toDestination();
const soloB = new Solo().toDestination();
new Signal(10).connect(soloA);
new Signal(20).connect(soloB);
soloA.solo = true;
},
10,
0.01
);
});
it("can solo multiple at once", () => {
return ConstantOutput(() => {
const soloA = new Solo().toDestination();
const soloB = new Solo().toDestination();
new Signal(10).connect(soloA);
new Signal(20).connect(soloB);
soloA.solo = true;
soloB.solo = true;
}, 30, 0.01);
return ConstantOutput(
() => {
const soloA = new Solo().toDestination();
const soloB = new Solo().toDestination();
new Signal(10).connect(soloA);
new Signal(20).connect(soloB);
soloA.solo = true;
soloB.solo = true;
},
30,
0.01
);
});
it("can unsolo all", () => {
return ConstantOutput(() => {
const soloA = new Solo().toDestination();
const soloB = new Solo().toDestination();
new Signal(10).connect(soloA);
new Signal(20).connect(soloB);
soloA.solo = true;
soloB.solo = true;
soloA.solo = false;
soloB.solo = false;
}, 30, 0.01);
return ConstantOutput(
() => {
const soloA = new Solo().toDestination();
const soloB = new Solo().toDestination();
new Signal(10).connect(soloA);
new Signal(20).connect(soloB);
soloA.solo = true;
soloB.solo = true;
soloA.solo = false;
soloB.solo = false;
},
30,
0.01
);
});
it("can solo and unsolo while keeping previous soloed", () => {
return ConstantOutput(() => {
const soloA = new Solo().toDestination();
const soloB = new Solo().toDestination();
new Signal(10).connect(soloA);
new Signal(20).connect(soloB);
soloA.solo = true;
soloB.solo = true;
soloB.solo = false;
}, 10, 0.01);
return ConstantOutput(
() => {
const soloA = new Solo().toDestination();
const soloB = new Solo().toDestination();
new Signal(10).connect(soloA);
new Signal(20).connect(soloB);
soloA.solo = true;
soloB.solo = true;
soloB.solo = false;
},
10,
0.01
);
});
});
});

View file

@ -1,6 +1,9 @@
import { BaseContext } from "../../core/context/BaseContext.js";
import { Gain } from "../../core/context/Gain.js";
import { ToneAudioNode, ToneAudioNodeOptions } from "../../core/context/ToneAudioNode.js";
import {
ToneAudioNode,
ToneAudioNodeOptions,
} from "../../core/context/ToneAudioNode.js";
import { optionsFromArguments } from "../../core/util/Defaults.js";
export interface SoloOptions extends ToneAudioNodeOptions {
@ -20,7 +23,6 @@ export interface SoloOptions extends ToneAudioNodeOptions {
* @category Component
*/
export class Solo extends ToneAudioNode<SoloOptions> {
readonly name: string = "Solo";
readonly input: Gain;
@ -32,9 +34,10 @@ export class Solo extends ToneAudioNode<SoloOptions> {
constructor(solo?: boolean);
constructor(options?: Partial<SoloOptions>);
constructor() {
super(optionsFromArguments(Solo.getDefaults(), arguments, ["solo"]));
const options = optionsFromArguments(Solo.getDefaults(), arguments, ["solo"]);
const options = optionsFromArguments(Solo.getDefaults(), arguments, [
"solo",
]);
this.input = this.output = new Gain({
context: this.context,
@ -79,7 +82,9 @@ export class Solo extends ToneAudioNode<SoloOptions> {
} else {
this._removeSolo();
}
(Solo._allSolos.get(this.context) as Set<Solo>).forEach(instance => instance._updateSolo());
(Solo._allSolos.get(this.context) as Set<Solo>).forEach((instance) =>
instance._updateSolo()
);
}
/**
@ -112,7 +117,10 @@ export class Solo extends ToneAudioNode<SoloOptions> {
* Is this on the soloed array
*/
private _isSoloed(): boolean {
return Solo._soloed.has(this.context) && (Solo._soloed.get(this.context) as Set<Solo>).has(this);
return (
Solo._soloed.has(this.context) &&
(Solo._soloed.get(this.context) as Set<Solo>).has(this)
);
}
/**
@ -120,9 +128,12 @@ export class Solo extends ToneAudioNode<SoloOptions> {
*/
private _noSolos(): boolean {
// either does not have any soloed added
return !Solo._soloed.has(this.context) ||
return (
!Solo._soloed.has(this.context) ||
// or has a solo set but doesn't include any items
(Solo._soloed.has(this.context) && (Solo._soloed.get(this.context) as Set<Solo>).size === 0);
(Solo._soloed.has(this.context) &&
(Solo._soloed.get(this.context) as Set<Solo>).size === 0)
);
}
/**

View file

@ -6,11 +6,9 @@ import { StereoSignal } from "../../../test/helper/StereoSignal.js";
import { Split } from "./Split.js";
describe("Split", () => {
BasicTests(Split);
context("Splitting", () => {
it("defaults to two channels", () => {
const split = new Split();
expect(split.numberOfOutputs).to.equal(2);

View file

@ -1,4 +1,7 @@
import { ToneAudioNode, ToneAudioNodeOptions } from "../../core/context/ToneAudioNode.js";
import {
ToneAudioNode,
ToneAudioNodeOptions,
} from "../../core/context/ToneAudioNode.js";
import { optionsFromArguments } from "../../core/util/Defaults.js";
interface SplitOptions extends ToneAudioNodeOptions {
@ -30,10 +33,17 @@ export class Split extends ToneAudioNode<SplitOptions> {
constructor(channels?: number);
constructor(options?: Partial<SplitOptions>);
constructor() {
super(optionsFromArguments(Split.getDefaults(), arguments, ["channels"]));
const options = optionsFromArguments(Split.getDefaults(), arguments, ["channels"]);
super(
optionsFromArguments(Split.getDefaults(), arguments, ["channels"])
);
const options = optionsFromArguments(Split.getDefaults(), arguments, [
"channels",
]);
this._splitter = this.input = this.output = this.context.createChannelSplitter(options.channels);
this._splitter =
this.input =
this.output =
this.context.createChannelSplitter(options.channels);
this._internalChannels = [this._splitter];
}

View file

@ -7,11 +7,9 @@ import { Signal } from "../../signal/Signal.js";
import { Volume } from "./Volume.js";
describe("Volume", () => {
BasicTests(Volume);
context("Volume", () => {
it("handles input and output connections", () => {
const vol = new Volume();
vol.connect(connectTo());
@ -63,7 +61,7 @@ describe("Volume", () => {
});
it("passes the incoming signal through", () => {
return PassAudio(input => {
return PassAudio((input) => {
const vol = new Volume().toDestination();
input.connect(vol);
});
@ -100,7 +98,7 @@ describe("Volume", () => {
const vol = new Volume(-Infinity).toDestination();
new Signal(1).connect(vol);
expect(vol.mute).to.equal(true);
}).then(buffer => {
}).then((buffer) => {
expect(buffer.isSilent()).to.equal(true);
});
});

View file

@ -1,6 +1,9 @@
import { Gain } from "../../core/context/Gain.js";
import { Param } from "../../core/context/Param.js";
import { ToneAudioNode, ToneAudioNodeOptions } from "../../core/context/ToneAudioNode.js";
import {
ToneAudioNode,
ToneAudioNodeOptions,
} from "../../core/context/ToneAudioNode.js";
import { Decibels } from "../../core/type/Units.js";
import { optionsFromArguments } from "../../core/util/Defaults.js";
import { readOnly } from "../../core/util/Interface.js";
@ -19,7 +22,6 @@ interface VolumeOptions extends ToneAudioNodeOptions {
* @category Component
*/
export class Volume extends ToneAudioNode<VolumeOptions> {
readonly name: string = "Volume";
/**
@ -52,9 +54,12 @@ export class Volume extends ToneAudioNode<VolumeOptions> {
constructor(volume?: Decibels);
constructor(options?: Partial<VolumeOptions>);
constructor() {
super(optionsFromArguments(Volume.getDefaults(), arguments, ["volume"]));
const options = optionsFromArguments(Volume.getDefaults(), arguments, ["volume"]);
super(
optionsFromArguments(Volume.getDefaults(), arguments, ["volume"])
);
const options = optionsFromArguments(Volume.getDefaults(), arguments, [
"volume",
]);
this.input = this.output = new Gain({
context: this.context,

View file

@ -4,11 +4,9 @@ import { PassAudio } from "../../../test/helper/PassAudio.js";
import { Compressor } from "./Compressor.js";
describe("Compressor", () => {
BasicTests(Compressor);
context("Compression", () => {
it("passes the incoming signal through", () => {
return PassAudio((input) => {
const comp = new Compressor().toDestination();
@ -26,7 +24,13 @@ describe("Compressor", () => {
threshold: -30,
};
comp.set(values);
expect(comp.get()).to.have.keys(["ratio", "threshold", "release", "attack", "ratio"]);
expect(comp.get()).to.have.keys([
"ratio",
"threshold",
"release",
"attack",
"ratio",
]);
comp.dispose();
});

View file

@ -1,5 +1,8 @@
import { Param } from "../../core/context/Param.js";
import { ToneAudioNode, ToneAudioNodeOptions } from "../../core/context/ToneAudioNode.js";
import {
ToneAudioNode,
ToneAudioNodeOptions,
} from "../../core/context/ToneAudioNode.js";
import { Decibels, Positive, Time } from "../../core/type/Units.js";
import { optionsFromArguments } from "../../core/util/Defaults.js";
import { readOnly } from "../../core/util/Interface.js";
@ -23,13 +26,13 @@ export interface CompressorOptions extends ToneAudioNodeOptions {
* @category Component
*/
export class Compressor extends ToneAudioNode<CompressorOptions> {
readonly name: string = "Compressor";
/**
* the compressor node
*/
private _compressor: DynamicsCompressorNode = this.context.createDynamicsCompressor();
private _compressor: DynamicsCompressorNode =
this.context.createDynamicsCompressor();
readonly input = this._compressor;
readonly output = this._compressor;
@ -76,9 +79,17 @@ export class Compressor extends ToneAudioNode<CompressorOptions> {
constructor(threshold?: Decibels, ratio?: Positive);
constructor(options?: Partial<CompressorOptions>);
constructor() {
super(optionsFromArguments(Compressor.getDefaults(), arguments, ["threshold", "ratio"]));
const options = optionsFromArguments(Compressor.getDefaults(), arguments, ["threshold", "ratio"]);
super(
optionsFromArguments(Compressor.getDefaults(), arguments, [
"threshold",
"ratio",
])
);
const options = optionsFromArguments(
Compressor.getDefaults(),
arguments,
["threshold", "ratio"]
);
this.threshold = new Param({
minValue: this._compressor.threshold.minValue,

View file

@ -7,26 +7,28 @@ import { CompareToFile } from "../../../test/helper/CompareToFile.js";
import { expect } from "chai";
describe("Gate", () => {
BasicTests(Gate);
it.only("matches a file", () => {
return CompareToFile(() => {
const gate = new Gate(-10, 0.1).toDestination();
const osc = new Oscillator().connect(gate);
osc.start(0);
osc.volume.value = -100;
osc.volume.exponentialRampToValueAtTime(0, 0.5);
}, "gate.wav", 0.18);
return CompareToFile(
() => {
const gate = new Gate(-10, 0.1).toDestination();
const osc = new Oscillator().connect(gate);
osc.start(0);
osc.volume.value = -100;
osc.volume.exponentialRampToValueAtTime(0, 0.5);
},
"gate.wav",
0.18
);
});
context("Signal Gating", () => {
it("handles getter/setter as Object", () => {
const gate = new Gate();
const values = {
smoothing: 0.2,
threshold: -20
threshold: -20,
};
gate.set(values);
expect(gate.get().smoothing).to.be.closeTo(0.2, 0.001);
@ -37,7 +39,7 @@ describe("Gate", () => {
it("can be constructed with an object", () => {
const gate = new Gate({
smoothing: 0.3,
threshold: -5
threshold: -5,
});
expect(gate.smoothing).to.be.closeTo(0.3, 0.001);
expect(gate.threshold).to.be.closeTo(-5, 0.1);
@ -69,4 +71,3 @@ describe("Gate", () => {
});
});
});

View file

@ -1,4 +1,7 @@
import { ToneAudioNode, ToneAudioNodeOptions } from "../../core/context/ToneAudioNode.js";
import {
ToneAudioNode,
ToneAudioNodeOptions,
} from "../../core/context/ToneAudioNode.js";
import { Decibels, Time } from "../../core/type/Units.js";
import { GreaterThan } from "../../signal/GreaterThan.js";
import { Gain } from "../../core/context/Gain.js";
@ -24,7 +27,6 @@ export interface GateOptions extends ToneAudioNodeOptions {
* @category Component
*/
export class Gate extends ToneAudioNode<GateOptions> {
readonly name: string = "Gate";
readonly input: ToneAudioNode;
@ -52,8 +54,18 @@ export class Gate extends ToneAudioNode<GateOptions> {
constructor(threshold?: Decibels, smoothing?: Time);
constructor(options?: Partial<GateOptions>);
constructor() {
super(Object.assign(optionsFromArguments(Gate.getDefaults(), arguments, ["threshold", "smoothing"])));
const options = optionsFromArguments(Gate.getDefaults(), arguments, ["threshold", "smoothing"]);
super(
Object.assign(
optionsFromArguments(Gate.getDefaults(), arguments, [
"threshold",
"smoothing",
])
)
);
const options = optionsFromArguments(Gate.getDefaults(), arguments, [
"threshold",
"smoothing",
]);
this._follower = new Follower({
context: this.context,
@ -75,7 +87,7 @@ export class Gate extends ToneAudioNode<GateOptions> {
static getDefaults(): GateOptions {
return Object.assign(ToneAudioNode.getDefaults(), {
smoothing: 0.1,
threshold: -40
threshold: -40,
});
}
@ -90,7 +102,7 @@ export class Gate extends ToneAudioNode<GateOptions> {
}
/**
* The attack/decay speed of the gate.
* The attack/decay speed of the gate.
* @see {@link Follower.smoothing}
*/
get smoothing(): Time {

View file

@ -4,11 +4,9 @@ import { PassAudio } from "../../../test/helper/PassAudio.js";
import { expect } from "chai";
describe("Limiter", () => {
BasicTests(Limiter);
context("Limiting", () => {
it("passes the incoming signal through", () => {
return PassAudio((input) => {
const limiter = new Limiter().toDestination();
@ -40,4 +38,3 @@ describe("Limiter", () => {
});
});
});

View file

@ -1,4 +1,9 @@
import { InputNode, OutputNode, ToneAudioNode, ToneAudioNodeOptions } from "../../core/context/ToneAudioNode.js";
import {
InputNode,
OutputNode,
ToneAudioNode,
ToneAudioNodeOptions,
} from "../../core/context/ToneAudioNode.js";
import { Decibels } from "../../core/type/Units.js";
import { optionsFromArguments } from "../../core/util/Defaults.js";
import { Compressor } from "./Compressor.js";
@ -12,7 +17,7 @@ export interface LimiterOptions extends ToneAudioNodeOptions {
/**
* Limiter will limit the loudness of an incoming signal.
* Under the hood it's composed of a {@link Compressor} with a fast attack
* and release and max compression ratio.
* and release and max compression ratio.
*
* @example
* const limiter = new Tone.Limiter(-20).toDestination();
@ -21,7 +26,6 @@ export interface LimiterOptions extends ToneAudioNodeOptions {
* @category Component
*/
export class Limiter extends ToneAudioNode<LimiterOptions> {
readonly name: string = "Limiter";
readonly input: InputNode;
@ -40,16 +44,27 @@ export class Limiter extends ToneAudioNode<LimiterOptions> {
constructor(threshold?: Decibels);
constructor(options?: Partial<LimiterOptions>);
constructor() {
super(Object.assign(optionsFromArguments(Limiter.getDefaults(), arguments, ["threshold"])));
const options = optionsFromArguments(Limiter.getDefaults(), arguments, ["threshold"]);
super(
Object.assign(
optionsFromArguments(Limiter.getDefaults(), arguments, [
"threshold",
])
)
);
const options = optionsFromArguments(Limiter.getDefaults(), arguments, [
"threshold",
]);
this._compressor = this.input = this.output = new Compressor({
context: this.context,
ratio: 20,
attack: 0.003,
release: 0.01,
threshold: options.threshold
});
this._compressor =
this.input =
this.output =
new Compressor({
context: this.context,
ratio: 20,
attack: 0.003,
release: 0.01,
threshold: options.threshold,
});
this.threshold = this._compressor.threshold;
readOnly(this, "threshold");
@ -57,13 +72,13 @@ export class Limiter extends ToneAudioNode<LimiterOptions> {
static getDefaults(): LimiterOptions {
return Object.assign(ToneAudioNode.getDefaults(), {
threshold: -12
threshold: -12,
});
}
/**
* A read-only decibel value for metering purposes, representing the current amount of gain
* reduction that the compressor is applying to the signal.
* reduction that the compressor is applying to the signal.
*/
get reduction(): Decibels {
return this._compressor.reduction;

View file

@ -4,11 +4,9 @@ import { PassAudio } from "../../../test/helper/PassAudio.js";
import { expect } from "chai";
describe("MidSideCompressor", () => {
BasicTests(MidSideCompressor);
context("Compression", () => {
it("passes the incoming signal through", () => {
return PassAudio((input) => {
const comp = new MidSideCompressor().toDestination();
@ -26,8 +24,8 @@ describe("MidSideCompressor", () => {
side: {
release: 0.5,
attack: 0.03,
knee: 20
}
knee: 20,
},
};
comp.set(values);
expect(comp.get()).to.have.keys(["mid", "side"]);
@ -45,8 +43,8 @@ describe("MidSideCompressor", () => {
side: {
release: 0.5,
attack: 0.03,
knee: 20
}
knee: 20,
},
});
expect(comp.mid.ratio.value).be.closeTo(16, 0.01);
expect(comp.mid.threshold.value).be.closeTo(-30, 0.01);
@ -56,4 +54,3 @@ describe("MidSideCompressor", () => {
});
});
});

View file

@ -1,4 +1,9 @@
import { InputNode, OutputNode, ToneAudioNode, ToneAudioNodeOptions } from "../../core/context/ToneAudioNode.js";
import {
InputNode,
OutputNode,
ToneAudioNode,
ToneAudioNodeOptions,
} from "../../core/context/ToneAudioNode.js";
import { Compressor, CompressorOptions } from "./Compressor.js";
import { optionsFromArguments } from "../../core/util/Defaults.js";
import { MidSideSplit } from "../channel/MidSideSplit.js";
@ -12,12 +17,11 @@ export interface MidSideCompressorOptions extends ToneAudioNodeOptions {
/**
* MidSideCompressor applies two different compressors to the {@link mid}
* and {@link side} signal components of the input.
* and {@link side} signal components of the input.
* @see {@link MidSideSplit} and {@link MidSideMerge}.
* @category Component
*/
export class MidSideCompressor extends ToneAudioNode<MidSideCompressorOptions> {
readonly name: string = "MidSideCompressor";
readonly input: InputNode;
@ -45,13 +49,28 @@ export class MidSideCompressor extends ToneAudioNode<MidSideCompressorOptions> {
constructor(options?: RecursivePartial<MidSideCompressorOptions>);
constructor() {
super(Object.assign(optionsFromArguments(MidSideCompressor.getDefaults(), arguments)));
const options = optionsFromArguments(MidSideCompressor.getDefaults(), arguments);
super(
Object.assign(
optionsFromArguments(MidSideCompressor.getDefaults(), arguments)
)
);
const options = optionsFromArguments(
MidSideCompressor.getDefaults(),
arguments
);
this._midSideSplit = this.input = new MidSideSplit({ context: this.context });
this._midSideMerge = this.output = new MidSideMerge({ context: this.context });
this.mid = new Compressor(Object.assign(options.mid, { context: this.context }));
this.side = new Compressor(Object.assign(options.side, { context: this.context }));
this._midSideSplit = this.input = new MidSideSplit({
context: this.context,
});
this._midSideMerge = this.output = new MidSideMerge({
context: this.context,
});
this.mid = new Compressor(
Object.assign(options.mid, { context: this.context })
);
this.side = new Compressor(
Object.assign(options.side, { context: this.context })
);
this._midSideSplit.mid.chain(this.mid, this._midSideMerge.mid);
this._midSideSplit.side.chain(this.side, this._midSideMerge.side);
@ -65,15 +84,15 @@ export class MidSideCompressor extends ToneAudioNode<MidSideCompressorOptions> {
threshold: -24,
release: 0.03,
attack: 0.02,
knee: 16
knee: 16,
},
side: {
ratio: 6,
threshold: -30,
release: 0.25,
attack: 0.03,
knee: 10
}
knee: 10,
},
});
}

View file

@ -4,11 +4,9 @@ import { PassAudio } from "../../../test/helper/PassAudio.js";
import { expect } from "chai";
describe("MultibandCompressor", () => {
BasicTests(MultibandCompressor);
context("Compression", () => {
it("passes the incoming signal through", () => {
return PassAudio((input) => {
const comp = new MultibandCompressor().toDestination();
@ -26,11 +24,17 @@ describe("MultibandCompressor", () => {
high: {
release: 0.5,
attack: 0.03,
knee: 20
}
knee: 20,
},
};
comp.set(values);
expect(comp.get()).to.have.keys(["low", "mid", "high", "lowFrequency", "highFrequency"]);
expect(comp.get()).to.have.keys([
"low",
"mid",
"high",
"lowFrequency",
"highFrequency",
]);
expect(comp.get().mid.ratio).be.closeTo(16, 0.01);
expect(comp.get().high.release).be.closeTo(0.5, 0.01);
comp.dispose();
@ -51,4 +55,3 @@ describe("MultibandCompressor", () => {
});
});
});

View file

@ -1,4 +1,8 @@
import { InputNode, ToneAudioNode, ToneAudioNodeOptions } from "../../core/context/ToneAudioNode.js";
import {
InputNode,
ToneAudioNode,
ToneAudioNodeOptions,
} from "../../core/context/ToneAudioNode.js";
import { Compressor, CompressorOptions } from "./Compressor.js";
import { optionsFromArguments } from "../../core/util/Defaults.js";
import { readOnly, RecursivePartial } from "../../core/util/Interface.js";
@ -16,7 +20,7 @@ export interface MultibandCompressorOptions extends ToneAudioNodeOptions {
}
/**
* A compressor with separate controls over low/mid/high dynamics.
* A compressor with separate controls over low/mid/high dynamics.
* @see {@link Compressor} and {@link MultibandSplit}
*
* @example
@ -30,7 +34,6 @@ export interface MultibandCompressorOptions extends ToneAudioNodeOptions {
* @category Component
*/
export class MultibandCompressor extends ToneAudioNode<MultibandCompressorOptions> {
readonly name: string = "MultibandCompressor";
readonly input: InputNode;
@ -68,20 +71,36 @@ export class MultibandCompressor extends ToneAudioNode<MultibandCompressorOption
constructor(options?: RecursivePartial<MultibandCompressorOptions>);
constructor() {
super(Object.assign(optionsFromArguments(MultibandCompressor.getDefaults(), arguments)));
const options = optionsFromArguments(MultibandCompressor.getDefaults(), arguments);
super(
Object.assign(
optionsFromArguments(
MultibandCompressor.getDefaults(),
arguments
)
)
);
const options = optionsFromArguments(
MultibandCompressor.getDefaults(),
arguments
);
this._splitter = this.input = new MultibandSplit({
context: this.context,
lowFrequency: options.lowFrequency,
highFrequency: options.highFrequency
highFrequency: options.highFrequency,
});
this.lowFrequency = this._splitter.lowFrequency;
this.highFrequency = this._splitter.highFrequency;
this.output = new Gain({ context: this.context });
this.low = new Compressor(Object.assign(options.low, { context: this.context }));
this.mid = new Compressor(Object.assign(options.mid, { context: this.context }));
this.high = new Compressor(Object.assign(options.high, { context: this.context }));
this.low = new Compressor(
Object.assign(options.low, { context: this.context })
);
this.mid = new Compressor(
Object.assign(options.mid, { context: this.context })
);
this.high = new Compressor(
Object.assign(options.high, { context: this.context })
);
// connect the compressor
this._splitter.low.chain(this.low, this.output);
@ -100,21 +119,21 @@ export class MultibandCompressor extends ToneAudioNode<MultibandCompressorOption
threshold: -30,
release: 0.25,
attack: 0.03,
knee: 10
knee: 10,
},
mid: {
ratio: 3,
threshold: -24,
release: 0.03,
attack: 0.02,
knee: 16
knee: 16,
},
high: {
ratio: 3,
threshold: -24,
release: 0.03,
attack: 0.02,
knee: 16
knee: 16,
},
});
}

View file

@ -9,11 +9,9 @@ import { AmplitudeEnvelope } from "./AmplitudeEnvelope.js";
import { Envelope } from "./Envelope.js";
describe("AmplitudeEnvelope", () => {
BasicTests(AmplitudeEnvelope);
context("Comparisons", () => {
it("matches a file", () => {
return CompareToFile(() => {
const ampEnv = new AmplitudeEnvelope({
@ -29,41 +27,47 @@ describe("AmplitudeEnvelope", () => {
});
it("matches a file with multiple retriggers", () => {
return CompareToFile(() => {
const ampEnv = new AmplitudeEnvelope({
attack: 0.1,
decay: 0.2,
release: 0.2,
sustain: 0.1,
}).toDestination();
const osc = new Oscillator().start(0).connect(ampEnv);
ampEnv.triggerAttack(0);
ampEnv.triggerAttack(0.3);
}, "ampEnvelope2.wav", 0.004);
return CompareToFile(
() => {
const ampEnv = new AmplitudeEnvelope({
attack: 0.1,
decay: 0.2,
release: 0.2,
sustain: 0.1,
}).toDestination();
const osc = new Oscillator().start(0).connect(ampEnv);
ampEnv.triggerAttack(0);
ampEnv.triggerAttack(0.3);
},
"ampEnvelope2.wav",
0.004
);
});
it("matches a file with ripple attack/release", () => {
return CompareToFile(() => {
const ampEnv = new AmplitudeEnvelope({
attack: 0.5,
attackCurve: "ripple",
decay: 0.2,
release: 0.3,
releaseCurve: "ripple",
sustain: 0.1,
}).toDestination();
const osc = new Oscillator().start(0).connect(ampEnv);
ampEnv.triggerAttack(0);
ampEnv.triggerRelease(0.7);
ampEnv.triggerAttack(1);
ampEnv.triggerRelease(1.6);
}, "ampEnvelope3.wav", 0.002);
return CompareToFile(
() => {
const ampEnv = new AmplitudeEnvelope({
attack: 0.5,
attackCurve: "ripple",
decay: 0.2,
release: 0.3,
releaseCurve: "ripple",
sustain: 0.1,
}).toDestination();
const osc = new Oscillator().start(0).connect(ampEnv);
ampEnv.triggerAttack(0);
ampEnv.triggerRelease(0.7);
ampEnv.triggerAttack(1);
ampEnv.triggerRelease(1.6);
},
"ampEnvelope3.wav",
0.002
);
});
});
context("Envelope", () => {
it("extends envelope", () => {
const ampEnv = new AmplitudeEnvelope();
expect(ampEnv).to.be.instanceOf(Envelope);

View file

@ -26,7 +26,6 @@ import { Envelope, EnvelopeOptions } from "./Envelope.js";
* @category Component
*/
export class AmplitudeEnvelope extends Envelope {
readonly name: string = "AmplitudeEnvelope";
private _gainNode: Gain = new Gain({
@ -45,10 +44,22 @@ export class AmplitudeEnvelope extends Envelope {
* @param release The amount of time after the release is triggered it takes to reach 0.
* Value must be greater than 0.
*/
constructor(attack?: Time, decay?: Time, sustain?: NormalRange, release?: Time);
constructor(options?: Partial<EnvelopeOptions>)
constructor(
attack?: Time,
decay?: Time,
sustain?: NormalRange,
release?: Time
);
constructor(options?: Partial<EnvelopeOptions>);
constructor() {
super(optionsFromArguments(AmplitudeEnvelope.getDefaults(), arguments, ["attack", "decay", "sustain", "release"]));
super(
optionsFromArguments(AmplitudeEnvelope.getDefaults(), arguments, [
"attack",
"decay",
"sustain",
"release",
])
);
this._sig.connect(this._gainNode.gain);
this.output = this._gainNode;
this.input = this._gainNode;

View file

@ -5,11 +5,9 @@ import { Offline } from "../../../test/helper/Offline.js";
import { Envelope, EnvelopeCurve } from "./Envelope.js";
describe("Envelope", () => {
BasicTests(Envelope);
context("Envelope", () => {
it("has an output connections", () => {
const env = new Envelope();
env.connect(connectTo());
@ -143,7 +141,7 @@ describe("Envelope", () => {
it("can set release to exponential or linear", () => {
return Offline(() => {
const env = new Envelope({
release: 0
release: 0,
});
env.toDestination();
env.triggerAttackRelease(0.4, 0);
@ -159,7 +157,7 @@ describe("Envelope", () => {
attack: 0.5,
decay: 0.0,
sustain: 1,
release: 0.5
release: 0.5,
}).toDestination();
env.triggerAttackRelease(0.5);
}, 0.7).then((buffer) => {
@ -180,17 +178,30 @@ describe("Envelope", () => {
sustain: 0.5,
};
return Offline(() => {
const env = new Envelope(e.attack, e.decay, e.sustain, e.release);
const env = new Envelope(
e.attack,
e.decay,
e.sustain,
e.release
);
env.attackCurve = "exponential";
env.toDestination();
env.triggerAttack(0);
}, 0.7).then((buffer) => {
buffer.forEachBetween((sample) => {
expect(sample).to.be.within(0, 1);
}, 0, e.attack);
buffer.forEachBetween((sample) => {
expect(sample).to.be.within(e.sustain - 0.001, 1);
}, e.attack, e.attack + e.decay);
buffer.forEachBetween(
(sample) => {
expect(sample).to.be.within(0, 1);
},
0,
e.attack
);
buffer.forEachBetween(
(sample) => {
expect(sample).to.be.within(e.sustain - 0.001, 1);
},
e.attack,
e.attack + e.decay
);
buffer.forEachBetween((sample) => {
expect(sample).to.be.closeTo(e.sustain, 0.01);
}, e.attack + e.decay);
@ -205,15 +216,24 @@ describe("Envelope", () => {
sustain: 0.5,
};
return Offline(() => {
const env = new Envelope(e.attack, e.decay, e.sustain, e.release);
const env = new Envelope(
e.attack,
e.decay,
e.sustain,
e.release
);
env.attackCurve = "exponential";
env.toDestination();
env.triggerAttack(0);
}, 0.7).then((buffer) => {
buffer.forEachBetween((sample, time) => {
const target = 1 - (time - 0.2) * 10;
expect(sample).to.be.closeTo(target, 0.01);
}, 0.2, 0.2);
buffer.forEachBetween(
(sample, time) => {
const target = 1 - (time - 0.2) * 10;
expect(sample).to.be.closeTo(target, 0.01);
},
0.2,
0.2
);
});
});
@ -225,7 +245,12 @@ describe("Envelope", () => {
sustain: 0,
};
return Offline(() => {
const env = new Envelope(e.attack, e.decay, e.sustain, e.release);
const env = new Envelope(
e.attack,
e.decay,
e.sustain,
e.release
);
env.decayCurve = "linear";
env.toDestination();
env.triggerAttack(0);
@ -248,7 +273,12 @@ describe("Envelope", () => {
sustain: 0,
};
return Offline(() => {
const env = new Envelope(e.attack, e.decay, e.sustain, e.release);
const env = new Envelope(
e.attack,
e.decay,
e.sustain,
e.release
);
env.decayCurve = "exponential";
env.toDestination();
env.triggerAttack(0);
@ -270,17 +300,30 @@ describe("Envelope", () => {
sustain: 0.1,
};
return Offline(() => {
const env = new Envelope(e.attack, e.decay, e.sustain, e.release);
const env = new Envelope(
e.attack,
e.decay,
e.sustain,
e.release
);
env.attackCurve = "exponential";
env.toDestination();
env.triggerAttack(0);
}, 0.2).then((buffer) => {
buffer.forEachBetween((sample) => {
expect(sample).to.be.within(0, 1);
}, 0, e.attack);
buffer.forEachBetween((sample) => {
expect(sample).to.be.within(e.sustain - 0.001, 1);
}, e.attack, e.attack + e.decay);
buffer.forEachBetween(
(sample) => {
expect(sample).to.be.within(0, 1);
},
0,
e.attack
);
buffer.forEachBetween(
(sample) => {
expect(sample).to.be.within(e.sustain - 0.001, 1);
},
e.attack,
e.attack + e.decay
);
buffer.forEachBetween((sample) => {
expect(sample).to.be.closeTo(e.sustain, 0.01);
}, e.attack + e.decay);
@ -308,16 +351,25 @@ describe("Envelope", () => {
};
const releaseTime = 0.2;
return Offline(() => {
const env = new Envelope(e.attack, e.decay, e.sustain, e.release);
const env = new Envelope(
e.attack,
e.decay,
e.sustain,
e.release
);
env.attackCurve = "exponential";
env.toDestination();
env.triggerAttackRelease(releaseTime);
}, 0.6).then((buffer) => {
const sustainStart = e.attack + e.decay;
const sustainEnd = sustainStart + releaseTime;
buffer.forEachBetween((sample) => {
expect(sample).to.be.below(e.sustain + 0.01);
}, sustainStart, sustainEnd);
buffer.forEachBetween(
(sample) => {
expect(sample).to.be.below(e.sustain + 0.01);
},
sustainStart,
sustainEnd
);
buffer.forEachBetween((sample) => {
expect(sample).to.be.closeTo(0, 0.01);
}, releaseTime + e.release);
@ -332,7 +384,7 @@ describe("Envelope", () => {
env.triggerAttack(0);
env.triggerRelease(0.4);
env.triggerAttack(0.4);
}, 0.6).then(buffer => {
}, 0.6).then((buffer) => {
expect(buffer.getValueAtTime(0.4)).be.closeTo(0.5, 0.01);
expect(buffer.getValueAtTime(0.40025)).be.closeTo(0.75, 0.01);
expect(buffer.getValueAtTime(0.4005)).be.closeTo(1, 0.01);
@ -349,14 +401,23 @@ describe("Envelope", () => {
const releaseTime = 0.2;
const attackTime = 0.1;
return Offline(() => {
const env = new Envelope(e.attack, e.decay, e.sustain, e.release);
const env = new Envelope(
e.attack,
e.decay,
e.sustain,
e.release
);
env.attackCurve = "exponential";
env.toDestination();
env.triggerAttack(attackTime);
env.triggerRelease(releaseTime);
}, 0.6).then((buffer) => {
expect(buffer.getValueAtTime(attackTime - 0.001)).to.equal(0);
expect(buffer.getValueAtTime(e.attack + e.decay + releaseTime + e.release)).to.be.below(0.01);
expect(
buffer.getValueAtTime(
e.attack + e.decay + releaseTime + e.release
)
).to.be.below(0.01);
});
});
@ -373,8 +434,12 @@ describe("Envelope", () => {
env.triggerAttack(attackTime);
}, 0.4).then((buffer) => {
buffer.forEach((sample, time) => {
expect(buffer.getValueAtTime(attackTime - 0.001)).to.equal(0);
expect(buffer.getValueAtTime(attackTime + e.attack + e.decay)).to.be.below(0.01);
expect(buffer.getValueAtTime(attackTime - 0.001)).to.equal(
0
);
expect(
buffer.getValueAtTime(attackTime + e.attack + e.decay)
).to.be.below(0.01);
});
});
});
@ -388,7 +453,12 @@ describe("Envelope", () => {
};
const releaseTime = 0.4;
return Offline(() => {
const env = new Envelope(e.attack, e.decay, e.sustain, e.release);
const env = new Envelope(
e.attack,
e.decay,
e.sustain,
e.release
);
env.toDestination();
env.triggerAttack(0);
env.triggerRelease(releaseTime);
@ -419,7 +489,12 @@ describe("Envelope", () => {
const releaseTime = 0.4;
const duration = 0.4;
return Offline(() => {
const env = new Envelope(e.attack, e.decay, e.sustain, e.release);
const env = new Envelope(
e.attack,
e.decay,
e.sustain,
e.release
);
env.toDestination();
env.triggerAttack(0);
env.triggerRelease(releaseTime);
@ -451,7 +526,12 @@ describe("Envelope", () => {
const duration = 0.4;
const velocity = 0.4;
return Offline(() => {
const env = new Envelope(e.attack, e.decay, e.sustain, e.release);
const env = new Envelope(
e.attack,
e.decay,
e.sustain,
e.release
);
env.toDestination();
env.triggerAttack(0, velocity);
env.triggerRelease(releaseTime);
@ -460,11 +540,17 @@ describe("Envelope", () => {
if (time < e.attack) {
expect(sample).to.be.within(0, velocity + 0.01);
} else if (time < e.attack + e.decay) {
expect(sample).to.be.within(e.sustain * velocity - 0.01, velocity + 0.01);
expect(sample).to.be.within(
e.sustain * velocity - 0.01,
velocity + 0.01
);
} else if (time < duration) {
expect(sample).to.be.closeTo(e.sustain * velocity, 0.1);
} else if (time < duration + e.release) {
expect(sample).to.be.within(0, e.sustain * velocity + 0.01);
expect(sample).to.be.within(
0,
e.sustain * velocity + 0.01
);
} else {
expect(sample).to.be.below(0.01);
}
@ -480,7 +566,12 @@ describe("Envelope", () => {
sustain: 0.0,
};
return Offline(() => {
const env = new Envelope(e.attack, e.decay, e.sustain, e.release);
const env = new Envelope(
e.attack,
e.decay,
e.sustain,
e.release
);
env.toDestination();
env.triggerAttack(0);
env.triggerAttack(0.5);
@ -632,13 +723,19 @@ describe("Envelope", () => {
});
context("Attack/Release Curves", () => {
const envelopeCurves: EnvelopeCurve[] = ["linear", "exponential", "bounce", "cosine", "ripple", "sine", "step"];
const envelopeCurves: EnvelopeCurve[] = [
"linear",
"exponential",
"bounce",
"cosine",
"ripple",
"sine",
"step",
];
it("can get set all of the types as the attackCurve", () => {
const env = new Envelope();
envelopeCurves.forEach(type => {
envelopeCurves.forEach((type) => {
env.attackCurve = type;
expect(env.attackCurve).to.equal(type);
});
@ -647,7 +744,7 @@ describe("Envelope", () => {
it("can get set all of the types as the releaseCurve", () => {
const env = new Envelope();
envelopeCurves.forEach(type => {
envelopeCurves.forEach((type) => {
env.releaseCurve = type;
expect(env.releaseCurve).to.equal(type);
});
@ -666,9 +763,13 @@ describe("Envelope", () => {
}).toDestination();
env.triggerAttackRelease(0.3, 0.1);
}, 0.8).then((buffer) => {
buffer.forEachBetween((sample) => {
expect(sample).to.be.above(0);
}, 0.101, 0.7);
buffer.forEachBetween(
(sample) => {
expect(sample).to.be.above(0);
},
0.101,
0.7
);
});
});
@ -684,9 +785,13 @@ describe("Envelope", () => {
}).toDestination();
env.triggerAttackRelease(0.3, 0.1);
}, 0.8).then((buffer) => {
buffer.forEachBetween((sample) => {
expect(sample).to.be.above(0);
}, 0.101, 0.7);
buffer.forEachBetween(
(sample) => {
expect(sample).to.be.above(0);
},
0.101,
0.7
);
});
});
@ -702,9 +807,13 @@ describe("Envelope", () => {
}).toDestination();
env.triggerAttackRelease(0.3, 0.1);
}, 0.8).then((buffer) => {
buffer.forEachBetween((sample) => {
expect(sample).to.be.above(0);
}, 0.101, 0.7);
buffer.forEachBetween(
(sample) => {
expect(sample).to.be.above(0);
},
0.101,
0.7
);
});
});
@ -720,9 +829,13 @@ describe("Envelope", () => {
}).toDestination();
env.triggerAttackRelease(0.3, 0.1);
}, 0.8).then((buffer) => {
buffer.forEachBetween((sample) => {
expect(sample).to.be.above(0);
}, 0.101, 0.7);
buffer.forEachBetween(
(sample) => {
expect(sample).to.be.above(0);
},
0.101,
0.7
);
});
});
@ -792,8 +905,8 @@ describe("Envelope", () => {
it("can render the envelope to a curve", async () => {
const env = new Envelope();
const curve = await env.asArray();
expect(curve.some(v => v > 0)).to.be.true;
curve.forEach(v => expect(v).to.be.within(0, 1));
expect(curve.some((v) => v > 0)).to.be.true;
curve.forEach((v) => expect(v).to.be.within(0, 1));
env.dispose();
});

View file

@ -1,5 +1,8 @@
import { InputNode, OutputNode } from "../../core/context/ToneAudioNode.js";
import { ToneAudioNode, ToneAudioNodeOptions } from "../../core/context/ToneAudioNode.js";
import {
ToneAudioNode,
ToneAudioNodeOptions,
} from "../../core/context/ToneAudioNode.js";
import { NormalRange, Time } from "../../core/type/Units.js";
import { optionsFromArguments } from "../../core/util/Defaults.js";
import { isArray, isObject, isString } from "../../core/util/TypeCheck.js";
@ -50,7 +53,6 @@ export interface EnvelopeOptions extends ToneAudioNodeOptions {
* @category Component
*/
export class Envelope extends ToneAudioNode<EnvelopeOptions> {
readonly name: string = "Envelope";
/**
@ -71,7 +73,7 @@ export class Envelope extends ToneAudioNode<EnvelopeOptions> {
* @max 2
*/
@timeRange(0)
attack: Time;
attack: Time;
/**
* After the attack portion of the envelope, the value will fall
@ -91,7 +93,7 @@ export class Envelope extends ToneAudioNode<EnvelopeOptions> {
* @max 2
*/
@timeRange(0)
decay: Time;
decay: Time;
/**
* The sustain value is the value
@ -110,7 +112,7 @@ export class Envelope extends ToneAudioNode<EnvelopeOptions> {
* ```
*/
@range(0, 1)
sustain: NormalRange;
sustain: NormalRange;
/**
* After triggerRelease is called, the envelope's
@ -131,7 +133,7 @@ export class Envelope extends ToneAudioNode<EnvelopeOptions> {
* @max 5
*/
@timeRange(0)
release: Time;
release: Time;
/**
* The automation curve type for the attack
@ -176,12 +178,27 @@ export class Envelope extends ToneAudioNode<EnvelopeOptions> {
* @param release The amount of time after the release is triggered it takes to reach 0.
* Value must be greater than 0.
*/
constructor(attack?: Time, decay?: Time, sustain?: NormalRange, release?: Time);
constructor(options?: Partial<EnvelopeOptions>)
constructor(
attack?: Time,
decay?: Time,
sustain?: NormalRange,
release?: Time
);
constructor(options?: Partial<EnvelopeOptions>);
constructor() {
super(optionsFromArguments(Envelope.getDefaults(), arguments, ["attack", "decay", "sustain", "release"]));
const options = optionsFromArguments(Envelope.getDefaults(), arguments, ["attack", "decay", "sustain", "release"]);
super(
optionsFromArguments(Envelope.getDefaults(), arguments, [
"attack",
"decay",
"sustain",
"release",
])
);
const options = optionsFromArguments(
Envelope.getDefaults(),
arguments,
["attack", "decay", "sustain", "release"]
);
this.attack = options.attack;
this.decay = options.decay;
@ -218,7 +235,10 @@ export class Envelope extends ToneAudioNode<EnvelopeOptions> {
* @param direction In/Out
* @return The curve name
*/
private _getCurve(curve: InternalEnvelopeCurve, direction: EnvelopeDirection): EnvelopeCurve {
private _getCurve(
curve: InternalEnvelopeCurve,
direction: EnvelopeDirection
): EnvelopeCurve {
if (isString(curve)) {
return curve;
} else {
@ -243,7 +263,7 @@ export class Envelope extends ToneAudioNode<EnvelopeOptions> {
private _setCurve(
name: "_attackCurve" | "_decayCurve" | "_releaseCurve",
direction: EnvelopeDirection,
curve: EnvelopeCurve,
curve: EnvelopeCurve
): void {
// check if it's a valid type
if (isString(curve) && Reflect.has(EnvelopeCurves, curve)) {
@ -385,9 +405,16 @@ export class Envelope extends ToneAudioNode<EnvelopeOptions> {
const decayStart = time + attack;
this.log("decay", decayStart);
if (this._decayCurve === "linear") {
this._sig.linearRampToValueAtTime(decayValue, decay + decayStart);
this._sig.linearRampToValueAtTime(
decayValue,
decay + decayStart
);
} else {
this._sig.exponentialApproachValueAtTime(decayValue, decayStart, decay);
this._sig.exponentialApproachValueAtTime(
decayValue,
decayStart,
decay
);
}
}
return this;
@ -418,9 +445,17 @@ export class Envelope extends ToneAudioNode<EnvelopeOptions> {
} else if (this._releaseCurve === "exponential") {
this._sig.targetRampTo(0, release, time);
} else {
assert(isArray(this._releaseCurve), "releaseCurve must be either 'linear', 'exponential' or an array");
assert(
isArray(this._releaseCurve),
"releaseCurve must be either 'linear', 'exponential' or an array"
);
this._sig.cancelAndHoldAtTime(time);
this._sig.setValueCurveAtTime(this._releaseCurve, time, release, currentValue);
this._sig.setValueCurveAtTime(
this._releaseCurve,
time,
release,
currentValue
);
}
}
return this;
@ -450,7 +485,11 @@ export class Envelope extends ToneAudioNode<EnvelopeOptions> {
* // trigger the release 0.5 seconds after the attack
* env.triggerAttackRelease(0.5);
*/
triggerAttackRelease(duration: Time, time?: Time, velocity: NormalRange = 1): this {
triggerAttackRelease(
duration: Time,
time?: Time,
velocity: NormalRange = 1
): this {
time = this.toSeconds(time);
this.triggerAttack(time, velocity);
this.triggerRelease(time + this.toSeconds(duration));
@ -480,21 +519,33 @@ export class Envelope extends ToneAudioNode<EnvelopeOptions> {
*/
async asArray(length = 1024): Promise<Float32Array> {
const duration = length / this.context.sampleRate;
const context = new OfflineContext(1, duration, this.context.sampleRate);
const context = new OfflineContext(
1,
duration,
this.context.sampleRate
);
// normalize the ADSR for the given duration with 20% sustain time
const attackPortion = this.toSeconds(this.attack) + this.toSeconds(this.decay);
const attackPortion =
this.toSeconds(this.attack) + this.toSeconds(this.decay);
const envelopeDuration = attackPortion + this.toSeconds(this.release);
const sustainTime = envelopeDuration * 0.1;
const totalDuration = envelopeDuration + sustainTime;
// @ts-ignore
const clone = new this.constructor(Object.assign(this.get(), {
attack: duration * this.toSeconds(this.attack) / totalDuration,
decay: duration * this.toSeconds(this.decay) / totalDuration,
release: duration * this.toSeconds(this.release) / totalDuration,
context
})) as Envelope;
const clone = new this.constructor(
Object.assign(this.get(), {
attack:
(duration * this.toSeconds(this.attack)) / totalDuration,
decay: (duration * this.toSeconds(this.decay)) / totalDuration,
release:
(duration * this.toSeconds(this.release)) / totalDuration,
context,
})
) as Envelope;
clone._sig.toDestination();
clone.triggerAttackRelease(duration * (attackPortion + sustainTime) / totalDuration, 0);
clone.triggerAttackRelease(
(duration * (attackPortion + sustainTime)) / totalDuration,
0
);
const buffer = await context.render();
return buffer.getChannelData(0);
}
@ -529,7 +580,6 @@ type EnvelopeCurveName = keyof EnvelopeCurveMap;
* Generate some complex envelope curves.
*/
const EnvelopeCurves: EnvelopeCurveMap = (() => {
const curveLen = 128;
let i: number;
@ -545,8 +595,9 @@ const EnvelopeCurves: EnvelopeCurveMap = (() => {
const rippleCurve: number[] = [];
const rippleCurveFreq = 6.4;
for (i = 0; i < curveLen - 1; i++) {
k = (i / (curveLen - 1));
const sineWave = Math.sin(k * (Math.PI * 2) * rippleCurveFreq - Math.PI / 2) + 1;
k = i / (curveLen - 1);
const sineWave =
Math.sin(k * (Math.PI * 2) * rippleCurveFreq - Math.PI / 2) + 1;
rippleCurve[i] = sineWave / 10 + k * 0.83;
}
rippleCurve[curveLen - 1] = 1;

View file

@ -6,11 +6,9 @@ import { Envelope } from "./Envelope.js";
import { expect } from "chai";
describe("FrequencyEnvelope", () => {
BasicTests(FrequencyEnvelope);
context("FrequencyEnvelope", () => {
it("has an output connections", () => {
const freqEnv = new FrequencyEnvelope();
freqEnv.connect(connectTo());
@ -30,7 +28,7 @@ describe("FrequencyEnvelope", () => {
attack: 0,
release: "4n",
baseFrequency: 20,
octaves: 4
octaves: 4,
};
freqEnv.set(values);
expect(freqEnv.get()).to.contain.keys(Object.keys(values));
@ -44,7 +42,7 @@ describe("FrequencyEnvelope", () => {
attack: 0,
decay: 0.5,
sustain: 1,
exponent: 3
exponent: 3,
});
expect(env0.attack).to.equal(0);
expect(env0.decay).to.equal(0.5);
@ -70,10 +68,14 @@ describe("FrequencyEnvelope", () => {
const e = {
attack: 0.01,
decay: 0.4,
sustain: 1
sustain: 1,
};
const buffer = await Offline(() => {
const freqEnv = new FrequencyEnvelope(e.attack, e.decay, e.sustain);
const freqEnv = new FrequencyEnvelope(
e.attack,
e.decay,
e.sustain
);
freqEnv.baseFrequency = 200;
freqEnv.octaves = 3;
freqEnv.attackCurve = "exponential";

View file

@ -13,7 +13,7 @@ export interface FrequencyEnvelopeOptions extends EnvelopeOptions {
/**
* FrequencyEnvelope is an {@link Envelope} which ramps between {@link baseFrequency}
* and {@link octaves}. It can also have an optional {@link exponent} to adjust the curve
* which it ramps.
* which it ramps.
* @example
* const oscillator = new Tone.Oscillator().toDestination().start();
* const freqEnv = new Tone.FrequencyEnvelope({
@ -26,7 +26,6 @@ export interface FrequencyEnvelopeOptions extends EnvelopeOptions {
* @category Component
*/
export class FrequencyEnvelope extends Envelope {
readonly name: string = "FrequencyEnvelope";
/**
@ -55,18 +54,34 @@ export class FrequencyEnvelope extends Envelope {
* @param sustain a percentage (0-1) of the full amplitude
* @param release the release time in seconds
*/
constructor(attack?: Time, decay?: Time, sustain?: NormalRange, release?: Time);
constructor(options?: Partial<FrequencyEnvelopeOptions>)
constructor(
attack?: Time,
decay?: Time,
sustain?: NormalRange,
release?: Time
);
constructor(options?: Partial<FrequencyEnvelopeOptions>);
constructor() {
super(optionsFromArguments(FrequencyEnvelope.getDefaults(), arguments, ["attack", "decay", "sustain", "release"]));
const options = optionsFromArguments(FrequencyEnvelope.getDefaults(), arguments, ["attack", "decay", "sustain", "release"]);
super(
optionsFromArguments(FrequencyEnvelope.getDefaults(), arguments, [
"attack",
"decay",
"sustain",
"release",
])
);
const options = optionsFromArguments(
FrequencyEnvelope.getDefaults(),
arguments,
["attack", "decay", "sustain", "release"]
);
this._octaves = options.octaves;
this._baseFrequency = this.toFrequency(options.baseFrequency);
this._exponent = this.input = new Pow({
context: this.context,
value: options.exponent
value: options.exponent,
});
this._scale = this.output = new Scale({
context: this.context,

View file

@ -6,11 +6,9 @@ import { Oscillator } from "../../source/oscillator/Oscillator.js";
import { BiquadFilter } from "./BiquadFilter.js";
describe("BiquadFilter", () => {
BasicTests(BiquadFilter);
context("BiquadFiltering", () => {
it("can be constructed with a arguments", () => {
const filter = new BiquadFilter(200, "highpass");
expect(filter.frequency.value).to.be.closeTo(200, 0.001);
@ -34,10 +32,15 @@ describe("BiquadFilter", () => {
Q: 2,
frequency: 440,
gain: -6,
type: "lowshelf" as const
type: "lowshelf" as const,
};
filter.set(values);
expect(filter.get()).to.include.keys(["type", "frequency", "Q", "gain"]);
expect(filter.get()).to.include.keys([
"type",
"frequency",
"Q",
"gain",
]);
expect(filter.type).to.equal(values.type);
expect(filter.frequency.value).to.equal(values.frequency);
expect(filter.Q.value).to.equal(values.Q);
@ -57,7 +60,7 @@ describe("BiquadFilter", () => {
});
it("passes the incoming signal through", () => {
return PassAudio(input => {
return PassAudio((input) => {
const filter = new BiquadFilter().toDestination();
input.connect(filter);
});
@ -65,8 +68,16 @@ describe("BiquadFilter", () => {
it("can set the basic filter types", () => {
const filter = new BiquadFilter();
const types: BiquadFilterType[] = ["lowpass", "highpass",
"bandpass", "lowshelf", "highshelf", "notch", "allpass", "peaking"];
const types: BiquadFilterType[] = [
"lowpass",
"highpass",
"bandpass",
"lowshelf",
"highshelf",
"notch",
"allpass",
"peaking",
];
for (const type of types) {
filter.type = type;
expect(filter.type).to.equal(type);
@ -89,6 +100,5 @@ describe("BiquadFilter", () => {
expect(buffer.getRmsAtTime(0.1)).to.be.within(0.37, 0.53);
});
});
});
});

View file

@ -1,4 +1,7 @@
import { ToneAudioNode, ToneAudioNodeOptions } from "../../core/context/ToneAudioNode.js";
import {
ToneAudioNode,
ToneAudioNodeOptions,
} from "../../core/context/ToneAudioNode.js";
import { Cents, Frequency, GainFactor } from "../../core/type/Units.js";
import { optionsFromArguments } from "../../core/util/Defaults.js";
import { Param } from "../../core/context/Param.js";
@ -13,8 +16,8 @@ export interface BiquadFilterOptions extends ToneAudioNodeOptions {
}
/**
* Thin wrapper around the native Web Audio [BiquadFilterNode](https://webaudio.github.io/web-audio-api/#biquadfilternode).
* BiquadFilter is similar to {@link Filter} but doesn't have the option to set the "rolloff" value.
* Thin wrapper around the native Web Audio [BiquadFilterNode](https://webaudio.github.io/web-audio-api/#biquadfilternode).
* BiquadFilter is similar to {@link Filter} but doesn't have the option to set the "rolloff" value.
* @category Component
*/
export class BiquadFilter extends ToneAudioNode<BiquadFilterOptions> {
@ -32,13 +35,13 @@ export class BiquadFilter extends ToneAudioNode<BiquadFilterOptions> {
* A detune value, in cents, for the frequency.
*/
readonly detune: Param<"cents">;
/**
* The Q factor of the filter.
* For lowpass and highpass filters the Q value is interpreted to be in dB.
* For lowpass and highpass filters the Q value is interpreted to be in dB.
* For these filters the nominal range is [𝑄𝑙𝑖𝑚,𝑄𝑙𝑖𝑚] where 𝑄𝑙𝑖𝑚 is the largest value for which 10𝑄/20 does not overflow. This is approximately 770.63678.
* For the bandpass, notch, allpass, and peaking filters, this value is a linear value.
* The value is related to the bandwidth of the filter and hence should be a positive value. The nominal range is
* For the bandpass, notch, allpass, and peaking filters, this value is a linear value.
* The value is related to the bandwidth of the filter and hence should be a positive value. The nominal range is
* [0,3.4028235𝑒38], the upper limit being the most-positive-single-float.
* This is not used for the lowshelf and highshelf filters.
*/
@ -58,8 +61,17 @@ export class BiquadFilter extends ToneAudioNode<BiquadFilterOptions> {
constructor(frequency?: Frequency, type?: BiquadFilterType);
constructor(options?: Partial<BiquadFilterOptions>);
constructor() {
super(optionsFromArguments(BiquadFilter.getDefaults(), arguments, ["frequency", "type"]));
const options = optionsFromArguments(BiquadFilter.getDefaults(), arguments, ["frequency", "type"]);
super(
optionsFromArguments(BiquadFilter.getDefaults(), arguments, [
"frequency",
"type",
])
);
const options = optionsFromArguments(
BiquadFilter.getDefaults(),
arguments,
["frequency", "type"]
);
this._filter = this.context.createBiquadFilter();
this.input = this.output = this._filter;
@ -70,21 +82,21 @@ export class BiquadFilter extends ToneAudioNode<BiquadFilterOptions> {
value: options.Q,
param: this._filter.Q,
});
this.frequency = new Param({
context: this.context,
units: "frequency",
value: options.frequency,
param: this._filter.frequency,
});
this.detune = new Param({
context: this.context,
units: "cents",
value: options.detune,
param: this._filter.detune,
});
this.gain = new Param({
context: this.context,
units: "decibels",
@ -114,8 +126,16 @@ export class BiquadFilter extends ToneAudioNode<BiquadFilterOptions> {
return this._filter.type;
}
set type(type) {
const types: BiquadFilterType[] = ["lowpass", "highpass", "bandpass",
"lowshelf", "highshelf", "notch", "allpass", "peaking"];
const types: BiquadFilterType[] = [
"lowpass",
"highpass",
"bandpass",
"lowshelf",
"highshelf",
"notch",
"allpass",
"peaking",
];
assert(types.indexOf(type) !== -1, `Invalid filter type: ${type}`);
this._filter.type = type;
}

View file

@ -4,7 +4,6 @@ import { ToneAudioBuffer } from "../../core/context/ToneAudioBuffer.js";
import { Convolver } from "./Convolver.js";
describe("Convolver", () => {
BasicTests(Convolver);
const ir = new ToneAudioBuffer();
@ -16,7 +15,6 @@ describe("Convolver", () => {
});
context("API", () => {
it("can pass in options in the constructor", () => {
const convolver = new Convolver({
normalize: false,
@ -66,7 +64,9 @@ describe("Convolver", () => {
it("can be constructed with a buffer", () => {
const convolver = new Convolver(ir);
expect((convolver.buffer as ToneAudioBuffer).get()).to.equal(ir.get());
expect((convolver.buffer as ToneAudioBuffer).get()).to.equal(
ir.get()
);
convolver.dispose();
});

View file

@ -1,4 +1,7 @@
import { ToneAudioNode, ToneAudioNodeOptions } from "../../core/context/ToneAudioNode.js";
import {
ToneAudioNode,
ToneAudioNodeOptions,
} from "../../core/context/ToneAudioNode.js";
import { ToneAudioBuffer } from "../../core/context/ToneAudioBuffer.js";
import { optionsFromArguments } from "../../core/util/Defaults.js";
import { Gain } from "../../core/context/Gain.js";
@ -22,7 +25,6 @@ export interface ConvolverOptions extends ToneAudioNodeOptions {
* @category Component
*/
export class Convolver extends ToneAudioNode<ConvolverOptions> {
readonly name: string = "Convolver";
/**
@ -42,14 +44,25 @@ export class Convolver extends ToneAudioNode<ConvolverOptions> {
* @param url The URL of the impulse response or the ToneAudioBuffer containing the impulse response.
* @param onload The callback to invoke when the url is loaded.
*/
constructor(url?: string | AudioBuffer | ToneAudioBuffer, onload?: () => void);
constructor(
url?: string | AudioBuffer | ToneAudioBuffer,
onload?: () => void
);
constructor(options?: Partial<ConvolverOptions>);
constructor() {
super(
optionsFromArguments(Convolver.getDefaults(), arguments, [
"url",
"onload",
])
);
const options = optionsFromArguments(
Convolver.getDefaults(),
arguments,
["url", "onload"]
);
super(optionsFromArguments(Convolver.getDefaults(), arguments, ["url", "onload"]));
const options = optionsFromArguments(Convolver.getDefaults(), arguments, ["url", "onload"]);
this._buffer = new ToneAudioBuffer(options.url, buffer => {
this._buffer = new ToneAudioBuffer(options.url, (buffer) => {
this.buffer = buffer;
options.onload();
});

View file

@ -5,11 +5,9 @@ import { PassAudio } from "../../../test/helper/PassAudio.js";
import { EQ3 } from "./EQ3.js";
describe("EQ3", () => {
BasicTests(EQ3);
context("EQing", () => {
it("can be constructed with an object", () => {
const eq3 = new EQ3({
high: -10,
@ -38,7 +36,7 @@ describe("EQ3", () => {
});
it("passes the incoming signal through", () => {
return PassAudio(input => {
return PassAudio((input) => {
const eq3 = new EQ3({
high: 12,
low: -20,

View file

@ -1,6 +1,9 @@
import { Gain } from "../../core/context/Gain.js";
import { Param } from "../../core/context/Param.js";
import { ToneAudioNode, ToneAudioNodeOptions } from "../../core/context/ToneAudioNode.js";
import {
ToneAudioNode,
ToneAudioNodeOptions,
} from "../../core/context/ToneAudioNode.js";
import { Decibels, Frequency } from "../../core/type/Units.js";
import { optionsFromArguments } from "../../core/util/Defaults.js";
import { readOnly, writable } from "../../core/util/Interface.js";
@ -16,11 +19,10 @@ interface EQ3Options extends ToneAudioNodeOptions {
}
/**
* EQ3 provides 3 equalizer bins: Low/Mid/High.
* EQ3 provides 3 equalizer bins: Low/Mid/High.
* @category Component
*/
export class EQ3 extends ToneAudioNode<EQ3Options> {
readonly name: string = "EQ3";
/**
@ -88,8 +90,18 @@ export class EQ3 extends ToneAudioNode<EQ3Options> {
constructor(lowLevel?: Decibels, midLevel?: Decibels, highLevel?: Decibels);
constructor(options: Partial<EQ3Options>);
constructor() {
super(optionsFromArguments(EQ3.getDefaults(), arguments, ["low", "mid", "high"]));
const options = optionsFromArguments(EQ3.getDefaults(), arguments, ["low", "mid", "high"]);
super(
optionsFromArguments(EQ3.getDefaults(), arguments, [
"low",
"mid",
"high",
])
);
const options = optionsFromArguments(EQ3.getDefaults(), arguments, [
"low",
"mid",
"high",
]);
this.input = this._multibandSplit = new MultibandSplit({
context: this.context,
@ -120,7 +132,7 @@ export class EQ3 extends ToneAudioNode<EQ3Options> {
this.high = this._highGain.gain;
this.Q = this._multibandSplit.Q;
this.lowFrequency = this._multibandSplit.lowFrequency;
this.highFrequency = this._multibandSplit.highFrequency;
this.highFrequency = this._multibandSplit.highFrequency;
// the frequency bands
this._multibandSplit.low.chain(this._lowGain, this.output);
@ -159,5 +171,4 @@ export class EQ3 extends ToneAudioNode<EQ3Options> {
this.Q.dispose();
return this;
}
}

View file

@ -7,11 +7,9 @@ import { Offline } from "../../../test/helper/Offline.js";
import { Signal } from "../../signal/index.js";
describe("FeedbackCombFilter", () => {
BasicTests(FeedbackCombFilter);
context("Comb Filtering", () => {
it("can be constructed with an object", () => {
const fbcf = new FeedbackCombFilter({
delayTime: 0.2,
@ -35,7 +33,7 @@ describe("FeedbackCombFilter", () => {
});
it("passes the incoming signal through", () => {
return PassAudio(input => {
return PassAudio((input) => {
const fbcf = new FeedbackCombFilter({
delayTime: 0.0,
resonance: 0,
@ -52,7 +50,7 @@ describe("FeedbackCombFilter", () => {
}).toDestination();
const sig = new Signal(0).connect(fbcf);
sig.setValueAtTime(1, 0);
}, 0.2).then(buffer => {
}, 0.2).then((buffer) => {
expect(buffer.getValueAtTime(0)).to.equal(0);
expect(buffer.getValueAtTime(0.999)).to.equal(0);
expect(buffer.getValueAtTime(0.101)).to.equal(1);
@ -69,7 +67,7 @@ describe("FeedbackCombFilter", () => {
const sig = new Signal(0).connect(fbcf);
sig.setValueAtTime(1, 0);
sig.setValueAtTime(0, 0.1);
}, 0.4).then(buffer => {
}, 0.4).then((buffer) => {
expect(buffer.getValueAtTime(0)).to.equal(0);
expect(buffer.getValueAtTime(0.101)).to.equal(1);
expect(buffer.getValueAtTime(0.201)).to.equal(0.5);
@ -93,4 +91,3 @@ describe("FeedbackCombFilter", () => {
};
});
});

View file

@ -1,6 +1,10 @@
import { Gain } from "../../core/context/Gain.js";
import { Param } from "../../core/context/Param.js";
import { connectSeries, ToneAudioNode, ToneAudioNodeOptions } from "../../core/context/ToneAudioNode.js";
import {
connectSeries,
ToneAudioNode,
ToneAudioNodeOptions,
} from "../../core/context/ToneAudioNode.js";
import { NormalRange, Time } from "../../core/type/Units.js";
import { optionsFromArguments } from "../../core/util/Defaults.js";
import { readOnly, RecursivePartial } from "../../core/util/Interface.js";
@ -15,14 +19,13 @@ export interface FeedbackCombFilterOptions extends ToneAudioNodeOptions {
/**
* Comb filters are basic building blocks for physical modeling. Read more
* about comb filters on [CCRMA's website](https://ccrma.stanford.edu/~jos/pasp/Feedback_Comb_Filters.html).
*
* This comb filter is implemented with the AudioWorkletNode which allows it to have feedback delays less than the
* Web Audio processing block of 128 samples. There is a polyfill for browsers that don't yet support the
* AudioWorkletNode, but it will add some latency and have slower performance than the AudioWorkletNode.
*
* This comb filter is implemented with the AudioWorkletNode which allows it to have feedback delays less than the
* Web Audio processing block of 128 samples. There is a polyfill for browsers that don't yet support the
* AudioWorkletNode, but it will add some latency and have slower performance than the AudioWorkletNode.
* @category Component
*/
export class FeedbackCombFilter extends ToneAudioWorklet<FeedbackCombFilterOptions> {
readonly name = "FeedbackCombFilter";
/**
@ -45,8 +48,17 @@ export class FeedbackCombFilter extends ToneAudioWorklet<FeedbackCombFilterOptio
constructor(delayTime?: Time, resonance?: NormalRange);
constructor(options?: RecursivePartial<FeedbackCombFilterOptions>);
constructor() {
super(optionsFromArguments(FeedbackCombFilter.getDefaults(), arguments, ["delayTime", "resonance"]));
const options = optionsFromArguments(FeedbackCombFilter.getDefaults(), arguments, ["delayTime", "resonance"]);
super(
optionsFromArguments(FeedbackCombFilter.getDefaults(), arguments, [
"delayTime",
"resonance",
])
);
const options = optionsFromArguments(
FeedbackCombFilter.getDefaults(),
arguments,
["delayTime", "resonance"]
);
this.input = new Gain({ context: this.context });
this.output = new Gain({ context: this.context });

View file

@ -4,7 +4,7 @@ import { registerProcessor } from "../../core/worklet/WorkletGlobalScope.js";
export const workletName = "feedback-comb-filter";
const feedbackCombFilter = /* javascript */`
const feedbackCombFilter = /* javascript */ `
class FeedbackCombFilterWorklet extends SingleIOProcessor {
constructor(options) {

View file

@ -6,11 +6,9 @@ import { Oscillator } from "../../source/oscillator/Oscillator.js";
import { Filter, FilterRollOff } from "./Filter.js";
describe("Filter", () => {
BasicTests(Filter);
context("Filtering", () => {
it("can be constructed with a arguments", () => {
const filter = new Filter(200, "highpass");
expect(filter.frequency.value).to.be.closeTo(200, 0.001);
@ -38,7 +36,13 @@ describe("Filter", () => {
type: "highpass" as BiquadFilterType,
};
filter.set(values);
expect(filter.get()).to.include.keys(["type", "frequency", "rolloff", "Q", "gain"]);
expect(filter.get()).to.include.keys([
"type",
"frequency",
"rolloff",
"Q",
"gain",
]);
expect(filter.type).to.equal(values.type);
expect(filter.frequency.value).to.equal(values.frequency);
expect(filter.rolloff).to.equal(values.rolloff);
@ -59,7 +63,7 @@ describe("Filter", () => {
});
it("passes the incoming signal through", () => {
return PassAudio(input => {
return PassAudio((input) => {
const filter = new Filter().toDestination();
input.connect(filter);
});
@ -85,8 +89,16 @@ describe("Filter", () => {
it("can set the basic filter types", () => {
const filter = new Filter();
const types: BiquadFilterType[] = ["lowpass", "highpass",
"bandpass", "lowshelf", "highshelf", "notch", "allpass", "peaking"];
const types: BiquadFilterType[] = [
"lowpass",
"highpass",
"bandpass",
"lowshelf",
"highshelf",
"notch",
"allpass",
"peaking",
];
for (const type of types) {
filter.type = type;
expect(filter.type).to.equal(type);
@ -109,6 +121,5 @@ describe("Filter", () => {
expect(buffer.getRmsAtTime(0.1)).to.be.within(0.37, 0.53);
});
});
});
});

View file

@ -1,5 +1,8 @@
import { Gain } from "../../core/context/Gain.js";
import { connectSeries, ToneAudioNode } from "../../core/context/ToneAudioNode.js";
import {
connectSeries,
ToneAudioNode,
} from "../../core/context/ToneAudioNode.js";
import { Frequency } from "../../core/type/Units.js";
import { optionsFromArguments } from "../../core/util/Defaults.js";
import { readOnly, writable } from "../../core/util/Interface.js";
@ -12,7 +15,7 @@ export type FilterRollOff = -12 | -24 | -48 | -96;
export type FilterOptions = BiquadFilterOptions & {
rolloff: FilterRollOff;
}
};
/**
* Tone.Filter is a filter which allows for all of the same native methods
@ -26,7 +29,6 @@ export type FilterOptions = BiquadFilterOptions & {
* @category Component
*/
export class Filter extends ToneAudioNode<FilterOptions> {
readonly name: string = "Filter";
readonly input = new Gain({ context: this.context });
@ -64,11 +66,25 @@ export class Filter extends ToneAudioNode<FilterOptions> {
* @param type The type of filter.
* @param rolloff The drop in decibels per octave after the cutoff frequency
*/
constructor(frequency?: Frequency, type?: BiquadFilterType, rolloff?: FilterRollOff);
constructor(
frequency?: Frequency,
type?: BiquadFilterType,
rolloff?: FilterRollOff
);
constructor(options?: Partial<FilterOptions>);
constructor() {
super(optionsFromArguments(Filter.getDefaults(), arguments, ["frequency", "type", "rolloff"]));
const options = optionsFromArguments(Filter.getDefaults(), arguments, ["frequency", "type", "rolloff"]);
super(
optionsFromArguments(Filter.getDefaults(), arguments, [
"frequency",
"type",
"rolloff",
])
);
const options = optionsFromArguments(Filter.getDefaults(), arguments, [
"frequency",
"type",
"rolloff",
]);
this._filters = [];
@ -117,11 +133,19 @@ export class Filter extends ToneAudioNode<FilterOptions> {
return this._type;
}
set type(type: BiquadFilterType) {
const types: BiquadFilterType[] = ["lowpass", "highpass", "bandpass",
"lowshelf", "highshelf", "notch", "allpass", "peaking"];
const types: BiquadFilterType[] = [
"lowpass",
"highpass",
"bandpass",
"lowshelf",
"highshelf",
"notch",
"allpass",
"peaking",
];
assert(types.indexOf(type) !== -1, `Invalid filter type: ${type}`);
this._type = type;
this._filters.forEach(filter => filter.type = type);
this._filters.forEach((filter) => (filter.type = type));
}
/**
@ -133,16 +157,21 @@ export class Filter extends ToneAudioNode<FilterOptions> {
return this._rolloff;
}
set rolloff(rolloff) {
const rolloffNum = isNumber(rolloff) ? rolloff : parseInt(rolloff, 10) as FilterRollOff;
const rolloffNum = isNumber(rolloff)
? rolloff
: (parseInt(rolloff, 10) as FilterRollOff);
const possibilities = [-12, -24, -48, -96];
let cascadingCount = possibilities.indexOf(rolloffNum);
// check the rolloff is valid
assert(cascadingCount !== -1, `rolloff can only be ${possibilities.join(", ")}`);
assert(
cascadingCount !== -1,
`rolloff can only be ${possibilities.join(", ")}`
);
cascadingCount += 1;
this._rolloff = rolloffNum;
this.input.disconnect();
this._filters.forEach(filter => filter.disconnect());
this._filters.forEach((filter) => filter.disconnect());
this._filters = new Array(cascadingCount);
for (let count = 0; count < cascadingCount; count++) {
@ -178,7 +207,7 @@ export class Filter extends ToneAudioNode<FilterOptions> {
const totalResponse = new Float32Array(len).map(() => 1);
this._filters.forEach(() => {
const response = filterClone.getFrequencyResponse(len);
response.forEach((val, i) => totalResponse[i] *= val);
response.forEach((val, i) => (totalResponse[i] *= val));
});
filterClone.dispose();
return totalResponse;
@ -189,7 +218,7 @@ export class Filter extends ToneAudioNode<FilterOptions> {
*/
dispose(): this {
super.dispose();
this._filters.forEach(filter => {
this._filters.forEach((filter) => {
filter.dispose();
});
writable(this, ["detune", "frequency", "gain", "Q"]);

View file

@ -6,16 +6,14 @@ import { Oscillator } from "../../source/oscillator/Oscillator.js";
import { expect } from "chai";
describe("LowpassCombFilter", () => {
BasicTests(LowpassCombFilter);
context("Comb Filtering", () => {
it("can be constructed with an object", () => {
const lpcf = new LowpassCombFilter({
delayTime: 0.2,
resonance: 0.3,
dampening: 2400
dampening: 2400,
});
expect(lpcf.delayTime.value).to.be.closeTo(0.2, 0.001);
expect(lpcf.resonance.value).to.be.closeTo(0.3, 0.001);
@ -28,7 +26,7 @@ describe("LowpassCombFilter", () => {
lpcf.set({
delayTime: 0.2,
resonance: 0.3,
dampening: 2000
dampening: 2000,
});
expect(lpcf.get().delayTime).to.be.closeTo(0.2, 0.001);
expect(lpcf.get().resonance).to.be.closeTo(0.3, 0.001);
@ -45,7 +43,11 @@ describe("LowpassCombFilter", () => {
it("produces a decay signal at high resonance", () => {
return Offline(() => {
const lpcf = new LowpassCombFilter(0.01, 0.9, 5000).toDestination();
const lpcf = new LowpassCombFilter(
0.01,
0.9,
5000
).toDestination();
const burst = new Oscillator(440).connect(lpcf);
burst.start(0);
burst.stop(0.1);

View file

@ -1,5 +1,10 @@
import { Param } from "../../core/context/Param.js";
import { InputNode, OutputNode, ToneAudioNode, ToneAudioNodeOptions } from "../../core/context/ToneAudioNode.js";
import {
InputNode,
OutputNode,
ToneAudioNode,
ToneAudioNodeOptions,
} from "../../core/context/ToneAudioNode.js";
import { Frequency, NormalRange, Time } from "../../core/type/Units.js";
import { optionsFromArguments } from "../../core/util/Defaults.js";
import { RecursivePartial } from "../../core/util/Interface.js";
@ -18,7 +23,6 @@ interface LowpassCombFilterOptions extends ToneAudioNodeOptions {
* @category Component
*/
export class LowpassCombFilter extends ToneAudioNode<LowpassCombFilterOptions> {
readonly name = "LowpassCombFilter";
/**
@ -49,11 +53,25 @@ export class LowpassCombFilter extends ToneAudioNode<LowpassCombFilterOptions> {
* @param resonance The resonance (feedback) of the comb filter
* @param dampening The cutoff of the lowpass filter dampens the signal as it is fedback.
*/
constructor(delayTime?: Time, resonance?: NormalRange, dampening?: Frequency);
constructor(
delayTime?: Time,
resonance?: NormalRange,
dampening?: Frequency
);
constructor(options?: RecursivePartial<LowpassCombFilterOptions>);
constructor() {
super(optionsFromArguments(LowpassCombFilter.getDefaults(), arguments, ["delayTime", "resonance", "dampening"]));
const options = optionsFromArguments(LowpassCombFilter.getDefaults(), arguments, ["delayTime", "resonance", "dampening"]);
super(
optionsFromArguments(LowpassCombFilter.getDefaults(), arguments, [
"delayTime",
"resonance",
"dampening",
])
);
const options = optionsFromArguments(
LowpassCombFilter.getDefaults(),
arguments,
["delayTime", "resonance", "dampening"]
);
this._combFilter = this.output = new FeedbackCombFilter({
context: this.context,
@ -80,7 +98,7 @@ export class LowpassCombFilter extends ToneAudioNode<LowpassCombFilterOptions> {
resonance: 0.5,
});
}
/**
* The dampening control of the feedback
*/

View file

@ -7,32 +7,44 @@ import { CompareToFile } from "../../../test/helper/CompareToFile.js";
import { atTime, Offline } from "../../../test/helper/Offline.js";
describe("OnePoleFilter", () => {
BasicTests(OnePoleFilter);
it("matches a file when set to lowpass", () => {
return CompareToFile(() => {
const filter = new OnePoleFilter(300, "lowpass").toDestination();
const osc = new Oscillator().connect(filter);
osc.type = "square";
osc.start(0).stop(0.1);
}, "onePoleLowpass.wav", 0.05);
return CompareToFile(
() => {
const filter = new OnePoleFilter(
300,
"lowpass"
).toDestination();
const osc = new Oscillator().connect(filter);
osc.type = "square";
osc.start(0).stop(0.1);
},
"onePoleLowpass.wav",
0.05
);
});
it("matches a file when set to highpass", () => {
return CompareToFile(() => {
const filter = new OnePoleFilter(700, "highpass").toDestination();
const osc = new Oscillator().connect(filter);
osc.type = "square";
osc.start(0).stop(0.1);
}, "onePoleHighpass.wav", 0.05);
return CompareToFile(
() => {
const filter = new OnePoleFilter(
700,
"highpass"
).toDestination();
const osc = new Oscillator().connect(filter);
osc.type = "square";
osc.start(0).stop(0.1);
},
"onePoleHighpass.wav",
0.05
);
});
context("Filtering", () => {
it("can set the frequency more than once", () => {
return Offline(() => {
const filter = new OnePoleFilter(200);
const filter = new OnePoleFilter(200);
filter.frequency = 300;
return atTime(0.1, () => {
filter.frequency = 400;
@ -43,7 +55,7 @@ describe("OnePoleFilter", () => {
it("can be constructed with an object", () => {
const filter = new OnePoleFilter({
frequency: 400,
type: "lowpass"
type: "lowpass",
});
expect(filter.frequency).to.be.closeTo(400, 0.1);
expect(filter.type).to.equal("lowpass");
@ -61,7 +73,7 @@ describe("OnePoleFilter", () => {
const filter = new OnePoleFilter();
filter.set({
frequency: 200,
type: "highpass"
type: "highpass",
});
expect(filter.get().type).to.equal("highpass");
expect(filter.get().frequency).to.be.closeTo(200, 0.1);
@ -77,14 +89,12 @@ describe("OnePoleFilter", () => {
});
context("Response Curve", () => {
it("can get the response curve", () => {
const filter = new OnePoleFilter();
const response = filter.getFrequencyResponse(128);
expect(response.length).to.equal(128);
response.forEach(v => expect(v).to.be.within(0, 1));
response.forEach((v) => expect(v).to.be.within(0, 1));
filter.dispose();
});
});
});

View file

@ -1,4 +1,7 @@
import { ToneAudioNode, ToneAudioNodeOptions } from "../../core/context/ToneAudioNode.js";
import {
ToneAudioNode,
ToneAudioNodeOptions,
} from "../../core/context/ToneAudioNode.js";
import { Frequency } from "../../core/type/Units.js";
import { optionsFromArguments } from "../../core/util/Defaults.js";
import { Gain } from "../../core/context/Gain.js";
@ -11,17 +14,16 @@ export interface OnePoleFilterOptions extends ToneAudioNodeOptions {
}
/**
* A one pole filter with 6db-per-octave rolloff. Either "highpass" or "lowpass".
* A one pole filter with 6db-per-octave rolloff. Either "highpass" or "lowpass".
* Note that changing the type or frequency may result in a discontinuity which
* can sound like a click or pop.
* References:
* * http://www.earlevel.com/main/2012/12/15/a-one-pole-filter/
* can sound like a click or pop.
* References:
* * http://www.earlevel.com/main/2012/12/15/a-one-pole-filter/
* * http://www.dspguide.com/ch19/2.htm
* * https://github.com/vitaliy-bobrov/js-rocks/blob/master/src/app/audio/effects/one-pole-filters.ts
* @category Component
*/
export class OnePoleFilter extends ToneAudioNode<OnePoleFilterOptions> {
readonly name: string = "OnePoleFilter";
/**
@ -47,11 +49,19 @@ export class OnePoleFilter extends ToneAudioNode<OnePoleFilterOptions> {
* @param type The filter type, either "lowpass" or "highpass"
*/
constructor(frequency?: Frequency, type?: OnePoleFilterType);
constructor(options?: Partial<OnePoleFilterOptions>)
constructor(options?: Partial<OnePoleFilterOptions>);
constructor() {
super(optionsFromArguments(OnePoleFilter.getDefaults(), arguments, ["frequency", "type"]));
const options = optionsFromArguments(OnePoleFilter.getDefaults(), arguments, ["frequency", "type"]);
super(
optionsFromArguments(OnePoleFilter.getDefaults(), arguments, [
"frequency",
"type",
])
);
const options = optionsFromArguments(
OnePoleFilter.getDefaults(),
arguments,
["frequency", "type"]
);
this._frequency = options.frequency;
this._type = options.type;
@ -63,7 +73,7 @@ export class OnePoleFilter extends ToneAudioNode<OnePoleFilterOptions> {
static getDefaults(): OnePoleFilterOptions {
return Object.assign(ToneAudioNode.getDefaults(), {
frequency: 880,
type: "lowpass" as OnePoleFilterType
type: "lowpass" as OnePoleFilterType,
});
}
@ -82,7 +92,7 @@ export class OnePoleFilter extends ToneAudioNode<OnePoleFilterOptions> {
const b1 = 1 / (t * this.context.sampleRate) - 1;
this._filter = this.context.createIIRFilter([1, -1], [1, b1]);
}
this.input.chain(this._filter, this.output);
if (oldFilter) {
// dispose it on the next block
@ -96,7 +106,7 @@ export class OnePoleFilter extends ToneAudioNode<OnePoleFilterOptions> {
}
/**
* The frequency value.
* The frequency value.
*/
get frequency(): Frequency {
return this._frequency;
@ -105,7 +115,7 @@ export class OnePoleFilter extends ToneAudioNode<OnePoleFilterOptions> {
this._frequency = fq;
this._createFilter();
}
/**
* The OnePole Filter type, either "highpass" or "lowpass"
*/

View file

@ -7,11 +7,9 @@ import { Subtract } from "../../signal/Subtract.js";
import { PhaseShiftAllpass } from "./PhaseShiftAllpass.js";
describe("PhaseShiftAllpass", () => {
BasicTests(PhaseShiftAllpass);
context("PhaseShiftAllpass", () => {
it("handles output connections", () => {
const phaseShifter = new PhaseShiftAllpass();
phaseShifter.connect(connectTo());
@ -27,45 +25,63 @@ describe("PhaseShiftAllpass", () => {
});
it("generates correct values with the phase shifted channel", () => {
return CompareToFile((context) => {
// create impulse with 5 samples offset
const constantNode = context.createConstantSource();
constantNode.start(0);
const oneSampleDelay = context.createIIRFilter([0.0, 1.0], [1.0, 0.0]);
const fiveSampleDelay = context.createIIRFilter([0.0, 0.0, 0.0, 0.0, 0.0, 1.0], [1.0, 0.0, 0.0, 0.0, 0.0, 0.0]);
const sub = new Subtract();
return CompareToFile(
(context) => {
// create impulse with 5 samples offset
const constantNode = context.createConstantSource();
constantNode.start(0);
const oneSampleDelay = context.createIIRFilter(
[0.0, 1.0],
[1.0, 0.0]
);
const fiveSampleDelay = context.createIIRFilter(
[0.0, 0.0, 0.0, 0.0, 0.0, 1.0],
[1.0, 0.0, 0.0, 0.0, 0.0, 0.0]
);
const sub = new Subtract();
connect(constantNode, oneSampleDelay);
connect(constantNode, sub);
connect(oneSampleDelay, sub.subtrahend);
connect(sub, fiveSampleDelay);
connect(constantNode, oneSampleDelay);
connect(constantNode, sub);
connect(oneSampleDelay, sub.subtrahend);
connect(sub, fiveSampleDelay);
const phaseShifter = new PhaseShiftAllpass();
connect(fiveSampleDelay, phaseShifter);
phaseShifter.toDestination();
}, "phaseShiftAllpass.wav", 0.001);
const phaseShifter = new PhaseShiftAllpass();
connect(fiveSampleDelay, phaseShifter);
phaseShifter.toDestination();
},
"phaseShiftAllpass.wav",
0.001
);
});
it("generates correct values with the offset90 channel", () => {
return CompareToFile((context) => {
// create impulse with 5 samples offset
const constantNode = context.createConstantSource();
constantNode.start(0);
const oneSampleDelay = context.createIIRFilter([0.0, 1.0], [1.0, 0.0]);
const fiveSampleDelay = context.createIIRFilter([0.0, 0.0, 0.0, 0.0, 0.0, 1.0], [1.0, 0.0, 0.0, 0.0, 0.0, 0.0]);
const sub = new Subtract();
return CompareToFile(
(context) => {
// create impulse with 5 samples offset
const constantNode = context.createConstantSource();
constantNode.start(0);
const oneSampleDelay = context.createIIRFilter(
[0.0, 1.0],
[1.0, 0.0]
);
const fiveSampleDelay = context.createIIRFilter(
[0.0, 0.0, 0.0, 0.0, 0.0, 1.0],
[1.0, 0.0, 0.0, 0.0, 0.0, 0.0]
);
const sub = new Subtract();
connect(constantNode, oneSampleDelay);
connect(constantNode, sub);
connect(oneSampleDelay, sub.subtrahend);
connect(sub, fiveSampleDelay);
connect(constantNode, oneSampleDelay);
connect(constantNode, sub);
connect(oneSampleDelay, sub.subtrahend);
connect(sub, fiveSampleDelay);
const phaseShifter = new PhaseShiftAllpass();
connect(fiveSampleDelay, phaseShifter);
phaseShifter.offset90.toDestination();
}, "phaseShiftAllpass1.wav", 0.001);
const phaseShifter = new PhaseShiftAllpass();
connect(fiveSampleDelay, phaseShifter);
phaseShifter.offset90.toDestination();
},
"phaseShiftAllpass1.wav",
0.001
);
});
});
});

View file

@ -1,5 +1,9 @@
import { Gain } from "../../core/context/Gain.js";
import { connectSeries, ToneAudioNode, ToneAudioNodeOptions } from "../../core/context/ToneAudioNode.js";
import {
connectSeries,
ToneAudioNode,
ToneAudioNodeOptions,
} from "../../core/context/ToneAudioNode.js";
/**
* PhaseShiftAllpass is an very efficient implementation of a Hilbert Transform
@ -10,7 +14,6 @@ import { connectSeries, ToneAudioNode, ToneAudioNodeOptions } from "../../core/c
* @category Component
*/
export class PhaseShiftAllpass extends ToneAudioNode<ToneAudioNodeOptions> {
readonly name: string = "PhaseShiftAllpass";
readonly input = new Gain({ context: this.context });
@ -41,18 +44,29 @@ export class PhaseShiftAllpass extends ToneAudioNode<ToneAudioNodeOptions> {
readonly offset90 = new Gain({ context: this.context });
constructor(options?: Partial<ToneAudioNodeOptions>) {
super(options);
const allpassBank1Values = [0.6923878, 0.9360654322959, 0.9882295226860, 0.9987488452737];
const allpassBank2Values = [0.4021921162426, 0.8561710882420, 0.9722909545651, 0.9952884791278];
const allpassBank1Values = [
0.6923878, 0.9360654322959, 0.988229522686, 0.9987488452737,
];
const allpassBank2Values = [
0.4021921162426, 0.856171088242, 0.9722909545651, 0.9952884791278,
];
this._bank0 = this._createAllPassFilterBank(allpassBank1Values);
this._bank1 = this._createAllPassFilterBank(allpassBank2Values);
this._oneSampleDelay = this.context.createIIRFilter([0.0, 1.0], [1.0, 0.0]);
this._oneSampleDelay = this.context.createIIRFilter(
[0.0, 1.0],
[1.0, 0.0]
);
// connect Allpass filter banks
connectSeries(this.input, ...this._bank0, this._oneSampleDelay, this.output);
connectSeries(
this.input,
...this._bank0,
this._oneSampleDelay,
this.output
);
connectSeries(this.input, ...this._bank1, this.offset90);
}
@ -60,9 +74,15 @@ export class PhaseShiftAllpass extends ToneAudioNode<ToneAudioNodeOptions> {
* Create all of the IIR filters from an array of values using the coefficient calculation.
*/
private _createAllPassFilterBank(bankValues: number[]): IIRFilterNode[] {
const nodes: IIRFilterNode[] = bankValues.map(value => {
const coefficients = [[value * value, 0, -1], [1, 0, -(value * value)]];
return this.context.createIIRFilter(coefficients[0], coefficients[1]);
const nodes: IIRFilterNode[] = bankValues.map((value) => {
const coefficients = [
[value * value, 0, -1],
[1, 0, -(value * value)],
];
return this.context.createIIRFilter(
coefficients[0],
coefficients[1]
);
});
return nodes;
@ -73,8 +93,8 @@ export class PhaseShiftAllpass extends ToneAudioNode<ToneAudioNodeOptions> {
this.input.dispose();
this.output.dispose();
this.offset90.dispose();
this._bank0.forEach(f => f.disconnect());
this._bank1.forEach(f => f.disconnect());
this._bank0.forEach((f) => f.disconnect());
this._bank1.forEach((f) => f.disconnect());
this._oneSampleDelay.disconnect();
return this;
}

View file

@ -1,10 +1,17 @@
import { version } from "../version.js";
import { AnyAudioContext, hasAudioContext, theWindow } from "./context/AudioContext.js";
import {
AnyAudioContext,
hasAudioContext,
theWindow,
} from "./context/AudioContext.js";
import { Context } from "./context/Context.js";
import { DummyContext } from "./context/DummyContext.js";
import { BaseContext } from "./context/BaseContext.js";
import { OfflineContext } from "./context/OfflineContext.js";
import { isAudioContext, isOfflineAudioContext } from "./util/AdvancedTypeCheck.js";
import {
isAudioContext,
isOfflineAudioContext,
} from "./util/AdvancedTypeCheck.js";
/**
* This dummy context is used to avoid throwing immediate errors when importing in Node.js
@ -34,7 +41,10 @@ export function getContext(): BaseContext {
* @param disposeOld Pass `true` if you don't need the old context to dispose it.
* @category Core
*/
export function setContext(context: BaseContext | AnyAudioContext, disposeOld = false): void {
export function setContext(
context: BaseContext | AnyAudioContext,
disposeOld = false
): void {
if (disposeOld) {
globalContext.dispose();
}

View file

@ -13,16 +13,15 @@ import { log } from "./util/Debug.js";
//-------------------------------------
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface BaseToneOptions { }
export interface BaseToneOptions {}
/**
* Tone is the base class of all other classes.
*
*
* @category Core
* @constructor
*/
export abstract class Tone {
/**
* The version number semver
*/
@ -64,7 +63,10 @@ export abstract class Tone {
protected log(...args: any[]): void {
// if the object is either set to debug = true
// or if there is a string on the Tone.global.with the class name
if (this.debug || (theWindow && this.toString() === theWindow.TONE_DEBUG_CLASS)) {
if (
this.debug ||
(theWindow && this.toString() === theWindow.TONE_DEBUG_CLASS)
) {
log(this, ...args);
}
}

View file

@ -6,11 +6,9 @@ import { noOp } from "../util/Interface.js";
import { Clock } from "./Clock.js";
describe("Clock", () => {
BasicTests(Clock);
context("Get/Set values", () => {
it("can get and set the frequency", () => {
const clock = new Clock(noOp, 2);
expect(clock.frequency.value).to.equal(2);
@ -20,7 +18,6 @@ describe("Clock", () => {
});
if (ONLINE_TESTING) {
it("invokes the callback when started", (done) => {
const clock = new Clock((time) => {
clock.dispose();
@ -38,7 +35,6 @@ describe("Clock", () => {
}).start();
expect(clock.frequency.value).to.equal(8);
});
}
it("can get and set it's values with the set/get", () => {
@ -53,7 +49,6 @@ describe("Clock", () => {
});
context("State", () => {
it("correctly returns the scheduled play state", () => {
return Offline(() => {
const clock = new Clock();
@ -93,7 +88,6 @@ describe("Clock", () => {
expect(clock.state).to.equal("stopped");
});
};
}, 0.5);
});
@ -137,15 +131,12 @@ describe("Clock", () => {
expect(clock.state).to.equal("started");
});
};
}, 0.5);
});
});
context("Scheduling", () => {
if (ONLINE_TESTING) {
it("passes a time to the callback", (done) => {
const clock = new Clock((time) => {
expect(time).to.be.a("number");
@ -192,7 +183,9 @@ describe("Clock", () => {
return Offline(() => {
new Clock((time) => {
invokations++;
}, 10).start(0).stop(0.45);
}, 10)
.start(0)
.stop(0.45);
}, 0.6).then(() => {
expect(invokations).to.equal(5);
});
@ -210,11 +203,9 @@ describe("Clock", () => {
expect(invokations).to.equal(4);
});
});
});
context("Seconds", () => {
it("can set the current seconds", () => {
return Offline(() => {
const clock = new Clock(noOp, 10);
@ -274,7 +265,6 @@ describe("Clock", () => {
});
context("Ticks", () => {
it("has 0 ticks when first created", () => {
const clock = new Clock();
expect(clock.ticks).to.equal(0);
@ -332,9 +322,11 @@ describe("Clock", () => {
});
it("starts incrementing where it left off after pause", () => {
return Offline(() => {
const clock = new Clock(noOp, 20).start(0).pause(0.1).start(0.2);
const clock = new Clock(noOp, 20)
.start(0)
.pause(0.1)
.start(0.2);
let pausedTicks = 0;
let tested = false;
@ -369,11 +361,9 @@ describe("Clock", () => {
clock.start(0, 4);
});
});
});
context("Events", () => {
it("triggers the start event on start", (done) => {
Offline(() => {
const clock = new Clock(noOp, 20);
@ -415,12 +405,14 @@ describe("Clock", () => {
it("triggers pause stop event", (done) => {
Offline(() => {
const clock = new Clock(noOp, 20);
clock.on("pause", (time) => {
expect(time).to.be.closeTo(0.1, 0.05);
}).on("stop", (time) => {
expect(time).to.be.closeTo(0.2, 0.05);
done();
});
clock
.on("pause", (time) => {
expect(time).to.be.closeTo(0.1, 0.05);
})
.on("stop", (time) => {
expect(time).to.be.closeTo(0.2, 0.05);
done();
});
clock.start().pause(0.1).stop(0.2);
}, 0.4);
});
@ -481,7 +473,6 @@ describe("Clock", () => {
});
context("[get/set]Ticks", () => {
it("always reports 0 if not started", () => {
return Offline(() => {
const clock = new Clock(noOp, 20);
@ -657,7 +648,5 @@ describe("Clock", () => {
clock.dispose();
});
});
});
});

View file

@ -1,4 +1,7 @@
import { ToneWithContext, ToneWithContextOptions } from "../context/ToneWithContext.js";
import {
ToneWithContext,
ToneWithContextOptions,
} from "../context/ToneWithContext.js";
import { Frequency, Hertz, Seconds, Ticks, Time } from "../type/Units.js";
import { optionsFromArguments } from "../util/Defaults.js";
import { Emitter } from "../util/Emitter.js";
@ -34,8 +37,9 @@ type ClockEvent = "start" | "stop" | "pause";
* @category Core
*/
export class Clock<TypeName extends "bpm" | "hertz" = "hertz">
extends ToneWithContext<ClockOptions> implements Emitter<ClockEvent> {
extends ToneWithContext<ClockOptions>
implements Emitter<ClockEvent>
{
readonly name: string = "Clock";
/**
@ -76,9 +80,16 @@ export class Clock<TypeName extends "bpm" | "hertz" = "hertz">
constructor(callback?: ClockCallback, frequency?: Frequency);
constructor(options: Partial<ClockOptions>);
constructor() {
super(optionsFromArguments(Clock.getDefaults(), arguments, ["callback", "frequency"]));
const options = optionsFromArguments(Clock.getDefaults(), arguments, ["callback", "frequency"]);
super(
optionsFromArguments(Clock.getDefaults(), arguments, [
"callback",
"frequency",
])
);
const options = optionsFromArguments(Clock.getDefaults(), arguments, [
"callback",
"frequency",
]);
this.callback = options.callback;
this._tickSource = new TickSource({
@ -241,14 +252,16 @@ export class Clock<TypeName extends "bpm" | "hertz" = "hertz">
nextTickTime(offset: Ticks, when: Time): Seconds {
const computedTime = this.toSeconds(when);
const currentTick = this.getTicksAtTime(computedTime);
return this._tickSource.getTimeOfTick(currentTick + offset, computedTime);
return this._tickSource.getTimeOfTick(
currentTick + offset,
computedTime
);
}
/**
* The scheduling loop.
*/
private _loop(): void {
const startTime = this._lastUpdate;
const endTime = this.now();
this._lastUpdate = endTime;
@ -256,7 +269,7 @@ export class Clock<TypeName extends "bpm" | "hertz" = "hertz">
if (startTime !== endTime) {
// the state change events
this._state.forEachBetween(startTime, endTime, e => {
this._state.forEachBetween(startTime, endTime, (e) => {
switch (e.state) {
case "started":
const offset = this._tickSource.getTicksAtTime(e.time);
@ -273,9 +286,13 @@ export class Clock<TypeName extends "bpm" | "hertz" = "hertz">
}
});
// the tick callbacks
this._tickSource.forEachTickBetween(startTime, endTime, (time, ticks) => {
this.callback(time, ticks);
});
this._tickSource.forEachTickBetween(
startTime,
endTime,
(time, ticks) => {
this.callback(time, ticks);
}
);
}
}
@ -310,7 +327,10 @@ export class Clock<TypeName extends "bpm" | "hertz" = "hertz">
on!: (event: ClockEvent, callback: (...args: any[]) => void) => this;
once!: (event: ClockEvent, callback: (...args: any[]) => void) => this;
off!: (event: ClockEvent, callback?: ((...args: any[]) => void) | undefined) => this;
off!: (
event: ClockEvent,
callback?: ((...args: any[]) => void) | undefined
) => this;
emit!: (event: any, ...args: any[]) => this;
}

View file

@ -3,11 +3,9 @@ import { BasicTests, testAudioContext } from "../../../test/helper/Basic.js";
import { TickParam } from "./TickParam.js";
describe("TickParam", () => {
// sanity checks
BasicTests(TickParam, {
context: testAudioContext,
param: testAudioContext.createOscillator().frequency,
});
});

View file

@ -8,7 +8,8 @@ type TickAutomationEvent = AutomationEvent & {
ticks: number;
};
interface TickParamOptions<TypeName extends UnitName> extends ParamOptions<TypeName> {
interface TickParamOptions<TypeName extends UnitName>
extends ParamOptions<TypeName> {
multiplier: number;
}
@ -17,8 +18,9 @@ interface TickParamOptions<TypeName extends UnitName> extends ParamOptions<TypeN
* but offers conversion to BPM values as well as ability to compute tick
* duration and elapsed ticks
*/
export class TickParam<TypeName extends "hertz" | "bpm"> extends Param<TypeName> {
export class TickParam<
TypeName extends "hertz" | "bpm",
> extends Param<TypeName> {
readonly name: string = "TickParam";
/**
@ -42,9 +44,14 @@ export class TickParam<TypeName extends "hertz" | "bpm"> extends Param<TypeName>
constructor(value?: number);
constructor(options: Partial<TickParamOptions<TypeName>>);
constructor() {
super(optionsFromArguments(TickParam.getDefaults(), arguments, ["value"]));
const options = optionsFromArguments(TickParam.getDefaults(), arguments, ["value"]);
super(
optionsFromArguments(TickParam.getDefaults(), arguments, ["value"])
);
const options = optionsFromArguments(
TickParam.getDefaults(),
arguments,
["value"]
);
// set the multiplier
this._multiplier = options.multiplier;
@ -69,7 +76,11 @@ export class TickParam<TypeName extends "hertz" | "bpm"> extends Param<TypeName>
});
}
setTargetAtTime(value: UnitMap[TypeName], time: Time, constant: number): this {
setTargetAtTime(
value: UnitMap[TypeName],
time: Time,
constant: number
): this {
// approximate it with multiple linear ramps
time = this.toSeconds(time);
this.setRampPoint(time);
@ -80,7 +91,13 @@ export class TickParam<TypeName extends "hertz" | "bpm"> extends Param<TypeName>
const segments = Math.round(Math.max(1 / constant, 1));
for (let i = 0; i <= segments; i++) {
const segTime = constant * i + time;
const rampVal = this._exponentialApproach(prevEvent.time, prevEvent.value, computedValue, constant, segTime);
const rampVal = this._exponentialApproach(
prevEvent.time,
prevEvent.value,
computedValue,
constant,
segTime
);
this.linearRampToValueAtTime(this._toType(rampVal), segTime);
}
return this;
@ -91,7 +108,10 @@ export class TickParam<TypeName extends "hertz" | "bpm"> extends Param<TypeName>
super.setValueAtTime(value, time);
const event = this._events.get(computedTime) as TickAutomationEvent;
const previousEvent = this._events.previousEvent(event);
const ticksUntilTime = this._getTicksUntilEvent(previousEvent, computedTime);
const ticksUntilTime = this._getTicksUntilEvent(
previousEvent,
computedTime
);
event.ticks = Math.max(ticksUntilTime, 0);
return this;
}
@ -101,7 +121,10 @@ export class TickParam<TypeName extends "hertz" | "bpm"> extends Param<TypeName>
super.linearRampToValueAtTime(value, time);
const event = this._events.get(computedTime) as TickAutomationEvent;
const previousEvent = this._events.previousEvent(event);
const ticksUntilTime = this._getTicksUntilEvent(previousEvent, computedTime);
const ticksUntilTime = this._getTicksUntilEvent(
previousEvent,
computedTime
);
event.ticks = Math.max(ticksUntilTime, 0);
return this;
}
@ -115,10 +138,16 @@ export class TickParam<TypeName extends "hertz" | "bpm"> extends Param<TypeName>
const prevEvent = this._events.get(time) as TickAutomationEvent;
// approx 10 segments per second
const segments = Math.round(Math.max((time - prevEvent.time) * 10, 1));
const segmentDur = ((time - prevEvent.time) / segments);
const segmentDur = (time - prevEvent.time) / segments;
for (let i = 0; i <= segments; i++) {
const segTime = segmentDur * i + prevEvent.time;
const rampVal = this._exponentialInterpolate(prevEvent.time, prevEvent.value, time, computedVal, segTime);
const rampVal = this._exponentialInterpolate(
prevEvent.time,
prevEvent.value,
time,
computedVal,
segTime
);
this.linearRampToValueAtTime(this._toType(rampVal), segTime);
}
return this;
@ -130,7 +159,10 @@ export class TickParam<TypeName extends "hertz" | "bpm"> extends Param<TypeName>
* @param event The time to get the tick count at
* @return The number of ticks which have elapsed at the time given any automations.
*/
private _getTicksUntilEvent(event: TickAutomationEvent | null, time: number): Ticks {
private _getTicksUntilEvent(
event: TickAutomationEvent | null,
time: number
): Ticks {
if (event === null) {
event = {
ticks: 0,
@ -146,7 +178,11 @@ export class TickParam<TypeName extends "hertz" | "bpm"> extends Param<TypeName>
let val1 = this._fromType(this.getValueAtTime(time));
// if it's right on the line, take the previous value
const onTheLineEvent = this._events.get(time);
if (onTheLineEvent && onTheLineEvent.time === time && onTheLineEvent.type === "setValueAtTime") {
if (
onTheLineEvent &&
onTheLineEvent.time === time &&
onTheLineEvent.type === "setValueAtTime"
) {
val1 = this._fromType(this.getValueAtTime(time - this.sampleTime));
}
return 0.5 * (time - event.time) * (val0 + val1) + event.ticks;
@ -185,13 +221,18 @@ export class TickParam<TypeName extends "hertz" | "bpm"> extends Param<TypeName>
const after = this._events.getAfter(tick, "ticks");
if (before && before.ticks === tick) {
return before.time;
} else if (before && after &&
} else if (
before &&
after &&
after.type === "linearRampToValueAtTime" &&
before.value !== after.value) {
before.value !== after.value
) {
const val0 = this._fromType(this.getValueAtTime(before.time));
const val1 = this._fromType(this.getValueAtTime(after.time));
const delta = (val1 - val0) / (after.time - before.time);
const k = Math.sqrt(Math.pow(val0, 2) - 2 * delta * (before.ticks - tick));
const k = Math.sqrt(
Math.pow(val0, 2) - 2 * delta * (before.ticks - tick)
);
const sol1 = (-val0 + k) / delta;
const sol2 = (-val0 - k) / delta;
return (sol1 > 0 ? sol1 : sol2) + before.time;
@ -249,7 +290,7 @@ export class TickParam<TypeName extends "hertz" | "bpm"> extends Param<TypeName>
*/
protected _toType(val: number): UnitMap[TypeName] {
if (this.units === "bpm" && this.multiplier) {
return (val / this.multiplier) * 60 as UnitMap[TypeName];
return ((val / this.multiplier) * 60) as UnitMap[TypeName];
} else {
return super._toType(val);
}

View file

@ -4,7 +4,6 @@ import { Offline } from "../../../test/helper/Offline.js";
import { TickSignal } from "./TickSignal.js";
describe("TickSignal", () => {
BasicTests(TickSignal);
it("can be created and disposed", () => {
@ -297,7 +296,7 @@ describe("TickSignal", () => {
return Offline((context) => {
const sched = new TickSignal(1).connect(context.destination);
sched.linearRampTo(3, 1, 0);
}, 1.01).then(buffer => {
}, 1.01).then((buffer) => {
expect(buffer.getValueAtTime(0)).to.be.closeTo(1, 0.01);
expect(buffer.getValueAtTime(0.5)).to.be.closeTo(2, 0.01);
expect(buffer.getValueAtTime(1)).to.be.closeTo(3, 0.01);
@ -311,7 +310,7 @@ describe("TickSignal", () => {
value: 120,
}).connect(context.destination);
sched.linearRampTo(60, 1, 0);
}, 1.01).then(buffer => {
}, 1.01).then((buffer) => {
expect(buffer.getValueAtTime(0)).to.be.closeTo(2, 0.01);
expect(buffer.getValueAtTime(0.5)).to.be.closeTo(1.5, 0.01);
expect(buffer.getValueAtTime(1)).to.be.closeTo(1, 0.01);
@ -326,7 +325,7 @@ describe("TickSignal", () => {
value: 60,
}).connect(context.destination);
sched.linearRampTo(120, 1, 0);
}, 1.01).then(buffer => {
}, 1.01).then((buffer) => {
expect(buffer.getValueAtTime(0)).to.be.closeTo(10, 0.01);
expect(buffer.getValueAtTime(0.5)).to.be.closeTo(15, 0.01);
expect(buffer.getValueAtTime(1)).to.be.closeTo(20, 0.01);
@ -334,13 +333,21 @@ describe("TickSignal", () => {
});
context("Ticks <-> Time", () => {
it("converts from time to ticks", () => {
return Offline(() => {
const tickSignal = new TickSignal(20);
expect(tickSignal.ticksToTime(20, 0).valueOf()).to.be.closeTo(1, 0.01);
expect(tickSignal.ticksToTime(10, 0).valueOf()).to.be.closeTo(0.5, 0.01);
expect(tickSignal.ticksToTime(10, 10).valueOf()).to.be.closeTo(0.5, 0.01);
expect(tickSignal.ticksToTime(20, 0).valueOf()).to.be.closeTo(
1,
0.01
);
expect(tickSignal.ticksToTime(10, 0).valueOf()).to.be.closeTo(
0.5,
0.01
);
expect(tickSignal.ticksToTime(10, 10).valueOf()).to.be.closeTo(
0.5,
0.01
);
tickSignal.dispose();
});
});
@ -349,10 +356,22 @@ describe("TickSignal", () => {
return Offline(() => {
const tickSignal = new TickSignal(1);
tickSignal.linearRampTo(2, 2, 1);
expect(tickSignal.ticksToTime(1, 0).valueOf()).to.be.closeTo(1, 0.01);
expect(tickSignal.ticksToTime(1, 1).valueOf()).to.be.closeTo(0.82, 0.01);
expect(tickSignal.ticksToTime(2, 0).valueOf()).to.be.closeTo(1.82, 0.01);
expect(tickSignal.ticksToTime(1, 3).valueOf()).to.be.closeTo(0.5, 0.01);
expect(tickSignal.ticksToTime(1, 0).valueOf()).to.be.closeTo(
1,
0.01
);
expect(tickSignal.ticksToTime(1, 1).valueOf()).to.be.closeTo(
0.82,
0.01
);
expect(tickSignal.ticksToTime(2, 0).valueOf()).to.be.closeTo(
1.82,
0.01
);
expect(tickSignal.ticksToTime(1, 3).valueOf()).to.be.closeTo(
0.5,
0.01
);
tickSignal.dispose();
});
});
@ -361,11 +380,26 @@ describe("TickSignal", () => {
return Offline(() => {
const tickSignal = new TickSignal(1);
tickSignal.setValueAtTime(2, 1);
expect(tickSignal.ticksToTime(1, 0).valueOf()).to.be.closeTo(1, 0.01);
expect(tickSignal.ticksToTime(1, 1).valueOf()).to.be.closeTo(0.5, 0.01);
expect(tickSignal.ticksToTime(2, 0).valueOf()).to.be.closeTo(1.5, 0.01);
expect(tickSignal.ticksToTime(1, 3).valueOf()).to.be.closeTo(0.5, 0.01);
expect(tickSignal.ticksToTime(1, 0.5).valueOf()).to.be.closeTo(0.75, 0.01);
expect(tickSignal.ticksToTime(1, 0).valueOf()).to.be.closeTo(
1,
0.01
);
expect(tickSignal.ticksToTime(1, 1).valueOf()).to.be.closeTo(
0.5,
0.01
);
expect(tickSignal.ticksToTime(2, 0).valueOf()).to.be.closeTo(
1.5,
0.01
);
expect(tickSignal.ticksToTime(1, 3).valueOf()).to.be.closeTo(
0.5,
0.01
);
expect(tickSignal.ticksToTime(1, 0.5).valueOf()).to.be.closeTo(
0.75,
0.01
);
tickSignal.dispose();
});
});
@ -374,10 +408,22 @@ describe("TickSignal", () => {
return Offline(() => {
const tickSignal = new TickSignal(1);
tickSignal.exponentialRampTo(2, 1, 1);
expect(tickSignal.ticksToTime(1, 0).valueOf()).to.be.closeTo(1, 0.01);
expect(tickSignal.ticksToTime(1, 1).valueOf()).to.be.closeTo(0.75, 0.01);
expect(tickSignal.ticksToTime(2, 0).valueOf()).to.be.closeTo(1.75, 0.01);
expect(tickSignal.ticksToTime(1, 3).valueOf()).to.be.closeTo(0.5, 0.01);
expect(tickSignal.ticksToTime(1, 0).valueOf()).to.be.closeTo(
1,
0.01
);
expect(tickSignal.ticksToTime(1, 1).valueOf()).to.be.closeTo(
0.75,
0.01
);
expect(tickSignal.ticksToTime(2, 0).valueOf()).to.be.closeTo(
1.75,
0.01
);
expect(tickSignal.ticksToTime(1, 3).valueOf()).to.be.closeTo(
0.5,
0.01
);
tickSignal.dispose();
});
});
@ -386,10 +432,22 @@ describe("TickSignal", () => {
return Offline(() => {
const tickSignal = new TickSignal(1);
tickSignal.setTargetAtTime(2, 1, 1);
expect(tickSignal.ticksToTime(1, 0).valueOf()).to.be.closeTo(1, 0.01);
expect(tickSignal.ticksToTime(1, 1).valueOf()).to.be.closeTo(0.79, 0.01);
expect(tickSignal.ticksToTime(2, 0).valueOf()).to.be.closeTo(1.79, 0.01);
expect(tickSignal.ticksToTime(1, 3).valueOf()).to.be.closeTo(0.61, 0.01);
expect(tickSignal.ticksToTime(1, 0).valueOf()).to.be.closeTo(
1,
0.01
);
expect(tickSignal.ticksToTime(1, 1).valueOf()).to.be.closeTo(
0.79,
0.01
);
expect(tickSignal.ticksToTime(2, 0).valueOf()).to.be.closeTo(
1.79,
0.01
);
expect(tickSignal.ticksToTime(1, 3).valueOf()).to.be.closeTo(
0.61,
0.01
);
tickSignal.dispose();
});
});
@ -397,9 +455,18 @@ describe("TickSignal", () => {
it("converts from ticks to time", () => {
return Offline(() => {
const tickSignal = new TickSignal(20);
expect(tickSignal.timeToTicks(1, 0).valueOf()).to.be.closeTo(20, 0.01);
expect(tickSignal.timeToTicks(0.5, 0).valueOf()).to.be.closeTo(10, 0.01);
expect(tickSignal.timeToTicks(0.5, 2).valueOf()).to.be.closeTo(10, 0.01);
expect(tickSignal.timeToTicks(1, 0).valueOf()).to.be.closeTo(
20,
0.01
);
expect(tickSignal.timeToTicks(0.5, 0).valueOf()).to.be.closeTo(
10,
0.01
);
expect(tickSignal.timeToTicks(0.5, 2).valueOf()).to.be.closeTo(
10,
0.01
);
tickSignal.dispose();
});
});
@ -408,10 +475,22 @@ describe("TickSignal", () => {
return Offline(() => {
const tickSignal = new TickSignal(1);
tickSignal.setValueAtTime(2, 1);
expect(tickSignal.timeToTicks(1, 0).valueOf()).to.be.closeTo(1, 0.01);
expect(tickSignal.timeToTicks(1, 1).valueOf()).to.be.closeTo(2, 0.01);
expect(tickSignal.timeToTicks(1, 2).valueOf()).to.be.closeTo(2, 0.01);
expect(tickSignal.timeToTicks(1, 0.5).valueOf()).to.be.closeTo(1.5, 0.01);
expect(tickSignal.timeToTicks(1, 0).valueOf()).to.be.closeTo(
1,
0.01
);
expect(tickSignal.timeToTicks(1, 1).valueOf()).to.be.closeTo(
2,
0.01
);
expect(tickSignal.timeToTicks(1, 2).valueOf()).to.be.closeTo(
2,
0.01
);
expect(tickSignal.timeToTicks(1, 0.5).valueOf()).to.be.closeTo(
1.5,
0.01
);
tickSignal.dispose();
});
});
@ -420,10 +499,22 @@ describe("TickSignal", () => {
return Offline(() => {
const tickSignal = new TickSignal(1);
tickSignal.linearRampTo(2, 1, 1);
expect(tickSignal.timeToTicks(1, 0).valueOf()).to.be.closeTo(1, 0.01);
expect(tickSignal.timeToTicks(1, 1).valueOf()).to.be.closeTo(1.5, 0.01);
expect(tickSignal.timeToTicks(1, 2).valueOf()).to.be.closeTo(2, 0.01);
expect(tickSignal.timeToTicks(1, 0.5).valueOf()).to.be.closeTo(1.12, 0.01);
expect(tickSignal.timeToTicks(1, 0).valueOf()).to.be.closeTo(
1,
0.01
);
expect(tickSignal.timeToTicks(1, 1).valueOf()).to.be.closeTo(
1.5,
0.01
);
expect(tickSignal.timeToTicks(1, 2).valueOf()).to.be.closeTo(
2,
0.01
);
expect(tickSignal.timeToTicks(1, 0.5).valueOf()).to.be.closeTo(
1.12,
0.01
);
tickSignal.dispose();
});
});
@ -432,10 +523,22 @@ describe("TickSignal", () => {
return Offline(() => {
const tickSignal = new TickSignal(1);
tickSignal.exponentialRampTo(2, 1, 1);
expect(tickSignal.timeToTicks(1, 0).valueOf()).to.be.closeTo(1, 0.01);
expect(tickSignal.timeToTicks(1, 1).valueOf()).to.be.closeTo(1.44, 0.01);
expect(tickSignal.timeToTicks(1, 2).valueOf()).to.be.closeTo(2, 0.01);
expect(tickSignal.timeToTicks(1, 0.5).valueOf()).to.be.closeTo(1.09, 0.01);
expect(tickSignal.timeToTicks(1, 0).valueOf()).to.be.closeTo(
1,
0.01
);
expect(tickSignal.timeToTicks(1, 1).valueOf()).to.be.closeTo(
1.44,
0.01
);
expect(tickSignal.timeToTicks(1, 2).valueOf()).to.be.closeTo(
2,
0.01
);
expect(tickSignal.timeToTicks(1, 0.5).valueOf()).to.be.closeTo(
1.09,
0.01
);
tickSignal.dispose();
});
});
@ -444,10 +547,22 @@ describe("TickSignal", () => {
return Offline(() => {
const tickSignal = new TickSignal(1);
tickSignal.setTargetAtTime(2, 1, 1);
expect(tickSignal.timeToTicks(1, 0).valueOf()).to.be.closeTo(1, 0.01);
expect(tickSignal.timeToTicks(1, 1).valueOf()).to.be.closeTo(1.31, 0.01);
expect(tickSignal.timeToTicks(1, 2).valueOf()).to.be.closeTo(1.63, 0.01);
expect(tickSignal.timeToTicks(1, 0.5).valueOf()).to.be.closeTo(1.07, 0.01);
expect(tickSignal.timeToTicks(1, 0).valueOf()).to.be.closeTo(
1,
0.01
);
expect(tickSignal.timeToTicks(1, 1).valueOf()).to.be.closeTo(
1.31,
0.01
);
expect(tickSignal.timeToTicks(1, 2).valueOf()).to.be.closeTo(
1.63,
0.01
);
expect(tickSignal.timeToTicks(1, 0.5).valueOf()).to.be.closeTo(
1.07,
0.01
);
tickSignal.dispose();
});
});

View file

@ -4,7 +4,8 @@ import { Seconds, Ticks, Time, UnitMap, UnitName } from "../type/Units.js";
import { optionsFromArguments } from "../util/Defaults.js";
import { TickParam } from "./TickParam.js";
interface TickSignalOptions<TypeName extends UnitName> extends SignalOptions<TypeName> {
interface TickSignalOptions<TypeName extends UnitName>
extends SignalOptions<TypeName> {
value: UnitMap[TypeName];
multiplier: number;
}
@ -18,8 +19,9 @@ interface TickSignalOptions<TypeName extends UnitName> extends SignalOptions<Typ
* for your [WAC paper](https://smartech.gatech.edu/bitstream/handle/1853/54588/WAC2016-49.pdf)
* describing integrating timing functions for tempo calculations.
*/
export class TickSignal<TypeName extends "hertz" | "bpm"> extends Signal<TypeName> {
export class TickSignal<
TypeName extends "hertz" | "bpm",
> extends Signal<TypeName> {
readonly name: string = "TickSignal";
/**
@ -34,9 +36,14 @@ export class TickSignal<TypeName extends "hertz" | "bpm"> extends Signal<TypeNam
constructor(value?: UnitMap[TypeName]);
constructor(options: Partial<TickSignalOptions<TypeName>>);
constructor() {
super(optionsFromArguments(TickSignal.getDefaults(), arguments, ["value"]));
const options = optionsFromArguments(TickSignal.getDefaults(), arguments, ["value"]);
super(
optionsFromArguments(TickSignal.getDefaults(), arguments, ["value"])
);
const options = optionsFromArguments(
TickSignal.getDefaults(),
arguments,
["value"]
);
this.input = this._param = new TickParam({
context: this.context,

View file

@ -4,11 +4,9 @@ import { Offline } from "../../../test/helper/Offline.js";
import { TickSource } from "./TickSource.js";
describe("TickSource", () => {
BasicTests(TickSource);
context("Constructor", () => {
it("can pass in the frequency", () => {
const source = new TickSource(2);
expect(source.frequency.value).to.equal(2);
@ -23,7 +21,6 @@ describe("TickSource", () => {
});
context("Ticks", () => {
it("ticks are 0 before started", () => {
const source = new TickSource();
expect(source.ticks).to.equal(0);
@ -42,7 +39,7 @@ describe("TickSource", () => {
return Offline(() => {
const source = new TickSource();
source.start(0);
return time => {
return (time) => {
expect(source.ticks).to.be.closeTo(time, 0.1);
};
}, 0.5);
@ -52,7 +49,7 @@ describe("TickSource", () => {
return Offline(() => {
const source = new TickSource(2);
source.start(0).stop(0.4);
return time => {
return (time) => {
if (time < 0.399) {
expect(source.ticks).to.be.closeTo(2 * time, 0.01);
} else if (time > 0.4) {
@ -67,7 +64,7 @@ describe("TickSource", () => {
const source = new TickSource(2);
source.start(0).pause(0.4);
let pausedTicks = -1;
return time => {
return (time) => {
if (time < 0.4) {
pausedTicks = source.ticks;
expect(source.ticks).to.be.closeTo(2 * time, 0.01);
@ -269,7 +266,6 @@ describe("TickSource", () => {
});
context("forEachTickBetween", () => {
it("invokes a callback function when started", () => {
const source = new TickSource(1);
source.start(0);
@ -296,7 +292,9 @@ describe("TickSource", () => {
const source = new TickSource(4);
source.start(0.2).pause(2).start(3.5).stop(5);
let iterations = 0;
const expectedTimes = [1.2, 1.45, 1.7, 1.95, 3.5, 3.75, 4, 4.25, 4.5, 4.75];
const expectedTimes = [
1.2, 1.45, 1.7, 1.95, 3.5, 3.75, 4, 4.25, 4.5, 4.75,
];
const expectedTicks = [4, 5, 6, 7, 7, 8, 9, 10, 11, 12];
source.forEachTickBetween(1, 7, (time, ticks) => {
expect(time).to.be.closeTo(expectedTimes[iterations], 0.001);
@ -401,7 +399,7 @@ describe("TickSource", () => {
source.start(0.5).stop(2).start(2.5).stop(4.1);
let iterations = 0;
const times = [0.5, 1.0, 1.5, 2.5, 3, 3.5, 4];
source.forEachTickBetween(0, 10, time => {
source.forEachTickBetween(0, 10, (time) => {
expect(times[iterations]).to.be.closeTo(time, 0.001);
iterations++;
});
@ -415,7 +413,7 @@ describe("TickSource", () => {
source.frequency.linearRampToValueAtTime(4, 1);
source.start(0.5);
let iterations = 0;
const times = [0.500, 0.833, 1.094, 1.344, 1.594, 1.844];
const times = [0.5, 0.833, 1.094, 1.344, 1.594, 1.844];
source.forEachTickBetween(0, 2, (time, ticks) => {
expect(time).to.be.closeTo(times[ticks], 0.001);
iterations++;
@ -471,7 +469,7 @@ describe("TickSource", () => {
source.start(0.5);
let iterations = 0;
let lastTime = 0.5;
source.forEachTickBetween(0.51, 2.01, time => {
source.forEachTickBetween(0.51, 2.01, (time) => {
expect(time - lastTime).to.be.closeTo(0.05, 0.001);
lastTime = time;
iterations++;
@ -545,15 +543,13 @@ describe("TickSource", () => {
});
source.dispose();
});
});
context("Seconds", () => {
it("get the elapsed time in seconds", () => {
return Offline(() => {
const source = new TickSource(1).start(0);
return time => {
return (time) => {
expect(source.seconds).to.be.closeTo(time, 0.01);
};
}, 2);
@ -563,14 +559,12 @@ describe("TickSource", () => {
const source = new TickSource(1);
expect(source.seconds).to.be.closeTo(0, 0.001);
source.dispose();
});
it("can set the seconds", () => {
const source = new TickSource(1);
expect(source.seconds).to.be.closeTo(0, 0.001);
source.dispose();
});
it("seconds pauses at last second count", () => {
@ -599,7 +593,7 @@ describe("TickSource", () => {
it("get the elapsed time in seconds when starting in the future", () => {
return Offline(() => {
const source = new TickSource(1).start(0.1);
return time => {
return (time) => {
if (time < 0.1) {
expect(source.seconds).to.be.closeTo(0, 0.001);
} else {
@ -610,7 +604,11 @@ describe("TickSource", () => {
});
it("handles multiple starts and stops", () => {
const source = new TickSource(1).start(0).stop(0.5).start(1).stop(1.5);
const source = new TickSource(1)
.start(0)
.stop(0.5)
.start(1)
.stop(1.5);
expect(source.getSecondsAtTime(0)).to.be.closeTo(0, 0.01);
expect(source.getSecondsAtTime(0.4)).to.be.closeTo(0.4, 0.01);
expect(source.getSecondsAtTime(0.5)).to.be.closeTo(0, 0.01);
@ -646,7 +644,6 @@ describe("TickSource", () => {
});
context("Frequency", () => {
it("can automate frequency with setValueAtTime", () => {
const source = new TickSource(1);
source.start(0).stop(0.3).start(0.4).stop(0.5).start(0.6);
@ -703,5 +700,4 @@ describe("TickSource", () => {
source.dispose();
});
});
});

View file

@ -1,8 +1,15 @@
import { ToneWithContext, ToneWithContextOptions } from "../context/ToneWithContext.js";
import {
ToneWithContext,
ToneWithContextOptions,
} from "../context/ToneWithContext.js";
import { Seconds, Ticks, Time } from "../type/Units.js";
import { optionsFromArguments } from "../util/Defaults.js";
import { readOnly } from "../util/Interface.js";
import { PlaybackState, StateTimeline, StateTimelineEvent } from "../util/StateTimeline.js";
import {
PlaybackState,
StateTimeline,
StateTimelineEvent,
} from "../util/StateTimeline.js";
import { Timeline, TimelineEvent } from "../util/Timeline.js";
import { isDefined } from "../util/TypeCheck.js";
import { TickSignal } from "./TickSignal.js";
@ -34,8 +41,9 @@ interface TickSourceSecondsAtTimeEvent extends TimelineEvent {
/**
* Uses [TickSignal](TickSignal) to track elapsed ticks with complex automation curves.
*/
export class TickSource<TypeName extends "bpm" | "hertz"> extends ToneWithContext<TickSourceOptions> {
export class TickSource<
TypeName extends "bpm" | "hertz",
> extends ToneWithContext<TickSourceOptions> {
readonly name: string = "TickSource";
/**
@ -56,12 +64,14 @@ export class TickSource<TypeName extends "bpm" | "hertz"> extends ToneWithContex
/**
* Memoized values of getTicksAtTime at events with state other than "started"
*/
private _ticksAtTime: Timeline<TickSourceTicksAtTimeEvent> = new Timeline<TickSourceTicksAtTimeEvent>();
private _ticksAtTime: Timeline<TickSourceTicksAtTimeEvent> =
new Timeline<TickSourceTicksAtTimeEvent>();
/**
* Memoized values of getSecondsAtTime at events with state other than "started"
*/
private _secondsAtTime: Timeline<TickSourceSecondsAtTimeEvent> = new Timeline<TickSourceSecondsAtTimeEvent>();
private _secondsAtTime: Timeline<TickSourceSecondsAtTimeEvent> =
new Timeline<TickSourceSecondsAtTimeEvent>();
/**
* @param frequency The initial frequency that the signal ticks at
@ -69,8 +79,16 @@ export class TickSource<TypeName extends "bpm" | "hertz"> extends ToneWithContex
constructor(frequency?: number);
constructor(options?: Partial<TickSourceOptions>);
constructor() {
super(optionsFromArguments(TickSource.getDefaults(), arguments, ["frequency"]));
const options = optionsFromArguments(TickSource.getDefaults(), arguments, ["frequency"]);
super(
optionsFromArguments(TickSource.getDefaults(), arguments, [
"frequency",
])
);
const options = optionsFromArguments(
TickSource.getDefaults(),
arguments,
["frequency"]
);
this.frequency = new TickSignal({
context: this.context,
@ -86,10 +104,13 @@ export class TickSource<TypeName extends "bpm" | "hertz"> extends ToneWithContex
}
static getDefaults(): TickSourceOptions {
return Object.assign({
frequency: 1,
units: "hertz" as const,
}, ToneWithContext.getDefaults());
return Object.assign(
{
frequency: 1,
units: "hertz" as const,
},
ToneWithContext.getDefaults()
);
}
/**
@ -174,38 +195,54 @@ export class TickSource<TypeName extends "bpm" | "hertz"> extends ToneWithContex
*/
getTicksAtTime(time?: Time): Ticks {
const computedTime = this.toSeconds(time);
const stopEvent = this._state.getLastState("stopped", computedTime) as StateTimelineEvent;
const stopEvent = this._state.getLastState(
"stopped",
computedTime
) as StateTimelineEvent;
// get previously memoized ticks if available
const memoizedEvent = this._ticksAtTime.get(computedTime);
// this event allows forEachBetween to iterate until the current time
const tmpEvent: StateTimelineEvent = { state: "paused", time: computedTime };
const tmpEvent: StateTimelineEvent = {
state: "paused",
time: computedTime,
};
this._state.add(tmpEvent);
// keep track of the previous offset event
let lastState = memoizedEvent ? memoizedEvent : stopEvent;
let elapsedTicks = memoizedEvent ? memoizedEvent.ticks : 0;
let eventToMemoize : TickSourceTicksAtTimeEvent | null = null;
let eventToMemoize: TickSourceTicksAtTimeEvent | null = null;
// iterate through all the events since the last stop
this._state.forEachBetween(lastState.time, computedTime + this.sampleTime, e => {
let periodStartTime = lastState.time;
// if there is an offset event in this period use that
const offsetEvent = this._tickOffset.get(e.time);
if (offsetEvent && offsetEvent.time >= lastState.time) {
elapsedTicks = offsetEvent.ticks;
periodStartTime = offsetEvent.time;
}
if (lastState.state === "started" && e.state !== "started") {
elapsedTicks += this.frequency.getTicksAtTime(e.time) - this.frequency.getTicksAtTime(periodStartTime);
// do not memoize the temporary event
if (e.time !== tmpEvent.time) {
eventToMemoize = { state: e.state, time: e.time, ticks: elapsedTicks };
this._state.forEachBetween(
lastState.time,
computedTime + this.sampleTime,
(e) => {
let periodStartTime = lastState.time;
// if there is an offset event in this period use that
const offsetEvent = this._tickOffset.get(e.time);
if (offsetEvent && offsetEvent.time >= lastState.time) {
elapsedTicks = offsetEvent.ticks;
periodStartTime = offsetEvent.time;
}
if (lastState.state === "started" && e.state !== "started") {
elapsedTicks +=
this.frequency.getTicksAtTime(e.time) -
this.frequency.getTicksAtTime(periodStartTime);
// do not memoize the temporary event
if (e.time !== tmpEvent.time) {
eventToMemoize = {
state: e.state,
time: e.time,
ticks: elapsedTicks,
};
}
}
lastState = e;
}
lastState = e;
});
);
// remove the temporary event
this._state.remove(tmpEvent);
@ -250,7 +287,10 @@ export class TickSource<TypeName extends "bpm" | "hertz"> extends ToneWithContex
*/
getSecondsAtTime(time: Time): Seconds {
time = this.toSeconds(time);
const stopEvent = this._state.getLastState("stopped", time) as StateTimelineEvent;
const stopEvent = this._state.getLastState(
"stopped",
time
) as StateTimelineEvent;
// this event allows forEachBetween to iterate until the current time
const tmpEvent: StateTimelineEvent = { state: "paused", time };
this._state.add(tmpEvent);
@ -261,26 +301,34 @@ export class TickSource<TypeName extends "bpm" | "hertz"> extends ToneWithContex
// keep track of the previous offset event
let lastState = memoizedEvent ? memoizedEvent : stopEvent;
let elapsedSeconds = memoizedEvent ? memoizedEvent.seconds : 0;
let eventToMemoize : TickSourceSecondsAtTimeEvent | null = null;
let eventToMemoize: TickSourceSecondsAtTimeEvent | null = null;
// iterate through all the events since the last stop
this._state.forEachBetween(lastState.time, time + this.sampleTime, e => {
let periodStartTime = lastState.time;
// if there is an offset event in this period use that
const offsetEvent = this._tickOffset.get(e.time);
if (offsetEvent && offsetEvent.time >= lastState.time) {
elapsedSeconds = offsetEvent.seconds;
periodStartTime = offsetEvent.time;
}
if (lastState.state === "started" && e.state !== "started") {
elapsedSeconds += e.time - periodStartTime;
// do not memoize the temporary event
if (e.time !== tmpEvent.time) {
eventToMemoize = { state: e.state, time: e.time, seconds: elapsedSeconds };
this._state.forEachBetween(
lastState.time,
time + this.sampleTime,
(e) => {
let periodStartTime = lastState.time;
// if there is an offset event in this period use that
const offsetEvent = this._tickOffset.get(e.time);
if (offsetEvent && offsetEvent.time >= lastState.time) {
elapsedSeconds = offsetEvent.seconds;
periodStartTime = offsetEvent.time;
}
if (lastState.state === "started" && e.state !== "started") {
elapsedSeconds += e.time - periodStartTime;
// do not memoize the temporary event
if (e.time !== tmpEvent.time) {
eventToMemoize = {
state: e.state,
time: e.time,
seconds: elapsedSeconds,
};
}
}
lastState = e;
}
lastState = e;
});
);
// remove the temporary event
this._state.remove(tmpEvent);
@ -333,7 +381,8 @@ export class TickSource<TypeName extends "bpm" | "hertz"> extends ToneWithContex
const offset = this._tickOffset.get(before) as TickSourceOffsetEvent;
const event = this._state.get(before) as StateTimelineEvent;
const startTime = Math.max(offset.time, event.time);
const absoluteTicks = this.frequency.getTicksAtTime(startTime) + tick - offset.ticks;
const absoluteTicks =
this.frequency.getTicksAtTime(startTime) + tick - offset.ticks;
return this.frequency.getTimeOfTick(absoluteTicks);
}
@ -344,12 +393,24 @@ export class TickSource<TypeName extends "bpm" | "hertz"> extends ToneWithContex
* @param endTime The end of the search range
* @param callback The callback to invoke with each tick
*/
forEachTickBetween(startTime: number, endTime: number, callback: (when: Seconds, ticks: Ticks) => void): this {
forEachTickBetween(
startTime: number,
endTime: number,
callback: (when: Seconds, ticks: Ticks) => void
): this {
// only iterate through the sections where it is "started"
let lastStateEvent = this._state.get(startTime);
this._state.forEachBetween(startTime, endTime, event => {
if (lastStateEvent && lastStateEvent.state === "started" && event.state !== "started") {
this.forEachTickBetween(Math.max(lastStateEvent.time, startTime), event.time - this.sampleTime, callback);
this._state.forEachBetween(startTime, endTime, (event) => {
if (
lastStateEvent &&
lastStateEvent.state === "started" &&
event.state !== "started"
) {
this.forEachTickBetween(
Math.max(lastStateEvent.time, startTime),
event.time - this.sampleTime,
callback
);
}
lastStateEvent = event;
});
@ -360,20 +421,30 @@ export class TickSource<TypeName extends "bpm" | "hertz"> extends ToneWithContex
const maxStartTime = Math.max(lastStateEvent.time, startTime);
// figure out the difference between the frequency ticks and the
const startTicks = this.frequency.getTicksAtTime(maxStartTime);
const ticksAtStart = this.frequency.getTicksAtTime(lastStateEvent.time);
const ticksAtStart = this.frequency.getTicksAtTime(
lastStateEvent.time
);
const diff = startTicks - ticksAtStart;
let offset = Math.ceil(diff) - diff;
// guard against floating point issues
offset = EQ(offset, 1) ? 0 : offset;
let nextTickTime = this.frequency.getTimeOfTick(startTicks + offset);
let nextTickTime = this.frequency.getTimeOfTick(
startTicks + offset
);
while (nextTickTime < endTime) {
try {
callback(nextTickTime, Math.round(this.getTicksAtTime(nextTickTime)));
callback(
nextTickTime,
Math.round(this.getTicksAtTime(nextTickTime))
);
} catch (e) {
error = e;
break;
}
nextTickTime += this.frequency.getDurationOfTicks(1, nextTickTime);
nextTickTime += this.frequency.getDurationOfTicks(
1,
nextTickTime
);
}
}

View file

@ -3,7 +3,6 @@ import { ONLINE_TESTING } from "../../../test/helper/Supports.js";
import { Ticker } from "./Ticker.js";
describe("Ticker", () => {
function empty(): void {
// do nothing
}
@ -32,55 +31,70 @@ describe("Ticker", () => {
});
if (ONLINE_TESTING) {
context("timeout", () => {
it("provides a callback when set to timeout", done => {
const ticker = new Ticker(() => {
ticker.dispose();
done();
}, "timeout", 0.01);
it("provides a callback when set to timeout", (done) => {
const ticker = new Ticker(
() => {
ticker.dispose();
done();
},
"timeout",
0.01
);
});
it("can adjust the interval when set to timeout", (done) => {
const ticker = new Ticker(() => {
ticker.dispose();
done();
}, "timeout", 0.01);
const ticker = new Ticker(
() => {
ticker.dispose();
done();
},
"timeout",
0.01
);
ticker.updateInterval = 0.1;
});
});
}
context("worker", () => {
it("provides a callback when set to worker", done => {
const ticker = new Ticker(() => {
ticker.dispose();
done();
}, "worker", 0.01);
it("provides a callback when set to worker", (done) => {
const ticker = new Ticker(
() => {
ticker.dispose();
done();
},
"worker",
0.01
);
});
it("falls back to timeout if the constructor throws an error", done => {
it("falls back to timeout if the constructor throws an error", (done) => {
const URL = window.URL;
// @ts-ignore
window.URL = null;
const ticker = new Ticker(() => {
expect(ticker.type).to.equal("timeout");
ticker.dispose();
window.URL = URL;
done();
}, "worker", 0.01);
const ticker = new Ticker(
() => {
expect(ticker.type).to.equal("timeout");
ticker.dispose();
window.URL = URL;
done();
},
"worker",
0.01
);
});
it("can adjust the interval when set to worker", (done) => {
const ticker = new Ticker(() => {
ticker.dispose();
done();
}, "worker", 0.01);
const ticker = new Ticker(
() => {
ticker.dispose();
done();
},
"worker",
0.01
);
ticker.updateInterval = 0.1;
});
});
});

View file

@ -7,7 +7,6 @@ export type TickerClockSource = "worker" | "timeout" | "offline";
* a Web Worker, or if that isn't supported, falls back to setTimeout.
*/
export class Ticker {
/**
* Either "worker" or "timeout" or "offline"
*/
@ -38,11 +37,18 @@ export class Ticker {
*/
private _worker!: Worker;
constructor(callback: () => void, type: TickerClockSource, updateInterval: Seconds, contextSampleRate?: number) {
constructor(
callback: () => void,
type: TickerClockSource,
updateInterval: Seconds,
contextSampleRate?: number
) {
this._callback = callback;
this._type = type;
this._minimumUpdateInterval = Math.max(128/(contextSampleRate || 44100), .001);
this._minimumUpdateInterval = Math.max(
128 / (contextSampleRate || 44100),
0.001
);
this.updateInterval = updateInterval;
// create the clock source for the first time
@ -53,9 +59,9 @@ export class Ticker {
* Generate a web worker
*/
private _createWorker(): void {
const blob = new Blob([
/* javascript */`
const blob = new Blob(
[
/* javascript */ `
// the initial timeout time
let timeoutTime = ${(this._updateInterval * 1000).toFixed(1)};
// onmessage callback
@ -70,8 +76,10 @@ export class Ticker {
}
// call tick initially
tick();
`
], { type: "text/javascript" });
`,
],
{ type: "text/javascript" }
);
const blobUrl = URL.createObjectURL(blob);
const worker = new Worker(blobUrl);

View file

@ -11,9 +11,7 @@ import { warns } from "../../../test/helper/Basic.js";
import { Synth } from "../../instrument/Synth.js";
describe("Transport", () => {
context("BPM and timeSignature", () => {
it("can get and set bpm", () => {
return Offline((context) => {
const transport = new TransportClass({ context });
@ -43,11 +41,9 @@ describe("Transport", () => {
expect(transport.timeSignature).to.equal(5);
});
});
});
context("looping", () => {
it("can get and set loop points", () => {
return Offline((context) => {
const transport = new TransportClass({ context });
@ -57,7 +53,10 @@ describe("Transport", () => {
expect(transport.loopEnd).to.be.closeTo(0.4, 0.01);
transport.setLoopPoints(0, "1m");
expect(transport.loopStart).to.be.closeTo(0, 0.01);
expect(transport.loopEnd).to.be.closeTo(transport.toSeconds("1m"), 0.01);
expect(transport.loopEnd).to.be.closeTo(
transport.toSeconds("1m"),
0.01
);
});
});
@ -90,42 +89,53 @@ describe("Transport", () => {
expect(looped).to.equal(true);
});
});
});
context("nextSubdivision", () => {
it("returns 0 if the transports not started", () => {
return Offline(context => {
return Offline((context) => {
const transport = new TransportClass({ context });
expect(transport.nextSubdivision()).to.equal(0);
});
});
it("can get the next subdivision of the transport", () => {
return Offline(context => {
return Offline((context) => {
const transport = new TransportClass({ context });
transport.start(0);
return time => {
return (time) => {
whenBetween(time, 0.05, 0.07, () => {
expect(transport.nextSubdivision(0.5)).to.be.closeTo(0.5, 0.01);
expect(transport.nextSubdivision(0.04)).to.be.closeTo(0.08, 0.01);
expect(transport.nextSubdivision(2)).to.be.closeTo(2, 0.01);
expect(transport.nextSubdivision(0.5)).to.be.closeTo(
0.5,
0.01
);
expect(transport.nextSubdivision(0.04)).to.be.closeTo(
0.08,
0.01
);
expect(transport.nextSubdivision(2)).to.be.closeTo(
2,
0.01
);
});
whenBetween(time, 0.09, 0.1, () => {
expect(transport.nextSubdivision(0.04)).to.be.closeTo(0.12, 0.01);
expect(transport.nextSubdivision("8n")).to.be.closeTo(0.25, 0.01);
expect(transport.nextSubdivision(0.04)).to.be.closeTo(
0.12,
0.01
);
expect(transport.nextSubdivision("8n")).to.be.closeTo(
0.25,
0.01
);
});
};
}, 0.1);
});
});
context("PPQ", () => {
it("can get and set pulses per quarter", () => {
return Offline(context => {
return Offline((context) => {
const transport = new TransportClass({ context });
transport.PPQ = 96;
expect(transport.PPQ).to.equal(96);
@ -133,10 +143,10 @@ describe("Transport", () => {
});
it("schedules a quarter note at the same time with a different PPQ", () => {
return Offline(context => {
return Offline((context) => {
const transport = new TransportClass({ context });
transport.PPQ = 1;
const id = transport.schedule(time => {
const id = transport.schedule((time) => {
expect(time).to.be.closeTo(transport.toSeconds("4n"), 0.1);
transport.clear(id);
}, "4n");
@ -145,27 +155,25 @@ describe("Transport", () => {
});
it("invokes the right number of ticks with a different PPQ", () => {
return Offline(context => {
return Offline((context) => {
const transport = new TransportClass({ context });
transport.bpm.value = 120;
const ppq = 20;
transport.PPQ = ppq;
transport.start();
return time => {
return (time) => {
if (time > 0.5) {
expect(transport.ticks).to.be.within(ppq, ppq * 1.2);
}
};
}, 0.55);
});
});
context("position", () => {
it("can jump to a specific tick number", () => {
return Offline(context => {
return Offline((context) => {
const transport = new TransportClass({ context });
transport.ticks = 200;
expect(transport.ticks).to.equal(200);
@ -181,7 +189,7 @@ describe("Transport", () => {
});
it("can get the current position in BarsBeatsSixteenths", () => {
return Offline(context => {
return Offline((context) => {
const transport = new TransportClass({ context });
expect(transport.position).to.equal("0:0:0");
transport.start(0);
@ -192,34 +200,40 @@ describe("Transport", () => {
});
it("can get the current position in seconds", () => {
return Offline(context => {
return Offline((context) => {
const transport = new TransportClass({ context });
expect(transport.seconds).to.equal(0);
transport.start(0.05);
return time => {
return (time) => {
if (time > 0.05) {
expect(transport.seconds).to.be.closeTo(time - 0.05, 0.01);
expect(transport.seconds).to.be.closeTo(
time - 0.05,
0.01
);
}
};
}, 0.1);
});
it("can get the current position in seconds during a bpm ramp", () => {
return Offline(context => {
return Offline((context) => {
const transport = new TransportClass({ context });
expect(transport.seconds).to.equal(0);
transport.start(0.05);
transport.bpm.linearRampTo(60, 0.5, 0.5);
return time => {
return (time) => {
if (time > 0.05) {
expect(transport.seconds).to.be.closeTo(time - 0.05, 0.01);
expect(transport.seconds).to.be.closeTo(
time - 0.05,
0.01
);
}
};
}, 0.7);
});
it("can set the current position in seconds", () => {
return Offline(context => {
return Offline((context) => {
const transport = new TransportClass({ context });
expect(transport.seconds).to.equal(0);
transport.seconds = 3;
@ -228,7 +242,7 @@ describe("Transport", () => {
});
it("can set the current position in BarsBeatsSixteenths", () => {
return Offline(context => {
return Offline((context) => {
const transport = new TransportClass({ context });
expect(transport.position).to.equal("0:0:0");
transport.position = "3:0";
@ -239,14 +253,15 @@ describe("Transport", () => {
});
it("can get the progress of the loop", () => {
return Offline(context => {
return Offline((context) => {
const transport = new TransportClass({ context });
transport.setLoopPoints(0, "1m").start();
transport.loop = true;
expect(transport.progress).to.be.equal(0);
transport.position = "2n";
expect(transport.progress).to.be.closeTo(0.5, 0.001);
transport.position = Time("2n").valueOf() + Time("4n").valueOf();
transport.position =
Time("2n").valueOf() + Time("4n").valueOf();
expect(transport.progress).to.be.closeTo(0.75, 0.001);
});
});
@ -260,28 +275,26 @@ describe("Transport", () => {
});
}, 0.2);
});
});
context("state", () => {
it("can start, pause, and restart", () => {
return Offline(({ transport }) => {
transport.start(0).pause(0.2).start(0.4);
const pulse = new Signal(0).toDestination();
transport.schedule(time => {
transport.schedule((time) => {
pulse.setValueAtTime(1, time);
pulse.setValueAtTime(0, time + 0.1);
}, 0);
transport.schedule(time => {
transport.schedule((time) => {
pulse.setValueAtTime(1, time);
pulse.setValueAtTime(0, time + 0.1);
}, 0.3);
return time => {
return (time) => {
whenBetween(time, 0, 0.2, () => {
expect(transport.state).to.equal("started");
});
@ -294,8 +307,7 @@ describe("Transport", () => {
expect(transport.state).to.equal("started");
});
};
}, 0.6).then(buffer => {
}, 0.6).then((buffer) => {
buffer.forEach((sample, time) => {
whenBetween(time, 0, 0.01, () => {
expect(sample).to.equal(1);
@ -309,32 +321,37 @@ describe("Transport", () => {
});
});
});
});
context("ticks", () => {
it("resets ticks on stop but not on pause", () => {
return Offline(context => {
return Offline((context) => {
const transport = new TransportClass({ context });
transport.start(0).pause(0.1).stop(0.2);
expect(transport.getTicksAtTime(0)).to.be.equal(Math.floor(transport.PPQ * 0));
expect(transport.getTicksAtTime(0.05)).to.be.equal(Math.floor(transport.PPQ * 0.1));
expect(transport.getTicksAtTime(0.1)).to.be.equal(Math.floor(transport.PPQ * 0.2));
expect(transport.getTicksAtTime(0.15)).to.be.equal(Math.floor(transport.PPQ * 0.2));
expect(transport.getTicksAtTime(0)).to.be.equal(
Math.floor(transport.PPQ * 0)
);
expect(transport.getTicksAtTime(0.05)).to.be.equal(
Math.floor(transport.PPQ * 0.1)
);
expect(transport.getTicksAtTime(0.1)).to.be.equal(
Math.floor(transport.PPQ * 0.2)
);
expect(transport.getTicksAtTime(0.15)).to.be.equal(
Math.floor(transport.PPQ * 0.2)
);
expect(transport.getTicksAtTime(0.2)).to.be.equal(0);
}, 0.3);
});
it("tracks ticks after start", () => {
return Offline(context => {
return Offline((context) => {
const transport = new TransportClass({ context });
transport.bpm.value = 120;
const ppq = transport.PPQ;
transport.start();
return time => {
return (time) => {
if (time > 0.5) {
expect(transport.ticks).to.at.least(ppq);
}
@ -343,11 +360,11 @@ describe("Transport", () => {
});
it("can start with a tick offset", () => {
return Offline(context => {
return Offline((context) => {
const transport = new TransportClass({ context });
transport.start(0, "200i");
return time => {
return (time) => {
if (time < 0.01) {
expect(transport.ticks).to.at.least(200);
}
@ -356,12 +373,12 @@ describe("Transport", () => {
});
it("can toggle the state of the transport", () => {
return Offline(context => {
return Offline((context) => {
const transport = new TransportClass({ context });
transport.toggle(0);
transport.toggle(0.2);
return time => {
return (time) => {
whenBetween(time, 0, 0.2, () => {
expect(transport.state).to.equal("started");
});
@ -374,14 +391,13 @@ describe("Transport", () => {
});
it("tracks ticks correctly with a different PPQ and BPM", () => {
return Offline(context => {
return Offline((context) => {
const transport = new TransportClass({ context });
transport.PPQ = 96;
transport.bpm.value = 90;
transport.start();
return time => {
return (time) => {
if (time > 0.5) {
expect(transport.ticks).to.at.least(72);
}
@ -394,7 +410,7 @@ describe("Transport", () => {
const times = [0, 1.5];
return Offline(({ transport }) => {
transport.PPQ = 1;
transport.schedule(time => {
transport.schedule((time) => {
expect(time).to.be.closeTo(times[invocations], 0.01);
invocations++;
}, 0);
@ -406,25 +422,23 @@ describe("Transport", () => {
expect(invocations).to.equal(2);
});
});
});
context("schedule", () => {
it("can schedule an event on the timeline", () => {
return Offline(context => {
return Offline((context) => {
const transport = new TransportClass({ context });
const eventID = transport.schedule(() => { }, 0);
const eventID = transport.schedule(() => {}, 0);
expect(eventID).to.be.a("number");
});
});
it("scheduled event gets invoked with the time of the event", () => {
let wasCalled = false;
return Offline(context => {
return Offline((context) => {
const transport = new TransportClass({ context });
const startTime = 0.1;
transport.schedule(time => {
transport.schedule((time) => {
expect(time).to.be.closeTo(startTime, 0.01);
wasCalled = true;
}, 0);
@ -436,11 +450,11 @@ describe("Transport", () => {
it("can schedule events with TransportTime", () => {
let wasCalled = false;
return Offline(context => {
return Offline((context) => {
const transport = new TransportClass({ context });
const startTime = 0.1;
const eighth = transport.toSeconds("8n");
transport.schedule(time => {
transport.schedule((time) => {
expect(time).to.be.closeTo(startTime + eighth, 0.01);
wasCalled = true;
}, TransportTime("8n"));
@ -451,7 +465,7 @@ describe("Transport", () => {
});
it("can clear a scheduled event", () => {
return Offline(context => {
return Offline((context) => {
const transport = new TransportClass({ context });
const eventID = transport.schedule(() => {
throw new Error("should not call this function");
@ -462,7 +476,7 @@ describe("Transport", () => {
});
it("can cancel the timeline of scheduled object", () => {
return Offline(context => {
return Offline((context) => {
const transport = new TransportClass({ context });
transport.schedule(() => {
throw new Error("should not call this");
@ -473,7 +487,7 @@ describe("Transport", () => {
});
it("can cancel the timeline of scheduleOnce object", () => {
return Offline(context => {
return Offline((context) => {
const transport = new TransportClass({ context });
transport.scheduleOnce(() => {
throw new Error("should not call this");
@ -485,10 +499,10 @@ describe("Transport", () => {
it("scheduled event anywhere along the timeline", () => {
let wasCalled = false;
return Offline(context => {
return Offline((context) => {
const transport = new TransportClass({ context });
const startTime = transport.now();
transport.schedule(time => {
transport.schedule((time) => {
expect(time).to.be.closeTo(startTime + 0.5, 0.001);
wasCalled = true;
}, 0.5);
@ -500,7 +514,7 @@ describe("Transport", () => {
it("can schedule multiple events and invoke them in the right order", () => {
let wasCalled = false;
return Offline(context => {
return Offline((context) => {
const transport = new TransportClass({ context });
let first = false;
transport.schedule(() => {
@ -518,7 +532,7 @@ describe("Transport", () => {
it("invokes the event again if the timeline is restarted", () => {
let iterations = 0;
return Offline(context => {
return Offline((context) => {
const transport = new TransportClass({ context });
transport.schedule(() => {
iterations++;
@ -531,11 +545,11 @@ describe("Transport", () => {
it("can add an event after the Transport is started", () => {
let wasCalled = false;
return Offline(context => {
return Offline((context) => {
const transport = new TransportClass({ context });
transport.start(0);
let wasScheduled = false;
return time => {
return (time) => {
if (time > 0.1 && !wasScheduled) {
wasScheduled = true;
transport.schedule(() => {
@ -557,16 +571,13 @@ describe("Transport", () => {
});
}, 0);
transport.start(0);
}, 0.3).then(() => {
});
}, 0.3).then(() => {});
});
});
context("scheduleRepeat", () => {
it("can schedule a repeated event", () => {
return Offline(context => {
return Offline((context) => {
const transport = new TransportClass({ context });
const eventID = transport.scheduleRepeat(noOp, 1);
expect(eventID).to.be.a("number");
@ -575,14 +586,18 @@ describe("Transport", () => {
it("scheduled event gets invoked with the time of the event", () => {
let invoked = false;
return Offline(context => {
return Offline((context) => {
const transport = new TransportClass({ context });
const startTime = 0.1;
const eventID = transport.scheduleRepeat(time => {
expect(time).to.be.closeTo(startTime, 0.01);
invoked = true;
transport.clear(eventID);
}, 1, 0);
const eventID = transport.scheduleRepeat(
(time) => {
expect(time).to.be.closeTo(startTime, 0.01);
invoked = true;
transport.clear(eventID);
},
1,
0
);
transport.start(startTime);
}, 0.3).then(() => {
expect(invoked).to.equal(true);
@ -590,11 +605,15 @@ describe("Transport", () => {
});
it("can cancel the timeline of scheduleRepeat", () => {
return Offline(context => {
return Offline((context) => {
const transport = new TransportClass({ context });
transport.scheduleRepeat(() => {
throw new Error("should not call this");
}, 0.01, 0);
transport.scheduleRepeat(
() => {
throw new Error("should not call this");
},
0.01,
0
);
transport.cancel(0);
transport.start(0);
});
@ -602,14 +621,18 @@ describe("Transport", () => {
it("can schedule events with TransportTime", () => {
let invoked = false;
return Offline(context => {
return Offline((context) => {
const transport = new TransportClass({ context });
const startTime = 0.1;
const eighth = transport.toSeconds("8n");
transport.scheduleRepeat(time => {
expect(time).to.be.closeTo(startTime + eighth, 0.01);
invoked = true;
}, "1n", TransportTime("8n"));
transport.scheduleRepeat(
(time) => {
expect(time).to.be.closeTo(startTime + eighth, 0.01);
invoked = true;
},
"1n",
TransportTime("8n")
);
transport.start(startTime);
}, 0.4).then(() => {
expect(invoked).to.equal(true);
@ -617,11 +640,15 @@ describe("Transport", () => {
});
it("can clear a scheduled event", () => {
return Offline(context => {
return Offline((context) => {
const transport = new TransportClass({ context });
const eventID = transport.scheduleRepeat(() => {
throw new Error("should not call this function");
}, 1, 0);
const eventID = transport.scheduleRepeat(
() => {
throw new Error("should not call this function");
},
1,
0
);
transport.clear(eventID);
transport.stop();
});
@ -629,14 +656,18 @@ describe("Transport", () => {
it("can be scheduled in the future", () => {
let invoked = false;
return Offline(context => {
return Offline((context) => {
const transport = new TransportClass({ context });
const startTime = 0.1;
const eventID = transport.scheduleRepeat(time => {
transport.clear(eventID);
expect(time).to.be.closeTo(startTime + 0.2, 0.01);
invoked = true;
}, 1, 0.2);
const eventID = transport.scheduleRepeat(
(time) => {
transport.clear(eventID);
expect(time).to.be.closeTo(startTime + 0.2, 0.01);
invoked = true;
},
1,
0.2
);
transport.start(startTime);
}, 0.5).then(() => {
expect(invoked).to.equal(true);
@ -645,11 +676,15 @@ describe("Transport", () => {
it("repeats a repeat event", () => {
let invocations = 0;
return Offline(context => {
return Offline((context) => {
const transport = new TransportClass({ context });
transport.scheduleRepeat(() => {
invocations++;
}, 0.1, 0);
transport.scheduleRepeat(
() => {
invocations++;
},
0.1,
0
);
transport.start();
}, 0.51).then(() => {
expect(invocations).to.equal(6);
@ -658,16 +693,20 @@ describe("Transport", () => {
it("repeats at the repeat interval", () => {
let wasCalled = false;
return Offline(context => {
return Offline((context) => {
const transport = new TransportClass({ context });
let repeatTime = -1;
transport.scheduleRepeat(time => {
if (repeatTime !== -1) {
expect(time - repeatTime).to.be.closeTo(0.1, 0.01);
}
repeatTime = time;
wasCalled = true;
}, 0.1, 0);
transport.scheduleRepeat(
(time) => {
if (repeatTime !== -1) {
expect(time - repeatTime).to.be.closeTo(0.1, 0.01);
}
repeatTime = time;
wasCalled = true;
},
0.1,
0
);
transport.start();
}, 0.5).then(() => {
expect(wasCalled).to.equal(true);
@ -677,17 +716,25 @@ describe("Transport", () => {
it("can schedule multiple events and invoke them in the right order", () => {
let first = false;
let second = false;
return Offline(context => {
return Offline((context) => {
const transport = new TransportClass({ context });
const firstID = transport.scheduleRepeat(() => {
first = true;
transport.clear(firstID);
}, 1, 0.1);
const secondID = transport.scheduleRepeat(() => {
transport.clear(secondID);
expect(first).to.equal(true);
second = true;
}, 1, 0.11);
const firstID = transport.scheduleRepeat(
() => {
first = true;
transport.clear(firstID);
},
1,
0.1
);
const secondID = transport.scheduleRepeat(
() => {
transport.clear(secondID);
expect(first).to.equal(true);
second = true;
},
1,
0.11
);
transport.start();
}, 0.3).then(() => {
expect(first);
@ -697,11 +744,16 @@ describe("Transport", () => {
it("repeats for the given interval", () => {
let repeatCount = 0;
return Offline(context => {
return Offline((context) => {
const transport = new TransportClass({ context });
transport.scheduleRepeat(time => {
repeatCount++;
}, 0.1, 0, 0.5);
transport.scheduleRepeat(
(time) => {
repeatCount++;
},
0.1,
0,
0.5
);
transport.start();
}, 0.61).then(() => {
expect(repeatCount).to.equal(5);
@ -710,18 +762,25 @@ describe("Transport", () => {
it("can add an event after the Transport is started", () => {
let invocations = 0;
return Offline(context => {
return Offline((context) => {
const transport = new TransportClass({ context });
transport.start(0);
let wasScheduled = false;
const times = [0.15, 0.3];
return time => {
return (time) => {
if (time > 0.1 && !wasScheduled) {
wasScheduled = true;
transport.scheduleRepeat(repeatedTime => {
expect(repeatedTime).to.be.closeTo(times[invocations], 0.01);
invocations++;
}, 0.15, 0.15);
transport.scheduleRepeat(
(repeatedTime) => {
expect(repeatedTime).to.be.closeTo(
times[invocations],
0.01
);
invocations++;
},
0.15,
0.15
);
}
};
}, 0.31).then(() => {
@ -731,31 +790,36 @@ describe("Transport", () => {
it("can add an event to the past after the Transport is started", () => {
let invocations = 0;
return Offline(context => {
return Offline((context) => {
const transport = new TransportClass({ context });
transport.start(0);
let wasScheduled = false;
const times = [0.15, 0.25];
return time => {
return (time) => {
if (time >= 0.12 && !wasScheduled) {
wasScheduled = true;
transport.scheduleRepeat(repeatedTime => {
expect(repeatedTime).to.be.closeTo(times[invocations], 0.01);
invocations++;
}, 0.1, 0.05);
transport.scheduleRepeat(
(repeatedTime) => {
expect(repeatedTime).to.be.closeTo(
times[invocations],
0.01
);
invocations++;
},
0.1,
0.05
);
}
};
}, 0.3).then(() => {
expect(invocations).to.equal(2);
});
});
});
context("scheduleOnce", () => {
it("can schedule a single event on the timeline", () => {
return Offline(context => {
return Offline((context) => {
const transport = new TransportClass({ context });
const eventID = transport.scheduleOnce(() => {}, 0);
expect(eventID).to.be.a("number");
@ -764,10 +828,10 @@ describe("Transport", () => {
it("scheduled event gets invoked with the time of the event", () => {
let invoked = false;
return Offline(context => {
return Offline((context) => {
const transport = new TransportClass({ context });
const startTime = 0.1;
const eventID = transport.scheduleOnce(time => {
const eventID = transport.scheduleOnce((time) => {
invoked = true;
transport.clear(eventID);
expect(time).to.be.closeTo(startTime, 0.01);
@ -780,11 +844,11 @@ describe("Transport", () => {
it("can schedule events with TransportTime", () => {
let invoked = false;
return Offline(context => {
return Offline((context) => {
const transport = new TransportClass({ context });
const startTime = 0.1;
const eighth = transport.toSeconds("8n");
transport.scheduleOnce(time => {
transport.scheduleOnce((time) => {
expect(time).to.be.closeTo(startTime + eighth, 0.01);
invoked = true;
}, TransportTime("8n"));
@ -795,7 +859,7 @@ describe("Transport", () => {
});
it("can clear a scheduled event", () => {
return Offline(context => {
return Offline((context) => {
const transport = new TransportClass({ context });
const eventID = transport.scheduleOnce(() => {
throw new Error("should not call this function");
@ -807,10 +871,10 @@ describe("Transport", () => {
it("can be scheduled in the future", () => {
let invoked = false;
return Offline(context => {
return Offline((context) => {
const transport = new TransportClass({ context });
const startTime = transport.now() + 0.1;
const eventID = transport.scheduleOnce(time => {
const eventID = transport.scheduleOnce((time) => {
transport.clear(eventID);
expect(time).to.be.closeTo(startTime + 0.3, 0.01);
invoked = true;
@ -823,7 +887,7 @@ describe("Transport", () => {
it("the event is removed after is is invoked", () => {
let iterations = 0;
return Offline(context => {
return Offline((context) => {
const transport = new TransportClass({ context });
transport.scheduleOnce(() => {
iterations++;
@ -833,14 +897,12 @@ describe("Transport", () => {
expect(iterations).to.be.lessThan(2);
});
});
});
context("events", () => {
it("invokes start/stop/pause events", () => {
let invocations = 0;
return Offline(context => {
return Offline((context) => {
const transport = new TransportClass({ context });
transport.on("start", () => {
invocations++;
@ -859,7 +921,7 @@ describe("Transport", () => {
it("invokes start event with correct offset", () => {
let wasCalled = false;
return Offline(context => {
return Offline((context) => {
const transport = new TransportClass({ context });
transport.on("start", (time, offset) => {
expect(time).to.be.closeTo(0.2, 0.01);
@ -874,10 +936,13 @@ describe("Transport", () => {
it("invokes the event just before the scheduled time", () => {
let invoked = false;
return Offline(context => {
return Offline((context) => {
const transport = new TransportClass({ context });
transport.on("start", (time, offset) => {
expect(time - transport.context.currentTime).to.be.closeTo(0, 0.01);
expect(time - transport.context.currentTime).to.be.closeTo(
0,
0.01
);
expect(offset).to.equal(0);
invoked = true;
});
@ -889,14 +954,14 @@ describe("Transport", () => {
it("passes in the time argument to the events", () => {
let invocations = 0;
return Offline(context => {
return Offline((context) => {
const transport = new TransportClass({ context });
const now = transport.now();
transport.on("start", time => {
transport.on("start", (time) => {
invocations++;
expect(time).to.be.closeTo(now + 0.1, 0.01);
});
transport.on("stop", time => {
transport.on("stop", (time) => {
invocations++;
expect(time).to.be.closeTo(now + 0.2, 0.01);
});
@ -908,13 +973,13 @@ describe("Transport", () => {
it("invokes the 'loop' method on loop", () => {
let loops = 0;
return Offline(context => {
return Offline((context) => {
const transport = new TransportClass({ context });
const sixteenth = transport.toSeconds("16n");
transport.setLoopPoints(0, sixteenth);
transport.loop = true;
let lastLoop = -1;
transport.on("loop", time => {
transport.on("loop", (time) => {
loops++;
if (lastLoop !== -1) {
expect(time - lastLoop).to.be.closeTo(sixteenth, 0.001);
@ -929,9 +994,8 @@ describe("Transport", () => {
});
context("swing", () => {
it("can get/set the swing subdivision", () => {
return Offline(context => {
return Offline((context) => {
const transport = new TransportClass({ context });
transport.swingSubdivision = "8n";
expect(transport.swingSubdivision).to.equal("8n");
@ -941,7 +1005,7 @@ describe("Transport", () => {
});
it("can get/set the swing amount", () => {
return Offline(context => {
return Offline((context) => {
const transport = new TransportClass({ context });
transport.swing = 0.5;
expect(transport.swing).to.equal(0.5);
@ -952,28 +1016,28 @@ describe("Transport", () => {
it("can swing", () => {
let invocations = 0;
return Offline(context => {
return Offline((context) => {
const transport = new TransportClass({ context });
transport.swing = 1;
transport.swingSubdivision = "8n";
const eightNote = transport.toSeconds("8n");
// downbeat, no swing
transport.schedule(time => {
transport.schedule((time) => {
invocations++;
expect(time).is.closeTo(0, 0.001);
}, 0);
// eighth note has swing
transport.schedule(time => {
transport.schedule((time) => {
invocations++;
expect(time).is.closeTo(eightNote * 5 / 3, 0.001);
expect(time).is.closeTo((eightNote * 5) / 3, 0.001);
}, "8n");
// sixteenth note is also swung
transport.schedule(time => {
transport.schedule((time) => {
invocations++;
expect(time).is.closeTo(eightNote, 0.05);
}, "16n");
// no swing on the quarter
transport.schedule(time => {
transport.schedule((time) => {
invocations++;
expect(time).is.closeTo(eightNote * 2, 0.001);
}, "4n");
@ -983,5 +1047,4 @@ describe("Transport", () => {
});
});
});
});

View file

@ -89,7 +89,8 @@ type TransportCallback = (time: Seconds) => void;
*/
export class TransportClass
extends ToneWithContext<TransportOptions>
implements Emitter<TransportEventNames> {
implements Emitter<TransportEventNames>
{
readonly name: string = "Transport";
//-------------------------------------
@ -366,7 +367,10 @@ export class TransportClass
* timeline it was added to.
* @returns the event id which was just added
*/
private _addEvent(event: TransportEvent, timeline: Timeline<TransportEvent>): number {
private _addEvent(
event: TransportEvent,
timeline: Timeline<TransportEvent>
): number {
this._scheduledEvents[event.id.toString()] = {
event,
timeline,
@ -625,7 +629,10 @@ export class TransportClass
if (this.state === "started") {
const ticks = this._clock.getTicksAtTime(now);
// schedule to start on the next tick, #573
const remainingTick = this._clock.frequency.getDurationOfTicks(Math.ceil(ticks) - ticks, now);
const remainingTick = this._clock.frequency.getDurationOfTicks(
Math.ceil(ticks) - ticks,
now
);
const time = now + remainingTick;
this.emit("stop", time);
this._clock.setTicksAtTime(t, time);
@ -710,9 +717,9 @@ export class TransportClass
*/
syncSignal(signal: Signal<any>, ratio?: number): this {
const now = this.now();
let source : TickParam<"bpm"> | ToneAudioNode<any> = this.bpm;
let source: TickParam<"bpm"> | ToneAudioNode<any> = this.bpm;
let sourceValue = 1 / (60 / source.getValueAtTime(now) / this.PPQ);
let nodes : ToneAudioNode<any>[] = [];
let nodes: ToneAudioNode<any>[] = [];
// If the signal is in the time domain, sync it to the reciprocal of
// the tempo instead of the tempo.
if (signal.units === "time") {

View file

@ -4,7 +4,6 @@ import { TransportClass } from "./Transport.js";
import { TransportEvent } from "./TransportEvent.js";
describe("TransportEvent", () => {
it("can be created and disposed", () => {
return Offline((context) => {
const transport = new TransportClass({ context });

View file

@ -15,7 +15,6 @@ export interface TransportEventOptions {
* handled from within Tone.Transport.
*/
export class TransportEvent {
/**
* Reference to the Transport that created it
*/
@ -43,7 +42,7 @@ export class TransportEvent {
/**
* The remaining value between the passed in time, and Math.floor(time).
* This value is later added back when scheduling to get sub-tick precision.
* This value is later added back when scheduling to get sub-tick precision.
*/
protected _remainderTime = 0;
@ -51,8 +50,10 @@ export class TransportEvent {
* @param transport The transport object which the event belongs to
*/
constructor(transport: Transport, opts: Partial<TransportEventOptions>) {
const options: TransportEventOptions = Object.assign(TransportEvent.getDefaults(), opts);
const options: TransportEventOptions = Object.assign(
TransportEvent.getDefaults(),
opts
);
this.transport = transport;
this.callback = options.callback;

View file

@ -4,7 +4,6 @@ import { TransportClass } from "./Transport.js";
import { TransportRepeatEvent } from "./TransportRepeatEvent.js";
describe("TransportRepeatEvent", () => {
it("can be created and disposed", () => {
return Offline((context) => {
const transport = new TransportClass({ context });
@ -39,5 +38,4 @@ describe("TransportRepeatEvent", () => {
expect(transport._timeline.length).to.equal(0);
});
});
});

View file

@ -16,7 +16,6 @@ interface TransportRepeatEventOptions extends TransportEventOptions {
* to schedule repeat events. This class should not be instantiated directly.
*/
export class TransportRepeatEvent extends TransportEvent {
/**
* When the event should stop repeating
*/
@ -55,8 +54,10 @@ export class TransportRepeatEvent extends TransportEvent {
/**
* @param transport The transport object which the event belongs to
*/
constructor(transport: Transport, opts: Partial<TransportRepeatEventOptions>) {
constructor(
transport: Transport,
opts: Partial<TransportRepeatEventOptions>
) {
super(transport, opts);
const options = Object.assign(TransportRepeatEvent.getDefaults(), opts);
@ -96,8 +97,10 @@ export class TransportRepeatEvent extends TransportEvent {
*/
private _createEvent(): number {
if (LT(this._nextTick, this.floatTime + this.duration)) {
return this.transport.scheduleOnce(this.invoke.bind(this),
new TicksClass(this.context, this._nextTick).toSeconds());
return this.transport.scheduleOnce(
this.invoke.bind(this),
new TicksClass(this.context, this._nextTick).toSeconds()
);
}
return -1;
}
@ -109,11 +112,15 @@ export class TransportRepeatEvent extends TransportEvent {
// schedule the next event
// const ticks = this.transport.getTicksAtTime(time);
// if the next tick is within the bounds set by "duration"
if (LT(this._nextTick + this._interval, this.floatTime + this.duration)) {
if (
LT(this._nextTick + this._interval, this.floatTime + this.duration)
) {
this._nextTick += this._interval;
this._currentId = this._nextId;
this._nextId = this.transport.scheduleOnce(this.invoke.bind(this),
new TicksClass(this.context, this._nextTick).toSeconds());
this._nextId = this.transport.scheduleOnce(
this.invoke.bind(this),
new TicksClass(this.context, this._nextTick).toSeconds()
);
}
}
@ -128,7 +135,10 @@ export class TransportRepeatEvent extends TransportEvent {
const ticks = this.transport.getTicksAtTime(time);
if (GT(ticks, this.time)) {
// the event is not being scheduled from the beginning and should be offset
this._nextTick = this.floatTime + Math.ceil((ticks - this.floatTime) / this._interval) * this._interval;
this._nextTick =
this.floatTime +
Math.ceil((ticks - this.floatTime) / this._interval) *
this._interval;
}
this._currentId = this._createEvent();
this._nextTick += this._interval;

View file

@ -4,7 +4,6 @@ import { Time, UnitMap, UnitName } from "../type/Units.js";
* Abstract base class for {@link Param} and {@link Signal}
*/
export abstract class AbstractParam<TypeName extends UnitName> {
/**
* Schedules a parameter value change at the given time.
* @param value The value to set the signal.
@ -40,7 +39,7 @@ export abstract class AbstractParam<TypeName extends UnitName> {
* Automation methods like {@link linearRampToValueAtTime} and {@link exponentialRampToValueAtTime}
* require a starting automation value usually set by {@link setValueAtTime}. This method
* is useful since it will do a `setValueAtTime` with whatever the currently computed
* value at the given time is.
* value at the given time is.
* @param time When to add a ramp point.
* @example
* const osc = new Tone.Oscillator().toDestination().start();
@ -61,7 +60,10 @@ export abstract class AbstractParam<TypeName extends UnitName> {
* signal.linearRampToValueAtTime(1, 0.4);
* }, 0.5, 1);
*/
abstract linearRampToValueAtTime(value: UnitMap[TypeName], time: Time): this;
abstract linearRampToValueAtTime(
value: UnitMap[TypeName],
time: Time
): this;
/**
* Schedules an exponential continuous change in parameter value from
@ -74,7 +76,10 @@ export abstract class AbstractParam<TypeName extends UnitName> {
* signal.exponentialRampToValueAtTime(0, 0.4);
* }, 0.5, 1);
*/
abstract exponentialRampToValueAtTime(value: UnitMap[TypeName], time: Time): this;
abstract exponentialRampToValueAtTime(
value: UnitMap[TypeName],
time: Time
): this;
/**
* Schedules an exponential continuous change in parameter value from
@ -96,7 +101,11 @@ export abstract class AbstractParam<TypeName extends UnitName> {
* signal.exponentialRampTo(5, 0.3, 0.1);
* }, 0.5, 1);
*/
abstract exponentialRampTo(value: UnitMap[TypeName], rampTime: Time, startTime?: Time): this;
abstract exponentialRampTo(
value: UnitMap[TypeName],
rampTime: Time,
startTime?: Time
): this;
/**
* Schedules an linear continuous change in parameter value from
@ -120,7 +129,11 @@ export abstract class AbstractParam<TypeName extends UnitName> {
* signal.linearRampTo(0, 0.3, 0.1);
* }, 0.5, 1);
*/
abstract linearRampTo(value: UnitMap[TypeName], rampTime: Time, startTime?: Time): this;
abstract linearRampTo(
value: UnitMap[TypeName],
rampTime: Time,
startTime?: Time
): this;
/**
* Start exponentially approaching the target value at the given time. Since it
@ -137,7 +150,11 @@ export abstract class AbstractParam<TypeName extends UnitName> {
* signal.targetRampTo(0, 0.3, 0.1);
* }, 0.5, 1);
*/
abstract targetRampTo(value: UnitMap[TypeName], rampTime: Time, startTime?: Time): this;
abstract targetRampTo(
value: UnitMap[TypeName],
rampTime: Time,
startTime?: Time
): this;
/**
* Start exponentially approaching the target value at the given time. Since it
@ -152,7 +169,11 @@ export abstract class AbstractParam<TypeName extends UnitName> {
* // exponential approach over 4 seconds starting in 1 second
* osc.frequency.exponentialApproachValueAtTime("C4", "+1", 4);
*/
abstract exponentialApproachValueAtTime(value: UnitMap[TypeName], time: Time, rampTime: Time): this;
abstract exponentialApproachValueAtTime(
value: UnitMap[TypeName],
time: Time,
rampTime: Time
): this;
/**
* Start exponentially approaching the target value at the given time with
@ -161,7 +182,11 @@ export abstract class AbstractParam<TypeName extends UnitName> {
* @param startTime
* @param timeConstant
*/
abstract setTargetAtTime(value: UnitMap[TypeName], startTime: Time, timeConstant: number): this;
abstract setTargetAtTime(
value: UnitMap[TypeName],
startTime: Time,
timeConstant: number
): this;
/**
* Sets an array of arbitrary parameter values starting at the given time
@ -177,7 +202,12 @@ export abstract class AbstractParam<TypeName extends UnitName> {
* signal.setValueCurveAtTime([1, 0.2, 0.8, 0.1, 0], 0.2, 0.3);
* }, 0.5, 1);
*/
abstract setValueCurveAtTime(values: UnitMap[TypeName][], startTime: Time, duration: Time, scaling?: number): this;
abstract setValueCurveAtTime(
values: UnitMap[TypeName][],
startTime: Time,
duration: Time,
scaling?: number
): this;
/**
* Cancels all scheduled parameter changes with times greater than or
@ -224,7 +254,11 @@ export abstract class AbstractParam<TypeName extends UnitName> {
* // schedule it to ramp starting at a specific time
* osc.frequency.rampTo("A2", 10, "+2");
*/
abstract rampTo(value: UnitMap[TypeName], rampTime: Time, startTime?: Time): this;
abstract rampTo(
value: UnitMap[TypeName],
rampTime: Time,
startTime?: Time
): this;
/**
* The current value of the parameter. Setting this value

View file

@ -1,7 +1,7 @@
import {
AudioContext as stdAudioContext,
AudioWorkletNode as stdAudioWorkletNode,
OfflineAudioContext as stdOfflineAudioContext
OfflineAudioContext as stdOfflineAudioContext,
} from "standardized-audio-context";
import { assert } from "../util/Debug.js";
import { isDefined } from "../util/TypeCheck.js";
@ -9,15 +9,25 @@ import { isDefined } from "../util/TypeCheck.js";
/**
* Create a new AudioContext
*/
export function createAudioContext(options?: AudioContextOptions): AudioContext {
export function createAudioContext(
options?: AudioContextOptions
): AudioContext {
return new stdAudioContext(options) as unknown as AudioContext;
}
/**
* Create a new OfflineAudioContext
*/
export function createOfflineAudioContext(channels: number, length: number, sampleRate: number): OfflineAudioContext {
return new stdOfflineAudioContext(channels, length, sampleRate) as unknown as OfflineAudioContext;
export function createOfflineAudioContext(
channels: number,
length: number,
sampleRate: number
): OfflineAudioContext {
return new stdOfflineAudioContext(
channels,
length,
sampleRate
) as unknown as OfflineAudioContext;
}
/**
@ -37,24 +47,34 @@ interface ToneWindow extends Window {
* A reference to the window object
* @hidden
*/
export const theWindow: ToneWindow | null = typeof self === "object" ? self : null;
export const theWindow: ToneWindow | null =
typeof self === "object" ? self : null;
/**
* If the browser has a window object which has an AudioContext
* @hidden
*/
export const hasAudioContext = theWindow &&
(theWindow.hasOwnProperty("AudioContext") || theWindow.hasOwnProperty("webkitAudioContext"));
export const hasAudioContext =
theWindow &&
(theWindow.hasOwnProperty("AudioContext") ||
theWindow.hasOwnProperty("webkitAudioContext"));
export function createAudioWorkletNode(context: AnyAudioContext, name: string, options?: Partial<AudioWorkletNodeOptions>): AudioWorkletNode {
assert(isDefined(stdAudioWorkletNode), "This node only works in a secure context (https or localhost)");
export function createAudioWorkletNode(
context: AnyAudioContext,
name: string,
options?: Partial<AudioWorkletNodeOptions>
): AudioWorkletNode {
assert(
isDefined(stdAudioWorkletNode),
"This node only works in a secure context (https or localhost)"
);
// @ts-ignore
return new stdAudioWorkletNode(context, name, options);
}
/**
* This promise resolves to a boolean which indicates if the
* functionality is supported within the currently used browse.
* This promise resolves to a boolean which indicates if the
* functionality is supported within the currently used browse.
* Taken from [standardized-audio-context](https://github.com/chrisguttandin/standardized-audio-context#issupported)
*/
export { isSupported as supported } from "standardized-audio-context";

View file

@ -20,15 +20,16 @@ export type ExcludedFromBaseAudioContext =
// the subset of the BaseAudioContext which Tone.Context implements.
export type BaseAudioContextSubset = Omit<
BaseAudioContext,
ExcludedFromBaseAudioContext
BaseAudioContext,
ExcludedFromBaseAudioContext
>;
export type ContextLatencyHint = AudioContextLatencyCategory;
export abstract class BaseContext
extends Emitter<"statechange" | "tick">
implements BaseAudioContextSubset {
implements BaseAudioContextSubset
{
//---------------------------
// BASE AUDIO CONTEXT METHODS
//---------------------------
@ -104,9 +105,7 @@ export abstract class BaseContext
abstract get rawContext(): AnyAudioContext;
abstract addAudioWorkletModule(
_url: string
): Promise<void>;
abstract addAudioWorkletModule(_url: string): Promise<void>;
abstract lookAhead: number;

View file

@ -75,7 +75,7 @@ describe("Context", () => {
clockSource: "timeout",
latencyHint: "playback",
lookAhead: 0.2,
updateInterval: 0.1
updateInterval: 0.1,
});
expect(ctx.lookAhead).to.equal(0.2);
expect(ctx.updateInterval).to.equal(0.1);

View file

@ -135,9 +135,13 @@ export class Context extends BaseContext {
this._context.onstatechange = () => {
this.emit("statechange", this.state);
};
// if no custom updateInterval provided, updateInterval will be derived by lookAhead setter
this[arguments[0]?.hasOwnProperty("updateInterval") ? "_lookAhead" : "lookAhead"] = options.lookAhead;
this[
arguments[0]?.hasOwnProperty("updateInterval")
? "_lookAhead"
: "lookAhead"
] = options.lookAhead;
}
static getDefaults(): ContextOptions {
@ -377,7 +381,7 @@ export class Context extends BaseContext {
* Returns a promise which resolves when all of the worklets have been loaded on this context
*/
protected async workletsAreReady(): Promise<void> {
await this._workletPromise ? this._workletPromise : Promise.resolve();
(await this._workletPromise) ? this._workletPromise : Promise.resolve();
}
//---------------------------
@ -421,8 +425,8 @@ export class Context extends BaseContext {
set lookAhead(time: Seconds) {
this._lookAhead = time;
// if lookAhead is 0, default to .01 updateInterval
this.updateInterval = time ? (time / 2) : .01;
}
this.updateInterval = time ? time / 2 : 0.01;
}
private _lookAhead!: Seconds;
/**
@ -475,7 +479,7 @@ export class Context extends BaseContext {
/**
* Starts the audio context from a suspended state. This is required
* to initially start the AudioContext.
* to initially start the AudioContext.
* @see {@link start}
*/
resume(): Promise<void> {
@ -491,7 +495,11 @@ export class Context extends BaseContext {
* any AudioNodes created from the context will be silent.
*/
async close(): Promise<void> {
if (isAudioContext(this._context) && (this.state !== "closed") && !this._closeStarted) {
if (
isAudioContext(this._context) &&
this.state !== "closed" &&
!this._closeStarted
) {
this._closeStarted = true;
await this._context.close();
}

View file

@ -21,7 +21,7 @@ export function onContextInit(cb: (ctx: Context) => void): void {
*/
export function initializeContext(ctx: Context): void {
// add any additional modules
notifyNewContext.forEach(cb => cb(ctx));
notifyNewContext.forEach((cb) => cb(ctx));
}
/**
@ -38,5 +38,5 @@ export function onContextClose(cb: (ctx: Context) => void): void {
export function closeContext(ctx: Context): void {
// remove any additional modules
notifyCloseContext.forEach(cb => cb(ctx));
notifyCloseContext.forEach((cb) => cb(ctx));
}

View file

@ -6,7 +6,6 @@ import { connect } from "../context/ToneAudioNode.js";
import { Delay } from "./Delay.js";
describe("Delay", () => {
BasicTests(Delay);
it("can be created and disposed", () => {
@ -40,7 +39,7 @@ describe("Delay", () => {
it("clamps the delayTime range between 0 and maxDelay", () => {
const delay = new Delay({
maxDelay: 1
maxDelay: 1,
});
expect(() => {
delay.delayTime.value = 2;
@ -97,7 +96,7 @@ describe("Delay", () => {
});
it("passes audio through", () => {
return PassAudio(input => {
return PassAudio((input) => {
const delay = new Delay().toDestination();
connect(input, delay);
});

View file

@ -22,7 +22,6 @@ export interface DelayOptions extends ToneAudioNodeOptions {
* }, 0.5, 1);
*/
export class Delay extends ToneAudioNode<DelayOptions> {
readonly name: string = "Delay";
/**
@ -55,14 +54,28 @@ export class Delay extends ToneAudioNode<DelayOptions> {
constructor(delayTime?: Time, maxDelay?: Time);
constructor(options?: Partial<DelayOptions>);
constructor() {
super(optionsFromArguments(Delay.getDefaults(), arguments, ["delayTime", "maxDelay"]));
super(
optionsFromArguments(Delay.getDefaults(), arguments, [
"delayTime",
"maxDelay",
])
);
const options = optionsFromArguments(Delay.getDefaults(), arguments, ["delayTime", "maxDelay"]);
const options = optionsFromArguments(Delay.getDefaults(), arguments, [
"delayTime",
"maxDelay",
]);
const maxDelayInSeconds = this.toSeconds(options.maxDelay);
this._maxDelay = Math.max(maxDelayInSeconds, this.toSeconds(options.delayTime));
this._maxDelay = Math.max(
maxDelayInSeconds,
this.toSeconds(options.delayTime)
);
this._delayNode = this.input = this.output = this.context.createDelay(maxDelayInSeconds);
this._delayNode =
this.input =
this.output =
this.context.createDelay(maxDelayInSeconds);
this.delayTime = new Param({
context: this.context,

Some files were not shown because too many files have changed in this diff Show more