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