From: Thomas Karpiniec Date: Tue, 14 Jan 2025 11:13:31 +0000 (+1100) Subject: Integrating modulator X-Git-Url: https://code.octet-stream.net/m17rt/commitdiff_plain/cd3124ca701db72364554f91fdb6a119faa876ec?ds=sidebyside;hp=4e2358c2ade1278f4835bc263e7df1dd8e74e8c9 Integrating modulator --- diff --git a/m17app/src/soundmodem.rs b/m17app/src/soundmodem.rs index 9d9cec9..4970d92 100644 --- a/m17app/src/soundmodem.rs +++ b/m17app/src/soundmodem.rs @@ -1,5 +1,6 @@ use std::io::{self, ErrorKind, Read, Write}; - +use std::sync::RwLock; +use std::collections::VecDeque; use crate::tnc::{Tnc, TncError}; use cpal::traits::DeviceTrait; use cpal::traits::HostTrait; @@ -7,7 +8,7 @@ use cpal::traits::StreamTrait; use cpal::{SampleFormat, SampleRate}; use log::debug; use m17core::kiss::MAX_FRAME_LEN; -use m17core::modem::{Demodulator, SoftDemodulator}; +use m17core::modem::{Demodulator, Modulator, ModulatorAction, SoftDemodulator, SoftModulator}; use m17core::tnc::SoftTnc; use std::fs::File; use std::path::PathBuf; @@ -22,11 +23,11 @@ pub struct Soundmodem { } impl Soundmodem { - pub fn new_with_input(input: T) -> Self { + pub fn new_with_input_and_output(input: I, output: O) -> Self { // must create TNC here let (event_tx, event_rx) = sync_channel(128); let (kiss_out_tx, kiss_out_rx) = sync_channel(128); - spawn_soundmodem_worker(event_tx.clone(), event_rx, kiss_out_tx, Box::new(input)); + spawn_soundmodem_worker(event_tx.clone(), event_rx, kiss_out_tx, Box::new(input), Box::new(output)); Self { event_tx, kiss_out_rx: Arc::new(Mutex::new(kiss_out_rx)), @@ -110,6 +111,11 @@ pub enum SoundmodemEvent { BasebandInput(Arc<[i16]>), Start, Close, + DidReadFromOutputBuffer { + len: usize, + timestamp: Instant, + }, + OutputUnderrun, } fn spawn_soundmodem_worker( @@ -117,13 +123,23 @@ fn spawn_soundmodem_worker( event_rx: Receiver, kiss_out_tx: SyncSender>, input: Box, + output: Box, ) { std::thread::spawn(move || { // TODO: should be able to provide a custom Demodulator for a soundmodem - let mut demod = SoftDemodulator::new(); + let mut demodulator = SoftDemodulator::new(); + let mut modulator = SoftModulator::new(); let mut tnc = SoftTnc::new(); let mut buf = [0u8; MAX_FRAME_LEN]; + let out_buffer = Arc::new(RwLock::new(OutputBuffer::new())); + let mut out_samples = [0i16; 1024]; + let start = Instant::now(); while let Ok(ev) = event_rx.recv() { + // Update clock on TNC before we do anything + let sample_time = (start.elapsed().as_nanos() / 48000) as u64; + tnc.set_now(sample_time); + + // Handle event match ev { SoundmodemEvent::Kiss(k) => { let _n = tnc.write_kiss(&k); @@ -132,7 +148,7 @@ fn spawn_soundmodem_worker( } SoundmodemEvent::BasebandInput(b) => { for sample in &*b { - if let Some(frame) = demod.demod(*sample) { + if let Some(frame) = demodulator.demod(*sample) { tnc.handle_frame(frame); loop { let n = tnc.read_kiss(&mut buf); @@ -144,9 +160,53 @@ fn spawn_soundmodem_worker( } } } + tnc.set_data_carrier_detect(demodulator.data_carrier_detect()); } - SoundmodemEvent::Start => input.start(event_tx.clone()), + SoundmodemEvent::Start => { + input.start(event_tx.clone()); + output.start(event_tx.clone(), out_buffer.clone()); + }, SoundmodemEvent::Close => break, + SoundmodemEvent::DidReadFromOutputBuffer { len, timestamp } => { + let (occupied, internal_latency) = { + let out_buffer = out_buffer.read().unwrap(); + (out_buffer.samples.len(), out_buffer.latency) + }; + let internal_latency = (internal_latency.as_secs_f32() * 48000.0) as usize; + let dynamic_latency = len.saturating_sub((timestamp.elapsed().as_secs_f32() * 48000.0) as usize); + modulator.update_output_buffer(occupied, 48000, internal_latency + dynamic_latency); + }, + SoundmodemEvent::OutputUnderrun => { + // TODO: cancel transmission, send empty data frame to host + } + } + + // Let the modulator do what it wants + while let Some(action) = modulator.run() { + match action { + ModulatorAction::SetIdle(idling) => { + out_buffer.write().unwrap().idling = idling; + }, + ModulatorAction::GetNextFrame => { + modulator.provide_next_frame(tnc.read_tx_frame()); + }, + ModulatorAction::ReadOutput => { + loop { + let n = modulator.read_output_samples(&mut out_samples); + if n == 0 { + break; + } + let mut out_buffer = out_buffer.write().unwrap(); + for s in &out_samples[0..n] { + out_buffer.samples.push_back(*s); + } + } + + }, + ModulatorAction::TransmissionWillEnd(in_samples) => { + tnc.set_tx_end_time(in_samples); + }, + } } } }); @@ -158,6 +218,7 @@ pub trait InputSource: Send + Sync + 'static { } pub struct InputSoundcard { + // TODO: allow for inversion both here and in output cpal_name: Option, end_tx: Mutex>>, } @@ -281,3 +342,90 @@ impl InputSource for InputRrcFile { let _ = self.end_tx.lock().unwrap().take(); } } + +pub struct OutputBuffer { + idling: bool, + // TODO: something more efficient + samples: VecDeque, + latency: Duration, +} + +impl OutputBuffer { + pub fn new() -> Self { + Self { + idling: true, + samples: VecDeque::new(), + latency: Duration::ZERO, + } + } +} + +pub trait OutputSink: Send + Sync + 'static { + fn start(&self, event_tx: SyncSender, buffer: Arc>); + fn close(&self); +} + +pub struct OutputRrcFile { + path: PathBuf, + end_tx: Mutex>>, +} + +impl OutputRrcFile { + pub fn new(path: PathBuf) -> Self { + Self { + path, + end_tx: Mutex::new(None), + } + } +} + +impl OutputSink for OutputRrcFile { + fn start(&self, event_tx: SyncSender, buffer: Arc>) { + let (end_tx, end_rx) = channel(); + let path = self.path.clone(); + std::thread::spawn(move || { + // TODO: error handling + let mut file = File::create(path).unwrap(); + + // assuming 48 kHz for now + const TICK: Duration = Duration::from_millis(25); + const SAMPLES_PER_TICK: usize = 1200; + + // flattened BE i16s for writing + let mut buf = [0u8; SAMPLES_PER_TICK * 2]; + let mut next_tick = Instant::now() + TICK; + + loop { + std::thread::sleep(next_tick.duration_since(Instant::now())); + next_tick = next_tick + TICK; + if end_rx.try_recv() != Err(TryRecvError::Empty) { + break; + } + + let mut buffer = buffer.write().unwrap(); + for out in buf.chunks_mut(2) { + if let Some(s) = buffer.samples.pop_front() { + let be = s.to_be_bytes(); + out.copy_from_slice(&[be[0], be[1]]); + } else if buffer.idling { + out.copy_from_slice(&[0, 0]); + } else { + debug!("output rrc file had underrun"); + let _ = event_tx.send(SoundmodemEvent::OutputUnderrun); + break; + } + } + if let Err(e) = file.write_all(&buf) { + debug!("failed to write to rrc file: {e:?}"); + break; + } + } + + }); + *self.end_tx.lock().unwrap() = Some(end_tx); + } + + fn close(&self) { + let _ = self.end_tx.lock().unwrap().take(); + } +} diff --git a/m17core/src/modem.rs b/m17core/src/modem.rs index e07826f..1d972bb 100644 --- a/m17core/src/modem.rs +++ b/m17core/src/modem.rs @@ -184,7 +184,7 @@ pub trait Modulator { ); /// Supply the next frame available from the TNC, if it was requested. - fn next_frame(&mut self, frame: Option); + fn provide_next_frame(&mut self, frame: Option); /// Calculate and write out output samples for the soundcard. /// @@ -350,7 +350,7 @@ impl Modulator for SoftModulator { self.request_frame_if_space(); } - fn next_frame(&mut self, frame: Option) { + fn provide_next_frame(&mut self, frame: Option) { let Some(frame) = frame else { self.try_get_frame = false; return; diff --git a/m17core/src/tnc.rs b/m17core/src/tnc.rs index ae95628..f3622e5 100644 --- a/m17core/src/tnc.rs +++ b/m17core/src/tnc.rs @@ -1,4 +1,5 @@ use crate::kiss::{KissBuffer, KissFrame}; +use crate::modem::ModulatorFrame; use crate::protocol::{Frame, LichCollection, LsfFrame, Mode, PacketFrameCounter}; /// Handles the KISS protocol and frame management for `SoftModulator` and `SoftDemodulator`. @@ -129,15 +130,19 @@ impl SoftTnc { } } - /// Update the number of samples that have been received by the incoming stream, as a form of timekeeping - pub fn advance_samples(&mut self, _samples: u64) {} - pub fn set_data_carrier_detect(&mut self, _dcd: bool) {} + + pub fn set_now(&mut self, samples: u64) {} + + pub fn set_tx_end_time(&mut self, in_samples: usize) { + // This is a relative time from now, expressed in samples + // Use the time from set_now() to decide when to drop PTT + } - pub fn read_tx_frame(&mut self) -> Result, SoftTncError> { - // yes we want to deal with Frames here + pub fn read_tx_frame(&mut self) -> Option { + // yes we want to deal with frames here // it's important to establish successful decode that SoftDemodulator is aware of the frame innards - Ok(None) + None } /// Read KISS message to be sent to host.