Skip to main content

vst3_host/backends/
cpal_backend.rs

1//! CPAL audio backend implementation
2
3use crate::{
4    audio::{AudioBackend, AudioConfig, AudioStream},
5    error::{Error, Result},
6};
7use cpal::{
8    traits::{DeviceTrait, HostTrait, StreamTrait},
9    BufferSize, Device, Stream, StreamConfig, SupportedBufferSize,
10};
11
12/// Clamp a requested `block_size` into a device-advertised supported buffer range.
13///
14/// `SupportedBufferSize::Range` → `Fixed(block_size clamped to [min, max])`;
15/// `Unknown` → `Default` (the device gives no range, so let it choose). Pure and unit-tested.
16fn clamp_block_to_buffer_size(supported: &SupportedBufferSize, block_size: u32) -> BufferSize {
17    match supported {
18        SupportedBufferSize::Range { min, max } => BufferSize::Fixed(block_size.clamp(*min, *max)),
19        SupportedBufferSize::Unknown => BufferSize::Default,
20    }
21}
22
23/// Pick a buffer size the device will actually accept, given its advertised config ranges,
24/// the requested sample rate / channel count, and the desired `block_size`.
25///
26/// Many devices (notably CoreAudio on macOS) reject `BufferSize::Fixed` outright, so we only
27/// request a fixed size when a matching range is advertised — and clamp into it. Otherwise we
28/// fall back to `BufferSize::Default`. The channel count and sample rate are NOT changed here:
29/// the bridge interleaves based on the requested channel count, so silently changing it would
30/// garble audio.
31fn resolve_buffer_size(
32    ranges: impl Iterator<Item = cpal::SupportedStreamConfigRange>,
33    want_sr: u32,
34    want_ch: u16,
35    block_size: u32,
36) -> BufferSize {
37    // Prefer a range matching both the channel count and sample rate. Many devices (notably
38    // pro multichannel interfaces) advertise their config ranges only at their native channel
39    // count, so a stereo request would never find an exact match and silently lose the
40    // configured block size. The buffer-size range is a device-level property independent of
41    // the stream's channel count, so fall back to any range that covers the sample rate.
42    let mut sr_only_fallback: Option<BufferSize> = None;
43    for range in ranges {
44        if want_sr < range.min_sample_rate() || want_sr > range.max_sample_rate() {
45            continue;
46        }
47        let resolved = clamp_block_to_buffer_size(range.buffer_size(), block_size);
48        if range.channels() == want_ch {
49            return resolved; // exact channel + sample-rate match wins
50        }
51        sr_only_fallback.get_or_insert(resolved);
52    }
53    sr_only_fallback.unwrap_or(BufferSize::Default)
54}
55
56/// Resolve the output-stream buffer size for `device` and `config`.
57fn resolve_output_buffer_size(device: &Device, config: &AudioConfig) -> BufferSize {
58    // cpal 0.18: `SampleRate` is a `u32` type alias.
59    match device.supported_output_configs() {
60        Ok(ranges) => resolve_buffer_size(
61            ranges,
62            config.sample_rate as u32,
63            config.output_channels as u16,
64            config.block_size as u32,
65        ),
66        Err(_) => BufferSize::Default,
67    }
68}
69
70/// Resolve the input-stream buffer size for `device` and `config`.
71///
72/// Mirrors [`resolve_output_buffer_size`] for the capture side: an unconditional
73/// `BufferSize::Fixed` (what this used to send) is rejected by CoreAudio on many input devices.
74fn resolve_input_buffer_size(device: &Device, config: &AudioConfig) -> BufferSize {
75    match device.supported_input_configs() {
76        Ok(ranges) => resolve_buffer_size(
77            ranges,
78            config.sample_rate as u32,
79            config.input_channels as u16,
80            config.block_size as u32,
81        ),
82        Err(_) => BufferSize::Default,
83    }
84}
85
86/// CPAL stream wrapper
87pub struct CpalStream {
88    // We use Option to allow moving the stream in drop
89    stream: Option<Stream>,
90}
91
92// Manually implement Send for CpalStream
93// This is safe because we only use the stream for play/pause operations
94unsafe impl Send for CpalStream {}
95
96impl AudioStream for CpalStream {
97    fn play(&self) -> std::result::Result<(), Box<dyn std::error::Error>> {
98        if let Some(ref stream) = self.stream {
99            stream
100                .play()
101                .map_err(|e| Box::new(e) as Box<dyn std::error::Error>)
102        } else {
103            Err(Box::new(std::io::Error::other("Stream has been dropped")))
104        }
105    }
106
107    fn pause(&self) -> std::result::Result<(), Box<dyn std::error::Error>> {
108        if let Some(ref stream) = self.stream {
109            stream
110                .pause()
111                .map_err(|e| Box::new(e) as Box<dyn std::error::Error>)
112        } else {
113            Err(Box::new(std::io::Error::other("Stream has been dropped")))
114        }
115    }
116}
117
118impl Drop for CpalStream {
119    fn drop(&mut self) {
120        // Drop the stream
121        self.stream.take();
122    }
123}
124
125/// CPAL-based audio backend
126pub struct CpalBackend {
127    host: cpal::Host,
128}
129
130impl CpalBackend {
131    /// Create a new CPAL backend
132    pub fn new() -> Result<Self> {
133        Ok(Self {
134            host: cpal::default_host(),
135        })
136    }
137
138    /// List all available output devices
139    pub fn list_output_devices(&self) -> Result<Vec<String>> {
140        let devices: Vec<String> = self
141            .host
142            .output_devices()
143            .map_err(|e| Error::AudioBackendError(format!("Failed to enumerate devices: {}", e)))?
144            .map(|d| d.to_string())
145            .collect();
146        Ok(devices)
147    }
148
149    /// List all available input devices
150    pub fn list_input_devices(&self) -> Result<Vec<String>> {
151        let devices: Vec<String> = self
152            .host
153            .input_devices()
154            .map_err(|e| Error::AudioBackendError(format!("Failed to enumerate devices: {}", e)))?
155            .map(|d| d.to_string())
156            .collect();
157        Ok(devices)
158    }
159}
160
161impl AudioBackend for CpalBackend {
162    type Stream = CpalStream;
163    type Device = Device;
164    type Error = Error;
165
166    fn enumerate_output_devices(&self) -> Result<Vec<Self::Device>> {
167        let devices: Vec<Device> = self
168            .host
169            .output_devices()
170            .map_err(|e| {
171                Error::AudioBackendError(format!("Failed to enumerate output devices: {}", e))
172            })?
173            .collect();
174        Ok(devices)
175    }
176
177    fn enumerate_input_devices(&self) -> Result<Vec<Self::Device>> {
178        let devices: Vec<Device> = self
179            .host
180            .input_devices()
181            .map_err(|e| {
182                Error::AudioBackendError(format!("Failed to enumerate input devices: {}", e))
183            })?
184            .collect();
185        Ok(devices)
186    }
187
188    fn default_output_device(&self) -> Option<Self::Device> {
189        self.host.default_output_device()
190    }
191
192    fn default_input_device(&self) -> Option<Self::Device> {
193        self.host.default_input_device()
194    }
195
196    fn create_output_stream(
197        &self,
198        device: &Self::Device,
199        config: AudioConfig,
200        mut data_callback: Box<dyn FnMut(&mut [f32]) + Send>,
201        mut error_callback: Box<dyn FnMut(Self::Error) + Send>,
202    ) -> Result<Self::Stream> {
203        let stream_config = StreamConfig {
204            channels: config.output_channels as u16,
205            sample_rate: config.sample_rate as u32,
206            buffer_size: resolve_output_buffer_size(device, &config),
207        };
208
209        let stream = device
210            .build_output_stream(
211                stream_config,
212                move |data: &mut [f32], _: &cpal::OutputCallbackInfo| {
213                    data_callback(data);
214                },
215                move |err| {
216                    error_callback(Error::AudioBackendError(format!("Stream error: {}", err)));
217                },
218                None,
219            )
220            .map_err(|e| {
221                Error::AudioBackendError(format!("Failed to build output stream: {}", e))
222            })?;
223
224        Ok(CpalStream {
225            stream: Some(stream),
226        })
227    }
228
229    fn create_input_stream(
230        &self,
231        device: &Self::Device,
232        config: AudioConfig,
233        mut data_callback: Box<dyn FnMut(&[f32]) + Send>,
234        mut error_callback: Box<dyn FnMut(Self::Error) + Send>,
235    ) -> Result<Self::Stream> {
236        let stream_config = StreamConfig {
237            channels: config.input_channels as u16,
238            sample_rate: config.sample_rate as u32,
239            buffer_size: resolve_input_buffer_size(device, &config),
240        };
241
242        let stream = device
243            .build_input_stream(
244                stream_config,
245                move |data: &[f32], _: &cpal::InputCallbackInfo| {
246                    data_callback(data);
247                },
248                move |err| {
249                    error_callback(Error::AudioBackendError(format!("Stream error: {}", err)));
250                },
251                None,
252            )
253            .map_err(|e| {
254                Error::AudioBackendError(format!("Failed to build input stream: {}", e))
255            })?;
256
257        Ok(CpalStream {
258            stream: Some(stream),
259        })
260    }
261}
262
263impl Default for CpalBackend {
264    fn default() -> Self {
265        Self::new().expect("Failed to create CPAL backend")
266    }
267}
268
269#[cfg(test)]
270mod tests {
271    use super::*;
272    use cpal::{SampleFormat, SupportedStreamConfigRange};
273
274    /// Build a config range with the given channels, sample-rate bounds, and fixed buffer range.
275    fn range(
276        channels: u16,
277        min_sr: u32,
278        max_sr: u32,
279        buf_min: u32,
280        buf_max: u32,
281    ) -> SupportedStreamConfigRange {
282        SupportedStreamConfigRange::new(
283            channels,
284            min_sr,
285            max_sr,
286            SupportedBufferSize::Range {
287                min: buf_min,
288                max: buf_max,
289            },
290            SampleFormat::F32,
291        )
292    }
293
294    #[test]
295    fn resolve_exact_channel_and_sr_match_clamps() {
296        let ranges = vec![range(2, 44_100, 48_000, 64, 2048)];
297        assert_eq!(
298            resolve_buffer_size(ranges.into_iter(), 48_000, 2, 512),
299            BufferSize::Fixed(512)
300        );
301    }
302
303    #[test]
304    fn resolve_channel_mismatch_falls_back_to_sample_rate_match() {
305        // Device advertises only an 8-channel range (e.g. a pro interface); a stereo request
306        // must still honor the device's buffer-size range rather than dropping to Default.
307        let ranges = vec![range(8, 44_100, 96_000, 128, 1024)];
308        assert_eq!(
309            resolve_buffer_size(ranges.into_iter(), 48_000, 2, 4096),
310            BufferSize::Fixed(1024) // clamped into the device range
311        );
312    }
313
314    #[test]
315    fn resolve_prefers_exact_channel_over_sr_only() {
316        // sr-only candidate first, exact match second — exact must win.
317        let ranges = vec![
318            range(8, 44_100, 96_000, 128, 1024),
319            range(2, 44_100, 96_000, 64, 2048),
320        ];
321        assert_eq!(
322            resolve_buffer_size(ranges.into_iter(), 48_000, 2, 512),
323            BufferSize::Fixed(512)
324        );
325    }
326
327    #[test]
328    fn resolve_sample_rate_out_of_range_is_default() {
329        let ranges = vec![range(2, 44_100, 48_000, 64, 2048)];
330        assert_eq!(
331            resolve_buffer_size(ranges.into_iter(), 96_000, 2, 512),
332            BufferSize::Default
333        );
334    }
335
336    #[test]
337    fn resolve_no_ranges_is_default() {
338        let ranges: Vec<SupportedStreamConfigRange> = vec![];
339        assert_eq!(
340            resolve_buffer_size(ranges.into_iter(), 48_000, 2, 512),
341            BufferSize::Default
342        );
343    }
344
345    #[test]
346    fn clamp_within_range_keeps_requested_size() {
347        let supported = SupportedBufferSize::Range { min: 64, max: 2048 };
348        assert_eq!(
349            clamp_block_to_buffer_size(&supported, 512),
350            BufferSize::Fixed(512)
351        );
352    }
353
354    #[test]
355    fn clamp_below_min_raises_to_min() {
356        let supported = SupportedBufferSize::Range {
357            min: 256,
358            max: 2048,
359        };
360        assert_eq!(
361            clamp_block_to_buffer_size(&supported, 64),
362            BufferSize::Fixed(256)
363        );
364    }
365
366    #[test]
367    fn clamp_above_max_lowers_to_max() {
368        let supported = SupportedBufferSize::Range { min: 64, max: 1024 };
369        assert_eq!(
370            clamp_block_to_buffer_size(&supported, 4096),
371            BufferSize::Fixed(1024)
372        );
373    }
374
375    #[test]
376    fn clamp_unknown_range_falls_back_to_default() {
377        assert_eq!(
378            clamp_block_to_buffer_size(&SupportedBufferSize::Unknown, 512),
379            BufferSize::Default
380        );
381    }
382}