Skip to main content

maolan_engine/plugins/
lv2_proc.rs

1//! Out-of-process LV2 processor using `maolan-plugin-host` IPC.
2
3use crate::audio::io::AudioIO;
4use crate::midi::io::MidiEvent;
5use crate::mutex::UnsafeMutex;
6use crate::plugins::ipc;
7use maolan_plugin_protocol::events::EventPair;
8use maolan_plugin_protocol::protocol::*;
9use maolan_plugin_protocol::ringbuf::RingBuffer;
10use maolan_plugin_protocol::shm::ShmMapping;
11use std::collections::HashMap;
12use std::path::PathBuf;
13use std::process::Child;
14use std::sync::atomic::{AtomicBool, Ordering};
15use std::sync::{Arc, atomic::AtomicU32};
16use std::time::{Duration, Instant};
17
18/// Shared state for an out-of-process LV2 plugin instance.
19pub struct Lv2Processor {
20    uri: String,
21    name: String,
22    audio_inputs: Vec<Arc<AudioIO>>,
23    audio_outputs: Vec<Arc<AudioIO>>,
24    main_audio_inputs: usize,
25    main_audio_outputs: usize,
26    param_values: UnsafeMutex<HashMap<u32, f64>>,
27    bypassed: Arc<AtomicBool>,
28    // IPC state
29    child: UnsafeMutex<Option<Child>>,
30    mapping: Option<ShmMapping>,
31    events: Option<EventPair>,
32    shm_name: String,
33    // Crash recovery
34    crash_count: AtomicU32,
35    last_process_time: UnsafeMutex<Instant>,
36}
37
38pub type SharedLv2Processor = Arc<UnsafeMutex<Lv2Processor>>;
39
40impl Lv2Processor {
41    pub fn new(
42        sample_rate: f64,
43        buffer_size: usize,
44        plugin_uri: &str,
45        input_count: usize,
46        output_count: usize,
47        host_binary: PathBuf,
48    ) -> Result<Self, String> {
49        let audio_inputs = (0..input_count.max(1))
50            .map(|_| Arc::new(AudioIO::new(buffer_size)))
51            .collect::<Vec<_>>();
52        let audio_outputs = (0..output_count.max(1))
53            .map(|_| Arc::new(AudioIO::new(buffer_size)))
54            .collect::<Vec<_>>();
55
56        let instance_id = format!("lv2-{}", std::process::id());
57        let sample_rate_str = sample_rate.to_string();
58        let buffer_size_str = buffer_size.to_string();
59        let num_inputs_str = input_count.max(1).to_string();
60        let num_outputs_str = output_count.max(1).to_string();
61        let (mut child, mapping, events, shm_name) = ipc::spawn_host(ipc::HostSpawnArgs {
62            host_binary: &host_binary,
63            format: "lv2",
64            plugin_spec: plugin_uri,
65            instance_id: &instance_id,
66            extra_args: &[
67                &sample_rate_str,
68                &buffer_size_str,
69                &num_inputs_str,
70                &num_outputs_str,
71            ],
72        })?;
73
74        let header = unsafe { header_ref(mapping.as_ptr()) };
75        if !ipc::wait_for_ready(header, Duration::from_secs(10)) {
76            let _ = child.kill();
77            return Err("LV2 host did not signal ready".to_string());
78        }
79
80        let name = plugin_uri
81            .rsplit_once('/')
82            .map(|(_, name)| name)
83            .unwrap_or(plugin_uri)
84            .to_string();
85
86        Ok(Self {
87            uri: plugin_uri.to_string(),
88            name,
89            audio_inputs,
90            audio_outputs,
91            main_audio_inputs: input_count.max(1),
92            main_audio_outputs: output_count.max(1),
93            param_values: UnsafeMutex::new(HashMap::new()),
94            bypassed: Arc::new(AtomicBool::new(false)),
95            child: UnsafeMutex::new(Some(child)),
96            mapping: Some(mapping),
97            events: Some(events),
98            shm_name,
99            crash_count: AtomicU32::new(0),
100            last_process_time: UnsafeMutex::new(Instant::now()),
101        })
102    }
103
104    pub fn setup_audio_ports(&self) {
105        for port in &self.audio_inputs {
106            port.setup();
107        }
108        for port in &self.audio_outputs {
109            port.setup();
110        }
111    }
112
113    pub fn audio_inputs(&self) -> &[Arc<AudioIO>] {
114        &self.audio_inputs
115    }
116
117    pub fn audio_outputs(&self) -> &[Arc<AudioIO>] {
118        &self.audio_outputs
119    }
120
121    pub fn main_audio_input_count(&self) -> usize {
122        self.main_audio_inputs
123    }
124
125    pub fn main_audio_output_count(&self) -> usize {
126        self.main_audio_outputs
127    }
128
129    pub fn midi_input_count(&self) -> usize {
130        0
131    }
132
133    pub fn midi_output_count(&self) -> usize {
134        0
135    }
136
137    pub fn set_bypassed(&self, bypassed: bool) {
138        self.bypassed.store(bypassed, Ordering::Relaxed);
139    }
140
141    pub fn is_bypassed(&self) -> bool {
142        self.bypassed.load(Ordering::Relaxed)
143    }
144
145    pub fn parameter_values(&self) -> HashMap<u32, f64> {
146        self.param_values.lock().clone()
147    }
148
149    pub fn set_parameter(&self, param_id: u32, value: f64) -> Result<(), String> {
150        self.set_parameter_at(param_id, value, 0)
151    }
152
153    pub fn set_parameter_at(&self, param_id: u32, value: f64, _frame: u32) -> Result<(), String> {
154        self.param_values.lock().insert(param_id, value);
155        if let Some(ref mapping) = self.mapping {
156            let ring = unsafe {
157                let buf = param_ring_ptr(mapping.as_ptr());
158                let (w, r) = param_indices(mapping.as_ptr());
159                RingBuffer::new(buf, w, r, RING_CAPACITY)
160            };
161            let ev = ParameterEvent {
162                param_index: param_id,
163                value: value as f32,
164                sample_offset: 0,
165                event_kind: maolan_plugin_protocol::PARAM_EVENT_VALUE,
166            };
167            if !ring.push(ev) {
168                tracing::warn!("LV2 param ring full, dropping parameter event");
169            }
170        }
171        Ok(())
172    }
173
174    pub fn begin_parameter_edit(&self, _param_id: u32) -> Result<(), String> {
175        Ok(())
176    }
177
178    pub fn end_parameter_edit(&self, _param_id: u32) -> Result<(), String> {
179        Ok(())
180    }
181
182    pub fn is_parameter_edit_active(&self, _param_id: u32) -> bool {
183        false
184    }
185
186    pub fn snapshot_state(&self) -> Result<Vec<u8>, String> {
187        let (mapping, events) = match (&self.mapping, &self.events) {
188            (Some(m), Some(e)) => (m, e),
189            _ => return Err("LV2 processor not initialized".to_string()),
190        };
191        let ptr = mapping.as_ptr();
192        let header = unsafe { header_mut(ptr) };
193
194        header.request_type.store(1, Ordering::Release);
195        header.request_status.store(0, Ordering::Release);
196        if let Err(e) = events.signal_host() {
197            header.request_type.store(0, Ordering::Release);
198            return Err(format!("Failed to signal host for state save: {}", e));
199        }
200
201        if let Err(e) = events.wait_host(Duration::from_secs(5)) {
202            header.request_type.store(0, Ordering::Release);
203            return Err(format!("Host did not respond to state save: {}", e));
204        }
205
206        let status = header.request_status.load(Ordering::Acquire);
207        let size = header.scratch_size.load(Ordering::Acquire) as usize;
208        if status != 1 {
209            header.request_type.store(0, Ordering::Release);
210            return Err("State save failed in host".to_string());
211        }
212
213        let scratch = unsafe { scratch_ptr(ptr) };
214        let mut bytes = vec![0u8; size];
215        unsafe {
216            std::ptr::copy_nonoverlapping(scratch, bytes.as_mut_ptr(), size);
217        }
218        header.request_type.store(0, Ordering::Release);
219        Ok(bytes)
220    }
221
222    pub fn restore_state(&self, state: &[u8]) -> Result<(), String> {
223        let (mapping, events) = match (&self.mapping, &self.events) {
224            (Some(m), Some(e)) => (m, e),
225            _ => return Err("LV2 processor not initialized".to_string()),
226        };
227        let ptr = mapping.as_ptr();
228        let header = unsafe { header_mut(ptr) };
229
230        let scratch = unsafe { scratch_ptr(ptr) };
231        let size = state.len().min(SCRATCH_SIZE);
232        unsafe {
233            std::ptr::copy_nonoverlapping(state.as_ptr(), scratch, size);
234        }
235        header.scratch_size.store(size as u32, Ordering::Release);
236
237        header.request_type.store(2, Ordering::Release);
238        header.request_status.store(0, Ordering::Release);
239        if let Err(e) = events.signal_host() {
240            header.request_type.store(0, Ordering::Release);
241            return Err(format!("Failed to signal host for state restore: {}", e));
242        }
243
244        if let Err(e) = events.wait_host(Duration::from_secs(5)) {
245            header.request_type.store(0, Ordering::Release);
246            return Err(format!("Host did not respond to state restore: {}", e));
247        }
248
249        let status = header.request_status.load(Ordering::Acquire);
250        header.request_type.store(0, Ordering::Release);
251        if status != 1 {
252            return Err("State restore failed in host".to_string());
253        }
254        Ok(())
255    }
256
257    pub fn process_with_audio_io(&self, frames: usize) {
258        let _ = self.process_with_midi(frames, &[]);
259    }
260
261    pub fn process_with_midi(&self, frames: usize, _midi_in: &[MidiEvent]) -> Vec<MidiEvent> {
262        if self.bypassed.load(Ordering::Relaxed) {
263            ipc::bypass_copy_inputs_to_outputs(&self.audio_inputs, &self.audio_outputs);
264            return Vec::new();
265        }
266
267        // Check if host process has crashed.
268        {
269            let child = self.child.lock();
270            if let Some(ref mut c) = child.as_mut() {
271                match c.try_wait() {
272                    Ok(Some(status)) if !status.success() => {
273                        tracing::error!(
274                            "LV2 plugin host crashed for '{}' ({})",
275                            self.name,
276                            self.uri
277                        );
278                        self.crash_count.fetch_add(1, Ordering::Relaxed);
279                        ipc::bypass_copy_inputs_to_outputs(&self.audio_inputs, &self.audio_outputs);
280                        return Vec::new();
281                    }
282                    Ok(Some(status)) => {
283                        eprintln!("[LV2 debug] host exited with success: {:?}", status);
284                    }
285                    Ok(None) => {
286                        eprintln!("[LV2 debug] host still alive");
287                    }
288                    Err(e) => {
289                        eprintln!("[LV2 debug] try_wait error: {}", e);
290                    }
291                }
292            }
293        }
294
295        let started = Instant::now();
296
297        let (mapping, events) = match (&self.mapping, &self.events) {
298            (Some(m), Some(e)) => (m, e),
299            _ => {
300                ipc::bypass_copy_inputs_to_outputs(&self.audio_inputs, &self.audio_outputs);
301                return Vec::new();
302            }
303        };
304
305        let ptr = mapping.as_ptr();
306        let num_in = self.audio_inputs.len();
307        let num_out = self.audio_outputs.len();
308        unsafe {
309            ipc::configure_shm_header(ptr, frames, num_in, num_out);
310            // Write default transport state (can be overridden by track later).
311            let t = transport_mut(ptr);
312            t.playhead_sample = 0;
313            t.tempo = 120.0;
314            t.numerator = 4;
315            t.denominator = 4;
316            t.flags = 1; // playing
317
318            // Copy input AudioIO buffers to shared memory (bus 0).
319            ipc::copy_inputs_to_shm(&self.audio_inputs, ptr, frames);
320        }
321
322        // Signal host to process.
323        if let Err(e) = events.signal_host() {
324            tracing::error!("Failed to signal LV2 host: {e}");
325            ipc::bypass_copy_inputs_to_outputs(&self.audio_inputs, &self.audio_outputs);
326            return Vec::new();
327        }
328        eprintln!("[LV2 debug] signal_host succeeded");
329
330        // Wait for host to complete (with timeout).
331        let timeout = Duration::from_millis(100);
332        match events.wait_host(timeout) {
333            Ok(()) => {
334                eprintln!("[LV2 debug] wait_host succeeded");
335            }
336            Err(e) => {
337                eprintln!(
338                    "[LV2 debug] host did not respond for '{}' ({}): {}",
339                    self.name, self.uri, e
340                );
341                ipc::bypass_copy_inputs_to_outputs(&self.audio_inputs, &self.audio_outputs);
342                return Vec::new();
343            }
344        }
345
346        // Copy output shared memory (bus 1) back to AudioIO buffers.
347        unsafe {
348            ipc::copy_outputs_from_shm(&self.audio_outputs, ptr, frames);
349        }
350
351        let elapsed = started.elapsed();
352        if elapsed > Duration::from_millis(20) {
353            tracing::warn!(
354                "Slow LV2 process '{}' ({}) took {:.3} ms for {} frames",
355                self.name,
356                self.uri,
357                elapsed.as_secs_f64() * 1000.0,
358                frames
359            );
360        }
361
362        *self.last_process_time.lock() = Instant::now();
363        Vec::new()
364    }
365
366    pub fn uri(&self) -> &str {
367        &self.uri
368    }
369
370    pub fn name(&self) -> &str {
371        &self.name
372    }
373
374    pub fn begin_parameter_edit_at(&self, _param_id: u32, _frame: u32) -> Result<(), String> {
375        Ok(())
376    }
377
378    pub fn end_parameter_edit_at(&self, _param_id: u32, _frame: u32) -> Result<(), String> {
379        Ok(())
380    }
381
382    pub fn run_host_callbacks_main_thread(&self) {}
383
384    pub fn reconfigure_ports_if_needed(&self) -> Result<bool, String> {
385        Ok(false)
386    }
387
388    pub fn gui_set_parent_x11(&self, window: usize) -> Result<(), String> {
389        if let Some(ref mapping) = self.mapping {
390            let header = unsafe { header_mut(mapping.as_ptr()) };
391            header.set_parent_window(window);
392            return Ok(());
393        }
394        Err("No active host to set parent window".to_string())
395    }
396
397    pub fn gui_show(&self) -> Result<(), String> {
398        if let Some(ref mapping) = self.mapping
399            && let Some(ref events) = self.events
400        {
401            let header = unsafe { header_mut(mapping.as_ptr()) };
402            header.request_type.store(3, Ordering::Release);
403            let _ = events.signal_host();
404            return Ok(());
405        }
406        Err("No active host to show GUI".to_string())
407    }
408
409    pub fn gui_hide(&self) {
410        if let Some(ref mapping) = self.mapping
411            && let Some(ref events) = self.events
412        {
413            let header = unsafe { header_mut(mapping.as_ptr()) };
414            header.request_type.store(4, Ordering::Release);
415            let _ = events.signal_host();
416        }
417    }
418
419    pub fn drain_echoed_parameters(&self) -> Vec<ParameterEvent> {
420        let mut result = Vec::new();
421        if let Some(ref mapping) = self.mapping {
422            let ring = unsafe {
423                let buf = echo_ring_ptr(mapping.as_ptr());
424                let (w, r) = echo_indices(mapping.as_ptr());
425                RingBuffer::new(buf, w, r, RING_CAPACITY)
426            };
427            while let Some(ev) = ring.pop() {
428                result.push(ev);
429            }
430        }
431        result
432    }
433
434    pub fn drain_midi_outputs(&self) -> Vec<crate::midi::io::MidiEvent> {
435        let mut result = Vec::new();
436        if let Some(ref mapping) = self.mapping {
437            let ring = unsafe {
438                let buf = midi_out_ring_ptr(mapping.as_ptr());
439                let (w, r) = midi_out_indices(mapping.as_ptr());
440                RingBuffer::new(buf, w, r, RING_CAPACITY)
441            };
442            while let Some(ev) = ring.pop() {
443                result.push(crate::midi::io::MidiEvent {
444                    frame: ev.sample_offset,
445                    data: ev.data.to_vec(),
446                });
447            }
448        }
449        result
450    }
451}
452
453impl Drop for Lv2Processor {
454    fn drop(&mut self) {
455        ipc::drop_host(&self.mapping, &self.events, &self.child, &self.shm_name);
456    }
457}
458
459crate::impl_ipc_processor_wrapper!(Lv2Processor);
460
461impl UnsafeMutex<Lv2Processor> {
462    pub fn process_with_midi(&self, frames: usize, midi_events: &[MidiEvent]) -> Vec<MidiEvent> {
463        self.lock().process_with_midi(frames, midi_events)
464    }
465
466    pub fn snapshot_state(&self) -> Result<Vec<u8>, String> {
467        self.lock().snapshot_state()
468    }
469
470    pub fn restore_state(&self, state: &[u8]) -> Result<(), String> {
471        self.lock().restore_state(state)
472    }
473
474    pub fn drain_echoed_parameters(&self) -> Vec<ParameterEvent> {
475        self.lock().drain_echoed_parameters()
476    }
477
478    pub fn drain_midi_outputs(&self) -> Vec<crate::midi::io::MidiEvent> {
479        self.lock().drain_midi_outputs()
480    }
481}
482
483#[cfg(test)]
484mod tests {
485    use super::*;
486
487    fn find_host_binary() -> PathBuf {
488        ipc::find_plugin_host_binary().expect("maolan-plugin-host binary should be built for tests")
489    }
490
491    #[test]
492    fn find_host_binary_locates_binary() {
493        let host_bin = find_host_binary();
494        assert!(
495            host_bin.exists(),
496            "plugin-host binary should exist at {}",
497            host_bin.display()
498        );
499    }
500
501    #[test]
502    fn lv2_processor_crash_bypass() {
503        let host_bin = find_host_binary();
504
505        let processor = Lv2Processor::new(48000.0, 256, "__crash__", 1, 1, host_bin)
506            .expect("should create LV2 processor for crash test");
507
508        processor.setup_audio_ports();
509
510        // Fill input buffer.
511        {
512            let buf = processor.audio_inputs()[0].buffer.lock();
513            buf.fill(1.0);
514            *processor.audio_inputs()[0].finished.lock() = true;
515        }
516
517        // First process should trigger the crash; subsequent calls should bypass.
518        processor.process_with_audio_io(256);
519
520        // After crash, output should be a copy of input (bypass).
521        let out_buf = processor.audio_outputs()[0].buffer.lock();
522        let first_few: Vec<f32> = out_buf.iter().take(10).copied().collect();
523        assert!(
524            out_buf.iter().all(|&s| s == 1.0),
525            "after crash, output should be bypass copy of input, got: {:?}",
526            first_few
527        );
528    }
529}