bbx_plugin/
audio.rs

1//! Audio processing FFI functions.
2//!
3//! This module provides the generic audio processing function that bridges
4//! JUCE's processBlock to the Rust effects chain using zero-copy buffer handling.
5
6use std::panic::{AssertUnwindSafe, catch_unwind};
7
8use bbx_dsp::PluginDsp;
9use bbx_midi::MidiEvent;
10
11use crate::handle::{BbxGraph, graph_from_handle};
12
13/// Maximum samples per buffer.
14///
15/// Buffers larger than this are processed up to this limit only.
16/// This value accommodates most audio workflows (e.g., 4096 samples at 192kHz = ~21ms).
17/// Configure your DAW to use buffer sizes <= 4096 for full coverage.
18const MAX_SAMPLES: usize = 4096;
19
20/// Maximum channels supported.
21const MAX_CHANNELS: usize = 2;
22
23/// Process a block of audio through the effects chain.
24///
25/// This function is called by the macro-generated `bbx_graph_process` FFI function.
26/// It uses zero-copy buffer handling for optimal performance.
27///
28/// # Safety
29///
30/// - `handle` must be a valid pointer from `bbx_graph_create`.
31/// - `inputs` must be a valid pointer to an array of `num_channels` pointers, or null.
32/// - `outputs` must be a valid pointer to an array of `num_channels` pointers.
33/// - Each input/output channel pointer must be valid for `num_samples` floats.
34/// - `params` must be valid for `num_params` floats, or null.
35/// - `midi_events` must be valid for `num_midi_events` MidiEvent structs, or null.
36#[allow(clippy::too_many_arguments)]
37pub unsafe fn process_audio<D: PluginDsp>(
38    handle: *mut BbxGraph,
39    inputs: *const *const f32,
40    outputs: *mut *mut f32,
41    num_channels: u32,
42    num_samples: u32,
43    params: *const f32,
44    num_params: u32,
45    midi_events: *const MidiEvent,
46    num_midi_events: u32,
47) {
48    if handle.is_null() || outputs.is_null() {
49        return;
50    }
51
52    let result = catch_unwind(AssertUnwindSafe(|| unsafe {
53        let inner = graph_from_handle::<D>(handle);
54
55        let num_channels = num_channels as usize;
56        let num_samples = num_samples as usize;
57
58        if !inner.prepared {
59            for i in 0..num_channels {
60                let output_ptr = *outputs.add(i);
61                if !output_ptr.is_null() {
62                    std::ptr::write_bytes(output_ptr, 0, num_samples * size_of::<f32>());
63                }
64            }
65            return;
66        }
67
68        // Debug check for buffer size limits
69        debug_assert!(
70            num_samples <= MAX_SAMPLES,
71            "Buffer size {num_samples} exceeds MAX_SAMPLES ({MAX_SAMPLES}). Only first {MAX_SAMPLES} samples processed."
72        );
73        debug_assert!(
74            num_channels <= MAX_CHANNELS,
75            "Channel count {num_channels} exceeds MAX_CHANNELS ({MAX_CHANNELS}). Only first {MAX_CHANNELS} channels processed."
76        );
77
78        let samples_to_process = num_samples.min(MAX_SAMPLES);
79        let channels_to_process = num_channels.min(MAX_CHANNELS);
80
81        // Apply parameters if provided
82        if !params.is_null() && num_params > 0 {
83            let param_slice = std::slice::from_raw_parts(params, num_params as usize);
84            inner.dsp.apply_parameters(param_slice);
85        }
86
87        // Build MIDI slice (empty if null pointer or zero count)
88        let midi_slice: &[MidiEvent] = if !midi_events.is_null() && num_midi_events > 0 {
89            std::slice::from_raw_parts(midi_events, num_midi_events as usize)
90        } else {
91            &[]
92        };
93
94        // Build input slices directly from FFI pointers (zero-copy)
95        // Use a small stack buffer for silent channels when input is null
96        let silent_buffer: [f32; MAX_SAMPLES] = [0.0; MAX_SAMPLES];
97        let silent_slice: &[f32] = &silent_buffer[..samples_to_process];
98
99        let mut input_slices_storage: [&[f32]; MAX_CHANNELS] = [silent_slice; MAX_CHANNELS];
100
101        if !inputs.is_null() {
102            #[allow(clippy::needless_range_loop)] // ch is used for both array indexing and pointer arithmetic
103            for ch in 0..channels_to_process {
104                let input_ptr = *inputs.add(ch);
105                if !input_ptr.is_null() {
106                    input_slices_storage[ch] = std::slice::from_raw_parts(input_ptr, samples_to_process);
107                }
108            }
109        }
110
111        // Build output slices directly from FFI pointers (zero-copy)
112        // JUCE guarantees valid output pointers for all requested channels
113        let output_ptr_0 = *outputs.add(0);
114        let output_ptr_1 = if channels_to_process > 1 {
115            *outputs.add(1)
116        } else {
117            output_ptr_0
118        };
119
120        if output_ptr_0.is_null() {
121            return;
122        }
123
124        let output_slice_0 = std::slice::from_raw_parts_mut(output_ptr_0, samples_to_process);
125
126        if channels_to_process == 1 {
127            let mut output_refs: [&mut [f32]; 1] = [output_slice_0];
128            inner
129                .dsp
130                .process(&input_slices_storage[..1], &mut output_refs, midi_slice, &inner.context);
131        } else {
132            if output_ptr_1.is_null() {
133                return;
134            }
135            let output_slice_1 = std::slice::from_raw_parts_mut(output_ptr_1, samples_to_process);
136            let mut output_refs: [&mut [f32]; 2] = [output_slice_0, output_slice_1];
137            inner.dsp.process(
138                &input_slices_storage[..channels_to_process],
139                &mut output_refs,
140                midi_slice,
141                &inner.context,
142            );
143        }
144    }));
145
146    // On panic, zero all outputs to produce silence instead of crashing the host
147    if result.is_err() {
148        unsafe {
149            for i in 0..num_channels as usize {
150                let output_ptr = *outputs.add(i);
151                if !output_ptr.is_null() {
152                    std::ptr::write_bytes(output_ptr, 0, num_samples as usize * size_of::<f32>());
153                }
154            }
155        }
156    }
157}