/* * Copyright © 2011 Mozilla Foundation * * This program is made available under an ISC-style license. See the * accompanying file LICENSE for details. */ #undef NDEBUG #include #include #include #include #include #include #if !TARGET_OS_IPHONE #include #include #include #include #endif #include "cubeb-internal.h" #include "cubeb/cubeb.h" #include "cubeb_mixer.h" #include #include #if !TARGET_OS_IPHONE #include "cubeb_osx_run_loop.h" #endif #include "cubeb_resampler.h" #include "cubeb_ring_array.h" #include #include #include #include #include #include using namespace std; #if MAC_OS_X_VERSION_MIN_REQUIRED < 101000 typedef UInt32 AudioFormatFlags; #endif #define AU_OUT_BUS 0 #define AU_IN_BUS 1 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 = 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); 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 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 = &audiounit_ops; owned_critical_section mutex; int active_streams = 0; uint32_t global_latency_frames = 0; 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 input_device_array; vector 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 layout{CUBEB_LAYOUT_UNDEFINED}; uint32_t channels = 0; }; static unique_ptr make_sized_audio_channel_layout(size_t sz) { assert(sz >= sizeof(AudioChannelLayout)); AudioChannelLayout * acl = reinterpret_cast(calloc(1, sz)); assert(acl); // Assert the allocation works. return unique_ptr(acl, free); } enum class io_side { INPUT, OUTPUT, }; static char const * to_string(io_side side) { switch (side) { case io_side::INPUT: return "input"; case io_side::OUTPUT: return "output"; } } struct device_info { AudioDeviceID id = kAudioObjectUnknown; device_flags_value flags = DEV_UNKNOWN; }; 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; 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_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 = nullptr; AudioUnit output_unit = nullptr; /* I/O device sample 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 = 0; owned_critical_section mutex; // Hold the input samples in every input callback iteration. // Only accessed on input/output callback thread and during initial configure. unique_ptr input_linear_buffer; /* Frame counters */ atomic frames_played{0}; uint64_t frames_queued = 0; // How many frames got read from the input since the stream started (includes // padded silence) atomic frames_read{0}; // How many frames got written to the output device since the stream started atomic frames_written{0}; atomic shutdown{true}; atomic draining{false}; atomic reinit_pending{false}; atomic destroy_pending{false}; /* Latency requested by the user. */ uint32_t latency_frames = 0; atomic current_latency_frames{0}; atomic total_output_latency_frames{0}; unique_ptr resampler; /* This is true if a device change callback is currently running. */ atomic switching_device{false}; atomic 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 mixer; /* Buffer where remixing/resampling will occur when upmixing is required */ /* Only accessed from callback thread */ unique_ptr temp_buffer; size_t temp_buffer_size = 0; // size in bytes. /* Listeners indicating what system events are monitored. */ unique_ptr default_input_listener; unique_ptr default_output_listener; unique_ptr input_alive_listener; unique_ptr input_source_listener; unique_ptr output_source_listener; }; bool has_input(cubeb_stream * stm) { return stm->input_stream_params.rate != 0; } 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 ConvertHostTimeToNanos(uint64_t host_time) { static struct mach_timebase_info timebase_info; static bool initialized = false; if (!initialized) { mach_timebase_info(&timebase_info); initialized = true; } long double answer = host_time; if (timebase_info.numer != timebase_info.denom) { answer *= timebase_info.numer; answer /= timebase_info.denom; } return (uint64_t)answer; } static void audiounit_increment_active_streams(cubeb * ctx) { ctx->mutex.assert_current_thread_owns(); ctx->active_streams += 1; } static void audiounit_decrement_active_streams(cubeb * ctx) { ctx->mutex.assert_current_thread_owns(); ctx->active_streams -= 1; } static int audiounit_active_streams(cubeb * ctx) { ctx->mutex.assert_current_thread_owns(); return ctx->active_streams; } static void audiounit_set_global_latency(cubeb * ctx, uint32_t latency_frames) { ctx->mutex.assert_current_thread_owns(); assert(audiounit_active_streams(ctx) == 1); ctx->global_latency_frames = latency_frames; } static void audiounit_make_silent(AudioBuffer * ioData) { assert(ioData); assert(ioData->mData); memset(ioData->mData, 0, ioData->mDataByteSize); } static OSStatus audiounit_render_input(cubeb_stream * stm, AudioUnitRenderActionFlags * flags, AudioTimeStamp const * tstamp, UInt32 bus, UInt32 input_frames) { /* Create the AudioBufferList to store input. */ AudioBufferList input_buffer_list; input_buffer_list.mBuffers[0].mDataByteSize = stm->input_desc.mBytesPerFrame * input_frames; input_buffer_list.mBuffers[0].mData = nullptr; input_buffer_list.mBuffers[0].mNumberChannels = stm->input_desc.mChannelsPerFrame; input_buffer_list.mNumberBuffers = 1; /* Render input samples */ OSStatus r = AudioUnitRender(stm->input_unit, flags, tstamp, bus, input_frames, &input_buffer_list); if (r != noErr) { 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); } /* 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; } static OSStatus audiounit_input_callback(void * user_ptr, AudioUnitRenderActionFlags * flags, AudioTimeStamp const * tstamp, UInt32 bus, UInt32 input_frames, AudioBufferList * /* bufs */) { cubeb_stream * stm = static_cast(user_ptr); assert(stm->input_unit != NULL); assert(AU_IN_BUS == bus); if (stm->shutdown) { ALOG("(%p) input shutdown", stm); return noErr; } if (stm->draining) { OSStatus r = AudioOutputUnitStop(stm->input_unit); assert(r == 0); // Only fire state callback in input-only stream. For duplex stream, // the state callback will be fired in output callback. if (stm->output_unit == NULL) { stm->state_callback(stm, stm->user_ptr, CUBEB_STATE_DRAINED); } return noErr; } OSStatus r = audiounit_render_input(stm, flags, tstamp, bus, input_frames); if (r != noErr) { return r; } // Full Duplex. We'll call data_callback in the AudioUnit output callback. if (stm->output_unit != NULL) { return noErr; } /* Input only. Call the user callback through resampler. 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; long outframes = cubeb_resampler_fill(stm->resampler.get(), stm->input_linear_buffer->data(), &total_input_frames, NULL, 0); if (outframes < 0) { stm->shutdown = true; OSStatus r = AudioOutputUnitStop(stm->input_unit); assert(r == 0); stm->state_callback(stm, stm->user_ptr, CUBEB_STATE_ERROR); return noErr; } stm->draining = outframes < total_input_frames; // Reset input buffer stm->input_linear_buffer->clear(); return noErr; } 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 (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 audiounit_output_callback(void * user_ptr, AudioUnitRenderActionFlags * /* flags */, AudioTimeStamp const * tstamp, UInt32 bus, UInt32 output_frames, AudioBufferList * outBufferList) { assert(AU_OUT_BUS == bus); assert(outBufferList->mNumberBuffers == 1); cubeb_stream * stm = static_cast(user_ptr); uint64_t now = ConvertHostTimeToNanos(mach_absolute_time()); uint64_t audio_output_time = ConvertHostTimeToNanos(tstamp->mHostTime); uint64_t output_latency_ns = audio_output_time - now; 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; 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) { ALOG("(%p) output shutdown.", stm); audiounit_make_silent(&outBufferList->mBuffers[0]); return noErr; } if (stm->draining) { OSStatus r = AudioOutputUnitStop(stm->output_unit); assert(r == 0); stm->state_callback(stm, stm->user_ptr, CUBEB_STATE_DRAINED); audiounit_make_silent(&outBufferList->mBuffers[0]); return noErr; } /* Get output buffer. */ 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 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. 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. */ long outframes = cubeb_resampler_fill(stm->resampler.get(), input_buffer, input_buffer ? &input_frames : NULL, output_buffer, output_frames); if (input_buffer) { // 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 || 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; } stm->draining = (UInt32)outframes < output_frames; stm->frames_played = stm->frames_queued; stm->frames_queued += outframes; /* Post process output samples. */ if (stm->draining) { /* Clear missing frames (silence) */ 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); } /* 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; } extern "C" { int audiounit_init(cubeb ** context, char const * /* context_name */) { #if !TARGET_OS_IPHONE cubeb_set_coreaudio_notification_runloop(); #endif *context = new cubeb; return CUBEB_OK; } } static char const * audiounit_get_backend_id(cubeb * /* ctx */) { return "audiounit"; } #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_set_device_info(cubeb_stream * stm, AudioDeviceID id, io_side side) { assert(stm); device_info * info = nullptr; cubeb_device_type type = CUBEB_DEVICE_TYPE_UNKNOWN; 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; if (side == io_side::INPUT) { info->flags |= DEV_INPUT; } else if (side == io_side::OUTPUT) { info->flags |= DEV_OUTPUT; } 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_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 all device change listeners.", stm); } { auto_lock lock(stm->mutex); float volume = 0.0; 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); 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); } // If the stream was running, start it again. if (!stm->shutdown) { 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, 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, %u events.", stm, (unsigned int)address_count); for (UInt32 i = 0; i < address_count; i++) { switch (addresses[i].mSelector) { case kAudioHardwarePropertyDefaultOutputDevice: { LOG("Event[%u] - mSelector == kAudioHardwarePropertyDefaultOutputDevice " "for id=%d", (unsigned int)i, id); } break; case kAudioHardwarePropertyDefaultInputDevice: { LOG("Event[%u] - mSelector == kAudioHardwarePropertyDefaultInputDevice " "for id=%d", (unsigned int)i, id); } break; case kAudioDevicePropertyDeviceIsAlive: { 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->input_device.flags & DEV_SYSTEM_DEFAULT) { LOG("It's the default input device, ignore the event"); stm->switching_device = false; return noErr; } } break; case kAudioDevicePropertyDataSource: { 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: case kAudioHardwarePropertyDefaultInputDevice: case kAudioDevicePropertyDeviceIsAlive: /* fall through */ case kAudioDevicePropertyDataSource: { auto_lock dev_cb_lock(stm->device_changed_callback_lock); if (stm->device_changed_callback) { stm->device_changed_callback(stm->user_ptr); } break; } } } audiounit_reinit_stream_async(stm, switch_side); return noErr; } OSStatus audiounit_add_listener(const property_listener * listener) { assert(listener); return AudioObjectAddPropertyListener(listener->device_id, listener->property_address, listener->callback, listener->stream); } OSStatus audiounit_remove_listener(const property_listener * listener) { assert(listener); return AudioObjectRemovePropertyListener( listener->device_id, listener->property_address, listener->callback, listener->stream); } static int audiounit_install_device_changed_callback(cubeb_stream * stm) { 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. */ 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. */ 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. */ 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 r; } static int audiounit_install_system_changed_callback(cubeb_stream * stm) { OSStatus r; if (stm->output_unit) { /* This event will notify us when the default audio device changes, * 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. */ 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; } } if (stm->input_unit) { /* This event will notify us when the default input device changes. */ 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; } } return CUBEB_OK; } static int audiounit_uninstall_device_changed_callback(cubeb_stream * stm) { OSStatus rv; // Failing to uninstall listeners is not a fatal error. int r = CUBEB_OK; 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_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(); } 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 r; } static int audiounit_uninstall_system_changed_callback(cubeb_stream * stm) { OSStatus r; 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->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; } /* Get the acceptable buffer size (in frames) that this device can work with. */ static int audiounit_get_acceptable_latency_range(AudioValueRange * latency_range) { UInt32 size; OSStatus r; AudioDeviceID output_device_id; AudioObjectPropertyAddress output_device_buffer_size_range = { kAudioDevicePropertyBufferFrameSizeRange, kAudioDevicePropertyScopeOutput, kAudioObjectPropertyElementMaster}; 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; } /* Get the buffer size range this device supports */ size = sizeof(*latency_range); r = AudioObjectGetPropertyData(output_device_id, &output_device_buffer_size_range, 0, NULL, &size, latency_range); if (r != noErr) { LOG("AudioObjectGetPropertyData/buffer size range rv=%d", r); return CUBEB_ERROR; } return CUBEB_OK; } #endif /* !TARGET_OS_IPHONE */ static AudioObjectID audiounit_get_default_device_id(cubeb_device_type type) { const AudioObjectPropertyAddress * adr; if (type == CUBEB_DEVICE_TYPE_OUTPUT) { adr = &DEFAULT_OUTPUT_DEVICE_PROPERTY_ADDRESS; } else if (type == CUBEB_DEVICE_TYPE_INPUT) { adr = &DEFAULT_INPUT_DEVICE_PROPERTY_ADDRESS; } else { return kAudioObjectUnknown; } AudioDeviceID devid; UInt32 size = sizeof(AudioDeviceID); if (AudioObjectGetPropertyData(kAudioObjectSystemObject, adr, 0, NULL, &size, &devid) != noErr) { return kAudioObjectUnknown; } return devid; } int audiounit_get_max_channel_count(cubeb * ctx, uint32_t * max_channels) { #if TARGET_OS_IPHONE // TODO: [[AVAudioSession sharedInstance] maximumOutputNumberOfChannels] *max_channels = 2; #else UInt32 size; OSStatus r; AudioDeviceID output_device_id; AudioStreamBasicDescription stream_format; AudioObjectPropertyAddress stream_format_address = { kAudioDevicePropertyStreamFormat, kAudioDevicePropertyScopeOutput, kAudioObjectPropertyElementMaster}; assert(ctx && max_channels); output_device_id = audiounit_get_default_device_id(CUBEB_DEVICE_TYPE_OUTPUT); if (output_device_id == kAudioObjectUnknown) { return CUBEB_ERROR; } size = sizeof(stream_format); r = AudioObjectGetPropertyData(output_device_id, &stream_format_address, 0, NULL, &size, &stream_format); if (r != noErr) { LOG("AudioObjectPropertyAddress/StreamFormat rv=%d", r); return CUBEB_ERROR; } *max_channels = stream_format.mChannelsPerFrame; #endif return CUBEB_OK; } static int audiounit_get_min_latency(cubeb * /* ctx */, cubeb_stream_params /* params */, uint32_t * latency_frames) { #if TARGET_OS_IPHONE // TODO: [[AVAudioSession sharedInstance] inputLatency] return CUBEB_ERROR_NOT_SUPPORTED; #else AudioValueRange latency_range; if (audiounit_get_acceptable_latency_range(&latency_range) != CUBEB_OK) { LOG("Could not get acceptable latency range."); return CUBEB_ERROR; } *latency_frames = max(latency_range.mMinimum, SAFE_MIN_LATENCY_FRAMES); #endif return CUBEB_OK; } static int audiounit_get_preferred_sample_rate(cubeb * /* ctx */, uint32_t * rate) { #if TARGET_OS_IPHONE // TODO return CUBEB_ERROR_NOT_SUPPORTED; #else UInt32 size; OSStatus r; Float64 fsamplerate; AudioDeviceID output_device_id; AudioObjectPropertyAddress samplerate_address = { kAudioDevicePropertyNominalSampleRate, kAudioObjectPropertyScopeGlobal, kAudioObjectPropertyElementMaster}; output_device_id = audiounit_get_default_device_id(CUBEB_DEVICE_TYPE_OUTPUT); if (output_device_id == kAudioObjectUnknown) { return CUBEB_ERROR; } size = sizeof(fsamplerate); r = AudioObjectGetPropertyData(output_device_id, &samplerate_address, 0, NULL, &size, &fsamplerate); if (r != noErr) { return CUBEB_ERROR; } *rate = static_cast(fsamplerate); #endif return CUBEB_OK; } 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) { { 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)); } // Destroying a cubeb context with device collection callbacks registered // is misuse of the API, assert then attempt to clean up. assert(!ctx->input_collection_changed_callback && !ctx->input_collection_changed_user_ptr && !ctx->output_collection_changed_callback && !ctx->output_collection_changed_user_ptr); /* Unregister the callback if necessary. */ 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); } } dispatch_release(ctx->serial_queue); delete ctx; } static void audiounit_stream_destroy(cubeb_stream * stm); static int audio_stream_desc_init(AudioStreamBasicDescription * ss, const cubeb_stream_params * stream_params) { switch (stream_params->format) { case CUBEB_SAMPLE_S16LE: ss->mBitsPerChannel = 16; ss->mFormatFlags = kAudioFormatFlagIsSignedInteger; break; case CUBEB_SAMPLE_S16BE: ss->mBitsPerChannel = 16; ss->mFormatFlags = kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsBigEndian; break; case CUBEB_SAMPLE_FLOAT32LE: ss->mBitsPerChannel = 32; ss->mFormatFlags = kAudioFormatFlagIsFloat; break; case CUBEB_SAMPLE_FLOAT32BE: ss->mBitsPerChannel = 32; ss->mFormatFlags = kAudioFormatFlagIsFloat | kAudioFormatFlagIsBigEndian; break; default: return CUBEB_ERROR_INVALID_FORMAT; } ss->mFormatID = kAudioFormatLinearPCM; ss->mFormatFlags |= kLinearPCMFormatFlagIsPacked; ss->mSampleRate = stream_params->rate; ss->mChannelsPerFrame = stream_params->channels; ss->mBytesPerFrame = (ss->mBitsPerChannel / 8) * ss->mChannelsPerFrame; ss->mFramesPerPacket = 1; ss->mBytesPerPacket = ss->mBytesPerFrame * ss->mFramesPerPacket; ss->mReserved = 0; 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_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(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 audiounit_get_sub_devices(AudioDeviceID device_id) { vector 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(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 output_sub_devices = audiounit_get_sub_devices(output_device_id); const vector 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 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; OSStatus rv; desc.componentType = kAudioUnitType_Output; #if TARGET_OS_IPHONE 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. if ((device->flags & DEV_SYSTEM_DEFAULT) && (device->flags & DEV_OUTPUT)) { desc.componentSubType = kAudioUnitSubType_DefaultOutput; } else { desc.componentSubType = kAudioUnitSubType_HALOutput; } #endif desc.componentManufacturer = kAudioUnitManufacturer_Apple; desc.componentFlags = 0; desc.componentFlagsMask = 0; comp = AudioComponentFindNext(NULL, &desc); if (comp == NULL) { LOG("Could not find matching audio hardware."); return CUBEB_ERROR; } rv = AudioComponentInstanceNew(comp, unit); if (rv != noErr) { LOG("AudioComponentInstanceNew rv=%d", rv); return CUBEB_ERROR; } return CUBEB_OK; } enum enable_state { DISABLE, ENABLE, }; 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_create_unit(AudioUnit * unit, device_info * device) { 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 ((device->flags & DEV_SYSTEM_DEFAULT) && (device->flags & DEV_OUTPUT)) { return CUBEB_OK; } 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); } 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 int audiounit_init_input_linear_buffer(cubeb_stream * stream, uint32_t capacity) { 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(size)); } else { stream->input_linear_buffer.reset(new auto_array_wrapper_impl(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(audiounit_active_streams(stm->context) > 0); if (audiounit_active_streams(stm->context) == 1) { return max(min(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 int r; UInt32 output_buffer_size = 0; UInt32 size = sizeof(output_buffer_size); if (stm->output_unit) { r = AudioUnitGetProperty( stm->output_unit, kAudioDevicePropertyBufferFrameSize, kAudioUnitScope_Output, AU_OUT_BUS, &output_buffer_size, &size); if (r != noErr) { LOG("AudioUnitGetProperty/output/kAudioDevicePropertyBufferFrameSize " "rv=%d", r); return 0; } output_buffer_size = max(min(output_buffer_size, SAFE_MAX_LATENCY_FRAMES), SAFE_MIN_LATENCY_FRAMES); } UInt32 input_buffer_size = 0; if (stm->input_unit) { r = AudioUnitGetProperty( stm->input_unit, kAudioDevicePropertyBufferFrameSize, kAudioUnitScope_Input, AU_IN_BUS, &input_buffer_size, &size); if (r != noErr) { LOG("AudioUnitGetProperty/input/kAudioDevicePropertyBufferFrameSize " "rv=%d", r); return 0; } input_buffer_size = max(min(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 = min(input_buffer_size, output_buffer_size); } else if (input_buffer_size != 0) { upper_latency_limit = input_buffer_size; } else if (output_buffer_size != 0) { upper_latency_limit = output_buffer_size; } else { upper_latency_limit = SAFE_MAX_LATENCY_FRAMES; } return max(min(latency_frames, upper_latency_limit), SAFE_MIN_LATENCY_FRAMES); } /* * Change buffer size is prone to deadlock thus we change it * following the steps: * - register a listener for the buffer size property * - change the property * - wait until the listener is executed * - property has changed, remove the listener * */ static void buffer_size_changed_callback(void * inClientData, AudioUnit inUnit, AudioUnitPropertyID inPropertyID, AudioUnitScope inScope, AudioUnitElement inElement) { cubeb_stream * stm = (cubeb_stream *)inClientData; AudioUnit au = inUnit; AudioUnitScope au_scope = kAudioUnitScope_Input; AudioUnitElement au_element = inElement; char const * au_type = "output"; if (AU_IN_BUS == inElement) { au_scope = kAudioUnitScope_Output; au_type = "input"; } switch (inPropertyID) { case kAudioDevicePropertyBufferFrameSize: { if (inScope != au_scope) { break; } UInt32 new_buffer_size; UInt32 outSize = sizeof(UInt32); OSStatus r = AudioUnitGetProperty(au, kAudioDevicePropertyBufferFrameSize, au_scope, au_element, &new_buffer_size, &outSize); if (r != noErr) { LOG("(%p) Event: kAudioDevicePropertyBufferFrameSize: Cannot get current " "buffer size", stm); } else { LOG("(%p) Event: kAudioDevicePropertyBufferFrameSize: New %s buffer size " "= %d for scope %d", stm, au_type, new_buffer_size, inScope); } stm->buffer_size_change_state = true; break; } } } static int 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; if (side == io_side::INPUT) { au = stm->input_unit; au_scope = kAudioUnitScope_Output; au_element = AU_IN_BUS; } uint32_t buffer_frames = 0; UInt32 size = sizeof(buffer_frames); int r = AudioUnitGetProperty(au, kAudioDevicePropertyBufferFrameSize, au_scope, au_element, &buffer_frames, &size); if (r != noErr) { 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, to_string(side), buffer_frames); return CUBEB_OK; } r = AudioUnitAddPropertyListener(au, kAudioDevicePropertyBufferFrameSize, buffer_size_changed_callback, stm); if (r != noErr) { LOG("AudioUnitAddPropertyListener/%s/kAudioDevicePropertyBufferFrameSize " "rv=%d", to_string(side), r); return CUBEB_ERROR; } stm->buffer_size_change_state = false; r = AudioUnitSetProperty(au, kAudioDevicePropertyBufferFrameSize, au_scope, au_element, &new_size_frames, sizeof(new_size_frames)); if (r != noErr) { LOG("AudioUnitSetProperty/%s/kAudioDevicePropertyBufferFrameSize rv=%d", to_string(side), r); r = AudioUnitRemovePropertyListenerWithUserData( au, kAudioDevicePropertyBufferFrameSize, buffer_size_changed_callback, stm); if (r != noErr) { LOG("AudioUnitAddPropertyListener/%s/kAudioDevicePropertyBufferFrameSize " "rv=%d", to_string(side), r); } return CUBEB_ERROR; } int count = 0; while (!stm->buffer_size_change_state && count++ < 30) { struct timespec req, rem; req.tv_sec = 0; req.tv_nsec = 100000000L; // 0.1 sec if (nanosleep(&req, &rem) < 0) { LOG("(%p) Warning: nanosleep call failed or interrupted. Remaining time " "%ld nano secs \n", stm, rem.tv_nsec); } LOG("(%p) audiounit_set_buffer_size : wait count = %d", stm, count); } r = AudioUnitRemovePropertyListenerWithUserData( au, kAudioDevicePropertyBufferFrameSize, buffer_size_changed_callback, stm); if (r != noErr) { LOG("AudioUnitAddPropertyListener/%s/kAudioDevicePropertyBufferFrameSize " "rv=%d", to_string(side), r); return CUBEB_ERROR; } if (!stm->buffer_size_change_state && count >= 30) { LOG("(%p) Error, did not get buffer size change callback ...", stm); return CUBEB_ERROR; } 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; LOG("(%p) Opening input side: rate %u, channels %u, format %d, latency in " "frames %u.", stm, stm->input_stream_params.rate, stm->input_stream_params.channels, stm->input_stream_params.format, stm->latency_frames); /* Get input device sample rate. */ AudioStreamBasicDescription input_hw_desc; size = sizeof(AudioStreamBasicDescription); r = AudioUnitGetProperty(stm->input_unit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, AU_IN_BUS, &input_hw_desc, &size); if (r != noErr) { LOG("AudioUnitGetProperty/input/kAudioUnitProperty_StreamFormat rv=%d", r); return CUBEB_ERROR; } stm->input_hw_rate = input_hw_desc.mSampleRate; LOG("(%p) Input device sampling rate: %.2f", stm, stm->input_hw_rate); /* Set format description according to the input params. */ r = audio_stream_desc_init(&stm->input_desc, &stm->input_stream_params); if (r != CUBEB_OK) { LOG("(%p) Setting format description for input failed.", stm); return r; } // Use latency to set buffer size 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; } AudioStreamBasicDescription src_desc = stm->input_desc; /* Input AudioUnit must be configured with device's sample rate. we will resample inside input callback. */ src_desc.mSampleRate = stm->input_hw_rate; r = AudioUnitSetProperty(stm->input_unit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Output, AU_IN_BUS, &src_desc, sizeof(AudioStreamBasicDescription)); if (r != noErr) { LOG("AudioUnitSetProperty/input/kAudioUnitProperty_StreamFormat rv=%d", r); return CUBEB_ERROR; } /* Frames per buffer in the input callback. */ r = AudioUnitSetProperty( stm->input_unit, kAudioUnitProperty_MaximumFramesPerSlice, kAudioUnitScope_Global, AU_IN_BUS, &stm->latency_frames, sizeof(UInt32)); if (r != noErr) { LOG("AudioUnitSetProperty/input/kAudioUnitProperty_MaximumFramesPerSlice " "rv=%d", r); return CUBEB_ERROR; } // Input only capacity unsigned int array_capacity = 1; if (has_output(stm)) { // Full-duplex increase capacity array_capacity = 8; } if (audiounit_init_input_linear_buffer(stm, array_capacity) != CUBEB_OK) { return CUBEB_ERROR; } aurcbs_in.inputProc = audiounit_input_callback; aurcbs_in.inputProcRefCon = stm; r = AudioUnitSetProperty( stm->input_unit, kAudioOutputUnitProperty_SetInputCallback, kAudioUnitScope_Global, AU_OUT_BUS, &aurcbs_in, sizeof(aurcbs_in)); if (r != noErr) { 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; } static int audiounit_configure_output(cubeb_stream * stm) { assert(stm && stm->output_unit); int r; AURenderCallbackStruct aurcbs_out; UInt32 size; LOG("(%p) Opening output side: rate %u, channels %u, format %d, latency in " "frames %u.", stm, stm->output_stream_params.rate, stm->output_stream_params.channels, stm->output_stream_params.format, stm->latency_frames); r = audio_stream_desc_init(&stm->output_desc, &stm->output_stream_params); if (r != CUBEB_OK) { LOG("(%p) Could not initialize the audio stream description.", stm); return r; } /* Get output device sample rate. */ AudioStreamBasicDescription output_hw_desc; size = sizeof(AudioStreamBasicDescription); memset(&output_hw_desc, 0, size); r = AudioUnitGetProperty(stm->output_unit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Output, AU_OUT_BUS, &output_hw_desc, &size); if (r != noErr) { 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, kAudioUnitScope_Input, AU_OUT_BUS, &stm->output_desc, sizeof(AudioStreamBasicDescription)); if (r != noErr) { LOG("AudioUnitSetProperty/output/kAudioUnitProperty_StreamFormat rv=%d", r); return CUBEB_ERROR; } 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; } /* Frames per buffer in the input callback. */ r = AudioUnitSetProperty( stm->output_unit, kAudioUnitProperty_MaximumFramesPerSlice, kAudioUnitScope_Global, AU_OUT_BUS, &stm->latency_frames, sizeof(UInt32)); if (r != noErr) { LOG("AudioUnitSetProperty/output/kAudioUnitProperty_MaximumFramesPerSlice " "rv=%d", r); return CUBEB_ERROR; } aurcbs_out.inputProc = audiounit_output_callback; aurcbs_out.inputProcRefCon = stm; r = AudioUnitSetProperty( stm->output_unit, kAudioUnitProperty_SetRenderCallback, kAudioUnitScope_Global, AU_OUT_BUS, &aurcbs_out, sizeof(aurcbs_out)); if (r != noErr) { 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; } static int 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, &in_dev_info); if (r != CUBEB_OK) { LOG("(%p) AudioUnit creation for input failed.", stm); return r; } } if (has_output(stm)) { r = audiounit_create_unit(&stm->output_unit, &out_dev_info); if (r != CUBEB_OK) { LOG("(%p) AudioUnit creation for output failed.", stm); return r; } } /* Latency cannot change if another stream is operating in parallel. In this * case 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. */ stm->latency_frames = audiounit_clamp_latency(stm, stm->latency_frames); assert(stm->latency_frames); // Ugly error check audiounit_set_global_latency(stm->context, stm->latency_frames); } /* Configure I/O stream */ if (has_input(stm)) { r = audiounit_configure_input(stm); if (r != CUBEB_OK) { LOG("(%p) Configure audiounit input failed.", stm); return r; } } if (has_output(stm)) { r = audiounit_configure_output(stm); if (r != CUBEB_OK) { LOG("(%p) Configure audiounit output failed.", stm); return r; } } // Setting the latency doesn't work well for USB headsets (eg. plantronics). // Keep the default latency for now. #if 0 buffer_size = latency; /* Get the range of latency this particular device can work with, and clamp * the requested latency to this acceptable range. */ #if !TARGET_OS_IPHONE if (audiounit_get_acceptable_latency_range(&latency_range) != CUBEB_OK) { return CUBEB_ERROR; } if (buffer_size < (unsigned int) latency_range.mMinimum) { buffer_size = (unsigned int) latency_range.mMinimum; } else if (buffer_size > (unsigned int) latency_range.mMaximum) { buffer_size = (unsigned int) latency_range.mMaximum; } /** * Get the default buffer size. If our latency request is below the default, * set it. Otherwise, use the default latency. **/ size = sizeof(default_buffer_size); if (AudioUnitGetProperty(stm->output_unit, kAudioDevicePropertyBufferFrameSize, kAudioUnitScope_Output, 0, &default_buffer_size, &size) != 0) { return CUBEB_ERROR; } if (buffer_size < default_buffer_size) { /* Set the maximum number of frame that the render callback will ask for, * effectively setting the latency of the stream. This is process-wide. */ if (AudioUnitSetProperty(stm->output_unit, kAudioDevicePropertyBufferFrameSize, kAudioUnitScope_Output, 0, &buffer_size, sizeof(buffer_size)) != 0) { return CUBEB_ERROR; } } #else // TARGET_OS_IPHONE //TODO: [[AVAudioSession sharedInstance] inputLatency] // http://stackoverflow.com/questions/13157523/kaudiodevicepropertybufferframesize-replacement-for-ios #endif #endif /* We use a resampler because input AudioUnit operates * reliable only in the capture device sample rate. * Resampler will convert it to the user sample rate * and deliver it to the callback. */ uint32_t target_sample_rate; if (has_input(stm)) { target_sample_rate = stm->input_stream_params.rate; } else { assert(has_output(stm)); target_sample_rate = stm->output_stream_params.rate; } cubeb_stream_params input_unconverted_params; if (has_input(stm)) { input_unconverted_params = stm->input_stream_params; /* Use the rate of the input device. */ input_unconverted_params.rate = stm->input_hw_rate; } /* Create resampler. Output params are unchanged * because we do not need conversion on the output. */ 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, CUBEB_RESAMPLER_RECLOCK_NONE)); if (!stm->resampler) { LOG("(%p) Could not create resampler.", stm); return CUBEB_ERROR; } if (stm->input_unit != NULL) { r = AudioUnitInitialize(stm->input_unit); if (r != noErr) { LOG("AudioUnitInitialize/input rv=%d", r); return CUBEB_ERROR; } } if (stm->output_unit != NULL) { r = AudioUnitInitialize(stm->output_unit); if (r != noErr) { 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(unit_s * stm->output_desc.mSampleRate); } } if (stm->input_unit && stm->output_unit) { // According to the I/O hardware rate it is expected a specific pattern of // callbacks for example is input is 44100 and output is 48000 we expected // no more than 2 out callback in a row. stm->expected_output_callbacks_in_a_row = ceilf(stm->output_hw_rate / stm->input_hw_rate); } r = audiounit_install_device_changed_callback(stm); if (r != CUBEB_OK) { 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, char const * /* stream_name */, cubeb_devid input_device, cubeb_stream_params * input_stream_params, cubeb_devid output_device, cubeb_stream_params * output_stream_params, unsigned int latency_frames, cubeb_data_callback data_callback, cubeb_state_callback state_callback, void * user_ptr) { assert(context); auto_lock context_lock(context->mutex); audiounit_increment_active_streams(context); unique_ptr stm( new cubeb_stream(context), audiounit_stream_destroy_internal); int r; *stream = NULL; assert(latency_frames > 0); /* These could be different in the future if we have both * full-duplex stream and different devices for input vs output. */ stm->data_callback = data_callback; stm->state_callback = state_callback; stm->user_ptr = user_ptr; stm->latency_frames = latency_frames; 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; r = audiounit_set_device_info( stm.get(), reinterpret_cast(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; r = audiounit_set_device_info( stm.get(), reinterpret_cast(output_device), io_side::OUTPUT); if (r != CUBEB_OK) { LOG("(%p) Fail to set device info for output.", stm.get()); return r; } } { // 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`. auto_lock lock(stm->mutex); r = audiounit_setup_stream(stm.get()); } if (r != CUBEB_OK) { LOG("(%p) Could not setup the audiounit stream.", stm.get()); return r; } r = audiounit_install_system_changed_callback(stm.get()); if (r != CUBEB_OK) { LOG("(%p) Could not install the device change callback.", stm.get()); return r; } *stream = stm.release(); LOG("(%p) Cubeb stream init successful.", *stream); return CUBEB_OK; } static void audiounit_close_stream(cubeb_stream * stm) { stm->mutex.assert_current_thread_owns(); if (stm->input_unit) { AudioUnitUninitialize(stm->input_unit); AudioComponentInstanceDispose(stm->input_unit); stm->input_unit = nullptr; } stm->input_linear_buffer.reset(); if (stm->output_unit) { AudioUnitUninitialize(stm->output_unit); AudioComponentInstanceDispose(stm->output_unit); stm->output_unit = nullptr; } 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_internal(cubeb_stream * stm) { 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 all device change listeners", 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) { 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 all device change listeners", 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 context_lock(stm->context->mutex); audiounit_stream_destroy_internal(stm); }); LOG("Cubeb stream (%p) destroyed successful.", stm); delete stm; } static int audiounit_stream_start_internal(cubeb_stream * stm) { OSStatus r; if (stm->input_unit != NULL) { r = AudioOutputUnitStart(stm->input_unit); if (r != noErr) { LOG("AudioOutputUnitStart (input) rv=%d", r); return CUBEB_ERROR; } } if (stm->output_unit != NULL) { r = AudioOutputUnitStart(stm->output_unit); if (r != noErr) { LOG("AudioOutputUnitStart (output) rv=%d", r); return CUBEB_ERROR; } } return CUBEB_OK; } static int audiounit_stream_start(cubeb_stream * stm) { auto_lock context_lock(stm->context->mutex); stm->shutdown = false; stm->draining = false; int r = audiounit_stream_start_internal(stm); if (r != CUBEB_OK) { return r; } stm->state_callback(stm, stm->user_ptr, CUBEB_STATE_STARTED); LOG("Cubeb stream (%p) started successfully.", stm); return CUBEB_OK; } void audiounit_stream_stop_internal(cubeb_stream * stm) { OSStatus r; if (stm->input_unit != NULL) { r = AudioOutputUnitStop(stm->input_unit); assert(r == 0); } if (stm->output_unit != NULL) { r = AudioOutputUnitStop(stm->output_unit); assert(r == 0); } } static int audiounit_stream_stop(cubeb_stream * stm) { auto_lock context_lock(stm->context->mutex); stm->shutdown = true; audiounit_stream_stop_internal(stm); stm->state_callback(stm, stm->user_ptr, CUBEB_STATE_STOPPED); LOG("Cubeb stream (%p) stopped successfully.", stm); return CUBEB_OK; } static int audiounit_stream_get_position(cubeb_stream * stm, uint64_t * position) { assert(stm); if (stm->current_latency_frames > stm->frames_played) { *position = 0; } else { *position = stm->frames_played - stm->current_latency_frames; } return CUBEB_OK; } int audiounit_stream_get_latency(cubeb_stream * stm, uint32_t * latency) { #if TARGET_OS_IPHONE // TODO return CUBEB_ERROR_NOT_SUPPORTED; #else *latency = stm->total_output_latency_frames; return CUBEB_OK; #endif } static int audiounit_stream_get_volume(cubeb_stream * stm, float * volume) { assert(stm->output_unit); OSStatus r = AudioUnitGetParameter(stm->output_unit, kHALOutputParam_Volume, kAudioUnitScope_Global, 0, volume); if (r != noErr) { LOG("AudioUnitGetParameter/kHALOutputParam_Volume rv=%d", r); return CUBEB_ERROR; } return CUBEB_OK; } 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) { LOG("AudioUnitSetParameter/kHALOutputParam_Volume rv=%d", r); return CUBEB_ERROR; } return CUBEB_OK; } unique_ptr convert_uint32_into_string(UInt32 data) { // Simply create an empty string if no data. size_t size = data == 0 ? 0 : 4; // 4 bytes for uint32. auto str = unique_ptr{new char[size + 1]}; // + 1 for '\0'. str[size] = '\0'; if (size < 4) { return str; } // 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_get_default_device_datasource(cubeb_device_type type, UInt32 * data) { AudioDeviceID id = audiounit_get_default_device_id(type); if (id == kAudioObjectUnknown) { return CUBEB_ERROR; } 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) { *data = 0; } return CUBEB_OK; } int audiounit_get_default_device_name(cubeb_stream * stm, cubeb_device * const device, cubeb_device_type type) { assert(stm); assert(device); UInt32 data; int r = audiounit_get_default_device_datasource(type, &data); if (r != CUBEB_OK) { return r; } 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; } 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); int r = audiounit_get_default_device_name(stm, *device, CUBEB_DEVICE_TYPE_OUTPUT); if (r != CUBEB_OK) { return r; } r = audiounit_get_default_device_name(stm, *device, CUBEB_DEVICE_TYPE_INPUT); if (r != CUBEB_OK) { return r; } return CUBEB_OK; #endif } int audiounit_stream_device_destroy(cubeb_stream * /* stream */, cubeb_device * device) { delete[] device->output_name; delete[] device->input_name; delete device; return CUBEB_OK; } 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(!device_changed_callback || !stream->device_changed_callback); stream->device_changed_callback = device_changed_callback; return CUBEB_OK; } static char * audiounit_strref_to_cstr_utf8(CFStringRef strref) { CFIndex len, size; char * ret; if (strref == NULL) { return NULL; } len = CFStringGetLength(strref); // 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)) { delete[] ret; ret = NULL; } return ret; } static uint32_t audiounit_get_channel_count(AudioObjectID devid, AudioObjectPropertyScope scope) { AudioObjectPropertyAddress adr = {0, scope, kAudioObjectPropertyElementMaster}; UInt32 size = 0; uint32_t i, ret = 0; adr.mSelector = kAudioDevicePropertyStreamConfiguration; if (AudioObjectGetPropertyDataSize(devid, &adr, 0, NULL, &size) == noErr && size > 0) { AudioBufferList * list = static_cast(alloca(size)); if (AudioObjectGetPropertyData(devid, &adr, 0, NULL, &size, list) == noErr) { for (i = 0; i < list->mNumberBuffers; i++) ret += list->mBuffers[i].mNumberChannels; } } return ret; } static void audiounit_get_available_samplerate(AudioObjectID devid, AudioObjectPropertyScope scope, uint32_t * min, uint32_t * max, uint32_t * def) { AudioObjectPropertyAddress adr = {0, scope, kAudioObjectPropertyElementMaster}; adr.mSelector = kAudioDevicePropertyNominalSampleRate; if (AudioObjectHasProperty(devid, &adr)) { UInt32 size = sizeof(Float64); Float64 fvalue = 0.0; if (AudioObjectGetPropertyData(devid, &adr, 0, NULL, &size, &fvalue) == noErr) { *def = fvalue; } } adr.mSelector = kAudioDevicePropertyAvailableNominalSampleRates; UInt32 size = 0; AudioValueRange range; if (AudioObjectHasProperty(devid, &adr) && AudioObjectGetPropertyDataSize(devid, &adr, 0, NULL, &size) == noErr) { uint32_t count = size / sizeof(AudioValueRange); vector ranges(count); range.mMinimum = 9999999999.0; range.mMaximum = 0.0; 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; } } *max = static_cast(range.mMaximum); *min = static_cast(range.mMinimum); } else { *min = *max = 0; } } static UInt32 audiounit_get_device_presentation_latency(AudioObjectID devid, AudioObjectPropertyScope scope) { AudioObjectPropertyAddress adr = {0, scope, kAudioObjectPropertyElementMaster}; UInt32 size, dev, stream = 0; AudioStreamID sid[1]; adr.mSelector = kAudioDevicePropertyLatency; size = sizeof(UInt32); if (AudioObjectGetPropertyData(devid, &adr, 0, NULL, &size, &dev) != noErr) { dev = 0; } adr.mSelector = kAudioDevicePropertyStreams; size = sizeof(sid); if (AudioObjectGetPropertyData(devid, &adr, 0, NULL, &size, sid) == noErr) { adr.mSelector = kAudioStreamPropertyLatency; size = sizeof(UInt32); AudioObjectGetPropertyData(sid[0], &adr, 0, NULL, &size, &stream); } return dev + stream; } 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; if (type == CUBEB_DEVICE_TYPE_OUTPUT) { adr.mScope = kAudioDevicePropertyScopeOutput; } else if (type == CUBEB_DEVICE_TYPE_INPUT) { adr.mScope = kAudioDevicePropertyScopeInput; } else { return CUBEB_ERROR; } UInt32 ch = audiounit_get_channel_count(devid, adr.mScope); if (ch == 0) { return CUBEB_ERROR; } PodZero(dev_info, 1); CFStringRef device_id_str = nullptr; size = sizeof(CFStringRef); adr.mSelector = kAudioDevicePropertyDeviceUID; 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(devid); dev_info->group_id = dev_info->device_id; CFRelease(device_id_str); } 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); } // 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; 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); } 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; dev_info->max_channels = ch; dev_info->format = (cubeb_device_fmt)CUBEB_DEVICE_FMT_ALL; /* CoreAudio supports All! */ /* kAudioFormatFlagsAudioUnitCanonical is deprecated, prefer floating point */ dev_info->default_format = CUBEB_DEVICE_FMT_F32NE; audiounit_get_available_samplerate(devid, adr.mScope, &dev_info->min_rate, &dev_info->max_rate, &dev_info->default_rate); UInt32 latency = audiounit_get_device_presentation_latency(devid, adr.mScope); AudioValueRange range; adr.mSelector = kAudioDevicePropertyBufferFrameSizeRange; size = sizeof(AudioValueRange); 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 { 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 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) { vector input_devs; vector output_devs; // 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); } if (type & CUBEB_DEVICE_TYPE_INPUT) { input_devs = audiounit_get_devices_of_type(CUBEB_DEVICE_TYPE_INPUT); } auto devices = new cubeb_device_info[output_devs.size() + input_devs.size()]; collection->count = 0; 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 (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; } } if (collection->count > 0) { collection->device = devices; } else { delete[] devices; collection->device = NULL; } return CUBEB_OK; } static void audiounit_device_destroy(cubeb_device_info * device) { delete[] device->device_id; delete[] device->friendly_name; delete[] device->vendor_name; } static int audiounit_device_collection_destroy(cubeb * /* context */, cubeb_device_collection * collection) { for (size_t i = 0; i < collection->count; i++) { audiounit_device_destroy(&collection->device[i]); } delete[] collection->device; return CUBEB_OK; } static vector audiounit_get_devices_of_type(cubeb_device_type devtype) { UInt32 size = 0; OSStatus ret = AudioObjectGetPropertyDataSize( kAudioObjectSystemObject, &DEVICES_PROPERTY_ADDRESS, 0, NULL, &size); if (ret != noErr) { return vector(); } vector devices(size / sizeof(AudioObjectID)); ret = AudioObjectGetPropertyData(kAudioObjectSystemObject, &DEVICES_PROPERTY_ADDRESS, 0, NULL, &size, devices.data()); if (ret != noErr) { return vector(); } // 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. */ sort(devices.begin(), devices.end(), [](AudioObjectID a, AudioObjectID b) { return a < b; }); if (devtype == (CUBEB_DEVICE_TYPE_INPUT | CUBEB_DEVICE_TYPE_OUTPUT)) { return devices; } AudioObjectPropertyScope scope = (devtype == CUBEB_DEVICE_TYPE_INPUT) ? kAudioDevicePropertyScopeInput : kAudioDevicePropertyScopeOutput; vector 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.push_back(devices[i]); } } return devices_in_scope; } static OSStatus audiounit_collection_changed_callback( AudioObjectID /* inObjectID */, UInt32 /* inNumberAddresses */, const AudioObjectPropertyAddress * /* inAddresses */, void * inClientData) { cubeb * context = static_cast(inClientData); // 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; } if (context->input_collection_changed_callback) { vector 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 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; } static OSStatus audiounit_add_device_listener( cubeb * context, cubeb_device_type devtype, 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((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; } } 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, cubeb_device_type devtype) { context->mutex.assert_current_thread_owns(); if (devtype & CUBEB_DEVICE_TYPE_INPUT) { context->input_collection_changed_callback = nullptr; context->input_collection_changed_user_ptr = nullptr; context->input_device_array.clear(); } 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, cubeb_device_type devtype, 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, collection_changed_callback, user_ptr); } else { ret = audiounit_remove_device_listener(context, devtype); } return (ret == noErr) ? CUBEB_OK : CUBEB_ERROR; } cubeb_ops const audiounit_ops = { /*.init =*/audiounit_init, /*.get_backend_id =*/audiounit_get_backend_id, /*.get_max_channel_count =*/audiounit_get_max_channel_count, /*.get_min_latency =*/audiounit_get_min_latency, /*.get_preferred_sample_rate =*/audiounit_get_preferred_sample_rate, /*.get_supported_input_processing_params =*/NULL, /*.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_get_position =*/audiounit_stream_get_position, /*.stream_get_latency =*/audiounit_stream_get_latency, /*.stream_get_input_latency =*/NULL, /*.stream_set_volume =*/audiounit_stream_set_volume, /*.stream_set_name =*/NULL, /*.stream_get_current_device =*/audiounit_stream_get_current_device, /*.stream_set_input_mute =*/NULL, /*.stream_set_input_processing_params =*/NULL, /*.stream_device_destroy =*/audiounit_stream_device_destroy, /*.stream_register_device_changed_callback =*/ audiounit_stream_register_device_changed_callback, /*.register_device_collection_changed =*/ audiounit_register_device_collection_changed};