Skip to main content

maolan_plugin_host/
host.rs

1#[cfg(unix)]
2use crate::clap::{
3    CLAP_EXT_AUDIO_PORTS, ClapAudioBuffer, ClapEventHeader, ClapEventParamGesture,
4    ClapEventParamMod, ClapEventParamValue, ClapPluginParams, EventBuffer, PluginInstance,
5    host_timers,
6};
7#[cfg(unix)]
8use crate::clap::{
9    CLAP_EXT_PARAMS, CLAP_EXT_POSIX_FD_SUPPORT, CLAP_EXT_TIMER_SUPPORT, ClapProcess, EventCapture,
10    ThreadType, host_fds, set_thread_type,
11};
12use crate::events::EventPair;
13use crate::protocol::*;
14#[cfg(unix)]
15use crate::ringbuf::RingBuffer;
16use crate::shm::ShmMapping;
17#[cfg(unix)]
18use std::ptr;
19use std::sync::atomic::{AtomicBool, Ordering};
20use std::time::{Duration, Instant};
21
22/// Flush flag set by `host_params_request_flush`.
23static PARAMS_FLUSH_REQUESTED: AtomicBool = AtomicBool::new(false);
24
25pub fn request_params_flush() {
26    PARAMS_FLUSH_REQUESTED.store(true, Ordering::Release);
27}
28
29/// Rescan flag set by `host_audio_ports_rescan`.
30static AUDIO_PORTS_RESCAN_REQUESTED: AtomicBool = AtomicBool::new(false);
31
32pub fn request_audio_ports_rescan() {
33    AUDIO_PORTS_RESCAN_REQUESTED.store(true, Ordering::Release);
34}
35
36/// Audio port buffer configuration built from `clap.audio-ports`.
37#[cfg(unix)]
38struct PortBuffers {
39    inputs: Vec<ClapAudioBuffer>,
40    outputs: Vec<ClapAudioBuffer>,
41    _input_ptrs: Vec<Vec<*mut f32>>,
42    _output_ptrs: Vec<Vec<*mut f32>>,
43}
44
45#[cfg(unix)]
46impl PortBuffers {
47    /// Query the plugin's `clap.audio-ports` extension and build per-port buffers
48    /// that point into the SHM audio planes. Returns `None` if the extension is missing.
49    fn from_plugin(
50        plugin: *const crate::clap::ClapPlugin,
51        ptr: *mut u8,
52        num_in: usize,
53        num_out: usize,
54    ) -> Option<Self> {
55        let ext = unsafe {
56            (*plugin)
57                .get_extension
58                .map(|f| f(plugin, CLAP_EXT_AUDIO_PORTS.as_ptr()))
59        }?;
60        if ext.is_null() {
61            return None;
62        }
63        let ap = unsafe { &*(ext as *const crate::clap::ClapPluginAudioPorts) };
64        let in_count = ap.count.map(|f| unsafe { f(plugin, true) }).unwrap_or(0) as usize;
65        let out_count = ap.count.map(|f| unsafe { f(plugin, false) }).unwrap_or(0) as usize;
66
67        let mut inputs = Vec::with_capacity(in_count);
68        let mut input_ptrs = Vec::with_capacity(in_count);
69        let mut global_ch: usize = 0;
70        for i in 0..in_count {
71            let mut info = crate::clap::ClapAudioPortInfo {
72                id: 0,
73                name: [0; 256],
74                flags: 0,
75                channel_count: 1,
76                port_type: ptr::null(),
77                in_place_pair: 0,
78            };
79            let ch_count = if ap
80                .get
81                .map(|f| unsafe { f(plugin, i as u32, true, &mut info) })
82                .unwrap_or(false)
83            {
84                info.channel_count.max(1) as usize
85            } else {
86                1
87            };
88            let mut port_channels = Vec::with_capacity(ch_count);
89            for _ in 0..ch_count {
90                let shm_ptr = if global_ch < num_in {
91                    unsafe { audio_channel_ptr(ptr, global_ch, 0) }
92                } else {
93                    ptr::null_mut()
94                };
95                port_channels.push(shm_ptr);
96                global_ch += 1;
97            }
98            inputs.push(ClapAudioBuffer {
99                data32: port_channels.as_mut_ptr(),
100                data64: ptr::null_mut(),
101                channel_count: port_channels.len() as u32,
102                latency: 0,
103                constant_mask: 0,
104            });
105            input_ptrs.push(port_channels);
106        }
107
108        let mut outputs = Vec::with_capacity(out_count);
109        let mut output_ptrs = Vec::with_capacity(out_count);
110        global_ch = 0;
111        for i in 0..out_count {
112            let mut info = crate::clap::ClapAudioPortInfo {
113                id: 0,
114                name: [0; 256],
115                flags: 0,
116                channel_count: 1,
117                port_type: ptr::null(),
118                in_place_pair: 0,
119            };
120            let ch_count = if ap
121                .get
122                .map(|f| unsafe { f(plugin, i as u32, false, &mut info) })
123                .unwrap_or(false)
124            {
125                info.channel_count.max(1) as usize
126            } else {
127                1
128            };
129            let mut port_channels = Vec::with_capacity(ch_count);
130            for _ in 0..ch_count {
131                let shm_ptr = if global_ch < num_out {
132                    unsafe { audio_channel_ptr(ptr, global_ch, 1) }
133                } else {
134                    ptr::null_mut()
135                };
136                port_channels.push(shm_ptr);
137                global_ch += 1;
138            }
139            outputs.push(ClapAudioBuffer {
140                data32: port_channels.as_mut_ptr(),
141                data64: ptr::null_mut(),
142                channel_count: port_channels.len() as u32,
143                latency: 0,
144                constant_mask: 0,
145            });
146            output_ptrs.push(port_channels);
147        }
148
149        Some(Self {
150            inputs,
151            outputs,
152            _input_ptrs: input_ptrs,
153            _output_ptrs: output_ptrs,
154        })
155    }
156}
157
158/// Runtime state for the plugin-host process.
159pub struct HostRuntime {
160    pub mapping: ShmMapping,
161    pub events: EventPair,
162    pub format: String,
163    pub plugin_path: String,
164    pub instance_id: String,
165}
166
167impl HostRuntime {
168    /// Attach to an existing shared-memory segment and event pipes.
169    pub fn attach(
170        shm_name: &str,
171        events: EventPair,
172        format: String,
173        plugin_path: String,
174        instance_id: String,
175    ) -> Result<Self, String> {
176        let mapping = ShmMapping::open_existing(shm_name, SHM_SIZE)?;
177        Ok(Self {
178            mapping,
179            events,
180            format,
181            plugin_path,
182            instance_id,
183        })
184    }
185
186    /// Extract the plugin ID for CLAP factories.
187    /// `plugin_path` may be encoded as "path::plugin_id" or "path#plugin_id" by the caller.
188    #[cfg(unix)]
189    fn plugin_id(&self) -> &str {
190        if let Some(pos) = self.plugin_path.rfind("::") {
191            &self.plugin_path[pos + 2..]
192        } else if let Some(pos) = self.plugin_path.rfind('#') {
193            &self.plugin_path[pos + 1..]
194        } else {
195            ""
196        }
197    }
198
199    /// Extract the real file path (without the optional #plugin_id suffix).
200    #[cfg(unix)]
201    fn real_plugin_path(&self) -> &str {
202        if let Some(pos) = self.plugin_path.rfind("::") {
203            &self.plugin_path[..pos]
204        } else if let Some(pos) = self.plugin_path.rfind('#') {
205            &self.plugin_path[..pos]
206        } else {
207            &self.plugin_path
208        }
209    }
210
211    /// Signal readiness to the DAW.
212    pub fn signal_ready(&self) {
213        let header = unsafe { header_mut(self.mapping.as_ptr()) };
214        header.ready.store(1, Ordering::Release);
215        tracing::info!(instance_id = %self.instance_id, "Plugin host ready");
216    }
217
218    /// Write a test magic number into the scratch area.
219    pub fn write_test_magic(&self) {
220        let scratch = unsafe { scratch_ptr(self.mapping.as_ptr()) };
221        let magic: u32 = 0xDEADBEEF;
222        unsafe {
223            std::ptr::write_unaligned(scratch as *mut u32, magic);
224        }
225    }
226
227    /// Blocking wait until the DAW requests shutdown or a fatal signal arrives.
228    /// Uses the event pipe to sleep instead of burning CPU.
229    pub fn run_until_shutdown(&self) {
230        let header = unsafe { header_ref(self.mapping.as_ptr()) };
231        let start = Instant::now();
232        loop {
233            if header.shutdown_request.load(Ordering::Acquire) != 0 {
234                tracing::info!(instance_id = %self.instance_id, "Shutdown requested");
235                break;
236            }
237            if start.elapsed() >= Duration::from_millis(100) {
238                header.heartbeat.fetch_add(1, Ordering::Relaxed);
239            }
240            match self.events.wait_daw(Duration::from_millis(100)) {
241                Ok(()) => continue,
242                Err(e) if e.kind() == std::io::ErrorKind::TimedOut => continue,
243                Err(e) => {
244                    tracing::error!("Event pipe error: {e}");
245                    break;
246                }
247            }
248        }
249    }
250
251    /// Run the dummy null plugin: copy input audio channels to output channels.
252    /// Blocks on the event pipe for each block, then signals completion.
253    pub fn run_null_plugin(&self) {
254        let header = unsafe { header_ref(self.mapping.as_ptr()) };
255        let ptr = self.mapping.as_ptr();
256        tracing::info!(instance_id = %self.instance_id, "Null plugin running");
257
258        loop {
259            if header.shutdown_request.load(Ordering::Acquire) != 0 {
260                tracing::info!(instance_id = %self.instance_id, "Null plugin shutdown requested");
261                break;
262            }
263
264            // Wait for DAW to wake us with a new block.
265            match self.events.wait_daw(Duration::from_millis(100)) {
266                Ok(()) => {}
267                Err(e) if e.kind() == std::io::ErrorKind::TimedOut => continue,
268                Err(e) => {
269                    tracing::error!("Null plugin event error: {e}");
270                    break;
271                }
272            }
273
274            let block_size = header.block_size.load(Ordering::Acquire) as usize;
275            let num_in = header.num_input_channels.load(Ordering::Acquire) as usize;
276            let num_out = header.num_output_channels.load(Ordering::Acquire) as usize;
277
278            if block_size == 0 || block_size > MAX_BLOCK_SIZE {
279                tracing::warn!("Invalid block size {block_size}, skipping");
280                let _ = self.events.signal_daw();
281                continue;
282            }
283
284            // Copy each input channel to the corresponding output channel.
285            // Input uses bus 0 (main), output uses bus 1 (sidechain) so we don't overwrite input.
286            let max_ch = num_in.min(num_out).min(MAX_CHANNELS);
287            for ch in 0..max_ch {
288                let in_ptr = unsafe { audio_channel_ptr(ptr, ch, 0) };
289                let out_ptr = unsafe { audio_channel_ptr(ptr, ch, 1) };
290                unsafe {
291                    std::ptr::copy_nonoverlapping(in_ptr, out_ptr, block_size);
292                }
293            }
294
295            // Signal completion.
296            if let Err(e) = self.events.signal_daw() {
297                tracing::error!("Failed to signal DAW: {e}");
298                break;
299            }
300        }
301    }
302
303    /// Run a CLAP plugin inside the host process, marshalling audio via shared memory.
304    #[cfg(unix)]
305    pub fn run_clap_plugin(&self) {
306        let mut plugin = match PluginInstance::new(self.real_plugin_path(), self.plugin_id()) {
307            Ok(p) => p,
308            Err(e) => {
309                tracing::error!(
310                    "Failed to load CLAP plugin '{}': {e}",
311                    self.real_plugin_path()
312                );
313                return;
314            }
315        };
316
317        tracing::info!(
318            instance_id = %self.instance_id,
319            name = %plugin.name(),
320            "CLAP plugin loaded"
321        );
322
323        let ptr = self.mapping.as_ptr();
324        let header = unsafe { header_ref(self.mapping.as_ptr()) };
325
326        unsafe {
327            maolan_plugin_protocol::protocol::write_plugin_name_to_scratch(ptr, &plugin.name());
328        }
329
330        // Read sample rate from transport, fallback to 48 kHz for backward compat.
331        let sample_rate = unsafe {
332            let ts = transport_ref(ptr);
333            if ts.sample_rate_hz > 0.0 {
334                ts.sample_rate_hz
335            } else {
336                48000.0
337            }
338        };
339
340        if let Err(e) = plugin.activate(sample_rate, 1, MAX_BLOCK_SIZE as u32) {
341            tracing::error!("Failed to activate plugin: {e}");
342            return;
343        }
344
345        // Build per-port audio buffers from the plugin's audio-ports extension.
346        let mut port_buffers = PortBuffers::from_plugin(plugin.plugin_ptr(), ptr, 0, 0);
347
348        let has_note_ports = unsafe {
349            (*plugin.plugin_ptr())
350                .get_extension
351                .map(|f| {
352                    f(
353                        plugin.plugin_ptr(),
354                        crate::clap::CLAP_EXT_NOTE_PORTS.as_ptr(),
355                    )
356                })
357                .filter(|p| !p.is_null())
358                .is_some()
359        };
360        // Set up ring buffers.
361
362        let param_ring = unsafe {
363            let buf = param_ring_ptr(ptr);
364            let (w, r) = param_indices(ptr);
365            RingBuffer::new(buf, w, r, RING_CAPACITY)
366        };
367
368        let echo_ring = unsafe {
369            let buf = echo_ring_ptr(ptr);
370            let (w, r) = echo_indices(ptr);
371            RingBuffer::new(buf, w, r, RING_CAPACITY)
372        };
373
374        let midi_ring = unsafe {
375            let buf = midi_ring_ptr(ptr);
376            let (w, r) = midi_indices(ptr);
377            RingBuffer::new(buf, w, r, RING_CAPACITY)
378        };
379        // Cache plugin extension pointers for idle callbacks.
380
381        let params_ext = unsafe {
382            (*plugin.plugin_ptr())
383                .get_extension
384                .map(|f| f(plugin.plugin_ptr(), CLAP_EXT_PARAMS.as_ptr()))
385                .filter(|p| !p.is_null())
386                .map(|p| p as *const ClapPluginParams)
387        };
388
389        let timer_ext = unsafe {
390            (*plugin.plugin_ptr())
391                .get_extension
392                .map(|f| f(plugin.plugin_ptr(), CLAP_EXT_TIMER_SUPPORT.as_ptr()))
393                .filter(|p| !p.is_null())
394                .map(|p| p as *const crate::clap::ClapPluginTimerSupport)
395        };
396
397        let fd_ext = unsafe {
398            (*plugin.plugin_ptr())
399                .get_extension
400                .map(|f| f(plugin.plugin_ptr(), CLAP_EXT_POSIX_FD_SUPPORT.as_ptr()))
401                .filter(|p| !p.is_null())
402                .map(|p| p as *const crate::clap::ClapPluginPosixFdSupport)
403        };
404        let mut steady_time: i64 = 0;
405        let daw_read_fd = self.events.host_read_fd();
406        let mut started_processing = false;
407
408        loop {
409            if header.shutdown_request.load(Ordering::Acquire) != 0 {
410                tracing::info!(instance_id = %self.instance_id, "CLAP plugin shutdown requested");
411                break;
412            }
413
414            // Handle non-audio requests (GUI, state).
415            let req = header.request_type.load(Ordering::Acquire);
416            if req != 0 {
417                let result = match req {
418                    3 => {
419                        tracing::info!(instance_id = %self.instance_id, "GUI show requested");
420                        if !plugin.gui_is_supported() {
421                            Err("Plugin does not support GUI".to_string())
422                        } else {
423                            let window_id = header.parent_window_usize() as u64;
424                            let is_floating = window_id == 0;
425                            // Always recreate GUI if already created, to handle parent/floating changes.
426                            if plugin.gui_created() {
427                                plugin.gui_destroy();
428                            }
429                            let create_result = plugin.gui_create("x11", is_floating);
430                            create_result
431                                .and_then(|_| {
432                                    if window_id != 0 {
433                                        plugin.gui_set_parent(window_id)
434                                    } else {
435                                        Ok(())
436                                    }
437                                })
438                                .and_then(|_| plugin.gui_show())
439                        }
440                    }
441                    4 => {
442                        tracing::info!(instance_id = %self.instance_id, "GUI hide requested");
443                        plugin.gui_hide()
444                    }
445                    _ => Err(format!("Unknown request type: {req}")),
446                };
447                header
448                    .request_status
449                    .store(if result.is_ok() { 1 } else { 2 }, Ordering::Release);
450                if req == 1 || req == 2 {
451                    let _ = self.events.signal_daw();
452                }
453                header.request_type.store(0, Ordering::Release);
454                continue;
455            }
456
457            // Idle work: timers, FDs, parameter flush, on_main_thread callback.
458            set_thread_type(ThreadType::MainThread);
459            self.handle_idle_work(&plugin, params_ext, timer_ext);
460
461            // Wait for DAW signal or timeout (max 100 ms to service timers/FDs).
462            let timeout_ms = self.next_timer_ms().min(100);
463            let (daw_ready, ready_fds) = match timeout_ms {
464                0 => (true, Vec::new()), // timers expired immediately
465                ms => self.poll_daw_and_fds(daw_read_fd, Duration::from_millis(ms)),
466            };
467
468            // Fire FD callbacks only for FDs that actually signaled readiness.
469            if let Some(ext) = fd_ext {
470                for (fd, flags) in ready_fds {
471                    unsafe {
472                        if let Some(cb) = (*ext).on_fd {
473                            cb(plugin.plugin_ptr(), fd, flags);
474                        }
475                    }
476                }
477            }
478
479            if !daw_ready {
480                // Timeout only — loop around to handle timers/FDs.
481                continue;
482            }
483
484            let block_size = header.block_size.load(Ordering::Acquire) as usize;
485            let num_in = header.num_input_channels.load(Ordering::Acquire) as usize;
486            let num_out = header.num_output_channels.load(Ordering::Acquire) as usize;
487
488            if block_size == 0 || block_size > MAX_BLOCK_SIZE {
489                tracing::warn!("Invalid block size {block_size}, skipping");
490                let _ = self.events.signal_daw();
491                continue;
492            }
493
494            // Rebuild port buffers if the plugin requested an audio-ports rescan.
495            if AUDIO_PORTS_RESCAN_REQUESTED.swap(false, Ordering::Acquire) {
496                tracing::info!(instance_id = %self.instance_id, "Rebuilding audio port buffers after rescan");
497                port_buffers = PortBuffers::from_plugin(plugin.plugin_ptr(), ptr, num_in, num_out);
498            }
499
500            // Update SHM pointers each block in case the DAW remapped channels.
501            if let Some(ref mut pb) = port_buffers {
502                let mut global_ch: usize = 0;
503                for port in &mut pb._input_ptrs {
504                    for ch in port.iter_mut() {
505                        *ch = if global_ch < num_in {
506                            unsafe { audio_channel_ptr(ptr, global_ch, 0) }
507                        } else {
508                            ptr::null_mut()
509                        };
510                        global_ch += 1;
511                    }
512                }
513                global_ch = 0;
514                for port in &mut pb._output_ptrs {
515                    for ch in port.iter_mut() {
516                        *ch = if global_ch < num_out {
517                            unsafe { audio_channel_ptr(ptr, global_ch, 1) }
518                        } else {
519                            ptr::null_mut()
520                        };
521                        global_ch += 1;
522                    }
523                }
524            } else {
525                // Fallback: single bus with all channels (old behavior).
526                let mut in_ptrs: [*mut f32; MAX_CHANNELS] = [ptr::null_mut(); MAX_CHANNELS];
527                let mut out_ptrs: [*mut f32; MAX_CHANNELS] = [ptr::null_mut(); MAX_CHANNELS];
528                for (ch, in_ptr) in in_ptrs
529                    .iter_mut()
530                    .enumerate()
531                    .take(num_in.min(MAX_CHANNELS))
532                {
533                    *in_ptr = unsafe { audio_channel_ptr(ptr, ch, 0) };
534                }
535                for (ch, out_ptr) in out_ptrs
536                    .iter_mut()
537                    .enumerate()
538                    .take(num_out.min(MAX_CHANNELS))
539                {
540                    *out_ptr = unsafe { audio_channel_ptr(ptr, ch, 1) };
541                }
542            }
543
544            // Build input event list from parameter and MIDI ring buffers.
545            let mut event_buf = EventBuffer::new();
546            while let Some(ev) = param_ring.pop() {
547                match ev.event_kind {
548                    PARAM_EVENT_MOD => {
549                        event_buf.push_param_mod(ev.param_index, ev.value as f64, ev.sample_offset);
550                    }
551                    PARAM_EVENT_GESTURE_BEGIN => {
552                        event_buf.push_param_gesture_begin(ev.param_index, ev.sample_offset);
553                    }
554                    PARAM_EVENT_GESTURE_END => {
555                        event_buf.push_param_gesture_end(ev.param_index, ev.sample_offset);
556                    }
557                    _ => {
558                        event_buf.push_param_value(
559                            ev.param_index,
560                            ev.value as f64,
561                            ev.sample_offset,
562                        );
563                    }
564                }
565            }
566            while let Some(ev) = midi_ring.pop() {
567                if has_note_ports {
568                    self.push_midi_as_clap_events(
569                        &mut event_buf,
570                        ev.data,
571                        ev.channel as u16,
572                        ev.sample_offset,
573                    );
574                } else {
575                    event_buf.push_midi(ev.data, ev.channel as u16, ev.sample_offset);
576                }
577            }
578            let in_events = event_buf.as_input_events();
579
580            // Capture events the plugin emits back to the DAW.
581            let mut event_capture = EventCapture::new();
582            let out_events = event_capture.as_output_events();
583
584            // Flush parameters on main thread if requested.
585            if PARAMS_FLUSH_REQUESTED.swap(false, Ordering::Acquire)
586                && let Some(params_ptr) = params_ext
587            {
588                unsafe {
589                    let flush = (*params_ptr).flush;
590                    if let Some(f) = flush {
591                        let empty_in = crate::clap::empty_input_events();
592                        let mut flush_capture = EventCapture::new();
593                        let flush_out = flush_capture.as_output_events();
594                        f(plugin.plugin_ptr(), &empty_in, &flush_out);
595                        // Echo flushed events immediately.
596                        for bytes in flush_capture.drain() {
597                            if bytes.len() >= std::mem::size_of::<ClapEventHeader>() {
598                                let h = &*(bytes.as_ptr() as *const ClapEventHeader);
599                                self.echo_event_to_daw(h, &bytes, &echo_ring);
600                            }
601                        }
602                    }
603                }
604            }
605
606            let transport =
607                unsafe { transport_ref(ptr) as *const TransportState as *const std::ffi::c_void };
608
609            tracing::debug!(
610                instance_id = %self.instance_id,
611                block_size,
612                num_in,
613                num_out,
614                events_in = event_buf.len(),
615                "Processing block"
616            );
617
618            if !started_processing {
619                set_thread_type(ThreadType::AudioThread);
620                if let Err(e) = plugin.start_processing() {
621                    tracing::error!("Failed to start processing: {e}");
622                    break;
623                }
624                started_processing = true;
625            }
626
627            set_thread_type(ThreadType::AudioThread);
628
629            let process_result = if let Some(ref mut pb) = port_buffers {
630                let process = ClapProcess {
631                    steady_time,
632                    frames_count: block_size as u32,
633                    transport,
634                    audio_inputs: pb.inputs.as_ptr(),
635                    audio_outputs: pb.outputs.as_mut_ptr(),
636                    audio_inputs_count: pb.inputs.len() as u32,
637                    audio_outputs_count: pb.outputs.len() as u32,
638                    in_events: &in_events,
639                    out_events: &out_events,
640                };
641                plugin.process(&process)
642            } else {
643                let mut fallback_in_ptrs: Vec<*mut f32> = Vec::new();
644                let mut fallback_out_ptrs: Vec<*mut f32> = Vec::new();
645                fallback_in_ptrs.resize(num_in.min(MAX_CHANNELS), ptr::null_mut());
646                fallback_out_ptrs.resize(num_out.min(MAX_CHANNELS), ptr::null_mut());
647                for (ch, in_ptr) in fallback_in_ptrs.iter_mut().enumerate() {
648                    *in_ptr = unsafe { audio_channel_ptr(ptr, ch, 0) };
649                }
650                for (ch, out_ptr) in fallback_out_ptrs.iter_mut().enumerate() {
651                    *out_ptr = unsafe { audio_channel_ptr(ptr, ch, 1) };
652                }
653                let fallback_audio_in = ClapAudioBuffer {
654                    data32: fallback_in_ptrs.as_mut_ptr(),
655                    data64: ptr::null_mut(),
656                    channel_count: num_in as u32,
657                    latency: 0,
658                    constant_mask: 0,
659                };
660                let mut fallback_audio_out = ClapAudioBuffer {
661                    data32: fallback_out_ptrs.as_mut_ptr(),
662                    data64: ptr::null_mut(),
663                    channel_count: num_out as u32,
664                    latency: 0,
665                    constant_mask: 0,
666                };
667                let process = ClapProcess {
668                    steady_time,
669                    frames_count: block_size as u32,
670                    transport,
671                    audio_inputs: &fallback_audio_in,
672                    audio_outputs: &mut fallback_audio_out,
673                    audio_inputs_count: 1,
674                    audio_outputs_count: 1,
675                    in_events: &in_events,
676                    out_events: &out_events,
677                };
678                plugin.process(&process)
679            };
680
681            set_thread_type(ThreadType::MainThread);
682
683            if let Err(e) = process_result {
684                tracing::error!("Plugin process error: {e}");
685                break;
686            }
687
688            steady_time += block_size as i64;
689
690            // Forward captured events to the DAW via echo ring.
691            for bytes in event_capture.drain() {
692                if bytes.len() >= std::mem::size_of::<ClapEventHeader>() {
693                    let h = unsafe { &*(bytes.as_ptr() as *const ClapEventHeader) };
694                    self.echo_event_to_daw(h, &bytes, &echo_ring);
695                }
696            }
697
698            tracing::debug!(instance_id = %self.instance_id, "Block processed, signalling DAW");
699
700            if let Err(e) = self.events.signal_daw() {
701                tracing::error!("Failed to signal DAW: {e}");
702                break;
703            }
704        }
705
706        if started_processing {
707            set_thread_type(ThreadType::AudioThread);
708            plugin.stop_processing();
709            set_thread_type(ThreadType::MainThread);
710        }
711        plugin.deactivate();
712        tracing::info!(instance_id = %self.instance_id, "CLAP plugin stopped");
713    }
714
715    /// Clean up and exit.
716    pub fn shutdown(self) {
717        tracing::info!(instance_id = %self.instance_id, "Plugin host shutting down");
718    }
719
720    // ------------------------------------------------------------------
721    // Helpers
722    // ------------------------------------------------------------------
723
724    /// Convert a raw MIDI event to CLAP note events if applicable, pushing into `event_buf`.
725    #[cfg(unix)]
726    fn push_midi_as_clap_events(
727        &self,
728        event_buf: &mut EventBuffer,
729        data: [u8; 3],
730        port_index: u16,
731        sample_offset: u32,
732    ) {
733        let status = data[0] & 0xF0;
734        let channel = (data[0] & 0x0F) as i16;
735        let note_id = -1i32;
736        match status {
737            0x90 => {
738                let velocity = data[2] as f64 / 127.0;
739                if velocity > 0.0 {
740                    event_buf.push_note_on(
741                        note_id,
742                        port_index as i16,
743                        channel,
744                        data[1] as i16,
745                        velocity,
746                        sample_offset,
747                    );
748                } else {
749                    event_buf.push_note_off(
750                        note_id,
751                        port_index as i16,
752                        channel,
753                        data[1] as i16,
754                        0.0,
755                        sample_offset,
756                    );
757                }
758            }
759            0x80 => {
760                let velocity = data[2] as f64 / 127.0;
761                event_buf.push_note_off(
762                    note_id,
763                    port_index as i16,
764                    channel,
765                    data[1] as i16,
766                    velocity,
767                    sample_offset,
768                );
769            }
770            _ => {}
771        }
772        // Always emit the raw MIDI event as well.
773        event_buf.push_midi(data, port_index, sample_offset);
774    }
775
776    /// Convert a captured CLAP event into a `ParameterEvent` and push it to the echo ring.
777    #[cfg(unix)]
778    fn echo_event_to_daw(
779        &self,
780        header: &ClapEventHeader,
781        bytes: &[u8],
782        echo_ring: &RingBuffer<ParameterEvent>,
783    ) {
784        match header.type_ {
785            crate::clap::CLAP_EVENT_PARAM_VALUE
786                if bytes.len() >= std::mem::size_of::<ClapEventParamValue>() =>
787            {
788                let ev = unsafe { &*(bytes.as_ptr() as *const ClapEventParamValue) };
789                let echo = ParameterEvent {
790                    param_index: ev.param_id,
791                    value: ev.value as f32,
792                    sample_offset: ev.header.time,
793                    event_kind: PARAM_EVENT_VALUE,
794                };
795                if !echo_ring.push(echo) {
796                    tracing::warn!("Echo ring full, dropping parameter value event");
797                }
798            }
799            crate::clap::CLAP_EVENT_PARAM_MOD
800                if bytes.len() >= std::mem::size_of::<ClapEventParamMod>() =>
801            {
802                let ev = unsafe { &*(bytes.as_ptr() as *const ClapEventParamMod) };
803                let echo = ParameterEvent {
804                    param_index: ev.param_id,
805                    value: ev.amount as f32,
806                    sample_offset: ev.header.time,
807                    event_kind: PARAM_EVENT_MOD,
808                };
809                if !echo_ring.push(echo) {
810                    tracing::warn!("Echo ring full, dropping parameter mod event");
811                }
812            }
813            crate::clap::CLAP_EVENT_PARAM_GESTURE_BEGIN
814                if bytes.len() >= std::mem::size_of::<ClapEventParamGesture>() =>
815            {
816                let ev = unsafe { &*(bytes.as_ptr() as *const ClapEventParamGesture) };
817                let echo = ParameterEvent {
818                    param_index: ev.param_id,
819                    value: 0.0,
820                    sample_offset: ev.header.time,
821                    event_kind: PARAM_EVENT_GESTURE_BEGIN,
822                };
823                if !echo_ring.push(echo) {
824                    tracing::warn!("Echo ring full, dropping gesture begin event");
825                }
826            }
827            crate::clap::CLAP_EVENT_PARAM_GESTURE_END
828                if bytes.len() >= std::mem::size_of::<ClapEventParamGesture>() =>
829            {
830                let ev = unsafe { &*(bytes.as_ptr() as *const ClapEventParamGesture) };
831                let echo = ParameterEvent {
832                    param_index: ev.param_id,
833                    value: 0.0,
834                    sample_offset: ev.header.time,
835                    event_kind: PARAM_EVENT_GESTURE_END,
836                };
837                if !echo_ring.push(echo) {
838                    tracing::warn!("Echo ring full, dropping gesture end event");
839                }
840            }
841            _ => {
842                // Other event types are not echoed via the parameter ring.
843            }
844        }
845    }
846
847    /// Poll the DAW pipe and registered FDs.
848    /// Returns `(daw_ready, ready_fds)` where `ready_fds` contains only FDs that signaled.
849    #[cfg(unix)]
850    fn poll_daw_and_fds(&self, daw_fd: i32, timeout: Duration) -> (bool, Vec<(i32, u32)>) {
851        let fds = host_fds().lock().unwrap();
852        if fds.is_empty() {
853            return (self.events.wait_daw(timeout).is_ok(), Vec::new());
854        }
855        let mut poll_fds: Vec<libc::pollfd> = Vec::with_capacity(fds.len() + 1);
856        poll_fds.push(libc::pollfd {
857            fd: daw_fd,
858            events: libc::POLLIN,
859            revents: 0,
860        });
861        for f in fds.iter() {
862            let mut events = 0;
863            if f.flags & 1 != 0 {
864                events |= libc::POLLIN;
865            }
866            if f.flags & 2 != 0 {
867                events |= libc::POLLOUT;
868            }
869            if f.flags & 4 != 0 {
870                events |= libc::POLLERR;
871            }
872            poll_fds.push(libc::pollfd {
873                fd: f.fd,
874                events,
875                revents: 0,
876            });
877        }
878        let ms = timeout.as_millis().clamp(0, i32::MAX as u128) as i32;
879        let rc = unsafe { libc::poll(poll_fds.as_mut_ptr(), poll_fds.len() as libc::nfds_t, ms) };
880        if rc < 0 {
881            return (false, Vec::new());
882        }
883        let mut ready_fds = Vec::new();
884        for (i, f) in fds.iter().enumerate() {
885            let pfd = &poll_fds[i + 1];
886            if pfd.revents != 0 {
887                let mut flags = 0;
888                if pfd.revents & libc::POLLIN != 0 {
889                    flags |= 1;
890                }
891                if pfd.revents & libc::POLLOUT != 0 {
892                    flags |= 2;
893                }
894                if pfd.revents & libc::POLLERR != 0 {
895                    flags |= 4;
896                }
897                tracing::debug!(fd = f.fd, flags, "FD event");
898                ready_fds.push((f.fd, flags));
899            }
900        }
901        (poll_fds[0].revents & libc::POLLIN != 0, ready_fds)
902    }
903
904    /// Return the number of milliseconds until the next timer expires (0 if already expired).
905    #[cfg(unix)]
906    fn next_timer_ms(&self) -> u64 {
907        let timers = host_timers().lock().unwrap();
908        let now = Instant::now();
909        timers
910            .iter()
911            .map(|t| {
912                if t.deadline <= now {
913                    0
914                } else {
915                    (t.deadline - now).as_millis() as u64
916                }
917            })
918            .min()
919            .unwrap_or(100)
920    }
921
922    /// Handle timers, FD callbacks, and on_main_thread.
923    #[cfg(unix)]
924    fn handle_idle_work(
925        &self,
926        plugin: &PluginInstance,
927        _params_ext: Option<*const ClapPluginParams>,
928        timer_ext: Option<*const crate::clap::ClapPluginTimerSupport>,
929    ) {
930        let now = Instant::now();
931        let mut fired_timers = Vec::new();
932        {
933            let mut timers = host_timers().lock().unwrap();
934            for t in timers.iter_mut() {
935                if t.deadline <= now {
936                    fired_timers.push(t.id);
937                    t.deadline = now + Duration::from_millis(t.period_ms as u64);
938                }
939            }
940        }
941        if let Some(ext) = timer_ext {
942            for id in fired_timers {
943                unsafe {
944                    if let Some(f) = (*ext).on_timer {
945                        f(plugin.plugin_ptr(), id);
946                    }
947                }
948            }
949        }
950    }
951}