X-Git-Url: https://code.octet-stream.net/m17rt/blobdiff_plain/18349069c27a0b4fb2e39669fb535600805ad160..54fc21708c9121409560731f90b91e59699f4ddc:/m17codec2/src/lib.rs?ds=sidebyside diff --git a/m17codec2/src/lib.rs b/m17codec2/src/lib.rs index 5eb54a4..c338f6a 100755 --- a/m17codec2/src/lib.rs +++ b/m17codec2/src/lib.rs @@ -1,3 +1,5 @@ +#![doc = include_str!("../README.md")] + use codec2::{Codec2, Codec2Mode}; use cpal::traits::DeviceTrait; use cpal::traits::HostTrait; @@ -6,12 +8,10 @@ use cpal::{Sample, SampleFormat, SampleRate}; use log::debug; use m17app::adapter::StreamAdapter; use m17app::app::TxHandle; +use m17app::error::AdapterError; 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; @@ -23,6 +23,7 @@ use std::sync::{ }; use std::time::Duration; use std::time::Instant; +use thiserror::Error; pub fn decode_codec2>(data: &[u8], out_path: P) -> Vec { let codec2 = Codec2::new(Codec2Mode::MODE_3200); @@ -43,6 +44,7 @@ pub fn decode_codec2>(data: &[u8], out_path: P) -> Vec { all_samples } +/// Subscribes to M17 streams and attempts to play the decoded Codec2 pub struct Codec2Adapter { state: Arc>, // TODO: make this configurable @@ -64,6 +66,12 @@ impl Codec2Adapter { } } +impl Default for Codec2Adapter { + fn default() -> Self { + Self::new() + } +} + struct AdapterState { tx: Option, /// Circular buffer of output samples for playback @@ -73,27 +81,27 @@ struct AdapterState { } impl StreamAdapter for Codec2Adapter { - fn adapter_registered(&self, _id: usize, handle: TxHandle) { + fn start(&self, handle: TxHandle) -> Result<(), AdapterError> { self.state.lock().unwrap().tx = Some(handle); let (end_tx, end_rx) = channel(); + let (setup_tx, setup_rx) = channel(); let state = self.state.clone(); let output_card = self.output_card.clone(); - std::thread::spawn(move || stream_thread(end_rx, state, output_card)); + std::thread::spawn(move || stream_thread(end_rx, setup_tx, state, output_card)); self.state.lock().unwrap().end_tx = Some(end_tx); + // Propagate any errors arising in the thread + Ok(setup_rx.recv()??) } - fn adapter_removed(&self) { + fn close(&self) -> Result<(), AdapterError> { let mut state = self.state.lock().unwrap(); state.tx = None; state.end_tx = None; + Ok(()) } - fn tnc_started(&self) {} - - fn tnc_closed(&self) {} - - fn stream_began(&self, _lsf: LsfFrame) { + fn stream_began(&self, _link_setup: LinkSetup) { // for now we will assume: // - unencrypted // - data type is Voice (Codec2 3200), not Voice+Data @@ -127,41 +135,93 @@ fn output_cb(data: &mut [i16], state: &Mutex) { } /// Create and manage the stream from a dedicated thread since it's `!Send` -fn stream_thread(end: Receiver<()>, state: Arc>, output_card: String) { +fn stream_thread( + end: Receiver<()>, + setup_tx: Sender>, + state: Arc>, + output_card: String, +) { let host = cpal::default_host(); - let device = host + let device = match host .output_devices() .unwrap() .find(|d| d.name().unwrap() == output_card) - .unwrap(); - let mut configs = device.supported_output_configs().unwrap(); + { + Some(d) => d, + None => { + let _ = setup_tx.send(Err(M17Codec2Error::CardUnavailable(output_card).into())); + return; + } + }; + let mut configs = match device.supported_output_configs() { + Ok(c) => c, + Err(e) => { + let _ = setup_tx.send(Err(M17Codec2Error::OutputConfigsUnavailable( + output_card, + e, + ) + .into())); + return; + } + }; // 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() - .with_sample_rate(SampleRate(8000)); - let stream = device - .build_output_stream( - &config.into(), - move |data: &mut [i16], _info: &cpal::OutputCallbackInfo| { - output_cb(data, &state); - }, - |e| { - // trigger end_tx here? always more edge cases - debug!("error occurred in codec2 playback: {e:?}"); - }, - None, - ) - .unwrap(); - stream.play().unwrap(); + let config = match configs.find(|c| c.channels() == 1 && c.sample_format() == SampleFormat::I16) + { + Some(c) => c, + None => { + let _ = setup_tx.send(Err( + M17Codec2Error::SupportedOutputUnavailable(output_card).into() + )); + return; + } + }; + + let config = config.with_sample_rate(SampleRate(8000)); + let stream = match device.build_output_stream( + &config.into(), + move |data: &mut [i16], _info: &cpal::OutputCallbackInfo| { + output_cb(data, &state); + }, + |e| { + // trigger end_tx here? always more edge cases + debug!("error occurred in codec2 playback: {e:?}"); + }, + None, + ) { + Ok(s) => s, + Err(e) => { + let _ = setup_tx.send(Err( + M17Codec2Error::OutputStreamBuildError(output_card, e).into() + )); + return; + } + }; + match stream.play() { + Ok(()) => (), + Err(e) => { + let _ = setup_tx.send(Err( + M17Codec2Error::OutputStreamPlayError(output_card, e).into() + )); + return; + } + } + let _ = setup_tx.send(Ok(())); let _ = end.recv(); // it seems concrete impls of Stream have a Drop implementation that will handle termination } +/// Transmits a wave file as an M17 stream pub struct WavePlayer; impl WavePlayer { + /// Plays a wave file (blocking). + /// + /// * `path`: wave file to transmit, must be 8 kHz mono and 16-bit LE + /// * `tx`: a `TxHandle` obtained from an `M17App` + /// * `source`: address of transmission source + /// * `destination`: address of transmission destination + /// * `channel_access_number`: from 0 to 15, usually 0 pub fn play( path: PathBuf, tx: TxHandle, @@ -186,8 +246,8 @@ impl WavePlayer { loop { let mut last_one = false; - for mut out in out_buf.chunks_mut(8) { - for i in 0..160 { + for out in out_buf.chunks_mut(8) { + for i in in_buf.iter_mut() { let sample = match samples.next() { Some(Ok(sample)) => sample, _ => { @@ -195,16 +255,16 @@ impl WavePlayer { 0 } }; - in_buf[i] = sample; + *i = sample; } - codec.encode(&mut out, &in_buf); + codec.encode(out, &in_buf); } tx.transmit_stream_next(&StreamFrame { lich_idx: lsf_chunk as u8, lich_part: setup.lich_part(lsf_chunk as u8), frame_number, end_of_stream: last_one, - stream_data: out_buf.clone(), + stream_data: out_buf, }); frame_number += 1; lsf_chunk = (lsf_chunk + 1) % 6; @@ -218,3 +278,21 @@ impl WavePlayer { } } } + +#[derive(Debug, Error)] +pub enum M17Codec2Error { + #[error("selected card '{0}' does not exist or is in use")] + CardUnavailable(String), + + #[error("selected card '{0}' failed to list available output configs: '{1}'")] + OutputConfigsUnavailable(String, #[source] cpal::SupportedStreamConfigsError), + + #[error("selected card '{0}' did not offer a compatible output config type, either due to hardware limitations or because it is currently in use")] + SupportedOutputUnavailable(String), + + #[error("selected card '{0}' was unable to build an output stream: '{1}'")] + OutputStreamBuildError(String, #[source] cpal::BuildStreamError), + + #[error("selected card '{0}' was unable to play an output stream: '{1}'")] + OutputStreamPlayError(String, #[source] cpal::PlayStreamError), +}