]> code.octet-stream.net Git - m17rt/blob - m17app/src/soundcard.rs
Support i16 and i32
[m17rt] / m17app / src / soundcard.rs
1 use std::{
2 borrow::Borrow,
3 sync::{
4 Arc, RwLock,
5 mpsc::{Receiver, SyncSender, sync_channel},
6 },
7 time::{Duration, Instant},
8 };
9
10 use cpal::{
11 BuildStreamError, Device, DevicesError, InputCallbackInfo, OutputCallbackInfo, PlayStreamError,
12 SampleFormat, SampleRate, Stream, StreamError, SupportedStreamConfig,
13 SupportedStreamConfigRange, SupportedStreamConfigsError,
14 traits::{DeviceTrait, HostTrait, StreamTrait},
15 };
16 use num_traits::{NumCast, WrappingNeg};
17 use thiserror::Error;
18
19 use crate::soundmodem::{
20 InputSource, OutputBuffer, OutputSink, SoundmodemErrorSender, SoundmodemEvent,
21 };
22
23 /// A soundcard for used for transmitting/receiving baseband with a `Soundmodem`.
24 ///
25 /// Use `input()` and `output()` to retrieve source/sink handles for the soundmodem.
26 /// It is fine to use an input from one soundcard and and output from another.
27 ///
28 /// If you try to create more than one `Soundcard` instance at a time for the same card
29 /// then it may not work.
30 pub struct Soundcard {
31 event_tx: SyncSender<SoundcardEvent>,
32 }
33
34 impl Soundcard {
35 pub fn new<S: Into<String>>(card_name: S) -> Result<Self, SoundcardError> {
36 let (card_tx, card_rx) = sync_channel(128);
37 let (setup_tx, setup_rx) = sync_channel(1);
38 spawn_soundcard_worker(card_rx, setup_tx, card_name.into());
39 match setup_rx.recv() {
40 Ok(Ok(())) => Ok(Self { event_tx: card_tx }),
41 Ok(Err(e)) => Err(e),
42 Err(_) => Err(SoundcardError::SoundcardInit),
43 }
44 }
45
46 pub fn input(&self) -> SoundcardInputSource {
47 SoundcardInputSource {
48 event_tx: self.event_tx.clone(),
49 }
50 }
51
52 pub fn output(&self) -> SoundcardOutputSink {
53 SoundcardOutputSink {
54 event_tx: self.event_tx.clone(),
55 }
56 }
57
58 pub fn set_rx_inverted(&self, inverted: bool) {
59 let _ = self.event_tx.send(SoundcardEvent::SetRxInverted(inverted));
60 }
61
62 pub fn set_tx_inverted(&self, inverted: bool) {
63 let _ = self.event_tx.send(SoundcardEvent::SetTxInverted(inverted));
64 }
65
66 /// List soundcards supported for soundmodem output.
67 ///
68 /// Today, this requires support for a 48kHz sample rate.
69 pub fn supported_output_cards() -> Vec<String> {
70 let mut out = vec![];
71 let host = cpal::default_host();
72 let Ok(output_devices) = host.output_devices() else {
73 return out;
74 };
75 for d in output_devices {
76 let Ok(mut configs) = d.supported_output_configs() else {
77 continue;
78 };
79 if configs.any(config_is_compatible) {
80 let Ok(name) = d.name() else {
81 continue;
82 };
83 out.push(name);
84 }
85 }
86 out.sort();
87 out
88 }
89
90 /// List soundcards supported for soundmodem input.
91 ///
92 /// Today, this requires support for a 48kHz sample rate.
93 pub fn supported_input_cards() -> Vec<String> {
94 let mut out = vec![];
95 let host = cpal::default_host();
96 let Ok(input_devices) = host.input_devices() else {
97 return out;
98 };
99 for d in input_devices {
100 let Ok(mut configs) = d.supported_input_configs() else {
101 continue;
102 };
103 if configs.any(config_is_compatible) {
104 let Ok(name) = d.name() else {
105 continue;
106 };
107 out.push(name);
108 }
109 }
110 out.sort();
111 out
112 }
113 }
114
115 fn config_is_compatible<C: Borrow<SupportedStreamConfigRange>>(config: C) -> bool {
116 let config = config.borrow();
117 (config.channels() == 1 || config.channels() == 2)
118 && (config.sample_format() == SampleFormat::I16
119 || config.sample_format() == SampleFormat::I32)
120 && config.min_sample_rate().0 <= 48000
121 && config.max_sample_rate().0 >= 48000
122 }
123
124 enum SoundcardEvent {
125 SetRxInverted(bool),
126 SetTxInverted(bool),
127 StartInput {
128 samples: SyncSender<SoundmodemEvent>,
129 errors: SoundmodemErrorSender,
130 },
131 CloseInput,
132 StartOutput {
133 event_tx: SyncSender<SoundmodemEvent>,
134 buffer: Arc<RwLock<OutputBuffer>>,
135 errors: SoundmodemErrorSender,
136 },
137 CloseOutput,
138 }
139
140 pub struct SoundcardInputSource {
141 event_tx: SyncSender<SoundcardEvent>,
142 }
143
144 impl InputSource for SoundcardInputSource {
145 fn start(&self, samples: SyncSender<SoundmodemEvent>, errors: SoundmodemErrorSender) {
146 let _ = self
147 .event_tx
148 .send(SoundcardEvent::StartInput { samples, errors });
149 }
150
151 fn close(&self) {
152 let _ = self.event_tx.send(SoundcardEvent::CloseInput);
153 }
154 }
155
156 pub struct SoundcardOutputSink {
157 event_tx: SyncSender<SoundcardEvent>,
158 }
159
160 impl OutputSink for SoundcardOutputSink {
161 fn start(
162 &self,
163 event_tx: SyncSender<SoundmodemEvent>,
164 buffer: Arc<RwLock<OutputBuffer>>,
165 errors: SoundmodemErrorSender,
166 ) {
167 let _ = self.event_tx.send(SoundcardEvent::StartOutput {
168 event_tx,
169 buffer,
170 errors,
171 });
172 }
173
174 fn close(&self) {
175 let _ = self.event_tx.send(SoundcardEvent::CloseOutput);
176 }
177 }
178
179 fn build_input_cb<T: NumCast + WrappingNeg + Clone>(
180 samples: SyncSender<SoundmodemEvent>,
181 channels: u16,
182 rx_inverted: bool,
183 ) -> impl Fn(&[T], &InputCallbackInfo) {
184 move |data: &[T], _info: &cpal::InputCallbackInfo| {
185 let mut out = vec![];
186 for d in data.chunks(channels as usize) {
187 // if we were given multi-channel input we'll pick the first channel
188 let mut sample = d[0].clone();
189 if rx_inverted {
190 sample = sample.wrapping_neg();
191 }
192 out.push(NumCast::from(sample).unwrap());
193 }
194 let _ = samples.try_send(SoundmodemEvent::BasebandInput(out.into()));
195 }
196 }
197
198 fn build_input_stream(
199 device: &Device,
200 input_config: SupportedStreamConfig,
201 errors: SoundmodemErrorSender,
202 samples: SyncSender<SoundmodemEvent>,
203 channels: u16,
204 rx_inverted: bool,
205 ) -> Result<Stream, BuildStreamError> {
206 if input_config.sample_format() == SampleFormat::I16 {
207 device.build_input_stream(
208 &input_config.into(),
209 build_input_cb::<i16>(samples, channels, rx_inverted),
210 move |e| {
211 errors.send_error(SoundcardError::Stream(e));
212 },
213 None,
214 )
215 } else {
216 device.build_input_stream(
217 &input_config.into(),
218 build_input_cb::<i32>(samples, channels, rx_inverted),
219 move |e| {
220 errors.send_error(SoundcardError::Stream(e));
221 },
222 None,
223 )
224 }
225 }
226
227 fn build_output_cb<T: NumCast + WrappingNeg + Clone>(
228 event_tx: SyncSender<SoundmodemEvent>,
229 channels: u16,
230 tx_inverted: bool,
231 buffer: Arc<RwLock<OutputBuffer>>,
232 ) -> impl Fn(&mut [T], &OutputCallbackInfo) {
233 move |data: &mut [T], info: &cpal::OutputCallbackInfo| {
234 let mut taken = 0;
235 let ts = info.timestamp();
236 let latency = ts
237 .playback
238 .duration_since(&ts.callback)
239 .unwrap_or(Duration::ZERO);
240 let mut buffer = buffer.write().unwrap();
241 buffer.latency = latency;
242 for out in data.chunks_mut(channels as usize) {
243 if let Some(s) = buffer.samples.pop_front() {
244 out.fill(NumCast::from(if tx_inverted { s.saturating_neg() } else { s }).unwrap());
245 taken += 1;
246 } else if buffer.idling {
247 out.fill(NumCast::from(0).unwrap());
248 } else {
249 let _ = event_tx.send(SoundmodemEvent::OutputUnderrun);
250 break;
251 }
252 }
253 let _ = event_tx.send(SoundmodemEvent::DidReadFromOutputBuffer {
254 len: taken,
255 timestamp: Instant::now(),
256 });
257 }
258 }
259
260 fn build_output_stream(
261 device: &Device,
262 output_config: SupportedStreamConfig,
263 errors: SoundmodemErrorSender,
264 event_tx: SyncSender<SoundmodemEvent>,
265 channels: u16,
266 tx_inverted: bool,
267 buffer: Arc<RwLock<OutputBuffer>>,
268 ) -> Result<Stream, BuildStreamError> {
269 if output_config.sample_format() == SampleFormat::I16 {
270 device.build_output_stream(
271 &output_config.into(),
272 build_output_cb::<i16>(event_tx, channels, tx_inverted, buffer),
273 move |e| {
274 errors.send_error(SoundcardError::Stream(e));
275 },
276 None,
277 )
278 } else {
279 device.build_output_stream(
280 &output_config.into(),
281 build_output_cb::<i32>(event_tx, channels, tx_inverted, buffer),
282 move |e| {
283 errors.send_error(SoundcardError::Stream(e));
284 },
285 None,
286 )
287 }
288 }
289
290 fn spawn_soundcard_worker(
291 event_rx: Receiver<SoundcardEvent>,
292 setup_tx: SyncSender<Result<(), SoundcardError>>,
293 card_name: String,
294 ) {
295 std::thread::spawn(move || {
296 let host = cpal::default_host();
297 let Some(device) = host
298 .devices()
299 .unwrap()
300 .find(|d| d.name().unwrap() == card_name)
301 else {
302 let _ = setup_tx.send(Err(SoundcardError::CardNotFound(card_name)));
303 return;
304 };
305
306 let _ = setup_tx.send(Ok(()));
307 let mut rx_inverted = false;
308 let mut tx_inverted = false;
309 let mut input_stream: Option<Stream> = None;
310 let mut output_stream: Option<Stream> = None;
311
312 while let Ok(ev) = event_rx.recv() {
313 match ev {
314 SoundcardEvent::SetRxInverted(inv) => rx_inverted = inv,
315 SoundcardEvent::SetTxInverted(inv) => tx_inverted = inv,
316 SoundcardEvent::StartInput { samples, errors } => {
317 let mut input_configs = match device.supported_input_configs() {
318 Ok(c) => c,
319 Err(e) => {
320 errors.send_error(SoundcardError::SupportedConfigs(e));
321 continue;
322 }
323 };
324 let input_config = match input_configs.find(|c| config_is_compatible(c)) {
325 Some(c) => c,
326 None => {
327 errors.send_error(SoundcardError::NoValidConfigAvailable);
328 continue;
329 }
330 };
331 let input_config = input_config.with_sample_rate(SampleRate(48000));
332 let channels = input_config.channels();
333 let stream = match build_input_stream(
334 &device,
335 input_config,
336 errors.clone(),
337 samples,
338 channels,
339 rx_inverted,
340 ) {
341 Ok(s) => s,
342 Err(e) => {
343 errors.send_error(SoundcardError::StreamBuild(e));
344 continue;
345 }
346 };
347 if let Err(e) = stream.play() {
348 errors.send_error(SoundcardError::StreamPlay(e));
349 continue;
350 }
351 input_stream = Some(stream);
352 }
353 SoundcardEvent::CloseInput => {
354 let _ = input_stream.take();
355 }
356 SoundcardEvent::StartOutput {
357 event_tx,
358 buffer,
359 errors,
360 } => {
361 let mut output_configs = match device.supported_output_configs() {
362 Ok(c) => c,
363 Err(e) => {
364 errors.send_error(SoundcardError::SupportedConfigs(e));
365 continue;
366 }
367 };
368 let output_config = match output_configs.find(|c| config_is_compatible(c)) {
369 Some(c) => c,
370 None => {
371 errors.send_error(SoundcardError::NoValidConfigAvailable);
372 continue;
373 }
374 };
375 let output_config = output_config.with_sample_rate(SampleRate(48000));
376 let channels = output_config.channels();
377 let stream = match build_output_stream(
378 &device,
379 output_config,
380 errors.clone(),
381 event_tx,
382 channels,
383 tx_inverted,
384 buffer,
385 ) {
386 Ok(s) => s,
387 Err(e) => {
388 errors.send_error(SoundcardError::StreamBuild(e));
389 continue;
390 }
391 };
392 if let Err(e) = stream.play() {
393 errors.send_error(SoundcardError::StreamPlay(e));
394 continue;
395 }
396 output_stream = Some(stream);
397 }
398 SoundcardEvent::CloseOutput => {
399 let _ = output_stream.take();
400 }
401 }
402 }
403 });
404 }
405
406 #[derive(Debug, Error)]
407 pub enum SoundcardError {
408 #[error("sound card init aborted unexpectedly")]
409 SoundcardInit,
410
411 #[error("unable to enumerate devices: {0}")]
412 Host(DevicesError),
413
414 #[error("unable to locate sound card '{0}' - is it in use?")]
415 CardNotFound(String),
416
417 #[error("error occurred in soundcard i/o: {0}")]
418 Stream(#[source] StreamError),
419
420 #[error("unable to retrieve supported configs for soundcard: {0}")]
421 SupportedConfigs(#[source] SupportedStreamConfigsError),
422
423 #[error("could not find a suitable soundcard config")]
424 NoValidConfigAvailable,
425
426 #[error("unable to build soundcard stream: {0}")]
427 StreamBuild(#[source] BuildStreamError),
428
429 #[error("unable to play stream")]
430 StreamPlay(#[source] PlayStreamError),
431 }