@jadujoel/web-audio-clip-node 0.1.0 → 0.1.1

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.
Files changed (51) hide show
  1. package/dist/audio/ClipNode.js +312 -0
  2. package/dist/audio/processor-code.js +2 -0
  3. package/dist/audio/processor-kernel.js +861 -0
  4. package/dist/audio/processor.js +80 -0
  5. package/dist/audio/types.js +9 -0
  6. package/dist/audio/utils.js +128 -0
  7. package/dist/audio/version.d.ts +1 -0
  8. package/dist/audio/version.js +2 -0
  9. package/dist/audio/workletUrl.js +17 -0
  10. package/dist/components/AudioControl.js +99 -0
  11. package/dist/components/ContextMenu.js +73 -0
  12. package/dist/components/ControlSection.js +74 -0
  13. package/dist/components/DetuneControl.js +44 -0
  14. package/dist/components/DisplayPanel.js +6 -0
  15. package/dist/components/FilterControl.js +48 -0
  16. package/dist/components/GainControl.js +44 -0
  17. package/dist/components/PanControl.js +50 -0
  18. package/dist/components/PlaybackRateControl.js +44 -0
  19. package/dist/components/PlayheadSlider.js +20 -0
  20. package/dist/components/SnappableSlider.js +174 -0
  21. package/dist/components/TransportButtons.js +9 -0
  22. package/dist/controls/controlDefs.js +211 -0
  23. package/dist/controls/formatValueText.js +80 -0
  24. package/dist/controls/linkedControlPairs.js +51 -0
  25. package/dist/data/cache.js +17 -0
  26. package/dist/data/fileStore.js +39 -0
  27. package/dist/hooks/useClipNode.js +338 -0
  28. package/dist/lib-react.js +17 -19
  29. package/dist/lib.js +16 -44
  30. package/dist/store/clipStore.js +71 -0
  31. package/examples/README.md +10 -0
  32. package/examples/cdn-vanilla/README.md +13 -0
  33. package/examples/cdn-vanilla/index.html +61 -0
  34. package/examples/esm-bundler/README.md +8 -0
  35. package/examples/esm-bundler/index.html +12 -0
  36. package/examples/esm-bundler/package.json +15 -0
  37. package/examples/esm-bundler/src/main.ts +43 -0
  38. package/examples/react/README.md +10 -0
  39. package/examples/react/index.html +12 -0
  40. package/examples/react/package.json +21 -0
  41. package/examples/react/src/App.tsx +20 -0
  42. package/examples/react/src/main.tsx +9 -0
  43. package/examples/react/vite.config.ts +6 -0
  44. package/examples/self-hosted/README.md +11 -0
  45. package/examples/self-hosted/index.html +12 -0
  46. package/examples/self-hosted/package.json +16 -0
  47. package/examples/self-hosted/public/.gitkeep +1 -0
  48. package/examples/self-hosted/src/main.ts +46 -0
  49. package/package.json +3 -2
  50. package/dist/lib-react.js.map +0 -9
  51. package/dist/lib.js.map +0 -9
@@ -0,0 +1,861 @@
1
+ // processor-kernel.ts — Pure DSP logic, state machine, all filters
2
+ // NO AudioWorklet or platform dependencies. Fully testable.
3
+ import { State, } from "./types";
4
+ // ---------------------------------------------------------------------------
5
+ // Constants
6
+ // ---------------------------------------------------------------------------
7
+ export const SAMPLE_BLOCK_SIZE = 128;
8
+ function createStreamBufferState(buffer = []) {
9
+ const totalLength = buffer[0]?.length ?? 0;
10
+ const hasBuffer = totalLength > 0;
11
+ return {
12
+ totalLength: hasBuffer ? totalLength : null,
13
+ committedLength: hasBuffer ? totalLength : 0,
14
+ streamEnded: hasBuffer,
15
+ streaming: false,
16
+ writtenSpans: hasBuffer ? [{ startSample: 0, endSample: totalLength }] : [],
17
+ pendingWrites: [],
18
+ lowWaterThreshold: SAMPLE_BLOCK_SIZE * 4,
19
+ lowWaterNotified: false,
20
+ lastUnderrunSample: null,
21
+ };
22
+ }
23
+ function getBufferLength(buffer) {
24
+ return buffer[0]?.length ?? 0;
25
+ }
26
+ function getLogicalBufferLength(properties) {
27
+ return (properties.streamBuffer.totalLength ?? getBufferLength(properties.buffer));
28
+ }
29
+ function createSilentBuffer(channels, length) {
30
+ return Array.from({ length: channels }, () => new Float32Array(length));
31
+ }
32
+ function mergeWrittenSpan(spans, nextSpan) {
33
+ const merged = [...spans, nextSpan].sort((a, b) => a.startSample - b.startSample);
34
+ const result = [];
35
+ for (const span of merged) {
36
+ const previous = result[result.length - 1];
37
+ if (!previous || span.startSample > previous.endSample) {
38
+ result.push({ ...span });
39
+ continue;
40
+ }
41
+ previous.endSample = Math.max(previous.endSample, span.endSample);
42
+ }
43
+ return result;
44
+ }
45
+ function getCommittedLength(spans) {
46
+ let committedLength = 0;
47
+ for (const span of spans) {
48
+ if (span.startSample > committedLength)
49
+ break;
50
+ committedLength = Math.max(committedLength, span.endSample);
51
+ }
52
+ return committedLength;
53
+ }
54
+ function resetLowWaterState(streamBuffer, playhead) {
55
+ if (streamBuffer.committedLength - Math.floor(playhead) >=
56
+ streamBuffer.lowWaterThreshold) {
57
+ streamBuffer.lowWaterNotified = false;
58
+ }
59
+ }
60
+ function ensureBufferCapacity(properties, requiredChannels, requiredLength) {
61
+ const currentLength = getBufferLength(properties.buffer);
62
+ const currentChannels = properties.buffer.length;
63
+ if (currentLength >= requiredLength && currentChannels >= requiredChannels) {
64
+ return;
65
+ }
66
+ const nextLength = Math.max(currentLength, requiredLength);
67
+ const nextChannels = Math.max(currentChannels, requiredChannels);
68
+ const nextBuffer = createSilentBuffer(nextChannels, nextLength);
69
+ for (let ch = 0; ch < currentChannels; ch++) {
70
+ nextBuffer[ch].set(properties.buffer[ch].subarray(0, currentLength));
71
+ }
72
+ properties.buffer = nextBuffer;
73
+ if (properties.streamBuffer.totalLength == null ||
74
+ properties.streamBuffer.totalLength < nextLength) {
75
+ properties.streamBuffer.totalLength = nextLength;
76
+ }
77
+ }
78
+ function applyBufferRangeWrite(properties, write) {
79
+ const startSample = Math.max(Math.floor(write.startSample), 0);
80
+ const writeLength = write.channelData[0]?.length ?? 0;
81
+ const requestedTotalLength = write.totalLength ?? null;
82
+ const requiredLength = Math.max(startSample + writeLength, requestedTotalLength ?? 0);
83
+ ensureBufferCapacity(properties, Math.max(write.channelData.length, properties.buffer.length, 1), requiredLength);
84
+ for (let ch = 0; ch < write.channelData.length; ch++) {
85
+ properties.buffer[ch].set(write.channelData[ch], startSample);
86
+ }
87
+ if (requestedTotalLength != null) {
88
+ properties.streamBuffer.totalLength = requestedTotalLength;
89
+ }
90
+ if (writeLength > 0) {
91
+ properties.streamBuffer.writtenSpans = mergeWrittenSpan(properties.streamBuffer.writtenSpans, { startSample, endSample: startSample + writeLength });
92
+ properties.streamBuffer.committedLength = getCommittedLength(properties.streamBuffer.writtenSpans);
93
+ }
94
+ if (write.streamEnded === true) {
95
+ properties.streamBuffer.streamEnded = true;
96
+ }
97
+ resetLowWaterState(properties.streamBuffer, properties.playhead);
98
+ }
99
+ function applyPendingBufferWrites(properties) {
100
+ if (properties.streamBuffer.pendingWrites.length === 0) {
101
+ return;
102
+ }
103
+ for (const write of properties.streamBuffer.pendingWrites) {
104
+ applyBufferRangeWrite(properties, write);
105
+ }
106
+ properties.streamBuffer.pendingWrites = [];
107
+ }
108
+ function setWholeBuffer(properties, buffer) {
109
+ properties.buffer = buffer;
110
+ properties.streamBuffer = createStreamBufferState(buffer);
111
+ }
112
+ // ---------------------------------------------------------------------------
113
+ // Properties & offset
114
+ // ---------------------------------------------------------------------------
115
+ export function getProperties(opts = {}, sampleRate) {
116
+ const { buffer = [], streamBuffer = createStreamBufferState(buffer), duration = -1, loop = false, loopStart = 0, loopEnd = (buffer[0]?.length ?? 0) / sampleRate, loopCrossfade = 0, playhead = 0, offset = 0, startWhen = 0, stopWhen = 0, pauseWhen = 0, resumeWhen = 0, playedSamples = 0, state = State.Initial, timesLooped = 0, fadeInDuration = 0, fadeOutDuration = 0, enableFadeIn = fadeInDuration > 0, enableFadeOut = fadeOutDuration > 0, enableLoopStart = true, enableLoopEnd = true, enableLoopCrossfade = loopCrossfade > 0, enableHighpass = true, enableLowpass = true, enableGain = true, enablePan = true, enableDetune = true, enablePlaybackRate = true, } = opts;
117
+ return {
118
+ buffer,
119
+ streamBuffer,
120
+ loop,
121
+ loopStart,
122
+ loopEnd,
123
+ loopCrossfade,
124
+ duration,
125
+ playhead,
126
+ offset,
127
+ startWhen,
128
+ stopWhen,
129
+ pauseWhen,
130
+ resumeWhen,
131
+ playedSamples,
132
+ state,
133
+ timesLooped,
134
+ fadeInDuration,
135
+ fadeOutDuration,
136
+ enableFadeIn,
137
+ enableFadeOut,
138
+ enableLoopStart,
139
+ enableLoopEnd,
140
+ enableHighpass,
141
+ enableLowpass,
142
+ enableGain,
143
+ enablePan,
144
+ enableDetune,
145
+ enablePlaybackRate,
146
+ enableLoopCrossfade,
147
+ };
148
+ }
149
+ function getBufferDurationSeconds(properties, sampleRate) {
150
+ return getLogicalBufferLength(properties) / sampleRate;
151
+ }
152
+ function normalizeLoopBounds(properties, sampleRate) {
153
+ const bufferDuration = getBufferDurationSeconds(properties, sampleRate);
154
+ if (bufferDuration <= 0) {
155
+ properties.loopStart = 0;
156
+ properties.loopEnd = 0;
157
+ return;
158
+ }
159
+ if (!Number.isFinite(properties.loopStart) || properties.loopStart < 0) {
160
+ properties.loopStart = 0;
161
+ }
162
+ if (properties.loopStart >= bufferDuration) {
163
+ properties.loopStart = 0;
164
+ }
165
+ if (!Number.isFinite(properties.loopEnd) ||
166
+ properties.loopEnd <= properties.loopStart ||
167
+ properties.loopEnd > bufferDuration) {
168
+ properties.loopEnd = bufferDuration;
169
+ }
170
+ }
171
+ export function setOffset(properties, offset, sampleRate) {
172
+ if (offset === undefined) {
173
+ properties.offset = 0;
174
+ return 0;
175
+ }
176
+ if (offset < 0) {
177
+ return setOffset(properties, getLogicalBufferLength(properties) + offset, sampleRate);
178
+ }
179
+ if (offset > (getLogicalBufferLength(properties) || 1) - 1) {
180
+ return setOffset(properties, getLogicalBufferLength(properties) % offset, sampleRate);
181
+ }
182
+ const offs = Math.floor(offset * sampleRate);
183
+ properties.offset = offs;
184
+ return offs;
185
+ }
186
+ // ---------------------------------------------------------------------------
187
+ // Index calculation
188
+ // ---------------------------------------------------------------------------
189
+ export function findIndexesNormal(p) {
190
+ const { playhead, bufferLength, loop, loopStartSamples, loopEndSamples } = p;
191
+ let length = 128;
192
+ if (!loop && playhead + 128 > bufferLength) {
193
+ length = Math.max(bufferLength - playhead, 0);
194
+ }
195
+ const indexes = new Array(length);
196
+ if (!loop) {
197
+ for (let i = 0, head = playhead; i < length; i++, head++) {
198
+ indexes[i] = head;
199
+ }
200
+ const nextPlayhead = playhead + length;
201
+ return {
202
+ playhead: nextPlayhead,
203
+ indexes,
204
+ looped: false,
205
+ ended: nextPlayhead >= bufferLength,
206
+ };
207
+ }
208
+ let head = playhead;
209
+ let looped = false;
210
+ for (let i = 0; i < length; i++, head++) {
211
+ if (head >= loopEndSamples) {
212
+ head = loopStartSamples + (head - loopEndSamples);
213
+ looped = true;
214
+ }
215
+ indexes[i] = head;
216
+ }
217
+ return { indexes, looped, ended: false, playhead: head };
218
+ }
219
+ export function findIndexesWithPlaybackRates(p) {
220
+ const { playhead, bufferLength, loop, loopStartSamples, loopEndSamples, playbackRates, } = p;
221
+ let length = 128;
222
+ if (!loop && playhead + 128 > bufferLength) {
223
+ length = Math.max(bufferLength - playhead, 0);
224
+ }
225
+ const indexes = new Array(length);
226
+ let head = playhead;
227
+ let looped = false;
228
+ if (loop) {
229
+ for (let i = 0; i < length; i++) {
230
+ indexes[i] = Math.min(Math.max(Math.floor(head), 0), bufferLength - 1);
231
+ const rate = playbackRates[i] ?? playbackRates[0] ?? 1;
232
+ head += rate;
233
+ if (rate >= 0 && (head > loopEndSamples || head > bufferLength)) {
234
+ head = loopStartSamples;
235
+ looped = true;
236
+ }
237
+ else if (rate < 0 && (head < loopStartSamples || head < 0)) {
238
+ head = loopEndSamples;
239
+ looped = true;
240
+ }
241
+ }
242
+ return { playhead: head, indexes, looped, ended: false };
243
+ }
244
+ for (let i = 0; i < length; i++) {
245
+ indexes[i] = Math.min(Math.max(Math.floor(head), 0), bufferLength - 1);
246
+ head += playbackRates[i] ?? playbackRates[0] ?? 1;
247
+ }
248
+ return {
249
+ playhead: head,
250
+ indexes,
251
+ looped: false,
252
+ ended: head >= bufferLength || head < 0,
253
+ };
254
+ }
255
+ // ---------------------------------------------------------------------------
256
+ // Buffer operations
257
+ // ---------------------------------------------------------------------------
258
+ export function fill(target, source, indexes) {
259
+ for (let i = 0; i < indexes.length; i++) {
260
+ for (let ch = 0; ch < target.length; ch++) {
261
+ target[ch][i] = source[ch][indexes[i]];
262
+ }
263
+ }
264
+ for (let i = indexes.length; i < target[0].length; i++) {
265
+ for (let ch = 0; ch < target.length; ch++) {
266
+ target[ch][i] = 0;
267
+ }
268
+ }
269
+ }
270
+ export function fillWithSilence(buffer) {
271
+ for (let ch = 0; ch < buffer.length; ch++) {
272
+ for (let j = 0; j < buffer[ch].length; j++) {
273
+ buffer[ch][j] = 0;
274
+ }
275
+ }
276
+ }
277
+ export function monoToStereo(signal) {
278
+ const r = new Float32Array(signal[0].length);
279
+ for (let i = 0; i < signal[0].length; i++) {
280
+ r[i] = signal[0][i];
281
+ }
282
+ signal.push(r);
283
+ }
284
+ export function copy(source, target) {
285
+ for (let i = target.length; i < source.length; i++) {
286
+ target[i] = new Float32Array(source[i].length);
287
+ }
288
+ for (let ch = 0; ch < source.length; ch++) {
289
+ for (let i = 0; i < source[ch].length; i++) {
290
+ target[ch][i] = source[ch][i];
291
+ }
292
+ }
293
+ }
294
+ export function checkNans(output) {
295
+ let numNans = 0;
296
+ for (let ch = 0; ch < output.length; ch++) {
297
+ for (let j = 0; j < output[ch].length; j++) {
298
+ if (Number.isNaN(output[ch][j])) {
299
+ numNans++;
300
+ output[ch][j] = 0;
301
+ }
302
+ }
303
+ }
304
+ return numNans;
305
+ }
306
+ export function createFilterState() {
307
+ return [
308
+ { x_1: 0, x_2: 0, y_1: 0, y_2: 0 },
309
+ { x_1: 0, x_2: 0, y_1: 0, y_2: 0 },
310
+ ];
311
+ }
312
+ export function gainFilter(arr, gains) {
313
+ if (gains.length === 1) {
314
+ const g = gains[0];
315
+ if (g === 1)
316
+ return;
317
+ for (const ch of arr) {
318
+ for (let i = 0; i < ch.length; i++)
319
+ ch[i] *= g;
320
+ }
321
+ return;
322
+ }
323
+ let g = gains[0];
324
+ for (const ch of arr) {
325
+ for (let i = 0; i < ch.length; i++) {
326
+ g = gains[i] ?? g;
327
+ ch[i] *= g;
328
+ }
329
+ }
330
+ }
331
+ export function panFilter(signal, pans) {
332
+ let pan = pans[0];
333
+ for (let i = 0; i < signal[0].length; i++) {
334
+ pan = pans[i] ?? pan;
335
+ const leftGain = pan <= 0 ? 1 : 1 - pan;
336
+ const rightGain = pan >= 0 ? 1 : 1 + pan;
337
+ signal[0][i] *= leftGain;
338
+ signal[1][i] *= rightGain;
339
+ }
340
+ }
341
+ export function lowpassFilter(buffer, cutoffs, sampleRate, states) {
342
+ for (let channel = 0; channel < buffer.length; channel++) {
343
+ const arr = buffer[channel];
344
+ let { x_1, x_2, y_1, y_2 } = states[channel] ?? {
345
+ x_1: 0,
346
+ x_2: 0,
347
+ y_1: 0,
348
+ y_2: 0,
349
+ };
350
+ if (cutoffs.length === 1) {
351
+ const cutoff = cutoffs[0];
352
+ if (cutoff >= 20000)
353
+ return;
354
+ const w0 = (2 * Math.PI * cutoff) / sampleRate;
355
+ const alpha = Math.sin(w0) / 2;
356
+ const b0 = (1 - Math.cos(w0)) / 2;
357
+ const b1 = 1 - Math.cos(w0);
358
+ const b2 = (1 - Math.cos(w0)) / 2;
359
+ const a0 = 1 + alpha;
360
+ const a1 = -2 * Math.cos(w0);
361
+ const a2 = 1 - alpha;
362
+ const h0 = b0 / a0, h1 = b1 / a0, h2 = b2 / a0, h3 = a1 / a0, h4 = a2 / a0;
363
+ for (let i = 0; i < arr.length; i++) {
364
+ const x = arr[i];
365
+ const y = h0 * x + h1 * x_1 + h2 * x_2 - h3 * y_1 - h4 * y_2;
366
+ x_2 = x_1;
367
+ x_1 = x;
368
+ y_2 = y_1;
369
+ y_1 = y;
370
+ arr[i] = y;
371
+ }
372
+ }
373
+ else {
374
+ const prevCutoff = cutoffs[0];
375
+ for (let i = 0; i < arr.length; i++) {
376
+ const cutoff = cutoffs[i] ?? prevCutoff;
377
+ const w0 = (2 * Math.PI * cutoff) / sampleRate;
378
+ const alpha = Math.sin(w0) / 2;
379
+ const b0 = (1 - Math.cos(w0)) / 2;
380
+ const b1 = 1 - Math.cos(w0);
381
+ const b2 = (1 - Math.cos(w0)) / 2;
382
+ const a0 = 1 + alpha;
383
+ const a1 = -2 * Math.cos(w0);
384
+ const a2 = 1 - alpha;
385
+ const x = arr[i];
386
+ const y = (b0 / a0) * x +
387
+ (b1 / a0) * x_1 +
388
+ (b2 / a0) * x_2 -
389
+ (a1 / a0) * y_1 -
390
+ (a2 / a0) * y_2;
391
+ x_2 = x_1;
392
+ x_1 = x;
393
+ y_2 = y_1;
394
+ y_1 = y;
395
+ arr[i] = y;
396
+ }
397
+ }
398
+ states[channel] = { x_1, x_2, y_1, y_2 };
399
+ }
400
+ }
401
+ export function highpassFilter(buffer, cutoffs, sampleRate, states) {
402
+ for (let channel = 0; channel < buffer.length; channel++) {
403
+ const arr = buffer[channel];
404
+ let { x_1, x_2, y_1, y_2 } = states[channel] ?? {
405
+ x_1: 0,
406
+ x_2: 0,
407
+ y_1: 0,
408
+ y_2: 0,
409
+ };
410
+ if (cutoffs.length === 1) {
411
+ const cutoff = cutoffs[0];
412
+ if (cutoff <= 20)
413
+ return;
414
+ const w0 = (2 * Math.PI * cutoff) / sampleRate;
415
+ const alpha = Math.sin(w0) / 2;
416
+ const b0 = (1 + Math.cos(w0)) / 2;
417
+ const b1 = -(1 + Math.cos(w0));
418
+ const b2 = (1 + Math.cos(w0)) / 2;
419
+ const a0 = 1 + alpha;
420
+ const a1 = -2 * Math.cos(w0);
421
+ const a2 = 1 - alpha;
422
+ for (let i = 0; i < arr.length; i++) {
423
+ const x = arr[i];
424
+ const y = (b0 / a0) * x +
425
+ (b1 / a0) * x_1 +
426
+ (b2 / a0) * x_2 -
427
+ (a1 / a0) * y_1 -
428
+ (a2 / a0) * y_2;
429
+ x_2 = x_1;
430
+ x_1 = x;
431
+ y_2 = y_1;
432
+ y_1 = y;
433
+ arr[i] = y;
434
+ }
435
+ }
436
+ else {
437
+ const prevCutoff = cutoffs[0];
438
+ for (let i = 0; i < arr.length; i++) {
439
+ const cutoff = cutoffs[i] ?? prevCutoff;
440
+ const w0 = (2 * Math.PI * cutoff) / sampleRate;
441
+ const alpha = Math.sin(w0) / 2;
442
+ const b0 = (1 + Math.cos(w0)) / 2;
443
+ const b1 = -(1 + Math.cos(w0));
444
+ const b2 = (1 + Math.cos(w0)) / 2;
445
+ const a0 = 1 + alpha;
446
+ const a1 = -2 * Math.cos(w0);
447
+ const a2 = 1 - alpha;
448
+ const x = arr[i];
449
+ const y = (b0 / a0) * x +
450
+ (b1 / a0) * x_1 +
451
+ (b2 / a0) * x_2 -
452
+ (a1 / a0) * y_1 -
453
+ (a2 / a0) * y_2;
454
+ x_2 = x_1;
455
+ x_1 = x;
456
+ y_2 = y_1;
457
+ y_1 = y;
458
+ arr[i] = y;
459
+ }
460
+ }
461
+ states[channel] = { x_1, x_2, y_1, y_2 };
462
+ }
463
+ }
464
+ export function handleProcessorMessage(properties, message, currentTime, sampleRate) {
465
+ const { type, data } = message;
466
+ switch (type) {
467
+ case "buffer":
468
+ setWholeBuffer(properties, data);
469
+ normalizeLoopBounds(properties, sampleRate);
470
+ return [];
471
+ case "bufferInit": {
472
+ const init = data;
473
+ properties.buffer = createSilentBuffer(init.channels, init.totalLength);
474
+ properties.streamBuffer = {
475
+ ...createStreamBufferState(),
476
+ totalLength: init.totalLength,
477
+ streamEnded: false,
478
+ streaming: init.streaming ?? true,
479
+ };
480
+ normalizeLoopBounds(properties, sampleRate);
481
+ return [];
482
+ }
483
+ case "bufferRange":
484
+ properties.streamBuffer.pendingWrites.push(data);
485
+ return [];
486
+ case "bufferEnd": {
487
+ const endData = data;
488
+ if (endData?.totalLength != null) {
489
+ properties.streamBuffer.totalLength = endData.totalLength;
490
+ }
491
+ properties.streamBuffer.streamEnded = true;
492
+ return [];
493
+ }
494
+ case "bufferReset":
495
+ properties.buffer = [];
496
+ properties.streamBuffer = createStreamBufferState();
497
+ normalizeLoopBounds(properties, sampleRate);
498
+ return [];
499
+ case "start":
500
+ properties.timesLooped = 0;
501
+ {
502
+ const d = data;
503
+ properties.duration = d?.duration ?? -1;
504
+ if (properties.duration === -1) {
505
+ properties.duration = properties.loop
506
+ ? Number.MAX_SAFE_INTEGER
507
+ : (properties.buffer[0]?.length ?? 0) / sampleRate;
508
+ }
509
+ setOffset(properties, d?.offset, sampleRate);
510
+ normalizeLoopBounds(properties, sampleRate);
511
+ properties.playhead = properties.offset;
512
+ properties.startWhen = d?.when ?? currentTime;
513
+ properties.stopWhen = properties.startWhen + properties.duration;
514
+ properties.playedSamples = 0;
515
+ properties.state = State.Scheduled;
516
+ }
517
+ return [{ type: "scheduled" }];
518
+ case "stop":
519
+ if (properties.state === State.Ended ||
520
+ properties.state === State.Initial)
521
+ return [];
522
+ properties.stopWhen = data ?? properties.stopWhen;
523
+ properties.state = State.Stopped;
524
+ return [{ type: "stopped" }];
525
+ case "pause":
526
+ properties.state = State.Paused;
527
+ properties.pauseWhen = data ?? currentTime;
528
+ return [{ type: "paused" }];
529
+ case "resume":
530
+ properties.state = State.Started;
531
+ properties.startWhen = data ?? currentTime;
532
+ return [{ type: "resume" }];
533
+ case "dispose":
534
+ properties.state = State.Disposed;
535
+ properties.buffer = [];
536
+ properties.streamBuffer = createStreamBufferState();
537
+ return [{ type: "disposed" }];
538
+ case "loop": {
539
+ const loop = data;
540
+ const st = properties.state;
541
+ if (loop && (st === State.Scheduled || st === State.Started)) {
542
+ properties.stopWhen = Number.MAX_SAFE_INTEGER;
543
+ properties.duration = Number.MAX_SAFE_INTEGER;
544
+ }
545
+ properties.loop = loop;
546
+ if (loop) {
547
+ normalizeLoopBounds(properties, sampleRate);
548
+ }
549
+ return [];
550
+ }
551
+ case "loopStart":
552
+ properties.loopStart = data;
553
+ return [];
554
+ case "loopEnd":
555
+ properties.loopEnd = data;
556
+ return [];
557
+ case "loopCrossfade":
558
+ properties.loopCrossfade = data;
559
+ return [];
560
+ case "playhead":
561
+ properties.playhead = Math.floor(data);
562
+ return [];
563
+ case "fadeIn":
564
+ properties.fadeInDuration = data;
565
+ return [];
566
+ case "fadeOut":
567
+ properties.fadeOutDuration = data;
568
+ return [];
569
+ case "toggleGain":
570
+ properties.enableGain =
571
+ data ?? !properties.enableGain;
572
+ return [];
573
+ case "togglePan":
574
+ properties.enablePan =
575
+ data ?? !properties.enablePan;
576
+ return [];
577
+ case "toggleLowpass":
578
+ properties.enableLowpass =
579
+ data ?? !properties.enableLowpass;
580
+ return [];
581
+ case "toggleHighpass":
582
+ properties.enableHighpass =
583
+ data ?? !properties.enableHighpass;
584
+ return [];
585
+ case "toggleDetune":
586
+ properties.enableDetune =
587
+ data ?? !properties.enableDetune;
588
+ return [];
589
+ case "togglePlaybackRate":
590
+ properties.enablePlaybackRate =
591
+ data ?? !properties.enablePlaybackRate;
592
+ return [];
593
+ case "toggleFadeIn":
594
+ properties.enableFadeIn =
595
+ data ?? !properties.enableFadeIn;
596
+ return [];
597
+ case "toggleFadeOut":
598
+ properties.enableFadeOut =
599
+ data ?? !properties.enableFadeOut;
600
+ return [];
601
+ case "toggleLoopStart":
602
+ properties.enableLoopStart =
603
+ data ?? !properties.enableLoopStart;
604
+ return [];
605
+ case "toggleLoopEnd":
606
+ properties.enableLoopEnd =
607
+ data ?? !properties.enableLoopEnd;
608
+ return [];
609
+ case "toggleLoopCrossfade":
610
+ properties.enableLoopCrossfade =
611
+ data ?? !properties.enableLoopCrossfade;
612
+ return [];
613
+ case "logState":
614
+ return [];
615
+ }
616
+ return [];
617
+ }
618
+ export function processBlock(props, outputs, parameters, ctx, filterState) {
619
+ const messages = [];
620
+ let state = props.state;
621
+ if (state === State.Disposed)
622
+ return { keepAlive: false, messages };
623
+ applyPendingBufferWrites(props);
624
+ if (state === State.Initial)
625
+ return { keepAlive: true, messages };
626
+ if (state === State.Ended) {
627
+ fillWithSilence(outputs[0]);
628
+ return { keepAlive: true, messages };
629
+ }
630
+ if (state === State.Scheduled) {
631
+ if (ctx.currentTime >= props.startWhen) {
632
+ state = props.state = State.Started;
633
+ messages.push({ type: "started" });
634
+ }
635
+ else {
636
+ fillWithSilence(outputs[0]);
637
+ return { keepAlive: true, messages };
638
+ }
639
+ }
640
+ else if (state === State.Paused) {
641
+ if (ctx.currentTime > props.pauseWhen) {
642
+ fillWithSilence(outputs[0]);
643
+ return { keepAlive: true, messages };
644
+ }
645
+ }
646
+ if (ctx.currentTime > props.stopWhen) {
647
+ fillWithSilence(outputs[0]);
648
+ props.state = State.Ended;
649
+ messages.push({ type: "ended" });
650
+ props.playedSamples = 0;
651
+ return { keepAlive: true, messages };
652
+ }
653
+ const output0 = outputs[0];
654
+ const sourceLength = getLogicalBufferLength(props);
655
+ if (sourceLength === 0) {
656
+ fillWithSilence(output0);
657
+ return { keepAlive: true, messages };
658
+ }
659
+ const { playbackRate: playbackRates, detune: detunes, lowpass, highpass, gain: gains, pan: pans, } = parameters;
660
+ const { buffer, loopStart, loopEnd, loopCrossfade, stopWhen, playedSamples, enableLowpass, enableHighpass, enableGain, enablePan, enableDetune, enableFadeOut, enableFadeIn, enableLoopStart, enableLoopEnd, enableLoopCrossfade, playhead, fadeInDuration, fadeOutDuration, } = props;
661
+ const hasIncompleteStream = props.streamBuffer.streaming &&
662
+ props.streamBuffer.committedLength < sourceLength;
663
+ const loop = props.loop && !hasIncompleteStream;
664
+ const nc = Math.min(buffer.length, output0.length);
665
+ const durationSamples = props.duration * ctx.sampleRate;
666
+ const loopCrossfadeSamples = Math.floor(ctx.sampleRate * loopCrossfade);
667
+ const maxLoopStartSample = Math.max(sourceLength - SAMPLE_BLOCK_SIZE, 0);
668
+ const loopStartSamples = enableLoopStart
669
+ ? Math.min(Math.floor(loopStart * ctx.sampleRate), maxLoopStartSample)
670
+ : 0;
671
+ const loopEndSamples = enableLoopEnd
672
+ ? Math.min(Math.floor(loopEnd * ctx.sampleRate), sourceLength)
673
+ : sourceLength;
674
+ const loopLengthSamples = loopEndSamples - loopStartSamples;
675
+ // Apply detune to playback rates: effectiveRate = rate * 2^(detune/1200)
676
+ const needsDetune = enableDetune && detunes.length > 0 && detunes[0] !== 0;
677
+ let effectiveRates = playbackRates;
678
+ if (needsDetune) {
679
+ const len = Math.max(playbackRates.length, detunes.length, SAMPLE_BLOCK_SIZE);
680
+ effectiveRates = new Float32Array(len);
681
+ for (let i = 0; i < len; i++) {
682
+ const rate = playbackRates[i] ?? playbackRates[playbackRates.length - 1];
683
+ const cents = detunes[i] ?? detunes[detunes.length - 1];
684
+ effectiveRates[i] = rate * 2 ** (cents / 1200);
685
+ }
686
+ }
687
+ const useRateIndexing = props.enablePlaybackRate || needsDetune;
688
+ const isZeroRateBlock = useRateIndexing &&
689
+ effectiveRates.length > 0 &&
690
+ effectiveRates.every((rate) => rate === 0);
691
+ if (props.streamBuffer.streaming &&
692
+ !props.streamBuffer.streamEnded &&
693
+ !props.streamBuffer.lowWaterNotified &&
694
+ props.streamBuffer.committedLength - Math.floor(playhead) <
695
+ props.streamBuffer.lowWaterThreshold) {
696
+ messages.push({
697
+ type: "bufferLowWater",
698
+ data: {
699
+ playhead: Math.floor(playhead),
700
+ committedLength: props.streamBuffer.committedLength,
701
+ },
702
+ });
703
+ props.streamBuffer.lowWaterNotified = true;
704
+ }
705
+ if (isZeroRateBlock) {
706
+ fillWithSilence(output0);
707
+ for (let i = 1; i < outputs.length; i++) {
708
+ copy(output0, outputs[i]);
709
+ }
710
+ return { keepAlive: true, messages };
711
+ }
712
+ const blockParams = {
713
+ bufferLength: sourceLength,
714
+ loop,
715
+ playhead,
716
+ loopStartSamples,
717
+ loopEndSamples,
718
+ durationSamples,
719
+ playbackRates: effectiveRates,
720
+ };
721
+ const { indexes, ended, looped, playhead: updatedPlayhead, } = useRateIndexing
722
+ ? findIndexesWithPlaybackRates(blockParams)
723
+ : findIndexesNormal(blockParams);
724
+ const underrunSample = indexes.find((index) => index >= props.streamBuffer.committedLength && index < sourceLength);
725
+ if (underrunSample !== undefined &&
726
+ !props.streamBuffer.streamEnded &&
727
+ props.streamBuffer.lastUnderrunSample !== underrunSample) {
728
+ messages.push({
729
+ type: "bufferUnderrun",
730
+ data: {
731
+ playhead: Math.floor(playhead),
732
+ committedLength: props.streamBuffer.committedLength,
733
+ requestedSample: underrunSample,
734
+ },
735
+ });
736
+ props.streamBuffer.lastUnderrunSample = underrunSample;
737
+ }
738
+ else if (underrunSample === undefined) {
739
+ props.streamBuffer.lastUnderrunSample = null;
740
+ }
741
+ fill(output0, buffer, indexes);
742
+ // --- Loop crossfade ---
743
+ const xfadeNumSamples = Math.min(Math.floor(loopCrossfade * ctx.sampleRate), loopLengthSamples);
744
+ const isWithinLoopRange = loop && playhead > loopStartSamples && playhead < loopEndSamples;
745
+ const needsCrossfade = enableLoopCrossfade &&
746
+ loopCrossfadeSamples > 0 &&
747
+ sourceLength > SAMPLE_BLOCK_SIZE;
748
+ if (isWithinLoopRange && needsCrossfade) {
749
+ // Crossfade out at loop start: fade out tail of previous loop iteration.
750
+ // Source: reads from END of loop (loopEnd - xfade to loopEnd).
751
+ {
752
+ const endIndex = loopStartSamples + xfadeNumSamples;
753
+ if (xfadeNumSamples > 0 &&
754
+ playhead > loopStartSamples &&
755
+ playhead < endIndex) {
756
+ const elapsed = playhead - loopStartSamples;
757
+ const n = Math.min(Math.floor(endIndex - playhead), SAMPLE_BLOCK_SIZE);
758
+ for (let i = 0; i < n; i++) {
759
+ const position = (elapsed + i) / xfadeNumSamples;
760
+ const g = Math.cos((Math.PI * position) / 2);
761
+ const srcIdx = Math.floor(loopEndSamples - xfadeNumSamples + elapsed + i);
762
+ if (srcIdx >= 0 && srcIdx < sourceLength) {
763
+ for (let ch = 0; ch < nc; ch++) {
764
+ output0[ch][i] += buffer[ch][srcIdx] * g;
765
+ }
766
+ }
767
+ }
768
+ }
769
+ }
770
+ // Crossfade in approaching loop end: fade in head of next loop iteration.
771
+ // Source: reads from START of loop (loopStart to loopStart + xfade).
772
+ {
773
+ const startIndex = loopEndSamples - xfadeNumSamples;
774
+ if (xfadeNumSamples > 0 &&
775
+ playhead > startIndex &&
776
+ playhead < loopEndSamples) {
777
+ const elapsed = playhead - startIndex;
778
+ const n = Math.min(Math.floor(loopEndSamples - playhead), SAMPLE_BLOCK_SIZE);
779
+ for (let i = 0; i < n; i++) {
780
+ const position = (elapsed + i) / xfadeNumSamples;
781
+ const g = Math.sin((Math.PI * position) / 2);
782
+ const srcIdx = Math.floor(loopStartSamples + elapsed + i);
783
+ if (srcIdx >= 0 && srcIdx < sourceLength) {
784
+ for (let ch = 0; ch < nc; ch++) {
785
+ output0[ch][i] += buffer[ch][srcIdx] * g;
786
+ }
787
+ }
788
+ }
789
+ }
790
+ }
791
+ }
792
+ // --- Fade in ---
793
+ if (enableFadeIn && fadeInDuration > 0) {
794
+ const fadeInSamples = Math.floor(fadeInDuration * ctx.sampleRate);
795
+ const remaining = fadeInSamples - playedSamples;
796
+ if (remaining > 0) {
797
+ const n = Math.min(remaining, SAMPLE_BLOCK_SIZE);
798
+ for (let i = 0; i < n; i++) {
799
+ const t = (playedSamples + i) / fadeInSamples;
800
+ const g = t * t * t; // cubic: slow start, fast finish
801
+ for (let ch = 0; ch < nc; ch++) {
802
+ output0[ch][i] *= g;
803
+ }
804
+ }
805
+ }
806
+ }
807
+ // --- Fade out ---
808
+ if (enableFadeOut && fadeOutDuration > 0) {
809
+ const fadeOutSamples = Math.floor(fadeOutDuration * ctx.sampleRate);
810
+ const remainingSamples = Math.floor(ctx.sampleRate * (stopWhen - ctx.currentTime));
811
+ if (remainingSamples < fadeOutSamples + SAMPLE_BLOCK_SIZE) {
812
+ for (let i = 0; i < SAMPLE_BLOCK_SIZE; i++) {
813
+ const sampleRemaining = remainingSamples - i;
814
+ if (sampleRemaining >= fadeOutSamples)
815
+ continue; // not yet in fade zone
816
+ const t = sampleRemaining <= 0 ? 0 : sampleRemaining / fadeOutSamples;
817
+ const g = t * t * t; // cubic fade-out: fast drop, slow tail
818
+ for (let ch = 0; ch < nc; ch++) {
819
+ output0[ch][i] *= g;
820
+ }
821
+ }
822
+ }
823
+ }
824
+ // --- Filters ---
825
+ if (enableLowpass)
826
+ lowpassFilter(output0, lowpass, ctx.sampleRate, filterState.lowpass);
827
+ if (enableHighpass)
828
+ highpassFilter(output0, highpass, ctx.sampleRate, filterState.highpass);
829
+ if (enableGain)
830
+ gainFilter(output0, gains);
831
+ if (nc === 1)
832
+ monoToStereo(output0);
833
+ if (enablePan)
834
+ panFilter(output0, pans);
835
+ if (looped) {
836
+ props.timesLooped++;
837
+ messages.push({ type: "looped", data: props.timesLooped });
838
+ }
839
+ if (ended) {
840
+ props.state = State.Ended;
841
+ messages.push({ type: "ended" });
842
+ }
843
+ props.playedSamples += indexes.length;
844
+ props.playhead = updatedPlayhead;
845
+ const numNans = checkNans(output0);
846
+ if (numNans > 0) {
847
+ console.log({
848
+ numNans,
849
+ indexes,
850
+ playhead: updatedPlayhead,
851
+ ended,
852
+ looped,
853
+ sourceLength,
854
+ });
855
+ return { keepAlive: true, messages };
856
+ }
857
+ for (let i = 1; i < outputs.length; i++) {
858
+ copy(output0, outputs[i]);
859
+ }
860
+ return { keepAlive: true, messages };
861
+ }