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