]> code.octet-stream.net Git - m17rt/blobdiff - m17app/src/soundcard.rs
Support i16 and i32
[m17rt] / m17app / src / soundcard.rs
index dc08036effa7dbe99fc462b07a4d9e13283f7f2b..a61c962b417cdc12cbf2c4abe15f2403aa2d5e88 100644 (file)
@@ -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<SoundcardEvent>,
 }
@@ -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<String> {
         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<String> {
         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<C: Borrow<SupportedStreamConfigRange>>(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<T: NumCast + WrappingNeg + Clone>(
+    samples: SyncSender<SoundmodemEvent>,
+    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<SoundmodemEvent>,
+    channels: u16,
+    rx_inverted: bool,
+) -> Result<Stream, BuildStreamError> {
+    if input_config.sample_format() == SampleFormat::I16 {
+        device.build_input_stream(
+            &input_config.into(),
+            build_input_cb::<i16>(samples, channels, rx_inverted),
+            move |e| {
+                errors.send_error(SoundcardError::Stream(e));
+            },
+            None,
+        )
+    } else {
+        device.build_input_stream(
+            &input_config.into(),
+            build_input_cb::<i32>(samples, channels, rx_inverted),
+            move |e| {
+                errors.send_error(SoundcardError::Stream(e));
+            },
+            None,
+        )
+    }
+}
+
+fn build_output_cb<T: NumCast + WrappingNeg + Clone>(
+    event_tx: SyncSender<SoundmodemEvent>,
+    channels: u16,
+    tx_inverted: bool,
+    buffer: Arc<RwLock<OutputBuffer>>,
+) -> 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<SoundmodemEvent>,
+    channels: u16,
+    tx_inverted: bool,
+    buffer: Arc<RwLock<OutputBuffer>>,
+) -> Result<Stream, BuildStreamError> {
+    if output_config.sample_format() == SampleFormat::I16 {
+        device.build_output_stream(
+            &output_config.into(),
+            build_output_cb::<i16>(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::<i32>(event_tx, channels, tx_inverted, buffer),
+            move |e| {
+                errors.send_error(SoundcardError::Stream(e));
+            },
+            None,
+        )
+    }
+}
+
 fn spawn_soundcard_worker(
     event_rx: Receiver<SoundcardEvent>,
     setup_tx: SyncSender<Result<(), SoundcardError>>,
@@ -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<i16> = 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) => {