Skip to content

Commit

Permalink
[coreaudio] Fix handling of non-default sample rates for input streams (
Browse files Browse the repository at this point in the history
RustAudio#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 RustAudio#213.

* Update CHANGELOG for coreaudio input stream sample rate fix

* Publish 0.8.1 for coreaudio input stream sample rate fix
  • Loading branch information
mitchmindtree authored and tomaka committed Apr 1, 2018
1 parent 06a0f0a commit a2fe938
Show file tree
Hide file tree
Showing 3 changed files with 168 additions and 8 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "cpal"
version = "0.8.0"
version = "0.8.1"
authors = ["The CPAL contributors", "Pierre Krieger <[email protected]>"]
description = "Low-level cross-platform audio playing library in pure Rust."
repository = "https://github.com/tomaka/cpal"
Expand Down
170 changes: 163 additions & 7 deletions src/coreaudio/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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();
Expand All @@ -463,12 +475,156 @@ impl EventLoop {
format: &Format,
) -> Result<StreamId, CreationError>
{
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::<f64>() 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::<AudioValueRange>();
let mut ranges: Vec<u8> = 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::<f64>();
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))?;
Expand Down Expand Up @@ -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))
}
Expand Down Expand Up @@ -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))
}
Expand Down

0 comments on commit a2fe938

Please sign in to comment.