Commit Graph
630 Commits
Author SHA1 Message Date
Ian MartinandClaude Opus 5.5 ced57be723 vst3-host: the late-exit test expects Windows' exit order
module_exit_shutdown asserts that the exit handler alone runs the module
exit too late. That holds where exit handlers share one list. Windows
runs the executable's handlers before a DLL's own, so the fixture's late
state is still alive and the child exits cleanly; the test failed there.
The Windows case now asserts the clean exit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 10:34:41 -06:00
Ian MartinandClaude Opus 5.5 f55010d244 vst3-host: fixture builds on Windows (bus flags are i32 there)
The VST3 bindings type C enum constants as i32 on Windows and u32
elsewhere, so assigning kDefaultActive to BusInfo.flags (u32) failed to
compile and seven integration tests could not build their fixture.
Cast it like the other constants in this file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 10:34:41 -06:00
Ian MartinandClaude Opus 5.5 b2f6361265 engine: build the expected missing-file path the way the engine does
saved_audio_is_trusted_only_in_the_folder_it_was_saved_in compared path
strings. On Windows the engine reports <session folder>\Audio Files/x.wav
while the test expected <folder>/Audio Files/x.wav, so the test failed
there although the two name the same file. The expected value is now the
session folder joined with the stored relative path, on every platform.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 10:34:41 -06:00
Ian MartinandClaude Opus 5.5 218cbb6670 vst3-host: build the symlink test on Unix only
On Windows the test body was compiled out but its setup was not, so
`lib` was an unused variable and clippy -D warnings failed the gate.
The test never ran anything on Windows; it is now cfg(unix) as a whole.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 10:34:41 -06:00
Ian MartinandClaude Opus 5.5 557553cb06 video: collapse the MP3 esds branch into a match guard
Rust 1.95's clippy flags the nested if (collapsible_match), so
cargo xtask ci failed on a machine with the README's minimum toolchain.
No behaviour change: other MP3 sample entries still fall through to the
catch-all arm.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 10:34:41 -06:00
Limestudio90andClaude Opus 5.5 a1e9660e60 Audio device settings: system, devices, sample rate, buffer size; opt-in ASIO
Setup > Playback Engine now chooses the audio system (cpal host), output
and input device, device sample rate and buffer size, kept per machine
with the UI preferences and applied by reopening the devices (refused
while recording; the transport position is kept). ui.audio_device does
the same for agents and lists every system with its devices, channels,
rates and buffer range.

Player and recorder open the chosen device, fall back to the defaults
for anything unavailable (unplugged device, unsupported rate), retry
with the device's default buffer when the chosen one is refused, and
convert 32/16-bit integer devices from and to the f32 mix through
scratch buffers allocated up front (no allocation in the callbacks).

ASIO is behind an opt-in 'asio' feature (playback and app): building it
downloads Steinberg's ASIO SDK (proprietary licence or GPLv3) and needs
LLVM, so it stays off by default. Verified on Windows with an SSL 2+:
WASAPI output/input switching and buffer 256, ASIO duplex at 128.

Docs: G-01 shrunk, hardware-parity rows, roadmap log.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 18:22:41 +02:00
grovebit 35609b756b Make the Zoom Preset, Waveform Zoom and Clip Rating keys work; add Commands Focus 1-5 and N
From #186: waveform zoom could not be zoomed back out, zoom presets had
no keys, and N did nothing for Insertion Follows Playback.

The registry writes three shortcuts as key families: "Cmd+Ctrl+1-5"
(Zoom Preset), "Cmd+Alt+0-5" (Clip Rating) and "Cmd+Alt+[ / ]"
(Waveform Zoom). The shortcut parser can't read them, so they never
fired. The menus still showed them.

- The keyboard handler takes these keys itself, as it does F1-F4:
  Cmd+Alt+[ and ] zoom the waveforms out and in, Cmd+Alt+0-5 rate the
  selected clips and Cmd+Ctrl+1-5 recall the zoom presets. Other digits
  do nothing.
- Waveform Zoom and Clip Rating work on every OS (Ctrl+Alt on Windows
  and Linux). Cmd+Ctrl+1-5 stays a macOS key, like the other Mac
  Control chords, which the shortcut list already leaves out off macOS.
- Commands Focus (the a-z button) gains Pro Tools' 1-5 for the zoom
  presets and N for Insertion Follows Playback, on every OS.
2026-10-12 17:19:46 +01:00
Limestudio90andClaude Opus 5.5 434c60047d clap-host: host embedded plugin editors on Windows
Plugins that offer no floating editor (most CLAP plugins on Windows) now
get a host window of SoundCraft's: the GUI is created embedded, parented
to a top-level HWND owned by the app window, sized from get_size, resized
on request_resize (applied by the editor's idle on the main thread, then
set_size), and torn down when the user closes the window. Floating
editors work as before; other platforms still report embedded editors
as unsupported.

Docs: plugin-parity editor rows and the roadmap log.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 17:44:36 +02:00
Limestudio90andClaude Opus 5.5 b1cb6cb368 vst3-host: host plugin editors on Windows
Adds a Windows implementation of the editor host window: a top-level HWND
owned by the app's active window (no taskbar button), sized from the
view's rect, resized on IPlugFrame::resizeView, hidden on close and torn
down by the editor link's idle like on macOS. The main thread is the one
Rust names "main", where the app runs its message loop.

Also fixes two Windows-only build problems that kept the VST3 tests from
running there: the fixture's bus flags (the vst3 bindings use i32 enums
with MSVC) and an unused variable in a unix-only test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 17:24:26 +02:00
grovebit c9e53ee704 Look up clip effects and Real-Time Properties without allocating
Once a session has any edit.values entry (click settings, Real-Time
Properties, clip effects), MixEngine::render allocated every block,
although the crate docs say steady-state rendering doesn't: every audio
clip formatted two Strings and collected a Vec to look up its clip
effects, and every MIDI clip formatted four keys for its Real-Time
Properties. Strips render on rayon's workers while the audio callback
waits for them, or on the audio thread itself below 4 strips.

Lookups now format their keys on the stack (Key) and range over
edit.values with &str bounds. A new test checks that Real-Time
Properties still reach the instrument.

A counting allocator on all threads, over 1000 blocks of the demo
session with a clip effect on every clip and Real-Time Properties on
every track, saw 10 to 43 allocations per block before and none after,
apart from rayon's job queue (a new chunk every 63 blocks) and the
one-time setup of each clip's effects when it first plays.
2026-10-12 16:02:37 +01:00
grovebit 2d42cf278c Free replaced session snapshots on the UI thread
Every document change sends the audio callback a new Arc<Session>.
Engine::session_mut clones the document whenever the callback still holds
the previous snapshot, so after a non-undoable edit (selection, scrolling)
and on every step of a merged gesture (a fader drag) the callback held the
only reference to the snapshot it replaced. Replacing it freed the whole
tree (tracks, clips, automation, plugin state strings) inside the audio
callback: 268 frees for the demo session, and more for real ones.

The callback now hands the replaced snapshot back over a bounded channel
(preallocated, so try_send never allocates), and the UI thread drops it in
Player::idle, as it does retired plugin instances. update_session empties
the channel before each send, so it holds one or two at a time; if it is
ever full, the snapshot is dropped in place, as before.
2026-10-12 16:02:29 +01:00
grovebit 56ffce6dec Take a plugin that puts out NaN out of the mix instead of silencing it
One insert or instrument putting out NaN or infinity silenced the whole
mix: the value spread through the busses and the main mix (NaN plus
anything is NaN), and the output guard turned all of it into silence.
It can also poison the state of filters, reverbs and compressors
downstream, which then stay silent after the plugin has calmed down.

After each insert (track or master) and each instrument processes, the
engine checks its block. If the block holds NaN or infinity, the engine
silences that block of the strip and skips the plugin until the next
MixEngine::reset (playback starts or stops), which resets it and tries
it again. Meanwhile the strip plays on without it, as if bypassed. The
check costs about 60 ns per insert for a stereo 512-sample block.

StripMeter::plugin_failures counts how often a strip lost a plugin and
names the slot of the latest, so the UI names that plugin in the status
line each time, also when it fails again after a restart. The AU host
no longer zeroes non-finite output itself, so Audio Units are taken out
and named like CLAP and VST3 plugins.
2026-10-12 15:50:58 +01:00
grovebit 4c21ff8fe4 Keep a plugin's saved settings when it refuses to load them
When a third-party plugin refused its stored state (a newer plugin
version that can't read it, a damaged blob), it ran with its defaults,
and the next save or two-minute autosave read its state back into the
session: the saved settings were gone, even after updating the plugin.

create_instance now says whether the plugin refused the state
(NewInstance::refused_state). Playback leaves such an instance out of
the states it reads for saving, so the session keeps the stored state,
until the plugin's own editor is opened: from then on its state is the
user's work. Parameter values are saved apart from the state, so edits
made with SoundCraft's own controls survive either way.
2026-10-12 15:50:58 +01:00
grovebit 793432bbfc Read plugin state on the UI thread, not the audio thread
Saving, bouncing and the two-minute autosave read every third-party
plugin's state through the audio callback: a command made the callback
call each plugin's save_state between two blocks and reply, while the
UI waited for up to 2 s. So:
- CLAP's clap.state and VST3's getState ran on the audio thread,
  although both formats want them on the main thread.
- The callback allocated and waited on the plugins; a plugin with a
  large state (a sampler) could make the audio drop out on every save
  and autosave.
- Without a running callback (a stalled or lost device), every save
  waited 2 s and then kept stale plugin states.

Each instance now hands Side::prepare a state reader before it leaves
for the audio thread, the way it hands over its editor handle. CLAP's
GuiLink and VST3's EditorLink (which now holds the component as well)
read the state under their lock; Audio Units get a StateLink that does
the same. Each instance cuts its link off before it is destroyed, so a
reader never calls into a dead plugin. Player::capture_states reads
them on the calling (UI) thread, and the CaptureStates command is gone.

Each host's tests read the state while another thread processes the
instance, and get nothing once it is dropped. Playback reads states
with no audio thread at all.
2026-10-12 15:50:58 +01:00
Isaac Levine 57baab86d4 MCP: confine the headless session to the automation roots 2026-10-12 10:50:05 -04:00
Isaac Levine 29497296a1 Engine: check automation read and write roots on every path a command reads or writes 2026-10-12 10:50:04 -04:00
Marco Arnulfo 4bacfb13d5 Apply MIDI track offsets when scheduling notes (fixes #234)
event.midi_track_offsets stored midi.offset.<track> but the note
scheduler never read it, so the render was unchanged. schedule_notes
now looks that many samples back into the clips, which plays the track
exactly as if its MIDI had been moved by the offset. The scheduler is
shared by offline bounce and realtime playback.
2026-10-12 16:48:50 +02:00
Marco Arnulfo d5f069417e Keep the last selection out of the document so selecting leaves the session clean 2026-10-12 16:47:56 +02:00
grovebit 4b98064e46 Automation passes replace the automation they play over
A write pass added a point every 20 ms of playback and never removed
the old points between them, so the old automation survived between
the new points and the level jumped between the two rides. A covered
window, which runs only once a second since #192, wrote one point a
second and kept almost all of the old ride.

automation.set_point takes an optional `from`, and the points between
`from` and `at` are removed first. Each point of a pass gives the
previous one, so it replaces everything since then however far apart
the passes run, and Stop writes a last point where playback stopped.
A locate or a restart while playing starts afresh, so a jump never
erases what it skipped.

The test writes over a dense old ride with passes a quarter second
apart, jumps twice and stops, then checks each stretch. Removing any
of the four changes fails it.
2026-10-12 15:47:08 +01:00
Marco Arnulfo 1f7a4ab3e4 Restore Last Selection brings back the previous selection (fixes #231) 2026-10-12 16:42:19 +02:00
Marco Arnulfo 4235def686 Locate clears plugin and delay-compensation state (fixes #247) 2026-10-12 16:40:16 +02:00
Scott Campbell 7d8c185d13 Add a sampler UI: sample strip, drag and drop, instrument sliders
Built-in instruments now open a window with their parameters (driven by
mix.instrument_param). The Sampler's window adds a waveform with draggable
start and loop markers, Load Sample and Clear. Dropping an audio file on an
instrument track or a Sampler window loads it; in the browser the dropped
bytes are read and loaded through engine::io::load_sampler_bytes as one undo
step.
2026-10-12 10:39:45 -04:00
Marco Arnulfo 3765a32b9a fix(mix): pan automation reads the same in playback and bounce (fixes #244) 2026-10-12 16:36:59 +02:00
Marco Arnulfo 5e7ca6de22 Apply the Feet+Frames reference to counters and typed positions (fixes #235)
Setup > Current Feet+Frames Position stored its origin in
feet_frames.start but nothing read it, so every Feet+Frames reading
still counted from sample 0. format_position/parse_position now take
the origin alongside the timecode start, and Session::format_position
and Session::parse_position pass both, so the counters, rulers,
session.format_time and typed positions share one reference.
2026-10-12 16:36:18 +02:00
MarekSpodBiedry dabcb4840a Open .m4b audiobook files like .m4a
M4B is the same MP4/AAC container as M4A, and the decoder already reads it,
but the Open dialog, the web file picker and the file browser hid .m4b files.
Add m4b to those lists and to the extension fallback.
2026-10-12 16:27:57 +02:00
grovebit eb5bf699c7 Bounce Mix: Browse for the file, and start in the session folder
From #186: the Bounce dialog had no Browse button, so the file path had
to be typed, and it always proposed the home folder.

- A Browse… button opens the system save panel with an Audio filter
  (WAV, AIFF, FLAC). The chosen file goes in the field, and its
  extension sets the Format.
- A saved session bounces into its own folder by default; unsaved
  sessions still start in the home folder.
- The desktop save panel now picks its filter by purpose (bounce or
  session) and opens in the proposed file's folder when it exists, so
  Save As starts there too instead of wherever the panel was last.
- The file field is 420pt wide, like the other path prompts.
2026-10-12 15:13:25 +01:00
grovebit 57edb43856 Rename a marker by double-clicking it in the ruler
From #186 ("markers created using Numpad Enter cannot be renamed"):
double-clicking a marker in a marker ruler added a second marker on top
of it, whose label overprinted the first. Renaming was only possible
from the Memory Locations list.

- Double-clicking a marker's flag now opens a Rename Marker dialog,
  like double-clicking a marker in Pro Tools opens its memory location.
  The field opens with the old name selected, as Rename Track and
  Rename Clip do, and OK runs `markers.edit`.
- Double-clicking empty ruler space still adds a marker, and a single
  click on a flag still recalls it.
2026-10-12 15:07:37 +01:00
grovebit 5a156c2468 Drag or type the tempo in the toolbar
The first point of #189: clicking and dragging the toolbar tempo did
nothing, because it was painted text. Typing a tempo needed Tempo
Operations or a tempo-ruler dialog.

- The tempo is now a drag value styled like the counter. It looks the
  same when idle and shows a faint frame on hover. Drag to change it
  (Shift for fine steps, 0.1 BPM per point otherwise), or click and
  type a value. The range is 5-1000 BPM, as the tempo map allows.
- It edits the tempo change in effect at the playhead, the one the
  field shows, so later tempo changes are left alone.
- A drag is one undo step.
2026-10-12 15:02:11 +01:00
grovebit 7a58e49775 Reorder tracks by dragging them in the Tracks list
Fixes #170: dragging a row in the Tracks list did nothing. Pro Tools
reorders tracks when their names are dragged in the Track List, and
the Edit window's name plates already did that here.

- Rows now sense drags. While dragging, a line marks the gap between
  rows where the track will land, and the cursor shows a grab.
- Dropping runs `track.move`, so it is one undo step. Dropping a row
  back on its own gap changes nothing.
- The gap comes from the row pitch (row height plus the list's item
  spacing), so long drags land where the line is.
- A click still selects the track as before.
2026-10-12 14:55:40 +01:00
Marco Arnulfo 4e14c980a4 fix(edit): trims stop at the neighbouring clips instead of overlapping them (#228, #229)
Trim to File Boundaries and Trim to Fill Selection extended a clip within
its source without looking at the clips around it, leaving two unfaded
clips over the same samples. Both now go through one helper that caps the
extension at the end of the previous clip and the start of the next one on
the same playlist, the way the other commands that grow an existing clip
do (Paste Merge MIDI, step input). An existing crossfade overlap is kept.
2026-10-12 15:50:38 +02:00
grovebit cf9cced90d Create MIDI clips with the Pencil, Consolidate and the MIDI Editor
Fixes #282: on an empty MIDI or instrument track there was no UI way to
get a MIDI clip, so notes couldn't be entered by hand. `midi.new_clip`
existed but had no menu item, key or tool.

- Consolidate now works on MIDI and instrument tracks. The MIDI in the
  selection becomes one clip covering the whole selection, gaps
  included. It keeps the sound: muted clips stay silent, and notes are
  cut where their clip ended. An empty selection gives an empty clip,
  as in Pro Tools. Audio tracks are unchanged.
- The Pencil draws a MIDI clip on an empty part of a MIDI or instrument
  track: a click makes one bar, a drag covers the bars it crosses, with
  an outline while dragging. Clips start and end on barlines, the rule
  Pro Tools uses for MIDI it creates. A clip stops at its neighbour
  rather than overwriting it, and clicks on existing clips add nothing.
  The new clip is selected, so the MIDI Editor shows it.
- With an empty MIDI or instrument track selected, the MIDI Editor says
  so, and a click adds a one-bar clip at the insertion point (or a clip
  over the selection).
2026-10-12 14:49:51 +01:00
Marco Arnulfo e5d5996f7d fix(event): Constant tempo applies only over its range (#238) 2026-10-12 15:36:40 +02:00
Marco Arnulfo 93ddd419f5 fix(track): Clear Trim Automation removes the trim lane (#233) 2026-10-12 15:24:32 +02:00
Marco Arnulfo c85952ecc3 fix(edit): cut clip gain keeps the removed gain for paste (#230) 2026-10-12 15:17:52 +02:00
Marco Arnulfo 5f4cc96578 fix(midi-editor): Backspace in a text field leaves the selected notes alone (#223) 2026-10-12 15:13:18 +02:00
Marco Arnulfo fe919e5d57 fix(clip): Elastic Properties stretches the clip by the requested ratio (#227) 2026-10-12 14:46:05 +02:00
Marco Arnulfo 96ea49c7f6 fix(dsp): chorus and flanger tails follow their feedback (#242) 2026-10-12 14:44:57 +02:00
Marco Arnulfo ae46052cac fix(dsp): pitch_shift reaches ±48 semitones (#243) 2026-10-12 14:34:02 +02:00
Marco Arnulfo 35e9d63bee fix(markers): markers.edit applies the documented name (#236) 2026-10-12 14:30:57 +02:00
Marco Arnulfo 9144570b6e fix(video): H.264 parameter sets check their arithmetic before range checks (#251, #252) 2026-10-12 12:38:03 +02:00
Marco Arnulfo c73a8e2272 fix(eq): switches and choices redesign the filter on a running EQ (#239) 2026-10-12 12:34:35 +02:00
Marco Arnulfo f792eae580 fix(midi-editor): Quantize snaps to the grid the editor shows (#226) 2026-10-12 12:32:41 +02:00
Marco Arnulfo 0d18612a87 fix(edit-window): Pencil clicks on plugin lanes use the plugin's range (#224) 2026-10-12 12:30:02 +02:00
Marco Arnulfo bff9ca1390 fix(window): Hide All Floating Windows also hides Automation (#253) 2026-10-12 12:18:00 +02:00
Marco Arnulfo 89ff513b78 fix(video-universe): filmstrip thumbnails use the clip's own movie time (#225) 2026-10-12 12:15:28 +02:00
Marco Arnulfo 7d173d3a25 fix(midi-editor): a selected video clip is not a MIDI target (#258) 2026-10-12 12:13:21 +02:00
Marco Arnulfo 4c4414d006 fix(window): keep the MIDI Editor open on Bring to Front (#254) 2026-10-12 11:58:36 +02:00
Marco Arnulfo 67158020f0 fix(control): apply the engine.commands filter to UI commands (#255) 2026-10-12 11:56:06 +02:00
Marco Arnulfo 30372886cf fix(track): vca: null clears a VCA assignment, as documented (#237) 2026-10-12 11:03:28 +02:00
Marco Arnulfo 217bea462d fix(dsp): a huge finite sample rate is no pitch, not an overflow (#241) 2026-10-12 10:59:23 +02:00