Skip to main content

maolan_engine/plugins/
lv2_proc.rs

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