X-Git-Url: https://code.octet-stream.net/m17rt/blobdiff_plain/6440cd74346c4b2d63d4774476e8c6113c032534..c74046ba58c5fed4c1efc2a4e06ea12325d1d4cd:/m17app/src/soundcard.rs diff --git a/m17app/src/soundcard.rs b/m17app/src/soundcard.rs index b12ef88..9209de2 100644 --- a/m17app/src/soundcard.rs +++ b/m17app/src/soundcard.rs @@ -1,4 +1,5 @@ use std::{ + borrow::Borrow, sync::{ mpsc::{sync_channel, Receiver, SyncSender}, Arc, RwLock, @@ -8,27 +9,35 @@ use std::{ use cpal::{ traits::{DeviceTrait, HostTrait, StreamTrait}, - SampleFormat, SampleRate, Stream, + BuildStreamError, DevicesError, PlayStreamError, SampleFormat, SampleRate, Stream, StreamError, + SupportedStreamConfigRange, SupportedStreamConfigsError, }; +use thiserror::Error; -use crate::{ - error::M17Error, - soundmodem::{InputSource, OutputBuffer, OutputSink, SoundmodemEvent}, +use crate::soundmodem::{ + InputSource, OutputBuffer, OutputSink, SoundmodemErrorSender, SoundmodemEvent, }; +/// A soundcard for used for transmitting/receiving baseband with a `Soundmodem`. +/// +/// Use `input()` and `output()` to retrieve source/sink handles for the soundmodem. +/// It is fine to use an input from one soundcard and and output from another. +/// +/// If you try to create more than one `Soundcard` instance at a time for the same card +/// then it may not work. pub struct Soundcard { event_tx: SyncSender, } impl Soundcard { - pub fn new>(card_name: S) -> Result { + pub fn new>(card_name: S) -> Result { let (card_tx, card_rx) = sync_channel(128); let (setup_tx, setup_rx) = sync_channel(1); spawn_soundcard_worker(card_rx, setup_tx, card_name.into()); match setup_rx.recv() { Ok(Ok(())) => Ok(Self { event_tx: card_tx }), Ok(Err(e)) => Err(e), - Err(_) => Err(M17Error::SoundcardInit), + Err(_) => Err(SoundcardError::SoundcardInit), } } @@ -52,6 +61,9 @@ impl Soundcard { let _ = self.event_tx.send(SoundcardEvent::SetTxInverted(inverted)); } + /// List soundcards supported for soundmodem output. + /// + /// Today, this requires support for a 48kHz sample rate. pub fn supported_output_cards() -> Vec { let mut out = vec![]; let host = cpal::default_host(); @@ -62,12 +74,7 @@ impl Soundcard { let Ok(mut configs) = d.supported_output_configs() else { continue; }; - if configs - .find(|config| { - config.channels() == 1 && config.sample_format() == SampleFormat::I16 - }) - .is_some() - { + if configs.any(config_is_compatible) { let Ok(name) = d.name() else { continue; }; @@ -78,6 +85,9 @@ impl Soundcard { out } + /// List soundcards supported for soundmodem input. + /// + /// Today, this requires support for a 48kHz sample rate. pub fn supported_input_cards() -> Vec { let mut out = vec![]; let host = cpal::default_host(); @@ -88,12 +98,7 @@ impl Soundcard { let Ok(mut configs) = d.supported_input_configs() else { continue; }; - if configs - .find(|config| { - config.channels() == 1 && config.sample_format() == SampleFormat::I16 - }) - .is_some() - { + if configs.any(config_is_compatible) { let Ok(name) = d.name() else { continue; }; @@ -105,16 +110,26 @@ impl Soundcard { } } +fn config_is_compatible>(config: C) -> bool { + let config = config.borrow(); + (config.channels() == 1 || config.channels() == 2) + && config.sample_format() == SampleFormat::I16 + && config.min_sample_rate().0 <= 48000 + && config.max_sample_rate().0 >= 48000 +} + enum SoundcardEvent { SetRxInverted(bool), SetTxInverted(bool), StartInput { samples: SyncSender, + errors: SoundmodemErrorSender, }, CloseInput, StartOutput { event_tx: SyncSender, buffer: Arc>, + errors: SoundmodemErrorSender, }, CloseOutput, } @@ -124,8 +139,10 @@ pub struct SoundcardInputSource { } impl InputSource for SoundcardInputSource { - fn start(&self, samples: SyncSender) { - let _ = self.event_tx.send(SoundcardEvent::StartInput { samples }); + fn start(&self, samples: SyncSender, errors: SoundmodemErrorSender) { + let _ = self + .event_tx + .send(SoundcardEvent::StartInput { samples, errors }); } fn close(&self) { @@ -138,10 +155,17 @@ pub struct SoundcardOutputSink { } impl OutputSink for SoundcardOutputSink { - fn start(&self, event_tx: SyncSender, buffer: Arc>) { - let _ = self - .event_tx - .send(SoundcardEvent::StartOutput { event_tx, buffer }); + fn start( + &self, + event_tx: SyncSender, + buffer: Arc>, + errors: SoundmodemErrorSender, + ) { + let _ = self.event_tx.send(SoundcardEvent::StartOutput { + event_tx, + buffer, + errors, + }); } fn close(&self) { @@ -151,7 +175,7 @@ impl OutputSink for SoundcardOutputSink { fn spawn_soundcard_worker( event_rx: Receiver, - setup_tx: SyncSender>, + setup_tx: SyncSender>, card_name: String, ) { std::thread::spawn(move || { @@ -161,7 +185,7 @@ fn spawn_soundcard_worker( .unwrap() .find(|d| d.name().unwrap() == card_name) else { - let _ = setup_tx.send(Err(M17Error::SoundcardNotFound(card_name))); + let _ = setup_tx.send(Err(SoundcardError::CardNotFound(card_name))); return; }; @@ -175,81 +199,122 @@ fn spawn_soundcard_worker( match ev { SoundcardEvent::SetRxInverted(inv) => rx_inverted = inv, SoundcardEvent::SetTxInverted(inv) => tx_inverted = inv, - SoundcardEvent::StartInput { samples } => { - let mut input_configs = device.supported_input_configs().unwrap(); - let input_config = input_configs - .find(|c| c.channels() == 1 && c.sample_format() == SampleFormat::I16) - .unwrap() - .with_sample_rate(SampleRate(48000)); - let stream = device - .build_input_stream( - &input_config.into(), - move |data: &[i16], _info: &cpal::InputCallbackInfo| { - let out: Vec = data - .iter() - .map(|s| if rx_inverted { s.saturating_neg() } else { *s }) - .collect(); - let _ = - samples.try_send(SoundmodemEvent::BasebandInput(out.into())); - }, - |e| { - // TODO: abort? - log::debug!("error occurred in soundcard input: {e:?}"); - }, - None, - ) - .unwrap(); - stream.play().unwrap(); + SoundcardEvent::StartInput { samples, errors } => { + let mut input_configs = match device.supported_input_configs() { + Ok(c) => c, + Err(e) => { + errors.send_error(SoundcardError::SupportedConfigs(e)); + continue; + } + }; + let input_config = match input_configs.find(|c| config_is_compatible(c)) { + Some(c) => c, + None => { + errors.send_error(SoundcardError::NoValidConfigAvailable); + continue; + } + }; + let input_config = input_config.with_sample_rate(SampleRate(48000)); + let channels = input_config.channels(); + let errors_1 = errors.clone(); + let stream = match device.build_input_stream( + &input_config.into(), + move |data: &[i16], _info: &cpal::InputCallbackInfo| { + let mut out = vec![]; + for d in data.chunks(channels as usize) { + // if we were given multi-channel input we'll pick the first channel + let mut sample = d[0]; + if rx_inverted { + sample = sample.saturating_neg(); + } + out.push(sample); + } + let _ = samples.try_send(SoundmodemEvent::BasebandInput(out.into())); + }, + move |e| { + errors_1.send_error(SoundcardError::Stream(e)); + }, + None, + ) { + Ok(s) => s, + Err(e) => { + errors.send_error(SoundcardError::StreamBuild(e)); + continue; + } + }; + if let Err(e) = stream.play() { + errors.send_error(SoundcardError::StreamPlay(e)); + continue; + } input_stream = Some(stream); } SoundcardEvent::CloseInput => { let _ = input_stream.take(); } - SoundcardEvent::StartOutput { event_tx, buffer } => { - let mut output_configs = device.supported_output_configs().unwrap(); - // TODO: more error handling - let output_config = output_configs - .find(|c| c.channels() == 1 && c.sample_format() == SampleFormat::I16) - .unwrap() - .with_sample_rate(SampleRate(48000)); - let stream = device - .build_output_stream( - &output_config.into(), - move |data: &mut [i16], info: &cpal::OutputCallbackInfo| { - let mut taken = 0; - let ts = info.timestamp(); - let latency = ts - .playback - .duration_since(&ts.callback) - .unwrap_or(Duration::ZERO); - let mut buffer = buffer.write().unwrap(); - buffer.latency = latency; - for out in data.iter_mut() { - if let Some(s) = buffer.samples.pop_front() { - *out = if tx_inverted { s.saturating_neg() } else { s }; - taken += 1; - } else if buffer.idling { - *out = 0; - } else { - log::debug!("output soundcard had underrun"); - let _ = event_tx.send(SoundmodemEvent::OutputUnderrun); - break; - } + SoundcardEvent::StartOutput { + event_tx, + buffer, + errors, + } => { + let mut output_configs = match device.supported_output_configs() { + Ok(c) => c, + Err(e) => { + errors.send_error(SoundcardError::SupportedConfigs(e)); + continue; + } + }; + let output_config = match output_configs.find(|c| config_is_compatible(c)) { + Some(c) => c, + None => { + errors.send_error(SoundcardError::NoValidConfigAvailable); + continue; + } + }; + let output_config = output_config.with_sample_rate(SampleRate(48000)); + let channels = output_config.channels(); + let errors_1 = errors.clone(); + let stream = match device.build_output_stream( + &output_config.into(), + move |data: &mut [i16], info: &cpal::OutputCallbackInfo| { + let mut taken = 0; + let ts = info.timestamp(); + let latency = ts + .playback + .duration_since(&ts.callback) + .unwrap_or(Duration::ZERO); + let mut buffer = buffer.write().unwrap(); + buffer.latency = latency; + for out in data.chunks_mut(channels as usize) { + if let Some(s) = buffer.samples.pop_front() { + out.fill(if tx_inverted { s.saturating_neg() } else { s }); + taken += 1; + } else if buffer.idling { + out.fill(0); + } else { + let _ = event_tx.send(SoundmodemEvent::OutputUnderrun); + break; } - //debug!("latency is {} ms, taken {taken}", latency.as_millis()); - let _ = event_tx.send(SoundmodemEvent::DidReadFromOutputBuffer { - len: taken, - timestamp: Instant::now(), - }); - }, - |e| { - // TODO: abort? - log::debug!("error occurred in soundcard output: {e:?}"); - }, - None, - ) - .unwrap(); - stream.play().unwrap(); + } + let _ = event_tx.send(SoundmodemEvent::DidReadFromOutputBuffer { + len: taken, + timestamp: Instant::now(), + }); + }, + move |e| { + errors_1.send_error(SoundcardError::Stream(e)); + }, + None, + ) { + Ok(s) => s, + Err(e) => { + errors.send_error(SoundcardError::StreamBuild(e)); + continue; + } + }; + if let Err(e) = stream.play() { + errors.send_error(SoundcardError::StreamPlay(e)); + continue; + } output_stream = Some(stream); } SoundcardEvent::CloseOutput => { @@ -259,3 +324,30 @@ fn spawn_soundcard_worker( } }); } + +#[derive(Debug, Error)] +pub enum SoundcardError { + #[error("sound card init aborted unexpectedly")] + SoundcardInit, + + #[error("unable to enumerate devices: {0}")] + Host(DevicesError), + + #[error("unable to locate sound card '{0}' - is it in use?")] + CardNotFound(String), + + #[error("error occurred in soundcard i/o: {0}")] + Stream(#[source] StreamError), + + #[error("unable to retrieve supported configs for soundcard: {0}")] + SupportedConfigs(#[source] SupportedStreamConfigsError), + + #[error("could not find a suitable soundcard config")] + NoValidConfigAvailable, + + #[error("unable to build soundcard stream: {0}")] + StreamBuild(#[source] BuildStreamError), + + #[error("unable to play stream")] + StreamPlay(#[source] PlayStreamError), +}