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