1 | import { dbToGain, gainToDb } from "../type/Conversions.js";
|
2 | import { isAudioParam } from "../util/AdvancedTypeCheck.js";
|
3 | import { optionsFromArguments } from "../util/Defaults.js";
|
4 | import { Timeline } from "../util/Timeline.js";
|
5 | import { isDefined } from "../util/TypeCheck.js";
|
6 | import { ToneWithContext } from "./ToneWithContext.js";
|
7 | import { EQ } from "../util/Math.js";
|
8 | import { assert, assertRange } from "../util/Debug.js";
|
9 |
|
10 |
|
11 |
|
12 |
|
13 |
|
14 |
|
15 |
|
16 | export 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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
113 |
|
114 | _is(arg, type) {
|
115 | return this.units === type;
|
116 | }
|
117 | |
118 |
|
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 |
|
128 |
|
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 |
|
147 | return 0;
|
148 | }
|
149 | else {
|
150 | return val;
|
151 | }
|
152 | }
|
153 | |
154 |
|
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 |
|
166 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
342 | assert(isFinite(computedTime), `Invalid argument to cancelAndHoldAtTime: ${JSON.stringify(time)}`);
|
343 | this.log(this.units, "cancelAndHoldAtTime", computedTime, "value=" + valueAtTime);
|
344 |
|
345 |
|
346 | const before = this._events.get(computedTime);
|
347 | const after = this._events.getAfter(computedTime);
|
348 | if (before && EQ(before.time, computedTime)) {
|
349 |
|
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 |
|
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 |
|
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 |
|
392 |
|
393 |
|
394 |
|
395 | apply(param) {
|
396 | const now = this.context.currentTime;
|
397 |
|
398 | param.setValueAtTime(this.getValueAtTime(now), now);
|
399 |
|
400 | const previousEvent = this._events.get(now);
|
401 | if (previousEvent && previousEvent.type === "setTargetAtTime") {
|
402 |
|
403 | const nextEvent = this._events.getAfter(previousEvent.time);
|
404 |
|
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 |
|
426 |
|
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 |
|
447 |
|
448 |
|
449 |
|
450 | _exponentialApproach(t0, v0, v1, timeConstant, t) {
|
451 | return v1 + (v0 - v1) * Math.exp(-(t - t0) / timeConstant);
|
452 | }
|
453 |
|
454 | _linearInterpolate(t0, v0, t1, v1, t) {
|
455 | return v0 + (v1 - v0) * ((t - t0) / (t1 - t0));
|
456 | }
|
457 |
|
458 | _exponentialInterpolate(t0, v0, t1, v1, t) {
|
459 | return v0 * Math.pow(v1 / v0, (t - t0) / (t1 - t0));
|
460 | }
|
461 | }
|
462 |
|
\ | No newline at end of file |