Skip to content

Latest commit

 

History

History
848 lines (679 loc) · 39.8 KB

File metadata and controls

848 lines (679 loc) · 39.8 KB

Beamer Architecture

This document describes the high-level architecture of Beamer, a Rust framework for building audio plugins with WebView-based GUIs.

For detailed API documentation, see docs/REFERENCE.md. For example coverage and testing roadmap, see docs/EXAMPLE_COVERAGE.md.


Overview

What Is Beamer?

A Rust framework for building audio plugins (Audio Unit and VST3) with WebView-based GUIs. Named after the beams that connect notes in sheet music, Beamer links your DSP logic and WebView interface together. Inspired by Tauri's architecture but focused specifically on the audio plugin context.

Why?

  • Rust for audio: Memory safety, performance, no GC pauses
  • WebView for UI: Leverage modern web technologies (React, Svelte, Vue, etc.)
  • Multi-format: Audio Unit and VST3 support from a single codebase
  • Lightweight: Use OS-native WebViews, no bundled browser engine
  • Cross-platform: macOS today (Intel and Apple Silicon); Windows planned (the architecture is platform-agnostic, but the WebView backend is macOS-only so far)

Goals

  • Audio Unit support (macOS, AUv2 and AUv3) ✅
  • VST3 plugin support (VST3 3.8, MIT licensed) ✅
  • WebView GUI using OS-native engines
  • Cross-platform: macOS and Windows
  • Tauri-inspired IPC (invoke/emit pattern)
  • Optional parameter binding helpers
  • Developer-friendly: hot reload in dev mode
  • Framework-agnostic frontend (React, Svelte, Vue, vanilla JS)
  • MIDI event processing (instruments and MIDI effects)

Architecture Diagrams

Audio Unit Architecture (Hybrid ObjC/Rust)

The AU wrapper uses a hybrid architecture: native Objective-C for Apple runtime compatibility, with all DSP in Rust via C-ABI bridge. Both AUv2 (.component) and AUv3 (.appex) formats are supported through the same C-ABI bridge layer.

┌──────────────────────────────────────────────────────────────────┐
│                     DAW Host (macOS)                             │
├──────────────────────────────────────────────────────────────────┤
│   AUv2 API (.component)         │     AUv3 API (.appex)          │
│   AudioComponentPlugInInterface │     AUAudioUnit subclass       │
├─────────────────────────────────┼────────────────────────────────┤
│                                 │                                │
│   Audio Thread                  │         Main Thread            │
│   ┌──────────────┐              │         ┌──────────────────┐   │
│   │              │              │         │  Native ObjC     │   │
│   │ Render Call  │◄─────────────┼────────►│  Wrapper Layer   │   │
│   │  (AUv2/v3)   │   C-ABI      │         │                  │   │
│   │              │   calls      │         └────────┬─────────┘   │
│   └──────┬───────┘              │                  │             │
│          │                      │                  │ NSView      │
│          │ beamer_au_render()   │         ┌────────▼─────────┐   │
│   ┌──────▼───────┐              │         │                  │   │
│   │ bridge.rs    │              │         │  WebView Window  │   │
│   │ RenderBlock  │              │         │   (WKWebView)    │   │
│   │ AuProcessor  │              │         │                  │   │
│   └──────────────┘              │         └──────────────────┘   │
│                                 │                                │
└─────────────────────────────────┴────────────────────────────────┘

Why Hybrid? Native Objective-C integrates naturally with Apple's frameworks, provides better debuggability with Apple's tools and avoids the complexity of Rust FFI bindings for AUAudioUnit subclassing. The hybrid approach guarantees Apple compatibility while keeping all audio processing in Rust.

VST3 Architecture

The VST3 wrapper uses COM (Component Object Model) interfaces implemented directly in Rust. A single Vst3Processor<P> class implements all required interfaces, with the processor handling audio on the audio thread and the edit controller managing parameters on the UI thread.

┌─────────────────────────────────────────────────────────────────┐
│                         DAW Host                                │
├─────────────────────────────────────────────────────────────────┤
│                      VST3 Interface                             │
│              (IComponent, IAudioProcessor, IEditController)     │
├────────────────────────────────┬────────────────────────────────┤
│                                │                                │
│    Audio Thread                │         UI Thread              │
│    ┌──────────────┐            │         ┌──────────────────┐   │
│    │              │            │         │                  │   │
│    │  Processor   │◄───────────┼────────►│  EditController  │   │
│    │  (DSP code)  │  lock-free │         │                  │   │
│    │              │  queue     │         └────────┬─────────┘   │
│    └──────────────┘            │                  │             │
│                                │                  │ IPlugView   │
│                                │         ┌────────▼─────────┐   │
│                                │         │                  │   │
│                                │         │  WebView Window  │   │
│                                │         │  (WKWebView /    │   │
│                                │         │   WebView2)      │   │
│                                │         │                  │   │
│                                │         └──────────────────┘   │
└────────────────────────────────┴────────────────────────────────┘

Why COM in Rust? The VST3 SDK is C++ based, but Rust can implement COM interfaces directly using vtable pointers. This avoids C++ interop complexity while maintaining full compatibility with VST3 hosts.

Unified Core

Both formats share the same core traits and processing logic:

┌─────────────────────────────────────────────────────────────────┐
│                       beamer-core                               │
│  • Descriptor trait (unprepared state)                          │
│  • Processor trait (prepared state)                             │
│  • Buffer, AuxiliaryBuffers, MidiBuffer                         │
│  • Parameters trait, ParameterStore                             │
│  • ProcessContext, Transport                                    │
└──────────────────────┬──────────────────┬───────────────────────┘
                       │                  │
         ┌─────────────▼──────┐  ┌────────▼─────────────┐
         │    beamer-au       │  │   beamer-vst3        │
         │                    │  │                      │
         │ • AuProcessor<P>   │  │ • Vst3Processor<P>   │
         │ • C-ABI bridge     │  │ • COM interfaces     │
         │ • Native ObjC wrap │  │ • VST3 MIDI          │
         │ • UMP MIDI         │  │ • Factory            │
         └────────────────────┘  └──────────────────────┘

Threading Model

Thread Responsibilities Constraints
Audio Thread DSP processing, buffer handling Real-time safe: no allocations, no locks, no syscalls
UI Thread Parameter changes, WebView, IPC Can allocate, can block (briefly)
Host Thread Plugin lifecycle, state save/load Varies by host

Crate Structure

beamer/
├── crates/
│   ├── beamer/              # Main crate (re-exports)
│   ├── beamer-core/         # Plugin traits, MIDI types, buffers
│   ├── beamer-macros/       # Proc macros (#[beamer::export], #[derive(Parameters)], #[derive(EnumParameter)], #[derive(HasParameters)])
│   ├── beamer-utils/        # Shared utilities (zero deps)
│   ├── beamer-au/           # Audio Unit wrapper implementation (macOS)
│   ├── beamer-vst3/         # VST3 wrapper implementation
│   └── beamer-webview/      # WebView per platform
├── examples/
│   ├── gain/                # Audio effect example
│   ├── compressor/          # Dynamics compressor
│   ├── equalizer/           # 3-band parametric EQ
│   ├── delay/               # Delay effect with tempo sync
│   ├── synthesizer/         # Polyphonic synthesizer with MIDI CC emulation
│   ├── drums/               # Drum synthesizer with multi-output buses
│   ├── midi-transform/      # MIDI effect example
│   └── webview-demo/        # WebView GUI with React
└── xtask/                   # Build tooling (bundle, install)

Crate Responsibilities

Crate Purpose
beamer Facade crate, re-exports public API via prelude
beamer-core Platform-agnostic traits (Descriptor, Processor, HasParameters), buffer types, MIDI types, Config
beamer-macros Derive macros: #[derive(Parameters)], #[derive(EnumParameter)], #[derive(HasParameters)]. Attribute macro: #[beamer::export] (reads Config.toml/Presets.toml)
beamer-utils Internal utilities shared between crates (zero external deps)
beamer-au Audio Unit (AUv2 and AUv3) integration via hybrid ObjC/Rust architecture, C-ABI bridge (macOS only)
beamer-vst3 VST3 SDK integration, COM interfaces, host communication
beamer-webview Platform-native WebView embedding

Plugin Lifecycle

Beamer uses type-safe initialization via prepare() that eliminates placeholder values:

┌─────────────────────────────────────────────────────────────────┐
│                     Descriptor (Unprepared)                     │
│  • Created via Default::default()                               │
│  • Holds parameters and bus configuration                       │
│  • No sample rate or audio state                                │
└─────────────────────────────────┬───────────────────────────────┘
                                  │
                                  │ self.prepare(setup)
                                  │ [setupProcessing]
                                  ▼
┌─────────────────────────────────────────────────────────────────┐
│                   Processor (Prepared)                          │
│  • Created with real sample rate and buffer size                │
│  • Allocates DSP state (delay buffers, filter coefficients)     │
│  • Ready for process() calls                                    │
└─────────────────────────────────┬───────────────────────────────┘
                                  │
                                  │ unprepare()
                                  │ [sample rate change]
                                  ▼
┌─────────────────────────────────────────────────────────────────┐
│                     Descriptor (Unprepared)                     │
│  • Parameters preserved                                         │
│  • DSP state discarded                                          │
│  • Ready for prepare() with new setup                           │
└─────────────────────────────────────────────────────────────────┘

Why This Design?

Audio plugins need sample rate for buffer allocation, filter coefficients and envelope timing, but the sample rate isn't known until the host calls setupProcessing(). The prepare() design ensures DSP state is only created with valid configuration.

Design Rationale

Beamer's design follows the Rust principle of making invalid states unrepresentable. This is the typestate pattern - different types represent different states and the compiler enforces valid transitions.

The Processor type is always fully initialized, so process() code is clean:

impl Processor for DelayProcessor {
    fn process(&mut self, buffer: &mut Buffer, ...) {
        // self.sample_rate is guaranteed valid
        // self.buffer is guaranteed allocated
        // No Option<T>, no .expect(), no placeholder checks
    }
}

Three-Struct Pattern

Beamer plugins use three structs for clear separation of concerns:

  1. *Parameters - Pure parameter definitions with #[derive(Parameters)]
  2. *Descriptor - Plugin descriptor that holds parameters and implements Descriptor
  3. *Processor - Runtime processor created by prepare(), implements Processor
// 1. Parameters - pure data
#[derive(Parameters)]
pub struct GainParameters {
    #[parameter(id = "gain", name = "Gain", default = 0.0, range = -60.0..=12.0, kind = "db")]
    pub gain: FloatParameter,
}

Parameter attributes:

Attribute Description
id = "..." String ID (required), hashed to u32 via FNV-1a
name = "..." Display name
default = <value> Default value (float, int or bool)
range = start..=end Value range (inclusive)
kind = "..." Unit type: db, db_log, db_log_offset, hz, ms, seconds, percent, pan, ratio, linear, semitones
short_name = "..." Short name for constrained UIs
smoothing = "..." Parameter smoothing: "exp:5.0" or "linear:50.0" (time constant in ms)
step = <value> Discrete step size (FloatParameter only)
group = "..." Flat visual grouping in DAW
bypass Mark as bypass parameter (BoolParameter only)
// 2. Descriptor - holds parameters, describes plugin to host
#[derive(Default, HasParameters)]
pub struct GainDescriptor {
    #[parameters]
    parameters: GainParameters,
}

impl Descriptor for GainDescriptor {
    // No setup needed for simple effects; use SampleRate for delays, MaxBufferSize for FFT
    type Setup = ();
    type Processor = GainProcessor;

    fn prepare(self, _: ()) -> GainProcessor {
        GainProcessor { parameters: self.parameters }
    }
}

// 3. Processor - prepared state, ready for audio
#[derive(HasParameters)]
pub struct GainProcessor {
    #[parameters]
    parameters: GainParameters,
}

impl Processor for GainProcessor {
    type Descriptor = GainDescriptor;
    fn process(&mut self, buffer: &mut Buffer, ...) { /* ... */ }
}

Plugins with DSP state add fields to the Processor:

#[derive(HasParameters)]
struct DelayProcessor {
    #[parameters]
    parameters: DelayParameters,
    sample_rate: f64,    // Always valid
    buffer: Vec<f64>,    // Always allocated
}

Setup Types

Type Use Case Value
() Stateless plugins (gain, pan) -
SampleRate Most plugins (delay, filter, envelope) f64 via .hz()
MaxBufferSize FFT, lookahead usize
MainOutputChannels Per-channel state u32
(A, B, ...) Combine multiple types Tuples up to 5 elements

For IDE autocomplete, use beamer::setup::* to import all available types.

Trait Responsibilities

Trait State Responsibilities
HasParameters Both Parameter access (parameters(), parameters_mut(), set_parameters()) - use #[derive(HasParameters)]
Descriptor Unprepared Bus configuration, MIDI mapping, prepare() transformation
Processor Prepared DSP processing, state persistence, MIDI processing, unprepare() (has default impl)

Parameter Ownership

Parameters are owned by both Descriptor and Processor, moving between them during state transitions:

Descriptor                       Processor
┌─────────────────────┐        ┌─────────────────────┐
│ parameters ─────────┼──────► │ parameters          │
└─────────────────────┘        └─────────────────────┘
       prepare() moves              unprepare() moves
       parameters →                 ← parameters back

This is the type-state pattern - a Rust idiom for encoding state machines at the type level. The same pattern appears in std::fs::File and session types.

Why ownership instead of shared references?

  1. Zero overhead: Direct field access: self.parameters.gain.get()
  2. No synchronization: Owned data needs no Arc, Mutex, or atomics for internal access
  3. Clear lifecycle: Parameters exist exactly where they're used
  4. Smoother mutation: Smoothers advance state each sample; ownership makes this natural

The HasParameters trait:

Both Descriptor and Processor implement HasParameters because the host needs parameter access in both states:

  • Before prepare(): Host queries parameter info, user adjusts values
  • After prepare(): Host automates parameters during playback

Use #[derive(HasParameters)] with a #[parameters] field annotation on both Descriptor and Processor:

// Descriptor with HasParameters
#[derive(Default, HasParameters)]
pub struct GainDescriptor {
    #[parameters]
    parameters: GainParameters,
}

// Processor with HasParameters
#[derive(HasParameters)]
pub struct GainProcessor {
    #[parameters]
    parameters: GainParameters,
    // Additional DSP state...
}

The derive macro generates the parameters(), parameters_mut() and set_parameters() methods automatically.


Plugin Configuration and Export

Beamer uses TOML-based configuration with compile-time code generation to eliminate boilerplate.

Configuration Architecture

Plugin metadata is declared in Config.toml (required) and optionally Presets.toml in the plugin crate root. The #[beamer::export] attribute macro reads these files at compile time and generates:

  • The CONFIG static with all metadata
  • Factory presets implementation (if Presets.toml exists)
  • Format entry points via export_plugin! macro
Plugin Crate Root
├── Cargo.toml
├── Config.toml              # Required: plugin metadata
├── Presets.toml             # Optional: factory presets
└── src/
    └── lib.rs
        ├── Parameters struct
        ├── Descriptor struct with #[beamer::export]
        └── Processor struct

Example: Plugin with TOML Configuration

Config.toml (place in crate root next to Cargo.toml):

name = "My Gain Plugin"
category = "effect"
subcategories = ["dynamics"]
manufacturer_code = "Demo"
plugin_code = "gain"
vendor = "My Company"
url = "https://example.com"
email = "support@example.com"

Presets.toml (optional):

[[preset]]
name = "Default"
gain = 0.0

[[preset]]
name = "+6dB"
gain = 6.0

src/lib.rs:

use beamer::prelude::*;

#[derive(Parameters)]
pub struct GainParameters {
    #[parameter(id = "gain", name = "Gain", default = 0.0, range = -60.0..=12.0, kind = "db")]
    pub gain: FloatParameter,
}

#[beamer::export]  // Reads Config.toml and generates everything
#[derive(Default, HasParameters)]
pub struct GainDescriptor {
    #[parameters]
    parameters: GainParameters,
}

impl Descriptor for GainDescriptor {
    type Setup = ();
    type Processor = GainProcessor;
    fn prepare(self, _: ()) -> GainProcessor {
        GainProcessor { parameters: self.parameters }
    }
}

#[derive(HasParameters)]
pub struct GainProcessor {
    #[parameters]
    parameters: GainParameters,
}

impl Processor for GainProcessor {
    type Descriptor = GainDescriptor;
    fn process(&mut self, buffer: &mut Buffer, _aux: &mut AuxiliaryBuffers, _context: &ProcessContext) {
        // ... DSP code
    }
}

The #[beamer::export] attribute reads Config.toml and generates the required CONFIG static and plugin export code.

Configuration Fields

Config.toml:

  • name - Display name in DAW (required)
  • category - Plugin type: "effect", "instrument", "midi_effect", or "generator" (required)
  • subcategories - Array of subcategory strings (optional, e.g., ["dynamics", "eq"])
  • manufacturer_code - 4-character manufacturer code for AU (required, e.g., "Demo")
  • plugin_code - 4-character plugin code for AU (required, e.g., "gain")
  • vendor - Company/developer name (optional)
  • url - Plugin URL (optional)
  • email - Support email (optional)
  • vst3_id - Explicit VST3 UUID override (optional, format: "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX")
  • has_gui - GUI enabled flag (optional, default: false)
  • gui_size - Initial GUI size as [width, height] in pixels (required when has_gui is true)

Field Ordering Convention (for readability):

# 1. Core identity
name = "..."
category = "..."
subcategories = [...]  # Optional, grouped with category

# 2. Technical identifiers
manufacturer_code = "..."
plugin_code = "..."

# 3. Metadata
vendor = "..."
url = "..."
email = "..."

Presets.toml:

[[preset]]
name = "Preset Name"
parameter_id = value
another_parameter = value

Use parameter IDs (not Rust field names) and plain numeric values.

Why TOML Configuration?

  1. Cleaner code - No manual CONFIG statics or export macros
  2. Easier to modify - Change metadata without touching Rust code
  3. Consistent format - Same structure across all plugins

Generated Code

The #[beamer::export] macro generates:

  1. CONFIG static: pub static CONFIG: Config = Config::new(...).with_vendor(...).with_version(...)
  2. Presets implementation (if Presets.toml exists): Implements FactoryPresets trait
  3. Plugin exports: Calls export_plugin!(CONFIG, DescriptorStruct) or export_plugin!(CONFIG, DescriptorStruct, PresetsStruct)

The macro uses include_str!() to track file dependencies, so Cargo automatically rebuilds when Config.toml or Presets.toml changes.

VST3 ID Generation

VST3 requires a unique 128-bit identifier. By default, Beamer derives it from your manufacturer_code and plugin_code using FNV-1a hashing. This ensures consistent IDs across builds.

To use an explicit UUID (e.g., for backward compatibility):

vst3_id = "12345678-9ABC-DEF0-ABCD-EF1234567890"

Building Multi-Format Plugins

Use xtask to build formats:

# AUv2 only (macOS, native architecture)
cargo xtask bundle my-plugin --auv2 --release

# AUv3 only (macOS, native architecture)
cargo xtask bundle my-plugin --auv3 --release

# VST3 only (native architecture)
cargo xtask bundle my-plugin --vst3 --release

# All formats (macOS)
cargo xtask bundle my-plugin --auv2 --auv3 --vst3 --release

# Install to system plugin directories
cargo xtask bundle my-plugin --auv2 --auv3 --vst3 --release --install

# Universal binary for distribution (x86_64 + arm64)
cargo xtask bundle my-plugin --auv2 --auv3 --vst3 --arch universal --release

Architecture options: --arch native (default), --arch universal, --arch arm64, --arch x86_64

Building WebView Plugins

Plugins with has_gui = true in Config.toml need their web assets built before bundling. The #[beamer::export] macro scans webview/dist/ at compile time and embeds all files via include_bytes!().

# Build web assets first (from the example's webview/ directory)
cd examples/webview-demo/webview && bun install && bun run build && cd -

# Then bundle as usual
cargo xtask bundle webview-demo --auv3 --release --install

The web assets (HTML, CSS, JS) are served at runtime through a custom URL scheme (beamer://) rather than a local HTTP server.

beamer-webview uses platform APIs directly (objc2 + objc2-web-kit for WKWebView) rather than a wrapper like wry. Audio plugin hosts provide their own parent views (NSView, HWND) that the WebView must attach to, and the AU wrapper needs a C-ABI bridge for its generated ObjC code. A high-level wrapper would add indirection without solving either problem.


Format-Specific Implementation Details

While both formats share the same beamer-core abstractions, they differ significantly in their platform APIs.

Audio Unit Implementation

Architecture: Hybrid Objective-C/Rust

  • AUv2: AudioComponentPlugInInterface with selector-based dispatch
  • AUv3: BeamerAuWrapper native ObjC class (subclass of AUAudioUnit)
  • Shared C-ABI bridge layer (BeamerAuBridge.hbridge.rs) with 40+ functions
  • Uses type erasure (AuPluginInstance trait) for generic plugin support
  • Render blocks call into Rust via beamer_au_render()
  • Full feature parity with VST3 wrapper

Key Files:

Objective-C Layer:

Rust Layer:

MIDI: UMP MIDI 1.0/2.0 → beamer-core::MidiEvent

  • Universal MIDI Packet format (32-bit packets)
  • Supports Note On/Off, CC, Pitch Bend, Channel Pressure, SysEx
  • 1024 event buffer (matches VST3)
  • MidiCcState tracking for CC/pitch bend queries
  • SysExOutputPool for real-time safe SysEx output

Parameters: Push model via KVO (Key-Value Observing)

  • AUParameterTree built from ParameterStore
  • implementorValueObserver - Host → plugin changes
  • implementorValueProvider - Plugin → host reads
  • Automation via AURenderEventParameter/ParameterRamp (sample-accurate via sub-block processing)

State: NSDictionary with NSData

  • Full processor state persistence (save_state/load_state)
  • Deferred state loading via pending_state (matches VST3)
  • Compatible with VST3 format

Real-time Safety:

  • Pre-allocated f64↔f32 conversion buffers (main + aux buses)
  • Pre-allocated MIDI/SysEx buffers
  • No heap allocation in render path

VST3 Implementation

Architecture: COM-based (Component Object Model)

  • Single Vst3Processor<P> class implements 15+ COM interfaces
  • Uses combined component pattern (processor + controller in one class)
  • Direct function pointer vtables for interface calls

Key Files:

MIDI: VST3 Event union → beamer-core::MidiEvent

  • 16+ event types (NoteOn, NoteOff, MIDI CC, PolyPressure, etc.)
  • Supports VST3-specific events (NoteExpression, Chord, Scale)
  • Legacy MIDI CC output for host compatibility

Parameters: Pull model via COM methods

  • getParameterInfo() - Host queries parameter metadata
  • setParamNormalized() - Host sets parameter value
  • getParamNormalized() - Host reads parameter value

State: Binary blob via IBStream

Comparison Table

Feature Audio Unit VST3
Platform macOS only macOS; Windows planned (UI backend macOS-only so far)
API Style Hybrid ObjC/Rust via C-ABI COM (C++ style)
Language ObjC + Rust + cc crate Rust + vst3-sys
Code Size Multiple files (ObjC + Rust) Single file
MIDI Format UMP MIDI 1.0/2.0 VST3 Event union
MIDI Buffer 1024 events 1024 events
MidiCcState
MIDI Output ✓ (instruments/MIDI effects only)
SysEx Output ✓ (pool) ✓ (pool)
Parameter Sync Push (KVO callbacks) Pull (COM methods)
Param Automation Sample-accurate (sub-block) Buffer-quantized + smoothing
Audio Buffers AudioBufferList float** arrays
f64 Conversion Pre-allocated Pre-allocated
State Format NSDictionary Binary blob
Processor State
Bundle Type .component (AUv2) / .appex (AUv3) .vst3
Registration ObjC factory + module init GetPluginFactory()
Feature Parity ✓ Full parity Reference

Code Reuse Statistics

Shared (beamer-core): ~100%

  • All DSP processing logic
  • Parameter management
  • MIDI event representation
  • Buffer abstractions
  • Transport and context

Format-specific: ~0% overlap

  • Different C APIs (COM vs ObjC)
  • Different MIDI formats
  • Different parameter models
  • Different state serialization

The format wrappers are thin translation layers that adapt the platform API to beamer-core abstractions.


Operational Guarantees

This section documents the invariants that Beamer enforces. These are API contracts that plugin authors can rely on.

Real-Time Safety

Guarantee: No heap allocations occur on the audio thread during process().

Component Mechanism
Buffer<S> Stack-allocated [Option<&[S]>; MAX_CHANNELS] arrays
AuxiliaryBuffers<S> Stack-allocated nested fixed arrays
MidiBuffer Pre-allocated fixed capacity (1024 events default)
SysExOutputPool Pre-allocated slots (16 × 512 bytes default)
ProcessBufferStorage<S> Pre-allocated Vecs with reserved capacity; clear() + push() never allocate

Enforcement:

  • setupProcessing() pre-allocates all buffers based on plugin configuration
  • process() uses only stack storage and pre-allocated pools
  • Bounds checking via .take(max) prevents allocation even if host misbehaves

Deterministic Bus Limits

Guarantee: Channel and bus counts are bounded at compile time.

Constant Value Purpose
MAX_CHANNELS 32 Supports up to 22.2 surround and Dolby Atmos 9.1.6
MAX_BUSES 16 Main + sidechain + 14 aux buses
MAX_AUX_BUSES 15 Auxiliary buses (total minus main)

Enforcement:

  • validate_bus_limits() checks plugin config against constants at initialization
  • validate_speaker_arrangement() rejects invalid host arrangements in setBusArrangements()
  • setupProcessing() returns kResultFalse and logs error if limits exceeded

MIDI Data Fidelity

Guarantee: MIDI data passes through without loss or corruption under normal conditions.

Aspect Mechanism
Tuning preservation NoteOn.tuning and NoteOff.tuning fields (f32 cents, ±120.0)
Length preservation NoteOn.length field (i32 samples, 0 = unknown)
Sample accuracy MidiEvent.sample_offset preserved through VST3 round-trip
Note ID tracking NoteId maintained for proper note-on/note-off pairing

Overflow Handling:

  • MidiBuffer::has_overflowed() flag set when capacity exceeded
  • SysExOutputPool::has_overflowed() flag set when pool exhausted
  • Automatic log::warn!() on first overflow per block
  • Optional sysex-heap-fallback feature for guaranteed SysEx delivery (breaks real-time guarantee)

Buffer Management Contracts

ProcessBufferStorage (defined in beamer-core, with format-specific extensions):

pub struct ProcessBufferStorage<S: Sample> {
    pub main_inputs: Vec<*const S>,
    pub main_outputs: Vec<*mut S>,
    pub aux_inputs: Vec<Vec<*const S>>,
    pub aux_outputs: Vec<Vec<*mut S>>,
    pub internal_output_buffers: Option<Vec<Vec<S>>>,
    pub max_frames: usize,
}
  • Pre-allocated in setupProcessing() based on plugin's actual bus configuration (not worst-case)
  • Config-based allocation: stereo plugin uses 32 bytes, not 4KB worst-case
  • Lazy aux allocation: no heap allocation for plugins without aux buses
  • Internal output buffers allocated only for instruments (hosts may provide null pointers)
  • clear() resets length to 0 without deallocating
  • push() into reserved capacity never allocates

Plugin-Declared Capacity (configurable via Config.toml):

# Config.toml
sysex_slots = 64 # Default: 16
sysex_buffer_size = 4096 # Default: 512 bytes

Allocation Lifecycle

The buffer allocation flow ensures all memory is reserved before audio processing begins:

Plugin Load (creates Descriptor in Unprepared state)
    │
    ▼
┌─────────────────────────────────────────────────────────────┐
│ validate_bus_limits(descriptor)                             │
│   • Check declared buses ≤ MAX_BUSES                        │
│   • Check declared channels per bus ≤ MAX_CHANNELS          │
│   • Return error if exceeded (plugin fails to load)         │
└─────────────────────────────────────────────────────────────┘
    │
    ▼
┌─────────────────────────────────────────────────────────────┐
│ setBusArrangements(inputs, outputs)  [VST3 host call]       │
│   • validate_speaker_arrangement() for each bus             │
│   • Reject if any arrangement exceeds MAX_CHANNELS          │
│   • Return kResultFalse on rejection (host tries another)   │
└─────────────────────────────────────────────────────────────┘
    │
    ▼
┌─────────────────────────────────────────────────────────────┐
│ setupProcessing(sample_rate, max_block_size)                │
│   • descriptor.prepare(setup) → Processor                   │
│     - Descriptor consumed, Processor created                │
│     - DSP state allocated with real sample rate             │
│   • ProcessBufferStorage::allocate()                        │
│     - input_ptrs.reserve(main_channels)                     │
│     - output_ptrs.reserve(main_channels)                    │
│     - aux_input_ptrs[i].reserve(aux_channels[i])            │
│     - aux_output_ptrs[i].reserve(aux_channels[i])           │
│   • All Vecs now have capacity, length = 0                  │
│   • Return kResultFalse + log if allocation fails           │
└─────────────────────────────────────────────────────────────┘
    │
    ▼
┌─────────────────────────────────────────────────────────────┐
│ process() [audio thread, called repeatedly]                 │
│   • storage.clear() - sets len=0, no deallocation           │
│   • storage.push(ptr) - into reserved capacity, no alloc    │
│   • .take(MAX_CHANNELS) - bounds check even if host lies    │
│   • Build Buffer/AuxiliaryBuffers from pointers             │
│   • Call Processor::process()                               │
└─────────────────────────────────────────────────────────────┘
    │
    ▼ (on sample rate change)
┌─────────────────────────────────────────────────────────────┐
│ setupProcessing() with new setup                            │
│   • Processor::unprepare() → Descriptor                     │
│     - Parameters preserved, DSP state discarded             │
│   • descriptor.prepare(new_setup) → Processor               │
│     - DSP state reallocated for new sample rate             │
└─────────────────────────────────────────────────────────────┘

Key invariant: After setupProcessing() succeeds, process() never allocates.


Inspiration

Project
Tauri WebView integration, IPC patterns
Apple AUv3 Audio Unit v3 specification
VST3 SDK VST3 specification and reference
Coupler VST3 Rust bindings (dependency)
nih-plug Rust plugin framework reference
iPlug2 C++ plugin framework reference
JUCE C++ plugin framework reference