From: Thomas Karpiniec Date: Thu, 23 Jan 2025 22:37:24 +0000 (+1100) Subject: New Soundcard struct, use it in test programs X-Git-Tag: v0.1.0~4 X-Git-Url: https://code.octet-stream.net/m17rt/commitdiff_plain/6440cd74346c4b2d63d4774476e8c6113c032534?ds=inline;hp=1a444762d8fd7d48e4f56a87c6bd77f837522d5d New Soundcard struct, use it in test programs --- diff --git a/Cargo.lock b/Cargo.lock index a1a9c8b..6a2bcd0 100755 --- a/Cargo.lock +++ b/Cargo.lock @@ -516,6 +516,13 @@ dependencies = [ "m17core", ] +[[package]] +name = "m17rt-soundcards" +version = "0.1.0" +dependencies = [ + "m17app", +] + [[package]] name = "m17rt-txpacket" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 8d1b91d..fab7146 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] resolver = "2" members = [ - "m17app", "m17codec2", "m17core", "tools/m17rt-demod", "tools/m17rt-mod", "tools/m17rt-txpacket", "tools/m17rt-rxpacket" + "m17app", "m17codec2", "m17core", "tools/m17rt-demod", "tools/m17rt-mod", "tools/m17rt-txpacket", "tools/m17rt-rxpacket", "tools/m17rt-soundcards" ] diff --git a/m17app/src/adapter.rs b/m17app/src/adapter.rs index 57e01bb..8fdf9d9 100644 --- a/m17app/src/adapter.rs +++ b/m17app/src/adapter.rs @@ -3,26 +3,36 @@ use m17core::protocol::PacketType; use std::sync::Arc; pub trait PacketAdapter: Send + Sync + 'static { - fn adapter_registered(&self, _id: usize, _handle: TxHandle) {} + fn adapter_registered(&self, id: usize, handle: TxHandle) { + let _ = id; + let _ = handle; + } fn adapter_removed(&self) {} fn tnc_started(&self) {} fn tnc_closed(&self) {} - fn packet_received( - &self, - _link_setup: LinkSetup, - _packet_type: PacketType, - _content: Arc<[u8]>, - ) { + fn packet_received(&self, link_setup: LinkSetup, packet_type: PacketType, content: Arc<[u8]>) { + let _ = link_setup; + let _ = packet_type; + let _ = content; } } pub trait StreamAdapter: Send + Sync + 'static { - fn adapter_registered(&self, _id: usize, _handle: TxHandle) {} + fn adapter_registered(&self, id: usize, handle: TxHandle) { + let _ = id; + let _ = handle; + } fn adapter_removed(&self) {} fn tnc_started(&self) {} fn tnc_closed(&self) {} - fn stream_began(&self, _link_setup: LinkSetup) {} - fn stream_data(&self, _frame_number: u16, _is_final: bool, _data: Arc<[u8; 16]>) {} + fn stream_began(&self, link_setup: LinkSetup) { + let _ = link_setup; + } + fn stream_data(&self, frame_number: u16, is_final: bool, data: Arc<[u8; 16]>) { + let _ = frame_number; + let _ = is_final; + let _ = data; + } // TODO // fn stream_lost(&self); diff --git a/m17app/src/error.rs b/m17app/src/error.rs index f7079ca..ee624f9 100644 --- a/m17app/src/error.rs +++ b/m17app/src/error.rs @@ -7,4 +7,10 @@ pub enum M17Error { #[error("given callsign is {0} characters long; maximum is 9")] CallsignTooLong(usize), + + #[error("error during soundcard initialisation")] + SoundcardInit, + + #[error("unable to locate sound card '{0}' - is it in use?")] + SoundcardNotFound(String), } diff --git a/m17app/src/lib.rs b/m17app/src/lib.rs index ce67840..543bdc5 100755 --- a/m17app/src/lib.rs +++ b/m17app/src/lib.rs @@ -3,5 +3,9 @@ pub mod app; pub mod error; pub mod link_setup; pub mod serial; +pub mod soundcard; pub mod soundmodem; pub mod tnc; + +// Protocol definitions needed to implement stream and packet adapters or create fully custom LSFs +pub use m17core::protocol::{LsfFrame, PacketType, StreamFrame}; diff --git a/m17app/src/soundcard.rs b/m17app/src/soundcard.rs new file mode 100644 index 0000000..b12ef88 --- /dev/null +++ b/m17app/src/soundcard.rs @@ -0,0 +1,261 @@ +use std::{ + sync::{ + mpsc::{sync_channel, Receiver, SyncSender}, + Arc, RwLock, + }, + time::{Duration, Instant}, +}; + +use cpal::{ + traits::{DeviceTrait, HostTrait, StreamTrait}, + SampleFormat, SampleRate, Stream, +}; + +use crate::{ + error::M17Error, + soundmodem::{InputSource, OutputBuffer, OutputSink, SoundmodemEvent}, +}; + +pub struct Soundcard { + event_tx: SyncSender, +} + +impl Soundcard { + 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), + } + } + + pub fn input(&self) -> SoundcardInputSource { + SoundcardInputSource { + event_tx: self.event_tx.clone(), + } + } + + pub fn output(&self) -> SoundcardOutputSink { + SoundcardOutputSink { + event_tx: self.event_tx.clone(), + } + } + + pub fn set_rx_inverted(&self, inverted: bool) { + let _ = self.event_tx.send(SoundcardEvent::SetRxInverted(inverted)); + } + + pub fn set_tx_inverted(&self, inverted: bool) { + let _ = self.event_tx.send(SoundcardEvent::SetTxInverted(inverted)); + } + + pub fn supported_output_cards() -> Vec { + let mut out = vec![]; + let host = cpal::default_host(); + let Ok(output_devices) = host.output_devices() else { + return out; + }; + for d in output_devices { + let Ok(mut configs) = d.supported_output_configs() else { + continue; + }; + if configs + .find(|config| { + config.channels() == 1 && config.sample_format() == SampleFormat::I16 + }) + .is_some() + { + let Ok(name) = d.name() else { + continue; + }; + out.push(name); + } + } + out.sort(); + out + } + + pub fn supported_input_cards() -> Vec { + let mut out = vec![]; + let host = cpal::default_host(); + let Ok(input_devices) = host.input_devices() else { + return out; + }; + for d in input_devices { + let Ok(mut configs) = d.supported_input_configs() else { + continue; + }; + if configs + .find(|config| { + config.channels() == 1 && config.sample_format() == SampleFormat::I16 + }) + .is_some() + { + let Ok(name) = d.name() else { + continue; + }; + out.push(name); + } + } + out.sort(); + out + } +} + +enum SoundcardEvent { + SetRxInverted(bool), + SetTxInverted(bool), + StartInput { + samples: SyncSender, + }, + CloseInput, + StartOutput { + event_tx: SyncSender, + buffer: Arc>, + }, + CloseOutput, +} + +pub struct SoundcardInputSource { + event_tx: SyncSender, +} + +impl InputSource for SoundcardInputSource { + fn start(&self, samples: SyncSender) { + let _ = self.event_tx.send(SoundcardEvent::StartInput { samples }); + } + + fn close(&self) { + let _ = self.event_tx.send(SoundcardEvent::CloseInput); + } +} + +pub struct SoundcardOutputSink { + event_tx: SyncSender, +} + +impl OutputSink for SoundcardOutputSink { + fn start(&self, event_tx: SyncSender, buffer: Arc>) { + let _ = self + .event_tx + .send(SoundcardEvent::StartOutput { event_tx, buffer }); + } + + fn close(&self) { + let _ = self.event_tx.send(SoundcardEvent::CloseOutput); + } +} + +fn spawn_soundcard_worker( + event_rx: Receiver, + setup_tx: SyncSender>, + card_name: String, +) { + std::thread::spawn(move || { + let host = cpal::default_host(); + let Some(device) = host + .devices() + .unwrap() + .find(|d| d.name().unwrap() == card_name) + else { + let _ = setup_tx.send(Err(M17Error::SoundcardNotFound(card_name))); + return; + }; + + let _ = setup_tx.send(Ok(())); + let mut rx_inverted = false; + let mut tx_inverted = false; + let mut input_stream: Option = None; + let mut output_stream: Option = None; + + while let Ok(ev) = event_rx.recv() { + 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(); + 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; + } + } + //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(); + output_stream = Some(stream); + } + SoundcardEvent::CloseOutput => { + let _ = output_stream.take(); + } + } + } + }); +} diff --git a/m17app/src/soundmodem.rs b/m17app/src/soundmodem.rs index dced157..2d005b1 100644 --- a/m17app/src/soundmodem.rs +++ b/m17app/src/soundmodem.rs @@ -1,8 +1,4 @@ use crate::tnc::{Tnc, TncError}; -use cpal::traits::DeviceTrait; -use cpal::traits::HostTrait; -use cpal::traits::StreamTrait; -use cpal::{SampleFormat, SampleRate}; use log::debug; use m17core::kiss::MAX_FRAME_LEN; use m17core::modem::{Demodulator, Modulator, ModulatorAction, SoftDemodulator, SoftModulator}; @@ -239,72 +235,6 @@ pub trait InputSource: Send + Sync + 'static { fn close(&self); } -pub struct InputSoundcard { - // TODO: allow for inversion both here and in output - cpal_name: Option, - end_tx: Mutex>>, -} - -impl InputSoundcard { - pub fn new() -> Self { - Self { - cpal_name: None, - end_tx: Mutex::new(None), - } - } - - pub fn new_with_card(card_name: String) -> Self { - Self { - cpal_name: Some(card_name), - end_tx: Mutex::new(None), - } - } -} - -impl InputSource for InputSoundcard { - fn start(&self, samples: SyncSender) { - let (end_tx, end_rx) = channel(); - let cpal_name = self.cpal_name.clone(); - std::thread::spawn(move || { - let host = cpal::default_host(); - let device = if let Some(name) = cpal_name.as_deref() { - host.input_devices() - .unwrap() - .find(|d| d.name().unwrap() == name) - .unwrap() - } else { - host.default_input_device().unwrap() - }; - let mut configs = device.supported_input_configs().unwrap(); - let config = configs - .find(|c| c.channels() == 1 && c.sample_format() == SampleFormat::I16) - .unwrap() - .with_sample_rate(SampleRate(48000)); - let stream = device - .build_input_stream( - &config.into(), - move |data: &[i16], _info: &cpal::InputCallbackInfo| { - let out: Vec = data.iter().map(|s| *s).collect(); - let _ = samples.try_send(SoundmodemEvent::BasebandInput(out.into())); - }, - |e| { - // TODO: abort? - debug!("error occurred in soundcard input: {e:?}"); - }, - None, - ) - .unwrap(); - stream.play().unwrap(); - let _ = end_rx.recv(); - }); - *self.end_tx.lock().unwrap() = Some(end_tx); - } - - fn close(&self) { - let _ = self.end_tx.lock().unwrap().take(); - } -} - pub struct InputRrcFile { path: PathBuf, end_tx: Mutex>>, @@ -407,10 +337,10 @@ impl InputSource for NullInputSource { } pub struct OutputBuffer { - idling: bool, + pub idling: bool, // TODO: something more efficient - samples: VecDeque, - latency: Duration, + pub samples: VecDeque, + pub latency: Duration, } impl OutputBuffer { @@ -553,96 +483,6 @@ impl OutputSink for NullOutputSink { } } -pub struct OutputSoundcard { - // TODO: allow for inversion both here and in output - cpal_name: Option, - end_tx: Mutex>>, -} - -impl OutputSoundcard { - pub fn new() -> Self { - Self { - cpal_name: None, - end_tx: Mutex::new(None), - } - } - - pub fn new_with_card(card_name: String) -> Self { - Self { - cpal_name: Some(card_name), - end_tx: Mutex::new(None), - } - } -} - -impl OutputSink for OutputSoundcard { - fn start(&self, event_tx: SyncSender, buffer: Arc>) { - let (end_tx, end_rx) = channel(); - let cpal_name = self.cpal_name.clone(); - std::thread::spawn(move || { - let host = cpal::default_host(); - let device = if let Some(name) = cpal_name.as_deref() { - host.output_devices() - .unwrap() - .find(|d| d.name().unwrap() == name) - .unwrap() - } else { - host.default_output_device().unwrap() - }; - let mut configs = device.supported_output_configs().unwrap(); - // TODO: more error handling - let config = configs - .find(|c| c.channels() == 1 && c.sample_format() == SampleFormat::I16) - .unwrap() - .with_sample_rate(SampleRate(48000)); - let stream = device - .build_output_stream( - &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 = s; - taken += 1; - } else if buffer.idling { - *out = 0; - } else { - debug!("output soundcard had underrun"); - 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? - debug!("error occurred in soundcard output: {e:?}"); - }, - None, - ) - .unwrap(); - stream.play().unwrap(); - let _ = end_rx.recv(); - }); - *self.end_tx.lock().unwrap() = Some(end_tx); - } - - fn close(&self) { - let _ = self.end_tx.lock().unwrap().take(); - } -} - pub trait Ptt: Send + 'static { fn ptt_on(&mut self); fn ptt_off(&mut self); diff --git a/m17codec2/src/lib.rs b/m17codec2/src/lib.rs index 0f3c8a5..016acc7 100755 --- a/m17codec2/src/lib.rs +++ b/m17codec2/src/lib.rs @@ -8,10 +8,7 @@ use m17app::adapter::StreamAdapter; use m17app::app::TxHandle; use m17app::link_setup::LinkSetup; use m17app::link_setup::M17Address; -use m17core::address::Address; -use m17core::address::Callsign; -use m17core::protocol::LsfFrame; -use m17core::protocol::StreamFrame; +use m17app::StreamFrame; use std::collections::VecDeque; use std::fs::File; use std::io::Write; diff --git a/m17core/src/tnc.rs b/m17core/src/tnc.rs index 93a4363..8965499 100644 --- a/m17core/src/tnc.rs +++ b/m17core/src/tnc.rs @@ -102,6 +102,10 @@ impl SoftTnc { /// Process an individual `Frame` that has been decoded by the modem. pub fn handle_frame(&mut self, frame: Frame) { + if self.ptt { + // Ignore self-decodes + return; + } match frame { Frame::Lsf(lsf) => { // A new LSF implies a clean slate. diff --git a/tools/m17rt-demod/src/main.rs b/tools/m17rt-demod/src/main.rs index 9c229af..af5fafd 100755 --- a/tools/m17rt-demod/src/main.rs +++ b/tools/m17rt-demod/src/main.rs @@ -1,22 +1,21 @@ use m17app::app::M17App; -use m17app::soundmodem::{InputRrcFile, InputSoundcard, NullOutputSink, NullPtt, Soundmodem}; +use m17app::soundcard::Soundcard; +use m17app::soundmodem::{NullOutputSink, NullPtt, Soundmodem}; use m17codec2::Codec2Adapter; -use std::path::PathBuf; -pub fn m17app_test() { - //let path = PathBuf::from("../../../Data/test_vk7xt.rrc"); - let path = PathBuf::from("../../../Data/mymod.rrc"); - //let path = PathBuf::from("../../../Data/mymod-noisy.raw"); - let source = InputRrcFile::new(path); - //let source = InputSoundcard::new(); - let soundmodem = Soundmodem::new(source, NullOutputSink::new(), NullPtt::new()); +pub fn demod_test() { + let soundcard = Soundcard::new("plughw:CARD=Device,DEV=0").unwrap(); + let soundmodem = Soundmodem::new(soundcard.input(), NullOutputSink::new(), NullPtt::new()); let app = M17App::new(soundmodem); app.add_stream_adapter(Codec2Adapter::new()); app.start(); - std::thread::sleep(std::time::Duration::from_secs(15)); + + loop { + std::thread::park(); + } } fn main() { env_logger::init(); - m17app_test(); + demod_test(); } diff --git a/tools/m17rt-mod/src/main.rs b/tools/m17rt-mod/src/main.rs index c616b2e..50358bd 100644 --- a/tools/m17rt-mod/src/main.rs +++ b/tools/m17rt-mod/src/main.rs @@ -1,31 +1,29 @@ use m17app::app::M17App; use m17app::link_setup::M17Address; -use m17app::soundmodem::{ - InputRrcFile, InputSoundcard, NullInputSource, NullOutputSink, NullPtt, OutputRrcFile, - OutputSoundcard, Soundmodem, -}; -use m17codec2::{Codec2Adapter, WavePlayer}; +use m17app::serial::{PttPin, SerialPtt}; +use m17app::soundcard::Soundcard; +use m17app::soundmodem::Soundmodem; +use m17codec2::WavePlayer; use std::path::PathBuf; pub fn mod_test() { - let in_path = PathBuf::from("../../../Data/test_vk7xt_8k.wav"); - let out_path = PathBuf::from("../../../Data/mymod.rrc"); - let output = OutputRrcFile::new(out_path); - //let output = OutputSoundcard::new(); - let soundmodem = Soundmodem::new(NullInputSource::new(), output, NullPtt::new()); + let soundcard = Soundcard::new("plughw:CARD=Device,DEV=0").unwrap(); + let ptt = SerialPtt::new("/dev/ttyUSB0", PttPin::Rts); + let soundmodem = Soundmodem::new(soundcard.input(), soundcard.output(), ptt); let app = M17App::new(soundmodem); app.start(); std::thread::sleep(std::time::Duration::from_secs(1)); println!("Beginning playback..."); WavePlayer::play( - in_path, + PathBuf::from("../../../Data/test_vk7xt_8k.wav"), app.tx(), - &M17Address::from_callsign("VK7XT").unwrap(), + &M17Address::from_callsign("VK7XT-1").unwrap(), &M17Address::new_broadcast(), 0, ); - println!("Playback complete, terminating in 5 secs"); - std::thread::sleep(std::time::Duration::from_secs(5)); + println!("Playback complete."); + std::thread::sleep(std::time::Duration::from_secs(1)); + app.close(); } fn main() { diff --git a/tools/m17rt-rxpacket/src/main.rs b/tools/m17rt-rxpacket/src/main.rs index 1cdd1d8..012cc46 100755 --- a/tools/m17rt-rxpacket/src/main.rs +++ b/tools/m17rt-rxpacket/src/main.rs @@ -1,18 +1,14 @@ use m17app::adapter::PacketAdapter; use m17app::app::M17App; use m17app::link_setup::LinkSetup; -use m17app::soundmodem::{InputRrcFile, NullOutputSink, NullPtt, Soundmodem}; -use m17core::protocol::PacketType; -use std::path::PathBuf; +use m17app::soundcard::Soundcard; +use m17app::soundmodem::{NullOutputSink, NullPtt, Soundmodem}; +use m17app::PacketType; use std::sync::Arc; fn main() { - let path = PathBuf::from("../../../Data/mypacket.rrc"); - let soundmodem = Soundmodem::new( - InputRrcFile::new(path), - NullOutputSink::new(), - NullPtt::new(), - ); + let soundcard = Soundcard::new("plughw:CARD=Device,DEV=0").unwrap(); + let soundmodem = Soundmodem::new(soundcard.input(), NullOutputSink::new(), NullPtt::new()); let app = M17App::new(soundmodem); app.add_packet_adapter(PacketPrinter); app.start(); diff --git a/tools/m17rt-soundcards/Cargo.toml b/tools/m17rt-soundcards/Cargo.toml new file mode 100644 index 0000000..b8f7db5 --- /dev/null +++ b/tools/m17rt-soundcards/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "m17rt-soundcards" +version = "0.1.0" +edition = "2021" +license = "MIT" +authors = ["Thomas Karpiniec