use anyhow::{anyhow, Context};
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
use tokio::sync::mpsc;
#[derive(Debug, Clone, Copy)]
pub struct CaptureInfo {
pub sample_rate: u32,
pub channels: u16,
}
pub enum CaptureKeepAlive {
Cpal(cpal::Stream),
#[cfg(target_os = "macos")]
Sck(crate::audio::loopback::loopback_macos::LoopbackKeepAlive),
#[cfg(target_os = "windows")]
Wasapi(crate::audio::loopback::loopback_windows::WasapiKeepAlive),
}
pub struct CaptureHandle {
pub frames_rx: mpsc::Receiver<Vec<f32>>,
pub info: CaptureInfo,
pub stream: CaptureKeepAlive,
}
impl CaptureHandle {
pub fn split(self) -> (mpsc::Receiver<Vec<f32>>, CaptureInfo, CaptureKeepAlive) {
(self.frames_rx, self.info, self.stream)
}
}
pub fn capture_default_input() -> anyhow::Result<CaptureHandle> {
capture_input(None)
}
pub fn capture_input(name: Option<&str>) -> anyhow::Result<CaptureHandle> {
let host = cpal::default_host();
let device = match name {
None => host
.default_input_device()
.ok_or_else(|| anyhow!("no default input device"))?,
Some(want) => {
let mut found = None;
if let Ok(devices) = host.input_devices() {
for d in devices {
if matches!(d.name(), Ok(n) if n == want) {
found = Some(d);
break;
}
}
}
found.ok_or_else(|| anyhow!("input device not found: {want}"))?
}
};
let config = device
.default_input_config()
.context("default input config")?;
let sample_rate = config.sample_rate().0;
let channels = config.channels();
let format = config.sample_format();
let (tx, rx) = mpsc::channel::<Vec<f32>>(64);
let err_fn = |e| log::error!("cpal stream error: {e}");
let stream = match format {
cpal::SampleFormat::F32 => {
let tx = tx.clone();
device.build_input_stream(
&config.into(),
move |data: &[f32], _: &cpal::InputCallbackInfo| {
let _ = tx.blocking_send(data.to_vec());
},
err_fn,
None,
)
}
cpal::SampleFormat::I16 => {
let tx = tx.clone();
device.build_input_stream(
&config.into(),
move |data: &[i16], _: &cpal::InputCallbackInfo| {
let v: Vec<f32> = data.iter().map(|s| *s as f32 / i16::MAX as f32).collect();
let _ = tx.blocking_send(v);
},
err_fn,
None,
)
}
cpal::SampleFormat::U16 => {
let tx = tx.clone();
device.build_input_stream(
&config.into(),
move |data: &[u16], _: &cpal::InputCallbackInfo| {
let v: Vec<f32> = data
.iter()
.map(|s| (*s as f32 - 32768.0) / 32768.0)
.collect();
let _ = tx.blocking_send(v);
},
err_fn,
None,
)
}
f => return Err(anyhow!("unsupported sample format: {f:?}")),
}
.context("build input stream")?;
stream.play().context("stream.play()")?;
Ok(CaptureHandle {
frames_rx: rx,
info: CaptureInfo {
sample_rate,
channels,
},
stream: CaptureKeepAlive::Cpal(stream),
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn capture_input_with_unknown_name_returns_err() {
match capture_input(Some("___definitely_not_a_real_device___")) {
Ok(_) => panic!("expected Err for bogus device name"),
Err(e) => {
let msg = format!("{e}");
assert!(
msg.contains("not found"),
"error message should mention 'not found', got: {msg}"
);
}
}
}
}