use crate::audio::AudioError;
pub trait CaptureBackend: Send {
fn open(device: Option<&str>, config: &CaptureConfig) -> Result<Self, AudioError>
where
Self: Sized;
fn read(&mut self, buffer: &mut [f32]) -> Result<usize, AudioError>;
fn close(&mut self) -> Result<(), AudioError>;
fn backend_name() -> &'static str
where
Self: Sized;
}
#[cfg(all(target_os = "linux", feature = "audio-alsa"))]
pub struct AlsaBackend {
pcm: alsa::PCM,
config: CaptureConfig,
i16_buffer: Vec<i16>,
}
#[cfg(all(target_os = "linux", feature = "audio-alsa"))]
impl std::fmt::Debug for AlsaBackend {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("AlsaBackend")
.field("config", &self.config)
.field("buffer_size", &self.i16_buffer.len())
.finish_non_exhaustive()
}
}
#[cfg(all(target_os = "linux", feature = "audio-alsa"))]
impl AlsaBackend {
pub fn list_devices() -> Result<Vec<AudioDevice>, AudioError> {
use alsa::device_name::HintIter;
use std::ffi::CStr;
let mut devices = Vec::new();
let pcm_cstr = CStr::from_bytes_with_nul(b"pcm\0")
.map_err(|e| AudioError::CaptureError(format!("Invalid CStr: {e}")))?;
let hints = HintIter::new(None, pcm_cstr)
.map_err(|e| AudioError::CaptureError(format!("Failed to enumerate devices: {e}")))?;
for hint in hints {
if let Some(name) = hint.name {
if name == "null" || name.contains("surround") {
continue;
}
let desc = hint.desc.unwrap_or_else(String::new);
let is_default = name == "default" || name == "pulse";
devices.push(AudioDevice {
id: name.clone(),
name: desc.lines().next().unwrap_or(&name).to_string(),
max_sample_rate: 48000, input_channels: 2,
is_default,
});
}
}
Ok(devices)
}
#[inline]
fn i16_to_f32(sample: i16) -> f32 {
if sample >= 0 {
f32::from(sample) / 32767.0
} else {
f32::from(sample) / 32768.0
}
}
}
#[cfg(all(target_os = "linux", feature = "audio-alsa"))]
impl CaptureBackend for AlsaBackend {
fn open(device: Option<&str>, config: &CaptureConfig) -> Result<Self, AudioError> {
use alsa::pcm::{Access, Format, HwParams};
use alsa::{Direction, ValueOr};
config.validate()?;
let device_name = device.unwrap_or("default");
let pcm = alsa::PCM::new(device_name, Direction::Capture, false).map_err(|e| {
AudioError::CaptureError(format!("Failed to open ALSA device '{device_name}': {e}"))
})?;
{
let hwp = HwParams::any(&pcm)
.map_err(|e| AudioError::CaptureError(format!("Failed to get HW params: {e}")))?;
hwp.set_access(Access::RWInterleaved)
.map_err(|e| AudioError::CaptureError(format!("Failed to set access: {e}")))?;
hwp.set_format(Format::s16())
.map_err(|e| AudioError::CaptureError(format!("Failed to set format: {e}")))?;
hwp.set_rate(config.sample_rate, ValueOr::Nearest)
.map_err(|e| AudioError::CaptureError(format!("Failed to set rate: {e}")))?;
hwp.set_channels(u32::from(config.channels))
.map_err(|e| AudioError::CaptureError(format!("Failed to set channels: {e}")))?;
let buffer_frames = config.buffer_size / config.channels as usize;
hwp.set_buffer_size_near((buffer_frames * 4) as i64)
.map_err(|e| AudioError::CaptureError(format!("Failed to set buffer size: {e}")))?;
hwp.set_period_size_near(buffer_frames as i64, ValueOr::Nearest)
.map_err(|e| AudioError::CaptureError(format!("Failed to set period size: {e}")))?;
pcm.hw_params(&hwp)
.map_err(|e| AudioError::CaptureError(format!("Failed to apply HW params: {e}")))?;
}
pcm.prepare()
.map_err(|e| AudioError::CaptureError(format!("Failed to prepare PCM: {e}")))?;
let i16_buffer = vec![0i16; config.buffer_size * config.channels as usize];
Ok(Self {
pcm,
config: config.clone(),
i16_buffer,
})
}
fn read(&mut self, buffer: &mut [f32]) -> Result<usize, AudioError> {
let samples_needed = buffer.len();
if self.i16_buffer.len() < samples_needed {
self.i16_buffer.resize(samples_needed, 0);
}
let io = self
.pcm
.io_i16()
.map_err(|e| AudioError::CaptureError(format!("Failed to get IO interface: {e}")))?;
let frames_read = match io.readi(&mut self.i16_buffer[..samples_needed]) {
Ok(n) => n,
Err(e) => {
if e.errno() == -32 {
self.pcm.prepare().map_err(|e| {
AudioError::CaptureError(format!("Failed to recover from xrun: {e}"))
})?;
io.readi(&mut self.i16_buffer[..samples_needed])
.map_err(|e| {
AudioError::CaptureError(format!("Failed to read after recovery: {e}"))
})?
} else {
return Err(AudioError::CaptureError(format!("Failed to read: {e}")));
}
}
};
let samples_read = frames_read * self.config.channels as usize;
for (i, &sample) in self.i16_buffer[..samples_read].iter().enumerate() {
buffer[i] = Self::i16_to_f32(sample);
}
Ok(samples_read)
}
fn close(&mut self) -> Result<(), AudioError> {
self.pcm
.drain()
.map_err(|e| AudioError::CaptureError(format!("Failed to drain PCM: {e}")))?;
Ok(())
}
fn backend_name() -> &'static str {
"ALSA"
}
}
#[cfg(all(target_os = "macos", feature = "audio-coreaudio"))]
pub struct CoreAudioBackend {
config: CaptureConfig,
}
#[cfg(all(target_os = "macos", feature = "audio-coreaudio"))]
impl CaptureBackend for CoreAudioBackend {
fn open(_device: Option<&str>, config: &CaptureConfig) -> Result<Self, AudioError> {
config.validate()?;
Err(AudioError::NotImplemented(
"CoreAudio backend pending implementation - requires coreaudio-rs dependency"
.to_string(),
))
}
fn read(&mut self, _buffer: &mut [f32]) -> Result<usize, AudioError> {
Err(AudioError::NotImplemented("CoreAudio read".to_string()))
}
fn close(&mut self) -> Result<(), AudioError> {
Ok(())
}
fn backend_name() -> &'static str {
"CoreAudio"
}
}
#[cfg(all(target_os = "windows", feature = "audio-wasapi"))]
pub struct WasapiBackend {
config: CaptureConfig,
}
#[cfg(all(target_os = "windows", feature = "audio-wasapi"))]
impl CaptureBackend for WasapiBackend {
fn open(_device: Option<&str>, config: &CaptureConfig) -> Result<Self, AudioError> {
config.validate()?;
Err(AudioError::NotImplemented(
"WASAPI backend pending implementation - requires wasapi dependency".to_string(),
))
}
fn read(&mut self, _buffer: &mut [f32]) -> Result<usize, AudioError> {
Err(AudioError::NotImplemented("WASAPI read".to_string()))
}
fn close(&mut self) -> Result<(), AudioError> {
Ok(())
}
fn backend_name() -> &'static str {
"WASAPI"
}
}
#[cfg(all(target_arch = "wasm32", feature = "audio-webaudio"))]
pub struct WebAudioBackend {
config: CaptureConfig,
}
#[cfg(all(target_arch = "wasm32", feature = "audio-webaudio"))]
impl CaptureBackend for WebAudioBackend {
fn open(_device: Option<&str>, config: &CaptureConfig) -> Result<Self, AudioError> {
config.validate()?;
Err(AudioError::NotImplemented(
"WebAudio backend pending implementation - requires web-sys dependency".to_string(),
))
}
fn read(&mut self, _buffer: &mut [f32]) -> Result<usize, AudioError> {
Err(AudioError::NotImplemented("WebAudio read".to_string()))
}
fn close(&mut self) -> Result<(), AudioError> {
Ok(())
}
fn backend_name() -> &'static str {
"WebAudio"
}
}
include!("audio.rs");
include!("buffer_capture_source.rs");