Commit Graph
186 Commits
Author SHA1 Message Date
Brandon Thomas 47f75df15e Merge pull request #182 from justjohn12345/pr/layer-box-visibility
Graphics: only selected layers have a box, and a hidden layer has none
2026-10-07 17:44:08 -04:00
Brandon Thomas 91a36a4a3f Merge pull request #166 from justjohn12345/pr/text-box-modes
Graphics: point text and paragraph text, as Premiere has them
2026-10-07 17:44:07 -04:00
justjohn12345andClaude Opus 5.5 b6281f968f Graphics: only selected layers have a box, and a hidden layer has none
Every layer of the selected graphic was outlined on the Program monitor,
so a box stayed after its layer was deselected, and a layer whose
visibility was off kept its box and could still be clicked and dragged.

As in Premiere Pro 26: a layer has a box only while it is selected (or
typed into), and a layer whose visibility is off is not on the monitor
at all: no box, no handles, no click target.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 13:24:43 -04:00
Brandon Thomas 5695626216 Merge pull request #156 from storytold/fix/docs-85-86-87-88-99
Docs: releasing guide, CLI export options, README and CLAUDE.md drift (#85, #86, #87, #88, #99)
2026-10-07 10:49:14 -04:00
Brandon ThomasandClaude Opus 5.5 fa67dfa372 Docs: releasing guide, CLI export options, README and CLAUDE.md drift (#85, #86, #87, #88, #99)
- docs/releasing.md (#99): release.yml points readers to it twice and it didn't exist. Written from
  the workflow and packaging scripts as they are (macOS, Windows x64/x86/arm64, Linux x86_64/aarch64
  with the glibc 2.35 floor, web; draft release, secrets).
- filmcraft-cli help (#86): export lists --scale, --quality, --no-audio and --queue, and apv among
  the formats.
- README (#87): more than 650 engine commands (671 today), not about 135.
- README (#88): the web line says the browser app ships with every release and links docs/web.md.
- CLAUDE.md (#85): the Illustrator-class sibling is ../vectorcraft (formerly DrawCraft).

Fixes #85
Fixes #86
Fixes #87
Fixes #88
Fixes #99

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 10:49:08 -04:00
Brandon Thomas 2647a2d9f8 Merge remote-tracking branch 'origin/main' into pr151
# Conflicts:
#	crates/engine/src/settings_tests.rs
#	crates/engine/src/transcript_tests.rs
2026-10-07 10:44:47 -04:00
Brandon ThomasandClaude Opus 5.5 bd12dcae81 Bridge: a blocking export runs as an app job, so it neither times out nor freezes the app (#91, #92)
file.exportMedia with wait: true was forwarded to the app unchanged. The app then encoded on its UI
thread: it showed no progress and answered no other control request until the encode ended, and the
control server's 60 s reply limit reported "timeout" for any longer export while it went on and
succeeded.

BridgeClient::execute now starts the export without wait and polls jobs.list until the job
finishes, returning the same {job, path, result} (or the export's error). filmcraft-cli --bridge
exec and MCP bridge mode both use it, and MCP command_run in bridge mode now gets the same progress
notifications and cancellation as headless mode.

Tested against a fake app (wait: true never reaches it, one export started, result and error
returned) and end to end against the desktop app: a blocking demo export returned its 642-frame
result with exit 0 while ui.inspect was answered in ~0.05 s throughout.

Fixes #91
Fixes #92

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 10:42:28 -04:00
carlitosteso3andClaude Opus 5.5 b8fa786cad transcripts: disabled, not failing, in builds without speech-to-text (#89, #97, #98)
- transcript.generate is enabled only with a recogniser: one installed in
  Session::transcriber, or the build's own (feature `whisper`). Otherwise
  describe, command_list enabled_only and the menu report it disabled with
  "speech-to-text is not available in this build" as the reason (#97).
  Transcribe Sequence, which runs it, follows.
- transcript.downloadModel is enabled only with `speech-download` (#98).
- file.import with Automatically transcribe clips on no longer skips
  transcription silently when it can't run: the disabled command's reason
  lands in the import's errors as "transcription: ..." (#89).
- docs/transcripts.md describes the disabled state.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 09:14:05 -05:00
Brandon ThomasandClaude Opus 5.5 65c6b7365d MCP: render_frame and render_preview leave the playhead where it was (#114)
Both tools are annotated readOnlyHint, but a call with `seconds` moved the active sequence's
playhead (and, with Selection Follows Playhead on, the selection) to that time, in headless and
bridge mode alike.

- headless: Session::render_program_at renders the frame at `seconds` (frame-snapped, as the
  playhead would be) without touching the session.
- bridge: the Program monitor screenshot needs the live playhead there, so the tool records the
  playhead and selection, takes the screenshot and puts both back (the selection only if it changed,
  since selecting also leaves trim mode).

Regression test: rendering at 5 s and 8 s with Selection Follows Playhead on leaves playhead and
selection unchanged and still renders the requested frame (it moved the playhead to 8 s before).

Fixes #114

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 09:55:24 -04:00
justjohn12345andClaude Opus 5.5 8407f233c1 Graphics: point text and paragraph text, as Premiere has them
A text layer is now one of two types, each with Premiere Pro 26's handles:

- Point text (Type tool click): every handle scales the layer about its
  anchor point, both axes alike; the font size is untouched.
- Paragraph text (Type tool drag): the dragged rectangle is a box the text
  wraps in. Handles move that side or corner of the box, the opposite side
  stays, the text re-wraps at the same size. Lines below the box are not
  shown and the bottom-right handle turns into a red plus.
- The anchor point can be dragged with the Selection tool; the layer stays.
  It is picked before anything under it, so a shape dragged by its exact
  centre moves its anchor.
- Properties > Text > wrench opens Text Properties, where Text Layer Type
  switches a layer between the two (graphics.setTextType); the text stays
  where it is.

Shapes keep stretching away from their opposite side.

New: text layer parameter box_height, graphics.newText `box`, and
textType / box / overflow / anchor / scale in graphics.list. A layer saved
before a parameter existed can now be given it with graphics.set.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 09:54:26 -04:00
Brandon Thomas cd3fe20ef9 Merge remote-tracking branch 'origin/main' into apv-105 2026-10-07 07:09:54 -04:00
Brandon Thomas 16475596d3 Merge remote-tracking branch 'origin/main' into nest-116
# Conflicts:
#	crates/engine/src/commands.rs
2026-10-07 07:09:20 -04:00
Brandon Thomas d4e438e932 Merge remote-tracking branch 'origin/main' into apv-105 2026-10-07 07:08:58 -04:00
Brandon Thomas 7916cfb80e Merge pull request #131 from punkhop/fix/audio-plays-ahead
Audio: mix playback 200 ms ahead so a busy Mac doesn't drop sound
2026-10-07 07:08:29 -04:00
Brandon Thomas 005745d9ec Merge remote-tracking branch 'origin/main' into apv-105
# Conflicts:
#	crates/codecs/src/mp4.rs
2026-10-07 06:01:14 -04:00
Brandon Thomas 19d2f15148 Merge remote-tracking branch 'origin/main' into nest-116
# Conflicts:
#	crates/ui-egui/src/lib.rs
#	crates/ui-egui/src/panels/timeline.rs
2026-10-07 06:00:36 -04:00
Brandon Thomas a4c4d38550 Merge pull request #123 from Luskish/fix/control-close-on-malformed-input
Control: close the connection on input that isn't a request (#119)
2026-10-07 05:57:55 -04:00
Peet GuercioandClaude Opus 5.5 bb5d7ca0a6 Audio: mix playback 200 ms ahead so a busy Mac doesn't drop sound
Problem: playback decoded and mixed every ~10 ms device buffer inside the
cpal callback (AAC decode, effects, opening a clip's file at a cut, rayon,
locks). On a loaded Mac any stall missed the device deadline: static and
dropouts, different on every play.

Solution: play_ahead mixes on its own thread into a lock-free ring kept
200 ms ahead; the callback only copies. An underrun plays silence, is
counted, and the mixer skips the same span so sound stays locked to the
picture. Any device width plays the channels it shares with the mix.
perf.stats reports playback.audio (callbacks, underruns, missingFrames,
minLeadMs). The program mix moved to playback_mix so bench_audio can play a
real project through a simulated 48 kHz device and count dropouts.

Context: on a real 22-sequence project, load average 38-58, 15 s per cut:
mixing in the callback overran 8-170 times, mixing ahead 0 (ring never
below 149 ms). The web build keeps mixing in its callback (no threads).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 23:47:47 -07:00
LuskishandClaude Opus 5.5 023127e146 Control: close the connection on input that isn't a request (#119)
A line that is not a JSON object with a string `method` (an HTTP request
line, other non-JSON text, a JSON array, invalid UTF-8, a line over 4 MiB)
now gets one error reply and the connection is closed, so a browser's
cross-origin fetch can't smuggle a command in its body. Connections are
capped at 16. Same transport as LightCraft (storytold/lightcraft#129).

Fixes #119.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 22:32:13 -04:00
justjohn12345andClaude Opus 5.5 1ebc1440e7 NEST11: the architecture doc says a plain nest's layers go into the frame plan
docs/architecture.md still listed every nested sequence among the things
rendered on the CPU and handed to the GPU as one image. Since NEST11 that
holds only for nests that are not plain.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 20:53:53 -04:00
justjohn12345andClaude Opus 5.5 e13151d87b NEST10: the captions doc says nested captions are part of the nest's picture
docs/captions.md still said nested sequences do not burn in their own
captions. Since NEST10 the captions of a nested sequence are drawn wherever
the nest is shown, in the Program monitor and in exports.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 20:53:53 -04:00
justjohn12345andClaude Opus 5.5 9b31f4eb84 NEST15: nested sequences in AAF and OMF, the way Premiere Pro writes them
A nested sequence was left out of both formats as a gap, with a report entry
telling the user to "render and replace" it first (a command FilmCraft does
not have). An AAF with nested compositions was read with gaps in their place.

What Premiere Pro 26.5.2 does, read off files it exported for a sequence with
a nest:
- AAF: the nested sequence is a composition mob of its own, and its clips are
  source clips that point at it.
- OMF: the nested sequence's sound is rendered into the document as one clip
  named after the sequence.

Export
- AAF: a nested sequence is written as a composition mob (no usage code; the
  exported sequence stays the top-level one). A clip of it is a source clip
  at that mob's first slot of the clip's kind, its start in that slot's edit
  units. Nests in nests are followed; a nest used twice is written once. With
  embedded or consolidated audio, the media of the clips inside a nest is
  prepared with the rest (`NestNeeds::Inside`).
- OMF: `audio_needs` lists the range of the nested sequence each nest clip
  plays (`NestNeeds::Render`); the engine mixes it through the export audio
  pipeline with what is on the clip, and it is written as media. Without that
  essence the nest is a gap and the report names the sequence.

Import (AAF)
- A source clip that points at a composition becomes a clip of a nested
  sequence; the composition is read once.
- Premiere Pro's layout is read: all video tracks in one slot (a nested scope
  with a segment per track; before, only the last segment was read, so a
  Premiere AAF came in with one video track), and "top-level" tagged on nested
  compositions too (top-level are those no other composition uses).
- A composition without a frame size takes that of the first picture it
  shows, not of the first picture in the file.

A sequence that claims to be inside itself is a gap with a report entry in
both directions; the walks are bounded (16 levels).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 20:53:34 -04:00
justjohn12345andClaude Opus 5.5 eeec772671 NEST14: a project reopens with its sequence tabs and their views
Premiere Pro 26.5.2 reopens a project with the same sequence tabs, in the
same order, the same one shown, each at the zoom it was left at. FilmCraft
opened every project on its first sequence; the "Restore open sequences when
opening projects" preference existed and nothing read it.

A saved project file now has a `view` beside `project`: the open sequences in
tab order, the active one, and each sequence's view from NEST12. It is not
part of the project: no undo step, no "edited" mark, and no schema bump (older
builds ignore it). `file.open` restores it when the preference is on.

Nothing in the view is trusted. Ids that are not sequences of the project and
repeats are dropped, numbers are clamped, and a view that cannot be read is
ignored instead of failing the open. A view with no open sequence leaves the
project on its first sequence, so a project saved by a script or the CLI does
not open on an empty Timeline.

Opening or recovering a project now tells the frontend which sequence is
shown, so the Timeline takes that sequence's view.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 20:23:19 -04:00
mweinbach 15787f7fe8 Codecs: add clean-room APV (RFC 9924) decoder, encoder, container support, and export presets 2026-10-06 17:24:34 -04:00
EthanSuquetandClaude Opus 5.5 3eae70b11e h264enc: strict ffmpeg oracle that sees damaged slices (#74)
ffmpeg prints "corrupt decoded frame" as a warning, so the oracle's
`-v error -ec 0` decode let a stream with a whole slice missing pass
silently. Switching that decode to `-v warning` is not enough: with
`-ec 0`, ffmpeg 7.1/8.1 flags every picture that has more than one
slice, libx264's multi-slice streams included, while the pixels are
unchanged. That is the 119/120 in #74; the encoder's bitstream is not
at fault (VideoToolbox decodes the same streams bit-identically, and
ffmpeg with strict error detection reports nothing).

The oracle now decodes with ffmpeg's default concealment, `-v warning`
and `-err_detect +crccheck+bitstream+buffer+explode`, fails on any
output or a non-zero exit, and still compares every pixel with the
encoder's reconstruction. A negative control drops and truncates
slices (CAVLC and CABAC, 3 slices) and checks the oracle reports each
one; it fails with the old flags.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 16:02:53 -04:00
Brandon ThomasandClaude Opus 5.5 7f8e102b13 Fonts: the Japanese interface uses craft-fonts first; absolute CRAFT_FONTS_DIR
The merged Japanese interface (#38) looked only for a system font. When
FilmCraft is built with craft-fonts, theme::install already appends the
Japanese faces to every font family, so switching to Japanese uses them
(no system scan) and works on the web too; otherwise it falls back to an
installed system font as before. Docs use an absolute CRAFT_FONTS_DIR
(build.rs runs in crates/text, so a relative path resolves from there).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 14:26:14 -04:00
Brandon ThomasandClaude Opus 5.5 9953befe28 Fonts: use craft-fonts as an optional build input
crates/text/build.rs embeds the fonts of a storytold/craft-fonts checkout
named by CRAFT_FONTS_DIR as filmcraft_text::fonts::CRAFT_FONTS (native:
all of them; wasm32: only BIZ UDPGothic Regular). Unset, CRAFT_FONTS is
empty and nothing changes. A bad CRAFT_FONTS_DIR is a warning, or an
error with CRAFT_FONTS_REQUIRED=1. Never a Cargo dependency, no network.

- UI: the Japanese faces are the last fallback of every egui font family
  (BIZ UDPGothic first, Bold first in the semibold/medium families).
- Text engine: registered after the bundled faces; fallback_for picks
  them after the bundled faces and before system faces (Mincho for serif
  text, Gothic otherwise, nearest weight).
- Tests: Japanese glyphs (no tofu) in the UI and title paths, skipped
  without craft-fonts; the theme works with CRAFT_FONTS empty.
- Release: every build job checks out craft-fonts at a pinned commit and
  builds with CRAFT_FONTS_DIR + CRAFT_FONTS_REQUIRED; packages carry
  OFL-<family>.txt. NOTICE, ATTRIBUTION.md (optional build input), AGENTS.md,
  CLAUDE.md, README and docs/contributing.md describe it.

FilmCraft main bundles no Japanese font, so no font files move.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 11:40:29 -04:00
Brandon Thomas 5c8471c9f4 Merge pull request #41 from justjohn12345/pr/decode-memory
MEM1–MEM4 + HW1: clip memory on a timeline from 12 GB to 1.9 GB; hardware decode actually on
2026-10-06 11:38:03 -04:00
Brandon Thomas 475c294678 Merge pull request #45 from punkhop/fix/move-linked-partners
engine: timeline.move takes linked partners along
2026-10-06 11:37:38 -04:00
Brandon Thomas 51026bd2b2 Merge pull request #60 from punkhop/fix/place-on-a-sound-track
engine: timeline.place on an audio track places the sound there
2026-10-06 11:37:32 -04:00
Brandon Thomas d886d2f322 Merge pull request #44 from punkhop/fix/unknown-track-is-an-error
engine: naming a track that does not exist is an error
2026-10-06 11:37:28 -04:00
Brandon Thomas 39ef62607d Merge pull request #47 from punkhop/fix/app-help-and-unknown-flags
filmcraft: --help prints usage and an unknown option is an error
2026-10-06 11:37:22 -04:00
Brandon Thomas 8dfa8fff4c Merge pull request #52 from punkhop/fix/explicit-targets-enable
engine: a command whose targets are named in the parameters does not need a selection
2026-10-06 11:37:16 -04:00
Brandon Thomas 8a795324f4 Merge pull request #50 from punkhop/fix/replace-reports-short-media
engine: Replace With Clip reports clips that run past the new media
2026-10-06 11:37:11 -04:00
Brandon Thomas eb3ca3e811 Merge pull request #49 from punkhop/fix/replace-from-bin-keep-source-in
engine: clip.replaceFromBin can keep the clip's source In
2026-10-06 11:37:05 -04:00
Brandon Thomas 950879ef10 Merge pull request #59 from punkhop/fix/inspect-reports-reverse
engine: sequence.inspect reports a reversed clip
2026-10-06 11:36:58 -04:00
Brandon Thomas 7377def476 Merge pull request #48 from punkhop/fix/sequence-inspect-readback
engine: sequence.inspect reports clip ends, gain and marker comments
2026-10-06 11:36:53 -04:00
Brandon Thomas 5bc07026c7 Merge pull request #43 from punkhop/fix/import-auto-points
engine: a project saved after an interchange import opens again
2026-10-06 11:36:46 -04:00
Brandon Thomas a407099019 Merge pull request #46 from punkhop/fix/export-interchange-unknown-format
engine: file.exportInterchange refuses an unknown format
2026-10-06 11:36:36 -04:00
Brandon Thomas bccc3dd031 Merge pull request #42 from punkhop/fix/mp4-audio-thumps
codecs: resampled MP4 audio reads apply the edit list once
2026-10-06 11:36:31 -04:00
Brandon Thomas 39d81aee14 Merge remote-tracking branch 'origin/main' into pr-47-fix 2026-10-06 08:46:53 -04:00
Brandon Thomas 508a5a933f Merge pull request #28 from storybored379/pr/mcp-conventions
MCP: tool annotations, core tools, resources, export progress and cancellation
2026-10-06 07:27:58 -04:00
Brandon Thomas 5012fbd598 Merge pull request #19 from storybored379/pr/deterministic-export
Export: same file on every machine (fixed mux interleave, H.264 slices by frame size)
2026-10-06 07:27:44 -04:00
punkhopandClaude Opus 5.5 4df7e75085 engine: timeline.place on an audio track places the sound there
Problem: `timeline.place {item, track: "A3"}` (an audio track given as
`track`). For an item with picture and sound it put a video item on the
audio track A3 and a second, sound clip on the default audio track. For
a sound-only item it ignored `track` and placed on the default audio
track.

Solution: `track` is where the picture goes and `audioTrack` where the
sound goes. An audio track given as `track` now places the sound alone,
on that track. What cannot be meant is a parameter error that says why,
and changes nothing: an audio track as `track` together with
`audioTrack`; a video track as `audioTrack`; an audio track for an item
with no sound; a video track alone for an item with no picture. Calls
that name a video track as `track` and an audio track as `audioTrack`
(the timeline panel's drop does) are unchanged. The parameter hint and
`docs/agents.md` say so.

Tests: track_params_tests.rs places a clip with picture and sound and a
sound-only item on audio tracks by name and by id, checks each refusal
for its error kind, its text, an untouched project and undo history,
and checks the usual forms still work.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 23:14:24 -07:00
punkhopandClaude Opus 5.5 7eb4f1fc7b engine: sequence.inspect reports a reversed clip
Problem: a clip set to play backward (`clip.speedDuration` with
`reverse: true`) read back from `sequence.inspect` exactly like a
forward clip: same `speed`, same source range, nothing that said it was
reversed. An agent could set it and not confirm it.

Solution: clips gain `reverse` (bool) in `sequence.inspect`, named as
in the project file and in the command. Additive; existing keys are
unchanged. `docs/agents.md` lists it and says what it means for the
source range.

Tests: sequence_inspect_tests.rs reverses a demo clip through the
command and checks that only that clip reports `reverse: true`, and
none after undo.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 22:45:21 -07:00
punkhopandClaude Opus 5.5 ff8aa8749d engine: a project saved after an interchange import opens again
Problem: a Final Cut 7 XML whose Basic Motion filter sets only `scale`
makes the importer add the Motion effect with its defaults, where
`position` and `anchor` are the NaN "auto" sentinel. Placing a clip
resolves those with `resolve_auto_points`; the interchange import did
not (any format: the clips are built with the effect defaults).
serde_json writes NaN as `null`, and the reader then refused the file:
"project file is damaged: invalid type: null, expected f64". The project
could be saved but never opened again.

Solution:
- project: a point parameter's value (`ParamValue::Vec2`) reads a `null`
  coordinate back as NaN, so "auto" survives a save. Only there: `Vec2`
  itself is unchanged, and a `null` in a mask vertex or any other point
  is still a damaged file.
- project: `Project::resolve_placed_auto_points` resolves the points of
  the clips in chosen sequences. Points measured in the frame use the
  sequence size. `anchor` is measured in the clip's source
  (`Project::source_size`, a subclip through its parent, as the renderer
  resolves it) and is resolved only for sources the caller says have
  their final size.
- engine: the import resolves in the edits it already makes, so the undo
  steps are what they were. In "Import ...": everything but the anchors
  of file media, whose size is only what the document says so far (an
  EDL says nothing and the importer assumes the sequence size). In each
  "Link Media": the anchors of that file's clips, from the probed size.
  A clip whose file is not found keeps an "auto" anchor, which the
  renderer centres in whatever size the media has, as before this
  change.
- engine: a later Link Media (relink.rs `apply`, behind `media.relink`
  and `media.autoRelink`) resolves those waiting anchors in its own undo
  step, when it has read the file. Nothing else resolves them: a file
  that merely comes back at its old path, or a forced relink that does
  not read the file, leaves the anchor "auto" (still rendered centred).

Why no schema step and no repair on load: the file's shape is not
changed. Files with `null` points were written by every build before
this one, at the current schema version, so a migration (keyed on the
version) cannot select them, and a bump would only make every new
project unreadable to those builds. They open now and their points are
"auto", exactly as in the session that wrote them; resolving them on
load would mean guessing the size of media that was never linked.
docs/project-files.md describes `null` points.

Tests: project
`point_params_keep_auto_through_json_and_no_other_point_reads_null` and
`placed_auto_points_resolve_once_the_source_size_is_final`; format
`auto_points_written_as_null_open_again`; engine
(interchange_auto_points_tests.rs): import with the media missing, save,
reopen, undo, redo; a linked file whose real size differs from the
declared and the sequence size (undo of Link Media and of the import,
redo); an EDL clip smaller than its sequence; import with the media
missing, save, reopen, relink (anchor becomes a number, undo, redo, a
second relink leaves it). All fail without the change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 20:28:54 -07:00
punkhopandClaude Opus 5.5 8f74d18829 engine: timeline.move takes linked partners along
Behaviour change to the default: with Linked Selection on (the default),
`timeline.move` now also moves the linked partners of the listed clips.
A partner lands on its own track in the same mode as the listed clip, so
a caller that lists only a picture clip can now overwrite audio at the
destination time. `"linked": false` gives the old behaviour (only the
listed clips move); the timeline panel passes it, so dragging is
unchanged.

Problem: `timeline.move {"moves": [{clip, track, time}]}` moved only the
listed clip. Its linked sound stayed where it was, out of sync, although
Linked Selection was on and `timeline.trim`, slide and select all act on
linked partners. Separately, a `time` near i64::MAX overflowed the tick
addition in the edit (a panic in debug builds, a wrapped position in
release).

Solution: a linked partner that is not itself in `moves` moves by the
same time offset and stays on its own track. A partner that is listed is
moved once, to where the list says. Two kinds of partner are left
exactly as they are: one on a locked track (the listed clip still moves,
as it did before), and one whose position would not change, which is the
case when the listed clip only changes track. Re-placing such a partner
would drop the transitions at its edges and report a move that did not
happen. A new optional `linked` parameter overrides the toggle for one
call. The command returns `{"moved": [ids]}`, the clips that moved.

No clip is moved before the sequence start. When one would be (a split
edit whose sound starts earlier, or several listed clips asked too far
left), all clips of the call land later by the same amount, so the
spacing asked for is kept. This replaces the per-clip clamp for a call
that lists several clips with a time below zero; a single clip still
lands at zero. The offsets are summed in i128 and checked on the way
back: a start past Tick::MAX is a parameter error and nothing moves.

Tests (timeline_move_tests.rs): partners follow, undo / redo, both
listed, `linked: false`, toggle off, `linked: true`; a locked partner; a
picture moved to another track at the same time leaves its sound and the
sound's crossfade untouched; the common stop at the sequence start with
a split edit and with two listed clips; hostile times (i64::MAX,
i64::MIN, junk). All fail without the change, the hostile one with the
overflow panic. docs/control-protocol.md and docs/agents.md describe the
default, the `linked` parameter and the result.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 20:24:23 -07:00
punkhopandClaude Opus 5.5 6de389c75a engine: a command whose targets are named in the parameters does not need a selection
Problem: a command gated on the selection refused to run although the
caller named what to act on. `clip.replaceFromBin {clips, item}` answered
"select a clip in the Project panel"; `edit.clear`, `edit.rippleDelete`,
`edit.pasteAttributes`, `clip.enable`, `clip.link`, `clip.speedDuration`
and `clip.audioGain` with `clips` answered "no clips selected". A script
or agent had to change the user's selection first. The cause is the order
in Session::execute: `enabled()` runs before the parameters are read, and
it only knows the selection.

Fix: Session::execute asks enablement per call. When `enabled()` passes,
nothing changes. When it fails and the parameters name targets under a
key the command documents as ids (`clips` / `clip`: clips of the active
sequence, `items` / `item`: project items), the same `enabled()` runs
once more with those targets standing in for the selection, and the
selections are put back before the command runs. So only the selection
condition is answered by the named targets: no sequence, an empty
clipboard or the wrong kind of clip still disable the command, ids of
nothing do not count, and a command that does not document the key
(`edit.cut`) still needs a selection. This works for every command
because the predicates already resolve their targets the way `run` does
with empty parameters. `is_enabled` (menus, `engine.commands`) is
unchanged.

`clip.audioGain` already read `clips`; its parameter doc now says so.

Tests (explicit_targets_tests.rs): the commands above with and without
parameters, undo/redo, the selections left untouched, menu enablement
unchanged, and the cases that must still be refused. Four of the five
fail without the change in execute.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 20:05:12 -07:00
punkhopandClaude Opus 5.5 5bf2203bb8 codecs: resampled MP4 audio reads apply the edit list once
Problem: a clip with AAC audio in MP4 whose sample rate differs from the
sequence's (48 kHz clip in a 44.1 kHz sequence) exported with loud
low-frequency thumps, several times full scale, recurring through the clip.
Every exported container was affected (AAC clips them into thumps, PCM
clips at full scale); a WAV export had a single one at the clip's start.

Cause: Mp4Source::audio decodes a window starting at s0, the requested
presentation sample minus the edit-list offset, then resamples it. The
resampler derived its sub-sample phase from s0, so the edit-list offset
(-1024 samples of AAC priming) was applied a second time: the read
position started about 1024 samples before the window, was clamped to
index 0 with a large negative fraction, and linear interpolation
extrapolated a ramp over the first ~940 samples of every request. Export
asks for audio one batch of video frames at a time, so the ramp repeated
at each request. Sources at the sequence rate take the copy path and
were unaffected, as were tracks without an edit list.

Fix: take the phase from the presentation sample the window starts at
(before the edit-list offset is subtracted).

Test: aac_mp4_resampled_reads_follow_the_edit_list
(crates/codecs/tests/mp4_oracle.rs, with `require_ffmpeg!` and the
shared fixture helper as docs/testing.md asks of new oracle tests) reads
an ffmpeg AAC fixture at 44.1 kHz in frame-sized pieces and compares it
with the 48 kHz decode interpolated at the same times (peak was 7.35
against a source level of 0.15 before the fix). docs/testing.md lists it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 19:29:26 -07:00
punkhopandClaude Opus 5.5 d3ee1dfd22 engine: Replace With Clip reports clips that run past the new media
Problem: when the replacement is shorter than the clip's slot, the clip
silently runs past the end of its media and the renderer holds the last
frame. Nothing in the command's result says so, so a script or agent
that swaps a take cannot tell.

Solution: the result of the three Replace With Clip commands
(`clip.replaceFromSource`, `clip.replaceFromSourceMatchFrame`,
`clip.replaceFromBin`) gains `short`: one `{clip, shortByFrames}` per
replaced clip that now plays past the end of its media, in sequence
frames and never more than the clip's own length. It is empty when none
does; stills and other sources without an end, and held frames, are
never short. The edit itself is not changed, and the parameter docs are
untouched (results are described in docs/control-protocol.md).

Test: `replace_with_clip_reports_clips_that_run_past_the_new_media`
(From Bin with a 12-frame subclip, undo / redo, From Source Monitor,
Match Frame, the cases that are not short, and an In past the end of the
new media capped at the clip's length).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 19:28:20 -07:00