truce_core/export.rs
1use std::sync::Arc;
2
3use crate::plugin::PluginRuntime;
4use truce_params::{ParamInfo, Params};
5
6/// Unified export trait for all plugin formats.
7///
8/// Implement this once on your plugin type. All format wrappers
9/// (CLAP, VST3, AU, standalone) use this to construct your plugin
10/// and access its parameters.
11///
12/// ```ignore
13/// impl PluginExport for MyPlugin {
14/// type Params = MyParams;
15/// fn create() -> Self { Self::new() }
16/// fn params(&self) -> &MyParams { &self.params }
17/// fn params_arc(&self) -> Arc<MyParams> { self.params.clone() }
18/// }
19/// ```
20///
21/// All parameter mutation goes through the atomic-backed accessors on
22/// `&Params` - no `&mut Params` accessor is required, which keeps the
23/// trait usable while the editor holds an `Arc<Params>` reader.
24pub trait PluginExport: PluginRuntime + Sized {
25 type Params: Params;
26
27 /// Construct a new instance of the plugin.
28 fn create() -> Self;
29
30 /// Immutable access to the parameter struct.
31 fn params(&self) -> &Self::Params;
32
33 /// Get a shared `Arc` reference to the parameter struct.
34 ///
35 /// Used by format wrappers to pass params to GUI closures without
36 /// raw pointers. The Arc is cloned (cheap ref-count bump), not the
37 /// params themselves.
38 fn params_arc(&self) -> Arc<Self::Params>;
39
40 /// Shared meter storage handle, mirroring [`Self::params_arc`]:
41 /// the audio thread publishes meter values into this store from
42 /// inside `process()` (via the shells' meter callback), and GUI
43 /// `get_meter` closures read it - never the plugin instance,
44 /// whose `&mut` the audio thread holds for the whole block.
45 ///
46 /// The `truce::plugin!` shells own the store and return their
47 /// handle here; a hand-written `PluginExport` impl keeps one
48 /// alongside its params `Arc`.
49 fn meter_store(&self) -> Arc<crate::meters::MeterStore>;
50
51 /// Shared snapshot slot for lock-free state save. The shell (audio
52 /// thread) publishes the plugin's custom state into it after each
53 /// block when the plugin overrides `snapshot_into`; the wrapper's
54 /// `save_state` reads it without taking the plugin lock. A plugin
55 /// that doesn't opt in never publishes, so the slot stays empty and
56 /// `save_state` falls back to the locked path.
57 ///
58 /// The `truce::plugin!` shells own the slot and return their handle
59 /// here; a hand-written impl keeps one alongside its params `Arc`.
60 fn snapshot_slot(&self) -> Arc<crate::snapshot::SnapshotSlot>;
61
62 /// The plugin's background-task spawner, or `None` when the plugin
63 /// wired no `tasks:` on `plugin!`. Format wrappers stamp it into the
64 /// editor's `PluginContext` (via `PluginContext::with_tasks`) so the
65 /// GUI can schedule work, matching what the shell does for `process`.
66 /// The default suits plugins with no background tasks.
67 fn task_spawner(&self) -> Option<crate::tasks::AnyTaskSpawner> {
68 None
69 }
70
71 /// A lock-free editor builder: hand it the param store and it
72 /// returns the editor (or `None` for a headless plugin).
73 ///
74 /// Called once at instance creation - format wrappers cache the
75 /// returned closure *outside* the plugin lock (alongside
76 /// `params_arc`), then invoke it when the host opens the GUI, so
77 /// editor construction never takes the plugin lock and never waits
78 /// on an in-flight audio block. The closure binds only the
79 /// lock-free param store, so a `--shell` build's closure rebuilds
80 /// from the *reloaded* dylib (GUI hot-reload survives, picked up on
81 /// the next editor close+open). The `truce::plugin!` shells provide
82 /// this; a hand-written impl returns a closure that builds its
83 /// editor directly. Default: a closure that returns `None`.
84 fn editor_builder(&self) -> crate::editor::EditorBuilder<Self::Params> {
85 Box::new(|_params| None)
86 }
87
88 /// Static parameter metadata for registration-time access.
89 ///
90 /// Format wrappers' `register_*` paths (`truce-vst2`, `truce-vst3`,
91 /// `truce-au`, `truce-aax`) call this instead of the historical
92 /// `Self::create().params().param_infos()` walk, which constructed
93 /// a full plugin instance - including any allocation the
94 /// constructor did (DSP buffers, FFT plans, image atlases) - just
95 /// to read static metadata. On platforms where registration runs
96 /// from C++ static initializers (notably AAX `Describe`) those
97 /// allocations sit in a fragile init-order regime; avoiding them
98 /// closes a class of platform-dependent registration bugs.
99 ///
100 /// Default impl prefers
101 /// [`Params::param_infos_static`]
102 /// when it returns a non-empty vec (the `#[derive(Params)]` path
103 /// emits an override built from compile-time metadata) and falls
104 /// back to the runtime construction otherwise - so plugins with
105 /// hand-written `Params` impls that don't override the static
106 /// path keep working unchanged.
107 #[must_use]
108 fn param_infos_static() -> Vec<ParamInfo> {
109 let from_params = <Self::Params as Params>::param_infos_static();
110 if from_params.is_empty() {
111 Self::create().params().param_infos()
112 } else {
113 from_params
114 }
115 }
116
117 /// Static "does this plugin have an editor" predicate. AAX's
118 /// `Describe` path needs to know this at registration time
119 /// (`has_editor` field on the static descriptor). Paired with
120 /// [`Self::param_infos_static`], this is the second reason every
121 /// format's registration walk constructed a plugin.
122 ///
123 /// Default impl falls back to that runtime path so unannotated
124 /// plugins keep working. Plugins that want to avoid the
125 /// static-init plugin construction (notably for AAX hosts that
126 /// run `Describe` very early) override with a `const`-style
127 /// answer:
128 ///
129 /// ```ignore
130 /// impl PluginExport for MyPlugin {
131 /// // ...
132 /// fn has_editor_static() -> bool { true }
133 /// }
134 /// ```
135 ///
136 /// VST2 / VST3 / AU never call this - they don't need the answer
137 /// at registration time.
138 #[must_use]
139 fn has_editor_static() -> bool {
140 let plugin = Self::create();
141 plugin.editor_builder()(plugin.params_arc()).is_some()
142 }
143}
144
145/// Serialize a plugin's custom state for a **non-realtime** save path -
146/// LV2, the standalone host, preset export. Prefers the off-thread
147/// publisher lane; otherwise serializes live via `snapshot_into` (the
148/// canonical path, whose default delegates to a legacy `save_state`).
149/// Not for CLAP / VST3 / AU: those read the lock-free snapshot slot
150/// directly so a host save never stalls the audio thread.
151#[must_use]
152pub fn read_custom_state_offthread<P: PluginExport>(plugin: &P) -> Vec<u8> {
153 plugin.snapshot_slot().read_offthread().unwrap_or_else(|| {
154 let mut buf = Vec::new();
155 plugin.snapshot_into(&mut buf);
156 buf
157 })
158}