Skip to main content

maolan_engine/plugins/
vst3_proc.rs

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