Tone.js/Tone/core/context/Param.test.ts

607 lines
20 KiB
TypeScript

import { Compare, Plot } from "@tonejs/plot";
import { expect } from "chai";
import { BasicTests, testAudioContext } from "test/helper/Basic";
import { atTime, Offline } from "test/helper/Offline";
import { SCHEDULE_RAMP_AFTER_SET_TARGET } from "test/helper/Supports";
import { BPM, Decibels, Frequency, Positive, Seconds, Time, Unit, UnitName } from "Tone/core/type/Units";
import { Signal } from "Tone/signal/Signal";
import { getContext } from "../Global";
import { Param } from "./Param";
import { connect } from "./ToneAudioNode";
const audioContext = getContext();
describe("Param", () => {
BasicTests(Param, {
context: testAudioContext,
param: testAudioContext.createOscillator().frequency,
});
context("constructor", () => {
it("can be created and disposed", async () => {
await Offline(context => {
const param = new Param<"time">({
context,
param: context.createConstantSource().offset,
units: "time",
});
expect(param.getValueAtTime(0)).to.equal(1);
param.dispose();
});
});
it("can pass in a value", async () => {
await Offline(context => {
const param = new Param({
context,
param: context.createConstantSource().offset,
value: 1.1,
});
expect(param.getValueAtTime(0)).to.equal(1.1);
param.dispose();
});
});
it("requires a param in the constructor", () => {
expect(() => {
const param = new Param({
value: 1.1,
});
}).throws(Error);
});
});
context("Scheduling Curves", () => {
const sampleRate = 11025;
function matchesOutputCurve(param, outBuffer): void {
outBuffer.toArray()[0].forEach((sample, index) => {
try {
expect(param.getValueAtTime(index / sampleRate)).to.be.closeTo(sample, 0.1);
} catch (e) {
throw e;
}
});
}
if (SCHEDULE_RAMP_AFTER_SET_TARGET) {
it("correctly handles setTargetAtTime followed by a ramp", async () => {
let param;
// this fails on FF
const testBuffer = await Offline(context => {
const source = context.createConstantSource();
source.connect(context.rawContext.destination);
source.start(0);
param = new Param({
context,
param: source.offset,
});
param.setTargetAtTime(2, 0.5, 0.1);
expect(param.getValueAtTime(0.6)).to.be.closeTo(1.6, 0.1);
param.linearRampToValueAtTime(0.5, 0.7);
expect(param.getValueAtTime(0.6)).to.be.closeTo(0.75, 0.1);
}, 1.5, 1, sampleRate);
document.body.appendChild(await Plot.signal(testBuffer));
matchesOutputCurve(param, testBuffer);
});
it("schedules a value curve", async () => {
let param;
const testBuffer = await Offline(context => {
const source = context.createConstantSource();
source.connect(context.rawContext.destination);
source.start(0);
param = new Param({
context,
param: source.offset,
units: "number",
value: 0,
});
param.setValueCurveAtTime([0, 0.5, 0, 1, 1.5], 0.1, 0.8, 0.5);
expect(param.getValueAtTime(0.91)).to.be.closeTo(0.75, 0.01);
}, 1, 1, sampleRate);
document.body.appendChild(await Plot.signal(testBuffer));
matchesOutputCurve(param, testBuffer);
});
it("a mixture of scheduling curves", async () => {
let param;
const testBuffer = await Offline(context => {
const source = context.createConstantSource();
source.connect(context.rawContext.destination);
source.start(0);
param = new Param({
context,
param: source.offset,
value: 0.1,
});
param.setValueAtTime(0, 0);
param.setValueAtTime(1, 0.1);
param.linearRampToValueAtTime(3, 0.2);
param.exponentialRampToValueAtTime(0.01, 0.3);
param.setTargetAtTime(-1, 0.35, 0.2);
param.cancelAndHoldAtTime(0.6);
param.rampTo(1.1, 0.2, 0.7);
param.exponentialRampTo(0, 0.1, 0.85);
param.setValueAtTime(0, 1);
param.linearRampTo(1, 0.2, 1);
param.targetRampTo(0, 0.1, 1.1);
param.setValueAtTime(4, 1.2);
param.cancelScheduledValues(1.2);
param.linearRampToValueAtTime(1, 1.3);
}, 1.5, 1, sampleRate);
document.body.appendChild(await Plot.signal(testBuffer));
matchesOutputCurve(param, testBuffer);
});
it.skip("can cancel and hold", async () => {
let param;
const testBuffer = await Offline(context => {
const source = context.createConstantSource();
source.connect(context.rawContext.destination);
source.start(0);
param = new Param({
context,
param: source.offset,
value: 0.1,
});
param.setValueAtTime(0, 0);
param.setValueAtTime(1, 0.2);
param.cancelAndHoldAtTime(0.1);
param.linearRampToValueAtTime(1, 0.3);
param.cancelAndHoldAtTime(0.2);
expect(param.getValueAtTime(0.2)).to.be.closeTo(0.5, 0.001);
param.exponentialRampToValueAtTime(0, 0.4);
param.cancelAndHoldAtTime(0.25);
expect(param.getValueAtTime(0.25)).to.be.closeTo(0.033, 0.001);
param.setTargetAtTime(1, 0.3, 0.1);
param.cancelAndHoldAtTime(0.4);
expect(param.getValueAtTime(0.4)).to.be.closeTo(0.644, 0.001);
param.setValueAtTime(0, 0.45);
param.setValueAtTime(1, 0.48);
param.cancelAndHoldAtTime(0.45);
expect(param.getValueAtTime(0.45)).to.be.closeTo(0, 0.001);
}, 0.5, 1, sampleRate);
matchesOutputCurve(param, testBuffer);
// document.body.appendChild(await Plot.signal(testBuffer));
});
// it ("matches known values", async () => {
// await Compare.toFile(context => {
// const source = context.createConstantSource();
// source.connect(context.rawContext.destination);
// source.start(0);
// const param = new Param({
// context,
// param: source.offset,
// value: 0.1,
// });
// param.setValueAtTime(0, 0);
// param.setValueAtTime(1, 0.2);
// param.cancelAndHoldAtTime(0.1);
// param.linearRampToValueAtTime(1, 0.3);
// param.cancelAndHoldAtTime(0.2);
// param.exponentialRampToValueAtTime(0, 0.4);
// param.cancelAndHoldAtTime(0.25);
// param.setTargetAtTime(1, 0.3, 0.1);
// param.cancelAndHoldAtTime(0.4);
// }, "/base/test/audio/param/curve_0.wav", 0.01, 0.5, 1, 11025);
// });
}
});
context("Units", () => {
it("throws an error with invalid values", () => {
const osc = audioContext.createOscillator();
const param = new Param<"frequency">({
context: audioContext,
param: osc.frequency,
units: "frequency",
});
expect(() => {
// @ts-ignore
expect(param.setValueAtTime("bad", "bad"));
}).to.throw(Error);
expect(() => {
// @ts-ignore
expect(param.linearRampToValueAtTime("bad", "bad"));
}).to.throw(Error);
expect(() => {
// @ts-ignore
expect(param.exponentialRampToValueAtTime("bad", "bad"));
}).to.throw(Error);
expect(() => {
// @ts-ignore
expect(param.setTargetAtTime("bad", "bad", 0.1));
}).to.throw(Error);
expect(() => {
// @ts-ignore
expect(param.cancelScheduledValues("bad"));
}).to.throw(Error);
param.dispose();
});
it("can be created with specific units", () => {
const gain = audioContext.createGain();
const param = new Param<"bpm">({
context: audioContext,
param: gain.gain,
units: "bpm",
});
expect(param.units).to.equal("bpm");
param.dispose();
});
it("can evaluate the given units", () => {
const gain = audioContext.createGain();
const param = new Param<"decibels">({
context: audioContext,
param: gain.gain,
units: "decibels",
});
param.value = 0.5;
expect(param.value).to.be.closeTo(0.5, 0.001);
param.dispose();
});
it("can be forced to not convert", async () => {
const testBuffer = await Offline(context => {
const source = context.createConstantSource();
source.connect(context.rawContext.destination);
source.start(0);
const param = new Param({
context,
convert: false,
param: source.offset,
units: "decibels",
});
param.value = -10;
expect(param.value).to.be.closeTo(-10, 0.01);
}, 0.001, 1);
expect(testBuffer.getValueAtTime(0)).to.be.closeTo(-10, 0.01);
});
});
context("apply", () => {
it("can apply a scheduled curve", () => {
let sig;
return Offline(context => {
const signal = new Signal();
sig = signal;
signal.setValueAtTime(0, 0);
signal.linearRampToValueAtTime(0.5, 0.1);
signal.exponentialRampToValueAtTime(0.2, 0.5);
signal.linearRampToValueAtTime(4, 2);
signal.cancelScheduledValues(1);
signal.setTargetAtTime(4, 1, 0.1);
const source = context.createConstantSource();
source.start(0);
connect(source, context.destination);
return atTime(0.4, () => {
signal.apply(source.offset);
});
}, 2).then(async buffer => {
for (let time = 0.41; time < 2; time += 0.1) {
expect(buffer.getValueAtTime(time)).to.be.closeTo(sig.getValueAtTime(time), 0.01);
}
// document.body.appendChild(await Plot.signal(buffer));
});
});
it("can apply a scheduled curve that starts with a setTargetAtTime", () => {
let sig;
return Offline(context => {
const signal = new Signal();
sig = signal;
signal.setTargetAtTime(2, 0, 0.2);
const source = context.createConstantSource();
source.start(0);
connect(source, context.destination);
return atTime(0.4, () => {
signal.apply(source.offset);
});
}, 2).then(async buffer => {
for (let time = 0.41; time < 2; time += 0.1) {
expect(buffer.getValueAtTime(time)).to.be.closeTo(sig.getValueAtTime(time), 0.05);
}
// document.body.appendChild(await Plot.signal(buffer));
});
});
it("can apply a scheduled curve that starts with a setTargetAtTime and then schedules other things", () => {
let sig;
return Offline(context => {
const signal = new Signal();
sig = signal;
signal.setTargetAtTime(2, 0, 0.2);
signal.setValueAtTime(1, 0.8);
signal.linearRampToValueAtTime(0, 2);
const source = context.createConstantSource();
source.start(0);
connect(source, context.destination);
return atTime(0.4, () => {
signal.apply(source.offset);
});
}, 2).then(async buffer => {
for (let time = 0.41; time < 2; time += 0.1) {
expect(buffer.getValueAtTime(time)).to.be.closeTo(sig.getValueAtTime(time), 0.05);
}
// document.body.appendChild(await Plot.signal(buffer));
});
});
it("can set the param if the Param is marked as swappable", () => {
return Offline(context => {
const constSource = context.createConstantSource();
const param = new Param({
swappable: true,
param: constSource.offset,
});
param.setValueAtTime(0.1, 0.1);
param.setValueAtTime(0.2, 0.2);
param.setValueAtTime(0.3, 0.3);
const constSource2 = context.createConstantSource();
constSource2.start(0);
param.setParam(constSource2.offset);
connect(constSource2, context.destination);
}, 0.5).then(buffer => {
expect(buffer.getValueAtTime(0.1)).to.be.closeTo(0.1, 0.001);
expect(buffer.getValueAtTime(0.2)).to.be.closeTo(0.2, 0.001);
expect(buffer.getValueAtTime(0.3)).to.be.closeTo(0.3, 0.001);
});
});
it("throws an error if the param is not set to swappable", () => {
return Offline(context => {
const constSource = context.createConstantSource();
const param = new Param({
param: constSource.offset,
});
const constSource2 = context.createConstantSource();
expect(() => {
param.setParam(constSource2.offset);
}).to.throw(Error);
}, 0.5);
});
});
context("Unit Conversions", () => {
function testUnitConversion(units: UnitName, inputValue: any, inputVerification: number, outputValue: number): void {
it(`converts to ${units}`, async () => {
const testBuffer = await Offline(context => {
const source = context.createConstantSource();
source.connect(context.rawContext.destination);
source.start(0);
const param = new Param({
context,
param: source.offset,
units,
});
param.value = inputValue;
expect(param.value).to.be.closeTo(inputVerification, 0.01);
}, 0.001, 1);
expect(testBuffer.getValueAtTime(0)).to.be.closeTo(outputValue, 0.01);
});
}
testUnitConversion("number", 3, 3, 3);
testUnitConversion("decibels", -10, -10, 0.31);
testUnitConversion("decibels", -20, -20, 0.1);
testUnitConversion("decibels", -100, -100, 0);
testUnitConversion("gain", 1.2, 1.2, 1.2);
testUnitConversion("positive", 1.5, 1.5, 1.5);
testUnitConversion("positive", -1.5, 0, 0);
testUnitConversion("time", 2, 2, 2);
testUnitConversion("time", 0, 0, 0);
testUnitConversion("frequency", 20, 20, 20);
testUnitConversion("frequency", 0.1, 0.1, 0.1);
testUnitConversion("normalRange", -1, 0, 0);
testUnitConversion("normalRange", 0.5, 0.5, 0.5);
testUnitConversion("normalRange", 1.5, 1, 1);
testUnitConversion("audioRange", -1.1, -1, -1);
testUnitConversion("audioRange", 0.5, 0.5, 0.5);
testUnitConversion("audioRange", 1.5, 1, 1);
});
context("min/maxValue", () => {
function testMinMaxValue(units: UnitName, min, max): void {
it(`has proper min/max for ${units}`, () => {
const source = audioContext.createConstantSource();
source.connect(audioContext.rawContext.destination);
const param = new Param({
context: audioContext,
param: source.offset,
units,
});
expect(param.minValue).to.be.equal(min);
expect(param.maxValue).to.be.equal(max);
});
}
// number, decibels, normalRange, audioRange, gain
// positive, time, frequency, transportTime, ticks, bpm, degrees, samples, hertz
const rangeMax = 3.4028234663852886e+38;
testMinMaxValue("number", -rangeMax, rangeMax);
testMinMaxValue("decibels", -Infinity, rangeMax);
testMinMaxValue("normalRange", 0, 1);
testMinMaxValue("audioRange", -1, 1);
testMinMaxValue("gain", -rangeMax, rangeMax);
testMinMaxValue("positive", 0, rangeMax);
testMinMaxValue("time", 0, rangeMax);
testMinMaxValue("frequency", 0, rangeMax);
testMinMaxValue("transportTime", 0, rangeMax);
testMinMaxValue("ticks", 0, rangeMax);
testMinMaxValue("bpm", 0, rangeMax);
testMinMaxValue("degrees", -rangeMax, rangeMax);
testMinMaxValue("samples", 0, rangeMax);
testMinMaxValue("hertz", 0, rangeMax);
it("can pass in a min and max value", () => {
const source = audioContext.createConstantSource();
source.connect(audioContext.rawContext.destination);
const param = new Param({
context: audioContext,
param: source.offset,
minValue: 0.3,
maxValue: 0.5,
});
expect(param.minValue).to.be.equal(0.3);
expect(param.maxValue).to.be.equal(0.5);
});
});
context("defaultValue", () => {
it("has the right default value for default units", () => {
const source = audioContext.createConstantSource();
source.connect(audioContext.rawContext.destination);
const param = new Param({
context: audioContext,
param: source.offset,
});
expect(param.defaultValue).to.be.equal(1);
});
it("has the right default value for default decibels", () => {
const source = audioContext.createConstantSource();
source.connect(audioContext.rawContext.destination);
const param = new Param({
context: audioContext,
param: source.offset,
units: "decibels",
});
expect(param.defaultValue).to.be.equal(0);
});
});
// const allSchedulingMethods = ['setValueAtTime', 'linearRampToValueAtTime', 'exponentialRampToValueAtTime']
context("setValueAtTime", () => {
function testSetValueAtTime(units: UnitName, value0, value1, value2): void {
it(`can schedule value with units ${units}`, async () => {
const testBuffer = await Offline(context => {
const source = context.createConstantSource();
source.connect(context.rawContext.destination);
source.start(0);
const param = new Param({
context,
param: source.offset,
units,
});
param.setValueAtTime(value0, 0);
param.setValueAtTime(value1, 0.01);
param.setValueAtTime(value2, 0.02);
expect(param.getValueAtTime(0)).to.be.closeTo(value0, 0.01);
expect(param.getValueAtTime(0.01)).to.be.closeTo(value1, 0.01);
expect(param.getValueAtTime(0.02)).to.be.closeTo(value2, 0.01);
}, 0.022, 1);
expect(testBuffer.getValueAtTime(0)).to.be.closeTo(0, 0.01);
expect(testBuffer.getValueAtTime(0.011)).to.be.closeTo(1, 0.01);
expect(testBuffer.getValueAtTime(0.021)).to.be.closeTo(0.5, 0.01);
});
}
const allUnits: UnitName[] = ["number", "decibels", "normalRange", "audioRange", "gain",
"positive", "time", "frequency", "transportTime", "ticks", "bpm", "degrees", "samples", "hertz"];
allUnits.forEach(unit => {
if (unit === "decibels") {
testSetValueAtTime(unit, -100, 0, -6);
} else {
testSetValueAtTime(unit, 0, 1, 0.5);
}
});
});
["linearRampToValueAtTime", "exponentialRampToValueAtTime"].forEach(method => {
context(method, () => {
function testRampToValueAtTime(units: UnitName, value0, value1, value2): void {
it(`can schedule value with units ${units}`, async () => {
const testBuffer = await Offline(context => {
const source = context.createConstantSource();
source.connect(context.rawContext.destination);
source.start(0);
const param = new Param({
context,
param: source.offset,
units,
});
param.setValueAtTime(value0, 0);
param[method](value1, 0.01);
param[method](value2, 0.02);
expect(param.getValueAtTime(0)).to.be.closeTo(value0, 0.01);
expect(param.getValueAtTime(0.01)).to.be.closeTo(value1, 0.01);
expect(param.getValueAtTime(0.02)).to.be.closeTo(value2, 0.01);
}, 0.022, 1);
expect(testBuffer.getValueAtTime(0)).to.be.closeTo(1, 0.01);
expect(testBuffer.getValueAtTime(0.01)).to.be.closeTo(0.7, 0.01);
expect(testBuffer.getValueAtTime(0.02)).to.be.closeTo(0, 0.01);
});
}
const allUnits: UnitName[] = ["number", "decibels", "normalRange", "audioRange", "gain",
"positive", "time", "frequency", "transportTime", "ticks", "bpm", "degrees", "samples", "hertz"];
allUnits.forEach(unit => {
if (unit === "decibels") {
testRampToValueAtTime(unit, 0, -3, -100);
} else {
testRampToValueAtTime(unit, 1, 0.7, 0);
}
});
});
});
["linearRampTo", "exponentialRampTo", "rampTo", "targetRampTo"].forEach(method => {
context(method, () => {
function testRampToValueAtTime(units: UnitName, value0, value1, value2): void {
it(`can schedule value with units ${units}`, async () => {
const testBuffer = await Offline(context => {
const source = context.createConstantSource();
source.connect(context.rawContext.destination);
source.start(0);
const param = new Param({
context,
param: source.offset,
units,
value: value0,
});
param[method](value1, 0.009, 0);
param[method](value2, 0.01, 0.01);
expect(param.getValueAtTime(0)).to.be.closeTo(value0, 0.02);
expect(param.getValueAtTime(0.01)).to.be.closeTo(value1, 0.02);
if (units !== "decibels") {
expect(param.getValueAtTime(0.025)).to.be.closeTo(value2, 0.01);
}
}, 0.021, 1);
// document.body.appendChild(await Plot.signal(testBuffer));
expect(testBuffer.getValueAtTime(0)).to.be.closeTo(1, 0.01);
expect(testBuffer.getValueAtTime(0.01)).to.be.closeTo(0.7, 0.01);
expect(testBuffer.getValueAtTime(0.02)).to.be.closeTo(0, 0.01);
});
}
const allUnits: UnitName[] = ["number", "decibels", "normalRange", "audioRange", "gain",
"positive", "time", "frequency", "transportTime", "ticks", "bpm", "degrees", "samples", "hertz"];
allUnits.forEach(unit => {
if (unit === "decibels") {
testRampToValueAtTime(unit, 0, -3, -100);
} else {
testRampToValueAtTime(unit, 1, 0.7, 0);
}
});
});
});
});