Completes the cycle-recording spec. Three related pieces:
MIDI separate takes (Preferences > Audio > "Cycle MIDI recording"):
- Each pass becomes its own MIDI clip, folded into a take folder — the same
shape audio always gets — instead of merging into one clip. Merge stays
the default.
- Notes are bucketed by the pass they were played in. The pass counter bumps
BETWEEN close_active_notes and the re-note_on at a wrap, so a key held
across the boundary has its sounding half filed under the pass that's
ending and its re-opened half under the pass that's beginning. Put the bump
on either side of that pair and the whole note lands in one pass; there's a
test named for exactly that.
- A silent INTERIOR pass still yields an empty take, so take N is always pass
N — otherwise the numbering silently shifts and "take 3" stops meaning "the
third time round". A TRAILING empty pass is dropped: that's what hitting
stop shortly after a wrap gives you, a stop artifact rather than a take you
played. (Audio already behaved this way via its short-final-take rule.)
- Still triggers on the wrap: stop inside the first pass and it's an ordinary
single recording, whatever the preference says.
Append to an existing take folder (AppendTakesAction):
- Cycle-recording over a region that already holds a take folder now ADDS to
that folder rather than dropping a second clip on top of it, which stranded
the new takes in an overlapping clip you couldn't audition against the old.
- "Same region" means same start AND same loop length: resize the cycle region
and you get a fresh folder, rather than takes of a different length appended
to an existing one, which would break the uniform-take invariant that
comping-via-split depends on.
- The recording's own clip/instance are throwaway scaffolding here (the takes
already live in the backend pools), so they're discarded; the single
AppendTakesAction is the whole undoable step.
Don't play the region you're recording over:
- While recording into a MIDI track, every clip on that track is silenced
EXCEPT the one being recorded into. A take folder already sitting in the
cycle region was otherwise playing its active take underneath you on every
pass, fighting the part you were trying to record.
- The recording clip itself is exempt, because in merge mode that's precisely
what you want to hear: the overdub you've been building up. Other tracks are
untouched.
The prior audio-tags commit put real FLAC + metadata into export/audio_exporter.rs
— which turned out to be dead code (declared, never called; whole file was
EngineController::start_export_audio → daw-backend's export_audio, which still
routed FLAC to the erroring hound stub — hence "not implemented in daw-backend".
Move the work to where export actually happens:
- daw-backend/src/audio/export.rs: real ffmpeg FLAC (16-bit S16 / 24-bit S32,
skipping the trailing empty flush packet the FLAC muxer rejects); apply_metadata
on MP3/AAC/FLAC output; RIFF LIST/INFO chunk appended to WAV. New metadata field
on the backend ExportSettings, threaded from the UI in run_audio_export. Tests
assert real fLaC magic + round-tripped tags, and a valid WAV INFO chunk.
- Delete the dead export/audio_exporter.rs (removes the duplicate FLAC impl).
Smart tag defaults (filled only when empty, never clobbering edits):
- Year → current civil year, computed from the system clock with i64 math (no
date crate; correct past 2038/2106 — tests cover post-i32/u32 timestamps).
- Artist → last-used value, else the OS username ($USER/%USERNAME%).
- Album → last-used value.
Last-used Artist/Album persist in AppConfig and prefill next export.
- FLAC is now real FLAC via ffmpeg, not WAV bytes in a .flac file. 16-bit uses
S16, 24-bit uses S32 (ffmpeg's flac encoder emits bits_per_raw_sample=24).
The flush emits a trailing empty packet that the FLAC muxer rejects as
"invalid data" — it's skipped.
- Tag metadata (title/artist/album/genre/year/track/comment) written into every
format via each container's native tags: ID3v2 (MP3), MP4 atoms (M4A), Vorbis
comments (FLAC) set through ffmpeg's output metadata; RIFF LIST/INFO appended
to the hound-written WAV (with a fixed-up RIFF size). New AudioMetadata type
on AudioExportSettings; dialog gains a Tags section and defaults Title to the
project name.
Tests: FLAC is a real fLaC container with round-tripped tags; WAV keeps a valid
RIFF with a working INFO chunk.
- VideoManager.frame_cache: unbounded HashMap (grew per distinct frame during
playback) -> LruCache evicted by a 256MB byte budget. Byte-budget rather than
frame count is robust across resolutions (a 4K frame is ~33MB vs ~2MB at
800x600). unload_video pops per-clip keys (LruCache has no retain).
- mux_video_and_audio: stream-merge the two inputs by PTS with one pending
packet per stream (O(1) memory) instead of collecting every packet into Vecs
first (O(duration)). Output is byte-identical.
- export AAC: sanitize the planar-f32 path (non-finite -> 0, finite clamped to
[-1,1]) like the integer paths, with a one-time warning. A stray NaN/Inf
render sample no longer fails the whole export.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>