diff options
Diffstat (limited to 'media/libcubeb/src')
35 files changed, 8967 insertions, 3312 deletions
diff --git a/media/libcubeb/src/android/cubeb-output-latency.h b/media/libcubeb/src/android/cubeb-output-latency.h new file mode 100644 index 0000000000..a824fc1c24 --- /dev/null +++ b/media/libcubeb/src/android/cubeb-output-latency.h @@ -0,0 +1,76 @@ +#ifndef _CUBEB_OUTPUT_LATENCY_H_ +#define _CUBEB_OUTPUT_LATENCY_H_ + +#include <stdbool.h> +#include "cubeb_media_library.h" +#include "../cubeb-jni.h" + +struct output_latency_function { + media_lib * from_lib; + cubeb_jni * from_jni; + int version; +}; + +typedef struct output_latency_function output_latency_function; + +const int ANDROID_JELLY_BEAN_MR1_4_2 = 17; + +output_latency_function * +cubeb_output_latency_load_method(int version) +{ + output_latency_function * ol = NULL; + ol = calloc(1, sizeof(output_latency_function)); + + ol->version = version; + + if (ol->version > ANDROID_JELLY_BEAN_MR1_4_2){ + ol->from_jni = cubeb_jni_init(); + return ol; + } + + ol->from_lib = cubeb_load_media_library(); + return ol; +} + +bool +cubeb_output_latency_method_is_loaded(output_latency_function * ol) +{ + assert(ol && (ol->from_jni || ol->from_lib)); + if (ol->version > ANDROID_JELLY_BEAN_MR1_4_2){ + return !!ol->from_jni; + } + + return !!ol->from_lib; +} + +void +cubeb_output_latency_unload_method(output_latency_function * ol) +{ + if (!ol) { + return; + } + + if (ol->version > ANDROID_JELLY_BEAN_MR1_4_2 && ol->from_jni) { + cubeb_jni_destroy(ol->from_jni); + } + + if (ol->version <= ANDROID_JELLY_BEAN_MR1_4_2 && ol->from_lib) { + cubeb_close_media_library(ol->from_lib); + } + + free(ol); +} + +uint32_t +cubeb_get_output_latency(output_latency_function * ol) +{ + assert(cubeb_output_latency_method_is_loaded(ol)); + + if (ol->version > ANDROID_JELLY_BEAN_MR1_4_2){ + return cubeb_get_output_latency_from_jni(ol->from_jni); + } + + return cubeb_get_output_latency_from_media_library(ol->from_lib); +} + +#endif // _CUBEB_OUTPUT_LATENCY_H_ diff --git a/media/libcubeb/src/android/cubeb_media_library.h b/media/libcubeb/src/android/cubeb_media_library.h new file mode 100644 index 0000000000..ab21b779df --- /dev/null +++ b/media/libcubeb/src/android/cubeb_media_library.h @@ -0,0 +1,62 @@ +#ifndef _CUBEB_MEDIA_LIBRARY_H_ +#define _CUBEB_MEDIA_LIBRARY_H_ + +struct media_lib { + void * libmedia; + int32_t (* get_output_latency)(uint32_t * latency, int stream_type); +}; + +typedef struct media_lib media_lib; + +media_lib * +cubeb_load_media_library() +{ + media_lib ml = {0}; + ml.libmedia = dlopen("libmedia.so", RTLD_LAZY); + if (!ml.libmedia) { + return NULL; + } + + // Get the latency, in ms, from AudioFlinger. First, try the most recent signature. + // status_t AudioSystem::getOutputLatency(uint32_t* latency, audio_stream_type_t streamType) + ml.get_output_latency = + dlsym(ml.libmedia, "_ZN7android11AudioSystem16getOutputLatencyEPj19audio_stream_type_t"); + if (!ml.get_output_latency) { + // In case of failure, try the signature from legacy version. + // status_t AudioSystem::getOutputLatency(uint32_t* latency, int streamType) + ml.get_output_latency = + dlsym(ml.libmedia, "_ZN7android11AudioSystem16getOutputLatencyEPji"); + if (!ml.get_output_latency) { + return NULL; + } + } + + media_lib * rv = NULL; + rv = calloc(1, sizeof(media_lib)); + assert(rv); + *rv = ml; + return rv; +} + +void +cubeb_close_media_library(media_lib * ml) +{ + dlclose(ml->libmedia); + ml->libmedia = NULL; + ml->get_output_latency = NULL; + free(ml); +} + +uint32_t +cubeb_get_output_latency_from_media_library(media_lib * ml) +{ + uint32_t latency = 0; + const int audio_stream_type_music = 3; + int32_t r = ml->get_output_latency(&latency, audio_stream_type_music); + if (r) { + return 0; + } + return latency; +} + +#endif // _CUBEB_MEDIA_LIBRARY_H_ diff --git a/media/libcubeb/src/android/sles_definitions.h b/media/libcubeb/src/android/sles_definitions.h index 1b1ace567e..06d2e8d496 100644 --- a/media/libcubeb/src/android/sles_definitions.h +++ b/media/libcubeb/src/android/sles_definitions.h @@ -43,10 +43,9 @@ #define SL_ANDROID_RECORDING_PRESET_VOICE_RECOGNITION ((SLuint32) 0x00000003) /** uses the main microphone tuned for audio communications */ #define SL_ANDROID_RECORDING_PRESET_VOICE_COMMUNICATION ((SLuint32) 0x00000004) +/** uses the main microphone unprocessed */ +#define SL_ANDROID_RECORDING_PRESET_UNPROCESSED ((SLuint32) 0x00000005) -/** Audio recording get session ID (read only) */ -/** Audio recording get session ID key */ -#define SL_ANDROID_KEY_RECORDING_SESSION_ID ((const SLchar*) "androidRecordingSessionId") /*---------------------------------------------------------------------------*/ /* Android AudioPlayer configuration */ @@ -69,9 +68,35 @@ #define SL_ANDROID_STREAM_ALARM ((SLint32) 0x00000004) /* same as android.media.AudioManager.STREAM_NOTIFICATION */ #define SL_ANDROID_STREAM_NOTIFICATION ((SLint32) 0x00000005) -/* same as android.media.AudioManager.STREAM_BLUETOOTH_SCO */ -#define SL_ANDROID_STREAM_BLUETOOTH_SCO ((SLint32) 0x00000006) -/* same as android.media.AudioManager.STREAM_SYSTEM_ENFORCED */ -#define SL_ANDROID_STREAM_SYSTEM_ENFORCED ((SLint32) 0x00000007) + + +/*---------------------------------------------------------------------------*/ +/* Android AudioPlayer and AudioRecorder configuration */ +/*---------------------------------------------------------------------------*/ + +/** Audio Performance mode. + * Performance mode tells the framework how to configure the audio path + * for a player or recorder according to application performance and + * functional requirements. + * It affects the output or input latency based on acceptable tradeoffs on + * battery drain and use of pre or post processing effects. + * Performance mode should be set before realizing the object and should be + * read after realizing the object to check if the requested mode could be + * granted or not. + */ +/** Audio Performance mode key */ +#define SL_ANDROID_KEY_PERFORMANCE_MODE ((const SLchar*) "androidPerformanceMode") + +/** Audio performance values */ +/* No specific performance requirement. Allows HW and SW pre/post processing. */ +#define SL_ANDROID_PERFORMANCE_NONE ((SLuint32) 0x00000000) +/* Priority given to latency. No HW or software pre/post processing. + * This is the default if no performance mode is specified. */ +#define SL_ANDROID_PERFORMANCE_LATENCY ((SLuint32) 0x00000001) +/* Priority given to latency while still allowing HW pre and post processing. */ +#define SL_ANDROID_PERFORMANCE_LATENCY_EFFECTS ((SLuint32) 0x00000002) +/* Priority given to power saving if latency is not a concern. + * Allows HW and SW pre/post processing. */ +#define SL_ANDROID_PERFORMANCE_POWER_SAVING ((SLuint32) 0x00000003) #endif /* OPENSL_ES_ANDROIDCONFIGURATION_H_ */ diff --git a/media/libcubeb/src/cubeb-internal.h b/media/libcubeb/src/cubeb-internal.h index dfcc186c57..312a9ea3a1 100644 --- a/media/libcubeb/src/cubeb-internal.h +++ b/media/libcubeb/src/cubeb-internal.h @@ -9,6 +9,7 @@ #include "cubeb/cubeb.h" #include "cubeb_log.h" +#include "cubeb_assert.h" #include <stdio.h> #include <string.h> @@ -28,9 +29,6 @@ extern "C" { #endif -/* Crash the caller. */ -void cubeb_crash() CLANG_ANALYZER_NORETURN; - #if defined(__cplusplus) } #endif @@ -44,7 +42,9 @@ struct cubeb_ops { uint32_t * latency_ms); int (* get_preferred_sample_rate)(cubeb * context, uint32_t * rate); int (* enumerate_devices)(cubeb * context, cubeb_device_type type, - cubeb_device_collection ** collection); + cubeb_device_collection * collection); + int (* device_collection_destroy)(cubeb * context, + cubeb_device_collection * collection); void (* destroy)(cubeb * context); int (* stream_init)(cubeb * context, cubeb_stream ** stream, @@ -60,10 +60,10 @@ struct cubeb_ops { void (* stream_destroy)(cubeb_stream * stream); int (* stream_start)(cubeb_stream * stream); int (* stream_stop)(cubeb_stream * stream); + int (* stream_reset_default_device)(cubeb_stream * stream); int (* stream_get_position)(cubeb_stream * stream, uint64_t * position); int (* stream_get_latency)(cubeb_stream * stream, uint32_t * latency); int (* stream_set_volume)(cubeb_stream * stream, float volumes); - int (* stream_set_panning)(cubeb_stream * stream, float panning); int (* stream_get_current_device)(cubeb_stream * stream, cubeb_device ** const device); int (* stream_device_destroy)(cubeb_stream * stream, @@ -76,11 +76,4 @@ struct cubeb_ops { void * user_ptr); }; -#define XASSERT(expr) do { \ - if (!(expr)) { \ - fprintf(stderr, "%s:%d - fatal error: %s\n", __FILE__, __LINE__, #expr); \ - cubeb_crash(); \ - } \ - } while (0) - #endif /* CUBEB_INTERNAL_0eb56756_4e20_4404_a76d_42bf88cd15a5 */ diff --git a/media/libcubeb/src/cubeb-jni-instances.h b/media/libcubeb/src/cubeb-jni-instances.h new file mode 100644 index 0000000000..19c5c29da9 --- /dev/null +++ b/media/libcubeb/src/cubeb-jni-instances.h @@ -0,0 +1,34 @@ +#ifndef _CUBEB_JNI_INSTANCES_H_ +#define _CUBEB_JNI_INSTANCES_H_ + +#include "GeneratedJNIWrappers.h" +#include "mozilla/jni/Utils.h" + +/* + * The methods in this file offer a way to pass in the required + * JNI instances in the cubeb library. By default they return NULL. + * In this case part of the cubeb API that depends on JNI + * will return CUBEB_ERROR_NOT_SUPPORTED. Currently only one + * method depends on that: + * + * cubeb_stream_get_position() + * + * Users that want to use that cubeb API method must "override" + * the methods bellow to return a valid instance of JavaVM + * and application's Context object. + * */ + +JNIEnv * +cubeb_get_jni_env_for_thread() +{ + return mozilla::jni::GetEnvForThread(); +} + +jobject +cubeb_jni_get_context_instance() +{ + auto context = mozilla::java::GeckoAppShell::GetApplicationContext(); + return context.Forget(); +} + +#endif //_CUBEB_JNI_INSTANCES_H_ diff --git a/media/libcubeb/src/cubeb-jni.cpp b/media/libcubeb/src/cubeb-jni.cpp new file mode 100644 index 0000000000..a5066967af --- /dev/null +++ b/media/libcubeb/src/cubeb-jni.cpp @@ -0,0 +1,68 @@ +#include "jni.h" +#include <assert.h> +#include "cubeb-jni-instances.h" + +#define AUDIO_STREAM_TYPE_MUSIC 3 + +struct cubeb_jni { + jobject s_audio_manager_obj = nullptr; + jclass s_audio_manager_class = nullptr; + jmethodID s_get_output_latency_id = nullptr; +}; + +extern "C" +cubeb_jni * +cubeb_jni_init() +{ + jobject ctx_obj = cubeb_jni_get_context_instance(); + JNIEnv * jni_env = cubeb_get_jni_env_for_thread(); + if (!jni_env || !ctx_obj) { + return nullptr; + } + + cubeb_jni * cubeb_jni_ptr = new cubeb_jni; + assert(cubeb_jni_ptr); + + // Find the audio manager object and make it global to call it from another method + jclass context_class = jni_env->FindClass("android/content/Context"); + jfieldID audio_service_field = jni_env->GetStaticFieldID(context_class, "AUDIO_SERVICE", "Ljava/lang/String;"); + jstring jstr = (jstring)jni_env->GetStaticObjectField(context_class, audio_service_field); + jmethodID get_system_service_id = jni_env->GetMethodID(context_class, "getSystemService", "(Ljava/lang/String;)Ljava/lang/Object;"); + jobject audio_manager_obj = jni_env->CallObjectMethod(ctx_obj, get_system_service_id, jstr); + cubeb_jni_ptr->s_audio_manager_obj = reinterpret_cast<jobject>(jni_env->NewGlobalRef(audio_manager_obj)); + + // Make the audio manager class a global reference in order to preserve method id + jclass audio_manager_class = jni_env->FindClass("android/media/AudioManager"); + cubeb_jni_ptr->s_audio_manager_class = reinterpret_cast<jclass>(jni_env->NewGlobalRef(audio_manager_class)); + cubeb_jni_ptr->s_get_output_latency_id = jni_env->GetMethodID (audio_manager_class, "getOutputLatency", "(I)I"); + + jni_env->DeleteLocalRef(ctx_obj); + jni_env->DeleteLocalRef(context_class); + jni_env->DeleteLocalRef(jstr); + jni_env->DeleteLocalRef(audio_manager_obj); + jni_env->DeleteLocalRef(audio_manager_class); + + return cubeb_jni_ptr; +} + +extern "C" +int cubeb_get_output_latency_from_jni(cubeb_jni * cubeb_jni_ptr) +{ + assert(cubeb_jni_ptr); + JNIEnv * jni_env = cubeb_get_jni_env_for_thread(); + return jni_env->CallIntMethod(cubeb_jni_ptr->s_audio_manager_obj, cubeb_jni_ptr->s_get_output_latency_id, AUDIO_STREAM_TYPE_MUSIC); //param: AudioManager.STREAM_MUSIC +} + +extern "C" +void cubeb_jni_destroy(cubeb_jni * cubeb_jni_ptr) +{ + assert(cubeb_jni_ptr); + + JNIEnv * jni_env = cubeb_get_jni_env_for_thread(); + assert(jni_env); + + jni_env->DeleteGlobalRef(cubeb_jni_ptr->s_audio_manager_obj); + jni_env->DeleteGlobalRef(cubeb_jni_ptr->s_audio_manager_class); + + delete cubeb_jni_ptr; +} diff --git a/media/libcubeb/src/cubeb-jni.h b/media/libcubeb/src/cubeb-jni.h new file mode 100644 index 0000000000..8c7ddb6acf --- /dev/null +++ b/media/libcubeb/src/cubeb-jni.h @@ -0,0 +1,10 @@ +#ifndef _CUBEB_JNI_H_ +#define _CUBEB_JNI_H_ + +typedef struct cubeb_jni cubeb_jni; + +cubeb_jni * cubeb_jni_init(); +int cubeb_get_output_latency_from_jni(cubeb_jni * cubeb_jni_ptr); +void cubeb_jni_destroy(cubeb_jni * cubeb_jni_ptr); + +#endif // _CUBEB_JNI_H_ diff --git a/media/libcubeb/src/cubeb.c b/media/libcubeb/src/cubeb.c index a239319a46..3ad39dee0c 100644 --- a/media/libcubeb/src/cubeb.c +++ b/media/libcubeb/src/cubeb.c @@ -8,20 +8,23 @@ #include <assert.h> #include <stddef.h> #include <stdlib.h> +#include <string.h> #include "cubeb/cubeb.h" #include "cubeb-internal.h" #define NELEMS(x) ((int) (sizeof(x) / sizeof(x[0]))) -cubeb_log_level g_log_level; -cubeb_log_callback g_log_callback; - struct cubeb { struct cubeb_ops * ops; }; struct cubeb_stream { + /* + * Note: All implementations of cubeb_stream must keep the following + * layout. + */ struct cubeb * context; + void * user_ptr; }; #if defined(USE_PULSE) @@ -45,6 +48,9 @@ int wasapi_init(cubeb ** context, char const * context_name); #if defined(USE_SNDIO) int sndio_init(cubeb ** context, char const * context_name); #endif +#if defined(USE_SUN) +int sun_init(cubeb ** context, char const * context_name); +#endif #if defined(USE_OPENSL) int opensl_init(cubeb ** context, char const * context_name); #endif @@ -54,10 +60,6 @@ int audiotrack_init(cubeb ** context, char const * context_name); #if defined(USE_KAI) int kai_init(cubeb ** context, char const * context_name); #endif -#if defined(USE_SUN) -int sunaudio_init(cubeb ** context, char const * context_name); -#endif - static int validate_stream_params(cubeb_stream_params * input_stream_params, @@ -66,7 +68,7 @@ validate_stream_params(cubeb_stream_params * input_stream_params, XASSERT(input_stream_params || output_stream_params); if (output_stream_params) { if (output_stream_params->rate < 1000 || output_stream_params->rate > 192000 || - output_stream_params->channels < 1 || output_stream_params->channels > 8) { + output_stream_params->channels < 1 || output_stream_params->channels > UINT8_MAX) { return CUBEB_ERROR_INVALID_FORMAT; } } @@ -99,8 +101,6 @@ validate_stream_params(cubeb_stream_params * input_stream_params, return CUBEB_ERROR_INVALID_FORMAT; } - - static int validate_latency(int latency) { @@ -111,15 +111,75 @@ validate_latency(int latency) } int -cubeb_init(cubeb ** context, char const * context_name) +cubeb_init(cubeb ** context, char const * context_name, char const * backend_name) { - int (* init[])(cubeb **, char const *) = { + int (* init_oneshot)(cubeb **, char const *) = NULL; + + if (backend_name != NULL) { + if (!strcmp(backend_name, "pulse")) { +#if defined(USE_PULSE) + init_oneshot = pulse_init; +#endif + } else if (!strcmp(backend_name, "jack")) { #if defined(USE_JACK) - jack_init, + init_oneshot = jack_init; +#endif + } else if (!strcmp(backend_name, "alsa")) { +#if defined(USE_ALSA) + init_oneshot = alsa_init; +#endif + } else if (!strcmp(backend_name, "audiounit")) { +#if defined(USE_AUDIOUNIT) + init_oneshot = audiounit_init; +#endif + } else if (!strcmp(backend_name, "wasapi")) { +#if defined(USE_WASAPI) + init_oneshot = wasapi_init; +#endif + } else if (!strcmp(backend_name, "winmm")) { +#if defined(USE_WINMM) + init_oneshot = winmm_init; +#endif + } else if (!strcmp(backend_name, "sndio")) { +#if defined(USE_SNDIO) + init_oneshot = sndio_init; +#endif + } else if (!strcmp(backend_name, "sun")) { +#if defined(USE_SUN) + init_oneshot = sun_init; #endif + } else if (!strcmp(backend_name, "opensl")) { +#if defined(USE_OPENSL) + init_oneshot = opensl_init; +#endif + } else if (!strcmp(backend_name, "audiotrack")) { +#if defined(USE_AUDIOTRACK) + init_oneshot = audiotrack_init; +#endif + } else if (!strcmp(backend_name, "kai")) { +#if defined(USE_KAI) + init_oneshot = kai_init; +#endif + } else { + /* Already set */ + } + } + + int (* default_init[])(cubeb **, char const *) = { + /* + * init_oneshot must be at the top to allow user + * to override all other choices + */ + init_oneshot, #if defined(USE_PULSE) pulse_init, #endif +#if defined(USE_JACK) + jack_init, +#endif +#if defined(USE_SNDIO) + sndio_init, +#endif #if defined(USE_ALSA) alsa_init, #endif @@ -132,8 +192,8 @@ cubeb_init(cubeb ** context, char const * context_name) #if defined(USE_WINMM) winmm_init, #endif -#if defined(USE_SNDIO) - sndio_init, +#if defined(USE_SUN) + sun_init, #endif #if defined(USE_OPENSL) opensl_init, @@ -144,9 +204,6 @@ cubeb_init(cubeb ** context, char const * context_name) #if defined(USE_KAI) kai_init, #endif -#if defined(USE_SUN) - sunaudio_init, -#endif }; int i; @@ -154,10 +211,10 @@ cubeb_init(cubeb ** context, char const * context_name) return CUBEB_ERROR_INVALID_PARAMETER; } - for (i = 0; i < NELEMS(init); ++i) { - if (init[i](context, context_name) == CUBEB_OK) { - /* Assert that the minimal API is implemented. */ #define OK(fn) assert((* context)->ops->fn) + for (i = 0; i < NELEMS(default_init); ++i) { + if (default_init[i] && default_init[i](context, context_name) == CUBEB_OK) { + /* Assert that the minimal API is implemented. */ OK(get_backend_id); OK(destroy); OK(stream_init); @@ -168,7 +225,6 @@ cubeb_init(cubeb ** context, char const * context_name) return CUBEB_OK; } } - return CUBEB_ERROR; } @@ -197,9 +253,9 @@ cubeb_get_max_channel_count(cubeb * context, uint32_t * max_channels) } int -cubeb_get_min_latency(cubeb * context, cubeb_stream_params params, uint32_t * latency_ms) +cubeb_get_min_latency(cubeb * context, cubeb_stream_params * params, uint32_t * latency_ms) { - if (!context || !latency_ms) { + if (!context || !params || !latency_ms) { return CUBEB_ERROR_INVALID_PARAMETER; } @@ -207,7 +263,7 @@ cubeb_get_min_latency(cubeb * context, cubeb_stream_params params, uint32_t * la return CUBEB_ERROR_NOT_SUPPORTED; } - return context->ops->get_min_latency(context, params, latency_ms); + return context->ops->get_min_latency(context, *params, latency_ms); } int @@ -247,7 +303,7 @@ cubeb_stream_init(cubeb * context, cubeb_stream ** stream, char const * stream_n { int r; - if (!context || !stream) { + if (!context || !stream || !data_callback || !state_callback) { return CUBEB_ERROR_INVALID_PARAMETER; } @@ -256,15 +312,24 @@ cubeb_stream_init(cubeb * context, cubeb_stream ** stream, char const * stream_n return r; } - return context->ops->stream_init(context, stream, stream_name, - input_device, - input_stream_params, - output_device, - output_stream_params, - latency, - data_callback, - state_callback, - user_ptr); + r = context->ops->stream_init(context, stream, stream_name, + input_device, + input_stream_params, + output_device, + output_stream_params, + latency, + data_callback, + state_callback, + user_ptr); + + if (r == CUBEB_ERROR_INVALID_FORMAT) { + LOG("Invalid format, %p %p %d %d", + output_stream_params, input_stream_params, + output_stream_params && output_stream_params->format, + input_stream_params && input_stream_params->format); + } + + return r; } void @@ -298,6 +363,20 @@ cubeb_stream_stop(cubeb_stream * stream) } int +cubeb_stream_reset_default_device(cubeb_stream * stream) +{ + if (!stream) { + return CUBEB_ERROR_INVALID_PARAMETER; + } + + if (!stream->context->ops->stream_reset_default_device) { + return CUBEB_ERROR_NOT_SUPPORTED; + } + + return stream->context->ops->stream_reset_default_device(stream); +} + +int cubeb_stream_get_position(cubeb_stream * stream, uint64_t * position) { if (!stream || !position) { @@ -335,19 +414,6 @@ cubeb_stream_set_volume(cubeb_stream * stream, float volume) return stream->context->ops->stream_set_volume(stream, volume); } -int cubeb_stream_set_panning(cubeb_stream * stream, float panning) -{ - if (!stream || panning < -1.0 || panning > 1.0) { - return CUBEB_ERROR_INVALID_PARAMETER; - } - - if (!stream->context->ops->stream_set_panning) { - return CUBEB_ERROR_NOT_SUPPORTED; - } - - return stream->context->ops->stream_set_panning(stream, panning); -} - int cubeb_stream_get_current_device(cubeb_stream * stream, cubeb_device ** const device) { @@ -390,6 +456,15 @@ int cubeb_stream_register_device_changed_callback(cubeb_stream * stream, return stream->context->ops->stream_register_device_changed_callback(stream, device_changed_callback); } +void * cubeb_stream_user_ptr(cubeb_stream * stream) +{ + if (!stream) { + return NULL; + } + + return stream->user_ptr; +} + static void log_device(cubeb_device_info * device_info) { @@ -479,7 +554,7 @@ void log_device(cubeb_device_info * device_info) int cubeb_enumerate_devices(cubeb * context, cubeb_device_type devtype, - cubeb_device_collection ** collection) + cubeb_device_collection * collection) { int rv; if ((devtype & (CUBEB_DEVICE_TYPE_INPUT | CUBEB_DEVICE_TYPE_OUTPUT)) == 0) @@ -491,42 +566,36 @@ int cubeb_enumerate_devices(cubeb * context, rv = context->ops->enumerate_devices(context, devtype, collection); - if (g_log_callback) { - for (uint32_t i = 0; i < (*collection)->count; i++) { - log_device((*collection)->device[i]); + if (g_cubeb_log_callback) { + for (size_t i = 0; i < collection->count; i++) { + log_device(&collection->device[i]); } } return rv; } -int cubeb_device_collection_destroy(cubeb_device_collection * collection) +int cubeb_device_collection_destroy(cubeb * context, + cubeb_device_collection * collection) { - uint32_t i; + int r; - if (collection == NULL) + if (context == NULL || collection == NULL) return CUBEB_ERROR_INVALID_PARAMETER; - for (i = 0; i < collection->count; i++) - cubeb_device_info_destroy(collection->device[i]); + if (!context->ops->device_collection_destroy) + return CUBEB_ERROR_NOT_SUPPORTED; - free(collection); - return CUBEB_OK; -} + if (!collection->device) + return CUBEB_OK; -int cubeb_device_info_destroy(cubeb_device_info * info) -{ - if (info == NULL) { - return CUBEB_ERROR_INVALID_PARAMETER; + r = context->ops->device_collection_destroy(context, collection); + if (r == CUBEB_OK) { + collection->device = NULL; + collection->count = 0; } - free(info->device_id); - free(info->friendly_name); - free(info->group_id); - free(info->vendor_name); - - free(info); - return CUBEB_OK; + return r; } int cubeb_register_device_collection_changed(cubeb * context, @@ -555,20 +624,22 @@ int cubeb_set_log_callback(cubeb_log_level log_level, return CUBEB_ERROR_INVALID_PARAMETER; } - if (g_log_callback && log_callback) { + if (g_cubeb_log_callback && log_callback) { return CUBEB_ERROR_NOT_SUPPORTED; } - g_log_callback = log_callback; - g_log_level = log_level; + g_cubeb_log_callback = log_callback; + g_cubeb_log_level = log_level; - return CUBEB_OK; -} + // Logging a message here allows to initialize the asynchronous logger from a + // thread that is not the audio rendering thread, and especially to not + // initialize it the first time we find a verbose log, which is often in the + // audio rendering callback, that runs from the audio rendering thread, and + // that is high priority, and that we don't want to block. + if (log_level >= CUBEB_LOG_VERBOSE) { + ALOGV("Starting cubeb log"); + } -void -cubeb_crash() -{ - *((volatile int *) NULL) = 0; - abort(); + return CUBEB_OK; } diff --git a/media/libcubeb/src/cubeb_alsa.c b/media/libcubeb/src/cubeb_alsa.c index 72a6acfb1c..a564fbfc6c 100644 --- a/media/libcubeb/src/cubeb_alsa.c +++ b/media/libcubeb/src/cubeb_alsa.c @@ -14,10 +14,58 @@ #include <limits.h> #include <poll.h> #include <unistd.h> +#include <dlfcn.h> #include <alsa/asoundlib.h> #include "cubeb/cubeb.h" #include "cubeb-internal.h" +#ifdef DISABLE_LIBASOUND_DLOPEN +#define WRAP(x) x +#else +#define WRAP(x) cubeb_##x +#define LIBASOUND_API_VISIT(X) \ + X(snd_config) \ + X(snd_config_add) \ + X(snd_config_copy) \ + X(snd_config_delete) \ + X(snd_config_get_id) \ + X(snd_config_get_string) \ + X(snd_config_imake_integer) \ + X(snd_config_search) \ + X(snd_config_search_definition) \ + X(snd_lib_error_set_handler) \ + X(snd_pcm_avail_update) \ + X(snd_pcm_close) \ + X(snd_pcm_delay) \ + X(snd_pcm_drain) \ + X(snd_pcm_frames_to_bytes) \ + X(snd_pcm_get_params) \ + X(snd_pcm_hw_params_any) \ + X(snd_pcm_hw_params_get_channels_max) \ + X(snd_pcm_hw_params_get_rate) \ + X(snd_pcm_hw_params_set_rate_near) \ + X(snd_pcm_hw_params_sizeof) \ + X(snd_pcm_nonblock) \ + X(snd_pcm_open) \ + X(snd_pcm_open_lconf) \ + X(snd_pcm_pause) \ + X(snd_pcm_poll_descriptors) \ + X(snd_pcm_poll_descriptors_count) \ + X(snd_pcm_poll_descriptors_revents) \ + X(snd_pcm_readi) \ + X(snd_pcm_recover) \ + X(snd_pcm_set_params) \ + X(snd_pcm_start) \ + X(snd_pcm_state) \ + X(snd_pcm_writei) \ + +#define MAKE_TYPEDEF(x) static typeof(x) * cubeb_##x; +LIBASOUND_API_VISIT(MAKE_TYPEDEF); +#undef MAKE_TYPEDEF +/* snd_pcm_hw_params_alloca is actually a macro */ +#define snd_pcm_hw_params_sizeof cubeb_snd_pcm_hw_params_sizeof +#endif + #define CUBEB_STREAM_MAX 16 #define CUBEB_WATCHDOG_MS 10000 @@ -36,6 +84,7 @@ static struct cubeb_ops const alsa_ops; struct cubeb { struct cubeb_ops const * ops; + void * libasound; pthread_t thread; @@ -76,13 +125,15 @@ enum stream_state { }; struct cubeb_stream { + /* Note: Must match cubeb_stream layout in cubeb.c. */ cubeb * context; + void * user_ptr; + /**/ pthread_mutex_t mutex; snd_pcm_t * pcm; cubeb_data_callback data_callback; cubeb_state_callback state_callback; - void * user_ptr; - snd_pcm_uframes_t write_position; + snd_pcm_uframes_t stream_position; snd_pcm_uframes_t last_position; snd_pcm_uframes_t buffer_size; cubeb_stream_params params; @@ -107,6 +158,12 @@ struct cubeb_stream { being logically active and playing. */ struct timeval last_activity; float volume; + + char * buffer; + snd_pcm_uframes_t bufframes; + snd_pcm_stream_t stream_type; + + struct cubeb_stream * other_stream; }; static int @@ -235,6 +292,14 @@ set_timeout(struct timeval * timeout, unsigned int ms) } static void +stream_buffer_decrement(cubeb_stream * stm, long count) +{ + char * bufremains = stm->buffer + WRAP(snd_pcm_frames_to_bytes)(stm->pcm, count); + memmove(stm->buffer, bufremains, WRAP(snd_pcm_frames_to_bytes)(stm->pcm, stm->bufframes - count)); + stm->bufframes -= count; +} + +static void alsa_set_stream_state(cubeb_stream * stm, enum stream_state state) { cubeb * ctx; @@ -249,97 +314,173 @@ alsa_set_stream_state(cubeb_stream * stm, enum stream_state state) } static enum stream_state -alsa_refill_stream(cubeb_stream * stm) +alsa_process_stream(cubeb_stream * stm) { + unsigned short revents; snd_pcm_sframes_t avail; - long got; - void * p; int draining; draining = 0; pthread_mutex_lock(&stm->mutex); - avail = snd_pcm_avail_update(stm->pcm); - if (avail < 0) { - snd_pcm_recover(stm->pcm, avail, 1); - avail = snd_pcm_avail_update(stm->pcm); - } + /* Call _poll_descriptors_revents() even if we don't use it + to let underlying plugins clear null events. Otherwise poll() + may wake up again and again, producing unnecessary CPU usage. */ + WRAP(snd_pcm_poll_descriptors_revents)(stm->pcm, stm->fds, stm->nfds, &revents); - /* Failed to recover from an xrun, this stream must be broken. */ - if (avail < 0) { + avail = WRAP(snd_pcm_avail_update)(stm->pcm); + + /* Got null event? Bail and wait for another wakeup. */ + if (avail == 0) { pthread_mutex_unlock(&stm->mutex); - stm->state_callback(stm, stm->user_ptr, CUBEB_STATE_ERROR); - return ERROR; + return RUNNING; } - /* This should never happen. */ + /* This could happen if we were suspended with SIGSTOP/Ctrl+Z for a long time. */ if ((unsigned int) avail > stm->buffer_size) { avail = stm->buffer_size; } - /* poll(2) claims this stream is active, so there should be some space - available to write. If avail is still zero here, the stream must be in - a funky state, bail and wait for another wakeup. */ - if (avail == 0) { + /* Capture: Read available frames */ + if (stm->stream_type == SND_PCM_STREAM_CAPTURE && avail > 0) { + snd_pcm_sframes_t got; + + if (avail + stm->bufframes > stm->buffer_size) { + /* Buffer overflow. Skip and overwrite with new data. */ + stm->bufframes = 0; + // TODO: should it be marked as DRAINING? + } + + got = WRAP(snd_pcm_readi)(stm->pcm, stm->buffer+stm->bufframes, avail); + + if (got < 0) { + avail = got; // the error handler below will recover us + } else { + stm->bufframes += got; + stm->stream_position += got; + + gettimeofday(&stm->last_activity, NULL); + } + } + + /* Capture: Pass read frames to callback function */ + if (stm->stream_type == SND_PCM_STREAM_CAPTURE && stm->bufframes > 0 && + (!stm->other_stream || stm->other_stream->bufframes < stm->other_stream->buffer_size)) { + snd_pcm_sframes_t wrote = stm->bufframes; + struct cubeb_stream * mainstm = stm->other_stream ? stm->other_stream : stm; + void * other_buffer = stm->other_stream ? stm->other_stream->buffer + stm->other_stream->bufframes : NULL; + + /* Correct write size to the other stream available space */ + if (stm->other_stream && wrote > (snd_pcm_sframes_t) (stm->other_stream->buffer_size - stm->other_stream->bufframes)) { + wrote = stm->other_stream->buffer_size - stm->other_stream->bufframes; + } + pthread_mutex_unlock(&stm->mutex); - return RUNNING; + wrote = stm->data_callback(mainstm, stm->user_ptr, stm->buffer, other_buffer, wrote); + pthread_mutex_lock(&stm->mutex); + + if (wrote < 0) { + avail = wrote; // the error handler below will recover us + } else { + stream_buffer_decrement(stm, wrote); + + if (stm->other_stream) { + stm->other_stream->bufframes += wrote; + } + } } - p = calloc(1, snd_pcm_frames_to_bytes(stm->pcm, avail)); - assert(p); + /* Playback: Don't have enough data? Let's ask for more. */ + if (stm->stream_type == SND_PCM_STREAM_PLAYBACK && avail > (snd_pcm_sframes_t) stm->bufframes && + (!stm->other_stream || stm->other_stream->bufframes > 0)) { + long got = avail - stm->bufframes; + void * other_buffer = stm->other_stream ? stm->other_stream->buffer : NULL; + char * buftail = stm->buffer + WRAP(snd_pcm_frames_to_bytes)(stm->pcm, stm->bufframes); + + /* Correct read size to the other stream available frames */ + if (stm->other_stream && got > (snd_pcm_sframes_t) stm->other_stream->bufframes) { + got = stm->other_stream->bufframes; + } - pthread_mutex_unlock(&stm->mutex); - got = stm->data_callback(stm, stm->user_ptr, NULL, p, avail); - pthread_mutex_lock(&stm->mutex); - if (got < 0) { pthread_mutex_unlock(&stm->mutex); - stm->state_callback(stm, stm->user_ptr, CUBEB_STATE_ERROR); - free(p); - return ERROR; + got = stm->data_callback(stm, stm->user_ptr, other_buffer, buftail, got); + pthread_mutex_lock(&stm->mutex); + + if (got < 0) { + avail = got; // the error handler below will recover us + } else { + stm->bufframes += got; + + if (stm->other_stream) { + stream_buffer_decrement(stm->other_stream, got); + } + } } - if (got > 0) { + + /* Playback: Still don't have enough data? Add some silence. */ + if (stm->stream_type == SND_PCM_STREAM_PLAYBACK && avail > (snd_pcm_sframes_t) stm->bufframes) { + long drain_frames = avail - stm->bufframes; + double drain_time = (double) drain_frames / stm->params.rate; + + char * buftail = stm->buffer + WRAP(snd_pcm_frames_to_bytes)(stm->pcm, stm->bufframes); + memset(buftail, 0, WRAP(snd_pcm_frames_to_bytes)(stm->pcm, drain_frames)); + stm->bufframes = avail; + + /* Mark as draining, unless we're waiting for capture */ + if (!stm->other_stream || stm->other_stream->bufframes > 0) { + set_timeout(&stm->drain_timeout, drain_time * 1000); + + draining = 1; + } + } + + /* Playback: Have enough data and no errors. Let's write it out. */ + if (stm->stream_type == SND_PCM_STREAM_PLAYBACK && avail > 0) { snd_pcm_sframes_t wrote; if (stm->params.format == CUBEB_SAMPLE_FLOAT32NE) { - float * b = (float *) p; - for (uint32_t i = 0; i < got * stm->params.channels; i++) { + float * b = (float *) stm->buffer; + for (uint32_t i = 0; i < avail * stm->params.channels; i++) { b[i] *= stm->volume; } } else { - short * b = (short *) p; - for (uint32_t i = 0; i < got * stm->params.channels; i++) { + short * b = (short *) stm->buffer; + for (uint32_t i = 0; i < avail * stm->params.channels; i++) { b[i] *= stm->volume; } } - wrote = snd_pcm_writei(stm->pcm, p, got); + + wrote = WRAP(snd_pcm_writei)(stm->pcm, stm->buffer, avail); if (wrote < 0) { - snd_pcm_recover(stm->pcm, wrote, 1); - wrote = snd_pcm_writei(stm->pcm, p, got); - } - if (wrote < 0 || wrote != got) { - /* Recovery failed, somehow. */ - pthread_mutex_unlock(&stm->mutex); - stm->state_callback(stm, stm->user_ptr, CUBEB_STATE_ERROR); - return ERROR; + avail = wrote; // the error handler below will recover us + } else { + stream_buffer_decrement(stm, wrote); + + stm->stream_position += wrote; + gettimeofday(&stm->last_activity, NULL); } - stm->write_position += wrote; - gettimeofday(&stm->last_activity, NULL); } - if (got != avail) { - long buffer_fill = stm->buffer_size - (avail - got); - double buffer_time = (double) buffer_fill / stm->params.rate; - /* Fill the remaining buffer with silence to guarantee one full period - has been written. */ - snd_pcm_writei(stm->pcm, (char *) p + got, avail - got); + /* Got some error? Let's try to recover the stream. */ + if (avail < 0) { + avail = WRAP(snd_pcm_recover)(stm->pcm, avail, 0); - set_timeout(&stm->drain_timeout, buffer_time * 1000); + /* Capture pcm must be started after initial setup/recover */ + if (avail >= 0 && + stm->stream_type == SND_PCM_STREAM_CAPTURE && + WRAP(snd_pcm_state)(stm->pcm) == SND_PCM_STATE_PREPARED) { + avail = WRAP(snd_pcm_start)(stm->pcm); + } + } - draining = 1; + /* Failed to recover, this stream must be broken. */ + if (avail < 0) { + pthread_mutex_unlock(&stm->mutex); + stm->state_callback(stm, stm->user_ptr, CUBEB_STATE_ERROR); + return ERROR; } - free(p); pthread_mutex_unlock(&stm->mutex); return draining ? DRAINING : RUNNING; } @@ -395,7 +536,7 @@ alsa_run(cubeb * ctx) if (stm && stm->state == RUNNING && stm->fds && any_revents(stm->fds, stm->nfds)) { alsa_set_stream_state(stm, PROCESSING); pthread_mutex_unlock(&ctx->mutex); - state = alsa_refill_stream(stm); + state = alsa_process_stream(stm); pthread_mutex_lock(&ctx->mutex); alsa_set_stream_state(stm, state); } @@ -445,26 +586,26 @@ get_slave_pcm_node(snd_config_t * lconf, snd_config_t * root_pcm) slave_def = NULL; - r = snd_config_search(root_pcm, "slave", &slave_pcm); + r = WRAP(snd_config_search)(root_pcm, "slave", &slave_pcm); if (r < 0) { return NULL; } - r = snd_config_get_string(slave_pcm, &string); + r = WRAP(snd_config_get_string)(slave_pcm, &string); if (r >= 0) { - r = snd_config_search_definition(lconf, "pcm_slave", string, &slave_def); + r = WRAP(snd_config_search_definition)(lconf, "pcm_slave", string, &slave_def); if (r < 0) { return NULL; } } do { - r = snd_config_search(slave_def ? slave_def : slave_pcm, "pcm", &pcm); + r = WRAP(snd_config_search)(slave_def ? slave_def : slave_pcm, "pcm", &pcm); if (r < 0) { break; } - r = snd_config_get_string(slave_def ? slave_def : slave_pcm, &string); + r = WRAP(snd_config_get_string)(slave_def ? slave_def : slave_pcm, &string); if (r < 0) { break; } @@ -473,7 +614,7 @@ get_slave_pcm_node(snd_config_t * lconf, snd_config_t * root_pcm) if (r < 0 || r > (int) sizeof(node_name)) { break; } - r = snd_config_search(lconf, node_name, &pcm); + r = WRAP(snd_config_search)(lconf, node_name, &pcm); if (r < 0) { break; } @@ -482,7 +623,7 @@ get_slave_pcm_node(snd_config_t * lconf, snd_config_t * root_pcm) } while (0); if (slave_def) { - snd_config_delete(slave_def); + WRAP(snd_config_delete)(slave_def); } return NULL; @@ -505,22 +646,22 @@ init_local_config_with_workaround(char const * pcm_name) lconf = NULL; - if (snd_config == NULL) { + if (*WRAP(snd_config) == NULL) { return NULL; } - r = snd_config_copy(&lconf, snd_config); + r = WRAP(snd_config_copy)(&lconf, *WRAP(snd_config)); if (r < 0) { return NULL; } do { - r = snd_config_search_definition(lconf, "pcm", pcm_name, &pcm_node); + r = WRAP(snd_config_search_definition)(lconf, "pcm", pcm_name, &pcm_node); if (r < 0) { break; } - r = snd_config_get_id(pcm_node, &string); + r = WRAP(snd_config_get_id)(pcm_node, &string); if (r < 0) { break; } @@ -529,7 +670,7 @@ init_local_config_with_workaround(char const * pcm_name) if (r < 0 || r > (int) sizeof(node_name)) { break; } - r = snd_config_search(lconf, node_name, &pcm_node); + r = WRAP(snd_config_search)(lconf, node_name, &pcm_node); if (r < 0) { break; } @@ -540,12 +681,12 @@ init_local_config_with_workaround(char const * pcm_name) } /* Fetch the PCM node's type, and bail out if it's not the PulseAudio plugin. */ - r = snd_config_search(pcm_node, "type", &node); + r = WRAP(snd_config_search)(pcm_node, "type", &node); if (r < 0) { break; } - r = snd_config_get_string(node, &string); + r = WRAP(snd_config_get_string)(node, &string); if (r < 0) { break; } @@ -556,18 +697,18 @@ init_local_config_with_workaround(char const * pcm_name) /* Don't clobber an explicit existing handle_underrun value, set it only if it doesn't already exist. */ - r = snd_config_search(pcm_node, "handle_underrun", &node); + r = WRAP(snd_config_search)(pcm_node, "handle_underrun", &node); if (r != -ENOENT) { break; } /* Disable pcm_pulse's asynchronous underrun handling. */ - r = snd_config_imake_integer(&node, "handle_underrun", 0); + r = WRAP(snd_config_imake_integer)(&node, "handle_underrun", 0); if (r < 0) { break; } - r = snd_config_add(pcm_node, node); + r = WRAP(snd_config_add)(pcm_node, node); if (r < 0) { break; } @@ -575,21 +716,21 @@ init_local_config_with_workaround(char const * pcm_name) return lconf; } while (0); - snd_config_delete(lconf); + WRAP(snd_config_delete)(lconf); return NULL; } static int -alsa_locked_pcm_open(snd_pcm_t ** pcm, snd_pcm_stream_t stream, snd_config_t * local_config) +alsa_locked_pcm_open(snd_pcm_t ** pcm, char const * pcm_name, snd_pcm_stream_t stream, snd_config_t * local_config) { int r; pthread_mutex_lock(&cubeb_alsa_mutex); if (local_config) { - r = snd_pcm_open_lconf(pcm, CUBEB_ALSA_PCM_NAME, stream, SND_PCM_NONBLOCK, local_config); + r = WRAP(snd_pcm_open_lconf)(pcm, pcm_name, stream, SND_PCM_NONBLOCK, local_config); } else { - r = snd_pcm_open(pcm, CUBEB_ALSA_PCM_NAME, stream, SND_PCM_NONBLOCK); + r = WRAP(snd_pcm_open)(pcm, pcm_name, stream, SND_PCM_NONBLOCK); } pthread_mutex_unlock(&cubeb_alsa_mutex); @@ -602,7 +743,7 @@ alsa_locked_pcm_close(snd_pcm_t * pcm) int r; pthread_mutex_lock(&cubeb_alsa_mutex); - r = snd_pcm_close(pcm); + r = WRAP(snd_pcm_close)(pcm); pthread_mutex_unlock(&cubeb_alsa_mutex); return r; @@ -658,6 +799,7 @@ silent_error_handler(char const * file, int line, char const * function, alsa_init(cubeb ** context, char const * context_name) { (void)context_name; + void * libasound = NULL; cubeb * ctx; int r; int i; @@ -668,9 +810,30 @@ alsa_init(cubeb ** context, char const * context_name) assert(context); *context = NULL; +#ifndef DISABLE_LIBASOUND_DLOPEN + libasound = dlopen("libasound.so.2", RTLD_LAZY); + if (!libasound) { + libasound = dlopen("libasound.so", RTLD_LAZY); + if (!libasound) { + return CUBEB_ERROR; + } + } + +#define LOAD(x) { \ + cubeb_##x = dlsym(libasound, #x); \ + if (!cubeb_##x) { \ + dlclose(libasound); \ + return CUBEB_ERROR; \ + } \ + } + + LIBASOUND_API_VISIT(LOAD); +#undef LOAD +#endif + pthread_mutex_lock(&cubeb_alsa_mutex); if (!cubeb_alsa_error_handler_set) { - snd_lib_error_set_handler(silent_error_handler); + WRAP(snd_lib_error_set_handler)(silent_error_handler); cubeb_alsa_error_handler_set = 1; } pthread_mutex_unlock(&cubeb_alsa_mutex); @@ -679,6 +842,7 @@ alsa_init(cubeb ** context, char const * context_name) assert(ctx); ctx->ops = &alsa_ops; + ctx->libasound = libasound; r = pthread_mutex_init(&ctx->mutex, NULL); assert(r == 0); @@ -712,7 +876,7 @@ alsa_init(cubeb ** context, char const * context_name) /* Open a dummy PCM to force the configuration space to be evaluated so that init_local_config_with_workaround can find and modify the default node. */ - r = alsa_locked_pcm_open(&dummy, SND_PCM_STREAM_PLAYBACK, NULL); + r = alsa_locked_pcm_open(&dummy, CUBEB_ALSA_PCM_NAME, SND_PCM_STREAM_PLAYBACK, NULL); if (r >= 0) { alsa_locked_pcm_close(dummy); } @@ -722,12 +886,12 @@ alsa_init(cubeb ** context, char const * context_name) pthread_mutex_unlock(&cubeb_alsa_mutex); if (ctx->local_config) { ctx->is_pa = 1; - r = alsa_locked_pcm_open(&dummy, SND_PCM_STREAM_PLAYBACK, ctx->local_config); + r = alsa_locked_pcm_open(&dummy, CUBEB_ALSA_PCM_NAME, SND_PCM_STREAM_PLAYBACK, ctx->local_config); /* If we got a local_config, we found a PA PCM. If opening a PCM with that config fails with EINVAL, the PA PCM is too old for this workaround. */ if (r == -EINVAL) { pthread_mutex_lock(&cubeb_alsa_mutex); - snd_config_delete(ctx->local_config); + WRAP(snd_config_delete)(ctx->local_config); pthread_mutex_unlock(&cubeb_alsa_mutex); ctx->local_config = NULL; } else if (r >= 0) { @@ -769,24 +933,28 @@ alsa_destroy(cubeb * ctx) if (ctx->local_config) { pthread_mutex_lock(&cubeb_alsa_mutex); - snd_config_delete(ctx->local_config); + WRAP(snd_config_delete)(ctx->local_config); pthread_mutex_unlock(&cubeb_alsa_mutex); } + if (ctx->libasound) { + dlclose(ctx->libasound); + } + free(ctx); } static void alsa_stream_destroy(cubeb_stream * stm); static int -alsa_stream_init(cubeb * ctx, 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) +alsa_stream_init_single(cubeb * ctx, cubeb_stream ** stream, char const * stream_name, + snd_pcm_stream_t stream_type, + cubeb_devid deviceid, + cubeb_stream_params * stream_params, + unsigned int latency_frames, + cubeb_data_callback data_callback, + cubeb_state_callback state_callback, + void * user_ptr) { (void)stream_name; cubeb_stream * stm; @@ -794,23 +962,17 @@ alsa_stream_init(cubeb * ctx, cubeb_stream ** stream, char const * stream_name, snd_pcm_format_t format; snd_pcm_uframes_t period_size; int latency_us = 0; - + char const * pcm_name = deviceid ? (char const *) deviceid : CUBEB_ALSA_PCM_NAME; assert(ctx && stream); - if (input_stream_params) { - /* Capture support not yet implemented. */ - return CUBEB_ERROR_NOT_SUPPORTED; - } + *stream = NULL; - if (input_device || output_device) { - /* Device selection not yet implemented. */ - return CUBEB_ERROR_DEVICE_UNAVAILABLE; + if (stream_params->prefs & CUBEB_STREAM_PREF_LOOPBACK) { + return CUBEB_ERROR_NOT_SUPPORTED; } - *stream = NULL; - - switch (output_stream_params->format) { + switch (stream_params->format) { case CUBEB_SAMPLE_S16LE: format = SND_PCM_FORMAT_S16_LE; break; @@ -842,20 +1004,27 @@ alsa_stream_init(cubeb * ctx, cubeb_stream ** stream, char const * stream_name, stm->data_callback = data_callback; stm->state_callback = state_callback; stm->user_ptr = user_ptr; - stm->params = *output_stream_params; + stm->params = *stream_params; stm->state = INACTIVE; stm->volume = 1.0; + stm->buffer = NULL; + stm->bufframes = 0; + stm->stream_type = stream_type; + stm->other_stream = NULL; r = pthread_mutex_init(&stm->mutex, NULL); assert(r == 0); - r = alsa_locked_pcm_open(&stm->pcm, SND_PCM_STREAM_PLAYBACK, ctx->local_config); + r = pthread_cond_init(&stm->cond, NULL); + assert(r == 0); + + r = alsa_locked_pcm_open(&stm->pcm, pcm_name, stm->stream_type, ctx->local_config); if (r < 0) { alsa_stream_destroy(stm); return CUBEB_ERROR; } - r = snd_pcm_nonblock(stm->pcm, 1); + r = WRAP(snd_pcm_nonblock)(stm->pcm, 1); assert(r == 0); latency_us = latency_frames * 1e6 / stm->params.rate; @@ -868,7 +1037,7 @@ alsa_stream_init(cubeb * ctx, cubeb_stream ** stream, char const * stream_name, latency_us = latency_us < min_latency ? min_latency: latency_us; } - r = snd_pcm_set_params(stm->pcm, format, SND_PCM_ACCESS_RW_INTERLEAVED, + r = WRAP(snd_pcm_set_params)(stm->pcm, format, SND_PCM_ACCESS_RW_INTERLEAVED, stm->params.channels, stm->params.rate, 1, latency_us); if (r < 0) { @@ -876,20 +1045,22 @@ alsa_stream_init(cubeb * ctx, cubeb_stream ** stream, char const * stream_name, return CUBEB_ERROR_INVALID_FORMAT; } - r = snd_pcm_get_params(stm->pcm, &stm->buffer_size, &period_size); + r = WRAP(snd_pcm_get_params)(stm->pcm, &stm->buffer_size, &period_size); assert(r == 0); - stm->nfds = snd_pcm_poll_descriptors_count(stm->pcm); + /* Double internal buffer size to have enough space when waiting for the other side of duplex connection */ + stm->buffer_size *= 2; + stm->buffer = calloc(1, WRAP(snd_pcm_frames_to_bytes)(stm->pcm, stm->buffer_size)); + assert(stm->buffer); + + stm->nfds = WRAP(snd_pcm_poll_descriptors_count)(stm->pcm); assert(stm->nfds > 0); stm->saved_fds = calloc(stm->nfds, sizeof(struct pollfd)); assert(stm->saved_fds); - r = snd_pcm_poll_descriptors(stm->pcm, stm->saved_fds, stm->nfds); + r = WRAP(snd_pcm_poll_descriptors)(stm->pcm, stm->saved_fds, stm->nfds); assert((nfds_t) r == stm->nfds); - r = pthread_cond_init(&stm->cond, NULL); - assert(r == 0); - if (alsa_register_stream(ctx, stm) != 0) { alsa_stream_destroy(stm); return CUBEB_ERROR; @@ -900,6 +1071,45 @@ alsa_stream_init(cubeb * ctx, cubeb_stream ** stream, char const * stream_name, return CUBEB_OK; } +static int +alsa_stream_init(cubeb * ctx, 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) +{ + int result = CUBEB_OK; + cubeb_stream * instm = NULL, * outstm = NULL; + + if (result == CUBEB_OK && input_stream_params) { + result = alsa_stream_init_single(ctx, &instm, stream_name, SND_PCM_STREAM_CAPTURE, + input_device, input_stream_params, latency_frames, + data_callback, state_callback, user_ptr); + } + + if (result == CUBEB_OK && output_stream_params) { + result = alsa_stream_init_single(ctx, &outstm, stream_name, SND_PCM_STREAM_PLAYBACK, + output_device, output_stream_params, latency_frames, + data_callback, state_callback, user_ptr); + } + + if (result == CUBEB_OK && input_stream_params && output_stream_params) { + instm->other_stream = outstm; + outstm->other_stream = instm; + } + + if (result != CUBEB_OK && instm) { + alsa_stream_destroy(instm); + } + + *stream = outstm ? outstm : instm; + + return result; +} + static void alsa_stream_destroy(cubeb_stream * stm) { @@ -912,10 +1122,15 @@ alsa_stream_destroy(cubeb_stream * stm) ctx = stm->context; + if (stm->other_stream) { + stm->other_stream->other_stream = NULL; // to stop infinite recursion + alsa_stream_destroy(stm->other_stream); + } + pthread_mutex_lock(&stm->mutex); if (stm->pcm) { if (stm->state == DRAINING) { - snd_pcm_drain(stm->pcm); + WRAP(snd_pcm_drain)(stm->pcm); } alsa_locked_pcm_close(stm->pcm); stm->pcm = NULL; @@ -934,6 +1149,8 @@ alsa_stream_destroy(cubeb_stream * stm) ctx->active_streams -= 1; pthread_mutex_unlock(&ctx->mutex); + free(stm->buffer); + free(stm); } @@ -957,12 +1174,14 @@ alsa_get_max_channel_count(cubeb * ctx, uint32_t * max_channels) return CUBEB_ERROR; } - r = snd_pcm_hw_params_any(stm->pcm, hw_params); + assert(stm); + + r = WRAP(snd_pcm_hw_params_any)(stm->pcm, hw_params); if (r < 0) { return CUBEB_ERROR; } - r = snd_pcm_hw_params_get_channels_max(hw_params, max_channels); + r = WRAP(snd_pcm_hw_params_get_channels_max)(hw_params, max_channels); if (r < 0) { return CUBEB_ERROR; } @@ -983,34 +1202,34 @@ alsa_get_preferred_sample_rate(cubeb * ctx, uint32_t * rate) { /* get a pcm, disabling resampling, so we get a rate the * hardware/dmix/pulse/etc. supports. */ - r = snd_pcm_open(&pcm, CUBEB_ALSA_PCM_NAME, SND_PCM_STREAM_PLAYBACK, SND_PCM_NO_AUTO_RESAMPLE); + r = WRAP(snd_pcm_open)(&pcm, CUBEB_ALSA_PCM_NAME, SND_PCM_STREAM_PLAYBACK, SND_PCM_NO_AUTO_RESAMPLE); if (r < 0) { return CUBEB_ERROR; } - r = snd_pcm_hw_params_any(pcm, hw_params); + r = WRAP(snd_pcm_hw_params_any)(pcm, hw_params); if (r < 0) { - snd_pcm_close(pcm); + WRAP(snd_pcm_close)(pcm); return CUBEB_ERROR; } - r = snd_pcm_hw_params_get_rate(hw_params, rate, &dir); + r = WRAP(snd_pcm_hw_params_get_rate)(hw_params, rate, &dir); if (r >= 0) { /* There is a default rate: use it. */ - snd_pcm_close(pcm); + WRAP(snd_pcm_close)(pcm); return CUBEB_OK; } /* Use a common rate, alsa may adjust it based on hw/etc. capabilities. */ *rate = 44100; - r = snd_pcm_hw_params_set_rate_near(pcm, hw_params, rate, NULL); + r = WRAP(snd_pcm_hw_params_set_rate_near)(pcm, hw_params, rate, NULL); if (r < 0) { - snd_pcm_close(pcm); + WRAP(snd_pcm_close)(pcm); return CUBEB_ERROR; } - snd_pcm_close(pcm); + WRAP(snd_pcm_close)(pcm); return CUBEB_OK; } @@ -1034,8 +1253,19 @@ alsa_stream_start(cubeb_stream * stm) assert(stm); ctx = stm->context; + if (stm->stream_type == SND_PCM_STREAM_PLAYBACK && stm->other_stream) { + int r = alsa_stream_start(stm->other_stream); + if (r != CUBEB_OK) + return r; + } + pthread_mutex_lock(&stm->mutex); - snd_pcm_pause(stm->pcm, 0); + /* Capture pcm must be started after initial setup/recover */ + if (stm->stream_type == SND_PCM_STREAM_CAPTURE && + WRAP(snd_pcm_state)(stm->pcm) == SND_PCM_STATE_PREPARED) { + WRAP(snd_pcm_start)(stm->pcm); + } + WRAP(snd_pcm_pause)(stm->pcm, 0); gettimeofday(&stm->last_activity, NULL); pthread_mutex_unlock(&stm->mutex); @@ -1059,6 +1289,12 @@ alsa_stream_stop(cubeb_stream * stm) assert(stm); ctx = stm->context; + if (stm->stream_type == SND_PCM_STREAM_PLAYBACK && stm->other_stream) { + int r = alsa_stream_stop(stm->other_stream); + if (r != CUBEB_OK) + return r; + } + pthread_mutex_lock(&ctx->mutex); while (stm->state == PROCESSING) { r = pthread_cond_wait(&stm->cond, &ctx->mutex); @@ -1069,7 +1305,7 @@ alsa_stream_stop(cubeb_stream * stm) pthread_mutex_unlock(&ctx->mutex); pthread_mutex_lock(&stm->mutex); - snd_pcm_pause(stm->pcm, 1); + WRAP(snd_pcm_pause)(stm->pcm, 1); pthread_mutex_unlock(&stm->mutex); return CUBEB_OK; @@ -1085,8 +1321,8 @@ alsa_stream_get_position(cubeb_stream * stm, uint64_t * position) pthread_mutex_lock(&stm->mutex); delay = -1; - if (snd_pcm_state(stm->pcm) != SND_PCM_STATE_RUNNING || - snd_pcm_delay(stm->pcm, &delay) != 0) { + if (WRAP(snd_pcm_state)(stm->pcm) != SND_PCM_STATE_RUNNING || + WRAP(snd_pcm_delay)(stm->pcm, &delay) != 0) { *position = stm->last_position; pthread_mutex_unlock(&stm->mutex); return CUBEB_OK; @@ -1095,8 +1331,8 @@ alsa_stream_get_position(cubeb_stream * stm, uint64_t * position) assert(delay >= 0); *position = 0; - if (stm->write_position >= (snd_pcm_uframes_t) delay) { - *position = stm->write_position - delay; + if (stm->stream_position >= (snd_pcm_uframes_t) delay) { + *position = stm->stream_position - delay; } stm->last_position = *position; @@ -1111,7 +1347,7 @@ alsa_stream_get_latency(cubeb_stream * stm, uint32_t * latency) snd_pcm_sframes_t delay; /* This function returns the delay in frames until a frame written using snd_pcm_writei is sent to the DAC. The DAC delay should be < 1ms anyways. */ - if (snd_pcm_delay(stm->pcm, &delay)) { + if (WRAP(snd_pcm_delay)(stm->pcm, &delay)) { return CUBEB_ERROR; } @@ -1131,22 +1367,84 @@ alsa_stream_set_volume(cubeb_stream * stm, float volume) return CUBEB_OK; } +static int +alsa_enumerate_devices(cubeb * context, cubeb_device_type type, + cubeb_device_collection * collection) +{ + cubeb_device_info* device = NULL; + + if (!context) + return CUBEB_ERROR; + + uint32_t rate, max_channels; + int r; + + r = alsa_get_preferred_sample_rate(context, &rate); + if (r != CUBEB_OK) { + return CUBEB_ERROR; + } + + r = alsa_get_max_channel_count(context, &max_channels); + if (r != CUBEB_OK) { + return CUBEB_ERROR; + } + + char const * a_name = "default"; + device = (cubeb_device_info *) calloc(1, sizeof(cubeb_device_info)); + assert(device); + if (!device) + return CUBEB_ERROR; + + device->device_id = a_name; + device->devid = (cubeb_devid) device->device_id; + device->friendly_name = a_name; + device->group_id = a_name; + device->vendor_name = a_name; + device->type = type; + device->state = CUBEB_DEVICE_STATE_ENABLED; + device->preferred = CUBEB_DEVICE_PREF_ALL; + device->format = CUBEB_DEVICE_FMT_S16NE; + device->default_format = CUBEB_DEVICE_FMT_S16NE; + device->max_channels = max_channels; + device->min_rate = rate; + device->max_rate = rate; + device->default_rate = rate; + device->latency_lo = 0; + device->latency_hi = 0; + + collection->device = device; + collection->count = 1; + + return CUBEB_OK; +} + +static int +alsa_device_collection_destroy(cubeb * context, + cubeb_device_collection * collection) +{ + assert(collection->count == 1); + (void) context; + free(collection->device); + return CUBEB_OK; +} + static struct cubeb_ops const alsa_ops = { .init = alsa_init, .get_backend_id = alsa_get_backend_id, .get_max_channel_count = alsa_get_max_channel_count, .get_min_latency = alsa_get_min_latency, .get_preferred_sample_rate = alsa_get_preferred_sample_rate, - .enumerate_devices = NULL, + .enumerate_devices = alsa_enumerate_devices, + .device_collection_destroy = alsa_device_collection_destroy, .destroy = alsa_destroy, .stream_init = alsa_stream_init, .stream_destroy = alsa_stream_destroy, .stream_start = alsa_stream_start, .stream_stop = alsa_stream_stop, + .stream_reset_default_device = NULL, .stream_get_position = alsa_stream_get_position, .stream_get_latency = alsa_stream_get_latency, .stream_set_volume = alsa_stream_set_volume, - .stream_set_panning = NULL, .stream_get_current_device = NULL, .stream_device_destroy = NULL, .stream_register_device_changed_callback = NULL, diff --git a/media/libcubeb/src/cubeb_array_queue.h b/media/libcubeb/src/cubeb_array_queue.h new file mode 100644 index 0000000000..a8ea4cd177 --- /dev/null +++ b/media/libcubeb/src/cubeb_array_queue.h @@ -0,0 +1,97 @@ +/* + * Copyright © 2016 Mozilla Foundation + * + * This program is made available under an ISC-style license. See the + * accompanying file LICENSE for details. + */ + +#ifndef CUBEB_ARRAY_QUEUE_H +#define CUBEB_ARRAY_QUEUE_H + +#include <assert.h> +#include <pthread.h> +#include <unistd.h> + +#if defined(__cplusplus) +extern "C" { +#endif + +typedef struct +{ + void ** buf; + size_t num; + size_t writePos; + size_t readPos; + pthread_mutex_t mutex; +} array_queue; + +array_queue * array_queue_create(size_t num) +{ + assert(num != 0); + array_queue * new_queue = (array_queue*)calloc(1, sizeof(array_queue)); + new_queue->buf = (void **)calloc(1, sizeof(void *) * num); + new_queue->readPos = 0; + new_queue->writePos = 0; + new_queue->num = num; + + pthread_mutex_init(&new_queue->mutex, NULL); + + return new_queue; +} + +void array_queue_destroy(array_queue * aq) +{ + assert(aq); + + free(aq->buf); + pthread_mutex_destroy(&aq->mutex); + free(aq); +} + +int array_queue_push(array_queue * aq, void * item) +{ + assert(item); + + pthread_mutex_lock(&aq->mutex); + int ret = -1; + if(aq->buf[aq->writePos % aq->num] == NULL) + { + aq->buf[aq->writePos % aq->num] = item; + aq->writePos = (aq->writePos + 1) % aq->num; + ret = 0; + } + // else queue is full + pthread_mutex_unlock(&aq->mutex); + return ret; +} + +void* array_queue_pop(array_queue * aq) +{ + pthread_mutex_lock(&aq->mutex); + void * value = aq->buf[aq->readPos % aq->num]; + if(value) + { + aq->buf[aq->readPos % aq->num] = NULL; + aq->readPos = (aq->readPos + 1) % aq->num; + } + pthread_mutex_unlock(&aq->mutex); + return value; +} + +size_t array_queue_get_size(array_queue * aq) +{ + pthread_mutex_lock(&aq->mutex); + ssize_t r = aq->writePos - aq->readPos; + if (r < 0) { + r = aq->num + r; + assert(r >= 0); + } + pthread_mutex_unlock(&aq->mutex); + return (size_t)r; +} + +#if defined(__cplusplus) +} +#endif + +#endif //CUBE_ARRAY_QUEUE_H diff --git a/media/libcubeb/src/cubeb_assert.h b/media/libcubeb/src/cubeb_assert.h new file mode 100644 index 0000000000..00d48d8ec3 --- /dev/null +++ b/media/libcubeb/src/cubeb_assert.h @@ -0,0 +1,17 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef CUBEB_ASSERT +#define CUBEB_ASSERT + +#include <stdio.h> +#include <stdlib.h> +#include <mozilla/Assertions.h> + +/* Forward fatal asserts to MOZ_RELEASE_ASSERT when built inside Gecko. */ +#define XASSERT(expr) MOZ_RELEASE_ASSERT(expr) + +#endif diff --git a/media/libcubeb/src/cubeb_audiotrack.c b/media/libcubeb/src/cubeb_audiotrack.c index fe2603405e..22f1fe0bc8 100644 --- a/media/libcubeb/src/cubeb_audiotrack.c +++ b/media/libcubeb/src/cubeb_audiotrack.c @@ -13,7 +13,7 @@ #include <stdlib.h> #include <time.h> #include <dlfcn.h> -#include "android/log.h" +#include <android/log.h> #include "cubeb/cubeb.h" #include "cubeb-internal.h" @@ -75,12 +75,14 @@ struct cubeb { }; struct cubeb_stream { + /* Note: Must match cubeb_stream layout in cubeb.c. */ cubeb * context; + void * user_ptr; + /**/ cubeb_stream_params params; cubeb_data_callback data_callback; cubeb_state_callback state_callback; void * instance; - void * user_ptr; /* Number of frames that have been passed to the AudioTrack callback */ long unsigned written; int draining; @@ -145,9 +147,9 @@ audiotrack_get_min_frame_count(cubeb * ctx, cubeb_stream_params * params, int * status_t status; /* Recent Android have a getMinFrameCount method. */ if (!audiotrack_version_is_gingerbread(ctx)) { - status = ctx->klass.get_min_frame_count(min_frame_count, params->stream_type, params->rate); + status = ctx->klass.get_min_frame_count(min_frame_count, AUDIO_STREAM_TYPE_MUSIC, params->rate); } else { - status = ctx->klass.get_min_frame_count_gingerbread(min_frame_count, params->stream_type, params->rate); + status = ctx->klass.get_min_frame_count_gingerbread(min_frame_count, AUDIO_STREAM_TYPE_MUSIC, params->rate); } if (status != 0) { ALOG("error getting the min frame count"); @@ -325,7 +327,7 @@ audiotrack_stream_init(cubeb * ctx, cubeb_stream ** stream, char const * stream_ channels = stm->params.channels == 2 ? AUDIO_CHANNEL_OUT_STEREO_ICS : AUDIO_CHANNEL_OUT_MONO_ICS; } - ctx->klass.ctor(stm->instance, stm->params.stream_type, stm->params.rate, + ctx->klass.ctor(stm->instance, AUDIO_STREAM_TYPE_MUSIC, stm->params.rate, AUDIO_FORMAT_PCM_16_BIT, channels, min_frame_count, 0, audiotrack_refill, stm, 0, 0); @@ -422,15 +424,16 @@ static struct cubeb_ops const audiotrack_ops = { .get_min_latency = audiotrack_get_min_latency, .get_preferred_sample_rate = audiotrack_get_preferred_sample_rate, .enumerate_devices = NULL, + .device_collection_destroy = NULL, .destroy = audiotrack_destroy, .stream_init = audiotrack_stream_init, .stream_destroy = audiotrack_stream_destroy, .stream_start = audiotrack_stream_start, .stream_stop = audiotrack_stream_stop, + .stream_reset_default_device = NULL, .stream_get_position = audiotrack_stream_get_position, .stream_get_latency = audiotrack_stream_get_latency, .stream_set_volume = audiotrack_stream_set_volume, - .stream_set_panning = NULL, .stream_get_current_device = NULL, .stream_device_destroy = NULL, .stream_register_device_changed_callback = NULL, diff --git a/media/libcubeb/src/cubeb_audiounit.cpp b/media/libcubeb/src/cubeb_audiounit.cpp index 9483c2795e..e0c8fc696e 100644 --- a/media/libcubeb/src/cubeb_audiounit.cpp +++ b/media/libcubeb/src/cubeb_audiounit.cpp @@ -22,200 +22,243 @@ #include <AudioToolbox/AudioToolbox.h> #include "cubeb/cubeb.h" #include "cubeb-internal.h" -#include "cubeb_panner.h" +#include "cubeb_mixer.h" #if !TARGET_OS_IPHONE #include "cubeb_osx_run_loop.h" #endif #include "cubeb_resampler.h" #include "cubeb_ring_array.h" -#include "cubeb_utils.h" #include <algorithm> #include <atomic> +#include <vector> +#include <set> +#include <sys/time.h> +#include <string> -#if !defined(kCFCoreFoundationVersionNumber10_7) -/* From CoreFoundation CFBase.h */ -#define kCFCoreFoundationVersionNumber10_7 635.00 -#endif - -#if !TARGET_OS_IPHONE && MAC_OS_X_VERSION_MIN_REQUIRED < 1060 -#define AudioComponent Component -#define AudioComponentDescription ComponentDescription -#define AudioComponentFindNext FindNextComponent -#define AudioComponentInstanceNew OpenAComponent -#define AudioComponentInstanceDispose CloseComponent -#endif +using namespace std; #if MAC_OS_X_VERSION_MIN_REQUIRED < 101000 -typedef UInt32 AudioFormatFlags; +typedef UInt32 AudioFormatFlags; #endif -#define CUBEB_STREAM_MAX 8 - #define AU_OUT_BUS 0 #define AU_IN_BUS 1 -#define PRINT_ERROR_CODE(str, r) do { \ - LOG("System call failed: %s (rv: %d)", str, r); \ -} while(0) - const char * DISPATCH_QUEUE_LABEL = "org.mozilla.cubeb"; +const char * PRIVATE_AGGREGATE_DEVICE_NAME = "CubebAggregateDevice"; + +#ifdef ALOGV +#undef ALOGV +#endif +#define ALOGV(msg, ...) dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{LOGV(msg, ##__VA_ARGS__);}) + +#ifdef ALOG +#undef ALOG +#endif +#define ALOG(msg, ...) dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{LOG(msg, ##__VA_ARGS__);}) /* Testing empirically, some headsets report a minimal latency that is very * low, but this does not work in practice. Lie and say the minimum is 256 * frames. */ -const uint32_t SAFE_MIN_LATENCY_FRAMES = 256; +const uint32_t SAFE_MIN_LATENCY_FRAMES = 128; const uint32_t SAFE_MAX_LATENCY_FRAMES = 512; +const AudioObjectPropertyAddress DEFAULT_INPUT_DEVICE_PROPERTY_ADDRESS = { + kAudioHardwarePropertyDefaultInputDevice, + kAudioObjectPropertyScopeGlobal, + kAudioObjectPropertyElementMaster +}; + +const AudioObjectPropertyAddress DEFAULT_OUTPUT_DEVICE_PROPERTY_ADDRESS = { + kAudioHardwarePropertyDefaultOutputDevice, + kAudioObjectPropertyScopeGlobal, + kAudioObjectPropertyElementMaster +}; + +const AudioObjectPropertyAddress DEVICE_IS_ALIVE_PROPERTY_ADDRESS = { + kAudioDevicePropertyDeviceIsAlive, + kAudioObjectPropertyScopeGlobal, + kAudioObjectPropertyElementMaster +}; + +const AudioObjectPropertyAddress DEVICES_PROPERTY_ADDRESS = { + kAudioHardwarePropertyDevices, + kAudioObjectPropertyScopeGlobal, + kAudioObjectPropertyElementMaster +}; + +const AudioObjectPropertyAddress INPUT_DATA_SOURCE_PROPERTY_ADDRESS = { + kAudioDevicePropertyDataSource, + kAudioDevicePropertyScopeInput, + kAudioObjectPropertyElementMaster +}; + +const AudioObjectPropertyAddress OUTPUT_DATA_SOURCE_PROPERTY_ADDRESS = { + kAudioDevicePropertyDataSource, + kAudioDevicePropertyScopeOutput, + kAudioObjectPropertyElementMaster +}; + +typedef uint32_t device_flags_value; + +enum device_flags { + DEV_UNKNOWN = 0x00, /* Unknown */ + DEV_INPUT = 0x01, /* Record device like mic */ + DEV_OUTPUT = 0x02, /* Playback device like speakers */ + DEV_SYSTEM_DEFAULT = 0x04, /* System default device */ + DEV_SELECTED_DEFAULT = 0x08, /* User selected to use the system default device */ +}; + void audiounit_stream_stop_internal(cubeb_stream * stm); -void audiounit_stream_start_internal(cubeb_stream * stm); +static int audiounit_stream_start_internal(cubeb_stream * stm); static void audiounit_close_stream(cubeb_stream *stm); static int audiounit_setup_stream(cubeb_stream *stm); +static vector<AudioObjectID> +audiounit_get_devices_of_type(cubeb_device_type devtype); +static UInt32 audiounit_get_device_presentation_latency(AudioObjectID devid, AudioObjectPropertyScope scope); + +#if !TARGET_OS_IPHONE +static AudioObjectID audiounit_get_default_device_id(cubeb_device_type type); +static int audiounit_uninstall_device_changed_callback(cubeb_stream * stm); +static int audiounit_uninstall_system_changed_callback(cubeb_stream * stm); +static void audiounit_reinit_stream_async(cubeb_stream * stm, device_flags_value flags); +#endif extern cubeb_ops const audiounit_ops; struct cubeb { - cubeb_ops const * ops; + cubeb_ops const * ops = &audiounit_ops; owned_critical_section mutex; - std::atomic<int> active_streams; + int active_streams = 0; uint32_t global_latency_frames = 0; - int limit_streams; - cubeb_device_collection_changed_callback collection_changed_callback; - void * collection_changed_user_ptr; - /* Differentiate input from output devices. */ - cubeb_device_type collection_changed_devtype; - uint32_t devtype_device_count; - AudioObjectID * devtype_device_array; - // The queue is asynchronously deallocated once all references to it are released + cubeb_device_collection_changed_callback input_collection_changed_callback = nullptr; + void * input_collection_changed_user_ptr = nullptr; + cubeb_device_collection_changed_callback output_collection_changed_callback = nullptr; + void * output_collection_changed_user_ptr = nullptr; + // Store list of devices to detect changes + vector<AudioObjectID> input_device_array; + vector<AudioObjectID> output_device_array; + // The queue should be released when it’s no longer needed. dispatch_queue_t serial_queue = dispatch_queue_create(DISPATCH_QUEUE_LABEL, DISPATCH_QUEUE_SERIAL); + // Current used channel layout + atomic<cubeb_channel_layout> layout{ CUBEB_LAYOUT_UNDEFINED }; + uint32_t channels = 0; }; -class auto_array_wrapper +static unique_ptr<AudioChannelLayout, decltype(&free)> +make_sized_audio_channel_layout(size_t sz) { -public: - explicit auto_array_wrapper(auto_array<float> * ar) - : float_ar(ar) - , short_ar(nullptr) - {assert((float_ar && !short_ar) || (!float_ar && short_ar));} - - explicit auto_array_wrapper(auto_array<short> * ar) - : float_ar(nullptr) - , short_ar(ar) - {assert((float_ar && !short_ar) || (!float_ar && short_ar));} - - ~auto_array_wrapper() { - auto_lock l(lock); - assert((float_ar && !short_ar) || (!float_ar && short_ar)); - delete float_ar; - delete short_ar; - } - - void push(void * elements, size_t length){ - assert((float_ar && !short_ar) || (!float_ar && short_ar)); - auto_lock l(lock); - if (float_ar) - return float_ar->push(static_cast<float*>(elements), length); - return short_ar->push(static_cast<short*>(elements), length); - } - - size_t length() { - assert((float_ar && !short_ar) || (!float_ar && short_ar)); - auto_lock l(lock); - if (float_ar) - return float_ar->length(); - return short_ar->length(); - } - - void push_silence(size_t length) { - assert((float_ar && !short_ar) || (!float_ar && short_ar)); - auto_lock l(lock); - if (float_ar) - return float_ar->push_silence(length); - return short_ar->push_silence(length); - } + assert(sz >= sizeof(AudioChannelLayout)); + AudioChannelLayout * acl = reinterpret_cast<AudioChannelLayout *>(calloc(1, sz)); + assert(acl); // Assert the allocation works. + return unique_ptr<AudioChannelLayout, decltype(&free)>(acl, free); +} - bool pop(void * elements, size_t length) { - assert((float_ar && !short_ar) || (!float_ar && short_ar)); - auto_lock l(lock); - if (float_ar) - return float_ar->pop(static_cast<float*>(elements), length); - return short_ar->pop(static_cast<short*>(elements), length); - } +enum class io_side { + INPUT, + OUTPUT, +}; - void * data() { - assert((float_ar && !short_ar) || (!float_ar && short_ar)); - auto_lock l(lock); - if (float_ar) - return float_ar->data(); - return short_ar->data(); +static char const * +to_string(io_side side) +{ + switch (side) { + case io_side::INPUT: + return "input"; + case io_side::OUTPUT: + return "output"; } +} - void clear() { - assert((float_ar && !short_ar) || (!float_ar && short_ar)); - auto_lock l(lock); - if (float_ar) { - float_ar->clear(); - } else { - short_ar->clear(); - } - } +struct device_info { + AudioDeviceID id = kAudioObjectUnknown; + device_flags_value flags = DEV_UNKNOWN; +}; -private: - auto_array<float> * float_ar; - auto_array<short> * short_ar; - owned_critical_section lock; +struct property_listener { + AudioDeviceID device_id; + const AudioObjectPropertyAddress * property_address; + AudioObjectPropertyListenerProc callback; + cubeb_stream * stream; + + property_listener(AudioDeviceID id, + const AudioObjectPropertyAddress * address, + AudioObjectPropertyListenerProc proc, + cubeb_stream * stm) + : device_id(id) + , property_address(address) + , callback(proc) + , stream(stm) + {} }; struct cubeb_stream { + explicit cubeb_stream(cubeb * context); + + /* Note: Must match cubeb_stream layout in cubeb.c. */ cubeb * context; - cubeb_data_callback data_callback; - cubeb_state_callback state_callback; - cubeb_device_changed_callback device_changed_callback; + void * user_ptr = nullptr; + /**/ + + cubeb_data_callback data_callback = nullptr; + cubeb_state_callback state_callback = nullptr; + cubeb_device_changed_callback device_changed_callback = nullptr; + owned_critical_section device_changed_callback_lock; /* Stream creation parameters */ - cubeb_stream_params input_stream_params; - cubeb_stream_params output_stream_params; - cubeb_devid input_device; - bool is_default_input; - cubeb_devid output_device; - /* User pointer of data_callback */ - void * user_ptr; + cubeb_stream_params input_stream_params = { CUBEB_SAMPLE_FLOAT32NE, 0, 0, CUBEB_LAYOUT_UNDEFINED, CUBEB_STREAM_PREF_NONE }; + cubeb_stream_params output_stream_params = { CUBEB_SAMPLE_FLOAT32NE, 0, 0, CUBEB_LAYOUT_UNDEFINED, CUBEB_STREAM_PREF_NONE }; + device_info input_device; + device_info output_device; /* Format descriptions */ AudioStreamBasicDescription input_desc; AudioStreamBasicDescription output_desc; /* I/O AudioUnits */ - AudioUnit input_unit; - AudioUnit output_unit; + AudioUnit input_unit = nullptr; + AudioUnit output_unit = nullptr; /* I/O device sample rate */ - Float64 input_hw_rate; - Float64 output_hw_rate; + Float64 input_hw_rate = 0; + Float64 output_hw_rate = 0; /* Expected I/O thread interleave, * calculated from I/O hw rate. */ - int expected_output_callbacks_in_a_row; + int expected_output_callbacks_in_a_row = 0; owned_critical_section mutex; - /* Hold the input samples in every - * input callback iteration */ - auto_array_wrapper * input_linear_buffer; - /* Frames on input buffer */ - std::atomic<uint32_t> input_buffer_frames; + // Hold the input samples in every input callback iteration. + // Only accessed on input/output callback thread and during initial configure. + unique_ptr<auto_array_wrapper> input_linear_buffer; /* Frame counters */ - uint64_t frames_played; - uint64_t frames_queued; - std::atomic<int64_t> frames_read; - std::atomic<bool> shutdown; - std::atomic<bool> draining; + atomic<uint64_t> frames_played{ 0 }; + uint64_t frames_queued = 0; + // How many frames got read from the input since the stream started (includes + // padded silence) + atomic<int64_t> frames_read{ 0 }; + // How many frames got written to the output device since the stream started + atomic<int64_t> frames_written{ 0 }; + atomic<bool> shutdown{ true }; + atomic<bool> draining{ false }; + atomic<bool> reinit_pending { false }; + atomic<bool> destroy_pending{ false }; /* Latency requested by the user. */ - uint32_t latency_frames; - std::atomic<uint64_t> current_latency_frames; - uint64_t hw_latency_frames; - std::atomic<float> panning; - cubeb_resampler * resampler; - /* This is the number of output callback we got in a row. This is usually one, - * but can be two when the input and output rate are different, and more when - * a device has been plugged or unplugged, as there can be some time before - * the device is ready. */ - std::atomic<int> output_callback_in_a_row; + uint32_t latency_frames = 0; + atomic<uint32_t> current_latency_frames{ 0 }; + atomic<uint32_t> total_output_latency_frames { 0 }; + unique_ptr<cubeb_resampler, decltype(&cubeb_resampler_destroy)> resampler; /* This is true if a device change callback is currently running. */ - std::atomic<bool> switching_device; - std::atomic<bool> buffer_size_change_state{ false }; + atomic<bool> switching_device{ false }; + atomic<bool> buffer_size_change_state{ false }; + AudioDeviceID aggregate_device_id = kAudioObjectUnknown; // the aggregate device id + AudioObjectID plugin_id = kAudioObjectUnknown; // used to create aggregate device + /* Mixer interface */ + unique_ptr<cubeb_mixer, decltype(&cubeb_mixer_destroy)> mixer; + /* Buffer where remixing/resampling will occur when upmixing is required */ + /* Only accessed from callback thread */ + unique_ptr<uint8_t[]> temp_buffer; + size_t temp_buffer_size = 0; // size in bytes. + /* Listeners indicating what system events are monitored. */ + unique_ptr<property_listener> default_input_listener; + unique_ptr<property_listener> default_output_listener; + unique_ptr<property_listener> input_alive_listener; + unique_ptr<property_listener> input_source_listener; + unique_ptr<property_listener> output_source_listener; }; bool has_input(cubeb_stream * stm) @@ -228,14 +271,106 @@ bool has_output(cubeb_stream * stm) return stm->output_stream_params.rate != 0; } +cubeb_channel +channel_label_to_cubeb_channel(UInt32 label) +{ + switch (label) { + case kAudioChannelLabel_Left: + return CHANNEL_FRONT_LEFT; + case kAudioChannelLabel_Right: + return CHANNEL_FRONT_RIGHT; + case kAudioChannelLabel_Center: + return CHANNEL_FRONT_CENTER; + case kAudioChannelLabel_LFEScreen: + return CHANNEL_LOW_FREQUENCY; + case kAudioChannelLabel_LeftSurround: + return CHANNEL_BACK_LEFT; + case kAudioChannelLabel_RightSurround: + return CHANNEL_BACK_RIGHT; + case kAudioChannelLabel_LeftCenter: + return CHANNEL_FRONT_LEFT_OF_CENTER; + case kAudioChannelLabel_RightCenter: + return CHANNEL_FRONT_RIGHT_OF_CENTER; + case kAudioChannelLabel_CenterSurround: + return CHANNEL_BACK_CENTER; + case kAudioChannelLabel_LeftSurroundDirect: + return CHANNEL_SIDE_LEFT; + case kAudioChannelLabel_RightSurroundDirect: + return CHANNEL_SIDE_RIGHT; + case kAudioChannelLabel_TopCenterSurround: + return CHANNEL_TOP_CENTER; + case kAudioChannelLabel_VerticalHeightLeft: + return CHANNEL_TOP_FRONT_LEFT; + case kAudioChannelLabel_VerticalHeightCenter: + return CHANNEL_TOP_FRONT_CENTER; + case kAudioChannelLabel_VerticalHeightRight: + return CHANNEL_TOP_FRONT_RIGHT; + case kAudioChannelLabel_TopBackLeft: + return CHANNEL_TOP_BACK_LEFT; + case kAudioChannelLabel_TopBackCenter: + return CHANNEL_TOP_BACK_CENTER; + case kAudioChannelLabel_TopBackRight: + return CHANNEL_TOP_BACK_RIGHT; + default: + return CHANNEL_UNKNOWN; + } +} + +AudioChannelLabel +cubeb_channel_to_channel_label(cubeb_channel channel) +{ + switch (channel) { + case CHANNEL_FRONT_LEFT: + return kAudioChannelLabel_Left; + case CHANNEL_FRONT_RIGHT: + return kAudioChannelLabel_Right; + case CHANNEL_FRONT_CENTER: + return kAudioChannelLabel_Center; + case CHANNEL_LOW_FREQUENCY: + return kAudioChannelLabel_LFEScreen; + case CHANNEL_BACK_LEFT: + return kAudioChannelLabel_LeftSurround; + case CHANNEL_BACK_RIGHT: + return kAudioChannelLabel_RightSurround; + case CHANNEL_FRONT_LEFT_OF_CENTER: + return kAudioChannelLabel_LeftCenter; + case CHANNEL_FRONT_RIGHT_OF_CENTER: + return kAudioChannelLabel_RightCenter; + case CHANNEL_BACK_CENTER: + return kAudioChannelLabel_CenterSurround; + case CHANNEL_SIDE_LEFT: + return kAudioChannelLabel_LeftSurroundDirect; + case CHANNEL_SIDE_RIGHT: + return kAudioChannelLabel_RightSurroundDirect; + case CHANNEL_TOP_CENTER: + return kAudioChannelLabel_TopCenterSurround; + case CHANNEL_TOP_FRONT_LEFT: + return kAudioChannelLabel_VerticalHeightLeft; + case CHANNEL_TOP_FRONT_CENTER: + return kAudioChannelLabel_VerticalHeightCenter; + case CHANNEL_TOP_FRONT_RIGHT: + return kAudioChannelLabel_VerticalHeightRight; + case CHANNEL_TOP_BACK_LEFT: + return kAudioChannelLabel_TopBackLeft; + case CHANNEL_TOP_BACK_CENTER: + return kAudioChannelLabel_TopBackCenter; + case CHANNEL_TOP_BACK_RIGHT: + return kAudioChannelLabel_TopBackRight; + default: + return kAudioChannelLabel_Unknown; + } +} + #if TARGET_OS_IPHONE typedef UInt32 AudioDeviceID; typedef UInt32 AudioObjectID; #define AudioGetCurrentHostTime mach_absolute_time +#endif + uint64_t -AudioConvertHostTimeToNanos(uint64_t host_time) +ConvertHostTimeToNanos(uint64_t host_time) { static struct mach_timebase_info timebase_info; static bool initialized = false; @@ -251,27 +386,34 @@ AudioConvertHostTimeToNanos(uint64_t host_time) } return (uint64_t)answer; } -#endif -static int64_t -audiotimestamp_to_latency(AudioTimeStamp const * tstamp, cubeb_stream * stream) +static void +audiounit_increment_active_streams(cubeb * ctx) { - if (!(tstamp->mFlags & kAudioTimeStampHostTimeValid)) { - return 0; - } + ctx->mutex.assert_current_thread_owns(); + ctx->active_streams += 1; +} - uint64_t pres = AudioConvertHostTimeToNanos(tstamp->mHostTime); - uint64_t now = AudioConvertHostTimeToNanos(AudioGetCurrentHostTime()); +static void +audiounit_decrement_active_streams(cubeb * ctx) +{ + ctx->mutex.assert_current_thread_owns(); + ctx->active_streams -= 1; +} - return ((pres - now) * stream->output_desc.mSampleRate) / 1000000000LL; +static int +audiounit_active_streams(cubeb * ctx) +{ + ctx->mutex.assert_current_thread_owns(); + return ctx->active_streams; } static void -audiounit_set_global_latency(cubeb_stream * stm, uint32_t latency_frames) +audiounit_set_global_latency(cubeb * ctx, uint32_t latency_frames) { - stm->mutex.assert_current_thread_owns(); - assert(stm->context->active_streams == 1); - stm->context->global_latency_frames = latency_frames; + ctx->mutex.assert_current_thread_owns(); + assert(audiounit_active_streams(ctx) == 1); + ctx->global_latency_frames = latency_frames; } static void @@ -306,24 +448,38 @@ audiounit_render_input(cubeb_stream * stm, &input_buffer_list); if (r != noErr) { - PRINT_ERROR_CODE("AudioUnitRender", r); - return r; + LOG("AudioUnitRender rv=%d", r); + if (r != kAudioUnitErr_CannotDoInCurrentContext) { + return r; + } + if (stm->output_unit) { + // kAudioUnitErr_CannotDoInCurrentContext is returned when using a BT + // headset and the profile is changed from A2DP to HFP/HSP. The previous + // output device is no longer valid and must be reset. + audiounit_reinit_stream_async(stm, DEV_INPUT | DEV_OUTPUT); + } + // For now state that no error occurred and feed silence, stream will be + // resumed once reinit has completed. + ALOGV("(%p) input: reinit pending feeding silence instead", stm); + stm->input_linear_buffer->push_silence(input_frames * stm->input_desc.mChannelsPerFrame); + } else { + /* Copy input data in linear buffer. */ + stm->input_linear_buffer->push(input_buffer_list.mBuffers[0].mData, + input_frames * stm->input_desc.mChannelsPerFrame); } - /* Copy input data in linear buffer. */ - stm->input_linear_buffer->push(input_buffer_list.mBuffers[0].mData, - input_frames * stm->input_desc.mChannelsPerFrame); - - LOGV("(%p) input: buffers %d, size %d, channels %d, frames %d.", - stm, input_buffer_list.mNumberBuffers, - input_buffer_list.mBuffers[0].mDataByteSize, - input_buffer_list.mBuffers[0].mNumberChannels, - input_frames); - /* Advance input frame counter. */ assert(input_frames > 0); stm->frames_read += input_frames; + ALOGV("(%p) input: buffers %u, size %u, channels %u, rendered frames %d, total frames %lu.", + stm, + (unsigned int) input_buffer_list.mNumberBuffers, + (unsigned int) input_buffer_list.mBuffers[0].mDataByteSize, + (unsigned int) input_buffer_list.mBuffers[0].mNumberChannels, + (unsigned int) input_frames, + stm->input_linear_buffer->length() / stm->input_desc.mChannelsPerFrame); + return noErr; } @@ -336,26 +492,15 @@ audiounit_input_callback(void * user_ptr, AudioBufferList * /* bufs */) { cubeb_stream * stm = static_cast<cubeb_stream *>(user_ptr); - long outframes; assert(stm->input_unit != NULL); assert(AU_IN_BUS == bus); if (stm->shutdown) { - LOG("(%p) input shutdown", stm); + ALOG("(%p) input shutdown", stm); return noErr; } - // This happens when we're finally getting a new input callback after having - // switched device, we can clear the input buffer now, only keeping the data - // we just got. - if (stm->output_callback_in_a_row > stm->expected_output_callbacks_in_a_row) { - stm->input_linear_buffer->pop( - nullptr, - stm->input_linear_buffer->length() - - input_frames * stm->input_stream_params.channels); - } - OSStatus r = audiounit_render_input(stm, flags, tstamp, bus, input_frames); if (r != noErr) { return r; @@ -363,7 +508,6 @@ audiounit_input_callback(void * user_ptr, // Full Duplex. We'll call data_callback in the AudioUnit output callback. if (stm->output_unit != NULL) { - stm->output_callback_in_a_row = 0; return noErr; } @@ -371,41 +515,59 @@ audiounit_input_callback(void * user_ptr, Resampler will deliver input buffer in the correct rate. */ assert(input_frames <= stm->input_linear_buffer->length() / stm->input_desc.mChannelsPerFrame); long total_input_frames = stm->input_linear_buffer->length() / stm->input_desc.mChannelsPerFrame; - outframes = cubeb_resampler_fill(stm->resampler, - stm->input_linear_buffer->data(), - &total_input_frames, - NULL, - 0); - // Reset input buffer - stm->input_linear_buffer->clear(); - - if (outframes < 0 || outframes != input_frames) { - stm->shutdown = true; + long outframes = cubeb_resampler_fill(stm->resampler.get(), + stm->input_linear_buffer->data(), + &total_input_frames, + NULL, + 0); + if (outframes < total_input_frames) { + OSStatus r = AudioOutputUnitStop(stm->input_unit); + assert(r == 0); + stm->state_callback(stm, stm->user_ptr, CUBEB_STATE_DRAINED); return noErr; } + // Reset input buffer + stm->input_linear_buffer->clear(); + return noErr; } -static bool -is_extra_input_needed(cubeb_stream * stm) +static void +audiounit_mix_output_buffer(cubeb_stream * stm, + size_t output_frames, + void * input_buffer, + size_t input_buffer_size, + void * output_buffer, + size_t output_buffer_size) +{ + assert(input_buffer_size >= + cubeb_sample_size(stm->output_stream_params.format) * + stm->output_stream_params.channels * output_frames); + assert(output_buffer_size >= stm->output_desc.mBytesPerFrame * output_frames); + + int r = cubeb_mixer_mix(stm->mixer.get(), + output_frames, + input_buffer, + input_buffer_size, + output_buffer, + output_buffer_size); + if (r != 0) { + LOG("Remix error = %d", r); + } +} + +// Return how many input frames (sampled at input_hw_rate) are needed to provide +// output_frames (sampled at output_stream_params.rate) +static int64_t +minimum_resampling_input_frames(cubeb_stream * stm, uint32_t output_frames) { - /* If the output callback came first and this is a duplex stream, we need to - * fill in some additional silence in the resampler. - * Otherwise, if we had more than expected callbacks in a row, or we're currently - * switching, we add some silence as well to compensate for the fact that - * we're lacking some input data. */ - - /* If resampling is taking place after every output callback - * the input buffer expected to be empty. Any frame left over - * from resampling is stored inside the resampler available to - * be used in next iteration as needed. - * BUT when noop_resampler is operating we have left over - * frames since it does not store anything internally. */ - return stm->frames_read == 0 || - (stm->input_linear_buffer->length() == 0 && - (stm->output_callback_in_a_row > stm->expected_output_callbacks_in_a_row || - stm->switching_device)); + if (stm->input_hw_rate == stm->output_stream_params.rate) { + // Fast path. + return output_frames; + } + return ceil(stm->input_hw_rate * output_frames / + stm->output_stream_params.rate); } static OSStatus @@ -421,23 +583,32 @@ audiounit_output_callback(void * user_ptr, cubeb_stream * stm = static_cast<cubeb_stream *>(user_ptr); - stm->output_callback_in_a_row++; + uint64_t now = ConvertHostTimeToNanos(mach_absolute_time()); + uint64_t audio_output_time = ConvertHostTimeToNanos(tstamp->mHostTime); + uint64_t output_latency_ns = audio_output_time - now; - LOGV("(%p) output: buffers %d, size %d, channels %d, frames %d.", - stm, outBufferList->mNumberBuffers, - outBufferList->mBuffers[0].mDataByteSize, - outBufferList->mBuffers[0].mNumberChannels, output_frames); + const int ns2s = 1e9; + // The total output latency is the timestamp difference + the stream latency + + // the hardware latency. + stm->total_output_latency_frames = output_latency_ns * stm->output_hw_rate / ns2s + stm->current_latency_frames; - long outframes = 0, input_frames = 0; + ALOGV("(%p) output: buffers %u, size %u, channels %u, frames %u, total input frames %lu.", + stm, + (unsigned int) outBufferList->mNumberBuffers, + (unsigned int) outBufferList->mBuffers[0].mDataByteSize, + (unsigned int) outBufferList->mBuffers[0].mNumberChannels, + (unsigned int) output_frames, + has_input(stm) ? stm->input_linear_buffer->length() / stm->input_desc.mChannelsPerFrame : 0); + + long input_frames = 0; void * output_buffer = NULL, * input_buffer = NULL; if (stm->shutdown) { - LOG("(%p) output shutdown.", stm); + ALOG("(%p) output shutdown.", stm); audiounit_make_silent(&outBufferList->mBuffers[0]); return noErr; } - stm->current_latency_frames = audiotimestamp_to_latency(tstamp, stm); if (stm->draining) { OSStatus r = AudioOutputUnitStop(stm->output_unit); assert(r == 0); @@ -449,61 +620,98 @@ audiounit_output_callback(void * user_ptr, audiounit_make_silent(&outBufferList->mBuffers[0]); return noErr; } + /* Get output buffer. */ - output_buffer = outBufferList->mBuffers[0].mData; + if (stm->mixer) { + // If remixing needs to occur, we can't directly work in our final + // destination buffer as data may be overwritten or too small to start with. + size_t size_needed = output_frames * stm->output_stream_params.channels * + cubeb_sample_size(stm->output_stream_params.format); + if (stm->temp_buffer_size < size_needed) { + stm->temp_buffer.reset(new uint8_t[size_needed]); + stm->temp_buffer_size = size_needed; + } + output_buffer = stm->temp_buffer.get(); + } else { + output_buffer = outBufferList->mBuffers[0].mData; + } + + stm->frames_written += output_frames; + /* If Full duplex get also input buffer */ if (stm->input_unit != NULL) { - if (is_extra_input_needed(stm)) { - uint32_t min_input_frames_required = ceilf(stm->input_hw_rate / stm->output_hw_rate * - stm->input_buffer_frames); - stm->input_linear_buffer->push_silence(min_input_frames_required * stm->input_desc.mChannelsPerFrame); - LOG("(%p) %s pushed %u frames of input silence.", stm, stm->frames_read == 0 ? "Input hasn't started," : - stm->switching_device ? "Device switching," : "Drop out,", min_input_frames_required); - } - // The input buffer + /* If the output callback came first and this is a duplex stream, we need to + * fill in some additional silence in the resampler. + * Otherwise, if we had more than expected callbacks in a row, or we're + * currently switching, we add some silence as well to compensate for the + * fact that we're lacking some input data. */ + uint32_t input_frames_needed = + minimum_resampling_input_frames(stm, stm->frames_written); + long missing_frames = input_frames_needed - stm->frames_read; + if (missing_frames > 0) { + stm->input_linear_buffer->push_silence(missing_frames * stm->input_desc.mChannelsPerFrame); + stm->frames_read = input_frames_needed; + + ALOG("(%p) %s pushed %ld frames of input silence.", stm, stm->frames_read == 0 ? "Input hasn't started," : + stm->switching_device ? "Device switching," : "Drop out,", missing_frames); + } input_buffer = stm->input_linear_buffer->data(); - // Number of input frames in the buffer + // Number of input frames in the buffer. It will change to actually used frames + // inside fill input_frames = stm->input_linear_buffer->length() / stm->input_desc.mChannelsPerFrame; } /* Call user callback through resampler. */ - outframes = cubeb_resampler_fill(stm->resampler, - input_buffer, - input_buffer ? &input_frames : NULL, - output_buffer, - output_frames); + long outframes = cubeb_resampler_fill(stm->resampler.get(), + input_buffer, + input_buffer ? &input_frames : NULL, + output_buffer, + output_frames); if (input_buffer) { - stm->input_linear_buffer->pop(nullptr, input_frames * stm->input_desc.mChannelsPerFrame); + // Pop from the buffer the frames used by the the resampler. + stm->input_linear_buffer->pop(input_frames * stm->input_desc.mChannelsPerFrame); } - if (outframes < 0) { + if (outframes < 0 || outframes > output_frames) { stm->shutdown = true; + OSStatus r = AudioOutputUnitStop(stm->output_unit); + assert(r == 0); + if (stm->input_unit) { + r = AudioOutputUnitStop(stm->input_unit); + assert(r == 0); + } + stm->state_callback(stm, stm->user_ptr, CUBEB_STATE_ERROR); + audiounit_make_silent(&outBufferList->mBuffers[0]); return noErr; } - size_t outbpf = stm->output_desc.mBytesPerFrame; - stm->draining = outframes < output_frames; + stm->draining = (UInt32) outframes < output_frames; stm->frames_played = stm->frames_queued; stm->frames_queued += outframes; - AudioFormatFlags outaff = stm->output_desc.mFormatFlags; - float panning = (stm->output_desc.mChannelsPerFrame == 2) ? - stm->panning.load(std::memory_order_relaxed) : 0.0f; - /* Post process output samples. */ if (stm->draining) { /* Clear missing frames (silence) */ - memset((uint8_t*)output_buffer + outframes * outbpf, 0, (output_frames - outframes) * outbpf); + size_t channels = stm->output_stream_params.channels; + size_t missing_samples = (output_frames - outframes) * channels; + size_t size_sample = cubeb_sample_size(stm->output_stream_params.format); + /* number of bytes that have been filled with valid audio by the callback. */ + size_t audio_byte_count = outframes * channels * size_sample; + PodZero((uint8_t*)output_buffer + audio_byte_count, + missing_samples * size_sample); } - /* Pan stereo. */ - if (panning != 0.0f) { - if (outaff & kAudioFormatFlagIsFloat) { - cubeb_pan_stereo_buffer_float((float*)output_buffer, outframes, panning); - } else if (outaff & kAudioFormatFlagIsSignedInteger) { - cubeb_pan_stereo_buffer_int((short*)output_buffer, outframes, panning); - } + + /* Mixing */ + if (stm->mixer) { + audiounit_mix_output_buffer(stm, + output_frames, + output_buffer, + stm->temp_buffer_size, + outBufferList->mBuffers[0].mData, + outBufferList->mBuffers[0].mDataByteSize); } + return noErr; } @@ -511,25 +719,11 @@ extern "C" { int audiounit_init(cubeb ** context, char const * /* context_name */) { - cubeb * ctx; - - *context = NULL; - - ctx = (cubeb *)calloc(1, sizeof(cubeb)); - assert(ctx); - // Placement new to call the ctors of cubeb members. - new (ctx) cubeb(); - - ctx->ops = &audiounit_ops; - - ctx->active_streams = 0; - - ctx->limit_streams = kCFCoreFoundationVersionNumber < kCFCoreFoundationVersionNumber10_7; #if !TARGET_OS_IPHONE cubeb_set_coreaudio_notification_runloop(); #endif - *context = ctx; + *context = new cubeb; return CUBEB_OK; } @@ -542,146 +736,233 @@ audiounit_get_backend_id(cubeb * /* ctx */) } #if !TARGET_OS_IPHONE + +static int audiounit_stream_get_volume(cubeb_stream * stm, float * volume); +static int audiounit_stream_set_volume(cubeb_stream * stm, float volume); + static int -audiounit_get_output_device_id(AudioDeviceID * device_id) +audiounit_set_device_info(cubeb_stream * stm, AudioDeviceID id, io_side side) { - UInt32 size; - OSStatus r; - AudioObjectPropertyAddress output_device_address = { - kAudioHardwarePropertyDefaultOutputDevice, - kAudioObjectPropertyScopeGlobal, - kAudioObjectPropertyElementMaster - }; + assert(stm); - size = sizeof(*device_id); + device_info * info = nullptr; + cubeb_device_type type = CUBEB_DEVICE_TYPE_UNKNOWN; - r = AudioObjectGetPropertyData(kAudioObjectSystemObject, - &output_device_address, - 0, - NULL, - &size, - device_id); - if (r != noErr) { - PRINT_ERROR_CODE("output_device_id", r); - return CUBEB_ERROR; + if (side == io_side::INPUT) { + info = &stm->input_device; + type = CUBEB_DEVICE_TYPE_INPUT; + } else if (side == io_side::OUTPUT) { + info = &stm->output_device; + type = CUBEB_DEVICE_TYPE_OUTPUT; } + memset(info, 0, sizeof(device_info)); + info->id = id; - return CUBEB_OK; -} - -static int -audiounit_get_input_device_id(AudioDeviceID * device_id) -{ - UInt32 size; - OSStatus r; - AudioObjectPropertyAddress input_device_address = { - kAudioHardwarePropertyDefaultInputDevice, - kAudioObjectPropertyScopeGlobal, - kAudioObjectPropertyElementMaster - }; - - size = sizeof(*device_id); + if (side == io_side::INPUT) { + info->flags |= DEV_INPUT; + } else if (side == io_side::OUTPUT) { + info->flags |= DEV_OUTPUT; + } - r = AudioObjectGetPropertyData(kAudioObjectSystemObject, - &input_device_address, - 0, - NULL, - &size, - device_id); - if (r != noErr) { + AudioDeviceID default_device_id = audiounit_get_default_device_id(type); + if (default_device_id == kAudioObjectUnknown) { return CUBEB_ERROR; } + if (id == kAudioObjectUnknown) { + info->id = default_device_id; + info->flags |= DEV_SELECTED_DEFAULT; + } + + if (info->id == default_device_id) { + info->flags |= DEV_SYSTEM_DEFAULT; + } + + assert(info->id); + assert(info->flags & DEV_INPUT && !(info->flags & DEV_OUTPUT) || + !(info->flags & DEV_INPUT) && info->flags & DEV_OUTPUT); return CUBEB_OK; } -static int audiounit_stream_get_volume(cubeb_stream * stm, float * volume); -static int audiounit_stream_set_volume(cubeb_stream * stm, float volume); -static int audiounit_uninstall_device_changed_callback(cubeb_stream * stm); static int -audiounit_reinit_stream(cubeb_stream * stm) +audiounit_reinit_stream(cubeb_stream * stm, device_flags_value flags) { auto_lock context_lock(stm->context->mutex); + assert((flags & DEV_INPUT && stm->input_unit) || + (flags & DEV_OUTPUT && stm->output_unit)); if (!stm->shutdown) { audiounit_stream_stop_internal(stm); } int r = audiounit_uninstall_device_changed_callback(stm); if (r != CUBEB_OK) { - LOG("(%p) Could not uninstall the device changed callback", stm); + LOG("(%p) Could not uninstall all device change listeners.", stm); } { auto_lock lock(stm->mutex); float volume = 0.0; - int vol_rv = audiounit_stream_get_volume(stm, &volume); + int vol_rv = CUBEB_ERROR; + if (stm->output_unit) { + vol_rv = audiounit_stream_get_volume(stm, &volume); + } audiounit_close_stream(stm); + /* Reinit occurs in one of the following case: + * - When the device is not alive any more + * - When the default system device change. + * - The bluetooth device changed from A2DP to/from HFP/HSP profile + * We first attempt to re-use the same device id, should that fail we will + * default to the (potentially new) default device. */ + AudioDeviceID input_device = flags & DEV_INPUT ? stm->input_device.id : kAudioObjectUnknown; + if (flags & DEV_INPUT) { + r = audiounit_set_device_info(stm, input_device, io_side::INPUT); + if (r != CUBEB_OK) { + LOG("(%p) Set input device info failed. This can happen when last media device is unplugged", stm); + return CUBEB_ERROR; + } + } + + /* Always use the default output on reinit. This is not correct in every + * case but it is sufficient for Firefox and prevent reinit from reporting + * failures. It will change soon when reinit mechanism will be updated. */ + r = audiounit_set_device_info(stm, kAudioObjectUnknown, io_side::OUTPUT); + if (r != CUBEB_OK) { + LOG("(%p) Set output device info failed. This can happen when last media device is unplugged", stm); + return CUBEB_ERROR; + } + if (audiounit_setup_stream(stm) != CUBEB_OK) { LOG("(%p) Stream reinit failed.", stm); - return CUBEB_ERROR; + if (flags & DEV_INPUT && input_device != kAudioObjectUnknown) { + // Attempt to re-use the same device-id failed, so attempt again with + // default input device. + audiounit_close_stream(stm); + if (audiounit_set_device_info(stm, kAudioObjectUnknown, io_side::INPUT) != CUBEB_OK || + audiounit_setup_stream(stm) != CUBEB_OK) { + LOG("(%p) Second stream reinit failed.", stm); + return CUBEB_ERROR; + } + } } if (vol_rv == CUBEB_OK) { audiounit_stream_set_volume(stm, volume); } - // Reset input frames to force new stream pre-buffer - // silence if needed, check `is_extra_input_needed()` - stm->frames_read = 0; - // If the stream was running, start it again. if (!stm->shutdown) { - audiounit_stream_start_internal(stm); + r = audiounit_stream_start_internal(stm); + if (r != CUBEB_OK) { + return CUBEB_ERROR; + } } } return CUBEB_OK; } +static void +audiounit_reinit_stream_async(cubeb_stream * stm, device_flags_value flags) +{ + if (std::atomic_exchange(&stm->reinit_pending, true)) { + // A reinit task is already pending, nothing more to do. + ALOG("(%p) re-init stream task already pending, cancelling request", stm); + return; + } + + // Use a new thread, through the queue, to avoid deadlock when calling + // Get/SetProperties method from inside notify callback + dispatch_async(stm->context->serial_queue, ^() { + if (stm->destroy_pending) { + ALOG("(%p) stream pending destroy, cancelling reinit task", stm); + return; + } + + if (audiounit_reinit_stream(stm, flags) != CUBEB_OK) { + if (audiounit_uninstall_system_changed_callback(stm) != CUBEB_OK) { + LOG("(%p) Could not uninstall system changed callback", stm); + } + stm->state_callback(stm, stm->user_ptr, CUBEB_STATE_ERROR); + LOG("(%p) Could not reopen the stream after switching.", stm); + } + stm->switching_device = false; + stm->reinit_pending = false; + }); +} + +static char const * +event_addr_to_string(AudioObjectPropertySelector selector) +{ + switch(selector) { + case kAudioHardwarePropertyDefaultOutputDevice: + return "kAudioHardwarePropertyDefaultOutputDevice"; + case kAudioHardwarePropertyDefaultInputDevice: + return "kAudioHardwarePropertyDefaultInputDevice"; + case kAudioDevicePropertyDeviceIsAlive: + return "kAudioDevicePropertyDeviceIsAlive"; + case kAudioDevicePropertyDataSource: + return "kAudioDevicePropertyDataSource"; + default: + return "Unknown"; + } +} + static OSStatus -audiounit_property_listener_callback(AudioObjectID /* id */, UInt32 address_count, +audiounit_property_listener_callback(AudioObjectID id, UInt32 address_count, const AudioObjectPropertyAddress * addresses, void * user) { cubeb_stream * stm = (cubeb_stream*) user; + if (stm->switching_device) { + LOG("Switching is already taking place. Skip Event %s for id=%d", event_addr_to_string(addresses[0].mSelector), id); + return noErr; + } stm->switching_device = true; - LOG("(%p) Audio device changed, %d events.", stm, address_count); + LOG("(%p) Audio device changed, %u events.", stm, (unsigned int) address_count); for (UInt32 i = 0; i < address_count; i++) { switch(addresses[i].mSelector) { case kAudioHardwarePropertyDefaultOutputDevice: { - LOG("Event[%d] - mSelector == kAudioHardwarePropertyDefaultOutputDevice", i); - // Allow restart to choose the new default - stm->output_device = nullptr; + LOG("Event[%u] - mSelector == kAudioHardwarePropertyDefaultOutputDevice for id=%d", (unsigned int) i, id); } break; case kAudioHardwarePropertyDefaultInputDevice: { - LOG("Event[%d] - mSelector == kAudioHardwarePropertyDefaultInputDevice", i); - // Allow restart to choose the new default - stm->input_device = nullptr; + LOG("Event[%u] - mSelector == kAudioHardwarePropertyDefaultInputDevice for id=%d", (unsigned int) i, id); } break; case kAudioDevicePropertyDeviceIsAlive: { - LOG("Event[%d] - mSelector == kAudioDevicePropertyDeviceIsAlive", i); + LOG("Event[%u] - mSelector == kAudioDevicePropertyDeviceIsAlive for id=%d", (unsigned int) i, id); // If this is the default input device ignore the event, // kAudioHardwarePropertyDefaultInputDevice will take care of the switch - if (stm->is_default_input) { + if (stm->input_device.flags & DEV_SYSTEM_DEFAULT) { LOG("It's the default input device, ignore the event"); + stm->switching_device = false; return noErr; } - // Allow restart to choose the new default. Event register only for input. - stm->input_device = nullptr; } break; case kAudioDevicePropertyDataSource: { - LOG("Event[%d] - mSelector == kAudioHardwarePropertyDataSource", i); - return noErr; + LOG("Event[%u] - mSelector == kAudioDevicePropertyDataSource for id=%d", (unsigned int) i, id); } + break; + default: + LOG("Event[%u] - mSelector == Unexpected Event id %d, return", (unsigned int) i, addresses[i].mSelector); + stm->switching_device = false; + return noErr; } } + // Allow restart to choose the new default + device_flags_value switch_side = DEV_UNKNOWN; + if (has_input(stm)) { + switch_side |= DEV_INPUT; + } + if (has_output(stm)) { + switch_side |= DEV_OUTPUT; + } + for (UInt32 i = 0; i < address_count; i++) { switch(addresses[i].mSelector) { case kAudioHardwarePropertyDefaultOutputDevice: @@ -689,7 +970,7 @@ audiounit_property_listener_callback(AudioObjectID /* id */, UInt32 address_coun case kAudioDevicePropertyDeviceIsAlive: /* fall through */ case kAudioDevicePropertyDataSource: { - auto_lock lock(stm->mutex); + auto_lock dev_cb_lock(stm->device_changed_callback_lock); if (stm->device_changed_callback) { stm->device_changed_callback(stm->user_ptr); } @@ -698,99 +979,77 @@ audiounit_property_listener_callback(AudioObjectID /* id */, UInt32 address_coun } } - // Use a new thread, through the queue, to avoid deadlock when calling - // Get/SetProperties method from inside notify callback - dispatch_async(stm->context->serial_queue, ^() { - if (audiounit_reinit_stream(stm) != CUBEB_OK) { - stm->state_callback(stm, stm->user_ptr, CUBEB_STATE_STOPPED); - LOG("(%p) Could not reopen the stream after switching.", stm); - } - stm->switching_device = false; - }); + audiounit_reinit_stream_async(stm, switch_side); return noErr; } OSStatus -audiounit_add_listener(cubeb_stream * stm, AudioDeviceID id, AudioObjectPropertySelector selector, - AudioObjectPropertyScope scope, AudioObjectPropertyListenerProc listener) +audiounit_add_listener(const property_listener * listener) { - AudioObjectPropertyAddress address = { - selector, - scope, - kAudioObjectPropertyElementMaster - }; - - return AudioObjectAddPropertyListener(id, &address, listener, stm); + assert(listener); + return AudioObjectAddPropertyListener(listener->device_id, + listener->property_address, + listener->callback, + listener->stream); } OSStatus -audiounit_remove_listener(cubeb_stream * stm, AudioDeviceID id, - AudioObjectPropertySelector selector, - AudioObjectPropertyScope scope, - AudioObjectPropertyListenerProc listener) -{ - AudioObjectPropertyAddress address = { - selector, - scope, - kAudioObjectPropertyElementMaster - }; - - return AudioObjectRemovePropertyListener(id, &address, listener, stm); +audiounit_remove_listener(const property_listener * listener) +{ + assert(listener); + return AudioObjectRemovePropertyListener(listener->device_id, + listener->property_address, + listener->callback, + listener->stream); } -static AudioObjectID audiounit_get_default_device_id(cubeb_device_type type); - static int audiounit_install_device_changed_callback(cubeb_stream * stm) { - OSStatus r; + OSStatus rv; + int r = CUBEB_OK; if (stm->output_unit) { /* This event will notify us when the data source on the same device changes, * for example when the user plugs in a normal (non-usb) headset in the * headphone jack. */ - AudioDeviceID output_dev_id; - r = audiounit_get_output_device_id(&output_dev_id); - if (r != noErr) { - return CUBEB_ERROR; - } - - r = audiounit_add_listener(stm, output_dev_id, kAudioDevicePropertyDataSource, - kAudioDevicePropertyScopeOutput, &audiounit_property_listener_callback); - if (r != noErr) { - PRINT_ERROR_CODE("AudioObjectAddPropertyListener/output/kAudioDevicePropertyDataSource", r); - return CUBEB_ERROR; + stm->output_source_listener.reset(new property_listener( + stm->output_device.id, &OUTPUT_DATA_SOURCE_PROPERTY_ADDRESS, + &audiounit_property_listener_callback, stm)); + rv = audiounit_add_listener(stm->output_source_listener.get()); + if (rv != noErr) { + stm->output_source_listener.reset(); + LOG("AudioObjectAddPropertyListener/output/kAudioDevicePropertyDataSource rv=%d, device id=%d", rv, stm->output_device.id); + r = CUBEB_ERROR; } } if (stm->input_unit) { /* This event will notify us when the data source on the input device changes. */ - AudioDeviceID input_dev_id; - r = audiounit_get_input_device_id(&input_dev_id); - if (r != noErr) { - return CUBEB_ERROR; - } - - r = audiounit_add_listener(stm, input_dev_id, kAudioDevicePropertyDataSource, - kAudioDevicePropertyScopeInput, &audiounit_property_listener_callback); - if (r != noErr) { - PRINT_ERROR_CODE("AudioObjectAddPropertyListener/input/kAudioDevicePropertyDataSource", r); - return CUBEB_ERROR; + stm->input_source_listener.reset(new property_listener( + stm->input_device.id, &INPUT_DATA_SOURCE_PROPERTY_ADDRESS, + &audiounit_property_listener_callback, stm)); + rv = audiounit_add_listener(stm->input_source_listener.get()); + if (rv != noErr) { + stm->input_source_listener.reset(); + LOG("AudioObjectAddPropertyListener/input/kAudioDevicePropertyDataSource rv=%d, device id=%d", rv, stm->input_device.id); + r = CUBEB_ERROR; } /* Event to notify when the input is going away. */ - AudioDeviceID dev = stm->input_device ? reinterpret_cast<intptr_t>(stm->input_device) : - audiounit_get_default_device_id(CUBEB_DEVICE_TYPE_INPUT); - r = audiounit_add_listener(stm, dev, kAudioDevicePropertyDeviceIsAlive, - kAudioObjectPropertyScopeGlobal, &audiounit_property_listener_callback); - if (r != noErr) { - PRINT_ERROR_CODE("AudioObjectAddPropertyListener/input/kAudioDevicePropertyDeviceIsAlive", r); - return CUBEB_ERROR; + stm->input_alive_listener.reset(new property_listener( + stm->input_device.id, &DEVICE_IS_ALIVE_PROPERTY_ADDRESS, + &audiounit_property_listener_callback, stm)); + rv = audiounit_add_listener(stm->input_alive_listener.get()); + if (rv != noErr) { + stm->input_alive_listener.reset(); + LOG("AudioObjectAddPropertyListener/input/kAudioDevicePropertyDeviceIsAlive rv=%d, device id =%d", rv, stm->input_device.id); + r = CUBEB_ERROR; } } - return CUBEB_OK; + return r; } static int @@ -803,9 +1062,12 @@ audiounit_install_system_changed_callback(cubeb_stream * stm) * for example when the user plugs in a USB headset and the system chooses it * automatically as the default, or when another device is chosen in the * dropdown list. */ - r = audiounit_add_listener(stm, kAudioObjectSystemObject, kAudioHardwarePropertyDefaultOutputDevice, - kAudioObjectPropertyScopeGlobal, &audiounit_property_listener_callback); + stm->default_output_listener.reset(new property_listener( + kAudioObjectSystemObject, &DEFAULT_OUTPUT_DEVICE_PROPERTY_ADDRESS, + &audiounit_property_listener_callback, stm)); + r = audiounit_add_listener(stm->default_output_listener.get()); if (r != noErr) { + stm->default_output_listener.reset(); LOG("AudioObjectAddPropertyListener/output/kAudioHardwarePropertyDefaultOutputDevice rv=%d", r); return CUBEB_ERROR; } @@ -813,9 +1075,12 @@ audiounit_install_system_changed_callback(cubeb_stream * stm) if (stm->input_unit) { /* This event will notify us when the default input device changes. */ - r = audiounit_add_listener(stm, kAudioObjectSystemObject, kAudioHardwarePropertyDefaultInputDevice, - kAudioObjectPropertyScopeGlobal, &audiounit_property_listener_callback); + stm->default_input_listener.reset(new property_listener( + kAudioObjectSystemObject, &DEFAULT_INPUT_DEVICE_PROPERTY_ADDRESS, + &audiounit_property_listener_callback, stm)); + r = audiounit_add_listener(stm->default_input_listener.get()); if (r != noErr) { + stm->default_input_listener.reset(); LOG("AudioObjectAddPropertyListener/input/kAudioHardwarePropertyDefaultInputDevice rv=%d", r); return CUBEB_ERROR; } @@ -827,36 +1092,38 @@ audiounit_install_system_changed_callback(cubeb_stream * stm) static int audiounit_uninstall_device_changed_callback(cubeb_stream * stm) { - OSStatus r; - - if (stm->output_unit) { - AudioDeviceID output_dev_id; - r = audiounit_get_output_device_id(&output_dev_id); - if (r != noErr) { - return CUBEB_ERROR; - } + OSStatus rv; + // Failing to uninstall listeners is not a fatal error. + int r = CUBEB_OK; - r = audiounit_remove_listener(stm, output_dev_id, kAudioDevicePropertyDataSource, - kAudioDevicePropertyScopeOutput, &audiounit_property_listener_callback); - if (r != noErr) { - return CUBEB_ERROR; + if (stm->output_source_listener) { + rv = audiounit_remove_listener(stm->output_source_listener.get()); + if (rv != noErr) { + LOG("AudioObjectRemovePropertyListener/output/kAudioDevicePropertyDataSource rv=%d, device id=%d", rv, stm->output_device.id); + r = CUBEB_ERROR; } + stm->output_source_listener.reset(); } - if (stm->input_unit) { - AudioDeviceID input_dev_id; - r = audiounit_get_input_device_id(&input_dev_id); - if (r != noErr) { - return CUBEB_ERROR; + if (stm->input_source_listener) { + rv = audiounit_remove_listener(stm->input_source_listener.get()); + if (rv != noErr) { + LOG("AudioObjectRemovePropertyListener/input/kAudioDevicePropertyDataSource rv=%d, device id=%d", rv, stm->input_device.id); + r = CUBEB_ERROR; } + stm->input_source_listener.reset(); + } - r = audiounit_remove_listener(stm, input_dev_id, kAudioDevicePropertyDataSource, - kAudioDevicePropertyScopeInput, &audiounit_property_listener_callback); - if (r != noErr) { - return CUBEB_ERROR; + if (stm->input_alive_listener) { + rv = audiounit_remove_listener(stm->input_alive_listener.get()); + if (rv != noErr) { + LOG("AudioObjectRemovePropertyListener/input/kAudioDevicePropertyDeviceIsAlive rv=%d, device id=%d", rv, stm->input_device.id); + r = CUBEB_ERROR; } + stm->input_alive_listener.reset(); } - return CUBEB_OK; + + return r; } static int @@ -864,20 +1131,20 @@ audiounit_uninstall_system_changed_callback(cubeb_stream * stm) { OSStatus r; - if (stm->output_unit) { - r = audiounit_remove_listener(stm, kAudioObjectSystemObject, kAudioHardwarePropertyDefaultOutputDevice, - kAudioObjectPropertyScopeGlobal, &audiounit_property_listener_callback); + if (stm->default_output_listener) { + r = audiounit_remove_listener(stm->default_output_listener.get()); if (r != noErr) { return CUBEB_ERROR; } + stm->default_output_listener.reset(); } - if (stm->input_unit) { - r = audiounit_remove_listener(stm, kAudioObjectSystemObject, kAudioHardwarePropertyDefaultInputDevice, - kAudioObjectPropertyScopeGlobal, &audiounit_property_listener_callback); + if (stm->default_input_listener) { + r = audiounit_remove_listener(stm->default_input_listener.get()); if (r != noErr) { return CUBEB_ERROR; } + stm->default_input_listener.reset(); } return CUBEB_OK; } @@ -895,7 +1162,8 @@ audiounit_get_acceptable_latency_range(AudioValueRange * latency_range) kAudioObjectPropertyElementMaster }; - if (audiounit_get_output_device_id(&output_device_id) != CUBEB_OK) { + output_device_id = audiounit_get_default_device_id(CUBEB_DEVICE_TYPE_OUTPUT); + if (output_device_id == kAudioObjectUnknown) { LOG("Could not get default output device id."); return CUBEB_ERROR; } @@ -910,7 +1178,7 @@ audiounit_get_acceptable_latency_range(AudioValueRange * latency_range) &size, latency_range); if (r != noErr) { - PRINT_ERROR_CODE("AudioObjectGetPropertyData/buffer size range", r); + LOG("AudioObjectGetPropertyData/buffer size range rv=%d", r); return CUBEB_ERROR; } @@ -921,20 +1189,19 @@ audiounit_get_acceptable_latency_range(AudioValueRange * latency_range) static AudioObjectID audiounit_get_default_device_id(cubeb_device_type type) { - AudioObjectPropertyAddress adr = { 0, kAudioObjectPropertyScopeGlobal, kAudioObjectPropertyElementMaster }; - AudioDeviceID devid; - UInt32 size; - + const AudioObjectPropertyAddress * adr; if (type == CUBEB_DEVICE_TYPE_OUTPUT) { - adr.mSelector = kAudioHardwarePropertyDefaultOutputDevice; + adr = &DEFAULT_OUTPUT_DEVICE_PROPERTY_ADDRESS; } else if (type == CUBEB_DEVICE_TYPE_INPUT) { - adr.mSelector = kAudioHardwarePropertyDefaultInputDevice; + adr = &DEFAULT_INPUT_DEVICE_PROPERTY_ADDRESS; } else { return kAudioObjectUnknown; } - size = sizeof(AudioDeviceID); - if (AudioObjectGetPropertyData(kAudioObjectSystemObject, &adr, 0, NULL, &size, &devid) != noErr) { + AudioDeviceID devid; + UInt32 size = sizeof(AudioDeviceID); + if (AudioObjectGetPropertyData(kAudioObjectSystemObject, + adr, 0, NULL, &size, &devid) != noErr) { return kAudioObjectUnknown; } @@ -960,7 +1227,8 @@ audiounit_get_max_channel_count(cubeb * ctx, uint32_t * max_channels) assert(ctx && max_channels); - if (audiounit_get_output_device_id(&output_device_id) != CUBEB_OK) { + output_device_id = audiounit_get_default_device_id(CUBEB_DEVICE_TYPE_OUTPUT); + if (output_device_id == kAudioObjectUnknown) { return CUBEB_ERROR; } @@ -973,7 +1241,7 @@ audiounit_get_max_channel_count(cubeb * ctx, uint32_t * max_channels) &size, &stream_format); if (r != noErr) { - PRINT_ERROR_CODE("AudioObjectPropertyAddress/StreamFormat", r); + LOG("AudioObjectPropertyAddress/StreamFormat rv=%d", r); return CUBEB_ERROR; } @@ -997,7 +1265,7 @@ audiounit_get_min_latency(cubeb * /* ctx */, return CUBEB_ERROR; } - *latency_frames = std::max<uint32_t>(latency_range.mMinimum, + *latency_frames = max<uint32_t>(latency_range.mMinimum, SAFE_MIN_LATENCY_FRAMES); #endif @@ -1021,7 +1289,8 @@ audiounit_get_preferred_sample_rate(cubeb * /* ctx */, uint32_t * rate) kAudioObjectPropertyElementMaster }; - if (audiounit_get_output_device_id(&output_device_id) != CUBEB_OK) { + output_device_id = audiounit_get_default_device_id(CUBEB_DEVICE_TYPE_OUTPUT); + if (output_device_id == kAudioObjectUnknown) { return CUBEB_ERROR; } @@ -1042,24 +1311,133 @@ audiounit_get_preferred_sample_rate(cubeb * /* ctx */, uint32_t * rate) return CUBEB_OK; } -static OSStatus audiounit_remove_device_listener(cubeb * context); +static cubeb_channel_layout +audiounit_convert_channel_layout(AudioChannelLayout * layout) +{ + // When having one or two channel, force mono or stereo. Some devices (namely, + // Bose QC35, mark 1 and 2), expose a single channel mapped to the right for + // some reason. + if (layout->mNumberChannelDescriptions == 1) { + return CUBEB_LAYOUT_MONO; + } else if (layout->mNumberChannelDescriptions == 2) { + return CUBEB_LAYOUT_STEREO; + } + + if (layout->mChannelLayoutTag != kAudioChannelLayoutTag_UseChannelDescriptions) { + // kAudioChannelLayoutTag_UseChannelBitmap + // kAudioChannelLayoutTag_Mono + // kAudioChannelLayoutTag_Stereo + // .... + LOG("Only handle UseChannelDescriptions for now.\n"); + return CUBEB_LAYOUT_UNDEFINED; + } + + cubeb_channel_layout cl = 0; + for (UInt32 i = 0; i < layout->mNumberChannelDescriptions; ++i) { + cubeb_channel cc = channel_label_to_cubeb_channel( + layout->mChannelDescriptions[i].mChannelLabel); + if (cc == CHANNEL_UNKNOWN) { + return CUBEB_LAYOUT_UNDEFINED; + } + cl |= cc; + } + + return cl; +} + +static cubeb_channel_layout +audiounit_get_preferred_channel_layout(AudioUnit output_unit) +{ + OSStatus rv = noErr; + UInt32 size = 0; + rv = AudioUnitGetPropertyInfo(output_unit, + kAudioDevicePropertyPreferredChannelLayout, + kAudioUnitScope_Output, + AU_OUT_BUS, + &size, + nullptr); + if (rv != noErr) { + LOG("AudioUnitGetPropertyInfo/kAudioDevicePropertyPreferredChannelLayout rv=%d", rv); + return CUBEB_LAYOUT_UNDEFINED; + } + assert(size > 0); + + auto layout = make_sized_audio_channel_layout(size); + rv = AudioUnitGetProperty(output_unit, + kAudioDevicePropertyPreferredChannelLayout, + kAudioUnitScope_Output, + AU_OUT_BUS, + layout.get(), + &size); + if (rv != noErr) { + LOG("AudioUnitGetProperty/kAudioDevicePropertyPreferredChannelLayout rv=%d", rv); + return CUBEB_LAYOUT_UNDEFINED; + } + + return audiounit_convert_channel_layout(layout.get()); +} + +static cubeb_channel_layout +audiounit_get_current_channel_layout(AudioUnit output_unit) +{ + OSStatus rv = noErr; + UInt32 size = 0; + rv = AudioUnitGetPropertyInfo(output_unit, + kAudioUnitProperty_AudioChannelLayout, + kAudioUnitScope_Output, + AU_OUT_BUS, + &size, + nullptr); + if (rv != noErr) { + LOG("AudioUnitGetPropertyInfo/kAudioUnitProperty_AudioChannelLayout rv=%d", rv); + // This property isn't known before macOS 10.12, attempt another method. + return audiounit_get_preferred_channel_layout(output_unit); + } + assert(size > 0); + + auto layout = make_sized_audio_channel_layout(size); + rv = AudioUnitGetProperty(output_unit, + kAudioUnitProperty_AudioChannelLayout, + kAudioUnitScope_Output, + AU_OUT_BUS, + layout.get(), + &size); + if (rv != noErr) { + LOG("AudioUnitGetProperty/kAudioUnitProperty_AudioChannelLayout rv=%d", rv); + return CUBEB_LAYOUT_UNDEFINED; + } + + return audiounit_convert_channel_layout(layout.get()); +} + +static int audiounit_create_unit(AudioUnit * unit, device_info * device); + +static OSStatus audiounit_remove_device_listener(cubeb * context, cubeb_device_type devtype); static void audiounit_destroy(cubeb * ctx) { - // Disabling this assert for bug 1083664 -- we seem to leak a stream - // assert(ctx->active_streams == 0); - { auto_lock lock(ctx->mutex); + + // Disabling this assert for bug 1083664 -- we seem to leak a stream + // assert(ctx->active_streams == 0); + if (audiounit_active_streams(ctx) > 0) { + LOG("(%p) API misuse, %d streams active when context destroyed!", ctx, audiounit_active_streams(ctx)); + } + /* Unregister the callback if necessary. */ - if(ctx->collection_changed_callback) { - audiounit_remove_device_listener(ctx); + if (ctx->input_collection_changed_callback) { + audiounit_remove_device_listener(ctx, CUBEB_DEVICE_TYPE_INPUT); + } + if (ctx->output_collection_changed_callback) { + audiounit_remove_device_listener(ctx, CUBEB_DEVICE_TYPE_OUTPUT); } } - ctx->~cubeb(); - free(ctx); + dispatch_release(ctx->serial_queue); + + delete ctx; } static void audiounit_stream_destroy(cubeb_stream * stm); @@ -1105,29 +1483,527 @@ audio_stream_desc_init(AudioStreamBasicDescription * ss, return CUBEB_OK; } +void +audiounit_init_mixer(cubeb_stream * stm) +{ + // We can't rely on macOS' AudioUnit to properly downmix (or upmix) the audio + // data, it silently drop the channels so we need to remix the + // audio data by ourselves to keep all the information. + stm->mixer.reset(cubeb_mixer_create(stm->output_stream_params.format, + stm->output_stream_params.channels, + stm->output_stream_params.layout, + stm->context->channels, + stm->context->layout)); + assert(stm->mixer); +} + static int -audiounit_create_unit(AudioUnit * unit, - bool is_input, - const cubeb_stream_params * /* stream_params */, - cubeb_devid device) +audiounit_set_channel_layout(AudioUnit unit, + io_side side, + cubeb_channel_layout layout) +{ + if (side != io_side::OUTPUT) { + return CUBEB_ERROR; + } + + if (layout == CUBEB_LAYOUT_UNDEFINED) { + // We leave everything as-is... + return CUBEB_OK; + } + + + OSStatus r; + uint32_t nb_channels = cubeb_channel_layout_nb_channels(layout); + + // We do not use CoreAudio standard layout for lack of documentation on what + // the actual channel orders are. So we set a custom layout. + size_t size = offsetof(AudioChannelLayout, mChannelDescriptions[nb_channels]); + auto au_layout = make_sized_audio_channel_layout(size); + au_layout->mChannelLayoutTag = kAudioChannelLayoutTag_UseChannelDescriptions; + au_layout->mNumberChannelDescriptions = nb_channels; + + uint32_t channels = 0; + cubeb_channel_layout channelMap = layout; + for (uint32_t i = 0; channelMap != 0; ++i) { + XASSERT(channels < nb_channels); + uint32_t channel = (channelMap & 1) << i; + if (channel != 0) { + au_layout->mChannelDescriptions[channels].mChannelLabel = + cubeb_channel_to_channel_label(static_cast<cubeb_channel>(channel)); + au_layout->mChannelDescriptions[channels].mChannelFlags = kAudioChannelFlags_AllOff; + channels++; + } + channelMap = channelMap >> 1; + } + + r = AudioUnitSetProperty(unit, + kAudioUnitProperty_AudioChannelLayout, + kAudioUnitScope_Input, + AU_OUT_BUS, + au_layout.get(), + size); + if (r != noErr) { + LOG("AudioUnitSetProperty/%s/kAudioUnitProperty_AudioChannelLayout rv=%d", to_string(side), r); + return CUBEB_ERROR; + } + + return CUBEB_OK; +} + +void +audiounit_layout_init(cubeb_stream * stm, io_side side) +{ + // We currently don't support the input layout setting. + if (side == io_side::INPUT) { + return; + } + + stm->context->layout = audiounit_get_current_channel_layout(stm->output_unit); + + audiounit_set_channel_layout(stm->output_unit, io_side::OUTPUT, stm->context->layout); +} + +static vector<AudioObjectID> +audiounit_get_sub_devices(AudioDeviceID device_id) +{ + vector<AudioDeviceID> sub_devices; + AudioObjectPropertyAddress property_address = { kAudioAggregateDevicePropertyActiveSubDeviceList, + kAudioObjectPropertyScopeGlobal, + kAudioObjectPropertyElementMaster }; + UInt32 size = 0; + OSStatus rv = AudioObjectGetPropertyDataSize(device_id, + &property_address, + 0, + nullptr, + &size); + + if (rv != noErr) { + sub_devices.push_back(device_id); + return sub_devices; + } + + uint32_t count = static_cast<uint32_t>(size / sizeof(AudioObjectID)); + sub_devices.resize(count); + rv = AudioObjectGetPropertyData(device_id, + &property_address, + 0, + nullptr, + &size, + sub_devices.data()); + if (rv != noErr) { + sub_devices.clear(); + sub_devices.push_back(device_id); + } else { + LOG("Found %u sub-devices", count); + } + return sub_devices; +} + +static int +audiounit_create_blank_aggregate_device(AudioObjectID * plugin_id, AudioDeviceID * aggregate_device_id) +{ + AudioObjectPropertyAddress address_plugin_bundle_id = { kAudioHardwarePropertyPlugInForBundleID, + kAudioObjectPropertyScopeGlobal, + kAudioObjectPropertyElementMaster }; + UInt32 size = 0; + OSStatus r = AudioObjectGetPropertyDataSize(kAudioObjectSystemObject, + &address_plugin_bundle_id, + 0, NULL, + &size); + if (r != noErr) { + LOG("AudioObjectGetPropertyDataSize/kAudioHardwarePropertyPlugInForBundleID, rv=%d", r); + return CUBEB_ERROR; + } + + AudioValueTranslation translation_value; + CFStringRef in_bundle_ref = CFSTR("com.apple.audio.CoreAudio"); + translation_value.mInputData = &in_bundle_ref; + translation_value.mInputDataSize = sizeof(in_bundle_ref); + translation_value.mOutputData = plugin_id; + translation_value.mOutputDataSize = sizeof(*plugin_id); + + r = AudioObjectGetPropertyData(kAudioObjectSystemObject, + &address_plugin_bundle_id, + 0, + nullptr, + &size, + &translation_value); + if (r != noErr) { + LOG("AudioObjectGetPropertyData/kAudioHardwarePropertyPlugInForBundleID, rv=%d", r); + return CUBEB_ERROR; + } + + AudioObjectPropertyAddress create_aggregate_device_address = { kAudioPlugInCreateAggregateDevice, + kAudioObjectPropertyScopeGlobal, + kAudioObjectPropertyElementMaster }; + r = AudioObjectGetPropertyDataSize(*plugin_id, + &create_aggregate_device_address, + 0, + nullptr, + &size); + if (r != noErr) { + LOG("AudioObjectGetPropertyDataSize/kAudioPlugInCreateAggregateDevice, rv=%d", r); + return CUBEB_ERROR; + } + + CFMutableDictionaryRef aggregate_device_dict = CFDictionaryCreateMutable(kCFAllocatorDefault, 0, + &kCFTypeDictionaryKeyCallBacks, + &kCFTypeDictionaryValueCallBacks); + struct timeval timestamp; + gettimeofday(×tamp, NULL); + long long int time_id = timestamp.tv_sec * 1000000LL + timestamp.tv_usec; + CFStringRef aggregate_device_name = CFStringCreateWithFormat(NULL, NULL, CFSTR("%s_%llx"), PRIVATE_AGGREGATE_DEVICE_NAME, time_id); + CFDictionaryAddValue(aggregate_device_dict, CFSTR(kAudioAggregateDeviceNameKey), aggregate_device_name); + CFRelease(aggregate_device_name); + + CFStringRef aggregate_device_UID = CFStringCreateWithFormat(NULL, NULL, CFSTR("org.mozilla.%s_%llx"), PRIVATE_AGGREGATE_DEVICE_NAME, time_id); + CFDictionaryAddValue(aggregate_device_dict, CFSTR(kAudioAggregateDeviceUIDKey), aggregate_device_UID); + CFRelease(aggregate_device_UID); + + int private_value = 1; + CFNumberRef aggregate_device_private_key = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &private_value); + CFDictionaryAddValue(aggregate_device_dict, CFSTR(kAudioAggregateDeviceIsPrivateKey), aggregate_device_private_key); + CFRelease(aggregate_device_private_key); + + int stacked_value = 0; + CFNumberRef aggregate_device_stacked_key = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &stacked_value); + CFDictionaryAddValue(aggregate_device_dict, CFSTR(kAudioAggregateDeviceIsStackedKey), aggregate_device_stacked_key); + CFRelease(aggregate_device_stacked_key); + + r = AudioObjectGetPropertyData(*plugin_id, + &create_aggregate_device_address, + sizeof(aggregate_device_dict), + &aggregate_device_dict, + &size, + aggregate_device_id); + CFRelease(aggregate_device_dict); + if (r != noErr) { + LOG("AudioObjectGetPropertyData/kAudioPlugInCreateAggregateDevice, rv=%d", r); + return CUBEB_ERROR; + } + LOG("New aggregate device %u", *aggregate_device_id); + + return CUBEB_OK; +} + +// The returned CFStringRef object needs to be released (via CFRelease) +// if it's not NULL, since the reference count of the returned CFStringRef +// object is increased. +static CFStringRef +get_device_name(AudioDeviceID id) +{ + UInt32 size = sizeof(CFStringRef); + CFStringRef UIname = nullptr; + AudioObjectPropertyAddress address_uuid = { kAudioDevicePropertyDeviceUID, + kAudioObjectPropertyScopeGlobal, + kAudioObjectPropertyElementMaster }; + OSStatus err = AudioObjectGetPropertyData(id, &address_uuid, 0, nullptr, &size, &UIname); + return (err == noErr) ? UIname : NULL; +} + +static int +audiounit_set_aggregate_sub_device_list(AudioDeviceID aggregate_device_id, + AudioDeviceID input_device_id, + AudioDeviceID output_device_id) +{ + LOG("Add devices input %u and output %u into aggregate device %u", + input_device_id, output_device_id, aggregate_device_id); + const vector<AudioDeviceID> output_sub_devices = audiounit_get_sub_devices(output_device_id); + const vector<AudioDeviceID> input_sub_devices = audiounit_get_sub_devices(input_device_id); + + CFMutableArrayRef aggregate_sub_devices_array = CFArrayCreateMutable(NULL, 0, &kCFTypeArrayCallBacks); + /* The order of the items in the array is significant and is used to determine the order of the streams + of the AudioAggregateDevice. */ + for (UInt32 i = 0; i < output_sub_devices.size(); i++) { + CFStringRef ref = get_device_name(output_sub_devices[i]); + if (ref == NULL) { + CFRelease(aggregate_sub_devices_array); + return CUBEB_ERROR; + } + CFArrayAppendValue(aggregate_sub_devices_array, ref); + CFRelease(ref); + } + for (UInt32 i = 0; i < input_sub_devices.size(); i++) { + CFStringRef ref = get_device_name(input_sub_devices[i]); + if (ref == NULL) { + CFRelease(aggregate_sub_devices_array); + return CUBEB_ERROR; + } + CFArrayAppendValue(aggregate_sub_devices_array, ref); + CFRelease(ref); + } + + AudioObjectPropertyAddress aggregate_sub_device_list = { kAudioAggregateDevicePropertyFullSubDeviceList, + kAudioObjectPropertyScopeGlobal, + kAudioObjectPropertyElementMaster }; + UInt32 size = sizeof(CFMutableArrayRef); + OSStatus rv = AudioObjectSetPropertyData(aggregate_device_id, + &aggregate_sub_device_list, + 0, + nullptr, + size, + &aggregate_sub_devices_array); + CFRelease(aggregate_sub_devices_array); + if (rv != noErr) { + LOG("AudioObjectSetPropertyData/kAudioAggregateDevicePropertyFullSubDeviceList, rv=%d", rv); + return CUBEB_ERROR; + } + + return CUBEB_OK; +} + +static int +audiounit_set_master_aggregate_device(const AudioDeviceID aggregate_device_id) +{ + assert(aggregate_device_id != kAudioObjectUnknown); + AudioObjectPropertyAddress master_aggregate_sub_device = { kAudioAggregateDevicePropertyMasterSubDevice, + kAudioObjectPropertyScopeGlobal, + kAudioObjectPropertyElementMaster }; + + // Master become the 1st output sub device + AudioDeviceID output_device_id = audiounit_get_default_device_id(CUBEB_DEVICE_TYPE_OUTPUT); + const vector<AudioDeviceID> output_sub_devices = audiounit_get_sub_devices(output_device_id); + CFStringRef master_sub_device = get_device_name(output_sub_devices[0]); + + UInt32 size = sizeof(CFStringRef); + OSStatus rv = AudioObjectSetPropertyData(aggregate_device_id, + &master_aggregate_sub_device, + 0, + NULL, + size, + &master_sub_device); + if (master_sub_device) { + CFRelease(master_sub_device); + } + if (rv != noErr) { + LOG("AudioObjectSetPropertyData/kAudioAggregateDevicePropertyMasterSubDevice, rv=%d", rv); + return CUBEB_ERROR; + } + + return CUBEB_OK; +} + +static int +audiounit_activate_clock_drift_compensation(const AudioDeviceID aggregate_device_id) +{ + assert(aggregate_device_id != kAudioObjectUnknown); + AudioObjectPropertyAddress address_owned = { kAudioObjectPropertyOwnedObjects, + kAudioObjectPropertyScopeGlobal, + kAudioObjectPropertyElementMaster }; + + UInt32 qualifier_data_size = sizeof(AudioObjectID); + AudioClassID class_id = kAudioSubDeviceClassID; + void * qualifier_data = &class_id; + UInt32 size = 0; + OSStatus rv = AudioObjectGetPropertyDataSize(aggregate_device_id, + &address_owned, + qualifier_data_size, + qualifier_data, + &size); + if (rv != noErr) { + LOG("AudioObjectGetPropertyDataSize/kAudioObjectPropertyOwnedObjects, rv=%d", rv); + return CUBEB_ERROR; + } + + UInt32 subdevices_num = 0; + subdevices_num = size / sizeof(AudioObjectID); + AudioObjectID sub_devices[subdevices_num]; + size = sizeof(sub_devices); + + rv = AudioObjectGetPropertyData(aggregate_device_id, + &address_owned, + qualifier_data_size, + qualifier_data, + &size, + sub_devices); + if (rv != noErr) { + LOG("AudioObjectGetPropertyData/kAudioObjectPropertyOwnedObjects, rv=%d", rv); + return CUBEB_ERROR; + } + + AudioObjectPropertyAddress address_drift = { kAudioSubDevicePropertyDriftCompensation, + kAudioObjectPropertyScopeGlobal, + kAudioObjectPropertyElementMaster }; + + // Start from the second device since the first is the master clock + for (UInt32 i = 1; i < subdevices_num; ++i) { + UInt32 drift_compensation_value = 1; + rv = AudioObjectSetPropertyData(sub_devices[i], + &address_drift, + 0, + nullptr, + sizeof(UInt32), + &drift_compensation_value); + if (rv != noErr) { + LOG("AudioObjectSetPropertyData/kAudioSubDevicePropertyDriftCompensation, rv=%d", rv); + return CUBEB_OK; + } + } + return CUBEB_OK; +} + +static int audiounit_destroy_aggregate_device(AudioObjectID plugin_id, AudioDeviceID * aggregate_device_id); +static void audiounit_get_available_samplerate(AudioObjectID devid, AudioObjectPropertyScope scope, + uint32_t * min, uint32_t * max, uint32_t * def); +static int +audiounit_create_device_from_hwdev(cubeb_device_info * dev_info, AudioObjectID devid, cubeb_device_type type); +static void audiounit_device_destroy(cubeb_device_info * device); + +static void +audiounit_workaround_for_airpod(cubeb_stream * stm) +{ + cubeb_device_info input_device_info; + audiounit_create_device_from_hwdev(&input_device_info, stm->input_device.id, CUBEB_DEVICE_TYPE_INPUT); + + cubeb_device_info output_device_info; + audiounit_create_device_from_hwdev(&output_device_info, stm->output_device.id, CUBEB_DEVICE_TYPE_OUTPUT); + + std::string input_name_str(input_device_info.friendly_name); + std::string output_name_str(output_device_info.friendly_name); + + if(input_name_str.find("AirPods") != std::string::npos && + output_name_str.find("AirPods") != std::string::npos) { + uint32_t input_min_rate = 0; + uint32_t input_max_rate = 0; + uint32_t input_nominal_rate = 0; + audiounit_get_available_samplerate(stm->input_device.id, kAudioObjectPropertyScopeGlobal, + &input_min_rate, &input_max_rate, &input_nominal_rate); + LOG("(%p) Input device %u, name: %s, min: %u, max: %u, nominal rate: %u", stm, stm->input_device.id + , input_device_info.friendly_name, input_min_rate, input_max_rate, input_nominal_rate); + uint32_t output_min_rate = 0; + uint32_t output_max_rate = 0; + uint32_t output_nominal_rate = 0; + audiounit_get_available_samplerate(stm->output_device.id, kAudioObjectPropertyScopeGlobal, + &output_min_rate, &output_max_rate, &output_nominal_rate); + LOG("(%p) Output device %u, name: %s, min: %u, max: %u, nominal rate: %u", stm, stm->output_device.id + , output_device_info.friendly_name, output_min_rate, output_max_rate, output_nominal_rate); + + Float64 rate = input_nominal_rate; + AudioObjectPropertyAddress addr = {kAudioDevicePropertyNominalSampleRate, + kAudioObjectPropertyScopeGlobal, + kAudioObjectPropertyElementMaster}; + + OSStatus rv = AudioObjectSetPropertyData(stm->aggregate_device_id, + &addr, + 0, + nullptr, + sizeof(Float64), + &rate); + if (rv != noErr) { + LOG("Non fatal error, AudioObjectSetPropertyData/kAudioDevicePropertyNominalSampleRate, rv=%d", rv); + } + } + audiounit_device_destroy(&input_device_info); + audiounit_device_destroy(&output_device_info); +} + +/* + * Aggregate Device is a virtual audio interface which utilizes inputs and outputs + * of one or more physical audio interfaces. It is possible to use the clock of + * one of the devices as a master clock for all the combined devices and enable + * drift compensation for the devices that are not designated clock master. + * + * Creating a new aggregate device programmatically requires [0][1]: + * 1. Locate the base plug-in ("com.apple.audio.CoreAudio") + * 2. Create a dictionary that describes the aggregate device + * (don't add sub-devices in that step, prone to fail [0]) + * 3. Ask the base plug-in to create the aggregate device (blank) + * 4. Add the array of sub-devices. + * 5. Set the master device (1st output device in our case) + * 6. Enable drift compensation for the non-master devices + * + * [0] https://lists.apple.com/archives/coreaudio-api/2006/Apr/msg00092.html + * [1] https://lists.apple.com/archives/coreaudio-api/2005/Jul/msg00150.html + * [2] CoreAudio.framework/Headers/AudioHardware.h + * */ +static int +audiounit_create_aggregate_device(cubeb_stream * stm) +{ + int r = audiounit_create_blank_aggregate_device(&stm->plugin_id, &stm->aggregate_device_id); + if (r != CUBEB_OK) { + LOG("(%p) Failed to create blank aggregate device", stm); + return CUBEB_ERROR; + } + + r = audiounit_set_aggregate_sub_device_list(stm->aggregate_device_id, stm->input_device.id, stm->output_device.id); + if (r != CUBEB_OK) { + LOG("(%p) Failed to set aggregate sub-device list", stm); + audiounit_destroy_aggregate_device(stm->plugin_id, &stm->aggregate_device_id); + return CUBEB_ERROR; + } + + r = audiounit_set_master_aggregate_device(stm->aggregate_device_id); + if (r != CUBEB_OK) { + LOG("(%p) Failed to set master sub-device for aggregate device", stm); + audiounit_destroy_aggregate_device(stm->plugin_id, &stm->aggregate_device_id); + return CUBEB_ERROR; + } + + r = audiounit_activate_clock_drift_compensation(stm->aggregate_device_id); + if (r != CUBEB_OK) { + LOG("(%p) Failed to activate clock drift compensation for aggregate device", stm); + audiounit_destroy_aggregate_device(stm->plugin_id, &stm->aggregate_device_id); + return CUBEB_ERROR; + } + + audiounit_workaround_for_airpod(stm); + + return CUBEB_OK; +} + +static int +audiounit_destroy_aggregate_device(AudioObjectID plugin_id, AudioDeviceID * aggregate_device_id) +{ + assert(aggregate_device_id && + *aggregate_device_id != kAudioDeviceUnknown && + plugin_id != kAudioObjectUnknown); + AudioObjectPropertyAddress destroy_aggregate_device_addr = { kAudioPlugInDestroyAggregateDevice, + kAudioObjectPropertyScopeGlobal, + kAudioObjectPropertyElementMaster}; + UInt32 size; + OSStatus rv = AudioObjectGetPropertyDataSize(plugin_id, + &destroy_aggregate_device_addr, + 0, + NULL, + &size); + if (rv != noErr) { + LOG("AudioObjectGetPropertyDataSize/kAudioPlugInDestroyAggregateDevice, rv=%d", rv); + return CUBEB_ERROR; + } + + rv = AudioObjectGetPropertyData(plugin_id, + &destroy_aggregate_device_addr, + 0, + NULL, + &size, + aggregate_device_id); + if (rv != noErr) { + LOG("AudioObjectGetPropertyData/kAudioPlugInDestroyAggregateDevice, rv=%d", rv); + return CUBEB_ERROR; + } + + LOG("Destroyed aggregate device %d", *aggregate_device_id); + *aggregate_device_id = kAudioObjectUnknown; + return CUBEB_OK; +} + +static int +audiounit_new_unit_instance(AudioUnit * unit, device_info * device) { AudioComponentDescription desc; AudioComponent comp; - UInt32 enable; - AudioDeviceID devid; OSStatus rv; desc.componentType = kAudioUnitType_Output; #if TARGET_OS_IPHONE - bool use_default_output = false; desc.componentSubType = kAudioUnitSubType_RemoteIO; #else // Use the DefaultOutputUnit for output when no device is specified // so we retain automatic output device switching when the default // changes. Once we have complete support for device notifications // and switching, we can use the AUHAL for everything. - bool use_default_output = device == NULL && !is_input; - if (use_default_output) { + if ((device->flags & DEV_SYSTEM_DEFAULT) && + (device->flags & DEV_OUTPUT)) { desc.componentSubType = kAudioUnitSubType_DefaultOutput; } else { desc.componentSubType = kAudioUnitSubType_HALOutput; @@ -1144,96 +2020,118 @@ audiounit_create_unit(AudioUnit * unit, rv = AudioComponentInstanceNew(comp, unit); if (rv != noErr) { - PRINT_ERROR_CODE("AudioComponentInstanceNew", rv); + LOG("AudioComponentInstanceNew rv=%d", rv); return CUBEB_ERROR; } + return CUBEB_OK; +} - if (!use_default_output) { - enable = 1; - rv = AudioUnitSetProperty(*unit, kAudioOutputUnitProperty_EnableIO, - is_input ? kAudioUnitScope_Input : kAudioUnitScope_Output, - is_input ? AU_IN_BUS : AU_OUT_BUS, &enable, sizeof(UInt32)); - if (rv != noErr) { - PRINT_ERROR_CODE("AudioUnitSetProperty/kAudioOutputUnitProperty_EnableIO", rv); - return CUBEB_ERROR; - } - - enable = 0; - rv = AudioUnitSetProperty(*unit, kAudioOutputUnitProperty_EnableIO, - is_input ? kAudioUnitScope_Output : kAudioUnitScope_Input, - is_input ? AU_OUT_BUS : AU_IN_BUS, &enable, sizeof(UInt32)); - if (rv != noErr) { - PRINT_ERROR_CODE("AudioUnitSetProperty/kAudioOutputUnitProperty_EnableIO", rv); - return CUBEB_ERROR; - } +enum enable_state { + DISABLE, + ENABLE, +}; - if (device == NULL) { - assert(is_input); - devid = audiounit_get_default_device_id(CUBEB_DEVICE_TYPE_INPUT); - } else { - devid = reinterpret_cast<intptr_t>(device); - } - rv = AudioUnitSetProperty(*unit, kAudioOutputUnitProperty_CurrentDevice, - kAudioUnitScope_Global, - is_input ? AU_IN_BUS : AU_OUT_BUS, - &devid, sizeof(AudioDeviceID)); - if (rv != noErr) { - PRINT_ERROR_CODE("AudioUnitSetProperty/kAudioOutputUnitProperty_CurrentDevice", rv); - return CUBEB_ERROR; - } +static int +audiounit_enable_unit_scope(AudioUnit * unit, io_side side, enable_state state) +{ + OSStatus rv; + UInt32 enable = state; + rv = AudioUnitSetProperty(*unit, kAudioOutputUnitProperty_EnableIO, + (side == io_side::INPUT) ? kAudioUnitScope_Input : kAudioUnitScope_Output, + (side == io_side::INPUT) ? AU_IN_BUS : AU_OUT_BUS, + &enable, + sizeof(UInt32)); + if (rv != noErr) { + LOG("AudioUnitSetProperty/kAudioOutputUnitProperty_EnableIO rv=%d", rv); + return CUBEB_ERROR; } - return CUBEB_OK; } static int -audiounit_init_input_linear_buffer(cubeb_stream * stream, uint32_t capacity) +audiounit_create_unit(AudioUnit * unit, device_info * device) { - if (stream->input_desc.mFormatFlags & kAudioFormatFlagIsSignedInteger) { - stream->input_linear_buffer = new auto_array_wrapper( - new auto_array<short>(capacity * - stream->input_buffer_frames * - stream->input_desc.mChannelsPerFrame) ); - } else { - stream->input_linear_buffer = new auto_array_wrapper( - new auto_array<float>(capacity * - stream->input_buffer_frames * - stream->input_desc.mChannelsPerFrame) ); + assert(*unit == nullptr); + assert(device); + + OSStatus rv; + int r; + + r = audiounit_new_unit_instance(unit, device); + if (r != CUBEB_OK) { + return r; } + assert(*unit); - if (!stream->input_linear_buffer) { - return CUBEB_ERROR; + if ((device->flags & DEV_SYSTEM_DEFAULT) && + (device->flags & DEV_OUTPUT)) { + return CUBEB_OK; } - assert(stream->input_linear_buffer->length() == 0); - // Pre-buffer silence if needed - if (capacity != 1) { - size_t silence_size = stream->input_buffer_frames * - stream->input_desc.mChannelsPerFrame; - stream->input_linear_buffer->push_silence(silence_size); + if (device->flags & DEV_INPUT) { + r = audiounit_enable_unit_scope(unit, io_side::INPUT, ENABLE); + if (r != CUBEB_OK) { + LOG("Failed to enable audiounit input scope"); + return r; + } + r = audiounit_enable_unit_scope(unit, io_side::OUTPUT, DISABLE); + if (r != CUBEB_OK) { + LOG("Failed to disable audiounit output scope"); + return r; + } + } else if (device->flags & DEV_OUTPUT) { + r = audiounit_enable_unit_scope(unit, io_side::OUTPUT, ENABLE); + if (r != CUBEB_OK) { + LOG("Failed to enable audiounit output scope"); + return r; + } + r = audiounit_enable_unit_scope(unit, io_side::INPUT, DISABLE); + if (r != CUBEB_OK) { + LOG("Failed to disable audiounit input scope"); + return r; + } + } else { + assert(false); + } - assert(stream->input_linear_buffer->length() == silence_size); + rv = AudioUnitSetProperty(*unit, + kAudioOutputUnitProperty_CurrentDevice, + kAudioUnitScope_Global, + 0, + &device->id, sizeof(AudioDeviceID)); + if (rv != noErr) { + LOG("AudioUnitSetProperty/kAudioOutputUnitProperty_CurrentDevice rv=%d", rv); + return CUBEB_ERROR; } return CUBEB_OK; } -static void -audiounit_destroy_input_linear_buffer(cubeb_stream * stream) +static int +audiounit_init_input_linear_buffer(cubeb_stream * stream, uint32_t capacity) { - delete stream->input_linear_buffer; + uint32_t size = capacity * stream->latency_frames * stream->input_desc.mChannelsPerFrame; + if (stream->input_desc.mFormatFlags & kAudioFormatFlagIsSignedInteger) { + stream->input_linear_buffer.reset(new auto_array_wrapper_impl<short>(size)); + } else { + stream->input_linear_buffer.reset(new auto_array_wrapper_impl<float>(size)); + } + assert(stream->input_linear_buffer->length() == 0); + + return CUBEB_OK; } static uint32_t audiounit_clamp_latency(cubeb_stream * stm, uint32_t latency_frames) { // For the 1st stream set anything within safe min-max - assert(stm->context->active_streams > 0); - if (stm->context->active_streams == 1) { - return std::max(std::min<uint32_t>(latency_frames, SAFE_MAX_LATENCY_FRAMES), + assert(audiounit_active_streams(stm->context) > 0); + if (audiounit_active_streams(stm->context) == 1) { + return max(min<uint32_t>(latency_frames, SAFE_MAX_LATENCY_FRAMES), SAFE_MIN_LATENCY_FRAMES); } + assert(stm->output_unit); // If more than one stream operates in parallel // allow only lower values of latency @@ -1248,11 +2146,11 @@ audiounit_clamp_latency(cubeb_stream * stm, uint32_t latency_frames) &output_buffer_size, &size); if (r != noErr) { - PRINT_ERROR_CODE("AudioUnitGetProperty/output/kAudioDevicePropertyBufferFrameSize", r); + LOG("AudioUnitGetProperty/output/kAudioDevicePropertyBufferFrameSize rv=%d", r); return 0; } - output_buffer_size = std::max(std::min<uint32_t>(output_buffer_size, SAFE_MAX_LATENCY_FRAMES), + output_buffer_size = max(min<uint32_t>(output_buffer_size, SAFE_MAX_LATENCY_FRAMES), SAFE_MIN_LATENCY_FRAMES); } @@ -1265,18 +2163,18 @@ audiounit_clamp_latency(cubeb_stream * stm, uint32_t latency_frames) &input_buffer_size, &size); if (r != noErr) { - PRINT_ERROR_CODE("AudioUnitGetProperty/input/kAudioDevicePropertyBufferFrameSize", r); + LOG("AudioUnitGetProperty/input/kAudioDevicePropertyBufferFrameSize rv=%d", r); return 0; } - input_buffer_size = std::max(std::min<uint32_t>(input_buffer_size, SAFE_MAX_LATENCY_FRAMES), + input_buffer_size = max(min<uint32_t>(input_buffer_size, SAFE_MAX_LATENCY_FRAMES), SAFE_MIN_LATENCY_FRAMES); } // Every following active streams can only set smaller latency UInt32 upper_latency_limit = 0; if (input_buffer_size != 0 && output_buffer_size != 0) { - upper_latency_limit = std::min<uint32_t>(input_buffer_size, output_buffer_size); + upper_latency_limit = min<uint32_t>(input_buffer_size, output_buffer_size); } else if (input_buffer_size != 0) { upper_latency_limit = input_buffer_size; } else if (output_buffer_size != 0) { @@ -1285,7 +2183,7 @@ audiounit_clamp_latency(cubeb_stream * stm, uint32_t latency_frames) upper_latency_limit = SAFE_MAX_LATENCY_FRAMES; } - return std::max(std::min<uint32_t>(latency_frames, upper_latency_limit), + return max(min<uint32_t>(latency_frames, upper_latency_limit), SAFE_MIN_LATENCY_FRAMES); } @@ -1300,18 +2198,18 @@ audiounit_clamp_latency(cubeb_stream * stm, uint32_t latency_frames) static void buffer_size_changed_callback(void * inClientData, AudioUnit inUnit, - AudioUnitPropertyID inPropertyID, - AudioUnitScope inScope, - AudioUnitElement inElement) + AudioUnitPropertyID inPropertyID, + AudioUnitScope inScope, + AudioUnitElement inElement) { cubeb_stream * stm = (cubeb_stream *)inClientData; AudioUnit au = inUnit; AudioUnitScope au_scope = kAudioUnitScope_Input; AudioUnitElement au_element = inElement; - const char * au_type = "output"; + char const * au_type = "output"; - if (au == stm->input_unit) { + if (AU_IN_BUS == inElement) { au_scope = kAudioUnitScope_Output; au_type = "input"; } @@ -1342,24 +2240,17 @@ buffer_size_changed_callback(void * inClientData, } } -enum set_buffer_size_side { - INPUT, - OUTPUT, -}; - static int -audiounit_set_buffer_size(cubeb_stream * stm, uint32_t new_size_frames, set_buffer_size_side set_side) +audiounit_set_buffer_size(cubeb_stream * stm, uint32_t new_size_frames, io_side side) { AudioUnit au = stm->output_unit; AudioUnitScope au_scope = kAudioUnitScope_Input; AudioUnitElement au_element = AU_OUT_BUS; - const char * au_type = "output"; - if (set_side == INPUT) { + if (side == io_side::INPUT) { au = stm->input_unit; au_scope = kAudioUnitScope_Output; au_element = AU_IN_BUS; - au_type = "input"; } uint32_t buffer_frames = 0; @@ -1371,16 +2262,12 @@ audiounit_set_buffer_size(cubeb_stream * stm, uint32_t new_size_frames, set_buff &buffer_frames, &size); if (r != noErr) { - if (set_side == INPUT) { - PRINT_ERROR_CODE("AudioUnitGetProperty/input/kAudioDevicePropertyBufferFrameSize", r); - } else { - PRINT_ERROR_CODE("AudioUnitGetProperty/output/kAudioDevicePropertyBufferFrameSize", r); - } + LOG("AudioUnitGetProperty/%s/kAudioDevicePropertyBufferFrameSize rv=%d", to_string(side), r); return CUBEB_ERROR; } if (new_size_frames == buffer_frames) { - LOG("(%p) No need to update %s buffer size already %u frames", stm, au_type, buffer_frames); + LOG("(%p) No need to update %s buffer size already %u frames", stm, to_string(side), buffer_frames); return CUBEB_OK; } @@ -1389,11 +2276,7 @@ audiounit_set_buffer_size(cubeb_stream * stm, uint32_t new_size_frames, set_buff buffer_size_changed_callback, stm); if (r != noErr) { - if (set_side == INPUT) { - PRINT_ERROR_CODE("AudioUnitAddPropertyListener/input/kAudioDevicePropertyBufferFrameSize", r); - } else { - PRINT_ERROR_CODE("AudioUnitAddPropertyListener/output/kAudioDevicePropertyBufferFrameSize", r); - } + LOG("AudioUnitAddPropertyListener/%s/kAudioDevicePropertyBufferFrameSize rv=%d", to_string(side), r); return CUBEB_ERROR; } @@ -1406,22 +2289,14 @@ audiounit_set_buffer_size(cubeb_stream * stm, uint32_t new_size_frames, set_buff &new_size_frames, sizeof(new_size_frames)); if (r != noErr) { - if (set_side == INPUT) { - PRINT_ERROR_CODE("AudioUnitSetProperty/input/kAudioDevicePropertyBufferFrameSize", r); - } else { - PRINT_ERROR_CODE("AudioUnitSetProperty/output/kAudioDevicePropertyBufferFrameSize", r); - } + LOG("AudioUnitSetProperty/%s/kAudioDevicePropertyBufferFrameSize rv=%d", to_string(side), r); r = AudioUnitRemovePropertyListenerWithUserData(au, kAudioDevicePropertyBufferFrameSize, buffer_size_changed_callback, stm); if (r != noErr) { - if (set_side == INPUT) { - PRINT_ERROR_CODE("AudioUnitAddPropertyListener/input/kAudioDevicePropertyBufferFrameSize", r); - } else { - PRINT_ERROR_CODE("AudioUnitAddPropertyListener/output/kAudioDevicePropertyBufferFrameSize", r); - } + LOG("AudioUnitAddPropertyListener/%s/kAudioDevicePropertyBufferFrameSize rv=%d", to_string(side), r); } return CUBEB_ERROR; @@ -1443,11 +2318,7 @@ audiounit_set_buffer_size(cubeb_stream * stm, uint32_t new_size_frames, set_buff buffer_size_changed_callback, stm); if (r != noErr) { - if (set_side == INPUT) { - PRINT_ERROR_CODE("AudioUnitAddPropertyListener/input/kAudioDevicePropertyBufferFrameSize", r); - } else { - PRINT_ERROR_CODE("AudioUnitAddPropertyListener/output/kAudioDevicePropertyBufferFrameSize", r); - } + LOG("AudioUnitAddPropertyListener/%s/kAudioDevicePropertyBufferFrameSize rv=%d", to_string(side), r); return CUBEB_ERROR; } @@ -1456,13 +2327,15 @@ audiounit_set_buffer_size(cubeb_stream * stm, uint32_t new_size_frames, set_buff return CUBEB_ERROR; } - LOG("(%p) %s buffer size changed to %u frames.", stm, au_type, new_size_frames); + LOG("(%p) %s buffer size changed to %u frames.", stm, to_string(side), new_size_frames); return CUBEB_OK; } static int audiounit_configure_input(cubeb_stream * stm) { + assert(stm && stm->input_unit); + int r = 0; UInt32 size; AURenderCallbackStruct aurcbs_in; @@ -1481,7 +2354,7 @@ audiounit_configure_input(cubeb_stream * stm) &input_hw_desc, &size); if (r != noErr) { - PRINT_ERROR_CODE("AudioUnitGetProperty/input/kAudioUnitProperty_StreamFormat", r); + LOG("AudioUnitGetProperty/input/kAudioUnitProperty_StreamFormat rv=%d", r); return CUBEB_ERROR; } stm->input_hw_rate = input_hw_desc.mSampleRate; @@ -1495,8 +2368,7 @@ audiounit_configure_input(cubeb_stream * stm) } // Use latency to set buffer size - stm->input_buffer_frames = stm->latency_frames; - r = audiounit_set_buffer_size(stm, stm->input_buffer_frames, INPUT); + r = audiounit_set_buffer_size(stm, stm->latency_frames, io_side::INPUT); if (r != CUBEB_OK) { LOG("(%p) Error in change input buffer size.", stm); return CUBEB_ERROR; @@ -1514,7 +2386,7 @@ audiounit_configure_input(cubeb_stream * stm) &src_desc, sizeof(AudioStreamBasicDescription)); if (r != noErr) { - PRINT_ERROR_CODE("AudioUnitSetProperty/input/kAudioUnitProperty_StreamFormat", r); + LOG("AudioUnitSetProperty/input/kAudioUnitProperty_StreamFormat rv=%d", r); return CUBEB_ERROR; } @@ -1523,10 +2395,10 @@ audiounit_configure_input(cubeb_stream * stm) kAudioUnitProperty_MaximumFramesPerSlice, kAudioUnitScope_Global, AU_IN_BUS, - &stm->input_buffer_frames, + &stm->latency_frames, sizeof(UInt32)); if (r != noErr) { - PRINT_ERROR_CODE("AudioUnitSetProperty/input/kAudioUnitProperty_MaximumFramesPerSlice", r); + LOG("AudioUnitSetProperty/input/kAudioUnitProperty_MaximumFramesPerSlice rv=%d", r); return CUBEB_ERROR; } @@ -1540,7 +2412,6 @@ audiounit_configure_input(cubeb_stream * stm) return CUBEB_ERROR; } - assert(stm->input_unit != NULL); aurcbs_in.inputProc = audiounit_input_callback; aurcbs_in.inputProcRefCon = stm; @@ -1551,9 +2422,12 @@ audiounit_configure_input(cubeb_stream * stm) &aurcbs_in, sizeof(aurcbs_in)); if (r != noErr) { - PRINT_ERROR_CODE("AudioUnitSetProperty/input/kAudioOutputUnitProperty_SetInputCallback", r); + LOG("AudioUnitSetProperty/input/kAudioOutputUnitProperty_SetInputCallback rv=%d", r); return CUBEB_ERROR; } + + stm->frames_read = 0; + LOG("(%p) Input audiounit init successfully.", stm); return CUBEB_OK; @@ -1562,6 +2436,8 @@ audiounit_configure_input(cubeb_stream * stm) static int audiounit_configure_output(cubeb_stream * stm) { + assert(stm && stm->output_unit); + int r; AURenderCallbackStruct aurcbs_out; UInt32 size; @@ -1588,11 +2464,30 @@ audiounit_configure_output(cubeb_stream * stm) &output_hw_desc, &size); if (r != noErr) { - PRINT_ERROR_CODE("AudioUnitGetProperty/output/tkAudioUnitProperty_StreamFormat", r); + LOG("AudioUnitGetProperty/output/kAudioUnitProperty_StreamFormat rv=%d", r); return CUBEB_ERROR; } stm->output_hw_rate = output_hw_desc.mSampleRate; LOG("(%p) Output device sampling rate: %.2f", stm, output_hw_desc.mSampleRate); + stm->context->channels = output_hw_desc.mChannelsPerFrame; + + // Set the input layout to match the output device layout. + audiounit_layout_init(stm, io_side::OUTPUT); + if (stm->context->channels != stm->output_stream_params.channels || + stm->context->layout != stm->output_stream_params.layout) { + LOG("Incompatible channel layouts detected, setting up remixer"); + audiounit_init_mixer(stm); + // We will be remixing the data before it reaches the output device. + // We need to adjust the number of channels and other + // AudioStreamDescription details. + stm->output_desc.mChannelsPerFrame = stm->context->channels; + stm->output_desc.mBytesPerFrame = (stm->output_desc.mBitsPerChannel / 8) * + stm->output_desc.mChannelsPerFrame; + stm->output_desc.mBytesPerPacket = + stm->output_desc.mBytesPerFrame * stm->output_desc.mFramesPerPacket; + } else { + stm->mixer = nullptr; + } r = AudioUnitSetProperty(stm->output_unit, kAudioUnitProperty_StreamFormat, @@ -1601,11 +2496,11 @@ audiounit_configure_output(cubeb_stream * stm) &stm->output_desc, sizeof(AudioStreamBasicDescription)); if (r != noErr) { - PRINT_ERROR_CODE("AudioUnitSetProperty/output/kAudioUnitProperty_StreamFormat", r); + LOG("AudioUnitSetProperty/output/kAudioUnitProperty_StreamFormat rv=%d", r); return CUBEB_ERROR; } - r = audiounit_set_buffer_size(stm, stm->latency_frames, OUTPUT); + r = audiounit_set_buffer_size(stm, stm->latency_frames, io_side::OUTPUT); if (r != CUBEB_OK) { LOG("(%p) Error in change output buffer size.", stm); return CUBEB_ERROR; @@ -1619,11 +2514,10 @@ audiounit_configure_output(cubeb_stream * stm) &stm->latency_frames, sizeof(UInt32)); if (r != noErr) { - PRINT_ERROR_CODE("AudioUnitSetProperty/output/kAudioUnitProperty_MaximumFramesPerSlice", r); + LOG("AudioUnitSetProperty/output/kAudioUnitProperty_MaximumFramesPerSlice rv=%d", r); return CUBEB_ERROR; } - assert(stm->output_unit != NULL); aurcbs_out.inputProc = audiounit_output_callback; aurcbs_out.inputProcRefCon = stm; r = AudioUnitSetProperty(stm->output_unit, @@ -1633,10 +2527,12 @@ audiounit_configure_output(cubeb_stream * stm) &aurcbs_out, sizeof(aurcbs_out)); if (r != noErr) { - PRINT_ERROR_CODE("AudioUnitSetProperty/output/kAudioUnitProperty_SetRenderCallback", r); + LOG("AudioUnitSetProperty/output/kAudioUnitProperty_SetRenderCallback rv=%d", r); return CUBEB_ERROR; } + stm->frames_written = 0; + LOG("(%p) Output audiounit init successfully.", stm); return CUBEB_OK; } @@ -1646,11 +2542,37 @@ audiounit_setup_stream(cubeb_stream * stm) { stm->mutex.assert_current_thread_owns(); + if ((stm->input_stream_params.prefs & CUBEB_STREAM_PREF_LOOPBACK) || + (stm->output_stream_params.prefs & CUBEB_STREAM_PREF_LOOPBACK)) { + LOG("(%p) Loopback not supported for audiounit.", stm); + return CUBEB_ERROR_NOT_SUPPORTED; + } + int r = 0; + + device_info in_dev_info = stm->input_device; + device_info out_dev_info = stm->output_device; + + if (has_input(stm) && has_output(stm) && + stm->input_device.id != stm->output_device.id) { + r = audiounit_create_aggregate_device(stm); + if (r != CUBEB_OK) { + stm->aggregate_device_id = kAudioObjectUnknown; + LOG("(%p) Create aggregate devices failed.", stm); + // !!!NOTE: It is not necessary to return here. If it does not + // return it will fallback to the old implementation. The intention + // is to investigate how often it fails. I plan to remove + // it after a couple of weeks. + return r; + } else { + in_dev_info.id = out_dev_info.id = stm->aggregate_device_id; + in_dev_info.flags = DEV_INPUT; + out_dev_info.flags = DEV_OUTPUT; + } + } + if (has_input(stm)) { - r = audiounit_create_unit(&stm->input_unit, true, - &stm->input_stream_params, - stm->input_device); + r = audiounit_create_unit(&stm->input_unit, &in_dev_info); if (r != CUBEB_OK) { LOG("(%p) AudioUnit creation for input failed.", stm); return r; @@ -1658,9 +2580,7 @@ audiounit_setup_stream(cubeb_stream * stm) } if (has_output(stm)) { - r = audiounit_create_unit(&stm->output_unit, false, - &stm->output_stream_params, - stm->output_device); + r = audiounit_create_unit(&stm->output_unit, &out_dev_info); if (r != CUBEB_OK) { LOG("(%p) AudioUnit creation for output failed.", stm); return r; @@ -1668,20 +2588,20 @@ audiounit_setup_stream(cubeb_stream * stm) } /* Latency cannot change if another stream is operating in parallel. In this case - * latecy is set to the other stream value. */ - if (stm->context->active_streams > 1) { + * latency is set to the other stream value. */ + if (audiounit_active_streams(stm->context) > 1) { LOG("(%p) More than one active stream, use global latency.", stm); stm->latency_frames = stm->context->global_latency_frames; } else { /* Silently clamp the latency down to the platform default, because we - * synthetize the clock from the callbacks, and we want the clock to update - * often. */ + * synthetize the clock from the callbacks, and we want the clock to update + * often. */ stm->latency_frames = audiounit_clamp_latency(stm, stm->latency_frames); - assert(stm->latency_frames); // Ungly error check - audiounit_set_global_latency(stm, stm->latency_frames); + assert(stm->latency_frames); // Ugly error check + audiounit_set_global_latency(stm->context, stm->latency_frames); } - /* Setup Input Stream! */ + /* Configure I/O stream */ if (has_input(stm)) { r = audiounit_configure_input(stm); if (r != CUBEB_OK) { @@ -1690,7 +2610,6 @@ audiounit_setup_stream(cubeb_stream * stm) } } - /* Setup Output Stream! */ if (has_output(stm)) { r = audiounit_configure_output(stm); if (r != CUBEB_OK) { @@ -1762,13 +2681,13 @@ audiounit_setup_stream(cubeb_stream * stm) /* Create resampler. Output params are unchanged * because we do not need conversion on the output. */ - stm->resampler = cubeb_resampler_create(stm, - has_input(stm) ? &input_unconverted_params : NULL, - has_output(stm) ? &stm->output_stream_params : NULL, - target_sample_rate, - stm->data_callback, - stm->user_ptr, - CUBEB_RESAMPLER_QUALITY_DESKTOP); + stm->resampler.reset(cubeb_resampler_create(stm, + has_input(stm) ? &input_unconverted_params : NULL, + has_output(stm) ? &stm->output_stream_params : NULL, + target_sample_rate, + stm->data_callback, + stm->user_ptr, + CUBEB_RESAMPLER_QUALITY_DESKTOP)); if (!stm->resampler) { LOG("(%p) Could not create resampler.", stm); return CUBEB_ERROR; @@ -1777,7 +2696,7 @@ audiounit_setup_stream(cubeb_stream * stm) if (stm->input_unit != NULL) { r = AudioUnitInitialize(stm->input_unit); if (r != noErr) { - PRINT_ERROR_CODE("AudioUnitInitialize/input", r); + LOG("AudioUnitInitialize/input rv=%d", r); return CUBEB_ERROR; } } @@ -1785,9 +2704,17 @@ audiounit_setup_stream(cubeb_stream * stm) if (stm->output_unit != NULL) { r = AudioUnitInitialize(stm->output_unit); if (r != noErr) { - PRINT_ERROR_CODE("AudioUnitInitialize/output", r); + LOG("AudioUnitInitialize/output rv=%d", r); return CUBEB_ERROR; } + + stm->current_latency_frames = audiounit_get_device_presentation_latency(stm->output_device.id, kAudioDevicePropertyScopeOutput); + + Float64 unit_s; + UInt32 size = sizeof(unit_s); + if (AudioUnitGetProperty(stm->output_unit, kAudioUnitProperty_Latency, kAudioUnitScope_Global, 0, &unit_s, &size) == noErr) { + stm->current_latency_frames += static_cast<uint32_t>(unit_s * stm->output_desc.mSampleRate); + } } if (stm->input_unit && stm->output_unit) { @@ -1799,13 +2726,24 @@ audiounit_setup_stream(cubeb_stream * stm) r = audiounit_install_device_changed_callback(stm); if (r != CUBEB_OK) { - LOG("(%p) Could not install the device change callback.", stm); - return r; + LOG("(%p) Could not install all device change callback.", stm); } + return CUBEB_OK; } +cubeb_stream::cubeb_stream(cubeb * context) + : context(context) + , resampler(nullptr, cubeb_resampler_destroy) + , mixer(nullptr, cubeb_mixer_destroy) +{ + PodZero(&input_desc, 1); + PodZero(&output_desc, 1); +} + +static void audiounit_stream_destroy_internal(cubeb_stream * stm); + static int audiounit_stream_init(cubeb * context, cubeb_stream ** stream, @@ -1819,72 +2757,64 @@ audiounit_stream_init(cubeb * context, cubeb_state_callback state_callback, void * user_ptr) { - cubeb_stream * stm; - int r; - assert(context); + auto_lock context_lock(context->mutex); + audiounit_increment_active_streams(context); + unique_ptr<cubeb_stream, decltype(&audiounit_stream_destroy)> stm(new cubeb_stream(context), + audiounit_stream_destroy_internal); + int r; *stream = NULL; - assert(latency_frames > 0); - if (context->limit_streams && context->active_streams >= CUBEB_STREAM_MAX) { - LOG("Reached the stream limit of %d", CUBEB_STREAM_MAX); - return CUBEB_ERROR; - } - - stm = (cubeb_stream *) calloc(1, sizeof(cubeb_stream)); - assert(stm); - // Placement new to call the ctors of cubeb_stream members. - new (stm) cubeb_stream(); /* These could be different in the future if we have both * full-duplex stream and different devices for input vs output. */ - stm->context = context; stm->data_callback = data_callback; stm->state_callback = state_callback; stm->user_ptr = user_ptr; stm->latency_frames = latency_frames; - stm->device_changed_callback = NULL; + + if ((input_device && !input_stream_params) || + (output_device && !output_stream_params)) { + return CUBEB_ERROR_INVALID_PARAMETER; + } if (input_stream_params) { stm->input_stream_params = *input_stream_params; - stm->input_device = input_device; - stm->is_default_input = input_device == nullptr || - (audiounit_get_default_device_id(CUBEB_DEVICE_TYPE_INPUT) == - reinterpret_cast<intptr_t>(input_device)); + r = audiounit_set_device_info(stm.get(), reinterpret_cast<uintptr_t>(input_device), io_side::INPUT); + if (r != CUBEB_OK) { + LOG("(%p) Fail to set device info for input.", stm.get()); + return r; + } } if (output_stream_params) { stm->output_stream_params = *output_stream_params; - stm->output_device = output_device; + r = audiounit_set_device_info(stm.get(), reinterpret_cast<uintptr_t>(output_device), io_side::OUTPUT); + if (r != CUBEB_OK) { + LOG("(%p) Fail to set device info for output.", stm.get()); + return r; + } } - /* Init data members where necessary */ - stm->hw_latency_frames = UINT64_MAX; - - stm->switching_device = false; - - auto_lock context_lock(context->mutex); { // It's not critical to lock here, because no other thread has been started // yet, but it allows to assert that the lock has been taken in // `audiounit_setup_stream`. - context->active_streams += 1; auto_lock lock(stm->mutex); - r = audiounit_setup_stream(stm); + r = audiounit_setup_stream(stm.get()); } if (r != CUBEB_OK) { - LOG("(%p) Could not setup the audiounit stream.", stm); - audiounit_stream_destroy(stm); + LOG("(%p) Could not setup the audiounit stream.", stm.get()); return r; } - r = audiounit_install_system_changed_callback(stm); + r = audiounit_install_system_changed_callback(stm.get()); if (r != CUBEB_OK) { - LOG("(%p) Could not install the device change callback.", stm); + LOG("(%p) Could not install the device change callback.", stm.get()); return r; } - *stream = stm; - LOG("Cubeb stream (%p) init successful.", stm); + *stream = stm.release(); + LOG("(%p) Cubeb stream init successful.", *stream); return CUBEB_OK; } @@ -1896,64 +2826,86 @@ audiounit_close_stream(cubeb_stream *stm) if (stm->input_unit) { AudioUnitUninitialize(stm->input_unit); AudioComponentInstanceDispose(stm->input_unit); + stm->input_unit = nullptr; } - audiounit_destroy_input_linear_buffer(stm); + stm->input_linear_buffer.reset(); if (stm->output_unit) { AudioUnitUninitialize(stm->output_unit); AudioComponentInstanceDispose(stm->output_unit); + stm->output_unit = nullptr; } - cubeb_resampler_destroy(stm->resampler); + stm->resampler.reset(); + stm->mixer.reset(); + + if (stm->aggregate_device_id != kAudioObjectUnknown) { + audiounit_destroy_aggregate_device(stm->plugin_id, &stm->aggregate_device_id); + stm->aggregate_device_id = kAudioObjectUnknown; + } } static void -audiounit_stream_destroy(cubeb_stream * stm) +audiounit_stream_destroy_internal(cubeb_stream *stm) { - stm->shutdown = true; + stm->context->mutex.assert_current_thread_owns(); int r = audiounit_uninstall_system_changed_callback(stm); if (r != CUBEB_OK) { LOG("(%p) Could not uninstall the device changed callback", stm); } - r = audiounit_uninstall_device_changed_callback(stm); if (r != CUBEB_OK) { - LOG("(%p) Could not uninstall the device changed callback", stm); + LOG("(%p) Could not uninstall all device change listeners", stm); } - auto_lock context_lock(stm->context->mutex); - audiounit_stream_stop_internal(stm); + auto_lock lock(stm->mutex); + audiounit_close_stream(stm); + assert(audiounit_active_streams(stm->context) >= 1); + audiounit_decrement_active_streams(stm->context); +} +static void +audiounit_stream_destroy(cubeb_stream * stm) +{ + if (!stm->shutdown.load()){ + auto_lock context_lock(stm->context->mutex); + audiounit_stream_stop_internal(stm); + stm->shutdown = true; + } + + stm->destroy_pending = true; // Execute close in serial queue to avoid collision // with reinit when un/plug devices dispatch_sync(stm->context->serial_queue, ^() { - auto_lock lock(stm->mutex); - audiounit_close_stream(stm); + auto_lock context_lock(stm->context->mutex); + audiounit_stream_destroy_internal(stm); }); - assert(stm->context->active_streams >= 1); - stm->context->active_streams -= 1; - LOG("Cubeb stream (%p) destroyed successful.", stm); - - stm->~cubeb_stream(); - free(stm); + delete stm; } -void +static int audiounit_stream_start_internal(cubeb_stream * stm) { OSStatus r; if (stm->input_unit != NULL) { r = AudioOutputUnitStart(stm->input_unit); - assert(r == 0); + if (r != noErr) { + LOG("AudioOutputUnitStart (input) rv=%d", r); + return CUBEB_ERROR; + } } if (stm->output_unit != NULL) { r = AudioOutputUnitStart(stm->output_unit); - assert(r == 0); + if (r != noErr) { + LOG("AudioOutputUnitStart (output) rv=%d", r); + return CUBEB_ERROR; + } } + return CUBEB_OK; } static int @@ -1963,7 +2915,10 @@ audiounit_stream_start(cubeb_stream * stm) stm->shutdown = false; stm->draining = false; - audiounit_stream_start_internal(stm); + int r = audiounit_stream_start_internal(stm); + if (r != CUBEB_OK) { + return r; + } stm->state_callback(stm, stm->user_ptr, CUBEB_STATE_STARTED); @@ -2002,9 +2957,12 @@ audiounit_stream_stop(cubeb_stream * stm) static int audiounit_stream_get_position(cubeb_stream * stm, uint64_t * position) { - auto_lock lock(stm->mutex); - - *position = stm->frames_played; + assert(stm); + if (stm->current_latency_frames > stm->frames_played) { + *position = 0; + } else { + *position = stm->frames_played - stm->current_latency_frames; + } return CUBEB_OK; } @@ -2015,74 +2973,7 @@ audiounit_stream_get_latency(cubeb_stream * stm, uint32_t * latency) //TODO return CUBEB_ERROR_NOT_SUPPORTED; #else - auto_lock lock(stm->mutex); - if (stm->hw_latency_frames == UINT64_MAX) { - UInt32 size; - uint32_t device_latency_frames, device_safety_offset; - double unit_latency_sec; - AudioDeviceID output_device_id; - OSStatus r; - AudioObjectPropertyAddress latency_address = { - kAudioDevicePropertyLatency, - kAudioDevicePropertyScopeOutput, - kAudioObjectPropertyElementMaster - }; - AudioObjectPropertyAddress safety_offset_address = { - kAudioDevicePropertySafetyOffset, - kAudioDevicePropertyScopeOutput, - kAudioObjectPropertyElementMaster - }; - - r = audiounit_get_output_device_id(&output_device_id); - if (r != noErr) { - return CUBEB_ERROR; - } - - size = sizeof(unit_latency_sec); - r = AudioUnitGetProperty(stm->output_unit, - kAudioUnitProperty_Latency, - kAudioUnitScope_Global, - 0, - &unit_latency_sec, - &size); - if (r != noErr) { - PRINT_ERROR_CODE("AudioUnitGetProperty/kAudioUnitProperty_Latency", r); - return CUBEB_ERROR; - } - - size = sizeof(device_latency_frames); - r = AudioObjectGetPropertyData(output_device_id, - &latency_address, - 0, - NULL, - &size, - &device_latency_frames); - if (r != noErr) { - PRINT_ERROR_CODE("AudioUnitGetPropertyData/latency_frames", r); - return CUBEB_ERROR; - } - - size = sizeof(device_safety_offset); - r = AudioObjectGetPropertyData(output_device_id, - &safety_offset_address, - 0, - NULL, - &size, - &device_safety_offset); - if (r != noErr) { - PRINT_ERROR_CODE("AudioUnitGetPropertyData/safety_offset", r); - return CUBEB_ERROR; - } - - /* This part is fixed and depend on the stream parameter and the hardware. */ - stm->hw_latency_frames = - static_cast<uint32_t>(unit_latency_sec * stm->output_desc.mSampleRate) - + device_latency_frames - + device_safety_offset; - } - - *latency = stm->hw_latency_frames + stm->current_latency_frames; - + *latency = stm->total_output_latency_frames; return CUBEB_OK; #endif } @@ -2102,119 +2993,110 @@ audiounit_stream_get_volume(cubeb_stream * stm, float * volume) return CUBEB_OK; } -int audiounit_stream_set_volume(cubeb_stream * stm, float volume) +static int +audiounit_stream_set_volume(cubeb_stream * stm, float volume) { + assert(stm->output_unit); OSStatus r; - r = AudioUnitSetParameter(stm->output_unit, kHALOutputParam_Volume, kAudioUnitScope_Global, 0, volume, 0); if (r != noErr) { - PRINT_ERROR_CODE("AudioUnitSetParameter/kHALOutputParam_Volume", r); + LOG("AudioUnitSetParameter/kHALOutputParam_Volume rv=%d", r); return CUBEB_ERROR; } return CUBEB_OK; } -int audiounit_stream_set_panning(cubeb_stream * stm, float panning) +unique_ptr<char[]> convert_uint32_into_string(UInt32 data) { - if (stm->output_desc.mChannelsPerFrame > 2) { - return CUBEB_ERROR_INVALID_PARAMETER; + // Simply create an empty string if no data. + size_t size = data == 0 ? 0 : 4; // 4 bytes for uint32. + auto str = unique_ptr<char[]> { new char[size + 1] }; // + 1 for '\0'. + str[size] = '\0'; + if (size < 4) { + return str; } - stm->panning.store(panning, std::memory_order_relaxed); - return CUBEB_OK; + // Reverse 0xWXYZ into 0xZYXW. + str[0] = (char)(data >> 24); + str[1] = (char)(data >> 16); + str[2] = (char)(data >> 8); + str[3] = (char)(data); + return str; } -int audiounit_stream_get_current_device(cubeb_stream * stm, - cubeb_device ** const device) +int audiounit_get_default_device_datasource(cubeb_device_type type, + UInt32 * data) { -#if TARGET_OS_IPHONE - //TODO - return CUBEB_ERROR_NOT_SUPPORTED; -#else - OSStatus r; - UInt32 size; - UInt32 data; - char strdata[4]; - AudioDeviceID output_device_id; - AudioDeviceID input_device_id; - - AudioObjectPropertyAddress datasource_address = { - kAudioDevicePropertyDataSource, - kAudioDevicePropertyScopeOutput, - kAudioObjectPropertyElementMaster - }; - - AudioObjectPropertyAddress datasource_address_input = { - kAudioDevicePropertyDataSource, - kAudioDevicePropertyScopeInput, - kAudioObjectPropertyElementMaster - }; - - *device = NULL; - - if (audiounit_get_output_device_id(&output_device_id) != CUBEB_OK) { + AudioDeviceID id = audiounit_get_default_device_id(type); + if (id == kAudioObjectUnknown) { return CUBEB_ERROR; } - *device = new cubeb_device; - if (!*device) { - return CUBEB_ERROR; - } - PodZero(*device, 1); - - size = sizeof(UInt32); - /* This fails with some USB headset, so simply return an empty string. */ - r = AudioObjectGetPropertyData(output_device_id, - &datasource_address, - 0, NULL, &size, &data); + UInt32 size = sizeof(*data); + /* This fails with some USB headsets (e.g., Plantronic .Audio 628). */ + OSStatus r = AudioObjectGetPropertyData(id, + type == CUBEB_DEVICE_TYPE_INPUT ? + &INPUT_DATA_SOURCE_PROPERTY_ADDRESS : + &OUTPUT_DATA_SOURCE_PROPERTY_ADDRESS, + 0, NULL, &size, data); if (r != noErr) { - size = 0; - data = 0; - } - - (*device)->output_name = new char[size + 1]; - if (!(*device)->output_name) { - return CUBEB_ERROR; + *data = 0; } - // Turn the four chars packed into a uint32 into a string - strdata[0] = (char)(data >> 24); - strdata[1] = (char)(data >> 16); - strdata[2] = (char)(data >> 8); - strdata[3] = (char)(data); + return CUBEB_OK; +} - memcpy((*device)->output_name, strdata, size); - (*device)->output_name[size] = '\0'; +int audiounit_get_default_device_name(cubeb_stream * stm, + cubeb_device * const device, + cubeb_device_type type) +{ + assert(stm); + assert(device); - if (audiounit_get_input_device_id(&input_device_id) != CUBEB_OK) { - return CUBEB_ERROR; + UInt32 data; + int r = audiounit_get_default_device_datasource(type, &data); + if (r != CUBEB_OK) { + return r; } - - size = sizeof(UInt32); - r = AudioObjectGetPropertyData(input_device_id, &datasource_address_input, 0, NULL, &size, &data); - if (r != noErr) { - LOG("(%p) Error when getting device !", stm); - size = 0; - data = 0; + char ** name = type == CUBEB_DEVICE_TYPE_INPUT ? + &device->input_name : &device->output_name; + *name = convert_uint32_into_string(data).release(); + if (!strlen(*name)) { // empty string. + LOG("(%p) name of %s device is empty!", stm, + type == CUBEB_DEVICE_TYPE_INPUT ? "input" : "output"); } + return CUBEB_OK; +} - (*device)->input_name = new char[size + 1]; - if (!(*device)->input_name) { + +int audiounit_stream_get_current_device(cubeb_stream * stm, + cubeb_device ** const device) +{ +#if TARGET_OS_IPHONE + //TODO + return CUBEB_ERROR_NOT_SUPPORTED; +#else + *device = new cubeb_device; + if (!*device) { return CUBEB_ERROR; } + PodZero(*device, 1); - // Turn the four chars packed into a uint32 into a string - strdata[0] = (char)(data >> 24); - strdata[1] = (char)(data >> 16); - strdata[2] = (char)(data >> 8); - strdata[3] = (char)(data); + int r = audiounit_get_default_device_name(stm, *device, + CUBEB_DEVICE_TYPE_OUTPUT); + if (r != CUBEB_OK) { + return r; + } - memcpy((*device)->input_name, strdata, size); - (*device)->input_name[size] = '\0'; + r = audiounit_get_default_device_name(stm, *device, + CUBEB_DEVICE_TYPE_INPUT); + if (r != CUBEB_OK) { + return r; + } return CUBEB_OK; #endif @@ -2232,52 +3114,14 @@ int audiounit_stream_device_destroy(cubeb_stream * /* stream */, int audiounit_stream_register_device_changed_callback(cubeb_stream * stream, cubeb_device_changed_callback device_changed_callback) { + auto_lock dev_cb_lock(stream->device_changed_callback_lock); /* Note: second register without unregister first causes 'nope' error. * Current implementation requires unregister before register a new cb. */ - assert(!stream->device_changed_callback); - - auto_lock lock(stream->mutex); - + assert(!device_changed_callback || !stream->device_changed_callback); stream->device_changed_callback = device_changed_callback; - return CUBEB_OK; } -static OSStatus -audiounit_get_devices(AudioObjectID ** devices, uint32_t * count) -{ - OSStatus ret; - UInt32 size = 0; - AudioObjectPropertyAddress adr = { kAudioHardwarePropertyDevices, - kAudioObjectPropertyScopeGlobal, - kAudioObjectPropertyElementMaster }; - - ret = AudioObjectGetPropertyDataSize(kAudioObjectSystemObject, &adr, 0, NULL, &size); - if (ret != noErr) { - return ret; - } - - *count = static_cast<uint32_t>(size / sizeof(AudioObjectID)); - if (size >= sizeof(AudioObjectID)) { - if (*devices != NULL) { - delete [] (*devices); - } - *devices = new AudioObjectID[*count]; - PodZero(*devices, *count); - - ret = AudioObjectGetPropertyData(kAudioObjectSystemObject, &adr, 0, NULL, &size, (void *)*devices); - if (ret != noErr) { - delete [] (*devices); - *devices = NULL; - } - } else { - *devices = NULL; - ret = -1; - } - - return ret; -} - static char * audiounit_strref_to_cstr_utf8(CFStringRef strref) { @@ -2288,11 +3132,12 @@ audiounit_strref_to_cstr_utf8(CFStringRef strref) } len = CFStringGetLength(strref); - size = CFStringGetMaximumSizeForEncoding(len, kCFStringEncodingUTF8); - ret = static_cast<char *>(malloc(size)); + // Add 1 to size to allow for '\0' termination character. + size = CFStringGetMaximumSizeForEncoding(len, kCFStringEncodingUTF8) + 1; + ret = new char[size]; if (!CFStringGetCString(strref, ret, size, kCFStringEncodingUTF8)) { - free(ret); + delete [] ret; ret = NULL; } @@ -2339,19 +3184,18 @@ audiounit_get_available_samplerate(AudioObjectID devid, AudioObjectPropertyScope AudioValueRange range; if (AudioObjectHasProperty(devid, &adr) && AudioObjectGetPropertyDataSize(devid, &adr, 0, NULL, &size) == noErr) { - uint32_t i, count = size / sizeof(AudioValueRange); - AudioValueRange * ranges = new AudioValueRange[count]; + uint32_t count = size / sizeof(AudioValueRange); + vector<AudioValueRange> ranges(count); range.mMinimum = 9999999999.0; range.mMaximum = 0.0; - if (AudioObjectGetPropertyData(devid, &adr, 0, NULL, &size, ranges) == noErr) { - for (i = 0; i < count; i++) { + if (AudioObjectGetPropertyData(devid, &adr, 0, NULL, &size, ranges.data()) == noErr) { + for (uint32_t i = 0; i < count; i++) { if (ranges[i].mMaximum > range.mMaximum) range.mMaximum = ranges[i].mMaximum; if (ranges[i].mMinimum < range.mMinimum) range.mMinimum = ranges[i].mMinimum; } } - delete [] ranges; *max = static_cast<uint32_t>(range.mMaximum); *min = static_cast<uint32_t>(range.mMinimum); } else { @@ -2364,7 +3208,7 @@ static UInt32 audiounit_get_device_presentation_latency(AudioObjectID devid, AudioObjectPropertyScope scope) { AudioObjectPropertyAddress adr = { 0, scope, kAudioObjectPropertyElementMaster }; - UInt32 size, dev, stream = 0, offset; + UInt32 size, dev, stream = 0; AudioStreamID sid[1]; adr.mSelector = kAudioDevicePropertyLatency; @@ -2381,216 +3225,249 @@ audiounit_get_device_presentation_latency(AudioObjectID devid, AudioObjectProper AudioObjectGetPropertyData(sid[0], &adr, 0, NULL, &size, &stream); } - adr.mSelector = kAudioDevicePropertySafetyOffset; - size = sizeof(UInt32); - if (AudioObjectGetPropertyData(devid, &adr, 0, NULL, &size, &offset) != noErr) { - offset = 0; - } - - return dev + stream + offset; + return dev + stream; } -static cubeb_device_info * -audiounit_create_device_from_hwdev(AudioObjectID devid, cubeb_device_type type) +static int +audiounit_create_device_from_hwdev(cubeb_device_info * dev_info, AudioObjectID devid, cubeb_device_type type) { AudioObjectPropertyAddress adr = { 0, 0, kAudioObjectPropertyElementMaster }; - UInt32 size, ch, latency; - cubeb_device_info * ret; - CFStringRef str = NULL; - AudioValueRange range; + UInt32 size; if (type == CUBEB_DEVICE_TYPE_OUTPUT) { adr.mScope = kAudioDevicePropertyScopeOutput; } else if (type == CUBEB_DEVICE_TYPE_INPUT) { adr.mScope = kAudioDevicePropertyScopeInput; } else { - return NULL; + return CUBEB_ERROR; } - ch = audiounit_get_channel_count(devid, adr.mScope); + UInt32 ch = audiounit_get_channel_count(devid, adr.mScope); if (ch == 0) { - return NULL; + return CUBEB_ERROR; } - ret = new cubeb_device_info; - PodZero(ret, 1); + PodZero(dev_info, 1); + CFStringRef device_id_str = nullptr; size = sizeof(CFStringRef); adr.mSelector = kAudioDevicePropertyDeviceUID; - if (AudioObjectGetPropertyData(devid, &adr, 0, NULL, &size, &str) == noErr && str != NULL) { - ret->device_id = audiounit_strref_to_cstr_utf8(str); - ret->devid = (cubeb_devid)(size_t)devid; - ret->group_id = strdup(ret->device_id); - CFRelease(str); + OSStatus ret = AudioObjectGetPropertyData(devid, &adr, 0, NULL, &size, &device_id_str); + if ( ret == noErr && device_id_str != NULL) { + dev_info->device_id = audiounit_strref_to_cstr_utf8(device_id_str); + static_assert(sizeof(cubeb_devid) >= sizeof(decltype(devid)), "cubeb_devid can't represent devid"); + dev_info->devid = reinterpret_cast<cubeb_devid>(devid); + dev_info->group_id = dev_info->device_id; + CFRelease(device_id_str); } - size = sizeof(CFStringRef); - adr.mSelector = kAudioObjectPropertyName; - if (AudioObjectGetPropertyData(devid, &adr, 0, NULL, &size, &str) == noErr && str != NULL) { - UInt32 ds; - size = sizeof(UInt32); - adr.mSelector = kAudioDevicePropertyDataSource; - if (AudioObjectGetPropertyData(devid, &adr, 0, NULL, &size, &ds) == noErr) { - CFStringRef dsname; - AudioValueTranslation trl = { &ds, sizeof(ds), &dsname, sizeof(dsname) }; - adr.mSelector = kAudioDevicePropertyDataSourceNameForIDCFString; - size = sizeof(AudioValueTranslation); - // If there is a datasource for this device, use it instead of the device - // name. - if (AudioObjectGetPropertyData(devid, &adr, 0, NULL, &size, &trl) == noErr) { - CFRelease(str); - str = dsname; - } - } + CFStringRef friendly_name_str = nullptr; + UInt32 ds; + size = sizeof(UInt32); + adr.mSelector = kAudioDevicePropertyDataSource; + ret = AudioObjectGetPropertyData(devid, &adr, 0, NULL, &size, &ds); + if (ret == noErr) { + AudioValueTranslation trl = { &ds, sizeof(ds), &friendly_name_str, sizeof(CFStringRef) }; + adr.mSelector = kAudioDevicePropertyDataSourceNameForIDCFString; + size = sizeof(AudioValueTranslation); + AudioObjectGetPropertyData(devid, &adr, 0, NULL, &size, &trl); + } - ret->friendly_name = audiounit_strref_to_cstr_utf8(str); - CFRelease(str); + // If there is no datasource for this device, fall back to the + // device name. + if (!friendly_name_str) { + size = sizeof(CFStringRef); + adr.mSelector = kAudioObjectPropertyName; + AudioObjectGetPropertyData(devid, &adr, 0, NULL, &size, &friendly_name_str); } + if (friendly_name_str) { + dev_info->friendly_name = audiounit_strref_to_cstr_utf8(friendly_name_str); + CFRelease(friendly_name_str); + } else { + // Couldn't get a datasource name nor a device name, return a + // valid string of length 0. + char * fallback_name = new char[1]; + fallback_name[0] = '\0'; + dev_info->friendly_name = fallback_name; + } + + CFStringRef vendor_name_str = nullptr; size = sizeof(CFStringRef); adr.mSelector = kAudioObjectPropertyManufacturer; - if (AudioObjectGetPropertyData(devid, &adr, 0, NULL, &size, &str) == noErr && str != NULL) { - ret->vendor_name = audiounit_strref_to_cstr_utf8(str); - CFRelease(str); + ret = AudioObjectGetPropertyData(devid, &adr, 0, NULL, &size, &vendor_name_str); + if (ret == noErr && vendor_name_str != NULL) { + dev_info->vendor_name = audiounit_strref_to_cstr_utf8(vendor_name_str); + CFRelease(vendor_name_str); } - ret->type = type; - ret->state = CUBEB_DEVICE_STATE_ENABLED; - ret->preferred = (devid == audiounit_get_default_device_id(type)) ? + dev_info->type = type; + dev_info->state = CUBEB_DEVICE_STATE_ENABLED; + dev_info->preferred = (devid == audiounit_get_default_device_id(type)) ? CUBEB_DEVICE_PREF_ALL : CUBEB_DEVICE_PREF_NONE; - ret->max_channels = ch; - ret->format = (cubeb_device_fmt)CUBEB_DEVICE_FMT_ALL; /* CoreAudio supports All! */ + dev_info->max_channels = ch; + dev_info->format = (cubeb_device_fmt)CUBEB_DEVICE_FMT_ALL; /* CoreAudio supports All! */ /* kAudioFormatFlagsAudioUnitCanonical is deprecated, prefer floating point */ - ret->default_format = CUBEB_DEVICE_FMT_F32NE; + dev_info->default_format = CUBEB_DEVICE_FMT_F32NE; audiounit_get_available_samplerate(devid, adr.mScope, - &ret->min_rate, &ret->max_rate, &ret->default_rate); + &dev_info->min_rate, &dev_info->max_rate, &dev_info->default_rate); - latency = audiounit_get_device_presentation_latency(devid, adr.mScope); + UInt32 latency = audiounit_get_device_presentation_latency(devid, adr.mScope); + AudioValueRange range; adr.mSelector = kAudioDevicePropertyBufferFrameSizeRange; size = sizeof(AudioValueRange); - if (AudioObjectGetPropertyData(devid, &adr, 0, NULL, &size, &range) == noErr) { - ret->latency_lo = latency + range.mMinimum; - ret->latency_hi = latency + range.mMaximum; + ret = AudioObjectGetPropertyData(devid, &adr, 0, NULL, &size, &range); + if (ret == noErr) { + dev_info->latency_lo = latency + range.mMinimum; + dev_info->latency_hi = latency + range.mMaximum; } else { - ret->latency_lo = 10 * ret->default_rate / 1000; /* Default to 10ms */ - ret->latency_hi = 100 * ret->default_rate / 1000; /* Default to 100ms */ + dev_info->latency_lo = 10 * dev_info->default_rate / 1000; /* Default to 10ms */ + dev_info->latency_hi = 100 * dev_info->default_rate / 1000; /* Default to 100ms */ } - return ret; + return CUBEB_OK; +} + +bool +is_aggregate_device(cubeb_device_info * device_info) +{ + assert(device_info->friendly_name); + return !strncmp(device_info->friendly_name, PRIVATE_AGGREGATE_DEVICE_NAME, + strlen(PRIVATE_AGGREGATE_DEVICE_NAME)); } static int audiounit_enumerate_devices(cubeb * /* context */, cubeb_device_type type, - cubeb_device_collection ** collection) + cubeb_device_collection * collection) { - AudioObjectID * hwdevs = NULL; - uint32_t i, hwdevcount = 0; - OSStatus err; + vector<AudioObjectID> input_devs; + vector<AudioObjectID> output_devs; - if ((err = audiounit_get_devices(&hwdevs, &hwdevcount)) != noErr) { - return CUBEB_ERROR; + // Count number of input and output devices. This is not + // necessarily the same as the count of raw devices supported by the + // system since, for example, with Soundflower installed, some + // devices may report as being both input *and* output and cubeb + // separates those into two different devices. + + if (type & CUBEB_DEVICE_TYPE_OUTPUT) { + output_devs = audiounit_get_devices_of_type(CUBEB_DEVICE_TYPE_OUTPUT); } - *collection = static_cast<cubeb_device_collection *>(malloc(sizeof(cubeb_device_collection) + - sizeof(cubeb_device_info*) * (hwdevcount > 0 ? hwdevcount - 1 : 0))); - (*collection)->count = 0; + if (type & CUBEB_DEVICE_TYPE_INPUT) { + input_devs = audiounit_get_devices_of_type(CUBEB_DEVICE_TYPE_INPUT); + } - if (hwdevcount > 0) { - cubeb_device_info * cur; + auto devices = new cubeb_device_info[output_devs.size() + input_devs.size()]; + collection->count = 0; - if (type & CUBEB_DEVICE_TYPE_OUTPUT) { - for (i = 0; i < hwdevcount; i++) { - if ((cur = audiounit_create_device_from_hwdev(hwdevs[i], CUBEB_DEVICE_TYPE_OUTPUT)) != NULL) - (*collection)->device[(*collection)->count++] = cur; + if (type & CUBEB_DEVICE_TYPE_OUTPUT) { + for (auto dev: output_devs) { + auto device = &devices[collection->count]; + auto err = audiounit_create_device_from_hwdev(device, dev, CUBEB_DEVICE_TYPE_OUTPUT); + if (err != CUBEB_OK || is_aggregate_device(device)) { + continue; } + collection->count += 1; } + } - if (type & CUBEB_DEVICE_TYPE_INPUT) { - for (i = 0; i < hwdevcount; i++) { - if ((cur = audiounit_create_device_from_hwdev(hwdevs[i], CUBEB_DEVICE_TYPE_INPUT)) != NULL) - (*collection)->device[(*collection)->count++] = cur; + if (type & CUBEB_DEVICE_TYPE_INPUT) { + for (auto dev: input_devs) { + auto device = &devices[collection->count]; + auto err = audiounit_create_device_from_hwdev(device, dev, CUBEB_DEVICE_TYPE_INPUT); + if (err != CUBEB_OK || is_aggregate_device(device)) { + continue; } + collection->count += 1; } } - delete [] hwdevs; + if (collection->count > 0) { + collection->device = devices; + } else { + delete [] devices; + collection->device = NULL; + } return CUBEB_OK; } -/* qsort compare method. */ -int compare_devid(const void * a, const void * b) +static void +audiounit_device_destroy(cubeb_device_info * device) { - return (*(AudioObjectID*)a - *(AudioObjectID*)b); + delete [] device->device_id; + delete [] device->friendly_name; + delete [] device->vendor_name; } -static uint32_t -audiounit_get_devices_of_type(cubeb_device_type devtype, AudioObjectID ** devid_array) +static int +audiounit_device_collection_destroy(cubeb * /* context */, + cubeb_device_collection * collection) { - assert(devid_array == NULL || *devid_array == NULL); + for (size_t i = 0; i < collection->count; i++) { + audiounit_device_destroy(&collection->device[i]); + } + delete [] collection->device; - AudioObjectPropertyAddress adr = { kAudioHardwarePropertyDevices, - kAudioObjectPropertyScopeGlobal, - kAudioObjectPropertyElementMaster }; + return CUBEB_OK; +} + +static vector<AudioObjectID> +audiounit_get_devices_of_type(cubeb_device_type devtype) +{ UInt32 size = 0; - OSStatus ret = AudioObjectGetPropertyDataSize(kAudioObjectSystemObject, &adr, 0, NULL, &size); + OSStatus ret = AudioObjectGetPropertyDataSize(kAudioObjectSystemObject, + &DEVICES_PROPERTY_ADDRESS, 0, + NULL, &size); if (ret != noErr) { - return 0; + return vector<AudioObjectID>(); } - /* Total number of input and output devices. */ - uint32_t count = (uint32_t)(size / sizeof(AudioObjectID)); - - AudioObjectID devices[count]; - ret = AudioObjectGetPropertyData(kAudioObjectSystemObject, &adr, 0, NULL, &size, &devices); + vector<AudioObjectID> devices(size / sizeof(AudioObjectID)); + ret = AudioObjectGetPropertyData(kAudioObjectSystemObject, + &DEVICES_PROPERTY_ADDRESS, 0, NULL, &size, + devices.data()); if (ret != noErr) { - return 0; + return vector<AudioObjectID>(); } + + // Remove the aggregate device from the list of devices (if any). + for (auto it = devices.begin(); it != devices.end();) { + CFStringRef name = get_device_name(*it); + if (name && CFStringFind(name, CFSTR("CubebAggregateDevice"), 0).location != + kCFNotFound) { + it = devices.erase(it); + } else { + it++; + } + if (name) { + CFRelease(name); + } + } + /* Expected sorted but did not find anything in the docs. */ - qsort(devices, count, sizeof(AudioObjectID), compare_devid); + sort(devices.begin(), devices.end(), [](AudioObjectID a, AudioObjectID b) { + return a < b; + }); if (devtype == (CUBEB_DEVICE_TYPE_INPUT | CUBEB_DEVICE_TYPE_OUTPUT)) { - if (devid_array) { - *devid_array = new AudioObjectID[count]; - assert(*devid_array); - memcpy(*devid_array, &devices, count * sizeof(AudioObjectID)); - } - return count; + return devices; } AudioObjectPropertyScope scope = (devtype == CUBEB_DEVICE_TYPE_INPUT) ? kAudioDevicePropertyScopeInput : kAudioDevicePropertyScopeOutput; - uint32_t dev_count = 0; - AudioObjectID devices_in_scope[count]; - for(uint32_t i = 0; i < count; ++i) { + vector<AudioObjectID> devices_in_scope; + for (uint32_t i = 0; i < devices.size(); ++i) { /* For device in the given scope channel must be > 0. */ if (audiounit_get_channel_count(devices[i], scope) > 0) { - devices_in_scope[dev_count] = devices[i]; - ++dev_count; + devices_in_scope.push_back(devices[i]); } } - if (devid_array && dev_count > 0) { - *devid_array = new AudioObjectID[dev_count]; - assert(*devid_array); - memcpy(*devid_array, &devices_in_scope, dev_count * sizeof(AudioObjectID)); - } - return dev_count; -} - -static uint32_t -audiounit_equal_arrays(AudioObjectID * left, AudioObjectID * right, uint32_t size) -{ - /* Expected sorted arrays. */ - for (uint32_t i = 0; i < size; ++i) { - if (left[i] != right[i]) { - return 0; - } - } - return 1; + return devices_in_scope; } static OSStatus @@ -2600,33 +3477,32 @@ audiounit_collection_changed_callback(AudioObjectID /* inObjectID */, void * inClientData) { cubeb * context = static_cast<cubeb *>(inClientData); - auto_lock lock(context->mutex); - - if (context->collection_changed_callback == NULL) { - /* Listener removed while waiting in mutex, abort. */ - return noErr; - } - /* Differentiate input from output changes. */ - if (context->collection_changed_devtype == CUBEB_DEVICE_TYPE_INPUT || - context->collection_changed_devtype == CUBEB_DEVICE_TYPE_OUTPUT) { - AudioObjectID * devices = NULL; - uint32_t new_number_of_devices = audiounit_get_devices_of_type(context->collection_changed_devtype, &devices); - /* When count is the same examine the devid for the case of coalescing. */ - if (context->devtype_device_count == new_number_of_devices && - audiounit_equal_arrays(devices, context->devtype_device_array, new_number_of_devices)) { - /* Device changed for the other scope, ignore. */ - delete [] devices; - return noErr; + // This can be called from inside an AudioUnit function, dispatch to another queue. + dispatch_async(context->serial_queue, ^() { + auto_lock lock(context->mutex); + if (!context->input_collection_changed_callback && + !context->output_collection_changed_callback) { + /* Listener removed while waiting in mutex, abort. */ + return; } - /* Device on desired scope changed, reset counter and array. */ - context->devtype_device_count = new_number_of_devices; - /* Free the old array before replace. */ - delete [] context->devtype_device_array; - context->devtype_device_array = devices; - } - - context->collection_changed_callback(context, context->collection_changed_user_ptr); + if (context->input_collection_changed_callback) { + vector<AudioObjectID> devices = audiounit_get_devices_of_type(CUBEB_DEVICE_TYPE_INPUT); + /* Elements in the vector expected sorted. */ + if (context->input_device_array != devices) { + context->input_device_array = devices; + context->input_collection_changed_callback(context, context->input_collection_changed_user_ptr); + } + } + if (context->output_collection_changed_callback) { + vector<AudioObjectID> devices = audiounit_get_devices_of_type(CUBEB_DEVICE_TYPE_OUTPUT); + /* Elements in the vector expected sorted. */ + if (context->output_device_array != devices) { + context->output_device_array = devices; + context->output_collection_changed_callback(context, context->output_collection_changed_user_ptr); + } + } + }); return noErr; } @@ -2636,62 +3512,65 @@ audiounit_add_device_listener(cubeb * context, cubeb_device_collection_changed_callback collection_changed_callback, void * user_ptr) { + context->mutex.assert_current_thread_owns(); + assert(devtype & (CUBEB_DEVICE_TYPE_INPUT | CUBEB_DEVICE_TYPE_OUTPUT)); /* Note: second register without unregister first causes 'nope' error. * Current implementation requires unregister before register a new cb. */ - assert(context->collection_changed_callback == NULL); - - AudioObjectPropertyAddress devAddr; - devAddr.mSelector = kAudioHardwarePropertyDevices; - devAddr.mScope = kAudioObjectPropertyScopeGlobal; - devAddr.mElement = kAudioObjectPropertyElementMaster; - - OSStatus ret = AudioObjectAddPropertyListener(kAudioObjectSystemObject, - &devAddr, - audiounit_collection_changed_callback, - context); - if (ret == noErr) { - /* Expected zero after unregister. */ - assert(context->devtype_device_count == 0); - assert(context->devtype_device_array == NULL); - /* Listener works for input and output. - * When requested one of them we need to differentiate. */ - if (devtype == CUBEB_DEVICE_TYPE_INPUT || - devtype == CUBEB_DEVICE_TYPE_OUTPUT) { - /* Used to differentiate input from output device changes. */ - context->devtype_device_count = audiounit_get_devices_of_type(devtype, &context->devtype_device_array); - } - context->collection_changed_devtype = devtype; - context->collection_changed_callback = collection_changed_callback; - context->collection_changed_user_ptr = user_ptr; + assert((devtype & CUBEB_DEVICE_TYPE_INPUT) && !context->input_collection_changed_callback || + (devtype & CUBEB_DEVICE_TYPE_OUTPUT) && !context->output_collection_changed_callback); + + if (!context->input_collection_changed_callback && + !context->output_collection_changed_callback) { + OSStatus ret = AudioObjectAddPropertyListener(kAudioObjectSystemObject, + &DEVICES_PROPERTY_ADDRESS, + audiounit_collection_changed_callback, + context); + if (ret != noErr) { + return ret; + } } - return ret; + if (devtype & CUBEB_DEVICE_TYPE_INPUT) { + /* Expected empty after unregister. */ + assert(context->input_device_array.empty()); + context->input_device_array = audiounit_get_devices_of_type(CUBEB_DEVICE_TYPE_INPUT); + context->input_collection_changed_callback = collection_changed_callback; + context->input_collection_changed_user_ptr = user_ptr; + } + if (devtype & CUBEB_DEVICE_TYPE_OUTPUT) { + /* Expected empty after unregister. */ + assert(context->output_device_array.empty()); + context->output_device_array = audiounit_get_devices_of_type(CUBEB_DEVICE_TYPE_OUTPUT); + context->output_collection_changed_callback = collection_changed_callback; + context->output_collection_changed_user_ptr = user_ptr; + } + return noErr; } static OSStatus -audiounit_remove_device_listener(cubeb * context) +audiounit_remove_device_listener(cubeb * context, cubeb_device_type devtype) { - AudioObjectPropertyAddress devAddr; - devAddr.mSelector = kAudioHardwarePropertyDevices; - devAddr.mScope = kAudioObjectPropertyScopeGlobal; - devAddr.mElement = kAudioObjectPropertyElementMaster; + context->mutex.assert_current_thread_owns(); - /* Note: unregister a non registered cb is not a problem, not checking. */ - OSStatus ret = AudioObjectRemovePropertyListener(kAudioObjectSystemObject, - &devAddr, - audiounit_collection_changed_callback, - context); - if (ret == noErr) { - /* Reset all values. */ - context->collection_changed_devtype = CUBEB_DEVICE_TYPE_UNKNOWN; - context->collection_changed_callback = NULL; - context->collection_changed_user_ptr = NULL; - context->devtype_device_count = 0; - if (context->devtype_device_array) { - delete [] context->devtype_device_array; - context->devtype_device_array = NULL; - } + if (devtype & CUBEB_DEVICE_TYPE_INPUT) { + context->input_collection_changed_callback = nullptr; + context->input_collection_changed_user_ptr = nullptr; + context->input_device_array.clear(); } - return ret; + if (devtype & CUBEB_DEVICE_TYPE_OUTPUT) { + context->output_collection_changed_callback = nullptr; + context->output_collection_changed_user_ptr = nullptr; + context->output_device_array.clear(); + } + + if (context->input_collection_changed_callback || + context->output_collection_changed_callback) { + return noErr; + } + /* Note: unregister a non registered cb is not a problem, not checking. */ + return AudioObjectRemovePropertyListener(kAudioObjectSystemObject, + &DEVICES_PROPERTY_ADDRESS, + audiounit_collection_changed_callback, + context); } int audiounit_register_device_collection_changed(cubeb * context, @@ -2699,14 +3578,18 @@ int audiounit_register_device_collection_changed(cubeb * context, cubeb_device_collection_changed_callback collection_changed_callback, void * user_ptr) { + if (devtype == CUBEB_DEVICE_TYPE_UNKNOWN) { + return CUBEB_ERROR_INVALID_PARAMETER; + } OSStatus ret; auto_lock lock(context->mutex); if (collection_changed_callback) { - ret = audiounit_add_device_listener(context, devtype, + ret = audiounit_add_device_listener(context, + devtype, collection_changed_callback, user_ptr); } else { - ret = audiounit_remove_device_listener(context); + ret = audiounit_remove_device_listener(context, devtype); } return (ret == noErr) ? CUBEB_OK : CUBEB_ERROR; } @@ -2718,15 +3601,16 @@ cubeb_ops const audiounit_ops = { /*.get_min_latency =*/ audiounit_get_min_latency, /*.get_preferred_sample_rate =*/ audiounit_get_preferred_sample_rate, /*.enumerate_devices =*/ audiounit_enumerate_devices, + /*.device_collection_destroy =*/ audiounit_device_collection_destroy, /*.destroy =*/ audiounit_destroy, /*.stream_init =*/ audiounit_stream_init, /*.stream_destroy =*/ audiounit_stream_destroy, /*.stream_start =*/ audiounit_stream_start, /*.stream_stop =*/ audiounit_stream_stop, + /*.stream_reset_default_device =*/ nullptr, /*.stream_get_position =*/ audiounit_stream_get_position, /*.stream_get_latency =*/ audiounit_stream_get_latency, /*.stream_set_volume =*/ audiounit_stream_set_volume, - /*.stream_set_panning =*/ audiounit_stream_set_panning, /*.stream_get_current_device =*/ audiounit_stream_get_current_device, /*.stream_device_destroy =*/ audiounit_stream_device_destroy, /*.stream_register_device_changed_callback =*/ audiounit_stream_register_device_changed_callback, diff --git a/media/libcubeb/src/cubeb_jack.cpp b/media/libcubeb/src/cubeb_jack.cpp index 8f995da661..1ab876c3f5 100644 --- a/media/libcubeb/src/cubeb_jack.cpp +++ b/media/libcubeb/src/cubeb_jack.cpp @@ -8,23 +8,20 @@ */ #define _DEFAULT_SOURCE #define _BSD_SOURCE +#ifndef __FreeBSD__ #define _POSIX_SOURCE -#include <algorithm> +#endif #include <dlfcn.h> -#include <limits> #include <stdio.h> -#include <sys/time.h> -#include <assert.h> #include <string.h> #include <limits.h> -#include <poll.h> -#include <unistd.h> #include <stdlib.h> #include <pthread.h> #include <math.h> #include "cubeb/cubeb.h" #include "cubeb-internal.h" #include "cubeb_resampler.h" +#include "cubeb_utils.h" #include <jack/jack.h> #include <jack/statistics.h> @@ -98,7 +95,9 @@ static int cbjack_stream_device_destroy(cubeb_stream * stream, cubeb_device * device); static int cbjack_stream_get_current_device(cubeb_stream * stm, cubeb_device ** const device); static int cbjack_enumerate_devices(cubeb * context, cubeb_device_type type, - cubeb_device_collection ** collection); + cubeb_device_collection * collection); +static int cbjack_device_collection_destroy(cubeb * context, + cubeb_device_collection * collection); static int cbjack_stream_init(cubeb * context, cubeb_stream ** stream, char const * stream_name, cubeb_devid input_device, cubeb_stream_params * input_stream_params, @@ -121,15 +120,16 @@ static struct cubeb_ops const cbjack_ops = { .get_min_latency = cbjack_get_min_latency, .get_preferred_sample_rate = cbjack_get_preferred_sample_rate, .enumerate_devices = cbjack_enumerate_devices, + .device_collection_destroy = cbjack_device_collection_destroy, .destroy = cbjack_destroy, .stream_init = cbjack_stream_init, .stream_destroy = cbjack_stream_destroy, .stream_start = cbjack_stream_start, .stream_stop = cbjack_stream_stop, + .stream_reset_default_device = NULL, .stream_get_position = cbjack_stream_get_position, .stream_get_latency = cbjack_get_latency, .stream_set_volume = cbjack_stream_set_volume, - .stream_set_panning = NULL, .stream_get_current_device = cbjack_stream_get_current_device, .stream_device_destroy = cbjack_stream_device_destroy, .stream_register_device_changed_callback = NULL, @@ -137,7 +137,10 @@ static struct cubeb_ops const cbjack_ops = { }; struct cubeb_stream { + /* Note: Must match cubeb_stream layout in cubeb.c. */ cubeb * context; + void * user_ptr; + /**/ /**< Mutex for each stream */ pthread_mutex_t mutex; @@ -147,7 +150,6 @@ struct cubeb_stream { cubeb_data_callback data_callback; cubeb_state_callback state_callback; - void * user_ptr; cubeb_stream_params in_params; cubeb_stream_params out_params; @@ -183,7 +185,6 @@ struct cubeb { cubeb_stream streams[MAX_STREAMS]; unsigned int active_streams; - cubeb_device_info * devinfo[2]; cubeb_device_collection_changed_callback collection_changed_callback; bool active; @@ -210,6 +211,9 @@ load_jack_lib(cubeb * context) # endif #else context->libjack = dlopen("libjack.so.0", RTLD_LAZY); + if (!context->libjack) { + context->libjack = dlopen("libjack.so", RTLD_LAZY); + } #endif if (!context->libjack) { return CUBEB_ERROR; @@ -230,9 +234,10 @@ load_jack_lib(cubeb * context) return CUBEB_OK; } -static void +static int cbjack_connect_ports (cubeb_stream * stream) { + int r = CUBEB_ERROR; const char ** phys_in_ports = api_jack_get_ports (stream->context->jack_client, NULL, NULL, JackPortIsInput @@ -242,7 +247,7 @@ cbjack_connect_ports (cubeb_stream * stream) JackPortIsOutput | JackPortIsPhysical); - if (*phys_in_ports == NULL) { + if (phys_in_ports == NULL || *phys_in_ports == NULL) { goto skipplayback; } @@ -252,9 +257,10 @@ cbjack_connect_ports (cubeb_stream * stream) api_jack_connect (stream->context->jack_client, src_port, phys_in_ports[c]); } + r = CUBEB_OK; skipplayback: - if (*phys_out_ports == NULL) { + if (phys_out_ports == NULL || *phys_out_ports == NULL) { goto end; } // Connect inputs to capture @@ -263,9 +269,15 @@ skipplayback: api_jack_connect (stream->context->jack_client, phys_out_ports[c], src_port); } + r = CUBEB_OK; end: - api_jack_free(phys_out_ports); - api_jack_free(phys_in_ports); + if (phys_out_ports) { + api_jack_free(phys_out_ports); + } + if (phys_in_ports) { + api_jack_free(phys_in_ports); + } + return r; } static int @@ -426,7 +438,6 @@ cbjack_process(jack_nframes_t nframes, void * arg) return 0; } - static void cbjack_deinterleave_playback_refill_float(cubeb_stream * stream, float ** in, float ** bufs_out, jack_nframes_t nframes) { @@ -439,7 +450,6 @@ cbjack_deinterleave_playback_refill_float(cubeb_stream * stream, float ** in, fl long done_frames = 0; long input_frames_count = (in != NULL) ? nframes : 0; - done_frames = cubeb_resampler_fill(stream->resampler, inptr, &input_frames_count, @@ -736,6 +746,12 @@ cbjack_stream_init(cubeb * context, cubeb_stream ** stream, char const * stream_ if (input_device || output_device) return CUBEB_ERROR_NOT_SUPPORTED; + // Loopback is unsupported + if ((input_stream_params && (input_stream_params->prefs & CUBEB_STREAM_PREF_LOOPBACK)) || + (output_stream_params && (output_stream_params->prefs & CUBEB_STREAM_PREF_LOOPBACK))) { + return CUBEB_ERROR_NOT_SUPPORTED; + } + *stream = NULL; // Find a free stream. @@ -867,7 +883,11 @@ cbjack_stream_init(cubeb * context, cubeb_stream ** stream, char const * stream_ } } - cbjack_connect_ports(stm); + if (cbjack_connect_ports(stm) != CUBEB_OK) { + pthread_mutex_unlock(&stm->mutex); + cbjack_stream_destroy(stm); + return CUBEB_ERROR; + } *stream = stm; @@ -940,7 +960,6 @@ cbjack_stream_set_volume(cubeb_stream * stm, float volume) return CUBEB_OK; } - static int cbjack_stream_get_current_device(cubeb_stream * stm, cubeb_device ** const device) { @@ -978,70 +997,77 @@ cbjack_stream_device_destroy(cubeb_stream * /*stream*/, return CUBEB_OK; } +#define JACK_DEFAULT_IN "JACK capture" +#define JACK_DEFAULT_OUT "JACK playback" + static int cbjack_enumerate_devices(cubeb * context, cubeb_device_type type, - cubeb_device_collection ** collection) + cubeb_device_collection * collection) { if (!context) return CUBEB_ERROR; uint32_t rate; - uint8_t i = 0; - uint8_t j; cbjack_get_preferred_sample_rate(context, &rate); - const char * j_in = "JACK capture"; - const char * j_out = "JACK playback"; + + cubeb_device_info * devices = new cubeb_device_info[2]; + if (!devices) + return CUBEB_ERROR; + PodZero(devices, 2); + collection->count = 0; if (type & CUBEB_DEVICE_TYPE_OUTPUT) { - context->devinfo[i] = (cubeb_device_info *)malloc(sizeof(cubeb_device_info)); - context->devinfo[i]->device_id = strdup(j_out); - context->devinfo[i]->devid = context->devinfo[i]->device_id; - context->devinfo[i]->friendly_name = strdup(j_out); - context->devinfo[i]->group_id = strdup(j_out); - context->devinfo[i]->vendor_name = strdup(j_out); - context->devinfo[i]->type = CUBEB_DEVICE_TYPE_OUTPUT; - context->devinfo[i]->state = CUBEB_DEVICE_STATE_ENABLED; - context->devinfo[i]->preferred = CUBEB_DEVICE_PREF_ALL; - context->devinfo[i]->format = CUBEB_DEVICE_FMT_F32NE; - context->devinfo[i]->default_format = CUBEB_DEVICE_FMT_F32NE; - context->devinfo[i]->max_channels = MAX_CHANNELS; - context->devinfo[i]->min_rate = rate; - context->devinfo[i]->max_rate = rate; - context->devinfo[i]->default_rate = rate; - context->devinfo[i]->latency_lo = 0; - context->devinfo[i]->latency_hi = 0; - i++; + cubeb_device_info * cur = &devices[collection->count]; + cur->device_id = JACK_DEFAULT_OUT; + cur->devid = (cubeb_devid) cur->device_id; + cur->friendly_name = JACK_DEFAULT_OUT; + cur->group_id = JACK_DEFAULT_OUT; + cur->vendor_name = JACK_DEFAULT_OUT; + cur->type = CUBEB_DEVICE_TYPE_OUTPUT; + cur->state = CUBEB_DEVICE_STATE_ENABLED; + cur->preferred = CUBEB_DEVICE_PREF_ALL; + cur->format = CUBEB_DEVICE_FMT_F32NE; + cur->default_format = CUBEB_DEVICE_FMT_F32NE; + cur->max_channels = MAX_CHANNELS; + cur->min_rate = rate; + cur->max_rate = rate; + cur->default_rate = rate; + cur->latency_lo = 0; + cur->latency_hi = 0; + collection->count +=1 ; } if (type & CUBEB_DEVICE_TYPE_INPUT) { - context->devinfo[i] = (cubeb_device_info *)malloc(sizeof(cubeb_device_info)); - context->devinfo[i]->device_id = strdup(j_in); - context->devinfo[i]->devid = context->devinfo[i]->device_id; - context->devinfo[i]->friendly_name = strdup(j_in); - context->devinfo[i]->group_id = strdup(j_in); - context->devinfo[i]->vendor_name = strdup(j_in); - context->devinfo[i]->type = CUBEB_DEVICE_TYPE_INPUT; - context->devinfo[i]->state = CUBEB_DEVICE_STATE_ENABLED; - context->devinfo[i]->preferred = CUBEB_DEVICE_PREF_ALL; - context->devinfo[i]->format = CUBEB_DEVICE_FMT_F32NE; - context->devinfo[i]->default_format = CUBEB_DEVICE_FMT_F32NE; - context->devinfo[i]->max_channels = MAX_CHANNELS; - context->devinfo[i]->min_rate = rate; - context->devinfo[i]->max_rate = rate; - context->devinfo[i]->default_rate = rate; - context->devinfo[i]->latency_lo = 0; - context->devinfo[i]->latency_hi = 0; - i++; + cubeb_device_info * cur = &devices[collection->count]; + cur->device_id = JACK_DEFAULT_IN; + cur->devid = (cubeb_devid) cur->device_id; + cur->friendly_name = JACK_DEFAULT_IN; + cur->group_id = JACK_DEFAULT_IN; + cur->vendor_name = JACK_DEFAULT_IN; + cur->type = CUBEB_DEVICE_TYPE_INPUT; + cur->state = CUBEB_DEVICE_STATE_ENABLED; + cur->preferred = CUBEB_DEVICE_PREF_ALL; + cur->format = CUBEB_DEVICE_FMT_F32NE; + cur->default_format = CUBEB_DEVICE_FMT_F32NE; + cur->max_channels = MAX_CHANNELS; + cur->min_rate = rate; + cur->max_rate = rate; + cur->default_rate = rate; + cur->latency_lo = 0; + cur->latency_hi = 0; + collection->count += 1; } - *collection = (cubeb_device_collection *) - malloc(sizeof(cubeb_device_collection) + - i * sizeof(cubeb_device_info *)); + collection->device = devices; - (*collection)->count = i; + return CUBEB_OK; +} - for (j = 0; j < i; j++) { - (*collection)->device[j] = context->devinfo[j]; - } +static int +cbjack_device_collection_destroy(cubeb * /*ctx*/, + cubeb_device_collection * collection) +{ + XASSERT(collection); + delete [] collection->device; return CUBEB_OK; } diff --git a/media/libcubeb/src/cubeb_log.cpp b/media/libcubeb/src/cubeb_log.cpp new file mode 100644 index 0000000000..54c7f4a154 --- /dev/null +++ b/media/libcubeb/src/cubeb_log.cpp @@ -0,0 +1,144 @@ +/* + * Copyright © 2016 Mozilla Foundation + * + * This program is made available under an ISC-style license. See the + * accompanying file LICENSE for details. + */ +#define NOMINMAX + +#include "cubeb_log.h" +#include "cubeb_ringbuffer.h" +#include <cstdarg> +#ifdef _WIN32 +#include <windows.h> +#else +#include <time.h> +#endif + +cubeb_log_level g_cubeb_log_level; +cubeb_log_callback g_cubeb_log_callback; + +/** The maximum size of a log message, after having been formatted. */ +const size_t CUBEB_LOG_MESSAGE_MAX_SIZE = 256; +/** The maximum number of log messages that can be queued before dropping + * messages. */ +const size_t CUBEB_LOG_MESSAGE_QUEUE_DEPTH = 40; +/** Number of milliseconds to wait before dequeuing log messages. */ +#define CUBEB_LOG_BATCH_PRINT_INTERVAL_MS 10 + +/** + * This wraps an inline buffer, that represents a log message, that must be + * null-terminated. + * This class should not use system calls or other potentially blocking code. + */ +class cubeb_log_message +{ +public: + cubeb_log_message() + { + *storage = '\0'; + } + cubeb_log_message(char const str[CUBEB_LOG_MESSAGE_MAX_SIZE]) + { + size_t length = strlen(str); + /* paranoia against malformed message */ + assert(length < CUBEB_LOG_MESSAGE_MAX_SIZE); + if (length > CUBEB_LOG_MESSAGE_MAX_SIZE - 1) { + return; + } + PodCopy(storage, str, length); + storage[length] = '\0'; + } + char const * get() { + return storage; + } +private: + char storage[CUBEB_LOG_MESSAGE_MAX_SIZE]; +}; + +/** Lock-free asynchronous logger, made so that logging from a + * real-time audio callback does not block the audio thread. */ +class cubeb_async_logger +{ +public: + /* This is thread-safe since C++11 */ + static cubeb_async_logger & get() { + static cubeb_async_logger instance; + return instance; + } + void push(char const str[CUBEB_LOG_MESSAGE_MAX_SIZE]) + { + cubeb_log_message msg(str); + msg_queue.enqueue(msg); + } + void run() + { + std::thread([this]() { + while (true) { + cubeb_log_message msg; + while (msg_queue.dequeue(&msg, 1)) { + LOGV("%s", msg.get()); + } +#ifdef _WIN32 + Sleep(CUBEB_LOG_BATCH_PRINT_INTERVAL_MS); +#else + timespec sleep_duration = sleep_for; + timespec remainder; + do { + if (nanosleep(&sleep_duration, &remainder) == 0 || + errno != EINTR) { + break; + } + sleep_duration = remainder; + } while (remainder.tv_sec || remainder.tv_nsec); +#endif + } + }).detach(); + } + // Tell the underlying queue the producer thread has changed, so it does not + // assert in debug. This should be called with the thread stopped. + void reset_producer_thread() + { + msg_queue.reset_thread_ids(); + } +private: +#ifndef _WIN32 + const struct timespec sleep_for = { + CUBEB_LOG_BATCH_PRINT_INTERVAL_MS/1000, + (CUBEB_LOG_BATCH_PRINT_INTERVAL_MS%1000)*1000*1000 + }; +#endif + cubeb_async_logger() + : msg_queue(CUBEB_LOG_MESSAGE_QUEUE_DEPTH) + { + run(); + } + /** This is quite a big data structure, but is only instantiated if the + * asynchronous logger is used.*/ + lock_free_queue<cubeb_log_message> msg_queue; +}; + + +void cubeb_async_log(char const * fmt, ...) +{ + if (!g_cubeb_log_callback) { + return; + } + // This is going to copy a 256 bytes array around, which is fine. + // We don't want to allocate memory here, because this is made to + // be called from a real-time callback. + va_list args; + va_start(args, fmt); + char msg[CUBEB_LOG_MESSAGE_MAX_SIZE]; + vsnprintf(msg, CUBEB_LOG_MESSAGE_MAX_SIZE, fmt, args); + cubeb_async_logger::get().push(msg); + va_end(args); +} + +void cubeb_async_log_reset_threads() +{ + if (!g_cubeb_log_callback) { + return; + } + cubeb_async_logger::get().reset_producer_thread(); +} diff --git a/media/libcubeb/src/cubeb_log.h b/media/libcubeb/src/cubeb_log.h index bca98c96fe..a79976bb3f 100644 --- a/media/libcubeb/src/cubeb_log.h +++ b/media/libcubeb/src/cubeb_log.h @@ -8,6 +8,8 @@ #ifndef CUBEB_LOG #define CUBEB_LOG +#include "cubeb/cubeb.h" + #ifdef __cplusplus extern "C" { #endif @@ -18,8 +20,10 @@ extern "C" { #define PRINTF_FORMAT(fmt, args) #endif -extern cubeb_log_level g_log_level; -extern cubeb_log_callback g_log_callback PRINTF_FORMAT(1, 2); +extern cubeb_log_level g_cubeb_log_level; +extern cubeb_log_callback g_cubeb_log_callback PRINTF_FORMAT(1, 2); +void cubeb_async_log(const char * fmt, ...); +void cubeb_async_log_reset_threads(); #ifdef __cplusplus } @@ -29,9 +33,15 @@ extern cubeb_log_callback g_log_callback PRINTF_FORMAT(1, 2); #define LOG(msg, ...) LOG_INTERNAL(CUBEB_LOG_NORMAL, msg, ##__VA_ARGS__) #define LOG_INTERNAL(level, fmt, ...) do { \ - if (g_log_callback && level <= g_log_level) { \ - g_log_callback("%s:%d: " fmt "\n", __FILE__, __LINE__, ##__VA_ARGS__); \ + if (g_cubeb_log_callback && level <= g_cubeb_log_level) { \ + g_cubeb_log_callback("%s:%d: " fmt "\n", __FILE__, __LINE__, ##__VA_ARGS__); \ } \ } while(0) +/* Asynchronous verbose logging, to log in real-time callbacks. */ +#define ALOGV(fmt, ...) \ +do { \ + cubeb_async_log(fmt, ##__VA_ARGS__); \ +} while(0) + #endif // CUBEB_LOG diff --git a/media/libcubeb/src/cubeb_mixer.cpp b/media/libcubeb/src/cubeb_mixer.cpp new file mode 100644 index 0000000000..2ab7f673af --- /dev/null +++ b/media/libcubeb/src/cubeb_mixer.cpp @@ -0,0 +1,663 @@ +/* + * Copyright © 2016 Mozilla Foundation + * + * This program is made available under an ISC-style license. See the + * accompanying file LICENSE for details. + * + * Adapted from code based on libswresample's rematrix.c + */ + +#define NOMINMAX + +#include <algorithm> +#include <cassert> +#include <climits> +#include <cmath> +#include <cstdlib> +#include <memory> +#include <type_traits> +#include "cubeb-internal.h" +#include "cubeb_mixer.h" +#include "cubeb_utils.h" + +#ifndef FF_ARRAY_ELEMS +#define FF_ARRAY_ELEMS(a) (sizeof(a) / sizeof((a)[0])) +#endif + +#define CHANNELS_MAX 32 +#define FRONT_LEFT 0 +#define FRONT_RIGHT 1 +#define FRONT_CENTER 2 +#define LOW_FREQUENCY 3 +#define BACK_LEFT 4 +#define BACK_RIGHT 5 +#define FRONT_LEFT_OF_CENTER 6 +#define FRONT_RIGHT_OF_CENTER 7 +#define BACK_CENTER 8 +#define SIDE_LEFT 9 +#define SIDE_RIGHT 10 +#define TOP_CENTER 11 +#define TOP_FRONT_LEFT 12 +#define TOP_FRONT_CENTER 13 +#define TOP_FRONT_RIGHT 14 +#define TOP_BACK_LEFT 15 +#define TOP_BACK_CENTER 16 +#define TOP_BACK_RIGHT 17 +#define NUM_NAMED_CHANNELS 18 + +#ifndef M_SQRT1_2 +#define M_SQRT1_2 0.70710678118654752440 /* 1/sqrt(2) */ +#endif +#ifndef M_SQRT2 +#define M_SQRT2 1.41421356237309504880 /* sqrt(2) */ +#endif +#define SQRT3_2 1.22474487139158904909 /* sqrt(3/2) */ + +#define C30DB M_SQRT2 +#define C15DB 1.189207115 +#define C__0DB 1.0 +#define C_15DB 0.840896415 +#define C_30DB M_SQRT1_2 +#define C_45DB 0.594603558 +#define C_60DB 0.5 + +static cubeb_channel_layout +cubeb_channel_layout_check(cubeb_channel_layout l, uint32_t c) +{ + if (l == CUBEB_LAYOUT_UNDEFINED) { + switch (c) { + case 1: return CUBEB_LAYOUT_MONO; + case 2: return CUBEB_LAYOUT_STEREO; + } + } + return l; +} + +unsigned int cubeb_channel_layout_nb_channels(cubeb_channel_layout x) +{ +#if __GNUC__ || __clang__ + return __builtin_popcount (x); +#else + x -= (x >> 1) & 0x55555555; + x = (x & 0x33333333) + ((x >> 2) & 0x33333333); + x = (x + (x >> 4)) & 0x0F0F0F0F; + x += x >> 8; + return (x + (x >> 16)) & 0x3F; +#endif +} + +struct MixerContext { + MixerContext(cubeb_sample_format f, + uint32_t in_channels, + cubeb_channel_layout in, + uint32_t out_channels, + cubeb_channel_layout out) + : _format(f) + , _in_ch_layout(cubeb_channel_layout_check(in, in_channels)) + , _out_ch_layout(cubeb_channel_layout_check(out, out_channels)) + , _in_ch_count(in_channels) + , _out_ch_count(out_channels) + { + if (in_channels != cubeb_channel_layout_nb_channels(in) || + out_channels != cubeb_channel_layout_nb_channels(out)) { + // Mismatch between channels and layout, aborting. + return; + } + _valid = init() >= 0; + } + + static bool even(cubeb_channel_layout layout) + { + if (!layout) { + return true; + } + if (layout & (layout - 1)) { + return true; + } + return false; + } + + // Ensure that the layout is sane (that is have symmetrical left/right + // channels), if not, layout will be treated as mono. + static cubeb_channel_layout clean_layout(cubeb_channel_layout layout) + { + if (layout && layout != CHANNEL_FRONT_LEFT && !(layout & (layout - 1))) { + LOG("Treating layout as mono"); + return CHANNEL_FRONT_CENTER; + } + + return layout; + } + + static bool sane_layout(cubeb_channel_layout layout) + { + if (!(layout & CUBEB_LAYOUT_3F)) { // at least 1 front speaker + return false; + } + if (!even(layout & (CHANNEL_FRONT_LEFT | + CHANNEL_FRONT_RIGHT))) { // no asymetric front + return false; + } + if (!even(layout & + (CHANNEL_SIDE_LEFT | CHANNEL_SIDE_RIGHT))) { // no asymetric side + return false; + } + if (!even(layout & (CHANNEL_BACK_LEFT | CHANNEL_BACK_RIGHT))) { + return false; + } + if (!even(layout & + (CHANNEL_FRONT_LEFT_OF_CENTER | CHANNEL_FRONT_RIGHT_OF_CENTER))) { + return false; + } + if (cubeb_channel_layout_nb_channels(layout) >= CHANNELS_MAX) { + return false; + } + return true; + } + + int auto_matrix(); + int init(); + + const cubeb_sample_format _format; + const cubeb_channel_layout _in_ch_layout; ///< input channel layout + const cubeb_channel_layout _out_ch_layout; ///< output channel layout + const uint32_t _in_ch_count; ///< input channel count + const uint32_t _out_ch_count; ///< output channel count + const float _surround_mix_level = C_30DB; ///< surround mixing level + const float _center_mix_level = C_30DB; ///< center mixing level + const float _lfe_mix_level = 1; ///< LFE mixing level + double _matrix[CHANNELS_MAX][CHANNELS_MAX] = {{ 0 }}; ///< floating point rematrixing coefficients + float _matrix_flt[CHANNELS_MAX][CHANNELS_MAX] = {{ 0 }}; ///< single precision floating point rematrixing coefficients + int32_t _matrix32[CHANNELS_MAX][CHANNELS_MAX] = {{ 0 }}; ///< 17.15 fixed point rematrixing coefficients + uint8_t _matrix_ch[CHANNELS_MAX][CHANNELS_MAX+1] = {{ 0 }}; ///< Lists of input channels per output channel that have non zero rematrixing coefficients + bool _clipping = false; ///< Set to true if clipping detection is required + bool _valid = false; ///< Set to true if context is valid. +}; + +int MixerContext::auto_matrix() +{ + double matrix[NUM_NAMED_CHANNELS][NUM_NAMED_CHANNELS] = { { 0 } }; + double maxcoef = 0; + float maxval; + + cubeb_channel_layout in_ch_layout = clean_layout(_in_ch_layout); + cubeb_channel_layout out_ch_layout = clean_layout(_out_ch_layout); + + if (!sane_layout(in_ch_layout)) { + // Channel Not Supported + LOG("Input Layout %x is not supported", _in_ch_layout); + return -1; + } + + if (!sane_layout(out_ch_layout)) { + LOG("Output Layout %x is not supported", _out_ch_layout); + return -1; + } + + for (uint32_t i = 0; i < FF_ARRAY_ELEMS(matrix); i++) { + if (in_ch_layout & out_ch_layout & (1U << i)) { + matrix[i][i] = 1.0; + } + } + + cubeb_channel_layout unaccounted = in_ch_layout & ~out_ch_layout; + + // Rematrixing is done via a matrix of coefficient that should be applied to + // all channels. Channels are treated as pair and must be symmetrical (if a + // left channel exists, the corresponding right should exist too) unless the + // output layout has similar layout. Channels are then mixed toward the front + // center or back center if they exist with a slight bias toward the front. + + if (unaccounted & CHANNEL_FRONT_CENTER) { + if ((out_ch_layout & CUBEB_LAYOUT_STEREO) == CUBEB_LAYOUT_STEREO) { + if (in_ch_layout & CUBEB_LAYOUT_STEREO) { + matrix[FRONT_LEFT][FRONT_CENTER] += _center_mix_level; + matrix[FRONT_RIGHT][FRONT_CENTER] += _center_mix_level; + } else { + matrix[FRONT_LEFT][FRONT_CENTER] += M_SQRT1_2; + matrix[FRONT_RIGHT][FRONT_CENTER] += M_SQRT1_2; + } + } + } + if (unaccounted & CUBEB_LAYOUT_STEREO) { + if (out_ch_layout & CHANNEL_FRONT_CENTER) { + matrix[FRONT_CENTER][FRONT_LEFT] += M_SQRT1_2; + matrix[FRONT_CENTER][FRONT_RIGHT] += M_SQRT1_2; + if (in_ch_layout & CHANNEL_FRONT_CENTER) + matrix[FRONT_CENTER][FRONT_CENTER] = _center_mix_level * M_SQRT2; + } + } + + if (unaccounted & CHANNEL_BACK_CENTER) { + if (out_ch_layout & CHANNEL_BACK_LEFT) { + matrix[BACK_LEFT][BACK_CENTER] += M_SQRT1_2; + matrix[BACK_RIGHT][BACK_CENTER] += M_SQRT1_2; + } else if (out_ch_layout & CHANNEL_SIDE_LEFT) { + matrix[SIDE_LEFT][BACK_CENTER] += M_SQRT1_2; + matrix[SIDE_RIGHT][BACK_CENTER] += M_SQRT1_2; + } else if (out_ch_layout & CHANNEL_FRONT_LEFT) { + matrix[FRONT_LEFT][BACK_CENTER] += _surround_mix_level * M_SQRT1_2; + matrix[FRONT_RIGHT][BACK_CENTER] += _surround_mix_level * M_SQRT1_2; + } else if (out_ch_layout & CHANNEL_FRONT_CENTER) { + matrix[FRONT_CENTER][BACK_CENTER] += + _surround_mix_level * M_SQRT1_2; + } + } + if (unaccounted & CHANNEL_BACK_LEFT) { + if (out_ch_layout & CHANNEL_BACK_CENTER) { + matrix[BACK_CENTER][BACK_LEFT] += M_SQRT1_2; + matrix[BACK_CENTER][BACK_RIGHT] += M_SQRT1_2; + } else if (out_ch_layout & CHANNEL_SIDE_LEFT) { + if (in_ch_layout & CHANNEL_SIDE_LEFT) { + matrix[SIDE_LEFT][BACK_LEFT] += M_SQRT1_2; + matrix[SIDE_RIGHT][BACK_RIGHT] += M_SQRT1_2; + } else { + matrix[SIDE_LEFT][BACK_LEFT] += 1.0; + matrix[SIDE_RIGHT][BACK_RIGHT] += 1.0; + } + } else if (out_ch_layout & CHANNEL_FRONT_LEFT) { + matrix[FRONT_LEFT][BACK_LEFT] += _surround_mix_level; + matrix[FRONT_RIGHT][BACK_RIGHT] += _surround_mix_level; + } else if (out_ch_layout & CHANNEL_FRONT_CENTER) { + matrix[FRONT_CENTER][BACK_LEFT] += _surround_mix_level * M_SQRT1_2; + matrix[FRONT_CENTER][BACK_RIGHT] += _surround_mix_level * M_SQRT1_2; + } + } + + if (unaccounted & CHANNEL_SIDE_LEFT) { + if (out_ch_layout & CHANNEL_BACK_LEFT) { + /* if back channels do not exist in the input, just copy side + channels to back channels, otherwise mix side into back */ + if (in_ch_layout & CHANNEL_BACK_LEFT) { + matrix[BACK_LEFT][SIDE_LEFT] += M_SQRT1_2; + matrix[BACK_RIGHT][SIDE_RIGHT] += M_SQRT1_2; + } else { + matrix[BACK_LEFT][SIDE_LEFT] += 1.0; + matrix[BACK_RIGHT][SIDE_RIGHT] += 1.0; + } + } else if (out_ch_layout & CHANNEL_BACK_CENTER) { + matrix[BACK_CENTER][SIDE_LEFT] += M_SQRT1_2; + matrix[BACK_CENTER][SIDE_RIGHT] += M_SQRT1_2; + } else if (out_ch_layout & CHANNEL_FRONT_LEFT) { + matrix[FRONT_LEFT][SIDE_LEFT] += _surround_mix_level; + matrix[FRONT_RIGHT][SIDE_RIGHT] += _surround_mix_level; + } else if (out_ch_layout & CHANNEL_FRONT_CENTER) { + matrix[FRONT_CENTER][SIDE_LEFT] += _surround_mix_level * M_SQRT1_2; + matrix[FRONT_CENTER][SIDE_RIGHT] += _surround_mix_level * M_SQRT1_2; + } + } + + if (unaccounted & CHANNEL_FRONT_LEFT_OF_CENTER) { + if (out_ch_layout & CHANNEL_FRONT_LEFT) { + matrix[FRONT_LEFT][FRONT_LEFT_OF_CENTER] += 1.0; + matrix[FRONT_RIGHT][FRONT_RIGHT_OF_CENTER] += 1.0; + } else if (out_ch_layout & CHANNEL_FRONT_CENTER) { + matrix[FRONT_CENTER][FRONT_LEFT_OF_CENTER] += M_SQRT1_2; + matrix[FRONT_CENTER][FRONT_RIGHT_OF_CENTER] += M_SQRT1_2; + } + } + /* mix LFE into front left/right or center */ + if (unaccounted & CHANNEL_LOW_FREQUENCY) { + if (out_ch_layout & CHANNEL_FRONT_CENTER) { + matrix[FRONT_CENTER][LOW_FREQUENCY] += _lfe_mix_level; + } else if (out_ch_layout & CHANNEL_FRONT_LEFT) { + matrix[FRONT_LEFT][LOW_FREQUENCY] += _lfe_mix_level * M_SQRT1_2; + matrix[FRONT_RIGHT][LOW_FREQUENCY] += _lfe_mix_level * M_SQRT1_2; + } + } + + // Normalize the conversion matrix. + for (uint32_t out_i = 0, i = 0; i < CHANNELS_MAX; i++) { + double sum = 0; + int in_i = 0; + if ((out_ch_layout & (1U << i)) == 0) { + continue; + } + for (uint32_t j = 0; j < CHANNELS_MAX; j++) { + if ((in_ch_layout & (1U << j)) == 0) { + continue; + } + if (i < FF_ARRAY_ELEMS(matrix) && j < FF_ARRAY_ELEMS(matrix[0])) { + _matrix[out_i][in_i] = matrix[i][j]; + } else { + _matrix[out_i][in_i] = + i == j && (in_ch_layout & out_ch_layout & (1U << i)); + } + sum += fabs(_matrix[out_i][in_i]); + in_i++; + } + maxcoef = std::max(maxcoef, sum); + out_i++; + } + + if (_format == CUBEB_SAMPLE_S16NE) { + maxval = 1.0; + } else { + maxval = INT_MAX; + } + + // Normalize matrix if needed. + if (maxcoef > maxval) { + maxcoef /= maxval; + for (uint32_t i = 0; i < CHANNELS_MAX; i++) + for (uint32_t j = 0; j < CHANNELS_MAX; j++) { + _matrix[i][j] /= maxcoef; + } + } + + if (_format == CUBEB_SAMPLE_FLOAT32NE) { + for (uint32_t i = 0; i < FF_ARRAY_ELEMS(_matrix); i++) { + for (uint32_t j = 0; j < FF_ARRAY_ELEMS(_matrix[0]); j++) { + _matrix_flt[i][j] = _matrix[i][j]; + } + } + } + + return 0; +} + +int MixerContext::init() +{ + int r = auto_matrix(); + if (r) { + return r; + } + + // Determine if matrix operation would overflow + if (_format == CUBEB_SAMPLE_S16NE) { + int maxsum = 0; + for (uint32_t i = 0; i < _out_ch_count; i++) { + double rem = 0; + int sum = 0; + + for (uint32_t j = 0; j < _in_ch_count; j++) { + double target = _matrix[i][j] * 32768 + rem; + int value = lrintf(target); + rem += target - value; + sum += std::abs(value); + } + maxsum = std::max(maxsum, sum); + } + if (maxsum > 32768) { + _clipping = true; + } + } + + // FIXME quantize for integers + for (uint32_t i = 0; i < CHANNELS_MAX; i++) { + int ch_in = 0; + for (uint32_t j = 0; j < CHANNELS_MAX; j++) { + _matrix32[i][j] = lrintf(_matrix[i][j] * 32768); + if (_matrix[i][j]) { + _matrix_ch[i][++ch_in] = j; + } + } + _matrix_ch[i][0] = ch_in; + } + + return 0; +} + +template<typename TYPE_SAMPLE, typename TYPE_COEFF, typename F> +void +sum2(TYPE_SAMPLE * out, + uint32_t stride_out, + const TYPE_SAMPLE * in1, + const TYPE_SAMPLE * in2, + uint32_t stride_in, + TYPE_COEFF coeff1, + TYPE_COEFF coeff2, + F&& operand, + uint32_t frames) +{ + static_assert( + std::is_same<TYPE_COEFF, + typename std::result_of<F(TYPE_COEFF)>::type>::value, + "function must return the same type as used by matrix_coeff"); + for (uint32_t i = 0; i < frames; i++) { + *out = operand(coeff1 * *in1 + coeff2 * *in2); + out += stride_out; + in1 += stride_in; + in2 += stride_in; + } +} + +template<typename TYPE_SAMPLE, typename TYPE_COEFF, typename F> +void +copy(TYPE_SAMPLE * out, + uint32_t stride_out, + const TYPE_SAMPLE * in, + uint32_t stride_in, + TYPE_COEFF coeff, + F&& operand, + uint32_t frames) +{ + static_assert( + std::is_same<TYPE_COEFF, + typename std::result_of<F(TYPE_COEFF)>::type>::value, + "function must return the same type as used by matrix_coeff"); + for (uint32_t i = 0; i < frames; i++) { + *out = operand(coeff * *in); + out += stride_out; + in += stride_in; + } +} + +template <typename TYPE, typename TYPE_COEFF, size_t COLS, typename F> +static int rematrix(const MixerContext * s, TYPE * aOut, const TYPE * aIn, + const TYPE_COEFF (&matrix_coeff)[COLS][COLS], + F&& aF, uint32_t frames) +{ + static_assert( + std::is_same<TYPE_COEFF, + typename std::result_of<F(TYPE_COEFF)>::type>::value, + "function must return the same type as used by matrix_coeff"); + + for (uint32_t out_i = 0; out_i < s->_out_ch_count; out_i++) { + TYPE* out = aOut + out_i; + switch (s->_matrix_ch[out_i][0]) { + case 0: + for (uint32_t i = 0; i < frames; i++) { + out[i * s->_out_ch_count] = 0; + } + break; + case 1: { + int in_i = s->_matrix_ch[out_i][1]; + copy(out, + s->_out_ch_count, + aIn + in_i, + s->_in_ch_count, + matrix_coeff[out_i][in_i], + aF, + frames); + } break; + case 2: + sum2(out, + s->_out_ch_count, + aIn + s->_matrix_ch[out_i][1], + aIn + s->_matrix_ch[out_i][2], + s->_in_ch_count, + matrix_coeff[out_i][s->_matrix_ch[out_i][1]], + matrix_coeff[out_i][s->_matrix_ch[out_i][2]], + aF, + frames); + break; + default: + for (uint32_t i = 0; i < frames; i++) { + TYPE_COEFF v = 0; + for (uint32_t j = 0; j < s->_matrix_ch[out_i][0]; j++) { + uint32_t in_i = s->_matrix_ch[out_i][1 + j]; + v += + *(aIn + in_i + i * s->_in_ch_count) * matrix_coeff[out_i][in_i]; + } + out[i * s->_out_ch_count] = aF(v); + } + break; + } + } + return 0; +} + +struct cubeb_mixer +{ + cubeb_mixer(cubeb_sample_format format, + uint32_t in_channels, + cubeb_channel_layout in_layout, + uint32_t out_channels, + cubeb_channel_layout out_layout) + : _context(format, in_channels, in_layout, out_channels, out_layout) + { + } + + template<typename T> + void copy_and_trunc(size_t frames, + const T * input_buffer, + T * output_buffer) const + { + if (_context._in_ch_count <= _context._out_ch_count) { + // Not enough channels to copy, fill the gaps with silence. + if (_context._in_ch_count == 1 && _context._out_ch_count >= 2) { + // Special case for upmixing mono input to stereo and more. We will + // duplicate the mono channel to the first two channels. On most system, + // the first two channels are for left and right. It is commonly + // expected that mono will on both left+right channels + for (uint32_t i = 0; i < frames; i++) { + output_buffer[0] = output_buffer[1] = *input_buffer; + PodZero(output_buffer + 2, _context._out_ch_count - 2); + output_buffer += _context._out_ch_count; + input_buffer++; + } + return; + } + for (uint32_t i = 0; i < frames; i++) { + PodCopy(output_buffer, input_buffer, _context._in_ch_count); + output_buffer += _context._in_ch_count; + input_buffer += _context._in_ch_count; + PodZero(output_buffer, _context._out_ch_count - _context._in_ch_count); + output_buffer += _context._out_ch_count - _context._in_ch_count; + } + } else { + for (uint32_t i = 0; i < frames; i++) { + PodCopy(output_buffer, input_buffer, _context._out_ch_count); + output_buffer += _context._out_ch_count; + input_buffer += _context._in_ch_count; + } + } + } + + int mix(size_t frames, + const void * input_buffer, + size_t input_buffer_size, + void * output_buffer, + size_t output_buffer_size) const + { + if (frames <= 0 || _context._out_ch_count == 0) { + return 0; + } + + // Check if output buffer is of sufficient size. + size_t size_read_needed = + frames * _context._in_ch_count * cubeb_sample_size(_context._format); + if (input_buffer_size < size_read_needed) { + // We don't have enough data to read! + return -1; + } + if (output_buffer_size * _context._in_ch_count < + size_read_needed * _context._out_ch_count) { + return -1; + } + + if (!valid()) { + // The channel layouts were invalid or unsupported, instead we will simply + // either drop the extra channels, or fill with silence the missing ones + if (_context._format == CUBEB_SAMPLE_FLOAT32NE) { + copy_and_trunc(frames, + static_cast<const float*>(input_buffer), + static_cast<float*>(output_buffer)); + } else { + assert(_context._format == CUBEB_SAMPLE_S16NE); + copy_and_trunc(frames, + static_cast<const int16_t*>(input_buffer), + reinterpret_cast<int16_t*>(output_buffer)); + } + return 0; + } + + switch (_context._format) + { + case CUBEB_SAMPLE_FLOAT32NE: { + auto f = [](float x) { return x; }; + return rematrix(&_context, + static_cast<float*>(output_buffer), + static_cast<const float*>(input_buffer), + _context._matrix_flt, + f, + frames); + } + case CUBEB_SAMPLE_S16NE: + if (_context._clipping) { + auto f = [](int x) { + int y = (x + 16384) >> 15; + // clip the signed integer value into the -32768,32767 range. + if ((y + 0x8000U) & ~0xFFFF) { + return (y >> 31) ^ 0x7FFF; + } + return y; + }; + return rematrix(&_context, + static_cast<int16_t*>(output_buffer), + static_cast<const int16_t*>(input_buffer), + _context._matrix32, + f, + frames); + } else { + auto f = [](int x) { return (x + 16384) >> 15; }; + return rematrix(&_context, + static_cast<int16_t*>(output_buffer), + static_cast<const int16_t*>(input_buffer), + _context._matrix32, + f, + frames); + } + break; + default: + assert(false); + break; + } + + return -1; + } + + // Return false if any of the input or ouput layout were invalid. + bool valid() const { return _context._valid; } + + virtual ~cubeb_mixer(){}; + + MixerContext _context; +}; + +cubeb_mixer* cubeb_mixer_create(cubeb_sample_format format, + uint32_t in_channels, + cubeb_channel_layout in_layout, + uint32_t out_channels, + cubeb_channel_layout out_layout) +{ + return new cubeb_mixer( + format, in_channels, in_layout, out_channels, out_layout); +} + +void cubeb_mixer_destroy(cubeb_mixer * mixer) +{ + delete mixer; +} + +int cubeb_mixer_mix(cubeb_mixer * mixer, + size_t frames, + const void * input_buffer, + size_t input_buffer_size, + void * output_buffer, + size_t output_buffer_size) +{ + return mixer->mix( + frames, input_buffer, input_buffer_size, output_buffer, output_buffer_size); +} diff --git a/media/libcubeb/src/cubeb_mixer.h b/media/libcubeb/src/cubeb_mixer.h new file mode 100644 index 0000000000..d43a237f9e --- /dev/null +++ b/media/libcubeb/src/cubeb_mixer.h @@ -0,0 +1,37 @@ +/* + * Copyright © 2016 Mozilla Foundation + * + * This program is made available under an ISC-style license. See the + * accompanying file LICENSE for details. + */ + +#ifndef CUBEB_MIXER +#define CUBEB_MIXER + +#include "cubeb/cubeb.h" // for cubeb_channel_layout and cubeb_stream_params. + +#if defined(__cplusplus) +extern "C" { +#endif + +typedef struct cubeb_mixer cubeb_mixer; +cubeb_mixer * cubeb_mixer_create(cubeb_sample_format format, + uint32_t in_channels, + cubeb_channel_layout in_layout, + uint32_t out_channels, + cubeb_channel_layout out_layout); +void cubeb_mixer_destroy(cubeb_mixer * mixer); +int cubeb_mixer_mix(cubeb_mixer * mixer, + size_t frames, + const void * input_buffer, + size_t input_buffer_size, + void * output_buffer, + size_t output_buffer_size); + +unsigned int cubeb_channel_layout_nb_channels(cubeb_channel_layout channel_layout); + +#if defined(__cplusplus) +} +#endif + +#endif // CUBEB_MIXER diff --git a/media/libcubeb/src/cubeb_opensl.c b/media/libcubeb/src/cubeb_opensl.c index dd54162280..96374ec077 100644 --- a/media/libcubeb/src/cubeb_opensl.c +++ b/media/libcubeb/src/cubeb_opensl.c @@ -9,6 +9,7 @@ #include <dlfcn.h> #include <stdlib.h> #include <pthread.h> +#include <errno.h> #include <SLES/OpenSLES.h> #include <math.h> #include <time.h> @@ -19,82 +20,236 @@ #include <SLES/OpenSLES_Android.h> #include <android/log.h> #include <android/api-level.h> -#define LOG(args...) __android_log_print(ANDROID_LOG_INFO, "Cubeb_OpenSL" , ## args) -#define ANDROID_VERSION_GINGERBREAD_MR1 10 -#define ANDROID_VERSION_LOLLIPOP 21 -#define ANDROID_VERSION_MARSHMALLOW 23 #endif #include "cubeb/cubeb.h" #include "cubeb-internal.h" #include "cubeb_resampler.h" #include "cubeb-sles.h" +#include "cubeb_array_queue.h" +#include "android/cubeb-output-latency.h" + +#if defined(__ANDROID__) +#ifdef LOG +#undef LOG +#endif +//#define LOGGING_ENABLED +#ifdef LOGGING_ENABLED +#define LOG(args...) __android_log_print(ANDROID_LOG_INFO, "Cubeb_OpenSL" , ## args) +#else +#define LOG(...) +#endif + +//#define TIMESTAMP_ENABLED +#ifdef TIMESTAMP_ENABLED +#define FILENAME (strrchr(__FILE__, '/') ? strrchr(__FILE__, '/') + 1 : __FILE__) +#define LOG_TS(args...) __android_log_print(ANDROID_LOG_INFO, "Cubeb_OpenSL ES: Timestamp(usec)" , ## args) +#define TIMESTAMP(msg) do { \ + struct timeval timestamp; \ + int ts_ret = gettimeofday(×tamp, NULL); \ + if (ts_ret == 0) { \ + LOG_TS("%lld: %s (%s %s:%d)", timestamp.tv_sec * 1000000LL + timestamp.tv_usec, msg, __FUNCTION__, FILENAME, __LINE__);\ + } else { \ + LOG_TS("Error: %s (%s %s:%d) - %s", msg, __FUNCTION__, FILENAME, __LINE__);\ + } \ +} while(0) +#else +#define TIMESTAMP(...) +#endif + +#define ANDROID_VERSION_GINGERBREAD_MR1 10 +#define ANDROID_VERSION_JELLY_BEAN 18 +#define ANDROID_VERSION_LOLLIPOP 21 +#define ANDROID_VERSION_MARSHMALLOW 23 +#define ANDROID_VERSION_N_MR1 25 +#endif + +#define DEFAULT_SAMPLE_RATE 48000 +#define DEFAULT_NUM_OF_FRAMES 480 +// If the latency requested is above this threshold, this stream is considered +// intended for playback (vs. real-time). Tell Android it should favor saving +// power over performance or latency. +// This is around 100ms at 44100 or 48000 +#define POWERSAVE_LATENCY_FRAMES_THRESHOLD 4000 static struct cubeb_ops const opensl_ops; struct cubeb { struct cubeb_ops const * ops; void * lib; - void * libmedia; - int32_t (* get_output_latency)(uint32_t * latency, int stream_type); SLInterfaceID SL_IID_BUFFERQUEUE; SLInterfaceID SL_IID_PLAY; #if defined(__ANDROID__) SLInterfaceID SL_IID_ANDROIDCONFIGURATION; + SLInterfaceID SL_IID_ANDROIDSIMPLEBUFFERQUEUE; #endif SLInterfaceID SL_IID_VOLUME; + SLInterfaceID SL_IID_RECORD; SLObjectItf engObj; SLEngineItf eng; SLObjectItf outmixObj; + output_latency_function * p_output_latency_function; }; #define NELEMS(A) (sizeof(A) / sizeof A[0]) -#define NBUFS 4 -#define AUDIO_STREAM_TYPE_MUSIC 3 +#define NBUFS 2 struct cubeb_stream { + /* Note: Must match cubeb_stream layout in cubeb.c. */ cubeb * context; + void * user_ptr; + /**/ pthread_mutex_t mutex; SLObjectItf playerObj; SLPlayItf play; SLBufferQueueItf bufq; SLVolumeItf volume; - uint8_t *queuebuf[NBUFS]; + void ** queuebuf; + uint32_t queuebuf_capacity; int queuebuf_idx; long queuebuf_len; long bytespersec; long framesize; + /* Total number of played frames. + * Synchronized by stream::mutex lock. */ long written; + /* Flag indicating draining. Synchronized + * by stream::mutex lock. */ int draining; - cubeb_stream_type stream_type; - + /* Flags to determine in/out.*/ + uint32_t input_enabled; + uint32_t output_enabled; + /* Recorder abstract object. */ + SLObjectItf recorderObj; + /* Recorder Itf for input capture. */ + SLRecordItf recorderItf; + /* Buffer queue for input capture. */ + SLAndroidSimpleBufferQueueItf recorderBufferQueueItf; + /* Store input buffers. */ + void ** input_buffer_array; + /* The capacity of the array. + * On capture only can be small (4). + * On full duplex is calculated to + * store 1 sec of data buffers. */ + uint32_t input_array_capacity; + /* Current filled index of input buffer array. + * It is initiated to -1 indicating buffering + * have not started yet. */ + int input_buffer_index; + /* Length of input buffer.*/ + uint32_t input_buffer_length; + /* Input frame size */ + uint32_t input_frame_size; + /* Device sampling rate. If user rate is not + * accepted an compatible rate is set. If it is + * accepted this is equal to params.rate. */ + uint32_t input_device_rate; + /* Exchange input buffers between input + * and full duplex threads. */ + array_queue * input_queue; + /* Silent input buffer used on full duplex. */ + void * input_silent_buffer; + /* Number of input frames from the start of the stream*/ + uint32_t input_total_frames; + /* Flag to stop the execution of user callback and + * close all working threads. Synchronized by + * stream::mutex lock. */ + uint32_t shutdown; + /* Store user callback. */ cubeb_data_callback data_callback; + /* Store state callback. */ cubeb_state_callback state_callback; - void * user_ptr; cubeb_resampler * resampler; - unsigned int inputrate; - unsigned int outputrate; - unsigned int latency; + unsigned int user_output_rate; + unsigned int output_configured_rate; + unsigned int buffer_size_frames; + // Audio output latency used in cubeb_stream_get_position(). + unsigned int output_latency_ms; int64_t lastPosition; int64_t lastPositionTimeStamp; int64_t lastCompensativePosition; + int voice; }; +/* Forward declaration. */ +static int opensl_stop_player(cubeb_stream * stm); +static int opensl_stop_recorder(cubeb_stream * stm); + +static int +opensl_get_draining(cubeb_stream * stm) +{ +#ifdef DEBUG + int r = pthread_mutex_trylock(&stm->mutex); + assert((r == EDEADLK || r == EBUSY) && "get_draining: mutex should be locked but it's not."); +#endif + return stm->draining; +} + +static void +opensl_set_draining(cubeb_stream * stm, int value) +{ +#ifdef DEBUG + int r = pthread_mutex_trylock(&stm->mutex); + LOG("set draining try r = %d", r); + assert((r == EDEADLK || r == EBUSY) && "set_draining: mutex should be locked but it's not."); +#endif + assert(value == 0 || value == 1); + stm->draining = value; +} + +static void +opensl_notify_drained(cubeb_stream * stm) +{ + assert(stm); + int r = pthread_mutex_lock(&stm->mutex); + assert(r == 0); + int draining = opensl_get_draining(stm); + r = pthread_mutex_unlock(&stm->mutex); + assert(r == 0); + if (draining) { + stm->state_callback(stm, stm->user_ptr, CUBEB_STATE_DRAINED); + if (stm->play) { + LOG("stop player in play_callback"); + r = opensl_stop_player(stm); + assert(r == CUBEB_OK); + } + if (stm->recorderItf) { + r = opensl_stop_recorder(stm); + assert(r == CUBEB_OK); + } + } +} + +static uint32_t +opensl_get_shutdown(cubeb_stream * stm) +{ +#ifdef DEBUG + int r = pthread_mutex_trylock(&stm->mutex); + assert((r == EDEADLK || r == EBUSY) && "get_shutdown: mutex should be locked but it's not."); +#endif + return stm->shutdown; +} + +static void +opensl_set_shutdown(cubeb_stream * stm, uint32_t value) +{ +#ifdef DEBUG + int r = pthread_mutex_trylock(&stm->mutex); + LOG("set shutdown try r = %d", r); + assert((r == EDEADLK || r == EBUSY) && "set_shutdown: mutex should be locked but it's not."); +#endif + assert(value == 0 || value == 1); + stm->shutdown = value; +} + static void play_callback(SLPlayItf caller, void * user_ptr, SLuint32 event) { cubeb_stream * stm = user_ptr; - int draining; assert(stm); switch (event) { - case SL_PLAYEVENT_HEADATMARKER: - pthread_mutex_lock(&stm->mutex); - draining = stm->draining; - pthread_mutex_unlock(&stm->mutex); - if (draining) { - stm->state_callback(stm, stm->user_ptr, CUBEB_STATE_DRAINED); - (*stm->play)->SetPlayState(stm->play, SL_PLAYSTATE_PAUSED); - } + case SL_PLAYEVENT_HEADATMARKER: + opensl_notify_drained(stm); break; default: break; @@ -102,96 +257,365 @@ play_callback(SLPlayItf caller, void * user_ptr, SLuint32 event) } static void +recorder_marker_callback (SLRecordItf caller, void * pContext, SLuint32 event) +{ + cubeb_stream * stm = pContext; + assert(stm); + + if (event == SL_RECORDEVENT_HEADATMARKER) { + int r = pthread_mutex_lock(&stm->mutex); + assert(r == 0); + int draining = opensl_get_draining(stm); + r = pthread_mutex_unlock(&stm->mutex); + assert(r == 0); + if (draining) { + stm->state_callback(stm, stm->user_ptr, CUBEB_STATE_DRAINED); + if (stm->recorderItf) { + r = opensl_stop_recorder(stm); + assert(r == CUBEB_OK); + } + if (stm->play) { + r = opensl_stop_player(stm); + assert(r == CUBEB_OK); + } + } + } +} + +static void bufferqueue_callback(SLBufferQueueItf caller, void * user_ptr) { cubeb_stream * stm = user_ptr; assert(stm); SLBufferQueueState state; SLresult res; + long written = 0; res = (*stm->bufq)->GetState(stm->bufq, &state); assert(res == SL_RESULT_SUCCESS); - if (state.count > 1) + if (state.count > 1) { return; + } - SLuint32 i; - for (i = state.count; i < NBUFS; i++) { - uint8_t *buf = stm->queuebuf[stm->queuebuf_idx]; - long written = 0; - pthread_mutex_lock(&stm->mutex); - int draining = stm->draining; - pthread_mutex_unlock(&stm->mutex); - - if (!draining) { - written = cubeb_resampler_fill(stm->resampler, - NULL, NULL, - buf, stm->queuebuf_len / stm->framesize); - if (written < 0 || written * stm->framesize > stm->queuebuf_len) { - (*stm->play)->SetPlayState(stm->play, SL_PLAYSTATE_PAUSED); - return; - } + uint8_t *buf = stm->queuebuf[stm->queuebuf_idx]; + written = 0; + int r = pthread_mutex_lock(&stm->mutex); + assert(r == 0); + int draining = opensl_get_draining(stm); + uint32_t shutdown = opensl_get_shutdown(stm); + r = pthread_mutex_unlock(&stm->mutex); + assert(r == 0); + if (!draining && !shutdown) { + written = cubeb_resampler_fill(stm->resampler, + NULL, NULL, + buf, stm->queuebuf_len / stm->framesize); + LOG("bufferqueue_callback: resampler fill returned %ld frames", written); + if (written < 0 || written * stm->framesize > stm->queuebuf_len) { + r = pthread_mutex_lock(&stm->mutex); + assert(r == 0); + opensl_set_shutdown(stm, 1); + r = pthread_mutex_unlock(&stm->mutex); + assert(r == 0); + opensl_stop_player(stm); + stm->state_callback(stm, stm->user_ptr, CUBEB_STATE_ERROR); + return; } + } - // Keep sending silent data even in draining mode to prevent the audio - // back-end from being stopped automatically by OpenSL/ES. - memset(buf + written * stm->framesize, 0, stm->queuebuf_len - written * stm->framesize); - res = (*stm->bufq)->Enqueue(stm->bufq, buf, stm->queuebuf_len); - assert(res == SL_RESULT_SUCCESS); - stm->queuebuf_idx = (stm->queuebuf_idx + 1) % NBUFS; - if (written > 0) { - pthread_mutex_lock(&stm->mutex); - stm->written += written; - pthread_mutex_unlock(&stm->mutex); - } + // Keep sending silent data even in draining mode to prevent the audio + // back-end from being stopped automatically by OpenSL/ES. + assert(stm->queuebuf_len >= written * stm->framesize); + memset(buf + written * stm->framesize, 0, stm->queuebuf_len - written * stm->framesize); + res = (*stm->bufq)->Enqueue(stm->bufq, buf, stm->queuebuf_len); + assert(res == SL_RESULT_SUCCESS); + stm->queuebuf_idx = (stm->queuebuf_idx + 1) % stm->queuebuf_capacity; - if (!draining && written * stm->framesize < stm->queuebuf_len) { - pthread_mutex_lock(&stm->mutex); - int64_t written_duration = INT64_C(1000) * stm->written * stm->framesize / stm->bytespersec; - stm->draining = 1; - pthread_mutex_unlock(&stm->mutex); + if (written > 0) { + pthread_mutex_lock(&stm->mutex); + stm->written += written; + pthread_mutex_unlock(&stm->mutex); + } + + if (!draining && written * stm->framesize < stm->queuebuf_len) { + LOG("bufferqueue_callback draining"); + r = pthread_mutex_lock(&stm->mutex); + assert(r == 0); + int64_t written_duration = INT64_C(1000) * stm->written * stm->framesize / stm->bytespersec; + opensl_set_draining(stm, 1); + r = pthread_mutex_unlock(&stm->mutex); + assert(r == 0); + + if (written_duration == 0) { + // since we didn't write any sample, it's not possible to reach the marker + // time and trigger the callback. We should initiative notify drained. + opensl_notify_drained(stm); + } else { // Use SL_PLAYEVENT_HEADATMARKER event from slPlayCallback of SLPlayItf // to make sure all the data has been processed. (*stm->play)->SetMarkerPosition(stm->play, (SLmillisecond)written_duration); - return; } + return; } } -#if defined(__ANDROID__) -static SLuint32 -convert_stream_type_to_sl_stream(cubeb_stream_type stream_type) -{ - switch(stream_type) { - case CUBEB_STREAM_TYPE_SYSTEM: - return SL_ANDROID_STREAM_SYSTEM; - case CUBEB_STREAM_TYPE_MUSIC: - return SL_ANDROID_STREAM_MEDIA; - case CUBEB_STREAM_TYPE_NOTIFICATION: - return SL_ANDROID_STREAM_NOTIFICATION; - case CUBEB_STREAM_TYPE_ALARM: - return SL_ANDROID_STREAM_ALARM; - case CUBEB_STREAM_TYPE_VOICE_CALL: - return SL_ANDROID_STREAM_VOICE; - case CUBEB_STREAM_TYPE_RING: - return SL_ANDROID_STREAM_RING; - case CUBEB_STREAM_TYPE_SYSTEM_ENFORCED: - return SL_ANDROID_STREAM_SYSTEM_ENFORCED; - default: - return 0xFFFFFFFF; +static int +opensl_enqueue_recorder(cubeb_stream * stm, void ** last_filled_buffer) +{ + assert(stm); + + int current_index = stm->input_buffer_index; + void * last_buffer = NULL; + + if (current_index < 0) { + // This is the first enqueue + current_index = 0; + } else { + // The current index hold the last filled buffer get it before advance index. + last_buffer = stm->input_buffer_array[current_index]; + // Advance to get next available buffer + current_index = (current_index + 1) % stm->input_array_capacity; + } + // enqueue next empty buffer to be filled by the recorder + SLresult res = (*stm->recorderBufferQueueItf)->Enqueue(stm->recorderBufferQueueItf, + stm->input_buffer_array[current_index], + stm->input_buffer_length); + if (res != SL_RESULT_SUCCESS ) { + LOG("Enqueue recorder failed. Error code: %lu", res); + return CUBEB_ERROR; } + // All good, update buffer and index. + stm->input_buffer_index = current_index; + if (last_filled_buffer) { + *last_filled_buffer = last_buffer; + } + return CUBEB_OK; +} + +// input data callback +void recorder_callback(SLAndroidSimpleBufferQueueItf bq, void * context) +{ + assert(context); + cubeb_stream * stm = context; + assert(stm->recorderBufferQueueItf); + + int r = pthread_mutex_lock(&stm->mutex); + assert(r == 0); + uint32_t shutdown = opensl_get_shutdown(stm); + int draining = opensl_get_draining(stm); + r = pthread_mutex_unlock(&stm->mutex); + assert(r == 0); + + if (shutdown || draining) { + // According to the OpenSL ES 1.1 Specification, 8.14 SLBufferQueueItf + // page 184, on transition to the SL_RECORDSTATE_STOPPED state, + // the application should continue to enqueue buffers onto the queue + // to retrieve the residual recorded data in the system. + r = opensl_enqueue_recorder(stm, NULL); + assert(r == CUBEB_OK); + return; + } + + // Enqueue next available buffer and get the last filled buffer. + void * input_buffer = NULL; + r = opensl_enqueue_recorder(stm, &input_buffer); + assert(r == CUBEB_OK); + assert(input_buffer); + // Fill resampler with last input + long input_frame_count = stm->input_buffer_length / stm->input_frame_size; + long got = cubeb_resampler_fill(stm->resampler, + input_buffer, + &input_frame_count, + NULL, + 0); + // Error case + if (got < 0 || got > input_frame_count) { + r = pthread_mutex_lock(&stm->mutex); + assert(r == 0); + opensl_set_shutdown(stm, 1); + r = pthread_mutex_unlock(&stm->mutex); + assert(r == 0); + r = opensl_stop_recorder(stm); + assert(r == CUBEB_OK); + stm->state_callback(stm, stm->user_ptr, CUBEB_STATE_ERROR); + } + + // Advance total stream frames + stm->input_total_frames += got; + + if (got < input_frame_count) { + r = pthread_mutex_lock(&stm->mutex); + assert(r == 0); + opensl_set_draining(stm, 1); + r = pthread_mutex_unlock(&stm->mutex); + assert(r == 0); + int64_t duration = INT64_C(1000) * stm->input_total_frames / stm->input_device_rate; + (*stm->recorderItf)->SetMarkerPosition(stm->recorderItf, (SLmillisecond)duration); + return; + } +} + +void recorder_fullduplex_callback(SLAndroidSimpleBufferQueueItf bq, void * context) +{ + assert(context); + cubeb_stream * stm = context; + assert(stm->recorderBufferQueueItf); + + int r = pthread_mutex_lock(&stm->mutex); + assert(r == 0); + int draining = opensl_get_draining(stm); + uint32_t shutdown = opensl_get_shutdown(stm); + r = pthread_mutex_unlock(&stm->mutex); + assert(r == 0); + + if (shutdown || draining) { + /* On draining and shutdown the recorder should have been stoped from + * the one set the flags. Accordint to the doc, on transition to + * the SL_RECORDSTATE_STOPPED state, the application should + * continue to enqueue buffers onto the queue to retrieve the residual + * recorded data in the system. */ + LOG("Input shutdown %d or drain %d", shutdown, draining); + int r = opensl_enqueue_recorder(stm, NULL); + assert(r == CUBEB_OK); + return; + } + + // Enqueue next available buffer and get the last filled buffer. + void * input_buffer = NULL; + r = opensl_enqueue_recorder(stm, &input_buffer); + assert(r == CUBEB_OK); + assert(input_buffer); + + assert(stm->input_queue); + r = array_queue_push(stm->input_queue, input_buffer); + if (r == -1) { + LOG("Input queue is full, drop input ..."); + return; + } + + LOG("Input pushed in the queue, input array %zu", + array_queue_get_size(stm->input_queue)); +} + +static void +player_fullduplex_callback(SLBufferQueueItf caller, void * user_ptr) +{ + TIMESTAMP("ENTER"); + cubeb_stream * stm = user_ptr; + assert(stm); + SLresult res; + + int r = pthread_mutex_lock(&stm->mutex); + assert(r == 0); + int draining = opensl_get_draining(stm); + uint32_t shutdown = opensl_get_shutdown(stm); + r = pthread_mutex_unlock(&stm->mutex); + assert(r == 0); + + // Get output + void * output_buffer = NULL; + r = pthread_mutex_lock(&stm->mutex); + assert(r == 0); + output_buffer = stm->queuebuf[stm->queuebuf_idx]; + // Advance the output buffer queue index + stm->queuebuf_idx = (stm->queuebuf_idx + 1) % stm->queuebuf_capacity; + r = pthread_mutex_unlock(&stm->mutex); + assert(r == 0); + + if (shutdown || draining) { + LOG("Shutdown/draining, send silent"); + // Set silent on buffer + memset(output_buffer, 0, stm->queuebuf_len); + + // Enqueue data in player buffer queue + res = (*stm->bufq)->Enqueue(stm->bufq, + output_buffer, + stm->queuebuf_len); + assert(res == SL_RESULT_SUCCESS); + return; + } + + // Get input. + void * input_buffer = array_queue_pop(stm->input_queue); + long input_frame_count = stm->input_buffer_length / stm->input_frame_size; + long frames_needed = stm->queuebuf_len / stm->framesize; + if (!input_buffer) { + LOG("Input hole set silent input buffer"); + input_buffer = stm->input_silent_buffer; + } + + long written = 0; + // Trigger user callback through resampler + written = cubeb_resampler_fill(stm->resampler, + input_buffer, + &input_frame_count, + output_buffer, + frames_needed); + + LOG("Fill: written %ld, frames_needed %ld, input array size %zu", + written, frames_needed, array_queue_get_size(stm->input_queue)); + + if (written < 0 || written > frames_needed) { + // Error case + r = pthread_mutex_lock(&stm->mutex); + assert(r == 0); + opensl_set_shutdown(stm, 1); + r = pthread_mutex_unlock(&stm->mutex); + assert(r == 0); + opensl_stop_player(stm); + opensl_stop_recorder(stm); + stm->state_callback(stm, stm->user_ptr, CUBEB_STATE_ERROR); + memset(output_buffer, 0, stm->queuebuf_len); + + // Enqueue data in player buffer queue + res = (*stm->bufq)->Enqueue(stm->bufq, + output_buffer, + stm->queuebuf_len); + assert(res == SL_RESULT_SUCCESS); + return; + } + + // Advance total out written frames counter + r = pthread_mutex_lock(&stm->mutex); + assert(r == 0); + stm->written += written; + r = pthread_mutex_unlock(&stm->mutex); + assert(r == 0); + + if ( written < frames_needed) { + r = pthread_mutex_lock(&stm->mutex); + assert(r == 0); + int64_t written_duration = INT64_C(1000) * stm->written * stm->framesize / stm->bytespersec; + opensl_set_draining(stm, 1); + r = pthread_mutex_unlock(&stm->mutex); + assert(r == 0); + + // Use SL_PLAYEVENT_HEADATMARKER event from slPlayCallback of SLPlayItf + // to make sure all the data has been processed. + (*stm->play)->SetMarkerPosition(stm->play, (SLmillisecond)written_duration); + } + + // Keep sending silent data even in draining mode to prevent the audio + // back-end from being stopped automatically by OpenSL/ES. + memset((uint8_t *)output_buffer + written * stm->framesize, 0, + stm->queuebuf_len - written * stm->framesize); + + // Enqueue data in player buffer queue + res = (*stm->bufq)->Enqueue(stm->bufq, + output_buffer, + stm->queuebuf_len); + assert(res == SL_RESULT_SUCCESS); + TIMESTAMP("EXIT"); } -#endif static void opensl_destroy(cubeb * ctx); #if defined(__ANDROID__) - #if (__ANDROID_API__ >= ANDROID_VERSION_LOLLIPOP) typedef int (system_property_get)(const char*, char*); static int -__system_property_get(const char* name, char* value) +wrap_system_property_get(const char* name, char* value) { void* libc = dlopen("libc.so", RTLD_LAZY); if (!libc) { @@ -216,14 +640,18 @@ get_android_version(void) memset(version_string, 0, PROP_VALUE_MAX); +#if (__ANDROID_API__ >= ANDROID_VERSION_LOLLIPOP) + int len = wrap_system_property_get("ro.build.version.sdk", version_string); +#else int len = __system_property_get("ro.build.version.sdk", version_string); +#endif if (len <= 0) { LOG("Failed to get Android version!\n"); return len; } int version = (int)strtol(version_string, NULL, 10); - LOG("%d", version); + LOG("Android version %d", version); return version; } #endif @@ -249,30 +677,11 @@ opensl_init(cubeb ** context, char const * context_name) ctx->ops = &opensl_ops; ctx->lib = dlopen("libOpenSLES.so", RTLD_LAZY); - ctx->libmedia = dlopen("libmedia.so", RTLD_LAZY); - if (!ctx->lib || !ctx->libmedia) { + if (!ctx->lib) { free(ctx); return CUBEB_ERROR; } - /* Get the latency, in ms, from AudioFlinger */ - /* status_t AudioSystem::getOutputLatency(uint32_t* latency, - * audio_stream_type_t streamType) */ - /* First, try the most recent signature. */ - ctx->get_output_latency = - dlsym(ctx->libmedia, "_ZN7android11AudioSystem16getOutputLatencyEPj19audio_stream_type_t"); - if (!ctx->get_output_latency) { - /* in case of failure, try the legacy version. */ - /* status_t AudioSystem::getOutputLatency(uint32_t* latency, - * int streamType) */ - ctx->get_output_latency = - dlsym(ctx->libmedia, "_ZN7android11AudioSystem16getOutputLatencyEPji"); - if (!ctx->get_output_latency) { - opensl_destroy(ctx); - return CUBEB_ERROR; - } - } - typedef SLresult (*slCreateEngine_t)(SLObjectItf *, SLuint32, const SLEngineOption *, @@ -287,16 +696,21 @@ opensl_init(cubeb ** context, char const * context_name) ctx->SL_IID_BUFFERQUEUE = *(SLInterfaceID *)dlsym(ctx->lib, "SL_IID_BUFFERQUEUE"); #if defined(__ANDROID__) ctx->SL_IID_ANDROIDCONFIGURATION = *(SLInterfaceID *)dlsym(ctx->lib, "SL_IID_ANDROIDCONFIGURATION"); + ctx->SL_IID_ANDROIDSIMPLEBUFFERQUEUE = *(SLInterfaceID *)dlsym(ctx->lib, "SL_IID_ANDROIDSIMPLEBUFFERQUEUE"); #endif ctx->SL_IID_PLAY = *(SLInterfaceID *)dlsym(ctx->lib, "SL_IID_PLAY"); + ctx->SL_IID_RECORD = *(SLInterfaceID *)dlsym(ctx->lib, "SL_IID_RECORD"); + if (!f_slCreateEngine || !SL_IID_ENGINE || !SL_IID_OUTPUTMIX || !ctx->SL_IID_BUFFERQUEUE || #if defined(__ANDROID__) !ctx->SL_IID_ANDROIDCONFIGURATION || + !ctx->SL_IID_ANDROIDSIMPLEBUFFERQUEUE || #endif - !ctx->SL_IID_PLAY) { + !ctx->SL_IID_PLAY || + !ctx->SL_IID_RECORD) { opensl_destroy(ctx); return CUBEB_ERROR; } @@ -337,8 +751,14 @@ opensl_init(cubeb ** context, char const * context_name) return CUBEB_ERROR; } + ctx->p_output_latency_function = cubeb_output_latency_load_method(android_version); + if (!ctx->p_output_latency_function) { + LOG("Warning: output latency is not available, cubeb_stream_get_position() is not supported"); + } + *context = ctx; + LOG("Cubeb init (%p) success", ctx); return CUBEB_OK; } @@ -359,200 +779,257 @@ opensl_get_max_channel_count(cubeb * ctx, uint32_t * max_channels) return CUBEB_OK; } -static int -opensl_get_preferred_sample_rate(cubeb * ctx, uint32_t * rate) -{ - /* https://android.googlesource.com/platform/ndk.git/+/master/docs/opensles/index.html - * We don't want to deal with JNI here (and we don't have Java on b2g anyways), - * so we just dlopen the library and get the two symbols we need. */ - int r; - void * libmedia; - uint32_t (*get_primary_output_samplingrate)(); - uint32_t (*get_output_samplingrate)(int * samplingRate, int streamType); - - libmedia = dlopen("libmedia.so", RTLD_LAZY); - if (!libmedia) { - return CUBEB_ERROR; - } - - /* uint32_t AudioSystem::getPrimaryOutputSamplingRate(void) */ - get_primary_output_samplingrate = - dlsym(libmedia, "_ZN7android11AudioSystem28getPrimaryOutputSamplingRateEv"); - if (!get_primary_output_samplingrate) { - /* fallback to - * status_t AudioSystem::getOutputSamplingRate(int* samplingRate, int streamType) - * if we cannot find getPrimaryOutputSamplingRate. */ - get_output_samplingrate = - dlsym(libmedia, "_ZN7android11AudioSystem21getOutputSamplingRateEPj19audio_stream_type_t"); - if (!get_output_samplingrate) { - /* Another signature exists, with a int instead of an audio_stream_type_t */ - get_output_samplingrate = - dlsym(libmedia, "_ZN7android11AudioSystem21getOutputSamplingRateEPii"); - if (!get_output_samplingrate) { - dlclose(libmedia); - return CUBEB_ERROR; - } - } - } +static void +opensl_destroy(cubeb * ctx) +{ + if (ctx->outmixObj) + (*ctx->outmixObj)->Destroy(ctx->outmixObj); + if (ctx->engObj) + cubeb_destroy_sles_engine(&ctx->engObj); + dlclose(ctx->lib); + if (ctx->p_output_latency_function) + cubeb_output_latency_unload_method(ctx->p_output_latency_function); + free(ctx); +} - if (get_primary_output_samplingrate) { - *rate = get_primary_output_samplingrate(); - } else { - /* We don't really know about the type, here, so we just pass music. */ - r = get_output_samplingrate((int *) rate, AUDIO_STREAM_TYPE_MUSIC); - if (r) { - dlclose(libmedia); - return CUBEB_ERROR; - } - } +static void opensl_stream_destroy(cubeb_stream * stm); - dlclose(libmedia); +static int +opensl_set_format(SLDataFormat_PCM * format, cubeb_stream_params * params) +{ + assert(format); + assert(params); - /* Depending on which method we called above, we can get a zero back, yet have - * a non-error return value, especially if the audio system is not - * ready/shutting down (i.e. when we can't get our hand on the AudioFlinger - * thread). */ - if (*rate == 0) { - return CUBEB_ERROR; + format->formatType = SL_DATAFORMAT_PCM; + format->numChannels = params->channels; + // samplesPerSec is in milliHertz + format->samplesPerSec = params->rate * 1000; + format->bitsPerSample = SL_PCMSAMPLEFORMAT_FIXED_16; + format->containerSize = SL_PCMSAMPLEFORMAT_FIXED_16; + format->channelMask = params->channels == 1 ? + SL_SPEAKER_FRONT_CENTER : + SL_SPEAKER_FRONT_LEFT | SL_SPEAKER_FRONT_RIGHT; + + switch (params->format) { + case CUBEB_SAMPLE_S16LE: + format->endianness = SL_BYTEORDER_LITTLEENDIAN; + break; + case CUBEB_SAMPLE_S16BE: + format->endianness = SL_BYTEORDER_BIGENDIAN; + break; + default: + return CUBEB_ERROR_INVALID_FORMAT; } - return CUBEB_OK; } static int -opensl_get_min_latency(cubeb * ctx, cubeb_stream_params params, uint32_t * latency_frames) +opensl_configure_capture(cubeb_stream * stm, cubeb_stream_params * params) { - /* https://android.googlesource.com/platform/ndk.git/+/master/docs/opensles/index.html - * We don't want to deal with JNI here (and we don't have Java on b2g anyways), - * so we just dlopen the library and get the two symbols we need. */ - - int r; - void * libmedia; - size_t (*get_primary_output_frame_count)(void); - int (*get_output_frame_count)(size_t * frameCount, int streamType); - uint32_t primary_sampling_rate; - size_t primary_buffer_size; + assert(stm); + assert(params); - r = opensl_get_preferred_sample_rate(ctx, &primary_sampling_rate); + SLDataLocator_AndroidSimpleBufferQueue lDataLocatorOut; + lDataLocatorOut.locatorType = SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE; + lDataLocatorOut.numBuffers = NBUFS; - if (r) { - return CUBEB_ERROR; + SLDataFormat_PCM lDataFormat; + int r = opensl_set_format(&lDataFormat, params); + if (r != CUBEB_OK) { + return CUBEB_ERROR_INVALID_FORMAT; } - libmedia = dlopen("libmedia.so", RTLD_LAZY); - if (!libmedia) { - return CUBEB_ERROR; - } + /* For now set device rate to params rate. */ + stm->input_device_rate = params->rate; + + SLDataSink lDataSink; + lDataSink.pLocator = &lDataLocatorOut; + lDataSink.pFormat = &lDataFormat; + + SLDataLocator_IODevice lDataLocatorIn; + lDataLocatorIn.locatorType = SL_DATALOCATOR_IODEVICE; + lDataLocatorIn.deviceType = SL_IODEVICE_AUDIOINPUT; + lDataLocatorIn.deviceID = SL_DEFAULTDEVICEID_AUDIOINPUT; + lDataLocatorIn.device = NULL; + + SLDataSource lDataSource; + lDataSource.pLocator = &lDataLocatorIn; + lDataSource.pFormat = NULL; + + const SLInterfaceID lSoundRecorderIIDs[] = { stm->context->SL_IID_RECORD, + stm->context->SL_IID_ANDROIDSIMPLEBUFFERQUEUE, + stm->context->SL_IID_ANDROIDCONFIGURATION }; + + const SLboolean lSoundRecorderReqs[] = { SL_BOOLEAN_TRUE, SL_BOOLEAN_TRUE, SL_BOOLEAN_TRUE }; + // create the audio recorder abstract object + SLresult res = (*stm->context->eng)->CreateAudioRecorder(stm->context->eng, + &stm->recorderObj, + &lDataSource, + &lDataSink, + NELEMS(lSoundRecorderIIDs), + lSoundRecorderIIDs, + lSoundRecorderReqs); + // Sample rate not supported. Try again with default sample rate! + if (res == SL_RESULT_CONTENT_UNSUPPORTED) { + if (stm->output_enabled && stm->output_configured_rate != 0) { + // Set the same with the player. Since there is no + // api for input device this is a safe choice. + stm->input_device_rate = stm->output_configured_rate; + } else { + // The output preferred rate is used for an input only scenario. + // The default rate expected to be supported from all android devices. + stm->input_device_rate = DEFAULT_SAMPLE_RATE; + } + lDataFormat.samplesPerSec = stm->input_device_rate * 1000; + res = (*stm->context->eng)->CreateAudioRecorder(stm->context->eng, + &stm->recorderObj, + &lDataSource, + &lDataSink, + NELEMS(lSoundRecorderIIDs), + lSoundRecorderIIDs, + lSoundRecorderReqs); - /* JB variant */ - /* size_t AudioSystem::getPrimaryOutputFrameCount(void) */ - get_primary_output_frame_count = - dlsym(libmedia, "_ZN7android11AudioSystem26getPrimaryOutputFrameCountEv"); - if (!get_primary_output_frame_count) { - /* ICS variant */ - /* status_t AudioSystem::getOutputFrameCount(int* frameCount, int streamType) */ - get_output_frame_count = - dlsym(libmedia, "_ZN7android11AudioSystem19getOutputFrameCountEPii"); - if (!get_output_frame_count) { - dlclose(libmedia); + if (res != SL_RESULT_SUCCESS) { + LOG("Failed to create recorder. Error code: %lu", res); return CUBEB_ERROR; } } - if (get_primary_output_frame_count) { - primary_buffer_size = get_primary_output_frame_count(); - } else { - if (get_output_frame_count(&primary_buffer_size, params.stream_type) != 0) { + + if (get_android_version() > ANDROID_VERSION_JELLY_BEAN) { + SLAndroidConfigurationItf recorderConfig; + res = (*stm->recorderObj) + ->GetInterface(stm->recorderObj, + stm->context->SL_IID_ANDROIDCONFIGURATION, + &recorderConfig); + + if (res != SL_RESULT_SUCCESS) { + LOG("Failed to get the android configuration interface for recorder. Error " + "code: %lu", + res); return CUBEB_ERROR; } - } - /* To get a fast track in Android's mixer, we need to be at the native - * samplerate, which is device dependant. Some devices might be able to - * resample when playing a fast track, but it's pretty rare. */ - *latency_frames = NBUFS * primary_buffer_size; + // Voice recognition is the lowest latency, according to the docs. Camcorder + // uses a microphone that is in the same direction as the camera. + SLint32 streamType = stm->voice ? SL_ANDROID_RECORDING_PRESET_VOICE_RECOGNITION + : SL_ANDROID_RECORDING_PRESET_CAMCORDER; - dlclose(libmedia); + res = (*recorderConfig) + ->SetConfiguration(recorderConfig, SL_ANDROID_KEY_RECORDING_PRESET, + &streamType, sizeof(SLint32)); - return CUBEB_OK; -} + if (res != SL_RESULT_SUCCESS) { + LOG("Failed to set the android configuration to VOICE for the recorder. " + "Error code: %lu", res); + return CUBEB_ERROR; + } + } + // realize the audio recorder + res = (*stm->recorderObj)->Realize(stm->recorderObj, SL_BOOLEAN_FALSE); + if (res != SL_RESULT_SUCCESS) { + LOG("Failed to realize recorder. Error code: %lu", res); + return CUBEB_ERROR; + } + // get the record interface + res = (*stm->recorderObj)->GetInterface(stm->recorderObj, + stm->context->SL_IID_RECORD, + &stm->recorderItf); + if (res != SL_RESULT_SUCCESS) { + LOG("Failed to get recorder interface. Error code: %lu", res); + return CUBEB_ERROR; + } -static void -opensl_destroy(cubeb * ctx) -{ - if (ctx->outmixObj) - (*ctx->outmixObj)->Destroy(ctx->outmixObj); - if (ctx->engObj) - cubeb_destroy_sles_engine(&ctx->engObj); - dlclose(ctx->lib); - dlclose(ctx->libmedia); - free(ctx); -} + res = (*stm->recorderItf)->RegisterCallback(stm->recorderItf, recorder_marker_callback, stm); + if (res != SL_RESULT_SUCCESS) { + LOG("Failed to register recorder marker callback. Error code: %lu", res); + return CUBEB_ERROR; + } -static void opensl_stream_destroy(cubeb_stream * stm); + (*stm->recorderItf)->SetMarkerPosition(stm->recorderItf, (SLmillisecond)0); -static int -opensl_stream_init(cubeb * ctx, 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) -{ - cubeb_stream * stm; + res = (*stm->recorderItf)->SetCallbackEventsMask(stm->recorderItf, (SLuint32)SL_RECORDEVENT_HEADATMARKER); + if (res != SL_RESULT_SUCCESS) { + LOG("Failed to set headatmarker event mask. Error code: %lu", res); + return CUBEB_ERROR; + } + // get the simple android buffer queue interface + res = (*stm->recorderObj)->GetInterface(stm->recorderObj, + stm->context->SL_IID_ANDROIDSIMPLEBUFFERQUEUE, + &stm->recorderBufferQueueItf); + if (res != SL_RESULT_SUCCESS) { + LOG("Failed to get recorder (android) buffer queue interface. Error code: %lu", res); + return CUBEB_ERROR; + } - assert(ctx); - assert(!input_stream_params && "not supported"); - if (input_device || output_device) { - /* Device selection not yet implemented. */ - return CUBEB_ERROR_DEVICE_UNAVAILABLE; + // register callback on record (input) buffer queue + slAndroidSimpleBufferQueueCallback rec_callback = recorder_callback; + if (stm->output_enabled) { + // Register full duplex callback instead. + rec_callback = recorder_fullduplex_callback; + } + res = (*stm->recorderBufferQueueItf)->RegisterCallback(stm->recorderBufferQueueItf, + rec_callback, + stm); + if (res != SL_RESULT_SUCCESS) { + LOG("Failed to register recorder buffer queue callback. Error code: %lu", res); + return CUBEB_ERROR; } - *stream = NULL; + // Calculate length of input buffer according to requested latency + stm->input_frame_size = params->channels * sizeof(int16_t); + stm->input_buffer_length = (stm->input_frame_size * stm->buffer_size_frames); - SLDataFormat_PCM format; + // Calculate the capacity of input array + stm->input_array_capacity = NBUFS; + if (stm->output_enabled) { + // Full duplex, update capacity to hold 1 sec of data + stm->input_array_capacity = 1 * stm->input_device_rate / stm->input_buffer_length; + } + // Allocate input array + stm->input_buffer_array = (void**)calloc(1, sizeof(void*)*stm->input_array_capacity); + // Buffering has not started yet. + stm->input_buffer_index = -1; + // Prepare input buffers + for(uint32_t i = 0; i < stm->input_array_capacity; ++i) { + stm->input_buffer_array[i] = calloc(1, stm->input_buffer_length); + } - format.formatType = SL_DATAFORMAT_PCM; - format.numChannels = output_stream_params->channels; - // samplesPerSec is in milliHertz - format.samplesPerSec = output_stream_params->rate * 1000; - format.bitsPerSample = SL_PCMSAMPLEFORMAT_FIXED_16; - format.containerSize = SL_PCMSAMPLEFORMAT_FIXED_16; - format.channelMask = output_stream_params->channels == 1 ? - SL_SPEAKER_FRONT_CENTER : - SL_SPEAKER_FRONT_LEFT | SL_SPEAKER_FRONT_RIGHT; - - switch (output_stream_params->format) { - case CUBEB_SAMPLE_S16LE: - format.endianness = SL_BYTEORDER_LITTLEENDIAN; - break; - case CUBEB_SAMPLE_S16BE: - format.endianness = SL_BYTEORDER_BIGENDIAN; - break; - default: - return CUBEB_ERROR_INVALID_FORMAT; + // On full duplex allocate input queue and silent buffer + if (stm->output_enabled) { + stm->input_queue = array_queue_create(stm->input_array_capacity); + assert(stm->input_queue); + stm->input_silent_buffer = calloc(1, stm->input_buffer_length); + assert(stm->input_silent_buffer); } - stm = calloc(1, sizeof(*stm)); - assert(stm); + // Enqueue buffer to start rolling once recorder started + r = opensl_enqueue_recorder(stm, NULL); + if (r != CUBEB_OK) { + return r; + } - stm->context = ctx; - stm->data_callback = data_callback; - stm->state_callback = state_callback; - stm->user_ptr = user_ptr; + LOG("Cubeb stream init recorder success"); - stm->inputrate = output_stream_params->rate; - stm->latency = latency_frames; - stm->stream_type = output_stream_params->stream_type; - stm->framesize = output_stream_params->channels * sizeof(int16_t); + return CUBEB_OK; +} + +static int +opensl_configure_playback(cubeb_stream * stm, cubeb_stream_params * params) { + assert(stm); + assert(params); + + stm->user_output_rate = params->rate; + stm->framesize = params->channels * sizeof(int16_t); stm->lastPosition = -1; stm->lastPositionTimeStamp = 0; stm->lastCompensativePosition = -1; - int r = pthread_mutex_init(&stm->mutex, NULL); - assert(r == 0); + SLDataFormat_PCM format; + int r = opensl_set_format(&format, params); + if (r != CUBEB_OK) { + return CUBEB_ERROR_INVALID_FORMAT; + } SLDataLocator_BufferQueue loc_bufq; loc_bufq.locatorType = SL_DATALOCATOR_BUFFERQUEUE; @@ -563,15 +1040,15 @@ opensl_stream_init(cubeb * ctx, cubeb_stream ** stream, char const * stream_name SLDataLocator_OutputMix loc_outmix; loc_outmix.locatorType = SL_DATALOCATOR_OUTPUTMIX; - loc_outmix.outputMix = ctx->outmixObj; + loc_outmix.outputMix = stm->context->outmixObj; SLDataSink sink; sink.pLocator = &loc_outmix; sink.pFormat = NULL; #if defined(__ANDROID__) - const SLInterfaceID ids[] = {ctx->SL_IID_BUFFERQUEUE, - ctx->SL_IID_VOLUME, - ctx->SL_IID_ANDROIDCONFIGURATION}; + const SLInterfaceID ids[] = {stm->context->SL_IID_BUFFERQUEUE, + stm->context->SL_IID_VOLUME, + stm->context->SL_IID_ANDROIDCONFIGURATION}; const SLboolean req[] = {SL_BOOLEAN_TRUE, SL_BOOLEAN_TRUE, SL_BOOLEAN_TRUE}; #else const SLInterfaceID ids[] = {ctx->SL_IID_BUFFERQUEUE, ctx->SL_IID_VOLUME}; @@ -579,115 +1056,166 @@ opensl_stream_init(cubeb * ctx, cubeb_stream ** stream, char const * stream_name #endif assert(NELEMS(ids) == NELEMS(req)); - uint32_t preferred_sampling_rate = stm->inputrate; -#if defined(__ANDROID__) - if (get_android_version() >= ANDROID_VERSION_MARSHMALLOW) { - // Reset preferred samping rate to trigger fallback to native sampling rate. - preferred_sampling_rate = 0; - if (opensl_get_min_latency(ctx, *output_stream_params, &latency_frames) != CUBEB_OK) { - // Default to AudioFlinger's advertised fast track latency of 10ms. - latency_frames = 440; - } - stm->latency = latency_frames; - } -#endif - + uint32_t preferred_sampling_rate = stm->user_output_rate; SLresult res = SL_RESULT_CONTENT_UNSUPPORTED; if (preferred_sampling_rate) { - res = (*ctx->eng)->CreateAudioPlayer(ctx->eng, &stm->playerObj, &source, - &sink, NELEMS(ids), ids, req); + res = (*stm->context->eng)->CreateAudioPlayer(stm->context->eng, + &stm->playerObj, + &source, + &sink, + NELEMS(ids), + ids, + req); } // Sample rate not supported? Try again with primary sample rate! - if (res == SL_RESULT_CONTENT_UNSUPPORTED) { - if (opensl_get_preferred_sample_rate(ctx, &preferred_sampling_rate)) { - opensl_stream_destroy(stm); - return CUBEB_ERROR; - } - + if (res == SL_RESULT_CONTENT_UNSUPPORTED && + preferred_sampling_rate != DEFAULT_SAMPLE_RATE) { + preferred_sampling_rate = DEFAULT_SAMPLE_RATE; format.samplesPerSec = preferred_sampling_rate * 1000; - res = (*ctx->eng)->CreateAudioPlayer(ctx->eng, &stm->playerObj, - &source, &sink, NELEMS(ids), ids, req); + res = (*stm->context->eng)->CreateAudioPlayer(stm->context->eng, + &stm->playerObj, + &source, + &sink, + NELEMS(ids), + ids, + req); } if (res != SL_RESULT_SUCCESS) { - opensl_stream_destroy(stm); + LOG("Failed to create audio player. Error code: %lu", res); return CUBEB_ERROR; } - stm->outputrate = preferred_sampling_rate; - stm->bytespersec = stm->outputrate * stm->framesize; - stm->queuebuf_len = stm->framesize * latency_frames / NBUFS; - // round up to the next multiple of stm->framesize, if needed. - if (stm->queuebuf_len % stm->framesize) { - stm->queuebuf_len += stm->framesize - (stm->queuebuf_len % stm->framesize); + stm->output_configured_rate = preferred_sampling_rate; + stm->bytespersec = stm->output_configured_rate * stm->framesize; + stm->queuebuf_len = stm->framesize * stm->buffer_size_frames; + + // Calculate the capacity of input array + stm->queuebuf_capacity = NBUFS; + if (stm->output_enabled) { + // Full duplex, update capacity to hold 1 sec of data + stm->queuebuf_capacity = 1 * stm->output_configured_rate / stm->queuebuf_len; + } + // Allocate input array + stm->queuebuf = (void**)calloc(1, sizeof(void*) * stm->queuebuf_capacity); + for (uint32_t i = 0; i < stm->queuebuf_capacity; ++i) { + stm->queuebuf[i] = calloc(1, stm->queuebuf_len); + assert(stm->queuebuf[i]); } - cubeb_stream_params params = *output_stream_params; - params.rate = preferred_sampling_rate; + SLAndroidConfigurationItf playerConfig = NULL; - stm->resampler = cubeb_resampler_create(stm, NULL, ¶ms, - output_stream_params->rate, - data_callback, - user_ptr, - CUBEB_RESAMPLER_QUALITY_DEFAULT); + if (get_android_version() >= ANDROID_VERSION_N_MR1) { + res = (*stm->playerObj) + ->GetInterface(stm->playerObj, + stm->context->SL_IID_ANDROIDCONFIGURATION, + &playerConfig); + if (res != SL_RESULT_SUCCESS) { + LOG("Failed to get Android configuration interface. Error code: %lu", res); + return CUBEB_ERROR; + } - if (!stm->resampler) { - opensl_stream_destroy(stm); - return CUBEB_ERROR; - } + SLint32 streamType = SL_ANDROID_STREAM_MEDIA; + if (stm->voice) { + streamType = SL_ANDROID_STREAM_VOICE; + } + res = (*playerConfig)->SetConfiguration(playerConfig, + SL_ANDROID_KEY_STREAM_TYPE, + &streamType, + sizeof(streamType)); + if (res != SL_RESULT_SUCCESS) { + LOG("Failed to set Android configuration to %d Error code: %lu", + streamType, res); + } - int i; - for (i = 0; i < NBUFS; i++) { - stm->queuebuf[i] = malloc(stm->queuebuf_len); - assert(stm->queuebuf[i]); - } + SLuint32 performanceMode = SL_ANDROID_PERFORMANCE_LATENCY; + if (stm->buffer_size_frames > POWERSAVE_LATENCY_FRAMES_THRESHOLD) { + performanceMode = SL_ANDROID_PERFORMANCE_POWER_SAVING; + } -#if defined(__ANDROID__) - SLuint32 stream_type = convert_stream_type_to_sl_stream(output_stream_params->stream_type); - if (stream_type != 0xFFFFFFFF) { - SLAndroidConfigurationItf playerConfig; - res = (*stm->playerObj)->GetInterface(stm->playerObj, - ctx->SL_IID_ANDROIDCONFIGURATION, &playerConfig); res = (*playerConfig)->SetConfiguration(playerConfig, - SL_ANDROID_KEY_STREAM_TYPE, &stream_type, sizeof(SLint32)); + SL_ANDROID_KEY_PERFORMANCE_MODE, + &performanceMode, + sizeof(performanceMode)); if (res != SL_RESULT_SUCCESS) { - opensl_stream_destroy(stm); - return CUBEB_ERROR; + LOG("Failed to set Android performance mode to %d Error code: %lu. This is" + " not fatal", performanceMode, res); } } -#endif res = (*stm->playerObj)->Realize(stm->playerObj, SL_BOOLEAN_FALSE); if (res != SL_RESULT_SUCCESS) { - opensl_stream_destroy(stm); + LOG("Failed to realize player object. Error code: %lu", res); return CUBEB_ERROR; } - res = (*stm->playerObj)->GetInterface(stm->playerObj, ctx->SL_IID_PLAY, &stm->play); + // There are two ways of getting the audio output latency: + // - a configuration value, only available on some devices (notably devices + // running FireOS) + // - A Java method, that we call using JNI. + // + // The first method is prefered, if available, because it can account for more + // latency causes, and is more precise. + + // Latency has to be queried after the realization of the interface, when + // using SL_IID_ANDROIDCONFIGURATION. + SLuint32 audioLatency = 0; + SLuint32 paramSize = sizeof(SLuint32); + // The reported latency is in milliseconds. + if (playerConfig) { + res = (*playerConfig)->GetConfiguration(playerConfig, + (const SLchar *)"androidGetAudioLatency", + ¶mSize, + &audioLatency); + if (res == SL_RESULT_SUCCESS) { + LOG("Got playback latency using android configuration extension"); + stm->output_latency_ms = audioLatency; + } + } + // `playerConfig` is available, but the above failed, or `playerConfig` is not + // available. In both cases, we need to acquire the output latency by an other + // mean. + if ((playerConfig && res != SL_RESULT_SUCCESS) || + !playerConfig) { + if (cubeb_output_latency_method_is_loaded(stm->context->p_output_latency_function)) { + LOG("Got playback latency using JNI"); + stm->output_latency_ms = cubeb_get_output_latency(stm->context->p_output_latency_function); + } else { + LOG("No alternate latency querying method loaded, A/V sync will be off."); + stm->output_latency_ms = 0; + } + } + + LOG("Audio output latency: %dms", stm->output_latency_ms); + + res = (*stm->playerObj)->GetInterface(stm->playerObj, + stm->context->SL_IID_PLAY, + &stm->play); if (res != SL_RESULT_SUCCESS) { - opensl_stream_destroy(stm); + LOG("Failed to get play interface. Error code: %lu", res); return CUBEB_ERROR; } - res = (*stm->playerObj)->GetInterface(stm->playerObj, ctx->SL_IID_BUFFERQUEUE, + res = (*stm->playerObj)->GetInterface(stm->playerObj, + stm->context->SL_IID_BUFFERQUEUE, &stm->bufq); if (res != SL_RESULT_SUCCESS) { - opensl_stream_destroy(stm); + LOG("Failed to get bufferqueue interface. Error code: %lu", res); return CUBEB_ERROR; } - res = (*stm->playerObj)->GetInterface(stm->playerObj, ctx->SL_IID_VOLUME, + res = (*stm->playerObj)->GetInterface(stm->playerObj, + stm->context->SL_IID_VOLUME, &stm->volume); - if (res != SL_RESULT_SUCCESS) { - opensl_stream_destroy(stm); + LOG("Failed to get volume interface. Error code: %lu", res); return CUBEB_ERROR; } res = (*stm->play)->RegisterCallback(stm->play, play_callback, stm); if (res != SL_RESULT_SUCCESS) { - opensl_stream_destroy(stm); + LOG("Failed to register play callback. Error code: %lu", res); return CUBEB_ERROR; } @@ -696,13 +1224,17 @@ opensl_stream_init(cubeb * ctx, cubeb_stream ** stream, char const * stream_name res = (*stm->play)->SetCallbackEventsMask(stm->play, (SLuint32)SL_PLAYEVENT_HEADATMARKER); if (res != SL_RESULT_SUCCESS) { - opensl_stream_destroy(stm); + LOG("Failed to set headatmarker event mask. Error code: %lu", res); return CUBEB_ERROR; } - res = (*stm->bufq)->RegisterCallback(stm->bufq, bufferqueue_callback, stm); + slBufferQueueCallback player_callback = bufferqueue_callback; + if (stm->input_enabled) { + player_callback = player_fullduplex_callback; + } + res = (*stm->bufq)->RegisterCallback(stm->bufq, player_callback, stm); if (res != SL_RESULT_SUCCESS) { - opensl_stream_destroy(stm); + LOG("Failed to register bufferqueue callback. Error code: %lu", res); return CUBEB_ERROR; } @@ -717,55 +1249,340 @@ opensl_stream_init(cubeb * ctx, cubeb_stream ** stream, char const * stream_name assert(res == SL_RESULT_SUCCESS); } - *stream = stm; + LOG("Cubeb stream init playback success"); return CUBEB_OK; } -static void -opensl_stream_destroy(cubeb_stream * stm) +static int +opensl_validate_stream_param(cubeb_stream_params * stream_params) { - if (stm->playerObj) - (*stm->playerObj)->Destroy(stm->playerObj); - int i; - for (i = 0; i < NBUFS; i++) { - free(stm->queuebuf[i]); + if ((stream_params && + (stream_params->channels < 1 || stream_params->channels > 32))) { + return CUBEB_ERROR_INVALID_FORMAT; } - pthread_mutex_destroy(&stm->mutex); + if ((stream_params && + (stream_params->prefs & CUBEB_STREAM_PREF_LOOPBACK))) { + LOG("Loopback is not supported"); + return CUBEB_ERROR_NOT_SUPPORTED; + } + return CUBEB_OK; +} - cubeb_resampler_destroy(stm->resampler); +int has_pref_set(cubeb_stream_params* input_params, + cubeb_stream_params* output_params, + cubeb_stream_prefs pref) +{ + return (input_params && input_params->prefs & pref) || + (output_params && output_params->prefs & pref); +} - free(stm); +static int +opensl_stream_init(cubeb * ctx, 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) +{ + cubeb_stream * stm; + + assert(ctx); + if (input_device || output_device) { + LOG("Device selection is not supported in Android. The default will be used"); + } + + *stream = NULL; + + int r = opensl_validate_stream_param(output_stream_params); + if(r != CUBEB_OK) { + LOG("Output stream params not valid"); + return r; + } + r = opensl_validate_stream_param(input_stream_params); + if(r != CUBEB_OK) { + LOG("Input stream params not valid"); + return r; + } + + stm = calloc(1, sizeof(*stm)); + assert(stm); + + stm->context = ctx; + stm->data_callback = data_callback; + stm->state_callback = state_callback; + stm->user_ptr = user_ptr; + stm->buffer_size_frames = latency_frames ? latency_frames : DEFAULT_NUM_OF_FRAMES; + stm->input_enabled = (input_stream_params) ? 1 : 0; + stm->output_enabled = (output_stream_params) ? 1 : 0; + stm->shutdown = 1; + stm->voice = has_pref_set(input_stream_params, output_stream_params, CUBEB_STREAM_PREF_VOICE); + + LOG("cubeb stream prefs: voice: %s", stm->voice ? "true" : "false"); + + +#ifdef DEBUG + pthread_mutexattr_t attr; + pthread_mutexattr_init(&attr); + pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_ERRORCHECK); + r = pthread_mutex_init(&stm->mutex, &attr); +#else + r = pthread_mutex_init(&stm->mutex, NULL); +#endif + assert(r == 0); + + if (output_stream_params) { + LOG("Playback params: Rate %d, channels %d, format %d, latency in frames %d.", + output_stream_params->rate, output_stream_params->channels, + output_stream_params->format, stm->buffer_size_frames); + r = opensl_configure_playback(stm, output_stream_params); + if (r != CUBEB_OK) { + opensl_stream_destroy(stm); + return r; + } + } + + if (input_stream_params) { + LOG("Capture params: Rate %d, channels %d, format %d, latency in frames %d.", + input_stream_params->rate, input_stream_params->channels, + input_stream_params->format, stm->buffer_size_frames); + r = opensl_configure_capture(stm, input_stream_params); + if (r != CUBEB_OK) { + opensl_stream_destroy(stm); + return r; + } + } + + /* Configure resampler*/ + uint32_t target_sample_rate; + if (input_stream_params) { + target_sample_rate = input_stream_params->rate; + } else { + assert(output_stream_params); + target_sample_rate = output_stream_params->rate; + } + + // Use the actual configured rates for input + // and output. + cubeb_stream_params input_params; + if (input_stream_params) { + input_params = *input_stream_params; + input_params.rate = stm->input_device_rate; + } + cubeb_stream_params output_params; + if (output_stream_params) { + output_params = *output_stream_params; + output_params.rate = stm->output_configured_rate; + } + + stm->resampler = cubeb_resampler_create(stm, + input_stream_params ? &input_params : NULL, + output_stream_params ? &output_params : NULL, + target_sample_rate, + data_callback, + user_ptr, + CUBEB_RESAMPLER_QUALITY_DEFAULT); + if (!stm->resampler) { + LOG("Failed to create resampler"); + opensl_stream_destroy(stm); + return CUBEB_ERROR; + } + + *stream = stm; + LOG("Cubeb stream (%p) init success", stm); + return CUBEB_OK; +} + +static int +opensl_start_player(cubeb_stream * stm) +{ + assert(stm->playerObj); + SLuint32 playerState; + (*stm->playerObj)->GetState(stm->playerObj, &playerState); + if (playerState == SL_OBJECT_STATE_REALIZED) { + SLresult res = (*stm->play)->SetPlayState(stm->play, SL_PLAYSTATE_PLAYING); + if(res != SL_RESULT_SUCCESS) { + LOG("Failed to start player. Error code: %lu", res); + return CUBEB_ERROR; + } + } + return CUBEB_OK; +} + +static int +opensl_start_recorder(cubeb_stream * stm) +{ + assert(stm->recorderObj); + SLuint32 recorderState; + (*stm->recorderObj)->GetState(stm->recorderObj, &recorderState); + if (recorderState == SL_OBJECT_STATE_REALIZED) { + SLresult res = (*stm->recorderItf)->SetRecordState(stm->recorderItf, SL_RECORDSTATE_RECORDING); + if(res != SL_RESULT_SUCCESS) { + LOG("Failed to start recorder. Error code: %lu", res); + return CUBEB_ERROR; + } + } + return CUBEB_OK; } static int opensl_stream_start(cubeb_stream * stm) { - SLresult res = (*stm->play)->SetPlayState(stm->play, SL_PLAYSTATE_PLAYING); - if (res != SL_RESULT_SUCCESS) - return CUBEB_ERROR; + assert(stm); + + int r = pthread_mutex_lock(&stm->mutex); + assert(r == 0); + opensl_set_shutdown(stm, 0); + opensl_set_draining(stm, 0); + r = pthread_mutex_unlock(&stm->mutex); + assert(r == 0); + + if (stm->playerObj) { + r = opensl_start_player(stm); + if (r != CUBEB_OK) { + return r; + } + } + + if (stm->recorderObj) { + int r = opensl_start_recorder(stm); + if (r != CUBEB_OK) { + return r; + } + } + stm->state_callback(stm, stm->user_ptr, CUBEB_STATE_STARTED); + LOG("Cubeb stream (%p) started", stm); return CUBEB_OK; } static int -opensl_stream_stop(cubeb_stream * stm) +opensl_stop_player(cubeb_stream * stm) { + assert(stm->playerObj); + assert(stm->shutdown || stm->draining); + SLresult res = (*stm->play)->SetPlayState(stm->play, SL_PLAYSTATE_PAUSED); - if (res != SL_RESULT_SUCCESS) + if (res != SL_RESULT_SUCCESS) { + LOG("Failed to stop player. Error code: %lu", res); + return CUBEB_ERROR; + } + + return CUBEB_OK; +} + +static int +opensl_stop_recorder(cubeb_stream * stm) +{ + assert(stm->recorderObj); + assert(stm->shutdown || stm->draining); + + SLresult res = (*stm->recorderItf)->SetRecordState(stm->recorderItf, SL_RECORDSTATE_PAUSED); + if (res != SL_RESULT_SUCCESS) { + LOG("Failed to stop recorder. Error code: %lu", res); return CUBEB_ERROR; + } + + return CUBEB_OK; +} + +static int +opensl_stream_stop(cubeb_stream * stm) +{ + assert(stm); + + int r = pthread_mutex_lock(&stm->mutex); + assert(r == 0); + opensl_set_shutdown(stm, 1); + r = pthread_mutex_unlock(&stm->mutex); + assert(r == 0); + + if (stm->playerObj) { + r = opensl_stop_player(stm); + if (r != CUBEB_OK) { + return r; + } + } + + if (stm->recorderObj) { + int r = opensl_stop_recorder(stm); + if (r != CUBEB_OK) { + return r; + } + } + stm->state_callback(stm, stm->user_ptr, CUBEB_STATE_STOPPED); + LOG("Cubeb stream (%p) stopped", stm); return CUBEB_OK; } static int +opensl_destroy_recorder(cubeb_stream * stm) +{ + assert(stm); + assert(stm->recorderObj); + + if (stm->recorderBufferQueueItf) { + SLresult res = (*stm->recorderBufferQueueItf)->Clear(stm->recorderBufferQueueItf); + if (res != SL_RESULT_SUCCESS) { + LOG("Failed to clear recorder buffer queue. Error code: %lu", res); + return CUBEB_ERROR; + } + stm->recorderBufferQueueItf = NULL; + for (uint32_t i = 0; i < stm->input_array_capacity; ++i) { + free(stm->input_buffer_array[i]); + } + } + + (*stm->recorderObj)->Destroy(stm->recorderObj); + stm->recorderObj = NULL; + stm->recorderItf = NULL; + + if (stm->input_queue) { + array_queue_destroy(stm->input_queue); + } + free(stm->input_silent_buffer); + + return CUBEB_OK; +} + +static void +opensl_stream_destroy(cubeb_stream * stm) +{ + assert(stm->draining || stm->shutdown); + + if (stm->playerObj) { + (*stm->playerObj)->Destroy(stm->playerObj); + stm->playerObj = NULL; + stm->play = NULL; + stm->bufq = NULL; + for (uint32_t i = 0; i < stm->queuebuf_capacity; ++i) { + free(stm->queuebuf[i]); + } + } + + if (stm->recorderObj) { + int r = opensl_destroy_recorder(stm); + assert(r == CUBEB_OK); + } + + if (stm->resampler) { + cubeb_resampler_destroy(stm->resampler); + } + + pthread_mutex_destroy(&stm->mutex); + + LOG("Cubeb stream (%p) destroyed", stm); + free(stm); +} + +static int opensl_stream_get_position(cubeb_stream * stm, uint64_t * position) { SLmillisecond msec; - uint64_t samplerate; - SLresult res; - int r; - uint32_t mixer_latency; uint32_t compensation_msec = 0; + SLresult res; res = (*stm->play)->GetPosition(stm->play, &msec); if (res != SL_RESULT_SUCCESS) @@ -781,27 +1598,23 @@ opensl_stream_get_position(cubeb_stream * stm, uint64_t * position) stm->lastPosition = msec; } - samplerate = stm->inputrate; - - r = stm->context->get_output_latency(&mixer_latency, stm->stream_type); - if (r) { - return CUBEB_ERROR; - } + uint64_t samplerate = stm->user_output_rate; + uint32_t output_latency = stm->output_latency_ms; pthread_mutex_lock(&stm->mutex); - int64_t maximum_position = stm->written * (int64_t)stm->inputrate / stm->outputrate; + int64_t maximum_position = stm->written * (int64_t)stm->user_output_rate / stm->output_configured_rate; pthread_mutex_unlock(&stm->mutex); assert(maximum_position >= 0); - if (msec > mixer_latency) { + if (msec > output_latency) { int64_t unadjusted_position; if (stm->lastCompensativePosition > msec + compensation_msec) { // Over compensation, use lastCompensativePosition. unadjusted_position = - samplerate * (stm->lastCompensativePosition - mixer_latency) / 1000; + samplerate * (stm->lastCompensativePosition - output_latency) / 1000; } else { unadjusted_position = - samplerate * (msec - mixer_latency + compensation_msec) / 1000; + samplerate * (msec - output_latency + compensation_msec) / 1000; stm->lastCompensativePosition = msec + compensation_msec; } *position = unadjusted_position < maximum_position ? @@ -813,24 +1626,6 @@ opensl_stream_get_position(cubeb_stream * stm, uint64_t * position) } int -opensl_stream_get_latency(cubeb_stream * stm, uint32_t * latency) -{ - int r; - uint32_t mixer_latency; // The latency returned by AudioFlinger is in ms. - - /* audio_stream_type_t is an int, so this is okay. */ - r = stm->context->get_output_latency(&mixer_latency, stm->stream_type); - if (r) { - return CUBEB_ERROR; - } - - *latency = stm->latency * stm->inputrate / 1000 + // OpenSL latency - mixer_latency * stm->inputrate / 1000; // AudioFlinger latency - - return CUBEB_OK; -} - -int opensl_stream_set_volume(cubeb_stream * stm, float volume) { SLresult res; @@ -865,18 +1660,19 @@ static struct cubeb_ops const opensl_ops = { .init = opensl_init, .get_backend_id = opensl_get_backend_id, .get_max_channel_count = opensl_get_max_channel_count, - .get_min_latency = opensl_get_min_latency, - .get_preferred_sample_rate = opensl_get_preferred_sample_rate, + .get_min_latency = NULL, + .get_preferred_sample_rate = NULL, .enumerate_devices = NULL, + .device_collection_destroy = NULL, .destroy = opensl_destroy, .stream_init = opensl_stream_init, .stream_destroy = opensl_stream_destroy, .stream_start = opensl_stream_start, .stream_stop = opensl_stream_stop, + .stream_reset_default_device = NULL, .stream_get_position = opensl_stream_get_position, - .stream_get_latency = opensl_stream_get_latency, + .stream_get_latency = NULL, .stream_set_volume = opensl_stream_set_volume, - .stream_set_panning = NULL, .stream_get_current_device = NULL, .stream_device_destroy = NULL, .stream_register_device_changed_callback = NULL, diff --git a/media/libcubeb/src/cubeb_pulse.c b/media/libcubeb/src/cubeb_pulse.c index 4f474452d4..846ba426ab 100644 --- a/media/libcubeb/src/cubeb_pulse.c +++ b/media/libcubeb/src/cubeb_pulse.c @@ -7,12 +7,14 @@ #undef NDEBUG #include <assert.h> #include <dlfcn.h> -#include <stdlib.h> #include <pulse/pulseaudio.h> +#include <stdio.h> +#include <stdlib.h> #include <string.h> -#include "cubeb/cubeb.h" #include "cubeb-internal.h" -#include <stdio.h> +#include "cubeb/cubeb.h" +#include "cubeb_mixer.h" +#include "cubeb_strings.h" #ifdef DISABLE_LIBPULSE_DLOPEN #define WRAP(x) x @@ -20,13 +22,14 @@ #define WRAP(x) cubeb_##x #define LIBPULSE_API_VISIT(X) \ X(pa_channel_map_can_balance) \ - X(pa_channel_map_init_auto) \ + X(pa_channel_map_init) \ X(pa_context_connect) \ X(pa_context_disconnect) \ X(pa_context_drain) \ X(pa_context_get_server_info) \ X(pa_context_get_sink_info_by_name) \ X(pa_context_get_sink_info_list) \ + X(pa_context_get_sink_input_info) \ X(pa_context_get_source_info_list) \ X(pa_context_get_state) \ X(pa_context_new) \ @@ -81,33 +84,50 @@ X(pa_context_set_subscribe_callback) \ X(pa_context_subscribe) \ X(pa_mainloop_api_once) \ + X(pa_get_library_version) \ + X(pa_channel_map_init_auto) \ #define MAKE_TYPEDEF(x) static typeof(x) * cubeb_##x; LIBPULSE_API_VISIT(MAKE_TYPEDEF); #undef MAKE_TYPEDEF #endif +#if PA_CHECK_VERSION(2, 0, 0) +static int has_pulse_v2 = 0; +#endif + static struct cubeb_ops const pulse_ops; +struct cubeb_default_sink_info { + pa_channel_map channel_map; + uint32_t sample_spec_rate; + pa_sink_flags_t flags; +}; + struct cubeb { struct cubeb_ops const * ops; void * libpulse; pa_threaded_mainloop * mainloop; pa_context * context; - pa_sink_info * default_sink_info; + struct cubeb_default_sink_info * default_sink_info; char * context_name; int error; - cubeb_device_collection_changed_callback collection_changed_callback; - void * collection_changed_user_ptr; + cubeb_device_collection_changed_callback output_collection_changed_callback; + void * output_collection_changed_user_ptr; + cubeb_device_collection_changed_callback input_collection_changed_callback; + void * input_collection_changed_user_ptr; + cubeb_strings * device_ids; }; struct cubeb_stream { + /* Note: Must match cubeb_stream layout in cubeb.c. */ cubeb * context; + void * user_ptr; + /**/ pa_stream * output_stream; pa_stream * input_stream; cubeb_data_callback data_callback; cubeb_state_callback state_callback; - void * user_ptr; pa_time_event * drain_timer; pa_sample_spec output_sample_spec; pa_sample_spec input_sample_spec; @@ -124,6 +144,24 @@ enum cork_state { NOTIFY = 1 << 1 }; +static int +intern_device_id(cubeb * ctx, char const ** id) +{ + char const * interned; + + assert(ctx); + assert(id); + + interned = cubeb_strings_intern(ctx->device_ids, *id); + if (!interned) { + return CUBEB_ERROR; + } + + *id = interned; + + return CUBEB_OK; +} + static void sink_info_callback(pa_context * context, const pa_sink_info * info, int eol, void * u) { @@ -131,8 +169,10 @@ sink_info_callback(pa_context * context, const pa_sink_info * info, int eol, voi cubeb * ctx = u; if (!eol) { free(ctx->default_sink_info); - ctx->default_sink_info = malloc(sizeof(pa_sink_info)); - memcpy(ctx->default_sink_info, info, sizeof(pa_sink_info)); + ctx->default_sink_info = malloc(sizeof(struct cubeb_default_sink_info)); + memcpy(&ctx->default_sink_info->channel_map, &info->channel_map, sizeof(pa_channel_map)); + ctx->default_sink_info->sample_spec_rate = info->sample_spec.rate; + ctx->default_sink_info->flags = info->flags; } WRAP(pa_threaded_mainloop_signal)(ctx->mainloop, 0); } @@ -140,7 +180,11 @@ sink_info_callback(pa_context * context, const pa_sink_info * info, int eol, voi static void server_info_callback(pa_context * context, const pa_server_info * info, void * u) { - WRAP(pa_context_get_sink_info_by_name)(context, info->default_sink_name, sink_info_callback, u); + pa_operation * o; + o = WRAP(pa_context_get_sink_info_by_name)(context, info->default_sink_name, sink_info_callback, u); + if (o) { + WRAP(pa_operation_unref)(o); + } } static void @@ -467,12 +511,81 @@ stream_update_timing_info(cubeb_stream * stm) return r; } +static pa_channel_position_t +cubeb_channel_to_pa_channel(cubeb_channel channel) +{ + switch (channel) { + case CHANNEL_FRONT_LEFT: + return PA_CHANNEL_POSITION_FRONT_LEFT; + case CHANNEL_FRONT_RIGHT: + return PA_CHANNEL_POSITION_FRONT_RIGHT; + case CHANNEL_FRONT_CENTER: + return PA_CHANNEL_POSITION_FRONT_CENTER; + case CHANNEL_LOW_FREQUENCY: + return PA_CHANNEL_POSITION_LFE; + case CHANNEL_BACK_LEFT: + return PA_CHANNEL_POSITION_REAR_LEFT; + case CHANNEL_BACK_RIGHT: + return PA_CHANNEL_POSITION_REAR_RIGHT; + case CHANNEL_FRONT_LEFT_OF_CENTER: + return PA_CHANNEL_POSITION_FRONT_LEFT_OF_CENTER; + case CHANNEL_FRONT_RIGHT_OF_CENTER: + return PA_CHANNEL_POSITION_FRONT_RIGHT_OF_CENTER; + case CHANNEL_BACK_CENTER: + return PA_CHANNEL_POSITION_REAR_CENTER; + case CHANNEL_SIDE_LEFT: + return PA_CHANNEL_POSITION_SIDE_LEFT; + case CHANNEL_SIDE_RIGHT: + return PA_CHANNEL_POSITION_SIDE_RIGHT; + case CHANNEL_TOP_CENTER: + return PA_CHANNEL_POSITION_TOP_CENTER; + case CHANNEL_TOP_FRONT_LEFT: + return PA_CHANNEL_POSITION_TOP_FRONT_LEFT; + case CHANNEL_TOP_FRONT_CENTER: + return PA_CHANNEL_POSITION_TOP_FRONT_CENTER; + case CHANNEL_TOP_FRONT_RIGHT: + return PA_CHANNEL_POSITION_TOP_FRONT_RIGHT; + case CHANNEL_TOP_BACK_LEFT: + return PA_CHANNEL_POSITION_TOP_REAR_LEFT; + case CHANNEL_TOP_BACK_CENTER: + return PA_CHANNEL_POSITION_TOP_REAR_CENTER; + case CHANNEL_TOP_BACK_RIGHT: + return PA_CHANNEL_POSITION_TOP_REAR_RIGHT; + default: + return PA_CHANNEL_POSITION_INVALID; + } +} + +static void +layout_to_channel_map(cubeb_channel_layout layout, pa_channel_map * cm) +{ + assert(cm && layout != CUBEB_LAYOUT_UNDEFINED); + + WRAP(pa_channel_map_init)(cm); + + uint32_t channels = 0; + cubeb_channel_layout channelMap = layout; + for (uint32_t i = 0 ; channelMap != 0; ++i) { + uint32_t channel = (channelMap & 1) << i; + if (channel != 0) { + cm->map[channels] = cubeb_channel_to_pa_channel(channel); + channels++; + } + channelMap = channelMap >> 1; + } + unsigned int channels_from_layout = cubeb_channel_layout_nb_channels(layout); + assert(channels_from_layout <= UINT8_MAX); + cm->channels = (uint8_t) channels_from_layout; +} + static void pulse_context_destroy(cubeb * ctx); static void pulse_destroy(cubeb * ctx); static int pulse_context_init(cubeb * ctx) { + int r; + if (ctx->context) { assert(ctx->error == 1); pulse_context_destroy(ctx); @@ -486,9 +599,9 @@ pulse_context_init(cubeb * ctx) WRAP(pa_context_set_state_callback)(ctx->context, context_state_callback, ctx); WRAP(pa_threaded_mainloop_lock)(ctx->mainloop); - WRAP(pa_context_connect)(ctx->context, NULL, 0, NULL); + r = WRAP(pa_context_connect)(ctx->context, NULL, 0, NULL); - if (wait_until_context_ready(ctx) != 0) { + if (r < 0 || wait_until_context_ready(ctx) != 0) { WRAP(pa_threaded_mainloop_unlock)(ctx->mainloop); pulse_context_destroy(ctx); ctx->context = NULL; @@ -502,18 +615,25 @@ pulse_context_init(cubeb * ctx) return 0; } +static int pulse_subscribe_notifications(cubeb * context, + pa_subscription_mask_t mask); + /*static*/ int pulse_init(cubeb ** context, char const * context_name) { void * libpulse = NULL; cubeb * ctx; + pa_operation * o; *context = NULL; #ifndef DISABLE_LIBPULSE_DLOPEN libpulse = dlopen("libpulse.so.0", RTLD_LAZY); if (!libpulse) { - return CUBEB_ERROR; + libpulse = dlopen("libpulse.so", RTLD_LAZY); + if (!libpulse) { + return CUBEB_ERROR; + } } #define LOAD(x) { \ @@ -528,11 +648,20 @@ pulse_init(cubeb ** context, char const * context_name) #undef LOAD #endif +#if PA_CHECK_VERSION(2, 0, 0) + const char* version = WRAP(pa_get_library_version)(); + has_pulse_v2 = strtol(version, NULL, 10) >= 2; +#endif + ctx = calloc(1, sizeof(*ctx)); assert(ctx); ctx->ops = &pulse_ops; ctx->libpulse = libpulse; + if (cubeb_strings_init(&ctx->device_ids) != CUBEB_OK) { + pulse_destroy(ctx); + return CUBEB_ERROR; + } ctx->mainloop = WRAP(pa_threaded_mainloop_new)(); ctx->default_sink_info = NULL; @@ -545,10 +674,20 @@ pulse_init(cubeb ** context, char const * context_name) return CUBEB_ERROR; } + /* server_info_callback performs a second async query, which is + responsible for initializing default_sink_info and signalling the + mainloop to end the wait. */ WRAP(pa_threaded_mainloop_lock)(ctx->mainloop); - WRAP(pa_context_get_server_info)(ctx->context, server_info_callback, ctx); + o = WRAP(pa_context_get_server_info)(ctx->context, server_info_callback, ctx); + if (o) { + operation_wait(ctx, NULL, o); + WRAP(pa_operation_unref)(o); + } WRAP(pa_threaded_mainloop_unlock)(ctx->mainloop); + /* Update `default_sink_info` when the default device changes. */ + pulse_subscribe_notifications(ctx, PA_SUBSCRIPTION_MASK_SERVER); + *context = ctx; return CUBEB_OK; @@ -567,11 +706,8 @@ pulse_get_max_channel_count(cubeb * ctx, uint32_t * max_channels) (void)ctx; assert(ctx && max_channels); - WRAP(pa_threaded_mainloop_lock)(ctx->mainloop); - while (!ctx->default_sink_info) { - WRAP(pa_threaded_mainloop_wait)(ctx->mainloop); - } - WRAP(pa_threaded_mainloop_unlock)(ctx->mainloop); + if (!ctx->default_sink_info) + return CUBEB_ERROR; *max_channels = ctx->default_sink_info->channel_map.channels; @@ -584,13 +720,10 @@ pulse_get_preferred_sample_rate(cubeb * ctx, uint32_t * rate) assert(ctx && rate); (void)ctx; - WRAP(pa_threaded_mainloop_lock)(ctx->mainloop); - while (!ctx->default_sink_info) { - WRAP(pa_threaded_mainloop_wait)(ctx->mainloop); - } - WRAP(pa_threaded_mainloop_unlock)(ctx->mainloop); + if (!ctx->default_sink_info) + return CUBEB_ERROR; - *rate = ctx->default_sink_info->sample_spec.rate; + *rate = ctx->default_sink_info->sample_spec_rate; return CUBEB_OK; } @@ -625,9 +758,7 @@ pulse_context_destroy(cubeb * ctx) static void pulse_destroy(cubeb * ctx) { - if (ctx->context_name) { - free(ctx->context_name); - } + free(ctx->context_name); if (ctx->context) { pulse_context_destroy(ctx); } @@ -637,12 +768,14 @@ pulse_destroy(cubeb * ctx) WRAP(pa_threaded_mainloop_free)(ctx->mainloop); } + if (ctx->device_ids) { + cubeb_strings_destroy(ctx->device_ids); + } + if (ctx->libpulse) { dlclose(ctx->libpulse); } - if (ctx->default_sink_info) { - free(ctx->default_sink_info); - } + free(ctx->default_sink_info); free(ctx); } @@ -665,6 +798,25 @@ to_pulse_format(cubeb_sample_format format) } } +static cubeb_channel_layout +pulse_default_layout_for_channels(uint32_t ch) +{ + assert (ch > 0 && ch <= 8); + switch (ch) { + case 1: return CUBEB_LAYOUT_MONO; + case 2: return CUBEB_LAYOUT_STEREO; + case 3: return CUBEB_LAYOUT_3F; + case 4: return CUBEB_LAYOUT_QUAD; + case 5: return CUBEB_LAYOUT_3F2; + case 6: return CUBEB_LAYOUT_3F_LFE | + CHANNEL_SIDE_LEFT | CHANNEL_SIDE_RIGHT; + case 7: return CUBEB_LAYOUT_3F3R_LFE; + case 8: return CUBEB_LAYOUT_3F4_LFE; + } + // Never get here! + return CUBEB_LAYOUT_UNDEFINED; +} + static int create_pa_stream(cubeb_stream * stm, pa_stream ** pa_stm, @@ -672,15 +824,39 @@ create_pa_stream(cubeb_stream * stm, char const * stream_name) { assert(stm && stream_params); + assert(&stm->input_stream == pa_stm || (&stm->output_stream == pa_stm && + (stream_params->layout == CUBEB_LAYOUT_UNDEFINED || + (stream_params->layout != CUBEB_LAYOUT_UNDEFINED && + cubeb_channel_layout_nb_channels(stream_params->layout) == stream_params->channels)))); + if (stream_params->prefs & CUBEB_STREAM_PREF_LOOPBACK) { + return CUBEB_ERROR_NOT_SUPPORTED; + } *pa_stm = NULL; pa_sample_spec ss; ss.format = to_pulse_format(stream_params->format); if (ss.format == PA_SAMPLE_INVALID) return CUBEB_ERROR_INVALID_FORMAT; ss.rate = stream_params->rate; - ss.channels = stream_params->channels; - - *pa_stm = WRAP(pa_stream_new)(stm->context->context, stream_name, &ss, NULL); + if (stream_params->channels > UINT8_MAX) + return CUBEB_ERROR_INVALID_FORMAT; + ss.channels = (uint8_t) stream_params->channels; + + if (stream_params->layout == CUBEB_LAYOUT_UNDEFINED) { + pa_channel_map cm; + if (stream_params->channels <= 8 && + !WRAP(pa_channel_map_init_auto)(&cm, stream_params->channels, PA_CHANNEL_MAP_DEFAULT)) { + LOG("Layout undefined and PulseAudio's default layout has not been configured, guess one."); + layout_to_channel_map(pulse_default_layout_for_channels(stream_params->channels), &cm); + *pa_stm = WRAP(pa_stream_new)(stm->context->context, stream_name, &ss, &cm); + } else { + LOG("Layout undefined, PulseAudio will use its default."); + *pa_stm = WRAP(pa_stream_new)(stm->context->context, stream_name, &ss, NULL); + } + } else { + pa_channel_map cm; + layout_to_channel_map(stream_params->layout, &cm); + *pa_stm = WRAP(pa_stream_new)(stm->context->context, stream_name, &ss, &cm); + } return (*pa_stm == NULL) ? CUBEB_ERROR : CUBEB_OK; } @@ -753,7 +929,7 @@ pulse_stream_init(cubeb * context, battr = set_buffering_attribute(latency_frames, &stm->output_sample_spec); WRAP(pa_stream_connect_playback)(stm->output_stream, - output_device, + (char const *) output_device, &battr, PA_STREAM_AUTO_TIMING_UPDATE | PA_STREAM_INTERPOLATE_TIMING | PA_STREAM_START_CORKED | PA_STREAM_ADJUST_LATENCY, @@ -776,7 +952,7 @@ pulse_stream_init(cubeb * context, battr = set_buffering_attribute(latency_frames, &stm->input_sample_spec); WRAP(pa_stream_connect_record)(stm->input_stream, - input_device, + (char const *) input_device, &battr, PA_STREAM_AUTO_TIMING_UPDATE | PA_STREAM_INTERPOLATE_TIMING | PA_STREAM_START_CORKED | PA_STREAM_ADJUST_LATENCY); @@ -796,7 +972,7 @@ pulse_stream_init(cubeb * context, return CUBEB_ERROR; } - if (g_log_level) { + if (g_cubeb_log_level) { if (output_stream_params){ const pa_buffer_attr * output_att; output_att = WRAP(pa_stream_get_buffer_attr)(stm->output_stream); @@ -813,6 +989,7 @@ pulse_stream_init(cubeb * context, } *stream = stm; + LOG("Cubeb stream (%p) init successful.", *stream); return CUBEB_OK; } @@ -844,6 +1021,7 @@ pulse_stream_destroy(cubeb_stream * stm) } WRAP(pa_threaded_mainloop_unlock)(stm->context->mainloop); + LOG("Cubeb stream (%p) destroyed successfully.", stm); free(stm); } @@ -875,6 +1053,7 @@ pulse_stream_start(cubeb_stream * stm) WRAP(pa_threaded_mainloop_unlock)(stm->context->mainloop); } + LOG("Cubeb stream (%p) started successfully.", stm); return CUBEB_OK; } @@ -890,6 +1069,7 @@ pulse_stream_stop(cubeb_stream * stm) WRAP(pa_threaded_mainloop_unlock)(stm->context->mainloop); stream_cork(stm, CORK | NOTIFY); + LOG("Cubeb stream (%p) stopped successfully.", stm); return CUBEB_OK; } @@ -962,6 +1142,7 @@ pulse_stream_set_volume(cubeb_stream * stm, float volume) pa_volume_t vol; pa_cvolume cvol; const pa_sample_spec * ss; + cubeb * ctx; if (!stm->output_stream) { return CUBEB_ERROR; @@ -969,13 +1150,11 @@ pulse_stream_set_volume(cubeb_stream * stm, float volume) WRAP(pa_threaded_mainloop_lock)(stm->context->mainloop); - while (!stm->context->default_sink_info) { - WRAP(pa_threaded_mainloop_wait)(stm->context->mainloop); - } - /* if the pulse daemon is configured to use flat volumes, * apply our own gain instead of changing the input volume on the sink. */ - if (stm->context->default_sink_info->flags & PA_SINK_FLAT_VOLUME) { + ctx = stm->context; + if (ctx->default_sink_info && + (ctx->default_sink_info->flags & PA_SINK_FLAT_VOLUME)) { stm->volume = volume; } else { ss = WRAP(pa_stream_get_sample_spec)(stm->output_stream); @@ -985,46 +1164,40 @@ pulse_stream_set_volume(cubeb_stream * stm, float volume) index = WRAP(pa_stream_get_index)(stm->output_stream); - op = WRAP(pa_context_set_sink_input_volume)(stm->context->context, + op = WRAP(pa_context_set_sink_input_volume)(ctx->context, index, &cvol, volume_success, stm); if (op) { - operation_wait(stm->context, stm->output_stream, op); + operation_wait(ctx, stm->output_stream, op); WRAP(pa_operation_unref)(op); } } - WRAP(pa_threaded_mainloop_unlock)(stm->context->mainloop); + WRAP(pa_threaded_mainloop_unlock)(ctx->mainloop); return CUBEB_OK; } -static int -pulse_stream_set_panning(cubeb_stream * stream, float panning) -{ - const pa_channel_map * map; - pa_cvolume vol; - - if (!stream->output_stream) { - return CUBEB_ERROR; - } - - map = WRAP(pa_stream_get_channel_map)(stream->output_stream); +struct sink_input_info_result { + pa_cvolume * cvol; + pa_threaded_mainloop * mainloop; +}; - if (!WRAP(pa_channel_map_can_balance)(map)) { - return CUBEB_ERROR; +static void +sink_input_info_cb(pa_context * c, pa_sink_input_info const * i, int eol, void * u) +{ + struct sink_input_info_result * r = u; + if (!eol) { + *r->cvol = i->volume; } - - WRAP(pa_cvolume_set_balance)(&vol, map, panning); - - return CUBEB_OK; + WRAP(pa_threaded_mainloop_signal)(r->mainloop, 0); } typedef struct { char * default_sink_name; char * default_source_name; - cubeb_device_info ** devinfo; + cubeb_device_info * devinfo; uint32_t max; uint32_t count; cubeb * context; @@ -1053,7 +1226,7 @@ pulse_ensure_dev_list_data_list_size (pulse_dev_list_data * list_data) if (list_data->count == list_data->max) { list_data->max += 8; list_data->devinfo = realloc(list_data->devinfo, - sizeof(cubeb_device_info *) * list_data->max); + sizeof(cubeb_device_info) * list_data->max); } } @@ -1062,33 +1235,47 @@ pulse_get_state_from_sink_port(pa_sink_port_info * info) { if (info != NULL) { #if PA_CHECK_VERSION(2, 0, 0) - if (info->available == PA_PORT_AVAILABLE_NO) + if (has_pulse_v2 && info->available == PA_PORT_AVAILABLE_NO) return CUBEB_DEVICE_STATE_UNPLUGGED; else /*if (info->available == PA_PORT_AVAILABLE_YES) + UNKNOWN */ #endif return CUBEB_DEVICE_STATE_ENABLED; } - return CUBEB_DEVICE_STATE_DISABLED; + return CUBEB_DEVICE_STATE_ENABLED; } static void pulse_sink_info_cb(pa_context * context, const pa_sink_info * info, - int eol, void * user_data) + int eol, void * user_data) { pulse_dev_list_data * list_data = user_data; cubeb_device_info * devinfo; - const char * prop; + char const * prop = NULL; + char const * device_id = NULL; (void)context; - if (eol || info == NULL) + if (eol) { + WRAP(pa_threaded_mainloop_signal)(list_data->context->mainloop, 0); + return; + } + + if (info == NULL) return; - devinfo = calloc(1, sizeof(cubeb_device_info)); + device_id = info->name; + if (intern_device_id(list_data->context, &device_id) != CUBEB_OK) { + assert(NULL); + return; + } + + pulse_ensure_dev_list_data_list_size(list_data); + devinfo = &list_data->devinfo[list_data->count]; + memset(devinfo, 0, sizeof(cubeb_device_info)); - devinfo->device_id = strdup(info->name); - devinfo->devid = devinfo->device_id; + devinfo->device_id = device_id; + devinfo->devid = (cubeb_devid) devinfo->device_id; devinfo->friendly_name = strdup(info->description); prop = WRAP(pa_proplist_gets)(info->proplist, "sysfs.path"); if (prop) @@ -1099,7 +1286,8 @@ pulse_sink_info_cb(pa_context * context, const pa_sink_info * info, devinfo->type = CUBEB_DEVICE_TYPE_OUTPUT; devinfo->state = pulse_get_state_from_sink_port(info->active_port); - devinfo->preferred = strcmp(info->name, list_data->default_sink_name) == 0; + devinfo->preferred = (strcmp(info->name, list_data->default_sink_name) == 0) ? + CUBEB_DEVICE_PREF_ALL : CUBEB_DEVICE_PREF_NONE; devinfo->format = CUBEB_DEVICE_FMT_ALL; devinfo->default_format = pulse_format_to_cubeb_format(info->sample_spec.format); @@ -1111,10 +1299,7 @@ pulse_sink_info_cb(pa_context * context, const pa_sink_info * info, devinfo->latency_lo = 0; devinfo->latency_hi = 0; - pulse_ensure_dev_list_data_list_size (list_data); - list_data->devinfo[list_data->count++] = devinfo; - - WRAP(pa_threaded_mainloop_signal)(list_data->context->mainloop, 0); + list_data->count += 1; } static cubeb_device_state @@ -1122,14 +1307,14 @@ pulse_get_state_from_source_port(pa_source_port_info * info) { if (info != NULL) { #if PA_CHECK_VERSION(2, 0, 0) - if (info->available == PA_PORT_AVAILABLE_NO) + if (has_pulse_v2 && info->available == PA_PORT_AVAILABLE_NO) return CUBEB_DEVICE_STATE_UNPLUGGED; else /*if (info->available == PA_PORT_AVAILABLE_YES) + UNKNOWN */ #endif return CUBEB_DEVICE_STATE_ENABLED; } - return CUBEB_DEVICE_STATE_DISABLED; + return CUBEB_DEVICE_STATE_ENABLED; } static void @@ -1138,17 +1323,28 @@ pulse_source_info_cb(pa_context * context, const pa_source_info * info, { pulse_dev_list_data * list_data = user_data; cubeb_device_info * devinfo; - const char * prop; + char const * prop = NULL; + char const * device_id = NULL; (void)context; - if (eol) + if (eol) { + WRAP(pa_threaded_mainloop_signal)(list_data->context->mainloop, 0); + return; + } + + device_id = info->name; + if (intern_device_id(list_data->context, &device_id) != CUBEB_OK) { + assert(NULL); return; + } - devinfo = calloc(1, sizeof(cubeb_device_info)); + pulse_ensure_dev_list_data_list_size(list_data); + devinfo = &list_data->devinfo[list_data->count]; + memset(devinfo, 0, sizeof(cubeb_device_info)); - devinfo->device_id = strdup(info->name); - devinfo->devid = devinfo->device_id; + devinfo->device_id = device_id; + devinfo->devid = (cubeb_devid) devinfo->device_id; devinfo->friendly_name = strdup(info->description); prop = WRAP(pa_proplist_gets)(info->proplist, "sysfs.path"); if (prop) @@ -1159,7 +1355,8 @@ pulse_source_info_cb(pa_context * context, const pa_source_info * info, devinfo->type = CUBEB_DEVICE_TYPE_INPUT; devinfo->state = pulse_get_state_from_source_port(info->active_port); - devinfo->preferred = strcmp(info->name, list_data->default_source_name) == 0; + devinfo->preferred = (strcmp(info->name, list_data->default_source_name) == 0) ? + CUBEB_DEVICE_PREF_ALL : CUBEB_DEVICE_PREF_NONE; devinfo->format = CUBEB_DEVICE_FMT_ALL; devinfo->default_format = pulse_format_to_cubeb_format(info->sample_spec.format); @@ -1171,10 +1368,7 @@ pulse_source_info_cb(pa_context * context, const pa_source_info * info, devinfo->latency_lo = 0; devinfo->latency_hi = 0; - pulse_ensure_dev_list_data_list_size (list_data); - list_data->devinfo[list_data->count++] = devinfo; - - WRAP(pa_threaded_mainloop_signal)(list_data->context->mainloop, 0); + list_data->count += 1; } static void @@ -1186,19 +1380,20 @@ pulse_server_info_cb(pa_context * c, const pa_server_info * i, void * userdata) free(list_data->default_sink_name); free(list_data->default_source_name); - list_data->default_sink_name = strdup(i->default_sink_name); - list_data->default_source_name = strdup(i->default_source_name); + list_data->default_sink_name = + i->default_sink_name ? strdup(i->default_sink_name) : NULL; + list_data->default_source_name = + i->default_source_name ? strdup(i->default_source_name) : NULL; WRAP(pa_threaded_mainloop_signal)(list_data->context->mainloop, 0); } static int pulse_enumerate_devices(cubeb * context, cubeb_device_type type, - cubeb_device_collection ** collection) + cubeb_device_collection * collection) { pulse_dev_list_data user_data = { NULL, NULL, NULL, 0, 0, context }; pa_operation * o; - uint32_t i; WRAP(pa_threaded_mainloop_lock)(context->mainloop); @@ -1229,15 +1424,26 @@ pulse_enumerate_devices(cubeb * context, cubeb_device_type type, WRAP(pa_threaded_mainloop_unlock)(context->mainloop); - *collection = malloc(sizeof(cubeb_device_collection) + - sizeof(cubeb_device_info *) * (user_data.count > 0 ? user_data.count - 1 : 0)); - (*collection)->count = user_data.count; - for (i = 0; i < user_data.count; i++) - (*collection)->device[i] = user_data.devinfo[i]; + collection->device = user_data.devinfo; + collection->count = user_data.count; free(user_data.default_sink_name); free(user_data.default_source_name); - free(user_data.devinfo); + return CUBEB_OK; +} + +static int +pulse_device_collection_destroy(cubeb * ctx, cubeb_device_collection * collection) +{ + size_t n; + + for (n = 0; n < collection->count; n++) { + free((void *) collection->device[n].friendly_name); + free((void *) collection->device[n].vendor_name); + free((void *) collection->device[n].group_id); + } + + free(collection->device); return CUBEB_OK; } @@ -1285,29 +1491,40 @@ pulse_subscribe_callback(pa_context * ctx, cubeb * context = userdata; switch (t & PA_SUBSCRIPTION_EVENT_FACILITY_MASK) { + case PA_SUBSCRIPTION_EVENT_SERVER: + if ((t & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_CHANGE) { + LOG("Server changed %d", index); + WRAP(pa_context_get_server_info)(context->context, server_info_callback, context); + } + break; case PA_SUBSCRIPTION_EVENT_SOURCE: case PA_SUBSCRIPTION_EVENT_SINK: - if (g_log_level) { + if (g_cubeb_log_level) { if ((t & PA_SUBSCRIPTION_EVENT_FACILITY_MASK) == PA_SUBSCRIPTION_EVENT_SOURCE && (t & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE) { - LOG("Removing sink index %d", index); + LOG("Removing source index %d", index); } else if ((t & PA_SUBSCRIPTION_EVENT_FACILITY_MASK) == PA_SUBSCRIPTION_EVENT_SOURCE && (t & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_NEW) { - LOG("Adding sink index %d", index); + LOG("Adding source index %d", index); } if ((t & PA_SUBSCRIPTION_EVENT_FACILITY_MASK) == PA_SUBSCRIPTION_EVENT_SINK && (t & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE) { - LOG("Removing source index %d", index); + LOG("Removing sink index %d", index); } else if ((t & PA_SUBSCRIPTION_EVENT_FACILITY_MASK) == PA_SUBSCRIPTION_EVENT_SINK && (t & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_NEW) { - LOG("Adding source index %d", index); + LOG("Adding sink index %d", index); } } if ((t & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE || (t & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_NEW) { - context->collection_changed_callback(context, context->collection_changed_user_ptr); + if ((t & PA_SUBSCRIPTION_EVENT_FACILITY_MASK) == PA_SUBSCRIPTION_EVENT_SOURCE) { + context->input_collection_changed_callback(context, context->input_collection_changed_user_ptr); + } + if ((t & PA_SUBSCRIPTION_EVENT_FACILITY_MASK) == PA_SUBSCRIPTION_EVENT_SINK) { + context->output_collection_changed_callback(context, context->output_collection_changed_user_ptr); + } } break; } @@ -1323,34 +1540,15 @@ subscribe_success(pa_context *c, int success, void *userdata) } static int -pulse_register_device_collection_changed(cubeb * context, - cubeb_device_type devtype, - cubeb_device_collection_changed_callback collection_changed_callback, - void * user_ptr) -{ - context->collection_changed_callback = collection_changed_callback; - context->collection_changed_user_ptr = user_ptr; - +pulse_subscribe_notifications(cubeb * context, pa_subscription_mask_t mask) { WRAP(pa_threaded_mainloop_lock)(context->mainloop); - pa_subscription_mask_t mask; - if (context->collection_changed_callback == NULL) { - // Unregister subscription - WRAP(pa_context_set_subscribe_callback)(context->context, NULL, NULL); - mask = PA_SUBSCRIPTION_MASK_NULL; - } else { - WRAP(pa_context_set_subscribe_callback)(context->context, pulse_subscribe_callback, context); - if (devtype == CUBEB_DEVICE_TYPE_INPUT) - mask = PA_SUBSCRIPTION_MASK_SOURCE; - else if (devtype == CUBEB_DEVICE_TYPE_OUTPUT) - mask = PA_SUBSCRIPTION_MASK_SINK; - else - mask = PA_SUBSCRIPTION_MASK_SINK | PA_SUBSCRIPTION_MASK_SOURCE; - } + WRAP(pa_context_set_subscribe_callback)(context->context, pulse_subscribe_callback, context); pa_operation * o; o = WRAP(pa_context_subscribe)(context->context, mask, subscribe_success, context); if (o == NULL) { + WRAP(pa_threaded_mainloop_unlock)(context->mainloop); LOG("Context subscribe failed"); return CUBEB_ERROR; } @@ -1362,6 +1560,37 @@ pulse_register_device_collection_changed(cubeb * context, return CUBEB_OK; } +static int +pulse_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_INPUT) { + context->input_collection_changed_callback = collection_changed_callback; + context->input_collection_changed_user_ptr = user_ptr; + } + if (devtype & CUBEB_DEVICE_TYPE_OUTPUT) { + context->output_collection_changed_callback = collection_changed_callback; + context->output_collection_changed_user_ptr = user_ptr; + } + + pa_subscription_mask_t mask = PA_SUBSCRIPTION_MASK_NULL; + if (context->input_collection_changed_callback) { + /* Input added or removed */ + mask |= PA_SUBSCRIPTION_MASK_SOURCE; + } + if (context->output_collection_changed_callback) { + /* Output added or removed */ + mask |= PA_SUBSCRIPTION_MASK_SINK; + } + /* Default device changed, this is always registered in order to update the + * `default_sink_info` when the default device changes. */ + mask |= PA_SUBSCRIPTION_MASK_SERVER; + + return pulse_subscribe_notifications(context, mask); +} + static struct cubeb_ops const pulse_ops = { .init = pulse_init, .get_backend_id = pulse_get_backend_id, @@ -1369,15 +1598,16 @@ static struct cubeb_ops const pulse_ops = { .get_min_latency = pulse_get_min_latency, .get_preferred_sample_rate = pulse_get_preferred_sample_rate, .enumerate_devices = pulse_enumerate_devices, + .device_collection_destroy = pulse_device_collection_destroy, .destroy = pulse_destroy, .stream_init = pulse_stream_init, .stream_destroy = pulse_stream_destroy, .stream_start = pulse_stream_start, .stream_stop = pulse_stream_stop, + .stream_reset_default_device = NULL, .stream_get_position = pulse_stream_get_position, .stream_get_latency = pulse_stream_get_latency, .stream_set_volume = pulse_stream_set_volume, - .stream_set_panning = pulse_stream_set_panning, .stream_get_current_device = pulse_stream_get_current_device, .stream_device_destroy = pulse_stream_device_destroy, .stream_register_device_changed_callback = NULL, diff --git a/media/libcubeb/src/cubeb_resampler.cpp b/media/libcubeb/src/cubeb_resampler.cpp index f6676946c0..2bc8893404 100644 --- a/media/libcubeb/src/cubeb_resampler.cpp +++ b/media/libcubeb/src/cubeb_resampler.cpp @@ -35,29 +35,54 @@ to_speex_quality(cubeb_resampler_quality q) } } -long noop_resampler::fill(void * input_buffer, long * input_frames_count, - void * output_buffer, long output_frames) +uint32_t min_buffered_audio_frame(uint32_t sample_rate) +{ + return sample_rate / 20; +} + +template<typename T> +passthrough_resampler<T>::passthrough_resampler(cubeb_stream * s, + cubeb_data_callback cb, + void * ptr, + uint32_t input_channels, + uint32_t sample_rate) + : processor(input_channels) + , stream(s) + , data_callback(cb) + , user_ptr(ptr) + , sample_rate(sample_rate) +{ +} + +template<typename T> +long passthrough_resampler<T>::fill(void * input_buffer, long * input_frames_count, + void * output_buffer, long output_frames) { if (input_buffer) { assert(input_frames_count); } - assert((input_buffer && output_buffer && - *input_frames_count >= output_frames) || - (!input_buffer && (!input_frames_count || *input_frames_count == 0)) || - (!output_buffer && output_frames == 0)); - - if (output_buffer == nullptr) { - assert(input_buffer); - output_frames = *input_frames_count; + assert((input_buffer && output_buffer) || + (output_buffer && !input_buffer && (!input_frames_count || *input_frames_count == 0)) || + (input_buffer && !output_buffer && output_frames == 0)); + + if (input_buffer) { + if (!output_buffer) { + output_frames = *input_frames_count; + } + internal_input_buffer.push(static_cast<T*>(input_buffer), + frames_to_samples(*input_frames_count)); } - if (input_buffer && *input_frames_count != output_frames) { - assert(*input_frames_count > output_frames); + long rv = data_callback(stream, user_ptr, internal_input_buffer.data(), + output_buffer, output_frames); + + if (input_buffer) { + internal_input_buffer.pop(nullptr, frames_to_samples(output_frames)); *input_frames_count = output_frames; + drop_audio_if_needed(); } - return data_callback(stream, user_ptr, - input_buffer, output_buffer, output_frames); + return rv; } template<typename T, typename InputProcessor, typename OutputProcessor> @@ -120,27 +145,32 @@ cubeb_resampler_speex<T, InputProcessor, OutputProcessor> assert(!input_buffer && (!input_frames_count || *input_frames_count == 0) && output_buffer && output_frames_needed); - long got = 0; - T * out_unprocessed = nullptr; - long output_frames_before_processing = 0; + if (!draining) { + long got = 0; + T * out_unprocessed = nullptr; + long output_frames_before_processing = 0; + /* fill directly the input buffer of the output processor to save a copy */ + output_frames_before_processing = + output_processor->input_needed_for_output(output_frames_needed); - /* fill directly the input buffer of the output processor to save a copy */ - output_frames_before_processing = - output_processor->input_needed_for_output(output_frames_needed); + out_unprocessed = + output_processor->input_buffer(output_frames_before_processing); - out_unprocessed = - output_processor->input_buffer(output_frames_before_processing); + got = data_callback(stream, user_ptr, + nullptr, out_unprocessed, + output_frames_before_processing); - got = data_callback(stream, user_ptr, - nullptr, out_unprocessed, - output_frames_before_processing); + if (got < output_frames_before_processing) { + draining = true; - if (got < 0) { - return got; - } + if (got < 0) { + return got; + } + } - output_processor->written(got); + output_processor->written(got); + } /* Process the output. If not enough frames have been returned from the * callback, drain the processors. */ @@ -163,7 +193,10 @@ cubeb_resampler_speex<T, InputProcessor, OutputProcessor> /* process the input, and present exactly `output_frames_needed` in the * callback. */ input_processor->input(input_buffer, *input_frames_count); - resampled_input = input_processor->output(resampled_frame_count); + + size_t frames_resampled = 0; + resampled_input = input_processor->output(resampled_frame_count, &frames_resampled); + *input_frames_count = frames_resampled; long got = data_callback(stream, user_ptr, resampled_input, nullptr, resampled_frame_count); @@ -174,18 +207,22 @@ cubeb_resampler_speex<T, InputProcessor, OutputProcessor> return (*input_frames_count) * (got / resampled_frame_count); } - template<typename T, typename InputProcessor, typename OutputProcessor> long cubeb_resampler_speex<T, InputProcessor, OutputProcessor> ::fill_internal_duplex(T * in_buffer, long * input_frames_count, T * out_buffer, long output_frames_needed) { + if (draining) { + // discard input and drain any signal remaining in the resampler. + return output_processor->output(out_buffer, output_frames_needed); + } + /* The input data, after eventual resampling. This is passed to the callback. */ T * resampled_input = nullptr; /* The output buffer passed down in the callback, that might be resampled. */ T * out_unprocessed = nullptr; - size_t output_frames_before_processing = 0; + long output_frames_before_processing = 0; /* The number of frames returned from the callback. */ long got = 0; @@ -210,8 +247,11 @@ cubeb_resampler_speex<T, InputProcessor, OutputProcessor> /* process the input, and present exactly `output_frames_needed` in the * callback. */ input_processor->input(in_buffer, *input_frames_count); + + size_t frames_resampled = 0; resampled_input = - input_processor->output(output_frames_before_processing); + input_processor->output(output_frames_before_processing, &frames_resampled); + *input_frames_count = frames_resampled; } else { resampled_input = nullptr; } @@ -220,15 +260,25 @@ cubeb_resampler_speex<T, InputProcessor, OutputProcessor> resampled_input, out_unprocessed, output_frames_before_processing); - if (got < 0) { - return got; + if (got < output_frames_before_processing) { + draining = true; + + if (got < 0) { + return got; + } } output_processor->written(got); + input_processor->drop_audio_if_needed(); + /* Process the output. If not enough frames have been returned from the * callback, drain the processors. */ - return output_processor->output(out_buffer, output_frames_needed); + got = output_processor->output(out_buffer, output_frames_needed); + + output_processor->drop_audio_if_needed(); + + return got; } /* Resampler C API */ diff --git a/media/libcubeb/src/cubeb_resampler.h b/media/libcubeb/src/cubeb_resampler.h index 020ccc17ab..f6b551373e 100644 --- a/media/libcubeb/src/cubeb_resampler.h +++ b/media/libcubeb/src/cubeb_resampler.h @@ -25,8 +25,15 @@ typedef enum { * Create a resampler to adapt the requested sample rate into something that * is accepted by the audio backend. * @param stream A cubeb_stream instance supplied to the data callback. - * @param params Used to calculate bytes per frame and buffer size for resampling. - * @param target_rate The sampling rate after resampling. + * @param input_params Used to calculate bytes per frame and buffer size for + * resampling of the input side of the stream. NULL if input should not be + * resampled. + * @param output_params Used to calculate bytes per frame and buffer size for + * resampling of the output side of the stream. NULL if output should not be + * resampled. + * @param target_rate The sampling rate after resampling for the input side of + * the stream, and/or the sampling rate prior to resampling of the output side + * of the stream. * @param callback A callback to request data for resampling. * @param user_ptr User data supplied to the data callback. * @param quality Quality of the resampler. @@ -47,8 +54,8 @@ cubeb_resampler * cubeb_resampler_create(cubeb_stream * stream, * @param input_buffer A buffer of input samples * @param input_frame_count The size of the buffer. Returns the number of frames * consumed. - * @param buffer The buffer to be filled. - * @param frames_needed Number of frames that should be produced. + * @param output_buffer The buffer to be filled. + * @param output_frames_needed Number of frames that should be produced. * @retval Number of frames that are actually produced. * @retval CUBEB_ERROR on error. */ diff --git a/media/libcubeb/src/cubeb_resampler_internal.h b/media/libcubeb/src/cubeb_resampler_internal.h index 3c37a04b9c..fb69992ff1 100644 --- a/media/libcubeb/src/cubeb_resampler_internal.h +++ b/media/libcubeb/src/cubeb_resampler_internal.h @@ -39,6 +39,13 @@ MOZ_END_STD_NAMESPACE /* This header file contains the internal C++ API of the resamplers, for testing. */ +// When dropping audio input frames to prevent building +// an input delay, this function returns the number of frames +// to keep in the buffer. +// @parameter sample_rate The sample rate of the stream. +// @return A number of frames to keep. +uint32_t min_buffered_audio_frame(uint32_t sample_rate); + int to_speex_quality(cubeb_resampler_quality q); struct cubeb_resampler { @@ -48,31 +55,6 @@ struct cubeb_resampler { virtual ~cubeb_resampler() {} }; -class noop_resampler : public cubeb_resampler { -public: - noop_resampler(cubeb_stream * s, - cubeb_data_callback cb, - void * ptr) - : stream(s) - , data_callback(cb) - , user_ptr(ptr) - { - } - - virtual long fill(void * input_buffer, long * input_frames_count, - void * output_buffer, long output_frames); - - virtual long latency() - { - return 0; - } - -private: - cubeb_stream * const stream; - const cubeb_data_callback data_callback; - void * const user_ptr; -}; - /** Base class for processors. This is just used to share methods for now. */ class processor { public: @@ -80,11 +62,11 @@ public: : channels(channels) {} protected: - size_t frames_to_samples(size_t frames) + size_t frames_to_samples(size_t frames) const { return frames * channels; } - size_t samples_to_frames(size_t samples) + size_t samples_to_frames(size_t samples) const { assert(!(samples % channels)); return samples / channels; @@ -93,6 +75,43 @@ protected: const uint32_t channels; }; +template<typename T> +class passthrough_resampler : public cubeb_resampler + , public processor { +public: + passthrough_resampler(cubeb_stream * s, + cubeb_data_callback cb, + void * ptr, + uint32_t input_channels, + uint32_t sample_rate); + + virtual long fill(void * input_buffer, long * input_frames_count, + void * output_buffer, long output_frames); + + virtual long latency() + { + return 0; + } + + void drop_audio_if_needed() + { + uint32_t to_keep = min_buffered_audio_frame(sample_rate); + uint32_t available = samples_to_frames(internal_input_buffer.length()); + if (available > to_keep) { + internal_input_buffer.pop(nullptr, frames_to_samples(available - to_keep)); + } + } + +private: + cubeb_stream * const stream; + const cubeb_data_callback data_callback; + void * const user_ptr; + /* This allows to buffer some input to account for the fact that we buffer + * some inputs. */ + auto_array<T> internal_input_buffer; + uint32_t sample_rate; +}; + /** Bidirectional resampler, can resample an input and an output stream, or just * an input stream or output stream. In this case a delay is inserted in the * opposite direction to keep the streams synchronized. */ @@ -138,6 +157,7 @@ private: cubeb_stream * const stream; const cubeb_data_callback data_callback; void * const user_ptr; + bool draining = false; }; /** Handles one way of a (possibly) duplex resampler, working on interleaved @@ -163,6 +183,7 @@ public: int quality) : processor(channels) , resampling_ratio(static_cast<float>(source_rate) / target_rate) + , source_rate(source_rate) , additional_latency(0) , leftover_samples(0) { @@ -221,7 +242,7 @@ public: /** Returns a buffer containing exactly `output_frame_count` resampled frames. * The consumer should not hold onto the pointer. */ - T * output(size_t output_frame_count) + T * output(size_t output_frame_count, size_t * input_frames_used) { if (resampling_out_buffer.capacity() < frames_to_samples(output_frame_count)) { resampling_out_buffer.reserve(frames_to_samples(output_frame_count)); @@ -238,6 +259,7 @@ public: /* This shifts back any unresampled samples to the beginning of the input buffer. */ resampling_in_buffer.pop(nullptr, frames_to_samples(in_len)); + *input_frames_used = in_len; return resampling_out_buffer.data(); } @@ -261,7 +283,7 @@ public: * exactly `output_frame_count` resampled frames. This can return a number * slightly bigger than what is strictly necessary, but it guaranteed that the * number of output frames will be exactly equal. */ - uint32_t input_needed_for_output(uint32_t output_frame_count) + uint32_t input_needed_for_output(uint32_t output_frame_count) const { int32_t unresampled_frames_left = samples_to_frames(resampling_in_buffer.length()); int32_t resampled_frames_left = samples_to_frames(resampling_out_buffer.length()); @@ -294,6 +316,16 @@ public: resampling_in_buffer.set_length(leftover_samples + frames_to_samples(written_frames)); } + + void drop_audio_if_needed() + { + // Keep at most 100ms buffered. + uint32_t available = samples_to_frames(resampling_in_buffer.length()); + uint32_t to_keep = min_buffered_audio_frame(source_rate); + if (available > to_keep) { + resampling_in_buffer.pop(nullptr, frames_to_samples(available - to_keep)); + } + } private: /** Wrapper for the speex resampling functions to have a typed * interface. */ @@ -330,6 +362,7 @@ private: SpeexResamplerState * speex_resampler; /** Source rate / target rate. */ const float resampling_ratio; + const uint32_t source_rate; /** Storage for the input frames, to be resampled. Also contains * any unresampled frames after resampling. */ auto_array<T> resampling_in_buffer; @@ -348,11 +381,13 @@ class delay_line : public processor { public: /** Constructor * @parameter frames the number of frames of delay. - * @parameter channels the number of channels of this delay line. */ - delay_line(uint32_t frames, uint32_t channels) + * @parameter channels the number of channels of this delay line. + * @parameter sample_rate sample-rate of the audio going through this delay line */ + delay_line(uint32_t frames, uint32_t channels, uint32_t sample_rate) : processor(channels) , length(frames) , leftover_samples(0) + , sample_rate(sample_rate) { /* Fill the delay line with some silent frames to add latency. */ delay_input_buffer.push_silence(frames * channels); @@ -375,7 +410,7 @@ public: * @parameter frames_needed the number of frames to be returned. * @return a buffer containing the delayed frames. The consumer should not * hold onto the pointer. */ - T * output(uint32_t frames_needed) + T * output(uint32_t frames_needed, size_t * input_frames_used) { if (delay_output_buffer.capacity() < frames_to_samples(frames_needed)) { delay_output_buffer.reserve(frames_to_samples(frames_needed)); @@ -385,6 +420,7 @@ public: delay_output_buffer.push(delay_input_buffer.data(), frames_to_samples(frames_needed)); delay_input_buffer.pop(nullptr, frames_to_samples(frames_needed)); + *input_frames_used = frames_needed; return delay_output_buffer.data(); } @@ -426,7 +462,7 @@ public: * @parameter frames_needed the number of frames one want to write into the * delay_line * @returns the number of frames one will get. */ - size_t input_needed_for_output(uint32_t frames_needed) + size_t input_needed_for_output(uint32_t frames_needed) const { return frames_needed; } @@ -441,6 +477,15 @@ public: { return length; } + + void drop_audio_if_needed() + { + size_t available = samples_to_frames(delay_input_buffer.length()); + uint32_t to_keep = min_buffered_audio_frame(sample_rate); + if (available > to_keep) { + delay_input_buffer.pop(nullptr, frames_to_samples(available - to_keep)); + } + } private: /** The length, in frames, of this delay line */ uint32_t length; @@ -452,6 +497,7 @@ private: /** The output buffer. This is only ever used if using the ::output with a * single argument. */ auto_array<T> delay_output_buffer; + uint32_t sample_rate; }; /** This sits behind the C API and is more typed. */ @@ -480,7 +526,10 @@ cubeb_resampler_create_internal(cubeb_stream * stream, (output_params && output_params->rate == target_rate)) || (input_params && !output_params && (input_params->rate == target_rate)) || (output_params && !input_params && (output_params->rate == target_rate))) { - return new noop_resampler(stream, callback, user_ptr); + return new passthrough_resampler<T>(stream, callback, + user_ptr, + input_params ? input_params->channels : 0, + target_rate); } /* Determine if we need to resampler one or both directions, and create the @@ -512,13 +561,15 @@ cubeb_resampler_create_internal(cubeb_stream * stream, * other direction so that the streams are synchronized. */ if (input_resampler && !output_resampler && input_params && output_params) { output_delay.reset(new delay_line<T>(input_resampler->latency(), - output_params->channels)); + output_params->channels, + output_params->rate)); if (!output_delay) { return NULL; } } else if (output_resampler && !input_resampler && input_params && output_params) { input_delay.reset(new delay_line<T>(output_resampler->latency(), - input_params->channels)); + input_params->channels, + output_params->rate)); if (!input_delay) { return NULL; } diff --git a/media/libcubeb/src/cubeb_ringbuffer.h b/media/libcubeb/src/cubeb_ringbuffer.h new file mode 100644 index 0000000000..b6696e886d --- /dev/null +++ b/media/libcubeb/src/cubeb_ringbuffer.h @@ -0,0 +1,495 @@ +/* + * Copyright © 2016 Mozilla Foundation + * + * This program is made available under an ISC-style license. See the + * accompanying file LICENSE for details. + */ + +#ifndef CUBEB_RING_BUFFER_H +#define CUBEB_RING_BUFFER_H + +#include "cubeb_utils.h" +#include <algorithm> +#include <atomic> +#include <cstdint> +#include <memory> +#include <thread> + +/** + * Single producer single consumer lock-free and wait-free ring buffer. + * + * This data structure allows producing data from one thread, and consuming it on + * another thread, safely and without explicit synchronization. If used on two + * threads, this data structure uses atomics for thread safety. It is possible + * to disable the use of atomics at compile time and only use this data + * structure on one thread. + * + * The role for the producer and the consumer must be constant, i.e., the + * producer should always be on one thread and the consumer should always be on + * another thread. + * + * Some words about the inner workings of this class: + * - Capacity is fixed. Only one allocation is performed, in the constructor. + * When reading and writing, the return value of the method allows checking if + * the ring buffer is empty or full. + * - We always keep the read index at least one element ahead of the write + * index, so we can distinguish between an empty and a full ring buffer: an + * empty ring buffer is when the write index is at the same position as the + * read index. A full buffer is when the write index is exactly one position + * before the read index. + * - We synchronize updates to the read index after having read the data, and + * the write index after having written the data. This means that the each + * thread can only touch a portion of the buffer that is not touched by the + * other thread. + * - Callers are expected to provide buffers. When writing to the queue, + * elements are copied into the internal storage from the buffer passed in. + * When reading from the queue, the user is expected to provide a buffer. + * Because this is a ring buffer, data might not be contiguous in memory, + * providing an external buffer to copy into is an easy way to have linear + * data for further processing. + */ +template <typename T> +class ring_buffer_base +{ +public: + /** + * Constructor for a ring buffer. + * + * This performs an allocation, but is the only allocation that will happen + * for the life time of a `ring_buffer_base`. + * + * @param capacity The maximum number of element this ring buffer will hold. + */ + ring_buffer_base(int capacity) + /* One more element to distinguish from empty and full buffer. */ + : capacity_(capacity + 1) + { + assert(storage_capacity() < + std::numeric_limits<int>::max() / 2 && + "buffer too large for the type of index used."); + assert(capacity_ > 0); + + data_.reset(new T[storage_capacity()]); + /* If this queue is using atomics, initializing those members as the last + * action in the constructor acts as a full barrier, and allow capacity() to + * be thread-safe. */ + write_index_ = 0; + read_index_ = 0; + } + /** + * Push `count` zero or default constructed elements in the array. + * + * Only safely called on the producer thread. + * + * @param count The number of elements to enqueue. + * @return The number of element enqueued. + */ + int enqueue_default(int count) + { + return enqueue(nullptr, count); + } + /** + * @brief Put an element in the queue + * + * Only safely called on the producer thread. + * + * @param element The element to put in the queue. + * + * @return 1 if the element was inserted, 0 otherwise. + */ + int enqueue(T& element) + { + return enqueue(&element, 1); + } + /** + * Push `count` elements in the ring buffer. + * + * Only safely called on the producer thread. + * + * @param elements a pointer to a buffer containing at least `count` elements. + * If `elements` is nullptr, zero or default constructed elements are enqueued. + * @param count The number of elements to read from `elements` + * @return The number of elements successfully coped from `elements` and inserted + * into the ring buffer. + */ + int enqueue(T * elements, int count) + { +#ifndef NDEBUG + assert_correct_thread(producer_id); +#endif + + int rd_idx = read_index_.load(std::memory_order::memory_order_relaxed); + int wr_idx = write_index_.load(std::memory_order::memory_order_relaxed); + + if (full_internal(rd_idx, wr_idx)) { + return 0; + } + + int to_write = + std::min(available_write_internal(rd_idx, wr_idx), count); + + /* First part, from the write index to the end of the array. */ + int first_part = std::min(storage_capacity() - wr_idx, + to_write); + /* Second part, from the beginning of the array */ + int second_part = to_write - first_part; + + if (elements) { + Copy(data_.get() + wr_idx, elements, first_part); + Copy(data_.get(), elements + first_part, second_part); + } else { + ConstructDefault(data_.get() + wr_idx, first_part); + ConstructDefault(data_.get(), second_part); + } + + write_index_.store(increment_index(wr_idx, to_write), std::memory_order::memory_order_release); + + return to_write; + } + /** + * Retrieve at most `count` elements from the ring buffer, and copy them to + * `elements`, if non-null. + * + * Only safely called on the consumer side. + * + * @param elements A pointer to a buffer with space for at least `count` + * elements. If `elements` is `nullptr`, `count` element will be discarded. + * @param count The maximum number of elements to dequeue. + * @return The number of elements written to `elements`. + */ + int dequeue(T * elements, int count) + { +#ifndef NDEBUG + assert_correct_thread(consumer_id); +#endif + + int wr_idx = write_index_.load(std::memory_order::memory_order_acquire); + int rd_idx = read_index_.load(std::memory_order::memory_order_relaxed); + + if (empty_internal(rd_idx, wr_idx)) { + return 0; + } + + int to_read = + std::min(available_read_internal(rd_idx, wr_idx), count); + + int first_part = std::min(storage_capacity() - rd_idx, to_read); + int second_part = to_read - first_part; + + if (elements) { + Copy(elements, data_.get() + rd_idx, first_part); + Copy(elements + first_part, data_.get(), second_part); + } + + read_index_.store(increment_index(rd_idx, to_read), std::memory_order::memory_order_relaxed); + + return to_read; + } + /** + * Get the number of available element for consuming. + * + * Only safely called on the consumer thread. + * + * @return The number of available elements for reading. + */ + int available_read() const + { +#ifndef NDEBUG + assert_correct_thread(consumer_id); +#endif + return available_read_internal(read_index_.load(std::memory_order::memory_order_relaxed), + write_index_.load(std::memory_order::memory_order_relaxed)); + } + /** + * Get the number of available elements for consuming. + * + * Only safely called on the producer thread. + * + * @return The number of empty slots in the buffer, available for writing. + */ + int available_write() const + { +#ifndef NDEBUG + assert_correct_thread(producer_id); +#endif + return available_write_internal(read_index_.load(std::memory_order::memory_order_relaxed), + write_index_.load(std::memory_order::memory_order_relaxed)); + } + /** + * Get the total capacity, for this ring buffer. + * + * Can be called safely on any thread. + * + * @return The maximum capacity of this ring buffer. + */ + int capacity() const + { + return storage_capacity() - 1; + } + /** + * Reset the consumer and producer thread identifier, in case the thread are + * being changed. This has to be externally synchronized. This is no-op when + * asserts are disabled. + */ + void reset_thread_ids() + { +#ifndef NDEBUG + consumer_id = producer_id = std::thread::id(); +#endif + } +private: + /** Return true if the ring buffer is empty. + * + * @param read_index the read index to consider + * @param write_index the write index to consider + * @return true if the ring buffer is empty, false otherwise. + **/ + bool empty_internal(int read_index, + int write_index) const + { + return write_index == read_index; + } + /** Return true if the ring buffer is full. + * + * This happens if the write index is exactly one element behind the read + * index. + * + * @param read_index the read index to consider + * @param write_index the write index to consider + * @return true if the ring buffer is full, false otherwise. + **/ + bool full_internal(int read_index, + int write_index) const + { + return (write_index + 1) % storage_capacity() == read_index; + } + /** + * Return the size of the storage. It is one more than the number of elements + * that can be stored in the buffer. + * + * @return the number of elements that can be stored in the buffer. + */ + int storage_capacity() const + { + return capacity_; + } + /** + * Returns the number of elements available for reading. + * + * @return the number of available elements for reading. + */ + int + available_read_internal(int read_index, + int write_index) const + { + if (write_index >= read_index) { + return write_index - read_index; + } else { + return write_index + storage_capacity() - read_index; + } + } + /** + * Returns the number of empty elements, available for writing. + * + * @return the number of elements that can be written into the array. + */ + int + available_write_internal(int read_index, + int write_index) const + { + /* We substract one element here to always keep at least one sample + * free in the buffer, to distinguish between full and empty array. */ + int rv = read_index - write_index - 1; + if (write_index >= read_index) { + rv += storage_capacity(); + } + return rv; + } + /** + * Increments an index, wrapping it around the storage. + * + * @param index a reference to the index to increment. + * @param increment the number by which `index` is incremented. + * @return the new index. + */ + int + increment_index(int index, int increment) const + { + assert(increment >= 0); + return (index + increment) % storage_capacity(); + } + /** + * @brief This allows checking that enqueue (resp. dequeue) are always called + * by the right thread. + * + * @param id the id of the thread that has called the calling method first. + */ +#ifndef NDEBUG + static void assert_correct_thread(std::thread::id& id) + { + if (id == std::thread::id()) { + id = std::this_thread::get_id(); + return; + } + assert(id == std::this_thread::get_id()); + } +#endif + /** Index at which the oldest element is at, in samples. */ + std::atomic<int> read_index_; + /** Index at which to write new elements. `write_index` is always at + * least one element ahead of `read_index_`. */ + std::atomic<int> write_index_; + /** Maximum number of elements that can be stored in the ring buffer. */ + const int capacity_; + /** Data storage */ + std::unique_ptr<T[]> data_; +#ifndef NDEBUG + /** The id of the only thread that is allowed to read from the queue. */ + mutable std::thread::id consumer_id; + /** The id of the only thread that is allowed to write from the queue. */ + mutable std::thread::id producer_id; +#endif +}; + +/** + * Adapter for `ring_buffer_base` that exposes an interface in frames. + */ +template <typename T> +class audio_ring_buffer_base +{ +public: + /** + * @brief Constructor. + * + * @param channel_count Number of channels. + * @param capacity_in_frames The capacity in frames. + */ + audio_ring_buffer_base(int channel_count, int capacity_in_frames) + : channel_count(channel_count) + , ring_buffer(frames_to_samples(capacity_in_frames)) + { + assert(channel_count > 0); + } + /** + * @brief Enqueue silence. + * + * Only safely called on the producer thread. + * + * @param frame_count The number of frames of silence to enqueue. + * @return The number of frames of silence actually written to the queue. + */ + int enqueue_default(int frame_count) + { + return samples_to_frames(ring_buffer.enqueue(nullptr, frames_to_samples(frame_count))); + } + /** + * @brief Enqueue `frames_count` frames of audio. + * + * Only safely called from the producer thread. + * + * @param [in] frames If non-null, the frames to enqueue. + * Otherwise, silent frames are enqueued. + * @param frame_count The number of frames to enqueue. + * + * @return The number of frames enqueued + */ + + int enqueue(T * frames, int frame_count) + { + return samples_to_frames(ring_buffer.enqueue(frames, frames_to_samples(frame_count))); + } + + /** + * @brief Removes `frame_count` frames from the buffer, and + * write them to `frames` if it is non-null. + * + * Only safely called on the consumer thread. + * + * @param frames If non-null, the frames are copied to `frames`. + * Otherwise, they are dropped. + * @param frame_count The number of frames to remove. + * + * @return The number of frames actually dequeud. + */ + int dequeue(T * frames, int frame_count) + { + return samples_to_frames(ring_buffer.dequeue(frames, frames_to_samples(frame_count))); + } + /** + * Get the number of available frames of audio for consuming. + * + * Only safely called on the consumer thread. + * + * @return The number of available frames of audio for reading. + */ + int available_read() const + { + return samples_to_frames(ring_buffer.available_read()); + } + /** + * Get the number of available frames of audio for consuming. + * + * Only safely called on the producer thread. + * + * @return The number of empty slots in the buffer, available for writing. + */ + int available_write() const + { + return samples_to_frames(ring_buffer.available_write()); + } + /** + * Get the total capacity, for this ring buffer. + * + * Can be called safely on any thread. + * + * @return The maximum capacity of this ring buffer. + */ + int capacity() const + { + return samples_to_frames(ring_buffer.capacity()); + } +private: + /** + * @brief Frames to samples conversion. + * + * @param frames The number of frames. + * + * @return A number of samples. + */ + int frames_to_samples(int frames) const + { + return frames * channel_count; + } + /** + * @brief Samples to frames conversion. + * + * @param samples The number of samples. + * + * @return A number of frames. + */ + int samples_to_frames(int samples) const + { + return samples / channel_count; + } + /** Number of channels of audio that will stream through this ring buffer. */ + int channel_count; + /** The underlying ring buffer that is used to store the data. */ + ring_buffer_base<T> ring_buffer; +}; + +/** + * Lock-free instantiation of the `ring_buffer_base` type. This is safe to use + * from two threads, one producer, one consumer (that never change role), + * without explicit synchronization. + */ +template<typename T> +using lock_free_queue = ring_buffer_base<T>; +/** + * Lock-free instantiation of the `audio_ring_buffer` type. This is safe to use + * from two threads, one producer, one consumer (that never change role), + * without explicit synchronization. + */ +template<typename T> +using lock_free_audio_ring_buffer = audio_ring_buffer_base<T>; + +#endif // CUBEB_RING_BUFFER_H diff --git a/media/libcubeb/src/cubeb_sndio.c b/media/libcubeb/src/cubeb_sndio.c index 7937897658..4a05bd8450 100644 --- a/media/libcubeb/src/cubeb_sndio.c +++ b/media/libcubeb/src/cubeb_sndio.c @@ -4,6 +4,7 @@ * This program is made available under an ISC-style license. See the * accompanying file LICENSE for details. */ +#include <inttypes.h> #include <math.h> #include <poll.h> #include <pthread.h> @@ -11,6 +12,7 @@ #include <stdbool.h> #include <stdlib.h> #include <stdio.h> +#include <dlfcn.h> #include <assert.h> #include "cubeb/cubeb.h" #include "cubeb-internal.h" @@ -21,39 +23,87 @@ #define DPR(...) do {} while(0) #endif +#ifdef DISABLE_LIBSNDIO_DLOPEN +#define WRAP(x) x +#else +#define WRAP(x) cubeb_##x +#define LIBSNDIO_API_VISIT(X) \ + X(sio_close) \ + X(sio_eof) \ + X(sio_getpar) \ + X(sio_initpar) \ + X(sio_onmove) \ + X(sio_open) \ + X(sio_pollfd) \ + X(sio_read) \ + X(sio_revents) \ + X(sio_setpar) \ + X(sio_start) \ + X(sio_stop) \ + X(sio_write) \ + +#define MAKE_TYPEDEF(x) static typeof(x) * cubeb_##x; +LIBSNDIO_API_VISIT(MAKE_TYPEDEF); +#undef MAKE_TYPEDEF +#endif + static struct cubeb_ops const sndio_ops; struct cubeb { struct cubeb_ops const * ops; + void * libsndio; }; struct cubeb_stream { + /* Note: Must match cubeb_stream layout in cubeb.c. */ cubeb * context; - pthread_t th; /* to run real-time audio i/o */ - pthread_mutex_t mtx; /* protects hdl and pos */ - struct sio_hdl *hdl; /* link us to sndio */ - int active; /* cubec_start() called */ - int conv; /* need float->s16 conversion */ - unsigned char *buf; /* data is prepared here */ - unsigned int nfr; /* number of frames in buf */ - unsigned int bpf; /* bytes per frame */ - unsigned int pchan; /* number of play channels */ - uint64_t rdpos; /* frame number Joe hears right now */ - uint64_t wrpos; /* number of written frames */ + void * arg; /* user arg to {data,state}_cb */ + /**/ + pthread_t th; /* to run real-time audio i/o */ + pthread_mutex_t mtx; /* protects hdl and pos */ + struct sio_hdl *hdl; /* link us to sndio */ + int mode; /* bitmap of SIO_{PLAY,REC} */ + int active; /* cubec_start() called */ + int conv; /* need float->s16 conversion */ + unsigned char *rbuf; /* rec data consumed from here */ + unsigned char *pbuf; /* play data is prepared here */ + unsigned int nfr; /* number of frames in ibuf and obuf */ + unsigned int rbpf; /* rec bytes per frame */ + unsigned int pbpf; /* play bytes per frame */ + unsigned int rchan; /* number of rec channels */ + unsigned int pchan; /* number of play channels */ + unsigned int nblks; /* number of blocks in the buffer */ + uint64_t hwpos; /* frame number Joe hears right now */ + uint64_t swpos; /* number of frames produced/consumed */ cubeb_data_callback data_cb; /* cb to preapare data */ cubeb_state_callback state_cb; /* cb to notify about state changes */ - void *arg; /* user arg to {data,state}_cb */ + float volume; /* current volume */ }; static void -float_to_s16(void *ptr, long nsamp) +s16_setvol(void *ptr, long nsamp, float volume) +{ + int16_t *dst = ptr; + int32_t mult = volume * 32768; + int32_t s; + + while (nsamp-- > 0) { + s = *dst; + s = (s * mult) >> 15; + *(dst++) = s; + } +} + +static void +float_to_s16(void *ptr, long nsamp, float volume) { int16_t *dst = ptr; float *src = ptr; + float mult = volume * 32768; int s; while (nsamp-- > 0) { - s = lrintf(*(src++) * 32768); + s = lrintf(*(src++) * mult); if (s < -32768) s = -32768; else if (s > 32767) @@ -63,11 +113,23 @@ float_to_s16(void *ptr, long nsamp) } static void +s16_to_float(void *ptr, long nsamp) +{ + int16_t *src = ptr; + float *dst = ptr; + + src += nsamp; + dst += nsamp; + while (nsamp-- > 0) + *(--dst) = (1. / 32768) * *(--src); +} + +static void sndio_onmove(void *arg, int delta) { cubeb_stream *s = (cubeb_stream *)arg; - s->rdpos += delta * s->bpf; + s->hwpos += delta; } static void * @@ -76,48 +138,99 @@ sndio_mainloop(void *arg) #define MAXFDS 8 struct pollfd pfds[MAXFDS]; cubeb_stream *s = arg; - int n, nfds, revents, state = CUBEB_STATE_STARTED; - size_t start = 0, end = 0; + int n, eof = 0, prime, nfds, events, revents, state = CUBEB_STATE_STARTED; + size_t pstart = 0, pend = 0, rstart = 0, rend = 0; long nfr; DPR("sndio_mainloop()\n"); s->state_cb(s, s->arg, CUBEB_STATE_STARTED); pthread_mutex_lock(&s->mtx); - if (!sio_start(s->hdl)) { + if (!WRAP(sio_start)(s->hdl)) { pthread_mutex_unlock(&s->mtx); return NULL; } DPR("sndio_mainloop(), started\n"); - start = end = s->nfr; + if (s->mode & SIO_PLAY) { + pstart = pend = s->nfr * s->pbpf; + prime = s->nblks; + if (s->mode & SIO_REC) { + memset(s->rbuf, 0, s->nfr * s->rbpf); + rstart = rend = s->nfr * s->rbpf; + } + } else { + prime = 0; + rstart = 0; + rend = s->nfr * s->rbpf; + } + for (;;) { if (!s->active) { DPR("sndio_mainloop() stopped\n"); state = CUBEB_STATE_STOPPED; break; } - if (start == end) { - if (end < s->nfr) { + + /* do we have a complete block? */ + if ((!(s->mode & SIO_PLAY) || pstart == pend) && + (!(s->mode & SIO_REC) || rstart == rend)) { + + if (eof) { DPR("sndio_mainloop() drained\n"); state = CUBEB_STATE_DRAINED; break; } + + if ((s->mode & SIO_REC) && s->conv) + s16_to_float(s->rbuf, s->nfr * s->rchan); + + /* invoke call-back, it returns less that s->nfr if done */ pthread_mutex_unlock(&s->mtx); - nfr = s->data_cb(s, s->arg, NULL, s->buf, s->nfr); + nfr = s->data_cb(s, s->arg, s->rbuf, s->pbuf, s->nfr); pthread_mutex_lock(&s->mtx); if (nfr < 0) { DPR("sndio_mainloop() cb err\n"); state = CUBEB_STATE_ERROR; break; } - if (s->conv) - float_to_s16(s->buf, nfr * s->pchan); - start = 0; - end = nfr * s->bpf; + s->swpos += nfr; + + /* was this last call-back invocation (aka end-of-stream) ? */ + if (nfr < s->nfr) { + + if (!(s->mode & SIO_PLAY) || nfr == 0) { + state = CUBEB_STATE_DRAINED; + break; + } + + /* need to write (aka drain) the partial play block we got */ + pend = nfr * s->pbpf; + eof = 1; + } + + if (prime > 0) + prime--; + + if (s->mode & SIO_PLAY) { + if (s->conv) + float_to_s16(s->pbuf, nfr * s->pchan, s->volume); + else + s16_setvol(s->pbuf, nfr * s->pchan, s->volume); + } + + if (s->mode & SIO_REC) + rstart = 0; + if (s->mode & SIO_PLAY) + pstart = 0; } - if (end == 0) - continue; - nfds = sio_pollfd(s->hdl, pfds, POLLOUT); + + events = 0; + if ((s->mode & SIO_REC) && rstart < rend && prime == 0) + events |= POLLIN; + if ((s->mode & SIO_PLAY) && pstart < pend) + events |= POLLOUT; + nfds = WRAP(sio_pollfd)(s->hdl, pfds, events); + if (nfds > 0) { pthread_mutex_unlock(&s->mtx); n = poll(pfds, nfds, -1); @@ -125,22 +238,40 @@ sndio_mainloop(void *arg) if (n < 0) continue; } - revents = sio_revents(s->hdl, pfds); - if (revents & POLLHUP) + + revents = WRAP(sio_revents)(s->hdl, pfds); + + if (revents & POLLHUP) { + state = CUBEB_STATE_ERROR; break; + } + if (revents & POLLOUT) { - n = sio_write(s->hdl, s->buf + start, end - start); - if (n == 0) { + n = WRAP(sio_write)(s->hdl, s->pbuf + pstart, pend - pstart); + if (n == 0 && WRAP(sio_eof)(s->hdl)) { DPR("sndio_mainloop() werr\n"); state = CUBEB_STATE_ERROR; break; } - s->wrpos += n; - start += n; + pstart += n; } + + if (revents & POLLIN) { + n = WRAP(sio_read)(s->hdl, s->rbuf + rstart, rend - rstart); + if (n == 0 && WRAP(sio_eof)(s->hdl)) { + DPR("sndio_mainloop() rerr\n"); + state = CUBEB_STATE_ERROR; + break; + } + rstart += n; + } + + /* skip rec block, if not recording (yet) */ + if (prime > 0 && (s->mode & SIO_REC)) + rstart = rend; } - sio_stop(s->hdl); - s->rdpos = s->wrpos; + WRAP(sio_stop)(s->hdl); + s->hwpos = s->swpos; pthread_mutex_unlock(&s->mtx); s->state_cb(s, s->arg, state); return NULL; @@ -149,8 +280,34 @@ sndio_mainloop(void *arg) /*static*/ int sndio_init(cubeb **context, char const *context_name) { + void * libsndio = NULL; + +#ifndef DISABLE_LIBSNDIO_DLOPEN + libsndio = dlopen("libsndio.so.7.0", RTLD_LAZY); + if (!libsndio) { + libsndio = dlopen("libsndio.so", RTLD_LAZY); + if (!libsndio) { + DPR("sndio_init(%s) failed dlopen(libsndio.so)\n", context_name); + return CUBEB_ERROR; + } + } + +#define LOAD(x) { \ + cubeb_##x = dlsym(libsndio, #x); \ + if (!cubeb_##x) { \ + DPR("sndio_init(%s) failed dlsym(%s)\n", context_name, #x); \ + dlclose(libsndio); \ + return CUBEB_ERROR; \ + } \ + } + + LIBSNDIO_API_VISIT(LOAD); +#undef LOAD +#endif + DPR("sndio_init(%s)\n", context_name); *context = malloc(sizeof(*context)); + (*context)->libsndio = libsndio; (*context)->ops = &sndio_ops; (void)context_name; return CUBEB_OK; @@ -166,6 +323,8 @@ static void sndio_destroy(cubeb *context) { DPR("sndio_destroy()\n"); + if (context->libsndio) + dlclose(context->libsndio); free(context); } @@ -184,29 +343,49 @@ sndio_stream_init(cubeb * context, { cubeb_stream *s; struct sio_par wpar, rpar; - DPR("sndio_stream_init(%s)\n", stream_name); - size_t size; + cubeb_sample_format format; + int rate; + size_t bps; - assert(!input_stream_params && "not supported."); - if (input_device || output_device) { - /* Device selection not yet implemented. */ - return CUBEB_ERROR_DEVICE_UNAVAILABLE; - } + DPR("sndio_stream_init(%s)\n", stream_name); s = malloc(sizeof(cubeb_stream)); if (s == NULL) return CUBEB_ERROR; + memset(s, 0, sizeof(cubeb_stream)); + s->mode = 0; + if (input_stream_params) { + if (input_stream_params->prefs & CUBEB_STREAM_PREF_LOOPBACK) { + DPR("sndio_stream_init(), loopback not supported\n"); + goto err; + } + s->mode |= SIO_REC; + format = input_stream_params->format; + rate = input_stream_params->rate; + } + if (output_stream_params) { + if (output_stream_params->prefs & CUBEB_STREAM_PREF_LOOPBACK) { + DPR("sndio_stream_init(), loopback not supported\n"); + goto err; + } + s->mode |= SIO_PLAY; + format = output_stream_params->format; + rate = output_stream_params->rate; + } + if (s->mode == 0) { + DPR("sndio_stream_init(), neither playing nor recording\n"); + goto err; + } s->context = context; - s->hdl = sio_open(NULL, SIO_PLAY, 1); + s->hdl = WRAP(sio_open)(NULL, s->mode, 1); if (s->hdl == NULL) { - free(s); DPR("sndio_stream_init(), sio_open() failed\n"); - return CUBEB_ERROR; + goto err; } - sio_initpar(&wpar); + WRAP(sio_initpar)(&wpar); wpar.sig = 1; wpar.bits = 16; - switch (output_stream_params->format) { + switch (format) { case CUBEB_SAMPLE_S16LE: wpar.le = 1; break; @@ -218,53 +397,70 @@ sndio_stream_init(cubeb * context, break; default: DPR("sndio_stream_init() unsupported format\n"); - return CUBEB_ERROR_INVALID_FORMAT; + goto err; } - wpar.rate = output_stream_params->rate; - wpar.pchan = output_stream_params->channels; + wpar.rate = rate; + if (s->mode & SIO_REC) + wpar.rchan = input_stream_params->channels; + if (s->mode & SIO_PLAY) + wpar.pchan = output_stream_params->channels; wpar.appbufsz = latency_frames; - if (!sio_setpar(s->hdl, &wpar) || !sio_getpar(s->hdl, &rpar)) { - sio_close(s->hdl); - free(s); + if (!WRAP(sio_setpar)(s->hdl, &wpar) || !WRAP(sio_getpar)(s->hdl, &rpar)) { DPR("sndio_stream_init(), sio_setpar() failed\n"); - return CUBEB_ERROR; + goto err; } if (rpar.bits != wpar.bits || rpar.le != wpar.le || rpar.sig != wpar.sig || rpar.rate != wpar.rate || - rpar.pchan != wpar.pchan) { - sio_close(s->hdl); - free(s); + ((s->mode & SIO_REC) && rpar.rchan != wpar.rchan) || + ((s->mode & SIO_PLAY) && rpar.pchan != wpar.pchan)) { DPR("sndio_stream_init() unsupported params\n"); - return CUBEB_ERROR_INVALID_FORMAT; + goto err; } - sio_onmove(s->hdl, sndio_onmove, s); + WRAP(sio_onmove)(s->hdl, sndio_onmove, s); s->active = 0; s->nfr = rpar.round; - s->bpf = rpar.bps * rpar.pchan; + s->rbpf = rpar.bps * rpar.rchan; + s->pbpf = rpar.bps * rpar.pchan; + s->rchan = rpar.rchan; s->pchan = rpar.pchan; + s->nblks = rpar.bufsz / rpar.round; s->data_cb = data_callback; s->state_cb = state_callback; s->arg = user_ptr; - s->mtx = PTHREAD_MUTEX_INITIALIZER; - s->rdpos = s->wrpos = 0; - if (output_stream_params->format == CUBEB_SAMPLE_FLOAT32LE) { + s->mtx = (pthread_mutex_t)PTHREAD_MUTEX_INITIALIZER; + s->hwpos = s->swpos = 0; + if (format == CUBEB_SAMPLE_FLOAT32LE) { s->conv = 1; - size = rpar.round * rpar.pchan * sizeof(float); + bps = sizeof(float); } else { s->conv = 0; - size = rpar.round * rpar.pchan * rpar.bps; + bps = rpar.bps; } - s->buf = malloc(size); - if (s->buf == NULL) { - sio_close(s->hdl); - free(s); - return CUBEB_ERROR; + if (s->mode & SIO_PLAY) { + s->pbuf = malloc(bps * rpar.pchan * rpar.round); + if (s->pbuf == NULL) + goto err; } + if (s->mode & SIO_REC) { + s->rbuf = malloc(bps * rpar.rchan * rpar.round); + if (s->rbuf == NULL) + goto err; + } + s->volume = 1.; *stream = s; DPR("sndio_stream_init() end, ok\n"); (void)context; (void)stream_name; return CUBEB_OK; +err: + if (s->hdl) + WRAP(sio_close)(s->hdl); + if (s->pbuf) + free(s->pbuf); + if (s->rbuf) + free(s->pbuf); + free(s); + return CUBEB_ERROR; } static int @@ -280,16 +476,21 @@ sndio_get_max_channel_count(cubeb * ctx, uint32_t * max_channels) static int sndio_get_preferred_sample_rate(cubeb * ctx, uint32_t * rate) { - // XXX Not yet implemented. - *rate = 44100; - + /* + * We've no device-independent prefered rate; any rate will work if + * sndiod is running. If it isn't, 48kHz is what is most likely to + * work as most (but not all) devices support it. + */ + *rate = 48000; return CUBEB_OK; } static int sndio_get_min_latency(cubeb * ctx, cubeb_stream_params params, uint32_t * latency_frames) { - // XXX Not yet implemented. + /* + * We've no device-independent minimum latency. + */ *latency_frames = 2048; return CUBEB_OK; @@ -299,7 +500,11 @@ static void sndio_stream_destroy(cubeb_stream *s) { DPR("sndio_stream_destroy()\n"); - sio_close(s->hdl); + WRAP(sio_close)(s->hdl); + if (s->mode & SIO_PLAY) + free(s->pbuf); + if (s->mode & SIO_REC) + free(s->rbuf); free(s); } @@ -335,8 +540,8 @@ static int sndio_stream_get_position(cubeb_stream *s, uint64_t *p) { pthread_mutex_lock(&s->mtx); - DPR("sndio_stream_get_position() %lld\n", s->rdpos); - *p = s->rdpos / s->bpf; + DPR("sndio_stream_get_position() %" PRId64 "\n", s->hwpos); + *p = s->hwpos; pthread_mutex_unlock(&s->mtx); return CUBEB_OK; } @@ -346,7 +551,11 @@ sndio_stream_set_volume(cubeb_stream *s, float volume) { DPR("sndio_stream_set_volume(%f)\n", volume); pthread_mutex_lock(&s->mtx); - sio_setvol(s->hdl, SIO_MAXVOL * volume); + if (volume < 0.) + volume = 0.; + else if (volume > 1.0) + volume = 1.; + s->volume = volume; pthread_mutex_unlock(&s->mtx); return CUBEB_OK; } @@ -356,7 +565,47 @@ sndio_stream_get_latency(cubeb_stream * stm, uint32_t * latency) { // http://www.openbsd.org/cgi-bin/man.cgi?query=sio_open // in the "Measuring the latency and buffers usage" paragraph. - *latency = (stm->wrpos - stm->rdpos) / stm->bpf; + *latency = stm->swpos - stm->hwpos; + return CUBEB_OK; +} + +static int +sndio_enumerate_devices(cubeb *context, cubeb_device_type type, + cubeb_device_collection *collection) +{ + static char dev[] = SIO_DEVANY; + cubeb_device_info *device; + + device = malloc(sizeof(cubeb_device_info)); + if (device == NULL) + return CUBEB_ERROR; + + device->devid = dev; /* passed to stream_init() */ + device->device_id = dev; /* printable in UI */ + device->friendly_name = dev; /* same, but friendly */ + device->group_id = dev; /* actual device if full-duplex */ + device->vendor_name = NULL; /* may be NULL */ + device->type = type; /* Input/Output */ + device->state = CUBEB_DEVICE_STATE_ENABLED; + device->preferred = CUBEB_DEVICE_PREF_ALL; + device->format = CUBEB_DEVICE_FMT_S16NE; + device->default_format = CUBEB_DEVICE_FMT_S16NE; + device->max_channels = 16; + device->default_rate = 48000; + device->min_rate = 4000; + device->max_rate = 192000; + device->latency_lo = 480; + device->latency_hi = 9600; + collection->device = device; + collection->count = 1; + return CUBEB_OK; +} + +static int +sndio_device_collection_destroy(cubeb * context, + cubeb_device_collection * collection) +{ + free(collection->device); return CUBEB_OK; } @@ -366,16 +615,17 @@ static struct cubeb_ops const sndio_ops = { .get_max_channel_count = sndio_get_max_channel_count, .get_min_latency = sndio_get_min_latency, .get_preferred_sample_rate = sndio_get_preferred_sample_rate, - .enumerate_devices = NULL, + .enumerate_devices = sndio_enumerate_devices, + .device_collection_destroy = sndio_device_collection_destroy, .destroy = sndio_destroy, .stream_init = sndio_stream_init, .stream_destroy = sndio_stream_destroy, .stream_start = sndio_stream_start, .stream_stop = sndio_stream_stop, + .stream_reset_default_device = NULL, .stream_get_position = sndio_stream_get_position, .stream_get_latency = sndio_stream_get_latency, .stream_set_volume = sndio_stream_set_volume, - .stream_set_panning = NULL, .stream_get_current_device = NULL, .stream_device_destroy = NULL, .stream_register_device_changed_callback = NULL, diff --git a/media/libcubeb/src/cubeb_strings.c b/media/libcubeb/src/cubeb_strings.c new file mode 100644 index 0000000000..79d7d21b38 --- /dev/null +++ b/media/libcubeb/src/cubeb_strings.c @@ -0,0 +1,155 @@ +/* + * Copyright © 2011 Mozilla Foundation + * + * This program is made available under an ISC-style license. See the + * accompanying file LICENSE for details. + */ + +#include "cubeb_strings.h" + +#include <assert.h> +#include <stdlib.h> +#include <string.h> + +#define CUBEB_STRINGS_INLINE_COUNT 4 + +struct cubeb_strings { + uint32_t size; + uint32_t count; + char ** data; + char * small_store[CUBEB_STRINGS_INLINE_COUNT]; +}; + +int +cubeb_strings_init(cubeb_strings ** strings) +{ + cubeb_strings* strs = NULL; + + if (!strings) { + return CUBEB_ERROR; + } + + strs = calloc(1, sizeof(cubeb_strings)); + assert(strs); + + if (!strs) { + return CUBEB_ERROR; + } + + strs->size = sizeof(strs->small_store) / sizeof(strs->small_store[0]); + strs->count = 0; + strs->data = strs->small_store; + + *strings = strs; + + return CUBEB_OK; +} + +void +cubeb_strings_destroy(cubeb_strings * strings) +{ + char ** sp = NULL; + char ** se = NULL; + + if (!strings) { + return; + } + + sp = strings->data; + se = sp + strings->count; + + for ( ; sp != se; sp++) { + if (*sp) { + free(*sp); + } + } + + if (strings->data != strings->small_store) { + free(strings->data); + } + + free(strings); +} + +/** Look for string in string storage. + @param strings Opaque pointer to interned string storage. + @param s String to look up. + @retval Read-only string or NULL if not found. */ +static char const * +cubeb_strings_lookup(cubeb_strings * strings, char const * s) +{ + char ** sp = NULL; + char ** se = NULL; + + if (!strings || !s) { + return NULL; + } + + sp = strings->data; + se = sp + strings->count; + + for ( ; sp != se; sp++) { + if (*sp && strcmp(*sp, s) == 0) { + return *sp; + } + } + + return NULL; +} + +static char const * +cubeb_strings_push(cubeb_strings * strings, char const * s) +{ + char * is = NULL; + + if (strings->count == strings->size) { + char ** new_data; + uint32_t value_size = sizeof(char const *); + uint32_t new_size = strings->size * 2; + if (!new_size || value_size > (uint32_t)-1 / new_size) { + // overflow + return NULL; + } + + if (strings->small_store == strings->data) { + // First time heap allocation. + new_data = malloc(new_size * value_size); + if (new_data) { + memcpy(new_data, strings->small_store, sizeof(strings->small_store)); + } + } else { + new_data = realloc(strings->data, new_size * value_size); + } + + if (!new_data) { + // out of memory + return NULL; + } + + strings->size = new_size; + strings->data = new_data; + } + + is = strdup(s); + strings->data[strings->count++] = is; + + return is; +} + +char const * +cubeb_strings_intern(cubeb_strings * strings, char const * s) +{ + char const * is = NULL; + + if (!strings || !s) { + return NULL; + } + + is = cubeb_strings_lookup(strings, s); + if (is) { + return is; + } + + return cubeb_strings_push(strings, s); +} + diff --git a/media/libcubeb/src/cubeb_strings.h b/media/libcubeb/src/cubeb_strings.h new file mode 100644 index 0000000000..a918a01c5d --- /dev/null +++ b/media/libcubeb/src/cubeb_strings.h @@ -0,0 +1,44 @@ +/* + * Copyright © 2011 Mozilla Foundation + * + * This program is made available under an ISC-style license. See the + * accompanying file LICENSE for details. + */ + +#ifndef CUBEB_STRINGS_H +#define CUBEB_STRINGS_H + +#include "cubeb/cubeb.h" + +#if defined(__cplusplus) +extern "C" { +#endif + +/** Opaque handle referencing interned string storage. */ +typedef struct cubeb_strings cubeb_strings; + +/** Initialize an interned string structure. + @param strings An out param where an opaque pointer to the + interned string storage will be returned. + @retval CUBEB_OK in case of success. + @retval CUBEB_ERROR in case of error. */ +CUBEB_EXPORT int cubeb_strings_init(cubeb_strings ** strings); + +/** Destroy an interned string structure freeing all associated memory. + @param strings An opaque pointer to the interned string storage to + destroy. */ +CUBEB_EXPORT void cubeb_strings_destroy(cubeb_strings * strings); + +/** Add string to internal storage. + @param strings Opaque pointer to interned string storage. + @param s String to add to storage. + @retval CUBEB_OK + @retval CUBEB_ERROR + */ +CUBEB_EXPORT char const * cubeb_strings_intern(cubeb_strings * strings, char const * s); + +#if defined(__cplusplus) +} +#endif + +#endif // !CUBEB_STRINGS_H diff --git a/media/libcubeb/src/cubeb_sun.c b/media/libcubeb/src/cubeb_sun.c index b768bca561..64ab0b5b14 100644 --- a/media/libcubeb/src/cubeb_sun.c +++ b/media/libcubeb/src/cubeb_sun.c @@ -1,504 +1,752 @@ /* - * Copyright (c) 2013, 2017 Ginn Chen <ginnchen@gmail.com> + * Copyright © 2019 Nia Alarie * * This program is made available under an ISC-style license. See the * accompanying file LICENSE for details. */ -#include <poll.h> +#include <sys/audioio.h> +#include <sys/ioctl.h> +#include <fcntl.h> +#include <unistd.h> #include <pthread.h> #include <stdlib.h> #include <stdio.h> -#include <errno.h> -#include <fcntl.h> -#include <sys/audio.h> -#include <sys/stat.h> -#include <unistd.h> -#include <sys/stropts.h> +#include <string.h> +#include <math.h> #include "cubeb/cubeb.h" #include "cubeb-internal.h" -/* Macros copied from audio_oss.h */ -/* - * CDDL HEADER START - * - * The contents of this file are subject to the terms of the - * Common Development and Distribution License (the "License"). - * You may not use this file except in compliance with the License. - * - * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE - * or http://www.opensolaris.org/os/licensing. - * See the License for the specific language governing permissions - * and limitations under the License. - * - * When distributing Covered Code, include this CDDL HEADER in each - * file and include the License file at usr/src/OPENSOLARIS.LICENSE. - * If applicable, add the following below this CDDL HEADER, with the - * fields enclosed by brackets "[]" replaced with your own identifying - * information: Portions Copyright [yyyy] [name of copyright owner] - * - * CDDL HEADER END - */ +#define BYTES_TO_FRAMES(bytes, channels) \ + (bytes / (channels * sizeof(int16_t))) + +#define FRAMES_TO_BYTES(frames, channels) \ + (frames * (channels * sizeof(int16_t))) + +/* Default to 4 + 1 for the default device. */ +#ifndef SUN_DEVICE_COUNT +#define SUN_DEVICE_COUNT (5) +#endif + +/* Supported well by most hardware. */ +#ifndef SUN_PREFER_RATE +#define SUN_PREFER_RATE (48000) +#endif + +/* Standard acceptable minimum. */ +#ifndef SUN_LATENCY_MS +#define SUN_LATENCY_MS (40) +#endif + +#ifndef SUN_DEFAULT_DEVICE +#define SUN_DEFAULT_DEVICE "/dev/audio" +#endif + +#ifndef SUN_POLL_TIMEOUT +#define SUN_POLL_TIMEOUT (1000) +#endif + +#ifndef SUN_BUFFER_FRAMES +#define SUN_BUFFER_FRAMES (32) +#endif + /* - * Copyright (C) 4Front Technologies 1996-2008. - * - * Copyright 2009 Sun Microsystems, Inc. All rights reserved. - * Use is subject to license terms. + * Supported on NetBSD regardless of hardware. */ -#define OSSIOCPARM_MASK 0x1fff /* parameters must be < 8192 bytes */ -#define OSSIOC_VOID 0x00000000 /* no parameters */ -#define OSSIOC_OUT 0x20000000 /* copy out parameters */ -#define OSSIOC_IN 0x40000000 /* copy in parameters */ -#define OSSIOC_INOUT (OSSIOC_IN|OSSIOC_OUT) -#define OSSIOC_SZ(t) ((sizeof (t) & OSSIOCPARM_MASK) << 16) -#define __OSSIO(x, y) ((int)(OSSIOC_VOID|(x<<8)|y)) -#define __OSSIOR(x, y, t) ((int)(OSSIOC_OUT|OSSIOC_SZ(t)|(x<<8)|y)) -#define __OSSIOWR(x, y, t) ((int)(OSSIOC_INOUT|OSSIOC_SZ(t)|(x<<8)|y)) -#define SNDCTL_DSP_SPEED __OSSIOWR('P', 2, int) -#define SNDCTL_DSP_CHANNELS __OSSIOWR('P', 6, int) -#define SNDCTL_DSP_SETFMT __OSSIOWR('P', 5, int) /* Selects ONE fmt */ -#define SNDCTL_DSP_GETODELAY __OSSIOR('P', 23, int) -#define SNDCTL_DSP_HALT_OUTPUT __OSSIO('P', 34) -#define AFMT_S16_LE 0x00000010 -#define AFMT_S16_BE 0x00000020 - -#if defined(WORDS_BIGENDIAN) || defined(__BIG_ENDIAN__) -#define AFMT_S16_NE AFMT_S16_BE -#else -#define AFMT_S16_NE AFMT_S16_LE -#endif -#define DEFAULT_AUDIO_DEVICE "/dev/audio" -#define DEFAULT_DSP_DEVICE "/dev/dsp" +#ifndef SUN_MAX_CHANNELS +# ifdef __NetBSD__ +# define SUN_MAX_CHANNELS (12) +# else +# define SUN_MAX_CHANNELS (2) +# endif +#endif -#define BUF_SIZE_MS 10 +#ifndef SUN_MIN_RATE +#define SUN_MIN_RATE (1000) +#endif -#if defined(CUBEB_SUNAUDIO_DEBUG) -#define DPR(...) fprintf(stderr, __VA_ARGS__); -#else -#define DPR(...) do {} while(0) +#ifndef SUN_MAX_RATE +#define SUN_MAX_RATE (192000) #endif -static struct cubeb_ops const sunaudio_ops; +static struct cubeb_ops const sun_ops; struct cubeb { struct cubeb_ops const * ops; }; struct cubeb_stream { - cubeb * context; - pthread_t th; /* to run real-time audio i/o */ - pthread_mutex_t mutex; /* protects fd and frm_played */ - int fd; /* link us to sunaudio */ - int active; /* cubec_start() called */ - int conv; /* need float->s16 conversion */ - int using_oss; - unsigned char *buf; /* data is prepared here */ - unsigned int rate; - unsigned int n_channles; - unsigned int bytes_per_ch; - unsigned int n_frm; - unsigned int buffer_size; - int64_t frm_played; - cubeb_data_callback data_cb; /* cb to preapare data */ - cubeb_state_callback state_cb; /* cb to notify about state changes */ - void *arg; /* user arg to {data,state}_cb */ + struct cubeb * context; + void * user_ptr; + pthread_t thread; + pthread_mutex_t mutex; /* protects running, volume, frames_written */ + int floating; + int running; + int play_fd; + int record_fd; + float volume; + struct audio_info p_info; /* info for the play fd */ + struct audio_info r_info; /* info for the record fd */ + cubeb_data_callback data_cb; + cubeb_state_callback state_cb; + int16_t * play_buf; + int16_t * record_buf; + float * f_play_buf; + float * f_record_buf; + char input_name[32]; + char output_name[32]; + uint64_t frames_written; + uint64_t blocks_written; }; +int +sun_init(cubeb ** context, char const * context_name) +{ + cubeb * c; + + (void)context_name; + if ((c = calloc(1, sizeof(cubeb))) == NULL) { + return CUBEB_ERROR; + } + c->ops = &sun_ops; + *context = c; + return CUBEB_OK; +} + static void -float_to_s16(void *ptr, long nsamp) +sun_destroy(cubeb * context) { - int16_t *dst = ptr; - float *src = ptr; + free(context); +} - while (nsamp-- > 0) - *(dst++) = *(src++) * 32767; +static char const * +sun_get_backend_id(cubeb * context) +{ + return "sun"; } -static void * -sunaudio_mainloop(void *arg) +static int +sun_get_preferred_sample_rate(cubeb * context, uint32_t * rate) { - struct cubeb_stream *s = arg; - int state; + (void)context; - DPR("sunaudio_mainloop()\n"); + *rate = SUN_PREFER_RATE; + return CUBEB_OK; +} - s->state_cb(s, s->arg, CUBEB_STATE_STARTED); +static int +sun_get_max_channel_count(cubeb * context, uint32_t * max_channels) +{ + (void)context; - pthread_mutex_lock(&s->mutex); - DPR("sunaudio_mainloop(), started\n"); + *max_channels = SUN_MAX_CHANNELS; + return CUBEB_OK; +} - for (;;) { - if (!s->active) { - DPR("sunaudio_mainloop() stopped\n"); - state = CUBEB_STATE_STOPPED; - break; - } +static int +sun_get_min_latency(cubeb * context, cubeb_stream_params params, + uint32_t * latency_frames) +{ + (void)context; - if (!s->using_oss) { - audio_info_t info; - ioctl(s->fd, AUDIO_GETINFO, &info); - if (s->frm_played > info.play.samples + 3 * s->n_frm) { - pthread_mutex_unlock(&s->mutex); - struct timespec ts = {0, 10000}; // 10 ms - nanosleep(&ts, NULL); - pthread_mutex_lock(&s->mutex); - continue; - } - } + *latency_frames = SUN_LATENCY_MS * params.rate / 1000; + return CUBEB_OK; +} - pthread_mutex_unlock(&s->mutex); - unsigned int got = s->data_cb(s, s->arg, NULL, s->buf, s->n_frm); - DPR("sunaudio_mainloop() ask %d got %d\n", s->n_frm, got); - pthread_mutex_lock(&s->mutex); +static int +sun_get_hwinfo(const char * device, struct audio_info * format, + int * props, struct audio_device * dev) +{ + int fd = -1; - if (got < 0) { - DPR("sunaudio_mainloop() cb err\n"); - state = CUBEB_STATE_ERROR; - break; - } + if ((fd = open(device, O_RDONLY)) == -1) { + goto error; + } +#ifdef AUDIO_GETFORMAT + if (ioctl(fd, AUDIO_GETFORMAT, format) != 0) { + goto error; + } +#endif +#ifdef AUDIO_GETPROPS + if (ioctl(fd, AUDIO_GETPROPS, props) != 0) { + goto error; + } +#endif + if (ioctl(fd, AUDIO_GETDEV, dev) != 0) { + goto error; + } + close(fd); + return CUBEB_OK; +error: + if (fd != -1) { + close(fd); + } + return CUBEB_ERROR; +} - if (s->conv) { - float_to_s16(s->buf, got * s->n_channles); - } +/* + * XXX: PR kern/54264 + */ +static int +sun_prinfo_verify_sanity(struct audio_prinfo * prinfo) +{ + return prinfo->precision >= 8 && prinfo->precision <= 32 && + prinfo->channels >= 1 && prinfo->channels < SUN_MAX_CHANNELS && + prinfo->sample_rate < SUN_MAX_RATE && prinfo->sample_rate > SUN_MIN_RATE; +} - unsigned int avail = got * 2 * s->n_channles; // coverted to s16 - unsigned int pos = 0; +static int +sun_enumerate_devices(cubeb * context, cubeb_device_type type, + cubeb_device_collection * collection) +{ + unsigned i; + cubeb_device_info device = {0}; + char dev[16] = SUN_DEFAULT_DEVICE; + char dev_friendly[64]; + struct audio_info hwfmt; + struct audio_device hwname; + struct audio_prinfo *prinfo = NULL; + int hwprops; + + collection->device = calloc(SUN_DEVICE_COUNT, sizeof(cubeb_device_info)); + if (collection->device == NULL) { + return CUBEB_ERROR; + } + collection->count = 0; - while (avail > 0 && s->active) { - int written = write(s->fd, s->buf + pos, avail); - if (written == -1) { - if (errno != EINTR && errno != EWOULDBLOCK) { - DPR("sunaudio_mainloop() write err\n"); - state = CUBEB_STATE_ERROR; - break; - } - pthread_mutex_unlock(&s->mutex); - struct timespec ts = {0, 10000}; // 10 ms - nanosleep(&ts, NULL); - pthread_mutex_lock(&s->mutex); - } else { - pos += written; - DPR("sunaudio_mainloop() write %d pos %d\n", written, pos); - s->frm_played += written / 2 / s->n_channles; - avail -= written; - } + for (i = 0; i < SUN_DEVICE_COUNT; ++i) { + if (i > 0) { + (void)snprintf(dev, sizeof(dev), "/dev/audio%u", i - 1); } - - if ((got < s->n_frm)) { - DPR("sunaudio_mainloop() drained\n"); - state = CUBEB_STATE_DRAINED; + if (sun_get_hwinfo(dev, &hwfmt, &hwprops, &hwname) != CUBEB_OK) { + continue; + } +#ifdef AUDIO_GETPROPS + device.type = 0; + if ((hwprops & AUDIO_PROP_CAPTURE) != 0 && + sun_prinfo_verify_sanity(&hwfmt.record)) { + /* the device supports recording, probably */ + device.type |= CUBEB_DEVICE_TYPE_INPUT; + } + if ((hwprops & AUDIO_PROP_PLAYBACK) != 0 && + sun_prinfo_verify_sanity(&hwfmt.play)) { + /* the device supports playback, probably */ + device.type |= CUBEB_DEVICE_TYPE_OUTPUT; + } + switch (device.type) { + case 0: + /* device doesn't do input or output, aliens probably involved */ + continue; + case CUBEB_DEVICE_TYPE_INPUT: + if ((type & CUBEB_DEVICE_TYPE_INPUT) == 0) { + /* this device is input only, not scanning for those, skip it */ + continue; + } break; + case CUBEB_DEVICE_TYPE_OUTPUT: + if ((type & CUBEB_DEVICE_TYPE_OUTPUT) == 0) { + /* this device is output only, not scanning for those, skip it */ + continue; + } + break; + } + if ((type & CUBEB_DEVICE_TYPE_INPUT) != 0) { + prinfo = &hwfmt.record; + } + if ((type & CUBEB_DEVICE_TYPE_OUTPUT) != 0) { + prinfo = &hwfmt.play; } +#endif + if (i > 0) { + (void)snprintf(dev_friendly, sizeof(dev_friendly), "%s %s %s (%d)", + hwname.name, hwname.version, hwname.config, i - 1); + } else { + (void)snprintf(dev_friendly, sizeof(dev_friendly), "%s %s %s (default)", + hwname.name, hwname.version, hwname.config); + } + device.devid = (void *)(uintptr_t)i; + device.device_id = strdup(dev); + device.friendly_name = strdup(dev_friendly); + device.group_id = strdup(dev); + device.vendor_name = strdup(hwname.name); + device.type = type; + device.state = CUBEB_DEVICE_STATE_ENABLED; + device.preferred = (i == 0) ? CUBEB_DEVICE_PREF_ALL : CUBEB_DEVICE_PREF_NONE; +#ifdef AUDIO_GETFORMAT + device.max_channels = prinfo->channels; + device.default_rate = prinfo->sample_rate; +#else + device.max_channels = 2; + device.default_rate = SUN_PREFER_RATE; +#endif + device.default_format = CUBEB_DEVICE_FMT_S16NE; + device.format = CUBEB_DEVICE_FMT_S16NE; + device.min_rate = SUN_MIN_RATE; + device.max_rate = SUN_MAX_RATE; + device.latency_lo = SUN_LATENCY_MS * SUN_MIN_RATE / 1000; + device.latency_hi = SUN_LATENCY_MS * SUN_MAX_RATE / 1000; + collection->device[collection->count++] = device; } + return CUBEB_OK; +} - pthread_mutex_unlock(&s->mutex); - s->state_cb(s, s->arg, state); +static int +sun_device_collection_destroy(cubeb * context, + cubeb_device_collection * collection) +{ + unsigned i; - return NULL; + for (i = 0; i < collection->count; ++i) { + free((char *)collection->device[i].device_id); + free((char *)collection->device[i].friendly_name); + free((char *)collection->device[i].group_id); + free((char *)collection->device[i].vendor_name); + } + free(collection->device); + return CUBEB_OK; } -/*static*/ int -sunaudio_init(cubeb **context, char const *context_name) +static int +sun_copy_params(int fd, cubeb_stream * stream, cubeb_stream_params * params, + struct audio_info * info, struct audio_prinfo * prinfo) { - DPR("sunaudio_init(%s)\n", context_name); - *context = malloc(sizeof(*context)); - (*context)->ops = &sunaudio_ops; - (void)context_name; + prinfo->channels = params->channels; + prinfo->sample_rate = params->rate; + prinfo->precision = 16; +#ifdef AUDIO_ENCODING_SLINEAR_LE + switch (params->format) { + case CUBEB_SAMPLE_S16LE: + prinfo->encoding = AUDIO_ENCODING_SLINEAR_LE; + break; + case CUBEB_SAMPLE_S16BE: + prinfo->encoding = AUDIO_ENCODING_SLINEAR_BE; + break; + case CUBEB_SAMPLE_FLOAT32NE: + stream->floating = 1; + prinfo->encoding = AUDIO_ENCODING_SLINEAR; + break; + default: + LOG("Unsupported format"); + return CUBEB_ERROR_INVALID_FORMAT; + } +#else + switch (params->format) { + case CUBEB_SAMPLE_S16NE: + prinfo->encoding = AUDIO_ENCODING_LINEAR; + break; + case CUBEB_SAMPLE_FLOAT32NE: + stream->floating = 1; + prinfo->encoding = AUDIO_ENCODING_LINEAR; + break; + default: + LOG("Unsupported format"); + return CUBEB_ERROR_INVALID_FORMAT; + } +#endif + if (ioctl(fd, AUDIO_SETINFO, info) == -1) { + return CUBEB_ERROR; + } + if (ioctl(fd, AUDIO_GETINFO, info) == -1) { + return CUBEB_ERROR; + } return CUBEB_OK; } -static char const * -sunaudio_get_backend_id(cubeb *context) +static int +sun_stream_stop(cubeb_stream * s) { - return "sunaudio"; + pthread_mutex_lock(&s->mutex); + if (s->running) { + s->running = 0; + pthread_mutex_unlock(&s->mutex); + pthread_join(s->thread, NULL); + } else { + pthread_mutex_unlock(&s->mutex); + } + return CUBEB_OK; } static void -sunaudio_destroy(cubeb *context) +sun_stream_destroy(cubeb_stream * s) { - DPR("sunaudio_destroy()\n"); - free(context); + pthread_mutex_destroy(&s->mutex); + sun_stream_stop(s); + if (s->play_fd != -1) { + close(s->play_fd); + } + if (s->record_fd != -1) { + close(s->record_fd); + } + free(s->f_play_buf); + free(s->f_record_buf); + free(s->play_buf); + free(s->record_buf); + free(s); } -static int -sunaudio_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, - cubeb_data_callback data_callback, - cubeb_state_callback state_callback, - void *user_ptr) +static void +sun_float_to_linear(float * in, int16_t * out, + unsigned channels, long frames, float vol) { - struct cubeb_stream *s; - DPR("sunaudio_stream_init(%s)\n", stream_name); - size_t size; - - s = malloc(sizeof(struct cubeb_stream)); - if (s == NULL) - return CUBEB_ERROR; - s->context = context; - - // If UTAUDIODEV is set, use it with Sun Audio interface - char * sa_device_name = getenv("UTAUDIODEV"); - char * dsp_device_name = NULL; - if (!sa_device_name) { - dsp_device_name = getenv("AUDIODSP"); - if (!dsp_device_name) { - dsp_device_name = DEFAULT_DSP_DEVICE; - } - sa_device_name = getenv("AUDIODEV"); - if (!sa_device_name) { - sa_device_name = DEFAULT_AUDIO_DEVICE; + unsigned i, sample_count = frames * channels; + float multiplier = vol * 0x8000; + + for (i = 0; i < sample_count; ++i) { + int32_t sample = lrintf(in[i] * multiplier); + if (sample < -0x8000) { + out[i] = -0x8000; + } else if (sample > 0x7fff) { + out[i] = 0x7fff; + } else { + out[i] = sample; } } +} - s->using_oss = 0; - // Try to use OSS if available - if (dsp_device_name) { - s->fd = open(dsp_device_name, O_WRONLY | O_NONBLOCK); - if (s->fd >= 0) { - s->using_oss = 1; - } - } +static void +sun_linear_to_float(int16_t * in, float * out, + unsigned channels, long frames) +{ + unsigned i, sample_count = frames * channels; - // Try Sun Audio - if (!s->using_oss) { - s->fd = open(sa_device_name, O_WRONLY | O_NONBLOCK); + for (i = 0; i < sample_count; ++i) { + out[i] = (1.0 / 0x8000) * in[i]; } +} - if (s->fd < 0) { - free(s); - DPR("sunaudio_stream_init(), open() failed\n"); - return CUBEB_ERROR; +static void +sun_linear_set_vol(int16_t * buf, unsigned channels, long frames, float vol) +{ + unsigned i, sample_count = frames * channels; + int32_t multiplier = vol * 0x8000; + + for (i = 0; i < sample_count; ++i) { + buf[i] = (buf[i] * multiplier) >> 15; } +} - if (s->using_oss) { - if (ioctl(s->fd, SNDCTL_DSP_SPEED, &output_stream_params->rate) < 0) { - DPR("ioctl SNDCTL_DSP_SPEED failed.\n"); - close(s->fd); - free(s); - return CUBEB_ERROR_INVALID_FORMAT; +static void * +sun_io_routine(void * arg) +{ + cubeb_stream *s = arg; + cubeb_state state = CUBEB_STATE_STARTED; + size_t to_read = 0; + long to_write = 0; + size_t write_ofs = 0; + size_t read_ofs = 0; + int drain = 0; + + s->state_cb(s, s->user_ptr, CUBEB_STATE_STARTED); + while (state != CUBEB_STATE_ERROR) { + pthread_mutex_lock(&s->mutex); + if (!s->running) { + pthread_mutex_unlock(&s->mutex); + state = CUBEB_STATE_STOPPED; + break; } - - if (ioctl(s->fd, SNDCTL_DSP_CHANNELS, &output_stream_params->channels) < 0) { - DPR("ioctl SNDCTL_DSP_CHANNELS failed.\n"); - close(s->fd); - free(s); - return CUBEB_ERROR_INVALID_FORMAT; + pthread_mutex_unlock(&s->mutex); + if (s->floating) { + if (s->record_fd != -1) { + sun_linear_to_float(s->record_buf, s->f_record_buf, + s->r_info.record.channels, SUN_BUFFER_FRAMES); + } + to_write = s->data_cb(s, s->user_ptr, + s->f_record_buf, s->f_play_buf, SUN_BUFFER_FRAMES); + if (to_write == CUBEB_ERROR) { + state = CUBEB_STATE_ERROR; + break; + } + if (s->play_fd != -1) { + pthread_mutex_lock(&s->mutex); + sun_float_to_linear(s->f_play_buf, s->play_buf, + s->p_info.play.channels, to_write, s->volume); + pthread_mutex_unlock(&s->mutex); + } + } else { + to_write = s->data_cb(s, s->user_ptr, + s->record_buf, s->play_buf, SUN_BUFFER_FRAMES); + if (to_write == CUBEB_ERROR) { + state = CUBEB_STATE_ERROR; + break; + } + if (s->play_fd != -1) { + pthread_mutex_lock(&s->mutex); + sun_linear_set_vol(s->play_buf, s->p_info.play.channels, to_write, s->volume); + pthread_mutex_unlock(&s->mutex); + } } - - int format = AFMT_S16_NE; - if (ioctl(s->fd, SNDCTL_DSP_SETFMT, &format) < 0) { - DPR("ioctl SNDCTL_DSP_SETFMT failed.\n"); - close(s->fd); - free(s); - return CUBEB_ERROR_INVALID_FORMAT; + if (to_write < SUN_BUFFER_FRAMES) { + drain = 1; } - } else { - audio_info_t audio_info; - AUDIO_INITINFO(&audio_info) - audio_info.play.sample_rate = output_stream_params->rate; - audio_info.play.channels = output_stream_params->channels; - audio_info.play.encoding = AUDIO_ENCODING_LINEAR; - audio_info.play.precision = 16; - if (ioctl(s->fd, AUDIO_SETINFO, &audio_info) == -1) { - DPR("ioctl AUDIO_SETINFO failed.\n"); - close(s->fd); - free(s); - return CUBEB_ERROR_INVALID_FORMAT; + to_write = s->play_fd != -1 ? to_write : 0; + to_read = s->record_fd != -1 ? SUN_BUFFER_FRAMES : 0; + write_ofs = 0; + read_ofs = 0; + while (to_write > 0 || to_read > 0) { + size_t bytes; + ssize_t n, frames; + + if (to_write > 0) { + bytes = FRAMES_TO_BYTES(to_write, s->p_info.play.channels); + if ((n = write(s->play_fd, s->play_buf + write_ofs, bytes)) < 0) { + state = CUBEB_STATE_ERROR; + break; + } + frames = BYTES_TO_FRAMES(n, s->p_info.play.channels); + pthread_mutex_lock(&s->mutex); + s->frames_written += frames; + pthread_mutex_unlock(&s->mutex); + to_write -= frames; + write_ofs += frames; + } + if (to_read > 0) { + bytes = FRAMES_TO_BYTES(to_read, s->r_info.record.channels); + if ((n = read(s->record_fd, s->record_buf + read_ofs, bytes)) < 0) { + state = CUBEB_STATE_ERROR; + break; + } + frames = BYTES_TO_FRAMES(n, s->r_info.record.channels); + to_read -= frames; + read_ofs += frames; + } } - } - - s->conv = 0; - switch (output_stream_params->format) { - case CUBEB_SAMPLE_S16NE: - s->bytes_per_ch = 2; - break; - case CUBEB_SAMPLE_FLOAT32NE: - s->bytes_per_ch = 4; - s->conv = 1; + if (drain && state != CUBEB_STATE_ERROR) { + state = CUBEB_STATE_DRAINED; break; - default: - DPR("sunaudio_stream_init() unsupported format\n"); - close(s->fd); - free(s); - return CUBEB_ERROR_INVALID_FORMAT; + } } + s->state_cb(s, s->user_ptr, state); + return NULL; +} - s->active = 0; - s->rate = output_stream_params->rate; - s->n_channles = output_stream_params->channels; - s->data_cb = data_callback; +static int +sun_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 latency_frames, + cubeb_data_callback data_callback, + cubeb_state_callback state_callback, + void * user_ptr) +{ + int ret = CUBEB_OK; + cubeb_stream *s = NULL; + + (void)stream_name; + (void)latency_frames; + if ((s = calloc(1, sizeof(cubeb_stream))) == NULL) { + ret = CUBEB_ERROR; + goto error; + } + s->record_fd = -1; + s->play_fd = -1; + if (input_device != 0) { + snprintf(s->input_name, sizeof(s->input_name), + "/dev/audio%zu", (uintptr_t)input_device - 1); + } else { + snprintf(s->input_name, sizeof(s->input_name), "%s", SUN_DEFAULT_DEVICE); + } + if (output_device != 0) { + snprintf(s->output_name, sizeof(s->output_name), + "/dev/audio%zu", (uintptr_t)output_device - 1); + } else { + snprintf(s->output_name, sizeof(s->output_name), "%s", SUN_DEFAULT_DEVICE); + } + if (input_stream_params != NULL) { + if (input_stream_params->prefs & CUBEB_STREAM_PREF_LOOPBACK) { + LOG("Loopback not supported"); + ret = CUBEB_ERROR_NOT_SUPPORTED; + goto error; + } + if (s->record_fd == -1) { + if ((s->record_fd = open(s->input_name, O_RDONLY)) == -1) { + LOG("Audio device cannot be opened as read-only"); + ret = CUBEB_ERROR_DEVICE_UNAVAILABLE; + goto error; + } + } + AUDIO_INITINFO(&s->r_info); +#ifdef AUMODE_RECORD + s->r_info.mode = AUMODE_RECORD; +#endif + if ((ret = sun_copy_params(s->record_fd, s, input_stream_params, + &s->r_info, &s->r_info.record)) != CUBEB_OK) { + LOG("Setting record params failed"); + goto error; + } + } + if (output_stream_params != NULL) { + if (output_stream_params->prefs & CUBEB_STREAM_PREF_LOOPBACK) { + LOG("Loopback not supported"); + ret = CUBEB_ERROR_NOT_SUPPORTED; + goto error; + } + if (s->play_fd == -1) { + if ((s->play_fd = open(s->output_name, O_WRONLY)) == -1) { + LOG("Audio device cannot be opened as write-only"); + ret = CUBEB_ERROR_DEVICE_UNAVAILABLE; + goto error; + } + } + AUDIO_INITINFO(&s->p_info); +#ifdef AUMODE_PLAY + s->p_info.mode = AUMODE_PLAY; +#endif + if ((ret = sun_copy_params(s->play_fd, s, output_stream_params, + &s->p_info, &s->p_info.play)) != CUBEB_OK) { + LOG("Setting play params failed"); + goto error; + } + } + s->context = context; + s->volume = 1.0; s->state_cb = state_callback; - s->arg = user_ptr; + s->data_cb = data_callback; + s->user_ptr = user_ptr; if (pthread_mutex_init(&s->mutex, NULL) != 0) { - free(s); - return CUBEB_ERROR; + LOG("Failed to create mutex"); + goto error; } - s->frm_played = 0; - s->n_frm = s->rate * BUF_SIZE_MS / 1000; - s->buffer_size = s->bytes_per_ch * s->n_channles * s->n_frm; - s->buf = malloc(s->buffer_size); - if (s->buf == NULL) { - close(s->fd); - free(s); - return CUBEB_ERROR; + if (s->play_fd != -1 && (s->play_buf = calloc(SUN_BUFFER_FRAMES, + s->p_info.play.channels * sizeof(int16_t))) == NULL) { + ret = CUBEB_ERROR; + goto error; + } + if (s->record_fd != -1 && (s->record_buf = calloc(SUN_BUFFER_FRAMES, + s->r_info.record.channels * sizeof(int16_t))) == NULL) { + ret = CUBEB_ERROR; + goto error; + } + if (s->floating) { + if (s->play_fd != -1 && (s->f_play_buf = calloc(SUN_BUFFER_FRAMES, + s->p_info.play.channels * sizeof(float))) == NULL) { + ret = CUBEB_ERROR; + goto error; + } + if (s->record_fd != -1 && (s->f_record_buf = calloc(SUN_BUFFER_FRAMES, + s->r_info.record.channels * sizeof(float))) == NULL) { + ret = CUBEB_ERROR; + goto error; + } } - *stream = s; - DPR("sunaudio_stream_init() end, ok\n"); return CUBEB_OK; -} - -static void -sunaudio_stream_destroy(cubeb_stream *s) -{ - DPR("sunaudio_stream_destroy()\n"); - if (s->fd > 0) { - // Flush buffer - if (s->using_oss) { - ioctl(s->fd, SNDCTL_DSP_HALT_OUTPUT); - } else { - ioctl(s->fd, I_FLUSH); - } - close(s->fd); +error: + if (s != NULL) { + sun_stream_destroy(s); } - free(s->buf); - free(s); + return ret; } static int -sunaudio_stream_start(cubeb_stream *s) +sun_stream_start(cubeb_stream * s) { - int err; - - DPR("sunaudio_stream_start()\n"); - s->active = 1; - err = pthread_create(&s->th, NULL, sunaudio_mainloop, s); - if (err) { - s->active = 0; + s->running = 1; + if (pthread_create(&s->thread, NULL, sun_io_routine, s) != 0) { + LOG("Couldn't create thread"); return CUBEB_ERROR; } return CUBEB_OK; } static int -sunaudio_stream_stop(cubeb_stream *s) +sun_stream_get_position(cubeb_stream * s, uint64_t * position) { - void *dummy; +#ifdef AUDIO_GETOOFFS + struct audio_offset offset; - DPR("sunaudio_stream_stop()\n"); - if (s->active) { - s->active = 0; - pthread_join(s->th, &dummy); + if (ioctl(s->play_fd, AUDIO_GETOOFFS, &offset) == -1) { + return CUBEB_ERROR; } + s->blocks_written += offset.deltablks; + *position = BYTES_TO_FRAMES(s->blocks_written * s->p_info.blocksize, + s->p_info.play.channels); return CUBEB_OK; -} - -static int -sunaudio_stream_get_position(cubeb_stream *s, uint64_t *p) -{ - int rv = CUBEB_OK; +#else pthread_mutex_lock(&s->mutex); - if (s->active && s->fd > 0) { - if (s->using_oss) { - int delay; - ioctl(s->fd, SNDCTL_DSP_GETODELAY, &delay); - int64_t t = s->frm_played - delay / s->n_channles / 2; - if (t < 0) { - *p = 0; - } else { - *p = t; - } - } else { - audio_info_t info; - ioctl(s->fd, AUDIO_GETINFO, &info); - *p = info.play.samples; - } - DPR("sunaudio_stream_get_position() %lld\n", *p); - } else { - rv = CUBEB_ERROR; - } + *position = s->frames_written; pthread_mutex_unlock(&s->mutex); - return rv; + return CUBEB_OK; +#endif } static int -sunaudio_get_max_channel_count(cubeb * ctx, uint32_t * max_channels) +sun_stream_get_latency(cubeb_stream * stream, uint32_t * latency) { - if (!ctx || !max_channels) - return CUBEB_ERROR; +#ifdef AUDIO_GETBUFINFO + struct audio_info info; - *max_channels = 2; + if (ioctl(stream->play_fd, AUDIO_GETBUFINFO, &info) == -1) { + return CUBEB_ERROR; + } + *latency = BYTES_TO_FRAMES(info.play.seek + info.blocksize, + info.play.channels); return CUBEB_OK; +#else + cubeb_stream_params params; + + params.rate = stream->p_info.play.sample_rate; + + return sun_get_min_latency(NULL, params, latency); +#endif } static int -sunaudio_get_preferred_sample_rate(cubeb * ctx, uint32_t * rate) +sun_stream_set_volume(cubeb_stream * stream, float volume) { - if (!ctx || !rate) - return CUBEB_ERROR; - - // XXX Not yet implemented. - *rate = 44100; - + pthread_mutex_lock(&stream->mutex); + stream->volume = volume; + pthread_mutex_unlock(&stream->mutex); return CUBEB_OK; } static int -sunaudio_get_min_latency(cubeb * ctx, cubeb_stream_params params, uint32_t * latency_ms) +sun_get_current_device(cubeb_stream * stream, cubeb_device ** const device) { - if (!ctx || !latency_ms) + *device = calloc(1, sizeof(cubeb_device)); + if (*device == NULL) { return CUBEB_ERROR; - - // XXX Not yet implemented. - *latency_ms = 20; - + } + (*device)->input_name = stream->record_fd != -1 ? + strdup(stream->input_name) : NULL; + (*device)->output_name = stream->play_fd != -1 ? + strdup(stream->output_name) : NULL; return CUBEB_OK; } static int -sunaudio_stream_get_latency(cubeb_stream * s, uint32_t * latency) +sun_stream_device_destroy(cubeb_stream * stream, cubeb_device * device) { - if (!s || !latency) - return CUBEB_ERROR; - - int rv = CUBEB_OK; - pthread_mutex_lock(&s->mutex); - if (s->active && s->fd > 0) { - if (s->using_oss) { - int delay; - ioctl(s->fd, SNDCTL_DSP_GETODELAY, &delay); - *latency = delay / s->n_channles / 2 / s->rate; - } else { - audio_info_t info; - ioctl(s->fd, AUDIO_GETINFO, &info); - *latency = (s->frm_played - info.play.samples) / s->rate; - } - DPR("sunaudio_stream_get_position() %lld\n", *p); - } else { - rv = CUBEB_ERROR; - } - pthread_mutex_unlock(&s->mutex); - return rv; + (void)stream; + free(device->input_name); + free(device->output_name); + free(device); + return CUBEB_OK; } -static struct cubeb_ops const sunaudio_ops = { - .init = sunaudio_init, - .get_backend_id = sunaudio_get_backend_id, - .destroy = sunaudio_destroy, - .get_preferred_sample_rate = sunaudio_get_preferred_sample_rate, - .stream_init = sunaudio_stream_init, - .stream_destroy = sunaudio_stream_destroy, - .stream_start = sunaudio_stream_start, - .stream_stop = sunaudio_stream_stop, - .stream_get_position = sunaudio_stream_get_position, - .get_max_channel_count = sunaudio_get_max_channel_count, - .get_min_latency = sunaudio_get_min_latency, - .stream_get_latency = sunaudio_stream_get_latency +static struct cubeb_ops const sun_ops = { + .init = sun_init, + .get_backend_id = sun_get_backend_id, + .get_max_channel_count = sun_get_max_channel_count, + .get_min_latency = sun_get_min_latency, + .get_preferred_sample_rate = sun_get_preferred_sample_rate, + .enumerate_devices = sun_enumerate_devices, + .device_collection_destroy = sun_device_collection_destroy, + .destroy = sun_destroy, + .stream_init = sun_stream_init, + .stream_destroy = sun_stream_destroy, + .stream_start = sun_stream_start, + .stream_stop = sun_stream_stop, + .stream_reset_default_device = NULL, + .stream_get_position = sun_stream_get_position, + .stream_get_latency = sun_stream_get_latency, + .stream_set_volume = sun_stream_set_volume, + .stream_get_current_device = sun_get_current_device, + .stream_device_destroy = sun_stream_device_destroy, + .stream_register_device_changed_callback = NULL, + .register_device_collection_changed = NULL }; diff --git a/media/libcubeb/src/cubeb_utils.cpp b/media/libcubeb/src/cubeb_utils.cpp new file mode 100644 index 0000000000..85572a9fea --- /dev/null +++ b/media/libcubeb/src/cubeb_utils.cpp @@ -0,0 +1,23 @@ +/* + * Copyright © 2018 Mozilla Foundation + * + * This program is made available under an ISC-style license. See the + * accompanying file LICENSE for details. + */ + +#include "cubeb_utils.h" + +size_t cubeb_sample_size(cubeb_sample_format format) +{ + switch (format) { + case CUBEB_SAMPLE_S16LE: + case CUBEB_SAMPLE_S16BE: + return sizeof(int16_t); + case CUBEB_SAMPLE_FLOAT32LE: + case CUBEB_SAMPLE_FLOAT32BE: + return sizeof(float); + default: + // should never happen as all cases are handled above. + assert(false); + } +} diff --git a/media/libcubeb/src/cubeb_utils.h b/media/libcubeb/src/cubeb_utils.h index d8e9928fe2..df67511559 100644 --- a/media/libcubeb/src/cubeb_utils.h +++ b/media/libcubeb/src/cubeb_utils.h @@ -8,11 +8,16 @@ #if !defined(CUBEB_UTILS) #define CUBEB_UTILS +#include "cubeb/cubeb.h" + +#ifdef __cplusplus + #include <stdint.h> #include <string.h> #include <assert.h> +#include <mutex> #include <type_traits> -#if defined(WIN32) +#if defined(_WIN32) #include "cubeb_utils_win.h" #else #include "cubeb_utils_unix.h" @@ -23,6 +28,7 @@ template<typename T> void PodCopy(T * destination, const T * source, size_t count) { static_assert(std::is_trivial<T>::value, "Requires trivial type"); + assert(destination && source); memcpy(destination, source, count * sizeof(T)); } @@ -31,6 +37,7 @@ template<typename T> void PodMove(T * destination, const T * source, size_t count) { static_assert(std::is_trivial<T>::value, "Requires trivial type"); + assert(destination && source); memmove(destination, source, count * sizeof(T)); } @@ -39,9 +46,67 @@ template<typename T> void PodZero(T * destination, size_t count) { static_assert(std::is_trivial<T>::value, "Requires trivial type"); + assert(destination); memset(destination, 0, count * sizeof(T)); } +namespace { +template<typename T, typename Trait> +void Copy(T * destination, const T * source, size_t count, Trait) +{ + for (size_t i = 0; i < count; i++) { + destination[i] = source[i]; + } +} + +template<typename T> +void Copy(T * destination, const T * source, size_t count, std::true_type) +{ + PodCopy(destination, source, count); +} +} + +/** + * This allows copying a number of elements from a `source` pointer to a + * `destination` pointer, using `memcpy` if it is safe to do so, or a loop that + * calls the constructors and destructors otherwise. + */ +template<typename T> +void Copy(T * destination, const T * source, size_t count) +{ + assert(destination && source); + Copy(destination, source, count, typename std::is_trivial<T>::type()); +} + +namespace { +template<typename T, typename Trait> +void ConstructDefault(T * destination, size_t count, Trait) +{ + for (size_t i = 0; i < count; i++) { + destination[i] = T(); + } +} + +template<typename T> +void ConstructDefault(T * destination, + size_t count, std::true_type) +{ + PodZero(destination, count); +} +} + +/** + * This allows zeroing (using memset) or default-constructing a number of + * elements calling the constructors and destructors if necessary. + */ +template<typename T> +void ConstructDefault(T * destination, size_t count) +{ + assert(destination); + ConstructDefault(destination, count, + typename std::is_arithmetic<T>::type()); +} + template<typename T> class auto_array { @@ -63,6 +128,11 @@ public: return data_; } + T * end() const + { + return data_ + length_; + } + const T& at(size_t index) const { assert(index < length_ && "out of range"); @@ -198,18 +268,76 @@ private: size_t length_; }; -struct auto_lock { - explicit auto_lock(owned_critical_section & lock) - : lock(lock) - { - lock.enter(); +struct auto_array_wrapper { + virtual void push(void * elements, size_t length) = 0; + virtual size_t length() = 0; + virtual void push_silence(size_t length) = 0; + virtual bool pop(size_t length) = 0; + virtual void * data() = 0; + virtual void * end() = 0; + virtual void clear() = 0; + virtual bool reserve(size_t capacity) = 0; + virtual void set_length(size_t length) = 0; + virtual ~auto_array_wrapper() {} +}; + +template <typename T> +struct auto_array_wrapper_impl : public auto_array_wrapper { + auto_array_wrapper_impl() {} + + explicit auto_array_wrapper_impl(uint32_t size) + : ar(size) + {} + + void push(void * elements, size_t length) override { + ar.push(static_cast<T *>(elements), length); } - ~auto_lock() - { - lock.leave(); + + size_t length() override { + return ar.length(); + } + + void push_silence(size_t length) override { + ar.push_silence(length); + } + + bool pop(size_t length) override { + return ar.pop(nullptr, length); + } + + void * data() override { + return ar.data(); + } + + void * end() override { + return ar.end(); + } + + void clear() override { + ar.clear(); + } + + bool reserve(size_t capacity) override { + return ar.reserve(capacity); } + + void set_length(size_t length) override { + ar.set_length(length); + } + + ~auto_array_wrapper_impl() { + ar.clear(); + } + private: - owned_critical_section & lock; + auto_array<T> ar; }; +extern "C" { + size_t cubeb_sample_size(cubeb_sample_format format); +} + +using auto_lock = std::lock_guard<owned_critical_section>; +#endif // __cplusplus + #endif /* CUBEB_UTILS */ diff --git a/media/libcubeb/src/cubeb_utils_unix.h b/media/libcubeb/src/cubeb_utils_unix.h index 80219d58b1..4876d015fe 100644 --- a/media/libcubeb/src/cubeb_utils_unix.h +++ b/media/libcubeb/src/cubeb_utils_unix.h @@ -48,7 +48,7 @@ public: #endif } - void enter() + void lock() { #ifndef NDEBUG int r = @@ -59,7 +59,7 @@ public: #endif } - void leave() + void unlock() { #ifndef NDEBUG int r = diff --git a/media/libcubeb/src/cubeb_utils_win.h b/media/libcubeb/src/cubeb_utils_win.h index 2b094cd93d..0112ad6d3c 100644 --- a/media/libcubeb/src/cubeb_utils_win.h +++ b/media/libcubeb/src/cubeb_utils_win.h @@ -29,7 +29,7 @@ public: DeleteCriticalSection(&critical_section); } - void enter() + void lock() { EnterCriticalSection(&critical_section); #ifndef NDEBUG @@ -38,7 +38,7 @@ public: #endif } - void leave() + void unlock() { #ifndef NDEBUG /* GetCurrentThreadId cannot return 0: it is not a the valid thread id */ diff --git a/media/libcubeb/src/cubeb_wasapi.cpp b/media/libcubeb/src/cubeb_wasapi.cpp index e88d6becd8..6acf1c1cc0 100644 --- a/media/libcubeb/src/cubeb_wasapi.cpp +++ b/media/libcubeb/src/cubeb_wasapi.cpp @@ -4,6 +4,7 @@ * This program is made available under an ISC-style license. See the * accompanying file LICENSE for details. */ +#define _WIN32_WINNT 0x0600 #define NOMINMAX #include <initguid.h> @@ -23,20 +24,61 @@ #include <memory> #include <limits> #include <atomic> +#include <vector> #include "cubeb/cubeb.h" #include "cubeb-internal.h" +#include "cubeb_mixer.h" #include "cubeb_resampler.h" +#include "cubeb_strings.h" #include "cubeb_utils.h" -/* devicetopology.h missing in MinGW. */ -#ifndef __devicetopology_h__ -#include "cubeb_devicetopology.h" +// Windows 10 exposes the IAudioClient3 interface to create low-latency streams. +// Copy the interface definition from audioclient.h here to make the code simpler +// and so that we can still access IAudioClient3 via COM if cubeb was compiled +// against an older SDK. +#ifndef __IAudioClient3_INTERFACE_DEFINED__ +#define __IAudioClient3_INTERFACE_DEFINED__ +MIDL_INTERFACE("7ED4EE07-8E67-4CD4-8C1A-2B7A5987AD42") +IAudioClient3 : public IAudioClient +{ +public: + virtual HRESULT STDMETHODCALLTYPE GetSharedModeEnginePeriod( + /* [annotation][in] */ + _In_ const WAVEFORMATEX *pFormat, + /* [annotation][out] */ + _Out_ UINT32 *pDefaultPeriodInFrames, + /* [annotation][out] */ + _Out_ UINT32 *pFundamentalPeriodInFrames, + /* [annotation][out] */ + _Out_ UINT32 *pMinPeriodInFrames, + /* [annotation][out] */ + _Out_ UINT32 *pMaxPeriodInFrames) = 0; + + virtual HRESULT STDMETHODCALLTYPE GetCurrentSharedModeEnginePeriod( + /* [unique][annotation][out] */ + _Out_ WAVEFORMATEX **ppFormat, + /* [annotation][out] */ + _Out_ UINT32 *pCurrentPeriodInFrames) = 0; + + virtual HRESULT STDMETHODCALLTYPE InitializeSharedAudioStream( + /* [annotation][in] */ + _In_ DWORD StreamFlags, + /* [annotation][in] */ + _In_ UINT32 PeriodInFrames, + /* [annotation][in] */ + _In_ const WAVEFORMATEX *pFormat, + /* [annotation][in] */ + _In_opt_ LPCGUID AudioSessionGuid) = 0; +}; +#ifdef __CRT_UUID_DECL +// Required for MinGW +__CRT_UUID_DECL(IAudioClient3, 0x7ED4EE07, 0x8E67, 0x4CD4, 0x8C, 0x1A, 0x2B, 0x7A, 0x59, 0x87, 0xAD, 0x42) #endif - -/* Taken from winbase.h, Not in MinGW. */ -#ifndef STACK_SIZE_PARAM_IS_A_RESERVATION -#define STACK_SIZE_PARAM_IS_A_RESERVATION 0x00010000 // Threads only +#endif +// Copied from audioclient.h in the Windows 10 SDK +#ifndef AUDCLNT_E_ENGINE_PERIODICITY_LOCKED +#define AUDCLNT_E_ENGINE_PERIODICITY_LOCKED AUDCLNT_ERR(0x028) #endif #ifndef PKEY_Device_FriendlyName @@ -47,6 +89,15 @@ DEFINE_PROPERTYKEY(PKEY_Device_InstanceId, 0x78c34fc8, 0x104a, 0x4aca, 0x9e #endif namespace { +struct com_heap_ptr_deleter { + void operator()(void * ptr) const noexcept { + CoTaskMemFree(ptr); + } +}; + +template <typename T> +using com_heap_ptr = std::unique_ptr<T, com_heap_ptr_deleter>; + template<typename T, size_t N> constexpr size_t ARRAY_LENGTH(T(&)[N]) @@ -54,51 +105,83 @@ ARRAY_LENGTH(T(&)[N]) return N; } -void -SafeRelease(HANDLE handle) -{ - if (handle) { - CloseHandle(handle); - } -} +template <typename T> +class no_addref_release : public T { + ULONG STDMETHODCALLTYPE AddRef() = 0; + ULONG STDMETHODCALLTYPE Release() = 0; +}; template <typename T> -void SafeRelease(T * ptr) -{ - if (ptr) { - ptr->Release(); +class com_ptr { +public: + com_ptr() noexcept = default; + + com_ptr(com_ptr const & other) noexcept = delete; + com_ptr & operator=(com_ptr const & other) noexcept = delete; + T ** operator&() const noexcept = delete; + + ~com_ptr() noexcept { + release(); } -} -struct auto_com { - auto_com() { - result = CoInitializeEx(NULL, COINIT_MULTITHREADED); - } - ~auto_com() { - if (result == RPC_E_CHANGED_MODE) { - // This is not an error, COM was not initialized by this function, so it is - // not necessary to uninit it. - LOG("COM was already initialized in STA."); - } else if (result == S_FALSE) { - // This is not an error. We are allowed to call CoInitializeEx more than - // once, as long as it is matches by an CoUninitialize call. - // We do that in the dtor which is guaranteed to be called. - LOG("COM was already initialized in MTA"); - } - if (SUCCEEDED(result)) { - CoUninitialize(); + com_ptr(com_ptr && other) noexcept + : ptr(other.ptr) + { + other.ptr = nullptr; + } + + com_ptr & operator=(com_ptr && other) noexcept { + if (ptr != other.ptr) { + release(); + ptr = other.ptr; + other.ptr = nullptr; } + return *this; + } + + explicit operator bool() const noexcept { + return nullptr != ptr; } - bool ok() { - return result == RPC_E_CHANGED_MODE || SUCCEEDED(result); + + no_addref_release<T> * operator->() const noexcept { + return static_cast<no_addref_release<T> *>(ptr); + } + + T * get() const noexcept { + return ptr; + } + + T ** receive() noexcept { + XASSERT(ptr == nullptr); + return &ptr; + } + + void ** receive_vpp() noexcept { + return reinterpret_cast<void **>(receive()); + } + + com_ptr & operator=(std::nullptr_t) noexcept { + release(); + return *this; } + + void reset(T * p = nullptr) noexcept { + release(); + ptr = p; + } + private: - HRESULT result; -}; + void release() noexcept { + T * temp = ptr; -typedef HANDLE (WINAPI *set_mm_thread_characteristics_function)( - const char * TaskName, LPDWORD TaskIndex); -typedef BOOL (WINAPI *revert_mm_thread_characteristics_function)(HANDLE handle); + if (temp) { + ptr = nullptr; + temp->Release(); + } + } + + T * ptr = nullptr; +}; extern cubeb_ops const wasapi_ops; @@ -106,19 +189,28 @@ int wasapi_stream_stop(cubeb_stream * stm); int wasapi_stream_start(cubeb_stream * stm); void close_wasapi_stream(cubeb_stream * stm); int setup_wasapi_stream(cubeb_stream * stm); -static char * wstr_to_utf8(const wchar_t * str); -static std::unique_ptr<const wchar_t[]> utf8_to_wstr(char* str); +ERole pref_to_role(cubeb_stream_prefs param); +static char const * wstr_to_utf8(wchar_t const * str); +static std::unique_ptr<wchar_t const []> utf8_to_wstr(char const * str); } -struct cubeb -{ - cubeb_ops const * ops; - /* Library dynamically opened to increase the render thread priority, and - the two function pointers we need. */ - HMODULE mmcss_module; - set_mm_thread_characteristics_function set_mm_thread_characteristics; - revert_mm_thread_characteristics_function revert_mm_thread_characteristics; +class wasapi_collection_notification_client; +class monitor_device_notifications; + +struct cubeb { + cubeb_ops const * ops = &wasapi_ops; + cubeb_strings * device_ids; + /* Device enumerator to get notifications when the + device collection change. */ + com_ptr<IMMDeviceEnumerator> device_collection_enumerator; + com_ptr<wasapi_collection_notification_client> collection_notification_client; + /* Collection changed for input (capture) devices. */ + cubeb_device_collection_changed_callback input_collection_changed_callback = nullptr; + void * input_collection_changed_user_ptr = nullptr; + /* Collection changed for output (render) devices. */ + cubeb_device_collection_changed_callback output_collection_changed_callback = nullptr; + void * output_collection_changed_user_ptr = nullptr; }; class wasapi_endpoint_notification_client; @@ -132,27 +224,35 @@ class wasapi_endpoint_notification_client; */ typedef bool (*wasapi_refill_callback)(cubeb_stream * stm); -struct cubeb_stream -{ - cubeb * context; +struct cubeb_stream { + /* Note: Must match cubeb_stream layout in cubeb.c. */ + cubeb * context = nullptr; + void * user_ptr = nullptr; + /**/ + /* Mixer pameters. We need to convert the input stream to this samplerate/channel layout, as WASAPI does not resample nor upmix itself. */ - cubeb_stream_params input_mix_params; - cubeb_stream_params output_mix_params; + cubeb_stream_params input_mix_params = { CUBEB_SAMPLE_FLOAT32NE, 0, 0, CUBEB_LAYOUT_UNDEFINED, CUBEB_STREAM_PREF_NONE }; + cubeb_stream_params output_mix_params = { CUBEB_SAMPLE_FLOAT32NE, 0, 0, CUBEB_LAYOUT_UNDEFINED, CUBEB_STREAM_PREF_NONE }; /* Stream parameters. This is what the client requested, * and what will be presented in the callback. */ - cubeb_stream_params input_stream_params; - cubeb_stream_params output_stream_params; + 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 }; + /* A MMDevice role for this stream: either communication or console here. */ + ERole role; /* The input and output device, or NULL for default. */ - cubeb_devid input_device; - cubeb_devid output_device; + std::unique_ptr<const wchar_t[]> input_device; + std::unique_ptr<const wchar_t[]> output_device; /* The latency initially requested for this stream, in frames. */ - unsigned latency; - cubeb_state_callback state_callback; - cubeb_data_callback data_callback; - wasapi_refill_callback refill_callback; - void * user_ptr; + unsigned latency = 0; + cubeb_state_callback state_callback = nullptr; + cubeb_data_callback data_callback = nullptr; + wasapi_refill_callback refill_callback = nullptr; + /* True when a loopback device is requested with no output device. In this + case a dummy output device is opened to drive the loopback, but should not + be exposed. */ + bool has_dummy_output = false; /* Lifetime considerations: - client, render_client, audio_clock and audio_stream_volume are interface pointer to the IAudioClient. @@ -160,70 +260,324 @@ struct cubeb_stream mix_buffer are the same as the cubeb_stream instance. */ /* Main handle on the WASAPI stream. */ - IAudioClient * output_client; + com_ptr<IAudioClient> output_client; /* Interface pointer to use the event-driven interface. */ - IAudioRenderClient * render_client; + com_ptr<IAudioRenderClient> render_client; /* Interface pointer to use the volume facilities. */ - IAudioStreamVolume * audio_stream_volume; + com_ptr<IAudioStreamVolume> audio_stream_volume; /* Interface pointer to use the stream audio clock. */ - IAudioClock * audio_clock; + com_ptr<IAudioClock> audio_clock; /* Frames written to the stream since it was opened. Reset on device change. Uses mix_params.rate. */ - UINT64 frames_written; + UINT64 frames_written = 0; /* Frames written to the (logical) stream since it was first created. Updated on device change. Uses stream_params.rate. */ - UINT64 total_frames_written; + UINT64 total_frames_written = 0; /* Last valid reported stream position. Used to ensure the position reported by stream_get_position increases monotonically. */ - UINT64 prev_position; + UINT64 prev_position = 0; /* Device enumerator to be able to be notified when the default device change. */ - IMMDeviceEnumerator * device_enumerator; + com_ptr<IMMDeviceEnumerator> device_enumerator; /* Device notification client, to be able to be notified when the default audio device changes and route the audio to the new default audio output device */ - wasapi_endpoint_notification_client * notification_client; + com_ptr<wasapi_endpoint_notification_client> notification_client; /* Main andle to the WASAPI capture stream. */ - IAudioClient * input_client; + com_ptr<IAudioClient> input_client; /* Interface to use the event driven capture interface */ - IAudioCaptureClient * capture_client; + com_ptr<IAudioCaptureClient> capture_client; /* This event is set by the stream_stop and stream_destroy function, so the render loop can exit properly. */ - HANDLE shutdown_event; + HANDLE shutdown_event = 0; /* Set by OnDefaultDeviceChanged when a stream reconfiguration is required. The reconfiguration is handled by the render loop thread. */ - HANDLE reconfigure_event; + HANDLE reconfigure_event = 0; /* This is set by WASAPI when we should refill the stream. */ - HANDLE refill_event; + HANDLE refill_event = 0; /* This is set by WASAPI when we should read from the input stream. In * practice, we read from the input stream in the output callback, so * this is not used, but it is necessary to start getting input data. */ - HANDLE input_available_event; + HANDLE input_available_event = 0; /* Each cubeb_stream has its own thread. */ - HANDLE thread; + HANDLE thread = 0; /* The lock protects all members that are touched by the render thread or change during a device reset, including: audio_clock, audio_stream_volume, client, frames_written, mix_params, total_frames_written, prev_position. */ owned_critical_section stream_reset_lock; /* Maximum number of frames that can be passed down in a callback. */ - uint32_t input_buffer_frame_count; + uint32_t input_buffer_frame_count = 0; /* Maximum number of frames that can be requested in a callback. */ - uint32_t output_buffer_frame_count; + uint32_t output_buffer_frame_count = 0; /* Resampler instance. Resampling will only happen if necessary. */ - cubeb_resampler * resampler; - /* A buffer for up/down mixing multi-channel audio. */ - float * mix_buffer; + std::unique_ptr<cubeb_resampler, decltype(&cubeb_resampler_destroy)> resampler = { nullptr, cubeb_resampler_destroy }; + /* Mixer interfaces */ + std::unique_ptr<cubeb_mixer, decltype(&cubeb_mixer_destroy)> output_mixer = { nullptr, cubeb_mixer_destroy }; + std::unique_ptr<cubeb_mixer, decltype(&cubeb_mixer_destroy)> input_mixer = { nullptr, cubeb_mixer_destroy }; + /* A buffer for up/down mixing multi-channel audio output. */ + std::vector<BYTE> mix_buffer; /* WASAPI input works in "packets". We re-linearize the audio packets * into this buffer before handing it to the resampler. */ - auto_array<float> linear_input_buffer; + std::unique_ptr<auto_array_wrapper> linear_input_buffer; + /* Bytes per sample. This multiplied by the number of channels is the number + * of bytes per frame. */ + size_t bytes_per_sample = 0; + /* WAVEFORMATEXTENSIBLE sub-format: either PCM or float. */ + GUID waveformatextensible_sub_format = GUID_NULL; /* Stream volume. Set via stream_set_volume and used to reset volume on device changes. */ - float volume; + float volume = 1.0; /* True if the stream is draining. */ - bool draining; + bool draining = false; /* True when we've destroyed the stream. This pointer is leaked on stream * destruction if we could not join the thread. */ - std::atomic<std::atomic<bool>*> emergency_bailout; + std::atomic<std::atomic<bool>*> emergency_bailout { nullptr }; + /* Synchronizes render thread start to ensure safe access to emergency_bailout. */ + HANDLE thread_ready_event = 0; +}; + +class monitor_device_notifications { +public: + monitor_device_notifications(cubeb * context) + : cubeb_context(context) + { + create_thread(); + } + + ~monitor_device_notifications() + { + SetEvent(shutdown); + WaitForSingleObject(thread, INFINITE); + CloseHandle(thread); + + CloseHandle(input_changed); + CloseHandle(output_changed); + CloseHandle(shutdown); + } + + void notify(EDataFlow flow) + { + XASSERT(cubeb_context); + if (flow == eCapture && cubeb_context->input_collection_changed_callback) { + bool res = SetEvent(input_changed); + if (!res) { + LOG("Failed to set input changed event"); + } + return; + } + if (flow == eRender && cubeb_context->output_collection_changed_callback) { + bool res = SetEvent(output_changed); + if (!res) { + LOG("Failed to set output changed event"); + } + } + } +private: + static unsigned int __stdcall + thread_proc(LPVOID args) + { + XASSERT(args); + static_cast<monitor_device_notifications*>(args) + ->notification_thread_loop(); + return 0; + } + + void notification_thread_loop() + { + struct auto_com { + auto_com() { + HRESULT hr = CoInitializeEx(nullptr, COINIT_MULTITHREADED); + XASSERT(SUCCEEDED(hr)); + } + ~auto_com() { + CoUninitialize(); + } + } com; + + HANDLE wait_array[3] = { + input_changed, + output_changed, + shutdown, + }; + + while (true) { + Sleep(200); + + DWORD wait_result = WaitForMultipleObjects(ARRAY_LENGTH(wait_array), + wait_array, + FALSE, + INFINITE); + if (wait_result == WAIT_OBJECT_0) { // input changed + cubeb_context->input_collection_changed_callback(cubeb_context, + cubeb_context->input_collection_changed_user_ptr); + } else if (wait_result == WAIT_OBJECT_0 + 1) { // output changed + cubeb_context->output_collection_changed_callback(cubeb_context, + cubeb_context->output_collection_changed_user_ptr); + } else if (wait_result == WAIT_OBJECT_0 + 2) { // shutdown + break; + } else { + LOG("Unexpected result %lu", wait_result); + } + } // loop + } + + void create_thread() + { + output_changed = CreateEvent(nullptr, 0, 0, nullptr); + if (!output_changed) { + LOG("Failed to create output changed event."); + return; + } + + input_changed = CreateEvent(nullptr, 0, 0, nullptr); + if (!input_changed) { + LOG("Failed to create input changed event."); + return; + } + + shutdown = CreateEvent(nullptr, 0, 0, nullptr); + if (!shutdown) { + LOG("Failed to create shutdown event."); + return; + } + + thread = (HANDLE) _beginthreadex(nullptr, + 256 * 1024, + thread_proc, + this, + STACK_SIZE_PARAM_IS_A_RESERVATION, + nullptr); + if (!thread) { + LOG("Failed to create thread."); + return; + } + } + + HANDLE thread = INVALID_HANDLE_VALUE; + HANDLE output_changed = INVALID_HANDLE_VALUE; + HANDLE input_changed = INVALID_HANDLE_VALUE; + HANDLE shutdown = INVALID_HANDLE_VALUE; + + cubeb * cubeb_context = nullptr; +}; + +class wasapi_collection_notification_client : public IMMNotificationClient +{ +public: + /* The implementation of MSCOM was copied from MSDN. */ + ULONG STDMETHODCALLTYPE + AddRef() + { + return InterlockedIncrement(&ref_count); + } + + ULONG STDMETHODCALLTYPE + Release() + { + ULONG ulRef = InterlockedDecrement(&ref_count); + if (0 == ulRef) { + delete this; + } + return ulRef; + } + + HRESULT STDMETHODCALLTYPE + QueryInterface(REFIID riid, VOID **ppvInterface) + { + if (__uuidof(IUnknown) == riid) { + AddRef(); + *ppvInterface = (IUnknown*)this; + } else if (__uuidof(IMMNotificationClient) == riid) { + AddRef(); + *ppvInterface = (IMMNotificationClient*)this; + } else { + *ppvInterface = NULL; + return E_NOINTERFACE; + } + return S_OK; + } + + wasapi_collection_notification_client(cubeb * context) + : ref_count(1) + , cubeb_context(context) + , monitor_notifications(context) + { + XASSERT(cubeb_context); + } + + virtual ~wasapi_collection_notification_client() + { } + + HRESULT STDMETHODCALLTYPE + OnDefaultDeviceChanged(EDataFlow flow, ERole role, LPCWSTR device_id) + { + LOG("collection: Audio device default changed, id = %S.", device_id); + return S_OK; + } + + /* The remaining methods are not implemented, they simply log when called (if + log is enabled), for debugging. */ + HRESULT STDMETHODCALLTYPE OnDeviceAdded(LPCWSTR device_id) + { + LOG("collection: Audio device added."); + return S_OK; + }; + + HRESULT STDMETHODCALLTYPE OnDeviceRemoved(LPCWSTR device_id) + { + LOG("collection: Audio device removed."); + return S_OK; + } + + HRESULT STDMETHODCALLTYPE + OnDeviceStateChanged(LPCWSTR device_id, DWORD new_state) + { + XASSERT(cubeb_context->output_collection_changed_callback || + cubeb_context->input_collection_changed_callback); + LOG("collection: Audio device state changed, id = %S, state = %lu.", device_id, new_state); + EDataFlow flow; + HRESULT hr = GetDataFlow(device_id, &flow); + if (FAILED(hr)) { + return hr; + } + monitor_notifications.notify(flow); + return S_OK; + } + + HRESULT STDMETHODCALLTYPE + OnPropertyValueChanged(LPCWSTR device_id, const PROPERTYKEY key) + { + //Audio device property value changed. + return S_OK; + } + +private: + HRESULT GetDataFlow(LPCWSTR device_id, EDataFlow * flow) + { + com_ptr<IMMDevice> device; + com_ptr<IMMEndpoint> endpoint; + + HRESULT hr = cubeb_context->device_collection_enumerator + ->GetDevice(device_id, device.receive()); + if (FAILED(hr)) { + LOG("collection: Could not get device: %lx", hr); + return hr; + } + + hr = device->QueryInterface(IID_PPV_ARGS(endpoint.receive())); + if (FAILED(hr)) { + LOG("collection: Could not get endpoint: %lx", hr); + return hr; + } + + return endpoint->GetDataFlow(flow); + } + + /* refcount for this instance, necessary to implement MSCOM semantics. */ + LONG ref_count; + + cubeb * cubeb_context = nullptr; + monitor_device_notifications monitor_notifications; }; class wasapi_endpoint_notification_client : public IMMNotificationClient @@ -262,9 +616,10 @@ public: return S_OK; } - wasapi_endpoint_notification_client(HANDLE event) + wasapi_endpoint_notification_client(HANDLE event, ERole role) : ref_count(1) , reconfigure_event(event) + , role(role) { } virtual ~wasapi_endpoint_notification_client() @@ -273,16 +628,16 @@ public: HRESULT STDMETHODCALLTYPE OnDefaultDeviceChanged(EDataFlow flow, ERole role, LPCWSTR device_id) { - LOG("Audio device default changed."); + LOG("endpoint: Audio device default changed."); /* we only support a single stream type for now. */ - if (flow != eRender && role != eConsole) { + if (flow != eRender && role != this->role) { return S_OK; } BOOL ok = SetEvent(reconfigure_event); if (!ok) { - LOG("SetEvent on reconfigure_event failed: %x", GetLastError()); + LOG("endpoint: SetEvent on reconfigure_event failed: %lx", GetLastError()); } return S_OK; @@ -292,36 +647,54 @@ public: log is enabled), for debugging. */ HRESULT STDMETHODCALLTYPE OnDeviceAdded(LPCWSTR device_id) { - LOG("Audio device added."); + LOG("endpoint: Audio device added."); return S_OK; }; HRESULT STDMETHODCALLTYPE OnDeviceRemoved(LPCWSTR device_id) { - LOG("Audio device removed."); + LOG("endpoint: Audio device removed."); return S_OK; } HRESULT STDMETHODCALLTYPE OnDeviceStateChanged(LPCWSTR device_id, DWORD new_state) { - LOG("Audio device state changed."); + LOG("endpoint: Audio device state changed."); return S_OK; } HRESULT STDMETHODCALLTYPE OnPropertyValueChanged(LPCWSTR device_id, const PROPERTYKEY key) { - LOG("Audio device property value changed."); + //Audio device property value changed. return S_OK; } private: /* refcount for this instance, necessary to implement MSCOM semantics. */ LONG ref_count; HANDLE reconfigure_event; + ERole role; }; namespace { + +char const * +intern_device_id(cubeb * ctx, wchar_t const * id) +{ + XASSERT(id); + + char const * tmp = wstr_to_utf8(id); + if (!tmp) + return nullptr; + + char const * interned = cubeb_strings_intern(ctx->device_ids, tmp); + + free((void *) tmp); + + return interned; +} + bool has_input(cubeb_stream * stm) { return stm->input_stream_params.rate != 0; @@ -332,22 +705,33 @@ bool has_output(cubeb_stream * stm) return stm->output_stream_params.rate != 0; } -bool should_upmix(cubeb_stream_params & stream, cubeb_stream_params & mixer) +double stream_to_mix_samplerate_ratio(cubeb_stream_params & stream, cubeb_stream_params & mixer) { - return mixer.channels > stream.channels; + return double(stream.rate) / mixer.rate; } -bool should_downmix(cubeb_stream_params & stream, cubeb_stream_params & mixer) -{ - return mixer.channels < stream.channels; -} +/* Convert the channel layout into the corresponding KSAUDIO_CHANNEL_CONFIG. + See more: https://msdn.microsoft.com/en-us/library/windows/hardware/ff537083(v=vs.85).aspx */ -double stream_to_mix_samplerate_ratio(cubeb_stream_params & stream, cubeb_stream_params & mixer) +cubeb_channel_layout +mask_to_channel_layout(WAVEFORMATEX const * fmt) { - return double(stream.rate) / mixer.rate; + cubeb_channel_layout mask = 0; + + if (fmt->wFormatTag == WAVE_FORMAT_EXTENSIBLE) { + WAVEFORMATEXTENSIBLE const * ext = reinterpret_cast<WAVEFORMATEXTENSIBLE const *>(fmt); + mask = ext->dwChannelMask; + } else if (fmt->wFormatTag == WAVE_FORMAT_PCM || + fmt->wFormatTag == WAVE_FORMAT_IEEE_FLOAT) { + if (fmt->nChannels == 1) { + mask = CHANNEL_FRONT_CENTER; + } else if (fmt->nChannels == 2) { + mask = CHANNEL_FRONT_LEFT | CHANNEL_FRONT_RIGHT; + } + } + return mask; } - uint32_t get_rate(cubeb_stream * stm) { @@ -356,99 +740,21 @@ get_rate(cubeb_stream * stm) } uint32_t -ms_to_hns(uint32_t ms) -{ - return ms * 10000; -} - -uint32_t -hns_to_ms(REFERENCE_TIME hns) -{ - return static_cast<uint32_t>(hns / 10000); -} - -double -hns_to_s(REFERENCE_TIME hns) +hns_to_frames(uint32_t rate, REFERENCE_TIME hns) { - return static_cast<double>(hns) / 10000000; + return std::ceil((hns - 1) / 10000000.0 * rate); } uint32_t hns_to_frames(cubeb_stream * stm, REFERENCE_TIME hns) { - return hns_to_ms(hns * get_rate(stm)) / 1000; -} - -uint32_t -hns_to_frames(uint32_t rate, REFERENCE_TIME hns) -{ - return hns_to_ms(hns * rate) / 1000; + return hns_to_frames(get_rate(stm), hns); } REFERENCE_TIME frames_to_hns(cubeb_stream * stm, uint32_t frames) { - return frames * 1000 / get_rate(stm); -} - -/* Upmix function, copies a mono channel into L and R */ -template<typename T> -void -mono_to_stereo(T * in, long insamples, T * out, int32_t out_channels) -{ - for (int i = 0, j = 0; i < insamples; ++i, j += out_channels) { - out[j] = out[j + 1] = in[i]; - } -} - -template<typename T> -void -upmix(T * in, long inframes, T * out, int32_t in_channels, int32_t out_channels) -{ - XASSERT(out_channels >= in_channels && in_channels > 0); - - /* Either way, if we have 2 or more channels, the first two are L and R. */ - /* If we are playing a mono stream over stereo speakers, copy the data over. */ - if (in_channels == 1 && out_channels >= 2) { - mono_to_stereo(in, inframes, out, out_channels); - } else { - /* Copy through. */ - for (int i = 0, o = 0; i < inframes * in_channels; - i += in_channels, o += out_channels) { - for (int j = 0; j < in_channels; ++j) { - out[o + j] = in[i + j]; - } - } - } - - /* Check if more channels. */ - if (out_channels <= 2) { - return; - } - - /* Put silence in remaining channels. */ - for (long i = 0, o = 0; i < inframes; ++i, o += out_channels) { - for (int j = 2; j < out_channels; ++j) { - out[o + j] = 0.0; - } - } -} - -template<typename T> -void -downmix(T * in, long inframes, T * out, int32_t in_channels, int32_t out_channels) -{ - XASSERT(in_channels >= out_channels); - /* We could use a downmix matrix here, applying mixing weight based on the - channel, but directsound and winmm simply drop the channels that cannot be - rendered by the hardware, so we do the same for consistency. */ - long out_index = 0; - for (long i = 0; i < inframes * in_channels; i += in_channels) { - for (int j = 0; j < out_channels; ++j) { - out[out_index + j] = in[i + j]; - } - out_index += out_channels; - } + return std::ceil(frames * 10000000.0 / get_rate(stm)); } /* This returns the size of a frame in the stream, before the eventual upmix @@ -456,30 +762,31 @@ downmix(T * in, long inframes, T * out, int32_t in_channels, int32_t out_channel static size_t frames_to_bytes_before_mix(cubeb_stream * stm, size_t frames) { - size_t stream_frame_size = stm->output_stream_params.channels * sizeof(float); - return stream_frame_size * frames; + // This is called only when we has a output client. + XASSERT(has_output(stm)); + return stm->output_stream_params.channels * stm->bytes_per_sample * frames; } /* This function handles the processing of the input and output audio, * converting it to rate and channel layout specified at initialization. * It then calls the data callback, via the resampler. */ long -refill(cubeb_stream * stm, float * input_buffer, long input_frames_count, - float * output_buffer, long output_frames_needed) +refill(cubeb_stream * stm, void * input_buffer, long input_frames_count, + void * output_buffer, long output_frames_needed) { + XASSERT(!stm->draining); /* If we need to upmix after resampling, resample into the mix buffer to - avoid a copy. */ - float * dest = nullptr; - if (has_output(stm)) { - if (should_upmix(stm->output_stream_params, stm->output_mix_params) || - should_downmix(stm->output_stream_params, stm->output_mix_params)) { - dest = stm->mix_buffer; + avoid a copy. Avoid exposing output if it is a dummy stream. */ + void * dest = nullptr; + if (has_output(stm) && !stm->has_dummy_output) { + if (stm->output_mixer) { + dest = stm->mix_buffer.data(); } else { dest = output_buffer; } } - long out_frames = cubeb_resampler_fill(stm->resampler, + long out_frames = cubeb_resampler_fill(stm->resampler.get(), input_buffer, &input_frames_count, dest, @@ -492,47 +799,51 @@ refill(cubeb_stream * stm, float * input_buffer, long input_frames_count, stm->frames_written += out_frames; } - /* Go in draining mode if we got fewer frames than requested. */ - if (out_frames < output_frames_needed) { + /* Go in draining mode if we got fewer frames than requested. If the stream + has no output we still expect the callback to return number of frames read + from input, otherwise we stop. */ + if ((out_frames < output_frames_needed) || + (!has_output(stm) && out_frames < input_frames_count)) { LOG("start draining."); stm->draining = true; } /* If this is not true, there will be glitches. It is alright to have produced less frames if we are draining, though. */ - XASSERT(out_frames == output_frames_needed || stm->draining || !has_output(stm)); - - if (has_output(stm)) { - if (should_upmix(stm->output_stream_params, stm->output_mix_params)) { - upmix(dest, out_frames, output_buffer, - stm->output_stream_params.channels, stm->output_mix_params.channels); - } else if (should_downmix(stm->output_stream_params, stm->output_mix_params)) { - downmix(dest, out_frames, output_buffer, - stm->output_stream_params.channels, stm->output_mix_params.channels); + XASSERT(out_frames == output_frames_needed || stm->draining || !has_output(stm) || stm->has_dummy_output); + + // We don't bother mixing dummy output as it will be silenced, otherwise mix output if needed + if (!stm->has_dummy_output && has_output(stm) && stm->output_mixer) { + XASSERT(dest == stm->mix_buffer.data()); + size_t dest_size = + out_frames * stm->output_stream_params.channels * stm->bytes_per_sample; + XASSERT(dest_size <= stm->mix_buffer.size()); + size_t output_buffer_size = + out_frames * stm->output_mix_params.channels * stm->bytes_per_sample; + int ret = cubeb_mixer_mix(stm->output_mixer.get(), + out_frames, + dest, + dest_size, + output_buffer, + output_buffer_size); + if (ret < 0) { + LOG("Error remixing content (%d)", ret); } } return out_frames; } +int wasapi_stream_reset_default_device(cubeb_stream * stm); + /* This helper grabs all the frames available from a capture client, put them in * linear_input_buffer. linear_input_buffer should be cleared before the - * callback exits. */ + * callback exits. This helper does not work with exclusive mode streams. */ bool get_input_buffer(cubeb_stream * stm) { - HRESULT hr; - UINT32 padding_in; - XASSERT(has_input(stm)); - hr = stm->input_client->GetCurrentPadding(&padding_in); - if (FAILED(hr)) { - LOG("Failed to get padding"); - return false; - } - XASSERT(padding_in <= stm->input_buffer_frame_count); - UINT32 total_available_input = padding_in; - + HRESULT hr; BYTE * input_packet = NULL; DWORD flags; UINT64 dev_pos; @@ -540,73 +851,97 @@ bool get_input_buffer(cubeb_stream * stm) /* Get input packets until we have captured enough frames, and put them in a * contiguous buffer. */ uint32_t offset = 0; - while (offset != total_available_input) { - hr = stm->capture_client->GetNextPacketSize(&next); + // If the input stream is event driven we should only ever expect to read a + // single packet each time. However, if we're pulling from the stream we may + // need to grab multiple packets worth of frames that have accumulated (so + // need a loop). + for (hr = stm->capture_client->GetNextPacketSize(&next); + next > 0; + hr = stm->capture_client->GetNextPacketSize(&next)) { + if (hr == AUDCLNT_E_DEVICE_INVALIDATED) { + // Application can recover from this error. More info + // https://msdn.microsoft.com/en-us/library/windows/desktop/dd316605(v=vs.85).aspx + LOG("Device invalidated error, reset default device"); + wasapi_stream_reset_default_device(stm); + return true; + } + if (FAILED(hr)) { - LOG("cannot get next packet size: %x", hr); + LOG("cannot get next packet size: %lx", hr); return false; } - /* This can happen if the capture stream has stopped. Just return in this - * case. */ - if (!next) { - break; - } - UINT32 packet_size; + UINT32 frames; hr = stm->capture_client->GetBuffer(&input_packet, - &packet_size, + &frames, &flags, &dev_pos, NULL); if (FAILED(hr)) { - LOG("GetBuffer failed for capture: %x", hr); + LOG("GetBuffer failed for capture: %lx", hr); return false; } - XASSERT(packet_size == next); + XASSERT(frames == next); + + UINT32 input_stream_samples = frames * stm->input_stream_params.channels; + // We do not explicitly handle the AUDCLNT_BUFFERFLAGS_DATA_DISCONTINUITY + // flag. There a two primary (non exhaustive) scenarios we anticipate this + // flag being set in: + // - The first GetBuffer after Start has this flag undefined. In this + // case the flag may be set but is meaningless and can be ignored. + // - If a glitch is introduced into the input. This should not happen + // for event based inputs, and should be mitigated by using a dummy + // stream to drive input in the case of input only loopback. Without + // a dummy output, input only loopback would glitch on silence. However, + // the dummy input should push silence to the loopback and prevent + // discontinuities. See https://blogs.msdn.microsoft.com/matthew_van_eerde/2008/12/16/sample-wasapi-loopback-capture-record-what-you-hear/ + // As the first scenario can be ignored, and we anticipate the second + // scenario is mitigated, we ignore the flag. + // For more info: https://msdn.microsoft.com/en-us/library/windows/desktop/dd370859(v=vs.85).aspx, + // https://msdn.microsoft.com/en-us/library/windows/desktop/dd371458(v=vs.85).aspx if (flags & AUDCLNT_BUFFERFLAGS_SILENT) { - LOG("insert silence: ps=%u", packet_size); - stm->linear_input_buffer.push_silence(packet_size * stm->input_stream_params.channels); + LOG("insert silence: ps=%u", frames); + stm->linear_input_buffer->push_silence(input_stream_samples); } else { - if (should_upmix(stm->input_mix_params, stm->input_stream_params)) { - bool ok = stm->linear_input_buffer.reserve(stm->linear_input_buffer.length() + - packet_size * stm->input_stream_params.channels); - assert(ok); - upmix(reinterpret_cast<float*>(input_packet), packet_size, - stm->linear_input_buffer.data() + stm->linear_input_buffer.length(), - stm->input_mix_params.channels, - stm->input_stream_params.channels); - stm->linear_input_buffer.set_length(stm->linear_input_buffer.length() + packet_size * stm->input_stream_params.channels); - } else if (should_downmix(stm->input_mix_params, stm->input_stream_params)) { - bool ok = stm->linear_input_buffer.reserve(stm->linear_input_buffer.length() + - packet_size * stm->input_stream_params.channels); - assert(ok); - downmix(reinterpret_cast<float*>(input_packet), packet_size, - stm->linear_input_buffer.data() + stm->linear_input_buffer.length(), - stm->input_mix_params.channels, - stm->input_stream_params.channels); - stm->linear_input_buffer.set_length(stm->linear_input_buffer.length() + packet_size * stm->input_stream_params.channels); + if (stm->input_mixer) { + bool ok = stm->linear_input_buffer->reserve( + stm->linear_input_buffer->length() + input_stream_samples); + XASSERT(ok); + size_t input_packet_size = + frames * stm->input_mix_params.channels * + cubeb_sample_size(stm->input_mix_params.format); + size_t linear_input_buffer_size = + input_stream_samples * + cubeb_sample_size(stm->input_stream_params.format); + cubeb_mixer_mix(stm->input_mixer.get(), + frames, + input_packet, + input_packet_size, + stm->linear_input_buffer->end(), + linear_input_buffer_size); + stm->linear_input_buffer->set_length( + stm->linear_input_buffer->length() + input_stream_samples); } else { - stm->linear_input_buffer.push(reinterpret_cast<float*>(input_packet), - packet_size * stm->input_stream_params.channels); + stm->linear_input_buffer->push( + input_packet, input_stream_samples); } } - hr = stm->capture_client->ReleaseBuffer(packet_size); + hr = stm->capture_client->ReleaseBuffer(frames); if (FAILED(hr)) { LOG("FAILED to release intput buffer"); return false; } - offset += packet_size; + offset += input_stream_samples; } - assert(stm->linear_input_buffer.length() >= total_available_input && - offset == total_available_input); + XASSERT(stm->linear_input_buffer->length() >= offset); return true; } /* Get an output buffer from the render_client. It has to be released before * exiting the callback. */ -bool get_output_buffer(cubeb_stream * stm, float *& buffer, size_t & frame_count) +bool get_output_buffer(cubeb_stream * stm, void *& buffer, size_t & frame_count) { UINT32 padding_out; HRESULT hr; @@ -614,10 +949,19 @@ bool get_output_buffer(cubeb_stream * stm, float *& buffer, size_t & frame_count XASSERT(has_output(stm)); hr = stm->output_client->GetCurrentPadding(&padding_out); + if (hr == AUDCLNT_E_DEVICE_INVALIDATED) { + // Application can recover from this error. More info + // https://msdn.microsoft.com/en-us/library/windows/desktop/dd316605(v=vs.85).aspx + LOG("Device invalidated error, reset default device"); + wasapi_stream_reset_default_device(stm); + return true; + } + if (FAILED(hr)) { - LOG("Failed to get padding: %x", hr); + LOG("Failed to get padding: %lx", hr); return false; } + XASSERT(padding_out <= stm->output_buffer_frame_count); if (stm->draining) { @@ -639,7 +983,7 @@ bool get_output_buffer(cubeb_stream * stm, float *& buffer, size_t & frame_count return false; } - buffer = reinterpret_cast<float*>(output_buffer); + buffer = output_buffer; return true; } @@ -651,7 +995,7 @@ bool refill_callback_duplex(cubeb_stream * stm) { HRESULT hr; - float * output_buffer = nullptr; + void * output_buffer = nullptr; size_t output_frames = 0; size_t input_frames; bool rv; @@ -663,7 +1007,7 @@ refill_callback_duplex(cubeb_stream * stm) return rv; } - input_frames = stm->linear_input_buffer.length() / stm->input_stream_params.channels; + input_frames = stm->linear_input_buffer->length() / stm->input_stream_params.channels; if (!input_frames) { return true; } @@ -680,29 +1024,43 @@ refill_callback_duplex(cubeb_stream * stm) return true; } - // When WASAPI has not filled the input buffer yet, send silence. - double output_duration = double(output_frames) / stm->output_mix_params.rate; - double input_duration = double(stm->linear_input_buffer.length() / stm->input_stream_params.channels) / stm->input_mix_params.rate; - if (input_duration < output_duration) { - size_t padding = size_t(round((output_duration - input_duration) * stm->input_mix_params.rate)); - LOG("padding silence: out=%f in=%f pad=%u", output_duration, input_duration, padding); - stm->linear_input_buffer.push_front_silence(padding * stm->input_stream_params.channels); + /* Wait for draining is not important on duplex. */ + if (stm->draining) { + return false; } - LOGV("Duplex callback: input frames: %zu, output frames: %zu", - stm->linear_input_buffer.length(), output_frames); + if (stm->has_dummy_output) { + ALOGV("Duplex callback (dummy output): input frames: %Iu, output frames: %Iu", + input_frames, output_frames); + + // We don't want to expose the dummy output to the callback so don't pass + // the output buffer (it will be released later with silence in it) + refill(stm, + stm->linear_input_buffer->data(), + input_frames, + nullptr, + 0); + } else { + ALOGV("Duplex callback: input frames: %Iu, output frames: %Iu", + input_frames, output_frames); - refill(stm, - stm->linear_input_buffer.data(), - stm->linear_input_buffer.length(), - output_buffer, - output_frames); + refill(stm, + stm->linear_input_buffer->data(), + input_frames, + output_buffer, + output_frames); + } - stm->linear_input_buffer.clear(); + stm->linear_input_buffer->clear(); - hr = stm->render_client->ReleaseBuffer(output_frames, 0); + if (stm->has_dummy_output) { + // If output is a dummy output, make sure it's silent + hr = stm->render_client->ReleaseBuffer(output_frames, AUDCLNT_BUFFERFLAGS_SILENT); + } else { + hr = stm->render_client->ReleaseBuffer(output_frames, 0); + } if (FAILED(hr)) { - LOG("failed to release buffer: %x", hr); + LOG("failed to release buffer: %lx", hr); return false; } return true; @@ -711,7 +1069,8 @@ refill_callback_duplex(cubeb_stream * stm) bool refill_callback_input(cubeb_stream * stm) { - bool rv, consumed_all_buffer; + bool rv; + size_t input_frames; XASSERT(has_input(stm) && !has_output(stm)); @@ -720,24 +1079,24 @@ refill_callback_input(cubeb_stream * stm) return rv; } - // This can happen at the very beginning of the stream. - if (!stm->linear_input_buffer.length()) { + input_frames = stm->linear_input_buffer->length() / stm->input_stream_params.channels; + if (!input_frames) { return true; } - LOGV("Input callback: input frames: %zu", stm->linear_input_buffer.length()); + ALOGV("Input callback: input frames: %Iu", input_frames); long read = refill(stm, - stm->linear_input_buffer.data(), - stm->linear_input_buffer.length(), + stm->linear_input_buffer->data(), + input_frames, nullptr, 0); - consumed_all_buffer = read == stm->linear_input_buffer.length(); + XASSERT(read >= 0); - stm->linear_input_buffer.clear(); + stm->linear_input_buffer->clear(); - return consumed_all_buffer; + return !stm->draining; } bool @@ -745,7 +1104,7 @@ refill_callback_output(cubeb_stream * stm) { bool rv; HRESULT hr; - float * output_buffer = nullptr; + void * output_buffer = nullptr; size_t output_frames = 0; XASSERT(!has_input(stm) && has_output(stm)); @@ -765,19 +1124,19 @@ refill_callback_output(cubeb_stream * stm) output_buffer, output_frames); - LOGV("Output callback: output frames requested: %zu, got %ld", - output_frames, got); + ALOGV("Output callback: output frames requested: %Iu, got %ld", + output_frames, got); XASSERT(got >= 0); - XASSERT(got == output_frames || stm->draining); + XASSERT(size_t(got) == output_frames || stm->draining); hr = stm->render_client->ReleaseBuffer(got, 0); if (FAILED(hr)) { - LOG("failed to release buffer: %x", hr); + LOG("failed to release buffer: %lx", hr); return false; } - return got == output_frames || stm->draining; + return size_t(got) == output_frames || stm->draining; } static unsigned int __stdcall @@ -786,6 +1145,13 @@ wasapi_stream_render_loop(LPVOID stream) cubeb_stream * stm = static_cast<cubeb_stream *>(stream); std::atomic<bool> * emergency_bailout = stm->emergency_bailout; + // Signal wasapi_stream_start that we've copied emergency_bailout. + BOOL ok = SetEvent(stm->thread_ready_event); + if (!ok) { + LOG("thread_ready SetEvent failed: %lx", GetLastError()); + return 0; + } + bool is_playing = true; HANDLE wait_array[4] = { stm->shutdown_event, @@ -796,25 +1162,22 @@ wasapi_stream_render_loop(LPVOID stream) HANDLE mmcss_handle = NULL; HRESULT hr = 0; DWORD mmcss_task_index = 0; - auto_com com; - if (!com.ok()) { - LOG("COM initialization failed on render_loop thread."); - stm->state_callback(stm, stm->user_ptr, CUBEB_STATE_ERROR); - return 0; - } + struct auto_com { + auto_com() { + HRESULT hr = CoInitializeEx(nullptr, COINIT_MULTITHREADED); + XASSERT(SUCCEEDED(hr)); + } + ~auto_com() { + CoUninitialize(); + } + } com; /* We could consider using "Pro Audio" here for WebAudio and maybe WebRTC. */ - mmcss_handle = - stm->context->set_mm_thread_characteristics("Audio", &mmcss_task_index); + mmcss_handle = AvSetMmThreadCharacteristicsA("Audio", &mmcss_task_index); if (!mmcss_handle) { /* This is not fatal, but we might glitch under heavy load. */ - LOG("Unable to use mmcss to bump the render thread priority: %x", GetLastError()); - } - - // This has already been nulled out, simply exit. - if (!emergency_bailout) { - is_playing = false; + LOG("Unable to use mmcss to bump the render thread priority: %lx", GetLastError()); } /* WaitForMultipleObjects timeout can trigger in cases where we don't want to @@ -822,7 +1185,7 @@ wasapi_stream_render_loop(LPVOID stream) the timeout error handling only when the timeout_limit is reached, which is reset on each successful loop. */ unsigned timeout_count = 0; - const unsigned timeout_limit = 5; + const unsigned timeout_limit = 3; while (is_playing) { // We want to check the emergency bailout variable before a // and after the WaitForMultipleObject, because the handles WaitForMultipleObjects @@ -883,18 +1246,28 @@ wasapi_stream_render_loop(LPVOID stream) } XASSERT(stm->output_client || stm->input_client); if (stm->output_client) { - stm->output_client->Start(); + hr = stm->output_client->Start(); + if (FAILED(hr)) { + LOG("Error starting output after reconfigure, error: %lx", hr); + is_playing = false; + continue; + } LOG("Output started after reconfigure."); } if (stm->input_client) { - stm->input_client->Start(); + hr = stm->input_client->Start(); + if (FAILED(hr)) { + LOG("Error starting input after reconfiguring, error: %lx", hr); + is_playing = false; + continue; + } LOG("Input started after reconfigure."); } break; } case WAIT_OBJECT_0 + 2: /* refill */ - XASSERT(has_input(stm) && has_output(stm) || - !has_input(stm) && has_output(stm)); + XASSERT((has_input(stm) && has_output(stm)) || + (!has_input(stm) && has_output(stm))); is_playing = stm->refill_callback(stm); break; case WAIT_OBJECT_0 + 3: /* input available */ @@ -910,7 +1283,7 @@ wasapi_stream_render_loop(LPVOID stream) } break; default: - LOG("case %d not handled in render loop.", waitResult); + LOG("case %lu not handled in render loop.", waitResult); abort(); } } @@ -919,41 +1292,31 @@ wasapi_stream_render_loop(LPVOID stream) stm->state_callback(stm, stm->user_ptr, CUBEB_STATE_ERROR); } - stm->context->revert_mm_thread_characteristics(mmcss_handle); + if (mmcss_handle) { + AvRevertMmThreadCharacteristics(mmcss_handle); + } return 0; } void wasapi_destroy(cubeb * context); -HANDLE WINAPI set_mm_thread_characteristics_noop(const char *, LPDWORD mmcss_task_index) -{ - return (HANDLE)1; -} - -BOOL WINAPI revert_mm_thread_characteristics_noop(HANDLE mmcss_handle) -{ - return true; -} - HRESULT register_notification_client(cubeb_stream * stm) { HRESULT hr = CoCreateInstance(__uuidof(MMDeviceEnumerator), NULL, CLSCTX_INPROC_SERVER, - IID_PPV_ARGS(&stm->device_enumerator)); + IID_PPV_ARGS(stm->device_enumerator.receive())); if (FAILED(hr)) { - LOG("Could not get device enumerator: %x", hr); + LOG("Could not get device enumerator: %lx", hr); return hr; } - stm->notification_client = new wasapi_endpoint_notification_client(stm->reconfigure_event); + stm->notification_client.reset(new wasapi_endpoint_notification_client(stm->reconfigure_event, stm->role)); - hr = stm->device_enumerator->RegisterEndpointNotificationCallback(stm->notification_client); + hr = stm->device_enumerator->RegisterEndpointNotificationCallback(stm->notification_client.get()); if (FAILED(hr)) { - LOG("Could not register endpoint notification callback: %x", hr); - SafeRelease(stm->notification_client); + LOG("Could not register endpoint notification callback: %lx", hr); stm->notification_client = nullptr; - SafeRelease(stm->device_enumerator); stm->device_enumerator = nullptr; } @@ -969,61 +1332,92 @@ HRESULT unregister_notification_client(cubeb_stream * stm) return S_OK; } - hr = stm->device_enumerator->UnregisterEndpointNotificationCallback(stm->notification_client); + hr = stm->device_enumerator->UnregisterEndpointNotificationCallback(stm->notification_client.get()); if (FAILED(hr)) { // We can't really do anything here, we'll probably leak the // notification client, but we can at least release the enumerator. - SafeRelease(stm->device_enumerator); + stm->device_enumerator = nullptr; return S_OK; } - SafeRelease(stm->notification_client); - SafeRelease(stm->device_enumerator); + stm->notification_client = nullptr; + stm->device_enumerator = nullptr; return S_OK; } -HRESULT get_endpoint(IMMDevice ** device, LPCWSTR devid) +HRESULT get_endpoint(com_ptr<IMMDevice> & device, LPCWSTR devid) { - IMMDeviceEnumerator * enumerator; + com_ptr<IMMDeviceEnumerator> enumerator; HRESULT hr = CoCreateInstance(__uuidof(MMDeviceEnumerator), NULL, CLSCTX_INPROC_SERVER, - IID_PPV_ARGS(&enumerator)); + IID_PPV_ARGS(enumerator.receive())); if (FAILED(hr)) { - LOG("Could not get device enumerator: %x", hr); + LOG("Could not get device enumerator: %lx", hr); return hr; } - hr = enumerator->GetDevice(devid, device); + hr = enumerator->GetDevice(devid, device.receive()); if (FAILED(hr)) { - LOG("Could not get device: %x", hr); - SafeRelease(enumerator); + LOG("Could not get device: %lx", hr); return hr; } - SafeRelease(enumerator); - return S_OK; } -HRESULT get_default_endpoint(IMMDevice ** device, EDataFlow direction) +HRESULT register_collection_notification_client(cubeb * context) { - IMMDeviceEnumerator * enumerator; HRESULT hr = CoCreateInstance(__uuidof(MMDeviceEnumerator), NULL, CLSCTX_INPROC_SERVER, - IID_PPV_ARGS(&enumerator)); + IID_PPV_ARGS(context->device_collection_enumerator.receive())); if (FAILED(hr)) { - LOG("Could not get device enumerator: %x", hr); + LOG("Could not get device enumerator: %lx", hr); return hr; } - hr = enumerator->GetDefaultAudioEndpoint(direction, eConsole, device); + + context->collection_notification_client.reset(new wasapi_collection_notification_client(context)); + + hr = context->device_collection_enumerator->RegisterEndpointNotificationCallback( + context->collection_notification_client.get()); + if (FAILED(hr)) { + LOG("Could not register endpoint notification callback: %lx", hr); + context->collection_notification_client.reset(); + context->device_collection_enumerator.reset(); + } + + return hr; +} + +HRESULT unregister_collection_notification_client(cubeb * context) +{ + HRESULT hr = context->device_collection_enumerator-> + UnregisterEndpointNotificationCallback(context->collection_notification_client.get()); if (FAILED(hr)) { - LOG("Could not get default audio endpoint: %x", hr); - SafeRelease(enumerator); return hr; } - SafeRelease(enumerator); + context->collection_notification_client = nullptr; + context->device_collection_enumerator = nullptr; + + return hr; +} + +HRESULT get_default_endpoint(com_ptr<IMMDevice> & device, EDataFlow direction, ERole role) +{ + com_ptr<IMMDeviceEnumerator> enumerator; + HRESULT hr = CoCreateInstance(__uuidof(MMDeviceEnumerator), + NULL, CLSCTX_INPROC_SERVER, + IID_PPV_ARGS(enumerator.receive())); + if (FAILED(hr)) { + LOG("Could not get device enumerator: %lx", hr); + return hr; + } + hr = enumerator->GetDefaultAudioEndpoint(direction, role, device.receive()); + if (FAILED(hr)) { + LOG("Could not get default audio endpoint: %lx", hr); + return hr; + } return ERROR_SUCCESS; } @@ -1043,14 +1437,14 @@ current_stream_delay(cubeb_stream * stm) UINT64 freq; HRESULT hr = stm->audio_clock->GetFrequency(&freq); if (FAILED(hr)) { - LOG("GetFrequency failed: %x", hr); + LOG("GetFrequency failed: %lx", hr); return 0; } UINT64 pos; hr = stm->audio_clock->GetPosition(&pos, NULL); if (FAILED(hr)) { - LOG("GetPosition failed: %x", hr); + LOG("GetPosition failed: %lx", hr); return 0; } @@ -1073,8 +1467,8 @@ stream_set_volume(cubeb_stream * stm, float volume) uint32_t channels; HRESULT hr = stm->audio_stream_volume->GetChannelCount(&channels); - if (hr != S_OK) { - LOG("could not get the channel count: %x", hr); + if (FAILED(hr)) { + LOG("could not get the channel count: %lx", hr); return CUBEB_ERROR; } @@ -1089,8 +1483,8 @@ stream_set_volume(cubeb_stream * stm, float volume) } hr = stm->audio_stream_volume->SetAllVolumes(channels, volumes); - if (hr != S_OK) { - LOG("could not set the channels volume: %x", hr); + if (FAILED(hr)) { + LOG("could not set the channels volume: %lx", hr); return CUBEB_ERROR; } @@ -1101,49 +1495,27 @@ stream_set_volume(cubeb_stream * stm, float volume) extern "C" { int wasapi_init(cubeb ** context, char const * context_name) { - HRESULT hr; - auto_com com; - if (!com.ok()) { - return CUBEB_ERROR; - } - /* We don't use the device yet, but need to make sure we can initialize one so that this backend is not incorrectly enabled on platforms that don't support WASAPI. */ - IMMDevice * device; - hr = get_default_endpoint(&device, eRender); + com_ptr<IMMDevice> device; + HRESULT hr = get_default_endpoint(device, eRender, eConsole); if (FAILED(hr)) { - LOG("Could not get device: %x", hr); - return CUBEB_ERROR; + XASSERT(hr != CO_E_NOTINITIALIZED); + LOG("It wasn't able to find a default rendering device: %lx", hr); + hr = get_default_endpoint(device, eCapture, eConsole); + if (FAILED(hr)) { + LOG("It wasn't able to find a default capture device: %lx", hr); + return CUBEB_ERROR; + } } - SafeRelease(device); - cubeb * ctx = (cubeb *)calloc(1, sizeof(cubeb)); - if (!ctx) { - return CUBEB_ERROR; - } + cubeb * ctx = new cubeb(); ctx->ops = &wasapi_ops; - - ctx->mmcss_module = LoadLibraryA("Avrt.dll"); - - if (ctx->mmcss_module) { - ctx->set_mm_thread_characteristics = - (set_mm_thread_characteristics_function) GetProcAddress( - ctx->mmcss_module, "AvSetMmThreadCharacteristicsA"); - ctx->revert_mm_thread_characteristics = - (revert_mm_thread_characteristics_function) GetProcAddress( - ctx->mmcss_module, "AvRevertMmThreadCharacteristics"); - if (!(ctx->set_mm_thread_characteristics && ctx->revert_mm_thread_characteristics)) { - LOG("Could not load AvSetMmThreadCharacteristics or AvRevertMmThreadCharacteristics: %x", GetLastError()); - FreeLibrary(ctx->mmcss_module); - } - } else { - // This is not a fatal error, but we might end up glitching when - // the system is under high load. - LOG("Could not load Avrt.dll"); - ctx->set_mm_thread_characteristics = &set_mm_thread_characteristics_noop; - ctx->revert_mm_thread_characteristics = &revert_mm_thread_characteristics_noop; + if (cubeb_strings_init(&ctx->device_ids) != CUBEB_OK) { + delete ctx; + return CUBEB_ERROR; } *context = ctx; @@ -1170,31 +1542,22 @@ bool stop_and_join_render_thread(cubeb_stream * stm) BOOL ok = SetEvent(stm->shutdown_event); if (!ok) { - LOG("Destroy SetEvent failed: %d", GetLastError()); + LOG("Destroy SetEvent failed: %lx", GetLastError()); } /* Wait five seconds for the rendering thread to return. It's supposed to * check its event loop very often, five seconds is rather conservative. */ DWORD r = WaitForSingleObject(stm->thread, 5000); - if (r == WAIT_TIMEOUT) { + if (r != WAIT_OBJECT_0) { /* Something weird happened, leak the thread and continue the shutdown * process. */ *(stm->emergency_bailout) = true; // We give the ownership to the rendering thread. stm->emergency_bailout = nullptr; - LOG("Destroy WaitForSingleObject on thread timed out," - " leaking the thread: %d", GetLastError()); - rv = false; - } - if (r == WAIT_FAILED) { - *(stm->emergency_bailout) = true; - // We give the ownership to the rendering thread. - stm->emergency_bailout = nullptr; - LOG("Destroy WaitForSingleObject on thread failed: %d", GetLastError()); + LOG("Destroy WaitForSingleObject on thread failed: %lx, %lx", r, GetLastError()); rv = false; } - // Only attempts to close and null out the thread and event if the // WaitForSingleObject above succeeded, so that calling this function again // attemps to clean up the thread and event each time. @@ -1212,10 +1575,11 @@ bool stop_and_join_render_thread(cubeb_stream * stm) void wasapi_destroy(cubeb * context) { - if (context->mmcss_module) { - FreeLibrary(context->mmcss_module); + if (context->device_ids) { + cubeb_strings_destroy(context->device_ids); } - free(context); + + delete context; } char const * wasapi_get_backend_id(cubeb * context) @@ -1226,228 +1590,308 @@ char const * wasapi_get_backend_id(cubeb * context) int wasapi_get_max_channel_count(cubeb * ctx, uint32_t * max_channels) { - HRESULT hr; - IAudioClient * client; - WAVEFORMATEX * mix_format; - auto_com com; - if (!com.ok()) { - return CUBEB_ERROR; - } - XASSERT(ctx && max_channels); - IMMDevice * device; - hr = get_default_endpoint(&device, eRender); + com_ptr<IMMDevice> device; + HRESULT hr = get_default_endpoint(device, eRender, eConsole); if (FAILED(hr)) { return CUBEB_ERROR; } + com_ptr<IAudioClient> client; hr = device->Activate(__uuidof(IAudioClient), CLSCTX_INPROC_SERVER, - NULL, (void **)&client); - SafeRelease(device); + NULL, client.receive_vpp()); if (FAILED(hr)) { return CUBEB_ERROR; } - hr = client->GetMixFormat(&mix_format); + WAVEFORMATEX * tmp = nullptr; + hr = client->GetMixFormat(&tmp); if (FAILED(hr)) { - SafeRelease(client); return CUBEB_ERROR; } + com_heap_ptr<WAVEFORMATEX> mix_format(tmp); *max_channels = mix_format->nChannels; - CoTaskMemFree(mix_format); - SafeRelease(client); - return CUBEB_OK; } int wasapi_get_min_latency(cubeb * ctx, cubeb_stream_params params, uint32_t * latency_frames) { - HRESULT hr; - IAudioClient * client; - REFERENCE_TIME default_period; - auto_com com; - if (!com.ok()) { - return CUBEB_ERROR; - } - - if (params.format != CUBEB_SAMPLE_FLOAT32NE) { + if (params.format != CUBEB_SAMPLE_FLOAT32NE && params.format != CUBEB_SAMPLE_S16NE) { return CUBEB_ERROR_INVALID_FORMAT; } - IMMDevice * device; - hr = get_default_endpoint(&device, eRender); + ERole role = pref_to_role(params.prefs); + + com_ptr<IMMDevice> device; + HRESULT hr = get_default_endpoint(device, eRender, role); if (FAILED(hr)) { - LOG("Could not get default endpoint: %x", hr); + LOG("Could not get default endpoint: %lx", hr); return CUBEB_ERROR; } + com_ptr<IAudioClient> client; hr = device->Activate(__uuidof(IAudioClient), CLSCTX_INPROC_SERVER, - NULL, (void **)&client); - SafeRelease(device); + NULL, client.receive_vpp()); if (FAILED(hr)) { - LOG("Could not activate device for latency: %x", hr); + LOG("Could not activate device for latency: %lx", hr); return CUBEB_ERROR; } - /* The second parameter is for exclusive mode, that we don't use. */ - hr = client->GetDevicePeriod(&default_period, NULL); + REFERENCE_TIME minimum_period; + REFERENCE_TIME default_period; + hr = client->GetDevicePeriod(&default_period, &minimum_period); if (FAILED(hr)) { - SafeRelease(client); - LOG("Could not get device period: %x", hr); + LOG("Could not get device period: %lx", hr); return CUBEB_ERROR; } - LOG("default device period: %lld", default_period); + LOG("default device period: %I64d, minimum device period: %I64d", default_period, minimum_period); - /* According to the docs, the best latency we can achieve is by synchronizing - the stream and the engine. + /* If we're on Windows 10, we can use IAudioClient3 to get minimal latency. + Otherwise, according to the docs, the best latency we can achieve is by + synchronizing the stream and the engine. http://msdn.microsoft.com/en-us/library/windows/desktop/dd370871%28v=vs.85%29.aspx */ - *latency_frames = hns_to_frames(params.rate, default_period); + #ifdef _WIN32_WINNT_WIN10 + *latency_frames = hns_to_frames(params.rate, minimum_period); + #else + *latency_frames = hns_to_frames(params.rate, default_period); + #endif LOG("Minimum latency in frames: %u", *latency_frames); - SafeRelease(client); - return CUBEB_OK; } int wasapi_get_preferred_sample_rate(cubeb * ctx, uint32_t * rate) { - HRESULT hr; - IAudioClient * client; - WAVEFORMATEX * mix_format; - auto_com com; - if (!com.ok()) { - return CUBEB_ERROR; - } - - IMMDevice * device; - hr = get_default_endpoint(&device, eRender); + com_ptr<IMMDevice> device; + HRESULT hr = get_default_endpoint(device, eRender, eConsole); if (FAILED(hr)) { return CUBEB_ERROR; } + com_ptr<IAudioClient> client; hr = device->Activate(__uuidof(IAudioClient), CLSCTX_INPROC_SERVER, - NULL, (void **)&client); - SafeRelease(device); + NULL, client.receive_vpp()); if (FAILED(hr)) { return CUBEB_ERROR; } - hr = client->GetMixFormat(&mix_format); + WAVEFORMATEX * tmp = nullptr; + hr = client->GetMixFormat(&tmp); if (FAILED(hr)) { - SafeRelease(client); return CUBEB_ERROR; } + com_heap_ptr<WAVEFORMATEX> mix_format(tmp); *rate = mix_format->nSamplesPerSec; LOG("Preferred sample rate for output: %u", *rate); - CoTaskMemFree(mix_format); - SafeRelease(client); - return CUBEB_OK; } void wasapi_stream_destroy(cubeb_stream * stm); -/* Based on the mix format and the stream format, try to find a way to play - what the user requested. */ static void -handle_channel_layout(cubeb_stream * stm, WAVEFORMATEX ** mix_format, const cubeb_stream_params * stream_params) +waveformatex_update_derived_properties(WAVEFORMATEX * format) { - /* Common case: the hardware is stereo. Up-mixing and down-mixing will be - handled in the callback. */ - if ((*mix_format)->nChannels <= 2) { - return; + format->nBlockAlign = format->wBitsPerSample * format->nChannels / 8; + format->nAvgBytesPerSec = format->nSamplesPerSec * format->nBlockAlign; + if (format->wFormatTag == WAVE_FORMAT_EXTENSIBLE) { + WAVEFORMATEXTENSIBLE * format_pcm = reinterpret_cast<WAVEFORMATEXTENSIBLE *>(format); + format_pcm->Samples.wValidBitsPerSample = format->wBitsPerSample; } +} +/* Based on the mix format and the stream format, try to find a way to play + what the user requested. */ +static void +handle_channel_layout(cubeb_stream * stm, EDataFlow direction, com_heap_ptr<WAVEFORMATEX> & mix_format, const cubeb_stream_params * stream_params) +{ + com_ptr<IAudioClient> & audio_client = (direction == eRender) ? stm->output_client : stm->input_client; + XASSERT(audio_client); /* The docs say that GetMixFormat is always of type WAVEFORMATEXTENSIBLE [1], so the reinterpret_cast below should be safe. In practice, this is not true, and we just want to bail out and let the rest of the code find a good conversion path instead of trying to make WASAPI do it by itself. [1]: http://msdn.microsoft.com/en-us/library/windows/desktop/dd370811%28v=vs.85%29.aspx*/ - if ((*mix_format)->wFormatTag != WAVE_FORMAT_EXTENSIBLE) { + if (mix_format->wFormatTag != WAVE_FORMAT_EXTENSIBLE) { return; } - WAVEFORMATEXTENSIBLE * format_pcm = reinterpret_cast<WAVEFORMATEXTENSIBLE *>(*mix_format); + WAVEFORMATEXTENSIBLE * format_pcm = reinterpret_cast<WAVEFORMATEXTENSIBLE *>(mix_format.get()); /* Stash a copy of the original mix format in case we need to restore it later. */ WAVEFORMATEXTENSIBLE hw_mix_format = *format_pcm; - /* The hardware is in surround mode, we want to only use front left and front - right. Try that, and check if it works. */ - switch (stream_params->channels) { - case 1: /* Mono */ - format_pcm->dwChannelMask = KSAUDIO_SPEAKER_MONO; - break; - case 2: /* Stereo */ - format_pcm->dwChannelMask = KSAUDIO_SPEAKER_STEREO; - break; - default: - XASSERT(false && "Channel layout not supported."); - break; - } - (*mix_format)->nChannels = stream_params->channels; - (*mix_format)->nBlockAlign = ((*mix_format)->wBitsPerSample * (*mix_format)->nChannels) / 8; - (*mix_format)->nAvgBytesPerSec = (*mix_format)->nSamplesPerSec * (*mix_format)->nBlockAlign; - format_pcm->SubFormat = KSDATAFORMAT_SUBTYPE_IEEE_FLOAT; - (*mix_format)->wBitsPerSample = 32; - format_pcm->Samples.wValidBitsPerSample = (*mix_format)->wBitsPerSample; + /* Get the channel mask by the channel layout. + If the layout is not supported, we will get a closest settings below. */ + format_pcm->dwChannelMask = stream_params->layout; + mix_format->nChannels = stream_params->channels; + waveformatex_update_derived_properties(mix_format.get()); /* Check if wasapi will accept our channel layout request. */ WAVEFORMATEX * closest; - HRESULT hr = stm->output_client->IsFormatSupported(AUDCLNT_SHAREMODE_SHARED, - *mix_format, - &closest); + HRESULT hr = audio_client->IsFormatSupported(AUDCLNT_SHAREMODE_SHARED, + mix_format.get(), + &closest); if (hr == S_FALSE) { - /* Not supported, but WASAPI gives us a suggestion. Use it, and handle the - eventual upmix/downmix ourselves */ + /* Channel layout not supported, but WASAPI gives us a suggestion. Use it, + and handle the eventual upmix/downmix ourselves. Ignore the subformat of + the suggestion, since it seems to always be IEEE_FLOAT. */ LOG("Using WASAPI suggested format: channels: %d", closest->nChannels); + XASSERT(closest->wFormatTag == WAVE_FORMAT_EXTENSIBLE); WAVEFORMATEXTENSIBLE * closest_pcm = reinterpret_cast<WAVEFORMATEXTENSIBLE *>(closest); - XASSERT(closest_pcm->SubFormat == format_pcm->SubFormat); - CoTaskMemFree(*mix_format); - *mix_format = closest; + format_pcm->dwChannelMask = closest_pcm->dwChannelMask; + mix_format->nChannels = closest->nChannels; + waveformatex_update_derived_properties(mix_format.get()); } else if (hr == AUDCLNT_E_UNSUPPORTED_FORMAT) { /* Not supported, no suggestion. This should not happen, but it does in the field with some sound cards. We restore the mix format, and let the rest of the code figure out the right conversion path. */ - *reinterpret_cast<WAVEFORMATEXTENSIBLE *>(*mix_format) = hw_mix_format; + XASSERT(mix_format->wFormatTag == WAVE_FORMAT_EXTENSIBLE); + *reinterpret_cast<WAVEFORMATEXTENSIBLE *>(mix_format.get()) = hw_mix_format; } else if (hr == S_OK) { LOG("Requested format accepted by WASAPI."); } else { - LOG("IsFormatSupported unhandled error: %x", hr); + LOG("IsFormatSupported unhandled error: %lx", hr); } } +static bool +initialize_iaudioclient3(com_ptr<IAudioClient> & audio_client, + cubeb_stream * stm, + const com_heap_ptr<WAVEFORMATEX> & mix_format, + DWORD flags, + EDataFlow direction) +{ + com_ptr<IAudioClient3> audio_client3; + audio_client->QueryInterface<IAudioClient3>(audio_client3.receive()); + if (!audio_client3) { + LOG("Could not get IAudioClient3 interface"); + return false; + } + + if (flags & AUDCLNT_STREAMFLAGS_LOOPBACK) { + // IAudioClient3 doesn't work with loopback streams, and will return error + // 88890021: AUDCLNT_E_INVALID_STREAM_FLAG + LOG("Audio stream is loopback, not using IAudioClient3"); + return false; + } + + // IAudioClient3 doesn't support AUDCLNT_STREAMFLAGS_NOPERSIST, and will return + // AUDCLNT_E_INVALID_STREAM_FLAG. This is undocumented. + flags = flags ^ AUDCLNT_STREAMFLAGS_NOPERSIST; + + // Some people have reported glitches with capture streams: + // http://blog.nirbheek.in/2018/03/low-latency-audio-on-windows-with.html + if (direction == eCapture) { + LOG("Audio stream is capture, not using IAudioClient3"); + return false; + } + + // Possibly initialize a shared-mode stream using IAudioClient3. Initializing + // a stream this way lets you request lower latencies, but also locks the global + // WASAPI engine at that latency. + // - If we request a shared-mode stream, streams created with IAudioClient will + // have their latency adjusted to match. When the shared-mode stream is + // closed, they'll go back to normal. + // - If there's already a shared-mode stream running, then we cannot request + // the engine change to a different latency - we have to match it. + // - It's antisocial to lock the WASAPI engine at its default latency. If we + // would do this, then stop and use IAudioClient instead. + + HRESULT hr; + uint32_t default_period = 0, fundamental_period = 0, min_period = 0, max_period = 0; + hr = audio_client3->GetSharedModeEnginePeriod(mix_format.get(), &default_period, &fundamental_period, &min_period, &max_period); + if (FAILED(hr)) { + LOG("Could not get shared mode engine period: error: %lx", hr); + return false; + } + uint32_t requested_latency = stm->latency; + if (requested_latency >= default_period) { + LOG("Requested latency %i greater than default latency %i, not using IAudioClient3", requested_latency, default_period); + return false; + } + LOG("Got shared mode engine period: default=%i fundamental=%i min=%i max=%i", default_period, fundamental_period, min_period, max_period); + // Snap requested latency to a valid value + uint32_t old_requested_latency = requested_latency; + if (requested_latency < min_period) { + requested_latency = min_period; + } + requested_latency -= (requested_latency - min_period) % fundamental_period; + if (requested_latency != old_requested_latency) { + LOG("Requested latency %i was adjusted to %i", old_requested_latency, requested_latency); + } + + hr = audio_client3->InitializeSharedAudioStream(flags, requested_latency, mix_format.get(), NULL); + if (SUCCEEDED(hr)) { + return true; + } + else if (hr == AUDCLNT_E_ENGINE_PERIODICITY_LOCKED) { + LOG("Got AUDCLNT_E_ENGINE_PERIODICITY_LOCKED, adjusting latency request"); + } else { + LOG("Could not initialize shared stream with IAudioClient3: error: %lx", hr); + return false; + } + + uint32_t current_period = 0; + WAVEFORMATEX* current_format = nullptr; + // We have to pass a valid WAVEFORMATEX** and not nullptr, otherwise + // GetCurrentSharedModeEnginePeriod will return E_POINTER + hr = audio_client3->GetCurrentSharedModeEnginePeriod(¤t_format, ¤t_period); + CoTaskMemFree(current_format); + if (FAILED(hr)) { + LOG("Could not get current shared mode engine period: error: %lx", hr); + return false; + } + + if (current_period >= default_period) { + LOG("Current shared mode engine period %i too high, not using IAudioClient", current_period); + return false; + } + + hr = audio_client3->InitializeSharedAudioStream(flags, current_period, mix_format.get(), NULL); + if (SUCCEEDED(hr)) { + LOG("Current shared mode engine period is %i instead of requested %i", current_period, requested_latency); + return true; + } + + LOG("Could not initialize shared stream with IAudioClient3: error: %lx", hr); + return false; +} + #define DIRECTION_NAME (direction == eCapture ? "capture" : "render") template<typename T> int setup_wasapi_stream_one_side(cubeb_stream * stm, cubeb_stream_params * stream_params, - cubeb_devid devid, + wchar_t const * devid, EDataFlow direction, REFIID riid, - IAudioClient ** audio_client, + com_ptr<IAudioClient> & audio_client, uint32_t * buffer_frame_count, HANDLE & event, - T ** render_or_capture_client, + T & render_or_capture_client, cubeb_stream_params * mix_params) { - IMMDevice * device; - WAVEFORMATEX * mix_format; + com_ptr<IMMDevice> device; HRESULT hr; + bool is_loopback = stream_params->prefs & CUBEB_STREAM_PREF_LOOPBACK; + if (is_loopback && direction != eCapture) { + LOG("Loopback pref can only be used with capture streams!\n"); + return CUBEB_ERROR; + } stm->stream_reset_lock.assert_current_thread_owns(); bool try_again = false; @@ -1455,35 +1899,46 @@ int setup_wasapi_stream_one_side(cubeb_stream * stm, // possibilities. do { if (devid) { - std::unique_ptr<const wchar_t[]> id(utf8_to_wstr(reinterpret_cast<char*>(devid))); - hr = get_endpoint(&device, id.get()); + hr = get_endpoint(device, devid); if (FAILED(hr)) { - LOG("Could not get %s endpoint, error: %x\n", DIRECTION_NAME, hr); + LOG("Could not get %s endpoint, error: %lx\n", DIRECTION_NAME, hr); return CUBEB_ERROR; } - } - else { - hr = get_default_endpoint(&device, direction); + } else { + // If caller has requested loopback but not specified a device, look for + // the default render device. Otherwise look for the default device + // appropriate to the direction. + hr = get_default_endpoint(device, is_loopback ? eRender : direction, pref_to_role(stream_params->prefs)); if (FAILED(hr)) { - LOG("Could not get default %s endpoint, error: %x\n", DIRECTION_NAME, hr); + if (is_loopback) { + LOG("Could not get default render endpoint for loopback, error: %lx\n", hr); + } else { + LOG("Could not get default %s endpoint, error: %lx\n", DIRECTION_NAME, hr); + } return CUBEB_ERROR; } } /* Get a client. We will get all other interfaces we need from * this pointer. */ - hr = device->Activate(__uuidof(IAudioClient), - CLSCTX_INPROC_SERVER, - NULL, (void **)audio_client); - SafeRelease(device); + // hr = device->Activate(__uuidof(IAudioClient3), + // CLSCTX_INPROC_SERVER, + // NULL, audio_client.receive_vpp()); + // if (hr == E_NOINTERFACE) { + hr = device->Activate(__uuidof(IAudioClient), + CLSCTX_INPROC_SERVER, + NULL, audio_client.receive_vpp()); + //} + if (FAILED(hr)) { LOG("Could not activate the device to get an audio" - " client for %s: error: %x\n", DIRECTION_NAME, hr); + " client for %s: error: %lx\n", DIRECTION_NAME, hr); // A particular device can't be activated because it has been // unplugged, try fall back to the default audio device. if (devid && hr == AUDCLNT_E_DEVICE_INVALIDATED) { LOG("Trying again with the default %s audio device.", DIRECTION_NAME); devid = nullptr; + device = nullptr; try_again = true; } else { return CUBEB_ERROR; @@ -1495,62 +1950,85 @@ int setup_wasapi_stream_one_side(cubeb_stream * stm, /* We have to distinguish between the format the mixer uses, * and the format the stream we want to play uses. */ - hr = (*audio_client)->GetMixFormat(&mix_format); + WAVEFORMATEX * tmp = nullptr; + hr = audio_client->GetMixFormat(&tmp); if (FAILED(hr)) { LOG("Could not fetch current mix format from the audio" - " client for %s: error: %x", DIRECTION_NAME, hr); + " client for %s: error: %lx", DIRECTION_NAME, hr); return CUBEB_ERROR; } + com_heap_ptr<WAVEFORMATEX> mix_format(tmp); - handle_channel_layout(stm, &mix_format, stream_params); + mix_format->wBitsPerSample = stm->bytes_per_sample * 8; + if (mix_format->wFormatTag == WAVE_FORMAT_EXTENSIBLE) { + WAVEFORMATEXTENSIBLE * format_pcm = reinterpret_cast<WAVEFORMATEXTENSIBLE *>(mix_format.get()); + format_pcm->SubFormat = stm->waveformatextensible_sub_format; + } + waveformatex_update_derived_properties(mix_format.get()); - /* Shared mode WASAPI always supports float32 sample format, so this - * is safe. */ - mix_params->format = CUBEB_SAMPLE_FLOAT32NE; + /* Set channel layout only when there're more than two channels. Otherwise, + * use the default setting retrieved from the stream format of the audio + * engine's internal processing by GetMixFormat. */ + if (mix_format->nChannels > 2) { + handle_channel_layout(stm, direction, mix_format, stream_params); + } + + mix_params->format = stream_params->format; mix_params->rate = mix_format->nSamplesPerSec; mix_params->channels = mix_format->nChannels; - LOG("Setup requested=[f=%d r=%u c=%u] mix=[f=%d r=%u c=%u]", + mix_params->layout = mask_to_channel_layout(mix_format.get()); + + LOG("Setup requested=[f=%d r=%u c=%u l=%u] mix=[f=%d r=%u c=%u l=%u]", stream_params->format, stream_params->rate, stream_params->channels, - mix_params->format, mix_params->rate, mix_params->channels); - - hr = (*audio_client)->Initialize(AUDCLNT_SHAREMODE_SHARED, - AUDCLNT_STREAMFLAGS_EVENTCALLBACK | - AUDCLNT_STREAMFLAGS_NOPERSIST, - frames_to_hns(stm, stm->latency), - 0, - mix_format, - NULL); + stream_params->layout, + mix_params->format, mix_params->rate, mix_params->channels, + mix_params->layout); + + DWORD flags = AUDCLNT_STREAMFLAGS_NOPERSIST; + + // Check if a loopback device should be requested. Note that event callbacks + // do not work with loopback devices, so only request these if not looping. + if (is_loopback) { + flags |= AUDCLNT_STREAMFLAGS_LOOPBACK; + } else { + flags |= AUDCLNT_STREAMFLAGS_EVENTCALLBACK; + } + + // if (initialize_iaudioclient3(audio_client, stm, mix_format, flags, direction)) { + // LOG("Initialized with IAudioClient3"); + // } else { + hr = audio_client->Initialize(AUDCLNT_SHAREMODE_SHARED, + flags, + frames_to_hns(stm, stm->latency), + 0, + mix_format.get(), + NULL); + // } if (FAILED(hr)) { - LOG("Unable to initialize audio client for %s: %x.", DIRECTION_NAME, hr); + LOG("Unable to initialize audio client for %s: %lx.", DIRECTION_NAME, hr); return CUBEB_ERROR; } - CoTaskMemFree(mix_format); - - hr = (*audio_client)->GetBufferSize(buffer_frame_count); + hr = audio_client->GetBufferSize(buffer_frame_count); if (FAILED(hr)) { LOG("Could not get the buffer size from the client" - " for %s %x.", DIRECTION_NAME, hr); + " for %s %lx.", DIRECTION_NAME, hr); return CUBEB_ERROR; } - // Input is up/down mixed when depacketized in get_input_buffer. - if (has_output(stm) && - (should_upmix(*stream_params, *mix_params) || - should_downmix(*stream_params, *mix_params))) { - stm->mix_buffer = (float *)malloc(frames_to_bytes_before_mix(stm, *buffer_frame_count)); - } - - hr = (*audio_client)->SetEventHandle(event); - if (FAILED(hr)) { - LOG("Could set the event handle for the %s client %x.", - DIRECTION_NAME, hr); - return CUBEB_ERROR; + // Events are used if not looping back + if (!is_loopback) { + hr = audio_client->SetEventHandle(event); + if (FAILED(hr)) { + LOG("Could set the event handle for the %s client %lx.", + DIRECTION_NAME, hr); + return CUBEB_ERROR; + } } - hr = (*audio_client)->GetService(riid, (void **)render_or_capture_client); + hr = audio_client->GetService(riid, render_or_capture_client.receive_vpp()); if (FAILED(hr)) { - LOG("Could not get the %s client %x.", DIRECTION_NAME, hr); + LOG("Could not get the %s client %lx.", DIRECTION_NAME, hr); return CUBEB_ERROR; } @@ -1561,66 +2039,94 @@ int setup_wasapi_stream_one_side(cubeb_stream * stm, int setup_wasapi_stream(cubeb_stream * stm) { - HRESULT hr; int rv; stm->stream_reset_lock.assert_current_thread_owns(); - auto_com com; - if (!com.ok()) { - LOG("Failure to initialize COM."); - return CUBEB_ERROR; - } - XASSERT((!stm->output_client || !stm->input_client) && "WASAPI stream already setup, close it first."); if (has_input(stm)) { - LOG("Setup capture: device=%x", (int)stm->input_device); + LOG("(%p) Setup capture: device=%p", stm, stm->input_device.get()); rv = setup_wasapi_stream_one_side(stm, &stm->input_stream_params, - stm->input_device, + stm->input_device.get(), eCapture, __uuidof(IAudioCaptureClient), - &stm->input_client, + stm->input_client, &stm->input_buffer_frame_count, stm->input_available_event, - &stm->capture_client, + stm->capture_client, &stm->input_mix_params); if (rv != CUBEB_OK) { LOG("Failure to open the input side."); return rv; } + + // We initializing an input stream, buffer ahead two buffers worth of silence. + // This delays the input side slightly, but allow to not glitch when no input + // is available when calling into the resampler to call the callback: the input + // refill event will be set shortly after to compensate for this lack of data. + // In debug, four buffers are used, to avoid tripping up assertions down the line. +#if !defined(DEBUG) + const int silent_buffer_count = 2; +#else + const int silent_buffer_count = 6; +#endif + stm->linear_input_buffer->push_silence(stm->input_buffer_frame_count * + stm->input_stream_params.channels * + silent_buffer_count); + } + + // If we don't have an output device but are requesting a loopback device, + // we attempt to open that same device in output mode in order to drive the + // loopback via the output events. + stm->has_dummy_output = false; + if (!has_output(stm) && stm->input_stream_params.prefs & CUBEB_STREAM_PREF_LOOPBACK) { + stm->output_stream_params.rate = stm->input_stream_params.rate; + stm->output_stream_params.channels = stm->input_stream_params.channels; + stm->output_stream_params.layout = stm->input_stream_params.layout; + if (stm->input_device) { + size_t len = wcslen(stm->input_device.get()); + std::unique_ptr<wchar_t[]> tmp(new wchar_t[len + 1]); + if (wcsncpy_s(tmp.get(), len + 1, stm->input_device.get(), len) != 0) { + LOG("Failed to copy device identifier while copying input stream" + " configuration to output stream configuration to drive loopback."); + return CUBEB_ERROR; + } + stm->output_device = move(tmp); + } + stm->has_dummy_output = true; } if (has_output(stm)) { - LOG("Setup render: device=%x", (int)stm->output_device); + LOG("(%p) Setup render: device=%p", stm, stm->output_device.get()); rv = setup_wasapi_stream_one_side(stm, &stm->output_stream_params, - stm->output_device, + stm->output_device.get(), eRender, __uuidof(IAudioRenderClient), - &stm->output_client, + stm->output_client, &stm->output_buffer_frame_count, stm->refill_event, - &stm->render_client, + stm->render_client, &stm->output_mix_params); if (rv != CUBEB_OK) { LOG("Failure to open the output side."); return rv; } - hr = stm->output_client->GetService(__uuidof(IAudioStreamVolume), - (void **)&stm->audio_stream_volume); + HRESULT hr = stm->output_client->GetService(__uuidof(IAudioStreamVolume), + stm->audio_stream_volume.receive_vpp()); if (FAILED(hr)) { - LOG("Could not get the IAudioStreamVolume: %x", hr); + LOG("Could not get the IAudioStreamVolume: %lx", hr); return CUBEB_ERROR; } XASSERT(stm->frames_written == 0); hr = stm->output_client->GetService(__uuidof(IAudioClock), - (void **)&stm->audio_clock); + stm->audio_clock.receive_vpp()); if (FAILED(hr)) { - LOG("Could not get the IAudioClock: %x", hr); + LOG("Could not get the IAudioClock: %lx", hr); return CUBEB_ERROR; } @@ -1635,7 +2141,7 @@ int setup_wasapi_stream(cubeb_stream * stm) * the highest sample rate available. */ int32_t target_sample_rate; if (has_input(stm) && has_output(stm)) { - assert(stm->input_stream_params.rate == stm->output_stream_params.rate); + XASSERT(stm->input_stream_params.rate == stm->output_stream_params.rate); target_sample_rate = stm->input_stream_params.rate; } else if (has_input(stm)) { target_sample_rate = stm->input_stream_params.rate; @@ -1655,14 +2161,14 @@ int setup_wasapi_stream(cubeb_stream * stm) cubeb_stream_params output_params = stm->output_mix_params; output_params.channels = stm->output_stream_params.channels; - stm->resampler = + stm->resampler.reset( cubeb_resampler_create(stm, has_input(stm) ? &input_params : nullptr, has_output(stm) ? &output_params : nullptr, target_sample_rate, stm->data_callback, stm->user_ptr, - CUBEB_RESAMPLER_QUALITY_DESKTOP); + CUBEB_RESAMPLER_QUALITY_DESKTOP)); if (!stm->resampler) { LOG("Could not get a resampler"); return CUBEB_ERROR; @@ -1678,9 +2184,52 @@ int setup_wasapi_stream(cubeb_stream * stm) stm->refill_callback = refill_callback_output; } + // Create input mixer. + if (has_input(stm) && + ((stm->input_mix_params.layout != CUBEB_LAYOUT_UNDEFINED && + stm->input_mix_params.layout != stm->input_stream_params.layout) || + (stm->input_mix_params.channels != stm->input_stream_params.channels))) { + if (stm->input_mix_params.layout == CUBEB_LAYOUT_UNDEFINED) { + LOG("Input stream using undefined layout! Any mixing may be " + "unpredictable!\n"); + } + stm->input_mixer.reset(cubeb_mixer_create(stm->input_stream_params.format, + stm->input_mix_params.channels, + stm->input_mix_params.layout, + stm->input_stream_params.channels, + stm->input_stream_params.layout)); + assert(stm->input_mixer); + } + + // Create output mixer. + if (has_output(stm) && stm->output_mix_params.layout != stm->output_stream_params.layout) { + if (stm->output_mix_params.layout == CUBEB_LAYOUT_UNDEFINED) { + LOG("Output stream using undefined layout! Any mixing may be unpredictable!\n"); + } + stm->output_mixer.reset(cubeb_mixer_create(stm->output_stream_params.format, + stm->output_stream_params.channels, + stm->output_stream_params.layout, + stm->output_mix_params.channels, + stm->output_mix_params.layout)); + assert(stm->output_mixer); + // Input is up/down mixed when depacketized in get_input_buffer. + stm->mix_buffer.resize( + frames_to_bytes_before_mix(stm, stm->output_buffer_frame_count)); + } + return CUBEB_OK; } +ERole +pref_to_role(cubeb_stream_prefs prefs) +{ + if (prefs & CUBEB_STREAM_PREF_VOICE) { + return eCommunications; + } + + return eConsole; +} + int wasapi_stream_init(cubeb * context, cubeb_stream ** stream, char const * stream_name, @@ -1691,92 +2240,100 @@ wasapi_stream_init(cubeb * context, cubeb_stream ** stream, unsigned int latency_frames, cubeb_data_callback data_callback, cubeb_state_callback state_callback, void * user_ptr) { - HRESULT hr; int rv; - auto_com com; - if (!com.ok()) { - return CUBEB_ERROR; - } XASSERT(context && stream && (input_stream_params || output_stream_params)); - if (output_stream_params && output_stream_params->format != CUBEB_SAMPLE_FLOAT32NE || - input_stream_params && input_stream_params->format != CUBEB_SAMPLE_FLOAT32NE) { - LOG("Invalid format, %p %p %d %d", - output_stream_params, input_stream_params, - output_stream_params && output_stream_params->format, - input_stream_params && input_stream_params->format); + if (output_stream_params && input_stream_params && + output_stream_params->format != input_stream_params->format) { return CUBEB_ERROR_INVALID_FORMAT; } - cubeb_stream * stm = (cubeb_stream *)calloc(1, sizeof(cubeb_stream)); - - XASSERT(stm); + std::unique_ptr<cubeb_stream, decltype(&wasapi_stream_destroy)> stm(new cubeb_stream(), wasapi_stream_destroy); stm->context = context; stm->data_callback = data_callback; stm->state_callback = state_callback; stm->user_ptr = user_ptr; - stm->draining = false; + + if (stm->output_stream_params.prefs & CUBEB_STREAM_PREF_VOICE || + stm->input_stream_params.prefs & CUBEB_STREAM_PREF_VOICE) { + stm->role = eCommunications; + } else { + stm->role = eConsole; + } + if (input_stream_params) { stm->input_stream_params = *input_stream_params; - stm->input_device = input_device; + stm->input_device = utf8_to_wstr(reinterpret_cast<char const *>(input_device)); } if (output_stream_params) { stm->output_stream_params = *output_stream_params; - stm->output_device = output_device; + stm->output_device = utf8_to_wstr(reinterpret_cast<char const *>(output_device)); } - stm->latency = latency_frames; - stm->volume = 1.0; + switch (output_stream_params ? output_stream_params->format : input_stream_params->format) { + case CUBEB_SAMPLE_S16NE: + stm->bytes_per_sample = sizeof(short); + stm->waveformatextensible_sub_format = KSDATAFORMAT_SUBTYPE_PCM; + stm->linear_input_buffer.reset(new auto_array_wrapper_impl<short>); + break; + case CUBEB_SAMPLE_FLOAT32NE: + stm->bytes_per_sample = sizeof(float); + stm->waveformatextensible_sub_format = KSDATAFORMAT_SUBTYPE_IEEE_FLOAT; + stm->linear_input_buffer.reset(new auto_array_wrapper_impl<float>); + break; + default: + return CUBEB_ERROR_INVALID_FORMAT; + } - // Placement new to call ctor. - new (&stm->stream_reset_lock) owned_critical_section(); + stm->latency = latency_frames; stm->reconfigure_event = CreateEvent(NULL, 0, 0, NULL); if (!stm->reconfigure_event) { - LOG("Can't create the reconfigure event, error: %x", GetLastError()); - wasapi_stream_destroy(stm); + LOG("Can't create the reconfigure event, error: %lx", GetLastError()); return CUBEB_ERROR; } /* Unconditionally create the two events so that the wait logic is simpler. */ stm->refill_event = CreateEvent(NULL, 0, 0, NULL); if (!stm->refill_event) { - LOG("Can't create the refill event, error: %x", GetLastError()); - wasapi_stream_destroy(stm); + LOG("Can't create the refill event, error: %lx", GetLastError()); return CUBEB_ERROR; } stm->input_available_event = CreateEvent(NULL, 0, 0, NULL); if (!stm->input_available_event) { - LOG("Can't create the input available event , error: %x", GetLastError()); - wasapi_stream_destroy(stm); + LOG("Can't create the input available event , error: %lx", GetLastError()); return CUBEB_ERROR; } - { /* Locking here is not strictly necessary, because we don't have a notification client that can reset the stream yet, but it lets us assert that the lock is held in the function. */ auto_lock lock(stm->stream_reset_lock); - rv = setup_wasapi_stream(stm); + rv = setup_wasapi_stream(stm.get()); } if (rv != CUBEB_OK) { - wasapi_stream_destroy(stm); return rv; } - hr = register_notification_client(stm); - if (FAILED(hr)) { - /* this is not fatal, we can still play audio, but we won't be able - to keep using the default audio endpoint if it changes. */ - LOG("failed to register notification client, %x", hr); + if (!((input_stream_params ? + (input_stream_params->prefs & CUBEB_STREAM_PREF_DISABLE_DEVICE_SWITCHING) : 0) || + (output_stream_params ? + (output_stream_params->prefs & CUBEB_STREAM_PREF_DISABLE_DEVICE_SWITCHING) : 0))) { + HRESULT hr = register_notification_client(stm.get()); + if (FAILED(hr)) { + /* this is not fatal, we can still play audio, but we won't be able + to keep using the default audio endpoint if it changes. */ + LOG("failed to register notification client, %lx", hr); + } } - *stream = stm; + *stream = stm.release(); + LOG("Stream init succesfull (%p)", *stream); return CUBEB_OK; } @@ -1786,61 +2343,55 @@ void close_wasapi_stream(cubeb_stream * stm) stm->stream_reset_lock.assert_current_thread_owns(); - SafeRelease(stm->output_client); - stm->output_client = NULL; - SafeRelease(stm->input_client); - stm->input_client = NULL; - - SafeRelease(stm->render_client); - stm->render_client = NULL; + stm->output_client = nullptr; + stm->render_client = nullptr; - SafeRelease(stm->capture_client); - stm->capture_client = NULL; + stm->input_client = nullptr; + stm->capture_client = nullptr; - SafeRelease(stm->audio_stream_volume); - stm->audio_stream_volume = NULL; + stm->audio_stream_volume = nullptr; - SafeRelease(stm->audio_clock); - stm->audio_clock = NULL; + stm->audio_clock = nullptr; stm->total_frames_written += static_cast<UINT64>(round(stm->frames_written * stream_to_mix_samplerate_ratio(stm->output_stream_params, stm->output_mix_params))); stm->frames_written = 0; - if (stm->resampler) { - cubeb_resampler_destroy(stm->resampler); - stm->resampler = NULL; - } - - free(stm->mix_buffer); - stm->mix_buffer = NULL; + stm->resampler.reset(); + stm->output_mixer.reset(); + stm->input_mixer.reset(); + stm->mix_buffer.clear(); } void wasapi_stream_destroy(cubeb_stream * stm) { XASSERT(stm); + LOG("Stream destroy (%p)", stm); - // Only free stm->emergency_bailout if we could not join the thread. - // If we could not join the thread, stm->emergency_bailout is true + // Only free stm->emergency_bailout if we could join the thread. + // If we could not join the thread, stm->emergency_bailout is true // and is still alive until the thread wakes up and exits cleanly. if (stop_and_join_render_thread(stm)) { delete stm->emergency_bailout.load(); stm->emergency_bailout = nullptr; } - unregister_notification_client(stm); + if (stm->notification_client) { + unregister_notification_client(stm); + } - SafeRelease(stm->reconfigure_event); - SafeRelease(stm->refill_event); - SafeRelease(stm->input_available_event); + CloseHandle(stm->reconfigure_event); + CloseHandle(stm->refill_event); + CloseHandle(stm->input_available_event); + + // The variables intialized in wasapi_stream_init, + // must be destroyed in wasapi_stream_destroy. + stm->linear_input_buffer.reset(); { auto_lock lock(stm->stream_reset_lock); close_wasapi_stream(stm); } - // Need to call dtor to free the resource in owned_critical_section. - stm->stream_reset_lock.~owned_critical_section(); - - free(stm); + delete stm; } enum StreamDirection { @@ -1860,7 +2411,7 @@ int stream_start_one_side(cubeb_stream * stm, StreamDirection dir) BOOL ok = ResetEvent(stm->reconfigure_event); if (!ok) { - LOG("resetting reconfig event failed for %s stream: %x", + LOG("resetting reconfig event failed for %s stream: %lx", dir == OUTPUT ? "output" : "input", GetLastError()); } @@ -1873,12 +2424,12 @@ int stream_start_one_side(cubeb_stream * stm, StreamDirection dir) HRESULT hr2 = dir == OUTPUT ? stm->output_client->Start() : stm->input_client->Start(); if (FAILED(hr2)) { - LOG("could not start the %s stream after reconfig: %x", + LOG("could not start the %s stream after reconfig: %lx", dir == OUTPUT ? "output" : "input", hr); return CUBEB_ERROR; } } else if (FAILED(hr)) { - LOG("could not start the %s stream: %x.", + LOG("could not start the %s stream: %lx.", dir == OUTPUT ? "output" : "input", hr); return CUBEB_ERROR; } @@ -1911,16 +2462,31 @@ int wasapi_stream_start(cubeb_stream * stm) stm->shutdown_event = CreateEvent(NULL, 0, 0, NULL); if (!stm->shutdown_event) { - LOG("Can't create the shutdown event, error: %x", GetLastError()); + LOG("Can't create the shutdown event, error: %lx", GetLastError()); return CUBEB_ERROR; } + stm->thread_ready_event = CreateEvent(NULL, 0, 0, NULL); + if (!stm->thread_ready_event) { + LOG("Can't create the thread_ready event, error: %lx", GetLastError()); + return CUBEB_ERROR; + } + + cubeb_async_log_reset_threads(); stm->thread = (HANDLE) _beginthreadex(NULL, 512 * 1024, wasapi_stream_render_loop, stm, STACK_SIZE_PARAM_IS_A_RESERVATION, NULL); if (stm->thread == NULL) { LOG("could not create WASAPI render thread."); return CUBEB_ERROR; } + // Wait for wasapi_stream_render_loop to signal that emergency_bailout has + // been read, avoiding a bailout situation where we could free `stm` + // before wasapi_stream_render_loop had a chance to run. + HRESULT hr = WaitForSingleObject(stm->thread_ready_event, INFINITE); + XASSERT(hr == WAIT_OBJECT_0); + CloseHandle(stm->thread_ready_event); + stm->thread_ready_event = 0; + stm->state_callback(stm, stm->user_ptr, CUBEB_STATE_STARTED); return CUBEB_OK; @@ -1950,21 +2516,32 @@ int wasapi_stream_stop(cubeb_stream * stm) } } - stm->state_callback(stm, stm->user_ptr, CUBEB_STATE_STOPPED); } if (stop_and_join_render_thread(stm)) { - // This is null if we've given the pointer to the other thread - if (stm->emergency_bailout.load()) { - delete stm->emergency_bailout.load(); - stm->emergency_bailout = nullptr; - } + delete stm->emergency_bailout.load(); + stm->emergency_bailout = nullptr; + } else { + // If we could not join the thread, put the stream in error. + stm->state_callback(stm, stm->user_ptr, CUBEB_STATE_ERROR); + return CUBEB_ERROR; } return CUBEB_OK; } +int wasapi_stream_reset_default_device(cubeb_stream * stm) +{ + XASSERT(stm && stm->reconfigure_event); + BOOL ok = SetEvent(stm->reconfigure_event); + if (!ok) { + LOG("SetEvent on reconfigure_event failed: %lx", GetLastError()); + return CUBEB_ERROR; + } + return CUBEB_OK; +} + int wasapi_stream_get_position(cubeb_stream * stm, uint64_t * position) { XASSERT(stm && position); @@ -2037,253 +2614,329 @@ int wasapi_stream_set_volume(cubeb_stream * stm, float volume) return CUBEB_OK; } -static char * +static char const * wstr_to_utf8(LPCWSTR str) { - char * ret = NULL; - int size; - - size = ::WideCharToMultiByte(CP_UTF8, 0, str, -1, ret, 0, NULL, NULL); - if (size > 0) { - ret = static_cast<char *>(malloc(size)); - ::WideCharToMultiByte(CP_UTF8, 0, str, -1, ret, size, NULL, NULL); + int size = ::WideCharToMultiByte(CP_UTF8, 0, str, -1, nullptr, 0, NULL, NULL); + if (size <= 0) { + return nullptr; } + char * ret = static_cast<char *>(malloc(size)); + ::WideCharToMultiByte(CP_UTF8, 0, str, -1, ret, size, NULL, NULL); return ret; } -static std::unique_ptr<const wchar_t[]> -utf8_to_wstr(char* str) +static std::unique_ptr<wchar_t const []> +utf8_to_wstr(char const * str) { - std::unique_ptr<wchar_t[]> ret; - int size; - - size = ::MultiByteToWideChar(CP_UTF8, 0, str, -1, nullptr, 0); - if (size > 0) { - ret.reset(new wchar_t[size]); - ::MultiByteToWideChar(CP_UTF8, 0, str, -1, ret.get(), size); + int size = ::MultiByteToWideChar(CP_UTF8, 0, str, -1, nullptr, 0); + if (size <= 0) { + return nullptr; } - return std::move(ret); + std::unique_ptr<wchar_t []> ret(new wchar_t[size]); + ::MultiByteToWideChar(CP_UTF8, 0, str, -1, ret.get(), size); + return ret; } -static IMMDevice * +static com_ptr<IMMDevice> wasapi_get_device_node(IMMDeviceEnumerator * enumerator, IMMDevice * dev) { - IMMDevice * ret = NULL; - IDeviceTopology * devtopo = NULL; - IConnector * connector = NULL; - - if (SUCCEEDED(dev->Activate(__uuidof(IDeviceTopology), CLSCTX_ALL, NULL, (void**)&devtopo)) && - SUCCEEDED(devtopo->GetConnector(0, &connector))) { - LPWSTR filterid; - if (SUCCEEDED(connector->GetDeviceIdConnectedTo(&filterid))) { - if (FAILED(enumerator->GetDevice(filterid, &ret))) + com_ptr<IMMDevice> ret; + com_ptr<IDeviceTopology> devtopo; + com_ptr<IConnector> connector; + + if (SUCCEEDED(dev->Activate(__uuidof(IDeviceTopology), CLSCTX_ALL, NULL, devtopo.receive_vpp())) && + SUCCEEDED(devtopo->GetConnector(0, connector.receive()))) { + wchar_t * tmp = nullptr; + if (SUCCEEDED(connector->GetDeviceIdConnectedTo(&tmp))) { + com_heap_ptr<wchar_t> filterid(tmp); + if (FAILED(enumerator->GetDevice(filterid.get(), ret.receive()))) ret = NULL; - CoTaskMemFree(filterid); } } - SafeRelease(connector); - SafeRelease(devtopo); return ret; } static BOOL wasapi_is_default_device(EDataFlow flow, ERole role, LPCWSTR device_id, - IMMDeviceEnumerator * enumerator) + IMMDeviceEnumerator * enumerator) { BOOL ret = FALSE; - IMMDevice * dev; + com_ptr<IMMDevice> dev; HRESULT hr; - hr = enumerator->GetDefaultAudioEndpoint(flow, role, &dev); + hr = enumerator->GetDefaultAudioEndpoint(flow, role, dev.receive()); if (SUCCEEDED(hr)) { - LPWSTR defdevid = NULL; - if (SUCCEEDED(dev->GetId(&defdevid))) - ret = (wcscmp(defdevid, device_id) == 0); - if (defdevid != NULL) - CoTaskMemFree(defdevid); - SafeRelease(dev); + wchar_t * tmp = nullptr; + if (SUCCEEDED(dev->GetId(&tmp))) { + com_heap_ptr<wchar_t> defdevid(tmp); + ret = (wcscmp(defdevid.get(), device_id) == 0); + } } return ret; } -static cubeb_device_info * -wasapi_create_device(IMMDeviceEnumerator * enumerator, IMMDevice * dev) +int +wasapi_create_device(cubeb * ctx, cubeb_device_info& ret, IMMDeviceEnumerator * enumerator, IMMDevice * dev) { - IMMEndpoint * endpoint = NULL; - IMMDevice * devnode = NULL; - IAudioClient * client = NULL; - cubeb_device_info * ret = NULL; + com_ptr<IMMEndpoint> endpoint; + com_ptr<IMMDevice> devnode; + com_ptr<IAudioClient> client; EDataFlow flow; - LPWSTR device_id = NULL; DWORD state = DEVICE_STATE_NOTPRESENT; - IPropertyStore * propstore = NULL; - PROPVARIANT propvar; + com_ptr<IPropertyStore> propstore; REFERENCE_TIME def_period, min_period; HRESULT hr; - PropVariantInit(&propvar); + struct prop_variant : public PROPVARIANT { + prop_variant() { PropVariantInit(this); } + ~prop_variant() { PropVariantClear(this); } + prop_variant(prop_variant const &) = delete; + prop_variant & operator=(prop_variant const &) = delete; + }; - hr = dev->QueryInterface(IID_PPV_ARGS(&endpoint)); - if (FAILED(hr)) goto done; + hr = dev->QueryInterface(IID_PPV_ARGS(endpoint.receive())); + if (FAILED(hr)) return CUBEB_ERROR; hr = endpoint->GetDataFlow(&flow); - if (FAILED(hr)) goto done; + if (FAILED(hr)) return CUBEB_ERROR; - hr = dev->GetId(&device_id); - if (FAILED(hr)) goto done; + wchar_t * tmp = nullptr; + hr = dev->GetId(&tmp); + if (FAILED(hr)) return CUBEB_ERROR; + com_heap_ptr<wchar_t> device_id(tmp); - hr = dev->OpenPropertyStore(STGM_READ, &propstore); - if (FAILED(hr)) goto done; + char const * device_id_intern = intern_device_id(ctx, device_id.get()); + if (!device_id_intern) { + return CUBEB_ERROR; + } - hr = dev->GetState(&state); - if (FAILED(hr)) goto done; + hr = dev->OpenPropertyStore(STGM_READ, propstore.receive()); + if (FAILED(hr)) return CUBEB_ERROR; - ret = (cubeb_device_info *)calloc(1, sizeof(cubeb_device_info)); + hr = dev->GetState(&state); + if (FAILED(hr)) return CUBEB_ERROR; - ret->devid = ret->device_id = wstr_to_utf8(device_id); - hr = propstore->GetValue(PKEY_Device_FriendlyName, &propvar); + ret.device_id = device_id_intern; + ret.devid = reinterpret_cast<cubeb_devid>(ret.device_id); + prop_variant namevar; + hr = propstore->GetValue(PKEY_Device_FriendlyName, &namevar); if (SUCCEEDED(hr)) - ret->friendly_name = wstr_to_utf8(propvar.pwszVal); + ret.friendly_name = wstr_to_utf8(namevar.pwszVal); devnode = wasapi_get_device_node(enumerator, dev); - if (devnode != NULL) { - IPropertyStore * ps = NULL; - hr = devnode->OpenPropertyStore(STGM_READ, &ps); - if (FAILED(hr)) goto done; + if (devnode) { + com_ptr<IPropertyStore> ps; + hr = devnode->OpenPropertyStore(STGM_READ, ps.receive()); + if (FAILED(hr)) return CUBEB_ERROR; - PropVariantClear(&propvar); - hr = ps->GetValue(PKEY_Device_InstanceId, &propvar); + prop_variant instancevar; + hr = ps->GetValue(PKEY_Device_InstanceId, &instancevar); if (SUCCEEDED(hr)) { - ret->group_id = wstr_to_utf8(propvar.pwszVal); + ret.group_id = wstr_to_utf8(instancevar.pwszVal); } - SafeRelease(ps); } - ret->preferred = CUBEB_DEVICE_PREF_NONE; - if (wasapi_is_default_device(flow, eConsole, device_id, enumerator)) - ret->preferred = (cubeb_device_pref)(ret->preferred | CUBEB_DEVICE_PREF_MULTIMEDIA); - if (wasapi_is_default_device(flow, eCommunications, device_id, enumerator)) - ret->preferred = (cubeb_device_pref)(ret->preferred | CUBEB_DEVICE_PREF_VOICE); - if (wasapi_is_default_device(flow, eConsole, device_id, enumerator)) - ret->preferred = (cubeb_device_pref)(ret->preferred | CUBEB_DEVICE_PREF_NOTIFICATION); + ret.preferred = CUBEB_DEVICE_PREF_NONE; + if (wasapi_is_default_device(flow, eConsole, device_id.get(), enumerator)) + ret.preferred = (cubeb_device_pref)(ret.preferred | CUBEB_DEVICE_PREF_MULTIMEDIA); + if (wasapi_is_default_device(flow, eCommunications, device_id.get(), enumerator)) + ret.preferred = (cubeb_device_pref)(ret.preferred | CUBEB_DEVICE_PREF_VOICE); + if (wasapi_is_default_device(flow, eConsole, device_id.get(), enumerator)) + ret.preferred = (cubeb_device_pref)(ret.preferred | CUBEB_DEVICE_PREF_NOTIFICATION); - if (flow == eRender) ret->type = CUBEB_DEVICE_TYPE_OUTPUT; - else if (flow == eCapture) ret->type = CUBEB_DEVICE_TYPE_INPUT; + if (flow == eRender) ret.type = CUBEB_DEVICE_TYPE_OUTPUT; + else if (flow == eCapture) ret.type = CUBEB_DEVICE_TYPE_INPUT; switch (state) { case DEVICE_STATE_ACTIVE: - ret->state = CUBEB_DEVICE_STATE_ENABLED; + ret.state = CUBEB_DEVICE_STATE_ENABLED; break; case DEVICE_STATE_UNPLUGGED: - ret->state = CUBEB_DEVICE_STATE_UNPLUGGED; + ret.state = CUBEB_DEVICE_STATE_UNPLUGGED; break; default: - ret->state = CUBEB_DEVICE_STATE_DISABLED; + ret.state = CUBEB_DEVICE_STATE_DISABLED; break; }; - ret->format = CUBEB_DEVICE_FMT_F32NE; /* cubeb only supports 32bit float at the moment */ - ret->default_format = CUBEB_DEVICE_FMT_F32NE; - PropVariantClear(&propvar); - hr = propstore->GetValue(PKEY_AudioEngine_DeviceFormat, &propvar); - if (SUCCEEDED(hr) && propvar.vt == VT_BLOB) { - if (propvar.blob.cbSize == sizeof(PCMWAVEFORMAT)) { - const PCMWAVEFORMAT * pcm = reinterpret_cast<const PCMWAVEFORMAT *>(propvar.blob.pBlobData); + ret.format = static_cast<cubeb_device_fmt>(CUBEB_DEVICE_FMT_F32NE | CUBEB_DEVICE_FMT_S16NE); + ret.default_format = CUBEB_DEVICE_FMT_F32NE; + prop_variant fmtvar; + hr = propstore->GetValue(PKEY_AudioEngine_DeviceFormat, &fmtvar); + if (SUCCEEDED(hr) && fmtvar.vt == VT_BLOB) { + if (fmtvar.blob.cbSize == sizeof(PCMWAVEFORMAT)) { + const PCMWAVEFORMAT * pcm = reinterpret_cast<const PCMWAVEFORMAT *>(fmtvar.blob.pBlobData); - ret->max_rate = ret->min_rate = ret->default_rate = pcm->wf.nSamplesPerSec; - ret->max_channels = pcm->wf.nChannels; - } else if (propvar.blob.cbSize >= sizeof(WAVEFORMATEX)) { - WAVEFORMATEX* wfx = reinterpret_cast<WAVEFORMATEX*>(propvar.blob.pBlobData); + ret.max_rate = ret.min_rate = ret.default_rate = pcm->wf.nSamplesPerSec; + ret.max_channels = pcm->wf.nChannels; + } else if (fmtvar.blob.cbSize >= sizeof(WAVEFORMATEX)) { + WAVEFORMATEX* wfx = reinterpret_cast<WAVEFORMATEX*>(fmtvar.blob.pBlobData); - if (propvar.blob.cbSize >= sizeof(WAVEFORMATEX) + wfx->cbSize || + if (fmtvar.blob.cbSize >= sizeof(WAVEFORMATEX) + wfx->cbSize || wfx->wFormatTag == WAVE_FORMAT_PCM) { - ret->max_rate = ret->min_rate = ret->default_rate = wfx->nSamplesPerSec; - ret->max_channels = wfx->nChannels; + ret.max_rate = ret.min_rate = ret.default_rate = wfx->nSamplesPerSec; + ret.max_channels = wfx->nChannels; } } } - if (SUCCEEDED(dev->Activate(__uuidof(IAudioClient), CLSCTX_INPROC_SERVER, NULL, (void**)&client)) && + if (SUCCEEDED(dev->Activate(__uuidof(IAudioClient), CLSCTX_INPROC_SERVER, NULL, client.receive_vpp())) && SUCCEEDED(client->GetDevicePeriod(&def_period, &min_period))) { - ret->latency_lo = hns_to_frames(ret->default_rate, min_period); - ret->latency_hi = hns_to_frames(ret->default_rate, def_period); + ret.latency_lo = hns_to_frames(ret.default_rate, min_period); + ret.latency_hi = hns_to_frames(ret.default_rate, def_period); } else { - ret->latency_lo = 0; - ret->latency_hi = 0; - } - SafeRelease(client); - -done: - SafeRelease(devnode); - SafeRelease(endpoint); - SafeRelease(propstore); - if (device_id != NULL) - CoTaskMemFree(device_id); - PropVariantClear(&propvar); - return ret; + ret.latency_lo = 0; + ret.latency_hi = 0; + } + + return CUBEB_OK; } static int wasapi_enumerate_devices(cubeb * context, cubeb_device_type type, - cubeb_device_collection ** out) + cubeb_device_collection * out) { - auto_com com; - IMMDeviceEnumerator * enumerator; - IMMDeviceCollection * collection; - IMMDevice * dev; - cubeb_device_info * cur; + com_ptr<IMMDeviceEnumerator> enumerator; + com_ptr<IMMDeviceCollection> collection; HRESULT hr; UINT cc, i; EDataFlow flow; - *out = NULL; - - if (!com.ok()) - return CUBEB_ERROR; - hr = CoCreateInstance(__uuidof(MMDeviceEnumerator), NULL, - CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&enumerator)); + CLSCTX_INPROC_SERVER, IID_PPV_ARGS(enumerator.receive())); if (FAILED(hr)) { - LOG("Could not get device enumerator: %x", hr); + LOG("Could not get device enumerator: %lx", hr); return CUBEB_ERROR; } if (type == CUBEB_DEVICE_TYPE_OUTPUT) flow = eRender; else if (type == CUBEB_DEVICE_TYPE_INPUT) flow = eCapture; - else if (type & (CUBEB_DEVICE_TYPE_INPUT | CUBEB_DEVICE_TYPE_INPUT)) flow = eAll; + else if (type & (CUBEB_DEVICE_TYPE_INPUT | CUBEB_DEVICE_TYPE_OUTPUT)) flow = eAll; else return CUBEB_ERROR; - hr = enumerator->EnumAudioEndpoints(flow, DEVICE_STATEMASK_ALL, &collection); + hr = enumerator->EnumAudioEndpoints(flow, DEVICE_STATEMASK_ALL, collection.receive()); if (FAILED(hr)) { - LOG("Could not enumerate audio endpoints: %x", hr); + LOG("Could not enumerate audio endpoints: %lx", hr); return CUBEB_ERROR; } hr = collection->GetCount(&cc); if (FAILED(hr)) { - LOG("IMMDeviceCollection::GetCount() failed: %x", hr); + LOG("IMMDeviceCollection::GetCount() failed: %lx", hr); return CUBEB_ERROR; } - *out = (cubeb_device_collection *) malloc(sizeof(cubeb_device_collection) + - sizeof(cubeb_device_info*) * (cc > 0 ? cc - 1 : 0)); - if (!*out) { + cubeb_device_info * devices = new cubeb_device_info[cc]; + if (!devices) return CUBEB_ERROR; - } - (*out)->count = 0; + + PodZero(devices, cc); + out->count = 0; for (i = 0; i < cc; i++) { - hr = collection->Item(i, &dev); + com_ptr<IMMDevice> dev; + hr = collection->Item(i, dev.receive()); + if (FAILED(hr)) { + LOG("IMMDeviceCollection::Item(%u) failed: %lx", i-1, hr); + continue; + } + if (wasapi_create_device(context, devices[out->count], + enumerator.get(), dev.get()) == CUBEB_OK) { + out->count += 1; + } + } + + out->device = devices; + return CUBEB_OK; +} + +static int +wasapi_device_collection_destroy(cubeb * /*ctx*/, cubeb_device_collection * collection) +{ + XASSERT(collection); + + for (size_t n = 0; n < collection->count; n++) { + cubeb_device_info& dev = collection->device[n]; + delete [] dev.friendly_name; + delete [] dev.group_id; + } + + delete [] collection->device; + return CUBEB_OK; +} + +static int +wasapi_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; + } + + if (collection_changed_callback) { + // Make sure it has been unregistered first. + XASSERT(((devtype & CUBEB_DEVICE_TYPE_INPUT) && + !context->input_collection_changed_callback) || + ((devtype & CUBEB_DEVICE_TYPE_OUTPUT) && + !context->output_collection_changed_callback)); + + // Stop the notification client. Notifications arrive on + // a separate thread. We stop them here to avoid + // synchronization issues during the update. + if (context->device_collection_enumerator.get()) { + HRESULT hr = unregister_collection_notification_client(context); + if (FAILED(hr)) { + return CUBEB_ERROR; + } + } + + if (devtype & 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) { + context->output_collection_changed_callback = collection_changed_callback; + context->output_collection_changed_user_ptr = user_ptr; + } + + HRESULT hr = register_collection_notification_client(context); + if (FAILED(hr)) { + return CUBEB_ERROR; + } + } else { + if (!context->device_collection_enumerator.get()) { + // Already unregistered, ignore it. + return CUBEB_OK; + } + + HRESULT hr = unregister_collection_notification_client(context); if (FAILED(hr)) { - LOG("IMMDeviceCollection::Item(%u) failed: %x", i-1, hr); - } else if ((cur = wasapi_create_device(enumerator, dev)) != NULL) { - (*out)->device[(*out)->count++] = cur; + return CUBEB_ERROR; + } + if (devtype & CUBEB_DEVICE_TYPE_INPUT) { + context->input_collection_changed_callback = nullptr; + context->input_collection_changed_user_ptr = nullptr; + } + if (devtype & CUBEB_DEVICE_TYPE_OUTPUT) { + context->output_collection_changed_callback = nullptr; + context->output_collection_changed_user_ptr = nullptr; + } + + // If after the updates we still have registered + // callbacks restart the notification client. + if (context->input_collection_changed_callback || + context->output_collection_changed_callback) { + hr = register_collection_notification_client(context); + if (FAILED(hr)) { + return CUBEB_ERROR; + } } } - SafeRelease(collection); - SafeRelease(enumerator); return CUBEB_OK; } @@ -2294,18 +2947,19 @@ cubeb_ops const wasapi_ops = { /*.get_min_latency =*/ wasapi_get_min_latency, /*.get_preferred_sample_rate =*/ wasapi_get_preferred_sample_rate, /*.enumerate_devices =*/ wasapi_enumerate_devices, + /*.device_collection_destroy =*/ wasapi_device_collection_destroy, /*.destroy =*/ wasapi_destroy, /*.stream_init =*/ wasapi_stream_init, /*.stream_destroy =*/ wasapi_stream_destroy, /*.stream_start =*/ wasapi_stream_start, /*.stream_stop =*/ wasapi_stream_stop, + /*.stream_reset_default_device =*/ wasapi_stream_reset_default_device, /*.stream_get_position =*/ wasapi_stream_get_position, /*.stream_get_latency =*/ wasapi_stream_get_latency, /*.stream_set_volume =*/ wasapi_stream_set_volume, - /*.stream_set_panning =*/ NULL, /*.stream_get_current_device =*/ NULL, /*.stream_device_destroy =*/ NULL, /*.stream_register_device_changed_callback =*/ NULL, - /*.register_device_collection_changed =*/ NULL + /*.register_device_collection_changed =*/ wasapi_register_device_collection_changed, }; } // namespace anonymous diff --git a/media/libcubeb/src/cubeb_winmm.c b/media/libcubeb/src/cubeb_winmm.c index 585d11e89d..e064ca0798 100644 --- a/media/libcubeb/src/cubeb_winmm.c +++ b/media/libcubeb/src/cubeb_winmm.c @@ -4,7 +4,6 @@ * This program is made available under an ISC-style license. See the * accompanying file LICENSE for details. */ -#define __MSVCRT_VERSION__ 0x0700 #undef WINVER #define WINVER 0x0501 #undef WIN32_LEAN_AND_MEAN @@ -93,11 +92,13 @@ struct cubeb { }; struct cubeb_stream { + /* Note: Must match cubeb_stream layout in cubeb.c. */ cubeb * context; + void * user_ptr; + /**/ cubeb_stream_params params; cubeb_data_callback data_callback; cubeb_state_callback state_callback; - void * user_ptr; WAVEHDR buffers[NBUFS]; size_t buffer_size; int next_buffer; @@ -402,6 +403,7 @@ winmm_stream_init(cubeb * context, cubeb_stream ** stream, char const * stream_n XASSERT(context); XASSERT(stream); + XASSERT(output_stream_params); if (input_stream_params) { /* Capture support not yet implemented. */ @@ -413,6 +415,11 @@ winmm_stream_init(cubeb * context, cubeb_stream ** stream, char const * stream_n return CUBEB_ERROR_DEVICE_UNAVAILABLE; } + if (output_stream_params->prefs & CUBEB_STREAM_PREF_LOOPBACK) { + /* Loopback is not supported */ + return CUBEB_ERROR_NOT_SUPPORTED; + } + *stream = NULL; memset(&wfx, 0, sizeof(wfx)); @@ -512,7 +519,6 @@ winmm_stream_init(cubeb * context, cubeb_stream ** stream, char const * stream_n return CUBEB_ERROR; } - for (i = 0; i < NBUFS; ++i) { WAVEHDR * hdr = &stm->buffers[i]; @@ -769,7 +775,6 @@ winmm_calculate_device_rate(cubeb_device_info * info, DWORD formats) } } - #define MM_S16_MASK (WAVE_FORMAT_1M16 | WAVE_FORMAT_1S16 | WAVE_FORMAT_2M16 | WAVE_FORMAT_2S16 | WAVE_FORMAT_4M16 | \ WAVE_FORMAT_4S16 | WAVE_FORMAT_48M16 | WAVE_FORMAT_48S16 | WAVE_FORMAT_96M16 | WAVE_FORMAT_96S16) static int @@ -803,11 +808,11 @@ winmm_query_supported_formats(UINT devid, DWORD formats, static char * guid_to_cstr(LPGUID guid) { - char * ret = malloc(sizeof(char) * 40); + char * ret = malloc(40); if (!ret) { return NULL; } - _snprintf_s(ret, sizeof(char) * 40, _TRUNCATE, + _snprintf_s(ret, 40, _TRUNCATE, "{%08X-%04X-%04X-%02X%02X-%02X%02X%02X%02X%02X%02X}", guid->Data1, guid->Data2, guid->Data3, guid->Data4[0], guid->Data4[1], guid->Data4[2], guid->Data4[3], @@ -821,12 +826,12 @@ winmm_query_preferred_out_device(UINT devid) DWORD mmpref = WAVE_MAPPER, compref = WAVE_MAPPER, status; cubeb_device_pref ret = CUBEB_DEVICE_PREF_NONE; - if (waveOutMessage((HWAVEOUT)(size_t)WAVE_MAPPER, DRVM_MAPPER_PREFERRED_GET, + if (waveOutMessage((HWAVEOUT) WAVE_MAPPER, DRVM_MAPPER_PREFERRED_GET, (DWORD_PTR)&mmpref, (DWORD_PTR)&status) == MMSYSERR_NOERROR && devid == mmpref) ret |= CUBEB_DEVICE_PREF_MULTIMEDIA | CUBEB_DEVICE_PREF_NOTIFICATION; - if (waveOutMessage((HWAVEOUT)(size_t)WAVE_MAPPER, DRVM_MAPPER_CONSOLEVOICECOM_GET, + if (waveOutMessage((HWAVEOUT) WAVE_MAPPER, DRVM_MAPPER_CONSOLEVOICECOM_GET, (DWORD_PTR)&compref, (DWORD_PTR)&status) == MMSYSERR_NOERROR && devid == compref) ret |= CUBEB_DEVICE_PREF_VOICE; @@ -837,7 +842,7 @@ winmm_query_preferred_out_device(UINT devid) static char * device_id_idx(UINT devid) { - char * ret = (char *)malloc(sizeof(char)*16); + char * ret = malloc(16); if (!ret) { return NULL; } @@ -845,16 +850,11 @@ device_id_idx(UINT devid) return ret; } -static cubeb_device_info * -winmm_create_device_from_outcaps2(LPWAVEOUTCAPS2A caps, UINT devid) +static void +winmm_create_device_from_outcaps2(cubeb_device_info * ret, LPWAVEOUTCAPS2A caps, UINT devid) { - cubeb_device_info * ret; - - ret = calloc(1, sizeof(cubeb_device_info)); - if (!ret) { - return NULL; - } - ret->devid = (cubeb_devid)(size_t)devid; + XASSERT(ret); + ret->devid = (cubeb_devid) devid; ret->device_id = device_id_idx(devid); ret->friendly_name = _strdup(caps->szPname); ret->group_id = guid_to_cstr(&caps->ProductGuid); @@ -869,23 +869,16 @@ winmm_create_device_from_outcaps2(LPWAVEOUTCAPS2A caps, UINT devid) winmm_query_supported_formats(devid, caps->dwFormats, &ret->format, &ret->default_format); - /* Hardcoed latency estimates... */ + /* Hardcoded latency estimates... */ ret->latency_lo = 100 * ret->default_rate / 1000; ret->latency_hi = 200 * ret->default_rate / 1000; - - return ret; } -static cubeb_device_info * -winmm_create_device_from_outcaps(LPWAVEOUTCAPSA caps, UINT devid) +static void +winmm_create_device_from_outcaps(cubeb_device_info * ret, LPWAVEOUTCAPSA caps, UINT devid) { - cubeb_device_info * ret; - - ret = calloc(1, sizeof(cubeb_device_info)); - if (!ret) { - return NULL; - } - ret->devid = (cubeb_devid)(size_t)devid; + XASSERT(ret); + ret->devid = (cubeb_devid) devid; ret->device_id = device_id_idx(devid); ret->friendly_name = _strdup(caps->szPname); ret->group_id = NULL; @@ -900,11 +893,9 @@ winmm_create_device_from_outcaps(LPWAVEOUTCAPSA caps, UINT devid) winmm_query_supported_formats(devid, caps->dwFormats, &ret->format, &ret->default_format); - /* Hardcoed latency estimates... */ + /* Hardcoded latency estimates... */ ret->latency_lo = 100 * ret->default_rate / 1000; ret->latency_hi = 200 * ret->default_rate / 1000; - - return ret; } static cubeb_device_pref @@ -913,12 +904,12 @@ winmm_query_preferred_in_device(UINT devid) DWORD mmpref = WAVE_MAPPER, compref = WAVE_MAPPER, status; cubeb_device_pref ret = CUBEB_DEVICE_PREF_NONE; - if (waveInMessage((HWAVEIN)(size_t)WAVE_MAPPER, DRVM_MAPPER_PREFERRED_GET, + if (waveInMessage((HWAVEIN) WAVE_MAPPER, DRVM_MAPPER_PREFERRED_GET, (DWORD_PTR)&mmpref, (DWORD_PTR)&status) == MMSYSERR_NOERROR && devid == mmpref) ret |= CUBEB_DEVICE_PREF_MULTIMEDIA | CUBEB_DEVICE_PREF_NOTIFICATION; - if (waveInMessage((HWAVEIN)(size_t)WAVE_MAPPER, DRVM_MAPPER_CONSOLEVOICECOM_GET, + if (waveInMessage((HWAVEIN) WAVE_MAPPER, DRVM_MAPPER_CONSOLEVOICECOM_GET, (DWORD_PTR)&compref, (DWORD_PTR)&status) == MMSYSERR_NOERROR && devid == compref) ret |= CUBEB_DEVICE_PREF_VOICE; @@ -926,16 +917,11 @@ winmm_query_preferred_in_device(UINT devid) return ret; } -static cubeb_device_info * -winmm_create_device_from_incaps2(LPWAVEINCAPS2A caps, UINT devid) +static void +winmm_create_device_from_incaps2(cubeb_device_info * ret, LPWAVEINCAPS2A caps, UINT devid) { - cubeb_device_info * ret; - - ret = calloc(1, sizeof(cubeb_device_info)); - if (!ret) { - return NULL; - } - ret->devid = (cubeb_devid)(size_t)devid; + XASSERT(ret); + ret->devid = (cubeb_devid) devid; ret->device_id = device_id_idx(devid); ret->friendly_name = _strdup(caps->szPname); ret->group_id = guid_to_cstr(&caps->ProductGuid); @@ -950,23 +936,16 @@ winmm_create_device_from_incaps2(LPWAVEINCAPS2A caps, UINT devid) winmm_query_supported_formats(devid, caps->dwFormats, &ret->format, &ret->default_format); - /* Hardcoed latency estimates... */ + /* Hardcoded latency estimates... */ ret->latency_lo = 100 * ret->default_rate / 1000; ret->latency_hi = 200 * ret->default_rate / 1000; - - return ret; } -static cubeb_device_info * -winmm_create_device_from_incaps(LPWAVEINCAPSA caps, UINT devid) +static void +winmm_create_device_from_incaps(cubeb_device_info * ret, LPWAVEINCAPSA caps, UINT devid) { - cubeb_device_info * ret; - - ret = calloc(1, sizeof(cubeb_device_info)); - if (!ret) { - return NULL; - } - ret->devid = (cubeb_devid)(size_t)devid; + XASSERT(ret); + ret->devid = (cubeb_devid) devid; ret->device_id = device_id_idx(devid); ret->friendly_name = _strdup(caps->szPname); ret->group_id = NULL; @@ -981,29 +960,25 @@ winmm_create_device_from_incaps(LPWAVEINCAPSA caps, UINT devid) winmm_query_supported_formats(devid, caps->dwFormats, &ret->format, &ret->default_format); - /* Hardcoed latency estimates... */ + /* Hardcoded latency estimates... */ ret->latency_lo = 100 * ret->default_rate / 1000; ret->latency_hi = 200 * ret->default_rate / 1000; - - return ret; } static int winmm_enumerate_devices(cubeb * context, cubeb_device_type type, - cubeb_device_collection ** collection) + cubeb_device_collection * collection) { UINT i, incount, outcount, total; - cubeb_device_info * cur; + cubeb_device_info * devices; + cubeb_device_info * dev; outcount = waveOutGetNumDevs(); incount = waveInGetNumDevs(); total = outcount + incount; - if (total > 0) { - total -= 1; - } - *collection = malloc(sizeof(cubeb_device_collection) + - sizeof(cubeb_device_info*) * total); - (*collection)->count = 0; + + devices = calloc(total, sizeof(cubeb_device_info)); + collection->count = 0; if (type & CUBEB_DEVICE_TYPE_OUTPUT) { WAVEOUTCAPSA woc; @@ -1013,12 +988,13 @@ winmm_enumerate_devices(cubeb * context, cubeb_device_type type, ZeroMemory(&woc2, sizeof(woc2)); for (i = 0; i < outcount; i++) { - if ((waveOutGetDevCapsA(i, (LPWAVEOUTCAPSA)&woc2, sizeof(woc2)) == MMSYSERR_NOERROR && - (cur = winmm_create_device_from_outcaps2(&woc2, i)) != NULL) || - (waveOutGetDevCapsA(i, &woc, sizeof(woc)) == MMSYSERR_NOERROR && - (cur = winmm_create_device_from_outcaps(&woc, i)) != NULL) - ) { - (*collection)->device[(*collection)->count++] = cur; + dev = &devices[collection->count]; + if (waveOutGetDevCapsA(i, (LPWAVEOUTCAPSA)&woc2, sizeof(woc2)) == MMSYSERR_NOERROR) { + winmm_create_device_from_outcaps2(dev, &woc2, i); + collection->count += 1; + } else if (waveOutGetDevCapsA(i, &woc, sizeof(woc)) == MMSYSERR_NOERROR) { + winmm_create_device_from_outcaps(dev, &woc, i); + collection->count += 1; } } } @@ -1031,16 +1007,39 @@ winmm_enumerate_devices(cubeb * context, cubeb_device_type type, ZeroMemory(&wic2, sizeof(wic2)); for (i = 0; i < incount; i++) { - if ((waveInGetDevCapsA(i, (LPWAVEINCAPSA)&wic2, sizeof(wic2)) == MMSYSERR_NOERROR && - (cur = winmm_create_device_from_incaps2(&wic2, i)) != NULL) || - (waveInGetDevCapsA(i, &wic, sizeof(wic)) == MMSYSERR_NOERROR && - (cur = winmm_create_device_from_incaps(&wic, i)) != NULL) - ) { - (*collection)->device[(*collection)->count++] = cur; + dev = &devices[collection->count]; + if (waveInGetDevCapsA(i, (LPWAVEINCAPSA)&wic2, sizeof(wic2)) == MMSYSERR_NOERROR) { + winmm_create_device_from_incaps2(dev, &wic2, i); + collection->count += 1; + } else if (waveInGetDevCapsA(i, &wic, sizeof(wic)) == MMSYSERR_NOERROR) { + winmm_create_device_from_incaps(dev, &wic, i); + collection->count += 1; } } } + collection->device = devices; + + return CUBEB_OK; +} + +static int +winmm_device_collection_destroy(cubeb * ctx, + cubeb_device_collection * collection) +{ + uint32_t i; + XASSERT(collection); + + (void) ctx; + + for (i = 0; i < collection->count; i++) { + free((void *) collection->device[i].device_id); + free((void *) collection->device[i].friendly_name); + free((void *) collection->device[i].group_id); + free((void *) collection->device[i].vendor_name); + } + + free(collection->device); return CUBEB_OK; } @@ -1051,15 +1050,16 @@ static struct cubeb_ops const winmm_ops = { /*.get_min_latency=*/ winmm_get_min_latency, /*.get_preferred_sample_rate =*/ winmm_get_preferred_sample_rate, /*.enumerate_devices =*/ winmm_enumerate_devices, + /*.device_collection_destroy =*/ winmm_device_collection_destroy, /*.destroy =*/ winmm_destroy, /*.stream_init =*/ winmm_stream_init, /*.stream_destroy =*/ winmm_stream_destroy, /*.stream_start =*/ winmm_stream_start, /*.stream_stop =*/ winmm_stream_stop, + /*.stream_reset_default_device =*/ NULL, /*.stream_get_position =*/ winmm_stream_get_position, /*.stream_get_latency = */ winmm_stream_get_latency, /*.stream_set_volume =*/ winmm_stream_set_volume, - /*.stream_set_panning =*/ NULL, /*.stream_get_current_device =*/ NULL, /*.stream_device_destroy =*/ NULL, /*.stream_register_device_changed_callback=*/ NULL, diff --git a/media/libcubeb/src/moz.build b/media/libcubeb/src/moz.build index b6d07126a3..be56876c6b 100644 --- a/media/libcubeb/src/moz.build +++ b/media/libcubeb/src/moz.build @@ -10,7 +10,11 @@ Library('cubeb') SOURCES += [ 'cubeb.c', - 'cubeb_panner.cpp' + 'cubeb_log.cpp', + 'cubeb_mixer.cpp', + 'cubeb_panner.cpp', + 'cubeb_strings.c', + 'cubeb_utils.cpp' ] if CONFIG['MOZ_ALSA']: @@ -76,6 +80,7 @@ if CONFIG['OS_TARGET'] == 'WINNT': if CONFIG['OS_TARGET'] == 'Android': SOURCES += ['cubeb_opensl.c'] SOURCES += ['cubeb_resampler.cpp'] + SOURCES += ['cubeb-jni.cpp'] DEFINES['USE_OPENSL'] = True SOURCES += [ 'cubeb_audiotrack.c', @@ -85,6 +90,7 @@ if CONFIG['OS_TARGET'] == 'Android': FINAL_LIBRARY = 'gkmedias' CFLAGS += CONFIG['MOZ_ALSA_CFLAGS'] +CFLAGS += CONFIG['MOZ_JACK_CFLAGS'] CFLAGS += CONFIG['MOZ_PULSEAUDIO_CFLAGS'] # We allow warnings for third-party code that can be updated from upstream. |