AUDIO
Source snapshot · 6 November 2026

Plug-in export layer

Exporting a plug-in

Implement PluginExport on a marker type that names the processor and its product, then call export_plugin! once at the root of a cdylib crate:

use ravet_plugin::{PluginExport, Product};

pub struct GainPlugin;
impl PluginExport for GainPlugin {
    type Processor = ravet_gain::GainProcessor;
    const PRODUCT: Product<'static> = /* the product description */;
    fn create() -> ravet_gain::GainProcessor {
        ravet_gain::GainProcessor::default()
    }
}
ravet_plugin::export_plugin!(GainPlugin);

Any existing Processor<f32> can be exported unchanged. The crate must not forbid(unsafe_code), because the plug-in entry point is a foreign function; deny is fine. examples/gain-plugin exports the gain reference this way, and python3 scripts/bundle-clap.py --plugin generic bundles it. The tempo-synced delay in examples/delay-plugin is a fuller example with explicit parameter IDs, host tempo, a tail and a web editor; --plugin delay bundles it. The deterministic instrument adds notes, MIDI echo, CC response and switchable nonzero latency through the same generic layer.

The processor receives parameter changes as ParameterValue events with the product's host IDs and physical values, already clamped and snapped to range. migrate_state and import_state supply the state hooks.

Automation and bypass policies

Every format runs the product's processor inside a PolicyProcessor, so these policies behave the same in CLAP and in the native table:

use ravet_plugin::{AutomationPolicy, BypassPolicy};

const PRODUCT: Product = Product::new(/* … */)
    .with_parameters(PARAMETERS) // must include a Parameter::bypass
    .with_automation(AutomationPolicy::PerBlock)
    .with_bypass(BypassPolicy::Crossfade { milliseconds: 10.0 });
Policy Behaviour
AutomationPolicy::SampleAccurate (default) Parameter events keep the host's frame offsets.
AutomationPolicy::PerBlock Every parameter event (values, modulation, gestures, global or scoped) moves to offset 0, ahead of the block's other events, in its original order.
BypassPolicy::Processor (default) The processor receives the bypass parameter and implements bypass itself.
BypassPolicy::Hard The processor keeps running; the output switches to the dry input at the bypass event's offset.
BypassPolicy::Crossfade { milliseconds } As Hard, with a linear crossfade (more than 0, at most 1,000 ms) starting at the event's offset. A reset completes a running crossfade.
BypassPolicy::Frozen While bypassed the processor does not run, so its history is frozen. The input is copied to the output with AudioBuffer::pass_through, without scratch storage. Parameter events still reach the processor in a zero-frame block; notes and other events are discarded. The switch happens on block boundaries, using the block's last bypass value. Not available with auxiliary buses.

Every policy except Processor consumes the bypass parameter: the processor never receives its events, and Product::resolve rejects the policy unless the product declares a Parameter::bypass (ProductError::BypassPolicyWithoutParameter). Hard and Crossfade keep one prepared dry copy per output channel; with fewer inputs than outputs the last input feeds the remaining outputs, and an instrument's dry signal is silence. The dry path is not latency-compensated: a product that reports latency and needs a compensated bypass keeps BypassPolicy::Processor. Processing and reset never allocate under any policy.

AudioBuffer::pass_through (in ravet-foundation) is also usable directly: with separate storage it copies input channel c, or the last input, to output c; in place it does nothing; with no inputs it clears the outputs.

Denormal guard

Every export wraps the policy processor in ravet_native_dsp::DenormalProcessor with DenormalMode::FlushBoth: for the duration of each processing call, denormal inputs and results are flushed to zero (FTZ/DAZ on x86-64, FZ on AArch64), and the previous floating-point mode is restored afterwards, also on unwinding. The product's processor and the policies therefore run with denormals flushed in every format; products need no guard of their own. A platform where the mode cannot be set fails activation (CLAP) or creation (the native table) instead of processing without the guard.

The native product library additionally exports ravet_plugin_with_denormal_mode_v1(mode, callback, context) from RavetPlugin.h. Mode 0 preserves the current mode; mode 1 enters ScopedDenormalMode::FlushBoth. It calls the C callback synchronously on the calling thread and restores the exact prior control and sticky status on return. Nested calls restore lexically. Returns 0 on success, 1 for an invalid mode/null callback, or 2 for an unsupported actual environment without invoking the callback. No allocation, token or arbitrary register setter is involved.

static void process_native(void *context) { /* bounded DSP using context */ }
int32_t status = ravet_plugin_with_denormal_mode_v1(1, process_native, context);

The caller keeps the context valid and the callback must return normally on this thread: no unwind, longjmp or retained scope. The symbol lives in the exported product library; adapters need no separate call because their table processing already has the guard. VST3 Component::process, AU render chunks, AAX algorithm callbacks and LV2 run chunks dispatch through that same native process pointer. CLAP process/active flush dispatch through the wrapped bridge. Host event translation outside product DSP does not change the host's floating-point policy. Native table tests observe flushing inside DSP, and the instrument's C callback test checks nested scope entry and exact restoration. See the instrument and decision 0291.

CLAP export

The clap feature (plugin-clap on the umbrella crate) builds the CLAP side:

Limits for now:

Native table

With the native feature, export_plugin! also exports ravet_plugin_v1, which returns the RavetPluginEntryV1 table declared in crates/ravet-plugin/include/RavetPlugin.h. The VST3, AUv2, AAX and LV2 adapters are written once against it. The header is the contract; the Rust side (ravet_plugin::native) behaves as follows:

Limits for now: main bus only; product extra state is not saved. Named-host runs of the Delay through the native adapters are recorded separately in the support matrix; those runs do not qualify arbitrary products. The table's own unit tests drive every function through the table (including a C99 compile of the header that checks every record offset against the Rust definitions), and examples/delay-plugin/tests/native.rs uses the delay's exported table end to end.

AAX export

The generic AAX adapter in crates/ravet-plugin/aax turns any product into an unsigned .aaxplugin on macOS and Windows from the product's table (ravet_plugin_v1): python3 scripts/bundle-aax.py --plugin delay --aax-sdk <external AAX SDK>, or --package <crate> for another product crate. Vendors sign (PACE-wrap) their own builds. See AAX for what maps where, the Windows commands, checks and limits.