101 lines
3.6 KiB
Rust
101 lines
3.6 KiB
Rust
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
|
|
use crossbeam_channel::Sender;
|
|
|
|
pub struct AudioCapture {
|
|
_stream: cpal::Stream,
|
|
}
|
|
|
|
pub fn get_device_name(dev: &cpal::Device) -> String {
|
|
dev.description()
|
|
.map(|d| d.name().to_string())
|
|
.unwrap_or_else(|_| dev.to_string())
|
|
}
|
|
|
|
pub fn list_input_devices() -> Vec<String> {
|
|
let host = cpal::default_host();
|
|
let mut names = Vec::new();
|
|
if let Ok(devices) = host.input_devices() {
|
|
for dev in devices {
|
|
let name = get_device_name(&dev);
|
|
if !names.contains(&name) {
|
|
names.push(name);
|
|
}
|
|
}
|
|
}
|
|
names
|
|
}
|
|
|
|
impl AudioCapture {
|
|
pub fn new(sender: Sender<Vec<f32>>) -> Result<(Self, u32, u16), Box<dyn std::error::Error + Send + Sync>> {
|
|
Self::new_with_device(sender, None)
|
|
}
|
|
|
|
pub fn new_with_device(
|
|
sender: Sender<Vec<f32>>,
|
|
device_name: Option<&str>,
|
|
) -> Result<(Self, u32, u16), Box<dyn std::error::Error + Send + Sync>> {
|
|
let host = cpal::default_host();
|
|
let device = if let Some(target) = device_name
|
|
&& target != "default"
|
|
&& !target.trim().is_empty() {
|
|
if let Ok(mut devs) = host.input_devices() {
|
|
devs.find(|d| get_device_name(d) == target)
|
|
.or_else(|| host.default_input_device())
|
|
.ok_or("Audio device not found")?
|
|
} else {
|
|
host.default_input_device().ok_or("No audio input device available")?
|
|
}
|
|
} else {
|
|
// When default is selected, prefer desktop audio monitor if available so gameplay sound is recorded
|
|
if let Ok(mut devs) = host.input_devices() {
|
|
devs.find(|d| {
|
|
let n = get_device_name(d).to_lowercase();
|
|
n.contains("monitor")
|
|
})
|
|
.or_else(|| host.default_input_device())
|
|
.ok_or("No audio input device available")?
|
|
} else {
|
|
host.default_input_device().ok_or("No audio input device available")?
|
|
}
|
|
};
|
|
|
|
let dev_name = get_device_name(&device);
|
|
let config = device.default_input_config()?;
|
|
let sample_rate = config.sample_rate();
|
|
let channels = config.channels();
|
|
|
|
println!("Audio capture initialized on [{}]: {} Hz, {} channels", dev_name, sample_rate, channels);
|
|
|
|
let err_fn = |err| eprintln!("Audio stream error: {}", err);
|
|
let stream_config: cpal::StreamConfig = config.into();
|
|
|
|
let stream = match config.sample_format() {
|
|
cpal::SampleFormat::F32 => {
|
|
device.build_input_stream(
|
|
stream_config,
|
|
move |data: &[f32], _: &_| {
|
|
let _ = sender.try_send(data.to_vec());
|
|
},
|
|
err_fn,
|
|
None,
|
|
)?
|
|
}
|
|
cpal::SampleFormat::I16 => {
|
|
device.build_input_stream(
|
|
stream_config,
|
|
move |data: &[i16], _: &_| {
|
|
let f32_data: Vec<f32> = data.iter().map(|&s| s as f32 / i16::MAX as f32).collect();
|
|
let _ = sender.try_send(f32_data);
|
|
},
|
|
err_fn,
|
|
None,
|
|
)?
|
|
}
|
|
_ => return Err("Unsupported audio format".into()),
|
|
};
|
|
|
|
stream.play()?;
|
|
|
|
Ok((Self { _stream: stream }, sample_rate, channels))
|
|
}
|
|
}
|