diff --git a/Cargo.lock b/Cargo.lock index d1f74fd..00b7aa7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -79,7 +79,7 @@ version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a" dependencies = [ - "windows-sys", + "windows-sys 0.52.0", ] [[package]] @@ -89,7 +89,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8" dependencies = [ "anstyle", - "windows-sys", + "windows-sys 0.52.0", ] [[package]] @@ -104,6 +104,20 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +[[package]] +name = "asio-sys" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb111d0b927c4b860b42a8bd869b94f5f973b0395ed5892f1ab2be2dc6831ea8" +dependencies = [ + "bindgen", + "cc", + "num-derive", + "num-traits", + "parse_cfg", + "walkdir", +] + [[package]] name = "autocfg" version = "1.3.0" @@ -123,12 +137,15 @@ dependencies = [ "itertools", "lazy_static", "lazycell", + "log", + "prettyplease", "proc-macro2", "quote", "regex", "rustc-hash", "shlex", "syn", + "which", ] [[package]] @@ -216,7 +233,7 @@ dependencies = [ "lazy_static", "libc", "unicode-width 0.1.13", - "windows-sys", + "windows-sys 0.52.0", ] [[package]] @@ -321,6 +338,16 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "errno" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + [[package]] name = "fast-interleave" version = "0.1.1" @@ -446,6 +473,15 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "home" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" +dependencies = [ + "windows-sys 0.59.0", +] + [[package]] name = "indexmap" version = "2.8.0" @@ -475,6 +511,7 @@ version = "0.1.0" dependencies = [ "alsa", "anyhow", + "asio-sys", "bitflags 2.9.0", "cfg_aliases", "coreaudio-rs", @@ -488,6 +525,7 @@ dependencies = [ "log", "ndarray", "nix 0.29.0", + "num-traits", "oneshot", "pipewire", "rtrb", @@ -601,6 +639,12 @@ dependencies = [ "system-deps", ] +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + [[package]] name = "log" version = "0.4.27" @@ -686,6 +730,17 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "num-integer" version = "0.1.46" @@ -722,6 +777,15 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4ce411919553d3f9fa53a0880544cda985a112117a0444d5ff1e870a893d6ea" +[[package]] +name = "parse_cfg" +version = "4.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "905787a434a2c721408e7c9a252e85f3d93ca0f118a5283022636c0e05a7ea49" +dependencies = [ + "nom", +] + [[package]] name = "pin-project-lite" version = "0.2.16" @@ -783,6 +847,16 @@ dependencies = [ "portable-atomic", ] +[[package]] +name = "prettyplease" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "664ec5419c51e34154eec046ebcba56312d5a2fc3b09a06da188e1ad21afadf6" +dependencies = [ + "proc-macro2", + "syn", +] + [[package]] name = "primal-check" version = "0.3.4" @@ -916,6 +990,28 @@ dependencies = [ "version_check", ] +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags 2.9.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.52.0", +] + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "serde" version = "1.0.219" @@ -1128,6 +1224,16 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "wasm-bindgen" version = "0.2.100" @@ -1195,6 +1301,18 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "which" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +dependencies = [ + "either", + "home", + "once_cell", + "rustix", +] + [[package]] name = "winapi" version = "0.3.9" @@ -1211,6 +1329,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +dependencies = [ + "windows-sys 0.52.0", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" @@ -1327,6 +1454,15 @@ dependencies = [ "windows-targets", ] +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + [[package]] name = "windows-targets" version = "0.52.6" diff --git a/Cargo.toml b/Cargo.toml index 394bc0b..00fe84d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,6 +28,7 @@ cfg_aliases = "0.2.1" [features] pipewire = ["dep:pipewire", "dep:libspa", "dep:libspa-sys", "dep:zerocopy"] +asio = ["dep:asio-sys", "dep:num-traits"] [target.'cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "netbsd"))'.dependencies] alsa = "0.9.1" @@ -52,6 +53,8 @@ windows = { version = "0.61.1", features = [ "Win32_Media_Multimedia", "Win32_UI_Shell_PropertiesSystem" ]} +asio-sys = { version = "0.2.2", optional = true } +num-traits = {version = "0.2.19", optional = true } [[example]] name = "enumerate_alsa" @@ -68,4 +71,9 @@ path = "examples/enumerate_wasapi.rs" [[example]] name = "enumerate_pipewire" path = "examples/enumerate_pipewire.rs" -required-features = ["pipewire"] \ No newline at end of file +required-features = ["pipewire"] + +[[example]] +name = "enumerate_asio" +path = "examples/enumerate_asio.rs" +required-features = ["asio"] diff --git a/build.rs b/build.rs index f843958..75158a8 100644 --- a/build.rs +++ b/build.rs @@ -9,6 +9,7 @@ fn main() { os_coreaudio: { any (target_os = "macos", target_os = "ios") }, os_pipewire: { any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "netbsd") }, os_wasapi: { target_os = "windows" }, - unsupported: { not(any(os_alsa, os_coreaudio, os_wasapi)) } + os_asio: { all(target_os = "windows", feature = "asio") }, + unsupported: { not(any(os_alsa, os_coreaudio, os_wasapi))} } } diff --git a/examples/enumerate_asio.rs b/examples/enumerate_asio.rs new file mode 100644 index 0000000..63d083b --- /dev/null +++ b/examples/enumerate_asio.rs @@ -0,0 +1,14 @@ +mod util; + +#[cfg(os_asio)] +fn main() -> Result<(), Box> { + use crate::util::enumerate::enumerate_devices; + use interflow::backends::asio::AsioDriver; + enumerate_devices(AsioDriver::new()?)?; + Ok(()) +} + +#[cfg(not(os_asio))] +fn main() { + println!("ASIO driver is not available on this platform"); +} diff --git a/src/backends/asio/device.rs b/src/backends/asio/device.rs new file mode 100644 index 0000000..03c2dd4 --- /dev/null +++ b/src/backends/asio/device.rs @@ -0,0 +1,737 @@ +use std::{ + borrow::Cow, + sync::{Arc, Mutex}, +}; + +use asio_sys as asio; +use num_traits::PrimInt; + +use crate::{ + audio_buffer::{AudioMut, AudioRef}, + device::{ + AudioDevice, AudioDuplexDevice, AudioInputDevice, AudioOutputDevice, Channel, DeviceType, + }, + duplex::AudioDuplexCallback, + stream::{ + AudioCallbackContext, AudioInput, AudioInputCallback, AudioOutput, AudioOutputCallback, + StreamConfig, + }, + timestamp::{self, Timestamp}, + SendEverywhereButOnWeb, +}; + +use super::{error::AsioError, stream::AsioStream}; + +/// The ASIO device. +#[derive(Clone)] +pub struct AsioDevice { + driver: Arc, + device_type: DeviceType, + asio_streams: Arc>, +} + +impl AsioDevice { + /// Create a new ASIO device. + pub fn new(driver: Arc) -> Result { + let is_input = driver.channels()?.ins > 0; + let is_output = driver.channels()?.outs > 0; + let device_type = match (is_input, is_output) { + (true, true) => DeviceType::DUPLEX, + (true, false) => DeviceType::INPUT, + (false, true) => DeviceType::OUTPUT, + // todo + (false, false) => return Err(AsioError::BackendError(asio::AsioError::NoDrivers)), + }; + let asio_streams = Arc::new(Mutex::new(asio::AsioStreams { + input: None, + output: None, + })); + Ok(AsioDevice { + driver, + device_type, + asio_streams, + }) + } + + pub fn device_type(&self) -> DeviceType { + self.device_type + } + + /// Create an input stream with the given configuration. + fn create_input_stream(&self, stream_config: StreamConfig) -> Result { + let num_channels = stream_config.channels as usize; + let buffer_size = match stream_config.buffer_size_range { + (Some(min), Some(max)) if min == max => Some(min as i32), + + _ => None, + }; + + self.driver.set_sample_rate(stream_config.samplerate)?; + + let mut streams = self.asio_streams.lock().unwrap(); + + match streams.input { + Some(ref input) => Ok(input.buffer_size as usize), + None => { + let output = streams.output.take(); + self.driver + .prepare_input_stream(output, num_channels, buffer_size) + .map(|new_streams| { + let bs = match new_streams.input { + Some(ref inp) => inp.buffer_size as usize, + None => unreachable!(), + }; + *streams = new_streams; + bs + }) + .map_err(AsioError::BackendError) + } + } + } + + /// Creates an output stream with the given configuration. + fn create_output_stream(&self, stream_config: StreamConfig) -> Result { + let num_channels = stream_config.channels as usize; + let buffer_size = match stream_config.buffer_size_range { + (Some(min), Some(max)) if min == max => Some(min as i32), + _ => None, + }; + + self.driver.set_sample_rate(stream_config.samplerate)?; + + let mut streams = self.asio_streams.lock().unwrap(); + + match streams.output { + Some(ref output) => Ok(output.buffer_size as usize), + None => { + let input = streams.input.take(); + self.driver + .prepare_output_stream(input, num_channels, buffer_size) + .map(|new_streams| { + let bs = match new_streams.output { + Some(ref out) => out.buffer_size as usize, + None => unreachable!(), + }; + *streams = new_streams; + bs + }) + .map_err(AsioError::BackendError) + } + } + } +} + +impl AudioDevice for AsioDevice { + type Error = AsioError; + + fn name(&self) -> Cow { + Cow::Borrowed(self.driver.name()) + } + + fn is_config_supported(&self, config: &StreamConfig) -> bool { + let sample_rate = config.samplerate; + self.driver.can_sample_rate(sample_rate).unwrap_or(false) + && self.driver.channels().is_ok_and(|channels| { + let num_channels = config.channels as i32; + if self.device_type.contains(DeviceType::DUPLEX) { + channels.ins >= num_channels && channels.outs >= num_channels + } else if self.device_type.contains(DeviceType::INPUT) { + channels.ins >= num_channels + } else if self.device_type.contains(DeviceType::OUTPUT) { + channels.outs >= num_channels + } else { + false + } + }) + && self.driver.buffersize_range().is_ok_and(|(min, max)| { + match config.buffer_size_range { + (Some(min_config), Some(max_config)) => { + min_config >= min as usize && max_config <= max as usize + } + _ => false, + } + }) + } + + fn enumerate_configurations(&self) -> Option> { + let sample_rates = [44100.0, 48000.0, 96000.0, 192000.0, 384000.0, 768000.0]; + + let buffer_size_range = self + .driver + .buffersize_range() + .ok() + .map(|(min, max)| (Some(min as usize), Some(max as usize)))?; + + let channels = self.driver.channels().ok().and_then(|channels| { + if self.device_type.contains(DeviceType::DUPLEX) { + Some(channels.ins.max(channels.outs) as u32) + } else if self.device_type.contains(DeviceType::INPUT) { + Some(channels.ins as u32) + } else if self.device_type.contains(DeviceType::OUTPUT) { + Some(channels.outs as u32) + } else { + // Return None if device type is neither input nor output + None + } + })?; + + Some(sample_rates.into_iter().filter_map(move |samplerate| { + self.driver + .can_sample_rate(samplerate) + .ok() + .filter(|&can| can) + .map(|_| StreamConfig { + channels, + samplerate, + buffer_size_range, + exclusive: false, + }) + })) + } +} + +impl AudioInputDevice for AsioDevice { + type StreamHandle = AsioStream; + + fn input_channel_map(&self) -> impl Iterator { + [].into_iter() + } + + fn default_input_config(&self) -> Result { + let channels = self.driver.channels()?.ins as u32; + let samplerate = self.driver.sample_rate()?; + let (min_buffer_size, max_buffer_size) = self.driver.buffersize_range()?; + Ok(StreamConfig { + channels, + samplerate, + buffer_size_range: ( + Some(min_buffer_size as usize), + Some(max_buffer_size as usize), + ), + exclusive: false, + }) + } + + fn create_input_stream( + &self, + stream_config: StreamConfig, + callback: Callback, + ) -> Result, Self::Error> { + let input_data_type = self.driver.input_data_type()?; + + let num_channels = stream_config.channels as usize; + + let buffer_size = self.create_input_stream(stream_config)?; + let num_samples = buffer_size * num_channels; + + let mut buffer = vec![0.0f32; num_samples]; + + let mut streams = self.asio_streams.lock().unwrap(); + let input_stream = streams.input.take().ok_or(AsioError::MultipleStreams)?; + + let (tx, rx) = oneshot::channel::>(); + let mut callback = Some(callback); + + let mut timestamp = Timestamp { + samplerate: stream_config.samplerate, + counter: 0, + }; + + let callback_id = self.driver.add_callback(move |callback_info| unsafe { + if let Ok(sender) = rx.try_recv() { + sender.send(callback.take().unwrap()).unwrap(); + return; + } + + let buffer_index = callback_info.buffer_index as usize; + + timestamp += input_stream.buffer_size as u64; + + let context = AudioCallbackContext { + stream_config, + timestamp, + }; + + let input = create_input( + &input_data_type, + &input_stream, + &mut buffer, + buffer_index, + num_channels, + timestamp, + ); + + if let Some(callback) = &mut callback { + callback.on_input_data(context, input); + } + }); + + self.driver.start()?; + + Ok(AsioStream { + driver: self.driver.clone(), + callback_id, + callback_retrieve: tx, + }) + } +} + +impl AudioOutputDevice for AsioDevice { + type StreamHandle = AsioStream; + + fn output_channel_map(&self) -> impl Iterator { + [].into_iter() + } + + fn default_output_config(&self) -> Result { + let channels = self.driver.channels()?.outs as u32; + let samplerate = self.driver.sample_rate()?; + let (min_buffer_size, max_buffer_size) = self.driver.buffersize_range()?; + Ok(StreamConfig { + channels, + samplerate, + buffer_size_range: ( + Some(min_buffer_size as usize), + Some(max_buffer_size as usize), + ), + exclusive: false, + }) + } + + fn create_output_stream( + &self, + stream_config: StreamConfig, + callback: Callback, + ) -> Result, Self::Error> { + let output_data_type = self.driver.output_data_type()?; + + let num_channels = stream_config.channels as usize; + + let buffer_size = self.create_output_stream(stream_config)?; + let num_samples = buffer_size * num_channels; + + let mut buffer = vec![0.0f32; num_samples]; + + let mut streams = self.asio_streams.lock().unwrap(); + let mut output_stream = streams.output.take().ok_or(AsioError::MultipleStreams)?; + + let (tx, rx) = oneshot::channel::>(); + let mut callback = Some(callback); + + let mut timestamp = Timestamp { + samplerate: stream_config.samplerate, + counter: 0, + }; + + let callback_id = self.driver.add_callback(move |callback_info| unsafe { + if let Ok(sender) = rx.try_recv() { + sender.send(callback.take().unwrap()).unwrap(); + return; + } + + let buffer_index = callback_info.buffer_index as usize; + + timestamp += output_stream.buffer_size as u64; + + let context = AudioCallbackContext { + stream_config, + timestamp, + }; + + let output = create_output( + &output_data_type, + &mut output_stream, + &mut buffer, + buffer_index, + num_channels, + timestamp, + ); + + if let Some(callback) = &mut callback { + callback.on_output_data(context, output); + } + }); + + self.driver.start()?; + + Ok(AsioStream { + driver: self.driver.clone(), + callback_id, + callback_retrieve: tx, + }) + } +} + +impl AudioDuplexDevice for AsioDevice { + type StreamHandle = AsioStream; + + fn default_duplex_config(&self) -> Result { + let channels = self.driver.channels()?.ins as u32; + let samplerate = self.driver.sample_rate()?; + let (min_buffer_size, max_buffer_size) = self.driver.buffersize_range()?; + Ok(StreamConfig { + channels, + samplerate, + buffer_size_range: ( + Some(min_buffer_size as usize), + Some(max_buffer_size as usize), + ), + exclusive: false, + }) + } + + fn create_duplex_stream( + &self, + stream_config: StreamConfig, + callback: Callback, + ) -> Result, Self::Error> { + let output_data_type = self.driver.output_data_type()?; + let input_data_type = self.driver.input_data_type()?; + + let num_channels = stream_config.channels as usize; + + // This creates both input and output streams if available + let buffer_size = self.create_output_stream(stream_config)?; + let num_samples = buffer_size * num_channels; + + let mut input_buffer = vec![0.0f32; num_samples]; + let mut output_buffer = vec![0.0f32; num_samples]; + + let mut streams = self.asio_streams.lock().unwrap(); + let mut output_stream = streams.output.take().ok_or(AsioError::MultipleStreams)?; + let input_stream = streams.input.take().ok_or(AsioError::MultipleStreams)?; + + let (tx, rx) = oneshot::channel::>(); + let mut callback = Some(callback); + + let mut timestamp = Timestamp { + samplerate: stream_config.samplerate, + counter: 0, + }; + + let callback_id = self.driver.add_callback(move |callback_info| unsafe { + if let Ok(sender) = rx.try_recv() { + sender.send(callback.take().unwrap()).unwrap(); + return; + } + + let buffer_index = callback_info.buffer_index as usize; + + timestamp += output_stream.buffer_size as u64; + + let input = create_input( + &input_data_type, + &input_stream, + &mut input_buffer, + buffer_index, + num_channels, + timestamp, + ); + + let output = create_output( + &output_data_type, + &mut output_stream, + &mut output_buffer, + buffer_index, + num_channels, + timestamp, + ); + + let context = AudioCallbackContext { + stream_config, + timestamp, + }; + + if let Some(callback) = &mut callback { + callback.on_audio_data(context, input, output); + } + }); + + self.driver.start()?; + + Ok(AsioStream { + driver: self.driver.clone(), + callback_id, + callback_retrieve: tx, + }) + } +} + +// HELPERS + +/// Create an `AudioOutput` from the ASIO stream and the buffer. +unsafe fn create_output<'a>( + output_data_type: &asio::AsioSampleType, + asio_stream: &mut asio::AsioStream, + buffer: &'a mut [f32], + buffer_index: usize, + num_channels: usize, + timestamp: timestamp::Timestamp, +) -> AudioOutput<'a, f32> { + let audio_output_buffer = match output_data_type { + asio::AsioSampleType::ASIOSTInt16MSB => create_output_buffer( + asio_stream, + buffer, + buffer_index, + num_channels, + to_be, + f32_to_i16, + ), + asio::AsioSampleType::ASIOSTInt16LSB => create_output_buffer( + asio_stream, + buffer, + buffer_index, + num_channels, + to_le, + f32_to_i16, + ), + asio::AsioSampleType::ASIOSTInt24MSB => create_output_buffer( + asio_stream, + buffer, + buffer_index, + num_channels, + to_be, + f32_to_i24, + ), + asio::AsioSampleType::ASIOSTInt24LSB => create_output_buffer( + asio_stream, + buffer, + buffer_index, + num_channels, + to_le, + f32_to_i24, + ), + asio::AsioSampleType::ASIOSTInt32MSB => create_output_buffer( + asio_stream, + buffer, + buffer_index, + num_channels, + to_be, + f32_to_i32, + ), + asio::AsioSampleType::ASIOSTInt32LSB => create_output_buffer( + asio_stream, + buffer, + buffer_index, + num_channels, + to_le, + f32_to_i32, + ), + asio::AsioSampleType::ASIOSTFloat32MSB => create_output_buffer::( + asio_stream, + buffer, + buffer_index, + num_channels, + to_be, + f32::to_bits, + ), + asio::AsioSampleType::ASIOSTFloat32LSB => create_output_buffer::( + asio_stream, + buffer, + buffer_index, + num_channels, + to_le, + f32::to_bits, + ), + unsupported_format => { + unreachable!("returned with unsupported format {:?}", unsupported_format) + } + }; + + AudioOutput { + timestamp, + buffer: audio_output_buffer, + } +} + +/// Create an `AudioInput` from the ASIO stream and the buffer. +unsafe fn create_input<'a>( + input_data_type: &asio::AsioSampleType, + asio_stream: &asio::AsioStream, + buffer: &'a mut [f32], + buffer_index: usize, + num_channels: usize, + timestamp: timestamp::Timestamp, +) -> AudioInput<'a, f32> { + let audio_input_buffer = match input_data_type { + asio::AsioSampleType::ASIOSTInt16MSB => create_input_buffer( + asio_stream, + buffer, + buffer_index, + num_channels, + from_be, + i16_to_f32, + ), + asio::AsioSampleType::ASIOSTInt16LSB => create_input_buffer( + asio_stream, + buffer, + buffer_index, + num_channels, + from_le, + i16_to_f32, + ), + asio::AsioSampleType::ASIOSTInt24MSB => create_input_buffer( + asio_stream, + buffer, + buffer_index, + num_channels, + from_be, + i24_to_f32, + ), + asio::AsioSampleType::ASIOSTInt24LSB => create_input_buffer( + asio_stream, + buffer, + buffer_index, + num_channels, + from_le, + i24_to_f32, + ), + asio::AsioSampleType::ASIOSTInt32MSB => create_input_buffer( + asio_stream, + buffer, + buffer_index, + num_channels, + from_be, + i32_to_f32, + ), + asio::AsioSampleType::ASIOSTInt32LSB => create_input_buffer( + asio_stream, + buffer, + buffer_index, + num_channels, + from_le, + i32_to_f32, + ), + asio::AsioSampleType::ASIOSTFloat32MSB => create_input_buffer::( + asio_stream, + buffer, + buffer_index, + num_channels, + from_be, + f32::from_bits, + ), + asio::AsioSampleType::ASIOSTFloat32LSB => create_input_buffer::( + asio_stream, + buffer, + buffer_index, + num_channels, + from_le, + f32::from_bits, + ), + unsupported_format => { + unreachable!("returned with unsupported format {:?}", unsupported_format) + } + }; + + AudioInput { + timestamp, + buffer: audio_input_buffer, + } +} + +unsafe fn create_input_buffer<'a, SampleType: Copy>( + asio_stream: &asio::AsioStream, + buffer: &'a mut [f32], + buffer_index: usize, + num_channels: usize, + from_endian: impl Fn(SampleType) -> SampleType, + to_f32: impl Fn(SampleType) -> f32, +) -> AudioRef<'a, f32> { + for channel_index in 0..num_channels { + let channel_buffer = + asio_channel_slice::(asio_stream, buffer_index, channel_index); + for (frame, asio_sample) in buffer.chunks_mut(num_channels).zip(channel_buffer) { + frame[channel_index] = to_f32(from_endian(*asio_sample)); + } + } + AudioRef::from_interleaved(buffer, num_channels).unwrap() +} + +unsafe fn create_output_buffer<'a, SampleType: Copy>( + asio_stream: &mut asio::AsioStream, + buffer: &'a mut [f32], + buffer_index: usize, + num_channels: usize, + to_endian: impl Fn(SampleType) -> SampleType, + from_f32: impl Fn(f32) -> SampleType, +) -> AudioMut<'a, f32> { + for channel_index in 0..num_channels { + let channel_buffer = + asio_channel_slice_mut::(asio_stream, buffer_index, channel_index); + for (frame, asio_sample) in buffer.chunks_mut(num_channels).zip(channel_buffer) { + *asio_sample = to_endian(from_f32(frame[channel_index])); + } + } + AudioMut::from_interleaved_mut(buffer, num_channels).unwrap() +} + +unsafe fn asio_channel_slice( + asio_stream: &asio::AsioStream, + buffer_index: usize, + channel_index: usize, +) -> &[T] { + let buffer_size = asio_stream.buffer_size as usize; + let buff_ptr: *const T = + asio_stream.buffer_infos[channel_index].buffers[buffer_index] as *const _; + std::slice::from_raw_parts(buff_ptr, buffer_size) +} + +unsafe fn asio_channel_slice_mut( + asio_stream: &mut asio::AsioStream, + buffer_index: usize, + channel_index: usize, +) -> &mut [T] { + let buffer_size = asio_stream.buffer_size as usize; + let buff_ptr: *mut T = asio_stream.buffer_infos[channel_index].buffers[buffer_index] as *mut _; + std::slice::from_raw_parts_mut(buff_ptr, buffer_size) +} + +/// Helper function to convert from little endianness. +fn from_le(t: T) -> T { + T::from_le(t) +} + +/// Helper function to convert from big endianness. +fn from_be(t: T) -> T { + T::from_be(t) +} + +/// Helper function to convert to little endianness. +fn to_le(t: T) -> T { + t.to_le() +} + +/// Helper function to convert to big endianness. +fn to_be(t: T) -> T { + t.to_be() +} + +/// Helper function to convert from i16 to f32. +fn i16_to_f32(i: i16) -> f32 { + i as f32 / i16::MAX as f32 +} + +/// Helper function to convert from i32 to f32. +fn i32_to_f32(i: i32) -> f32 { + i as f32 / i32::MAX as f32 +} + +/// Helper function to convert from i24 to f32. +fn i24_to_f32(i: i32) -> f32 { + i as f32 / 0x7FFFFF as f32 +} + +/// Helper function to convert from f32 to i16. +fn f32_to_i16(f: f32) -> i16 { + (f * i16::MAX as f32) as i16 +} + +/// Helper function to convert from f32 to i32. +fn f32_to_i32(f: f32) -> i32 { + (f * i32::MAX as f32) as i32 +} + +/// Helper function to convert from f32 to i24. +fn f32_to_i24(f: f32) -> i32 { + (f * 0x7FFFFF as f32) as i32 +} diff --git a/src/backends/asio/driver.rs b/src/backends/asio/driver.rs new file mode 100644 index 0000000..1901356 --- /dev/null +++ b/src/backends/asio/driver.rs @@ -0,0 +1,73 @@ +use std::{borrow::Cow, sync::Arc}; + +use asio_sys as asio; + +use crate::{device::DeviceType, driver::AudioDriver}; +use super::{device::AsioDevice, error::AsioError}; + +/// The ASIO driver. +#[derive(Debug, Clone, Default)] +pub struct AsioDriver { + asio: Arc, +} + +impl AsioDriver { + /// Create a new ASIO driver. + pub fn new() -> Result { + let asio = Arc::new(asio::Asio::new()); + Ok(AsioDriver { asio }) + } +} + +impl AudioDriver for AsioDriver { + type Error = AsioError; + type Device = AsioDevice; + + const DISPLAY_NAME: &'static str = "ASIO"; + + fn version(&self) -> Result, Self::Error> { + Ok(Cow::Borrowed("unknown")) + } + + fn default_device(&self, device_type: DeviceType) -> Result, Self::Error> { + let mut iter = AsioDeviceList::new(self.asio.clone())?; + + let dd = iter.find(|device| device_type.intersects(device.device_type())); + Ok(dd) + } + + fn list_devices(&self) -> Result, Self::Error> { + AsioDeviceList::new(self.asio.clone()) + } +} + +pub struct AsioDeviceList { + asio: Arc, + drivers: std::vec::IntoIter, +} + +impl AsioDeviceList { + pub fn new(asio: Arc) -> Result { + let drivers = asio.driver_names().into_iter(); + Ok(AsioDeviceList { asio, drivers }) + } +} + +impl Iterator for AsioDeviceList { + type Item = AsioDevice; + + fn next(&mut self) -> Option { + loop { + match self.drivers.next() { + Some(name) => match self.asio.load_driver(&name) { + Ok(driver) => { + let driver = Arc::new(driver); + return AsioDevice::new(driver).ok(); + } + Err(_) => continue, + }, + None => return None, + } + } + } +} diff --git a/src/backends/asio/error.rs b/src/backends/asio/error.rs new file mode 100644 index 0000000..26d89ac --- /dev/null +++ b/src/backends/asio/error.rs @@ -0,0 +1,18 @@ +use asio_sys::AsioError as AsioSysError; +use thiserror::Error; + +/// Type of errors from the ASIO backend. +#[derive(Debug, Error)] +#[error("ASIO error: ")] +pub enum AsioError { + /// Error originating from ASIO. + #[error("{0}")] + BackendError(#[from] AsioSysError), + /// Requested WASAPI device configuration is not available + #[error("Configuration not available")] + ConfigurationNotAvailable, + #[error("Device unavailable")] + DeviceUnavailable, + #[error("Multiple streams not supported")] + MultipleStreams, +} diff --git a/src/backends/asio/mod.rs b/src/backends/asio/mod.rs new file mode 100644 index 0000000..cf5ef49 --- /dev/null +++ b/src/backends/asio/mod.rs @@ -0,0 +1,5 @@ +mod device; +mod driver; +pub use driver::AsioDriver; +mod error; +mod stream; diff --git a/src/backends/asio/stream.rs b/src/backends/asio/stream.rs new file mode 100644 index 0000000..6da4835 --- /dev/null +++ b/src/backends/asio/stream.rs @@ -0,0 +1,26 @@ +use std::sync::Arc; + +use asio_sys as asio; + +use crate::stream::AudioStreamHandle; + +use super::error::AsioError; + +pub struct AsioStream { + pub driver: Arc, + pub callback_id: asio::CallbackId, + pub callback_retrieve: oneshot::Sender>, +} + +impl AudioStreamHandle for AsioStream { + type Error = AsioError; + + fn eject(self) -> Result { + let (tx, rx) = oneshot::channel(); + self.callback_retrieve.send(tx).unwrap(); + let callback = rx.recv().unwrap(); + self.driver.stop()?; + self.driver.remove_callback(self.callback_id); + Ok(callback) + } +} diff --git a/src/backends/mod.rs b/src/backends/mod.rs index 009a8c1..693a07f 100644 --- a/src/backends/mod.rs +++ b/src/backends/mod.rs @@ -24,6 +24,9 @@ pub mod wasapi; #[cfg(all(os_pipewire, feature = "pipewire"))] pub mod pipewire; +#[cfg(os_asio)] +pub mod asio; + /// Returns the default driver. /// /// "Default" here means that it is a supported driver that is available on the platform. diff --git a/src/backends/wasapi/stream.rs b/src/backends/wasapi/stream.rs index 0317552..be10dd3 100644 --- a/src/backends/wasapi/stream.rs +++ b/src/backends/wasapi/stream.rs @@ -3,7 +3,8 @@ use crate::audio_buffer::{AudioMut, AudioRef}; use crate::backends::wasapi::util::WasapiMMDevice; use crate::channel_map::Bitset; use crate::stream::{ - AudioCallbackContext, AudioInput, AudioInputCallback, AudioOutput, AudioOutputCallback, AudioStreamHandle, StreamConfig + AudioCallbackContext, AudioInput, AudioInputCallback, AudioOutput, AudioOutputCallback, + AudioStreamHandle, StreamConfig, }; use crate::timestamp::Timestamp; use duplicate::duplicate_item; diff --git a/src/device.rs b/src/device.rs index 8ae8cd2..77b2d6d 100644 --- a/src/device.rs +++ b/src/device.rs @@ -1,7 +1,7 @@ -use bitflags::bitflags; use crate::duplex::AudioDuplexCallback; use crate::stream::{AudioInputCallback, AudioOutputCallback, AudioStreamHandle, StreamConfig}; use crate::SendEverywhereButOnWeb; +use bitflags::bitflags; use std::borrow::Cow; /// Trait for types describing audio devices. Audio devices have zero or more inputs and outputs, @@ -130,7 +130,6 @@ pub trait AudioDuplexDevice: AudioDevice { } } - bitflags! { /// Represents the types/capabilities of an audio device. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] @@ -155,7 +154,6 @@ bitflags! { } } - impl DeviceType { /// Returns true if this device type has the input capability. pub fn is_input(&self) -> bool { diff --git a/src/lib.rs b/src/lib.rs index c26369b..163b950 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,8 +7,8 @@ use std::fmt::Formatter; use crate::audio_buffer::{AudioMut, AudioRef}; use crate::channel_map::ChannelMap32; -use crate::timestamp::Timestamp; use crate::device::DeviceType; +use crate::timestamp::Timestamp; pub mod audio_buffer; pub mod backends; @@ -20,7 +20,6 @@ pub mod prelude; pub mod stream; pub mod timestamp; - /// Audio drivers provide access to the inputs and outputs of devices. /// Several drivers might provide the same accesses, some sharing it with other applications, /// while others work in exclusive mode. @@ -45,7 +44,6 @@ pub trait AudioDriver { fn list_devices(&self) -> Result, Self::Error>; } - /// Configuration for an audio stream. #[derive(Debug, Clone, Copy, PartialEq)] pub struct StreamConfig {