]> code.octet-stream.net Git - m17rt/blob - m17app/src/soundcard.rs
Error handler for soundmodem components
[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 BuildStreamError, DevicesError, PlayStreamError, SampleFormat, SampleRate, Stream, StreamError,
12 SupportedStreamConfigsError,
13 };
14 use thiserror::Error;
15
16 use crate::soundmodem::{
17 InputSource, OutputBuffer, OutputSink, SoundmodemErrorSender, SoundmodemEvent,
18 };
19
20 pub struct Soundcard {
21 event_tx: SyncSender<SoundcardEvent>,
22 }
23
24 impl Soundcard {
25 pub fn new<S: Into<String>>(card_name: S) -> Result<Self, SoundcardError> {
26 let (card_tx, card_rx) = sync_channel(128);
27 let (setup_tx, setup_rx) = sync_channel(1);
28 spawn_soundcard_worker(card_rx, setup_tx, card_name.into());
29 match setup_rx.recv() {
30 Ok(Ok(())) => Ok(Self { event_tx: card_tx }),
31 Ok(Err(e)) => Err(e),
32 Err(_) => Err(SoundcardError::SoundcardInit),
33 }
34 }
35
36 pub fn input(&self) -> SoundcardInputSource {
37 SoundcardInputSource {
38 event_tx: self.event_tx.clone(),
39 }
40 }
41
42 pub fn output(&self) -> SoundcardOutputSink {
43 SoundcardOutputSink {
44 event_tx: self.event_tx.clone(),
45 }
46 }
47
48 pub fn set_rx_inverted(&self, inverted: bool) {
49 let _ = self.event_tx.send(SoundcardEvent::SetRxInverted(inverted));
50 }
51
52 pub fn set_tx_inverted(&self, inverted: bool) {
53 let _ = self.event_tx.send(SoundcardEvent::SetTxInverted(inverted));
54 }
55
56 pub fn supported_output_cards() -> Vec<String> {
57 let mut out = vec![];
58 let host = cpal::default_host();
59 let Ok(output_devices) = host.output_devices() else {
60 return out;
61 };
62 for d in output_devices {
63 let Ok(mut configs) = d.supported_output_configs() else {
64 continue;
65 };
66 if configs
67 .any(|config| config.channels() == 1 && config.sample_format() == SampleFormat::I16)
68 {
69 let Ok(name) = d.name() else {
70 continue;
71 };
72 out.push(name);
73 }
74 }
75 out.sort();
76 out
77 }
78
79 pub fn supported_input_cards() -> Vec<String> {
80 let mut out = vec![];
81 let host = cpal::default_host();
82 let Ok(input_devices) = host.input_devices() else {
83 return out;
84 };
85 for d in input_devices {
86 let Ok(mut configs) = d.supported_input_configs() else {
87 continue;
88 };
89 if configs
90 .any(|config| config.channels() == 1 && config.sample_format() == SampleFormat::I16)
91 {
92 let Ok(name) = d.name() else {
93 continue;
94 };
95 out.push(name);
96 }
97 }
98 out.sort();
99 out
100 }
101 }
102
103 enum SoundcardEvent {
104 SetRxInverted(bool),
105 SetTxInverted(bool),
106 StartInput {
107 samples: SyncSender<SoundmodemEvent>,
108 errors: SoundmodemErrorSender,
109 },
110 CloseInput,
111 StartOutput {
112 event_tx: SyncSender<SoundmodemEvent>,
113 buffer: Arc<RwLock<OutputBuffer>>,
114 errors: SoundmodemErrorSender,
115 },
116 CloseOutput,
117 }
118
119 pub struct SoundcardInputSource {
120 event_tx: SyncSender<SoundcardEvent>,
121 }
122
123 impl InputSource for SoundcardInputSource {
124 fn start(&self, samples: SyncSender<SoundmodemEvent>, errors: SoundmodemErrorSender) {
125 let _ = self
126 .event_tx
127 .send(SoundcardEvent::StartInput { samples, errors });
128 }
129
130 fn close(&self) {
131 let _ = self.event_tx.send(SoundcardEvent::CloseInput);
132 }
133 }
134
135 pub struct SoundcardOutputSink {
136 event_tx: SyncSender<SoundcardEvent>,
137 }
138
139 impl OutputSink for SoundcardOutputSink {
140 fn start(
141 &self,
142 event_tx: SyncSender<SoundmodemEvent>,
143 buffer: Arc<RwLock<OutputBuffer>>,
144 errors: SoundmodemErrorSender,
145 ) {
146 let _ = self.event_tx.send(SoundcardEvent::StartOutput {
147 event_tx,
148 buffer,
149 errors,
150 });
151 }
152
153 fn close(&self) {
154 let _ = self.event_tx.send(SoundcardEvent::CloseOutput);
155 }
156 }
157
158 fn spawn_soundcard_worker(
159 event_rx: Receiver<SoundcardEvent>,
160 setup_tx: SyncSender<Result<(), SoundcardError>>,
161 card_name: String,
162 ) {
163 std::thread::spawn(move || {
164 let host = cpal::default_host();
165 let Some(device) = host
166 .devices()
167 .unwrap()
168 .find(|d| d.name().unwrap() == card_name)
169 else {
170 let _ = setup_tx.send(Err(SoundcardError::CardNotFound(card_name)));
171 return;
172 };
173
174 let _ = setup_tx.send(Ok(()));
175 let mut rx_inverted = false;
176 let mut tx_inverted = false;
177 let mut input_stream: Option<Stream> = None;
178 let mut output_stream: Option<Stream> = None;
179
180 while let Ok(ev) = event_rx.recv() {
181 match ev {
182 SoundcardEvent::SetRxInverted(inv) => rx_inverted = inv,
183 SoundcardEvent::SetTxInverted(inv) => tx_inverted = inv,
184 SoundcardEvent::StartInput { samples, errors } => {
185 let mut input_configs = match device.supported_input_configs() {
186 Ok(c) => c,
187 Err(e) => {
188 errors.send_error(SoundcardError::SupportedConfigs(e));
189 continue;
190 }
191 };
192 let input_config = match input_configs
193 .find(|c| c.channels() == 1 && c.sample_format() == SampleFormat::I16)
194 {
195 Some(c) => c,
196 None => {
197 errors.send_error(SoundcardError::NoValidConfigAvailable);
198 continue;
199 }
200 };
201 let input_config = input_config.with_sample_rate(SampleRate(48000));
202 let errors_1 = errors.clone();
203 let stream = match device.build_input_stream(
204 &input_config.into(),
205 move |data: &[i16], _info: &cpal::InputCallbackInfo| {
206 let out: Vec<i16> = data
207 .iter()
208 .map(|s| if rx_inverted { s.saturating_neg() } else { *s })
209 .collect();
210 let _ = samples.try_send(SoundmodemEvent::BasebandInput(out.into()));
211 },
212 move |e| {
213 errors_1.send_error(SoundcardError::Stream(e));
214 },
215 None,
216 ) {
217 Ok(s) => s,
218 Err(e) => {
219 errors.send_error(SoundcardError::StreamBuild(e));
220 continue;
221 }
222 };
223 if let Err(e) = stream.play() {
224 errors.send_error(SoundcardError::StreamPlay(e));
225 continue;
226 }
227 input_stream = Some(stream);
228 }
229 SoundcardEvent::CloseInput => {
230 let _ = input_stream.take();
231 }
232 SoundcardEvent::StartOutput {
233 event_tx,
234 buffer,
235 errors,
236 } => {
237 let mut output_configs = match device.supported_output_configs() {
238 Ok(c) => c,
239 Err(e) => {
240 errors.send_error(SoundcardError::SupportedConfigs(e));
241 continue;
242 }
243 };
244 let output_config = match output_configs
245 .find(|c| c.channels() == 1 && c.sample_format() == SampleFormat::I16)
246 {
247 Some(c) => c,
248 None => {
249 errors.send_error(SoundcardError::NoValidConfigAvailable);
250 continue;
251 }
252 };
253 let output_config = output_config.with_sample_rate(SampleRate(48000));
254 let errors_1 = errors.clone();
255 let stream = match device.build_output_stream(
256 &output_config.into(),
257 move |data: &mut [i16], info: &cpal::OutputCallbackInfo| {
258 let mut taken = 0;
259 let ts = info.timestamp();
260 let latency = ts
261 .playback
262 .duration_since(&ts.callback)
263 .unwrap_or(Duration::ZERO);
264 let mut buffer = buffer.write().unwrap();
265 buffer.latency = latency;
266 for out in data.iter_mut() {
267 if let Some(s) = buffer.samples.pop_front() {
268 *out = if tx_inverted { s.saturating_neg() } else { s };
269 taken += 1;
270 } else if buffer.idling {
271 *out = 0;
272 } else {
273 let _ = event_tx.send(SoundmodemEvent::OutputUnderrun);
274 break;
275 }
276 }
277 let _ = event_tx.send(SoundmodemEvent::DidReadFromOutputBuffer {
278 len: taken,
279 timestamp: Instant::now(),
280 });
281 },
282 move |e| {
283 errors_1.send_error(SoundcardError::Stream(e));
284 },
285 None,
286 ) {
287 Ok(s) => s,
288 Err(e) => {
289 errors.send_error(SoundcardError::StreamBuild(e));
290 continue;
291 }
292 };
293 if let Err(e) = stream.play() {
294 errors.send_error(SoundcardError::StreamPlay(e));
295 continue;
296 }
297 output_stream = Some(stream);
298 }
299 SoundcardEvent::CloseOutput => {
300 let _ = output_stream.take();
301 }
302 }
303 }
304 });
305 }
306
307 #[derive(Debug, Error)]
308 pub enum SoundcardError {
309 #[error("sound card init aborted unexpectedly")]
310 SoundcardInit,
311
312 #[error("unable to enumerate devices: {0}")]
313 Host(DevicesError),
314
315 #[error("unable to locate sound card '{0}' - is it in use?")]
316 CardNotFound(String),
317
318 #[error("error occurred in soundcard i/o: {0}")]
319 Stream(#[source] StreamError),
320
321 #[error("unable to retrieve supported configs for soundcard: {0}")]
322 SupportedConfigs(#[source] SupportedStreamConfigsError),
323
324 #[error("could not find a suitable soundcard config")]
325 NoValidConfigAvailable,
326
327 #[error("unable to build soundcard stream: {0}")]
328 StreamBuild(#[source] BuildStreamError),
329
330 #[error("unable to play stream")]
331 StreamPlay(#[source] PlayStreamError),
332 }