]> code.octet-stream.net Git - m17rt/blob - m17app/src/soundcard.rs
Fix timing bugs and add documentation
[m17rt] / m17app / src / soundcard.rs
1 use std::{
2 sync::{
3 mpsc::{sync_channel, Receiver, SyncSender},
4 Arc, RwLock,
5 },
6 time::{Duration, Instant},
7 };
8
9 use cpal::{
10 traits::{DeviceTrait, HostTrait, StreamTrait},
11 SampleFormat, SampleRate, Stream,
12 };
13
14 use crate::{
15 error::M17Error,
16 soundmodem::{InputSource, OutputBuffer, OutputSink, SoundmodemEvent},
17 };
18
19 pub struct Soundcard {
20 event_tx: SyncSender<SoundcardEvent>,
21 }
22
23 impl Soundcard {
24 pub fn new<S: Into<String>>(card_name: S) -> Result<Self, M17Error> {
25 let (card_tx, card_rx) = sync_channel(128);
26 let (setup_tx, setup_rx) = sync_channel(1);
27 spawn_soundcard_worker(card_rx, setup_tx, card_name.into());
28 match setup_rx.recv() {
29 Ok(Ok(())) => Ok(Self { event_tx: card_tx }),
30 Ok(Err(e)) => Err(e),
31 Err(_) => Err(M17Error::SoundcardInit),
32 }
33 }
34
35 pub fn input(&self) -> SoundcardInputSource {
36 SoundcardInputSource {
37 event_tx: self.event_tx.clone(),
38 }
39 }
40
41 pub fn output(&self) -> SoundcardOutputSink {
42 SoundcardOutputSink {
43 event_tx: self.event_tx.clone(),
44 }
45 }
46
47 pub fn set_rx_inverted(&self, inverted: bool) {
48 let _ = self.event_tx.send(SoundcardEvent::SetRxInverted(inverted));
49 }
50
51 pub fn set_tx_inverted(&self, inverted: bool) {
52 let _ = self.event_tx.send(SoundcardEvent::SetTxInverted(inverted));
53 }
54
55 pub fn supported_output_cards() -> Vec<String> {
56 let mut out = vec![];
57 let host = cpal::default_host();
58 let Ok(output_devices) = host.output_devices() else {
59 return out;
60 };
61 for d in output_devices {
62 let Ok(mut configs) = d.supported_output_configs() else {
63 continue;
64 };
65 if configs
66 .any(|config| config.channels() == 1 && config.sample_format() == SampleFormat::I16)
67 {
68 let Ok(name) = d.name() else {
69 continue;
70 };
71 out.push(name);
72 }
73 }
74 out.sort();
75 out
76 }
77
78 pub fn supported_input_cards() -> Vec<String> {
79 let mut out = vec![];
80 let host = cpal::default_host();
81 let Ok(input_devices) = host.input_devices() else {
82 return out;
83 };
84 for d in input_devices {
85 let Ok(mut configs) = d.supported_input_configs() else {
86 continue;
87 };
88 if configs
89 .any(|config| config.channels() == 1 && config.sample_format() == SampleFormat::I16)
90 {
91 let Ok(name) = d.name() else {
92 continue;
93 };
94 out.push(name);
95 }
96 }
97 out.sort();
98 out
99 }
100 }
101
102 enum SoundcardEvent {
103 SetRxInverted(bool),
104 SetTxInverted(bool),
105 StartInput {
106 samples: SyncSender<SoundmodemEvent>,
107 },
108 CloseInput,
109 StartOutput {
110 event_tx: SyncSender<SoundmodemEvent>,
111 buffer: Arc<RwLock<OutputBuffer>>,
112 },
113 CloseOutput,
114 }
115
116 pub struct SoundcardInputSource {
117 event_tx: SyncSender<SoundcardEvent>,
118 }
119
120 impl InputSource for SoundcardInputSource {
121 fn start(&self, samples: SyncSender<SoundmodemEvent>) {
122 let _ = self.event_tx.send(SoundcardEvent::StartInput { samples });
123 }
124
125 fn close(&self) {
126 let _ = self.event_tx.send(SoundcardEvent::CloseInput);
127 }
128 }
129
130 pub struct SoundcardOutputSink {
131 event_tx: SyncSender<SoundcardEvent>,
132 }
133
134 impl OutputSink for SoundcardOutputSink {
135 fn start(&self, event_tx: SyncSender<SoundmodemEvent>, buffer: Arc<RwLock<OutputBuffer>>) {
136 let _ = self
137 .event_tx
138 .send(SoundcardEvent::StartOutput { event_tx, buffer });
139 }
140
141 fn close(&self) {
142 let _ = self.event_tx.send(SoundcardEvent::CloseOutput);
143 }
144 }
145
146 fn spawn_soundcard_worker(
147 event_rx: Receiver<SoundcardEvent>,
148 setup_tx: SyncSender<Result<(), M17Error>>,
149 card_name: String,
150 ) {
151 std::thread::spawn(move || {
152 let host = cpal::default_host();
153 let Some(device) = host
154 .devices()
155 .unwrap()
156 .find(|d| d.name().unwrap() == card_name)
157 else {
158 let _ = setup_tx.send(Err(M17Error::SoundcardNotFound(card_name)));
159 return;
160 };
161
162 let _ = setup_tx.send(Ok(()));
163 let mut rx_inverted = false;
164 let mut tx_inverted = false;
165 let mut input_stream: Option<Stream> = None;
166 let mut output_stream: Option<Stream> = None;
167
168 while let Ok(ev) = event_rx.recv() {
169 match ev {
170 SoundcardEvent::SetRxInverted(inv) => rx_inverted = inv,
171 SoundcardEvent::SetTxInverted(inv) => tx_inverted = inv,
172 SoundcardEvent::StartInput { samples } => {
173 let mut input_configs = device.supported_input_configs().unwrap();
174 let input_config = input_configs
175 .find(|c| c.channels() == 1 && c.sample_format() == SampleFormat::I16)
176 .unwrap()
177 .with_sample_rate(SampleRate(48000));
178 let stream = device
179 .build_input_stream(
180 &input_config.into(),
181 move |data: &[i16], _info: &cpal::InputCallbackInfo| {
182 let out: Vec<i16> = data
183 .iter()
184 .map(|s| if rx_inverted { s.saturating_neg() } else { *s })
185 .collect();
186 let _ =
187 samples.try_send(SoundmodemEvent::BasebandInput(out.into()));
188 },
189 |e| {
190 // TODO: abort?
191 log::debug!("error occurred in soundcard input: {e:?}");
192 },
193 None,
194 )
195 .unwrap();
196 stream.play().unwrap();
197 input_stream = Some(stream);
198 }
199 SoundcardEvent::CloseInput => {
200 let _ = input_stream.take();
201 }
202 SoundcardEvent::StartOutput { event_tx, buffer } => {
203 let mut output_configs = device.supported_output_configs().unwrap();
204 // TODO: more error handling
205 let output_config = output_configs
206 .find(|c| c.channels() == 1 && c.sample_format() == SampleFormat::I16)
207 .unwrap()
208 .with_sample_rate(SampleRate(48000));
209 let stream = device
210 .build_output_stream(
211 &output_config.into(),
212 move |data: &mut [i16], info: &cpal::OutputCallbackInfo| {
213 let mut taken = 0;
214 let ts = info.timestamp();
215 let latency = ts
216 .playback
217 .duration_since(&ts.callback)
218 .unwrap_or(Duration::ZERO);
219 let mut buffer = buffer.write().unwrap();
220 buffer.latency = latency;
221 for out in data.iter_mut() {
222 if let Some(s) = buffer.samples.pop_front() {
223 *out = if tx_inverted { s.saturating_neg() } else { s };
224 taken += 1;
225 } else if buffer.idling {
226 *out = 0;
227 } else {
228 log::debug!("output soundcard had underrun");
229 let _ = event_tx.send(SoundmodemEvent::OutputUnderrun);
230 break;
231 }
232 }
233 //debug!("latency is {} ms, taken {taken}", latency.as_millis());
234 let _ = event_tx.send(SoundmodemEvent::DidReadFromOutputBuffer {
235 len: taken,
236 timestamp: Instant::now(),
237 });
238 },
239 |e| {
240 // TODO: abort?
241 log::debug!("error occurred in soundcard output: {e:?}");
242 },
243 None,
244 )
245 .unwrap();
246 stream.play().unwrap();
247 output_stream = Some(stream);
248 }
249 SoundcardEvent::CloseOutput => {
250 let _ = output_stream.take();
251 }
252 }
253 }
254 });
255 }