//! Records a WAV file (roughly 3 seconds long) using the default input device and config. //! //! The input data is recorded to "$CARGO_MANIFEST_DIR/recorded.wav". use clap::Parser; use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; use cpal::{FromSample, Sample}; use std::fs::File; use std::io::BufWriter; use std::sync::{Arc, Mutex}; #[derive(Parser, Debug)] #[command(version, about = "CPAL record_wav example", long_about = None)] struct Opt { /// The audio device to use #[arg(short, long, default_value_t = String::from("default"))] device: String, /// Use the JACK host #[cfg(all( any( target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "netbsd" ), feature = "jack" ))] #[arg(short, long)] #[allow(dead_code)] jack: bool, } fn main() -> Result<(), anyhow::Error> { let opt = Opt::parse(); // Conditionally compile with jack if the feature is specified. #[cfg(all( any( target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "netbsd" ), feature = "jack" ))] // Manually check for flags. Can be passed through cargo with -- e.g. // cargo run --release --example beep --features jack -- --jack let host = if opt.jack { cpal::host_from_id(cpal::available_hosts() .into_iter() .find(|id| *id == cpal::HostId::Jack) .expect( "make sure --features jack is specified. only works on OSes where jack is available", )).expect("jack host unavailable") } else { cpal::default_host() }; #[cfg(any( not(any( target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "netbsd" )), not(feature = "jack") ))] let host = cpal::default_host(); // Set up the input device and stream with the default input config. let device = if opt.device == "default" { host.default_input_device() } else { host.input_devices()? .find(|x| x.name().map(|y| y == opt.device).unwrap_or(false)) } .expect("failed to find input device"); println!("Input device: {}", device.name()?); let config = device .default_input_config() .expect("Failed to get default input config"); println!("Default input config: {:?}", config); // The WAV file we're recording to. const PATH: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/recorded.wav"); let spec = wav_spec_from_config(&config); let writer = hound::WavWriter::create(PATH, spec)?; let writer = Arc::new(Mutex::new(Some(writer))); // A flag to indicate that recording is in progress. println!("Begin recording..."); // Run the input stream on a separate thread. let writer_2 = writer.clone(); let err_fn = move |err| { eprintln!("an error occurred on stream: {}", err); }; let stream = match config.sample_format() { cpal::SampleFormat::I8 => device.build_input_stream( &config.into(), move |data, _: &_| write_input_data::(data, &writer_2), err_fn, None, )?, cpal::SampleFormat::I16 => device.build_input_stream( &config.into(), move |data, _: &_| write_input_data::(data, &writer_2), err_fn, None, )?, cpal::SampleFormat::I32 => device.build_input_stream( &config.into(), move |data, _: &_| write_input_data::(data, &writer_2), err_fn, None, )?, cpal::SampleFormat::F32 => device.build_input_stream( &config.into(), move |data, _: &_| write_input_data::(data, &writer_2), err_fn, None, )?, sample_format => { return Err(anyhow::Error::msg(format!( "Unsupported sample format '{sample_format}'" ))) } }; stream.play()?; // Let recording go for roughly three seconds. std::thread::sleep(std::time::Duration::from_secs(3)); drop(stream); writer.lock().unwrap().take().unwrap().finalize()?; println!("Recording {} complete!", PATH); Ok(()) } fn sample_format(format: cpal::SampleFormat) -> hound::SampleFormat { if format.is_float() { hound::SampleFormat::Float } else { hound::SampleFormat::Int } } fn wav_spec_from_config(config: &cpal::SupportedStreamConfig) -> hound::WavSpec { hound::WavSpec { channels: config.channels() as _, sample_rate: config.sample_rate().0 as _, bits_per_sample: (config.sample_format().sample_size() * 8) as _, sample_format: sample_format(config.sample_format()), } } type WavWriterHandle = Arc>>>>; fn write_input_data(input: &[T], writer: &WavWriterHandle) where T: Sample, U: Sample + hound::Sample + FromSample, { if let Ok(mut guard) = writer.try_lock() { if let Some(writer) = guard.as_mut() { for &sample in input.iter() { let sample: U = U::from_sample(sample); writer.write_sample(sample).ok(); } } } }