//! Feeds back the input stream directly into the output stream. //! //! Assumes that the input and output devices can use the same stream configuration and that they //! support the f32 sample format. //! //! Uses a delay of `LATENCY_MS` milliseconds in case the default input and output streams are not //! precisely synchronised. use clap::Parser; use cpal::{ traits::{DeviceTrait, HostTrait, StreamTrait}, HostUnavailable, }; use ringbuf::{ traits::{Consumer, Producer, Split}, HeapRb, }; #[derive(Parser, Debug)] #[command(version, about = "CPAL feedback example", long_about = None)] struct Opt { /// The input audio device to use #[arg(short, long, value_name = "IN")] input_device: Option, /// The output audio device to use #[arg(short, long, value_name = "OUT")] output_device: Option, /// Specify the delay between input and output #[arg(short, long, value_name = "DELAY_MS", default_value_t = 150.0)] latency: f32, /// 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, } fn main() -> anyhow::Result<()> { 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); #[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); } } // 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 { 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 { cpal::default_host() }; // Find devices. let input_device = if let Some(device) = opt.input_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"); let output_device = if let Some(device) = opt.output_device { let id = &device.parse().expect("failed to parse output device id"); host.device_by_id(id) } else { host.default_output_device() } .expect("failed to find output device"); println!("Using input device: \"{}\"", input_device.id()?); println!("Using output device: \"{}\"", output_device.id()?); // We'll try and use the same configuration between streams to keep it simple. let config: cpal::StreamConfig = input_device.default_input_config()?.into(); // Create a delay in case the input and output devices aren't synced. let latency_frames = (opt.latency / 1_000.0) * config.sample_rate as f32; let latency_samples = latency_frames as usize * config.channels as usize; // The buffer to share samples let ring = HeapRb::::new(latency_samples * 2); let (mut producer, mut consumer) = ring.split(); // Fill the samples with 0.0 equal to the length of the delay. for _ in 0..latency_samples { // The ring buffer has twice as much space as necessary to add latency here, // so this should never fail producer.try_push(0.0).unwrap(); } let input_data_fn = move |data: &[f32], _: &cpal::InputCallbackInfo| { let mut output_fell_behind = false; for &sample in data { if producer.try_push(sample).is_err() { output_fell_behind = true; } } if output_fell_behind { eprintln!("output stream fell behind: try increasing latency"); } }; let output_data_fn = move |data: &mut [f32], _: &cpal::OutputCallbackInfo| { let mut input_fell_behind = false; for sample in data { *sample = match consumer.try_pop() { Some(s) => s, None => { input_fell_behind = true; 0.0 } }; } if input_fell_behind { eprintln!("input stream fell behind: try increasing latency"); } }; // Build streams. println!("Attempting to build both streams with f32 samples and `{config:?}`."); let input_stream = input_device.build_input_stream(config, input_data_fn, err_fn, None)?; let output_stream = output_device.build_output_stream(config, output_data_fn, err_fn, None)?; println!("Successfully built streams."); // Play the streams. println!( "Starting the input and output streams with `{}` milliseconds of latency.", opt.latency ); input_stream.play()?; output_stream.play()?; // Run for 10 seconds before closing. println!("Playing for 10 seconds... "); std::thread::sleep(std::time::Duration::from_secs(10)); drop(input_stream); drop(output_stream); println!("Done!"); Ok(()) } fn err_fn(err: cpal::StreamError) { eprintln!("an error occurred on stream: {err}"); }