waa-play 0.2.0 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +41 -89
- package/dist/adapters.cjs +6 -6
- package/dist/adapters.d.cts +1 -1
- package/dist/adapters.d.ts +1 -1
- package/dist/adapters.js +1 -1
- package/dist/buffer.cjs +5 -5
- package/dist/buffer.d.cts +1 -1
- package/dist/buffer.d.ts +1 -1
- package/dist/buffer.js +1 -1
- package/dist/{chunk-T74FBKTY.js → chunk-2FGUFHZM.js} +2 -2
- package/dist/{chunk-CPAT75WD.cjs → chunk-3VTU5OX5.cjs} +2 -2
- package/dist/{chunk-2DL7CAEP.js → chunk-7JUVBZ6B.js} +2 -2
- package/dist/{chunk-D5CD5KQZ.cjs → chunk-BRS7LZVH.cjs} +2 -2
- package/dist/{chunk-QWNV2BZ5.cjs → chunk-F6WXD3XW.cjs} +2 -2
- package/dist/{chunk-C2ASIYN5.js → chunk-FESPIMZM.js} +3 -7
- package/dist/{chunk-GYH2JSCY.js → chunk-FY273Z3I.js} +2 -2
- package/dist/{chunk-SIMLANWE.cjs → chunk-G37HMZEX.cjs} +1028 -955
- package/dist/{chunk-2FFORBOP.js → chunk-GDBOHOGF.js} +1027 -955
- package/dist/{chunk-5J7S6QV3.cjs → chunk-HIF3UAF3.cjs} +2 -2
- package/dist/{chunk-CRODJ4KS.js → chunk-HTN52U23.js} +13 -6
- package/dist/{chunk-X4IFO7U7.js → chunk-HYRDCTBO.js} +143 -116
- package/dist/{chunk-VKT7YCWK.js → chunk-JIHPQAEA.js} +6 -3
- package/dist/chunk-KVKW7W66.cjs +148 -0
- package/dist/{chunk-4LNVRSTM.cjs → chunk-OIY6I4TU.cjs} +3 -7
- package/dist/{chunk-7S5KWTZ6.cjs → chunk-OZN5X4N6.cjs} +6 -3
- package/dist/{chunk-CJJC6ASU.js → chunk-PL4J3NR7.js} +2 -2
- package/dist/{chunk-IMNRPYBM.js → chunk-QFJQU7TQ.js} +10 -10
- package/dist/{chunk-M5PDY5EZ.cjs → chunk-QGZGERGK.cjs} +2 -2
- package/dist/{chunk-QFFQQMU4.cjs → chunk-VOSIA3GF.cjs} +13 -6
- package/dist/{chunk-CTUCTTIE.cjs → chunk-VY4UMZMJ.cjs} +145 -118
- package/dist/{chunk-LETS7FKB.js → chunk-YFK7ETCF.js} +2 -2
- package/dist/context.d.cts +1 -1
- package/dist/context.d.ts +1 -1
- package/dist/emitter.cjs +2 -2
- package/dist/emitter.js +1 -1
- package/dist/engine-7DCOERRN.js +4 -0
- package/dist/engine-ALWPAIX6.cjs +17 -0
- package/dist/fade.cjs +5 -5
- package/dist/fade.d.cts +1 -1
- package/dist/fade.d.ts +1 -1
- package/dist/fade.js +1 -1
- package/dist/index.cjs +44 -44
- package/dist/index.d.cts +7 -7
- package/dist/index.d.ts +7 -7
- package/dist/index.js +10 -10
- package/dist/nodes.cjs +11 -11
- package/dist/nodes.js +1 -1
- package/dist/play.cjs +3 -3
- package/dist/play.d.cts +1 -1
- package/dist/play.d.ts +1 -1
- package/dist/play.js +2 -2
- package/dist/player.cjs +11 -11
- package/dist/player.d.cts +1 -1
- package/dist/player.d.ts +1 -1
- package/dist/player.js +10 -10
- package/dist/scheduler.cjs +3 -3
- package/dist/scheduler.d.cts +1 -1
- package/dist/scheduler.d.ts +1 -1
- package/dist/scheduler.js +1 -1
- package/dist/stretcher.cjs +3 -3
- package/dist/stretcher.d.cts +4 -4
- package/dist/stretcher.d.ts +4 -4
- package/dist/stretcher.js +2 -2
- package/dist/synth.cjs +4 -4
- package/dist/synth.js +1 -1
- package/dist/{types-DUrbEbPl.d.cts → types-BYC6m7Q0.d.cts} +6 -6
- package/dist/{types-DUrbEbPl.d.ts → types-BYC6m7Q0.d.ts} +6 -6
- package/dist/waveform.cjs +4 -4
- package/dist/waveform.d.cts +1 -1
- package/dist/waveform.d.ts +1 -1
- package/dist/waveform.js +1 -1
- package/package.json +14 -6
- package/dist/adapters.cjs.map +0 -1
- package/dist/adapters.js.map +0 -1
- package/dist/buffer.cjs.map +0 -1
- package/dist/buffer.js.map +0 -1
- package/dist/chunk-2DL7CAEP.js.map +0 -1
- package/dist/chunk-2FFORBOP.js.map +0 -1
- package/dist/chunk-37CPPRLV.js.map +0 -1
- package/dist/chunk-4LNVRSTM.cjs.map +0 -1
- package/dist/chunk-5J7S6QV3.cjs.map +0 -1
- package/dist/chunk-6UTN73HG.cjs.map +0 -1
- package/dist/chunk-7S5KWTZ6.cjs.map +0 -1
- package/dist/chunk-C2ASIYN5.js.map +0 -1
- package/dist/chunk-CJJC6ASU.js.map +0 -1
- package/dist/chunk-CPAT75WD.cjs.map +0 -1
- package/dist/chunk-CRODJ4KS.js.map +0 -1
- package/dist/chunk-CTUCTTIE.cjs.map +0 -1
- package/dist/chunk-D5CD5KQZ.cjs.map +0 -1
- package/dist/chunk-GYH2JSCY.js.map +0 -1
- package/dist/chunk-IMNRPYBM.js.map +0 -1
- package/dist/chunk-LETS7FKB.js.map +0 -1
- package/dist/chunk-M5PDY5EZ.cjs.map +0 -1
- package/dist/chunk-QFFQQMU4.cjs.map +0 -1
- package/dist/chunk-QWNV2BZ5.cjs.map +0 -1
- package/dist/chunk-SIMLANWE.cjs.map +0 -1
- package/dist/chunk-T74FBKTY.js.map +0 -1
- package/dist/chunk-VKT7YCWK.js.map +0 -1
- package/dist/chunk-X4IFO7U7.js.map +0 -1
- package/dist/chunk-XZBMBZA3.cjs +0 -148
- package/dist/chunk-XZBMBZA3.cjs.map +0 -1
- package/dist/context.cjs.map +0 -1
- package/dist/context.js.map +0 -1
- package/dist/emitter.cjs.map +0 -1
- package/dist/emitter.js.map +0 -1
- package/dist/engine-QUMYW73L.cjs +0 -13
- package/dist/engine-QUMYW73L.cjs.map +0 -1
- package/dist/engine-TYI7OX7O.js +0 -4
- package/dist/engine-TYI7OX7O.js.map +0 -1
- package/dist/fade.cjs.map +0 -1
- package/dist/fade.js.map +0 -1
- package/dist/index.cjs.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/nodes.cjs.map +0 -1
- package/dist/nodes.js.map +0 -1
- package/dist/play.cjs.map +0 -1
- package/dist/play.js.map +0 -1
- package/dist/player.cjs.map +0 -1
- package/dist/player.js.map +0 -1
- package/dist/scheduler.cjs.map +0 -1
- package/dist/scheduler.js.map +0 -1
- package/dist/stretcher.cjs.map +0 -1
- package/dist/stretcher.js.map +0 -1
- package/dist/synth.cjs.map +0 -1
- package/dist/synth.js.map +0 -1
- package/dist/waveform.cjs.map +0 -1
- package/dist/waveform.js.map +0 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createEmitter } from './chunk-
|
|
1
|
+
import { createEmitter } from './chunk-FY273Z3I.js';
|
|
2
2
|
|
|
3
3
|
// src/stretcher/constants.ts
|
|
4
4
|
var CHUNK_DURATION_SEC = 8;
|
|
@@ -22,7 +22,300 @@ var WORKER_POOL_SIZE = 2;
|
|
|
22
22
|
var MAX_WORKER_CRASHES = 3;
|
|
23
23
|
var MAX_CHUNK_RETRIES = 3;
|
|
24
24
|
var LOOKAHEAD_INTERVAL_MS = 200;
|
|
25
|
-
var LOOKAHEAD_THRESHOLD_SEC =
|
|
25
|
+
var LOOKAHEAD_THRESHOLD_SEC = 3;
|
|
26
|
+
var PROACTIVE_SCHEDULE_THRESHOLD_SEC = 5;
|
|
27
|
+
|
|
28
|
+
// src/stretcher/buffer-monitor.ts
|
|
29
|
+
function createBufferMonitor(options) {
|
|
30
|
+
const healthySec = BUFFER_HEALTHY_SEC;
|
|
31
|
+
const lowSec = BUFFER_LOW_SEC;
|
|
32
|
+
const criticalSec = BUFFER_CRITICAL_SEC;
|
|
33
|
+
const resumeSec = BUFFER_RESUME_SEC;
|
|
34
|
+
const chunkDurSec = CHUNK_DURATION_SEC;
|
|
35
|
+
function getAheadSeconds(currentChunkIndex, chunks) {
|
|
36
|
+
let aheadSec = 0;
|
|
37
|
+
for (let i = currentChunkIndex; i < chunks.length; i++) {
|
|
38
|
+
const chunk = chunks[i];
|
|
39
|
+
if (!chunk || chunk.state !== "ready") break;
|
|
40
|
+
aheadSec += chunkDurSec;
|
|
41
|
+
}
|
|
42
|
+
return aheadSec;
|
|
43
|
+
}
|
|
44
|
+
function getHealth(currentChunkIndex, chunks) {
|
|
45
|
+
const ahead = getAheadSeconds(currentChunkIndex, chunks);
|
|
46
|
+
if (ahead >= healthySec) return "healthy";
|
|
47
|
+
if (ahead >= lowSec) return "low";
|
|
48
|
+
if (ahead >= criticalSec) return "critical";
|
|
49
|
+
return "empty";
|
|
50
|
+
}
|
|
51
|
+
function shouldEnterBuffering(currentChunkIndex, chunks) {
|
|
52
|
+
const ahead = getAheadSeconds(currentChunkIndex, chunks);
|
|
53
|
+
if (ahead >= criticalSec) return false;
|
|
54
|
+
const nextChunk = chunks[currentChunkIndex + 1];
|
|
55
|
+
if (nextChunk && nextChunk.state === "ready") return false;
|
|
56
|
+
const currentChunk = chunks[currentChunkIndex];
|
|
57
|
+
if (!currentChunk || currentChunk.state !== "ready") return true;
|
|
58
|
+
return ahead < criticalSec;
|
|
59
|
+
}
|
|
60
|
+
function shouldExitBuffering(currentChunkIndex, chunks) {
|
|
61
|
+
const currentChunk = chunks[currentChunkIndex];
|
|
62
|
+
if (!currentChunk || currentChunk.state !== "ready") return false;
|
|
63
|
+
const ahead = getAheadSeconds(currentChunkIndex, chunks);
|
|
64
|
+
if (ahead >= resumeSec) return true;
|
|
65
|
+
const nextChunk = chunks[currentChunkIndex + 1];
|
|
66
|
+
if (nextChunk && nextChunk.state === "ready") return true;
|
|
67
|
+
const allReady = chunks.every((c) => c.state === "ready" || c.state === "skipped");
|
|
68
|
+
if (allReady) return true;
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
getHealth,
|
|
73
|
+
getAheadSeconds,
|
|
74
|
+
shouldEnterBuffering,
|
|
75
|
+
shouldExitBuffering
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// src/stretcher/transition-timing.ts
|
|
80
|
+
var TRANSITION_MARGIN_MS = 50;
|
|
81
|
+
function calcTransitionDelay(startTime, currentTime) {
|
|
82
|
+
return Math.max(0, (startTime - currentTime) * 1e3 + TRANSITION_MARGIN_MS);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// src/stretcher/chunk-player.ts
|
|
86
|
+
var CURVE_LENGTH = 256;
|
|
87
|
+
function createEqualPowerCurve(fadeIn) {
|
|
88
|
+
const curve = new Float32Array(CURVE_LENGTH);
|
|
89
|
+
for (let i = 0; i < CURVE_LENGTH; i++) {
|
|
90
|
+
const t = i / (CURVE_LENGTH - 1);
|
|
91
|
+
curve[i] = fadeIn ? Math.sin(t * Math.PI / 2) : Math.cos(t * Math.PI / 2);
|
|
92
|
+
}
|
|
93
|
+
return curve;
|
|
94
|
+
}
|
|
95
|
+
var fadeInCurve = createEqualPowerCurve(true);
|
|
96
|
+
var fadeOutCurve = createEqualPowerCurve(false);
|
|
97
|
+
function createChunkPlayer(ctx, options) {
|
|
98
|
+
const destination = options.destination ?? ctx.destination;
|
|
99
|
+
const through = options.through ?? [];
|
|
100
|
+
const crossfadeSec = options.crossfadeSec ?? CROSSFADE_SEC;
|
|
101
|
+
let currentSource = null;
|
|
102
|
+
let nextSource = null;
|
|
103
|
+
let currentGain = null;
|
|
104
|
+
let nextGain = null;
|
|
105
|
+
let playStartCtxTime = 0;
|
|
106
|
+
let playStartOffset = 0;
|
|
107
|
+
let currentChunkDuration = 0;
|
|
108
|
+
let nextStartCtxTime = 0;
|
|
109
|
+
let paused = false;
|
|
110
|
+
let pausedPosition = 0;
|
|
111
|
+
let stopped = true;
|
|
112
|
+
let lookaheadTimer = null;
|
|
113
|
+
let transitionTimerId = null;
|
|
114
|
+
let onChunkEnded = null;
|
|
115
|
+
let onNeedNext = null;
|
|
116
|
+
let onTransition = null;
|
|
117
|
+
let disposed = false;
|
|
118
|
+
function connectToDestination(node) {
|
|
119
|
+
if (through.length > 0) {
|
|
120
|
+
node.connect(through[0]);
|
|
121
|
+
for (let i = 0; i < through.length - 1; i++) {
|
|
122
|
+
through[i].connect(through[i + 1]);
|
|
123
|
+
}
|
|
124
|
+
through[through.length - 1].connect(destination);
|
|
125
|
+
} else {
|
|
126
|
+
node.connect(destination);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
function createSourceFromBuffer(buffer, gain) {
|
|
130
|
+
const src = ctx.createBufferSource();
|
|
131
|
+
src.buffer = buffer;
|
|
132
|
+
src.connect(gain);
|
|
133
|
+
connectToDestination(gain);
|
|
134
|
+
return src;
|
|
135
|
+
}
|
|
136
|
+
function stopCurrentSource() {
|
|
137
|
+
if (currentSource) {
|
|
138
|
+
currentSource.onended = null;
|
|
139
|
+
try {
|
|
140
|
+
currentSource.stop();
|
|
141
|
+
} catch {
|
|
142
|
+
}
|
|
143
|
+
currentSource.disconnect();
|
|
144
|
+
currentSource = null;
|
|
145
|
+
}
|
|
146
|
+
if (currentGain) {
|
|
147
|
+
currentGain.disconnect();
|
|
148
|
+
currentGain = null;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
function stopNextSource() {
|
|
152
|
+
if (nextSource) {
|
|
153
|
+
nextSource.onended = null;
|
|
154
|
+
try {
|
|
155
|
+
nextSource.stop();
|
|
156
|
+
} catch {
|
|
157
|
+
}
|
|
158
|
+
nextSource.disconnect();
|
|
159
|
+
nextSource = null;
|
|
160
|
+
}
|
|
161
|
+
if (nextGain) {
|
|
162
|
+
nextGain.disconnect();
|
|
163
|
+
nextGain = null;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
function startLookahead() {
|
|
167
|
+
if (lookaheadTimer !== null) return;
|
|
168
|
+
lookaheadTimer = setInterval(() => {
|
|
169
|
+
if (paused || stopped || disposed) return;
|
|
170
|
+
const pos = getElapsedInChunk();
|
|
171
|
+
const remaining = currentChunkDuration - pos;
|
|
172
|
+
if (remaining <= LOOKAHEAD_THRESHOLD_SEC && !nextSource) {
|
|
173
|
+
onNeedNext?.();
|
|
174
|
+
}
|
|
175
|
+
}, LOOKAHEAD_INTERVAL_MS);
|
|
176
|
+
}
|
|
177
|
+
function stopLookahead() {
|
|
178
|
+
if (lookaheadTimer !== null) {
|
|
179
|
+
clearInterval(lookaheadTimer);
|
|
180
|
+
lookaheadTimer = null;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
function cancelTransition() {
|
|
184
|
+
if (transitionTimerId !== null) {
|
|
185
|
+
clearTimeout(transitionTimerId);
|
|
186
|
+
transitionTimerId = null;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
function doTransition(buffer, startCtxTime) {
|
|
190
|
+
stopCurrentSource();
|
|
191
|
+
currentSource = nextSource;
|
|
192
|
+
currentGain = nextGain;
|
|
193
|
+
nextSource = null;
|
|
194
|
+
nextGain = null;
|
|
195
|
+
currentChunkDuration = buffer.duration;
|
|
196
|
+
playStartOffset = 0;
|
|
197
|
+
playStartCtxTime = startCtxTime;
|
|
198
|
+
if (currentSource) {
|
|
199
|
+
currentSource.onended = handleCurrentSourceEnded;
|
|
200
|
+
}
|
|
201
|
+
onTransition?.();
|
|
202
|
+
}
|
|
203
|
+
function handleCurrentSourceEnded() {
|
|
204
|
+
if (disposed || paused || stopped) return;
|
|
205
|
+
if (nextSource) {
|
|
206
|
+
const buf = nextSource.buffer;
|
|
207
|
+
if (!buf) {
|
|
208
|
+
onChunkEnded?.();
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
cancelTransition();
|
|
212
|
+
doTransition(buf, nextStartCtxTime);
|
|
213
|
+
} else {
|
|
214
|
+
onChunkEnded?.();
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
function getElapsedInChunk() {
|
|
218
|
+
if (paused) return pausedPosition;
|
|
219
|
+
if (stopped) return 0;
|
|
220
|
+
return ctx.currentTime - playStartCtxTime + playStartOffset;
|
|
221
|
+
}
|
|
222
|
+
function playChunk(buffer, _startTime, offsetInChunk = 0, skipFadeIn = false) {
|
|
223
|
+
cancelTransition();
|
|
224
|
+
stopCurrentSource();
|
|
225
|
+
stopNextSource();
|
|
226
|
+
currentGain = ctx.createGain();
|
|
227
|
+
currentSource = createSourceFromBuffer(buffer, currentGain);
|
|
228
|
+
currentChunkDuration = buffer.duration;
|
|
229
|
+
playStartOffset = offsetInChunk;
|
|
230
|
+
playStartCtxTime = ctx.currentTime;
|
|
231
|
+
paused = false;
|
|
232
|
+
stopped = false;
|
|
233
|
+
currentSource.onended = handleCurrentSourceEnded;
|
|
234
|
+
currentSource.start(0, offsetInChunk);
|
|
235
|
+
if (crossfadeSec > 0 && !skipFadeIn) {
|
|
236
|
+
currentGain.gain.setValueCurveAtTime(fadeInCurve, ctx.currentTime, crossfadeSec);
|
|
237
|
+
}
|
|
238
|
+
startLookahead();
|
|
239
|
+
}
|
|
240
|
+
function scheduleNext(buffer, startTime) {
|
|
241
|
+
if (disposed) return;
|
|
242
|
+
stopNextSource();
|
|
243
|
+
nextGain = ctx.createGain();
|
|
244
|
+
nextSource = createSourceFromBuffer(buffer, nextGain);
|
|
245
|
+
nextSource.onended = handleCurrentSourceEnded;
|
|
246
|
+
nextStartCtxTime = startTime - crossfadeSec;
|
|
247
|
+
nextSource.start(nextStartCtxTime);
|
|
248
|
+
if (crossfadeSec > 0 && currentGain) {
|
|
249
|
+
currentGain.gain.setValueCurveAtTime(fadeOutCurve, nextStartCtxTime, crossfadeSec);
|
|
250
|
+
nextGain.gain.setValueCurveAtTime(fadeInCurve, nextStartCtxTime, crossfadeSec);
|
|
251
|
+
}
|
|
252
|
+
const transitionDelay = calcTransitionDelay(startTime, ctx.currentTime);
|
|
253
|
+
cancelTransition();
|
|
254
|
+
transitionTimerId = setTimeout(() => {
|
|
255
|
+
transitionTimerId = null;
|
|
256
|
+
if (disposed || !nextSource) return;
|
|
257
|
+
doTransition(buffer, nextStartCtxTime);
|
|
258
|
+
}, transitionDelay);
|
|
259
|
+
}
|
|
260
|
+
function handleSeek(buffer, offsetInChunk) {
|
|
261
|
+
playChunk(buffer, 0, offsetInChunk);
|
|
262
|
+
}
|
|
263
|
+
function pause() {
|
|
264
|
+
if (paused || stopped || disposed) return;
|
|
265
|
+
pausedPosition = getElapsedInChunk();
|
|
266
|
+
paused = true;
|
|
267
|
+
cancelTransition();
|
|
268
|
+
stopCurrentSource();
|
|
269
|
+
stopNextSource();
|
|
270
|
+
stopLookahead();
|
|
271
|
+
}
|
|
272
|
+
function resume() {
|
|
273
|
+
if (!paused || disposed) return;
|
|
274
|
+
}
|
|
275
|
+
function stop() {
|
|
276
|
+
if (stopped || disposed) return;
|
|
277
|
+
stopped = true;
|
|
278
|
+
paused = false;
|
|
279
|
+
pausedPosition = 0;
|
|
280
|
+
cancelTransition();
|
|
281
|
+
stopCurrentSource();
|
|
282
|
+
stopNextSource();
|
|
283
|
+
stopLookahead();
|
|
284
|
+
}
|
|
285
|
+
function getCurrentPosition() {
|
|
286
|
+
return getElapsedInChunk();
|
|
287
|
+
}
|
|
288
|
+
function hasNextScheduled() {
|
|
289
|
+
return nextSource !== null;
|
|
290
|
+
}
|
|
291
|
+
return {
|
|
292
|
+
playChunk,
|
|
293
|
+
scheduleNext,
|
|
294
|
+
hasNextScheduled,
|
|
295
|
+
handleSeek,
|
|
296
|
+
pause,
|
|
297
|
+
resume,
|
|
298
|
+
stop,
|
|
299
|
+
getCurrentPosition,
|
|
300
|
+
setOnChunkEnded(callback) {
|
|
301
|
+
onChunkEnded = callback;
|
|
302
|
+
},
|
|
303
|
+
setOnNeedNext(callback) {
|
|
304
|
+
onNeedNext = callback;
|
|
305
|
+
},
|
|
306
|
+
setOnTransition(callback) {
|
|
307
|
+
onTransition = callback;
|
|
308
|
+
},
|
|
309
|
+
dispose() {
|
|
310
|
+
if (disposed) return;
|
|
311
|
+
disposed = true;
|
|
312
|
+
cancelTransition();
|
|
313
|
+
stopCurrentSource();
|
|
314
|
+
stopNextSource();
|
|
315
|
+
stopLookahead();
|
|
316
|
+
}
|
|
317
|
+
};
|
|
318
|
+
}
|
|
26
319
|
|
|
27
320
|
// src/stretcher/chunk-splitter.ts
|
|
28
321
|
function splitIntoChunks(totalSamples, sampleRate, chunkDurationSec = CHUNK_DURATION_SEC, overlapSec = OVERLAP_SEC) {
|
|
@@ -84,9 +377,7 @@ function extractChunkData(buffer, chunk) {
|
|
|
84
377
|
for (let ch = 0; ch < buffer.numberOfChannels; ch++) {
|
|
85
378
|
const fullChannel = buffer.getChannelData(ch);
|
|
86
379
|
const chunkData = new Float32Array(length);
|
|
87
|
-
chunkData.set(
|
|
88
|
-
fullChannel.subarray(chunk.inputStartSample, chunk.inputEndSample)
|
|
89
|
-
);
|
|
380
|
+
chunkData.set(fullChannel.subarray(chunk.inputStartSample, chunk.inputEndSample));
|
|
90
381
|
channels.push(chunkData);
|
|
91
382
|
}
|
|
92
383
|
return channels;
|
|
@@ -107,370 +398,273 @@ function getChunkIndexForTime(chunks, timeSeconds, sampleRate) {
|
|
|
107
398
|
return getChunkIndexForSample(chunks, sample);
|
|
108
399
|
}
|
|
109
400
|
|
|
110
|
-
// src/stretcher/
|
|
111
|
-
function
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
var HOP_SIZE = ${WSOLA_HOP_SIZE};
|
|
116
|
-
var TOLERANCE = ${WSOLA_TOLERANCE};
|
|
117
|
-
|
|
118
|
-
function createHannWindow(size) {
|
|
119
|
-
var w = new Float32Array(size);
|
|
120
|
-
for (var i = 0; i < size; i++) {
|
|
121
|
-
w[i] = 0.5 * (1 - Math.cos((2 * Math.PI * i) / (size - 1)));
|
|
122
|
-
}
|
|
123
|
-
return w;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
function findBestOffset(ref, search, overlapSize, maxOffset) {
|
|
127
|
-
var bestOffset = 0;
|
|
128
|
-
var bestCorr = -Infinity;
|
|
129
|
-
var searchLen = search.length;
|
|
130
|
-
var refLen = ref.length;
|
|
131
|
-
var len = Math.min(overlapSize, refLen);
|
|
132
|
-
|
|
133
|
-
for (var offset = 0; offset <= maxOffset; offset++) {
|
|
134
|
-
if (offset + len > searchLen) break;
|
|
135
|
-
var corr = 0;
|
|
136
|
-
var normRef = 0;
|
|
137
|
-
var normSearch = 0;
|
|
138
|
-
for (var i = 0; i < len; i++) {
|
|
139
|
-
var r = ref[i];
|
|
140
|
-
var s = search[offset + i];
|
|
141
|
-
corr += r * s;
|
|
142
|
-
normRef += r * r;
|
|
143
|
-
normSearch += s * s;
|
|
144
|
-
}
|
|
145
|
-
var denom = Math.sqrt(normRef * normSearch);
|
|
146
|
-
var ncc = denom > 1e-10 ? corr / denom : 0;
|
|
147
|
-
if (ncc > bestCorr) {
|
|
148
|
-
bestCorr = ncc;
|
|
149
|
-
bestOffset = offset;
|
|
150
|
-
}
|
|
401
|
+
// src/stretcher/priority-queue.ts
|
|
402
|
+
function createPriorityQueue(compareFn) {
|
|
403
|
+
const heap = [];
|
|
404
|
+
function parent(i) {
|
|
405
|
+
return Math.floor((i - 1) / 2);
|
|
151
406
|
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
function wsolaTimeStretch(channels, tempo, sampleRate) {
|
|
156
|
-
if (channels.length === 0) {
|
|
157
|
-
return { output: [], length: 0 };
|
|
407
|
+
function left(i) {
|
|
408
|
+
return 2 * i + 1;
|
|
158
409
|
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
if (inputLength === 0) {
|
|
162
|
-
return { output: channels.map(function() { return new Float32Array(0); }), length: 0 };
|
|
410
|
+
function right(i) {
|
|
411
|
+
return 2 * i + 2;
|
|
163
412
|
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
if (numFrames <= 0) {
|
|
170
|
-
return {
|
|
171
|
-
output: channels.map(function(ch) { return new Float32Array(ch); }),
|
|
172
|
-
length: inputLength
|
|
173
|
-
};
|
|
413
|
+
function swap(i, j) {
|
|
414
|
+
const tmp = heap[i];
|
|
415
|
+
heap[i] = heap[j];
|
|
416
|
+
heap[j] = tmp;
|
|
174
417
|
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
var prevOutputFrame = channels.map(function() {
|
|
184
|
-
return new Float32Array(FRAME_SIZE);
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
var inputPos = 0;
|
|
188
|
-
var outputPos = 0;
|
|
189
|
-
var actualOutputLength = 0;
|
|
190
|
-
|
|
191
|
-
for (var frame = 0; frame < numFrames; frame++) {
|
|
192
|
-
if (cancelled) return null;
|
|
193
|
-
if (inputPos + FRAME_SIZE > inputLength) break;
|
|
194
|
-
|
|
195
|
-
var actualInputPos = inputPos;
|
|
196
|
-
|
|
197
|
-
if (frame > 0 && TOLERANCE > 0) {
|
|
198
|
-
var searchStart = Math.max(0, inputPos - TOLERANCE);
|
|
199
|
-
var searchEnd = Math.min(inputLength - FRAME_SIZE, inputPos + TOLERANCE);
|
|
200
|
-
var searchRange = searchEnd - searchStart;
|
|
201
|
-
|
|
202
|
-
if (searchRange > 0) {
|
|
203
|
-
var refChannel = prevOutputFrame[0];
|
|
204
|
-
var inputChannel = channels[0];
|
|
205
|
-
|
|
206
|
-
var overlapStart = FRAME_SIZE - synthesisHop;
|
|
207
|
-
var overlapSize = Math.min(synthesisHop, FRAME_SIZE - overlapStart);
|
|
208
|
-
|
|
209
|
-
var refSlice = refChannel.subarray(overlapStart, overlapStart + overlapSize);
|
|
210
|
-
var searchSlice = inputChannel.subarray(searchStart, searchEnd + overlapSize);
|
|
211
|
-
|
|
212
|
-
var bestOffset = findBestOffset(
|
|
213
|
-
refSlice, searchSlice, overlapSize,
|
|
214
|
-
Math.min(searchRange, searchSlice.length - overlapSize)
|
|
215
|
-
);
|
|
216
|
-
|
|
217
|
-
actualInputPos = searchStart + bestOffset;
|
|
418
|
+
function siftUp(i) {
|
|
419
|
+
while (i > 0) {
|
|
420
|
+
const p = parent(i);
|
|
421
|
+
if (compareFn(heap[i], heap[p]) < 0) {
|
|
422
|
+
swap(i, p);
|
|
423
|
+
i = p;
|
|
424
|
+
} else {
|
|
425
|
+
break;
|
|
218
426
|
}
|
|
219
427
|
}
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
428
|
+
}
|
|
429
|
+
function siftDown(i) {
|
|
430
|
+
const n = heap.length;
|
|
431
|
+
while (true) {
|
|
432
|
+
let smallest = i;
|
|
433
|
+
const l = left(i);
|
|
434
|
+
const r = right(i);
|
|
435
|
+
if (l < n && compareFn(heap[l], heap[smallest]) < 0) {
|
|
436
|
+
smallest = l;
|
|
437
|
+
}
|
|
438
|
+
if (r < n && compareFn(heap[r], heap[smallest]) < 0) {
|
|
439
|
+
smallest = r;
|
|
440
|
+
}
|
|
441
|
+
if (smallest !== i) {
|
|
442
|
+
swap(i, smallest);
|
|
443
|
+
i = smallest;
|
|
444
|
+
} else {
|
|
445
|
+
break;
|
|
233
446
|
}
|
|
234
447
|
}
|
|
235
|
-
|
|
236
|
-
for (var i = 0; i < FRAME_SIZE; i++) {
|
|
237
|
-
var outIdx = outputPos + i;
|
|
238
|
-
if (outIdx >= estimatedOutputLength) break;
|
|
239
|
-
normBuffer[outIdx] += windowFunc[i];
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
inputPos += analysisHop;
|
|
243
|
-
outputPos += synthesisHop;
|
|
244
|
-
actualOutputLength = Math.min(outputPos + FRAME_SIZE, estimatedOutputLength);
|
|
245
448
|
}
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
for (var i = 0; i < actualOutputLength; i++) {
|
|
250
|
-
var norm = normBuffer[i];
|
|
251
|
-
if (norm > 1e-8) {
|
|
252
|
-
output[i] /= norm;
|
|
253
|
-
}
|
|
449
|
+
function heapify() {
|
|
450
|
+
for (let i = Math.floor(heap.length / 2) - 1; i >= 0; i--) {
|
|
451
|
+
siftDown(i);
|
|
254
452
|
}
|
|
255
453
|
}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
if (
|
|
277
|
-
|
|
454
|
+
return {
|
|
455
|
+
enqueue(item) {
|
|
456
|
+
heap.push(item);
|
|
457
|
+
siftUp(heap.length - 1);
|
|
458
|
+
},
|
|
459
|
+
dequeue() {
|
|
460
|
+
if (heap.length === 0) return void 0;
|
|
461
|
+
const min = heap[0];
|
|
462
|
+
const last = heap.pop();
|
|
463
|
+
if (heap.length > 0) {
|
|
464
|
+
heap[0] = last;
|
|
465
|
+
siftDown(0);
|
|
466
|
+
}
|
|
467
|
+
return min;
|
|
468
|
+
},
|
|
469
|
+
peek() {
|
|
470
|
+
return heap[0];
|
|
471
|
+
},
|
|
472
|
+
remove(predicate) {
|
|
473
|
+
const idx = heap.findIndex(predicate);
|
|
474
|
+
if (idx === -1) return false;
|
|
475
|
+
if (idx === heap.length - 1) {
|
|
476
|
+
heap.pop();
|
|
278
477
|
} else {
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
);
|
|
478
|
+
heap[idx] = heap.pop();
|
|
479
|
+
siftDown(idx);
|
|
480
|
+
siftUp(idx);
|
|
283
481
|
}
|
|
284
|
-
|
|
285
|
-
|
|
482
|
+
return true;
|
|
483
|
+
},
|
|
484
|
+
rebuild() {
|
|
485
|
+
heapify();
|
|
486
|
+
},
|
|
487
|
+
clear() {
|
|
488
|
+
heap.length = 0;
|
|
489
|
+
},
|
|
490
|
+
size() {
|
|
491
|
+
return heap.length;
|
|
492
|
+
},
|
|
493
|
+
toArray() {
|
|
494
|
+
return [...heap];
|
|
286
495
|
}
|
|
287
|
-
}
|
|
288
|
-
};
|
|
289
|
-
`;
|
|
290
|
-
}
|
|
291
|
-
function createWorkerURL() {
|
|
292
|
-
const code = getWorkerCode();
|
|
293
|
-
const blob = new Blob([code], { type: "application/javascript" });
|
|
294
|
-
return URL.createObjectURL(blob);
|
|
295
|
-
}
|
|
296
|
-
function revokeWorkerURL(url) {
|
|
297
|
-
URL.revokeObjectURL(url);
|
|
496
|
+
};
|
|
298
497
|
}
|
|
299
498
|
|
|
300
|
-
// src/stretcher/
|
|
301
|
-
function
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
const
|
|
305
|
-
const
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
return workerURL;
|
|
311
|
-
}
|
|
312
|
-
function isAllDead() {
|
|
313
|
-
return slots.every((s) => s.worker === null);
|
|
499
|
+
// src/stretcher/conversion-scheduler.ts
|
|
500
|
+
function createConversionScheduler(chunks, workerManager, extractChunkData2, sampleRate, tempo, options, onChunkReady, onChunkFailed) {
|
|
501
|
+
const forwardWeight = options?.forwardWeight ?? PRIORITY_FORWARD_WEIGHT;
|
|
502
|
+
const backwardWeight = options?.backwardWeight ?? PRIORITY_BACKWARD_WEIGHT;
|
|
503
|
+
const cancelDistThreshold = options?.cancelDistanceThreshold ?? CANCEL_DISTANCE_THRESHOLD;
|
|
504
|
+
const keepAhead = options?.keepAheadChunks ?? Math.max(KEEP_AHEAD_CHUNKS, Math.ceil(KEEP_AHEAD_SECONDS / CHUNK_DURATION_SEC));
|
|
505
|
+
const keepBehind = options?.keepBehindChunks ?? Math.max(KEEP_BEHIND_CHUNKS, Math.ceil(KEEP_BEHIND_SECONDS / CHUNK_DURATION_SEC));
|
|
506
|
+
function isInActiveWindow(chunkIndex, playheadIndex) {
|
|
507
|
+
const dist = chunkIndex - playheadIndex;
|
|
508
|
+
return dist <= keepAhead && dist >= -keepBehind;
|
|
314
509
|
}
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
return;
|
|
510
|
+
let currentTempo = tempo;
|
|
511
|
+
let currentChunkIdx = 0;
|
|
512
|
+
let previousTempoCache = null;
|
|
513
|
+
let disposed = false;
|
|
514
|
+
const queue = createPriorityQueue((a, b) => a.priority - b.priority);
|
|
515
|
+
function calcPriority(chunkIndex, playheadIndex) {
|
|
516
|
+
const distance = chunkIndex - playheadIndex;
|
|
517
|
+
if (distance >= 0) {
|
|
518
|
+
return distance * forwardWeight;
|
|
324
519
|
}
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
520
|
+
return Math.abs(distance) * backwardWeight;
|
|
521
|
+
}
|
|
522
|
+
function updatePriorities(playheadIndex) {
|
|
523
|
+
currentChunkIdx = playheadIndex;
|
|
524
|
+
queue.clear();
|
|
525
|
+
for (const chunk of chunks) {
|
|
526
|
+
if (chunk.state === "pending" || chunk.state === "queued" || chunk.state === "failed") {
|
|
527
|
+
chunk.priority = calcPriority(chunk.index, playheadIndex);
|
|
528
|
+
chunk.state = "queued";
|
|
529
|
+
queue.enqueue(chunk);
|
|
530
|
+
} else if (chunk.state === "evicted" && isInActiveWindow(chunk.index, playheadIndex)) {
|
|
531
|
+
chunk.state = "queued";
|
|
532
|
+
chunk.retryCount = 0;
|
|
533
|
+
chunk.priority = calcPriority(chunk.index, playheadIndex);
|
|
534
|
+
queue.enqueue(chunk);
|
|
330
535
|
}
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
536
|
+
}
|
|
537
|
+
for (const chunk of chunks) {
|
|
538
|
+
if (chunk.state === "converting") {
|
|
539
|
+
const dist = Math.abs(chunk.index - playheadIndex);
|
|
540
|
+
if (dist > cancelDistThreshold) {
|
|
541
|
+
workerManager.cancelChunk(chunk.index);
|
|
542
|
+
}
|
|
336
543
|
}
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
if (failedChunkIndex !== null) {
|
|
346
|
-
onError({
|
|
347
|
-
type: "error",
|
|
348
|
-
chunkIndex: failedChunkIndex,
|
|
349
|
-
error: `Worker crashed: ${e.message}`
|
|
350
|
-
});
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
function dispatchNext() {
|
|
547
|
+
if (disposed) return;
|
|
548
|
+
while (workerManager.hasCapacity()) {
|
|
549
|
+
let nextChunk = queue.dequeue();
|
|
550
|
+
while (nextChunk && nextChunk.state === "ready") {
|
|
551
|
+
nextChunk = queue.dequeue();
|
|
351
552
|
}
|
|
352
|
-
if (
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
slot.worker.terminate();
|
|
356
|
-
slot.worker = null;
|
|
553
|
+
if (!nextChunk) return;
|
|
554
|
+
if (nextChunk.state !== "queued" && nextChunk.state !== "pending" && nextChunk.state !== "failed") {
|
|
555
|
+
continue;
|
|
357
556
|
}
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
557
|
+
nextChunk.state = "converting";
|
|
558
|
+
const data = extractChunkData2(nextChunk.index);
|
|
559
|
+
workerManager.postConvert(nextChunk.index, data, currentTempo, sampleRate);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
function handleResult(chunkIndex, outputData, outputLength) {
|
|
563
|
+
if (disposed) return;
|
|
564
|
+
const chunk = chunks[chunkIndex];
|
|
565
|
+
if (!chunk) return;
|
|
566
|
+
if (chunk.state !== "converting") {
|
|
567
|
+
dispatchNext();
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
chunk.state = "ready";
|
|
571
|
+
chunk.outputBuffer = outputData;
|
|
572
|
+
chunk.outputLength = outputLength;
|
|
573
|
+
onChunkReady?.(chunkIndex);
|
|
574
|
+
dispatchNext();
|
|
575
|
+
}
|
|
576
|
+
function handleError(chunkIndex, error) {
|
|
577
|
+
if (disposed) return;
|
|
578
|
+
const chunk = chunks[chunkIndex];
|
|
579
|
+
if (!chunk) return;
|
|
580
|
+
chunk.retryCount++;
|
|
581
|
+
if (chunk.retryCount < MAX_CHUNK_RETRIES) {
|
|
582
|
+
chunk.state = "queued";
|
|
583
|
+
chunk.priority = calcPriority(chunk.index, currentChunkIdx);
|
|
584
|
+
queue.enqueue(chunk);
|
|
585
|
+
} else {
|
|
586
|
+
chunk.state = "failed";
|
|
587
|
+
onChunkFailed?.(chunkIndex, error);
|
|
588
|
+
}
|
|
589
|
+
dispatchNext();
|
|
590
|
+
}
|
|
591
|
+
function handleSeek(newChunkIndex) {
|
|
592
|
+
for (const chunk of chunks) {
|
|
593
|
+
if (chunk.state === "converting") {
|
|
594
|
+
const dist = Math.abs(chunk.index - newChunkIndex);
|
|
595
|
+
if (dist > cancelDistThreshold) {
|
|
596
|
+
workerManager.cancelChunk(chunk.index);
|
|
368
597
|
}
|
|
369
598
|
}
|
|
370
|
-
}
|
|
371
|
-
|
|
599
|
+
}
|
|
600
|
+
updatePriorities(newChunkIndex);
|
|
601
|
+
dispatchNext();
|
|
372
602
|
}
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
603
|
+
function handleTempoChange(newTempo) {
|
|
604
|
+
previousTempoCache = {
|
|
605
|
+
tempo: currentTempo,
|
|
606
|
+
chunks: chunks.map((c) => {
|
|
607
|
+
if (isInActiveWindow(c.index, currentChunkIdx) && c.outputBuffer) {
|
|
608
|
+
return { outputBuffer: c.outputBuffer, outputLength: c.outputLength };
|
|
609
|
+
}
|
|
610
|
+
return { outputBuffer: null, outputLength: 0 };
|
|
611
|
+
})
|
|
379
612
|
};
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
613
|
+
currentTempo = newTempo;
|
|
614
|
+
workerManager.cancelCurrent();
|
|
615
|
+
for (const chunk of chunks) {
|
|
616
|
+
if (chunk.state === "evicted") continue;
|
|
617
|
+
if (isInActiveWindow(chunk.index, currentChunkIdx)) {
|
|
618
|
+
chunk.outputBuffer = null;
|
|
619
|
+
chunk.outputLength = 0;
|
|
620
|
+
chunk.state = "pending";
|
|
621
|
+
chunk.retryCount = 0;
|
|
622
|
+
} else {
|
|
623
|
+
chunk.outputBuffer = null;
|
|
624
|
+
chunk.outputLength = 0;
|
|
625
|
+
chunk.state = "evicted";
|
|
390
626
|
}
|
|
391
627
|
}
|
|
392
|
-
|
|
628
|
+
updatePriorities(currentChunkIdx);
|
|
629
|
+
dispatchNext();
|
|
393
630
|
}
|
|
394
|
-
function
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
631
|
+
function restorePreviousTempo() {
|
|
632
|
+
if (!previousTempoCache) return false;
|
|
633
|
+
currentTempo = previousTempoCache.tempo;
|
|
634
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
635
|
+
const cached = previousTempoCache.chunks[i];
|
|
636
|
+
const chunk = chunks[i];
|
|
637
|
+
if (chunk && cached?.outputBuffer) {
|
|
638
|
+
chunk.outputBuffer = cached.outputBuffer;
|
|
639
|
+
chunk.outputLength = cached.outputLength;
|
|
640
|
+
chunk.state = "ready";
|
|
398
641
|
}
|
|
399
642
|
}
|
|
400
|
-
|
|
643
|
+
previousTempoCache = null;
|
|
644
|
+
workerManager.cancelCurrent();
|
|
645
|
+
updatePriorities(currentChunkIdx);
|
|
646
|
+
dispatchNext();
|
|
647
|
+
return true;
|
|
648
|
+
}
|
|
649
|
+
function start(playheadIndex) {
|
|
650
|
+
updatePriorities(playheadIndex);
|
|
651
|
+
dispatchNext();
|
|
401
652
|
}
|
|
402
653
|
return {
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
transferables
|
|
414
|
-
);
|
|
415
|
-
},
|
|
416
|
-
cancelCurrent() {
|
|
417
|
-
if (terminated) return;
|
|
418
|
-
for (const slot of slots) {
|
|
419
|
-
if (slot.busy && slot.worker && slot.currentChunkIndex !== null) {
|
|
420
|
-
slot.worker.postMessage({ type: "cancel", chunkIndex: slot.currentChunkIndex });
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
},
|
|
424
|
-
cancelChunk(chunkIndex) {
|
|
425
|
-
if (terminated) return;
|
|
426
|
-
const slot = findSlotByChunk(chunkIndex);
|
|
427
|
-
if (slot && slot.worker) {
|
|
428
|
-
slot.worker.postMessage({ type: "cancel", chunkIndex });
|
|
429
|
-
}
|
|
430
|
-
},
|
|
431
|
-
isBusy() {
|
|
432
|
-
return slots.every((s) => s.busy || !s.worker);
|
|
433
|
-
},
|
|
434
|
-
hasCapacity() {
|
|
435
|
-
return findFreeSlot() !== null;
|
|
436
|
-
},
|
|
437
|
-
getCurrentChunkIndex() {
|
|
438
|
-
for (const slot of slots) {
|
|
439
|
-
if (slot.busy && slot.currentChunkIndex !== null) {
|
|
440
|
-
return slot.currentChunkIndex;
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
return null;
|
|
444
|
-
},
|
|
445
|
-
getLastPostTime() {
|
|
446
|
-
let latest = null;
|
|
447
|
-
for (const t of postTimes.values()) {
|
|
448
|
-
if (latest === null || t > latest) {
|
|
449
|
-
latest = t;
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
return latest;
|
|
453
|
-
},
|
|
454
|
-
getPostTimeForChunk(chunkIndex) {
|
|
455
|
-
return postTimes.get(chunkIndex) ?? null;
|
|
654
|
+
start,
|
|
655
|
+
updatePriorities,
|
|
656
|
+
handleSeek,
|
|
657
|
+
handleTempoChange,
|
|
658
|
+
restorePreviousTempo,
|
|
659
|
+
dispatchNext,
|
|
660
|
+
getChunks: () => chunks,
|
|
661
|
+
dispose() {
|
|
662
|
+
disposed = true;
|
|
663
|
+
queue.clear();
|
|
456
664
|
},
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
for (const slot of slots) {
|
|
461
|
-
if (slot.worker) {
|
|
462
|
-
slot.worker.onmessage = null;
|
|
463
|
-
slot.worker.onerror = null;
|
|
464
|
-
slot.worker.terminate();
|
|
465
|
-
slot.worker = null;
|
|
466
|
-
}
|
|
467
|
-
}
|
|
468
|
-
if (workerURL) {
|
|
469
|
-
revokeWorkerURL(workerURL);
|
|
470
|
-
workerURL = null;
|
|
471
|
-
}
|
|
472
|
-
postTimes.clear();
|
|
473
|
-
}
|
|
665
|
+
// Expose internal handlers for the engine to wire up
|
|
666
|
+
_handleResult: handleResult,
|
|
667
|
+
_handleError: handleError
|
|
474
668
|
};
|
|
475
669
|
}
|
|
476
670
|
|
|
@@ -517,6 +711,13 @@ function wsolaTimeStretch(channels, tempo, _sampleRate, frameSize = WSOLA_FRAME_
|
|
|
517
711
|
if (inputLength === 0) {
|
|
518
712
|
return { output: channels.map(() => new Float32Array(0)), length: 0 };
|
|
519
713
|
}
|
|
714
|
+
const TEMPO_IDENTITY_EPSILON = 1e-3;
|
|
715
|
+
if (Math.abs(tempo - 1) < TEMPO_IDENTITY_EPSILON) {
|
|
716
|
+
return {
|
|
717
|
+
output: channels.map((ch) => new Float32Array(ch)),
|
|
718
|
+
length: inputLength
|
|
719
|
+
};
|
|
720
|
+
}
|
|
520
721
|
const synthesisHop = hopSize;
|
|
521
722
|
const analysisHop = Math.round(hopSize * tempo);
|
|
522
723
|
const numFrames = Math.floor((inputLength - frameSize) / analysisHop) + 1;
|
|
@@ -527,9 +728,7 @@ function wsolaTimeStretch(channels, tempo, _sampleRate, frameSize = WSOLA_FRAME_
|
|
|
527
728
|
};
|
|
528
729
|
}
|
|
529
730
|
const estimatedOutputLength = (numFrames - 1) * synthesisHop + frameSize;
|
|
530
|
-
const outputChannels = channels.map(
|
|
531
|
-
() => new Float32Array(estimatedOutputLength)
|
|
532
|
-
);
|
|
731
|
+
const outputChannels = channels.map(() => new Float32Array(estimatedOutputLength));
|
|
533
732
|
const windowFunc = createHannWindow(frameSize);
|
|
534
733
|
const normBuffer = new Float32Array(estimatedOutputLength);
|
|
535
734
|
const prevOutputFrame = channels.map(() => new Float32Array(frameSize));
|
|
@@ -548,14 +747,8 @@ function wsolaTimeStretch(channels, tempo, _sampleRate, frameSize = WSOLA_FRAME_
|
|
|
548
747
|
const inputChannel = channels[0];
|
|
549
748
|
const overlapStart = frameSize - synthesisHop;
|
|
550
749
|
const overlapSize = Math.min(synthesisHop, frameSize - overlapStart);
|
|
551
|
-
const refSlice = refChannel.subarray(
|
|
552
|
-
|
|
553
|
-
overlapStart + overlapSize
|
|
554
|
-
);
|
|
555
|
-
const searchSlice = inputChannel.subarray(
|
|
556
|
-
searchStart,
|
|
557
|
-
searchEnd + overlapSize
|
|
558
|
-
);
|
|
750
|
+
const refSlice = refChannel.subarray(overlapStart, overlapStart + overlapSize);
|
|
751
|
+
const searchSlice = inputChannel.subarray(searchStart, searchEnd + overlapSize);
|
|
559
752
|
const bestOffset = findBestOffset(
|
|
560
753
|
refSlice,
|
|
561
754
|
searchSlice,
|
|
@@ -576,7 +769,7 @@ function wsolaTimeStretch(channels, tempo, _sampleRate, frameSize = WSOLA_FRAME_
|
|
|
576
769
|
if (outIdx >= estimatedOutputLength) break;
|
|
577
770
|
const sample = input[inIdx];
|
|
578
771
|
output[outIdx] += sample * windowFunc[i];
|
|
579
|
-
prevFrame[i] = sample;
|
|
772
|
+
prevFrame[i] = sample * windowFunc[i];
|
|
580
773
|
}
|
|
581
774
|
}
|
|
582
775
|
for (let i = 0; i < frameSize; i++) {
|
|
@@ -597,9 +790,7 @@ function wsolaTimeStretch(channels, tempo, _sampleRate, frameSize = WSOLA_FRAME_
|
|
|
597
790
|
}
|
|
598
791
|
}
|
|
599
792
|
}
|
|
600
|
-
const trimmedOutput = outputChannels.map(
|
|
601
|
-
(ch) => ch.subarray(0, actualOutputLength)
|
|
602
|
-
);
|
|
793
|
+
const trimmedOutput = outputChannels.map((ch) => ch.subarray(0, actualOutputLength));
|
|
603
794
|
return { output: trimmedOutput, length: actualOutputLength };
|
|
604
795
|
}
|
|
605
796
|
|
|
@@ -608,7 +799,7 @@ function createMainThreadProcessor(onResult, onError) {
|
|
|
608
799
|
let terminated = false;
|
|
609
800
|
const postTimes = /* @__PURE__ */ new Map();
|
|
610
801
|
let currentChunkIndex = null;
|
|
611
|
-
|
|
802
|
+
const cancelledChunks = /* @__PURE__ */ new Set();
|
|
612
803
|
let busy = false;
|
|
613
804
|
return {
|
|
614
805
|
postConvert(chunkIndex, inputData, tempo, sampleRate) {
|
|
@@ -681,569 +872,412 @@ function createMainThreadProcessor(onResult, onError) {
|
|
|
681
872
|
}
|
|
682
873
|
return latest;
|
|
683
874
|
},
|
|
684
|
-
getPostTimeForChunk(chunkIndex) {
|
|
685
|
-
return postTimes.get(chunkIndex) ?? null;
|
|
686
|
-
},
|
|
687
|
-
terminate() {
|
|
688
|
-
if (terminated) return;
|
|
689
|
-
terminated = true;
|
|
690
|
-
cancelledChunks.clear();
|
|
691
|
-
postTimes.clear();
|
|
692
|
-
}
|
|
693
|
-
};
|
|
694
|
-
}
|
|
695
|
-
|
|
696
|
-
// src/stretcher/priority-queue.ts
|
|
697
|
-
function createPriorityQueue(compareFn) {
|
|
698
|
-
const heap = [];
|
|
699
|
-
function parent(i) {
|
|
700
|
-
return Math.floor((i - 1) / 2);
|
|
701
|
-
}
|
|
702
|
-
function left(i) {
|
|
703
|
-
return 2 * i + 1;
|
|
704
|
-
}
|
|
705
|
-
function right(i) {
|
|
706
|
-
return 2 * i + 2;
|
|
707
|
-
}
|
|
708
|
-
function swap(i, j) {
|
|
709
|
-
const tmp = heap[i];
|
|
710
|
-
heap[i] = heap[j];
|
|
711
|
-
heap[j] = tmp;
|
|
712
|
-
}
|
|
713
|
-
function siftUp(i) {
|
|
714
|
-
while (i > 0) {
|
|
715
|
-
const p = parent(i);
|
|
716
|
-
if (compareFn(heap[i], heap[p]) < 0) {
|
|
717
|
-
swap(i, p);
|
|
718
|
-
i = p;
|
|
719
|
-
} else {
|
|
720
|
-
break;
|
|
721
|
-
}
|
|
722
|
-
}
|
|
723
|
-
}
|
|
724
|
-
function siftDown(i) {
|
|
725
|
-
const n = heap.length;
|
|
726
|
-
while (true) {
|
|
727
|
-
let smallest = i;
|
|
728
|
-
const l = left(i);
|
|
729
|
-
const r = right(i);
|
|
730
|
-
if (l < n && compareFn(heap[l], heap[smallest]) < 0) {
|
|
731
|
-
smallest = l;
|
|
732
|
-
}
|
|
733
|
-
if (r < n && compareFn(heap[r], heap[smallest]) < 0) {
|
|
734
|
-
smallest = r;
|
|
735
|
-
}
|
|
736
|
-
if (smallest !== i) {
|
|
737
|
-
swap(i, smallest);
|
|
738
|
-
i = smallest;
|
|
739
|
-
} else {
|
|
740
|
-
break;
|
|
741
|
-
}
|
|
742
|
-
}
|
|
743
|
-
}
|
|
744
|
-
function heapify() {
|
|
745
|
-
for (let i = Math.floor(heap.length / 2) - 1; i >= 0; i--) {
|
|
746
|
-
siftDown(i);
|
|
747
|
-
}
|
|
748
|
-
}
|
|
749
|
-
return {
|
|
750
|
-
enqueue(item) {
|
|
751
|
-
heap.push(item);
|
|
752
|
-
siftUp(heap.length - 1);
|
|
753
|
-
},
|
|
754
|
-
dequeue() {
|
|
755
|
-
if (heap.length === 0) return void 0;
|
|
756
|
-
const min = heap[0];
|
|
757
|
-
const last = heap.pop();
|
|
758
|
-
if (heap.length > 0) {
|
|
759
|
-
heap[0] = last;
|
|
760
|
-
siftDown(0);
|
|
761
|
-
}
|
|
762
|
-
return min;
|
|
763
|
-
},
|
|
764
|
-
peek() {
|
|
765
|
-
return heap[0];
|
|
766
|
-
},
|
|
767
|
-
remove(predicate) {
|
|
768
|
-
const idx = heap.findIndex(predicate);
|
|
769
|
-
if (idx === -1) return false;
|
|
770
|
-
if (idx === heap.length - 1) {
|
|
771
|
-
heap.pop();
|
|
772
|
-
} else {
|
|
773
|
-
heap[idx] = heap.pop();
|
|
774
|
-
siftDown(idx);
|
|
775
|
-
siftUp(idx);
|
|
776
|
-
}
|
|
777
|
-
return true;
|
|
778
|
-
},
|
|
779
|
-
rebuild() {
|
|
780
|
-
heapify();
|
|
781
|
-
},
|
|
782
|
-
clear() {
|
|
783
|
-
heap.length = 0;
|
|
784
|
-
},
|
|
785
|
-
size() {
|
|
786
|
-
return heap.length;
|
|
875
|
+
getPostTimeForChunk(chunkIndex) {
|
|
876
|
+
return postTimes.get(chunkIndex) ?? null;
|
|
787
877
|
},
|
|
788
|
-
|
|
789
|
-
return
|
|
878
|
+
terminate() {
|
|
879
|
+
if (terminated) return;
|
|
880
|
+
terminated = true;
|
|
881
|
+
cancelledChunks.clear();
|
|
882
|
+
postTimes.clear();
|
|
790
883
|
}
|
|
791
884
|
};
|
|
792
885
|
}
|
|
793
886
|
|
|
794
|
-
// src/stretcher/
|
|
795
|
-
function
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
const
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
}
|
|
823
|
-
return Math.abs(distance) * backwardWeight;
|
|
887
|
+
// src/stretcher/position-calc.ts
|
|
888
|
+
function calcPositionInOriginalBuffer(p) {
|
|
889
|
+
if (p.phase === "ended") return p.totalDuration;
|
|
890
|
+
if (p.phase === "waiting") return p.offset;
|
|
891
|
+
if ((p.phase === "buffering" || p.phase === "paused") && p.bufferingResumePosition !== null) {
|
|
892
|
+
return p.bufferingResumePosition;
|
|
893
|
+
}
|
|
894
|
+
if (!p.chunk) return 0;
|
|
895
|
+
const nominalStartSample = p.chunk.inputStartSample + p.chunk.overlapBefore;
|
|
896
|
+
const nominalStartSec = nominalStartSample / p.sampleRate;
|
|
897
|
+
const crossfadeOffset = p.chunk.overlapBefore > 0 ? p.crossfadeSec : 0;
|
|
898
|
+
const adjustedPosInChunk = Math.max(0, p.posInChunk - crossfadeOffset);
|
|
899
|
+
const posInOriginal = adjustedPosInChunk * p.currentTempo;
|
|
900
|
+
return Math.min(nominalStartSec + posInOriginal, p.totalDuration);
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
// src/stretcher/worker-inline.ts
|
|
904
|
+
function getWorkerCode() {
|
|
905
|
+
return `"use strict";
|
|
906
|
+
|
|
907
|
+
var FRAME_SIZE = ${WSOLA_FRAME_SIZE};
|
|
908
|
+
var HOP_SIZE = ${WSOLA_HOP_SIZE};
|
|
909
|
+
var TOLERANCE = ${WSOLA_TOLERANCE};
|
|
910
|
+
|
|
911
|
+
function createHannWindow(size) {
|
|
912
|
+
var w = new Float32Array(size);
|
|
913
|
+
for (var i = 0; i < size; i++) {
|
|
914
|
+
w[i] = 0.5 * (1 - Math.cos((2 * Math.PI * i) / (size - 1)));
|
|
824
915
|
}
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
for (
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
}
|
|
916
|
+
return w;
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
function findBestOffset(ref, search, overlapSize, maxOffset) {
|
|
920
|
+
var bestOffset = 0;
|
|
921
|
+
var bestCorr = -Infinity;
|
|
922
|
+
var searchLen = search.length;
|
|
923
|
+
var refLen = ref.length;
|
|
924
|
+
var len = Math.min(overlapSize, refLen);
|
|
925
|
+
|
|
926
|
+
for (var offset = 0; offset <= maxOffset; offset++) {
|
|
927
|
+
if (offset + len > searchLen) break;
|
|
928
|
+
var corr = 0;
|
|
929
|
+
var normRef = 0;
|
|
930
|
+
var normSearch = 0;
|
|
931
|
+
for (var i = 0; i < len; i++) {
|
|
932
|
+
var r = ref[i];
|
|
933
|
+
var s = search[offset + i];
|
|
934
|
+
corr += r * s;
|
|
935
|
+
normRef += r * r;
|
|
936
|
+
normSearch += s * s;
|
|
847
937
|
}
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
if (
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
while (nextChunk && nextChunk.state === "ready") {
|
|
854
|
-
nextChunk = queue.dequeue();
|
|
855
|
-
}
|
|
856
|
-
if (!nextChunk) return;
|
|
857
|
-
if (nextChunk.state !== "queued" && nextChunk.state !== "pending" && nextChunk.state !== "failed") {
|
|
858
|
-
continue;
|
|
859
|
-
}
|
|
860
|
-
nextChunk.state = "converting";
|
|
861
|
-
const data = extractChunkData2(nextChunk.index);
|
|
862
|
-
workerManager.postConvert(
|
|
863
|
-
nextChunk.index,
|
|
864
|
-
data,
|
|
865
|
-
currentTempo,
|
|
866
|
-
sampleRate
|
|
867
|
-
);
|
|
938
|
+
var denom = Math.sqrt(normRef * normSearch);
|
|
939
|
+
var ncc = denom > 1e-10 ? corr / denom : 0;
|
|
940
|
+
if (ncc > bestCorr) {
|
|
941
|
+
bestCorr = ncc;
|
|
942
|
+
bestOffset = offset;
|
|
868
943
|
}
|
|
869
944
|
}
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
onChunkReady?.(chunkIndex);
|
|
877
|
-
dispatchNext();
|
|
878
|
-
}
|
|
879
|
-
function handleError(chunkIndex, error) {
|
|
880
|
-
const chunk = chunks[chunkIndex];
|
|
881
|
-
if (!chunk) return;
|
|
882
|
-
chunk.retryCount++;
|
|
883
|
-
if (chunk.retryCount < MAX_CHUNK_RETRIES) {
|
|
884
|
-
chunk.state = "queued";
|
|
885
|
-
chunk.priority = calcPriority(chunk.index, currentChunkIdx);
|
|
886
|
-
queue.enqueue(chunk);
|
|
887
|
-
} else {
|
|
888
|
-
chunk.state = "failed";
|
|
889
|
-
onChunkFailed?.(chunkIndex, error);
|
|
890
|
-
}
|
|
891
|
-
dispatchNext();
|
|
945
|
+
return bestOffset;
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
function wsolaTimeStretch(channels, tempo, sampleRate) {
|
|
949
|
+
if (channels.length === 0) {
|
|
950
|
+
return { output: [], length: 0 };
|
|
892
951
|
}
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
if (dist > cancelDistThreshold) {
|
|
898
|
-
workerManager.cancelChunk(chunk.index);
|
|
899
|
-
}
|
|
900
|
-
}
|
|
901
|
-
}
|
|
902
|
-
updatePriorities(newChunkIndex);
|
|
903
|
-
dispatchNext();
|
|
952
|
+
|
|
953
|
+
var inputLength = channels[0].length;
|
|
954
|
+
if (inputLength === 0) {
|
|
955
|
+
return { output: channels.map(function() { return new Float32Array(0); }), length: 0 };
|
|
904
956
|
}
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
}
|
|
912
|
-
return { outputBuffer: null, outputLength: 0 };
|
|
913
|
-
})
|
|
957
|
+
|
|
958
|
+
var TEMPO_IDENTITY_EPSILON = 0.001;
|
|
959
|
+
if (Math.abs(tempo - 1.0) < TEMPO_IDENTITY_EPSILON) {
|
|
960
|
+
return {
|
|
961
|
+
output: channels.map(function(ch) { return new Float32Array(ch); }),
|
|
962
|
+
length: inputLength
|
|
914
963
|
};
|
|
915
|
-
currentTempo = newTempo;
|
|
916
|
-
workerManager.cancelCurrent();
|
|
917
|
-
for (const chunk of chunks) {
|
|
918
|
-
if (chunk.state === "evicted") continue;
|
|
919
|
-
if (isInActiveWindow(chunk.index, currentChunkIdx)) {
|
|
920
|
-
chunk.outputBuffer = null;
|
|
921
|
-
chunk.outputLength = 0;
|
|
922
|
-
chunk.state = "pending";
|
|
923
|
-
chunk.retryCount = 0;
|
|
924
|
-
} else {
|
|
925
|
-
chunk.outputBuffer = null;
|
|
926
|
-
chunk.outputLength = 0;
|
|
927
|
-
chunk.state = "evicted";
|
|
928
|
-
}
|
|
929
|
-
}
|
|
930
|
-
updatePriorities(currentChunkIdx);
|
|
931
|
-
dispatchNext();
|
|
932
|
-
}
|
|
933
|
-
function restorePreviousTempo() {
|
|
934
|
-
if (!previousTempoCache) return false;
|
|
935
|
-
currentTempo = previousTempoCache.tempo;
|
|
936
|
-
for (let i = 0; i < chunks.length; i++) {
|
|
937
|
-
const cached = previousTempoCache.chunks[i];
|
|
938
|
-
const chunk = chunks[i];
|
|
939
|
-
if (chunk && cached?.outputBuffer) {
|
|
940
|
-
chunk.outputBuffer = cached.outputBuffer;
|
|
941
|
-
chunk.outputLength = cached.outputLength;
|
|
942
|
-
chunk.state = "ready";
|
|
943
|
-
}
|
|
944
|
-
}
|
|
945
|
-
previousTempoCache = null;
|
|
946
|
-
workerManager.cancelCurrent();
|
|
947
|
-
updatePriorities(currentChunkIdx);
|
|
948
|
-
dispatchNext();
|
|
949
|
-
return true;
|
|
950
964
|
}
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
965
|
+
|
|
966
|
+
var synthesisHop = HOP_SIZE;
|
|
967
|
+
var analysisHop = Math.round(HOP_SIZE * tempo);
|
|
968
|
+
var numFrames = Math.floor((inputLength - FRAME_SIZE) / analysisHop) + 1;
|
|
969
|
+
|
|
970
|
+
if (numFrames <= 0) {
|
|
971
|
+
return {
|
|
972
|
+
output: channels.map(function(ch) { return new Float32Array(ch); }),
|
|
973
|
+
length: inputLength
|
|
974
|
+
};
|
|
954
975
|
}
|
|
955
|
-
return {
|
|
956
|
-
start,
|
|
957
|
-
updatePriorities,
|
|
958
|
-
handleSeek,
|
|
959
|
-
handleTempoChange,
|
|
960
|
-
restorePreviousTempo,
|
|
961
|
-
dispatchNext,
|
|
962
|
-
getChunks: () => chunks,
|
|
963
|
-
dispose() {
|
|
964
|
-
disposed = true;
|
|
965
|
-
queue.clear();
|
|
966
|
-
},
|
|
967
|
-
// Expose internal handlers for the engine to wire up
|
|
968
|
-
_handleResult: handleResult,
|
|
969
|
-
_handleError: handleError
|
|
970
|
-
};
|
|
971
|
-
}
|
|
972
976
|
|
|
973
|
-
|
|
974
|
-
var
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
}
|
|
983
|
-
|
|
984
|
-
var
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
977
|
+
var estimatedOutputLength = (numFrames - 1) * synthesisHop + FRAME_SIZE;
|
|
978
|
+
var outputChannels = channels.map(function() {
|
|
979
|
+
return new Float32Array(estimatedOutputLength);
|
|
980
|
+
});
|
|
981
|
+
var windowFunc = createHannWindow(FRAME_SIZE);
|
|
982
|
+
var normBuffer = new Float32Array(estimatedOutputLength);
|
|
983
|
+
|
|
984
|
+
var prevOutputFrame = channels.map(function() {
|
|
985
|
+
return new Float32Array(FRAME_SIZE);
|
|
986
|
+
});
|
|
987
|
+
|
|
988
|
+
var inputPos = 0;
|
|
989
|
+
var outputPos = 0;
|
|
990
|
+
var actualOutputLength = 0;
|
|
991
|
+
|
|
992
|
+
for (var frame = 0; frame < numFrames; frame++) {
|
|
993
|
+
if (cancelled) return null;
|
|
994
|
+
if (inputPos + FRAME_SIZE > inputLength) break;
|
|
995
|
+
|
|
996
|
+
var actualInputPos = inputPos;
|
|
997
|
+
|
|
998
|
+
if (frame > 0 && TOLERANCE > 0) {
|
|
999
|
+
var searchStart = Math.max(0, inputPos - TOLERANCE);
|
|
1000
|
+
var searchEnd = Math.min(inputLength - FRAME_SIZE, inputPos + TOLERANCE);
|
|
1001
|
+
var searchRange = searchEnd - searchStart;
|
|
1002
|
+
|
|
1003
|
+
if (searchRange > 0) {
|
|
1004
|
+
var refChannel = prevOutputFrame[0];
|
|
1005
|
+
var inputChannel = channels[0];
|
|
1006
|
+
|
|
1007
|
+
var overlapStart = FRAME_SIZE - synthesisHop;
|
|
1008
|
+
var overlapSize = Math.min(synthesisHop, FRAME_SIZE - overlapStart);
|
|
1009
|
+
|
|
1010
|
+
var refSlice = refChannel.subarray(overlapStart, overlapStart + overlapSize);
|
|
1011
|
+
var searchSlice = inputChannel.subarray(searchStart, searchEnd + overlapSize);
|
|
1012
|
+
|
|
1013
|
+
var bestOffset = findBestOffset(
|
|
1014
|
+
refSlice, searchSlice, overlapSize,
|
|
1015
|
+
Math.min(searchRange, searchSlice.length - overlapSize)
|
|
1016
|
+
);
|
|
1017
|
+
|
|
1018
|
+
actualInputPos = searchStart + bestOffset;
|
|
1010
1019
|
}
|
|
1011
|
-
through[through.length - 1].connect(destination);
|
|
1012
|
-
} else {
|
|
1013
|
-
node.connect(destination);
|
|
1014
1020
|
}
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
} catch {
|
|
1021
|
+
|
|
1022
|
+
for (var ch = 0; ch < channels.length; ch++) {
|
|
1023
|
+
var input = channels[ch];
|
|
1024
|
+
var output = outputChannels[ch];
|
|
1025
|
+
var prevFrame = prevOutputFrame[ch];
|
|
1026
|
+
for (var i = 0; i < FRAME_SIZE; i++) {
|
|
1027
|
+
var inIdx = actualInputPos + i;
|
|
1028
|
+
if (inIdx >= inputLength) break;
|
|
1029
|
+
var outIdx = outputPos + i;
|
|
1030
|
+
if (outIdx >= estimatedOutputLength) break;
|
|
1031
|
+
var sample = input[inIdx];
|
|
1032
|
+
output[outIdx] += sample * windowFunc[i];
|
|
1033
|
+
prevFrame[i] = sample * windowFunc[i];
|
|
1029
1034
|
}
|
|
1030
|
-
currentSource.disconnect();
|
|
1031
|
-
currentSource = null;
|
|
1032
1035
|
}
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
+
|
|
1037
|
+
for (var i = 0; i < FRAME_SIZE; i++) {
|
|
1038
|
+
var outIdx = outputPos + i;
|
|
1039
|
+
if (outIdx >= estimatedOutputLength) break;
|
|
1040
|
+
normBuffer[outIdx] += windowFunc[i];
|
|
1036
1041
|
}
|
|
1042
|
+
|
|
1043
|
+
inputPos += analysisHop;
|
|
1044
|
+
outputPos += synthesisHop;
|
|
1045
|
+
actualOutputLength = Math.min(outputPos + FRAME_SIZE, estimatedOutputLength);
|
|
1037
1046
|
}
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1047
|
+
|
|
1048
|
+
for (var ch = 0; ch < outputChannels.length; ch++) {
|
|
1049
|
+
var output = outputChannels[ch];
|
|
1050
|
+
for (var i = 0; i < actualOutputLength; i++) {
|
|
1051
|
+
var norm = normBuffer[i];
|
|
1052
|
+
if (norm > 1e-8) {
|
|
1053
|
+
output[i] /= norm;
|
|
1044
1054
|
}
|
|
1045
|
-
nextSource.disconnect();
|
|
1046
|
-
nextSource = null;
|
|
1047
|
-
}
|
|
1048
|
-
if (nextGain) {
|
|
1049
|
-
nextGain.disconnect();
|
|
1050
|
-
nextGain = null;
|
|
1051
1055
|
}
|
|
1052
1056
|
}
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1057
|
+
|
|
1058
|
+
var trimmedOutput = outputChannels.map(function(ch) {
|
|
1059
|
+
return ch.slice(0, actualOutputLength);
|
|
1060
|
+
});
|
|
1061
|
+
|
|
1062
|
+
return { output: trimmedOutput, length: actualOutputLength };
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
var cancelled = false;
|
|
1066
|
+
|
|
1067
|
+
self.onmessage = function(e) {
|
|
1068
|
+
var msg = e.data;
|
|
1069
|
+
if (msg.type === "cancel") {
|
|
1070
|
+
cancelled = true;
|
|
1071
|
+
return;
|
|
1063
1072
|
}
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1073
|
+
if (msg.type === "convert") {
|
|
1074
|
+
cancelled = false;
|
|
1075
|
+
try {
|
|
1076
|
+
var result = wsolaTimeStretch(msg.inputData, msg.tempo, msg.sampleRate);
|
|
1077
|
+
if (cancelled || result === null) {
|
|
1078
|
+
self.postMessage({ type: "cancelled", chunkIndex: msg.chunkIndex });
|
|
1079
|
+
} else {
|
|
1080
|
+
self.postMessage(
|
|
1081
|
+
{ type: "result", chunkIndex: msg.chunkIndex, outputData: result.output, outputLength: result.length },
|
|
1082
|
+
result.output.map(function(ch) { return ch.buffer; })
|
|
1083
|
+
);
|
|
1084
|
+
}
|
|
1085
|
+
} catch (err) {
|
|
1086
|
+
self.postMessage({ type: "error", chunkIndex: msg.chunkIndex, error: String(err) });
|
|
1068
1087
|
}
|
|
1069
1088
|
}
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1089
|
+
};
|
|
1090
|
+
`;
|
|
1091
|
+
}
|
|
1092
|
+
function createWorkerURL() {
|
|
1093
|
+
const code = getWorkerCode();
|
|
1094
|
+
const blob = new Blob([code], { type: "application/javascript" });
|
|
1095
|
+
return URL.createObjectURL(blob);
|
|
1096
|
+
}
|
|
1097
|
+
function revokeWorkerURL(url) {
|
|
1098
|
+
URL.revokeObjectURL(url);
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
// src/stretcher/worker-manager.ts
|
|
1102
|
+
function createWorkerManager(onResult, onError, maxCrashes = MAX_WORKER_CRASHES, poolSize = WORKER_POOL_SIZE, onAllDead) {
|
|
1103
|
+
let workerURL = null;
|
|
1104
|
+
let terminated = false;
|
|
1105
|
+
let allDeadFired = false;
|
|
1106
|
+
const postTimes = /* @__PURE__ */ new Map();
|
|
1107
|
+
const slots = [];
|
|
1108
|
+
function ensureWorkerURL() {
|
|
1109
|
+
if (!workerURL) {
|
|
1110
|
+
workerURL = createWorkerURL();
|
|
1074
1111
|
}
|
|
1112
|
+
return workerURL;
|
|
1075
1113
|
}
|
|
1076
|
-
function
|
|
1077
|
-
|
|
1078
|
-
if (stopped) return 0;
|
|
1079
|
-
return ctx.currentTime - playStartCtxTime + playStartOffset;
|
|
1114
|
+
function isAllDead() {
|
|
1115
|
+
return slots.every((s) => s.worker === null);
|
|
1080
1116
|
}
|
|
1081
|
-
function
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
if (
|
|
1094
|
-
|
|
1117
|
+
function spawnWorkerForSlot(slot) {
|
|
1118
|
+
if (terminated) return;
|
|
1119
|
+
const url = ensureWorkerURL();
|
|
1120
|
+
let worker;
|
|
1121
|
+
try {
|
|
1122
|
+
worker = new Worker(url);
|
|
1123
|
+
} catch {
|
|
1124
|
+
slot.worker = null;
|
|
1125
|
+
return;
|
|
1126
|
+
}
|
|
1127
|
+
worker.onmessage = (e) => {
|
|
1128
|
+
const response = e.data;
|
|
1129
|
+
if (response.type === "result" || response.type === "cancelled") {
|
|
1130
|
+
slot.busy = false;
|
|
1131
|
+
slot.currentChunkIndex = null;
|
|
1132
|
+
postTimes.delete(response.chunkIndex);
|
|
1133
|
+
}
|
|
1134
|
+
if (response.type === "error") {
|
|
1135
|
+
slot.busy = false;
|
|
1136
|
+
slot.currentChunkIndex = null;
|
|
1137
|
+
postTimes.delete(response.chunkIndex);
|
|
1138
|
+
onError(response);
|
|
1139
|
+
return;
|
|
1140
|
+
}
|
|
1141
|
+
onResult(response);
|
|
1142
|
+
};
|
|
1143
|
+
worker.onerror = (e) => {
|
|
1144
|
+
e.preventDefault();
|
|
1145
|
+
slot.busy = false;
|
|
1146
|
+
const failedChunkIndex = slot.currentChunkIndex;
|
|
1147
|
+
slot.currentChunkIndex = null;
|
|
1148
|
+
slot.crashCount++;
|
|
1149
|
+
if (failedChunkIndex !== null) {
|
|
1150
|
+
postTimes.delete(failedChunkIndex);
|
|
1151
|
+
onError({
|
|
1152
|
+
type: "error",
|
|
1153
|
+
chunkIndex: failedChunkIndex,
|
|
1154
|
+
error: `Worker crashed: ${e.message}`
|
|
1155
|
+
});
|
|
1156
|
+
}
|
|
1157
|
+
if (slot.worker) {
|
|
1158
|
+
slot.worker.onmessage = null;
|
|
1159
|
+
slot.worker.onerror = null;
|
|
1160
|
+
slot.worker.terminate();
|
|
1161
|
+
slot.worker = null;
|
|
1162
|
+
}
|
|
1163
|
+
if (slot.crashCount < maxCrashes) {
|
|
1164
|
+
spawnWorkerForSlot(slot);
|
|
1165
|
+
} else {
|
|
1166
|
+
onError({
|
|
1167
|
+
type: "error",
|
|
1168
|
+
chunkIndex: failedChunkIndex ?? -1,
|
|
1169
|
+
error: `Worker crashed ${slot.crashCount} times, giving up`
|
|
1170
|
+
});
|
|
1171
|
+
if (!allDeadFired && isAllDead()) {
|
|
1172
|
+
allDeadFired = true;
|
|
1173
|
+
onAllDead?.();
|
|
1174
|
+
}
|
|
1095
1175
|
}
|
|
1096
1176
|
};
|
|
1097
|
-
|
|
1098
|
-
if (crossfadeSec > 0) {
|
|
1099
|
-
currentGain.gain.setValueCurveAtTime(fadeInCurve, ctx.currentTime, crossfadeSec);
|
|
1100
|
-
}
|
|
1101
|
-
startLookahead();
|
|
1177
|
+
slot.worker = worker;
|
|
1102
1178
|
}
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
if (!disposed && !paused && !stopped) {
|
|
1110
|
-
onChunkEnded?.();
|
|
1111
|
-
}
|
|
1179
|
+
for (let i = 0; i < poolSize; i++) {
|
|
1180
|
+
const slot = {
|
|
1181
|
+
worker: null,
|
|
1182
|
+
busy: false,
|
|
1183
|
+
currentChunkIndex: null,
|
|
1184
|
+
crashCount: 0
|
|
1112
1185
|
};
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
currentGain.gain.setValueCurveAtTime(fadeOutCurve, startTime - crossfadeSec, crossfadeSec);
|
|
1116
|
-
nextGain.gain.setValueCurveAtTime(fadeInCurve, startTime - crossfadeSec, crossfadeSec);
|
|
1117
|
-
}
|
|
1118
|
-
const transitionDelay = Math.max(
|
|
1119
|
-
0,
|
|
1120
|
-
(startTime - ctx.currentTime) * 1e3 + 50
|
|
1121
|
-
);
|
|
1122
|
-
cancelTransition();
|
|
1123
|
-
transitionTimerId = setTimeout(() => {
|
|
1124
|
-
transitionTimerId = null;
|
|
1125
|
-
if (disposed) return;
|
|
1126
|
-
stopCurrentSource();
|
|
1127
|
-
currentSource = nextSource;
|
|
1128
|
-
currentGain = nextGain;
|
|
1129
|
-
nextSource = null;
|
|
1130
|
-
nextGain = null;
|
|
1131
|
-
currentChunkDuration = buffer.duration;
|
|
1132
|
-
playStartOffset = 0;
|
|
1133
|
-
playStartCtxTime = startTime - crossfadeSec;
|
|
1134
|
-
onTransition?.();
|
|
1135
|
-
}, transitionDelay);
|
|
1136
|
-
}
|
|
1137
|
-
function handleSeek(buffer, offsetInChunk) {
|
|
1138
|
-
playChunk(buffer, 0, offsetInChunk);
|
|
1139
|
-
}
|
|
1140
|
-
function pause() {
|
|
1141
|
-
if (paused || stopped || disposed) return;
|
|
1142
|
-
pausedPosition = getElapsedInChunk();
|
|
1143
|
-
paused = true;
|
|
1144
|
-
cancelTransition();
|
|
1145
|
-
stopCurrentSource();
|
|
1146
|
-
stopNextSource();
|
|
1147
|
-
stopLookahead();
|
|
1148
|
-
}
|
|
1149
|
-
function resume() {
|
|
1150
|
-
if (!paused || disposed) return;
|
|
1186
|
+
slots.push(slot);
|
|
1187
|
+
spawnWorkerForSlot(slot);
|
|
1151
1188
|
}
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
paused = false;
|
|
1156
|
-
pausedPosition = 0;
|
|
1157
|
-
cancelTransition();
|
|
1158
|
-
stopCurrentSource();
|
|
1159
|
-
stopNextSource();
|
|
1160
|
-
stopLookahead();
|
|
1189
|
+
if (!allDeadFired && isAllDead()) {
|
|
1190
|
+
allDeadFired = true;
|
|
1191
|
+
onAllDead?.();
|
|
1161
1192
|
}
|
|
1162
|
-
function
|
|
1163
|
-
|
|
1193
|
+
function findFreeSlot() {
|
|
1194
|
+
for (const slot of slots) {
|
|
1195
|
+
if (!slot.busy && slot.worker) {
|
|
1196
|
+
return slot;
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
return null;
|
|
1164
1200
|
}
|
|
1165
|
-
function
|
|
1166
|
-
|
|
1201
|
+
function findSlotByChunk(chunkIndex) {
|
|
1202
|
+
for (const slot of slots) {
|
|
1203
|
+
if (slot.busy && slot.currentChunkIndex === chunkIndex) {
|
|
1204
|
+
return slot;
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
return null;
|
|
1167
1208
|
}
|
|
1168
1209
|
return {
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1210
|
+
postConvert(chunkIndex, inputData, tempo, sampleRate) {
|
|
1211
|
+
if (terminated) return;
|
|
1212
|
+
const slot = findFreeSlot();
|
|
1213
|
+
if (!slot || !slot.worker) return;
|
|
1214
|
+
slot.busy = true;
|
|
1215
|
+
slot.currentChunkIndex = chunkIndex;
|
|
1216
|
+
postTimes.set(chunkIndex, performance.now());
|
|
1217
|
+
const transferables = inputData.map((ch) => ch.buffer);
|
|
1218
|
+
slot.worker.postMessage(
|
|
1219
|
+
{ type: "convert", chunkIndex, inputData, tempo, sampleRate },
|
|
1220
|
+
transferables
|
|
1221
|
+
);
|
|
1179
1222
|
},
|
|
1180
|
-
|
|
1181
|
-
|
|
1223
|
+
cancelCurrent() {
|
|
1224
|
+
if (terminated) return;
|
|
1225
|
+
for (const slot of slots) {
|
|
1226
|
+
if (slot.busy && slot.worker && slot.currentChunkIndex !== null) {
|
|
1227
|
+
slot.worker.postMessage({ type: "cancel", chunkIndex: slot.currentChunkIndex });
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1182
1230
|
},
|
|
1183
|
-
|
|
1184
|
-
|
|
1231
|
+
cancelChunk(chunkIndex) {
|
|
1232
|
+
if (terminated) return;
|
|
1233
|
+
const slot = findSlotByChunk(chunkIndex);
|
|
1234
|
+
if (slot?.worker) {
|
|
1235
|
+
slot.worker.postMessage({ type: "cancel", chunkIndex });
|
|
1236
|
+
}
|
|
1185
1237
|
},
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1238
|
+
isBusy() {
|
|
1239
|
+
return slots.every((s) => s.busy || !s.worker);
|
|
1240
|
+
},
|
|
1241
|
+
hasCapacity() {
|
|
1242
|
+
return findFreeSlot() !== null;
|
|
1243
|
+
},
|
|
1244
|
+
getCurrentChunkIndex() {
|
|
1245
|
+
for (const slot of slots) {
|
|
1246
|
+
if (slot.busy && slot.currentChunkIndex !== null) {
|
|
1247
|
+
return slot.currentChunkIndex;
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
return null;
|
|
1251
|
+
},
|
|
1252
|
+
getLastPostTime() {
|
|
1253
|
+
let latest = null;
|
|
1254
|
+
for (const t of postTimes.values()) {
|
|
1255
|
+
if (latest === null || t > latest) {
|
|
1256
|
+
latest = t;
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
return latest;
|
|
1260
|
+
},
|
|
1261
|
+
getPostTimeForChunk(chunkIndex) {
|
|
1262
|
+
return postTimes.get(chunkIndex) ?? null;
|
|
1263
|
+
},
|
|
1264
|
+
terminate() {
|
|
1265
|
+
if (terminated) return;
|
|
1266
|
+
terminated = true;
|
|
1267
|
+
for (const slot of slots) {
|
|
1268
|
+
if (slot.worker) {
|
|
1269
|
+
slot.worker.onmessage = null;
|
|
1270
|
+
slot.worker.onerror = null;
|
|
1271
|
+
slot.worker.terminate();
|
|
1272
|
+
slot.worker = null;
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
if (workerURL) {
|
|
1276
|
+
revokeWorkerURL(workerURL);
|
|
1277
|
+
workerURL = null;
|
|
1278
|
+
}
|
|
1279
|
+
postTimes.clear();
|
|
1210
1280
|
}
|
|
1211
|
-
return aheadSec;
|
|
1212
|
-
}
|
|
1213
|
-
function getHealth(currentChunkIndex, chunks) {
|
|
1214
|
-
const ahead = getAheadSeconds(currentChunkIndex, chunks);
|
|
1215
|
-
if (ahead >= healthySec) return "healthy";
|
|
1216
|
-
if (ahead >= lowSec) return "low";
|
|
1217
|
-
if (ahead >= criticalSec) return "critical";
|
|
1218
|
-
return "empty";
|
|
1219
|
-
}
|
|
1220
|
-
function shouldEnterBuffering(currentChunkIndex, chunks) {
|
|
1221
|
-
const ahead = getAheadSeconds(currentChunkIndex, chunks);
|
|
1222
|
-
if (ahead >= criticalSec) return false;
|
|
1223
|
-
const nextChunk = chunks[currentChunkIndex + 1];
|
|
1224
|
-
if (nextChunk && nextChunk.state === "ready") return false;
|
|
1225
|
-
const currentChunk = chunks[currentChunkIndex];
|
|
1226
|
-
if (!currentChunk || currentChunk.state !== "ready") return true;
|
|
1227
|
-
return ahead < criticalSec;
|
|
1228
|
-
}
|
|
1229
|
-
function shouldExitBuffering(currentChunkIndex, chunks) {
|
|
1230
|
-
const currentChunk = chunks[currentChunkIndex];
|
|
1231
|
-
if (!currentChunk || currentChunk.state !== "ready") return false;
|
|
1232
|
-
const ahead = getAheadSeconds(currentChunkIndex, chunks);
|
|
1233
|
-
if (ahead >= resumeSec) return true;
|
|
1234
|
-
const nextChunk = chunks[currentChunkIndex + 1];
|
|
1235
|
-
if (nextChunk && nextChunk.state === "ready") return true;
|
|
1236
|
-
const allReady = chunks.every(
|
|
1237
|
-
(c) => c.state === "ready" || c.state === "skipped"
|
|
1238
|
-
);
|
|
1239
|
-
if (allReady) return true;
|
|
1240
|
-
return false;
|
|
1241
|
-
}
|
|
1242
|
-
return {
|
|
1243
|
-
getHealth,
|
|
1244
|
-
getAheadSeconds,
|
|
1245
|
-
shouldEnterBuffering,
|
|
1246
|
-
shouldExitBuffering
|
|
1247
1281
|
};
|
|
1248
1282
|
}
|
|
1249
1283
|
|
|
@@ -1254,7 +1288,7 @@ function trimOverlap(outputData, outputLength, chunk, sampleRate) {
|
|
|
1254
1288
|
return { data: outputData, length: outputLength };
|
|
1255
1289
|
}
|
|
1256
1290
|
const ratio = outputLength / inputLength;
|
|
1257
|
-
const crossfadeKeep = Math.round(CROSSFADE_SEC * sampleRate);
|
|
1291
|
+
const crossfadeKeep = Math.round(CROSSFADE_SEC * sampleRate * Math.min(1, ratio));
|
|
1258
1292
|
const overlapBeforeOutput = Math.round(chunk.overlapBefore * ratio);
|
|
1259
1293
|
const overlapAfterOutput = Math.round(chunk.overlapAfter * ratio);
|
|
1260
1294
|
const keepBefore = chunk.overlapBefore > 0 ? Math.min(crossfadeKeep, overlapBeforeOutput) : 0;
|
|
@@ -1290,14 +1324,15 @@ function createStretcherEngine(ctx, buffer, options) {
|
|
|
1290
1324
|
let bufferingStartTime = 0;
|
|
1291
1325
|
let currentChunkIndex = 0;
|
|
1292
1326
|
let bufferingResumePosition = null;
|
|
1327
|
+
let expectedTransitionFrom = null;
|
|
1328
|
+
let nextChunkScheduledIndex = null;
|
|
1329
|
+
let pendingTempoChange = false;
|
|
1293
1330
|
const keepAhead = Math.max(KEEP_AHEAD_CHUNKS, Math.ceil(KEEP_AHEAD_SECONDS / CHUNK_DURATION_SEC));
|
|
1294
|
-
const keepBehind = Math.max(
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
sampleRate,
|
|
1298
|
-
CHUNK_DURATION_SEC,
|
|
1299
|
-
OVERLAP_SEC
|
|
1331
|
+
const keepBehind = Math.max(
|
|
1332
|
+
KEEP_BEHIND_CHUNKS,
|
|
1333
|
+
Math.ceil(KEEP_BEHIND_SECONDS / CHUNK_DURATION_SEC)
|
|
1300
1334
|
);
|
|
1335
|
+
const chunks = splitIntoChunks(buffer.length, sampleRate, CHUNK_DURATION_SEC, OVERLAP_SEC);
|
|
1301
1336
|
const monitor = createBufferMonitor();
|
|
1302
1337
|
const poolSize = options.workerPoolSize ?? WORKER_POOL_SIZE;
|
|
1303
1338
|
function handleWorkerResult(response) {
|
|
@@ -1313,11 +1348,7 @@ function createStretcherEngine(ctx, buffer, options) {
|
|
|
1313
1348
|
chunk,
|
|
1314
1349
|
sampleRate
|
|
1315
1350
|
);
|
|
1316
|
-
schedulerInternal._handleResult(
|
|
1317
|
-
response.chunkIndex,
|
|
1318
|
-
trimmed.data,
|
|
1319
|
-
trimmed.length
|
|
1320
|
-
);
|
|
1351
|
+
schedulerInternal._handleResult(response.chunkIndex, trimmed.data, trimmed.length);
|
|
1321
1352
|
}
|
|
1322
1353
|
} else if (response.type === "cancelled") {
|
|
1323
1354
|
const chunk = chunks[response.chunkIndex];
|
|
@@ -1330,10 +1361,7 @@ function createStretcherEngine(ctx, buffer, options) {
|
|
|
1330
1361
|
function handleWorkerError(response) {
|
|
1331
1362
|
if (disposed) return;
|
|
1332
1363
|
if (response.type === "error") {
|
|
1333
|
-
schedulerInternal._handleError(
|
|
1334
|
-
response.chunkIndex,
|
|
1335
|
-
response.error ?? "Unknown error"
|
|
1336
|
-
);
|
|
1364
|
+
schedulerInternal._handleError(response.chunkIndex, response.error ?? "Unknown error");
|
|
1337
1365
|
}
|
|
1338
1366
|
}
|
|
1339
1367
|
function switchToMainThread() {
|
|
@@ -1369,26 +1397,34 @@ function createStretcherEngine(ctx, buffer, options) {
|
|
|
1369
1397
|
if (disposed || phase === "paused" || phase === "ended") return;
|
|
1370
1398
|
advanceToNextChunk();
|
|
1371
1399
|
});
|
|
1400
|
+
function tryScheduleNext(nextIdx) {
|
|
1401
|
+
if (nextChunkScheduledIndex === nextIdx) return;
|
|
1402
|
+
if (nextIdx >= chunks.length) return;
|
|
1403
|
+
const nextChunk = chunks[nextIdx];
|
|
1404
|
+
if (!nextChunk || nextChunk.state !== "ready" || !nextChunk.outputBuffer) return;
|
|
1405
|
+
if (chunkPlayer.hasNextScheduled()) return;
|
|
1406
|
+
const curChunk = chunks[currentChunkIndex];
|
|
1407
|
+
const curOutputDuration = curChunk ? curChunk.outputLength / sampleRate : 0;
|
|
1408
|
+
const elapsed = chunkPlayer.getCurrentPosition();
|
|
1409
|
+
const remaining = curOutputDuration - elapsed;
|
|
1410
|
+
if (remaining <= 0) return;
|
|
1411
|
+
const audioBuffer = createAudioBufferFromChunk(nextChunk);
|
|
1412
|
+
if (!audioBuffer) return;
|
|
1413
|
+
const startTime = ctx.currentTime + remaining;
|
|
1414
|
+
expectedTransitionFrom = currentChunkIndex;
|
|
1415
|
+
chunkPlayer.scheduleNext(audioBuffer, startTime);
|
|
1416
|
+
nextChunkScheduledIndex = nextIdx;
|
|
1417
|
+
}
|
|
1372
1418
|
chunkPlayer.setOnNeedNext(() => {
|
|
1373
1419
|
if (disposed) return;
|
|
1374
|
-
|
|
1375
|
-
if (nextIdx < chunks.length) {
|
|
1376
|
-
const nextChunk = chunks[nextIdx];
|
|
1377
|
-
if (nextChunk.state === "ready" && nextChunk.outputBuffer) {
|
|
1378
|
-
const audioBuffer = createAudioBufferFromChunk(nextChunk);
|
|
1379
|
-
if (audioBuffer) {
|
|
1380
|
-
const curChunk = chunks[currentChunkIndex];
|
|
1381
|
-
const curOutputDuration = curChunk ? curChunk.outputLength / sampleRate : 0;
|
|
1382
|
-
const elapsed = chunkPlayer.getCurrentPosition();
|
|
1383
|
-
const remaining = curOutputDuration - elapsed;
|
|
1384
|
-
const startTime = ctx.currentTime + Math.max(0, remaining);
|
|
1385
|
-
chunkPlayer.scheduleNext(audioBuffer, startTime);
|
|
1386
|
-
}
|
|
1387
|
-
}
|
|
1388
|
-
}
|
|
1420
|
+
tryScheduleNext(currentChunkIndex + 1);
|
|
1389
1421
|
});
|
|
1390
1422
|
chunkPlayer.setOnTransition(() => {
|
|
1391
1423
|
if (disposed) return;
|
|
1424
|
+
if (expectedTransitionFrom !== null && currentChunkIndex !== expectedTransitionFrom) {
|
|
1425
|
+
return;
|
|
1426
|
+
}
|
|
1427
|
+
nextChunkScheduledIndex = null;
|
|
1392
1428
|
const nextIdx = currentChunkIndex + 1;
|
|
1393
1429
|
if (nextIdx < chunks.length) {
|
|
1394
1430
|
currentChunkIndex = nextIdx;
|
|
@@ -1407,21 +1443,12 @@ function createStretcherEngine(ctx, buffer, options) {
|
|
|
1407
1443
|
}
|
|
1408
1444
|
}
|
|
1409
1445
|
if (phase === "playing" && chunkIndex === currentChunkIndex + 1) {
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
const nextChunk = chunks[chunkIndex];
|
|
1417
|
-
if (nextChunk && nextChunk.outputBuffer) {
|
|
1418
|
-
const audioBuffer = createAudioBufferFromChunk(nextChunk);
|
|
1419
|
-
if (audioBuffer) {
|
|
1420
|
-
const startTime = ctx.currentTime + Math.max(0, remaining);
|
|
1421
|
-
chunkPlayer.scheduleNext(audioBuffer, startTime);
|
|
1422
|
-
}
|
|
1423
|
-
}
|
|
1424
|
-
}
|
|
1446
|
+
const curChunk = chunks[currentChunkIndex];
|
|
1447
|
+
const curOutputDuration = curChunk ? curChunk.outputLength / sampleRate : 0;
|
|
1448
|
+
const elapsed = chunkPlayer.getCurrentPosition();
|
|
1449
|
+
const remaining = curOutputDuration - elapsed;
|
|
1450
|
+
if (remaining <= PROACTIVE_SCHEDULE_THRESHOLD_SEC) {
|
|
1451
|
+
tryScheduleNext(chunkIndex);
|
|
1425
1452
|
}
|
|
1426
1453
|
}
|
|
1427
1454
|
const allDone = chunks.every(
|
|
@@ -1441,23 +1468,19 @@ function createStretcherEngine(ctx, buffer, options) {
|
|
|
1441
1468
|
function createAudioBufferFromChunk(chunk) {
|
|
1442
1469
|
if (!chunk.outputBuffer || chunk.outputLength === 0) return null;
|
|
1443
1470
|
const numChannels = chunk.outputBuffer.length;
|
|
1444
|
-
const audioBuf = ctx.createBuffer(
|
|
1445
|
-
numChannels,
|
|
1446
|
-
chunk.outputLength,
|
|
1447
|
-
sampleRate
|
|
1448
|
-
);
|
|
1471
|
+
const audioBuf = ctx.createBuffer(numChannels, chunk.outputLength, sampleRate);
|
|
1449
1472
|
for (let ch = 0; ch < numChannels; ch++) {
|
|
1450
1473
|
const channelData = chunk.outputBuffer[ch];
|
|
1451
1474
|
audioBuf.getChannelData(ch).set(channelData.subarray(0, chunk.outputLength));
|
|
1452
1475
|
}
|
|
1453
1476
|
return audioBuf;
|
|
1454
1477
|
}
|
|
1455
|
-
function playCurrentChunk(offsetInBuffer = 0) {
|
|
1478
|
+
function playCurrentChunk(offsetInBuffer = 0, skipFadeIn = false) {
|
|
1456
1479
|
const chunk = chunks[currentChunkIndex];
|
|
1457
1480
|
if (!chunk || chunk.state !== "ready" || !chunk.outputBuffer) return;
|
|
1458
1481
|
const audioBuf = createAudioBufferFromChunk(chunk);
|
|
1459
1482
|
if (!audioBuf) return;
|
|
1460
|
-
chunkPlayer.playChunk(audioBuf, ctx.currentTime, offsetInBuffer);
|
|
1483
|
+
chunkPlayer.playChunk(audioBuf, ctx.currentTime, offsetInBuffer, skipFadeIn);
|
|
1461
1484
|
}
|
|
1462
1485
|
function advanceToNextChunk() {
|
|
1463
1486
|
const nextIdx = currentChunkIndex + 1;
|
|
@@ -1486,6 +1509,7 @@ function createStretcherEngine(ctx, buffer, options) {
|
|
|
1486
1509
|
return;
|
|
1487
1510
|
}
|
|
1488
1511
|
currentChunkIndex = nextIdx;
|
|
1512
|
+
nextChunkScheduledIndex = null;
|
|
1489
1513
|
scheduler.updatePriorities(currentChunkIndex);
|
|
1490
1514
|
const chunk = chunks[currentChunkIndex];
|
|
1491
1515
|
if (chunk && chunk.state === "ready") {
|
|
@@ -1503,6 +1527,7 @@ function createStretcherEngine(ctx, buffer, options) {
|
|
|
1503
1527
|
if (phase === "buffering" && reason !== "tempo-change" && reason !== "seek") return;
|
|
1504
1528
|
phase = "buffering";
|
|
1505
1529
|
bufferingStartTime = performance.now();
|
|
1530
|
+
nextChunkScheduledIndex = null;
|
|
1506
1531
|
chunkPlayer.pause();
|
|
1507
1532
|
emitter.emit("buffering", { reason });
|
|
1508
1533
|
}
|
|
@@ -1564,26 +1589,22 @@ function createStretcherEngine(ctx, buffer, options) {
|
|
|
1564
1589
|
});
|
|
1565
1590
|
}
|
|
1566
1591
|
function getPositionInOriginalBuffer() {
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
const posInOriginal = adjustedPosInChunk * currentTempo;
|
|
1580
|
-
return Math.min(nominalStartSec + posInOriginal, totalDuration);
|
|
1592
|
+
const chunk = chunks[currentChunkIndex] ?? null;
|
|
1593
|
+
return calcPositionInOriginalBuffer({
|
|
1594
|
+
phase,
|
|
1595
|
+
totalDuration,
|
|
1596
|
+
offset,
|
|
1597
|
+
bufferingResumePosition,
|
|
1598
|
+
currentTempo,
|
|
1599
|
+
sampleRate,
|
|
1600
|
+
crossfadeSec: CROSSFADE_SEC,
|
|
1601
|
+
chunk: chunk ? { inputStartSample: chunk.inputStartSample, overlapBefore: chunk.overlapBefore } : null,
|
|
1602
|
+
posInChunk: chunkPlayer.getCurrentPosition()
|
|
1603
|
+
});
|
|
1581
1604
|
}
|
|
1582
1605
|
function getStatus() {
|
|
1583
1606
|
const readyCount = chunks.filter((c) => c.state === "ready").length;
|
|
1584
|
-
const convertingCount = chunks.filter(
|
|
1585
|
-
(c) => c.state === "converting"
|
|
1586
|
-
).length;
|
|
1607
|
+
const convertingCount = chunks.filter((c) => c.state === "converting").length;
|
|
1587
1608
|
const total = chunks.length;
|
|
1588
1609
|
return {
|
|
1589
1610
|
phase,
|
|
@@ -1607,9 +1628,7 @@ function createStretcherEngine(ctx, buffer, options) {
|
|
|
1607
1628
|
function getSnapshot() {
|
|
1608
1629
|
const readyCount = chunks.filter((c) => c.state === "ready").length;
|
|
1609
1630
|
const total = chunks.length;
|
|
1610
|
-
const convertingCount = chunks.filter(
|
|
1611
|
-
(c) => c.state === "converting"
|
|
1612
|
-
).length;
|
|
1631
|
+
const convertingCount = chunks.filter((c) => c.state === "converting").length;
|
|
1613
1632
|
const windowStart = Math.max(0, currentChunkIndex - keepBehind);
|
|
1614
1633
|
const windowEnd = Math.min(total - 1, currentChunkIndex + keepAhead);
|
|
1615
1634
|
const windowSize = windowEnd - windowStart + 1;
|
|
@@ -1644,12 +1663,37 @@ function createStretcherEngine(ctx, buffer, options) {
|
|
|
1644
1663
|
}
|
|
1645
1664
|
function resume() {
|
|
1646
1665
|
if (disposed || phase !== "paused") return;
|
|
1666
|
+
if (pendingTempoChange) {
|
|
1667
|
+
pendingTempoChange = false;
|
|
1668
|
+
currentChunkIndex = getChunkIndexForTime(chunks, bufferingResumePosition, sampleRate);
|
|
1669
|
+
enterBuffering("tempo-change");
|
|
1670
|
+
scheduler.updatePriorities(currentChunkIndex);
|
|
1671
|
+
scheduler.handleTempoChange(currentTempo);
|
|
1672
|
+
return;
|
|
1673
|
+
}
|
|
1674
|
+
if (bufferingResumePosition !== null) {
|
|
1675
|
+
const resumePos = bufferingResumePosition;
|
|
1676
|
+
bufferingResumePosition = null;
|
|
1677
|
+
const chunk2 = chunks[currentChunkIndex];
|
|
1678
|
+
if (chunk2 && chunk2.state === "ready" && chunk2.outputBuffer) {
|
|
1679
|
+
const nominalStartSample = chunk2.inputStartSample + chunk2.overlapBefore;
|
|
1680
|
+
const nominalStartSec = nominalStartSample / sampleRate;
|
|
1681
|
+
const offsetInOriginal = resumePos - nominalStartSec;
|
|
1682
|
+
const offsetInOutput = Math.max(0, offsetInOriginal / currentTempo);
|
|
1683
|
+
phase = "playing";
|
|
1684
|
+
playCurrentChunk(getCrossfadeStart(chunk2) + offsetInOutput, true);
|
|
1685
|
+
} else {
|
|
1686
|
+
bufferingResumePosition = resumePos;
|
|
1687
|
+
enterBuffering("seek");
|
|
1688
|
+
}
|
|
1689
|
+
return;
|
|
1690
|
+
}
|
|
1647
1691
|
const chunk = chunks[currentChunkIndex];
|
|
1648
1692
|
if (chunk && chunk.state === "ready") {
|
|
1649
1693
|
const resumePosition = chunkPlayer.getCurrentPosition();
|
|
1650
1694
|
phase = "playing";
|
|
1651
1695
|
chunkPlayer.resume();
|
|
1652
|
-
playCurrentChunk(resumePosition);
|
|
1696
|
+
playCurrentChunk(resumePosition, true);
|
|
1653
1697
|
} else {
|
|
1654
1698
|
enterBuffering("underrun");
|
|
1655
1699
|
}
|
|
@@ -1659,6 +1703,7 @@ function createStretcherEngine(ctx, buffer, options) {
|
|
|
1659
1703
|
const clamped = Math.max(0, Math.min(position, totalDuration));
|
|
1660
1704
|
const newChunkIdx = getChunkIndexForTime(chunks, clamped, sampleRate);
|
|
1661
1705
|
currentChunkIndex = newChunkIdx;
|
|
1706
|
+
nextChunkScheduledIndex = null;
|
|
1662
1707
|
scheduler.handleSeek(newChunkIdx);
|
|
1663
1708
|
const chunk = chunks[newChunkIdx];
|
|
1664
1709
|
if (chunk && chunk.state === "ready") {
|
|
@@ -1675,10 +1720,16 @@ function createStretcherEngine(ctx, buffer, options) {
|
|
|
1675
1720
|
const clampedOffset = Math.min(Math.max(0, bufferOffset), audioBuf.duration - 1e-3);
|
|
1676
1721
|
chunkPlayer.handleSeek(audioBuf, clampedOffset);
|
|
1677
1722
|
}
|
|
1723
|
+
} else if (phase === "paused") {
|
|
1724
|
+
bufferingResumePosition = clamped;
|
|
1678
1725
|
}
|
|
1679
1726
|
} else {
|
|
1680
|
-
|
|
1681
|
-
|
|
1727
|
+
if (phase === "paused") {
|
|
1728
|
+
bufferingResumePosition = clamped;
|
|
1729
|
+
} else {
|
|
1730
|
+
bufferingResumePosition = clamped;
|
|
1731
|
+
enterBuffering("seek");
|
|
1732
|
+
}
|
|
1682
1733
|
}
|
|
1683
1734
|
}
|
|
1684
1735
|
function stop() {
|
|
@@ -1689,18 +1740,39 @@ function createStretcherEngine(ctx, buffer, options) {
|
|
|
1689
1740
|
function setLoop(value) {
|
|
1690
1741
|
isLooping = value;
|
|
1691
1742
|
}
|
|
1743
|
+
let tempoDebounceTimer = null;
|
|
1692
1744
|
function setTempo(newTempo) {
|
|
1693
1745
|
if (disposed || phase === "ended" || newTempo === currentTempo) return;
|
|
1694
|
-
|
|
1695
|
-
|
|
1746
|
+
if (phase === "paused") {
|
|
1747
|
+
if (!pendingTempoChange) {
|
|
1748
|
+
bufferingResumePosition = getPositionInOriginalBuffer();
|
|
1749
|
+
}
|
|
1750
|
+
currentTempo = newTempo;
|
|
1751
|
+
pendingTempoChange = true;
|
|
1752
|
+
return;
|
|
1753
|
+
}
|
|
1754
|
+
const isFirstInBurst = tempoDebounceTimer === null;
|
|
1755
|
+
if (isFirstInBurst) {
|
|
1756
|
+
bufferingResumePosition = getPositionInOriginalBuffer();
|
|
1757
|
+
currentChunkIndex = getChunkIndexForTime(chunks, bufferingResumePosition, sampleRate);
|
|
1758
|
+
enterBuffering("tempo-change");
|
|
1759
|
+
}
|
|
1696
1760
|
currentTempo = newTempo;
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1761
|
+
if (tempoDebounceTimer !== null) clearTimeout(tempoDebounceTimer);
|
|
1762
|
+
tempoDebounceTimer = setTimeout(() => {
|
|
1763
|
+
tempoDebounceTimer = null;
|
|
1764
|
+
if (disposed || phase === "ended") return;
|
|
1765
|
+
scheduler.updatePriorities(currentChunkIndex);
|
|
1766
|
+
scheduler.handleTempoChange(currentTempo);
|
|
1767
|
+
}, 50);
|
|
1700
1768
|
}
|
|
1701
1769
|
function dispose() {
|
|
1702
1770
|
if (disposed) return;
|
|
1703
1771
|
disposed = true;
|
|
1772
|
+
if (tempoDebounceTimer !== null) {
|
|
1773
|
+
clearTimeout(tempoDebounceTimer);
|
|
1774
|
+
tempoDebounceTimer = null;
|
|
1775
|
+
}
|
|
1704
1776
|
chunkPlayer.dispose();
|
|
1705
1777
|
scheduler.dispose();
|
|
1706
1778
|
workerManager.terminate();
|
|
@@ -1723,6 +1795,6 @@ function createStretcherEngine(ctx, buffer, options) {
|
|
|
1723
1795
|
};
|
|
1724
1796
|
}
|
|
1725
1797
|
|
|
1726
|
-
export { createStretcherEngine };
|
|
1727
|
-
//# sourceMappingURL=chunk-
|
|
1728
|
-
//# sourceMappingURL=chunk-
|
|
1798
|
+
export { createStretcherEngine, trimOverlap };
|
|
1799
|
+
//# sourceMappingURL=chunk-GDBOHOGF.js.map
|
|
1800
|
+
//# sourceMappingURL=chunk-GDBOHOGF.js.map
|