Merge branch 'dev'

This commit is contained in:
Yotam Mann 2024-05-15 15:43:17 -04:00
commit cf73c22874
398 changed files with 21471 additions and 18113 deletions

View file

@ -1,7 +1,13 @@
module.exports = { module.exports = {
parser: "@typescript-eslint/parser", parser: "@typescript-eslint/parser",
plugins: ["jsdoc", "html"], plugins: ["jsdoc", "html", "file-extension-in-import-ts"],
extends: ["plugin:@typescript-eslint/recommended"], extends: ["plugin:@typescript-eslint/recommended"],
settings: {
"import/extensions": [".js", ".ts"],
"import/resolver": {
typescript: true,
},
},
rules: { rules: {
"prefer-rest-params": "off", "prefer-rest-params": "off",
"@typescript-eslint/ban-ts-ignore": "off", "@typescript-eslint/ban-ts-ignore": "off",
@ -23,7 +29,7 @@ module.exports = {
"no-useless-call": ["error"], "no-useless-call": ["error"],
"no-unmodified-loop-condition": ["error"], "no-unmodified-loop-condition": ["error"],
"quote-props": ["error", "as-needed"], "quote-props": ["error", "as-needed"],
quotes: ["error", "double"], quotes: ["error", "double", { avoidEscape: true }],
"no-shadow": "error", "no-shadow": "error",
"no-console": ["error", { allow: ["warn"] }], "no-console": ["error", { allow: ["warn"] }],
"@typescript-eslint/no-object-literal-type-assertion": "off", "@typescript-eslint/no-object-literal-type-assertion": "off",
@ -38,25 +44,11 @@ module.exports = {
}, },
], ],
"no-lonely-if": ["error"], "no-lonely-if": ["error"],
semi: ["error", "always"],
"no-cond-assign": ["error", "always"], "no-cond-assign": ["error", "always"],
indent: "off",
"no-var": "error", "no-var": "error",
"prefer-arrow-callback": "error", "prefer-arrow-callback": "error",
"@typescript-eslint/indent": [
"error",
"tab",
{ SwitchCase: 1, MemberExpression: 2 },
],
"@typescript-eslint/explicit-member-accessibility": "off", "@typescript-eslint/explicit-member-accessibility": "off",
"@typescript-eslint/explicit-function-return-type": "off", "@typescript-eslint/explicit-function-return-type": "off",
"no-multi-spaces": ["error"],
"array-bracket-spacing": ["error", "never"],
"block-spacing": ["error", "always"],
"func-call-spacing": ["error", "never"],
"key-spacing": ["error", { beforeColon: false, afterColon: true }],
"brace-style": ["error", "1tbs"],
"space-in-parens": ["error", "never"],
"eol-last": ["error", "always"], "eol-last": ["error", "always"],
"@typescript-eslint/no-explicit-any": "off", "@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-use-before-define": "off", "@typescript-eslint/no-use-before-define": "off",
@ -70,21 +62,7 @@ module.exports = {
}, },
], ],
"lines-between-class-members": "off", "lines-between-class-members": "off",
"no-multiple-empty-lines": ["error", { max: 1, maxEOF: 1, maxBOF: 0 }],
"no-unneeded-ternary": ["error"], "no-unneeded-ternary": ["error"],
"object-curly-spacing": ["error", "always"], "file-extension-in-import-ts/file-extension-in-import-ts": "error",
"space-unary-ops": ["error", { words: true, nonwords: false }],
"block-spacing": ["error", "always"],
"keyword-spacing": ["error", { before: true }],
"space-before-function-paren": [
"error",
{ anonymous: "never", named: "never", asyncArrow: "always" },
],
"comma-spacing": ["error", { before: false, after: true }],
"arrow-spacing": ["error", { before: true, after: true }],
"space-before-blocks": [
"error",
{ functions: "always", keywords: "always", classes: "always" },
],
}, },
}; };

View file

@ -26,12 +26,10 @@ jobs:
- name: Setup Nodejs - name: Setup Nodejs
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: 18.12.0 node-version: 18.18.0
cache: 'npm' cache: 'npm'
- name: Install dependencies - name: Install dependencies
run: npm install run: npm install
- name: Build
run: npm run build
- name: All tests - name: All tests
run: npm run test run: npm run test
- name: Upload coverage - name: Upload coverage
@ -52,7 +50,7 @@ jobs:
- name: Setup Nodejs - name: Setup Nodejs
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: 18.12.0 node-version: 18.18.0
cache: 'npm' cache: 'npm'
- name: Install dependencies - name: Install dependencies
run: npm install run: npm install
@ -72,7 +70,7 @@ jobs:
- name: Setup Nodejs - name: Setup Nodejs
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: 18.12.0 node-version: 18.18.0
cache: 'npm' cache: 'npm'
- name: Install dependencies - name: Install dependencies
run: npm install run: npm install
@ -92,7 +90,7 @@ jobs:
- name: Setup Nodejs - name: Setup Nodejs
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: 18.12.0 node-version: 18.18.0
cache: 'npm' cache: 'npm'
- name: Install dependencies - name: Install dependencies
run: npm install run: npm install
@ -110,7 +108,7 @@ jobs:
- name: Setup Nodejs - name: Setup Nodejs
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: 18.12.0 node-version: 18.18.0
cache: 'npm' cache: 'npm'
- name: Install dependencies - name: Install dependencies
run: npm install run: npm install
@ -118,10 +116,30 @@ jobs:
run: npm run build run: npm run build
- name: Test - name: Test
run: npm run test:readme run: npm run test:readme
test-integrations:
name: Test integrations
permissions:
contents: read
id-token: write
runs-on: ubuntu-latest
steps:
- name: Check out Git repository
uses: actions/checkout@v4
- name: Setup Nodejs
uses: actions/setup-node@v4
with:
node-version: 18.18.0
cache: 'npm'
- name: Install dependencies
run: npm install
- name: Build
run: npm run build
- name: Test
run: npm run test:integrations
publish: publish:
runs-on: ubuntu-latest runs-on: ubuntu-latest
# make sure all the tests pass first # make sure all the tests pass first
needs: [run-tests, test-code-examples, test-html-examples, test-lint, test-readme] needs: [run-tests, test-code-examples, test-html-examples, test-lint, test-readme, test-integrations]
# not on PRs # not on PRs
if: github.event_name != 'pull_request' if: github.event_name != 'pull_request'
env: env:
@ -132,7 +150,7 @@ jobs:
# Setup .npmrc file to publish to npm # Setup .npmrc file to publish to npm
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version: 18.12.0 node-version: 18.18.0
registry-url: 'https://registry.npmjs.org' registry-url: 'https://registry.npmjs.org'
- name: Install dependencies - name: Install dependencies
run: npm install run: npm install

3
.gitignore vendored
View file

@ -21,7 +21,7 @@ test/supports.html
coverage/ coverage/
build/* build/*
dist/* **/dist/*
examples/scratch.js examples/scratch.js
examples/scratch.ts examples/scratch.ts
@ -34,3 +34,4 @@ docs
.vscode .vscode
tone.d.ts tone.d.ts
examples/scratch.ts examples/scratch.ts
test/integration/*/package-lock.json

View file

@ -1,7 +1,7 @@
export * from "./core/index"; export * from "./core/index.js";
export * from "./source/index"; export * from "./source/index.js";
export * from "./signal/index"; export * from "./signal/index.js";
export * from "./instrument/index"; export * from "./instrument/index.js";
export * from "./event/index"; export * from "./event/index.js";
export * from "./effect/index"; export * from "./effect/index.js";
export * from "./component/index"; export * from "./component/index.js";

View file

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

View file

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

View file

@ -1,16 +1,13 @@
import { expect } from "chai"; import { expect } from "chai";
import { BasicTests, warns } from "test/helper/Basic"; import { BasicTests, warns } from "../../../test/helper/Basic.js";
import { PassAudio } from "test/helper/PassAudio"; import { PassAudio } from "../../../test/helper/PassAudio.js";
import { ONLINE_TESTING } from "test/helper/Supports"; import { Signal } from "../../signal/Signal.js";
import { Signal } from "Tone/signal/Signal"; import { DCMeter } from "./DCMeter.js";
import { DCMeter } from "./DCMeter";
describe("DCMeter", () => { describe("DCMeter", () => {
BasicTests(DCMeter); BasicTests(DCMeter);
context("DCMetering", () => { context("DCMetering", () => {
it("passes the audio through", () => { it("passes the audio through", () => {
return PassAudio((input) => { return PassAudio((input) => {
const meter = new DCMeter().toDestination(); const meter = new DCMeter().toDestination();
@ -18,17 +15,15 @@ describe("DCMeter", () => {
}); });
}); });
if (ONLINE_TESTING) { it("can get the rms level of the incoming signal", (done) => {
it("can get the rms level of the incoming signal", (done) => { const meter = new DCMeter();
const meter = new DCMeter(); const osc = new Signal(2).connect(meter);
const osc = new Signal(2).connect(meter); setTimeout(() => {
setTimeout(() => { expect(meter.getValue()).to.be.closeTo(2, 0.1);
expect(meter.getValue()).to.be.closeTo(2, 0.1); meter.dispose();
meter.dispose(); osc.dispose();
osc.dispose(); done();
done(); }, 400);
}, 400); });
});
}
}); });
}); });

View file

@ -1,10 +1,10 @@
import { optionsFromArguments } from "../../core/util/Defaults"; import { optionsFromArguments } from "../../core/util/Defaults.js";
import { MeterBase, MeterBaseOptions } from "./MeterBase"; import { MeterBase, MeterBaseOptions } from "./MeterBase.js";
export type DCMeterOptions = MeterBaseOptions; 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}. * @see {@link Meter}.
* *
* @example * @example
@ -18,7 +18,6 @@ export type DCMeterOptions = MeterBaseOptions;
* @category Component * @category Component
*/ */
export class DCMeter extends MeterBase<DCMeterOptions> { export class DCMeter extends MeterBase<DCMeterOptions> {
readonly name: string = "DCMeter"; readonly name: string = "DCMeter";
constructor(options?: Partial<DCMeterOptions>); constructor(options?: Partial<DCMeterOptions>);

View file

@ -1,11 +1,9 @@
import { expect } from "chai"; import { expect } from "chai";
import { BasicTests } from "test/helper/Basic"; import { BasicTests } from "../../../test/helper/Basic.js";
import { ONLINE_TESTING } from "test/helper/Supports"; import { Noise } from "../../source/Noise.js";
import { Noise } from "Tone/source/Noise"; import { FFT } from "./FFT.js";
import { FFT } from "./FFT";
describe("FFT", () => { describe("FFT", () => {
BasicTests(FFT); BasicTests(FFT);
it("can get and set properties", () => { it("can get and set properties", () => {
@ -38,7 +36,10 @@ describe("FFT", () => {
it("can get the frequency values of each index of the return array", () => { it("can get the frequency values of each index of the return array", () => {
const fft = new FFT(32); const fft = new FFT(32);
expect(fft.getFrequencyOfIndex(0)).to.be.closeTo(0, 1); 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(); fft.dispose();
}); });
@ -51,7 +52,7 @@ describe("FFT", () => {
setTimeout(() => { setTimeout(() => {
const analysis = fft.getValue(); const analysis = fft.getValue();
expect(analysis.length).to.equal(256); expect(analysis.length).to.equal(256);
analysis.forEach(value => { analysis.forEach((value) => {
expect(value).is.within(-Infinity, 0); expect(value).is.within(-Infinity, 0);
}); });
fft.dispose(); fft.dispose();
@ -60,24 +61,22 @@ describe("FFT", () => {
}, 300); }, 300);
}); });
if (ONLINE_TESTING) { it("outputs a normal range", (done) => {
it("outputs a normal range", (done) => { const noise = new Noise();
const noise = new Noise(); const fft = new FFT({
const fft = new FFT({ normalRange: true,
normalRange: true,
});
noise.connect(fft);
noise.start();
setTimeout(() => {
const analysis = fft.getValue();
analysis.forEach(value => {
expect(value).is.within(0, 1);
});
fft.dispose();
noise.dispose();
done();
}, 300);
}); });
} noise.connect(fft);
noise.start();
setTimeout(() => {
const analysis = fft.getValue();
analysis.forEach((value) => {
expect(value).is.within(0, 1);
});
fft.dispose();
noise.dispose();
done();
}, 300);
});
}); });

View file

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

View file

@ -1,16 +1,14 @@
import { Follower } from "./Follower"; import { Follower } from "./Follower.js";
import { BasicTests } from "test/helper/Basic"; import { BasicTests } from "../../../test/helper/Basic.js";
import { Offline } from "test/helper/Offline"; import { Offline } from "../../../test/helper/Offline.js";
import { Signal } from "Tone/signal/Signal"; import { Signal } from "../../signal/Signal.js";
import { PassAudio } from "test/helper/PassAudio"; import { PassAudio } from "../../../test/helper/PassAudio.js";
import { expect } from "chai"; import { expect } from "chai";
describe("Follower", () => { describe("Follower", () => {
BasicTests(Follower); BasicTests(Follower);
context("Envelope Following", () => { context("Envelope Following", () => {
it("handles getter/setter as Object", () => { it("handles getter/setter as Object", () => {
const foll = new Follower(); const foll = new Follower();
const values = { const values = {
@ -108,4 +106,3 @@ describe("Follower", () => {
}); });
}); });
}); });

View file

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

View file

@ -1,19 +1,16 @@
import { expect } from "chai"; import { expect } from "chai";
import { BasicTests, warns } from "test/helper/Basic"; import { BasicTests, warns } from "../../../test/helper/Basic.js";
import { PassAudio } from "test/helper/PassAudio"; import { PassAudio } from "../../../test/helper/PassAudio.js";
import { ONLINE_TESTING } from "test/helper/Supports"; import { Signal } from "../../signal/Signal.js";
import { Signal } from "Tone/signal/Signal"; import { Oscillator } from "../../source/oscillator/Oscillator.js";
import { Oscillator } from "Tone/source/oscillator/Oscillator"; import { Meter } from "./Meter.js";
import { Meter } from "./Meter"; import { Panner } from "../channel/Panner.js";
import { Panner } from "Tone/component/channel/Panner"; import { Merge } from "../channel/Merge.js";
import { Merge } from "Tone/component/channel/Merge";
describe("Meter", () => { describe("Meter", () => {
BasicTests(Meter); BasicTests(Meter);
context("Metering", () => { context("Metering", () => {
it("handles getter/setter as Object", () => { it("handles getter/setter as Object", () => {
const meter = new Meter(); const meter = new Meter();
const values = { const values = {
@ -60,57 +57,54 @@ describe("Meter", () => {
meter.dispose(); 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();
osc.volume.value = -6;
setTimeout(() => {
expect(meter.getValue()).to.be.closeTo(-9, 1);
meter.dispose();
osc.dispose();
done();
}, 400);
});
it("can get the values in normal range", (done) => { it("can get the rms level of the incoming signal", (done) => {
const meter = new Meter({ const meter = new Meter();
normalRange: true, const osc = new Oscillator().connect(meter).start();
}); osc.volume.value = -6;
const osc = new Oscillator().connect(meter).start(); setTimeout(() => {
osc.volume.value = -6; expect(meter.getValue()).to.be.closeTo(-9, 1);
setTimeout(() => { meter.dispose();
expect(meter.getValue()).to.be.closeTo(0.35, 0.15); osc.dispose();
meter.dispose(); done();
osc.dispose(); }, 400);
done(); });
}, 400);
});
it("can get the rms levels for multiple channels", (done) => { it("can get the values in normal range", (done) => {
const meter = new Meter({ const meter = new Meter({
channelCount: 2, normalRange: true,
smoothing: 0.5,
});
const merge = new Merge().connect(meter);
const osc0 = new Oscillator().connect(merge, 0, 0).start();
const osc1 = new Oscillator().connect(merge, 0, 1).start();
osc0.volume.value = -6;
osc1.volume.value = -18;
setTimeout(() => {
const values = meter.getValue();
expect(values).to.have.lengthOf(2);
expect(values[0]).to.be.closeTo(-9, 1);
expect(values[1]).to.be.closeTo(-21, 1);
meter.dispose();
merge.dispose();
osc0.dispose();
osc1.dispose();
done();
}, 400);
}); });
} const osc = new Oscillator().connect(meter).start();
osc.volume.value = -6;
setTimeout(() => {
expect(meter.getValue()).to.be.closeTo(0.35, 0.15);
meter.dispose();
osc.dispose();
done();
}, 400);
});
it("can get the rms levels for multiple channels", (done) => {
const meter = new Meter({
channelCount: 2,
smoothing: 0.5,
});
const merge = new Merge().connect(meter);
const osc0 = new Oscillator().connect(merge, 0, 0).start();
const osc1 = new Oscillator().connect(merge, 0, 1).start();
osc0.volume.value = -6;
osc1.volume.value = -18;
setTimeout(() => {
const values = meter.getValue();
expect(values).to.have.lengthOf(2);
expect(values[0]).to.be.closeTo(-9, 1);
expect(values[1]).to.be.closeTo(-21, 1);
meter.dispose();
merge.dispose();
osc0.dispose();
osc1.dispose();
done();
}, 400);
});
}); });
}); });

View file

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

View file

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

View file

@ -1,11 +1,9 @@
import { expect } from "chai"; import { expect } from "chai";
import { BasicTests } from "test/helper/Basic"; import { BasicTests } from "../../../test/helper/Basic.js";
import { ONLINE_TESTING } from "test/helper/Supports"; import { Noise } from "../../source/Noise.js";
import { Noise } from "Tone/source/Noise"; import { Waveform } from "./Waveform.js";
import { Waveform } from "./Waveform";
describe("Waveform", () => { describe("Waveform", () => {
BasicTests(Waveform); BasicTests(Waveform);
it("can get and set properties", () => { it("can get and set properties", () => {
@ -26,24 +24,21 @@ describe("Waveform", () => {
anl.dispose(); anl.dispose();
}); });
if (ONLINE_TESTING) { it("can run waveform analysis", (done) => {
const noise = new Noise();
const anl = new Waveform(256);
noise.connect(anl);
noise.start();
it("can run waveform analysis", (done) => { setTimeout(() => {
const noise = new Noise(); const analysis = anl.getValue();
const anl = new Waveform(256); expect(analysis.length).to.equal(256);
noise.connect(anl); analysis.forEach((value) => {
noise.start(); expect(value).is.within(-1, 1);
});
setTimeout(() => { anl.dispose();
const analysis = anl.getValue(); noise.dispose();
expect(analysis.length).to.equal(256); done();
analysis.forEach(value => { }, 300);
expect(value).is.within(-1, 1); });
});
anl.dispose();
noise.dispose();
done();
}, 300);
});
}
}); });

View file

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

View file

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

View file

@ -1,11 +1,16 @@
import { AudioRange, Decibels } from "../../core/type/Units"; import { AudioRange, Decibels } from "../../core/type/Units.js";
import { InputNode, OutputNode, ToneAudioNode, ToneAudioNodeOptions } from "../../core/context/ToneAudioNode"; import {
import { optionsFromArguments } from "../../core/util/Defaults"; InputNode,
import { Solo } from "./Solo"; OutputNode,
import { PanVol } from "./PanVol"; ToneAudioNode,
import { Param } from "../../core/context/Param"; ToneAudioNodeOptions,
import { readOnly } from "../../core/util/Interface"; } from "../../core/context/ToneAudioNode.js";
import { Gain } from "../../core/context/Gain"; import { optionsFromArguments } from "../../core/util/Defaults.js";
import { Solo } from "./Solo.js";
import { PanVol } from "./PanVol.js";
import { Param } from "../../core/context/Param.js";
import { readOnly } from "../../core/util/Interface.js";
import { Gain } from "../../core/context/Gain.js";
export interface ChannelOptions extends ToneAudioNodeOptions { export interface ChannelOptions extends ToneAudioNodeOptions {
pan: AudioRange; pan: AudioRange;
@ -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} * @see {@link PanVol} and {@link Solo}
* @example * @example
* // pan the incoming signal left and drop the volume 12db * // pan the incoming signal left and drop the volume 12db
@ -24,7 +29,6 @@ export interface ChannelOptions extends ToneAudioNodeOptions {
* @category Component * @category Component
*/ */
export class Channel extends ToneAudioNode<ChannelOptions> { export class Channel extends ToneAudioNode<ChannelOptions> {
readonly name: string = "Channel"; readonly name: string = "Channel";
readonly input: InputNode; readonly input: InputNode;
@ -59,8 +63,11 @@ export class Channel extends ToneAudioNode<ChannelOptions> {
constructor(volume?: Decibels, pan?: AudioRange); constructor(volume?: Decibels, pan?: AudioRange);
constructor(options?: Partial<ChannelOptions>); constructor(options?: Partial<ChannelOptions>);
constructor() { constructor() {
super(optionsFromArguments(Channel.getDefaults(), arguments, ["volume", "pan"])); const options = optionsFromArguments(Channel.getDefaults(), arguments, [
const options = optionsFromArguments(Channel.getDefaults(), arguments, ["volume", "pan"]); "volume",
"pan",
]);
super(options);
this._solo = this.input = new Solo({ this._solo = this.input = new Solo({
solo: options.solo, solo: options.solo,
@ -71,7 +78,7 @@ export class Channel extends ToneAudioNode<ChannelOptions> {
pan: options.pan, pan: options.pan,
volume: options.volume, volume: options.volume,
mute: options.mute, mute: options.mute,
channelCount: options.channelCount channelCount: options.channelCount,
}); });
this.pan = this._panVol.pan; this.pan = this._panVol.pan;
this.volume = this._panVol.volume; this.volume = this._panVol.volume;
@ -119,7 +126,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(); private static buses: Map<string, Gain> = new Map();
@ -137,11 +144,11 @@ export class Channel extends ToneAudioNode<ChannelOptions> {
/** /**
* Send audio to another channel using a string. `send` is a lot like * 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} * be useful in large applications to decouple sections since {@link send}
* and {@link receive} can be invoked separately in order to connect an object * and {@link receive} can be invoked separately in order to connect an object
* @param name The channel name to send the audio * @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 * Defaults to 0db, i.e. send the entire signal
* @returns Returns the gain node of this connection. * @returns Returns the gain node of this connection.
*/ */
@ -158,7 +165,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. * @param name The channel name to receive audio from.
*/ */
receive(name: string): this { receive(name: string): this {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,21 +1,23 @@
import { expect } from "chai"; import { expect } from "chai";
import { BasicTests } from "test/helper/Basic"; import { BasicTests } from "../../../test/helper/Basic.js";
import { Offline } from "test/helper/Offline"; import { Offline } from "../../../test/helper/Offline.js";
import { StereoSignal } from "test/helper/StereoSignal"; import { StereoSignal } from "../../../test/helper/StereoSignal.js";
import { Signal } from "Tone/signal/Signal"; import { Signal } from "../../signal/Signal.js";
import { Mono } from "./Mono"; import { Mono } from "./Mono.js";
describe("Mono", () => { describe("Mono", () => {
BasicTests(Mono); BasicTests(Mono);
context("Mono", () => { context("Mono", () => {
it("Makes a mono signal in both channels", () => { it("Makes a mono signal in both channels", () => {
return Offline(() => { return Offline(
const mono = new Mono().toDestination(); () => {
const signal = new Signal(2).connect(mono); const mono = new Mono().toDestination();
}, 0.1, 2).then((buffer) => { const signal = new Signal(2).connect(mono);
},
0.1,
2
).then((buffer) => {
expect(buffer.toArray()[0][0]).to.equal(2); expect(buffer.toArray()[0][0]).to.equal(2);
expect(buffer.toArray()[1][0]).to.equal(2); expect(buffer.toArray()[1][0]).to.equal(2);
expect(buffer.toArray()[0][100]).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", () => { it("Sums a stereo signal into a mono signal", () => {
return Offline(() => { return Offline(
const mono = new Mono().toDestination(); () => {
const signal = StereoSignal(2, 2).connect(mono); const mono = new Mono().toDestination();
}, 0.1, 2).then((buffer) => { const signal = StereoSignal(2, 2).connect(mono);
},
0.1,
2
).then((buffer) => {
expect(buffer.toArray()[0][0]).to.equal(2); expect(buffer.toArray()[0][0]).to.equal(2);
expect(buffer.toArray()[1][0]).to.equal(2); expect(buffer.toArray()[1][0]).to.equal(2);
expect(buffer.toArray()[0][100]).to.equal(2); expect(buffer.toArray()[0][100]).to.equal(2);
@ -40,4 +46,3 @@ describe("Mono", () => {
}); });
}); });
}); });

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,16 +1,14 @@
import { expect } from "chai"; import { expect } from "chai";
import { BasicTests } from "test/helper/Basic"; import { BasicTests } from "../../../test/helper/Basic.js";
import { Offline } from "test/helper/Offline"; import { Offline } from "../../../test/helper/Offline.js";
import { PassAudio } from "test/helper/PassAudio"; import { PassAudio } from "../../../test/helper/PassAudio.js";
import { Signal } from "Tone/signal/Signal"; import { Signal } from "../../signal/Signal.js";
import { Panner } from "./Panner"; import { Panner } from "./Panner.js";
describe("Panner", () => { describe("Panner", () => {
BasicTests(Panner); BasicTests(Panner);
context("Panning", () => { context("Panning", () => {
it("can be constructed with the panning value", () => { it("can be constructed with the panning value", () => {
const panner = new Panner(0.3); const panner = new Panner(0.3);
expect(panner.pan.value).to.be.closeTo(0.3, 0.001); 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", () => { it("pans hard left when the pan is set to -1", () => {
return Offline(() => { return Offline(
const panner = new Panner(-1).toDestination(); () => {
new Signal(1).connect(panner); const panner = new Panner(-1).toDestination();
}, 0.1, 2).then((buffer) => { new Signal(1).connect(panner);
},
0.1,
2
).then((buffer) => {
const l = buffer.toArray()[0]; const l = buffer.toArray()[0];
const r = buffer.toArray()[1]; const r = buffer.toArray()[1];
expect(l[0]).to.be.closeTo(1, 0.01); 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", () => { it("pans hard right when the pan is set to 1", () => {
return Offline(() => { return Offline(
const panner = new Panner(1).toDestination(); () => {
new Signal(1).connect(panner); const panner = new Panner(1).toDestination();
}, 0.1, 2).then((buffer) => { new Signal(1).connect(panner);
},
0.1,
2
).then((buffer) => {
const l = buffer.toArray()[0]; const l = buffer.toArray()[0];
const r = buffer.toArray()[1]; const r = buffer.toArray()[1];
expect(l[0]).to.be.closeTo(0, 0.01); 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", () => { it("mixes the signal in equal power when panned center", () => {
return Offline(() => { return Offline(
const panner = new Panner(0).toDestination(); () => {
new Signal(1).connect(panner); const panner = new Panner(0).toDestination();
}, 0.1, 2).then((buffer) => { new Signal(1).connect(panner);
},
0.1,
2
).then((buffer) => {
const l = buffer.toArray()[0]; const l = buffer.toArray()[0];
const r = buffer.toArray()[1]; const r = buffer.toArray()[1];
expect(l[0]).to.be.closeTo(0.707, 0.01); 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", () => { it("can chain two panners when channelCount is 2", () => {
return Offline(() => { return Offline(
const panner1 = new Panner({ () => {
channelCount: 2, const panner1 = new Panner({
}).toDestination(); channelCount: 2,
const panner0 = new Panner(-1).connect(panner1); }).toDestination();
new Signal(1).connect(panner0); const panner0 = new Panner(-1).connect(panner1);
}, 0.1, 2).then((buffer) => { new Signal(1).connect(panner0);
},
0.1,
2
).then((buffer) => {
const l = buffer.toArray()[0]; const l = buffer.toArray()[0];
const r = buffer.toArray()[1]; const r = buffer.toArray()[1];
expect(l[0]).to.be.closeTo(1, 0.01); expect(l[0]).to.be.closeTo(1, 0.01);

View file

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

View file

@ -1,10 +1,9 @@
import { expect } from "chai"; import { expect } from "chai";
import { BasicTests } from "test/helper/Basic"; import { BasicTests } from "../../../test/helper/Basic.js";
import { PassAudio } from "test/helper/PassAudio"; import { PassAudio } from "../../../test/helper/PassAudio.js";
import { Panner3D } from "./Panner3D"; import { Panner3D } from "./Panner3D.js";
describe("Panner3D", () => { describe("Panner3D", () => {
BasicTests(Panner3D); BasicTests(Panner3D);
it("passes the incoming signal through", () => { it("passes the incoming signal through", () => {

View file

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

View file

@ -1,14 +1,12 @@
import { expect } from "chai"; import { expect } from "chai";
import { connectFrom } from "test/helper/Connect"; import { connectFrom } from "../../../test/helper/Connect.js";
import { Recorder } from "./Recorder"; import { Recorder } from "./Recorder.js";
import { Context } from "Tone/core/context/Context"; import { Context } from "../../core/context/Context.js";
import { ToneWithContext } from "Tone/core/context/ToneWithContext"; import { ToneWithContext } from "../../core/context/ToneWithContext.js";
import { Synth } from "Tone/instrument/Synth"; import { Synth } from "../../instrument/Synth.js";
describe("Recorder", () => { describe("Recorder", () => {
context("basic", () => { context("basic", () => {
it("can be created and disposed", () => { it("can be created and disposed", () => {
const rec = new Recorder(); const rec = new Recorder();
rec.dispose(); rec.dispose();
@ -33,11 +31,13 @@ describe("Recorder", () => {
it("can set a different context", () => { it("can set a different context", () => {
const testContext = new Context(); const testContext = new Context();
const rec = new Recorder({ const rec = new Recorder({
context: testContext context: testContext,
}); });
for (const member in rec) { for (const member in rec) {
if (rec[member] instanceof ToneWithContext) { 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(); testContext.dispose();
@ -47,11 +47,10 @@ describe("Recorder", () => {
}); });
function wait(time) { function wait(time) {
return new Promise(done => setTimeout(done, time)); return new Promise((done) => setTimeout(done, time));
} }
context("start/stop/pause", () => { context("start/stop/pause", () => {
it("can be started", () => { it("can be started", () => {
const rec = new Recorder(); const rec = new Recorder();
rec.start(); rec.start();
@ -114,6 +113,5 @@ describe("Recorder", () => {
rec.dispose(); rec.dispose();
synth.dispose(); synth.dispose();
}); });
}); });
}); });

View file

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

View file

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

View file

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

View file

@ -1,16 +1,14 @@
import { expect } from "chai"; import { expect } from "chai";
import { BasicTests } from "test/helper/Basic"; import { BasicTests } from "../../../test/helper/Basic.js";
import { connectTo } from "test/helper/Connect"; import { connectTo } from "../../../test/helper/Connect.js";
import { ConstantOutput } from "test/helper/ConstantOutput"; import { ConstantOutput } from "../../../test/helper/ConstantOutput.js";
import { StereoSignal } from "test/helper/StereoSignal"; import { StereoSignal } from "../../../test/helper/StereoSignal.js";
import { Split } from "./Split"; import { Split } from "./Split.js";
describe("Split", () => { describe("Split", () => {
BasicTests(Split); BasicTests(Split);
context("Splitting", () => { context("Splitting", () => {
it("defaults to two channels", () => { it("defaults to two channels", () => {
const split = new Split(); const split = new Split();
expect(split.numberOfOutputs).to.equal(2); expect(split.numberOfOutputs).to.equal(2);

View file

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

View file

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

View file

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

View file

@ -1,14 +1,12 @@
import { expect } from "chai"; import { expect } from "chai";
import { BasicTests } from "test/helper/Basic"; import { BasicTests } from "../../../test/helper/Basic.js";
import { PassAudio } from "test/helper/PassAudio"; import { PassAudio } from "../../../test/helper/PassAudio.js";
import { Compressor } from "./Compressor"; import { Compressor } from "./Compressor.js";
describe("Compressor", () => { describe("Compressor", () => {
BasicTests(Compressor); BasicTests(Compressor);
context("Compression", () => { context("Compression", () => {
it("passes the incoming signal through", () => { it("passes the incoming signal through", () => {
return PassAudio((input) => { return PassAudio((input) => {
const comp = new Compressor().toDestination(); const comp = new Compressor().toDestination();
@ -26,7 +24,13 @@ describe("Compressor", () => {
threshold: -30, threshold: -30,
}; };
comp.set(values); 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(); comp.dispose();
}); });

View file

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

View file

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

View file

@ -1,10 +1,13 @@
import { ToneAudioNode, ToneAudioNodeOptions } from "../../core/context/ToneAudioNode"; import {
import { Decibels, Time } from "../../core/type/Units"; ToneAudioNode,
import { GreaterThan } from "../../signal/GreaterThan"; ToneAudioNodeOptions,
import { Gain } from "../../core/context/Gain"; } from "../../core/context/ToneAudioNode.js";
import { Follower } from "../analysis/Follower"; import { Decibels, Time } from "../../core/type/Units.js";
import { optionsFromArguments } from "../../core/util/Defaults"; import { GreaterThan } from "../../signal/GreaterThan.js";
import { dbToGain, gainToDb } from "../../core/type/Conversions"; import { Gain } from "../../core/context/Gain.js";
import { Follower } from "../analysis/Follower.js";
import { optionsFromArguments } from "../../core/util/Defaults.js";
import { dbToGain, gainToDb } from "../../core/type/Conversions.js";
export interface GateOptions extends ToneAudioNodeOptions { export interface GateOptions extends ToneAudioNodeOptions {
threshold: Decibels; threshold: Decibels;
@ -24,7 +27,6 @@ export interface GateOptions extends ToneAudioNodeOptions {
* @category Component * @category Component
*/ */
export class Gate extends ToneAudioNode<GateOptions> { export class Gate extends ToneAudioNode<GateOptions> {
readonly name: string = "Gate"; readonly name: string = "Gate";
readonly input: ToneAudioNode; readonly input: ToneAudioNode;
@ -52,8 +54,11 @@ export class Gate extends ToneAudioNode<GateOptions> {
constructor(threshold?: Decibels, smoothing?: Time); constructor(threshold?: Decibels, smoothing?: Time);
constructor(options?: Partial<GateOptions>); constructor(options?: Partial<GateOptions>);
constructor() { constructor() {
super(Object.assign(optionsFromArguments(Gate.getDefaults(), arguments, ["threshold", "smoothing"]))); const options = optionsFromArguments(Gate.getDefaults(), arguments, [
const options = optionsFromArguments(Gate.getDefaults(), arguments, ["threshold", "smoothing"]); "threshold",
"smoothing",
]);
super(options);
this._follower = new Follower({ this._follower = new Follower({
context: this.context, context: this.context,
@ -75,7 +80,7 @@ export class Gate extends ToneAudioNode<GateOptions> {
static getDefaults(): GateOptions { static getDefaults(): GateOptions {
return Object.assign(ToneAudioNode.getDefaults(), { return Object.assign(ToneAudioNode.getDefaults(), {
smoothing: 0.1, smoothing: 0.1,
threshold: -40 threshold: -40,
}); });
} }
@ -90,7 +95,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} * @see {@link Follower.smoothing}
*/ */
get smoothing(): Time { get smoothing(): Time {

View file

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

View file

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

View file

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

View file

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

View file

@ -1,14 +1,12 @@
import { MultibandCompressor } from "./MultibandCompressor"; import { MultibandCompressor } from "./MultibandCompressor.js";
import { BasicTests } from "test/helper/Basic"; import { BasicTests } from "../../../test/helper/Basic.js";
import { PassAudio } from "test/helper/PassAudio"; import { PassAudio } from "../../../test/helper/PassAudio.js";
import { expect } from "chai"; import { expect } from "chai";
describe("MultibandCompressor", () => { describe("MultibandCompressor", () => {
BasicTests(MultibandCompressor); BasicTests(MultibandCompressor);
context("Compression", () => { context("Compression", () => {
it("passes the incoming signal through", () => { it("passes the incoming signal through", () => {
return PassAudio((input) => { return PassAudio((input) => {
const comp = new MultibandCompressor().toDestination(); const comp = new MultibandCompressor().toDestination();
@ -26,11 +24,17 @@ describe("MultibandCompressor", () => {
high: { high: {
release: 0.5, release: 0.5,
attack: 0.03, attack: 0.03,
knee: 20 knee: 20,
} },
}; };
comp.set(values); 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().mid.ratio).be.closeTo(16, 0.01);
expect(comp.get().high.release).be.closeTo(0.5, 0.01); expect(comp.get().high.release).be.closeTo(0.5, 0.01);
comp.dispose(); comp.dispose();
@ -51,4 +55,3 @@ describe("MultibandCompressor", () => {
}); });
}); });
}); });

View file

@ -1,11 +1,15 @@
import { InputNode, ToneAudioNode, ToneAudioNodeOptions } from "../../core/context/ToneAudioNode"; import {
import { Compressor, CompressorOptions } from "./Compressor"; InputNode,
import { optionsFromArguments } from "../../core/util/Defaults"; ToneAudioNode,
import { readOnly, RecursivePartial } from "../../core/util/Interface"; ToneAudioNodeOptions,
import { Frequency } from "../../core/type/Units"; } from "../../core/context/ToneAudioNode.js";
import { MultibandSplit } from "../channel/MultibandSplit"; import { Compressor, CompressorOptions } from "./Compressor.js";
import { Signal } from "../../signal/Signal"; import { optionsFromArguments } from "../../core/util/Defaults.js";
import { Gain } from "../../core/context/Gain"; import { readOnly, RecursivePartial } from "../../core/util/Interface.js";
import { Frequency } from "../../core/type/Units.js";
import { MultibandSplit } from "../channel/MultibandSplit.js";
import { Signal } from "../../signal/Signal.js";
import { Gain } from "../../core/context/Gain.js";
export interface MultibandCompressorOptions extends ToneAudioNodeOptions { export interface MultibandCompressorOptions extends ToneAudioNodeOptions {
mid: Omit<CompressorOptions, keyof ToneAudioNodeOptions>; mid: Omit<CompressorOptions, keyof ToneAudioNodeOptions>;
@ -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} * @see {@link Compressor} and {@link MultibandSplit}
* *
* @example * @example
@ -30,7 +34,6 @@ export interface MultibandCompressorOptions extends ToneAudioNodeOptions {
* @category Component * @category Component
*/ */
export class MultibandCompressor extends ToneAudioNode<MultibandCompressorOptions> { export class MultibandCompressor extends ToneAudioNode<MultibandCompressorOptions> {
readonly name: string = "MultibandCompressor"; readonly name: string = "MultibandCompressor";
readonly input: InputNode; readonly input: InputNode;
@ -68,20 +71,29 @@ export class MultibandCompressor extends ToneAudioNode<MultibandCompressorOption
constructor(options?: RecursivePartial<MultibandCompressorOptions>); constructor(options?: RecursivePartial<MultibandCompressorOptions>);
constructor() { constructor() {
super(Object.assign(optionsFromArguments(MultibandCompressor.getDefaults(), arguments))); const options = optionsFromArguments(
const options = optionsFromArguments(MultibandCompressor.getDefaults(), arguments); MultibandCompressor.getDefaults(),
arguments
);
super(options);
this._splitter = this.input = new MultibandSplit({ this._splitter = this.input = new MultibandSplit({
context: this.context, context: this.context,
lowFrequency: options.lowFrequency, lowFrequency: options.lowFrequency,
highFrequency: options.highFrequency highFrequency: options.highFrequency,
}); });
this.lowFrequency = this._splitter.lowFrequency; this.lowFrequency = this._splitter.lowFrequency;
this.highFrequency = this._splitter.highFrequency; this.highFrequency = this._splitter.highFrequency;
this.output = new Gain({ context: this.context }); this.output = new Gain({ context: this.context });
this.low = new Compressor(Object.assign(options.low, { context: this.context })); this.low = new Compressor(
this.mid = new Compressor(Object.assign(options.mid, { context: this.context })); Object.assign(options.low, { context: this.context })
this.high = new Compressor(Object.assign(options.high, { 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 // connect the compressor
this._splitter.low.chain(this.low, this.output); this._splitter.low.chain(this.low, this.output);
@ -100,21 +112,21 @@ export class MultibandCompressor extends ToneAudioNode<MultibandCompressorOption
threshold: -30, threshold: -30,
release: 0.25, release: 0.25,
attack: 0.03, attack: 0.03,
knee: 10 knee: 10,
}, },
mid: { mid: {
ratio: 3, ratio: 3,
threshold: -24, threshold: -24,
release: 0.03, release: 0.03,
attack: 0.02, attack: 0.02,
knee: 16 knee: 16,
}, },
high: { high: {
ratio: 3, ratio: 3,
threshold: -24, threshold: -24,
release: 0.03, release: 0.03,
attack: 0.02, attack: 0.02,
knee: 16 knee: 16,
}, },
}); });
} }

View file

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

View file

@ -1,7 +1,7 @@
import { Gain } from "../../core/context/Gain"; import { Gain } from "../../core/context/Gain.js";
import { NormalRange, Time } from "../../core/type/Units"; import { NormalRange, Time } from "../../core/type/Units.js";
import { optionsFromArguments } from "../../core/util/Defaults"; import { optionsFromArguments } from "../../core/util/Defaults.js";
import { Envelope, EnvelopeOptions } from "./Envelope"; import { Envelope, EnvelopeOptions } from "./Envelope.js";
/** /**
* AmplitudeEnvelope is a Tone.Envelope connected to a gain node. * AmplitudeEnvelope is a Tone.Envelope connected to a gain node.
@ -26,7 +26,6 @@ import { Envelope, EnvelopeOptions } from "./Envelope";
* @category Component * @category Component
*/ */
export class AmplitudeEnvelope extends Envelope { export class AmplitudeEnvelope extends Envelope {
readonly name: string = "AmplitudeEnvelope"; readonly name: string = "AmplitudeEnvelope";
private _gainNode: Gain = new Gain({ 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. * @param release The amount of time after the release is triggered it takes to reach 0.
* Value must be greater than 0. * Value must be greater than 0.
*/ */
constructor(attack?: Time, decay?: Time, sustain?: NormalRange, release?: Time); constructor(
constructor(options?: Partial<EnvelopeOptions>) attack?: Time,
decay?: Time,
sustain?: NormalRange,
release?: Time
);
constructor(options?: Partial<EnvelopeOptions>);
constructor() { 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._sig.connect(this._gainNode.gain);
this.output = this._gainNode; this.output = this._gainNode;
this.input = this._gainNode; this.input = this._gainNode;

View file

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

View file

@ -1,12 +1,15 @@
import { InputNode, OutputNode } from "../../core/context/ToneAudioNode"; import { InputNode, OutputNode } from "../../core/context/ToneAudioNode.js";
import { ToneAudioNode, ToneAudioNodeOptions } from "../../core/context/ToneAudioNode"; import {
import { NormalRange, Time } from "../../core/type/Units"; ToneAudioNode,
import { optionsFromArguments } from "../../core/util/Defaults"; ToneAudioNodeOptions,
import { isArray, isObject, isString } from "../../core/util/TypeCheck"; } from "../../core/context/ToneAudioNode.js";
import { connectSignal, Signal } from "../../signal/Signal"; import { NormalRange, Time } from "../../core/type/Units.js";
import { OfflineContext } from "../../core/context/OfflineContext"; import { optionsFromArguments } from "../../core/util/Defaults.js";
import { assert } from "../../core/util/Debug"; import { isArray, isObject, isString } from "../../core/util/TypeCheck.js";
import { range, timeRange } from "../../core/util/Decorator"; import { connectSignal, Signal } from "../../signal/Signal.js";
import { OfflineContext } from "../../core/context/OfflineContext.js";
import { assert } from "../../core/util/Debug.js";
import { range, timeRange } from "../../core/util/Decorator.js";
type BasicEnvelopeCurve = "linear" | "exponential"; type BasicEnvelopeCurve = "linear" | "exponential";
type InternalEnvelopeCurve = BasicEnvelopeCurve | number[]; type InternalEnvelopeCurve = BasicEnvelopeCurve | number[];
@ -50,7 +53,6 @@ export interface EnvelopeOptions extends ToneAudioNodeOptions {
* @category Component * @category Component
*/ */
export class Envelope extends ToneAudioNode<EnvelopeOptions> { export class Envelope extends ToneAudioNode<EnvelopeOptions> {
readonly name: string = "Envelope"; readonly name: string = "Envelope";
/** /**
@ -176,12 +178,20 @@ export class Envelope extends ToneAudioNode<EnvelopeOptions> {
* @param release The amount of time after the release is triggered it takes to reach 0. * @param release The amount of time after the release is triggered it takes to reach 0.
* Value must be greater than 0. * Value must be greater than 0.
*/ */
constructor(attack?: Time, decay?: Time, sustain?: NormalRange, release?: Time); constructor(
constructor(options?: Partial<EnvelopeOptions>) attack?: Time,
decay?: Time,
sustain?: NormalRange,
release?: Time
);
constructor(options?: Partial<EnvelopeOptions>);
constructor() { constructor() {
const options = optionsFromArguments(
super(optionsFromArguments(Envelope.getDefaults(), arguments, ["attack", "decay", "sustain", "release"])); Envelope.getDefaults(),
const options = optionsFromArguments(Envelope.getDefaults(), arguments, ["attack", "decay", "sustain", "release"]); arguments,
["attack", "decay", "sustain", "release"]
);
super(options);
this.attack = options.attack; this.attack = options.attack;
this.decay = options.decay; this.decay = options.decay;
@ -218,7 +228,10 @@ export class Envelope extends ToneAudioNode<EnvelopeOptions> {
* @param direction In/Out * @param direction In/Out
* @return The curve name * @return The curve name
*/ */
private _getCurve(curve: InternalEnvelopeCurve, direction: EnvelopeDirection): EnvelopeCurve { private _getCurve(
curve: InternalEnvelopeCurve,
direction: EnvelopeDirection
): EnvelopeCurve {
if (isString(curve)) { if (isString(curve)) {
return curve; return curve;
} else { } else {
@ -243,7 +256,7 @@ export class Envelope extends ToneAudioNode<EnvelopeOptions> {
private _setCurve( private _setCurve(
name: "_attackCurve" | "_decayCurve" | "_releaseCurve", name: "_attackCurve" | "_decayCurve" | "_releaseCurve",
direction: EnvelopeDirection, direction: EnvelopeDirection,
curve: EnvelopeCurve, curve: EnvelopeCurve
): void { ): void {
// check if it's a valid type // check if it's a valid type
if (isString(curve) && Reflect.has(EnvelopeCurves, curve)) { if (isString(curve) && Reflect.has(EnvelopeCurves, curve)) {
@ -385,9 +398,16 @@ export class Envelope extends ToneAudioNode<EnvelopeOptions> {
const decayStart = time + attack; const decayStart = time + attack;
this.log("decay", decayStart); this.log("decay", decayStart);
if (this._decayCurve === "linear") { if (this._decayCurve === "linear") {
this._sig.linearRampToValueAtTime(decayValue, decay + decayStart); this._sig.linearRampToValueAtTime(
decayValue,
decay + decayStart
);
} else { } else {
this._sig.exponentialApproachValueAtTime(decayValue, decayStart, decay); this._sig.exponentialApproachValueAtTime(
decayValue,
decayStart,
decay
);
} }
} }
return this; return this;
@ -418,9 +438,17 @@ export class Envelope extends ToneAudioNode<EnvelopeOptions> {
} else if (this._releaseCurve === "exponential") { } else if (this._releaseCurve === "exponential") {
this._sig.targetRampTo(0, release, time); this._sig.targetRampTo(0, release, time);
} else { } 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.cancelAndHoldAtTime(time);
this._sig.setValueCurveAtTime(this._releaseCurve, time, release, currentValue); this._sig.setValueCurveAtTime(
this._releaseCurve,
time,
release,
currentValue
);
} }
} }
return this; return this;
@ -450,7 +478,11 @@ export class Envelope extends ToneAudioNode<EnvelopeOptions> {
* // trigger the release 0.5 seconds after the attack * // trigger the release 0.5 seconds after the attack
* env.triggerAttackRelease(0.5); * 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); time = this.toSeconds(time);
this.triggerAttack(time, velocity); this.triggerAttack(time, velocity);
this.triggerRelease(time + this.toSeconds(duration)); this.triggerRelease(time + this.toSeconds(duration));
@ -480,21 +512,33 @@ export class Envelope extends ToneAudioNode<EnvelopeOptions> {
*/ */
async asArray(length = 1024): Promise<Float32Array> { async asArray(length = 1024): Promise<Float32Array> {
const duration = length / this.context.sampleRate; 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 // 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 envelopeDuration = attackPortion + this.toSeconds(this.release);
const sustainTime = envelopeDuration * 0.1; const sustainTime = envelopeDuration * 0.1;
const totalDuration = envelopeDuration + sustainTime; const totalDuration = envelopeDuration + sustainTime;
// @ts-ignore // @ts-ignore
const clone = new this.constructor(Object.assign(this.get(), { const clone = new this.constructor(
attack: duration * this.toSeconds(this.attack) / totalDuration, Object.assign(this.get(), {
decay: duration * this.toSeconds(this.decay) / totalDuration, attack:
release: duration * this.toSeconds(this.release) / totalDuration, (duration * this.toSeconds(this.attack)) / totalDuration,
context decay: (duration * this.toSeconds(this.decay)) / totalDuration,
})) as Envelope; release:
(duration * this.toSeconds(this.release)) / totalDuration,
context,
})
) as Envelope;
clone._sig.toDestination(); clone._sig.toDestination();
clone.triggerAttackRelease(duration * (attackPortion + sustainTime) / totalDuration, 0); clone.triggerAttackRelease(
(duration * (attackPortion + sustainTime)) / totalDuration,
0
);
const buffer = await context.render(); const buffer = await context.render();
return buffer.getChannelData(0); return buffer.getChannelData(0);
} }
@ -529,7 +573,6 @@ type EnvelopeCurveName = keyof EnvelopeCurveMap;
* Generate some complex envelope curves. * Generate some complex envelope curves.
*/ */
const EnvelopeCurves: EnvelopeCurveMap = (() => { const EnvelopeCurves: EnvelopeCurveMap = (() => {
const curveLen = 128; const curveLen = 128;
let i: number; let i: number;
@ -545,8 +588,9 @@ const EnvelopeCurves: EnvelopeCurveMap = (() => {
const rippleCurve: number[] = []; const rippleCurve: number[] = [];
const rippleCurveFreq = 6.4; const rippleCurveFreq = 6.4;
for (i = 0; i < curveLen - 1; i++) { for (i = 0; i < curveLen - 1; i++) {
k = (i / (curveLen - 1)); k = i / (curveLen - 1);
const sineWave = Math.sin(k * (Math.PI * 2) * rippleCurveFreq - Math.PI / 2) + 1; const sineWave =
Math.sin(k * (Math.PI * 2) * rippleCurveFreq - Math.PI / 2) + 1;
rippleCurve[i] = sineWave / 10 + k * 0.83; rippleCurve[i] = sineWave / 10 + k * 0.83;
} }
rippleCurve[curveLen - 1] = 1; rippleCurve[curveLen - 1] = 1;

View file

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

View file

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

View file

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

View file

@ -1,8 +1,11 @@
import { ToneAudioNode, ToneAudioNodeOptions } from "../../core/context/ToneAudioNode"; import {
import { Cents, Frequency, GainFactor } from "../../core/type/Units"; ToneAudioNode,
import { optionsFromArguments } from "../../core/util/Defaults"; ToneAudioNodeOptions,
import { Param } from "../../core/context/Param"; } from "../../core/context/ToneAudioNode.js";
import { assert } from "../../core/util/Debug"; import { Cents, Frequency, GainFactor } from "../../core/type/Units.js";
import { optionsFromArguments } from "../../core/util/Defaults.js";
import { Param } from "../../core/context/Param.js";
import { assert } from "../../core/util/Debug.js";
export interface BiquadFilterOptions extends ToneAudioNodeOptions { export interface BiquadFilterOptions extends ToneAudioNodeOptions {
frequency: Frequency; frequency: Frequency;
@ -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). * 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. * BiquadFilter is similar to {@link Filter} but doesn't have the option to set the "rolloff" value.
* @category Component * @category Component
*/ */
export class BiquadFilter extends ToneAudioNode<BiquadFilterOptions> { export class BiquadFilter extends ToneAudioNode<BiquadFilterOptions> {
@ -32,13 +35,13 @@ export class BiquadFilter extends ToneAudioNode<BiquadFilterOptions> {
* A detune value, in cents, for the frequency. * A detune value, in cents, for the frequency.
*/ */
readonly detune: Param<"cents">; readonly detune: Param<"cents">;
/** /**
* The Q factor of the filter. * 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 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. * 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 * 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. * [0,3.4028235𝑒38], the upper limit being the most-positive-single-float.
* This is not used for the lowshelf and highshelf filters. * This is not used for the lowshelf and highshelf filters.
*/ */
@ -58,8 +61,12 @@ export class BiquadFilter extends ToneAudioNode<BiquadFilterOptions> {
constructor(frequency?: Frequency, type?: BiquadFilterType); constructor(frequency?: Frequency, type?: BiquadFilterType);
constructor(options?: Partial<BiquadFilterOptions>); constructor(options?: Partial<BiquadFilterOptions>);
constructor() { constructor() {
super(optionsFromArguments(BiquadFilter.getDefaults(), arguments, ["frequency", "type"])); const options = optionsFromArguments(
const options = optionsFromArguments(BiquadFilter.getDefaults(), arguments, ["frequency", "type"]); BiquadFilter.getDefaults(),
arguments,
["frequency", "type"]
);
super(options);
this._filter = this.context.createBiquadFilter(); this._filter = this.context.createBiquadFilter();
this.input = this.output = this._filter; this.input = this.output = this._filter;
@ -70,21 +77,21 @@ export class BiquadFilter extends ToneAudioNode<BiquadFilterOptions> {
value: options.Q, value: options.Q,
param: this._filter.Q, param: this._filter.Q,
}); });
this.frequency = new Param({ this.frequency = new Param({
context: this.context, context: this.context,
units: "frequency", units: "frequency",
value: options.frequency, value: options.frequency,
param: this._filter.frequency, param: this._filter.frequency,
}); });
this.detune = new Param({ this.detune = new Param({
context: this.context, context: this.context,
units: "cents", units: "cents",
value: options.detune, value: options.detune,
param: this._filter.detune, param: this._filter.detune,
}); });
this.gain = new Param({ this.gain = new Param({
context: this.context, context: this.context,
units: "decibels", units: "decibels",
@ -114,8 +121,16 @@ export class BiquadFilter extends ToneAudioNode<BiquadFilterOptions> {
return this._filter.type; return this._filter.type;
} }
set type(type) { set type(type) {
const types: BiquadFilterType[] = ["lowpass", "highpass", "bandpass", const types: BiquadFilterType[] = [
"lowshelf", "highshelf", "notch", "allpass", "peaking"]; "lowpass",
"highpass",
"bandpass",
"lowshelf",
"highshelf",
"notch",
"allpass",
"peaking",
];
assert(types.indexOf(type) !== -1, `Invalid filter type: ${type}`); assert(types.indexOf(type) !== -1, `Invalid filter type: ${type}`);
this._filter.type = type; this._filter.type = type;
} }

View file

@ -1,27 +1,20 @@
import { expect } from "chai"; import { expect } from "chai";
import { BasicTests } from "test/helper/Basic"; import { BasicTests } from "../../../test/helper/Basic.js";
import { ToneAudioBuffer } from "Tone/core/context/ToneAudioBuffer"; import { ToneAudioBuffer } from "../../core/context/ToneAudioBuffer.js";
import { Convolver } from "./Convolver"; import { Convolver } from "./Convolver.js";
// @ts-ignore
if (window.__karma__) {
ToneAudioBuffer.baseUrl = "/base/test/";
}
describe("Convolver", () => { describe("Convolver", () => {
BasicTests(Convolver); BasicTests(Convolver);
const ir = new ToneAudioBuffer(); const ir = new ToneAudioBuffer();
const testFile = "./audio/sineStereo.wav"; const testFile = "./test/audio/sineStereo.wav";
before(() => { before(() => {
return ir.load(testFile); return ir.load(testFile);
}); });
context("API", () => { context("API", () => {
it("can pass in options in the constructor", () => { it("can pass in options in the constructor", () => {
const convolver = new Convolver({ const convolver = new Convolver({
normalize: false, normalize: false,
@ -71,7 +64,9 @@ describe("Convolver", () => {
it("can be constructed with a buffer", () => { it("can be constructed with a buffer", () => {
const convolver = new Convolver(ir); 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(); convolver.dispose();
}); });

View file

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

View file

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

View file

@ -1,11 +1,14 @@
import { Gain } from "../../core/context/Gain"; import { Gain } from "../../core/context/Gain.js";
import { Param } from "../../core/context/Param"; import { Param } from "../../core/context/Param.js";
import { ToneAudioNode, ToneAudioNodeOptions } from "../../core/context/ToneAudioNode"; import {
import { Decibels, Frequency } from "../../core/type/Units"; ToneAudioNode,
import { optionsFromArguments } from "../../core/util/Defaults"; ToneAudioNodeOptions,
import { readOnly, writable } from "../../core/util/Interface"; } from "../../core/context/ToneAudioNode.js";
import { Signal } from "../../signal/Signal"; import { Decibels, Frequency } from "../../core/type/Units.js";
import { MultibandSplit } from "../channel/MultibandSplit"; import { optionsFromArguments } from "../../core/util/Defaults.js";
import { readOnly, writable } from "../../core/util/Interface.js";
import { Signal } from "../../signal/Signal.js";
import { MultibandSplit } from "../channel/MultibandSplit.js";
interface EQ3Options extends ToneAudioNodeOptions { interface EQ3Options extends ToneAudioNodeOptions {
low: Decibels; low: Decibels;
@ -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 * @category Component
*/ */
export class EQ3 extends ToneAudioNode<EQ3Options> { export class EQ3 extends ToneAudioNode<EQ3Options> {
readonly name: string = "EQ3"; readonly name: string = "EQ3";
/** /**
@ -88,8 +90,12 @@ export class EQ3 extends ToneAudioNode<EQ3Options> {
constructor(lowLevel?: Decibels, midLevel?: Decibels, highLevel?: Decibels); constructor(lowLevel?: Decibels, midLevel?: Decibels, highLevel?: Decibels);
constructor(options: Partial<EQ3Options>); constructor(options: Partial<EQ3Options>);
constructor() { constructor() {
super(optionsFromArguments(EQ3.getDefaults(), arguments, ["low", "mid", "high"])); const options = optionsFromArguments(EQ3.getDefaults(), arguments, [
const options = optionsFromArguments(EQ3.getDefaults(), arguments, ["low", "mid", "high"]); "low",
"mid",
"high",
]);
super(options);
this.input = this._multibandSplit = new MultibandSplit({ this.input = this._multibandSplit = new MultibandSplit({
context: this.context, context: this.context,
@ -120,7 +126,7 @@ export class EQ3 extends ToneAudioNode<EQ3Options> {
this.high = this._highGain.gain; this.high = this._highGain.gain;
this.Q = this._multibandSplit.Q; this.Q = this._multibandSplit.Q;
this.lowFrequency = this._multibandSplit.lowFrequency; this.lowFrequency = this._multibandSplit.lowFrequency;
this.highFrequency = this._multibandSplit.highFrequency; this.highFrequency = this._multibandSplit.highFrequency;
// the frequency bands // the frequency bands
this._multibandSplit.low.chain(this._lowGain, this.output); this._multibandSplit.low.chain(this._lowGain, this.output);
@ -159,5 +165,4 @@ export class EQ3 extends ToneAudioNode<EQ3Options> {
this.Q.dispose(); this.Q.dispose();
return this; return this;
} }
} }

View file

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

View file

@ -1,11 +1,15 @@
import { Gain } from "../../core/context/Gain"; import { Gain } from "../../core/context/Gain.js";
import { Param } from "../../core/context/Param"; import { Param } from "../../core/context/Param.js";
import { connectSeries, ToneAudioNode, ToneAudioNodeOptions } from "../../core/context/ToneAudioNode"; import {
import { NormalRange, Time } from "../../core/type/Units"; connectSeries,
import { optionsFromArguments } from "../../core/util/Defaults"; ToneAudioNode,
import { readOnly, RecursivePartial } from "../../core/util/Interface"; ToneAudioNodeOptions,
import { ToneAudioWorklet } from "../../core/worklet/ToneAudioWorklet"; } from "../../core/context/ToneAudioNode.js";
import { workletName } from "./FeedbackCombFilter.worklet"; import { NormalRange, Time } from "../../core/type/Units.js";
import { optionsFromArguments } from "../../core/util/Defaults.js";
import { readOnly, RecursivePartial } from "../../core/util/Interface.js";
import { ToneAudioWorklet } from "../../core/worklet/ToneAudioWorklet.js";
import { workletName } from "./FeedbackCombFilter.worklet.js";
export interface FeedbackCombFilterOptions extends ToneAudioNodeOptions { export interface FeedbackCombFilterOptions extends ToneAudioNodeOptions {
delayTime: Time; delayTime: Time;
@ -15,14 +19,13 @@ export interface FeedbackCombFilterOptions extends ToneAudioNodeOptions {
/** /**
* Comb filters are basic building blocks for physical modeling. Read more * 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). * 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 * 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 * 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. * AudioWorkletNode, but it will add some latency and have slower performance than the AudioWorkletNode.
* @category Component * @category Component
*/ */
export class FeedbackCombFilter extends ToneAudioWorklet<FeedbackCombFilterOptions> { export class FeedbackCombFilter extends ToneAudioWorklet<FeedbackCombFilterOptions> {
readonly name = "FeedbackCombFilter"; readonly name = "FeedbackCombFilter";
/** /**
@ -45,8 +48,12 @@ export class FeedbackCombFilter extends ToneAudioWorklet<FeedbackCombFilterOptio
constructor(delayTime?: Time, resonance?: NormalRange); constructor(delayTime?: Time, resonance?: NormalRange);
constructor(options?: RecursivePartial<FeedbackCombFilterOptions>); constructor(options?: RecursivePartial<FeedbackCombFilterOptions>);
constructor() { constructor() {
super(optionsFromArguments(FeedbackCombFilter.getDefaults(), arguments, ["delayTime", "resonance"])); const options = optionsFromArguments(
const options = optionsFromArguments(FeedbackCombFilter.getDefaults(), arguments, ["delayTime", "resonance"]); FeedbackCombFilter.getDefaults(),
arguments,
["delayTime", "resonance"]
);
super(options);
this.input = new Gain({ context: this.context }); this.input = new Gain({ context: this.context });
this.output = new Gain({ context: this.context }); this.output = new Gain({ context: this.context });

View file

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

View file

@ -1,16 +1,14 @@
import { expect } from "chai"; import { expect } from "chai";
import { BasicTests } from "test/helper/Basic"; import { BasicTests } from "../../../test/helper/Basic.js";
import { Offline } from "test/helper/Offline"; import { Offline } from "../../../test/helper/Offline.js";
import { PassAudio } from "test/helper/PassAudio"; import { PassAudio } from "../../../test/helper/PassAudio.js";
import { Oscillator } from "../../source/oscillator/Oscillator"; import { Oscillator } from "../../source/oscillator/Oscillator.js";
import { Filter, FilterRollOff } from "./Filter"; import { Filter, FilterRollOff } from "./Filter.js";
describe("Filter", () => { describe("Filter", () => {
BasicTests(Filter); BasicTests(Filter);
context("Filtering", () => { context("Filtering", () => {
it("can be constructed with a arguments", () => { it("can be constructed with a arguments", () => {
const filter = new Filter(200, "highpass"); const filter = new Filter(200, "highpass");
expect(filter.frequency.value).to.be.closeTo(200, 0.001); expect(filter.frequency.value).to.be.closeTo(200, 0.001);
@ -38,7 +36,13 @@ describe("Filter", () => {
type: "highpass" as BiquadFilterType, type: "highpass" as BiquadFilterType,
}; };
filter.set(values); 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.type).to.equal(values.type);
expect(filter.frequency.value).to.equal(values.frequency); expect(filter.frequency.value).to.equal(values.frequency);
expect(filter.rolloff).to.equal(values.rolloff); expect(filter.rolloff).to.equal(values.rolloff);
@ -59,7 +63,7 @@ describe("Filter", () => {
}); });
it("passes the incoming signal through", () => { it("passes the incoming signal through", () => {
return PassAudio(input => { return PassAudio((input) => {
const filter = new Filter().toDestination(); const filter = new Filter().toDestination();
input.connect(filter); input.connect(filter);
}); });
@ -85,8 +89,16 @@ describe("Filter", () => {
it("can set the basic filter types", () => { it("can set the basic filter types", () => {
const filter = new Filter(); const filter = new Filter();
const types: BiquadFilterType[] = ["lowpass", "highpass", const types: BiquadFilterType[] = [
"bandpass", "lowshelf", "highshelf", "notch", "allpass", "peaking"]; "lowpass",
"highpass",
"bandpass",
"lowshelf",
"highshelf",
"notch",
"allpass",
"peaking",
];
for (const type of types) { for (const type of types) {
filter.type = type; filter.type = type;
expect(filter.type).to.equal(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); expect(buffer.getRmsAtTime(0.1)).to.be.within(0.37, 0.53);
}); });
}); });
}); });
}); });

View file

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

View file

@ -1,21 +1,19 @@
import { LowpassCombFilter } from "./LowpassCombFilter"; import { LowpassCombFilter } from "./LowpassCombFilter.js";
import { BasicTests } from "test/helper/Basic"; import { BasicTests } from "../../../test/helper/Basic.js";
import { Offline } from "test/helper/Offline"; import { Offline } from "../../../test/helper/Offline.js";
import { PassAudio } from "test/helper/PassAudio"; import { PassAudio } from "../../../test/helper/PassAudio.js";
import { Oscillator } from "Tone/source/oscillator/Oscillator"; import { Oscillator } from "../../source/oscillator/Oscillator.js";
import { expect } from "chai"; import { expect } from "chai";
describe("LowpassCombFilter", () => { describe("LowpassCombFilter", () => {
BasicTests(LowpassCombFilter); BasicTests(LowpassCombFilter);
context("Comb Filtering", () => { context("Comb Filtering", () => {
it("can be constructed with an object", () => { it("can be constructed with an object", () => {
const lpcf = new LowpassCombFilter({ const lpcf = new LowpassCombFilter({
delayTime: 0.2, delayTime: 0.2,
resonance: 0.3, resonance: 0.3,
dampening: 2400 dampening: 2400,
}); });
expect(lpcf.delayTime.value).to.be.closeTo(0.2, 0.001); expect(lpcf.delayTime.value).to.be.closeTo(0.2, 0.001);
expect(lpcf.resonance.value).to.be.closeTo(0.3, 0.001); expect(lpcf.resonance.value).to.be.closeTo(0.3, 0.001);
@ -28,7 +26,7 @@ describe("LowpassCombFilter", () => {
lpcf.set({ lpcf.set({
delayTime: 0.2, delayTime: 0.2,
resonance: 0.3, resonance: 0.3,
dampening: 2000 dampening: 2000,
}); });
expect(lpcf.get().delayTime).to.be.closeTo(0.2, 0.001); expect(lpcf.get().delayTime).to.be.closeTo(0.2, 0.001);
expect(lpcf.get().resonance).to.be.closeTo(0.3, 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", () => { it("produces a decay signal at high resonance", () => {
return Offline(() => { 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); const burst = new Oscillator(440).connect(lpcf);
burst.start(0); burst.start(0);
burst.stop(0.1); burst.stop(0.1);

View file

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

View file

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

View file

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

View file

@ -1,17 +1,15 @@
import { BasicTests } from "test/helper/Basic"; import { BasicTests } from "../../../test/helper/Basic.js";
import { CompareToFile } from "test/helper/CompareToFile"; import { CompareToFile } from "../../../test/helper/CompareToFile.js";
import { connectTo } from "test/helper/Connect"; import { connectTo } from "../../../test/helper/Connect.js";
import { PassAudio } from "test/helper/PassAudio"; import { PassAudio } from "../../../test/helper/PassAudio.js";
import { connect } from "Tone/core/context/ToneAudioNode"; import { connect } from "../../core/context/ToneAudioNode.js";
import { Subtract } from "Tone/signal/Subtract"; import { Subtract } from "../../signal/Subtract.js";
import { PhaseShiftAllpass } from "./PhaseShiftAllpass"; import { PhaseShiftAllpass } from "./PhaseShiftAllpass.js";
describe("PhaseShiftAllpass", () => { describe("PhaseShiftAllpass", () => {
BasicTests(PhaseShiftAllpass); BasicTests(PhaseShiftAllpass);
context("PhaseShiftAllpass", () => { context("PhaseShiftAllpass", () => {
it("handles output connections", () => { it("handles output connections", () => {
const phaseShifter = new PhaseShiftAllpass(); const phaseShifter = new PhaseShiftAllpass();
phaseShifter.connect(connectTo()); phaseShifter.connect(connectTo());
@ -27,45 +25,63 @@ describe("PhaseShiftAllpass", () => {
}); });
it("generates correct values with the phase shifted channel", () => { it("generates correct values with the phase shifted channel", () => {
return CompareToFile((context) => { return CompareToFile(
// create impulse with 5 samples offset (context) => {
const constantNode = context.createConstantSource(); // create impulse with 5 samples offset
constantNode.start(0); const constantNode = context.createConstantSource();
const oneSampleDelay = context.createIIRFilter([0.0, 1.0], [1.0, 0.0]); constantNode.start(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 oneSampleDelay = context.createIIRFilter(
const sub = new Subtract(); [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, oneSampleDelay);
connect(constantNode, sub); connect(constantNode, sub);
connect(oneSampleDelay, sub.subtrahend); connect(oneSampleDelay, sub.subtrahend);
connect(sub, fiveSampleDelay); connect(sub, fiveSampleDelay);
const phaseShifter = new PhaseShiftAllpass(); const phaseShifter = new PhaseShiftAllpass();
connect(fiveSampleDelay, phaseShifter); connect(fiveSampleDelay, phaseShifter);
phaseShifter.toDestination(); phaseShifter.toDestination();
},
}, "phaseShiftAllpass.wav", 0.001); "phaseShiftAllpass.wav",
0.001
);
}); });
it("generates correct values with the offset90 channel", () => { it("generates correct values with the offset90 channel", () => {
return CompareToFile((context) => { return CompareToFile(
// create impulse with 5 samples offset (context) => {
const constantNode = context.createConstantSource(); // create impulse with 5 samples offset
constantNode.start(0); const constantNode = context.createConstantSource();
const oneSampleDelay = context.createIIRFilter([0.0, 1.0], [1.0, 0.0]); constantNode.start(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 oneSampleDelay = context.createIIRFilter(
const sub = new Subtract(); [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, oneSampleDelay);
connect(constantNode, sub); connect(constantNode, sub);
connect(oneSampleDelay, sub.subtrahend); connect(oneSampleDelay, sub.subtrahend);
connect(sub, fiveSampleDelay); connect(sub, fiveSampleDelay);
const phaseShifter = new PhaseShiftAllpass(); const phaseShifter = new PhaseShiftAllpass();
connect(fiveSampleDelay, phaseShifter); connect(fiveSampleDelay, phaseShifter);
phaseShifter.offset90.toDestination(); phaseShifter.offset90.toDestination();
},
}, "phaseShiftAllpass1.wav", 0.001); "phaseShiftAllpass1.wav",
0.001
);
}); });
}); });
}); });

View file

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

View file

@ -1,39 +1,39 @@
export * from "./analysis/Analyser"; export * from "./analysis/Analyser.js";
export * from "./analysis/Meter"; export * from "./analysis/Meter.js";
export * from "./analysis/FFT"; export * from "./analysis/FFT.js";
export * from "./analysis/DCMeter"; export * from "./analysis/DCMeter.js";
export * from "./analysis/Waveform"; export * from "./analysis/Waveform.js";
export * from "./analysis/Follower"; export * from "./analysis/Follower.js";
export * from "./channel/Channel"; export * from "./channel/Channel.js";
export * from "./channel/CrossFade"; export * from "./channel/CrossFade.js";
export * from "./channel/Merge"; export * from "./channel/Merge.js";
export * from "./channel/MidSideMerge"; export * from "./channel/MidSideMerge.js";
export * from "./channel/MidSideSplit"; export * from "./channel/MidSideSplit.js";
export * from "./channel/Mono"; export * from "./channel/Mono.js";
export * from "./channel/MultibandSplit"; export * from "./channel/MultibandSplit.js";
export * from "./channel/Panner"; export * from "./channel/Panner.js";
export * from "./channel/Panner3D"; export * from "./channel/Panner3D.js";
export * from "./channel/PanVol"; export * from "./channel/PanVol.js";
export * from "./channel/Recorder"; export * from "./channel/Recorder.js";
export * from "./channel/Solo"; export * from "./channel/Solo.js";
export * from "./channel/Split"; export * from "./channel/Split.js";
export * from "./channel/Volume"; export * from "./channel/Volume.js";
export * from "./dynamics/Compressor"; export * from "./dynamics/Compressor.js";
export * from "./dynamics/Gate"; export * from "./dynamics/Gate.js";
export * from "./dynamics/Limiter"; export * from "./dynamics/Limiter.js";
export * from "./dynamics/MidSideCompressor"; export * from "./dynamics/MidSideCompressor.js";
export * from "./dynamics/MultibandCompressor"; export * from "./dynamics/MultibandCompressor.js";
export * from "./envelope/AmplitudeEnvelope"; export * from "./envelope/AmplitudeEnvelope.js";
export * from "./envelope/Envelope"; export * from "./envelope/Envelope.js";
export * from "./envelope/FrequencyEnvelope"; export * from "./envelope/FrequencyEnvelope.js";
export * from "./filter/EQ3"; export * from "./filter/EQ3.js";
export * from "./filter/Filter"; export * from "./filter/Filter.js";
export * from "./filter/OnePoleFilter"; export * from "./filter/OnePoleFilter.js";
export * from "./filter/FeedbackCombFilter"; export * from "./filter/FeedbackCombFilter.js";
export * from "./filter/LowpassCombFilter"; export * from "./filter/LowpassCombFilter.js";
export * from "./filter/Convolver"; export * from "./filter/Convolver.js";
export * from "./filter/BiquadFilter"; export * from "./filter/BiquadFilter.js";

View file

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

View file

@ -2,27 +2,26 @@
* Tone.js * Tone.js
* @author Yotam Mann * @author Yotam Mann
* @license http://opensource.org/licenses/MIT MIT License * @license http://opensource.org/licenses/MIT MIT License
* @copyright 2014-2019 Yotam Mann * @copyright 2014-2024 Yotam Mann
*/ */
import { version } from "../version"; import { version } from "../version.js";
import { theWindow } from "./context/AudioContext"; import { theWindow } from "./context/AudioContext.js";
import { log } from "./util/Debug"; import { log } from "./util/Debug.js";
//------------------------------------- //-------------------------------------
// TONE // TONE
//------------------------------------- //-------------------------------------
// eslint-disable-next-line @typescript-eslint/no-empty-interface // eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface BaseToneOptions { } export interface BaseToneOptions {}
/** /**
* Tone is the base class of all other classes. * Tone is the base class of all other classes.
* *
* @category Core * @category Core
* @constructor * @constructor
*/ */
export abstract class Tone { export abstract class Tone {
/** /**
* The version number semver * The version number semver
*/ */
@ -64,7 +63,10 @@ export abstract class Tone {
protected log(...args: any[]): void { protected log(...args: any[]): void {
// if the object is either set to debug = true // if the object is either set to debug = true
// or if there is a string on the Tone.global.with the class name // 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); log(this, ...args);
} }
} }

View file

@ -1,16 +1,13 @@
import { expect } from "chai"; import { expect } from "chai";
import { BasicTests } from "test/helper/Basic"; import { BasicTests } from "../../../test/helper/Basic.js";
import { atTime, Offline, whenBetween } from "test/helper/Offline"; import { atTime, Offline, whenBetween } from "../../../test/helper/Offline.js";
import { ONLINE_TESTING } from "test/helper/Supports"; import { noOp } from "../util/Interface.js";
import { noOp } from "../util/Interface"; import { Clock } from "./Clock.js";
import { Clock } from "./Clock";
describe("Clock", () => { describe("Clock", () => {
BasicTests(Clock); BasicTests(Clock);
context("Get/Set values", () => { context("Get/Set values", () => {
it("can get and set the frequency", () => { it("can get and set the frequency", () => {
const clock = new Clock(noOp, 2); const clock = new Clock(noOp, 2);
expect(clock.frequency.value).to.equal(2); expect(clock.frequency.value).to.equal(2);
@ -19,27 +16,23 @@ describe("Clock", () => {
clock.dispose(); clock.dispose();
}); });
if (ONLINE_TESTING) { it("invokes the callback when started", (done) => {
const clock = new Clock((time) => {
clock.dispose();
done();
}, 10).start();
});
it("invokes the callback when started", (done) => { it("can be constructed with an options object", (done) => {
const clock = new Clock((time) => { const clock = new Clock({
callback(): void {
clock.dispose(); clock.dispose();
done(); done();
}, 10).start(); },
}); frequency: 8,
}).start();
it("can be constructed with an options object", (done) => { expect(clock.frequency.value).to.equal(8);
const clock = new Clock({ });
callback(): void {
clock.dispose();
done();
},
frequency: 8,
}).start();
expect(clock.frequency.value).to.equal(8);
});
}
it("can get and set it's values with the set/get", () => { it("can get and set it's values with the set/get", () => {
const clock = new Clock(); const clock = new Clock();
@ -53,7 +46,6 @@ describe("Clock", () => {
}); });
context("State", () => { context("State", () => {
it("correctly returns the scheduled play state", () => { it("correctly returns the scheduled play state", () => {
return Offline(() => { return Offline(() => {
const clock = new Clock(); const clock = new Clock();
@ -93,7 +85,6 @@ describe("Clock", () => {
expect(clock.state).to.equal("stopped"); expect(clock.state).to.equal("stopped");
}); });
}; };
}, 0.5); }, 0.5);
}); });
@ -137,44 +128,39 @@ describe("Clock", () => {
expect(clock.state).to.equal("started"); expect(clock.state).to.equal("started");
}); });
}; };
}, 0.5); }, 0.5);
}); });
}); });
context("Scheduling", () => { context("Scheduling", () => {
it("passes a time to the callback", (done) => {
const clock = new Clock((time) => {
expect(time).to.be.a("number");
clock.dispose();
done();
}, 10).start();
});
if (ONLINE_TESTING) { it("invokes the callback with a time great than now", (done) => {
const clock = new Clock((time) => {
clock.dispose();
expect(time).to.be.greaterThan(now);
done();
}, 10);
const now = clock.now();
const startTime = now + 0.1;
clock.start(startTime);
});
it("passes a time to the callback", (done) => { it("invokes the first callback at the given start time", (done) => {
const clock = new Clock((time) => { const clock = new Clock((time) => {
expect(time).to.be.a("number"); clock.dispose();
clock.dispose(); expect(time).to.be.closeTo(startTime, 0.01);
done(); done();
}, 10).start(); }, 10);
}); const startTime = clock.now() + 0.1;
clock.start(startTime);
it("invokes the callback with a time great than now", (done) => { });
const clock = new Clock((time) => {
clock.dispose();
expect(time).to.be.greaterThan(now);
done();
}, 10);
const now = clock.now();
const startTime = now + 0.1;
clock.start(startTime);
});
it("invokes the first callback at the given start time", (done) => {
const clock = new Clock((time) => {
clock.dispose();
expect(time).to.be.closeTo(startTime, 0.01);
done();
}, 10);
const startTime = clock.now() + 0.1;
clock.start(startTime);
});
}
it("can be scheduled to start in the future", () => { it("can be scheduled to start in the future", () => {
let invokations = 0; let invokations = 0;
@ -192,7 +178,9 @@ describe("Clock", () => {
return Offline(() => { return Offline(() => {
new Clock((time) => { new Clock((time) => {
invokations++; invokations++;
}, 10).start(0).stop(0.45); }, 10)
.start(0)
.stop(0.45);
}, 0.6).then(() => { }, 0.6).then(() => {
expect(invokations).to.equal(5); expect(invokations).to.equal(5);
}); });
@ -210,11 +198,9 @@ describe("Clock", () => {
expect(invokations).to.equal(4); expect(invokations).to.equal(4);
}); });
}); });
}); });
context("Seconds", () => { context("Seconds", () => {
it("can set the current seconds", () => { it("can set the current seconds", () => {
return Offline(() => { return Offline(() => {
const clock = new Clock(noOp, 10); const clock = new Clock(noOp, 10);
@ -274,7 +260,6 @@ describe("Clock", () => {
}); });
context("Ticks", () => { context("Ticks", () => {
it("has 0 ticks when first created", () => { it("has 0 ticks when first created", () => {
const clock = new Clock(); const clock = new Clock();
expect(clock.ticks).to.equal(0); expect(clock.ticks).to.equal(0);
@ -332,9 +317,11 @@ describe("Clock", () => {
}); });
it("starts incrementing where it left off after pause", () => { it("starts incrementing where it left off after pause", () => {
return Offline(() => { 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 pausedTicks = 0;
let tested = false; let tested = false;
@ -369,11 +356,9 @@ describe("Clock", () => {
clock.start(0, 4); clock.start(0, 4);
}); });
}); });
}); });
context("Events", () => { context("Events", () => {
it("triggers the start event on start", (done) => { it("triggers the start event on start", (done) => {
Offline(() => { Offline(() => {
const clock = new Clock(noOp, 20); const clock = new Clock(noOp, 20);
@ -415,12 +400,14 @@ describe("Clock", () => {
it("triggers pause stop event", (done) => { it("triggers pause stop event", (done) => {
Offline(() => { Offline(() => {
const clock = new Clock(noOp, 20); const clock = new Clock(noOp, 20);
clock.on("pause", (time) => { clock
expect(time).to.be.closeTo(0.1, 0.05); .on("pause", (time) => {
}).on("stop", (time) => { expect(time).to.be.closeTo(0.1, 0.05);
expect(time).to.be.closeTo(0.2, 0.05); })
done(); .on("stop", (time) => {
}); expect(time).to.be.closeTo(0.2, 0.05);
done();
});
clock.start().pause(0.1).stop(0.2); clock.start().pause(0.1).stop(0.2);
}, 0.4); }, 0.4);
}); });
@ -481,7 +468,6 @@ describe("Clock", () => {
}); });
context("[get/set]Ticks", () => { context("[get/set]Ticks", () => {
it("always reports 0 if not started", () => { it("always reports 0 if not started", () => {
return Offline(() => { return Offline(() => {
const clock = new Clock(noOp, 20); const clock = new Clock(noOp, 20);
@ -657,7 +643,5 @@ describe("Clock", () => {
clock.dispose(); clock.dispose();
}); });
}); });
}); });
}); });

View file

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

View file

@ -1,15 +1,11 @@
import { Compare, Plot } from "@tonejs/plot"; import { BasicTests, testAudioContext } from "../../../test/helper/Basic.js";
import { expect } from "chai"; // import { atTime, Offline } from "../../../test/helper/Offline";
import { BasicTests, testAudioContext } from "test/helper/Basic"; import { TickParam } from "./TickParam.js";
// import { atTime, Offline } from "test/helper/Offline";
import { TickParam } from "./TickParam";
describe("TickParam", () => { describe("TickParam", () => {
// sanity checks // sanity checks
BasicTests(TickParam, { BasicTests(TickParam, {
context: testAudioContext, context: testAudioContext,
param: testAudioContext.createOscillator().frequency, param: testAudioContext.createOscillator().frequency,
}); });
}); });

View file

@ -1,14 +1,15 @@
import { AutomationEvent, Param, ParamOptions } from "../context/Param"; import { AutomationEvent, Param, ParamOptions } from "../context/Param.js";
import { Seconds, Ticks, Time, UnitMap, UnitName } from "../type/Units"; import { Seconds, Ticks, Time, UnitMap, UnitName } from "../type/Units.js";
import { optionsFromArguments } from "../util/Defaults"; import { optionsFromArguments } from "../util/Defaults.js";
import { Timeline } from "../util/Timeline"; import { Timeline } from "../util/Timeline.js";
import { isUndef } from "../util/TypeCheck"; import { isUndef } from "../util/TypeCheck.js";
type TickAutomationEvent = AutomationEvent & { type TickAutomationEvent = AutomationEvent & {
ticks: number; ticks: number;
}; };
interface TickParamOptions<TypeName extends UnitName> extends ParamOptions<TypeName> { interface TickParamOptions<TypeName extends UnitName>
extends ParamOptions<TypeName> {
multiplier: number; 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 * but offers conversion to BPM values as well as ability to compute tick
* duration and elapsed ticks * 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"; readonly name: string = "TickParam";
/** /**
@ -42,9 +44,12 @@ export class TickParam<TypeName extends "hertz" | "bpm"> extends Param<TypeName>
constructor(value?: number); constructor(value?: number);
constructor(options: Partial<TickParamOptions<TypeName>>); constructor(options: Partial<TickParamOptions<TypeName>>);
constructor() { constructor() {
const options = optionsFromArguments(
super(optionsFromArguments(TickParam.getDefaults(), arguments, ["value"])); TickParam.getDefaults(),
const options = optionsFromArguments(TickParam.getDefaults(), arguments, ["value"]); arguments,
["value"]
);
super(options);
// set the multiplier // set the multiplier
this._multiplier = options.multiplier; this._multiplier = options.multiplier;
@ -69,7 +74,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 // approximate it with multiple linear ramps
time = this.toSeconds(time); time = this.toSeconds(time);
this.setRampPoint(time); this.setRampPoint(time);
@ -80,7 +89,13 @@ export class TickParam<TypeName extends "hertz" | "bpm"> extends Param<TypeName>
const segments = Math.round(Math.max(1 / constant, 1)); const segments = Math.round(Math.max(1 / constant, 1));
for (let i = 0; i <= segments; i++) { for (let i = 0; i <= segments; i++) {
const segTime = constant * i + time; 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); this.linearRampToValueAtTime(this._toType(rampVal), segTime);
} }
return this; return this;
@ -91,7 +106,10 @@ export class TickParam<TypeName extends "hertz" | "bpm"> extends Param<TypeName>
super.setValueAtTime(value, time); super.setValueAtTime(value, time);
const event = this._events.get(computedTime) as TickAutomationEvent; const event = this._events.get(computedTime) as TickAutomationEvent;
const previousEvent = this._events.previousEvent(event); const previousEvent = this._events.previousEvent(event);
const ticksUntilTime = this._getTicksUntilEvent(previousEvent, computedTime); const ticksUntilTime = this._getTicksUntilEvent(
previousEvent,
computedTime
);
event.ticks = Math.max(ticksUntilTime, 0); event.ticks = Math.max(ticksUntilTime, 0);
return this; return this;
} }
@ -101,7 +119,10 @@ export class TickParam<TypeName extends "hertz" | "bpm"> extends Param<TypeName>
super.linearRampToValueAtTime(value, time); super.linearRampToValueAtTime(value, time);
const event = this._events.get(computedTime) as TickAutomationEvent; const event = this._events.get(computedTime) as TickAutomationEvent;
const previousEvent = this._events.previousEvent(event); const previousEvent = this._events.previousEvent(event);
const ticksUntilTime = this._getTicksUntilEvent(previousEvent, computedTime); const ticksUntilTime = this._getTicksUntilEvent(
previousEvent,
computedTime
);
event.ticks = Math.max(ticksUntilTime, 0); event.ticks = Math.max(ticksUntilTime, 0);
return this; return this;
} }
@ -115,10 +136,16 @@ export class TickParam<TypeName extends "hertz" | "bpm"> extends Param<TypeName>
const prevEvent = this._events.get(time) as TickAutomationEvent; const prevEvent = this._events.get(time) as TickAutomationEvent;
// approx 10 segments per second // approx 10 segments per second
const segments = Math.round(Math.max((time - prevEvent.time) * 10, 1)); 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++) { for (let i = 0; i <= segments; i++) {
const segTime = segmentDur * i + prevEvent.time; 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); this.linearRampToValueAtTime(this._toType(rampVal), segTime);
} }
return this; return this;
@ -130,7 +157,10 @@ export class TickParam<TypeName extends "hertz" | "bpm"> extends Param<TypeName>
* @param event The time to get the tick count at * @param event The time to get the tick count at
* @return The number of ticks which have elapsed at the time given any automations. * @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) { if (event === null) {
event = { event = {
ticks: 0, ticks: 0,
@ -146,7 +176,11 @@ export class TickParam<TypeName extends "hertz" | "bpm"> extends Param<TypeName>
let val1 = this._fromType(this.getValueAtTime(time)); let val1 = this._fromType(this.getValueAtTime(time));
// if it's right on the line, take the previous value // if it's right on the line, take the previous value
const onTheLineEvent = this._events.get(time); 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)); val1 = this._fromType(this.getValueAtTime(time - this.sampleTime));
} }
return 0.5 * (time - event.time) * (val0 + val1) + event.ticks; return 0.5 * (time - event.time) * (val0 + val1) + event.ticks;
@ -185,13 +219,18 @@ export class TickParam<TypeName extends "hertz" | "bpm"> extends Param<TypeName>
const after = this._events.getAfter(tick, "ticks"); const after = this._events.getAfter(tick, "ticks");
if (before && before.ticks === tick) { if (before && before.ticks === tick) {
return before.time; return before.time;
} else if (before && after && } else if (
before &&
after &&
after.type === "linearRampToValueAtTime" && after.type === "linearRampToValueAtTime" &&
before.value !== after.value) { before.value !== after.value
) {
const val0 = this._fromType(this.getValueAtTime(before.time)); const val0 = this._fromType(this.getValueAtTime(before.time));
const val1 = this._fromType(this.getValueAtTime(after.time)); const val1 = this._fromType(this.getValueAtTime(after.time));
const delta = (val1 - val0) / (after.time - before.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 sol1 = (-val0 + k) / delta;
const sol2 = (-val0 - k) / delta; const sol2 = (-val0 - k) / delta;
return (sol1 > 0 ? sol1 : sol2) + before.time; return (sol1 > 0 ? sol1 : sol2) + before.time;
@ -249,7 +288,7 @@ export class TickParam<TypeName extends "hertz" | "bpm"> extends Param<TypeName>
*/ */
protected _toType(val: number): UnitMap[TypeName] { protected _toType(val: number): UnitMap[TypeName] {
if (this.units === "bpm" && this.multiplier) { if (this.units === "bpm" && this.multiplier) {
return (val / this.multiplier) * 60 as UnitMap[TypeName]; return ((val / this.multiplier) * 60) as UnitMap[TypeName];
} else { } else {
return super._toType(val); return super._toType(val);
} }

View file

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

View file

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

View file

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

View file

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

View file

@ -1,9 +1,7 @@
import { expect } from "chai"; import { expect } from "chai";
import { ONLINE_TESTING } from "test/helper/Supports"; import { Ticker } from "./Ticker.js";
import { Ticker } from "./Ticker";
describe("Ticker", () => { describe("Ticker", () => {
function empty(): void { function empty(): void {
// do nothing // do nothing
} }
@ -31,56 +29,69 @@ describe("Ticker", () => {
ticker.dispose(); ticker.dispose();
}); });
if (ONLINE_TESTING) { context("timeout", () => {
it("provides a callback when set to timeout", (done) => {
context("timeout", () => { const ticker = new Ticker(
() => {
it("provides a callback when set to timeout", done => {
const ticker = new Ticker(() => {
ticker.dispose(); ticker.dispose();
done(); done();
}, "timeout", 0.01); },
}); "timeout",
0.01
it("can adjust the interval when set to timeout", (done) => { );
const ticker = new Ticker(() => {
ticker.dispose();
done();
}, "timeout", 0.01);
ticker.updateInterval = 0.1;
});
}); });
}
it("can adjust the interval when set to timeout", (done) => {
const ticker = new Ticker(
() => {
ticker.dispose();
done();
},
"timeout",
0.01
);
ticker.updateInterval = 0.1;
});
});
context("worker", () => { context("worker", () => {
it("provides a callback when set to worker", (done) => {
it("provides a callback when set to worker", done => { const ticker = new Ticker(
const ticker = new Ticker(() => { () => {
ticker.dispose(); ticker.dispose();
done(); done();
}, "worker", 0.01); },
"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; const URL = window.URL;
// @ts-ignore // @ts-ignore
window.URL = null; window.URL = null;
const ticker = new Ticker(() => { const ticker = new Ticker(
expect(ticker.type).to.equal("timeout"); () => {
ticker.dispose(); expect(ticker.type).to.equal("timeout");
window.URL = URL; ticker.dispose();
done(); window.URL = URL;
}, "worker", 0.01); done();
},
"worker",
0.01
);
}); });
it("can adjust the interval when set to worker", (done) => { it("can adjust the interval when set to worker", (done) => {
const ticker = new Ticker(() => { const ticker = new Ticker(
ticker.dispose(); () => {
done(); ticker.dispose();
}, "worker", 0.01); done();
},
"worker",
0.01
);
ticker.updateInterval = 0.1; ticker.updateInterval = 0.1;
}); });
}); });
}); });

View file

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

View file

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

View file

@ -1,20 +1,20 @@
import { TimeClass } from "../../core/type/Time"; import { TimeClass } from "../../core/type/Time.js";
import { PlaybackState } from "../../core/util/StateTimeline"; import { PlaybackState } from "../../core/util/StateTimeline.js";
import { TimelineValue } from "../../core/util/TimelineValue"; import { TimelineValue } from "../../core/util/TimelineValue.js";
import { ToneAudioNode } from "../../core/context/ToneAudioNode"; import { ToneAudioNode } from "../../core/context/ToneAudioNode.js";
import { Pow } from "../../signal/Pow"; import { Pow } from "../../signal/Pow.js";
import { Signal } from "../../signal/Signal"; import { Signal } from "../../signal/Signal.js";
import { import {
onContextClose, onContextClose,
onContextInit, onContextInit,
} from "../context/ContextInitialization"; } from "../context/ContextInitialization.js";
import { Gain } from "../context/Gain"; import { Gain } from "../context/Gain.js";
import { import {
ToneWithContext, ToneWithContext,
ToneWithContextOptions, ToneWithContextOptions,
} from "../context/ToneWithContext"; } from "../context/ToneWithContext.js";
import { TicksClass } from "../type/Ticks"; import { TicksClass } from "../type/Ticks.js";
import { TransportTimeClass } from "../type/TransportTime"; import { TransportTimeClass } from "../type/TransportTime.js";
import { import {
BarsBeatsSixteenths, BarsBeatsSixteenths,
BPM, BPM,
@ -25,18 +25,18 @@ import {
Time, Time,
TimeSignature, TimeSignature,
TransportTime, TransportTime,
} from "../type/Units"; } from "../type/Units.js";
import { enterScheduledCallback } from "../util/Debug"; import { enterScheduledCallback } from "../util/Debug.js";
import { optionsFromArguments } from "../util/Defaults"; import { optionsFromArguments } from "../util/Defaults.js";
import { Emitter } from "../util/Emitter"; import { Emitter } from "../util/Emitter.js";
import { readOnly, writable } from "../util/Interface"; import { readOnly, writable } from "../util/Interface.js";
import { IntervalTimeline } from "../util/IntervalTimeline"; import { IntervalTimeline } from "../util/IntervalTimeline.js";
import { Timeline } from "../util/Timeline"; import { Timeline } from "../util/Timeline.js";
import { isArray, isDefined } from "../util/TypeCheck"; import { isArray, isDefined } from "../util/TypeCheck.js";
import { Clock } from "./Clock"; import { Clock } from "./Clock.js";
import { TickParam } from "./TickParam"; import { TickParam } from "./TickParam.js";
import { TransportEvent } from "./TransportEvent"; import { TransportEvent } from "./TransportEvent.js";
import { TransportRepeatEvent } from "./TransportRepeatEvent"; import { TransportRepeatEvent } from "./TransportRepeatEvent.js";
interface TransportOptions extends ToneWithContextOptions { interface TransportOptions extends ToneWithContextOptions {
bpm: BPM; bpm: BPM;
@ -89,7 +89,8 @@ type TransportCallback = (time: Seconds) => void;
*/ */
export class TransportClass export class TransportClass
extends ToneWithContext<TransportOptions> extends ToneWithContext<TransportOptions>
implements Emitter<TransportEventNames> { implements Emitter<TransportEventNames>
{
readonly name: string = "Transport"; readonly name: string = "Transport";
//------------------------------------- //-------------------------------------
@ -187,11 +188,11 @@ export class TransportClass
constructor(options?: Partial<TransportOptions>); constructor(options?: Partial<TransportOptions>);
constructor() { constructor() {
super(optionsFromArguments(TransportClass.getDefaults(), arguments));
const options = optionsFromArguments( const options = optionsFromArguments(
TransportClass.getDefaults(), TransportClass.getDefaults(),
arguments arguments
); );
super(options);
// CLOCK/TEMPO // CLOCK/TEMPO
this._ppq = options.ppq; this._ppq = options.ppq;
@ -366,7 +367,10 @@ export class TransportClass
* timeline it was added to. * timeline it was added to.
* @returns the event id which was just added * @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()] = { this._scheduledEvents[event.id.toString()] = {
event, event,
timeline, timeline,
@ -625,7 +629,10 @@ export class TransportClass
if (this.state === "started") { if (this.state === "started") {
const ticks = this._clock.getTicksAtTime(now); const ticks = this._clock.getTicksAtTime(now);
// schedule to start on the next tick, #573 // 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; const time = now + remainingTick;
this.emit("stop", time); this.emit("stop", time);
this._clock.setTicksAtTime(t, time); this._clock.setTicksAtTime(t, time);
@ -710,9 +717,9 @@ export class TransportClass
*/ */
syncSignal(signal: Signal<any>, ratio?: number): this { syncSignal(signal: Signal<any>, ratio?: number): this {
const now = this.now(); 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 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 // If the signal is in the time domain, sync it to the reciprocal of
// the tempo instead of the tempo. // the tempo instead of the tempo.
if (signal.units === "time") { if (signal.units === "time") {

View file

@ -1,10 +1,9 @@
import { expect } from "chai"; import { expect } from "chai";
import { Offline } from "test/helper/Offline"; import { Offline } from "../../../test/helper/Offline.js";
import { TransportClass } from "./Transport"; import { TransportClass } from "./Transport.js";
import { TransportEvent } from "./TransportEvent"; import { TransportEvent } from "./TransportEvent.js";
describe("TransportEvent", () => { describe("TransportEvent", () => {
it("can be created and disposed", () => { it("can be created and disposed", () => {
return Offline((context) => { return Offline((context) => {
const transport = new TransportClass({ context }); const transport = new TransportClass({ context });

View file

@ -1,7 +1,6 @@
import { Seconds, Ticks } from "../type/Units"; import { Seconds, Ticks } from "../type/Units.js";
import { noOp } from "../util/Interface"; import { noOp } from "../util/Interface.js";
import type { TransportClass as Transport } from "./Transport.js";
type Transport = import("../clock/Transport").TransportClass;
export interface TransportEventOptions { export interface TransportEventOptions {
callback: (time: number) => void; callback: (time: number) => void;
@ -15,7 +14,6 @@ export interface TransportEventOptions {
* handled from within Tone.Transport. * handled from within Tone.Transport.
*/ */
export class TransportEvent { export class TransportEvent {
/** /**
* Reference to the Transport that created it * Reference to the Transport that created it
*/ */
@ -43,7 +41,7 @@ export class TransportEvent {
/** /**
* The remaining value between the passed in time, and Math.floor(time). * 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; protected _remainderTime = 0;
@ -51,8 +49,10 @@ export class TransportEvent {
* @param transport The transport object which the event belongs to * @param transport The transport object which the event belongs to
*/ */
constructor(transport: Transport, opts: Partial<TransportEventOptions>) { constructor(transport: Transport, opts: Partial<TransportEventOptions>) {
const options: TransportEventOptions = Object.assign(
const options: TransportEventOptions = Object.assign(TransportEvent.getDefaults(), opts); TransportEvent.getDefaults(),
opts
);
this.transport = transport; this.transport = transport;
this.callback = options.callback; this.callback = options.callback;

View file

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

View file

@ -1,10 +1,9 @@
import { BaseContext } from "../context/BaseContext"; import { BaseContext } from "../context/BaseContext.js";
import { TicksClass } from "../type/Ticks"; import { TicksClass } from "../type/Ticks.js";
import { Seconds, Ticks, Time } from "../type/Units"; import { Seconds, Ticks, Time } from "../type/Units.js";
import { TransportEvent, TransportEventOptions } from "./TransportEvent"; import { TransportEvent, TransportEventOptions } from "./TransportEvent.js";
import { GT, LT } from "../util/Math"; import { GT, LT } from "../util/Math.js";
import type { TransportClass as Transport } from "./Transport.js";
type Transport = import("../clock/Transport").TransportClass;
interface TransportRepeatEventOptions extends TransportEventOptions { interface TransportRepeatEventOptions extends TransportEventOptions {
interval: Ticks; interval: Ticks;
@ -16,7 +15,6 @@ interface TransportRepeatEventOptions extends TransportEventOptions {
* to schedule repeat events. This class should not be instantiated directly. * to schedule repeat events. This class should not be instantiated directly.
*/ */
export class TransportRepeatEvent extends TransportEvent { export class TransportRepeatEvent extends TransportEvent {
/** /**
* When the event should stop repeating * When the event should stop repeating
*/ */
@ -55,8 +53,10 @@ export class TransportRepeatEvent extends TransportEvent {
/** /**
* @param transport The transport object which the event belongs to * @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); super(transport, opts);
const options = Object.assign(TransportRepeatEvent.getDefaults(), opts); const options = Object.assign(TransportRepeatEvent.getDefaults(), opts);
@ -96,8 +96,10 @@ export class TransportRepeatEvent extends TransportEvent {
*/ */
private _createEvent(): number { private _createEvent(): number {
if (LT(this._nextTick, this.floatTime + this.duration)) { if (LT(this._nextTick, this.floatTime + this.duration)) {
return this.transport.scheduleOnce(this.invoke.bind(this), return this.transport.scheduleOnce(
new TicksClass(this.context, this._nextTick).toSeconds()); this.invoke.bind(this),
new TicksClass(this.context, this._nextTick).toSeconds()
);
} }
return -1; return -1;
} }
@ -109,11 +111,15 @@ export class TransportRepeatEvent extends TransportEvent {
// schedule the next event // schedule the next event
// const ticks = this.transport.getTicksAtTime(time); // const ticks = this.transport.getTicksAtTime(time);
// if the next tick is within the bounds set by "duration" // 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._nextTick += this._interval;
this._currentId = this._nextId; this._currentId = this._nextId;
this._nextId = this.transport.scheduleOnce(this.invoke.bind(this), this._nextId = this.transport.scheduleOnce(
new TicksClass(this.context, this._nextTick).toSeconds()); this.invoke.bind(this),
new TicksClass(this.context, this._nextTick).toSeconds()
);
} }
} }
@ -128,7 +134,10 @@ export class TransportRepeatEvent extends TransportEvent {
const ticks = this.transport.getTicksAtTime(time); const ticks = this.transport.getTicksAtTime(time);
if (GT(ticks, this.time)) { if (GT(ticks, this.time)) {
// the event is not being scheduled from the beginning and should be offset // 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._currentId = this._createEvent();
this._nextTick += this._interval; this._nextTick += this._interval;

View file

@ -1,10 +1,9 @@
import { Time, UnitMap, UnitName } from "../type/Units"; import { Time, UnitMap, UnitName } from "../type/Units.js";
/** /**
* Abstract base class for {@link Param} and {@link Signal} * Abstract base class for {@link Param} and {@link Signal}
*/ */
export abstract class AbstractParam<TypeName extends UnitName> { export abstract class AbstractParam<TypeName extends UnitName> {
/** /**
* Schedules a parameter value change at the given time. * Schedules a parameter value change at the given time.
* @param value The value to set the signal. * @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} * Automation methods like {@link linearRampToValueAtTime} and {@link exponentialRampToValueAtTime}
* require a starting automation value usually set by {@link setValueAtTime}. This method * 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 * 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. * @param time When to add a ramp point.
* @example * @example
* const osc = new Tone.Oscillator().toDestination().start(); * const osc = new Tone.Oscillator().toDestination().start();
@ -61,7 +60,10 @@ export abstract class AbstractParam<TypeName extends UnitName> {
* signal.linearRampToValueAtTime(1, 0.4); * signal.linearRampToValueAtTime(1, 0.4);
* }, 0.5, 1); * }, 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 * 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); * signal.exponentialRampToValueAtTime(0, 0.4);
* }, 0.5, 1); * }, 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 * 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); * signal.exponentialRampTo(5, 0.3, 0.1);
* }, 0.5, 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 * 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); * signal.linearRampTo(0, 0.3, 0.1);
* }, 0.5, 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 * 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); * signal.targetRampTo(0, 0.3, 0.1);
* }, 0.5, 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 * 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 * // exponential approach over 4 seconds starting in 1 second
* osc.frequency.exponentialApproachValueAtTime("C4", "+1", 4); * 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 * 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 startTime
* @param timeConstant * @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 * 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); * signal.setValueCurveAtTime([1, 0.2, 0.8, 0.1, 0], 0.2, 0.3);
* }, 0.5, 1); * }, 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 * 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 * // schedule it to ramp starting at a specific time
* osc.frequency.rampTo("A2", 10, "+2"); * 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 * The current value of the parameter. Setting this value

View file

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

View file

@ -1,11 +1,10 @@
import { Seconds } from "../type/Units"; import { Seconds } from "../type/Units.js";
import { Emitter } from "../util/Emitter"; import { Emitter } from "../util/Emitter.js";
import { AnyAudioContext } from "./AudioContext"; import { AnyAudioContext } from "./AudioContext.js";
import type { DrawClass as Draw } from "../util/Draw.js";
type Draw = import("../util/Draw").DrawClass; import type { DestinationClass as Destination } from "./Destination.js";
type Destination = import("./Destination").DestinationClass; import type { TransportClass as Transport } from "../clock/Transport.js";
type Transport = import("../clock/Transport").TransportClass; import type { ListenerClass as Listener } from "./Listener.js";
type Listener = import("./Listener").ListenerClass;
// these are either not used in Tone.js or deprecated and not implemented. // these are either not used in Tone.js or deprecated and not implemented.
export type ExcludedFromBaseAudioContext = export type ExcludedFromBaseAudioContext =
@ -20,15 +19,16 @@ export type ExcludedFromBaseAudioContext =
// the subset of the BaseAudioContext which Tone.Context implements. // the subset of the BaseAudioContext which Tone.Context implements.
export type BaseAudioContextSubset = Omit< export type BaseAudioContextSubset = Omit<
BaseAudioContext, BaseAudioContext,
ExcludedFromBaseAudioContext ExcludedFromBaseAudioContext
>; >;
export type ContextLatencyHint = AudioContextLatencyCategory; export type ContextLatencyHint = AudioContextLatencyCategory;
export abstract class BaseContext export abstract class BaseContext
extends Emitter<"statechange" | "tick"> extends Emitter<"statechange" | "tick">
implements BaseAudioContextSubset { implements BaseAudioContextSubset
{
//--------------------------- //---------------------------
// BASE AUDIO CONTEXT METHODS // BASE AUDIO CONTEXT METHODS
//--------------------------- //---------------------------
@ -104,9 +104,7 @@ export abstract class BaseContext
abstract get rawContext(): AnyAudioContext; abstract get rawContext(): AnyAudioContext;
abstract addAudioWorkletModule( abstract addAudioWorkletModule(_url: string): Promise<void>;
_url: string
): Promise<void>;
abstract lookAhead: number; abstract lookAhead: number;

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