]> code.octet-stream.net Git - m17rt/blob - m17codec2/src/lib.rs
Begin adding support for sound card output via codec2 adapter
[m17rt] / m17codec2 / src / lib.rs
1 use codec2::{Codec2, Codec2Mode};
2
3 use m17app::adapter::StreamAdapter;
4 use m17app::app::TxHandle;
5 use m17core::protocol::LsfFrame;
6
7 use std::collections::VecDeque;
8 use std::fs::File;
9 use std::io::Write;
10 use std::path::Path;
11 use std::sync::{
12 mpsc::{channel, Receiver, Sender},
13 Arc, Mutex,
14 };
15
16 use cpal::traits::DeviceTrait;
17 use cpal::traits::HostTrait;
18 use cpal::traits::StreamTrait;
19 use cpal::{Sample, SampleFormat, SampleRate};
20
21 use log::debug;
22
23 pub fn decode_codec2<P: AsRef<Path>>(data: &[u8], out_path: P) -> Vec<i16> {
24 let codec2 = Codec2::new(Codec2Mode::MODE_3200);
25 let var_name = codec2;
26 let mut codec = var_name;
27 let mut all_samples: Vec<i16> = vec![];
28 for i in 0..(data.len() / 8) {
29 let mut samples = vec![0; codec.samples_per_frame()];
30 codec.decode(&mut samples, &data[i * 8..((i + 1) * 8)]);
31 all_samples.append(&mut samples);
32 }
33
34 // dude this works
35 let mut speech_out = File::create(out_path).unwrap();
36 for b in &all_samples {
37 speech_out.write_all(&b.to_le_bytes()).unwrap();
38 }
39 all_samples
40 }
41
42 pub struct Codec2Adapter {
43 state: Arc<Mutex<AdapterState>>,
44 // TODO: make this configurable
45 output_card: String,
46 }
47
48 impl Codec2Adapter {
49 pub fn new() -> Self {
50 Self {
51 state: Arc::new(Mutex::new(AdapterState {
52 tx: None,
53 out_buf: VecDeque::new(),
54 codec2: Codec2::new(Codec2Mode::MODE_3200),
55 end_tx: None,
56 })),
57 output_card: "default".to_owned(),
58 }
59 }
60 }
61
62 struct AdapterState {
63 tx: Option<TxHandle>,
64 /// Circular buffer of output samples for playback
65 out_buf: VecDeque<i16>,
66 codec2: Codec2,
67 end_tx: Option<Sender<()>>,
68 }
69
70 impl StreamAdapter for Codec2Adapter {
71 fn adapter_registered(&self, _id: usize, handle: TxHandle) {
72 self.state.lock().unwrap().tx = Some(handle);
73
74 let (end_tx, end_rx) = channel();
75 let state = self.state.clone();
76 let output_card = self.output_card.clone();
77 std::thread::spawn(move || stream_thread(end_rx, state, output_card));
78 self.state.lock().unwrap().end_tx = Some(end_tx);
79 }
80
81 fn adapter_removed(&self) {
82 let mut state = self.state.lock().unwrap();
83 state.tx = None;
84 state.end_tx = None;
85 }
86
87 fn tnc_started(&self) {}
88
89 fn tnc_closed(&self) {}
90
91 fn stream_began(&self, lsf: LsfFrame) {
92 // for now we will assume:
93 // - unencrypted
94 // - data type is Voice (Codec2 3200), not Voice+Data
95 // TODO: is encryption handled here or in M17App, such that we get a decrypted stream?
96 // TODO: handle the Voice+Data combination with Codec2 1600
97 self.state.lock().unwrap().codec2 = Codec2::new(Codec2Mode::MODE_3200);
98 }
99
100 fn stream_data(&self, frame_number: u16, is_final: bool, data: Arc<[u8; 16]>) {
101 let mut state = self.state.lock().unwrap();
102 for encoded in data.chunks(8) {
103 if state.out_buf.len() < 1024 {
104 let mut samples = [i16::EQUILIBRIUM; 160]; // while assuming 3200
105 state.codec2.decode(&mut samples, encoded);
106 // TODO: maybe get rid of VecDeque so we can decode directly into ring buffer?
107 for s in samples {
108 state.out_buf.push_back(s);
109 }
110 } else {
111 debug!("out_buf overflow");
112 }
113 }
114 }
115 }
116
117 fn output_cb(data: &mut [i16], state: &Mutex<AdapterState>) {
118 let mut state = state.lock().unwrap();
119 debug!(
120 "sound card wants {} samples, we have {} in the buffer",
121 data.len(),
122 state.out_buf.len()
123 );
124 for d in data {
125 *d = state.out_buf.pop_front().unwrap_or(i16::EQUILIBRIUM);
126 }
127 }
128
129 /// Create and manage the stream from a dedicated thread since it's `!Send`
130 fn stream_thread(end: Receiver<()>, state: Arc<Mutex<AdapterState>>, output_card: String) {
131 let host = cpal::default_host();
132 let device = host
133 .output_devices()
134 .unwrap()
135 .find(|d| d.name().unwrap() == output_card)
136 .unwrap();
137 let mut configs = device.supported_output_configs().unwrap();
138 let config = configs
139 .find(|c| c.channels() == 1 && c.sample_format() == SampleFormat::I16)
140 .unwrap()
141 .with_sample_rate(SampleRate(8000));
142 let stream = device
143 .build_output_stream(
144 &config.into(),
145 move |data: &mut [i16], _: &cpal::OutputCallbackInfo| {
146 output_cb(data, &state);
147 },
148 |e| {
149 // trigger end_tx here? always more edge cases
150 debug!("error occurred in codec2 playback: {e:?}");
151 },
152 None,
153 )
154 .unwrap();
155 stream.play().unwrap();
156 let _ = end.recv();
157 // it seems concrete impls of Stream have a Drop implementation that will handle termination
158 }