//! 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, HostUnavailable, 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)] device: Option, /// How long to record, in seconds #[arg(long, default_value_t = 3)] duration: u64, /// Use the JACK host. Requires `--features jack`. #[arg(long, default_value_t = false)] jack: bool, /// Use the PulseAudio host. Requires `--features pulseaudio`. #[arg(long, default_value_t = false)] pulseaudio: bool, /// Use the Pipewire host. Requires `--features pipewire` #[arg(long, default_value_t = false)] pipewire: bool, } fn main() -> Result<(), anyhow::Error> { let opt = Opt::parse(); // Jack/PulseAudio support must be enabled at compile time, and is // only available on some platforms. #[allow(unused_mut, unused_assignments)] let mut jack_host_id = Err(HostUnavailable); #[allow(unused_mut, unused_assignments)] let mut pulseaudio_host_id = Err(HostUnavailable); #[allow(unused_mut, unused_assignments)] let mut pipewire_host_id = Err(HostUnavailable); #[cfg(any( target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "netbsd" ))] { #[cfg(feature = "jack")] { jack_host_id = Ok(cpal::HostId::Jack); } #[cfg(feature = "pulseaudio")] { pulseaudio_host_id = Ok(cpal::HostId::PulseAudio); } #[cfg(feature = "pipewire")] { pipewire_host_id = Ok(cpal::HostId::PipeWire); } } // Manually check for flags. Can be passed through cargo with -- e.g. // cargo run --release --example record_wav --features jack -- --jack let host = if opt.jack { jack_host_id .and_then(cpal::host_from_id) .expect("make sure `--features jack` is specified, and the platform is supported") } else if opt.pulseaudio { pulseaudio_host_id .and_then(cpal::host_from_id) .expect("make sure `--features pulseaudio` is specified, and the platform is supported") } else if opt.pipewire { pipewire_host_id .and_then(cpal::host_from_id) .expect("make sure `--features pipewire` is specified, and the platform is supported") } else { cpal::default_host() }; // Set up the input device and stream with the default input config. let device = if let Some(device) = opt.device { let id = &device.parse().expect("failed to parse input device id"); host.device_by_id(id) } else { host.default_input_device() } .expect("failed to find input device"); println!("Input device: {}", device.id()?); let config = if device.supports_input() { device.default_input_config() } else { device.default_output_config() } .expect("Failed to get default input/output config"); println!("Default input/output 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(opt.duration)); drop(stream); writer.lock().unwrap().take().unwrap().finalize()?; println!("Recording {PATH} complete!"); Ok(()) } fn sample_format(format: cpal::SampleFormat) -> hound::SampleFormat { if format.is_dsd() { panic!("DSD formats cannot be written to WAV files"); } else 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() 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(); } } } }