@ainsej/rubberband-wasm 4.0.0 → 4.0.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.
@@ -0,0 +1,689 @@
1
+ /*!
2
+ * rubberband-wasm v4.0.1 (https://www.npmjs.com/package/rubberband-wasm)
3
+ * (c) Dani Biro
4
+ * @license GPLv2
5
+ */
6
+
7
+ (function () {
8
+ 'use strict';
9
+
10
+ var RubberBandOption;
11
+ (function (RubberBandOption) {
12
+ RubberBandOption[RubberBandOption["RubberBandOptionProcessOffline"] = 0] = "RubberBandOptionProcessOffline";
13
+ RubberBandOption[RubberBandOption["RubberBandOptionProcessRealTime"] = 1] = "RubberBandOptionProcessRealTime";
14
+ RubberBandOption[RubberBandOption["RubberBandOptionStretchElastic"] = 0] = "RubberBandOptionStretchElastic";
15
+ RubberBandOption[RubberBandOption["RubberBandOptionStretchPrecise"] = 16] = "RubberBandOptionStretchPrecise";
16
+ RubberBandOption[RubberBandOption["RubberBandOptionTransientsCrisp"] = 0] = "RubberBandOptionTransientsCrisp";
17
+ RubberBandOption[RubberBandOption["RubberBandOptionTransientsMixed"] = 256] = "RubberBandOptionTransientsMixed";
18
+ RubberBandOption[RubberBandOption["RubberBandOptionTransientsSmooth"] = 512] = "RubberBandOptionTransientsSmooth";
19
+ RubberBandOption[RubberBandOption["RubberBandOptionDetectorCompound"] = 0] = "RubberBandOptionDetectorCompound";
20
+ RubberBandOption[RubberBandOption["RubberBandOptionDetectorPercussive"] = 1024] = "RubberBandOptionDetectorPercussive";
21
+ RubberBandOption[RubberBandOption["RubberBandOptionDetectorSoft"] = 2048] = "RubberBandOptionDetectorSoft";
22
+ RubberBandOption[RubberBandOption["RubberBandOptionPhaseLaminar"] = 0] = "RubberBandOptionPhaseLaminar";
23
+ RubberBandOption[RubberBandOption["RubberBandOptionPhaseIndependent"] = 8192] = "RubberBandOptionPhaseIndependent";
24
+ RubberBandOption[RubberBandOption["RubberBandOptionThreadingAuto"] = 0] = "RubberBandOptionThreadingAuto";
25
+ RubberBandOption[RubberBandOption["RubberBandOptionThreadingNever"] = 65536] = "RubberBandOptionThreadingNever";
26
+ RubberBandOption[RubberBandOption["RubberBandOptionThreadingAlways"] = 131072] = "RubberBandOptionThreadingAlways";
27
+ RubberBandOption[RubberBandOption["RubberBandOptionWindowStandard"] = 0] = "RubberBandOptionWindowStandard";
28
+ RubberBandOption[RubberBandOption["RubberBandOptionWindowShort"] = 1048576] = "RubberBandOptionWindowShort";
29
+ RubberBandOption[RubberBandOption["RubberBandOptionWindowLong"] = 2097152] = "RubberBandOptionWindowLong";
30
+ RubberBandOption[RubberBandOption["RubberBandOptionSmoothingOff"] = 0] = "RubberBandOptionSmoothingOff";
31
+ RubberBandOption[RubberBandOption["RubberBandOptionSmoothingOn"] = 8388608] = "RubberBandOptionSmoothingOn";
32
+ RubberBandOption[RubberBandOption["RubberBandOptionFormantShifted"] = 0] = "RubberBandOptionFormantShifted";
33
+ RubberBandOption[RubberBandOption["RubberBandOptionFormantPreserved"] = 16777216] = "RubberBandOptionFormantPreserved";
34
+ RubberBandOption[RubberBandOption["RubberBandOptionPitchHighSpeed"] = 0] = "RubberBandOptionPitchHighSpeed";
35
+ RubberBandOption[RubberBandOption["RubberBandOptionPitchHighQuality"] = 33554432] = "RubberBandOptionPitchHighQuality";
36
+ RubberBandOption[RubberBandOption["RubberBandOptionPitchHighConsistency"] = 67108864] = "RubberBandOptionPitchHighConsistency";
37
+ RubberBandOption[RubberBandOption["RubberBandOptionChannelsApart"] = 0] = "RubberBandOptionChannelsApart";
38
+ RubberBandOption[RubberBandOption["RubberBandOptionChannelsTogether"] = 268435456] = "RubberBandOptionChannelsTogether";
39
+ RubberBandOption[RubberBandOption["RubberBandOptionEngineFaster"] = 0] = "RubberBandOptionEngineFaster";
40
+ RubberBandOption[RubberBandOption["RubberBandOptionEngineFiner"] = 536870912] = "RubberBandOptionEngineFiner";
41
+ })(RubberBandOption || (RubberBandOption = {}));
42
+ var RubberBandPresetOption;
43
+ (function (RubberBandPresetOption) {
44
+ RubberBandPresetOption[RubberBandPresetOption["DefaultOptions"] = 0] = "DefaultOptions";
45
+ RubberBandPresetOption[RubberBandPresetOption["PercussiveOptions"] = 1056768] = "PercussiveOptions";
46
+ })(RubberBandPresetOption || (RubberBandPresetOption = {}));
47
+ var RubberBandLiveOption;
48
+ (function (RubberBandLiveOption) {
49
+ RubberBandLiveOption[RubberBandLiveOption["RubberBandLiveOptionWindowShort"] = 0] = "RubberBandLiveOptionWindowShort";
50
+ RubberBandLiveOption[RubberBandLiveOption["RubberBandLiveOptionWindowMedium"] = 1048576] = "RubberBandLiveOptionWindowMedium";
51
+ RubberBandLiveOption[RubberBandLiveOption["RubberBandLiveOptionFormantShifted"] = 0] = "RubberBandLiveOptionFormantShifted";
52
+ RubberBandLiveOption[RubberBandLiveOption["RubberBandLiveOptionFormantPreserved"] = 16777216] = "RubberBandLiveOptionFormantPreserved";
53
+ RubberBandLiveOption[RubberBandLiveOption["RubberBandLiveOptionChannelsApart"] = 0] = "RubberBandLiveOptionChannelsApart";
54
+ RubberBandLiveOption[RubberBandLiveOption["RubberBandLiveOptionChannelsTogether"] = 268435456] = "RubberBandLiveOptionChannelsTogether";
55
+ })(RubberBandLiveOption || (RubberBandLiveOption = {}));
56
+ class RubberBandInterface {
57
+ constructor() { }
58
+ static async initialize(module) {
59
+ if (typeof WebAssembly === "undefined") {
60
+ throw new Error("WebAssembly is not supported in this environment!");
61
+ }
62
+ let heap = {};
63
+ const errorHandler = (...params) => {
64
+ console.error("WASI called with params", params);
65
+ return 52;
66
+ };
67
+ let printBuffer = [];
68
+ const wasmInstance = await WebAssembly.instantiate(module, {
69
+ env: {
70
+ emscripten_notify_memory_growth: () => {
71
+ heap.HEAP8 = new Uint8Array(wasmInstance.exports.memory.buffer);
72
+ heap.HEAP32 = new Uint32Array(wasmInstance.exports.memory.buffer);
73
+ },
74
+ },
75
+ wasi_snapshot_preview1: {
76
+ proc_exit: (...params) => errorHandler("proc_exit", params),
77
+ fd_read: (...params) => errorHandler("fd_read", params),
78
+ fd_write: (fd, iov, iovcnt, pnum) => {
79
+ if (fd > 2)
80
+ return 52;
81
+ let num = 0;
82
+ for (let i = 0; i < iovcnt; i++) {
83
+ const ptr = heap.HEAP32[iov >> 2];
84
+ const len = heap.HEAP32[(iov + 4) >> 2];
85
+ iov += 8;
86
+ for (let j = 0; j < len; j++) {
87
+ const curr = heap.HEAP8[ptr + j];
88
+ if (curr === 0 || curr === 10) {
89
+ console.log(printBuffer.join(""));
90
+ printBuffer.length = 0;
91
+ }
92
+ else {
93
+ printBuffer.push(String.fromCharCode(curr));
94
+ }
95
+ }
96
+ num += len;
97
+ }
98
+ heap.HEAP32[pnum >> 2] = num;
99
+ return 0;
100
+ },
101
+ fd_seek: (...params) => errorHandler("fd_seek", params),
102
+ fd_close: (...params) => errorHandler("fd_close", params),
103
+ environ_sizes_get: (penviron_count, penviron_buf_size) => {
104
+ // heap.HEAP32[penviron_count >> 2] = 0;
105
+ // heap.HEAP32[penviron_buf_size >> 2] = 0;
106
+ return 52; // NO_SYS
107
+ },
108
+ environ_get: (...params) => errorHandler("environ_get", params),
109
+ clock_time_get: (...params) => errorHandler("clock_time_get", params),
110
+ },
111
+ });
112
+ const exports = wasmInstance.exports;
113
+ heap.HEAP8 = new Uint8Array(wasmInstance.exports.memory.buffer);
114
+ heap.HEAP32 = new Uint32Array(wasmInstance.exports.memory.buffer);
115
+ exports._initialize();
116
+ const instance = { heap, exports };
117
+ const ret = new RubberBandInterface();
118
+ ret.wasm = instance;
119
+ return ret;
120
+ }
121
+ malloc(size) {
122
+ return this.wasm.exports.wasm_malloc(size);
123
+ }
124
+ memWrite(destPtr, data) {
125
+ const uint8Array = data instanceof Uint8Array ? data : new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
126
+ this.wasm.heap.HEAP8.set(uint8Array, destPtr);
127
+ }
128
+ memWritePtr(destPtr, srcPtr) {
129
+ const buf = new Uint8Array(4);
130
+ const view = new DataView(buf.buffer);
131
+ view.setUint32(0, srcPtr, true);
132
+ this.wasm.heap.HEAP8.set(buf, destPtr);
133
+ }
134
+ memReadU8(srcPtr, length) {
135
+ return this.wasm.heap.HEAP8.subarray(srcPtr, srcPtr + length);
136
+ }
137
+ memReadF32(srcPtr, length) {
138
+ const res = this.memReadU8(srcPtr, length * 4);
139
+ return new Float32Array(res.buffer, res.byteOffset, length);
140
+ }
141
+ free(ptr) {
142
+ this.wasm.exports.wasm_free(ptr);
143
+ }
144
+ rubberband_new(sampleRate, channels, options, initialTimeRatio, initialPitchScale) {
145
+ return this.wasm.exports.rb_new(sampleRate, channels, options, initialTimeRatio, initialPitchScale);
146
+ }
147
+ rubberband_delete(state) {
148
+ this.wasm.exports.rb_delete(state);
149
+ }
150
+ rubberband_reset(state) {
151
+ this.wasm.exports.rb_reset(state);
152
+ }
153
+ rubberband_get_engine_version(state) {
154
+ return this.wasm.exports.rb_get_engine_version(state);
155
+ }
156
+ rubberband_set_time_ratio(state, ratio) {
157
+ this.wasm.exports.rb_set_time_ratio(state, ratio);
158
+ }
159
+ rubberband_set_pitch_scale(state, scale) {
160
+ this.wasm.exports.rb_set_pitch_scale(state, scale);
161
+ }
162
+ rubberband_set_formant_scale(state, scale) {
163
+ this.wasm.exports.rb_set_formant_scale(state, scale);
164
+ }
165
+ rubberband_get_time_ratio(state) {
166
+ return this.wasm.exports.rb_get_time_ratio(state);
167
+ }
168
+ rubberband_get_pitch_scale(state) {
169
+ return this.wasm.exports.rb_get_pitch_scale(state);
170
+ }
171
+ rubberband_get_formant_scale(state) {
172
+ return this.wasm.exports.rb_get_formant_scale(state);
173
+ }
174
+ rubberband_get_preferred_start_pad(state) {
175
+ return this.wasm.exports.rb_get_preferred_start_pad(state);
176
+ }
177
+ rubberband_get_start_delay(state) {
178
+ return this.wasm.exports.rb_get_start_delay(state);
179
+ }
180
+ rubberband_get_latency(state) {
181
+ return this.wasm.exports.rb_get_latency(state);
182
+ }
183
+ rubberband_set_transients_option(state, options) {
184
+ this.wasm.exports.rb_set_transients_option(state, options);
185
+ }
186
+ rubberband_set_detector_option(state, options) {
187
+ this.wasm.exports.rb_set_detector_option(state, options);
188
+ }
189
+ rubberband_set_phase_option(state, options) {
190
+ this.wasm.exports.rb_set_phase_option(state, options);
191
+ }
192
+ rubberband_set_formant_option(state, options) {
193
+ this.wasm.exports.rb_set_formant_option(state, options);
194
+ }
195
+ rubberband_set_pitch_option(state, options) {
196
+ this.wasm.exports.rb_set_pitch_option(state, options);
197
+ }
198
+ rubberband_set_expected_input_duration(state, samples) {
199
+ this.wasm.exports.rb_set_expected_input_duration(state, samples);
200
+ }
201
+ rubberband_get_samples_required(state) {
202
+ return this.wasm.exports.rb_get_samples_required(state);
203
+ }
204
+ rubberband_set_max_process_size(state, samples) {
205
+ this.wasm.exports.rb_set_max_process_size(state, samples);
206
+ }
207
+ rubberband_get_process_size_limit(state) {
208
+ return this.wasm.exports.rb_get_process_size_limit(state);
209
+ }
210
+ rubberband_set_key_frame_map(state, keyframecount, from, to) {
211
+ this.wasm.exports.rb_set_key_frame_map(state, keyframecount, from, to);
212
+ }
213
+ rubberband_study(state, input, samples, final) {
214
+ this.wasm.exports.rb_study(state, input, samples, final);
215
+ }
216
+ rubberband_process(state, input, samples, final) {
217
+ this.wasm.exports.rb_process(state, input, samples, final);
218
+ }
219
+ rubberband_available(state) {
220
+ return this.wasm.exports.rb_available(state);
221
+ }
222
+ rubberband_retrieve(state, output, samples) {
223
+ return this.wasm.exports.rb_retrieve(state, output, samples);
224
+ }
225
+ rubberband_get_channel_count(state) {
226
+ return this.wasm.exports.rb_get_channel_count(state);
227
+ }
228
+ rubberband_calculate_stretch(state) {
229
+ this.wasm.exports.rb_calculate_stretch(state);
230
+ }
231
+ rubberband_set_debug_level(state, level) {
232
+ this.wasm.exports.rb_set_debug_level(state, level);
233
+ }
234
+ rubberband_set_default_debug_level(level) {
235
+ this.wasm.exports.rb_set_default_debug_level(level);
236
+ }
237
+ // RubberBandLiveShifter — real-time pitch shifter introduced in Rubber Band 4.0.
238
+ rubberband_live_new(sampleRate, channels, options) {
239
+ return this.wasm.exports.rb_live_new(sampleRate, channels, options);
240
+ }
241
+ rubberband_live_delete(state) {
242
+ this.wasm.exports.rb_live_delete(state);
243
+ }
244
+ rubberband_live_reset(state) {
245
+ this.wasm.exports.rb_live_reset(state);
246
+ }
247
+ rubberband_live_set_pitch_scale(state, scale) {
248
+ this.wasm.exports.rb_live_set_pitch_scale(state, scale);
249
+ }
250
+ rubberband_live_get_pitch_scale(state) {
251
+ return this.wasm.exports.rb_live_get_pitch_scale(state);
252
+ }
253
+ rubberband_live_set_formant_scale(state, scale) {
254
+ this.wasm.exports.rb_live_set_formant_scale(state, scale);
255
+ }
256
+ rubberband_live_get_formant_scale(state) {
257
+ return this.wasm.exports.rb_live_get_formant_scale(state);
258
+ }
259
+ rubberband_live_get_start_delay(state) {
260
+ return this.wasm.exports.rb_live_get_start_delay(state);
261
+ }
262
+ rubberband_live_set_formant_option(state, options) {
263
+ this.wasm.exports.rb_live_set_formant_option(state, options);
264
+ }
265
+ rubberband_live_get_block_size(state) {
266
+ return this.wasm.exports.rb_live_get_block_size(state);
267
+ }
268
+ rubberband_live_shift(state, input, output) {
269
+ this.wasm.exports.rb_live_shift(state, input, output);
270
+ }
271
+ rubberband_live_get_channel_count(state) {
272
+ return this.wasm.exports.rb_live_get_channel_count(state);
273
+ }
274
+ rubberband_live_set_debug_level(state, level) {
275
+ this.wasm.exports.rb_live_set_debug_level(state, level);
276
+ }
277
+ rubberband_live_set_default_debug_level(level) {
278
+ this.wasm.exports.rb_live_set_default_debug_level(level);
279
+ }
280
+ }
281
+
282
+ const MAX_BLOCK = 8192; // max frames fed to / retrieved from RB per call (scratch size)
283
+ const RING_CAPACITY = 1 << 16; // per-channel output ring buffer size (frames), power of two
284
+ const BUFFER_TARGET = 4096; // frames to keep buffered ahead in the output ring
285
+ const PUMP_BUDGET = 256; // max feed/drain cycles per process() call (render-deadline guard)
286
+ const DEFAULT_OPTIONS = RubberBandOption.RubberBandOptionProcessRealTime |
287
+ RubberBandOption.RubberBandOptionEngineFaster |
288
+ // Required for click-free pitch changes that vary over time and cross 1.0,
289
+ // which is exactly what live pitch control does (R2 engine only).
290
+ RubberBandOption.RubberBandOptionPitchHighConsistency;
291
+ class RubberBandProcessor extends AudioWorkletProcessor {
292
+ constructor() {
293
+ super();
294
+ this.rb = null;
295
+ this.state = 0;
296
+ this.channels = 0;
297
+ this.options = DEFAULT_OPTIONS;
298
+ this.closed = false;
299
+ // wasm scratch (de-interleaved): pointer arrays + per-channel data buffers
300
+ this.inPtr = 0;
301
+ this.outPtr = 0;
302
+ this.inChan = [];
303
+ this.outChan = [];
304
+ // source material
305
+ this.source = [];
306
+ this.sourceLen = 0;
307
+ this.sourceSampleRate = sampleRate;
308
+ this.rateFactor = 1; // contextRate / sourceRate
309
+ // transport
310
+ this.readPos = 0;
311
+ this.playing = false;
312
+ this.loop = false;
313
+ this.finished = false;
314
+ this.sentFinal = false;
315
+ this.endedPosted = false;
316
+ this.startPad = 0; // leading silent input frames still to inject
317
+ this.toDrop = 0; // output frames still to discard (start delay)
318
+ this.needStart = true; // start pad/delay must be (re)computed on next pump
319
+ // ratios (user-facing)
320
+ this.speed = 1; // playback speed; timeRatio = rateFactor / speed
321
+ this.pitchScale = 1; // 1.0 = no shift
322
+ // output ring buffer (shared indices; one Float32Array per channel)
323
+ this.ring = [];
324
+ this.ringRead = 0;
325
+ this.ringWrite = 0;
326
+ this.ringCount = 0;
327
+ this.framesSinceReport = 0;
328
+ this.port.onmessage = (e) => this.onMessage(e.data);
329
+ }
330
+ async onMessage(msg) {
331
+ if (this.closed)
332
+ return;
333
+ switch (msg && msg.type) {
334
+ case "initialise":
335
+ await this.initialise(msg);
336
+ break;
337
+ case "buffer":
338
+ this.setBuffer(msg);
339
+ break;
340
+ case "play":
341
+ if (this.finished)
342
+ this.resetTransport(0);
343
+ this.endedPosted = false;
344
+ this.preroll();
345
+ this.playing = true;
346
+ break;
347
+ case "pause":
348
+ this.playing = false;
349
+ break;
350
+ case "stop":
351
+ this.playing = false;
352
+ this.resetTransport(0);
353
+ break;
354
+ case "seek": {
355
+ const frame = Math.max(0, Math.round((msg.seconds || 0) * this.sourceSampleRate));
356
+ this.resetTransport(Math.min(frame, this.sourceLen));
357
+ if (this.playing)
358
+ this.preroll();
359
+ break;
360
+ }
361
+ case "loop":
362
+ this.loop = !!msg.value;
363
+ break;
364
+ case "speed":
365
+ this.speed = msg.value > 0 ? msg.value : 1;
366
+ this.applyRatios();
367
+ break;
368
+ case "pitch":
369
+ this.pitchScale = msg.value > 0 ? msg.value : 1;
370
+ this.applyRatios();
371
+ break;
372
+ case "close":
373
+ this.dispose();
374
+ break;
375
+ }
376
+ }
377
+ async initialise(msg) {
378
+ try {
379
+ this.channels = msg.channels;
380
+ if (typeof msg.options === "number")
381
+ this.options = msg.options;
382
+ // A compiled WebAssembly.Module cannot be reliably structured-cloned into
383
+ // an AudioWorkletGlobalScope, so the wasm is passed as bytes and compiled
384
+ // here. (A pre-compiled Module is also accepted, e.g. for non-worklet use.)
385
+ const w = msg.wasm;
386
+ const mod = w instanceof WebAssembly.Module ? w : await WebAssembly.compile(w);
387
+ if (this.closed)
388
+ return;
389
+ this.rb = await RubberBandInterface.initialize(mod);
390
+ // A 'close' may have arrived while we were awaiting; don't resurrect.
391
+ if (this.closed) {
392
+ this.rb = null;
393
+ return;
394
+ }
395
+ this.allocate();
396
+ this.createState();
397
+ this.port.postMessage({ type: "ready" });
398
+ }
399
+ catch (e) {
400
+ this.port.postMessage({ type: "error", error: String((e && e.message) || e) });
401
+ }
402
+ }
403
+ allocate() {
404
+ const rb = this.rb;
405
+ this.inPtr = rb.malloc(this.channels * 4);
406
+ this.outPtr = rb.malloc(this.channels * 4);
407
+ this.inChan = [];
408
+ this.outChan = [];
409
+ this.ring = [];
410
+ for (let ch = 0; ch < this.channels; ch++) {
411
+ const ip = rb.malloc(MAX_BLOCK * 4);
412
+ const op = rb.malloc(MAX_BLOCK * 4);
413
+ this.inChan.push(ip);
414
+ this.outChan.push(op);
415
+ rb.memWritePtr(this.inPtr + ch * 4, ip);
416
+ rb.memWritePtr(this.outPtr + ch * 4, op);
417
+ this.ring.push(new Float32Array(RING_CAPACITY));
418
+ }
419
+ }
420
+ createState() {
421
+ const rb = this.rb;
422
+ if (this.state) {
423
+ rb.rubberband_delete(this.state);
424
+ this.state = 0;
425
+ }
426
+ // Construct at the *context* sample rate (the worklet global `sampleRate`),
427
+ // not the source rate, so output frames map 1:1 onto render frames.
428
+ this.state = rb.rubberband_new(sampleRate, this.channels, this.options, 1, 1);
429
+ rb.rubberband_set_max_process_size(this.state, MAX_BLOCK);
430
+ this.applyRatios();
431
+ }
432
+ applyRatios() {
433
+ if (!this.rb || !this.state)
434
+ return;
435
+ // Fold any source/context sample-rate difference into the ratios so RB does
436
+ // the resampling for us: feeding source-rate frames to a context-rate
437
+ // stretcher shifts pitch by rateFactor and speed by rateFactor, which we
438
+ // counteract here.
439
+ const timeRatio = this.rateFactor / this.speed;
440
+ const pitch = this.pitchScale / this.rateFactor;
441
+ this.rb.rubberband_set_time_ratio(this.state, timeRatio);
442
+ this.rb.rubberband_set_pitch_scale(this.state, pitch);
443
+ }
444
+ setBuffer(msg) {
445
+ this.source = msg.channels || [];
446
+ this.sourceLen = this.source[0] ? this.source[0].length : 0;
447
+ this.sourceSampleRate = msg.sampleRate || sampleRate;
448
+ this.rateFactor = sampleRate / this.sourceSampleRate;
449
+ this.playing = false;
450
+ this.resetTransport(0);
451
+ }
452
+ /** Reset transport + stretcher to a clean start at `startFrame`. */
453
+ resetTransport(startFrame) {
454
+ this.readPos = startFrame;
455
+ this.ringRead = 0;
456
+ this.ringWrite = 0;
457
+ this.ringCount = 0;
458
+ this.finished = false;
459
+ this.sentFinal = false;
460
+ this.endedPosted = false;
461
+ this.startPad = 0;
462
+ this.toDrop = 0;
463
+ // Defer querying the start pad / delay until the first pump: they depend on
464
+ // the time/pitch ratios, which may still change (e.g. setBuffer then
465
+ // setPitch/setTempo) before playback of this segment actually begins.
466
+ this.needStart = true;
467
+ const rb = this.rb;
468
+ const st = this.state;
469
+ if (rb && st) {
470
+ rb.rubberband_reset(st);
471
+ this.applyRatios();
472
+ }
473
+ }
474
+ /** Fill the output ring off the render-deadline (called from message handler). */
475
+ preroll() {
476
+ this.pump(BUFFER_TARGET, 1 << 20);
477
+ }
478
+ /**
479
+ * Drive the stretcher until the output ring holds `target` frames (or the
480
+ * stream ends / the budget is exhausted).
481
+ */
482
+ pump(target, budget) {
483
+ const rb = this.rb;
484
+ const st = this.state;
485
+ if (!rb || !st || this.finished)
486
+ return;
487
+ if (this.needStart) {
488
+ // Ratios for this segment are now set; query start pad / delay against them.
489
+ this.applyRatios();
490
+ this.startPad = rb.rubberband_get_preferred_start_pad(st);
491
+ this.toDrop = rb.rubberband_get_start_delay(st);
492
+ this.needStart = false;
493
+ }
494
+ while (this.ringCount < target && !this.finished && budget-- > 0) {
495
+ const avail = rb.rubberband_available(st);
496
+ if (avail < 0) {
497
+ // -1: all input processed and all output read => fully finished.
498
+ this.finished = true;
499
+ break;
500
+ }
501
+ if (avail > 0) {
502
+ this.drain(avail);
503
+ continue;
504
+ }
505
+ // avail === 0: RB needs more input to make progress.
506
+ if (this.sentFinal)
507
+ break; // already flushed; nothing more will come.
508
+ let req = rb.rubberband_get_samples_required(st);
509
+ if (req <= 0)
510
+ req = 1; // ensure forward progress
511
+ if (req > MAX_BLOCK)
512
+ req = MAX_BLOCK;
513
+ const isFinal = this.assembleInput(req);
514
+ rb.rubberband_process(st, this.inPtr, req, isFinal ? 1 : 0);
515
+ if (isFinal)
516
+ this.sentFinal = true;
517
+ }
518
+ }
519
+ /** Assemble `req` input frames into the wasm scratch; returns whether `final`. */
520
+ assembleInput(req) {
521
+ const rb = this.rb;
522
+ const channels = this.channels;
523
+ const dst = [];
524
+ for (let ch = 0; ch < channels; ch++)
525
+ dst.push(rb.memReadF32(this.inChan[ch], req));
526
+ let final = false;
527
+ for (let i = 0; i < req; i++) {
528
+ if (this.startPad > 0) {
529
+ for (let ch = 0; ch < channels; ch++)
530
+ dst[ch][i] = 0;
531
+ this.startPad--;
532
+ continue;
533
+ }
534
+ if (this.readPos >= this.sourceLen) {
535
+ if (this.loop && this.sourceLen > 0) {
536
+ // Wrap within the same block so no frames are withheld at the seam.
537
+ this.readPos = 0;
538
+ }
539
+ else {
540
+ for (let ch = 0; ch < channels; ch++)
541
+ dst[ch][i] = 0;
542
+ final = true; // source exhausted, not looping: last block.
543
+ continue;
544
+ }
545
+ }
546
+ const p = this.readPos;
547
+ for (let ch = 0; ch < channels; ch++) {
548
+ const src = this.source[ch];
549
+ dst[ch][i] = src ? src[p] : 0;
550
+ }
551
+ this.readPos++;
552
+ }
553
+ return final;
554
+ }
555
+ /** Retrieve up to `avail` frames from RB into the ring, applying start-delay trim. */
556
+ drain(avail) {
557
+ const rb = this.rb;
558
+ const st = this.state;
559
+ let remaining = avail;
560
+ while (remaining > 0) {
561
+ const free = RING_CAPACITY - this.ringCount;
562
+ if (free <= 0)
563
+ break;
564
+ const want = Math.min(remaining, MAX_BLOCK, free);
565
+ const got = rb.rubberband_retrieve(st, this.outPtr, want);
566
+ if (got <= 0)
567
+ break;
568
+ // Trim the start delay by FRAME (once per retrieve), not per channel.
569
+ let start = 0;
570
+ if (this.toDrop > 0) {
571
+ const d = Math.min(this.toDrop, got);
572
+ this.toDrop -= d;
573
+ start = d;
574
+ }
575
+ const keep = got - start;
576
+ if (keep > 0) {
577
+ const views = [];
578
+ for (let ch = 0; ch < this.channels; ch++)
579
+ views.push(rb.memReadF32(this.outChan[ch], got));
580
+ this.push(views, start, keep);
581
+ }
582
+ remaining -= got;
583
+ }
584
+ }
585
+ /** Push `keep` frames (from offset `start`) of each channel view into the ring. */
586
+ push(views, start, keep) {
587
+ const cap = RING_CAPACITY;
588
+ const w = this.ringWrite;
589
+ const first = Math.min(keep, cap - w);
590
+ const second = keep - first;
591
+ for (let ch = 0; ch < this.channels; ch++) {
592
+ const src = views[ch];
593
+ this.ring[ch].set(src.subarray(start, start + first), w);
594
+ if (second > 0)
595
+ this.ring[ch].set(src.subarray(start + first, start + keep), 0);
596
+ }
597
+ this.ringWrite = (w + keep) % cap;
598
+ this.ringCount += keep;
599
+ }
600
+ /** Pop `n` frames from the ring into the output channel arrays. */
601
+ pop(out, n) {
602
+ const cap = RING_CAPACITY;
603
+ const r = this.ringRead;
604
+ const first = Math.min(n, cap - r);
605
+ const second = n - first;
606
+ for (let ch = 0; ch < this.channels; ch++) {
607
+ const dst = out[ch];
608
+ if (!dst)
609
+ continue;
610
+ dst.set(this.ring[ch].subarray(r, r + first), 0);
611
+ if (second > 0)
612
+ dst.set(this.ring[ch].subarray(0, second), first);
613
+ }
614
+ this.ringRead = (r + n) % cap;
615
+ this.ringCount -= n;
616
+ }
617
+ dispose() {
618
+ this.closed = true;
619
+ const rb = this.rb;
620
+ if (rb) {
621
+ if (this.state)
622
+ rb.rubberband_delete(this.state);
623
+ for (const p of this.inChan)
624
+ rb.free(p);
625
+ for (const p of this.outChan)
626
+ rb.free(p);
627
+ if (this.inPtr)
628
+ rb.free(this.inPtr);
629
+ if (this.outPtr)
630
+ rb.free(this.outPtr);
631
+ }
632
+ this.state = 0;
633
+ this.rb = null;
634
+ this.port.onmessage = null;
635
+ }
636
+ process(_inputs, outputs) {
637
+ if (this.closed)
638
+ return false; // release the processor
639
+ const out = outputs[0];
640
+ if (!out || out.length === 0)
641
+ return true;
642
+ const n = out[0].length;
643
+ if (!this.rb || !this.state || !this.playing) {
644
+ for (let ch = 0; ch < out.length; ch++)
645
+ out[ch].fill(0);
646
+ return true;
647
+ }
648
+ // Top up the ring with a bounded amount of work (steady-state warm-up was
649
+ // already done off-deadline in preroll()).
650
+ this.pump(BUFFER_TARGET, PUMP_BUDGET);
651
+ const give = Math.min(n, this.ringCount);
652
+ if (give > 0)
653
+ this.pop(out, give);
654
+ for (let ch = 0; ch < out.length; ch++) {
655
+ if (ch >= this.channels) {
656
+ out[ch].fill(0);
657
+ }
658
+ else if (give < n) {
659
+ out[ch].fill(0, give); // underrun / end: pad with silence
660
+ }
661
+ }
662
+ if (this.finished && this.ringCount === 0 && !this.endedPosted) {
663
+ this.endedPosted = true;
664
+ this.playing = false;
665
+ this.port.postMessage({ type: "ended" });
666
+ }
667
+ this.framesSinceReport += n;
668
+ if (this.framesSinceReport >= sampleRate / 30) {
669
+ this.framesSinceReport = 0;
670
+ this.port.postMessage({ type: "position", seconds: this.audiblePositionSeconds() });
671
+ }
672
+ return true;
673
+ }
674
+ // The audible source-time playhead: the input read cursor minus the output
675
+ // still buffered in the ring (converted back to source frames), so it tracks
676
+ // what the listener actually hears rather than what has been fed in.
677
+ audiblePositionSeconds() {
678
+ const bufferedSource = (this.ringCount * this.speed * this.sourceSampleRate) / sampleRate;
679
+ let frames = this.readPos - bufferedSource;
680
+ if (frames < 0)
681
+ frames = this.loop && this.sourceLen ? frames + this.sourceLen : 0;
682
+ if (frames < 0)
683
+ frames = 0;
684
+ return frames / this.sourceSampleRate;
685
+ }
686
+ }
687
+ registerProcessor("rubberband-processor", RubberBandProcessor);
688
+
689
+ })();