X-Git-Url: https://code.octet-stream.net/m17rt/blobdiff_plain/64599440f241f7bb897a95b72ed7130231966518..0de0222c8409705d19d9c9df8652bc6a9293ef55:/m17app/src/soundcard.rs diff --git a/m17app/src/soundcard.rs b/m17app/src/soundcard.rs index dc08036..a61c962 100644 --- a/m17app/src/soundcard.rs +++ b/m17app/src/soundcard.rs @@ -1,22 +1,32 @@ use std::{ + borrow::Borrow, sync::{ - mpsc::{sync_channel, Receiver, SyncSender}, Arc, RwLock, + mpsc::{Receiver, SyncSender, sync_channel}, }, time::{Duration, Instant}, }; use cpal::{ + BuildStreamError, Device, DevicesError, InputCallbackInfo, OutputCallbackInfo, PlayStreamError, + SampleFormat, SampleRate, Stream, StreamError, SupportedStreamConfig, + SupportedStreamConfigRange, SupportedStreamConfigsError, traits::{DeviceTrait, HostTrait, StreamTrait}, - BuildStreamError, DevicesError, PlayStreamError, SampleFormat, SampleRate, Stream, StreamError, - SupportedStreamConfigsError, }; +use num_traits::{NumCast, WrappingNeg}; use thiserror::Error; 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, } @@ -53,6 +63,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(); @@ -63,9 +76,7 @@ impl Soundcard { let Ok(mut configs) = d.supported_output_configs() else { continue; }; - if configs - .any(|config| config.channels() == 1 && config.sample_format() == SampleFormat::I16) - { + if configs.any(config_is_compatible) { let Ok(name) = d.name() else { continue; }; @@ -76,6 +87,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(); @@ -86,9 +100,7 @@ impl Soundcard { let Ok(mut configs) = d.supported_input_configs() else { continue; }; - if configs - .any(|config| config.channels() == 1 && config.sample_format() == SampleFormat::I16) - { + if configs.any(config_is_compatible) { let Ok(name) = d.name() else { continue; }; @@ -100,6 +112,15 @@ 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.sample_format() == SampleFormat::I32) + && config.min_sample_rate().0 <= 48000 + && config.max_sample_rate().0 >= 48000 +} + enum SoundcardEvent { SetRxInverted(bool), SetTxInverted(bool), @@ -155,6 +176,117 @@ impl OutputSink for SoundcardOutputSink { } } +fn build_input_cb( + samples: SyncSender, + channels: u16, + rx_inverted: bool, +) -> impl Fn(&[T], &InputCallbackInfo) { + move |data: &[T], _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].clone(); + if rx_inverted { + sample = sample.wrapping_neg(); + } + out.push(NumCast::from(sample).unwrap()); + } + let _ = samples.try_send(SoundmodemEvent::BasebandInput(out.into())); + } +} + +fn build_input_stream( + device: &Device, + input_config: SupportedStreamConfig, + errors: SoundmodemErrorSender, + samples: SyncSender, + channels: u16, + rx_inverted: bool, +) -> Result { + if input_config.sample_format() == SampleFormat::I16 { + device.build_input_stream( + &input_config.into(), + build_input_cb::(samples, channels, rx_inverted), + move |e| { + errors.send_error(SoundcardError::Stream(e)); + }, + None, + ) + } else { + device.build_input_stream( + &input_config.into(), + build_input_cb::(samples, channels, rx_inverted), + move |e| { + errors.send_error(SoundcardError::Stream(e)); + }, + None, + ) + } +} + +fn build_output_cb( + event_tx: SyncSender, + channels: u16, + tx_inverted: bool, + buffer: Arc>, +) -> impl Fn(&mut [T], &OutputCallbackInfo) { + move |data: &mut [T], 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(NumCast::from(if tx_inverted { s.saturating_neg() } else { s }).unwrap()); + taken += 1; + } else if buffer.idling { + out.fill(NumCast::from(0).unwrap()); + } else { + let _ = event_tx.send(SoundmodemEvent::OutputUnderrun); + break; + } + } + let _ = event_tx.send(SoundmodemEvent::DidReadFromOutputBuffer { + len: taken, + timestamp: Instant::now(), + }); + } +} + +fn build_output_stream( + device: &Device, + output_config: SupportedStreamConfig, + errors: SoundmodemErrorSender, + event_tx: SyncSender, + channels: u16, + tx_inverted: bool, + buffer: Arc>, +) -> Result { + if output_config.sample_format() == SampleFormat::I16 { + device.build_output_stream( + &output_config.into(), + build_output_cb::(event_tx, channels, tx_inverted, buffer), + move |e| { + errors.send_error(SoundcardError::Stream(e)); + }, + None, + ) + } else { + device.build_output_stream( + &output_config.into(), + build_output_cb::(event_tx, channels, tx_inverted, buffer), + move |e| { + errors.send_error(SoundcardError::Stream(e)); + }, + None, + ) + } +} + fn spawn_soundcard_worker( event_rx: Receiver, setup_tx: SyncSender>, @@ -189,9 +321,7 @@ fn spawn_soundcard_worker( continue; } }; - let input_config = match input_configs - .find(|c| c.channels() == 1 && c.sample_format() == SampleFormat::I16) - { + let input_config = match input_configs.find(|c| config_is_compatible(c)) { Some(c) => c, None => { errors.send_error(SoundcardError::NoValidConfigAvailable); @@ -199,20 +329,14 @@ fn spawn_soundcard_worker( } }; let input_config = input_config.with_sample_rate(SampleRate(48000)); - let errors_1 = errors.clone(); - let stream = match 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())); - }, - move |e| { - errors_1.send_error(SoundcardError::Stream(e)); - }, - None, + let channels = input_config.channels(); + let stream = match build_input_stream( + &device, + input_config, + errors.clone(), + samples, + channels, + rx_inverted, ) { Ok(s) => s, Err(e) => { @@ -241,9 +365,7 @@ fn spawn_soundcard_worker( continue; } }; - let output_config = match output_configs - .find(|c| c.channels() == 1 && c.sample_format() == SampleFormat::I16) - { + let output_config = match output_configs.find(|c| config_is_compatible(c)) { Some(c) => c, None => { errors.send_error(SoundcardError::NoValidConfigAvailable); @@ -251,38 +373,15 @@ fn spawn_soundcard_worker( } }; let output_config = output_config.with_sample_rate(SampleRate(48000)); - 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.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 { - let _ = event_tx.send(SoundmodemEvent::OutputUnderrun); - break; - } - } - let _ = event_tx.send(SoundmodemEvent::DidReadFromOutputBuffer { - len: taken, - timestamp: Instant::now(), - }); - }, - move |e| { - errors_1.send_error(SoundcardError::Stream(e)); - }, - None, + let channels = output_config.channels(); + let stream = match build_output_stream( + &device, + output_config, + errors.clone(), + event_tx, + channels, + tx_inverted, + buffer, ) { Ok(s) => s, Err(e) => {