Skip to content

Commit 24f736e

Browse files
olamide226EtienneLescot
authored andcommitted
fix(audio): stop tracks inside a trimmed stretch from playing
A track's head was projected onto the trim-compressed programme but its LENGTH was still read off the raw ruler. So a voiceover living entirely inside a trimmed stretch projected both of its ends onto the same cut, kept its full raw length there, and played — audible, and in the wrong place, with nothing on screen to account for it. Both ends now go through the same projection the head does. A track buried in a cut has a zero-length output span and is dropped from the preview and the mix list; one that merely crosses a cut shortens by exactly what the cut removed. The contiguous playback #502 chose across an interior trim is unchanged — this is only about how long a track lasts, not what it plays. Claude-Session: https://claude.ai/code/session_01DvB2GJXp18fPr79DHfk356
1 parent eeecb15 commit 24f736e

4 files changed

Lines changed: 149 additions & 3 deletions

File tree

‎src/components/ai-edition/VirtualPreview.audio.test.ts‎

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,75 @@ describe("resolveTimelineAudioPlayback", () => {
203203
});
204204
});
205205

206+
describe("resolveTimelineAudioPlayback under a trim", () => {
207+
const clip: AxcutClip = {
208+
id: "clip_1",
209+
assetId: "asset_1",
210+
sourceStartSec: 0,
211+
sourceEndSec: 20,
212+
timelineStartSec: 0,
213+
timelineEndSec: 20,
214+
wordRefs: [],
215+
origin: "user",
216+
reason: "",
217+
};
218+
const trim: AxcutTrimRange = {
219+
id: "trim_1",
220+
assetId: "asset_1",
221+
startSec: 4,
222+
endSec: 8,
223+
origin: "user",
224+
reason: "",
225+
};
226+
const project = (rawSec: number) => projectRawTimelineSecToPlayback([clip], [trim], rawSec);
227+
228+
const buried: AxcutAudioTrack = {
229+
id: "buried",
230+
assetId: "a1",
231+
kind: "music",
232+
startMs: 5000,
233+
endMs: 7000,
234+
durationSec: 30,
235+
offsetMs: 0,
236+
gainDb: 0,
237+
loop: false,
238+
fadeInMs: 0,
239+
fadeOutMs: 0,
240+
muted: false,
241+
label: "",
242+
origin: "user",
243+
};
244+
245+
it("never plays a track buried inside the trim", () => {
246+
// Both ends project onto the cut, so the track's OUTPUT span is zero. Read
247+
// off the raw span instead it stayed 2s long and played at the boundary,
248+
// with nothing on screen to explain the sound.
249+
const outStart = project(buried.startMs / 1000);
250+
const outSpan = project(buried.endMs / 1000) - outStart;
251+
expect(outSpan).toBeCloseTo(0, 6);
252+
for (const raw of [3, 5, 6, 9, 12]) {
253+
expect(resolveTimelineAudioPlayback(project(raw), outStart, buried, outSpan).shouldPlay).toBe(
254+
false,
255+
);
256+
}
257+
});
258+
259+
it("plays a track that merely crosses the trim, for the length that survives", () => {
260+
const crossing = { ...buried, id: "crossing", startMs: 2000, endMs: 12_000 };
261+
const outStart = project(crossing.startMs / 1000);
262+
const outSpan = project(crossing.endMs / 1000) - outStart;
263+
// Raw 2..12 with raw 4..8 cut leaves 6s of programme.
264+
expect(outSpan).toBeCloseTo(6, 6);
265+
expect(resolveTimelineAudioPlayback(project(3), outStart, crossing, outSpan).shouldPlay).toBe(
266+
true,
267+
);
268+
// Just past the end of what survives.
269+
expect(
270+
resolveTimelineAudioPlayback(outStart + 6.1, outStart, crossing, outSpan).shouldPlay,
271+
).toBe(false);
272+
});
273+
});
274+
206275
describe("timelineAudioFadeAt", () => {
207276
const track: AxcutAudioTrack = {
208277
id: "t1",

‎src/components/ai-edition/VirtualPreview.tsx‎

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -648,7 +648,19 @@ export function VirtualPreview({
648648
trimRangesRef.current,
649649
track.startMs / 1000,
650650
);
651-
const spanSec = Math.max(0, (track.endMs - track.startMs) / 1000);
651+
// The span on the OUTPUT programme, not the raw ruler: both ends go
652+
// through the same projection the head does. A track buried inside a
653+
// trimmed stretch collapses to zero length and stays silent, instead
654+
// of playing its full raw length parked at the cut — audible with
655+
// nothing on screen to explain it.
656+
const spanSec = Math.max(
657+
0,
658+
projectRawTimelineSecToPlayback(
659+
clipsRef.current,
660+
trimRangesRef.current,
661+
track.endMs / 1000,
662+
) - outputStartSec,
663+
);
652664
const trackTarget = resolveTimelineAudioPlayback(
653665
outputTimeSec,
654666
outputStartSec,

‎src/native/sceneDescription.test.ts‎

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2097,6 +2097,61 @@ describe("buildSceneDescription.audioTracks", () => {
20972097
expect(buildSceneDescription(doc).audioTracks[0]?.startSec).toBeCloseTo(3, 6);
20982098
});
20992099

2100+
it("drops a track that sits entirely inside a trimmed stretch", () => {
2101+
// The trim takes raw 4..8 out of the programme; a track living at raw 5..7
2102+
// has nowhere left to play. It used to project both ends onto the cut and
2103+
// then play its full raw length there — audible, and out of place, with
2104+
// nothing on screen to account for it.
2105+
const screen = makeAsset({ id: "scr", originalPath: "/screen.mp4", durationSec: 20 });
2106+
const doc = makeDoc({
2107+
assets: [screen, audioAsset],
2108+
clips: [
2109+
makeClip({
2110+
id: "c1",
2111+
assetId: "scr",
2112+
sourceStartSec: 0,
2113+
sourceEndSec: 20,
2114+
timelineStartSec: 0,
2115+
timelineEndSec: 20,
2116+
}),
2117+
],
2118+
timeline: {
2119+
trimRanges: [
2120+
{ id: "t1", assetId: "scr", startSec: 4, endSec: 8, reason: "", origin: "user" },
2121+
],
2122+
},
2123+
audioTracks: [{ ...track, startMs: 5000, endMs: 7000 }],
2124+
});
2125+
expect(buildSceneDescription(doc).audioTracks).toEqual([]);
2126+
});
2127+
2128+
it("shortens a track by the trim it crosses", () => {
2129+
// Raw 2..12 with raw 4..8 cut is 6s of programme, not 10.
2130+
const screen = makeAsset({ id: "scr", originalPath: "/screen.mp4", durationSec: 20 });
2131+
const doc = makeDoc({
2132+
assets: [screen, audioAsset],
2133+
clips: [
2134+
makeClip({
2135+
id: "c1",
2136+
assetId: "scr",
2137+
sourceStartSec: 0,
2138+
sourceEndSec: 20,
2139+
timelineStartSec: 0,
2140+
timelineEndSec: 20,
2141+
}),
2142+
],
2143+
timeline: {
2144+
trimRanges: [
2145+
{ id: "t1", assetId: "scr", startSec: 4, endSec: 8, reason: "", origin: "user" },
2146+
],
2147+
},
2148+
audioTracks: [{ ...track, startMs: 2000, endMs: 12_000, offsetMs: 0 }],
2149+
});
2150+
const [entry] = buildSceneDescription(doc).audioTracks;
2151+
expect(entry.startSec).toBeCloseTo(2, 6);
2152+
expect(entry.trimEndSec - entry.trimStartSec).toBeCloseTo(6, 6);
2153+
});
2154+
21002155
it("drops a track whose asset has no resolvable path", () => {
21012156
const doc = makeDoc({ assets: [], audioTracks: [track] });
21022157
expect(buildSceneDescription(doc).audioTracks).toEqual([]);

‎src/native/sceneDescription.ts‎

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -550,13 +550,23 @@ export function buildSceneDescription(
550550
if (!asset?.originalPath) return [];
551551
const sourceDurationSec = asset.durationSec ?? track.durationSec;
552552
const offsetSec = track.offsetMs / 1000;
553-
const spanSec = (track.endMs - track.startMs) / 1000;
554-
if (spanSec <= 0) return [];
555553
const startSec = projectRawTimelineSecToPlayback(
556554
projectedClips,
557555
document.timeline.trimRanges,
558556
track.startMs / 1000,
559557
);
558+
// Measure the span on the OUTPUT programme, not the raw ruler. A track
559+
// sitting inside a trimmed stretch projects BOTH its ends onto the same
560+
// boundary, so its output span is zero and it is dropped — where the raw
561+
// span kept it alive and played it, in full, at the cut. A track that
562+
// merely crosses a trim shortens by what the cut removed.
563+
const endSec = projectRawTimelineSecToPlayback(
564+
projectedClips,
565+
document.timeline.trimRanges,
566+
track.endMs / 1000,
567+
);
568+
const spanSec = endSec - startSec;
569+
if (spanSec <= 0) return [];
560570
const base = {
561571
path: asset.originalPath,
562572
gainDb: track.gainDb,

0 commit comments

Comments
 (0)