@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.
- package/dist/cache/AudioMixBlockCache.d.ts +18 -0
- package/dist/cache/AudioMixBlockCache.d.ts.map +1 -0
- package/dist/cache/AudioMixBlockCache.js +57 -0
- package/dist/cache/AudioMixBlockCache.js.map +1 -0
- package/dist/cache/CacheManager.d.ts +1 -1
- package/dist/cache/CacheManager.d.ts.map +1 -1
- package/dist/cache/CacheManager.js +1 -2
- package/dist/cache/CacheManager.js.map +1 -1
- package/dist/cache/l1/AudioL1Cache.d.ts +0 -15
- package/dist/cache/l1/AudioL1Cache.d.ts.map +1 -1
- package/dist/cache/l1/AudioL1Cache.js +57 -65
- package/dist/cache/l1/AudioL1Cache.js.map +1 -1
- package/dist/controllers/PlaybackController.d.ts +0 -1
- package/dist/controllers/PlaybackController.d.ts.map +1 -1
- package/dist/controllers/PlaybackController.js +16 -35
- package/dist/controllers/PlaybackController.js.map +1 -1
- package/dist/controllers/PlaybackStateMachine.d.ts.map +1 -1
- package/dist/controllers/PlaybackStateMachine.js +22 -2
- package/dist/controllers/PlaybackStateMachine.js.map +1 -1
- package/dist/controllers/types.d.ts +3 -0
- package/dist/controllers/types.d.ts.map +1 -1
- package/dist/controllers/types.js +1 -0
- package/dist/controllers/types.js.map +1 -1
- package/dist/orchestrator/GlobalAudioSession.d.ts +27 -7
- package/dist/orchestrator/GlobalAudioSession.d.ts.map +1 -1
- package/dist/orchestrator/GlobalAudioSession.js +292 -103
- package/dist/orchestrator/GlobalAudioSession.js.map +1 -1
- package/dist/orchestrator/OnDemandVideoSession.d.ts.map +1 -1
- package/dist/orchestrator/OnDemandVideoSession.js +13 -0
- package/dist/orchestrator/OnDemandVideoSession.js.map +1 -1
- package/dist/orchestrator/Orchestrator.d.ts +1 -1
- package/dist/orchestrator/Orchestrator.d.ts.map +1 -1
- package/dist/orchestrator/Orchestrator.js +44 -17
- package/dist/orchestrator/Orchestrator.js.map +1 -1
- package/dist/stages/compose/OfflineAudioMixer.d.ts.map +1 -1
- package/dist/stages/compose/OfflineAudioMixer.js +4 -1
- package/dist/stages/compose/OfflineAudioMixer.js.map +1 -1
- package/dist/stages/load/ResourceLoader.d.ts +6 -5
- package/dist/stages/load/ResourceLoader.d.ts.map +1 -1
- package/dist/stages/load/ResourceLoader.js +53 -39
- package/dist/stages/load/ResourceLoader.js.map +1 -1
- package/dist/stages/load/TaskManager.d.ts +5 -12
- package/dist/stages/load/TaskManager.d.ts.map +1 -1
- package/dist/stages/load/TaskManager.js +60 -46
- package/dist/stages/load/TaskManager.js.map +1 -1
- package/dist/stages/mux/MP4Muxer.d.ts.map +1 -1
- package/dist/stages/mux/MP4Muxer.js +3 -0
- package/dist/stages/mux/MP4Muxer.js.map +1 -1
- package/dist/utils/platform-utils.d.ts +1 -0
- package/dist/utils/platform-utils.d.ts.map +1 -1
- package/dist/utils/platform-utils.js +19 -6
- package/dist/utils/platform-utils.js.map +1 -1
- package/dist/workers/stages/decode/video-decode.worker.BnWVUkng.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"GlobalAudioSession.d.ts","sourceRoot":"","sources":["../../src/orchestrator/GlobalAudioSession.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AAE7C,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AACjD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AACvD,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;
|
|
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
|
-
//
|
|
16
|
-
|
|
17
|
-
//
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
78
|
+
const model = this.model;
|
|
79
|
+
if (!model) return;
|
|
39
80
|
const mode = options?.mode ?? "blocking";
|
|
40
|
-
const
|
|
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.
|
|
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.
|
|
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.
|
|
118
|
-
await this.
|
|
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.
|
|
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
|
-
|
|
135
|
-
if (this.
|
|
136
|
-
|
|
137
|
-
this.
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
if (
|
|
143
|
-
this.
|
|
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.
|
|
203
|
-
this.
|
|
204
|
-
this.
|
|
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.
|
|
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
|
-
|
|
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.
|
|
664
|
+
data: this.packPlanarF32Data(planes)
|
|
476
665
|
});
|
|
477
666
|
}
|
|
478
|
-
|
|
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
|
|
483
|
-
for (let
|
|
484
|
-
|
|
485
|
-
|
|
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
|
|
677
|
+
return packed.buffer;
|
|
489
678
|
}
|
|
490
679
|
}
|
|
491
680
|
export {
|