UNPKG

18.2 kBJavaScriptView Raw
1import { dbToGain, gainToDb } from "../type/Conversions.js";
2import { isAudioParam } from "../util/AdvancedTypeCheck.js";
3import { optionsFromArguments } from "../util/Defaults.js";
4import { Timeline } from "../util/Timeline.js";
5import { isDefined } from "../util/TypeCheck.js";
6import { ToneWithContext } from "./ToneWithContext.js";
7import { EQ } from "../util/Math.js";
8import { assert, assertRange } from "../util/Debug.js";
9/**
10 * Param wraps the native Web Audio's AudioParam to provide
11 * additional unit conversion functionality. It also
12 * serves as a base-class for classes which have a single,
13 * automatable parameter.
14 * @category Core
15 */
16export class Param extends ToneWithContext {
17 constructor() {
18 const options = optionsFromArguments(Param.getDefaults(), arguments, [
19 "param",
20 "units",
21 "convert",
22 ]);
23 super(options);
24 this.name = "Param";
25 this.overridden = false;
26 /**
27 * The minimum output value
28 */
29 this._minOutput = 1e-7;
30 assert(isDefined(options.param) &&
31 (isAudioParam(options.param) || options.param instanceof Param), "param must be an AudioParam");
32 while (!isAudioParam(options.param)) {
33 options.param = options.param._param;
34 }
35 this._swappable = isDefined(options.swappable)
36 ? options.swappable
37 : false;
38 if (this._swappable) {
39 this.input = this.context.createGain();
40 // initialize
41 this._param = options.param;
42 this.input.connect(this._param);
43 }
44 else {
45 this._param = this.input = options.param;
46 }
47 this._events = new Timeline(1000);
48 this._initialValue = this._param.defaultValue;
49 this.units = options.units;
50 this.convert = options.convert;
51 this._minValue = options.minValue;
52 this._maxValue = options.maxValue;
53 // if the value is defined, set it immediately
54 if (isDefined(options.value) &&
55 options.value !== this._toType(this._initialValue)) {
56 this.setValueAtTime(options.value, 0);
57 }
58 }
59 static getDefaults() {
60 return Object.assign(ToneWithContext.getDefaults(), {
61 convert: true,
62 units: "number",
63 });
64 }
65 get value() {
66 const now = this.now();
67 return this.getValueAtTime(now);
68 }
69 set value(value) {
70 this.cancelScheduledValues(this.now());
71 this.setValueAtTime(value, this.now());
72 }
73 get minValue() {
74 // if it's not the default minValue, return it
75 if (isDefined(this._minValue)) {
76 return this._minValue;
77 }
78 else if (this.units === "time" ||
79 this.units === "frequency" ||
80 this.units === "normalRange" ||
81 this.units === "positive" ||
82 this.units === "transportTime" ||
83 this.units === "ticks" ||
84 this.units === "bpm" ||
85 this.units === "hertz" ||
86 this.units === "samples") {
87 return 0;
88 }
89 else if (this.units === "audioRange") {
90 return -1;
91 }
92 else if (this.units === "decibels") {
93 return -Infinity;
94 }
95 else {
96 return this._param.minValue;
97 }
98 }
99 get maxValue() {
100 if (isDefined(this._maxValue)) {
101 return this._maxValue;
102 }
103 else if (this.units === "normalRange" ||
104 this.units === "audioRange") {
105 return 1;
106 }
107 else {
108 return this._param.maxValue;
109 }
110 }
111 /**
112 * Type guard based on the unit name
113 */
114 _is(arg, type) {
115 return this.units === type;
116 }
117 /**
118 * Make sure the value is always in the defined range
119 */
120 _assertRange(value) {
121 if (isDefined(this.maxValue) && isDefined(this.minValue)) {
122 assertRange(value, this._fromType(this.minValue), this._fromType(this.maxValue));
123 }
124 return value;
125 }
126 /**
127 * Convert the given value from the type specified by Param.units
128 * into the destination value (such as Gain or Frequency).
129 */
130 _fromType(val) {
131 if (this.convert && !this.overridden) {
132 if (this._is(val, "time")) {
133 return this.toSeconds(val);
134 }
135 else if (this._is(val, "decibels")) {
136 return dbToGain(val);
137 }
138 else if (this._is(val, "frequency")) {
139 return this.toFrequency(val);
140 }
141 else {
142 return val;
143 }
144 }
145 else if (this.overridden) {
146 // if it's overridden, should only schedule 0s
147 return 0;
148 }
149 else {
150 return val;
151 }
152 }
153 /**
154 * Convert the parameters value into the units specified by Param.units.
155 */
156 _toType(val) {
157 if (this.convert && this.units === "decibels") {
158 return gainToDb(val);
159 }
160 else {
161 return val;
162 }
163 }
164 //-------------------------------------
165 // ABSTRACT PARAM INTERFACE
166 // all docs are generated from ParamInterface.ts
167 //-------------------------------------
168 setValueAtTime(value, time) {
169 const computedTime = this.toSeconds(time);
170 const numericValue = this._fromType(value);
171 assert(isFinite(numericValue) && isFinite(computedTime), `Invalid argument(s) to setValueAtTime: ${JSON.stringify(value)}, ${JSON.stringify(time)}`);
172 this._assertRange(numericValue);
173 this.log(this.units, "setValueAtTime", value, computedTime);
174 this._events.add({
175 time: computedTime,
176 type: "setValueAtTime",
177 value: numericValue,
178 });
179 this._param.setValueAtTime(numericValue, computedTime);
180 return this;
181 }
182 getValueAtTime(time) {
183 const computedTime = Math.max(this.toSeconds(time), 0);
184 const after = this._events.getAfter(computedTime);
185 const before = this._events.get(computedTime);
186 let value = this._initialValue;
187 // if it was set by
188 if (before === null) {
189 value = this._initialValue;
190 }
191 else if (before.type === "setTargetAtTime" &&
192 (after === null || after.type === "setValueAtTime")) {
193 const previous = this._events.getBefore(before.time);
194 let previousVal;
195 if (previous === null) {
196 previousVal = this._initialValue;
197 }
198 else {
199 previousVal = previous.value;
200 }
201 if (before.type === "setTargetAtTime") {
202 value = this._exponentialApproach(before.time, previousVal, before.value, before.constant, computedTime);
203 }
204 }
205 else if (after === null) {
206 value = before.value;
207 }
208 else if (after.type === "linearRampToValueAtTime" ||
209 after.type === "exponentialRampToValueAtTime") {
210 let beforeValue = before.value;
211 if (before.type === "setTargetAtTime") {
212 const previous = this._events.getBefore(before.time);
213 if (previous === null) {
214 beforeValue = this._initialValue;
215 }
216 else {
217 beforeValue = previous.value;
218 }
219 }
220 if (after.type === "linearRampToValueAtTime") {
221 value = this._linearInterpolate(before.time, beforeValue, after.time, after.value, computedTime);
222 }
223 else {
224 value = this._exponentialInterpolate(before.time, beforeValue, after.time, after.value, computedTime);
225 }
226 }
227 else {
228 value = before.value;
229 }
230 return this._toType(value);
231 }
232 setRampPoint(time) {
233 time = this.toSeconds(time);
234 let currentVal = this.getValueAtTime(time);
235 this.cancelAndHoldAtTime(time);
236 if (this._fromType(currentVal) === 0) {
237 currentVal = this._toType(this._minOutput);
238 }
239 this.setValueAtTime(currentVal, time);
240 return this;
241 }
242 linearRampToValueAtTime(value, endTime) {
243 const numericValue = this._fromType(value);
244 const computedTime = this.toSeconds(endTime);
245 assert(isFinite(numericValue) && isFinite(computedTime), `Invalid argument(s) to linearRampToValueAtTime: ${JSON.stringify(value)}, ${JSON.stringify(endTime)}`);
246 this._assertRange(numericValue);
247 this._events.add({
248 time: computedTime,
249 type: "linearRampToValueAtTime",
250 value: numericValue,
251 });
252 this.log(this.units, "linearRampToValueAtTime", value, computedTime);
253 this._param.linearRampToValueAtTime(numericValue, computedTime);
254 return this;
255 }
256 exponentialRampToValueAtTime(value, endTime) {
257 let numericValue = this._fromType(value);
258 // the value can't be 0
259 numericValue = EQ(numericValue, 0) ? this._minOutput : numericValue;
260 this._assertRange(numericValue);
261 const computedTime = this.toSeconds(endTime);
262 assert(isFinite(numericValue) && isFinite(computedTime), `Invalid argument(s) to exponentialRampToValueAtTime: ${JSON.stringify(value)}, ${JSON.stringify(endTime)}`);
263 // store the event
264 this._events.add({
265 time: computedTime,
266 type: "exponentialRampToValueAtTime",
267 value: numericValue,
268 });
269 this.log(this.units, "exponentialRampToValueAtTime", value, computedTime);
270 this._param.exponentialRampToValueAtTime(numericValue, computedTime);
271 return this;
272 }
273 exponentialRampTo(value, rampTime, startTime) {
274 startTime = this.toSeconds(startTime);
275 this.setRampPoint(startTime);
276 this.exponentialRampToValueAtTime(value, startTime + this.toSeconds(rampTime));
277 return this;
278 }
279 linearRampTo(value, rampTime, startTime) {
280 startTime = this.toSeconds(startTime);
281 this.setRampPoint(startTime);
282 this.linearRampToValueAtTime(value, startTime + this.toSeconds(rampTime));
283 return this;
284 }
285 targetRampTo(value, rampTime, startTime) {
286 startTime = this.toSeconds(startTime);
287 this.setRampPoint(startTime);
288 this.exponentialApproachValueAtTime(value, startTime, rampTime);
289 return this;
290 }
291 exponentialApproachValueAtTime(value, time, rampTime) {
292 time = this.toSeconds(time);
293 rampTime = this.toSeconds(rampTime);
294 const timeConstant = Math.log(rampTime + 1) / Math.log(200);
295 this.setTargetAtTime(value, time, timeConstant);
296 // at 90% start a linear ramp to the final value
297 this.cancelAndHoldAtTime(time + rampTime * 0.9);
298 this.linearRampToValueAtTime(value, time + rampTime);
299 return this;
300 }
301 setTargetAtTime(value, startTime, timeConstant) {
302 const numericValue = this._fromType(value);
303 // The value will never be able to approach without timeConstant > 0.
304 assert(isFinite(timeConstant) && timeConstant > 0, "timeConstant must be a number greater than 0");
305 const computedTime = this.toSeconds(startTime);
306 this._assertRange(numericValue);
307 assert(isFinite(numericValue) && isFinite(computedTime), `Invalid argument(s) to setTargetAtTime: ${JSON.stringify(value)}, ${JSON.stringify(startTime)}`);
308 this._events.add({
309 constant: timeConstant,
310 time: computedTime,
311 type: "setTargetAtTime",
312 value: numericValue,
313 });
314 this.log(this.units, "setTargetAtTime", value, computedTime, timeConstant);
315 this._param.setTargetAtTime(numericValue, computedTime, timeConstant);
316 return this;
317 }
318 setValueCurveAtTime(values, startTime, duration, scaling = 1) {
319 duration = this.toSeconds(duration);
320 startTime = this.toSeconds(startTime);
321 const startingValue = this._fromType(values[0]) * scaling;
322 this.setValueAtTime(this._toType(startingValue), startTime);
323 const segTime = duration / (values.length - 1);
324 for (let i = 1; i < values.length; i++) {
325 const numericValue = this._fromType(values[i]) * scaling;
326 this.linearRampToValueAtTime(this._toType(numericValue), startTime + i * segTime);
327 }
328 return this;
329 }
330 cancelScheduledValues(time) {
331 const computedTime = this.toSeconds(time);
332 assert(isFinite(computedTime), `Invalid argument to cancelScheduledValues: ${JSON.stringify(time)}`);
333 this._events.cancel(computedTime);
334 this._param.cancelScheduledValues(computedTime);
335 this.log(this.units, "cancelScheduledValues", computedTime);
336 return this;
337 }
338 cancelAndHoldAtTime(time) {
339 const computedTime = this.toSeconds(time);
340 const valueAtTime = this._fromType(this.getValueAtTime(computedTime));
341 // remove the schedule events
342 assert(isFinite(computedTime), `Invalid argument to cancelAndHoldAtTime: ${JSON.stringify(time)}`);
343 this.log(this.units, "cancelAndHoldAtTime", computedTime, "value=" + valueAtTime);
344 // if there is an event at the given computedTime
345 // and that even is not a "set"
346 const before = this._events.get(computedTime);
347 const after = this._events.getAfter(computedTime);
348 if (before && EQ(before.time, computedTime)) {
349 // remove everything after
350 if (after) {
351 this._param.cancelScheduledValues(after.time);
352 this._events.cancel(after.time);
353 }
354 else {
355 this._param.cancelAndHoldAtTime(computedTime);
356 this._events.cancel(computedTime + this.sampleTime);
357 }
358 }
359 else if (after) {
360 this._param.cancelScheduledValues(after.time);
361 // cancel the next event(s)
362 this._events.cancel(after.time);
363 if (after.type === "linearRampToValueAtTime") {
364 this.linearRampToValueAtTime(this._toType(valueAtTime), computedTime);
365 }
366 else if (after.type === "exponentialRampToValueAtTime") {
367 this.exponentialRampToValueAtTime(this._toType(valueAtTime), computedTime);
368 }
369 }
370 // set the value at the given time
371 this._events.add({
372 time: computedTime,
373 type: "setValueAtTime",
374 value: valueAtTime,
375 });
376 this._param.setValueAtTime(valueAtTime, computedTime);
377 return this;
378 }
379 rampTo(value, rampTime = 0.1, startTime) {
380 if (this.units === "frequency" ||
381 this.units === "bpm" ||
382 this.units === "decibels") {
383 this.exponentialRampTo(value, rampTime, startTime);
384 }
385 else {
386 this.linearRampTo(value, rampTime, startTime);
387 }
388 return this;
389 }
390 /**
391 * Apply all of the previously scheduled events to the passed in Param or AudioParam.
392 * The applied values will start at the context's current time and schedule
393 * all of the events which are scheduled on this Param onto the passed in param.
394 */
395 apply(param) {
396 const now = this.context.currentTime;
397 // set the param's value at the current time and schedule everything else
398 param.setValueAtTime(this.getValueAtTime(now), now);
399 // if the previous event was a curve, then set the rest of it
400 const previousEvent = this._events.get(now);
401 if (previousEvent && previousEvent.type === "setTargetAtTime") {
402 // approx it until the next event with linear ramps
403 const nextEvent = this._events.getAfter(previousEvent.time);
404 // or for 2 seconds if there is no event
405 const endTime = nextEvent ? nextEvent.time : now + 2;
406 const subdivisions = (endTime - now) / 10;
407 for (let i = now; i < endTime; i += subdivisions) {
408 param.linearRampToValueAtTime(this.getValueAtTime(i), i);
409 }
410 }
411 this._events.forEachAfter(this.context.currentTime, (event) => {
412 if (event.type === "cancelScheduledValues") {
413 param.cancelScheduledValues(event.time);
414 }
415 else if (event.type === "setTargetAtTime") {
416 param.setTargetAtTime(event.value, event.time, event.constant);
417 }
418 else {
419 param[event.type](event.value, event.time);
420 }
421 });
422 return this;
423 }
424 /**
425 * Replace the Param's internal AudioParam. Will apply scheduled curves
426 * onto the parameter and replace the connections.
427 */
428 setParam(param) {
429 assert(this._swappable, "The Param must be assigned as 'swappable' in the constructor");
430 const input = this.input;
431 input.disconnect(this._param);
432 this.apply(param);
433 this._param = param;
434 input.connect(this._param);
435 return this;
436 }
437 dispose() {
438 super.dispose();
439 this._events.dispose();
440 return this;
441 }
442 get defaultValue() {
443 return this._toType(this._param.defaultValue);
444 }
445 //-------------------------------------
446 // AUTOMATION CURVE CALCULATIONS
447 // MIT License, copyright (c) 2014 Jordan Santell
448 //-------------------------------------
449 // Calculates the the value along the curve produced by setTargetAtTime
450 _exponentialApproach(t0, v0, v1, timeConstant, t) {
451 return v1 + (v0 - v1) * Math.exp(-(t - t0) / timeConstant);
452 }
453 // Calculates the the value along the curve produced by linearRampToValueAtTime
454 _linearInterpolate(t0, v0, t1, v1, t) {
455 return v0 + (v1 - v0) * ((t - t0) / (t1 - t0));
456 }
457 // Calculates the the value along the curve produced by exponentialRampToValueAtTime
458 _exponentialInterpolate(t0, v0, t1, v1, t) {
459 return v0 * Math.pow(v1 / v0, (t - t0) / (t1 - t0));
460 }
461}
462//# sourceMappingURL=Param.js.map
\No newline at end of file