import { AbstractParam } from "../context/AbstractParam.js"; import { dbToGain, gainToDb } from "../type/Conversions.js"; import { Decibels, Frequency, Positive, Time, UnitMap, UnitName, } from "../type/Units.js"; import { isAudioParam } from "../util/AdvancedTypeCheck.js"; import { optionsFromArguments } from "../util/Defaults.js"; import { Timeline } from "../util/Timeline.js"; import { isDefined } from "../util/TypeCheck.js"; import { ToneWithContext, ToneWithContextOptions } from "./ToneWithContext.js"; import { EQ } from "../util/Math.js"; import { assert, assertRange } from "../util/Debug.js"; export interface ParamOptions extends ToneWithContextOptions { units: TypeName; value?: UnitMap[TypeName]; param: AudioParam | Param; convert: boolean; minValue?: number; maxValue?: number; swappable?: boolean; } /** * the possible automation types */ type AutomationType = | "linearRampToValueAtTime" | "exponentialRampToValueAtTime" | "setValueAtTime" | "setTargetAtTime" | "cancelScheduledValues"; interface TargetAutomationEvent { type: "setTargetAtTime"; time: number; value: number; constant: number; } interface NormalAutomationEvent { type: Exclude; time: number; value: number; } /** * The events on the automation */ export type AutomationEvent = NormalAutomationEvent | TargetAutomationEvent; /** * Param wraps the native Web Audio's AudioParam to provide * additional unit conversion functionality. It also * serves as a base-class for classes which have a single, * automatable parameter. * @category Core */ export class Param extends ToneWithContext> implements AbstractParam { readonly name: string = "Param"; readonly input: GainNode | AudioParam; readonly units: UnitName; convert: boolean; overridden = false; /** * The timeline which tracks all of the automations. */ protected _events: Timeline; /** * The native parameter to control */ protected _param: AudioParam; /** * The default value before anything is assigned */ protected _initialValue: number; /** * The minimum output value */ private _minOutput = 1e-7; /** * Private reference to the min and max values if passed into the constructor */ private readonly _minValue?: number; private readonly _maxValue?: number; /** * If the underlying AudioParam can be swapped out * using the setParam method. */ protected readonly _swappable: boolean; /** * @param param The AudioParam to wrap * @param units The unit name * @param convert Whether or not to convert the value to the target units */ constructor(param: AudioParam, units?: TypeName, convert?: boolean); constructor(options: Partial>); constructor() { super( optionsFromArguments(Param.getDefaults(), arguments, [ "param", "units", "convert", ]) ); const options = optionsFromArguments(Param.getDefaults(), arguments, [ "param", "units", "convert", ]); assert( isDefined(options.param) && (isAudioParam(options.param) || options.param instanceof Param), "param must be an AudioParam" ); while (!isAudioParam(options.param)) { options.param = options.param._param; } this._swappable = isDefined(options.swappable) ? options.swappable : false; if (this._swappable) { this.input = this.context.createGain(); // initialize this._param = options.param; this.input.connect(this._param); } else { this._param = this.input = options.param; } this._events = new Timeline(1000); this._initialValue = this._param.defaultValue; this.units = options.units; this.convert = options.convert; this._minValue = options.minValue; this._maxValue = options.maxValue; // if the value is defined, set it immediately if ( isDefined(options.value) && options.value !== this._toType(this._initialValue) ) { this.setValueAtTime(options.value, 0); } } static getDefaults(): ParamOptions { return Object.assign(ToneWithContext.getDefaults(), { convert: true, units: "number" as UnitName, } as ParamOptions); } get value(): UnitMap[TypeName] { const now = this.now(); return this.getValueAtTime(now); } set value(value) { this.cancelScheduledValues(this.now()); this.setValueAtTime(value, this.now()); } get minValue(): number { // if it's not the default minValue, return it if (isDefined(this._minValue)) { return this._minValue; } else 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 (isDefined(this._maxValue)) { return this._maxValue; } else if ( this.units === "normalRange" || this.units === "audioRange" ) { return 1; } else { return this._param.maxValue; } } /** * Type guard based on the unit name */ private _is(arg: any, type: UnitName): arg is T { return this.units === type; } /** * Make sure the value is always in the defined range */ private _assertRange(value: number): number { if (isDefined(this.maxValue) && isDefined(this.minValue)) { assertRange( value, this._fromType(this.minValue), this._fromType(this.maxValue) ); } return value; } /** * Convert the given value from the type specified by Param.units * into the destination value (such as Gain or Frequency). */ protected _fromType(val: UnitMap[TypeName]): number { if (this.convert && !this.overridden) { if (this._is