@kidlib/web-audio 0.1.0 → 0.1.2

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.
@@ -1,1627 +1,1479 @@
1
- var __typeError = (msg) => {
2
- throw TypeError(msg);
3
- };
4
- var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot " + msg);
5
- var __privateGet = (obj, member, getter) => (__accessCheck(obj, member, "read from private field"), getter ? getter.call(obj) : member.get(obj));
6
- var __privateAdd = (obj, member, value) => member.has(obj) ? __typeError("Cannot add the same private member more than once") : member instanceof WeakSet ? member.add(obj) : member.set(obj, value);
7
- var __privateMethod = (obj, member, method) => (__accessCheck(obj, member, "access private method"), method);
8
- var _SamplePlayerProcessor_instances, handleMessage_fn, resetState_fn, stop_fn, smoothLoopWrap_fn, _clamp, _clampZeroCrossing, findNearestZeroCrossing_fn, normalizedToSamples_fn, samplesToNormalized_fn, midiVelocityToGain_fn, getBufferDurationSeconds_fn, getMusicalNoteDurations_fn, quantizeLoopDuration_fn, extractPositionParams_fn, calculatePlaybackRange_fn, calculateLoopRange_fn, getSafeParam_fn, getConstantFlags_fn, resetDurationPreservation_fn, isDurationPreservationActive_fn, prepareDurationPreservingSample_fn, advanceDurationPreservingPlayback_fn, generateLoopDrift_fn, analyzeLoopAmplitude_fn;
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
- if (sortedArray.length === 0) {
11
- throw new Error("Array cannot be empty");
12
- }
13
- if (sortedArray.length === 1) {
14
- return 0;
15
- }
16
- const targetValue = target;
17
- const firstValue = getValue(sortedArray[0]);
18
- const lastValue = getValue(sortedArray[sortedArray.length - 1]);
19
- if (targetValue <= firstValue) return 0;
20
- if (targetValue >= lastValue) return sortedArray.length - 1;
21
- let left = 0;
22
- let right = sortedArray.length - 1;
23
- while (left < right - 1) {
24
- const mid = Math.floor((left + right) / 2);
25
- const midValue = getValue(sortedArray[mid]);
26
- if (midValue === targetValue) {
27
- return mid;
28
- } else if (midValue < targetValue) {
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
- const index = findClosestIdx(
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
- const SAMPLE_PLAYER_WORKLET_AUDIOPARAMS = {
51
- masterGain: {
52
- name: "masterGain",
53
- defaultValue: 1,
54
- minValue: 0,
55
- maxValue: 2,
56
- automationRate: "k-rate"
57
- },
58
- envGain: {
59
- name: "envGain",
60
- defaultValue: 0,
61
- minValue: 0,
62
- maxValue: 1,
63
- automationRate: "a-rate"
64
- },
65
- velocity: {
66
- name: "velocity",
67
- defaultValue: 100,
68
- minValue: 0,
69
- maxValue: 127,
70
- automationRate: "k-rate"
71
- },
72
- pan: {
73
- name: "pan",
74
- defaultValue: 0,
75
- minValue: -1,
76
- // -1 hard left
77
- maxValue: 1,
78
- // 1 hard right
79
- automationRate: "k-rate"
80
- },
81
- playbackRate: {
82
- name: "playbackRate",
83
- defaultValue: 1,
84
- minValue: 0.1,
85
- maxValue: 24,
86
- automationRate: "a-rate"
87
- },
88
- // NOTE: Time based params use seconds
89
- loopStart: {
90
- name: "loopStart",
91
- defaultValue: 0,
92
- minValue: 0,
93
- maxValue: 99999,
94
- // Max sample length in seconds
95
- automationRate: "k-rate"
96
- },
97
- loopEnd: {
98
- name: "loopEnd",
99
- defaultValue: 99999,
100
- // Will be set to actual buffer duration when loaded
101
- minValue: 0,
102
- maxValue: 99999,
103
- automationRate: "k-rate"
104
- },
105
- startPoint: {
106
- name: "startPoint",
107
- defaultValue: 0,
108
- minValue: 0,
109
- maxValue: 9999,
110
- // Max sample length in seconds
111
- automationRate: "k-rate"
112
- },
113
- endPoint: {
114
- name: "endPoint",
115
- defaultValue: 9999,
116
- // Will be set to actual buffer duration when loaded
117
- minValue: 0,
118
- maxValue: 9999,
119
- automationRate: "k-rate"
120
- },
121
- playbackPosition: {
122
- name: "playbackPosition",
123
- defaultValue: 0,
124
- minValue: 0,
125
- maxValue: 99999,
126
- automationRate: "k-rate"
127
- },
128
- loopDurationDriftAmount: {
129
- name: "loopDurationDriftAmount",
130
- defaultValue: 0,
131
- minValue: 0,
132
- maxValue: 1,
133
- // 0 = no drift, 1 = max drift (up to 100% of loop duration)
134
- automationRate: "k-rate"
135
- },
136
- maxLoopCount: {
137
- name: "maxLoopCount",
138
- defaultValue: 999999,
139
- minValue: 1,
140
- maxValue: 999999,
141
- automationRate: "k-rate"
142
- },
143
- tempo: {
144
- name: "tempo",
145
- defaultValue: 120,
146
- minValue: 20,
147
- maxValue: 300,
148
- automationRate: "k-rate"
149
- }
150
- };
151
- const SAMPLE_PLAYER_WORKLET_AUDIOPARAM_DESCRIPTORS = Object.values(
152
- SAMPLE_PLAYER_WORKLET_AUDIOPARAMS
153
- );
154
- class SamplePlayerProcessor extends AudioWorkletProcessor {
155
- // ===== CONSTRUCTOR =====
156
- constructor() {
157
- super();
158
- __privateAdd(this, _SamplePlayerProcessor_instances);
159
- __privateAdd(this, _clamp, (value, min, max) => Math.max(min, Math.min(max, value)));
160
- __privateAdd(this, _clampZeroCrossing, (value) => __privateGet(this, _clamp).call(this, value, this.minZeroCrossing, this.maxZeroCrossing));
161
- this.buffer = null;
162
- this.minZeroCrossing = 0;
163
- this.maxZeroCrossing = 0;
164
- this.usePlaybackPosition = false;
165
- this.enableLoopSmoothing = true;
166
- this.enableAdaptiveDrift = true;
167
- this.enableAmplitudeCompensation = true;
168
- this.syncLoopToTempo = false;
169
- this.keytrackLoopAmount = 0;
170
- this.durationPreservation = {
171
- enabled: false,
172
- maxDriftSamples: Math.floor(sampleRate * 0.04),
173
- timelinePosition: 0,
174
- resetPending: false
175
- };
176
- this.PITCH_PRESERVATION_THRESHOLD = Math.floor(sampleRate * 0.061);
177
- this.AMPLITUDE_COMPENSATION_THRESHOLD = Math.floor(sampleRate / 16.35);
178
- this.port.onmessage = __privateMethod(this, _SamplePlayerProcessor_instances, handleMessage_fn).bind(this);
179
- __privateMethod(this, _SamplePlayerProcessor_instances, resetState_fn).call(this);
180
- this.port.postMessage({ type: "initialized" });
181
- }
182
- // ===== PARAMETER DESCRIPTORS =====
183
- static get parameterDescriptors() {
184
- return SAMPLE_PLAYER_WORKLET_AUDIOPARAM_DESCRIPTORS;
185
- }
186
- // ===== MAIN PROCESS METHOD =====
187
- process(inputs, outputs, parameters) {
188
- var _a, _b, _c;
189
- const output = outputs[0];
190
- this.debugCounter++;
191
- if (!output || !this.isPlaying || !((_b = (_a = this.buffer) == null ? void 0 : _a[0]) == null ? void 0 : _b.length)) {
192
- return true;
193
- }
194
- const masterGain = parameters.masterGain[0];
195
- const positionParams = __privateMethod(this, _SamplePlayerProcessor_instances, extractPositionParams_fn).call(this, parameters);
196
- const playbackRange = __privateMethod(this, _SamplePlayerProcessor_instances, calculatePlaybackRange_fn).call(this, positionParams);
197
- const effectivePlaybackRate = parameters.playbackRate[0] * this.transpositionPlaybackrate;
198
- const tempo = parameters.tempo[0];
199
- const loopRange = __privateMethod(this, _SamplePlayerProcessor_instances, calculateLoopRange_fn).call(this, positionParams, playbackRange, parameters.loopDurationDriftAmount[0], tempo, effectivePlaybackRate);
200
- const amplitudeGain = __privateMethod(this, _SamplePlayerProcessor_instances, analyzeLoopAmplitude_fn).call(this, loopRange.loopStartSamples, loopRange.loopEndSamples);
201
- const velocityGain = __privateMethod(this, _SamplePlayerProcessor_instances, midiVelocityToGain_fn).call(this, parameters.velocity[0]) * this.velocitySensitivity;
202
- const basePan = parameters.pan[0];
203
- const effectivePan = this.panDriftEnabled ? Math.max(-1, Math.min(1, basePan + this.currentPanDrift)) : basePan;
204
- let outputChannels;
205
- if (output instanceof Float32Array) {
206
- outputChannels = [output];
207
- } else if (Array.isArray(output) && output.every((ch) => ch instanceof Float32Array)) {
208
- outputChannels = output;
209
- } else {
210
- console.error("Unexpected output structure:", {
211
- outputType: typeof output,
212
- isArray: Array.isArray(output),
213
- constructor: (_c = output == null ? void 0 : output.constructor) == null ? void 0 : _c.name,
214
- length: output == null ? void 0 : output.length
215
- });
216
- return true;
217
- }
218
- const numChannels = outputChannels.length;
219
- const isConstant = __privateMethod(this, _SamplePlayerProcessor_instances, getConstantFlags_fn).call(this, parameters);
220
- const silencePadTail = loopRange.loopEndSamples > playbackRange.endSamples;
221
- const TAIL_FADE_SAMPLES = 64;
222
- if (this.playbackPosition === 0) {
223
- this.playbackPosition = this.reversePlayback ? playbackRange.endSamples - 1 : playbackRange.startSamples;
224
- __privateMethod(this, _SamplePlayerProcessor_instances, resetDurationPreservation_fn).call(this, this.playbackPosition);
225
- }
226
- for (let sample = 0; sample < outputChannels[0].length; sample++) {
227
- const envelopeGain = __privateMethod(this, _SamplePlayerProcessor_instances, getSafeParam_fn).call(this, parameters.envGain, sample, isConstant.envGain);
228
- const baseRate = __privateMethod(this, _SamplePlayerProcessor_instances, getSafeParam_fn).call(this, parameters.playbackRate, sample, isConstant.playbackRate);
229
- const effectiveRate = this.reversePlayback ? -Math.abs(baseRate) : Math.abs(baseRate);
230
- const playbackStep = effectiveRate * this.transpositionPlaybackrate;
231
- const canWrapLoop = this.loopEnabled && this.loopCount < parameters.maxLoopCount[0];
232
- if (canWrapLoop) {
233
- if (!this.reversePlayback && this.playbackPosition >= loopRange.loopEndSamples) {
234
- __privateMethod(this, _SamplePlayerProcessor_instances, smoothLoopWrap_fn).call(this, silencePadTail ? 0 : this.buffer[0][Math.floor(this.playbackPosition - 1)] || 0, this.buffer[0][Math.floor(loopRange.loopStartSamples)] || 0);
235
- this.playbackPosition = loopRange.loopStartSamples;
236
- this.loopCount++;
237
- this.nextDriftGenerated = false;
238
- } else if (this.reversePlayback && this.playbackPosition <= loopRange.loopStartSamples) {
239
- __privateMethod(this, _SamplePlayerProcessor_instances, smoothLoopWrap_fn).call(this, this.buffer[0][Math.floor(loopRange.loopStartSamples)] || 0, silencePadTail ? 0 : this.buffer[0][Math.floor(loopRange.loopEndSamples) - 1] || 0);
240
- this.playbackPosition = loopRange.loopEndSamples;
241
- this.loopCount++;
242
- this.nextDriftGenerated = false;
243
- }
244
- }
245
- const durationResetTarget = __privateMethod(this, _SamplePlayerProcessor_instances, prepareDurationPreservingSample_fn).call(this, playbackStep, loopRange);
246
- const shouldStopForward = !this.reversePlayback && (__privateMethod(this, _SamplePlayerProcessor_instances, isDurationPreservationActive_fn).call(this, loopRange) ? this.durationPreservation.timelinePosition : this.playbackPosition) >= playbackRange.endSamples;
247
- const shouldStopReverse = this.reversePlayback && (__privateMethod(this, _SamplePlayerProcessor_instances, isDurationPreservationActive_fn).call(this, loopRange) ? this.durationPreservation.timelinePosition : this.playbackPosition) <= playbackRange.startSamples;
248
- const isWithinLoop = this.loopEnabled && this.playbackPosition >= loopRange.loopStartSamples && this.playbackPosition <= loopRange.loopEndSamples;
249
- if ((shouldStopForward || shouldStopReverse) && !(this.loopEnabled && isWithinLoop)) {
250
- __privateMethod(this, _SamplePlayerProcessor_instances, stop_fn).call(this);
251
- return true;
252
- }
253
- let tailGain = 1;
254
- if (silencePadTail) {
255
- const distToEnd = playbackRange.endSamples - this.playbackPosition;
256
- if (distToEnd < TAIL_FADE_SAMPLES) {
257
- tailGain = Math.max(0, distToEnd / TAIL_FADE_SAMPLES);
258
- }
259
- }
260
- const currentPosition = Math.floor(this.playbackPosition);
261
- const positionOffset = this.playbackPosition - currentPosition;
262
- let nextPosition, interpWeight;
263
- if (this.reversePlayback) {
264
- nextPosition = Math.max(
265
- currentPosition - 1,
266
- playbackRange.startSamples
267
- );
268
- interpWeight = 1 - positionOffset;
269
- } else {
270
- nextPosition = Math.min(
271
- currentPosition + 1,
272
- playbackRange.endSamples - 1
273
- );
274
- interpWeight = positionOffset;
275
- }
276
- for (let channel = 0; channel < numChannels; channel++) {
277
- if (!outputChannels[channel]) {
278
- console.warn(
279
- `Output channel ${channel} does not exist. Available channels:`,
280
- outputChannels.length
281
- );
282
- continue;
283
- }
284
- const bufferChannelIndex = Math.min(channel, this.buffer.length - 1);
285
- const bufferChannel = this.buffer[bufferChannelIndex];
286
- const currentSample = bufferChannel[currentPosition] || 0;
287
- const nextSample = bufferChannel[nextPosition] || 0;
288
- let interpolatedSample = currentSample + interpWeight * (nextSample - currentSample);
289
- if (this.applyClickCompensation) {
290
- interpolatedSample += this.loopClickCompensation;
291
- if (this.compensationDecay) {
292
- this.loopClickCompensation *= this.compensationDecay;
293
- if (Math.abs(this.loopClickCompensation) < 1e-3) {
294
- this.applyClickCompensation = false;
295
- }
296
- } else {
297
- this.applyClickCompensation = false;
298
- }
299
- }
300
- const finalSample = interpolatedSample * velocityGain * envelopeGain * masterGain * amplitudeGain * tailGain;
301
- let panAdjustedSample = finalSample;
302
- if (outputChannels.length === 2) {
303
- if (channel === 0) {
304
- panAdjustedSample = finalSample * (1 - Math.max(0, effectivePan));
305
- } else if (channel === 1) {
306
- panAdjustedSample = finalSample * (1 - Math.max(0, -effectivePan));
307
- }
308
- }
309
- outputChannels[channel][sample] = Math.max(
310
- -1,
311
- Math.min(1, isFinite(panAdjustedSample) ? panAdjustedSample : 0)
312
- );
313
- }
314
- __privateMethod(this, _SamplePlayerProcessor_instances, advanceDurationPreservingPlayback_fn).call(this, playbackStep, durationResetTarget, loopRange, canWrapLoop);
315
- }
316
- if (this.usePlaybackPosition) {
317
- const normalizedPosition = __privateMethod(this, _SamplePlayerProcessor_instances, samplesToNormalized_fn).call(this, this.playbackPosition);
318
- this.port.postMessage({
319
- type: "voice:position",
320
- position: normalizedPosition
321
- });
322
- }
323
- return true;
324
- }
325
- }
326
- _SamplePlayerProcessor_instances = new WeakSet();
327
- // ===== MESSAGE HANDLING =====
328
- handleMessage_fn = function(event) {
329
- const {
330
- type,
331
- value,
332
- buffer,
333
- timestamp,
334
- durationSeconds,
335
- zeroCrossings,
336
- semitones,
337
- allowedPeriods,
338
- playbackDirection
339
- } = event.data;
340
- switch (type) {
341
- case "voice:reset":
342
- __privateMethod(this, _SamplePlayerProcessor_instances, resetState_fn).call(this);
343
- this.port.postMessage({ type: "voice:reset" });
344
- break;
345
- case "voice:setBuffer":
346
- __privateMethod(this, _SamplePlayerProcessor_instances, resetState_fn).call(this);
347
- this.zeroCrossings = [];
348
- this.minZeroCrossing = 0;
349
- this.maxZeroCrossing = 0;
350
- this.buffer = null;
351
- this.buffer = buffer;
352
- this.port.postMessage({
353
- type: "voice:loaded",
354
- durationSeconds,
355
- time: currentTime
356
- });
357
- break;
358
- case "transpose":
359
- this.transpositionPlaybackrate = Math.pow(2, semitones / 12);
360
- this.port.postMessage({
361
- type: "voice:transposed",
362
- semitones,
363
- time: currentTime
364
- });
365
- break;
366
- case "voice:setZeroCrossings":
367
- this.zeroCrossings = (zeroCrossings || []).map(
368
- (timeSec) => timeSec * sampleRate
369
- );
370
- if (this.zeroCrossings.length > 0) {
371
- this.minZeroCrossing = this.zeroCrossings[0];
372
- this.maxZeroCrossing = this.zeroCrossings[this.zeroCrossings.length - 1];
373
- }
374
- break;
375
- case "voice:start":
376
- this.isReleasing = false;
377
- this.isPlaying = true;
378
- this.loopCount = 0;
379
- this.playbackPosition = 0;
380
- this.port.postMessage({
381
- type: "voice:started",
382
- time: timestamp || currentTime
383
- });
384
- break;
385
- case "voice:release":
386
- this.isReleasing = true;
387
- this.port.postMessage({
388
- type: "voice:releasing",
389
- time: currentTime
390
- });
391
- break;
392
- case "voice:stop":
393
- __privateMethod(this, _SamplePlayerProcessor_instances, stop_fn).call(this);
394
- break;
395
- case "setLoopEnabled":
396
- this.loopEnabled = value;
397
- this.port.postMessage({
398
- type: "loop:enabled",
399
- enabled: value
400
- });
401
- break;
402
- case "setPanDriftEnabled":
403
- this.panDriftEnabled = value;
404
- break;
405
- case "voice:setPlaybackDirection": {
406
- const reverse = playbackDirection === "reverse";
407
- if (reverse !== this.reversePlayback && this.playbackPosition > 0) {
408
- this.playbackPosition += reverse ? 1 : -1;
409
- }
410
- this.reversePlayback = reverse;
411
- this.port.postMessage({
412
- type: "voice:playbackDirectionChange",
413
- playbackDirection
414
- });
415
- break;
416
- }
417
- case "voice:usePlaybackPosition":
418
- this.usePlaybackPosition = value;
419
- break;
420
- case "syncLoopToTempo":
421
- this.syncLoopToTempo = value;
422
- this.port.postMessage({
423
- type: "loop:syncToTempo",
424
- enabled: value
425
- });
426
- break;
427
- case "setKeytrackLoopAmount":
428
- this.keytrackLoopAmount = Math.max(0, Math.min(1, value));
429
- break;
430
- case "setPreserveDuration":
431
- this.durationPreservation.enabled = Boolean(value);
432
- __privateMethod(this, _SamplePlayerProcessor_instances, resetDurationPreservation_fn).call(this, this.playbackPosition);
433
- break;
434
- }
435
- };
436
- // ===== METHODS =====
437
- resetState_fn = function() {
438
- this.isPlaying = false;
439
- this.isReleasing = false;
440
- this.loopEnabled = false;
441
- this.transpositionPlaybackrate = 1;
442
- this.velocitySensitivity = 1;
443
- this.reversePlayback = false;
444
- this.playbackPosition = 0;
445
- this.debugCounter = 0;
446
- this.loopCount = 0;
447
- this.applyClickCompensation = false;
448
- this.loopClickCompensation = 0;
449
- this.driftUpdateCounter = 0;
450
- this.currentLoopDrift = 0;
451
- this.currentPanDrift = 0;
452
- this.panDriftEnabled = true;
453
- this.nextDriftGenerated = false;
454
- this.loopAmplitudeGain = 1;
455
- this.lastAnalyzedLoopStart = -1;
456
- this.lastAnalyzedLoopEnd = -1;
457
- __privateMethod(this, _SamplePlayerProcessor_instances, resetDurationPreservation_fn).call(this);
458
- };
459
- stop_fn = function() {
460
- this.isPlaying = false;
461
- this.isReleasing = false;
462
- this.playbackPosition = 0;
463
- this.port.postMessage({ type: "voice:stopped" });
464
- };
465
- // Arm click compensation for a loop-wrap discontinuity between the sample
466
- // just emitted and the first sample of the next pass.
467
- smoothLoopWrap_fn = function(lastLoopSample, newFirstSample) {
468
- const discontinuity = lastLoopSample - newFirstSample;
469
- if (this.enableLoopSmoothing && Math.abs(discontinuity) > 0.01) {
470
- this.loopClickCompensation = discontinuity * 0.5;
471
- this.compensationDecay = 0.9;
472
- this.applyClickCompensation = true;
473
- }
474
- };
475
- _clamp = new WeakMap();
476
- _clampZeroCrossing = new WeakMap();
477
- findNearestZeroCrossing_fn = function(position, direction = "any", maxDistance = null) {
478
- if (!this.zeroCrossings || this.zeroCrossings.length === 0) {
479
- return position;
480
- }
481
- const closestValue = findClosest(this.zeroCrossings, position, direction);
482
- if (maxDistance !== null && Math.abs(closestValue - position) > maxDistance) {
483
- return position;
484
- }
485
- return closestValue;
486
- };
487
- // ===== CONVERSION UTILITIES =====
488
- /**
489
- * Convert normalized position (0-1) to sample index
490
- * @param {number} normalizedPosition - Position as 0-1 value
491
- * @returns {number} - Sample index
492
- */
493
- normalizedToSamples_fn = function(normalizedPosition) {
494
- if (!this.buffer || !this.buffer[0]) return 0;
495
- return normalizedPosition * this.buffer[0].length;
496
- };
497
- /**
498
- * Convert sample index to normalized position (0-1)
499
- * @param {number} sampleIndex - Sample index
500
- * @returns {number} - Normalized position 0-1
501
- */
502
- samplesToNormalized_fn = function(sampleIndex) {
503
- if (!this.buffer || !this.buffer[0]) return 0;
504
- return sampleIndex / this.buffer[0].length;
505
- };
506
- /**
507
- * Convert MIDI velocity (0-127) to gain multiplier (0-1)
508
- * @param {number} midiVelocity - MIDI velocity 0-127
509
- * @returns {number} - Gain multiplier 0-1
510
- */
511
- midiVelocityToGain_fn = function(midiVelocity) {
512
- return Math.max(0, Math.min(1, midiVelocity / 127));
513
- };
514
- /**
515
- * Get buffer duration in seconds
516
- * @returns {number} - Buffer duration in seconds
517
- */
518
- getBufferDurationSeconds_fn = function() {
519
- var _a, _b;
520
- return (((_b = (_a = this.buffer) == null ? void 0 : _a[0]) == null ? void 0 : _b.length) || 0) / sampleRate;
521
- };
522
- /**
523
- * Calculate musical note durations in samples for given tempo
524
- * @param {number} tempo - BPM
525
- * @returns {Object} - Musical note durations in samples
526
- */
527
- getMusicalNoteDurations_fn = function(tempo) {
528
- const beatsPerSecond = tempo / 60;
529
- const samplesPerBeat = sampleRate / beatsPerSecond;
530
- return {
531
- // Standard notes
532
- whole: samplesPerBeat * 4,
533
- half: samplesPerBeat * 2,
534
- quarter: samplesPerBeat,
535
- eighth: samplesPerBeat / 2,
536
- sixteenth: samplesPerBeat / 4,
537
- thirtySecond: samplesPerBeat / 8,
538
- // Triplets (divide by 3/2 = multiply by 2/3)
539
- quarterTriplet: samplesPerBeat * 2 / 3,
540
- eighthTriplet: samplesPerBeat / 2 * 2 / 3,
541
- sixteenthTriplet: samplesPerBeat / 4 * 2 / 3
542
- };
543
- };
544
- /**
545
- * Quantize loop duration to nearest musical interval (skips if below the smallest quantize option)
546
- * @param {number} loopDurationSamples - Current loop duration in samples
547
- * @param {number} tempo - Current tempo in BPM
548
- * @param {number} playbackRate - Current playback rate
549
- * @returns {number} - Quantized loop duration in samples
550
- */
551
- quantizeLoopDuration_fn = function(loopDurationSamples, tempo, playbackRate) {
552
- if (!this.syncLoopToTempo) {
553
- return loopDurationSamples;
554
- }
555
- const noteDurations = __privateMethod(this, _SamplePlayerProcessor_instances, getMusicalNoteDurations_fn).call(this, tempo);
556
- const effectiveDuration = loopDurationSamples / Math.abs(playbackRate);
557
- const smallestInterval = noteDurations.thirtySecond;
558
- if (effectiveDuration < smallestInterval) {
559
- return loopDurationSamples;
560
- }
561
- const intervals = Object.values(noteDurations);
562
- let closestInterval = intervals[0];
563
- let smallestDiff = Math.abs(effectiveDuration - closestInterval);
564
- for (const interval of intervals) {
565
- const diff = Math.abs(effectiveDuration - interval);
566
- if (diff < smallestDiff) {
567
- smallestDiff = diff;
568
- closestInterval = interval;
569
- }
570
- }
571
- return Math.floor(closestInterval * Math.abs(playbackRate));
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.buffer = null;
144
+ this.minZeroCrossing = 0;
145
+ this.maxZeroCrossing = 0;
146
+ this.usePlaybackPosition = false;
147
+ this.enableLoopSmoothing = true;
148
+ this.enableAdaptiveDrift = true;
149
+ this.enableAmplitudeCompensation = true;
150
+ this.syncLoopToTempo = false;
151
+ this.keytrackLoopAmount = 0;
152
+ this.durationPreservation = {
153
+ enabled: false,
154
+ maxDriftSamples: Math.floor(sampleRate * .04),
155
+ timelinePosition: 0,
156
+ resetPending: false
157
+ };
158
+ this.PITCH_PRESERVATION_THRESHOLD = Math.floor(sampleRate * .061);
159
+ this.AMPLITUDE_COMPENSATION_THRESHOLD = Math.floor(sampleRate / 16.35);
160
+ this.port.onmessage = this.#handleMessage.bind(this);
161
+ this.#resetState();
162
+ this.port.postMessage({ type: "initialized" });
163
+ }
164
+ #handleMessage(event) {
165
+ const { type, value, buffer, timestamp, durationSeconds, zeroCrossings, semitones, allowedPeriods, playbackDirection } = event.data;
166
+ switch (type) {
167
+ case "voice:reset":
168
+ this.#resetState();
169
+ this.port.postMessage({ type: "voice:reset" });
170
+ break;
171
+ case "voice:setBuffer":
172
+ this.#resetState();
173
+ this.zeroCrossings = [];
174
+ this.minZeroCrossing = 0;
175
+ this.maxZeroCrossing = 0;
176
+ this.buffer = null;
177
+ this.buffer = buffer;
178
+ this.port.postMessage({
179
+ type: "voice:loaded",
180
+ durationSeconds,
181
+ time: currentTime
182
+ });
183
+ break;
184
+ case "transpose":
185
+ this.transpositionPlaybackrate = Math.pow(2, semitones / 12);
186
+ this.port.postMessage({
187
+ type: "voice:transposed",
188
+ semitones,
189
+ time: currentTime
190
+ });
191
+ break;
192
+ case "voice:setZeroCrossings":
193
+ this.zeroCrossings = (zeroCrossings || []).map((timeSec) => timeSec * sampleRate);
194
+ if (this.zeroCrossings.length > 0) {
195
+ this.minZeroCrossing = this.zeroCrossings[0];
196
+ this.maxZeroCrossing = this.zeroCrossings[this.zeroCrossings.length - 1];
197
+ }
198
+ break;
199
+ case "voice:start":
200
+ this.isReleasing = false;
201
+ this.isPlaying = true;
202
+ this.loopCount = 0;
203
+ this.playbackPosition = 0;
204
+ this.port.postMessage({
205
+ type: "voice:started",
206
+ time: timestamp || currentTime
207
+ });
208
+ break;
209
+ case "voice:release":
210
+ this.isReleasing = true;
211
+ this.port.postMessage({
212
+ type: "voice:releasing",
213
+ time: currentTime
214
+ });
215
+ break;
216
+ case "voice:stop":
217
+ this.#stop();
218
+ break;
219
+ case "setLoopEnabled":
220
+ this.loopEnabled = value;
221
+ this.port.postMessage({
222
+ type: "loop:enabled",
223
+ enabled: value
224
+ });
225
+ break;
226
+ case "setPanDriftEnabled":
227
+ this.panDriftEnabled = value;
228
+ break;
229
+ case "voice:setPlaybackDirection": {
230
+ const reverse = playbackDirection === "reverse";
231
+ if (reverse !== this.reversePlayback && this.playbackPosition > 0) this.playbackPosition += reverse ? 1 : -1;
232
+ this.reversePlayback = reverse;
233
+ this.port.postMessage({
234
+ type: "voice:playbackDirectionChange",
235
+ playbackDirection
236
+ });
237
+ break;
238
+ }
239
+ case "voice:usePlaybackPosition":
240
+ this.usePlaybackPosition = value;
241
+ break;
242
+ case "syncLoopToTempo":
243
+ this.syncLoopToTempo = value;
244
+ this.port.postMessage({
245
+ type: "loop:syncToTempo",
246
+ enabled: value
247
+ });
248
+ break;
249
+ case "setKeytrackLoopAmount":
250
+ this.keytrackLoopAmount = Math.max(0, Math.min(1, value));
251
+ break;
252
+ case "setPreserveDuration":
253
+ this.durationPreservation.enabled = Boolean(value);
254
+ this.#resetDurationPreservation(this.playbackPosition);
255
+ }
256
+ }
257
+ #resetState() {
258
+ this.isPlaying = false;
259
+ this.isReleasing = false;
260
+ this.loopEnabled = false;
261
+ this.transpositionPlaybackrate = 1;
262
+ this.velocitySensitivity = 1;
263
+ this.reversePlayback = false;
264
+ this.playbackPosition = 0;
265
+ this.debugCounter = 0;
266
+ this.loopCount = 0;
267
+ this.applyClickCompensation = false;
268
+ this.loopClickCompensation = 0;
269
+ this.driftUpdateCounter = 0;
270
+ this.currentLoopDrift = 0;
271
+ this.currentPanDrift = 0;
272
+ this.panDriftEnabled = true;
273
+ this.nextDriftGenerated = false;
274
+ this.loopAmplitudeGain = 1;
275
+ this.lastAnalyzedLoopStart = -1;
276
+ this.lastAnalyzedLoopEnd = -1;
277
+ this.#resetDurationPreservation();
278
+ }
279
+ #stop() {
280
+ this.isPlaying = false;
281
+ this.isReleasing = false;
282
+ this.playbackPosition = 0;
283
+ this.port.postMessage({ type: "voice:stopped" });
284
+ }
285
+ #smoothLoopWrap(lastLoopSample, newFirstSample) {
286
+ const discontinuity = lastLoopSample - newFirstSample;
287
+ if (this.enableLoopSmoothing && Math.abs(discontinuity) > .01) {
288
+ this.loopClickCompensation = discontinuity * .5;
289
+ this.compensationDecay = .9;
290
+ this.applyClickCompensation = true;
291
+ }
292
+ }
293
+ #clamp = (value, min, max) => Math.max(min, Math.min(max, value));
294
+ #clampZeroCrossing = (value) => this.#clamp(value, this.minZeroCrossing, this.maxZeroCrossing);
295
+ #findNearestZeroCrossing(position, direction = "any", maxDistance = null) {
296
+ if (!this.zeroCrossings || this.zeroCrossings.length === 0) return position;
297
+ const closestValue = findClosest(this.zeroCrossings, position, direction);
298
+ if (maxDistance !== null && Math.abs(closestValue - position) > maxDistance) return position;
299
+ return closestValue;
300
+ }
301
+ /**
302
+ * Convert normalized position (0-1) to sample index
303
+ * @param {number} normalizedPosition - Position as 0-1 value
304
+ * @returns {number} - Sample index
305
+ */
306
+ #normalizedToSamples(normalizedPosition) {
307
+ if (!this.buffer || !this.buffer[0]) return 0;
308
+ return normalizedPosition * this.buffer[0].length;
309
+ }
310
+ /**
311
+ * Convert sample index to normalized position (0-1)
312
+ * @param {number} sampleIndex - Sample index
313
+ * @returns {number} - Normalized position 0-1
314
+ */
315
+ #samplesToNormalized(sampleIndex) {
316
+ if (!this.buffer || !this.buffer[0]) return 0;
317
+ return sampleIndex / this.buffer[0].length;
318
+ }
319
+ /**
320
+ * Convert MIDI velocity (0-127) to gain multiplier (0-1)
321
+ * @param {number} midiVelocity - MIDI velocity 0-127
322
+ * @returns {number} - Gain multiplier 0-1
323
+ */
324
+ #midiVelocityToGain(midiVelocity) {
325
+ return Math.max(0, Math.min(1, midiVelocity / 127));
326
+ }
327
+ /**
328
+ * Get buffer duration in seconds
329
+ * @returns {number} - Buffer duration in seconds
330
+ */
331
+ #getBufferDurationSeconds() {
332
+ return (this.buffer?.[0]?.length || 0) / sampleRate;
333
+ }
334
+ /**
335
+ * Calculate musical note durations in samples for given tempo
336
+ * @param {number} tempo - BPM
337
+ * @returns {Object} - Musical note durations in samples
338
+ */
339
+ #getMusicalNoteDurations(tempo) {
340
+ const beatsPerSecond = tempo / 60;
341
+ const samplesPerBeat = sampleRate / beatsPerSecond;
342
+ return {
343
+ whole: samplesPerBeat * 4,
344
+ half: samplesPerBeat * 2,
345
+ quarter: samplesPerBeat,
346
+ eighth: samplesPerBeat / 2,
347
+ sixteenth: samplesPerBeat / 4,
348
+ thirtySecond: samplesPerBeat / 8,
349
+ quarterTriplet: samplesPerBeat * 2 / 3,
350
+ eighthTriplet: samplesPerBeat / 2 * 2 / 3,
351
+ sixteenthTriplet: samplesPerBeat / 4 * 2 / 3
352
+ };
353
+ }
354
+ /**
355
+ * Quantize loop duration to nearest musical interval (skips if below the smallest quantize option)
356
+ * @param {number} loopDurationSamples - Current loop duration in samples
357
+ * @param {number} tempo - Current tempo in BPM
358
+ * @param {number} playbackRate - Current playback rate
359
+ * @returns {number} - Quantized loop duration in samples
360
+ */
361
+ #quantizeLoopDuration(loopDurationSamples, tempo, playbackRate) {
362
+ if (!this.syncLoopToTempo) return loopDurationSamples;
363
+ const noteDurations = this.#getMusicalNoteDurations(tempo);
364
+ const effectiveDuration = loopDurationSamples / Math.abs(playbackRate);
365
+ if (effectiveDuration < noteDurations.thirtySecond) return loopDurationSamples;
366
+ const intervals = Object.values(noteDurations);
367
+ let closestInterval = intervals[0];
368
+ let smallestDiff = Math.abs(effectiveDuration - closestInterval);
369
+ for (const interval of intervals) {
370
+ const diff = Math.abs(effectiveDuration - interval);
371
+ if (diff < smallestDiff) {
372
+ smallestDiff = diff;
373
+ closestInterval = interval;
374
+ }
375
+ }
376
+ return Math.floor(closestInterval * Math.abs(playbackRate));
377
+ }
378
+ /**
379
+ * Extract and convert all position parameters from seconds to samples
380
+ * @param {Object} parameters - AudioWorkletProcessor parameters
381
+ * @returns {Object} - Converted parameters in samples
382
+ */
383
+ #extractPositionParams(parameters) {
384
+ return {
385
+ startPointSamples: Math.floor(parameters.startPoint[0] * sampleRate),
386
+ endPointSamples: Math.floor(parameters.endPoint[0] * sampleRate),
387
+ loopStartSamples: Math.floor(parameters.loopStart[0] * sampleRate),
388
+ loopEndSamples: Math.floor(parameters.loopEnd[0] * sampleRate)
389
+ };
390
+ }
391
+ /**
392
+ * Calculate effective playback range in samples
393
+ * @param {Object} params - Position parameters from #extractPositionParams
394
+ * @returns {Object} - Effective start and end positions
395
+ */
396
+ #calculatePlaybackRange(params) {
397
+ const bufferLength = this.buffer?.[0]?.length || 0;
398
+ const start = Math.max(0, params.startPointSamples);
399
+ const end = params.endPointSamples > start ? Math.min(bufferLength, params.endPointSamples) : bufferLength;
400
+ const snappedStart = this.#findNearestZeroCrossing(start, "right");
401
+ const snappedEnd = this.#findNearestZeroCrossing(end, "left");
402
+ return {
403
+ startSamples: snappedStart,
404
+ endSamples: snappedEnd,
405
+ durationSamples: snappedEnd - snappedStart
406
+ };
407
+ }
408
+ /**
409
+ * Calculate effective loop range in samples with optional drift
410
+ * @param {Object} params - Position parameters from #extractPositionParams
411
+ * @param {Object} playbackRange - Range from #calculatePlaybackRange
412
+ * @param {number} driftAmount - Loop duration drift amount (0-1)
413
+ * @param {number} tempo - Current tempo in BPM
414
+ * @param {number} playbackRate - Current playback rate
415
+ * @returns {Object} - Effective loop start and end positions with drift applied
416
+ */
417
+ #calculateLoopRange(params, playbackRange, driftAmount = 0, tempo = 120, playbackRate = 1) {
418
+ const lpStart = params.loopStartSamples;
419
+ const lpEnd = params.loopEndSamples;
420
+ let calcLoopStart = lpStart < lpEnd && lpStart >= 0 ? lpStart : playbackRange.startSamples;
421
+ let calcLoopEnd = lpEnd > lpStart && lpEnd <= playbackRange.endSamples ? lpEnd : playbackRange.endSamples;
422
+ let baseDuration = calcLoopEnd - calcLoopStart;
423
+ if (this.syncLoopToTempo) {
424
+ const quantizedDuration = this.#quantizeLoopDuration(baseDuration, tempo, playbackRate);
425
+ calcLoopEnd = calcLoopStart + quantizedDuration;
426
+ calcLoopEnd = Math.min(calcLoopEnd, playbackRange.endSamples);
427
+ }
428
+ if (baseDuration > this.PITCH_PRESERVATION_THRESHOLD && this.keytrackLoopAmount > 0 && !this.syncLoopToTempo) {
429
+ const scale = 1 + this.keytrackLoopAmount * (Math.abs(playbackRate) - 1);
430
+ baseDuration = Math.max(1, Math.floor(baseDuration * scale));
431
+ calcLoopEnd = calcLoopStart + baseDuration;
432
+ }
433
+ baseDuration = calcLoopEnd - calcLoopStart;
434
+ if (baseDuration > this.PITCH_PRESERVATION_THRESHOLD) calcLoopStart = this.#findNearestZeroCrossing(calcLoopStart, "right");
435
+ if (driftAmount > 0 && this.loopEnabled) {
436
+ if (!this.nextDriftGenerated || this.loopCount === 0) {
437
+ const updateInterval = baseDuration <= this.PITCH_PRESERVATION_THRESHOLD ? Math.max(1, Math.floor(this.PITCH_PRESERVATION_THRESHOLD / baseDuration)) : 1;
438
+ if (this.driftUpdateCounter % updateInterval === 0) {
439
+ this.currentLoopDrift = this.#generateLoopDrift(driftAmount, baseDuration);
440
+ if (this.panDriftEnabled && driftAmount > 0 && this.loopCount > 0) {
441
+ const panDriftAmountScalar = 1e-4;
442
+ this.currentPanDrift = this.currentLoopDrift * panDriftAmountScalar;
443
+ } else this.currentPanDrift = 0;
444
+ }
445
+ this.driftUpdateCounter++;
446
+ this.nextDriftGenerated = true;
447
+ }
448
+ const driftedLoopEnd = calcLoopEnd + this.currentLoopDrift;
449
+ const minLoopDuration = Math.max(1, Math.floor(baseDuration * .1));
450
+ const maxLoopEnd = Math.max(playbackRange.endSamples, calcLoopEnd);
451
+ calcLoopEnd = Math.max(calcLoopStart + minLoopDuration, Math.min(maxLoopEnd, driftedLoopEnd));
452
+ } else this.currentPanDrift = 0;
453
+ if (baseDuration > this.PITCH_PRESERVATION_THRESHOLD && calcLoopEnd <= playbackRange.endSamples) calcLoopEnd = Math.max(calcLoopStart + 1, this.#findNearestZeroCrossing(calcLoopEnd, "left"));
454
+ const loopDuration = calcLoopEnd - calcLoopStart;
455
+ return {
456
+ loopStartSamples: calcLoopStart,
457
+ loopEndSamples: calcLoopEnd,
458
+ loopDurationSamples: loopDuration
459
+ };
460
+ }
461
+ #getSafeParam(paramArray, index, isConstant) {
462
+ return isConstant ? paramArray[0] : paramArray[Math.min(index, paramArray.length - 1)];
463
+ }
464
+ #getConstantFlags(parameters) {
465
+ this.constantFlags ??= {
466
+ envGain: true,
467
+ playbackRate: true
468
+ };
469
+ this.constantFlags.envGain = parameters.envGain.length === 1;
470
+ this.constantFlags.playbackRate = parameters.playbackRate.length === 1;
471
+ return this.constantFlags;
472
+ }
473
+ #resetDurationPreservation(position = 0) {
474
+ this.durationPreservation.timelinePosition = position;
475
+ this.durationPreservation.resetPending = false;
476
+ }
477
+ #isDurationPreservationActive(loopRange) {
478
+ return this.durationPreservation.enabled && Boolean(this.zeroCrossings?.length) && (!this.loopEnabled || loopRange.loopDurationSamples > this.PITCH_PRESERVATION_THRESHOLD);
479
+ }
480
+ #prepareDurationPreservingSample(playbackRate, loopRange) {
481
+ const state = this.durationPreservation;
482
+ if (!this.#isDurationPreservationActive(loopRange)) return null;
483
+ if (Math.abs(this.playbackPosition - state.timelinePosition) > state.maxDriftSamples) state.resetPending = true;
484
+ if (!state.resetPending) return null;
485
+ const direction = playbackRate < 0 ? "left" : "right";
486
+ const outgoingZero = this.#findNearestZeroCrossing(this.playbackPosition, direction);
487
+ if (Math.abs(outgoingZero - this.playbackPosition) > Math.abs(playbackRate)) return null;
488
+ this.playbackPosition = outgoingZero;
489
+ state.resetPending = false;
490
+ return this.#findNearestZeroCrossing(state.timelinePosition, "any", state.maxDriftSamples);
491
+ }
492
+ #advanceDurationPreservingPlayback(playbackRate, resetTarget, loopRange, canWrapLoop) {
493
+ const state = this.durationPreservation;
494
+ this.playbackPosition = resetTarget === null ? this.playbackPosition + playbackRate : resetTarget;
495
+ if (this.#isDurationPreservationActive(loopRange)) {
496
+ state.timelinePosition += playbackRate < 0 ? -1 : 1;
497
+ if (canWrapLoop && playbackRate >= 0 && state.timelinePosition >= loopRange.loopEndSamples) state.timelinePosition = loopRange.loopStartSamples;
498
+ else if (canWrapLoop && playbackRate < 0 && state.timelinePosition <= loopRange.loopStartSamples) state.timelinePosition = loopRange.loopEndSamples - 1;
499
+ } else this.#resetDurationPreservation(this.playbackPosition);
500
+ }
501
+ /**
502
+ * Generate a new drift amount for the current loop iteration
503
+ * @param {number} driftAmount - Maximum drift amount (0-1)
504
+ * @param {number} baseDuration - Base loop duration in samples
505
+ * @returns {number} - Drift amount in samples
506
+ */
507
+ #generateLoopDrift(driftAmount, baseDuration) {
508
+ if (driftAmount <= 0) return 0;
509
+ const randomFactor = (Math.random() - .5) * 2;
510
+ let effectiveDriftAmount = driftAmount;
511
+ if (this.enableAdaptiveDrift) {
512
+ const shortThreshold = 1024;
513
+ const longThreshold = 8192;
514
+ if (baseDuration < shortThreshold) effectiveDriftAmount *= .1;
515
+ else if (baseDuration < longThreshold) {
516
+ const scaleFactor = .1 + .9 * (baseDuration - shortThreshold) / 7168;
517
+ effectiveDriftAmount *= scaleFactor;
518
+ }
519
+ }
520
+ const maxDriftSamples = effectiveDriftAmount * baseDuration;
521
+ return Math.floor(randomFactor * maxDriftSamples);
522
+ }
523
+ /**
524
+ * Analyze loop amplitude and calculate makeup gain for short loops
525
+ * @param {number} loopStart - Loop start position in samples
526
+ * @param {number} loopEnd - Loop end position in samples
527
+ * @returns {number} - Makeup gain multiplier (1.0 = no change)
528
+ */
529
+ #analyzeLoopAmplitude(loopStart, loopEnd) {
530
+ if (!this.enableAmplitudeCompensation || !this.buffer || !this.buffer[0]) return 1;
531
+ if (loopEnd - loopStart >= this.AMPLITUDE_COMPENSATION_THRESHOLD) return 1;
532
+ if (loopStart === this.lastAnalyzedLoopStart && loopEnd === this.lastAnalyzedLoopEnd) return this.loopAmplitudeGain;
533
+ let sumSquares = 0;
534
+ let sampleCount = 0;
535
+ const channel = this.buffer[0];
536
+ const startIndex = Math.floor(loopStart);
537
+ const endIndex = Math.floor(loopEnd);
538
+ for (let i = startIndex; i < endIndex && i < channel.length; i++) {
539
+ const sample = channel[i];
540
+ sumSquares += sample * sample;
541
+ sampleCount++;
542
+ }
543
+ if (sampleCount === 0) return 1;
544
+ const rmsAmplitude = Math.sqrt(sumSquares / sampleCount);
545
+ const targetAmplitude = .3;
546
+ let makeupGain = 1;
547
+ if (rmsAmplitude < targetAmplitude) {
548
+ makeupGain = targetAmplitude / Math.max(rmsAmplitude, .001);
549
+ makeupGain = Math.min(2, makeupGain);
550
+ }
551
+ this.lastAnalyzedLoopStart = loopStart;
552
+ this.lastAnalyzedLoopEnd = loopEnd;
553
+ this.loopAmplitudeGain = makeupGain;
554
+ return makeupGain;
555
+ }
556
+ process(inputs, outputs, parameters) {
557
+ const output = outputs[0];
558
+ this.debugCounter++;
559
+ if (!output || !this.isPlaying || !this.buffer?.[0]?.length) return true;
560
+ const masterGain = parameters.masterGain[0];
561
+ const positionParams = this.#extractPositionParams(parameters);
562
+ const playbackRange = this.#calculatePlaybackRange(positionParams);
563
+ const effectivePlaybackRate = parameters.playbackRate[0] * this.transpositionPlaybackrate;
564
+ const tempo = parameters.tempo[0];
565
+ const loopRange = this.#calculateLoopRange(positionParams, playbackRange, parameters.loopDurationDriftAmount[0], tempo, effectivePlaybackRate);
566
+ const amplitudeGain = this.#analyzeLoopAmplitude(loopRange.loopStartSamples, loopRange.loopEndSamples);
567
+ const velocityGain = this.#midiVelocityToGain(parameters.velocity[0]) * this.velocitySensitivity;
568
+ const basePan = parameters.pan[0];
569
+ const effectivePan = this.panDriftEnabled ? Math.max(-1, Math.min(1, basePan + this.currentPanDrift)) : basePan;
570
+ let outputChannels;
571
+ if (output instanceof Float32Array) outputChannels = [output];
572
+ else if (Array.isArray(output) && output.every((ch) => ch instanceof Float32Array)) outputChannels = output;
573
+ else {
574
+ console.error("Unexpected output structure:", {
575
+ outputType: typeof output,
576
+ isArray: Array.isArray(output),
577
+ constructor: output?.constructor?.name,
578
+ length: output?.length
579
+ });
580
+ return true;
581
+ }
582
+ const numChannels = outputChannels.length;
583
+ const isConstant = this.#getConstantFlags(parameters);
584
+ const silencePadTail = loopRange.loopEndSamples > playbackRange.endSamples;
585
+ const TAIL_FADE_SAMPLES = 64;
586
+ if (this.playbackPosition === 0) {
587
+ this.playbackPosition = this.reversePlayback ? playbackRange.endSamples - 1 : playbackRange.startSamples;
588
+ this.#resetDurationPreservation(this.playbackPosition);
589
+ }
590
+ for (let sample = 0; sample < outputChannels[0].length; sample++) {
591
+ const envelopeGain = this.#getSafeParam(parameters.envGain, sample, isConstant.envGain);
592
+ const baseRate = this.#getSafeParam(parameters.playbackRate, sample, isConstant.playbackRate);
593
+ const playbackStep = (this.reversePlayback ? -Math.abs(baseRate) : Math.abs(baseRate)) * this.transpositionPlaybackrate;
594
+ const canWrapLoop = this.loopEnabled && this.loopCount < parameters.maxLoopCount[0];
595
+ if (canWrapLoop) {
596
+ if (!this.reversePlayback && this.playbackPosition >= loopRange.loopEndSamples) {
597
+ this.#smoothLoopWrap(silencePadTail ? 0 : this.buffer[0][Math.floor(this.playbackPosition - 1)] || 0, this.buffer[0][Math.floor(loopRange.loopStartSamples)] || 0);
598
+ this.playbackPosition = loopRange.loopStartSamples;
599
+ this.loopCount++;
600
+ this.nextDriftGenerated = false;
601
+ } else if (this.reversePlayback && this.playbackPosition <= loopRange.loopStartSamples) {
602
+ this.#smoothLoopWrap(this.buffer[0][Math.floor(loopRange.loopStartSamples)] || 0, silencePadTail ? 0 : this.buffer[0][Math.floor(loopRange.loopEndSamples) - 1] || 0);
603
+ this.playbackPosition = loopRange.loopEndSamples;
604
+ this.loopCount++;
605
+ this.nextDriftGenerated = false;
606
+ }
607
+ }
608
+ const durationResetTarget = this.#prepareDurationPreservingSample(playbackStep, loopRange);
609
+ const shouldStopForward = !this.reversePlayback && (this.#isDurationPreservationActive(loopRange) ? this.durationPreservation.timelinePosition : this.playbackPosition) >= playbackRange.endSamples;
610
+ const shouldStopReverse = this.reversePlayback && (this.#isDurationPreservationActive(loopRange) ? this.durationPreservation.timelinePosition : this.playbackPosition) <= playbackRange.startSamples;
611
+ const isWithinLoop = this.loopEnabled && this.playbackPosition >= loopRange.loopStartSamples && this.playbackPosition <= loopRange.loopEndSamples;
612
+ if ((shouldStopForward || shouldStopReverse) && !(this.loopEnabled && isWithinLoop)) {
613
+ this.#stop();
614
+ return true;
615
+ }
616
+ let tailGain = 1;
617
+ if (silencePadTail) {
618
+ const distToEnd = playbackRange.endSamples - this.playbackPosition;
619
+ if (distToEnd < TAIL_FADE_SAMPLES) tailGain = Math.max(0, distToEnd / TAIL_FADE_SAMPLES);
620
+ }
621
+ const currentPosition = Math.floor(this.playbackPosition);
622
+ const positionOffset = this.playbackPosition - currentPosition;
623
+ let nextPosition, interpWeight;
624
+ if (this.reversePlayback) {
625
+ nextPosition = Math.max(currentPosition - 1, playbackRange.startSamples);
626
+ interpWeight = 1 - positionOffset;
627
+ } else {
628
+ nextPosition = Math.min(currentPosition + 1, playbackRange.endSamples - 1);
629
+ interpWeight = positionOffset;
630
+ }
631
+ for (let channel = 0; channel < numChannels; channel++) {
632
+ if (!outputChannels[channel]) {
633
+ console.warn(`Output channel ${channel} does not exist. Available channels:`, outputChannels.length);
634
+ continue;
635
+ }
636
+ const bufferChannelIndex = Math.min(channel, this.buffer.length - 1);
637
+ const bufferChannel = this.buffer[bufferChannelIndex];
638
+ const currentSample = bufferChannel[currentPosition] || 0;
639
+ const nextSample = bufferChannel[nextPosition] || 0;
640
+ let interpolatedSample = currentSample + interpWeight * (nextSample - currentSample);
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
- * Extract and convert all position parameters from seconds to samples
575
- * @param {Object} parameters - AudioWorkletProcessor parameters
576
- * @returns {Object} - Converted parameters in samples
577
- */
578
- extractPositionParams_fn = function(parameters) {
579
- const samples = {
580
- startPointSamples: Math.floor(parameters.startPoint[0] * sampleRate),
581
- endPointSamples: Math.floor(parameters.endPoint[0] * sampleRate),
582
- loopStartSamples: Math.floor(parameters.loopStart[0] * sampleRate),
583
- loopEndSamples: Math.floor(parameters.loopEnd[0] * sampleRate)
584
- };
585
- return samples;
586
- };
587
- /**
588
- * Calculate effective playback range in samples
589
- * @param {Object} params - Position parameters from #extractPositionParams
590
- * @returns {Object} - Effective start and end positions
591
- */
592
- calculatePlaybackRange_fn = function(params) {
593
- var _a, _b;
594
- const bufferLength = ((_b = (_a = this.buffer) == null ? void 0 : _a[0]) == null ? void 0 : _b.length) || 0;
595
- const start = Math.max(0, params.startPointSamples);
596
- const end = params.endPointSamples > start ? Math.min(bufferLength, params.endPointSamples) : bufferLength;
597
- const snappedStart = __privateMethod(this, _SamplePlayerProcessor_instances, findNearestZeroCrossing_fn).call(this, start, "right");
598
- const snappedEnd = __privateMethod(this, _SamplePlayerProcessor_instances, findNearestZeroCrossing_fn).call(this, end, "left");
599
- return {
600
- startSamples: snappedStart,
601
- endSamples: snappedEnd,
602
- durationSamples: snappedEnd - snappedStart
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
- advanceDurationPreservingPlayback_fn = function(playbackRate, resetTarget, loopRange, canWrapLoop) {
714
- const state = this.durationPreservation;
715
- this.playbackPosition = resetTarget === null ? this.playbackPosition + playbackRate : resetTarget;
716
- if (__privateMethod(this, _SamplePlayerProcessor_instances, isDurationPreservationActive_fn).call(this, loopRange)) {
717
- state.timelinePosition += playbackRate < 0 ? -1 : 1;
718
- if (canWrapLoop && playbackRate >= 0 && state.timelinePosition >= loopRange.loopEndSamples) {
719
- state.timelinePosition = loopRange.loopStartSamples;
720
- } else if (canWrapLoop && playbackRate < 0 && state.timelinePosition <= loopRange.loopStartSamples) {
721
- state.timelinePosition = loopRange.loopEndSamples - 1;
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
- * Generate a new drift amount for the current loop iteration
729
- * @param {number} driftAmount - Maximum drift amount (0-1)
730
- * @param {number} baseDuration - Base loop duration in samples
731
- * @returns {number} - Drift amount in samples
732
- */
733
- generateLoopDrift_fn = function(driftAmount, baseDuration) {
734
- if (driftAmount <= 0) return 0;
735
- const randomFactor = (Math.random() - 0.5) * 2;
736
- let effectiveDriftAmount = driftAmount;
737
- if (this.enableAdaptiveDrift) {
738
- const shortThreshold = 1024;
739
- const longThreshold = 8192;
740
- if (baseDuration < shortThreshold) {
741
- effectiveDriftAmount *= 0.1;
742
- } else if (baseDuration < longThreshold) {
743
- const scaleFactor = 0.1 + 0.9 * (baseDuration - shortThreshold) / (longThreshold - shortThreshold);
744
- effectiveDriftAmount *= scaleFactor;
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
- * Analyze loop amplitude and calculate makeup gain for short loops
752
- * @param {number} loopStart - Loop start position in samples
753
- * @param {number} loopEnd - Loop end position in samples
754
- * @returns {number} - Makeup gain multiplier (1.0 = no change)
755
- */
756
- analyzeLoopAmplitude_fn = function(loopStart, loopEnd) {
757
- if (!this.enableAmplitudeCompensation || !this.buffer || !this.buffer[0]) {
758
- return 1;
759
- }
760
- const loopDuration = loopEnd - loopStart;
761
- if (loopDuration >= this.AMPLITUDE_COMPENSATION_THRESHOLD) {
762
- return 1;
763
- }
764
- if (loopStart === this.lastAnalyzedLoopStart && loopEnd === this.lastAnalyzedLoopEnd) {
765
- return this.loopAmplitudeGain;
766
- }
767
- let sumSquares = 0;
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
- registerProcessor("sample-player-processor", SamplePlayerProcessor);
792
- class RandomNoiseProcessor extends AudioWorkletProcessor {
793
- constructor() {
794
- super();
795
- this.previousNoise = 0;
796
- this.previousFiltered = 0;
797
- this.hpfHz = 150;
798
- this.alpha = this.hpfHz / (this.hpfHz + sampleRate / (2 * Math.PI));
799
- this.port.onmessage = (event) => {
800
- if (event.data.type === "setHpfHz") {
801
- this.hpfHz = event.data.value;
802
- this.alpha = this.calculateAlpha(this.hpfHz);
803
- }
804
- };
805
- this.port.postMessage({ type: "initialized" });
806
- }
807
- calculateAlpha(frequency) {
808
- return frequency / (frequency + sampleRate / (2 * Math.PI));
809
- }
810
- process(inputs, outputs, parameters) {
811
- const output = outputs[0];
812
- output.forEach((channel) => {
813
- for (let i = 0; i < channel.length; i++) {
814
- const noise = Math.random() * 2 - 1;
815
- const filtered = this.alpha * (noise - this.previousNoise) + this.previousFiltered;
816
- this.previousNoise = noise;
817
- this.previousFiltered = filtered;
818
- channel[i] = filtered;
819
- }
820
- });
821
- return true;
822
- }
823
- }
824
- registerProcessor("random-noise-processor", RandomNoiseProcessor);
825
- const cheapSoftClipSingleSample = (sample, max = 0.9) => {
826
- const a = Math.abs(sample);
827
- if (a <= max) return sample;
828
- const x = a / max;
829
- const compressed = x / (1 + x);
830
- return Math.sign(sample) * max * compressed;
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
- const compressSingleSample = (input, threshold = 0.75, ratio = 4, limiter = { enabled: true, type: "soft", outputRange: { min: -1, max: 1 } }) => {
833
- const { min, max } = limiter.outputRange;
834
- let x = input;
835
- if (Math.abs(x) > threshold) {
836
- x = Math.sign(x) * (threshold + (Math.abs(x) - threshold) / ratio);
837
- }
838
- if (limiter.enabled) {
839
- if (limiter.type === "soft") {
840
- x = cheapSoftClipSingleSample(x, Math.abs(max));
841
- } else if (limiter.type === "hard") {
842
- x = Math.max(min, Math.min(max, x));
843
- }
844
- }
845
- return x;
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
- class DelayBuffer {
848
- constructor(maxDelaySamples) {
849
- this.buffer = new Float32Array(maxDelaySamples);
850
- this.writePtr = 0;
851
- this.readPtr = 0;
852
- }
853
- write(sample) {
854
- this.buffer[this.writePtr] = sample;
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
- const DEFAULT_CHARACTER_CONFIG = {
1075
- bitCrushed: {
1076
- bits: 11,
1077
- // bits for bit reduction (e.g. 4 = 16 levels)
1078
- downsample: 3
1079
- // downsample factor (1 = no downsampling, 4 = 1/4 samplerate)
1080
- },
1081
- filtered: {
1082
- freq: 900,
1083
- // Hz
1084
- Q: 0.15
1085
- // very subtle / broad
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
- class Distortion {
1439
- constructor() {
1440
- this.limitingMode = "hard-clipping";
1441
- }
1442
- applyDrive(sample, driveAmount) {
1443
- if (driveAmount <= 0) return sample;
1444
- const driveMultiplier = 1 + driveAmount * 3;
1445
- const drivenSample = sample * driveMultiplier;
1446
- return drivenSample;
1447
- }
1448
- applyClipping(sample, clippingAmount, clipThreshold) {
1449
- if (clippingAmount <= 0) return sample;
1450
- let clippedSample;
1451
- switch (this.limitingMode) {
1452
- case "soft-clipping":
1453
- clippedSample = clipThreshold * Math.tanh(sample / clipThreshold);
1454
- break;
1455
- case "hard-clipping":
1456
- clippedSample = Math.max(
1457
- -clipThreshold,
1458
- Math.min(clipThreshold, sample)
1459
- );
1460
- break;
1461
- case "bypass":
1462
- default:
1463
- clippedSample = sample;
1464
- break;
1465
- }
1466
- if (clipThreshold < 0.08) {
1467
- const makeupGain = Math.min(2, Math.pow(0.1 / clipThreshold, 0.5));
1468
- clippedSample *= makeupGain;
1469
- }
1470
- const blended = sample * (1 - clippingAmount) + clippedSample * clippingAmount;
1471
- return blended;
1472
- }
1473
- setLimitingMode(mode) {
1474
- this.limitingMode = mode;
1475
- }
1476
- }
1477
- registerProcessor(
1478
- "distortion-processor",
1479
- class extends AudioWorkletProcessor {
1480
- static get parameterDescriptors() {
1481
- return [
1482
- {
1483
- name: "distortionDrive",
1484
- defaultValue: 0,
1485
- minValue: 0,
1486
- maxValue: 1,
1487
- automationRate: "a-rate"
1488
- },
1489
- {
1490
- name: "clippingAmount",
1491
- defaultValue: 0,
1492
- minValue: 0,
1493
- maxValue: 1,
1494
- automationRate: "a-rate"
1495
- },
1496
- {
1497
- name: "clippingThreshold",
1498
- defaultValue: 0.5,
1499
- minValue: 0,
1500
- maxValue: 1,
1501
- automationRate: "k-rate"
1502
- }
1503
- ];
1504
- }
1505
- constructor() {
1506
- super();
1507
- this.distortion = new Distortion();
1508
- this.setupMessageHandling();
1509
- this.port.postMessage({ type: "initialized" });
1510
- }
1511
- setupMessageHandling() {
1512
- this.port.onmessage = (event) => {
1513
- switch (event.data.type) {
1514
- case "setLimitingMode":
1515
- this.distortion.setLimitingMode(event.data.mode);
1516
- break;
1517
- default:
1518
- console.warn("distortion-processor: Unsupported message");
1519
- break;
1520
- }
1521
- };
1522
- }
1523
- process(inputs, outputs, parameters) {
1524
- const input = inputs[0];
1525
- const output = outputs[0];
1526
- if (!input || !output) return true;
1527
- const clipThreshold = parameters.clippingThreshold[0];
1528
- for (let i = 0; i < output[0].length; ++i) {
1529
- const distortionDrive = parameters.distortionDrive[Math.min(i, parameters.distortionDrive.length - 1)];
1530
- const clippingAmount = parameters.clippingAmount[Math.min(i, parameters.clippingAmount.length - 1)];
1531
- for (let c = 0; c < Math.min(input.length, output.length); c++) {
1532
- let sample = input[c][i];
1533
- sample = this.distortion.applyDrive(sample, distortionDrive);
1534
- sample = this.distortion.applyClipping(
1535
- sample,
1536
- clippingAmount,
1537
- clipThreshold
1538
- );
1539
- output[c][i] = Math.max(-0.999, Math.min(0.999, sample));
1540
- }
1541
- }
1542
- return true;
1543
- }
1544
- }
1545
- );
1546
- registerProcessor(
1547
- "envelope-follower-processor",
1548
- class extends AudioWorkletProcessor {
1549
- static get parameterDescriptors() {
1550
- return [
1551
- {
1552
- name: "inputGain",
1553
- // linear gain (1.0 = unity)
1554
- defaultValue: 1,
1555
- minValue: 0,
1556
- maxValue: 10,
1557
- automationRate: "k-rate"
1558
- },
1559
- {
1560
- name: "outputGain",
1561
- // linear gain (1.0 = unity)
1562
- defaultValue: 1,
1563
- minValue: 0,
1564
- maxValue: 10,
1565
- automationRate: "k-rate"
1566
- },
1567
- {
1568
- name: "attack",
1569
- // seconds
1570
- defaultValue: 3e-3,
1571
- minValue: 1e-3,
1572
- maxValue: 1,
1573
- automationRate: "k-rate"
1574
- },
1575
- {
1576
- name: "release",
1577
- // seconds
1578
- defaultValue: 0.05,
1579
- minValue: 1e-3,
1580
- maxValue: 5,
1581
- automationRate: "k-rate"
1582
- }
1583
- ];
1584
- }
1585
- constructor() {
1586
- super();
1587
- this.envelope = 0;
1588
- this.gateThreshold = 5e-3;
1589
- this.debugCounter = 0;
1590
- this.port.postMessage({ type: "initialized" });
1591
- }
1592
- process(inputs, outputs, parameters) {
1593
- const input = inputs[0];
1594
- const output = outputs[0];
1595
- const channel = inputs[0][0];
1596
- if (!input || !output || !channel || input.length === 0 || output.length === 0 || channel.length === 0) {
1597
- return true;
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