The landing branch already syncs host-set values to the controller before
saving state (lock-free atomics + EditorLink::sync_param); the PR's later
ControllerPending table did the same, so keep one. Keeps the PR's new
'a loaded state replaces earlier edits' assertion, which passes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#309 and this branch both added io::read_file and read_text. There is
one of each now: read_file checks the automation read root (#309), then
refuses anything but a regular file (this branch), and read_text is
read_file as UTF-8. Opening a session keeps #309's explicit read-root
check, so a refused path is reported with its reason, then decodes
through this branch's decode_file. The clip group importer and
decode_file take their de-duplication key from a new io::file_key,
since #309's test keeps std::fs calls inside io.rs. Session saves use
#309's make_dir. Import Session Data keeps decoding through
decode_file, so the load_source that #309 had switched to read_file
stays removed.
#304's MIDI Track Offset shifts the block schedule_notes reads, which
now carries this branch's controllers too. chase_controllers takes the
same offset, so a seek chases the controller values in effect where the
offset puts the track, not at the raw transport position. New test: with
the track 0.5 s late, starting at 1.25 s chases the sustain pedal still
down (without the offset it sent the pedal-up that only plays at 1.5 s).
Also: reset() sets the chase flag beside #301's alignment flush, the
VST3 Instance keeps #400's set_normalized beside midi_mapping, and the
mix tests keep #210's Keys plugin beside this branch's Clock and Ears.
Timecode::to_frames takes main's i128 version (#260), which already
covers the overflow this branch guarded with saturating arithmetic.
The Spot dialog formats and parses through Session::format_position
and parse_position, so it also gets #294's feet+frames start. Nudge
keeps #363's `next` parameter beside this branch's Edit Mode, Snap to
Grid and Shuffle Lock commands. The mode tooltips use main's `fkey`.
Main's track rows (#382 tint, #383 headers, #359 grid over clips) take
this branch's borrowed track and shared session snapshot. The ruler
labels use main's format_position with the feet+frames start, but keep
the frame rate timebase_ruler is given, so the second timecode ruler
still shows its own rate without a session copy.
#309 confines file writes to the automation write root: make_dir,
write_path and replace_file now check every path first. This branch
bounces through Bounce, which streams to a .part file and renames it
over the target, so Bounce::new now checks both paths before it creates
the part file. That covers Bounce Mix, bus bounces and stems, which
main writes through write_file and write_path. Stems keep the 4-hour
limit and use make_dir. replace_file keeps #309's checks in front of
this branch's replace_with. automation_roots' bounce cases pass.
#400 (fixes#257) made Vst3Plugin::save_state tell the edit controller
the values the host set before taking the snapshot. This branch reads
plugin state on the UI thread through EditorLink instead, and removes
Vst3Plugin::save_state, so keeping its side of that conflict would have
dropped the fix.
The values now wait in a ControllerPending table shared by the instance
and its EditorLink: one AtomicU64 slot per parameter, which set_param
fills on the audio thread without locking or allocating.
EditorLink::save_state sends the waiting values with setParamNormalized
under its lock, before it reads the controller's state. A state load
clears them, as #400's refresh did. Instance::set_normalized, which only
the old save_state used, is gone. #400's round-trip test passes, plus a
check that a loaded state replaces earlier host edits.
#327's a_new_instance_replaces_a_matching_one_in_its_slot told the two
instances apart through MixEngine::capture_states, which this branch
removes (states are read on the UI thread). The two instances now
differ in gain instead (-6 and -20 dB), and the test checks that the new
level plays and that the old instance is retired.
Also: Side::new sets #210's old_sessions field.
Press a checkbox and drag over its neighbours: each box the pointer
passes takes the state the first one switched to, until the button is
released. A click toggles one box, as before. Same design as VectorCraft
and PhotoCraft.
widgets::checkbox wraps egui's checkbox and adds the drag; every
ui.checkbox call in ui-egui uses it. Only boxes the pointer can see are
set, so rows scrolled out of a list or under another window stay as
they are.
Tests: widgets::tests (drag sets, drag unchecks and stops at release,
hidden rows untouched, a click toggles one) and tests/group_dialog.rs
(dragging down the Group dialog's track list groups every track).
Window > Mix in Separate Window (window.mix_separate, kept with the UI
preferences) shows the Mix window in its own native window through an
egui immediate viewport, so Edit and Mix can be seen at once, e.g. one
per monitor. The main window then keeps Edit; Window > Mix and
Cmd/Ctrl+= bring the Mix window to the front; closing it puts Mix back
into the main window. Backends without multiple windows show it as a
floating egui window.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Applying new audio device settings replaced the player while plugin
editor windows were open: the old instances went away with it (possibly
on the audio thread, where windows cannot be closed) and their editor
windows stayed on screen orphaned. Player::close_editors closes them on
the main thread first.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>