diff --git a/tuitar/src/input.rs b/tuitar/src/input.rs index 5dc2715..fc8af8f 100644 --- a/tuitar/src/input.rs +++ b/tuitar/src/input.rs @@ -1,5 +1,5 @@ use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; -use cpal::{Stream, StreamConfig}; +use cpal::{FromSample, Sample, SampleFormat, SizedSample, Stream, StreamConfig}; pub struct Recorder { stream: Stream, @@ -13,45 +13,199 @@ impl Recorder { { let host = cpal::default_host(); - let device = host - .input_devices() - .unwrap() - .find(|d| d.name().unwrap().contains(device_name)) - .expect("No pipewire input device found"); + let requested = device_name.to_lowercase(); + let mut matching_device = None; - println!("Using input device: {:?}", device.name().unwrap()); + match host.input_devices() { + Ok(devices) => { + for device in devices { + match device.name() { + Ok(name) => { + if name.to_lowercase().contains(&requested) { + matching_device = Some((device, name)); + break; + } + } + Err(err) => { + eprintln!("Skipping input device with unreadable name: {err}"); + } + } + } + } + Err(err) => { + eprintln!("Failed to enumerate input devices: {err}"); + } + } + + let (device, device_name) = if let Some(device) = matching_device { + device + } else { + let fallback = host + .default_input_device() + .expect("No audio input devices detected"); + + let name = fallback + .name() + .unwrap_or_else(|err| format!("unknown device ({err})")); + println!("Falling back to default input device: {name}"); + (fallback, name) + }; + + println!("Using input device: {device_name}"); + + let supported_config = match device.default_input_config() { + Ok(config) => config, + Err(err) => { + eprintln!("Failed to fetch default input config: {err}"); + + let mut chosen_config = None; + match device.supported_input_configs() { + Ok(mut configs) => { + while let Some(range) = configs.next() { + let config = range.with_max_sample_rate(); - let mut supported_configs_range = device.supported_input_configs().unwrap(); - let supported_config = supported_configs_range - .next() - .expect("no supported config") - .with_max_sample_rate() - .config(); + if config.sample_format() == SampleFormat::I16 { + chosen_config = Some(config); + break; + } - println!("Using config: {supported_config:?}"); + if chosen_config.is_none() { + chosen_config = Some(config); + } + } + } + Err(list_err) => { + eprintln!("Unable to read supported input configs: {list_err}"); + } + } + chosen_config.expect("Device has no supported audio configs") + } + }; + + let sample_format = supported_config.sample_format(); + let stream_config = supported_config.config(); + + println!("Using config: {stream_config:?}"); + println!("Using sample format: {sample_format:?}"); + + let mut device = Some(device); + let mut stream_config = Some(stream_config); + let mut callback = Some(callback); + + match sample_format { + SampleFormat::I16 => Self::build_passthrough( + device.take().unwrap(), + stream_config.take().unwrap(), + callback.take().unwrap(), + ), + SampleFormat::U16 => Self::build_stream_with_conversion::( + device.take().unwrap(), + stream_config.take().unwrap(), + callback.take().unwrap(), + ), + SampleFormat::I32 => Self::build_stream_with_conversion::( + device.take().unwrap(), + stream_config.take().unwrap(), + callback.take().unwrap(), + ), + SampleFormat::U32 => Self::build_stream_with_conversion::( + device.take().unwrap(), + stream_config.take().unwrap(), + callback.take().unwrap(), + ), + SampleFormat::F32 => Self::build_stream_with_conversion::( + device.take().unwrap(), + stream_config.take().unwrap(), + callback.take().unwrap(), + ), + SampleFormat::F64 => Self::build_stream_with_conversion::( + device.take().unwrap(), + stream_config.take().unwrap(), + callback.take().unwrap(), + ), + SampleFormat::I8 => Self::build_stream_with_conversion::( + device.take().unwrap(), + stream_config.take().unwrap(), + callback.take().unwrap(), + ), + SampleFormat::U8 => Self::build_stream_with_conversion::( + device.take().unwrap(), + stream_config.take().unwrap(), + callback.take().unwrap(), + ), + SampleFormat::I64 => Self::build_stream_with_conversion::( + device.take().unwrap(), + stream_config.take().unwrap(), + callback.take().unwrap(), + ), + SampleFormat::U64 => Self::build_stream_with_conversion::( + device.take().unwrap(), + stream_config.take().unwrap(), + callback.take().unwrap(), + ), + _ => panic!("Unsupported sample format: {sample_format:?}"), + } + } + + pub fn start(&self) { + self.stream.play().unwrap(); + } + + pub fn sample_rate(&self) -> u32 { + self.config.sample_rate.0 + } +} + +impl Recorder { + fn build_passthrough(device: cpal::Device, config: StreamConfig, mut callback: D) -> Self + where + D: FnMut(&[i16], &cpal::InputCallbackInfo) + Send + 'static, + { let stream = device .build_input_stream( - &supported_config, - callback, + &config, + move |data: &[i16], info| { + callback(data, info); + }, |err| { panic!("Error: {err}"); }, None, ) - .unwrap(); + .unwrap_or_else(|err| panic!("Failed to build input stream: {err}")); - Self { - stream, - config: supported_config, - } + Self { stream, config } } - pub fn start(&self) { - self.stream.play().unwrap(); - } + fn build_stream_with_conversion( + device: cpal::Device, + config: StreamConfig, + mut callback: D, + ) -> Self + where + T: Sample + SizedSample, + i16: FromSample, + D: FnMut(&[i16], &cpal::InputCallbackInfo) + Send + 'static, + { + let mut scratch: Vec = Vec::new(); - pub fn sample_rate(&self) -> u32 { - self.config.sample_rate.0 + let stream = device + .build_input_stream( + &config, + move |data: &[T], info| { + scratch.clear(); + scratch.reserve(data.len()); + scratch.extend(data.iter().map(|sample| (*sample).to_sample::())); + callback(&scratch, info); + }, + |err| { + panic!("Error: {err}"); + }, + None, + ) + .unwrap_or_else(|err| panic!("Failed to build input stream: {err}")); + + Self { stream, config } } }