@meframe/core 0.1.8 → 0.2.0

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.
Files changed (54) hide show
  1. package/dist/cache/AudioMixBlockCache.d.ts +18 -0
  2. package/dist/cache/AudioMixBlockCache.d.ts.map +1 -0
  3. package/dist/cache/AudioMixBlockCache.js +57 -0
  4. package/dist/cache/AudioMixBlockCache.js.map +1 -0
  5. package/dist/cache/CacheManager.d.ts +1 -1
  6. package/dist/cache/CacheManager.d.ts.map +1 -1
  7. package/dist/cache/CacheManager.js +1 -2
  8. package/dist/cache/CacheManager.js.map +1 -1
  9. package/dist/cache/l1/AudioL1Cache.d.ts +0 -15
  10. package/dist/cache/l1/AudioL1Cache.d.ts.map +1 -1
  11. package/dist/cache/l1/AudioL1Cache.js +57 -65
  12. package/dist/cache/l1/AudioL1Cache.js.map +1 -1
  13. package/dist/controllers/PlaybackController.d.ts +0 -1
  14. package/dist/controllers/PlaybackController.d.ts.map +1 -1
  15. package/dist/controllers/PlaybackController.js +16 -35
  16. package/dist/controllers/PlaybackController.js.map +1 -1
  17. package/dist/controllers/PlaybackStateMachine.d.ts.map +1 -1
  18. package/dist/controllers/PlaybackStateMachine.js +22 -2
  19. package/dist/controllers/PlaybackStateMachine.js.map +1 -1
  20. package/dist/controllers/types.d.ts +3 -0
  21. package/dist/controllers/types.d.ts.map +1 -1
  22. package/dist/controllers/types.js +1 -0
  23. package/dist/controllers/types.js.map +1 -1
  24. package/dist/orchestrator/GlobalAudioSession.d.ts +27 -7
  25. package/dist/orchestrator/GlobalAudioSession.d.ts.map +1 -1
  26. package/dist/orchestrator/GlobalAudioSession.js +292 -103
  27. package/dist/orchestrator/GlobalAudioSession.js.map +1 -1
  28. package/dist/orchestrator/OnDemandVideoSession.d.ts.map +1 -1
  29. package/dist/orchestrator/OnDemandVideoSession.js +13 -0
  30. package/dist/orchestrator/OnDemandVideoSession.js.map +1 -1
  31. package/dist/orchestrator/Orchestrator.d.ts +1 -1
  32. package/dist/orchestrator/Orchestrator.d.ts.map +1 -1
  33. package/dist/orchestrator/Orchestrator.js +44 -17
  34. package/dist/orchestrator/Orchestrator.js.map +1 -1
  35. package/dist/stages/compose/OfflineAudioMixer.d.ts.map +1 -1
  36. package/dist/stages/compose/OfflineAudioMixer.js +4 -1
  37. package/dist/stages/compose/OfflineAudioMixer.js.map +1 -1
  38. package/dist/stages/load/ResourceLoader.d.ts +6 -5
  39. package/dist/stages/load/ResourceLoader.d.ts.map +1 -1
  40. package/dist/stages/load/ResourceLoader.js +53 -39
  41. package/dist/stages/load/ResourceLoader.js.map +1 -1
  42. package/dist/stages/load/TaskManager.d.ts +5 -12
  43. package/dist/stages/load/TaskManager.d.ts.map +1 -1
  44. package/dist/stages/load/TaskManager.js +60 -46
  45. package/dist/stages/load/TaskManager.js.map +1 -1
  46. package/dist/stages/mux/MP4Muxer.d.ts.map +1 -1
  47. package/dist/stages/mux/MP4Muxer.js +3 -0
  48. package/dist/stages/mux/MP4Muxer.js.map +1 -1
  49. package/dist/utils/platform-utils.d.ts +1 -0
  50. package/dist/utils/platform-utils.d.ts.map +1 -1
  51. package/dist/utils/platform-utils.js +19 -6
  52. package/dist/utils/platform-utils.js.map +1 -1
  53. package/dist/workers/stages/decode/video-decode.worker.BnWVUkng.js.map +1 -1
  54. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"GlobalAudioSession.d.ts","sourceRoot":"","sources":["../../src/orchestrator/GlobalAudioSession.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"GlobalAudioSession.d.ts","sourceRoot":"","sources":["../../src/orchestrator/GlobalAudioSession.ts"],"names":[],"mappings":"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"}
@@ -1,5 +1,6 @@
1
1
  import { OfflineAudioMixer } from "../stages/compose/OfflineAudioMixer.js";
2
2
  import { MeframeEvent } from "../event/events.js";
3
+ import { AudioMixBlockCache } from "../cache/AudioMixBlockCache.js";
3
4
  import { AudioChunkEncoder } from "../stages/encode/AudioChunkEncoder.js";
4
5
  import { AudioChunkDecoder } from "../stages/decode/AudioChunkDecoder.js";
5
6
  import { hasResourceId, isAudioClip } from "../model/types.js";
@@ -12,20 +13,59 @@ class GlobalAudioSession {
12
13
  volume = 1;
13
14
  playbackRate = 1;
14
15
  isPlaying = false;
15
- // Lookahead scheduling state
16
- nextScheduleTime = 0;
17
- // Next AudioContext time to schedule
18
- nextContentTimeUs = 0;
19
- // Next timeline position (Us)
20
- scheduledSources = /* @__PURE__ */ new Set();
21
- LOOKAHEAD_TIME = 0.2;
22
- // 200ms lookahead
23
- CHUNK_DURATION = 0.1;
24
- // 100ms chunks
16
+ // Preview strategy (unified):
17
+ // - Always schedule audio in fixed 60s "mix blocks"
18
+ // - Cache 2~3 mixed AudioBuffer blocks (LRU) to accelerate seek
19
+ // - Schedule ahead using AudioContext clock to avoid underrun
20
+ PREVIEW_BLOCK_DURATION_US = 60 * 1e6;
21
+ // 60s
22
+ PREVIEW_BLOCK_CACHE_SIZE = 3;
23
+ PREVIEW_SCHEDULE_AHEAD_SEC = 12;
24
+ // keep enough scheduled audio to hide mixing latency
25
+ PREVIEW_BUFFER_GUARD_US = 2e6;
26
+ // if next block isn't ready near boundary -> buffering
27
+ PREVIEW_BLOCK_FADE_SEC = 0.01;
28
+ // 10ms fade-in/out to avoid click at boundaries
29
+ previewBlockCache = new AudioMixBlockCache(this.PREVIEW_BLOCK_CACHE_SIZE);
30
+ previewScheduleTask = null;
31
+ previewScheduleToken = 0;
32
+ previewMixToken = 0;
33
+ previewNextBlockIndex = 0;
34
+ previewNextScheduleTime = 0;
35
+ // AudioContext time
36
+ previewFirstBlockOffsetUs = 0;
37
+ // seek offset within the first block
38
+ previewLastTimelineUs = 0;
39
+ previewLastStallWarnAt = 0;
40
+ // AudioContext time (sec)
41
+ previewScheduledSources = /* @__PURE__ */ new Set();
42
+ previewMixQueue = Promise.resolve();
25
43
  constructor(deps) {
26
44
  this.deps = deps;
27
45
  this.mixer = new OfflineAudioMixer(deps.cacheManager, () => this.model);
28
46
  }
47
+ enqueuePreviewMix(work, token) {
48
+ const run = () => work();
49
+ const next = this.previewMixQueue.then(
50
+ () => {
51
+ if (this.previewMixToken !== token) {
52
+ return null;
53
+ }
54
+ return run();
55
+ },
56
+ () => {
57
+ if (this.previewMixToken !== token) {
58
+ return null;
59
+ }
60
+ return run();
61
+ }
62
+ );
63
+ this.previewMixQueue = next.then(
64
+ () => void 0,
65
+ () => void 0
66
+ );
67
+ return next;
68
+ }
29
69
  setModel(model) {
30
70
  this.model = model;
31
71
  }
@@ -35,15 +75,41 @@ class GlobalAudioSession {
35
75
  this.deps.cacheManager.putClipAudioData(sessionId, audioData, clipDurationUs, globalTimeUs);
36
76
  }
37
77
  async ensureAudioForTime(timeUs, options) {
38
- if (!this.model) return;
78
+ const model = this.model;
79
+ if (!model) return;
39
80
  const mode = options?.mode ?? "blocking";
40
- const WINDOW_DURATION = 3e6;
41
- const windowEndUs = Math.min(this.model.durationUs, timeUs + WINDOW_DURATION);
81
+ const blockIndex = Math.floor(Math.max(0, timeUs) / this.PREVIEW_BLOCK_DURATION_US);
42
82
  if (mode === "probe") {
43
- void this.ensureAudioForTimeRange(timeUs, windowEndUs, { mode, loadResource: true });
83
+ void this.getOrCreateMixedBlock(blockIndex);
84
+ const blockEndUs2 = (blockIndex + 1) * this.PREVIEW_BLOCK_DURATION_US;
85
+ const remainingToBoundaryUs2 = blockEndUs2 - Math.max(0, timeUs);
86
+ const lookaheadUs = Math.floor(this.PREVIEW_SCHEDULE_AHEAD_SEC * 1e6);
87
+ if (remainingToBoundaryUs2 > 0 && remainingToBoundaryUs2 <= lookaheadUs) {
88
+ void this.getOrCreateMixedBlock(blockIndex + 1);
89
+ }
44
90
  return;
45
91
  }
46
- await this.ensureAudioForTimeRange(timeUs, windowEndUs, { mode, loadResource: true });
92
+ await this.getOrCreateMixedBlock(blockIndex);
93
+ const blockEndUs = (blockIndex + 1) * this.PREVIEW_BLOCK_DURATION_US;
94
+ const remainingToBoundaryUs = blockEndUs - Math.max(0, timeUs);
95
+ if (remainingToBoundaryUs > 0 && remainingToBoundaryUs <= this.PREVIEW_BUFFER_GUARD_US) {
96
+ await this.getOrCreateMixedBlock(blockIndex + 1);
97
+ }
98
+ }
99
+ isPreviewMixBlockCached(timeUs) {
100
+ const blockIndex = Math.floor(Math.max(0, timeUs) / this.PREVIEW_BLOCK_DURATION_US);
101
+ return this.previewBlockCache.get(blockIndex) !== null;
102
+ }
103
+ shouldEnterBufferingForUpcomingPreviewAudio(timeUs) {
104
+ const model = this.model;
105
+ if (!model) return false;
106
+ const clampedUs = Math.max(0, timeUs);
107
+ const blockIndex = Math.floor(clampedUs / this.PREVIEW_BLOCK_DURATION_US);
108
+ const nextBlockStartUs = (blockIndex + 1) * this.PREVIEW_BLOCK_DURATION_US;
109
+ if (nextBlockStartUs >= model.durationUs) return false;
110
+ const remainingToBoundaryUs = nextBlockStartUs - clampedUs;
111
+ if (remainingToBoundaryUs > this.PREVIEW_BUFFER_GUARD_US) return false;
112
+ return !this.isPreviewMixBlockCached(nextBlockStartUs);
47
113
  }
48
114
  /**
49
115
  * Fast readiness probe for preview playback.
@@ -114,12 +180,15 @@ class GlobalAudioSession {
114
180
  }
115
181
  await this.ensureAudioForTime(timeUs, { mode: "blocking" });
116
182
  this.isPlaying = true;
117
- this.resetPlaybackStates();
118
- await this.scheduleAudio(timeUs, audioContext);
183
+ this.startPreviewBlockScheduling(timeUs, audioContext);
184
+ await this.scheduleNextPreviewBlock(audioContext, this.previewScheduleToken);
185
+ void this.scheduleAudio(timeUs, audioContext);
119
186
  }
120
187
  stopPlayback() {
121
188
  this.isPlaying = false;
122
- this.stopAllAudioSources();
189
+ this.stopAllPreviewSources();
190
+ this.cancelPreviewBlockScheduling();
191
+ this.previewMixToken += 1;
123
192
  }
124
193
  updateTime(_timeUs) {
125
194
  }
@@ -131,89 +200,219 @@ class GlobalAudioSession {
131
200
  if (!this.isPlaying || !this.model || !this.audioContext) {
132
201
  return;
133
202
  }
134
- const lookaheadTime = audioContext.currentTime + this.LOOKAHEAD_TIME;
135
- if (this.nextScheduleTime === 0) {
136
- this.nextScheduleTime = audioContext.currentTime + 0.01;
137
- this.nextContentTimeUs = currentTimelineUs;
138
- }
139
- while (this.nextScheduleTime < lookaheadTime) {
140
- if (this.nextScheduleTime < audioContext.currentTime) {
141
- const timeDrift = audioContext.currentTime - this.nextScheduleTime;
142
- if (timeDrift > 0.02) {
143
- this.nextScheduleTime = audioContext.currentTime + 0.02;
144
- const skippedUs = Math.round(timeDrift * 1e6);
145
- this.nextContentTimeUs += skippedUs;
146
- } else {
147
- this.nextScheduleTime = audioContext.currentTime + 0.01;
148
- }
149
- }
150
- const chunkDurationUs = Math.round(this.CHUNK_DURATION * 1e6);
151
- const startUs = this.nextContentTimeUs;
152
- const endUs = startUs + chunkDurationUs;
153
- if (endUs > this.model.durationUs) {
154
- break;
155
- }
156
- try {
157
- await this.ensureAudioForTimeRange(startUs, endUs, {
158
- mode: "blocking",
159
- loadResource: true
160
- });
161
- const mixedBuffer = await this.mixer.mix(startUs, endUs);
162
- if (this.nextScheduleTime < audioContext.currentTime) {
163
- const timeDrift = audioContext.currentTime - this.nextScheduleTime;
164
- if (timeDrift > 0.02) {
165
- console.warn(
166
- `[Audio] Skip chunk due to time drift: ${(timeDrift * 1e3).toFixed(1)}ms`
167
- );
168
- this.nextScheduleTime = audioContext.currentTime + 0.02;
169
- this.nextContentTimeUs += chunkDurationUs;
170
- continue;
171
- }
172
- this.nextScheduleTime = audioContext.currentTime + 0.01;
203
+ this.previewLastTimelineUs = currentTimelineUs;
204
+ if (this.previewScheduleTask) return;
205
+ if (this.previewNextScheduleTime === 0) {
206
+ this.startPreviewBlockScheduling(currentTimelineUs, audioContext);
207
+ }
208
+ const token = this.previewScheduleToken;
209
+ this.previewScheduleTask = this.runPreviewBlockSchedulingLoop(audioContext, token).finally(
210
+ () => {
211
+ if (this.previewScheduleToken === token) {
212
+ this.previewScheduleTask = null;
173
213
  }
174
- const source = audioContext.createBufferSource();
175
- source.buffer = mixedBuffer;
176
- source.playbackRate.value = this.playbackRate;
177
- const gainNode = audioContext.createGain();
178
- gainNode.gain.value = this.volume;
179
- source.connect(gainNode);
180
- gainNode.connect(audioContext.destination);
181
- source.start(this.nextScheduleTime);
182
- this.scheduledSources.add(source);
183
- source.onended = () => {
184
- source.disconnect();
185
- gainNode.disconnect();
186
- this.scheduledSources.delete(source);
187
- };
188
- const actualDuration = mixedBuffer.duration;
189
- this.nextScheduleTime += actualDuration;
190
- this.nextContentTimeUs += chunkDurationUs;
191
- } catch (error) {
192
- console.warn("[GlobalAudioSession] Mix error, skipping chunk:", error);
193
- this.nextScheduleTime += this.CHUNK_DURATION;
194
- this.nextContentTimeUs += chunkDurationUs;
195
214
  }
196
- }
215
+ );
197
216
  }
198
217
  /**
199
218
  * Reset playback states (called on seek)
200
219
  */
201
220
  resetPlaybackStates() {
202
- this.stopAllAudioSources();
203
- this.nextScheduleTime = 0;
204
- this.nextContentTimeUs = 0;
221
+ this.stopAllPreviewSources();
222
+ this.cancelPreviewBlockScheduling();
223
+ this.previewMixToken += 1;
205
224
  }
206
225
  setVolume(volume) {
207
226
  this.volume = volume;
227
+ const audioContext = this.audioContext;
228
+ if (!audioContext) return;
229
+ const t = audioContext.currentTime;
230
+ for (const { gain } of this.previewScheduledSources) {
231
+ try {
232
+ gain.gain.cancelScheduledValues(t);
233
+ gain.gain.setValueAtTime(gain.gain.value, t);
234
+ gain.gain.linearRampToValueAtTime(volume, t + 0.01);
235
+ } catch {
236
+ }
237
+ }
208
238
  }
209
239
  setPlaybackRate(rate) {
210
240
  this.playbackRate = rate;
211
241
  this.resetPlaybackStates();
212
242
  }
243
+ startPreviewBlockScheduling(startTimelineUs, audioContext) {
244
+ this.cancelPreviewBlockScheduling();
245
+ this.stopAllPreviewSources();
246
+ const clampedUs = Math.max(0, startTimelineUs);
247
+ const blockIndex = Math.floor(clampedUs / this.PREVIEW_BLOCK_DURATION_US);
248
+ const blockStartUs = blockIndex * this.PREVIEW_BLOCK_DURATION_US;
249
+ this.previewFirstBlockOffsetUs = clampedUs - blockStartUs;
250
+ this.previewNextBlockIndex = blockIndex;
251
+ this.previewNextScheduleTime = audioContext.currentTime + 0.02;
252
+ this.previewLastTimelineUs = startTimelineUs;
253
+ }
254
+ cancelPreviewBlockScheduling() {
255
+ this.previewScheduleToken += 1;
256
+ this.previewScheduleTask = null;
257
+ this.previewNextBlockIndex = 0;
258
+ this.previewNextScheduleTime = 0;
259
+ this.previewFirstBlockOffsetUs = 0;
260
+ this.previewLastTimelineUs = 0;
261
+ }
262
+ realignPreviewSchedulingToTimeline(audioContext) {
263
+ const model = this.model;
264
+ if (!model) return;
265
+ const clampedUs = Math.max(0, this.previewLastTimelineUs);
266
+ const blockIndex = Math.floor(clampedUs / this.PREVIEW_BLOCK_DURATION_US);
267
+ const blockStartUs = blockIndex * this.PREVIEW_BLOCK_DURATION_US;
268
+ if (blockStartUs >= model.durationUs) return;
269
+ this.stopAllPreviewSources();
270
+ this.previewFirstBlockOffsetUs = clampedUs - blockStartUs;
271
+ this.previewNextBlockIndex = blockIndex;
272
+ this.previewNextScheduleTime = audioContext.currentTime + 0.02;
273
+ }
274
+ async runPreviewBlockSchedulingLoop(audioContext, token) {
275
+ while (this.isPlaying && this.previewScheduleToken === token) {
276
+ const model = this.model;
277
+ if (!model) return;
278
+ const nextBlockStartUs = this.previewNextBlockIndex * this.PREVIEW_BLOCK_DURATION_US;
279
+ if (nextBlockStartUs >= model.durationUs) {
280
+ this.previewNextScheduleTime = audioContext.currentTime + this.PREVIEW_SCHEDULE_AHEAD_SEC;
281
+ return;
282
+ }
283
+ const scheduleAheadTime = audioContext.currentTime + this.PREVIEW_SCHEDULE_AHEAD_SEC;
284
+ if (this.previewNextScheduleTime >= scheduleAheadTime) return;
285
+ const prevBlockIndex = this.previewNextBlockIndex;
286
+ const prevScheduleTime = this.previewNextScheduleTime;
287
+ await this.scheduleNextPreviewBlock(audioContext, token);
288
+ if (this.previewScheduleToken === token && this.previewNextBlockIndex === prevBlockIndex && this.previewNextScheduleTime === prevScheduleTime) {
289
+ const now = audioContext.currentTime;
290
+ if (now - this.previewLastStallWarnAt >= 1) {
291
+ this.previewLastStallWarnAt = now;
292
+ console.warn(
293
+ "[GlobalAudioSession][preview] scheduling stalled; stop loop to avoid spin",
294
+ {
295
+ prevBlockIndex,
296
+ prevScheduleTime,
297
+ now
298
+ }
299
+ );
300
+ }
301
+ return;
302
+ }
303
+ }
304
+ }
305
+ async getOrCreateMixedBlock(blockIndex) {
306
+ const model = this.model;
307
+ if (!model) return null;
308
+ const token = this.previewMixToken;
309
+ return await this.previewBlockCache.getOrCreate(blockIndex, async () => {
310
+ return await this.enqueuePreviewMix(async () => {
311
+ const startUs = blockIndex * this.PREVIEW_BLOCK_DURATION_US;
312
+ const endUs = Math.min(startUs + this.PREVIEW_BLOCK_DURATION_US, model.durationUs);
313
+ await this.ensureAudioForTimeRange(startUs, endUs, {
314
+ mode: "blocking",
315
+ loadResource: true
316
+ });
317
+ const mixed = await this.mixer.mix(startUs, endUs);
318
+ this.deps.cacheManager.clearAudioCache();
319
+ return mixed;
320
+ }, token);
321
+ });
322
+ }
323
+ async scheduleNextPreviewBlock(audioContext, token) {
324
+ const model = this.model;
325
+ if (!this.isPlaying || !model) return;
326
+ if (this.previewScheduleToken !== token) return;
327
+ if (this.previewNextScheduleTime < audioContext.currentTime + 0.01) {
328
+ this.realignPreviewSchedulingToTimeline(audioContext);
329
+ }
330
+ const blockIndex = this.previewNextBlockIndex;
331
+ const blockStartUs = blockIndex * this.PREVIEW_BLOCK_DURATION_US;
332
+ if (blockStartUs >= model.durationUs) {
333
+ this.previewFirstBlockOffsetUs = 0;
334
+ this.previewNextBlockIndex = blockIndex + 1;
335
+ this.previewNextScheduleTime = audioContext.currentTime + this.PREVIEW_SCHEDULE_AHEAD_SEC;
336
+ return;
337
+ }
338
+ const buffer = await this.getOrCreateMixedBlock(blockIndex);
339
+ if (!buffer) {
340
+ this.previewFirstBlockOffsetUs = 0;
341
+ this.previewNextBlockIndex = blockIndex + 1;
342
+ this.previewNextScheduleTime = audioContext.currentTime + 0.02;
343
+ return;
344
+ }
345
+ if (this.previewScheduleToken !== token) return;
346
+ const rate = this.playbackRate || 1;
347
+ const offsetUs = this.previewFirstBlockOffsetUs;
348
+ let startTime = this.previewNextScheduleTime;
349
+ let offsetSec = offsetUs > 0 ? offsetUs / 1e6 : 0;
350
+ const now = audioContext.currentTime;
351
+ if (startTime < now + 0.01) {
352
+ const targetStartTime = now + 0.02;
353
+ const skippedSec = Math.max(0, (targetStartTime - startTime) * rate);
354
+ startTime = targetStartTime;
355
+ offsetSec += skippedSec;
356
+ }
357
+ if (offsetSec >= buffer.duration) {
358
+ this.previewFirstBlockOffsetUs = 0;
359
+ this.previewNextBlockIndex = blockIndex + 1;
360
+ this.previewNextScheduleTime = startTime;
361
+ return;
362
+ }
363
+ const remainingSec = Math.max(0, buffer.duration - offsetSec);
364
+ if (remainingSec <= 0) return;
365
+ const source = audioContext.createBufferSource();
366
+ source.buffer = buffer;
367
+ source.playbackRate.value = rate;
368
+ const gainNode = audioContext.createGain();
369
+ const volume = this.volume;
370
+ const fadeSec = Math.min(this.PREVIEW_BLOCK_FADE_SEC, remainingSec / 2);
371
+ gainNode.gain.setValueAtTime(0, startTime);
372
+ gainNode.gain.linearRampToValueAtTime(volume, startTime + fadeSec);
373
+ gainNode.gain.setValueAtTime(volume, startTime + Math.max(fadeSec, remainingSec - fadeSec));
374
+ gainNode.gain.linearRampToValueAtTime(0, startTime + remainingSec);
375
+ source.connect(gainNode);
376
+ gainNode.connect(audioContext.destination);
377
+ source.start(startTime, offsetSec);
378
+ const entry = { source, gain: gainNode };
379
+ this.previewScheduledSources.add(entry);
380
+ source.onended = () => {
381
+ try {
382
+ source.disconnect();
383
+ gainNode.disconnect();
384
+ } catch {
385
+ }
386
+ this.previewScheduledSources.delete(entry);
387
+ };
388
+ this.previewFirstBlockOffsetUs = 0;
389
+ this.previewNextBlockIndex = blockIndex + 1;
390
+ this.previewNextScheduleTime = startTime + remainingSec / rate;
391
+ }
392
+ stopAllPreviewSources() {
393
+ for (const { source, gain } of this.previewScheduledSources) {
394
+ try {
395
+ source.disconnect();
396
+ } catch {
397
+ }
398
+ try {
399
+ source.stop(0);
400
+ } catch {
401
+ }
402
+ try {
403
+ gain.disconnect();
404
+ } catch {
405
+ }
406
+ }
407
+ this.previewScheduledSources.clear();
408
+ }
213
409
  reset() {
214
- this.stopAllAudioSources();
410
+ this.stopAllPreviewSources();
215
411
  this.deps.cacheManager.clearAudioCache();
216
412
  this.activeClips.clear();
413
+ this.previewBlockCache.clear();
414
+ this.cancelPreviewBlockScheduling();
415
+ this.previewMixToken += 1;
217
416
  }
218
417
  /**
219
418
  * Mix and encode audio for a specific segment (used by ExportScheduler)
@@ -269,16 +468,7 @@ class GlobalAudioSession {
269
468
  this.exportEncoder = null;
270
469
  this.exportEncoderStream = null;
271
470
  }
272
- stopAllAudioSources() {
273
- for (const source of this.scheduledSources) {
274
- try {
275
- source.disconnect();
276
- source.stop(0);
277
- } catch {
278
- }
279
- }
280
- this.scheduledSources.clear();
281
- }
471
+ // Preview source scheduling is managed by stopAllPreviewSources()/previewScheduledSources.
282
472
  /**
283
473
  * Core method to ensure audio for all clips in a time range
284
474
  * Unified implementation used by ensureAudioForTime, scheduleAudio, and export
@@ -466,26 +656,25 @@ class GlobalAudioSession {
466
656
  planes.push(buffer.getChannelData(channel));
467
657
  }
468
658
  return new AudioData({
469
- format: "f32",
470
- // interleaved format
659
+ format: "f32-planar",
471
660
  sampleRate,
472
661
  numberOfFrames,
473
662
  numberOfChannels,
474
663
  timestamp: timestampUs,
475
- data: this.interleavePlanarData(planes)
664
+ data: this.packPlanarF32Data(planes)
476
665
  });
477
666
  }
478
- interleavePlanarData(planes) {
667
+ packPlanarF32Data(planes) {
479
668
  const numberOfChannels = planes.length;
480
669
  const numberOfFrames = planes[0]?.length ?? 0;
481
670
  const totalSamples = numberOfChannels * numberOfFrames;
482
- const interleaved = new Float32Array(totalSamples);
483
- for (let frame = 0; frame < numberOfFrames; frame++) {
484
- for (let channel = 0; channel < numberOfChannels; channel++) {
485
- interleaved[frame * numberOfChannels + channel] = planes[channel][frame];
486
- }
671
+ const packed = new Float32Array(totalSamples);
672
+ for (let channel = 0; channel < numberOfChannels; channel++) {
673
+ const plane = planes[channel];
674
+ if (!plane) continue;
675
+ packed.set(plane, channel * numberOfFrames);
487
676
  }
488
- return interleaved.buffer;
677
+ return packed.buffer;
489
678
  }
490
679
  }
491
680
  export {