Skip to main content

songbird-host-ffi

FFI bridge for hosting C++ (JUCE) VST3/AU plugins from the Rust audio engine. Rust owns the audio thread. Third-party plugins are loaded and processed via JUCE through a thin C ABI layer. The JucePlugin wrapper implements the Rust Plugin trait, so external plugins are interchangeable with stock Rust plugins in the audio graph.

Architecture

Rust audio thread
  → JucePlugin.process()
    → extern "C" ffi_plugin_process(handle, input_ptrs, output_ptrs, num_samples, num_channels)
      → C++ JUCE wrapper
        → juce::AudioProcessor::processBlock()

FFI Declarations (16 functions)

FunctionPurpose
ffi_plugin_loadLoad a plugin by path/format (VST3, AU, etc.)
ffi_plugin_unloadUnload and free a plugin instance
ffi_plugin_initializeSet sample rate and block size
ffi_plugin_resetReset plugin state
ffi_plugin_processProcess audio buffer (non-interleaved float pointers)
ffi_plugin_get_param_countNumber of parameters
ffi_plugin_get_param_nameParameter name by index
ffi_plugin_get_param_valueCurrent parameter value (0.0–1.0)
ffi_plugin_set_param_valueSet parameter value
ffi_plugin_get_param_defaultDefault parameter value
ffi_plugin_get_latencyPlugin latency in samples
ffi_plugin_get_tailTail length in seconds
ffi_plugin_save_stateSave binary plugin state
ffi_plugin_load_stateRestore binary plugin state
ffi_plugin_accepts_midiWhether plugin processes MIDI
ffi_plugin_send_midiSend MIDI events to plugin

JucePlugin Wrapper

pub struct JucePlugin {
    handle: *mut c_void,           // Opaque C++ plugin handle
    name: String,
    type_name: String,
    param_cache: Vec<PluginParam>, // Cached parameter metadata
    sample_rate: f64,
}

impl Plugin for JucePlugin {
    fn process(&mut self, buffer: &mut AudioBuffer, midi_events: &[MidiEvent]) {
        // Calls extern "C" ffi_plugin_process with raw buffer pointers
        // Buffer layout: non-interleaved, matching JUCE's AudioBuffer<float>
    }
    // ... all other Plugin trait methods delegate to FFI calls
}

Buffer Layout

Both sides use non-interleaved (planar) float buffers:
Channel 0: [sample_0, sample_1, ..., sample_N]
Channel 1: [sample_0, sample_1, ..., sample_N]
This matches JUCE’s AudioBuffer<float>::getWritePointer() layout, so no format conversion is needed at the boundary.

Out-of-Process Hosting (Future)

The Plugin trait does not assume shared memory. The FFI bridge can be extended to host plugins in a separate process (like Ableton/Bitwig do) by:
  1. Replacing ffi_plugin_process with IPC (shared memory ring buffer or pipe)
  2. Running the JUCE host in a child process
  3. Crashing plugin = child process dies, not the DAW
The JucePlugin wrapper’s interface stays the same — only the transport layer changes.

Status

  • Rust side: Complete (16 extern "C" declarations, JucePlugin wrapper, 6 unit tests)
  • C++ side: Stubs only (needs a thin JUCE wrapper implementing the 16 functions)

Testing

cargo test -p songbird-host-ffi  # Tests FFI struct layout and parameter caching