From 64237354070f714ecb27d12350b9680f61aa0ebb Mon Sep 17 00:00:00 2001 From: Nathan Graule Date: Tue, 27 May 2025 13:36:30 +0200 Subject: [PATCH 01/15] 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/15] 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/15] 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/15] 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/15] 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/15] 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/15] 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/15] 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/15] 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/15] 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/15] 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/15] 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/15] 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/15] 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/15] 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 {}