mirror of
https://github.com/Tonejs/Tone.js
synced 2024-12-25 19:13:08 +00:00
Merge branch 'dev'
This commit is contained in:
commit
cf73c22874
398 changed files with 21471 additions and 18113 deletions
|
@ -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" },
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
36
.github/workflows/test.yml
vendored
36
.github/workflows/test.yml
vendored
|
@ -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
3
.gitignore
vendored
|
@ -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
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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();
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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);
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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>);
|
||||||
|
|
|
@ -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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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", () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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];
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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");
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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);
|
||||||
});
|
});
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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", () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -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({
|
||||||
|
|
|
@ -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", () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -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 });
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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", () => {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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();
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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);
|
||||||
};
|
};
|
||||||
|
|
|
@ -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
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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", () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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", () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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", () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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", () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -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();
|
||||||
});
|
});
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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", () => {
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -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 });
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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"]);
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -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();
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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"
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -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
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -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
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
@ -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", () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -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") {
|
||||||
|
|
|
@ -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 });
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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
Loading…
Reference in a new issue