Skip to main content

maolan_engine/plugins/
lv2_proc.rs

1use crate::audio::io::AudioIO;
2use crate::midi::io::{MIDIIO, MidiEvent};
3use crate::plugins::ipc;
4use arc_swap::ArcSwapOption;
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::cell::UnsafeCell;
10use std::collections::HashMap;
11use std::path::PathBuf;
12use std::process::{Child, ChildStderr};
13use std::sync::Arc;
14use std::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, Ordering};
15use std::time::{Duration, Instant};
16
17fn wait_for_host_request_complete(
18    header: &ShmHeader,
19    events: &EventPair,
20    timeout: Duration,
21) -> Result<(), String> {
22    let start = Instant::now();
23    loop {
24        if header.request_status.load(Ordering::Acquire) != 0 {
25            return Ok(());
26        }
27        let elapsed = start.elapsed();
28        if elapsed >= timeout {
29            return Err("Host did not respond to request".to_string());
30        }
31        match events.wait_host(timeout - elapsed) {
32            Ok(()) => {
33                std::sync::atomic::fence(Ordering::Acquire);
34                continue;
35            }
36            Err(e) if e.kind() == std::io::ErrorKind::TimedOut => continue,
37            Err(e) => return Err(format!("Host did not respond to request: {e}")),
38        }
39    }
40}
41
42pub struct Lv2Processor {
43    uri: String,
44    name: String,
45    audio_inputs: Vec<Arc<AudioIO>>,
46    audio_outputs: Vec<Arc<AudioIO>>,
47    main_audio_inputs: usize,
48    main_audio_outputs: usize,
49    midi_input_ports: Vec<Arc<MIDIIO>>,
50    midi_output_ports: Vec<Arc<MIDIIO>>,
51    /// Current value of every known parameter, keyed by parameter id and
52    /// stored as `f64` bits. Only ever touched through atomic loads/stores.
53    param_values: HashMap<u32, AtomicU64>,
54    bypassed: Arc<AtomicBool>,
55
56    /// Host child process handle. Interior-mutable so `process_with_audio_buffers`
57    /// (which takes `&self`) can poll it with `try_wait`.
58    ///
59    /// Invariant: at any moment there is at most one accessor — either the
60    /// audio thread running this plugin's own plan task node (exactly one per
61    /// cycle), or control code running after the last `Arc` reference to this
62    /// processor is gone (`Drop`).
63    child: UnsafeCell<Option<Child>>,
64    /// Host stderr pipe; control-side only (`take_stderr`). RCU-published so
65    /// no blocking primitive is involved.
66    stderr: ArcSwapOption<ChildStderr>,
67    mapping: Option<ShmMapping>,
68    events: Option<EventPair>,
69    shm_name: String,
70
71    crash_count: AtomicU32,
72}
73
74// Safety: see the invariants on `child` and `stderr` above. Every other field
75// is either immutable after construction or synchronized on its own
76// (atomics / RCU).
77unsafe impl Sync for Lv2Processor {}
78
79pub type SharedLv2Processor = Arc<Lv2Processor>;
80
81impl Lv2Processor {
82    pub fn new(
83        sample_rate: f64,
84        buffer_size: usize,
85        plugin_uri: &str,
86        input_count: usize,
87        output_count: usize,
88        host_binary: PathBuf,
89    ) -> Result<Self, String> {
90        let audio_inputs = (0..input_count.max(1))
91            .map(|_| Arc::new(AudioIO::new(buffer_size)))
92            .collect::<Vec<_>>();
93        let audio_outputs = (0..output_count.max(1))
94            .map(|_| Arc::new(AudioIO::new(buffer_size)))
95            .collect::<Vec<_>>();
96
97        let instance_id = ipc::unique_instance_id("lv2");
98        let sample_rate_str = sample_rate.to_string();
99        let buffer_size_str = buffer_size.to_string();
100        let num_inputs_str = input_count.max(1).to_string();
101        let num_outputs_str = output_count.max(1).to_string();
102        let (mut child, mapping, events, shm_name, stderr) = ipc::spawn_host(ipc::HostSpawnArgs {
103            host_binary: &host_binary,
104            format: "lv2",
105            plugin_spec: plugin_uri,
106            instance_id: &instance_id,
107            extra_args: &[
108                &sample_rate_str,
109                &buffer_size_str,
110                &num_inputs_str,
111                &num_outputs_str,
112            ],
113        })?;
114
115        let header = unsafe { header_ref(mapping.as_ptr()) };
116        if !ipc::wait_for_ready(header, Duration::from_secs(10)) {
117            let _ = child.kill();
118            return Err("LV2 host did not signal ready".to_string());
119        }
120
121        let name = unsafe {
122            maolan_plugin_protocol::protocol::read_plugin_name_from_scratch(mapping.as_ptr())
123                .unwrap_or_else(|| {
124                    plugin_uri
125                        .rsplit_once('/')
126                        .map(|(_, name)| name)
127                        .unwrap_or(plugin_uri)
128                        .to_string()
129                })
130        };
131
132        let header = unsafe { header_ref(mapping.as_ptr()) };
133        let midi_in_count = header.midi_in_port_count.load(Ordering::Acquire) as usize;
134        let midi_out_count = header.midi_out_port_count.load(Ordering::Acquire) as usize;
135        let midi_input_ports: Vec<_> = (0..midi_in_count)
136            .map(|_| Arc::new(MIDIIO::new()))
137            .collect();
138        let midi_output_ports: Vec<_> = (0..midi_out_count)
139            .map(|_| Arc::new(MIDIIO::new()))
140            .collect();
141
142        let param_values: HashMap<u32, AtomicU64> = HashMap::new();
143
144        Ok(Self {
145            uri: plugin_uri.to_string(),
146            name,
147            audio_inputs,
148            audio_outputs,
149            main_audio_inputs: input_count.max(1),
150            main_audio_outputs: output_count.max(1),
151            midi_input_ports,
152            midi_output_ports,
153            param_values,
154            bypassed: Arc::new(AtomicBool::new(false)),
155            child: UnsafeCell::new(Some(child)),
156            stderr: ArcSwapOption::from_pointee(stderr),
157            mapping: Some(mapping),
158            events: Some(events),
159            shm_name,
160            crash_count: AtomicU32::new(0),
161        })
162    }
163
164    /// Access the host child process handle.
165    ///
166    /// # Safety
167    /// The caller must be the sole accessor of `child` at this time: either
168    /// the audio thread running this plugin's own plan task node (exactly one
169    /// per cycle), or control code running after the last `Arc` reference to
170    /// this processor is gone.
171    unsafe fn with_child<R>(&self, f: impl FnOnce(&mut Option<Child>) -> R) -> R {
172        f(unsafe { &mut *self.child.get() })
173    }
174
175    pub fn setup_audio_ports(&self) {
176        for port in &self.audio_inputs {
177            port.setup();
178        }
179        for port in &self.audio_outputs {
180            port.setup();
181        }
182    }
183
184    pub fn setup_midi_ports(&self) {
185        for port in &self.midi_input_ports {
186            // Safety: plan single-writer invariant — this task is the sole
187            // writer of its own ports this cycle; sources it reads were
188            // produced by earlier plan nodes (LOCKLESS.md Phase 3).
189            unsafe { port.setup() };
190        }
191        for port in &self.midi_output_ports {
192            // Safety: as above — sole writer of this port this cycle.
193            unsafe { port.setup() };
194        }
195    }
196
197    pub fn audio_inputs(&self) -> &[Arc<AudioIO>] {
198        &self.audio_inputs
199    }
200
201    pub fn audio_outputs(&self) -> &[Arc<AudioIO>] {
202        &self.audio_outputs
203    }
204
205    pub fn main_audio_input_count(&self) -> usize {
206        self.main_audio_inputs
207    }
208
209    pub fn main_audio_output_count(&self) -> usize {
210        self.main_audio_outputs
211    }
212
213    pub fn midi_input_count(&self) -> usize {
214        self.midi_input_ports.len()
215    }
216
217    pub fn midi_output_count(&self) -> usize {
218        self.midi_output_ports.len()
219    }
220
221    pub fn midi_input_ports(&self) -> &[Arc<MIDIIO>] {
222        &self.midi_input_ports
223    }
224
225    pub fn midi_output_ports(&self) -> &[Arc<MIDIIO>] {
226        &self.midi_output_ports
227    }
228
229    pub fn set_bypassed(&self, bypassed: bool) {
230        self.bypassed.store(bypassed, Ordering::Relaxed);
231    }
232
233    pub fn is_bypassed(&self) -> bool {
234        self.bypassed.load(Ordering::Relaxed)
235    }
236
237    pub fn parameter_values(&self) -> HashMap<u32, f64> {
238        self.param_values
239            .iter()
240            .map(|(&id, value)| (id, f64::from_bits(value.load(Ordering::Relaxed))))
241            .collect()
242    }
243
244    pub fn set_parameter(&self, param_id: u32, value: f64) -> Result<(), String> {
245        self.set_parameter_at(param_id, value, 0)
246    }
247
248    pub fn set_parameter_at(&self, param_id: u32, value: f64, _frame: u32) -> Result<(), String> {
249        if let Some(slot) = self.param_values.get(&param_id) {
250            slot.store(value.to_bits(), Ordering::Relaxed);
251        } else {
252            tracing::warn!("LV2 set_parameter_at: unknown parameter id {param_id}");
253        }
254
255        if let Some(ref mapping) = self.mapping {
256            let ring = unsafe {
257                let buf = param_ring_ptr(mapping.as_ptr());
258                let (w, r) = param_indices(mapping.as_ptr());
259                RingBuffer::new(buf, w, r, RING_CAPACITY)
260            };
261            let ev = ParameterEvent {
262                param_index: param_id,
263                value: value as f32,
264                sample_offset: 0,
265                event_kind: maolan_plugin_protocol::PARAM_EVENT_VALUE,
266            };
267            if !ring.push(ev) {}
268        }
269        Ok(())
270    }
271
272    pub fn begin_parameter_edit(&self, _param_id: u32) -> Result<(), String> {
273        Ok(())
274    }
275
276    pub fn end_parameter_edit(&self, _param_id: u32) -> Result<(), String> {
277        Ok(())
278    }
279
280    pub fn is_parameter_edit_active(&self, _param_id: u32) -> bool {
281        false
282    }
283
284    pub fn snapshot_state(&self) -> Result<Vec<u8>, String> {
285        let (mapping, events) = match (&self.mapping, &self.events) {
286            (Some(m), Some(e)) => (m, e),
287            _ => return Err("LV2 processor not initialized".to_string()),
288        };
289        let ptr = mapping.as_ptr();
290        let header = unsafe { header_mut(ptr) };
291
292        tracing::info!("LV2 snapshot_state: sending request to host");
293        header.request_type.store(1, Ordering::Release);
294        header.request_status.store(0, Ordering::Release);
295        if let Err(e) = events.signal_host() {
296            header.request_type.store(0, Ordering::Release);
297            return Err(format!("Failed to signal host for state save: {}", e));
298        }
299
300        if let Err(e) = wait_for_host_request_complete(header, events, Duration::from_secs(5)) {
301            header.request_type.store(0, Ordering::Release);
302            return Err(format!("Host did not respond to state save: {}", e));
303        }
304
305        let status = header.request_status.load(Ordering::Acquire);
306        let size = header.scratch_size.load(Ordering::Acquire) as usize;
307        tracing::info!(status, size, "LV2 snapshot_state: host responded");
308        if status != 1 {
309            header.request_type.store(0, Ordering::Release);
310            return Err("State save failed in host".to_string());
311        }
312
313        let scratch = unsafe { scratch_ptr(ptr) };
314        let mut bytes = vec![0u8; size];
315        unsafe {
316            std::ptr::copy_nonoverlapping(scratch, bytes.as_mut_ptr(), size);
317        }
318        header.request_type.store(0, Ordering::Release);
319        Ok(bytes)
320    }
321
322    pub fn restore_state(&self, state: &[u8]) -> Result<(), String> {
323        let (mapping, events) = match (&self.mapping, &self.events) {
324            (Some(m), Some(e)) => (m, e),
325            _ => return Err("LV2 processor not initialized".to_string()),
326        };
327        let ptr = mapping.as_ptr();
328        let header = unsafe { header_mut(ptr) };
329
330        let scratch = unsafe { scratch_ptr(ptr) };
331        let size = state.len().min(SCRATCH_SIZE);
332        unsafe {
333            std::ptr::copy_nonoverlapping(state.as_ptr(), scratch, size);
334        }
335        header.scratch_size.store(size as u32, Ordering::Release);
336
337        header.request_type.store(2, Ordering::Release);
338        header.request_status.store(0, Ordering::Release);
339        if let Err(e) = events.signal_host() {
340            header.request_type.store(0, Ordering::Release);
341            return Err(format!("Failed to signal host for state restore: {}", e));
342        }
343
344        if let Err(e) = wait_for_host_request_complete(header, events, Duration::from_secs(5)) {
345            header.request_type.store(0, Ordering::Release);
346            return Err(format!("Host did not respond to state restore: {}", e));
347        }
348
349        let status = header.request_status.load(Ordering::Acquire);
350        header.request_type.store(0, Ordering::Release);
351        if status != 1 {
352            return Err("State restore failed in host".to_string());
353        }
354        Ok(())
355    }
356
357    fn deserialize_lv2_control_ports(
358        scratch: *const u8,
359        size: usize,
360    ) -> Result<Vec<crate::message::Lv2ControlPortInfo>, String> {
361        if size < 4 {
362            return Err("scratch too small for LV2 control ports".to_string());
363        }
364        let mut offset = 0usize;
365
366        let count = unsafe { std::ptr::read_unaligned(scratch.add(offset) as *const u32) } as usize;
367        offset += 4;
368
369        let mut ports = Vec::with_capacity(count);
370        for _ in 0..count {
371            if offset + 4 > size {
372                return Err("scratch underflow".to_string());
373            }
374            let index = unsafe { std::ptr::read_unaligned(scratch.add(offset) as *const u32) };
375            offset += 4;
376
377            if offset + 4 > size {
378                return Err("scratch underflow".to_string());
379            }
380            let name_len =
381                unsafe { std::ptr::read_unaligned(scratch.add(offset) as *const u32) } as usize;
382            offset += 4;
383            if offset + name_len > size {
384                return Err("scratch underflow".to_string());
385            }
386            let mut name_bytes = vec![0u8; name_len];
387            unsafe {
388                std::ptr::copy_nonoverlapping(
389                    scratch.add(offset),
390                    name_bytes.as_mut_ptr(),
391                    name_len,
392                );
393            }
394            offset += name_len;
395            let name = String::from_utf8(name_bytes).map_err(|e| e.to_string())?;
396
397            if offset + 12 > size {
398                return Err("scratch underflow".to_string());
399            }
400            let min = f32::from_bits(unsafe {
401                std::ptr::read_unaligned(scratch.add(offset) as *const u32)
402            });
403            let max = f32::from_bits(unsafe {
404                std::ptr::read_unaligned(scratch.add(offset + 4) as *const u32)
405            });
406            let value = f32::from_bits(unsafe {
407                std::ptr::read_unaligned(scratch.add(offset + 8) as *const u32)
408            });
409            offset += 12;
410
411            ports.push(crate::message::Lv2ControlPortInfo {
412                index,
413                name,
414                min,
415                max,
416                value,
417            });
418        }
419
420        Ok(ports)
421    }
422
423    fn deserialize_lv2_note_names(
424        scratch: *const u8,
425        size: usize,
426    ) -> Result<HashMap<u8, String>, String> {
427        if size < 4 {
428            return Err("scratch too small for LV2 note names".to_string());
429        }
430        let mut offset = 0usize;
431        let count = unsafe { std::ptr::read_unaligned(scratch.add(offset) as *const u32) } as usize;
432        offset += 4;
433
434        let mut note_names = HashMap::with_capacity(count);
435        for _ in 0..count {
436            if offset + 4 > size {
437                return Err("scratch underflow".to_string());
438            }
439            let note = unsafe { std::ptr::read_unaligned(scratch.add(offset) as *const u32) } as u8;
440            offset += 4;
441
442            if offset + 4 > size {
443                return Err("scratch underflow".to_string());
444            }
445            let name_len =
446                unsafe { std::ptr::read_unaligned(scratch.add(offset) as *const u32) } as usize;
447            offset += 4;
448            if offset + name_len > size {
449                return Err("scratch underflow".to_string());
450            }
451            let mut name_bytes = vec![0u8; name_len];
452            unsafe {
453                std::ptr::copy_nonoverlapping(
454                    scratch.add(offset),
455                    name_bytes.as_mut_ptr(),
456                    name_len,
457                );
458            }
459            offset += name_len;
460            let name = String::from_utf8(name_bytes).map_err(|e| e.to_string())?;
461            note_names.insert(note, name);
462        }
463
464        Ok(note_names)
465    }
466
467    pub fn control_ports(&self) -> Result<Vec<crate::message::Lv2ControlPortInfo>, String> {
468        let (mapping, events) = match (&self.mapping, &self.events) {
469            (Some(m), Some(e)) => (m, e),
470            _ => return Err("LV2 processor not initialized".to_string()),
471        };
472        let ptr = mapping.as_ptr();
473        let header = unsafe { header_mut(ptr) };
474
475        tracing::info!("LV2 control_ports: sending request to host");
476        header.request_type.store(
477            maolan_plugin_protocol::protocol::REQUEST_LV2_CONTROL_PORTS,
478            Ordering::Release,
479        );
480        header.request_status.store(0, Ordering::Release);
481        if let Err(e) = events.signal_host() {
482            header.request_type.store(0, Ordering::Release);
483            return Err(format!("Failed to signal host for LV2 control ports: {e}"));
484        }
485
486        if let Err(e) = wait_for_host_request_complete(header, events, Duration::from_secs(5)) {
487            header.request_type.store(0, Ordering::Release);
488            return Err(format!("Host did not respond to LV2 control ports: {e}"));
489        }
490
491        let status = header.request_status.load(Ordering::Acquire);
492        let size = header.scratch_size.load(Ordering::Acquire) as usize;
493        tracing::info!(status, size, "LV2 control_ports: host responded");
494        if status != 1 {
495            header.request_type.store(0, Ordering::Release);
496            return Err("LV2 control port enumeration failed in host".to_string());
497        }
498
499        let scratch = unsafe { scratch_ptr(ptr) };
500        let result = Self::deserialize_lv2_control_ports(scratch, size);
501        match &result {
502            Ok(ports) => tracing::info!(count = ports.len(), "LV2 control_ports: deserialized"),
503            Err(e) => tracing::error!("LV2 control_ports: deserialize failed: {e}"),
504        }
505        header.request_type.store(0, Ordering::Release);
506        result
507    }
508
509    pub fn note_names(&self) -> Result<HashMap<u8, String>, String> {
510        let (mapping, events) = match (&self.mapping, &self.events) {
511            (Some(m), Some(e)) => (m, e),
512            _ => return Err("LV2 processor not initialized".to_string()),
513        };
514        let ptr = mapping.as_ptr();
515        let header = unsafe { header_mut(ptr) };
516
517        header.request_type.store(
518            maolan_plugin_protocol::protocol::REQUEST_LV2_MIDNAM,
519            Ordering::Release,
520        );
521        header.request_status.store(0, Ordering::Release);
522        if let Err(e) = events.signal_host() {
523            header.request_type.store(0, Ordering::Release);
524            return Err(format!("Failed to signal host for LV2 midnam: {e}"));
525        }
526
527        if let Err(e) = wait_for_host_request_complete(header, events, Duration::from_secs(5)) {
528            header.request_type.store(0, Ordering::Release);
529            return Err(format!("Host did not respond to LV2 midnam: {e}"));
530        }
531
532        let status = header.request_status.load(Ordering::Acquire);
533        let size = header.scratch_size.load(Ordering::Acquire) as usize;
534        if status != 1 {
535            header.request_type.store(0, Ordering::Release);
536            return Err("LV2 midnam enumeration failed in host".to_string());
537        }
538
539        let scratch = unsafe { scratch_ptr(ptr) };
540        let result = Self::deserialize_lv2_note_names(scratch, size);
541        header.request_type.store(0, Ordering::Release);
542        result
543    }
544
545    pub fn set_resource_directory(&self, dir: &std::path::Path) -> Result<(), String> {
546        let (mapping, events) = match (&self.mapping, &self.events) {
547            (Some(m), Some(e)) => (m, e),
548            _ => return Err("LV2 processor not initialized".to_string()),
549        };
550        let ptr = mapping.as_ptr();
551        let header = unsafe { header_mut(ptr) };
552        let path_str = dir.to_string_lossy().to_string();
553        unsafe {
554            write_resource_directory_to_scratch(ptr, &path_str)
555                .map_err(|e| format!("Failed to write resource directory: {e}"))?;
556        }
557        std::sync::atomic::fence(Ordering::SeqCst);
558
559        header.request_type.store(5, Ordering::Release);
560        header.request_status.store(0, Ordering::Release);
561        if let Err(e) = events.signal_host() {
562            header.request_type.store(0, Ordering::Release);
563            return Err(format!("Failed to signal host for resource directory: {e}"));
564        }
565
566        if let Err(e) = wait_for_host_request_complete(header, events, Duration::from_secs(5)) {
567            header.request_type.store(0, Ordering::Release);
568            return Err(format!("Host did not respond to resource directory: {e}"));
569        }
570
571        let status = header.request_status.load(Ordering::Acquire);
572        header.request_type.store(0, Ordering::Release);
573        if status != 1 {
574            return Err("Resource directory update failed in host".to_string());
575        }
576        Ok(())
577    }
578
579    pub fn process_with_audio_buffers(
580        &self,
581        frames: usize,
582        audio_inputs: &[&[f32]],
583        audio_outputs: &mut [&mut [f32]],
584    ) -> Vec<MidiEvent> {
585        if self.bypassed.load(Ordering::Relaxed) {
586            ipc::bypass_copy_input_slices_to_outputs(audio_inputs, audio_outputs);
587            return Vec::new();
588        }
589
590        // Safety: the sole RT accessor of `child` is this processor's own
591        // plan task node, which runs exactly once per cycle.
592        let crashed = unsafe {
593            self.with_child(|child| {
594                if let Some(c) = child.as_mut()
595                    && let Ok(Some(status)) = c.try_wait()
596                    && !status.success()
597                {
598                    self.crash_count.fetch_add(1, Ordering::Relaxed);
599                    return true;
600                }
601                false
602            })
603        };
604        if crashed {
605            ipc::bypass_copy_input_slices_to_outputs(audio_inputs, audio_outputs);
606            return Vec::new();
607        }
608
609        let (mapping, events) = match (&self.mapping, &self.events) {
610            (Some(m), Some(e)) => (m, e),
611            _ => {
612                ipc::bypass_copy_input_slices_to_outputs(audio_inputs, audio_outputs);
613                return Vec::new();
614            }
615        };
616
617        let ptr = mapping.as_ptr();
618        let num_in = audio_inputs.len();
619        let num_out = audio_outputs.len();
620        let midi_in_count = self.midi_input_ports.len();
621        let midi_out_count = self.midi_output_ports.len();
622        unsafe {
623            ipc::configure_shm_header(ptr, frames, num_in, num_out, midi_in_count, midi_out_count);
624
625            let t = transport_mut(ptr);
626            t.playhead_sample = 0;
627            t.tempo = 120.0;
628            t.numerator = 4;
629            t.denominator = 4;
630            t.flags = 1;
631
632            ipc::copy_input_slices_to_shm(audio_inputs, ptr, frames);
633
634            for (port_idx, port) in self.midi_input_ports.iter().enumerate() {
635                let buf = midi_in_ring_ptr(ptr, port_idx);
636                let (w, r) = midi_in_indices(ptr, port_idx);
637                let ring = RingBuffer::new(buf, w, r, RING_CAPACITY);
638                // Safety: plan single-writer invariant — this task is the sole
639                // writer of its own ports this cycle; this read is of the
640                // port's own buffer, which no other node touches now
641                // (LOCKLESS.md Phase 3).
642                let port_buffer = port.buffer();
643                for ev in port_buffer {
644                    let data = {
645                        let mut d = [0u8; 3];
646                        for (i, b) in ev.data.iter().enumerate().take(3) {
647                            d[i] = *b;
648                        }
649                        d
650                    };
651                    let _ = ring.push(maolan_plugin_protocol::MidiEvent {
652                        sample_offset: ev.frame,
653                        data,
654                        channel: ev.data.first().copied().unwrap_or(0) & 0x0F,
655                        flags: 0,
656                        _pad: 0,
657                    });
658                }
659                port.mark_finished();
660            }
661        }
662
663        if events.signal_host().is_err() {
664            ipc::bypass_copy_input_slices_to_outputs(audio_inputs, audio_outputs);
665            return Vec::new();
666        }
667
668        let timeout = Duration::from_millis(100);
669        match events.wait_host(timeout) {
670            Ok(()) => {}
671            Err(_) => {
672                ipc::bypass_copy_input_slices_to_outputs(audio_inputs, audio_outputs);
673                return Vec::new();
674            }
675        }
676
677        unsafe {
678            ipc::copy_outputs_from_shm_to_slices(audio_outputs, ptr, frames);
679
680            let mut output_events = Vec::new();
681            for (port_idx, port) in self.midi_output_ports.iter().enumerate() {
682                let buf = midi_out_ring_ptr(ptr, port_idx);
683                let (w, r) = midi_out_indices(ptr, port_idx);
684                let ring = RingBuffer::new(buf, w, r, RING_CAPACITY);
685                // Safety: plan single-writer invariant — this task is the sole
686                // writer of its own ports this cycle (LOCKLESS.md Phase 3).
687                let mut port_buffer = port.buffer_mut();
688                port_buffer.clear();
689                while let Some(ev) = ring.pop() {
690                    let event = MidiEvent {
691                        frame: ev.sample_offset,
692                        data: ev.data.to_vec(),
693                    };
694                    port_buffer.push(event.clone());
695                    output_events.push(event);
696                }
697                port.mark_finished();
698            }
699            output_events
700        }
701    }
702
703    pub fn uri(&self) -> &str {
704        &self.uri
705    }
706
707    pub fn name(&self) -> &str {
708        &self.name
709    }
710
711    pub fn take_stderr(&self) -> Option<ChildStderr> {
712        // Control-side only: the EC is the sole accessor, so after `swap`
713        // the `Arc` is unique and `try_unwrap` cannot fail in practice.
714        self.stderr.swap(None).and_then(|s| Arc::try_unwrap(s).ok())
715    }
716
717    pub fn begin_parameter_edit_at(&self, _param_id: u32, _frame: u32) -> Result<(), String> {
718        Ok(())
719    }
720
721    pub fn end_parameter_edit_at(&self, _param_id: u32, _frame: u32) -> Result<(), String> {
722        Ok(())
723    }
724
725    pub fn run_host_callbacks_main_thread(&self) {}
726
727    pub fn reconfigure_ports_if_needed(&self) -> Result<bool, String> {
728        Ok(false)
729    }
730
731    pub fn gui_set_parent_x11(&self, window: usize) -> Result<(), String> {
732        if let Some(ref mapping) = self.mapping {
733            let header = unsafe { header_mut(mapping.as_ptr()) };
734            header.set_parent_window(window);
735            return Ok(());
736        }
737        Err("No active host to set parent window".to_string())
738    }
739
740    pub fn gui_set_floating_mode(&self, floating: bool) -> Result<(), String> {
741        if let Some(ref mapping) = self.mapping {
742            let header = unsafe { header_mut(mapping.as_ptr()) };
743            header.set_gui_mode(if floating {
744                GuiMode::Floating
745            } else {
746                GuiMode::Embedded
747            });
748            return Ok(());
749        }
750        Err("No active host to set GUI mode".to_string())
751    }
752
753    pub fn gui_show(&self) -> Result<(), String> {
754        if let Some(ref mapping) = self.mapping
755            && let Some(ref events) = self.events
756        {
757            let header = unsafe { header_mut(mapping.as_ptr()) };
758            header.request_type.store(3, Ordering::Release);
759            let _ = events.signal_host();
760            return Ok(());
761        }
762        Err("No active host to show GUI".to_string())
763    }
764
765    pub fn gui_hide(&self) {
766        if let Some(ref mapping) = self.mapping
767            && let Some(ref events) = self.events
768        {
769            let header = unsafe { header_mut(mapping.as_ptr()) };
770            header.request_type.store(4, Ordering::Release);
771            let _ = events.signal_host();
772        }
773    }
774
775    pub fn drain_echoed_parameters(&self) -> Vec<ParameterEvent> {
776        let mut result = Vec::new();
777        if let Some(ref mapping) = self.mapping {
778            let ring = unsafe {
779                let buf = echo_ring_ptr(mapping.as_ptr());
780                let (w, r) = echo_indices(mapping.as_ptr());
781                RingBuffer::new(buf, w, r, RING_CAPACITY)
782            };
783            while let Some(ev) = ring.pop() {
784                result.push(ev);
785            }
786        }
787        result
788    }
789
790    pub fn drain_midi_outputs(&self) -> Vec<crate::midi::io::MidiEvent> {
791        let mut result = Vec::new();
792        if let Some(ref mapping) = self.mapping {
793            let ring = unsafe {
794                let buf = midi_out_ring_ptr(mapping.as_ptr(), 0);
795                let (w, r) = midi_out_indices(mapping.as_ptr(), 0);
796                RingBuffer::new(buf, w, r, RING_CAPACITY)
797            };
798            while let Some(ev) = ring.pop() {
799                result.push(crate::midi::io::MidiEvent {
800                    frame: ev.sample_offset,
801                    data: ev.data.to_vec(),
802                });
803            }
804        }
805        result
806    }
807}
808
809impl Drop for Lv2Processor {
810    fn drop(&mut self) {
811        let mapping = self.mapping.take();
812        let events = self.events.take();
813        let child = self.child.get_mut().take();
814        let shm_name = std::mem::take(&mut self.shm_name);
815        ipc::drop_host(mapping, events, child, shm_name);
816    }
817}
818
819#[cfg(test)]
820mod tests {
821    use super::*;
822
823    fn find_host_binary() -> PathBuf {
824        ipc::find_plugin_host_binary().expect("maolan-plugin-host binary should be built for tests")
825    }
826
827    #[cfg_attr(
828        all(miri, target_os = "freebsd"),
829        ignore = "plugin host discovery/runtime uses OS facilities not supported by Miri on FreeBSD"
830    )]
831    #[test]
832    fn find_host_binary_locates_binary() {
833        let host_bin = find_host_binary();
834        assert!(
835            host_bin.exists(),
836            "plugin-host binary should exist at {}",
837            host_bin.display()
838        );
839    }
840
841    #[cfg_attr(
842        all(miri, target_os = "freebsd"),
843        ignore = "plugin host discovery/runtime uses OS facilities not supported by Miri on FreeBSD"
844    )]
845    #[test]
846    fn lv2_processor_crash_bypass() {
847        let host_bin = find_host_binary();
848
849        let processor = Lv2Processor::new(48000.0, 256, "__crash__", 1, 1, host_bin)
850            .expect("should create LV2 processor for crash test");
851
852        processor.setup_audio_ports();
853
854        let input_buffers = [vec![1.0; 256]];
855        let mut output_buffers = [vec![0.0; 256]];
856        let inputs = input_buffers.iter().map(Vec::as_slice).collect::<Vec<_>>();
857        let mut outputs = output_buffers
858            .iter_mut()
859            .map(Vec::as_mut_slice)
860            .collect::<Vec<_>>();
861        processor.process_with_audio_buffers(256, &inputs, &mut outputs);
862
863        let out_buf = &output_buffers[0];
864        let first_few: Vec<f32> = out_buf.iter().take(10).copied().collect();
865        assert!(
866            out_buf.iter().all(|&s| s == 1.0),
867            "after crash, output should be bypass copy of input, got: {:?}",
868            first_few
869        );
870    }
871}