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

486 lines
16 KiB
TypeScript
Raw Normal View History

2019-05-23 18:00:49 +00:00
import { AbstractParam } from "../context/AbstractParam";
2019-04-12 14:37:47 +00:00
import { dbToGain, gainToDb } from "../type/Conversions";
import { AudioRange, Decibels, Frequency, NormalRange, Positive, Time, Unit, UnitName } from "../type/Units";
2019-08-19 17:01:37 +00:00
import { isAudioParam } from "../util/AdvancedTypeCheck";
2019-05-23 18:00:49 +00:00
import { optionsFromArguments } from "../util/Defaults";
import { Timeline } from "../util/Timeline";
2019-08-19 17:01:37 +00:00
import { isDefined } from "../util/TypeCheck";
2019-05-23 18:00:49 +00:00
import { ToneWithContext, ToneWithContextOptions } from "./ToneWithContext";
2019-04-12 14:37:47 +00:00
2019-05-23 18:00:49 +00:00
export interface ParamOptions extends ToneWithContextOptions {
2019-07-15 19:37:25 +00:00
units: UnitName;
value?: any;
2019-04-12 14:37:47 +00:00
param: AudioParam;
convert: boolean;
}
/**
* the possible automation types
*/
type AutomationType = "linearRampToValueAtTime" | "exponentialRampToValueAtTime" |
"setValueAtTime" | "setTargetAtTime" | "cancelScheduledValues";
2019-04-12 14:37:47 +00:00
interface TargetAutomationEvent {
type: "setTargetAtTime";
time: number;
value: number;
constant: number;
}
interface NormalAutomationEvent {
type: Exclude<AutomationType, "setTargetAtTime">;
2019-04-12 14:37:47 +00:00
time: number;
value: number;
}
/**
* The events on the automation
*/
export type AutomationEvent = NormalAutomationEvent | TargetAutomationEvent;
2019-04-12 14:37:47 +00:00
/**
2019-08-21 20:01:12 +00:00
* Param wraps the native Web Audio's AudioParam to provide
2019-04-12 14:37:47 +00:00
* additional unit conversion functionality. It also
* serves as a base-class for classes which have a single,
* automatable parameter.
2019-08-21 20:01:12 +00:00
* @category Core
2019-04-12 14:37:47 +00:00
*/
2019-07-15 19:37:25 +00:00
export class Param<Type extends Unit = number>
2019-05-23 18:00:49 +00:00
extends ToneWithContext<ParamOptions>
2019-04-12 14:37:47 +00:00
implements AbstractParam<Type> {
2019-08-21 20:01:12 +00:00
readonly name: string = "Param";
2019-04-12 14:37:47 +00:00
static getDefaults(): ParamOptions {
2019-05-23 18:00:49 +00:00
return Object.assign(ToneWithContext.getDefaults(), {
2019-04-12 14:37:47 +00:00
convert: true,
2019-07-15 19:37:25 +00:00
units: "number" as UnitName,
2019-04-12 14:37:47 +00:00
} as ParamOptions);
}
/**
* The input connection
*/
readonly input: AudioParam;
2019-07-15 19:37:25 +00:00
readonly units: UnitName;
2019-04-12 14:37:47 +00:00
convert: boolean;
overridden: boolean = false;
/**
* The timeline which tracks all of the automations.
*/
2019-05-23 18:00:49 +00:00
protected _events: Timeline<AutomationEvent>;
2019-04-12 14:37:47 +00:00
/**
* The native parameter to control
*/
protected _param: AudioParam;
/**
* The default value before anything is assigned
*/
2019-05-23 18:00:49 +00:00
protected _initialValue: number;
2019-04-12 14:37:47 +00:00
/**
* The minimum output value
*/
2019-08-19 17:01:37 +00:00
private _minOutput = 1e-7;
2019-04-12 14:37:47 +00:00
2019-08-27 17:02:31 +00:00
/**
* @param param The AudioParam to wrap
* @param units The unit name
* @param convert Whether or not to convert the value to the target units
*/
2019-04-12 14:37:47 +00:00
constructor(param: AudioParam, units?: Unit, convert?: boolean);
constructor(options: Partial<ParamOptions>);
constructor() {
super(optionsFromArguments(Param.getDefaults(), arguments, ["param", "units", "convert"]));
const options = optionsFromArguments(Param.getDefaults(), arguments, ["param", "units", "convert"]);
this.assert(isDefined(options.param) && isAudioParam(options.param), "param must be an AudioParam");
2019-04-12 14:37:47 +00:00
// initialize
this._param = this.input = options.param;
this._events = new Timeline<AutomationEvent>(1000);
2019-08-07 03:19:03 +00:00
this._initialValue = this._param.defaultValue;
2019-04-12 14:37:47 +00:00
this.units = options.units;
this.convert = options.convert;
// if the value is defined, set it immediately
if (isDefined(options.value) && options.value !== this._toType(this._initialValue)) {
2019-04-12 14:37:47 +00:00
this.setValueAtTime(options.value, 0);
}
}
2019-07-15 19:37:25 +00:00
get value(): Type {
2019-04-12 14:37:47 +00:00
const now = this.now();
return this.getValueAtTime(now);
}
2019-07-15 19:37:25 +00:00
set value(value: Type) {
2019-04-12 14:37:47 +00:00
this._initialValue = this._fromType(value);
this.cancelScheduledValues(this.now());
this.setValueAtTime(value, this.now());
2019-04-12 14:37:47 +00:00
}
get minValue(): number {
if (this.units === "time" || this.units === "frequency" ||
this.units === "normalRange" || this.units === "positive" ||
this.units === "transportTime" || this.units === "ticks" ||
this.units === "bpm" || this.units === "hertz" || this.units === "samples") {
return 0;
} else if (this.units === "audioRange") {
return -1;
} else if (this.units === "decibels") {
return -Infinity;
} else {
return this._param.minValue;
}
}
get maxValue(): number {
if (this.units === "normalRange" ||
this.units === "audioRange") {
return 1;
} else {
return this._param.maxValue;
}
}
/**
* Type guard based on the unit name
*/
2019-07-15 19:37:25 +00:00
private _is<T>(arg: any, type: UnitName): arg is T {
2019-04-12 14:37:47 +00:00
return this.units === type;
}
/**
* Convert the given value from the type specified by Param.units
* into the destination value (such as Gain or Frequency).
*/
2019-07-15 19:37:25 +00:00
protected _fromType(val: Type): number {
2019-04-12 14:37:47 +00:00
if (this.convert && !this.overridden) {
if (this._is<Time>(val, "time")) {
return this.toSeconds(val);
} else if (this._is<Decibels>(val, "decibels")) {
return dbToGain(val);
} else if (this._is<Frequency>(val, "frequency")) {
return this.toFrequency(val);
} else if (this._is<NormalRange>(val, "normalRange")) {
return Math.min(Math.max(val, 0), 1);
} else if (this._is<AudioRange>(val, "audioRange")) {
return Math.min(Math.max(val, -1), 1);
} else if (this._is<Positive>(val, "positive")) {
return Math.max(val, 0);
} else if (this._is<number>(val, "number")) {
return val;
} else {
return val as number;
}
} else {
return val as number;
}
}
/**
* Convert the parameters value into the units specified by Param.units.
*/
2019-07-15 19:37:25 +00:00
protected _toType(val: number): Type {
2019-04-12 14:37:47 +00:00
if (this.convert && this.units === "decibels") {
2019-07-15 19:37:25 +00:00
return gainToDb(val) as Type;
2019-04-12 14:37:47 +00:00
} else {
2019-07-15 19:37:25 +00:00
return val as Type;
2019-04-12 14:37:47 +00:00
}
}
///////////////////////////////////////////////////////////////////////////
// ABSTRACT PARAM INTERFACE
// all docs are generated from ParamInterface.ts
///////////////////////////////////////////////////////////////////////////
2019-07-15 19:37:25 +00:00
setValueAtTime(value: Type, time: Time): this {
const computedTime = this.toSeconds(time);
2019-04-12 14:37:47 +00:00
const numericValue = this._fromType(value);
2019-08-07 03:19:03 +00:00
this.assert(isFinite(numericValue) && isFinite(computedTime),
`Invalid argument(s) to setValueAtTime: ${JSON.stringify(value)}, ${JSON.stringify(time)}`);
this.log(this.units, "setValueAtTime", value, computedTime);
2019-04-12 14:37:47 +00:00
this._events.add({
time: computedTime,
type: "setValueAtTime",
2019-04-12 14:37:47 +00:00
value: numericValue,
});
this._param.setValueAtTime(numericValue, computedTime);
2019-04-12 14:37:47 +00:00
return this;
}
2019-07-15 19:37:25 +00:00
getValueAtTime(time: Time): Type {
2019-06-18 01:51:22 +00:00
const computedTime = Math.max(this.toSeconds(time), 0);
const after = this._events.getAfter(computedTime);
const before = this._events.get(computedTime);
2019-04-12 14:37:47 +00:00
let value = this._initialValue;
// if it was set by
if (before === null) {
value = this._initialValue;
} else if (before.type === "setTargetAtTime" && (after === null || after.type === "setValueAtTime")) {
2019-04-12 14:37:47 +00:00
const previous = this._events.getBefore(before.time);
let previousVal;
if (previous === null) {
previousVal = this._initialValue;
} else {
previousVal = previous.value;
}
if (before.type === "setTargetAtTime") {
2019-06-18 01:51:22 +00:00
value = this._exponentialApproach(before.time, previousVal, before.value, before.constant, computedTime);
2019-04-12 14:37:47 +00:00
}
} else if (after === null) {
value = before.value;
} else if (after.type === "linearRampToValueAtTime" || after.type === "exponentialRampToValueAtTime") {
2019-04-12 14:37:47 +00:00
let beforeValue = before.value;
if (before.type === "setTargetAtTime") {
2019-04-12 14:37:47 +00:00
const previous = this._events.getBefore(before.time);
if (previous === null) {
beforeValue = this._initialValue;
} else {
beforeValue = previous.value;
}
}
if (after.type === "linearRampToValueAtTime") {
2019-06-18 01:51:22 +00:00
value = this._linearInterpolate(before.time, beforeValue, after.time, after.value, computedTime);
2019-04-12 14:37:47 +00:00
} else {
2019-06-18 01:51:22 +00:00
value = this._exponentialInterpolate(before.time, beforeValue, after.time, after.value, computedTime);
2019-04-12 14:37:47 +00:00
}
} else {
value = before.value;
}
return this._toType(value);
}
setRampPoint(time: Time): this {
time = this.toSeconds(time);
let currentVal = this.getValueAtTime(time);
this.cancelAndHoldAtTime(time);
if (this._fromType(currentVal) === 0) {
currentVal = this._toType(this._minOutput);
}
this.setValueAtTime(currentVal, time);
return this;
}
2019-07-15 19:37:25 +00:00
linearRampToValueAtTime(value: Type, endTime: Time): this {
2019-04-12 14:37:47 +00:00
const numericValue = this._fromType(value);
const computedTime = this.toSeconds(endTime);
2019-08-07 03:19:03 +00:00
this.assert(isFinite(numericValue) && isFinite(computedTime),
`Invalid argument(s) to linearRampToValueAtTime: ${JSON.stringify(value)}, ${JSON.stringify(endTime)}`);
2019-04-12 14:37:47 +00:00
this._events.add({
time: computedTime,
type: "linearRampToValueAtTime",
2019-04-12 14:37:47 +00:00
value : numericValue,
});
this.log(this.units, "linearRampToValueAtTime", value, computedTime);
this._param.linearRampToValueAtTime(numericValue, computedTime);
2019-04-12 14:37:47 +00:00
return this;
}
2019-07-15 19:37:25 +00:00
exponentialRampToValueAtTime(value: Type, endTime: Time): this {
2019-04-12 14:37:47 +00:00
let numericValue = this._fromType(value);
numericValue = Math.max(this._minOutput, numericValue);
const computedTime = this.toSeconds(endTime);
2019-08-07 03:19:03 +00:00
this.assert(isFinite(numericValue) && isFinite(computedTime),
`Invalid argument(s) to exponentialRampToValueAtTime: ${JSON.stringify(value)}, ${JSON.stringify(endTime)}`);
2019-04-12 14:37:47 +00:00
// store the event
this._events.add({
time: computedTime,
type: "exponentialRampToValueAtTime",
2019-04-12 14:37:47 +00:00
value : numericValue,
});
this.log(this.units, "exponentialRampToValueAtTime", value, computedTime);
this._param.exponentialRampToValueAtTime(numericValue, computedTime);
2019-04-12 14:37:47 +00:00
return this;
}
2019-07-15 19:37:25 +00:00
exponentialRampTo(value: Type, rampTime: Time, startTime?: Time): this {
2019-04-12 14:37:47 +00:00
startTime = this.toSeconds(startTime);
this.setRampPoint(startTime);
this.exponentialRampToValueAtTime(value, startTime + this.toSeconds(rampTime));
return this;
}
2019-07-15 19:37:25 +00:00
linearRampTo(value: Type, rampTime: Time, startTime?: Time): this {
2019-04-12 14:37:47 +00:00
startTime = this.toSeconds(startTime);
this.setRampPoint(startTime);
this.linearRampToValueAtTime(value, startTime + this.toSeconds(rampTime));
return this;
}
2019-07-15 19:37:25 +00:00
targetRampTo(value: Type, rampTime: Time, startTime?: Time): this {
2019-04-12 14:37:47 +00:00
startTime = this.toSeconds(startTime);
this.setRampPoint(startTime);
this.exponentialApproachValueAtTime(value, startTime, rampTime);
return this;
}
2019-07-15 19:37:25 +00:00
exponentialApproachValueAtTime(value: Type, time: Time, rampTime: Time): this {
2019-04-12 14:37:47 +00:00
time = this.toSeconds(time);
rampTime = this.toSeconds(rampTime);
const timeConstant = Math.log(rampTime + 1) / Math.log(200);
this.setTargetAtTime(value, time, timeConstant);
// at 90% start a linear ramp to the final value
this.cancelAndHoldAtTime(time + rampTime * 0.9);
this.linearRampToValueAtTime(value, time + rampTime);
return this;
2019-04-12 14:37:47 +00:00
}
2019-07-15 19:37:25 +00:00
setTargetAtTime(value: Type, startTime: Time, timeConstant: Positive): this {
2019-04-12 14:37:47 +00:00
const numericValue = this._fromType(value);
// The value will never be able to approach without timeConstant > 0.
this.assert(isFinite(timeConstant) && timeConstant > 0, "timeConstant must be a number greater than 0");
const computedTime = this.toSeconds(startTime);
2019-08-07 03:19:03 +00:00
this.assert(isFinite(numericValue) && isFinite(computedTime),
`Invalid argument(s) to setTargetAtTime: ${JSON.stringify(value)}, ${JSON.stringify(startTime)}`);
2019-04-12 14:37:47 +00:00
this._events.add({
constant: timeConstant,
time: computedTime,
type: "setTargetAtTime",
2019-04-12 14:37:47 +00:00
value: numericValue,
});
this.log(this.units, "setTargetAtTime", value, computedTime, timeConstant);
this._param.setTargetAtTime(numericValue, computedTime, timeConstant);
2019-04-12 14:37:47 +00:00
return this;
}
2019-07-15 19:37:25 +00:00
setValueCurveAtTime(values: Type[], startTime: Time, duration: Time, scaling: number = 1): this {
2019-04-12 14:37:47 +00:00
duration = this.toSeconds(duration);
startTime = this.toSeconds(startTime);
const startingValue = this._fromType(values[0]) * scaling;
this.setValueAtTime(this._toType(startingValue), startTime);
const segTime = duration / (values.length - 1);
for (let i = 1; i < values.length; i++) {
const numericValue = this._fromType(values[i]) * scaling;
this.linearRampToValueAtTime(this._toType(numericValue), startTime + i * segTime);
}
return this;
}
cancelScheduledValues(time: Time): this {
const computedTime = this.toSeconds(time);
this.assert(isFinite(computedTime), `Invalid argument to cancelScheduledValues: ${JSON.stringify(time)}`);
2019-08-07 03:19:03 +00:00
this._events.cancel(computedTime);
this._param.cancelScheduledValues(computedTime);
this.log(this.units, "cancelScheduledValues", computedTime);
2019-04-12 14:37:47 +00:00
return this;
}
cancelAndHoldAtTime(time: Time): this {
const computedTime = this.toSeconds(time);
const valueAtTime = this._fromType(this.getValueAtTime(computedTime));
2019-04-12 14:37:47 +00:00
// remove the schedule events
this.assert(isFinite(computedTime), `Invalid argument to cancelAndHoldAtTime: ${JSON.stringify(time)}`);
2019-08-07 03:19:03 +00:00
this.log(this.units, "cancelAndHoldAtTime", computedTime, "value=" + valueAtTime);
this._param.cancelScheduledValues(computedTime);
2019-04-12 14:37:47 +00:00
// if there is an event at the given computedTime
2019-04-12 14:37:47 +00:00
// and that even is not a "set"
const before = this._events.get(computedTime);
const after = this._events.getAfter(computedTime);
if (before && before.time === computedTime) {
2019-04-12 14:37:47 +00:00
// remove everything after
if (after) {
this._events.cancel(after.time);
} else {
this._events.cancel(computedTime + this.sampleTime);
2019-04-12 14:37:47 +00:00
}
} else if (after) {
// cancel the next event(s)
this._events.cancel(after.time);
if (after.type === "linearRampToValueAtTime") {
this.linearRampToValueAtTime(this._toType(valueAtTime), computedTime);
} else if (after.type === "exponentialRampToValueAtTime") {
this.exponentialRampToValueAtTime(this._toType(valueAtTime), computedTime);
2019-04-12 14:37:47 +00:00
}
}
// set the value at the given time
this._events.add({
time: computedTime,
type: "setValueAtTime",
2019-04-12 14:37:47 +00:00
value: valueAtTime,
});
this._param.setValueAtTime(valueAtTime, computedTime);
2019-04-12 14:37:47 +00:00
return this;
}
2019-07-15 19:37:25 +00:00
rampTo(value: Type, rampTime: Time = 0.1, startTime?: Time): this {
2019-04-12 14:37:47 +00:00
if (this.units === "frequency" || this.units === "bpm" || this.units === "decibels") {
this.exponentialRampTo(value, rampTime, startTime);
} else {
this.linearRampTo(value, rampTime, startTime);
}
return this;
}
/**
* Apply all of the previously scheduled events to the passed in Param or AudioParam.
* The applied values will start at the context's current time and schedule
* all of the events which are scheduled on this Param onto the passed in param.
*/
apply(param: Param | AudioParam): this {
const now = this.context.currentTime;
// set the param's value at the current time and schedule everything else
param.setValueAtTime(this.getValueAtTime(now) as number, now);
// if the previous event was a curve, then set the rest of it
const previousEvent = this._events.get(now);
if (previousEvent && previousEvent.type === "setTargetAtTime") {
// approx it until the next event with linear ramps
const nextEvent = this._events.getAfter(previousEvent.time);
// or for 2 seconds if there is no event
const endTime = nextEvent ? nextEvent.time : now + 2;
const subdivisions = (endTime - now) / 10;
for (let i = now; i < endTime; i += subdivisions) {
param.linearRampToValueAtTime(this.getValueAtTime(i) as number, i);
}
}
this._events.forEachAfter(this.context.currentTime, event => {
if (event.type === "cancelScheduledValues") {
param.cancelScheduledValues(event.time);
} else if (event.type === "setTargetAtTime") {
param.setTargetAtTime(event.value, event.time, event.constant);
} else {
param[event.type](event.value, event.time);
}
});
return this;
}
2019-04-12 14:37:47 +00:00
dispose(): this {
super.dispose();
2019-04-12 14:37:47 +00:00
this._events.dispose();
return this;
}
2019-09-06 21:16:54 +00:00
get defaultValue(): Type {
return this._toType(this._param.defaultValue);
}
2019-04-12 14:37:47 +00:00
///////////////////////////////////////////////////////////////////////////
// AUTOMATION CURVE CALCULATIONS
// MIT License, copyright (c) 2014 Jordan Santell
///////////////////////////////////////////////////////////////////////////
// Calculates the the value along the curve produced by setTargetAtTime
2019-05-23 18:00:49 +00:00
protected _exponentialApproach(t0: number, v0: number, v1: number, timeConstant: number, t: number): number {
2019-04-12 14:37:47 +00:00
return v1 + (v0 - v1) * Math.exp(-(t - t0) / timeConstant);
}
// Calculates the the value along the curve produced by linearRampToValueAtTime
2019-05-23 18:00:49 +00:00
protected _linearInterpolate(t0: number, v0: number, t1: number, v1: number, t: number): number {
2019-04-12 14:37:47 +00:00
return v0 + (v1 - v0) * ((t - t0) / (t1 - t0));
}
// Calculates the the value along the curve produced by exponentialRampToValueAtTime
2019-05-23 18:00:49 +00:00
protected _exponentialInterpolate(t0: number, v0: number, t1: number, v1: number, t: number): number {
2019-04-12 14:37:47 +00:00
return v0 * Math.pow(v1 / v0, (t - t0) / (t1 - t0));
}
}