Skip to main content

truce_loader/
static_shell.rs

1//! StaticShell — embeds PluginLogic directly into the plugin binary.
2//!
3//! No dlopen, no file watcher, no Mutex. Same types as HotShell
4//! but zero runtime overhead. Use via `export_static!`.
5
6use std::sync::atomic::{AtomicU32, Ordering};
7use std::sync::Arc;
8
9use truce_core::buffer::AudioBuffer;
10use truce_core::bus::BusLayout;
11use truce_core::events::{EventBody, EventList};
12use truce_core::info::PluginInfo;
13use truce_core::process::{ProcessContext, ProcessStatus};
14use truce_core::plugin::Plugin;
15use truce_params::Params;
16
17use crate::traits::PluginLogic;
18
19// ---------------------------------------------------------------------------
20// StaticShell
21// ---------------------------------------------------------------------------
22
23/// A static plugin shell that embeds `PluginLogic` directly.
24///
25/// Same bridging as `HotShell` but without `NativeLoader`, `Mutex`,
26/// file watching, or any dynamic loading overhead. Use via `export_static!`.
27pub struct StaticShell<P: Params, L: PluginLogic> {
28    pub params: Arc<P>,
29    logic: L,
30    meters: Arc<[AtomicU32; 256]>,
31    sample_rate: f64,
32}
33
34unsafe impl<P: Params, L: PluginLogic> Send for StaticShell<P, L> {}
35
36impl<P: Params + Default + 'static, L: PluginLogic + 'static> StaticShell<P, L> {
37    pub fn new(params: P) -> Self {
38        Self {
39            params: Arc::new(params),
40            logic: L::new(),
41            meters: Arc::new(std::array::from_fn(|_| AtomicU32::new(0))),
42            sample_rate: 44100.0,
43        }
44    }
45
46    /// Access the plugin logic (for testing).
47    pub fn logic_ref(&self) -> &L {
48        &self.logic
49    }
50
51    /// Mutable access to the plugin logic (for testing).
52    pub fn logic_ref_mut(&mut self) -> &mut L {
53        &mut self.logic
54    }
55
56    /// Try to get a custom editor from the plugin logic.
57    pub fn try_custom_editor(&self) -> Option<Box<dyn truce_core::editor::Editor>> {
58        self.logic.custom_editor()
59    }
60
61    /// Try to create a `BuiltinEditor` from the plugin's layout.
62    /// Returns `None` if the layout has zero size.
63    pub fn try_builtin_editor(&self) -> Option<truce_gui::editor::BuiltinEditor<P>> {
64        let layout = self.logic.layout();
65        if layout.width == 0 || layout.height == 0 {
66            return None;
67        }
68        Some(truce_gui::editor::BuiltinEditor::new_grid(Arc::clone(&self.params), layout))
69    }
70}
71
72impl<P: Params + Default + 'static, L: PluginLogic + 'static> Plugin for StaticShell<P, L> {
73    fn info() -> PluginInfo where Self: Sized {
74        unreachable!("StaticShell::info() should not be called statically")
75    }
76
77    fn bus_layouts() -> Vec<BusLayout> where Self: Sized {
78        unreachable!("StaticShell::bus_layouts() should not be called statically")
79    }
80
81    fn init(&mut self) {}
82
83    fn reset(&mut self, sample_rate: f64, max_block_size: usize) {
84        self.sample_rate = sample_rate;
85        self.params.set_sample_rate(sample_rate);
86        self.logic.reset(sample_rate, max_block_size);
87    }
88
89    fn process(
90        &mut self,
91        buffer: &mut AudioBuffer,
92        events: &EventList,
93        context: &mut ProcessContext,
94    ) -> ProcessStatus {
95        // Apply parameter change events to the shell's params.
96        // ParamChange values from format wrappers are PLAIN (already denormalized).
97        // Using set_normalized here would double-denormalize. (Regression: see param_sync_test)
98        for e in events.iter() {
99            if let EventBody::ParamChange { id, value } = &e.body {
100                self.params.set_plain(*id, *value);
101            }
102        }
103
104        // Sync the plugin's own params from the shell's params.
105        // Uses set_plain WITHOUT snap_smoothers — smoothers advance naturally
106        // via smoothed_next() in the plugin's process(). (Regression: see smoother_test)
107        if let Some(plugin_params) = self.logic.params_mut() {
108            for info in self.params.param_infos() {
109                if let Some(value) = self.params.get_plain(info.id) {
110                    plugin_params.set_plain(info.id, value);
111                }
112            }
113        }
114
115        // Build a ProcessContext with param/meter callbacks for the logic.
116        let params = &self.params;
117        let meters = &self.meters;
118        let param_fn = |id: u32| -> f64 { params.get_plain(id).unwrap_or(0.0) };
119        let meter_fn = |id: u32, v: f32| {
120            if let Some(slot) = meters.get(id as usize) {
121                slot.store(v.to_bits(), Ordering::Relaxed);
122            }
123        };
124        let mut output_events = EventList::new();
125        let mut ctx = ProcessContext::new(
126            context.transport,
127            context.sample_rate,
128            buffer.num_samples(),
129            &mut output_events,
130        )
131        .with_params(&param_fn)
132        .with_meters(&meter_fn);
133
134        let result = self.logic.process(buffer, events, &mut ctx);
135
136        // Copy output events back to the host.
137        for event in output_events.iter() {
138            context.output_events.push(event.clone());
139        }
140
141        result
142    }
143
144    fn save_state(&self) -> Option<Vec<u8>> {
145        let data = self.logic.save_state();
146        if data.is_empty() { None } else { Some(data) }
147    }
148
149    fn load_state(&mut self, data: &[u8]) {
150        self.logic.load_state(data);
151    }
152
153    fn editor(&mut self) -> Option<Box<dyn truce_core::editor::Editor>> {
154        if let Some(editor) = self.logic.custom_editor() {
155            return Some(editor);
156        }
157        self.try_builtin_editor()
158            .map(|e| Box::new(e) as Box<dyn truce_core::editor::Editor>)
159    }
160
161    fn latency(&self) -> u32 { self.logic.latency() }
162    fn tail(&self) -> u32 { self.logic.tail() }
163
164    fn get_meter(&self, meter_id: u32) -> f32 {
165        if let Some(slot) = self.meters.get(meter_id as usize) {
166            f32::from_bits(slot.load(Ordering::Relaxed))
167        } else {
168            0.0
169        }
170    }
171}
172
173// ---------------------------------------------------------------------------
174// export_static! macro
175// ---------------------------------------------------------------------------
176
177/// Compile-time static embedding of a `PluginLogic` type.
178///
179/// Produces a `__HotShellWrapper` struct that implements `Plugin + PluginExport`,
180/// so format export macros (`export_clap!`, `export_vst3!`, etc.) work unchanged.
181/// No dlopen, no file watcher, zero runtime overhead.
182///
183/// ```ignore
184/// export_static! {
185///     params: GainParams,
186///     info: plugin_info!(...),
187///     bus_layouts: [BusLayout::stereo()],
188///     logic: Gain,
189/// }
190///
191/// #[cfg(feature = "clap")]
192/// truce_clap::export_clap!(__HotShellWrapper);
193/// ```
194#[macro_export]
195macro_rules! export_static {
196    (
197        params: $params:ty,
198        info: $info:expr,
199        bus_layouts: [$($layout:expr),* $(,)?],
200        logic: $logic:ty,
201        editor: { $($editor_body:tt)* },
202    ) => {
203        struct __HotShellWrapper {
204            inner: $crate::static_shell::StaticShell<$params, $logic>,
205        }
206
207        impl truce_core::plugin::Plugin for __HotShellWrapper {
208            fn info() -> truce_core::info::PluginInfo where Self: Sized {
209                $info
210            }
211
212            fn bus_layouts() -> Vec<truce_core::bus::BusLayout> where Self: Sized {
213                vec![$($layout),*]
214            }
215
216            fn init(&mut self) {
217                self.inner.init();
218            }
219
220            fn reset(&mut self, sample_rate: f64, max_block_size: usize) {
221                self.inner.reset(sample_rate, max_block_size);
222            }
223
224            fn process(
225                &mut self,
226                buffer: &mut truce_core::buffer::AudioBuffer,
227                events: &truce_core::events::EventList,
228                context: &mut truce_core::process::ProcessContext,
229            ) -> truce_core::process::ProcessStatus {
230                self.inner.process(buffer, events, context)
231            }
232
233            fn save_state(&self) -> Option<Vec<u8>> {
234                self.inner.save_state()
235            }
236
237            fn load_state(&mut self, data: &[u8]) {
238                self.inner.load_state(data);
239            }
240
241            fn editor(&mut self) -> Option<Box<dyn truce_core::editor::Editor>> {
242                if let Some(e) = self.inner.try_custom_editor() {
243                    return Some(e);
244                }
245                if let Some(builtin) = self.inner.try_builtin_editor() {
246                    return Some($($editor_body)*(builtin));
247                }
248                None
249            }
250
251            fn latency(&self) -> u32 { self.inner.latency() }
252            fn tail(&self) -> u32 { self.inner.tail() }
253            fn get_meter(&self, meter_id: u32) -> f32 { self.inner.get_meter(meter_id) }
254        }
255
256        impl truce_core::export::PluginExport for __HotShellWrapper {
257            type Params = $params;
258
259            fn create() -> Self {
260                Self {
261                    inner: $crate::static_shell::StaticShell::new(<$params>::new()),
262                }
263            }
264
265            fn params(&self) -> &$params {
266                &self.inner.params
267            }
268
269            fn params_mut(&mut self) -> &mut $params {
270                // SAFETY: Only called during activate/deactivate when the editor
271                // is not open (no concurrent Arc refs to params).
272                std::sync::Arc::get_mut(&mut self.inner.params)
273                    .expect("params_mut called while Arc has other refs")
274            }
275
276            fn params_arc(&self) -> std::sync::Arc<$params> {
277                std::sync::Arc::clone(&self.inner.params)
278            }
279        }
280    };
281}