X-Git-Url: https://code.octet-stream.net/m17rt/blobdiff_plain/9058451e46e4d36264282abe381aa9b6fd2c773f..6440cd74346c4b2d63d4774476e8c6113c032534:/m17app/src/soundmodem.rs?ds=inline diff --git a/m17app/src/soundmodem.rs b/m17app/src/soundmodem.rs index 99085a0..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}; @@ -23,7 +19,7 @@ pub struct Soundmodem { } impl Soundmodem { - pub fn new_with_input_and_output(input: I, output: O) -> Self { + pub fn new(input: I, output: O, ptt: P) -> Self { // must create TNC here let (event_tx, event_rx) = sync_channel(128); let (kiss_out_tx, kiss_out_rx) = sync_channel(128); @@ -33,6 +29,7 @@ impl Soundmodem { kiss_out_tx, Box::new(input), Box::new(output), + Box::new(ptt), ); Self { event_tx, @@ -127,6 +124,7 @@ fn spawn_soundmodem_worker( kiss_out_tx: SyncSender>, input: Box, output: Box, + mut ptt_driver: Box, ) { std::thread::spawn(move || { // TODO: should be able to provide a custom Demodulator for a soundmodem @@ -170,7 +168,10 @@ fn spawn_soundmodem_worker( input.start(event_tx.clone()); output.start(event_tx.clone(), out_buffer.clone()); } - SoundmodemEvent::Close => break, + SoundmodemEvent::Close => { + ptt_driver.ptt_off(); + break; + } SoundmodemEvent::DidReadFromOutputBuffer { len, timestamp } => { let (occupied, internal_latency) = { let out_buffer = out_buffer.read().unwrap(); @@ -194,9 +195,9 @@ fn spawn_soundmodem_worker( let new_ptt = tnc.ptt(); if new_ptt != ptt { if new_ptt { - // turn it on + ptt_driver.ptt_on(); } else { - // turn it off + ptt_driver.ptt_off(); } } ptt = new_ptt; @@ -234,73 +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| { - 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) { - let _ = self.end_tx.lock().unwrap().take(); - } -} - pub struct InputRrcFile { path: PathBuf, end_tx: Mutex>>, @@ -360,11 +294,53 @@ impl InputSource for InputRrcFile { } } +pub struct NullInputSource { + end_tx: Mutex>>, +} + +impl NullInputSource { + pub fn new() -> Self { + Self { + end_tx: Mutex::new(None), + } + } +} + +impl InputSource for NullInputSource { + fn start(&self, samples: SyncSender) { + let (end_tx, end_rx) = channel(); + std::thread::spawn(move || { + // assuming 48 kHz for now + const TICK: Duration = Duration::from_millis(25); + const SAMPLES_PER_TICK: usize = 1200; + 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; + } + if let Err(e) = samples.try_send(SoundmodemEvent::BasebandInput( + [0i16; SAMPLES_PER_TICK].into(), + )) { + debug!("overflow feeding soundmodem: {e:?}"); + } + } + }); + *self.end_tx.lock().unwrap() = Some(end_tx); + } + + fn close(&self) { + let _ = self.end_tx.lock().unwrap().take(); + } +} + pub struct OutputBuffer { - idling: bool, + pub idling: bool, // TODO: something more efficient - samples: VecDeque, - latency: Duration, + pub samples: VecDeque, + pub latency: Duration, } impl OutputBuffer { @@ -418,24 +394,30 @@ impl OutputSink for OutputRrcFile { if end_rx.try_recv() != Err(TryRecvError::Empty) { break; } + // For now only write deliberately modulated (non-idling) samples + // Multiple transmissions will get smooshed together + let mut buf_used = 0; 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(); + let be = s.to_le_bytes(); out.copy_from_slice(&[be[0], be[1]]); - } else if buffer.idling { - out.copy_from_slice(&[0, 0]); - } else { + buf_used += 2; + } else if !buffer.idling { debug!("output rrc file had underrun"); let _ = event_tx.send(SoundmodemEvent::OutputUnderrun); break; } } - if let Err(e) = file.write_all(&buf) { + if let Err(e) = file.write_all(&buf[0..buf_used]) { debug!("failed to write to rrc file: {e:?}"); break; } + let _ = event_tx.send(SoundmodemEvent::DidReadFromOutputBuffer { + len: buf_used / 2, + timestamp: Instant::now(), + }); } }); *self.end_tx.lock().unwrap() = Some(end_tx); @@ -475,13 +457,22 @@ impl OutputSink for NullOutputSink { } let mut buffer = buffer.write().unwrap(); + let mut taken = 0; for _ in 0..SAMPLES_PER_TICK { - if !buffer.samples.pop_front().is_some() && !buffer.idling { - debug!("null output had underrun"); - let _ = event_tx.send(SoundmodemEvent::OutputUnderrun); - break; + if !buffer.samples.pop_front().is_some() { + if !buffer.idling { + debug!("null output had underrun"); + let _ = event_tx.send(SoundmodemEvent::OutputUnderrun); + break; + } + } else { + taken += 1; } } + let _ = event_tx.send(SoundmodemEvent::DidReadFromOutputBuffer { + len: taken, + timestamp: Instant::now(), + }); } }); *self.end_tx.lock().unwrap() = Some(end_tx); @@ -491,3 +482,22 @@ impl OutputSink for NullOutputSink { let _ = self.end_tx.lock().unwrap().take(); } } + +pub trait Ptt: Send + 'static { + fn ptt_on(&mut self); + fn ptt_off(&mut self); +} + +/// There is no PTT because this TNC will never make transmissions on a real radio. +pub struct NullPtt; + +impl NullPtt { + pub fn new() -> Self { + Self + } +} + +impl Ptt for NullPtt { + fn ptt_on(&mut self) {} + fn ptt_off(&mut self) {} +}