use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::thread;
use std::time::{Duration, Instant};
use anyhow::{Context, Result};
use clap::Parser;
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
use hound::WavWriter;
use ringbuf::HeapRb;
use ringbuf::traits::{Consumer, Observer, Producer, Split};
use sonora::config::{EchoCanceller, GainController2, NoiseSuppression};
use sonora::{AudioProcessing, Config, StreamConfig};
#[allow(dead_code, reason = "shared helpers for multi-channel examples")]
mod common;
const SAMPLE_RATE: u32 = 48_000;
const NUM_CHANNELS: u16 = 1;
const FRAME_SIZE: usize = (SAMPLE_RATE / 100) as usize;
#[derive(Parser, Debug)]
#[command(about = "Record and process microphone audio through sonora")]
struct Args {
#[arg(short, long, default_value_t = 5)]
duration: u64,
#[arg(long, default_value = "raw.wav")]
raw_output: String,
#[arg(long, default_value = "processed.wav")]
processed_output: String,
#[arg(long)]
aec: bool,
#[arg(long)]
ns: bool,
#[arg(long)]
agc: bool,
}
fn main() -> Result<()> {
let args = Args::parse();
let running = Arc::new(AtomicBool::new(true));
ctrlc::set_handler({
let running = running.clone();
move || running.store(false, Ordering::SeqCst)
})?;
let host = cpal::default_host();
let input_device = host
.default_input_device()
.context("no input device available")?;
println!("Recording from: {}", input_device.name()?);
let cpal_config = cpal::StreamConfig {
channels: NUM_CHANNELS,
sample_rate: cpal::SampleRate(SAMPLE_RATE),
buffer_size: cpal::BufferSize::Default,
};
let ring_size = FRAME_SIZE * 8;
let (mut prod, mut cons) = HeapRb::<f32>::new(ring_size).split();
let input_stream = input_device.build_input_stream(
&cpal_config,
move |data: &[f32], _: &cpal::InputCallbackInfo| {
prod.push_slice(data);
},
|err| eprintln!("input error: {err}"),
None,
)?;
input_stream.play()?;
let config = Config {
echo_canceller: if args.aec {
Some(EchoCanceller::default())
} else {
None
},
noise_suppression: if args.ns {
Some(NoiseSuppression::default())
} else {
None
},
gain_controller2: if args.agc {
Some(GainController2::default())
} else {
None
},
..Default::default()
};
let stream_config = StreamConfig::new(SAMPLE_RATE, NUM_CHANNELS);
let mut apm = AudioProcessing::builder()
.config(config)
.capture_config(stream_config)
.render_config(stream_config)
.build();
let spec = hound::WavSpec {
channels: NUM_CHANNELS,
sample_rate: SAMPLE_RATE,
bits_per_sample: 32,
sample_format: hound::SampleFormat::Float,
};
let mut raw_writer = WavWriter::create(&args.raw_output, spec)?;
let mut proc_writer = WavWriter::create(&args.processed_output, spec)?;
println!(
"Recording for {} seconds (Ctrl+C to stop early)...",
args.duration
);
let deadline = Instant::now() + Duration::from_secs(args.duration);
let mut input_buf = vec![0.0f32; FRAME_SIZE];
let mut output_buf = vec![0.0f32; FRAME_SIZE];
while running.load(Ordering::SeqCst) && Instant::now() < deadline {
if cons.occupied_len() < FRAME_SIZE {
thread::sleep(Duration::from_millis(1));
continue;
}
cons.pop_slice(&mut input_buf);
for &s in &input_buf {
raw_writer.write_sample(s)?;
}
apm.process_capture_f32(&[&input_buf], &mut [&mut output_buf])
.unwrap();
for &s in &output_buf {
proc_writer.write_sample(s)?;
}
}
raw_writer.finalize()?;
proc_writer.finalize()?;
println!("Wrote {} and {}", args.raw_output, args.processed_output);
Ok(())
}