use cpal::{
traits::{DeviceTrait, HostTrait, StreamTrait},
- SampleFormat, SampleRate, Stream,
+ BuildStreamError, DevicesError, PlayStreamError, SampleFormat, SampleRate, Stream, StreamError,
+ SupportedStreamConfigsError,
};
+use thiserror::Error;
-use crate::{
- error::M17Error,
- soundmodem::{InputSource, OutputBuffer, OutputSink, SoundmodemEvent},
+use crate::soundmodem::{
+ InputSource, OutputBuffer, OutputSink, SoundmodemErrorSender, SoundmodemEvent,
};
pub struct Soundcard {
}
impl Soundcard {
- pub fn new<S: Into<String>>(card_name: S) -> Result<Self, M17Error> {
+ pub fn new<S: Into<String>>(card_name: S) -> Result<Self, SoundcardError> {
let (card_tx, card_rx) = sync_channel(128);
let (setup_tx, setup_rx) = sync_channel(1);
spawn_soundcard_worker(card_rx, setup_tx, card_name.into());
match setup_rx.recv() {
Ok(Ok(())) => Ok(Self { event_tx: card_tx }),
Ok(Err(e)) => Err(e),
- Err(_) => Err(M17Error::SoundcardInit),
+ Err(_) => Err(SoundcardError::SoundcardInit),
}
}
continue;
};
if configs
- .find(|config| {
- config.channels() == 1 && config.sample_format() == SampleFormat::I16
- })
- .is_some()
+ .any(|config| config.channels() == 1 && config.sample_format() == SampleFormat::I16)
{
let Ok(name) = d.name() else {
continue;
continue;
};
if configs
- .find(|config| {
- config.channels() == 1 && config.sample_format() == SampleFormat::I16
- })
- .is_some()
+ .any(|config| config.channels() == 1 && config.sample_format() == SampleFormat::I16)
{
let Ok(name) = d.name() else {
continue;
SetTxInverted(bool),
StartInput {
samples: SyncSender<SoundmodemEvent>,
+ errors: SoundmodemErrorSender,
},
CloseInput,
StartOutput {
event_tx: SyncSender<SoundmodemEvent>,
buffer: Arc<RwLock<OutputBuffer>>,
+ errors: SoundmodemErrorSender,
},
CloseOutput,
}
}
impl InputSource for SoundcardInputSource {
- fn start(&self, samples: SyncSender<SoundmodemEvent>) {
- let _ = self.event_tx.send(SoundcardEvent::StartInput { samples });
+ fn start(&self, samples: SyncSender<SoundmodemEvent>, errors: SoundmodemErrorSender) {
+ let _ = self
+ .event_tx
+ .send(SoundcardEvent::StartInput { samples, errors });
}
fn close(&self) {
}
impl OutputSink for SoundcardOutputSink {
- fn start(&self, event_tx: SyncSender<SoundmodemEvent>, buffer: Arc<RwLock<OutputBuffer>>) {
- let _ = self
- .event_tx
- .send(SoundcardEvent::StartOutput { event_tx, buffer });
+ fn start(
+ &self,
+ event_tx: SyncSender<SoundmodemEvent>,
+ buffer: Arc<RwLock<OutputBuffer>>,
+ errors: SoundmodemErrorSender,
+ ) {
+ let _ = self.event_tx.send(SoundcardEvent::StartOutput {
+ event_tx,
+ buffer,
+ errors,
+ });
}
fn close(&self) {
fn spawn_soundcard_worker(
event_rx: Receiver<SoundcardEvent>,
- setup_tx: SyncSender<Result<(), M17Error>>,
+ setup_tx: SyncSender<Result<(), SoundcardError>>,
card_name: String,
) {
std::thread::spawn(move || {
.unwrap()
.find(|d| d.name().unwrap() == card_name)
else {
- let _ = setup_tx.send(Err(M17Error::SoundcardNotFound(card_name)));
+ let _ = setup_tx.send(Err(SoundcardError::CardNotFound(card_name)));
return;
};
match ev {
SoundcardEvent::SetRxInverted(inv) => rx_inverted = inv,
SoundcardEvent::SetTxInverted(inv) => tx_inverted = inv,
- SoundcardEvent::StartInput { samples } => {
- let mut input_configs = device.supported_input_configs().unwrap();
- let input_config = input_configs
+ SoundcardEvent::StartInput { samples, errors } => {
+ let mut input_configs = match device.supported_input_configs() {
+ Ok(c) => c,
+ Err(e) => {
+ errors.send_error(SoundcardError::SupportedConfigs(e));
+ continue;
+ }
+ };
+ let input_config = match input_configs
.find(|c| c.channels() == 1 && c.sample_format() == SampleFormat::I16)
- .unwrap()
- .with_sample_rate(SampleRate(48000));
- let stream = device
- .build_input_stream(
- &input_config.into(),
- move |data: &[i16], _info: &cpal::InputCallbackInfo| {
- let out: Vec<i16> = data
- .iter()
- .map(|s| if rx_inverted { s.saturating_neg() } else { *s })
- .collect();
- let _ =
- samples.try_send(SoundmodemEvent::BasebandInput(out.into()));
- },
- |e| {
- // TODO: abort?
- log::debug!("error occurred in soundcard input: {e:?}");
- },
- None,
- )
- .unwrap();
- stream.play().unwrap();
+ {
+ Some(c) => c,
+ None => {
+ errors.send_error(SoundcardError::NoValidConfigAvailable);
+ continue;
+ }
+ };
+ let input_config = input_config.with_sample_rate(SampleRate(48000));
+ let errors_1 = errors.clone();
+ let stream = match device.build_input_stream(
+ &input_config.into(),
+ move |data: &[i16], _info: &cpal::InputCallbackInfo| {
+ let out: Vec<i16> = data
+ .iter()
+ .map(|s| if rx_inverted { s.saturating_neg() } else { *s })
+ .collect();
+ let _ = samples.try_send(SoundmodemEvent::BasebandInput(out.into()));
+ },
+ move |e| {
+ errors_1.send_error(SoundcardError::Stream(e));
+ },
+ None,
+ ) {
+ Ok(s) => s,
+ Err(e) => {
+ errors.send_error(SoundcardError::StreamBuild(e));
+ continue;
+ }
+ };
+ if let Err(e) = stream.play() {
+ errors.send_error(SoundcardError::StreamPlay(e));
+ continue;
+ }
input_stream = Some(stream);
}
SoundcardEvent::CloseInput => {
let _ = input_stream.take();
}
- SoundcardEvent::StartOutput { event_tx, buffer } => {
- let mut output_configs = device.supported_output_configs().unwrap();
- // TODO: more error handling
- let output_config = output_configs
+ SoundcardEvent::StartOutput {
+ event_tx,
+ buffer,
+ errors,
+ } => {
+ let mut output_configs = match device.supported_output_configs() {
+ Ok(c) => c,
+ Err(e) => {
+ errors.send_error(SoundcardError::SupportedConfigs(e));
+ continue;
+ }
+ };
+ let output_config = match output_configs
.find(|c| c.channels() == 1 && c.sample_format() == SampleFormat::I16)
- .unwrap()
- .with_sample_rate(SampleRate(48000));
- let stream = device
- .build_output_stream(
- &output_config.into(),
- move |data: &mut [i16], info: &cpal::OutputCallbackInfo| {
- let mut taken = 0;
- let ts = info.timestamp();
- let latency = ts
- .playback
- .duration_since(&ts.callback)
- .unwrap_or(Duration::ZERO);
- let mut buffer = buffer.write().unwrap();
- buffer.latency = latency;
- for out in data.iter_mut() {
- if let Some(s) = buffer.samples.pop_front() {
- *out = if tx_inverted { s.saturating_neg() } else { s };
- taken += 1;
- } else if buffer.idling {
- *out = 0;
- } else {
- log::debug!("output soundcard had underrun");
- let _ = event_tx.send(SoundmodemEvent::OutputUnderrun);
- break;
- }
+ {
+ Some(c) => c,
+ None => {
+ errors.send_error(SoundcardError::NoValidConfigAvailable);
+ continue;
+ }
+ };
+ let output_config = output_config.with_sample_rate(SampleRate(48000));
+ let errors_1 = errors.clone();
+ let stream = match device.build_output_stream(
+ &output_config.into(),
+ move |data: &mut [i16], info: &cpal::OutputCallbackInfo| {
+ let mut taken = 0;
+ let ts = info.timestamp();
+ let latency = ts
+ .playback
+ .duration_since(&ts.callback)
+ .unwrap_or(Duration::ZERO);
+ let mut buffer = buffer.write().unwrap();
+ buffer.latency = latency;
+ for out in data.iter_mut() {
+ if let Some(s) = buffer.samples.pop_front() {
+ *out = if tx_inverted { s.saturating_neg() } else { s };
+ taken += 1;
+ } else if buffer.idling {
+ *out = 0;
+ } else {
+ let _ = event_tx.send(SoundmodemEvent::OutputUnderrun);
+ break;
}
- //debug!("latency is {} ms, taken {taken}", latency.as_millis());
- let _ = event_tx.send(SoundmodemEvent::DidReadFromOutputBuffer {
- len: taken,
- timestamp: Instant::now(),
- });
- },
- |e| {
- // TODO: abort?
- log::debug!("error occurred in soundcard output: {e:?}");
- },
- None,
- )
- .unwrap();
- stream.play().unwrap();
+ }
+ let _ = event_tx.send(SoundmodemEvent::DidReadFromOutputBuffer {
+ len: taken,
+ timestamp: Instant::now(),
+ });
+ },
+ move |e| {
+ errors_1.send_error(SoundcardError::Stream(e));
+ },
+ None,
+ ) {
+ Ok(s) => s,
+ Err(e) => {
+ errors.send_error(SoundcardError::StreamBuild(e));
+ continue;
+ }
+ };
+ if let Err(e) = stream.play() {
+ errors.send_error(SoundcardError::StreamPlay(e));
+ continue;
+ }
output_stream = Some(stream);
}
SoundcardEvent::CloseOutput => {
}
});
}
+
+#[derive(Debug, Error)]
+pub enum SoundcardError {
+ #[error("sound card init aborted unexpectedly")]
+ SoundcardInit,
+
+ #[error("unable to enumerate devices: {0}")]
+ Host(DevicesError),
+
+ #[error("unable to locate sound card '{0}' - is it in use?")]
+ CardNotFound(String),
+
+ #[error("error occurred in soundcard i/o: {0}")]
+ Stream(#[source] StreamError),
+
+ #[error("unable to retrieve supported configs for soundcard: {0}")]
+ SupportedConfigs(#[source] SupportedStreamConfigsError),
+
+ #[error("could not find a suitable soundcard config")]
+ NoValidConfigAvailable,
+
+ #[error("unable to build soundcard stream: {0}")]
+ StreamBuild(#[source] BuildStreamError),
+
+ #[error("unable to play stream")]
+ StreamPlay(#[source] PlayStreamError),
+}