feat(ui): clip effects + transitions, original-sound volume, video/audio import, file-lane slide/trim, audio lanes heard on the monitor, wheel customisation; fix: timeline syntax break; render: separated lane starts at its clip's place in the render
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
61e856af37
commit
01fc0e4a71
@@ -147,7 +147,17 @@ def _live_tracks(project, clips_by_scene: dict) -> list[dict]:
|
||||
if t.get("kind") != "separated" or not t.get("scene_id") or t["scene_id"] in clips_by_scene]
|
||||
|
||||
|
||||
def _audio_tracks(project, clips_by_scene: dict, resolve) -> list[dict]:
|
||||
def _clip_starts(clips: list[dict]) -> dict:
|
||||
"""Where each clip begins in THIS render (clips that are out of the cut take no room)."""
|
||||
at, out = 0.0, {}
|
||||
for c in clips:
|
||||
if c.get("scene_id"):
|
||||
out[c["scene_id"]] = at
|
||||
at += _f(c.get("dur"))
|
||||
return out
|
||||
|
||||
|
||||
def _audio_tracks(project, clips_by_scene: dict, resolve, starts: dict | None = None) -> list[dict]:
|
||||
"""The project's extra audio lanes as {path, start_sec, volume[, source_in, source_dur]}.
|
||||
|
||||
A "separated" lane is a clip's own sound pulled onto a lane: it plays the audio pinned at
|
||||
@@ -158,6 +168,8 @@ def _audio_tracks(project, clips_by_scene: dict, resolve) -> list[dict]:
|
||||
separated = t.get("kind") == "separated" or (t.get("scene_id") and not t.get("media_ref")
|
||||
and t.get("kind") != "overlay")
|
||||
start, vol = _f(t.get("start_sec")), _f(t.get("volume"), 1.0)
|
||||
if separated and starts and t.get("scene_id") in starts: # a separated lane rides its clip: where the clip is in this render, plus how far it was slid
|
||||
start = max(0.0, starts[t["scene_id"]] + _f(t.get("offset_sec")))
|
||||
if separated:
|
||||
clip = clips_by_scene.get(t.get("scene_id")) or {}
|
||||
path = None
|
||||
@@ -279,7 +291,7 @@ def render(project, clips: list[dict]) -> dict:
|
||||
raise RenderError(str(exc)) from exc
|
||||
|
||||
by_scene = {c["scene_id"]: c for c in clips if c.get("scene_id")}
|
||||
tracks = _audio_tracks(project, by_scene, files.clip_path) if clips or project.audio_tracks else []
|
||||
tracks = _audio_tracks(project, by_scene, files.clip_path, _clip_starts(clips)) if clips or project.audio_tracks else []
|
||||
keep = bool(project.keep_original_audio) and not blank
|
||||
|
||||
cmd = [ffmpeg, "-y"]
|
||||
|
||||
@@ -337,3 +337,16 @@ def test_a_separated_lane_goes_quiet_with_a_clip_that_is_not_in_the_cut():
|
||||
{"id": "a", "kind": "separated", "scene_id": "gone"}, {"id": "b", "kind": "separated", "scene_id": "here"},
|
||||
{"id": "c", "media_ref": "aaaaaaaaaaaa", "start_sec": 0}])
|
||||
assert [t["id"] for t in stitch._live_tracks(proj, {"here": {}})] == ["b", "c"]
|
||||
|
||||
|
||||
def test_a_separated_lane_starts_where_its_clip_is_in_this_render_plus_its_slide(tmp_path, monkeypatch):
|
||||
f = tmp_path / "x.mp4"
|
||||
f.write_bytes(b"")
|
||||
monkeypatch.setattr(stitch.files, "has_audio", lambda p: True)
|
||||
proj = projects.Project(width=160, height=120, audio_tracks=[
|
||||
{"id": "a", "kind": "separated", "scene_id": "s2", "start_sec": 99, "offset_sec": 0.5}, # start_sec is the editor's guess, the render's own timeline wins
|
||||
{"id": "b", "kind": "separated", "scene_id": "s2", "offset_sec": -9}])
|
||||
clips = [{"scene_id": "s1", "dur": 2.0}, {"scene_id": "s2", "dur": 3.0}]
|
||||
by = {c["scene_id"]: c for c in clips}
|
||||
out = stitch._audio_tracks(proj, by, lambda c: str(f), stitch._clip_starts(clips))
|
||||
assert [round(t["start_sec"], 2) for t in out] == [2.5, 0.0]
|
||||
|
||||
Reference in New Issue
Block a user