From a2fe9386af444a593bd715da285ce0f55dcff1af Mon Sep 17 00:00:00 2001 From: mitchmindtree Date: Sun, 1 Apr 2018 17:10:46 +0545 Subject: [PATCH] [coreaudio] Fix handling of non-default sample rates for input streams (#214) * [coreaudio] Fix handling of non-default sample rates for input streams Currently when building an input stream the coreaudio backend only specifies the sample rate for the audio unit, however coreaudio requires that the audio unit sample rate matches the device sample rate. This changes the `build_input_stream` behaviour to: 1. Check if the device sample rate differs from the desired one. 2. If so, check that there are no existing audio units using the device at the current sample rate. If there are, panic with a message explaining why. 3. Otherwise, change the device sample rate. 4. Continue building the input stream audio unit as normal. Closes #213. * Update CHANGELOG for coreaudio input stream sample rate fix * Publish 0.8.1 for coreaudio input stream sample rate fix --- CHANGELOG.md | 4 + Cargo.toml | 2 +- src/coreaudio/mod.rs | 170 +++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 168 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3acb746..d8b76ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Unreleased +# Version 0.8.1 (2018-03-18) + +- Fix the handling of non-default sample rates for coreaudio input streams. + # Version 0.8.0 (2018-02-15) - Add `record_wav.rs` example. Records 3 seconds to diff --git a/Cargo.toml b/Cargo.toml index 946c247..843ac8c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cpal" -version = "0.8.0" +version = "0.8.1" authors = ["The CPAL contributors", "Pierre Krieger "] description = "Low-level cross-platform audio playing library in pure Rust." repository = "https://github.com/tomaka/cpal" diff --git a/src/coreaudio/mod.rs b/src/coreaudio/mod.rs index d0a1d18..ed6c32d 100644 --- a/src/coreaudio/mod.rs +++ b/src/coreaudio/mod.rs @@ -29,15 +29,21 @@ use self::coreaudio::sys::{ AudioBuffer, AudioBufferList, AudioDeviceID, + AudioObjectAddPropertyListener, AudioObjectGetPropertyData, AudioObjectGetPropertyDataSize, + AudioObjectID, AudioObjectPropertyAddress, AudioObjectPropertyScope, + AudioObjectRemovePropertyListener, + AudioObjectSetPropertyData, AudioStreamBasicDescription, AudioValueRange, kAudioDevicePropertyAvailableNominalSampleRates, kAudioDevicePropertyDeviceNameCFString, + kAudioDevicePropertyNominalSampleRate, kAudioObjectPropertyScopeInput, + kAudioObjectPropertyScopeGlobal, kAudioDevicePropertyScopeOutput, kAudioDevicePropertyStreamConfiguration, kAudioDevicePropertyStreamFormat, @@ -253,7 +259,7 @@ impl Device { if status != kAudioHardwareNoError as i32 { let err = default_format_error_from_os_status(status) - .expect("no known error for OsStatus"); + .expect("no known error for OSStatus"); return Err(err); } @@ -314,6 +320,11 @@ struct ActiveCallbacks { struct StreamInner { playing: bool, audio_unit: AudioUnit, + // Track the device with which the audio unit was spawned. + // + // We must do this so that we can avoid changing the device sample rate if there is already + // a stream associated with the device. + device_id: AudioDeviceID, } // TODO need stronger error identification @@ -442,10 +453,11 @@ impl EventLoop { } // Add the stream to the list of streams within `self`. - fn add_stream(&self, stream_id: usize, au: AudioUnit) { + fn add_stream(&self, stream_id: usize, au: AudioUnit, device_id: AudioDeviceID) { let inner = StreamInner { playing: true, audio_unit: au, + device_id: device_id, }; let mut streams_lock = self.streams.lock().unwrap(); @@ -463,12 +475,156 @@ impl EventLoop { format: &Format, ) -> Result { - let mut audio_unit = audio_unit_from_device(device, true)?; - - // The scope and element for working with a device's output stream. + // The scope and element for working with a device's input stream. let scope = Scope::Output; let element = Element::Input; + // Check whether or not we need to change the device sample rate to suit the one specified for the stream. + unsafe { + // Get the current sample rate. + let mut property_address = AudioObjectPropertyAddress { + mSelector: kAudioDevicePropertyNominalSampleRate, + mScope: kAudioObjectPropertyScopeGlobal, + mElement: kAudioObjectPropertyElementMaster, + }; + let sample_rate: f64 = 0.0; + let data_size = mem::size_of::() as u32; + let status = AudioObjectGetPropertyData( + device.audio_device_id, + &property_address as *const _, + 0, + null(), + &data_size as *const _ as *mut _, + &sample_rate as *const _ as *mut _, + ); + coreaudio::Error::from_os_status(status)?; + + // If the requested sample rate is different to the device sample rate, update the device. + if sample_rate as u32 != format.sample_rate.0 { + + // In order to avoid breaking existing input streams we `panic!` if there is already an + // active input stream for this device with the actual sample rate. + for stream in &*self.streams.lock().unwrap() { + if let Some(stream) = stream.as_ref() { + if stream.device_id == device.audio_device_id { + panic!("cannot change device sample rate for stream as an existing stream \ + is already running at the current sample rate."); + } + } + } + + // Get available sample rate ranges. + property_address.mSelector = kAudioDevicePropertyAvailableNominalSampleRates; + let data_size = 0u32; + let status = AudioObjectGetPropertyDataSize( + device.audio_device_id, + &property_address as *const _, + 0, + null(), + &data_size as *const _ as *mut _, + ); + coreaudio::Error::from_os_status(status)?; + let n_ranges = data_size as usize / mem::size_of::(); + let mut ranges: Vec = vec![]; + ranges.reserve_exact(data_size as usize); + let status = AudioObjectGetPropertyData( + device.audio_device_id, + &property_address as *const _, + 0, + null(), + &data_size as *const _ as *mut _, + ranges.as_mut_ptr() as *mut _, + ); + coreaudio::Error::from_os_status(status)?; + let ranges: *mut AudioValueRange = ranges.as_mut_ptr() as *mut _; + let ranges: &'static [AudioValueRange] = slice::from_raw_parts(ranges, n_ranges); + + // Now that we have the available ranges, pick the one matching the desired rate. + let sample_rate = format.sample_rate.0; + let maybe_index = ranges + .iter() + .position(|r| r.mMinimum as u32 == sample_rate && r.mMaximum as u32 == sample_rate); + let range_index = match maybe_index { + None => return Err(CreationError::FormatNotSupported), + Some(i) => i, + }; + + // Update the property selector to specify the nominal sample rate. + property_address.mSelector = kAudioDevicePropertyNominalSampleRate; + + // Setting the sample rate of a device is an asynchronous process in coreaudio. + // + // Thus we are required to set a `listener` so that we may be notified when the + // change occurs. + unsafe extern "C" fn rate_listener( + device_id: AudioObjectID, + _n_addresses: u32, + _properties: *const AudioObjectPropertyAddress, + rate_ptr: *mut ::std::os::raw::c_void, + ) -> OSStatus { + let rate_ptr: *const f64 = rate_ptr as *const _; + let data_size = mem::size_of::(); + let property_address = AudioObjectPropertyAddress { + mSelector: kAudioDevicePropertyNominalSampleRate, + mScope: kAudioObjectPropertyScopeGlobal, + mElement: kAudioObjectPropertyElementMaster, + }; + AudioObjectGetPropertyData( + device_id, + &property_address as *const _, + 0, + null(), + &data_size as *const _ as *mut _, + rate_ptr as *const _ as *mut _, + ) + } + + // Add our sample rate change listener callback. + let reported_rate: f64 = 0.0; + let status = AudioObjectAddPropertyListener( + device.audio_device_id, + &property_address as *const _, + Some(rate_listener), + &reported_rate as *const _ as *mut _, + ); + coreaudio::Error::from_os_status(status)?; + + // Finally, set the sample rate. + let sample_rate = sample_rate as f64; + let status = AudioObjectSetPropertyData( + device.audio_device_id, + &property_address as *const _, + 0, + null(), + data_size, + &ranges[range_index] as *const _ as *const _, + ); + coreaudio::Error::from_os_status(status)?; + + // Wait for the reported_rate to change. + // + // This should not take longer than a few ms, but we timeout after 1 sec just in case. + let timer = ::std::time::Instant::now(); + while sample_rate != reported_rate { + if timer.elapsed() > ::std::time::Duration::from_secs(1) { + panic!("timeout waiting for sample rate update for device"); + } + ::std::thread::sleep(::std::time::Duration::from_millis(5)); + } + + // Remove the `rate_listener` callback. + let status = AudioObjectRemovePropertyListener( + device.audio_device_id, + &property_address as *const _, + Some(rate_listener), + &reported_rate as *const _ as *mut _, + ); + coreaudio::Error::from_os_status(status)?; + } + } + + let mut audio_unit = audio_unit_from_device(device, true)?; + // Set the stream in interleaved mode. let asbd = asbd_from_format(format); audio_unit.set_property(kAudioUnitProperty_StreamFormat, scope, element, Some(&asbd))?; @@ -525,7 +681,7 @@ impl EventLoop { audio_unit.start()?; // Add the stream to the list of streams within `self`. - self.add_stream(stream_id, audio_unit); + self.add_stream(stream_id, audio_unit, device.audio_device_id); Ok(StreamId(stream_id)) } @@ -602,7 +758,7 @@ impl EventLoop { audio_unit.start()?; // Add the stream to the list of streams within `self`. - self.add_stream(stream_id, audio_unit); + self.add_stream(stream_id, audio_unit, device.audio_device_id); Ok(StreamId(stream_id)) }