@kidlib/web-audio 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +24 -0
- package/dist/components.d.ts +94 -0
- package/dist/components.js +398 -0
- package/dist/index.d.ts +1283 -0
- package/dist/index.js +7550 -0
- package/dist/io.d.ts +239 -0
- package/dist/io.js +193 -0
- package/dist/keymap-3lZMR1Ak.js +167 -0
- package/dist/processors/processors.js +1627 -0
- package/package.json +72 -0
|
@@ -0,0 +1,1627 @@
|
|
|
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;
|
|
9
|
+
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;
|
|
39
|
+
}
|
|
40
|
+
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];
|
|
49
|
+
}
|
|
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));
|
|
572
|
+
};
|
|
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);
|
|
712
|
+
};
|
|
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
|
+
}
|
|
726
|
+
};
|
|
727
|
+
/**
|
|
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);
|
|
749
|
+
};
|
|
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;
|
|
790
|
+
};
|
|
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;
|
|
831
|
+
};
|
|
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;
|
|
846
|
+
};
|
|
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
|
+
}
|
|
1073
|
+
};
|
|
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
|
+
}
|
|
1087
|
+
};
|
|
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
|
+
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
|
+
);
|