mirror of
https://github.com/Tonejs/Tone.js
synced 2024-12-27 03:53:07 +00:00
prettier all of the files
This commit is contained in:
parent
2f3807c855
commit
3e73d88157
336 changed files with 7691 additions and 4067 deletions
|
@ -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();
|
||||
});
|
||||
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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>);
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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", () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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];
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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", () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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", () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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 });
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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", () => {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
|
|
|
@ -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
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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];
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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", () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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", () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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", () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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", () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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", () => {
|
|||
};
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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 });
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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"]);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
*/
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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"
|
||||
*/
|
||||
|
|
|
@ -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
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
|
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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", () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
|
|
@ -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") {
|
||||
|
|
|
@ -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 });
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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
Loading…
Reference in a new issue