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