AUDIO
Source snapshot · 6 November 2026

Plug-in export layer

Status: Prototype, under construction for 0.1.0. The design is decision 0281. The product description, saved-state envelope, editor protocol, automation and bypass policies, a generic CLAP export and the generic C table for the other formats exist in ravet-plugin (umbrella features plugin, plugin-clap and plugin-native; crate feature native for the table). Generic adapters over that table exist as prototypes for VST3, AUv2, AAX and LV2. Product web editors open on macOS, Windows and Linux (decision 0293), and the unified bundle tool builds every format.

Topics in this guide

Product description

A Product describes a plug-in once, usually as a const:

use ravet_plugin::{Category, Parameter, Product, StateFormat};

const PARAMETERS: &[Parameter] = &[
    Parameter::linear("gain_db", "Gain", -60.0, 12.0, 0.0).with_unit("dB"),
    Parameter::bypass("bypass", "Bypass"),
];
const PRODUCT: Product = Product::new(
    "Example Gain",
    "Example Audio",
    "1.0.0",
    "com.example.gain",
    Category::Effect,
    StateFormat { tag: *b"EXGAIN01", version: 1 },
)
.with_parameters(PARAMETERS);

Product::resolve validates the description on a control thread. It checks names, the three-part version, the reverse-domain plug-in ID, layouts (effects need audio input), at most 1,024 parameters, keys, defaults and a single bypass toggle. It then resolves parameter IDs and format identities.

Each parameter has a permanent key: an ASCII letter or underscore followed by up to 63 letters, digits or underscores. Its host ID is either explicit (with_id), so existing sessions keep loading, or derived from the key by 32-bit FNV-1a with the top bit cleared. IDs never depend on declaration order. Two parameters whose derived IDs collide are rejected; give one an explicit ID.

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.

Robustness at the host boundary

Exports run inside third-party hosts, so no panic or C++ exception may cross into one (decision 0287):

Linux and Windows builds

On Linux the bundle scripts run natively and produce CLAP, VST3 and LV2 bundles (AAX is built on macOS and Windows only):

python3 scripts/bundle-clap.py --plugin delay        # target/clap/release/RAVET Delay.clap
python3 scripts/bundle-vst3.py --plugin delay --sdk /path/to/vst3sdk
python3 scripts/bundle-lv2.py --plugin delay --sdk /path/to/lv2

The CLAP bundle on Linux and Windows is the product library itself, renamed to <Name>.clap. Editor products need GTK 3, WebKitGTK 4.1 and JSON-GLib development packages on Linux, and on Windows the WebView2 SDK at build time (RAVET_WEBVIEW2_SDK, an extracted Microsoft.Web.WebView2 NuGet package; see bundle tool) and the WebView2 Runtime on the user's machine. MSVC builds link the SDK's static loader, so no WebView2Loader.dll ships in any bundle.

Windows cross-builds

--target windows on bundle-clap.py, bundle-vst3.py, bundle-lv2.py and bundle-aax.py builds the x86_64 Windows bundles from Linux (or macOS) (decision 0285):

# Once: cargo-xwin and the Windows target. The first cargo-xwin run downloads
# Microsoft's CRT and Windows SDK and asks you to accept their licence.
cargo install --locked cargo-xwin
rustup target add x86_64-pc-windows-msvc
# Also needed: CMake, clang (clang-cl), and Wine for LV2 (optional otherwise).

python3 scripts/bundle-clap.py --plugin delay --target windows
python3 scripts/bundle-vst3.py --plugin delay --sdk /path/to/vst3sdk --target windows
python3 scripts/bundle-lv2.py --plugin delay --sdk /path/to/lv2 --target windows
python3 scripts/bundle-aax.py --plugin delay --aax-sdk /path/to/AAX_SDK --target windows
Format Windows bundle Windows check to run there
CLAP target/clap/windows/release/RAVET Delay.clap clap-validator.exe validate "RAVET Delay.clap"
VST3 target/vst3/generic/delay/windows-release/VST3/Release/ravet_delay_vst3.vst3 validator.exe from that build's bin/
LV2 target/lv2/windows/ravet_delay.lv2 ravet_lv2_contracts.exe from target/lv2/windows/generic-ravet_delay/
AAX target/aax-plugin/windows-delay/RAVET Delay.aaxplugin ravet_aax_check.exe from the same directory (not Avid's validator)

How it works:

The official clap-validator Windows release needs the Visual C++ Redistributable; on a Windows installation without it, build clap-validator from source with cargo xwin build --release --target x86_64-pc-windows-msvc and CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_RUSTFLAGS="-C target-feature=+crt-static". Native Windows builds (MSVC) use the commands in each format's guide; they have not been run. Evidence for both platforms is in work status.

Format identities

All identities derive from the plug-in ID, using the same derivations as RAVET Foundry:

Format Identity
CLAP The plug-in ID
VST3 Processor and controller class IDs from SHA-256 of <id>/processor and <id>/controller
AU Type from the category (aufx, aumu, aumf); subtype from the plug-in ID, manufacturer from the vendor (four letters each)
AAX Manufacturer, product and one type ID per layout, from SHA-256 of <id>/MANUFACTURER, <id>/PRODUCT, <id>/MONO, <id>/STEREO or <id>/<inputs>x<outputs>
LV2 urn:<id>

IdentityOverrides keeps any of them from an earlier product. Tests pin every derivation, which never changes within a major version.

Saved state

ravet_plugin::state defines one little-endian envelope for every format: magic RAVETSTA, envelope version, product tag and state version, parameter ID/value pairs and optional product extra data (up to 16 MiB). Read-only parameters are not saved.

state::restore matches saved values to the current product by ID. Unknown IDs (removed parameters) are dropped and missing ones take defaults. Values outside the current range are rejected rather than clamped. State from a newer product version is rejected. A product's StateHooks can migrate state written by an earlier product state version, and import data without the RAVET magic, such as sessions from another framework. Restoring never partially applies state.

These rules implement the saved-state section of the compatibility policy.

Editor protocol

ravet_plugin::editor defines the JSON messages between a plug-in and its web editor, so products write no editor codec. Messages are objects with a type; parameters are addressed by key, and values are physical unless sent as normalized.

Direction type Fields
Editor → plug-in ready protocol (currently 1)
begin, end key
set key, value or normalized, inside an open gesture
commit key, value or normalized: one complete edit
product data, product-defined
Plug-in → editor info protocol, product name/vendor/version/category, parameter metadata
snapshot sequence, all values by key, optional telemetry and product
changes sequence, changed values by key
product data

editor::parse rejects messages over 64 KiB, malformed JSON, unknown types or keys, edits to read-only parameters, nonfinite values, and a protocol other than 1. Values clamp and snap to the parameter's range. Gestures tracks open gestures per editor: set needs an open gesture, commit is refused while one is open on the same parameter, and close_all returns the gestures to end when the editor closes. Several parameters may have gestures open at once. sequence increases with each snapshot or change message, so editors can drop stale updates.

The transport and injected JavaScript client are described below.

Editors

Product::with_editor(Editor { html, width, height, resizable }) gives a product a web editor: one HTML page of up to 1 MiB, between 64 and 4096 pixels in each direction. Resizable editors may grow to four times their size.

The editor is built only with the ravet-plugin feature editor (umbrella plugin-editor), which adds ravet-webview:

ravet-plugin = { version = "…", features = ["clap", "native", "editor"] }

Without it, clap and native build no web view: the CLAP export offers no GUI extension and the native table reports a 0 × 0 editor, so adapters show none. Products without an editor leave the feature off and link no WebKit or Cocoa bindings. The delay and gain examples turn it on.

The editor opens in every format that has an embedded editor on the platform (decision 0293):

Platform Web view CLAP VST3 LV2 AAX AUv2
macOS Cocoa/WebKit cocoa NSView CocoaUI NSView Cocoa view
Windows Win32/WebView2 win32 HWND WindowsUI HWND —
Linux GTK 3/WebKitGTK 4.1 on X11 or XWayland x11 X11EmbedWindowID X11UI — —

Windows needs the WebView2 Runtime installed (Windows 11 ships it); editor sizes there are the host's physical pixels, and WebView2 scales page content by the monitor's DPI, so above 100 % display scaling a fixed-size page has fewer CSS pixels than its declared size. Keep layouts responsive; content-scale negotiation is open work. On Linux the parent is an X11 window, which need not be an XEmbed socket: the editor's GtkPlug is mapped inside plain host windows such as REAPER's. GTK work is pumped by editor_idle (VST3 IRunLoop timer, LV2 idle interface) or a CLAP host timer; hosts already running GTK on Wayland, or built on GTK 2/GTK 4, are refused. Keyboard input in Windows and Linux editors is not validated yet. Evidence and hosts are listed in the support matrix and work status.

Editor assets

Images, fonts, scripts and styles need not be inlined into the 1 MiB page or sent through 64 KiB messages. Declare them as EditorAssets and refer to them by relative URL:

```rust,ignore use ravet_plugin::{Editor, EditorAsset};

const ASSETS: &[EditorAsset] = &[ EditorAsset::new("app.js", include_bytes!("../editor/app.js")), EditorAsset::new("img/knob.png", include_bytes!("../editor/img/knob.png")), // An explicit content type when the extension does not give one. EditorAsset { path: "data/curve.bin", mime: "application/octet-stream", data: include_bytes!("../editor/curve.bin") }, ]; // Product::new(…).with_editor(Editor { … }).with_editor_assets(ASSETS) ```

The page then refers to each asset by its relative path, exactly as declared: a stylesheet link to style.css, a script element for app.js and an image whose source is img/knob.png.

With assets, the page itself is served as index.html from a local asset origin that also serves them: ravet://editor/ on macOS, ravet-<id>://editor/ on Linux and https://editor.ravet.invalid/ on Windows. Requests are answered from the binary's memory (GET only, 404 for unknown paths); nothing is read from disk and nothing reaches the network. Products without assets keep the inline page. Limits, checked by Product::resolve (ProductError::InvalidEditorAsset): at most 4,096 assets, 64 MiB each and 256 MiB in total; paths of at most 512 bytes from ASCII letters, digits and ._-/, with no ., .. or empty segment; index.html is reserved. An empty mime infers the type from the extension (HTML, CSS, JavaScript, JSON, SVG, PNG, JPEG, GIF, WebP, ICO, WOFF/WOFF2, TTF/OTF, WASM, WAV, MP3, text), else application/octet-stream. Each request copies the asset once. The Gain example ships asset.css this way.

RAVET injects its editor client into the page's <head>. The client speaks the editor protocol and exposes window.ravetPlugin:

Member Purpose
info Product and parameter metadata, or null until it arrives
value(key), normalized(key) Current physical or 0–1 value
on("info" \| "values" \| "product", listener) Metadata, value updates ({values, changed}) and product messages
set(key, value), setNormalized(key, n) One complete edit
begin(key), update(key, value), updateNormalized(key, n), end(key) A continuous gesture
send(data) A product-channel message; PluginExport::editor_message may reply

On macOS, focusing a text field in the page takes keyboard focus from hosts that keep it for their own shortcuts, such as Pro Tools, and gives it back when the field loses focus; see keyboard focus. The editor protocol is unchanged.

examples/gain-plugin/editor/index.html is a minimal example; examples/delay-plugin/editor/index.html draws knobs and a division selector.

Editor edits update the parameter cache immediately, then travel through a bounded lock-free queue (256 edits) to whichever thread delivers parameter events. That thread sends the host gesture-begin, value and gesture-end events, and gives the processor the values at the start of the block; host automation later in the block still wins. A complete edit is queued all at once or not at all. If the queue is full, the edit is dropped and the page keeps showing the host's value. Closing the editor ends every gesture the page left open.

The web view asks the plug-in for its next message 20 times a second. Queued messages (info, product replies) go first, then a snapshot, which is rebuilt only when a value changes. Messages are limited to 64 KiB. A product with so many parameters that info exceeds this receives no info. Static data belongs in editor assets; larger dynamic data still has to be split by the product.

Build and distribute the product

Use the unified bundle tool to build one product crate and assemble CLAP, VST3, native AUv2, AAX and LV2 bundles. That guide documents SDK settings/environment precedence, external manifests, output paths, vendor signing hooks and optional format validators. Foundry generates consumer-owned PluginExport projects and calls the same tool. Shipping, production signing and host validation remain product responsibilities.