waa-play 0.1.1 → 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 +40 -123
- package/dist/adapters.cjs +6 -6
- package/dist/adapters.d.cts +15 -3
- package/dist/adapters.d.ts +15 -3
- 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-TULV7V5M.cjs → chunk-G37HMZEX.cjs} +1075 -982
- package/dist/chunk-G37HMZEX.cjs.map +1 -0
- package/dist/{chunk-V2QX5K42.js → chunk-GDBOHOGF.js} +1074 -982
- 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-RWJ4EWJT.js → chunk-HYRDCTBO.js} +152 -116
- package/dist/chunk-HYRDCTBO.js.map +1 -0
- package/dist/chunk-JIHPQAEA.js +90 -0
- 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-OZN5X4N6.cjs +96 -0
- 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-QFJQU7TQ.js +146 -0
- 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-PZE6HTZR.cjs → chunk-VY4UMZMJ.cjs} +154 -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-M2U4LE3F.js.map → engine-7DCOERRN.js.map} +1 -1
- package/dist/engine-ALWPAIX6.cjs +17 -0
- package/dist/{engine-5JK2FCNL.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 +47 -42
- package/dist/index.d.cts +7 -6
- package/dist/index.d.ts +7 -6
- package/dist/index.js +10 -9
- 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 +22 -0
- package/dist/player.cjs.map +1 -0
- package/dist/player.d.cts +64 -0
- package/dist/player.d.ts +64 -0
- package/dist/player.js +13 -0
- package/dist/player.js.map +1 -0
- 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 +5 -3
- package/dist/stretcher.d.ts +5 -3
- 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 +19 -7
- package/dist/chunk-2DL7CAEP.js.map +0 -1
- package/dist/chunk-4LNVRSTM.cjs.map +0 -1
- package/dist/chunk-5J7S6QV3.cjs.map +0 -1
- package/dist/chunk-AGP2IRC6.js +0 -63
- package/dist/chunk-AGP2IRC6.js.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-D5CD5KQZ.cjs.map +0 -1
- package/dist/chunk-GYH2JSCY.js.map +0 -1
- package/dist/chunk-HTGOHC73.cjs +0 -69
- package/dist/chunk-HTGOHC73.cjs.map +0 -1
- package/dist/chunk-LETS7FKB.js.map +0 -1
- package/dist/chunk-M5PDY5EZ.cjs.map +0 -1
- package/dist/chunk-PZE6HTZR.cjs.map +0 -1
- package/dist/chunk-QFFQQMU4.cjs.map +0 -1
- package/dist/chunk-QWNV2BZ5.cjs.map +0 -1
- package/dist/chunk-RWJ4EWJT.js.map +0 -1
- package/dist/chunk-T74FBKTY.js.map +0 -1
- package/dist/chunk-TULV7V5M.cjs.map +0 -1
- package/dist/chunk-V2QX5K42.js.map +0 -1
- package/dist/engine-5JK2FCNL.cjs +0 -13
- package/dist/engine-M2U4LE3F.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,688 +24,393 @@ 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;
|
|
28
29
|
|
|
29
|
-
// src/stretcher/
|
|
30
|
-
function
|
|
31
|
-
|
|
32
|
-
|
|
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;
|
|
33
45
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
return
|
|
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";
|
|
38
52
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
overlapAfter: 0,
|
|
48
|
-
outputBuffer: null,
|
|
49
|
-
outputLength: 0,
|
|
50
|
-
priority: 0,
|
|
51
|
-
retryCount: 0
|
|
52
|
-
}
|
|
53
|
-
];
|
|
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;
|
|
54
61
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
const inputEnd = Math.min(nominalEnd + overlapAfter, totalSamples);
|
|
66
|
-
chunks.push({
|
|
67
|
-
index,
|
|
68
|
-
state: "pending",
|
|
69
|
-
inputStartSample: inputStart,
|
|
70
|
-
inputEndSample: inputEnd,
|
|
71
|
-
overlapBefore,
|
|
72
|
-
overlapAfter,
|
|
73
|
-
outputBuffer: null,
|
|
74
|
-
outputLength: 0,
|
|
75
|
-
priority: 0,
|
|
76
|
-
retryCount: 0
|
|
77
|
-
});
|
|
78
|
-
start = nominalEnd;
|
|
79
|
-
index++;
|
|
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;
|
|
80
72
|
}
|
|
81
|
-
return
|
|
73
|
+
return {
|
|
74
|
+
getHealth,
|
|
75
|
+
getAheadSeconds,
|
|
76
|
+
shouldEnterBuffering,
|
|
77
|
+
shouldExitBuffering
|
|
78
|
+
};
|
|
82
79
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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);
|
|
93
94
|
}
|
|
94
|
-
return
|
|
95
|
+
return curve;
|
|
95
96
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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);
|
|
103
129
|
}
|
|
104
130
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
// src/stretcher/worker-inline.ts
|
|
113
|
-
function getWorkerCode() {
|
|
114
|
-
return `"use strict";
|
|
115
|
-
|
|
116
|
-
var FRAME_SIZE = ${WSOLA_FRAME_SIZE};
|
|
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)));
|
|
131
|
+
function createSourceFromBuffer(buffer, gain) {
|
|
132
|
+
const src = ctx.createBufferSource();
|
|
133
|
+
src.buffer = buffer;
|
|
134
|
+
src.connect(gain);
|
|
135
|
+
connectToDestination(gain);
|
|
136
|
+
return src;
|
|
124
137
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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;
|
|
138
|
+
function stopCurrentSource() {
|
|
139
|
+
if (currentSource) {
|
|
140
|
+
currentSource.onended = null;
|
|
141
|
+
try {
|
|
142
|
+
currentSource.stop();
|
|
143
|
+
} catch {
|
|
144
|
+
}
|
|
145
|
+
currentSource.disconnect();
|
|
146
|
+
currentSource = null;
|
|
146
147
|
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
bestCorr = ncc;
|
|
151
|
-
bestOffset = offset;
|
|
148
|
+
if (currentGain) {
|
|
149
|
+
currentGain.disconnect();
|
|
150
|
+
currentGain = null;
|
|
152
151
|
}
|
|
153
152
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
+
}
|
|
160
167
|
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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);
|
|
165
178
|
}
|
|
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
|
-
};
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
var estimatedOutputLength = (numFrames - 1) * synthesisHop + FRAME_SIZE;
|
|
179
|
-
var outputChannels = channels.map(function() {
|
|
180
|
-
return new Float32Array(estimatedOutputLength);
|
|
181
|
-
});
|
|
182
|
-
var windowFunc = createHannWindow(FRAME_SIZE);
|
|
183
|
-
var normBuffer = new Float32Array(estimatedOutputLength);
|
|
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;
|
|
220
|
-
}
|
|
179
|
+
function stopLookahead() {
|
|
180
|
+
if (lookaheadTimer !== null) {
|
|
181
|
+
clearInterval(lookaheadTimer);
|
|
182
|
+
lookaheadTimer = null;
|
|
221
183
|
}
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
for (var i = 0; i < FRAME_SIZE; i++) {
|
|
228
|
-
var inIdx = actualInputPos + i;
|
|
229
|
-
if (inIdx >= inputLength) break;
|
|
230
|
-
var outIdx = outputPos + i;
|
|
231
|
-
if (outIdx >= estimatedOutputLength) break;
|
|
232
|
-
var sample = input[inIdx];
|
|
233
|
-
output[outIdx] += sample * windowFunc[i];
|
|
234
|
-
prevFrame[i] = sample;
|
|
235
|
-
}
|
|
184
|
+
}
|
|
185
|
+
function cancelTransition() {
|
|
186
|
+
if (transitionTimerId !== null) {
|
|
187
|
+
clearTimeout(transitionTimerId);
|
|
188
|
+
transitionTimerId = null;
|
|
236
189
|
}
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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;
|
|
242
202
|
}
|
|
243
|
-
|
|
244
|
-
inputPos += analysisHop;
|
|
245
|
-
outputPos += synthesisHop;
|
|
246
|
-
actualOutputLength = Math.min(outputPos + FRAME_SIZE, estimatedOutputLength);
|
|
203
|
+
onTransition?.();
|
|
247
204
|
}
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
205
|
+
function handleCurrentSourceEnded() {
|
|
206
|
+
if (disposed || paused || stopped) return;
|
|
207
|
+
if (nextSource) {
|
|
208
|
+
const buf = nextSource.buffer;
|
|
209
|
+
if (!buf) {
|
|
210
|
+
onChunkEnded?.();
|
|
211
|
+
return;
|
|
255
212
|
}
|
|
213
|
+
cancelTransition();
|
|
214
|
+
doTransition(buf, nextStartCtxTime);
|
|
215
|
+
} else {
|
|
216
|
+
onChunkEnded?.();
|
|
256
217
|
}
|
|
257
218
|
}
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
return
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
return { output: trimmedOutput, length: actualOutputLength };
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
var cancelled = false;
|
|
267
|
-
|
|
268
|
-
self.onmessage = function(e) {
|
|
269
|
-
var msg = e.data;
|
|
270
|
-
if (msg.type === "cancel") {
|
|
271
|
-
cancelled = true;
|
|
272
|
-
return;
|
|
219
|
+
function getElapsedInChunk() {
|
|
220
|
+
if (paused) return pausedPosition;
|
|
221
|
+
if (stopped) return 0;
|
|
222
|
+
return ctx.currentTime - playStartCtxTime + playStartOffset;
|
|
273
223
|
}
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
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);
|
|
288
239
|
}
|
|
240
|
+
startLookahead();
|
|
289
241
|
}
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
// src/stretcher/worker-manager.ts
|
|
303
|
-
function createWorkerManager(onResult, onError, maxCrashes = MAX_WORKER_CRASHES, poolSize = WORKER_POOL_SIZE, onAllDead) {
|
|
304
|
-
let workerURL = null;
|
|
305
|
-
let terminated = false;
|
|
306
|
-
const postTimes = /* @__PURE__ */ new Map();
|
|
307
|
-
const slots = [];
|
|
308
|
-
function ensureWorkerURL() {
|
|
309
|
-
if (!workerURL) {
|
|
310
|
-
workerURL = createWorkerURL();
|
|
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);
|
|
311
253
|
}
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
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);
|
|
316
261
|
}
|
|
317
|
-
function
|
|
318
|
-
|
|
319
|
-
const url = ensureWorkerURL();
|
|
320
|
-
let worker;
|
|
321
|
-
try {
|
|
322
|
-
worker = new Worker(url);
|
|
323
|
-
} catch {
|
|
324
|
-
slot.worker = null;
|
|
325
|
-
return;
|
|
326
|
-
}
|
|
327
|
-
worker.onmessage = (e) => {
|
|
328
|
-
const response = e.data;
|
|
329
|
-
if (response.type === "result" || response.type === "cancelled") {
|
|
330
|
-
slot.busy = false;
|
|
331
|
-
slot.currentChunkIndex = null;
|
|
332
|
-
}
|
|
333
|
-
if (response.type === "error") {
|
|
334
|
-
slot.busy = false;
|
|
335
|
-
slot.currentChunkIndex = null;
|
|
336
|
-
onError(response);
|
|
337
|
-
return;
|
|
338
|
-
}
|
|
339
|
-
onResult(response);
|
|
340
|
-
};
|
|
341
|
-
worker.onerror = (e) => {
|
|
342
|
-
e.preventDefault();
|
|
343
|
-
slot.busy = false;
|
|
344
|
-
const failedChunkIndex = slot.currentChunkIndex;
|
|
345
|
-
slot.currentChunkIndex = null;
|
|
346
|
-
slot.crashCount++;
|
|
347
|
-
if (failedChunkIndex !== null) {
|
|
348
|
-
onError({
|
|
349
|
-
type: "error",
|
|
350
|
-
chunkIndex: failedChunkIndex,
|
|
351
|
-
error: `Worker crashed: ${e.message}`
|
|
352
|
-
});
|
|
353
|
-
}
|
|
354
|
-
if (slot.worker) {
|
|
355
|
-
slot.worker.onmessage = null;
|
|
356
|
-
slot.worker.onerror = null;
|
|
357
|
-
slot.worker.terminate();
|
|
358
|
-
slot.worker = null;
|
|
359
|
-
}
|
|
360
|
-
if (slot.crashCount < maxCrashes) {
|
|
361
|
-
spawnWorkerForSlot(slot);
|
|
362
|
-
} else {
|
|
363
|
-
onError({
|
|
364
|
-
type: "error",
|
|
365
|
-
chunkIndex: failedChunkIndex ?? -1,
|
|
366
|
-
error: `Worker crashed ${slot.crashCount} times, giving up`
|
|
367
|
-
});
|
|
368
|
-
if (isAllDead()) {
|
|
369
|
-
onAllDead?.();
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
};
|
|
373
|
-
slot.worker = worker;
|
|
262
|
+
function handleSeek(buffer, offsetInChunk) {
|
|
263
|
+
playChunk(buffer, 0, offsetInChunk);
|
|
374
264
|
}
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
spawnWorkerForSlot(slot);
|
|
265
|
+
function pause() {
|
|
266
|
+
if (paused || stopped || disposed) return;
|
|
267
|
+
pausedPosition = getElapsedInChunk();
|
|
268
|
+
paused = true;
|
|
269
|
+
cancelTransition();
|
|
270
|
+
stopCurrentSource();
|
|
271
|
+
stopNextSource();
|
|
272
|
+
stopLookahead();
|
|
384
273
|
}
|
|
385
|
-
|
|
386
|
-
|
|
274
|
+
function resume() {
|
|
275
|
+
if (!paused || disposed) return;
|
|
387
276
|
}
|
|
388
|
-
function
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
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();
|
|
395
286
|
}
|
|
396
|
-
function
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
}
|
|
402
|
-
return null;
|
|
287
|
+
function getCurrentPosition() {
|
|
288
|
+
return getElapsedInChunk();
|
|
289
|
+
}
|
|
290
|
+
function hasNextScheduled() {
|
|
291
|
+
return nextSource !== null;
|
|
403
292
|
}
|
|
404
293
|
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;
|
|
294
|
+
playChunk,
|
|
295
|
+
scheduleNext,
|
|
296
|
+
hasNextScheduled,
|
|
297
|
+
handleSeek,
|
|
298
|
+
pause,
|
|
299
|
+
resume,
|
|
300
|
+
stop,
|
|
301
|
+
getCurrentPosition,
|
|
302
|
+
setOnChunkEnded(callback) {
|
|
303
|
+
onChunkEnded = callback;
|
|
446
304
|
},
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
for (const t of postTimes.values()) {
|
|
450
|
-
if (latest === null || t > latest) {
|
|
451
|
-
latest = t;
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
return latest;
|
|
305
|
+
setOnNeedNext(callback) {
|
|
306
|
+
onNeedNext = callback;
|
|
455
307
|
},
|
|
456
|
-
|
|
457
|
-
|
|
308
|
+
setOnTransition(callback) {
|
|
309
|
+
onTransition = callback;
|
|
458
310
|
},
|
|
459
|
-
|
|
460
|
-
if (
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
slot.worker.terminate();
|
|
467
|
-
slot.worker = null;
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
|
-
if (workerURL) {
|
|
471
|
-
revokeWorkerURL(workerURL);
|
|
472
|
-
workerURL = null;
|
|
473
|
-
}
|
|
474
|
-
postTimes.clear();
|
|
311
|
+
dispose() {
|
|
312
|
+
if (disposed) return;
|
|
313
|
+
disposed = true;
|
|
314
|
+
cancelTransition();
|
|
315
|
+
stopCurrentSource();
|
|
316
|
+
stopNextSource();
|
|
317
|
+
stopLookahead();
|
|
475
318
|
}
|
|
476
319
|
};
|
|
477
320
|
}
|
|
478
321
|
|
|
479
|
-
// src/stretcher/
|
|
480
|
-
function
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
window[i] = 0.5 * (1 - Math.cos(2 * Math.PI * i / (size - 1)));
|
|
322
|
+
// src/stretcher/chunk-splitter.ts
|
|
323
|
+
function splitIntoChunks(totalSamples, sampleRate, chunkDurationSec = CHUNK_DURATION_SEC, overlapSec = OVERLAP_SEC) {
|
|
324
|
+
if (totalSamples <= 0 || sampleRate <= 0) {
|
|
325
|
+
return [];
|
|
484
326
|
}
|
|
485
|
-
|
|
327
|
+
const chunkSamples = Math.round(chunkDurationSec * sampleRate);
|
|
328
|
+
const overlapSamples = Math.round(overlapSec * sampleRate);
|
|
329
|
+
if (chunkSamples <= 0) {
|
|
330
|
+
return [];
|
|
331
|
+
}
|
|
332
|
+
if (totalSamples <= chunkSamples) {
|
|
333
|
+
return [
|
|
334
|
+
{
|
|
335
|
+
index: 0,
|
|
336
|
+
state: "pending",
|
|
337
|
+
inputStartSample: 0,
|
|
338
|
+
inputEndSample: totalSamples,
|
|
339
|
+
overlapBefore: 0,
|
|
340
|
+
overlapAfter: 0,
|
|
341
|
+
outputBuffer: null,
|
|
342
|
+
outputLength: 0,
|
|
343
|
+
priority: 0,
|
|
344
|
+
retryCount: 0
|
|
345
|
+
}
|
|
346
|
+
];
|
|
347
|
+
}
|
|
348
|
+
const chunks = [];
|
|
349
|
+
let start = 0;
|
|
350
|
+
let index = 0;
|
|
351
|
+
while (start < totalSamples) {
|
|
352
|
+
const isFirst = index === 0;
|
|
353
|
+
const nominalEnd = Math.min(start + chunkSamples, totalSamples);
|
|
354
|
+
const isLast = nominalEnd >= totalSamples;
|
|
355
|
+
const overlapBefore = isFirst ? 0 : Math.min(overlapSamples, start);
|
|
356
|
+
const overlapAfter = isLast ? 0 : Math.min(overlapSamples, totalSamples - nominalEnd);
|
|
357
|
+
const inputStart = start - overlapBefore;
|
|
358
|
+
const inputEnd = Math.min(nominalEnd + overlapAfter, totalSamples);
|
|
359
|
+
chunks.push({
|
|
360
|
+
index,
|
|
361
|
+
state: "pending",
|
|
362
|
+
inputStartSample: inputStart,
|
|
363
|
+
inputEndSample: inputEnd,
|
|
364
|
+
overlapBefore,
|
|
365
|
+
overlapAfter,
|
|
366
|
+
outputBuffer: null,
|
|
367
|
+
outputLength: 0,
|
|
368
|
+
priority: 0,
|
|
369
|
+
retryCount: 0
|
|
370
|
+
});
|
|
371
|
+
start = nominalEnd;
|
|
372
|
+
index++;
|
|
373
|
+
}
|
|
374
|
+
return chunks;
|
|
486
375
|
}
|
|
487
|
-
function
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
const denom = Math.sqrt(normRef * normSearch);
|
|
506
|
-
const ncc = denom > 1e-10 ? corr / denom : 0;
|
|
507
|
-
if (ncc > bestCorr) {
|
|
508
|
-
bestCorr = ncc;
|
|
509
|
-
bestOffset = offset;
|
|
376
|
+
function extractChunkData(buffer, chunk) {
|
|
377
|
+
const channels = [];
|
|
378
|
+
const length = chunk.inputEndSample - chunk.inputStartSample;
|
|
379
|
+
for (let ch = 0; ch < buffer.numberOfChannels; ch++) {
|
|
380
|
+
const fullChannel = buffer.getChannelData(ch);
|
|
381
|
+
const chunkData = new Float32Array(length);
|
|
382
|
+
chunkData.set(fullChannel.subarray(chunk.inputStartSample, chunk.inputEndSample));
|
|
383
|
+
channels.push(chunkData);
|
|
384
|
+
}
|
|
385
|
+
return channels;
|
|
386
|
+
}
|
|
387
|
+
function getChunkIndexForSample(chunks, sample) {
|
|
388
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
389
|
+
const chunk = chunks[i];
|
|
390
|
+
const nominalStart = chunk.inputStartSample + chunk.overlapBefore;
|
|
391
|
+
const nominalEnd = chunk.inputEndSample - chunk.overlapAfter;
|
|
392
|
+
if (sample >= nominalStart && sample < nominalEnd) {
|
|
393
|
+
return i;
|
|
510
394
|
}
|
|
511
395
|
}
|
|
512
|
-
return
|
|
396
|
+
return Math.max(0, chunks.length - 1);
|
|
513
397
|
}
|
|
514
|
-
function
|
|
515
|
-
|
|
516
|
-
|
|
398
|
+
function getChunkIndexForTime(chunks, timeSeconds, sampleRate) {
|
|
399
|
+
const sample = Math.round(timeSeconds * sampleRate);
|
|
400
|
+
return getChunkIndexForSample(chunks, sample);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// src/stretcher/priority-queue.ts
|
|
404
|
+
function createPriorityQueue(compareFn) {
|
|
405
|
+
const heap = [];
|
|
406
|
+
function parent(i) {
|
|
407
|
+
return Math.floor((i - 1) / 2);
|
|
517
408
|
}
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
return { output: channels.map(() => new Float32Array(0)), length: 0 };
|
|
409
|
+
function left(i) {
|
|
410
|
+
return 2 * i + 1;
|
|
521
411
|
}
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
const numFrames = Math.floor((inputLength - frameSize) / analysisHop) + 1;
|
|
525
|
-
if (numFrames <= 0) {
|
|
526
|
-
return {
|
|
527
|
-
output: channels.map((ch) => new Float32Array(ch)),
|
|
528
|
-
length: inputLength
|
|
529
|
-
};
|
|
530
|
-
}
|
|
531
|
-
const estimatedOutputLength = (numFrames - 1) * synthesisHop + frameSize;
|
|
532
|
-
const outputChannels = channels.map(
|
|
533
|
-
() => new Float32Array(estimatedOutputLength)
|
|
534
|
-
);
|
|
535
|
-
const windowFunc = createHannWindow(frameSize);
|
|
536
|
-
const normBuffer = new Float32Array(estimatedOutputLength);
|
|
537
|
-
const prevOutputFrame = channels.map(() => new Float32Array(frameSize));
|
|
538
|
-
let inputPos = 0;
|
|
539
|
-
let outputPos = 0;
|
|
540
|
-
let actualOutputLength = 0;
|
|
541
|
-
for (let frame = 0; frame < numFrames; frame++) {
|
|
542
|
-
if (inputPos + frameSize > inputLength) break;
|
|
543
|
-
let actualInputPos = inputPos;
|
|
544
|
-
if (frame > 0 && tolerance > 0) {
|
|
545
|
-
const searchStart = Math.max(0, inputPos - tolerance);
|
|
546
|
-
const searchEnd = Math.min(inputLength - frameSize, inputPos + tolerance);
|
|
547
|
-
const searchRange = searchEnd - searchStart;
|
|
548
|
-
if (searchRange > 0) {
|
|
549
|
-
const refChannel = prevOutputFrame[0];
|
|
550
|
-
const inputChannel = channels[0];
|
|
551
|
-
const overlapStart = frameSize - synthesisHop;
|
|
552
|
-
const overlapSize = Math.min(synthesisHop, frameSize - overlapStart);
|
|
553
|
-
const refSlice = refChannel.subarray(
|
|
554
|
-
overlapStart,
|
|
555
|
-
overlapStart + overlapSize
|
|
556
|
-
);
|
|
557
|
-
const searchSlice = inputChannel.subarray(
|
|
558
|
-
searchStart,
|
|
559
|
-
searchEnd + overlapSize
|
|
560
|
-
);
|
|
561
|
-
const bestOffset = findBestOffset(
|
|
562
|
-
refSlice,
|
|
563
|
-
searchSlice,
|
|
564
|
-
overlapSize,
|
|
565
|
-
Math.min(searchRange, searchSlice.length - overlapSize)
|
|
566
|
-
);
|
|
567
|
-
actualInputPos = searchStart + bestOffset;
|
|
568
|
-
}
|
|
569
|
-
}
|
|
570
|
-
for (let ch = 0; ch < channels.length; ch++) {
|
|
571
|
-
const input = channels[ch];
|
|
572
|
-
const output = outputChannels[ch];
|
|
573
|
-
const prevFrame = prevOutputFrame[ch];
|
|
574
|
-
for (let i = 0; i < frameSize; i++) {
|
|
575
|
-
const inIdx = actualInputPos + i;
|
|
576
|
-
if (inIdx >= inputLength) break;
|
|
577
|
-
const outIdx = outputPos + i;
|
|
578
|
-
if (outIdx >= estimatedOutputLength) break;
|
|
579
|
-
const sample = input[inIdx];
|
|
580
|
-
output[outIdx] += sample * windowFunc[i];
|
|
581
|
-
prevFrame[i] = sample;
|
|
582
|
-
}
|
|
583
|
-
}
|
|
584
|
-
for (let i = 0; i < frameSize; i++) {
|
|
585
|
-
const outIdx = outputPos + i;
|
|
586
|
-
if (outIdx >= estimatedOutputLength) break;
|
|
587
|
-
normBuffer[outIdx] += windowFunc[i];
|
|
588
|
-
}
|
|
589
|
-
inputPos += analysisHop;
|
|
590
|
-
outputPos += synthesisHop;
|
|
591
|
-
actualOutputLength = Math.min(outputPos + frameSize, estimatedOutputLength);
|
|
592
|
-
}
|
|
593
|
-
for (let ch = 0; ch < outputChannels.length; ch++) {
|
|
594
|
-
const output = outputChannels[ch];
|
|
595
|
-
for (let i = 0; i < actualOutputLength; i++) {
|
|
596
|
-
const norm = normBuffer[i];
|
|
597
|
-
if (norm > 1e-8) {
|
|
598
|
-
output[i] /= norm;
|
|
599
|
-
}
|
|
600
|
-
}
|
|
601
|
-
}
|
|
602
|
-
const trimmedOutput = outputChannels.map(
|
|
603
|
-
(ch) => ch.subarray(0, actualOutputLength)
|
|
604
|
-
);
|
|
605
|
-
return { output: trimmedOutput, length: actualOutputLength };
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
// src/stretcher/main-thread-processor.ts
|
|
609
|
-
function createMainThreadProcessor(onResult, onError) {
|
|
610
|
-
let terminated = false;
|
|
611
|
-
const postTimes = /* @__PURE__ */ new Map();
|
|
612
|
-
let currentChunkIndex = null;
|
|
613
|
-
let cancelledChunks = /* @__PURE__ */ new Set();
|
|
614
|
-
let busy = false;
|
|
615
|
-
return {
|
|
616
|
-
postConvert(chunkIndex, inputData, tempo, sampleRate) {
|
|
617
|
-
if (terminated) return;
|
|
618
|
-
busy = true;
|
|
619
|
-
currentChunkIndex = chunkIndex;
|
|
620
|
-
postTimes.set(chunkIndex, performance.now());
|
|
621
|
-
setTimeout(() => {
|
|
622
|
-
if (terminated) return;
|
|
623
|
-
if (cancelledChunks.has(chunkIndex)) {
|
|
624
|
-
cancelledChunks.delete(chunkIndex);
|
|
625
|
-
busy = false;
|
|
626
|
-
currentChunkIndex = null;
|
|
627
|
-
onResult({ type: "cancelled", chunkIndex });
|
|
628
|
-
return;
|
|
629
|
-
}
|
|
630
|
-
try {
|
|
631
|
-
const result = wsolaTimeStretch(inputData, tempo, sampleRate);
|
|
632
|
-
if (cancelledChunks.has(chunkIndex)) {
|
|
633
|
-
cancelledChunks.delete(chunkIndex);
|
|
634
|
-
busy = false;
|
|
635
|
-
currentChunkIndex = null;
|
|
636
|
-
onResult({ type: "cancelled", chunkIndex });
|
|
637
|
-
return;
|
|
638
|
-
}
|
|
639
|
-
busy = false;
|
|
640
|
-
currentChunkIndex = null;
|
|
641
|
-
onResult({
|
|
642
|
-
type: "result",
|
|
643
|
-
chunkIndex,
|
|
644
|
-
outputData: result.output,
|
|
645
|
-
outputLength: result.length
|
|
646
|
-
});
|
|
647
|
-
} catch (err) {
|
|
648
|
-
busy = false;
|
|
649
|
-
currentChunkIndex = null;
|
|
650
|
-
onError({
|
|
651
|
-
type: "error",
|
|
652
|
-
chunkIndex,
|
|
653
|
-
error: String(err)
|
|
654
|
-
});
|
|
655
|
-
}
|
|
656
|
-
}, 0);
|
|
657
|
-
},
|
|
658
|
-
cancelCurrent() {
|
|
659
|
-
if (terminated) return;
|
|
660
|
-
if (currentChunkIndex !== null) {
|
|
661
|
-
cancelledChunks.add(currentChunkIndex);
|
|
662
|
-
}
|
|
663
|
-
},
|
|
664
|
-
cancelChunk(chunkIndex) {
|
|
665
|
-
if (terminated) return;
|
|
666
|
-
cancelledChunks.add(chunkIndex);
|
|
667
|
-
},
|
|
668
|
-
isBusy() {
|
|
669
|
-
return busy;
|
|
670
|
-
},
|
|
671
|
-
hasCapacity() {
|
|
672
|
-
return !busy;
|
|
673
|
-
},
|
|
674
|
-
getCurrentChunkIndex() {
|
|
675
|
-
return currentChunkIndex;
|
|
676
|
-
},
|
|
677
|
-
getLastPostTime() {
|
|
678
|
-
let latest = null;
|
|
679
|
-
for (const t of postTimes.values()) {
|
|
680
|
-
if (latest === null || t > latest) {
|
|
681
|
-
latest = t;
|
|
682
|
-
}
|
|
683
|
-
}
|
|
684
|
-
return latest;
|
|
685
|
-
},
|
|
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;
|
|
412
|
+
function right(i) {
|
|
413
|
+
return 2 * i + 2;
|
|
709
414
|
}
|
|
710
415
|
function swap(i, j) {
|
|
711
416
|
const tmp = heap[i];
|
|
@@ -798,14 +503,8 @@ function createConversionScheduler(chunks, workerManager, extractChunkData2, sam
|
|
|
798
503
|
const forwardWeight = options?.forwardWeight ?? PRIORITY_FORWARD_WEIGHT;
|
|
799
504
|
const backwardWeight = options?.backwardWeight ?? PRIORITY_BACKWARD_WEIGHT;
|
|
800
505
|
const cancelDistThreshold = options?.cancelDistanceThreshold ?? CANCEL_DISTANCE_THRESHOLD;
|
|
801
|
-
const keepAhead = options?.keepAheadChunks ?? Math.max(
|
|
802
|
-
|
|
803
|
-
Math.ceil(KEEP_AHEAD_SECONDS / CHUNK_DURATION_SEC)
|
|
804
|
-
);
|
|
805
|
-
const keepBehind = options?.keepBehindChunks ?? Math.max(
|
|
806
|
-
KEEP_BEHIND_CHUNKS,
|
|
807
|
-
Math.ceil(KEEP_BEHIND_SECONDS / CHUNK_DURATION_SEC)
|
|
808
|
-
);
|
|
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));
|
|
809
508
|
function isInActiveWindow(chunkIndex, playheadIndex) {
|
|
810
509
|
const dist = chunkIndex - playheadIndex;
|
|
811
510
|
return dist <= keepAhead && dist >= -keepBehind;
|
|
@@ -814,9 +513,7 @@ function createConversionScheduler(chunks, workerManager, extractChunkData2, sam
|
|
|
814
513
|
let currentChunkIdx = 0;
|
|
815
514
|
let previousTempoCache = null;
|
|
816
515
|
let disposed = false;
|
|
817
|
-
const queue = createPriorityQueue(
|
|
818
|
-
(a, b) => a.priority - b.priority
|
|
819
|
-
);
|
|
516
|
+
const queue = createPriorityQueue((a, b) => a.priority - b.priority);
|
|
820
517
|
function calcPriority(chunkIndex, playheadIndex) {
|
|
821
518
|
const distance = chunkIndex - playheadIndex;
|
|
822
519
|
if (distance >= 0) {
|
|
@@ -861,17 +558,17 @@ function createConversionScheduler(chunks, workerManager, extractChunkData2, sam
|
|
|
861
558
|
}
|
|
862
559
|
nextChunk.state = "converting";
|
|
863
560
|
const data = extractChunkData2(nextChunk.index);
|
|
864
|
-
workerManager.postConvert(
|
|
865
|
-
nextChunk.index,
|
|
866
|
-
data,
|
|
867
|
-
currentTempo,
|
|
868
|
-
sampleRate
|
|
869
|
-
);
|
|
561
|
+
workerManager.postConvert(nextChunk.index, data, currentTempo, sampleRate);
|
|
870
562
|
}
|
|
871
563
|
}
|
|
872
564
|
function handleResult(chunkIndex, outputData, outputLength) {
|
|
565
|
+
if (disposed) return;
|
|
873
566
|
const chunk = chunks[chunkIndex];
|
|
874
567
|
if (!chunk) return;
|
|
568
|
+
if (chunk.state !== "converting") {
|
|
569
|
+
dispatchNext();
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
875
572
|
chunk.state = "ready";
|
|
876
573
|
chunk.outputBuffer = outputData;
|
|
877
574
|
chunk.outputLength = outputLength;
|
|
@@ -879,6 +576,7 @@ function createConversionScheduler(chunks, workerManager, extractChunkData2, sam
|
|
|
879
576
|
dispatchNext();
|
|
880
577
|
}
|
|
881
578
|
function handleError(chunkIndex, error) {
|
|
579
|
+
if (disposed) return;
|
|
882
580
|
const chunk = chunks[chunkIndex];
|
|
883
581
|
if (!chunk) return;
|
|
884
582
|
chunk.retryCount++;
|
|
@@ -972,281 +670,616 @@ function createConversionScheduler(chunks, workerManager, extractChunkData2, sam
|
|
|
972
670
|
};
|
|
973
671
|
}
|
|
974
672
|
|
|
975
|
-
// src/stretcher/
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
const
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
673
|
+
// src/stretcher/wsola.ts
|
|
674
|
+
function createHannWindow(size) {
|
|
675
|
+
const window = new Float32Array(size);
|
|
676
|
+
for (let i = 0; i < size; i++) {
|
|
677
|
+
window[i] = 0.5 * (1 - Math.cos(2 * Math.PI * i / (size - 1)));
|
|
678
|
+
}
|
|
679
|
+
return window;
|
|
680
|
+
}
|
|
681
|
+
function findBestOffset(ref, search, overlapSize, maxOffset) {
|
|
682
|
+
let bestOffset = 0;
|
|
683
|
+
let bestCorr = -Infinity;
|
|
684
|
+
const searchLen = search.length;
|
|
685
|
+
const refLen = ref.length;
|
|
686
|
+
const len = Math.min(overlapSize, refLen);
|
|
687
|
+
for (let offset = 0; offset <= maxOffset; offset++) {
|
|
688
|
+
if (offset + len > searchLen) break;
|
|
689
|
+
let corr = 0;
|
|
690
|
+
let normRef = 0;
|
|
691
|
+
let normSearch = 0;
|
|
692
|
+
for (let i = 0; i < len; i++) {
|
|
693
|
+
const r = ref[i];
|
|
694
|
+
const s = search[offset + i];
|
|
695
|
+
corr += r * s;
|
|
696
|
+
normRef += r * r;
|
|
697
|
+
normSearch += s * s;
|
|
698
|
+
}
|
|
699
|
+
const denom = Math.sqrt(normRef * normSearch);
|
|
700
|
+
const ncc = denom > 1e-10 ? corr / denom : 0;
|
|
701
|
+
if (ncc > bestCorr) {
|
|
702
|
+
bestCorr = ncc;
|
|
703
|
+
bestOffset = offset;
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
return bestOffset;
|
|
707
|
+
}
|
|
708
|
+
function wsolaTimeStretch(channels, tempo, _sampleRate, frameSize = WSOLA_FRAME_SIZE, hopSize = WSOLA_HOP_SIZE, tolerance = WSOLA_TOLERANCE) {
|
|
709
|
+
if (channels.length === 0) {
|
|
710
|
+
return { output: [], length: 0 };
|
|
711
|
+
}
|
|
712
|
+
const inputLength = channels[0].length;
|
|
713
|
+
if (inputLength === 0) {
|
|
714
|
+
return { output: channels.map(() => new Float32Array(0)), length: 0 };
|
|
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
|
+
}
|
|
723
|
+
const synthesisHop = hopSize;
|
|
724
|
+
const analysisHop = Math.round(hopSize * tempo);
|
|
725
|
+
const numFrames = Math.floor((inputLength - frameSize) / analysisHop) + 1;
|
|
726
|
+
if (numFrames <= 0) {
|
|
727
|
+
return {
|
|
728
|
+
output: channels.map((ch) => new Float32Array(ch)),
|
|
729
|
+
length: inputLength
|
|
730
|
+
};
|
|
731
|
+
}
|
|
732
|
+
const estimatedOutputLength = (numFrames - 1) * synthesisHop + frameSize;
|
|
733
|
+
const outputChannels = channels.map(() => new Float32Array(estimatedOutputLength));
|
|
734
|
+
const windowFunc = createHannWindow(frameSize);
|
|
735
|
+
const normBuffer = new Float32Array(estimatedOutputLength);
|
|
736
|
+
const prevOutputFrame = channels.map(() => new Float32Array(frameSize));
|
|
737
|
+
let inputPos = 0;
|
|
738
|
+
let outputPos = 0;
|
|
739
|
+
let actualOutputLength = 0;
|
|
740
|
+
for (let frame = 0; frame < numFrames; frame++) {
|
|
741
|
+
if (inputPos + frameSize > inputLength) break;
|
|
742
|
+
let actualInputPos = inputPos;
|
|
743
|
+
if (frame > 0 && tolerance > 0) {
|
|
744
|
+
const searchStart = Math.max(0, inputPos - tolerance);
|
|
745
|
+
const searchEnd = Math.min(inputLength - frameSize, inputPos + tolerance);
|
|
746
|
+
const searchRange = searchEnd - searchStart;
|
|
747
|
+
if (searchRange > 0) {
|
|
748
|
+
const refChannel = prevOutputFrame[0];
|
|
749
|
+
const inputChannel = channels[0];
|
|
750
|
+
const overlapStart = frameSize - synthesisHop;
|
|
751
|
+
const overlapSize = Math.min(synthesisHop, frameSize - overlapStart);
|
|
752
|
+
const refSlice = refChannel.subarray(overlapStart, overlapStart + overlapSize);
|
|
753
|
+
const searchSlice = inputChannel.subarray(searchStart, searchEnd + overlapSize);
|
|
754
|
+
const bestOffset = findBestOffset(
|
|
755
|
+
refSlice,
|
|
756
|
+
searchSlice,
|
|
757
|
+
overlapSize,
|
|
758
|
+
Math.min(searchRange, searchSlice.length - overlapSize)
|
|
759
|
+
);
|
|
760
|
+
actualInputPos = searchStart + bestOffset;
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
for (let ch = 0; ch < channels.length; ch++) {
|
|
764
|
+
const input = channels[ch];
|
|
765
|
+
const output = outputChannels[ch];
|
|
766
|
+
const prevFrame = prevOutputFrame[ch];
|
|
767
|
+
for (let i = 0; i < frameSize; i++) {
|
|
768
|
+
const inIdx = actualInputPos + i;
|
|
769
|
+
if (inIdx >= inputLength) break;
|
|
770
|
+
const outIdx = outputPos + i;
|
|
771
|
+
if (outIdx >= estimatedOutputLength) break;
|
|
772
|
+
const sample = input[inIdx];
|
|
773
|
+
output[outIdx] += sample * windowFunc[i];
|
|
774
|
+
prevFrame[i] = sample * windowFunc[i];
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
for (let i = 0; i < frameSize; i++) {
|
|
778
|
+
const outIdx = outputPos + i;
|
|
779
|
+
if (outIdx >= estimatedOutputLength) break;
|
|
780
|
+
normBuffer[outIdx] += windowFunc[i];
|
|
781
|
+
}
|
|
782
|
+
inputPos += analysisHop;
|
|
783
|
+
outputPos += synthesisHop;
|
|
784
|
+
actualOutputLength = Math.min(outputPos + frameSize, estimatedOutputLength);
|
|
785
|
+
}
|
|
786
|
+
for (let ch = 0; ch < outputChannels.length; ch++) {
|
|
787
|
+
const output = outputChannels[ch];
|
|
788
|
+
for (let i = 0; i < actualOutputLength; i++) {
|
|
789
|
+
const norm = normBuffer[i];
|
|
790
|
+
if (norm > 1e-8) {
|
|
791
|
+
output[i] /= norm;
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
const trimmedOutput = outputChannels.map((ch) => ch.subarray(0, actualOutputLength));
|
|
796
|
+
return { output: trimmedOutput, length: actualOutputLength };
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
// src/stretcher/main-thread-processor.ts
|
|
800
|
+
function createMainThreadProcessor(onResult, onError) {
|
|
801
|
+
let terminated = false;
|
|
802
|
+
const postTimes = /* @__PURE__ */ new Map();
|
|
803
|
+
let currentChunkIndex = null;
|
|
804
|
+
const cancelledChunks = /* @__PURE__ */ new Set();
|
|
805
|
+
let busy = false;
|
|
806
|
+
return {
|
|
807
|
+
postConvert(chunkIndex, inputData, tempo, sampleRate) {
|
|
808
|
+
if (terminated) return;
|
|
809
|
+
busy = true;
|
|
810
|
+
currentChunkIndex = chunkIndex;
|
|
811
|
+
postTimes.set(chunkIndex, performance.now());
|
|
812
|
+
setTimeout(() => {
|
|
813
|
+
if (terminated) return;
|
|
814
|
+
if (cancelledChunks.has(chunkIndex)) {
|
|
815
|
+
cancelledChunks.delete(chunkIndex);
|
|
816
|
+
busy = false;
|
|
817
|
+
currentChunkIndex = null;
|
|
818
|
+
onResult({ type: "cancelled", chunkIndex });
|
|
819
|
+
return;
|
|
820
|
+
}
|
|
821
|
+
try {
|
|
822
|
+
const result = wsolaTimeStretch(inputData, tempo, sampleRate);
|
|
823
|
+
if (cancelledChunks.has(chunkIndex)) {
|
|
824
|
+
cancelledChunks.delete(chunkIndex);
|
|
825
|
+
busy = false;
|
|
826
|
+
currentChunkIndex = null;
|
|
827
|
+
onResult({ type: "cancelled", chunkIndex });
|
|
828
|
+
return;
|
|
829
|
+
}
|
|
830
|
+
busy = false;
|
|
831
|
+
currentChunkIndex = null;
|
|
832
|
+
onResult({
|
|
833
|
+
type: "result",
|
|
834
|
+
chunkIndex,
|
|
835
|
+
outputData: result.output,
|
|
836
|
+
outputLength: result.length
|
|
837
|
+
});
|
|
838
|
+
} catch (err) {
|
|
839
|
+
busy = false;
|
|
840
|
+
currentChunkIndex = null;
|
|
841
|
+
onError({
|
|
842
|
+
type: "error",
|
|
843
|
+
chunkIndex,
|
|
844
|
+
error: String(err)
|
|
845
|
+
});
|
|
846
|
+
}
|
|
847
|
+
}, 0);
|
|
848
|
+
},
|
|
849
|
+
cancelCurrent() {
|
|
850
|
+
if (terminated) return;
|
|
851
|
+
if (currentChunkIndex !== null) {
|
|
852
|
+
cancelledChunks.add(currentChunkIndex);
|
|
853
|
+
}
|
|
854
|
+
},
|
|
855
|
+
cancelChunk(chunkIndex) {
|
|
856
|
+
if (terminated) return;
|
|
857
|
+
cancelledChunks.add(chunkIndex);
|
|
858
|
+
},
|
|
859
|
+
isBusy() {
|
|
860
|
+
return busy;
|
|
861
|
+
},
|
|
862
|
+
hasCapacity() {
|
|
863
|
+
return !busy;
|
|
864
|
+
},
|
|
865
|
+
getCurrentChunkIndex() {
|
|
866
|
+
return currentChunkIndex;
|
|
867
|
+
},
|
|
868
|
+
getLastPostTime() {
|
|
869
|
+
let latest = null;
|
|
870
|
+
for (const t of postTimes.values()) {
|
|
871
|
+
if (latest === null || t > latest) {
|
|
872
|
+
latest = t;
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
return latest;
|
|
876
|
+
},
|
|
877
|
+
getPostTimeForChunk(chunkIndex) {
|
|
878
|
+
return postTimes.get(chunkIndex) ?? null;
|
|
879
|
+
},
|
|
880
|
+
terminate() {
|
|
881
|
+
if (terminated) return;
|
|
882
|
+
terminated = true;
|
|
883
|
+
cancelledChunks.clear();
|
|
884
|
+
postTimes.clear();
|
|
885
|
+
}
|
|
886
|
+
};
|
|
887
|
+
}
|
|
888
|
+
|
|
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)));
|
|
917
|
+
}
|
|
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;
|
|
939
|
+
}
|
|
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;
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
return bestOffset;
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
function wsolaTimeStretch(channels, tempo, sampleRate) {
|
|
951
|
+
if (channels.length === 0) {
|
|
952
|
+
return { output: [], length: 0 };
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
var inputLength = channels[0].length;
|
|
956
|
+
if (inputLength === 0) {
|
|
957
|
+
return { output: channels.map(function() { return new Float32Array(0); }), length: 0 };
|
|
958
|
+
}
|
|
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
|
|
965
|
+
};
|
|
966
|
+
}
|
|
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
|
+
};
|
|
977
|
+
}
|
|
978
|
+
|
|
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);
|
|
1097
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);
|
|
1098
1144
|
};
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
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
|
+
}
|
|
1113
1177
|
}
|
|
1114
1178
|
};
|
|
1115
|
-
|
|
1116
|
-
if (crossfadeSec > 0 && currentGain) {
|
|
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();
|
|
1179
|
+
slot.worker = worker;
|
|
1150
1180
|
}
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1181
|
+
for (let i = 0; i < poolSize; i++) {
|
|
1182
|
+
const slot = {
|
|
1183
|
+
worker: null,
|
|
1184
|
+
busy: false,
|
|
1185
|
+
currentChunkIndex: null,
|
|
1186
|
+
crashCount: 0
|
|
1187
|
+
};
|
|
1188
|
+
slots.push(slot);
|
|
1189
|
+
spawnWorkerForSlot(slot);
|
|
1154
1190
|
}
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
paused = false;
|
|
1159
|
-
pausedPosition = 0;
|
|
1160
|
-
cancelTransition();
|
|
1161
|
-
stopCurrentSource();
|
|
1162
|
-
stopNextSource();
|
|
1163
|
-
stopLookahead();
|
|
1191
|
+
if (!allDeadFired && isAllDead()) {
|
|
1192
|
+
allDeadFired = true;
|
|
1193
|
+
onAllDead?.();
|
|
1164
1194
|
}
|
|
1165
|
-
function
|
|
1166
|
-
|
|
1195
|
+
function findFreeSlot() {
|
|
1196
|
+
for (const slot of slots) {
|
|
1197
|
+
if (!slot.busy && slot.worker) {
|
|
1198
|
+
return slot;
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
return null;
|
|
1167
1202
|
}
|
|
1168
|
-
function
|
|
1169
|
-
|
|
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;
|
|
1170
1210
|
}
|
|
1171
1211
|
return {
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
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
|
+
);
|
|
1182
1224
|
},
|
|
1183
|
-
|
|
1184
|
-
|
|
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
|
+
}
|
|
1185
1232
|
},
|
|
1186
|
-
|
|
1187
|
-
|
|
1233
|
+
cancelChunk(chunkIndex) {
|
|
1234
|
+
if (terminated) return;
|
|
1235
|
+
const slot = findSlotByChunk(chunkIndex);
|
|
1236
|
+
if (slot?.worker) {
|
|
1237
|
+
slot.worker.postMessage({ type: "cancel", chunkIndex });
|
|
1238
|
+
}
|
|
1188
1239
|
},
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
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();
|
|
1213
1282
|
}
|
|
1214
|
-
return aheadSec;
|
|
1215
|
-
}
|
|
1216
|
-
function getHealth(currentChunkIndex, chunks) {
|
|
1217
|
-
const ahead = getAheadSeconds(currentChunkIndex, chunks);
|
|
1218
|
-
if (ahead >= healthySec) return "healthy";
|
|
1219
|
-
if (ahead >= lowSec) return "low";
|
|
1220
|
-
if (ahead >= criticalSec) return "critical";
|
|
1221
|
-
return "empty";
|
|
1222
|
-
}
|
|
1223
|
-
function shouldEnterBuffering(currentChunkIndex, chunks) {
|
|
1224
|
-
const ahead = getAheadSeconds(currentChunkIndex, chunks);
|
|
1225
|
-
if (ahead >= criticalSec) return false;
|
|
1226
|
-
const nextChunk = chunks[currentChunkIndex + 1];
|
|
1227
|
-
if (nextChunk && nextChunk.state === "ready") return false;
|
|
1228
|
-
const currentChunk = chunks[currentChunkIndex];
|
|
1229
|
-
if (!currentChunk || currentChunk.state !== "ready") return true;
|
|
1230
|
-
return ahead < criticalSec;
|
|
1231
|
-
}
|
|
1232
|
-
function shouldExitBuffering(currentChunkIndex, chunks) {
|
|
1233
|
-
const currentChunk = chunks[currentChunkIndex];
|
|
1234
|
-
if (!currentChunk || currentChunk.state !== "ready") return false;
|
|
1235
|
-
const ahead = getAheadSeconds(currentChunkIndex, chunks);
|
|
1236
|
-
if (ahead >= resumeSec) return true;
|
|
1237
|
-
const nextChunk = chunks[currentChunkIndex + 1];
|
|
1238
|
-
if (nextChunk && nextChunk.state === "ready") return true;
|
|
1239
|
-
const allReady = chunks.every(
|
|
1240
|
-
(c) => c.state === "ready" || c.state === "skipped"
|
|
1241
|
-
);
|
|
1242
|
-
if (allReady) return true;
|
|
1243
|
-
return false;
|
|
1244
|
-
}
|
|
1245
|
-
return {
|
|
1246
|
-
getHealth,
|
|
1247
|
-
getAheadSeconds,
|
|
1248
|
-
shouldEnterBuffering,
|
|
1249
|
-
shouldExitBuffering
|
|
1250
1283
|
};
|
|
1251
1284
|
}
|
|
1252
1285
|
|
|
@@ -1257,7 +1290,7 @@ function trimOverlap(outputData, outputLength, chunk, sampleRate) {
|
|
|
1257
1290
|
return { data: outputData, length: outputLength };
|
|
1258
1291
|
}
|
|
1259
1292
|
const ratio = outputLength / inputLength;
|
|
1260
|
-
const crossfadeKeep = Math.round(CROSSFADE_SEC * sampleRate);
|
|
1293
|
+
const crossfadeKeep = Math.round(CROSSFADE_SEC * sampleRate * Math.min(1, ratio));
|
|
1261
1294
|
const overlapBeforeOutput = Math.round(chunk.overlapBefore * ratio);
|
|
1262
1295
|
const overlapAfterOutput = Math.round(chunk.overlapAfter * ratio);
|
|
1263
1296
|
const keepBefore = chunk.overlapBefore > 0 ? Math.min(crossfadeKeep, overlapBeforeOutput) : 0;
|
|
@@ -1279,26 +1312,29 @@ function createStretcherEngine(ctx, buffer, options) {
|
|
|
1279
1312
|
const {
|
|
1280
1313
|
tempo: initialTempo,
|
|
1281
1314
|
offset = 0,
|
|
1315
|
+
loop: initialLoop = false,
|
|
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";
|
|
1289
1323
|
let currentTempo = initialTempo;
|
|
1324
|
+
let isLooping = initialLoop;
|
|
1290
1325
|
let disposed = false;
|
|
1291
1326
|
let bufferingStartTime = 0;
|
|
1292
1327
|
let currentChunkIndex = 0;
|
|
1293
1328
|
let bufferingResumePosition = null;
|
|
1329
|
+
let expectedTransitionFrom = null;
|
|
1330
|
+
let nextChunkScheduledIndex = null;
|
|
1331
|
+
let pendingTempoChange = false;
|
|
1294
1332
|
const keepAhead = Math.max(KEEP_AHEAD_CHUNKS, Math.ceil(KEEP_AHEAD_SECONDS / CHUNK_DURATION_SEC));
|
|
1295
|
-
const keepBehind = Math.max(
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
sampleRate,
|
|
1299
|
-
CHUNK_DURATION_SEC,
|
|
1300
|
-
OVERLAP_SEC
|
|
1333
|
+
const keepBehind = Math.max(
|
|
1334
|
+
KEEP_BEHIND_CHUNKS,
|
|
1335
|
+
Math.ceil(KEEP_BEHIND_SECONDS / CHUNK_DURATION_SEC)
|
|
1301
1336
|
);
|
|
1337
|
+
const chunks = splitIntoChunks(buffer.length, sampleRate, CHUNK_DURATION_SEC, OVERLAP_SEC);
|
|
1302
1338
|
const monitor = createBufferMonitor();
|
|
1303
1339
|
const poolSize = options.workerPoolSize ?? WORKER_POOL_SIZE;
|
|
1304
1340
|
function handleWorkerResult(response) {
|
|
@@ -1314,11 +1350,7 @@ function createStretcherEngine(ctx, buffer, options) {
|
|
|
1314
1350
|
chunk,
|
|
1315
1351
|
sampleRate
|
|
1316
1352
|
);
|
|
1317
|
-
schedulerInternal._handleResult(
|
|
1318
|
-
response.chunkIndex,
|
|
1319
|
-
trimmed.data,
|
|
1320
|
-
trimmed.length
|
|
1321
|
-
);
|
|
1353
|
+
schedulerInternal._handleResult(response.chunkIndex, trimmed.data, trimmed.length);
|
|
1322
1354
|
}
|
|
1323
1355
|
} else if (response.type === "cancelled") {
|
|
1324
1356
|
const chunk = chunks[response.chunkIndex];
|
|
@@ -1331,10 +1363,7 @@ function createStretcherEngine(ctx, buffer, options) {
|
|
|
1331
1363
|
function handleWorkerError(response) {
|
|
1332
1364
|
if (disposed) return;
|
|
1333
1365
|
if (response.type === "error") {
|
|
1334
|
-
schedulerInternal._handleError(
|
|
1335
|
-
response.chunkIndex,
|
|
1336
|
-
response.error ?? "Unknown error"
|
|
1337
|
-
);
|
|
1366
|
+
schedulerInternal._handleError(response.chunkIndex, response.error ?? "Unknown error");
|
|
1338
1367
|
}
|
|
1339
1368
|
}
|
|
1340
1369
|
function switchToMainThread() {
|
|
@@ -1370,26 +1399,34 @@ function createStretcherEngine(ctx, buffer, options) {
|
|
|
1370
1399
|
if (disposed || phase === "paused" || phase === "ended") return;
|
|
1371
1400
|
advanceToNextChunk();
|
|
1372
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
|
+
}
|
|
1373
1420
|
chunkPlayer.setOnNeedNext(() => {
|
|
1374
1421
|
if (disposed) return;
|
|
1375
|
-
|
|
1376
|
-
if (nextIdx < chunks.length) {
|
|
1377
|
-
const nextChunk = chunks[nextIdx];
|
|
1378
|
-
if (nextChunk.state === "ready" && nextChunk.outputBuffer) {
|
|
1379
|
-
const audioBuffer = createAudioBufferFromChunk(nextChunk);
|
|
1380
|
-
if (audioBuffer) {
|
|
1381
|
-
const curChunk = chunks[currentChunkIndex];
|
|
1382
|
-
const curOutputDuration = curChunk ? curChunk.outputLength / sampleRate : 0;
|
|
1383
|
-
const elapsed = chunkPlayer.getCurrentPosition();
|
|
1384
|
-
const remaining = curOutputDuration - elapsed;
|
|
1385
|
-
const startTime = ctx.currentTime + Math.max(0, remaining);
|
|
1386
|
-
chunkPlayer.scheduleNext(audioBuffer, startTime);
|
|
1387
|
-
}
|
|
1388
|
-
}
|
|
1389
|
-
}
|
|
1422
|
+
tryScheduleNext(currentChunkIndex + 1);
|
|
1390
1423
|
});
|
|
1391
1424
|
chunkPlayer.setOnTransition(() => {
|
|
1392
1425
|
if (disposed) return;
|
|
1426
|
+
if (expectedTransitionFrom !== null && currentChunkIndex !== expectedTransitionFrom) {
|
|
1427
|
+
return;
|
|
1428
|
+
}
|
|
1429
|
+
nextChunkScheduledIndex = null;
|
|
1393
1430
|
const nextIdx = currentChunkIndex + 1;
|
|
1394
1431
|
if (nextIdx < chunks.length) {
|
|
1395
1432
|
currentChunkIndex = nextIdx;
|
|
@@ -1408,21 +1445,12 @@ function createStretcherEngine(ctx, buffer, options) {
|
|
|
1408
1445
|
}
|
|
1409
1446
|
}
|
|
1410
1447
|
if (phase === "playing" && chunkIndex === currentChunkIndex + 1) {
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
const nextChunk = chunks[chunkIndex];
|
|
1418
|
-
if (nextChunk && nextChunk.outputBuffer) {
|
|
1419
|
-
const audioBuffer = createAudioBufferFromChunk(nextChunk);
|
|
1420
|
-
if (audioBuffer) {
|
|
1421
|
-
const startTime = ctx.currentTime + Math.max(0, remaining);
|
|
1422
|
-
chunkPlayer.scheduleNext(audioBuffer, startTime);
|
|
1423
|
-
}
|
|
1424
|
-
}
|
|
1425
|
-
}
|
|
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);
|
|
1426
1454
|
}
|
|
1427
1455
|
}
|
|
1428
1456
|
const allDone = chunks.every(
|
|
@@ -1442,27 +1470,37 @@ function createStretcherEngine(ctx, buffer, options) {
|
|
|
1442
1470
|
function createAudioBufferFromChunk(chunk) {
|
|
1443
1471
|
if (!chunk.outputBuffer || chunk.outputLength === 0) return null;
|
|
1444
1472
|
const numChannels = chunk.outputBuffer.length;
|
|
1445
|
-
const audioBuf = ctx.createBuffer(
|
|
1446
|
-
numChannels,
|
|
1447
|
-
chunk.outputLength,
|
|
1448
|
-
sampleRate
|
|
1449
|
-
);
|
|
1473
|
+
const audioBuf = ctx.createBuffer(numChannels, chunk.outputLength, sampleRate);
|
|
1450
1474
|
for (let ch = 0; ch < numChannels; ch++) {
|
|
1451
1475
|
const channelData = chunk.outputBuffer[ch];
|
|
1452
1476
|
audioBuf.getChannelData(ch).set(channelData.subarray(0, chunk.outputLength));
|
|
1453
1477
|
}
|
|
1454
1478
|
return audioBuf;
|
|
1455
1479
|
}
|
|
1456
|
-
function playCurrentChunk(offsetInBuffer = 0) {
|
|
1480
|
+
function playCurrentChunk(offsetInBuffer = 0, skipFadeIn = false) {
|
|
1457
1481
|
const chunk = chunks[currentChunkIndex];
|
|
1458
1482
|
if (!chunk || chunk.state !== "ready" || !chunk.outputBuffer) return;
|
|
1459
1483
|
const audioBuf = createAudioBufferFromChunk(chunk);
|
|
1460
1484
|
if (!audioBuf) return;
|
|
1461
|
-
chunkPlayer.playChunk(audioBuf, ctx.currentTime, offsetInBuffer);
|
|
1485
|
+
chunkPlayer.playChunk(audioBuf, ctx.currentTime, offsetInBuffer, skipFadeIn);
|
|
1462
1486
|
}
|
|
1463
1487
|
function advanceToNextChunk() {
|
|
1464
1488
|
const nextIdx = currentChunkIndex + 1;
|
|
1465
1489
|
if (nextIdx >= chunks.length) {
|
|
1490
|
+
if (isLooping) {
|
|
1491
|
+
currentChunkIndex = 0;
|
|
1492
|
+
scheduler.handleSeek(0);
|
|
1493
|
+
const chunk2 = chunks[0];
|
|
1494
|
+
if (chunk2 && chunk2.state === "ready") {
|
|
1495
|
+
playCurrentChunk();
|
|
1496
|
+
} else {
|
|
1497
|
+
bufferingResumePosition = 0;
|
|
1498
|
+
enterBuffering("seek");
|
|
1499
|
+
}
|
|
1500
|
+
emitter.emit("loop", void 0);
|
|
1501
|
+
evictDistantChunks();
|
|
1502
|
+
return;
|
|
1503
|
+
}
|
|
1466
1504
|
phase = "ended";
|
|
1467
1505
|
chunkPlayer.stop();
|
|
1468
1506
|
emitter.emit("ended", void 0);
|
|
@@ -1473,6 +1511,7 @@ function createStretcherEngine(ctx, buffer, options) {
|
|
|
1473
1511
|
return;
|
|
1474
1512
|
}
|
|
1475
1513
|
currentChunkIndex = nextIdx;
|
|
1514
|
+
nextChunkScheduledIndex = null;
|
|
1476
1515
|
scheduler.updatePriorities(currentChunkIndex);
|
|
1477
1516
|
const chunk = chunks[currentChunkIndex];
|
|
1478
1517
|
if (chunk && chunk.state === "ready") {
|
|
@@ -1490,6 +1529,7 @@ function createStretcherEngine(ctx, buffer, options) {
|
|
|
1490
1529
|
if (phase === "buffering" && reason !== "tempo-change" && reason !== "seek") return;
|
|
1491
1530
|
phase = "buffering";
|
|
1492
1531
|
bufferingStartTime = performance.now();
|
|
1532
|
+
nextChunkScheduledIndex = null;
|
|
1493
1533
|
chunkPlayer.pause();
|
|
1494
1534
|
emitter.emit("buffering", { reason });
|
|
1495
1535
|
}
|
|
@@ -1551,26 +1591,22 @@ function createStretcherEngine(ctx, buffer, options) {
|
|
|
1551
1591
|
});
|
|
1552
1592
|
}
|
|
1553
1593
|
function getPositionInOriginalBuffer() {
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
const posInOriginal = adjustedPosInChunk * currentTempo;
|
|
1567
|
-
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
|
+
});
|
|
1568
1606
|
}
|
|
1569
1607
|
function getStatus() {
|
|
1570
1608
|
const readyCount = chunks.filter((c) => c.state === "ready").length;
|
|
1571
|
-
const convertingCount = chunks.filter(
|
|
1572
|
-
(c) => c.state === "converting"
|
|
1573
|
-
).length;
|
|
1609
|
+
const convertingCount = chunks.filter((c) => c.state === "converting").length;
|
|
1574
1610
|
const total = chunks.length;
|
|
1575
1611
|
return {
|
|
1576
1612
|
phase,
|
|
@@ -1594,9 +1630,7 @@ function createStretcherEngine(ctx, buffer, options) {
|
|
|
1594
1630
|
function getSnapshot() {
|
|
1595
1631
|
const readyCount = chunks.filter((c) => c.state === "ready").length;
|
|
1596
1632
|
const total = chunks.length;
|
|
1597
|
-
const convertingCount = chunks.filter(
|
|
1598
|
-
(c) => c.state === "converting"
|
|
1599
|
-
).length;
|
|
1633
|
+
const convertingCount = chunks.filter((c) => c.state === "converting").length;
|
|
1600
1634
|
const windowStart = Math.max(0, currentChunkIndex - keepBehind);
|
|
1601
1635
|
const windowEnd = Math.min(total - 1, currentChunkIndex + keepAhead);
|
|
1602
1636
|
const windowSize = windowEnd - windowStart + 1;
|
|
@@ -1631,11 +1665,37 @@ function createStretcherEngine(ctx, buffer, options) {
|
|
|
1631
1665
|
}
|
|
1632
1666
|
function resume() {
|
|
1633
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
|
+
}
|
|
1634
1693
|
const chunk = chunks[currentChunkIndex];
|
|
1635
1694
|
if (chunk && chunk.state === "ready") {
|
|
1695
|
+
const resumePosition = chunkPlayer.getCurrentPosition();
|
|
1636
1696
|
phase = "playing";
|
|
1637
1697
|
chunkPlayer.resume();
|
|
1638
|
-
playCurrentChunk(
|
|
1698
|
+
playCurrentChunk(resumePosition, true);
|
|
1639
1699
|
} else {
|
|
1640
1700
|
enterBuffering("underrun");
|
|
1641
1701
|
}
|
|
@@ -1645,6 +1705,7 @@ function createStretcherEngine(ctx, buffer, options) {
|
|
|
1645
1705
|
const clamped = Math.max(0, Math.min(position, totalDuration));
|
|
1646
1706
|
const newChunkIdx = getChunkIndexForTime(chunks, clamped, sampleRate);
|
|
1647
1707
|
currentChunkIndex = newChunkIdx;
|
|
1708
|
+
nextChunkScheduledIndex = null;
|
|
1648
1709
|
scheduler.handleSeek(newChunkIdx);
|
|
1649
1710
|
const chunk = chunks[newChunkIdx];
|
|
1650
1711
|
if (chunk && chunk.state === "ready") {
|
|
@@ -1661,10 +1722,16 @@ function createStretcherEngine(ctx, buffer, options) {
|
|
|
1661
1722
|
const clampedOffset = Math.min(Math.max(0, bufferOffset), audioBuf.duration - 1e-3);
|
|
1662
1723
|
chunkPlayer.handleSeek(audioBuf, clampedOffset);
|
|
1663
1724
|
}
|
|
1725
|
+
} else if (phase === "paused") {
|
|
1726
|
+
bufferingResumePosition = clamped;
|
|
1664
1727
|
}
|
|
1665
1728
|
} else {
|
|
1666
|
-
|
|
1667
|
-
|
|
1729
|
+
if (phase === "paused") {
|
|
1730
|
+
bufferingResumePosition = clamped;
|
|
1731
|
+
} else {
|
|
1732
|
+
bufferingResumePosition = clamped;
|
|
1733
|
+
enterBuffering("seek");
|
|
1734
|
+
}
|
|
1668
1735
|
}
|
|
1669
1736
|
}
|
|
1670
1737
|
function stop() {
|
|
@@ -1672,18 +1739,42 @@ function createStretcherEngine(ctx, buffer, options) {
|
|
|
1672
1739
|
phase = "ended";
|
|
1673
1740
|
chunkPlayer.stop();
|
|
1674
1741
|
}
|
|
1742
|
+
function setLoop(value) {
|
|
1743
|
+
isLooping = value;
|
|
1744
|
+
}
|
|
1745
|
+
let tempoDebounceTimer = null;
|
|
1675
1746
|
function setTempo(newTempo) {
|
|
1676
1747
|
if (disposed || phase === "ended" || newTempo === currentTempo) return;
|
|
1677
|
-
|
|
1678
|
-
|
|
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
|
+
}
|
|
1679
1762
|
currentTempo = newTempo;
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
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);
|
|
1683
1770
|
}
|
|
1684
1771
|
function dispose() {
|
|
1685
1772
|
if (disposed) return;
|
|
1686
1773
|
disposed = true;
|
|
1774
|
+
if (tempoDebounceTimer !== null) {
|
|
1775
|
+
clearTimeout(tempoDebounceTimer);
|
|
1776
|
+
tempoDebounceTimer = null;
|
|
1777
|
+
}
|
|
1687
1778
|
chunkPlayer.dispose();
|
|
1688
1779
|
scheduler.dispose();
|
|
1689
1780
|
workerManager.terminate();
|
|
@@ -1696,6 +1787,7 @@ function createStretcherEngine(ctx, buffer, options) {
|
|
|
1696
1787
|
seek,
|
|
1697
1788
|
stop,
|
|
1698
1789
|
setTempo,
|
|
1790
|
+
setLoop,
|
|
1699
1791
|
getCurrentPosition: getPositionInOriginalBuffer,
|
|
1700
1792
|
getStatus,
|
|
1701
1793
|
getSnapshot,
|
|
@@ -1706,5 +1798,6 @@ function createStretcherEngine(ctx, buffer, options) {
|
|
|
1706
1798
|
}
|
|
1707
1799
|
|
|
1708
1800
|
exports.createStretcherEngine = createStretcherEngine;
|
|
1709
|
-
|
|
1710
|
-
//# sourceMappingURL=chunk-
|
|
1801
|
+
exports.trimOverlap = trimOverlap;
|
|
1802
|
+
//# sourceMappingURL=chunk-G37HMZEX.cjs.map
|
|
1803
|
+
//# sourceMappingURL=chunk-G37HMZEX.cjs.map
|