From 64237354070f714ecb27d12350b9680f61aa0ebb Mon Sep 17 00:00:00 2001 From: Nathan Graule Date: Tue, 27 May 2025 13:36:30 +0200 Subject: [PATCH 01/38] wip: remove input/output specific traits and make code compile again --- examples/duplex.rs | 20 +-- examples/input.rs | 4 +- examples/loopback.rs | 4 +- examples/util/meter.rs | 2 +- examples/util/sine.rs | 14 +- src/audio_buffer.rs | 38 +++-- src/backends/alsa/device.rs | 12 +- src/backends/alsa/input.rs | 4 +- src/backends/alsa/stream.rs | 2 +- src/backends/coreaudio.rs | 203 +++++++++++++---------- src/backends/mod.rs | 10 +- src/backends/pipewire/device.rs | 12 +- src/backends/pipewire/stream.rs | 14 +- src/backends/wasapi/device.rs | 12 +- src/backends/wasapi/stream.rs | 24 +-- src/duplex.rs | 281 ++++++++++++++++++-------------- src/lib.rs | 200 ++++++++++------------- src/prelude.rs | 4 +- 18 files changed, 453 insertions(+), 407 deletions(-) diff --git a/examples/duplex.rs b/examples/duplex.rs index 4e007c1..83b1df6 100644 --- a/examples/duplex.rs +++ b/examples/duplex.rs @@ -1,20 +1,17 @@ use crate::util::sine::SineWave; use anyhow::Result; -use interflow::duplex::AudioDuplexCallback; use interflow::prelude::*; mod util; +//noinspection RsUnwrap fn main() -> Result<()> { let input = default_input_device(); let output = default_output_device(); - let mut input_config = input.default_input_config().unwrap(); - input_config.buffer_size_range = (Some(128), Some(512)); - let mut output_config = output.default_output_config().unwrap(); - output_config.buffer_size_range = (Some(128), Some(512)); - let duplex_config = DuplexStreamConfig::new(input_config, output_config); - let stream = - duplex::create_duplex_stream(input, output, RingMod::new(), duplex_config).unwrap(); + let mut config = output.default_config().unwrap(); + config.buffer_size_range = (Some(128), Some(512)); + let duplex_config = DuplexStreamConfig::new(config); + let stream = create_duplex_stream(input, output, RingMod::new(), duplex_config).unwrap(); println!("Press Enter to stop"); std::io::stdin().read_line(&mut String::new())?; stream.eject().unwrap(); @@ -33,15 +30,16 @@ impl RingMod { } } -impl AudioDuplexCallback for RingMod { - fn on_audio_data( +impl AudioCallback for RingMod { + fn prepare(&mut self, context: AudioCallbackContext) {} + fn process_audio( &mut self, context: AudioCallbackContext, input: AudioInput, mut output: AudioOutput, ) { let sr = context.stream_config.samplerate as f32; - for i in 0..output.buffer.num_samples() { + for i in 0..output.buffer.num_frames() { let inp = input.buffer.get_frame(i)[0]; let c = self.carrier.next_sample(sr); output.buffer.set_mono(i, inp * c); diff --git a/examples/input.rs b/examples/input.rs index d1a72b7..792a0db 100644 --- a/examples/input.rs +++ b/examples/input.rs @@ -30,8 +30,8 @@ impl RmsMeter { } } -impl AudioInputCallback for RmsMeter { - fn on_input_data(&mut self, context: AudioCallbackContext, input: AudioInput) { +impl AudioCallback for RmsMeter { + fn process_audio(&mut self, context: AudioCallbackContext, input: AudioInput) { let meter = self .meter .get_or_insert_with(|| PeakMeter::new(context.stream_config.samplerate as f32, 15.0)); diff --git a/examples/loopback.rs b/examples/loopback.rs index db72372..69ae077 100644 --- a/examples/loopback.rs +++ b/examples/loopback.rs @@ -14,8 +14,8 @@ fn main() -> Result<()> { input_config.buffer_size_range = (Some(128), Some(512)); let mut output_config = output.default_output_config().unwrap(); output_config.buffer_size_range = (Some(128), Some(512)); - input_config.channels = 0b01; - output_config.channels = 0b11; + input_config.output_channels = 0b01; + output_config.output_channels = 0b11; let value = Arc::new(AtomicF32::new(0.)); let config = DuplexStreamConfig::new(input_config, output_config); let stream = diff --git a/examples/util/meter.rs b/examples/util/meter.rs index 36acee6..d8f505e 100644 --- a/examples/util/meter.rs +++ b/examples/util/meter.rs @@ -42,7 +42,7 @@ impl PeakMeter { } pub fn process_buffer(&mut self, buffer: AudioRef) -> f32 { - let buffer_duration = buffer.num_samples() as f32 * self.dt; + let buffer_duration = buffer.num_frames() as f32 * self.dt; let peak_lin = buffer .channels() .flat_map(|ch| ch.iter().copied().max_by(f32::total_cmp)) diff --git a/examples/util/sine.rs b/examples/util/sine.rs index f780329..73b8121 100644 --- a/examples/util/sine.rs +++ b/examples/util/sine.rs @@ -1,4 +1,4 @@ -use interflow::{AudioCallbackContext, AudioOutput, AudioOutputCallback}; +use interflow::{AudioCallback, AudioCallbackContext, AudioInput, AudioOutput}; use std::f32::consts::TAU; pub struct SineWave { @@ -6,14 +6,20 @@ pub struct SineWave { pub phase: f32, } -impl AudioOutputCallback for SineWave { - fn on_output_data(&mut self, context: AudioCallbackContext, mut output: AudioOutput) { +impl AudioCallback for SineWave { + fn prepare(&mut self, _context: AudioCallbackContext) {} + fn process_audio( + &mut self, + context: AudioCallbackContext, + _input: AudioInput, + mut output: AudioOutput, + ) { eprintln!( "Callback called, timestamp: {:2.3} s", context.timestamp.as_seconds() ); let sr = context.timestamp.samplerate as f32; - for i in 0..output.buffer.num_samples() { + for i in 0..output.buffer.num_frames() { output.buffer.set_mono(i, self.next_sample(sr)); } // Reduce amplitude to not blow up speakers and ears diff --git a/src/audio_buffer.rs b/src/audio_buffer.rs index 6872350..6898af6 100644 --- a/src/audio_buffer.rs +++ b/src/audio_buffer.rs @@ -112,7 +112,7 @@ where impl AudioBufferBase { /// Number of samples present in this buffer. - pub fn num_samples(&self) -> usize { + pub fn num_frames(&self) -> usize { self.storage.ncols() } @@ -141,7 +141,7 @@ impl AudioBufferBase { let end = match range.end_bound() { Bound::Included(i) => *i - 1, Bound::Excluded(i) => *i, - Bound::Unbounded => self.num_samples(), + Bound::Unbounded => self.num_frames(), }; let storage = self.storage.slice(s![.., start..end]); AudioRef { storage } @@ -151,10 +151,10 @@ impl AudioBufferBase { pub fn chunks(&self, size: usize) -> impl Iterator> { let mut i = 0; std::iter::from_fn(move || { - if i >= self.num_samples() { + if i >= self.num_frames() { return None; } - let range = i..(i + size).min(self.num_samples()); + let range = i..(i + size).min(self.num_frames()); i += size; Some(self.slice(range)) }) @@ -165,7 +165,7 @@ impl AudioBufferBase { pub fn chunks_exact(&self, size: usize) -> impl Iterator> { let mut i = 0; std::iter::from_fn(move || { - if i + size >= self.num_samples() { + if i + size >= self.num_frames() { return None; } let range = i..i + size; @@ -182,10 +182,10 @@ impl AudioBufferBase { pub fn windows(&self, size: usize) -> impl Iterator> { let mut i = 0; std::iter::from_fn(move || { - if i + size >= self.num_samples() { + if i + size >= self.num_frames() { return None; } - let range = i..(i + size).min(self.num_samples()); + let range = i..(i + size).min(self.num_frames()); i += 1; Some(self.slice(range)) }) @@ -264,7 +264,7 @@ impl AudioBufferBase { let end = match range.end_bound() { Bound::Included(i) => *i - 1, Bound::Excluded(i) => *i, - Bound::Unbounded => self.num_samples(), + Bound::Unbounded => self.num_frames(), }; let storage = self.storage.slice_mut(s![.., start..end]); AudioMut { storage } @@ -338,6 +338,22 @@ impl AudioBufferBase { } } +impl AudioRef<'static, T> { + pub fn empty() -> Self { + Self { + storage: ArrayView2::from_shape((0, 0), &[]).unwrap(), + } + } +} + +impl AudioMut<'static, T> { + pub fn empty() -> Self { + Self { + storage: ArrayViewMut2::from_shape((0, 0), &mut []).unwrap(), + } + } +} + impl<'a, T: 'a> AudioRef<'a, T> where ViewRepr<&'a T>: Sized, @@ -623,7 +639,7 @@ mod tests { fn test_buffer_creation() { let buf = create_test_buffer(); assert_eq!(buf.num_channels(), 2); - assert_eq!(buf.num_samples(), 4); + assert_eq!(buf.num_frames(), 4); // Verify sample values assert_eq!(buf.get_channel(0).to_vec(), vec![0.0, 1.0, 2.0, 3.0]); @@ -636,7 +652,7 @@ mod tests { // Test immutable slice let slice = buf.slice(1..3); - assert_eq!(slice.num_samples(), 2); + assert_eq!(slice.num_frames(), 2); assert_eq!(slice.get_channel(0).to_vec(), vec![1.0, 2.0]); // Test mutable slice @@ -680,7 +696,7 @@ mod tests { let buf = AudioRef::from_interleaved(&data, 2).unwrap(); assert_eq!(buf.num_channels(), 2); - assert_eq!(buf.num_samples(), 2); + assert_eq!(buf.num_frames(), 2); assert_eq!(buf.get_channel(0).to_vec(), vec![1.0, 3.0]); assert_eq!(buf.get_channel(1).to_vec(), vec![2.0, 4.0]); diff --git a/src/backends/alsa/device.rs b/src/backends/alsa/device.rs index 7ee4d4a..cc14556 100644 --- a/src/backends/alsa/device.rs +++ b/src/backends/alsa/device.rs @@ -1,8 +1,8 @@ use crate::backends::alsa::stream::AlsaStream; use crate::backends::alsa::AlsaError; use crate::{ - AudioDevice, AudioInputCallback, AudioInputDevice, AudioOutputCallback, AudioOutputDevice, - Channel, DeviceType, StreamConfig, + AudioCallback, AudioDevice, AudioInputDevice, AudioOutputCallback, AudioOutputDevice, Channel, + DeviceType, StreamConfig, }; use alsa::{pcm, PCM}; use std::borrow::Cow; @@ -60,13 +60,13 @@ impl AudioDevice for AlsaDevice { } impl AudioInputDevice for AlsaDevice { - type StreamHandle = AlsaStream; + type StreamHandle = AlsaStream; fn default_input_config(&self) -> Result { self.default_config() } - fn create_input_stream( + fn create_input_stream( &self, stream_config: StreamConfig, callback: Callback, @@ -114,7 +114,7 @@ impl AlsaDevice { fn get_hwp(&self, config: &StreamConfig) -> Result, alsa::Error> { let hwp = pcm::HwParams::any(&self.pcm)?; - hwp.set_channels(config.channels as _)?; + hwp.set_channels(config.output_channels as _)?; hwp.set_rate(config.samplerate as _, alsa::ValueOr::Nearest)?; if let Some(min) = config.buffer_size_range.0 { hwp.set_buffer_size_min(min as _)?; @@ -152,7 +152,7 @@ impl AlsaDevice { let channels = (1 << channel_count) - 1; Ok(StreamConfig { samplerate: samplerate as _, - channels, + output_channels: channels, buffer_size_range: (None, None), exclusive: false, }) diff --git a/src/backends/alsa/input.rs b/src/backends/alsa/input.rs index 19ec8b9..6d90f22 100644 --- a/src/backends/alsa/input.rs +++ b/src/backends/alsa/input.rs @@ -2,9 +2,9 @@ use crate::audio_buffer::AudioRef; use crate::backends::alsa::stream::AlsaStream; use crate::backends::alsa::AlsaError; use crate::prelude::alsa::device::AlsaDevice; -use crate::{AudioCallbackContext, AudioInput, AudioInputCallback, StreamConfig}; +use crate::{AudioCallback, AudioCallbackContext, AudioInput, StreamConfig}; -impl AlsaStream { +impl AlsaStream { pub(super) fn new_input( name: String, stream_config: StreamConfig, diff --git a/src/backends/alsa/stream.rs b/src/backends/alsa/stream.rs index ac649c1..fedec01 100644 --- a/src/backends/alsa/stream.rs +++ b/src/backends/alsa/stream.rs @@ -69,7 +69,7 @@ impl AlsaStream { log::info!("Sample rate : {samplerate}"); let stream_config = StreamConfig { samplerate, - channels: ChannelMap32::default() + output_channels: ChannelMap32::default() .with_indices(std::iter::repeat(1).take(num_channels)), buffer_size_range: (Some(period_size), Some(period_size)), exclusive: false, diff --git a/src/backends/coreaudio.rs b/src/backends/coreaudio.rs index 93dd7a6..cbacd27 100644 --- a/src/backends/coreaudio.rs +++ b/src/backends/coreaudio.rs @@ -62,12 +62,12 @@ use thiserror::Error; use crate::audio_buffer::{AudioBuffer, Sample}; use crate::channel_map::Bitset; -use crate::prelude::ChannelMap32; +use crate::prelude::{AudioMut, AudioRef, ChannelMap32}; use crate::timestamp::Timestamp; use crate::{ - AudioCallbackContext, AudioDevice, AudioDriver, AudioInput, AudioInputCallback, - AudioInputDevice, AudioOutput, AudioOutputCallback, AudioOutputDevice, AudioStreamHandle, - Channel, DeviceType, SendEverywhereButOnWeb, StreamConfig, + AudioCallback, AudioCallbackContext, AudioDevice, AudioDriver, AudioInput, AudioOutput, + AudioStreamHandle, Channel, DeviceType, ResolvedStreamConfig, SendEverywhereButOnWeb, + StreamConfig, }; /// Type of errors from the CoreAudio backend @@ -167,13 +167,14 @@ impl CoreAudioDevice { } impl AudioDevice for CoreAudioDevice { + type StreamHandle = CoreAudioStream; type Error = CoreAudioError; fn name(&self) -> Cow { match get_device_name(self.device_id) { Ok(std) => Cow::Owned(std), Err(err) => { - eprintln!("Cannot get audio device name: {err}"); + log::error!("Cannot get audio device name: {err}"); Cow::Borrowed("") } } @@ -184,14 +185,14 @@ impl AudioDevice for CoreAudioDevice { } fn channel_map(&self) -> impl IntoIterator { - let is_input = matches!(self.device_type, DeviceType::INPUT); - let channels = match audio_unit_from_device_id(self.device_id, is_input) { + let channels = match audio_unit_from_device_id(self.device_id, self.device_type.is_input()) + { Err(err) => { eprintln!("CoreAudio error getting audio unit: {err}"); 0 } Ok(audio_unit) => { - let stream_format = if is_input { + let stream_format = if self.device_type.is_input() { audio_unit.input_stream_format().unwrap() } else { audio_unit.output_stream_format().unwrap() @@ -205,6 +206,31 @@ impl AudioDevice for CoreAudioDevice { }) } + fn default_config(&self) -> Result { + let audio_unit = audio_unit_from_device_id(self.device_id, self.device_type.is_input())?; + let format = if self.device_type.is_input() { + audio_unit.input_stream_format()? + } else { + audio_unit.output_stream_format()? + }; + + Ok(StreamConfig { + samplerate: audio_unit.sample_rate()?, + input_channels: if self.device_type.is_input() { + format.channels as _ + } else { + 0 + }, + output_channels: if self.device_type.is_output() { + format.channels as _ + } else { + 0 + }, + buffer_size_range: (None, None), + exclusive: false, + }) + } + fn is_config_supported(&self, _config: &StreamConfig) -> bool { true } @@ -231,7 +257,7 @@ impl AudioDevice for CoreAudioDevice { let supported_list = get_supported_physical_stream_formats(self.device_id) .inspect_err(|err| eprintln!("Error getting stream formats: {err}")) .ok()?; - let buffer_size_range = self.buffer_size_range().unwrap_or((None, None)); + let device_type = self.device_type; Some(supported_list.into_iter().flat_map(move |asbd| { let samplerate_range = asbd.mSampleRateRange.mMinimum..asbd.mSampleRateRange.mMaximum; TYPICAL_SAMPLERATES @@ -244,46 +270,29 @@ impl AudioDevice for CoreAudioDevice { .map(move |exclusive| (sr, exclusive)) }) .map(move |(samplerate, exclusive)| { - let channels = 1 << (asbd.mFormat.mChannelsPerFrame - 1); + let channels = asbd.mFormat.mChannelsPerFrame; + let input_channels = if device_type.is_input() { + channels as _ + } else { + 0 + }; + let output_channels = if device_type.is_output() { + channels as _ + } else { + 0 + }; StreamConfig { samplerate, - channels, + input_channels, + output_channels, buffer_size_range, exclusive, } }) })) } -} -fn input_stream_format(sample_rate: f64, channels: ChannelMap32) -> StreamFormat { - StreamFormat { - sample_rate, - sample_format: SampleFormat::I16, - flags: LinearPcmFlags::IS_SIGNED_INTEGER, - channels: channels.count() as _, - } -} - -impl AudioInputDevice for CoreAudioDevice { - type StreamHandle = CoreAudioStream; - - fn default_input_config(&self) -> Result { - let audio_unit = audio_unit_from_device_id(self.device_id, true)?; - let samplerate = audio_unit.get_property::( - kAudioUnitProperty_SampleRate, - Scope::Input, - Element::Input, - )?; - Ok(StreamConfig { - channels: 0b11, - samplerate, - buffer_size_range: self.buffer_size_range()?, - exclusive: false, - }) - } - - fn create_input_stream( + fn create_stream( &self, stream_config: StreamConfig, callback: Callback, @@ -291,40 +300,25 @@ impl AudioInputDevice for CoreAudioDevice { let mut device = *self; device.device_type = DeviceType::INPUT; device.set_buffer_size_from_config(&stream_config)?; - CoreAudioStream::new_input(self.device_id, stream_config, callback) + CoreAudioStream::new(self.device_id, self.device_type, stream_config, callback) } } -fn output_stream_format(sample_rate: f64, channels: ChannelMap32) -> StreamFormat { +fn input_stream_format(sample_rate: f64, channel_count: usize) -> StreamFormat { StreamFormat { sample_rate, - sample_format: SampleFormat::F32, - flags: LinearPcmFlags::IS_NON_INTERLEAVED | LinearPcmFlags::IS_FLOAT, - channels, + sample_format: SampleFormat::I16, + flags: LinearPcmFlags::IS_SIGNED_INTEGER, + channels: channel_count as _, } } -impl AudioOutputDevice for CoreAudioDevice { - type StreamHandle = CoreAudioStream; - - fn default_output_config(&self) -> Result { - let audio_unit = audio_unit_from_device_id(self.device_id, false)?; - let samplerate = audio_unit.sample_rate()?; - Ok(StreamConfig { - samplerate, - buffer_size_range: self.buffer_size_range()?, - channels: 0b11, - exclusive: false, - }) - } - - fn create_output_stream( - &self, - stream_config: StreamConfig, - callback: Callback, - ) -> Result, Self::Error> { - self.set_buffer_size_from_config(&stream_config)?; - CoreAudioStream::new_output(self.device_id, stream_config, callback) +fn output_stream_format(sample_rate: f64, channel_count: usize) -> StreamFormat { + StreamFormat { + sample_rate, + sample_format: SampleFormat::F32, + flags: LinearPcmFlags::IS_NON_INTERLEAVED | LinearPcmFlags::IS_FLOAT, + channels: channel_count as _, } } @@ -339,36 +333,54 @@ impl AudioStreamHandle for CoreAudioStream { fn eject(mut self) -> Result { let (tx, rx) = oneshot::channel(); - self.callback_retrieve.send(tx).unwrap(); - let callback = rx.recv().unwrap(); + self.callback_retrieve + .send(tx) + .expect("Callback receiver cannot have been dropped yet"); + let callback = rx.recv().expect("Oneshot receiver must be used"); self.audio_unit.free_input_callback(); self.audio_unit.free_render_callback(); Ok(callback) } } -impl CoreAudioStream { +impl CoreAudioStream { + fn new( + device_id: AudioDeviceID, + device_type: DeviceType, + stream_config: StreamConfig, + callback: Callback, + ) -> Result { + if device_type.is_input() && !device_type.is_output() { + Self::new_input(device_id, stream_config, callback) + } else { + Self::new_output(device_id, stream_config, callback) + } + } + fn new_input( device_id: AudioDeviceID, stream_config: StreamConfig, callback: Callback, ) -> Result { let mut audio_unit = audio_unit_from_device_id(device_id, true)?; - let asbd = input_stream_format(stream_config.samplerate, stream_config.channels).to_asbd(); + let asbd = + input_stream_format(stream_config.samplerate, stream_config.input_channels).to_asbd(); audio_unit.set_property( kAudioUnitProperty_StreamFormat, Scope::Output, Element::Input, Some(&asbd), )?; - let max_frames: u32 = audio_unit.get_property( - kAudioUnitProperty_MaximumFramesPerSlice, - Scope::Global, - Element::Output, - )?; - let mut buffer = AudioBuffer::zeroed(stream_config.channels.count(), max_frames as usize); + let stream_config = ResolvedStreamConfig { + samplerate: asbd.mSampleRate, + input_channels: asbd.mChannelsPerFrame as _, + output_channels: 0, + max_frame_count: asbd.mFramesPerPacket as _, + }; + let mut buffer = + AudioBuffer::zeroed(asbd.mChannelsPerFrame as _, stream_config.samplerate as _); - // Set up the callback retrieval process, without needing to make the callback `Sync` + // Set up the callback retrieval process without needing to make the callback `Sync` let (tx, rx) = oneshot::channel::>(); let mut callback = Some(callback); audio_unit.set_input_callback(move |args: Args>| { @@ -390,13 +402,18 @@ impl CoreAudioStream { buffer: buffer.as_ref(), timestamp, }; + let dummy_output = AudioOutput { + buffer: AudioMut::empty(), + timestamp: Timestamp::new(asbd.mSampleRate), + }; if let Some(callback) = &mut callback { - callback.on_input_data( + callback.process_audio( AudioCallbackContext { stream_config, timestamp, }, input, + dummy_output, ); } Ok(()) @@ -407,30 +424,30 @@ impl CoreAudioStream { callback_retrieve: tx, }) } -} -impl CoreAudioStream { fn new_output( device_id: AudioDeviceID, stream_config: StreamConfig, callback: Callback, ) -> Result { let mut audio_unit = audio_unit_from_device_id(device_id, false)?; - let asbd = output_stream_format(stream_config.samplerate, stream_config.channels).to_asbd(); + let asbd = + output_stream_format(stream_config.samplerate, stream_config.output_channels).to_asbd(); audio_unit.set_property( kAudioUnitProperty_StreamFormat, Scope::Input, Element::Output, Some(&asbd), )?; - let max_frames: u32 = audio_unit.get_property( - kAudioUnitProperty_MaximumFramesPerSlice, - Scope::Global, - Element::Output, - )?; - let mut buffer = AudioBuffer::zeroed(stream_config.channels.count(), max_frames as usize); - - // Set up the callback retrieval process, without needing to make the callback `Sync` + let stream_config = ResolvedStreamConfig { + samplerate: asbd.mSampleRate, + input_channels: 0, + output_channels: asbd.mChannelsPerFrame as _, + max_frame_count: asbd.mFramesPerPacket as _, + }; + let mut buffer = + AudioBuffer::zeroed(stream_config.output_channels, stream_config.samplerate as _); + // Set up the callback retrieval process without needing to make the callback `Sync` let (tx, rx) = oneshot::channel::>(); let mut callback = Some(callback); audio_unit.set_render_callback(move |mut args: Args>| { @@ -441,16 +458,22 @@ impl CoreAudioStream { let mut buffer = buffer.slice_mut(..args.num_frames); let timestamp = Timestamp::from_count(stream_config.samplerate, args.time_stamp.mSampleTime as _); + let dummy_input = AudioInput { + buffer: AudioRef::empty(), + timestamp: Timestamp::new(asbd.mSampleRate), + }; let output = AudioOutput { buffer: buffer.as_mut(), timestamp, }; + if let Some(callback) = &mut callback { - callback.on_output_data( + callback.process_audio( AudioCallbackContext { stream_config, timestamp, }, + dummy_input, output, ); for (output, inner) in args.data.channels_mut().zip(buffer.channels()) { diff --git a/src/backends/mod.rs b/src/backends/mod.rs index 6e8289d..78ab3cf 100644 --- a/src/backends/mod.rs +++ b/src/backends/mod.rs @@ -5,7 +5,7 @@ //! Each backend is provided in its own submodule. Types should be public so that the user isn't //! limited to going through the main API if they want to choose a specific backend. -use crate::{AudioDriver, AudioInputDevice, AudioOutputDevice, DeviceType}; +use crate::{AudioDevice, AudioDriver, DeviceType}; #[cfg(unsupported)] compile_error!("Unsupported platform (supports ALSA, CoreAudio, and WASAPI)"); @@ -55,7 +55,7 @@ pub fn default_driver() -> impl AudioDriver { /// The default device is usually the one the user has selected in its system settings. pub fn default_input_device_from(driver: &Driver) -> Driver::Device where - Driver::Device: AudioInputDevice, + Driver::Device: AudioDevice, { driver .default_device(DeviceType::PHYSICAL | DeviceType::INPUT) @@ -70,7 +70,7 @@ where /// driver from [`default_driver`]. #[cfg(any(feature = "pipewire", os_alsa, os_coreaudio, os_wasapi))] #[allow(clippy::needless_return)] -pub fn default_input_device() -> impl AudioInputDevice { +pub fn default_input_device() -> impl AudioDevice { #[cfg(all(os_pipewire, feature = "pipewire"))] return default_input_device_from(&pipewire::driver::PipewireDriver::new().unwrap()); #[cfg(all(not(all(os_pipewire, feature = "pipewire")), os_alsa))] @@ -86,7 +86,7 @@ pub fn default_input_device() -> impl AudioInputDevice { /// The default device is usually the one the user has selected in its system settings. pub fn default_output_device_from(driver: &Driver) -> Driver::Device where - Driver::Device: AudioOutputDevice, + Driver::Device: AudioDevice, { driver .default_device(DeviceType::PHYSICAL | DeviceType::OUTPUT) @@ -101,7 +101,7 @@ where /// driver from [`default_driver`]. #[cfg(any(os_alsa, os_coreaudio, os_wasapi, feature = "pipewire"))] #[allow(clippy::needless_return)] -pub fn default_output_device() -> impl AudioOutputDevice { +pub fn default_output_device() -> impl AudioDevice { #[cfg(all(os_pipewire, feature = "pipewire"))] return default_output_device_from(&pipewire::driver::PipewireDriver::new().unwrap()); #[cfg(all(not(all(os_pipewire, feature = "pipewire")), os_alsa))] diff --git a/src/backends/pipewire/device.rs b/src/backends/pipewire/device.rs index b8646fb..4e2a659 100644 --- a/src/backends/pipewire/device.rs +++ b/src/backends/pipewire/device.rs @@ -1,8 +1,8 @@ use super::stream::StreamHandle; use crate::backends::pipewire::error::PipewireError; use crate::{ - AudioDevice, AudioInputCallback, AudioInputDevice, AudioOutputCallback, AudioOutputDevice, - Channel, DeviceType, SendEverywhereButOnWeb, StreamConfig, + AudioCallback, AudioDevice, AudioInputDevice, AudioOutputCallback, AudioOutputDevice, Channel, + DeviceType, SendEverywhereButOnWeb, StreamConfig, }; use pipewire::context::Context; use pipewire::main_loop::MainLoop; @@ -70,18 +70,18 @@ impl AudioDevice for PipewireDevice { } impl AudioInputDevice for PipewireDevice { - type StreamHandle = StreamHandle; + type StreamHandle = StreamHandle; fn default_input_config(&self) -> Result { Ok(StreamConfig { samplerate: 48000.0, - channels: 0b11, + output_channels: 0b11, exclusive: false, buffer_size_range: (None, None), }) } - fn create_input_stream( + fn create_input_stream( &self, stream_config: StreamConfig, callback: Callback, @@ -102,7 +102,7 @@ impl AudioOutputDevice for PipewireDevice { fn default_output_config(&self) -> Result { Ok(StreamConfig { samplerate: 48000.0, - channels: 0b11, + output_channels: 0b11, exclusive: false, buffer_size_range: (None, None), }) diff --git a/src/backends/pipewire/stream.rs b/src/backends/pipewire/stream.rs index 4fac9f2..c39f2fb 100644 --- a/src/backends/pipewire/stream.rs +++ b/src/backends/pipewire/stream.rs @@ -3,7 +3,7 @@ use crate::backends::pipewire::error::PipewireError; use crate::channel_map::Bitset; use crate::timestamp::Timestamp; use crate::{ - AudioCallbackContext, AudioInput, AudioInputCallback, AudioOutput, AudioOutputCallback, + AudioCallback, AudioCallbackContext, AudioInput, AudioOutput, AudioOutputCallback, AudioStreamHandle, StreamConfig, }; use libspa::buffer::Data; @@ -86,7 +86,7 @@ impl StreamInner { stream_config: self.config, timestamp: self.timestamp, }; - let num_frames = buffer.num_samples(); + let num_frames = buffer.num_frames(); let output = AudioOutput { buffer, timestamp: self.timestamp, @@ -100,7 +100,7 @@ impl StreamInner { } } -impl StreamInner { +impl StreamInner { fn process_input(&mut self, channels: usize, frames: usize) -> usize { let buffer = AudioRef::from_interleaved(&self.scratch_buffer[..channels * frames], channels) @@ -110,12 +110,12 @@ impl StreamInner { stream_config: self.config, timestamp: self.timestamp, }; - let num_frames = buffer.num_samples(); + let num_frames = buffer.num_frames(); let input = AudioInput { buffer, timestamp: self.timestamp, }; - callback.on_input_data(context, input); + callback.process_audio(context, input); self.timestamp += num_frames as u64; num_frames } else { @@ -161,7 +161,7 @@ impl StreamHandle { let context = Context::new(&main_loop)?; let core = context.connect(None)?; - let channels = config.channels.count(); + let channels = config.output_channels.count(); let channels_str = channels.to_string(); let buffer_size = stream_buffer_size(config.buffer_size_range); @@ -248,7 +248,7 @@ impl StreamHandle { } } -impl StreamHandle { +impl StreamHandle { /// Create an input Pipewire stream pub fn new_input( device_object_serial: Option, diff --git a/src/backends/wasapi/device.rs b/src/backends/wasapi/device.rs index ebc8d68..f3e3843 100644 --- a/src/backends/wasapi/device.rs +++ b/src/backends/wasapi/device.rs @@ -3,8 +3,8 @@ use crate::backends::wasapi::stream::WasapiStream; use crate::channel_map::Bitset; use crate::prelude::wasapi::util::WasapiMMDevice; use crate::{ - AudioDevice, AudioInputCallback, AudioInputDevice, AudioOutputCallback, AudioOutputDevice, - Channel, DeviceType, StreamConfig, + AudioCallback, AudioDevice, AudioInputDevice, AudioOutputCallback, AudioOutputDevice, Channel, + DeviceType, StreamConfig, }; use std::borrow::Cow; use windows::Win32::Media::Audio; @@ -57,7 +57,7 @@ impl AudioDevice for WasapiDevice { } impl AudioInputDevice for WasapiDevice { - type StreamHandle = WasapiStream; + type StreamHandle = WasapiStream; fn default_input_config(&self) -> Result { let audio_client = self.device.activate::()?; @@ -66,14 +66,14 @@ impl AudioInputDevice for WasapiDevice { .map(|i| i as usize) .ok(); Ok(StreamConfig { - channels: 0u32.with_indices(0..format.nChannels as _), + output_channels: 0u32.with_indices(0..format.nChannels as _), exclusive: false, samplerate: format.nSamplesPerSec as _, buffer_size_range: (frame_size, frame_size), }) } - fn create_input_stream( + fn create_input_stream( &self, stream_config: StreamConfig, callback: Callback, @@ -96,7 +96,7 @@ impl AudioOutputDevice for WasapiDevice { .map(|i| i as usize) .ok(); Ok(StreamConfig { - channels: 0u32.with_indices(0..format.nChannels as _), + output_channels: 0u32.with_indices(0..format.nChannels as _), exclusive: false, samplerate: format.nSamplesPerSec as _, buffer_size_range: (frame_size, frame_size), diff --git a/src/backends/wasapi/stream.rs b/src/backends/wasapi/stream.rs index 815851e..bb40df1 100644 --- a/src/backends/wasapi/stream.rs +++ b/src/backends/wasapi/stream.rs @@ -4,7 +4,7 @@ use crate::backends::wasapi::util::WasapiMMDevice; use crate::channel_map::Bitset; use crate::prelude::{AudioRef, Timestamp}; use crate::{ - AudioCallbackContext, AudioInput, AudioInputCallback, AudioOutput, AudioOutputCallback, + AudioCallback, AudioCallbackContext, AudioInput, AudioOutput, AudioOutputCallback, AudioStreamHandle, StreamConfig, }; use duplicate::duplicate_item; @@ -173,7 +173,8 @@ impl AudioThread { format.Format = actual_format.read_unaligned(); CoTaskMemFree(actual_format.cast()); let sample_rate = format.Format.nSamplesPerSec; - stream_config.channels = 0u32.with_indices(0..format.Format.nChannels as _); + stream_config.output_channels = + 0u32.with_indices(0..format.Format.nChannels as _); stream_config.samplerate = sample_rate as _; } format @@ -246,7 +247,7 @@ impl AudioThread { } } -impl AudioThread { +impl AudioThread { fn run(mut self) -> Result { set_thread_priority(); unsafe { @@ -270,7 +271,7 @@ impl AudioThread::from_client( &self.interface, - self.stream_config.channels.count(), + self.stream_config.output_channels.count(), )? else { eprintln!("Null buffer from WASAPI"); @@ -282,9 +283,10 @@ impl AudioThread AudioThread::from_client( &self.interface, - self.stream_config.channels.count(), + self.stream_config.output_channels.count(), frames_requested, )?; let timestamp = self.output_timestamp()?; @@ -330,7 +332,7 @@ impl AudioThread AudioStreamHandle for WasapiStream { } } -impl WasapiStream { +impl WasapiStream { pub(crate) fn new_input( device: WasapiMMDevice, stream_config: StreamConfig, @@ -440,7 +442,7 @@ fn stream_instant(audio_clock: &Audio::IAudioClock) -> Result Audio::WAVEFORMATEXTENSIBLE { let format_tag = KernelStreaming::WAVE_FORMAT_EXTENSIBLE; - let channels = config.channels as u16; + let channels = config.output_channels as u16; let sample_rate = config.samplerate as u32; let sample_bytes = size_of::() as u16; let avg_bytes_per_sec = u32::from(channels) * sample_rate * u32::from(sample_bytes); @@ -507,7 +509,7 @@ pub(crate) fn is_output_config_supported( let new_channels = 0u32.with_indices(0..format.Format.nChannels as _); let new_samplerate = sample_rate as f64; if stream_config.samplerate != new_samplerate - || stream_config.channels.count() != new_channels.count() + || stream_config.output_channels.count() != new_channels.count() { return Ok(false); } diff --git a/src/duplex.rs b/src/duplex.rs index 027d111..45b5f76 100644 --- a/src/duplex.rs +++ b/src/duplex.rs @@ -1,37 +1,20 @@ //! Module for simultaneous input/output audio processing //! -//! This module includes a proxy for gathering an input audio stream, and optionally process it to resample it to the +//! This module includes a proxy for gathering an input audio stream and optionally processing it to resample it to the //! output sample rate. use crate::audio_buffer::AudioRef; use crate::channel_map::Bitset; use crate::{ - AudioCallbackContext, AudioDevice, AudioInput, AudioInputCallback, AudioInputDevice, - AudioOutput, AudioOutputCallback, AudioOutputDevice, AudioStreamHandle, SendEverywhereButOnWeb, - StreamConfig, + AudioCallback, AudioCallbackContext, AudioDevice, AudioInput, AudioOutput, AudioStreamHandle, + SendEverywhereButOnWeb, StreamConfig, }; use fixed_resample::{PushStatus, ReadStatus, ResamplingChannelConfig}; -use std::error::Error; use std::num::NonZeroUsize; +use std::ops::IndexMut; use thiserror::Error; const MAX_CHANNELS: usize = 64; -/// Trait of types that can process both input and output audio streams at the same time. -pub trait AudioDuplexCallback: 'static + SendEverywhereButOnWeb { - /// Processes audio data in a duplex stream. - /// - /// # Arguments - /// * `context` - The context containing stream configuration and timing information - /// * `input` - The input audio buffer containing captured audio data - /// * `output` - The output audio buffer to be filled with processed audio data - fn on_audio_data( - &mut self, - context: AudioCallbackContext, - input: AudioInput, - output: AudioOutput, - ); -} - /// Type which handles both a duplex stream handle. pub struct DuplexStream { _input_stream: Box>, @@ -41,6 +24,7 @@ pub struct DuplexStream { /// Input proxy for transferring an input signal to a separate output callback to be processed as a duplex stream. pub struct InputProxy { producer: Option>, + scratch_buffer: Option>, // TODO: switch to non-interleaved processing receive_output_samplerate: rtrb::Consumer, send_consumer: rtrb::Producer>, } @@ -59,59 +43,79 @@ impl InputProxy { Self { producer: None, receive_output_samplerate, + scratch_buffer: None, send_consumer, }, produce_output_samplerate, receive_consumer, ) } + + fn change_output_samplerate( + &mut self, + context: AudioCallbackContext, + output_samplerate: u32, + ) -> bool { + let Some(num_channels) = NonZeroUsize::new(context.stream_config.output_channels) else { + log::error!("Input proxy: no input channels given"); + return true; + }; + let input_samplerate = context.stream_config.samplerate as _; + log::debug!( + "Creating resampling channel ({} Hz) -> ({} Hz) ({} channels)", + input_samplerate, + output_samplerate, + num_channels.get() + ); + let (tx, rx) = fixed_resample::resampling_channel( + num_channels, + input_samplerate, + output_samplerate, + ResamplingChannelConfig { + latency_seconds: 0.01, + quality: fixed_resample::ResampleQuality::Low, + ..Default::default() + }, + ); + self.producer.replace(tx); + match self.send_consumer.push(rx) { + Ok(_) => { + log::debug!( + "Input proxy: resampling channel ({} Hz) sent", + context.stream_config.samplerate + ); + } + Err(err) => { + log::error!("Input proxy: cannot send resampling channel: {}", err); + } + } + false + } } -impl AudioInputCallback for InputProxy { - /// Processes incoming audio data and stores it in the internal buffer. - /// - /// Handles sample rate conversion between input and output streams. - /// - /// # Arguments - /// * `context` - The context containing stream configuration and timing information - /// * `input` - The input audio buffer containing captured audio data - fn on_input_data(&mut self, context: AudioCallbackContext, input: AudioInput) { - log::trace!(num_samples = input.buffer.num_samples(), num_channels = input.buffer.num_channels(); - "on_input_data"); +impl AudioCallback for InputProxy { + fn prepare(&mut self, context: AudioCallbackContext) { + let len = context.stream_config.input_channels * context.stream_config.max_frame_count; + self.scratch_buffer = Some(Box::from_iter(std::iter::repeat_n(0.0, len))); + } + + fn process_audio( + &mut self, + context: AudioCallbackContext, + input: AudioInput, + output: AudioOutput, + ) { + debug_assert_eq!( + 0, + output.buffer.num_channels(), + "Input proxy should not be receiving audio output data" + ); + log::trace!(num_samples = input.buffer.num_frames(), num_channels = input.buffer.num_channels(); + "InputProxy::process_audio"); + if let Ok(output_samplerate) = self.receive_output_samplerate.pop() { - let Some(num_channels) = NonZeroUsize::new(context.stream_config.channels.count()) - else { - log::error!("Input proxy: no input channels given"); + if self.change_output_samplerate(context, output_samplerate) { return; - }; - let input_samplerate = context.stream_config.samplerate as _; - log::debug!( - "Creating resampling channel ({} Hz) -> ({} Hz) ({} channels)", - input_samplerate, - output_samplerate, - num_channels.get() - ); - let (tx, rx) = fixed_resample::resampling_channel( - num_channels, - input_samplerate, - output_samplerate, - ResamplingChannelConfig { - latency_seconds: 0.01, - quality: fixed_resample::ResampleQuality::Low, - ..Default::default() - }, - ); - self.producer.replace(tx); - match self.send_consumer.push(rx) { - Ok(_) => { - log::debug!( - "Input proxy: resampling channel ({} Hz) sent", - context.stream_config.samplerate - ); - } - Err(err) => { - log::error!("Input proxy: cannot send resampling channel: {}", err); - } } } let Some(producer) = &mut self.producer else { @@ -119,22 +123,24 @@ impl AudioInputCallback for InputProxy { return; }; - let mut scratch = [0f32; 32 * MAX_CHANNELS]; - for slice in input.buffer.chunks(32) { - let len = slice.num_samples() * slice.num_channels(); - debug_assert!( - slice.copy_into_interleaved(&mut scratch[..len]), - "Cannot fail: len is computed from slice itself" - ); - match producer.push_interleaved(&scratch[..len]) { - PushStatus::OverflowOccurred { .. } => { - log::error!("Input proxy: overflow occurred"); - } - PushStatus::UnderflowCorrected { .. } => { - log::error!("Input proxy: underflow corrected"); - } - _ => {} + let scratch = self + .scratch_buffer + .as_mut() + .unwrap() + .index_mut(0..input.buffer.num_frames()); + let len = input.buffer.num_frames() * input.buffer.num_channels(); + debug_assert!( + input.buffer.copy_into_interleaved(scratch), + "Cannot fail: len is computed from slice itself" + ); + match producer.push_interleaved(&scratch[..len]) { + PushStatus::OverflowOccurred { .. } => { + log::error!("Input proxy: overflow occurred"); } + PushStatus::UnderflowCorrected { .. } => { + log::error!("Input proxy: underflow corrected"); + } + _ => {} } } } @@ -150,8 +156,6 @@ pub enum DuplexCallbackError { InputError(InputError), /// An error occurred in the output stream OutputError(OutputError), - /// An error that doesn't fit into other categories - Other(Box), } /// [`AudioOutputCallback`] implementation for which runs the provided [`AudioDuplexCallback`]. @@ -160,7 +164,7 @@ pub struct DuplexCallback { receive_consumer: rtrb::Consumer>, send_samplerate: rtrb::Producer, callback: Callback, - storage_raw: Box<[f32]>, + storage_raw: Option>, current_samplerate: u32, num_input_channels: usize, resample_config: ResamplingChannelConfig, @@ -171,14 +175,33 @@ impl DuplexCallback { /// /// # Returns /// The wrapped callback instance or an error if extraction fails - pub fn into_inner(self) -> Result> { - Ok(self.callback) + pub fn into_inner(self) -> Callback { + self.callback } } -impl AudioOutputCallback for DuplexCallback { - fn on_output_data(&mut self, context: AudioCallbackContext, output: AudioOutput) { - // If changed, send new output samplerate to input proxy +impl AudioCallback for DuplexCallback { + fn prepare(&mut self, context: AudioCallbackContext) { + let len = context.stream_config.output_channels * context.stream_config.max_frame_count; + self.storage_raw = Some(Box::from_iter(std::iter::repeat_n(0.0, len))); + self.callback.prepare(context); + } + + fn process_audio( + &mut self, + context: AudioCallbackContext, + input: AudioInput, + output: AudioOutput, + ) { + debug_assert_eq!( + 0, + input.buffer.num_channels(), + "DuplexCallback should not be receiving audio input data" + ); + log::trace!(num_samples = output.buffer.num_frames(), num_channels = output.buffer.num_channels(); + "DuplexCallback::process_audio"); + + // If changed, send the new output samplerate to input proxy let samplerate = context.stream_config.samplerate as u32; if samplerate != self.current_samplerate && self.send_samplerate.push(samplerate).is_ok() { log::debug!("Output samplerate changed to {}", samplerate); @@ -196,11 +219,12 @@ impl AudioOutputCallback for DuplexCallback { log::error!("Output resample channel underflow occurred"); @@ -220,7 +244,7 @@ impl AudioOutputCallback for DuplexCallback AudioOutputCallback for DuplexCallback` -/// * `OutputHandle` - The type of the output stream handle, must implement `AudioStreamHandle>` +/// * `InputHandle` - The type of the input stream handle must implement `AudioStreamHandle` +/// * `OutputHandle` - The type of the output stream handle must implement `AudioStreamHandle>` /// /// # Example /// /// ```no_run -/// use interflow::duplex::AudioDuplexCallback; /// use interflow::prelude::*; /// /// let input_device = default_input_device(); @@ -252,18 +275,20 @@ impl AudioOutputCallback for DuplexCallback Self { Self } /// } /// -/// impl AudioDuplexCallback for MyCallback { -/// fn on_audio_data(&mut self, context: AudioCallbackContext, input: AudioInput, output: AudioOutput) { +/// impl AudioCallback for MyCallback { +/// fn prepare(&mut self, context: AudioCallbackContext) {} +/// fn process_audio(&mut self, context: AudioCallbackContext, input: AudioInput, output: AudioOutput) { /// // Implementation left as an exercise to the reader /// } /// } /// /// // Create and use a duplex stream +/// let config = output_device.default_config().unwrap(); /// let stream_handle = create_duplex_stream( /// input_device, /// output_device, /// MyCallback::new(), -/// DuplexStreamConfig::new(input_config, output_config), +/// DuplexStreamConfig::new(config), /// ).expect("Failed to create duplex stream"); /// /// // Later, stop the stream and retrieve the callback @@ -296,9 +321,7 @@ impl< .output_handle .eject() .map_err(DuplexCallbackError::OutputError)?; - duplex_callback - .into_inner() - .map_err(DuplexCallbackError::Other) + Ok(duplex_callback.into_inner()) } } @@ -306,22 +329,35 @@ impl< #[derive(Debug, Copy, Clone)] pub struct DuplexStreamConfig { /// Input stream configuration - pub input: StreamConfig, - /// Output stream configuration - pub output: StreamConfig, - /// Use high quality resampling. Increases latency and CPU usage. + pub stream_config: StreamConfig, + /// Use high-quality resampling. Increases latency and CPU usage. pub high_quality_resampling: bool, /// Target latency. May be higher if the resampling takes too much latency. pub target_latency_secs: f32, } +impl DuplexStreamConfig { + pub(crate) fn input_config(&self) -> StreamConfig { + StreamConfig { + output_channels: 0, + ..self.stream_config + } + } + + pub(crate) fn output_config(&self) -> StreamConfig { + StreamConfig { + input_channels: 0, + ..self.stream_config + } + } +} + impl DuplexStreamConfig { /// Create a new duplex stream config with the provided input and output stream configuration, and default /// resampler values. - pub fn new(input: StreamConfig, output: StreamConfig) -> Self { + pub fn new(stream_config: StreamConfig) -> Self { Self { - input, - output, + stream_config, high_quality_resampling: false, target_latency_secs: 0.01, } @@ -331,8 +367,8 @@ impl DuplexStreamConfig { /// Type alias of the result of creating a duplex stream. pub type DuplexStreamResult = Result< DuplexStreamHandle< - ::StreamHandle, - ::StreamHandle>, + ::StreamHandle, + ::StreamHandle>, >, DuplexCallbackError<::Error, ::Error>, >; @@ -360,7 +396,6 @@ pub type DuplexStreamResult = Result< /// # Example /// /// ```no_run -/// use interflow::duplex::AudioDuplexCallback; /// use interflow::prelude::*; /// /// struct MyCallback; @@ -371,9 +406,10 @@ pub type DuplexStreamResult = Result< /// } /// } /// -/// impl AudioDuplexCallback for MyCallback { -/// fn on_audio_data(&mut self, context: AudioCallbackContext, input: AudioInput, output: AudioOutput) { -/// // Implementation left as exercise to the reader +/// impl AudioCallback for MyCallback { +/// fn prepare(&mut self, context: AudioCallbackContext) {} +/// fn process_audio(&mut self, context: AudioCallbackContext, input: AudioInput, output: AudioOutput) { +/// // Implementation left as an exercise to the reader /// } /// } /// @@ -384,19 +420,20 @@ pub type DuplexStreamResult = Result< /// /// let callback = MyCallback::new(); /// +/// let config = output_device.default_config().unwrap(); /// let duplex_stream = create_duplex_stream( /// input_device, /// output_device, /// callback, -/// DuplexStreamConfig::new(input_config, output_config), +/// DuplexStreamConfig::new(config), /// ).expect("Failed to create duplex stream"); /// /// ``` #[allow(clippy::type_complexity)] // Allowing because moving to a type alias would be just as complex pub fn create_duplex_stream< - InputDevice: AudioInputDevice, - OutputDevice: AudioOutputDevice, - Callback: AudioDuplexCallback, + InputDevice: AudioDevice, + OutputDevice: AudioDevice, + Callback: AudioCallback, >( input_device: InputDevice, output_device: OutputDevice, @@ -411,19 +448,19 @@ pub fn create_duplex_stream< > { let (proxy, send_samplerate, receive_consumer) = InputProxy::new(); let input_handle = input_device - .create_input_stream(config.input, proxy) + .create_stream(config.input_config(), proxy) .map_err(DuplexCallbackError::InputError)?; let output_handle = output_device - .create_output_stream( - config.output, + .create_stream( + config.output_config(), DuplexCallback { input: None, send_samplerate, receive_consumer, callback, - storage_raw: vec![0f32; 8192 * MAX_CHANNELS].into_boxed_slice(), + storage_raw: None, current_samplerate: 0, - num_input_channels: config.input.channels.count(), + num_input_channels: config.stream_config.input_channels, resample_config: ResamplingChannelConfig { capacity_seconds: (2.0 * config.target_latency_secs as f64).max(0.5), latency_seconds: config.target_latency_secs as f64, diff --git a/src/lib.rs b/src/lib.rs index 6a07515..8c8415c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -40,11 +40,11 @@ bitflags! { } /// Audio drivers provide access to the inputs and outputs of devices. -/// Several drivers might provide the same accesses, some sharing it with other applications, +/// Several drivers might provide the same access, some sharing it with other applications, /// while others work in exclusive mode. pub trait AudioDriver { /// Type of errors that can happen when using this audio driver. - type Error: std::error::Error; + type Error: SendEverywhereButOnWeb + std::error::Error; /// Type of audio devices this driver provides. type Device: AudioDevice; @@ -101,13 +101,10 @@ pub struct StreamConfig { /// Configured sample rate of the requested stream. The opened stream can have a different /// sample rate, so don't rely on this parameter being correct at runtime. pub samplerate: f64, - /// Map of channels requested by the stream. Entries correspond in order to - /// [AudioDevice::channel_map]. - /// - /// Some drivers allow specifying which channels are going to be opened and available through - /// the audio buffers. For other drivers, only the number of requested channels is used, and - /// order does not matter. - pub channels: ChannelMap32, + /// Number of input channels requested + pub input_channels: usize, + /// Number of output channels requested + pub output_channels: usize, /// Range of preferential buffer sizes, in units of audio samples per channel. /// The library will make a best-effort attempt at honoring this setting, and in future versions /// may provide additional buffering to ensure it, but for now you should not make assumptions @@ -118,12 +115,26 @@ pub struct StreamConfig { pub exclusive: bool, } +/// Configuration for an audio stream. +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct ResolvedStreamConfig { + /// Configured sample rate of the requested stream. The opened stream can have a different + /// sample rate, so don't rely on this parameter being correct at runtime. + pub samplerate: f64, + /// Number of input channels requested + pub input_channels: usize, + /// Number of output channels requested + pub output_channels: usize, + /// Maximum number of frames the audio callback will receive + pub max_frame_count: usize, +} + /// Audio channel description. #[derive(Debug, Clone)] pub struct Channel<'a> { /// Index of the channel in the device pub index: usize, - /// Display name for the channel, if available, else a generic name like "Channel 1" + /// Display the name for the channel, if available, else a generic name like "Channel 1" pub name: Cow<'a, str>, } @@ -131,8 +142,13 @@ pub struct Channel<'a> { /// and depending on the driver, can be duplex devices which can provide both of them at the same /// time natively. pub trait AudioDevice { + /// Type of the resulting stream. This stream can be used to control the audio processing + /// externally or stop it completely and give back ownership of the callback with + /// [`AudioStreamHandle::eject`]. + type StreamHandle: AudioStreamHandle; + /// Type of errors that can happen when using this device. - type Error: std::error::Error; + type Error: SendEverywhereButOnWeb + std::error::Error; /// Device display name fn name(&self) -> Cow<'_, str>; @@ -144,93 +160,19 @@ pub trait AudioDevice { /// specifying which channels to open when creating an audio stream. fn channel_map(&self) -> impl IntoIterator>; + /// Default configuration for this device. If [`Ok`], should return a [`StreamConfig`] that is supported (i.e., + /// returns `true` when passed to [`Self::is_config_supported`]). + fn default_config(&self) -> Result; + /// Not all configuration values make sense for a particular device, and this method tests a /// configuration to see if it can be used in an audio stream. fn is_config_supported(&self, config: &StreamConfig) -> bool; - /// Enumerate all possible configurations this device supports. If that is not provided by - /// the device, and not easily generated manually, this will return `None`. - fn enumerate_configurations(&self) -> Option>; - - /// Returns the supported I/O buffer size range for the device. - fn buffer_size_range(&self) -> Result<(Option, Option), Self::Error> { - Ok((None, None)) - } -} - -/// Marker trait for values which are [Send] everywhere but on the web (as WASM does not yet have -/// web targets. -/// -/// This should only be used to define the traits and should not be relied upon in external code. -/// -/// This definition is selected on non-web platforms, and does require [`Send`]. -#[cfg(not(wasm))] -pub trait SendEverywhereButOnWeb: 'static + Send {} -#[cfg(not(wasm))] -impl SendEverywhereButOnWeb for T {} - -/// Marker trait for values which are [Send] everywhere but on the web (as WASM does not yet have -/// web targets. -/// -/// This should only be used to define the traits and should not be relied upon in external code. -/// -/// This definition is selected on web platforms, and does not require [`Send`]. -#[cfg(wasm)] -pub trait SendEverywhereButOnWeb {} -#[cfg(wasm)] -impl SendEverywhereButOnWeb for T {} - -/// Trait for types which can provide input streams. -/// -/// Input devices require a [`AudioInputCallback`] which receives the audio data from the input -/// device, and processes it. -pub trait AudioInputDevice: AudioDevice { - /// Type of the resulting stream. This stream can be used to control the audio processing - /// externally, or stop it completely and give back ownership of the callback with - /// [`AudioStreamHandle::eject`]. - type StreamHandle: AudioStreamHandle; - - /// Return the default configuration for this device, if there is one. The returned configuration *must* be - /// valid according to [`Self::is_config_supported`]. - fn default_input_config(&self) -> Result; - - /// Creates an input stream with the provided stream configuration. For this call to be - /// valid, [`AudioDevice::is_config_supported`] should have returned `true` on the provided - /// configuration. - /// - /// An input callback is required to process the audio, whose ownership will be transferred - /// to the audio stream. - fn create_input_stream( - &self, - stream_config: StreamConfig, - callback: Callback, - ) -> Result, Self::Error>; - - /// Create an input stream with the default configuration (as returned by [`Self::default_input_config`]). - /// - /// # Arguments + /// List all possible configurations this device supports. If that is not provided by + /// the device and not easily generated manually, this will return `None`. /// - /// - `callback`: Callback to process the audio input - fn default_input_stream( - &self, - callback: Callback, - ) -> Result, Self::Error> { - self.create_input_stream(self.default_input_config()?, callback) - } -} - -/// Trait for types which can provide output streams. -/// -/// Output devices require a [`AudioOutputCallback`] which receives the audio data from the output -/// device, and processes it. -pub trait AudioOutputDevice: AudioDevice { - /// Type of the resulting stream. This stream can be used to control the audio processing - /// externally, or stop it completely and give back ownership of the callback with - /// [`AudioStreamHandle::eject`]. - type StreamHandle: AudioStreamHandle; - - /// Return the default output configuration for this device, if it exists - fn default_output_config(&self) -> Result; + /// The returned configurations should be supported as valid when passed to [`Self::is_config_supported`]. + fn enumerate_configurations(&self) -> Option>; /// Creates an output stream with the provided stream configuration. For this call to be /// valid, [`AudioDevice::is_config_supported`] should have returned `true` on the provided @@ -238,7 +180,7 @@ pub trait AudioOutputDevice: AudioDevice { /// /// An output callback is required to process the audio, whose ownership will be transferred /// to the audio stream. - fn create_output_stream( + fn create_stream( &self, stream_config: StreamConfig, callback: Callback, @@ -249,25 +191,35 @@ pub trait AudioOutputDevice: AudioDevice { /// # Arguments /// /// - `callback`: Output callback to generate audio data with. - fn default_output_stream( + fn default_stream( &self, callback: Callback, ) -> Result, Self::Error> { - self.create_output_stream(self.default_output_config()?, callback) + self.create_stream(self.default_config()?, callback) } } -/// Trait for types which handles an audio stream (input or output). -pub trait AudioStreamHandle { - /// Type of errors which have caused the stream to fail. - type Error: std::error::Error; +/// Marker trait for values which are [`Send`] everywhere but on the web (as WASM does not yet have +/// web targets). +/// +/// This should only be used to define the traits and should not be relied upon in external code. +/// +/// This definition is selected on non-web platforms and does require [`Send`]. +#[cfg(not(wasm))] +pub trait SendEverywhereButOnWeb: 'static + Send {} +#[cfg(not(wasm))] +impl SendEverywhereButOnWeb for T {} - /// Eject the stream, returning ownership of the callback. - /// - /// An error can occur when an irrecoverable error has occured and ownership has been lost - /// already. - fn eject(self) -> Result; -} +/// Marker trait for values which are [Send] everywhere but on the web (as WASM does not yet have +/// web targets. +/// +/// This should only be used to define the traits and should not be relied upon in external code. +/// +/// This definition is selected on web platforms and does not require [`Send`]. +#[cfg(wasm)] +pub trait SendEverywhereButOnWeb {} +#[cfg(wasm)] +impl SendEverywhereButOnWeb for T {} #[duplicate::duplicate_item( name bufty; @@ -292,21 +244,35 @@ pub struct name<'a, T> { pub struct AudioCallbackContext { /// Passed-in stream configuration. Values have been updated where necessary to correspond to /// the actual stream properties. - pub stream_config: StreamConfig, + pub stream_config: ResolvedStreamConfig, /// Callback-wide timestamp. pub timestamp: Timestamp, } -/// Trait of types which process input audio data. This is the trait that users will want to -/// implement when processing an input device. -pub trait AudioInputCallback { - /// Callback called when input data is available to be processed. - fn on_input_data(&mut self, context: AudioCallbackContext, input: AudioInput); +/// Trait for types which handles an audio stream (input or output). +pub trait AudioStreamHandle { + /// Type of errors which have caused the stream to fail. + type Error: SendEverywhereButOnWeb + std::error::Error; + + /// Eject the stream, returning ownership of the callback. + /// + /// An error can occur when an irrecoverable error has occured and ownership has been lost + /// already. + fn eject(self) -> Result; } -/// Trait of types which process output audio data. This is the trait that users will want to -/// implement when processing an output device. -pub trait AudioOutputCallback { - /// Callback called when output data is available to be processed. - fn on_output_data(&mut self, context: AudioCallbackContext, input: AudioOutput); +/// Trait of types which process audio data. This is the trait that users will want to +/// implement when processing audio from a device. +pub trait AudioCallback: SendEverywhereButOnWeb { + /// Prepare the audio callback to process audio. This function is *not* real-time safe (i.e., allocations can be + /// performed), in preparation for processing the stream with [`Self::process_audio`]. + fn prepare(&mut self, context: AudioCallbackContext); + + /// Callback called when audio data can be processed. + fn process_audio( + &mut self, + context: AudioCallbackContext, + input: AudioInput, + output: AudioOutput, + ); } diff --git a/src/prelude.rs b/src/prelude.rs index 1588298..ddca334 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -4,7 +4,5 @@ #[cfg(os_wasapi)] pub use crate::backends::wasapi::prelude::*; pub use crate::backends::*; -pub use crate::duplex::{ - create_duplex_stream, AudioDuplexCallback, DuplexStreamConfig, DuplexStreamHandle, -}; +pub use crate::duplex::{create_duplex_stream, DuplexStreamConfig, DuplexStreamHandle}; pub use crate::*; From 51a94d48759c4530942c76f51dad6626d4ca4bfc Mon Sep 17 00:00:00 2001 From: Nathan Graule Date: Tue, 27 May 2025 15:24:49 +0200 Subject: [PATCH 02/38] fix: examples and CoreAudio backend --- examples/duplex.rs | 19 ++++++++++---- examples/input.rs | 16 +++++++----- examples/loopback.rs | 23 +++++++++------- examples/sine_wave.rs | 2 +- examples/util/sine.rs | 12 ++++++--- src/backends/coreaudio.rs | 45 +++++++++++++++++++++++--------- src/duplex.rs | 29 +++++++++++++++------ src/lib.rs | 2 +- src/timestamp.rs | 55 ++++++++++++++++++++++++++++++++++++--- 9 files changed, 153 insertions(+), 50 deletions(-) diff --git a/examples/duplex.rs b/examples/duplex.rs index 83b1df6..9c080d3 100644 --- a/examples/duplex.rs +++ b/examples/duplex.rs @@ -6,10 +6,16 @@ mod util; //noinspection RsUnwrap fn main() -> Result<()> { + env_logger::init(); let input = default_input_device(); let output = default_output_device(); - let mut config = output.default_config().unwrap(); - config.buffer_size_range = (Some(128), Some(512)); + log::info!("Opening input: {}", input.name()); + log::info!("Opening output: {}", output.name()); + let config = StreamConfig { + buffer_size_range: (Some(128), Some(512)), + input_channels: 1, + ..output.default_config().unwrap() + }; let duplex_config = DuplexStreamConfig::new(config); let stream = create_duplex_stream(input, output, RingMod::new(), duplex_config).unwrap(); println!("Press Enter to stop"); @@ -31,17 +37,20 @@ impl RingMod { } impl AudioCallback for RingMod { - fn prepare(&mut self, context: AudioCallbackContext) {} + fn prepare(&mut self, context: AudioCallbackContext) { + self.carrier.prepare(context); + } + fn process_audio( &mut self, context: AudioCallbackContext, input: AudioInput, mut output: AudioOutput, ) { - let sr = context.stream_config.samplerate as f32; + let sr = context.stream_config.sample_rate as f32; for i in 0..output.buffer.num_frames() { let inp = input.buffer.get_frame(i)[0]; - let c = self.carrier.next_sample(sr); + let c = self.carrier.next_sample(); output.buffer.set_mono(i, inp * c); } } diff --git a/examples/input.rs b/examples/input.rs index 792a0db..87c9d29 100644 --- a/examples/input.rs +++ b/examples/input.rs @@ -11,9 +11,7 @@ fn main() -> Result<()> { let device = default_input_device(); let value = Arc::new(AtomicF32::new(0.)); - let stream = device - .default_input_stream(RmsMeter::new(value.clone())) - .unwrap(); + let stream = device.default_stream(RmsMeter::new(value.clone())).unwrap(); util::display_peakmeter(value)?; stream.eject().unwrap(); Ok(()) @@ -31,11 +29,17 @@ impl RmsMeter { } impl AudioCallback for RmsMeter { - fn process_audio(&mut self, context: AudioCallbackContext, input: AudioInput) { + fn prepare(&mut self, _: AudioCallbackContext) {} + fn process_audio( + &mut self, + context: AudioCallbackContext, + input: AudioInput, + _output: AudioOutput, + ) { let meter = self .meter - .get_or_insert_with(|| PeakMeter::new(context.stream_config.samplerate as f32, 15.0)); - meter.set_samplerate(context.stream_config.samplerate as f32); + .get_or_insert_with(|| PeakMeter::new(context.stream_config.sample_rate as f32, 15.0)); + meter.set_samplerate(context.stream_config.sample_rate as f32); meter.process_buffer(input.buffer.as_ref()); self.value .store(meter.value(), std::sync::atomic::Ordering::Relaxed); diff --git a/examples/loopback.rs b/examples/loopback.rs index 69ae077..dcd80a9 100644 --- a/examples/loopback.rs +++ b/examples/loopback.rs @@ -10,14 +10,16 @@ fn main() -> Result<()> { let input = default_input_device(); let output = default_output_device(); - let mut input_config = input.default_input_config().unwrap(); - input_config.buffer_size_range = (Some(128), Some(512)); - let mut output_config = output.default_output_config().unwrap(); - output_config.buffer_size_range = (Some(128), Some(512)); - input_config.output_channels = 0b01; - output_config.output_channels = 0b11; + log::info!("Opening input : {}", input.name()); + log::info!("Opening output: {}", output.name()); + let config = StreamConfig { + buffer_size_range: (Some(128), Some(512)), + input_channels: 1, + output_channels: 1, + ..output.default_config().unwrap() + }; let value = Arc::new(AtomicF32::new(0.)); - let config = DuplexStreamConfig::new(input_config, output_config); + let config = DuplexStreamConfig::new(config); let stream = create_duplex_stream(input, output, Loopback::new(44100., value.clone()), config).unwrap(); util::display_peakmeter(value)?; @@ -39,15 +41,16 @@ impl Loopback { } } -impl AudioDuplexCallback for Loopback { - fn on_audio_data( +impl AudioCallback for Loopback { + fn prepare(&mut self, context: AudioCallbackContext) {} + fn process_audio( &mut self, context: AudioCallbackContext, input: AudioInput, mut output: AudioOutput, ) { self.meter - .set_samplerate(context.stream_config.samplerate as f32); + .set_samplerate(context.stream_config.sample_rate as f32); let rms = self.meter.process_buffer(input.buffer.as_ref()); self.value.store(rms, std::sync::atomic::Ordering::Relaxed); output.buffer.as_interleaved_mut().fill(0.0); diff --git a/examples/sine_wave.rs b/examples/sine_wave.rs index 6a0b14e..b65b26e 100644 --- a/examples/sine_wave.rs +++ b/examples/sine_wave.rs @@ -9,7 +9,7 @@ fn main() -> Result<()> { let device = default_output_device(); println!("Using device {}", device.name()); - let stream = device.default_output_stream(SineWave::new(440.0)).unwrap(); + let stream = device.default_stream(SineWave::new(440.0)).unwrap(); println!("Press Enter to stop"); std::io::stdin().read_line(&mut String::new())?; stream.eject().unwrap(); diff --git a/examples/util/sine.rs b/examples/util/sine.rs index 73b8121..5e5f596 100644 --- a/examples/util/sine.rs +++ b/examples/util/sine.rs @@ -4,10 +4,13 @@ use std::f32::consts::TAU; pub struct SineWave { pub frequency: f32, pub phase: f32, + step_frequency_scaling: f32, } impl AudioCallback for SineWave { - fn prepare(&mut self, _context: AudioCallbackContext) {} + fn prepare(&mut self, context: AudioCallbackContext) { + self.step_frequency_scaling = context.stream_config.sample_rate.recip() as f32; + } fn process_audio( &mut self, context: AudioCallbackContext, @@ -20,7 +23,7 @@ impl AudioCallback for SineWave { ); let sr = context.timestamp.samplerate as f32; for i in 0..output.buffer.num_frames() { - output.buffer.set_mono(i, self.next_sample(sr)); + output.buffer.set_mono(i, self.next_sample()); } // Reduce amplitude to not blow up speakers and ears output.buffer.change_amplitude(0.125); @@ -32,11 +35,12 @@ impl SineWave { Self { frequency, phase: 0.0, + step_frequency_scaling: 0.0, } } - pub fn next_sample(&mut self, samplerate: f32) -> f32 { - let step = samplerate.recip() * self.frequency; + pub fn next_sample(&mut self) -> f32 { + let step = self.step_frequency_scaling * self.frequency; let y = (TAU * self.phase).sin(); self.phase += step; if self.phase > 1. { diff --git a/src/backends/coreaudio.rs b/src/backends/coreaudio.rs index cbacd27..6193d26 100644 --- a/src/backends/coreaudio.rs +++ b/src/backends/coreaudio.rs @@ -61,7 +61,6 @@ fn set_device_property( use thiserror::Error; use crate::audio_buffer::{AudioBuffer, Sample}; -use crate::channel_map::Bitset; use crate::prelude::{AudioMut, AudioRef, ChannelMap32}; use crate::timestamp::Timestamp; use crate::{ @@ -360,7 +359,7 @@ impl CoreAudioStream { fn new_input( device_id: AudioDeviceID, stream_config: StreamConfig, - callback: Callback, + mut callback: Callback, ) -> Result { let mut audio_unit = audio_unit_from_device_id(device_id, true)?; let asbd = @@ -371,18 +370,28 @@ impl CoreAudioStream { Element::Input, Some(&asbd), )?; + let frame_count = audio_unit.get_property( + kAudioDevicePropertyBufferFrameSize, + Scope::Input, + Element::Input, + )?; let stream_config = ResolvedStreamConfig { - samplerate: asbd.mSampleRate, + sample_rate: asbd.mSampleRate, input_channels: asbd.mChannelsPerFrame as _, output_channels: 0, - max_frame_count: asbd.mFramesPerPacket as _, + max_frame_count: frame_count, }; let mut buffer = - AudioBuffer::zeroed(asbd.mChannelsPerFrame as _, stream_config.samplerate as _); + AudioBuffer::zeroed(asbd.mChannelsPerFrame as _, stream_config.sample_rate as _); // Set up the callback retrieval process without needing to make the callback `Sync` let (tx, rx) = oneshot::channel::>(); + callback.prepare(AudioCallbackContext { + stream_config, + timestamp: Timestamp::new(asbd.mSampleRate), + }); let mut callback = Some(callback); + audio_unit.set_input_callback(move |args: Args>| { if let Ok(sender) = rx.try_recv() { sender.send(callback.take().unwrap()).unwrap(); @@ -397,7 +406,7 @@ impl CoreAudioStream { *out = inp.into_float(); } let timestamp = - Timestamp::from_count(stream_config.samplerate, args.time_stamp.mSampleTime as _); + Timestamp::from_count(stream_config.sample_rate, args.time_stamp.mSampleTime as _); let input = AudioInput { buffer: buffer.as_ref(), timestamp, @@ -428,7 +437,7 @@ impl CoreAudioStream { fn new_output( device_id: AudioDeviceID, stream_config: StreamConfig, - callback: Callback, + mut callback: Callback, ) -> Result { let mut audio_unit = audio_unit_from_device_id(device_id, false)?; let asbd = @@ -439,17 +448,29 @@ impl CoreAudioStream { Element::Output, Some(&asbd), )?; + let frame_size = audio_unit.get_property( + kAudioDevicePropertyBufferFrameSize, + Scope::Output, + Element::Output, + )?; let stream_config = ResolvedStreamConfig { - samplerate: asbd.mSampleRate, + sample_rate: asbd.mSampleRate, input_channels: 0, output_channels: asbd.mChannelsPerFrame as _, - max_frame_count: asbd.mFramesPerPacket as _, + max_frame_count: frame_size, }; - let mut buffer = - AudioBuffer::zeroed(stream_config.output_channels, stream_config.samplerate as _); + let mut buffer = AudioBuffer::zeroed( + stream_config.output_channels, + stream_config.sample_rate as _, + ); // Set up the callback retrieval process without needing to make the callback `Sync` let (tx, rx) = oneshot::channel::>(); + callback.prepare(AudioCallbackContext { + stream_config, + timestamp: Timestamp::new(asbd.mSampleRate), + }); let mut callback = Some(callback); + audio_unit.set_render_callback(move |mut args: Args>| { if let Ok(sender) = rx.try_recv() { sender.send(callback.take().unwrap()).unwrap(); @@ -457,7 +478,7 @@ impl CoreAudioStream { } let mut buffer = buffer.slice_mut(..args.num_frames); let timestamp = - Timestamp::from_count(stream_config.samplerate, args.time_stamp.mSampleTime as _); + Timestamp::from_count(stream_config.sample_rate, args.time_stamp.mSampleTime as _); let dummy_input = AudioInput { buffer: AudioRef::empty(), timestamp: Timestamp::new(asbd.mSampleRate), diff --git a/src/duplex.rs b/src/duplex.rs index 45b5f76..aad0cae 100644 --- a/src/duplex.rs +++ b/src/duplex.rs @@ -3,14 +3,15 @@ //! This module includes a proxy for gathering an input audio stream and optionally processing it to resample it to the //! output sample rate. use crate::audio_buffer::AudioRef; -use crate::channel_map::Bitset; +use crate::timestamp::{AtomicTimestamp, Timestamp}; use crate::{ AudioCallback, AudioCallbackContext, AudioDevice, AudioInput, AudioOutput, AudioStreamHandle, - SendEverywhereButOnWeb, StreamConfig, + StreamConfig, }; use fixed_resample::{PushStatus, ReadStatus, ResamplingChannelConfig}; use std::num::NonZeroUsize; use std::ops::IndexMut; +use std::sync::Arc; use thiserror::Error; const MAX_CHANNELS: usize = 64; @@ -24,6 +25,7 @@ pub struct DuplexStream { /// Input proxy for transferring an input signal to a separate output callback to be processed as a duplex stream. pub struct InputProxy { producer: Option>, + timestamp: Arc, scratch_buffer: Option>, // TODO: switch to non-interleaved processing receive_output_samplerate: rtrb::Consumer, send_consumer: rtrb::Producer>, @@ -42,6 +44,7 @@ impl InputProxy { ( Self { producer: None, + timestamp: Arc::new(Timestamp::new(0.0).into()), receive_output_samplerate, scratch_buffer: None, send_consumer, @@ -56,11 +59,11 @@ impl InputProxy { context: AudioCallbackContext, output_samplerate: u32, ) -> bool { - let Some(num_channels) = NonZeroUsize::new(context.stream_config.output_channels) else { + let Some(num_channels) = NonZeroUsize::new(context.stream_config.input_channels) else { log::error!("Input proxy: no input channels given"); return true; }; - let input_samplerate = context.stream_config.samplerate as _; + let input_samplerate = context.stream_config.sample_rate as _; log::debug!( "Creating resampling channel ({} Hz) -> ({} Hz) ({} channels)", input_samplerate, @@ -82,7 +85,7 @@ impl InputProxy { Ok(_) => { log::debug!( "Input proxy: resampling channel ({} Hz) sent", - context.stream_config.samplerate + context.stream_config.sample_rate ); } Err(err) => { @@ -97,6 +100,8 @@ impl AudioCallback for InputProxy { fn prepare(&mut self, context: AudioCallbackContext) { let len = context.stream_config.input_channels * context.stream_config.max_frame_count; self.scratch_buffer = Some(Box::from_iter(std::iter::repeat_n(0.0, len))); + self.timestamp + .update(Timestamp::new(context.stream_config.sample_rate)); } fn process_audio( @@ -133,6 +138,7 @@ impl AudioCallback for InputProxy { input.buffer.copy_into_interleaved(scratch), "Cannot fail: len is computed from slice itself" ); + self.timestamp.add_frames(input.buffer.num_frames() as _); match producer.push_interleaved(&scratch[..len]) { PushStatus::OverflowOccurred { .. } => { log::error!("Input proxy: overflow occurred"); @@ -166,6 +172,7 @@ pub struct DuplexCallback { callback: Callback, storage_raw: Option>, current_samplerate: u32, + input_timestamp: Arc, num_input_channels: usize, resample_config: ResamplingChannelConfig, } @@ -202,7 +209,7 @@ impl AudioCallback for DuplexCallback { "DuplexCallback::process_audio"); // If changed, send the new output samplerate to input proxy - let samplerate = context.stream_config.samplerate as u32; + let samplerate = context.stream_config.sample_rate as u32; if samplerate != self.current_samplerate && self.send_samplerate.push(samplerate).is_ok() { log::debug!("Output samplerate changed to {}", samplerate); self.current_samplerate = samplerate; @@ -236,11 +243,15 @@ impl AudioCallback for DuplexCallback { } AudioRef::from_interleaved(slice, input.num_channels().get()).unwrap() } else { - AudioRef::from_interleaved(&[], self.num_input_channels).unwrap() + log::error!("No resampling input, dropping input frames"); + let len = frames * self.num_input_channels; + let slice = self.storage_raw.as_mut().unwrap().index_mut(..len); + slice.fill(0.0); + AudioRef::from_interleaved(slice, self.num_input_channels).unwrap() }; let input = AudioInput { - timestamp: context.timestamp, + timestamp: self.input_timestamp.as_timestamp(), buffer: storage, }; // Run user callback @@ -447,6 +458,7 @@ pub fn create_duplex_stream< DuplexCallbackError, > { let (proxy, send_samplerate, receive_consumer) = InputProxy::new(); + let input_timestamp = proxy.timestamp.clone(); let input_handle = input_device .create_stream(config.input_config(), proxy) .map_err(DuplexCallbackError::InputError)?; @@ -456,6 +468,7 @@ pub fn create_duplex_stream< DuplexCallback { input: None, send_samplerate, + input_timestamp, receive_consumer, callback, storage_raw: None, diff --git a/src/lib.rs b/src/lib.rs index 8c8415c..c4c1e89 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -120,7 +120,7 @@ pub struct StreamConfig { pub struct ResolvedStreamConfig { /// Configured sample rate of the requested stream. The opened stream can have a different /// sample rate, so don't rely on this parameter being correct at runtime. - pub samplerate: f64, + pub sample_rate: f64, /// Number of input channels requested pub input_channels: usize, /// Number of output channels requested diff --git a/src/timestamp.rs b/src/timestamp.rs index 3831dc0..7b8c62e 100644 --- a/src/timestamp.rs +++ b/src/timestamp.rs @@ -26,6 +26,7 @@ use std::ops; use std::ops::AddAssign; +use std::sync::atomic::AtomicU64; use std::time::Duration; /// Timestamp value, which computes duration information from a provided samplerate and a running @@ -140,13 +141,61 @@ impl Timestamp { } } + /// Compute the number of seconds represented in this [`Timestamp`]. + pub fn as_seconds(&self) -> f64 { + self.counter as f64 / self.samplerate + } + /// Compute the duration represented by this [`Timestamp`]. pub fn as_duration(&self) -> Duration { Duration::from_secs_f64(self.as_seconds()) } +} - /// Compute the number of seconds represented in this [`Timestamp`]. - pub fn as_seconds(&self) -> f64 { - self.counter as f64 / self.samplerate +/// Atomic version of [`Timestamp`] to be shared between threads. Mainly used by the [`crate::duplex`] module, but +/// may be useful in user code as well. +pub struct AtomicTimestamp { + samplerate: AtomicU64, + counter: AtomicU64, +} + +impl AtomicTimestamp { + /// Update the contents with the provided [`Timestamp`]. + pub fn update(&self, ts: Timestamp) { + self.samplerate.store( + ts.samplerate.to_bits(), + std::sync::atomic::Ordering::Relaxed, + ); + self.counter + .store(ts.counter, std::sync::atomic::Ordering::Relaxed); + } + + /// Load values and return them as a [`Timestamp`]. + pub fn as_timestamp(&self) -> Timestamp { + Timestamp { + samplerate: f64::from_bits(self.samplerate.load(std::sync::atomic::Ordering::Relaxed)), + counter: self.counter.load(std::sync::atomic::Ordering::Relaxed), + } + } + + /// Add the provided number of frames to this. + pub fn add_frames(&self, frames: u64) { + self.counter + .fetch_add(frames, std::sync::atomic::Ordering::Relaxed); + } +} + +impl From for AtomicTimestamp { + fn from(value: Timestamp) -> Self { + Self { + samplerate: AtomicU64::new(value.samplerate.to_bits()), + counter: AtomicU64::new(value.counter), + } + } +} + +impl From for Timestamp { + fn from(value: AtomicTimestamp) -> Self { + value.as_timestamp() } } From 6cd037ea1b50032f368cf05415ab779b7a20f945 Mon Sep 17 00:00:00 2001 From: Nathan Graule Date: Tue, 27 May 2025 16:39:29 +0200 Subject: [PATCH 03/38] fix: compile errors in alsa backend --- src/backends/alsa/device.rs | 90 +++++++++++++++++-------------------- src/backends/alsa/input.rs | 9 +++- src/backends/alsa/mod.rs | 2 +- src/backends/alsa/output.rs | 15 ++++--- src/backends/alsa/stream.rs | 37 +++++++++------ 5 files changed, 83 insertions(+), 70 deletions(-) diff --git a/src/backends/alsa/device.rs b/src/backends/alsa/device.rs index cc14556..bc8d815 100644 --- a/src/backends/alsa/device.rs +++ b/src/backends/alsa/device.rs @@ -1,10 +1,9 @@ use crate::backends::alsa::stream::AlsaStream; use crate::backends::alsa::AlsaError; use crate::{ - AudioCallback, AudioDevice, AudioInputDevice, AudioOutputCallback, AudioOutputDevice, Channel, - DeviceType, StreamConfig, + AudioCallback, AudioDevice, Channel, DeviceType, SendEverywhereButOnWeb, StreamConfig, }; -use alsa::{pcm, PCM}; +use alsa::{pcm, Direction, PCM}; use std::borrow::Cow; use std::fmt; use std::rc::Rc; @@ -27,6 +26,7 @@ impl fmt::Debug for AlsaDevice { } impl AudioDevice for AlsaDevice { + type StreamHandle = AlsaStream; type Error = AlsaError; fn name(&self) -> Cow<'_, str> { @@ -44,6 +44,36 @@ impl AudioDevice for AlsaDevice { [] } + fn default_config(&self) -> Result { + let params = self.pcm.hw_params_current()?; + let min = params + .get_buffer_size_min() + .inspect_err(|err| log::warn!("Cannot get buffer size: {err}")) + .ok() + .map(|x| x as usize); + let max = params + .get_buffer_size_max() + .inspect_err(|err| log::warn!("Cannot get buffer size: {err}")) + .ok() + .map(|x| x as usize); + + let channels = params.get_channels()? as usize; + let (input_channels, output_channels) = + if matches!(self.direction, alsa::Direction::Capture) { + (channels, 0) + } else { + (0, channels) + }; + + Ok(StreamConfig { + samplerate: params.get_rate()? as _, + buffer_size_range: (min, max), + exclusive: false, + input_channels, + output_channels, + }) + } + fn is_config_supported(&self, config: &StreamConfig) -> bool { self.get_hwp(config) .inspect_err(|err| { @@ -57,49 +87,25 @@ impl AudioDevice for AlsaDevice { log::info!("TODO: enumerate configurations"); None::<[StreamConfig; 0]> } -} - -impl AudioInputDevice for AlsaDevice { - type StreamHandle = AlsaStream; - - fn default_input_config(&self) -> Result { - self.default_config() - } - fn create_input_stream( + fn create_stream( &self, stream_config: StreamConfig, callback: Callback, ) -> Result, Self::Error> { - AlsaStream::new_input(self.name.clone(), stream_config, callback) - } -} - -impl AudioOutputDevice for AlsaDevice { - type StreamHandle = AlsaStream; - - fn default_output_config(&self) -> Result { - self.default_config() - } - - fn create_output_stream( - &self, - stream_config: StreamConfig, - callback: Callback, - ) -> Result, Self::Error> { - AlsaStream::new_output(self.name.clone(), stream_config, callback) + match self.direction { + Direction::Playback => { + AlsaStream::new_output(self.name.clone(), stream_config, callback) + } + Direction::Capture => AlsaStream::new_input(self.name.clone(), stream_config, callback), + } } } impl AlsaDevice { /// Shortcut constructor for getting ALSA devices directly. - pub fn default_device(direction: alsa::Direction) -> Result, alsa::Error> { - let pcm = Rc::new(PCM::new("default", direction, true)?); - Ok(Some(Self { - pcm, - direction, - name: "default".to_string(), - })) + pub fn default_device(direction: alsa::Direction) -> Result { + Self::new("default", direction) } pub(super) fn new(name: &str, direction: alsa::Direction) -> Result { @@ -145,16 +151,4 @@ impl AlsaDevice { Ok((hwp, swp, io)) } - - fn default_config(&self) -> Result { - let samplerate = 48e3; // Default ALSA sample rate - let channel_count = 2; // Stereo stream - let channels = (1 << channel_count) - 1; - Ok(StreamConfig { - samplerate: samplerate as _, - output_channels: channels, - buffer_size_range: (None, None), - exclusive: false, - }) - } } diff --git a/src/backends/alsa/input.rs b/src/backends/alsa/input.rs index 6d90f22..2a91fc8 100644 --- a/src/backends/alsa/input.rs +++ b/src/backends/alsa/input.rs @@ -2,7 +2,8 @@ use crate::audio_buffer::AudioRef; use crate::backends::alsa::stream::AlsaStream; use crate::backends::alsa::AlsaError; use crate::prelude::alsa::device::AlsaDevice; -use crate::{AudioCallback, AudioCallbackContext, AudioInput, StreamConfig}; +use crate::prelude::{AudioMut, Timestamp}; +use crate::{AudioCallback, AudioCallbackContext, AudioInput, AudioOutput, StreamConfig}; impl AlsaStream { pub(super) fn new_input( @@ -27,7 +28,11 @@ impl AlsaStream { buffer, timestamp: *ctx.timestamp, }; - ctx.callback.on_input_data(context, input); + let dummy_output = AudioOutput { + timestamp: Timestamp::new(ctx.config.sample_rate), + buffer: AudioMut::empty(), + }; + ctx.callback.process_audio(context, input, dummy_output); *ctx.timestamp += ctx.num_frames as u64; Ok(()) }, diff --git a/src/backends/alsa/mod.rs b/src/backends/alsa/mod.rs index 4a6e952..2efa805 100644 --- a/src/backends/alsa/mod.rs +++ b/src/backends/alsa/mod.rs @@ -50,7 +50,7 @@ impl AudioDriver for AlsaDriver { _ if device_type.is_output() => alsa::Direction::Playback, _ => return Ok(None), }; - Ok(AlsaDevice::default_device(direction)?) + Ok(Some(AlsaDevice::default_device(direction)?)) } fn list_devices(&self) -> Result, Self::Error> { diff --git a/src/backends/alsa/output.rs b/src/backends/alsa/output.rs index 029f95e..aa802a7 100644 --- a/src/backends/alsa/output.rs +++ b/src/backends/alsa/output.rs @@ -2,9 +2,10 @@ use crate::audio_buffer::AudioMut; use crate::backends::alsa::stream::AlsaStream; use crate::backends::alsa::AlsaError; use crate::prelude::alsa::device::AlsaDevice; -use crate::{AudioCallbackContext, AudioOutput, AudioOutputCallback, StreamConfig}; +use crate::prelude::{AudioRef, Timestamp}; +use crate::{AudioCallback, AudioCallbackContext, AudioInput, AudioOutput, StreamConfig}; -impl AlsaStream { +impl AlsaStream { pub(super) fn new_output( name: String, stream_config: StreamConfig, @@ -16,15 +17,19 @@ impl AlsaStream { callback, move |ctx, recover| { let context = AudioCallbackContext { - stream_config, + stream_config: *ctx.config, timestamp: *ctx.timestamp, }; - let input = AudioOutput { + let dummy_input = AudioInput { + timestamp: Timestamp::new(ctx.config.sample_rate), + buffer: AudioRef::empty(), + }; + let output = AudioOutput { buffer: AudioMut::from_interleaved_mut(&mut ctx.buffer[..], ctx.num_channels) .unwrap(), timestamp: *ctx.timestamp, }; - ctx.callback.on_output_data(context, input); + ctx.callback.process_audio(context, dummy_input, output); *ctx.timestamp += ctx.num_frames as u64; if let Err(err) = ctx.io.writei(&ctx.buffer[..]) { recover(err)?; diff --git a/src/backends/alsa/stream.rs b/src/backends/alsa/stream.rs index fedec01..e284e16 100644 --- a/src/backends/alsa/stream.rs +++ b/src/backends/alsa/stream.rs @@ -2,9 +2,11 @@ use crate::backends::alsa::device::AlsaDevice; use crate::backends::alsa::{triggerfd, AlsaError}; use crate::channel_map::{Bitset, ChannelMap32}; use crate::timestamp::Timestamp; -use crate::{AudioStreamHandle, StreamConfig}; -use alsa::pcm; +use crate::{ + AudioCallback, AudioCallbackContext, AudioStreamHandle, ResolvedStreamConfig, StreamConfig, +}; use alsa::PollDescriptors; +use alsa::{pcm, Direction}; use std::sync::Arc; use std::thread::JoinHandle; use std::time::Duration; @@ -29,7 +31,7 @@ impl AudioStreamHandle for AlsaStream { } } -impl AlsaStream { +impl AlsaStream { pub(super) fn new_generic( stream_config: StreamConfig, device: impl 'static + Send + FnOnce() -> Result, @@ -64,24 +66,31 @@ impl AlsaStream { let period_size = period_size as usize; log::info!("Period size : {period_size}"); let num_channels = hwp.get_channels()? as usize; + let (input_channels, output_channels) = match device.direction { + Direction::Playback => (0, num_channels), + Direction::Capture => (num_channels, 0), + }; log::info!("Num channels: {num_channels}"); - let samplerate = hwp.get_rate()? as f64; - log::info!("Sample rate : {samplerate}"); - let stream_config = StreamConfig { - samplerate, - output_channels: ChannelMap32::default() - .with_indices(std::iter::repeat(1).take(num_channels)), - buffer_size_range: (Some(period_size), Some(period_size)), - exclusive: false, + let sample_rate = hwp.get_rate()? as f64; + log::info!("Sample rate : {sample_rate}"); + let stream_config = ResolvedStreamConfig { + sample_rate, + input_channels, + output_channels, + max_frame_count: period_size, }; - let mut timestamp = Timestamp::new(samplerate); + let mut timestamp = Timestamp::new(sample_rate); let mut buffer = vec![0f32; period_size * num_channels]; - let latency = period_size as f64 / samplerate; + let latency = period_size as f64 / sample_rate; device.pcm.prepare()?; if device.pcm.state() != pcm::State::Running { log::info!("Device not already started, starting now"); device.pcm.start()?; } + callback.prepare(AudioCallbackContext { + stream_config, + timestamp: Timestamp::new(sample_rate), + }); let _try = || loop { let frames = device.pcm.avail_update()? as usize; if frames == 0 { @@ -133,7 +142,7 @@ impl AlsaStream { } pub(super) struct StreamContext<'a, Callback: 'a> { - pub(super) config: &'a StreamConfig, + pub(super) config: &'a ResolvedStreamConfig, pub(super) timestamp: &'a mut Timestamp, pub(super) io: &'a pcm::IO<'a, f32>, pub(super) num_channels: usize, From 836bf79440941f6f57e442733ba0cbb9dfe157a3 Mon Sep 17 00:00:00 2001 From: Nathan Graule Date: Tue, 27 May 2025 23:56:43 +0200 Subject: [PATCH 04/38] fix: alsa and pipewire backends work --- .idea/deployment.xml | 21 +++++++++++ src/backends/alsa/device.rs | 12 +++---- src/backends/alsa/stream.rs | 1 - src/backends/pipewire/device.rs | 63 ++++++++++----------------------- src/backends/pipewire/driver.rs | 1 + src/backends/pipewire/error.rs | 2 ++ src/backends/pipewire/stream.rs | 59 ++++++++++++++++++++---------- src/lib.rs | 2 +- 8 files changed, 89 insertions(+), 72 deletions(-) create mode 100644 .idea/deployment.xml diff --git a/.idea/deployment.xml b/.idea/deployment.xml new file mode 100644 index 0000000..fa53927 --- /dev/null +++ b/.idea/deployment.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/backends/alsa/device.rs b/src/backends/alsa/device.rs index bc8d815..aeaea02 100644 --- a/src/backends/alsa/device.rs +++ b/src/backends/alsa/device.rs @@ -48,14 +48,14 @@ impl AudioDevice for AlsaDevice { let params = self.pcm.hw_params_current()?; let min = params .get_buffer_size_min() + .map(|x| x as usize) .inspect_err(|err| log::warn!("Cannot get buffer size: {err}")) - .ok() - .map(|x| x as usize); + .ok(); let max = params .get_buffer_size_max() + .map(|x| x as usize) .inspect_err(|err| log::warn!("Cannot get buffer size: {err}")) - .ok() - .map(|x| x as usize); + .ok(); let channels = params.get_channels()? as usize; let (input_channels, output_channels) = @@ -66,7 +66,7 @@ impl AudioDevice for AlsaDevice { }; Ok(StreamConfig { - samplerate: params.get_rate()? as _, + sample_rate: params.get_rate()? as _, buffer_size_range: (min, max), exclusive: false, input_channels, @@ -121,7 +121,7 @@ impl AlsaDevice { fn get_hwp(&self, config: &StreamConfig) -> Result, alsa::Error> { let hwp = pcm::HwParams::any(&self.pcm)?; hwp.set_channels(config.output_channels as _)?; - hwp.set_rate(config.samplerate as _, alsa::ValueOr::Nearest)?; + hwp.set_rate(config.sample_rate as _, alsa::ValueOr::Nearest)?; if let Some(min) = config.buffer_size_range.0 { hwp.set_buffer_size_min(min as _)?; } diff --git a/src/backends/alsa/stream.rs b/src/backends/alsa/stream.rs index e284e16..24ed8ee 100644 --- a/src/backends/alsa/stream.rs +++ b/src/backends/alsa/stream.rs @@ -1,6 +1,5 @@ use crate::backends::alsa::device::AlsaDevice; use crate::backends::alsa::{triggerfd, AlsaError}; -use crate::channel_map::{Bitset, ChannelMap32}; use crate::timestamp::Timestamp; use crate::{ AudioCallback, AudioCallbackContext, AudioStreamHandle, ResolvedStreamConfig, StreamConfig, diff --git a/src/backends/pipewire/device.rs b/src/backends/pipewire/device.rs index 4e2a659..b4fae1b 100644 --- a/src/backends/pipewire/device.rs +++ b/src/backends/pipewire/device.rs @@ -1,8 +1,7 @@ use super::stream::StreamHandle; use crate::backends::pipewire::error::PipewireError; use crate::{ - AudioCallback, AudioDevice, AudioInputDevice, AudioOutputCallback, AudioOutputDevice, Channel, - DeviceType, SendEverywhereButOnWeb, StreamConfig, + AudioCallback, AudioDevice, Channel, DeviceType, SendEverywhereButOnWeb, StreamConfig, }; use pipewire::context::Context; use pipewire::main_loop::MainLoop; @@ -31,6 +30,8 @@ impl PipewireDevice { } impl AudioDevice for PipewireDevice { + type StreamHandle = StreamHandle; + type Error = PipewireError; fn name(&self) -> Cow<'_, str> { @@ -65,61 +66,33 @@ impl AudioDevice for PipewireDevice { } fn enumerate_configurations(&self) -> Option> { - Some([]) + None::<[StreamConfig; 0]> } -} - -impl AudioInputDevice for PipewireDevice { - type StreamHandle = StreamHandle; - fn default_input_config(&self) -> Result { + fn default_config(&self) -> Result { + let input_channels = if self.device_type.is_input() { 2 } else { 0 }; + let output_channels = if self.device_type.is_output() { 2 } else { 0 }; Ok(StreamConfig { - samplerate: 48000.0, - output_channels: 0b11, - exclusive: false, + sample_rate: 48000.0, + input_channels, + output_channels, buffer_size_range: (None, None), - }) - } - - fn create_input_stream( - &self, - stream_config: StreamConfig, - callback: Callback, - ) -> Result, Self::Error> { - StreamHandle::new_input( - self.object_serial.clone(), - &self.stream_name, - stream_config, - self.stream_properties.clone(), - callback, - ) - } -} - -impl AudioOutputDevice for PipewireDevice { - type StreamHandle = StreamHandle; - - fn default_output_config(&self) -> Result { - Ok(StreamConfig { - samplerate: 48000.0, - output_channels: 0b11, exclusive: false, - buffer_size_range: (None, None), }) } - fn create_output_stream( + fn create_stream( &self, stream_config: StreamConfig, callback: Callback, ) -> Result, Self::Error> { - StreamHandle::new_output( - self.object_serial.clone(), - &self.stream_name, - stream_config, - self.stream_properties.clone(), - callback, - ) + if self.device_type.is_input() && !self.device_type.is_output() { + StreamHandle::new_input(&self.stream_name, stream_config, callback) + } else if self.device_type.is_output() { + StreamHandle::new_output(&self.stream_name, stream_config, callback) + } else { + Err(PipewireError::InvalidDeviceType) + } } } diff --git a/src/backends/pipewire/driver.rs b/src/backends/pipewire/driver.rs index a980420..fc05421 100644 --- a/src/backends/pipewire/driver.rs +++ b/src/backends/pipewire/driver.rs @@ -7,6 +7,7 @@ use std::collections::HashMap; use std::marker::PhantomData; pub struct PipewireDriver { + // Needed to make this type unable to be constructed directly __init: PhantomData<()>, } diff --git a/src/backends/pipewire/error.rs b/src/backends/pipewire/error.rs index 174ab04..2c061d6 100644 --- a/src/backends/pipewire/error.rs +++ b/src/backends/pipewire/error.rs @@ -6,4 +6,6 @@ pub enum PipewireError { BackendError(#[from] pipewire::Error), #[error("Cannot create Pipewire stream: {0}")] GenError(#[from] libspa::pod::serialize::GenError), + #[error("Device has invalid type")] + InvalidDeviceType, } diff --git a/src/backends/pipewire/stream.rs b/src/backends/pipewire/stream.rs index c39f2fb..98290fc 100644 --- a/src/backends/pipewire/stream.rs +++ b/src/backends/pipewire/stream.rs @@ -1,10 +1,12 @@ -use crate::audio_buffer::{AudioMut, AudioRef}; use crate::backends::pipewire::error::PipewireError; use crate::channel_map::Bitset; use crate::timestamp::Timestamp; use crate::{ - AudioCallback, AudioCallbackContext, AudioInput, AudioOutput, AudioOutputCallback, - AudioStreamHandle, StreamConfig, + audio_buffer::{AudioMut, AudioRef}, + ResolvedStreamConfig, +}; +use crate::{ + AudioCallback, AudioCallbackContext, AudioInput, AudioOutput, AudioStreamHandle, StreamConfig, }; use libspa::buffer::Data; use libspa::param::audio::{AudioFormat, AudioInfoRaw}; @@ -39,17 +41,21 @@ struct StreamInner { commands: rtrb::Consumer>, scratch_buffer: Box<[f32]>, callback: Option, - config: StreamConfig, + config: ResolvedStreamConfig, timestamp: Timestamp, loop_ref: WeakMainLoop, } -impl StreamInner { +impl StreamInner { fn handle_command(&mut self, command: StreamCommands) { log::debug!("Handling command: {command:?}"); match command { - StreamCommands::ReceiveCallback(callback) => { + StreamCommands::ReceiveCallback(mut callback) => { debug_assert!(self.callback.is_none()); + callback.prepare(AudioCallbackContext { + stream_config: self.config, + timestamp: self.timestamp, + }); self.callback = Some(callback); } StreamCommands::Eject(reply) => { @@ -74,10 +80,10 @@ impl StreamInner { } } -impl StreamInner { - fn process_output(&mut self, channels: usize, buffer_size: usize) -> usize { - let buffer = AudioMut::from_interleaved_mut( - &mut self.scratch_buffer[..channels * buffer_size], +impl StreamInner { + fn process_output(&mut self, channels: usize, frames: usize) -> usize { + let buffer = AudioMut::from_noninterleaved_mut( + &mut self.scratch_buffer[..channels * frames], channels, ) .unwrap(); @@ -87,11 +93,15 @@ impl StreamInner { timestamp: self.timestamp, }; let num_frames = buffer.num_frames(); + let dummy_input = AudioInput { + timestamp: Timestamp::new(self.config.sample_rate), + buffer: AudioRef::empty(), + }; let output = AudioOutput { buffer, timestamp: self.timestamp, }; - callback.on_output_data(context, output); + callback.process_audio(context, dummy_input, output); self.timestamp += num_frames as u64; num_frames } else { @@ -115,7 +125,8 @@ impl StreamInner { buffer, timestamp: self.timestamp, }; - callback.process_audio(context, input); + let dummy_output = AudioOutput { timestamp: Timestamp::new(self.config.sample_rate), buffer: AudioMut::empty() }; + callback.process_audio(context, input, dummy_output); self.timestamp += num_frames as u64; num_frames } else { @@ -143,12 +154,11 @@ impl AudioStreamHandle for StreamHandle { } } -impl StreamHandle { +impl StreamHandle { fn create_stream( device_object_serial: Option, name: String, - mut config: StreamConfig, - user_properties: HashMap, Vec>, + config: StreamConfig, callback: Callback, direction: pipewire::spa::utils::Direction, process_frames: impl Fn(&mut [Data], &mut StreamInner, usize) -> usize @@ -161,7 +171,7 @@ impl StreamHandle { let context = Context::new(&main_loop)?; let core = context.connect(None)?; - let channels = config.output_channels.count(); + let channels = config.output_channels; let channels_str = channels.to_string(); let buffer_size = stream_buffer_size(config.buffer_size_range); @@ -170,6 +180,17 @@ impl StreamHandle { properties.insert(key, value); } + let input_channels = if direction == pipewire::spa::utils::Direction::Input { + channels + } else { + 0 + }; + let output_channels = if direction == pipewire::spa::utils::Direction::Output { + channels + } else { + 0 + }; + properties.insert(*keys::MEDIA_TYPE, "Audio"); properties.insert(*keys::MEDIA_ROLE, "Music"); properties.insert(*keys::MEDIA_CATEGORY, get_category(direction)); @@ -189,7 +210,7 @@ impl StreamHandle { scratch_buffer: vec![0.0; MAX_FRAMES * channels].into_boxed_slice(), loop_ref: main_loop.downgrade(), config, - timestamp: Timestamp::new(config.samplerate), + timestamp: Timestamp::new(config.sample_rate), }) .process(move |stream, inner| { log::debug!("Processing stream"); @@ -220,7 +241,7 @@ impl StreamHandle { properties: { let mut info = AudioInfoRaw::new(); info.set_format(AudioFormat::F32LE); - info.set_rate(config.samplerate as u32); + info.set_rate(config.sample_rate as u32); info.set_channels(channels as u32); info.into() }, @@ -298,7 +319,7 @@ fn get_category(direction: pipewire::spa::utils::Direction) -> &'static str { } } -impl StreamHandle { +impl StreamHandle { /// Create an output Pipewire stream pub fn new_output( device_object_serial: Option, diff --git a/src/lib.rs b/src/lib.rs index c4c1e89..36e6032 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -100,7 +100,7 @@ impl DeviceType { pub struct StreamConfig { /// Configured sample rate of the requested stream. The opened stream can have a different /// sample rate, so don't rely on this parameter being correct at runtime. - pub samplerate: f64, + pub sample_rate: f64, /// Number of input channels requested pub input_channels: usize, /// Number of output channels requested From 2c17962cc2dad11a0698f71fb8251cee658153a8 Mon Sep 17 00:00:00 2001 From: Nathan Graule Date: Mon, 2 Jun 2025 19:58:39 +0000 Subject: [PATCH 05/38] wip: update ALSA and Pipewire backends --- .vscode/settings.json | 5 + examples/duplex.rs | 3 +- src/backends/alsa/device.rs | 137 +++++++++++-- src/backends/alsa/input.rs | 41 ---- src/backends/alsa/mod.rs | 4 +- src/backends/alsa/output.rs | 41 ---- src/backends/alsa/stream.rs | 340 +++++++++++++++++++++----------- src/backends/pipewire/filter.rs | 24 +++ src/backends/pipewire/mod.rs | 1 + src/backends/pipewire/stream.rs | 86 +++++--- src/duplex.rs | 2 + 11 files changed, 441 insertions(+), 243 deletions(-) create mode 100644 .vscode/settings.json delete mode 100644 src/backends/alsa/input.rs delete mode 100644 src/backends/alsa/output.rs create mode 100644 src/backends/pipewire/filter.rs diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..64d456c --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "rust-analyzer.cargo.features": [ + "pipewire" + ] +} \ No newline at end of file diff --git a/examples/duplex.rs b/examples/duplex.rs index 9c080d3..adc8429 100644 --- a/examples/duplex.rs +++ b/examples/duplex.rs @@ -13,7 +13,8 @@ fn main() -> Result<()> { log::info!("Opening output: {}", output.name()); let config = StreamConfig { buffer_size_range: (Some(128), Some(512)), - input_channels: 1, + input_channels: 2, + output_channels: 2, ..output.default_config().unwrap() }; let duplex_config = DuplexStreamConfig::new(config); diff --git a/src/backends/alsa/device.rs b/src/backends/alsa/device.rs index aeaea02..7ea3c29 100644 --- a/src/backends/alsa/device.rs +++ b/src/backends/alsa/device.rs @@ -7,6 +7,7 @@ use alsa::{pcm, Direction, PCM}; use std::borrow::Cow; use std::fmt; use std::rc::Rc; +use std::sync::Arc; /// Type of ALSA devices. #[derive(Clone)] @@ -93,12 +94,26 @@ impl AudioDevice for AlsaDevice { stream_config: StreamConfig, callback: Callback, ) -> Result, Self::Error> { - match self.direction { - Direction::Playback => { - AlsaStream::new_output(self.name.clone(), stream_config, callback) + let name = Arc::::from(self.name.to_owned()); + let direction = self.direction; + let input_device = { + let name = name.clone(); + move || match direction { + alsa::Direction::Capture => Ok(Some(AlsaDevice::new( + &name.clone(), + alsa::Direction::Capture, + )?)), + alsa::Direction::Playback => Ok(None), } - Direction::Capture => AlsaStream::new_input(self.name.clone(), stream_config, callback), - } + }; + let output_device = move || match direction { + alsa::Direction::Capture => Ok(None), + alsa::Direction::Playback => Ok(Some(AlsaDevice::new( + &name.clone(), + alsa::Direction::Playback, + )?)), + }; + AlsaStream::new(input_device, output_device, stream_config, callback) } } @@ -118,21 +133,6 @@ impl AlsaDevice { }) } - fn get_hwp(&self, config: &StreamConfig) -> Result, alsa::Error> { - let hwp = pcm::HwParams::any(&self.pcm)?; - hwp.set_channels(config.output_channels as _)?; - hwp.set_rate(config.sample_rate as _, alsa::ValueOr::Nearest)?; - if let Some(min) = config.buffer_size_range.0 { - hwp.set_buffer_size_min(min as _)?; - } - if let Some(max) = config.buffer_size_range.1 { - hwp.set_buffer_size_max(max as _)?; - } - hwp.set_format(pcm::Format::float())?; - hwp.set_access(pcm::Access::RWInterleaved)?; - Ok(hwp) - } - pub(super) fn apply_config( &self, config: &StreamConfig, @@ -145,10 +145,107 @@ impl AlsaDevice { log::debug!("Apply config: hwp {hwp:#?}"); + if matches!(self.direction, alsa::Direction::Playback) { + hwp.set_channels(config.output_channels as _)?; + } else { + hwp.set_channels(config.input_channels as _)?; + } + swp.set_start_threshold(hwp.get_buffer_size()?)?; self.pcm.sw_params(&swp)?; log::debug!("Apply config: swp {swp:#?}"); Ok((hwp, swp, io)) } + + fn get_hwp(&self, config: &StreamConfig) -> Result { + let hwp = pcm::HwParams::any(&self.pcm)?; + hwp.set_channels(config.output_channels as _)?; + hwp.set_rate(config.sample_rate as _, alsa::ValueOr::Nearest)?; + if let Some(min) = config.buffer_size_range.0 { + hwp.set_buffer_size_min(min as _)?; + } + if let Some(max) = config.buffer_size_range.1 { + hwp.set_buffer_size_max(max as _)?; + } + hwp.set_format(pcm::Format::float())?; + hwp.set_access(pcm::Access::RWInterleaved)?; + Ok(hwp) + } +} + +#[derive(Debug, Clone)] +pub struct AlsaDuplex { + pub capture: AlsaDevice, + pub playback: AlsaDevice, +} + +impl AudioDevice for AlsaDuplex { + type StreamHandle = AlsaStream; + + type Error = AlsaError; + + fn name(&self) -> Cow { + Cow::Owned(format!("{} / {}", &self.capture.name, &self.playback.name)) + } + + fn device_type(&self) -> DeviceType { + DeviceType::PHYSICAL | DeviceType::DUPLEX + } + + fn channel_map(&self) -> impl IntoIterator { + [] + } + + fn default_config(&self) -> Result { + let hwp_inp = self.capture.pcm.hw_params_current()?; + let hwp_out = self.playback.pcm.hw_params_current()?; + let sample_rate = hwp_out.get_rate()? as f64; + let input_channels = hwp_inp.get_channels()? as usize; + let output_channels = hwp_out.get_channels()? as usize; + let min_size = { + let inp_min = hwp_inp.get_buffer_size_min().unwrap_or(0); + let out_min = hwp_out.get_buffer_size_min().unwrap_or(0); + inp_min.max(out_min) + }; + let max_size = { + let inp_max = hwp_inp.get_buffer_size_max().unwrap_or(0); + let out_max = hwp_out.get_buffer_size_max().unwrap_or(0); + inp_max.min(out_max) + }; + Ok(StreamConfig { + sample_rate, + input_channels, + output_channels, + buffer_size_range: ( + (min_size == 0).then_some(min_size as _), + (max_size == 0).then_some(max_size as _), + ), + exclusive: false, + }) + } + + fn is_config_supported(&self, config: &StreamConfig) -> bool { + self.capture.is_config_supported(config) && self.playback.is_config_supported(config) + } + + fn enumerate_configurations(&self) -> Option> { + None::<[StreamConfig; 0]> + } + + fn create_stream( + &self, + stream_config: StreamConfig, + callback: Callback, + ) -> Result, Self::Error> { + let input_device = { + let name = self.capture.name.to_owned(); + move || AlsaDevice::new(&name, alsa::Direction::Capture).map(Some) + }; + let output_device = { + let name = self.playback.name.to_owned(); + move || AlsaDevice::new(&name, alsa::Direction::Playback).map(Some) + }; + AlsaStream::new(input_device, output_device, stream_config, callback) + } } diff --git a/src/backends/alsa/input.rs b/src/backends/alsa/input.rs deleted file mode 100644 index 2a91fc8..0000000 --- a/src/backends/alsa/input.rs +++ /dev/null @@ -1,41 +0,0 @@ -use crate::audio_buffer::AudioRef; -use crate::backends::alsa::stream::AlsaStream; -use crate::backends::alsa::AlsaError; -use crate::prelude::alsa::device::AlsaDevice; -use crate::prelude::{AudioMut, Timestamp}; -use crate::{AudioCallback, AudioCallbackContext, AudioInput, AudioOutput, StreamConfig}; - -impl AlsaStream { - pub(super) fn new_input( - name: String, - stream_config: StreamConfig, - callback: Callback, - ) -> Result { - Self::new_generic( - stream_config, - move || AlsaDevice::new(&name, alsa::Direction::Capture), - callback, - move |ctx, recover| { - if let Err(err) = ctx.io.readi(&mut ctx.buffer[..]) { - recover(err)?; - } - let buffer = AudioRef::from_interleaved(ctx.buffer, ctx.num_channels).unwrap(); - let context = AudioCallbackContext { - stream_config: *ctx.config, - timestamp: *ctx.timestamp, - }; - let input = AudioInput { - buffer, - timestamp: *ctx.timestamp, - }; - let dummy_output = AudioOutput { - timestamp: Timestamp::new(ctx.config.sample_rate), - buffer: AudioMut::empty(), - }; - ctx.callback.process_audio(context, input, dummy_output); - *ctx.timestamp += ctx.num_frames as u64; - Ok(()) - }, - ) - } -} diff --git a/src/backends/alsa/mod.rs b/src/backends/alsa/mod.rs index 2efa805..74f5b7f 100644 --- a/src/backends/alsa/mod.rs +++ b/src/backends/alsa/mod.rs @@ -12,8 +12,6 @@ use std::borrow::Cow; use thiserror::Error; mod device; -mod input; -mod output; mod stream; mod triggerfd; @@ -27,6 +25,8 @@ pub enum AlsaError { /// Error originates from I/O operations. #[error("I/O error: {0}")] IoError(#[from] nix::Error), + #[error("No channels have been opened")] + NoChannelsOpened, } /// ALSA driver type. ALSA is statically available without client configuration, therefore this type diff --git a/src/backends/alsa/output.rs b/src/backends/alsa/output.rs deleted file mode 100644 index aa802a7..0000000 --- a/src/backends/alsa/output.rs +++ /dev/null @@ -1,41 +0,0 @@ -use crate::audio_buffer::AudioMut; -use crate::backends::alsa::stream::AlsaStream; -use crate::backends::alsa::AlsaError; -use crate::prelude::alsa::device::AlsaDevice; -use crate::prelude::{AudioRef, Timestamp}; -use crate::{AudioCallback, AudioCallbackContext, AudioInput, AudioOutput, StreamConfig}; - -impl AlsaStream { - pub(super) fn new_output( - name: String, - stream_config: StreamConfig, - callback: Callback, - ) -> Result { - Self::new_generic( - stream_config, - move || AlsaDevice::new(&name, alsa::Direction::Playback), - callback, - move |ctx, recover| { - let context = AudioCallbackContext { - stream_config: *ctx.config, - timestamp: *ctx.timestamp, - }; - let dummy_input = AudioInput { - timestamp: Timestamp::new(ctx.config.sample_rate), - buffer: AudioRef::empty(), - }; - let output = AudioOutput { - buffer: AudioMut::from_interleaved_mut(&mut ctx.buffer[..], ctx.num_channels) - .unwrap(), - timestamp: *ctx.timestamp, - }; - ctx.callback.process_audio(context, dummy_input, output); - *ctx.timestamp += ctx.num_frames as u64; - if let Err(err) = ctx.io.writei(&ctx.buffer[..]) { - recover(err)?; - } - Ok(()) - }, - ) - } -} diff --git a/src/backends/alsa/stream.rs b/src/backends/alsa/stream.rs index 24ed8ee..a827a6f 100644 --- a/src/backends/alsa/stream.rs +++ b/src/backends/alsa/stream.rs @@ -1,14 +1,227 @@ +use crate::audio_buffer::{AudioMut, AudioRef}; use crate::backends::alsa::device::AlsaDevice; use crate::backends::alsa::{triggerfd, AlsaError}; use crate::timestamp::Timestamp; use crate::{ - AudioCallback, AudioCallbackContext, AudioStreamHandle, ResolvedStreamConfig, StreamConfig, + AudioCallback, AudioCallbackContext, AudioInput, AudioOutput, AudioStreamHandle, + ResolvedStreamConfig, StreamConfig, }; +use alsa::pcm; use alsa::PollDescriptors; -use alsa::{pcm, Direction}; +use std::rc::{Rc, Weak}; use std::sync::Arc; use std::thread::JoinHandle; -use std::time::Duration; + +struct AlsaStreamData<'io> { + pcm: Weak, + io: Option>, + buffer: Box<[f32]>, + timestamp: Timestamp, + sample_rate: f64, + channels: usize, + max_frame_count: usize, + latency: f64, +} + +impl<'io> AlsaStreamData<'io> { + fn new(device: &'io AlsaDevice, config: StreamConfig) -> Result { + let (hwp, _, io) = device.apply_config(&config)?; + let (_, period_size) = device.pcm.get_params()?; + let period_size = period_size as usize; + log::info!("[{}] Period size : {period_size}", &device.name); + let channels = hwp.get_channels()? as usize; + log::info!("[{}] channels: {channels}", &device.name); + let sample_rate = hwp.get_rate()? as f64; + log::info!("[{}] Sample rate : {sample_rate}", &device.name); + + let buffer = std::iter::repeat_n(0.0, period_size * channels).collect(); + let timestamp = Timestamp::new(sample_rate); + let latency = period_size as f64 / sample_rate; + + Ok(Self { + pcm: Rc::downgrade(&device.pcm), + io: Some(io), + buffer, + timestamp, + sample_rate, + channels, + max_frame_count: period_size, + latency, + }) + } + + fn new_empty() -> Self { + Self { + pcm: Weak::new(), + io: None, + buffer: Box::new([]), + timestamp: Timestamp::new(0.0), + sample_rate: 0.0, + channels: 0, + max_frame_count: 0, + latency: 0.0, + } + } + + fn descriptor_count(&self) -> usize { + let Some(pcm) = self.pcm.upgrade() else { + return 0; + }; + return pcm.count(); + } + + fn available(&self) -> Result { + let Some(pcm) = self.pcm.upgrade() else { + return Ok(0); + }; + Ok(pcm.avail_update()? as usize) + } + + fn read(&mut self, num_frames: usize) -> Result, AlsaError> { + let Some(io) = self.io.as_mut() else { + return Ok(self.get_buffer(0)); + }; + let len = num_frames * self.channels; + let buffer = &mut self.buffer[..len]; + if let Err(err) = io.readi(buffer) { + self.pcm.upgrade().unwrap().try_recover(err, true)?; + } + Ok(AudioRef::from_interleaved(buffer, self.channels).unwrap()) + } + + fn read_input(&mut self, num_frames: usize) -> Result, AlsaError> { + Ok(AudioInput { + timestamp: self.timestamp, + buffer: self.read(num_frames)?, + }) + } + + fn get_buffer(&self, num_frames: usize) -> AudioRef { + let len = num_frames * self.channels; + AudioRef::from_interleaved(&self.buffer[..len], self.channels).unwrap() + } + + fn get_buffer_mut(&mut self, num_frames: usize) -> AudioMut { + let len = num_frames * self.channels; + AudioMut::from_interleaved_mut(&mut self.buffer[..len], self.channels).unwrap() + } + + fn write(&mut self, num_frames: usize) -> Result<(), AlsaError> { + let Some(io) = self.io.as_mut() else { + return Ok(()); + }; + let len = num_frames * self.channels; + let scratch = &self.buffer[..len]; + if let Err(err) = io.writei(scratch) { + self.pcm.upgrade().unwrap().try_recover(err, true)?; + } + Ok(()) + } + + fn provide_output(&mut self, num_frames: usize) -> AudioOutput { + AudioOutput { + timestamp: self.timestamp, + buffer: self.get_buffer_mut(num_frames), + } + } + + fn tick_timestamp(&mut self, samples: u64) { + self.timestamp += samples; + } +} + +struct AlsaThread { + callback: Callback, + eject_trigger: triggerfd::Receiver, + stream_config: StreamConfig, + input_device: Option, + output_device: Option, +} + +impl AlsaThread { + fn new( + callback: Callback, + eject_trigger: triggerfd::Receiver, + stream_config: StreamConfig, + input_device: Option, + output_device: Option, + ) -> Self { + Self { + callback, + eject_trigger, + stream_config, + input_device, + output_device, + } + } + + fn thread_loop(mut self) -> Result { + let mut input_stream = self + .input_device + .as_ref() + .map(|d| AlsaStreamData::new(d, self.stream_config)) + .transpose()? + .unwrap_or_else(AlsaStreamData::new_empty); + let mut output_stream = self + .output_device + .as_ref() + .map(|d| AlsaStreamData::new(d, self.stream_config)) + .transpose()? + .unwrap_or_else(AlsaStreamData::new_empty); + + let stream_config = ResolvedStreamConfig { + sample_rate: output_stream.sample_rate, + input_channels: input_stream.channels, + output_channels: output_stream.channels, + max_frame_count: output_stream + .max_frame_count + .max(input_stream.max_frame_count), + }; + let mut poll_descriptors = { + let mut buf = vec![self.eject_trigger.as_pollfd()]; + let num_descriptors = + input_stream.descriptor_count() + output_stream.descriptor_count(); + buf.extend( + std::iter::repeat(libc::pollfd { + fd: 0, + events: 0, + revents: 0, + }) + .take(num_descriptors), + ); + buf + }; + self.callback.prepare(AudioCallbackContext { + stream_config, + timestamp: Timestamp::new(stream_config.sample_rate), + }); + loop { + let out_frames = output_stream.available()?; + let in_frames = input_stream.available()?; + + if out_frames == 0 && in_frames == 0 { + let latency = input_stream.latency.round() as i32; + if alsa::poll::poll(&mut poll_descriptors, latency)? > 0 { + log::debug!("Eject requested, returning ownership of callback"); + break Ok(self.callback); + } + continue; + } + + log::debug!("Frames available: out {out_frames}, in {in_frames}"); + let context = AudioCallbackContext { + timestamp: output_stream.timestamp, + stream_config, + }; + let input = input_stream.read_input(in_frames)?; + let output = output_stream.provide_output(out_frames); + self.callback.process_audio(context, input, output); + input_stream.tick_timestamp(in_frames as u64); + output_stream.tick_timestamp(out_frames as u64); + output_stream.write(out_frames)?; + } + } +} /// Type of ALSA streams. /// @@ -31,107 +244,22 @@ impl AudioStreamHandle for AlsaStream { } impl AlsaStream { - pub(super) fn new_generic( + pub(super) fn new( + input_device: impl 'static + Send + FnOnce() -> Result, alsa::Error>, + output_device: impl 'static + Send + FnOnce() -> Result, alsa::Error>, stream_config: StreamConfig, - device: impl 'static + Send + FnOnce() -> Result, - mut callback: Callback, - loop_callback: impl 'static - + Send - + Fn( - StreamContext, - &dyn Fn(alsa::Error) -> Result<(), alsa::Error>, - ) -> Result<(), alsa::Error>, + callback: Callback, ) -> Result { let (tx, rx) = triggerfd::trigger()?; - let join_handle = std::thread::spawn({ - move || { - let device = device()?; - let recover = |err| device.pcm.try_recover(err, true); - let mut poll_descriptors = { - let mut buf = vec![rx.as_pollfd()]; - let num_descriptors = device.pcm.count(); - buf.extend( - std::iter::repeat(libc::pollfd { - fd: 0, - events: 0, - revents: 0, - }) - .take(num_descriptors), - ); - buf - }; - let (hwp, _, io) = device.apply_config(&stream_config)?; - let (_, period_size) = device.pcm.get_params()?; - let period_size = period_size as usize; - log::info!("Period size : {period_size}"); - let num_channels = hwp.get_channels()? as usize; - let (input_channels, output_channels) = match device.direction { - Direction::Playback => (0, num_channels), - Direction::Capture => (num_channels, 0), - }; - log::info!("Num channels: {num_channels}"); - let sample_rate = hwp.get_rate()? as f64; - log::info!("Sample rate : {sample_rate}"); - let stream_config = ResolvedStreamConfig { - sample_rate, - input_channels, - output_channels, - max_frame_count: period_size, - }; - let mut timestamp = Timestamp::new(sample_rate); - let mut buffer = vec![0f32; period_size * num_channels]; - let latency = period_size as f64 / sample_rate; - device.pcm.prepare()?; - if device.pcm.state() != pcm::State::Running { - log::info!("Device not already started, starting now"); - device.pcm.start()?; - } - callback.prepare(AudioCallbackContext { - stream_config, - timestamp: Timestamp::new(sample_rate), - }); - let _try = || loop { - let frames = device.pcm.avail_update()? as usize; - if frames == 0 { - let latency = latency.round() as i32; - if alsa::poll::poll(&mut poll_descriptors, latency)? > 0 { - log::debug!("Eject requested, returning ownership of callback"); - break Ok(callback); - } - continue; - } - - log::debug!("Frames available: {frames}"); - let frames = std::cmp::min(frames, period_size); - let len = frames * num_channels; - - loop_callback( - StreamContext { - config: &stream_config, - timestamp: &mut timestamp, - io: &io, - num_channels, - num_frames: frames, - buffer: &mut buffer[..len], - callback: &mut callback, - }, - &recover, - )?; - - match device.pcm.state() { - pcm::State::Suspended => { - if hwp.can_resume() { - device.pcm.resume()?; - } else { - device.pcm.prepare()?; - } - } - pcm::State::Paused => std::thread::sleep(Duration::from_secs(1)), - _ => {} - } - }; - _try() - } + let join_handle = std::thread::spawn(move || { + let worker = AlsaThread::new( + callback, + rx, + stream_config, + input_device()?, + output_device()?, + ); + worker.thread_loop() }); Ok(Self { eject_trigger: Arc::new(tx), @@ -139,13 +267,3 @@ impl AlsaStream { }) } } - -pub(super) struct StreamContext<'a, Callback: 'a> { - pub(super) config: &'a ResolvedStreamConfig, - pub(super) timestamp: &'a mut Timestamp, - pub(super) io: &'a pcm::IO<'a, f32>, - pub(super) num_channels: usize, - pub(super) num_frames: usize, - pub(super) buffer: &'a mut [f32], - pub(super) callback: &'a mut Callback, -} diff --git a/src/backends/pipewire/filter.rs b/src/backends/pipewire/filter.rs new file mode 100644 index 0000000..a09e9de --- /dev/null +++ b/src/backends/pipewire/filter.rs @@ -0,0 +1,24 @@ +use std::ptr::NonNull; + +use pipewire::properties::Properties; +use pipewire::{core::Core, sys::pw_filter}; + +pub struct Filter { + data: NonNull, +} + +impl Filter { + pub fn new(core: &Core, name: impl AsRef<[u8]>, properties: Properties) -> Self { + let core = core.as_raw(); + let name = { + let s = CString::new(name.as_ref()); + }; + let filter = unsafe { pw_filter_new(core, )}; + } +} + + + + + + diff --git a/src/backends/pipewire/mod.rs b/src/backends/pipewire/mod.rs index f571420..4686839 100644 --- a/src/backends/pipewire/mod.rs +++ b/src/backends/pipewire/mod.rs @@ -3,3 +3,4 @@ pub mod driver; pub mod error; pub mod stream; mod utils; +mod filter; diff --git a/src/backends/pipewire/stream.rs b/src/backends/pipewire/stream.rs index 98290fc..b32320a 100644 --- a/src/backends/pipewire/stream.rs +++ b/src/backends/pipewire/stream.rs @@ -13,11 +13,11 @@ use libspa::param::audio::{AudioFormat, AudioInfoRaw}; use libspa::pod::Pod; use libspa::utils::Direction; use libspa_sys::{SPA_PARAM_EnumFormat, SPA_TYPE_OBJECT_Format}; -use pipewire::context::Context; use pipewire::keys; use pipewire::main_loop::{MainLoop, WeakMainLoop}; use pipewire::properties::Properties; use pipewire::stream::{Stream, StreamFlags}; +use pipewire::{context::Context, node::NodeListener}; use std::collections::HashMap; use std::fmt; use std::fmt::Formatter; @@ -52,6 +52,7 @@ impl StreamInner { match command { StreamCommands::ReceiveCallback(mut callback) => { debug_assert!(self.callback.is_none()); + log::debug!("StreamCommands::ReceiveCallback prepare {:#?}", self.config); callback.prepare(AudioCallbackContext { stream_config: self.config, timestamp: self.timestamp, @@ -87,26 +88,25 @@ impl StreamInner { channels, ) .unwrap(); - if let Some(callback) = self.callback.as_mut() { - let context = AudioCallbackContext { - stream_config: self.config, - timestamp: self.timestamp, - }; - let num_frames = buffer.num_frames(); - let dummy_input = AudioInput { - timestamp: Timestamp::new(self.config.sample_rate), - buffer: AudioRef::empty(), - }; - let output = AudioOutput { - buffer, - timestamp: self.timestamp, - }; - callback.process_audio(context, dummy_input, output); - self.timestamp += num_frames as u64; - num_frames - } else { - 0 - } + let Some(callback) = self.callback.as_mut() else { + return 0; + }; + let context = AudioCallbackContext { + stream_config: self.config, + timestamp: self.timestamp, + }; + let num_frames = buffer.num_frames(); + let dummy_input = AudioInput { + timestamp: Timestamp::new(self.config.sample_rate), + buffer: AudioRef::empty(), + }; + let output = AudioOutput { + buffer, + timestamp: self.timestamp, + }; + callback.process_audio(context, dummy_input, output); + self.timestamp += num_frames as u64; + num_frames } } @@ -125,7 +125,10 @@ impl StreamInner { buffer, timestamp: self.timestamp, }; - let dummy_output = AudioOutput { timestamp: Timestamp::new(self.config.sample_rate), buffer: AudioMut::empty() }; + let dummy_output = AudioOutput { + timestamp: Timestamp::new(self.config.sample_rate), + buffer: AudioMut::empty(), + }; callback.process_audio(context, input, dummy_output); self.timestamp += num_frames as u64; num_frames @@ -155,7 +158,19 @@ impl AudioStreamHandle for StreamHandle { } impl StreamHandle { - fn create_stream( + fn create_stream(name: String, serial: Option, config: StreamConfig, callback: Callback) -> Result { + let handle = std::thread::Builder::new() + .name(format!("{name} audio thread")) + .spawn(move || { + let main_loop = MainLoop::new(None)?; + let context = Context::new(&main_loop)?; + let core = context.connect(None)?; + Ok(todo!()) + }); + } + + fn create_stream_old( device_object_serial: Option, name: String, config: StreamConfig, @@ -171,7 +186,11 @@ impl StreamHandle { let context = Context::new(&main_loop)?; let core = context.connect(None)?; - let channels = config.output_channels; + let channels = if direction == pipewire::spa::utils::Direction::Input { + config.input_channels + } else { + config.output_channels + }; let channels_str = channels.to_string(); let buffer_size = stream_buffer_size(config.buffer_size_range); @@ -191,6 +210,13 @@ impl StreamHandle { 0 }; + let config = ResolvedStreamConfig { + sample_rate: config.sample_rate.round(), + input_channels, + output_channels, + max_frame_count, + }; + properties.insert(*keys::MEDIA_TYPE, "Audio"); properties.insert(*keys::MEDIA_ROLE, "Music"); properties.insert(*keys::MEDIA_CATEGORY, get_category(direction)); @@ -207,7 +233,13 @@ impl StreamHandle { .add_local_listener_with_user_data(StreamInner { callback: None, commands: rx, - scratch_buffer: vec![0.0; MAX_FRAMES * channels].into_boxed_slice(), + scratch_buffer: { + log::debug!( + "StreamInner: allocating {} frames", + max_frame_count * channels + ); + vec![0.0; max_frame_count * channels].into_boxed_slice() + }, loop_ref: main_loop.downgrade(), config, timestamp: Timestamp::new(config.sample_rate), @@ -278,7 +310,7 @@ impl StreamHandle { properties: HashMap, Vec>, callback: Callback, ) -> Result { - Self::create_stream( + Self::create_stream_old( device_object_serial, name.to_string(), config, @@ -328,7 +360,7 @@ impl StreamHandle { properties: HashMap, Vec>, callback: Callback, ) -> Result { - Self::create_stream( + Self::create_stream_old( device_object_serial, name.to_string(), config, diff --git a/src/duplex.rs b/src/duplex.rs index aad0cae..022a2fc 100644 --- a/src/duplex.rs +++ b/src/duplex.rs @@ -189,7 +189,9 @@ impl DuplexCallback { impl AudioCallback for DuplexCallback { fn prepare(&mut self, context: AudioCallbackContext) { + log::debug!("Prepare duplex callback {:#?}", context.stream_config); let len = context.stream_config.output_channels * context.stream_config.max_frame_count; + log::debug!("Create storage space for {len} elements"); self.storage_raw = Some(Box::from_iter(std::iter::repeat_n(0.0, len))); self.callback.prepare(context); } From e5cc1e250f80b115e51d236ef8e4651f245dcd10 Mon Sep 17 00:00:00 2001 From: Nathan Graule Date: Sat, 1 Nov 2025 19:39:51 +0100 Subject: [PATCH 06/38] feat(coreaudio): do input/output detection for devices --- src/backends/coreaudio.rs | 137 ++++++++++++++++++++++---------------- 1 file changed, 80 insertions(+), 57 deletions(-) diff --git a/src/backends/coreaudio.rs b/src/backends/coreaudio.rs index 6193d26..8ef637f 100644 --- a/src/backends/coreaudio.rs +++ b/src/backends/coreaudio.rs @@ -7,15 +7,13 @@ use std::convert::Infallible; use coreaudio::audio_unit::audio_format::LinearPcmFlags; use coreaudio::audio_unit::macos_helpers::{ - audio_unit_from_device_id, get_audio_device_ids_for_scope, get_default_device_id, - get_device_name, get_supported_physical_stream_formats, + audio_unit_from_device_id, get_audio_device_ids_for_scope, get_audio_device_supports_scope, + get_default_device_id, get_device_name, get_supported_physical_stream_formats, }; use coreaudio::audio_unit::render_callback::{data, Args}; -use coreaudio::audio_unit::{AudioUnit, Element, SampleFormat, Scope, StreamFormat}; +use coreaudio::audio_unit::{AudioUnit, Element, IOType, SampleFormat, Scope, StreamFormat}; use coreaudio_sys::{ - kAudioDevicePropertyBufferFrameSize, kAudioDevicePropertyBufferFrameSizeRange, - kAudioObjectPropertyElementMaster, kAudioObjectPropertyScopeInput, - kAudioObjectPropertyScopeOutput, kAudioUnitProperty_MaximumFramesPerSlice, + kAudioDevicePropertyBufferFrameSize, kAudioOutputUnitProperty_EnableIO, kAudioUnitProperty_SampleRate, kAudioUnitProperty_StreamFormat, AudioDeviceID, AudioObjectGetPropertyData, AudioObjectPropertyAddress, AudioValueRange, }; @@ -65,9 +63,12 @@ use crate::prelude::{AudioMut, AudioRef, ChannelMap32}; use crate::timestamp::Timestamp; use crate::{ AudioCallback, AudioCallbackContext, AudioDevice, AudioDriver, AudioInput, AudioOutput, - AudioStreamHandle, Channel, DeviceType, ResolvedStreamConfig, SendEverywhereButOnWeb, + AudioStreamHandle, Channel, DeviceType, ResolvedStreamConfig, StreamConfig, }; +use crate::duplex::InputProxy; + +type Result = std::result::Result; /// Type of errors from the CoreAudio backend #[derive(Debug, Error)] @@ -90,28 +91,25 @@ impl AudioDriver for CoreAudioDriver { type Device = CoreAudioDevice; const DISPLAY_NAME: &'static str = "CoreAudio"; - fn version(&self) -> Result, Self::Error> { + fn version(&self) -> Result> { Ok(Cow::Borrowed("unknown")) } - fn default_device(&self, device_type: DeviceType) -> Result, Self::Error> { + fn default_device(&self, device_type: DeviceType) -> Result> { let Some(device_id) = get_default_device_id(device_type.is_input()) else { return Ok(None); }; - Ok(Some(CoreAudioDevice::from_id( - device_id, - device_type.is_input(), - )?)) + Ok(Some(CoreAudioDevice::from_id(device_id)?)) } - fn list_devices(&self) -> Result, Self::Error> { + fn list_devices(&self) -> Result> { let per_scope = [Scope::Input, Scope::Output] .into_iter() .map(|scope| { let audio_ids = get_audio_device_ids_for_scope(scope)?; audio_ids .into_iter() - .map(|id| CoreAudioDevice::from_id(id, matches!(scope, Scope::Input))) + .map(|id| CoreAudioDevice::from_id(id)) .collect::, _>>() }) .collect::, _>>()?; @@ -127,10 +125,11 @@ pub struct CoreAudioDevice { } impl CoreAudioDevice { - fn from_id(device_id: AudioDeviceID, is_input: bool) -> Result { - let is_output = !is_input; // TODO: Interact with CoreAudio directly to be able to work with duplex devices - let is_default = get_default_device_id(true) == Some(device_id) - || get_default_device_id(false) == Some(device_id); + fn from_id(device_id: AudioDeviceID) -> Result { + let is_input = get_audio_device_supports_scope(device_id, Scope::Input)?; + let is_output = get_audio_device_supports_scope(device_id, Scope::Output)?; + let is_default = is_input && get_default_device_id(true) == Some(device_id) + || is_output && get_default_device_id(false) == Some(device_id); let mut device_type = DeviceType::empty(); device_type.set(DeviceType::INPUT, is_input); device_type.set(DeviceType::OUTPUT, is_output); @@ -141,6 +140,33 @@ impl CoreAudioDevice { }) } + fn get_audio_unit(&self) -> Result { + let mut unit = AudioUnit::new(IOType::HalOutput)?; + { + let value = if self.device_type.is_input() { 1u32 } else { 0 }; + unit.set_property( + kAudioOutputUnitProperty_EnableIO, + Scope::Input, + Element::Input, + Some(&value), + )?; + } + { + let value = if self.device_type.is_output() { + 1u32 + } else { + 0 + }; + unit.set_property( + kAudioOutputUnitProperty_EnableIO, + Scope::Output, + Element::Output, + Some(&value), + )?; + } + Ok(unit) + } + /// Sets the device's buffer size if requested in the `StreamConfig`. /// This must be done before creating the AudioUnit. fn set_buffer_size_from_config( @@ -206,25 +232,20 @@ impl AudioDevice for CoreAudioDevice { } fn default_config(&self) -> Result { - let audio_unit = audio_unit_from_device_id(self.device_id, self.device_type.is_input())?; - let format = if self.device_type.is_input() { - audio_unit.input_stream_format()? - } else { - audio_unit.output_stream_format()? - }; + let audio_unit = self.get_audio_unit()?; + let input_channels = audio_unit + .input_stream_format() + .map(|fmt| fmt.channels as usize) + .unwrap_or(0); + let output_channels = audio_unit + .output_stream_format() + .map(|fmt| fmt.channels as usize) + .unwrap_or(0); Ok(StreamConfig { - samplerate: audio_unit.sample_rate()?, - input_channels: if self.device_type.is_input() { - format.channels as _ - } else { - 0 - }, - output_channels: if self.device_type.is_output() { - format.channels as _ - } else { - 0 - }, + sample_rate: audio_unit.sample_rate()?, + input_channels, + output_channels, buffer_size_range: (None, None), exclusive: false, }) @@ -268,7 +289,7 @@ impl AudioDevice for CoreAudioDevice { .into_iter() .map(move |exclusive| (sr, exclusive)) }) - .map(move |(samplerate, exclusive)| { + .map(move |(sample_rate, exclusive)| { let channels = asbd.mFormat.mChannelsPerFrame; let input_channels = if device_type.is_input() { channels as _ @@ -281,7 +302,7 @@ impl AudioDevice for CoreAudioDevice { 0 }; StreamConfig { - samplerate, + sample_rate, input_channels, output_channels, buffer_size_range, @@ -291,15 +312,12 @@ impl AudioDevice for CoreAudioDevice { })) } - fn create_stream( + fn create_stream( &self, stream_config: StreamConfig, callback: Callback, ) -> Result, Self::Error> { - let mut device = *self; - device.device_type = DeviceType::INPUT; - device.set_buffer_size_from_config(&stream_config)?; - CoreAudioStream::new(self.device_id, self.device_type, stream_config, callback) + CoreAudioStream::new(self, stream_config, callback) } } @@ -344,26 +362,32 @@ impl AudioStreamHandle for CoreAudioStream { impl CoreAudioStream { fn new( - device_id: AudioDeviceID, - device_type: DeviceType, + device: &CoreAudioDevice, stream_config: StreamConfig, callback: Callback, ) -> Result { - if device_type.is_input() && !device_type.is_output() { - Self::new_input(device_id, stream_config, callback) + let requested_type = stream_config.requested_device_type(); + assert!(!requested_type.is_duplex(), "CoreAudio does not support native duplex mode"); + + let unsupported = device.device_type & !requested_type; + if !unsupported.is_empty() { + log::warn!("Cannot request {unsupported:?} for {device}, ignoring", device=device.name()); + } + let unit = device.get_audio_unit()?; + if requested_type.is_input() { + Self::new_input(unit, stream_config, callback) } else { - Self::new_output(device_id, stream_config, callback) + Self::new_output(unit, stream_config, callback) } } fn new_input( - device_id: AudioDeviceID, + mut audio_unit: AudioUnit, stream_config: StreamConfig, mut callback: Callback, ) -> Result { - let mut audio_unit = audio_unit_from_device_id(device_id, true)?; let asbd = - input_stream_format(stream_config.samplerate, stream_config.input_channels).to_asbd(); + input_stream_format(stream_config.sample_rate, stream_config.input_channels).to_asbd(); audio_unit.set_property( kAudioUnitProperty_StreamFormat, Scope::Output, @@ -435,13 +459,12 @@ impl CoreAudioStream { } fn new_output( - device_id: AudioDeviceID, + mut audio_unit: AudioUnit, stream_config: StreamConfig, mut callback: Callback, ) -> Result { - let mut audio_unit = audio_unit_from_device_id(device_id, false)?; - let asbd = - output_stream_format(stream_config.samplerate, stream_config.output_channels).to_asbd(); + let asbd = output_stream_format(stream_config.sample_rate, stream_config.output_channels) + .to_asbd(); audio_unit.set_property( kAudioUnitProperty_StreamFormat, Scope::Input, @@ -467,7 +490,7 @@ impl CoreAudioStream { let (tx, rx) = oneshot::channel::>(); callback.prepare(AudioCallbackContext { stream_config, - timestamp: Timestamp::new(asbd.mSampleRate), + timestamp: Timestamp::new(stream_config.sample_rate), }); let mut callback = Some(callback); @@ -481,7 +504,7 @@ impl CoreAudioStream { Timestamp::from_count(stream_config.sample_rate, args.time_stamp.mSampleTime as _); let dummy_input = AudioInput { buffer: AudioRef::empty(), - timestamp: Timestamp::new(asbd.mSampleRate), + timestamp: Timestamp::new(stream_config.sample_rate), }; let output = AudioOutput { buffer: buffer.as_mut(), From 6e98f93fa82a9734194b8a34af8989443fc753d7 Mon Sep 17 00:00:00 2001 From: Nathan Graule Date: Sat, 1 Nov 2025 19:40:50 +0100 Subject: [PATCH 07/38] fix(examples): fix compilation errors --- examples/input.rs | 17 ++++++++++++----- examples/sine_wave.rs | 2 +- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/examples/input.rs b/examples/input.rs index 87c9d29..5dfd6a6 100644 --- a/examples/input.rs +++ b/examples/input.rs @@ -11,7 +11,9 @@ fn main() -> Result<()> { let device = default_input_device(); let value = Arc::new(AtomicF32::new(0.)); - let stream = device.default_stream(RmsMeter::new(value.clone())).unwrap(); + let stream = device + .default_stream(DeviceType::INPUT, RmsMeter::new(value.clone())) + .unwrap(); util::display_peakmeter(value)?; stream.eject().unwrap(); Ok(()) @@ -29,17 +31,22 @@ impl RmsMeter { } impl AudioCallback for RmsMeter { - fn prepare(&mut self, _: AudioCallbackContext) {} + fn prepare(&mut self, context: AudioCallbackContext) { + let meter = self + .meter + .get_or_insert_with(|| PeakMeter::new(context.stream_config.sample_rate as f32, 15.0)); + meter.set_samplerate(context.stream_config.sample_rate as f32); + } fn process_audio( &mut self, - context: AudioCallbackContext, + _: AudioCallbackContext, input: AudioInput, _output: AudioOutput, ) { let meter = self .meter - .get_or_insert_with(|| PeakMeter::new(context.stream_config.sample_rate as f32, 15.0)); - meter.set_samplerate(context.stream_config.sample_rate as f32); + .as_mut() + .expect("Peak meter not constructed, prepare not called"); meter.process_buffer(input.buffer.as_ref()); self.value .store(meter.value(), std::sync::atomic::Ordering::Relaxed); diff --git a/examples/sine_wave.rs b/examples/sine_wave.rs index b65b26e..e828c76 100644 --- a/examples/sine_wave.rs +++ b/examples/sine_wave.rs @@ -9,7 +9,7 @@ fn main() -> Result<()> { let device = default_output_device(); println!("Using device {}", device.name()); - let stream = device.default_stream(SineWave::new(440.0)).unwrap(); + let stream = device.default_stream(DeviceType::OUTPUT, SineWave::new(440.0)).unwrap(); println!("Press Enter to stop"); std::io::stdin().read_line(&mut String::new())?; stream.eject().unwrap(); From db5441a916988fcfb7d9184191d31b18fbd04ed3 Mon Sep 17 00:00:00 2001 From: Nathan Graule Date: Sat, 1 Nov 2025 19:41:44 +0100 Subject: [PATCH 08/38] refactor: remove `SendEverywhereButOnWeb` as it was useless (web will require Send too) --- src/backends/alsa/device.rs | 6 +++--- src/backends/pipewire/device.rs | 4 ++-- src/duplex.rs | 2 +- src/lib.rs | 37 +++++++-------------------------- 4 files changed, 14 insertions(+), 35 deletions(-) diff --git a/src/backends/alsa/device.rs b/src/backends/alsa/device.rs index 7ea3c29..fe8158c 100644 --- a/src/backends/alsa/device.rs +++ b/src/backends/alsa/device.rs @@ -1,7 +1,7 @@ use crate::backends::alsa::stream::AlsaStream; use crate::backends::alsa::AlsaError; use crate::{ - AudioCallback, AudioDevice, Channel, DeviceType, SendEverywhereButOnWeb, StreamConfig, + AudioCallback, AudioDevice, Channel, DeviceType, StreamConfig, }; use alsa::{pcm, Direction, PCM}; use std::borrow::Cow; @@ -89,7 +89,7 @@ impl AudioDevice for AlsaDevice { None::<[StreamConfig; 0]> } - fn create_stream( + fn create_stream( &self, stream_config: StreamConfig, callback: Callback, @@ -233,7 +233,7 @@ impl AudioDevice for AlsaDuplex { None::<[StreamConfig; 0]> } - fn create_stream( + fn create_stream( &self, stream_config: StreamConfig, callback: Callback, diff --git a/src/backends/pipewire/device.rs b/src/backends/pipewire/device.rs index b4fae1b..b91bb0a 100644 --- a/src/backends/pipewire/device.rs +++ b/src/backends/pipewire/device.rs @@ -1,7 +1,7 @@ use super::stream::StreamHandle; use crate::backends::pipewire::error::PipewireError; use crate::{ - AudioCallback, AudioDevice, Channel, DeviceType, SendEverywhereButOnWeb, StreamConfig, + AudioCallback, AudioDevice, Channel, DeviceType, StreamConfig, }; use pipewire::context::Context; use pipewire::main_loop::MainLoop; @@ -81,7 +81,7 @@ impl AudioDevice for PipewireDevice { }) } - fn create_stream( + fn create_stream( &self, stream_config: StreamConfig, callback: Callback, diff --git a/src/duplex.rs b/src/duplex.rs index 022a2fc..69d5961 100644 --- a/src/duplex.rs +++ b/src/duplex.rs @@ -446,7 +446,7 @@ pub type DuplexStreamResult = Result< pub fn create_duplex_stream< InputDevice: AudioDevice, OutputDevice: AudioDevice, - Callback: AudioCallback, + Callback: 'static + AudioCallback, >( input_device: InputDevice, output_device: OutputDevice, diff --git a/src/lib.rs b/src/lib.rs index 36e6032..690b895 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -44,7 +44,7 @@ bitflags! { /// while others work in exclusive mode. pub trait AudioDriver { /// Type of errors that can happen when using this audio driver. - type Error: SendEverywhereButOnWeb + std::error::Error; + type Error: Send + std::error::Error; /// Type of audio devices this driver provides. type Device: AudioDevice; @@ -148,7 +148,7 @@ pub trait AudioDevice { type StreamHandle: AudioStreamHandle; /// Type of errors that can happen when using this device. - type Error: SendEverywhereButOnWeb + std::error::Error; + type Error: Send + std::error::Error; /// Device display name fn name(&self) -> Cow<'_, str>; @@ -180,7 +180,7 @@ pub trait AudioDevice { /// /// An output callback is required to process the audio, whose ownership will be transferred /// to the audio stream. - fn create_stream( + fn create_stream( &self, stream_config: StreamConfig, callback: Callback, @@ -191,36 +191,15 @@ pub trait AudioDevice { /// # Arguments /// /// - `callback`: Output callback to generate audio data with. - fn default_stream( + fn default_stream( &self, + requested_type: DeviceType, callback: Callback, ) -> Result, Self::Error> { - self.create_stream(self.default_config()?, callback) + self.create_stream(self.default_config()?.restrict(requested_type), callback) } } -/// Marker trait for values which are [`Send`] everywhere but on the web (as WASM does not yet have -/// web targets). -/// -/// This should only be used to define the traits and should not be relied upon in external code. -/// -/// This definition is selected on non-web platforms and does require [`Send`]. -#[cfg(not(wasm))] -pub trait SendEverywhereButOnWeb: 'static + Send {} -#[cfg(not(wasm))] -impl SendEverywhereButOnWeb for T {} - -/// Marker trait for values which are [Send] everywhere but on the web (as WASM does not yet have -/// web targets. -/// -/// This should only be used to define the traits and should not be relied upon in external code. -/// -/// This definition is selected on web platforms and does not require [`Send`]. -#[cfg(wasm)] -pub trait SendEverywhereButOnWeb {} -#[cfg(wasm)] -impl SendEverywhereButOnWeb for T {} - #[duplicate::duplicate_item( name bufty; [AudioInput] [AudioRef < 'a, T >]; @@ -252,7 +231,7 @@ pub struct AudioCallbackContext { /// Trait for types which handles an audio stream (input or output). pub trait AudioStreamHandle { /// Type of errors which have caused the stream to fail. - type Error: SendEverywhereButOnWeb + std::error::Error; + type Error: Send + std::error::Error; /// Eject the stream, returning ownership of the callback. /// @@ -263,7 +242,7 @@ pub trait AudioStreamHandle { /// Trait of types which process audio data. This is the trait that users will want to /// implement when processing audio from a device. -pub trait AudioCallback: SendEverywhereButOnWeb { +pub trait AudioCallback: Send { /// Prepare the audio callback to process audio. This function is *not* real-time safe (i.e., allocations can be /// performed), in preparation for processing the stream with [`Self::process_audio`]. fn prepare(&mut self, context: AudioCallbackContext); From d1eeea23b5d1328f5ca5d68b82cb3502880f3f8c Mon Sep 17 00:00:00 2001 From: Nathan Graule Date: Sat, 1 Nov 2025 19:42:30 +0100 Subject: [PATCH 09/38] feat: `StreamConfig` methods that use `DeviceType` --- src/lib.rs | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index 690b895..fda7700 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -115,6 +115,31 @@ pub struct StreamConfig { pub exclusive: bool, } +impl StreamConfig { + /// Returns a [`DeviceType`] that describes this [`StreamConfig`]. Only [`DeviceType::INPUT`] and + /// [`DeviceType::OUTPUT`] are set. + pub fn requested_device_type(&self) -> DeviceType { + let mut ret = DeviceType::empty(); + ret.set(DeviceType::INPUT, self.input_channels > 0); + ret.set(DeviceType::OUTPUT, self.output_channels > 0); + ret + } + + /// Changes the [`StreamConfig`] such that it matches the configuration of a stream created with a device with + /// the given [`DeviceType`]. + /// + /// This method returns a copy of the input [`StreamConfig`]. + pub fn restrict(mut self, requested_type: DeviceType) -> Self { + if !requested_type.is_input() { + self.input_channels = 0; + } + if !requested_type.is_output() { + self.output_channels = 0; + } + self + } +} + /// Configuration for an audio stream. #[derive(Debug, Clone, Copy, PartialEq)] pub struct ResolvedStreamConfig { From fb59144f995b332960d5e76636fc60d1961a25f6 Mon Sep 17 00:00:00 2001 From: Jackson Goode <54308792+jacksongoode@users.noreply.github.com> Date: Sat, 1 Nov 2025 20:01:55 +0100 Subject: [PATCH 10/38] fix: add back `buffer_size_range` to `AudioDevice` after rebase Signed-off-by: Nathan Graule --- src/backends/coreaudio.rs | 29 ++++++++++++++++++----------- src/lib.rs | 6 +++++- 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/src/backends/coreaudio.rs b/src/backends/coreaudio.rs index 8ef637f..38342ca 100644 --- a/src/backends/coreaudio.rs +++ b/src/backends/coreaudio.rs @@ -13,7 +13,9 @@ use coreaudio::audio_unit::macos_helpers::{ use coreaudio::audio_unit::render_callback::{data, Args}; use coreaudio::audio_unit::{AudioUnit, Element, IOType, SampleFormat, Scope, StreamFormat}; use coreaudio_sys::{ - kAudioDevicePropertyBufferFrameSize, kAudioOutputUnitProperty_EnableIO, + kAudioDevicePropertyBufferFrameSize, kAudioDevicePropertyBufferFrameSizeRange, + kAudioObjectPropertyElementMaster, kAudioObjectPropertyScopeInput, + kAudioObjectPropertyScopeOutput, kAudioOutputUnitProperty_EnableIO, kAudioUnitProperty_SampleRate, kAudioUnitProperty_StreamFormat, AudioDeviceID, AudioObjectGetPropertyData, AudioObjectPropertyAddress, AudioValueRange, }; @@ -59,14 +61,13 @@ fn set_device_property( use thiserror::Error; use crate::audio_buffer::{AudioBuffer, Sample}; +use crate::duplex::InputProxy; use crate::prelude::{AudioMut, AudioRef, ChannelMap32}; use crate::timestamp::Timestamp; use crate::{ AudioCallback, AudioCallbackContext, AudioDevice, AudioDriver, AudioInput, AudioOutput, - AudioStreamHandle, Channel, DeviceType, ResolvedStreamConfig, - StreamConfig, + AudioStreamHandle, Channel, DeviceType, ResolvedStreamConfig, StreamConfig, }; -use crate::duplex::InputProxy; type Result = std::result::Result; @@ -251,11 +252,6 @@ impl AudioDevice for CoreAudioDevice { }) } - fn is_config_supported(&self, _config: &StreamConfig) -> bool { - true - } - - /// Returns the supported I/O buffer size range for the device. fn buffer_size_range(&self) -> Result<(Option, Option), CoreAudioError> { let property_address = AudioObjectPropertyAddress { mSelector: kAudioDevicePropertyBufferFrameSizeRange, @@ -272,6 +268,10 @@ impl AudioDevice for CoreAudioDevice { Ok((Some(range.mMinimum as usize), Some(range.mMaximum as usize))) } + fn is_config_supported(&self, _config: &StreamConfig) -> bool { + true + } + fn enumerate_configurations(&self) -> Option> { const TYPICAL_SAMPLERATES: [f64; 5] = [44100., 48000., 96000., 128000., 192000.]; let supported_list = get_supported_physical_stream_formats(self.device_id) @@ -367,11 +367,17 @@ impl CoreAudioStream { callback: Callback, ) -> Result { let requested_type = stream_config.requested_device_type(); - assert!(!requested_type.is_duplex(), "CoreAudio does not support native duplex mode"); + assert!( + !requested_type.is_duplex(), + "CoreAudio does not support native duplex mode" + ); let unsupported = device.device_type & !requested_type; if !unsupported.is_empty() { - log::warn!("Cannot request {unsupported:?} for {device}, ignoring", device=device.name()); + log::warn!( + "Cannot request {unsupported:?} for {device}, ignoring", + device = device.name() + ); } let unit = device.get_audio_unit()?; if requested_type.is_input() { @@ -537,6 +543,7 @@ impl CoreAudioStream { #[cfg(test)] mod tests { use super::*; + use coreaudio_sys::{kAudioObjectPropertyElementMaster, kAudioObjectPropertyScopeOutput}; #[test] fn test_set_device_buffersize() { diff --git a/src/lib.rs b/src/lib.rs index fda7700..2f275ff 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,7 +3,6 @@ use bitflags::bitflags; use std::borrow::Cow; - use crate::audio_buffer::{AudioMut, AudioRef}; use crate::channel_map::ChannelMap32; use crate::timestamp::Timestamp; @@ -189,6 +188,11 @@ pub trait AudioDevice { /// returns `true` when passed to [`Self::is_config_supported`]). fn default_config(&self) -> Result; + /// Returns the supported I/O buffer size range for the device. + fn buffer_size_range(&self) -> Result<(Option, Option), Self::Error> { + Ok((None, None)) + } + /// Not all configuration values make sense for a particular device, and this method tests a /// configuration to see if it can be used in an audio stream. fn is_config_supported(&self, config: &StreamConfig) -> bool; From 71fdec83b0ba50dea7fd55b81f9b2c3967bb5681 Mon Sep 17 00:00:00 2001 From: Nathan Graule Date: Sat, 1 Nov 2025 20:21:34 +0100 Subject: [PATCH 11/38] test: fix compilation errors Signed-off-by: Nathan Graule --- examples/set_buffer_size.rs | 21 +++++++++++++-------- src/backends/coreaudio.rs | 2 +- src/duplex.rs | 4 ---- 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/examples/set_buffer_size.rs b/examples/set_buffer_size.rs index 7e86ecd..0f72e3d 100644 --- a/examples/set_buffer_size.rs +++ b/examples/set_buffer_size.rs @@ -6,7 +6,7 @@ mod util; #[cfg(os_coreaudio)] fn main() -> anyhow::Result<()> { use interflow::backends::coreaudio::CoreAudioDriver; - use interflow::channel_map::{ChannelMap32, CreateBitset}; + use interflow::channel_map::CreateBitset; use interflow::prelude::*; use std::sync::{ atomic::{AtomicBool, Ordering}, @@ -19,19 +19,23 @@ fn main() -> anyhow::Result<()> { sine_wave: SineWave, } - impl AudioOutputCallback for MyCallback { - fn on_output_data(&mut self, context: AudioCallbackContext, mut output: AudioOutput) { + impl AudioCallback for MyCallback { + fn prepare(&mut self, context: AudioCallbackContext) { + self.sine_wave.prepare(context); + } + + fn process_audio(&mut self, _: AudioCallbackContext, _: AudioInput, mut output: AudioOutput) { if self.first_callback.swap(false, Ordering::SeqCst) { println!( "Actual buffer size granted by OS: {}", - output.buffer.num_samples() + output.buffer.num_frames() ); } for mut frame in output.buffer.as_interleaved_mut().rows_mut() { let sample = self .sine_wave - .next_sample(context.stream_config.samplerate as f32); + .next_sample(); for channel_sample in &mut frame { *channel_sample = sample; } @@ -61,8 +65,9 @@ fn main() -> anyhow::Result<()> { println!("Requesting buffer size: {}", requested_buffer_size); let stream_config = StreamConfig { - samplerate: 48000.0, - channels: ChannelMap32::from_indices([0, 1]), + sample_rate: 48000.0, + input_channels: 0, + output_channels: 2, buffer_size_range: (Some(requested_buffer_size), Some(requested_buffer_size)), exclusive: false, }; @@ -72,7 +77,7 @@ fn main() -> anyhow::Result<()> { sine_wave: SineWave::new(440.0), }; - let stream = device.create_output_stream(stream_config, callback)?; + let stream = device.create_stream(stream_config, callback)?; println!("Playing sine wave... Press enter to stop."); std::io::stdin().read_line(&mut String::new())?; diff --git a/src/backends/coreaudio.rs b/src/backends/coreaudio.rs index 38342ca..5f915a1 100644 --- a/src/backends/coreaudio.rs +++ b/src/backends/coreaudio.rs @@ -305,7 +305,7 @@ impl AudioDevice for CoreAudioDevice { sample_rate, input_channels, output_channels, - buffer_size_range, + buffer_size_range: self.buffer_size_range().unwrap_or((None, None)), exclusive, } }) diff --git a/src/duplex.rs b/src/duplex.rs index 69d5961..c60bfdd 100644 --- a/src/duplex.rs +++ b/src/duplex.rs @@ -279,8 +279,6 @@ impl AudioCallback for DuplexCallback { /// /// let input_device = default_input_device(); /// let output_device = default_output_device(); -/// let input_config = input_device.default_input_config().unwrap(); -/// let output_config = output_device.default_output_config().unwrap(); /// /// struct MyCallback; /// @@ -428,8 +426,6 @@ pub type DuplexStreamResult = Result< /// /// let input_device = default_input_device(); /// let output_device = default_output_device(); -/// let input_config = input_device.default_input_config().unwrap(); -/// let output_config = output_device.default_output_config().unwrap(); /// /// let callback = MyCallback::new(); /// From 164d19bd78c328ddf72ba2b7f8794f86f21d16f0 Mon Sep 17 00:00:00 2001 From: Nathan Graule Date: Sat, 20 Dec 2025 01:45:28 +0100 Subject: [PATCH 12/38] wip(wasapi): stream format negociation --- src/backends/wasapi/device.rs | 73 ++--- src/backends/wasapi/driver.rs | 30 +- src/backends/wasapi/error.rs | 3 + src/backends/wasapi/stream.rs | 535 +++++++++++++++++++++++----------- src/backends/wasapi/util.rs | 46 ++- src/lib.rs | 12 +- src/sample.rs | 72 +++++ 7 files changed, 550 insertions(+), 221 deletions(-) create mode 100644 src/sample.rs diff --git a/src/backends/wasapi/device.rs b/src/backends/wasapi/device.rs index f3e3843..104672d 100644 --- a/src/backends/wasapi/device.rs +++ b/src/backends/wasapi/device.rs @@ -1,11 +1,8 @@ use super::{error, stream}; use crate::backends::wasapi::stream::WasapiStream; -use crate::channel_map::Bitset; +use crate::prelude::wasapi::stream::{FindSupportedConfig, StreamDirection}; use crate::prelude::wasapi::util::WasapiMMDevice; -use crate::{ - AudioCallback, AudioDevice, AudioInputDevice, AudioOutputCallback, AudioOutputDevice, Channel, - DeviceType, StreamConfig, -}; +use crate::{AudioCallback, AudioDevice, Channel, DeviceType, StreamConfig}; use std::borrow::Cow; use windows::Win32::Media::Audio; @@ -27,6 +24,7 @@ impl WasapiDevice { impl AudioDevice for WasapiDevice { type Error = error::WasapiError; + type StreamHandle = WasapiStream; fn name(&self) -> Cow { match self.device.name() { @@ -47,69 +45,50 @@ impl AudioDevice for WasapiDevice { } fn is_config_supported(&self, config: &StreamConfig) -> bool { - self.device_type.contains(DeviceType::OUTPUT) - && stream::is_output_config_supported(self.device.clone(), config) + if self.device_type.is_duplex() || !self.device_type.is_physical() { + return false; + } + FindSupportedConfig { + config, + device: &self.device, + is_output: self.device_type.is_output(), + } + .supported_config() + .is_some() } fn enumerate_configurations(&self) -> Option> { None::<[StreamConfig; 0]> } -} -impl AudioInputDevice for WasapiDevice { - type StreamHandle = WasapiStream; - - fn default_input_config(&self) -> Result { + fn default_config(&self) -> Result { let audio_client = self.device.activate::()?; let format = unsafe { audio_client.GetMixFormat()?.read_unaligned() }; let frame_size = unsafe { audio_client.GetBufferSize() } .map(|i| i as usize) .ok(); + let (input_channels, output_channels) = if self.device_type.is_input() { + (format.nChannels as _, 0) + } else { + (0, format.nChannels as _) + }; Ok(StreamConfig { - output_channels: 0u32.with_indices(0..format.nChannels as _), - exclusive: false, - samplerate: format.nSamplesPerSec as _, + sample_rate: format.nSamplesPerSec as _, + input_channels, + output_channels, buffer_size_range: (frame_size, frame_size), - }) - } - - fn create_input_stream( - &self, - stream_config: StreamConfig, - callback: Callback, - ) -> Result, Self::Error> { - Ok(WasapiStream::new_input( - self.device.clone(), - stream_config, - callback, - )) - } -} - -impl AudioOutputDevice for WasapiDevice { - type StreamHandle = WasapiStream; - - fn default_output_config(&self) -> Result { - let audio_client = self.device.activate::()?; - let format = unsafe { audio_client.GetMixFormat()?.read_unaligned() }; - let frame_size = unsafe { audio_client.GetBufferSize() } - .map(|i| i as usize) - .ok(); - Ok(StreamConfig { - output_channels: 0u32.with_indices(0..format.nChannels as _), exclusive: false, - samplerate: format.nSamplesPerSec as _, - buffer_size_range: (frame_size, frame_size), }) } - fn create_output_stream( + fn create_stream( &self, stream_config: StreamConfig, callback: Callback, ) -> Result, Self::Error> { - Ok(WasapiStream::new_output( + Ok(WasapiStream::new( self.device.clone(), + StreamDirection::try_from(self.device_type)?, stream_config, callback, )) @@ -117,7 +96,7 @@ impl AudioOutputDevice for WasapiDevice { } /// An iterable collection WASAPI devices. -pub struct WasapiDeviceList { +pub(crate) struct WasapiDeviceList { pub(crate) collection: Audio::IMMDeviceCollection, pub(crate) total_count: u32, pub(crate) next_item: u32, diff --git a/src/backends/wasapi/driver.rs b/src/backends/wasapi/driver.rs index f505d1d..2fce33c 100644 --- a/src/backends/wasapi/driver.rs +++ b/src/backends/wasapi/driver.rs @@ -61,16 +61,34 @@ impl AudioDeviceEnumerator { &self, device_type: DeviceType, ) -> Result, error::WasapiError> { - let data_flow = bitflags_match!(device_type, { + let Some(flow) = bitflags_match!(device_type, { DeviceType::INPUT | DeviceType::PHYSICAL => Some(Audio::eCapture), DeviceType::OUTPUT | DeviceType::PHYSICAL => Some(Audio::eRender), _ => None, - }); + }) else { + return Ok(None); + }; - data_flow.map_or(Ok(None), |flow| unsafe { - let device = self.0.GetDefaultAudioEndpoint(flow, Audio::eConsole)?; - Ok(Some(WasapiDevice::new(device, device_type))) - }) + self.get_default_device_with_role(flow, Audio::eConsole) + .map(Some) + } + + fn get_default_device_with_role( + &self, + flow: Audio::EDataFlow, + role: Audio::ERole, + ) -> Result { + unsafe { + let device = self.0.GetDefaultAudioEndpoint(flow, role)?; + let device_type = match flow { + Audio::eRender => DeviceType::OUTPUT, + _ => DeviceType::INPUT, + }; + Ok(WasapiDevice::new( + device, + DeviceType::PHYSICAL | device_type, + )) + } } // Returns a chained iterator of output and input devices. diff --git a/src/backends/wasapi/error.rs b/src/backends/wasapi/error.rs index 964be9b..fc833a7 100644 --- a/src/backends/wasapi/error.rs +++ b/src/backends/wasapi/error.rs @@ -13,4 +13,7 @@ pub enum WasapiError { /// Windows Foundation error #[error("Win32 error: {0}")] FoundationError(String), + /// Duplex stream requested, unsupported by WASAPI + #[error("Unsupported duplex stream requested")] + DuplexStreamRequested, } diff --git a/src/backends/wasapi/stream.rs b/src/backends/wasapi/stream.rs index bb40df1..9fb065c 100644 --- a/src/backends/wasapi/stream.rs +++ b/src/backends/wasapi/stream.rs @@ -1,11 +1,13 @@ use super::error; -use crate::audio_buffer::AudioMut; use crate::backends::wasapi::util::WasapiMMDevice; use crate::channel_map::Bitset; -use crate::prelude::{AudioRef, Timestamp}; +use crate::prelude::wasapi::util::CoTaskOwned; +use crate::prelude::{AudioRef, Timestamp, WasapiError}; +use crate::sample::ConvertSample; +use crate::{audio_buffer::AudioMut, ResolvedStreamConfig}; use crate::{ - AudioCallback, AudioCallbackContext, AudioInput, AudioOutput, AudioOutputCallback, - AudioStreamHandle, StreamConfig, + AudioCallback, AudioCallbackContext, AudioInput, AudioOutput, AudioStreamHandle, DeviceType, + StreamConfig, }; use duplicate::duplicate_item; use std::marker::PhantomData; @@ -122,12 +124,14 @@ struct AudioThread { audio_client: Audio::IAudioClient, interface: Interface, audio_clock: Audio::IAudioClock, - stream_config: StreamConfig, + stream_config: ResolvedStreamConfig, eject_signal: EjectSignal, frame_size: usize, callback: Callback, event_handle: HANDLE, clock_start: Duration, + convert_scratch_buffer: Box<[f32]>, + process_fn: fn(&mut Self) -> Result<(), error::WasapiError>, } impl AudioThread { @@ -145,59 +149,46 @@ impl AudioThread { } impl AudioThread { - fn new( + fn new_with_process_fn( device: WasapiMMDevice, + direction: StreamDirection, eject_signal: EjectSignal, - mut stream_config: StreamConfig, + stream_config: StreamConfig, callback: Callback, + process_fn: impl FnOnce(SupportedConfig) -> fn(&mut Self) -> Result<(), error::WasapiError>, ) -> Result { + eprintln!("Current stream config: {stream_config:#?}"); + let supported_config = FindSupportedConfig { + config: &stream_config, + device: &device, + is_output: matches!(direction, StreamDirection::Output), + } + .supported_config() + .ok_or(WasapiError::ConfigurationNotAvailable)?; + let frame_size = stream_config + .buffer_size_range + .0 + .or(stream_config.buffer_size_range.1); + let buffer_duration = frame_size + .map(|frame_size| buffer_size_to_duration(frame_size, stream_config.sample_rate as _)) + .unwrap_or(0); unsafe { - let audio_client: Audio::IAudioClient = device.activate()?; - let sharemode = if stream_config.exclusive { - Audio::AUDCLNT_SHAREMODE_EXCLUSIVE - } else { - Audio::AUDCLNT_SHAREMODE_SHARED - }; - let format = { - let mut format = config_to_waveformatextensible(&stream_config); - let mut actual_format = ptr::null_mut(); - audio_client - .IsFormatSupported( - sharemode, - &format.Format, - (!stream_config.exclusive).then_some(&mut actual_format), - ) - .ok()?; - if !stream_config.exclusive { - assert!(!actual_format.is_null()); - format.Format = actual_format.read_unaligned(); - CoTaskMemFree(actual_format.cast()); - let sample_rate = format.Format.nSamplesPerSec; - stream_config.output_channels = - 0u32.with_indices(0..format.Format.nChannels as _); - stream_config.samplerate = sample_rate as _; - } - format - }; - let frame_size = stream_config - .buffer_size_range - .0 - .or(stream_config.buffer_size_range.1); - let buffer_duration = frame_size - .map(|frame_size| { - buffer_size_to_duration(frame_size, stream_config.samplerate as _) - }) - .unwrap_or(0); + let audio_client = device.activate::()?; audio_client.Initialize( - sharemode, + if stream_config.exclusive { + Audio::AUDCLNT_SHAREMODE_EXCLUSIVE + } else { + Audio::AUDCLNT_SHAREMODE_SHARED + }, Audio::AUDCLNT_STREAMFLAGS_EVENTCALLBACK | Audio::AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM, buffer_duration, 0, - &format.Format, + supported_config.format(), None, )?; let buffer_size = audio_client.GetBufferSize()? as usize; + let resolved_config = supported_config.resolved_config(direction); let event_handle = { let event_handle = Threading::CreateEventA(None, false, false, windows::core::PCSTR(ptr::null()))?; @@ -214,12 +205,15 @@ impl AudioThread { event_handle, frame_size, eject_signal, - stream_config: StreamConfig { - buffer_size_range: (Some(frame_size), Some(frame_size)), - ..stream_config - }, + stream_config: resolved_config, clock_start: Duration::ZERO, callback, + convert_scratch_buffer: if matches!(supported_config, SupportedConfig::Float(..)) { + Box::new([]) + } else { + vec![0.0; resolved_config.max_frame_count].into_boxed_slice() + }, + process_fn: process_fn(supported_config), }) } } @@ -241,13 +235,33 @@ impl AudioThread { let clock = stream_instant(&self.audio_clock)?; let diff = clock - self.clock_start; Ok(Timestamp::from_duration( - self.stream_config.samplerate, + self.stream_config.sample_rate, diff, )) } } impl AudioThread { + pub(super) fn new( + device: WasapiMMDevice, + eject_signal: EjectSignal, + stream_config: StreamConfig, + callback: Callback, + ) -> Result { + Self::new_with_process_fn( + device, + StreamDirection::Input, + eject_signal, + stream_config, + callback, + |config| match config { + SupportedConfig::Float(..) => Self::process_float, + SupportedConfig::I32(..) => Self::process::, + SupportedConfig::U32(..) => Self::process::, + }, + ) + } + fn run(mut self) -> Result { set_thread_priority(); unsafe { @@ -259,19 +273,19 @@ impl AudioThread break self.finalize(); } self.await_frame()?; - self.process()?; + (self.process_fn)(&mut self)?; } .inspect_err(|err| eprintln!("Render thread process error: {err}")) } - fn process(&mut self) -> Result<(), error::WasapiError> { + fn process_float(&mut self) -> Result<(), error::WasapiError> { let frames_available = unsafe { self.interface.GetNextPacketSize()? as usize }; if frames_available == 0 { return Ok(()); } let Some(mut buffer) = AudioCaptureBuffer::::from_client( &self.interface, - self.stream_config.output_channels.count(), + self.stream_config.output_channels, )? else { eprintln!("Null buffer from WASAPI"); @@ -283,15 +297,78 @@ impl AudioThread timestamp, }; let buffer = - AudioRef::from_interleaved(&mut buffer, self.stream_config.output_channels.count()) - .unwrap(); - let output = AudioInput { timestamp, buffer }; - self.callback.process_audio(context, output); + AudioRef::from_interleaved(&mut buffer, self.stream_config.output_channels).unwrap(); + let input = AudioInput { timestamp, buffer }; + self.callback.process_audio( + context, + input, + AudioOutput { + timestamp, + buffer: AudioMut::empty(), + }, + ); + Ok(()) + } + + fn process(&mut self) -> Result<(), error::WasapiError> { + let frames_available = unsafe { self.interface.GetNextPacketSize()? as usize }; + if frames_available == 0 { + return Ok(()); + } + let Some(buffer) = AudioCaptureBuffer::::from_client( + &self.interface, + self.stream_config.output_channels, + )? + else { + eprintln!("Null buffer from WASAPI"); + return Ok(()); + }; + T::convert_to_slice(&mut *self.convert_scratch_buffer, &*buffer); + + let timestamp = self.output_timestamp()?; + let context = AudioCallbackContext { + stream_config: self.stream_config, + timestamp, + }; + let buffer = AudioRef::from_interleaved( + &mut self.convert_scratch_buffer[..buffer.len()], + self.stream_config.output_channels, + ) + .unwrap(); + let input = AudioInput { timestamp, buffer }; + self.callback.process_audio( + context, + input, + AudioOutput { + timestamp, + buffer: AudioMut::empty(), + }, + ); Ok(()) } } -impl AudioThread { +impl AudioThread { + pub(super) fn new( + device: WasapiMMDevice, + eject_signal: EjectSignal, + stream_config: StreamConfig, + callback: Callback, + ) -> Result { + Self::new_with_process_fn( + device, + StreamDirection::Input, + eject_signal, + stream_config, + callback, + |config| match config { + SupportedConfig::Float(..) => Self::process_float, + SupportedConfig::I32(..) => Self::process::, + SupportedConfig::U32(..) => Self::process::, + }, + ) + } + fn run(mut self) -> Result { set_thread_priority(); unsafe { @@ -303,12 +380,12 @@ impl AudioThread Result<(), error::WasapiError> { + fn process_float(&mut self) -> Result<(), error::WasapiError> { let frames_available = unsafe { let padding = self.audio_client.GetCurrentPadding()? as usize; self.frame_size - padding @@ -316,14 +393,10 @@ impl AudioThread::from_client( &self.interface, - self.stream_config.output_channels.count(), + self.stream_config.output_channels, frames_requested, )?; let timestamp = self.output_timestamp()?; @@ -332,14 +405,81 @@ impl AudioThread(&mut self) -> Result<(), error::WasapiError> { + let frames_available = unsafe { + let padding = self.audio_client.GetCurrentPadding()? as usize; + self.frame_size - padding + }; + if frames_available == 0 { + return Ok(()); + } + let frames_requested = frames_available.min(self.stream_config.max_frame_count); + let mut buffer = AudioRenderBuffer::::from_client( + &self.interface, + self.stream_config.output_channels, + frames_requested, + )?; + let timestamp = self.output_timestamp()?; + let context = AudioCallbackContext { + stream_config: self.stream_config, + timestamp, + }; + let output = AudioOutput { + timestamp, + buffer: AudioMut::from_interleaved_mut( + &mut self.convert_scratch_buffer[..buffer.len()], + self.stream_config.output_channels, + ) + .unwrap(), + }; + self.callback.process_audio( + context, + AudioInput { + timestamp: timestamp, + buffer: AudioRef::empty(), + }, + output, + ); + let len = buffer.len(); + T::convert_from_slice(&mut *buffer, &self.convert_scratch_buffer[..len]); Ok(()) } } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum StreamDirection { + Input, + Output, +} + +impl TryFrom for StreamDirection { + type Error = WasapiError; + + fn try_from(value: DeviceType) -> Result { + if value.is_input() { + Ok(Self::Input) + } else if value.is_output() { + Ok(Self::Output) + } else { + Err(WasapiError::DuplexStreamRequested) + } + } +} + /// Type representing a WASAPI audio stream. pub struct WasapiStream { join_handle: JoinHandle>, @@ -358,8 +498,9 @@ impl AudioStreamHandle for WasapiStream { } impl WasapiStream { - pub(crate) fn new_input( + pub(crate) fn new( device: WasapiMMDevice, + direction: StreamDirection, stream_config: StreamConfig, callback: Callback, ) -> Self { @@ -368,41 +509,23 @@ impl WasapiStream { .name("interflow_wasapi_output_stream".to_string()) .spawn({ let eject_signal = eject_signal.clone(); - move || { - let inner: AudioThread = - AudioThread::new(device, eject_signal, stream_config, callback) - .inspect_err(|err| { - eprintln!("Failed to create render thread: {err}") - })?; - inner.run() - } - }) - .expect("Cannot spawn audio output thread"); - Self { - join_handle, - eject_signal, - } - } -} - -impl WasapiStream { - pub(crate) fn new_output( - device: WasapiMMDevice, - stream_config: StreamConfig, - callback: Callback, - ) -> Self { - let eject_signal = EjectSignal::default(); - let join_handle = std::thread::Builder::new() - .name("interflow_wasapi_output_stream".to_string()) - .spawn({ - let eject_signal = eject_signal.clone(); - move || { - let inner: AudioThread = - AudioThread::new(device, eject_signal, stream_config, callback) - .inspect_err(|err| { - eprintln!("Failed to create render thread: {err}") - })?; - inner.run() + move || match direction { + StreamDirection::Input => AudioThread::<_, Audio::IAudioCaptureClient>::new( + device, + eject_signal, + stream_config, + callback, + ) + .inspect_err(|err| eprintln!("Failed to create render thread: {err}"))? + .run(), + StreamDirection::Output => AudioThread::<_, Audio::IAudioRenderClient>::new( + device, + eject_signal, + stream_config, + callback, + ) + .inspect_err(|err| eprintln!("Failed to create render thread: {err}"))? + .run(), } }) .expect("Cannot spawn audio output thread"); @@ -440,20 +563,52 @@ fn stream_instant(audio_clock: &Audio::IAudioClock) -> Result Audio::WAVEFORMATEXTENSIBLE { - let format_tag = KernelStreaming::WAVE_FORMAT_EXTENSIBLE; - let channels = config.output_channels as u16; - let sample_rate = config.samplerate as u32; +const fn config_to_waveformatextensible( + config: &StreamConfig, + is_output: bool, +) -> Audio::WAVEFORMATEXTENSIBLE { + const CB_SIZE: u16 = { + const EXTENSIBLE_SIZE: usize = size_of::(); + const EX_SIZE: usize = size_of::(); + (EXTENSIBLE_SIZE - EX_SIZE) as u16 + }; + let waveformatex = config_to_waveformatex::( + config, + is_output, + KernelStreaming::WAVE_FORMAT_EXTENSIBLE, + CB_SIZE, + ); + let sample_bytes = size_of::() as u16; - let avg_bytes_per_sec = u32::from(channels) * sample_rate * u32::from(sample_bytes); - let block_align = channels * sample_bytes; let bits_per_sample = 8 * sample_bytes; + let channel_mask = KernelStreaming::KSAUDIO_SPEAKER_DIRECTOUT; + let sub_format = Multimedia::KSDATAFORMAT_SUBTYPE_IEEE_FLOAT; + + Audio::WAVEFORMATEXTENSIBLE { + Format: waveformatex, + Samples: Audio::WAVEFORMATEXTENSIBLE_0 { + wSamplesPerBlock: bits_per_sample, + }, + dwChannelMask: channel_mask, + SubFormat: sub_format, + } +} - let cb_size = { - let extensible_size = size_of::(); - let ex_size = size_of::(); - (extensible_size - ex_size) as u16 +const fn config_to_waveformatex( + config: &StreamConfig, + is_output: bool, + format_tag: u32, + cb_size: u16, +) -> Audio::WAVEFORMATEX { + let channels = if is_output { + config.output_channels as u16 + } else { + config.input_channels as u16 }; + let sample_rate = config.sample_rate as u32; + let sample_bytes = size_of::() as u16; + let avg_bytes_per_sec = channels as u32 * sample_rate * sample_bytes as u32; + let block_align = channels * sample_bytes; let waveformatex = Audio::WAVEFORMATEX { wFormatTag: format_tag as u16, @@ -461,62 +616,116 @@ pub(crate) fn config_to_waveformatextensible(config: &StreamConfig) -> Audio::WA nSamplesPerSec: sample_rate, nAvgBytesPerSec: avg_bytes_per_sec, nBlockAlign: block_align, - wBitsPerSample: bits_per_sample, + wBitsPerSample: 8 * sample_bytes, cbSize: cb_size, }; + waveformatex +} - let channel_mask = KernelStreaming::KSAUDIO_SPEAKER_DIRECTOUT; +pub(super) enum SupportedConfig { + Float(Audio::WAVEFORMATEXTENSIBLE), + I32(Audio::WAVEFORMATEX), + U32(Audio::WAVEFORMATEX), +} - let sub_format = Multimedia::KSDATAFORMAT_SUBTYPE_IEEE_FLOAT; +impl SupportedConfig { + fn format(&self) -> &Audio::WAVEFORMATEX { + match self { + SupportedConfig::Float(wfx) => &wfx.Format, + SupportedConfig::I32(wfx) => wfx, + SupportedConfig::U32(wfx) => wfx, + } + } - let waveformatextensible = Audio::WAVEFORMATEXTENSIBLE { - Format: waveformatex, - Samples: Audio::WAVEFORMATEXTENSIBLE_0 { - wSamplesPerBlock: bits_per_sample, - }, - dwChannelMask: channel_mask, - SubFormat: sub_format, - }; + fn resolved_config(&self, direction: StreamDirection) -> ResolvedStreamConfig { + let format = self.format(); + ResolvedStreamConfig { + sample_rate: format.nSamplesPerSec as _, + input_channels: if direction == StreamDirection::Input { + format.nChannels as _ + } else { + 0 + }, + output_channels: if direction == StreamDirection::Output { + format.nChannels as _ + } else { + 0 + }, + max_frame_count: format.cbSize as _, + } + } +} - waveformatextensible +pub(super) struct FindSupportedConfig<'a> { + pub(super) device: &'a WasapiMMDevice, + pub(super) config: &'a StreamConfig, + pub(super) is_output: bool, } -pub(crate) fn is_output_config_supported( - device: WasapiMMDevice, - stream_config: &StreamConfig, -) -> bool { - let mut try_ = || unsafe { - let audio_client: Audio::IAudioClient = device.activate()?; - let sharemode = if stream_config.exclusive { - Audio::AUDCLNT_SHAREMODE_EXCLUSIVE - } else { - Audio::AUDCLNT_SHAREMODE_SHARED - }; - let mut format = config_to_waveformatextensible(&stream_config); - let mut actual_format = ptr::null_mut(); - audio_client - .IsFormatSupported( - sharemode, - &format.Format, - (!stream_config.exclusive).then_some(&mut actual_format), - ) - .ok()?; - if !stream_config.exclusive { - assert!(!actual_format.is_null()); - format.Format = actual_format.read_unaligned(); - CoTaskMemFree(actual_format.cast()); - let sample_rate = format.Format.nSamplesPerSec; - let new_channels = 0u32.with_indices(0..format.Format.nChannels as _); - let new_samplerate = sample_rate as f64; - if stream_config.samplerate != new_samplerate - || stream_config.output_channels.count() != new_channels.count() - { - return Ok(false); +impl<'a> FindSupportedConfig<'a> { + pub(super) fn supported_config(&self) -> Option { + self.supported_config_float() + .map(SupportedConfig::Float) + .or_else(|| self.supported_config_i32().map(SupportedConfig::I32)) + .or_else(|| self.supported_config_u32().map(SupportedConfig::U32)) + } + + fn supported_config_float(&self) -> Option { + let format = config_to_waveformatextensible(self.config, self.is_output); + self.check_format(self.config, &format.Format) + .then_some(format) + } + + fn supported_config_i32(&self) -> Option { + let format = + config_to_waveformatex::(self.config, self.is_output, Audio::WAVE_FORMAT_PCM, 0); + self.check_format(self.config, &format).then_some(format) + } + + fn supported_config_u32(&self) -> Option { + let format = + config_to_waveformatex::(self.config, self.is_output, Audio::WAVE_FORMAT_PCM, 0); + self.check_format(self.config, &format).then_some(format) + } + + fn check_format(&self, config: &StreamConfig, format: &Audio::WAVEFORMATEX) -> bool { + let try_ = || -> Result<_, WasapiError> { + unsafe { + let audio_client = self.device.activate::()?; + let sharemode = if self.config.exclusive { + Audio::AUDCLNT_SHAREMODE_EXCLUSIVE + } else { + Audio::AUDCLNT_SHAREMODE_SHARED + }; + if self.config.exclusive { + audio_client + .IsFormatSupported(Audio::AUDCLNT_SHAREMODE_EXCLUSIVE, format, None) + .ok()?; + return Ok(true); + } else { + let Some(actual_format) = CoTaskOwned::construct(|ptr| { + audio_client + .IsFormatSupported(sharemode, format, Some(ptr)) + .is_ok() + }) else { + return Ok(false); + }; + let format = actual_format.read_unaligned(); + let sample_rate = format.nSamplesPerSec; + let new_channels = format.nChannels; + let new_sample_rate = sample_rate as f64; + if config.sample_rate != new_sample_rate + || config.output_channels != new_channels.count() + { + Ok(false) + } else { + Ok(true) + } + } } - } - Ok::<_, error::WasapiError>(true) - }; - try_() - .inspect_err(|err| eprintln!("Error while checking configuration is valid: {err}")) - .unwrap_or(false) + }; + try_() + .inspect_err(|err| eprintln!("Error while checking configuration is valid: {err}")) + .unwrap_or(false) + } } diff --git a/src/backends/wasapi/util.rs b/src/backends/wasapi/util.rs index a0744fc..64056f9 100644 --- a/src/backends/wasapi/util.rs +++ b/src/backends/wasapi/util.rs @@ -1,12 +1,14 @@ use crate::prelude::wasapi::error; use std::ffi::OsString; use std::marker::PhantomData; +use std::ops; use std::os::windows::ffi::OsStringExt; -use windows::core::Interface; +use std::ptr::{self, NonNull}; +use windows::core::{Interface, HRESULT}; use windows::Win32::Devices::Properties; use windows::Win32::Foundation::RPC_E_CHANGED_MODE; use windows::Win32::Media::Audio; -use windows::Win32::System::Com; +use windows::Win32::System::Com::{self, CoTaskMemFree}; use windows::Win32::System::Com::{ CoInitializeEx, CoUninitialize, StructuredStorage, COINIT_APARTMENTTHREADED, STGM_READ, }; @@ -130,3 +132,43 @@ fn get_device_name(device: &Audio::IMMDevice) -> Option { Some(name) } } + +#[repr(transparent)] +pub(super) struct CoTaskOwned { + ptr: ptr::NonNull, +} + +impl ops::Deref for CoTaskOwned { + type Target = ptr::NonNull; + fn deref(&self) -> &Self::Target { + &self.ptr + } +} + +impl ops::DerefMut for CoTaskOwned { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.ptr + } +} + +impl Drop for CoTaskOwned { + fn drop(&mut self) { + unsafe { + CoTaskMemFree(Some(self.ptr.as_ptr().cast())); + } + } +} + +impl CoTaskOwned { + pub(super) const unsafe fn new(ptr: NonNull) -> Self { + Self { ptr } + } + + pub(super) unsafe fn construct(func: impl FnOnce(*mut *mut T) -> bool) -> Option { + let mut ptr = ptr::null_mut(); + if !func(&mut ptr) { + return None; + } + ptr::NonNull::new(ptr).map(|ptr| Self { ptr }) + } +} diff --git a/src/lib.rs b/src/lib.rs index 2f275ff..e7bd271 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,17 +1,18 @@ #![doc = include_str!("../README.md")] #![warn(missing_docs)] -use bitflags::bitflags; -use std::borrow::Cow; use crate::audio_buffer::{AudioMut, AudioRef}; use crate::channel_map::ChannelMap32; use crate::timestamp::Timestamp; +use bitflags::bitflags; +use std::borrow::Cow; pub mod audio_buffer; pub mod backends; pub mod channel_map; pub mod duplex; pub mod prelude; +mod sample; pub mod timestamp; bitflags! { @@ -225,7 +226,12 @@ pub trait AudioDevice { requested_type: DeviceType, callback: Callback, ) -> Result, Self::Error> { - self.create_stream(self.default_config()?.restrict(requested_type), callback) + let config = self.default_config()?.restrict(requested_type); + debug_assert!( + self.is_config_supported(&config), + "Default configuration is not supported" + ); + self.create_stream(config, callback) } } diff --git a/src/sample.rs b/src/sample.rs new file mode 100644 index 0000000..1f18018 --- /dev/null +++ b/src/sample.rs @@ -0,0 +1,72 @@ +use duplicate::duplicate_item; + +pub trait ConvertSample: Sized + Copy { + const ZERO: Self; + + fn convert_to_f32(self) -> f32; + fn convert_from_f32(v: f32) -> Self; + + fn convert_to_slice(output: &mut [f32], input: &[Self]) { + assert!(output.len() >= input.len()); + for (out, sample) in output.iter_mut().zip(input) { + *out = sample.convert_to_f32(); + } + } + + fn convert_from_slice(output: &mut [Self], input: &[f32]) { + assert!(output.len() >= input.len()); + for (out, &sample) in output.iter_mut().zip(input) { + *out = Self::convert_from_f32(sample); + } + } +} + +impl ConvertSample for f32 { + const ZERO: Self = 0.0; + + #[inline] + fn convert_to_f32(self) -> f32 { + self + } + + #[inline] + fn convert_from_f32(v: f32) -> Self { + v + } +} + +#[duplicate_item( +int; +[i8]; +[i16]; +[i32]; +)] +impl ConvertSample for int { + const ZERO: Self = 0; + + fn convert_to_f32(self) -> f32 { + self as f32 / Self::MAX as f32 + } + + fn convert_from_f32(f: f32) -> Self { + (f * Self::MAX as f32) as Self + } +} + +#[duplicate_item( +uint zero; +[u8] [128]; +[u16] [32768]; +[u32] [2147483648]; +)] +impl ConvertSample for uint { + const ZERO: Self = zero; + + fn convert_to_f32(self) -> f32 { + 2.0 * self as f32 / Self::MAX as f32 - 1.0 + } + + fn convert_from_f32(f: f32) -> Self { + ((f + 1.0) * Self::MAX as f32 / 2.0) as Self + } +} From 75d65b8d3ed3fffd818f2a0c2de08398d7217e2f Mon Sep 17 00:00:00 2001 From: Nathan Graule Date: Sun, 8 Feb 2026 15:21:28 +0100 Subject: [PATCH 13/38] wip: move to workspace structure --- Cargo.lock | 43 +-- Cargo.toml | 14 +- crates/interflow-core/Cargo.toml | 12 + crates/interflow-core/src/buffer.rs | 411 ++++++++++++++++++++++++++ crates/interflow-core/src/device.rs | 135 +++++++++ crates/interflow-core/src/lib.rs | 65 ++++ crates/interflow-core/src/platform.rs | 19 ++ crates/interflow-core/src/proxies.rs | 48 +++ crates/interflow-core/src/stream.rs | 78 +++++ crates/interflow-core/src/timing.rs | 175 +++++++++++ crates/interflow-core/src/traits.rs | 100 +++++++ 11 files changed, 1078 insertions(+), 22 deletions(-) create mode 100644 crates/interflow-core/Cargo.toml create mode 100644 crates/interflow-core/src/buffer.rs create mode 100644 crates/interflow-core/src/device.rs create mode 100644 crates/interflow-core/src/lib.rs create mode 100644 crates/interflow-core/src/platform.rs create mode 100644 crates/interflow-core/src/proxies.rs create mode 100644 crates/interflow-core/src/stream.rs create mode 100644 crates/interflow-core/src/timing.rs create mode 100644 crates/interflow-core/src/traits.rs diff --git a/Cargo.lock b/Cargo.lock index e1a7501..141d028 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "aho-corasick" @@ -296,9 +296,9 @@ dependencies = [ [[package]] name = "duplicate" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97af9b5f014e228b33e77d75ee0e6e87960124f0f4b16337b586a6bec91867b1" +checksum = "8e92f10a49176cbffacaedabfaa11d51db1ea0f80a83c26e1873b43cd1742c24" dependencies = [ "heck", "proc-macro2", @@ -517,11 +517,21 @@ dependencies = [ "oneshot", "pipewire", "rtrb", - "thiserror 2.0.17", + "thiserror 2.0.18", "windows", "zerocopy", ] +[[package]] +name = "interflow-core" +version = "0.1.0" +dependencies = [ + "bitflags 2.10.0", + "duplicate", + "thiserror 2.0.18", + "zerocopy", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.1" @@ -903,7 +913,6 @@ dependencies = [ "quote", "syn", "version_check", - "yansi", ] [[package]] @@ -1111,11 +1120,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.17" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "thiserror-impl 2.0.17", + "thiserror-impl 2.0.18", ] [[package]] @@ -1131,9 +1140,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.17" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", @@ -1587,12 +1596,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "yansi" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" - [[package]] name = "yansi-term" version = "0.1.2" @@ -1604,18 +1607,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.27" +version = "0.8.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" +checksum = "db6d35d663eadb6c932438e763b262fe1a70987f9ae936e60158176d710cae4a" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.27" +version = "0.8.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" +checksum = "4122cd3169e94605190e77839c9a40d40ed048d305bfdc146e7df40ab0f3e517" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 327d08e..5c5b7d5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,10 +1,20 @@ -[package] -name = "interflow" +[workspace] +resolver = "3" +members = ["crates/*", "."] + +[workspace.package] version = "0.1.0" edition = "2021" rust-version = "1.85" license = "MIT" +[package] +name = "interflow" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true + [dependencies] bitflags = "2.10.0" duplicate = "2.0.0" diff --git a/crates/interflow-core/Cargo.toml b/crates/interflow-core/Cargo.toml new file mode 100644 index 0000000..09259b6 --- /dev/null +++ b/crates/interflow-core/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "interflow-core" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true + +[dependencies] +bitflags = "2.10.0" +duplicate = "2.0.1" +thiserror = "2.0.18" +zerocopy = { version = "0.8.39", features = ["alloc"] } \ No newline at end of file diff --git a/crates/interflow-core/src/buffer.rs b/crates/interflow-core/src/buffer.rs new file mode 100644 index 0000000..bca0248 --- /dev/null +++ b/crates/interflow-core/src/buffer.rs @@ -0,0 +1,411 @@ +use std::num::NonZeroUsize; +use std::ops; +use zerocopy::FromZeros; + +/// Audio buffer type. Data is stored in a contiguous array, in non-interleaved format. +#[derive(Clone)] +pub struct AudioBuffer { + data: Box<[T]>, + frames: NonZeroUsize, +} + +impl ops::Index for AudioBuffer { + type Output = [T]; + fn index(&self, index: usize) -> &Self::Output { + self.channel(index) + } +} + +impl ops::IndexMut for AudioBuffer { + fn index_mut(&mut self, index: usize) -> &mut Self::Output { + &mut self.data[index * self.frames.get()..(index + 1) * self.frames.get()] + } +} + +impl ops::Index<(usize, usize)> for AudioBuffer { + type Output = T; + fn index(&self, (index, channel): (usize, usize)) -> &Self::Output { + &self.data[channel * self.frames.get() + index] + } +} + +impl ops::IndexMut<(usize, usize)> for AudioBuffer { + fn index_mut(&mut self, (index, channel): (usize, usize)) -> &mut Self::Output { + &mut self.data[channel * self.frames.get() + index] + } +} + +impl AudioBuffer +where + [T]: FromZeros, +{ + /// Creates a new buffer with the given number of frames and channels. The audio buffer will be zeroed out. + pub fn zeroed(frames: NonZeroUsize, channels: NonZeroUsize) -> Self { + let len = frames.get() * channels.get(); + AudioBuffer { + data: <[T] as FromZeros>::new_box_zeroed_with_elems(len).unwrap(), + frames, + } + } + + pub fn resize_channels(&mut self, channels: NonZeroUsize) { + let mut data = + <[T] as FromZeros>::new_box_zeroed_with_elems(channels.get() * self.frames.get()) + .unwrap(); + for channel in 0..channels.get() { + let old_channel = channel % self.channels(); + let old_data = + &self.data[old_channel * self.frames.get()..(old_channel + 1) * self.frames.get()]; + data[channel * self.frames.get()..(channel + 1) * self.frames.get()] + .copy_from_slice(old_data); + } + self.data = data; + } + + pub fn resize_frames(&mut self, frames: NonZeroUsize) { + let mut data = + <[T] as FromZeros>::new_box_zeroed_with_elems(frames.get() * self.channels()).unwrap(); + let min_frames = self.frames.min(frames).get(); + data[..min_frames * self.channels()] + .copy_from_slice(&self.data[..min_frames * self.channels()]); + self.frames = frames; + self.data = data; + } + + pub fn copy_to_interleaved(&self, out: &mut [T]) { + debug_assert!(out.len() >= self.channels() * self.frames()); + for (i, sample) in out.iter_mut().enumerate() { + let frame = i / self.channels(); + let channel = i % self.channels(); + let i = channel * self.frames.get() + frame; + *sample = self.data[i]; + } + } + + pub fn copy_from_interleaved(&mut self, data: &[T]) { + debug_assert!(data.len() <= self.channels() * self.frames()); + for (i, sample) in data.iter().enumerate() { + let frame = i / self.channels(); + let channel = i % self.channels(); + let i = channel * self.frames.get() + frame; + self.data[i] = *sample; + } + } + + pub fn as_ref(&self) -> AudioRef<'_, T> { + AudioRef { + buffer: self, + frame_slice: (0, self.frames.get()), + } + } + + pub fn as_mut(&mut self) -> AudioMut<'_, T> { + let end = self.frames.get(); + AudioMut { + buffer: self, + frame_slice: (0, end), + } + } +} + +#[derive(Debug, thiserror::Error)] +pub enum FromDataError { + #[error("Empty buffer")] + Empty, + #[error("Invalid number of channels: {channels} for buffer length {len} (len % channels) == {}", len % channels)] + InvalidChannelCount { len: usize, channels: usize }, + #[error("Invalid number of frames: {frames} for buffer length {len} (len % frames) == {}", len % frames)] + InvalidFrameCount { len: usize, frames: usize }, +} + +impl AudioBuffer { + pub fn from_fn( + channels: NonZeroUsize, + frames: NonZeroUsize, + f: impl Fn(usize, usize) -> T, + ) -> Self { + let mut data = Vec::with_capacity(channels.get() * frames.get()); + for channel in 0..channels.get() { + for frame in 0..frames.get() { + data.push(f(channel, frame)); + } + } + AudioBuffer { + data: data.into_boxed_slice(), + frames, + } + } + + pub fn from_data_channels( + data: Box<[T]>, + channels: NonZeroUsize, + ) -> Result { + if data.is_empty() { + return Err(FromDataError::Empty); + } + if data.len() % channels.get() != 0 { + return Err(FromDataError::InvalidChannelCount { + len: data.len(), + channels: channels.get(), + }); + } + + let frames = NonZeroUsize::new(data.len() / channels.get()).unwrap(); + Ok(AudioBuffer { data, frames }) + } + + pub fn from_data_frames(data: Box<[T]>, frames: NonZeroUsize) -> Result { + if data.is_empty() { + return Err(FromDataError::Empty); + } + if data.len() % frames.get() != 0 { + return Err(FromDataError::InvalidFrameCount { + len: data.len(), + frames: frames.get(), + }); + } + + Ok(AudioBuffer { data, frames }) + } + + pub fn frames(&self) -> usize { + self.frames.get() + } + + pub fn channels(&self) -> usize { + self.data.len() / self.frames.get() + } + + pub fn len(&self) -> usize { + self.data.len() + } + + pub fn is_empty(&self) -> bool { + self.data.is_empty() + } + + //noinspection ALL + #[duplicate::duplicate_item( + name reference(type) out; + [frame] [&'_ type] [FrameRef::<'_, T>]; + [frame_mut] [&'_ mut type] [FrameMut::<'_, T>]; + )] + pub fn name(self: reference([Self]), frame: usize) -> out { + debug_assert!(frame < self.frames()); + out { + buffer: self, + frame, + } + } + + #[duplicate::duplicate_item( + name reference(type); + [channel] [& type]; + [channel_mut] [&mut type]; + )] + pub fn name(self: reference([Self]), channel: usize) -> reference([[T]]) { + debug_assert!(channel < self.channels()); + reference([self.data[channel * self.frames.get()..(channel + 1) * self.frames.get()]]) + } + + // noinspection ALL + #[duplicate::duplicate_item( + name reference(type) out; + [slice] [& type] [AudioRef::<'_, T>]; + [slice_mut] [&mut type] [AudioMut::<'_, T>]; + )] + pub fn name(self: reference([Self]), index: impl ops::RangeBounds) -> out { + let begin = match index.start_bound() { + ops::Bound::Included(i) => *i, + ops::Bound::Excluded(i) => *i + 1, + ops::Bound::Unbounded => 0, + }; + let end = match index.end_bound() { + ops::Bound::Included(i) => *i - 1, + ops::Bound::Excluded(i) => *i, + ops::Bound::Unbounded => self.frames(), + }; + debug_assert!(begin <= end); + debug_assert!(end <= self.frames()); + out { + buffer: self, + frame_slice: (begin, end + 1), + } + } + + pub fn chunks(&self, size: usize) -> impl Iterator> { + (0..self.frames()) + .step_by(size) + .map(move |frame| self.slice(frame..(frame + size).min(self.frames()))) + } + + pub fn chunks_exact(&self, size: usize) -> impl Iterator> { + (0..self.frames()).step_by(size).filter_map(move |frame| { + let end = frame + size; + if end > self.frames() { + return None; + } + Some(self.slice(frame..end)) + }) + } + + pub fn windows(&self, size: usize) -> impl Iterator> { + (0..self.frames() - size).map(move |frame| self.slice(frame..(frame + size))) + } + + pub fn iter_frames(&self) -> impl Iterator> { + (0..self.frames()).map(move |frame| self.frame(frame)) + } + + pub fn iter_frames_mut(&mut self) -> impl Iterator> { + IterFramesMut { + buffer: self, + frame: 0, + } + } + + pub fn iter_channels(&self) -> impl Iterator { + self.data.chunks(self.frames.get()) + } + + pub fn iter_channels_mut(&mut self) -> impl Iterator { + self.data.chunks_mut(self.frames.get()) + } + + pub fn get_channels(&self, indices: [usize; N]) -> [&[T]; N] { + indices.map(|i| self.channel(i)) + } + + pub fn get_channels_mut(&mut self, indices: [usize; N]) -> [&mut [T]; N] { + self.data.get_disjoint_mut(indices.map(|i| i * self.frames.get()..(i + 1) * self.frames.get())).unwrap() + } +} + +#[duplicate::duplicate_item( +name reference(lifetime, type) derive; +[FrameRef] [&'lifetime type] [derive(Clone, Copy)]; +[FrameMut] [&'lifetime mut type] [derive()] +)] +#[derive] +pub struct name<'a, T> { + buffer: reference([a], [AudioBuffer]), + frame: usize, +} + +#[duplicate::duplicate_item( +name; +[FrameRef]; +[FrameMut]; +)] +impl name<'_, T> { + pub fn get(&self, channel: usize) -> T { + debug_assert!(channel < self.buffer.channels()); + self.buffer[channel][self.frame] + } + + pub fn get_frame(&self, out: &mut [T]) { + debug_assert!(out.len() >= self.buffer.channels()); + for (channel, value) in out.iter_mut().enumerate() { + *value = self.get(channel); + } + } +} + +impl FrameMut<'_, T> { + pub fn set(&mut self, channel: usize, value: T) { + debug_assert!(channel < self.buffer.channels()); + self.buffer[channel][self.frame] = value; + } + + pub fn set_frame(&mut self, data: &[T]) { + debug_assert!(data.len() >= self.buffer.channels()); + for (channel, value) in data.iter().enumerate() { + self.set(channel, *value); + } + } +} + +struct IterFramesMut<'a, T> { + buffer: &'a mut AudioBuffer, + frame: usize, +} + +impl<'a, T> Iterator for IterFramesMut<'a, T> { + type Item = FrameMut<'a, T>; + fn next(&mut self) -> Option { + if self.frame < self.buffer.frames() { + let frame = self.frame; + self.frame += 1; + // SAFETY: + // Lifetime of the frame is actually 'a, but the compiler cannot see that + unsafe { + let buffer_ptr = self.buffer as *mut AudioBuffer; + Some((*buffer_ptr).frame_mut(frame)) + } + } else { + None + } + } +} + +#[duplicate::duplicate_item( +name reference(lifetime, type) derive; +[AudioRef] [&'lifetime type] [derive(Clone, Copy)]; +[AudioMut] [&'lifetime mut type] [derive()] +)] +#[derive] +pub struct name<'a, T> { + buffer: reference([a], [AudioBuffer]), + frame_slice: (usize, usize), +} + +#[duplicate::duplicate_item( +name; +[AudioRef]; +[AudioMut]; +)] +impl ops::Index for name<'_, T> { + type Output = [T]; + + fn index(&self, index: usize) -> &Self::Output { + &self.buffer[self.frame_slice.0 + index] + } +} + +impl ops::IndexMut for AudioMut<'_, T> { + fn index_mut(&mut self, index: usize) -> &mut Self::Output { + &mut self.buffer[self.frame_slice.0 + index] + } +} + +#[duplicate::duplicate_item( +name; +[AudioRef]; +[AudioMut]; +)] +impl name<'_, T> { + pub fn frame(&self, index: usize) -> FrameRef<'_, T> { + self.buffer.frame(self.frame_slice.0 + index) + } + + pub fn channel(&self, channel: usize) -> &[T] { + let slice = self.buffer.channel(channel); + &slice[self.frame_slice.0..self.frame_slice.1] + } +} + +impl AudioMut<'_, T> { + pub fn frame_mut(&mut self, index: usize) -> FrameMut<'_, T> { + let frame = index + self.frame_slice.0; + debug_assert!(frame < self.frame_slice.1); + FrameMut { + buffer: self.buffer, + frame, + } + } + + pub fn channel_mut(&mut self, channel: usize) -> &mut [T] { + let slice = self.buffer.channel_mut(channel); + &mut slice[self.frame_slice.0..self.frame_slice.1] + } +} diff --git a/crates/interflow-core/src/device.rs b/crates/interflow-core/src/device.rs new file mode 100644 index 0000000..1faac6d --- /dev/null +++ b/crates/interflow-core/src/device.rs @@ -0,0 +1,135 @@ +use std::borrow::Cow; +use crate::DeviceType; +use crate::stream::{self, StreamHandle}; +use crate::traits::ExtensionProvider; + +/// Configuration for an audio stream. +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct StreamConfig { + /// Configured sample rate of the requested stream. The opened stream can have a different + /// sample rate, so don't rely on this parameter being correct at runtime. + pub sample_rate: f64, + /// Number of input channels requested + pub input_channels: usize, + /// Number of output channels requested + pub output_channels: usize, + /// Range of preferential buffer sizes, in units of audio samples per channel. + /// The library will make a best-effort attempt at honoring this setting, and in future versions + /// may provide additional buffering to ensure it, but for now you should not make assumptions + /// on buffer sizes based on this setting. + pub buffer_size_range: (Option, Option), + /// Whether the device should be exclusively held (meaning no other application can open the + /// same device). + pub exclusive: bool, +} + +impl StreamConfig { + /// Returns a [`DeviceType`] that describes this [`StreamConfig`]. Only [`DeviceType::INPUT`] and + /// [`DeviceType::OUTPUT`] are set. + pub fn requested_device_type(&self) -> DeviceType { + let mut ret = DeviceType::empty(); + ret.set(DeviceType::INPUT, self.input_channels > 0); + ret.set(DeviceType::OUTPUT, self.output_channels > 0); + ret + } + + /// Changes the [`StreamConfig`] such that it matches the configuration of a stream created with a device with + /// the given [`DeviceType`]. + /// + /// This method returns a copy of the input [`StreamConfig`]. + pub fn restrict(mut self, requested_type: DeviceType) -> Self { + if !requested_type.is_input() { + self.input_channels = 0; + } + if !requested_type.is_output() { + self.output_channels = 0; + } + self + } +} + +/// Configuration for an audio stream. +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct ResolvedStreamConfig { + /// Configured sample rate of the requested stream. The opened stream can have a different + /// sample rate, so don't rely on this parameter being correct at runtime. + pub sample_rate: f64, + /// Number of input channels requested + pub input_channels: usize, + /// Number of output channels requested + pub output_channels: usize, + /// Maximum number of frames the audio callback will receive + pub max_frame_count: usize, +} + +/// Trait for types describing audio devices. Audio devices have zero or more inputs and outputs, +/// and depending on the driver, can be duplex devices which can provide both of them at the same +/// time natively. +pub trait Device: ExtensionProvider { + type Error: Send + Sync + std::error::Error; + type StreamHandle: StreamHandle>; + + fn name(&self) -> Cow<'_, str>; + + fn device_type(&self) -> DeviceType; + + /// Default configuration for this device. If [`Ok`], should return a [`StreamConfig`] that is supported (i.e., + /// returns `true` when passed to [`Self::is_config_supported`]). + fn default_config(&self) -> Result; + + /// Returns the supported I/O buffer size range for the device. + fn buffer_size_range(&self) -> Result<(Option, Option), Self::Error> { + Ok((None, None)) + } + + /// Not all configuration values make sense for a particular device, and this method tests a + /// configuration to see if it can be used in an audio stream. + fn is_config_supported(&self, config: &StreamConfig) -> bool; + + /// Creates an output stream with the provided stream configuration. For this call to be + /// valid, [`AudioDevice::is_config_supported`] should have returned `true` on the provided + /// configuration. + /// + /// An output callback is required to process the audio, whose ownership will be transferred + /// to the audio stream. + fn create_stream( + &self, + stream_config: StreamConfig, + callback: Callback, + ) -> Result, Self::Error>; + + /// Create an output stream using the default configuration as returned by [`Self::default_output_config`]. + /// + /// # Arguments + /// + /// - `callback`: Output callback to generate audio data with. + fn default_stream( + &self, + requested_type: DeviceType, + callback: Callback, + ) -> Result, Self::Error> { + let config = self.default_config()?.restrict(requested_type); + debug_assert!( + self.is_config_supported(&config), + "Default configuration is not supported" + ); + self.create_stream(config, callback) + } +} + +/// Audio channel description. +#[derive(Debug, Clone)] +pub struct Channel<'a> { + /// Index of the channel in the device + pub index: usize, + /// Display the name for the channel, if available, else a generic name like "Channel 1" + pub name: Cow<'a, str>, +} + +pub trait NamedChannels { + fn channel_map(&self) -> impl Iterator>; +} + +pub trait ConfigurationList { + fn enumerate_configurations(&self) -> impl Iterator; +} diff --git a/crates/interflow-core/src/lib.rs b/crates/interflow-core/src/lib.rs new file mode 100644 index 0000000..0c5e465 --- /dev/null +++ b/crates/interflow-core/src/lib.rs @@ -0,0 +1,65 @@ +mod platform; +mod traits; +mod device; +mod stream; +mod proxies; +mod timing; +mod buffer; + +use bitflags::bitflags; + +bitflags! { + /// Represents the types/capabilities of an audio device. + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] + pub struct DeviceType: u32 { + /// Device supports audio input. + const INPUT = 1 << 0; + + /// Device supports audio output. + const OUTPUT = 1 << 1; + + /// Physical audio device (hardware). + const PHYSICAL = 1 << 2; + + /// Virtual/software application device. + const APPLICATION = 1 << 3; + + /// This device is set as default + const DEFAULT = 1 << 4; + + /// Device that supports both input and output. + const DUPLEX = Self::INPUT.bits() | Self::OUTPUT.bits(); + } +} + +impl DeviceType { + /// Returns true if this device type has the input capability. + pub fn is_input(&self) -> bool { + self.contains(Self::INPUT) + } + + /// Returns true if this device type has the output capability. + pub fn is_output(&self) -> bool { + self.contains(Self::OUTPUT) + } + + /// Returns true if this device type is a physical device. + pub fn is_physical(&self) -> bool { + self.contains(Self::PHYSICAL) + } + + /// Returns true if this device type is an application/virtual device. + pub fn is_application(&self) -> bool { + self.contains(Self::APPLICATION) + } + + /// Returns true if this device is set as default + pub fn is_default(&self) -> bool { + self.contains(Self::DEFAULT) + } + + /// Returns true if this device type supports both input and output. + pub fn is_duplex(&self) -> bool { + self.contains(Self::DUPLEX) + } +} diff --git a/crates/interflow-core/src/platform.rs b/crates/interflow-core/src/platform.rs new file mode 100644 index 0000000..a1b5f90 --- /dev/null +++ b/crates/interflow-core/src/platform.rs @@ -0,0 +1,19 @@ +use std::borrow::Cow; +use crate::device::Device; +use crate::DeviceType; +use crate::traits::ExtensionProvider; + +/// Trait for platforms which provide audio devices. +pub trait Platform: ExtensionProvider { + type Error: Send + Sync + std::error::Error; + type Device: Device>; + const NAME: &'static str; + + fn default_device(device_type: DeviceType) -> Result; + + fn list_devices(&self) -> Result, Self::Error>; +} + +pub trait ServerInfo { + fn version(&self) -> Cow<'_, str>; +} \ No newline at end of file diff --git a/crates/interflow-core/src/proxies.rs b/crates/interflow-core/src/proxies.rs new file mode 100644 index 0000000..3c9f5dc --- /dev/null +++ b/crates/interflow-core/src/proxies.rs @@ -0,0 +1,48 @@ +use std::borrow::Cow; +use crate::device::{Device, StreamConfig}; +use crate::DeviceType; +use crate::traits::ExtensionProvider; + +pub type Error = Box; + +pub trait DeviceProxy: ExtensionProvider { + fn name(&self) -> Cow<'_, str>; + fn device_type(&self) -> DeviceType; + fn default_config(&self) -> Result; + fn is_config_supported(&self, config: &StreamConfig) -> bool; + fn buffer_size_range(&self) -> Result<(Option, Option), Error>; +} + +impl DeviceProxy for D { + #[inline] + fn name(&self) -> Cow<'_, str> { + Device::name(self) + } + + fn device_type(&self) -> DeviceType { + Device::device_type(self) + } + + fn default_config(&self) -> Result { + Ok(Device::default_config(self)?) + } + + fn is_config_supported(&self, config: &StreamConfig) -> bool { + Device::is_config_supported(self, config) + } + + fn buffer_size_range(&self) -> Result<(Option, Option), Error> { + Ok(Device::buffer_size_range(self)?) + } +} + +pub trait IntoDeviceProxy { + fn into_device_proxy(self) -> Box; +} + +impl IntoDeviceProxy for D { + #[inline] + fn into_device_proxy(self) -> Box { + Box::new(self) + } +} diff --git a/crates/interflow-core/src/stream.rs b/crates/interflow-core/src/stream.rs new file mode 100644 index 0000000..29b9fe0 --- /dev/null +++ b/crates/interflow-core/src/stream.rs @@ -0,0 +1,78 @@ +use bitflags::bitflags; +use crate::buffer::{AudioMut, AudioRef}; +use crate::device::ResolvedStreamConfig; +use crate::timing::Timestamp; +use crate::traits::ExtensionProvider; + +pub trait StreamProxy: Send + Sync + ExtensionProvider {} + +bitflags! { + pub struct ChannelFlags: u32 { + const INACTIVE = 0x0000_0001; + const UNDERFLOW = 0x0000_0002; + const OVERFLOW = 0x0000_0004; + } +} + +pub trait StreamLatency { + fn input_latency(&self, channel: usize) -> Option; + fn output_latency(&self, channel: usize) -> Option; +} + +#[duplicate::duplicate_item( + name bufty; + [AudioInput] [AudioRef < 'a, T >]; + [AudioOutput] [AudioMut < 'a, T >]; +)] +/// Plain-old-data object holding references to the audio buffer and the associated time-keeping +/// [`Timestamp`]. This timestamp is associated with the stream, and in the cases where the +/// driver provides timing information, it is used instead of relying on sample-counting. +pub struct name<'a, T> { + /// Associated time stamp for this callback. The time represents the duration for which the + /// stream has been opened, and is either provided by the driver if available, or is kept up + /// manually by the library. + pub timestamp: Timestamp, + /// Audio buffer data. + pub buffer: bufty, + pub channel_flags: &'a [ChannelFlags], +} + +/// Plain-old-data object holding the passed-in stream configuration, as well as a general +/// callback timestamp, which can be different from the input and output streams in case of +/// cross-stream latencies; differences in timing can indicate desync. +pub struct CallbackContext<'a> { + /// Passed-in stream configuration. Values have been updated where necessary to correspond to + /// the actual stream properties. + pub stream_config: &'a ResolvedStreamConfig, + /// Callback-wide timestamp. + pub timestamp: Timestamp, + pub stream_proxy: &'a dyn StreamProxy, +} + +/// Trait for types which handles an audio stream (input or output). +pub trait StreamHandle { + /// Type of errors which have caused the stream to fail. + type Error: Send + std::error::Error; + + /// Eject the stream, returning ownership of the callback. + /// + /// An error can occur when an irrecoverable error has occured and ownership has been lost + /// already. + fn eject(self) -> Result; +} + +/// Trait of types which process audio data. This is the trait that users will want to +/// implement when processing audio from a device. +pub trait Callback: Send { + /// Prepare the audio callback to process audio. This function is *not* real-time safe (i.e., allocations can be + /// performed), in preparation for processing the stream with [`Self::process_audio`]. + fn prepare(&mut self, context: CallbackContext); + + /// Callback called when audio data can be processed. + fn process_audio( + &mut self, + context: CallbackContext, + input: AudioInput, + output: AudioOutput, + ); +} diff --git a/crates/interflow-core/src/timing.rs b/crates/interflow-core/src/timing.rs new file mode 100644 index 0000000..5b01269 --- /dev/null +++ b/crates/interflow-core/src/timing.rs @@ -0,0 +1,175 @@ +use std::ops; +use std::ops::AddAssign; +use std::sync::atomic::AtomicU64; +use std::time::Duration; + +/// Timestamp value, which computes duration information from a provided samplerate and a running +/// sample counter. +/// +/// You can update the timestamp by add-assigning sample counts to it: +/// +/// ```rust +/// use std::time::Duration; +/// use interflow::timestamp::Timestamp; +/// let mut ts = Timestamp::new(48000.); +/// assert_eq!(ts.as_duration(), Duration::from_nanos(0)); +/// ts += 48; +/// assert_eq!(ts.as_duration(), Duration::from_millis(1)); +/// ``` +/// +/// Adding also works, returning a new timestamp: +/// +/// ```rust +/// use std::time::Duration; +/// use interflow::timestamp::Timestamp; +/// let mut ts = Timestamp::new(48000.); +/// assert_eq!(ts.as_duration(), Duration::from_nanos(0)); +/// let ts2 = ts + 48; +/// assert_eq!(ts.as_duration(), Duration::from_millis(0)); +/// assert_eq!(ts2.as_duration(), Duration::from_millis(1)); +/// ``` +/// +/// Similarly, you can compute sample offsets by adding a [`Duration`] to it: +/// +/// ```rust +/// use std::time::Duration; +/// use interflow::timestamp::Timestamp; +/// let ts = Timestamp::from_count(48000., 48); +/// let ts_off = ts + Duration::from_millis(100); +/// assert_eq!(ts_off.as_duration(), Duration::from_millis(101)); +/// assert_eq!(ts_off.counter, 4848); +/// ``` +/// +/// Or simply construct a [`Timestamp`] from a specified duration: +/// +/// ```rust +/// use std::time::Duration; +/// use interflow::timestamp::Timestamp; +/// let ts = Timestamp::from_duration(44100., Duration::from_millis(1)); +/// assert_eq!(ts.counter, 44); // Note that the conversion is lossy, as only whole samples are +/// // stored in the timestamp. +/// ``` +#[derive(Debug, Copy, Clone, PartialEq)] +pub struct Timestamp { + /// Number of samples counted in this timestamp. + pub counter: u64, + /// Samplerate of the audio stream associated with the counter. + pub samplerate: f64, +} + +impl AddAssign for Timestamp { + fn add_assign(&mut self, rhs: Duration) { + let samples = rhs.as_secs_f64() * self.samplerate; + self.counter += samples as u64; + } +} + +impl AddAssign for Timestamp { + fn add_assign(&mut self, rhs: u64) { + self.counter += rhs; + } +} + +impl ops::Add for Timestamp +where + Self: AddAssign, +{ + type Output = Self; + + fn add(mut self, rhs: T) -> Self { + self.add_assign(rhs); + self + } +} + +impl Timestamp { + /// Create a zeroed timestamp with the provided sample rate. + pub fn new(samplerate: f64) -> Self { + Self { + counter: 0, + samplerate, + } + } + + /// Create a timestamp from the given sample rate and existing sample count. + pub fn from_count(samplerate: f64, counter: u64) -> Self { + Self { + samplerate, + counter, + } + } + + /// Compute the sample offset that most closely matches the provided duration for the given + /// sample rate. + pub fn from_duration(samplerate: f64, duration: Duration) -> Self { + Self::from_seconds(samplerate, duration.as_secs_f64()) + } + + /// Compute the sample offset that most closely matches the provided duration in seconds for + /// the given sample rate. + pub fn from_seconds(samplerate: f64, seconds: f64) -> Self { + let samples = samplerate * seconds; + Self { + samplerate, + counter: samples as _, + } + } + + /// Compute the number of seconds represented in this [`Timestamp`]. + pub fn as_seconds(&self) -> f64 { + self.counter as f64 / self.samplerate + } + + /// Compute the duration represented by this [`Timestamp`]. + pub fn as_duration(&self) -> Duration { + Duration::from_secs_f64(self.as_seconds()) + } +} + +/// Atomic version of [`Timestamp`] to be shared between threads. Mainly used by the [`crate::duplex`] module, but +/// may be useful in user code as well. +pub struct AtomicTimestamp { + samplerate: AtomicU64, + counter: AtomicU64, +} + +impl AtomicTimestamp { + /// Update the contents with the provided [`Timestamp`]. + pub fn update(&self, ts: Timestamp) { + self.samplerate.store( + ts.samplerate.to_bits(), + std::sync::atomic::Ordering::Relaxed, + ); + self.counter + .store(ts.counter, std::sync::atomic::Ordering::Relaxed); + } + + /// Load values and return them as a [`Timestamp`]. + pub fn as_timestamp(&self) -> Timestamp { + Timestamp { + samplerate: f64::from_bits(self.samplerate.load(std::sync::atomic::Ordering::Relaxed)), + counter: self.counter.load(std::sync::atomic::Ordering::Relaxed), + } + } + + /// Add the provided number of frames to this. + pub fn add_frames(&self, frames: u64) { + self.counter + .fetch_add(frames, std::sync::atomic::Ordering::Relaxed); + } +} + +impl From for AtomicTimestamp { + fn from(value: Timestamp) -> Self { + Self { + samplerate: AtomicU64::new(value.samplerate.to_bits()), + counter: AtomicU64::new(value.counter), + } + } +} + +impl From for Timestamp { + fn from(value: AtomicTimestamp) -> Self { + value.as_timestamp() + } +} diff --git a/crates/interflow-core/src/traits.rs b/crates/interflow-core/src/traits.rs new file mode 100644 index 0000000..74579f6 --- /dev/null +++ b/crates/interflow-core/src/traits.rs @@ -0,0 +1,100 @@ +use std::any::TypeId; +use std::marker::PhantomData; +use std::mem::MaybeUninit; + +/// A fully type-erased pointer, that can work with both thin and fat pointers. +/// Copied from . +#[derive(Copy, Clone)] +struct ErasedPtr { + value: MaybeUninit<[usize; 2]>, +} + +impl ErasedPtr { + /// Erase `ptr`. + fn new(ptr: *const T) -> Self { + let mut res = ErasedPtr { + value: MaybeUninit::zeroed(), + }; + + let len = size_of::<*const T>(); + assert!(len <= size_of::<[usize; 2]>()); + + let ptr_val = (&ptr) as *const *const T as *const u8; + let target = res.value.as_mut_ptr() as *mut u8; + // SAFETY: The target is valid for at least `len` bytes, and has no + // requirements on the value. + unsafe { + core::ptr::copy_nonoverlapping(ptr_val, target, len); + } + + res + } + + /// Convert the type erased pointer back into a pointer. + /// + /// # Safety + /// + /// The type `T` must be the same type as the one used with `new`. + unsafe fn as_ptr(&self) -> *const T { + // SAFETY: The constructor ensures that the first `size_of::()` + // bytes of `&self.value` are a valid `*const T` pointer. + unsafe { + core::mem::transmute_copy(&self.value) + } + } +} + +/// Type that can dynamically retrieve a type registered by an [`ExtensionProvider`] object. +/// Types are queried on-demand, every time an extension is requested. +/// +/// Consumers of [`ExtensionProvider`] should instead use [`ExtensionExt::lookup`]. +pub struct Selector<'a> { + __lifetime: PhantomData<&'a ()>, + target: TypeId, + found: Option, +} + +impl<'a> Selector<'a> { + pub(crate) const fn new() -> Self { + Self { + __lifetime: PhantomData, + target: TypeId::of::(), + found: None, + } + } + + pub fn register(&mut self, value: &T) -> &mut Self { + if self.target == TypeId::of::() { + self.found = Some(ErasedPtr::new(value)); + } + self + } + + pub(crate) fn finish(self) -> Option<&'a I> { + assert_eq!(self.target, TypeId::of::()); + Some(unsafe { &*self.found?.as_ptr() }) + } +} + +/// Trait for types that have optional data available on-demand. +pub trait ExtensionProvider: 'static { + /// Register on-demand types. Note that the implementation details around registering extensions mean that this + /// function will be called for every request. Runtime checks are expected, but this function should remain as fast + /// as possible. + fn register(&self, selector: &mut Selector<'_>) -> &mut Selector<'_>; +} + +const _EXTENSION_TRAIT_ASSERTS: () = { + const fn typeable() {} + typeable::(); +}; + +/// Additional extensions for [`ExtensionProvider`] objects. +pub trait ExtensionProviderExt: ExtensionProvider { + /// Look up [`T`] from the extension if it is registered. + fn lookup<'a, T: 'static + ?Sized>(&self) -> Option<&'a T> { + let mut selector = Selector::new::(); + self.register(&mut selector); + selector.finish::() + } +} \ No newline at end of file From 582825f4e965a62c3cfae72fab98dc487ab56eb1 Mon Sep 17 00:00:00 2001 From: Nathan Graule Date: Sun, 8 Feb 2026 21:51:38 +0100 Subject: [PATCH 14/38] wip: wasapi backend --- Cargo.lock | 144 +++++++++++++++++++---- Cargo.toml | 9 +- crates/interflow-core/Cargo.toml | 6 +- crates/interflow-core/src/device.rs | 6 +- crates/interflow-core/src/lib.rs | 14 +-- crates/interflow-core/src/traits.rs | 6 +- crates/interflow-wasapi/src/device.rs | 151 ++++++++++++++++++++++++ crates/interflow-wasapi/src/stream.rs | 6 + crates/interflow-wasapi/src/util.rs | 159 ++++++++++++++++++++++++++ 9 files changed, 464 insertions(+), 37 deletions(-) create mode 100644 crates/interflow-wasapi/src/device.rs create mode 100644 crates/interflow-wasapi/src/stream.rs create mode 100644 crates/interflow-wasapi/src/util.rs diff --git a/Cargo.lock b/Cargo.lock index 141d028..b063e5a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -518,7 +518,7 @@ dependencies = [ "pipewire", "rtrb", "thiserror 2.0.18", - "windows", + "windows 0.61.3", "zerocopy", ] @@ -532,6 +532,18 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "interflow-wasapi" +version = "0.1.0" +dependencies = [ + "bitflags 2.10.0", + "duplicate", + "interflow-core", + "thiserror 2.0.18", + "windows 0.62.2", + "zerocopy", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.1" @@ -1336,11 +1348,23 @@ version = "0.61.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" dependencies = [ - "windows-collections", - "windows-core", - "windows-future", - "windows-link", - "windows-numerics", + "windows-collections 0.2.0", + "windows-core 0.61.2", + "windows-future 0.2.1", + "windows-link 0.1.3", + "windows-numerics 0.2.0", +] + +[[package]] +name = "windows" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" +dependencies = [ + "windows-collections 0.3.2", + "windows-core 0.62.2", + "windows-future 0.3.2", + "windows-numerics 0.3.1", ] [[package]] @@ -1349,7 +1373,16 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" dependencies = [ - "windows-core", + "windows-core 0.61.2", +] + +[[package]] +name = "windows-collections" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" +dependencies = [ + "windows-core 0.62.2", ] [[package]] @@ -1360,9 +1393,22 @@ checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" dependencies = [ "windows-implement", "windows-interface", - "windows-link", - "windows-result", - "windows-strings", + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", ] [[package]] @@ -1371,16 +1417,27 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" dependencies = [ - "windows-core", - "windows-link", - "windows-threading", + "windows-core 0.61.2", + "windows-link 0.1.3", + "windows-threading 0.1.0", +] + +[[package]] +name = "windows-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" +dependencies = [ + "windows-core 0.62.2", + "windows-link 0.2.1", + "windows-threading 0.2.1", ] [[package]] name = "windows-implement" -version = "0.60.0" +version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", @@ -1389,9 +1446,9 @@ dependencies = [ [[package]] name = "windows-interface" -version = "0.59.1" +version = "0.59.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", @@ -1404,14 +1461,30 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + [[package]] name = "windows-numerics" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" dependencies = [ - "windows-core", - "windows-link", + "windows-core 0.61.2", + "windows-link 0.1.3", +] + +[[package]] +name = "windows-numerics" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" +dependencies = [ + "windows-core 0.62.2", + "windows-link 0.2.1", ] [[package]] @@ -1420,7 +1493,16 @@ version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" dependencies = [ - "windows-link", + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link 0.2.1", ] [[package]] @@ -1429,7 +1511,16 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" dependencies = [ - "windows-link", + "windows-link 0.1.3", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link 0.2.1", ] [[package]] @@ -1488,7 +1579,16 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" dependencies = [ - "windows-link", + "windows-link 0.1.3", +] + +[[package]] +name = "windows-threading" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" +dependencies = [ + "windows-link 0.2.1", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 5c5b7d5..f0dadc7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,13 @@ edition = "2021" rust-version = "1.85" license = "MIT" +[workspace.dependencies] +bitflags = "2.10.0" +duplicate = "2.0.1" +interflow-core = { path = "crates/interflow-core" } +thiserror = "2.0.18" + + [package] name = "interflow" version.workspace = true @@ -16,6 +23,7 @@ rust-version.workspace = true license.workspace = true [dependencies] +thiserror.workspace = true bitflags = "2.10.0" duplicate = "2.0.0" fixed-resample = "0.9.2" @@ -23,7 +31,6 @@ log = { version = "0.4.28", features = ["kv"] } ndarray = "0.16.1" oneshot = "0.1.11" rtrb = "0.3.2" -thiserror = "2.0.17" zerocopy = { version = "0.8.27", optional = true } [dev-dependencies] diff --git a/crates/interflow-core/Cargo.toml b/crates/interflow-core/Cargo.toml index 09259b6..a1a4e23 100644 --- a/crates/interflow-core/Cargo.toml +++ b/crates/interflow-core/Cargo.toml @@ -6,7 +6,7 @@ rust-version.workspace = true license.workspace = true [dependencies] -bitflags = "2.10.0" -duplicate = "2.0.1" -thiserror = "2.0.18" +bitflags.workspace = true +duplicate.workspace = true +thiserror.workspace = true zerocopy = { version = "0.8.39", features = ["alloc"] } \ No newline at end of file diff --git a/crates/interflow-core/src/device.rs b/crates/interflow-core/src/device.rs index 1faac6d..aaa1079 100644 --- a/crates/interflow-core/src/device.rs +++ b/crates/interflow-core/src/device.rs @@ -4,7 +4,7 @@ use crate::stream::{self, StreamHandle}; use crate::traits::ExtensionProvider; /// Configuration for an audio stream. -#[derive(Debug, Clone, Copy, PartialEq)] +#[derive(Debug, Clone, PartialEq)] pub struct StreamConfig { /// Configured sample rate of the requested stream. The opened stream can have a different /// sample rate, so don't rely on this parameter being correct at runtime. @@ -133,3 +133,7 @@ pub trait NamedChannels { pub trait ConfigurationList { fn enumerate_configurations(&self) -> impl Iterator; } + +pub trait DeviceState { + fn connected(&self) -> bool; +} \ No newline at end of file diff --git a/crates/interflow-core/src/lib.rs b/crates/interflow-core/src/lib.rs index 0c5e465..9beb874 100644 --- a/crates/interflow-core/src/lib.rs +++ b/crates/interflow-core/src/lib.rs @@ -1,10 +1,10 @@ -mod platform; -mod traits; -mod device; -mod stream; -mod proxies; -mod timing; -mod buffer; +pub mod platform; +pub mod traits; +pub mod device; +pub mod stream; +pub mod proxies; +pub mod timing; +pub mod buffer; use bitflags::bitflags; diff --git a/crates/interflow-core/src/traits.rs b/crates/interflow-core/src/traits.rs index 74579f6..35beb5f 100644 --- a/crates/interflow-core/src/traits.rs +++ b/crates/interflow-core/src/traits.rs @@ -81,7 +81,7 @@ pub trait ExtensionProvider: 'static { /// Register on-demand types. Note that the implementation details around registering extensions mean that this /// function will be called for every request. Runtime checks are expected, but this function should remain as fast /// as possible. - fn register(&self, selector: &mut Selector<'_>) -> &mut Selector<'_>; + fn register<'a, 'sel>(&'a self, selector: &'sel mut Selector<'a>) -> &'sel mut Selector<'a>; } const _EXTENSION_TRAIT_ASSERTS: () = { @@ -92,9 +92,9 @@ const _EXTENSION_TRAIT_ASSERTS: () = { /// Additional extensions for [`ExtensionProvider`] objects. pub trait ExtensionProviderExt: ExtensionProvider { /// Look up [`T`] from the extension if it is registered. - fn lookup<'a, T: 'static + ?Sized>(&self) -> Option<&'a T> { + fn lookup(&self) -> Option<&T> { let mut selector = Selector::new::(); - self.register(&mut selector); + { self.register(&mut selector); } selector.finish::() } } \ No newline at end of file diff --git a/crates/interflow-wasapi/src/device.rs b/crates/interflow-wasapi/src/device.rs new file mode 100644 index 0000000..f5f3a37 --- /dev/null +++ b/crates/interflow-wasapi/src/device.rs @@ -0,0 +1,151 @@ +use crate::util::MMDevice; +use interflow_core::device::{ResolvedStreamConfig, StreamConfig}; +use interflow_core::stream; +use interflow_core::traits::{ExtensionProvider, Selector}; +use interflow_core::{device, DeviceType}; +use std::borrow::Cow; +use windows::Win32::Media::Audio; +use windows::Win32::Media::Audio::{IAudioClient, IAudioClient3}; + +#[derive(Debug, Clone)] +pub struct Device { + pub(crate) handle: MMDevice, + pub(crate) device_type: DeviceType, +} + +impl ExtensionProvider for Device { + fn register<'a, 'sel>(&'a self, selector: &'sel mut Selector<'a>) -> &'sel mut Selector<'a> { + selector + } +} + +impl device::Device for Device { + type Error = crate::Error; + type StreamHandle = (); + + fn name(&self) -> Cow<'_, str> { + Cow::Owned(self.handle.name()) + } + + fn device_type(&self) -> DeviceType { + self.device_type + } + + fn default_config(&self) -> Result { + self.get_mix_format_iac3() + .or_else(|_| self.get_mix_format()) + } + + fn is_config_supported(&self, config: &StreamConfig) -> bool { + todo!() + } + + fn create_stream( + &self, + stream_config: StreamConfig, + callback: Callback, + ) -> Result, Self::Error> { + todo!() + } +} + +impl Device { + fn get_mix_format(&self) -> Result { + let client = self.handle.activate::()?; + let mix_format = unsafe { client.GetMixFormat() }?; + let format = unsafe { mix_format.read_unaligned() }; + let channels = format.nChannels as usize; + let input_channels = if self.device_type.is_input() { + channels + } else { + 0 + }; + let output_channels = if self.device_type.is_output() { + channels + } else { + 0 + }; + Ok(StreamConfig { + sample_rate: format.nSamplesPerSec as _, + input_channels, + output_channels, + buffer_size_range: (None, None), + exclusive: false, + }) + } + + fn get_mix_format_iac3(&self) -> Result { + let client = self.handle.activate::()?; + let mut period_default = 0u32; + let mut period_min = 0u32; + let mut period_max = 0u32; + let format = unsafe { client.GetMixFormat() }?; + unsafe { + let mut _fundamental_period = 0u32; + client.GetSharedModeEnginePeriod( + format.cast_const(), + &mut period_default, + &mut _fundamental_period, + &mut period_min, + &mut period_max, + )?; + } + let format = unsafe { format.read_unaligned() }; + let channels = format.nChannels as usize; + let input_channels = if self.device_type.is_input() { + channels + } else { + 0 + }; + let output_channels = if self.device_type.is_output() { + channels + } else { + 0 + }; + Ok(StreamConfig { + sample_rate: format.nSamplesPerSec as _, + input_channels, + output_channels, + buffer_size_range: (Some(period_min as usize), Some(period_max as usize)), + exclusive: false, + }) + } +} + +/// An iterable collection WASAPI devices. +pub(crate) struct DeviceList { + pub(crate) collection: Audio::IMMDeviceCollection, + pub(crate) total_count: u32, + pub(crate) next_item: u32, + pub(crate) device_type: DeviceType, +} + +unsafe impl Send for DeviceList {} + +unsafe impl Sync for DeviceList {} + +impl Iterator for DeviceList { + type Item = Device; + + fn next(&mut self) -> Option { + if self.next_item >= self.total_count { + return None; + } + + unsafe { + let device = self.collection.Item(self.next_item).unwrap(); + self.next_item += 1; + Some(Device { + handle: MMDevice::new(device), + device_type: self.device_type, + }) + } + } + + fn size_hint(&self) -> (usize, Option) { + let rest = (self.total_count - self.next_item) as usize; + (rest, Some(rest)) + } +} + +impl ExactSizeIterator for DeviceList {} diff --git a/crates/interflow-wasapi/src/stream.rs b/crates/interflow-wasapi/src/stream.rs new file mode 100644 index 0000000..ecd61ea --- /dev/null +++ b/crates/interflow-wasapi/src/stream.rs @@ -0,0 +1,6 @@ +use windows::Win32::Media::Audio::IAudioClient; +use crate::util::CoTask; + +struct Handle { + audio_client: CoTask, +} \ No newline at end of file diff --git a/crates/interflow-wasapi/src/util.rs b/crates/interflow-wasapi/src/util.rs new file mode 100644 index 0000000..0729b3a --- /dev/null +++ b/crates/interflow-wasapi/src/util.rs @@ -0,0 +1,159 @@ +use std::ffi::OsString; +use std::marker::PhantomData; +use std::ops; +use std::os::windows::ffi::OsStringExt; +use std::ptr::{self, NonNull}; +use std::sync::OnceLock; +use windows::core::Interface; +use windows::Win32::Devices::Properties; +use windows::Win32::Media::Audio; +use windows::Win32::System::Com::{self, CoTaskMemFree, CLSCTX, COINIT_MULTITHREADED, CoInitializeEx, CoUninitialize, StructuredStorage, STGM_READ}; +use windows::Win32::System::Variant::VT_LPWSTR; + +/// RAII object that guards the fact that COM is initialized. +/// +// We store a raw pointer because it's the only way at the moment to remove `Send`/`Sync` from the +// object. +struct ComponentObjectModel(PhantomData<()>); + +impl ComponentObjectModel { + pub unsafe fn create_instance< + P1: windows::core::Param, + T: Interface, + >( + &self, + guid: *const windows::core::GUID, + param1: P1, + class_context: CLSCTX, + ) -> windows::core::Result { + Com::CoCreateInstance(guid, param1, class_context) + } +} + +impl Drop for ComponentObjectModel { + #[inline] + fn drop(&mut self) { + unsafe { CoUninitialize() }; + } +} + +/// Ensures that COM is initialized in this thread. +#[inline] +pub fn com() -> windows::core::Result<&'static ComponentObjectModel> { + static VALUE: OnceLock = OnceLock::new(); + let Some(value) = VALUE.get() else { + unsafe { + CoInitializeEx(None, COINIT_MULTITHREADED).ok()?; + } + let _ = VALUE.set(ComponentObjectModel(PhantomData)); + return Ok(VALUE.get().unwrap()); + }; + Ok(value) +} + +#[derive(Debug, Clone)] +pub struct MMDevice(Audio::IMMDevice); + +unsafe impl Send for MMDevice {} + +impl MMDevice { + pub(crate) fn new(device: Audio::IMMDevice) -> Self { + Self(device) + } + + pub(crate) fn activate(&self) -> Result { + unsafe { + self.0 + .Activate::(Com::CLSCTX_ALL, None) + .map_err(crate::Error::BackendError) + } + } + + pub(crate) fn name(&self) -> String { + get_device_name(&self.0) + } +} + +fn get_device_name(device: &Audio::IMMDevice) -> String { + unsafe { + // Open the device's property store. + let property_store = device + .OpenPropertyStore(STGM_READ) + .expect("could not open property store"); + + // Get the endpoint's friendly-name property, else the interface's friendly-name, else the device description. + let mut property_value = property_store + .GetValue(&Properties::DEVPKEY_Device_FriendlyName as *const _ as *const _) + .or(property_store.GetValue( + &Properties::DEVPKEY_DeviceInterface_FriendlyName as *const _ as *const _, + )) + .or(property_store + .GetValue(&Properties::DEVPKEY_Device_DeviceDesc as *const _ as *const _)) + .unwrap(); + + let prop_variant = &property_value.Anonymous.Anonymous; + + // Read the friendly-name from the union data field, expecting a *const u16. + assert_eq!(VT_LPWSTR, prop_variant.vt); + + let ptr_utf16 = *(&prop_variant.Anonymous as *const _ as *const *const u16); + + // Find the length of the friendly name. + let mut len = 0; + while *ptr_utf16.offset(len) != 0 { + len += 1; + } + + // Convert to a string. + let name_slice = std::slice::from_raw_parts(ptr_utf16, len as usize); + let name_os_string: OsString = OsStringExt::from_wide(name_slice); + let name = name_os_string + .into_string() + .unwrap_or_else(|os_string| os_string.to_string_lossy().into()); + + // Clean up. + StructuredStorage::PropVariantClear(&mut property_value).unwrap(); + + name + } +} + +#[repr(transparent)] +pub(super) struct CoTask { + ptr: NonNull, +} + +impl ops::Deref for CoTask { + type Target = NonNull; + fn deref(&self) -> &Self::Target { + &self.ptr + } +} + +impl ops::DerefMut for CoTask { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.ptr + } +} + +impl Drop for CoTask { + fn drop(&mut self) { + unsafe { + CoTaskMemFree(Some(self.ptr.as_ptr().cast())); + } + } +} + +impl CoTask { + pub(super) const unsafe fn new(ptr: NonNull) -> Self { + Self { ptr } + } + + pub(super) unsafe fn construct(func: impl FnOnce(*mut *mut T) -> bool) -> Option { + let mut ptr = ptr::null_mut(); + if !func(&mut ptr) { + return None; + } + NonNull::new(ptr).map(|ptr| Self { ptr }) + } +} From 5dfaf3cea2f9ca7d3fc9e17fe0feedb8b8570dad Mon Sep 17 00:00:00 2001 From: Nathan Graule Date: Mon, 9 Feb 2026 22:27:39 +0100 Subject: [PATCH 15/38] fix(wasapi): forgot unversioned files --- crates/interflow-wasapi/Cargo.toml | 26 +++++ crates/interflow-wasapi/src/lib.rs | 162 +++++++++++++++++++++++++++++ 2 files changed, 188 insertions(+) create mode 100644 crates/interflow-wasapi/Cargo.toml create mode 100644 crates/interflow-wasapi/src/lib.rs diff --git a/crates/interflow-wasapi/Cargo.toml b/crates/interflow-wasapi/Cargo.toml new file mode 100644 index 0000000..8b8f599 --- /dev/null +++ b/crates/interflow-wasapi/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "interflow-wasapi" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true + +[dependencies] +bitflags.workspace = true +duplicate.workspace = true +interflow-core.workspace = true +thiserror.workspace = true +windows = { version = "0.62.2", features = [ + "Win32_Media_Audio", + "Win32_Foundation", + "Win32_Devices_Properties", + "Win32_Media_KernelStreaming", + "Win32_System_Com_StructuredStorage", + "Win32_System_Threading", + "Win32_Security", + "Win32_System_SystemServices", + "Win32_System_Variant", + "Win32_Media_Multimedia", + "Win32_UI_Shell_PropertiesSystem", +] } +zerocopy = { version = "0.8.39", features = ["std"] } diff --git a/crates/interflow-wasapi/src/lib.rs b/crates/interflow-wasapi/src/lib.rs new file mode 100644 index 0000000..9357430 --- /dev/null +++ b/crates/interflow-wasapi/src/lib.rs @@ -0,0 +1,162 @@ +pub mod device; +mod util; +mod stream; + +use std::sync::OnceLock; +use bitflags::bitflags_match; +use windows::Win32::Media::Audio; +use windows::Win32::System::Com; +use device::Device; +use interflow_core::{platform, DeviceType}; +use interflow_core::traits::{ExtensionProvider, Selector}; +use crate::util::MMDevice; + +#[derive(Debug, thiserror::Error)] +pub enum Error { + /// Error originating from WASAPI. + #[error("{} (code {})", .0.message(), .0.code())] + BackendError(#[from] windows::core::Error), + /// Requested WASAPI device configuration is not available + #[error("Configuration not available")] + ConfigurationNotAvailable, + /// Windows Foundation error + #[error("Win32 error: {0}")] + FoundationError(String), + /// Duplex stream requested, unsupported by WASAPI + #[error("Unsupported duplex stream requested")] + DuplexStreamRequested, +} + +#[derive(Debug, Copy, Clone)] +pub struct Platform; + +impl ExtensionProvider for Platform { + fn register<'a, 'sel>(&'a self, selector: &'sel mut Selector<'a>) -> &'sel mut Selector<'a> { + selector.register::(self) + } +} + +impl platform::Platform for Platform { + type Error = Error; + type Device = Device; + const NAME: &'static str = ""; + + fn default_device(device_type: DeviceType) -> Result { + let Some(device) = audio_device_enumerator().get_default_device(device_type)? else { + return Err(Error::ConfigurationNotAvailable); + }; + Ok(device) + } + + fn list_devices(&self) -> Result, Self::Error> { + audio_device_enumerator().get_device_list() + } +} + +pub trait DefaultByRole { + fn default_by_role(&self, flow: Audio::EDataFlow, role: Audio::ERole) -> Result; +} + +impl DefaultByRole for Platform { + fn default_by_role(&self, flow: Audio::EDataFlow, role: Audio::ERole) -> Result { + audio_device_enumerator().get_default_device_with_role(flow, role) + } +} + +fn audio_device_enumerator() -> &'static AudioDeviceEnumerator { + static ENUMERATOR: OnceLock = OnceLock::new(); + ENUMERATOR.get_or_init(|| { + // Make sure COM is initialised. + let com = util::com().unwrap(); + + unsafe { + let enumerator = com.create_instance::<_, Audio::IMMDeviceEnumerator>( + &Audio::MMDeviceEnumerator, + None, + Com::CLSCTX_ALL, + ).unwrap(); + + AudioDeviceEnumerator(enumerator) + } + }) +} + +/// Send/Sync wrapper around `IMMDeviceEnumerator`. +pub struct AudioDeviceEnumerator(Audio::IMMDeviceEnumerator); + +impl AudioDeviceEnumerator { + // Returns the default output device. + fn get_default_device( + &self, + device_type: DeviceType, + ) -> Result, Error> { + let Some(flow) = bitflags_match!(device_type, { + DeviceType::INPUT | DeviceType::PHYSICAL => Some(Audio::eCapture), + DeviceType::OUTPUT | DeviceType::PHYSICAL => Some(Audio::eRender), + _ => None, + }) else { + return Ok(None); + }; + + self.get_default_device_with_role(flow, Audio::eConsole) + .map(Some) + } + + fn get_default_device_with_role( + &self, + flow: Audio::EDataFlow, + role: Audio::ERole, + ) -> Result { + unsafe { + let device = self.0.GetDefaultAudioEndpoint(flow, role)?; + let device_type = match flow { + Audio::eRender => DeviceType::OUTPUT, + _ => DeviceType::INPUT, + }; + Ok(Device { + handle: MMDevice::new(device), + device_type: DeviceType::PHYSICAL | device_type, + }) + } + } + + // Returns a chained iterator of output and input devices. + fn get_device_list( + &self, + ) -> Result, Error> { + // Create separate collections for output and input devices and then chain them. + unsafe { + let output_collection = self + .0 + .EnumAudioEndpoints(Audio::eRender, Audio::DEVICE_STATE_ACTIVE)?; + + let count = output_collection.GetCount()?; + + let output_device_list = device::DeviceList { + collection: output_collection, + total_count: count, + next_item: 0, + device_type: DeviceType::OUTPUT, + }; + + let input_collection = self + .0 + .EnumAudioEndpoints(Audio::eCapture, Audio::DEVICE_STATE_ACTIVE)?; + + let count = input_collection.GetCount()?; + + let input_device_list = device::DeviceList { + collection: input_collection, + total_count: count, + next_item: 0, + device_type: DeviceType::INPUT, + }; + + Ok(output_device_list.chain(input_device_list)) + } + } +} + +unsafe impl Send for AudioDeviceEnumerator {} + +unsafe impl Sync for AudioDeviceEnumerator {} From 09e235a57c699dba2ddb9c44a8bc9effab020d73 Mon Sep 17 00:00:00 2001 From: Nathan Graule Date: Fri, 13 Feb 2026 22:22:15 +0100 Subject: [PATCH 16/38] chore: formatting/linting Signed-off-by: Nathan Graule --- crates/interflow-core/src/buffer.rs | 4 ++- crates/interflow-core/src/device.rs | 10 ++++---- crates/interflow-core/src/lib.rs | 8 +++--- crates/interflow-core/src/platform.rs | 10 ++++---- crates/interflow-core/src/proxies.rs | 10 ++++---- crates/interflow-core/src/stream.rs | 2 +- crates/interflow-core/src/traits.rs | 10 ++++---- crates/interflow-wasapi/src/lib.rs | 35 ++++++++++++--------------- crates/interflow-wasapi/src/stream.rs | 4 +-- crates/interflow-wasapi/src/util.rs | 5 +++- 10 files changed, 50 insertions(+), 48 deletions(-) diff --git a/crates/interflow-core/src/buffer.rs b/crates/interflow-core/src/buffer.rs index bca0248..b013e93 100644 --- a/crates/interflow-core/src/buffer.rs +++ b/crates/interflow-core/src/buffer.rs @@ -277,7 +277,9 @@ impl AudioBuffer { } pub fn get_channels_mut(&mut self, indices: [usize; N]) -> [&mut [T]; N] { - self.data.get_disjoint_mut(indices.map(|i| i * self.frames.get()..(i + 1) * self.frames.get())).unwrap() + self.data + .get_disjoint_mut(indices.map(|i| i * self.frames.get()..(i + 1) * self.frames.get())) + .unwrap() } } diff --git a/crates/interflow-core/src/device.rs b/crates/interflow-core/src/device.rs index aaa1079..fe74707 100644 --- a/crates/interflow-core/src/device.rs +++ b/crates/interflow-core/src/device.rs @@ -1,7 +1,7 @@ -use std::borrow::Cow; -use crate::DeviceType; use crate::stream::{self, StreamHandle}; use crate::traits::ExtensionProvider; +use crate::DeviceType; +use std::borrow::Cow; /// Configuration for an audio stream. #[derive(Debug, Clone, PartialEq)] @@ -68,9 +68,9 @@ pub struct ResolvedStreamConfig { pub trait Device: ExtensionProvider { type Error: Send + Sync + std::error::Error; type StreamHandle: StreamHandle>; - + fn name(&self) -> Cow<'_, str>; - + fn device_type(&self) -> DeviceType; /// Default configuration for this device. If [`Ok`], should return a [`StreamConfig`] that is supported (i.e., @@ -136,4 +136,4 @@ pub trait ConfigurationList { pub trait DeviceState { fn connected(&self) -> bool; -} \ No newline at end of file +} diff --git a/crates/interflow-core/src/lib.rs b/crates/interflow-core/src/lib.rs index 9beb874..4271eda 100644 --- a/crates/interflow-core/src/lib.rs +++ b/crates/interflow-core/src/lib.rs @@ -1,10 +1,10 @@ -pub mod platform; -pub mod traits; +pub mod buffer; pub mod device; -pub mod stream; +pub mod platform; pub mod proxies; +pub mod stream; pub mod timing; -pub mod buffer; +pub mod traits; use bitflags::bitflags; diff --git a/crates/interflow-core/src/platform.rs b/crates/interflow-core/src/platform.rs index a1b5f90..26b2ef5 100644 --- a/crates/interflow-core/src/platform.rs +++ b/crates/interflow-core/src/platform.rs @@ -1,19 +1,19 @@ -use std::borrow::Cow; use crate::device::Device; -use crate::DeviceType; use crate::traits::ExtensionProvider; +use crate::DeviceType; +use std::borrow::Cow; /// Trait for platforms which provide audio devices. pub trait Platform: ExtensionProvider { type Error: Send + Sync + std::error::Error; type Device: Device>; const NAME: &'static str; - + fn default_device(device_type: DeviceType) -> Result; - + fn list_devices(&self) -> Result, Self::Error>; } pub trait ServerInfo { fn version(&self) -> Cow<'_, str>; -} \ No newline at end of file +} diff --git a/crates/interflow-core/src/proxies.rs b/crates/interflow-core/src/proxies.rs index 3c9f5dc..b49eab3 100644 --- a/crates/interflow-core/src/proxies.rs +++ b/crates/interflow-core/src/proxies.rs @@ -1,7 +1,7 @@ -use std::borrow::Cow; use crate::device::{Device, StreamConfig}; -use crate::DeviceType; use crate::traits::ExtensionProvider; +use crate::DeviceType; +use std::borrow::Cow; pub type Error = Box; @@ -18,11 +18,11 @@ impl DeviceProxy for D { fn name(&self) -> Cow<'_, str> { Device::name(self) } - + fn device_type(&self) -> DeviceType { Device::device_type(self) } - + fn default_config(&self) -> Result { Ok(Device::default_config(self)?) } @@ -30,7 +30,7 @@ impl DeviceProxy for D { fn is_config_supported(&self, config: &StreamConfig) -> bool { Device::is_config_supported(self, config) } - + fn buffer_size_range(&self) -> Result<(Option, Option), Error> { Ok(Device::buffer_size_range(self)?) } diff --git a/crates/interflow-core/src/stream.rs b/crates/interflow-core/src/stream.rs index 29b9fe0..af23efb 100644 --- a/crates/interflow-core/src/stream.rs +++ b/crates/interflow-core/src/stream.rs @@ -1,8 +1,8 @@ -use bitflags::bitflags; use crate::buffer::{AudioMut, AudioRef}; use crate::device::ResolvedStreamConfig; use crate::timing::Timestamp; use crate::traits::ExtensionProvider; +use bitflags::bitflags; pub trait StreamProxy: Send + Sync + ExtensionProvider {} diff --git a/crates/interflow-core/src/traits.rs b/crates/interflow-core/src/traits.rs index 35beb5f..cc65509 100644 --- a/crates/interflow-core/src/traits.rs +++ b/crates/interflow-core/src/traits.rs @@ -38,9 +38,7 @@ impl ErasedPtr { unsafe fn as_ptr(&self) -> *const T { // SAFETY: The constructor ensures that the first `size_of::()` // bytes of `&self.value` are a valid `*const T` pointer. - unsafe { - core::mem::transmute_copy(&self.value) - } + unsafe { core::mem::transmute_copy(&self.value) } } } @@ -94,7 +92,9 @@ pub trait ExtensionProviderExt: ExtensionProvider { /// Look up [`T`] from the extension if it is registered. fn lookup(&self) -> Option<&T> { let mut selector = Selector::new::(); - { self.register(&mut selector); } + { + self.register(&mut selector); + } selector.finish::() } -} \ No newline at end of file +} diff --git a/crates/interflow-wasapi/src/lib.rs b/crates/interflow-wasapi/src/lib.rs index 9357430..94dfeb7 100644 --- a/crates/interflow-wasapi/src/lib.rs +++ b/crates/interflow-wasapi/src/lib.rs @@ -1,15 +1,15 @@ pub mod device; -mod util; mod stream; +mod util; -use std::sync::OnceLock; +use crate::util::MMDevice; use bitflags::bitflags_match; -use windows::Win32::Media::Audio; -use windows::Win32::System::Com; use device::Device; -use interflow_core::{platform, DeviceType}; use interflow_core::traits::{ExtensionProvider, Selector}; -use crate::util::MMDevice; +use interflow_core::{platform, DeviceType}; +use std::sync::OnceLock; +use windows::Win32::Media::Audio; +use windows::Win32::System::Com; #[derive(Debug, thiserror::Error)] pub enum Error { @@ -48,7 +48,7 @@ impl platform::Platform for Platform { Ok(device) } - fn list_devices(&self) -> Result, Self::Error> { + fn list_devices(&self) -> Result, Self::Error> { audio_device_enumerator().get_device_list() } } @@ -70,11 +70,13 @@ fn audio_device_enumerator() -> &'static AudioDeviceEnumerator { let com = util::com().unwrap(); unsafe { - let enumerator = com.create_instance::<_, Audio::IMMDeviceEnumerator>( - &Audio::MMDeviceEnumerator, - None, - Com::CLSCTX_ALL, - ).unwrap(); + let enumerator = com + .create_instance::<_, Audio::IMMDeviceEnumerator>( + &Audio::MMDeviceEnumerator, + None, + Com::CLSCTX_ALL, + ) + .unwrap(); AudioDeviceEnumerator(enumerator) } @@ -86,10 +88,7 @@ pub struct AudioDeviceEnumerator(Audio::IMMDeviceEnumerator); impl AudioDeviceEnumerator { // Returns the default output device. - fn get_default_device( - &self, - device_type: DeviceType, - ) -> Result, Error> { + fn get_default_device(&self, device_type: DeviceType) -> Result, Error> { let Some(flow) = bitflags_match!(device_type, { DeviceType::INPUT | DeviceType::PHYSICAL => Some(Audio::eCapture), DeviceType::OUTPUT | DeviceType::PHYSICAL => Some(Audio::eRender), @@ -121,9 +120,7 @@ impl AudioDeviceEnumerator { } // Returns a chained iterator of output and input devices. - fn get_device_list( - &self, - ) -> Result, Error> { + fn get_device_list(&self) -> Result, Error> { // Create separate collections for output and input devices and then chain them. unsafe { let output_collection = self diff --git a/crates/interflow-wasapi/src/stream.rs b/crates/interflow-wasapi/src/stream.rs index ecd61ea..8208e57 100644 --- a/crates/interflow-wasapi/src/stream.rs +++ b/crates/interflow-wasapi/src/stream.rs @@ -1,6 +1,6 @@ -use windows::Win32::Media::Audio::IAudioClient; use crate::util::CoTask; +use windows::Win32::Media::Audio::IAudioClient; struct Handle { audio_client: CoTask, -} \ No newline at end of file +} diff --git a/crates/interflow-wasapi/src/util.rs b/crates/interflow-wasapi/src/util.rs index 0729b3a..5062cfe 100644 --- a/crates/interflow-wasapi/src/util.rs +++ b/crates/interflow-wasapi/src/util.rs @@ -7,7 +7,10 @@ use std::sync::OnceLock; use windows::core::Interface; use windows::Win32::Devices::Properties; use windows::Win32::Media::Audio; -use windows::Win32::System::Com::{self, CoTaskMemFree, CLSCTX, COINIT_MULTITHREADED, CoInitializeEx, CoUninitialize, StructuredStorage, STGM_READ}; +use windows::Win32::System::Com::{ + self, CoInitializeEx, CoTaskMemFree, CoUninitialize, StructuredStorage, CLSCTX, + COINIT_MULTITHREADED, STGM_READ, +}; use windows::Win32::System::Variant::VT_LPWSTR; /// RAII object that guards the fact that COM is initialized. From 93137fb07ccdf498e3a87074e3272388da9a63b3 Mon Sep 17 00:00:00 2001 From: Nathan Graule Date: Fri, 13 Feb 2026 22:22:40 +0100 Subject: [PATCH 17/38] refactor: alias wasapi backend pointing to interflow_wasapi Signed-off-by: Nathan Graule --- Cargo.lock | 78 +++++++++++++++++++++++++++------------------ Cargo.toml | 43 ++++++++++++++----------- src/backends/mod.rs | 4 +-- 3 files changed, 74 insertions(+), 51 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 044dff6..b678ae2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -516,6 +516,8 @@ dependencies = [ "env_logger", "fixed-resample", "indicatif", + "interflow-core", + "interflow-wasapi", "libc", "libspa", "libspa-sys", @@ -526,6 +528,27 @@ dependencies = [ "pipewire", "rtrb", "thiserror 2.0.18", + "zerocopy", +] + +[[package]] +name = "interflow-core" +version = "0.1.0" +dependencies = [ + "bitflags 2.10.0", + "duplicate", + "thiserror 2.0.18", + "zerocopy", +] + +[[package]] +name = "interflow-wasapi" +version = "0.1.0" +dependencies = [ + "bitflags 2.10.0", + "duplicate", + "interflow-core", + "thiserror 2.0.18", "windows", "zerocopy", ] @@ -613,7 +636,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" dependencies = [ "cfg-if", - "windows-link 0.2.1", + "windows-link", ] [[package]] @@ -1345,47 +1368,46 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows" -version = "0.61.3" +version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" dependencies = [ "windows-collections", "windows-core", "windows-future", - "windows-link 0.1.3", "windows-numerics", ] [[package]] name = "windows-collections" -version = "0.2.0" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" dependencies = [ "windows-core", ] [[package]] name = "windows-core" -version = "0.61.2" +version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ "windows-implement", "windows-interface", - "windows-link 0.1.3", + "windows-link", "windows-result", "windows-strings", ] [[package]] name = "windows-future" -version = "0.2.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" dependencies = [ "windows-core", - "windows-link 0.1.3", + "windows-link", "windows-threading", ] @@ -1411,12 +1433,6 @@ dependencies = [ "syn", ] -[[package]] -name = "windows-link" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" - [[package]] name = "windows-link" version = "0.2.1" @@ -1425,30 +1441,30 @@ checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] name = "windows-numerics" -version = "0.2.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" dependencies = [ "windows-core", - "windows-link 0.1.3", + "windows-link", ] [[package]] name = "windows-result" -version = "0.3.4" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" dependencies = [ - "windows-link 0.1.3", + "windows-link", ] [[package]] name = "windows-strings" -version = "0.4.2" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" dependencies = [ - "windows-link 0.1.3", + "windows-link", ] [[package]] @@ -1457,16 +1473,16 @@ version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ - "windows-link 0.2.1", + "windows-link", ] [[package]] name = "windows-threading" -version = "0.1.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" dependencies = [ - "windows-link 0.1.3", + "windows-link", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 58fb043..240b2f8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,19 +1,38 @@ -[package] -name = "interflow" +[workspace] +resolver = "3" +members = ["crates/*", "."] +default-members = ["."] + +[workspace.package] version = "0.1.0" edition = "2021" rust-version = "1.85" license = "MIT" -[dependencies] +[workspace.dependencies] bitflags = "2.10.0" duplicate = "2.0.1" +interflow-core = { path = "crates/interflow-core" } +log = "0.4.29" +thiserror = "2.0.18" + +[package] +name = "interflow" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true + +[dependencies] +bitflags.workspace = true +duplicate.workspace = true fixed-resample = "0.9.2" -log = { version = "0.4.29", features = ["kv"] } +interflow-core.workspace = true +log = { workspace = true, features = ["kv"] } ndarray = "0.17.1" oneshot = "0.1.11" rtrb = "0.3.2" -thiserror = "2.0.17" +thiserror.workspace = true zerocopy = { version = "0.8.39", optional = true } [dev-dependencies] @@ -40,19 +59,7 @@ coreaudio-rs = "0.13.0" coreaudio-sys = "0.2.17" [target.'cfg(target_os = "windows")'.dependencies] -windows = { version = "0.61.3", features = [ - "Win32_Media_Audio", - "Win32_Foundation", - "Win32_Devices_Properties", - "Win32_Media_KernelStreaming", - "Win32_System_Com_StructuredStorage", - "Win32_System_Threading", - "Win32_Security", - "Win32_System_SystemServices", - "Win32_System_Variant", - "Win32_Media_Multimedia", - "Win32_UI_Shell_PropertiesSystem", -] } +interflow-wasapi = { path = "crates/interflow-wasapi" } [[example]] name = "eject_stream_pipewire" diff --git a/src/backends/mod.rs b/src/backends/mod.rs index 78ab3cf..8e87457 100644 --- a/src/backends/mod.rs +++ b/src/backends/mod.rs @@ -16,8 +16,8 @@ pub mod alsa; #[cfg(os_coreaudio)] pub mod coreaudio; -#[cfg(os_wasapi)] -pub mod wasapi; +#[cfg(target_os = "windows")] +pub use interflow_wasapi as wasapi; #[cfg(all(os_pipewire, feature = "pipewire"))] pub mod pipewire; From 73c6b69ab3b38467656de463672dad09c6c97533 Mon Sep 17 00:00:00 2001 From: Nathan Graule Date: Wed, 22 Jul 2026 00:03:37 +0200 Subject: [PATCH 18/38] wip: coreaudio v2 implementation --- Cargo.lock | 740 +----------------- Cargo.toml | 59 +- crates/interflow-core/src/buffer.rs | 6 + crates/interflow-core/src/platform.rs | 2 +- crates/interflow-core/src/proxies.rs | 106 ++- crates/interflow-core/src/stream.rs | 13 + crates/interflow-core/src/traits.rs | 5 +- crates/interflow-coreaudio/Cargo.toml | 15 + .../interflow-coreaudio/examples/sine_wave.rs | 52 ++ crates/interflow-coreaudio/src/lib.rs | 556 +++++++++++++ examples/duplex.rs | 58 -- examples/eject_stream_pipewire.rs | 77 -- examples/enumerate_alsa.rs | 16 - examples/enumerate_coreaudio.rs | 14 - examples/enumerate_pipewire.rs | 40 - examples/enumerate_wasapi.rs | 13 - examples/input.rs | 54 -- examples/loopback.rs | 59 -- examples/set_buffer_size.rs | 95 --- examples/sine_wave.rs | 4 +- examples/sine_wave_pipewire.rs | 26 - src/backends/coreaudio.rs | 571 -------------- src/backends/mod.rs | 105 +-- src/lib.rs | 288 +------ 24 files changed, 792 insertions(+), 2182 deletions(-) create mode 100644 crates/interflow-coreaudio/Cargo.toml create mode 100644 crates/interflow-coreaudio/examples/sine_wave.rs create mode 100644 crates/interflow-coreaudio/src/lib.rs delete mode 100644 examples/duplex.rs delete mode 100644 examples/eject_stream_pipewire.rs delete mode 100644 examples/enumerate_alsa.rs delete mode 100644 examples/enumerate_coreaudio.rs delete mode 100644 examples/enumerate_pipewire.rs delete mode 100644 examples/enumerate_wasapi.rs delete mode 100644 examples/input.rs delete mode 100644 examples/loopback.rs delete mode 100644 examples/set_buffer_size.rs delete mode 100644 examples/sine_wave_pipewire.rs delete mode 100644 src/backends/coreaudio.rs diff --git a/Cargo.lock b/Cargo.lock index b678ae2..71c7364 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,38 +11,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "alsa" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812947049edcd670a82cd5c73c3661d2e58468577ba8489de58e1a73c04cbd5d" -dependencies = [ - "alsa-sys", - "bitflags 2.10.0", - "cfg-if", - "libc", -] - -[[package]] -name = "alsa-sys" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad7569085a265dd3f607ebecce7458eaab2132a84393534c95b18dcbc3f31e04" -dependencies = [ - "libc", - "pkg-config", -] - -[[package]] -name = "annotate-snippets" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccaf7e9dfbb6ab22c82e473cd1a8a7bd313c19a5b7e40970f3d89ef5a5c9e81e" -dependencies = [ - "unicode-width 0.1.14", - "yansi-term", -] - [[package]] name = "anstream" version = "0.6.21" @@ -99,63 +67,24 @@ version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" -[[package]] -name = "arrayvec" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" - -[[package]] -name = "autocfg" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" - -[[package]] -name = "bindgen" -version = "0.69.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" -dependencies = [ - "annotate-snippets", - "bitflags 2.10.0", - "cexpr", - "clang-sys", - "itertools 0.12.1", - "lazy_static", - "lazycell", - "proc-macro2", - "quote", - "regex", - "rustc-hash 1.1.0", - "shlex", - "syn", -] - [[package]] name = "bindgen" version = "0.72.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" dependencies = [ - "bitflags 2.10.0", + "bitflags", "cexpr", "clang-sys", - "itertools 0.13.0", + "itertools", "proc-macro2", "quote", "regex", - "rustc-hash 2.1.1", + "rustc-hash", "shlex", "syn", ] -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - [[package]] name = "bitflags" version = "2.10.0" @@ -168,16 +97,6 @@ version = "3.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" -[[package]] -name = "cc" -version = "1.2.55" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b26a0954ae34af09b50f0de26458fa95369a0d478d8236d3f93082b219bd29" -dependencies = [ - "find-msvc-tools", - "shlex", -] - [[package]] name = "cexpr" version = "0.6.0" @@ -187,16 +106,6 @@ dependencies = [ "nom", ] -[[package]] -name = "cfg-expr" -version = "0.15.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" -dependencies = [ - "smallvec", - "target-lexicon", -] - [[package]] name = "cfg-if" version = "1.0.4" @@ -235,35 +144,17 @@ dependencies = [ "encode_unicode", "libc", "once_cell", - "unicode-width 0.2.2", + "unicode-width", "windows-sys", ] -[[package]] -name = "convert_case" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" -dependencies = [ - "unicode-segmentation", -] - -[[package]] -name = "cookie-factory" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9885fa71e26b8ab7855e2ec7cae6e9b380edff76cd052e07c683a0319d51b3a2" -dependencies = [ - "futures", -] - [[package]] name = "coreaudio-rs" -version = "0.13.0" +version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1aae284fbaf7d27aa0e292f7677dfbe26503b0d555026f702940805a630eac17" +checksum = "7d5d7dca3ebcf65a035582c9ad4385371a9d9ee6537474d2a278f4e1e475bb58" dependencies = [ - "bitflags 1.3.2", + "bitflags", "libc", "objc2-audio-toolbox", "objc2-core-audio", @@ -277,22 +168,16 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ceec7a6067e62d6f931a2baf6f3a751f4a892595bcec1461a3c94ef9949864b6" dependencies = [ - "bindgen 0.72.1", + "bindgen", ] -[[package]] -name = "crossbeam-utils" -version = "0.8.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" - [[package]] name = "dispatch2" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" dependencies = [ - "bitflags 2.10.0", + "bitflags", "objc2", ] @@ -342,153 +227,18 @@ dependencies = [ "log", ] -[[package]] -name = "equivalent" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" - -[[package]] -name = "fast-interleave" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0a5ab837f0983c0a9210d7847931f64e3be67fcb0ab93d625f7845a0132c825" - -[[package]] -name = "find-msvc-tools" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" - -[[package]] -name = "fixed-resample" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e548b80c8a294b230847d73ae8e53d1b2d5b407a1a9ec51c9698a761c4610b1" -dependencies = [ - "arrayvec", - "fast-interleave", - "ringbuf", - "rubato", -] - -[[package]] -name = "futures" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" -dependencies = [ - "futures-channel", - "futures-core", - "futures-executor", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-channel" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" -dependencies = [ - "futures-core", - "futures-sink", -] - -[[package]] -name = "futures-core" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" - -[[package]] -name = "futures-executor" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" -dependencies = [ - "futures-core", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-io" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" - -[[package]] -name = "futures-macro" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "futures-sink" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" - -[[package]] -name = "futures-task" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" - -[[package]] -name = "futures-util" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" -dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-macro", - "futures-sink", - "futures-task", - "memchr", - "pin-project-lite", - "pin-utils", - "slab", -] - [[package]] name = "glob" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" -[[package]] -name = "hashbrown" -version = "0.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" - [[package]] name = "heck" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" -[[package]] -name = "indexmap" -version = "2.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" -dependencies = [ - "equivalent", - "hashbrown", -] - [[package]] name = "indicatif" version = "0.18.3" @@ -497,7 +247,7 @@ checksum = "9375e112e4b463ec1b1c6c011953545c65a30164fbab5b581df32b3abf0dcb88" dependencies = [ "console", "portable-atomic", - "unicode-width 0.2.2", + "unicode-width", "unit-prefix", "web-time", ] @@ -506,49 +256,45 @@ dependencies = [ name = "interflow" version = "0.1.0" dependencies = [ - "alsa", "anyhow", - "bitflags 2.10.0", "cfg_aliases", - "coreaudio-rs", - "coreaudio-sys", - "duplicate", "env_logger", - "fixed-resample", "indicatif", "interflow-core", + "interflow-coreaudio", "interflow-wasapi", - "libc", - "libspa", - "libspa-sys", - "log", - "ndarray", - "nix 0.31.1", - "oneshot", - "pipewire", - "rtrb", - "thiserror 2.0.18", - "zerocopy", ] [[package]] name = "interflow-core" version = "0.1.0" dependencies = [ - "bitflags 2.10.0", + "bitflags", "duplicate", - "thiserror 2.0.18", + "thiserror", "zerocopy", ] +[[package]] +name = "interflow-coreaudio" +version = "0.1.0" +dependencies = [ + "coreaudio-rs", + "coreaudio-sys", + "interflow-core", + "log", + "oneshot", + "thiserror", +] + [[package]] name = "interflow-wasapi" version = "0.1.0" dependencies = [ - "bitflags 2.10.0", + "bitflags", "duplicate", "interflow-core", - "thiserror 2.0.18", + "thiserror", "windows", "zerocopy", ] @@ -559,15 +305,6 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" -[[package]] -name = "itertools" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" -dependencies = [ - "either", -] - [[package]] name = "itertools" version = "0.13.0" @@ -611,18 +348,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" - -[[package]] -name = "lazycell" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" - [[package]] name = "libc" version = "0.2.180" @@ -639,50 +364,12 @@ dependencies = [ "windows-link", ] -[[package]] -name = "libspa" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65f3a4b81b2a2d8c7f300643676202debd1b7c929dbf5c9bb89402ea11d19810" -dependencies = [ - "bitflags 2.10.0", - "cc", - "convert_case", - "cookie-factory", - "libc", - "libspa-sys", - "nix 0.27.1", - "nom", - "system-deps", -] - -[[package]] -name = "libspa-sys" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf0d9716420364790e85cbb9d3ac2c950bde16a7dd36f3209b7dfdfc4a24d01f" -dependencies = [ - "bindgen 0.69.5", - "cc", - "system-deps", -] - [[package]] name = "log" version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" -[[package]] -name = "matrixmultiply" -version = "0.3.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a06de3016e9fae57a36fd14dba131fccf49f74b40b7fbdb472f96e361ec71a08" -dependencies = [ - "autocfg", - "rawpointer", -] - [[package]] name = "memchr" version = "2.8.0" @@ -695,44 +382,6 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" -[[package]] -name = "ndarray" -version = "0.17.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "520080814a7a6b4a6e9070823bb24b4531daac8c4627e08ba5de8c5ef2f2752d" -dependencies = [ - "matrixmultiply", - "num-complex", - "num-integer", - "num-traits", - "portable-atomic", - "portable-atomic-util", - "rawpointer", -] - -[[package]] -name = "nix" -version = "0.27.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" -dependencies = [ - "bitflags 2.10.0", - "cfg-if", - "libc", -] - -[[package]] -name = "nix" -version = "0.31.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "225e7cfe711e0ba79a68baeddb2982723e4235247aefce1482f2f16c27865b66" -dependencies = [ - "bitflags 2.10.0", - "cfg-if", - "cfg_aliases", - "libc", -] - [[package]] name = "nom" version = "7.1.3" @@ -743,33 +392,6 @@ dependencies = [ "minimal-lexical", ] -[[package]] -name = "num-complex" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" -dependencies = [ - "num-traits", -] - -[[package]] -name = "num-integer" -version = "0.1.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" -dependencies = [ - "num-traits", -] - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", -] - [[package]] name = "objc2" version = "0.6.3" @@ -785,7 +407,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6948501a91121d6399b79abaa33a8aa4ea7857fe019f341b8c23ad6e81b79b08" dependencies = [ - "bitflags 2.10.0", + "bitflags", "libc", "objc2", "objc2-core-audio", @@ -812,7 +434,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a89f2ec274a0cf4a32642b2991e8b351a404d290da87bb6a9a9d8632490bd1c" dependencies = [ - "bitflags 2.10.0", + "bitflags", "objc2", ] @@ -822,7 +444,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ - "bitflags 2.10.0", + "bitflags", "dispatch2", "objc2", ] @@ -856,55 +478,9 @@ checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] name = "oneshot" -version = "0.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "269bca4c2591a28585d6bf10d9ed0332b7d76900a1b02bec41bdc3a2cdcda107" - -[[package]] -name = "pin-project-lite" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" - -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - -[[package]] -name = "pipewire" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08e645ba5c45109106d56610b3ee60eb13a6f2beb8b74f8dc8186cf261788dda" -dependencies = [ - "anyhow", - "bitflags 2.10.0", - "libc", - "libspa", - "libspa-sys", - "nix 0.27.1", - "once_cell", - "pipewire-sys", - "thiserror 1.0.69", -] - -[[package]] -name = "pipewire-sys" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "849e188f90b1dda88fe2bfe1ad31fe5f158af2c98f80fb5d13726c44f3f01112" -dependencies = [ - "bindgen 0.69.5", - "libspa-sys", - "system-deps", -] - -[[package]] -name = "pkg-config" -version = "0.3.32" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +checksum = "cfe21416a02c693fb9f980befcb230ecc70b0b3d1cc4abf88b9675c4c1457f0c" [[package]] name = "portable-atomic" @@ -921,15 +497,6 @@ dependencies = [ "portable-atomic", ] -[[package]] -name = "primal-check" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc0d895b311e3af9902528fbb8f928688abbd95872819320517cc24ca6b2bd08" -dependencies = [ - "num-integer", -] - [[package]] name = "proc-macro2" version = "1.0.106" @@ -960,21 +527,6 @@ dependencies = [ "proc-macro2", ] -[[package]] -name = "rawpointer" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" - -[[package]] -name = "realfft" -version = "3.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f821338fddb99d089116342c46e9f1fbf3828dba077674613e734e01d6ea8677" -dependencies = [ - "rustfft", -] - [[package]] name = "regex" version = "1.12.3" @@ -1004,76 +556,18 @@ version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" -[[package]] -name = "ringbuf" -version = "0.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe47b720588c8702e34b5979cb3271a8b1842c7cb6f57408efa70c779363488c" -dependencies = [ - "crossbeam-utils", - "portable-atomic", - "portable-atomic-util", -] - -[[package]] -name = "rtrb" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad8388ea1a9e0ea807e442e8263a699e7edcb320ecbcd21b4fa8ff859acce3ba" - -[[package]] -name = "rubato" -version = "0.16.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5258099699851cfd0082aeb645feb9c084d9a5e1f1b8d5372086b989fc5e56a1" -dependencies = [ - "num-complex", - "num-integer", - "num-traits", - "realfft", -] - -[[package]] -name = "rustc-hash" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" - [[package]] name = "rustc-hash" version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" -[[package]] -name = "rustfft" -version = "6.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21db5f9893e91f41798c88680037dba611ca6674703c1a18601b01a72c8adb89" -dependencies = [ - "num-complex", - "num-integer", - "num-traits", - "primal-check", - "strength_reduce", - "transpose", -] - [[package]] name = "rustversion" version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" -[[package]] -name = "serde" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" -dependencies = [ - "serde_core", -] - [[package]] name = "serde_core" version = "1.0.228" @@ -1094,39 +588,12 @@ dependencies = [ "syn", ] -[[package]] -name = "serde_spanned" -version = "0.6.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" -dependencies = [ - "serde", -] - [[package]] name = "shlex" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" -[[package]] -name = "slab" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" - -[[package]] -name = "smallvec" -version = "1.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" - -[[package]] -name = "strength_reduce" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe895eb47f22e2ddd4dabc02bce419d2e643c8e3b585c78158b349195bc24d82" - [[package]] name = "syn" version = "2.0.114" @@ -1138,52 +605,13 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "system-deps" -version = "6.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" -dependencies = [ - "cfg-expr", - "heck", - "pkg-config", - "toml", - "version-compare", -] - -[[package]] -name = "target-lexicon" -version = "0.12.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" - -[[package]] -name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl 1.0.69", -] - [[package]] name = "thiserror" version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "thiserror-impl 2.0.18", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "syn", + "thiserror-impl", ] [[package]] @@ -1197,68 +625,12 @@ dependencies = [ "syn", ] -[[package]] -name = "toml" -version = "0.8.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" -dependencies = [ - "serde", - "serde_spanned", - "toml_datetime", - "toml_edit", -] - -[[package]] -name = "toml_datetime" -version = "0.6.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" -dependencies = [ - "serde", -] - -[[package]] -name = "toml_edit" -version = "0.22.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" -dependencies = [ - "indexmap", - "serde", - "serde_spanned", - "toml_datetime", - "winnow", -] - -[[package]] -name = "transpose" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ad61aed86bc3faea4300c7aee358b4c6d0c8d6ccc36524c96e4c92ccf26e77e" -dependencies = [ - "num-integer", - "strength_reduce", -] - [[package]] name = "unicode-ident" version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "537dd038a89878be9b64dd4bd1b260315c1bb94f4d784956b81e27a088d9a09e" -[[package]] -name = "unicode-segmentation" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" - -[[package]] -name = "unicode-width" -version = "0.1.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" - [[package]] name = "unicode-width" version = "0.2.2" @@ -1277,12 +649,6 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" -[[package]] -name = "version-compare" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" - [[package]] name = "version_check" version = "0.9.5" @@ -1344,28 +710,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - [[package]] name = "windows" version = "0.62.2" @@ -1485,24 +829,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "winnow" -version = "0.7.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" -dependencies = [ - "memchr", -] - -[[package]] -name = "yansi-term" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe5c30ade05e61656247b2e334a031dfd0cc466fadef865bdcdea8d537951bf1" -dependencies = [ - "winapi", -] - [[package]] name = "zerocopy" version = "0.8.39" diff --git a/Cargo.toml b/Cargo.toml index 240b2f8..5cfab6a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ license = "MIT" bitflags = "2.10.0" duplicate = "2.0.1" interflow-core = { path = "crates/interflow-core" } +interflow-coreaudio = { path = "crates/interflow-coreaudio" } log = "0.4.29" thiserror = "2.0.18" @@ -24,16 +25,7 @@ rust-version.workspace = true license.workspace = true [dependencies] -bitflags.workspace = true -duplicate.workspace = true -fixed-resample = "0.9.2" interflow-core.workspace = true -log = { workspace = true, features = ["kv"] } -ndarray = "0.17.1" -oneshot = "0.1.11" -rtrb = "0.3.2" -thiserror.workspace = true -zerocopy = { version = "0.8.39", optional = true } [dev-dependencies] anyhow = "1.0.100" @@ -43,47 +35,16 @@ indicatif = "0.18.3" [build-dependencies] cfg_aliases = "0.2.1" -[features] -pipewire = ["dep:pipewire", "dep:libspa", "dep:libspa-sys", "dep:zerocopy"] - -[target.'cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "netbsd"))'.dependencies] -alsa = "0.11.0" -libc = "0.2.180" -libspa = { version = "0.8.0", optional = true } -libspa-sys = { version = "0.8.0", optional = true } -nix = "0.31.1" -pipewire = { version = "0.8.0", optional = true, features = ["v0_3_45"] } - [target.'cfg(any(target_os = "macos", target_os = "ios"))'.dependencies] -coreaudio-rs = "0.13.0" -coreaudio-sys = "0.2.17" +interflow-coreaudio.workspace = true + +# [target.'cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "netbsd"))'.dependencies] +# alsa = "0.11.0" +# libc = "0.2.180" +# libspa = { version = "0.8.0", optional = true } +# libspa-sys = { version = "0.8.0", optional = true } +# nix = "0.31.1" +# pipewire = { version = "0.8.0", optional = true, features = ["v0_3_45"] } [target.'cfg(target_os = "windows")'.dependencies] interflow-wasapi = { path = "crates/interflow-wasapi" } - -[[example]] -name = "eject_stream_pipewire" -path = "examples/eject_stream_pipewire.rs" -required-features = ["pipewire"] - -[[example]] -name = "enumerate_alsa" -path = "examples/enumerate_alsa.rs" - -[[example]] -name = "enumerate_coreaudio" -path = "examples/enumerate_coreaudio.rs" - -[[example]] -name = "enumerate_wasapi" -path = "examples/enumerate_wasapi.rs" - -[[example]] -name = "enumerate_pipewire" -path = "examples/enumerate_pipewire.rs" -required-features = ["pipewire"] - -[[example]] -name = "sine_wave_pipewire" -path = "examples/sine_wave_pipewire.rs" -required-features = ["pipewire"] diff --git a/crates/interflow-core/src/buffer.rs b/crates/interflow-core/src/buffer.rs index b013e93..af2b405 100644 --- a/crates/interflow-core/src/buffer.rs +++ b/crates/interflow-core/src/buffer.rs @@ -325,6 +325,12 @@ impl FrameMut<'_, T> { self.set(channel, *value); } } + + pub fn set_mono(&mut self, value: T) { + for ch in 0..self.buffer.channels() { + self.buffer[ch][self.frame] = value; + } + } } struct IterFramesMut<'a, T> { diff --git a/crates/interflow-core/src/platform.rs b/crates/interflow-core/src/platform.rs index 26b2ef5..83f8711 100644 --- a/crates/interflow-core/src/platform.rs +++ b/crates/interflow-core/src/platform.rs @@ -9,7 +9,7 @@ pub trait Platform: ExtensionProvider { type Device: Device>; const NAME: &'static str; - fn default_device(device_type: DeviceType) -> Result; + fn default_device(&self, device_type: DeviceType) -> Result; fn list_devices(&self) -> Result, Self::Error>; } diff --git a/crates/interflow-core/src/proxies.rs b/crates/interflow-core/src/proxies.rs index b49eab3..4958a71 100644 --- a/crates/interflow-core/src/proxies.rs +++ b/crates/interflow-core/src/proxies.rs @@ -1,10 +1,59 @@ use crate::device::{Device, StreamConfig}; +use crate::platform::Platform; +use crate::stream::StreamHandle; use crate::traits::ExtensionProvider; -use crate::DeviceType; +use crate::{stream, DeviceType}; use std::borrow::Cow; +use std::ptr::NonNull; +use std::rc::Rc; pub type Error = Box; +pub type DynPlatform = Rc; +pub type DynDevice = Rc; + +pub fn default_stream( + platform: &dyn PlatformProxy, + device_type: DeviceType, + callback: Callback, +) -> Result, Error> { + #[derive(Debug, thiserror::Error)] + #[error("Cannot create dynamic stream")] + struct CannotCreateDynStream; + + let device = platform.default_device(device_type)?; + let Some(ext) = (&*device as &dyn ExtensionProvider).lookup::>() + else { + return Err(Box::new(CannotCreateDynStream)); + }; + ext.create_default_stream(callback) +} + +pub trait PlatformProxy: ExtensionProvider { + fn name(&self) -> Cow<'static, str>; + fn enumerate_devices(&self) -> Result, Error>; + fn default_device(&self, device_type: DeviceType) -> Result; +} + +impl PlatformProxy for P { + fn name(&self) -> Cow<'static, str> { + Cow::Borrowed(P::NAME) + } + + fn enumerate_devices(&self) -> Result, Error> { + Ok(Vec::from_iter( + Platform::list_devices(self)? + .into_iter() + .map(|dev| Rc::new(dev) as DynDevice), + )) + } + + fn default_device(&self, device_type: DeviceType) -> Result { + let device = Platform::default_device(self, device_type)?; + Ok(Rc::new(device) as DynDevice) + } +} + pub trait DeviceProxy: ExtensionProvider { fn name(&self) -> Cow<'_, str>; fn device_type(&self) -> DeviceType; @@ -36,13 +85,56 @@ impl DeviceProxy for D { } } -pub trait IntoDeviceProxy { - fn into_device_proxy(self) -> Box; +pub trait CreateStream: DeviceProxy { + fn create_stream( + &self, + config: StreamConfig, + callback: Callback, + ) -> Result, Error>; + fn create_default_stream(&self, callback: Callback) -> Result, Error> { + let config = self.default_config()?; + self.create_stream(config, callback) + } } -impl IntoDeviceProxy for D { - #[inline] - fn into_device_proxy(self) -> Box { - Box::new(self) +impl CreateStream for D +where + as StreamHandle>::Error: 'static + Sync, +{ + fn create_stream( + &self, + config: StreamConfig, + callback: Callback, + ) -> Result, Error> { + let handle = Device::create_stream(self, config, callback)?; + Ok(StreamProxy::from_handle(handle)) + } +} + +pub struct StreamProxy { + data: Option>, + eject: unsafe fn(NonNull<()>) -> Result, +} + +impl StreamProxy { + pub fn from_handle>( + value: Handle, + ) -> Self { + let eject = |data: NonNull<()>| { + // SAFETY: StreamProxy only calls this function when `data` actually points to a valid + // `Handle` instance + let handle = unsafe { data.cast::().read() }; + Ok(handle.eject()?) + }; + let value = Box::into_raw(Box::new(value)); + let data = Some(NonNull::new(value).unwrap().cast()); + Self { data, eject } + } + + pub fn eject(mut self) -> Result { + let data = self.data.take().expect("Cannot have ejected twice"); + // SAFETY: Using a `Option` guarantees that we only eject once, and we can only create a + // pointer of the actual underlying type expected by `eject`. + unsafe { (self.eject)(data) } } } diff --git a/crates/interflow-core/src/stream.rs b/crates/interflow-core/src/stream.rs index af23efb..70c9855 100644 --- a/crates/interflow-core/src/stream.rs +++ b/crates/interflow-core/src/stream.rs @@ -76,3 +76,16 @@ pub trait Callback: Send { output: AudioOutput, ); } + +impl, AudioOutput)> Callback for F { + fn prepare(&mut self, _: CallbackContext) {} + + fn process_audio( + &mut self, + context: CallbackContext, + input: AudioInput, + output: AudioOutput, + ) { + (self)(context, input, output); + } +} diff --git a/crates/interflow-core/src/traits.rs b/crates/interflow-core/src/traits.rs index cc65509..417255b 100644 --- a/crates/interflow-core/src/traits.rs +++ b/crates/interflow-core/src/traits.rs @@ -87,10 +87,9 @@ const _EXTENSION_TRAIT_ASSERTS: () = { typeable::(); }; -/// Additional extensions for [`ExtensionProvider`] objects. -pub trait ExtensionProviderExt: ExtensionProvider { +impl dyn ExtensionProvider { /// Look up [`T`] from the extension if it is registered. - fn lookup(&self) -> Option<&T> { + pub fn lookup(&self) -> Option<&T> { let mut selector = Selector::new::(); { self.register(&mut selector); diff --git a/crates/interflow-coreaudio/Cargo.toml b/crates/interflow-coreaudio/Cargo.toml new file mode 100644 index 0000000..17626f6 --- /dev/null +++ b/crates/interflow-coreaudio/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "interflow-coreaudio" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true + +[dependencies] +interflow-core.workspace = true + +coreaudio-rs = "0.14.1" +coreaudio-sys = "0.2.17" +log.workspace = true +thiserror.workspace = true +oneshot = { version = "0.2.1", features = ["std"] } diff --git a/crates/interflow-coreaudio/examples/sine_wave.rs b/crates/interflow-coreaudio/examples/sine_wave.rs new file mode 100644 index 0000000..12d23a6 --- /dev/null +++ b/crates/interflow-coreaudio/examples/sine_wave.rs @@ -0,0 +1,52 @@ +use std::f32::consts::TAU; + +use interflow_core::{device::Device, platform::Platform as _, stream::Callback, DeviceType}; +use interflow_coreaudio::Platform; + +fn main() { + Platform + .default_device(DeviceType::PHYSICAL | DeviceType::OUTPUT) + .unwrap() + .default_stream(DeviceType::OUTPUT, Sine::new(440.0)) + .unwrap(); +} + +struct Sine { + phase: f32, + frequency: f32, + step: f32, +} + +impl Sine { + fn new(frequency: f32) -> Self { + Self { + phase: 0.0, + frequency, + step: 0.0, + } + } +} + +impl Callback for Sine { + fn prepare(&mut self, context: interflow_core::stream::CallbackContext) { + self.step = self.frequency * context.stream_config.sample_rate as f32; + self.phase = 0.0; + } + + fn process_audio( + &mut self, + context: interflow_core::stream::CallbackContext, + input: interflow_core::stream::AudioInput, + mut output: interflow_core::stream::AudioOutput, + ) { + let num_frames = output.buffer.channel(0).len(); + for i in 0..num_frames { + let v = (self.phase * TAU).sin() * 0.125; + self.phase += self.step; + while self.phase > 1.0 { + self.phase -= 1.0; + } + output.buffer.frame_mut(i).set_mono(v); + } + } +} diff --git a/crates/interflow-coreaudio/src/lib.rs b/crates/interflow-coreaudio/src/lib.rs new file mode 100644 index 0000000..8869574 --- /dev/null +++ b/crates/interflow-coreaudio/src/lib.rs @@ -0,0 +1,556 @@ +//! # Interflow CoreAudio +//! +//! Interflow backend using CoreAudio for macOS and iOS applications +#![warn(missing_docs)] +use std::borrow::Cow; +use std::convert::Infallible; +use std::num::NonZeroUsize; + +use coreaudio::audio_unit::audio_format::LinearPcmFlags; +use coreaudio::audio_unit::macos_helpers::{ + get_audio_device_ids, get_audio_device_supports_scope, get_default_device_id, get_device_name, +}; +use coreaudio::audio_unit::render_callback::{data, Args}; +use coreaudio::audio_unit::{AudioUnit, Element, IOType, SampleFormat, Scope, StreamFormat}; +use coreaudio_sys::{ + kAudioDevicePropertyBufferFrameSize, kAudioDevicePropertyBufferFrameSizeRange, + kAudioObjectPropertyElementMaster, kAudioObjectPropertyScopeInput, + kAudioObjectPropertyScopeOutput, kAudioOutputUnitProperty_EnableIO, + kAudioUnitProperty_StreamFormat, AudioDeviceID, AudioObjectGetPropertyData, + AudioObjectPropertyAddress, AudioValueRange, +}; +use interflow_core::{ + buffer::AudioBuffer, + device::{self, Device as _, ResolvedStreamConfig, StreamConfig}, + platform, stream, + stream::{AudioInput, AudioOutput, CallbackContext, StreamProxy}, + timing::Timestamp, + traits::{ExtensionProvider, Selector}, + DeviceType, +}; + +fn get_device_property( + device_id: AudioDeviceID, + address: AudioObjectPropertyAddress, +) -> Result { + let mut data = std::mem::MaybeUninit::::uninit(); + let mut size = size_of::() as u32; + let status = unsafe { + AudioObjectGetPropertyData( + device_id, + &address, + 0, + std::ptr::null(), + &mut size, + data.as_mut_ptr() as *mut _, + ) + }; + coreaudio::Error::from_os_status(status)?; + Ok(unsafe { data.assume_init() }) +} + +fn set_device_property( + device_id: AudioDeviceID, + address: AudioObjectPropertyAddress, + data: &T, +) -> Result<(), coreaudio::Error> { + let size = size_of::() as u32; + let status = unsafe { + coreaudio_sys::AudioObjectSetPropertyData( + device_id, + &address, + 0, + std::ptr::null(), + size, + data as *const T as *const _, + ) + }; + coreaudio::Error::from_os_status(status) +} + +/// Type of errors from the CoreAudio backend +#[derive(Debug, thiserror::Error)] +#[error("CoreAudio error: ")] +pub enum Error { + /// Error originating from CoreAudio + #[error(transparent)] + Backend(#[from] coreaudio::Error), + /// The scope given to an audio device is invalid. + #[error("Invalid scope {0:?}")] + InvalidScope(Scope), + #[error("No matching devices for type: {0:?}")] + NoMatchingDevices(DeviceType), +} + +impl From for Error { + fn from(_: Infallible) -> Self { + unreachable!() + } +} + +/// The CoreAudio driver. +pub struct Platform; + +impl ExtensionProvider for Platform { + fn register<'a, 'sel>(&'a self, selector: &'sel mut Selector<'a>) -> &'sel mut Selector<'a> { + selector + } +} + +impl platform::Platform for Platform { + type Error = Error; + + type Device = Device; + + const NAME: &'static str = "CoreAudio"; + + fn default_device(&self, device_type: DeviceType) -> Result { + if device_type.is_output() || !device_type.is_input() { + return Ok(Device::DefaultOutput); + } + + let Some(id) = get_default_device_id(device_type.is_input()) else { + return Err(Error::NoMatchingDevices(device_type)); + }; + Ok(Device::Specific(id)) + } + + fn list_devices(&self) -> Result, Self::Error> { + Ok(get_audio_device_ids()?.into_iter().map(Device::Specific)) + } +} + +/// CoreAudio device handle +#[derive(Debug, Copy, Clone)] +pub enum Device { + /// Automatically connects to the default device output, generic stream. + DefaultOutput, + /// Automatically connects to the default device output, notification stream. + SystemOutput, + /// Connect to this specific output. + Specific(AudioDeviceID), +} + +impl Device { + pub fn get_audio_unit(&self) -> Result { + let io_type = match self { + Self::DefaultOutput => IOType::DefaultOutput, + Self::SystemOutput => IOType::SystemOutput, + Self::Specific(..) => IOType::HalOutput, + }; + let mut unit = AudioUnit::new(io_type)?; + let device_type = self.device_type(); + let value = if device_type.is_input() { 1u32 } else { 0 }; + unit.set_property( + kAudioOutputUnitProperty_EnableIO, + Scope::Input, + Element::Input, + Some(&value), + )?; + let value = if device_type.is_output() { 1u32 } else { 0 }; + unit.set_property( + kAudioOutputUnitProperty_EnableIO, + Scope::Output, + Element::Output, + Some(&value), + )?; + Ok(unit) + } +} + +impl ExtensionProvider for Device { + fn register<'a, 'sel>(&'a self, selector: &'sel mut Selector<'a>) -> &'sel mut Selector<'a> { + selector + } +} + +impl device::Device for Device { + type Error = Error; + + type StreamHandle = StreamHandle; + + fn name(&self) -> Cow<'_, str> { + match self { + Self::DefaultOutput => Cow::Borrowed("Default output"), + Self::SystemOutput => Cow::Borrowed("Notifications output"), + &Self::Specific(id) => match get_device_name(id) { + Ok(s) => Cow::Owned(s), + Err(err) => { + log::error!("Cannot get device name for ID: {}: {err}", id); + Cow::Borrowed("") + } + }, + } + } + + fn device_type(&self) -> DeviceType { + let log_error = |result: Result| { + result + .inspect_err(|err| { + log::error!("Cannot get device type for {:?}: {err}", self.name()); + }) + .unwrap_or(false) + }; + let supports_scope = |scope| match self { + Self::DefaultOutput | Self::SystemOutput if matches!(scope, Scope::Output) => true, + &Self::Specific(id) => log_error(get_audio_device_supports_scope(id, scope)), + _ => false, + }; + let is_default = match self { + Self::DefaultOutput | Self::SystemOutput => true, + &Self::Specific(id) => { + get_default_device_id(true) == Some(id) || get_default_device_id(false) == Some(id) + } + }; + + let mut type_ = DeviceType::empty(); + type_.set(DeviceType::INPUT, supports_scope(Scope::Input)); + type_.set(DeviceType::OUTPUT, supports_scope(Scope::Output)); + type_.set(DeviceType::DEFAULT, is_default); + return type_; + } + + fn default_config(&self) -> Result { + let au = self.get_audio_unit()?; + let input_channels = au + .input_stream_format() + .map(|fmt| fmt.channels as usize) + .unwrap_or(0); + let output_channels = au + .output_stream_format() + .map(|fmt| fmt.channels as usize) + .unwrap_or(0); + let buffer_size_range = { + match self { + Self::DefaultOutput | Self::SystemOutput => (None, None), + &Self::Specific(id) => { + let address = AudioObjectPropertyAddress { + mSelector: kAudioDevicePropertyBufferFrameSizeRange, + mScope: if self.device_type().is_input() { + kAudioObjectPropertyScopeInput + } else { + kAudioObjectPropertyScopeOutput + }, + mElement: kAudioObjectPropertyElementMaster, + }; + let range = get_device_property::(id, address)?; + (Some(range.mMinimum as usize), Some(range.mMaximum as usize)) + } + } + }; + Ok(StreamConfig { + sample_rate: au.sample_rate()?, + input_channels, + output_channels, + buffer_size_range, + exclusive: false, + }) + } + + fn is_config_supported(&self, _config: &StreamConfig) -> bool { + true + } + + fn create_stream( + &self, + stream_config: StreamConfig, + callback: Callback, + ) -> Result, Self::Error> { + StreamHandle::new(self, stream_config, callback) + } +} + +/// Stream type created by opening up a stream on a [`Device`]. +pub struct StreamHandle { + audio_unit: AudioUnit, + callback_retrieve: oneshot::Sender>, +} + +impl stream::StreamHandle for StreamHandle { + type Error = Infallible; + + fn eject(mut self) -> Result { + let (tx, rx) = oneshot::channel(); + self.callback_retrieve + .send(tx) + .expect("Callback receiver cannot have been dropped yet"); + let callback = rx.recv().expect("Oneshot receiver must be used"); + self.audio_unit.free_input_callback(); + self.audio_unit.free_render_callback(); + Ok(callback) + } +} + +fn input_stream_format(sample_rate: f64, channel_count: usize) -> StreamFormat { + StreamFormat { + sample_rate, + sample_format: SampleFormat::I16, + flags: LinearPcmFlags::IS_SIGNED_INTEGER, + channels: channel_count as _, + } +} + +fn output_stream_format(sample_rate: f64, channel_count: usize) -> StreamFormat { + StreamFormat { + sample_rate, + sample_format: SampleFormat::F32, + flags: LinearPcmFlags::IS_NON_INTERLEAVED | LinearPcmFlags::IS_FLOAT, + channels: channel_count as _, + } +} + +struct DummyStreamProxy; + +impl ExtensionProvider for DummyStreamProxy { + fn register<'a, 'sel>(&'a self, selector: &'sel mut Selector<'a>) -> &'sel mut Selector<'a> { + selector + } +} + +impl StreamProxy for DummyStreamProxy {} + +static STREAM_PROXY: DummyStreamProxy = DummyStreamProxy; + +impl StreamHandle { + fn new( + device: &Device, + stream_config: StreamConfig, + callback: Callback, + ) -> Result { + let requested_type = stream_config.requested_device_type(); + assert!( + !requested_type.is_duplex(), + "CoreAudio does not support native duplex mode" + ); + + let unsupported = device.device_type() & !requested_type; + if !unsupported.is_empty() { + log::warn!( + "Cannot request {unsupported:?} for {device}, ignoring", + device = device.name() + ); + } + let unit = device.get_audio_unit()?; + if requested_type.is_input() { + Self::new_input(unit, stream_config, callback) + } else { + Self::new_output(unit, stream_config, callback) + } + } + + fn new_input( + mut audio_unit: AudioUnit, + stream_config: StreamConfig, + mut callback: Callback, + ) -> Result { + let asbd = + input_stream_format(stream_config.sample_rate, stream_config.input_channels).to_asbd(); + audio_unit.set_property( + kAudioUnitProperty_StreamFormat, + Scope::Output, + Element::Input, + Some(&asbd), + )?; + let frame_count: u32 = audio_unit.get_property( + kAudioDevicePropertyBufferFrameSize, + Scope::Input, + Element::Input, + )?; + let resolved_config = ResolvedStreamConfig { + sample_rate: asbd.mSampleRate, + input_channels: asbd.mChannelsPerFrame as _, + output_channels: 0, + max_frame_count: frame_count as _, + }; + let mut buffer = AudioBuffer::zeroed( + NonZeroUsize::new(frame_count as _).unwrap(), + NonZeroUsize::new(asbd.mChannelsPerFrame as _).unwrap(), + ); + + let (tx, rx) = oneshot::channel::>(); + callback.prepare(CallbackContext { + stream_config: &resolved_config, + timestamp: Timestamp::new(asbd.mSampleRate), + stream_proxy: &STREAM_PROXY, + }); + let mut callback = Some(callback); + + audio_unit.set_input_callback(move |args: Args>| { + if let Ok(sender) = rx.try_recv() { + sender.send(callback.take().unwrap()).unwrap(); + return Err(()); + } + let num_frames = args.num_frames; + let channels = asbd.mChannelsPerFrame as usize; + let input_data = args.data.buffer; + + for frame_idx in 0..num_frames { + for ch in 0..channels { + let sample = input_data[frame_idx * channels + ch]; + buffer + .frame_mut(frame_idx) + .set(ch, (sample as f32) / (i16::MAX as f32)); + } + } + + let timestamp = Timestamp::from_count( + resolved_config.sample_rate, + args.time_stamp.mSampleTime as _, + ); + + let input = AudioInput { + buffer: buffer.as_ref(), + timestamp, + channel_flags: &[], + }; + + let mut dummy_buf = AudioBuffer::zeroed( + NonZeroUsize::new(1).unwrap(), + NonZeroUsize::new(channels as _).unwrap(), + ); + let dummy_output = AudioOutput { + buffer: dummy_buf.as_mut(), + timestamp: Timestamp::new(asbd.mSampleRate), + channel_flags: &[], + }; + + if let Some(callback) = &mut callback { + callback.process_audio( + CallbackContext { + stream_config: &resolved_config, + timestamp, + stream_proxy: &STREAM_PROXY, + }, + input, + dummy_output, + ); + } + Ok(()) + })?; + audio_unit.start()?; + Ok(Self { + audio_unit, + callback_retrieve: tx, + }) + } + + fn new_output( + mut audio_unit: AudioUnit, + stream_config: StreamConfig, + mut callback: Callback, + ) -> Result { + let asbd = output_stream_format(stream_config.sample_rate, stream_config.output_channels) + .to_asbd(); + audio_unit.set_property( + kAudioUnitProperty_StreamFormat, + Scope::Input, + Element::Output, + Some(&asbd), + )?; + let frame_size: u32 = audio_unit.get_property( + kAudioDevicePropertyBufferFrameSize, + Scope::Output, + Element::Output, + )?; + let resolved_config = ResolvedStreamConfig { + sample_rate: asbd.mSampleRate, + input_channels: 0, + output_channels: asbd.mChannelsPerFrame as _, + max_frame_count: frame_size as _, + }; + let mut buffer = AudioBuffer::zeroed( + NonZeroUsize::new(frame_size as _).unwrap(), + NonZeroUsize::new(asbd.mChannelsPerFrame as _).unwrap(), + ); + + let (tx, rx) = oneshot::channel::>(); + callback.prepare(CallbackContext { + stream_config: &resolved_config, + timestamp: Timestamp::new(resolved_config.sample_rate), + stream_proxy: &STREAM_PROXY, + }); + let mut callback = Some(callback); + + audio_unit.set_render_callback(move |mut args: Args>| { + if let Ok(sender) = rx.try_recv() { + sender.send(callback.take().unwrap()).unwrap(); + return Err(()); + } + let timestamp = Timestamp::from_count( + resolved_config.sample_rate, + args.time_stamp.mSampleTime as _, + ); + + let dummy_buf = AudioBuffer::zeroed( + NonZeroUsize::new(1).unwrap(), + NonZeroUsize::new(asbd.mChannelsPerFrame as _).unwrap(), + ); + let dummy_input = AudioInput { + buffer: dummy_buf.as_ref(), + timestamp: Timestamp::new(resolved_config.sample_rate), + channel_flags: &[], + }; + + let output = AudioOutput { + buffer: buffer.as_mut(), + timestamp, + channel_flags: &[], + }; + + if let Some(callback) = &mut callback { + callback.process_audio( + CallbackContext { + stream_config: &resolved_config, + timestamp, + stream_proxy: &STREAM_PROXY, + }, + dummy_input, + output, + ); + for (out_ch, in_ch) in args.data.channels_mut().zip(buffer.iter_channels()) { + out_ch.copy_from_slice(in_ch); + } + } + Ok(()) + })?; + audio_unit.start()?; + Ok(Self { + audio_unit, + callback_retrieve: tx, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use coreaudio_sys::{kAudioObjectPropertyElementMaster, kAudioObjectPropertyScopeOutput}; + use interflow_core::platform::Platform as _; + + #[test] + fn test_set_device_buffersize() { + let platform = Platform; + let Some(Device::Specific(device_id)) = platform + .list_devices() + .unwrap() + .into_iter() + .find(|d| d.device_type().is_output()) + else { + println!("Skipping test: No specific output device found."); + return; + }; + + let buffer_size = 256u32; + + let property_address = AudioObjectPropertyAddress { + mSelector: kAudioDevicePropertyBufferFrameSize, + mScope: kAudioObjectPropertyScopeOutput, + mElement: kAudioObjectPropertyElementMaster, + }; + set_device_property(device_id, property_address, &buffer_size).unwrap(); + + let actual_buffer_size: u32 = get_device_property(device_id, property_address).unwrap(); + + assert_eq!(buffer_size, actual_buffer_size); + } +} diff --git a/examples/duplex.rs b/examples/duplex.rs deleted file mode 100644 index adc8429..0000000 --- a/examples/duplex.rs +++ /dev/null @@ -1,58 +0,0 @@ -use crate::util::sine::SineWave; -use anyhow::Result; -use interflow::prelude::*; - -mod util; - -//noinspection RsUnwrap -fn main() -> Result<()> { - env_logger::init(); - let input = default_input_device(); - let output = default_output_device(); - log::info!("Opening input: {}", input.name()); - log::info!("Opening output: {}", output.name()); - let config = StreamConfig { - buffer_size_range: (Some(128), Some(512)), - input_channels: 2, - output_channels: 2, - ..output.default_config().unwrap() - }; - let duplex_config = DuplexStreamConfig::new(config); - let stream = create_duplex_stream(input, output, RingMod::new(), duplex_config).unwrap(); - println!("Press Enter to stop"); - std::io::stdin().read_line(&mut String::new())?; - stream.eject().unwrap(); - Ok(()) -} - -struct RingMod { - carrier: SineWave, -} - -impl RingMod { - fn new() -> Self { - Self { - carrier: SineWave::new(440.0), - } - } -} - -impl AudioCallback for RingMod { - fn prepare(&mut self, context: AudioCallbackContext) { - self.carrier.prepare(context); - } - - fn process_audio( - &mut self, - context: AudioCallbackContext, - input: AudioInput, - mut output: AudioOutput, - ) { - let sr = context.stream_config.sample_rate as f32; - for i in 0..output.buffer.num_frames() { - let inp = input.buffer.get_frame(i)[0]; - let c = self.carrier.next_sample(); - output.buffer.set_mono(i, inp * c); - } - } -} diff --git a/examples/eject_stream_pipewire.rs b/examples/eject_stream_pipewire.rs deleted file mode 100644 index 3241bf2..0000000 --- a/examples/eject_stream_pipewire.rs +++ /dev/null @@ -1,77 +0,0 @@ -//! This example showcases the problem with ejecting stopped pipewire streams described in -//! https://github.com/SolarLiner/interflow/issues/105 -//! -//! It would be best as an integration test, but it has nontrivial prerequisites on the environment: -//! - running PipeWire daemon -//! - at least one PipeWire audio output device -//! - the `pw-link` program installed (bundled with pipewire) - -use interflow::prelude::*; -use std::ops::Deref; -use std::thread; -use util::sine::SineWave; - -mod util; - -#[cfg(all(os_pipewire, feature = "pipewire"))] -fn main() -> Result<(), Box> { - use interflow::prelude::pipewire::driver::PipewireDriver; - use std::{process, time::Duration}; - - env_logger::init(); - - let driver = PipewireDriver::new()?; - - // Select the highest-priority output device. Use this rather than `driver.default_device()` - // because we need its real name for disconnecting it below. - let devices = driver.list_devices()?; - let mut device = devices - .into_iter() - .filter(|d| d.device_type().is_output()) - .max_by_key(device_session_priority) - .expect("No output PipeWire devices?"); - println!("Using device {}", device.name()); - - let config = device.default_output_config()?; - device.with_stream_name("Interflow eject test 1"); - let stream_1 = device.create_output_stream(config, SineWave::new(440.0))?; - - println!("Playing sine wave for 1 second, then ejecting"); - thread::sleep(Duration::from_secs(1)); - let callback = stream_1.eject().unwrap(); - - println!("Playing sine wave for another second in a new stream but old callback"); - let stream_2 = device.create_output_stream(config, callback)?; - thread::sleep(Duration::from_secs(1)); - - // Disconnect our node from the device node. Call external program, doing this programmatically - // using pipewire-rs would be much more involved. - let mut command = process::Command::new("pw-link"); - command - .arg("--disconnect") - .arg("eject_stream_pipewire") - .arg(device.name().deref()); - println!("Disconnecting playback pipewire node from its device using {command:?}"); - let status = command.status()?; - assert!(status.success()); - - println!("Ejecting the callback from the new stream"); - // The hang occurred right in this call - stream_2.eject()?; - - println!("Exiting cleanly"); - Ok(()) -} - -#[cfg(all(os_pipewire, feature = "pipewire"))] -fn device_session_priority(device: &pipewire::device::PipewireDevice) -> Option { - let properties = device - .properties() - .expect("Cannot get pipewire device properties")?; - - let priority_property = properties.get("priority.session")?; - let priority = priority_property - .parse() - .expect("Cannot parse priority.session as i32"); - Some(priority) -} diff --git a/examples/enumerate_alsa.rs b/examples/enumerate_alsa.rs deleted file mode 100644 index 2c38560..0000000 --- a/examples/enumerate_alsa.rs +++ /dev/null @@ -1,16 +0,0 @@ -mod util; - -#[cfg(os_alsa)] -fn main() -> Result<(), Box> { - use crate::util::enumerate::enumerate_devices; - use interflow::backends::alsa::AlsaDriver; - - env_logger::init(); - - enumerate_devices(AlsaDriver) -} - -#[cfg(not(os_alsa))] -fn main() { - println!("ALSA driver is not available on this platform"); -} diff --git a/examples/enumerate_coreaudio.rs b/examples/enumerate_coreaudio.rs deleted file mode 100644 index 030883d..0000000 --- a/examples/enumerate_coreaudio.rs +++ /dev/null @@ -1,14 +0,0 @@ -mod util; - -#[cfg(os_coreaudio)] -fn main() -> Result<(), Box> { - use crate::util::enumerate::enumerate_devices; - use interflow::backends::coreaudio::CoreAudioDriver; - - enumerate_devices(CoreAudioDriver) -} - -#[cfg(not(os_coreaudio))] -fn main() { - println!("CoreAudio is not available on this platform"); -} diff --git a/examples/enumerate_pipewire.rs b/examples/enumerate_pipewire.rs deleted file mode 100644 index 6d2e6d3..0000000 --- a/examples/enumerate_pipewire.rs +++ /dev/null @@ -1,40 +0,0 @@ -use interflow::{prelude::pipewire::driver::PipewireDriver, AudioDevice, AudioDriver}; - -mod util; - -type Result = std::result::Result<(), Box>; - -#[cfg(all(os_pipewire, feature = "pipewire"))] -fn main() -> Result { - use crate::util::enumerate::enumerate_devices; - use interflow::backends::pipewire::driver::PipewireDriver; - env_logger::init(); - let driver = PipewireDriver::new()?; - enumerate_properties(&driver)?; - enumerate_devices(driver)?; - Ok(()) -} - -fn enumerate_properties(driver: &PipewireDriver) -> Result { - eprintln!("Properties:"); - - for device in driver.list_devices()? { - eprintln!("\t{}", device.name()); - - let Some(properties) = device.properties()? else { - eprintln!("\tNo properties found"); - continue; - }; - - for (key, value) in properties.dict().iter() { - eprintln!("\t\t{key}: {value}") - } - } - - Ok(()) -} - -#[cfg(not(all(os_pipewire, feature = "pipewire")))] -fn main() { - println!("Pipewire feature is not enabled"); -} diff --git a/examples/enumerate_wasapi.rs b/examples/enumerate_wasapi.rs deleted file mode 100644 index 05c658c..0000000 --- a/examples/enumerate_wasapi.rs +++ /dev/null @@ -1,13 +0,0 @@ -mod util; - -#[cfg(os_wasapi)] -fn main() -> Result<(), Box> { - use crate::util::enumerate::enumerate_devices; - use interflow::backends::wasapi::WasapiDriver; - enumerate_devices(WasapiDriver) -} - -#[cfg(not(os_wasapi))] -fn main() { - println!("WASAPI driver is not available on this platform"); -} diff --git a/examples/input.rs b/examples/input.rs deleted file mode 100644 index 5dfd6a6..0000000 --- a/examples/input.rs +++ /dev/null @@ -1,54 +0,0 @@ -use crate::util::meter::PeakMeter; -use crate::util::AtomicF32; -use anyhow::Result; -use interflow::prelude::*; -use std::sync::Arc; - -mod util; - -fn main() -> Result<()> { - env_logger::init(); - - let device = default_input_device(); - let value = Arc::new(AtomicF32::new(0.)); - let stream = device - .default_stream(DeviceType::INPUT, RmsMeter::new(value.clone())) - .unwrap(); - util::display_peakmeter(value)?; - stream.eject().unwrap(); - Ok(()) -} - -struct RmsMeter { - value: Arc, - meter: Option, -} - -impl RmsMeter { - fn new(value: Arc) -> Self { - Self { meter: None, value } - } -} - -impl AudioCallback for RmsMeter { - fn prepare(&mut self, context: AudioCallbackContext) { - let meter = self - .meter - .get_or_insert_with(|| PeakMeter::new(context.stream_config.sample_rate as f32, 15.0)); - meter.set_samplerate(context.stream_config.sample_rate as f32); - } - fn process_audio( - &mut self, - _: AudioCallbackContext, - input: AudioInput, - _output: AudioOutput, - ) { - let meter = self - .meter - .as_mut() - .expect("Peak meter not constructed, prepare not called"); - meter.process_buffer(input.buffer.as_ref()); - self.value - .store(meter.value(), std::sync::atomic::Ordering::Relaxed); - } -} diff --git a/examples/loopback.rs b/examples/loopback.rs deleted file mode 100644 index dcd80a9..0000000 --- a/examples/loopback.rs +++ /dev/null @@ -1,59 +0,0 @@ -use crate::util::meter::PeakMeter; -use crate::util::AtomicF32; -use anyhow::Result; -use interflow::prelude::*; -use std::sync::Arc; -mod util; - -fn main() -> Result<()> { - env_logger::init(); - - let input = default_input_device(); - let output = default_output_device(); - log::info!("Opening input : {}", input.name()); - log::info!("Opening output: {}", output.name()); - let config = StreamConfig { - buffer_size_range: (Some(128), Some(512)), - input_channels: 1, - output_channels: 1, - ..output.default_config().unwrap() - }; - let value = Arc::new(AtomicF32::new(0.)); - let config = DuplexStreamConfig::new(config); - let stream = - create_duplex_stream(input, output, Loopback::new(44100., value.clone()), config).unwrap(); - util::display_peakmeter(value)?; - stream.eject().unwrap(); - Ok(()) -} - -struct Loopback { - meter: PeakMeter, - value: Arc, -} - -impl Loopback { - fn new(samplerate: f32, value: Arc) -> Self { - Self { - meter: PeakMeter::new(samplerate, 15.0), - value, - } - } -} - -impl AudioCallback for Loopback { - fn prepare(&mut self, context: AudioCallbackContext) {} - fn process_audio( - &mut self, - context: AudioCallbackContext, - input: AudioInput, - mut output: AudioOutput, - ) { - self.meter - .set_samplerate(context.stream_config.sample_rate as f32); - let rms = self.meter.process_buffer(input.buffer.as_ref()); - self.value.store(rms, std::sync::atomic::Ordering::Relaxed); - output.buffer.as_interleaved_mut().fill(0.0); - output.buffer.mix(input.buffer.as_ref(), 1.0); - } -} diff --git a/examples/set_buffer_size.rs b/examples/set_buffer_size.rs deleted file mode 100644 index 781e5c2..0000000 --- a/examples/set_buffer_size.rs +++ /dev/null @@ -1,95 +0,0 @@ -//! This example demonstrates how to request a specific buffer size from the CoreAudio backend. -//! Probably only works on macOS. - -mod util; - -#[cfg(os_coreaudio)] -fn main() -> anyhow::Result<()> { - use interflow::backends::coreaudio::CoreAudioDriver; - use interflow::channel_map::CreateBitset; - use interflow::prelude::*; - use std::sync::{ - atomic::{AtomicBool, Ordering}, - Arc, - }; - use util::sine::SineWave; - - struct MyCallback { - first_callback: Arc, - sine_wave: SineWave, - } - - impl AudioCallback for MyCallback { - fn prepare(&mut self, context: AudioCallbackContext) { - self.sine_wave.prepare(context); - } - - fn process_audio( - &mut self, - _: AudioCallbackContext, - _: AudioInput, - mut output: AudioOutput, - ) { - if self.first_callback.swap(false, Ordering::SeqCst) { - println!( - "Actual buffer size granted by OS: {}", - output.buffer.num_frames() - ); - } - - for mut frame in output.buffer.as_interleaved_mut().rows_mut() { - let sample = self.sine_wave.next_sample(); - for channel_sample in &mut frame { - *channel_sample = sample; - } - } - } - } - - env_logger::init(); - - let driver = CoreAudioDriver; - let device = driver - .default_device(DeviceType::OUTPUT) - .expect("Failed to query for default output device") - .expect("No default output device found on this system"); - - println!("Using device: {}", device.name()); - - if let Ok((min, max)) = device.buffer_size_range() { - println!( - "Supported buffer size range: min={}, max={}", - min.map_or_else(|| "N/A".to_string(), |v| v.to_string()), - max.map_or_else(|| "N/A".to_string(), |v| v.to_string()) - ); - } - - let requested_buffer_size = 256; - println!("Requesting buffer size: {}", requested_buffer_size); - - let stream_config = StreamConfig { - sample_rate: 48000.0, - input_channels: 0, - output_channels: 2, - buffer_size_range: (Some(requested_buffer_size), Some(requested_buffer_size)), - exclusive: false, - }; - - let callback = MyCallback { - first_callback: Arc::new(AtomicBool::new(true)), - sine_wave: SineWave::new(440.0), - }; - - let stream = device.create_stream(stream_config, callback)?; - - println!("Playing sine wave... Press enter to stop."); - std::io::stdin().read_line(&mut String::new())?; - - stream.eject()?; - Ok(()) -} - -#[cfg(not(os_coreaudio))] -fn main() { - println!("This example is only available on platforms that support CoreAudio (e.g. macOS)."); -} diff --git a/examples/sine_wave.rs b/examples/sine_wave.rs index 7c3004e..861db80 100644 --- a/examples/sine_wave.rs +++ b/examples/sine_wave.rs @@ -1,5 +1,5 @@ use anyhow::Result; -use interflow::prelude::*; +use interflow::core::{proxies::DynDevice, DeviceType}; use util::sine::SineWave; mod util; @@ -7,7 +7,7 @@ mod util; fn main() -> Result<()> { env_logger::init(); - let device = default_output_device(); + let device: DynDevice = todo!("default_device"); println!("Using device {}", device.name()); let stream = device .default_stream(DeviceType::OUTPUT, SineWave::new(440.0)) diff --git a/examples/sine_wave_pipewire.rs b/examples/sine_wave_pipewire.rs deleted file mode 100644 index 550aa17..0000000 --- a/examples/sine_wave_pipewire.rs +++ /dev/null @@ -1,26 +0,0 @@ -use interflow::prelude::*; -use util::sine::SineWave; - -mod util; - -#[cfg(all(os_pipewire, feature = "pipewire"))] -fn main() -> Result<(), Box> { - use interflow::prelude::pipewire::driver::PipewireDriver; - - env_logger::init(); - - let driver = PipewireDriver::new()?; - let mut device = driver.default_device(DeviceType::OUTPUT)?.unwrap(); - println!("Using device {}", device.name()); - - let config = device.default_output_config()?; - device.with_stream_name("Interflow sine wave"); - let properties = [("node.custom-property".into(), "interflow".into())].into(); - device.with_stream_properties(properties); - let stream = device.create_output_stream(config, SineWave::new(440.0))?; - - println!("Press Enter to stop"); - std::io::stdin().read_line(&mut String::new())?; - stream.eject().unwrap(); - Ok(()) -} diff --git a/src/backends/coreaudio.rs b/src/backends/coreaudio.rs deleted file mode 100644 index 5f915a1..0000000 --- a/src/backends/coreaudio.rs +++ /dev/null @@ -1,571 +0,0 @@ -//! # CoreAudio backend -//! -//! CoreAudio is the audio backend for macOS and iOS devices. - -use std::borrow::Cow; -use std::convert::Infallible; - -use coreaudio::audio_unit::audio_format::LinearPcmFlags; -use coreaudio::audio_unit::macos_helpers::{ - audio_unit_from_device_id, get_audio_device_ids_for_scope, get_audio_device_supports_scope, - get_default_device_id, get_device_name, get_supported_physical_stream_formats, -}; -use coreaudio::audio_unit::render_callback::{data, Args}; -use coreaudio::audio_unit::{AudioUnit, Element, IOType, SampleFormat, Scope, StreamFormat}; -use coreaudio_sys::{ - kAudioDevicePropertyBufferFrameSize, kAudioDevicePropertyBufferFrameSizeRange, - kAudioObjectPropertyElementMaster, kAudioObjectPropertyScopeInput, - kAudioObjectPropertyScopeOutput, kAudioOutputUnitProperty_EnableIO, - kAudioUnitProperty_SampleRate, kAudioUnitProperty_StreamFormat, AudioDeviceID, - AudioObjectGetPropertyData, AudioObjectPropertyAddress, AudioValueRange, -}; - -fn get_device_property( - device_id: AudioDeviceID, - address: AudioObjectPropertyAddress, -) -> Result { - let mut data = std::mem::MaybeUninit::::uninit(); - let mut size = std::mem::size_of::() as u32; - let status = unsafe { - AudioObjectGetPropertyData( - device_id, - &address, - 0, - std::ptr::null(), - &mut size, - data.as_mut_ptr() as *mut _, - ) - }; - coreaudio::Error::from_os_status(status)?; - Ok(unsafe { data.assume_init() }) -} - -fn set_device_property( - device_id: AudioDeviceID, - address: AudioObjectPropertyAddress, - data: &T, -) -> Result<(), coreaudio::Error> { - let size = std::mem::size_of::() as u32; - let status = unsafe { - coreaudio_sys::AudioObjectSetPropertyData( - device_id, - &address, - 0, - std::ptr::null(), - size, - data as *const T as *const _, - ) - }; - coreaudio::Error::from_os_status(status) -} -use thiserror::Error; - -use crate::audio_buffer::{AudioBuffer, Sample}; -use crate::duplex::InputProxy; -use crate::prelude::{AudioMut, AudioRef, ChannelMap32}; -use crate::timestamp::Timestamp; -use crate::{ - AudioCallback, AudioCallbackContext, AudioDevice, AudioDriver, AudioInput, AudioOutput, - AudioStreamHandle, Channel, DeviceType, ResolvedStreamConfig, StreamConfig, -}; - -type Result = std::result::Result; - -/// Type of errors from the CoreAudio backend -#[derive(Debug, Error)] -#[error("CoreAudio error:")] -pub enum CoreAudioError { - /// Error originating from CoreAudio - #[error("{0}")] - BackendError(#[from] coreaudio::Error), - /// The scope given to an audio device is invalid. - #[error("Invalid scope {0:?}")] - InvalidScope(Scope), -} - -/// The CoreAudio driver. -#[derive(Debug, Copy, Clone)] -pub struct CoreAudioDriver; - -impl AudioDriver for CoreAudioDriver { - type Error = CoreAudioError; - type Device = CoreAudioDevice; - const DISPLAY_NAME: &'static str = "CoreAudio"; - - fn version(&self) -> Result> { - Ok(Cow::Borrowed("unknown")) - } - - fn default_device(&self, device_type: DeviceType) -> Result> { - let Some(device_id) = get_default_device_id(device_type.is_input()) else { - return Ok(None); - }; - Ok(Some(CoreAudioDevice::from_id(device_id)?)) - } - - fn list_devices(&self) -> Result> { - let per_scope = [Scope::Input, Scope::Output] - .into_iter() - .map(|scope| { - let audio_ids = get_audio_device_ids_for_scope(scope)?; - audio_ids - .into_iter() - .map(|id| CoreAudioDevice::from_id(id)) - .collect::, _>>() - }) - .collect::, _>>()?; - Ok(per_scope.into_iter().flatten()) - } -} - -/// Type of devices available from the CoreAudio driver. -#[derive(Debug, Clone, Copy)] -pub struct CoreAudioDevice { - device_id: AudioDeviceID, - device_type: DeviceType, -} - -impl CoreAudioDevice { - fn from_id(device_id: AudioDeviceID) -> Result { - let is_input = get_audio_device_supports_scope(device_id, Scope::Input)?; - let is_output = get_audio_device_supports_scope(device_id, Scope::Output)?; - let is_default = is_input && get_default_device_id(true) == Some(device_id) - || is_output && get_default_device_id(false) == Some(device_id); - let mut device_type = DeviceType::empty(); - device_type.set(DeviceType::INPUT, is_input); - device_type.set(DeviceType::OUTPUT, is_output); - device_type.set(DeviceType::DEFAULT, is_default); - Ok(Self { - device_id, - device_type, - }) - } - - fn get_audio_unit(&self) -> Result { - let mut unit = AudioUnit::new(IOType::HalOutput)?; - { - let value = if self.device_type.is_input() { 1u32 } else { 0 }; - unit.set_property( - kAudioOutputUnitProperty_EnableIO, - Scope::Input, - Element::Input, - Some(&value), - )?; - } - { - let value = if self.device_type.is_output() { - 1u32 - } else { - 0 - }; - unit.set_property( - kAudioOutputUnitProperty_EnableIO, - Scope::Output, - Element::Output, - Some(&value), - )?; - } - Ok(unit) - } - - /// Sets the device's buffer size if requested in the `StreamConfig`. - /// This must be done before creating the AudioUnit. - fn set_buffer_size_from_config( - &self, - stream_config: &StreamConfig, - ) -> Result<(), CoreAudioError> { - if let (Some(min), Some(max)) = stream_config.buffer_size_range { - if min == max { - let property_address = AudioObjectPropertyAddress { - mSelector: kAudioDevicePropertyBufferFrameSize, - mScope: if self.device_type.is_input() { - kAudioObjectPropertyScopeInput - } else { - kAudioObjectPropertyScopeOutput - }, - mElement: kAudioObjectPropertyElementMaster, - }; - set_device_property(self.device_id, property_address, &(min as u32))?; - } - } - Ok(()) - } -} - -impl AudioDevice for CoreAudioDevice { - type StreamHandle = CoreAudioStream; - type Error = CoreAudioError; - - fn name(&self) -> Cow { - match get_device_name(self.device_id) { - Ok(std) => Cow::Owned(std), - Err(err) => { - log::error!("Cannot get audio device name: {err}"); - Cow::Borrowed("") - } - } - } - - fn device_type(&self) -> DeviceType { - self.device_type - } - - fn channel_map(&self) -> impl IntoIterator { - let channels = match audio_unit_from_device_id(self.device_id, self.device_type.is_input()) - { - Err(err) => { - eprintln!("CoreAudio error getting audio unit: {err}"); - 0 - } - Ok(audio_unit) => { - let stream_format = if self.device_type.is_input() { - audio_unit.input_stream_format().unwrap() - } else { - audio_unit.output_stream_format().unwrap() - }; - stream_format.channels as usize - } - }; - (0..channels).map(|ch| Channel { - index: ch, - name: Cow::Owned(format!("Channel {}", ch)), - }) - } - - fn default_config(&self) -> Result { - let audio_unit = self.get_audio_unit()?; - let input_channels = audio_unit - .input_stream_format() - .map(|fmt| fmt.channels as usize) - .unwrap_or(0); - let output_channels = audio_unit - .output_stream_format() - .map(|fmt| fmt.channels as usize) - .unwrap_or(0); - - Ok(StreamConfig { - sample_rate: audio_unit.sample_rate()?, - input_channels, - output_channels, - buffer_size_range: (None, None), - exclusive: false, - }) - } - - fn buffer_size_range(&self) -> Result<(Option, Option), CoreAudioError> { - let property_address = AudioObjectPropertyAddress { - mSelector: kAudioDevicePropertyBufferFrameSizeRange, - mScope: if self.device_type.is_input() { - kAudioObjectPropertyScopeInput - } else { - kAudioObjectPropertyScopeOutput - }, - mElement: kAudioObjectPropertyElementMaster, - }; - - let range: AudioValueRange = get_device_property(self.device_id, property_address)?; - - Ok((Some(range.mMinimum as usize), Some(range.mMaximum as usize))) - } - - fn is_config_supported(&self, _config: &StreamConfig) -> bool { - true - } - - fn enumerate_configurations(&self) -> Option> { - const TYPICAL_SAMPLERATES: [f64; 5] = [44100., 48000., 96000., 128000., 192000.]; - let supported_list = get_supported_physical_stream_formats(self.device_id) - .inspect_err(|err| eprintln!("Error getting stream formats: {err}")) - .ok()?; - let device_type = self.device_type; - Some(supported_list.into_iter().flat_map(move |asbd| { - let samplerate_range = asbd.mSampleRateRange.mMinimum..asbd.mSampleRateRange.mMaximum; - TYPICAL_SAMPLERATES - .iter() - .copied() - .filter(move |sr| samplerate_range.contains(sr)) - .flat_map(move |sr| { - [false, true] - .into_iter() - .map(move |exclusive| (sr, exclusive)) - }) - .map(move |(sample_rate, exclusive)| { - let channels = asbd.mFormat.mChannelsPerFrame; - let input_channels = if device_type.is_input() { - channels as _ - } else { - 0 - }; - let output_channels = if device_type.is_output() { - channels as _ - } else { - 0 - }; - StreamConfig { - sample_rate, - input_channels, - output_channels, - buffer_size_range: self.buffer_size_range().unwrap_or((None, None)), - exclusive, - } - }) - })) - } - - fn create_stream( - &self, - stream_config: StreamConfig, - callback: Callback, - ) -> Result, Self::Error> { - CoreAudioStream::new(self, stream_config, callback) - } -} - -fn input_stream_format(sample_rate: f64, channel_count: usize) -> StreamFormat { - StreamFormat { - sample_rate, - sample_format: SampleFormat::I16, - flags: LinearPcmFlags::IS_SIGNED_INTEGER, - channels: channel_count as _, - } -} - -fn output_stream_format(sample_rate: f64, channel_count: usize) -> StreamFormat { - StreamFormat { - sample_rate, - sample_format: SampleFormat::F32, - flags: LinearPcmFlags::IS_NON_INTERLEAVED | LinearPcmFlags::IS_FLOAT, - channels: channel_count as _, - } -} - -/// Stream type created by opening up a stream on a [`CoreAudioDevice`]. -pub struct CoreAudioStream { - audio_unit: AudioUnit, - callback_retrieve: oneshot::Sender>, -} - -impl AudioStreamHandle for CoreAudioStream { - type Error = Infallible; - - fn eject(mut self) -> Result { - let (tx, rx) = oneshot::channel(); - self.callback_retrieve - .send(tx) - .expect("Callback receiver cannot have been dropped yet"); - let callback = rx.recv().expect("Oneshot receiver must be used"); - self.audio_unit.free_input_callback(); - self.audio_unit.free_render_callback(); - Ok(callback) - } -} - -impl CoreAudioStream { - fn new( - device: &CoreAudioDevice, - stream_config: StreamConfig, - callback: Callback, - ) -> Result { - let requested_type = stream_config.requested_device_type(); - assert!( - !requested_type.is_duplex(), - "CoreAudio does not support native duplex mode" - ); - - let unsupported = device.device_type & !requested_type; - if !unsupported.is_empty() { - log::warn!( - "Cannot request {unsupported:?} for {device}, ignoring", - device = device.name() - ); - } - let unit = device.get_audio_unit()?; - if requested_type.is_input() { - Self::new_input(unit, stream_config, callback) - } else { - Self::new_output(unit, stream_config, callback) - } - } - - fn new_input( - mut audio_unit: AudioUnit, - stream_config: StreamConfig, - mut callback: Callback, - ) -> Result { - let asbd = - input_stream_format(stream_config.sample_rate, stream_config.input_channels).to_asbd(); - audio_unit.set_property( - kAudioUnitProperty_StreamFormat, - Scope::Output, - Element::Input, - Some(&asbd), - )?; - let frame_count = audio_unit.get_property( - kAudioDevicePropertyBufferFrameSize, - Scope::Input, - Element::Input, - )?; - let stream_config = ResolvedStreamConfig { - sample_rate: asbd.mSampleRate, - input_channels: asbd.mChannelsPerFrame as _, - output_channels: 0, - max_frame_count: frame_count, - }; - let mut buffer = - AudioBuffer::zeroed(asbd.mChannelsPerFrame as _, stream_config.sample_rate as _); - - // Set up the callback retrieval process without needing to make the callback `Sync` - let (tx, rx) = oneshot::channel::>(); - callback.prepare(AudioCallbackContext { - stream_config, - timestamp: Timestamp::new(asbd.mSampleRate), - }); - let mut callback = Some(callback); - - audio_unit.set_input_callback(move |args: Args>| { - if let Ok(sender) = rx.try_recv() { - sender.send(callback.take().unwrap()).unwrap(); - return Err(()); - } - let mut buffer = buffer.slice_mut(..args.num_frames); - for (out, inp) in buffer - .as_interleaved_mut() - .iter_mut() - .zip(args.data.buffer.iter()) - { - *out = inp.into_float(); - } - let timestamp = - Timestamp::from_count(stream_config.sample_rate, args.time_stamp.mSampleTime as _); - let input = AudioInput { - buffer: buffer.as_ref(), - timestamp, - }; - let dummy_output = AudioOutput { - buffer: AudioMut::empty(), - timestamp: Timestamp::new(asbd.mSampleRate), - }; - if let Some(callback) = &mut callback { - callback.process_audio( - AudioCallbackContext { - stream_config, - timestamp, - }, - input, - dummy_output, - ); - } - Ok(()) - })?; - audio_unit.start()?; - Ok(Self { - audio_unit, - callback_retrieve: tx, - }) - } - - fn new_output( - mut audio_unit: AudioUnit, - stream_config: StreamConfig, - mut callback: Callback, - ) -> Result { - let asbd = output_stream_format(stream_config.sample_rate, stream_config.output_channels) - .to_asbd(); - audio_unit.set_property( - kAudioUnitProperty_StreamFormat, - Scope::Input, - Element::Output, - Some(&asbd), - )?; - let frame_size = audio_unit.get_property( - kAudioDevicePropertyBufferFrameSize, - Scope::Output, - Element::Output, - )?; - let stream_config = ResolvedStreamConfig { - sample_rate: asbd.mSampleRate, - input_channels: 0, - output_channels: asbd.mChannelsPerFrame as _, - max_frame_count: frame_size, - }; - let mut buffer = AudioBuffer::zeroed( - stream_config.output_channels, - stream_config.sample_rate as _, - ); - // Set up the callback retrieval process without needing to make the callback `Sync` - let (tx, rx) = oneshot::channel::>(); - callback.prepare(AudioCallbackContext { - stream_config, - timestamp: Timestamp::new(stream_config.sample_rate), - }); - let mut callback = Some(callback); - - audio_unit.set_render_callback(move |mut args: Args>| { - if let Ok(sender) = rx.try_recv() { - sender.send(callback.take().unwrap()).unwrap(); - return Err(()); - } - let mut buffer = buffer.slice_mut(..args.num_frames); - let timestamp = - Timestamp::from_count(stream_config.sample_rate, args.time_stamp.mSampleTime as _); - let dummy_input = AudioInput { - buffer: AudioRef::empty(), - timestamp: Timestamp::new(stream_config.sample_rate), - }; - let output = AudioOutput { - buffer: buffer.as_mut(), - timestamp, - }; - - if let Some(callback) = &mut callback { - callback.process_audio( - AudioCallbackContext { - stream_config, - timestamp, - }, - dummy_input, - output, - ); - for (output, inner) in args.data.channels_mut().zip(buffer.channels()) { - output.copy_from_slice(inner.as_slice().unwrap()); - } - } - Ok(()) - })?; - audio_unit.start()?; - Ok(Self { - audio_unit, - callback_retrieve: tx, - }) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use coreaudio_sys::{kAudioObjectPropertyElementMaster, kAudioObjectPropertyScopeOutput}; - - #[test] - fn test_set_device_buffersize() { - let driver = CoreAudioDriver; - if let Ok(Some(device)) = driver.default_device(DeviceType::OUTPUT) { - let buffer_size = 256; - - // Set the buffer size on the device. - let property_address = AudioObjectPropertyAddress { - mSelector: kAudioDevicePropertyBufferFrameSize, - mScope: kAudioObjectPropertyScopeOutput, - mElement: kAudioObjectPropertyElementMaster, - }; - set_device_property(device.device_id, property_address, &buffer_size).unwrap(); - - // Read it back to confirm. - let actual_buffer_size: u32 = - get_device_property(device.device_id, property_address).unwrap(); - - assert_eq!(buffer_size, actual_buffer_size); - } else { - println!("Skipping test: No default output device found."); - } - } -} diff --git a/src/backends/mod.rs b/src/backends/mod.rs index 8e87457..d4d53b6 100644 --- a/src/backends/mod.rs +++ b/src/backends/mod.rs @@ -5,109 +5,8 @@ //! Each backend is provided in its own submodule. Types should be public so that the user isn't //! limited to going through the main API if they want to choose a specific backend. -use crate::{AudioDevice, AudioDriver, DeviceType}; - -#[cfg(unsupported)] -compile_error!("Unsupported platform (supports ALSA, CoreAudio, and WASAPI)"); - -#[cfg(os_alsa)] -pub mod alsa; - -#[cfg(os_coreaudio)] -pub mod coreaudio; +#[cfg(any(target_os = "macos", target_os = "ios"))] +pub use interflow_coreaudio as coreaudio; #[cfg(target_os = "windows")] pub use interflow_wasapi as wasapi; - -#[cfg(all(os_pipewire, feature = "pipewire"))] -pub mod pipewire; - -/// Returns the default driver. -/// -/// "Default" here means that it is a supported driver that is available on the platform. -/// -/// The signature makes it unfortunately impossible to do runtime selection, and could change in -/// the future to make it possible. Until now, the "default" driver is the lowest common -/// denominator. -/// -/// Selects the following driver depending on platform: -/// -/// | **Platform** | **Driver** | -/// |:------------:|:---------------------------:| -/// | Linux | Pipewire (if enabled), ALSA | -/// | macOS | CoreAudio | -/// | Windows | WASAPI | -#[cfg(any(os_alsa, os_coreaudio, os_wasapi))] -#[allow(clippy::needless_return)] -pub fn default_driver() -> impl AudioDriver { - #[cfg(all(os_pipewire, feature = "pipewire"))] - return pipewire::driver::PipewireDriver::new().unwrap(); - #[cfg(all(not(all(os_pipewire, feature = "pipewire")), os_alsa))] - return alsa::AlsaDriver; - #[cfg(os_coreaudio)] - return coreaudio::CoreAudioDriver; - #[cfg(os_wasapi)] - return wasapi::WasapiDriver; -} - -/// Returns the default input device for the given audio driver. -/// -/// The default device is usually the one the user has selected in its system settings. -pub fn default_input_device_from(driver: &Driver) -> Driver::Device -where - Driver::Device: AudioDevice, -{ - driver - .default_device(DeviceType::PHYSICAL | DeviceType::INPUT) - .expect("Audio driver error") - .expect("No default device found") -} - -/// Default input device from the default driver for this platform. -/// -/// "Default" here means both in terms of platform support but also can include runtime selection. -/// Therefore, it is better to use this method directly rather than first getting the default -/// driver from [`default_driver`]. -#[cfg(any(feature = "pipewire", os_alsa, os_coreaudio, os_wasapi))] -#[allow(clippy::needless_return)] -pub fn default_input_device() -> impl AudioDevice { - #[cfg(all(os_pipewire, feature = "pipewire"))] - return default_input_device_from(&pipewire::driver::PipewireDriver::new().unwrap()); - #[cfg(all(not(all(os_pipewire, feature = "pipewire")), os_alsa))] - return default_input_device_from(&alsa::AlsaDriver); - #[cfg(os_coreaudio)] - return default_input_device_from(&coreaudio::CoreAudioDriver); - #[cfg(os_wasapi)] - return default_input_device_from(&wasapi::WasapiDriver); -} - -/// Returns the default input device for the given audio driver. -/// -/// The default device is usually the one the user has selected in its system settings. -pub fn default_output_device_from(driver: &Driver) -> Driver::Device -where - Driver::Device: AudioDevice, -{ - driver - .default_device(DeviceType::PHYSICAL | DeviceType::OUTPUT) - .expect("Audio driver error") - .expect("No default device found") -} - -/// Default output device from the default driver for this platform. -/// -/// "Default" here means both in terms of platform support but also can include runtime selection. -/// Therefore, it is better to use this method directly rather than first getting the default -/// driver from [`default_driver`]. -#[cfg(any(os_alsa, os_coreaudio, os_wasapi, feature = "pipewire"))] -#[allow(clippy::needless_return)] -pub fn default_output_device() -> impl AudioDevice { - #[cfg(all(os_pipewire, feature = "pipewire"))] - return default_output_device_from(&pipewire::driver::PipewireDriver::new().unwrap()); - #[cfg(all(not(all(os_pipewire, feature = "pipewire")), os_alsa))] - return default_output_device_from(&alsa::AlsaDriver); - #[cfg(os_coreaudio)] - return default_output_device_from(&coreaudio::CoreAudioDriver); - #[cfg(os_wasapi)] - return default_output_device_from(&wasapi::WasapiDriver); -} diff --git a/src/lib.rs b/src/lib.rs index e7bd271..f952584 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,292 +1,6 @@ #![doc = include_str!("../README.md")] #![warn(missing_docs)] -use crate::audio_buffer::{AudioMut, AudioRef}; -use crate::channel_map::ChannelMap32; -use crate::timestamp::Timestamp; -use bitflags::bitflags; -use std::borrow::Cow; +pub use interflow_core as core; -pub mod audio_buffer; pub mod backends; -pub mod channel_map; -pub mod duplex; -pub mod prelude; -mod sample; -pub mod timestamp; - -bitflags! { - /// Represents the types/capabilities of an audio device. - #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] - pub struct DeviceType: u32 { - /// Device supports audio input. - const INPUT = 1 << 0; - - /// Device supports audio output. - const OUTPUT = 1 << 1; - - /// Physical audio device (hardware). - const PHYSICAL = 1 << 2; - - /// Virtual/software application device. - const APPLICATION = 1 << 3; - - /// This device is set as default - const DEFAULT = 1 << 4; - - /// Device that supports both input and output. - const DUPLEX = Self::INPUT.bits() | Self::OUTPUT.bits(); - } -} - -/// Audio drivers provide access to the inputs and outputs of devices. -/// Several drivers might provide the same access, some sharing it with other applications, -/// while others work in exclusive mode. -pub trait AudioDriver { - /// Type of errors that can happen when using this audio driver. - type Error: Send + std::error::Error; - /// Type of audio devices this driver provides. - type Device: AudioDevice; - - /// Driver display name. - const DISPLAY_NAME: &'static str; - - /// Runtime version of the audio driver. If there is a difference between "client" and - /// "server" versions, then this should reflect the server version. - fn version(&self) -> Result, Self::Error>; - - /// Default device of the given type. This is most often tied to the audio settings at the - /// operating system level. - fn default_device(&self, device_type: DeviceType) -> Result, Self::Error>; - - /// List all devices available through this audio driver. - fn list_devices(&self) -> Result, Self::Error>; -} - -impl DeviceType { - /// Returns true if this device type has the input capability. - pub fn is_input(&self) -> bool { - self.contains(Self::INPUT) - } - - /// Returns true if this device type has the output capability. - pub fn is_output(&self) -> bool { - self.contains(Self::OUTPUT) - } - - /// Returns true if this device type is a physical device. - pub fn is_physical(&self) -> bool { - self.contains(Self::PHYSICAL) - } - - /// Returns true if this device type is an application/virtual device. - pub fn is_application(&self) -> bool { - self.contains(Self::APPLICATION) - } - - /// Returns true if this device is set as default - pub fn is_default(&self) -> bool { - self.contains(Self::DEFAULT) - } - - /// Returns true if this device type supports both input and output. - pub fn is_duplex(&self) -> bool { - self.contains(Self::DUPLEX) - } -} - -/// Configuration for an audio stream. -#[derive(Debug, Clone, Copy, PartialEq)] -pub struct StreamConfig { - /// Configured sample rate of the requested stream. The opened stream can have a different - /// sample rate, so don't rely on this parameter being correct at runtime. - pub sample_rate: f64, - /// Number of input channels requested - pub input_channels: usize, - /// Number of output channels requested - pub output_channels: usize, - /// Range of preferential buffer sizes, in units of audio samples per channel. - /// The library will make a best-effort attempt at honoring this setting, and in future versions - /// may provide additional buffering to ensure it, but for now you should not make assumptions - /// on buffer sizes based on this setting. - pub buffer_size_range: (Option, Option), - /// Whether the device should be exclusively held (meaning no other application can open the - /// same device). - pub exclusive: bool, -} - -impl StreamConfig { - /// Returns a [`DeviceType`] that describes this [`StreamConfig`]. Only [`DeviceType::INPUT`] and - /// [`DeviceType::OUTPUT`] are set. - pub fn requested_device_type(&self) -> DeviceType { - let mut ret = DeviceType::empty(); - ret.set(DeviceType::INPUT, self.input_channels > 0); - ret.set(DeviceType::OUTPUT, self.output_channels > 0); - ret - } - - /// Changes the [`StreamConfig`] such that it matches the configuration of a stream created with a device with - /// the given [`DeviceType`]. - /// - /// This method returns a copy of the input [`StreamConfig`]. - pub fn restrict(mut self, requested_type: DeviceType) -> Self { - if !requested_type.is_input() { - self.input_channels = 0; - } - if !requested_type.is_output() { - self.output_channels = 0; - } - self - } -} - -/// Configuration for an audio stream. -#[derive(Debug, Clone, Copy, PartialEq)] -pub struct ResolvedStreamConfig { - /// Configured sample rate of the requested stream. The opened stream can have a different - /// sample rate, so don't rely on this parameter being correct at runtime. - pub sample_rate: f64, - /// Number of input channels requested - pub input_channels: usize, - /// Number of output channels requested - pub output_channels: usize, - /// Maximum number of frames the audio callback will receive - pub max_frame_count: usize, -} - -/// Audio channel description. -#[derive(Debug, Clone)] -pub struct Channel<'a> { - /// Index of the channel in the device - pub index: usize, - /// Display the name for the channel, if available, else a generic name like "Channel 1" - pub name: Cow<'a, str>, -} - -/// Trait for types describing audio devices. Audio devices have zero or more inputs and outputs, -/// and depending on the driver, can be duplex devices which can provide both of them at the same -/// time natively. -pub trait AudioDevice { - /// Type of the resulting stream. This stream can be used to control the audio processing - /// externally or stop it completely and give back ownership of the callback with - /// [`AudioStreamHandle::eject`]. - type StreamHandle: AudioStreamHandle; - - /// Type of errors that can happen when using this device. - type Error: Send + std::error::Error; - - /// Device display name - fn name(&self) -> Cow<'_, str>; - - /// Device type. Either input, output, or duplex. - fn device_type(&self) -> DeviceType; - - /// Iterator of the available channels in this device. Channel indices are used when - /// specifying which channels to open when creating an audio stream. - fn channel_map(&self) -> impl IntoIterator>; - - /// Default configuration for this device. If [`Ok`], should return a [`StreamConfig`] that is supported (i.e., - /// returns `true` when passed to [`Self::is_config_supported`]). - fn default_config(&self) -> Result; - - /// Returns the supported I/O buffer size range for the device. - fn buffer_size_range(&self) -> Result<(Option, Option), Self::Error> { - Ok((None, None)) - } - - /// Not all configuration values make sense for a particular device, and this method tests a - /// configuration to see if it can be used in an audio stream. - fn is_config_supported(&self, config: &StreamConfig) -> bool; - - /// List all possible configurations this device supports. If that is not provided by - /// the device and not easily generated manually, this will return `None`. - /// - /// The returned configurations should be supported as valid when passed to [`Self::is_config_supported`]. - fn enumerate_configurations(&self) -> Option>; - - /// Creates an output stream with the provided stream configuration. For this call to be - /// valid, [`AudioDevice::is_config_supported`] should have returned `true` on the provided - /// configuration. - /// - /// An output callback is required to process the audio, whose ownership will be transferred - /// to the audio stream. - fn create_stream( - &self, - stream_config: StreamConfig, - callback: Callback, - ) -> Result, Self::Error>; - - /// Create an output stream using the default configuration as returned by [`Self::default_output_config`]. - /// - /// # Arguments - /// - /// - `callback`: Output callback to generate audio data with. - fn default_stream( - &self, - requested_type: DeviceType, - callback: Callback, - ) -> Result, Self::Error> { - let config = self.default_config()?.restrict(requested_type); - debug_assert!( - self.is_config_supported(&config), - "Default configuration is not supported" - ); - self.create_stream(config, callback) - } -} - -#[duplicate::duplicate_item( - name bufty; - [AudioInput] [AudioRef < 'a, T >]; - [AudioOutput] [AudioMut < 'a, T >]; -)] -/// Plain-old-data object holding references to the audio buffer and the associated time-keeping -/// [`Timestamp`]. This timestamp is associated with the stream, and in the cases where the -/// driver provides timing information, it is used instead of relying on sample-counting. -pub struct name<'a, T> { - /// Associated time stamp for this callback. The time represents the duration for which the - /// stream has been opened, and is either provided by the driver if available, or is kept up - /// manually by the library. - pub timestamp: Timestamp, - /// Audio buffer data. - pub buffer: bufty, -} - -/// Plain-old-data object holding the passed-in stream configuration, as well as a general -/// callback timestamp, which can be different from the input and output streams in case of -/// cross-stream latencies; differences in timing can indicate desync. -pub struct AudioCallbackContext { - /// Passed-in stream configuration. Values have been updated where necessary to correspond to - /// the actual stream properties. - pub stream_config: ResolvedStreamConfig, - /// Callback-wide timestamp. - pub timestamp: Timestamp, -} - -/// Trait for types which handles an audio stream (input or output). -pub trait AudioStreamHandle { - /// Type of errors which have caused the stream to fail. - type Error: Send + std::error::Error; - - /// Eject the stream, returning ownership of the callback. - /// - /// An error can occur when an irrecoverable error has occured and ownership has been lost - /// already. - fn eject(self) -> Result; -} - -/// Trait of types which process audio data. This is the trait that users will want to -/// implement when processing audio from a device. -pub trait AudioCallback: Send { - /// Prepare the audio callback to process audio. This function is *not* real-time safe (i.e., allocations can be - /// performed), in preparation for processing the stream with [`Self::process_audio`]. - fn prepare(&mut self, context: AudioCallbackContext); - - /// Callback called when audio data can be processed. - fn process_audio( - &mut self, - context: AudioCallbackContext, - input: AudioInput, - output: AudioOutput, - ); -} From a5d691b0f7f1fec708405d3bf8d62fb1ba56cff6 Mon Sep 17 00:00:00 2001 From: Nathan Graule Date: Wed, 22 Jul 2026 17:30:15 +0200 Subject: [PATCH 19/38] feat: proxy traits and types for type-erased backends --- Cargo.lock | 5 +- Cargo.toml | 4 +- crates/interflow-core/Cargo.toml | 3 +- crates/interflow-core/src/buffer.rs | 34 ++++ crates/interflow-core/src/device.rs | 2 +- crates/interflow-core/src/lib.rs | 4 + crates/interflow-core/src/proxies.rs | 257 +++++++++++++++++++------- crates/interflow-core/src/stream.rs | 1 + crates/interflow-core/src/traits.rs | 6 +- crates/interflow-coreaudio/src/lib.rs | 1 + src/lib.rs | 27 +++ 11 files changed, 270 insertions(+), 74 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 71c7364..15b754f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -63,9 +63,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.101" +version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" [[package]] name = "bindgen" @@ -269,6 +269,7 @@ dependencies = [ name = "interflow-core" version = "0.1.0" dependencies = [ + "anyhow", "bitflags", "duplicate", "thiserror", diff --git a/Cargo.toml b/Cargo.toml index 5cfab6a..da019fe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,7 @@ rust-version = "1.85" license = "MIT" [workspace.dependencies] +anyhow = "1.0.104" bitflags = "2.10.0" duplicate = "2.0.1" interflow-core = { path = "crates/interflow-core" } @@ -25,10 +26,11 @@ rust-version.workspace = true license.workspace = true [dependencies] +anyhow.workspace = true interflow-core.workspace = true [dev-dependencies] -anyhow = "1.0.100" +anyhow.workspace = true env_logger = "0.11.8" indicatif = "0.18.3" diff --git a/crates/interflow-core/Cargo.toml b/crates/interflow-core/Cargo.toml index a1a4e23..1b87f50 100644 --- a/crates/interflow-core/Cargo.toml +++ b/crates/interflow-core/Cargo.toml @@ -6,7 +6,8 @@ rust-version.workspace = true license.workspace = true [dependencies] +anyhow.workspace = true bitflags.workspace = true duplicate.workspace = true thiserror.workspace = true -zerocopy = { version = "0.8.39", features = ["alloc"] } \ No newline at end of file +zerocopy = { version = "0.8.39", features = ["alloc"] } diff --git a/crates/interflow-core/src/buffer.rs b/crates/interflow-core/src/buffer.rs index af2b405..b66ff15 100644 --- a/crates/interflow-core/src/buffer.rs +++ b/crates/interflow-core/src/buffer.rs @@ -281,6 +281,13 @@ impl AudioBuffer { .get_disjoint_mut(indices.map(|i| i * self.frames.get()..(i + 1) * self.frames.get())) .unwrap() } + + pub fn set_mono(&mut self, frame: usize, value: T) + where + T: Copy, + { + self.frame_mut(frame).set_mono(value); + } } #[duplicate::duplicate_item( @@ -392,6 +399,14 @@ name; [AudioMut]; )] impl name<'_, T> { + pub fn frames(&self) -> usize { + self.frame_slice.1 - self.frame_slice.0 + } + + pub fn channels(&self) -> usize { + self.buffer.channels() + } + pub fn frame(&self, index: usize) -> FrameRef<'_, T> { self.buffer.frame(self.frame_slice.0 + index) } @@ -416,4 +431,23 @@ impl AudioMut<'_, T> { let slice = self.buffer.channel_mut(channel); &mut slice[self.frame_slice.0..self.frame_slice.1] } + + pub fn set_mono(&mut self, frame: usize, value: T) + where + T: Copy, + { + self.frame_mut(frame).set_mono(value); + } + + pub fn change_amplitude(&mut self, factor: T) + where + T: Copy + ops::MulAssign, + { + let num_channels = self.channels(); + for frame in &mut self.buffer.data + [num_channels * self.frame_slice.0..num_channels * self.frame_slice.1] + { + *frame *= factor; + } + } } diff --git a/crates/interflow-core/src/device.rs b/crates/interflow-core/src/device.rs index fe74707..95dfabb 100644 --- a/crates/interflow-core/src/device.rs +++ b/crates/interflow-core/src/device.rs @@ -66,7 +66,7 @@ pub struct ResolvedStreamConfig { /// and depending on the driver, can be duplex devices which can provide both of them at the same /// time natively. pub trait Device: ExtensionProvider { - type Error: Send + Sync + std::error::Error; + type Error: Send + Sync + 'static + std::error::Error; type StreamHandle: StreamHandle>; fn name(&self) -> Cow<'_, str>; diff --git a/crates/interflow-core/src/lib.rs b/crates/interflow-core/src/lib.rs index 4271eda..aae486d 100644 --- a/crates/interflow-core/src/lib.rs +++ b/crates/interflow-core/src/lib.rs @@ -6,8 +6,12 @@ pub mod stream; pub mod timing; pub mod traits; +use std::rc::Rc; + use bitflags::bitflags; +use crate::proxies::PlatformProxy; + bitflags! { /// Represents the types/capabilities of an audio device. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] diff --git a/crates/interflow-core/src/proxies.rs b/crates/interflow-core/src/proxies.rs index 4958a71..f9a9860 100644 --- a/crates/interflow-core/src/proxies.rs +++ b/crates/interflow-core/src/proxies.rs @@ -1,46 +1,39 @@ use crate::device::{Device, StreamConfig}; use crate::platform::Platform; -use crate::stream::StreamHandle; +use crate::stream::{AudioInput, AudioOutput, CallbackContext}; use crate::traits::ExtensionProvider; use crate::{stream, DeviceType}; +use anyhow::Result; +use std::any::{Any, TypeId}; use std::borrow::Cow; +use std::marker::PhantomData; use std::ptr::NonNull; use std::rc::Rc; -pub type Error = Box; - +/// Type alias for a reference-counted platform proxy. pub type DynPlatform = Rc; -pub type DynDevice = Rc; -pub fn default_stream( - platform: &dyn PlatformProxy, - device_type: DeviceType, - callback: Callback, -) -> Result, Error> { - #[derive(Debug, thiserror::Error)] - #[error("Cannot create dynamic stream")] - struct CannotCreateDynStream; - - let device = platform.default_device(device_type)?; - let Some(ext) = (&*device as &dyn ExtensionProvider).lookup::>() - else { - return Err(Box::new(CannotCreateDynStream)); - }; - ext.create_default_stream(callback) -} +/// Type alias for a reference-counted device proxy. +pub type DynDevice = Rc; +/// Trait for platform proxies that provides access to audio devices. pub trait PlatformProxy: ExtensionProvider { fn name(&self) -> Cow<'static, str>; - fn enumerate_devices(&self) -> Result, Error>; - fn default_device(&self, device_type: DeviceType) -> Result; + fn list_devices(&self) -> Result>; + fn default_device(&self, device_type: DeviceType) -> Result; } -impl PlatformProxy for P { +impl PlatformProxy for P +where + <<

::Device as Device>::StreamHandle as stream::StreamHandle< + DynCallback, + >>::Error: Sync, +{ fn name(&self) -> Cow<'static, str> { Cow::Borrowed(P::NAME) } - fn enumerate_devices(&self) -> Result, Error> { + fn list_devices(&self) -> Result> { Ok(Vec::from_iter( Platform::list_devices(self)? .into_iter() @@ -48,21 +41,36 @@ impl PlatformProxy for P { )) } - fn default_device(&self, device_type: DeviceType) -> Result { + fn default_device(&self, device_type: DeviceType) -> Result { let device = Platform::default_device(self, device_type)?; Ok(Rc::new(device) as DynDevice) } } +/// Trait for device proxies that describes audio device capabilities. pub trait DeviceProxy: ExtensionProvider { fn name(&self) -> Cow<'_, str>; fn device_type(&self) -> DeviceType; - fn default_config(&self) -> Result; + fn default_config(&self) -> Result; fn is_config_supported(&self, config: &StreamConfig) -> bool; - fn buffer_size_range(&self) -> Result<(Option, Option), Error>; + fn buffer_size_range(&self) -> Result<(Option, Option)>; + fn create_stream_raw( + &self, + config: StreamConfig, + callback: DynCallback, + ) -> Result; + + fn create_default_stream_raw( + &self, + requested_type: DeviceType, + callback: DynCallback, + ) -> Result; } -impl DeviceProxy for D { +impl DeviceProxy for D +where + as stream::StreamHandle>::Error: Sync, +{ #[inline] fn name(&self) -> Cow<'_, str> { Device::name(self) @@ -72,7 +80,7 @@ impl DeviceProxy for D { Device::device_type(self) } - fn default_config(&self) -> Result { + fn default_config(&self) -> Result { Ok(Device::default_config(self)?) } @@ -80,61 +88,176 @@ impl DeviceProxy for D { Device::is_config_supported(self, config) } - fn buffer_size_range(&self) -> Result<(Option, Option), Error> { + fn buffer_size_range(&self) -> Result<(Option, Option)> { Ok(Device::buffer_size_range(self)?) } -} -pub trait CreateStream: DeviceProxy { - fn create_stream( + fn create_stream_raw( &self, config: StreamConfig, - callback: Callback, - ) -> Result, Error>; - fn create_default_stream(&self, callback: Callback) -> Result, Error> { - let config = self.default_config()?; - self.create_stream(config, callback) + callback: DynCallback, + ) -> Result { + let handle = Device::create_stream(self, config, callback)?; + Ok(RawStreamHandle::from_handle(handle)) + } + + fn create_default_stream_raw( + &self, + requested_type: DeviceType, + callback: DynCallback, + ) -> Result { + let handle = Device::default_stream(self, requested_type, callback)?; + Ok(RawStreamHandle::from_handle(handle)) } } -impl CreateStream for D -where - as StreamHandle>::Error: 'static + Sync, -{ - fn create_stream( +pub struct DynCallback { + type_id: TypeId, + handle: NonNull<()>, + prepare: unsafe fn(NonNull<()>, CallbackContext), + process_audio: unsafe fn(NonNull<()>, CallbackContext, AudioInput, AudioOutput), +} + +unsafe impl Send for DynCallback {} + +impl DynCallback { + pub fn from_callback(callback: Callback) -> Self { + Self { + type_id: TypeId::of::(), + handle: unsafe { create_nonnull(callback).cast() }, + prepare: |handle, context| { + unsafe { handle.cast::().as_mut() }.prepare(context) + }, + process_audio: |handle, context, input, output| { + unsafe { handle.cast::().as_mut() }.process_audio(context, input, output) + }, + } + } + + pub fn into_inner(self) -> Option> { + if self.type_id != TypeId::of::() { + return None; + } + Some(unsafe { consume_nonnull(self.handle.cast::()) }) + } +} + +impl stream::Callback for DynCallback { + fn prepare(&mut self, context: CallbackContext) { + unsafe { (self.prepare)(self.handle, context) } + } + + fn process_audio( + &mut self, + context: CallbackContext, + input: AudioInput, + output: AudioOutput, + ) { + unsafe { (self.process_audio)(self.handle, context, input, output) } + } +} + +pub struct RawStreamHandle { + handle: Option>, + eject: unsafe fn(NonNull<()>) -> Result, + drop: unsafe fn(NonNull<()>), +} + +impl RawStreamHandle { + pub fn from_handle>( + handle: Handle, + ) -> Self { + let handle = Box::into_raw(Box::new(handle)); + let handle = Some(unsafe { NonNull::new_unchecked(handle).cast() }); + Self { + handle, + eject: |ptr| { + let handle = unsafe { Box::from_raw(ptr.cast::().as_ptr()) }; + Ok(handle.eject()?) + }, + drop: |ptr| { + let _ = unsafe { Box::from_raw(ptr.cast::().as_ptr()) }; + }, + } + } + + pub fn eject(mut self) -> Result { + let Some(handle) = self.handle.take() else { + anyhow::bail!("Stream already ejected"); + }; + Ok(unsafe { (self.eject)(handle)? }) + } +} + +impl Drop for RawStreamHandle { + fn drop(&mut self) { + let Some(handle) = self.handle.take() else { + return; + }; + unsafe { (self.drop)(handle) }; + } +} + +pub trait CreateStreamExt: DeviceProxy { + fn create_stream( &self, config: StreamConfig, callback: Callback, - ) -> Result, Error> { - let handle = Device::create_stream(self, config, callback)?; - Ok(StreamProxy::from_handle(handle)) + ) -> Result> { + let callback = DynCallback::from_callback(callback); + let handle = self.create_stream_raw(config, callback)?; + Ok(unsafe { StreamHandle::from_raw(handle) }) + } + + fn default_stream( + &self, + requested_type: DeviceType, + callback: Callback, + ) -> Result> { + let callback = DynCallback::from_callback(callback); + let handle = self.create_default_stream_raw(requested_type, callback)?; + Ok(unsafe { StreamHandle::from_raw(handle) }) } } -pub struct StreamProxy { - data: Option>, - eject: unsafe fn(NonNull<()>) -> Result, +impl CreateStreamExt for C {} + +#[derive(Debug, thiserror::Error)] +#[error(transparent)] +pub struct AnyError(anyhow::Error); + +pub struct StreamHandle { + __callback: PhantomData, + raw: RawStreamHandle, } -impl StreamProxy { - pub fn from_handle>( - value: Handle, - ) -> Self { - let eject = |data: NonNull<()>| { - // SAFETY: StreamProxy only calls this function when `data` actually points to a valid - // `Handle` instance - let handle = unsafe { data.cast::().read() }; - Ok(handle.eject()?) - }; - let value = Box::into_raw(Box::new(value)); - let data = Some(NonNull::new(value).unwrap().cast()); - Self { data, eject } +impl StreamHandle { + unsafe fn from_raw(raw: RawStreamHandle) -> Self { + Self { + __callback: PhantomData, + raw, + } } +} + +impl stream::StreamHandle> + for StreamHandle +{ + type Error = AnyError; - pub fn eject(mut self) -> Result { - let data = self.data.take().expect("Cannot have ejected twice"); - // SAFETY: Using a `Option` guarantees that we only eject once, and we can only create a - // pointer of the actual underlying type expected by `eject`. - unsafe { (self.eject)(data) } + fn eject(self) -> std::result::Result, Self::Error> { + (|| -> Result> { + let raw_callback = self.raw.eject()?; + Ok(raw_callback.into_inner().unwrap()) + })() + .map_err(AnyError) } } + +unsafe fn create_nonnull(value: T) -> NonNull { + NonNull::new_unchecked(Box::into_raw(Box::new(value))) +} + +unsafe fn consume_nonnull(handle: NonNull) -> Box { + Box::from_raw(handle.as_ptr()) +} diff --git a/crates/interflow-core/src/stream.rs b/crates/interflow-core/src/stream.rs index 70c9855..62464f1 100644 --- a/crates/interflow-core/src/stream.rs +++ b/crates/interflow-core/src/stream.rs @@ -3,6 +3,7 @@ use crate::device::ResolvedStreamConfig; use crate::timing::Timestamp; use crate::traits::ExtensionProvider; use bitflags::bitflags; +use std::any::Any; pub trait StreamProxy: Send + Sync + ExtensionProvider {} diff --git a/crates/interflow-core/src/traits.rs b/crates/interflow-core/src/traits.rs index 417255b..3320095 100644 --- a/crates/interflow-core/src/traits.rs +++ b/crates/interflow-core/src/traits.rs @@ -87,9 +87,9 @@ const _EXTENSION_TRAIT_ASSERTS: () = { typeable::(); }; -impl dyn ExtensionProvider { +pub trait ExtensionProviderExt: ExtensionProvider { /// Look up [`T`] from the extension if it is registered. - pub fn lookup(&self) -> Option<&T> { + fn lookup(&self) -> Option<&T> { let mut selector = Selector::new::(); { self.register(&mut selector); @@ -97,3 +97,5 @@ impl dyn ExtensionProvider { selector.finish::() } } + +impl ExtensionProviderExt for E {} diff --git a/crates/interflow-coreaudio/src/lib.rs b/crates/interflow-coreaudio/src/lib.rs index 8869574..6b3857d 100644 --- a/crates/interflow-coreaudio/src/lib.rs +++ b/crates/interflow-coreaudio/src/lib.rs @@ -78,6 +78,7 @@ pub enum Error { /// The scope given to an audio device is invalid. #[error("Invalid scope {0:?}")] InvalidScope(Scope), + /// No matching devices for the given type. #[error("No matching devices for type: {0:?}")] NoMatchingDevices(DeviceType), } diff --git a/src/lib.rs b/src/lib.rs index f952584..dad39c9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,33 @@ #![doc = include_str!("../README.md")] #![warn(missing_docs)] +use std::rc::Rc; + +use core::{stream, DeviceType}; pub use interflow_core as core; +use interflow_core::proxies::CreateStreamExt; pub mod backends; + +/// Return the default platform. +/// The platform is selected automatically based on your available and enabled backends. +#[allow(unreachable_code)] +pub fn default_platform() -> core::proxies::DynPlatform { + #[cfg(any(target_os = "macos", target_os = "ios"))] + return Rc::new(backends::coreaudio::Platform); + todo!("null backend") +} + +/// Return the default device, using the default platform as returned by [`default_platform`]. +pub fn default_device(device_type: DeviceType) -> anyhow::Result { + default_platform().default_device(device_type) +} + +/// Create a stream using the default device as returned by [`default_device`]. +pub fn default_stream( + device_type: DeviceType, + callback: Callback, +) -> anyhow::Result> { + let device = default_device(device_type)?; + Ok(device.default_stream(device_type, callback)?) +} From c0d68c3aa2263152156f64b1a7d835643c4e2b64 Mon Sep 17 00:00:00 2001 From: Nathan Graule Date: Wed, 22 Jul 2026 17:30:43 +0200 Subject: [PATCH 20/38] wip: rework examples --- examples/enumerate.rs | 24 ++++++++++++++++++++++++ examples/sine_wave.rs | 15 ++++++++------- examples/util/enumerate.rs | 26 -------------------------- examples/util/meter.rs | 9 ++++----- examples/util/mod.rs | 1 - examples/util/sine.rs | 10 +++++----- 6 files changed, 41 insertions(+), 44 deletions(-) create mode 100644 examples/enumerate.rs delete mode 100644 examples/util/enumerate.rs diff --git a/examples/enumerate.rs b/examples/enumerate.rs new file mode 100644 index 0000000..0ab1b43 --- /dev/null +++ b/examples/enumerate.rs @@ -0,0 +1,24 @@ +use interflow::core::{proxies::PlatformProxy, DeviceType}; +use interflow::default_platform; + +pub fn enumerate_devices(platform: &dyn PlatformProxy) -> anyhow::Result<()> { + eprintln!("Driver name : {}", platform.name()); + eprintln!("Default device"); + for (s, device_type) in [("Input", DeviceType::INPUT), ("Output", DeviceType::OUTPUT)] { + let device_type = device_type | DeviceType::PHYSICAL; + eprint!("\t{s}:\t"); + let device = platform.default_device(device_type)?; + } + + eprintln!("All devices"); + for device in platform.list_devices()? { + eprintln!("\t{} ({:?})", device.name(), device.device_type()); + } + Ok(()) +} + +fn main() -> anyhow::Result<()> { + let platform = default_platform(); + enumerate_devices(&*platform)?; + Ok(()) +} diff --git a/examples/sine_wave.rs b/examples/sine_wave.rs index 861db80..0c0f30b 100644 --- a/examples/sine_wave.rs +++ b/examples/sine_wave.rs @@ -1,5 +1,6 @@ use anyhow::Result; -use interflow::core::{proxies::DynDevice, DeviceType}; +use interflow::core::DeviceType; +use interflow_core::stream::StreamHandle; use util::sine::SineWave; mod util; @@ -7,13 +8,13 @@ mod util; fn main() -> Result<()> { env_logger::init(); - let device: DynDevice = todo!("default_device"); - println!("Using device {}", device.name()); - let stream = device - .default_stream(DeviceType::OUTPUT, SineWave::new(440.0)) - .unwrap(); + let stream = interflow::default_stream( + DeviceType::OUTPUT | DeviceType::PHYSICAL, + SineWave::new(440.0), + )?; println!("Press Enter to stop"); std::io::stdin().read_line(&mut String::new())?; - stream.eject().unwrap(); + stream.eject()?; + println!("Stream ejected"); Ok(()) } diff --git a/examples/util/enumerate.rs b/examples/util/enumerate.rs deleted file mode 100644 index c01032f..0000000 --- a/examples/util/enumerate.rs +++ /dev/null @@ -1,26 +0,0 @@ -use interflow::{AudioDevice, AudioDriver, DeviceType}; -use std::error::Error; - -pub fn enumerate_devices(driver: Driver) -> Result<(), Box> -where - ::Error: 'static, -{ - eprintln!("Driver name : {}", Driver::DISPLAY_NAME); - eprintln!("Driver version: {}", driver.version()?); - eprintln!("Default device"); - for (s, device_type) in [("Input", DeviceType::INPUT), ("Output", DeviceType::OUTPUT)] { - let device_type = device_type | DeviceType::PHYSICAL; - eprint!("\t{s}:\t"); - if let Some(device) = driver.default_device(device_type)? { - eprintln!("{}", device.name()); - } else { - eprintln!("None"); - } - } - - eprintln!("All devices"); - for device in driver.list_devices()? { - eprintln!("\t{} ({:?})", device.name(), device.device_type()); - } - Ok(()) -} diff --git a/examples/util/meter.rs b/examples/util/meter.rs index d8f505e..1516546 100644 --- a/examples/util/meter.rs +++ b/examples/util/meter.rs @@ -1,4 +1,4 @@ -use interflow::audio_buffer::AudioRef; +use interflow::core::buffer::AudioRef; #[derive(Debug, Copy, Clone)] pub struct PeakMeter { @@ -42,10 +42,9 @@ impl PeakMeter { } pub fn process_buffer(&mut self, buffer: AudioRef) -> f32 { - let buffer_duration = buffer.num_frames() as f32 * self.dt; - let peak_lin = buffer - .channels() - .flat_map(|ch| ch.iter().copied().max_by(f32::total_cmp)) + let buffer_duration = buffer.frames() as f32 * self.dt; + let peak_lin = (0..buffer.channels()) + .flat_map(|ch| buffer[ch].iter().copied().max_by(f32::total_cmp)) .max_by(f32::total_cmp) .unwrap_or(0.); self.last_out = peak_lin.max(self.last_out * f32::exp(-self.decay * buffer_duration)); diff --git a/examples/util/mod.rs b/examples/util/mod.rs index 531feb7..8b3bb94 100644 --- a/examples/util/mod.rs +++ b/examples/util/mod.rs @@ -9,7 +9,6 @@ use std::sync::atomic::{AtomicBool, AtomicU32, Ordering}; use std::sync::Arc; use std::thread; -pub mod enumerate; pub mod meter; pub mod sine; diff --git a/examples/util/sine.rs b/examples/util/sine.rs index 5e5f596..b0d7887 100644 --- a/examples/util/sine.rs +++ b/examples/util/sine.rs @@ -1,4 +1,4 @@ -use interflow::{AudioCallback, AudioCallbackContext, AudioInput, AudioOutput}; +use interflow_core::stream::{AudioInput, AudioOutput, Callback, CallbackContext}; use std::f32::consts::TAU; pub struct SineWave { @@ -7,13 +7,13 @@ pub struct SineWave { step_frequency_scaling: f32, } -impl AudioCallback for SineWave { - fn prepare(&mut self, context: AudioCallbackContext) { +impl Callback for SineWave { + fn prepare(&mut self, context: CallbackContext) { self.step_frequency_scaling = context.stream_config.sample_rate.recip() as f32; } fn process_audio( &mut self, - context: AudioCallbackContext, + context: CallbackContext, _input: AudioInput, mut output: AudioOutput, ) { @@ -22,7 +22,7 @@ impl AudioCallback for SineWave { context.timestamp.as_seconds() ); let sr = context.timestamp.samplerate as f32; - for i in 0..output.buffer.num_frames() { + for i in 0..output.buffer.frames() { output.buffer.set_mono(i, self.next_sample()); } // Reduce amplitude to not blow up speakers and ears From e3daaac6f2d31e9910197869d093d53ff69791d8 Mon Sep 17 00:00:00 2001 From: Nathan Graule Date: Thu, 23 Jul 2026 09:00:43 +0200 Subject: [PATCH 21/38] wip: fix coreaudio implementation --- Cargo.toml | 2 +- .../interflow-coreaudio/examples/sine_wave.rs | 52 ----- crates/interflow-coreaudio/src/lib.rs | 188 ++++++++++-------- examples/enumerate.rs | 9 +- 4 files changed, 113 insertions(+), 138 deletions(-) delete mode 100644 crates/interflow-coreaudio/examples/sine_wave.rs diff --git a/Cargo.toml b/Cargo.toml index da019fe..a89f900 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,7 @@ default-members = ["."] [workspace.package] version = "0.1.0" edition = "2021" -rust-version = "1.85" +rust-version = "1.91" license = "MIT" [workspace.dependencies] diff --git a/crates/interflow-coreaudio/examples/sine_wave.rs b/crates/interflow-coreaudio/examples/sine_wave.rs deleted file mode 100644 index 12d23a6..0000000 --- a/crates/interflow-coreaudio/examples/sine_wave.rs +++ /dev/null @@ -1,52 +0,0 @@ -use std::f32::consts::TAU; - -use interflow_core::{device::Device, platform::Platform as _, stream::Callback, DeviceType}; -use interflow_coreaudio::Platform; - -fn main() { - Platform - .default_device(DeviceType::PHYSICAL | DeviceType::OUTPUT) - .unwrap() - .default_stream(DeviceType::OUTPUT, Sine::new(440.0)) - .unwrap(); -} - -struct Sine { - phase: f32, - frequency: f32, - step: f32, -} - -impl Sine { - fn new(frequency: f32) -> Self { - Self { - phase: 0.0, - frequency, - step: 0.0, - } - } -} - -impl Callback for Sine { - fn prepare(&mut self, context: interflow_core::stream::CallbackContext) { - self.step = self.frequency * context.stream_config.sample_rate as f32; - self.phase = 0.0; - } - - fn process_audio( - &mut self, - context: interflow_core::stream::CallbackContext, - input: interflow_core::stream::AudioInput, - mut output: interflow_core::stream::AudioOutput, - ) { - let num_frames = output.buffer.channel(0).len(); - for i in 0..num_frames { - let v = (self.phase * TAU).sin() * 0.125; - self.phase += self.step; - while self.phase > 1.0 { - self.phase -= 1.0; - } - output.buffer.frame_mut(i).set_mono(v); - } - } -} diff --git a/crates/interflow-coreaudio/src/lib.rs b/crates/interflow-coreaudio/src/lib.rs index 6b3857d..f36707a 100644 --- a/crates/interflow-coreaudio/src/lib.rs +++ b/crates/interflow-coreaudio/src/lib.rs @@ -3,6 +3,7 @@ //! Interflow backend using CoreAudio for macOS and iOS applications #![warn(missing_docs)] use std::borrow::Cow; +use std::cell::OnceCell; use std::convert::Infallible; use std::num::NonZeroUsize; @@ -81,6 +82,8 @@ pub enum Error { /// No matching devices for the given type. #[error("No matching devices for type: {0:?}")] NoMatchingDevices(DeviceType), + #[error("Duplex devices are not supported")] + DuplexUnavailable, } impl From for Error { @@ -107,24 +110,25 @@ impl platform::Platform for Platform { fn default_device(&self, device_type: DeviceType) -> Result { if device_type.is_output() || !device_type.is_input() { - return Ok(Device::DefaultOutput); + return Ok(Device::default_output()); } let Some(id) = get_default_device_id(device_type.is_input()) else { return Err(Error::NoMatchingDevices(device_type)); }; - Ok(Device::Specific(id)) + Ok(Device::from_id(id)) } fn list_devices(&self) -> Result, Self::Error> { - Ok(get_audio_device_ids()?.into_iter().map(Device::Specific)) + Ok(get_audio_device_ids()?.into_iter().map(Device::from_id)) } } -/// CoreAudio device handle -#[derive(Debug, Copy, Clone)] -pub enum Device { +/// Audio device request type +#[derive(Debug, Default, Copy, Clone)] +pub enum DeviceRequest { /// Automatically connects to the default device output, generic stream. + #[default] DefaultOutput, /// Automatically connects to the default device output, notification stream. SystemOutput, @@ -132,8 +136,8 @@ pub enum Device { Specific(AudioDeviceID), } -impl Device { - pub fn get_audio_unit(&self) -> Result { +impl DeviceRequest { + pub(crate) fn to_audio_unit(&self) -> Result { let io_type = match self { Self::DefaultOutput => IOType::DefaultOutput, Self::SystemOutput => IOType::SystemOutput, @@ -157,24 +161,12 @@ impl Device { )?; Ok(unit) } -} - -impl ExtensionProvider for Device { - fn register<'a, 'sel>(&'a self, selector: &'sel mut Selector<'a>) -> &'sel mut Selector<'a> { - selector - } -} - -impl device::Device for Device { - type Error = Error; - type StreamHandle = StreamHandle; - - fn name(&self) -> Cow<'_, str> { + pub fn name(&self) -> Cow<'_, str> { match self { - Self::DefaultOutput => Cow::Borrowed("Default output"), - Self::SystemOutput => Cow::Borrowed("Notifications output"), - &Self::Specific(id) => match get_device_name(id) { + DeviceRequest::DefaultOutput => Cow::Borrowed("Default output"), + DeviceRequest::SystemOutput => Cow::Borrowed("Notifications output"), + &DeviceRequest::Specific(id) => match get_device_name(id) { Ok(s) => Cow::Owned(s), Err(err) => { log::error!("Cannot get device name for ID: {}: {err}", id); @@ -184,7 +176,7 @@ impl device::Device for Device { } } - fn device_type(&self) -> DeviceType { + pub fn device_type(&self) -> DeviceType { let log_error = |result: Result| { result .inspect_err(|err| { @@ -208,7 +200,85 @@ impl device::Device for Device { type_.set(DeviceType::INPUT, supports_scope(Scope::Input)); type_.set(DeviceType::OUTPUT, supports_scope(Scope::Output)); type_.set(DeviceType::DEFAULT, is_default); - return type_; + type_ + } + + pub fn buffer_size_range(&self) -> Result<(Option, Option), Error> { + match self { + Self::DefaultOutput | Self::SystemOutput => Ok((None, None)), + &Self::Specific(id) => { + let address = AudioObjectPropertyAddress { + mSelector: kAudioDevicePropertyBufferFrameSizeRange, + mScope: if self.device_type().is_input() { + kAudioObjectPropertyScopeInput + } else { + kAudioObjectPropertyScopeOutput + }, + mElement: kAudioObjectPropertyElementMaster, + }; + let range = get_device_property::(id, address)?; + Ok((Some(range.mMinimum as usize), Some(range.mMaximum as usize))) + } + } + } +} + +pub struct Device { + pub(crate) request: DeviceRequest, + audio_unit: OnceCell, +} + +impl Device { + pub fn new(request: DeviceRequest) -> Self { + Self { + request, + audio_unit: OnceCell::new(), + } + } + + pub fn from_id(id: AudioDeviceID) -> Self { + Self::new(DeviceRequest::Specific(id)) + } + + pub fn default_output() -> Self { + Self::new(DeviceRequest::DefaultOutput) + } + + pub fn get_audio_unit(&self) -> Result<&AudioUnit, Error> { + if let Some(unit) = self.audio_unit.get() { + return Ok(unit); + } + let unit = self.request.to_audio_unit()?; + let result = self.audio_unit.set(unit); + assert!(result.is_ok()); + Ok(self.audio_unit.get().unwrap()) + } + + pub fn into_audio_unit(mut self) -> Result { + match self.audio_unit.take() { + Some(unit) => Ok(unit), + None => self.request.to_audio_unit(), + } + } +} + +impl ExtensionProvider for Device { + fn register<'a, 'sel>(&'a self, selector: &'sel mut Selector<'a>) -> &'sel mut Selector<'a> { + selector + } +} + +impl device::Device for Device { + type Error = Error; + + type StreamHandle = StreamHandle; + + fn name(&self) -> Cow<'_, str> { + self.request.name() + } + + fn device_type(&self) -> DeviceType { + self.request.device_type() } fn default_config(&self) -> Result { @@ -221,24 +291,7 @@ impl device::Device for Device { .output_stream_format() .map(|fmt| fmt.channels as usize) .unwrap_or(0); - let buffer_size_range = { - match self { - Self::DefaultOutput | Self::SystemOutput => (None, None), - &Self::Specific(id) => { - let address = AudioObjectPropertyAddress { - mSelector: kAudioDevicePropertyBufferFrameSizeRange, - mScope: if self.device_type().is_input() { - kAudioObjectPropertyScopeInput - } else { - kAudioObjectPropertyScopeOutput - }, - mElement: kAudioObjectPropertyElementMaster, - }; - let range = get_device_property::(id, address)?; - (Some(range.mMinimum as usize), Some(range.mMaximum as usize)) - } - } - }; + let buffer_size_range = self.request.buffer_size_range()?; Ok(StreamConfig { sample_rate: au.sample_rate()?, input_channels, @@ -319,10 +372,9 @@ impl StreamHandle { callback: Callback, ) -> Result { let requested_type = stream_config.requested_device_type(); - assert!( - !requested_type.is_duplex(), - "CoreAudio does not support native duplex mode" - ); + if requested_type.is_duplex() { + return Err(Error::DuplexUnavailable); + } let unsupported = device.device_type() & !requested_type; if !unsupported.is_empty() { @@ -331,7 +383,11 @@ impl StreamHandle { device = device.name() ); } - let unit = device.get_audio_unit()?; + // let unit = match device.audio_unit.take() { + // Some(unit) => unit, + // None => device.request.to_audio_unit()?, + // }; + let unit = todo!(); if requested_type.is_input() { Self::new_input(unit, stream_config, callback) } else { @@ -521,37 +577,3 @@ impl StreamHandle { }) } } - -#[cfg(test)] -mod tests { - use super::*; - use coreaudio_sys::{kAudioObjectPropertyElementMaster, kAudioObjectPropertyScopeOutput}; - use interflow_core::platform::Platform as _; - - #[test] - fn test_set_device_buffersize() { - let platform = Platform; - let Some(Device::Specific(device_id)) = platform - .list_devices() - .unwrap() - .into_iter() - .find(|d| d.device_type().is_output()) - else { - println!("Skipping test: No specific output device found."); - return; - }; - - let buffer_size = 256u32; - - let property_address = AudioObjectPropertyAddress { - mSelector: kAudioDevicePropertyBufferFrameSize, - mScope: kAudioObjectPropertyScopeOutput, - mElement: kAudioObjectPropertyElementMaster, - }; - set_device_property(device_id, property_address, &buffer_size).unwrap(); - - let actual_buffer_size: u32 = get_device_property(device_id, property_address).unwrap(); - - assert_eq!(buffer_size, actual_buffer_size); - } -} diff --git a/examples/enumerate.rs b/examples/enumerate.rs index 0ab1b43..a84b9ed 100644 --- a/examples/enumerate.rs +++ b/examples/enumerate.rs @@ -7,10 +7,14 @@ pub fn enumerate_devices(platform: &dyn PlatformProxy) -> anyhow::Result<()> { for (s, device_type) in [("Input", DeviceType::INPUT), ("Output", DeviceType::OUTPUT)] { let device_type = device_type | DeviceType::PHYSICAL; eprint!("\t{s}:\t"); - let device = platform.default_device(device_type)?; + let Ok(device) = platform.default_device(device_type) else { + println!("no default device"); + continue; + }; + println!("{}", device.name()); } - eprintln!("All devices"); + eprintln!("\nAll devices"); for device in platform.list_devices()? { eprintln!("\t{} ({:?})", device.name(), device.device_type()); } @@ -18,6 +22,7 @@ pub fn enumerate_devices(platform: &dyn PlatformProxy) -> anyhow::Result<()> { } fn main() -> anyhow::Result<()> { + env_logger::init(); let platform = default_platform(); enumerate_devices(&*platform)?; Ok(()) From 4e11b76ecad4f5b69e611fb7c87aabc7e235d395 Mon Sep 17 00:00:00 2001 From: Nathan Graule Date: Thu, 23 Jul 2026 09:50:27 +0200 Subject: [PATCH 22/38] chore: use anyhow::Context when delegating to `create_stream` --- crates/interflow-core/src/proxies.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/crates/interflow-core/src/proxies.rs b/crates/interflow-core/src/proxies.rs index f9a9860..8875bbc 100644 --- a/crates/interflow-core/src/proxies.rs +++ b/crates/interflow-core/src/proxies.rs @@ -3,7 +3,7 @@ use crate::platform::Platform; use crate::stream::{AudioInput, AudioOutput, CallbackContext}; use crate::traits::ExtensionProvider; use crate::{stream, DeviceType}; -use anyhow::Result; +use anyhow::{Context, Result}; use std::any::{Any, TypeId}; use std::borrow::Cow; use std::marker::PhantomData; @@ -97,7 +97,7 @@ where config: StreamConfig, callback: DynCallback, ) -> Result { - let handle = Device::create_stream(self, config, callback)?; + let handle = Device::create_stream(self, config, callback).context("Cannot open stream")?; Ok(RawStreamHandle::from_handle(handle)) } @@ -106,7 +106,8 @@ where requested_type: DeviceType, callback: DynCallback, ) -> Result { - let handle = Device::default_stream(self, requested_type, callback)?; + let handle = + Device::default_stream(self, requested_type, callback).context("Cannot open stream")?; Ok(RawStreamHandle::from_handle(handle)) } } From a4e2e209c2816f94a19ade83408865630c8cf811 Mon Sep 17 00:00:00 2001 From: Nathan Graule Date: Thu, 23 Jul 2026 10:00:49 +0200 Subject: [PATCH 23/38] fix: audio unit creation routine only needs to enable I/O on specific AudioDeviceID --- crates/interflow-coreaudio/src/lib.rs | 51 +++++++++++++-------------- 1 file changed, 24 insertions(+), 27 deletions(-) diff --git a/crates/interflow-coreaudio/src/lib.rs b/crates/interflow-coreaudio/src/lib.rs index f36707a..82e37d6 100644 --- a/crates/interflow-coreaudio/src/lib.rs +++ b/crates/interflow-coreaudio/src/lib.rs @@ -138,28 +138,29 @@ pub enum DeviceRequest { impl DeviceRequest { pub(crate) fn to_audio_unit(&self) -> Result { - let io_type = match self { - Self::DefaultOutput => IOType::DefaultOutput, - Self::SystemOutput => IOType::SystemOutput, - Self::Specific(..) => IOType::HalOutput, - }; - let mut unit = AudioUnit::new(io_type)?; - let device_type = self.device_type(); - let value = if device_type.is_input() { 1u32 } else { 0 }; - unit.set_property( - kAudioOutputUnitProperty_EnableIO, - Scope::Input, - Element::Input, - Some(&value), - )?; - let value = if device_type.is_output() { 1u32 } else { 0 }; - unit.set_property( - kAudioOutputUnitProperty_EnableIO, - Scope::Output, - Element::Output, - Some(&value), - )?; - Ok(unit) + match self { + Self::DefaultOutput => Ok(AudioUnit::new(IOType::DefaultOutput)?), + Self::SystemOutput => Ok(AudioUnit::new(IOType::SystemOutput)?), + Self::Specific(id) => { + let mut unit = AudioUnit::new(IOType::HalOutput)?; + let device_type = self.device_type(); + let value = if device_type.is_input() { 1u32 } else { 0 }; + unit.set_property( + kAudioOutputUnitProperty_EnableIO, + Scope::Input, + Element::Input, + Some(&value), + )?; + let value = if device_type.is_output() { 1u32 } else { 0 }; + unit.set_property( + kAudioOutputUnitProperty_EnableIO, + Scope::Output, + Element::Output, + Some(&value), + )?; + Ok(unit) + } + } } pub fn name(&self) -> Cow<'_, str> { @@ -383,11 +384,7 @@ impl StreamHandle { device = device.name() ); } - // let unit = match device.audio_unit.take() { - // Some(unit) => unit, - // None => device.request.to_audio_unit()?, - // }; - let unit = todo!(); + let unit = device.request.to_audio_unit()?; if requested_type.is_input() { Self::new_input(unit, stream_config, callback) } else { From b815a09f10592370e95769a62bdfec6212ccdf07 Mon Sep 17 00:00:00 2001 From: Nathan Graule Date: Thu, 23 Jul 2026 15:13:10 +0200 Subject: [PATCH 24/38] feat(coreaudio): move to different backends + add extension to get audio unit + prelude modules + warnings fix --- crates/interflow-core/src/lib.rs | 19 +- crates/interflow-core/src/stream.rs | 1 - crates/interflow-coreaudio/src/device.rs | 229 +++++++ crates/interflow-coreaudio/src/lib.rs | 559 +--------------- crates/interflow-coreaudio/src/platform.rs | 38 ++ crates/interflow-coreaudio/src/stream.rs | 275 ++++++++ crates/interflow-coreaudio/src/utils.rs | 41 ++ examples/enumerate.rs | 3 +- examples/sine_wave.rs | 5 +- src/audio_buffer.rs | 715 --------------------- src/channel_map.rs | 223 ------- src/lib.rs | 9 +- src/prelude.rs | 8 - src/sample.rs | 72 --- src/timestamp.rs | 201 ------ 15 files changed, 621 insertions(+), 1777 deletions(-) create mode 100644 crates/interflow-coreaudio/src/device.rs create mode 100644 crates/interflow-coreaudio/src/platform.rs create mode 100644 crates/interflow-coreaudio/src/stream.rs create mode 100644 crates/interflow-coreaudio/src/utils.rs delete mode 100644 src/audio_buffer.rs delete mode 100644 src/channel_map.rs delete mode 100644 src/prelude.rs delete mode 100644 src/sample.rs delete mode 100644 src/timestamp.rs diff --git a/crates/interflow-core/src/lib.rs b/crates/interflow-core/src/lib.rs index aae486d..a092e29 100644 --- a/crates/interflow-core/src/lib.rs +++ b/crates/interflow-core/src/lib.rs @@ -1,3 +1,5 @@ +use bitflags::bitflags; + pub mod buffer; pub mod device; pub mod platform; @@ -6,11 +8,18 @@ pub mod stream; pub mod timing; pub mod traits; -use std::rc::Rc; - -use bitflags::bitflags; - -use crate::proxies::PlatformProxy; +pub mod prelude { + pub use super::DeviceType; + pub use crate::buffer::{AudioBuffer, AudioMut, AudioRef}; + pub use crate::device::{self, Device as _}; + pub use crate::platform; + pub use crate::proxies::{self, CreateStreamExt, DeviceProxy, PlatformProxy}; + pub use crate::stream::{ + self, AudioInput, AudioOutput, ChannelFlags, StreamHandle, StreamLatency, StreamProxy, + }; + pub use crate::timing::{self, *}; + pub use crate::traits::{self, *}; +} bitflags! { /// Represents the types/capabilities of an audio device. diff --git a/crates/interflow-core/src/stream.rs b/crates/interflow-core/src/stream.rs index 62464f1..70c9855 100644 --- a/crates/interflow-core/src/stream.rs +++ b/crates/interflow-core/src/stream.rs @@ -3,7 +3,6 @@ use crate::device::ResolvedStreamConfig; use crate::timing::Timestamp; use crate::traits::ExtensionProvider; use bitflags::bitflags; -use std::any::Any; pub trait StreamProxy: Send + Sync + ExtensionProvider {} diff --git a/crates/interflow-coreaudio/src/device.rs b/crates/interflow-coreaudio/src/device.rs new file mode 100644 index 0000000..eaf6ab9 --- /dev/null +++ b/crates/interflow-coreaudio/src/device.rs @@ -0,0 +1,229 @@ +//! CoreAudio device implementation +use crate::stream::Handle; +use crate::{utils, Error}; +use coreaudio::audio_unit::macos_helpers::{ + get_audio_device_supports_scope, get_default_device_id, get_device_name, +}; +use coreaudio::audio_unit::{AudioUnit, Element, IOType, Scope}; +use coreaudio_sys::{ + kAudioDevicePropertyBufferFrameSizeRange, kAudioObjectPropertyElementMaster, + kAudioObjectPropertyScopeInput, kAudioObjectPropertyScopeOutput, + kAudioOutputUnitProperty_EnableIO, AudioDeviceID, AudioObjectPropertyAddress, AudioValueRange, +}; +use interflow_core::device::StreamConfig; +use interflow_core::traits::{ExtensionProvider, Selector}; +use interflow_core::{device, stream, DeviceType}; +use std::borrow::Cow; +use std::cell::OnceCell; + +/// Audio device request type. CoreAudio has separate code paths for getting the default device, and getting a +/// specific ID. Furthermore, "default device"s are automatically re-routed when the user changes the default device. +#[derive(Debug, Default, Copy, Clone)] +pub enum DeviceRequest { + /// Automatically connects to the default device output, generic stream. + #[default] + DefaultOutput, + /// Automatically connects to the default device output, notification stream. + SystemOutput, + /// Connect to this specific output. + Specific(AudioDeviceID), +} + +impl DeviceRequest { + pub(crate) fn to_audio_unit(&self) -> Result { + match self { + Self::DefaultOutput => Ok(AudioUnit::new(IOType::DefaultOutput)?), + Self::SystemOutput => Ok(AudioUnit::new(IOType::SystemOutput)?), + &Self::Specific(..) => { + // TODO: Use provided ID + let mut unit = AudioUnit::new(IOType::HalOutput)?; + let device_type = self.device_type(); + let value = if device_type.is_input() { 1u32 } else { 0 }; + unit.set_property( + kAudioOutputUnitProperty_EnableIO, + Scope::Input, + Element::Input, + Some(&value), + )?; + let value = if device_type.is_output() { 1u32 } else { 0 }; + unit.set_property( + kAudioOutputUnitProperty_EnableIO, + Scope::Output, + Element::Output, + Some(&value), + )?; + Ok(unit) + } + } + } + + /// Return the name associated with this [`DeviceRequest`]. This is a descriptive name, and does not uniquely + /// identify the device. + pub fn name(&self) -> Cow<'_, str> { + match self { + DeviceRequest::DefaultOutput => Cow::Borrowed("Default output"), + DeviceRequest::SystemOutput => Cow::Borrowed("Notifications output"), + &DeviceRequest::Specific(id) => match get_device_name(id) { + Ok(s) => Cow::Owned(s), + Err(err) => { + log::error!("Cannot get device name for ID: {}: {err}", id); + Cow::Borrowed("") + } + }, + } + } + + /// Returns the [`DeviceType`] that best describes this [`DeviceRequest`]. + pub fn device_type(&self) -> DeviceType { + let log_error = |result: Result| { + result + .inspect_err(|err| { + log::error!("Cannot get device type for {:?}: {err}", self.name()); + }) + .unwrap_or(false) + }; + let supports_scope = |scope| match self { + Self::DefaultOutput | Self::SystemOutput if matches!(scope, Scope::Output) => true, + &Self::Specific(id) => log_error(get_audio_device_supports_scope(id, scope)), + _ => false, + }; + let is_default = match self { + Self::DefaultOutput | Self::SystemOutput => true, + &Self::Specific(id) => { + get_default_device_id(true) == Some(id) || get_default_device_id(false) == Some(id) + } + }; + + let mut type_ = DeviceType::empty(); + type_.set(DeviceType::INPUT, supports_scope(Scope::Input)); + type_.set(DeviceType::OUTPUT, supports_scope(Scope::Output)); + type_.set(DeviceType::DEFAULT, is_default); + type_ + } + + /// Queries the accepted range of buffer sizes that the device accepts. + pub fn buffer_size_range(&self) -> Result<(Option, Option), Error> { + match self { + Self::DefaultOutput | Self::SystemOutput => Ok((None, None)), + &Self::Specific(id) => { + let address = AudioObjectPropertyAddress { + mSelector: kAudioDevicePropertyBufferFrameSizeRange, + mScope: if self.device_type().is_input() { + kAudioObjectPropertyScopeInput + } else { + kAudioObjectPropertyScopeOutput + }, + mElement: kAudioObjectPropertyElementMaster, + }; + let range = utils::get_device_property::(id, address)?; + Ok((Some(range.mMinimum as usize), Some(range.mMaximum as usize))) + } + } + } +} + +/// CoreAudio device type. Contains the requested device (including default output/notification stream) and +/// associated audio unit. +pub struct Device { + pub(crate) request: DeviceRequest, + audio_unit: OnceCell, +} + +impl Device { + /// Create a new device from a [`DeviceRequest`]. + pub fn new(request: DeviceRequest) -> Self { + Self { + request, + audio_unit: OnceCell::new(), + } + } + + /// Create a device from a specific device ID. + pub fn from_id(id: AudioDeviceID) -> Self { + Self::new(DeviceRequest::Specific(id)) + } + + /// Create a device following the default output. + pub fn default_output() -> Self { + Self::new(DeviceRequest::DefaultOutput) + } + + /// Consumes this device object, returning the associated Audio Unit. + pub fn into_audio_unit(mut self) -> Result { + match self.audio_unit.take() { + Some(unit) => Ok(unit), + None => self.request.to_audio_unit(), + } + } +} + +impl ExtensionProvider for Device { + fn register<'a, 'sel>(&'a self, selector: &'sel mut Selector<'a>) -> &'sel mut Selector<'a> { + selector.register::(self) + } +} + +impl device::Device for Device { + type Error = Error; + + type StreamHandle = Handle; + + fn name(&self) -> Cow<'_, str> { + self.request.name() + } + + fn device_type(&self) -> DeviceType { + self.request.device_type() + } + + fn default_config(&self) -> Result { + let au = self.get_audio_unit()?; + let input_channels = au + .input_stream_format() + .map(|fmt| fmt.channels as usize) + .unwrap_or(0); + let output_channels = au + .output_stream_format() + .map(|fmt| fmt.channels as usize) + .unwrap_or(0); + let buffer_size_range = self.request.buffer_size_range()?; + Ok(StreamConfig { + sample_rate: au.sample_rate()?, + input_channels, + output_channels, + buffer_size_range, + exclusive: false, + }) + } + + fn is_config_supported(&self, _config: &StreamConfig) -> bool { + true + } + + fn create_stream( + &self, + stream_config: StreamConfig, + callback: Callback, + ) -> Result, Self::Error> { + Handle::new(self, stream_config, callback) + } +} + +/// Extension trait for [`Device`]. Either use directly, or lookup the trait with `device.lookup::()`. +pub trait CoreAudioDeviceExt { + /// Return the audio unit associated with this device. Creates it at most once; subsequent calls return the same + /// object. + fn get_audio_unit(&self) -> Result<&AudioUnit, Error>; +} + +impl CoreAudioDeviceExt for Device { + fn get_audio_unit(&self) -> Result<&AudioUnit, Error> { + if let Some(unit) = self.audio_unit.get() { + return Ok(unit); + } + let unit = self.request.to_audio_unit()?; + let result = self.audio_unit.set(unit); + assert!(result.is_ok()); + Ok(self.audio_unit.get().unwrap()) + } +} diff --git a/crates/interflow-coreaudio/src/lib.rs b/crates/interflow-coreaudio/src/lib.rs index 82e37d6..53dac73 100644 --- a/crates/interflow-coreaudio/src/lib.rs +++ b/crates/interflow-coreaudio/src/lib.rs @@ -2,71 +2,20 @@ //! //! Interflow backend using CoreAudio for macOS and iOS applications #![warn(missing_docs)] -use std::borrow::Cow; -use std::cell::OnceCell; +use coreaudio::audio_unit; +use interflow_core::DeviceType; use std::convert::Infallible; -use std::num::NonZeroUsize; -use coreaudio::audio_unit::audio_format::LinearPcmFlags; -use coreaudio::audio_unit::macos_helpers::{ - get_audio_device_ids, get_audio_device_supports_scope, get_default_device_id, get_device_name, -}; -use coreaudio::audio_unit::render_callback::{data, Args}; -use coreaudio::audio_unit::{AudioUnit, Element, IOType, SampleFormat, Scope, StreamFormat}; -use coreaudio_sys::{ - kAudioDevicePropertyBufferFrameSize, kAudioDevicePropertyBufferFrameSizeRange, - kAudioObjectPropertyElementMaster, kAudioObjectPropertyScopeInput, - kAudioObjectPropertyScopeOutput, kAudioOutputUnitProperty_EnableIO, - kAudioUnitProperty_StreamFormat, AudioDeviceID, AudioObjectGetPropertyData, - AudioObjectPropertyAddress, AudioValueRange, -}; -use interflow_core::{ - buffer::AudioBuffer, - device::{self, Device as _, ResolvedStreamConfig, StreamConfig}, - platform, stream, - stream::{AudioInput, AudioOutput, CallbackContext, StreamProxy}, - timing::Timestamp, - traits::{ExtensionProvider, Selector}, - DeviceType, -}; +pub mod device; +pub mod platform; +pub mod stream; +mod utils; -fn get_device_property( - device_id: AudioDeviceID, - address: AudioObjectPropertyAddress, -) -> Result { - let mut data = std::mem::MaybeUninit::::uninit(); - let mut size = size_of::() as u32; - let status = unsafe { - AudioObjectGetPropertyData( - device_id, - &address, - 0, - std::ptr::null(), - &mut size, - data.as_mut_ptr() as *mut _, - ) - }; - coreaudio::Error::from_os_status(status)?; - Ok(unsafe { data.assume_init() }) -} - -fn set_device_property( - device_id: AudioDeviceID, - address: AudioObjectPropertyAddress, - data: &T, -) -> Result<(), coreaudio::Error> { - let size = size_of::() as u32; - let status = unsafe { - coreaudio_sys::AudioObjectSetPropertyData( - device_id, - &address, - 0, - std::ptr::null(), - size, - data as *const T as *const _, - ) - }; - coreaudio::Error::from_os_status(status) +/// Prelude module. Import all with `interflow_coreaudio::prelude::*`. +pub mod prelude { + pub use crate::device::{Device, DeviceRequest}; + pub use crate::platform::Platform; + pub use crate::stream::Handle; } /// Type of errors from the CoreAudio backend @@ -78,10 +27,11 @@ pub enum Error { Backend(#[from] coreaudio::Error), /// The scope given to an audio device is invalid. #[error("Invalid scope {0:?}")] - InvalidScope(Scope), + InvalidScope(audio_unit::Scope), /// No matching devices for the given type. #[error("No matching devices for type: {0:?}")] NoMatchingDevices(DeviceType), + /// User has requested a duplex (both input and output) stream, which is not supported by CoreAudio. #[error("Duplex devices are not supported")] DuplexUnavailable, } @@ -91,486 +41,3 @@ impl From for Error { unreachable!() } } - -/// The CoreAudio driver. -pub struct Platform; - -impl ExtensionProvider for Platform { - fn register<'a, 'sel>(&'a self, selector: &'sel mut Selector<'a>) -> &'sel mut Selector<'a> { - selector - } -} - -impl platform::Platform for Platform { - type Error = Error; - - type Device = Device; - - const NAME: &'static str = "CoreAudio"; - - fn default_device(&self, device_type: DeviceType) -> Result { - if device_type.is_output() || !device_type.is_input() { - return Ok(Device::default_output()); - } - - let Some(id) = get_default_device_id(device_type.is_input()) else { - return Err(Error::NoMatchingDevices(device_type)); - }; - Ok(Device::from_id(id)) - } - - fn list_devices(&self) -> Result, Self::Error> { - Ok(get_audio_device_ids()?.into_iter().map(Device::from_id)) - } -} - -/// Audio device request type -#[derive(Debug, Default, Copy, Clone)] -pub enum DeviceRequest { - /// Automatically connects to the default device output, generic stream. - #[default] - DefaultOutput, - /// Automatically connects to the default device output, notification stream. - SystemOutput, - /// Connect to this specific output. - Specific(AudioDeviceID), -} - -impl DeviceRequest { - pub(crate) fn to_audio_unit(&self) -> Result { - match self { - Self::DefaultOutput => Ok(AudioUnit::new(IOType::DefaultOutput)?), - Self::SystemOutput => Ok(AudioUnit::new(IOType::SystemOutput)?), - Self::Specific(id) => { - let mut unit = AudioUnit::new(IOType::HalOutput)?; - let device_type = self.device_type(); - let value = if device_type.is_input() { 1u32 } else { 0 }; - unit.set_property( - kAudioOutputUnitProperty_EnableIO, - Scope::Input, - Element::Input, - Some(&value), - )?; - let value = if device_type.is_output() { 1u32 } else { 0 }; - unit.set_property( - kAudioOutputUnitProperty_EnableIO, - Scope::Output, - Element::Output, - Some(&value), - )?; - Ok(unit) - } - } - } - - pub fn name(&self) -> Cow<'_, str> { - match self { - DeviceRequest::DefaultOutput => Cow::Borrowed("Default output"), - DeviceRequest::SystemOutput => Cow::Borrowed("Notifications output"), - &DeviceRequest::Specific(id) => match get_device_name(id) { - Ok(s) => Cow::Owned(s), - Err(err) => { - log::error!("Cannot get device name for ID: {}: {err}", id); - Cow::Borrowed("") - } - }, - } - } - - pub fn device_type(&self) -> DeviceType { - let log_error = |result: Result| { - result - .inspect_err(|err| { - log::error!("Cannot get device type for {:?}: {err}", self.name()); - }) - .unwrap_or(false) - }; - let supports_scope = |scope| match self { - Self::DefaultOutput | Self::SystemOutput if matches!(scope, Scope::Output) => true, - &Self::Specific(id) => log_error(get_audio_device_supports_scope(id, scope)), - _ => false, - }; - let is_default = match self { - Self::DefaultOutput | Self::SystemOutput => true, - &Self::Specific(id) => { - get_default_device_id(true) == Some(id) || get_default_device_id(false) == Some(id) - } - }; - - let mut type_ = DeviceType::empty(); - type_.set(DeviceType::INPUT, supports_scope(Scope::Input)); - type_.set(DeviceType::OUTPUT, supports_scope(Scope::Output)); - type_.set(DeviceType::DEFAULT, is_default); - type_ - } - - pub fn buffer_size_range(&self) -> Result<(Option, Option), Error> { - match self { - Self::DefaultOutput | Self::SystemOutput => Ok((None, None)), - &Self::Specific(id) => { - let address = AudioObjectPropertyAddress { - mSelector: kAudioDevicePropertyBufferFrameSizeRange, - mScope: if self.device_type().is_input() { - kAudioObjectPropertyScopeInput - } else { - kAudioObjectPropertyScopeOutput - }, - mElement: kAudioObjectPropertyElementMaster, - }; - let range = get_device_property::(id, address)?; - Ok((Some(range.mMinimum as usize), Some(range.mMaximum as usize))) - } - } - } -} - -pub struct Device { - pub(crate) request: DeviceRequest, - audio_unit: OnceCell, -} - -impl Device { - pub fn new(request: DeviceRequest) -> Self { - Self { - request, - audio_unit: OnceCell::new(), - } - } - - pub fn from_id(id: AudioDeviceID) -> Self { - Self::new(DeviceRequest::Specific(id)) - } - - pub fn default_output() -> Self { - Self::new(DeviceRequest::DefaultOutput) - } - - pub fn get_audio_unit(&self) -> Result<&AudioUnit, Error> { - if let Some(unit) = self.audio_unit.get() { - return Ok(unit); - } - let unit = self.request.to_audio_unit()?; - let result = self.audio_unit.set(unit); - assert!(result.is_ok()); - Ok(self.audio_unit.get().unwrap()) - } - - pub fn into_audio_unit(mut self) -> Result { - match self.audio_unit.take() { - Some(unit) => Ok(unit), - None => self.request.to_audio_unit(), - } - } -} - -impl ExtensionProvider for Device { - fn register<'a, 'sel>(&'a self, selector: &'sel mut Selector<'a>) -> &'sel mut Selector<'a> { - selector - } -} - -impl device::Device for Device { - type Error = Error; - - type StreamHandle = StreamHandle; - - fn name(&self) -> Cow<'_, str> { - self.request.name() - } - - fn device_type(&self) -> DeviceType { - self.request.device_type() - } - - fn default_config(&self) -> Result { - let au = self.get_audio_unit()?; - let input_channels = au - .input_stream_format() - .map(|fmt| fmt.channels as usize) - .unwrap_or(0); - let output_channels = au - .output_stream_format() - .map(|fmt| fmt.channels as usize) - .unwrap_or(0); - let buffer_size_range = self.request.buffer_size_range()?; - Ok(StreamConfig { - sample_rate: au.sample_rate()?, - input_channels, - output_channels, - buffer_size_range, - exclusive: false, - }) - } - - fn is_config_supported(&self, _config: &StreamConfig) -> bool { - true - } - - fn create_stream( - &self, - stream_config: StreamConfig, - callback: Callback, - ) -> Result, Self::Error> { - StreamHandle::new(self, stream_config, callback) - } -} - -/// Stream type created by opening up a stream on a [`Device`]. -pub struct StreamHandle { - audio_unit: AudioUnit, - callback_retrieve: oneshot::Sender>, -} - -impl stream::StreamHandle for StreamHandle { - type Error = Infallible; - - fn eject(mut self) -> Result { - let (tx, rx) = oneshot::channel(); - self.callback_retrieve - .send(tx) - .expect("Callback receiver cannot have been dropped yet"); - let callback = rx.recv().expect("Oneshot receiver must be used"); - self.audio_unit.free_input_callback(); - self.audio_unit.free_render_callback(); - Ok(callback) - } -} - -fn input_stream_format(sample_rate: f64, channel_count: usize) -> StreamFormat { - StreamFormat { - sample_rate, - sample_format: SampleFormat::I16, - flags: LinearPcmFlags::IS_SIGNED_INTEGER, - channels: channel_count as _, - } -} - -fn output_stream_format(sample_rate: f64, channel_count: usize) -> StreamFormat { - StreamFormat { - sample_rate, - sample_format: SampleFormat::F32, - flags: LinearPcmFlags::IS_NON_INTERLEAVED | LinearPcmFlags::IS_FLOAT, - channels: channel_count as _, - } -} - -struct DummyStreamProxy; - -impl ExtensionProvider for DummyStreamProxy { - fn register<'a, 'sel>(&'a self, selector: &'sel mut Selector<'a>) -> &'sel mut Selector<'a> { - selector - } -} - -impl StreamProxy for DummyStreamProxy {} - -static STREAM_PROXY: DummyStreamProxy = DummyStreamProxy; - -impl StreamHandle { - fn new( - device: &Device, - stream_config: StreamConfig, - callback: Callback, - ) -> Result { - let requested_type = stream_config.requested_device_type(); - if requested_type.is_duplex() { - return Err(Error::DuplexUnavailable); - } - - let unsupported = device.device_type() & !requested_type; - if !unsupported.is_empty() { - log::warn!( - "Cannot request {unsupported:?} for {device}, ignoring", - device = device.name() - ); - } - let unit = device.request.to_audio_unit()?; - if requested_type.is_input() { - Self::new_input(unit, stream_config, callback) - } else { - Self::new_output(unit, stream_config, callback) - } - } - - fn new_input( - mut audio_unit: AudioUnit, - stream_config: StreamConfig, - mut callback: Callback, - ) -> Result { - let asbd = - input_stream_format(stream_config.sample_rate, stream_config.input_channels).to_asbd(); - audio_unit.set_property( - kAudioUnitProperty_StreamFormat, - Scope::Output, - Element::Input, - Some(&asbd), - )?; - let frame_count: u32 = audio_unit.get_property( - kAudioDevicePropertyBufferFrameSize, - Scope::Input, - Element::Input, - )?; - let resolved_config = ResolvedStreamConfig { - sample_rate: asbd.mSampleRate, - input_channels: asbd.mChannelsPerFrame as _, - output_channels: 0, - max_frame_count: frame_count as _, - }; - let mut buffer = AudioBuffer::zeroed( - NonZeroUsize::new(frame_count as _).unwrap(), - NonZeroUsize::new(asbd.mChannelsPerFrame as _).unwrap(), - ); - - let (tx, rx) = oneshot::channel::>(); - callback.prepare(CallbackContext { - stream_config: &resolved_config, - timestamp: Timestamp::new(asbd.mSampleRate), - stream_proxy: &STREAM_PROXY, - }); - let mut callback = Some(callback); - - audio_unit.set_input_callback(move |args: Args>| { - if let Ok(sender) = rx.try_recv() { - sender.send(callback.take().unwrap()).unwrap(); - return Err(()); - } - let num_frames = args.num_frames; - let channels = asbd.mChannelsPerFrame as usize; - let input_data = args.data.buffer; - - for frame_idx in 0..num_frames { - for ch in 0..channels { - let sample = input_data[frame_idx * channels + ch]; - buffer - .frame_mut(frame_idx) - .set(ch, (sample as f32) / (i16::MAX as f32)); - } - } - - let timestamp = Timestamp::from_count( - resolved_config.sample_rate, - args.time_stamp.mSampleTime as _, - ); - - let input = AudioInput { - buffer: buffer.as_ref(), - timestamp, - channel_flags: &[], - }; - - let mut dummy_buf = AudioBuffer::zeroed( - NonZeroUsize::new(1).unwrap(), - NonZeroUsize::new(channels as _).unwrap(), - ); - let dummy_output = AudioOutput { - buffer: dummy_buf.as_mut(), - timestamp: Timestamp::new(asbd.mSampleRate), - channel_flags: &[], - }; - - if let Some(callback) = &mut callback { - callback.process_audio( - CallbackContext { - stream_config: &resolved_config, - timestamp, - stream_proxy: &STREAM_PROXY, - }, - input, - dummy_output, - ); - } - Ok(()) - })?; - audio_unit.start()?; - Ok(Self { - audio_unit, - callback_retrieve: tx, - }) - } - - fn new_output( - mut audio_unit: AudioUnit, - stream_config: StreamConfig, - mut callback: Callback, - ) -> Result { - let asbd = output_stream_format(stream_config.sample_rate, stream_config.output_channels) - .to_asbd(); - audio_unit.set_property( - kAudioUnitProperty_StreamFormat, - Scope::Input, - Element::Output, - Some(&asbd), - )?; - let frame_size: u32 = audio_unit.get_property( - kAudioDevicePropertyBufferFrameSize, - Scope::Output, - Element::Output, - )?; - let resolved_config = ResolvedStreamConfig { - sample_rate: asbd.mSampleRate, - input_channels: 0, - output_channels: asbd.mChannelsPerFrame as _, - max_frame_count: frame_size as _, - }; - let mut buffer = AudioBuffer::zeroed( - NonZeroUsize::new(frame_size as _).unwrap(), - NonZeroUsize::new(asbd.mChannelsPerFrame as _).unwrap(), - ); - - let (tx, rx) = oneshot::channel::>(); - callback.prepare(CallbackContext { - stream_config: &resolved_config, - timestamp: Timestamp::new(resolved_config.sample_rate), - stream_proxy: &STREAM_PROXY, - }); - let mut callback = Some(callback); - - audio_unit.set_render_callback(move |mut args: Args>| { - if let Ok(sender) = rx.try_recv() { - sender.send(callback.take().unwrap()).unwrap(); - return Err(()); - } - let timestamp = Timestamp::from_count( - resolved_config.sample_rate, - args.time_stamp.mSampleTime as _, - ); - - let dummy_buf = AudioBuffer::zeroed( - NonZeroUsize::new(1).unwrap(), - NonZeroUsize::new(asbd.mChannelsPerFrame as _).unwrap(), - ); - let dummy_input = AudioInput { - buffer: dummy_buf.as_ref(), - timestamp: Timestamp::new(resolved_config.sample_rate), - channel_flags: &[], - }; - - let output = AudioOutput { - buffer: buffer.as_mut(), - timestamp, - channel_flags: &[], - }; - - if let Some(callback) = &mut callback { - callback.process_audio( - CallbackContext { - stream_config: &resolved_config, - timestamp, - stream_proxy: &STREAM_PROXY, - }, - dummy_input, - output, - ); - for (out_ch, in_ch) in args.data.channels_mut().zip(buffer.iter_channels()) { - out_ch.copy_from_slice(in_ch); - } - } - Ok(()) - })?; - audio_unit.start()?; - Ok(Self { - audio_unit, - callback_retrieve: tx, - }) - } -} diff --git a/crates/interflow-coreaudio/src/platform.rs b/crates/interflow-coreaudio/src/platform.rs new file mode 100644 index 0000000..cbd1a0b --- /dev/null +++ b/crates/interflow-coreaudio/src/platform.rs @@ -0,0 +1,38 @@ +//! CoreAudio platform implementation +use crate::device::Device; +use crate::Error; +use coreaudio::audio_unit::macos_helpers::{get_audio_device_ids, get_default_device_id}; +use interflow_core::traits::{ExtensionProvider, Selector}; +use interflow_core::{platform, DeviceType}; + +/// The CoreAudio driver. +pub struct Platform; + +impl ExtensionProvider for Platform { + fn register<'a, 'sel>(&'a self, selector: &'sel mut Selector<'a>) -> &'sel mut Selector<'a> { + selector + } +} + +impl platform::Platform for Platform { + type Error = Error; + + type Device = Device; + + const NAME: &'static str = "CoreAudio"; + + fn default_device(&self, device_type: DeviceType) -> Result { + if device_type.is_output() || !device_type.is_input() { + return Ok(Device::default_output()); + } + + let Some(id) = get_default_device_id(device_type.is_input()) else { + return Err(Error::NoMatchingDevices(device_type)); + }; + Ok(Device::from_id(id)) + } + + fn list_devices(&self) -> Result, Self::Error> { + Ok(get_audio_device_ids()?.into_iter().map(Device::from_id)) + } +} diff --git a/crates/interflow-coreaudio/src/stream.rs b/crates/interflow-coreaudio/src/stream.rs new file mode 100644 index 0000000..83d1621 --- /dev/null +++ b/crates/interflow-coreaudio/src/stream.rs @@ -0,0 +1,275 @@ +//! CoreAudio stream implementation. +use crate::device::Device; +use crate::Error; +use coreaudio::audio_unit::audio_format::LinearPcmFlags; +use coreaudio::audio_unit::render_callback::{data, Args}; +use coreaudio::audio_unit::{AudioUnit, Element, SampleFormat, Scope, StreamFormat}; +use coreaudio_sys::{kAudioDevicePropertyBufferFrameSize, kAudioUnitProperty_StreamFormat}; +use interflow_core::buffer::AudioBuffer; +use interflow_core::device::{Device as _, ResolvedStreamConfig, StreamConfig}; +use interflow_core::stream; +use interflow_core::stream::{AudioInput, AudioOutput, CallbackContext, StreamProxy}; +use interflow_core::timing::Timestamp; +use interflow_core::traits::{ExtensionProvider, Selector}; +use std::convert::Infallible; +use std::num::NonZeroUsize; + +/// Stream type created by opening up a stream on a [`Device`]. +pub struct Handle { + audio_unit: AudioUnit, + callback_retrieve: oneshot::Sender>, +} + +impl stream::StreamHandle for Handle { + type Error = Infallible; + + fn eject(mut self) -> Result { + let (tx, rx) = oneshot::channel(); + self.callback_retrieve + .send(tx) + .expect("Callback receiver cannot have been dropped yet"); + let callback = rx.recv().expect("Oneshot receiver must be used"); + self.audio_unit.free_input_callback(); + self.audio_unit.free_render_callback(); + Ok(callback) + } +} + +fn input_stream_format(sample_rate: f64, channel_count: usize) -> StreamFormat { + StreamFormat { + sample_rate, + sample_format: SampleFormat::I16, + flags: LinearPcmFlags::IS_SIGNED_INTEGER, + channels: channel_count as _, + } +} + +fn output_stream_format(sample_rate: f64, channel_count: usize) -> StreamFormat { + StreamFormat { + sample_rate, + sample_format: SampleFormat::F32, + flags: LinearPcmFlags::IS_NON_INTERLEAVED | LinearPcmFlags::IS_FLOAT, + channels: channel_count as _, + } +} + +struct DummyStreamProxy; + +impl ExtensionProvider for DummyStreamProxy { + fn register<'a, 'sel>(&'a self, selector: &'sel mut Selector<'a>) -> &'sel mut Selector<'a> { + selector + } +} + +impl StreamProxy for DummyStreamProxy {} + +static STREAM_PROXY: DummyStreamProxy = DummyStreamProxy; + +impl Handle { + pub(crate) fn new( + device: &Device, + stream_config: StreamConfig, + callback: Callback, + ) -> Result { + let requested_type = stream_config.requested_device_type(); + if requested_type.is_duplex() { + return Err(Error::DuplexUnavailable); + } + + let unsupported = device.device_type() & !requested_type; + if !unsupported.is_empty() { + log::warn!( + "Cannot request {unsupported:?} for {device}, ignoring", + device = device.name() + ); + } + let unit = device.request.to_audio_unit()?; + if requested_type.is_input() { + Self::new_input(unit, stream_config, callback) + } else { + Self::new_output(unit, stream_config, callback) + } + } + + fn new_input( + mut audio_unit: AudioUnit, + stream_config: StreamConfig, + mut callback: Callback, + ) -> Result { + let asbd = + input_stream_format(stream_config.sample_rate, stream_config.input_channels).to_asbd(); + audio_unit.set_property( + kAudioUnitProperty_StreamFormat, + Scope::Output, + Element::Input, + Some(&asbd), + )?; + let frame_count: u32 = audio_unit.get_property( + kAudioDevicePropertyBufferFrameSize, + Scope::Input, + Element::Input, + )?; + let resolved_config = ResolvedStreamConfig { + sample_rate: asbd.mSampleRate, + input_channels: asbd.mChannelsPerFrame as _, + output_channels: 0, + max_frame_count: frame_count as _, + }; + let mut buffer = AudioBuffer::zeroed( + NonZeroUsize::new(frame_count as _).unwrap(), + NonZeroUsize::new(asbd.mChannelsPerFrame as _).unwrap(), + ); + + let (tx, rx) = oneshot::channel::>(); + callback.prepare(CallbackContext { + stream_config: &resolved_config, + timestamp: Timestamp::new(asbd.mSampleRate), + stream_proxy: &STREAM_PROXY, + }); + let mut callback = Some(callback); + + audio_unit.set_input_callback(move |args: Args>| { + if let Ok(sender) = rx.try_recv() { + sender.send(callback.take().unwrap()).unwrap(); + return Err(()); + } + let num_frames = args.num_frames; + let channels = asbd.mChannelsPerFrame as usize; + let input_data = args.data.buffer; + + for frame_idx in 0..num_frames { + for ch in 0..channels { + let sample = input_data[frame_idx * channels + ch]; + buffer + .frame_mut(frame_idx) + .set(ch, (sample as f32) / (i16::MAX as f32)); + } + } + + let timestamp = Timestamp::from_count( + resolved_config.sample_rate, + args.time_stamp.mSampleTime as _, + ); + + let input = AudioInput { + buffer: buffer.as_ref(), + timestamp, + channel_flags: &[], + }; + + let mut dummy_buf = AudioBuffer::zeroed( + NonZeroUsize::new(1).unwrap(), + NonZeroUsize::new(channels as _).unwrap(), + ); + let dummy_output = AudioOutput { + buffer: dummy_buf.as_mut(), + timestamp: Timestamp::new(asbd.mSampleRate), + channel_flags: &[], + }; + + if let Some(callback) = &mut callback { + callback.process_audio( + CallbackContext { + stream_config: &resolved_config, + timestamp, + stream_proxy: &STREAM_PROXY, + }, + input, + dummy_output, + ); + } + Ok(()) + })?; + audio_unit.start()?; + Ok(Self { + audio_unit, + callback_retrieve: tx, + }) + } + + fn new_output( + mut audio_unit: AudioUnit, + stream_config: StreamConfig, + mut callback: Callback, + ) -> Result { + let asbd = output_stream_format(stream_config.sample_rate, stream_config.output_channels) + .to_asbd(); + audio_unit.set_property( + kAudioUnitProperty_StreamFormat, + Scope::Input, + Element::Output, + Some(&asbd), + )?; + let frame_size: u32 = audio_unit.get_property( + kAudioDevicePropertyBufferFrameSize, + Scope::Output, + Element::Output, + )?; + let resolved_config = ResolvedStreamConfig { + sample_rate: asbd.mSampleRate, + input_channels: 0, + output_channels: asbd.mChannelsPerFrame as _, + max_frame_count: frame_size as _, + }; + let mut buffer = AudioBuffer::zeroed( + NonZeroUsize::new(frame_size as _).unwrap(), + NonZeroUsize::new(asbd.mChannelsPerFrame as _).unwrap(), + ); + + let (tx, rx) = oneshot::channel::>(); + callback.prepare(CallbackContext { + stream_config: &resolved_config, + timestamp: Timestamp::new(resolved_config.sample_rate), + stream_proxy: &STREAM_PROXY, + }); + let mut callback = Some(callback); + + audio_unit.set_render_callback(move |mut args: Args>| { + if let Ok(sender) = rx.try_recv() { + sender.send(callback.take().unwrap()).unwrap(); + return Err(()); + } + let timestamp = Timestamp::from_count( + resolved_config.sample_rate, + args.time_stamp.mSampleTime as _, + ); + + let dummy_buf = AudioBuffer::zeroed( + NonZeroUsize::new(1).unwrap(), + NonZeroUsize::new(asbd.mChannelsPerFrame as _).unwrap(), + ); + let dummy_input = AudioInput { + buffer: dummy_buf.as_ref(), + timestamp: Timestamp::new(resolved_config.sample_rate), + channel_flags: &[], + }; + + let output = AudioOutput { + buffer: buffer.as_mut(), + timestamp, + channel_flags: &[], + }; + + if let Some(callback) = &mut callback { + callback.process_audio( + CallbackContext { + stream_config: &resolved_config, + timestamp, + stream_proxy: &STREAM_PROXY, + }, + dummy_input, + output, + ); + for (out_ch, in_ch) in args.data.channels_mut().zip(buffer.iter_channels()) { + out_ch.copy_from_slice(in_ch); + } + } + Ok(()) + })?; + audio_unit.start()?; + Ok(Self { + audio_unit, + callback_retrieve: tx, + }) + } +} diff --git a/crates/interflow-coreaudio/src/utils.rs b/crates/interflow-coreaudio/src/utils.rs new file mode 100644 index 0000000..7eaccae --- /dev/null +++ b/crates/interflow-coreaudio/src/utils.rs @@ -0,0 +1,41 @@ +use coreaudio_sys::{AudioDeviceID, AudioObjectGetPropertyData, AudioObjectPropertyAddress}; + +pub(crate) fn get_device_property( + device_id: AudioDeviceID, + address: AudioObjectPropertyAddress, +) -> Result { + let mut data = std::mem::MaybeUninit::::uninit(); + let mut size = size_of::() as u32; + let status = unsafe { + AudioObjectGetPropertyData( + device_id, + &address, + 0, + std::ptr::null(), + &mut size, + data.as_mut_ptr() as *mut _, + ) + }; + coreaudio::Error::from_os_status(status)?; + Ok(unsafe { data.assume_init() }) +} + +#[expect(unused)] +pub(crate) fn set_device_property( + device_id: AudioDeviceID, + address: AudioObjectPropertyAddress, + data: &T, +) -> Result<(), coreaudio::Error> { + let size = size_of::() as u32; + let status = unsafe { + coreaudio_sys::AudioObjectSetPropertyData( + device_id, + &address, + 0, + std::ptr::null(), + size, + data as *const T as *const _, + ) + }; + coreaudio::Error::from_os_status(status) +} diff --git a/examples/enumerate.rs b/examples/enumerate.rs index a84b9ed..d2d9beb 100644 --- a/examples/enumerate.rs +++ b/examples/enumerate.rs @@ -1,5 +1,4 @@ -use interflow::core::{proxies::PlatformProxy, DeviceType}; -use interflow::default_platform; +use interflow::prelude::*; pub fn enumerate_devices(platform: &dyn PlatformProxy) -> anyhow::Result<()> { eprintln!("Driver name : {}", platform.name()); diff --git a/examples/sine_wave.rs b/examples/sine_wave.rs index 0c0f30b..f5da105 100644 --- a/examples/sine_wave.rs +++ b/examples/sine_wave.rs @@ -1,6 +1,5 @@ use anyhow::Result; -use interflow::core::DeviceType; -use interflow_core::stream::StreamHandle; +use interflow::prelude::*; use util::sine::SineWave; mod util; @@ -8,7 +7,7 @@ mod util; fn main() -> Result<()> { env_logger::init(); - let stream = interflow::default_stream( + let stream = default_stream( DeviceType::OUTPUT | DeviceType::PHYSICAL, SineWave::new(440.0), )?; diff --git a/src/audio_buffer.rs b/src/audio_buffer.rs deleted file mode 100644 index f2de8ee..0000000 --- a/src/audio_buffer.rs +++ /dev/null @@ -1,715 +0,0 @@ -//! Audio buffer types and traits for audio data manipulation. -//! -//! This module provides different types of audio buffers optimized for various use cases: -//! -//! - [`AudioBuffer`]: Owned buffer type for standard audio processing -//! - [`AudioRef`]: Immutable reference buffer for reading audio data -//! - [`AudioMut`]: Mutable reference buffer for modifying audio data -//! - [`AudioShared`]: Arc-backed shared buffer for multithreaded access -//! - [`AudioCow`]: Copy-on-write buffer (avoid in audio callbacks) -//! -//! The buffers support both interleaved and non-interleaved data formats and provide -//! convenient methods for: -//! -//! - Accessing individual channels and frames -//! - Slicing and subsetting audio data -//! - Computing audio metrics like RMS -//! - Mixing and amplitude adjustments -//! - Converting between different sample formats -//! -//! The buffers are built on top of ndarray for efficient multidimensional array operations. - -use ndarray::{ - s, Array0, ArrayBase, ArrayView1, ArrayView2, ArrayViewMut1, ArrayViewMut2, AsArray, CowRepr, - Data, DataMut, DataOwned, Ix1, Ix2, OwnedArcRepr, OwnedRepr, RawData, RawDataClone, ViewRepr, -}; -use std::collections::Bound; -use std::fmt; -use std::fmt::Formatter; -use std::ops::{AddAssign, RangeBounds}; - -/// Owned audio buffer type. -pub type AudioBuffer = AudioBufferBase>; -/// Immutably referenced audio buffer type. -pub type AudioRef<'a, T> = AudioBufferBase>; -/// Mutably referenced audio buffer type. -pub type AudioMut<'a, T> = AudioBufferBase>; -/// Arc-backed shared audio buffer type. -pub type AudioShared = AudioBufferBase>; -/// Copy-on-write audio buffer type. Should not be used within audio callbacks, as the copy will -/// introduce allocations. -pub type AudioCow<'a, T> = AudioBufferBase>; - -type Storage = ArrayBase; - -/// Audio buffer type, which backs all audio data interfacing with user code. -/// -/// This type is made to make manipulation of audio data easier, and is agnostic in its storage -/// representation, meaning that it can work with both interleaved and non-interleaved data. -/// -/// Audio is stored as "row-per-channel" -pub struct AudioBufferBase { - storage: Storage, -} - -impl fmt::Debug for AudioBufferBase { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - f.debug_struct("AudioBufferBase") - .field( - "storage", - &format!( - "[{}x{} buffer of {}]", - self.storage.nrows(), - self.storage.ncols(), - std::any::type_name::() - ), - ) - .finish_non_exhaustive() - } -} - -impl Clone for AudioBufferBase { - fn clone(&self) -> Self { - Self { - storage: self.storage.clone(), - } - } -} - -impl Copy for AudioBufferBase {} - -impl Default for AudioBufferBase { - fn default() -> Self { - Self { - storage: ArrayBase::from_shape_fn((0, 0), |(_, _)| unreachable!()), - } - } -} - -impl> PartialEq> for AudioBufferBase -where - S::Elem: PartialEq, -{ - fn eq(&self, other: &AudioBufferBase) -> bool { - self.storage.shape() == other.storage.shape() - && self.storage.iter().eq(other.storage.iter()) - } - - // Explicitely implementing `fn ne` may yield better performance with the shortcircuiting of the or operator - #[allow(clippy::partialeq_ne_impl)] - fn ne(&self, other: &AudioBufferBase) -> bool { - self.storage.shape() != other.storage.shape() - || self.storage.iter().ne(other.storage.iter()) - } -} - -impl Eq for AudioBufferBase -where - Self: PartialEq, - S::Elem: Eq, -{ -} - -impl AudioBufferBase { - /// Number of samples present in this buffer. - pub fn num_frames(&self) -> usize { - self.storage.ncols() - } - - /// Number of channels present in this buffer. - pub fn num_channels(&self) -> usize { - self.storage.nrows() - } -} - -impl AudioBufferBase { - /// Return an immutable audio buffer view, sharing the data with this buffer. - pub fn as_ref(&self) -> AudioRef<'_, S::Elem> { - AudioRef { - storage: self.storage.view(), - } - } - - /// Slice the contents of this audio buffer, returning an immutable view of this buffer - /// containing only the audio samples at indices within the provided range. - pub fn slice(&self, range: impl RangeBounds) -> AudioRef<'_, S::Elem> { - let start = match range.start_bound() { - Bound::Included(i) => *i, - Bound::Excluded(i) => *i + 1, - Bound::Unbounded => 0, - }; - let end = match range.end_bound() { - Bound::Included(i) => *i - 1, - Bound::Excluded(i) => *i, - Bound::Unbounded => self.num_frames(), - }; - let storage = self.storage.slice(s![.., start..end]); - AudioRef { storage } - } - - /// Iterate over non-overlapping chunks of this audio buffer. - pub fn chunks(&self, size: usize) -> impl Iterator> { - let mut i = 0; - std::iter::from_fn(move || { - if i >= self.num_frames() { - return None; - } - let range = i..(i + size).min(self.num_frames()); - i += size; - Some(self.slice(range)) - }) - } - - /// Iterate over non-overlapping chunks of this audio buffer. If the last chunk has a smaller length than the - /// requested size, it will not be yielded. - pub fn chunks_exact(&self, size: usize) -> impl Iterator> { - let mut i = 0; - std::iter::from_fn(move || { - if i + size >= self.num_frames() { - return None; - } - let range = i..i + size; - i += size; - Some(self.slice(range)) - }) - } - - /// Iterate over overlapping windows of this audio buffer. - /// - /// # Arguments - /// - /// - `size`: Size of the window - pub fn windows(&self, size: usize) -> impl Iterator> { - let mut i = 0; - std::iter::from_fn(move || { - if i + size >= self.num_frames() { - return None; - } - let range = i..(i + size).min(self.num_frames()); - i += 1; - Some(self.slice(range)) - }) - } - - /// Return an immutable view of a single channel. Panics when the requested channel does not - /// exist. - pub fn get_channel(&self, channel: usize) -> ArrayView1<'_, S::Elem> { - self.storage.row(channel) - } - - /// Return an iterator of immutable views of the channels present in this audio buffer. - pub fn channels(&self) -> impl '_ + Iterator> { - self.storage.rows().into_iter() - } - - /// Get a single frame, that is all channels at the specified sample index. Panics when the - /// sample is out of range. - pub fn get_frame(&self, sample: usize) -> ArrayView1<'_, S::Elem> { - self.storage.column(sample) - } - - /// Return an immutable interleaved 2-D array view, where samples are in rows and channels are - /// in columns. - pub fn as_interleaved(&self) -> ArrayView2<'_, S::Elem> { - self.storage.t() - } - - /// Copies this audio buffer to another, giving you a unique owned buffer in the end. - /// - /// Not realtime-safe. - pub fn to_owned(&self) -> AudioBuffer - where - S::Elem: Clone, - { - AudioBuffer { - storage: self.storage.to_owned(), - } - } - - /// Copies audio data in this buffer to the provided interleaved buffer. The `output` buffer - /// must represent an interleaved buffer with the same number of channels and same number of - /// samples. - #[must_use] - pub fn copy_into_interleaved(&self, output: &mut [S::Elem]) -> bool - where - S::Elem: Copy, - { - if output.len() != self.storage.len() { - return false; - } - - for (inp, out) in self.as_interleaved().iter().zip(output.iter_mut()) { - *out = *inp; - } - true - } -} - -impl AudioBufferBase { - /// Return a mutable audio buffer view. - pub fn as_mut(&mut self) -> AudioMut<'_, S::Elem> { - AudioMut { - storage: self.storage.view_mut(), - } - } - - /// Slice the contents of this audio buffer, returning a mutable view of this buffer - /// containing only the audio samples at indices within the provided range. - pub fn slice_mut(&mut self, range: impl RangeBounds) -> AudioMut<'_, S::Elem> { - let start = match range.start_bound() { - Bound::Included(i) => *i, - Bound::Excluded(i) => *i + 1, - Bound::Unbounded => 0, - }; - let end = match range.end_bound() { - Bound::Included(i) => *i - 1, - Bound::Excluded(i) => *i, - Bound::Unbounded => self.num_frames(), - }; - let storage = self.storage.slice_mut(s![.., start..end]); - AudioMut { storage } - } - - /// Return a mutable view of a single channel. Panics when the requested channel does not - /// exist. - pub fn get_channel_mut(&mut self, channel: usize) -> ArrayViewMut1<'_, S::Elem> { - self.storage.row_mut(channel) - } - - /// Return an iterator of mutable views of the channels present in this audio buffer. - pub fn channels_mut(&mut self) -> impl '_ + Iterator> { - self.storage.rows_mut().into_iter() - } - /// Return a mutable interleaved 2-D array view, where samples are in rows and channels are in - /// columns. - pub fn as_interleaved_mut(&mut self) -> ArrayViewMut2<'_, S::Elem> { - self.storage.view_mut().reversed_axes() - } - - /// Copies audio data into this buffer from the provided interleaved audio buffer. The `output` buffer - /// must represent an interleaved buffer with the same number of channels and same number of - /// samples. - #[must_use] - pub fn copy_from_interleaved(&mut self, input: &[S::Elem]) -> bool - where - S::Elem: Copy, - { - if input.len() != self.storage.len() { - return false; - } - - for (out, inp) in self.as_interleaved_mut().iter_mut().zip(input.iter()) { - *out = *inp; - } - true - } -} - -impl AudioBufferBase { - /// Create a new audio buffer with the provided number of channels and sample size, filling - /// it with the provided fill function. - /// - /// Not realtime-safe. - pub fn fill_with( - channels: usize, - sample_size: usize, - fill: impl Fn(usize, usize) -> S::Elem, - ) -> Self { - let storage = Storage::from_shape_fn((channels, sample_size), |(ch, i)| fill(ch, i)); - Self { storage } - } - - /// Create a new audio buffer with the provided number of channels and sample size, filling - /// it with the provided value. - pub fn fill(channels: usize, sample_size: usize, value: S::Elem) -> Self - where - S::Elem: Copy, - { - Self::fill_with(channels, sample_size, |_, _| value) - } - - /// Create a new audio buffer with the provided number of channels and sample size, filling - /// it with the [`Default`] value of that type. - pub fn defaulted(channels: usize, sample_size: usize) -> Self - where - S::Elem: Default, - { - Self::fill_with(channels, sample_size, |_, _| S::Elem::default()) - } -} - -impl AudioRef<'static, T> { - pub fn empty() -> Self { - Self { - storage: ArrayView2::from_shape((0, 0), &[]).unwrap(), - } - } -} - -impl AudioMut<'static, T> { - pub fn empty() -> Self { - Self { - storage: ArrayViewMut2::from_shape((0, 0), &mut []).unwrap(), - } - } -} - -impl<'a, T: 'a> AudioRef<'a, T> -where - ViewRepr<&'a T>: Sized, -{ - /// Create an audio buffer reference from interleaved data. This does *not* copy the data, - /// but creates a view over it, so that it can be accessed as any other audio buffer. - pub fn from_interleaved(data: &'a [T], channels: usize) -> Option { - let buffer_size = data.len() / channels; - let raw = ArrayView2::from_shape((buffer_size, channels), data).ok()?; - let storage = raw.reversed_axes(); - Some(Self { storage }) - } - - /// Create an audio buffer reference from non-interleaved data. This does *not* copy the data, - /// but creates a view over it, so that it can be accessed as any other audio buffer. - pub fn from_noninterleaved(data: &'a [T], channels: usize) -> Option { - let buffer_size = data.len() / channels; - let storage = ArrayView2::from_shape((channels, buffer_size), data).ok()?; - Some(Self { storage }) - } -} - -impl<'a, T: 'a> AudioMut<'a, T> { - /// Create an audio buffer mutable reference from interleaved data. This does *not* copy the - /// data, but creates a view over it, so that it can be accessed as any other audio buffer. - /// - /// Writes to the resulting buffer directly map to the provided slice, and asking an - /// interleaved view out of the resulting buffer (with [`AudioBufferBase::as_interleaved`]) - /// means the same slice is returned. This makes for efficient copying between different - /// interleaved buffers, even though a non-interleaved interface. - pub fn from_interleaved_mut(data: &'a mut [T], channels: usize) -> Option { - let buffer_size = data.len() / channels; - let raw = ArrayViewMut2::from_shape((buffer_size, channels), data).ok()?; - let storage = raw.reversed_axes(); - Some(Self { storage }) - } - - /// Create an audio buffer mutable reference from interleaved data. This does *not* copy the - /// data, but creates a view over it, so that it can be accessed as any other audio buffer. - /// - /// Writes to the resulting buffer directly map to the provided slice, and asking an - /// interleaved view out of the resulting buffer (with [`AudioBufferBase::as_interleaved`]) - /// means the same slice is returned. - pub fn from_noninterleaved_mut(data: &'a mut [T], channels: usize) -> Option { - let buffer_size = data.len() / channels; - let storage = ArrayViewMut2::from_shape((channels, buffer_size), data).ok()?; - Some(Self { storage }) - } -} - -impl AudioBufferBase -where - S::Elem: Clone, -{ - /// Returns a mutable view over each channel of the frame at the given index. - /// - /// # Arguments - /// - /// * `sample`: Sample index for the frame to return. - /// - /// # Panics - /// - /// Panics if the sample index is out of range. - /// - /// returns: ArrayBase::Elem>, Dim<[usize; 1]>> - pub fn get_frame_mut(&mut self, sample: usize) -> ArrayViewMut1<'_, S::Elem> { - self.storage.column_mut(sample) - } - - /// Sets audio data of a single frame, that is all channels at the specified sample index. - /// Panics when the sample is out of range. - pub fn set_frame<'a>(&mut self, sample: usize, data: impl AsArray<'a, S::Elem, Ix1>) - where - S::Elem: 'a, - { - let column = self.storage.column_mut(sample); - data.into().assign_to(column); - } - - /// Sets audio data of a single sample, copying the provided value to each channel at that - /// sample index. Panics when the sample index is out of range. - pub fn set_mono(&mut self, i: usize, value: S::Elem) { - Array0::from_elem([], value) - .broadcast((self.num_channels(),)) - .unwrap() - .assign_to(self.storage.column_mut(i)) - } -} - -/// Trait for sample types. Typical sample types can be `i32`, `f32`, etc. but more can be -/// implemented downstream. -pub trait Sample: Copy { - /// Floating-point type which can fit all or a big majority of this type's values. - /// This type is the type used in float conversions, as well as the type of the amplitude in - /// buffer amplitude operations. - type Float: Copy; - /// Zero value for this sample. This is *not specifically* the numerical zero of the type, - /// but the value for which the amplitude of the stream is zero. Unsigned types are an - /// example for which the two are different. - const ZERO: Self; - - /// Construct a sample of this type from the corresponding float signal value. - fn from_float(f: Self::Float) -> Self; - - /// Compute the RMS value out of an iterator of this type. - fn rms(it: impl Iterator) -> Self::Float; - - /// Convert this value into its floating point equivalent. - fn into_float(self) -> Self::Float; - - /// Change the "amplitude" of this value, ie. absolute values less than one will bring the - /// value closer to [`Self::ZERO`], whereas absolute values above one will move the value - /// further away. - fn change_amplitude(&mut self, amp: Self::Float); -} - -#[duplicate::duplicate_item( - ty fty; - [i8] [f32]; - [i16] [f32]; - [i32] [f32]; - [i64] [f64]; -)] -impl Sample for ty { - type Float = fty; - const ZERO: Self = 0; - - fn from_float(f: Self::Float) -> Self { - (f * ty::MAX as fty) as ty - } - fn rms(it: impl Iterator) -> Self::Float { - let mut i = 0.0; - it.map(|t| t.into_float().powi(2)) - .reduce(|a, b| { - let res = a * i / (i + 1.0) + b / (i + 1.0); - i += 1.0; - res - }) - .unwrap_or(0.0) - .sqrt() - } - - fn into_float(self) -> Self::Float { - self as fty / ty::MAX as fty - } - fn change_amplitude(&mut self, amp: Self::Float) { - *self = ((*self as fty) * amp) as Self; - } -} - -#[duplicate::duplicate_item( - ty fty; - [u8] [f32]; - [u16] [f32]; - [u32] [f32]; - [u64] [f64]; -)] -impl Sample for ty { - type Float = fty; - const ZERO: Self = 1 + Self::MAX / 2; - - fn from_float(f: Self::Float) -> Self { - ((f * 0.5 + 0.5) * (Self::MAX as Self::Float + 1.0)) as Self - } - - fn rms(it: impl Iterator) -> Self::Float { - let mut i = 0.0; - it.map(|t| t.into_float().powi(2)) - .reduce(|a, b| { - let res = a * i / (i + 1.0) + b / (i + 1.0); - i += 1.0; - res - }) - .unwrap_or(0.0) - .sqrt() - } - - fn into_float(self) -> Self::Float { - let t = self as Self::Float / (Self::MAX as Self::Float + 1.0); - t * 2.0 - 1.0 - } - - fn change_amplitude(&mut self, amp: Self::Float) { - let f = Self::into_float(*self) * amp; - *self = Self::from_float(f) - } -} - -#[duplicate::duplicate_item( - ty; - [f32]; - [f64]; -)] -impl Sample for ty { - type Float = Self; - const ZERO: Self = 0.0; - - fn from_float(f: Self::Float) -> Self { - f - } - - fn rms(it: impl Iterator) -> Self::Float { - let mut i = 0.0; - it.map(|t| t.powi(2)) - .reduce(|a, b| { - let res = a * i / (i + 1.0) + b / (i + 1.0); - i += 1.0; - res - }) - .unwrap_or(0.0) - .sqrt() - } - - fn into_float(self) -> Self::Float { - self - } - - fn change_amplitude(&mut self, amp: Self::Float) { - *self *= amp; - } -} - -impl AudioBuffer { - /// Construct a zeroed buffer with the provided channels and sample size. - /// - /// Not realtime-safe. - pub fn zeroed(channels: usize, sample_size: usize) -> Self { - Self::fill(channels, sample_size, T::ZERO) - } -} - -impl AudioBufferBase -where - S::Elem: Sample, -{ - /// Compute the RMS (Root Mean Square) value of this entire buffer, all channels considered - /// equally. The result is given in terms of linear amplitude values, as a float determined by - /// [`S::Float`]. - /// - /// You can convert the result to decibels with the formula `20. * rms.log10()`. - pub fn rms(&self) -> ::Float { - S::Elem::rms(self.storage.iter().copied()) - } - - /// Compute the RMS (Root Mean Square) value of this entire buffer for a single channel. The - /// result is given in terms of linear amplitude values, as a float determined by [`S::Float`]. - /// - /// You can convert the result to decibels with the formula `20. * rms.log10()`. - pub fn channel_rms(&self, channel: usize) -> ::Float { - S::Elem::rms(self.storage.column(channel).iter().copied()) - } -} - -impl> AudioBufferBase { - /// Change the amplitude of this buffer by the provided amplitude. - /// - /// See [`Sample::change_amplitude`] for more details. - pub fn change_amplitude(&mut self, amplitude: ::Float) { - for s in self.storage.iter_mut() { - s.change_amplitude(amplitude); - } - } - - /// Mix a buffer into this buffer at the specified amplitude. The audio will be mixed into - /// this buffer as a result, and the other buffer's amplitude will be changed similarly to - /// applying [`Self::change_amplitude`] first. - pub fn mix(&mut self, other: AudioRef, other_amplitude: ::Float) - where - S::Elem: AddAssign, - { - for (mut ch_a, ch_b) in self.channels_mut().zip(other.channels()) { - for (a, b) in ch_a.iter_mut().zip(ch_b) { - let mut b = *b; - b.change_amplitude(other_amplitude); - *a += b; - } - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use ndarray::ArrayView1; - - fn create_test_buffer() -> AudioBuffer { - AudioBuffer::fill_with(2, 4, |ch, i| (ch * 4 + i) as f32) - } - - #[test] - fn test_buffer_creation() { - let buf = create_test_buffer(); - assert_eq!(buf.num_channels(), 2); - assert_eq!(buf.num_frames(), 4); - - // Verify sample values - assert_eq!(buf.get_channel(0).to_vec(), vec![0.0, 1.0, 2.0, 3.0]); - assert_eq!(buf.get_channel(1).to_vec(), vec![4.0, 5.0, 6.0, 7.0]); - } - - #[test] - fn test_buffer_views() { - let mut buf = create_test_buffer(); - - // Test immutable slice - let slice = buf.slice(1..3); - assert_eq!(slice.num_frames(), 2); - assert_eq!(slice.get_channel(0).to_vec(), vec![1.0, 2.0]); - - // Test mutable slice - let mut slice = buf.slice_mut(1..3); - slice.get_channel_mut(0).fill(10.0); - assert_eq!(buf.get_channel(0).to_vec(), vec![0.0, 10.0, 10.0, 3.0]); - } - - #[test] - fn test_sample_conversions() { - // Test i16 <-> f32 conversion - assert_eq!(i16::from_float(0.5), 16383); - assert!((i16::MAX.into_float() - 1.0).abs() < f32::EPSILON); - - // Test u8 <-> f32 conversion - assert_eq!(u8::from_float(0.0), 128); - assert!((u8::ZERO.into_float()).abs() < f32::EPSILON); - } - - #[test] - fn test_rms() { - let mut buf = AudioBuffer::::zeroed(1, 4); - buf.get_channel_mut(0) - .assign(&ArrayView1::from(&[1.0, -1.0, 1.0, -1.0])); - let rms = buf.rms(); - assert!((rms - 1.0).abs() < f32::EPSILON, "RMS is incorrect: {rms}"); - } - - #[test] - fn test_mixing() { - let mut buf1 = AudioBuffer::::fill(1, 4, 1.0); - let buf2 = AudioBuffer::::fill(1, 4, 0.5); - - buf1.mix(buf2.as_ref(), 2.0); - assert_eq!(buf1.get_channel(0).to_vec(), vec![2.0, 2.0, 2.0, 2.0]); - } - - #[test] - fn test_interleaved() { - let data = vec![1.0f32, 2.0, 3.0, 4.0]; - let buf = AudioRef::from_interleaved(&data, 2).unwrap(); - - assert_eq!(buf.num_channels(), 2); - assert_eq!(buf.num_frames(), 2); - assert_eq!(buf.get_channel(0).to_vec(), vec![1.0, 3.0]); - assert_eq!(buf.get_channel(1).to_vec(), vec![2.0, 4.0]); - - let mut out = vec![0.0f32; 4]; - assert!(buf.copy_into_interleaved(&mut out)); - assert_eq!(out, data); - } -} diff --git a/src/channel_map.rs b/src/channel_map.rs deleted file mode 100644 index 46dfbd2..0000000 --- a/src/channel_map.rs +++ /dev/null @@ -1,223 +0,0 @@ -//! This module provides functionality for working with bitsets and channel mapping. -//! -//! A bitset is a data structure that efficiently stores a set of boolean values using bits. -//! Each bit represents a boolean state (true/false) for a specific index or channel. -//! -//! The module includes: -//! - Generic `Bitset` trait for types that can represent sets of boolean values -//! - `CreateBitset` trait for constructing bitsets from indices -//! - Implementations for standard unsigned integer types (u8, u16, u32, u64, u128) -//! - Slice-based implementation for working with arrays of bitsets -//! - Type aliases for common channel map sizes (32, 64, and 128 bits) -//! -//! # Example -//! -//! ``` -//! use interflow::channel_map::Bitset; -//! -//! let mut map = 0u32; -//! map.set_index(0, true); -//! map.set_index(5, true); -//! assert!(map.get_index(0)); -//! assert!(map.get_index(5)); -//! assert!(!map.get_index(1)); -//! ``` - -use core::panic; - -/// Trait for types which can represent bitsets. -/// -/// A bit set is a type which encodes a boolean value, functioning similarly in principle to a -/// `HashSet`. -pub trait Bitset: Sized { - /// Return the capacity of this bitset, that is, how many indices can be used with this type. - fn capacity(&self) -> usize; - - /// Get the value for a specific index. Implementations should panic when this value is out - /// of range. - fn get_index(&self, index: usize) -> bool; - - /// Sets the value for a specific index. Implementations should panic when this value is out - /// of range. - fn set_index(&mut self, index: usize, value: bool); - - /// Returns an iterator of indices for which the value has been set `true`. - fn indices(&self) -> impl IntoIterator { - (0..self.capacity()).filter(|i| self.get_index(*i)) - } - /// Count the number of `true` elements in this bit set. - fn count(&self) -> usize { - self.indices().into_iter().count() - } - - /// Builder-like method for setting a value at a specific index. - fn with_index(&mut self, index: usize, value: bool) -> &mut Self { - self.set_index(index, value); - self - } - /// Builder-like method for setting all provided indices to `. - fn with_indices(mut self, indices: impl IntoIterator) -> Self { - for ix in indices { - self.set_index(ix, true); - } - self - } -} - -/// Trait for bitsets that can be created from indices -pub trait CreateBitset: Bitset { - /// Create a [`Self`] from the given indices - /// - /// # Arguments - /// - /// - `indices`: [`IntoIterator`] implementation that returns [`usize`] values corresponding to the indices to - /// set in the bitset. - fn from_indices(indices: impl IntoIterator) -> Self; -} - -#[duplicate::duplicate_item( - ty; - [u8]; - [u16]; - [u32]; - [u64]; - [u128]; -)] -impl Bitset for ty { - fn capacity(&self) -> usize { - ty::BITS as usize - } - - fn get_index(&self, index: usize) -> bool { - let mask = 1 << index; - self & mask > 0 - } - - fn set_index(&mut self, index: usize, value: bool) { - let mask = 1 << index; - if value { - *self |= mask; - } else { - *self &= !mask; - } - } - - fn count(&self) -> usize { - self.count_ones() as _ - } -} - -#[duplicate::duplicate_item( - ty; - [u8]; - [u16]; - [u32]; - [u64]; - [u128]; -)] -impl CreateBitset for ty { - fn from_indices(indices: impl IntoIterator) -> Self { - indices - .into_iter() - .inspect(|x| assert!(*x < Self::BITS as usize, "Index out of range")) - .fold(0, |acc, ix| acc | (1 << ix)) - } -} - -fn get_inner_bitset_at(arr: &[T], mut index: usize) -> Option<(usize, usize)> { - arr.iter().enumerate().find_map({ - move |(i, b)| match index.checked_sub(b.capacity()) { - None => Some((i, index)), - Some(v) => { - index = v; - None - } - } - }) -} - -impl Bitset for &mut [T] { - fn capacity(&self) -> usize { - self.iter().map(|b| b.capacity()).sum() - } - - fn get_index(&self, index: usize) -> bool { - let Some((bitset_index, inner_index)) = get_inner_bitset_at(self, index) else { - return false; - }; - self[bitset_index].get_index(inner_index) - } - - fn set_index(&mut self, index: usize, value: bool) { - let Some((bitset_index, inner_index)) = get_inner_bitset_at(self, index) else { - panic!("Index {index} outside of range {}", self.capacity()); - }; - self[bitset_index].set_index(inner_index, value); - } -} - -/// Type alias for a bitset with a capacity of 32 slots. -pub type ChannelMap32 = u32; -/// Type alias for a bitset with a capacity of 64 slots. -pub type ChannelMap64 = u64; -/// Type alias for a bitset with a capacity of 128 slots. -pub type ChannelMap128 = u128; - -#[cfg(test)] -mod test { - use std::collections::HashSet; - use std::hash::RandomState; - - use super::*; - - #[test] - fn test_getset_index() { - let mut bitset = 0u8; - bitset.set_index(0, true); - bitset.set_index(2, true); - bitset.set_index(3, true); - bitset.set_index(2, false); - - assert_eq!(0b1001, bitset); - assert!(bitset.get_index(0)); - assert!(bitset.get_index(3)); - assert!(!bitset.get_index(2)); - } - - #[test] - fn test_from_indices() { - let bitset = u8::from_indices([0, 2, 3]); - assert_eq!(0b1101, bitset); - } - - #[test] - fn test_indices() { - let bitset = 0b10010100u8; - let result = HashSet::<_, RandomState>::from_iter(bitset.indices()); - assert_eq!(HashSet::from_iter([2, 4, 7]), result); - } - - #[test] - fn test_slice_getset() { - let mut storage = [0; 3]; - let mut bitset: &mut [u32] = &mut storage; - - bitset.set_index(0, true); - bitset.set_index(34, true); - bitset.set_index(81, true); - - assert_eq!([0b1, 0b100, 1 << (81 - 64)], bitset); - - assert!(bitset.get_index(0)); - assert!(bitset.get_index(34)); - assert!(bitset.get_index(81)); - } - - #[test] - fn test_slice_indices() { - let mut storage = [0b100101u8, (1 << 6) | (1 << 4), 1]; - let bitrate: &mut [u8] = &mut storage; - let result = HashSet::<_, RandomState>::from_iter(bitrate.indices()); - assert_eq!(HashSet::from_iter([0, 2, 5, 12, 14, 16]), result); - } -} diff --git a/src/lib.rs b/src/lib.rs index dad39c9..fa7f88d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,12 +9,19 @@ use interflow_core::proxies::CreateStreamExt; pub mod backends; +/// Prelude module. Import all with `interflow::prelude::*`. +pub mod prelude { + pub use super::{default_device, default_platform, default_stream}; + pub use interflow_core::prelude::*; + pub use interflow_coreaudio::prelude::*; +} + /// Return the default platform. /// The platform is selected automatically based on your available and enabled backends. #[allow(unreachable_code)] pub fn default_platform() -> core::proxies::DynPlatform { #[cfg(any(target_os = "macos", target_os = "ios"))] - return Rc::new(backends::coreaudio::Platform); + return Rc::new(interflow_coreaudio::platform::Platform); todo!("null backend") } diff --git a/src/prelude.rs b/src/prelude.rs deleted file mode 100644 index ddca334..0000000 --- a/src/prelude.rs +++ /dev/null @@ -1,8 +0,0 @@ -#![allow(unused)] -//! Prelude module for `interflow`. Use as a star-import. - -#[cfg(os_wasapi)] -pub use crate::backends::wasapi::prelude::*; -pub use crate::backends::*; -pub use crate::duplex::{create_duplex_stream, DuplexStreamConfig, DuplexStreamHandle}; -pub use crate::*; diff --git a/src/sample.rs b/src/sample.rs deleted file mode 100644 index 1f18018..0000000 --- a/src/sample.rs +++ /dev/null @@ -1,72 +0,0 @@ -use duplicate::duplicate_item; - -pub trait ConvertSample: Sized + Copy { - const ZERO: Self; - - fn convert_to_f32(self) -> f32; - fn convert_from_f32(v: f32) -> Self; - - fn convert_to_slice(output: &mut [f32], input: &[Self]) { - assert!(output.len() >= input.len()); - for (out, sample) in output.iter_mut().zip(input) { - *out = sample.convert_to_f32(); - } - } - - fn convert_from_slice(output: &mut [Self], input: &[f32]) { - assert!(output.len() >= input.len()); - for (out, &sample) in output.iter_mut().zip(input) { - *out = Self::convert_from_f32(sample); - } - } -} - -impl ConvertSample for f32 { - const ZERO: Self = 0.0; - - #[inline] - fn convert_to_f32(self) -> f32 { - self - } - - #[inline] - fn convert_from_f32(v: f32) -> Self { - v - } -} - -#[duplicate_item( -int; -[i8]; -[i16]; -[i32]; -)] -impl ConvertSample for int { - const ZERO: Self = 0; - - fn convert_to_f32(self) -> f32 { - self as f32 / Self::MAX as f32 - } - - fn convert_from_f32(f: f32) -> Self { - (f * Self::MAX as f32) as Self - } -} - -#[duplicate_item( -uint zero; -[u8] [128]; -[u16] [32768]; -[u32] [2147483648]; -)] -impl ConvertSample for uint { - const ZERO: Self = zero; - - fn convert_to_f32(self) -> f32 { - 2.0 * self as f32 / Self::MAX as f32 - 1.0 - } - - fn convert_from_f32(f: f32) -> Self { - ((f + 1.0) * Self::MAX as f32 / 2.0) as Self - } -} diff --git a/src/timestamp.rs b/src/timestamp.rs deleted file mode 100644 index 7b8c62e..0000000 --- a/src/timestamp.rs +++ /dev/null @@ -1,201 +0,0 @@ -//! Module for handling timestamp and duration calculations in audio processing. -//! -//! This module provides the [`Timestamp`] type, which manages time-related operations -//! for audio streams by tracking sample counts and their corresponding durations based -//! on a specified sample rate. It supports basic arithmetic operations for sample -//! counting and duration calculations, making it useful for audio stream synchronization -//! and timing operations. -//! -//! # Examples -//! -//! ```rust -//! use std::time::Duration; -//! use interflow::timestamp::Timestamp; -//! -//! // Create a timestamp for 48 kHz audio -//! let mut ts = Timestamp::new(48000.); -//! -//! // Add 48 samples (1ms at 48kHz) -//! ts += 48; -//! assert_eq!(ts.as_duration(), Duration::from_millis(1)); -//! -//! // Convert a duration to samples -//! let ts2 = Timestamp::from_duration(48000., Duration::from_millis(100)); -//! assert_eq!(ts2.counter, 4800); -//! ``` - -use std::ops; -use std::ops::AddAssign; -use std::sync::atomic::AtomicU64; -use std::time::Duration; - -/// Timestamp value, which computes duration information from a provided samplerate and a running -/// sample counter. -/// -/// You can update the timestamp by add-assigning sample counts to it: -/// -/// ```rust -/// use std::time::Duration; -/// use interflow::timestamp::Timestamp; -/// let mut ts = Timestamp::new(48000.); -/// assert_eq!(ts.as_duration(), Duration::from_nanos(0)); -/// ts += 48; -/// assert_eq!(ts.as_duration(), Duration::from_millis(1)); -/// ``` -/// -/// Adding also works, returning a new timestamp: -/// -/// ```rust -/// use std::time::Duration; -/// use interflow::timestamp::Timestamp; -/// let mut ts = Timestamp::new(48000.); -/// assert_eq!(ts.as_duration(), Duration::from_nanos(0)); -/// let ts2 = ts + 48; -/// assert_eq!(ts.as_duration(), Duration::from_millis(0)); -/// assert_eq!(ts2.as_duration(), Duration::from_millis(1)); -/// ``` -/// -/// Similarly, you can compute sample offsets by adding a [`Duration`] to it: -/// -/// ```rust -/// use std::time::Duration; -/// use interflow::timestamp::Timestamp; -/// let ts = Timestamp::from_count(48000., 48); -/// let ts_off = ts + Duration::from_millis(100); -/// assert_eq!(ts_off.as_duration(), Duration::from_millis(101)); -/// assert_eq!(ts_off.counter, 4848); -/// ``` -/// -/// Or simply construct a [`Timestamp`] from a specified duration: -/// -/// ```rust -/// use std::time::Duration; -/// use interflow::timestamp::Timestamp; -/// let ts = Timestamp::from_duration(44100., Duration::from_millis(1)); -/// assert_eq!(ts.counter, 44); // Note that the conversion is lossy, as only whole samples are -/// // stored in the timestamp. -/// ``` -#[derive(Debug, Copy, Clone, PartialEq)] -pub struct Timestamp { - /// Number of samples counted in this timestamp. - pub counter: u64, - /// Samplerate of the audio stream associated with the counter. - pub samplerate: f64, -} - -impl AddAssign for Timestamp { - fn add_assign(&mut self, rhs: Duration) { - let samples = rhs.as_secs_f64() * self.samplerate; - self.counter += samples as u64; - } -} - -impl AddAssign for Timestamp { - fn add_assign(&mut self, rhs: u64) { - self.counter += rhs; - } -} - -impl ops::Add for Timestamp -where - Self: AddAssign, -{ - type Output = Self; - - fn add(mut self, rhs: T) -> Self { - self.add_assign(rhs); - self - } -} - -impl Timestamp { - /// Create a zeroed timestamp with the provided sample rate. - pub fn new(samplerate: f64) -> Self { - Self { - counter: 0, - samplerate, - } - } - - /// Create a timestamp from the given sample rate and existing sample count. - pub fn from_count(samplerate: f64, counter: u64) -> Self { - Self { - samplerate, - counter, - } - } - - /// Compute the sample offset that most closely matches the provided duration for the given - /// sample rate. - pub fn from_duration(samplerate: f64, duration: Duration) -> Self { - Self::from_seconds(samplerate, duration.as_secs_f64()) - } - - /// Compute the sample offset that most closely matches the provided duration in seconds for - /// the given sample rate. - pub fn from_seconds(samplerate: f64, seconds: f64) -> Self { - let samples = samplerate * seconds; - Self { - samplerate, - counter: samples as _, - } - } - - /// Compute the number of seconds represented in this [`Timestamp`]. - pub fn as_seconds(&self) -> f64 { - self.counter as f64 / self.samplerate - } - - /// Compute the duration represented by this [`Timestamp`]. - pub fn as_duration(&self) -> Duration { - Duration::from_secs_f64(self.as_seconds()) - } -} - -/// Atomic version of [`Timestamp`] to be shared between threads. Mainly used by the [`crate::duplex`] module, but -/// may be useful in user code as well. -pub struct AtomicTimestamp { - samplerate: AtomicU64, - counter: AtomicU64, -} - -impl AtomicTimestamp { - /// Update the contents with the provided [`Timestamp`]. - pub fn update(&self, ts: Timestamp) { - self.samplerate.store( - ts.samplerate.to_bits(), - std::sync::atomic::Ordering::Relaxed, - ); - self.counter - .store(ts.counter, std::sync::atomic::Ordering::Relaxed); - } - - /// Load values and return them as a [`Timestamp`]. - pub fn as_timestamp(&self) -> Timestamp { - Timestamp { - samplerate: f64::from_bits(self.samplerate.load(std::sync::atomic::Ordering::Relaxed)), - counter: self.counter.load(std::sync::atomic::Ordering::Relaxed), - } - } - - /// Add the provided number of frames to this. - pub fn add_frames(&self, frames: u64) { - self.counter - .fetch_add(frames, std::sync::atomic::Ordering::Relaxed); - } -} - -impl From for AtomicTimestamp { - fn from(value: Timestamp) -> Self { - Self { - samplerate: AtomicU64::new(value.samplerate.to_bits()), - counter: AtomicU64::new(value.counter), - } - } -} - -impl From for Timestamp { - fn from(value: AtomicTimestamp) -> Self { - value.as_timestamp() - } -} From 245c1d8f8492bdaf89d19b2e93e8fa41e55c3a4d Mon Sep 17 00:00:00 2001 From: Nathan Graule Date: Thu, 23 Jul 2026 15:45:48 +0200 Subject: [PATCH 25/38] feat(core): register mutable references in ExtensionProvider, and automatically register Self as an available type to look up --- crates/interflow-core/src/traits.rs | 83 +++++++++++++++++++++++++---- 1 file changed, 74 insertions(+), 9 deletions(-) diff --git a/crates/interflow-core/src/traits.rs b/crates/interflow-core/src/traits.rs index 3320095..ab5fd85 100644 --- a/crates/interflow-core/src/traits.rs +++ b/crates/interflow-core/src/traits.rs @@ -2,7 +2,7 @@ use std::any::TypeId; use std::marker::PhantomData; use std::mem::MaybeUninit; -/// A fully type-erased pointer, that can work with both thin and fat pointers. +/// A fully type-erased pointer that can work with both thin and fat pointers. /// Copied from . #[derive(Copy, Clone)] struct ErasedPtr { @@ -30,16 +30,49 @@ impl ErasedPtr { res } + /// Erase `ptr`. + fn new_mut(ptr: *mut T) -> Self { + let mut res = ErasedPtr { + value: MaybeUninit::zeroed(), + }; + + let len = size_of::<*const T>(); + assert!(len <= size_of::<[usize; 2]>()); + + let ptr_val = (&ptr) as *const *mut T as *mut u8; + let target = res.value.as_mut_ptr() as *mut u8; + // SAFETY: The target is valid for at least `len` bytes, and has no + // requirements on the value. + unsafe { + core::ptr::copy_nonoverlapping(ptr_val, target, len); + } + + res + } + /// Convert the type erased pointer back into a pointer. /// /// # Safety /// - /// The type `T` must be the same type as the one used with `new`. + /// The type `T` must be the same type as the one used with `new` or `new_mut`. unsafe fn as_ptr(&self) -> *const T { // SAFETY: The constructor ensures that the first `size_of::()` // bytes of `&self.value` are a valid `*const T` pointer. unsafe { core::mem::transmute_copy(&self.value) } } + + /// Convert the type erased pointer back into a pointer. + /// + /// # Safety + /// + /// This [`ErasedPtr`] must have been constructed from [`Selector::new_mut`], using the same `T` type used in + /// this function. + unsafe fn as_mut_ptr(&self) -> *mut T { + // SAFETY: The constructor ensures that the first `size_of::()` + // bytes of `&self.value` are a valid `*const T` pointer. + // The caller ensures that the original pointer was mutable. + unsafe { core::mem::transmute_copy(&self.value) } + } } /// Type that can dynamically retrieve a type registered by an [`ExtensionProvider`] object. @@ -50,6 +83,7 @@ pub struct Selector<'a> { __lifetime: PhantomData<&'a ()>, target: TypeId, found: Option, + found_mut: Option, } impl<'a> Selector<'a> { @@ -58,6 +92,7 @@ impl<'a> Selector<'a> { __lifetime: PhantomData, target: TypeId::of::(), found: None, + found_mut: None, } } @@ -68,9 +103,22 @@ impl<'a> Selector<'a> { self } + pub fn register_mut(&mut self, value: &mut T) -> &mut Self { + if self.target == TypeId::of::() { + self.found_mut = Some(ErasedPtr::new_mut(value)); + } + self + } + pub(crate) fn finish(self) -> Option<&'a I> { assert_eq!(self.target, TypeId::of::()); - Some(unsafe { &*self.found?.as_ptr() }) + let target = self.found.or(self.found_mut)?; + Some(unsafe { &*target.as_ptr() }) + } + + pub(crate) fn finish_mut(self) -> Option<&'a mut I> { + assert_eq!(self.target, TypeId::of::()); + Some(unsafe { &mut *self.found_mut?.as_mut_ptr() }) } } @@ -80,6 +128,12 @@ pub trait ExtensionProvider: 'static { /// function will be called for every request. Runtime checks are expected, but this function should remain as fast /// as possible. fn register<'a, 'sel>(&'a self, selector: &'sel mut Selector<'a>) -> &'sel mut Selector<'a>; + fn register_mut<'a, 'sel>( + &'a mut self, + selector: &'sel mut Selector<'a>, + ) -> &'sel mut Selector<'a> { + self.register(selector) + } } const _EXTENSION_TRAIT_ASSERTS: () = { @@ -88,14 +142,25 @@ const _EXTENSION_TRAIT_ASSERTS: () = { }; pub trait ExtensionProviderExt: ExtensionProvider { - /// Look up [`T`] from the extension if it is registered. + /// Look up [`T`] from the extension if it is registered. Returns an immutable reference. + fn lookup(&self) -> Option<&T>; + + /// Look up [`T`] from the extension if it is registered. Returns as mutable reference. + fn lookup_mut(&mut self) -> Option<&mut T>; +} + +impl ExtensionProviderExt for E { fn lookup(&self) -> Option<&T> { let mut selector = Selector::new::(); - { - self.register(&mut selector); - } + selector.register::(self); + self.register(&mut selector); selector.finish::() } -} -impl ExtensionProviderExt for E {} + fn lookup_mut(&mut self) -> Option<&mut T> { + let mut selector = Selector::new::(); + selector.register_mut::(self); + self.register_mut(&mut selector); + selector.finish_mut::() + } +} From 5d2dc3b17701be92c057ca6f1ce5877ca7d06e3a Mon Sep 17 00:00:00 2001 From: Nathan Graule Date: Thu, 23 Jul 2026 16:28:17 +0200 Subject: [PATCH 26/38] feat(core): example showcasing dynamic lookup + dx fixes in core --- crates/interflow-core/src/device.rs | 14 ++++++--- crates/interflow-core/src/lib.rs | 13 +++++++- crates/interflow-core/src/traits.rs | 48 ++++++++++++++++++++++++++--- examples/ext_provider_lookup.rs | 48 +++++++++++++++++++++++++++++ src/lib.rs | 3 +- 5 files changed, 116 insertions(+), 10 deletions(-) create mode 100644 examples/ext_provider_lookup.rs diff --git a/crates/interflow-core/src/device.rs b/crates/interflow-core/src/device.rs index 95dfabb..6792264 100644 --- a/crates/interflow-core/src/device.rs +++ b/crates/interflow-core/src/device.rs @@ -1,6 +1,6 @@ use crate::stream::{self, StreamHandle}; -use crate::traits::ExtensionProvider; -use crate::DeviceType; +use crate::traits::{Enumerate, Enumerator, ExtensionProvider}; +use crate::{dyn_compatible, DeviceType}; use std::borrow::Cow; /// Configuration for an audio stream. @@ -127,13 +127,19 @@ pub struct Channel<'a> { } pub trait NamedChannels { - fn channel_map(&self) -> impl Iterator>; + fn enumerate_channels(&self) -> Enumerate<&'_ dyn Enumerator>>; } +dyn_compatible!(NamedChannels); + pub trait ConfigurationList { - fn enumerate_configurations(&self) -> impl Iterator; + fn enumerate_configurations(&self) -> Enumerate<&'_ dyn Enumerator>; } +dyn_compatible!(ConfigurationList); + pub trait DeviceState { fn connected(&self) -> bool; } + +dyn_compatible!(DeviceState); diff --git a/crates/interflow-core/src/lib.rs b/crates/interflow-core/src/lib.rs index a092e29..c8cb093 100644 --- a/crates/interflow-core/src/lib.rs +++ b/crates/interflow-core/src/lib.rs @@ -13,7 +13,7 @@ pub mod prelude { pub use crate::buffer::{AudioBuffer, AudioMut, AudioRef}; pub use crate::device::{self, Device as _}; pub use crate::platform; - pub use crate::proxies::{self, CreateStreamExt, DeviceProxy, PlatformProxy}; + pub use crate::proxies; pub use crate::stream::{ self, AudioInput, AudioOutput, ChannelFlags, StreamHandle, StreamLatency, StreamProxy, }; @@ -76,3 +76,14 @@ impl DeviceType { self.contains(Self::DUPLEX) } } + +/// Adds compile-time checks that the given trait is dyn-safe. +#[macro_export] +macro_rules! dyn_compatible { + ($(<$($generic:ident),+>)? $trait_:ident $(<$($(:$generic_name:ident =)?$generic_type:ident),+>)?) => { + const _: () = { + #[expect(unused)] + const fn typeable$(<$($generic),*>)?(_: &dyn $trait_ $(<$($($generic_name =)? $generic_type),*>)?) {} + }; + }; +} diff --git a/crates/interflow-core/src/traits.rs b/crates/interflow-core/src/traits.rs index ab5fd85..7b866c3 100644 --- a/crates/interflow-core/src/traits.rs +++ b/crates/interflow-core/src/traits.rs @@ -1,3 +1,4 @@ +use crate::dyn_compatible; use std::any::TypeId; use std::marker::PhantomData; use std::mem::MaybeUninit; @@ -136,10 +137,7 @@ pub trait ExtensionProvider: 'static { } } -const _EXTENSION_TRAIT_ASSERTS: () = { - const fn typeable() {} - typeable::(); -}; +dyn_compatible!(ExtensionProvider); pub trait ExtensionProviderExt: ExtensionProvider { /// Look up [`T`] from the extension if it is registered. Returns an immutable reference. @@ -164,3 +162,45 @@ impl ExtensionProviderExt for E { selector.finish_mut::() } } + +pub trait Enumerator { + type Item; + fn len(&self) -> usize; + fn at(&self, index: usize) -> Option; +} + +impl<'a, T> Enumerator for &'a dyn Enumerator { + type Item = T; + + fn len(&self) -> usize { + (*self).len() + } + + fn at(&self, index: usize) -> Option { + (*self).at(index) + } +} + +pub struct Enumerate { + enumerator: E, + i: usize, +} + +impl Iterator for Enumerate { + type Item = E::Item; + + fn next(&mut self) -> Option { + let result = self.enumerator.at(self.i); + self.i += 1; + result + } + + fn size_hint(&self) -> (usize, Option) { + let len = self.enumerator.len(); + (len, Some(len)) + } +} + +pub fn enumerate(enumerator: &dyn Enumerator) -> impl use<'_, T> + Iterator { + Enumerate { enumerator, i: 0 } +} diff --git a/examples/ext_provider_lookup.rs b/examples/ext_provider_lookup.rs new file mode 100644 index 0000000..46676de --- /dev/null +++ b/examples/ext_provider_lookup.rs @@ -0,0 +1,48 @@ +//! An example of using the [`ExtensionProvider`] trait through [`ExtensionProviderExt::lookup`] to dynamically +//! lookup types and traits registered by the backend types. +use interflow::prelude::*; +use interflow_coreaudio::device::CoreAudioDeviceExt; + +fn main() -> anyhow::Result<()> { + let device = default_device(DeviceType::OUTPUT)?; + + println!("Selected device name: {:?}", device.name()); + + // Assuming the above device originates from platform-independent code, we can query for the concrete backend + // type. + #[cfg(any(target_os = "macos", target_os = "ios"))] + if let Some(device) = device.lookup::() { + let audio_unit = device.get_audio_unit()?; + let stream_format = audio_unit.output_stream_format()?; + println!( + "CoreAudio output stream format for {:?}: {stream_format:#?}", + device.name() + ); + } + + // There are traits that can be implemented by several backends, they can also be queried for and used if they + // are registered by the backend type. + if let Some(ext) = device.lookup::() { + println!("Device is connected: {}", ext.connected()); + } + + // A concrete example is listing configurations: not all platforms support this feature, and instead of + // "(un)graceful degradation", we instead only implement and register this trait when it makes sense. + if let Some(enumerator) = device.lookup::() { + for config in enumerator.enumerate_configurations() { + println!("\tConfiguration: {config:?}"); + } + } else { + println!("Device does not support listing configurations"); + } + + // Another useful extension allows iterating over named channels of a device. + if let Some(enumerator) = device.lookup::() { + for channel in enumerator.enumerate_channels() { + println!("\tChannel {:3}: {}", channel.index, channel.name); + } + } else { + println!("Device does not have named channels"); + } + Ok(()) +} diff --git a/src/lib.rs b/src/lib.rs index fa7f88d..cccc402 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -13,7 +13,8 @@ pub mod backends; pub mod prelude { pub use super::{default_device, default_platform, default_stream}; pub use interflow_core::prelude::*; - pub use interflow_coreaudio::prelude::*; + #[cfg(any(target_os = "macos", target_os = "ios"))] + pub use interflow_coreaudio::prelude as coreaudio; } /// Return the default platform. From 1f736933ac9bc62718ca22d58a28ce82b8753600 Mon Sep 17 00:00:00 2001 From: Nathan Graule Date: Thu, 23 Jul 2026 23:45:04 +0200 Subject: [PATCH 27/38] refactor(core): make callback take references to input and output --- crates/interflow-core/src/proxies.rs | 6 +++--- crates/interflow-core/src/stream.rs | 10 +++++----- crates/interflow-coreaudio/src/stream.rs | 12 ++++++------ examples/util/sine.rs | 8 ++------ 4 files changed, 16 insertions(+), 20 deletions(-) diff --git a/crates/interflow-core/src/proxies.rs b/crates/interflow-core/src/proxies.rs index 8875bbc..ab88ba6 100644 --- a/crates/interflow-core/src/proxies.rs +++ b/crates/interflow-core/src/proxies.rs @@ -116,7 +116,7 @@ pub struct DynCallback { type_id: TypeId, handle: NonNull<()>, prepare: unsafe fn(NonNull<()>, CallbackContext), - process_audio: unsafe fn(NonNull<()>, CallbackContext, AudioInput, AudioOutput), + process_audio: unsafe fn(NonNull<()>, CallbackContext, &AudioInput, &mut AudioOutput), } unsafe impl Send for DynCallback {} @@ -151,8 +151,8 @@ impl stream::Callback for DynCallback { fn process_audio( &mut self, context: CallbackContext, - input: AudioInput, - output: AudioOutput, + input: &AudioInput, + output: &mut AudioOutput, ) { unsafe { (self.process_audio)(self.handle, context, input, output) } } diff --git a/crates/interflow-core/src/stream.rs b/crates/interflow-core/src/stream.rs index 70c9855..541b379 100644 --- a/crates/interflow-core/src/stream.rs +++ b/crates/interflow-core/src/stream.rs @@ -72,19 +72,19 @@ pub trait Callback: Send { fn process_audio( &mut self, context: CallbackContext, - input: AudioInput, - output: AudioOutput, + input: &AudioInput, + output: &mut AudioOutput, ); } -impl, AudioOutput)> Callback for F { +impl, &mut AudioOutput)> Callback for F { fn prepare(&mut self, _: CallbackContext) {} fn process_audio( &mut self, context: CallbackContext, - input: AudioInput, - output: AudioOutput, + input: &AudioInput, + output: &mut AudioOutput, ) { (self)(context, input, output); } diff --git a/crates/interflow-coreaudio/src/stream.rs b/crates/interflow-coreaudio/src/stream.rs index 83d1621..0407977 100644 --- a/crates/interflow-coreaudio/src/stream.rs +++ b/crates/interflow-coreaudio/src/stream.rs @@ -161,7 +161,7 @@ impl Handle { NonZeroUsize::new(1).unwrap(), NonZeroUsize::new(channels as _).unwrap(), ); - let dummy_output = AudioOutput { + let mut dummy_output = AudioOutput { buffer: dummy_buf.as_mut(), timestamp: Timestamp::new(asbd.mSampleRate), channel_flags: &[], @@ -174,8 +174,8 @@ impl Handle { timestamp, stream_proxy: &STREAM_PROXY, }, - input, - dummy_output, + &input, + &mut dummy_output, ); } Ok(()) @@ -244,7 +244,7 @@ impl Handle { channel_flags: &[], }; - let output = AudioOutput { + let mut output = AudioOutput { buffer: buffer.as_mut(), timestamp, channel_flags: &[], @@ -257,8 +257,8 @@ impl Handle { timestamp, stream_proxy: &STREAM_PROXY, }, - dummy_input, - output, + &dummy_input, + &mut output, ); for (out_ch, in_ch) in args.data.channels_mut().zip(buffer.iter_channels()) { out_ch.copy_from_slice(in_ch); diff --git a/examples/util/sine.rs b/examples/util/sine.rs index b0d7887..0540fdc 100644 --- a/examples/util/sine.rs +++ b/examples/util/sine.rs @@ -14,13 +14,9 @@ impl Callback for SineWave { fn process_audio( &mut self, context: CallbackContext, - _input: AudioInput, - mut output: AudioOutput, + input: &AudioInput, + output: &mut AudioOutput, ) { - eprintln!( - "Callback called, timestamp: {:2.3} s", - context.timestamp.as_seconds() - ); let sr = context.timestamp.samplerate as f32; for i in 0..output.buffer.frames() { output.buffer.set_mono(i, self.next_sample()); From 9ebc5d4bbe4c8d2d612f144e851c3cee2fd44989 Mon Sep 17 00:00:00 2001 From: Nathan Graule Date: Thu, 23 Jul 2026 23:45:54 +0200 Subject: [PATCH 28/38] refactor(core): reborrow AudioMut as immutable + make AudioRef copy/clone --- crates/interflow-core/src/buffer.rs | 7 +++++++ crates/interflow-core/src/stream.rs | 7 ++++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/crates/interflow-core/src/buffer.rs b/crates/interflow-core/src/buffer.rs index b66ff15..8d6383b 100644 --- a/crates/interflow-core/src/buffer.rs +++ b/crates/interflow-core/src/buffer.rs @@ -450,4 +450,11 @@ impl AudioMut<'_, T> { *frame *= factor; } } + + pub fn as_ref(&self) -> AudioRef<'_, T> { + AudioRef { + buffer: self.buffer, + frame_slice: self.frame_slice, + } + } } diff --git a/crates/interflow-core/src/stream.rs b/crates/interflow-core/src/stream.rs index 541b379..d3c94b1 100644 --- a/crates/interflow-core/src/stream.rs +++ b/crates/interflow-core/src/stream.rs @@ -20,13 +20,14 @@ pub trait StreamLatency { } #[duplicate::duplicate_item( - name bufty; - [AudioInput] [AudioRef < 'a, T >]; - [AudioOutput] [AudioMut < 'a, T >]; + name bufty derive; + [AudioInput] [AudioRef < 'a, T >] [derive(Copy, Clone)]; + [AudioOutput] [AudioMut < 'a, T >] [derive()]; )] /// Plain-old-data object holding references to the audio buffer and the associated time-keeping /// [`Timestamp`]. This timestamp is associated with the stream, and in the cases where the /// driver provides timing information, it is used instead of relying on sample-counting. +#[derive] pub struct name<'a, T> { /// Associated time stamp for this callback. The time represents the duration for which the /// stream has been opened, and is either provided by the driver if available, or is kept up From 92085e57f3f71d2bb5d148357aff4c5e2f209401 Mon Sep 17 00:00:00 2001 From: Nathan Graule Date: Thu, 23 Jul 2026 23:46:33 +0200 Subject: [PATCH 29/38] chore(examples): add specific device example with ability to choose output --- Cargo.lock | 113 +++++++++++++++- Cargo.toml | 1 + crates/interflow-core/src/lib.rs | 2 +- crates/interflow-core/src/proxies.rs | 11 ++ crates/interflow-core/src/timing.rs | 7 + crates/interflow-coreaudio/src/device.rs | 124 +++++++++++++++--- crates/interflow-coreaudio/src/platform.rs | 13 +- examples/enumerate.rs | 1 + ...vider_lookup.rs => ext-provider-lookup.rs} | 0 examples/specific-device.rs | 34 +++++ 10 files changed, 276 insertions(+), 30 deletions(-) rename examples/{ext_provider_lookup.rs => ext-provider-lookup.rs} (100%) create mode 100644 examples/specific-device.rs diff --git a/Cargo.lock b/Cargo.lock index 15b754f..b9a08aa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -171,6 +171,19 @@ dependencies = [ "bindgen", ] +[[package]] +name = "dialoguer" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25f104b501bf2364e78d0d3974cbc774f738f5865306ed128e1e0d7499c0ad96" +dependencies = [ + "console", + "fuzzy-matcher", + "shell-words", + "tempfile", + "zeroize", +] + [[package]] name = "dispatch2" version = "0.3.0" @@ -227,6 +240,42 @@ dependencies = [ "log", ] +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + +[[package]] +name = "fuzzy-matcher" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54614a3312934d066701a80f20f15fa3b56d67ac7722b39eea5b4c9dd1d66c94" +dependencies = [ + "thread_local", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi", +] + [[package]] name = "glob" version = "0.3.3" @@ -258,6 +307,7 @@ version = "0.1.0" dependencies = [ "anyhow", "cfg_aliases", + "dialoguer", "env_logger", "indicatif", "interflow-core", @@ -351,9 +401,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.180" +version = "0.2.189" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" [[package]] name = "libloading" @@ -365,6 +415,12 @@ dependencies = [ "windows-link", ] +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + [[package]] name = "log" version = "0.4.29" @@ -528,6 +584,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + [[package]] name = "regex" version = "1.12.3" @@ -563,6 +625,19 @@ version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + [[package]] name = "rustversion" version = "1.0.22" @@ -589,6 +664,12 @@ dependencies = [ "syn", ] +[[package]] +name = "shell-words" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc6fe69c597f9c37bfeeeeeb33da3530379845f10be461a66d16d03eca2ded77" + [[package]] name = "shlex" version = "1.3.0" @@ -606,6 +687,19 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom", + "once_cell", + "rustix", + "windows-sys", +] + [[package]] name = "thiserror" version = "2.0.18" @@ -626,6 +720,15 @@ dependencies = [ "syn", ] +[[package]] +name = "thread_local" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070" +dependencies = [ + "cfg-if", +] + [[package]] name = "unicode-ident" version = "1.0.23" @@ -849,3 +952,9 @@ dependencies = [ "quote", "syn", ] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" diff --git a/Cargo.toml b/Cargo.toml index a89f900..ab8be5b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,6 +32,7 @@ interflow-core.workspace = true [dev-dependencies] anyhow.workspace = true env_logger = "0.11.8" +dialoguer = { version = "0.12.0", features = ["completion", "fuzzy-select"] } indicatif = "0.18.3" [build-dependencies] diff --git a/crates/interflow-core/src/lib.rs b/crates/interflow-core/src/lib.rs index c8cb093..8897e1d 100644 --- a/crates/interflow-core/src/lib.rs +++ b/crates/interflow-core/src/lib.rs @@ -13,7 +13,7 @@ pub mod prelude { pub use crate::buffer::{AudioBuffer, AudioMut, AudioRef}; pub use crate::device::{self, Device as _}; pub use crate::platform; - pub use crate::proxies; + pub use crate::proxies::{self, CreateStreamExt}; pub use crate::stream::{ self, AudioInput, AudioOutput, ChannelFlags, StreamHandle, StreamLatency, StreamProxy, }; diff --git a/crates/interflow-core/src/proxies.rs b/crates/interflow-core/src/proxies.rs index ab88ba6..4ea628f 100644 --- a/crates/interflow-core/src/proxies.rs +++ b/crates/interflow-core/src/proxies.rs @@ -20,6 +20,7 @@ pub type DynDevice = Rc; pub trait PlatformProxy: ExtensionProvider { fn name(&self) -> Cow<'static, str>; fn list_devices(&self) -> Result>; + fn list_devices_matching(&self, device_type: DeviceType) -> Result>; fn default_device(&self, device_type: DeviceType) -> Result; } @@ -41,6 +42,16 @@ where )) } + fn list_devices_matching(&self, device_type: DeviceType) -> Result> { + Ok(Vec::from_iter( + Platform::list_devices(self)?.into_iter().filter_map(|dev| { + Device::device_type(&dev) + .contains(device_type) + .then(|| Rc::new(dev) as DynDevice) + }), + )) + } + fn default_device(&self, device_type: DeviceType) -> Result { let device = Platform::default_device(self, device_type)?; Ok(Rc::new(device) as DynDevice) diff --git a/crates/interflow-core/src/timing.rs b/crates/interflow-core/src/timing.rs index 5b01269..7e378de 100644 --- a/crates/interflow-core/src/timing.rs +++ b/crates/interflow-core/src/timing.rs @@ -134,6 +134,13 @@ pub struct AtomicTimestamp { } impl AtomicTimestamp { + /// Create a new [`AtomicTimestamp`] with zeroed values. + pub fn zeroed() -> Self { + Self { + samplerate: AtomicU64::new(0), + counter: AtomicU64::new(0), + } + } /// Update the contents with the provided [`Timestamp`]. pub fn update(&self, ts: Timestamp) { self.samplerate.store( diff --git a/crates/interflow-coreaudio/src/device.rs b/crates/interflow-coreaudio/src/device.rs index eaf6ab9..bdaa785 100644 --- a/crates/interflow-coreaudio/src/device.rs +++ b/crates/interflow-coreaudio/src/device.rs @@ -6,25 +6,35 @@ use coreaudio::audio_unit::macos_helpers::{ }; use coreaudio::audio_unit::{AudioUnit, Element, IOType, Scope}; use coreaudio_sys::{ - kAudioDevicePropertyBufferFrameSizeRange, kAudioObjectPropertyElementMaster, - kAudioObjectPropertyScopeInput, kAudioObjectPropertyScopeOutput, - kAudioOutputUnitProperty_EnableIO, AudioDeviceID, AudioObjectPropertyAddress, AudioValueRange, + kAudioDevicePropertyBufferFrameSizeRange, kAudioHardwarePropertyDefaultInputDevice, + kAudioHardwarePropertyDefaultOutputDevice, kAudioHardwarePropertyDefaultSystemOutputDevice, + kAudioObjectPropertyElementMaster, kAudioObjectPropertyScopeGlobal, + kAudioObjectPropertyScopeInput, kAudioObjectPropertyScopeOutput, kAudioObjectSystemObject, + kAudioOutputUnitProperty_CurrentDevice, kAudioOutputUnitProperty_EnableIO, AudioDeviceID, + AudioObjectGetPropertyData, AudioObjectID, AudioObjectPropertyAddress, AudioValueRange, }; use interflow_core::device::StreamConfig; use interflow_core::traits::{ExtensionProvider, Selector}; use interflow_core::{device, stream, DeviceType}; use std::borrow::Cow; use std::cell::OnceCell; +use std::ffi::c_uint; +use std::ptr::null; /// Audio device request type. CoreAudio has separate code paths for getting the default device, and getting a /// specific ID. Furthermore, "default device"s are automatically re-routed when the user changes the default device. #[derive(Debug, Default, Copy, Clone)] pub enum DeviceRequest { - /// Automatically connects to the default device output, generic stream. + /// Automatically connects to the default output device, generic stream. #[default] DefaultOutput, - /// Automatically connects to the default device output, notification stream. + /// Automatically connects to the default output device, notification stream. SystemOutput, + /// Automatically connects to the default input device. + DefaultInput, + /// Automatically connects to the default voice input device, using the system's voice enhancement processing if + /// enabled. + VoiceInput, /// Connect to this specific output. Specific(AudioDeviceID), } @@ -34,9 +44,37 @@ impl DeviceRequest { match self { Self::DefaultOutput => Ok(AudioUnit::new(IOType::DefaultOutput)?), Self::SystemOutput => Ok(AudioUnit::new(IOType::SystemOutput)?), - &Self::Specific(..) => { - // TODO: Use provided ID + Self::VoiceInput => Ok(AudioUnit::new(IOType::VoiceProcessingIO)?), + Self::DefaultInput => { let mut unit = AudioUnit::new(IOType::HalOutput)?; + unit.set_property( + kAudioOutputUnitProperty_CurrentDevice, + Scope::Global, + Element::Output, + Some(&get_default_device_id(true)), + )?; + unit.set_property( + kAudioOutputUnitProperty_EnableIO, + Scope::Input, + Element::Input, + Some(&1u32), + )?; + unit.set_property( + kAudioOutputUnitProperty_EnableIO, + Scope::Output, + Element::Output, + Some(&0u32), + )?; + Ok(unit) + } + &Self::Specific(id) => { + let mut unit = AudioUnit::new(IOType::HalOutput)?; + unit.set_property( + kAudioOutputUnitProperty_CurrentDevice, + Scope::Global, + Element::Output, + Some(&id), + )?; let device_type = self.device_type(); let value = if device_type.is_input() { 1u32 } else { 0 }; unit.set_property( @@ -60,17 +98,53 @@ impl DeviceRequest { /// Return the name associated with this [`DeviceRequest`]. This is a descriptive name, and does not uniquely /// identify the device. pub fn name(&self) -> Cow<'_, str> { - match self { - DeviceRequest::DefaultOutput => Cow::Borrowed("Default output"), - DeviceRequest::SystemOutput => Cow::Borrowed("Notifications output"), - &DeviceRequest::Specific(id) => match get_device_name(id) { - Ok(s) => Cow::Owned(s), - Err(err) => { - log::error!("Cannot get device name for ID: {}: {err}", id); - Cow::Borrowed("") + let (selector, fallback) = match self { + Self::DefaultOutput => (kAudioHardwarePropertyDefaultOutputDevice, "Default Output"), + Self::DefaultInput => (kAudioHardwarePropertyDefaultInputDevice, "Default Input"), + Self::SystemOutput => ( + kAudioHardwarePropertyDefaultSystemOutputDevice, + "Notifications output", + ), + Self::VoiceInput => (c_uint::MAX, "Voice Input"), + &Self::Specific(id) => { + return match get_device_name(id) { + Ok(s) => Cow::Owned(s), + Err(err) => { + log::error!("Cannot get device name for ID: {}: {err}", id); + Cow::Borrowed("") + } } - }, - } + } + }; + + (|| { + let property_address = AudioObjectPropertyAddress { + mSelector: selector, + mScope: kAudioObjectPropertyScopeGlobal, + mElement: kAudioObjectPropertyElementMaster, + }; + + let mut audio_device_id: AudioDeviceID = 0; + let mut data_size = size_of::() as u32; + let status = unsafe { + AudioObjectGetPropertyData( + kAudioObjectSystemObject as AudioObjectID, + &property_address, + 0, + null(), + &mut data_size, + (&raw mut audio_device_id).cast(), + ) + }; + coreaudio::Error::from_os_status(status)?; + let name = get_device_name(audio_device_id)?; + let name = Cow::Owned(format!("{fallback} ({name})")); + Ok(name) + })() + .unwrap_or_else(|err: Error| { + log::error!("Cannot get device name: {err}"); + Cow::Borrowed(fallback) + }) } /// Returns the [`DeviceType`] that best describes this [`DeviceRequest`]. @@ -84,17 +158,20 @@ impl DeviceRequest { }; let supports_scope = |scope| match self { Self::DefaultOutput | Self::SystemOutput if matches!(scope, Scope::Output) => true, + Self::DefaultInput | Self::VoiceInput if matches!(scope, Scope::Input) => true, &Self::Specific(id) => log_error(get_audio_device_supports_scope(id, scope)), _ => false, }; let is_default = match self { - Self::DefaultOutput | Self::SystemOutput => true, + Self::DefaultOutput | Self::SystemOutput | Self::DefaultInput | Self::VoiceInput => { + true + } &Self::Specific(id) => { get_default_device_id(true) == Some(id) || get_default_device_id(false) == Some(id) } }; - let mut type_ = DeviceType::empty(); + let mut type_ = DeviceType::PHYSICAL; type_.set(DeviceType::INPUT, supports_scope(Scope::Input)); type_.set(DeviceType::OUTPUT, supports_scope(Scope::Output)); type_.set(DeviceType::DEFAULT, is_default); @@ -104,7 +181,9 @@ impl DeviceRequest { /// Queries the accepted range of buffer sizes that the device accepts. pub fn buffer_size_range(&self) -> Result<(Option, Option), Error> { match self { - Self::DefaultOutput | Self::SystemOutput => Ok((None, None)), + Self::DefaultOutput | Self::SystemOutput | Self::DefaultInput | Self::VoiceInput => { + Ok((None, None)) + } &Self::Specific(id) => { let address = AudioObjectPropertyAddress { mSelector: kAudioDevicePropertyBufferFrameSizeRange, @@ -148,6 +227,11 @@ impl Device { Self::new(DeviceRequest::DefaultOutput) } + /// Create a device following the default input. + pub fn default_input() -> Self { + Self::new(DeviceRequest::DefaultInput) + } + /// Consumes this device object, returning the associated Audio Unit. pub fn into_audio_unit(mut self) -> Result { match self.audio_unit.take() { diff --git a/crates/interflow-coreaudio/src/platform.rs b/crates/interflow-coreaudio/src/platform.rs index cbd1a0b..d09318b 100644 --- a/crates/interflow-coreaudio/src/platform.rs +++ b/crates/interflow-coreaudio/src/platform.rs @@ -23,16 +23,15 @@ impl platform::Platform for Platform { fn default_device(&self, device_type: DeviceType) -> Result { if device_type.is_output() || !device_type.is_input() { - return Ok(Device::default_output()); + Ok(Device::default_output()) + } else { + Ok(Device::default_input()) } - - let Some(id) = get_default_device_id(device_type.is_input()) else { - return Err(Error::NoMatchingDevices(device_type)); - }; - Ok(Device::from_id(id)) } fn list_devices(&self) -> Result, Self::Error> { - Ok(get_audio_device_ids()?.into_iter().map(Device::from_id)) + Ok([Device::default_output(), Device::default_input()] + .into_iter() + .chain(get_audio_device_ids()?.into_iter().map(Device::from_id))) } } diff --git a/examples/enumerate.rs b/examples/enumerate.rs index d2d9beb..2d1ea71 100644 --- a/examples/enumerate.rs +++ b/examples/enumerate.rs @@ -1,3 +1,4 @@ +use interflow::core::proxies::PlatformProxy; use interflow::prelude::*; pub fn enumerate_devices(platform: &dyn PlatformProxy) -> anyhow::Result<()> { diff --git a/examples/ext_provider_lookup.rs b/examples/ext-provider-lookup.rs similarity index 100% rename from examples/ext_provider_lookup.rs rename to examples/ext-provider-lookup.rs diff --git a/examples/specific-device.rs b/examples/specific-device.rs new file mode 100644 index 0000000..01e6969 --- /dev/null +++ b/examples/specific-device.rs @@ -0,0 +1,34 @@ +use crate::util::sine::SineWave; +use anyhow::Context; +use dialoguer::console::Term; +use dialoguer::FuzzySelect; +use interflow::prelude::*; + +mod util; + +fn main() -> anyhow::Result<()> { + env_logger::init(); + + let term = Term::stdout(); + let platform = default_platform(); + let devices = platform + .list_devices_matching(DeviceType::OUTPUT) + .context("Cannot list devices")?; + let Some(index) = FuzzySelect::new() + .default(0) + .items(devices.iter().map(|dev| dev.name())) + .interact_on_opt(&term) + .context("Cannot display list")? + else { + return Ok(()); + }; + + let (callback, display) = util::prepare_display(SineWave::new(440.0)); + + let handle = devices[index] + .default_stream(DeviceType::OUTPUT, callback) + .context("Cannot create stream")?; + display()?; + let _ = handle.eject()?; + Ok(()) +} From 7135dc7e3bdb9d6ce307c61cf5f8b65ffa712b93 Mon Sep 17 00:00:00 2001 From: Nathan Graule Date: Thu, 23 Jul 2026 23:47:41 +0200 Subject: [PATCH 30/38] wip: input example --- examples/input.rs | 12 +++++++ examples/sine-wave.rs | 16 +++++++++ examples/sine_wave.rs | 19 ---------- examples/util/meter.rs | 78 ++++++++++++++++++++++++++++++++++++++---- examples/util/mod.rs | 27 +++++++++++++++ examples/util/noop.rs | 16 +++++++++ 6 files changed, 143 insertions(+), 25 deletions(-) create mode 100644 examples/input.rs create mode 100644 examples/sine-wave.rs delete mode 100644 examples/sine_wave.rs create mode 100644 examples/util/noop.rs diff --git a/examples/input.rs b/examples/input.rs new file mode 100644 index 0000000..e9287ec --- /dev/null +++ b/examples/input.rs @@ -0,0 +1,12 @@ +use crate::util::noop::Noop; +use interflow::prelude::*; + +mod util; + +fn main() -> anyhow::Result<()> { + let (callback, display) = util::prepare_display(Noop); + let handle = default_stream(DeviceType::INPUT, callback)?; + display()?; + let _ = handle.eject()?; + Ok(()) +} diff --git a/examples/sine-wave.rs b/examples/sine-wave.rs new file mode 100644 index 0000000..293dadb --- /dev/null +++ b/examples/sine-wave.rs @@ -0,0 +1,16 @@ +use anyhow::Result; +use interflow::prelude::*; +use util::sine::SineWave; + +mod util; + +fn main() -> Result<()> { + env_logger::init(); + let (callback, display) = util::prepare_display(SineWave::new(440.0)); + let stream = default_stream(DeviceType::OUTPUT | DeviceType::PHYSICAL, callback)?; + + display()?; + + let _ = stream.eject()?; + Ok(()) +} diff --git a/examples/sine_wave.rs b/examples/sine_wave.rs deleted file mode 100644 index f5da105..0000000 --- a/examples/sine_wave.rs +++ /dev/null @@ -1,19 +0,0 @@ -use anyhow::Result; -use interflow::prelude::*; -use util::sine::SineWave; - -mod util; - -fn main() -> Result<()> { - env_logger::init(); - - let stream = default_stream( - DeviceType::OUTPUT | DeviceType::PHYSICAL, - SineWave::new(440.0), - )?; - println!("Press Enter to stop"); - std::io::stdin().read_line(&mut String::new())?; - stream.eject()?; - println!("Stream ejected"); - Ok(()) -} diff --git a/examples/util/meter.rs b/examples/util/meter.rs index 1516546..0cbbdd8 100644 --- a/examples/util/meter.rs +++ b/examples/util/meter.rs @@ -1,21 +1,33 @@ +use crate::util::AtomicF32; use interflow::core::buffer::AudioRef; +use interflow_core::prelude::{AtomicTimestamp, AudioInput, AudioOutput}; +use interflow_core::stream; +use interflow_core::stream::CallbackContext; +use std::sync::atomic::Ordering; +use std::sync::Arc; -#[derive(Debug, Copy, Clone)] +#[derive(Clone)] pub struct PeakMeter { + output: Arc, last_out: f32, decay: f32, dt: f32, } impl PeakMeter { - pub fn new(samplerate: f32, decay: f32) -> Self { + pub fn new(decay: f32) -> Self { Self { + output: Arc::new(AtomicF32::new(0.)), last_out: 0., decay, - dt: 1. / samplerate, + dt: 0.0, } } + pub fn output(&self) -> Arc { + self.output.clone() + } + pub fn samplerate(&self) -> f32 { 1. / self.dt } @@ -32,12 +44,10 @@ impl PeakMeter { self.decay = decay; } - pub fn value(&self) -> f32 { - self.last_out - } pub fn process(&mut self, sample: f32) -> f32 { let k = f32::exp(-self.decay * self.dt); self.last_out = (k * sample).max(self.last_out); + self.output.store(self.last_out, Ordering::Relaxed); self.last_out } @@ -48,6 +58,62 @@ impl PeakMeter { .max_by(f32::total_cmp) .unwrap_or(0.); self.last_out = peak_lin.max(self.last_out * f32::exp(-self.decay * buffer_duration)); + self.output.store(self.last_out, Ordering::Relaxed); self.last_out } } + +#[derive(Clone)] +pub struct Metered { + pub inner: C, + input: PeakMeter, + output: PeakMeter, + timestamp: Arc, +} + +impl Metered { + pub fn new(inner: C, meter_decay: f32) -> Self { + Self { + inner, + input: PeakMeter::new(meter_decay), + output: PeakMeter::new(meter_decay), + timestamp: Arc::new(AtomicTimestamp::zeroed()), + } + } + + pub fn shared(&self) -> MeteredShared { + MeteredShared { + input: self.input.output.clone(), + output: self.output.output.clone(), + timestamp: self.timestamp.clone(), + } + } +} + +impl stream::Callback for Metered { + fn prepare(&mut self, context: CallbackContext) { + self.input + .set_samplerate(context.stream_config.sample_rate as _); + self.output + .set_samplerate(context.stream_config.sample_rate as _); + self.inner.prepare(context); + } + + fn process_audio( + &mut self, + context: CallbackContext, + input: &AudioInput, + output: &mut AudioOutput, + ) { + self.timestamp.update(context.timestamp); + self.input.process_buffer(input.buffer); + self.inner.process_audio(context, input, output); + self.output.process_buffer(output.buffer.as_ref()); + } +} + +pub struct MeteredShared { + pub input: Arc, + pub output: Arc, + pub timestamp: Arc, +} diff --git a/examples/util/mod.rs b/examples/util/mod.rs index 8b3bb94..63db0f0 100644 --- a/examples/util/mod.rs +++ b/examples/util/mod.rs @@ -4,12 +4,16 @@ // Unfortunate consequence is that we lose the genuine unused warnings. #![allow(unused)] +use crate::util::meter::Metered; +use dialoguer::console::Term; use indicatif::{ProgressBar, ProgressStyle}; +use interflow_core::stream; use std::sync::atomic::{AtomicBool, AtomicU32, Ordering}; use std::sync::Arc; use std::thread; pub mod meter; +pub mod noop; pub mod sine; #[derive(Debug)] @@ -68,3 +72,26 @@ pub fn normalize(min: f32, max: f32, value: f32) -> f32 { let range = max - min; (value - min) / range } + +pub fn prepare_display( + callback: C, +) -> (meter::Metered, impl Fn() -> anyhow::Result<()>) { + let meter = Metered::new(callback, 0.5); + let shared = meter.shared(); + let display = move || { + let term = Term::stdout(); + loop { + let elapsed = shared.timestamp.as_timestamp().as_seconds(); + let inp = shared.input.load(Ordering::Relaxed); + let inp = 20.0 * inp.log10(); + let out = shared.output.load(Ordering::Relaxed); + let out = 20.0 * out.log10(); + term.write_str(&format!( + "Elapsed: {elapsed:3.2} s\tInput: {inp:2.1} dB\tOutput: {out:2.1} dB" + ))?; + std::thread::sleep(std::time::Duration::from_millis(50)); + term.clear_line()?; + } + }; + (meter, display) +} diff --git a/examples/util/noop.rs b/examples/util/noop.rs new file mode 100644 index 0000000..61f5d4d --- /dev/null +++ b/examples/util/noop.rs @@ -0,0 +1,16 @@ +use interflow::prelude::*; +use interflow_core::stream::CallbackContext; + +pub struct Noop; + +impl stream::Callback for Noop { + fn prepare(&mut self, context: CallbackContext) {} + + fn process_audio( + &mut self, + context: CallbackContext, + input: &AudioInput, + output: &mut AudioOutput, + ) { + } +} From de58ccc49f3c85a75bdfaad21c711b9788f50cc6 Mon Sep 17 00:00:00 2001 From: Nathan Graule Date: Fri, 24 Jul 2026 11:05:01 +0200 Subject: [PATCH 31/38] fix: property ordering for Audio Units when creating stream configurations also add example choosing a specific input device --- crates/interflow-coreaudio/src/device.rs | 27 ++++++++++--------- crates/interflow-coreaudio/src/stream.rs | 4 +-- examples/specific-device-input.rs | 34 ++++++++++++++++++++++++ 3 files changed, 51 insertions(+), 14 deletions(-) create mode 100644 examples/specific-device-input.rs diff --git a/crates/interflow-coreaudio/src/device.rs b/crates/interflow-coreaudio/src/device.rs index bdaa785..79c94c8 100644 --- a/crates/interflow-coreaudio/src/device.rs +++ b/crates/interflow-coreaudio/src/device.rs @@ -46,13 +46,7 @@ impl DeviceRequest { Self::SystemOutput => Ok(AudioUnit::new(IOType::SystemOutput)?), Self::VoiceInput => Ok(AudioUnit::new(IOType::VoiceProcessingIO)?), Self::DefaultInput => { - let mut unit = AudioUnit::new(IOType::HalOutput)?; - unit.set_property( - kAudioOutputUnitProperty_CurrentDevice, - Scope::Global, - Element::Output, - Some(&get_default_device_id(true)), - )?; + let mut unit = AudioUnit::new_uninitialized(IOType::HalOutput)?; unit.set_property( kAudioOutputUnitProperty_EnableIO, Scope::Input, @@ -65,16 +59,18 @@ impl DeviceRequest { Element::Output, Some(&0u32), )?; - Ok(unit) - } - &Self::Specific(id) => { - let mut unit = AudioUnit::new(IOType::HalOutput)?; + let input_device_id = get_default_device_id(true).unwrap_or(0); unit.set_property( kAudioOutputUnitProperty_CurrentDevice, Scope::Global, Element::Output, - Some(&id), + Some(&input_device_id), )?; + unit.initialize()?; + Ok(unit) + } + &Self::Specific(id) => { + let mut unit = AudioUnit::new_uninitialized(IOType::HalOutput)?; let device_type = self.device_type(); let value = if device_type.is_input() { 1u32 } else { 0 }; unit.set_property( @@ -90,6 +86,13 @@ impl DeviceRequest { Element::Output, Some(&value), )?; + unit.set_property( + kAudioOutputUnitProperty_CurrentDevice, + Scope::Global, + Element::Output, + Some(&id), + )?; + unit.initialize()?; Ok(unit) } } diff --git a/crates/interflow-coreaudio/src/stream.rs b/crates/interflow-coreaudio/src/stream.rs index 0407977..fce2267 100644 --- a/crates/interflow-coreaudio/src/stream.rs +++ b/crates/interflow-coreaudio/src/stream.rs @@ -106,8 +106,8 @@ impl Handle { )?; let frame_count: u32 = audio_unit.get_property( kAudioDevicePropertyBufferFrameSize, - Scope::Input, - Element::Input, + Scope::Global, + Element::Output, )?; let resolved_config = ResolvedStreamConfig { sample_rate: asbd.mSampleRate, diff --git a/examples/specific-device-input.rs b/examples/specific-device-input.rs new file mode 100644 index 0000000..a37d90b --- /dev/null +++ b/examples/specific-device-input.rs @@ -0,0 +1,34 @@ +use crate::util::noop::Noop; +use anyhow::Context; +use dialoguer::console::Term; +use dialoguer::FuzzySelect; +use interflow::prelude::*; + +mod util; + +fn main() -> anyhow::Result<()> { + env_logger::init(); + + let term = Term::stdout(); + let platform = default_platform(); + let devices = platform + .list_devices_matching(DeviceType::INPUT) + .context("Cannot list devices")?; + let Some(index) = FuzzySelect::new() + .default(0) + .items(devices.iter().map(|dev| dev.name())) + .interact_on_opt(&term) + .context("Cannot display list")? + else { + return Ok(()); + }; + + let (callback, display) = util::prepare_display(Noop); + + let handle = devices[index] + .default_stream(DeviceType::INPUT, callback) + .context("Cannot create stream")?; + display()?; + let _ = handle.eject()?; + Ok(()) +} From da43a7ca8cd8320d261607735cd0272dc8ff5963 Mon Sep 17 00:00:00 2001 From: Nathan Graule Date: Fri, 24 Jul 2026 11:41:35 +0200 Subject: [PATCH 32/38] chore: better metering display in examples --- examples/util/meter.rs | 4 ++-- examples/util/mod.rs | 49 ++++++++++++++++++++++++++++++++++++++---- 2 files changed, 47 insertions(+), 6 deletions(-) diff --git a/examples/util/meter.rs b/examples/util/meter.rs index 0cbbdd8..3b332fd 100644 --- a/examples/util/meter.rs +++ b/examples/util/meter.rs @@ -19,7 +19,7 @@ impl PeakMeter { Self { output: Arc::new(AtomicF32::new(0.)), last_out: 0., - decay, + decay: decay.max(1e-6), dt: 0.0, } } @@ -57,7 +57,7 @@ impl PeakMeter { .flat_map(|ch| buffer[ch].iter().copied().max_by(f32::total_cmp)) .max_by(f32::total_cmp) .unwrap_or(0.); - self.last_out = peak_lin.max(self.last_out * f32::exp(-self.decay * buffer_duration)); + self.last_out = peak_lin.max(self.last_out * f32::exp(-buffer_duration / self.decay)); self.output.store(self.last_out, Ordering::Relaxed); self.last_out } diff --git a/examples/util/mod.rs b/examples/util/mod.rs index 63db0f0..f48e253 100644 --- a/examples/util/mod.rs +++ b/examples/util/mod.rs @@ -5,12 +5,13 @@ #![allow(unused)] use crate::util::meter::Metered; -use dialoguer::console::Term; +use dialoguer::console::{Style, Term}; use indicatif::{ProgressBar, ProgressStyle}; use interflow_core::stream; +use std::fmt::Write; use std::sync::atomic::{AtomicBool, AtomicU32, Ordering}; use std::sync::Arc; -use std::thread; +use std::{iter, thread}; pub mod meter; pub mod noop; @@ -76,18 +77,24 @@ pub fn normalize(min: f32, max: f32, value: f32) -> f32 { pub fn prepare_display( callback: C, ) -> (meter::Metered, impl Fn() -> anyhow::Result<()>) { - let meter = Metered::new(callback, 0.5); + let meter = Metered::new(callback, 100e-3); let shared = meter.shared(); + let meter_display = make_meter_display(-60.0, 6.0, 10); let display = move || { let term = Term::stdout(); loop { let elapsed = shared.timestamp.as_timestamp().as_seconds(); + let inp = shared.input.load(Ordering::Relaxed); let inp = 20.0 * inp.log10(); + let inp_display = meter_display(inp); + let out = shared.output.load(Ordering::Relaxed); let out = 20.0 * out.log10(); + let out_display = meter_display(out); + term.write_str(&format!( - "Elapsed: {elapsed:3.2} s\tInput: {inp:2.1} dB\tOutput: {out:2.1} dB" + "Elapsed: {elapsed:3.2} s\tInput: {inp_display} {inp:2.1} dB\tOutput: {out_display} {out:2.1} dB" ))?; std::thread::sleep(std::time::Duration::from_millis(50)); term.clear_line()?; @@ -95,3 +102,37 @@ pub fn prepare_display( }; (meter, display) } + +fn make_meter_display(min: f32, max: f32, initial_size: usize) -> impl Fn(f32) -> String { + const WIDTH: usize = 2; + let size = WIDTH * initial_size; + let fsize = size as f32; + let position = + move |v: f32| f32::round(fsize * normalize(min, max, v).clamp(0.0, 1.0)) as usize; + let fullscale_pos = position(0.0); + let style_below = Style::new().green(); + let style_above = Style::new().red(); + let style_background = Style::new().black().bright(); + + move |v: f32| { + let pos = position(v); + let mut s = String::new(); + for i in 0..initial_size { + let start = WIDTH * i; + let end = start + WIDTH; + let style = if end < fullscale_pos { + &style_below + } else { + &style_above + }; + if end < pos { + write!(s, "{}", style.apply_to('⣿')); + } else if start + 1 == pos { + write!(s, "{}", style.apply_to('⡇')); + } else { + write!(s, "{}", style_background.apply_to(' ')); + } + } + s + } +} From 37020dcc02bb9f6d7853deeba317f92dd6f3f795 Mon Sep 17 00:00:00 2001 From: Nathan Graule Date: Fri, 24 Jul 2026 15:18:19 +0200 Subject: [PATCH 33/38] feat: null backend which passes silence to input and discards output --- Cargo.lock | 1 + Cargo.toml | 1 + crates/interflow-core/src/buffer.rs | 30 ++-- crates/interflow-core/src/device.rs | 5 +- crates/interflow-core/src/stream.rs | 23 ++- crates/interflow-coreaudio/src/device.rs | 2 +- crates/interflow-coreaudio/src/stream.rs | 17 +-- src/backends/mod.rs | 2 + src/backends/null/mod.rs | 170 +++++++++++++++++++++++ src/lib.rs | 4 +- 10 files changed, 229 insertions(+), 26 deletions(-) create mode 100644 src/backends/null/mod.rs diff --git a/Cargo.lock b/Cargo.lock index b9a08aa..99746c5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -313,6 +313,7 @@ dependencies = [ "interflow-core", "interflow-coreaudio", "interflow-wasapi", + "log", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index ab8be5b..62eec33 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,6 +28,7 @@ license.workspace = true [dependencies] anyhow.workspace = true interflow-core.workspace = true +log.workspace = true [dev-dependencies] anyhow.workspace = true diff --git a/crates/interflow-core/src/buffer.rs b/crates/interflow-core/src/buffer.rs index 8d6383b..4693608 100644 --- a/crates/interflow-core/src/buffer.rs +++ b/crates/interflow-core/src/buffer.rs @@ -40,8 +40,8 @@ where [T]: FromZeros, { /// Creates a new buffer with the given number of frames and channels. The audio buffer will be zeroed out. - pub fn zeroed(frames: NonZeroUsize, channels: NonZeroUsize) -> Self { - let len = frames.get() * channels.get(); + pub fn zeroed(frames: NonZeroUsize, channels: usize) -> Self { + let len = frames.get() * channels; AudioBuffer { data: <[T] as FromZeros>::new_box_zeroed_with_elems(len).unwrap(), frames, @@ -119,6 +119,18 @@ pub enum FromDataError { } impl AudioBuffer { + /// Create an empty buffer that reports the given number of frames. No allocations are done since the slice is + /// 0-sized, as guaranteed by `Box::new`. + pub fn empty(frames: NonZeroUsize) -> Self { + const _NO_ALLOCATIONS: () = { + assert!(0 == size_of::<[f32; 0]>()); + }; + + Self { + data: Box::new([]), + frames, + } + } pub fn from_fn( channels: NonZeroUsize, frames: NonZeroUsize, @@ -136,28 +148,26 @@ impl AudioBuffer { } } - pub fn from_data_channels( - data: Box<[T]>, - channels: NonZeroUsize, - ) -> Result { + pub fn from_data_channels(data: Box<[T]>, channels: usize) -> Result { if data.is_empty() { return Err(FromDataError::Empty); } - if data.len() % channels.get() != 0 { + if data.len() % channels != 0 { return Err(FromDataError::InvalidChannelCount { len: data.len(), - channels: channels.get(), + channels, }); } - let frames = NonZeroUsize::new(data.len() / channels.get()).unwrap(); + let frames = NonZeroUsize::new(data.len() / channels).unwrap(); Ok(AudioBuffer { data, frames }) } pub fn from_data_frames(data: Box<[T]>, frames: NonZeroUsize) -> Result { if data.is_empty() { - return Err(FromDataError::Empty); + return Ok(Self::empty(frames)); } + if data.len() % frames.get() != 0 { return Err(FromDataError::InvalidFrameCount { len: data.len(), diff --git a/crates/interflow-core/src/device.rs b/crates/interflow-core/src/device.rs index 6792264..065f3a7 100644 --- a/crates/interflow-core/src/device.rs +++ b/crates/interflow-core/src/device.rs @@ -67,7 +67,10 @@ pub struct ResolvedStreamConfig { /// time natively. pub trait Device: ExtensionProvider { type Error: Send + Sync + 'static + std::error::Error; - type StreamHandle: StreamHandle>; + type StreamHandle: StreamHandle< + Callback, + Error: Into, + >; fn name(&self) -> Cow<'_, str>; diff --git a/crates/interflow-core/src/stream.rs b/crates/interflow-core/src/stream.rs index d3c94b1..727cb59 100644 --- a/crates/interflow-core/src/stream.rs +++ b/crates/interflow-core/src/stream.rs @@ -1,5 +1,6 @@ use crate::buffer::{AudioMut, AudioRef}; use crate::device::ResolvedStreamConfig; +use crate::stream; use crate::timing::Timestamp; use crate::traits::ExtensionProvider; use bitflags::bitflags; @@ -41,6 +42,7 @@ pub struct name<'a, T> { /// Plain-old-data object holding the passed-in stream configuration, as well as a general /// callback timestamp, which can be different from the input and output streams in case of /// cross-stream latencies; differences in timing can indicate desync. +#[derive(Copy, Clone)] pub struct CallbackContext<'a> { /// Passed-in stream configuration. Values have been updated where necessary to correspond to /// the actual stream properties. @@ -51,7 +53,7 @@ pub struct CallbackContext<'a> { } /// Trait for types which handles an audio stream (input or output). -pub trait StreamHandle { +pub trait StreamHandle { /// Type of errors which have caused the stream to fail. type Error: Send + std::error::Error; @@ -78,7 +80,24 @@ pub trait Callback: Send { ); } -impl, &mut AudioOutput)> Callback for F { +impl Callback for Box { + fn prepare(&mut self, context: CallbackContext) { + Callback::prepare(&mut **self, context) + } + + fn process_audio( + &mut self, + context: CallbackContext, + input: &AudioInput, + output: &mut AudioOutput, + ) { + Callback::process_audio(&mut **self, context, input, output) + } +} + +impl, &mut AudioOutput)> Callback + for &mut F +{ fn prepare(&mut self, _: CallbackContext) {} fn process_audio( diff --git a/crates/interflow-coreaudio/src/device.rs b/crates/interflow-coreaudio/src/device.rs index 79c94c8..5cf71ee 100644 --- a/crates/interflow-coreaudio/src/device.rs +++ b/crates/interflow-coreaudio/src/device.rs @@ -253,7 +253,7 @@ impl ExtensionProvider for Device { impl device::Device for Device { type Error = Error; - type StreamHandle = Handle; + type StreamHandle = Handle; fn name(&self) -> Cow<'_, str> { self.request.name() diff --git a/crates/interflow-coreaudio/src/stream.rs b/crates/interflow-coreaudio/src/stream.rs index fce2267..07b3e80 100644 --- a/crates/interflow-coreaudio/src/stream.rs +++ b/crates/interflow-coreaudio/src/stream.rs @@ -20,7 +20,7 @@ pub struct Handle { callback_retrieve: oneshot::Sender>, } -impl stream::StreamHandle for Handle { +impl stream::StreamHandle for Handle { type Error = Infallible; fn eject(mut self) -> Result { @@ -117,7 +117,7 @@ impl Handle { }; let mut buffer = AudioBuffer::zeroed( NonZeroUsize::new(frame_count as _).unwrap(), - NonZeroUsize::new(asbd.mChannelsPerFrame as _).unwrap(), + asbd.mChannelsPerFrame as _, ); let (tx, rx) = oneshot::channel::>(); @@ -157,10 +157,7 @@ impl Handle { channel_flags: &[], }; - let mut dummy_buf = AudioBuffer::zeroed( - NonZeroUsize::new(1).unwrap(), - NonZeroUsize::new(channels as _).unwrap(), - ); + let mut dummy_buf = AudioBuffer::zeroed(NonZeroUsize::new(1).unwrap(), channels as _); let mut dummy_output = AudioOutput { buffer: dummy_buf.as_mut(), timestamp: Timestamp::new(asbd.mSampleRate), @@ -213,7 +210,7 @@ impl Handle { }; let mut buffer = AudioBuffer::zeroed( NonZeroUsize::new(frame_size as _).unwrap(), - NonZeroUsize::new(asbd.mChannelsPerFrame as _).unwrap(), + asbd.mChannelsPerFrame as _, ); let (tx, rx) = oneshot::channel::>(); @@ -234,10 +231,8 @@ impl Handle { args.time_stamp.mSampleTime as _, ); - let dummy_buf = AudioBuffer::zeroed( - NonZeroUsize::new(1).unwrap(), - NonZeroUsize::new(asbd.mChannelsPerFrame as _).unwrap(), - ); + let dummy_buf = + AudioBuffer::zeroed(NonZeroUsize::new(1).unwrap(), asbd.mChannelsPerFrame as _); let dummy_input = AudioInput { buffer: dummy_buf.as_ref(), timestamp: Timestamp::new(resolved_config.sample_rate), diff --git a/src/backends/mod.rs b/src/backends/mod.rs index d4d53b6..3d28778 100644 --- a/src/backends/mod.rs +++ b/src/backends/mod.rs @@ -5,6 +5,8 @@ //! Each backend is provided in its own submodule. Types should be public so that the user isn't //! limited to going through the main API if they want to choose a specific backend. +pub mod null; + #[cfg(any(target_os = "macos", target_os = "ios"))] pub use interflow_coreaudio as coreaudio; diff --git a/src/backends/null/mod.rs b/src/backends/null/mod.rs new file mode 100644 index 0000000..2812975 --- /dev/null +++ b/src/backends/null/mod.rs @@ -0,0 +1,170 @@ +//! Null backend. Accepts any stream configuration, does nothing. + +use crate::prelude::*; +use interflow_core::device::{ResolvedStreamConfig, StreamConfig}; +use interflow_core::stream::CallbackContext; +use std::borrow::Cow; +use std::convert::Infallible; +use std::num::NonZeroUsize; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; +use std::thread::JoinHandle; +use std::time::Instant; + +/// Platform type for the null backend. +pub struct Platform; + +impl ExtensionProvider for Platform { + fn register<'a, 'sel>(&'a self, selector: &'sel mut Selector<'a>) -> &'sel mut Selector<'a> { + selector + } +} + +impl platform::Platform for Platform { + type Error = Infallible; + type Device = Device; + const NAME: &'static str = "Null"; + + fn default_device(&self, device_type: DeviceType) -> Result { + Ok(Device(device_type)) + } + + fn list_devices(&self) -> Result, Self::Error> { + Ok([Device(DeviceType::OUTPUT), Device(DeviceType::INPUT)]) + } +} + +#[derive(Debug, Clone, Copy)] +pub struct Device(pub DeviceType); + +impl ExtensionProvider for Device { + fn register<'a, 'sel>(&'a self, selector: &'sel mut Selector<'a>) -> &'sel mut Selector<'a> { + selector + } +} + +impl device::Device for Device { + type Error = Infallible; + type StreamHandle = Stream; + + fn name(&self) -> Cow<'_, str> { + Cow::Borrowed("Null device") + } + + fn device_type(&self) -> DeviceType { + self.0 + } + + fn default_config(&self) -> Result { + Ok(StreamConfig { + exclusive: true, + sample_rate: 48000.0, + input_channels: 2, + output_channels: 2, + buffer_size_range: (None, None), + }) + } + + fn is_config_supported(&self, _: &StreamConfig) -> bool { + true + } + + fn create_stream( + &self, + stream_config: StreamConfig, + callback: Callback, + ) -> Result, Self::Error> { + Ok(Stream::new(stream_config, callback)) + } +} + +pub struct Stream { + thread_handle: JoinHandle, + abort_signal: Arc, +} + +impl ExtensionProvider for Stream { + fn register<'a, 'sel>(&'a self, selector: &'sel mut Selector<'a>) -> &'sel mut Selector<'a> { + selector + } +} + +impl StreamHandle for Stream { + type Error = Infallible; + + fn eject(self) -> Result { + self.abort_signal.store(true, Ordering::Relaxed); + Ok(self.thread_handle.join().unwrap()) + } +} + +impl Stream { + fn new(stream_config: StreamConfig, mut callback: Callback) -> Self { + let abort_signal = Arc::new(AtomicBool::new(false)); + let thread_handle = std::thread::spawn({ + let abort_signal = abort_signal.clone(); + let frame_count = NonZeroUsize::new( + stream_config + .buffer_size_range + .1 + .or(stream_config.buffer_size_range.0) + .unwrap_or(512) + .max(1), + ) + .unwrap(); + let input_buffers = AudioBuffer::zeroed(frame_count, stream_config.input_channels); + let mut output_buffers = + AudioBuffer::zeroed(frame_count, stream_config.output_channels); + move || { + let stream_config = ResolvedStreamConfig { + sample_rate: stream_config.sample_rate, + input_channels: stream_config.input_channels, + output_channels: stream_config.output_channels, + max_frame_count: frame_count.get(), + }; + let mut context = CallbackContext { + stream_config: &stream_config, + timestamp: Timestamp::new(stream_config.sample_rate), + stream_proxy: &NullStreamProxy, + }; + callback.prepare(context); + let start = Instant::now(); + loop { + if abort_signal.load(Ordering::Relaxed) { + break callback; + } + + let audio_input = AudioInput { + timestamp: context.timestamp, + buffer: input_buffers.as_ref(), + channel_flags: &[], + }; + let mut audio_output = AudioOutput { + timestamp: context.timestamp, + buffer: output_buffers.as_mut(), + channel_flags: &[], + }; + callback.process_audio(context, &audio_input, &mut audio_output); + context.timestamp += stream_config.max_frame_count as u64; + while context.timestamp.as_duration() > start.elapsed() { + std::thread::yield_now(); + } + } + } + }); + Self { + thread_handle, + abort_signal, + } + } +} + +struct NullStreamProxy; + +impl ExtensionProvider for NullStreamProxy { + fn register<'a, 'sel>(&'a self, selector: &'sel mut Selector<'a>) -> &'sel mut Selector<'a> { + selector + } +} + +impl StreamProxy for NullStreamProxy {} diff --git a/src/lib.rs b/src/lib.rs index cccc402..e0d705b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -23,7 +23,9 @@ pub mod prelude { pub fn default_platform() -> core::proxies::DynPlatform { #[cfg(any(target_os = "macos", target_os = "ios"))] return Rc::new(interflow_coreaudio::platform::Platform); - todo!("null backend") + log::warn!("No available platform found, falling back to null output"); + // Fallback to null output + Rc::new(backends::null::Platform) } /// Return the default device, using the default platform as returned by [`default_platform`]. From 2a11e2152a0bfbc0f0a228f7ce1a1f09ee0be664 Mon Sep 17 00:00:00 2001 From: Nathan Graule Date: Fri, 24 Jul 2026 15:46:21 +0200 Subject: [PATCH 34/38] feat: implement registration of available platforms --- Cargo.lock | 90 ++++++++++++++++++++++ Cargo.toml | 6 +- crates/interflow-core/Cargo.toml | 5 ++ crates/interflow-core/src/collect.rs | 32 ++++++++ crates/interflow-core/src/lib.rs | 2 + crates/interflow-coreaudio/Cargo.toml | 4 + crates/interflow-coreaudio/src/platform.rs | 10 ++- src/backends/null/mod.rs | 11 +++ src/lib.rs | 17 ++-- 9 files changed, 166 insertions(+), 11 deletions(-) create mode 100644 crates/interflow-core/src/collect.rs diff --git a/Cargo.lock b/Cargo.lock index 99746c5..2936f17 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -97,6 +97,12 @@ version = "3.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" +[[package]] +name = "bytemuck" +version = "1.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797" + [[package]] name = "cexpr" version = "0.6.0" @@ -171,6 +177,16 @@ dependencies = [ "bindgen", ] +[[package]] +name = "ctor" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2e30e509674ef0ec91e21a7735766db37d163d46151b6a361d8b83dd79116bd" +dependencies = [ + "link-section", + "linktime-proc-macro", +] + [[package]] name = "dialoguer" version = "0.12.0" @@ -194,6 +210,12 @@ dependencies = [ "objc2", ] +[[package]] +name = "dtor" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d738e43aa64edab57c983d56de890d65fea7dc05605490c74451ce721dfd84b" + [[package]] name = "duplicate" version = "2.0.1" @@ -314,6 +336,7 @@ dependencies = [ "interflow-coreaudio", "interflow-wasapi", "log", + "scattered-collect", ] [[package]] @@ -323,6 +346,7 @@ dependencies = [ "anyhow", "bitflags", "duplicate", + "scattered-collect", "thiserror", "zerocopy", ] @@ -336,6 +360,7 @@ dependencies = [ "interflow-core", "log", "oneshot", + "scattered-collect", "thiserror", ] @@ -416,6 +441,32 @@ dependencies = [ "windows-link", ] +[[package]] +name = "link-section" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8dc98458dfe90986c5e2f6ddcf68360c7e5c4252600153e06aa4ee8176c0f8d1" +dependencies = [ + "linktime-proc-macro", +] + +[[package]] +name = "linktime" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "747e3b77d4686c927ec2052a2dcc3943e734a31b279c8f12b4332357ad9be5c8" +dependencies = [ + "ctor", + "dtor", + "link-section", +] + +[[package]] +name = "linktime-proc-macro" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c7b0a3383c2a1002d11349c92c85a666a5fb679e96c79d782cf0dbe557fd6ee" + [[package]] name = "linux-raw-sys" version = "0.12.1" @@ -645,6 +696,29 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +[[package]] +name = "safe_arch" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a52ec151f024d703f9fd65abb7cbe81e7cdb39f18917a3a37e3014470dc7c59" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "scattered-collect" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bdd12e1c322169b156f50601d3171a34980821744f0281bfb07b90c642c5686" +dependencies = [ + "ctor", + "link-section", + "linktime", + "linktime-proc-macro", + "wide", + "xxhash-rust", +] + [[package]] name = "serde_core" version = "1.0.228" @@ -815,6 +889,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "wide" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfdfe6a32973f2d1b268b8895845a8a96cac2f0191e72c27cc929036060dbf89" +dependencies = [ + "bytemuck", + "safe_arch", +] + [[package]] name = "windows" version = "0.62.2" @@ -934,6 +1018,12 @@ dependencies = [ "windows-link", ] +[[package]] +name = "xxhash-rust" +version = "0.8.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aee1b19627c7c60102ab80d3a9cbe18de90bfe03bfa6c3715447681f0e8c8af6" + [[package]] name = "zerocopy" version = "0.8.39" diff --git a/Cargo.toml b/Cargo.toml index 62eec33..3d5d78c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,7 @@ duplicate = "2.0.1" interflow-core = { path = "crates/interflow-core" } interflow-coreaudio = { path = "crates/interflow-coreaudio" } log = "0.4.29" +scattered-collect = "0.21.3" thiserror = "2.0.18" [package] @@ -27,7 +28,8 @@ license.workspace = true [dependencies] anyhow.workspace = true -interflow-core.workspace = true +interflow-core = { workspace = true, features = ["collect"] } +scattered-collect.workspace = true log.workspace = true [dev-dependencies] @@ -40,7 +42,7 @@ indicatif = "0.18.3" cfg_aliases = "0.2.1" [target.'cfg(any(target_os = "macos", target_os = "ios"))'.dependencies] -interflow-coreaudio.workspace = true +interflow-coreaudio = { workspace = true, features = ["collect"] } # [target.'cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "netbsd"))'.dependencies] # alsa = "0.11.0" diff --git a/crates/interflow-core/Cargo.toml b/crates/interflow-core/Cargo.toml index 1b87f50..584316e 100644 --- a/crates/interflow-core/Cargo.toml +++ b/crates/interflow-core/Cargo.toml @@ -10,4 +10,9 @@ anyhow.workspace = true bitflags.workspace = true duplicate.workspace = true thiserror.workspace = true + +scattered-collect = { version = "0.21.3", optional = true } zerocopy = { version = "0.8.39", features = ["alloc"] } + +[features] +collect = ["dep:scattered-collect"] diff --git a/crates/interflow-core/src/collect.rs b/crates/interflow-core/src/collect.rs new file mode 100644 index 0000000..36fdfcd --- /dev/null +++ b/crates/interflow-core/src/collect.rs @@ -0,0 +1,32 @@ +use crate::proxies::DynPlatform; +use scattered_collect::{gather, ScatteredSlice}; + +pub type PlatformConstructor = fn() -> Option; + +/// Registration type for platforms to be used in `interflow::default_platform`. +pub struct Registration { + pub constructor: PlatformConstructor, + pub priority: i32, +} + +/// Registrar. Do not use directly, instead call [`get_registry`] which returns a sorted list of constructors. +#[gather] +pub static REGISTRAR: ScatteredSlice; + +/// Returns a sorted list of constructors to try in sequence. +pub fn get_registry() -> impl Iterator { + let mut collected = Vec::from_iter(REGISTRAR.iter()); + collected.sort_by_key(|reg| -reg.priority); + collected.into_iter().map(|reg| reg.constructor) +} + +pub fn construct_platform() -> Option { + for registration in get_registry() { + if let Some(platform) = registration() { + return Some(platform); + } + } + None +} + +pub const DEFAULT_PLATFORM_PRIORITY: i32 = 100; diff --git a/crates/interflow-core/src/lib.rs b/crates/interflow-core/src/lib.rs index 8897e1d..68e4eb7 100644 --- a/crates/interflow-core/src/lib.rs +++ b/crates/interflow-core/src/lib.rs @@ -1,6 +1,8 @@ use bitflags::bitflags; pub mod buffer; +#[cfg(feature = "collect")] +pub mod collect; pub mod device; pub mod platform; pub mod proxies; diff --git a/crates/interflow-coreaudio/Cargo.toml b/crates/interflow-coreaudio/Cargo.toml index 17626f6..125a0d7 100644 --- a/crates/interflow-coreaudio/Cargo.toml +++ b/crates/interflow-coreaudio/Cargo.toml @@ -13,3 +13,7 @@ coreaudio-sys = "0.2.17" log.workspace = true thiserror.workspace = true oneshot = { version = "0.2.1", features = ["std"] } +scattered-collect = { workspace = true, optional = true } + +[features] +collect = ["interflow-core/collect", "dep:scattered-collect"] diff --git a/crates/interflow-coreaudio/src/platform.rs b/crates/interflow-coreaudio/src/platform.rs index d09318b..28e960d 100644 --- a/crates/interflow-coreaudio/src/platform.rs +++ b/crates/interflow-coreaudio/src/platform.rs @@ -3,7 +3,8 @@ use crate::device::Device; use crate::Error; use coreaudio::audio_unit::macos_helpers::{get_audio_device_ids, get_default_device_id}; use interflow_core::traits::{ExtensionProvider, Selector}; -use interflow_core::{platform, DeviceType}; +use interflow_core::{collect, platform, DeviceType}; +use std::rc::Rc; /// The CoreAudio driver. pub struct Platform; @@ -35,3 +36,10 @@ impl platform::Platform for Platform { .chain(get_audio_device_ids()?.into_iter().map(Device::from_id))) } } + +#[cfg(feature = "collect")] +#[scattered_collect::scatter(collect::REGISTRAR)] +static COREAUDIO_PLATFORM_REGISTRATION: collect::Registration = collect::Registration { + constructor: || Some(Rc::new(Platform)), + priority: collect::DEFAULT_PLATFORM_PRIORITY, +}; diff --git a/src/backends/null/mod.rs b/src/backends/null/mod.rs index 2812975..c485118 100644 --- a/src/backends/null/mod.rs +++ b/src/backends/null/mod.rs @@ -1,11 +1,13 @@ //! Null backend. Accepts any stream configuration, does nothing. use crate::prelude::*; +use interflow_core::collect; use interflow_core::device::{ResolvedStreamConfig, StreamConfig}; use interflow_core::stream::CallbackContext; use std::borrow::Cow; use std::convert::Infallible; use std::num::NonZeroUsize; +use std::rc::Rc; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; use std::thread::JoinHandle; @@ -168,3 +170,12 @@ impl ExtensionProvider for NullStreamProxy { } impl StreamProxy for NullStreamProxy {} + +#[scattered_collect::scatter(collect::REGISTRAR)] +static NULL_PLATFORM_REGISTRATION: collect::Registration = collect::Registration { + constructor: || { + log::error!("No platforms available, using null backend (you will not hear any sound)"); + Some(Rc::new(Platform)) + }, + priority: i32::MIN + 1, +}; diff --git a/src/lib.rs b/src/lib.rs index e0d705b..4af5554 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,8 +1,7 @@ #![doc = include_str!("../README.md")] #![warn(missing_docs)] -use std::rc::Rc; - +use anyhow::Context; use core::{stream, DeviceType}; pub use interflow_core as core; use interflow_core::proxies::CreateStreamExt; @@ -21,16 +20,18 @@ pub mod prelude { /// The platform is selected automatically based on your available and enabled backends. #[allow(unreachable_code)] pub fn default_platform() -> core::proxies::DynPlatform { - #[cfg(any(target_os = "macos", target_os = "ios"))] - return Rc::new(interflow_coreaudio::platform::Platform); - log::warn!("No available platform found, falling back to null output"); - // Fallback to null output - Rc::new(backends::null::Platform) + let platform = core::collect::construct_platform().expect("FATAL: no platforms available"); + log::info!("Using platform {}", platform.name()); + platform } /// Return the default device, using the default platform as returned by [`default_platform`]. pub fn default_device(device_type: DeviceType) -> anyhow::Result { - default_platform().default_device(device_type) + let device = default_platform() + .default_device(device_type) + .context("Cannot get default device")?; + log::info!("Using device {}", device.name()); + Ok(device) } /// Create a stream using the default device as returned by [`default_device`]. From b17d61414ed99076de57e7ed479c559a8b29885c Mon Sep 17 00:00:00 2001 From: Nathan Graule Date: Sat, 25 Jul 2026 15:53:16 +0200 Subject: [PATCH 35/38] fix: slice and change_amplitude use the wrong bound calculation --- crates/interflow-core/src/buffer.rs | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/crates/interflow-core/src/buffer.rs b/crates/interflow-core/src/buffer.rs index 4693608..30be040 100644 --- a/crates/interflow-core/src/buffer.rs +++ b/crates/interflow-core/src/buffer.rs @@ -231,7 +231,7 @@ impl AudioBuffer { ops::Bound::Unbounded => 0, }; let end = match index.end_bound() { - ops::Bound::Included(i) => *i - 1, + ops::Bound::Included(i) => *i + 1, ops::Bound::Excluded(i) => *i, ops::Bound::Unbounded => self.frames(), }; @@ -239,7 +239,7 @@ impl AudioBuffer { debug_assert!(end <= self.frames()); out { buffer: self, - frame_slice: (begin, end + 1), + frame_slice: (begin, end), } } @@ -454,10 +454,14 @@ impl AudioMut<'_, T> { T: Copy + ops::MulAssign, { let num_channels = self.channels(); - for frame in &mut self.buffer.data - [num_channels * self.frame_slice.0..num_channels * self.frame_slice.1] - { - *frame *= factor; + let start = self.frame_slice.0; + let end = self.frame_slice.1; + let hop = self.buffer.frames(); + for ch in 0..num_channels { + let offset = hop * ch; + for frame in &mut self.buffer.data[offset + start..offset + end] { + *frame *= factor; + } } } From 3d2dfdea7021fbaa6b2c6e9acbdd43f3f6b455c7 Mon Sep 17 00:00:00 2001 From: Nathan Graule Date: Sat, 25 Jul 2026 15:55:38 +0200 Subject: [PATCH 36/38] fix(wasapi): backend now works properly --- Cargo.lock | 2 + Cargo.toml | 2 +- crates/interflow-wasapi/Cargo.toml | 5 + crates/interflow-wasapi/src/device.rs | 82 ++- crates/interflow-wasapi/src/lib.rs | 153 +---- crates/interflow-wasapi/src/platform.rs | 138 +++++ crates/interflow-wasapi/src/stream.rs | 368 +++++++++++- crates/interflow-wasapi/src/util.rs | 27 +- examples/ext-provider-lookup.rs | 1 - src/backends/wasapi/stream.rs | 731 ------------------------ src/lib.rs | 2 + 11 files changed, 600 insertions(+), 911 deletions(-) create mode 100644 crates/interflow-wasapi/src/platform.rs delete mode 100644 src/backends/wasapi/stream.rs diff --git a/Cargo.lock b/Cargo.lock index 2936f17..081f108 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -371,6 +371,8 @@ dependencies = [ "bitflags", "duplicate", "interflow-core", + "log", + "scattered-collect", "thiserror", "windows", "zerocopy", diff --git a/Cargo.toml b/Cargo.toml index 3d5d78c..3d60b4a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -53,4 +53,4 @@ interflow-coreaudio = { workspace = true, features = ["collect"] } # pipewire = { version = "0.8.0", optional = true, features = ["v0_3_45"] } [target.'cfg(target_os = "windows")'.dependencies] -interflow-wasapi = { path = "crates/interflow-wasapi" } +interflow-wasapi = { path = "crates/interflow-wasapi", features = ["collect"] } diff --git a/crates/interflow-wasapi/Cargo.toml b/crates/interflow-wasapi/Cargo.toml index 8b8f599..c289fa2 100644 --- a/crates/interflow-wasapi/Cargo.toml +++ b/crates/interflow-wasapi/Cargo.toml @@ -5,11 +5,16 @@ edition.workspace = true rust-version.workspace = true license.workspace = true +[features] +collect = ["interflow-core/collect", "dep:scattered-collect"] + [dependencies] bitflags.workspace = true duplicate.workspace = true interflow-core.workspace = true +scattered-collect = { workspace = true, optional = true } thiserror.workspace = true +log.workspace = true windows = { version = "0.62.2", features = [ "Win32_Media_Audio", "Win32_Foundation", diff --git a/crates/interflow-wasapi/src/device.rs b/crates/interflow-wasapi/src/device.rs index f5f3a37..fcdbc6b 100644 --- a/crates/interflow-wasapi/src/device.rs +++ b/crates/interflow-wasapi/src/device.rs @@ -1,11 +1,13 @@ use crate::util::MMDevice; -use interflow_core::device::{ResolvedStreamConfig, StreamConfig}; -use interflow_core::stream; +use crate::Error; +use interflow_core::device::StreamConfig; use interflow_core::traits::{ExtensionProvider, Selector}; -use interflow_core::{device, DeviceType}; +use interflow_core::{device, stream, DeviceType}; use std::borrow::Cow; +use std::ptr::NonNull; use windows::Win32::Media::Audio; use windows::Win32::Media::Audio::{IAudioClient, IAudioClient3}; +use windows::Win32::Media::Multimedia; #[derive(Debug, Clone)] pub struct Device { @@ -20,8 +22,8 @@ impl ExtensionProvider for Device { } impl device::Device for Device { - type Error = crate::Error; - type StreamHandle = (); + type Error = Error; + type StreamHandle = crate::stream::Handle; fn name(&self) -> Cow<'_, str> { Cow::Owned(self.handle.name()) @@ -37,7 +39,7 @@ impl device::Device for Device { } fn is_config_supported(&self, config: &StreamConfig) -> bool { - todo!() + self.check_format(config).unwrap_or(false) } fn create_stream( @@ -45,13 +47,17 @@ impl device::Device for Device { stream_config: StreamConfig, callback: Callback, ) -> Result, Self::Error> { - todo!() + crate::stream::Handle::new(self, stream_config, callback) } } impl Device { - fn get_mix_format(&self) -> Result { - let client = self.handle.activate::()?; + pub(crate) fn activate_audio_client(&self) -> Result { + self.handle.activate::() + } + + fn get_mix_format(&self) -> Result { + let client = self.activate_audio_client()?; let mix_format = unsafe { client.GetMixFormat() }?; let format = unsafe { mix_format.read_unaligned() }; let channels = format.nChannels as usize; @@ -74,7 +80,7 @@ impl Device { }) } - fn get_mix_format_iac3(&self) -> Result { + fn get_mix_format_iac3(&self) -> Result { let client = self.handle.activate::()?; let mut period_default = 0u32; let mut period_min = 0u32; @@ -110,9 +116,59 @@ impl Device { exclusive: false, }) } + + fn check_format(&self, config: &StreamConfig) -> Result { + unsafe { + let audio_client = self.activate_audio_client()?; + let sharemode = if config.exclusive { + Audio::AUDCLNT_SHAREMODE_EXCLUSIVE + } else { + Audio::AUDCLNT_SHAREMODE_SHARED + }; + let format = Self::build_format(config); + if config.exclusive { + audio_client + .IsFormatSupported(sharemode, &format, None) + .ok()?; + return Ok(true); + } + let mut closest_ptr: *mut Audio::WAVEFORMATEX = std::ptr::null_mut(); + let result = audio_client + .IsFormatSupported(sharemode, &format, Some(&mut closest_ptr)); + let hr = result.0; + if hr == 0 { + return Ok(true); + } + if hr > 0 && !closest_ptr.is_null() { + let closest = + crate::util::CoTask::new(NonNull::new_unchecked(closest_ptr)); + let closest_format = closest.as_ptr().read_unaligned(); + return Ok(closest_format.nSamplesPerSec == config.sample_rate as u32 + && closest_format.nChannels as usize + == config.output_channels.max(config.input_channels)); + } + Ok(false) + } + } + + pub(crate) fn build_format(config: &StreamConfig) -> Audio::WAVEFORMATEX { + let channels = (config.input_channels.max(config.output_channels)) as u16; + let sample_rate = config.sample_rate as u32; + let sample_bytes = size_of::() as u16; + let avg_bytes_per_sec = channels as u32 * sample_rate * sample_bytes as u32; + let block_align = channels * sample_bytes; + Audio::WAVEFORMATEX { + wFormatTag: Multimedia::WAVE_FORMAT_IEEE_FLOAT as u16, + nChannels: channels, + nSamplesPerSec: sample_rate, + nAvgBytesPerSec: avg_bytes_per_sec, + nBlockAlign: block_align, + wBitsPerSample: 8 * sample_bytes, + cbSize: 0, + } + } } -/// An iterable collection WASAPI devices. pub(crate) struct DeviceList { pub(crate) collection: Audio::IMMDeviceCollection, pub(crate) total_count: u32, @@ -121,7 +177,6 @@ pub(crate) struct DeviceList { } unsafe impl Send for DeviceList {} - unsafe impl Sync for DeviceList {} impl Iterator for DeviceList { @@ -131,9 +186,8 @@ impl Iterator for DeviceList { if self.next_item >= self.total_count { return None; } - unsafe { - let device = self.collection.Item(self.next_item).unwrap(); + let device = self.collection.Item(self.next_item).ok()?; self.next_item += 1; Some(Device { handle: MMDevice::new(device), diff --git a/crates/interflow-wasapi/src/lib.rs b/crates/interflow-wasapi/src/lib.rs index 94dfeb7..b176865 100644 --- a/crates/interflow-wasapi/src/lib.rs +++ b/crates/interflow-wasapi/src/lib.rs @@ -1,159 +1,30 @@ pub mod device; -mod stream; +pub mod platform; +pub mod stream; mod util; -use crate::util::MMDevice; -use bitflags::bitflags_match; -use device::Device; -use interflow_core::traits::{ExtensionProvider, Selector}; -use interflow_core::{platform, DeviceType}; -use std::sync::OnceLock; -use windows::Win32::Media::Audio; -use windows::Win32::System::Com; +pub mod prelude { + pub use crate::device::Device; + pub use crate::platform::Platform; + pub use crate::stream::Handle; +} + +use std::convert::Infallible; #[derive(Debug, thiserror::Error)] pub enum Error { - /// Error originating from WASAPI. #[error("{} (code {})", .0.message(), .0.code())] BackendError(#[from] windows::core::Error), - /// Requested WASAPI device configuration is not available #[error("Configuration not available")] ConfigurationNotAvailable, - /// Windows Foundation error #[error("Win32 error: {0}")] FoundationError(String), - /// Duplex stream requested, unsupported by WASAPI #[error("Unsupported duplex stream requested")] DuplexStreamRequested, } -#[derive(Debug, Copy, Clone)] -pub struct Platform; - -impl ExtensionProvider for Platform { - fn register<'a, 'sel>(&'a self, selector: &'sel mut Selector<'a>) -> &'sel mut Selector<'a> { - selector.register::(self) - } -} - -impl platform::Platform for Platform { - type Error = Error; - type Device = Device; - const NAME: &'static str = ""; - - fn default_device(device_type: DeviceType) -> Result { - let Some(device) = audio_device_enumerator().get_default_device(device_type)? else { - return Err(Error::ConfigurationNotAvailable); - }; - Ok(device) - } - - fn list_devices(&self) -> Result, Self::Error> { - audio_device_enumerator().get_device_list() - } -} - -pub trait DefaultByRole { - fn default_by_role(&self, flow: Audio::EDataFlow, role: Audio::ERole) -> Result; -} - -impl DefaultByRole for Platform { - fn default_by_role(&self, flow: Audio::EDataFlow, role: Audio::ERole) -> Result { - audio_device_enumerator().get_default_device_with_role(flow, role) +impl From for Error { + fn from(_: Infallible) -> Self { + unreachable!() } } - -fn audio_device_enumerator() -> &'static AudioDeviceEnumerator { - static ENUMERATOR: OnceLock = OnceLock::new(); - ENUMERATOR.get_or_init(|| { - // Make sure COM is initialised. - let com = util::com().unwrap(); - - unsafe { - let enumerator = com - .create_instance::<_, Audio::IMMDeviceEnumerator>( - &Audio::MMDeviceEnumerator, - None, - Com::CLSCTX_ALL, - ) - .unwrap(); - - AudioDeviceEnumerator(enumerator) - } - }) -} - -/// Send/Sync wrapper around `IMMDeviceEnumerator`. -pub struct AudioDeviceEnumerator(Audio::IMMDeviceEnumerator); - -impl AudioDeviceEnumerator { - // Returns the default output device. - fn get_default_device(&self, device_type: DeviceType) -> Result, Error> { - let Some(flow) = bitflags_match!(device_type, { - DeviceType::INPUT | DeviceType::PHYSICAL => Some(Audio::eCapture), - DeviceType::OUTPUT | DeviceType::PHYSICAL => Some(Audio::eRender), - _ => None, - }) else { - return Ok(None); - }; - - self.get_default_device_with_role(flow, Audio::eConsole) - .map(Some) - } - - fn get_default_device_with_role( - &self, - flow: Audio::EDataFlow, - role: Audio::ERole, - ) -> Result { - unsafe { - let device = self.0.GetDefaultAudioEndpoint(flow, role)?; - let device_type = match flow { - Audio::eRender => DeviceType::OUTPUT, - _ => DeviceType::INPUT, - }; - Ok(Device { - handle: MMDevice::new(device), - device_type: DeviceType::PHYSICAL | device_type, - }) - } - } - - // Returns a chained iterator of output and input devices. - fn get_device_list(&self) -> Result, Error> { - // Create separate collections for output and input devices and then chain them. - unsafe { - let output_collection = self - .0 - .EnumAudioEndpoints(Audio::eRender, Audio::DEVICE_STATE_ACTIVE)?; - - let count = output_collection.GetCount()?; - - let output_device_list = device::DeviceList { - collection: output_collection, - total_count: count, - next_item: 0, - device_type: DeviceType::OUTPUT, - }; - - let input_collection = self - .0 - .EnumAudioEndpoints(Audio::eCapture, Audio::DEVICE_STATE_ACTIVE)?; - - let count = input_collection.GetCount()?; - - let input_device_list = device::DeviceList { - collection: input_collection, - total_count: count, - next_item: 0, - device_type: DeviceType::INPUT, - }; - - Ok(output_device_list.chain(input_device_list)) - } - } -} - -unsafe impl Send for AudioDeviceEnumerator {} - -unsafe impl Sync for AudioDeviceEnumerator {} diff --git a/crates/interflow-wasapi/src/platform.rs b/crates/interflow-wasapi/src/platform.rs new file mode 100644 index 0000000..95983b5 --- /dev/null +++ b/crates/interflow-wasapi/src/platform.rs @@ -0,0 +1,138 @@ +use crate::device::{Device, DeviceList}; +use crate::util::MMDevice; +use crate::Error; +use bitflags::bitflags_match; +#[cfg(feature = "collect")] +use interflow_core::collect; +use interflow_core::platform; +use interflow_core::traits::{ExtensionProvider, Selector}; +use interflow_core::DeviceType; +#[cfg(feature = "collect")] +use std::rc::Rc; +use std::sync::OnceLock; +use windows::Win32::Media::Audio; +use windows::Win32::System::Com; + +pub struct Platform; + +impl ExtensionProvider for Platform { + fn register<'a, 'sel>(&'a self, selector: &'sel mut Selector<'a>) -> &'sel mut Selector<'a> { + selector + } +} + +impl platform::Platform for Platform { + type Error = Error; + type Device = Device; + const NAME: &'static str = "WASAPI"; + + fn default_device(&self, device_type: DeviceType) -> Result { + let Some(device) = audio_device_enumerator().get_default_device(device_type)? else { + return Err(Error::ConfigurationNotAvailable); + }; + Ok(device) + } + + fn list_devices(&self) -> Result, Self::Error> { + audio_device_enumerator().get_device_list() + } +} + +pub trait DefaultByRole { + fn default_by_role(&self, flow: Audio::EDataFlow, role: Audio::ERole) -> Result; +} + +impl DefaultByRole for Platform { + fn default_by_role(&self, flow: Audio::EDataFlow, role: Audio::ERole) -> Result { + audio_device_enumerator().get_default_device_with_role(flow, role) + } +} + +fn audio_device_enumerator() -> &'static AudioDeviceEnumerator { + static ENUMERATOR: OnceLock = OnceLock::new(); + ENUMERATOR.get_or_init(|| { + let com = crate::util::com().unwrap(); + unsafe { + let enumerator = com + .create_instance::<_, Audio::IMMDeviceEnumerator>( + &Audio::MMDeviceEnumerator, + None, + Com::CLSCTX_ALL, + ) + .unwrap(); + AudioDeviceEnumerator(enumerator) + } + }) +} + +struct AudioDeviceEnumerator(Audio::IMMDeviceEnumerator); + +unsafe impl Send for AudioDeviceEnumerator {} +unsafe impl Sync for AudioDeviceEnumerator {} + +impl AudioDeviceEnumerator { + fn get_default_device(&self, device_type: DeviceType) -> Result, Error> { + let Some(flow) = bitflags_match!(device_type, { + DeviceType::INPUT => Some(Audio::eCapture), + DeviceType::OUTPUT => Some(Audio::eRender), + _ => None, + }) else { + return Ok(None); + }; + self.get_default_device_with_role(flow, Audio::eConsole) + .map(Some) + } + + fn get_default_device_with_role( + &self, + flow: Audio::EDataFlow, + role: Audio::ERole, + ) -> Result { + unsafe { + let device = self.0.GetDefaultAudioEndpoint(flow, role)?; + let device_type = match flow { + Audio::eRender => DeviceType::OUTPUT, + _ => DeviceType::INPUT, + }; + Ok(Device { + handle: MMDevice::new(device), + device_type: DeviceType::PHYSICAL | device_type, + }) + } + } + + fn get_device_list(&self) -> Result, Error> { + unsafe { + let output_collection = self + .0 + .EnumAudioEndpoints(Audio::eRender, Audio::DEVICE_STATE_ACTIVE)?; + let count = output_collection.GetCount()?; + let output_device_list = DeviceList { + collection: output_collection, + total_count: count, + next_item: 0, + device_type: DeviceType::OUTPUT, + }; + + let input_collection = self + .0 + .EnumAudioEndpoints(Audio::eCapture, Audio::DEVICE_STATE_ACTIVE)?; + let count = input_collection.GetCount()?; + let input_device_list = DeviceList { + collection: input_collection, + total_count: count, + next_item: 0, + device_type: DeviceType::INPUT, + }; + + Ok(output_device_list.chain(input_device_list)) + } + } +} + +#[cfg(feature = "collect")] +#[scattered_collect::scatter(collect::REGISTRAR)] +static WASAPI_PLATFORM_REGISTRATION: collect::Registration = collect::Registration { + constructor: || Some(Rc::new(Platform)), + priority: collect::DEFAULT_PLATFORM_PRIORITY, +}; diff --git a/crates/interflow-wasapi/src/stream.rs b/crates/interflow-wasapi/src/stream.rs index 8208e57..7dd3967 100644 --- a/crates/interflow-wasapi/src/stream.rs +++ b/crates/interflow-wasapi/src/stream.rs @@ -1,6 +1,366 @@ -use crate::util::CoTask; -use windows::Win32::Media::Audio::IAudioClient; +use crate::device::Device; +use crate::Error; +use interflow_core::buffer::AudioBuffer; +use interflow_core::device::{ResolvedStreamConfig, StreamConfig}; +use interflow_core::stream; +use interflow_core::stream::{AudioInput, AudioOutput, CallbackContext, StreamProxy}; +use interflow_core::timing::Timestamp; +use interflow_core::traits::{ExtensionProvider, Selector}; +use std::num::NonZeroUsize; +use std::ptr; +use std::slice; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; +use std::thread::JoinHandle; +use std::time::Duration; +use windows::Win32::Foundation::{CloseHandle, HANDLE, WAIT_FAILED}; +use windows::Win32::Media::Audio; +use windows::Win32::System::Threading; -struct Handle { - audio_client: CoTask, +type EjectSignal = Arc; + +pub struct Handle { + join_handle: JoinHandle>, + eject_signal: EjectSignal, +} + +impl stream::StreamHandle for Handle { + type Error = Error; + + fn eject(self) -> Result { + self.eject_signal.store(true, Ordering::Relaxed); + self.join_handle.join().expect("Audio thread panicked") + } +} + +impl Handle { + pub(crate) fn new( + device: &Device, + stream_config: StreamConfig, + callback: Callback, + ) -> Result { + let requested = stream_config.requested_device_type(); + if requested.is_duplex() { + return Err(Error::DuplexStreamRequested); + } + + let eject_signal = EjectSignal::default(); + let device = device.clone(); + let thread_name = if requested.is_input() { + "interflow_wasapi_input_stream" + } else { + "interflow_wasapi_output_stream" + }; + + let join_handle = std::thread::Builder::new() + .name(thread_name.to_string()) + .spawn({ + let eject = eject_signal.clone(); + move || { + set_thread_priority(); + if requested.is_input() { + run_input(device, stream_config, callback, eject) + } else { + run_output(device, stream_config, callback, eject) + } + } + }) + .expect("Cannot spawn audio thread"); + + Ok(Self { + join_handle, + eject_signal, + }) + } } + +fn run_output( + device: Device, + stream_config: StreamConfig, + mut callback: C, + eject_signal: EjectSignal, +) -> Result { + unsafe { + let audio_client = device.activate_audio_client()?; + + let format = Device::build_format(&stream_config); + let buffer_duration = stream_config + .buffer_size_range + .0 + .or(stream_config.buffer_size_range.1) + .map(|frames| buffer_size_to_duration(frames, stream_config.sample_rate as u32)) + .unwrap_or(0); + + audio_client.Initialize( + Audio::AUDCLNT_SHAREMODE_SHARED, + Audio::AUDCLNT_STREAMFLAGS_EVENTCALLBACK | Audio::AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM, + buffer_duration, + 0, + &format, + None, + )?; + + let frame_count = audio_client.GetBufferSize()? as usize; + let resolved_config = ResolvedStreamConfig { + sample_rate: stream_config.sample_rate, + input_channels: 0, + output_channels: stream_config.output_channels, + max_frame_count: frame_count, + }; + + let event_handle = + Threading::CreateEventA(None, false, false, windows::core::PCSTR(ptr::null()))?; + audio_client.SetEventHandle(event_handle)?; + + let render_client = audio_client.GetService::()?; + let audio_clock = audio_client.GetService::()?; + + let frame_size = NonZeroUsize::new(frame_count.max(1)).unwrap(); + let mut audio_output = AudioBuffer::zeroed(frame_size, stream_config.output_channels); + let audio_input = AudioBuffer::empty(frame_size); + + let context = CallbackContext { + stream_config: &resolved_config, + timestamp: Timestamp::new(resolved_config.sample_rate), + stream_proxy: &STREAM_PROXY, + }; + callback.prepare(context); + + audio_client.Start()?; + let clock_start = stream_instant(&audio_clock)?; + + loop { + if eject_signal.load(Ordering::Relaxed) { + break; + } + let result = Threading::WaitForSingleObject(event_handle, Threading::INFINITE); + if result == WAIT_FAILED { + let err = foundation_get_last_error(); + return Err(Error::FoundationError(format!( + "WaitForSingleObject failed: {:?}", + err + ))); + } + + let padding = audio_client.GetCurrentPadding()? as usize; + let frames_available = frame_count.saturating_sub(padding); + if frames_available == 0 { + continue; + } + + let timestamp = + output_timestamp(&audio_clock, clock_start, resolved_config.sample_rate)?; + + let dummy_input = AudioInput { + timestamp, + buffer: audio_input.slice(..0), + channel_flags: &[], + }; + + let mut output = AudioOutput { + timestamp, + buffer: audio_output.slice_mut(..frames_available), + channel_flags: &[], + }; + + callback.process_audio( + CallbackContext { + stream_config: &resolved_config, + timestamp, + stream_proxy: &STREAM_PROXY, + }, + &dummy_input, + &mut output, + ); + + let wasapi_buf = render_client.GetBuffer(frames_available as _)?; + let wasapi_slice = slice::from_raw_parts_mut( + wasapi_buf as *mut f32, + frames_available * stream_config.output_channels, + ); + for frame in 0..frames_available { + for ch in 0..stream_config.output_channels { + wasapi_slice[frame * stream_config.output_channels + ch] = + audio_output[ch][frame]; + } + } + render_client.ReleaseBuffer(frames_available as _, 0)?; + } + + audio_client.Stop()?; + CloseHandle(event_handle)?; + Ok(callback) + } +} + +fn run_input( + device: Device, + stream_config: StreamConfig, + mut callback: C, + eject_signal: EjectSignal, +) -> Result { + unsafe { + let audio_client = device.activate_audio_client()?; + + let format = Device::build_format(&stream_config); + let buffer_duration = stream_config + .buffer_size_range + .0 + .or(stream_config.buffer_size_range.1) + .map(|frames| buffer_size_to_duration(frames, stream_config.sample_rate as u32)) + .unwrap_or(0); + + audio_client.Initialize( + Audio::AUDCLNT_SHAREMODE_SHARED, + Audio::AUDCLNT_STREAMFLAGS_EVENTCALLBACK | Audio::AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM, + buffer_duration, + 0, + &format, + None, + )?; + + let frame_count = audio_client.GetBufferSize()? as usize; + let resolved_config = ResolvedStreamConfig { + sample_rate: stream_config.sample_rate, + input_channels: stream_config.input_channels, + output_channels: 0, + max_frame_count: frame_count, + }; + + let event_handle = + Threading::CreateEventA(None, false, false, windows::core::PCSTR(ptr::null()))?; + audio_client.SetEventHandle(event_handle)?; + + let capture_client = audio_client.GetService::()?; + let audio_clock = audio_client.GetService::()?; + + let frame_size = NonZeroUsize::new(frame_count.max(1)).unwrap(); + let mut audio_input = AudioBuffer::zeroed(frame_size, stream_config.input_channels); + let mut audio_output = AudioBuffer::empty(frame_size); + + let context = CallbackContext { + stream_config: &resolved_config, + timestamp: Timestamp::new(resolved_config.sample_rate), + stream_proxy: &STREAM_PROXY, + }; + callback.prepare(context); + + audio_client.Start()?; + let clock_start = stream_instant(&audio_clock)?; + + loop { + if eject_signal.load(Ordering::Relaxed) { + break; + } + let result = Threading::WaitForSingleObject(event_handle, Threading::INFINITE); + if result == WAIT_FAILED { + let err = foundation_get_last_error(); + return Err(Error::FoundationError(format!( + "WaitForSingleObject failed: {:?}", + err + ))); + } + + let mut buf_ptr = ptr::null_mut(); + let mut frames_available = 0u32; + let mut flags = 0u32; + capture_client.GetBuffer( + &mut buf_ptr, + &mut frames_available, + &mut flags, + None, + None, + )?; + + if frames_available == 0 { + continue; + } + + let wasapi_slice = slice::from_raw_parts( + buf_ptr as *const f32, + frames_available as usize * stream_config.input_channels, + ); + audio_input.copy_from_interleaved(wasapi_slice); + capture_client.ReleaseBuffer(frames_available)?; + + let timestamp = + output_timestamp(&audio_clock, clock_start, resolved_config.sample_rate)?; + + let input = AudioInput { + timestamp, + buffer: audio_input.slice(..frames_available as usize), + channel_flags: &[], + }; + + let mut dummy_output = AudioOutput { + timestamp, + buffer: audio_output.slice_mut(..0), + channel_flags: &[], + }; + + callback.process_audio( + CallbackContext { + stream_config: &resolved_config, + timestamp, + stream_proxy: &STREAM_PROXY, + }, + &input, + &mut dummy_output, + ); + } + + audio_client.Stop()?; + CloseHandle(event_handle)?; + Ok(callback) + } +} + +fn stream_instant(audio_clock: &Audio::IAudioClock) -> Result { + let mut position: u64 = 0; + let mut qpc_position: u64 = 0; + unsafe { + audio_clock.GetPosition(&mut position, Some(&mut qpc_position))?; + } + let qpc_nanos = qpc_position * 100; + Ok(Duration::from_nanos(qpc_nanos)) +} + +fn output_timestamp( + audio_clock: &Audio::IAudioClock, + clock_start: Duration, + sample_rate: f64, +) -> Result { + let clock = stream_instant(audio_clock)?; + let diff = clock - clock_start; + Ok(Timestamp::from_duration(sample_rate, diff)) +} + +fn buffer_size_to_duration(buffer_size: usize, sample_rate: u32) -> i64 { + (buffer_size as i64 * 10_000_000) / sample_rate as i64 +} + +fn set_thread_priority() { + unsafe { + let thread_id = Threading::GetCurrentThreadId(); + let _ = Threading::SetThreadPriority( + HANDLE(thread_id as isize as _), + Threading::THREAD_PRIORITY_TIME_CRITICAL, + ); + } +} + +fn foundation_get_last_error() -> windows::core::Error { + unsafe { windows::Win32::Foundation::GetLastError().into() } +} + +struct WASAPIStreamProxy; + +impl ExtensionProvider for WASAPIStreamProxy { + fn register<'a, 'sel>(&'a self, selector: &'sel mut Selector<'a>) -> &'sel mut Selector<'a> { + selector + } +} + +impl StreamProxy for WASAPIStreamProxy {} + +static STREAM_PROXY: WASAPIStreamProxy = WASAPIStreamProxy; diff --git a/crates/interflow-wasapi/src/util.rs b/crates/interflow-wasapi/src/util.rs index 5062cfe..84e32fd 100644 --- a/crates/interflow-wasapi/src/util.rs +++ b/crates/interflow-wasapi/src/util.rs @@ -2,22 +2,21 @@ use std::ffi::OsString; use std::marker::PhantomData; use std::ops; use std::os::windows::ffi::OsStringExt; -use std::ptr::{self, NonNull}; +use std::ptr::NonNull; use std::sync::OnceLock; use windows::core::Interface; use windows::Win32::Devices::Properties; use windows::Win32::Media::Audio; +use windows::Win32::System::Com::StructuredStorage; use windows::Win32::System::Com::{ - self, CoInitializeEx, CoTaskMemFree, CoUninitialize, StructuredStorage, CLSCTX, - COINIT_MULTITHREADED, STGM_READ, + self, CoInitializeEx, CoTaskMemFree, CoUninitialize, CLSCTX, COINIT_MULTITHREADED, STGM_READ, }; use windows::Win32::System::Variant::VT_LPWSTR; -/// RAII object that guards the fact that COM is initialized. -/// -// We store a raw pointer because it's the only way at the moment to remove `Send`/`Sync` from the -// object. -struct ComponentObjectModel(PhantomData<()>); +pub(crate) struct ComponentObjectModel(PhantomData<()>); + +unsafe impl Send for ComponentObjectModel {} +unsafe impl Sync for ComponentObjectModel {} impl ComponentObjectModel { pub unsafe fn create_instance< @@ -34,14 +33,11 @@ impl ComponentObjectModel { } impl Drop for ComponentObjectModel { - #[inline] fn drop(&mut self) { unsafe { CoUninitialize() }; } } -/// Ensures that COM is initialized in this thread. -#[inline] pub fn com() -> windows::core::Result<&'static ComponentObjectModel> { static VALUE: OnceLock = OnceLock::new(); let Some(value) = VALUE.get() else { @@ -79,12 +75,10 @@ impl MMDevice { fn get_device_name(device: &Audio::IMMDevice) -> String { unsafe { - // Open the device's property store. let property_store = device .OpenPropertyStore(STGM_READ) .expect("could not open property store"); - // Get the endpoint's friendly-name property, else the interface's friendly-name, else the device description. let mut property_value = property_store .GetValue(&Properties::DEVPKEY_Device_FriendlyName as *const _ as *const _) .or(property_store.GetValue( @@ -95,26 +89,21 @@ fn get_device_name(device: &Audio::IMMDevice) -> String { .unwrap(); let prop_variant = &property_value.Anonymous.Anonymous; - - // Read the friendly-name from the union data field, expecting a *const u16. assert_eq!(VT_LPWSTR, prop_variant.vt); let ptr_utf16 = *(&prop_variant.Anonymous as *const _ as *const *const u16); - // Find the length of the friendly name. let mut len = 0; while *ptr_utf16.offset(len) != 0 { len += 1; } - // Convert to a string. let name_slice = std::slice::from_raw_parts(ptr_utf16, len as usize); let name_os_string: OsString = OsStringExt::from_wide(name_slice); let name = name_os_string .into_string() .unwrap_or_else(|os_string| os_string.to_string_lossy().into()); - // Clean up. StructuredStorage::PropVariantClear(&mut property_value).unwrap(); name @@ -153,7 +142,7 @@ impl CoTask { } pub(super) unsafe fn construct(func: impl FnOnce(*mut *mut T) -> bool) -> Option { - let mut ptr = ptr::null_mut(); + let mut ptr = std::ptr::null_mut(); if !func(&mut ptr) { return None; } diff --git a/examples/ext-provider-lookup.rs b/examples/ext-provider-lookup.rs index 46676de..bdabc96 100644 --- a/examples/ext-provider-lookup.rs +++ b/examples/ext-provider-lookup.rs @@ -1,7 +1,6 @@ //! An example of using the [`ExtensionProvider`] trait through [`ExtensionProviderExt::lookup`] to dynamically //! lookup types and traits registered by the backend types. use interflow::prelude::*; -use interflow_coreaudio::device::CoreAudioDeviceExt; fn main() -> anyhow::Result<()> { let device = default_device(DeviceType::OUTPUT)?; diff --git a/src/backends/wasapi/stream.rs b/src/backends/wasapi/stream.rs deleted file mode 100644 index 9fb065c..0000000 --- a/src/backends/wasapi/stream.rs +++ /dev/null @@ -1,731 +0,0 @@ -use super::error; -use crate::backends::wasapi::util::WasapiMMDevice; -use crate::channel_map::Bitset; -use crate::prelude::wasapi::util::CoTaskOwned; -use crate::prelude::{AudioRef, Timestamp, WasapiError}; -use crate::sample::ConvertSample; -use crate::{audio_buffer::AudioMut, ResolvedStreamConfig}; -use crate::{ - AudioCallback, AudioCallbackContext, AudioInput, AudioOutput, AudioStreamHandle, DeviceType, - StreamConfig, -}; -use duplicate::duplicate_item; -use std::marker::PhantomData; -use std::ptr::NonNull; -use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::Arc; -use std::thread::JoinHandle; -use std::time::Duration; -use std::{ops, ptr, slice}; -use windows::core::imp::CoTaskMemFree; -use windows::core::Interface; -use windows::Win32::Foundation; -use windows::Win32::Foundation::{CloseHandle, HANDLE}; -use windows::Win32::Media::{Audio, KernelStreaming, Multimedia}; -use windows::Win32::System::Threading; - -type EjectSignal = Arc; - -#[duplicate_item( -name ty; -[AudioCaptureBuffer] [IAudioCaptureClient]; -[AudioRenderBuffer] [IAudioRenderClient]; -)] -struct name<'a, T> { - interface: &'a Audio::ty, - data: NonNull, - frame_size: usize, - channels: usize, - __type: PhantomData, -} - -#[duplicate_item( -name; -[AudioCaptureBuffer]; -[AudioRenderBuffer]; -)] -impl<'a, T> ops::Deref for name<'a, T> { - type Target = [T]; - - fn deref(&self) -> &Self::Target { - unsafe { slice::from_raw_parts(self.data.cast().as_ptr(), self.channels * self.frame_size) } - } -} - -#[duplicate_item( -name; -[AudioCaptureBuffer]; -[AudioRenderBuffer]; -)] -impl<'a, T> ops::DerefMut for name<'a, T> { - fn deref_mut(&mut self) -> &mut Self::Target { - unsafe { - slice::from_raw_parts_mut(self.data.cast().as_ptr(), self.channels * self.frame_size) - } - } -} - -impl Drop for AudioCaptureBuffer<'_, T> { - fn drop(&mut self) { - unsafe { self.interface.ReleaseBuffer(self.frame_size as _).unwrap() }; - } -} - -impl Drop for AudioRenderBuffer<'_, T> { - fn drop(&mut self) { - unsafe { - self.interface - .ReleaseBuffer(self.frame_size as _, 0) - .unwrap(); - } - } -} - -impl<'a, T> AudioRenderBuffer<'a, T> { - fn from_client( - render_client: &'a Audio::IAudioRenderClient, - channels: usize, - frame_size: usize, - ) -> Result { - let data = NonNull::new(unsafe { render_client.GetBuffer(frame_size as _) }?) - .expect("Audio buffer data is null"); - Ok(Self { - interface: render_client, - data, - frame_size, - channels, - __type: PhantomData, - }) - } -} -impl<'a, T> AudioCaptureBuffer<'a, T> { - fn from_client( - capture_client: &'a Audio::IAudioCaptureClient, - channels: usize, - ) -> Result, error::WasapiError> { - let mut buf_ptr = ptr::null_mut(); - let mut frame_size = 0; - let mut flags = 0; - unsafe { capture_client.GetBuffer(&mut buf_ptr, &mut frame_size, &mut flags, None, None) }?; - let Some(data) = NonNull::new(buf_ptr as _) else { - return Ok(None); - }; - Ok(Some(Self { - interface: capture_client, - data, - frame_size: frame_size as _, - channels, - __type: PhantomData, - })) - } -} - -struct AudioThread { - audio_client: Audio::IAudioClient, - interface: Interface, - audio_clock: Audio::IAudioClock, - stream_config: ResolvedStreamConfig, - eject_signal: EjectSignal, - frame_size: usize, - callback: Callback, - event_handle: HANDLE, - clock_start: Duration, - convert_scratch_buffer: Box<[f32]>, - process_fn: fn(&mut Self) -> Result<(), error::WasapiError>, -} - -impl AudioThread { - fn finalize(self) -> Result { - if !self.event_handle.is_invalid() { - unsafe { CloseHandle(self.event_handle) }?; - } - let _ = unsafe { - self.audio_client - .Stop() - .inspect_err(|err| eprintln!("Cannot stop audio thread: {err}")) - }; - Ok(self.callback) - } -} - -impl AudioThread { - fn new_with_process_fn( - device: WasapiMMDevice, - direction: StreamDirection, - eject_signal: EjectSignal, - stream_config: StreamConfig, - callback: Callback, - process_fn: impl FnOnce(SupportedConfig) -> fn(&mut Self) -> Result<(), error::WasapiError>, - ) -> Result { - eprintln!("Current stream config: {stream_config:#?}"); - let supported_config = FindSupportedConfig { - config: &stream_config, - device: &device, - is_output: matches!(direction, StreamDirection::Output), - } - .supported_config() - .ok_or(WasapiError::ConfigurationNotAvailable)?; - let frame_size = stream_config - .buffer_size_range - .0 - .or(stream_config.buffer_size_range.1); - let buffer_duration = frame_size - .map(|frame_size| buffer_size_to_duration(frame_size, stream_config.sample_rate as _)) - .unwrap_or(0); - unsafe { - let audio_client = device.activate::()?; - audio_client.Initialize( - if stream_config.exclusive { - Audio::AUDCLNT_SHAREMODE_EXCLUSIVE - } else { - Audio::AUDCLNT_SHAREMODE_SHARED - }, - Audio::AUDCLNT_STREAMFLAGS_EVENTCALLBACK - | Audio::AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM, - buffer_duration, - 0, - supported_config.format(), - None, - )?; - let buffer_size = audio_client.GetBufferSize()? as usize; - let resolved_config = supported_config.resolved_config(direction); - let event_handle = { - let event_handle = - Threading::CreateEventA(None, false, false, windows::core::PCSTR(ptr::null()))?; - audio_client.SetEventHandle(event_handle)?; - event_handle - }; - let interface = audio_client.GetService::()?; - let audio_clock = audio_client.GetService::()?; - let frame_size = buffer_size; - Ok(Self { - audio_client, - interface, - audio_clock, - event_handle, - frame_size, - eject_signal, - stream_config: resolved_config, - clock_start: Duration::ZERO, - callback, - convert_scratch_buffer: if matches!(supported_config, SupportedConfig::Float(..)) { - Box::new([]) - } else { - vec![0.0; resolved_config.max_frame_count].into_boxed_slice() - }, - process_fn: process_fn(supported_config), - }) - } - } - - fn await_frame(&mut self) -> Result<(), error::WasapiError> { - let _ = unsafe { - let result = Threading::WaitForSingleObject(self.event_handle, Threading::INFINITE); - if result == Foundation::WAIT_FAILED { - let err = Foundation::GetLastError(); - let description = format!("Waiting for event handle failed: {:?}", err); - return Err(error::WasapiError::FoundationError(description)); - } - result - }; - Ok(()) - } - - fn output_timestamp(&self) -> Result { - let clock = stream_instant(&self.audio_clock)?; - let diff = clock - self.clock_start; - Ok(Timestamp::from_duration( - self.stream_config.sample_rate, - diff, - )) - } -} - -impl AudioThread { - pub(super) fn new( - device: WasapiMMDevice, - eject_signal: EjectSignal, - stream_config: StreamConfig, - callback: Callback, - ) -> Result { - Self::new_with_process_fn( - device, - StreamDirection::Input, - eject_signal, - stream_config, - callback, - |config| match config { - SupportedConfig::Float(..) => Self::process_float, - SupportedConfig::I32(..) => Self::process::, - SupportedConfig::U32(..) => Self::process::, - }, - ) - } - - fn run(mut self) -> Result { - set_thread_priority(); - unsafe { - self.audio_client.Start()?; - } - self.clock_start = stream_instant(&self.audio_clock)?; - loop { - if self.eject_signal.load(Ordering::Relaxed) { - break self.finalize(); - } - self.await_frame()?; - (self.process_fn)(&mut self)?; - } - .inspect_err(|err| eprintln!("Render thread process error: {err}")) - } - - fn process_float(&mut self) -> Result<(), error::WasapiError> { - let frames_available = unsafe { self.interface.GetNextPacketSize()? as usize }; - if frames_available == 0 { - return Ok(()); - } - let Some(mut buffer) = AudioCaptureBuffer::::from_client( - &self.interface, - self.stream_config.output_channels, - )? - else { - eprintln!("Null buffer from WASAPI"); - return Ok(()); - }; - let timestamp = self.output_timestamp()?; - let context = AudioCallbackContext { - stream_config: self.stream_config, - timestamp, - }; - let buffer = - AudioRef::from_interleaved(&mut buffer, self.stream_config.output_channels).unwrap(); - let input = AudioInput { timestamp, buffer }; - self.callback.process_audio( - context, - input, - AudioOutput { - timestamp, - buffer: AudioMut::empty(), - }, - ); - Ok(()) - } - - fn process(&mut self) -> Result<(), error::WasapiError> { - let frames_available = unsafe { self.interface.GetNextPacketSize()? as usize }; - if frames_available == 0 { - return Ok(()); - } - let Some(buffer) = AudioCaptureBuffer::::from_client( - &self.interface, - self.stream_config.output_channels, - )? - else { - eprintln!("Null buffer from WASAPI"); - return Ok(()); - }; - T::convert_to_slice(&mut *self.convert_scratch_buffer, &*buffer); - - let timestamp = self.output_timestamp()?; - let context = AudioCallbackContext { - stream_config: self.stream_config, - timestamp, - }; - let buffer = AudioRef::from_interleaved( - &mut self.convert_scratch_buffer[..buffer.len()], - self.stream_config.output_channels, - ) - .unwrap(); - let input = AudioInput { timestamp, buffer }; - self.callback.process_audio( - context, - input, - AudioOutput { - timestamp, - buffer: AudioMut::empty(), - }, - ); - Ok(()) - } -} - -impl AudioThread { - pub(super) fn new( - device: WasapiMMDevice, - eject_signal: EjectSignal, - stream_config: StreamConfig, - callback: Callback, - ) -> Result { - Self::new_with_process_fn( - device, - StreamDirection::Input, - eject_signal, - stream_config, - callback, - |config| match config { - SupportedConfig::Float(..) => Self::process_float, - SupportedConfig::I32(..) => Self::process::, - SupportedConfig::U32(..) => Self::process::, - }, - ) - } - - fn run(mut self) -> Result { - set_thread_priority(); - unsafe { - self.audio_client.Start()?; - } - self.clock_start = stream_instant(&self.audio_clock)?; - loop { - if self.eject_signal.load(Ordering::Relaxed) { - break self.finalize(); - } - self.await_frame()?; - (self.process_fn)(&mut self)?; - } - .inspect_err(|err| eprintln!("Render thread process error: {err}")) - } - - fn process_float(&mut self) -> Result<(), error::WasapiError> { - let frames_available = unsafe { - let padding = self.audio_client.GetCurrentPadding()? as usize; - self.frame_size - padding - }; - if frames_available == 0 { - return Ok(()); - } - let frames_requested = frames_available.min(self.stream_config.max_frame_count); - let mut buffer = AudioRenderBuffer::::from_client( - &self.interface, - self.stream_config.output_channels, - frames_requested, - )?; - let timestamp = self.output_timestamp()?; - let context = AudioCallbackContext { - stream_config: self.stream_config, - timestamp, - }; - let buffer = - AudioMut::from_interleaved_mut(&mut buffer, self.stream_config.output_channels) - .unwrap(); - let output = AudioOutput { timestamp, buffer }; - self.callback.process_audio( - context, - AudioInput { - timestamp: timestamp, - buffer: AudioRef::empty(), - }, - output, - ); - Ok(()) - } - - fn process(&mut self) -> Result<(), error::WasapiError> { - let frames_available = unsafe { - let padding = self.audio_client.GetCurrentPadding()? as usize; - self.frame_size - padding - }; - if frames_available == 0 { - return Ok(()); - } - let frames_requested = frames_available.min(self.stream_config.max_frame_count); - let mut buffer = AudioRenderBuffer::::from_client( - &self.interface, - self.stream_config.output_channels, - frames_requested, - )?; - let timestamp = self.output_timestamp()?; - let context = AudioCallbackContext { - stream_config: self.stream_config, - timestamp, - }; - let output = AudioOutput { - timestamp, - buffer: AudioMut::from_interleaved_mut( - &mut self.convert_scratch_buffer[..buffer.len()], - self.stream_config.output_channels, - ) - .unwrap(), - }; - self.callback.process_audio( - context, - AudioInput { - timestamp: timestamp, - buffer: AudioRef::empty(), - }, - output, - ); - let len = buffer.len(); - T::convert_from_slice(&mut *buffer, &self.convert_scratch_buffer[..len]); - Ok(()) - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum StreamDirection { - Input, - Output, -} - -impl TryFrom for StreamDirection { - type Error = WasapiError; - - fn try_from(value: DeviceType) -> Result { - if value.is_input() { - Ok(Self::Input) - } else if value.is_output() { - Ok(Self::Output) - } else { - Err(WasapiError::DuplexStreamRequested) - } - } -} - -/// Type representing a WASAPI audio stream. -pub struct WasapiStream { - join_handle: JoinHandle>, - eject_signal: EjectSignal, -} - -impl AudioStreamHandle for WasapiStream { - type Error = error::WasapiError; - - fn eject(self) -> Result { - self.eject_signal.store(true, Ordering::Relaxed); - self.join_handle - .join() - .expect("Audio output thread panicked") - } -} - -impl WasapiStream { - pub(crate) fn new( - device: WasapiMMDevice, - direction: StreamDirection, - stream_config: StreamConfig, - callback: Callback, - ) -> Self { - let eject_signal = EjectSignal::default(); - let join_handle = std::thread::Builder::new() - .name("interflow_wasapi_output_stream".to_string()) - .spawn({ - let eject_signal = eject_signal.clone(); - move || match direction { - StreamDirection::Input => AudioThread::<_, Audio::IAudioCaptureClient>::new( - device, - eject_signal, - stream_config, - callback, - ) - .inspect_err(|err| eprintln!("Failed to create render thread: {err}"))? - .run(), - StreamDirection::Output => AudioThread::<_, Audio::IAudioRenderClient>::new( - device, - eject_signal, - stream_config, - callback, - ) - .inspect_err(|err| eprintln!("Failed to create render thread: {err}"))? - .run(), - } - }) - .expect("Cannot spawn audio output thread"); - Self { - join_handle, - eject_signal, - } - } -} - -fn set_thread_priority() { - unsafe { - let thread_id = Threading::GetCurrentThreadId(); - - let _ = Threading::SetThreadPriority( - HANDLE(thread_id as isize as _), - Threading::THREAD_PRIORITY_TIME_CRITICAL, - ); - } -} - -pub fn buffer_size_to_duration(buffer_size: usize, sample_rate: u32) -> i64 { - (buffer_size as i64 / sample_rate as i64) * (1_000_000_000 / 100) -} - -fn stream_instant(audio_clock: &Audio::IAudioClock) -> Result { - let mut position: u64 = 0; - let mut qpc_position: u64 = 0; - unsafe { - audio_clock.GetPosition(&mut position, Some(&mut qpc_position))?; - }; - // The `qpc_position` is in 100 nanosecond units. Convert it to nanoseconds. - let qpc_nanos = qpc_position * 100; - let instant = Duration::from_nanos(qpc_nanos); - Ok(instant) -} - -const fn config_to_waveformatextensible( - config: &StreamConfig, - is_output: bool, -) -> Audio::WAVEFORMATEXTENSIBLE { - const CB_SIZE: u16 = { - const EXTENSIBLE_SIZE: usize = size_of::(); - const EX_SIZE: usize = size_of::(); - (EXTENSIBLE_SIZE - EX_SIZE) as u16 - }; - let waveformatex = config_to_waveformatex::( - config, - is_output, - KernelStreaming::WAVE_FORMAT_EXTENSIBLE, - CB_SIZE, - ); - - let sample_bytes = size_of::() as u16; - let bits_per_sample = 8 * sample_bytes; - let channel_mask = KernelStreaming::KSAUDIO_SPEAKER_DIRECTOUT; - let sub_format = Multimedia::KSDATAFORMAT_SUBTYPE_IEEE_FLOAT; - - Audio::WAVEFORMATEXTENSIBLE { - Format: waveformatex, - Samples: Audio::WAVEFORMATEXTENSIBLE_0 { - wSamplesPerBlock: bits_per_sample, - }, - dwChannelMask: channel_mask, - SubFormat: sub_format, - } -} - -const fn config_to_waveformatex( - config: &StreamConfig, - is_output: bool, - format_tag: u32, - cb_size: u16, -) -> Audio::WAVEFORMATEX { - let channels = if is_output { - config.output_channels as u16 - } else { - config.input_channels as u16 - }; - let sample_rate = config.sample_rate as u32; - let sample_bytes = size_of::() as u16; - let avg_bytes_per_sec = channels as u32 * sample_rate * sample_bytes as u32; - let block_align = channels * sample_bytes; - - let waveformatex = Audio::WAVEFORMATEX { - wFormatTag: format_tag as u16, - nChannels: channels, - nSamplesPerSec: sample_rate, - nAvgBytesPerSec: avg_bytes_per_sec, - nBlockAlign: block_align, - wBitsPerSample: 8 * sample_bytes, - cbSize: cb_size, - }; - waveformatex -} - -pub(super) enum SupportedConfig { - Float(Audio::WAVEFORMATEXTENSIBLE), - I32(Audio::WAVEFORMATEX), - U32(Audio::WAVEFORMATEX), -} - -impl SupportedConfig { - fn format(&self) -> &Audio::WAVEFORMATEX { - match self { - SupportedConfig::Float(wfx) => &wfx.Format, - SupportedConfig::I32(wfx) => wfx, - SupportedConfig::U32(wfx) => wfx, - } - } - - fn resolved_config(&self, direction: StreamDirection) -> ResolvedStreamConfig { - let format = self.format(); - ResolvedStreamConfig { - sample_rate: format.nSamplesPerSec as _, - input_channels: if direction == StreamDirection::Input { - format.nChannels as _ - } else { - 0 - }, - output_channels: if direction == StreamDirection::Output { - format.nChannels as _ - } else { - 0 - }, - max_frame_count: format.cbSize as _, - } - } -} - -pub(super) struct FindSupportedConfig<'a> { - pub(super) device: &'a WasapiMMDevice, - pub(super) config: &'a StreamConfig, - pub(super) is_output: bool, -} - -impl<'a> FindSupportedConfig<'a> { - pub(super) fn supported_config(&self) -> Option { - self.supported_config_float() - .map(SupportedConfig::Float) - .or_else(|| self.supported_config_i32().map(SupportedConfig::I32)) - .or_else(|| self.supported_config_u32().map(SupportedConfig::U32)) - } - - fn supported_config_float(&self) -> Option { - let format = config_to_waveformatextensible(self.config, self.is_output); - self.check_format(self.config, &format.Format) - .then_some(format) - } - - fn supported_config_i32(&self) -> Option { - let format = - config_to_waveformatex::(self.config, self.is_output, Audio::WAVE_FORMAT_PCM, 0); - self.check_format(self.config, &format).then_some(format) - } - - fn supported_config_u32(&self) -> Option { - let format = - config_to_waveformatex::(self.config, self.is_output, Audio::WAVE_FORMAT_PCM, 0); - self.check_format(self.config, &format).then_some(format) - } - - fn check_format(&self, config: &StreamConfig, format: &Audio::WAVEFORMATEX) -> bool { - let try_ = || -> Result<_, WasapiError> { - unsafe { - let audio_client = self.device.activate::()?; - let sharemode = if self.config.exclusive { - Audio::AUDCLNT_SHAREMODE_EXCLUSIVE - } else { - Audio::AUDCLNT_SHAREMODE_SHARED - }; - if self.config.exclusive { - audio_client - .IsFormatSupported(Audio::AUDCLNT_SHAREMODE_EXCLUSIVE, format, None) - .ok()?; - return Ok(true); - } else { - let Some(actual_format) = CoTaskOwned::construct(|ptr| { - audio_client - .IsFormatSupported(sharemode, format, Some(ptr)) - .is_ok() - }) else { - return Ok(false); - }; - let format = actual_format.read_unaligned(); - let sample_rate = format.nSamplesPerSec; - let new_channels = format.nChannels; - let new_sample_rate = sample_rate as f64; - if config.sample_rate != new_sample_rate - || config.output_channels != new_channels.count() - { - Ok(false) - } else { - Ok(true) - } - } - } - }; - try_() - .inspect_err(|err| eprintln!("Error while checking configuration is valid: {err}")) - .unwrap_or(false) - } -} diff --git a/src/lib.rs b/src/lib.rs index 4af5554..91ec842 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,6 +14,8 @@ pub mod prelude { pub use interflow_core::prelude::*; #[cfg(any(target_os = "macos", target_os = "ios"))] pub use interflow_coreaudio::prelude as coreaudio; + #[cfg(target_os = "windows")] + pub use interflow_wasapi::prelude as wasapi; } /// Return the default platform. From 59ff9dabbe5d5c53b66b3247729da223a50a13ea Mon Sep 17 00:00:00 2001 From: Nathan Graule Date: Sat, 25 Jul 2026 16:53:12 +0200 Subject: [PATCH 37/38] chore: new example listing registered platforms --- crates/interflow-wasapi/src/platform.rs | 13 +++++++++++++ examples/list-platforms.rs | 18 ++++++++++++++++++ src/backends/null/mod.rs | 2 +- 3 files changed, 32 insertions(+), 1 deletion(-) create mode 100644 examples/list-platforms.rs diff --git a/crates/interflow-wasapi/src/platform.rs b/crates/interflow-wasapi/src/platform.rs index 95983b5..0a907a7 100644 --- a/crates/interflow-wasapi/src/platform.rs +++ b/crates/interflow-wasapi/src/platform.rs @@ -11,13 +11,16 @@ use interflow_core::DeviceType; use std::rc::Rc; use std::sync::OnceLock; use windows::Win32::Media::Audio; +use windows::Win32::Media::Audio::{EDataFlow, ERole}; use windows::Win32::System::Com; +use interflow_core::proxies::PlatformProxy; pub struct Platform; impl ExtensionProvider for Platform { fn register<'a, 'sel>(&'a self, selector: &'sel mut Selector<'a>) -> &'sel mut Selector<'a> { selector + .register::(self) } } @@ -130,6 +133,16 @@ impl AudioDeviceEnumerator { } } +pub trait DefaultForRole: PlatformProxy { + fn default_for_role(&self, flow: Audio::EDataFlow, role: Audio::ERole) -> Result; +} + +impl DefaultForRole for Platform { + fn default_for_role(&self, flow: EDataFlow, role: ERole) -> Result { + Ok(audio_device_enumerator().get_default_device_with_role(flow, role)?) + } +} + #[cfg(feature = "collect")] #[scattered_collect::scatter(collect::REGISTRAR)] static WASAPI_PLATFORM_REGISTRATION: collect::Registration = collect::Registration { diff --git a/examples/list-platforms.rs b/examples/list-platforms.rs new file mode 100644 index 0000000..9371041 --- /dev/null +++ b/examples/list-platforms.rs @@ -0,0 +1,18 @@ +use interflow::prelude::*; +use interflow_core::collect::get_registry; + +fn main() -> anyhow::Result<()> { + env_logger::init(); + + for ctor in get_registry() { + let Some(platform) = ctor() else { continue }; + println!("Platform: {}", platform.name()); + if let Ok(device) = platform.default_device(DeviceType::INPUT) { + println!("\tDefault input device: {}", device.name()); + } + if let Ok(device) = platform.default_device(DeviceType::OUTPUT) { + println!("\tDefault output device: {}", device.name()); + } + } + Ok(()) +} \ No newline at end of file diff --git a/src/backends/null/mod.rs b/src/backends/null/mod.rs index c485118..397b7d8 100644 --- a/src/backends/null/mod.rs +++ b/src/backends/null/mod.rs @@ -174,7 +174,7 @@ impl StreamProxy for NullStreamProxy {} #[scattered_collect::scatter(collect::REGISTRAR)] static NULL_PLATFORM_REGISTRATION: collect::Registration = collect::Registration { constructor: || { - log::error!("No platforms available, using null backend (you will not hear any sound)"); + log::warn!("Using null backend (you may have no sound when using `default_stream`)"); Some(Rc::new(Platform)) }, priority: i32::MIN + 1, From c47abeba2c140b956dc198126b45309f96bee6f6 Mon Sep 17 00:00:00 2001 From: Nathan Graule Date: Sat, 25 Jul 2026 17:07:57 +0200 Subject: [PATCH 38/38] docs: update README.md --- README.md | 58 +++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 54 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 92b5c16..be2d48d 100644 --- a/README.md +++ b/README.md @@ -23,13 +23,63 @@ and format conversion. - [x] WASAPI - [ ] ASIO -- [x] ALSA +- [ ] ALSA - [ ] PulseAudio -- [x] PipeWire +- [ ] PipeWire - [ ] JACK - [x] CoreAudio -## Getting Started +## Getting started + +Add `interflow` to the the list of dependencies in your `Cargo.toml` file: + +```toml +[dependencies] +interflow = { git = "https://github.com/SolarLiner/interflow.git", version = "0.1.0" } +``` + +Then, in your main function, import `interflow::prelude::*` and use the `default_stream` function: + +```rust +use interflow::prelude::*; + +fn main() { + let handle = default_stream(DeviceType::OUTPUT, |context, _input, output| { + let time = context.timestamp.as_seconds(); // stream timestamp + let time = output.timestamp.as_seconds(); // output stream provided timestamp (generally more accurate, also + // available for input) + for i in 0..output.buffer.frames() { + output.buffer.set_mono(0.0); // example: output silence + } + }); + std::thread::sleep(std::time::Duration::from_secs(10)); + let callback = handle.eject().unwrap(); // You can "eject" your callback and retrieve it, so you can reuse it in + // another stream. If you don't eject it, the callback will be dropped + // with the handle itself +} +``` + +It is important to import backends that you want to use, which is done automatically for default backends with `use +interflow::prelude::*;`. Additional third-party backends that participate in automatic registration must be imported +separately. + +The mechanism used is link-time registration through the +[`scattered-collections`](https://docs.rs/scattered-collect/latest/scattered_collect/) crate. It is possible to use +backends directly: + +```rust +use interflow::prelude::*; + +fn main() { + let device = wasapi::platform::Platform.default_device(DeviceType::OUTPUT).unwrap(); + let handle = device.create_default_stream(...); +} +``` + +Take a look at the [examples](./examples) for an overview of the available API, as well +as [the docs](https://solarliner.dev/interflow) for the generated reference documentation. + +## Contributing ### Prerequisites @@ -46,4 +96,4 @@ Ensure you have the following installed on your system: ### Building -`Interflow` uses `cargo` for dependency management and building. +`Interflow` uses `cargo` for dependency management and building. \ No newline at end of file