From: Thomas Karpiniec Date: Tue, 7 Jan 2025 10:00:57 +0000 (+1100) Subject: Sound card input support for demodulation chain X-Git-Url: https://code.octet-stream.net/m17rt/commitdiff_plain/d610ea7b4224b89b5a1c481697d6c9e8fa7ce87c?ds=sidebyside;hp=bd5155840661f90aab8c06413fb1e9f256824b55 Sound card input support for demodulation chain --- diff --git a/Cargo.lock b/Cargo.lock index 0f90ba5..c79675b 100755 --- a/Cargo.lock +++ b/Cargo.lock @@ -440,6 +440,7 @@ checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" name = "m17app" version = "0.1.0" dependencies = [ + "cpal", "log", "m17core", ] diff --git a/demod/src/main.rs b/demod/src/main.rs index 0f7f031..563b721 100755 --- a/demod/src/main.rs +++ b/demod/src/main.rs @@ -1,11 +1,12 @@ use m17app::app::M17App; -use m17app::soundmodem::{InputRrcFile, Soundmodem}; +use m17app::soundmodem::{InputRrcFile, InputSoundcard, Soundmodem}; use m17codec2::Codec2Adapter; use std::path::PathBuf; pub fn m17app_test() { - let path = PathBuf::from("../../Data/test_vk7xt.rrc"); - let source = InputRrcFile::new(path); + //let path = PathBuf::from("../../Data/test_vk7xt.rrc"); + //let source = InputRrcFile::new(path); + let source = InputSoundcard::new(); let soundmodem = Soundmodem::new_with_input(source); let app = M17App::new(soundmodem); app.add_stream_adapter(Codec2Adapter::new()); diff --git a/m17app/Cargo.toml b/m17app/Cargo.toml index 11805eb..daca51f 100755 --- a/m17app/Cargo.toml +++ b/m17app/Cargo.toml @@ -10,5 +10,6 @@ repository = "https://code.octet-stream.net/m17rt" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +cpal = "0.15.3" m17core = { path = "../m17core" } log = "0.4.22" diff --git a/m17app/src/soundmodem.rs b/m17app/src/soundmodem.rs index 8b7f80f..9d9cec9 100644 --- a/m17app/src/soundmodem.rs +++ b/m17app/src/soundmodem.rs @@ -1,6 +1,10 @@ use std::io::{self, ErrorKind, Read, Write}; 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, SoftDemodulator}; @@ -154,16 +158,68 @@ pub trait InputSource: Send + Sync + 'static { } pub struct InputSoundcard { - cpal_name: String, + 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) { - todo!() + 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| { + debug!("input has given us {} samples", data.len()); + 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) { - todo!() + let _ = self.end_tx.lock().unwrap().take(); } } diff --git a/m17codec2/src/lib.rs b/m17codec2/src/lib.rs index f09d2b8..bfd3095 100755 --- a/m17codec2/src/lib.rs +++ b/m17codec2/src/lib.rs @@ -50,6 +50,7 @@ impl Codec2Adapter { codec2: Codec2::new(Codec2Mode::MODE_3200), end_tx: None, })), + // TODO: this doesn't work on rpi. Use default_output_device() by default output_card: "default".to_owned(), } } @@ -131,6 +132,8 @@ fn stream_thread(end: Receiver<()>, state: Arc>, output_card .find(|d| d.name().unwrap() == output_card) .unwrap(); let mut configs = device.supported_output_configs().unwrap(); + // TODO: channels == 1 doesn't work on a Raspberry Pi + // make this configurable and support interleaving LRLR stereo samples if using 2 channels let config = configs .find(|c| c.channels() == 1 && c.sample_format() == SampleFormat::I16) .unwrap() diff --git a/m17core/src/tnc.rs b/m17core/src/tnc.rs index 8b8e5a1..ae95628 100644 --- a/m17core/src/tnc.rs +++ b/m17core/src/tnc.rs @@ -65,6 +65,7 @@ impl SoftTnc { let end = start + payload_len; rx.packet[start..(start + payload_len)] .copy_from_slice(&packet.payload); + // TODO: compatible packets should be sent on port 0 too let kiss = KissFrame::new_full_packet(&rx.lsf.0, &rx.packet[0..end]) .unwrap();