Commit Graph
15 Commits
Author SHA1 Message Date
aiongg e884d6eb37 Integration: VST3 host: keep room for note releases after a reset 2026-10-24 09:16:19 +00:00
aiongg 419e7726bb Merge PR #605: Release held VST3 notes on reset instead of discarding them 2026-10-24 06:53:57 +00:00
Marco Arnulfo 783ce8f44b VST3: show the plugin's own text for identity-mapped parameters (fixes #599) 2026-10-16 15:20:11 +02:00
NetterSilverandClaude Haiku 5.5 ea0e4719fc Release held VST3 notes on reset instead of discarding them
Fixes #441

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 04:00:26 +02:00
Brandon ThomasandClaude Opus 5.5 baea184bd1 Keep one VST3 controller-sync path after merging #306's latest
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>
2026-10-13 06:26:54 -04:00
Brandon Thomas 7a6df4cbbd Merge #306's latest into the landing branch 2026-10-13 06:25:56 -04:00
grovebit 776a7af06c Merge origin/main into plugin-safety
#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.
2026-10-13 10:58:07 +01:00
Brandon Thomas 056bc288c4 Merge main into #306 2026-10-13 05:31:14 -04:00
Brandon Thomas e32c16b82e Merge main into #367 2026-10-13 05:11:18 -04:00
Marco Arnulfo 40c5e9b42a VST3: hand host-set parameter values to the controller before saving state (fixes #257)
Plugin::set_param only queues the value for the processor, so a parameter that lives in the controller alone (no processor state) was saved with the controller's old value and came back wrong after load_state. set_param may run on the audio thread, so it now just marks the value; save_state, which already calls into the controller, pushes the marked values with setParamNormalized before taking the snapshot.
2026-10-13 07:06:22 +02: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
grovebit acd9e7d54e Play MIDI controllers through instruments
Imported MIDI keeps its controller events: sustain pedal and other CCs,
pitch bend, channel and poly pressure, program changes. Playback sent
instruments only note-ons and note-offs, so a piano part lost its pedal
and bends went flat.

- Plugin::midi takes any other MIDI 1.0 channel message at a sample
  offset. The mix engine sends each clip's controllers at their sample,
  after the Real-Time Properties delay (and transpose, for poly
  pressure), on channel 1 like the notes.
- After a seek, a stop or a loop wrap, an instrument first gets Reset All
  Controllers (CC 121), then the last value each controller had on its
  track before the playhead. A pedal held before the start point is
  still down.
- CLAP: raw MIDI events when the note port speaks the MIDI dialect.
- VST3: CCs, aftertouch and pitch bend become parameter changes at their
  offset, through the plugin's IMidiMapping. It is asked once in prepare,
  on the main thread, and there is one point per parameter per block.
  Poly pressure is a PolyPressureEvent. Program changes have no VST3
  mapping and are dropped.
- AU: MusicDeviceMIDIEvent for instruments and music effects.
- The built-in synth follows the sustain pedal, pitch bend (2 semitones
  each way), Reset All Controllers and All Notes Off.
- The sampler ignores the pedal, pitch bend and Reset All Controllers
  for now.

The AU check of MIDI volume lives inside the existing DLS test. Creating
two DLSMusicDevice instances at once on two threads crashes inside the
unit, with or without this change. Two instances created one after
another render in parallel fine.
2026-10-12 02:57:22 +01:00
grovebit 33ad8264c8 Give plugins the session's tempo and transport
Hosted plugins never learned the session's tempo or where the song was:
- CLAP got a null transport;
- VST3 got a fixed 120 BPM 4/4 context that never played and counted
  processed samples as the song position;
- Audio Units had no host callbacks.
Tempo-synced delays, LFOs and arpeggiators ran at 120 BPM and ignored
the transport.

Before each block the mix engine now hands every insert, instrument and
master-fader plugin a soundcraft_dsp::Transport (Plugin::set_transport):
play and record state, tempo and meter at the block start, and the
timeline position in samples, seconds and quarter notes, with the
current bar. Built-in plugins ignore it.
- CLAP: a clap_event_transport (fixed-point beats and seconds, bar start
  and number, time signature, play and record flags).
- VST3: ProcessContext tempo, time signature, project time in samples and
  quarter notes, bar position, kPlaying and kRecording. Continuous time
  stays the processed-sample count.
- AU: kAudioUnitProperty_HostCallbacks (beat and tempo, musical time
  location, transport state 1 and 2), read from atomics the instance
  owns and frees after disposing the unit.

Loop (cycle) ranges aren't reported yet: playback keeps the loop range
to itself.
2026-10-12 02:56:55 +01:00
Brandon ThomasandClaude Opus 5.5 cdefc57c91 Video track, Audio Unit hosting, plugin state and editors, Renderer and UI Customization
Video (new standalone crate soundcraft-video):
- Pure-Rust H.264 (Baseline/Main/High), ProRes (all flavours) and Motion JPEG decoding with
  ISO-BMFF demux, adapted from FilmCraft's crates; fuzzed with mutated/truncated movies
- TrackKind::Video with ClipContent::Video referencing Session.videos; file.import_video adds
  the picture track and the movie's audio; relinking on open; video.offset/online/list
- Video and Video Universe windows, thumbnails on the timeline, background decoding

Third-party plugins:
- Plugin state (CLAP state ext, VST3 component + controller state) saved in sessions
- Hosted instances created, state-loaded and dropped on the UI thread, never the audio thread
- Plugin editors: floating CLAP GUIs and VST3 views on macOS (verified with Melodyne)
- New isolated FFI crate soundcraft-au-host: Audio Units on macOS (effects and instruments,
  parameters, latency, MIDI, ClassInfo state), wired into the mixer and plugin browser

UI: Window > Renderer (speaker layout with live levels and object positions), Window > UI
Customization (interface scale, reduce motion), Window > Configurations > Auto-Update.

Fix: saturating sample-index math in clip rendering (hostile stretch values overflowed).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 01:41:05 -04:00
Brandon ThomasandClaude Opus 5.5 cc86842abe Surround mixing, VST3 hosting, and hardening against hostile inputs
Surround:
- main and bus formats up to 9.1.6 and Ambisonics; pairwise constant-power
  surround panner (divergence, centre, LFE, height) per track and send;
  ITU fold-down; multichannel bounce with correct WAV channel masks;
  multichannel device output; surround panner and multichannel meters in the
  Mix window; setup.main_format, mix.surround_pan, mix.send_surround_pan

VST3:
- soundcraft-vst3-host: isolated unsafe crate (vst3 bindings, MIT), scans
  standard folders, hosts components/processors/controllers, params,
  latency, notes; VST3 menu; engine.vst3_plugins; e2e tests with a fixture

Hardening:
- positions from parameters clamped; offline renders capped and bounded to
  track material; loop/transport limits; hostile-parameter property test

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 00:39:59 -04:00