diff options
author | Matt A. Tobin <email@mattatobin.com> | 2019-11-03 11:56:57 -0500 |
---|---|---|
committer | Matt A. Tobin <email@mattatobin.com> | 2019-11-03 11:56:57 -0500 |
commit | 2d4aca6d0036937afadfb93359d31fe3b4eabf84 (patch) | |
tree | c946f3a7500c91df93b1a25a38ade9411fe64462 /media/libcubeb/src/cubeb_audiounit.cpp | |
parent | 302bf1b523012e11b60425d6eee1221ebc2724eb (diff) | |
parent | 6ee3467a6cfa5a4bd5ca252e00e1b58c469a5011 (diff) | |
download | UXP-2d4aca6d0036937afadfb93359d31fe3b4eabf84.tar UXP-2d4aca6d0036937afadfb93359d31fe3b4eabf84.tar.gz UXP-2d4aca6d0036937afadfb93359d31fe3b4eabf84.tar.lz UXP-2d4aca6d0036937afadfb93359d31fe3b4eabf84.tar.xz UXP-2d4aca6d0036937afadfb93359d31fe3b4eabf84.zip |
Merge branch 'master' into mailnews-work
Diffstat (limited to 'media/libcubeb/src/cubeb_audiounit.cpp')
-rw-r--r-- | media/libcubeb/src/cubeb_audiounit.cpp | 2990 |
1 files changed, 1937 insertions, 1053 deletions
diff --git a/media/libcubeb/src/cubeb_audiounit.cpp b/media/libcubeb/src/cubeb_audiounit.cpp index 9483c2795..e0c8fc696 100644 --- a/media/libcubeb/src/cubeb_audiounit.cpp +++ b/media/libcubeb/src/cubeb_audiounit.cpp @@ -22,200 +22,243 @@ #include <AudioToolbox/AudioToolbox.h> #include "cubeb/cubeb.h" #include "cubeb-internal.h" -#include "cubeb_panner.h" +#include "cubeb_mixer.h" #if !TARGET_OS_IPHONE #include "cubeb_osx_run_loop.h" #endif #include "cubeb_resampler.h" #include "cubeb_ring_array.h" -#include "cubeb_utils.h" #include <algorithm> #include <atomic> +#include <vector> +#include <set> +#include <sys/time.h> +#include <string> -#if !defined(kCFCoreFoundationVersionNumber10_7) -/* From CoreFoundation CFBase.h */ -#define kCFCoreFoundationVersionNumber10_7 635.00 -#endif - -#if !TARGET_OS_IPHONE && MAC_OS_X_VERSION_MIN_REQUIRED < 1060 -#define AudioComponent Component -#define AudioComponentDescription ComponentDescription -#define AudioComponentFindNext FindNextComponent -#define AudioComponentInstanceNew OpenAComponent -#define AudioComponentInstanceDispose CloseComponent -#endif +using namespace std; #if MAC_OS_X_VERSION_MIN_REQUIRED < 101000 -typedef UInt32 AudioFormatFlags; +typedef UInt32 AudioFormatFlags; #endif -#define CUBEB_STREAM_MAX 8 - #define AU_OUT_BUS 0 #define AU_IN_BUS 1 -#define PRINT_ERROR_CODE(str, r) do { \ - LOG("System call failed: %s (rv: %d)", str, r); \ -} while(0) - const char * DISPATCH_QUEUE_LABEL = "org.mozilla.cubeb"; +const char * PRIVATE_AGGREGATE_DEVICE_NAME = "CubebAggregateDevice"; + +#ifdef ALOGV +#undef ALOGV +#endif +#define ALOGV(msg, ...) dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{LOGV(msg, ##__VA_ARGS__);}) + +#ifdef ALOG +#undef ALOG +#endif +#define ALOG(msg, ...) dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{LOG(msg, ##__VA_ARGS__);}) /* Testing empirically, some headsets report a minimal latency that is very * low, but this does not work in practice. Lie and say the minimum is 256 * frames. */ -const uint32_t SAFE_MIN_LATENCY_FRAMES = 256; +const uint32_t SAFE_MIN_LATENCY_FRAMES = 128; const uint32_t SAFE_MAX_LATENCY_FRAMES = 512; +const AudioObjectPropertyAddress DEFAULT_INPUT_DEVICE_PROPERTY_ADDRESS = { + kAudioHardwarePropertyDefaultInputDevice, + kAudioObjectPropertyScopeGlobal, + kAudioObjectPropertyElementMaster +}; + +const AudioObjectPropertyAddress DEFAULT_OUTPUT_DEVICE_PROPERTY_ADDRESS = { + kAudioHardwarePropertyDefaultOutputDevice, + kAudioObjectPropertyScopeGlobal, + kAudioObjectPropertyElementMaster +}; + +const AudioObjectPropertyAddress DEVICE_IS_ALIVE_PROPERTY_ADDRESS = { + kAudioDevicePropertyDeviceIsAlive, + kAudioObjectPropertyScopeGlobal, + kAudioObjectPropertyElementMaster +}; + +const AudioObjectPropertyAddress DEVICES_PROPERTY_ADDRESS = { + kAudioHardwarePropertyDevices, + kAudioObjectPropertyScopeGlobal, + kAudioObjectPropertyElementMaster +}; + +const AudioObjectPropertyAddress INPUT_DATA_SOURCE_PROPERTY_ADDRESS = { + kAudioDevicePropertyDataSource, + kAudioDevicePropertyScopeInput, + kAudioObjectPropertyElementMaster +}; + +const AudioObjectPropertyAddress OUTPUT_DATA_SOURCE_PROPERTY_ADDRESS = { + kAudioDevicePropertyDataSource, + kAudioDevicePropertyScopeOutput, + kAudioObjectPropertyElementMaster +}; + +typedef uint32_t device_flags_value; + +enum device_flags { + DEV_UNKNOWN = 0x00, /* Unknown */ + DEV_INPUT = 0x01, /* Record device like mic */ + DEV_OUTPUT = 0x02, /* Playback device like speakers */ + DEV_SYSTEM_DEFAULT = 0x04, /* System default device */ + DEV_SELECTED_DEFAULT = 0x08, /* User selected to use the system default device */ +}; + void audiounit_stream_stop_internal(cubeb_stream * stm); -void audiounit_stream_start_internal(cubeb_stream * stm); +static int audiounit_stream_start_internal(cubeb_stream * stm); static void audiounit_close_stream(cubeb_stream *stm); static int audiounit_setup_stream(cubeb_stream *stm); +static vector<AudioObjectID> +audiounit_get_devices_of_type(cubeb_device_type devtype); +static UInt32 audiounit_get_device_presentation_latency(AudioObjectID devid, AudioObjectPropertyScope scope); + +#if !TARGET_OS_IPHONE +static AudioObjectID audiounit_get_default_device_id(cubeb_device_type type); +static int audiounit_uninstall_device_changed_callback(cubeb_stream * stm); +static int audiounit_uninstall_system_changed_callback(cubeb_stream * stm); +static void audiounit_reinit_stream_async(cubeb_stream * stm, device_flags_value flags); +#endif extern cubeb_ops const audiounit_ops; struct cubeb { - cubeb_ops const * ops; + cubeb_ops const * ops = &audiounit_ops; owned_critical_section mutex; - std::atomic<int> active_streams; + int active_streams = 0; uint32_t global_latency_frames = 0; - int limit_streams; - cubeb_device_collection_changed_callback collection_changed_callback; - void * collection_changed_user_ptr; - /* Differentiate input from output devices. */ - cubeb_device_type collection_changed_devtype; - uint32_t devtype_device_count; - AudioObjectID * devtype_device_array; - // The queue is asynchronously deallocated once all references to it are released + cubeb_device_collection_changed_callback input_collection_changed_callback = nullptr; + void * input_collection_changed_user_ptr = nullptr; + cubeb_device_collection_changed_callback output_collection_changed_callback = nullptr; + void * output_collection_changed_user_ptr = nullptr; + // Store list of devices to detect changes + vector<AudioObjectID> input_device_array; + vector<AudioObjectID> output_device_array; + // The queue should be released when it’s no longer needed. dispatch_queue_t serial_queue = dispatch_queue_create(DISPATCH_QUEUE_LABEL, DISPATCH_QUEUE_SERIAL); + // Current used channel layout + atomic<cubeb_channel_layout> layout{ CUBEB_LAYOUT_UNDEFINED }; + uint32_t channels = 0; }; -class auto_array_wrapper +static unique_ptr<AudioChannelLayout, decltype(&free)> +make_sized_audio_channel_layout(size_t sz) { -public: - explicit auto_array_wrapper(auto_array<float> * ar) - : float_ar(ar) - , short_ar(nullptr) - {assert((float_ar && !short_ar) || (!float_ar && short_ar));} - - explicit auto_array_wrapper(auto_array<short> * ar) - : float_ar(nullptr) - , short_ar(ar) - {assert((float_ar && !short_ar) || (!float_ar && short_ar));} - - ~auto_array_wrapper() { - auto_lock l(lock); - assert((float_ar && !short_ar) || (!float_ar && short_ar)); - delete float_ar; - delete short_ar; - } - - void push(void * elements, size_t length){ - assert((float_ar && !short_ar) || (!float_ar && short_ar)); - auto_lock l(lock); - if (float_ar) - return float_ar->push(static_cast<float*>(elements), length); - return short_ar->push(static_cast<short*>(elements), length); - } - - size_t length() { - assert((float_ar && !short_ar) || (!float_ar && short_ar)); - auto_lock l(lock); - if (float_ar) - return float_ar->length(); - return short_ar->length(); - } - - void push_silence(size_t length) { - assert((float_ar && !short_ar) || (!float_ar && short_ar)); - auto_lock l(lock); - if (float_ar) - return float_ar->push_silence(length); - return short_ar->push_silence(length); - } + assert(sz >= sizeof(AudioChannelLayout)); + AudioChannelLayout * acl = reinterpret_cast<AudioChannelLayout *>(calloc(1, sz)); + assert(acl); // Assert the allocation works. + return unique_ptr<AudioChannelLayout, decltype(&free)>(acl, free); +} - bool pop(void * elements, size_t length) { - assert((float_ar && !short_ar) || (!float_ar && short_ar)); - auto_lock l(lock); - if (float_ar) - return float_ar->pop(static_cast<float*>(elements), length); - return short_ar->pop(static_cast<short*>(elements), length); - } +enum class io_side { + INPUT, + OUTPUT, +}; - void * data() { - assert((float_ar && !short_ar) || (!float_ar && short_ar)); - auto_lock l(lock); - if (float_ar) - return float_ar->data(); - return short_ar->data(); +static char const * +to_string(io_side side) +{ + switch (side) { + case io_side::INPUT: + return "input"; + case io_side::OUTPUT: + return "output"; } +} - void clear() { - assert((float_ar && !short_ar) || (!float_ar && short_ar)); - auto_lock l(lock); - if (float_ar) { - float_ar->clear(); - } else { - short_ar->clear(); - } - } +struct device_info { + AudioDeviceID id = kAudioObjectUnknown; + device_flags_value flags = DEV_UNKNOWN; +}; -private: - auto_array<float> * float_ar; - auto_array<short> * short_ar; - owned_critical_section lock; +struct property_listener { + AudioDeviceID device_id; + const AudioObjectPropertyAddress * property_address; + AudioObjectPropertyListenerProc callback; + cubeb_stream * stream; + + property_listener(AudioDeviceID id, + const AudioObjectPropertyAddress * address, + AudioObjectPropertyListenerProc proc, + cubeb_stream * stm) + : device_id(id) + , property_address(address) + , callback(proc) + , stream(stm) + {} }; struct cubeb_stream { + explicit cubeb_stream(cubeb * context); + + /* Note: Must match cubeb_stream layout in cubeb.c. */ cubeb * context; - cubeb_data_callback data_callback; - cubeb_state_callback state_callback; - cubeb_device_changed_callback device_changed_callback; + void * user_ptr = nullptr; + /**/ + + cubeb_data_callback data_callback = nullptr; + cubeb_state_callback state_callback = nullptr; + cubeb_device_changed_callback device_changed_callback = nullptr; + owned_critical_section device_changed_callback_lock; /* Stream creation parameters */ - cubeb_stream_params input_stream_params; - cubeb_stream_params output_stream_params; - cubeb_devid input_device; - bool is_default_input; - cubeb_devid output_device; - /* User pointer of data_callback */ - void * user_ptr; + cubeb_stream_params input_stream_params = { CUBEB_SAMPLE_FLOAT32NE, 0, 0, CUBEB_LAYOUT_UNDEFINED, CUBEB_STREAM_PREF_NONE }; + cubeb_stream_params output_stream_params = { CUBEB_SAMPLE_FLOAT32NE, 0, 0, CUBEB_LAYOUT_UNDEFINED, CUBEB_STREAM_PREF_NONE }; + device_info input_device; + device_info output_device; /* Format descriptions */ AudioStreamBasicDescription input_desc; AudioStreamBasicDescription output_desc; /* I/O AudioUnits */ - AudioUnit input_unit; - AudioUnit output_unit; + AudioUnit input_unit = nullptr; + AudioUnit output_unit = nullptr; /* I/O device sample rate */ - Float64 input_hw_rate; - Float64 output_hw_rate; + Float64 input_hw_rate = 0; + Float64 output_hw_rate = 0; /* Expected I/O thread interleave, * calculated from I/O hw rate. */ - int expected_output_callbacks_in_a_row; + int expected_output_callbacks_in_a_row = 0; owned_critical_section mutex; - /* Hold the input samples in every - * input callback iteration */ - auto_array_wrapper * input_linear_buffer; - /* Frames on input buffer */ - std::atomic<uint32_t> input_buffer_frames; + // Hold the input samples in every input callback iteration. + // Only accessed on input/output callback thread and during initial configure. + unique_ptr<auto_array_wrapper> input_linear_buffer; /* Frame counters */ - uint64_t frames_played; - uint64_t frames_queued; - std::atomic<int64_t> frames_read; - std::atomic<bool> shutdown; - std::atomic<bool> draining; + atomic<uint64_t> frames_played{ 0 }; + uint64_t frames_queued = 0; + // How many frames got read from the input since the stream started (includes + // padded silence) + atomic<int64_t> frames_read{ 0 }; + // How many frames got written to the output device since the stream started + atomic<int64_t> frames_written{ 0 }; + atomic<bool> shutdown{ true }; + atomic<bool> draining{ false }; + atomic<bool> reinit_pending { false }; + atomic<bool> destroy_pending{ false }; /* Latency requested by the user. */ - uint32_t latency_frames; - std::atomic<uint64_t> current_latency_frames; - uint64_t hw_latency_frames; - std::atomic<float> panning; - cubeb_resampler * resampler; - /* This is the number of output callback we got in a row. This is usually one, - * but can be two when the input and output rate are different, and more when - * a device has been plugged or unplugged, as there can be some time before - * the device is ready. */ - std::atomic<int> output_callback_in_a_row; + uint32_t latency_frames = 0; + atomic<uint32_t> current_latency_frames{ 0 }; + atomic<uint32_t> total_output_latency_frames { 0 }; + unique_ptr<cubeb_resampler, decltype(&cubeb_resampler_destroy)> resampler; /* This is true if a device change callback is currently running. */ - std::atomic<bool> switching_device; - std::atomic<bool> buffer_size_change_state{ false }; + atomic<bool> switching_device{ false }; + atomic<bool> buffer_size_change_state{ false }; + AudioDeviceID aggregate_device_id = kAudioObjectUnknown; // the aggregate device id + AudioObjectID plugin_id = kAudioObjectUnknown; // used to create aggregate device + /* Mixer interface */ + unique_ptr<cubeb_mixer, decltype(&cubeb_mixer_destroy)> mixer; + /* Buffer where remixing/resampling will occur when upmixing is required */ + /* Only accessed from callback thread */ + unique_ptr<uint8_t[]> temp_buffer; + size_t temp_buffer_size = 0; // size in bytes. + /* Listeners indicating what system events are monitored. */ + unique_ptr<property_listener> default_input_listener; + unique_ptr<property_listener> default_output_listener; + unique_ptr<property_listener> input_alive_listener; + unique_ptr<property_listener> input_source_listener; + unique_ptr<property_listener> output_source_listener; }; bool has_input(cubeb_stream * stm) @@ -228,14 +271,106 @@ bool has_output(cubeb_stream * stm) return stm->output_stream_params.rate != 0; } +cubeb_channel +channel_label_to_cubeb_channel(UInt32 label) +{ + switch (label) { + case kAudioChannelLabel_Left: + return CHANNEL_FRONT_LEFT; + case kAudioChannelLabel_Right: + return CHANNEL_FRONT_RIGHT; + case kAudioChannelLabel_Center: + return CHANNEL_FRONT_CENTER; + case kAudioChannelLabel_LFEScreen: + return CHANNEL_LOW_FREQUENCY; + case kAudioChannelLabel_LeftSurround: + return CHANNEL_BACK_LEFT; + case kAudioChannelLabel_RightSurround: + return CHANNEL_BACK_RIGHT; + case kAudioChannelLabel_LeftCenter: + return CHANNEL_FRONT_LEFT_OF_CENTER; + case kAudioChannelLabel_RightCenter: + return CHANNEL_FRONT_RIGHT_OF_CENTER; + case kAudioChannelLabel_CenterSurround: + return CHANNEL_BACK_CENTER; + case kAudioChannelLabel_LeftSurroundDirect: + return CHANNEL_SIDE_LEFT; + case kAudioChannelLabel_RightSurroundDirect: + return CHANNEL_SIDE_RIGHT; + case kAudioChannelLabel_TopCenterSurround: + return CHANNEL_TOP_CENTER; + case kAudioChannelLabel_VerticalHeightLeft: + return CHANNEL_TOP_FRONT_LEFT; + case kAudioChannelLabel_VerticalHeightCenter: + return CHANNEL_TOP_FRONT_CENTER; + case kAudioChannelLabel_VerticalHeightRight: + return CHANNEL_TOP_FRONT_RIGHT; + case kAudioChannelLabel_TopBackLeft: + return CHANNEL_TOP_BACK_LEFT; + case kAudioChannelLabel_TopBackCenter: + return CHANNEL_TOP_BACK_CENTER; + case kAudioChannelLabel_TopBackRight: + return CHANNEL_TOP_BACK_RIGHT; + default: + return CHANNEL_UNKNOWN; + } +} + +AudioChannelLabel +cubeb_channel_to_channel_label(cubeb_channel channel) +{ + switch (channel) { + case CHANNEL_FRONT_LEFT: + return kAudioChannelLabel_Left; + case CHANNEL_FRONT_RIGHT: + return kAudioChannelLabel_Right; + case CHANNEL_FRONT_CENTER: + return kAudioChannelLabel_Center; + case CHANNEL_LOW_FREQUENCY: + return kAudioChannelLabel_LFEScreen; + case CHANNEL_BACK_LEFT: + return kAudioChannelLabel_LeftSurround; + case CHANNEL_BACK_RIGHT: + return kAudioChannelLabel_RightSurround; + case CHANNEL_FRONT_LEFT_OF_CENTER: + return kAudioChannelLabel_LeftCenter; + case CHANNEL_FRONT_RIGHT_OF_CENTER: + return kAudioChannelLabel_RightCenter; + case CHANNEL_BACK_CENTER: + return kAudioChannelLabel_CenterSurround; + case CHANNEL_SIDE_LEFT: + return kAudioChannelLabel_LeftSurroundDirect; + case CHANNEL_SIDE_RIGHT: + return kAudioChannelLabel_RightSurroundDirect; + case CHANNEL_TOP_CENTER: + return kAudioChannelLabel_TopCenterSurround; + case CHANNEL_TOP_FRONT_LEFT: + return kAudioChannelLabel_VerticalHeightLeft; + case CHANNEL_TOP_FRONT_CENTER: + return kAudioChannelLabel_VerticalHeightCenter; + case CHANNEL_TOP_FRONT_RIGHT: + return kAudioChannelLabel_VerticalHeightRight; + case CHANNEL_TOP_BACK_LEFT: + return kAudioChannelLabel_TopBackLeft; + case CHANNEL_TOP_BACK_CENTER: + return kAudioChannelLabel_TopBackCenter; + case CHANNEL_TOP_BACK_RIGHT: + return kAudioChannelLabel_TopBackRight; + default: + return kAudioChannelLabel_Unknown; + } +} + #if TARGET_OS_IPHONE typedef UInt32 AudioDeviceID; typedef UInt32 AudioObjectID; #define AudioGetCurrentHostTime mach_absolute_time +#endif + uint64_t -AudioConvertHostTimeToNanos(uint64_t host_time) +ConvertHostTimeToNanos(uint64_t host_time) { static struct mach_timebase_info timebase_info; static bool initialized = false; @@ -251,27 +386,34 @@ AudioConvertHostTimeToNanos(uint64_t host_time) } return (uint64_t)answer; } -#endif -static int64_t -audiotimestamp_to_latency(AudioTimeStamp const * tstamp, cubeb_stream * stream) +static void +audiounit_increment_active_streams(cubeb * ctx) { - if (!(tstamp->mFlags & kAudioTimeStampHostTimeValid)) { - return 0; - } + ctx->mutex.assert_current_thread_owns(); + ctx->active_streams += 1; +} - uint64_t pres = AudioConvertHostTimeToNanos(tstamp->mHostTime); - uint64_t now = AudioConvertHostTimeToNanos(AudioGetCurrentHostTime()); +static void +audiounit_decrement_active_streams(cubeb * ctx) +{ + ctx->mutex.assert_current_thread_owns(); + ctx->active_streams -= 1; +} - return ((pres - now) * stream->output_desc.mSampleRate) / 1000000000LL; +static int +audiounit_active_streams(cubeb * ctx) +{ + ctx->mutex.assert_current_thread_owns(); + return ctx->active_streams; } static void -audiounit_set_global_latency(cubeb_stream * stm, uint32_t latency_frames) +audiounit_set_global_latency(cubeb * ctx, uint32_t latency_frames) { - stm->mutex.assert_current_thread_owns(); - assert(stm->context->active_streams == 1); - stm->context->global_latency_frames = latency_frames; + ctx->mutex.assert_current_thread_owns(); + assert(audiounit_active_streams(ctx) == 1); + ctx->global_latency_frames = latency_frames; } static void @@ -306,24 +448,38 @@ audiounit_render_input(cubeb_stream * stm, &input_buffer_list); if (r != noErr) { - PRINT_ERROR_CODE("AudioUnitRender", r); - return r; + LOG("AudioUnitRender rv=%d", r); + if (r != kAudioUnitErr_CannotDoInCurrentContext) { + return r; + } + if (stm->output_unit) { + // kAudioUnitErr_CannotDoInCurrentContext is returned when using a BT + // headset and the profile is changed from A2DP to HFP/HSP. The previous + // output device is no longer valid and must be reset. + audiounit_reinit_stream_async(stm, DEV_INPUT | DEV_OUTPUT); + } + // For now state that no error occurred and feed silence, stream will be + // resumed once reinit has completed. + ALOGV("(%p) input: reinit pending feeding silence instead", stm); + stm->input_linear_buffer->push_silence(input_frames * stm->input_desc.mChannelsPerFrame); + } else { + /* Copy input data in linear buffer. */ + stm->input_linear_buffer->push(input_buffer_list.mBuffers[0].mData, + input_frames * stm->input_desc.mChannelsPerFrame); } - /* Copy input data in linear buffer. */ - stm->input_linear_buffer->push(input_buffer_list.mBuffers[0].mData, - input_frames * stm->input_desc.mChannelsPerFrame); - - LOGV("(%p) input: buffers %d, size %d, channels %d, frames %d.", - stm, input_buffer_list.mNumberBuffers, - input_buffer_list.mBuffers[0].mDataByteSize, - input_buffer_list.mBuffers[0].mNumberChannels, - input_frames); - /* Advance input frame counter. */ assert(input_frames > 0); stm->frames_read += input_frames; + ALOGV("(%p) input: buffers %u, size %u, channels %u, rendered frames %d, total frames %lu.", + stm, + (unsigned int) input_buffer_list.mNumberBuffers, + (unsigned int) input_buffer_list.mBuffers[0].mDataByteSize, + (unsigned int) input_buffer_list.mBuffers[0].mNumberChannels, + (unsigned int) input_frames, + stm->input_linear_buffer->length() / stm->input_desc.mChannelsPerFrame); + return noErr; } @@ -336,26 +492,15 @@ audiounit_input_callback(void * user_ptr, AudioBufferList * /* bufs */) { cubeb_stream * stm = static_cast<cubeb_stream *>(user_ptr); - long outframes; assert(stm->input_unit != NULL); assert(AU_IN_BUS == bus); if (stm->shutdown) { - LOG("(%p) input shutdown", stm); + ALOG("(%p) input shutdown", stm); return noErr; } - // This happens when we're finally getting a new input callback after having - // switched device, we can clear the input buffer now, only keeping the data - // we just got. - if (stm->output_callback_in_a_row > stm->expected_output_callbacks_in_a_row) { - stm->input_linear_buffer->pop( - nullptr, - stm->input_linear_buffer->length() - - input_frames * stm->input_stream_params.channels); - } - OSStatus r = audiounit_render_input(stm, flags, tstamp, bus, input_frames); if (r != noErr) { return r; @@ -363,7 +508,6 @@ audiounit_input_callback(void * user_ptr, // Full Duplex. We'll call data_callback in the AudioUnit output callback. if (stm->output_unit != NULL) { - stm->output_callback_in_a_row = 0; return noErr; } @@ -371,41 +515,59 @@ audiounit_input_callback(void * user_ptr, Resampler will deliver input buffer in the correct rate. */ assert(input_frames <= stm->input_linear_buffer->length() / stm->input_desc.mChannelsPerFrame); long total_input_frames = stm->input_linear_buffer->length() / stm->input_desc.mChannelsPerFrame; - outframes = cubeb_resampler_fill(stm->resampler, - stm->input_linear_buffer->data(), - &total_input_frames, - NULL, - 0); - // Reset input buffer - stm->input_linear_buffer->clear(); - - if (outframes < 0 || outframes != input_frames) { - stm->shutdown = true; + long outframes = cubeb_resampler_fill(stm->resampler.get(), + stm->input_linear_buffer->data(), + &total_input_frames, + NULL, + 0); + if (outframes < total_input_frames) { + OSStatus r = AudioOutputUnitStop(stm->input_unit); + assert(r == 0); + stm->state_callback(stm, stm->user_ptr, CUBEB_STATE_DRAINED); return noErr; } + // Reset input buffer + stm->input_linear_buffer->clear(); + return noErr; } -static bool -is_extra_input_needed(cubeb_stream * stm) +static void +audiounit_mix_output_buffer(cubeb_stream * stm, + size_t output_frames, + void * input_buffer, + size_t input_buffer_size, + void * output_buffer, + size_t output_buffer_size) +{ + assert(input_buffer_size >= + cubeb_sample_size(stm->output_stream_params.format) * + stm->output_stream_params.channels * output_frames); + assert(output_buffer_size >= stm->output_desc.mBytesPerFrame * output_frames); + + int r = cubeb_mixer_mix(stm->mixer.get(), + output_frames, + input_buffer, + input_buffer_size, + output_buffer, + output_buffer_size); + if (r != 0) { + LOG("Remix error = %d", r); + } +} + +// Return how many input frames (sampled at input_hw_rate) are needed to provide +// output_frames (sampled at output_stream_params.rate) +static int64_t +minimum_resampling_input_frames(cubeb_stream * stm, uint32_t output_frames) { - /* If the output callback came first and this is a duplex stream, we need to - * fill in some additional silence in the resampler. - * Otherwise, if we had more than expected callbacks in a row, or we're currently - * switching, we add some silence as well to compensate for the fact that - * we're lacking some input data. */ - - /* If resampling is taking place after every output callback - * the input buffer expected to be empty. Any frame left over - * from resampling is stored inside the resampler available to - * be used in next iteration as needed. - * BUT when noop_resampler is operating we have left over - * frames since it does not store anything internally. */ - return stm->frames_read == 0 || - (stm->input_linear_buffer->length() == 0 && - (stm->output_callback_in_a_row > stm->expected_output_callbacks_in_a_row || - stm->switching_device)); + if (stm->input_hw_rate == stm->output_stream_params.rate) { + // Fast path. + return output_frames; + } + return ceil(stm->input_hw_rate * output_frames / + stm->output_stream_params.rate); } static OSStatus @@ -421,23 +583,32 @@ audiounit_output_callback(void * user_ptr, cubeb_stream * stm = static_cast<cubeb_stream *>(user_ptr); - stm->output_callback_in_a_row++; + uint64_t now = ConvertHostTimeToNanos(mach_absolute_time()); + uint64_t audio_output_time = ConvertHostTimeToNanos(tstamp->mHostTime); + uint64_t output_latency_ns = audio_output_time - now; - LOGV("(%p) output: buffers %d, size %d, channels %d, frames %d.", - stm, outBufferList->mNumberBuffers, - outBufferList->mBuffers[0].mDataByteSize, - outBufferList->mBuffers[0].mNumberChannels, output_frames); + const int ns2s = 1e9; + // The total output latency is the timestamp difference + the stream latency + + // the hardware latency. + stm->total_output_latency_frames = output_latency_ns * stm->output_hw_rate / ns2s + stm->current_latency_frames; - long outframes = 0, input_frames = 0; + ALOGV("(%p) output: buffers %u, size %u, channels %u, frames %u, total input frames %lu.", + stm, + (unsigned int) outBufferList->mNumberBuffers, + (unsigned int) outBufferList->mBuffers[0].mDataByteSize, + (unsigned int) outBufferList->mBuffers[0].mNumberChannels, + (unsigned int) output_frames, + has_input(stm) ? stm->input_linear_buffer->length() / stm->input_desc.mChannelsPerFrame : 0); + + long input_frames = 0; void * output_buffer = NULL, * input_buffer = NULL; if (stm->shutdown) { - LOG("(%p) output shutdown.", stm); + ALOG("(%p) output shutdown.", stm); audiounit_make_silent(&outBufferList->mBuffers[0]); return noErr; } - stm->current_latency_frames = audiotimestamp_to_latency(tstamp, stm); if (stm->draining) { OSStatus r = AudioOutputUnitStop(stm->output_unit); assert(r == 0); @@ -449,61 +620,98 @@ audiounit_output_callback(void * user_ptr, audiounit_make_silent(&outBufferList->mBuffers[0]); return noErr; } + /* Get output buffer. */ - output_buffer = outBufferList->mBuffers[0].mData; + if (stm->mixer) { + // If remixing needs to occur, we can't directly work in our final + // destination buffer as data may be overwritten or too small to start with. + size_t size_needed = output_frames * stm->output_stream_params.channels * + cubeb_sample_size(stm->output_stream_params.format); + if (stm->temp_buffer_size < size_needed) { + stm->temp_buffer.reset(new uint8_t[size_needed]); + stm->temp_buffer_size = size_needed; + } + output_buffer = stm->temp_buffer.get(); + } else { + output_buffer = outBufferList->mBuffers[0].mData; + } + + stm->frames_written += output_frames; + /* If Full duplex get also input buffer */ if (stm->input_unit != NULL) { - if (is_extra_input_needed(stm)) { - uint32_t min_input_frames_required = ceilf(stm->input_hw_rate / stm->output_hw_rate * - stm->input_buffer_frames); - stm->input_linear_buffer->push_silence(min_input_frames_required * stm->input_desc.mChannelsPerFrame); - LOG("(%p) %s pushed %u frames of input silence.", stm, stm->frames_read == 0 ? "Input hasn't started," : - stm->switching_device ? "Device switching," : "Drop out,", min_input_frames_required); - } - // The input buffer + /* If the output callback came first and this is a duplex stream, we need to + * fill in some additional silence in the resampler. + * Otherwise, if we had more than expected callbacks in a row, or we're + * currently switching, we add some silence as well to compensate for the + * fact that we're lacking some input data. */ + uint32_t input_frames_needed = + minimum_resampling_input_frames(stm, stm->frames_written); + long missing_frames = input_frames_needed - stm->frames_read; + if (missing_frames > 0) { + stm->input_linear_buffer->push_silence(missing_frames * stm->input_desc.mChannelsPerFrame); + stm->frames_read = input_frames_needed; + + ALOG("(%p) %s pushed %ld frames of input silence.", stm, stm->frames_read == 0 ? "Input hasn't started," : + stm->switching_device ? "Device switching," : "Drop out,", missing_frames); + } input_buffer = stm->input_linear_buffer->data(); - // Number of input frames in the buffer + // Number of input frames in the buffer. It will change to actually used frames + // inside fill input_frames = stm->input_linear_buffer->length() / stm->input_desc.mChannelsPerFrame; } /* Call user callback through resampler. */ - outframes = cubeb_resampler_fill(stm->resampler, - input_buffer, - input_buffer ? &input_frames : NULL, - output_buffer, - output_frames); + long outframes = cubeb_resampler_fill(stm->resampler.get(), + input_buffer, + input_buffer ? &input_frames : NULL, + output_buffer, + output_frames); if (input_buffer) { - stm->input_linear_buffer->pop(nullptr, input_frames * stm->input_desc.mChannelsPerFrame); + // Pop from the buffer the frames used by the the resampler. + stm->input_linear_buffer->pop(input_frames * stm->input_desc.mChannelsPerFrame); } - if (outframes < 0) { + if (outframes < 0 || outframes > output_frames) { stm->shutdown = true; + OSStatus r = AudioOutputUnitStop(stm->output_unit); + assert(r == 0); + if (stm->input_unit) { + r = AudioOutputUnitStop(stm->input_unit); + assert(r == 0); + } + stm->state_callback(stm, stm->user_ptr, CUBEB_STATE_ERROR); + audiounit_make_silent(&outBufferList->mBuffers[0]); return noErr; } - size_t outbpf = stm->output_desc.mBytesPerFrame; - stm->draining = outframes < output_frames; + stm->draining = (UInt32) outframes < output_frames; stm->frames_played = stm->frames_queued; stm->frames_queued += outframes; - AudioFormatFlags outaff = stm->output_desc.mFormatFlags; - float panning = (stm->output_desc.mChannelsPerFrame == 2) ? - stm->panning.load(std::memory_order_relaxed) : 0.0f; - /* Post process output samples. */ if (stm->draining) { /* Clear missing frames (silence) */ - memset((uint8_t*)output_buffer + outframes * outbpf, 0, (output_frames - outframes) * outbpf); + size_t channels = stm->output_stream_params.channels; + size_t missing_samples = (output_frames - outframes) * channels; + size_t size_sample = cubeb_sample_size(stm->output_stream_params.format); + /* number of bytes that have been filled with valid audio by the callback. */ + size_t audio_byte_count = outframes * channels * size_sample; + PodZero((uint8_t*)output_buffer + audio_byte_count, + missing_samples * size_sample); } - /* Pan stereo. */ - if (panning != 0.0f) { - if (outaff & kAudioFormatFlagIsFloat) { - cubeb_pan_stereo_buffer_float((float*)output_buffer, outframes, panning); - } else if (outaff & kAudioFormatFlagIsSignedInteger) { - cubeb_pan_stereo_buffer_int((short*)output_buffer, outframes, panning); - } + + /* Mixing */ + if (stm->mixer) { + audiounit_mix_output_buffer(stm, + output_frames, + output_buffer, + stm->temp_buffer_size, + outBufferList->mBuffers[0].mData, + outBufferList->mBuffers[0].mDataByteSize); } + return noErr; } @@ -511,25 +719,11 @@ extern "C" { int audiounit_init(cubeb ** context, char const * /* context_name */) { - cubeb * ctx; - - *context = NULL; - - ctx = (cubeb *)calloc(1, sizeof(cubeb)); - assert(ctx); - // Placement new to call the ctors of cubeb members. - new (ctx) cubeb(); - - ctx->ops = &audiounit_ops; - - ctx->active_streams = 0; - - ctx->limit_streams = kCFCoreFoundationVersionNumber < kCFCoreFoundationVersionNumber10_7; #if !TARGET_OS_IPHONE cubeb_set_coreaudio_notification_runloop(); #endif - *context = ctx; + *context = new cubeb; return CUBEB_OK; } @@ -542,146 +736,233 @@ audiounit_get_backend_id(cubeb * /* ctx */) } #if !TARGET_OS_IPHONE + +static int audiounit_stream_get_volume(cubeb_stream * stm, float * volume); +static int audiounit_stream_set_volume(cubeb_stream * stm, float volume); + static int -audiounit_get_output_device_id(AudioDeviceID * device_id) +audiounit_set_device_info(cubeb_stream * stm, AudioDeviceID id, io_side side) { - UInt32 size; - OSStatus r; - AudioObjectPropertyAddress output_device_address = { - kAudioHardwarePropertyDefaultOutputDevice, - kAudioObjectPropertyScopeGlobal, - kAudioObjectPropertyElementMaster - }; + assert(stm); - size = sizeof(*device_id); + device_info * info = nullptr; + cubeb_device_type type = CUBEB_DEVICE_TYPE_UNKNOWN; - r = AudioObjectGetPropertyData(kAudioObjectSystemObject, - &output_device_address, - 0, - NULL, - &size, - device_id); - if (r != noErr) { - PRINT_ERROR_CODE("output_device_id", r); - return CUBEB_ERROR; + if (side == io_side::INPUT) { + info = &stm->input_device; + type = CUBEB_DEVICE_TYPE_INPUT; + } else if (side == io_side::OUTPUT) { + info = &stm->output_device; + type = CUBEB_DEVICE_TYPE_OUTPUT; } + memset(info, 0, sizeof(device_info)); + info->id = id; - return CUBEB_OK; -} - -static int -audiounit_get_input_device_id(AudioDeviceID * device_id) -{ - UInt32 size; - OSStatus r; - AudioObjectPropertyAddress input_device_address = { - kAudioHardwarePropertyDefaultInputDevice, - kAudioObjectPropertyScopeGlobal, - kAudioObjectPropertyElementMaster - }; - - size = sizeof(*device_id); + if (side == io_side::INPUT) { + info->flags |= DEV_INPUT; + } else if (side == io_side::OUTPUT) { + info->flags |= DEV_OUTPUT; + } - r = AudioObjectGetPropertyData(kAudioObjectSystemObject, - &input_device_address, - 0, - NULL, - &size, - device_id); - if (r != noErr) { + AudioDeviceID default_device_id = audiounit_get_default_device_id(type); + if (default_device_id == kAudioObjectUnknown) { return CUBEB_ERROR; } + if (id == kAudioObjectUnknown) { + info->id = default_device_id; + info->flags |= DEV_SELECTED_DEFAULT; + } + + if (info->id == default_device_id) { + info->flags |= DEV_SYSTEM_DEFAULT; + } + + assert(info->id); + assert(info->flags & DEV_INPUT && !(info->flags & DEV_OUTPUT) || + !(info->flags & DEV_INPUT) && info->flags & DEV_OUTPUT); return CUBEB_OK; } -static int audiounit_stream_get_volume(cubeb_stream * stm, float * volume); -static int audiounit_stream_set_volume(cubeb_stream * stm, float volume); -static int audiounit_uninstall_device_changed_callback(cubeb_stream * stm); static int -audiounit_reinit_stream(cubeb_stream * stm) +audiounit_reinit_stream(cubeb_stream * stm, device_flags_value flags) { auto_lock context_lock(stm->context->mutex); + assert((flags & DEV_INPUT && stm->input_unit) || + (flags & DEV_OUTPUT && stm->output_unit)); if (!stm->shutdown) { audiounit_stream_stop_internal(stm); } int r = audiounit_uninstall_device_changed_callback(stm); if (r != CUBEB_OK) { - LOG("(%p) Could not uninstall the device changed callback", stm); + LOG("(%p) Could not uninstall all device change listeners.", stm); } { auto_lock lock(stm->mutex); float volume = 0.0; - int vol_rv = audiounit_stream_get_volume(stm, &volume); + int vol_rv = CUBEB_ERROR; + if (stm->output_unit) { + vol_rv = audiounit_stream_get_volume(stm, &volume); + } audiounit_close_stream(stm); + /* Reinit occurs in one of the following case: + * - When the device is not alive any more + * - When the default system device change. + * - The bluetooth device changed from A2DP to/from HFP/HSP profile + * We first attempt to re-use the same device id, should that fail we will + * default to the (potentially new) default device. */ + AudioDeviceID input_device = flags & DEV_INPUT ? stm->input_device.id : kAudioObjectUnknown; + if (flags & DEV_INPUT) { + r = audiounit_set_device_info(stm, input_device, io_side::INPUT); + if (r != CUBEB_OK) { + LOG("(%p) Set input device info failed. This can happen when last media device is unplugged", stm); + return CUBEB_ERROR; + } + } + + /* Always use the default output on reinit. This is not correct in every + * case but it is sufficient for Firefox and prevent reinit from reporting + * failures. It will change soon when reinit mechanism will be updated. */ + r = audiounit_set_device_info(stm, kAudioObjectUnknown, io_side::OUTPUT); + if (r != CUBEB_OK) { + LOG("(%p) Set output device info failed. This can happen when last media device is unplugged", stm); + return CUBEB_ERROR; + } + if (audiounit_setup_stream(stm) != CUBEB_OK) { LOG("(%p) Stream reinit failed.", stm); - return CUBEB_ERROR; + if (flags & DEV_INPUT && input_device != kAudioObjectUnknown) { + // Attempt to re-use the same device-id failed, so attempt again with + // default input device. + audiounit_close_stream(stm); + if (audiounit_set_device_info(stm, kAudioObjectUnknown, io_side::INPUT) != CUBEB_OK || + audiounit_setup_stream(stm) != CUBEB_OK) { + LOG("(%p) Second stream reinit failed.", stm); + return CUBEB_ERROR; + } + } } if (vol_rv == CUBEB_OK) { audiounit_stream_set_volume(stm, volume); } - // Reset input frames to force new stream pre-buffer - // silence if needed, check `is_extra_input_needed()` - stm->frames_read = 0; - // If the stream was running, start it again. if (!stm->shutdown) { - audiounit_stream_start_internal(stm); + r = audiounit_stream_start_internal(stm); + if (r != CUBEB_OK) { + return CUBEB_ERROR; + } } } return CUBEB_OK; } +static void +audiounit_reinit_stream_async(cubeb_stream * stm, device_flags_value flags) +{ + if (std::atomic_exchange(&stm->reinit_pending, true)) { + // A reinit task is already pending, nothing more to do. + ALOG("(%p) re-init stream task already pending, cancelling request", stm); + return; + } + + // Use a new thread, through the queue, to avoid deadlock when calling + // Get/SetProperties method from inside notify callback + dispatch_async(stm->context->serial_queue, ^() { + if (stm->destroy_pending) { + ALOG("(%p) stream pending destroy, cancelling reinit task", stm); + return; + } + + if (audiounit_reinit_stream(stm, flags) != CUBEB_OK) { + if (audiounit_uninstall_system_changed_callback(stm) != CUBEB_OK) { + LOG("(%p) Could not uninstall system changed callback", stm); + } + stm->state_callback(stm, stm->user_ptr, CUBEB_STATE_ERROR); + LOG("(%p) Could not reopen the stream after switching.", stm); + } + stm->switching_device = false; + stm->reinit_pending = false; + }); +} + +static char const * +event_addr_to_string(AudioObjectPropertySelector selector) +{ + switch(selector) { + case kAudioHardwarePropertyDefaultOutputDevice: + return "kAudioHardwarePropertyDefaultOutputDevice"; + case kAudioHardwarePropertyDefaultInputDevice: + return "kAudioHardwarePropertyDefaultInputDevice"; + case kAudioDevicePropertyDeviceIsAlive: + return "kAudioDevicePropertyDeviceIsAlive"; + case kAudioDevicePropertyDataSource: + return "kAudioDevicePropertyDataSource"; + default: + return "Unknown"; + } +} + static OSStatus -audiounit_property_listener_callback(AudioObjectID /* id */, UInt32 address_count, +audiounit_property_listener_callback(AudioObjectID id, UInt32 address_count, const AudioObjectPropertyAddress * addresses, void * user) { cubeb_stream * stm = (cubeb_stream*) user; + if (stm->switching_device) { + LOG("Switching is already taking place. Skip Event %s for id=%d", event_addr_to_string(addresses[0].mSelector), id); + return noErr; + } stm->switching_device = true; - LOG("(%p) Audio device changed, %d events.", stm, address_count); + LOG("(%p) Audio device changed, %u events.", stm, (unsigned int) address_count); for (UInt32 i = 0; i < address_count; i++) { switch(addresses[i].mSelector) { case kAudioHardwarePropertyDefaultOutputDevice: { - LOG("Event[%d] - mSelector == kAudioHardwarePropertyDefaultOutputDevice", i); - // Allow restart to choose the new default - stm->output_device = nullptr; + LOG("Event[%u] - mSelector == kAudioHardwarePropertyDefaultOutputDevice for id=%d", (unsigned int) i, id); } break; case kAudioHardwarePropertyDefaultInputDevice: { - LOG("Event[%d] - mSelector == kAudioHardwarePropertyDefaultInputDevice", i); - // Allow restart to choose the new default - stm->input_device = nullptr; + LOG("Event[%u] - mSelector == kAudioHardwarePropertyDefaultInputDevice for id=%d", (unsigned int) i, id); } break; case kAudioDevicePropertyDeviceIsAlive: { - LOG("Event[%d] - mSelector == kAudioDevicePropertyDeviceIsAlive", i); + LOG("Event[%u] - mSelector == kAudioDevicePropertyDeviceIsAlive for id=%d", (unsigned int) i, id); // If this is the default input device ignore the event, // kAudioHardwarePropertyDefaultInputDevice will take care of the switch - if (stm->is_default_input) { + if (stm->input_device.flags & DEV_SYSTEM_DEFAULT) { LOG("It's the default input device, ignore the event"); + stm->switching_device = false; return noErr; } - // Allow restart to choose the new default. Event register only for input. - stm->input_device = nullptr; } break; case kAudioDevicePropertyDataSource: { - LOG("Event[%d] - mSelector == kAudioHardwarePropertyDataSource", i); - return noErr; + LOG("Event[%u] - mSelector == kAudioDevicePropertyDataSource for id=%d", (unsigned int) i, id); } + break; + default: + LOG("Event[%u] - mSelector == Unexpected Event id %d, return", (unsigned int) i, addresses[i].mSelector); + stm->switching_device = false; + return noErr; } } + // Allow restart to choose the new default + device_flags_value switch_side = DEV_UNKNOWN; + if (has_input(stm)) { + switch_side |= DEV_INPUT; + } + if (has_output(stm)) { + switch_side |= DEV_OUTPUT; + } + for (UInt32 i = 0; i < address_count; i++) { switch(addresses[i].mSelector) { case kAudioHardwarePropertyDefaultOutputDevice: @@ -689,7 +970,7 @@ audiounit_property_listener_callback(AudioObjectID /* id */, UInt32 address_coun case kAudioDevicePropertyDeviceIsAlive: /* fall through */ case kAudioDevicePropertyDataSource: { - auto_lock lock(stm->mutex); + auto_lock dev_cb_lock(stm->device_changed_callback_lock); if (stm->device_changed_callback) { stm->device_changed_callback(stm->user_ptr); } @@ -698,99 +979,77 @@ audiounit_property_listener_callback(AudioObjectID /* id */, UInt32 address_coun } } - // Use a new thread, through the queue, to avoid deadlock when calling - // Get/SetProperties method from inside notify callback - dispatch_async(stm->context->serial_queue, ^() { - if (audiounit_reinit_stream(stm) != CUBEB_OK) { - stm->state_callback(stm, stm->user_ptr, CUBEB_STATE_STOPPED); - LOG("(%p) Could not reopen the stream after switching.", stm); - } - stm->switching_device = false; - }); + audiounit_reinit_stream_async(stm, switch_side); return noErr; } OSStatus -audiounit_add_listener(cubeb_stream * stm, AudioDeviceID id, AudioObjectPropertySelector selector, - AudioObjectPropertyScope scope, AudioObjectPropertyListenerProc listener) +audiounit_add_listener(const property_listener * listener) { - AudioObjectPropertyAddress address = { - selector, - scope, - kAudioObjectPropertyElementMaster - }; - - return AudioObjectAddPropertyListener(id, &address, listener, stm); + assert(listener); + return AudioObjectAddPropertyListener(listener->device_id, + listener->property_address, + listener->callback, + listener->stream); } OSStatus -audiounit_remove_listener(cubeb_stream * stm, AudioDeviceID id, - AudioObjectPropertySelector selector, - AudioObjectPropertyScope scope, - AudioObjectPropertyListenerProc listener) -{ - AudioObjectPropertyAddress address = { - selector, - scope, - kAudioObjectPropertyElementMaster - }; - - return AudioObjectRemovePropertyListener(id, &address, listener, stm); +audiounit_remove_listener(const property_listener * listener) +{ + assert(listener); + return AudioObjectRemovePropertyListener(listener->device_id, + listener->property_address, + listener->callback, + listener->stream); } -static AudioObjectID audiounit_get_default_device_id(cubeb_device_type type); - static int audiounit_install_device_changed_callback(cubeb_stream * stm) { - OSStatus r; + OSStatus rv; + int r = CUBEB_OK; if (stm->output_unit) { /* This event will notify us when the data source on the same device changes, * for example when the user plugs in a normal (non-usb) headset in the * headphone jack. */ - AudioDeviceID output_dev_id; - r = audiounit_get_output_device_id(&output_dev_id); - if (r != noErr) { - return CUBEB_ERROR; - } - - r = audiounit_add_listener(stm, output_dev_id, kAudioDevicePropertyDataSource, - kAudioDevicePropertyScopeOutput, &audiounit_property_listener_callback); - if (r != noErr) { - PRINT_ERROR_CODE("AudioObjectAddPropertyListener/output/kAudioDevicePropertyDataSource", r); - return CUBEB_ERROR; + stm->output_source_listener.reset(new property_listener( + stm->output_device.id, &OUTPUT_DATA_SOURCE_PROPERTY_ADDRESS, + &audiounit_property_listener_callback, stm)); + rv = audiounit_add_listener(stm->output_source_listener.get()); + if (rv != noErr) { + stm->output_source_listener.reset(); + LOG("AudioObjectAddPropertyListener/output/kAudioDevicePropertyDataSource rv=%d, device id=%d", rv, stm->output_device.id); + r = CUBEB_ERROR; } } if (stm->input_unit) { /* This event will notify us when the data source on the input device changes. */ - AudioDeviceID input_dev_id; - r = audiounit_get_input_device_id(&input_dev_id); - if (r != noErr) { - return CUBEB_ERROR; - } - - r = audiounit_add_listener(stm, input_dev_id, kAudioDevicePropertyDataSource, - kAudioDevicePropertyScopeInput, &audiounit_property_listener_callback); - if (r != noErr) { - PRINT_ERROR_CODE("AudioObjectAddPropertyListener/input/kAudioDevicePropertyDataSource", r); - return CUBEB_ERROR; + stm->input_source_listener.reset(new property_listener( + stm->input_device.id, &INPUT_DATA_SOURCE_PROPERTY_ADDRESS, + &audiounit_property_listener_callback, stm)); + rv = audiounit_add_listener(stm->input_source_listener.get()); + if (rv != noErr) { + stm->input_source_listener.reset(); + LOG("AudioObjectAddPropertyListener/input/kAudioDevicePropertyDataSource rv=%d, device id=%d", rv, stm->input_device.id); + r = CUBEB_ERROR; } /* Event to notify when the input is going away. */ - AudioDeviceID dev = stm->input_device ? reinterpret_cast<intptr_t>(stm->input_device) : - audiounit_get_default_device_id(CUBEB_DEVICE_TYPE_INPUT); - r = audiounit_add_listener(stm, dev, kAudioDevicePropertyDeviceIsAlive, - kAudioObjectPropertyScopeGlobal, &audiounit_property_listener_callback); - if (r != noErr) { - PRINT_ERROR_CODE("AudioObjectAddPropertyListener/input/kAudioDevicePropertyDeviceIsAlive", r); - return CUBEB_ERROR; + stm->input_alive_listener.reset(new property_listener( + stm->input_device.id, &DEVICE_IS_ALIVE_PROPERTY_ADDRESS, + &audiounit_property_listener_callback, stm)); + rv = audiounit_add_listener(stm->input_alive_listener.get()); + if (rv != noErr) { + stm->input_alive_listener.reset(); + LOG("AudioObjectAddPropertyListener/input/kAudioDevicePropertyDeviceIsAlive rv=%d, device id =%d", rv, stm->input_device.id); + r = CUBEB_ERROR; } } - return CUBEB_OK; + return r; } static int @@ -803,9 +1062,12 @@ audiounit_install_system_changed_callback(cubeb_stream * stm) * for example when the user plugs in a USB headset and the system chooses it * automatically as the default, or when another device is chosen in the * dropdown list. */ - r = audiounit_add_listener(stm, kAudioObjectSystemObject, kAudioHardwarePropertyDefaultOutputDevice, - kAudioObjectPropertyScopeGlobal, &audiounit_property_listener_callback); + stm->default_output_listener.reset(new property_listener( + kAudioObjectSystemObject, &DEFAULT_OUTPUT_DEVICE_PROPERTY_ADDRESS, + &audiounit_property_listener_callback, stm)); + r = audiounit_add_listener(stm->default_output_listener.get()); if (r != noErr) { + stm->default_output_listener.reset(); LOG("AudioObjectAddPropertyListener/output/kAudioHardwarePropertyDefaultOutputDevice rv=%d", r); return CUBEB_ERROR; } @@ -813,9 +1075,12 @@ audiounit_install_system_changed_callback(cubeb_stream * stm) if (stm->input_unit) { /* This event will notify us when the default input device changes. */ - r = audiounit_add_listener(stm, kAudioObjectSystemObject, kAudioHardwarePropertyDefaultInputDevice, - kAudioObjectPropertyScopeGlobal, &audiounit_property_listener_callback); + stm->default_input_listener.reset(new property_listener( + kAudioObjectSystemObject, &DEFAULT_INPUT_DEVICE_PROPERTY_ADDRESS, + &audiounit_property_listener_callback, stm)); + r = audiounit_add_listener(stm->default_input_listener.get()); if (r != noErr) { + stm->default_input_listener.reset(); LOG("AudioObjectAddPropertyListener/input/kAudioHardwarePropertyDefaultInputDevice rv=%d", r); return CUBEB_ERROR; } @@ -827,36 +1092,38 @@ audiounit_install_system_changed_callback(cubeb_stream * stm) static int audiounit_uninstall_device_changed_callback(cubeb_stream * stm) { - OSStatus r; - - if (stm->output_unit) { - AudioDeviceID output_dev_id; - r = audiounit_get_output_device_id(&output_dev_id); - if (r != noErr) { - return CUBEB_ERROR; - } + OSStatus rv; + // Failing to uninstall listeners is not a fatal error. + int r = CUBEB_OK; - r = audiounit_remove_listener(stm, output_dev_id, kAudioDevicePropertyDataSource, - kAudioDevicePropertyScopeOutput, &audiounit_property_listener_callback); - if (r != noErr) { - return CUBEB_ERROR; + if (stm->output_source_listener) { + rv = audiounit_remove_listener(stm->output_source_listener.get()); + if (rv != noErr) { + LOG("AudioObjectRemovePropertyListener/output/kAudioDevicePropertyDataSource rv=%d, device id=%d", rv, stm->output_device.id); + r = CUBEB_ERROR; } + stm->output_source_listener.reset(); } - if (stm->input_unit) { - AudioDeviceID input_dev_id; - r = audiounit_get_input_device_id(&input_dev_id); - if (r != noErr) { - return CUBEB_ERROR; + if (stm->input_source_listener) { + rv = audiounit_remove_listener(stm->input_source_listener.get()); + if (rv != noErr) { + LOG("AudioObjectRemovePropertyListener/input/kAudioDevicePropertyDataSource rv=%d, device id=%d", rv, stm->input_device.id); + r = CUBEB_ERROR; } + stm->input_source_listener.reset(); + } - r = audiounit_remove_listener(stm, input_dev_id, kAudioDevicePropertyDataSource, - kAudioDevicePropertyScopeInput, &audiounit_property_listener_callback); - if (r != noErr) { - return CUBEB_ERROR; + if (stm->input_alive_listener) { + rv = audiounit_remove_listener(stm->input_alive_listener.get()); + if (rv != noErr) { + LOG("AudioObjectRemovePropertyListener/input/kAudioDevicePropertyDeviceIsAlive rv=%d, device id=%d", rv, stm->input_device.id); + r = CUBEB_ERROR; } + stm->input_alive_listener.reset(); } - return CUBEB_OK; + + return r; } static int @@ -864,20 +1131,20 @@ audiounit_uninstall_system_changed_callback(cubeb_stream * stm) { OSStatus r; - if (stm->output_unit) { - r = audiounit_remove_listener(stm, kAudioObjectSystemObject, kAudioHardwarePropertyDefaultOutputDevice, - kAudioObjectPropertyScopeGlobal, &audiounit_property_listener_callback); + if (stm->default_output_listener) { + r = audiounit_remove_listener(stm->default_output_listener.get()); if (r != noErr) { return CUBEB_ERROR; } + stm->default_output_listener.reset(); } - if (stm->input_unit) { - r = audiounit_remove_listener(stm, kAudioObjectSystemObject, kAudioHardwarePropertyDefaultInputDevice, - kAudioObjectPropertyScopeGlobal, &audiounit_property_listener_callback); + if (stm->default_input_listener) { + r = audiounit_remove_listener(stm->default_input_listener.get()); if (r != noErr) { return CUBEB_ERROR; } + stm->default_input_listener.reset(); } return CUBEB_OK; } @@ -895,7 +1162,8 @@ audiounit_get_acceptable_latency_range(AudioValueRange * latency_range) kAudioObjectPropertyElementMaster }; - if (audiounit_get_output_device_id(&output_device_id) != CUBEB_OK) { + output_device_id = audiounit_get_default_device_id(CUBEB_DEVICE_TYPE_OUTPUT); + if (output_device_id == kAudioObjectUnknown) { LOG("Could not get default output device id."); return CUBEB_ERROR; } @@ -910,7 +1178,7 @@ audiounit_get_acceptable_latency_range(AudioValueRange * latency_range) &size, latency_range); if (r != noErr) { - PRINT_ERROR_CODE("AudioObjectGetPropertyData/buffer size range", r); + LOG("AudioObjectGetPropertyData/buffer size range rv=%d", r); return CUBEB_ERROR; } @@ -921,20 +1189,19 @@ audiounit_get_acceptable_latency_range(AudioValueRange * latency_range) static AudioObjectID audiounit_get_default_device_id(cubeb_device_type type) { - AudioObjectPropertyAddress adr = { 0, kAudioObjectPropertyScopeGlobal, kAudioObjectPropertyElementMaster }; - AudioDeviceID devid; - UInt32 size; - + const AudioObjectPropertyAddress * adr; if (type == CUBEB_DEVICE_TYPE_OUTPUT) { - adr.mSelector = kAudioHardwarePropertyDefaultOutputDevice; + adr = &DEFAULT_OUTPUT_DEVICE_PROPERTY_ADDRESS; } else if (type == CUBEB_DEVICE_TYPE_INPUT) { - adr.mSelector = kAudioHardwarePropertyDefaultInputDevice; + adr = &DEFAULT_INPUT_DEVICE_PROPERTY_ADDRESS; } else { return kAudioObjectUnknown; } - size = sizeof(AudioDeviceID); - if (AudioObjectGetPropertyData(kAudioObjectSystemObject, &adr, 0, NULL, &size, &devid) != noErr) { + AudioDeviceID devid; + UInt32 size = sizeof(AudioDeviceID); + if (AudioObjectGetPropertyData(kAudioObjectSystemObject, + adr, 0, NULL, &size, &devid) != noErr) { return kAudioObjectUnknown; } @@ -960,7 +1227,8 @@ audiounit_get_max_channel_count(cubeb * ctx, uint32_t * max_channels) assert(ctx && max_channels); - if (audiounit_get_output_device_id(&output_device_id) != CUBEB_OK) { + output_device_id = audiounit_get_default_device_id(CUBEB_DEVICE_TYPE_OUTPUT); + if (output_device_id == kAudioObjectUnknown) { return CUBEB_ERROR; } @@ -973,7 +1241,7 @@ audiounit_get_max_channel_count(cubeb * ctx, uint32_t * max_channels) &size, &stream_format); if (r != noErr) { - PRINT_ERROR_CODE("AudioObjectPropertyAddress/StreamFormat", r); + LOG("AudioObjectPropertyAddress/StreamFormat rv=%d", r); return CUBEB_ERROR; } @@ -997,7 +1265,7 @@ audiounit_get_min_latency(cubeb * /* ctx */, return CUBEB_ERROR; } - *latency_frames = std::max<uint32_t>(latency_range.mMinimum, + *latency_frames = max<uint32_t>(latency_range.mMinimum, SAFE_MIN_LATENCY_FRAMES); #endif @@ -1021,7 +1289,8 @@ audiounit_get_preferred_sample_rate(cubeb * /* ctx */, uint32_t * rate) kAudioObjectPropertyElementMaster }; - if (audiounit_get_output_device_id(&output_device_id) != CUBEB_OK) { + output_device_id = audiounit_get_default_device_id(CUBEB_DEVICE_TYPE_OUTPUT); + if (output_device_id == kAudioObjectUnknown) { return CUBEB_ERROR; } @@ -1042,24 +1311,133 @@ audiounit_get_preferred_sample_rate(cubeb * /* ctx */, uint32_t * rate) return CUBEB_OK; } -static OSStatus audiounit_remove_device_listener(cubeb * context); +static cubeb_channel_layout +audiounit_convert_channel_layout(AudioChannelLayout * layout) +{ + // When having one or two channel, force mono or stereo. Some devices (namely, + // Bose QC35, mark 1 and 2), expose a single channel mapped to the right for + // some reason. + if (layout->mNumberChannelDescriptions == 1) { + return CUBEB_LAYOUT_MONO; + } else if (layout->mNumberChannelDescriptions == 2) { + return CUBEB_LAYOUT_STEREO; + } + + if (layout->mChannelLayoutTag != kAudioChannelLayoutTag_UseChannelDescriptions) { + // kAudioChannelLayoutTag_UseChannelBitmap + // kAudioChannelLayoutTag_Mono + // kAudioChannelLayoutTag_Stereo + // .... + LOG("Only handle UseChannelDescriptions for now.\n"); + return CUBEB_LAYOUT_UNDEFINED; + } + + cubeb_channel_layout cl = 0; + for (UInt32 i = 0; i < layout->mNumberChannelDescriptions; ++i) { + cubeb_channel cc = channel_label_to_cubeb_channel( + layout->mChannelDescriptions[i].mChannelLabel); + if (cc == CHANNEL_UNKNOWN) { + return CUBEB_LAYOUT_UNDEFINED; + } + cl |= cc; + } + + return cl; +} + +static cubeb_channel_layout +audiounit_get_preferred_channel_layout(AudioUnit output_unit) +{ + OSStatus rv = noErr; + UInt32 size = 0; + rv = AudioUnitGetPropertyInfo(output_unit, + kAudioDevicePropertyPreferredChannelLayout, + kAudioUnitScope_Output, + AU_OUT_BUS, + &size, + nullptr); + if (rv != noErr) { + LOG("AudioUnitGetPropertyInfo/kAudioDevicePropertyPreferredChannelLayout rv=%d", rv); + return CUBEB_LAYOUT_UNDEFINED; + } + assert(size > 0); + + auto layout = make_sized_audio_channel_layout(size); + rv = AudioUnitGetProperty(output_unit, + kAudioDevicePropertyPreferredChannelLayout, + kAudioUnitScope_Output, + AU_OUT_BUS, + layout.get(), + &size); + if (rv != noErr) { + LOG("AudioUnitGetProperty/kAudioDevicePropertyPreferredChannelLayout rv=%d", rv); + return CUBEB_LAYOUT_UNDEFINED; + } + + return audiounit_convert_channel_layout(layout.get()); +} + +static cubeb_channel_layout +audiounit_get_current_channel_layout(AudioUnit output_unit) +{ + OSStatus rv = noErr; + UInt32 size = 0; + rv = AudioUnitGetPropertyInfo(output_unit, + kAudioUnitProperty_AudioChannelLayout, + kAudioUnitScope_Output, + AU_OUT_BUS, + &size, + nullptr); + if (rv != noErr) { + LOG("AudioUnitGetPropertyInfo/kAudioUnitProperty_AudioChannelLayout rv=%d", rv); + // This property isn't known before macOS 10.12, attempt another method. + return audiounit_get_preferred_channel_layout(output_unit); + } + assert(size > 0); + + auto layout = make_sized_audio_channel_layout(size); + rv = AudioUnitGetProperty(output_unit, + kAudioUnitProperty_AudioChannelLayout, + kAudioUnitScope_Output, + AU_OUT_BUS, + layout.get(), + &size); + if (rv != noErr) { + LOG("AudioUnitGetProperty/kAudioUnitProperty_AudioChannelLayout rv=%d", rv); + return CUBEB_LAYOUT_UNDEFINED; + } + + return audiounit_convert_channel_layout(layout.get()); +} + +static int audiounit_create_unit(AudioUnit * unit, device_info * device); + +static OSStatus audiounit_remove_device_listener(cubeb * context, cubeb_device_type devtype); static void audiounit_destroy(cubeb * ctx) { - // Disabling this assert for bug 1083664 -- we seem to leak a stream - // assert(ctx->active_streams == 0); - { auto_lock lock(ctx->mutex); + + // Disabling this assert for bug 1083664 -- we seem to leak a stream + // assert(ctx->active_streams == 0); + if (audiounit_active_streams(ctx) > 0) { + LOG("(%p) API misuse, %d streams active when context destroyed!", ctx, audiounit_active_streams(ctx)); + } + /* Unregister the callback if necessary. */ - if(ctx->collection_changed_callback) { - audiounit_remove_device_listener(ctx); + if (ctx->input_collection_changed_callback) { + audiounit_remove_device_listener(ctx, CUBEB_DEVICE_TYPE_INPUT); + } + if (ctx->output_collection_changed_callback) { + audiounit_remove_device_listener(ctx, CUBEB_DEVICE_TYPE_OUTPUT); } } - ctx->~cubeb(); - free(ctx); + dispatch_release(ctx->serial_queue); + + delete ctx; } static void audiounit_stream_destroy(cubeb_stream * stm); @@ -1105,29 +1483,527 @@ audio_stream_desc_init(AudioStreamBasicDescription * ss, return CUBEB_OK; } +void +audiounit_init_mixer(cubeb_stream * stm) +{ + // We can't rely on macOS' AudioUnit to properly downmix (or upmix) the audio + // data, it silently drop the channels so we need to remix the + // audio data by ourselves to keep all the information. + stm->mixer.reset(cubeb_mixer_create(stm->output_stream_params.format, + stm->output_stream_params.channels, + stm->output_stream_params.layout, + stm->context->channels, + stm->context->layout)); + assert(stm->mixer); +} + static int -audiounit_create_unit(AudioUnit * unit, - bool is_input, - const cubeb_stream_params * /* stream_params */, - cubeb_devid device) +audiounit_set_channel_layout(AudioUnit unit, + io_side side, + cubeb_channel_layout layout) +{ + if (side != io_side::OUTPUT) { + return CUBEB_ERROR; + } + + if (layout == CUBEB_LAYOUT_UNDEFINED) { + // We leave everything as-is... + return CUBEB_OK; + } + + + OSStatus r; + uint32_t nb_channels = cubeb_channel_layout_nb_channels(layout); + + // We do not use CoreAudio standard layout for lack of documentation on what + // the actual channel orders are. So we set a custom layout. + size_t size = offsetof(AudioChannelLayout, mChannelDescriptions[nb_channels]); + auto au_layout = make_sized_audio_channel_layout(size); + au_layout->mChannelLayoutTag = kAudioChannelLayoutTag_UseChannelDescriptions; + au_layout->mNumberChannelDescriptions = nb_channels; + + uint32_t channels = 0; + cubeb_channel_layout channelMap = layout; + for (uint32_t i = 0; channelMap != 0; ++i) { + XASSERT(channels < nb_channels); + uint32_t channel = (channelMap & 1) << i; + if (channel != 0) { + au_layout->mChannelDescriptions[channels].mChannelLabel = + cubeb_channel_to_channel_label(static_cast<cubeb_channel>(channel)); + au_layout->mChannelDescriptions[channels].mChannelFlags = kAudioChannelFlags_AllOff; + channels++; + } + channelMap = channelMap >> 1; + } + + r = AudioUnitSetProperty(unit, + kAudioUnitProperty_AudioChannelLayout, + kAudioUnitScope_Input, + AU_OUT_BUS, + au_layout.get(), + size); + if (r != noErr) { + LOG("AudioUnitSetProperty/%s/kAudioUnitProperty_AudioChannelLayout rv=%d", to_string(side), r); + return CUBEB_ERROR; + } + + return CUBEB_OK; +} + +void +audiounit_layout_init(cubeb_stream * stm, io_side side) +{ + // We currently don't support the input layout setting. + if (side == io_side::INPUT) { + return; + } + + stm->context->layout = audiounit_get_current_channel_layout(stm->output_unit); + + audiounit_set_channel_layout(stm->output_unit, io_side::OUTPUT, stm->context->layout); +} + +static vector<AudioObjectID> +audiounit_get_sub_devices(AudioDeviceID device_id) +{ + vector<AudioDeviceID> sub_devices; + AudioObjectPropertyAddress property_address = { kAudioAggregateDevicePropertyActiveSubDeviceList, + kAudioObjectPropertyScopeGlobal, + kAudioObjectPropertyElementMaster }; + UInt32 size = 0; + OSStatus rv = AudioObjectGetPropertyDataSize(device_id, + &property_address, + 0, + nullptr, + &size); + + if (rv != noErr) { + sub_devices.push_back(device_id); + return sub_devices; + } + + uint32_t count = static_cast<uint32_t>(size / sizeof(AudioObjectID)); + sub_devices.resize(count); + rv = AudioObjectGetPropertyData(device_id, + &property_address, + 0, + nullptr, + &size, + sub_devices.data()); + if (rv != noErr) { + sub_devices.clear(); + sub_devices.push_back(device_id); + } else { + LOG("Found %u sub-devices", count); + } + return sub_devices; +} + +static int +audiounit_create_blank_aggregate_device(AudioObjectID * plugin_id, AudioDeviceID * aggregate_device_id) +{ + AudioObjectPropertyAddress address_plugin_bundle_id = { kAudioHardwarePropertyPlugInForBundleID, + kAudioObjectPropertyScopeGlobal, + kAudioObjectPropertyElementMaster }; + UInt32 size = 0; + OSStatus r = AudioObjectGetPropertyDataSize(kAudioObjectSystemObject, + &address_plugin_bundle_id, + 0, NULL, + &size); + if (r != noErr) { + LOG("AudioObjectGetPropertyDataSize/kAudioHardwarePropertyPlugInForBundleID, rv=%d", r); + return CUBEB_ERROR; + } + + AudioValueTranslation translation_value; + CFStringRef in_bundle_ref = CFSTR("com.apple.audio.CoreAudio"); + translation_value.mInputData = &in_bundle_ref; + translation_value.mInputDataSize = sizeof(in_bundle_ref); + translation_value.mOutputData = plugin_id; + translation_value.mOutputDataSize = sizeof(*plugin_id); + + r = AudioObjectGetPropertyData(kAudioObjectSystemObject, + &address_plugin_bundle_id, + 0, + nullptr, + &size, + &translation_value); + if (r != noErr) { + LOG("AudioObjectGetPropertyData/kAudioHardwarePropertyPlugInForBundleID, rv=%d", r); + return CUBEB_ERROR; + } + + AudioObjectPropertyAddress create_aggregate_device_address = { kAudioPlugInCreateAggregateDevice, + kAudioObjectPropertyScopeGlobal, + kAudioObjectPropertyElementMaster }; + r = AudioObjectGetPropertyDataSize(*plugin_id, + &create_aggregate_device_address, + 0, + nullptr, + &size); + if (r != noErr) { + LOG("AudioObjectGetPropertyDataSize/kAudioPlugInCreateAggregateDevice, rv=%d", r); + return CUBEB_ERROR; + } + + CFMutableDictionaryRef aggregate_device_dict = CFDictionaryCreateMutable(kCFAllocatorDefault, 0, + &kCFTypeDictionaryKeyCallBacks, + &kCFTypeDictionaryValueCallBacks); + struct timeval timestamp; + gettimeofday(×tamp, NULL); + long long int time_id = timestamp.tv_sec * 1000000LL + timestamp.tv_usec; + CFStringRef aggregate_device_name = CFStringCreateWithFormat(NULL, NULL, CFSTR("%s_%llx"), PRIVATE_AGGREGATE_DEVICE_NAME, time_id); + CFDictionaryAddValue(aggregate_device_dict, CFSTR(kAudioAggregateDeviceNameKey), aggregate_device_name); + CFRelease(aggregate_device_name); + + CFStringRef aggregate_device_UID = CFStringCreateWithFormat(NULL, NULL, CFSTR("org.mozilla.%s_%llx"), PRIVATE_AGGREGATE_DEVICE_NAME, time_id); + CFDictionaryAddValue(aggregate_device_dict, CFSTR(kAudioAggregateDeviceUIDKey), aggregate_device_UID); + CFRelease(aggregate_device_UID); + + int private_value = 1; + CFNumberRef aggregate_device_private_key = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &private_value); + CFDictionaryAddValue(aggregate_device_dict, CFSTR(kAudioAggregateDeviceIsPrivateKey), aggregate_device_private_key); + CFRelease(aggregate_device_private_key); + + int stacked_value = 0; + CFNumberRef aggregate_device_stacked_key = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &stacked_value); + CFDictionaryAddValue(aggregate_device_dict, CFSTR(kAudioAggregateDeviceIsStackedKey), aggregate_device_stacked_key); + CFRelease(aggregate_device_stacked_key); + + r = AudioObjectGetPropertyData(*plugin_id, + &create_aggregate_device_address, + sizeof(aggregate_device_dict), + &aggregate_device_dict, + &size, + aggregate_device_id); + CFRelease(aggregate_device_dict); + if (r != noErr) { + LOG("AudioObjectGetPropertyData/kAudioPlugInCreateAggregateDevice, rv=%d", r); + return CUBEB_ERROR; + } + LOG("New aggregate device %u", *aggregate_device_id); + + return CUBEB_OK; +} + +// The returned CFStringRef object needs to be released (via CFRelease) +// if it's not NULL, since the reference count of the returned CFStringRef +// object is increased. +static CFStringRef +get_device_name(AudioDeviceID id) +{ + UInt32 size = sizeof(CFStringRef); + CFStringRef UIname = nullptr; + AudioObjectPropertyAddress address_uuid = { kAudioDevicePropertyDeviceUID, + kAudioObjectPropertyScopeGlobal, + kAudioObjectPropertyElementMaster }; + OSStatus err = AudioObjectGetPropertyData(id, &address_uuid, 0, nullptr, &size, &UIname); + return (err == noErr) ? UIname : NULL; +} + +static int +audiounit_set_aggregate_sub_device_list(AudioDeviceID aggregate_device_id, + AudioDeviceID input_device_id, + AudioDeviceID output_device_id) +{ + LOG("Add devices input %u and output %u into aggregate device %u", + input_device_id, output_device_id, aggregate_device_id); + const vector<AudioDeviceID> output_sub_devices = audiounit_get_sub_devices(output_device_id); + const vector<AudioDeviceID> input_sub_devices = audiounit_get_sub_devices(input_device_id); + + CFMutableArrayRef aggregate_sub_devices_array = CFArrayCreateMutable(NULL, 0, &kCFTypeArrayCallBacks); + /* The order of the items in the array is significant and is used to determine the order of the streams + of the AudioAggregateDevice. */ + for (UInt32 i = 0; i < output_sub_devices.size(); i++) { + CFStringRef ref = get_device_name(output_sub_devices[i]); + if (ref == NULL) { + CFRelease(aggregate_sub_devices_array); + return CUBEB_ERROR; + } + CFArrayAppendValue(aggregate_sub_devices_array, ref); + CFRelease(ref); + } + for (UInt32 i = 0; i < input_sub_devices.size(); i++) { + CFStringRef ref = get_device_name(input_sub_devices[i]); + if (ref == NULL) { + CFRelease(aggregate_sub_devices_array); + return CUBEB_ERROR; + } + CFArrayAppendValue(aggregate_sub_devices_array, ref); + CFRelease(ref); + } + + AudioObjectPropertyAddress aggregate_sub_device_list = { kAudioAggregateDevicePropertyFullSubDeviceList, + kAudioObjectPropertyScopeGlobal, + kAudioObjectPropertyElementMaster }; + UInt32 size = sizeof(CFMutableArrayRef); + OSStatus rv = AudioObjectSetPropertyData(aggregate_device_id, + &aggregate_sub_device_list, + 0, + nullptr, + size, + &aggregate_sub_devices_array); + CFRelease(aggregate_sub_devices_array); + if (rv != noErr) { + LOG("AudioObjectSetPropertyData/kAudioAggregateDevicePropertyFullSubDeviceList, rv=%d", rv); + return CUBEB_ERROR; + } + + return CUBEB_OK; +} + +static int +audiounit_set_master_aggregate_device(const AudioDeviceID aggregate_device_id) +{ + assert(aggregate_device_id != kAudioObjectUnknown); + AudioObjectPropertyAddress master_aggregate_sub_device = { kAudioAggregateDevicePropertyMasterSubDevice, + kAudioObjectPropertyScopeGlobal, + kAudioObjectPropertyElementMaster }; + + // Master become the 1st output sub device + AudioDeviceID output_device_id = audiounit_get_default_device_id(CUBEB_DEVICE_TYPE_OUTPUT); + const vector<AudioDeviceID> output_sub_devices = audiounit_get_sub_devices(output_device_id); + CFStringRef master_sub_device = get_device_name(output_sub_devices[0]); + + UInt32 size = sizeof(CFStringRef); + OSStatus rv = AudioObjectSetPropertyData(aggregate_device_id, + &master_aggregate_sub_device, + 0, + NULL, + size, + &master_sub_device); + if (master_sub_device) { + CFRelease(master_sub_device); + } + if (rv != noErr) { + LOG("AudioObjectSetPropertyData/kAudioAggregateDevicePropertyMasterSubDevice, rv=%d", rv); + return CUBEB_ERROR; + } + + return CUBEB_OK; +} + +static int +audiounit_activate_clock_drift_compensation(const AudioDeviceID aggregate_device_id) +{ + assert(aggregate_device_id != kAudioObjectUnknown); + AudioObjectPropertyAddress address_owned = { kAudioObjectPropertyOwnedObjects, + kAudioObjectPropertyScopeGlobal, + kAudioObjectPropertyElementMaster }; + + UInt32 qualifier_data_size = sizeof(AudioObjectID); + AudioClassID class_id = kAudioSubDeviceClassID; + void * qualifier_data = &class_id; + UInt32 size = 0; + OSStatus rv = AudioObjectGetPropertyDataSize(aggregate_device_id, + &address_owned, + qualifier_data_size, + qualifier_data, + &size); + if (rv != noErr) { + LOG("AudioObjectGetPropertyDataSize/kAudioObjectPropertyOwnedObjects, rv=%d", rv); + return CUBEB_ERROR; + } + + UInt32 subdevices_num = 0; + subdevices_num = size / sizeof(AudioObjectID); + AudioObjectID sub_devices[subdevices_num]; + size = sizeof(sub_devices); + + rv = AudioObjectGetPropertyData(aggregate_device_id, + &address_owned, + qualifier_data_size, + qualifier_data, + &size, + sub_devices); + if (rv != noErr) { + LOG("AudioObjectGetPropertyData/kAudioObjectPropertyOwnedObjects, rv=%d", rv); + return CUBEB_ERROR; + } + + AudioObjectPropertyAddress address_drift = { kAudioSubDevicePropertyDriftCompensation, + kAudioObjectPropertyScopeGlobal, + kAudioObjectPropertyElementMaster }; + + // Start from the second device since the first is the master clock + for (UInt32 i = 1; i < subdevices_num; ++i) { + UInt32 drift_compensation_value = 1; + rv = AudioObjectSetPropertyData(sub_devices[i], + &address_drift, + 0, + nullptr, + sizeof(UInt32), + &drift_compensation_value); + if (rv != noErr) { + LOG("AudioObjectSetPropertyData/kAudioSubDevicePropertyDriftCompensation, rv=%d", rv); + return CUBEB_OK; + } + } + return CUBEB_OK; +} + +static int audiounit_destroy_aggregate_device(AudioObjectID plugin_id, AudioDeviceID * aggregate_device_id); +static void audiounit_get_available_samplerate(AudioObjectID devid, AudioObjectPropertyScope scope, + uint32_t * min, uint32_t * max, uint32_t * def); +static int +audiounit_create_device_from_hwdev(cubeb_device_info * dev_info, AudioObjectID devid, cubeb_device_type type); +static void audiounit_device_destroy(cubeb_device_info * device); + +static void +audiounit_workaround_for_airpod(cubeb_stream * stm) +{ + cubeb_device_info input_device_info; + audiounit_create_device_from_hwdev(&input_device_info, stm->input_device.id, CUBEB_DEVICE_TYPE_INPUT); + + cubeb_device_info output_device_info; + audiounit_create_device_from_hwdev(&output_device_info, stm->output_device.id, CUBEB_DEVICE_TYPE_OUTPUT); + + std::string input_name_str(input_device_info.friendly_name); + std::string output_name_str(output_device_info.friendly_name); + + if(input_name_str.find("AirPods") != std::string::npos && + output_name_str.find("AirPods") != std::string::npos) { + uint32_t input_min_rate = 0; + uint32_t input_max_rate = 0; + uint32_t input_nominal_rate = 0; + audiounit_get_available_samplerate(stm->input_device.id, kAudioObjectPropertyScopeGlobal, + &input_min_rate, &input_max_rate, &input_nominal_rate); + LOG("(%p) Input device %u, name: %s, min: %u, max: %u, nominal rate: %u", stm, stm->input_device.id + , input_device_info.friendly_name, input_min_rate, input_max_rate, input_nominal_rate); + uint32_t output_min_rate = 0; + uint32_t output_max_rate = 0; + uint32_t output_nominal_rate = 0; + audiounit_get_available_samplerate(stm->output_device.id, kAudioObjectPropertyScopeGlobal, + &output_min_rate, &output_max_rate, &output_nominal_rate); + LOG("(%p) Output device %u, name: %s, min: %u, max: %u, nominal rate: %u", stm, stm->output_device.id + , output_device_info.friendly_name, output_min_rate, output_max_rate, output_nominal_rate); + + Float64 rate = input_nominal_rate; + AudioObjectPropertyAddress addr = {kAudioDevicePropertyNominalSampleRate, + kAudioObjectPropertyScopeGlobal, + kAudioObjectPropertyElementMaster}; + + OSStatus rv = AudioObjectSetPropertyData(stm->aggregate_device_id, + &addr, + 0, + nullptr, + sizeof(Float64), + &rate); + if (rv != noErr) { + LOG("Non fatal error, AudioObjectSetPropertyData/kAudioDevicePropertyNominalSampleRate, rv=%d", rv); + } + } + audiounit_device_destroy(&input_device_info); + audiounit_device_destroy(&output_device_info); +} + +/* + * Aggregate Device is a virtual audio interface which utilizes inputs and outputs + * of one or more physical audio interfaces. It is possible to use the clock of + * one of the devices as a master clock for all the combined devices and enable + * drift compensation for the devices that are not designated clock master. + * + * Creating a new aggregate device programmatically requires [0][1]: + * 1. Locate the base plug-in ("com.apple.audio.CoreAudio") + * 2. Create a dictionary that describes the aggregate device + * (don't add sub-devices in that step, prone to fail [0]) + * 3. Ask the base plug-in to create the aggregate device (blank) + * 4. Add the array of sub-devices. + * 5. Set the master device (1st output device in our case) + * 6. Enable drift compensation for the non-master devices + * + * [0] https://lists.apple.com/archives/coreaudio-api/2006/Apr/msg00092.html + * [1] https://lists.apple.com/archives/coreaudio-api/2005/Jul/msg00150.html + * [2] CoreAudio.framework/Headers/AudioHardware.h + * */ +static int +audiounit_create_aggregate_device(cubeb_stream * stm) +{ + int r = audiounit_create_blank_aggregate_device(&stm->plugin_id, &stm->aggregate_device_id); + if (r != CUBEB_OK) { + LOG("(%p) Failed to create blank aggregate device", stm); + return CUBEB_ERROR; + } + + r = audiounit_set_aggregate_sub_device_list(stm->aggregate_device_id, stm->input_device.id, stm->output_device.id); + if (r != CUBEB_OK) { + LOG("(%p) Failed to set aggregate sub-device list", stm); + audiounit_destroy_aggregate_device(stm->plugin_id, &stm->aggregate_device_id); + return CUBEB_ERROR; + } + + r = audiounit_set_master_aggregate_device(stm->aggregate_device_id); + if (r != CUBEB_OK) { + LOG("(%p) Failed to set master sub-device for aggregate device", stm); + audiounit_destroy_aggregate_device(stm->plugin_id, &stm->aggregate_device_id); + return CUBEB_ERROR; + } + + r = audiounit_activate_clock_drift_compensation(stm->aggregate_device_id); + if (r != CUBEB_OK) { + LOG("(%p) Failed to activate clock drift compensation for aggregate device", stm); + audiounit_destroy_aggregate_device(stm->plugin_id, &stm->aggregate_device_id); + return CUBEB_ERROR; + } + + audiounit_workaround_for_airpod(stm); + + return CUBEB_OK; +} + +static int +audiounit_destroy_aggregate_device(AudioObjectID plugin_id, AudioDeviceID * aggregate_device_id) +{ + assert(aggregate_device_id && + *aggregate_device_id != kAudioDeviceUnknown && + plugin_id != kAudioObjectUnknown); + AudioObjectPropertyAddress destroy_aggregate_device_addr = { kAudioPlugInDestroyAggregateDevice, + kAudioObjectPropertyScopeGlobal, + kAudioObjectPropertyElementMaster}; + UInt32 size; + OSStatus rv = AudioObjectGetPropertyDataSize(plugin_id, + &destroy_aggregate_device_addr, + 0, + NULL, + &size); + if (rv != noErr) { + LOG("AudioObjectGetPropertyDataSize/kAudioPlugInDestroyAggregateDevice, rv=%d", rv); + return CUBEB_ERROR; + } + + rv = AudioObjectGetPropertyData(plugin_id, + &destroy_aggregate_device_addr, + 0, + NULL, + &size, + aggregate_device_id); + if (rv != noErr) { + LOG("AudioObjectGetPropertyData/kAudioPlugInDestroyAggregateDevice, rv=%d", rv); + return CUBEB_ERROR; + } + + LOG("Destroyed aggregate device %d", *aggregate_device_id); + *aggregate_device_id = kAudioObjectUnknown; + return CUBEB_OK; +} + +static int +audiounit_new_unit_instance(AudioUnit * unit, device_info * device) { AudioComponentDescription desc; AudioComponent comp; - UInt32 enable; - AudioDeviceID devid; OSStatus rv; desc.componentType = kAudioUnitType_Output; #if TARGET_OS_IPHONE - bool use_default_output = false; desc.componentSubType = kAudioUnitSubType_RemoteIO; #else // Use the DefaultOutputUnit for output when no device is specified // so we retain automatic output device switching when the default // changes. Once we have complete support for device notifications // and switching, we can use the AUHAL for everything. - bool use_default_output = device == NULL && !is_input; - if (use_default_output) { + if ((device->flags & DEV_SYSTEM_DEFAULT) && + (device->flags & DEV_OUTPUT)) { desc.componentSubType = kAudioUnitSubType_DefaultOutput; } else { desc.componentSubType = kAudioUnitSubType_HALOutput; @@ -1144,96 +2020,118 @@ audiounit_create_unit(AudioUnit * unit, rv = AudioComponentInstanceNew(comp, unit); if (rv != noErr) { - PRINT_ERROR_CODE("AudioComponentInstanceNew", rv); + LOG("AudioComponentInstanceNew rv=%d", rv); return CUBEB_ERROR; } + return CUBEB_OK; +} - if (!use_default_output) { - enable = 1; - rv = AudioUnitSetProperty(*unit, kAudioOutputUnitProperty_EnableIO, - is_input ? kAudioUnitScope_Input : kAudioUnitScope_Output, - is_input ? AU_IN_BUS : AU_OUT_BUS, &enable, sizeof(UInt32)); - if (rv != noErr) { - PRINT_ERROR_CODE("AudioUnitSetProperty/kAudioOutputUnitProperty_EnableIO", rv); - return CUBEB_ERROR; - } - - enable = 0; - rv = AudioUnitSetProperty(*unit, kAudioOutputUnitProperty_EnableIO, - is_input ? kAudioUnitScope_Output : kAudioUnitScope_Input, - is_input ? AU_OUT_BUS : AU_IN_BUS, &enable, sizeof(UInt32)); - if (rv != noErr) { - PRINT_ERROR_CODE("AudioUnitSetProperty/kAudioOutputUnitProperty_EnableIO", rv); - return CUBEB_ERROR; - } +enum enable_state { + DISABLE, + ENABLE, +}; - if (device == NULL) { - assert(is_input); - devid = audiounit_get_default_device_id(CUBEB_DEVICE_TYPE_INPUT); - } else { - devid = reinterpret_cast<intptr_t>(device); - } - rv = AudioUnitSetProperty(*unit, kAudioOutputUnitProperty_CurrentDevice, - kAudioUnitScope_Global, - is_input ? AU_IN_BUS : AU_OUT_BUS, - &devid, sizeof(AudioDeviceID)); - if (rv != noErr) { - PRINT_ERROR_CODE("AudioUnitSetProperty/kAudioOutputUnitProperty_CurrentDevice", rv); - return CUBEB_ERROR; - } +static int +audiounit_enable_unit_scope(AudioUnit * unit, io_side side, enable_state state) +{ + OSStatus rv; + UInt32 enable = state; + rv = AudioUnitSetProperty(*unit, kAudioOutputUnitProperty_EnableIO, + (side == io_side::INPUT) ? kAudioUnitScope_Input : kAudioUnitScope_Output, + (side == io_side::INPUT) ? AU_IN_BUS : AU_OUT_BUS, + &enable, + sizeof(UInt32)); + if (rv != noErr) { + LOG("AudioUnitSetProperty/kAudioOutputUnitProperty_EnableIO rv=%d", rv); + return CUBEB_ERROR; } - return CUBEB_OK; } static int -audiounit_init_input_linear_buffer(cubeb_stream * stream, uint32_t capacity) +audiounit_create_unit(AudioUnit * unit, device_info * device) { - if (stream->input_desc.mFormatFlags & kAudioFormatFlagIsSignedInteger) { - stream->input_linear_buffer = new auto_array_wrapper( - new auto_array<short>(capacity * - stream->input_buffer_frames * - stream->input_desc.mChannelsPerFrame) ); - } else { - stream->input_linear_buffer = new auto_array_wrapper( - new auto_array<float>(capacity * - stream->input_buffer_frames * - stream->input_desc.mChannelsPerFrame) ); + assert(*unit == nullptr); + assert(device); + + OSStatus rv; + int r; + + r = audiounit_new_unit_instance(unit, device); + if (r != CUBEB_OK) { + return r; } + assert(*unit); - if (!stream->input_linear_buffer) { - return CUBEB_ERROR; + if ((device->flags & DEV_SYSTEM_DEFAULT) && + (device->flags & DEV_OUTPUT)) { + return CUBEB_OK; } - assert(stream->input_linear_buffer->length() == 0); - // Pre-buffer silence if needed - if (capacity != 1) { - size_t silence_size = stream->input_buffer_frames * - stream->input_desc.mChannelsPerFrame; - stream->input_linear_buffer->push_silence(silence_size); + if (device->flags & DEV_INPUT) { + r = audiounit_enable_unit_scope(unit, io_side::INPUT, ENABLE); + if (r != CUBEB_OK) { + LOG("Failed to enable audiounit input scope"); + return r; + } + r = audiounit_enable_unit_scope(unit, io_side::OUTPUT, DISABLE); + if (r != CUBEB_OK) { + LOG("Failed to disable audiounit output scope"); + return r; + } + } else if (device->flags & DEV_OUTPUT) { + r = audiounit_enable_unit_scope(unit, io_side::OUTPUT, ENABLE); + if (r != CUBEB_OK) { + LOG("Failed to enable audiounit output scope"); + return r; + } + r = audiounit_enable_unit_scope(unit, io_side::INPUT, DISABLE); + if (r != CUBEB_OK) { + LOG("Failed to disable audiounit input scope"); + return r; + } + } else { + assert(false); + } - assert(stream->input_linear_buffer->length() == silence_size); + rv = AudioUnitSetProperty(*unit, + kAudioOutputUnitProperty_CurrentDevice, + kAudioUnitScope_Global, + 0, + &device->id, sizeof(AudioDeviceID)); + if (rv != noErr) { + LOG("AudioUnitSetProperty/kAudioOutputUnitProperty_CurrentDevice rv=%d", rv); + return CUBEB_ERROR; } return CUBEB_OK; } -static void -audiounit_destroy_input_linear_buffer(cubeb_stream * stream) +static int +audiounit_init_input_linear_buffer(cubeb_stream * stream, uint32_t capacity) { - delete stream->input_linear_buffer; + uint32_t size = capacity * stream->latency_frames * stream->input_desc.mChannelsPerFrame; + if (stream->input_desc.mFormatFlags & kAudioFormatFlagIsSignedInteger) { + stream->input_linear_buffer.reset(new auto_array_wrapper_impl<short>(size)); + } else { + stream->input_linear_buffer.reset(new auto_array_wrapper_impl<float>(size)); + } + assert(stream->input_linear_buffer->length() == 0); + + return CUBEB_OK; } static uint32_t audiounit_clamp_latency(cubeb_stream * stm, uint32_t latency_frames) { // For the 1st stream set anything within safe min-max - assert(stm->context->active_streams > 0); - if (stm->context->active_streams == 1) { - return std::max(std::min<uint32_t>(latency_frames, SAFE_MAX_LATENCY_FRAMES), + assert(audiounit_active_streams(stm->context) > 0); + if (audiounit_active_streams(stm->context) == 1) { + return max(min<uint32_t>(latency_frames, SAFE_MAX_LATENCY_FRAMES), SAFE_MIN_LATENCY_FRAMES); } + assert(stm->output_unit); // If more than one stream operates in parallel // allow only lower values of latency @@ -1248,11 +2146,11 @@ audiounit_clamp_latency(cubeb_stream * stm, uint32_t latency_frames) &output_buffer_size, &size); if (r != noErr) { - PRINT_ERROR_CODE("AudioUnitGetProperty/output/kAudioDevicePropertyBufferFrameSize", r); + LOG("AudioUnitGetProperty/output/kAudioDevicePropertyBufferFrameSize rv=%d", r); return 0; } - output_buffer_size = std::max(std::min<uint32_t>(output_buffer_size, SAFE_MAX_LATENCY_FRAMES), + output_buffer_size = max(min<uint32_t>(output_buffer_size, SAFE_MAX_LATENCY_FRAMES), SAFE_MIN_LATENCY_FRAMES); } @@ -1265,18 +2163,18 @@ audiounit_clamp_latency(cubeb_stream * stm, uint32_t latency_frames) &input_buffer_size, &size); if (r != noErr) { - PRINT_ERROR_CODE("AudioUnitGetProperty/input/kAudioDevicePropertyBufferFrameSize", r); + LOG("AudioUnitGetProperty/input/kAudioDevicePropertyBufferFrameSize rv=%d", r); return 0; } - input_buffer_size = std::max(std::min<uint32_t>(input_buffer_size, SAFE_MAX_LATENCY_FRAMES), + input_buffer_size = max(min<uint32_t>(input_buffer_size, SAFE_MAX_LATENCY_FRAMES), SAFE_MIN_LATENCY_FRAMES); } // Every following active streams can only set smaller latency UInt32 upper_latency_limit = 0; if (input_buffer_size != 0 && output_buffer_size != 0) { - upper_latency_limit = std::min<uint32_t>(input_buffer_size, output_buffer_size); + upper_latency_limit = min<uint32_t>(input_buffer_size, output_buffer_size); } else if (input_buffer_size != 0) { upper_latency_limit = input_buffer_size; } else if (output_buffer_size != 0) { @@ -1285,7 +2183,7 @@ audiounit_clamp_latency(cubeb_stream * stm, uint32_t latency_frames) upper_latency_limit = SAFE_MAX_LATENCY_FRAMES; } - return std::max(std::min<uint32_t>(latency_frames, upper_latency_limit), + return max(min<uint32_t>(latency_frames, upper_latency_limit), SAFE_MIN_LATENCY_FRAMES); } @@ -1300,18 +2198,18 @@ audiounit_clamp_latency(cubeb_stream * stm, uint32_t latency_frames) static void buffer_size_changed_callback(void * inClientData, AudioUnit inUnit, - AudioUnitPropertyID inPropertyID, - AudioUnitScope inScope, - AudioUnitElement inElement) + AudioUnitPropertyID inPropertyID, + AudioUnitScope inScope, + AudioUnitElement inElement) { cubeb_stream * stm = (cubeb_stream *)inClientData; AudioUnit au = inUnit; AudioUnitScope au_scope = kAudioUnitScope_Input; AudioUnitElement au_element = inElement; - const char * au_type = "output"; + char const * au_type = "output"; - if (au == stm->input_unit) { + if (AU_IN_BUS == inElement) { au_scope = kAudioUnitScope_Output; au_type = "input"; } @@ -1342,24 +2240,17 @@ buffer_size_changed_callback(void * inClientData, } } -enum set_buffer_size_side { - INPUT, - OUTPUT, -}; - static int -audiounit_set_buffer_size(cubeb_stream * stm, uint32_t new_size_frames, set_buffer_size_side set_side) +audiounit_set_buffer_size(cubeb_stream * stm, uint32_t new_size_frames, io_side side) { AudioUnit au = stm->output_unit; AudioUnitScope au_scope = kAudioUnitScope_Input; AudioUnitElement au_element = AU_OUT_BUS; - const char * au_type = "output"; - if (set_side == INPUT) { + if (side == io_side::INPUT) { au = stm->input_unit; au_scope = kAudioUnitScope_Output; au_element = AU_IN_BUS; - au_type = "input"; } uint32_t buffer_frames = 0; @@ -1371,16 +2262,12 @@ audiounit_set_buffer_size(cubeb_stream * stm, uint32_t new_size_frames, set_buff &buffer_frames, &size); if (r != noErr) { - if (set_side == INPUT) { - PRINT_ERROR_CODE("AudioUnitGetProperty/input/kAudioDevicePropertyBufferFrameSize", r); - } else { - PRINT_ERROR_CODE("AudioUnitGetProperty/output/kAudioDevicePropertyBufferFrameSize", r); - } + LOG("AudioUnitGetProperty/%s/kAudioDevicePropertyBufferFrameSize rv=%d", to_string(side), r); return CUBEB_ERROR; } if (new_size_frames == buffer_frames) { - LOG("(%p) No need to update %s buffer size already %u frames", stm, au_type, buffer_frames); + LOG("(%p) No need to update %s buffer size already %u frames", stm, to_string(side), buffer_frames); return CUBEB_OK; } @@ -1389,11 +2276,7 @@ audiounit_set_buffer_size(cubeb_stream * stm, uint32_t new_size_frames, set_buff buffer_size_changed_callback, stm); if (r != noErr) { - if (set_side == INPUT) { - PRINT_ERROR_CODE("AudioUnitAddPropertyListener/input/kAudioDevicePropertyBufferFrameSize", r); - } else { - PRINT_ERROR_CODE("AudioUnitAddPropertyListener/output/kAudioDevicePropertyBufferFrameSize", r); - } + LOG("AudioUnitAddPropertyListener/%s/kAudioDevicePropertyBufferFrameSize rv=%d", to_string(side), r); return CUBEB_ERROR; } @@ -1406,22 +2289,14 @@ audiounit_set_buffer_size(cubeb_stream * stm, uint32_t new_size_frames, set_buff &new_size_frames, sizeof(new_size_frames)); if (r != noErr) { - if (set_side == INPUT) { - PRINT_ERROR_CODE("AudioUnitSetProperty/input/kAudioDevicePropertyBufferFrameSize", r); - } else { - PRINT_ERROR_CODE("AudioUnitSetProperty/output/kAudioDevicePropertyBufferFrameSize", r); - } + LOG("AudioUnitSetProperty/%s/kAudioDevicePropertyBufferFrameSize rv=%d", to_string(side), r); r = AudioUnitRemovePropertyListenerWithUserData(au, kAudioDevicePropertyBufferFrameSize, buffer_size_changed_callback, stm); if (r != noErr) { - if (set_side == INPUT) { - PRINT_ERROR_CODE("AudioUnitAddPropertyListener/input/kAudioDevicePropertyBufferFrameSize", r); - } else { - PRINT_ERROR_CODE("AudioUnitAddPropertyListener/output/kAudioDevicePropertyBufferFrameSize", r); - } + LOG("AudioUnitAddPropertyListener/%s/kAudioDevicePropertyBufferFrameSize rv=%d", to_string(side), r); } return CUBEB_ERROR; @@ -1443,11 +2318,7 @@ audiounit_set_buffer_size(cubeb_stream * stm, uint32_t new_size_frames, set_buff buffer_size_changed_callback, stm); if (r != noErr) { - if (set_side == INPUT) { - PRINT_ERROR_CODE("AudioUnitAddPropertyListener/input/kAudioDevicePropertyBufferFrameSize", r); - } else { - PRINT_ERROR_CODE("AudioUnitAddPropertyListener/output/kAudioDevicePropertyBufferFrameSize", r); - } + LOG("AudioUnitAddPropertyListener/%s/kAudioDevicePropertyBufferFrameSize rv=%d", to_string(side), r); return CUBEB_ERROR; } @@ -1456,13 +2327,15 @@ audiounit_set_buffer_size(cubeb_stream * stm, uint32_t new_size_frames, set_buff return CUBEB_ERROR; } - LOG("(%p) %s buffer size changed to %u frames.", stm, au_type, new_size_frames); + LOG("(%p) %s buffer size changed to %u frames.", stm, to_string(side), new_size_frames); return CUBEB_OK; } static int audiounit_configure_input(cubeb_stream * stm) { + assert(stm && stm->input_unit); + int r = 0; UInt32 size; AURenderCallbackStruct aurcbs_in; @@ -1481,7 +2354,7 @@ audiounit_configure_input(cubeb_stream * stm) &input_hw_desc, &size); if (r != noErr) { - PRINT_ERROR_CODE("AudioUnitGetProperty/input/kAudioUnitProperty_StreamFormat", r); + LOG("AudioUnitGetProperty/input/kAudioUnitProperty_StreamFormat rv=%d", r); return CUBEB_ERROR; } stm->input_hw_rate = input_hw_desc.mSampleRate; @@ -1495,8 +2368,7 @@ audiounit_configure_input(cubeb_stream * stm) } // Use latency to set buffer size - stm->input_buffer_frames = stm->latency_frames; - r = audiounit_set_buffer_size(stm, stm->input_buffer_frames, INPUT); + r = audiounit_set_buffer_size(stm, stm->latency_frames, io_side::INPUT); if (r != CUBEB_OK) { LOG("(%p) Error in change input buffer size.", stm); return CUBEB_ERROR; @@ -1514,7 +2386,7 @@ audiounit_configure_input(cubeb_stream * stm) &src_desc, sizeof(AudioStreamBasicDescription)); if (r != noErr) { - PRINT_ERROR_CODE("AudioUnitSetProperty/input/kAudioUnitProperty_StreamFormat", r); + LOG("AudioUnitSetProperty/input/kAudioUnitProperty_StreamFormat rv=%d", r); return CUBEB_ERROR; } @@ -1523,10 +2395,10 @@ audiounit_configure_input(cubeb_stream * stm) kAudioUnitProperty_MaximumFramesPerSlice, kAudioUnitScope_Global, AU_IN_BUS, - &stm->input_buffer_frames, + &stm->latency_frames, sizeof(UInt32)); if (r != noErr) { - PRINT_ERROR_CODE("AudioUnitSetProperty/input/kAudioUnitProperty_MaximumFramesPerSlice", r); + LOG("AudioUnitSetProperty/input/kAudioUnitProperty_MaximumFramesPerSlice rv=%d", r); return CUBEB_ERROR; } @@ -1540,7 +2412,6 @@ audiounit_configure_input(cubeb_stream * stm) return CUBEB_ERROR; } - assert(stm->input_unit != NULL); aurcbs_in.inputProc = audiounit_input_callback; aurcbs_in.inputProcRefCon = stm; @@ -1551,9 +2422,12 @@ audiounit_configure_input(cubeb_stream * stm) &aurcbs_in, sizeof(aurcbs_in)); if (r != noErr) { - PRINT_ERROR_CODE("AudioUnitSetProperty/input/kAudioOutputUnitProperty_SetInputCallback", r); + LOG("AudioUnitSetProperty/input/kAudioOutputUnitProperty_SetInputCallback rv=%d", r); return CUBEB_ERROR; } + + stm->frames_read = 0; + LOG("(%p) Input audiounit init successfully.", stm); return CUBEB_OK; @@ -1562,6 +2436,8 @@ audiounit_configure_input(cubeb_stream * stm) static int audiounit_configure_output(cubeb_stream * stm) { + assert(stm && stm->output_unit); + int r; AURenderCallbackStruct aurcbs_out; UInt32 size; @@ -1588,11 +2464,30 @@ audiounit_configure_output(cubeb_stream * stm) &output_hw_desc, &size); if (r != noErr) { - PRINT_ERROR_CODE("AudioUnitGetProperty/output/tkAudioUnitProperty_StreamFormat", r); + LOG("AudioUnitGetProperty/output/kAudioUnitProperty_StreamFormat rv=%d", r); return CUBEB_ERROR; } stm->output_hw_rate = output_hw_desc.mSampleRate; LOG("(%p) Output device sampling rate: %.2f", stm, output_hw_desc.mSampleRate); + stm->context->channels = output_hw_desc.mChannelsPerFrame; + + // Set the input layout to match the output device layout. + audiounit_layout_init(stm, io_side::OUTPUT); + if (stm->context->channels != stm->output_stream_params.channels || + stm->context->layout != stm->output_stream_params.layout) { + LOG("Incompatible channel layouts detected, setting up remixer"); + audiounit_init_mixer(stm); + // We will be remixing the data before it reaches the output device. + // We need to adjust the number of channels and other + // AudioStreamDescription details. + stm->output_desc.mChannelsPerFrame = stm->context->channels; + stm->output_desc.mBytesPerFrame = (stm->output_desc.mBitsPerChannel / 8) * + stm->output_desc.mChannelsPerFrame; + stm->output_desc.mBytesPerPacket = + stm->output_desc.mBytesPerFrame * stm->output_desc.mFramesPerPacket; + } else { + stm->mixer = nullptr; + } r = AudioUnitSetProperty(stm->output_unit, kAudioUnitProperty_StreamFormat, @@ -1601,11 +2496,11 @@ audiounit_configure_output(cubeb_stream * stm) &stm->output_desc, sizeof(AudioStreamBasicDescription)); if (r != noErr) { - PRINT_ERROR_CODE("AudioUnitSetProperty/output/kAudioUnitProperty_StreamFormat", r); + LOG("AudioUnitSetProperty/output/kAudioUnitProperty_StreamFormat rv=%d", r); return CUBEB_ERROR; } - r = audiounit_set_buffer_size(stm, stm->latency_frames, OUTPUT); + r = audiounit_set_buffer_size(stm, stm->latency_frames, io_side::OUTPUT); if (r != CUBEB_OK) { LOG("(%p) Error in change output buffer size.", stm); return CUBEB_ERROR; @@ -1619,11 +2514,10 @@ audiounit_configure_output(cubeb_stream * stm) &stm->latency_frames, sizeof(UInt32)); if (r != noErr) { - PRINT_ERROR_CODE("AudioUnitSetProperty/output/kAudioUnitProperty_MaximumFramesPerSlice", r); + LOG("AudioUnitSetProperty/output/kAudioUnitProperty_MaximumFramesPerSlice rv=%d", r); return CUBEB_ERROR; } - assert(stm->output_unit != NULL); aurcbs_out.inputProc = audiounit_output_callback; aurcbs_out.inputProcRefCon = stm; r = AudioUnitSetProperty(stm->output_unit, @@ -1633,10 +2527,12 @@ audiounit_configure_output(cubeb_stream * stm) &aurcbs_out, sizeof(aurcbs_out)); if (r != noErr) { - PRINT_ERROR_CODE("AudioUnitSetProperty/output/kAudioUnitProperty_SetRenderCallback", r); + LOG("AudioUnitSetProperty/output/kAudioUnitProperty_SetRenderCallback rv=%d", r); return CUBEB_ERROR; } + stm->frames_written = 0; + LOG("(%p) Output audiounit init successfully.", stm); return CUBEB_OK; } @@ -1646,11 +2542,37 @@ audiounit_setup_stream(cubeb_stream * stm) { stm->mutex.assert_current_thread_owns(); + if ((stm->input_stream_params.prefs & CUBEB_STREAM_PREF_LOOPBACK) || + (stm->output_stream_params.prefs & CUBEB_STREAM_PREF_LOOPBACK)) { + LOG("(%p) Loopback not supported for audiounit.", stm); + return CUBEB_ERROR_NOT_SUPPORTED; + } + int r = 0; + + device_info in_dev_info = stm->input_device; + device_info out_dev_info = stm->output_device; + + if (has_input(stm) && has_output(stm) && + stm->input_device.id != stm->output_device.id) { + r = audiounit_create_aggregate_device(stm); + if (r != CUBEB_OK) { + stm->aggregate_device_id = kAudioObjectUnknown; + LOG("(%p) Create aggregate devices failed.", stm); + // !!!NOTE: It is not necessary to return here. If it does not + // return it will fallback to the old implementation. The intention + // is to investigate how often it fails. I plan to remove + // it after a couple of weeks. + return r; + } else { + in_dev_info.id = out_dev_info.id = stm->aggregate_device_id; + in_dev_info.flags = DEV_INPUT; + out_dev_info.flags = DEV_OUTPUT; + } + } + if (has_input(stm)) { - r = audiounit_create_unit(&stm->input_unit, true, - &stm->input_stream_params, - stm->input_device); + r = audiounit_create_unit(&stm->input_unit, &in_dev_info); if (r != CUBEB_OK) { LOG("(%p) AudioUnit creation for input failed.", stm); return r; @@ -1658,9 +2580,7 @@ audiounit_setup_stream(cubeb_stream * stm) } if (has_output(stm)) { - r = audiounit_create_unit(&stm->output_unit, false, - &stm->output_stream_params, - stm->output_device); + r = audiounit_create_unit(&stm->output_unit, &out_dev_info); if (r != CUBEB_OK) { LOG("(%p) AudioUnit creation for output failed.", stm); return r; @@ -1668,20 +2588,20 @@ audiounit_setup_stream(cubeb_stream * stm) } /* Latency cannot change if another stream is operating in parallel. In this case - * latecy is set to the other stream value. */ - if (stm->context->active_streams > 1) { + * latency is set to the other stream value. */ + if (audiounit_active_streams(stm->context) > 1) { LOG("(%p) More than one active stream, use global latency.", stm); stm->latency_frames = stm->context->global_latency_frames; } else { /* Silently clamp the latency down to the platform default, because we - * synthetize the clock from the callbacks, and we want the clock to update - * often. */ + * synthetize the clock from the callbacks, and we want the clock to update + * often. */ stm->latency_frames = audiounit_clamp_latency(stm, stm->latency_frames); - assert(stm->latency_frames); // Ungly error check - audiounit_set_global_latency(stm, stm->latency_frames); + assert(stm->latency_frames); // Ugly error check + audiounit_set_global_latency(stm->context, stm->latency_frames); } - /* Setup Input Stream! */ + /* Configure I/O stream */ if (has_input(stm)) { r = audiounit_configure_input(stm); if (r != CUBEB_OK) { @@ -1690,7 +2610,6 @@ audiounit_setup_stream(cubeb_stream * stm) } } - /* Setup Output Stream! */ if (has_output(stm)) { r = audiounit_configure_output(stm); if (r != CUBEB_OK) { @@ -1762,13 +2681,13 @@ audiounit_setup_stream(cubeb_stream * stm) /* Create resampler. Output params are unchanged * because we do not need conversion on the output. */ - stm->resampler = cubeb_resampler_create(stm, - has_input(stm) ? &input_unconverted_params : NULL, - has_output(stm) ? &stm->output_stream_params : NULL, - target_sample_rate, - stm->data_callback, - stm->user_ptr, - CUBEB_RESAMPLER_QUALITY_DESKTOP); + stm->resampler.reset(cubeb_resampler_create(stm, + has_input(stm) ? &input_unconverted_params : NULL, + has_output(stm) ? &stm->output_stream_params : NULL, + target_sample_rate, + stm->data_callback, + stm->user_ptr, + CUBEB_RESAMPLER_QUALITY_DESKTOP)); if (!stm->resampler) { LOG("(%p) Could not create resampler.", stm); return CUBEB_ERROR; @@ -1777,7 +2696,7 @@ audiounit_setup_stream(cubeb_stream * stm) if (stm->input_unit != NULL) { r = AudioUnitInitialize(stm->input_unit); if (r != noErr) { - PRINT_ERROR_CODE("AudioUnitInitialize/input", r); + LOG("AudioUnitInitialize/input rv=%d", r); return CUBEB_ERROR; } } @@ -1785,9 +2704,17 @@ audiounit_setup_stream(cubeb_stream * stm) if (stm->output_unit != NULL) { r = AudioUnitInitialize(stm->output_unit); if (r != noErr) { - PRINT_ERROR_CODE("AudioUnitInitialize/output", r); + LOG("AudioUnitInitialize/output rv=%d", r); return CUBEB_ERROR; } + + stm->current_latency_frames = audiounit_get_device_presentation_latency(stm->output_device.id, kAudioDevicePropertyScopeOutput); + + Float64 unit_s; + UInt32 size = sizeof(unit_s); + if (AudioUnitGetProperty(stm->output_unit, kAudioUnitProperty_Latency, kAudioUnitScope_Global, 0, &unit_s, &size) == noErr) { + stm->current_latency_frames += static_cast<uint32_t>(unit_s * stm->output_desc.mSampleRate); + } } if (stm->input_unit && stm->output_unit) { @@ -1799,13 +2726,24 @@ audiounit_setup_stream(cubeb_stream * stm) r = audiounit_install_device_changed_callback(stm); if (r != CUBEB_OK) { - LOG("(%p) Could not install the device change callback.", stm); - return r; + LOG("(%p) Could not install all device change callback.", stm); } + return CUBEB_OK; } +cubeb_stream::cubeb_stream(cubeb * context) + : context(context) + , resampler(nullptr, cubeb_resampler_destroy) + , mixer(nullptr, cubeb_mixer_destroy) +{ + PodZero(&input_desc, 1); + PodZero(&output_desc, 1); +} + +static void audiounit_stream_destroy_internal(cubeb_stream * stm); + static int audiounit_stream_init(cubeb * context, cubeb_stream ** stream, @@ -1819,72 +2757,64 @@ audiounit_stream_init(cubeb * context, cubeb_state_callback state_callback, void * user_ptr) { - cubeb_stream * stm; - int r; - assert(context); + auto_lock context_lock(context->mutex); + audiounit_increment_active_streams(context); + unique_ptr<cubeb_stream, decltype(&audiounit_stream_destroy)> stm(new cubeb_stream(context), + audiounit_stream_destroy_internal); + int r; *stream = NULL; - assert(latency_frames > 0); - if (context->limit_streams && context->active_streams >= CUBEB_STREAM_MAX) { - LOG("Reached the stream limit of %d", CUBEB_STREAM_MAX); - return CUBEB_ERROR; - } - - stm = (cubeb_stream *) calloc(1, sizeof(cubeb_stream)); - assert(stm); - // Placement new to call the ctors of cubeb_stream members. - new (stm) cubeb_stream(); /* These could be different in the future if we have both * full-duplex stream and different devices for input vs output. */ - stm->context = context; stm->data_callback = data_callback; stm->state_callback = state_callback; stm->user_ptr = user_ptr; stm->latency_frames = latency_frames; - stm->device_changed_callback = NULL; + + if ((input_device && !input_stream_params) || + (output_device && !output_stream_params)) { + return CUBEB_ERROR_INVALID_PARAMETER; + } if (input_stream_params) { stm->input_stream_params = *input_stream_params; - stm->input_device = input_device; - stm->is_default_input = input_device == nullptr || - (audiounit_get_default_device_id(CUBEB_DEVICE_TYPE_INPUT) == - reinterpret_cast<intptr_t>(input_device)); + r = audiounit_set_device_info(stm.get(), reinterpret_cast<uintptr_t>(input_device), io_side::INPUT); + if (r != CUBEB_OK) { + LOG("(%p) Fail to set device info for input.", stm.get()); + return r; + } } if (output_stream_params) { stm->output_stream_params = *output_stream_params; - stm->output_device = output_device; + r = audiounit_set_device_info(stm.get(), reinterpret_cast<uintptr_t>(output_device), io_side::OUTPUT); + if (r != CUBEB_OK) { + LOG("(%p) Fail to set device info for output.", stm.get()); + return r; + } } - /* Init data members where necessary */ - stm->hw_latency_frames = UINT64_MAX; - - stm->switching_device = false; - - auto_lock context_lock(context->mutex); { // It's not critical to lock here, because no other thread has been started // yet, but it allows to assert that the lock has been taken in // `audiounit_setup_stream`. - context->active_streams += 1; auto_lock lock(stm->mutex); - r = audiounit_setup_stream(stm); + r = audiounit_setup_stream(stm.get()); } if (r != CUBEB_OK) { - LOG("(%p) Could not setup the audiounit stream.", stm); - audiounit_stream_destroy(stm); + LOG("(%p) Could not setup the audiounit stream.", stm.get()); return r; } - r = audiounit_install_system_changed_callback(stm); + r = audiounit_install_system_changed_callback(stm.get()); if (r != CUBEB_OK) { - LOG("(%p) Could not install the device change callback.", stm); + LOG("(%p) Could not install the device change callback.", stm.get()); return r; } - *stream = stm; - LOG("Cubeb stream (%p) init successful.", stm); + *stream = stm.release(); + LOG("(%p) Cubeb stream init successful.", *stream); return CUBEB_OK; } @@ -1896,64 +2826,86 @@ audiounit_close_stream(cubeb_stream *stm) if (stm->input_unit) { AudioUnitUninitialize(stm->input_unit); AudioComponentInstanceDispose(stm->input_unit); + stm->input_unit = nullptr; } - audiounit_destroy_input_linear_buffer(stm); + stm->input_linear_buffer.reset(); if (stm->output_unit) { AudioUnitUninitialize(stm->output_unit); AudioComponentInstanceDispose(stm->output_unit); + stm->output_unit = nullptr; } - cubeb_resampler_destroy(stm->resampler); + stm->resampler.reset(); + stm->mixer.reset(); + + if (stm->aggregate_device_id != kAudioObjectUnknown) { + audiounit_destroy_aggregate_device(stm->plugin_id, &stm->aggregate_device_id); + stm->aggregate_device_id = kAudioObjectUnknown; + } } static void -audiounit_stream_destroy(cubeb_stream * stm) +audiounit_stream_destroy_internal(cubeb_stream *stm) { - stm->shutdown = true; + stm->context->mutex.assert_current_thread_owns(); int r = audiounit_uninstall_system_changed_callback(stm); if (r != CUBEB_OK) { LOG("(%p) Could not uninstall the device changed callback", stm); } - r = audiounit_uninstall_device_changed_callback(stm); if (r != CUBEB_OK) { - LOG("(%p) Could not uninstall the device changed callback", stm); + LOG("(%p) Could not uninstall all device change listeners", stm); } - auto_lock context_lock(stm->context->mutex); - audiounit_stream_stop_internal(stm); + auto_lock lock(stm->mutex); + audiounit_close_stream(stm); + assert(audiounit_active_streams(stm->context) >= 1); + audiounit_decrement_active_streams(stm->context); +} +static void +audiounit_stream_destroy(cubeb_stream * stm) +{ + if (!stm->shutdown.load()){ + auto_lock context_lock(stm->context->mutex); + audiounit_stream_stop_internal(stm); + stm->shutdown = true; + } + + stm->destroy_pending = true; // Execute close in serial queue to avoid collision // with reinit when un/plug devices dispatch_sync(stm->context->serial_queue, ^() { - auto_lock lock(stm->mutex); - audiounit_close_stream(stm); + auto_lock context_lock(stm->context->mutex); + audiounit_stream_destroy_internal(stm); }); - assert(stm->context->active_streams >= 1); - stm->context->active_streams -= 1; - LOG("Cubeb stream (%p) destroyed successful.", stm); - - stm->~cubeb_stream(); - free(stm); + delete stm; } -void +static int audiounit_stream_start_internal(cubeb_stream * stm) { OSStatus r; if (stm->input_unit != NULL) { r = AudioOutputUnitStart(stm->input_unit); - assert(r == 0); + if (r != noErr) { + LOG("AudioOutputUnitStart (input) rv=%d", r); + return CUBEB_ERROR; + } } if (stm->output_unit != NULL) { r = AudioOutputUnitStart(stm->output_unit); - assert(r == 0); + if (r != noErr) { + LOG("AudioOutputUnitStart (output) rv=%d", r); + return CUBEB_ERROR; + } } + return CUBEB_OK; } static int @@ -1963,7 +2915,10 @@ audiounit_stream_start(cubeb_stream * stm) stm->shutdown = false; stm->draining = false; - audiounit_stream_start_internal(stm); + int r = audiounit_stream_start_internal(stm); + if (r != CUBEB_OK) { + return r; + } stm->state_callback(stm, stm->user_ptr, CUBEB_STATE_STARTED); @@ -2002,9 +2957,12 @@ audiounit_stream_stop(cubeb_stream * stm) static int audiounit_stream_get_position(cubeb_stream * stm, uint64_t * position) { - auto_lock lock(stm->mutex); - - *position = stm->frames_played; + assert(stm); + if (stm->current_latency_frames > stm->frames_played) { + *position = 0; + } else { + *position = stm->frames_played - stm->current_latency_frames; + } return CUBEB_OK; } @@ -2015,74 +2973,7 @@ audiounit_stream_get_latency(cubeb_stream * stm, uint32_t * latency) //TODO return CUBEB_ERROR_NOT_SUPPORTED; #else - auto_lock lock(stm->mutex); - if (stm->hw_latency_frames == UINT64_MAX) { - UInt32 size; - uint32_t device_latency_frames, device_safety_offset; - double unit_latency_sec; - AudioDeviceID output_device_id; - OSStatus r; - AudioObjectPropertyAddress latency_address = { - kAudioDevicePropertyLatency, - kAudioDevicePropertyScopeOutput, - kAudioObjectPropertyElementMaster - }; - AudioObjectPropertyAddress safety_offset_address = { - kAudioDevicePropertySafetyOffset, - kAudioDevicePropertyScopeOutput, - kAudioObjectPropertyElementMaster - }; - - r = audiounit_get_output_device_id(&output_device_id); - if (r != noErr) { - return CUBEB_ERROR; - } - - size = sizeof(unit_latency_sec); - r = AudioUnitGetProperty(stm->output_unit, - kAudioUnitProperty_Latency, - kAudioUnitScope_Global, - 0, - &unit_latency_sec, - &size); - if (r != noErr) { - PRINT_ERROR_CODE("AudioUnitGetProperty/kAudioUnitProperty_Latency", r); - return CUBEB_ERROR; - } - - size = sizeof(device_latency_frames); - r = AudioObjectGetPropertyData(output_device_id, - &latency_address, - 0, - NULL, - &size, - &device_latency_frames); - if (r != noErr) { - PRINT_ERROR_CODE("AudioUnitGetPropertyData/latency_frames", r); - return CUBEB_ERROR; - } - - size = sizeof(device_safety_offset); - r = AudioObjectGetPropertyData(output_device_id, - &safety_offset_address, - 0, - NULL, - &size, - &device_safety_offset); - if (r != noErr) { - PRINT_ERROR_CODE("AudioUnitGetPropertyData/safety_offset", r); - return CUBEB_ERROR; - } - - /* This part is fixed and depend on the stream parameter and the hardware. */ - stm->hw_latency_frames = - static_cast<uint32_t>(unit_latency_sec * stm->output_desc.mSampleRate) - + device_latency_frames - + device_safety_offset; - } - - *latency = stm->hw_latency_frames + stm->current_latency_frames; - + *latency = stm->total_output_latency_frames; return CUBEB_OK; #endif } @@ -2102,119 +2993,110 @@ audiounit_stream_get_volume(cubeb_stream * stm, float * volume) return CUBEB_OK; } -int audiounit_stream_set_volume(cubeb_stream * stm, float volume) +static int +audiounit_stream_set_volume(cubeb_stream * stm, float volume) { + assert(stm->output_unit); OSStatus r; - r = AudioUnitSetParameter(stm->output_unit, kHALOutputParam_Volume, kAudioUnitScope_Global, 0, volume, 0); if (r != noErr) { - PRINT_ERROR_CODE("AudioUnitSetParameter/kHALOutputParam_Volume", r); + LOG("AudioUnitSetParameter/kHALOutputParam_Volume rv=%d", r); return CUBEB_ERROR; } return CUBEB_OK; } -int audiounit_stream_set_panning(cubeb_stream * stm, float panning) +unique_ptr<char[]> convert_uint32_into_string(UInt32 data) { - if (stm->output_desc.mChannelsPerFrame > 2) { - return CUBEB_ERROR_INVALID_PARAMETER; + // Simply create an empty string if no data. + size_t size = data == 0 ? 0 : 4; // 4 bytes for uint32. + auto str = unique_ptr<char[]> { new char[size + 1] }; // + 1 for '\0'. + str[size] = '\0'; + if (size < 4) { + return str; } - stm->panning.store(panning, std::memory_order_relaxed); - return CUBEB_OK; + // Reverse 0xWXYZ into 0xZYXW. + str[0] = (char)(data >> 24); + str[1] = (char)(data >> 16); + str[2] = (char)(data >> 8); + str[3] = (char)(data); + return str; } -int audiounit_stream_get_current_device(cubeb_stream * stm, - cubeb_device ** const device) +int audiounit_get_default_device_datasource(cubeb_device_type type, + UInt32 * data) { -#if TARGET_OS_IPHONE - //TODO - return CUBEB_ERROR_NOT_SUPPORTED; -#else - OSStatus r; - UInt32 size; - UInt32 data; - char strdata[4]; - AudioDeviceID output_device_id; - AudioDeviceID input_device_id; - - AudioObjectPropertyAddress datasource_address = { - kAudioDevicePropertyDataSource, - kAudioDevicePropertyScopeOutput, - kAudioObjectPropertyElementMaster - }; - - AudioObjectPropertyAddress datasource_address_input = { - kAudioDevicePropertyDataSource, - kAudioDevicePropertyScopeInput, - kAudioObjectPropertyElementMaster - }; - - *device = NULL; - - if (audiounit_get_output_device_id(&output_device_id) != CUBEB_OK) { + AudioDeviceID id = audiounit_get_default_device_id(type); + if (id == kAudioObjectUnknown) { return CUBEB_ERROR; } - *device = new cubeb_device; - if (!*device) { - return CUBEB_ERROR; - } - PodZero(*device, 1); - - size = sizeof(UInt32); - /* This fails with some USB headset, so simply return an empty string. */ - r = AudioObjectGetPropertyData(output_device_id, - &datasource_address, - 0, NULL, &size, &data); + UInt32 size = sizeof(*data); + /* This fails with some USB headsets (e.g., Plantronic .Audio 628). */ + OSStatus r = AudioObjectGetPropertyData(id, + type == CUBEB_DEVICE_TYPE_INPUT ? + &INPUT_DATA_SOURCE_PROPERTY_ADDRESS : + &OUTPUT_DATA_SOURCE_PROPERTY_ADDRESS, + 0, NULL, &size, data); if (r != noErr) { - size = 0; - data = 0; - } - - (*device)->output_name = new char[size + 1]; - if (!(*device)->output_name) { - return CUBEB_ERROR; + *data = 0; } - // Turn the four chars packed into a uint32 into a string - strdata[0] = (char)(data >> 24); - strdata[1] = (char)(data >> 16); - strdata[2] = (char)(data >> 8); - strdata[3] = (char)(data); + return CUBEB_OK; +} - memcpy((*device)->output_name, strdata, size); - (*device)->output_name[size] = '\0'; +int audiounit_get_default_device_name(cubeb_stream * stm, + cubeb_device * const device, + cubeb_device_type type) +{ + assert(stm); + assert(device); - if (audiounit_get_input_device_id(&input_device_id) != CUBEB_OK) { - return CUBEB_ERROR; + UInt32 data; + int r = audiounit_get_default_device_datasource(type, &data); + if (r != CUBEB_OK) { + return r; } - - size = sizeof(UInt32); - r = AudioObjectGetPropertyData(input_device_id, &datasource_address_input, 0, NULL, &size, &data); - if (r != noErr) { - LOG("(%p) Error when getting device !", stm); - size = 0; - data = 0; + char ** name = type == CUBEB_DEVICE_TYPE_INPUT ? + &device->input_name : &device->output_name; + *name = convert_uint32_into_string(data).release(); + if (!strlen(*name)) { // empty string. + LOG("(%p) name of %s device is empty!", stm, + type == CUBEB_DEVICE_TYPE_INPUT ? "input" : "output"); } + return CUBEB_OK; +} - (*device)->input_name = new char[size + 1]; - if (!(*device)->input_name) { + +int audiounit_stream_get_current_device(cubeb_stream * stm, + cubeb_device ** const device) +{ +#if TARGET_OS_IPHONE + //TODO + return CUBEB_ERROR_NOT_SUPPORTED; +#else + *device = new cubeb_device; + if (!*device) { return CUBEB_ERROR; } + PodZero(*device, 1); - // Turn the four chars packed into a uint32 into a string - strdata[0] = (char)(data >> 24); - strdata[1] = (char)(data >> 16); - strdata[2] = (char)(data >> 8); - strdata[3] = (char)(data); + int r = audiounit_get_default_device_name(stm, *device, + CUBEB_DEVICE_TYPE_OUTPUT); + if (r != CUBEB_OK) { + return r; + } - memcpy((*device)->input_name, strdata, size); - (*device)->input_name[size] = '\0'; + r = audiounit_get_default_device_name(stm, *device, + CUBEB_DEVICE_TYPE_INPUT); + if (r != CUBEB_OK) { + return r; + } return CUBEB_OK; #endif @@ -2232,52 +3114,14 @@ int audiounit_stream_device_destroy(cubeb_stream * /* stream */, int audiounit_stream_register_device_changed_callback(cubeb_stream * stream, cubeb_device_changed_callback device_changed_callback) { + auto_lock dev_cb_lock(stream->device_changed_callback_lock); /* Note: second register without unregister first causes 'nope' error. * Current implementation requires unregister before register a new cb. */ - assert(!stream->device_changed_callback); - - auto_lock lock(stream->mutex); - + assert(!device_changed_callback || !stream->device_changed_callback); stream->device_changed_callback = device_changed_callback; - return CUBEB_OK; } -static OSStatus -audiounit_get_devices(AudioObjectID ** devices, uint32_t * count) -{ - OSStatus ret; - UInt32 size = 0; - AudioObjectPropertyAddress adr = { kAudioHardwarePropertyDevices, - kAudioObjectPropertyScopeGlobal, - kAudioObjectPropertyElementMaster }; - - ret = AudioObjectGetPropertyDataSize(kAudioObjectSystemObject, &adr, 0, NULL, &size); - if (ret != noErr) { - return ret; - } - - *count = static_cast<uint32_t>(size / sizeof(AudioObjectID)); - if (size >= sizeof(AudioObjectID)) { - if (*devices != NULL) { - delete [] (*devices); - } - *devices = new AudioObjectID[*count]; - PodZero(*devices, *count); - - ret = AudioObjectGetPropertyData(kAudioObjectSystemObject, &adr, 0, NULL, &size, (void *)*devices); - if (ret != noErr) { - delete [] (*devices); - *devices = NULL; - } - } else { - *devices = NULL; - ret = -1; - } - - return ret; -} - static char * audiounit_strref_to_cstr_utf8(CFStringRef strref) { @@ -2288,11 +3132,12 @@ audiounit_strref_to_cstr_utf8(CFStringRef strref) } len = CFStringGetLength(strref); - size = CFStringGetMaximumSizeForEncoding(len, kCFStringEncodingUTF8); - ret = static_cast<char *>(malloc(size)); + // Add 1 to size to allow for '\0' termination character. + size = CFStringGetMaximumSizeForEncoding(len, kCFStringEncodingUTF8) + 1; + ret = new char[size]; if (!CFStringGetCString(strref, ret, size, kCFStringEncodingUTF8)) { - free(ret); + delete [] ret; ret = NULL; } @@ -2339,19 +3184,18 @@ audiounit_get_available_samplerate(AudioObjectID devid, AudioObjectPropertyScope AudioValueRange range; if (AudioObjectHasProperty(devid, &adr) && AudioObjectGetPropertyDataSize(devid, &adr, 0, NULL, &size) == noErr) { - uint32_t i, count = size / sizeof(AudioValueRange); - AudioValueRange * ranges = new AudioValueRange[count]; + uint32_t count = size / sizeof(AudioValueRange); + vector<AudioValueRange> ranges(count); range.mMinimum = 9999999999.0; range.mMaximum = 0.0; - if (AudioObjectGetPropertyData(devid, &adr, 0, NULL, &size, ranges) == noErr) { - for (i = 0; i < count; i++) { + if (AudioObjectGetPropertyData(devid, &adr, 0, NULL, &size, ranges.data()) == noErr) { + for (uint32_t i = 0; i < count; i++) { if (ranges[i].mMaximum > range.mMaximum) range.mMaximum = ranges[i].mMaximum; if (ranges[i].mMinimum < range.mMinimum) range.mMinimum = ranges[i].mMinimum; } } - delete [] ranges; *max = static_cast<uint32_t>(range.mMaximum); *min = static_cast<uint32_t>(range.mMinimum); } else { @@ -2364,7 +3208,7 @@ static UInt32 audiounit_get_device_presentation_latency(AudioObjectID devid, AudioObjectPropertyScope scope) { AudioObjectPropertyAddress adr = { 0, scope, kAudioObjectPropertyElementMaster }; - UInt32 size, dev, stream = 0, offset; + UInt32 size, dev, stream = 0; AudioStreamID sid[1]; adr.mSelector = kAudioDevicePropertyLatency; @@ -2381,216 +3225,249 @@ audiounit_get_device_presentation_latency(AudioObjectID devid, AudioObjectProper AudioObjectGetPropertyData(sid[0], &adr, 0, NULL, &size, &stream); } - adr.mSelector = kAudioDevicePropertySafetyOffset; - size = sizeof(UInt32); - if (AudioObjectGetPropertyData(devid, &adr, 0, NULL, &size, &offset) != noErr) { - offset = 0; - } - - return dev + stream + offset; + return dev + stream; } -static cubeb_device_info * -audiounit_create_device_from_hwdev(AudioObjectID devid, cubeb_device_type type) +static int +audiounit_create_device_from_hwdev(cubeb_device_info * dev_info, AudioObjectID devid, cubeb_device_type type) { AudioObjectPropertyAddress adr = { 0, 0, kAudioObjectPropertyElementMaster }; - UInt32 size, ch, latency; - cubeb_device_info * ret; - CFStringRef str = NULL; - AudioValueRange range; + UInt32 size; if (type == CUBEB_DEVICE_TYPE_OUTPUT) { adr.mScope = kAudioDevicePropertyScopeOutput; } else if (type == CUBEB_DEVICE_TYPE_INPUT) { adr.mScope = kAudioDevicePropertyScopeInput; } else { - return NULL; + return CUBEB_ERROR; } - ch = audiounit_get_channel_count(devid, adr.mScope); + UInt32 ch = audiounit_get_channel_count(devid, adr.mScope); if (ch == 0) { - return NULL; + return CUBEB_ERROR; } - ret = new cubeb_device_info; - PodZero(ret, 1); + PodZero(dev_info, 1); + CFStringRef device_id_str = nullptr; size = sizeof(CFStringRef); adr.mSelector = kAudioDevicePropertyDeviceUID; - if (AudioObjectGetPropertyData(devid, &adr, 0, NULL, &size, &str) == noErr && str != NULL) { - ret->device_id = audiounit_strref_to_cstr_utf8(str); - ret->devid = (cubeb_devid)(size_t)devid; - ret->group_id = strdup(ret->device_id); - CFRelease(str); + OSStatus ret = AudioObjectGetPropertyData(devid, &adr, 0, NULL, &size, &device_id_str); + if ( ret == noErr && device_id_str != NULL) { + dev_info->device_id = audiounit_strref_to_cstr_utf8(device_id_str); + static_assert(sizeof(cubeb_devid) >= sizeof(decltype(devid)), "cubeb_devid can't represent devid"); + dev_info->devid = reinterpret_cast<cubeb_devid>(devid); + dev_info->group_id = dev_info->device_id; + CFRelease(device_id_str); } - size = sizeof(CFStringRef); - adr.mSelector = kAudioObjectPropertyName; - if (AudioObjectGetPropertyData(devid, &adr, 0, NULL, &size, &str) == noErr && str != NULL) { - UInt32 ds; - size = sizeof(UInt32); - adr.mSelector = kAudioDevicePropertyDataSource; - if (AudioObjectGetPropertyData(devid, &adr, 0, NULL, &size, &ds) == noErr) { - CFStringRef dsname; - AudioValueTranslation trl = { &ds, sizeof(ds), &dsname, sizeof(dsname) }; - adr.mSelector = kAudioDevicePropertyDataSourceNameForIDCFString; - size = sizeof(AudioValueTranslation); - // If there is a datasource for this device, use it instead of the device - // name. - if (AudioObjectGetPropertyData(devid, &adr, 0, NULL, &size, &trl) == noErr) { - CFRelease(str); - str = dsname; - } - } + CFStringRef friendly_name_str = nullptr; + UInt32 ds; + size = sizeof(UInt32); + adr.mSelector = kAudioDevicePropertyDataSource; + ret = AudioObjectGetPropertyData(devid, &adr, 0, NULL, &size, &ds); + if (ret == noErr) { + AudioValueTranslation trl = { &ds, sizeof(ds), &friendly_name_str, sizeof(CFStringRef) }; + adr.mSelector = kAudioDevicePropertyDataSourceNameForIDCFString; + size = sizeof(AudioValueTranslation); + AudioObjectGetPropertyData(devid, &adr, 0, NULL, &size, &trl); + } - ret->friendly_name = audiounit_strref_to_cstr_utf8(str); - CFRelease(str); + // If there is no datasource for this device, fall back to the + // device name. + if (!friendly_name_str) { + size = sizeof(CFStringRef); + adr.mSelector = kAudioObjectPropertyName; + AudioObjectGetPropertyData(devid, &adr, 0, NULL, &size, &friendly_name_str); } + if (friendly_name_str) { + dev_info->friendly_name = audiounit_strref_to_cstr_utf8(friendly_name_str); + CFRelease(friendly_name_str); + } else { + // Couldn't get a datasource name nor a device name, return a + // valid string of length 0. + char * fallback_name = new char[1]; + fallback_name[0] = '\0'; + dev_info->friendly_name = fallback_name; + } + + CFStringRef vendor_name_str = nullptr; size = sizeof(CFStringRef); adr.mSelector = kAudioObjectPropertyManufacturer; - if (AudioObjectGetPropertyData(devid, &adr, 0, NULL, &size, &str) == noErr && str != NULL) { - ret->vendor_name = audiounit_strref_to_cstr_utf8(str); - CFRelease(str); + ret = AudioObjectGetPropertyData(devid, &adr, 0, NULL, &size, &vendor_name_str); + if (ret == noErr && vendor_name_str != NULL) { + dev_info->vendor_name = audiounit_strref_to_cstr_utf8(vendor_name_str); + CFRelease(vendor_name_str); } - ret->type = type; - ret->state = CUBEB_DEVICE_STATE_ENABLED; - ret->preferred = (devid == audiounit_get_default_device_id(type)) ? + dev_info->type = type; + dev_info->state = CUBEB_DEVICE_STATE_ENABLED; + dev_info->preferred = (devid == audiounit_get_default_device_id(type)) ? CUBEB_DEVICE_PREF_ALL : CUBEB_DEVICE_PREF_NONE; - ret->max_channels = ch; - ret->format = (cubeb_device_fmt)CUBEB_DEVICE_FMT_ALL; /* CoreAudio supports All! */ + dev_info->max_channels = ch; + dev_info->format = (cubeb_device_fmt)CUBEB_DEVICE_FMT_ALL; /* CoreAudio supports All! */ /* kAudioFormatFlagsAudioUnitCanonical is deprecated, prefer floating point */ - ret->default_format = CUBEB_DEVICE_FMT_F32NE; + dev_info->default_format = CUBEB_DEVICE_FMT_F32NE; audiounit_get_available_samplerate(devid, adr.mScope, - &ret->min_rate, &ret->max_rate, &ret->default_rate); + &dev_info->min_rate, &dev_info->max_rate, &dev_info->default_rate); - latency = audiounit_get_device_presentation_latency(devid, adr.mScope); + UInt32 latency = audiounit_get_device_presentation_latency(devid, adr.mScope); + AudioValueRange range; adr.mSelector = kAudioDevicePropertyBufferFrameSizeRange; size = sizeof(AudioValueRange); - if (AudioObjectGetPropertyData(devid, &adr, 0, NULL, &size, &range) == noErr) { - ret->latency_lo = latency + range.mMinimum; - ret->latency_hi = latency + range.mMaximum; + ret = AudioObjectGetPropertyData(devid, &adr, 0, NULL, &size, &range); + if (ret == noErr) { + dev_info->latency_lo = latency + range.mMinimum; + dev_info->latency_hi = latency + range.mMaximum; } else { - ret->latency_lo = 10 * ret->default_rate / 1000; /* Default to 10ms */ - ret->latency_hi = 100 * ret->default_rate / 1000; /* Default to 100ms */ + dev_info->latency_lo = 10 * dev_info->default_rate / 1000; /* Default to 10ms */ + dev_info->latency_hi = 100 * dev_info->default_rate / 1000; /* Default to 100ms */ } - return ret; + return CUBEB_OK; +} + +bool +is_aggregate_device(cubeb_device_info * device_info) +{ + assert(device_info->friendly_name); + return !strncmp(device_info->friendly_name, PRIVATE_AGGREGATE_DEVICE_NAME, + strlen(PRIVATE_AGGREGATE_DEVICE_NAME)); } static int audiounit_enumerate_devices(cubeb * /* context */, cubeb_device_type type, - cubeb_device_collection ** collection) + cubeb_device_collection * collection) { - AudioObjectID * hwdevs = NULL; - uint32_t i, hwdevcount = 0; - OSStatus err; + vector<AudioObjectID> input_devs; + vector<AudioObjectID> output_devs; - if ((err = audiounit_get_devices(&hwdevs, &hwdevcount)) != noErr) { - return CUBEB_ERROR; + // Count number of input and output devices. This is not + // necessarily the same as the count of raw devices supported by the + // system since, for example, with Soundflower installed, some + // devices may report as being both input *and* output and cubeb + // separates those into two different devices. + + if (type & CUBEB_DEVICE_TYPE_OUTPUT) { + output_devs = audiounit_get_devices_of_type(CUBEB_DEVICE_TYPE_OUTPUT); } - *collection = static_cast<cubeb_device_collection *>(malloc(sizeof(cubeb_device_collection) + - sizeof(cubeb_device_info*) * (hwdevcount > 0 ? hwdevcount - 1 : 0))); - (*collection)->count = 0; + if (type & CUBEB_DEVICE_TYPE_INPUT) { + input_devs = audiounit_get_devices_of_type(CUBEB_DEVICE_TYPE_INPUT); + } - if (hwdevcount > 0) { - cubeb_device_info * cur; + auto devices = new cubeb_device_info[output_devs.size() + input_devs.size()]; + collection->count = 0; - if (type & CUBEB_DEVICE_TYPE_OUTPUT) { - for (i = 0; i < hwdevcount; i++) { - if ((cur = audiounit_create_device_from_hwdev(hwdevs[i], CUBEB_DEVICE_TYPE_OUTPUT)) != NULL) - (*collection)->device[(*collection)->count++] = cur; + if (type & CUBEB_DEVICE_TYPE_OUTPUT) { + for (auto dev: output_devs) { + auto device = &devices[collection->count]; + auto err = audiounit_create_device_from_hwdev(device, dev, CUBEB_DEVICE_TYPE_OUTPUT); + if (err != CUBEB_OK || is_aggregate_device(device)) { + continue; } + collection->count += 1; } + } - if (type & CUBEB_DEVICE_TYPE_INPUT) { - for (i = 0; i < hwdevcount; i++) { - if ((cur = audiounit_create_device_from_hwdev(hwdevs[i], CUBEB_DEVICE_TYPE_INPUT)) != NULL) - (*collection)->device[(*collection)->count++] = cur; + if (type & CUBEB_DEVICE_TYPE_INPUT) { + for (auto dev: input_devs) { + auto device = &devices[collection->count]; + auto err = audiounit_create_device_from_hwdev(device, dev, CUBEB_DEVICE_TYPE_INPUT); + if (err != CUBEB_OK || is_aggregate_device(device)) { + continue; } + collection->count += 1; } } - delete [] hwdevs; + if (collection->count > 0) { + collection->device = devices; + } else { + delete [] devices; + collection->device = NULL; + } return CUBEB_OK; } -/* qsort compare method. */ -int compare_devid(const void * a, const void * b) +static void +audiounit_device_destroy(cubeb_device_info * device) { - return (*(AudioObjectID*)a - *(AudioObjectID*)b); + delete [] device->device_id; + delete [] device->friendly_name; + delete [] device->vendor_name; } -static uint32_t -audiounit_get_devices_of_type(cubeb_device_type devtype, AudioObjectID ** devid_array) +static int +audiounit_device_collection_destroy(cubeb * /* context */, + cubeb_device_collection * collection) { - assert(devid_array == NULL || *devid_array == NULL); + for (size_t i = 0; i < collection->count; i++) { + audiounit_device_destroy(&collection->device[i]); + } + delete [] collection->device; - AudioObjectPropertyAddress adr = { kAudioHardwarePropertyDevices, - kAudioObjectPropertyScopeGlobal, - kAudioObjectPropertyElementMaster }; + return CUBEB_OK; +} + +static vector<AudioObjectID> +audiounit_get_devices_of_type(cubeb_device_type devtype) +{ UInt32 size = 0; - OSStatus ret = AudioObjectGetPropertyDataSize(kAudioObjectSystemObject, &adr, 0, NULL, &size); + OSStatus ret = AudioObjectGetPropertyDataSize(kAudioObjectSystemObject, + &DEVICES_PROPERTY_ADDRESS, 0, + NULL, &size); if (ret != noErr) { - return 0; + return vector<AudioObjectID>(); } - /* Total number of input and output devices. */ - uint32_t count = (uint32_t)(size / sizeof(AudioObjectID)); - - AudioObjectID devices[count]; - ret = AudioObjectGetPropertyData(kAudioObjectSystemObject, &adr, 0, NULL, &size, &devices); + vector<AudioObjectID> devices(size / sizeof(AudioObjectID)); + ret = AudioObjectGetPropertyData(kAudioObjectSystemObject, + &DEVICES_PROPERTY_ADDRESS, 0, NULL, &size, + devices.data()); if (ret != noErr) { - return 0; + return vector<AudioObjectID>(); } + + // Remove the aggregate device from the list of devices (if any). + for (auto it = devices.begin(); it != devices.end();) { + CFStringRef name = get_device_name(*it); + if (name && CFStringFind(name, CFSTR("CubebAggregateDevice"), 0).location != + kCFNotFound) { + it = devices.erase(it); + } else { + it++; + } + if (name) { + CFRelease(name); + } + } + /* Expected sorted but did not find anything in the docs. */ - qsort(devices, count, sizeof(AudioObjectID), compare_devid); + sort(devices.begin(), devices.end(), [](AudioObjectID a, AudioObjectID b) { + return a < b; + }); if (devtype == (CUBEB_DEVICE_TYPE_INPUT | CUBEB_DEVICE_TYPE_OUTPUT)) { - if (devid_array) { - *devid_array = new AudioObjectID[count]; - assert(*devid_array); - memcpy(*devid_array, &devices, count * sizeof(AudioObjectID)); - } - return count; + return devices; } AudioObjectPropertyScope scope = (devtype == CUBEB_DEVICE_TYPE_INPUT) ? kAudioDevicePropertyScopeInput : kAudioDevicePropertyScopeOutput; - uint32_t dev_count = 0; - AudioObjectID devices_in_scope[count]; - for(uint32_t i = 0; i < count; ++i) { + vector<AudioObjectID> devices_in_scope; + for (uint32_t i = 0; i < devices.size(); ++i) { /* For device in the given scope channel must be > 0. */ if (audiounit_get_channel_count(devices[i], scope) > 0) { - devices_in_scope[dev_count] = devices[i]; - ++dev_count; + devices_in_scope.push_back(devices[i]); } } - if (devid_array && dev_count > 0) { - *devid_array = new AudioObjectID[dev_count]; - assert(*devid_array); - memcpy(*devid_array, &devices_in_scope, dev_count * sizeof(AudioObjectID)); - } - return dev_count; -} - -static uint32_t -audiounit_equal_arrays(AudioObjectID * left, AudioObjectID * right, uint32_t size) -{ - /* Expected sorted arrays. */ - for (uint32_t i = 0; i < size; ++i) { - if (left[i] != right[i]) { - return 0; - } - } - return 1; + return devices_in_scope; } static OSStatus @@ -2600,33 +3477,32 @@ audiounit_collection_changed_callback(AudioObjectID /* inObjectID */, void * inClientData) { cubeb * context = static_cast<cubeb *>(inClientData); - auto_lock lock(context->mutex); - - if (context->collection_changed_callback == NULL) { - /* Listener removed while waiting in mutex, abort. */ - return noErr; - } - /* Differentiate input from output changes. */ - if (context->collection_changed_devtype == CUBEB_DEVICE_TYPE_INPUT || - context->collection_changed_devtype == CUBEB_DEVICE_TYPE_OUTPUT) { - AudioObjectID * devices = NULL; - uint32_t new_number_of_devices = audiounit_get_devices_of_type(context->collection_changed_devtype, &devices); - /* When count is the same examine the devid for the case of coalescing. */ - if (context->devtype_device_count == new_number_of_devices && - audiounit_equal_arrays(devices, context->devtype_device_array, new_number_of_devices)) { - /* Device changed for the other scope, ignore. */ - delete [] devices; - return noErr; + // This can be called from inside an AudioUnit function, dispatch to another queue. + dispatch_async(context->serial_queue, ^() { + auto_lock lock(context->mutex); + if (!context->input_collection_changed_callback && + !context->output_collection_changed_callback) { + /* Listener removed while waiting in mutex, abort. */ + return; } - /* Device on desired scope changed, reset counter and array. */ - context->devtype_device_count = new_number_of_devices; - /* Free the old array before replace. */ - delete [] context->devtype_device_array; - context->devtype_device_array = devices; - } - - context->collection_changed_callback(context, context->collection_changed_user_ptr); + if (context->input_collection_changed_callback) { + vector<AudioObjectID> devices = audiounit_get_devices_of_type(CUBEB_DEVICE_TYPE_INPUT); + /* Elements in the vector expected sorted. */ + if (context->input_device_array != devices) { + context->input_device_array = devices; + context->input_collection_changed_callback(context, context->input_collection_changed_user_ptr); + } + } + if (context->output_collection_changed_callback) { + vector<AudioObjectID> devices = audiounit_get_devices_of_type(CUBEB_DEVICE_TYPE_OUTPUT); + /* Elements in the vector expected sorted. */ + if (context->output_device_array != devices) { + context->output_device_array = devices; + context->output_collection_changed_callback(context, context->output_collection_changed_user_ptr); + } + } + }); return noErr; } @@ -2636,62 +3512,65 @@ audiounit_add_device_listener(cubeb * context, cubeb_device_collection_changed_callback collection_changed_callback, void * user_ptr) { + context->mutex.assert_current_thread_owns(); + assert(devtype & (CUBEB_DEVICE_TYPE_INPUT | CUBEB_DEVICE_TYPE_OUTPUT)); /* Note: second register without unregister first causes 'nope' error. * Current implementation requires unregister before register a new cb. */ - assert(context->collection_changed_callback == NULL); - - AudioObjectPropertyAddress devAddr; - devAddr.mSelector = kAudioHardwarePropertyDevices; - devAddr.mScope = kAudioObjectPropertyScopeGlobal; - devAddr.mElement = kAudioObjectPropertyElementMaster; - - OSStatus ret = AudioObjectAddPropertyListener(kAudioObjectSystemObject, - &devAddr, - audiounit_collection_changed_callback, - context); - if (ret == noErr) { - /* Expected zero after unregister. */ - assert(context->devtype_device_count == 0); - assert(context->devtype_device_array == NULL); - /* Listener works for input and output. - * When requested one of them we need to differentiate. */ - if (devtype == CUBEB_DEVICE_TYPE_INPUT || - devtype == CUBEB_DEVICE_TYPE_OUTPUT) { - /* Used to differentiate input from output device changes. */ - context->devtype_device_count = audiounit_get_devices_of_type(devtype, &context->devtype_device_array); - } - context->collection_changed_devtype = devtype; - context->collection_changed_callback = collection_changed_callback; - context->collection_changed_user_ptr = user_ptr; + assert((devtype & CUBEB_DEVICE_TYPE_INPUT) && !context->input_collection_changed_callback || + (devtype & CUBEB_DEVICE_TYPE_OUTPUT) && !context->output_collection_changed_callback); + + if (!context->input_collection_changed_callback && + !context->output_collection_changed_callback) { + OSStatus ret = AudioObjectAddPropertyListener(kAudioObjectSystemObject, + &DEVICES_PROPERTY_ADDRESS, + audiounit_collection_changed_callback, + context); + if (ret != noErr) { + return ret; + } } - return ret; + if (devtype & CUBEB_DEVICE_TYPE_INPUT) { + /* Expected empty after unregister. */ + assert(context->input_device_array.empty()); + context->input_device_array = audiounit_get_devices_of_type(CUBEB_DEVICE_TYPE_INPUT); + context->input_collection_changed_callback = collection_changed_callback; + context->input_collection_changed_user_ptr = user_ptr; + } + if (devtype & CUBEB_DEVICE_TYPE_OUTPUT) { + /* Expected empty after unregister. */ + assert(context->output_device_array.empty()); + context->output_device_array = audiounit_get_devices_of_type(CUBEB_DEVICE_TYPE_OUTPUT); + context->output_collection_changed_callback = collection_changed_callback; + context->output_collection_changed_user_ptr = user_ptr; + } + return noErr; } static OSStatus -audiounit_remove_device_listener(cubeb * context) +audiounit_remove_device_listener(cubeb * context, cubeb_device_type devtype) { - AudioObjectPropertyAddress devAddr; - devAddr.mSelector = kAudioHardwarePropertyDevices; - devAddr.mScope = kAudioObjectPropertyScopeGlobal; - devAddr.mElement = kAudioObjectPropertyElementMaster; + context->mutex.assert_current_thread_owns(); - /* Note: unregister a non registered cb is not a problem, not checking. */ - OSStatus ret = AudioObjectRemovePropertyListener(kAudioObjectSystemObject, - &devAddr, - audiounit_collection_changed_callback, - context); - if (ret == noErr) { - /* Reset all values. */ - context->collection_changed_devtype = CUBEB_DEVICE_TYPE_UNKNOWN; - context->collection_changed_callback = NULL; - context->collection_changed_user_ptr = NULL; - context->devtype_device_count = 0; - if (context->devtype_device_array) { - delete [] context->devtype_device_array; - context->devtype_device_array = NULL; - } + if (devtype & CUBEB_DEVICE_TYPE_INPUT) { + context->input_collection_changed_callback = nullptr; + context->input_collection_changed_user_ptr = nullptr; + context->input_device_array.clear(); } - return ret; + if (devtype & CUBEB_DEVICE_TYPE_OUTPUT) { + context->output_collection_changed_callback = nullptr; + context->output_collection_changed_user_ptr = nullptr; + context->output_device_array.clear(); + } + + if (context->input_collection_changed_callback || + context->output_collection_changed_callback) { + return noErr; + } + /* Note: unregister a non registered cb is not a problem, not checking. */ + return AudioObjectRemovePropertyListener(kAudioObjectSystemObject, + &DEVICES_PROPERTY_ADDRESS, + audiounit_collection_changed_callback, + context); } int audiounit_register_device_collection_changed(cubeb * context, @@ -2699,14 +3578,18 @@ int audiounit_register_device_collection_changed(cubeb * context, cubeb_device_collection_changed_callback collection_changed_callback, void * user_ptr) { + if (devtype == CUBEB_DEVICE_TYPE_UNKNOWN) { + return CUBEB_ERROR_INVALID_PARAMETER; + } OSStatus ret; auto_lock lock(context->mutex); if (collection_changed_callback) { - ret = audiounit_add_device_listener(context, devtype, + ret = audiounit_add_device_listener(context, + devtype, collection_changed_callback, user_ptr); } else { - ret = audiounit_remove_device_listener(context); + ret = audiounit_remove_device_listener(context, devtype); } return (ret == noErr) ? CUBEB_OK : CUBEB_ERROR; } @@ -2718,15 +3601,16 @@ cubeb_ops const audiounit_ops = { /*.get_min_latency =*/ audiounit_get_min_latency, /*.get_preferred_sample_rate =*/ audiounit_get_preferred_sample_rate, /*.enumerate_devices =*/ audiounit_enumerate_devices, + /*.device_collection_destroy =*/ audiounit_device_collection_destroy, /*.destroy =*/ audiounit_destroy, /*.stream_init =*/ audiounit_stream_init, /*.stream_destroy =*/ audiounit_stream_destroy, /*.stream_start =*/ audiounit_stream_start, /*.stream_stop =*/ audiounit_stream_stop, + /*.stream_reset_default_device =*/ nullptr, /*.stream_get_position =*/ audiounit_stream_get_position, /*.stream_get_latency =*/ audiounit_stream_get_latency, /*.stream_set_volume =*/ audiounit_stream_set_volume, - /*.stream_set_panning =*/ audiounit_stream_set_panning, /*.stream_get_current_device =*/ audiounit_stream_get_current_device, /*.stream_device_destroy =*/ audiounit_stream_device_destroy, /*.stream_register_device_changed_callback =*/ audiounit_stream_register_device_changed_callback, |