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