@kidlib/web-audio 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -7
- package/dist/components.d.ts +2 -2
- package/dist/components.js +254 -396
- package/dist/index.d.ts +70 -51
- package/dist/index.js +4983 -7379
- package/dist/io.d.ts +11 -11
- package/dist/io.js +129 -191
- package/dist/keymap-C9_BObLQ.js +239 -0
- package/dist/processors/processors.js +1464 -1612
- package/package.json +26 -23
- package/dist/keymap-3lZMR1Ak.js +0 -167
|
@@ -1,1627 +1,1479 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
1
|
+
//#region src/utils/search/findClosest.ts
|
|
2
|
+
/**
|
|
3
|
+
* Generic binary search that finds the closest element using a custom comparison function
|
|
4
|
+
* @param sortedArray - Array sorted according to the compareValue function
|
|
5
|
+
* @param target - Target value to search for
|
|
6
|
+
* @param getValue - Function to extract comparison value from array elements (defaults to identity for number arrays)
|
|
7
|
+
* @param getDistance - Optional function to calculate distance (defaults to absolute difference)
|
|
8
|
+
* @returns The array index of the element which value is closest to the target value
|
|
9
|
+
*/
|
|
9
10
|
function findClosestIdx(sortedArray, target, direction = "any", getValue = (x) => x, getDistance = (a, b) => Math.abs(a - b)) {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
left = mid;
|
|
30
|
-
} else {
|
|
31
|
-
right = mid;
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
if (direction === "left") return left;
|
|
35
|
-
if (direction === "right") return right;
|
|
36
|
-
const leftDistance = getDistance(getValue(sortedArray[left]), targetValue);
|
|
37
|
-
const rightDistance = getDistance(getValue(sortedArray[right]), targetValue);
|
|
38
|
-
return leftDistance <= rightDistance ? left : right;
|
|
11
|
+
if (sortedArray.length === 0) throw new Error("Array cannot be empty");
|
|
12
|
+
if (sortedArray.length === 1) return 0;
|
|
13
|
+
const targetValue = target;
|
|
14
|
+
const firstValue = getValue(sortedArray[0]);
|
|
15
|
+
const lastValue = getValue(sortedArray[sortedArray.length - 1]);
|
|
16
|
+
if (targetValue <= firstValue) return 0;
|
|
17
|
+
if (targetValue >= lastValue) return sortedArray.length - 1;
|
|
18
|
+
let left = 0;
|
|
19
|
+
let right = sortedArray.length - 1;
|
|
20
|
+
while (left < right - 1) {
|
|
21
|
+
const mid = Math.floor((left + right) / 2);
|
|
22
|
+
const midValue = getValue(sortedArray[mid]);
|
|
23
|
+
if (midValue === targetValue) return mid;
|
|
24
|
+
else if (midValue < targetValue) left = mid;
|
|
25
|
+
else right = mid;
|
|
26
|
+
}
|
|
27
|
+
if (direction === "left") return left;
|
|
28
|
+
if (direction === "right") return right;
|
|
29
|
+
return getDistance(getValue(sortedArray[left]), targetValue) <= getDistance(getValue(sortedArray[right]), targetValue) ? left : right;
|
|
39
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* Generic binary search that finds the closest element using a custom comparison function
|
|
33
|
+
* @param sortedArray - Array sorted according to the compareValue function
|
|
34
|
+
* @param target - Target value to search for
|
|
35
|
+
* @param getValue - Function to extract comparison value from array elements (defaults to identity for number arrays)
|
|
36
|
+
* @param getDistance - Optional function to calculate distance (defaults to absolute difference)
|
|
37
|
+
* @returns The array element which value is closest to the target value
|
|
38
|
+
*/
|
|
40
39
|
function findClosest(sortedArray, target, direction = "any", getValue = (x) => x, getDistance = (a, b) => Math.abs(a - b)) {
|
|
41
|
-
|
|
42
|
-
sortedArray,
|
|
43
|
-
target,
|
|
44
|
-
direction,
|
|
45
|
-
getValue,
|
|
46
|
-
getDistance
|
|
47
|
-
);
|
|
48
|
-
return sortedArray[index];
|
|
40
|
+
return sortedArray[findClosestIdx(sortedArray, target, direction, getValue, getDistance)];
|
|
49
41
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
};
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
}
|
|
506
|
-
/**
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
42
|
+
var SAMPLE_PLAYER_WORKLET_AUDIOPARAM_DESCRIPTORS = Object.values({
|
|
43
|
+
masterGain: {
|
|
44
|
+
name: "masterGain",
|
|
45
|
+
defaultValue: 1,
|
|
46
|
+
minValue: 0,
|
|
47
|
+
maxValue: 2,
|
|
48
|
+
automationRate: "k-rate"
|
|
49
|
+
},
|
|
50
|
+
envGain: {
|
|
51
|
+
name: "envGain",
|
|
52
|
+
defaultValue: 0,
|
|
53
|
+
minValue: 0,
|
|
54
|
+
maxValue: 1,
|
|
55
|
+
automationRate: "a-rate"
|
|
56
|
+
},
|
|
57
|
+
velocity: {
|
|
58
|
+
name: "velocity",
|
|
59
|
+
defaultValue: 100,
|
|
60
|
+
minValue: 0,
|
|
61
|
+
maxValue: 127,
|
|
62
|
+
automationRate: "k-rate"
|
|
63
|
+
},
|
|
64
|
+
pan: {
|
|
65
|
+
name: "pan",
|
|
66
|
+
defaultValue: 0,
|
|
67
|
+
minValue: -1,
|
|
68
|
+
maxValue: 1,
|
|
69
|
+
automationRate: "k-rate"
|
|
70
|
+
},
|
|
71
|
+
playbackRate: {
|
|
72
|
+
name: "playbackRate",
|
|
73
|
+
defaultValue: 1,
|
|
74
|
+
minValue: .1,
|
|
75
|
+
maxValue: 24,
|
|
76
|
+
automationRate: "a-rate"
|
|
77
|
+
},
|
|
78
|
+
loopStart: {
|
|
79
|
+
name: "loopStart",
|
|
80
|
+
defaultValue: 0,
|
|
81
|
+
minValue: 0,
|
|
82
|
+
maxValue: 99999,
|
|
83
|
+
automationRate: "k-rate"
|
|
84
|
+
},
|
|
85
|
+
loopEnd: {
|
|
86
|
+
name: "loopEnd",
|
|
87
|
+
defaultValue: 99999,
|
|
88
|
+
minValue: 0,
|
|
89
|
+
maxValue: 99999,
|
|
90
|
+
automationRate: "k-rate"
|
|
91
|
+
},
|
|
92
|
+
startPoint: {
|
|
93
|
+
name: "startPoint",
|
|
94
|
+
defaultValue: 0,
|
|
95
|
+
minValue: 0,
|
|
96
|
+
maxValue: 9999,
|
|
97
|
+
automationRate: "k-rate"
|
|
98
|
+
},
|
|
99
|
+
endPoint: {
|
|
100
|
+
name: "endPoint",
|
|
101
|
+
defaultValue: 9999,
|
|
102
|
+
minValue: 0,
|
|
103
|
+
maxValue: 9999,
|
|
104
|
+
automationRate: "k-rate"
|
|
105
|
+
},
|
|
106
|
+
playbackPosition: {
|
|
107
|
+
name: "playbackPosition",
|
|
108
|
+
defaultValue: 0,
|
|
109
|
+
minValue: 0,
|
|
110
|
+
maxValue: 99999,
|
|
111
|
+
automationRate: "k-rate"
|
|
112
|
+
},
|
|
113
|
+
loopDurationDriftAmount: {
|
|
114
|
+
name: "loopDurationDriftAmount",
|
|
115
|
+
defaultValue: 0,
|
|
116
|
+
minValue: 0,
|
|
117
|
+
maxValue: 1,
|
|
118
|
+
automationRate: "k-rate"
|
|
119
|
+
},
|
|
120
|
+
maxLoopCount: {
|
|
121
|
+
name: "maxLoopCount",
|
|
122
|
+
defaultValue: 999999,
|
|
123
|
+
minValue: 1,
|
|
124
|
+
maxValue: 999999,
|
|
125
|
+
automationRate: "k-rate"
|
|
126
|
+
},
|
|
127
|
+
tempo: {
|
|
128
|
+
name: "tempo",
|
|
129
|
+
defaultValue: 120,
|
|
130
|
+
minValue: 20,
|
|
131
|
+
maxValue: 300,
|
|
132
|
+
automationRate: "k-rate"
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
//#endregion
|
|
136
|
+
//#region src/worklets/processors/play/sample-player-processor.js
|
|
137
|
+
var SamplePlayerProcessor = class extends AudioWorkletProcessor {
|
|
138
|
+
static get parameterDescriptors() {
|
|
139
|
+
return SAMPLE_PLAYER_WORKLET_AUDIOPARAM_DESCRIPTORS;
|
|
140
|
+
}
|
|
141
|
+
constructor() {
|
|
142
|
+
super();
|
|
143
|
+
this.layers = [];
|
|
144
|
+
this.layerGain = 1;
|
|
145
|
+
this.minZeroCrossing = 0;
|
|
146
|
+
this.maxZeroCrossing = 0;
|
|
147
|
+
this.usePlaybackPosition = false;
|
|
148
|
+
this.enableLoopSmoothing = true;
|
|
149
|
+
this.enableAdaptiveDrift = true;
|
|
150
|
+
this.enableAmplitudeCompensation = true;
|
|
151
|
+
this.syncLoopToTempo = false;
|
|
152
|
+
this.keytrackLoopAmount = 0;
|
|
153
|
+
this.durationPreservation = {
|
|
154
|
+
enabled: false,
|
|
155
|
+
maxDriftSamples: Math.floor(sampleRate * .04),
|
|
156
|
+
timelinePosition: 0,
|
|
157
|
+
resetPending: false
|
|
158
|
+
};
|
|
159
|
+
this.PITCH_PRESERVATION_THRESHOLD = Math.floor(sampleRate * .061);
|
|
160
|
+
this.AMPLITUDE_COMPENSATION_THRESHOLD = Math.floor(sampleRate / 16.35);
|
|
161
|
+
this.port.onmessage = this.#handleMessage.bind(this);
|
|
162
|
+
this.#resetState();
|
|
163
|
+
this.port.postMessage({ type: "initialized" });
|
|
164
|
+
}
|
|
165
|
+
/** Authority layer. All range and duration math reads through this. */
|
|
166
|
+
get buffer() {
|
|
167
|
+
return this.layers[0] ?? null;
|
|
168
|
+
}
|
|
169
|
+
#handleMessage(event) {
|
|
170
|
+
const { type, value, buffer, layers, timestamp, durationSeconds, zeroCrossings, allowedPeriods, playbackDirection } = event.data;
|
|
171
|
+
switch (type) {
|
|
172
|
+
case "voice:reset":
|
|
173
|
+
this.#resetState();
|
|
174
|
+
this.port.postMessage({ type: "voice:reset" });
|
|
175
|
+
break;
|
|
176
|
+
case "voice:setBuffer":
|
|
177
|
+
case "voice:setLayers":
|
|
178
|
+
this.#resetState();
|
|
179
|
+
this.zeroCrossings = [];
|
|
180
|
+
this.minZeroCrossing = 0;
|
|
181
|
+
this.maxZeroCrossing = 0;
|
|
182
|
+
this.layers = (layers ?? (buffer ? [buffer] : [])).filter(Boolean);
|
|
183
|
+
this.layerGain = this.layers.length ? 1 / this.layers.length : 1;
|
|
184
|
+
this.port.postMessage({
|
|
185
|
+
type: "voice:loaded",
|
|
186
|
+
durationSeconds,
|
|
187
|
+
time: currentTime
|
|
188
|
+
});
|
|
189
|
+
break;
|
|
190
|
+
case "voice:setZeroCrossings":
|
|
191
|
+
this.zeroCrossings = (zeroCrossings || []).map((timeSec) => timeSec * sampleRate);
|
|
192
|
+
if (this.zeroCrossings.length > 0) {
|
|
193
|
+
this.minZeroCrossing = this.zeroCrossings[0];
|
|
194
|
+
this.maxZeroCrossing = this.zeroCrossings[this.zeroCrossings.length - 1];
|
|
195
|
+
}
|
|
196
|
+
break;
|
|
197
|
+
case "voice:start":
|
|
198
|
+
this.isReleasing = false;
|
|
199
|
+
this.isPlaying = true;
|
|
200
|
+
this.loopCount = 0;
|
|
201
|
+
this.playbackPosition = 0;
|
|
202
|
+
this.port.postMessage({
|
|
203
|
+
type: "voice:started",
|
|
204
|
+
time: timestamp || currentTime
|
|
205
|
+
});
|
|
206
|
+
break;
|
|
207
|
+
case "voice:release":
|
|
208
|
+
this.isReleasing = true;
|
|
209
|
+
this.port.postMessage({
|
|
210
|
+
type: "voice:releasing",
|
|
211
|
+
time: currentTime
|
|
212
|
+
});
|
|
213
|
+
break;
|
|
214
|
+
case "voice:stop":
|
|
215
|
+
this.#stop();
|
|
216
|
+
break;
|
|
217
|
+
case "setLoopEnabled":
|
|
218
|
+
this.loopEnabled = value;
|
|
219
|
+
this.port.postMessage({
|
|
220
|
+
type: "loop:enabled",
|
|
221
|
+
enabled: value
|
|
222
|
+
});
|
|
223
|
+
break;
|
|
224
|
+
case "setPanDriftEnabled":
|
|
225
|
+
this.panDriftEnabled = value;
|
|
226
|
+
break;
|
|
227
|
+
case "voice:setPlaybackDirection": {
|
|
228
|
+
const reverse = playbackDirection === "reverse";
|
|
229
|
+
if (reverse !== this.reversePlayback && this.playbackPosition > 0) this.playbackPosition += reverse ? 1 : -1;
|
|
230
|
+
this.reversePlayback = reverse;
|
|
231
|
+
this.port.postMessage({
|
|
232
|
+
type: "voice:playbackDirectionChange",
|
|
233
|
+
playbackDirection
|
|
234
|
+
});
|
|
235
|
+
break;
|
|
236
|
+
}
|
|
237
|
+
case "voice:usePlaybackPosition":
|
|
238
|
+
this.usePlaybackPosition = value;
|
|
239
|
+
break;
|
|
240
|
+
case "syncLoopToTempo":
|
|
241
|
+
this.syncLoopToTempo = value;
|
|
242
|
+
this.port.postMessage({
|
|
243
|
+
type: "loop:syncToTempo",
|
|
244
|
+
enabled: value
|
|
245
|
+
});
|
|
246
|
+
break;
|
|
247
|
+
case "setKeytrackLoopAmount":
|
|
248
|
+
this.keytrackLoopAmount = Math.max(0, Math.min(1, value));
|
|
249
|
+
break;
|
|
250
|
+
case "setPreserveDuration":
|
|
251
|
+
this.durationPreservation.enabled = Boolean(value);
|
|
252
|
+
this.#resetDurationPreservation(this.playbackPosition);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
#resetState() {
|
|
256
|
+
this.isPlaying = false;
|
|
257
|
+
this.isReleasing = false;
|
|
258
|
+
this.loopEnabled = false;
|
|
259
|
+
this.velocitySensitivity = 1;
|
|
260
|
+
this.reversePlayback = false;
|
|
261
|
+
this.playbackPosition = 0;
|
|
262
|
+
this.debugCounter = 0;
|
|
263
|
+
this.loopCount = 0;
|
|
264
|
+
this.applyClickCompensation = false;
|
|
265
|
+
this.loopClickCompensation = 0;
|
|
266
|
+
this.driftUpdateCounter = 0;
|
|
267
|
+
this.currentLoopDrift = 0;
|
|
268
|
+
this.currentPanDrift = 0;
|
|
269
|
+
this.panDriftEnabled = true;
|
|
270
|
+
this.nextDriftGenerated = false;
|
|
271
|
+
this.loopAmplitudeGain = 1;
|
|
272
|
+
this.lastAnalyzedLoopStart = -1;
|
|
273
|
+
this.lastAnalyzedLoopEnd = -1;
|
|
274
|
+
this.#resetDurationPreservation();
|
|
275
|
+
}
|
|
276
|
+
#stop() {
|
|
277
|
+
this.isPlaying = false;
|
|
278
|
+
this.isReleasing = false;
|
|
279
|
+
this.playbackPosition = 0;
|
|
280
|
+
this.port.postMessage({ type: "voice:stopped" });
|
|
281
|
+
}
|
|
282
|
+
#smoothLoopWrap(lastLoopSample, newFirstSample) {
|
|
283
|
+
const discontinuity = lastLoopSample - newFirstSample;
|
|
284
|
+
if (this.enableLoopSmoothing && Math.abs(discontinuity) > .01) {
|
|
285
|
+
this.loopClickCompensation = discontinuity * .5;
|
|
286
|
+
this.compensationDecay = .9;
|
|
287
|
+
this.applyClickCompensation = true;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
#clamp = (value, min, max) => Math.max(min, Math.min(max, value));
|
|
291
|
+
#clampZeroCrossing = (value) => this.#clamp(value, this.minZeroCrossing, this.maxZeroCrossing);
|
|
292
|
+
#findNearestZeroCrossing(position, direction = "any", maxDistance = null) {
|
|
293
|
+
if (!this.zeroCrossings || this.zeroCrossings.length === 0) return position;
|
|
294
|
+
const closestValue = findClosest(this.zeroCrossings, position, direction);
|
|
295
|
+
if (maxDistance !== null && Math.abs(closestValue - position) > maxDistance) return position;
|
|
296
|
+
return closestValue;
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Convert normalized position (0-1) to sample index
|
|
300
|
+
* @param {number} normalizedPosition - Position as 0-1 value
|
|
301
|
+
* @returns {number} - Sample index
|
|
302
|
+
*/
|
|
303
|
+
#normalizedToSamples(normalizedPosition) {
|
|
304
|
+
if (!this.buffer || !this.buffer[0]) return 0;
|
|
305
|
+
return normalizedPosition * this.buffer[0].length;
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Convert sample index to normalized position (0-1)
|
|
309
|
+
* @param {number} sampleIndex - Sample index
|
|
310
|
+
* @returns {number} - Normalized position 0-1
|
|
311
|
+
*/
|
|
312
|
+
#samplesToNormalized(sampleIndex) {
|
|
313
|
+
if (!this.buffer || !this.buffer[0]) return 0;
|
|
314
|
+
return sampleIndex / this.buffer[0].length;
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Convert MIDI velocity (0-127) to gain multiplier (0-1)
|
|
318
|
+
* @param {number} midiVelocity - MIDI velocity 0-127
|
|
319
|
+
* @returns {number} - Gain multiplier 0-1
|
|
320
|
+
*/
|
|
321
|
+
#midiVelocityToGain(midiVelocity) {
|
|
322
|
+
return Math.max(0, Math.min(1, midiVelocity / 127));
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Get buffer duration in seconds
|
|
326
|
+
* @returns {number} - Buffer duration in seconds
|
|
327
|
+
*/
|
|
328
|
+
#getBufferDurationSeconds() {
|
|
329
|
+
return (this.buffer?.[0]?.length || 0) / sampleRate;
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Calculate musical note durations in samples for given tempo
|
|
333
|
+
* @param {number} tempo - BPM
|
|
334
|
+
* @returns {Object} - Musical note durations in samples
|
|
335
|
+
*/
|
|
336
|
+
#getMusicalNoteDurations(tempo) {
|
|
337
|
+
const beatsPerSecond = tempo / 60;
|
|
338
|
+
const samplesPerBeat = sampleRate / beatsPerSecond;
|
|
339
|
+
return {
|
|
340
|
+
whole: samplesPerBeat * 4,
|
|
341
|
+
half: samplesPerBeat * 2,
|
|
342
|
+
quarter: samplesPerBeat,
|
|
343
|
+
eighth: samplesPerBeat / 2,
|
|
344
|
+
sixteenth: samplesPerBeat / 4,
|
|
345
|
+
thirtySecond: samplesPerBeat / 8,
|
|
346
|
+
quarterTriplet: samplesPerBeat * 2 / 3,
|
|
347
|
+
eighthTriplet: samplesPerBeat / 2 * 2 / 3,
|
|
348
|
+
sixteenthTriplet: samplesPerBeat / 4 * 2 / 3
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* Quantize loop duration to nearest musical interval (skips if below the smallest quantize option)
|
|
353
|
+
* @param {number} loopDurationSamples - Current loop duration in samples
|
|
354
|
+
* @param {number} tempo - Current tempo in BPM
|
|
355
|
+
* @param {number} playbackRate - Current playback rate
|
|
356
|
+
* @returns {number} - Quantized loop duration in samples
|
|
357
|
+
*/
|
|
358
|
+
#quantizeLoopDuration(loopDurationSamples, tempo, playbackRate) {
|
|
359
|
+
if (!this.syncLoopToTempo) return loopDurationSamples;
|
|
360
|
+
const noteDurations = this.#getMusicalNoteDurations(tempo);
|
|
361
|
+
const effectiveDuration = loopDurationSamples / Math.abs(playbackRate);
|
|
362
|
+
if (effectiveDuration < noteDurations.thirtySecond) return loopDurationSamples;
|
|
363
|
+
const intervals = Object.values(noteDurations);
|
|
364
|
+
let closestInterval = intervals[0];
|
|
365
|
+
let smallestDiff = Math.abs(effectiveDuration - closestInterval);
|
|
366
|
+
for (const interval of intervals) {
|
|
367
|
+
const diff = Math.abs(effectiveDuration - interval);
|
|
368
|
+
if (diff < smallestDiff) {
|
|
369
|
+
smallestDiff = diff;
|
|
370
|
+
closestInterval = interval;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
return Math.floor(closestInterval * Math.abs(playbackRate));
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Extract and convert all position parameters from seconds to samples
|
|
377
|
+
* @param {Object} parameters - AudioWorkletProcessor parameters
|
|
378
|
+
* @returns {Object} - Converted parameters in samples
|
|
379
|
+
*/
|
|
380
|
+
#extractPositionParams(parameters) {
|
|
381
|
+
return {
|
|
382
|
+
startPointSamples: Math.floor(parameters.startPoint[0] * sampleRate),
|
|
383
|
+
endPointSamples: Math.floor(parameters.endPoint[0] * sampleRate),
|
|
384
|
+
loopStartSamples: Math.floor(parameters.loopStart[0] * sampleRate),
|
|
385
|
+
loopEndSamples: Math.floor(parameters.loopEnd[0] * sampleRate)
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* Calculate effective playback range in samples
|
|
390
|
+
* @param {Object} params - Position parameters from #extractPositionParams
|
|
391
|
+
* @returns {Object} - Effective start and end positions
|
|
392
|
+
*/
|
|
393
|
+
#calculatePlaybackRange(params) {
|
|
394
|
+
const bufferLength = this.buffer?.[0]?.length || 0;
|
|
395
|
+
const start = Math.max(0, params.startPointSamples);
|
|
396
|
+
const end = params.endPointSamples > start ? Math.min(bufferLength, params.endPointSamples) : bufferLength;
|
|
397
|
+
const snappedStart = this.#findNearestZeroCrossing(start, "right");
|
|
398
|
+
const snappedEnd = this.#findNearestZeroCrossing(end, "left");
|
|
399
|
+
return {
|
|
400
|
+
startSamples: snappedStart,
|
|
401
|
+
endSamples: snappedEnd,
|
|
402
|
+
durationSamples: snappedEnd - snappedStart
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* Calculate effective loop range in samples with optional drift
|
|
407
|
+
* @param {Object} params - Position parameters from #extractPositionParams
|
|
408
|
+
* @param {Object} playbackRange - Range from #calculatePlaybackRange
|
|
409
|
+
* @param {number} driftAmount - Loop duration drift amount (0-1)
|
|
410
|
+
* @param {number} tempo - Current tempo in BPM
|
|
411
|
+
* @param {number} playbackRate - Current playback rate
|
|
412
|
+
* @returns {Object} - Effective loop start and end positions with drift applied
|
|
413
|
+
*/
|
|
414
|
+
#calculateLoopRange(params, playbackRange, driftAmount = 0, tempo = 120, playbackRate = 1) {
|
|
415
|
+
const lpStart = params.loopStartSamples;
|
|
416
|
+
const lpEnd = params.loopEndSamples;
|
|
417
|
+
let calcLoopStart = lpStart < lpEnd && lpStart >= 0 ? lpStart : playbackRange.startSamples;
|
|
418
|
+
let calcLoopEnd = lpEnd > lpStart && lpEnd <= playbackRange.endSamples ? lpEnd : playbackRange.endSamples;
|
|
419
|
+
let baseDuration = calcLoopEnd - calcLoopStart;
|
|
420
|
+
if (this.syncLoopToTempo) {
|
|
421
|
+
const quantizedDuration = this.#quantizeLoopDuration(baseDuration, tempo, playbackRate);
|
|
422
|
+
calcLoopEnd = calcLoopStart + quantizedDuration;
|
|
423
|
+
calcLoopEnd = Math.min(calcLoopEnd, playbackRange.endSamples);
|
|
424
|
+
}
|
|
425
|
+
if (baseDuration > this.PITCH_PRESERVATION_THRESHOLD && this.keytrackLoopAmount > 0 && !this.syncLoopToTempo) {
|
|
426
|
+
const scale = 1 + this.keytrackLoopAmount * (Math.abs(playbackRate) - 1);
|
|
427
|
+
baseDuration = Math.max(1, Math.floor(baseDuration * scale));
|
|
428
|
+
calcLoopEnd = calcLoopStart + baseDuration;
|
|
429
|
+
}
|
|
430
|
+
baseDuration = calcLoopEnd - calcLoopStart;
|
|
431
|
+
if (baseDuration > this.PITCH_PRESERVATION_THRESHOLD) calcLoopStart = this.#findNearestZeroCrossing(calcLoopStart, "right");
|
|
432
|
+
if (driftAmount > 0 && this.loopEnabled) {
|
|
433
|
+
if (!this.nextDriftGenerated || this.loopCount === 0) {
|
|
434
|
+
const updateInterval = baseDuration <= this.PITCH_PRESERVATION_THRESHOLD ? Math.max(1, Math.floor(this.PITCH_PRESERVATION_THRESHOLD / baseDuration)) : 1;
|
|
435
|
+
if (this.driftUpdateCounter % updateInterval === 0) {
|
|
436
|
+
this.currentLoopDrift = this.#generateLoopDrift(driftAmount, baseDuration);
|
|
437
|
+
if (this.panDriftEnabled && driftAmount > 0 && this.loopCount > 0) {
|
|
438
|
+
const panDriftAmountScalar = 1e-4;
|
|
439
|
+
this.currentPanDrift = this.currentLoopDrift * panDriftAmountScalar;
|
|
440
|
+
} else this.currentPanDrift = 0;
|
|
441
|
+
}
|
|
442
|
+
this.driftUpdateCounter++;
|
|
443
|
+
this.nextDriftGenerated = true;
|
|
444
|
+
}
|
|
445
|
+
const driftedLoopEnd = calcLoopEnd + this.currentLoopDrift;
|
|
446
|
+
const minLoopDuration = Math.max(1, Math.floor(baseDuration * .1));
|
|
447
|
+
const maxLoopEnd = Math.max(playbackRange.endSamples, calcLoopEnd);
|
|
448
|
+
calcLoopEnd = Math.max(calcLoopStart + minLoopDuration, Math.min(maxLoopEnd, driftedLoopEnd));
|
|
449
|
+
} else this.currentPanDrift = 0;
|
|
450
|
+
if (baseDuration > this.PITCH_PRESERVATION_THRESHOLD && calcLoopEnd <= playbackRange.endSamples) calcLoopEnd = Math.max(calcLoopStart + 1, this.#findNearestZeroCrossing(calcLoopEnd, "left"));
|
|
451
|
+
const loopDuration = calcLoopEnd - calcLoopStart;
|
|
452
|
+
return {
|
|
453
|
+
loopStartSamples: calcLoopStart,
|
|
454
|
+
loopEndSamples: calcLoopEnd,
|
|
455
|
+
loopDurationSamples: loopDuration
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
#getSafeParam(paramArray, index, isConstant) {
|
|
459
|
+
return isConstant ? paramArray[0] : paramArray[Math.min(index, paramArray.length - 1)];
|
|
460
|
+
}
|
|
461
|
+
#getConstantFlags(parameters) {
|
|
462
|
+
this.constantFlags ??= {
|
|
463
|
+
envGain: true,
|
|
464
|
+
playbackRate: true
|
|
465
|
+
};
|
|
466
|
+
this.constantFlags.envGain = parameters.envGain.length === 1;
|
|
467
|
+
this.constantFlags.playbackRate = parameters.playbackRate.length === 1;
|
|
468
|
+
return this.constantFlags;
|
|
469
|
+
}
|
|
470
|
+
#resetDurationPreservation(position = 0) {
|
|
471
|
+
this.durationPreservation.timelinePosition = position;
|
|
472
|
+
this.durationPreservation.resetPending = false;
|
|
473
|
+
}
|
|
474
|
+
#isDurationPreservationActive(loopRange) {
|
|
475
|
+
return this.durationPreservation.enabled && Boolean(this.zeroCrossings?.length) && (!this.loopEnabled || loopRange.loopDurationSamples > this.PITCH_PRESERVATION_THRESHOLD);
|
|
476
|
+
}
|
|
477
|
+
#prepareDurationPreservingSample(playbackRate, loopRange) {
|
|
478
|
+
const state = this.durationPreservation;
|
|
479
|
+
if (!this.#isDurationPreservationActive(loopRange)) return null;
|
|
480
|
+
if (Math.abs(this.playbackPosition - state.timelinePosition) > state.maxDriftSamples) state.resetPending = true;
|
|
481
|
+
if (!state.resetPending) return null;
|
|
482
|
+
const direction = playbackRate < 0 ? "left" : "right";
|
|
483
|
+
const outgoingZero = this.#findNearestZeroCrossing(this.playbackPosition, direction);
|
|
484
|
+
if (Math.abs(outgoingZero - this.playbackPosition) > Math.abs(playbackRate)) return null;
|
|
485
|
+
this.playbackPosition = outgoingZero;
|
|
486
|
+
state.resetPending = false;
|
|
487
|
+
return this.#findNearestZeroCrossing(state.timelinePosition, "any", state.maxDriftSamples);
|
|
488
|
+
}
|
|
489
|
+
#advanceDurationPreservingPlayback(playbackRate, resetTarget, loopRange, canWrapLoop) {
|
|
490
|
+
const state = this.durationPreservation;
|
|
491
|
+
this.playbackPosition = resetTarget === null ? this.playbackPosition + playbackRate : resetTarget;
|
|
492
|
+
if (this.#isDurationPreservationActive(loopRange)) {
|
|
493
|
+
state.timelinePosition += playbackRate < 0 ? -1 : 1;
|
|
494
|
+
if (canWrapLoop && playbackRate >= 0 && state.timelinePosition >= loopRange.loopEndSamples) state.timelinePosition = loopRange.loopStartSamples;
|
|
495
|
+
else if (canWrapLoop && playbackRate < 0 && state.timelinePosition <= loopRange.loopStartSamples) state.timelinePosition = loopRange.loopEndSamples - 1;
|
|
496
|
+
} else this.#resetDurationPreservation(this.playbackPosition);
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* Generate a new drift amount for the current loop iteration
|
|
500
|
+
* @param {number} driftAmount - Maximum drift amount (0-1)
|
|
501
|
+
* @param {number} baseDuration - Base loop duration in samples
|
|
502
|
+
* @returns {number} - Drift amount in samples
|
|
503
|
+
*/
|
|
504
|
+
#generateLoopDrift(driftAmount, baseDuration) {
|
|
505
|
+
if (driftAmount <= 0) return 0;
|
|
506
|
+
const randomFactor = (Math.random() - .5) * 2;
|
|
507
|
+
let effectiveDriftAmount = driftAmount;
|
|
508
|
+
if (this.enableAdaptiveDrift) {
|
|
509
|
+
const shortThreshold = 1024;
|
|
510
|
+
const longThreshold = 8192;
|
|
511
|
+
if (baseDuration < shortThreshold) effectiveDriftAmount *= .1;
|
|
512
|
+
else if (baseDuration < longThreshold) {
|
|
513
|
+
const scaleFactor = .1 + .9 * (baseDuration - shortThreshold) / 7168;
|
|
514
|
+
effectiveDriftAmount *= scaleFactor;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
const maxDriftSamples = effectiveDriftAmount * baseDuration;
|
|
518
|
+
return Math.floor(randomFactor * maxDriftSamples);
|
|
519
|
+
}
|
|
520
|
+
/**
|
|
521
|
+
* Analyze loop amplitude and calculate makeup gain for short loops
|
|
522
|
+
* @param {number} loopStart - Loop start position in samples
|
|
523
|
+
* @param {number} loopEnd - Loop end position in samples
|
|
524
|
+
* @returns {number} - Makeup gain multiplier (1.0 = no change)
|
|
525
|
+
*/
|
|
526
|
+
#analyzeLoopAmplitude(loopStart, loopEnd) {
|
|
527
|
+
if (!this.enableAmplitudeCompensation || !this.buffer || !this.buffer[0]) return 1;
|
|
528
|
+
if (loopEnd - loopStart >= this.AMPLITUDE_COMPENSATION_THRESHOLD) return 1;
|
|
529
|
+
if (loopStart === this.lastAnalyzedLoopStart && loopEnd === this.lastAnalyzedLoopEnd) return this.loopAmplitudeGain;
|
|
530
|
+
let sumSquares = 0;
|
|
531
|
+
let sampleCount = 0;
|
|
532
|
+
const channel = this.buffer[0];
|
|
533
|
+
const startIndex = Math.floor(loopStart);
|
|
534
|
+
const endIndex = Math.floor(loopEnd);
|
|
535
|
+
for (let i = startIndex; i < endIndex && i < channel.length; i++) {
|
|
536
|
+
const sample = channel[i];
|
|
537
|
+
sumSquares += sample * sample;
|
|
538
|
+
sampleCount++;
|
|
539
|
+
}
|
|
540
|
+
if (sampleCount === 0) return 1;
|
|
541
|
+
const rmsAmplitude = Math.sqrt(sumSquares / sampleCount);
|
|
542
|
+
const targetAmplitude = .3;
|
|
543
|
+
let makeupGain = 1;
|
|
544
|
+
if (rmsAmplitude < targetAmplitude) {
|
|
545
|
+
makeupGain = targetAmplitude / Math.max(rmsAmplitude, .001);
|
|
546
|
+
makeupGain = Math.min(2, makeupGain);
|
|
547
|
+
}
|
|
548
|
+
this.lastAnalyzedLoopStart = loopStart;
|
|
549
|
+
this.lastAnalyzedLoopEnd = loopEnd;
|
|
550
|
+
this.loopAmplitudeGain = makeupGain;
|
|
551
|
+
return makeupGain;
|
|
552
|
+
}
|
|
553
|
+
process(inputs, outputs, parameters) {
|
|
554
|
+
const output = outputs[0];
|
|
555
|
+
this.debugCounter++;
|
|
556
|
+
if (!output || !this.isPlaying || !this.buffer?.[0]?.length) return true;
|
|
557
|
+
const masterGain = parameters.masterGain[0];
|
|
558
|
+
const positionParams = this.#extractPositionParams(parameters);
|
|
559
|
+
const playbackRange = this.#calculatePlaybackRange(positionParams);
|
|
560
|
+
const effectivePlaybackRate = parameters.playbackRate[0];
|
|
561
|
+
const tempo = parameters.tempo[0];
|
|
562
|
+
const loopRange = this.#calculateLoopRange(positionParams, playbackRange, parameters.loopDurationDriftAmount[0], tempo, effectivePlaybackRate);
|
|
563
|
+
const amplitudeGain = this.#analyzeLoopAmplitude(loopRange.loopStartSamples, loopRange.loopEndSamples);
|
|
564
|
+
const velocityGain = this.#midiVelocityToGain(parameters.velocity[0]) * this.velocitySensitivity;
|
|
565
|
+
const basePan = parameters.pan[0];
|
|
566
|
+
const effectivePan = this.panDriftEnabled ? Math.max(-1, Math.min(1, basePan + this.currentPanDrift)) : basePan;
|
|
567
|
+
let outputChannels;
|
|
568
|
+
if (output instanceof Float32Array) outputChannels = [output];
|
|
569
|
+
else if (Array.isArray(output) && output.every((ch) => ch instanceof Float32Array)) outputChannels = output;
|
|
570
|
+
else {
|
|
571
|
+
console.error("Unexpected output structure:", {
|
|
572
|
+
outputType: typeof output,
|
|
573
|
+
isArray: Array.isArray(output),
|
|
574
|
+
constructor: output?.constructor?.name,
|
|
575
|
+
length: output?.length
|
|
576
|
+
});
|
|
577
|
+
return true;
|
|
578
|
+
}
|
|
579
|
+
const numChannels = outputChannels.length;
|
|
580
|
+
const isConstant = this.#getConstantFlags(parameters);
|
|
581
|
+
const silencePadTail = loopRange.loopEndSamples > playbackRange.endSamples;
|
|
582
|
+
const TAIL_FADE_SAMPLES = 64;
|
|
583
|
+
if (this.playbackPosition === 0) {
|
|
584
|
+
this.playbackPosition = this.reversePlayback ? playbackRange.endSamples - 1 : playbackRange.startSamples;
|
|
585
|
+
this.#resetDurationPreservation(this.playbackPosition);
|
|
586
|
+
}
|
|
587
|
+
for (let sample = 0; sample < outputChannels[0].length; sample++) {
|
|
588
|
+
const envelopeGain = this.#getSafeParam(parameters.envGain, sample, isConstant.envGain);
|
|
589
|
+
const baseRate = this.#getSafeParam(parameters.playbackRate, sample, isConstant.playbackRate);
|
|
590
|
+
const playbackStep = this.reversePlayback ? -Math.abs(baseRate) : Math.abs(baseRate);
|
|
591
|
+
const canWrapLoop = this.loopEnabled && this.loopCount < parameters.maxLoopCount[0];
|
|
592
|
+
if (canWrapLoop) {
|
|
593
|
+
if (!this.reversePlayback && this.playbackPosition >= loopRange.loopEndSamples) {
|
|
594
|
+
this.#smoothLoopWrap(silencePadTail ? 0 : this.buffer[0][Math.floor(this.playbackPosition - 1)] || 0, this.buffer[0][Math.floor(loopRange.loopStartSamples)] || 0);
|
|
595
|
+
this.playbackPosition = loopRange.loopStartSamples;
|
|
596
|
+
this.loopCount++;
|
|
597
|
+
this.nextDriftGenerated = false;
|
|
598
|
+
} else if (this.reversePlayback && this.playbackPosition <= loopRange.loopStartSamples) {
|
|
599
|
+
this.#smoothLoopWrap(this.buffer[0][Math.floor(loopRange.loopStartSamples)] || 0, silencePadTail ? 0 : this.buffer[0][Math.floor(loopRange.loopEndSamples) - 1] || 0);
|
|
600
|
+
this.playbackPosition = loopRange.loopEndSamples;
|
|
601
|
+
this.loopCount++;
|
|
602
|
+
this.nextDriftGenerated = false;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
const durationResetTarget = this.#prepareDurationPreservingSample(playbackStep, loopRange);
|
|
606
|
+
const shouldStopForward = !this.reversePlayback && (this.#isDurationPreservationActive(loopRange) ? this.durationPreservation.timelinePosition : this.playbackPosition) >= playbackRange.endSamples;
|
|
607
|
+
const shouldStopReverse = this.reversePlayback && (this.#isDurationPreservationActive(loopRange) ? this.durationPreservation.timelinePosition : this.playbackPosition) <= playbackRange.startSamples;
|
|
608
|
+
const isWithinLoop = this.loopEnabled && this.playbackPosition >= loopRange.loopStartSamples && this.playbackPosition <= loopRange.loopEndSamples;
|
|
609
|
+
if ((shouldStopForward || shouldStopReverse) && !(this.loopEnabled && isWithinLoop)) {
|
|
610
|
+
this.#stop();
|
|
611
|
+
return true;
|
|
612
|
+
}
|
|
613
|
+
let tailGain = 1;
|
|
614
|
+
if (silencePadTail) {
|
|
615
|
+
const distToEnd = playbackRange.endSamples - this.playbackPosition;
|
|
616
|
+
if (distToEnd < TAIL_FADE_SAMPLES) tailGain = Math.max(0, distToEnd / TAIL_FADE_SAMPLES);
|
|
617
|
+
}
|
|
618
|
+
const currentPosition = Math.floor(this.playbackPosition);
|
|
619
|
+
const positionOffset = this.playbackPosition - currentPosition;
|
|
620
|
+
let nextPosition, interpWeight;
|
|
621
|
+
if (this.reversePlayback) {
|
|
622
|
+
nextPosition = Math.max(currentPosition - 1, playbackRange.startSamples);
|
|
623
|
+
interpWeight = 1 - positionOffset;
|
|
624
|
+
} else {
|
|
625
|
+
nextPosition = Math.min(currentPosition + 1, playbackRange.endSamples - 1);
|
|
626
|
+
interpWeight = positionOffset;
|
|
627
|
+
}
|
|
628
|
+
for (let channel = 0; channel < numChannels; channel++) {
|
|
629
|
+
if (!outputChannels[channel]) {
|
|
630
|
+
console.warn(`Output channel ${channel} does not exist. Available channels:`, outputChannels.length);
|
|
631
|
+
continue;
|
|
632
|
+
}
|
|
633
|
+
let interpolatedSample = 0;
|
|
634
|
+
for (let l = 0; l < this.layers.length; l++) {
|
|
635
|
+
const layer = this.layers[l];
|
|
636
|
+
const layerChannel = layer[Math.min(channel, layer.length - 1)];
|
|
637
|
+
const currentSample = layerChannel[currentPosition] || 0;
|
|
638
|
+
const nextSample = layerChannel[nextPosition] || 0;
|
|
639
|
+
interpolatedSample += (currentSample + interpWeight * (nextSample - currentSample)) * this.layerGain;
|
|
640
|
+
}
|
|
641
|
+
if (this.applyClickCompensation) {
|
|
642
|
+
interpolatedSample += this.loopClickCompensation;
|
|
643
|
+
if (this.compensationDecay) {
|
|
644
|
+
this.loopClickCompensation *= this.compensationDecay;
|
|
645
|
+
if (Math.abs(this.loopClickCompensation) < .001) this.applyClickCompensation = false;
|
|
646
|
+
} else this.applyClickCompensation = false;
|
|
647
|
+
}
|
|
648
|
+
const finalSample = interpolatedSample * velocityGain * envelopeGain * masterGain * amplitudeGain * tailGain;
|
|
649
|
+
let panAdjustedSample = finalSample;
|
|
650
|
+
if (outputChannels.length === 2) {
|
|
651
|
+
if (channel === 0) panAdjustedSample = finalSample * (1 - Math.max(0, effectivePan));
|
|
652
|
+
else if (channel === 1) panAdjustedSample = finalSample * (1 - Math.max(0, -effectivePan));
|
|
653
|
+
}
|
|
654
|
+
outputChannels[channel][sample] = Math.max(-1, Math.min(1, isFinite(panAdjustedSample) ? panAdjustedSample : 0));
|
|
655
|
+
}
|
|
656
|
+
this.#advanceDurationPreservingPlayback(playbackStep, durationResetTarget, loopRange, canWrapLoop);
|
|
657
|
+
}
|
|
658
|
+
if (this.usePlaybackPosition) {
|
|
659
|
+
const normalizedPosition = this.#samplesToNormalized(this.playbackPosition);
|
|
660
|
+
this.port.postMessage({
|
|
661
|
+
type: "voice:position",
|
|
662
|
+
position: normalizedPosition
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
return true;
|
|
666
|
+
}
|
|
572
667
|
};
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
* Calculate effective loop range in samples with optional drift
|
|
607
|
-
* @param {Object} params - Position parameters from #extractPositionParams
|
|
608
|
-
* @param {Object} playbackRange - Range from #calculatePlaybackRange
|
|
609
|
-
* @param {number} driftAmount - Loop duration drift amount (0-1)
|
|
610
|
-
* @param {number} tempo - Current tempo in BPM
|
|
611
|
-
* @param {number} playbackRate - Current playback rate
|
|
612
|
-
* @returns {Object} - Effective loop start and end positions with drift applied
|
|
613
|
-
*/
|
|
614
|
-
calculateLoopRange_fn = function(params, playbackRange, driftAmount = 0, tempo = 120, playbackRate = 1) {
|
|
615
|
-
const lpStart = params.loopStartSamples;
|
|
616
|
-
const lpEnd = params.loopEndSamples;
|
|
617
|
-
let calcLoopStart = lpStart < lpEnd && lpStart >= 0 ? lpStart : playbackRange.startSamples;
|
|
618
|
-
let calcLoopEnd = lpEnd > lpStart && lpEnd <= playbackRange.endSamples ? lpEnd : playbackRange.endSamples;
|
|
619
|
-
let baseDuration = calcLoopEnd - calcLoopStart;
|
|
620
|
-
if (this.syncLoopToTempo) {
|
|
621
|
-
const quantizedDuration = __privateMethod(this, _SamplePlayerProcessor_instances, quantizeLoopDuration_fn).call(this, baseDuration, tempo, playbackRate);
|
|
622
|
-
calcLoopEnd = calcLoopStart + quantizedDuration;
|
|
623
|
-
calcLoopEnd = Math.min(calcLoopEnd, playbackRange.endSamples);
|
|
624
|
-
}
|
|
625
|
-
if (baseDuration > this.PITCH_PRESERVATION_THRESHOLD && this.keytrackLoopAmount > 0 && !this.syncLoopToTempo) {
|
|
626
|
-
const scale = 1 + this.keytrackLoopAmount * (Math.abs(playbackRate) - 1);
|
|
627
|
-
baseDuration = Math.max(1, Math.floor(baseDuration * scale));
|
|
628
|
-
calcLoopEnd = calcLoopStart + baseDuration;
|
|
629
|
-
}
|
|
630
|
-
baseDuration = calcLoopEnd - calcLoopStart;
|
|
631
|
-
if (baseDuration > this.PITCH_PRESERVATION_THRESHOLD) {
|
|
632
|
-
calcLoopStart = __privateMethod(this, _SamplePlayerProcessor_instances, findNearestZeroCrossing_fn).call(this, calcLoopStart, "right");
|
|
633
|
-
}
|
|
634
|
-
if (driftAmount > 0 && this.loopEnabled) {
|
|
635
|
-
if (!this.nextDriftGenerated || this.loopCount === 0) {
|
|
636
|
-
const updateInterval = baseDuration <= this.PITCH_PRESERVATION_THRESHOLD ? Math.max(
|
|
637
|
-
1,
|
|
638
|
-
Math.floor(this.PITCH_PRESERVATION_THRESHOLD / baseDuration)
|
|
639
|
-
) : 1;
|
|
640
|
-
const shouldUpdateDrift = this.driftUpdateCounter % updateInterval === 0;
|
|
641
|
-
if (shouldUpdateDrift) {
|
|
642
|
-
this.currentLoopDrift = __privateMethod(this, _SamplePlayerProcessor_instances, generateLoopDrift_fn).call(this, driftAmount, baseDuration);
|
|
643
|
-
if (this.panDriftEnabled && driftAmount > 0 && this.loopCount > 0) {
|
|
644
|
-
const panDriftAmountScalar = 1e-4;
|
|
645
|
-
this.currentPanDrift = this.currentLoopDrift * panDriftAmountScalar;
|
|
646
|
-
} else {
|
|
647
|
-
this.currentPanDrift = 0;
|
|
648
|
-
}
|
|
649
|
-
}
|
|
650
|
-
this.driftUpdateCounter++;
|
|
651
|
-
this.nextDriftGenerated = true;
|
|
652
|
-
}
|
|
653
|
-
const driftedLoopEnd = calcLoopEnd + this.currentLoopDrift;
|
|
654
|
-
const minLoopDuration = Math.max(1, Math.floor(baseDuration * 0.1));
|
|
655
|
-
const maxLoopEnd = Math.max(playbackRange.endSamples, calcLoopEnd);
|
|
656
|
-
calcLoopEnd = Math.max(
|
|
657
|
-
calcLoopStart + minLoopDuration,
|
|
658
|
-
Math.min(maxLoopEnd, driftedLoopEnd)
|
|
659
|
-
);
|
|
660
|
-
} else {
|
|
661
|
-
this.currentPanDrift = 0;
|
|
662
|
-
}
|
|
663
|
-
if (baseDuration > this.PITCH_PRESERVATION_THRESHOLD && calcLoopEnd <= playbackRange.endSamples) {
|
|
664
|
-
calcLoopEnd = Math.max(
|
|
665
|
-
calcLoopStart + 1,
|
|
666
|
-
__privateMethod(this, _SamplePlayerProcessor_instances, findNearestZeroCrossing_fn).call(this, calcLoopEnd, "left")
|
|
667
|
-
);
|
|
668
|
-
}
|
|
669
|
-
const loopDuration = calcLoopEnd - calcLoopStart;
|
|
670
|
-
return {
|
|
671
|
-
loopStartSamples: calcLoopStart,
|
|
672
|
-
loopEndSamples: calcLoopEnd,
|
|
673
|
-
loopDurationSamples: loopDuration
|
|
674
|
-
};
|
|
675
|
-
};
|
|
676
|
-
getSafeParam_fn = function(paramArray, index, isConstant) {
|
|
677
|
-
return isConstant ? paramArray[0] : paramArray[Math.min(index, paramArray.length - 1)];
|
|
678
|
-
};
|
|
679
|
-
getConstantFlags_fn = function(parameters) {
|
|
680
|
-
this.constantFlags ?? (this.constantFlags = {
|
|
681
|
-
envGain: true,
|
|
682
|
-
playbackRate: true
|
|
683
|
-
});
|
|
684
|
-
this.constantFlags.envGain = parameters.envGain.length === 1;
|
|
685
|
-
this.constantFlags.playbackRate = parameters.playbackRate.length === 1;
|
|
686
|
-
return this.constantFlags;
|
|
687
|
-
};
|
|
688
|
-
// ===== DURATION PRESERVATION =====
|
|
689
|
-
resetDurationPreservation_fn = function(position = 0) {
|
|
690
|
-
this.durationPreservation.timelinePosition = position;
|
|
691
|
-
this.durationPreservation.resetPending = false;
|
|
692
|
-
};
|
|
693
|
-
isDurationPreservationActive_fn = function(loopRange) {
|
|
694
|
-
var _a;
|
|
695
|
-
return this.durationPreservation.enabled && Boolean((_a = this.zeroCrossings) == null ? void 0 : _a.length) && (!this.loopEnabled || loopRange.loopDurationSamples > this.PITCH_PRESERVATION_THRESHOLD);
|
|
696
|
-
};
|
|
697
|
-
prepareDurationPreservingSample_fn = function(playbackRate, loopRange) {
|
|
698
|
-
const state = this.durationPreservation;
|
|
699
|
-
if (!__privateMethod(this, _SamplePlayerProcessor_instances, isDurationPreservationActive_fn).call(this, loopRange)) return null;
|
|
700
|
-
if (Math.abs(this.playbackPosition - state.timelinePosition) > state.maxDriftSamples) {
|
|
701
|
-
state.resetPending = true;
|
|
702
|
-
}
|
|
703
|
-
if (!state.resetPending) return null;
|
|
704
|
-
const direction = playbackRate < 0 ? "left" : "right";
|
|
705
|
-
const outgoingZero = __privateMethod(this, _SamplePlayerProcessor_instances, findNearestZeroCrossing_fn).call(this, this.playbackPosition, direction);
|
|
706
|
-
if (Math.abs(outgoingZero - this.playbackPosition) > Math.abs(playbackRate)) {
|
|
707
|
-
return null;
|
|
708
|
-
}
|
|
709
|
-
this.playbackPosition = outgoingZero;
|
|
710
|
-
state.resetPending = false;
|
|
711
|
-
return __privateMethod(this, _SamplePlayerProcessor_instances, findNearestZeroCrossing_fn).call(this, state.timelinePosition, "any", state.maxDriftSamples);
|
|
668
|
+
registerProcessor("sample-player-processor", SamplePlayerProcessor);
|
|
669
|
+
//#endregion
|
|
670
|
+
//#region src/worklets/processors/noise/random-noise-processor.js
|
|
671
|
+
var RandomNoiseProcessor = class extends AudioWorkletProcessor {
|
|
672
|
+
constructor() {
|
|
673
|
+
super();
|
|
674
|
+
this.previousNoise = 0;
|
|
675
|
+
this.previousFiltered = 0;
|
|
676
|
+
this.hpfHz = 150;
|
|
677
|
+
this.alpha = this.hpfHz / (this.hpfHz + sampleRate / (2 * Math.PI));
|
|
678
|
+
this.port.onmessage = (event) => {
|
|
679
|
+
if (event.data.type === "setHpfHz") {
|
|
680
|
+
this.hpfHz = event.data.value;
|
|
681
|
+
this.alpha = this.calculateAlpha(this.hpfHz);
|
|
682
|
+
}
|
|
683
|
+
};
|
|
684
|
+
this.port.postMessage({ type: "initialized" });
|
|
685
|
+
}
|
|
686
|
+
calculateAlpha(frequency) {
|
|
687
|
+
return frequency / (frequency + sampleRate / (2 * Math.PI));
|
|
688
|
+
}
|
|
689
|
+
process(inputs, outputs, parameters) {
|
|
690
|
+
outputs[0].forEach((channel) => {
|
|
691
|
+
for (let i = 0; i < channel.length; i++) {
|
|
692
|
+
const noise = Math.random() * 2 - 1;
|
|
693
|
+
const filtered = this.alpha * (noise - this.previousNoise) + this.previousFiltered;
|
|
694
|
+
this.previousNoise = noise;
|
|
695
|
+
this.previousFiltered = filtered;
|
|
696
|
+
channel[i] = filtered;
|
|
697
|
+
}
|
|
698
|
+
});
|
|
699
|
+
return true;
|
|
700
|
+
}
|
|
712
701
|
};
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
}
|
|
723
|
-
} else {
|
|
724
|
-
__privateMethod(this, _SamplePlayerProcessor_instances, resetDurationPreservation_fn).call(this, this.playbackPosition);
|
|
725
|
-
}
|
|
702
|
+
registerProcessor("random-noise-processor", RandomNoiseProcessor);
|
|
703
|
+
//#endregion
|
|
704
|
+
//#region src/worklets/shared/utils/compress-utils.ts
|
|
705
|
+
var cheapSoftClipSingleSample = (sample, max = .9) => {
|
|
706
|
+
const a = Math.abs(sample);
|
|
707
|
+
if (a <= max) return sample;
|
|
708
|
+
const x = a / max;
|
|
709
|
+
const compressed = x / (1 + x);
|
|
710
|
+
return Math.sign(sample) * max * compressed;
|
|
726
711
|
};
|
|
727
712
|
/**
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
const maxDriftSamples = effectiveDriftAmount * baseDuration;
|
|
748
|
-
return Math.floor(randomFactor * maxDriftSamples);
|
|
713
|
+
* Basic attenuation compressor for single sample
|
|
714
|
+
* Note: No validation since optimized for real time use
|
|
715
|
+
*/
|
|
716
|
+
var compressSingleSample = (input, threshold = .75, ratio = 4, limiter = {
|
|
717
|
+
enabled: true,
|
|
718
|
+
type: "soft",
|
|
719
|
+
outputRange: {
|
|
720
|
+
min: -1,
|
|
721
|
+
max: 1
|
|
722
|
+
}
|
|
723
|
+
}) => {
|
|
724
|
+
const { min, max } = limiter.outputRange;
|
|
725
|
+
let x = input;
|
|
726
|
+
if (Math.abs(x) > threshold) x = Math.sign(x) * (threshold + (Math.abs(x) - threshold) / ratio);
|
|
727
|
+
if (limiter.enabled) {
|
|
728
|
+
if (limiter.type === "soft") x = cheapSoftClipSingleSample(x, Math.abs(max));
|
|
729
|
+
else if (limiter.type === "hard") x = Math.max(min, Math.min(max, x));
|
|
730
|
+
}
|
|
731
|
+
return x;
|
|
749
732
|
};
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
let sampleCount = 0;
|
|
769
|
-
const channel = this.buffer[0];
|
|
770
|
-
const startIndex = Math.floor(loopStart);
|
|
771
|
-
const endIndex = Math.floor(loopEnd);
|
|
772
|
-
for (let i = startIndex; i < endIndex && i < channel.length; i++) {
|
|
773
|
-
const sample = channel[i];
|
|
774
|
-
sumSquares += sample * sample;
|
|
775
|
-
sampleCount++;
|
|
776
|
-
}
|
|
777
|
-
if (sampleCount === 0) return 1;
|
|
778
|
-
const rmsAmplitude = Math.sqrt(sumSquares / sampleCount);
|
|
779
|
-
const targetAmplitude = 0.3;
|
|
780
|
-
let makeupGain = 1;
|
|
781
|
-
if (rmsAmplitude < targetAmplitude) {
|
|
782
|
-
const safeRms = Math.max(rmsAmplitude, 1e-3);
|
|
783
|
-
makeupGain = targetAmplitude / safeRms;
|
|
784
|
-
makeupGain = Math.min(2, makeupGain);
|
|
785
|
-
}
|
|
786
|
-
this.lastAnalyzedLoopStart = loopStart;
|
|
787
|
-
this.lastAnalyzedLoopEnd = loopEnd;
|
|
788
|
-
this.loopAmplitudeGain = makeupGain;
|
|
789
|
-
return makeupGain;
|
|
733
|
+
//#endregion
|
|
734
|
+
//#region src/worklets/processors/delay/DelayBuffer.js
|
|
735
|
+
var DelayBuffer = class {
|
|
736
|
+
constructor(maxDelaySamples) {
|
|
737
|
+
this.buffer = new Float32Array(maxDelaySamples);
|
|
738
|
+
this.writePtr = 0;
|
|
739
|
+
this.readPtr = 0;
|
|
740
|
+
}
|
|
741
|
+
write(sample) {
|
|
742
|
+
this.buffer[this.writePtr] = sample;
|
|
743
|
+
}
|
|
744
|
+
read() {
|
|
745
|
+
return this.buffer[this.readPtr];
|
|
746
|
+
}
|
|
747
|
+
updatePointers(delaySamples) {
|
|
748
|
+
this.writePtr = (this.writePtr + 1) % this.buffer.length;
|
|
749
|
+
this.readPtr = (this.writePtr - delaySamples + this.buffer.length) % this.buffer.length;
|
|
750
|
+
}
|
|
790
751
|
};
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
752
|
+
//#endregion
|
|
753
|
+
//#region src/worklets/processors/delay/FeedbackDelay.js
|
|
754
|
+
var AUTO_GAIN_THRESHOLD = .8;
|
|
755
|
+
var SAFETY_GAIN_COMPENSATION = .2;
|
|
756
|
+
var FeedbackDelay = class {
|
|
757
|
+
constructor(sampleRate) {
|
|
758
|
+
this.sampleRate = sampleRate;
|
|
759
|
+
this.buffers = [];
|
|
760
|
+
this.initialized = false;
|
|
761
|
+
this.autoGainEnabled = false;
|
|
762
|
+
this.gainCompensation = SAFETY_GAIN_COMPENSATION;
|
|
763
|
+
this.lowpassStates = [];
|
|
764
|
+
this.highpassStates = [];
|
|
765
|
+
this.highpassInputStates = [];
|
|
766
|
+
}
|
|
767
|
+
initializeBuffers(channelCount) {
|
|
768
|
+
this.buffers = [];
|
|
769
|
+
this.lowpassStates = [];
|
|
770
|
+
this.highpassStates = [];
|
|
771
|
+
this.highpassInputStates = [];
|
|
772
|
+
const maxSamples = Math.floor(this.sampleRate * 2);
|
|
773
|
+
for (let c = 0; c < channelCount; c++) {
|
|
774
|
+
this.buffers[c] = new DelayBuffer(maxSamples);
|
|
775
|
+
this.lowpassStates[c] = 0;
|
|
776
|
+
this.highpassStates[c] = 0;
|
|
777
|
+
this.highpassInputStates[c] = 0;
|
|
778
|
+
}
|
|
779
|
+
this.initialized = true;
|
|
780
|
+
}
|
|
781
|
+
/** Simple one-pole lowpass filter */
|
|
782
|
+
lowpass(input, cutoffFreq, channelIndex) {
|
|
783
|
+
if (cutoffFreq >= this.sampleRate * .4) return input;
|
|
784
|
+
const omega = 2 * Math.PI * cutoffFreq / this.sampleRate;
|
|
785
|
+
const alpha = Math.max(0, Math.min(.99, Math.sin(omega) / (Math.sin(omega) + Math.cos(omega))));
|
|
786
|
+
this.lowpassStates[channelIndex] = alpha * input + (1 - alpha) * this.lowpassStates[channelIndex];
|
|
787
|
+
return this.lowpassStates[channelIndex];
|
|
788
|
+
}
|
|
789
|
+
/** Simple one-pole highpass filter */
|
|
790
|
+
highpass(input, cutoffFreq, channelIndex) {
|
|
791
|
+
if (cutoffFreq < 5) return input;
|
|
792
|
+
const omega = 2 * Math.PI * cutoffFreq / this.sampleRate;
|
|
793
|
+
const alpha = Math.max(0, Math.min(.99, Math.sin(omega) / (Math.sin(omega) + Math.cos(omega))));
|
|
794
|
+
const lowpassOutput = alpha * input + (1 - alpha) * this.highpassStates[channelIndex];
|
|
795
|
+
const highpassOutput = input - lowpassOutput;
|
|
796
|
+
this.highpassStates[channelIndex] = lowpassOutput;
|
|
797
|
+
return highpassOutput;
|
|
798
|
+
}
|
|
799
|
+
process(inputSample, channelIndex, feedbackAmount, delayTime, lowpassFreq = 1e4, highpassFreq = 100) {
|
|
800
|
+
if (!this.initialized) return inputSample;
|
|
801
|
+
const buffer = this.buffers[channelIndex] || this.buffers[0];
|
|
802
|
+
const delaySamples = Math.floor(this.sampleRate * delayTime);
|
|
803
|
+
const delayedSample = buffer.read();
|
|
804
|
+
let filteredDelay = this.highpass(delayedSample, highpassFreq, channelIndex);
|
|
805
|
+
filteredDelay = this.lowpass(filteredDelay, lowpassFreq, channelIndex);
|
|
806
|
+
const feedbackSample = feedbackAmount * filteredDelay + inputSample;
|
|
807
|
+
let outputSample = feedbackSample;
|
|
808
|
+
const compressedFeedback = compressSingleSample(feedbackSample, .5, 4, {
|
|
809
|
+
enabled: true,
|
|
810
|
+
outputRange: {
|
|
811
|
+
min: -.99,
|
|
812
|
+
max: .99
|
|
813
|
+
},
|
|
814
|
+
type: "soft"
|
|
815
|
+
});
|
|
816
|
+
if (this.autoGainEnabled && feedbackAmount > AUTO_GAIN_THRESHOLD) outputSample = compressedFeedback * (1 - (feedbackAmount - AUTO_GAIN_THRESHOLD) * this.gainCompensation);
|
|
817
|
+
return {
|
|
818
|
+
outputSample,
|
|
819
|
+
feedbackSample: compressedFeedback,
|
|
820
|
+
delaySamples
|
|
821
|
+
};
|
|
822
|
+
}
|
|
823
|
+
updateBuffer(channelIndex, sample, delaySamples) {
|
|
824
|
+
const buffer = this.buffers[channelIndex] || this.buffers[0];
|
|
825
|
+
buffer.write(sample);
|
|
826
|
+
buffer.updatePointers(delaySamples);
|
|
827
|
+
}
|
|
828
|
+
setAutoGain(enabled, compensation = SAFETY_GAIN_COMPENSATION) {
|
|
829
|
+
this.autoGainEnabled = enabled;
|
|
830
|
+
this.gainCompensation = compensation;
|
|
831
|
+
}
|
|
831
832
|
};
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
833
|
+
//#endregion
|
|
834
|
+
//#region src/worklets/processors/delay/feedback-delay-processor.js
|
|
835
|
+
registerProcessor("feedback-delay-processor", class extends AudioWorkletProcessor {
|
|
836
|
+
static get parameterDescriptors() {
|
|
837
|
+
return [
|
|
838
|
+
{
|
|
839
|
+
name: "feedbackAmount",
|
|
840
|
+
defaultValue: .5,
|
|
841
|
+
minValue: 0,
|
|
842
|
+
maxValue: 1,
|
|
843
|
+
automationRate: "k-rate"
|
|
844
|
+
},
|
|
845
|
+
{
|
|
846
|
+
name: "delayTime",
|
|
847
|
+
defaultValue: .5,
|
|
848
|
+
minValue: .00012656238799684143,
|
|
849
|
+
maxValue: 2,
|
|
850
|
+
automationRate: "k-rate"
|
|
851
|
+
},
|
|
852
|
+
{
|
|
853
|
+
name: "decay",
|
|
854
|
+
defaultValue: 1,
|
|
855
|
+
minValue: 0,
|
|
856
|
+
maxValue: 1,
|
|
857
|
+
automationRate: "k-rate"
|
|
858
|
+
},
|
|
859
|
+
{
|
|
860
|
+
name: "lowpass",
|
|
861
|
+
defaultValue: 1e4,
|
|
862
|
+
minValue: 100,
|
|
863
|
+
maxValue: 16e3,
|
|
864
|
+
automationRate: "k-rate"
|
|
865
|
+
}
|
|
866
|
+
];
|
|
867
|
+
}
|
|
868
|
+
constructor() {
|
|
869
|
+
super();
|
|
870
|
+
this.feedbackDelay = new FeedbackDelay(sampleRate);
|
|
871
|
+
this.decayStartTime = null;
|
|
872
|
+
this.decayActive = false;
|
|
873
|
+
this.baseFeedbackAmount = .5;
|
|
874
|
+
this.setupMessageHandling();
|
|
875
|
+
this.port.postMessage({ type: "initialized" });
|
|
876
|
+
}
|
|
877
|
+
setupMessageHandling() {
|
|
878
|
+
this.port.onmessage = (event) => {
|
|
879
|
+
switch (event.data.type) {
|
|
880
|
+
case "setAutoGain":
|
|
881
|
+
this.feedbackDelay.setAutoGain(event.data.enabled, event.data.amount);
|
|
882
|
+
break;
|
|
883
|
+
case "triggerDecay":
|
|
884
|
+
this.decayStartTime = currentTime;
|
|
885
|
+
this.decayActive = true;
|
|
886
|
+
this.baseFeedbackAmount = event.data.baseFeedbackAmount || .5;
|
|
887
|
+
break;
|
|
888
|
+
case "stopDecay":
|
|
889
|
+
this.decayActive = false;
|
|
890
|
+
this.decayStartTime = null;
|
|
891
|
+
}
|
|
892
|
+
};
|
|
893
|
+
}
|
|
894
|
+
process(inputs, outputs, parameters) {
|
|
895
|
+
const input = inputs[0];
|
|
896
|
+
const output = outputs[0];
|
|
897
|
+
if (!input || !output) return true;
|
|
898
|
+
if (!this.feedbackDelay.initialized || this.feedbackDelay.buffers.length !== input.length) this.feedbackDelay.initializeBuffers(input.length);
|
|
899
|
+
const baseFeedbackAmount = parameters.feedbackAmount[0];
|
|
900
|
+
const delayTime = parameters.delayTime[0];
|
|
901
|
+
const decay = parameters.decay[0];
|
|
902
|
+
const lowpassFreq = parameters.lowpass[0];
|
|
903
|
+
const channelCount = Math.min(input.length, output.length);
|
|
904
|
+
const frameCount = output[0].length;
|
|
905
|
+
for (let i = 0; i < frameCount; ++i) {
|
|
906
|
+
let effectiveFeedbackAmount = baseFeedbackAmount;
|
|
907
|
+
if (this.decayActive && this.decayStartTime !== null) {
|
|
908
|
+
const elapsedTime = currentTime - this.decayStartTime + i / sampleRate;
|
|
909
|
+
const delayCompensation = Math.min(100, .5 / delayTime);
|
|
910
|
+
const timeConstant = Math.pow(decay, 5) * 1e3 * delayCompensation + .5;
|
|
911
|
+
effectiveFeedbackAmount = baseFeedbackAmount * Math.exp(-elapsedTime / timeConstant);
|
|
912
|
+
if (effectiveFeedbackAmount < .01) {
|
|
913
|
+
this.decayActive = false;
|
|
914
|
+
effectiveFeedbackAmount = 0;
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
for (let c = 0; c < channelCount; c++) {
|
|
918
|
+
const processed = this.feedbackDelay.process(input[c][i], c, effectiveFeedbackAmount, delayTime, lowpassFreq);
|
|
919
|
+
output[c][i] = processed.outputSample;
|
|
920
|
+
this.feedbackDelay.updateBuffer(c, processed.feedbackSample, processed.delaySamples);
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
return true;
|
|
924
|
+
}
|
|
925
|
+
});
|
|
926
|
+
//#endregion
|
|
927
|
+
//#region src/worklets/processors/delay/delay-processor.js
|
|
928
|
+
var DEFAULT_DELAY_CONFIG = {
|
|
929
|
+
CHARACTER: ["filtered"],
|
|
930
|
+
SMOOTHING_FACTOR: {
|
|
931
|
+
slowest: 1e-4,
|
|
932
|
+
slow: 25e-5,
|
|
933
|
+
medium: 35e-5,
|
|
934
|
+
fast: 5e-4,
|
|
935
|
+
veryFast: .001,
|
|
936
|
+
superFast: .1,
|
|
937
|
+
none: 1
|
|
938
|
+
}
|
|
846
939
|
};
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
read() {
|
|
857
|
-
return this.buffer[this.readPtr];
|
|
858
|
-
}
|
|
859
|
-
updatePointers(delaySamples) {
|
|
860
|
-
this.writePtr = (this.writePtr + 1) % this.buffer.length;
|
|
861
|
-
this.readPtr = (this.writePtr - delaySamples + this.buffer.length) % this.buffer.length;
|
|
862
|
-
}
|
|
863
|
-
}
|
|
864
|
-
const AUTO_GAIN_THRESHOLD = 0.8;
|
|
865
|
-
const SAFETY_GAIN_COMPENSATION = 0.2;
|
|
866
|
-
class FeedbackDelay {
|
|
867
|
-
constructor(sampleRate2) {
|
|
868
|
-
this.sampleRate = sampleRate2;
|
|
869
|
-
this.buffers = [];
|
|
870
|
-
this.initialized = false;
|
|
871
|
-
this.autoGainEnabled = false;
|
|
872
|
-
this.gainCompensation = SAFETY_GAIN_COMPENSATION;
|
|
873
|
-
this.lowpassStates = [];
|
|
874
|
-
this.highpassStates = [];
|
|
875
|
-
this.highpassInputStates = [];
|
|
876
|
-
}
|
|
877
|
-
initializeBuffers(channelCount) {
|
|
878
|
-
this.buffers = [];
|
|
879
|
-
this.lowpassStates = [];
|
|
880
|
-
this.highpassStates = [];
|
|
881
|
-
this.highpassInputStates = [];
|
|
882
|
-
const maxSamples = Math.floor(this.sampleRate * 2);
|
|
883
|
-
for (let c = 0; c < channelCount; c++) {
|
|
884
|
-
this.buffers[c] = new DelayBuffer(maxSamples);
|
|
885
|
-
this.lowpassStates[c] = 0;
|
|
886
|
-
this.highpassStates[c] = 0;
|
|
887
|
-
this.highpassInputStates[c] = 0;
|
|
888
|
-
}
|
|
889
|
-
this.initialized = true;
|
|
890
|
-
}
|
|
891
|
-
/** Simple one-pole lowpass filter */
|
|
892
|
-
lowpass(input, cutoffFreq, channelIndex) {
|
|
893
|
-
if (cutoffFreq >= this.sampleRate * 0.4) {
|
|
894
|
-
return input;
|
|
895
|
-
}
|
|
896
|
-
const omega = 2 * Math.PI * cutoffFreq / this.sampleRate;
|
|
897
|
-
const alpha = Math.max(
|
|
898
|
-
0,
|
|
899
|
-
Math.min(0.99, Math.sin(omega) / (Math.sin(omega) + Math.cos(omega)))
|
|
900
|
-
);
|
|
901
|
-
this.lowpassStates[channelIndex] = alpha * input + (1 - alpha) * this.lowpassStates[channelIndex];
|
|
902
|
-
return this.lowpassStates[channelIndex];
|
|
903
|
-
}
|
|
904
|
-
/** Simple one-pole highpass filter */
|
|
905
|
-
highpass(input, cutoffFreq, channelIndex) {
|
|
906
|
-
if (cutoffFreq < 5) return input;
|
|
907
|
-
const omega = 2 * Math.PI * cutoffFreq / this.sampleRate;
|
|
908
|
-
const alpha = Math.max(
|
|
909
|
-
0,
|
|
910
|
-
Math.min(0.99, Math.sin(omega) / (Math.sin(omega) + Math.cos(omega)))
|
|
911
|
-
);
|
|
912
|
-
const lowpassOutput = alpha * input + (1 - alpha) * this.highpassStates[channelIndex];
|
|
913
|
-
const highpassOutput = input - lowpassOutput;
|
|
914
|
-
this.highpassStates[channelIndex] = lowpassOutput;
|
|
915
|
-
return highpassOutput;
|
|
916
|
-
}
|
|
917
|
-
process(inputSample, channelIndex, feedbackAmount, delayTime, lowpassFreq = 1e4, highpassFreq = 100) {
|
|
918
|
-
if (!this.initialized) return inputSample;
|
|
919
|
-
const buffer = this.buffers[channelIndex] || this.buffers[0];
|
|
920
|
-
const delaySamples = Math.floor(this.sampleRate * delayTime);
|
|
921
|
-
const delayedSample = buffer.read();
|
|
922
|
-
let filteredDelay = this.highpass(
|
|
923
|
-
delayedSample,
|
|
924
|
-
highpassFreq,
|
|
925
|
-
channelIndex
|
|
926
|
-
);
|
|
927
|
-
filteredDelay = this.lowpass(filteredDelay, lowpassFreq, channelIndex);
|
|
928
|
-
const feedbackSample = feedbackAmount * filteredDelay + inputSample;
|
|
929
|
-
let outputSample = feedbackSample;
|
|
930
|
-
const compressedFeedback = compressSingleSample(feedbackSample, 0.5, 4, {
|
|
931
|
-
enabled: true,
|
|
932
|
-
// limiter enabled
|
|
933
|
-
outputRange: { min: -0.99, max: 0.99 },
|
|
934
|
-
type: "soft"
|
|
935
|
-
// soft clip
|
|
936
|
-
});
|
|
937
|
-
if (this.autoGainEnabled && feedbackAmount > AUTO_GAIN_THRESHOLD) {
|
|
938
|
-
const safetyReduction = 1 - (feedbackAmount - AUTO_GAIN_THRESHOLD) * this.gainCompensation;
|
|
939
|
-
outputSample = compressedFeedback * safetyReduction;
|
|
940
|
-
}
|
|
941
|
-
return { outputSample, feedbackSample: compressedFeedback, delaySamples };
|
|
942
|
-
}
|
|
943
|
-
updateBuffer(channelIndex, sample, delaySamples) {
|
|
944
|
-
const buffer = this.buffers[channelIndex] || this.buffers[0];
|
|
945
|
-
buffer.write(sample);
|
|
946
|
-
buffer.updatePointers(delaySamples);
|
|
947
|
-
}
|
|
948
|
-
setAutoGain(enabled, compensation = SAFETY_GAIN_COMPENSATION) {
|
|
949
|
-
this.autoGainEnabled = enabled;
|
|
950
|
-
this.gainCompensation = compensation;
|
|
951
|
-
}
|
|
952
|
-
}
|
|
953
|
-
registerProcessor(
|
|
954
|
-
"feedback-delay-processor",
|
|
955
|
-
class extends AudioWorkletProcessor {
|
|
956
|
-
static get parameterDescriptors() {
|
|
957
|
-
return [
|
|
958
|
-
{
|
|
959
|
-
name: "feedbackAmount",
|
|
960
|
-
defaultValue: 0.5,
|
|
961
|
-
minValue: 0,
|
|
962
|
-
maxValue: 1,
|
|
963
|
-
automationRate: "k-rate"
|
|
964
|
-
},
|
|
965
|
-
{
|
|
966
|
-
name: "delayTime",
|
|
967
|
-
defaultValue: 0.5,
|
|
968
|
-
minValue: 12656238799684143e-20,
|
|
969
|
-
// <- B8 natural in seconds (highest note period that works)
|
|
970
|
-
maxValue: 2,
|
|
971
|
-
automationRate: "k-rate"
|
|
972
|
-
},
|
|
973
|
-
{
|
|
974
|
-
name: "decay",
|
|
975
|
-
// feedback decay time factor
|
|
976
|
-
defaultValue: 1,
|
|
977
|
-
minValue: 0,
|
|
978
|
-
maxValue: 1,
|
|
979
|
-
automationRate: "k-rate"
|
|
980
|
-
},
|
|
981
|
-
{
|
|
982
|
-
name: "lowpass",
|
|
983
|
-
defaultValue: 1e4,
|
|
984
|
-
minValue: 100,
|
|
985
|
-
maxValue: 16e3,
|
|
986
|
-
automationRate: "k-rate"
|
|
987
|
-
}
|
|
988
|
-
];
|
|
989
|
-
}
|
|
990
|
-
constructor() {
|
|
991
|
-
super();
|
|
992
|
-
this.feedbackDelay = new FeedbackDelay(sampleRate);
|
|
993
|
-
this.decayStartTime = null;
|
|
994
|
-
this.decayActive = false;
|
|
995
|
-
this.baseFeedbackAmount = 0.5;
|
|
996
|
-
this.setupMessageHandling();
|
|
997
|
-
this.port.postMessage({ type: "initialized" });
|
|
998
|
-
}
|
|
999
|
-
setupMessageHandling() {
|
|
1000
|
-
this.port.onmessage = (event) => {
|
|
1001
|
-
switch (event.data.type) {
|
|
1002
|
-
case "setAutoGain":
|
|
1003
|
-
this.feedbackDelay.setAutoGain(
|
|
1004
|
-
event.data.enabled,
|
|
1005
|
-
event.data.amount
|
|
1006
|
-
);
|
|
1007
|
-
break;
|
|
1008
|
-
case "triggerDecay":
|
|
1009
|
-
this.decayStartTime = currentTime;
|
|
1010
|
-
this.decayActive = true;
|
|
1011
|
-
this.baseFeedbackAmount = event.data.baseFeedbackAmount || 0.5;
|
|
1012
|
-
break;
|
|
1013
|
-
case "stopDecay":
|
|
1014
|
-
this.decayActive = false;
|
|
1015
|
-
this.decayStartTime = null;
|
|
1016
|
-
break;
|
|
1017
|
-
}
|
|
1018
|
-
};
|
|
1019
|
-
}
|
|
1020
|
-
process(inputs, outputs, parameters) {
|
|
1021
|
-
const input = inputs[0];
|
|
1022
|
-
const output = outputs[0];
|
|
1023
|
-
if (!input || !output) return true;
|
|
1024
|
-
if (!this.feedbackDelay.initialized || this.feedbackDelay.buffers.length !== input.length) {
|
|
1025
|
-
this.feedbackDelay.initializeBuffers(input.length);
|
|
1026
|
-
}
|
|
1027
|
-
const baseFeedbackAmount = parameters.feedbackAmount[0];
|
|
1028
|
-
const delayTime = parameters.delayTime[0];
|
|
1029
|
-
const decay = parameters.decay[0];
|
|
1030
|
-
const lowpassFreq = parameters.lowpass[0];
|
|
1031
|
-
const channelCount = Math.min(input.length, output.length);
|
|
1032
|
-
const frameCount = output[0].length;
|
|
1033
|
-
for (let i = 0; i < frameCount; ++i) {
|
|
1034
|
-
let effectiveFeedbackAmount = baseFeedbackAmount;
|
|
1035
|
-
if (this.decayActive && this.decayStartTime !== null) {
|
|
1036
|
-
const elapsedTime = currentTime - this.decayStartTime + i / sampleRate;
|
|
1037
|
-
const delayCompensation = Math.min(100, 0.5 / delayTime);
|
|
1038
|
-
const timeConstant = Math.pow(decay, 5) * 1e3 * delayCompensation + 0.5;
|
|
1039
|
-
const decayFactor = Math.exp(-elapsedTime / timeConstant);
|
|
1040
|
-
effectiveFeedbackAmount = baseFeedbackAmount * decayFactor;
|
|
1041
|
-
if (effectiveFeedbackAmount < 0.01) {
|
|
1042
|
-
this.decayActive = false;
|
|
1043
|
-
effectiveFeedbackAmount = 0;
|
|
1044
|
-
}
|
|
1045
|
-
}
|
|
1046
|
-
for (let c = 0; c < channelCount; c++) {
|
|
1047
|
-
const processed = this.feedbackDelay.process(
|
|
1048
|
-
input[c][i],
|
|
1049
|
-
c,
|
|
1050
|
-
effectiveFeedbackAmount,
|
|
1051
|
-
delayTime,
|
|
1052
|
-
lowpassFreq
|
|
1053
|
-
);
|
|
1054
|
-
output[c][i] = processed.outputSample;
|
|
1055
|
-
this.feedbackDelay.updateBuffer(
|
|
1056
|
-
c,
|
|
1057
|
-
processed.feedbackSample,
|
|
1058
|
-
processed.delaySamples
|
|
1059
|
-
);
|
|
1060
|
-
}
|
|
1061
|
-
}
|
|
1062
|
-
return true;
|
|
1063
|
-
}
|
|
1064
|
-
}
|
|
1065
|
-
);
|
|
1066
|
-
const DEFAULT_DELAY_CONFIG = {
|
|
1067
|
-
CHARACTER: ["filtered"],
|
|
1068
|
-
// 'clean' | 'bitCrushed' | 'filtered' or combo
|
|
1069
|
-
// Smoothing factor for delay time interpolation
|
|
1070
|
-
SMOOTHING_FACTOR: {
|
|
1071
|
-
slowest: 1e-4
|
|
1072
|
-
}
|
|
940
|
+
var DEFAULT_CHARACTER_CONFIG = {
|
|
941
|
+
bitCrushed: {
|
|
942
|
+
bits: 11,
|
|
943
|
+
downsample: 3
|
|
944
|
+
},
|
|
945
|
+
filtered: {
|
|
946
|
+
freq: 900,
|
|
947
|
+
Q: .15
|
|
948
|
+
}
|
|
1073
949
|
};
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
950
|
+
registerProcessor("delay-processor", class extends AudioWorkletProcessor {
|
|
951
|
+
static get parameterDescriptors() {
|
|
952
|
+
return [{
|
|
953
|
+
name: "delayTime",
|
|
954
|
+
defaultValue: .5,
|
|
955
|
+
minValue: .001,
|
|
956
|
+
maxValue: 2,
|
|
957
|
+
automationRate: "k-rate"
|
|
958
|
+
}, {
|
|
959
|
+
name: "feedbackAmount",
|
|
960
|
+
defaultValue: 0,
|
|
961
|
+
minValue: 0,
|
|
962
|
+
maxValue: .99,
|
|
963
|
+
automationRate: "k-rate"
|
|
964
|
+
}];
|
|
965
|
+
}
|
|
966
|
+
constructor() {
|
|
967
|
+
super();
|
|
968
|
+
this.buffers = [];
|
|
969
|
+
this.smoothedDelaySamples = [];
|
|
970
|
+
this.smoothingFactor = DEFAULT_DELAY_CONFIG.SMOOTHING_FACTOR.slowest;
|
|
971
|
+
this.characterModes = [...DEFAULT_DELAY_CONFIG.CHARACTER];
|
|
972
|
+
this._bpState = [];
|
|
973
|
+
this._bpFreq = DEFAULT_CHARACTER_CONFIG.filtered.freq;
|
|
974
|
+
this._bpQ = DEFAULT_CHARACTER_CONFIG.filtered.Q;
|
|
975
|
+
this._bpCoeffs = null;
|
|
976
|
+
this._lastBpFreq = -1;
|
|
977
|
+
this._lastBpQ = -1;
|
|
978
|
+
this.lofiBits = DEFAULT_CHARACTER_CONFIG["bitCrushed"].bits;
|
|
979
|
+
this.lofiDownsample = DEFAULT_CHARACTER_CONFIG["bitCrushed"].downsample;
|
|
980
|
+
this._lofiSampleHold = [];
|
|
981
|
+
this._lofiSampleCount = [];
|
|
982
|
+
this.initialized = false;
|
|
983
|
+
this.port.onmessage = (event) => {
|
|
984
|
+
if (event.data && event.data.type === "setCharacter" && Array.isArray(event.data.modes)) this.characterModes = [...event.data.modes];
|
|
985
|
+
if (event.data && event.data.type === "setBandpassFreq" && typeof event.data.hz === "number") this.setBandpassFreq(event.data.hz);
|
|
986
|
+
if (event.data && event.data.type === "trigger") {}
|
|
987
|
+
};
|
|
988
|
+
this.port.postMessage({ type: "initialized" });
|
|
989
|
+
}
|
|
990
|
+
setBandpassFreq(hz) {
|
|
991
|
+
this._bpFreq = hz;
|
|
992
|
+
this._lastBpFreq = -1;
|
|
993
|
+
}
|
|
994
|
+
_updateBandpassCoeffs() {
|
|
995
|
+
if (this._lastBpFreq === this._bpFreq && this._lastBpQ === this._bpQ) return;
|
|
996
|
+
const bpFreq = this._bpFreq;
|
|
997
|
+
const bpQ = this._bpQ;
|
|
998
|
+
const omega = 2 * Math.PI * bpFreq / sampleRate;
|
|
999
|
+
const alpha = Math.sin(omega) / (2 * bpQ);
|
|
1000
|
+
const cosw = Math.cos(omega);
|
|
1001
|
+
const b0 = alpha;
|
|
1002
|
+
const b1 = 0;
|
|
1003
|
+
const b2 = -alpha;
|
|
1004
|
+
const a0 = 1 + alpha;
|
|
1005
|
+
const a1 = -2 * cosw;
|
|
1006
|
+
const a2 = 1 - alpha;
|
|
1007
|
+
this._bpCoeffs = {
|
|
1008
|
+
b0: b0 / a0,
|
|
1009
|
+
b1: b1 / a0,
|
|
1010
|
+
b2: b2 / a0,
|
|
1011
|
+
a1: a1 / a0,
|
|
1012
|
+
a2: a2 / a0
|
|
1013
|
+
};
|
|
1014
|
+
this._lastBpFreq = bpFreq;
|
|
1015
|
+
this._lastBpQ = bpQ;
|
|
1016
|
+
}
|
|
1017
|
+
initializeBuffers(channelCount) {
|
|
1018
|
+
const maxSamples = Math.floor(sampleRate * 2);
|
|
1019
|
+
this.buffers = [];
|
|
1020
|
+
this.smoothedDelaySamples = [];
|
|
1021
|
+
this._lofiSampleHold = [];
|
|
1022
|
+
this._lofiSampleCount = [];
|
|
1023
|
+
for (let c = 0; c < channelCount; c++) {
|
|
1024
|
+
this.buffers[c] = new DelayBuffer(maxSamples);
|
|
1025
|
+
this.smoothedDelaySamples[c] = Math.floor(sampleRate * .5);
|
|
1026
|
+
this._lofiSampleHold[c] = 0;
|
|
1027
|
+
this._lofiSampleCount[c] = 0;
|
|
1028
|
+
}
|
|
1029
|
+
this.initialized = true;
|
|
1030
|
+
}
|
|
1031
|
+
_processLoFi(delayed, c) {
|
|
1032
|
+
if (this._lofiSampleCount[c] % this.lofiDownsample === 0) {
|
|
1033
|
+
const levels = Math.pow(2, this.lofiBits);
|
|
1034
|
+
delayed = Math.round(delayed * levels) / levels;
|
|
1035
|
+
this._lofiSampleHold[c] = delayed;
|
|
1036
|
+
} else delayed = this._lofiSampleHold[c];
|
|
1037
|
+
this._lofiSampleCount[c]++;
|
|
1038
|
+
return delayed;
|
|
1039
|
+
}
|
|
1040
|
+
_processBandpass(delayed, c) {
|
|
1041
|
+
if (!this._bpState) this._bpState = [];
|
|
1042
|
+
if (!this._bpState[c]) this._bpState[c] = {
|
|
1043
|
+
x1: 0,
|
|
1044
|
+
x2: 0,
|
|
1045
|
+
y1: 0,
|
|
1046
|
+
y2: 0
|
|
1047
|
+
};
|
|
1048
|
+
this._updateBandpassCoeffs();
|
|
1049
|
+
if (!this._bpCoeffs) return delayed;
|
|
1050
|
+
const { b0, b1, b2, a1, a2 } = this._bpCoeffs;
|
|
1051
|
+
const s = this._bpState[c];
|
|
1052
|
+
const y = b0 * delayed + b1 * s.x1 + b2 * s.x2 - a1 * s.y1 - a2 * s.y2;
|
|
1053
|
+
s.x2 = s.x1;
|
|
1054
|
+
s.x1 = delayed;
|
|
1055
|
+
s.y2 = s.y1;
|
|
1056
|
+
s.y1 = y;
|
|
1057
|
+
return y;
|
|
1058
|
+
}
|
|
1059
|
+
process(inputs, outputs, parameters) {
|
|
1060
|
+
const input = inputs[0];
|
|
1061
|
+
const output = outputs[0];
|
|
1062
|
+
if (!input || !output || input.length === 0 || output.length === 0) return true;
|
|
1063
|
+
if (!input[0] || !output[0] || input[0].length === 0 || output[0].length === 0) return true;
|
|
1064
|
+
if (!this.initialized || this.buffers.length !== input.length) this.initializeBuffers(input.length);
|
|
1065
|
+
const delayTime = parameters.delayTime[0];
|
|
1066
|
+
const feedbackAmount = parameters.feedbackAmount[0];
|
|
1067
|
+
const targetDelaySamples = sampleRate * delayTime;
|
|
1068
|
+
const channelCount = Math.min(input.length, output.length);
|
|
1069
|
+
const frameCount = output[0].length;
|
|
1070
|
+
const smoothing = this.smoothingFactor;
|
|
1071
|
+
for (let i = 0; i < frameCount; ++i) for (let c = 0; c < channelCount; c++) {
|
|
1072
|
+
const buf = this.buffers[c];
|
|
1073
|
+
if (!buf) continue;
|
|
1074
|
+
this.smoothedDelaySamples[c] += (targetDelaySamples - this.smoothedDelaySamples[c]) * smoothing;
|
|
1075
|
+
const smoothedDelay = this.smoothedDelaySamples[c];
|
|
1076
|
+
const intDelay = Math.floor(smoothedDelay);
|
|
1077
|
+
const frac = smoothedDelay - intDelay;
|
|
1078
|
+
const readPtrA = (buf.writePtr - intDelay + buf.buffer.length) % buf.buffer.length;
|
|
1079
|
+
const readPtrB = (readPtrA - 1 + buf.buffer.length) % buf.buffer.length;
|
|
1080
|
+
const sampleA = buf.buffer[readPtrA];
|
|
1081
|
+
const sampleB = buf.buffer[readPtrB];
|
|
1082
|
+
let delayed = sampleA * (1 - frac) + sampleB * frac;
|
|
1083
|
+
for (const mode of this.characterModes) if (mode === "bitCrushed") delayed = this._processLoFi(delayed, c);
|
|
1084
|
+
else if (mode === "filtered") delayed = this._processBandpass(delayed, c);
|
|
1085
|
+
output[c][i] = compressSingleSample(delayed, .75, 4, {
|
|
1086
|
+
enabled: true,
|
|
1087
|
+
type: "soft",
|
|
1088
|
+
outputRange: {
|
|
1089
|
+
min: -.9,
|
|
1090
|
+
max: .9
|
|
1091
|
+
}
|
|
1092
|
+
});
|
|
1093
|
+
const inputSample = input[c] && input[c][i] !== void 0 ? input[c][i] : 0;
|
|
1094
|
+
buf.write(inputSample + delayed * feedbackAmount);
|
|
1095
|
+
buf.updatePointers(intDelay);
|
|
1096
|
+
}
|
|
1097
|
+
return true;
|
|
1098
|
+
}
|
|
1099
|
+
});
|
|
1100
|
+
//#endregion
|
|
1101
|
+
//#region src/worklets/processors/reverb/dattorro-reverb-processor.js
|
|
1102
|
+
var DattorroReverb = class extends AudioWorkletProcessor {
|
|
1103
|
+
static get parameterDescriptors() {
|
|
1104
|
+
return [
|
|
1105
|
+
[
|
|
1106
|
+
"preDelay",
|
|
1107
|
+
0,
|
|
1108
|
+
0,
|
|
1109
|
+
sampleRate - 1,
|
|
1110
|
+
"k-rate"
|
|
1111
|
+
],
|
|
1112
|
+
[
|
|
1113
|
+
"bandwidth",
|
|
1114
|
+
.9999,
|
|
1115
|
+
0,
|
|
1116
|
+
1,
|
|
1117
|
+
"k-rate"
|
|
1118
|
+
],
|
|
1119
|
+
[
|
|
1120
|
+
"inputDiffusion1",
|
|
1121
|
+
.75,
|
|
1122
|
+
0,
|
|
1123
|
+
1,
|
|
1124
|
+
"k-rate"
|
|
1125
|
+
],
|
|
1126
|
+
[
|
|
1127
|
+
"inputDiffusion2",
|
|
1128
|
+
.625,
|
|
1129
|
+
0,
|
|
1130
|
+
1,
|
|
1131
|
+
"k-rate"
|
|
1132
|
+
],
|
|
1133
|
+
[
|
|
1134
|
+
"decay",
|
|
1135
|
+
.5,
|
|
1136
|
+
0,
|
|
1137
|
+
1,
|
|
1138
|
+
"k-rate"
|
|
1139
|
+
],
|
|
1140
|
+
[
|
|
1141
|
+
"decayDiffusion1",
|
|
1142
|
+
.7,
|
|
1143
|
+
0,
|
|
1144
|
+
.999999,
|
|
1145
|
+
"k-rate"
|
|
1146
|
+
],
|
|
1147
|
+
[
|
|
1148
|
+
"decayDiffusion2",
|
|
1149
|
+
.5,
|
|
1150
|
+
0,
|
|
1151
|
+
.999999,
|
|
1152
|
+
"k-rate"
|
|
1153
|
+
],
|
|
1154
|
+
[
|
|
1155
|
+
"damping",
|
|
1156
|
+
.005,
|
|
1157
|
+
0,
|
|
1158
|
+
1,
|
|
1159
|
+
"k-rate"
|
|
1160
|
+
],
|
|
1161
|
+
[
|
|
1162
|
+
"excursionRate",
|
|
1163
|
+
.5,
|
|
1164
|
+
0,
|
|
1165
|
+
2,
|
|
1166
|
+
"k-rate"
|
|
1167
|
+
],
|
|
1168
|
+
[
|
|
1169
|
+
"excursionDepth",
|
|
1170
|
+
.7,
|
|
1171
|
+
0,
|
|
1172
|
+
2,
|
|
1173
|
+
"k-rate"
|
|
1174
|
+
],
|
|
1175
|
+
[
|
|
1176
|
+
"wet",
|
|
1177
|
+
.3,
|
|
1178
|
+
0,
|
|
1179
|
+
1,
|
|
1180
|
+
"k-rate"
|
|
1181
|
+
],
|
|
1182
|
+
[
|
|
1183
|
+
"dry",
|
|
1184
|
+
.6,
|
|
1185
|
+
0,
|
|
1186
|
+
1,
|
|
1187
|
+
"k-rate"
|
|
1188
|
+
]
|
|
1189
|
+
].map((x) => /* @__PURE__ */ new Object({
|
|
1190
|
+
name: x[0],
|
|
1191
|
+
defaultValue: x[1],
|
|
1192
|
+
minValue: x[2],
|
|
1193
|
+
maxValue: x[3],
|
|
1194
|
+
automationRate: x[4]
|
|
1195
|
+
}));
|
|
1196
|
+
}
|
|
1197
|
+
constructor(options) {
|
|
1198
|
+
super(options);
|
|
1199
|
+
this._Delays = [];
|
|
1200
|
+
this._pDLength = sampleRate + (128 - sampleRate % 128);
|
|
1201
|
+
this._preDelay = new Float32Array(this._pDLength);
|
|
1202
|
+
this._pDWrite = 0;
|
|
1203
|
+
this._lp1 = 0;
|
|
1204
|
+
this._lp2 = 0;
|
|
1205
|
+
this._lp3 = 0;
|
|
1206
|
+
this._excPhase = 0;
|
|
1207
|
+
const SHORT_DELAY_SCALE = .5;
|
|
1208
|
+
[
|
|
1209
|
+
.004771345,
|
|
1210
|
+
.003595309,
|
|
1211
|
+
.012734787,
|
|
1212
|
+
.009307483,
|
|
1213
|
+
.022579886,
|
|
1214
|
+
.149625349,
|
|
1215
|
+
.060481839,
|
|
1216
|
+
.1249958,
|
|
1217
|
+
.030509727,
|
|
1218
|
+
.141695508,
|
|
1219
|
+
.089244313,
|
|
1220
|
+
.106280031
|
|
1221
|
+
].map((x) => x * SHORT_DELAY_SCALE).forEach((x) => this.makeDelay(x));
|
|
1222
|
+
this._taps = Int16Array.from([
|
|
1223
|
+
.008937872,
|
|
1224
|
+
.099929438,
|
|
1225
|
+
.064278754,
|
|
1226
|
+
.067067639,
|
|
1227
|
+
.066866033,
|
|
1228
|
+
.006283391,
|
|
1229
|
+
.035818689,
|
|
1230
|
+
.011861161,
|
|
1231
|
+
.121870905,
|
|
1232
|
+
.041262054,
|
|
1233
|
+
.08981553,
|
|
1234
|
+
.070931756,
|
|
1235
|
+
.011256342,
|
|
1236
|
+
.004065724
|
|
1237
|
+
], (x) => Math.round(x * sampleRate));
|
|
1238
|
+
this.port.postMessage({ type: "initialized" });
|
|
1239
|
+
}
|
|
1240
|
+
makeDelay(length) {
|
|
1241
|
+
let len = Math.round(length * sampleRate);
|
|
1242
|
+
let nextPow2 = 2 ** Math.ceil(Math.log2(len));
|
|
1243
|
+
this._Delays.push([
|
|
1244
|
+
new Float32Array(nextPow2),
|
|
1245
|
+
len - 1,
|
|
1246
|
+
0,
|
|
1247
|
+
nextPow2 - 1
|
|
1248
|
+
]);
|
|
1249
|
+
}
|
|
1250
|
+
writeDelay(index, data) {
|
|
1251
|
+
return this._Delays[index][0][this._Delays[index][1]] = data;
|
|
1252
|
+
}
|
|
1253
|
+
readDelay(index) {
|
|
1254
|
+
return this._Delays[index][0][this._Delays[index][2]];
|
|
1255
|
+
}
|
|
1256
|
+
readDelayAt(index, i) {
|
|
1257
|
+
let d = this._Delays[index];
|
|
1258
|
+
return d[0][d[2] + i & d[3]];
|
|
1259
|
+
}
|
|
1260
|
+
readDelayCAt(index, i) {
|
|
1261
|
+
let d = this._Delays[index], frac = i - ~~i, int = ~~i + d[2] - 1, mask = d[3];
|
|
1262
|
+
let x0 = d[0][int++ & mask], x1 = d[0][int++ & mask], x2 = d[0][int++ & mask], x3 = d[0][int & mask];
|
|
1263
|
+
let a = (3 * (x1 - x2) - x0 + x3) / 2, b = 2 * x2 + x0 - (5 * x1 + x3) / 2, c = (x2 - x0) / 2;
|
|
1264
|
+
return ((a * frac + b) * frac + c) * frac + x1;
|
|
1265
|
+
}
|
|
1266
|
+
process(inputs, outputs, parameters) {
|
|
1267
|
+
const TWO_PI = 6.283185307179586;
|
|
1268
|
+
const TWO_PI_DETUNE = 6.284702653297906;
|
|
1269
|
+
const pd = ~~parameters.preDelay[0], bw = parameters.bandwidth[0], fi = parameters.inputDiffusion1[0], si = parameters.inputDiffusion2[0], dc = parameters.decay[0], ft = parameters.decayDiffusion1[0], st = parameters.decayDiffusion2[0], dp = 1 - parameters.damping[0], ex = parameters.excursionRate[0] / sampleRate, ed = parameters.excursionDepth[0] * sampleRate / 1e3, we = parameters.wet[0] * .6, dr = parameters.dry[0];
|
|
1270
|
+
if (inputs[0].length == 2) for (let i = 127; i >= 0; i--) {
|
|
1271
|
+
this._preDelay[this._pDWrite + i] = (inputs[0][0][i] + inputs[0][1][i]) * .5;
|
|
1272
|
+
outputs[0][0][i] = inputs[0][0][i] * dr;
|
|
1273
|
+
outputs[0][1][i] = inputs[0][1][i] * dr;
|
|
1274
|
+
}
|
|
1275
|
+
else if (inputs[0].length > 0) {
|
|
1276
|
+
this._preDelay.set(inputs[0][0], this._pDWrite);
|
|
1277
|
+
for (let i = 127; i >= 0; i--) outputs[0][0][i] = outputs[0][1][i] = inputs[0][0][i] * dr;
|
|
1278
|
+
} else this._preDelay.set(/* @__PURE__ */ new Float32Array(128), this._pDWrite);
|
|
1279
|
+
let i = 0;
|
|
1280
|
+
while (i < 128) {
|
|
1281
|
+
let lo = 0, ro = 0;
|
|
1282
|
+
this._lp1 += bw * (this._preDelay[(this._pDLength + this._pDWrite - pd + i) % this._pDLength] - this._lp1);
|
|
1283
|
+
let pre = this.writeDelay(0, this._lp1 - fi * this.readDelay(0));
|
|
1284
|
+
pre = this.writeDelay(1, fi * (pre - this.readDelay(1)) + this.readDelay(0));
|
|
1285
|
+
pre = this.writeDelay(2, fi * pre + this.readDelay(1) - si * this.readDelay(2));
|
|
1286
|
+
pre = this.writeDelay(3, si * (pre - this.readDelay(3)) + this.readDelay(2));
|
|
1287
|
+
let split = si * pre + this.readDelay(3);
|
|
1288
|
+
let exc = ed * (1 + Math.cos(this._excPhase * TWO_PI));
|
|
1289
|
+
let exc2 = ed * (1 + Math.sin(this._excPhase * TWO_PI_DETUNE));
|
|
1290
|
+
let temp = this.writeDelay(4, split + dc * this.readDelay(11) + ft * this.readDelayCAt(4, exc));
|
|
1291
|
+
this.writeDelay(5, this.readDelayCAt(4, exc) - ft * temp);
|
|
1292
|
+
this._lp2 += dp * (this.readDelay(5) - this._lp2);
|
|
1293
|
+
temp = this.writeDelay(6, dc * this._lp2 - st * this.readDelay(6));
|
|
1294
|
+
this.writeDelay(7, this.readDelay(6) + st * temp);
|
|
1295
|
+
temp = this.writeDelay(8, split + dc * this.readDelay(7) + ft * this.readDelayCAt(8, exc2));
|
|
1296
|
+
this.writeDelay(9, this.readDelayCAt(8, exc2) - ft * temp);
|
|
1297
|
+
this._lp3 += dp * (this.readDelay(9) - this._lp3);
|
|
1298
|
+
temp = this.writeDelay(10, dc * this._lp3 - st * this.readDelay(10));
|
|
1299
|
+
this.writeDelay(11, this.readDelay(10) + st * temp);
|
|
1300
|
+
lo = this.readDelayAt(9, this._taps[0]) + this.readDelayAt(9, this._taps[1]) - this.readDelayAt(10, this._taps[2]) + this.readDelayAt(11, this._taps[3]) - this.readDelayAt(5, this._taps[4]) - this.readDelayAt(6, this._taps[5]) - this.readDelayAt(7, this._taps[6]);
|
|
1301
|
+
ro = this.readDelayAt(5, this._taps[7]) + this.readDelayAt(5, this._taps[8]) - this.readDelayAt(6, this._taps[9]) + this.readDelayAt(7, this._taps[10]) - this.readDelayAt(9, this._taps[11]) - this.readDelayAt(10, this._taps[12]) - this.readDelayAt(11, this._taps[13]);
|
|
1302
|
+
outputs[0][0][i] += lo * we;
|
|
1303
|
+
outputs[0][1][i] += ro * we;
|
|
1304
|
+
this._excPhase += ex;
|
|
1305
|
+
if (this._excPhase >= 1) this._excPhase -= 1;
|
|
1306
|
+
i++;
|
|
1307
|
+
const delays = this._Delays;
|
|
1308
|
+
for (let j = 0; j < delays.length; j++) {
|
|
1309
|
+
const d = delays[j];
|
|
1310
|
+
d[1] = d[1] + 1 & d[3];
|
|
1311
|
+
d[2] = d[2] + 1 & d[3];
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
this._pDWrite = (this._pDWrite + 128) % this._pDLength;
|
|
1315
|
+
return true;
|
|
1316
|
+
}
|
|
1087
1317
|
};
|
|
1088
|
-
registerProcessor(
|
|
1089
|
-
"delay-processor",
|
|
1090
|
-
class extends AudioWorkletProcessor {
|
|
1091
|
-
static get parameterDescriptors() {
|
|
1092
|
-
return [
|
|
1093
|
-
{
|
|
1094
|
-
name: "delayTime",
|
|
1095
|
-
defaultValue: 0.5,
|
|
1096
|
-
minValue: 1e-3,
|
|
1097
|
-
maxValue: 2,
|
|
1098
|
-
automationRate: "k-rate"
|
|
1099
|
-
},
|
|
1100
|
-
{
|
|
1101
|
-
name: "feedbackAmount",
|
|
1102
|
-
defaultValue: 0,
|
|
1103
|
-
minValue: 0,
|
|
1104
|
-
maxValue: 0.99,
|
|
1105
|
-
automationRate: "k-rate"
|
|
1106
|
-
}
|
|
1107
|
-
];
|
|
1108
|
-
}
|
|
1109
|
-
constructor() {
|
|
1110
|
-
super();
|
|
1111
|
-
this.buffers = [];
|
|
1112
|
-
this.smoothedDelaySamples = [];
|
|
1113
|
-
this.smoothingFactor = DEFAULT_DELAY_CONFIG.SMOOTHING_FACTOR.slowest;
|
|
1114
|
-
this.characterModes = [...DEFAULT_DELAY_CONFIG.CHARACTER];
|
|
1115
|
-
this._bpState = [];
|
|
1116
|
-
this._bpFreq = DEFAULT_CHARACTER_CONFIG.filtered.freq;
|
|
1117
|
-
this._bpQ = DEFAULT_CHARACTER_CONFIG.filtered.Q;
|
|
1118
|
-
this._bpCoeffs = null;
|
|
1119
|
-
this._lastBpFreq = -1;
|
|
1120
|
-
this._lastBpQ = -1;
|
|
1121
|
-
this.lofiBits = DEFAULT_CHARACTER_CONFIG["bitCrushed"].bits;
|
|
1122
|
-
this.lofiDownsample = DEFAULT_CHARACTER_CONFIG["bitCrushed"].downsample;
|
|
1123
|
-
this._lofiSampleHold = [];
|
|
1124
|
-
this._lofiSampleCount = [];
|
|
1125
|
-
this.initialized = false;
|
|
1126
|
-
this.port.onmessage = (event) => {
|
|
1127
|
-
if (event.data && event.data.type === "setCharacter" && Array.isArray(event.data.modes)) {
|
|
1128
|
-
this.characterModes = [...event.data.modes];
|
|
1129
|
-
}
|
|
1130
|
-
if (event.data && event.data.type === "setBandpassFreq" && typeof event.data.hz === "number") {
|
|
1131
|
-
this.setBandpassFreq(event.data.hz);
|
|
1132
|
-
}
|
|
1133
|
-
if (event.data && event.data.type === "trigger") ;
|
|
1134
|
-
};
|
|
1135
|
-
this.port.postMessage({ type: "initialized" });
|
|
1136
|
-
}
|
|
1137
|
-
setBandpassFreq(hz) {
|
|
1138
|
-
this._bpFreq = hz;
|
|
1139
|
-
this._lastBpFreq = -1;
|
|
1140
|
-
}
|
|
1141
|
-
_updateBandpassCoeffs() {
|
|
1142
|
-
if (this._lastBpFreq === this._bpFreq && this._lastBpQ === this._bpQ) {
|
|
1143
|
-
return;
|
|
1144
|
-
}
|
|
1145
|
-
const bpFreq = this._bpFreq;
|
|
1146
|
-
const bpQ = this._bpQ;
|
|
1147
|
-
const omega = 2 * Math.PI * bpFreq / sampleRate;
|
|
1148
|
-
const alpha = Math.sin(omega) / (2 * bpQ);
|
|
1149
|
-
const cosw = Math.cos(omega);
|
|
1150
|
-
const b0 = alpha;
|
|
1151
|
-
const b1 = 0;
|
|
1152
|
-
const b2 = -alpha;
|
|
1153
|
-
const a0 = 1 + alpha;
|
|
1154
|
-
const a1 = -2 * cosw;
|
|
1155
|
-
const a2 = 1 - alpha;
|
|
1156
|
-
this._bpCoeffs = {
|
|
1157
|
-
b0: b0 / a0,
|
|
1158
|
-
b1: b1 / a0,
|
|
1159
|
-
b2: b2 / a0,
|
|
1160
|
-
a1: a1 / a0,
|
|
1161
|
-
a2: a2 / a0
|
|
1162
|
-
};
|
|
1163
|
-
this._lastBpFreq = bpFreq;
|
|
1164
|
-
this._lastBpQ = bpQ;
|
|
1165
|
-
}
|
|
1166
|
-
initializeBuffers(channelCount) {
|
|
1167
|
-
const maxSamples = Math.floor(sampleRate * 2);
|
|
1168
|
-
this.buffers = [];
|
|
1169
|
-
this.smoothedDelaySamples = [];
|
|
1170
|
-
this._lofiSampleHold = [];
|
|
1171
|
-
this._lofiSampleCount = [];
|
|
1172
|
-
for (let c = 0; c < channelCount; c++) {
|
|
1173
|
-
this.buffers[c] = new DelayBuffer(maxSamples);
|
|
1174
|
-
this.smoothedDelaySamples[c] = Math.floor(sampleRate * 0.5);
|
|
1175
|
-
this._lofiSampleHold[c] = 0;
|
|
1176
|
-
this._lofiSampleCount[c] = 0;
|
|
1177
|
-
}
|
|
1178
|
-
this.initialized = true;
|
|
1179
|
-
}
|
|
1180
|
-
_processLoFi(delayed, c) {
|
|
1181
|
-
if (this._lofiSampleCount[c] % this.lofiDownsample === 0) {
|
|
1182
|
-
const levels = Math.pow(2, this.lofiBits);
|
|
1183
|
-
delayed = Math.round(delayed * levels) / levels;
|
|
1184
|
-
this._lofiSampleHold[c] = delayed;
|
|
1185
|
-
} else {
|
|
1186
|
-
delayed = this._lofiSampleHold[c];
|
|
1187
|
-
}
|
|
1188
|
-
this._lofiSampleCount[c]++;
|
|
1189
|
-
return delayed;
|
|
1190
|
-
}
|
|
1191
|
-
_processBandpass(delayed, c) {
|
|
1192
|
-
if (!this._bpState) this._bpState = [];
|
|
1193
|
-
if (!this._bpState[c]) {
|
|
1194
|
-
this._bpState[c] = { x1: 0, x2: 0, y1: 0, y2: 0 };
|
|
1195
|
-
}
|
|
1196
|
-
this._updateBandpassCoeffs();
|
|
1197
|
-
if (!this._bpCoeffs) {
|
|
1198
|
-
return delayed;
|
|
1199
|
-
}
|
|
1200
|
-
const { b0, b1, b2, a1, a2 } = this._bpCoeffs;
|
|
1201
|
-
const s = this._bpState[c];
|
|
1202
|
-
const y = b0 * delayed + b1 * s.x1 + b2 * s.x2 - a1 * s.y1 - a2 * s.y2;
|
|
1203
|
-
s.x2 = s.x1;
|
|
1204
|
-
s.x1 = delayed;
|
|
1205
|
-
s.y2 = s.y1;
|
|
1206
|
-
s.y1 = y;
|
|
1207
|
-
return y;
|
|
1208
|
-
}
|
|
1209
|
-
process(inputs, outputs, parameters) {
|
|
1210
|
-
const input = inputs[0];
|
|
1211
|
-
const output = outputs[0];
|
|
1212
|
-
if (!input || !output || input.length === 0 || output.length === 0) {
|
|
1213
|
-
return true;
|
|
1214
|
-
}
|
|
1215
|
-
if (!input[0] || !output[0] || input[0].length === 0 || output[0].length === 0) {
|
|
1216
|
-
return true;
|
|
1217
|
-
}
|
|
1218
|
-
if (!this.initialized || this.buffers.length !== input.length) {
|
|
1219
|
-
this.initializeBuffers(input.length);
|
|
1220
|
-
}
|
|
1221
|
-
const delayTime = parameters.delayTime[0];
|
|
1222
|
-
const feedbackAmount = parameters.feedbackAmount[0];
|
|
1223
|
-
const targetDelaySamples = sampleRate * delayTime;
|
|
1224
|
-
const channelCount = Math.min(input.length, output.length);
|
|
1225
|
-
const frameCount = output[0].length;
|
|
1226
|
-
const smoothing = this.smoothingFactor;
|
|
1227
|
-
for (let i = 0; i < frameCount; ++i) {
|
|
1228
|
-
for (let c = 0; c < channelCount; c++) {
|
|
1229
|
-
const buf = this.buffers[c];
|
|
1230
|
-
if (!buf) {
|
|
1231
|
-
continue;
|
|
1232
|
-
}
|
|
1233
|
-
this.smoothedDelaySamples[c] += (targetDelaySamples - this.smoothedDelaySamples[c]) * smoothing;
|
|
1234
|
-
const smoothedDelay = this.smoothedDelaySamples[c];
|
|
1235
|
-
const intDelay = Math.floor(smoothedDelay);
|
|
1236
|
-
const frac = smoothedDelay - intDelay;
|
|
1237
|
-
const readPtrA = (buf.writePtr - intDelay + buf.buffer.length) % buf.buffer.length;
|
|
1238
|
-
const readPtrB = (readPtrA - 1 + buf.buffer.length) % buf.buffer.length;
|
|
1239
|
-
const sampleA = buf.buffer[readPtrA];
|
|
1240
|
-
const sampleB = buf.buffer[readPtrB];
|
|
1241
|
-
let delayed = sampleA * (1 - frac) + sampleB * frac;
|
|
1242
|
-
for (const mode of this.characterModes) {
|
|
1243
|
-
if (mode === "bitCrushed") {
|
|
1244
|
-
delayed = this._processLoFi(delayed, c);
|
|
1245
|
-
} else if (mode === "filtered") {
|
|
1246
|
-
delayed = this._processBandpass(delayed, c);
|
|
1247
|
-
}
|
|
1248
|
-
}
|
|
1249
|
-
output[c][i] = compressSingleSample(delayed, 0.75, 4, {
|
|
1250
|
-
enabled: true,
|
|
1251
|
-
type: "soft",
|
|
1252
|
-
outputRange: { min: -0.9, max: 0.9 }
|
|
1253
|
-
});
|
|
1254
|
-
const inputSample = input[c] && input[c][i] !== void 0 ? input[c][i] : 0;
|
|
1255
|
-
buf.write(inputSample + delayed * feedbackAmount);
|
|
1256
|
-
buf.updatePointers(intDelay);
|
|
1257
|
-
}
|
|
1258
|
-
}
|
|
1259
|
-
return true;
|
|
1260
|
-
}
|
|
1261
|
-
}
|
|
1262
|
-
);
|
|
1263
|
-
class DattorroReverb extends AudioWorkletProcessor {
|
|
1264
|
-
static get parameterDescriptors() {
|
|
1265
|
-
return [
|
|
1266
|
-
["preDelay", 0, 0, sampleRate - 1, "k-rate"],
|
|
1267
|
-
["bandwidth", 0.9999, 0, 1, "k-rate"],
|
|
1268
|
-
["inputDiffusion1", 0.75, 0, 1, "k-rate"],
|
|
1269
|
-
["inputDiffusion2", 0.625, 0, 1, "k-rate"],
|
|
1270
|
-
["decay", 0.5, 0, 1, "k-rate"],
|
|
1271
|
-
["decayDiffusion1", 0.7, 0, 0.999999, "k-rate"],
|
|
1272
|
-
["decayDiffusion2", 0.5, 0, 0.999999, "k-rate"],
|
|
1273
|
-
["damping", 5e-3, 0, 1, "k-rate"],
|
|
1274
|
-
["excursionRate", 0.5, 0, 2, "k-rate"],
|
|
1275
|
-
["excursionDepth", 0.7, 0, 2, "k-rate"],
|
|
1276
|
-
["wet", 0.3, 0, 1, "k-rate"],
|
|
1277
|
-
["dry", 0.6, 0, 1, "k-rate"]
|
|
1278
|
-
].map(
|
|
1279
|
-
(x) => new Object({
|
|
1280
|
-
name: x[0],
|
|
1281
|
-
defaultValue: x[1],
|
|
1282
|
-
minValue: x[2],
|
|
1283
|
-
maxValue: x[3],
|
|
1284
|
-
automationRate: x[4]
|
|
1285
|
-
})
|
|
1286
|
-
);
|
|
1287
|
-
}
|
|
1288
|
-
constructor(options) {
|
|
1289
|
-
super(options);
|
|
1290
|
-
this._Delays = [];
|
|
1291
|
-
this._pDLength = sampleRate + (128 - sampleRate % 128);
|
|
1292
|
-
this._preDelay = new Float32Array(this._pDLength);
|
|
1293
|
-
this._pDWrite = 0;
|
|
1294
|
-
this._lp1 = 0;
|
|
1295
|
-
this._lp2 = 0;
|
|
1296
|
-
this._lp3 = 0;
|
|
1297
|
-
this._excPhase = 0;
|
|
1298
|
-
const SHORT_DELAY_SCALE = 0.5;
|
|
1299
|
-
[
|
|
1300
|
-
4771345e-9,
|
|
1301
|
-
3595309e-9,
|
|
1302
|
-
0.012734787,
|
|
1303
|
-
9307483e-9,
|
|
1304
|
-
0.022579886,
|
|
1305
|
-
0.149625349,
|
|
1306
|
-
0.060481839,
|
|
1307
|
-
0.1249958,
|
|
1308
|
-
0.030509727,
|
|
1309
|
-
0.141695508,
|
|
1310
|
-
0.089244313,
|
|
1311
|
-
0.106280031
|
|
1312
|
-
].map((x) => x * SHORT_DELAY_SCALE).forEach((x) => this.makeDelay(x));
|
|
1313
|
-
this._taps = Int16Array.from(
|
|
1314
|
-
[
|
|
1315
|
-
8937872e-9,
|
|
1316
|
-
0.099929438,
|
|
1317
|
-
0.064278754,
|
|
1318
|
-
0.067067639,
|
|
1319
|
-
0.066866033,
|
|
1320
|
-
6283391e-9,
|
|
1321
|
-
0.035818689,
|
|
1322
|
-
0.011861161,
|
|
1323
|
-
0.121870905,
|
|
1324
|
-
0.041262054,
|
|
1325
|
-
0.08981553,
|
|
1326
|
-
0.070931756,
|
|
1327
|
-
0.011256342,
|
|
1328
|
-
4065724e-9
|
|
1329
|
-
],
|
|
1330
|
-
(x) => Math.round(x * sampleRate)
|
|
1331
|
-
);
|
|
1332
|
-
this.port.postMessage({ type: "initialized" });
|
|
1333
|
-
}
|
|
1334
|
-
makeDelay(length) {
|
|
1335
|
-
let len = Math.round(length * sampleRate);
|
|
1336
|
-
let nextPow2 = 2 ** Math.ceil(Math.log2(len));
|
|
1337
|
-
this._Delays.push([
|
|
1338
|
-
new Float32Array(nextPow2),
|
|
1339
|
-
len - 1,
|
|
1340
|
-
// ? or should be 0 ?
|
|
1341
|
-
0 | 0,
|
|
1342
|
-
// ? or should be len - 1 ?
|
|
1343
|
-
nextPow2 - 1
|
|
1344
|
-
]);
|
|
1345
|
-
}
|
|
1346
|
-
writeDelay(index, data) {
|
|
1347
|
-
return this._Delays[index][0][this._Delays[index][1]] = data;
|
|
1348
|
-
}
|
|
1349
|
-
readDelay(index) {
|
|
1350
|
-
return this._Delays[index][0][this._Delays[index][2]];
|
|
1351
|
-
}
|
|
1352
|
-
readDelayAt(index, i) {
|
|
1353
|
-
let d = this._Delays[index];
|
|
1354
|
-
return d[0][d[2] + i & d[3]];
|
|
1355
|
-
}
|
|
1356
|
-
// cubic interpolation
|
|
1357
|
-
// O. Niemitalo: https://www.musicdsp.org/en/latest/Other/49-cubic-interpollation.html
|
|
1358
|
-
readDelayCAt(index, i) {
|
|
1359
|
-
let d = this._Delays[index], frac = i - ~~i, int = ~~i + d[2] - 1, mask = d[3];
|
|
1360
|
-
let x0 = d[0][int++ & mask], x1 = d[0][int++ & mask], x2 = d[0][int++ & mask], x3 = d[0][int & mask];
|
|
1361
|
-
let a = (3 * (x1 - x2) - x0 + x3) / 2, b = 2 * x2 + x0 - (5 * x1 + x3) / 2, c = (x2 - x0) / 2;
|
|
1362
|
-
return ((a * frac + b) * frac + c) * frac + x1;
|
|
1363
|
-
}
|
|
1364
|
-
// First input will be downmixed to mono if number of channels is not 2
|
|
1365
|
-
// Outputs Stereo.
|
|
1366
|
-
process(inputs, outputs, parameters) {
|
|
1367
|
-
const TWO_PI = 6.283185307179586;
|
|
1368
|
-
const TWO_PI_DETUNE = 6.284702653297906;
|
|
1369
|
-
const pd = ~~parameters.preDelay[0], bw = parameters.bandwidth[0], fi = parameters.inputDiffusion1[0], si = parameters.inputDiffusion2[0], dc = parameters.decay[0], ft = parameters.decayDiffusion1[0], st = parameters.decayDiffusion2[0], dp = 1 - parameters.damping[0], ex = parameters.excursionRate[0] / sampleRate, ed = parameters.excursionDepth[0] * sampleRate / 1e3, we = parameters.wet[0] * 0.6, dr = parameters.dry[0];
|
|
1370
|
-
if (inputs[0].length == 2) {
|
|
1371
|
-
for (let i2 = 127; i2 >= 0; i2--) {
|
|
1372
|
-
this._preDelay[this._pDWrite + i2] = (inputs[0][0][i2] + inputs[0][1][i2]) * 0.5;
|
|
1373
|
-
outputs[0][0][i2] = inputs[0][0][i2] * dr;
|
|
1374
|
-
outputs[0][1][i2] = inputs[0][1][i2] * dr;
|
|
1375
|
-
}
|
|
1376
|
-
} else if (inputs[0].length > 0) {
|
|
1377
|
-
this._preDelay.set(inputs[0][0], this._pDWrite);
|
|
1378
|
-
for (let i2 = 127; i2 >= 0; i2--)
|
|
1379
|
-
outputs[0][0][i2] = outputs[0][1][i2] = inputs[0][0][i2] * dr;
|
|
1380
|
-
} else {
|
|
1381
|
-
this._preDelay.set(new Float32Array(128), this._pDWrite);
|
|
1382
|
-
}
|
|
1383
|
-
let i = 0 | 0;
|
|
1384
|
-
while (i < 128) {
|
|
1385
|
-
let lo = 0, ro = 0;
|
|
1386
|
-
this._lp1 += bw * (this._preDelay[(this._pDLength + this._pDWrite - pd + i) % this._pDLength] - this._lp1);
|
|
1387
|
-
let pre = this.writeDelay(0, this._lp1 - fi * this.readDelay(0));
|
|
1388
|
-
pre = this.writeDelay(
|
|
1389
|
-
1,
|
|
1390
|
-
fi * (pre - this.readDelay(1)) + this.readDelay(0)
|
|
1391
|
-
);
|
|
1392
|
-
pre = this.writeDelay(
|
|
1393
|
-
2,
|
|
1394
|
-
fi * pre + this.readDelay(1) - si * this.readDelay(2)
|
|
1395
|
-
);
|
|
1396
|
-
pre = this.writeDelay(
|
|
1397
|
-
3,
|
|
1398
|
-
si * (pre - this.readDelay(3)) + this.readDelay(2)
|
|
1399
|
-
);
|
|
1400
|
-
let split = si * pre + this.readDelay(3);
|
|
1401
|
-
let exc = ed * (1 + Math.cos(this._excPhase * TWO_PI));
|
|
1402
|
-
let exc2 = ed * (1 + Math.sin(this._excPhase * TWO_PI_DETUNE));
|
|
1403
|
-
let temp = this.writeDelay(
|
|
1404
|
-
4,
|
|
1405
|
-
split + dc * this.readDelay(11) + ft * this.readDelayCAt(4, exc)
|
|
1406
|
-
);
|
|
1407
|
-
this.writeDelay(5, this.readDelayCAt(4, exc) - ft * temp);
|
|
1408
|
-
this._lp2 += dp * (this.readDelay(5) - this._lp2);
|
|
1409
|
-
temp = this.writeDelay(6, dc * this._lp2 - st * this.readDelay(6));
|
|
1410
|
-
this.writeDelay(7, this.readDelay(6) + st * temp);
|
|
1411
|
-
temp = this.writeDelay(
|
|
1412
|
-
8,
|
|
1413
|
-
split + dc * this.readDelay(7) + ft * this.readDelayCAt(8, exc2)
|
|
1414
|
-
);
|
|
1415
|
-
this.writeDelay(9, this.readDelayCAt(8, exc2) - ft * temp);
|
|
1416
|
-
this._lp3 += dp * (this.readDelay(9) - this._lp3);
|
|
1417
|
-
temp = this.writeDelay(10, dc * this._lp3 - st * this.readDelay(10));
|
|
1418
|
-
this.writeDelay(11, this.readDelay(10) + st * temp);
|
|
1419
|
-
lo = this.readDelayAt(9, this._taps[0]) + this.readDelayAt(9, this._taps[1]) - this.readDelayAt(10, this._taps[2]) + this.readDelayAt(11, this._taps[3]) - this.readDelayAt(5, this._taps[4]) - this.readDelayAt(6, this._taps[5]) - this.readDelayAt(7, this._taps[6]);
|
|
1420
|
-
ro = this.readDelayAt(5, this._taps[7]) + this.readDelayAt(5, this._taps[8]) - this.readDelayAt(6, this._taps[9]) + this.readDelayAt(7, this._taps[10]) - this.readDelayAt(9, this._taps[11]) - this.readDelayAt(10, this._taps[12]) - this.readDelayAt(11, this._taps[13]);
|
|
1421
|
-
outputs[0][0][i] += lo * we;
|
|
1422
|
-
outputs[0][1][i] += ro * we;
|
|
1423
|
-
this._excPhase += ex;
|
|
1424
|
-
if (this._excPhase >= 1) this._excPhase -= 1;
|
|
1425
|
-
i++;
|
|
1426
|
-
const delays = this._Delays;
|
|
1427
|
-
for (let j = 0; j < delays.length; j++) {
|
|
1428
|
-
const d = delays[j];
|
|
1429
|
-
d[1] = d[1] + 1 & d[3];
|
|
1430
|
-
d[2] = d[2] + 1 & d[3];
|
|
1431
|
-
}
|
|
1432
|
-
}
|
|
1433
|
-
this._pDWrite = (this._pDWrite + 128) % this._pDLength;
|
|
1434
|
-
return true;
|
|
1435
|
-
}
|
|
1436
|
-
}
|
|
1437
1318
|
registerProcessor("dattorro-reverb-processor", DattorroReverb);
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
const inChannel = input[0];
|
|
1600
|
-
if (!inChannel || inChannel.length === 0) return true;
|
|
1601
|
-
const attack = parameters.attack[0];
|
|
1602
|
-
const release = parameters.release[0];
|
|
1603
|
-
const inputGain = parameters.inputGain[0];
|
|
1604
|
-
const outputGain = parameters.outputGain[0];
|
|
1605
|
-
const attackCoeff = Math.exp(-1 / (attack * sampleRate));
|
|
1606
|
-
const releaseCoeff = Math.exp(-1 / (release * sampleRate));
|
|
1607
|
-
for (let sample = 0; sample < output[0].length; sample++) {
|
|
1608
|
-
const inputLevel = Math.abs((input[0][sample] || 0) * inputGain);
|
|
1609
|
-
if (inputLevel > 1e-6) {
|
|
1610
|
-
if (inputLevel > this.envelope) {
|
|
1611
|
-
this.envelope = inputLevel + (this.envelope - inputLevel) * attackCoeff;
|
|
1612
|
-
} else {
|
|
1613
|
-
this.envelope = inputLevel + (this.envelope - inputLevel) * releaseCoeff;
|
|
1614
|
-
}
|
|
1615
|
-
} else {
|
|
1616
|
-
this.envelope *= releaseCoeff;
|
|
1617
|
-
}
|
|
1618
|
-
if (this.envelope < this.gateThreshold) this.envelope = 0;
|
|
1619
|
-
const finalOutput = this.envelope * outputGain;
|
|
1620
|
-
for (let channel2 = 0; channel2 < output.length; channel2++) {
|
|
1621
|
-
output[channel2][sample] = finalOutput;
|
|
1622
|
-
}
|
|
1623
|
-
}
|
|
1624
|
-
return true;
|
|
1625
|
-
}
|
|
1626
|
-
}
|
|
1627
|
-
);
|
|
1319
|
+
//#endregion
|
|
1320
|
+
//#region src/worklets/processors/distortion/distortion-processor.js
|
|
1321
|
+
var Distortion = class {
|
|
1322
|
+
constructor() {
|
|
1323
|
+
this.limitingMode = "hard-clipping";
|
|
1324
|
+
}
|
|
1325
|
+
applyDrive(sample, driveAmount) {
|
|
1326
|
+
if (driveAmount <= 0) return sample;
|
|
1327
|
+
return sample * (1 + driveAmount * 3);
|
|
1328
|
+
}
|
|
1329
|
+
applyClipping(sample, clippingAmount, clipThreshold) {
|
|
1330
|
+
if (clippingAmount <= 0) return sample;
|
|
1331
|
+
let clippedSample;
|
|
1332
|
+
switch (this.limitingMode) {
|
|
1333
|
+
case "soft-clipping":
|
|
1334
|
+
clippedSample = clipThreshold * Math.tanh(sample / clipThreshold);
|
|
1335
|
+
break;
|
|
1336
|
+
case "hard-clipping":
|
|
1337
|
+
clippedSample = Math.max(-clipThreshold, Math.min(clipThreshold, sample));
|
|
1338
|
+
break;
|
|
1339
|
+
default: clippedSample = sample;
|
|
1340
|
+
}
|
|
1341
|
+
if (clipThreshold < .08) {
|
|
1342
|
+
const makeupGain = Math.min(2, Math.pow(.1 / clipThreshold, .5));
|
|
1343
|
+
clippedSample *= makeupGain;
|
|
1344
|
+
}
|
|
1345
|
+
return sample * (1 - clippingAmount) + clippedSample * clippingAmount;
|
|
1346
|
+
}
|
|
1347
|
+
setLimitingMode(mode) {
|
|
1348
|
+
this.limitingMode = mode;
|
|
1349
|
+
}
|
|
1350
|
+
};
|
|
1351
|
+
registerProcessor("distortion-processor", class extends AudioWorkletProcessor {
|
|
1352
|
+
static get parameterDescriptors() {
|
|
1353
|
+
return [
|
|
1354
|
+
{
|
|
1355
|
+
name: "distortionDrive",
|
|
1356
|
+
defaultValue: 0,
|
|
1357
|
+
minValue: 0,
|
|
1358
|
+
maxValue: 1,
|
|
1359
|
+
automationRate: "a-rate"
|
|
1360
|
+
},
|
|
1361
|
+
{
|
|
1362
|
+
name: "clippingAmount",
|
|
1363
|
+
defaultValue: 0,
|
|
1364
|
+
minValue: 0,
|
|
1365
|
+
maxValue: 1,
|
|
1366
|
+
automationRate: "a-rate"
|
|
1367
|
+
},
|
|
1368
|
+
{
|
|
1369
|
+
name: "clippingThreshold",
|
|
1370
|
+
defaultValue: .5,
|
|
1371
|
+
minValue: 0,
|
|
1372
|
+
maxValue: 1,
|
|
1373
|
+
automationRate: "k-rate"
|
|
1374
|
+
}
|
|
1375
|
+
];
|
|
1376
|
+
}
|
|
1377
|
+
constructor() {
|
|
1378
|
+
super();
|
|
1379
|
+
this.distortion = new Distortion();
|
|
1380
|
+
this.setupMessageHandling();
|
|
1381
|
+
this.port.postMessage({ type: "initialized" });
|
|
1382
|
+
}
|
|
1383
|
+
setupMessageHandling() {
|
|
1384
|
+
this.port.onmessage = (event) => {
|
|
1385
|
+
switch (event.data.type) {
|
|
1386
|
+
case "setLimitingMode":
|
|
1387
|
+
this.distortion.setLimitingMode(event.data.mode);
|
|
1388
|
+
break;
|
|
1389
|
+
default: console.warn("distortion-processor: Unsupported message");
|
|
1390
|
+
}
|
|
1391
|
+
};
|
|
1392
|
+
}
|
|
1393
|
+
process(inputs, outputs, parameters) {
|
|
1394
|
+
const input = inputs[0];
|
|
1395
|
+
const output = outputs[0];
|
|
1396
|
+
if (!input || !output) return true;
|
|
1397
|
+
const clipThreshold = parameters.clippingThreshold[0];
|
|
1398
|
+
for (let i = 0; i < output[0].length; ++i) {
|
|
1399
|
+
const distortionDrive = parameters.distortionDrive[Math.min(i, parameters.distortionDrive.length - 1)];
|
|
1400
|
+
const clippingAmount = parameters.clippingAmount[Math.min(i, parameters.clippingAmount.length - 1)];
|
|
1401
|
+
for (let c = 0; c < Math.min(input.length, output.length); c++) {
|
|
1402
|
+
let sample = input[c][i];
|
|
1403
|
+
sample = this.distortion.applyDrive(sample, distortionDrive);
|
|
1404
|
+
sample = this.distortion.applyClipping(sample, clippingAmount, clipThreshold);
|
|
1405
|
+
output[c][i] = Math.max(-.999, Math.min(.999, sample));
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
return true;
|
|
1409
|
+
}
|
|
1410
|
+
});
|
|
1411
|
+
//#endregion
|
|
1412
|
+
//#region src/worklets/processors/follower/envelope-follower-processor.js
|
|
1413
|
+
registerProcessor("envelope-follower-processor", class extends AudioWorkletProcessor {
|
|
1414
|
+
static get parameterDescriptors() {
|
|
1415
|
+
return [
|
|
1416
|
+
{
|
|
1417
|
+
name: "inputGain",
|
|
1418
|
+
defaultValue: 1,
|
|
1419
|
+
minValue: 0,
|
|
1420
|
+
maxValue: 10,
|
|
1421
|
+
automationRate: "k-rate"
|
|
1422
|
+
},
|
|
1423
|
+
{
|
|
1424
|
+
name: "outputGain",
|
|
1425
|
+
defaultValue: 1,
|
|
1426
|
+
minValue: 0,
|
|
1427
|
+
maxValue: 10,
|
|
1428
|
+
automationRate: "k-rate"
|
|
1429
|
+
},
|
|
1430
|
+
{
|
|
1431
|
+
name: "attack",
|
|
1432
|
+
defaultValue: .003,
|
|
1433
|
+
minValue: .001,
|
|
1434
|
+
maxValue: 1,
|
|
1435
|
+
automationRate: "k-rate"
|
|
1436
|
+
},
|
|
1437
|
+
{
|
|
1438
|
+
name: "release",
|
|
1439
|
+
defaultValue: .05,
|
|
1440
|
+
minValue: .001,
|
|
1441
|
+
maxValue: 5,
|
|
1442
|
+
automationRate: "k-rate"
|
|
1443
|
+
}
|
|
1444
|
+
];
|
|
1445
|
+
}
|
|
1446
|
+
constructor() {
|
|
1447
|
+
super();
|
|
1448
|
+
this.envelope = 0;
|
|
1449
|
+
this.gateThreshold = .005;
|
|
1450
|
+
this.debugCounter = 0;
|
|
1451
|
+
this.port.postMessage({ type: "initialized" });
|
|
1452
|
+
}
|
|
1453
|
+
process(inputs, outputs, parameters) {
|
|
1454
|
+
const input = inputs[0];
|
|
1455
|
+
const output = outputs[0];
|
|
1456
|
+
const channel = inputs[0][0];
|
|
1457
|
+
if (!input || !output || !channel || input.length === 0 || output.length === 0 || channel.length === 0) return true;
|
|
1458
|
+
const inChannel = input[0];
|
|
1459
|
+
if (!inChannel || inChannel.length === 0) return true;
|
|
1460
|
+
const attack = parameters.attack[0];
|
|
1461
|
+
const release = parameters.release[0];
|
|
1462
|
+
const inputGain = parameters.inputGain[0];
|
|
1463
|
+
const outputGain = parameters.outputGain[0];
|
|
1464
|
+
const attackCoeff = Math.exp(-1 / (attack * sampleRate));
|
|
1465
|
+
const releaseCoeff = Math.exp(-1 / (release * sampleRate));
|
|
1466
|
+
for (let sample = 0; sample < output[0].length; sample++) {
|
|
1467
|
+
const inputLevel = Math.abs((input[0][sample] || 0) * inputGain);
|
|
1468
|
+
if (inputLevel > 1e-6) {
|
|
1469
|
+
if (inputLevel > this.envelope) this.envelope = inputLevel + (this.envelope - inputLevel) * attackCoeff;
|
|
1470
|
+
else this.envelope = inputLevel + (this.envelope - inputLevel) * releaseCoeff;
|
|
1471
|
+
} else this.envelope *= releaseCoeff;
|
|
1472
|
+
if (this.envelope < this.gateThreshold) this.envelope = 0;
|
|
1473
|
+
const finalOutput = this.envelope * outputGain;
|
|
1474
|
+
for (let channel = 0; channel < output.length; channel++) output[channel][sample] = finalOutput;
|
|
1475
|
+
}
|
|
1476
|
+
return true;
|
|
1477
|
+
}
|
|
1478
|
+
});
|
|
1479
|
+
//#endregion
|