X-Git-Url: https://code.octet-stream.net/m17rt/blobdiff_plain/2fb25de49daca6ddff6f5af13bcf7c314aafafb3..5be3b9f857eefe983fbadae517a25da05b8b8982:/m17codec2/src/lib.rs diff --git a/m17codec2/src/lib.rs b/m17codec2/src/lib.rs old mode 100755 new mode 100644 index 2f05701..693cb09 --- a/m17codec2/src/lib.rs +++ b/m17codec2/src/lib.rs @@ -12,6 +12,9 @@ use m17app::error::AdapterError; use m17app::link_setup::LinkSetup; use m17app::link_setup::M17Address; use m17app::StreamFrame; +use rubato::Resampler; +use rubato::SincFixedIn; +use rubato::SincInterpolationParameters; use std::collections::VecDeque; use std::fs::File; use std::io::Write; @@ -25,6 +28,10 @@ use std::time::Duration; use std::time::Instant; use thiserror::Error; +pub mod soundcards; + +/// Write one or more 8-byte chunks of 3200-bit Codec2 to a raw S16LE file +/// and return the samples. pub fn decode_codec2>(data: &[u8], out_path: P) -> Vec { let codec2 = Codec2::new(Codec2Mode::MODE_3200); let var_name = codec2; @@ -35,8 +42,6 @@ pub fn decode_codec2>(data: &[u8], out_path: P) -> Vec { codec.decode(&mut samples, &data[i * 8..((i + 1) * 8)]); all_samples.append(&mut samples); } - - // dude this works let mut speech_out = File::create(out_path).unwrap(); for b in &all_samples { speech_out.write_all(&b.to_le_bytes()).unwrap(); @@ -47,8 +52,7 @@ pub fn decode_codec2>(data: &[u8], out_path: P) -> Vec { /// Subscribes to M17 streams and attempts to play the decoded Codec2 pub struct Codec2Adapter { state: Arc>, - // TODO: make this configurable - output_card: String, + output_card: Option, } impl Codec2Adapter { @@ -59,11 +63,15 @@ impl Codec2Adapter { out_buf: VecDeque::new(), codec2: Codec2::new(Codec2Mode::MODE_3200), end_tx: None, + resampler: None, })), - // TODO: this doesn't work on rpi. Use default_output_device() by default - output_card: "default".to_owned(), + output_card: None, } } + + pub fn set_output_card>(&mut self, card_name: S) { + self.output_card = Some(card_name.into()); + } } impl Default for Codec2Adapter { @@ -78,6 +86,7 @@ struct AdapterState { out_buf: VecDeque, codec2: Codec2, end_tx: Option>, + resampler: Option>, } impl StreamAdapter for Codec2Adapter { @@ -91,7 +100,21 @@ impl StreamAdapter for Codec2Adapter { 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 - setup_rx.recv()? + let sample_rate = setup_rx.recv()??; + debug!("selected codec2 output sample rate {sample_rate}"); + if sample_rate != 8000 { + let params = SincInterpolationParameters { + sinc_len: 256, + f_cutoff: 0.95, + oversampling_factor: 256, + interpolation: rubato::SincInterpolationType::Cubic, + window: rubato::WindowFunction::BlackmanHarris2, + }; + // TODO: fix unwrap + self.state.lock().unwrap().resampler = + Some(SincFixedIn::new(sample_rate as f64 / 8000f64, 1.0, params, 160, 1).unwrap()); + } + Ok(()) } fn close(&self) -> Result<(), AdapterError> { @@ -113,12 +136,21 @@ impl StreamAdapter for Codec2Adapter { fn stream_data(&self, _frame_number: u16, _is_final: bool, data: Arc<[u8; 16]>) { let mut state = self.state.lock().unwrap(); for encoded in data.chunks(8) { - if state.out_buf.len() < 1024 { + if state.out_buf.len() < 8192 { let mut samples = [i16::EQUILIBRIUM; 160]; // while assuming 3200 state.codec2.decode(&mut samples, encoded); - // TODO: maybe get rid of VecDeque so we can decode directly into ring buffer? - for s in samples { - state.out_buf.push_back(s); + if let Some(resampler) = state.resampler.as_mut() { + let samples_f: Vec = + samples.iter().map(|s| *s as f32 / 16384.0f32).collect(); + let res = resampler.process(&vec![samples_f], None).unwrap(); + for s in &res[0] { + state.out_buf.push_back((s * 16383.0f32) as i16); + } + } else { + // TODO: maybe get rid of VecDeque so we can decode directly into ring buffer? + for s in samples { + state.out_buf.push_back(s); + } } } else { debug!("out_buf overflow"); @@ -127,61 +159,78 @@ impl StreamAdapter for Codec2Adapter { } } -fn output_cb(data: &mut [i16], state: &Mutex) { +fn output_cb(data: &mut [i16], state: &Mutex, channels: u16) { let mut state = state.lock().unwrap(); - for d in data { - *d = state.out_buf.pop_front().unwrap_or(i16::EQUILIBRIUM); + for d in data.chunks_mut(channels as usize) { + d.fill(state.out_buf.pop_front().unwrap_or(i16::EQUILIBRIUM)); } } /// Create and manage the stream from a dedicated thread since it's `!Send` fn stream_thread( end: Receiver<()>, - setup_tx: Sender>, + setup_tx: Sender>, state: Arc>, - output_card: String, + output_card: Option, ) { let host = cpal::default_host(); - let device = match host - .output_devices() - .unwrap() - .find(|d| d.name().unwrap() == output_card) - { - Some(d) => d, - None => { - let _ = setup_tx.send(Err(M17Codec2Error::CardUnavailable(output_card).into())); - return; + let device = if let Some(output_card) = output_card { + // TODO: more error handling for unwraps + match host + .output_devices() + .unwrap() + .find(|d| d.name().unwrap() == output_card) + { + Some(d) => d, + None => { + let _ = setup_tx.send(Err(M17Codec2Error::CardUnavailable(output_card).into())); + return; + } + } + } else { + match host.default_output_device() { + Some(d) => d, + None => { + let _ = setup_tx.send(Err(M17Codec2Error::DefaultCardUnavailable.into())); + return; + } } }; + let card_name = device.name().unwrap(); let mut configs = match device.supported_output_configs() { Ok(c) => c, Err(e) => { - let _ = setup_tx.send(Err(M17Codec2Error::OutputConfigsUnavailable( - output_card, - e, - ) - .into())); + let _ = setup_tx.send(Err( + M17Codec2Error::OutputConfigsUnavailable(card_name, 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 = match configs.find(|c| c.channels() == 1 && c.sample_format() == SampleFormat::I16) - { + let config = match configs.find(|c| { + (c.channels() == 1 || c.channels() == 2) && c.sample_format() == SampleFormat::I16 + }) { Some(c) => c, None => { let _ = setup_tx.send(Err( - M17Codec2Error::SupportedOutputUnavailable(output_card).into() + M17Codec2Error::SupportedOutputUnavailable(card_name).into() )); return; } }; - let config = config.with_sample_rate(SampleRate(8000)); + let target_sample_rate = + if config.min_sample_rate().0 <= 8000 && config.max_sample_rate().0 >= 8000 { + 8000 + } else { + config.max_sample_rate().0 + }; + let channels = config.channels(); + + let config = config.with_sample_rate(SampleRate(target_sample_rate)); let stream = match device.build_output_stream( &config.into(), move |data: &mut [i16], _info: &cpal::OutputCallbackInfo| { - output_cb(data, &state); + output_cb(data, &state, channels); }, |e| { // trigger end_tx here? always more edge cases @@ -192,7 +241,7 @@ fn stream_thread( Ok(s) => s, Err(e) => { let _ = setup_tx.send(Err( - M17Codec2Error::OutputStreamBuildError(output_card, e).into() + M17Codec2Error::OutputStreamBuildError(card_name, e).into() )); return; } @@ -201,12 +250,12 @@ fn stream_thread( Ok(()) => (), Err(e) => { let _ = setup_tx.send(Err( - M17Codec2Error::OutputStreamPlayError(output_card, e).into() + M17Codec2Error::OutputStreamPlayError(card_name, e).into() )); return; } } - let _ = setup_tx.send(Ok(())); + let _ = setup_tx.send(Ok(target_sample_rate)); let _ = end.recv(); // it seems concrete impls of Stream have a Drop implementation that will handle termination } @@ -284,6 +333,9 @@ pub enum M17Codec2Error { #[error("selected card '{0}' does not exist or is in use")] CardUnavailable(String), + #[error("default output card is unavailable")] + DefaultCardUnavailable, + #[error("selected card '{0}' failed to list available output configs: '{1}'")] OutputConfigsUnavailable(String, #[source] cpal::SupportedStreamConfigsError),