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:
- The descriptor, audio ports, MIDI input/output ports and parameters come from the product. Parameters with integer steps, and the bypass toggle, are marked stepped.
- Parameter text uses
ravet_plugin::text, shared with the native table. The bypass parameter and unitless 0/1 toggles readOn/Off; hosts may type either the word or the number. Other values show two decimals below 100, one from 100 (dropped when zero) and none for logarithmic values from 1,000, so a fractional value never reads as a whole one. - Host values are snapped to their ranges before the processor sees them, and recorded in a shared parameter cache. Values the host sets on the main thread update the cache immediately, so saving state never misses them.
- State uses the versioned envelope and the product's hooks. Loading state sends every value to the processor and resets it before the next block.
- Latency is reported at activation; a later change requests a host restart.
Tail follows
Processor::tail. Activation applies cached controls before reporting latency; active parameter flushes also detect a latency change. - Declared MIDI output prepares at most 1,024 generated events and delivers validated short MIDI/portable notes through port 0. Both advertised ports use the MIDI dialect. Unrepresentable output rejects the generated stream; host queue rejection discards the remaining suffix. Successful audio remains valid, and no event is replayed on a later block. Cached-state synchronization discards generated zero-frame events, as in the native table.
- Every processing call runs inside the product's policies and the denormal guard.
- A product with an editor gets the CLAP GUI extension when the
editorfeature is on: embeddedcocoaviews on macOS,win32on Windows andx11on Linux. On Linux the editor also registers a 10 ms host timer (timer-support) to pump GTK while it exists; see editors. - A block carries at most 1,024 events, counting queued editor values. A block with more host events is rejected: its outputs are silenced and its values are not applied, as in the native table. Editor values that do not fit wait for the next block. Only core-namespace events are passed to the processor, so the preallocated event buffer never grows on the audio thread.
Limits for now:
- Only the first declared layout is used (no audio-port configurations).
- Processing is f32.
- Product extra state is not exported.
- Floating editor windows are not offered, and the CLAP
set_scalehint is accepted but not applied (see editors for the Windows DPI limit). - The automation reflection racing a concurrent state load can keep the automated value for that parameter.
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:
- Static data. The product record, per-format identities, layouts (with
their AAX type IDs) and parameter records are built once, on the first call,
and live as long as the library. An invalid product, or one declaring only
f64 processing, has no table (
NULL). - Parameters. Text uses the same formatting as CLAP; text that does not fit
the caller's buffer is truncated at a character boundary and reported with 1.
normalize/denormalizehonour scale and steps and clamp and snap. - Instances own the parameter cache, the editor edit queue and the
processor inside its policies and denormal guard.
set_value(a host-side change) and editor edits update the cache at once and mark the parameter; the nextprocessdelivers marked values at offset 0, ahead of the host's events. At most 1,024 events reach the processor per block; marked changes that do not fit wait for the next block.save_statewrites the envelope from the cache.load_stateapplies all values or none, using the product's migrate and import hooks; before the next block the processor receives every value in a zero-frame block, then a reset (events the processor generates in that zero-frame block are discarded). - Processing is 32-bit float, main bus only. Buses, events and transport are
converted by
ravet-native; host buffers are copied into prepared scratch, so in-place hosts are safe. Host parameter values need not be exactly representable asfloat; they are clamped, snapped and written to the cache, and events for unknown IDs or with nonfinite values are dropped.processandresetnever allocate.latency_changedreports a latency change after a block until the adapter readstiming. - Faults. A null or misaligned instance pointer is rejected (1, or
NaNfromget_value) without being dereferenced. Every function catches panics and returns 2. After a panic,process(silencing its outputs),resetandtimingreturn 2 until the next successfulprepare, which recreates the processor; parameter, state and editor calls keep working. - Editor.
editor_openattaches the product's web editor, using the same editor host as the CLAP GUI extension, to the platform's view kind:RAVET_PLUGIN_VIEW_COCOA(NSView) on macOS,RAVET_PLUGIN_VIEW_WIN32(HWND) on Windows andRAVET_PLUGIN_VIEW_X11(X11 Window ID) on Linux; other kinds return 1. Resizable editors accept one to four times the product's size.take_editsreturns the editor's begin/value/end edits for the adapter to report to the host. The web view polls the cache itself;editor_idlepumps GTK on Linux and does nothing elsewhere.
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.