From 0de0222c8409705d19d9c9df8652bc6a9293ef55 Mon Sep 17 00:00:00 2001 From: Thomas Karpiniec Date: Thu, 10 Jul 2025 20:29:12 +1000 Subject: [PATCH] Support i16 and i32 --- Cargo.lock | 1 + m17app/Cargo.toml | 1 + m17app/src/soundcard.rs | 184 ++++++++++++++++++++++++++++------------ 3 files changed, 133 insertions(+), 53 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4800984..0a4cb50 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -509,6 +509,7 @@ dependencies = [ "cpal", "log", "m17core", + "num-traits", "serialport", "thiserror 2.0.11", ] diff --git a/m17app/Cargo.toml b/m17app/Cargo.toml index 5a0e621..8ef9c20 100644 --- a/m17app/Cargo.toml +++ b/m17app/Cargo.toml @@ -16,5 +16,6 @@ readme = "README.md" cpal = "0.15.3" m17core = { version = "0.1", path = "../m17core" } log = "0.4.22" +num-traits = "0.2.19" serialport = { version = "4.7.0", default-features = false } thiserror = "2.0.11" diff --git a/m17app/src/soundcard.rs b/m17app/src/soundcard.rs index fb29282..a61c962 100644 --- a/m17app/src/soundcard.rs +++ b/m17app/src/soundcard.rs @@ -8,10 +8,12 @@ use std::{ }; use cpal::{ - BuildStreamError, DevicesError, PlayStreamError, SampleFormat, SampleRate, Stream, StreamError, + BuildStreamError, Device, DevicesError, InputCallbackInfo, OutputCallbackInfo, PlayStreamError, + SampleFormat, SampleRate, Stream, StreamError, SupportedStreamConfig, SupportedStreamConfigRange, SupportedStreamConfigsError, traits::{DeviceTrait, HostTrait, StreamTrait}, }; +use num_traits::{NumCast, WrappingNeg}; use thiserror::Error; use crate::soundmodem::{ @@ -113,7 +115,8 @@ 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::I16 + || config.sample_format() == SampleFormat::I32) && config.min_sample_rate().0 <= 48000 && config.max_sample_rate().0 >= 48000 } @@ -173,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>, @@ -216,25 +330,13 @@ fn spawn_soundcard_worker( }; 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, + let stream = match build_input_stream( + &device, + input_config, + errors.clone(), + samples, + channels, + rx_inverted, ) { Ok(s) => s, Err(e) => { @@ -272,38 +374,14 @@ fn spawn_soundcard_worker( }; 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; - } - } - let _ = event_tx.send(SoundmodemEvent::DidReadFromOutputBuffer { - len: taken, - timestamp: Instant::now(), - }); - }, - move |e| { - errors_1.send_error(SoundcardError::Stream(e)); - }, - None, + let stream = match build_output_stream( + &device, + output_config, + errors.clone(), + event_tx, + channels, + tx_inverted, + buffer, ) { Ok(s) => s, Err(e) => { -- 2.39.5