"windows-sys 0.59.0",
]
+[[package]]
+name = "ascii_table"
+version = "4.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fb3cd3541590f14025990035bb02b205870f8ca3f5297956dd2c6b32e6470fa1"
+
[[package]]
name = "autocfg"
version = "1.3.0"
name = "m17rt-soundcards"
version = "0.1.0"
dependencies = [
+ "ascii_table",
"m17app",
+ "m17codec2",
]
[[package]]
use std::{
+ borrow::Borrow,
sync::{
mpsc::{sync_channel, Receiver, SyncSender},
Arc, RwLock,
use cpal::{
traits::{DeviceTrait, HostTrait, StreamTrait},
BuildStreamError, DevicesError, PlayStreamError, SampleFormat, SampleRate, Stream, StreamError,
- SupportedStreamConfigsError,
+ SupportedStreamConfigRange, SupportedStreamConfigsError,
};
use thiserror::Error;
InputSource, OutputBuffer, OutputSink, SoundmodemErrorSender, SoundmodemEvent,
};
+/// A soundcard for used for transmitting/receiving baseband with a `Soundmodem`.
+///
+/// Use `input()` and `output()` to retrieve source/sink handles for the soundmodem.
+/// It is fine to use an input from one soundcard and and output from another.
+///
+/// If you try to create more than one `Soundcard` instance at a time for the same card
+/// then it may not work.
pub struct Soundcard {
event_tx: SyncSender<SoundcardEvent>,
}
let _ = self.event_tx.send(SoundcardEvent::SetTxInverted(inverted));
}
+ /// List soundcards supported for soundmodem output.
+ ///
+ /// Today, this requires support for a 48kHz sample rate.
pub fn supported_output_cards() -> Vec<String> {
let mut out = vec![];
let host = cpal::default_host();
let Ok(mut configs) = d.supported_output_configs() else {
continue;
};
- if configs
- .any(|config| config.channels() == 1 && config.sample_format() == SampleFormat::I16)
- {
+ if configs.any(config_is_compatible) {
let Ok(name) = d.name() else {
continue;
};
out
}
+ /// List soundcards supported for soundmodem input.
+ ///
+ /// Today, this requires support for a 48kHz sample rate.
pub fn supported_input_cards() -> Vec<String> {
let mut out = vec![];
let host = cpal::default_host();
let Ok(mut configs) = d.supported_input_configs() else {
continue;
};
- if configs
- .any(|config| config.channels() == 1 && config.sample_format() == SampleFormat::I16)
- {
+ if configs.any(config_is_compatible) {
let Ok(name) = d.name() else {
continue;
};
}
}
+fn config_is_compatible<C: Borrow<SupportedStreamConfigRange>>(config: C) -> bool {
+ let config = config.borrow();
+ (config.channels() == 1 || config.channels() == 2)
+ && config.sample_format() == SampleFormat::I16
+ && config.min_sample_rate().0 <= 48000
+ && config.max_sample_rate().0 >= 48000
+}
+
enum SoundcardEvent {
SetRxInverted(bool),
SetTxInverted(bool),
continue;
}
};
- let input_config = match input_configs
- .find(|c| c.channels() == 1 && c.sample_format() == SampleFormat::I16)
- {
+ let input_config = match input_configs.find(|c| config_is_compatible(c)) {
Some(c) => c,
None => {
errors.send_error(SoundcardError::NoValidConfigAvailable);
}
};
let input_config = input_config.with_sample_rate(SampleRate(48000));
+ let channels = input_config.channels();
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 mut out = vec![];
+ for d in data.chunks(channels as usize) {
+ // if we were given multi-channel input we'll pick the first channel
+ let mut sample = d[0];
+ if rx_inverted {
+ sample = sample.saturating_neg();
+ }
+ out.push(sample);
+ }
let _ = samples.try_send(SoundmodemEvent::BasebandInput(out.into()));
},
move |e| {
continue;
}
};
- let output_config = match output_configs
- .find(|c| c.channels() == 1 && c.sample_format() == SampleFormat::I16)
- {
+ let output_config = match output_configs.find(|c| config_is_compatible(c)) {
Some(c) => c,
None => {
errors.send_error(SoundcardError::NoValidConfigAvailable);
}
};
let output_config = output_config.with_sample_rate(SampleRate(48000));
+ let channels = output_config.channels();
let errors_1 = errors.clone();
let stream = match device.build_output_stream(
&output_config.into(),
.unwrap_or(Duration::ZERO);
let mut buffer = buffer.write().unwrap();
buffer.latency = latency;
- for out in data.iter_mut() {
+ for out in data.chunks_mut(channels as usize) {
if let Some(s) = buffer.samples.pop_front() {
- *out = if tx_inverted { s.saturating_neg() } else { s };
+ out.fill(if tx_inverted { s.saturating_neg() } else { s });
taken += 1;
} else if buffer.idling {
- *out = 0;
+ out.fill(0);
} else {
let _ = event_tx.send(SoundmodemEvent::OutputUnderrun);
break;
pub mod error;
pub mod rx;
-pub mod soundcards;
pub mod tx;
pub use error::M17Codec2Error;
pub fn set_output_card<S: Into<String>>(&mut self, card_name: S) {
self.output_card = Some(card_name.into());
}
+
+ /// List sound cards supported for audio output.
+ ///
+ /// M17RT will handle any card with 1 or 2 channels and 16-bit output.
+ pub fn supported_output_cards() -> Vec<String> {
+ let mut out = vec![];
+ let host = cpal::default_host();
+ let Ok(output_devices) = host.output_devices() else {
+ return out;
+ };
+ for d in output_devices {
+ let Ok(mut configs) = d.supported_output_configs() else {
+ continue;
+ };
+ if configs.any(|config| {
+ (config.channels() == 1 || config.channels() == 2)
+ && config.sample_format() == SampleFormat::I16
+ }) {
+ let Ok(name) = d.name() else {
+ continue;
+ };
+ out.push(name);
+ }
+ }
+ out.sort();
+ out
+ }
}
impl Default for Codec2RxAdapter {
+++ /dev/null
-//! Utilities for selecting suitable sound cards.
-
-use cpal::{
- traits::{DeviceTrait, HostTrait},
- SampleFormat,
-};
-
-/// List sound cards supported for audio output.
-///
-/// M17RT will handle any card with 1 or 2 channels and 16-bit output.
-pub fn supported_output_cards() -> Vec<String> {
- let mut out = vec![];
- let host = cpal::default_host();
- let Ok(output_devices) = host.output_devices() else {
- return out;
- };
- for d in output_devices {
- let Ok(mut configs) = d.supported_output_configs() else {
- continue;
- };
- if configs.any(|config| {
- (config.channels() == 1 || config.channels() == 2)
- && config.sample_format() == SampleFormat::I16
- }) {
- let Ok(name) = d.name() else {
- continue;
- };
- out.push(name);
- }
- }
- out.sort();
- out
-}
-
-/// List sound cards supported for audio input.
-///
-///
-/// M17RT will handle any card with 1 or 2 channels and 16-bit output.
-pub fn supported_input_cards() -> Vec<String> {
- let mut out = vec![];
- let host = cpal::default_host();
- let Ok(input_devices) = host.input_devices() else {
- return out;
- };
- for d in input_devices {
- let Ok(mut configs) = d.supported_input_configs() else {
- continue;
- };
- if configs.any(|config| {
- (config.channels() == 1 || config.channels() == 2)
- && config.sample_format() == SampleFormat::I16
- }) {
- let Ok(name) = d.name() else {
- continue;
- };
- out.push(name);
- }
- }
- out.sort();
- out
-}
tx: self.event_tx.clone(),
}
}
+
+ /// List sound cards supported for audio input.
+ ///
+ /// M17RT will handle any card with 1 or 2 channels and 16-bit output.
+ pub fn supported_input_cards() -> Vec<String> {
+ let mut out = vec![];
+ let host = cpal::default_host();
+ let Ok(input_devices) = host.input_devices() else {
+ return out;
+ };
+ for d in input_devices {
+ let Ok(mut configs) = d.supported_input_configs() else {
+ continue;
+ };
+ if configs.any(|config| {
+ (config.channels() == 1 || config.channels() == 2)
+ && config.sample_format() == SampleFormat::I16
+ }) {
+ let Ok(name) = d.name() else {
+ continue;
+ };
+ out.push(name);
+ }
+ }
+ out.sort();
+ out
+ }
}
enum Event {
[dependencies]
m17app = { path = "../../m17app" }
+m17codec2 = { path = "../../m17codec2" }
+
+ascii_table = "4.0.7"
+use ascii_table::{Align, AsciiTable};
use m17app::soundcard::Soundcard;
+use m17codec2::{rx::Codec2RxAdapter, tx::Codec2TxAdapter};
fn main() {
- let inputs = Soundcard::supported_input_cards();
- let outputs = Soundcard::supported_output_cards();
+ // On some platforms enumerating devices will emit junk to the terminal:
+ // https://github.com/RustAudio/cpal/issues/384
+ // To minimise the impact, enumerate first and put our output at the end.
+ let soundmodem_in = Soundcard::supported_input_cards();
+ let soundmodem_out = Soundcard::supported_output_cards();
+ let codec2_in = Codec2TxAdapter::supported_input_cards();
+ let codec2_out = Codec2RxAdapter::supported_output_cards();
- println!("\nSupported Input Soundcards ({}):\n", inputs.len());
- for i in inputs {
- println!("{}", i);
- }
+ println!("\nDetected sound cards compatible with M17 Rust Toolkit:");
- println!("\nSupported Output Soundcards ({}):\n", outputs.len());
- for o in outputs {
- println!("{}", o);
- }
+ generate_table(
+ "SOUNDMODEM",
+ "INPUT",
+ "OUTPUT",
+ &soundmodem_in,
+ &soundmodem_out,
+ );
+ generate_table("CODEC2 AUDIO", "TX", "RX", &codec2_in, &codec2_out);
+}
+
+fn generate_table(
+ heading: &str,
+ input: &str,
+ output: &str,
+ input_cards: &[String],
+ output_cards: &[String],
+) {
+ let mut merged: Vec<&str> = input_cards
+ .iter()
+ .chain(output_cards.iter())
+ .map(|s| s.as_str())
+ .collect();
+ merged.sort();
+ merged.dedup();
+ let yes = "OK";
+ let no = "";
+ let data = merged.into_iter().map(|c| {
+ [
+ c,
+ if input_cards.iter().any(|s| s == c) {
+ yes
+ } else {
+ no
+ },
+ if output_cards.iter().any(|s| s == c) {
+ yes
+ } else {
+ no
+ },
+ ]
+ });
+
+ let mut table = AsciiTable::default();
+ table.column(0).set_header(heading).set_align(Align::Left);
+ table.column(1).set_header(input).set_align(Align::Center);
+ table.column(2).set_header(output).set_align(Align::Center);
+ table.print(data);
}