use anyhow::{Context, Result};
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
use hound::{WavSpec, WavWriter};
use std::path::Path;
use std::sync::{Arc, Mutex};
pub struct LiveInput {
stream: Option<cpal::Stream>,
writer: Option<Arc<Mutex<WavWriter<std::io::BufWriter<std::fs::File>>>>>,
sample_rate: u32,
channels: u16,
}
impl LiveInput {
pub fn new() -> Result<Self> {
let host = cpal::default_host();
let device = host
.default_input_device()
.context("No input device available")?;
let config = device
.default_input_config()
.context("Failed to get default input config")?;
Ok(Self {
stream: None,
writer: None,
sample_rate: config.sample_rate().0,
channels: config.channels(),
})
}
pub fn start_recording(&mut self, path: impl AsRef<Path>) -> Result<()> {
if self.stream.is_some() {
anyhow::bail!("Already recording. Call stop() first.");
}
let spec = WavSpec {
channels: self.channels,
sample_rate: self.sample_rate,
bits_per_sample: 32,
sample_format: hound::SampleFormat::Float,
};
let writer = WavWriter::create(path.as_ref(), spec)
.context("Failed to create WAV file")?;
let writer = Arc::new(Mutex::new(writer));
self.writer = Some(Arc::clone(&writer));
let host = cpal::default_host();
let device = host
.default_input_device()
.context("No input device available")?;
let config = device
.default_input_config()
.context("Failed to get default input config")?;
let err_fn = |err| eprintln!("Audio stream error: {}", err);
let stream = match config.sample_format() {
cpal::SampleFormat::F32 => {
let config: cpal::StreamConfig = config.into();
device.build_input_stream(
&config,
move |data: &[f32], _: &cpal::InputCallbackInfo| {
if let Ok(mut writer) = writer.lock() {
for &sample in data {
writer.write_sample(sample).ok();
}
}
},
err_fn,
None,
)?
}
cpal::SampleFormat::I16 => {
let config: cpal::StreamConfig = config.into();
device.build_input_stream(
&config,
move |data: &[i16], _: &cpal::InputCallbackInfo| {
if let Ok(mut writer) = writer.lock() {
for &sample in data {
let normalized = sample as f32 / i16::MAX as f32;
writer.write_sample(normalized).ok();
}
}
},
err_fn,
None,
)?
}
cpal::SampleFormat::U16 => {
let config: cpal::StreamConfig = config.into();
device.build_input_stream(
&config,
move |data: &[u16], _: &cpal::InputCallbackInfo| {
if let Ok(mut writer) = writer.lock() {
for &sample in data {
let normalized = (sample as f32 - 32768.0) / 32768.0;
writer.write_sample(normalized).ok();
}
}
},
err_fn,
None,
)?
}
_ => anyhow::bail!("Unsupported sample format"),
};
stream.play()?;
self.stream = Some(stream);
println!(
"🎙️ Recording at {} Hz, {} channel(s)",
self.sample_rate, self.channels
);
Ok(())
}
pub fn stop(&mut self) -> Result<()> {
self.stream.take();
if let Some(writer) = self.writer.take() {
match Arc::try_unwrap(writer) {
Ok(mutex) => {
let writer = mutex.into_inner().unwrap();
writer.finalize().context("Failed to finalize WAV file")?;
}
Err(arc) => {
if let Ok(_writer) = arc.lock() {
}
}
}
}
println!("✅ Recording stopped");
Ok(())
}
pub fn sample_rate(&self) -> u32 {
self.sample_rate
}
pub fn channels(&self) -> u16 {
self.channels
}
pub fn is_recording(&self) -> bool {
self.stream.is_some()
}
}
impl Drop for LiveInput {
fn drop(&mut self) {
if self.is_recording() {
self.stop().ok();
}
}
}