@meframe/core 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Meframe.d.ts.map +1 -1
- package/dist/Meframe.js +7 -0
- package/dist/Meframe.js.map +1 -1
- package/dist/controllers/PlaybackController.d.ts.map +1 -1
- package/dist/controllers/PlaybackController.js +1 -1
- package/dist/controllers/PlaybackController.js.map +1 -1
- package/dist/orchestrator/AudioExportSession.d.ts +28 -0
- package/dist/orchestrator/AudioExportSession.d.ts.map +1 -0
- package/dist/orchestrator/AudioExportSession.js +95 -0
- package/dist/orchestrator/AudioExportSession.js.map +1 -0
- package/dist/orchestrator/AudioPreviewSession.d.ts +61 -0
- package/dist/orchestrator/AudioPreviewSession.d.ts.map +1 -0
- package/dist/orchestrator/AudioPreviewSession.js +340 -0
- package/dist/orchestrator/AudioPreviewSession.js.map +1 -0
- package/dist/orchestrator/AudioWindowPreparer.d.ts +62 -0
- package/dist/orchestrator/AudioWindowPreparer.d.ts.map +1 -0
- package/dist/orchestrator/AudioWindowPreparer.js +259 -0
- package/dist/orchestrator/AudioWindowPreparer.js.map +1 -0
- package/dist/orchestrator/ExportScheduler.d.ts +2 -2
- package/dist/orchestrator/ExportScheduler.js.map +1 -1
- package/dist/orchestrator/Orchestrator.d.ts +8 -2
- package/dist/orchestrator/Orchestrator.d.ts.map +1 -1
- package/dist/orchestrator/Orchestrator.js +22 -16
- package/dist/orchestrator/Orchestrator.js.map +1 -1
- package/dist/stages/mux/MP4Muxer.js.map +1 -1
- package/dist/stages/mux/MuxManager.d.ts +1 -4
- package/dist/stages/mux/MuxManager.d.ts.map +1 -1
- package/dist/stages/mux/MuxManager.js +1 -1
- package/dist/stages/mux/MuxManager.js.map +1 -1
- package/package.json +1 -1
- package/dist/orchestrator/GlobalAudioSession.d.ts +0 -139
- package/dist/orchestrator/GlobalAudioSession.d.ts.map +0 -1
- package/dist/orchestrator/GlobalAudioSession.js +0 -683
- package/dist/orchestrator/GlobalAudioSession.js.map +0 -1
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
import { OfflineAudioMixer } from "../stages/compose/OfflineAudioMixer.js";
|
|
2
|
+
import { AudioMixBlockCache } from "../cache/AudioMixBlockCache.js";
|
|
3
|
+
class AudioPreviewSession {
|
|
4
|
+
constructor(deps) {
|
|
5
|
+
this.deps = deps;
|
|
6
|
+
this.mixer = new OfflineAudioMixer(deps.cacheManager, () => deps.preparer.getModel());
|
|
7
|
+
}
|
|
8
|
+
mixer;
|
|
9
|
+
audioContext = null;
|
|
10
|
+
volume = 1;
|
|
11
|
+
playbackRate = 1;
|
|
12
|
+
isPlaying = false;
|
|
13
|
+
// Preview strategy (unified):
|
|
14
|
+
// - Always schedule audio in fixed 60s "mix blocks"
|
|
15
|
+
// - Cache 2~3 mixed AudioBuffer blocks (LRU) to accelerate seek
|
|
16
|
+
// - Schedule ahead using AudioContext clock to avoid underrun
|
|
17
|
+
PREVIEW_BLOCK_DURATION_US = 60 * 1e6;
|
|
18
|
+
// 60s
|
|
19
|
+
PREVIEW_BLOCK_CACHE_SIZE = 3;
|
|
20
|
+
PREVIEW_SCHEDULE_AHEAD_SEC = 12;
|
|
21
|
+
// keep enough scheduled audio to hide mixing latency
|
|
22
|
+
PREVIEW_BUFFER_GUARD_US = 2e6;
|
|
23
|
+
// if next block isn't ready near boundary -> buffering
|
|
24
|
+
PREVIEW_BLOCK_FADE_SEC = 0.01;
|
|
25
|
+
// 10ms fade-in/out to avoid click at boundaries
|
|
26
|
+
previewBlockCache = new AudioMixBlockCache(this.PREVIEW_BLOCK_CACHE_SIZE);
|
|
27
|
+
previewScheduleTask = null;
|
|
28
|
+
previewScheduleToken = 0;
|
|
29
|
+
previewMixToken = 0;
|
|
30
|
+
previewNextBlockIndex = 0;
|
|
31
|
+
previewNextScheduleTime = 0;
|
|
32
|
+
// AudioContext time
|
|
33
|
+
previewFirstBlockOffsetUs = 0;
|
|
34
|
+
// seek offset within the first block
|
|
35
|
+
previewLastTimelineUs = 0;
|
|
36
|
+
previewLastStallWarnAt = 0;
|
|
37
|
+
// AudioContext time (sec)
|
|
38
|
+
previewScheduledSources = /* @__PURE__ */ new Set();
|
|
39
|
+
previewMixQueue = Promise.resolve();
|
|
40
|
+
invalidatePreviewMixCache() {
|
|
41
|
+
this.previewBlockCache.clear();
|
|
42
|
+
this.previewMixToken += 1;
|
|
43
|
+
}
|
|
44
|
+
enqueuePreviewMix(work, token) {
|
|
45
|
+
const run = () => work();
|
|
46
|
+
const next = this.previewMixQueue.then(
|
|
47
|
+
() => {
|
|
48
|
+
if (this.previewMixToken !== token) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
return run();
|
|
52
|
+
},
|
|
53
|
+
() => {
|
|
54
|
+
if (this.previewMixToken !== token) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
return run();
|
|
58
|
+
}
|
|
59
|
+
);
|
|
60
|
+
this.previewMixQueue = next.then(
|
|
61
|
+
() => void 0,
|
|
62
|
+
() => void 0
|
|
63
|
+
);
|
|
64
|
+
return next;
|
|
65
|
+
}
|
|
66
|
+
async ensureAudioForTime(timeUs, options) {
|
|
67
|
+
const model = this.deps.preparer.getModel();
|
|
68
|
+
if (!model) return;
|
|
69
|
+
const mode = options?.mode ?? "blocking";
|
|
70
|
+
const blockIndex = Math.floor(Math.max(0, timeUs) / this.PREVIEW_BLOCK_DURATION_US);
|
|
71
|
+
if (mode === "probe") {
|
|
72
|
+
void this.getOrCreateMixedBlock(blockIndex);
|
|
73
|
+
const blockEndUs2 = (blockIndex + 1) * this.PREVIEW_BLOCK_DURATION_US;
|
|
74
|
+
const remainingToBoundaryUs2 = blockEndUs2 - Math.max(0, timeUs);
|
|
75
|
+
const lookaheadUs = Math.floor(this.PREVIEW_SCHEDULE_AHEAD_SEC * 1e6);
|
|
76
|
+
if (remainingToBoundaryUs2 > 0 && remainingToBoundaryUs2 <= lookaheadUs) {
|
|
77
|
+
void this.getOrCreateMixedBlock(blockIndex + 1);
|
|
78
|
+
}
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
await this.getOrCreateMixedBlock(blockIndex);
|
|
82
|
+
const blockEndUs = (blockIndex + 1) * this.PREVIEW_BLOCK_DURATION_US;
|
|
83
|
+
const remainingToBoundaryUs = blockEndUs - Math.max(0, timeUs);
|
|
84
|
+
if (remainingToBoundaryUs > 0 && remainingToBoundaryUs <= this.PREVIEW_BUFFER_GUARD_US) {
|
|
85
|
+
await this.getOrCreateMixedBlock(blockIndex + 1);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
isPreviewMixBlockCached(timeUs) {
|
|
89
|
+
const blockIndex = Math.floor(Math.max(0, timeUs) / this.PREVIEW_BLOCK_DURATION_US);
|
|
90
|
+
return this.previewBlockCache.get(blockIndex) !== null;
|
|
91
|
+
}
|
|
92
|
+
shouldEnterBufferingForUpcomingPreviewAudio(timeUs) {
|
|
93
|
+
const model = this.deps.preparer.getModel();
|
|
94
|
+
if (!model) return false;
|
|
95
|
+
const clampedUs = Math.max(0, timeUs);
|
|
96
|
+
const blockIndex = Math.floor(clampedUs / this.PREVIEW_BLOCK_DURATION_US);
|
|
97
|
+
const nextBlockStartUs = (blockIndex + 1) * this.PREVIEW_BLOCK_DURATION_US;
|
|
98
|
+
if (nextBlockStartUs >= model.durationUs) return false;
|
|
99
|
+
const remainingToBoundaryUs = nextBlockStartUs - clampedUs;
|
|
100
|
+
if (remainingToBoundaryUs > this.PREVIEW_BUFFER_GUARD_US) return false;
|
|
101
|
+
return !this.isPreviewMixBlockCached(nextBlockStartUs);
|
|
102
|
+
}
|
|
103
|
+
async startPlayback(timeUs, audioContext) {
|
|
104
|
+
this.audioContext = audioContext;
|
|
105
|
+
if (audioContext.state === "suspended") {
|
|
106
|
+
await audioContext.resume();
|
|
107
|
+
}
|
|
108
|
+
await this.ensureAudioForTime(timeUs, { mode: "blocking" });
|
|
109
|
+
this.isPlaying = true;
|
|
110
|
+
this.startPreviewBlockScheduling(timeUs, audioContext);
|
|
111
|
+
await this.scheduleNextPreviewBlock(audioContext, this.previewScheduleToken);
|
|
112
|
+
void this.scheduleAudio(timeUs, audioContext);
|
|
113
|
+
}
|
|
114
|
+
stopPlayback() {
|
|
115
|
+
this.isPlaying = false;
|
|
116
|
+
this.stopAllPreviewSources();
|
|
117
|
+
this.cancelPreviewBlockScheduling();
|
|
118
|
+
this.previewMixToken += 1;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Schedule audio chunks ahead of playback cursor.
|
|
122
|
+
*/
|
|
123
|
+
async scheduleAudio(currentTimelineUs, audioContext) {
|
|
124
|
+
if (!this.isPlaying || !this.deps.preparer.getModel() || !this.audioContext) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
this.previewLastTimelineUs = currentTimelineUs;
|
|
128
|
+
if (this.previewScheduleTask) return;
|
|
129
|
+
if (this.previewNextScheduleTime === 0) {
|
|
130
|
+
this.startPreviewBlockScheduling(currentTimelineUs, audioContext);
|
|
131
|
+
}
|
|
132
|
+
const token = this.previewScheduleToken;
|
|
133
|
+
this.previewScheduleTask = this.runPreviewBlockSchedulingLoop(audioContext, token).finally(
|
|
134
|
+
() => {
|
|
135
|
+
if (this.previewScheduleToken === token) {
|
|
136
|
+
this.previewScheduleTask = null;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Reset playback states (called on seek)
|
|
143
|
+
*/
|
|
144
|
+
resetPlaybackStates() {
|
|
145
|
+
this.stopAllPreviewSources();
|
|
146
|
+
this.cancelPreviewBlockScheduling();
|
|
147
|
+
this.previewMixToken += 1;
|
|
148
|
+
}
|
|
149
|
+
reset() {
|
|
150
|
+
this.stopPlayback();
|
|
151
|
+
this.deps.cacheManager.clearAudioCache();
|
|
152
|
+
this.previewBlockCache.clear();
|
|
153
|
+
this.deps.preparer.reset();
|
|
154
|
+
}
|
|
155
|
+
setVolume(volume) {
|
|
156
|
+
this.volume = volume;
|
|
157
|
+
const audioContext = this.audioContext;
|
|
158
|
+
if (!audioContext) return;
|
|
159
|
+
const t = audioContext.currentTime;
|
|
160
|
+
for (const { gain } of this.previewScheduledSources) {
|
|
161
|
+
try {
|
|
162
|
+
gain.gain.cancelScheduledValues(t);
|
|
163
|
+
gain.gain.setValueAtTime(gain.gain.value, t);
|
|
164
|
+
gain.gain.linearRampToValueAtTime(volume, t + 0.01);
|
|
165
|
+
} catch {
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
setPlaybackRate(rate) {
|
|
170
|
+
this.playbackRate = rate;
|
|
171
|
+
this.resetPlaybackStates();
|
|
172
|
+
}
|
|
173
|
+
startPreviewBlockScheduling(startTimelineUs, audioContext) {
|
|
174
|
+
this.cancelPreviewBlockScheduling();
|
|
175
|
+
this.stopAllPreviewSources();
|
|
176
|
+
const clampedUs = Math.max(0, startTimelineUs);
|
|
177
|
+
const blockIndex = Math.floor(clampedUs / this.PREVIEW_BLOCK_DURATION_US);
|
|
178
|
+
const blockStartUs = blockIndex * this.PREVIEW_BLOCK_DURATION_US;
|
|
179
|
+
this.previewFirstBlockOffsetUs = clampedUs - blockStartUs;
|
|
180
|
+
this.previewNextBlockIndex = blockIndex;
|
|
181
|
+
this.previewNextScheduleTime = audioContext.currentTime + 0.02;
|
|
182
|
+
this.previewLastTimelineUs = startTimelineUs;
|
|
183
|
+
}
|
|
184
|
+
cancelPreviewBlockScheduling() {
|
|
185
|
+
this.previewScheduleToken += 1;
|
|
186
|
+
this.previewScheduleTask = null;
|
|
187
|
+
this.previewNextBlockIndex = 0;
|
|
188
|
+
this.previewNextScheduleTime = 0;
|
|
189
|
+
this.previewFirstBlockOffsetUs = 0;
|
|
190
|
+
this.previewLastTimelineUs = 0;
|
|
191
|
+
}
|
|
192
|
+
realignPreviewSchedulingToTimeline(audioContext) {
|
|
193
|
+
const model = this.deps.preparer.getModel();
|
|
194
|
+
if (!model) return;
|
|
195
|
+
const clampedUs = Math.max(0, this.previewLastTimelineUs);
|
|
196
|
+
const blockIndex = Math.floor(clampedUs / this.PREVIEW_BLOCK_DURATION_US);
|
|
197
|
+
const blockStartUs = blockIndex * this.PREVIEW_BLOCK_DURATION_US;
|
|
198
|
+
if (blockStartUs >= model.durationUs) return;
|
|
199
|
+
this.stopAllPreviewSources();
|
|
200
|
+
this.previewFirstBlockOffsetUs = clampedUs - blockStartUs;
|
|
201
|
+
this.previewNextBlockIndex = blockIndex;
|
|
202
|
+
this.previewNextScheduleTime = audioContext.currentTime + 0.02;
|
|
203
|
+
}
|
|
204
|
+
async runPreviewBlockSchedulingLoop(audioContext, token) {
|
|
205
|
+
while (this.isPlaying && this.previewScheduleToken === token) {
|
|
206
|
+
const model = this.deps.preparer.getModel();
|
|
207
|
+
if (!model) return;
|
|
208
|
+
const nextBlockStartUs = this.previewNextBlockIndex * this.PREVIEW_BLOCK_DURATION_US;
|
|
209
|
+
if (nextBlockStartUs >= model.durationUs) {
|
|
210
|
+
this.previewNextScheduleTime = audioContext.currentTime + this.PREVIEW_SCHEDULE_AHEAD_SEC;
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
const scheduleAheadTime = audioContext.currentTime + this.PREVIEW_SCHEDULE_AHEAD_SEC;
|
|
214
|
+
if (this.previewNextScheduleTime >= scheduleAheadTime) return;
|
|
215
|
+
const prevBlockIndex = this.previewNextBlockIndex;
|
|
216
|
+
const prevScheduleTime = this.previewNextScheduleTime;
|
|
217
|
+
await this.scheduleNextPreviewBlock(audioContext, token);
|
|
218
|
+
if (this.previewScheduleToken === token && this.previewNextBlockIndex === prevBlockIndex && this.previewNextScheduleTime === prevScheduleTime) {
|
|
219
|
+
const now = audioContext.currentTime;
|
|
220
|
+
if (now - this.previewLastStallWarnAt >= 1) {
|
|
221
|
+
this.previewLastStallWarnAt = now;
|
|
222
|
+
console.warn("[AudioPreviewSession] scheduling stalled; stop loop to avoid spin", {
|
|
223
|
+
prevBlockIndex,
|
|
224
|
+
prevScheduleTime,
|
|
225
|
+
now
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
async getOrCreateMixedBlock(blockIndex) {
|
|
233
|
+
const model = this.deps.preparer.getModel();
|
|
234
|
+
if (!model) return null;
|
|
235
|
+
const token = this.previewMixToken;
|
|
236
|
+
return await this.previewBlockCache.getOrCreate(blockIndex, async () => {
|
|
237
|
+
return await this.enqueuePreviewMix(async () => {
|
|
238
|
+
const startUs = blockIndex * this.PREVIEW_BLOCK_DURATION_US;
|
|
239
|
+
const endUs = Math.min(startUs + this.PREVIEW_BLOCK_DURATION_US, model.durationUs);
|
|
240
|
+
await this.deps.preparer.ensureAudioForTimeRange(startUs, endUs, {
|
|
241
|
+
mode: "blocking",
|
|
242
|
+
loadResource: true
|
|
243
|
+
});
|
|
244
|
+
const mixed = await this.mixer.mix(startUs, endUs);
|
|
245
|
+
this.deps.cacheManager.clearAudioCache();
|
|
246
|
+
return mixed;
|
|
247
|
+
}, token);
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
async scheduleNextPreviewBlock(audioContext, token) {
|
|
251
|
+
const model = this.deps.preparer.getModel();
|
|
252
|
+
if (!this.isPlaying || !model) return;
|
|
253
|
+
if (this.previewScheduleToken !== token) return;
|
|
254
|
+
if (this.previewNextScheduleTime < audioContext.currentTime + 0.01) {
|
|
255
|
+
this.realignPreviewSchedulingToTimeline(audioContext);
|
|
256
|
+
}
|
|
257
|
+
const blockIndex = this.previewNextBlockIndex;
|
|
258
|
+
const blockStartUs = blockIndex * this.PREVIEW_BLOCK_DURATION_US;
|
|
259
|
+
if (blockStartUs >= model.durationUs) {
|
|
260
|
+
this.previewFirstBlockOffsetUs = 0;
|
|
261
|
+
this.previewNextBlockIndex = blockIndex + 1;
|
|
262
|
+
this.previewNextScheduleTime = audioContext.currentTime + this.PREVIEW_SCHEDULE_AHEAD_SEC;
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
const buffer = await this.getOrCreateMixedBlock(blockIndex);
|
|
266
|
+
if (!buffer) {
|
|
267
|
+
this.previewFirstBlockOffsetUs = 0;
|
|
268
|
+
this.previewNextBlockIndex = blockIndex + 1;
|
|
269
|
+
this.previewNextScheduleTime = audioContext.currentTime + 0.02;
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
if (this.previewScheduleToken !== token) return;
|
|
273
|
+
const rate = this.playbackRate || 1;
|
|
274
|
+
const offsetUs = this.previewFirstBlockOffsetUs;
|
|
275
|
+
let startTime = this.previewNextScheduleTime;
|
|
276
|
+
let offsetSec = offsetUs > 0 ? offsetUs / 1e6 : 0;
|
|
277
|
+
const now = audioContext.currentTime;
|
|
278
|
+
if (startTime < now + 0.01) {
|
|
279
|
+
const targetStartTime = now + 0.02;
|
|
280
|
+
const skippedSec = Math.max(0, (targetStartTime - startTime) * rate);
|
|
281
|
+
startTime = targetStartTime;
|
|
282
|
+
offsetSec += skippedSec;
|
|
283
|
+
}
|
|
284
|
+
if (offsetSec >= buffer.duration) {
|
|
285
|
+
this.previewFirstBlockOffsetUs = 0;
|
|
286
|
+
this.previewNextBlockIndex = blockIndex + 1;
|
|
287
|
+
this.previewNextScheduleTime = startTime;
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
const remainingSec = Math.max(0, buffer.duration - offsetSec);
|
|
291
|
+
if (remainingSec <= 0) return;
|
|
292
|
+
const source = audioContext.createBufferSource();
|
|
293
|
+
source.buffer = buffer;
|
|
294
|
+
source.playbackRate.value = rate;
|
|
295
|
+
const gainNode = audioContext.createGain();
|
|
296
|
+
const volume = this.volume;
|
|
297
|
+
const fadeSec = Math.min(this.PREVIEW_BLOCK_FADE_SEC, remainingSec / 2);
|
|
298
|
+
gainNode.gain.setValueAtTime(0, startTime);
|
|
299
|
+
gainNode.gain.linearRampToValueAtTime(volume, startTime + fadeSec);
|
|
300
|
+
gainNode.gain.setValueAtTime(volume, startTime + Math.max(fadeSec, remainingSec - fadeSec));
|
|
301
|
+
gainNode.gain.linearRampToValueAtTime(0, startTime + remainingSec);
|
|
302
|
+
source.connect(gainNode);
|
|
303
|
+
gainNode.connect(audioContext.destination);
|
|
304
|
+
source.start(startTime, offsetSec);
|
|
305
|
+
const entry = { source, gain: gainNode };
|
|
306
|
+
this.previewScheduledSources.add(entry);
|
|
307
|
+
source.onended = () => {
|
|
308
|
+
try {
|
|
309
|
+
source.disconnect();
|
|
310
|
+
gainNode.disconnect();
|
|
311
|
+
} catch {
|
|
312
|
+
}
|
|
313
|
+
this.previewScheduledSources.delete(entry);
|
|
314
|
+
};
|
|
315
|
+
this.previewFirstBlockOffsetUs = 0;
|
|
316
|
+
this.previewNextBlockIndex = blockIndex + 1;
|
|
317
|
+
this.previewNextScheduleTime = startTime + remainingSec / rate;
|
|
318
|
+
}
|
|
319
|
+
stopAllPreviewSources() {
|
|
320
|
+
for (const { source, gain } of this.previewScheduledSources) {
|
|
321
|
+
try {
|
|
322
|
+
source.disconnect();
|
|
323
|
+
} catch {
|
|
324
|
+
}
|
|
325
|
+
try {
|
|
326
|
+
source.stop(0);
|
|
327
|
+
} catch {
|
|
328
|
+
}
|
|
329
|
+
try {
|
|
330
|
+
gain.disconnect();
|
|
331
|
+
} catch {
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
this.previewScheduledSources.clear();
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
export {
|
|
338
|
+
AudioPreviewSession
|
|
339
|
+
};
|
|
340
|
+
//# sourceMappingURL=AudioPreviewSession.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AudioPreviewSession.js","sources":["../../src/orchestrator/AudioPreviewSession.ts"],"sourcesContent":["import type { TimeUs } from '../model/types';\nimport { OfflineAudioMixer } from '../stages/compose/OfflineAudioMixer';\nimport { AudioMixBlockCache } from '../cache/AudioMixBlockCache';\nimport type { CacheManager } from '../cache/CacheManager';\nimport type { RequestMode } from './types';\nimport type { AudioWindowPreparer } from './AudioWindowPreparer';\n\nexport class AudioPreviewSession {\n private mixer: OfflineAudioMixer;\n private audioContext: AudioContext | null = null;\n private volume = 1.0;\n private playbackRate = 1.0;\n private isPlaying = false;\n\n // Preview strategy (unified):\n // - Always schedule audio in fixed 60s \"mix blocks\"\n // - Cache 2~3 mixed AudioBuffer blocks (LRU) to accelerate seek\n // - Schedule ahead using AudioContext clock to avoid underrun\n private readonly PREVIEW_BLOCK_DURATION_US: TimeUs = 60 * 1_000_000; // 60s\n private readonly PREVIEW_BLOCK_CACHE_SIZE = 3;\n private readonly PREVIEW_SCHEDULE_AHEAD_SEC = 12.0; // keep enough scheduled audio to hide mixing latency\n private readonly PREVIEW_BUFFER_GUARD_US: TimeUs = 2_000_000; // if next block isn't ready near boundary -> buffering\n private readonly PREVIEW_BLOCK_FADE_SEC = 0.01; // 10ms fade-in/out to avoid click at boundaries\n\n private previewBlockCache = new AudioMixBlockCache(this.PREVIEW_BLOCK_CACHE_SIZE);\n\n private previewScheduleTask: Promise<void> | null = null;\n private previewScheduleToken = 0;\n private previewMixToken = 0;\n private previewNextBlockIndex = 0;\n private previewNextScheduleTime = 0; // AudioContext time\n private previewFirstBlockOffsetUs: TimeUs = 0; // seek offset within the first block\n private previewLastTimelineUs: TimeUs = 0;\n private previewLastStallWarnAt = 0; // AudioContext time (sec)\n\n private previewScheduledSources = new Set<{ source: AudioBufferSourceNode; gain: GainNode }>();\n private previewMixQueue: Promise<unknown> = Promise.resolve();\n\n constructor(private deps: { cacheManager: CacheManager; preparer: AudioWindowPreparer }) {\n this.mixer = new OfflineAudioMixer(deps.cacheManager, () => deps.preparer.getModel());\n }\n\n invalidatePreviewMixCache(): void {\n // Mixed AudioBuffer blocks embed per-clip audioConfig (volume/muted).\n // When model is replaced via setCompositionModel, these blocks must be invalidated,\n // otherwise preview may keep scheduling old AudioBuffers and volume changes won't take effect.\n this.previewBlockCache.clear();\n // Ensure any in-flight mix tasks are dropped.\n this.previewMixToken += 1;\n }\n\n private enqueuePreviewMix<T>(work: () => Promise<T>, token: number): Promise<T | null> {\n const run = () => work();\n const next = this.previewMixQueue.then(\n () => {\n // If a seek/reset happened since this task was enqueued, drop it.\n if (this.previewMixToken !== token) {\n return null;\n }\n return run();\n },\n () => {\n if (this.previewMixToken !== token) {\n return null;\n }\n return run();\n }\n );\n // Keep queue alive even if a task fails.\n this.previewMixQueue = next.then(\n () => undefined,\n () => undefined\n );\n return next as Promise<T | null>;\n }\n\n async ensureAudioForTime(timeUs: TimeUs, options?: { mode?: RequestMode }): Promise<void> {\n const model = this.deps.preparer.getModel();\n if (!model) return;\n\n const mode = options?.mode ?? 'blocking';\n\n // Preview contract:\n // - blocking: ensure the current 60s mixed block is ready (may be slow -> should be wrapped by buffering UI)\n // - probe: best-effort preheat current and next block without blocking\n const blockIndex = Math.floor(Math.max(0, timeUs) / this.PREVIEW_BLOCK_DURATION_US);\n\n if (mode === 'probe') {\n // Default: only preheat the current block.\n void this.getOrCreateMixedBlock(blockIndex);\n\n // If we're close enough to boundary (within scheduling lookahead), also preheat next block.\n const blockEndUs = (blockIndex + 1) * this.PREVIEW_BLOCK_DURATION_US;\n const remainingToBoundaryUs = blockEndUs - Math.max(0, timeUs);\n const lookaheadUs = Math.floor(this.PREVIEW_SCHEDULE_AHEAD_SEC * 1_000_000);\n if (remainingToBoundaryUs > 0 && remainingToBoundaryUs <= lookaheadUs) {\n void this.getOrCreateMixedBlock(blockIndex + 1);\n }\n return;\n }\n\n await this.getOrCreateMixedBlock(blockIndex);\n\n // If we're very close to the block boundary, also ensure the next block.\n const blockEndUs = (blockIndex + 1) * this.PREVIEW_BLOCK_DURATION_US;\n const remainingToBoundaryUs = blockEndUs - Math.max(0, timeUs);\n if (remainingToBoundaryUs > 0 && remainingToBoundaryUs <= this.PREVIEW_BUFFER_GUARD_US) {\n await this.getOrCreateMixedBlock(blockIndex + 1);\n }\n }\n\n isPreviewMixBlockCached(timeUs: TimeUs): boolean {\n const blockIndex = Math.floor(Math.max(0, timeUs) / this.PREVIEW_BLOCK_DURATION_US);\n return this.previewBlockCache.get(blockIndex) !== null;\n }\n\n shouldEnterBufferingForUpcomingPreviewAudio(timeUs: TimeUs): boolean {\n const model = this.deps.preparer.getModel();\n if (!model) return false;\n\n const clampedUs = Math.max(0, timeUs);\n const blockIndex = Math.floor(clampedUs / this.PREVIEW_BLOCK_DURATION_US);\n const nextBlockStartUs = (blockIndex + 1) * this.PREVIEW_BLOCK_DURATION_US;\n if (nextBlockStartUs >= model.durationUs) return false;\n\n const remainingToBoundaryUs = nextBlockStartUs - clampedUs;\n if (remainingToBoundaryUs > this.PREVIEW_BUFFER_GUARD_US) return false;\n\n // Probe next block readiness by checking cache at next block start time.\n return !this.isPreviewMixBlockCached(nextBlockStartUs);\n }\n\n async startPlayback(timeUs: TimeUs, audioContext: AudioContext): Promise<void> {\n this.audioContext = audioContext;\n\n // Resume AudioContext if suspended (required by modern browsers)\n if (audioContext.state === 'suspended') {\n await audioContext.resume();\n }\n\n // Ensure audio is decoded and ready (blocking mode for startup).\n await this.ensureAudioForTime(timeUs, { mode: 'blocking' });\n\n this.isPlaying = true;\n\n // Unified block scheduling: align to block index and schedule immediately.\n this.startPreviewBlockScheduling(timeUs, audioContext);\n // Schedule first block in blocking mode to avoid initial silence.\n await this.scheduleNextPreviewBlock(audioContext, this.previewScheduleToken);\n // Then keep scheduling in background.\n void this.scheduleAudio(timeUs, audioContext);\n }\n\n stopPlayback(): void {\n this.isPlaying = false;\n this.stopAllPreviewSources();\n this.cancelPreviewBlockScheduling();\n this.previewMixToken += 1;\n }\n\n /**\n * Schedule audio chunks ahead of playback cursor.\n */\n async scheduleAudio(currentTimelineUs: TimeUs, audioContext: AudioContext): Promise<void> {\n if (!this.isPlaying || !this.deps.preparer.getModel() || !this.audioContext) {\n return;\n }\n\n this.previewLastTimelineUs = currentTimelineUs;\n\n // Keep scheduling in the background to avoid blocking the render loop.\n if (this.previewScheduleTask) return;\n\n // Initialize if needed (e.g. after model switch without explicit startPlayback).\n if (this.previewNextScheduleTime === 0) {\n this.startPreviewBlockScheduling(currentTimelineUs, audioContext);\n }\n\n const token = this.previewScheduleToken;\n this.previewScheduleTask = this.runPreviewBlockSchedulingLoop(audioContext, token).finally(\n () => {\n if (this.previewScheduleToken === token) {\n this.previewScheduleTask = null;\n }\n }\n );\n }\n\n /**\n * Reset playback states (called on seek)\n */\n resetPlaybackStates(): void {\n this.stopAllPreviewSources();\n this.cancelPreviewBlockScheduling();\n this.previewMixToken += 1;\n }\n\n reset(): void {\n this.stopPlayback();\n this.deps.cacheManager.clearAudioCache();\n this.previewBlockCache.clear();\n this.deps.preparer.reset();\n }\n\n setVolume(volume: number): void {\n this.volume = volume;\n const audioContext = this.audioContext;\n if (!audioContext) return;\n\n // Apply volume to already scheduled nodes (small ramp to avoid click).\n const t = audioContext.currentTime;\n for (const { gain } of this.previewScheduledSources) {\n try {\n gain.gain.cancelScheduledValues(t);\n gain.gain.setValueAtTime(gain.gain.value, t);\n gain.gain.linearRampToValueAtTime(volume, t + 0.01);\n } catch {\n // ignore\n }\n }\n }\n\n setPlaybackRate(rate: number): void {\n this.playbackRate = rate;\n // Playback rate change requires restarting scheduling to keep sync with the timeline clock.\n this.resetPlaybackStates();\n }\n\n private startPreviewBlockScheduling(startTimelineUs: TimeUs, audioContext: AudioContext): void {\n this.cancelPreviewBlockScheduling();\n this.stopAllPreviewSources();\n\n const clampedUs = Math.max(0, startTimelineUs);\n const blockIndex = Math.floor(clampedUs / this.PREVIEW_BLOCK_DURATION_US);\n const blockStartUs = blockIndex * this.PREVIEW_BLOCK_DURATION_US;\n\n this.previewFirstBlockOffsetUs = clampedUs - blockStartUs;\n this.previewNextBlockIndex = blockIndex;\n this.previewNextScheduleTime = audioContext.currentTime + 0.02;\n this.previewLastTimelineUs = startTimelineUs;\n }\n\n private cancelPreviewBlockScheduling(): void {\n this.previewScheduleToken += 1;\n this.previewScheduleTask = null;\n this.previewNextBlockIndex = 0;\n this.previewNextScheduleTime = 0;\n this.previewFirstBlockOffsetUs = 0;\n this.previewLastTimelineUs = 0;\n }\n\n private realignPreviewSchedulingToTimeline(audioContext: AudioContext): void {\n const model = this.deps.preparer.getModel();\n if (!model) return;\n\n const clampedUs = Math.max(0, this.previewLastTimelineUs);\n const blockIndex = Math.floor(clampedUs / this.PREVIEW_BLOCK_DURATION_US);\n const blockStartUs = blockIndex * this.PREVIEW_BLOCK_DURATION_US;\n if (blockStartUs >= model.durationUs) return;\n\n this.stopAllPreviewSources();\n this.previewFirstBlockOffsetUs = clampedUs - blockStartUs;\n this.previewNextBlockIndex = blockIndex;\n this.previewNextScheduleTime = audioContext.currentTime + 0.02;\n }\n\n private async runPreviewBlockSchedulingLoop(\n audioContext: AudioContext,\n token: number\n ): Promise<void> {\n while (this.isPlaying && this.previewScheduleToken === token) {\n const model = this.deps.preparer.getModel();\n if (!model) return;\n\n // End-of-timeline: nothing more to schedule.\n const nextBlockStartUs = this.previewNextBlockIndex * this.PREVIEW_BLOCK_DURATION_US;\n if (nextBlockStartUs >= model.durationUs) {\n this.previewNextScheduleTime = audioContext.currentTime + this.PREVIEW_SCHEDULE_AHEAD_SEC;\n return;\n }\n\n const scheduleAheadTime = audioContext.currentTime + this.PREVIEW_SCHEDULE_AHEAD_SEC;\n if (this.previewNextScheduleTime >= scheduleAheadTime) return;\n\n const prevBlockIndex = this.previewNextBlockIndex;\n const prevScheduleTime = this.previewNextScheduleTime;\n\n await this.scheduleNextPreviewBlock(audioContext, token);\n\n // If scheduling made no progress, bail out to avoid a tight loop that can freeze UI.\n if (\n this.previewScheduleToken === token &&\n this.previewNextBlockIndex === prevBlockIndex &&\n this.previewNextScheduleTime === prevScheduleTime\n ) {\n const now = audioContext.currentTime;\n if (now - this.previewLastStallWarnAt >= 1) {\n this.previewLastStallWarnAt = now;\n console.warn('[AudioPreviewSession] scheduling stalled; stop loop to avoid spin', {\n prevBlockIndex,\n prevScheduleTime,\n now,\n });\n }\n return;\n }\n }\n }\n\n private async getOrCreateMixedBlock(blockIndex: number): Promise<AudioBuffer | null> {\n const model = this.deps.preparer.getModel();\n if (!model) return null;\n\n const token = this.previewMixToken;\n\n return await this.previewBlockCache.getOrCreate(blockIndex, async () => {\n return await this.enqueuePreviewMix(async () => {\n const startUs = blockIndex * this.PREVIEW_BLOCK_DURATION_US;\n const endUs = Math.min(startUs + this.PREVIEW_BLOCK_DURATION_US, model.durationUs);\n await this.deps.preparer.ensureAudioForTimeRange(startUs, endUs, {\n mode: 'blocking',\n loadResource: true,\n });\n const mixed = await this.mixer.mix(startUs, endUs);\n\n // Preview uses mixed AudioBuffer blocks as the primary cache.\n this.deps.cacheManager.clearAudioCache();\n\n return mixed;\n }, token);\n });\n }\n\n private async scheduleNextPreviewBlock(audioContext: AudioContext, token: number): Promise<void> {\n const model = this.deps.preparer.getModel();\n if (!this.isPlaying || !model) return;\n if (this.previewScheduleToken !== token) return;\n\n // If we're behind the audio clock, resync to avoid attempting to schedule in the past.\n if (this.previewNextScheduleTime < audioContext.currentTime + 0.01) {\n this.realignPreviewSchedulingToTimeline(audioContext);\n }\n\n const blockIndex = this.previewNextBlockIndex;\n const blockStartUs = blockIndex * this.PREVIEW_BLOCK_DURATION_US;\n if (blockStartUs >= model.durationUs) {\n // End-of-timeline: advance state so scheduling loop can finish without stalling.\n this.previewFirstBlockOffsetUs = 0;\n this.previewNextBlockIndex = blockIndex + 1;\n this.previewNextScheduleTime = audioContext.currentTime + this.PREVIEW_SCHEDULE_AHEAD_SEC;\n return;\n }\n\n const buffer = await this.getOrCreateMixedBlock(blockIndex);\n if (!buffer) {\n // Could be cancelled (seek/reset) or an empty-range guard; advance to avoid stalling.\n this.previewFirstBlockOffsetUs = 0;\n this.previewNextBlockIndex = blockIndex + 1;\n this.previewNextScheduleTime = audioContext.currentTime + 0.02;\n return;\n }\n if (this.previewScheduleToken !== token) return;\n\n const rate = this.playbackRate || 1.0;\n const offsetUs = this.previewFirstBlockOffsetUs;\n let startTime = this.previewNextScheduleTime;\n let offsetSec = offsetUs > 0 ? offsetUs / 1_000_000 : 0;\n\n // Mixing can be slow; after await, startTime might already be in the past.\n const now = audioContext.currentTime;\n if (startTime < now + 0.01) {\n const targetStartTime = now + 0.02;\n const skippedSec = Math.max(0, (targetStartTime - startTime) * rate);\n startTime = targetStartTime;\n offsetSec += skippedSec;\n }\n\n if (offsetSec >= buffer.duration) {\n this.previewFirstBlockOffsetUs = 0;\n this.previewNextBlockIndex = blockIndex + 1;\n this.previewNextScheduleTime = startTime;\n return;\n }\n\n const remainingSec = Math.max(0, buffer.duration - offsetSec);\n if (remainingSec <= 0) return;\n\n const source = audioContext.createBufferSource();\n source.buffer = buffer;\n source.playbackRate.value = rate;\n\n const gainNode = audioContext.createGain();\n const volume = this.volume;\n\n // Fade in/out to avoid click at block boundaries.\n const fadeSec = Math.min(this.PREVIEW_BLOCK_FADE_SEC, remainingSec / 2);\n gainNode.gain.setValueAtTime(0, startTime);\n gainNode.gain.linearRampToValueAtTime(volume, startTime + fadeSec);\n gainNode.gain.setValueAtTime(volume, startTime + Math.max(fadeSec, remainingSec - fadeSec));\n gainNode.gain.linearRampToValueAtTime(0, startTime + remainingSec);\n\n source.connect(gainNode);\n gainNode.connect(audioContext.destination);\n source.start(startTime, offsetSec);\n\n const entry = { source, gain: gainNode };\n this.previewScheduledSources.add(entry);\n\n source.onended = () => {\n try {\n source.disconnect();\n gainNode.disconnect();\n } catch {\n // ignore\n }\n this.previewScheduledSources.delete(entry);\n };\n\n // Advance to next block\n this.previewFirstBlockOffsetUs = 0;\n this.previewNextBlockIndex = blockIndex + 1;\n this.previewNextScheduleTime = startTime + remainingSec / rate;\n }\n\n private stopAllPreviewSources(): void {\n for (const { source, gain } of this.previewScheduledSources) {\n try {\n source.disconnect();\n } catch {\n // ignore\n }\n try {\n source.stop(0);\n } catch {\n // ignore\n }\n try {\n gain.disconnect();\n } catch {\n // ignore\n }\n }\n this.previewScheduledSources.clear();\n }\n}\n"],"names":["blockEndUs","remainingToBoundaryUs"],"mappings":"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"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { TimeUs } from '../model/types';
|
|
2
|
+
import { CompositionModel } from '../model';
|
|
3
|
+
import { ResourceLoader } from '../stages/load/ResourceLoader';
|
|
4
|
+
import { EventBus } from '../event/EventBus';
|
|
5
|
+
import { EventPayloadMap } from '../event/events';
|
|
6
|
+
import { CacheManager } from '../cache/CacheManager';
|
|
7
|
+
import { RequestMode } from './types';
|
|
8
|
+
|
|
9
|
+
interface AudioDataMessage {
|
|
10
|
+
sessionId: string;
|
|
11
|
+
audioData: AudioData;
|
|
12
|
+
clipStartUs: TimeUs;
|
|
13
|
+
clipDurationUs: TimeUs;
|
|
14
|
+
}
|
|
15
|
+
interface AudioWindowPreparerDeps {
|
|
16
|
+
cacheManager: CacheManager;
|
|
17
|
+
resourceLoader: ResourceLoader;
|
|
18
|
+
eventBus: EventBus<EventPayloadMap>;
|
|
19
|
+
}
|
|
20
|
+
export declare class AudioWindowPreparer {
|
|
21
|
+
private activeClips;
|
|
22
|
+
private deps;
|
|
23
|
+
private model;
|
|
24
|
+
constructor(deps: AudioWindowPreparerDeps);
|
|
25
|
+
setModel(model: CompositionModel): void;
|
|
26
|
+
getModel(): CompositionModel | null;
|
|
27
|
+
reset(): void;
|
|
28
|
+
onAudioData(message: AudioDataMessage): void;
|
|
29
|
+
/**
|
|
30
|
+
* Fast readiness probe for preview playback.
|
|
31
|
+
*
|
|
32
|
+
* This is intentionally synchronous and lightweight:
|
|
33
|
+
* - Only checks resource-level readiness (download + MP4 index parsing).
|
|
34
|
+
* - If any relevant resource isn't ready yet, return false.
|
|
35
|
+
* - Does NOT require audio samples / PCM window coverage (probe is resource-level only).
|
|
36
|
+
*/
|
|
37
|
+
isAudioResourceWindowReady(startUs: TimeUs, endUs: TimeUs): boolean;
|
|
38
|
+
activateAllAudioClips(): Promise<void>;
|
|
39
|
+
deactivateClip(clipId: string): Promise<void>;
|
|
40
|
+
/**
|
|
41
|
+
* Core method to ensure audio for all clips in a time range
|
|
42
|
+
*/
|
|
43
|
+
ensureAudioForTimeRange(startUs: TimeUs, endUs: TimeUs, options: {
|
|
44
|
+
mode?: RequestMode;
|
|
45
|
+
loadResource?: boolean;
|
|
46
|
+
strictMode?: boolean;
|
|
47
|
+
}): Promise<void>;
|
|
48
|
+
/**
|
|
49
|
+
* Ensure audio window for a clip
|
|
50
|
+
*/
|
|
51
|
+
ensureAudioWindow(clipId: string, startUs: TimeUs, endUs: TimeUs, strictMode?: boolean): Promise<void>;
|
|
52
|
+
/**
|
|
53
|
+
* Decode audio window for a clip
|
|
54
|
+
*/
|
|
55
|
+
private decodeAudioWindow;
|
|
56
|
+
/**
|
|
57
|
+
* Decode audio samples to PCM and cache
|
|
58
|
+
*/
|
|
59
|
+
private decodeAudioSamples;
|
|
60
|
+
}
|
|
61
|
+
export {};
|
|
62
|
+
//# sourceMappingURL=AudioWindowPreparer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AudioWindowPreparer.d.ts","sourceRoot":"","sources":["../../src/orchestrator/AudioWindowPreparer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AAC7C,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AACjD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAC;AACpE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAClD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAEvD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAG1D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAE3C,UAAU,gBAAgB;IACxB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,SAAS,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,MAAM,CAAC;CACxB;AAED,UAAU,uBAAuB;IAC/B,YAAY,EAAE,YAAY,CAAC;IAC3B,cAAc,EAAE,cAAc,CAAC;IAC/B,QAAQ,EAAE,QAAQ,CAAC,eAAe,CAAC,CAAC;CACrC;AAED,qBAAa,mBAAmB;IAC9B,OAAO,CAAC,WAAW,CAAqB;IACxC,OAAO,CAAC,IAAI,CAA0B;IACtC,OAAO,CAAC,KAAK,CAAiC;gBAElC,IAAI,EAAE,uBAAuB;IAIzC,QAAQ,CAAC,KAAK,EAAE,gBAAgB,GAAG,IAAI;IAIvC,QAAQ,IAAI,gBAAgB,GAAG,IAAI;IAInC,KAAK,IAAI,IAAI;IAKb,WAAW,CAAC,OAAO,EAAE,gBAAgB,GAAG,IAAI;IAM5C;;;;;;;OAOG;IACH,0BAA0B,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO;IAmB7D,qBAAqB,IAAI,OAAO,CAAC,IAAI,CAAC;IA6CtC,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IASnD;;OAEG;IACG,uBAAuB,CAC3B,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,MAAM,EACb,OAAO,EAAE;QAAE,IAAI,CAAC,EAAE,WAAW,CAAC;QAAC,YAAY,CAAC,EAAE,OAAO,CAAC;QAAC,UAAU,CAAC,EAAE,OAAO,CAAA;KAAE,GAC5E,OAAO,CAAC,IAAI,CAAC;IAwEhB;;OAEG;IACG,iBAAiB,CACrB,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,MAAM,EACb,UAAU,GAAE,OAAe,GAC1B,OAAO,CAAC,IAAI,CAAC;IAShB;;OAEG;YACW,iBAAiB;IA8E/B;;OAEG;YACW,kBAAkB;CAiEjC"}
|