Skip to main content

maolan_engine/plugins/
vst3_proc.rs

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