From f9087035efeea8eddb26638d159ef8d9cada4182 Mon Sep 17 00:00:00 2001 From: Marc Pabst <“marcpabst@users.noreply.github.com”> Date: Mon, 24 Mar 2025 14:56:07 +0000 Subject: [PATCH 1/4] add latency method for coreaudio --- src/host/coreaudio/macos/mod.rs | 154 ++++++++++++++++++++------------ src/platform/mod.rs | 11 +++ src/traits.rs | 5 ++ 3 files changed, 112 insertions(+), 58 deletions(-) diff --git a/src/host/coreaudio/macos/mod.rs b/src/host/coreaudio/macos/mod.rs index 49ad5d144..24ddc5e94 100644 --- a/src/host/coreaudio/macos/mod.rs +++ b/src/host/coreaudio/macos/mod.rs @@ -33,6 +33,7 @@ use std::fmt; use std::mem; use std::os::raw::c_char; use std::ptr::null; +use std::rc::Rc; use std::slice; use std::sync::mpsc::{channel, RecvTimeoutError}; use std::sync::{Arc, Mutex}; @@ -43,6 +44,9 @@ pub use self::enumerate::{ SupportedOutputConfigs, }; +use coreaudio::sys::{ + kAudioDevicePropertyLatency, kAudioDevicePropertySafetyOffset, kAudioStreamPropertyLatency, +}; use property_listener::AudioObjectPropertyListener; pub mod enumerate; @@ -296,18 +300,7 @@ impl Device { let ranges: *mut AudioValueRange = ranges.as_mut_ptr() as *mut _; let ranges: &'static [AudioValueRange] = slice::from_raw_parts(ranges, n_ranges); - #[allow(non_upper_case_globals)] - let input = match scope { - kAudioObjectPropertyScopeInput => Ok(true), - kAudioObjectPropertyScopeOutput => Ok(false), - _ => Err(BackendSpecificError { - description: format!( - "unexpected scope (neither input nor output): {:?}", - scope - ), - }), - }?; - let audio_unit = audio_unit_from_device(self, input)?; + let audio_unit = audio_unit_from_device(self, true)?; let buffer_size = get_io_buffer_frame_size_range(&audio_unit)?; // Collect the supported formats for the device. @@ -409,18 +402,7 @@ impl Device { } }; - #[allow(non_upper_case_globals)] - let input = match scope { - kAudioObjectPropertyScopeInput => Ok(true), - kAudioObjectPropertyScopeOutput => Ok(false), - _ => Err(BackendSpecificError { - description: format!( - "unexpected scope (neither input nor output): {:?}", - scope - ), - }), - }?; - let audio_unit = audio_unit_from_device(self, input)?; + let audio_unit = audio_unit_from_device(self, true)?; let buffer_size = get_io_buffer_frame_size_range(&audio_unit)?; let config = SupportedStreamConfig { @@ -462,32 +444,7 @@ struct StreamInner { // a stream associated with the device. #[allow(dead_code)] device_id: AudioDeviceID, -} - -impl StreamInner { - fn play(&mut self) -> Result<(), PlayStreamError> { - if !self.playing { - if let Err(e) = self.audio_unit.start() { - let description = format!("{}", e); - let err = BackendSpecificError { description }; - return Err(err.into()); - } - self.playing = true; - } - Ok(()) - } - - fn pause(&mut self) -> Result<(), PauseStreamError> { - if self.playing { - if let Err(e) = self.audio_unit.stop() { - let description = format!("{}", e); - let err = BackendSpecificError { description }; - return Err(err.into()); - } - self.playing = false; - } - Ok(()) - } + sample_rate: SampleRate, } /// Register the on-disconnect callback. @@ -500,7 +457,7 @@ fn add_disconnect_listener( where E: FnMut(StreamError) + Send + 'static, { - let stream_inner_weak = Arc::downgrade(&stream.inner); + let stream_copy = stream.clone(); let mut stream_inner = stream.inner.lock().unwrap(); stream_inner._disconnect_listener = Some(AudioObjectPropertyListener::new( stream_inner.device_id, @@ -510,11 +467,8 @@ where mElement: kAudioObjectPropertyElementMaster, }, move || { - if let Some(stream_inner_strong) = stream_inner_weak.upgrade() { - let mut stream_inner = stream_inner_strong.lock().unwrap(); - let _ = stream_inner.pause(); - (error_callback.lock().unwrap())(StreamError::DeviceNotAvailable); - } + let _ = stream_copy.pause(); + (error_callback.lock().unwrap())(StreamError::DeviceNotAvailable); }, )?); Ok(()) @@ -659,6 +613,7 @@ impl Device { _disconnect_listener: None, audio_unit, device_id: self.audio_device_id, + sample_rate: config.sample_rate, }); // If we didn't request the default device, stop the stream if the @@ -764,6 +719,7 @@ impl Device { _disconnect_listener: None, audio_unit, device_id: self.audio_device_id, + sample_rate: config.sample_rate, }); // If we didn't request the default device, stop the stream if the @@ -948,13 +904,95 @@ impl StreamTrait for Stream { fn play(&self) -> Result<(), PlayStreamError> { let mut stream = self.inner.lock().unwrap(); - stream.play() + if !stream.playing { + if let Err(e) = stream.audio_unit.start() { + let description = format!("{}", e); + let err = BackendSpecificError { description }; + return Err(err.into()); + } + stream.playing = true; + } + Ok(()) } fn pause(&self) -> Result<(), PauseStreamError> { let mut stream = self.inner.lock().unwrap(); - stream.pause() + if stream.playing { + if let Err(e) = stream.audio_unit.stop() { + let description = format!("{}", e); + let err = BackendSpecificError { description }; + return Err(err.into()); + } + + stream.playing = false; + } + Ok(()) + } + + // for coreaudio, latency is + // The minimum latency of an AudioStream on an AudioDevice is the sum of: + // - the Device's presentation latency, kAudioDevicePropertyLatency + // - the Stream's presentation latency, kAudioStreamPropertyLatency + // - the Device's safety offset, kAudioDevicePropertySafetyOffset + + // The latency between roughly when the IOProc is called and when the first sample hits the wire is the sum of: + // - the minimum latency for the Stream in question (as calculated above) + // - the IO buffer frame size, kAudioDevicePropertyBufferFrameSize + fn latency(&self) -> Option { + println!("latency"); + let stream = self.inner.lock().unwrap(); + let audio_unit = &stream.audio_unit; + + // device presentation latency + let device_latency: u32 = match audio_unit.get_property( + kAudioDevicePropertyLatency, + Scope::Global, + Element::Output, + ) { + Ok(device_latency) => device_latency, + Err(_) => return None, + }; + + // stream presentation latency + let stream_latency: u32 = match audio_unit.get_property( + kAudioStreamPropertyLatency, + Scope::Global, + Element::Output, + ) { + Ok(stream_latency) => stream_latency, + Err(_) => return None, + }; + + // device safety offset + let safety_offset: u32 = match audio_unit.get_property( + kAudioDevicePropertySafetyOffset, + Scope::Global, + Element::Output, + ) { + Ok(safety_offset) => safety_offset, + Err(_) => return None, + }; + + // IO buffer frame size + let buffer_size: u32 = match audio_unit.get_property( + kAudioDevicePropertyBufferFrameSize, + Scope::Global, + Element::Output, + ) { + Ok(buffer_size) => buffer_size, + Err(_) => return None, + }; + + // sample rate + let sample_rate = stream.sample_rate; + + let latency = device_latency + stream_latency + safety_offset + buffer_size; + + println!("latency: {} frames", latency); + println!("sample rate: {}", sample_rate.0); + + Some(latency) } } diff --git a/src/platform/mod.rs b/src/platform/mod.rs index 6fc35f6f4..c0958f94a 100644 --- a/src/platform/mod.rs +++ b/src/platform/mod.rs @@ -499,6 +499,17 @@ macro_rules! impl_platform_host { )* } } + + fn latency(&self) -> Option { + match self.0 { + $( + $(#[cfg($feat)])? + StreamInner::$HostVariant(ref s) => { + s.latency() + } + )* + } + } } impl From for Device { diff --git a/src/traits.rs b/src/traits.rs index 2f1bd3469..7d1f19840 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -220,4 +220,9 @@ pub trait StreamTrait { /// Note: Not all devices support suspending the stream at the hardware level. This method may /// fail in these cases. fn pause(&self) -> Result<(), PauseStreamError>; + + /// The current latency of the stream. + fn latency(&self) -> Option { + None + } } From b613261e2dd473c365f82ee142dd801fc302cb0b Mon Sep 17 00:00:00 2001 From: Marc Pabst <“marcpabst@users.noreply.github.com”> Date: Mon, 24 Mar 2025 20:33:30 +0000 Subject: [PATCH 2/4] cleanup coreaudio latency calculation --- src/host/coreaudio/macos/mod.rs | 21 +++++---------------- 1 file changed, 5 insertions(+), 16 deletions(-) diff --git a/src/host/coreaudio/macos/mod.rs b/src/host/coreaudio/macos/mod.rs index 24ddc5e94..422591354 100644 --- a/src/host/coreaudio/macos/mod.rs +++ b/src/host/coreaudio/macos/mod.rs @@ -930,17 +930,12 @@ impl StreamTrait for Stream { Ok(()) } - // for coreaudio, latency is - // The minimum latency of an AudioStream on an AudioDevice is the sum of: - // - the Device's presentation latency, kAudioDevicePropertyLatency - // - the Stream's presentation latency, kAudioStreamPropertyLatency - // - the Device's safety offset, kAudioDevicePropertySafetyOffset - - // The latency between roughly when the IOProc is called and when the first sample hits the wire is the sum of: - // - the minimum latency for the Stream in question (as calculated above) - // - the IO buffer frame size, kAudioDevicePropertyBufferFrameSize + // For coreaudio, the total latency is the sum of the + // - device presentation latency (kAudioDevicePropertyLatency) + // - stream presentation latency (kAudioStreamPropertyLatency) + // - device safety offset (kAudioDevicePropertySafetyOffset) + // - IO buffer frame size (kAudioDevicePropertyBufferFrameSize) fn latency(&self) -> Option { - println!("latency"); let stream = self.inner.lock().unwrap(); let audio_unit = &stream.audio_unit; @@ -984,14 +979,8 @@ impl StreamTrait for Stream { Err(_) => return None, }; - // sample rate - let sample_rate = stream.sample_rate; - let latency = device_latency + stream_latency + safety_offset + buffer_size; - println!("latency: {} frames", latency); - println!("sample rate: {}", sample_rate.0); - Some(latency) } } From 8ab5c0a65b068f5e721b9f39920725e7488c0ec2 Mon Sep 17 00:00:00 2001 From: Marc Pabst <“marcpabst@users.noreply.github.com”> Date: Mon, 24 Mar 2025 20:44:43 +0000 Subject: [PATCH 3/4] more cleanup --- src/host/coreaudio/macos/mod.rs | 86 ++++++++++++++++++++++----------- 1 file changed, 58 insertions(+), 28 deletions(-) diff --git a/src/host/coreaudio/macos/mod.rs b/src/host/coreaudio/macos/mod.rs index 422591354..1d236ecca 100644 --- a/src/host/coreaudio/macos/mod.rs +++ b/src/host/coreaudio/macos/mod.rs @@ -33,7 +33,6 @@ use std::fmt; use std::mem; use std::os::raw::c_char; use std::ptr::null; -use std::rc::Rc; use std::slice; use std::sync::mpsc::{channel, RecvTimeoutError}; use std::sync::{Arc, Mutex}; @@ -300,7 +299,18 @@ impl Device { let ranges: *mut AudioValueRange = ranges.as_mut_ptr() as *mut _; let ranges: &'static [AudioValueRange] = slice::from_raw_parts(ranges, n_ranges); - let audio_unit = audio_unit_from_device(self, true)?; + #[allow(non_upper_case_globals)] + let input = match scope { + kAudioObjectPropertyScopeInput => Ok(true), + kAudioObjectPropertyScopeOutput => Ok(false), + _ => Err(BackendSpecificError { + description: format!( + "unexpected scope (neither input nor output): {:?}", + scope + ), + }), + }?; + let audio_unit = audio_unit_from_device(self, input)?; let buffer_size = get_io_buffer_frame_size_range(&audio_unit)?; // Collect the supported formats for the device. @@ -402,7 +412,18 @@ impl Device { } }; - let audio_unit = audio_unit_from_device(self, true)?; + #[allow(non_upper_case_globals)] + let input = match scope { + kAudioObjectPropertyScopeInput => Ok(true), + kAudioObjectPropertyScopeOutput => Ok(false), + _ => Err(BackendSpecificError { + description: format!( + "unexpected scope (neither input nor output): {:?}", + scope + ), + }), + }?; + let audio_unit = audio_unit_from_device(self, input)?; let buffer_size = get_io_buffer_frame_size_range(&audio_unit)?; let config = SupportedStreamConfig { @@ -444,7 +465,32 @@ struct StreamInner { // a stream associated with the device. #[allow(dead_code)] device_id: AudioDeviceID, - sample_rate: SampleRate, +} + +impl StreamInner { + fn play(&mut self) -> Result<(), PlayStreamError> { + if !self.playing { + if let Err(e) = self.audio_unit.start() { + let description = format!("{}", e); + let err = BackendSpecificError { description }; + return Err(err.into()); + } + self.playing = true; + } + Ok(()) + } + + fn pause(&mut self) -> Result<(), PauseStreamError> { + if self.playing { + if let Err(e) = self.audio_unit.stop() { + let description = format!("{}", e); + let err = BackendSpecificError { description }; + return Err(err.into()); + } + self.playing = false; + } + Ok(()) + } } /// Register the on-disconnect callback. @@ -457,7 +503,7 @@ fn add_disconnect_listener( where E: FnMut(StreamError) + Send + 'static, { - let stream_copy = stream.clone(); + let stream_inner_weak = Arc::downgrade(&stream.inner); let mut stream_inner = stream.inner.lock().unwrap(); stream_inner._disconnect_listener = Some(AudioObjectPropertyListener::new( stream_inner.device_id, @@ -467,8 +513,11 @@ where mElement: kAudioObjectPropertyElementMaster, }, move || { - let _ = stream_copy.pause(); - (error_callback.lock().unwrap())(StreamError::DeviceNotAvailable); + if let Some(stream_inner_strong) = stream_inner_weak.upgrade() { + let mut stream_inner = stream_inner_strong.lock().unwrap(); + let _ = stream_inner.pause(); + (error_callback.lock().unwrap())(StreamError::DeviceNotAvailable); + } }, )?); Ok(()) @@ -613,7 +662,6 @@ impl Device { _disconnect_listener: None, audio_unit, device_id: self.audio_device_id, - sample_rate: config.sample_rate, }); // If we didn't request the default device, stop the stream if the @@ -719,7 +767,6 @@ impl Device { _disconnect_listener: None, audio_unit, device_id: self.audio_device_id, - sample_rate: config.sample_rate, }); // If we didn't request the default device, stop the stream if the @@ -904,30 +951,13 @@ impl StreamTrait for Stream { fn play(&self) -> Result<(), PlayStreamError> { let mut stream = self.inner.lock().unwrap(); - if !stream.playing { - if let Err(e) = stream.audio_unit.start() { - let description = format!("{}", e); - let err = BackendSpecificError { description }; - return Err(err.into()); - } - stream.playing = true; - } - Ok(()) + stream.play() } fn pause(&self) -> Result<(), PauseStreamError> { let mut stream = self.inner.lock().unwrap(); - if stream.playing { - if let Err(e) = stream.audio_unit.stop() { - let description = format!("{}", e); - let err = BackendSpecificError { description }; - return Err(err.into()); - } - - stream.playing = false; - } - Ok(()) + stream.pause() } // For coreaudio, the total latency is the sum of the From 84e6a334d9e9f4400e8394d3209f57acd4801804 Mon Sep 17 00:00:00 2001 From: Marc Pabst <“marcpabst@users.noreply.github.com”> Date: Tue, 25 Mar 2025 21:12:59 +0000 Subject: [PATCH 4/4] add documentation --- src/host/coreaudio/macos/mod.rs | 5 ----- src/traits.rs | 5 ++++- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/host/coreaudio/macos/mod.rs b/src/host/coreaudio/macos/mod.rs index 1d236ecca..8f0229828 100644 --- a/src/host/coreaudio/macos/mod.rs +++ b/src/host/coreaudio/macos/mod.rs @@ -960,11 +960,6 @@ impl StreamTrait for Stream { stream.pause() } - // For coreaudio, the total latency is the sum of the - // - device presentation latency (kAudioDevicePropertyLatency) - // - stream presentation latency (kAudioStreamPropertyLatency) - // - device safety offset (kAudioDevicePropertySafetyOffset) - // - IO buffer frame size (kAudioDevicePropertyBufferFrameSize) fn latency(&self) -> Option { let stream = self.inner.lock().unwrap(); let audio_unit = &stream.audio_unit; diff --git a/src/traits.rs b/src/traits.rs index 7d1f19840..405cfbd58 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -221,7 +221,10 @@ pub trait StreamTrait { /// fail in these cases. fn pause(&self) -> Result<(), PauseStreamError>; - /// The current latency of the stream. + /// The current latency of the stream in samples (if available). + /// + /// Note: This is not implemented on all platforms and not all backends return reliable + /// information. fn latency(&self) -> Option { None }