kizzasi_io/
audio.rs

1//! Audio input/output via cpal
2//!
3//! Provides comprehensive audio I/O with:
4//! - Audio input (recording)
5//! - Audio output (playback)
6//! - Multi-channel support
7//! - WAV file integration
8//! - Device enumeration
9//! - ASIO backend support (Windows)
10//! - JACK backend support (Linux/macOS)
11
12use crate::error::{IoError, IoResult};
13use crate::stream::{SignalStream, StreamConfig};
14use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
15use scirs2_core::ndarray::{Array1, Array2};
16use serde::{Deserialize, Serialize};
17use std::sync::{Arc, Mutex};
18use tracing::{debug, info, warn};
19
20#[cfg(feature = "file")]
21use crate::file::WavReader;
22
23/// Audio backend selection
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
25pub enum AudioBackend {
26    /// System default backend (WASAPI on Windows, CoreAudio on macOS, ALSA on Linux)
27    #[default]
28    Default,
29    /// ASIO backend (Windows only, requires ASIO driver installation)
30    #[cfg(target_os = "windows")]
31    Asio,
32    /// JACK Audio Connection Kit (Linux/macOS pro audio)
33    #[cfg(any(target_os = "linux", target_os = "macos"))]
34    Jack,
35}
36
37/// Configuration for audio I/O
38#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct AudioConfig {
40    /// Device name (None for default)
41    pub device_name: Option<String>,
42
43    /// Sample rate
44    #[serde(default = "default_sample_rate")]
45    pub sample_rate: u32,
46
47    /// Number of channels
48    #[serde(default = "default_channels")]
49    pub channels: u16,
50
51    /// Buffer size
52    #[serde(default = "default_buffer_size")]
53    pub buffer_size: u32,
54
55    /// Use output device (false for input)
56    #[serde(default)]
57    pub output: bool,
58
59    /// Audio backend to use
60    #[serde(default)]
61    pub backend: AudioBackend,
62}
63
64fn default_sample_rate() -> u32 {
65    44100
66}
67
68fn default_channels() -> u16 {
69    1
70}
71
72fn default_buffer_size() -> u32 {
73    1024
74}
75
76impl Default for AudioConfig {
77    fn default() -> Self {
78        Self {
79            device_name: None,
80            sample_rate: 44100,
81            channels: 1,
82            buffer_size: 1024,
83            output: false,
84            backend: AudioBackend::Default,
85        }
86    }
87}
88
89impl AudioConfig {
90    /// Create new audio configuration for input
91    pub fn new() -> Self {
92        Self::default()
93    }
94
95    /// Create new audio configuration for output
96    pub fn new_output() -> Self {
97        Self {
98            output: true,
99            ..Default::default()
100        }
101    }
102
103    /// Set sample rate
104    pub fn sample_rate(mut self, rate: u32) -> Self {
105        self.sample_rate = rate;
106        self
107    }
108
109    /// Set number of channels
110    pub fn channels(mut self, n: u16) -> Self {
111        self.channels = n;
112        self
113    }
114
115    /// Set buffer size
116    pub fn buffer_size(mut self, size: u32) -> Self {
117        self.buffer_size = size;
118        self
119    }
120
121    /// Set device name
122    pub fn device(mut self, name: &str) -> Self {
123        self.device_name = Some(name.to_string());
124        self
125    }
126
127    /// Set audio backend
128    pub fn backend(mut self, backend: AudioBackend) -> Self {
129        self.backend = backend;
130        self
131    }
132}
133
134/// Get the appropriate audio host based on backend selection
135fn get_host(backend: AudioBackend) -> IoResult<cpal::Host> {
136    match backend {
137        AudioBackend::Default => Ok(cpal::default_host()),
138        #[cfg(target_os = "windows")]
139        AudioBackend::Asio => {
140            // ASIO backend - iterate through available hosts
141            let available_hosts = cpal::available_hosts();
142            if available_hosts.contains(&cpal::HostId::Asio) {
143                Ok(cpal::host_from_id(cpal::HostId::Asio))
144            } else {
145                Err(IoError::ConfigError(
146                    "ASIO backend not available. Ensure ASIO drivers are installed.".into(),
147                ))
148            }
149        }
150        #[cfg(any(target_os = "linux", target_os = "macos"))]
151        AudioBackend::Jack => {
152            // JACK backend - for now, fall back to default as JACK support varies by cpal version
153            // In cpal 0.16, JACK may not be directly available via HostId
154            // Users can use JACK-enabled systems with the default host
155            info!("JACK support requested - using default host (ensure JACK is configured as default)");
156            Ok(cpal::default_host())
157        }
158    }
159}
160
161/// Audio input stream
162pub struct AudioInput {
163    config: StreamConfig,
164    audio_config: AudioConfig,
165    buffer: Arc<Mutex<Vec<f32>>>,
166    multi_channel_buffer: Arc<Mutex<Vec<Vec<f32>>>>,
167    #[allow(dead_code)]
168    stream: Option<cpal::Stream>,
169    active: bool,
170}
171
172impl AudioInput {
173    /// Create a new audio input
174    pub fn new(audio_config: AudioConfig) -> IoResult<Self> {
175        let stream_config = StreamConfig {
176            sample_rate: audio_config.sample_rate as f32,
177            channels: audio_config.channels as usize,
178            buffer_size: audio_config.buffer_size as usize,
179            timeout: None,
180        };
181
182        let num_channels = audio_config.channels as usize;
183
184        Ok(Self {
185            config: stream_config,
186            audio_config,
187            buffer: Arc::new(Mutex::new(Vec::new())),
188            multi_channel_buffer: Arc::new(Mutex::new(vec![Vec::new(); num_channels])),
189            stream: None,
190            active: false,
191        })
192    }
193
194    /// Start capturing audio
195    pub fn start(&mut self) -> IoResult<()> {
196        let host = get_host(self.audio_config.backend)?;
197
198        let device = if let Some(ref name) = self.audio_config.device_name {
199            host.input_devices()
200                .map_err(|e| IoError::ConfigError(e.to_string()))?
201                .find(|d| d.name().map(|n| n == *name).unwrap_or(false))
202                .ok_or_else(|| IoError::ConfigError(format!("Device not found: {}", name)))?
203        } else {
204            host.default_input_device()
205                .ok_or_else(|| IoError::ConfigError("No default input device".into()))?
206        };
207
208        let config = cpal::StreamConfig {
209            channels: self.audio_config.channels,
210            sample_rate: cpal::SampleRate(self.audio_config.sample_rate),
211            buffer_size: cpal::BufferSize::Fixed(self.audio_config.buffer_size),
212        };
213
214        info!(
215            "Starting audio input: {}Hz, {} channels, buffer={}",
216            self.audio_config.sample_rate,
217            self.audio_config.channels,
218            self.audio_config.buffer_size
219        );
220
221        let buffer = self.buffer.clone();
222        let multi_buffer = self.multi_channel_buffer.clone();
223        let num_channels = self.audio_config.channels as usize;
224
225        let stream = device
226            .build_input_stream(
227                &config,
228                move |data: &[f32], _: &cpal::InputCallbackInfo| {
229                    // Store interleaved samples
230                    if let Ok(mut buf) = buffer.lock() {
231                        buf.extend_from_slice(data);
232                    }
233
234                    // Store de-interleaved multi-channel samples
235                    if let Ok(mut multi_buf) = multi_buffer.lock() {
236                        for (i, &sample) in data.iter().enumerate() {
237                            let channel = i % num_channels;
238                            multi_buf[channel].push(sample);
239                        }
240                    }
241                },
242                |err| {
243                    warn!("Audio stream error: {}", err);
244                },
245                None,
246            )
247            .map_err(|e| IoError::StreamError(e.to_string()))?;
248
249        stream
250            .play()
251            .map_err(|e| IoError::StreamError(e.to_string()))?;
252
253        self.stream = Some(stream);
254        self.active = true;
255
256        info!("Audio input started");
257        Ok(())
258    }
259
260    /// Stop capturing audio
261    pub fn stop(&mut self) -> IoResult<()> {
262        self.stream = None;
263        self.active = false;
264        info!("Audio input stopped");
265        Ok(())
266    }
267
268    /// Read multi-channel data
269    pub fn read_channels(&mut self) -> IoResult<Array2<f32>> {
270        let mut multi_buffer = self
271            .multi_channel_buffer
272            .lock()
273            .map_err(|_| IoError::StreamError("Buffer lock failed".into()))?;
274
275        // Find minimum length across all channels
276        let min_len = multi_buffer
277            .iter()
278            .map(|ch| ch.len())
279            .min()
280            .unwrap_or(0)
281            .min(self.config.buffer_size);
282
283        if min_len == 0 {
284            return Ok(Array2::zeros((
285                self.config.buffer_size,
286                self.config.channels,
287            )));
288        }
289
290        let mut result = Array2::zeros((min_len, self.config.channels));
291        for (ch_idx, channel_data) in multi_buffer.iter_mut().enumerate() {
292            let samples: Vec<f32> = channel_data.drain(..min_len).collect();
293            for (i, &sample) in samples.iter().enumerate() {
294                result[[i, ch_idx]] = sample;
295            }
296        }
297
298        debug!(
299            "Read {} frames from {} channels",
300            min_len, self.config.channels
301        );
302        Ok(result)
303    }
304
305    /// Get available input devices for a specific backend
306    pub fn list_devices_with_backend(backend: AudioBackend) -> IoResult<Vec<String>> {
307        let host = get_host(backend)?;
308        let devices = host
309            .input_devices()
310            .map_err(|e| IoError::ConfigError(e.to_string()))?;
311
312        let names: Vec<String> = devices.filter_map(|d| d.name().ok()).collect();
313        Ok(names)
314    }
315
316    /// Get available input devices (using default backend)
317    pub fn list_devices() -> IoResult<Vec<String>> {
318        Self::list_devices_with_backend(AudioBackend::Default)
319    }
320}
321
322impl SignalStream for AudioInput {
323    fn read(&mut self) -> IoResult<Array1<f32>> {
324        let mut buffer = self
325            .buffer
326            .lock()
327            .map_err(|_| IoError::StreamError("Buffer lock failed".into()))?;
328
329        let size = self.config.buffer_size.min(buffer.len());
330        if size == 0 {
331            return Ok(Array1::zeros(self.config.buffer_size));
332        }
333
334        let data: Vec<f32> = buffer.drain(..size).collect();
335        let mut result = Array1::zeros(self.config.buffer_size);
336        for (i, val) in data.into_iter().enumerate() {
337            result[i] = val;
338        }
339        Ok(result)
340    }
341
342    fn is_active(&self) -> bool {
343        self.active
344    }
345
346    fn config(&self) -> &StreamConfig {
347        &self.config
348    }
349
350    fn close(&mut self) -> IoResult<()> {
351        self.stop()
352    }
353}
354
355/// Audio output stream (playback)
356pub struct AudioOutput {
357    #[allow(dead_code)]
358    config: StreamConfig,
359    audio_config: AudioConfig,
360    buffer: Arc<Mutex<Vec<f32>>>,
361    #[allow(dead_code)]
362    stream: Option<cpal::Stream>,
363    active: bool,
364    underrun_count: Arc<Mutex<usize>>,
365}
366
367impl AudioOutput {
368    /// Create a new audio output
369    pub fn new(audio_config: AudioConfig) -> IoResult<Self> {
370        let stream_config = StreamConfig {
371            sample_rate: audio_config.sample_rate as f32,
372            channels: audio_config.channels as usize,
373            buffer_size: audio_config.buffer_size as usize,
374            timeout: None,
375        };
376
377        Ok(Self {
378            config: stream_config,
379            audio_config,
380            buffer: Arc::new(Mutex::new(Vec::new())),
381            stream: None,
382            active: false,
383            underrun_count: Arc::new(Mutex::new(0)),
384        })
385    }
386
387    /// Start audio playback
388    pub fn start(&mut self) -> IoResult<()> {
389        let host = get_host(self.audio_config.backend)?;
390
391        let device = if let Some(ref name) = self.audio_config.device_name {
392            host.output_devices()
393                .map_err(|e| IoError::ConfigError(e.to_string()))?
394                .find(|d| d.name().map(|n| n == *name).unwrap_or(false))
395                .ok_or_else(|| IoError::ConfigError(format!("Device not found: {}", name)))?
396        } else {
397            host.default_output_device()
398                .ok_or_else(|| IoError::ConfigError("No default output device".into()))?
399        };
400
401        let config = cpal::StreamConfig {
402            channels: self.audio_config.channels,
403            sample_rate: cpal::SampleRate(self.audio_config.sample_rate),
404            buffer_size: cpal::BufferSize::Fixed(self.audio_config.buffer_size),
405        };
406
407        info!(
408            "Starting audio output: {}Hz, {} channels, buffer={}",
409            self.audio_config.sample_rate,
410            self.audio_config.channels,
411            self.audio_config.buffer_size
412        );
413
414        let buffer = self.buffer.clone();
415        let underrun_count = self.underrun_count.clone();
416
417        let stream = device
418            .build_output_stream(
419                &config,
420                move |data: &mut [f32], _: &cpal::OutputCallbackInfo| {
421                    let mut buf = match buffer.lock() {
422                        Ok(b) => b,
423                        Err(_) => return,
424                    };
425
426                    if buf.len() >= data.len() {
427                        // Sufficient data available
428                        for sample in data.iter_mut() {
429                            *sample = buf.remove(0);
430                        }
431                    } else {
432                        // Buffer underrun - fill with zeros
433                        if let Ok(mut count) = underrun_count.lock() {
434                            *count += 1;
435                        }
436                        for sample in data.iter_mut() {
437                            *sample = 0.0;
438                        }
439                    }
440                },
441                |err| {
442                    warn!("Audio output error: {}", err);
443                },
444                None,
445            )
446            .map_err(|e| IoError::StreamError(e.to_string()))?;
447
448        stream
449            .play()
450            .map_err(|e| IoError::StreamError(e.to_string()))?;
451
452        self.stream = Some(stream);
453        self.active = true;
454
455        info!("Audio output started");
456        Ok(())
457    }
458
459    /// Stop audio playback
460    pub fn stop(&mut self) -> IoResult<()> {
461        self.stream = None;
462        self.active = false;
463        info!("Audio output stopped");
464        Ok(())
465    }
466
467    /// Write samples to playback buffer
468    pub fn write(&mut self, samples: &Array1<f32>) -> IoResult<()> {
469        let mut buffer = self
470            .buffer
471            .lock()
472            .map_err(|_| IoError::StreamError("Buffer lock failed".into()))?;
473
474        buffer.extend(samples.iter());
475        debug!("Wrote {} samples to output buffer", samples.len());
476        Ok(())
477    }
478
479    /// Write multi-channel samples (interleaved)
480    pub fn write_channels(&mut self, samples: &Array2<f32>) -> IoResult<()> {
481        let mut buffer = self
482            .buffer
483            .lock()
484            .map_err(|_| IoError::StreamError("Buffer lock failed".into()))?;
485
486        // Interleave channels
487        for row in samples.outer_iter() {
488            buffer.extend(row.iter());
489        }
490
491        debug!(
492            "Wrote {} frames from {} channels to output buffer",
493            samples.nrows(),
494            samples.ncols()
495        );
496        Ok(())
497    }
498
499    /// Get buffer level (number of samples queued)
500    pub fn buffer_level(&self) -> usize {
501        self.buffer.lock().map(|b| b.len()).unwrap_or(0)
502    }
503
504    /// Get underrun count
505    pub fn underrun_count(&self) -> usize {
506        self.underrun_count.lock().map(|c| *c).unwrap_or(0)
507    }
508
509    /// Clear buffer
510    pub fn clear_buffer(&mut self) -> IoResult<()> {
511        let mut buffer = self
512            .buffer
513            .lock()
514            .map_err(|_| IoError::StreamError("Buffer lock failed".into()))?;
515
516        buffer.clear();
517        Ok(())
518    }
519
520    /// Get available output devices for a specific backend
521    pub fn list_devices_with_backend(backend: AudioBackend) -> IoResult<Vec<String>> {
522        let host = get_host(backend)?;
523        let devices = host
524            .output_devices()
525            .map_err(|e| IoError::ConfigError(e.to_string()))?;
526
527        let names: Vec<String> = devices.filter_map(|d| d.name().ok()).collect();
528        Ok(names)
529    }
530
531    /// Get available output devices (using default backend)
532    pub fn list_devices() -> IoResult<Vec<String>> {
533        Self::list_devices_with_backend(AudioBackend::Default)
534    }
535
536    /// Play a WAV file
537    #[cfg(feature = "file")]
538    pub async fn play_wav_file(&mut self, path: &str) -> IoResult<()> {
539        let reader = WavReader::open(path).await?;
540        let spec = reader.spec();
541
542        // Check compatibility
543        if spec.sample_rate != self.audio_config.sample_rate {
544            warn!(
545                "WAV sample rate ({}) differs from output config ({})",
546                spec.sample_rate, self.audio_config.sample_rate
547            );
548        }
549
550        if spec.channels != self.audio_config.channels {
551            return Err(IoError::ConfigError(format!(
552                "WAV channels ({}) differ from output config ({})",
553                spec.channels, self.audio_config.channels
554            )));
555        }
556
557        // Load and write samples
558        let samples = reader.read_all().await?;
559        self.write(&samples)?;
560
561        info!("Loaded WAV file: {} samples", samples.len());
562        Ok(())
563    }
564
565    /// Check if active
566    pub fn is_active(&self) -> bool {
567        self.active
568    }
569}
570
571#[cfg(test)]
572mod tests {
573    use super::*;
574
575    #[test]
576    fn test_audio_config() {
577        let config = AudioConfig::new()
578            .sample_rate(48000)
579            .channels(2)
580            .buffer_size(2048);
581
582        assert_eq!(config.sample_rate, 48000);
583        assert_eq!(config.channels, 2);
584        assert_eq!(config.buffer_size, 2048);
585        assert!(!config.output);
586    }
587
588    #[test]
589    fn test_audio_config_output() {
590        let config = AudioConfig::new_output().sample_rate(44100).channels(1);
591
592        assert_eq!(config.sample_rate, 44100);
593        assert_eq!(config.channels, 1);
594        assert!(config.output);
595    }
596
597    #[test]
598    fn test_list_input_devices() {
599        let result = AudioInput::list_devices();
600        assert!(result.is_ok());
601    }
602
603    #[test]
604    fn test_list_output_devices() {
605        let result = AudioOutput::list_devices();
606        assert!(result.is_ok());
607    }
608}