diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
commit | 26a029d407be480d791972afb5975cf62c9360a6 (patch) | |
tree | f435a8308119effd964b339f76abb83a57c29483 /media/libcubeb | |
parent | Initial commit. (diff) | |
download | firefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz firefox-26a029d407be480d791972afb5975cf62c9360a6.zip |
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'media/libcubeb')
74 files changed, 28884 insertions, 0 deletions
diff --git a/media/libcubeb/0001-disable-aaudio-before-android-31.patch b/media/libcubeb/0001-disable-aaudio-before-android-31.patch new file mode 100644 index 0000000000..36519edcb2 --- /dev/null +++ b/media/libcubeb/0001-disable-aaudio-before-android-31.patch @@ -0,0 +1,41 @@ +diff --git a/src/cubeb_aaudio.cpp b/media/libcubeb/src/cubeb_aaudio.cpp +--- a/src/cubeb_aaudio.cpp ++++ b/src/cubeb_aaudio.cpp +@@ -6,16 +6,17 @@ + */ + #include "cubeb-internal.h" + #include "cubeb/cubeb.h" + #include "cubeb_android.h" + #include "cubeb_log.h" + #include "cubeb_resampler.h" + #include "cubeb_triple_buffer.h" + #include <aaudio/AAudio.h> ++#include <android/api-level.h> + #include <atomic> + #include <cassert> + #include <chrono> + #include <condition_variable> + #include <cstdint> + #include <cstring> + #include <dlfcn.h> + #include <inttypes.h> +@@ -1700,16 +1701,19 @@ const static struct cubeb_ops aaudio_ops + /*.stream_get_current_device =*/nullptr, + /*.stream_device_destroy =*/nullptr, + /*.stream_register_device_changed_callback =*/nullptr, + /*.register_device_collection_changed =*/nullptr}; + + extern "C" /*static*/ int + aaudio_init(cubeb ** context, char const * /* context_name */) + { ++ if (android_get_device_api_level() <= 30) { ++ return CUBEB_ERROR; ++ } + // load api + void * libaaudio = nullptr; + #ifndef DISABLE_LIBAAUDIO_DLOPEN + libaaudio = dlopen("libaaudio.so", RTLD_NOW); + if (!libaaudio) { + return CUBEB_ERROR; + } + diff --git a/media/libcubeb/0002-disable-crash-reporter-death-test.patch b/media/libcubeb/0002-disable-crash-reporter-death-test.patch new file mode 100644 index 0000000000..b1217ba49d --- /dev/null +++ b/media/libcubeb/0002-disable-crash-reporter-death-test.patch @@ -0,0 +1,41 @@ +diff --git a/test/test_duplex.cpp b/test/test_duplex.cpp +--- a/test/test_duplex.cpp ++++ b/test/test_duplex.cpp +@@ -13,16 +13,18 @@ + #endif + #include "cubeb/cubeb.h" + #include <atomic> + #include <math.h> + #include <memory> + #include <stdio.h> + #include <stdlib.h> + ++#include "mozilla/gtest/MozHelpers.h" ++ + // #define ENABLE_NORMAL_LOG + // #define ENABLE_VERBOSE_LOG + #include "common.h" + + #define SAMPLE_FREQUENCY 48000 + #define STREAM_FORMAT CUBEB_SAMPLE_FLOAT32LE + #define INPUT_CHANNELS 1 + #define INPUT_LAYOUT CUBEB_LAYOUT_MONO +@@ -201,16 +203,18 @@ TEST(cubeb, duplex_collection_change) + ASSERT_EQ(r, CUBEB_OK); + } + + TEST(cubeb, duplex_collection_change_no_unregister) + { + cubeb * ctx; + int r; + ++ mozilla::gtest::DisableCrashReporter(); ++ + r = common_init(&ctx, "Cubeb duplex example with collection change"); + ASSERT_EQ(r, CUBEB_OK) << "Error initializing cubeb library"; + + /* This test needs an available input device, skip it if this host does not + * have one. */ + if (!can_run_audio_input_test(ctx)) { + cubeb_destroy(ctx); + return; diff --git a/media/libcubeb/AUTHORS b/media/libcubeb/AUTHORS new file mode 100644 index 0000000000..f0f9595227 --- /dev/null +++ b/media/libcubeb/AUTHORS @@ -0,0 +1,16 @@ +Matthew Gregan <kinetik@flim.org> +Alexandre Ratchov <alex@caoua.org> +Michael Wu <mwu@mozilla.com> +Paul Adenot <paul@paul.cx> +David Richards <drichards@mozilla.com> +Sebastien Alaiwan <sebastien.alaiwan@gmail.com> +KO Myung-Hun <komh@chollian.net> +Haakon Sporsheim <haakon.sporsheim@telenordigital.com> +Alex Chronopoulos <achronop@gmail.com> +Jan Beich <jbeich@FreeBSD.org> +Vito Caputo <vito.caputo@coreos.com> +Landry Breuil <landry@openbsd.org> +Jacek Caban <jacek@codeweavers.com> +Paul Hancock <Paul.Hancock.17041993@live.com> +Ted Mielczarek <ted@mielczarek.org> +Chun-Min Chang <chun.m.chang@gmail.com> diff --git a/media/libcubeb/LICENSE b/media/libcubeb/LICENSE new file mode 100644 index 0000000000..fffc9dc405 --- /dev/null +++ b/media/libcubeb/LICENSE @@ -0,0 +1,13 @@ +Copyright © 2011 Mozilla Foundation + +Permission to use, copy, modify, and distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/media/libcubeb/README.md b/media/libcubeb/README.md new file mode 100644 index 0000000000..e4e1658824 --- /dev/null +++ b/media/libcubeb/README.md @@ -0,0 +1,7 @@ +[![Build Status](https://github.com/mozilla/cubeb/actions/workflows/build.yml/badge.svg)](https://github.com/mozilla/cubeb/actions/workflows/build.yml) + +See INSTALL.md for build instructions. + +See [Backend Support](https://github.com/mozilla/cubeb/wiki/Backend-Support) in the wiki for the support level of each backend. + +Licensed under an ISC-style license. See LICENSE for details. diff --git a/media/libcubeb/gtest/moz.build b/media/libcubeb/gtest/moz.build new file mode 100644 index 0000000000..c4ee072488 --- /dev/null +++ b/media/libcubeb/gtest/moz.build @@ -0,0 +1,78 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +SOURCES += [ + '../test/test_audio.cpp', + '../test/test_callback_ret.cpp', + '../test/test_devices.cpp', + '../test/test_duplex.cpp', + '../test/test_latency.cpp', + '../test/test_logging.cpp', + '../test/test_overload_callback.cpp', + '../test/test_record.cpp', + '../test/test_resampler.cpp', + '../test/test_ring_array.cpp', + '../test/test_ring_buffer.cpp', + '../test/test_sanity.cpp', + '../test/test_tone.cpp', + '../test/test_triple_buffer.cpp', + '../test/test_utils.cpp' +] + +# Loopback stream is only implemented in the WASAPI backend. It fails in debug. +if CONFIG['OS_ARCH'] == 'WINNT' and not CONFIG['MOZ_DEBUG']: + SOURCES += [ '../test/test_loopback.cpp' ] + +# https://bugzilla.mozilla.org/show_bug.cgi?id=1864888 +if CONFIG['OS_ARCH'] != 'Linux': + SOURCES += [ '../test/test_device_changed_callback.cpp' ] + +LOCAL_INCLUDES += [ + '../include', + '../src' +] + +USE_LIBS += [ + 'cubeb', + 'speex' +] + +if CONFIG['OS_ARCH'] == 'WINNT': + DEFINES['UNICODE'] = True + # On windows, the WASAPI backend needs the resampler we have in + # /media/libspeex_resampler, so we can't get away with just linking cubeb's + # .o + USE_LIBS += [ + 'cubeb', + 'speex', + ] + OS_LIBS += [ + 'ole32' + ] +else: + # Otherwise, we can just grab all the compiled .o and compile against that, + # linking the appriopriate libraries. + USE_LIBS += [ + 'cubeb', + ] + # Don't link gkmedias for it introduces dependencies on Android. + if CONFIG['OS_TARGET'] == 'Android': + USE_LIBS += [ + 'speex', + ] + +if CONFIG['MOZ_WIDGET_TOOLKIT'] == 'cocoa': + OS_LIBS += [ + '-framework AudioUnit', + '-framework CoreAudio', + ] +elif CONFIG['MOZ_WIDGET_TOOLKIT'] == 'uikit': + OS_LIBS += [ + '-framework CoreFoundation', + '-framework AudioToolbox', + ] + +FINAL_LIBRARY = 'xul-gtest' diff --git a/media/libcubeb/include/cubeb-stdint.h b/media/libcubeb/include/cubeb-stdint.h new file mode 100644 index 0000000000..4622320729 --- /dev/null +++ b/media/libcubeb/include/cubeb-stdint.h @@ -0,0 +1,5 @@ +/* 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/. */ + +#include <stdint.h> diff --git a/media/libcubeb/include/cubeb/cubeb.h b/media/libcubeb/include/cubeb/cubeb.h new file mode 100644 index 0000000000..c970246283 --- /dev/null +++ b/media/libcubeb/include/cubeb/cubeb.h @@ -0,0 +1,777 @@ +/* + * Copyright © 2011 Mozilla Foundation + * + * This program is made available under an ISC-style license. See the + * accompanying file LICENSE for details. + */ +#if !defined(CUBEB_c2f983e9_c96f_e71c_72c3_bbf62992a382) +#define CUBEB_c2f983e9_c96f_e71c_72c3_bbf62992a382 + +#include "cubeb_export.h" +#include <stdint.h> +#include <stdlib.h> + +#if defined(__cplusplus) +extern "C" { +#endif + +/** @mainpage + + @section intro Introduction + + This is the documentation for the <tt>libcubeb</tt> C API. + <tt>libcubeb</tt> is a callback-based audio API library allowing the + authoring of portable multiplatform audio playback and recording. + + @section example Example code + + This example shows how to create a duplex stream that pipes the microphone + to the speakers, with minimal latency and the proper sample-rate for the + platform. + + @code + cubeb * app_ctx; + cubeb_init(&app_ctx, "Example Application", NULL); + int rv; + uint32_t rate; + uint32_t latency_frames; + uint64_t ts; + + rv = cubeb_get_preferred_sample_rate(app_ctx, &rate); + if (rv != CUBEB_OK) { + fprintf(stderr, "Could not get preferred sample-rate"); + return rv; + } + + cubeb_stream_params output_params; + output_params.format = CUBEB_SAMPLE_FLOAT32NE; + output_params.rate = rate; + output_params.channels = 2; + output_params.layout = CUBEB_LAYOUT_UNDEFINED; + output_params.prefs = CUBEB_STREAM_PREF_NONE; + + rv = cubeb_get_min_latency(app_ctx, &output_params, &latency_frames); + if (rv != CUBEB_OK) { + fprintf(stderr, "Could not get minimum latency"); + return rv; + } + + cubeb_stream_params input_params; + input_params.format = CUBEB_SAMPLE_FLOAT32NE; + input_params.rate = rate; + input_params.channels = 1; + input_params.layout = CUBEB_LAYOUT_UNDEFINED; + input_params.prefs = CUBEB_STREAM_PREF_NONE; + + cubeb_stream * stm; + rv = cubeb_stream_init(app_ctx, &stm, "Example Stream 1", + NULL, &input_params, + NULL, &output_params, + latency_frames, + data_cb, state_cb, + NULL); + if (rv != CUBEB_OK) { + fprintf(stderr, "Could not open the stream"); + return rv; + } + + rv = cubeb_stream_start(stm); + if (rv != CUBEB_OK) { + fprintf(stderr, "Could not start the stream"); + return rv; + } + for (;;) { + cubeb_stream_get_position(stm, &ts); + printf("time=%llu\n", ts); + sleep(1); + } + rv = cubeb_stream_stop(stm); + if (rv != CUBEB_OK) { + fprintf(stderr, "Could not stop the stream"); + return rv; + } + + cubeb_stream_destroy(stm); + cubeb_destroy(app_ctx); + @endcode + + @code + long data_cb(cubeb_stream * stm, void * user, + const void * input_buffer, void * output_buffer, long nframes) + { + const float * in = input_buffer; + float * out = output_buffer; + + for (int i = 0; i < nframes; ++i) { + for (int c = 0; c < 2; ++c) { + out[2 * i + c] = in[i]; + } + } + return nframes; + } + @endcode + + @code + void state_cb(cubeb_stream * stm, void * user, cubeb_state state) + { + printf("state=%d\n", state); + } + @endcode +*/ + +/** @file + The <tt>libcubeb</tt> C API. */ + +typedef struct cubeb + cubeb; /**< Opaque handle referencing the application state. */ +typedef struct cubeb_stream + cubeb_stream; /**< Opaque handle referencing the stream state. */ + +/** Sample format enumeration. */ +typedef enum { + /**< Little endian 16-bit signed PCM. */ + CUBEB_SAMPLE_S16LE, + /**< Big endian 16-bit signed PCM. */ + CUBEB_SAMPLE_S16BE, + /**< Little endian 32-bit IEEE floating point PCM. */ + CUBEB_SAMPLE_FLOAT32LE, + /**< Big endian 32-bit IEEE floating point PCM. */ + CUBEB_SAMPLE_FLOAT32BE, +#if defined(WORDS_BIGENDIAN) || defined(__BIG_ENDIAN__) + /**< Native endian 16-bit signed PCM. */ + CUBEB_SAMPLE_S16NE = CUBEB_SAMPLE_S16BE, + /**< Native endian 32-bit IEEE floating point PCM. */ + CUBEB_SAMPLE_FLOAT32NE = CUBEB_SAMPLE_FLOAT32BE +#else + /**< Native endian 16-bit signed PCM. */ + CUBEB_SAMPLE_S16NE = CUBEB_SAMPLE_S16LE, + /**< Native endian 32-bit IEEE floating point PCM. */ + CUBEB_SAMPLE_FLOAT32NE = CUBEB_SAMPLE_FLOAT32LE +#endif +} cubeb_sample_format; + +/** An opaque handle used to refer a particular input or output device + * across calls. */ +typedef void const * cubeb_devid; + +/** Level (verbosity) of logging for a particular cubeb context. */ +typedef enum { + CUBEB_LOG_DISABLED = 0, /** < Logging disabled */ + CUBEB_LOG_NORMAL = + 1, /**< Logging lifetime operation (creation/destruction). */ + CUBEB_LOG_VERBOSE = 2, /**< Verbose logging of callbacks, can have performance + implications. */ +} cubeb_log_level; + +/// A single channel position, to be used in a bitmask. +typedef enum { + CHANNEL_UNKNOWN = 0, + CHANNEL_FRONT_LEFT = 1 << 0, + CHANNEL_FRONT_RIGHT = 1 << 1, + CHANNEL_FRONT_CENTER = 1 << 2, + CHANNEL_LOW_FREQUENCY = 1 << 3, + CHANNEL_BACK_LEFT = 1 << 4, + CHANNEL_BACK_RIGHT = 1 << 5, + CHANNEL_FRONT_LEFT_OF_CENTER = 1 << 6, + CHANNEL_FRONT_RIGHT_OF_CENTER = 1 << 7, + CHANNEL_BACK_CENTER = 1 << 8, + CHANNEL_SIDE_LEFT = 1 << 9, + CHANNEL_SIDE_RIGHT = 1 << 10, + CHANNEL_TOP_CENTER = 1 << 11, + CHANNEL_TOP_FRONT_LEFT = 1 << 12, + CHANNEL_TOP_FRONT_CENTER = 1 << 13, + CHANNEL_TOP_FRONT_RIGHT = 1 << 14, + CHANNEL_TOP_BACK_LEFT = 1 << 15, + CHANNEL_TOP_BACK_CENTER = 1 << 16, + CHANNEL_TOP_BACK_RIGHT = 1 << 17 +} cubeb_channel; + +/// A bitmask representing the channel layout of a cubeb stream. This is +/// bit-compatible with WAVEFORMATEXENSIBLE and in the same order as the SMPTE +/// ordering. +typedef uint32_t cubeb_channel_layout; +// Some common layout definitions. +enum { + CUBEB_LAYOUT_UNDEFINED = 0, // Indicate the speaker's layout is undefined. + CUBEB_LAYOUT_MONO = CHANNEL_FRONT_CENTER, + CUBEB_LAYOUT_MONO_LFE = CUBEB_LAYOUT_MONO | CHANNEL_LOW_FREQUENCY, + CUBEB_LAYOUT_STEREO = CHANNEL_FRONT_LEFT | CHANNEL_FRONT_RIGHT, + CUBEB_LAYOUT_STEREO_LFE = CUBEB_LAYOUT_STEREO | CHANNEL_LOW_FREQUENCY, + CUBEB_LAYOUT_3F = + CHANNEL_FRONT_LEFT | CHANNEL_FRONT_RIGHT | CHANNEL_FRONT_CENTER, + CUBEB_LAYOUT_3F_LFE = CUBEB_LAYOUT_3F | CHANNEL_LOW_FREQUENCY, + CUBEB_LAYOUT_2F1 = + CHANNEL_FRONT_LEFT | CHANNEL_FRONT_RIGHT | CHANNEL_BACK_CENTER, + CUBEB_LAYOUT_2F1_LFE = CUBEB_LAYOUT_2F1 | CHANNEL_LOW_FREQUENCY, + CUBEB_LAYOUT_3F1 = CHANNEL_FRONT_LEFT | CHANNEL_FRONT_RIGHT | + CHANNEL_FRONT_CENTER | CHANNEL_BACK_CENTER, + CUBEB_LAYOUT_3F1_LFE = CUBEB_LAYOUT_3F1 | CHANNEL_LOW_FREQUENCY, + CUBEB_LAYOUT_2F2 = CHANNEL_FRONT_LEFT | CHANNEL_FRONT_RIGHT | + CHANNEL_SIDE_LEFT | CHANNEL_SIDE_RIGHT, + CUBEB_LAYOUT_2F2_LFE = CUBEB_LAYOUT_2F2 | CHANNEL_LOW_FREQUENCY, + CUBEB_LAYOUT_QUAD = CHANNEL_FRONT_LEFT | CHANNEL_FRONT_RIGHT | + CHANNEL_BACK_LEFT | CHANNEL_BACK_RIGHT, + CUBEB_LAYOUT_QUAD_LFE = CUBEB_LAYOUT_QUAD | CHANNEL_LOW_FREQUENCY, + CUBEB_LAYOUT_3F2 = CHANNEL_FRONT_LEFT | CHANNEL_FRONT_RIGHT | + CHANNEL_FRONT_CENTER | CHANNEL_SIDE_LEFT | + CHANNEL_SIDE_RIGHT, + CUBEB_LAYOUT_3F2_LFE = CUBEB_LAYOUT_3F2 | CHANNEL_LOW_FREQUENCY, + CUBEB_LAYOUT_3F2_BACK = CUBEB_LAYOUT_QUAD | CHANNEL_FRONT_CENTER, + CUBEB_LAYOUT_3F2_LFE_BACK = CUBEB_LAYOUT_3F2_BACK | CHANNEL_LOW_FREQUENCY, + CUBEB_LAYOUT_3F3R_LFE = CHANNEL_FRONT_LEFT | CHANNEL_FRONT_RIGHT | + CHANNEL_FRONT_CENTER | CHANNEL_LOW_FREQUENCY | + CHANNEL_BACK_CENTER | CHANNEL_SIDE_LEFT | + CHANNEL_SIDE_RIGHT, + CUBEB_LAYOUT_3F4_LFE = CHANNEL_FRONT_LEFT | CHANNEL_FRONT_RIGHT | + CHANNEL_FRONT_CENTER | CHANNEL_LOW_FREQUENCY | + CHANNEL_BACK_LEFT | CHANNEL_BACK_RIGHT | + CHANNEL_SIDE_LEFT | CHANNEL_SIDE_RIGHT, +}; + +/** Miscellaneous stream preferences. */ +typedef enum { + CUBEB_STREAM_PREF_NONE = 0x00, /**< No stream preferences are requested. */ + CUBEB_STREAM_PREF_LOOPBACK = + 0x01, /**< Request a loopback stream. Should be + specified on the input params and an + output device to loopback from should + be passed in place of an input device. */ + CUBEB_STREAM_PREF_DISABLE_DEVICE_SWITCHING = 0x02, /**< Disable switching + default device on OS + changes. */ + CUBEB_STREAM_PREF_VOICE = + 0x04, /**< This stream is going to transport voice data. + Depending on the backend and platform, this can + change the audio input or output devices + selected, as well as the quality of the stream, + for example to accomodate bluetooth SCO modes on + bluetooth devices. */ + CUBEB_STREAM_PREF_RAW = + 0x08, /**< Windows only. Bypass all signal processing + except for always on APO, driver and hardware. */ + CUBEB_STREAM_PREF_PERSIST = 0x10, /**< Request that the volume and mute + settings should persist across restarts + of the stream and/or application. This is + obsolete and ignored by all backends. */ + CUBEB_STREAM_PREF_JACK_NO_AUTO_CONNECT = 0x20 /**< Don't automatically try to + connect ports. Only affects + the jack backend. */ +} cubeb_stream_prefs; + +/** + * Input stream audio processing parameters. Only applicable with + * CUBEB_STREAM_PREF_VOICE. + */ +typedef enum { + CUBEB_INPUT_PROCESSING_PARAM_NONE = 0x00, + CUBEB_INPUT_PROCESSING_PARAM_ECHO_CANCELLATION = 0x01, + CUBEB_INPUT_PROCESSING_PARAM_NOISE_SUPPRESSION = 0x02, + CUBEB_INPUT_PROCESSING_PARAM_AUTOMATIC_GAIN_CONTROL = 0x04, + CUBEB_INPUT_PROCESSING_PARAM_VOICE_ISOLATION = 0x08, +} cubeb_input_processing_params; + +/** Stream format initialization parameters. */ +typedef struct { + cubeb_sample_format format; /**< Requested sample format. One of + #cubeb_sample_format. */ + uint32_t rate; /**< Requested sample rate. Valid range is [1000, 384000]. */ + uint32_t channels; /**< Requested channel count. Valid range is [1, 8]. */ + cubeb_channel_layout + layout; /**< Requested channel layout. This must be consistent with the + provided channels. CUBEB_LAYOUT_UNDEFINED if unknown */ + cubeb_stream_prefs prefs; /**< Requested preferences. */ +} cubeb_stream_params; + +/** Audio device description */ +typedef struct { + char * output_name; /**< The name of the output device */ + char * input_name; /**< The name of the input device */ +} cubeb_device; + +/** Stream states signaled via state_callback. */ +typedef enum { + CUBEB_STATE_STARTED, /**< Stream started. */ + CUBEB_STATE_STOPPED, /**< Stream stopped. */ + CUBEB_STATE_DRAINED, /**< Stream drained. */ + CUBEB_STATE_ERROR /**< Stream disabled due to error. */ +} cubeb_state; + +/** Result code enumeration. */ +enum { + CUBEB_OK = 0, /**< Success. */ + CUBEB_ERROR = -1, /**< Unclassified error. */ + CUBEB_ERROR_INVALID_FORMAT = + -2, /**< Unsupported #cubeb_stream_params requested. */ + CUBEB_ERROR_INVALID_PARAMETER = -3, /**< Invalid parameter specified. */ + CUBEB_ERROR_NOT_SUPPORTED = + -4, /**< Optional function not implemented in current backend. */ + CUBEB_ERROR_DEVICE_UNAVAILABLE = + -5 /**< Device specified by #cubeb_devid not available. */ +}; + +/** + * Whether a particular device is an input device (e.g. a microphone), or an + * output device (e.g. headphones). */ +typedef enum { + CUBEB_DEVICE_TYPE_UNKNOWN, + CUBEB_DEVICE_TYPE_INPUT, + CUBEB_DEVICE_TYPE_OUTPUT +} cubeb_device_type; + +/** + * The state of a device. + */ +typedef enum { + CUBEB_DEVICE_STATE_DISABLED, /**< The device has been disabled at the system + level. */ + CUBEB_DEVICE_STATE_UNPLUGGED, /**< The device is enabled, but nothing is + plugged into it. */ + CUBEB_DEVICE_STATE_ENABLED /**< The device is enabled. */ +} cubeb_device_state; + +/** + * Architecture specific sample type. + */ +typedef enum { + CUBEB_DEVICE_FMT_S16LE = 0x0010, /**< 16-bit integers, Little Endian. */ + CUBEB_DEVICE_FMT_S16BE = 0x0020, /**< 16-bit integers, Big Endian. */ + CUBEB_DEVICE_FMT_F32LE = 0x1000, /**< 32-bit floating point, Little Endian. */ + CUBEB_DEVICE_FMT_F32BE = 0x2000 /**< 32-bit floating point, Big Endian. */ +} cubeb_device_fmt; + +#if defined(WORDS_BIGENDIAN) || defined(__BIG_ENDIAN__) +/** 16-bit integers, native endianess, when on a Big Endian environment. */ +#define CUBEB_DEVICE_FMT_S16NE CUBEB_DEVICE_FMT_S16BE +/** 32-bit floating points, native endianess, when on a Big Endian environment. + */ +#define CUBEB_DEVICE_FMT_F32NE CUBEB_DEVICE_FMT_F32BE +#else +/** 16-bit integers, native endianess, when on a Little Endian environment. */ +#define CUBEB_DEVICE_FMT_S16NE CUBEB_DEVICE_FMT_S16LE +/** 32-bit floating points, native endianess, when on a Little Endian + * environment. */ +#define CUBEB_DEVICE_FMT_F32NE CUBEB_DEVICE_FMT_F32LE +#endif +/** All the 16-bit integers types. */ +#define CUBEB_DEVICE_FMT_S16_MASK \ + (CUBEB_DEVICE_FMT_S16LE | CUBEB_DEVICE_FMT_S16BE) +/** All the 32-bit floating points types. */ +#define CUBEB_DEVICE_FMT_F32_MASK \ + (CUBEB_DEVICE_FMT_F32LE | CUBEB_DEVICE_FMT_F32BE) +/** All the device formats types. */ +#define CUBEB_DEVICE_FMT_ALL \ + (CUBEB_DEVICE_FMT_S16_MASK | CUBEB_DEVICE_FMT_F32_MASK) + +/** Channel type for a `cubeb_stream`. Depending on the backend and platform + * used, this can control inter-stream interruption, ducking, and volume + * control. + */ +typedef enum { + CUBEB_DEVICE_PREF_NONE = 0x00, + CUBEB_DEVICE_PREF_MULTIMEDIA = 0x01, + CUBEB_DEVICE_PREF_VOICE = 0x02, + CUBEB_DEVICE_PREF_NOTIFICATION = 0x04, + CUBEB_DEVICE_PREF_ALL = 0x0F +} cubeb_device_pref; + +/** This structure holds the characteristics + * of an input or output audio device. It is obtained using + * `cubeb_enumerate_devices`, which returns these structures via + * `cubeb_device_collection` and must be destroyed via + * `cubeb_device_collection_destroy`. */ +typedef struct { + cubeb_devid devid; /**< Device identifier handle. */ + char const * + device_id; /**< Device identifier which might be presented in a UI. */ + char const * friendly_name; /**< Friendly device name which might be presented + in a UI. */ + char const * group_id; /**< Two devices have the same group identifier if they + belong to the same physical device; for example a + headset and microphone. */ + char const * vendor_name; /**< Optional vendor name, may be NULL. */ + + cubeb_device_type type; /**< Type of device (Input/Output). */ + cubeb_device_state state; /**< State of device disabled/enabled/unplugged. */ + cubeb_device_pref preferred; /**< Preferred device. */ + + cubeb_device_fmt format; /**< Sample format supported. */ + cubeb_device_fmt + default_format; /**< The default sample format for this device. */ + uint32_t max_channels; /**< Channels. */ + uint32_t default_rate; /**< Default/Preferred sample rate. */ + uint32_t max_rate; /**< Maximum sample rate supported. */ + uint32_t min_rate; /**< Minimum sample rate supported. */ + + uint32_t latency_lo; /**< Lowest possible latency in frames. */ + uint32_t latency_hi; /**< Higest possible latency in frames. */ +} cubeb_device_info; + +/** Device collection. + * Returned by `cubeb_enumerate_devices` and destroyed by + * `cubeb_device_collection_destroy`. */ +typedef struct { + cubeb_device_info * device; /**< Array of pointers to device info. */ + size_t count; /**< Device count in collection. */ +} cubeb_device_collection; + +/** User supplied data callback. + - Calling other cubeb functions from this callback is unsafe. + - The code in the callback should be non-blocking. + - Returning less than the number of frames this callback asks for or + provides puts the stream in drain mode. This callback will not be called + again, and the state callback will be called with CUBEB_STATE_DRAINED when + all the frames have been output. + @param stream The stream for which this callback fired. + @param user_ptr The pointer passed to cubeb_stream_init. + @param input_buffer A pointer containing the input data, or nullptr + if this is an output-only stream. + @param output_buffer A pointer to a buffer to be filled with audio samples, + or nullptr if this is an input-only stream. + @param nframes The number of frames of the two buffer. + @retval If the stream has output, this is the number of frames written to + the output buffer. In this case, if this number is less than + nframes then the stream will start to drain. If the stream is + input only, then returning nframes indicates data has been read. + In this case, a value less than nframes will result in the stream + being stopped. + @retval CUBEB_ERROR on error, in which case the data callback will stop + and the stream will enter a shutdown state. */ +typedef long (*cubeb_data_callback)(cubeb_stream * stream, void * user_ptr, + void const * input_buffer, + void * output_buffer, long nframes); + +/** User supplied state callback. + @param stream The stream for this this callback fired. + @param user_ptr The pointer passed to cubeb_stream_init. + @param state The new state of the stream. */ +typedef void (*cubeb_state_callback)(cubeb_stream * stream, void * user_ptr, + cubeb_state state); + +/** + * User supplied callback called when the underlying device changed. + * @param user_ptr The pointer passed to cubeb_stream_init. */ +typedef void (*cubeb_device_changed_callback)(void * user_ptr); + +/** + * User supplied callback called when the underlying device collection changed. + * @param context A pointer to the cubeb context. + * @param user_ptr The pointer passed to + * cubeb_register_device_collection_changed. */ +typedef void (*cubeb_device_collection_changed_callback)(cubeb * context, + void * user_ptr); + +/** User supplied callback called when a message needs logging. */ +typedef void (*cubeb_log_callback)(char const * fmt, ...); + +/** Initialize an application context. This will perform any library or + application scoped initialization. + + Note: On Windows platforms, COM must be initialized in MTA mode on + any thread that will call the cubeb API. + + @param context A out param where an opaque pointer to the application + context will be returned. + @param context_name A name for the context. Depending on the platform this + can appear in different locations. + @param backend_name The name of the cubeb backend user desires to select. + Accepted values self-documented in cubeb.c: init_oneshot + If NULL, a default ordering is used for backend choice. + A valid choice overrides all other possible backends, + so long as the backend was included at compile time. + @retval CUBEB_OK in case of success. + @retval CUBEB_ERROR in case of error, for example because the host + has no audio hardware. */ +CUBEB_EXPORT int +cubeb_init(cubeb ** context, char const * context_name, + char const * backend_name); + +/** Get a read-only string identifying this context's current backend. + @param context A pointer to the cubeb context. + @retval Read-only string identifying current backend. */ +CUBEB_EXPORT char const * +cubeb_get_backend_id(cubeb * context); + +/** Get the maximum possible number of channels. + @param context A pointer to the cubeb context. + @param max_channels The maximum number of channels. + @retval CUBEB_OK + @retval CUBEB_ERROR_INVALID_PARAMETER + @retval CUBEB_ERROR_NOT_SUPPORTED + @retval CUBEB_ERROR */ +CUBEB_EXPORT int +cubeb_get_max_channel_count(cubeb * context, uint32_t * max_channels); + +/** Get the minimal latency value, in frames, that is guaranteed to work + when creating a stream for the specified sample rate. This is platform, + hardware and backend dependent. + @param context A pointer to the cubeb context. + @param params On some backends, the minimum achievable latency depends on + the characteristics of the stream. + @param latency_frames The latency value, in frames, to pass to + cubeb_stream_init. + @retval CUBEB_OK + @retval CUBEB_ERROR_INVALID_PARAMETER + @retval CUBEB_ERROR_NOT_SUPPORTED */ +CUBEB_EXPORT int +cubeb_get_min_latency(cubeb * context, cubeb_stream_params * params, + uint32_t * latency_frames); + +/** Get the preferred sample rate for this backend: this is hardware and + platform dependent, and can avoid resampling, and/or trigger fastpaths. + @param context A pointer to the cubeb context. + @param rate The samplerate (in Hz) the current configuration prefers. + @retval CUBEB_OK + @retval CUBEB_ERROR_INVALID_PARAMETER + @retval CUBEB_ERROR_NOT_SUPPORTED */ +CUBEB_EXPORT int +cubeb_get_preferred_sample_rate(cubeb * context, uint32_t * rate); + +/** Get the supported input processing features for this backend. See + cubeb_stream_set_input_processing for how to set them for a particular input + stream. + @param context A pointer to the cubeb context. + @param params Out parameter for the input processing params supported by + this backend. + @retval CUBEB_OK + @retval CUBEB_ERROR_NOT_SUPPORTED */ +CUBEB_EXPORT int +cubeb_get_supported_input_processing_params( + cubeb * context, cubeb_input_processing_params * params); + +/** Destroy an application context. This must be called after all stream have + * been destroyed. + @param context A pointer to the cubeb context.*/ +CUBEB_EXPORT void +cubeb_destroy(cubeb * context); + +/** Initialize a stream associated with the supplied application context. + @param context A pointer to the cubeb context. + @param stream An out parameter to be filled with the an opaque pointer to a + cubeb stream. + @param stream_name A name for this stream. + @param input_device Device for the input side of the stream. If NULL the + default input device is used. Passing a valid + cubeb_devid means the stream only ever uses that device. Passing a NULL + cubeb_devid allows the stream to follow that device + type's OS default. + @param input_stream_params Parameters for the input side of the stream, or + NULL if this stream is output only. + @param output_device Device for the output side of the stream. If NULL the + default output device is used. Passing a valid + cubeb_devid means the stream only ever uses that device. Passing a NULL + cubeb_devid allows the stream to follow that device + type's OS default. + @param output_stream_params Parameters for the output side of the stream, or + NULL if this stream is input only. When input + and output stream parameters are supplied, their + rate has to be the same. + @param latency_frames Stream latency in frames. Valid range + is [1, 96000]. + @param data_callback Will be called to preroll data before playback is + started by cubeb_stream_start. + @param state_callback A pointer to a state callback. + @param user_ptr A pointer that will be passed to the callbacks. This pointer + must outlive the life time of the stream. + @retval CUBEB_OK + @retval CUBEB_ERROR + @retval CUBEB_ERROR_INVALID_FORMAT + @retval CUBEB_ERROR_DEVICE_UNAVAILABLE */ +CUBEB_EXPORT int +cubeb_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, + uint32_t latency_frames, cubeb_data_callback data_callback, + cubeb_state_callback state_callback, void * user_ptr); + +/** Destroy a stream. `cubeb_stream_stop` MUST be called before destroying a + stream. + @param stream The stream to destroy. */ +CUBEB_EXPORT void +cubeb_stream_destroy(cubeb_stream * stream); + +/** Start playback. + @param stream + @retval CUBEB_OK + @retval CUBEB_ERROR */ +CUBEB_EXPORT int +cubeb_stream_start(cubeb_stream * stream); + +/** Stop playback. + @param stream + @retval CUBEB_OK + @retval CUBEB_ERROR */ +CUBEB_EXPORT int +cubeb_stream_stop(cubeb_stream * stream); + +/** Get the current stream playback position. + @param stream + @param position Playback position in frames. + @retval CUBEB_OK + @retval CUBEB_ERROR */ +CUBEB_EXPORT int +cubeb_stream_get_position(cubeb_stream * stream, uint64_t * position); + +/** Get the latency for this stream, in frames. This is the number of frames + between the time cubeb acquires the data in the callback and the listener + can hear the sound. + @param stream + @param latency Current approximate stream latency in frames. + @retval CUBEB_OK + @retval CUBEB_ERROR_NOT_SUPPORTED + @retval CUBEB_ERROR */ +CUBEB_EXPORT int +cubeb_stream_get_latency(cubeb_stream * stream, uint32_t * latency); + +/** Get the input latency for this stream, in frames. This is the number of + frames between the time the audio input devices records the data, and they + are available in the data callback. + This returns CUBEB_ERROR when the stream is output-only. + @param stream + @param latency Current approximate stream latency in frames. + @retval CUBEB_OK + @retval CUBEB_ERROR_NOT_SUPPORTED + @retval CUBEB_ERROR */ +CUBEB_EXPORT int +cubeb_stream_get_input_latency(cubeb_stream * stream, uint32_t * latency); +/** Set the volume for a stream. + @param stream the stream for which to adjust the volume. + @param volume a float between 0.0 (muted) and 1.0 (maximum volume) + @retval CUBEB_OK + @retval CUBEB_ERROR_INVALID_PARAMETER volume is outside [0.0, 1.0] or + stream is an invalid pointer + @retval CUBEB_ERROR_NOT_SUPPORTED */ +CUBEB_EXPORT int +cubeb_stream_set_volume(cubeb_stream * stream, float volume); + +/** Change a stream's name. + @param stream the stream for which to set the name. + @param stream_name the new name for the stream + @retval CUBEB_OK + @retval CUBEB_ERROR_INVALID_PARAMETER if any pointer is invalid + @retval CUBEB_ERROR_NOT_SUPPORTED */ +CUBEB_EXPORT int +cubeb_stream_set_name(cubeb_stream * stream, char const * stream_name); + +/** Get the current output device for this stream. + @param stm the stream for which to query the current output device + @param device a pointer in which the current output device will be stored. + @retval CUBEB_OK in case of success + @retval CUBEB_ERROR_INVALID_PARAMETER if either stm, device or count are + invalid pointers + @retval CUBEB_ERROR_NOT_SUPPORTED */ +CUBEB_EXPORT int +cubeb_stream_get_current_device(cubeb_stream * stm, + cubeb_device ** const device); + +/** Set input mute state for this stream. Some platforms notify the user when an + application is accessing audio input. When all inputs are muted they can + prove to the user that the application is not actively capturing any input. + @param stream the stream for which to set input mute state + @param muted whether the input should mute or not + @retval CUBEB_OK + @retval CUBEB_ERROR_INVALID_PARAMETER if this stream does not have an input + device + @retval CUBEB_ERROR_NOT_SUPPORTED */ +CUBEB_EXPORT int +cubeb_stream_set_input_mute(cubeb_stream * stream, int mute); + +/** Set what input processing features to enable for this stream. + @param stream the stream for which to set input processing features. + @param params what input processing features to use + @retval CUBEB_OK + @retval CUBEB_ERROR if params could not be applied + @retval CUBEB_ERROR_INVALID_PARAMETER if a given param is not supported by + this backend, or if this stream does not have an input device + @retval CUBEB_ERROR_NOT_SUPPORTED */ +CUBEB_EXPORT int +cubeb_stream_set_input_processing_params(cubeb_stream * stream, + cubeb_input_processing_params params); + +/** Destroy a cubeb_device structure. + @param stream the stream passed in cubeb_stream_get_current_device + @param devices the devices to destroy + @retval CUBEB_OK in case of success + @retval CUBEB_ERROR_INVALID_PARAMETER if devices is an invalid pointer + @retval CUBEB_ERROR_NOT_SUPPORTED */ +CUBEB_EXPORT int +cubeb_stream_device_destroy(cubeb_stream * stream, cubeb_device * devices); + +/** Set a callback to be notified when the output device changes. + @param stream the stream for which to set the callback. + @param device_changed_callback a function called whenever the device has + changed. Passing NULL allow to unregister a function + @retval CUBEB_OK + @retval CUBEB_ERROR_INVALID_PARAMETER if either stream or + device_changed_callback are invalid pointers. + @retval CUBEB_ERROR_NOT_SUPPORTED */ +CUBEB_EXPORT int +cubeb_stream_register_device_changed_callback( + cubeb_stream * stream, + cubeb_device_changed_callback device_changed_callback); + +/** Return the user data pointer registered with the stream with + cubeb_stream_init. + @param stream the stream for which to retrieve user data pointer. + @retval user data pointer */ +CUBEB_EXPORT void * +cubeb_stream_user_ptr(cubeb_stream * stream); + +/** Returns enumerated devices. + @param context + @param devtype device type to include + @param collection output collection. Must be destroyed with + cubeb_device_collection_destroy + @retval CUBEB_OK in case of success + @retval CUBEB_ERROR_INVALID_PARAMETER if collection is an invalid pointer + @retval CUBEB_ERROR_NOT_SUPPORTED */ +CUBEB_EXPORT int +cubeb_enumerate_devices(cubeb * context, cubeb_device_type devtype, + cubeb_device_collection * collection); + +/** Destroy a cubeb_device_collection, and its `cubeb_device_info`. + @param context + @param collection collection to destroy + @retval CUBEB_OK + @retval CUBEB_ERROR_INVALID_PARAMETER if collection is an invalid pointer */ +CUBEB_EXPORT int +cubeb_device_collection_destroy(cubeb * context, + cubeb_device_collection * collection); + +/** Registers a callback which is called when the system detects + a new device or a device is removed. + @param context + @param devtype device type to include. Different callbacks and user pointers + can be registered for each devtype. The hybrid devtype + `CUBEB_DEVICE_TYPE_INPUT | CUBEB_DEVICE_TYPE_OUTPUT` is also valid + and will register the provided callback and user pointer in both + sides. + @param callback a function called whenever the system device list changes. + Passing NULL allow to unregister a function. You have to unregister + first before you register a new callback. + @param user_ptr pointer to user specified data which will be present in + subsequent callbacks. + @retval CUBEB_ERROR_NOT_SUPPORTED */ +CUBEB_EXPORT int +cubeb_register_device_collection_changed( + cubeb * context, cubeb_device_type devtype, + cubeb_device_collection_changed_callback callback, void * user_ptr); + +/** Set a callback to be called with a message. + @param log_level CUBEB_LOG_VERBOSE, CUBEB_LOG_NORMAL. + @param log_callback A function called with a message when there is + something to log. Pass NULL to unregister. + @retval CUBEB_OK in case of success. + @retval CUBEB_ERROR_INVALID_PARAMETER if either context or log_callback are + invalid pointers, or if level is not + in cubeb_log_level. */ +CUBEB_EXPORT int +cubeb_set_log_callback(cubeb_log_level log_level, + cubeb_log_callback log_callback); + +#if defined(__cplusplus) +} +#endif + +#endif /* CUBEB_c2f983e9_c96f_e71c_72c3_bbf62992a382 */ diff --git a/media/libcubeb/include/cubeb_export.h b/media/libcubeb/include/cubeb_export.h new file mode 100644 index 0000000000..a0b3543b72 --- /dev/null +++ b/media/libcubeb/include/cubeb_export.h @@ -0,0 +1,5 @@ +/** + * This defines CUBEB_EXPORT to an empty string, we don't need any annotation to + * build in Gecko. + */ +#define CUBEB_EXPORT diff --git a/media/libcubeb/include/moz.build b/media/libcubeb/include/moz.build new file mode 100644 index 0000000000..a5ce449cfe --- /dev/null +++ b/media/libcubeb/include/moz.build @@ -0,0 +1,11 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +EXPORTS.cubeb += [ + 'cubeb/cubeb.h', + 'cubeb_export.h' +] + diff --git a/media/libcubeb/moz.build b/media/libcubeb/moz.build new file mode 100644 index 0000000000..46df87d146 --- /dev/null +++ b/media/libcubeb/moz.build @@ -0,0 +1,12 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +with Files("**"): + BUG_COMPONENT = ("Core", "Audio/Video: cubeb") + +DIRS += ['include', 'src'] +TEST_DIRS += ['gtest'] + diff --git a/media/libcubeb/moz.yaml b/media/libcubeb/moz.yaml new file mode 100644 index 0000000000..b81e81dcac --- /dev/null +++ b/media/libcubeb/moz.yaml @@ -0,0 +1,52 @@ +schema: 1 + +bugzilla: + product: Core + component: "Audio/Video: cubeb" + +origin: + name: cubeb + description: "Cross platform audio library" + url: https://github.com/mozilla/cubeb + license: ISC + release: 46906c7bba281a9cc277881e9cf9e32909f8dbf2 (2024-02-07T16:57:06Z). + revision: 46906c7bba281a9cc277881e9cf9e32909f8dbf2 + +vendoring: + url: https://github.com/mozilla/cubeb + source-hosting: github + vendor-directory: media/libcubeb + patches: + - 0001-disable-aaudio-before-android-31.patch + - 0002-disable-crash-reporter-death-test.patch + skip-vendoring-steps: + - update-moz-build + exclude: + - ".*" + - CMakeLists.txt + - Config.cmake.in + - INSTALL.md + - cmake + - cubeb.supp + - docs + - scan-build-install.sh + - src/cubeb-jni-instances.h + - src/cubeb_assert.h + - src/cubeb_audiotrack.c + - src/cubeb_kai.c + - src/cubeb_osx_run_loop.cpp + - src/cubeb_pulse.c + - src/cubeb_tracing.h + - subprojects + - tools + keep: + - gtest/moz.build + - include/cubeb-stdint.h + - include/cubeb_export.h + - include/moz.build + - src/cubeb-jni-instances.h + - src/cubeb_assert.h + - src/cubeb_osx_run_loop.c + - src/cubeb_tracing.h + - src/moz.build + diff --git a/media/libcubeb/src/android/audiotrack_definitions.h b/media/libcubeb/src/android/audiotrack_definitions.h new file mode 100644 index 0000000000..f6b6931fa4 --- /dev/null +++ b/media/libcubeb/src/android/audiotrack_definitions.h @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <stdint.h> + +/* + * The following definitions are copied from the android sources. Only the + * relevant enum member and values needed are copied. + */ + +/* + * From + * https://android.googlesource.com/platform/frameworks/base/+/android-2.2.3_r2.1/include/utils/Errors.h + */ +typedef int32_t status_t; + +/* + * From + * https://android.googlesource.com/platform/frameworks/base/+/android-2.2.3_r2.1/include/media/AudioTrack.h + */ +struct Buffer { + uint32_t flags; + int channelCount; + int format; + size_t frameCount; + size_t size; + union { + void * raw; + short * i16; + int8_t * i8; + }; +}; + +enum event_type { + EVENT_MORE_DATA = 0, + EVENT_UNDERRUN = 1, + EVENT_LOOP_END = 2, + EVENT_MARKER = 3, + EVENT_NEW_POS = 4, + EVENT_BUFFER_END = 5 +}; + +/** + * From + * https://android.googlesource.com/platform/frameworks/base/+/android-2.2.3_r2.1/include/media/AudioSystem.h + * and + * https://android.googlesource.com/platform/system/core/+/android-4.2.2_r1/include/system/audio.h + */ + +#define AUDIO_STREAM_TYPE_MUSIC 3 + +enum { + AUDIO_CHANNEL_OUT_FRONT_LEFT_ICS = 0x1, + AUDIO_CHANNEL_OUT_FRONT_RIGHT_ICS = 0x2, + AUDIO_CHANNEL_OUT_MONO_ICS = AUDIO_CHANNEL_OUT_FRONT_LEFT_ICS, + AUDIO_CHANNEL_OUT_STEREO_ICS = + (AUDIO_CHANNEL_OUT_FRONT_LEFT_ICS | AUDIO_CHANNEL_OUT_FRONT_RIGHT_ICS) +} AudioTrack_ChannelMapping_ICS; + +enum { + AUDIO_CHANNEL_OUT_FRONT_LEFT_Legacy = 0x4, + AUDIO_CHANNEL_OUT_FRONT_RIGHT_Legacy = 0x8, + AUDIO_CHANNEL_OUT_MONO_Legacy = AUDIO_CHANNEL_OUT_FRONT_LEFT_Legacy, + AUDIO_CHANNEL_OUT_STEREO_Legacy = (AUDIO_CHANNEL_OUT_FRONT_LEFT_Legacy | + AUDIO_CHANNEL_OUT_FRONT_RIGHT_Legacy) +} AudioTrack_ChannelMapping_Legacy; + +typedef enum { + AUDIO_FORMAT_PCM = 0x00000000, + AUDIO_FORMAT_PCM_SUB_16_BIT = 0x1, + AUDIO_FORMAT_PCM_16_BIT = (AUDIO_FORMAT_PCM | AUDIO_FORMAT_PCM_SUB_16_BIT), +} AudioTrack_SampleType; 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..403276a749 --- /dev/null +++ b/media/libcubeb/src/android/cubeb-output-latency.h @@ -0,0 +1,79 @@ +#ifndef _CUBEB_OUTPUT_LATENCY_H_ +#define _CUBEB_OUTPUT_LATENCY_H_ + +#include "../cubeb-jni.h" +#include "cubeb_media_library.h" +#include <stdbool.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 = (output_latency_function *)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); + 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); +} + +extern "C" { + +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..a54427b4f7 --- /dev/null +++ b/media/libcubeb/src/android/cubeb_media_library.h @@ -0,0 +1,70 @@ +#include <cassert> +#include <cstdint> +#include <dlfcn.h> +#include <stdlib.h> +#ifndef _CUBEB_MEDIA_LIBRARY_H_ +#define _CUBEB_MEDIA_LIBRARY_H_ + +typedef int32_t (*get_output_latency_ptr)(uint32_t * latency, int stream_type); + +struct media_lib { + void * libmedia; + get_output_latency_ptr get_output_latency; +}; + +typedef struct media_lib media_lib; + +media_lib * +cubeb_load_media_library() +{ + media_lib ml = {}; + ml.libmedia = dlopen("libmedia.so", RTLD_LAZY); + if (!ml.libmedia) { + return nullptr; + } + + // 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 = (get_output_latency_ptr)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 = (get_output_latency_ptr)dlsym( + ml.libmedia, "_ZN7android11AudioSystem16getOutputLatencyEPji"); + if (!ml.get_output_latency) { + return nullptr; + } + } + + media_lib * rv = nullptr; + rv = (media_lib *)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 new file mode 100644 index 0000000000..b107003d1b --- /dev/null +++ b/media/libcubeb/src/android/sles_definitions.h @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * This file is similar to the file "OpenSLES_AndroidConfiguration.h" found in + * the Android NDK, but removes the #ifdef __cplusplus defines, so we can keep + * using a C compiler in cubeb. + */ + +#ifndef OPENSL_ES_ANDROIDCONFIGURATION_H_ +#define OPENSL_ES_ANDROIDCONFIGURATION_H_ + +/*---------------------------------------------------------------------------*/ +/* Android AudioRecorder configuration */ +/*---------------------------------------------------------------------------*/ + +/** Audio recording preset */ +/** Audio recording preset key */ +#define SL_ANDROID_KEY_RECORDING_PRESET \ + ((const SLchar *)"androidRecordingPreset") +/** Audio recording preset values */ +/** preset "none" cannot be set, it is used to indicate the current settings + * do not match any of the presets. */ +#define SL_ANDROID_RECORDING_PRESET_NONE ((SLuint32)0x00000000) +/** generic recording configuration on the platform */ +#define SL_ANDROID_RECORDING_PRESET_GENERIC ((SLuint32)0x00000001) +/** uses the microphone audio source with the same orientation as the camera + * if available, the main device microphone otherwise */ +#define SL_ANDROID_RECORDING_PRESET_CAMCORDER ((SLuint32)0x00000002) +/** uses the main microphone tuned for voice recognition */ +#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) + +/*---------------------------------------------------------------------------*/ +/* Android AudioPlayer configuration */ +/*---------------------------------------------------------------------------*/ + +/** Audio playback stream type */ +/** Audio playback stream type key */ +#define SL_ANDROID_KEY_STREAM_TYPE ((const SLchar *)"androidPlaybackStreamType") + +/** Audio playback stream type values */ +/* same as android.media.AudioManager.STREAM_VOICE_CALL */ +#define SL_ANDROID_STREAM_VOICE ((SLint32)0x00000000) +/* same as android.media.AudioManager.STREAM_SYSTEM */ +#define SL_ANDROID_STREAM_SYSTEM ((SLint32)0x00000001) +/* same as android.media.AudioManager.STREAM_RING */ +#define SL_ANDROID_STREAM_RING ((SLint32)0x00000002) +/* same as android.media.AudioManager.STREAM_MUSIC */ +#define SL_ANDROID_STREAM_MEDIA ((SLint32)0x00000003) +/* same as android.media.AudioManager.STREAM_ALARM */ +#define SL_ANDROID_STREAM_ALARM ((SLint32)0x00000004) +/* same as android.media.AudioManager.STREAM_NOTIFICATION */ +#define SL_ANDROID_STREAM_NOTIFICATION ((SLint32)0x00000005) + +/*---------------------------------------------------------------------------*/ +/* 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 new file mode 100644 index 0000000000..8357cdc7e1 --- /dev/null +++ b/media/libcubeb/src/cubeb-internal.h @@ -0,0 +1,79 @@ +/* + * Copyright © 2013 Mozilla Foundation + * + * This program is made available under an ISC-style license. See the + * accompanying file LICENSE for details. + */ +#if !defined(CUBEB_INTERNAL_0eb56756_4e20_4404_a76d_42bf88cd15a5) +#define CUBEB_INTERNAL_0eb56756_4e20_4404_a76d_42bf88cd15a5 + +#include "cubeb/cubeb.h" +#include "cubeb_assert.h" +#include "cubeb_log.h" +#include <stdio.h> +#include <string.h> + +#ifdef __clang__ +#ifndef CLANG_ANALYZER_NORETURN +#if __has_feature(attribute_analyzer_noreturn) +#define CLANG_ANALYZER_NORETURN __attribute__((analyzer_noreturn)) +#else +#define CLANG_ANALYZER_NORETURN +#endif // ifndef CLANG_ANALYZER_NORETURN +#endif // __has_feature(attribute_analyzer_noreturn) +#else // __clang__ +#define CLANG_ANALYZER_NORETURN +#endif + +#if defined(__cplusplus) +extern "C" { +#endif + +#if defined(__cplusplus) +} +#endif + +struct cubeb_ops { + int (*init)(cubeb ** context, char const * context_name); + char const * (*get_backend_id)(cubeb * context); + int (*get_max_channel_count)(cubeb * context, uint32_t * max_channels); + int (*get_min_latency)(cubeb * context, cubeb_stream_params params, + uint32_t * latency_ms); + int (*get_preferred_sample_rate)(cubeb * context, uint32_t * rate); + int (*get_supported_input_processing_params)( + cubeb * context, cubeb_input_processing_params * params); + int (*enumerate_devices)(cubeb * context, cubeb_device_type type, + 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, + 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); + void (*stream_destroy)(cubeb_stream * stream); + int (*stream_start)(cubeb_stream * stream); + int (*stream_stop)(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_get_input_latency)(cubeb_stream * stream, uint32_t * latency); + int (*stream_set_volume)(cubeb_stream * stream, float volumes); + int (*stream_set_name)(cubeb_stream * stream, char const * stream_name); + int (*stream_get_current_device)(cubeb_stream * stream, + cubeb_device ** const device); + int (*stream_set_input_mute)(cubeb_stream * stream, int mute); + int (*stream_set_input_processing_params)( + cubeb_stream * stream, cubeb_input_processing_params params); + int (*stream_device_destroy)(cubeb_stream * stream, cubeb_device * device); + int (*stream_register_device_changed_callback)( + cubeb_stream * stream, + cubeb_device_changed_callback device_changed_callback); + int (*register_device_collection_changed)( + cubeb * context, cubeb_device_type devtype, + cubeb_device_collection_changed_callback callback, void * user_ptr); +}; + +#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..6b06e08016 --- /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 "mozilla/java/GeckoAppShellWrappers.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..8e7345b8aa --- /dev/null +++ b/media/libcubeb/src/cubeb-jni.cpp @@ -0,0 +1,82 @@ +/* clang-format off */ +#include "jni.h" +#include <assert.h> +#include "cubeb-jni-instances.h" +/* clang-format on */ + +#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..d63629fb91 --- /dev/null +++ b/media/libcubeb/src/cubeb-jni.h @@ -0,0 +1,21 @@ +#ifndef _CUBEB_JNI_H_ +#define _CUBEB_JNI_H_ + +typedef struct cubeb_jni cubeb_jni; + +#ifdef __cplusplus +extern "C" { +#endif + +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); + +#ifdef __cplusplus +}; +#endif + +#endif // _CUBEB_JNI_H_ diff --git a/media/libcubeb/src/cubeb-speex-resampler.h b/media/libcubeb/src/cubeb-speex-resampler.h new file mode 100644 index 0000000000..9ecf747cb0 --- /dev/null +++ b/media/libcubeb/src/cubeb-speex-resampler.h @@ -0,0 +1 @@ +#include <speex/speex_resampler.h> diff --git a/media/libcubeb/src/cubeb.c b/media/libcubeb/src/cubeb.c new file mode 100644 index 0000000000..bc994f9536 --- /dev/null +++ b/media/libcubeb/src/cubeb.c @@ -0,0 +1,756 @@ +/* + * Copyright © 2013 Mozilla Foundation + * + * This program is made available under an ISC-style license. See the + * accompanying file LICENSE for details. + */ +#undef NDEBUG +#include "cubeb/cubeb.h" +#include "cubeb-internal.h" +#include <assert.h> +#include <stddef.h> +#include <stdlib.h> +#include <string.h> + +#define NELEMS(x) ((int)(sizeof(x) / sizeof(x[0]))) + +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) +int +pulse_init(cubeb ** context, char const * context_name); +#endif +#if defined(USE_PULSE_RUST) +int +pulse_rust_init(cubeb ** contet, char const * context_name); +#endif +#if defined(USE_JACK) +int +jack_init(cubeb ** context, char const * context_name); +#endif +#if defined(USE_ALSA) +int +alsa_init(cubeb ** context, char const * context_name); +#endif +#if defined(USE_AUDIOUNIT) +int +audiounit_init(cubeb ** context, char const * context_name); +#endif +#if defined(USE_AUDIOUNIT_RUST) +int +audiounit_rust_init(cubeb ** contet, char const * context_name); +#endif +#if defined(USE_WINMM) +int +winmm_init(cubeb ** context, char const * context_name); +#endif +#if defined(USE_WASAPI) +int +wasapi_init(cubeb ** context, char const * context_name); +#endif +#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 +#if defined(USE_OSS) +int +oss_init(cubeb ** context, char const * context_name); +#endif +#if defined(USE_AAUDIO) +int +aaudio_init(cubeb ** context, char const * context_name); +#endif +#if defined(USE_AUDIOTRACK) +int +audiotrack_init(cubeb ** context, char const * context_name); +#endif +#if defined(USE_KAI) +int +kai_init(cubeb ** context, char const * context_name); +#endif + +static int +validate_stream_params(cubeb_stream_params * input_stream_params, + cubeb_stream_params * output_stream_params) +{ + XASSERT(input_stream_params || output_stream_params); + if (output_stream_params) { + if (output_stream_params->rate < 1000 || + output_stream_params->rate > 384000 || + output_stream_params->channels < 1 || + output_stream_params->channels > UINT8_MAX) { + return CUBEB_ERROR_INVALID_FORMAT; + } + } + if (input_stream_params) { + if (input_stream_params->rate < 1000 || + input_stream_params->rate > 384000 || + input_stream_params->channels < 1 || + input_stream_params->channels > UINT8_MAX) { + return CUBEB_ERROR_INVALID_FORMAT; + } + } + // Rate and sample format must be the same for input and output, if using a + // duplex stream + if (input_stream_params && output_stream_params) { + if (input_stream_params->rate != output_stream_params->rate || + input_stream_params->format != output_stream_params->format) { + return CUBEB_ERROR_INVALID_FORMAT; + } + } + + cubeb_stream_params * params = + input_stream_params ? input_stream_params : output_stream_params; + + switch (params->format) { + case CUBEB_SAMPLE_S16LE: + case CUBEB_SAMPLE_S16BE: + case CUBEB_SAMPLE_FLOAT32LE: + case CUBEB_SAMPLE_FLOAT32BE: + return CUBEB_OK; + } + + return CUBEB_ERROR_INVALID_FORMAT; +} + +static int +validate_latency(int latency) +{ + if (latency < 1 || latency > 96000) { + return CUBEB_ERROR_INVALID_PARAMETER; + } + return CUBEB_OK; +} + +int +cubeb_init(cubeb ** context, char const * context_name, + char const * backend_name) +{ + 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, "pulse-rust")) { +#if defined(USE_PULSE_RUST) + init_oneshot = pulse_rust_init; +#endif + } else if (!strcmp(backend_name, "jack")) { +#if defined(USE_JACK) + 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, "audiounit-rust")) { +#if defined(USE_AUDIOUNIT_RUST) + init_oneshot = audiounit_rust_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, "oss")) { +#if defined(USE_OSS) + init_oneshot = oss_init; +#endif + } else if (!strcmp(backend_name, "aaudio")) { +#if defined(USE_AAUDIO) + init_oneshot = aaudio_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_RUST) + pulse_rust_init, +#endif +#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 +#if defined(USE_OSS) + oss_init, +#endif +#if defined(USE_AUDIOUNIT_RUST) + audiounit_rust_init, +#endif +#if defined(USE_AUDIOUNIT) + audiounit_init, +#endif +#if defined(USE_WASAPI) + wasapi_init, +#endif +#if defined(USE_WINMM) + winmm_init, +#endif +#if defined(USE_SUN) + sun_init, +#endif +#if defined(USE_AAUDIO) + aaudio_init, +#endif +#if defined(USE_OPENSL) + opensl_init, +#endif +#if defined(USE_AUDIOTRACK) + audiotrack_init, +#endif +#if defined(USE_KAI) + kai_init, +#endif + }; + int i; + + if (!context) { + return CUBEB_ERROR_INVALID_PARAMETER; + } + +#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); + OK(stream_destroy); + OK(stream_start); + OK(stream_stop); + OK(stream_get_position); + return CUBEB_OK; + } + } + return CUBEB_ERROR; +} + +char const * +cubeb_get_backend_id(cubeb * context) +{ + if (!context) { + return NULL; + } + + return context->ops->get_backend_id(context); +} + +int +cubeb_get_max_channel_count(cubeb * context, uint32_t * max_channels) +{ + if (!context || !max_channels) { + return CUBEB_ERROR_INVALID_PARAMETER; + } + + if (!context->ops->get_max_channel_count) { + return CUBEB_ERROR_NOT_SUPPORTED; + } + + return context->ops->get_max_channel_count(context, max_channels); +} + +int +cubeb_get_min_latency(cubeb * context, cubeb_stream_params * params, + uint32_t * latency_ms) +{ + if (!context || !params || !latency_ms) { + return CUBEB_ERROR_INVALID_PARAMETER; + } + + if (!context->ops->get_min_latency) { + return CUBEB_ERROR_NOT_SUPPORTED; + } + + return context->ops->get_min_latency(context, *params, latency_ms); +} + +int +cubeb_get_preferred_sample_rate(cubeb * context, uint32_t * rate) +{ + if (!context || !rate) { + return CUBEB_ERROR_INVALID_PARAMETER; + } + + if (!context->ops->get_preferred_sample_rate) { + return CUBEB_ERROR_NOT_SUPPORTED; + } + + return context->ops->get_preferred_sample_rate(context, rate); +} + +int +cubeb_get_supported_input_processing_params( + cubeb * context, cubeb_input_processing_params * params) +{ + if (!context || !params) { + return CUBEB_ERROR_INVALID_PARAMETER; + } + + if (!context->ops->get_supported_input_processing_params) { + return CUBEB_ERROR_NOT_SUPPORTED; + } + + return context->ops->get_supported_input_processing_params(context, params); +} + +void +cubeb_destroy(cubeb * context) +{ + if (!context) { + return; + } + + context->ops->destroy(context); + + cubeb_set_log_callback(CUBEB_LOG_DISABLED, NULL); +} + +int +cubeb_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) +{ + int r; + + if (!context || !stream || !data_callback || !state_callback) { + return CUBEB_ERROR_INVALID_PARAMETER; + } + + if ((r = validate_stream_params(input_stream_params, output_stream_params)) != + CUBEB_OK || + (r = validate_latency(latency)) != CUBEB_OK) { + return r; + } + + 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 +cubeb_stream_destroy(cubeb_stream * stream) +{ + if (!stream) { + return; + } + + stream->context->ops->stream_destroy(stream); +} + +int +cubeb_stream_start(cubeb_stream * stream) +{ + if (!stream) { + return CUBEB_ERROR_INVALID_PARAMETER; + } + + return stream->context->ops->stream_start(stream); +} + +int +cubeb_stream_stop(cubeb_stream * stream) +{ + if (!stream) { + return CUBEB_ERROR_INVALID_PARAMETER; + } + + return stream->context->ops->stream_stop(stream); +} + +int +cubeb_stream_get_position(cubeb_stream * stream, uint64_t * position) +{ + if (!stream || !position) { + return CUBEB_ERROR_INVALID_PARAMETER; + } + + return stream->context->ops->stream_get_position(stream, position); +} + +int +cubeb_stream_get_latency(cubeb_stream * stream, uint32_t * latency) +{ + if (!stream || !latency) { + return CUBEB_ERROR_INVALID_PARAMETER; + } + + if (!stream->context->ops->stream_get_latency) { + return CUBEB_ERROR_NOT_SUPPORTED; + } + + return stream->context->ops->stream_get_latency(stream, latency); +} + +int +cubeb_stream_get_input_latency(cubeb_stream * stream, uint32_t * latency) +{ + if (!stream || !latency) { + return CUBEB_ERROR_INVALID_PARAMETER; + } + + if (!stream->context->ops->stream_get_input_latency) { + return CUBEB_ERROR_NOT_SUPPORTED; + } + + return stream->context->ops->stream_get_input_latency(stream, latency); +} + +int +cubeb_stream_set_volume(cubeb_stream * stream, float volume) +{ + if (!stream || volume > 1.0 || volume < 0.0) { + return CUBEB_ERROR_INVALID_PARAMETER; + } + + if (!stream->context->ops->stream_set_volume) { + return CUBEB_ERROR_NOT_SUPPORTED; + } + + return stream->context->ops->stream_set_volume(stream, volume); +} + +int +cubeb_stream_set_name(cubeb_stream * stream, char const * stream_name) +{ + if (!stream || !stream_name) { + return CUBEB_ERROR_INVALID_PARAMETER; + } + + if (!stream->context->ops->stream_set_name) { + return CUBEB_ERROR_NOT_SUPPORTED; + } + + return stream->context->ops->stream_set_name(stream, stream_name); +} + +int +cubeb_stream_get_current_device(cubeb_stream * stream, + cubeb_device ** const device) +{ + if (!stream || !device) { + return CUBEB_ERROR_INVALID_PARAMETER; + } + + if (!stream->context->ops->stream_get_current_device) { + return CUBEB_ERROR_NOT_SUPPORTED; + } + + return stream->context->ops->stream_get_current_device(stream, device); +} + +int +cubeb_stream_set_input_mute(cubeb_stream * stream, int mute) +{ + if (!stream) { + return CUBEB_ERROR_INVALID_PARAMETER; + } + + if (!stream->context->ops->stream_set_input_mute) { + return CUBEB_ERROR_NOT_SUPPORTED; + } + + return stream->context->ops->stream_set_input_mute(stream, mute); +} + +int +cubeb_stream_set_input_processing_params(cubeb_stream * stream, + cubeb_input_processing_params params) +{ + if (!stream || !params) { + return CUBEB_ERROR_INVALID_PARAMETER; + } + + if (!stream->context->ops->stream_set_input_processing_params) { + return CUBEB_ERROR_NOT_SUPPORTED; + } + + return stream->context->ops->stream_set_input_processing_params(stream, + params); +} + +int +cubeb_stream_device_destroy(cubeb_stream * stream, cubeb_device * device) +{ + if (!stream || !device) { + return CUBEB_ERROR_INVALID_PARAMETER; + } + + if (!stream->context->ops->stream_device_destroy) { + return CUBEB_ERROR_NOT_SUPPORTED; + } + + return stream->context->ops->stream_device_destroy(stream, device); +} + +int +cubeb_stream_register_device_changed_callback( + cubeb_stream * stream, + cubeb_device_changed_callback device_changed_callback) +{ + if (!stream) { + return CUBEB_ERROR_INVALID_PARAMETER; + } + + if (!stream->context->ops->stream_register_device_changed_callback) { + return CUBEB_ERROR_NOT_SUPPORTED; + } + + 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) +{ + char devfmts[128] = ""; + const char *devtype, *devstate, *devdeffmt; + + switch (device_info->type) { + case CUBEB_DEVICE_TYPE_INPUT: + devtype = "input"; + break; + case CUBEB_DEVICE_TYPE_OUTPUT: + devtype = "output"; + break; + case CUBEB_DEVICE_TYPE_UNKNOWN: + default: + devtype = "unknown?"; + break; + }; + + switch (device_info->state) { + case CUBEB_DEVICE_STATE_DISABLED: + devstate = "disabled"; + break; + case CUBEB_DEVICE_STATE_UNPLUGGED: + devstate = "unplugged"; + break; + case CUBEB_DEVICE_STATE_ENABLED: + devstate = "enabled"; + break; + default: + devstate = "unknown?"; + break; + }; + + switch (device_info->default_format) { + case CUBEB_DEVICE_FMT_S16LE: + devdeffmt = "S16LE"; + break; + case CUBEB_DEVICE_FMT_S16BE: + devdeffmt = "S16BE"; + break; + case CUBEB_DEVICE_FMT_F32LE: + devdeffmt = "F32LE"; + break; + case CUBEB_DEVICE_FMT_F32BE: + devdeffmt = "F32BE"; + break; + default: + devdeffmt = "unknown?"; + break; + }; + + if (device_info->format & CUBEB_DEVICE_FMT_S16LE) { + strcat(devfmts, " S16LE"); + } + if (device_info->format & CUBEB_DEVICE_FMT_S16BE) { + strcat(devfmts, " S16BE"); + } + if (device_info->format & CUBEB_DEVICE_FMT_F32LE) { + strcat(devfmts, " F32LE"); + } + if (device_info->format & CUBEB_DEVICE_FMT_F32BE) { + strcat(devfmts, " F32BE"); + } + + LOG("DeviceID: \"%s\"%s\n" + "\tName:\t\"%s\"\n" + "\tGroup:\t\"%s\"\n" + "\tVendor:\t\"%s\"\n" + "\tType:\t%s\n" + "\tState:\t%s\n" + "\tMaximum channels:\t%u\n" + "\tFormat:\t%s (0x%x) (default: %s)\n" + "\tRate:\t[%u, %u] (default: %u)\n" + "\tLatency: lo %u frames, hi %u frames", + device_info->device_id, device_info->preferred ? " (PREFERRED)" : "", + device_info->friendly_name, device_info->group_id, + device_info->vendor_name, devtype, devstate, device_info->max_channels, + (devfmts[0] == '\0') ? devfmts : devfmts + 1, + (unsigned int)device_info->format, devdeffmt, device_info->min_rate, + device_info->max_rate, device_info->default_rate, device_info->latency_lo, + device_info->latency_hi); +} + +int +cubeb_enumerate_devices(cubeb * context, cubeb_device_type devtype, + cubeb_device_collection * collection) +{ + int rv; + if ((devtype & (CUBEB_DEVICE_TYPE_INPUT | CUBEB_DEVICE_TYPE_OUTPUT)) == 0) + return CUBEB_ERROR_INVALID_PARAMETER; + if (context == NULL || collection == NULL) + return CUBEB_ERROR_INVALID_PARAMETER; + if (!context->ops->enumerate_devices) + return CUBEB_ERROR_NOT_SUPPORTED; + + rv = context->ops->enumerate_devices(context, devtype, collection); + + if (cubeb_log_get_callback()) { + for (size_t i = 0; i < collection->count; i++) { + log_device(&collection->device[i]); + } + } + + return rv; +} + +int +cubeb_device_collection_destroy(cubeb * context, + cubeb_device_collection * collection) +{ + int r; + + if (context == NULL || collection == NULL) + return CUBEB_ERROR_INVALID_PARAMETER; + + if (!context->ops->device_collection_destroy) + return CUBEB_ERROR_NOT_SUPPORTED; + + if (!collection->device) + return CUBEB_OK; + + r = context->ops->device_collection_destroy(context, collection); + if (r == CUBEB_OK) { + collection->device = NULL; + collection->count = 0; + } + + return r; +} + +int +cubeb_register_device_collection_changed( + cubeb * context, cubeb_device_type devtype, + cubeb_device_collection_changed_callback callback, void * user_ptr) +{ + if (context == NULL || + (devtype & (CUBEB_DEVICE_TYPE_INPUT | CUBEB_DEVICE_TYPE_OUTPUT)) == 0) + return CUBEB_ERROR_INVALID_PARAMETER; + + if (!context->ops->register_device_collection_changed) { + return CUBEB_ERROR_NOT_SUPPORTED; + } + + return context->ops->register_device_collection_changed(context, devtype, + callback, user_ptr); +} + +int +cubeb_set_log_callback(cubeb_log_level log_level, + cubeb_log_callback log_callback) +{ + if (log_level < CUBEB_LOG_DISABLED || log_level > CUBEB_LOG_VERBOSE) { + return CUBEB_ERROR_INVALID_FORMAT; + } + + if (!log_callback && log_level != CUBEB_LOG_DISABLED) { + return CUBEB_ERROR_INVALID_PARAMETER; + } + + if (cubeb_log_get_callback() && log_callback) { + return CUBEB_ERROR_NOT_SUPPORTED; + } + + cubeb_log_set(log_level, log_callback); + + return CUBEB_OK; +} diff --git a/media/libcubeb/src/cubeb_aaudio.cpp b/media/libcubeb/src/cubeb_aaudio.cpp new file mode 100644 index 0000000000..df19602cd6 --- /dev/null +++ b/media/libcubeb/src/cubeb_aaudio.cpp @@ -0,0 +1,1802 @@ +/* ex: set tabstop=2 shiftwidth=2 expandtab: + * Copyright © 2019 Jan Kelling + * + * This program is made available under an ISC-style license. See the + * accompanying file LICENSE for details. + */ +#include "cubeb-internal.h" +#include "cubeb/cubeb.h" +#include "cubeb_android.h" +#include "cubeb_log.h" +#include "cubeb_resampler.h" +#include "cubeb_triple_buffer.h" +#include <aaudio/AAudio.h> +#include <android/api-level.h> +#include <atomic> +#include <cassert> +#include <chrono> +#include <condition_variable> +#include <cstdint> +#include <cstring> +#include <dlfcn.h> +#include <inttypes.h> +#include <limits> +#include <memory> +#include <mutex> +#include <thread> +#include <vector> + +using namespace std; + +#ifdef DISABLE_LIBAAUDIO_DLOPEN +#define WRAP(x) x +#else +#define WRAP(x) (*cubeb_##x) +#define LIBAAUDIO_API_VISIT(X) \ + X(AAudio_convertResultToText) \ + X(AAudio_convertStreamStateToText) \ + X(AAudio_createStreamBuilder) \ + X(AAudioStreamBuilder_openStream) \ + X(AAudioStreamBuilder_setChannelCount) \ + X(AAudioStreamBuilder_setBufferCapacityInFrames) \ + X(AAudioStreamBuilder_setDirection) \ + X(AAudioStreamBuilder_setFormat) \ + X(AAudioStreamBuilder_setSharingMode) \ + X(AAudioStreamBuilder_setPerformanceMode) \ + X(AAudioStreamBuilder_setSampleRate) \ + X(AAudioStreamBuilder_delete) \ + X(AAudioStreamBuilder_setDataCallback) \ + X(AAudioStreamBuilder_setErrorCallback) \ + X(AAudioStream_close) \ + X(AAudioStream_read) \ + X(AAudioStream_requestStart) \ + X(AAudioStream_requestPause) \ + X(AAudioStream_setBufferSizeInFrames) \ + X(AAudioStream_getTimestamp) \ + X(AAudioStream_requestFlush) \ + X(AAudioStream_requestStop) \ + X(AAudioStream_getPerformanceMode) \ + X(AAudioStream_getSharingMode) \ + X(AAudioStream_getBufferSizeInFrames) \ + X(AAudioStream_getBufferCapacityInFrames) \ + X(AAudioStream_getSampleRate) \ + X(AAudioStream_waitForStateChange) \ + X(AAudioStream_getFramesRead) \ + X(AAudioStream_getState) \ + X(AAudioStream_getFramesWritten) \ + X(AAudioStream_getFramesPerBurst) \ + X(AAudioStreamBuilder_setInputPreset) \ + X(AAudioStreamBuilder_setUsage) \ + X(AAudioStreamBuilder_setFramesPerDataCallback) + +// not needed or added later on +// X(AAudioStreamBuilder_setDeviceId) \ + // X(AAudioStreamBuilder_setSamplesPerFrame) \ + // X(AAudioStream_getSamplesPerFrame) \ + // X(AAudioStream_getDeviceId) \ + // X(AAudioStream_write) \ + // X(AAudioStream_getChannelCount) \ + // X(AAudioStream_getFormat) \ + // X(AAudioStream_getXRunCount) \ + // X(AAudioStream_isMMapUsed) \ + // X(AAudioStreamBuilder_setContentType) \ + // X(AAudioStreamBuilder_setSessionId) \ + // X(AAudioStream_getUsage) \ + // X(AAudioStream_getContentType) \ + // X(AAudioStream_getInputPreset) \ + // X(AAudioStream_getSessionId) \ +// END: not needed or added later on + +#define MAKE_TYPEDEF(x) static decltype(x) * cubeb_##x; +LIBAAUDIO_API_VISIT(MAKE_TYPEDEF) +#undef MAKE_TYPEDEF +#endif + +const uint8_t MAX_STREAMS = 16; +const int64_t NS_PER_S = static_cast<int64_t>(1e9); + +static void +aaudio_stream_destroy(cubeb_stream * stm); +static int +aaudio_stream_start(cubeb_stream * stm); +static int +aaudio_stream_stop(cubeb_stream * stm); + +static int +aaudio_stream_init_impl(cubeb_stream * stm, lock_guard<mutex> & lock); +static int +aaudio_stream_stop_locked(cubeb_stream * stm, lock_guard<mutex> & lock); +static void +aaudio_stream_destroy_locked(cubeb_stream * stm, lock_guard<mutex> & lock); +static int +aaudio_stream_start_locked(cubeb_stream * stm, lock_guard<mutex> & lock); + +static void +reinitialize_stream(cubeb_stream * stm); + +enum class stream_state { + INIT = 0, + STOPPED, + STOPPING, + STARTED, + STARTING, + DRAINING, + ERROR, + SHUTDOWN, +}; + +struct AAudioTimingInfo { + // The timestamp at which the audio engine last called the calback. + uint64_t tstamp; + // The number of output frames sent to the engine. + uint64_t output_frame_index; + // The current output latency in frames. 0 if there is no output stream. + uint32_t output_latency; + // The current input latency in frames. 0 if there is no input stream. + uint32_t input_latency; +}; + +struct cubeb_stream { + /* Note: Must match cubeb_stream layout in cubeb.c. */ + cubeb * context{}; + void * user_ptr{}; + + std::atomic<bool> in_use{false}; + std::atomic<bool> latency_metrics_available{false}; + std::atomic<stream_state> state{stream_state::INIT}; + std::atomic<bool> in_data_callback{false}; + triple_buffer<AAudioTimingInfo> timing_info; + + AAudioStream * ostream{}; + AAudioStream * istream{}; + cubeb_data_callback data_callback{}; + cubeb_state_callback state_callback{}; + cubeb_resampler * resampler{}; + + // mutex synchronizes access to the stream from the state thread + // and user-called functions. Everything that is accessed in the + // aaudio data (or error) callback is synchronized only via atomics. + // This lock is acquired for the entirety of the reinitialization period, when + // changing device. + std::mutex mutex; + + std::vector<uint8_t> in_buf; + unsigned in_frame_size{}; // size of one input frame + + unique_ptr<cubeb_stream_params> output_stream_params; + unique_ptr<cubeb_stream_params> input_stream_params; + uint32_t latency_frames{}; + cubeb_sample_format out_format{}; + uint32_t sample_rate{}; + std::atomic<float> volume{1.f}; + unsigned out_channels{}; + unsigned out_frame_size{}; + bool voice_input{}; + bool voice_output{}; + uint64_t previous_clock{}; +}; + +struct cubeb { + struct cubeb_ops const * ops{}; + void * libaaudio{}; + + struct { + // The state thread: it waits for state changes and stops + // drained streams. + std::thread thread; + std::thread notifier; + std::mutex mutex; + std::condition_variable cond; + std::atomic<bool> join{false}; + std::atomic<bool> waiting{false}; + } state; + + // streams[i].in_use signals whether a stream is used + struct cubeb_stream streams[MAX_STREAMS]; +}; + +struct AutoInCallback { + AutoInCallback(cubeb_stream * stm) : stm(stm) + { + stm->in_data_callback.store(true); + } + ~AutoInCallback() { stm->in_data_callback.store(false); } + cubeb_stream * stm; +}; + +// Returns when aaudio_stream's state is equal to desired_state. +// poll_frequency_ns is the duration that is slept in between asking for +// state updates and getting the new state. +// When waiting for a stream to stop, it is best to pick a value similar +// to the callback time because STOPPED will happen after +// draining. +static int +wait_for_state_change(AAudioStream * aaudio_stream, + aaudio_stream_state_t desired_state, + int64_t poll_frequency_ns) +{ + aaudio_stream_state_t new_state; + do { + aaudio_result_t res = WRAP(AAudioStream_waitForStateChange)( + aaudio_stream, AAUDIO_STREAM_STATE_UNKNOWN, &new_state, + poll_frequency_ns); + if (res != AAUDIO_OK) { + LOG("AAudioStream_waitForStateChanged: %s", + WRAP(AAudio_convertResultToText)(res)); + return CUBEB_ERROR; + } + } while (new_state != desired_state); + + LOG("wait_for_state_change: current state now: %s", + cubeb_AAudio_convertStreamStateToText(new_state)); + + return CUBEB_OK; +} + +// Only allowed from state thread, while mutex on stm is locked +static void +shutdown_with_error(cubeb_stream * stm) +{ + if (stm->istream) { + WRAP(AAudioStream_requestStop)(stm->istream); + } + if (stm->ostream) { + WRAP(AAudioStream_requestStop)(stm->ostream); + } + + int64_t poll_frequency_ns = NS_PER_S * stm->out_frame_size / stm->sample_rate; + if (stm->istream) { + wait_for_state_change(stm->istream, AAUDIO_STREAM_STATE_STOPPED, + poll_frequency_ns); + } + if (stm->ostream) { + wait_for_state_change(stm->ostream, AAUDIO_STREAM_STATE_STOPPED, + poll_frequency_ns); + } + + assert(!stm->in_data_callback.load()); + stm->state_callback(stm, stm->user_ptr, CUBEB_STATE_ERROR); + stm->state.store(stream_state::SHUTDOWN); +} + +// Returns whether the given state is one in which we wait for +// an asynchronous change +static bool +waiting_state(stream_state state) +{ + switch (state) { + case stream_state::DRAINING: + case stream_state::STARTING: + case stream_state::STOPPING: + return true; + default: + return false; + } +} + +static void +update_state(cubeb_stream * stm) +{ + // Fast path for streams that don't wait for state change or are invalid + enum stream_state old_state = stm->state.load(); + if (old_state == stream_state::INIT || old_state == stream_state::STARTED || + old_state == stream_state::STOPPED || + old_state == stream_state::SHUTDOWN) { + return; + } + + // If the main thread currently operates on this thread, we don't + // have to wait for it + unique_lock lock(stm->mutex, std::try_to_lock); + if (!lock.owns_lock()) { + return; + } + + // check again: if this is true now, the stream was destroyed or + // changed between our fast path check and locking the mutex + old_state = stm->state.load(); + if (old_state == stream_state::INIT || old_state == stream_state::STARTED || + old_state == stream_state::STOPPED || + old_state == stream_state::SHUTDOWN) { + return; + } + + // We compute the new state the stream has and then compare_exchange it + // if it has changed. This way we will never just overwrite state + // changes that were set from the audio thread in the meantime, + // such as a DRAINING or error state. + enum stream_state new_state; + do { + if (old_state == stream_state::SHUTDOWN) { + return; + } + + if (old_state == stream_state::ERROR) { + shutdown_with_error(stm); + return; + } + + new_state = old_state; + + aaudio_stream_state_t istate = 0; + aaudio_stream_state_t ostate = 0; + + // We use waitForStateChange (with zero timeout) instead of just + // getState since only the former internally updates the state. + // See the docs of aaudio getState/waitForStateChange for details, + // why we are passing STATE_UNKNOWN. + aaudio_result_t res; + if (stm->istream) { + res = WRAP(AAudioStream_waitForStateChange)( + stm->istream, AAUDIO_STREAM_STATE_UNKNOWN, &istate, 0); + if (res != AAUDIO_OK) { + LOG("AAudioStream_waitForStateChanged: %s", + WRAP(AAudio_convertResultToText)(res)); + return; + } + assert(istate); + } + + if (stm->ostream) { + res = WRAP(AAudioStream_waitForStateChange)( + stm->ostream, AAUDIO_STREAM_STATE_UNKNOWN, &ostate, 0); + if (res != AAUDIO_OK) { + LOG("AAudioStream_waitForStateChanged: %s", + WRAP(AAudio_convertResultToText)(res)); + return; + } + assert(ostate); + } + + // handle invalid stream states + if (istate == AAUDIO_STREAM_STATE_PAUSING || + istate == AAUDIO_STREAM_STATE_PAUSED || + istate == AAUDIO_STREAM_STATE_FLUSHING || + istate == AAUDIO_STREAM_STATE_FLUSHED || + istate == AAUDIO_STREAM_STATE_UNKNOWN || + istate == AAUDIO_STREAM_STATE_DISCONNECTED) { + LOG("Unexpected android input stream state %s", + WRAP(AAudio_convertStreamStateToText)(istate)); + shutdown_with_error(stm); + return; + } + + if (ostate == AAUDIO_STREAM_STATE_PAUSING || + ostate == AAUDIO_STREAM_STATE_PAUSED || + ostate == AAUDIO_STREAM_STATE_FLUSHING || + ostate == AAUDIO_STREAM_STATE_FLUSHED || + ostate == AAUDIO_STREAM_STATE_UNKNOWN || + ostate == AAUDIO_STREAM_STATE_DISCONNECTED) { + LOG("Unexpected android output stream state %s", + WRAP(AAudio_convertStreamStateToText)(istate)); + shutdown_with_error(stm); + return; + } + + switch (old_state) { + case stream_state::STARTING: + if ((!istate || istate == AAUDIO_STREAM_STATE_STARTED) && + (!ostate || ostate == AAUDIO_STREAM_STATE_STARTED)) { + stm->state_callback(stm, stm->user_ptr, CUBEB_STATE_STARTED); + new_state = stream_state::STARTED; + } + break; + case stream_state::DRAINING: + // The DRAINING state means that we want to stop the streams but + // may not have done so yet. + // The aaudio docs state that returning STOP from the callback isn't + // enough, the stream has to be stopped from another thread + // afterwards. + // No callbacks are triggered anymore when requestStop returns. + // That is important as we otherwise might read from a closed istream + // for a duplex stream. + // Therefor it is important to close ostream first. + if (ostate && ostate != AAUDIO_STREAM_STATE_STOPPING && + ostate != AAUDIO_STREAM_STATE_STOPPED) { + res = WRAP(AAudioStream_requestStop)(stm->ostream); + if (res != AAUDIO_OK) { + LOG("AAudioStream_requestStop: %s", + WRAP(AAudio_convertResultToText)(res)); + return; + } + } + if (istate && istate != AAUDIO_STREAM_STATE_STOPPING && + istate != AAUDIO_STREAM_STATE_STOPPED) { + res = WRAP(AAudioStream_requestStop)(stm->istream); + if (res != AAUDIO_OK) { + LOG("AAudioStream_requestStop: %s", + WRAP(AAudio_convertResultToText)(res)); + return; + } + } + + // we always wait until both streams are stopped until we + // send CUBEB_STATE_DRAINED. Then we can directly transition + // our logical state to STOPPED, not triggering + // an additional CUBEB_STATE_STOPPED callback (which might + // be unexpected for the user). + if ((!ostate || ostate == AAUDIO_STREAM_STATE_STOPPED) && + (!istate || istate == AAUDIO_STREAM_STATE_STOPPED)) { + new_state = stream_state::STOPPED; + stm->state_callback(stm, stm->user_ptr, CUBEB_STATE_DRAINED); + } + break; + case stream_state::STOPPING: + assert(!istate || istate == AAUDIO_STREAM_STATE_STOPPING || + istate == AAUDIO_STREAM_STATE_STOPPED); + assert(!ostate || ostate == AAUDIO_STREAM_STATE_STOPPING || + ostate == AAUDIO_STREAM_STATE_STOPPED); + if ((!istate || istate == AAUDIO_STREAM_STATE_STOPPED) && + (!ostate || ostate == AAUDIO_STREAM_STATE_STOPPED)) { + stm->state_callback(stm, stm->user_ptr, CUBEB_STATE_STOPPED); + new_state = stream_state::STOPPED; + } + break; + default: + assert(false && "Unreachable: invalid state"); + } + } while (old_state != new_state && + !stm->state.compare_exchange_strong(old_state, new_state)); +} + +// See https://nyorain.github.io/lock-free-wakeup.html for a note +// why this is needed. The audio thread notifies the state thread about +// state changes and must not block. The state thread on the other hand should +// sleep until there is work to be done. So we need a lockfree producer +// and blocking producer. This can only be achieved safely with a new thread +// that only serves as notifier backup (in case the notification happens +// right between the state thread checking and going to sleep in which case +// this thread will kick in and signal it right again). +static void +notifier_thread(cubeb * ctx) +{ + unique_lock lock(ctx->state.mutex); + + while (!ctx->state.join.load()) { + ctx->state.cond.wait(lock); + if (ctx->state.waiting.load()) { + // This must signal our state thread since there is no other + // thread currently waiting on the condition variable. + // The state change thread is guaranteed to be waiting since + // we hold the mutex it locks when awake. + ctx->state.cond.notify_one(); + } + } + + // make sure other thread joins as well + ctx->state.cond.notify_one(); + LOG("Exiting notifier thread"); +} + +static void +state_thread(cubeb * ctx) +{ + unique_lock lock(ctx->state.mutex); + + bool waiting = false; + while (!ctx->state.join.load()) { + waiting |= ctx->state.waiting.load(); + if (waiting) { + ctx->state.waiting.store(false); + waiting = false; + for (auto & stream : ctx->streams) { + cubeb_stream * stm = &stream; + update_state(stm); + waiting |= waiting_state(atomic_load(&stm->state)); + } + + // state changed from another thread, update again immediately + if (ctx->state.waiting.load()) { + waiting = true; + continue; + } + + // Not waiting for any change anymore: we can wait on the + // condition variable without timeout + if (!waiting) { + continue; + } + + // while any stream is waiting for state change we sleep with regular + // timeouts. But we wake up immediately if signaled. + // This might seem like a poor man's implementation of state change + // waiting but (as of october 2020), the implementation of + // AAudioStream_waitForStateChange is just sleeping with regular + // timeouts as well: + // https://android.googlesource.com/platform/frameworks/av/+/refs/heads/master/media/libaaudio/src/core/AudioStream.cpp + auto dur = std::chrono::milliseconds(5); + ctx->state.cond.wait_for(lock, dur); + } else { + ctx->state.cond.wait(lock); + } + } + + // make sure other thread joins as well + ctx->state.cond.notify_one(); + LOG("Exiting state thread"); +} + +static char const * +aaudio_get_backend_id(cubeb * /* ctx */) +{ + return "aaudio"; +} + +static int +aaudio_get_max_channel_count(cubeb * ctx, uint32_t * max_channels) +{ + assert(ctx && max_channels); + // NOTE: we might get more, AAudio docs don't specify anything. + *max_channels = 2; + return CUBEB_OK; +} + +static void +aaudio_destroy(cubeb * ctx) +{ + assert(ctx); + +#ifndef NDEBUG + // make sure all streams were destroyed + for (auto & stream : ctx->streams) { + assert(!stream.in_use.load()); + } +#endif + + // broadcast joining to both threads + // they will additionally signal each other before joining + ctx->state.join.store(true); + ctx->state.cond.notify_all(); + + if (ctx->state.thread.joinable()) { + ctx->state.thread.join(); + } + if (ctx->state.notifier.joinable()) { + ctx->state.notifier.join(); + } +#ifndef DISABLE_LIBAAUDIO_DLOPEN + if (ctx->libaaudio) { + dlclose(ctx->libaaudio); + } +#endif + delete ctx; +} + +static void +apply_volume(cubeb_stream * stm, void * audio_data, uint32_t num_frames) +{ + float volume = stm->volume.load(); + // optimization: we don't have to change anything in this case + if (volume == 1.f) { + return; + } + + switch (stm->out_format) { + case CUBEB_SAMPLE_S16NE: { + int16_t * integer_data = static_cast<int16_t *>(audio_data); + for (uint32_t i = 0u; i < num_frames * stm->out_channels; ++i) { + integer_data[i] = + static_cast<int16_t>(static_cast<float>(integer_data[i]) * volume); + } + break; + } + case CUBEB_SAMPLE_FLOAT32NE: + for (uint32_t i = 0u; i < num_frames * stm->out_channels; ++i) { + (static_cast<float *>(audio_data))[i] *= volume; + } + break; + default: + assert(false && "Unreachable: invalid stream out_format"); + } +} + +uint64_t +now_ns() +{ + using namespace std::chrono; + return duration_cast<nanoseconds>(steady_clock::now().time_since_epoch()) + .count(); +} + +// To be called from the real-time audio callback +uint64_t +aaudio_get_latency(cubeb_stream * stm, aaudio_direction_t direction, + uint64_t tstamp_ns) +{ + bool is_output = direction == AAUDIO_DIRECTION_OUTPUT; + int64_t hw_frame_index; + int64_t hw_tstamp; + AAudioStream * stream = is_output ? stm->ostream : stm->istream; + // For an output stream (resp. input stream), get the number of frames + // written to (resp read from) the hardware. + int64_t app_frame_index = is_output + ? WRAP(AAudioStream_getFramesWritten)(stream) + : WRAP(AAudioStream_getFramesRead)(stream); + + assert(tstamp_ns < std::numeric_limits<uint64_t>::max()); + int64_t signed_tstamp_ns = static_cast<int64_t>(tstamp_ns); + + // Get a timestamp for a particular frame index written to or read from the + // hardware. + auto result = WRAP(AAudioStream_getTimestamp)(stream, CLOCK_MONOTONIC, + &hw_frame_index, &hw_tstamp); + if (result != AAUDIO_OK) { + LOG("AAudioStream_getTimestamp failure for %s: %s", + is_output ? "output" : "input", + WRAP(AAudio_convertResultToText)(result)); + return 0; + } + + // Compute the difference between the app and the hardware indices. + int64_t frame_index_delta = app_frame_index - hw_frame_index; + // Convert to ns + int64_t frame_time_delta = (frame_index_delta * NS_PER_S) / stm->sample_rate; + // Extrapolate from the known timestamp for a particular frame presented. + int64_t app_frame_hw_time = hw_tstamp + frame_time_delta; + // For an output stream, the latency is positive, for an input stream, it's + // negative. + int64_t latency_ns = is_output ? app_frame_hw_time - signed_tstamp_ns + : signed_tstamp_ns - app_frame_hw_time; + int64_t latency_frames = stm->sample_rate * latency_ns / NS_PER_S; + + LOGV("Latency in frames (%s): %d (%dms)", is_output ? "output" : "input", + latency_frames, latency_ns / 1e6); + + return latency_frames; +} + +void +compute_and_report_latency_metrics(cubeb_stream * stm) +{ + AAudioTimingInfo info = {}; + + info.tstamp = now_ns(); + + if (stm->ostream) { + uint64_t latency_frames = + aaudio_get_latency(stm, AAUDIO_DIRECTION_OUTPUT, info.tstamp); + if (latency_frames) { + info.output_latency = latency_frames; + info.output_frame_index = + WRAP(AAudioStream_getFramesWritten)(stm->ostream); + } + } + if (stm->istream) { + uint64_t latency_frames = + aaudio_get_latency(stm, AAUDIO_DIRECTION_INPUT, info.tstamp); + if (latency_frames) { + info.input_latency = latency_frames; + } + } + + if (info.output_latency || info.input_latency) { + stm->latency_metrics_available = true; + stm->timing_info.write(info); + } +} + +// Returning AAUDIO_CALLBACK_RESULT_STOP seems to put the stream in +// an invalid state. Seems like an AAudio bug/bad documentation. +// We therefore only return it on error. + +static aaudio_data_callback_result_t +aaudio_duplex_data_cb(AAudioStream * astream, void * user_data, + void * audio_data, int32_t num_frames) +{ + cubeb_stream * stm = (cubeb_stream *)user_data; + AutoInCallback aic(stm); + assert(stm->ostream == astream); + assert(stm->istream); + assert(num_frames >= 0); + + stream_state state = atomic_load(&stm->state); + int istate = WRAP(AAudioStream_getState)(stm->istream); + int ostate = WRAP(AAudioStream_getState)(stm->ostream); + + // all other states may happen since the callback might be called + // from within requestStart + assert(state != stream_state::SHUTDOWN); + + // This might happen when we started draining but not yet actually + // stopped the stream from the state thread. + if (state == stream_state::DRAINING) { + LOG("Draining in duplex callback"); + std::memset(audio_data, 0x0, num_frames * stm->out_frame_size); + return AAUDIO_CALLBACK_RESULT_CONTINUE; + } + + if (num_frames * stm->in_frame_size > stm->in_buf.size()) { + LOG("Resizing input buffer in duplex callback"); + stm->in_buf.resize(num_frames * stm->in_frame_size); + } + // The aaudio docs state that AAudioStream_read must not be called on + // the stream associated with a callback. But we call it on the input stream + // while this callback is for the output stream so this is ok. + // We also pass timeout 0, giving us strong non-blocking guarantees. + // This is exactly how it's done in the aaudio duplex example code snippet. + long in_num_frames = + WRAP(AAudioStream_read)(stm->istream, stm->in_buf.data(), num_frames, 0); + if (in_num_frames < 0) { // error + if (in_num_frames == AAUDIO_STREAM_STATE_DISCONNECTED) { + LOG("AAudioStream_read: %s (reinitializing)", + WRAP(AAudio_convertResultToText)(in_num_frames)); + reinitialize_stream(stm); + } else { + stm->state.store(stream_state::ERROR); + } + LOG("AAudioStream_read: %s", + WRAP(AAudio_convertResultToText)(in_num_frames)); + return AAUDIO_CALLBACK_RESULT_STOP; + } + + ALOGV("aaudio duplex data cb on stream %p: state %ld (in: %d, out: %d), " + "num_frames: %ld, read: %ld", + (void *)stm, state, istate, ostate, num_frames, in_num_frames); + + compute_and_report_latency_metrics(stm); + + // This can happen shortly after starting the stream. AAudio might immediately + // begin to buffer output but not have any input ready yet. We could + // block AAudioStream_read (passing a timeout > 0) but that leads to issues + // since blocking in this callback is a bad idea in general and it might break + // the stream when it is stopped by another thread shortly after being + // started. We therefore simply send silent input to the application, as shown + // in the AAudio duplex stream code example. + if (in_num_frames < num_frames) { + // LOG("AAudioStream_read returned not enough frames: %ld instead of %d", + // in_num_frames, num_frames); + unsigned left = num_frames - in_num_frames; + uint8_t * buf = stm->in_buf.data() + in_num_frames * stm->in_frame_size; + std::memset(buf, 0x0, left * stm->in_frame_size); + in_num_frames = num_frames; + } + + long done_frames = + cubeb_resampler_fill(stm->resampler, stm->in_buf.data(), &in_num_frames, + audio_data, num_frames); + + if (done_frames < 0 || done_frames > num_frames) { + LOG("Error in data callback or resampler: %ld", done_frames); + stm->state.store(stream_state::ERROR); + return AAUDIO_CALLBACK_RESULT_STOP; + } + if (done_frames < num_frames) { + stm->state.store(stream_state::DRAINING); + stm->context->state.waiting.store(true); + stm->context->state.cond.notify_one(); + + char * begin = + static_cast<char *>(audio_data) + done_frames * stm->out_frame_size; + std::memset(begin, 0x0, (num_frames - done_frames) * stm->out_frame_size); + } + + apply_volume(stm, audio_data, done_frames); + return AAUDIO_CALLBACK_RESULT_CONTINUE; +} + +static aaudio_data_callback_result_t +aaudio_output_data_cb(AAudioStream * astream, void * user_data, + void * audio_data, int32_t num_frames) +{ + cubeb_stream * stm = (cubeb_stream *)user_data; + AutoInCallback aic(stm); + assert(stm->ostream == astream); + assert(!stm->istream); + assert(num_frames >= 0); + + stream_state state = stm->state.load(); + int ostate = WRAP(AAudioStream_getState)(stm->ostream); + ALOGV("aaudio output data cb on stream %p: state %ld (%d), num_frames: %ld", + stm, state, ostate, num_frames); + + // all other states may happen since the callback might be called + // from within requestStart + assert(state != stream_state::SHUTDOWN); + + // This might happen when we started draining but not yet actually + // stopped the stream from the state thread. + if (state == stream_state::DRAINING) { + std::memset(audio_data, 0x0, num_frames * stm->out_frame_size); + return AAUDIO_CALLBACK_RESULT_CONTINUE; + } + + compute_and_report_latency_metrics(stm); + + long done_frames = cubeb_resampler_fill(stm->resampler, nullptr, nullptr, + audio_data, num_frames); + if (done_frames < 0 || done_frames > num_frames) { + LOG("Error in data callback or resampler: %ld", done_frames); + stm->state.store(stream_state::ERROR); + return AAUDIO_CALLBACK_RESULT_STOP; + } + + if (done_frames < num_frames) { + stm->state.store(stream_state::DRAINING); + stm->context->state.waiting.store(true); + stm->context->state.cond.notify_one(); + + char * begin = + static_cast<char *>(audio_data) + done_frames * stm->out_frame_size; + std::memset(begin, 0x0, (num_frames - done_frames) * stm->out_frame_size); + } + + apply_volume(stm, audio_data, done_frames); + return AAUDIO_CALLBACK_RESULT_CONTINUE; +} + +static aaudio_data_callback_result_t +aaudio_input_data_cb(AAudioStream * astream, void * user_data, + void * audio_data, int32_t num_frames) +{ + cubeb_stream * stm = (cubeb_stream *)user_data; + AutoInCallback aic(stm); + assert(stm->istream == astream); + assert(!stm->ostream); + assert(num_frames >= 0); + + stream_state state = stm->state.load(); + int istate = WRAP(AAudioStream_getState)(stm->istream); + ALOGV("aaudio input data cb on stream %p: state %ld (%d), num_frames: %ld", + stm, state, istate, num_frames); + + // all other states may happen since the callback might be called + // from within requestStart + assert(state != stream_state::SHUTDOWN); + + // This might happen when we started draining but not yet actually + // STOPPED the stream from the state thread. + if (state == stream_state::DRAINING) { + return AAUDIO_CALLBACK_RESULT_CONTINUE; + } + + compute_and_report_latency_metrics(stm); + + long input_frame_count = num_frames; + long done_frames = cubeb_resampler_fill(stm->resampler, audio_data, + &input_frame_count, nullptr, 0); + + if (done_frames < 0 || done_frames > num_frames) { + LOG("Error in data callback or resampler: %ld", done_frames); + stm->state.store(stream_state::ERROR); + return AAUDIO_CALLBACK_RESULT_STOP; + } + + if (done_frames < input_frame_count) { + // we don't really drain an input stream, just have to + // stop it from the state thread. That is signaled via the + // DRAINING state. + stm->state.store(stream_state::DRAINING); + stm->context->state.waiting.store(true); + stm->context->state.cond.notify_one(); + } + + return AAUDIO_CALLBACK_RESULT_CONTINUE; +} + +static void +reinitialize_stream(cubeb_stream * stm) +{ + // This cannot be done from within the error callback, bounce to another + // thread. + // In this situation, the lock is acquired for the entire duration of the + // function, so that this reinitialization period is atomic. + std::thread([stm] { + lock_guard lock(stm->mutex); + stream_state state = stm->state.load(); + bool was_playing = state == stream_state::STARTED || + state == stream_state::STARTING || + state == stream_state::DRAINING; + int err = aaudio_stream_stop_locked(stm, lock); + // error ignored. + aaudio_stream_destroy_locked(stm, lock); + err = aaudio_stream_init_impl(stm, lock); + + assert(stm->in_use.load()); + + if (err != CUBEB_OK) { + aaudio_stream_destroy_locked(stm, lock); + LOG("aaudio_stream_init_impl error while reiniting: %s", + WRAP(AAudio_convertResultToText)(err)); + stm->state.store(stream_state::ERROR); + return; + } + + if (was_playing) { + err = aaudio_stream_start_locked(stm, lock); + if (err != CUBEB_OK) { + aaudio_stream_destroy_locked(stm, lock); + LOG("aaudio_stream_start error while reiniting: %s", + WRAP(AAudio_convertResultToText)(err)); + stm->state.store(stream_state::ERROR); + return; + } + } + }).detach(); +} + +static void +aaudio_error_cb(AAudioStream * astream, void * user_data, aaudio_result_t error) +{ + cubeb_stream * stm = static_cast<cubeb_stream *>(user_data); + assert(stm->ostream == astream || stm->istream == astream); + + // Device change -- reinitialize on the new default device. + if (error == AAUDIO_ERROR_DISCONNECTED) { + LOG("Audio device change, reinitializing stream"); + reinitialize_stream(stm); + return; + } + + LOG("AAudio error callback: %s", WRAP(AAudio_convertResultToText)(error)); + stm->state.store(stream_state::ERROR); +} + +static int +realize_stream(AAudioStreamBuilder * sb, const cubeb_stream_params * params, + AAudioStream ** stream, unsigned * frame_size) +{ + aaudio_result_t res; + assert(params->rate); + assert(params->channels); + + WRAP(AAudioStreamBuilder_setSampleRate) + (sb, static_cast<int32_t>(params->rate)); + WRAP(AAudioStreamBuilder_setChannelCount) + (sb, static_cast<int32_t>(params->channels)); + + aaudio_format_t fmt; + switch (params->format) { + case CUBEB_SAMPLE_S16NE: + fmt = AAUDIO_FORMAT_PCM_I16; + *frame_size = sizeof(int16_t) * params->channels; + break; + case CUBEB_SAMPLE_FLOAT32NE: + fmt = AAUDIO_FORMAT_PCM_FLOAT; + *frame_size = sizeof(float) * params->channels; + break; + default: + return CUBEB_ERROR_INVALID_FORMAT; + } + + WRAP(AAudioStreamBuilder_setFormat)(sb, fmt); + res = WRAP(AAudioStreamBuilder_openStream)(sb, stream); + if (res == AAUDIO_ERROR_INVALID_FORMAT) { + LOG("AAudio device doesn't support output format %d", fmt); + return CUBEB_ERROR_INVALID_FORMAT; + } + + if (params->rate && res == AAUDIO_ERROR_INVALID_RATE) { + // The requested rate is not supported. + // Just try again with default rate, we create a resampler anyways + WRAP(AAudioStreamBuilder_setSampleRate)(sb, AAUDIO_UNSPECIFIED); + res = WRAP(AAudioStreamBuilder_openStream)(sb, stream); + LOG("Requested rate of %u is not supported, inserting resampler", + params->rate); + } + + // When the app has no permission to record audio + // (android.permission.RECORD_AUDIO) but requested and input stream, this will + // return INVALID_ARGUMENT. + if (res != AAUDIO_OK) { + LOG("AAudioStreamBuilder_openStream: %s", + WRAP(AAudio_convertResultToText)(res)); + return CUBEB_ERROR; + } + + return CUBEB_OK; +} + +static void +aaudio_stream_destroy(cubeb_stream * stm) +{ + lock_guard lock(stm->mutex); + stm->in_use.store(false); + aaudio_stream_destroy_locked(stm, lock); +} + +static void +aaudio_stream_destroy_locked(cubeb_stream * stm, lock_guard<mutex> & lock) +{ + assert(stm->state == stream_state::STOPPED || + stm->state == stream_state::STOPPING || + stm->state == stream_state::INIT || + stm->state == stream_state::DRAINING || + stm->state == stream_state::ERROR || + stm->state == stream_state::SHUTDOWN); + + aaudio_result_t res; + + // No callbacks are triggered anymore when requestStop returns. + // That is important as we otherwise might read from a closed istream + // for a duplex stream. + if (stm->ostream) { + if (stm->state != stream_state::STOPPED && + stm->state != stream_state::STOPPING && + stm->state != stream_state::SHUTDOWN) { + res = WRAP(AAudioStream_requestStop)(stm->ostream); + if (res != AAUDIO_OK) { + LOG("AAudioStreamBuilder_requestStop: %s", + WRAP(AAudio_convertResultToText)(res)); + } + } + + WRAP(AAudioStream_close)(stm->ostream); + stm->ostream = nullptr; + } + + if (stm->istream) { + if (stm->state != stream_state::STOPPED && + stm->state != stream_state::STOPPING && + stm->state != stream_state::SHUTDOWN) { + res = WRAP(AAudioStream_requestStop)(stm->istream); + if (res != AAUDIO_OK) { + LOG("AAudioStreamBuilder_requestStop: %s", + WRAP(AAudio_convertResultToText)(res)); + } + } + + WRAP(AAudioStream_close)(stm->istream); + stm->istream = nullptr; + } + + if (stm->resampler) { + cubeb_resampler_destroy(stm->resampler); + stm->resampler = nullptr; + } + + stm->in_buf = {}; + stm->in_frame_size = {}; + stm->out_format = {}; + stm->out_channels = {}; + stm->out_frame_size = {}; + + stm->state.store(stream_state::INIT); +} + +static int +aaudio_stream_init_impl(cubeb_stream * stm, lock_guard<mutex> & lock) +{ + assert(stm->state.load() == stream_state::INIT); + + cubeb_async_log_reset_threads(); + + aaudio_result_t res; + AAudioStreamBuilder * sb; + res = WRAP(AAudio_createStreamBuilder)(&sb); + if (res != AAUDIO_OK) { + LOG("AAudio_createStreamBuilder: %s", + WRAP(AAudio_convertResultToText)(res)); + return CUBEB_ERROR; + } + + // make sure the builder is always destroyed + struct StreamBuilderDestructor { + void operator()(AAudioStreamBuilder * sb) + { + WRAP(AAudioStreamBuilder_delete)(sb); + } + }; + + std::unique_ptr<AAudioStreamBuilder, StreamBuilderDestructor> sbPtr(sb); + + WRAP(AAudioStreamBuilder_setErrorCallback)(sb, aaudio_error_cb, stm); + WRAP(AAudioStreamBuilder_setBufferCapacityInFrames) + (sb, static_cast<int32_t>(stm->latency_frames)); + + AAudioStream_dataCallback in_data_callback{}; + AAudioStream_dataCallback out_data_callback{}; + if (stm->output_stream_params && stm->input_stream_params) { + out_data_callback = aaudio_duplex_data_cb; + in_data_callback = nullptr; + } else if (stm->input_stream_params) { + in_data_callback = aaudio_input_data_cb; + } else if (stm->output_stream_params) { + out_data_callback = aaudio_output_data_cb; + } else { + LOG("Tried to open stream without input or output parameters"); + return CUBEB_ERROR; + } + +#ifdef CUBEB_AAUDIO_EXCLUSIVE_STREAM + LOG("AAudio setting exclusive share mode for stream"); + WRAP(AAudioStreamBuilder_setSharingMode)(sb, AAUDIO_SHARING_MODE_EXCLUSIVE); +#endif + + if (stm->latency_frames <= POWERSAVE_LATENCY_FRAMES_THRESHOLD) { + LOG("AAudio setting low latency mode for stream"); + WRAP(AAudioStreamBuilder_setPerformanceMode) + (sb, AAUDIO_PERFORMANCE_MODE_LOW_LATENCY); + } else { + LOG("AAudio setting power saving mode for stream"); + WRAP(AAudioStreamBuilder_setPerformanceMode) + (sb, AAUDIO_PERFORMANCE_MODE_POWER_SAVING); + } + + unsigned frame_size; + + // initialize streams + // output + cubeb_stream_params out_params; + if (stm->output_stream_params) { + int output_preset = stm->voice_output ? AAUDIO_USAGE_VOICE_COMMUNICATION + : AAUDIO_USAGE_MEDIA; + WRAP(AAudioStreamBuilder_setUsage)(sb, output_preset); + WRAP(AAudioStreamBuilder_setDirection)(sb, AAUDIO_DIRECTION_OUTPUT); + WRAP(AAudioStreamBuilder_setDataCallback)(sb, out_data_callback, stm); + assert(stm->latency_frames < std::numeric_limits<int32_t>::max()); + LOG("Frames per callback set to %d for output", stm->latency_frames); + WRAP(AAudioStreamBuilder_setFramesPerDataCallback) + (sb, static_cast<int32_t>(stm->latency_frames)); + + int res_err = realize_stream(sb, stm->output_stream_params.get(), + &stm->ostream, &frame_size); + if (res_err) { + return res_err; + } + + int32_t output_burst_size = + WRAP(AAudioStream_getFramesPerBurst)(stm->ostream); + LOG("AAudio output burst size: %d", output_burst_size); + // 3 times the burst size seems to be robust. + res = WRAP(AAudioStream_setBufferSizeInFrames)(stm->ostream, + output_burst_size * 3); + if (res < 0) { + LOG("AAudioStream_setBufferSizeInFrames error (ostream): %s", + WRAP(AAudio_convertResultToText)(res)); + // Not fatal + } + + int rate = WRAP(AAudioStream_getSampleRate)(stm->ostream); + LOG("AAudio output stream sharing mode: %d", + WRAP(AAudioStream_getSharingMode)(stm->ostream)); + LOG("AAudio output stream performance mode: %d", + WRAP(AAudioStream_getPerformanceMode)(stm->ostream)); + LOG("AAudio output stream buffer capacity: %d", + WRAP(AAudioStream_getBufferCapacityInFrames)(stm->ostream)); + LOG("AAudio output stream buffer size: %d", + WRAP(AAudioStream_getBufferSizeInFrames)(stm->ostream)); + LOG("AAudio output stream sample-rate: %d", rate); + + stm->sample_rate = stm->output_stream_params->rate; + out_params = *stm->output_stream_params; + out_params.rate = rate; + + stm->out_channels = stm->output_stream_params->channels; + stm->out_format = stm->output_stream_params->format; + stm->out_frame_size = frame_size; + stm->volume.store(1.f); + } + + // input + cubeb_stream_params in_params; + if (stm->input_stream_params) { + // Match what the OpenSL backend does for now, we could use UNPROCESSED and + // VOICE_COMMUNICATION here, but we'd need to make it clear that + // application-level AEC and other voice processing should be disabled + // there. + int input_preset = stm->voice_input ? AAUDIO_INPUT_PRESET_VOICE_RECOGNITION + : AAUDIO_INPUT_PRESET_CAMCORDER; + WRAP(AAudioStreamBuilder_setInputPreset)(sb, input_preset); + WRAP(AAudioStreamBuilder_setDirection)(sb, AAUDIO_DIRECTION_INPUT); + WRAP(AAudioStreamBuilder_setDataCallback)(sb, in_data_callback, stm); + assert(stm->latency_frames < std::numeric_limits<int32_t>::max()); + LOG("Frames per callback set to %d for input", stm->latency_frames); + WRAP(AAudioStreamBuilder_setFramesPerDataCallback) + (sb, static_cast<int32_t>(stm->latency_frames)); + int res_err = realize_stream(sb, stm->input_stream_params.get(), + &stm->istream, &frame_size); + if (res_err) { + return res_err; + } + + int32_t input_burst_size = + WRAP(AAudioStream_getFramesPerBurst)(stm->istream); + LOG("AAudio input burst size: %d", input_burst_size); + // 3 times the burst size seems to be robust. + res = WRAP(AAudioStream_setBufferSizeInFrames)(stm->istream, + input_burst_size * 3); + if (res < AAUDIO_OK) { + LOG("AAudioStream_setBufferSizeInFrames error (istream): %s", + WRAP(AAudio_convertResultToText)(res)); + // Not fatal + } + + int bcap = WRAP(AAudioStream_getBufferCapacityInFrames)(stm->istream); + int rate = WRAP(AAudioStream_getSampleRate)(stm->istream); + LOG("AAudio input stream sharing mode: %d", + WRAP(AAudioStream_getSharingMode)(stm->istream)); + LOG("AAudio input stream performance mode: %d", + WRAP(AAudioStream_getPerformanceMode)(stm->istream)); + LOG("AAudio input stream buffer capacity: %d", bcap); + LOG("AAudio input stream buffer size: %d", + WRAP(AAudioStream_getBufferSizeInFrames)(stm->istream)); + LOG("AAudio input stream buffer rate: %d", rate); + + stm->in_buf.resize(bcap * frame_size); + assert(!stm->sample_rate || + stm->sample_rate == stm->input_stream_params->rate); + + stm->sample_rate = stm->input_stream_params->rate; + in_params = *stm->input_stream_params; + in_params.rate = rate; + stm->in_frame_size = frame_size; + } + + // initialize resampler + stm->resampler = cubeb_resampler_create( + stm, stm->input_stream_params ? &in_params : nullptr, + stm->output_stream_params ? &out_params : nullptr, stm->sample_rate, + stm->data_callback, stm->user_ptr, CUBEB_RESAMPLER_QUALITY_DEFAULT, + CUBEB_RESAMPLER_RECLOCK_NONE); + + if (!stm->resampler) { + LOG("Failed to create resampler"); + return CUBEB_ERROR; + } + + // the stream isn't started initially. We don't need to differentiate + // between a stream that was just initialized and one that played + // already but was stopped. + stm->state.store(stream_state::STOPPED); + LOG("Cubeb stream (%p) INIT success", (void *)stm); + return CUBEB_OK; +} + +static int +aaudio_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) +{ + assert(!input_device); + assert(!output_device); + + // atomically find a free stream. + cubeb_stream * stm = nullptr; + unique_lock<mutex> lock; + for (auto & stream : ctx->streams) { + // This check is only an optimization, we don't strictly need it + // since we check again after locking the mutex. + if (stream.in_use.load()) { + continue; + } + + // if this fails, another thread initialized this stream + // between our check of in_use and this. + lock = unique_lock(stream.mutex, std::try_to_lock); + if (!lock.owns_lock()) { + continue; + } + + if (stream.in_use.load()) { + lock = {}; + continue; + } + + stm = &stream; + break; + } + + if (!stm) { + LOG("Error: maximum number of streams reached"); + return CUBEB_ERROR; + } + + stm->in_use.store(true); + stm->context = ctx; + stm->user_ptr = user_ptr; + stm->data_callback = data_callback; + stm->state_callback = state_callback; + stm->voice_input = input_stream_params && + !!(input_stream_params->prefs & CUBEB_STREAM_PREF_VOICE); + stm->voice_output = output_stream_params && + !!(output_stream_params->prefs & CUBEB_STREAM_PREF_VOICE); + stm->previous_clock = 0; + stm->latency_frames = latency_frames; + if (output_stream_params) { + stm->output_stream_params = std::make_unique<cubeb_stream_params>(); + *(stm->output_stream_params) = *output_stream_params; + } + if (input_stream_params) { + stm->input_stream_params = std::make_unique<cubeb_stream_params>(); + *(stm->input_stream_params) = *input_stream_params; + } + + LOG("cubeb stream prefs: voice_input: %s voice_output: %s", + stm->voice_input ? "true" : "false", + stm->voice_output ? "true" : "false"); + + // This is ok: the thread is marked as being in use + lock.unlock(); + int err; + + { + lock_guard guard(stm->mutex); + err = aaudio_stream_init_impl(stm, guard); + } + + if (err != CUBEB_OK) { + aaudio_stream_destroy(stm); + return err; + } + + *stream = stm; + return CUBEB_OK; +} + +static int +aaudio_stream_start(cubeb_stream * stm) +{ + lock_guard lock(stm->mutex); + return aaudio_stream_start_locked(stm, lock); +} + +static int +aaudio_stream_start_locked(cubeb_stream * stm, lock_guard<mutex> & lock) +{ + assert(stm && stm->in_use.load()); + stream_state state = stm->state.load(); + int istate = stm->istream ? WRAP(AAudioStream_getState)(stm->istream) : 0; + int ostate = stm->ostream ? WRAP(AAudioStream_getState)(stm->ostream) : 0; + LOGV("STARTING stream %p: %d (%d %d)", (void *)stm, state, istate, ostate); + + switch (state) { + case stream_state::STARTED: + case stream_state::STARTING: + LOG("cubeb stream %p already STARTING/STARTED", (void *)stm); + return CUBEB_OK; + case stream_state::ERROR: + case stream_state::SHUTDOWN: + return CUBEB_ERROR; + case stream_state::INIT: + assert(false && "Invalid stream"); + return CUBEB_ERROR; + case stream_state::STOPPED: + case stream_state::STOPPING: + case stream_state::DRAINING: + break; + } + + aaudio_result_t res; + + // Important to start istream before ostream. + // As soon as we start ostream, the callbacks might be triggered an we + // might read from istream (on duplex). If istream wasn't started yet + // this is a problem. + if (stm->istream) { + res = WRAP(AAudioStream_requestStart)(stm->istream); + if (res != AAUDIO_OK) { + LOG("AAudioStream_requestStart (istream): %s", + WRAP(AAudio_convertResultToText)(res)); + stm->state.store(stream_state::ERROR); + return CUBEB_ERROR; + } + } + + if (stm->ostream) { + res = WRAP(AAudioStream_requestStart)(stm->ostream); + if (res != AAUDIO_OK) { + LOG("AAudioStream_requestStart (ostream): %s", + WRAP(AAudio_convertResultToText)(res)); + stm->state.store(stream_state::ERROR); + return CUBEB_ERROR; + } + } + + int ret = CUBEB_OK; + bool success; + + while (!(success = stm->state.compare_exchange_strong( + state, stream_state::STARTING))) { + // we land here only if the state has changed in the meantime + switch (state) { + // If an error ocurred in the meantime, we can't change that. + // The stream will be stopped when shut down. + case stream_state::ERROR: + ret = CUBEB_ERROR; + break; + // The only situation in which the state could have switched to draining + // is if the callback was already fired and requested draining. Don't + // overwrite that. It's not an error either though. + case stream_state::DRAINING: + break; + + // If the state switched [DRAINING -> STOPPING] or [DRAINING/STOPPING -> + // STOPPED] in the meantime, we can simply overwrite that since we + // restarted the stream. + case stream_state::STOPPING: + case stream_state::STOPPED: + continue; + + // There is no situation in which the state could have been valid before + // but now in shutdown mode, since we hold the streams mutex. + // There is also no way that it switched *into* STARTING or + // STARTED mode. + default: + assert(false && "Invalid state change"); + ret = CUBEB_ERROR; + break; + } + + break; + } + + if (success) { + stm->context->state.waiting.store(true); + stm->context->state.cond.notify_one(); + } + + return ret; +} + +static int +aaudio_stream_stop(cubeb_stream * stm) +{ + assert(stm && stm->in_use.load()); + lock_guard lock(stm->mutex); + return aaudio_stream_stop_locked(stm, lock); +} + +static int +aaudio_stream_stop_locked(cubeb_stream * stm, lock_guard<mutex> & lock) +{ + assert(stm && stm->in_use.load()); + + stream_state state = stm->state.load(); + int istate = stm->istream ? WRAP(AAudioStream_getState)(stm->istream) : 0; + int ostate = stm->ostream ? WRAP(AAudioStream_getState)(stm->ostream) : 0; + LOG("STOPPING stream %p: %d (%d %d)", (void *)stm, state, istate, ostate); + + switch (state) { + case stream_state::STOPPED: + case stream_state::STOPPING: + case stream_state::DRAINING: + LOG("cubeb stream %p already STOPPING/STOPPED", (void *)stm); + return CUBEB_OK; + case stream_state::ERROR: + case stream_state::SHUTDOWN: + return CUBEB_ERROR; + case stream_state::INIT: + assert(false && "Invalid stream"); + return CUBEB_ERROR; + case stream_state::STARTED: + case stream_state::STARTING: + break; + } + + aaudio_result_t res; + + // No callbacks are triggered anymore when requestStop returns. + // That is important as we otherwise might read from a closed istream + // for a duplex stream. + // Therefor it is important to close ostream first. + if (stm->ostream) { + // Could use pause + flush here as well, the public cubeb interface + // doesn't state behavior. + res = WRAP(AAudioStream_requestStop)(stm->ostream); + if (res != AAUDIO_OK) { + LOG("AAudioStream_requestStop (ostream): %s", + WRAP(AAudio_convertResultToText)(res)); + stm->state.store(stream_state::ERROR); + return CUBEB_ERROR; + } + } + + if (stm->istream) { + res = WRAP(AAudioStream_requestStop)(stm->istream); + if (res != AAUDIO_OK) { + LOG("AAudioStream_requestStop (istream): %s", + WRAP(AAudio_convertResultToText)(res)); + stm->state.store(stream_state::ERROR); + return CUBEB_ERROR; + } + } + + int ret = CUBEB_OK; + bool success; + while (!(success = atomic_compare_exchange_strong(&stm->state, &state, + stream_state::STOPPING))) { + // we land here only if the state has changed in the meantime + switch (state) { + // If an error ocurred in the meantime, we can't change that. + // The stream will be STOPPED when shut down. + case stream_state::ERROR: + ret = CUBEB_ERROR; + break; + // If it was switched to DRAINING in the meantime, it was or + // will be STOPPED soon anyways. We don't interfere with + // the DRAINING process, no matter in which state. + // Not an error + case stream_state::DRAINING: + case stream_state::STOPPING: + case stream_state::STOPPED: + break; + + // If the state switched from STARTING to STARTED in the meantime + // we can simply overwrite that since we just STOPPED it. + case stream_state::STARTED: + continue; + + // There is no situation in which the state could have been valid before + // but now in shutdown mode, since we hold the streams mutex. + // There is also no way that it switched *into* STARTING mode. + default: + assert(false && "Invalid state change"); + ret = CUBEB_ERROR; + break; + } + + break; + } + + if (success) { + stm->context->state.waiting.store(true); + stm->context->state.cond.notify_one(); + } + + return ret; +} + +static int +aaudio_stream_get_position(cubeb_stream * stm, uint64_t * position) +{ + assert(stm && stm->in_use.load()); + lock_guard lock(stm->mutex); + + stream_state state = stm->state.load(); + AAudioStream * stream = stm->ostream ? stm->ostream : stm->istream; + switch (state) { + case stream_state::ERROR: + case stream_state::SHUTDOWN: + return CUBEB_ERROR; + case stream_state::DRAINING: + case stream_state::STOPPED: + case stream_state::STOPPING: + // getTimestamp is only valid when the stream is playing. + // Simply return the number of frames passed to aaudio + *position = WRAP(AAudioStream_getFramesRead)(stream); + if (*position < stm->previous_clock) { + *position = stm->previous_clock; + } else { + stm->previous_clock = *position; + } + return CUBEB_OK; + case stream_state::INIT: + assert(false && "Invalid stream"); + return CUBEB_ERROR; + case stream_state::STARTED: + case stream_state::STARTING: + break; + } + + // No callback yet, the stream hasn't really started. + if (stm->previous_clock == 0 && !stm->timing_info.updated()) { + LOG("Not timing info yet"); + *position = 0; + return CUBEB_OK; + } + + AAudioTimingInfo info = stm->timing_info.read(); + LOGV("AAudioTimingInfo idx:%lu tstamp:%lu latency:%u", + info.output_frame_index, info.tstamp, info.output_latency); + // Interpolate client side since the last callback. + uint64_t interpolation = + stm->sample_rate * (now_ns() - info.tstamp) / NS_PER_S; + *position = info.output_frame_index + interpolation - info.output_latency; + if (*position < stm->previous_clock) { + *position = stm->previous_clock; + } else { + stm->previous_clock = *position; + } + + LOG("aaudio_stream_get_position: %" PRIu64 " frames", *position); + + return CUBEB_OK; +} + +static int +aaudio_stream_get_latency(cubeb_stream * stm, uint32_t * latency) +{ + if (!stm->ostream) { + LOG("error: aaudio_stream_get_latency on input-only stream"); + return CUBEB_ERROR; + } + + if (!stm->latency_metrics_available) { + LOG("Not timing info yet (output)"); + return CUBEB_OK; + } + + AAudioTimingInfo info = stm->timing_info.read(); + + *latency = info.output_latency; + LOG("aaudio_stream_get_latency, %u frames", *latency); + + return CUBEB_OK; +} + +static int +aaudio_stream_get_input_latency(cubeb_stream * stm, uint32_t * latency) +{ + if (!stm->istream) { + LOG("error: aaudio_stream_get_input_latency on an output-only stream"); + return CUBEB_ERROR; + } + + if (!stm->latency_metrics_available) { + LOG("Not timing info yet (input)"); + return CUBEB_OK; + } + + AAudioTimingInfo info = stm->timing_info.read(); + + *latency = info.input_latency; + LOG("aaudio_stream_get_latency, %u frames", *latency); + + return CUBEB_OK; +} + +static int +aaudio_stream_set_volume(cubeb_stream * stm, float volume) +{ + assert(stm && stm->in_use.load() && stm->ostream); + stm->volume.store(volume); + return CUBEB_OK; +} + +aaudio_data_callback_result_t +dummy_callback(AAudioStream * stream, void * userData, void * audioData, + int32_t numFrames) +{ + return AAUDIO_CALLBACK_RESULT_STOP; +} + +// Returns a dummy stream with all default settings +static AAudioStream * +init_dummy_stream() +{ + AAudioStreamBuilder * streamBuilder; + aaudio_result_t res; + res = WRAP(AAudio_createStreamBuilder)(&streamBuilder); + if (res != AAUDIO_OK) { + LOG("init_dummy_stream: AAudio_createStreamBuilder: %s", + WRAP(AAudio_convertResultToText)(res)); + return nullptr; + } + WRAP(AAudioStreamBuilder_setDataCallback) + (streamBuilder, dummy_callback, nullptr); + WRAP(AAudioStreamBuilder_setPerformanceMode) + (streamBuilder, AAUDIO_PERFORMANCE_MODE_LOW_LATENCY); + + AAudioStream * stream; + res = WRAP(AAudioStreamBuilder_openStream)(streamBuilder, &stream); + if (res != AAUDIO_OK) { + LOG("init_dummy_stream: AAudioStreamBuilder_openStream %s", + WRAP(AAudio_convertResultToText)(res)); + return nullptr; + } + WRAP(AAudioStreamBuilder_delete)(streamBuilder); + + return stream; +} + +static void +destroy_dummy_stream(AAudioStream * stream) +{ + WRAP(AAudioStream_close)(stream); +} + +static int +aaudio_get_min_latency(cubeb * ctx, cubeb_stream_params params, + uint32_t * latency_frames) +{ + AAudioStream * stream = init_dummy_stream(); + + if (!stream) { + return CUBEB_ERROR; + } + + // https://android.googlesource.com/platform/compatibility/cdd/+/refs/heads/master/5_multimedia/5_6_audio-latency.md + *latency_frames = WRAP(AAudioStream_getFramesPerBurst)(stream); + + LOG("aaudio_get_min_latency: %u frames", *latency_frames); + + destroy_dummy_stream(stream); + + return CUBEB_OK; +} + +int +aaudio_get_preferred_sample_rate(cubeb * ctx, uint32_t * rate) +{ + AAudioStream * stream = init_dummy_stream(); + + if (!stream) { + return CUBEB_ERROR; + } + + *rate = WRAP(AAudioStream_getSampleRate)(stream); + + LOG("aaudio_get_preferred_sample_rate %uHz", *rate); + + destroy_dummy_stream(stream); + + return CUBEB_OK; +} + +extern "C" int +aaudio_init(cubeb ** context, char const * context_name); + +const static struct cubeb_ops aaudio_ops = { + /*.init =*/aaudio_init, + /*.get_backend_id =*/aaudio_get_backend_id, + /*.get_max_channel_count =*/aaudio_get_max_channel_count, + /* .get_min_latency =*/aaudio_get_min_latency, + /*.get_preferred_sample_rate =*/aaudio_get_preferred_sample_rate, + /*.get_supported_input_processing_params =*/nullptr, + /*.enumerate_devices =*/nullptr, + /*.device_collection_destroy =*/nullptr, + /*.destroy =*/aaudio_destroy, + /*.stream_init =*/aaudio_stream_init, + /*.stream_destroy =*/aaudio_stream_destroy, + /*.stream_start =*/aaudio_stream_start, + /*.stream_stop =*/aaudio_stream_stop, + /*.stream_get_position =*/aaudio_stream_get_position, + /*.stream_get_latency =*/aaudio_stream_get_latency, + /*.stream_get_input_latency =*/aaudio_stream_get_input_latency, + /*.stream_set_volume =*/aaudio_stream_set_volume, + /*.stream_set_name =*/nullptr, + /*.stream_get_current_device =*/nullptr, + /*.stream_set_input_mute =*/nullptr, + /*.stream_set_input_processing_params =*/nullptr, + /*.stream_device_destroy =*/nullptr, + /*.stream_register_device_changed_callback =*/nullptr, + /*.register_device_collection_changed =*/nullptr}; + +extern "C" /*static*/ int +aaudio_init(cubeb ** context, char const * /* context_name */) +{ + if (android_get_device_api_level() <= 30) { + return CUBEB_ERROR; + } + // load api + void * libaaudio = nullptr; +#ifndef DISABLE_LIBAAUDIO_DLOPEN + libaaudio = dlopen("libaaudio.so", RTLD_NOW); + if (!libaaudio) { + return CUBEB_ERROR; + } + +#define LOAD(x) \ + { \ + cubeb_##x = (decltype(x) *)(dlsym(libaaudio, #x)); \ + if (!WRAP(x)) { \ + LOG("AAudio: Failed to load %s", #x); \ + dlclose(libaaudio); \ + return CUBEB_ERROR; \ + } \ + } + + LIBAAUDIO_API_VISIT(LOAD); +#undef LOAD +#endif + + cubeb * ctx = new cubeb; + ctx->ops = &aaudio_ops; + ctx->libaaudio = libaaudio; + + ctx->state.thread = std::thread(state_thread, ctx); + + // NOTE: using platform-specific APIs we could set the priority of the + // notifier thread lower than the priority of the state thread. + // This way, it's more likely that the state thread will be woken up + // by the condition variable signal when both are currently waiting + ctx->state.notifier = std::thread(notifier_thread, ctx); + + *context = ctx; + return CUBEB_OK; +} diff --git a/media/libcubeb/src/cubeb_alsa.c b/media/libcubeb/src/cubeb_alsa.c new file mode 100644 index 0000000000..f114f27d7b --- /dev/null +++ b/media/libcubeb/src/cubeb_alsa.c @@ -0,0 +1,1493 @@ +/* + * Copyright © 2011 Mozilla Foundation + * + * This program is made available under an ISC-style license. See the + * accompanying file LICENSE for details. + */ +#undef NDEBUG +#define _DEFAULT_SOURCE +#define _BSD_SOURCE +#if defined(__NetBSD__) +#define _NETBSD_SOURCE /* timersub() */ +#endif +#define _XOPEN_SOURCE 500 +#include "cubeb-internal.h" +#include "cubeb/cubeb.h" +#include "cubeb_tracing.h" +#include <alsa/asoundlib.h> +#include <assert.h> +#include <dlfcn.h> +#include <limits.h> +#include <poll.h> +#include <pthread.h> +#include <sys/time.h> +#include <unistd.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 + +#define CUBEB_ALSA_PCM_NAME "default" + +#define ALSA_PA_PLUGIN "ALSA <-> PulseAudio PCM I/O Plugin" + +/* ALSA is not thread-safe. snd_pcm_t instances are individually protected + by the owning cubeb_stream's mutex. snd_pcm_t creation and destruction + is not thread-safe until ALSA 1.0.24 (see alsa-lib.git commit 91c9c8f1), + so those calls must be wrapped in the following mutex. */ +static pthread_mutex_t cubeb_alsa_mutex = PTHREAD_MUTEX_INITIALIZER; +static int cubeb_alsa_error_handler_set = 0; + +static struct cubeb_ops const alsa_ops; + +struct cubeb { + struct cubeb_ops const * ops; + void * libasound; + + pthread_t thread; + + /* Mutex for streams array, must not be held while blocked in poll(2). */ + pthread_mutex_t mutex; + + /* Sparse array of streams managed by this context. */ + cubeb_stream * streams[CUBEB_STREAM_MAX]; + + /* fds and nfds are only updated by alsa_run when rebuild is set. */ + struct pollfd * fds; + nfds_t nfds; + int rebuild; + + int shutdown; + + /* Control pipe for forcing poll to wake and rebuild fds or recalculate the + * timeout. */ + int control_fd_read; + int control_fd_write; + + /* Track number of active streams. This is limited to CUBEB_STREAM_MAX + due to resource contraints. */ + unsigned int active_streams; + + /* Local configuration with handle_underrun workaround set for PulseAudio + ALSA plugin. Will be NULL if the PA ALSA plugin is not in use or the + workaround is not required. */ + snd_config_t * local_config; + int is_pa; +}; + +enum stream_state { INACTIVE, RUNNING, DRAINING, PROCESSING, ERROR }; + +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; + snd_pcm_uframes_t stream_position; + snd_pcm_uframes_t last_position; + snd_pcm_uframes_t buffer_size; + cubeb_stream_params params; + + /* Every member after this comment is protected by the owning context's + mutex rather than the stream's mutex, or is only used on the context's + run thread. */ + pthread_cond_t cond; /* Signaled when the stream's state is changed. */ + + enum stream_state state; + + struct pollfd * saved_fds; /* A copy of the pollfds passed in at init time. */ + struct pollfd * + fds; /* Pointer to this waitable's pollfds within struct cubeb's fds. */ + nfds_t nfds; + + struct timeval drain_timeout; + + /* XXX: Horrible hack -- if an active stream has been idle for + CUBEB_WATCHDOG_MS it will be disabled and the error callback will be + called. This works around a bug seen with older versions of ALSA and + PulseAudio where streams would stop requesting new data despite still + 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 +any_revents(struct pollfd * fds, nfds_t nfds) +{ + nfds_t i; + + for (i = 0; i < nfds; ++i) { + if (fds[i].revents) { + return 1; + } + } + + return 0; +} + +static int +cmp_timeval(struct timeval * a, struct timeval * b) +{ + if (a->tv_sec == b->tv_sec) { + if (a->tv_usec == b->tv_usec) { + return 0; + } + return a->tv_usec > b->tv_usec ? 1 : -1; + } + return a->tv_sec > b->tv_sec ? 1 : -1; +} + +static int +timeval_to_relative_ms(struct timeval * tv) +{ + struct timeval now; + struct timeval dt; + long long t; + int r; + + gettimeofday(&now, NULL); + r = cmp_timeval(tv, &now); + if (r >= 0) { + timersub(tv, &now, &dt); + } else { + timersub(&now, tv, &dt); + } + t = dt.tv_sec; + t *= 1000; + t += (dt.tv_usec + 500) / 1000; + + if (t > INT_MAX) { + t = INT_MAX; + } else if (t < INT_MIN) { + t = INT_MIN; + } + + return r >= 0 ? t : -t; +} + +static int +ms_until(struct timeval * tv) +{ + return timeval_to_relative_ms(tv); +} + +static int +ms_since(struct timeval * tv) +{ + return -timeval_to_relative_ms(tv); +} + +static void +rebuild(cubeb * ctx) +{ + nfds_t nfds; + int i; + nfds_t j; + cubeb_stream * stm; + + assert(ctx->rebuild); + + /* Always count context's control pipe fd. */ + nfds = 1; + for (i = 0; i < CUBEB_STREAM_MAX; ++i) { + stm = ctx->streams[i]; + if (stm) { + stm->fds = NULL; + if (stm->state == RUNNING) { + nfds += stm->nfds; + } + } + } + + free(ctx->fds); + ctx->fds = calloc(nfds, sizeof(struct pollfd)); + assert(ctx->fds); + ctx->nfds = nfds; + + /* Include context's control pipe fd. */ + ctx->fds[0].fd = ctx->control_fd_read; + ctx->fds[0].events = POLLIN | POLLERR; + + for (i = 0, j = 1; i < CUBEB_STREAM_MAX; ++i) { + stm = ctx->streams[i]; + if (stm && stm->state == RUNNING) { + memcpy(&ctx->fds[j], stm->saved_fds, stm->nfds * sizeof(struct pollfd)); + stm->fds = &ctx->fds[j]; + j += stm->nfds; + } + } + + ctx->rebuild = 0; +} + +static void +poll_wake(cubeb * ctx) +{ + if (write(ctx->control_fd_write, "x", 1) < 0) { + /* ignore write error */ + } +} + +static void +set_timeout(struct timeval * timeout, unsigned int ms) +{ + gettimeofday(timeout, NULL); + timeout->tv_sec += ms / 1000; + timeout->tv_usec += (ms % 1000) * 1000; +} + +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; + int r; + + ctx = stm->context; + stm->state = state; + r = pthread_cond_broadcast(&stm->cond); + assert(r == 0); + ctx->rebuild = 1; + poll_wake(ctx); +} + +static enum stream_state +alsa_process_stream(cubeb_stream * stm) +{ + unsigned short revents; + snd_pcm_sframes_t avail; + int draining; + + draining = 0; + + pthread_mutex_lock(&stm->mutex); + + /* 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); + + 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); + return RUNNING; + } + + /* 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; + } + + /* 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); + 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; + } + } + } + + /* 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, 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); + } + } + } + + /* 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 *)stm->buffer; + for (uint32_t i = 0; i < avail * stm->params.channels; i++) { + b[i] *= stm->volume; + } + } else { + short * b = (short *)stm->buffer; + for (uint32_t i = 0; i < avail * stm->params.channels; i++) { + b[i] *= stm->volume; + } + } + + wrote = WRAP(snd_pcm_writei)(stm->pcm, stm->buffer, avail); + if (wrote < 0) { + avail = wrote; // the error handler below will recover us + } else { + stream_buffer_decrement(stm, wrote); + + stm->stream_position += wrote; + gettimeofday(&stm->last_activity, NULL); + } + } + + /* Got some error? Let's try to recover the stream. */ + if (avail < 0) { + avail = WRAP(snd_pcm_recover)(stm->pcm, avail, 0); + + /* 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); + } + } + + /* 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; + } + + pthread_mutex_unlock(&stm->mutex); + return draining ? DRAINING : RUNNING; +} + +static int +alsa_run(cubeb * ctx) +{ + int r; + int timeout; + int i; + char dummy; + cubeb_stream * stm; + enum stream_state state; + + pthread_mutex_lock(&ctx->mutex); + + if (ctx->rebuild) { + rebuild(ctx); + } + + /* Wake up at least once per second for the watchdog. */ + timeout = 1000; + for (i = 0; i < CUBEB_STREAM_MAX; ++i) { + stm = ctx->streams[i]; + if (stm && stm->state == DRAINING) { + r = ms_until(&stm->drain_timeout); + if (r >= 0 && timeout > r) { + timeout = r; + } + } + } + + pthread_mutex_unlock(&ctx->mutex); + r = poll(ctx->fds, ctx->nfds, timeout); + pthread_mutex_lock(&ctx->mutex); + + if (r > 0) { + if (ctx->fds[0].revents & POLLIN) { + if (read(ctx->control_fd_read, &dummy, 1) < 0) { + /* ignore read error */ + } + + if (ctx->shutdown) { + pthread_mutex_unlock(&ctx->mutex); + return -1; + } + } + + for (i = 0; i < CUBEB_STREAM_MAX; ++i) { + stm = ctx->streams[i]; + /* We can't use snd_pcm_poll_descriptors_revents here because of + https://github.com/kinetiknz/cubeb/issues/135. */ + 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_process_stream(stm); + pthread_mutex_lock(&ctx->mutex); + alsa_set_stream_state(stm, state); + } + } + } else if (r == 0) { + for (i = 0; i < CUBEB_STREAM_MAX; ++i) { + stm = ctx->streams[i]; + if (stm) { + if (stm->state == DRAINING && ms_since(&stm->drain_timeout) >= 0) { + alsa_set_stream_state(stm, INACTIVE); + stm->state_callback(stm, stm->user_ptr, CUBEB_STATE_DRAINED); + } else if (stm->state == RUNNING && + ms_since(&stm->last_activity) > CUBEB_WATCHDOG_MS) { + alsa_set_stream_state(stm, ERROR); + stm->state_callback(stm, stm->user_ptr, CUBEB_STATE_ERROR); + } + } + } + } + + pthread_mutex_unlock(&ctx->mutex); + + return 0; +} + +static void * +alsa_run_thread(void * context) +{ + cubeb * ctx = context; + int r; + + CUBEB_REGISTER_THREAD("cubeb rendering thread"); + + do { + r = alsa_run(ctx); + } while (r >= 0); + + CUBEB_UNREGISTER_THREAD(); + + return NULL; +} + +static snd_config_t * +get_slave_pcm_node(snd_config_t * lconf, snd_config_t * root_pcm) +{ + int r; + snd_config_t * slave_pcm; + snd_config_t * slave_def; + snd_config_t * pcm; + char const * string; + char node_name[64]; + + slave_def = NULL; + + r = WRAP(snd_config_search)(root_pcm, "slave", &slave_pcm); + if (r < 0) { + return NULL; + } + + r = WRAP(snd_config_get_string)(slave_pcm, &string); + if (r >= 0) { + r = WRAP(snd_config_search_definition)(lconf, "pcm_slave", string, + &slave_def); + if (r < 0) { + return NULL; + } + } + + do { + r = WRAP(snd_config_search)(slave_def ? slave_def : slave_pcm, "pcm", &pcm); + if (r < 0) { + break; + } + + r = WRAP(snd_config_get_string)(slave_def ? slave_def : slave_pcm, &string); + if (r < 0) { + break; + } + + r = snprintf(node_name, sizeof(node_name), "pcm.%s", string); + if (r < 0 || r > (int)sizeof(node_name)) { + break; + } + r = WRAP(snd_config_search)(lconf, node_name, &pcm); + if (r < 0) { + break; + } + + return pcm; + } while (0); + + if (slave_def) { + WRAP(snd_config_delete)(slave_def); + } + + return NULL; +} + +/* Work around PulseAudio ALSA plugin bug where the PA server forces a + higher than requested latency, but the plugin does not update its (and + ALSA's) internal state to reflect that, leading to an immediate underrun + situation. Inspired by WINE's make_handle_underrun_config. + Reference: http://mailman.alsa-project.org/pipermail/alsa-devel/2012-July/05 + */ +static snd_config_t * +init_local_config_with_workaround(char const * pcm_name) +{ + int r; + snd_config_t * lconf; + snd_config_t * pcm_node; + snd_config_t * node; + char const * string; + char node_name[64]; + + lconf = NULL; + + if (WRAP(snd_config) == NULL) { + return NULL; + } + + r = WRAP(snd_config_copy)(&lconf, WRAP(snd_config)); + if (r < 0) { + return NULL; + } + + do { + r = WRAP(snd_config_search_definition)(lconf, "pcm", pcm_name, &pcm_node); + if (r < 0) { + break; + } + + r = WRAP(snd_config_get_id)(pcm_node, &string); + if (r < 0) { + break; + } + + r = snprintf(node_name, sizeof(node_name), "pcm.%s", string); + if (r < 0 || r > (int)sizeof(node_name)) { + break; + } + r = WRAP(snd_config_search)(lconf, node_name, &pcm_node); + if (r < 0) { + break; + } + + /* If this PCM has a slave, walk the slave configurations until we reach the + * bottom. */ + while ((node = get_slave_pcm_node(lconf, pcm_node)) != NULL) { + pcm_node = node; + } + + /* Fetch the PCM node's type, and bail out if it's not the PulseAudio + * plugin. */ + r = WRAP(snd_config_search)(pcm_node, "type", &node); + if (r < 0) { + break; + } + + r = WRAP(snd_config_get_string)(node, &string); + if (r < 0) { + break; + } + + if (strcmp(string, "pulse") != 0) { + break; + } + + /* Don't clobber an explicit existing handle_underrun value, set it only + if it doesn't already exist. */ + r = WRAP(snd_config_search)(pcm_node, "handle_underrun", &node); + if (r != -ENOENT) { + break; + } + + /* Disable pcm_pulse's asynchronous underrun handling. */ + r = WRAP(snd_config_imake_integer)(&node, "handle_underrun", 0); + if (r < 0) { + break; + } + + r = WRAP(snd_config_add)(pcm_node, node); + if (r < 0) { + break; + } + + return lconf; + } while (0); + + WRAP(snd_config_delete)(lconf); + + return NULL; +} + +static int +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 = WRAP(snd_pcm_open_lconf)(pcm, pcm_name, stream, SND_PCM_NONBLOCK, + local_config); + } else { + r = WRAP(snd_pcm_open)(pcm, pcm_name, stream, SND_PCM_NONBLOCK); + } + pthread_mutex_unlock(&cubeb_alsa_mutex); + + return r; +} + +static int +alsa_locked_pcm_close(snd_pcm_t * pcm) +{ + int r; + + pthread_mutex_lock(&cubeb_alsa_mutex); + r = WRAP(snd_pcm_close)(pcm); + pthread_mutex_unlock(&cubeb_alsa_mutex); + + return r; +} + +static int +alsa_register_stream(cubeb * ctx, cubeb_stream * stm) +{ + int i; + + pthread_mutex_lock(&ctx->mutex); + for (i = 0; i < CUBEB_STREAM_MAX; ++i) { + if (!ctx->streams[i]) { + ctx->streams[i] = stm; + break; + } + } + pthread_mutex_unlock(&ctx->mutex); + + return i == CUBEB_STREAM_MAX; +} + +static void +alsa_unregister_stream(cubeb_stream * stm) +{ + cubeb * ctx; + int i; + + ctx = stm->context; + + pthread_mutex_lock(&ctx->mutex); + for (i = 0; i < CUBEB_STREAM_MAX; ++i) { + if (ctx->streams[i] == stm) { + ctx->streams[i] = NULL; + break; + } + } + pthread_mutex_unlock(&ctx->mutex); +} + +static void +silent_error_handler(char const * file, int line, char const * function, + int err, char const * fmt, ...) +{ + (void)file; + (void)line; + (void)function; + (void)err; + (void)fmt; +} + +/*static*/ int +alsa_init(cubeb ** context, char const * context_name) +{ + (void)context_name; + void * libasound = NULL; + cubeb * ctx; + int r; + int i; + int fd[2]; + pthread_attr_t attr; + snd_pcm_t * dummy; + + 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) { + WRAP(snd_lib_error_set_handler)(silent_error_handler); + cubeb_alsa_error_handler_set = 1; + } + pthread_mutex_unlock(&cubeb_alsa_mutex); + + ctx = calloc(1, sizeof(*ctx)); + assert(ctx); + + ctx->ops = &alsa_ops; + ctx->libasound = libasound; + + r = pthread_mutex_init(&ctx->mutex, NULL); + assert(r == 0); + + r = pipe(fd); + assert(r == 0); + + for (i = 0; i < 2; ++i) { + fcntl(fd[i], F_SETFD, fcntl(fd[i], F_GETFD) | FD_CLOEXEC); + fcntl(fd[i], F_SETFL, fcntl(fd[i], F_GETFL) | O_NONBLOCK); + } + + ctx->control_fd_read = fd[0]; + ctx->control_fd_write = fd[1]; + + /* Force an early rebuild when alsa_run is first called to ensure fds and + nfds have been initialized. */ + ctx->rebuild = 1; + + r = pthread_attr_init(&attr); + assert(r == 0); + + r = pthread_attr_setstacksize(&attr, 256 * 1024); + assert(r == 0); + + r = pthread_create(&ctx->thread, &attr, alsa_run_thread, ctx); + assert(r == 0); + + r = pthread_attr_destroy(&attr); + assert(r == 0); + + /* 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, CUBEB_ALSA_PCM_NAME, SND_PCM_STREAM_PLAYBACK, + NULL); + if (r >= 0) { + alsa_locked_pcm_close(dummy); + } + ctx->is_pa = 0; + pthread_mutex_lock(&cubeb_alsa_mutex); + ctx->local_config = init_local_config_with_workaround(CUBEB_ALSA_PCM_NAME); + pthread_mutex_unlock(&cubeb_alsa_mutex); + if (ctx->local_config) { + ctx->is_pa = 1; + 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); + WRAP(snd_config_delete)(ctx->local_config); + pthread_mutex_unlock(&cubeb_alsa_mutex); + ctx->local_config = NULL; + } else if (r >= 0) { + alsa_locked_pcm_close(dummy); + } + } + + *context = ctx; + + return CUBEB_OK; +} + +static char const * +alsa_get_backend_id(cubeb * ctx) +{ + (void)ctx; + return "alsa"; +} + +static void +alsa_destroy(cubeb * ctx) +{ + int r; + + assert(ctx); + + pthread_mutex_lock(&ctx->mutex); + ctx->shutdown = 1; + poll_wake(ctx); + pthread_mutex_unlock(&ctx->mutex); + + r = pthread_join(ctx->thread, NULL); + assert(r == 0); + + close(ctx->control_fd_read); + close(ctx->control_fd_write); + pthread_mutex_destroy(&ctx->mutex); + free(ctx->fds); + + if (ctx->local_config) { + pthread_mutex_lock(&cubeb_alsa_mutex); + WRAP(snd_config_delete)(ctx->local_config); + pthread_mutex_unlock(&cubeb_alsa_mutex); + } +#ifndef DISABLE_LIBASOUND_DLOPEN + if (ctx->libasound) { + dlclose(ctx->libasound); + } +#endif + free(ctx); +} + +static void +alsa_stream_destroy(cubeb_stream * stm); + +static int +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; + int r; + 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); + + *stream = NULL; + + if (stream_params->prefs & CUBEB_STREAM_PREF_LOOPBACK) { + return CUBEB_ERROR_NOT_SUPPORTED; + } + + switch (stream_params->format) { + case CUBEB_SAMPLE_S16LE: + format = SND_PCM_FORMAT_S16_LE; + break; + case CUBEB_SAMPLE_S16BE: + format = SND_PCM_FORMAT_S16_BE; + break; + case CUBEB_SAMPLE_FLOAT32LE: + format = SND_PCM_FORMAT_FLOAT_LE; + break; + case CUBEB_SAMPLE_FLOAT32BE: + format = SND_PCM_FORMAT_FLOAT_BE; + break; + default: + return CUBEB_ERROR_INVALID_FORMAT; + } + + pthread_mutex_lock(&ctx->mutex); + if (ctx->active_streams >= CUBEB_STREAM_MAX) { + pthread_mutex_unlock(&ctx->mutex); + return CUBEB_ERROR; + } + ctx->active_streams += 1; + pthread_mutex_unlock(&ctx->mutex); + + 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->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 = 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 = WRAP(snd_pcm_nonblock)(stm->pcm, 1); + assert(r == 0); + + latency_us = latency_frames * 1e6 / stm->params.rate; + + /* Ugly hack: the PA ALSA plugin allows buffer configurations that can't + possibly work. See https://bugzilla.mozilla.org/show_bug.cgi?id=761274. + Only resort to this hack if the handle_underrun workaround failed. */ + if (!ctx->local_config && ctx->is_pa) { + const int min_latency = 5e5; + latency_us = latency_us < min_latency ? min_latency : latency_us; + } + + 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) { + alsa_stream_destroy(stm); + return CUBEB_ERROR_INVALID_FORMAT; + } + + r = WRAP(snd_pcm_get_params)(stm->pcm, &stm->buffer_size, &period_size); + assert(r == 0); + + /* 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 = WRAP(snd_pcm_poll_descriptors)(stm->pcm, stm->saved_fds, stm->nfds); + assert((nfds_t)r == stm->nfds); + + if (alsa_register_stream(ctx, stm) != 0) { + alsa_stream_destroy(stm); + return CUBEB_ERROR; + } + + *stream = stm; + + 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) +{ + int r; + cubeb * ctx; + + assert(stm && (stm->state == INACTIVE || stm->state == ERROR || + stm->state == DRAINING)); + + 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) { + WRAP(snd_pcm_drain)(stm->pcm); + } + alsa_locked_pcm_close(stm->pcm); + stm->pcm = NULL; + } + free(stm->saved_fds); + pthread_mutex_unlock(&stm->mutex); + pthread_mutex_destroy(&stm->mutex); + + r = pthread_cond_destroy(&stm->cond); + assert(r == 0); + + alsa_unregister_stream(stm); + + pthread_mutex_lock(&ctx->mutex); + assert(ctx->active_streams >= 1); + ctx->active_streams -= 1; + pthread_mutex_unlock(&ctx->mutex); + + free(stm->buffer); + + free(stm); +} + +static int +alsa_get_max_channel_count(cubeb * ctx, uint32_t * max_channels) +{ + int r; + cubeb_stream * stm; + snd_pcm_hw_params_t * hw_params; + cubeb_stream_params params; + params.rate = 44100; + params.format = CUBEB_SAMPLE_FLOAT32NE; + params.channels = 2; + + snd_pcm_hw_params_alloca(&hw_params); + + assert(ctx); + + r = alsa_stream_init(ctx, &stm, "", NULL, NULL, NULL, ¶ms, 100, NULL, + NULL, NULL); + if (r != CUBEB_OK) { + return CUBEB_ERROR; + } + + assert(stm); + + r = WRAP(snd_pcm_hw_params_any)(stm->pcm, hw_params); + if (r < 0) { + return CUBEB_ERROR; + } + + r = WRAP(snd_pcm_hw_params_get_channels_max)(hw_params, max_channels); + if (r < 0) { + return CUBEB_ERROR; + } + + alsa_stream_destroy(stm); + + return CUBEB_OK; +} + +static int +alsa_get_preferred_sample_rate(cubeb * ctx, uint32_t * rate) +{ + (void)ctx; + int r, dir; + snd_pcm_t * pcm; + snd_pcm_hw_params_t * hw_params; + + snd_pcm_hw_params_alloca(&hw_params); + + /* get a pcm, disabling resampling, so we get a rate the + * hardware/dmix/pulse/etc. supports. */ + 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 = WRAP(snd_pcm_hw_params_any)(pcm, hw_params); + if (r < 0) { + WRAP(snd_pcm_close)(pcm); + return CUBEB_ERROR; + } + + r = WRAP(snd_pcm_hw_params_get_rate)(hw_params, rate, &dir); + if (r >= 0) { + /* There is a default rate: use it. */ + WRAP(snd_pcm_close)(pcm); + return CUBEB_OK; + } + + /* Use a common rate, alsa may adjust it based on hw/etc. capabilities. */ + *rate = 44100; + + r = WRAP(snd_pcm_hw_params_set_rate_near)(pcm, hw_params, rate, NULL); + if (r < 0) { + WRAP(snd_pcm_close)(pcm); + return CUBEB_ERROR; + } + + WRAP(snd_pcm_close)(pcm); + + return CUBEB_OK; +} + +static int +alsa_get_min_latency(cubeb * ctx, cubeb_stream_params params, + uint32_t * latency_frames) +{ + (void)ctx; + /* 40ms is found to be an acceptable minimum, even on a super low-end + * machine. */ + *latency_frames = 40 * params.rate / 1000; + + return CUBEB_OK; +} + +static int +alsa_stream_start(cubeb_stream * stm) +{ + cubeb * ctx; + + 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); + /* 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); + + pthread_mutex_lock(&ctx->mutex); + if (stm->state != INACTIVE) { + pthread_mutex_unlock(&ctx->mutex); + return CUBEB_ERROR; + } + alsa_set_stream_state(stm, RUNNING); + pthread_mutex_unlock(&ctx->mutex); + + return CUBEB_OK; +} + +static int +alsa_stream_stop(cubeb_stream * stm) +{ + cubeb * ctx; + int r; + + 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); + assert(r == 0); + } + + alsa_set_stream_state(stm, INACTIVE); + pthread_mutex_unlock(&ctx->mutex); + + pthread_mutex_lock(&stm->mutex); + WRAP(snd_pcm_pause)(stm->pcm, 1); + pthread_mutex_unlock(&stm->mutex); + + return CUBEB_OK; +} + +static int +alsa_stream_get_position(cubeb_stream * stm, uint64_t * position) +{ + snd_pcm_sframes_t delay; + + assert(stm && position); + + pthread_mutex_lock(&stm->mutex); + + delay = -1; + 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; + } + + assert(delay >= 0); + + *position = 0; + if (stm->stream_position >= (snd_pcm_uframes_t)delay) { + *position = stm->stream_position - delay; + } + + stm->last_position = *position; + + pthread_mutex_unlock(&stm->mutex); + return CUBEB_OK; +} + +static int +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 (WRAP(snd_pcm_delay)(stm->pcm, &delay)) { + return CUBEB_ERROR; + } + + *latency = delay; + + return CUBEB_OK; +} + +static int +alsa_stream_set_volume(cubeb_stream * stm, float volume) +{ + /* setting the volume using an API call does not seem very stable/supported */ + pthread_mutex_lock(&stm->mutex); + stm->volume = volume; + pthread_mutex_unlock(&stm->mutex); + + 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, + .get_supported_input_processing_params = 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_get_position = alsa_stream_get_position, + .stream_get_latency = alsa_stream_get_latency, + .stream_get_input_latency = NULL, + .stream_set_volume = alsa_stream_set_volume, + .stream_set_name = NULL, + .stream_get_current_device = NULL, + .stream_set_input_mute = NULL, + .stream_set_input_processing_params = NULL, + .stream_device_destroy = NULL, + .stream_register_device_changed_callback = NULL, + .register_device_collection_changed = NULL}; diff --git a/media/libcubeb/src/cubeb_android.h b/media/libcubeb/src/cubeb_android.h new file mode 100644 index 0000000000..c21a941ab5 --- /dev/null +++ b/media/libcubeb/src/cubeb_android.h @@ -0,0 +1,17 @@ +#ifndef CUBEB_ANDROID_H +#define CUBEB_ANDROID_H + +#ifdef __cplusplus +extern "C" { +#endif +// 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 +const uint16_t POWERSAVE_LATENCY_FRAMES_THRESHOLD = 4000; + +#ifdef __cplusplus +}; +#endif + +#endif // CUBEB_ANDROID_H diff --git a/media/libcubeb/src/cubeb_array_queue.h b/media/libcubeb/src/cubeb_array_queue.h new file mode 100644 index 0000000000..d6d9581325 --- /dev/null +++ b/media/libcubeb/src/cubeb_array_queue.h @@ -0,0 +1,99 @@ +/* + * 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_audiounit.cpp b/media/libcubeb/src/cubeb_audiounit.cpp new file mode 100644 index 0000000000..d823e80ff8 --- /dev/null +++ b/media/libcubeb/src/cubeb_audiounit.cpp @@ -0,0 +1,3708 @@ +/* + * Copyright © 2011 Mozilla Foundation + * + * This program is made available under an ISC-style license. See the + * accompanying file LICENSE for details. + */ +#undef NDEBUG + +#include <AudioUnit/AudioUnit.h> +#include <TargetConditionals.h> +#include <assert.h> +#include <mach/mach_time.h> +#include <pthread.h> +#include <stdlib.h> +#if !TARGET_OS_IPHONE +#include <AvailabilityMacros.h> +#include <CoreAudio/AudioHardware.h> +#include <CoreAudio/HostTime.h> +#include <CoreFoundation/CoreFoundation.h> +#endif +#include "cubeb-internal.h" +#include "cubeb/cubeb.h" +#include "cubeb_mixer.h" +#include <AudioToolbox/AudioToolbox.h> +#include <CoreAudio/CoreAudioTypes.h> +#if !TARGET_OS_IPHONE +#include "cubeb_osx_run_loop.h" +#endif +#include "cubeb_resampler.h" +#include "cubeb_ring_array.h" +#include <algorithm> +#include <atomic> +#include <set> +#include <string> +#include <sys/time.h> +#include <vector> + +using namespace std; + +#if MAC_OS_X_VERSION_MIN_REQUIRED < 101000 +typedef UInt32 AudioFormatFlags; +#endif + +#define AU_OUT_BUS 0 +#define AU_IN_BUS 1 + +const char * DISPATCH_QUEUE_LABEL = "org.mozilla.cubeb"; +const char * PRIVATE_AGGREGATE_DEVICE_NAME = "CubebAggregateDevice"; + +#ifdef ALOGV +#undef ALOGV +#endif +#define ALOGV(msg, ...) \ + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), \ + ^{ \ + LOGV(msg, ##__VA_ARGS__); \ + }) + +#ifdef ALOG +#undef ALOG +#endif +#define ALOG(msg, ...) \ + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), \ + ^{ \ + LOG(msg, ##__VA_ARGS__); \ + }) + +/* Testing empirically, some headsets report a minimal latency that is very + * low, but this does not work in practice. Lie and say the minimum is 256 + * frames. */ +const uint32_t SAFE_MIN_LATENCY_FRAMES = 128; +const uint32_t SAFE_MAX_LATENCY_FRAMES = 512; + +const AudioObjectPropertyAddress DEFAULT_INPUT_DEVICE_PROPERTY_ADDRESS = { + kAudioHardwarePropertyDefaultInputDevice, kAudioObjectPropertyScopeGlobal, + kAudioObjectPropertyElementMaster}; + +const AudioObjectPropertyAddress DEFAULT_OUTPUT_DEVICE_PROPERTY_ADDRESS = { + kAudioHardwarePropertyDefaultOutputDevice, kAudioObjectPropertyScopeGlobal, + kAudioObjectPropertyElementMaster}; + +const AudioObjectPropertyAddress DEVICE_IS_ALIVE_PROPERTY_ADDRESS = { + kAudioDevicePropertyDeviceIsAlive, kAudioObjectPropertyScopeGlobal, + kAudioObjectPropertyElementMaster}; + +const AudioObjectPropertyAddress DEVICES_PROPERTY_ADDRESS = { + kAudioHardwarePropertyDevices, kAudioObjectPropertyScopeGlobal, + kAudioObjectPropertyElementMaster}; + +const AudioObjectPropertyAddress INPUT_DATA_SOURCE_PROPERTY_ADDRESS = { + kAudioDevicePropertyDataSource, kAudioDevicePropertyScopeInput, + kAudioObjectPropertyElementMaster}; + +const AudioObjectPropertyAddress OUTPUT_DATA_SOURCE_PROPERTY_ADDRESS = { + kAudioDevicePropertyDataSource, kAudioDevicePropertyScopeOutput, + kAudioObjectPropertyElementMaster}; + +typedef uint32_t device_flags_value; + +enum device_flags { + DEV_UNKNOWN = 0x00, /* Unknown */ + DEV_INPUT = 0x01, /* Record device like mic */ + DEV_OUTPUT = 0x02, /* Playback device like speakers */ + DEV_SYSTEM_DEFAULT = 0x04, /* System default device */ + DEV_SELECTED_DEFAULT = + 0x08, /* User selected to use the system default device */ +}; + +void +audiounit_stream_stop_internal(cubeb_stream * stm); +static int +audiounit_stream_start_internal(cubeb_stream * stm); +static void +audiounit_close_stream(cubeb_stream * stm); +static int +audiounit_setup_stream(cubeb_stream * stm); +static vector<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 = &audiounit_ops; + owned_critical_section mutex; + int active_streams = 0; + uint32_t global_latency_frames = 0; + cubeb_device_collection_changed_callback input_collection_changed_callback = + nullptr; + void * input_collection_changed_user_ptr = nullptr; + cubeb_device_collection_changed_callback output_collection_changed_callback = + nullptr; + void * output_collection_changed_user_ptr = nullptr; + // Store list of devices to detect changes + vector<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; +}; + +static unique_ptr<AudioChannelLayout, decltype(&free)> +make_sized_audio_channel_layout(size_t sz) +{ + 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); +} + +enum class io_side { + INPUT, + OUTPUT, +}; + +static char const * +to_string(io_side side) +{ + switch (side) { + case io_side::INPUT: + return "input"; + case io_side::OUTPUT: + return "output"; + } +} + +struct device_info { + AudioDeviceID id = kAudioObjectUnknown; + device_flags_value flags = DEV_UNKNOWN; +}; + +struct property_listener { + AudioDeviceID device_id; + const AudioObjectPropertyAddress * property_address; + AudioObjectPropertyListenerProc callback; + cubeb_stream * stream; + + property_listener(AudioDeviceID id, + const AudioObjectPropertyAddress * address, + AudioObjectPropertyListenerProc proc, cubeb_stream * stm) + : device_id(id), property_address(address), callback(proc), stream(stm) + { + } +}; + +struct cubeb_stream { + explicit cubeb_stream(cubeb * context); + + /* Note: Must match cubeb_stream layout in cubeb.c. */ + cubeb * context; + void * user_ptr = nullptr; + /**/ + + cubeb_data_callback data_callback = nullptr; + cubeb_state_callback state_callback = nullptr; + cubeb_device_changed_callback device_changed_callback = nullptr; + owned_critical_section device_changed_callback_lock; + /* Stream creation parameters */ + cubeb_stream_params input_stream_params = {CUBEB_SAMPLE_FLOAT32NE, 0, 0, + CUBEB_LAYOUT_UNDEFINED, + CUBEB_STREAM_PREF_NONE}; + cubeb_stream_params output_stream_params = {CUBEB_SAMPLE_FLOAT32NE, 0, 0, + CUBEB_LAYOUT_UNDEFINED, + CUBEB_STREAM_PREF_NONE}; + device_info input_device; + device_info output_device; + /* Format descriptions */ + AudioStreamBasicDescription input_desc; + AudioStreamBasicDescription output_desc; + /* I/O AudioUnits */ + AudioUnit input_unit = nullptr; + AudioUnit output_unit = nullptr; + /* I/O device sample rate */ + Float64 input_hw_rate = 0; + Float64 output_hw_rate = 0; + /* Expected I/O thread interleave, + * calculated from I/O hw rate. */ + int expected_output_callbacks_in_a_row = 0; + owned_critical_section mutex; + // Hold the input samples in every input callback iteration. + // Only accessed on input/output callback thread and during initial configure. + unique_ptr<auto_array_wrapper> input_linear_buffer; + /* Frame counters */ + 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 = 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. */ + 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) +{ + return stm->input_stream_params.rate != 0; +} + +bool +has_output(cubeb_stream * stm) +{ + return stm->output_stream_params.rate != 0; +} + +cubeb_channel +channel_label_to_cubeb_channel(UInt32 label) +{ + switch (label) { + case kAudioChannelLabel_Left: + return CHANNEL_FRONT_LEFT; + case kAudioChannelLabel_Right: + return CHANNEL_FRONT_RIGHT; + case kAudioChannelLabel_Center: + return CHANNEL_FRONT_CENTER; + case kAudioChannelLabel_LFEScreen: + return CHANNEL_LOW_FREQUENCY; + case kAudioChannelLabel_LeftSurround: + return CHANNEL_BACK_LEFT; + case kAudioChannelLabel_RightSurround: + return CHANNEL_BACK_RIGHT; + case kAudioChannelLabel_LeftCenter: + return CHANNEL_FRONT_LEFT_OF_CENTER; + case kAudioChannelLabel_RightCenter: + return CHANNEL_FRONT_RIGHT_OF_CENTER; + case kAudioChannelLabel_CenterSurround: + return CHANNEL_BACK_CENTER; + case kAudioChannelLabel_LeftSurroundDirect: + return CHANNEL_SIDE_LEFT; + case kAudioChannelLabel_RightSurroundDirect: + return CHANNEL_SIDE_RIGHT; + case kAudioChannelLabel_TopCenterSurround: + return CHANNEL_TOP_CENTER; + case kAudioChannelLabel_VerticalHeightLeft: + return CHANNEL_TOP_FRONT_LEFT; + case kAudioChannelLabel_VerticalHeightCenter: + return CHANNEL_TOP_FRONT_CENTER; + case kAudioChannelLabel_VerticalHeightRight: + return CHANNEL_TOP_FRONT_RIGHT; + case kAudioChannelLabel_TopBackLeft: + return CHANNEL_TOP_BACK_LEFT; + case kAudioChannelLabel_TopBackCenter: + return CHANNEL_TOP_BACK_CENTER; + case kAudioChannelLabel_TopBackRight: + return CHANNEL_TOP_BACK_RIGHT; + default: + return CHANNEL_UNKNOWN; + } +} + +AudioChannelLabel +cubeb_channel_to_channel_label(cubeb_channel channel) +{ + switch (channel) { + case CHANNEL_FRONT_LEFT: + return kAudioChannelLabel_Left; + case CHANNEL_FRONT_RIGHT: + return kAudioChannelLabel_Right; + case CHANNEL_FRONT_CENTER: + return kAudioChannelLabel_Center; + case CHANNEL_LOW_FREQUENCY: + return kAudioChannelLabel_LFEScreen; + case CHANNEL_BACK_LEFT: + return kAudioChannelLabel_LeftSurround; + case CHANNEL_BACK_RIGHT: + return kAudioChannelLabel_RightSurround; + case CHANNEL_FRONT_LEFT_OF_CENTER: + return kAudioChannelLabel_LeftCenter; + case CHANNEL_FRONT_RIGHT_OF_CENTER: + return kAudioChannelLabel_RightCenter; + case CHANNEL_BACK_CENTER: + return kAudioChannelLabel_CenterSurround; + case CHANNEL_SIDE_LEFT: + return kAudioChannelLabel_LeftSurroundDirect; + case CHANNEL_SIDE_RIGHT: + return kAudioChannelLabel_RightSurroundDirect; + case CHANNEL_TOP_CENTER: + return kAudioChannelLabel_TopCenterSurround; + case CHANNEL_TOP_FRONT_LEFT: + return kAudioChannelLabel_VerticalHeightLeft; + case CHANNEL_TOP_FRONT_CENTER: + return kAudioChannelLabel_VerticalHeightCenter; + case CHANNEL_TOP_FRONT_RIGHT: + return kAudioChannelLabel_VerticalHeightRight; + case CHANNEL_TOP_BACK_LEFT: + return kAudioChannelLabel_TopBackLeft; + case CHANNEL_TOP_BACK_CENTER: + return kAudioChannelLabel_TopBackCenter; + case CHANNEL_TOP_BACK_RIGHT: + return kAudioChannelLabel_TopBackRight; + default: + return kAudioChannelLabel_Unknown; + } +} + +bool +is_common_sample_rate(Float64 sample_rate) +{ + /* Some commonly used sample rates and their multiples and divisors. */ + return sample_rate == 8000 || sample_rate == 16000 || sample_rate == 22050 || + sample_rate == 32000 || sample_rate == 44100 || sample_rate == 48000 || + sample_rate == 88200 || sample_rate == 96000; +} + +#if TARGET_OS_IPHONE +typedef UInt32 AudioDeviceID; +typedef UInt32 AudioObjectID; + +#define AudioGetCurrentHostTime mach_absolute_time + +#endif + +uint64_t +ConvertHostTimeToNanos(uint64_t host_time) +{ + static struct mach_timebase_info timebase_info; + static bool initialized = false; + if (!initialized) { + mach_timebase_info(&timebase_info); + initialized = true; + } + + long double answer = host_time; + if (timebase_info.numer != timebase_info.denom) { + answer *= timebase_info.numer; + answer /= timebase_info.denom; + } + return (uint64_t)answer; +} + +static void +audiounit_increment_active_streams(cubeb * ctx) +{ + ctx->mutex.assert_current_thread_owns(); + ctx->active_streams += 1; +} + +static void +audiounit_decrement_active_streams(cubeb * ctx) +{ + ctx->mutex.assert_current_thread_owns(); + ctx->active_streams -= 1; +} + +static int +audiounit_active_streams(cubeb * ctx) +{ + ctx->mutex.assert_current_thread_owns(); + return ctx->active_streams; +} + +static void +audiounit_set_global_latency(cubeb * ctx, uint32_t latency_frames) +{ + ctx->mutex.assert_current_thread_owns(); + assert(audiounit_active_streams(ctx) == 1); + ctx->global_latency_frames = latency_frames; +} + +static void +audiounit_make_silent(AudioBuffer * ioData) +{ + assert(ioData); + assert(ioData->mData); + memset(ioData->mData, 0, ioData->mDataByteSize); +} + +static OSStatus +audiounit_render_input(cubeb_stream * stm, AudioUnitRenderActionFlags * flags, + AudioTimeStamp const * tstamp, UInt32 bus, + UInt32 input_frames) +{ + /* Create the AudioBufferList to store input. */ + AudioBufferList input_buffer_list; + input_buffer_list.mBuffers[0].mDataByteSize = + stm->input_desc.mBytesPerFrame * input_frames; + input_buffer_list.mBuffers[0].mData = nullptr; + input_buffer_list.mBuffers[0].mNumberChannels = + stm->input_desc.mChannelsPerFrame; + input_buffer_list.mNumberBuffers = 1; + + /* Render input samples */ + OSStatus r = AudioUnitRender(stm->input_unit, flags, tstamp, bus, + input_frames, &input_buffer_list); + + if (r != noErr) { + LOG("AudioUnitRender rv=%d", r); + if (r != kAudioUnitErr_CannotDoInCurrentContext) { + return r; + } + if (stm->output_unit) { + // kAudioUnitErr_CannotDoInCurrentContext is returned when using a BT + // headset and the profile is changed from A2DP to HFP/HSP. The previous + // output device is no longer valid and must be reset. + audiounit_reinit_stream_async(stm, DEV_INPUT | DEV_OUTPUT); + } + // For now state that no error occurred and feed silence, stream will be + // resumed once reinit has completed. + ALOGV("(%p) input: reinit pending feeding silence instead", stm); + stm->input_linear_buffer->push_silence(input_frames * + stm->input_desc.mChannelsPerFrame); + } else { + /* Copy input data in linear buffer. */ + stm->input_linear_buffer->push(input_buffer_list.mBuffers[0].mData, + input_frames * + stm->input_desc.mChannelsPerFrame); + } + + /* Advance input frame counter. */ + assert(input_frames > 0); + stm->frames_read += input_frames; + + ALOGV("(%p) input: buffers %u, size %u, channels %u, rendered frames %d, " + "total frames %lu.", + stm, (unsigned int)input_buffer_list.mNumberBuffers, + (unsigned int)input_buffer_list.mBuffers[0].mDataByteSize, + (unsigned int)input_buffer_list.mBuffers[0].mNumberChannels, + (unsigned int)input_frames, + stm->input_linear_buffer->length() / stm->input_desc.mChannelsPerFrame); + + return noErr; +} + +static OSStatus +audiounit_input_callback(void * user_ptr, AudioUnitRenderActionFlags * flags, + AudioTimeStamp const * tstamp, UInt32 bus, + UInt32 input_frames, AudioBufferList * /* bufs */) +{ + cubeb_stream * stm = static_cast<cubeb_stream *>(user_ptr); + + assert(stm->input_unit != NULL); + assert(AU_IN_BUS == bus); + + if (stm->shutdown) { + ALOG("(%p) input shutdown", stm); + return noErr; + } + + if (stm->draining) { + OSStatus r = AudioOutputUnitStop(stm->input_unit); + assert(r == 0); + // Only fire state callback in input-only stream. For duplex stream, + // the state callback will be fired in output callback. + if (stm->output_unit == NULL) { + stm->state_callback(stm, stm->user_ptr, CUBEB_STATE_DRAINED); + } + return noErr; + } + + OSStatus r = audiounit_render_input(stm, flags, tstamp, bus, input_frames); + if (r != noErr) { + return r; + } + + // Full Duplex. We'll call data_callback in the AudioUnit output callback. + if (stm->output_unit != NULL) { + return noErr; + } + + /* Input only. Call the user callback through resampler. + Resampler will deliver input buffer in the correct rate. */ + assert(input_frames <= stm->input_linear_buffer->length() / + stm->input_desc.mChannelsPerFrame); + long total_input_frames = + stm->input_linear_buffer->length() / stm->input_desc.mChannelsPerFrame; + long outframes = cubeb_resampler_fill(stm->resampler.get(), + stm->input_linear_buffer->data(), + &total_input_frames, NULL, 0); + if (outframes < 0) { + stm->shutdown = true; + OSStatus r = AudioOutputUnitStop(stm->input_unit); + assert(r == 0); + stm->state_callback(stm, stm->user_ptr, CUBEB_STATE_ERROR); + return noErr; + } + stm->draining = outframes < total_input_frames; + + // Reset input buffer + stm->input_linear_buffer->clear(); + + return noErr; +} + +static void +audiounit_mix_output_buffer(cubeb_stream * stm, size_t output_frames, + void * input_buffer, size_t input_buffer_size, + void * output_buffer, size_t output_buffer_size) +{ + assert(input_buffer_size >= + cubeb_sample_size(stm->output_stream_params.format) * + stm->output_stream_params.channels * output_frames); + assert(output_buffer_size >= stm->output_desc.mBytesPerFrame * output_frames); + + int r = cubeb_mixer_mix(stm->mixer.get(), output_frames, input_buffer, + input_buffer_size, output_buffer, output_buffer_size); + if (r != 0) { + LOG("Remix error = %d", r); + } +} + +// Return how many input frames (sampled at input_hw_rate) are needed to provide +// output_frames (sampled at output_stream_params.rate) +static int64_t +minimum_resampling_input_frames(cubeb_stream * stm, uint32_t output_frames) +{ + if (stm->input_hw_rate == stm->output_stream_params.rate) { + // Fast path. + return output_frames; + } + return ceil(stm->input_hw_rate * output_frames / + stm->output_stream_params.rate); +} + +static OSStatus +audiounit_output_callback(void * user_ptr, + AudioUnitRenderActionFlags * /* flags */, + AudioTimeStamp const * tstamp, UInt32 bus, + UInt32 output_frames, AudioBufferList * outBufferList) +{ + assert(AU_OUT_BUS == bus); + assert(outBufferList->mNumberBuffers == 1); + + cubeb_stream * stm = static_cast<cubeb_stream *>(user_ptr); + + uint64_t now = ConvertHostTimeToNanos(mach_absolute_time()); + uint64_t audio_output_time = ConvertHostTimeToNanos(tstamp->mHostTime); + uint64_t output_latency_ns = audio_output_time - now; + + const int ns2s = 1e9; + // The total output latency is the timestamp difference + the stream latency + + // the hardware latency. + stm->total_output_latency_frames = + output_latency_ns * stm->output_hw_rate / ns2s + + stm->current_latency_frames; + + ALOGV("(%p) output: buffers %u, size %u, channels %u, frames %u, total input " + "frames %lu.", + stm, (unsigned int)outBufferList->mNumberBuffers, + (unsigned int)outBufferList->mBuffers[0].mDataByteSize, + (unsigned int)outBufferList->mBuffers[0].mNumberChannels, + (unsigned int)output_frames, + has_input(stm) ? stm->input_linear_buffer->length() / + stm->input_desc.mChannelsPerFrame + : 0); + + long input_frames = 0; + void *output_buffer = NULL, *input_buffer = NULL; + + if (stm->shutdown) { + ALOG("(%p) output shutdown.", stm); + audiounit_make_silent(&outBufferList->mBuffers[0]); + return noErr; + } + + if (stm->draining) { + OSStatus r = AudioOutputUnitStop(stm->output_unit); + assert(r == 0); + stm->state_callback(stm, stm->user_ptr, CUBEB_STATE_DRAINED); + audiounit_make_silent(&outBufferList->mBuffers[0]); + return noErr; + } + + /* Get output buffer. */ + if (stm->mixer) { + // If remixing needs to occur, we can't directly work in our final + // destination buffer as data may be overwritten or too small to start with. + size_t size_needed = output_frames * stm->output_stream_params.channels * + cubeb_sample_size(stm->output_stream_params.format); + if (stm->temp_buffer_size < size_needed) { + stm->temp_buffer.reset(new uint8_t[size_needed]); + stm->temp_buffer_size = size_needed; + } + output_buffer = stm->temp_buffer.get(); + } else { + output_buffer = outBufferList->mBuffers[0].mData; + } + + stm->frames_written += output_frames; + + /* If Full duplex get also input buffer */ + if (stm->input_unit != NULL) { + /* If the output callback came first and this is a duplex stream, we need to + * fill in some additional silence in the resampler. + * Otherwise, if we had more than expected callbacks in a row, or we're + * currently switching, we add some silence as well to compensate for the + * fact that we're lacking some input data. */ + uint32_t input_frames_needed = + minimum_resampling_input_frames(stm, stm->frames_written); + long missing_frames = input_frames_needed - stm->frames_read; + if (missing_frames > 0) { + stm->input_linear_buffer->push_silence(missing_frames * + stm->input_desc.mChannelsPerFrame); + stm->frames_read = input_frames_needed; + + ALOG("(%p) %s pushed %ld frames of input silence.", stm, + stm->frames_read == 0 ? "Input hasn't started," + : stm->switching_device ? "Device switching," + : "Drop out,", + missing_frames); + } + input_buffer = stm->input_linear_buffer->data(); + // Number of input frames in the buffer. It will change to actually used + // frames inside fill + input_frames = + stm->input_linear_buffer->length() / stm->input_desc.mChannelsPerFrame; + } + + /* Call user callback through resampler. */ + long outframes = cubeb_resampler_fill(stm->resampler.get(), input_buffer, + input_buffer ? &input_frames : NULL, + output_buffer, output_frames); + + if (input_buffer) { + // Pop from the buffer the frames used by the the resampler. + stm->input_linear_buffer->pop(input_frames * + stm->input_desc.mChannelsPerFrame); + } + + if (outframes < 0 || outframes > output_frames) { + stm->shutdown = true; + OSStatus r = AudioOutputUnitStop(stm->output_unit); + assert(r == 0); + if (stm->input_unit) { + r = AudioOutputUnitStop(stm->input_unit); + assert(r == 0); + } + stm->state_callback(stm, stm->user_ptr, CUBEB_STATE_ERROR); + audiounit_make_silent(&outBufferList->mBuffers[0]); + return noErr; + } + + stm->draining = (UInt32)outframes < output_frames; + stm->frames_played = stm->frames_queued; + stm->frames_queued += outframes; + + /* Post process output samples. */ + if (stm->draining) { + /* Clear missing frames (silence) */ + size_t channels = stm->output_stream_params.channels; + size_t missing_samples = (output_frames - outframes) * channels; + size_t size_sample = cubeb_sample_size(stm->output_stream_params.format); + /* number of bytes that have been filled with valid audio by the callback. + */ + size_t audio_byte_count = outframes * channels * size_sample; + PodZero((uint8_t *)output_buffer + audio_byte_count, + missing_samples * size_sample); + } + + /* Mixing */ + if (stm->mixer) { + audiounit_mix_output_buffer(stm, output_frames, output_buffer, + stm->temp_buffer_size, + outBufferList->mBuffers[0].mData, + outBufferList->mBuffers[0].mDataByteSize); + } + + return noErr; +} + +extern "C" { +int +audiounit_init(cubeb ** context, char const * /* context_name */) +{ +#if !TARGET_OS_IPHONE + cubeb_set_coreaudio_notification_runloop(); +#endif + + *context = new cubeb; + + return CUBEB_OK; +} +} + +static char const * +audiounit_get_backend_id(cubeb * /* ctx */) +{ + return "audiounit"; +} + +#if !TARGET_OS_IPHONE + +static int +audiounit_stream_get_volume(cubeb_stream * stm, float * volume); +static int +audiounit_stream_set_volume(cubeb_stream * stm, float volume); + +static int +audiounit_set_device_info(cubeb_stream * stm, AudioDeviceID id, io_side side) +{ + assert(stm); + + device_info * info = nullptr; + cubeb_device_type type = CUBEB_DEVICE_TYPE_UNKNOWN; + + if (side == io_side::INPUT) { + info = &stm->input_device; + type = CUBEB_DEVICE_TYPE_INPUT; + } else if (side == io_side::OUTPUT) { + info = &stm->output_device; + type = CUBEB_DEVICE_TYPE_OUTPUT; + } + memset(info, 0, sizeof(device_info)); + info->id = id; + + if (side == io_side::INPUT) { + info->flags |= DEV_INPUT; + } else if (side == io_side::OUTPUT) { + info->flags |= DEV_OUTPUT; + } + + AudioDeviceID default_device_id = audiounit_get_default_device_id(type); + if (default_device_id == kAudioObjectUnknown) { + return CUBEB_ERROR; + } + if (id == kAudioObjectUnknown) { + info->id = default_device_id; + info->flags |= DEV_SELECTED_DEFAULT; + } + + if (info->id == default_device_id) { + info->flags |= DEV_SYSTEM_DEFAULT; + } + + assert(info->id); + assert(info->flags & DEV_INPUT && !(info->flags & DEV_OUTPUT) || + !(info->flags & DEV_INPUT) && info->flags & DEV_OUTPUT); + + return CUBEB_OK; +} + +static int +audiounit_reinit_stream(cubeb_stream * stm, device_flags_value flags) +{ + auto_lock context_lock(stm->context->mutex); + assert((flags & DEV_INPUT && stm->input_unit) || + (flags & DEV_OUTPUT && stm->output_unit)); + if (!stm->shutdown) { + audiounit_stream_stop_internal(stm); + } + + int r = audiounit_uninstall_device_changed_callback(stm); + if (r != CUBEB_OK) { + LOG("(%p) Could not uninstall all device change listeners.", stm); + } + + { + auto_lock lock(stm->mutex); + float volume = 0.0; + int vol_rv = CUBEB_ERROR; + if (stm->output_unit) { + vol_rv = audiounit_stream_get_volume(stm, &volume); + } + + audiounit_close_stream(stm); + + /* Reinit occurs in one of the following case: + * - When the device is not alive any more + * - When the default system device change. + * - The bluetooth device changed from A2DP to/from HFP/HSP profile + * We first attempt to re-use the same device id, should that fail we will + * default to the (potentially new) default device. */ + AudioDeviceID input_device = + flags & DEV_INPUT ? stm->input_device.id : kAudioObjectUnknown; + if (flags & DEV_INPUT) { + r = audiounit_set_device_info(stm, input_device, io_side::INPUT); + if (r != CUBEB_OK) { + LOG("(%p) Set input device info failed. This can happen when last " + "media device is unplugged", + stm); + return CUBEB_ERROR; + } + } + + /* Always use the default output on reinit. This is not correct in every + * case but it is sufficient for Firefox and prevent reinit from reporting + * failures. It will change soon when reinit mechanism will be updated. */ + r = audiounit_set_device_info(stm, kAudioObjectUnknown, io_side::OUTPUT); + if (r != CUBEB_OK) { + LOG("(%p) Set output device info failed. This can happen when last media " + "device is unplugged", + stm); + return CUBEB_ERROR; + } + + if (audiounit_setup_stream(stm) != CUBEB_OK) { + LOG("(%p) Stream reinit failed.", stm); + if (flags & DEV_INPUT && input_device != kAudioObjectUnknown) { + // Attempt to re-use the same device-id failed, so attempt again with + // default input device. + audiounit_close_stream(stm); + if (audiounit_set_device_info(stm, kAudioObjectUnknown, + io_side::INPUT) != CUBEB_OK || + audiounit_setup_stream(stm) != CUBEB_OK) { + LOG("(%p) Second stream reinit failed.", stm); + return CUBEB_ERROR; + } + } + } + + if (vol_rv == CUBEB_OK) { + audiounit_stream_set_volume(stm, volume); + } + + // If the stream was running, start it again. + if (!stm->shutdown) { + r = audiounit_stream_start_internal(stm); + if (r != CUBEB_OK) { + return CUBEB_ERROR; + } + } + } + return CUBEB_OK; +} + +static void +audiounit_reinit_stream_async(cubeb_stream * stm, device_flags_value flags) +{ + if (std::atomic_exchange(&stm->reinit_pending, true)) { + // A reinit task is already pending, nothing more to do. + ALOG("(%p) re-init stream task already pending, cancelling request", stm); + return; + } + + // Use a new thread, through the queue, to avoid deadlock when calling + // Get/SetProperties method from inside notify callback + dispatch_async(stm->context->serial_queue, ^() { + if (stm->destroy_pending) { + ALOG("(%p) stream pending destroy, cancelling reinit task", stm); + return; + } + + if (audiounit_reinit_stream(stm, flags) != CUBEB_OK) { + if (audiounit_uninstall_system_changed_callback(stm) != CUBEB_OK) { + LOG("(%p) Could not uninstall system changed callback", stm); + } + stm->state_callback(stm, stm->user_ptr, CUBEB_STATE_ERROR); + LOG("(%p) Could not reopen the stream after switching.", stm); + } + stm->switching_device = false; + stm->reinit_pending = false; + }); +} + +static char const * +event_addr_to_string(AudioObjectPropertySelector selector) +{ + switch (selector) { + case kAudioHardwarePropertyDefaultOutputDevice: + return "kAudioHardwarePropertyDefaultOutputDevice"; + case kAudioHardwarePropertyDefaultInputDevice: + return "kAudioHardwarePropertyDefaultInputDevice"; + case kAudioDevicePropertyDeviceIsAlive: + return "kAudioDevicePropertyDeviceIsAlive"; + case kAudioDevicePropertyDataSource: + return "kAudioDevicePropertyDataSource"; + default: + return "Unknown"; + } +} + +static OSStatus +audiounit_property_listener_callback( + AudioObjectID id, UInt32 address_count, + const AudioObjectPropertyAddress * addresses, void * user) +{ + cubeb_stream * stm = (cubeb_stream *)user; + if (stm->switching_device) { + LOG("Switching is already taking place. Skip Event %s for id=%d", + event_addr_to_string(addresses[0].mSelector), id); + return noErr; + } + stm->switching_device = true; + + LOG("(%p) Audio device changed, %u events.", stm, + (unsigned int)address_count); + for (UInt32 i = 0; i < address_count; i++) { + switch (addresses[i].mSelector) { + case kAudioHardwarePropertyDefaultOutputDevice: { + LOG("Event[%u] - mSelector == kAudioHardwarePropertyDefaultOutputDevice " + "for id=%d", + (unsigned int)i, id); + } break; + case kAudioHardwarePropertyDefaultInputDevice: { + LOG("Event[%u] - mSelector == kAudioHardwarePropertyDefaultInputDevice " + "for id=%d", + (unsigned int)i, id); + } break; + case kAudioDevicePropertyDeviceIsAlive: { + LOG("Event[%u] - mSelector == kAudioDevicePropertyDeviceIsAlive for " + "id=%d", + (unsigned int)i, id); + // If this is the default input device ignore the event, + // kAudioHardwarePropertyDefaultInputDevice will take care of the switch + if (stm->input_device.flags & DEV_SYSTEM_DEFAULT) { + LOG("It's the default input device, ignore the event"); + stm->switching_device = false; + return noErr; + } + } break; + case kAudioDevicePropertyDataSource: { + LOG("Event[%u] - mSelector == kAudioDevicePropertyDataSource for id=%d", + (unsigned int)i, id); + } break; + default: + LOG("Event[%u] - mSelector == Unexpected Event id %d, return", + (unsigned int)i, addresses[i].mSelector); + stm->switching_device = false; + return noErr; + } + } + + // Allow restart to choose the new default + device_flags_value switch_side = DEV_UNKNOWN; + if (has_input(stm)) { + switch_side |= DEV_INPUT; + } + if (has_output(stm)) { + switch_side |= DEV_OUTPUT; + } + + for (UInt32 i = 0; i < address_count; i++) { + switch (addresses[i].mSelector) { + case kAudioHardwarePropertyDefaultOutputDevice: + case kAudioHardwarePropertyDefaultInputDevice: + case kAudioDevicePropertyDeviceIsAlive: + /* fall through */ + case kAudioDevicePropertyDataSource: { + auto_lock dev_cb_lock(stm->device_changed_callback_lock); + if (stm->device_changed_callback) { + stm->device_changed_callback(stm->user_ptr); + } + break; + } + } + } + + audiounit_reinit_stream_async(stm, switch_side); + + return noErr; +} + +OSStatus +audiounit_add_listener(const property_listener * listener) +{ + assert(listener); + return AudioObjectAddPropertyListener(listener->device_id, + listener->property_address, + listener->callback, listener->stream); +} + +OSStatus +audiounit_remove_listener(const property_listener * listener) +{ + assert(listener); + return AudioObjectRemovePropertyListener( + listener->device_id, listener->property_address, listener->callback, + listener->stream); +} + +static int +audiounit_install_device_changed_callback(cubeb_stream * stm) +{ + OSStatus rv; + int r = CUBEB_OK; + + if (stm->output_unit) { + /* This event will notify us when the data source on the same device + * changes, for example when the user plugs in a normal (non-usb) headset in + * the headphone jack. */ + stm->output_source_listener.reset(new property_listener( + stm->output_device.id, &OUTPUT_DATA_SOURCE_PROPERTY_ADDRESS, + &audiounit_property_listener_callback, stm)); + rv = audiounit_add_listener(stm->output_source_listener.get()); + if (rv != noErr) { + stm->output_source_listener.reset(); + LOG("AudioObjectAddPropertyListener/output/" + "kAudioDevicePropertyDataSource rv=%d, device id=%d", + rv, stm->output_device.id); + r = CUBEB_ERROR; + } + } + + if (stm->input_unit) { + /* This event will notify us when the data source on the input device + * changes. */ + stm->input_source_listener.reset(new property_listener( + stm->input_device.id, &INPUT_DATA_SOURCE_PROPERTY_ADDRESS, + &audiounit_property_listener_callback, stm)); + rv = audiounit_add_listener(stm->input_source_listener.get()); + if (rv != noErr) { + stm->input_source_listener.reset(); + LOG("AudioObjectAddPropertyListener/input/kAudioDevicePropertyDataSource " + "rv=%d, device id=%d", + rv, stm->input_device.id); + r = CUBEB_ERROR; + } + + /* Event to notify when the input is going away. */ + stm->input_alive_listener.reset(new property_listener( + stm->input_device.id, &DEVICE_IS_ALIVE_PROPERTY_ADDRESS, + &audiounit_property_listener_callback, stm)); + rv = audiounit_add_listener(stm->input_alive_listener.get()); + if (rv != noErr) { + stm->input_alive_listener.reset(); + LOG("AudioObjectAddPropertyListener/input/" + "kAudioDevicePropertyDeviceIsAlive rv=%d, device id =%d", + rv, stm->input_device.id); + r = CUBEB_ERROR; + } + } + + return r; +} + +static int +audiounit_install_system_changed_callback(cubeb_stream * stm) +{ + OSStatus r; + + if (stm->output_unit) { + /* This event will notify us when the default audio device changes, + * for example when the user plugs in a USB headset and the system chooses + * it automatically as the default, or when another device is chosen in the + * dropdown list. */ + stm->default_output_listener.reset(new property_listener( + kAudioObjectSystemObject, &DEFAULT_OUTPUT_DEVICE_PROPERTY_ADDRESS, + &audiounit_property_listener_callback, stm)); + r = audiounit_add_listener(stm->default_output_listener.get()); + if (r != noErr) { + stm->default_output_listener.reset(); + LOG("AudioObjectAddPropertyListener/output/" + "kAudioHardwarePropertyDefaultOutputDevice rv=%d", + r); + return CUBEB_ERROR; + } + } + + if (stm->input_unit) { + /* This event will notify us when the default input device changes. */ + stm->default_input_listener.reset(new property_listener( + kAudioObjectSystemObject, &DEFAULT_INPUT_DEVICE_PROPERTY_ADDRESS, + &audiounit_property_listener_callback, stm)); + r = audiounit_add_listener(stm->default_input_listener.get()); + if (r != noErr) { + stm->default_input_listener.reset(); + LOG("AudioObjectAddPropertyListener/input/" + "kAudioHardwarePropertyDefaultInputDevice rv=%d", + r); + return CUBEB_ERROR; + } + } + + return CUBEB_OK; +} + +static int +audiounit_uninstall_device_changed_callback(cubeb_stream * stm) +{ + OSStatus rv; + // Failing to uninstall listeners is not a fatal error. + int r = CUBEB_OK; + + if (stm->output_source_listener) { + rv = audiounit_remove_listener(stm->output_source_listener.get()); + if (rv != noErr) { + LOG("AudioObjectRemovePropertyListener/output/" + "kAudioDevicePropertyDataSource rv=%d, device id=%d", + rv, stm->output_device.id); + r = CUBEB_ERROR; + } + stm->output_source_listener.reset(); + } + + if (stm->input_source_listener) { + rv = audiounit_remove_listener(stm->input_source_listener.get()); + if (rv != noErr) { + LOG("AudioObjectRemovePropertyListener/input/" + "kAudioDevicePropertyDataSource rv=%d, device id=%d", + rv, stm->input_device.id); + r = CUBEB_ERROR; + } + stm->input_source_listener.reset(); + } + + if (stm->input_alive_listener) { + rv = audiounit_remove_listener(stm->input_alive_listener.get()); + if (rv != noErr) { + LOG("AudioObjectRemovePropertyListener/input/" + "kAudioDevicePropertyDeviceIsAlive rv=%d, device id=%d", + rv, stm->input_device.id); + r = CUBEB_ERROR; + } + stm->input_alive_listener.reset(); + } + + return r; +} + +static int +audiounit_uninstall_system_changed_callback(cubeb_stream * stm) +{ + OSStatus r; + + if (stm->default_output_listener) { + r = audiounit_remove_listener(stm->default_output_listener.get()); + if (r != noErr) { + return CUBEB_ERROR; + } + stm->default_output_listener.reset(); + } + + if (stm->default_input_listener) { + r = audiounit_remove_listener(stm->default_input_listener.get()); + if (r != noErr) { + return CUBEB_ERROR; + } + stm->default_input_listener.reset(); + } + return CUBEB_OK; +} + +/* Get the acceptable buffer size (in frames) that this device can work with. */ +static int +audiounit_get_acceptable_latency_range(AudioValueRange * latency_range) +{ + UInt32 size; + OSStatus r; + AudioDeviceID output_device_id; + AudioObjectPropertyAddress output_device_buffer_size_range = { + kAudioDevicePropertyBufferFrameSizeRange, kAudioDevicePropertyScopeOutput, + kAudioObjectPropertyElementMaster}; + + output_device_id = audiounit_get_default_device_id(CUBEB_DEVICE_TYPE_OUTPUT); + if (output_device_id == kAudioObjectUnknown) { + LOG("Could not get default output device id."); + return CUBEB_ERROR; + } + + /* Get the buffer size range this device supports */ + size = sizeof(*latency_range); + + r = AudioObjectGetPropertyData(output_device_id, + &output_device_buffer_size_range, 0, NULL, + &size, latency_range); + if (r != noErr) { + LOG("AudioObjectGetPropertyData/buffer size range rv=%d", r); + return CUBEB_ERROR; + } + + return CUBEB_OK; +} +#endif /* !TARGET_OS_IPHONE */ + +static AudioObjectID +audiounit_get_default_device_id(cubeb_device_type type) +{ + const AudioObjectPropertyAddress * adr; + if (type == CUBEB_DEVICE_TYPE_OUTPUT) { + adr = &DEFAULT_OUTPUT_DEVICE_PROPERTY_ADDRESS; + } else if (type == CUBEB_DEVICE_TYPE_INPUT) { + adr = &DEFAULT_INPUT_DEVICE_PROPERTY_ADDRESS; + } else { + return kAudioObjectUnknown; + } + + AudioDeviceID devid; + UInt32 size = sizeof(AudioDeviceID); + if (AudioObjectGetPropertyData(kAudioObjectSystemObject, adr, 0, NULL, &size, + &devid) != noErr) { + return kAudioObjectUnknown; + } + + return devid; +} + +int +audiounit_get_max_channel_count(cubeb * ctx, uint32_t * max_channels) +{ +#if TARGET_OS_IPHONE + // TODO: [[AVAudioSession sharedInstance] maximumOutputNumberOfChannels] + *max_channels = 2; +#else + UInt32 size; + OSStatus r; + AudioDeviceID output_device_id; + AudioStreamBasicDescription stream_format; + AudioObjectPropertyAddress stream_format_address = { + kAudioDevicePropertyStreamFormat, kAudioDevicePropertyScopeOutput, + kAudioObjectPropertyElementMaster}; + + assert(ctx && max_channels); + + output_device_id = audiounit_get_default_device_id(CUBEB_DEVICE_TYPE_OUTPUT); + if (output_device_id == kAudioObjectUnknown) { + return CUBEB_ERROR; + } + + size = sizeof(stream_format); + + r = AudioObjectGetPropertyData(output_device_id, &stream_format_address, 0, + NULL, &size, &stream_format); + if (r != noErr) { + LOG("AudioObjectPropertyAddress/StreamFormat rv=%d", r); + return CUBEB_ERROR; + } + + *max_channels = stream_format.mChannelsPerFrame; +#endif + return CUBEB_OK; +} + +static int +audiounit_get_min_latency(cubeb * /* ctx */, cubeb_stream_params /* params */, + uint32_t * latency_frames) +{ +#if TARGET_OS_IPHONE + // TODO: [[AVAudioSession sharedInstance] inputLatency] + return CUBEB_ERROR_NOT_SUPPORTED; +#else + AudioValueRange latency_range; + if (audiounit_get_acceptable_latency_range(&latency_range) != CUBEB_OK) { + LOG("Could not get acceptable latency range."); + return CUBEB_ERROR; + } + + *latency_frames = + max<uint32_t>(latency_range.mMinimum, SAFE_MIN_LATENCY_FRAMES); +#endif + + return CUBEB_OK; +} + +static int +audiounit_get_preferred_sample_rate(cubeb * /* ctx */, uint32_t * rate) +{ +#if TARGET_OS_IPHONE + // TODO + return CUBEB_ERROR_NOT_SUPPORTED; +#else + UInt32 size; + OSStatus r; + Float64 fsamplerate; + AudioDeviceID output_device_id; + AudioObjectPropertyAddress samplerate_address = { + kAudioDevicePropertyNominalSampleRate, kAudioObjectPropertyScopeGlobal, + kAudioObjectPropertyElementMaster}; + + output_device_id = audiounit_get_default_device_id(CUBEB_DEVICE_TYPE_OUTPUT); + if (output_device_id == kAudioObjectUnknown) { + return CUBEB_ERROR; + } + + size = sizeof(fsamplerate); + r = AudioObjectGetPropertyData(output_device_id, &samplerate_address, 0, NULL, + &size, &fsamplerate); + + if (r != noErr) { + return CUBEB_ERROR; + } + + *rate = static_cast<uint32_t>(fsamplerate); +#endif + return CUBEB_OK; +} + +static cubeb_channel_layout +audiounit_convert_channel_layout(AudioChannelLayout * layout) +{ + // When having one or two channel, force mono or stereo. Some devices (namely, + // Bose QC35, mark 1 and 2), expose a single channel mapped to the right for + // some reason. + if (layout->mNumberChannelDescriptions == 1) { + return CUBEB_LAYOUT_MONO; + } else if (layout->mNumberChannelDescriptions == 2) { + return CUBEB_LAYOUT_STEREO; + } + + if (layout->mChannelLayoutTag != + kAudioChannelLayoutTag_UseChannelDescriptions) { + // kAudioChannelLayoutTag_UseChannelBitmap + // kAudioChannelLayoutTag_Mono + // kAudioChannelLayoutTag_Stereo + // .... + LOG("Only handle UseChannelDescriptions for now.\n"); + return CUBEB_LAYOUT_UNDEFINED; + } + + cubeb_channel_layout cl = 0; + for (UInt32 i = 0; i < layout->mNumberChannelDescriptions; ++i) { + cubeb_channel cc = channel_label_to_cubeb_channel( + layout->mChannelDescriptions[i].mChannelLabel); + if (cc == CHANNEL_UNKNOWN) { + return CUBEB_LAYOUT_UNDEFINED; + } + cl |= cc; + } + + return cl; +} + +static cubeb_channel_layout +audiounit_get_preferred_channel_layout(AudioUnit output_unit) +{ + OSStatus rv = noErr; + UInt32 size = 0; + rv = AudioUnitGetPropertyInfo( + output_unit, kAudioDevicePropertyPreferredChannelLayout, + kAudioUnitScope_Output, AU_OUT_BUS, &size, nullptr); + if (rv != noErr) { + LOG("AudioUnitGetPropertyInfo/kAudioDevicePropertyPreferredChannelLayout " + "rv=%d", + rv); + return CUBEB_LAYOUT_UNDEFINED; + } + assert(size > 0); + + auto layout = make_sized_audio_channel_layout(size); + rv = AudioUnitGetProperty( + output_unit, kAudioDevicePropertyPreferredChannelLayout, + kAudioUnitScope_Output, AU_OUT_BUS, layout.get(), &size); + if (rv != noErr) { + LOG("AudioUnitGetProperty/kAudioDevicePropertyPreferredChannelLayout rv=%d", + rv); + return CUBEB_LAYOUT_UNDEFINED; + } + + return audiounit_convert_channel_layout(layout.get()); +} + +static cubeb_channel_layout +audiounit_get_current_channel_layout(AudioUnit output_unit) +{ + OSStatus rv = noErr; + UInt32 size = 0; + rv = AudioUnitGetPropertyInfo( + output_unit, kAudioUnitProperty_AudioChannelLayout, + kAudioUnitScope_Output, AU_OUT_BUS, &size, nullptr); + if (rv != noErr) { + LOG("AudioUnitGetPropertyInfo/kAudioUnitProperty_AudioChannelLayout rv=%d", + rv); + // This property isn't known before macOS 10.12, attempt another method. + return audiounit_get_preferred_channel_layout(output_unit); + } + assert(size > 0); + + auto layout = make_sized_audio_channel_layout(size); + rv = AudioUnitGetProperty(output_unit, kAudioUnitProperty_AudioChannelLayout, + kAudioUnitScope_Output, AU_OUT_BUS, layout.get(), + &size); + if (rv != noErr) { + LOG("AudioUnitGetProperty/kAudioUnitProperty_AudioChannelLayout rv=%d", rv); + return CUBEB_LAYOUT_UNDEFINED; + } + + return audiounit_convert_channel_layout(layout.get()); +} + +static int +audiounit_create_unit(AudioUnit * unit, device_info * device); + +static OSStatus +audiounit_remove_device_listener(cubeb * context, cubeb_device_type devtype); + +static void +audiounit_destroy(cubeb * ctx) +{ + { + auto_lock lock(ctx->mutex); + + // Disabling this assert for bug 1083664 -- we seem to leak a stream + // assert(ctx->active_streams == 0); + if (audiounit_active_streams(ctx) > 0) { + LOG("(%p) API misuse, %d streams active when context destroyed!", ctx, + audiounit_active_streams(ctx)); + } + + // Destroying a cubeb context with device collection callbacks registered + // is misuse of the API, assert then attempt to clean up. + assert(!ctx->input_collection_changed_callback && + !ctx->input_collection_changed_user_ptr && + !ctx->output_collection_changed_callback && + !ctx->output_collection_changed_user_ptr); + + /* Unregister the callback if necessary. */ + if (ctx->input_collection_changed_callback) { + audiounit_remove_device_listener(ctx, CUBEB_DEVICE_TYPE_INPUT); + } + if (ctx->output_collection_changed_callback) { + audiounit_remove_device_listener(ctx, CUBEB_DEVICE_TYPE_OUTPUT); + } + } + + dispatch_release(ctx->serial_queue); + + delete ctx; +} + +static void +audiounit_stream_destroy(cubeb_stream * stm); + +static int +audio_stream_desc_init(AudioStreamBasicDescription * ss, + const cubeb_stream_params * stream_params) +{ + switch (stream_params->format) { + case CUBEB_SAMPLE_S16LE: + ss->mBitsPerChannel = 16; + ss->mFormatFlags = kAudioFormatFlagIsSignedInteger; + break; + case CUBEB_SAMPLE_S16BE: + ss->mBitsPerChannel = 16; + ss->mFormatFlags = + kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsBigEndian; + break; + case CUBEB_SAMPLE_FLOAT32LE: + ss->mBitsPerChannel = 32; + ss->mFormatFlags = kAudioFormatFlagIsFloat; + break; + case CUBEB_SAMPLE_FLOAT32BE: + ss->mBitsPerChannel = 32; + ss->mFormatFlags = kAudioFormatFlagIsFloat | kAudioFormatFlagIsBigEndian; + break; + default: + return CUBEB_ERROR_INVALID_FORMAT; + } + + ss->mFormatID = kAudioFormatLinearPCM; + ss->mFormatFlags |= kLinearPCMFormatFlagIsPacked; + ss->mSampleRate = stream_params->rate; + ss->mChannelsPerFrame = stream_params->channels; + + ss->mBytesPerFrame = (ss->mBitsPerChannel / 8) * ss->mChannelsPerFrame; + ss->mFramesPerPacket = 1; + ss->mBytesPerPacket = ss->mBytesPerFrame * ss->mFramesPerPacket; + + ss->mReserved = 0; + + return CUBEB_OK; +} + +void +audiounit_init_mixer(cubeb_stream * stm) +{ + // We can't rely on macOS' AudioUnit to properly downmix (or upmix) the audio + // data, it silently drop the channels so we need to remix the + // audio data by ourselves to keep all the information. + stm->mixer.reset(cubeb_mixer_create( + stm->output_stream_params.format, stm->output_stream_params.channels, + stm->output_stream_params.layout, stm->context->channels, + stm->context->layout)); + assert(stm->mixer); +} + +static int +audiounit_set_channel_layout(AudioUnit unit, io_side side, + cubeb_channel_layout layout) +{ + if (side != io_side::OUTPUT) { + return CUBEB_ERROR; + } + + if (layout == CUBEB_LAYOUT_UNDEFINED) { + // We leave everything as-is... + return CUBEB_OK; + } + + OSStatus r; + uint32_t nb_channels = cubeb_channel_layout_nb_channels(layout); + + // We do not use CoreAudio standard layout for lack of documentation on what + // the actual channel orders are. So we set a custom layout. + size_t size = offsetof(AudioChannelLayout, mChannelDescriptions[nb_channels]); + auto au_layout = make_sized_audio_channel_layout(size); + au_layout->mChannelLayoutTag = kAudioChannelLayoutTag_UseChannelDescriptions; + au_layout->mNumberChannelDescriptions = nb_channels; + + uint32_t channels = 0; + cubeb_channel_layout channelMap = layout; + for (uint32_t i = 0; channelMap != 0; ++i) { + XASSERT(channels < nb_channels); + uint32_t channel = (channelMap & 1) << i; + if (channel != 0) { + au_layout->mChannelDescriptions[channels].mChannelLabel = + cubeb_channel_to_channel_label(static_cast<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; + OSStatus rv; + + desc.componentType = kAudioUnitType_Output; +#if TARGET_OS_IPHONE + desc.componentSubType = kAudioUnitSubType_RemoteIO; +#else + // Use the DefaultOutputUnit for output when no device is specified + // so we retain automatic output device switching when the default + // changes. Once we have complete support for device notifications + // and switching, we can use the AUHAL for everything. + if ((device->flags & DEV_SYSTEM_DEFAULT) && (device->flags & DEV_OUTPUT)) { + desc.componentSubType = kAudioUnitSubType_DefaultOutput; + } else { + desc.componentSubType = kAudioUnitSubType_HALOutput; + } +#endif + desc.componentManufacturer = kAudioUnitManufacturer_Apple; + desc.componentFlags = 0; + desc.componentFlagsMask = 0; + comp = AudioComponentFindNext(NULL, &desc); + if (comp == NULL) { + LOG("Could not find matching audio hardware."); + return CUBEB_ERROR; + } + + rv = AudioComponentInstanceNew(comp, unit); + if (rv != noErr) { + LOG("AudioComponentInstanceNew rv=%d", rv); + return CUBEB_ERROR; + } + return CUBEB_OK; +} + +enum enable_state { + DISABLE, + ENABLE, +}; + +static int +audiounit_enable_unit_scope(AudioUnit * unit, io_side side, enable_state state) +{ + OSStatus rv; + UInt32 enable = state; + rv = AudioUnitSetProperty(*unit, kAudioOutputUnitProperty_EnableIO, + (side == io_side::INPUT) ? kAudioUnitScope_Input + : kAudioUnitScope_Output, + (side == io_side::INPUT) ? AU_IN_BUS : AU_OUT_BUS, + &enable, sizeof(UInt32)); + if (rv != noErr) { + LOG("AudioUnitSetProperty/kAudioOutputUnitProperty_EnableIO rv=%d", rv); + return CUBEB_ERROR; + } + return CUBEB_OK; +} + +static int +audiounit_create_unit(AudioUnit * unit, device_info * device) +{ + assert(*unit == nullptr); + assert(device); + + OSStatus rv; + int r; + + r = audiounit_new_unit_instance(unit, device); + if (r != CUBEB_OK) { + return r; + } + assert(*unit); + + if ((device->flags & DEV_SYSTEM_DEFAULT) && (device->flags & DEV_OUTPUT)) { + return CUBEB_OK; + } + + if (device->flags & DEV_INPUT) { + r = audiounit_enable_unit_scope(unit, io_side::INPUT, ENABLE); + if (r != CUBEB_OK) { + LOG("Failed to enable audiounit input scope"); + return r; + } + r = audiounit_enable_unit_scope(unit, io_side::OUTPUT, DISABLE); + if (r != CUBEB_OK) { + LOG("Failed to disable audiounit output scope"); + return r; + } + } else if (device->flags & DEV_OUTPUT) { + r = audiounit_enable_unit_scope(unit, io_side::OUTPUT, ENABLE); + if (r != CUBEB_OK) { + LOG("Failed to enable audiounit output scope"); + return r; + } + r = audiounit_enable_unit_scope(unit, io_side::INPUT, DISABLE); + if (r != CUBEB_OK) { + LOG("Failed to disable audiounit input scope"); + return r; + } + } else { + assert(false); + } + + rv = AudioUnitSetProperty(*unit, kAudioOutputUnitProperty_CurrentDevice, + kAudioUnitScope_Global, 0, &device->id, + sizeof(AudioDeviceID)); + if (rv != noErr) { + LOG("AudioUnitSetProperty/kAudioOutputUnitProperty_CurrentDevice rv=%d", + rv); + return CUBEB_ERROR; + } + + return CUBEB_OK; +} + +static int +audiounit_init_input_linear_buffer(cubeb_stream * stream, uint32_t capacity) +{ + uint32_t size = + capacity * stream->latency_frames * stream->input_desc.mChannelsPerFrame; + if (stream->input_desc.mFormatFlags & kAudioFormatFlagIsSignedInteger) { + stream->input_linear_buffer.reset(new auto_array_wrapper_impl<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(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 + int r; + UInt32 output_buffer_size = 0; + UInt32 size = sizeof(output_buffer_size); + if (stm->output_unit) { + r = AudioUnitGetProperty( + stm->output_unit, kAudioDevicePropertyBufferFrameSize, + kAudioUnitScope_Output, AU_OUT_BUS, &output_buffer_size, &size); + if (r != noErr) { + LOG("AudioUnitGetProperty/output/kAudioDevicePropertyBufferFrameSize " + "rv=%d", + r); + return 0; + } + + output_buffer_size = + max(min<uint32_t>(output_buffer_size, SAFE_MAX_LATENCY_FRAMES), + SAFE_MIN_LATENCY_FRAMES); + } + + UInt32 input_buffer_size = 0; + if (stm->input_unit) { + r = AudioUnitGetProperty( + stm->input_unit, kAudioDevicePropertyBufferFrameSize, + kAudioUnitScope_Input, AU_IN_BUS, &input_buffer_size, &size); + if (r != noErr) { + LOG("AudioUnitGetProperty/input/kAudioDevicePropertyBufferFrameSize " + "rv=%d", + r); + return 0; + } + + input_buffer_size = + max(min<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 = 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) { + upper_latency_limit = output_buffer_size; + } else { + upper_latency_limit = SAFE_MAX_LATENCY_FRAMES; + } + + return max(min<uint32_t>(latency_frames, upper_latency_limit), + SAFE_MIN_LATENCY_FRAMES); +} + +/* + * Change buffer size is prone to deadlock thus we change it + * following the steps: + * - register a listener for the buffer size property + * - change the property + * - wait until the listener is executed + * - property has changed, remove the listener + * */ +static void +buffer_size_changed_callback(void * inClientData, AudioUnit inUnit, + AudioUnitPropertyID inPropertyID, + AudioUnitScope inScope, AudioUnitElement inElement) +{ + cubeb_stream * stm = (cubeb_stream *)inClientData; + + AudioUnit au = inUnit; + AudioUnitScope au_scope = kAudioUnitScope_Input; + AudioUnitElement au_element = inElement; + char const * au_type = "output"; + + if (AU_IN_BUS == inElement) { + au_scope = kAudioUnitScope_Output; + au_type = "input"; + } + + switch (inPropertyID) { + + case kAudioDevicePropertyBufferFrameSize: { + if (inScope != au_scope) { + break; + } + UInt32 new_buffer_size; + UInt32 outSize = sizeof(UInt32); + OSStatus r = + AudioUnitGetProperty(au, kAudioDevicePropertyBufferFrameSize, au_scope, + au_element, &new_buffer_size, &outSize); + if (r != noErr) { + LOG("(%p) Event: kAudioDevicePropertyBufferFrameSize: Cannot get current " + "buffer size", + stm); + } else { + LOG("(%p) Event: kAudioDevicePropertyBufferFrameSize: New %s buffer size " + "= %d for scope %d", + stm, au_type, new_buffer_size, inScope); + } + stm->buffer_size_change_state = true; + break; + } + } +} + +static int +audiounit_set_buffer_size(cubeb_stream * stm, uint32_t new_size_frames, + io_side side) +{ + AudioUnit au = stm->output_unit; + AudioUnitScope au_scope = kAudioUnitScope_Input; + AudioUnitElement au_element = AU_OUT_BUS; + + if (side == io_side::INPUT) { + au = stm->input_unit; + au_scope = kAudioUnitScope_Output; + au_element = AU_IN_BUS; + } + + uint32_t buffer_frames = 0; + UInt32 size = sizeof(buffer_frames); + int r = AudioUnitGetProperty(au, kAudioDevicePropertyBufferFrameSize, + au_scope, au_element, &buffer_frames, &size); + if (r != noErr) { + LOG("AudioUnitGetProperty/%s/kAudioDevicePropertyBufferFrameSize rv=%d", + to_string(side), r); + return CUBEB_ERROR; + } + + if (new_size_frames == buffer_frames) { + LOG("(%p) No need to update %s buffer size already %u frames", stm, + to_string(side), buffer_frames); + return CUBEB_OK; + } + + r = AudioUnitAddPropertyListener(au, kAudioDevicePropertyBufferFrameSize, + buffer_size_changed_callback, stm); + if (r != noErr) { + LOG("AudioUnitAddPropertyListener/%s/kAudioDevicePropertyBufferFrameSize " + "rv=%d", + to_string(side), r); + return CUBEB_ERROR; + } + + stm->buffer_size_change_state = false; + + r = AudioUnitSetProperty(au, kAudioDevicePropertyBufferFrameSize, au_scope, + au_element, &new_size_frames, + sizeof(new_size_frames)); + if (r != noErr) { + LOG("AudioUnitSetProperty/%s/kAudioDevicePropertyBufferFrameSize rv=%d", + to_string(side), r); + + r = AudioUnitRemovePropertyListenerWithUserData( + au, kAudioDevicePropertyBufferFrameSize, buffer_size_changed_callback, + stm); + if (r != noErr) { + LOG("AudioUnitAddPropertyListener/%s/kAudioDevicePropertyBufferFrameSize " + "rv=%d", + to_string(side), r); + } + + return CUBEB_ERROR; + } + + int count = 0; + while (!stm->buffer_size_change_state && count++ < 30) { + struct timespec req, rem; + req.tv_sec = 0; + req.tv_nsec = 100000000L; // 0.1 sec + if (nanosleep(&req, &rem) < 0) { + LOG("(%p) Warning: nanosleep call failed or interrupted. Remaining time " + "%ld nano secs \n", + stm, rem.tv_nsec); + } + LOG("(%p) audiounit_set_buffer_size : wait count = %d", stm, count); + } + + r = AudioUnitRemovePropertyListenerWithUserData( + au, kAudioDevicePropertyBufferFrameSize, buffer_size_changed_callback, + stm); + if (r != noErr) { + LOG("AudioUnitAddPropertyListener/%s/kAudioDevicePropertyBufferFrameSize " + "rv=%d", + to_string(side), r); + return CUBEB_ERROR; + } + + if (!stm->buffer_size_change_state && count >= 30) { + LOG("(%p) Error, did not get buffer size change callback ...", stm); + return CUBEB_ERROR; + } + + LOG("(%p) %s buffer size changed to %u frames.", stm, to_string(side), + new_size_frames); + return CUBEB_OK; +} + +static int +audiounit_configure_input(cubeb_stream * stm) +{ + assert(stm && stm->input_unit); + + int r = 0; + UInt32 size; + AURenderCallbackStruct aurcbs_in; + + LOG("(%p) Opening input side: rate %u, channels %u, format %d, latency in " + "frames %u.", + stm, stm->input_stream_params.rate, stm->input_stream_params.channels, + stm->input_stream_params.format, stm->latency_frames); + + /* Get input device sample rate. */ + AudioStreamBasicDescription input_hw_desc; + size = sizeof(AudioStreamBasicDescription); + r = AudioUnitGetProperty(stm->input_unit, kAudioUnitProperty_StreamFormat, + kAudioUnitScope_Input, AU_IN_BUS, &input_hw_desc, + &size); + if (r != noErr) { + LOG("AudioUnitGetProperty/input/kAudioUnitProperty_StreamFormat rv=%d", r); + return CUBEB_ERROR; + } + stm->input_hw_rate = input_hw_desc.mSampleRate; + LOG("(%p) Input device sampling rate: %.2f", stm, stm->input_hw_rate); + + /* Set format description according to the input params. */ + r = audio_stream_desc_init(&stm->input_desc, &stm->input_stream_params); + if (r != CUBEB_OK) { + LOG("(%p) Setting format description for input failed.", stm); + return r; + } + + // Use latency to set buffer size + r = audiounit_set_buffer_size(stm, stm->latency_frames, io_side::INPUT); + if (r != CUBEB_OK) { + LOG("(%p) Error in change input buffer size.", stm); + return CUBEB_ERROR; + } + + AudioStreamBasicDescription src_desc = stm->input_desc; + /* Input AudioUnit must be configured with device's sample rate. + we will resample inside input callback. */ + src_desc.mSampleRate = stm->input_hw_rate; + + r = AudioUnitSetProperty(stm->input_unit, kAudioUnitProperty_StreamFormat, + kAudioUnitScope_Output, AU_IN_BUS, &src_desc, + sizeof(AudioStreamBasicDescription)); + if (r != noErr) { + LOG("AudioUnitSetProperty/input/kAudioUnitProperty_StreamFormat rv=%d", r); + return CUBEB_ERROR; + } + + /* Frames per buffer in the input callback. */ + r = AudioUnitSetProperty( + stm->input_unit, kAudioUnitProperty_MaximumFramesPerSlice, + kAudioUnitScope_Global, AU_IN_BUS, &stm->latency_frames, sizeof(UInt32)); + if (r != noErr) { + LOG("AudioUnitSetProperty/input/kAudioUnitProperty_MaximumFramesPerSlice " + "rv=%d", + r); + return CUBEB_ERROR; + } + + // Input only capacity + unsigned int array_capacity = 1; + if (has_output(stm)) { + // Full-duplex increase capacity + array_capacity = 8; + } + if (audiounit_init_input_linear_buffer(stm, array_capacity) != CUBEB_OK) { + return CUBEB_ERROR; + } + + aurcbs_in.inputProc = audiounit_input_callback; + aurcbs_in.inputProcRefCon = stm; + + r = AudioUnitSetProperty( + stm->input_unit, kAudioOutputUnitProperty_SetInputCallback, + kAudioUnitScope_Global, AU_OUT_BUS, &aurcbs_in, sizeof(aurcbs_in)); + if (r != noErr) { + LOG("AudioUnitSetProperty/input/kAudioOutputUnitProperty_SetInputCallback " + "rv=%d", + r); + return CUBEB_ERROR; + } + + stm->frames_read = 0; + + LOG("(%p) Input audiounit init successfully.", stm); + + return CUBEB_OK; +} + +static int +audiounit_configure_output(cubeb_stream * stm) +{ + assert(stm && stm->output_unit); + + int r; + AURenderCallbackStruct aurcbs_out; + UInt32 size; + + LOG("(%p) Opening output side: rate %u, channels %u, format %d, latency in " + "frames %u.", + stm, stm->output_stream_params.rate, stm->output_stream_params.channels, + stm->output_stream_params.format, stm->latency_frames); + + r = audio_stream_desc_init(&stm->output_desc, &stm->output_stream_params); + if (r != CUBEB_OK) { + LOG("(%p) Could not initialize the audio stream description.", stm); + return r; + } + + /* Get output device sample rate. */ + AudioStreamBasicDescription output_hw_desc; + size = sizeof(AudioStreamBasicDescription); + memset(&output_hw_desc, 0, size); + r = AudioUnitGetProperty(stm->output_unit, kAudioUnitProperty_StreamFormat, + kAudioUnitScope_Output, AU_OUT_BUS, &output_hw_desc, + &size); + if (r != noErr) { + LOG("AudioUnitGetProperty/output/kAudioUnitProperty_StreamFormat rv=%d", r); + return CUBEB_ERROR; + } + stm->output_hw_rate = output_hw_desc.mSampleRate; + if (!is_common_sample_rate(stm->output_desc.mSampleRate)) { + /* For uncommon sample rates, we may run into issues with the OS + resampler if we don't do the resampling ourselves, so set the + AudioUnit sample rate to the hardware rate and resample. */ + stm->output_desc.mSampleRate = stm->output_hw_rate; + } + LOG("(%p) Output device sampling rate: %.2f", stm, + output_hw_desc.mSampleRate); + stm->context->channels = output_hw_desc.mChannelsPerFrame; + + // Set the input layout to match the output device layout. + audiounit_layout_init(stm, io_side::OUTPUT); + if (stm->context->channels != stm->output_stream_params.channels || + stm->context->layout != stm->output_stream_params.layout) { + LOG("Incompatible channel layouts detected, setting up remixer"); + audiounit_init_mixer(stm); + // We will be remixing the data before it reaches the output device. + // We need to adjust the number of channels and other + // AudioStreamDescription details. + stm->output_desc.mChannelsPerFrame = stm->context->channels; + stm->output_desc.mBytesPerFrame = (stm->output_desc.mBitsPerChannel / 8) * + stm->output_desc.mChannelsPerFrame; + stm->output_desc.mBytesPerPacket = + stm->output_desc.mBytesPerFrame * stm->output_desc.mFramesPerPacket; + } else { + stm->mixer = nullptr; + } + + r = AudioUnitSetProperty(stm->output_unit, kAudioUnitProperty_StreamFormat, + kAudioUnitScope_Input, AU_OUT_BUS, &stm->output_desc, + sizeof(AudioStreamBasicDescription)); + if (r != noErr) { + LOG("AudioUnitSetProperty/output/kAudioUnitProperty_StreamFormat rv=%d", r); + return CUBEB_ERROR; + } + + r = audiounit_set_buffer_size(stm, stm->latency_frames, io_side::OUTPUT); + if (r != CUBEB_OK) { + LOG("(%p) Error in change output buffer size.", stm); + return CUBEB_ERROR; + } + + /* Frames per buffer in the input callback. */ + r = AudioUnitSetProperty( + stm->output_unit, kAudioUnitProperty_MaximumFramesPerSlice, + kAudioUnitScope_Global, AU_OUT_BUS, &stm->latency_frames, sizeof(UInt32)); + if (r != noErr) { + LOG("AudioUnitSetProperty/output/kAudioUnitProperty_MaximumFramesPerSlice " + "rv=%d", + r); + return CUBEB_ERROR; + } + + aurcbs_out.inputProc = audiounit_output_callback; + aurcbs_out.inputProcRefCon = stm; + r = AudioUnitSetProperty( + stm->output_unit, kAudioUnitProperty_SetRenderCallback, + kAudioUnitScope_Global, AU_OUT_BUS, &aurcbs_out, sizeof(aurcbs_out)); + if (r != noErr) { + LOG("AudioUnitSetProperty/output/kAudioUnitProperty_SetRenderCallback " + "rv=%d", + r); + return CUBEB_ERROR; + } + + stm->frames_written = 0; + + LOG("(%p) Output audiounit init successfully.", stm); + return CUBEB_OK; +} + +static int +audiounit_setup_stream(cubeb_stream * stm) +{ + stm->mutex.assert_current_thread_owns(); + + if ((stm->input_stream_params.prefs & CUBEB_STREAM_PREF_LOOPBACK) || + (stm->output_stream_params.prefs & CUBEB_STREAM_PREF_LOOPBACK)) { + LOG("(%p) Loopback not supported for audiounit.", stm); + return CUBEB_ERROR_NOT_SUPPORTED; + } + + int r = 0; + + device_info in_dev_info = stm->input_device; + device_info out_dev_info = stm->output_device; + + if (has_input(stm) && has_output(stm) && + stm->input_device.id != stm->output_device.id) { + r = audiounit_create_aggregate_device(stm); + if (r != CUBEB_OK) { + stm->aggregate_device_id = kAudioObjectUnknown; + LOG("(%p) Create aggregate devices failed.", stm); + // !!!NOTE: It is not necessary to return here. If it does not + // return it will fallback to the old implementation. The intention + // is to investigate how often it fails. I plan to remove + // it after a couple of weeks. + return r; + } else { + in_dev_info.id = out_dev_info.id = stm->aggregate_device_id; + in_dev_info.flags = DEV_INPUT; + out_dev_info.flags = DEV_OUTPUT; + } + } + + if (has_input(stm)) { + r = audiounit_create_unit(&stm->input_unit, &in_dev_info); + if (r != CUBEB_OK) { + LOG("(%p) AudioUnit creation for input failed.", stm); + return r; + } + } + + if (has_output(stm)) { + r = audiounit_create_unit(&stm->output_unit, &out_dev_info); + if (r != CUBEB_OK) { + LOG("(%p) AudioUnit creation for output failed.", stm); + return r; + } + } + + /* Latency cannot change if another stream is operating in parallel. In this + * case latency is set to the other stream value. */ + if (audiounit_active_streams(stm->context) > 1) { + LOG("(%p) More than one active stream, use global latency.", stm); + stm->latency_frames = stm->context->global_latency_frames; + } else { + /* Silently clamp the latency down to the platform default, because we + * synthetize the clock from the callbacks, and we want the clock to update + * often. */ + stm->latency_frames = audiounit_clamp_latency(stm, stm->latency_frames); + assert(stm->latency_frames); // Ugly error check + audiounit_set_global_latency(stm->context, stm->latency_frames); + } + + /* Configure I/O stream */ + if (has_input(stm)) { + r = audiounit_configure_input(stm); + if (r != CUBEB_OK) { + LOG("(%p) Configure audiounit input failed.", stm); + return r; + } + } + + if (has_output(stm)) { + r = audiounit_configure_output(stm); + if (r != CUBEB_OK) { + LOG("(%p) Configure audiounit output failed.", stm); + return r; + } + } + + // Setting the latency doesn't work well for USB headsets (eg. plantronics). + // Keep the default latency for now. +#if 0 + buffer_size = latency; + + /* Get the range of latency this particular device can work with, and clamp + * the requested latency to this acceptable range. */ +#if !TARGET_OS_IPHONE + if (audiounit_get_acceptable_latency_range(&latency_range) != CUBEB_OK) { + return CUBEB_ERROR; + } + + if (buffer_size < (unsigned int) latency_range.mMinimum) { + buffer_size = (unsigned int) latency_range.mMinimum; + } else if (buffer_size > (unsigned int) latency_range.mMaximum) { + buffer_size = (unsigned int) latency_range.mMaximum; + } + + /** + * Get the default buffer size. If our latency request is below the default, + * set it. Otherwise, use the default latency. + **/ + size = sizeof(default_buffer_size); + if (AudioUnitGetProperty(stm->output_unit, kAudioDevicePropertyBufferFrameSize, + kAudioUnitScope_Output, 0, &default_buffer_size, &size) != 0) { + return CUBEB_ERROR; + } + + if (buffer_size < default_buffer_size) { + /* Set the maximum number of frame that the render callback will ask for, + * effectively setting the latency of the stream. This is process-wide. */ + if (AudioUnitSetProperty(stm->output_unit, kAudioDevicePropertyBufferFrameSize, + kAudioUnitScope_Output, 0, &buffer_size, sizeof(buffer_size)) != 0) { + return CUBEB_ERROR; + } + } +#else // TARGET_OS_IPHONE + //TODO: [[AVAudioSession sharedInstance] inputLatency] + // http://stackoverflow.com/questions/13157523/kaudiodevicepropertybufferframesize-replacement-for-ios +#endif +#endif + + /* We use a resampler because input AudioUnit operates + * reliable only in the capture device sample rate. + * Resampler will convert it to the user sample rate + * and deliver it to the callback. */ + uint32_t target_sample_rate; + if (has_input(stm)) { + target_sample_rate = stm->input_stream_params.rate; + } else { + assert(has_output(stm)); + target_sample_rate = stm->output_stream_params.rate; + } + + cubeb_stream_params input_unconverted_params; + if (has_input(stm)) { + input_unconverted_params = stm->input_stream_params; + /* Use the rate of the input device. */ + input_unconverted_params.rate = stm->input_hw_rate; + } + + cubeb_stream_params output_unconverted_params; + if (has_output(stm)) { + output_unconverted_params = stm->output_stream_params; + output_unconverted_params.rate = stm->output_desc.mSampleRate; + } + + /* Create resampler. */ + stm->resampler.reset(cubeb_resampler_create( + stm, has_input(stm) ? &input_unconverted_params : NULL, + has_output(stm) ? &output_unconverted_params : NULL, target_sample_rate, + stm->data_callback, stm->user_ptr, CUBEB_RESAMPLER_QUALITY_DESKTOP, + CUBEB_RESAMPLER_RECLOCK_NONE)); + if (!stm->resampler) { + LOG("(%p) Could not create resampler.", stm); + return CUBEB_ERROR; + } + + if (stm->input_unit != NULL) { + r = AudioUnitInitialize(stm->input_unit); + if (r != noErr) { + LOG("AudioUnitInitialize/input rv=%d", r); + return CUBEB_ERROR; + } + } + + if (stm->output_unit != NULL) { + r = AudioUnitInitialize(stm->output_unit); + if (r != noErr) { + LOG("AudioUnitInitialize/output rv=%d", r); + return CUBEB_ERROR; + } + + stm->current_latency_frames = audiounit_get_device_presentation_latency( + stm->output_device.id, kAudioDevicePropertyScopeOutput); + + Float64 unit_s; + UInt32 size = sizeof(unit_s); + if (AudioUnitGetProperty(stm->output_unit, kAudioUnitProperty_Latency, + kAudioUnitScope_Global, 0, &unit_s, + &size) == noErr) { + stm->current_latency_frames += + static_cast<uint32_t>(unit_s * stm->output_desc.mSampleRate); + } + } + + if (stm->input_unit && stm->output_unit) { + // According to the I/O hardware rate it is expected a specific pattern of + // callbacks for example is input is 44100 and output is 48000 we expected + // no more than 2 out callback in a row. + stm->expected_output_callbacks_in_a_row = + ceilf(stm->output_hw_rate / stm->input_hw_rate); + } + + r = audiounit_install_device_changed_callback(stm); + if (r != CUBEB_OK) { + LOG("(%p) Could not install all device change callback.", stm); + } + + return CUBEB_OK; +} + +cubeb_stream::cubeb_stream(cubeb * context) + : context(context), resampler(nullptr, cubeb_resampler_destroy), + mixer(nullptr, cubeb_mixer_destroy) +{ + PodZero(&input_desc, 1); + PodZero(&output_desc, 1); +} + +static void +audiounit_stream_destroy_internal(cubeb_stream * stm); + +static int +audiounit_stream_init(cubeb * context, cubeb_stream ** stream, + char const * /* stream_name */, cubeb_devid input_device, + cubeb_stream_params * input_stream_params, + cubeb_devid output_device, + cubeb_stream_params * output_stream_params, + unsigned int latency_frames, + cubeb_data_callback data_callback, + cubeb_state_callback state_callback, void * user_ptr) +{ + assert(context); + auto_lock context_lock(context->mutex); + audiounit_increment_active_streams(context); + unique_ptr<cubeb_stream, decltype(&audiounit_stream_destroy)> stm( + new cubeb_stream(context), audiounit_stream_destroy_internal); + int r; + *stream = NULL; + assert(latency_frames > 0); + + /* These could be different in the future if we have both + * full-duplex stream and different devices for input vs output. */ + stm->data_callback = data_callback; + stm->state_callback = state_callback; + stm->user_ptr = user_ptr; + stm->latency_frames = latency_frames; + + if ((input_device && !input_stream_params) || + (output_device && !output_stream_params)) { + return CUBEB_ERROR_INVALID_PARAMETER; + } + if (input_stream_params) { + stm->input_stream_params = *input_stream_params; + r = audiounit_set_device_info( + stm.get(), reinterpret_cast<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; + 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; + } + } + + { + // It's not critical to lock here, because no other thread has been started + // yet, but it allows to assert that the lock has been taken in + // `audiounit_setup_stream`. + auto_lock lock(stm->mutex); + r = audiounit_setup_stream(stm.get()); + } + + if (r != CUBEB_OK) { + LOG("(%p) Could not setup the audiounit stream.", stm.get()); + return r; + } + + r = audiounit_install_system_changed_callback(stm.get()); + if (r != CUBEB_OK) { + LOG("(%p) Could not install the device change callback.", stm.get()); + return r; + } + + *stream = stm.release(); + LOG("(%p) Cubeb stream init successful.", *stream); + return CUBEB_OK; +} + +static void +audiounit_close_stream(cubeb_stream * stm) +{ + stm->mutex.assert_current_thread_owns(); + + if (stm->input_unit) { + AudioUnitUninitialize(stm->input_unit); + AudioComponentInstanceDispose(stm->input_unit); + stm->input_unit = nullptr; + } + + stm->input_linear_buffer.reset(); + + if (stm->output_unit) { + AudioUnitUninitialize(stm->output_unit); + AudioComponentInstanceDispose(stm->output_unit); + stm->output_unit = nullptr; + } + + stm->resampler.reset(); + stm->mixer.reset(); + + if (stm->aggregate_device_id != kAudioObjectUnknown) { + audiounit_destroy_aggregate_device(stm->plugin_id, + &stm->aggregate_device_id); + stm->aggregate_device_id = kAudioObjectUnknown; + } +} + +static void +audiounit_stream_destroy_internal(cubeb_stream * stm) +{ + stm->context->mutex.assert_current_thread_owns(); + + int r = audiounit_uninstall_system_changed_callback(stm); + if (r != CUBEB_OK) { + LOG("(%p) Could not uninstall the device changed callback", stm); + } + r = audiounit_uninstall_device_changed_callback(stm); + if (r != CUBEB_OK) { + LOG("(%p) Could not uninstall all device change listeners", stm); + } + + auto_lock lock(stm->mutex); + audiounit_close_stream(stm); + assert(audiounit_active_streams(stm->context) >= 1); + audiounit_decrement_active_streams(stm->context); +} + +static void +audiounit_stream_destroy(cubeb_stream * stm) +{ + int r = audiounit_uninstall_system_changed_callback(stm); + if (r != CUBEB_OK) { + LOG("(%p) Could not uninstall the device changed callback", stm); + } + r = audiounit_uninstall_device_changed_callback(stm); + if (r != CUBEB_OK) { + LOG("(%p) Could not uninstall all device change listeners", stm); + } + + if (!stm->shutdown.load()) { + auto_lock context_lock(stm->context->mutex); + audiounit_stream_stop_internal(stm); + stm->shutdown = true; + } + + stm->destroy_pending = true; + // Execute close in serial queue to avoid collision + // with reinit when un/plug devices + dispatch_sync(stm->context->serial_queue, ^() { + auto_lock context_lock(stm->context->mutex); + audiounit_stream_destroy_internal(stm); + }); + + LOG("Cubeb stream (%p) destroyed successful.", stm); + delete stm; +} + +static int +audiounit_stream_start_internal(cubeb_stream * stm) +{ + OSStatus r; + if (stm->input_unit != NULL) { + r = AudioOutputUnitStart(stm->input_unit); + if (r != noErr) { + LOG("AudioOutputUnitStart (input) rv=%d", r); + return CUBEB_ERROR; + } + } + if (stm->output_unit != NULL) { + r = AudioOutputUnitStart(stm->output_unit); + if (r != noErr) { + LOG("AudioOutputUnitStart (output) rv=%d", r); + return CUBEB_ERROR; + } + } + return CUBEB_OK; +} + +static int +audiounit_stream_start(cubeb_stream * stm) +{ + auto_lock context_lock(stm->context->mutex); + stm->shutdown = false; + stm->draining = false; + + int r = audiounit_stream_start_internal(stm); + if (r != CUBEB_OK) { + return r; + } + + stm->state_callback(stm, stm->user_ptr, CUBEB_STATE_STARTED); + + LOG("Cubeb stream (%p) started successfully.", stm); + return CUBEB_OK; +} + +void +audiounit_stream_stop_internal(cubeb_stream * stm) +{ + OSStatus r; + if (stm->input_unit != NULL) { + r = AudioOutputUnitStop(stm->input_unit); + assert(r == 0); + } + if (stm->output_unit != NULL) { + r = AudioOutputUnitStop(stm->output_unit); + assert(r == 0); + } +} + +static int +audiounit_stream_stop(cubeb_stream * stm) +{ + auto_lock context_lock(stm->context->mutex); + stm->shutdown = true; + + audiounit_stream_stop_internal(stm); + + stm->state_callback(stm, stm->user_ptr, CUBEB_STATE_STOPPED); + + LOG("Cubeb stream (%p) stopped successfully.", stm); + return CUBEB_OK; +} + +static int +audiounit_stream_get_position(cubeb_stream * stm, uint64_t * position) +{ + assert(stm); + if (stm->current_latency_frames > stm->frames_played) { + *position = 0; + } else { + *position = stm->frames_played - stm->current_latency_frames; + } + return CUBEB_OK; +} + +int +audiounit_stream_get_latency(cubeb_stream * stm, uint32_t * latency) +{ +#if TARGET_OS_IPHONE + // TODO + return CUBEB_ERROR_NOT_SUPPORTED; +#else + *latency = stm->total_output_latency_frames; + return CUBEB_OK; +#endif +} + +static int +audiounit_stream_get_volume(cubeb_stream * stm, float * volume) +{ + assert(stm->output_unit); + OSStatus r = AudioUnitGetParameter(stm->output_unit, kHALOutputParam_Volume, + kAudioUnitScope_Global, 0, volume); + if (r != noErr) { + LOG("AudioUnitGetParameter/kHALOutputParam_Volume rv=%d", r); + return CUBEB_ERROR; + } + return CUBEB_OK; +} + +static int +audiounit_stream_set_volume(cubeb_stream * stm, float volume) +{ + assert(stm->output_unit); + OSStatus r; + r = AudioUnitSetParameter(stm->output_unit, kHALOutputParam_Volume, + kAudioUnitScope_Global, 0, volume, 0); + + if (r != noErr) { + LOG("AudioUnitSetParameter/kHALOutputParam_Volume rv=%d", r); + return CUBEB_ERROR; + } + return CUBEB_OK; +} + +unique_ptr<char[]> +convert_uint32_into_string(UInt32 data) +{ + // Simply create an empty string if no data. + size_t size = data == 0 ? 0 : 4; // 4 bytes for uint32. + auto str = unique_ptr<char[]>{new char[size + 1]}; // + 1 for '\0'. + str[size] = '\0'; + if (size < 4) { + return str; + } + + // Reverse 0xWXYZ into 0xZYXW. + str[0] = (char)(data >> 24); + str[1] = (char)(data >> 16); + str[2] = (char)(data >> 8); + str[3] = (char)(data); + return str; +} + +int +audiounit_get_default_device_datasource(cubeb_device_type type, UInt32 * data) +{ + AudioDeviceID id = audiounit_get_default_device_id(type); + if (id == kAudioObjectUnknown) { + return CUBEB_ERROR; + } + + UInt32 size = sizeof(*data); + /* This fails with some USB headsets (e.g., Plantronic .Audio 628). */ + OSStatus r = AudioObjectGetPropertyData( + id, + type == CUBEB_DEVICE_TYPE_INPUT ? &INPUT_DATA_SOURCE_PROPERTY_ADDRESS + : &OUTPUT_DATA_SOURCE_PROPERTY_ADDRESS, + 0, NULL, &size, data); + if (r != noErr) { + *data = 0; + } + + return CUBEB_OK; +} + +int +audiounit_get_default_device_name(cubeb_stream * stm, + cubeb_device * const device, + cubeb_device_type type) +{ + assert(stm); + assert(device); + + UInt32 data; + int r = audiounit_get_default_device_datasource(type, &data); + if (r != CUBEB_OK) { + return r; + } + char ** name = type == CUBEB_DEVICE_TYPE_INPUT ? &device->input_name + : &device->output_name; + *name = convert_uint32_into_string(data).release(); + if (!strlen(*name)) { // empty string. + LOG("(%p) name of %s device is empty!", stm, + type == CUBEB_DEVICE_TYPE_INPUT ? "input" : "output"); + } + return CUBEB_OK; +} + +int +audiounit_stream_get_current_device(cubeb_stream * stm, + cubeb_device ** const device) +{ +#if TARGET_OS_IPHONE + // TODO + return CUBEB_ERROR_NOT_SUPPORTED; +#else + *device = new cubeb_device; + if (!*device) { + return CUBEB_ERROR; + } + PodZero(*device, 1); + + int r = + audiounit_get_default_device_name(stm, *device, CUBEB_DEVICE_TYPE_OUTPUT); + if (r != CUBEB_OK) { + return r; + } + + r = audiounit_get_default_device_name(stm, *device, CUBEB_DEVICE_TYPE_INPUT); + if (r != CUBEB_OK) { + return r; + } + + return CUBEB_OK; +#endif +} + +int +audiounit_stream_device_destroy(cubeb_stream * /* stream */, + cubeb_device * device) +{ + delete[] device->output_name; + delete[] device->input_name; + delete device; + return CUBEB_OK; +} + +int +audiounit_stream_register_device_changed_callback( + cubeb_stream * stream, + cubeb_device_changed_callback device_changed_callback) +{ + auto_lock dev_cb_lock(stream->device_changed_callback_lock); + /* Note: second register without unregister first causes 'nope' error. + * Current implementation requires unregister before register a new cb. */ + assert(!device_changed_callback || !stream->device_changed_callback); + stream->device_changed_callback = device_changed_callback; + return CUBEB_OK; +} + +static char * +audiounit_strref_to_cstr_utf8(CFStringRef strref) +{ + CFIndex len, size; + char * ret; + if (strref == NULL) { + return NULL; + } + + len = CFStringGetLength(strref); + // Add 1 to size to allow for '\0' termination character. + size = CFStringGetMaximumSizeForEncoding(len, kCFStringEncodingUTF8) + 1; + ret = new char[size]; + + if (!CFStringGetCString(strref, ret, size, kCFStringEncodingUTF8)) { + delete[] ret; + ret = NULL; + } + + return ret; +} + +static uint32_t +audiounit_get_channel_count(AudioObjectID devid, AudioObjectPropertyScope scope) +{ + AudioObjectPropertyAddress adr = {0, scope, + kAudioObjectPropertyElementMaster}; + UInt32 size = 0; + uint32_t i, ret = 0; + + adr.mSelector = kAudioDevicePropertyStreamConfiguration; + + if (AudioObjectGetPropertyDataSize(devid, &adr, 0, NULL, &size) == noErr && + size > 0) { + AudioBufferList * list = static_cast<AudioBufferList *>(alloca(size)); + if (AudioObjectGetPropertyData(devid, &adr, 0, NULL, &size, list) == + noErr) { + for (i = 0; i < list->mNumberBuffers; i++) + ret += list->mBuffers[i].mNumberChannels; + } + } + + return ret; +} + +static void +audiounit_get_available_samplerate(AudioObjectID devid, + AudioObjectPropertyScope scope, + uint32_t * min, uint32_t * max, + uint32_t * def) +{ + AudioObjectPropertyAddress adr = {0, scope, + kAudioObjectPropertyElementMaster}; + + adr.mSelector = kAudioDevicePropertyNominalSampleRate; + if (AudioObjectHasProperty(devid, &adr)) { + UInt32 size = sizeof(Float64); + Float64 fvalue = 0.0; + if (AudioObjectGetPropertyData(devid, &adr, 0, NULL, &size, &fvalue) == + noErr) { + *def = fvalue; + } + } + + adr.mSelector = kAudioDevicePropertyAvailableNominalSampleRates; + UInt32 size = 0; + AudioValueRange range; + if (AudioObjectHasProperty(devid, &adr) && + AudioObjectGetPropertyDataSize(devid, &adr, 0, NULL, &size) == noErr) { + uint32_t count = size / sizeof(AudioValueRange); + vector<AudioValueRange> ranges(count); + range.mMinimum = 9999999999.0; + range.mMaximum = 0.0; + if (AudioObjectGetPropertyData(devid, &adr, 0, NULL, &size, + ranges.data()) == noErr) { + for (uint32_t i = 0; i < count; i++) { + if (ranges[i].mMaximum > range.mMaximum) + range.mMaximum = ranges[i].mMaximum; + if (ranges[i].mMinimum < range.mMinimum) + range.mMinimum = ranges[i].mMinimum; + } + } + *max = static_cast<uint32_t>(range.mMaximum); + *min = static_cast<uint32_t>(range.mMinimum); + } else { + *min = *max = 0; + } +} + +static UInt32 +audiounit_get_device_presentation_latency(AudioObjectID devid, + AudioObjectPropertyScope scope) +{ + AudioObjectPropertyAddress adr = {0, scope, + kAudioObjectPropertyElementMaster}; + UInt32 size, dev, stream = 0; + AudioStreamID sid[1]; + + adr.mSelector = kAudioDevicePropertyLatency; + size = sizeof(UInt32); + if (AudioObjectGetPropertyData(devid, &adr, 0, NULL, &size, &dev) != noErr) { + dev = 0; + } + + adr.mSelector = kAudioDevicePropertyStreams; + size = sizeof(sid); + if (AudioObjectGetPropertyData(devid, &adr, 0, NULL, &size, sid) == noErr) { + adr.mSelector = kAudioStreamPropertyLatency; + size = sizeof(UInt32); + AudioObjectGetPropertyData(sid[0], &adr, 0, NULL, &size, &stream); + } + + return dev + stream; +} + +static int +audiounit_create_device_from_hwdev(cubeb_device_info * dev_info, + AudioObjectID devid, cubeb_device_type type) +{ + AudioObjectPropertyAddress adr = {0, 0, kAudioObjectPropertyElementMaster}; + UInt32 size; + + if (type == CUBEB_DEVICE_TYPE_OUTPUT) { + adr.mScope = kAudioDevicePropertyScopeOutput; + } else if (type == CUBEB_DEVICE_TYPE_INPUT) { + adr.mScope = kAudioDevicePropertyScopeInput; + } else { + return CUBEB_ERROR; + } + + UInt32 ch = audiounit_get_channel_count(devid, adr.mScope); + if (ch == 0) { + return CUBEB_ERROR; + } + + PodZero(dev_info, 1); + + CFStringRef device_id_str = nullptr; + size = sizeof(CFStringRef); + adr.mSelector = kAudioDevicePropertyDeviceUID; + OSStatus ret = + AudioObjectGetPropertyData(devid, &adr, 0, NULL, &size, &device_id_str); + if (ret == noErr && device_id_str != NULL) { + dev_info->device_id = audiounit_strref_to_cstr_utf8(device_id_str); + static_assert(sizeof(cubeb_devid) >= sizeof(decltype(devid)), + "cubeb_devid can't represent devid"); + dev_info->devid = reinterpret_cast<cubeb_devid>(devid); + dev_info->group_id = dev_info->device_id; + CFRelease(device_id_str); + } + + CFStringRef friendly_name_str = nullptr; + UInt32 ds; + size = sizeof(UInt32); + adr.mSelector = kAudioDevicePropertyDataSource; + ret = AudioObjectGetPropertyData(devid, &adr, 0, NULL, &size, &ds); + if (ret == noErr) { + AudioValueTranslation trl = {&ds, sizeof(ds), &friendly_name_str, + sizeof(CFStringRef)}; + adr.mSelector = kAudioDevicePropertyDataSourceNameForIDCFString; + size = sizeof(AudioValueTranslation); + AudioObjectGetPropertyData(devid, &adr, 0, NULL, &size, &trl); + } + + // If there is no datasource for this device, fall back to the + // device name. + if (!friendly_name_str) { + size = sizeof(CFStringRef); + adr.mSelector = kAudioObjectPropertyName; + AudioObjectGetPropertyData(devid, &adr, 0, NULL, &size, &friendly_name_str); + } + + if (friendly_name_str) { + dev_info->friendly_name = audiounit_strref_to_cstr_utf8(friendly_name_str); + CFRelease(friendly_name_str); + } else { + // Couldn't get a datasource name nor a device name, return a + // valid string of length 0. + char * fallback_name = new char[1]; + fallback_name[0] = '\0'; + dev_info->friendly_name = fallback_name; + } + + CFStringRef vendor_name_str = nullptr; + size = sizeof(CFStringRef); + adr.mSelector = kAudioObjectPropertyManufacturer; + ret = + AudioObjectGetPropertyData(devid, &adr, 0, NULL, &size, &vendor_name_str); + if (ret == noErr && vendor_name_str != NULL) { + dev_info->vendor_name = audiounit_strref_to_cstr_utf8(vendor_name_str); + CFRelease(vendor_name_str); + } + + dev_info->type = type; + dev_info->state = CUBEB_DEVICE_STATE_ENABLED; + dev_info->preferred = (devid == audiounit_get_default_device_id(type)) + ? CUBEB_DEVICE_PREF_ALL + : CUBEB_DEVICE_PREF_NONE; + + dev_info->max_channels = ch; + dev_info->format = + (cubeb_device_fmt)CUBEB_DEVICE_FMT_ALL; /* CoreAudio supports All! */ + /* kAudioFormatFlagsAudioUnitCanonical is deprecated, prefer floating point */ + dev_info->default_format = CUBEB_DEVICE_FMT_F32NE; + audiounit_get_available_samplerate(devid, adr.mScope, &dev_info->min_rate, + &dev_info->max_rate, + &dev_info->default_rate); + + UInt32 latency = audiounit_get_device_presentation_latency(devid, adr.mScope); + + AudioValueRange range; + adr.mSelector = kAudioDevicePropertyBufferFrameSizeRange; + size = sizeof(AudioValueRange); + ret = AudioObjectGetPropertyData(devid, &adr, 0, NULL, &size, &range); + if (ret == noErr) { + dev_info->latency_lo = latency + range.mMinimum; + dev_info->latency_hi = latency + range.mMaximum; + } else { + dev_info->latency_lo = + 10 * dev_info->default_rate / 1000; /* Default to 10ms */ + dev_info->latency_hi = + 100 * dev_info->default_rate / 1000; /* Default to 100ms */ + } + + return CUBEB_OK; +} + +bool +is_aggregate_device(cubeb_device_info * device_info) +{ + assert(device_info->friendly_name); + return !strncmp(device_info->friendly_name, PRIVATE_AGGREGATE_DEVICE_NAME, + strlen(PRIVATE_AGGREGATE_DEVICE_NAME)); +} + +static int +audiounit_enumerate_devices(cubeb * /* context */, cubeb_device_type type, + cubeb_device_collection * collection) +{ + vector<AudioObjectID> input_devs; + vector<AudioObjectID> output_devs; + + // Count number of input and output devices. This is not + // necessarily the same as the count of raw devices supported by the + // system since, for example, with Soundflower installed, some + // devices may report as being both input *and* output and cubeb + // separates those into two different devices. + + if (type & CUBEB_DEVICE_TYPE_OUTPUT) { + output_devs = audiounit_get_devices_of_type(CUBEB_DEVICE_TYPE_OUTPUT); + } + + if (type & CUBEB_DEVICE_TYPE_INPUT) { + input_devs = audiounit_get_devices_of_type(CUBEB_DEVICE_TYPE_INPUT); + } + + auto devices = new cubeb_device_info[output_devs.size() + input_devs.size()]; + collection->count = 0; + + if (type & CUBEB_DEVICE_TYPE_OUTPUT) { + for (auto dev : output_devs) { + auto device = &devices[collection->count]; + auto err = audiounit_create_device_from_hwdev(device, dev, + CUBEB_DEVICE_TYPE_OUTPUT); + if (err != CUBEB_OK || is_aggregate_device(device)) { + continue; + } + collection->count += 1; + } + } + + if (type & CUBEB_DEVICE_TYPE_INPUT) { + for (auto dev : input_devs) { + auto device = &devices[collection->count]; + auto err = audiounit_create_device_from_hwdev(device, dev, + CUBEB_DEVICE_TYPE_INPUT); + if (err != CUBEB_OK || is_aggregate_device(device)) { + continue; + } + collection->count += 1; + } + } + + if (collection->count > 0) { + collection->device = devices; + } else { + delete[] devices; + collection->device = NULL; + } + + return CUBEB_OK; +} + +static void +audiounit_device_destroy(cubeb_device_info * device) +{ + delete[] device->device_id; + delete[] device->friendly_name; + delete[] device->vendor_name; +} + +static int +audiounit_device_collection_destroy(cubeb * /* context */, + cubeb_device_collection * collection) +{ + for (size_t i = 0; i < collection->count; i++) { + audiounit_device_destroy(&collection->device[i]); + } + delete[] collection->device; + + return CUBEB_OK; +} + +static vector<AudioObjectID> +audiounit_get_devices_of_type(cubeb_device_type devtype) +{ + UInt32 size = 0; + OSStatus ret = AudioObjectGetPropertyDataSize( + kAudioObjectSystemObject, &DEVICES_PROPERTY_ADDRESS, 0, NULL, &size); + if (ret != noErr) { + return vector<AudioObjectID>(); + } + vector<AudioObjectID> devices(size / sizeof(AudioObjectID)); + ret = AudioObjectGetPropertyData(kAudioObjectSystemObject, + &DEVICES_PROPERTY_ADDRESS, 0, NULL, &size, + devices.data()); + if (ret != noErr) { + 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. */ + sort(devices.begin(), devices.end(), + [](AudioObjectID a, AudioObjectID b) { return a < b; }); + + if (devtype == (CUBEB_DEVICE_TYPE_INPUT | CUBEB_DEVICE_TYPE_OUTPUT)) { + return devices; + } + + AudioObjectPropertyScope scope = (devtype == CUBEB_DEVICE_TYPE_INPUT) + ? kAudioDevicePropertyScopeInput + : kAudioDevicePropertyScopeOutput; + + vector<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.push_back(devices[i]); + } + } + + return devices_in_scope; +} + +static OSStatus +audiounit_collection_changed_callback( + AudioObjectID /* inObjectID */, UInt32 /* inNumberAddresses */, + const AudioObjectPropertyAddress * /* inAddresses */, void * inClientData) +{ + cubeb * context = static_cast<cubeb *>(inClientData); + + // This can be called from inside an AudioUnit function, dispatch to another + // queue. + dispatch_async(context->serial_queue, ^() { + auto_lock lock(context->mutex); + if (!context->input_collection_changed_callback && + !context->output_collection_changed_callback) { + /* Listener removed while waiting in mutex, abort. */ + return; + } + if (context->input_collection_changed_callback) { + vector<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; +} + +static OSStatus +audiounit_add_device_listener( + cubeb * context, cubeb_device_type devtype, + cubeb_device_collection_changed_callback collection_changed_callback, + void * user_ptr) +{ + context->mutex.assert_current_thread_owns(); + assert(devtype & (CUBEB_DEVICE_TYPE_INPUT | CUBEB_DEVICE_TYPE_OUTPUT)); + /* Note: second register without unregister first causes 'nope' error. + * Current implementation requires unregister before register a new cb. */ + assert((devtype & CUBEB_DEVICE_TYPE_INPUT) && + !context->input_collection_changed_callback || + (devtype & CUBEB_DEVICE_TYPE_OUTPUT) && + !context->output_collection_changed_callback); + + if (!context->input_collection_changed_callback && + !context->output_collection_changed_callback) { + OSStatus ret = AudioObjectAddPropertyListener( + kAudioObjectSystemObject, &DEVICES_PROPERTY_ADDRESS, + audiounit_collection_changed_callback, context); + if (ret != noErr) { + return ret; + } + } + if (devtype & CUBEB_DEVICE_TYPE_INPUT) { + /* Expected empty after unregister. */ + assert(context->input_device_array.empty()); + context->input_device_array = + audiounit_get_devices_of_type(CUBEB_DEVICE_TYPE_INPUT); + context->input_collection_changed_callback = collection_changed_callback; + context->input_collection_changed_user_ptr = user_ptr; + } + if (devtype & CUBEB_DEVICE_TYPE_OUTPUT) { + /* Expected empty after unregister. */ + assert(context->output_device_array.empty()); + context->output_device_array = + audiounit_get_devices_of_type(CUBEB_DEVICE_TYPE_OUTPUT); + context->output_collection_changed_callback = collection_changed_callback; + context->output_collection_changed_user_ptr = user_ptr; + } + return noErr; +} + +static OSStatus +audiounit_remove_device_listener(cubeb * context, cubeb_device_type devtype) +{ + context->mutex.assert_current_thread_owns(); + + if (devtype & CUBEB_DEVICE_TYPE_INPUT) { + context->input_collection_changed_callback = nullptr; + context->input_collection_changed_user_ptr = nullptr; + context->input_device_array.clear(); + } + if (devtype & CUBEB_DEVICE_TYPE_OUTPUT) { + context->output_collection_changed_callback = nullptr; + context->output_collection_changed_user_ptr = nullptr; + context->output_device_array.clear(); + } + + if (context->input_collection_changed_callback || + context->output_collection_changed_callback) { + return noErr; + } + /* Note: unregister a non registered cb is not a problem, not checking. */ + return AudioObjectRemovePropertyListener( + kAudioObjectSystemObject, &DEVICES_PROPERTY_ADDRESS, + audiounit_collection_changed_callback, context); +} + +int +audiounit_register_device_collection_changed( + cubeb * context, cubeb_device_type devtype, + cubeb_device_collection_changed_callback collection_changed_callback, + void * user_ptr) +{ + if (devtype == CUBEB_DEVICE_TYPE_UNKNOWN) { + return CUBEB_ERROR_INVALID_PARAMETER; + } + OSStatus ret; + auto_lock lock(context->mutex); + if (collection_changed_callback) { + ret = audiounit_add_device_listener(context, devtype, + collection_changed_callback, user_ptr); + } else { + ret = audiounit_remove_device_listener(context, devtype); + } + return (ret == noErr) ? CUBEB_OK : CUBEB_ERROR; +} + +cubeb_ops const audiounit_ops = { + /*.init =*/audiounit_init, + /*.get_backend_id =*/audiounit_get_backend_id, + /*.get_max_channel_count =*/audiounit_get_max_channel_count, + /*.get_min_latency =*/audiounit_get_min_latency, + /*.get_preferred_sample_rate =*/audiounit_get_preferred_sample_rate, + /*.get_supported_input_processing_params =*/NULL, + /*.enumerate_devices =*/audiounit_enumerate_devices, + /*.device_collection_destroy =*/audiounit_device_collection_destroy, + /*.destroy =*/audiounit_destroy, + /*.stream_init =*/audiounit_stream_init, + /*.stream_destroy =*/audiounit_stream_destroy, + /*.stream_start =*/audiounit_stream_start, + /*.stream_stop =*/audiounit_stream_stop, + /*.stream_get_position =*/audiounit_stream_get_position, + /*.stream_get_latency =*/audiounit_stream_get_latency, + /*.stream_get_input_latency =*/NULL, + /*.stream_set_volume =*/audiounit_stream_set_volume, + /*.stream_set_name =*/NULL, + /*.stream_get_current_device =*/audiounit_stream_get_current_device, + /*.stream_set_input_mute =*/NULL, + /*.stream_set_input_processing_params =*/NULL, + /*.stream_device_destroy =*/audiounit_stream_device_destroy, + /*.stream_register_device_changed_callback =*/ + audiounit_stream_register_device_changed_callback, + /*.register_device_collection_changed =*/ + audiounit_register_device_collection_changed}; diff --git a/media/libcubeb/src/cubeb_jack.cpp b/media/libcubeb/src/cubeb_jack.cpp new file mode 100644 index 0000000000..b417078fc3 --- /dev/null +++ b/media/libcubeb/src/cubeb_jack.cpp @@ -0,0 +1,1183 @@ +/* + * Copyright © 2012 David Richards + * Copyright © 2013 Sebastien Alaiwan + * Copyright © 2016 Damien Zammit + * + * This program is made available under an ISC-style license. See the + * accompanying file LICENSE for details. + */ +#define _DEFAULT_SOURCE +#define _BSD_SOURCE +#if !defined(__FreeBSD__) && !defined(__NetBSD__) +#define _POSIX_SOURCE +#endif +#include "cubeb-internal.h" +#include "cubeb/cubeb.h" +#include "cubeb_resampler.h" +#include "cubeb_utils.h" +#include <dlfcn.h> +#include <limits.h> +#include <math.h> +#include <pthread.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#include <jack/jack.h> +#include <jack/statistics.h> + +#ifdef DISABLE_LIBJACK_DLOPEN +#define WRAP(x) x +#else +#define WRAP(x) (*api_##x) +#define JACK_API_VISIT(X) \ + X(jack_activate) \ + X(jack_client_close) \ + X(jack_client_open) \ + X(jack_connect) \ + X(jack_free) \ + X(jack_get_ports) \ + X(jack_get_sample_rate) \ + X(jack_get_xrun_delayed_usecs) \ + X(jack_get_buffer_size) \ + X(jack_port_get_buffer) \ + X(jack_port_name) \ + X(jack_port_register) \ + X(jack_port_unregister) \ + X(jack_port_get_latency_range) \ + X(jack_set_process_callback) \ + X(jack_set_xrun_callback) \ + X(jack_set_graph_order_callback) \ + X(jack_set_error_function) \ + X(jack_set_info_function) + +#define IMPORT_FUNC(x) static decltype(x) * api_##x; +JACK_API_VISIT(IMPORT_FUNC); +#undef IMPORT_FUNC +#endif + +#define JACK_DEFAULT_IN "JACK capture" +#define JACK_DEFAULT_OUT "JACK playback" + +static const int MAX_STREAMS = 16; +static const int MAX_CHANNELS = 8; +static const int FIFO_SIZE = 4096 * sizeof(float); + +enum devstream { + NONE = 0, + IN_ONLY, + OUT_ONLY, + DUPLEX, +}; + +enum cbjack_connect_ports_options { + CBJACK_CP_OPTIONS_NONE = 0x0, + CBJACK_CP_OPTIONS_SKIP_OUTPUT = 0x1, + CBJACK_CP_OPTIONS_SKIP_INPUT = 0x2, +}; + +static void +s16ne_to_float(float * dst, const int16_t * src, size_t n) +{ + for (size_t i = 0; i < n; i++) + *(dst++) = (float)((float)*(src++) / 32767.0f); +} + +static void +float_to_s16ne(int16_t * dst, float * src, size_t n) +{ + for (size_t i = 0; i < n; i++) { + if (*src > 1.f) + *src = 1.f; + if (*src < -1.f) + *src = -1.f; + *(dst++) = (int16_t)((int16_t)(*(src++) * 32767)); + } +} + +extern "C" { +/*static*/ int +jack_init(cubeb ** context, char const * context_name); +} +static char const * +cbjack_get_backend_id(cubeb * context); +static int +cbjack_get_max_channel_count(cubeb * ctx, uint32_t * max_channels); +static int +cbjack_get_min_latency(cubeb * ctx, cubeb_stream_params params, + uint32_t * latency_frames); +static int +cbjack_get_latency(cubeb_stream * stm, unsigned int * latency_frames); +static int +cbjack_get_preferred_sample_rate(cubeb * ctx, uint32_t * rate); +static void +cbjack_destroy(cubeb * context); +static void +cbjack_interleave_capture(cubeb_stream * stream, float ** in, + jack_nframes_t nframes, bool format_mismatch); +static void +cbjack_deinterleave_playback_refill_s16ne(cubeb_stream * stream, + short ** bufs_in, float ** bufs_out, + jack_nframes_t nframes); +static void +cbjack_deinterleave_playback_refill_float(cubeb_stream * stream, + float ** bufs_in, float ** bufs_out, + jack_nframes_t nframes); +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); +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, + 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); +static void +cbjack_stream_destroy(cubeb_stream * stream); +static int +cbjack_stream_start(cubeb_stream * stream); +static int +cbjack_stream_stop(cubeb_stream * stream); +static int +cbjack_stream_get_position(cubeb_stream * stream, uint64_t * position); +static int +cbjack_stream_set_volume(cubeb_stream * stm, float volume); + +static struct cubeb_ops const cbjack_ops = { + .init = jack_init, + .get_backend_id = cbjack_get_backend_id, + .get_max_channel_count = cbjack_get_max_channel_count, + .get_min_latency = cbjack_get_min_latency, + .get_preferred_sample_rate = cbjack_get_preferred_sample_rate, + .get_supported_input_processing_params = NULL, + .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_get_position = cbjack_stream_get_position, + .stream_get_latency = cbjack_get_latency, + .stream_get_input_latency = NULL, + .stream_set_volume = cbjack_stream_set_volume, + .stream_set_name = NULL, + .stream_get_current_device = cbjack_stream_get_current_device, + .stream_set_input_mute = NULL, + .stream_set_input_processing_params = NULL, + .stream_device_destroy = cbjack_stream_device_destroy, + .stream_register_device_changed_callback = NULL, + .register_device_collection_changed = NULL}; + +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; + + bool in_use; /**< Set to false iff the stream is free */ + bool ports_ready; /**< Set to true iff the JACK ports are ready */ + + cubeb_data_callback data_callback; + cubeb_state_callback state_callback; + cubeb_stream_params in_params; + cubeb_stream_params out_params; + + cubeb_resampler * resampler; + + uint64_t position; + bool pause; + float ratio; + enum devstream devs; + char stream_name[256]; + jack_port_t * output_ports[MAX_CHANNELS]; + jack_port_t * input_ports[MAX_CHANNELS]; + float volume; +}; + +struct cubeb { + struct cubeb_ops const * ops; + void * libjack; + + /**< Mutex for whole context */ + pthread_mutex_t mutex; + + /**< Audio buffers, converted to float */ + float in_float_interleaved_buffer[FIFO_SIZE * MAX_CHANNELS]; + float out_float_interleaved_buffer[FIFO_SIZE * MAX_CHANNELS]; + + /**< Audio buffer, at the sampling rate of the output */ + float in_resampled_interleaved_buffer_float[FIFO_SIZE * MAX_CHANNELS * 3]; + int16_t in_resampled_interleaved_buffer_s16ne[FIFO_SIZE * MAX_CHANNELS * 3]; + float out_resampled_interleaved_buffer_float[FIFO_SIZE * MAX_CHANNELS * 3]; + int16_t out_resampled_interleaved_buffer_s16ne[FIFO_SIZE * MAX_CHANNELS * 3]; + + cubeb_stream streams[MAX_STREAMS]; + unsigned int active_streams; + + cubeb_device_collection_changed_callback collection_changed_callback; + + bool active; + unsigned int jack_sample_rate; + unsigned int jack_latency; + unsigned int jack_xruns; + unsigned int jack_buffer_size; + unsigned int fragment_size; + unsigned int output_bytes_per_frame; + jack_client_t * jack_client; +}; + +static int +load_jack_lib(cubeb * context) +{ +#ifndef DISABLE_LIBJACK_DLOPEN +#ifdef __APPLE__ + context->libjack = dlopen("libjack.0.dylib", RTLD_LAZY); + context->libjack = dlopen("/usr/local/lib/libjack.0.dylib", RTLD_LAZY); +#elif defined(__WIN32__) +#ifdef _WIN64 + context->libjack = LoadLibrary("libjack64.dll"); +#else + context->libjack = LoadLibrary("libjack.dll"); +#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; + } + +#define LOAD(x) \ + { \ + api_##x = (decltype(x) *)dlsym(context->libjack, #x); \ + if (!api_##x) { \ + dlclose(context->libjack); \ + return CUBEB_ERROR; \ + } \ + } + + JACK_API_VISIT(LOAD); +#undef LOAD +#endif + return CUBEB_OK; +} + +static void +cbjack_connect_port_out(cubeb_stream * stream, const size_t out_port, + const char * const phys_in_port) +{ + const char * src_port = WRAP(jack_port_name)(stream->output_ports[out_port]); + + WRAP(jack_connect)(stream->context->jack_client, src_port, phys_in_port); +} + +static void +cbjack_connect_port_in(cubeb_stream * stream, const char * const phys_out_port, + size_t in_port) +{ + const char * src_port = WRAP(jack_port_name)(stream->input_ports[in_port]); + + WRAP(jack_connect)(stream->context->jack_client, phys_out_port, src_port); +} + +static int +cbjack_connect_ports(cubeb_stream * stream, + enum cbjack_connect_ports_options options) +{ + int r = CUBEB_ERROR; + const char ** phys_in_ports = + WRAP(jack_get_ports)(stream->context->jack_client, NULL, NULL, + JackPortIsInput | JackPortIsPhysical); + const char ** phys_out_ports = + WRAP(jack_get_ports)(stream->context->jack_client, NULL, NULL, + JackPortIsOutput | JackPortIsPhysical); + + if (phys_in_ports == NULL || *phys_in_ports == NULL || + options & CBJACK_CP_OPTIONS_SKIP_OUTPUT) { + goto skipplayback; + } + + // Connect outputs to playback + for (unsigned int c = 0; + c < stream->out_params.channels && phys_in_ports[c] != NULL; c++) { + cbjack_connect_port_out(stream, c, phys_in_ports[c]); + } + + // Special case playing mono source in stereo + if (stream->out_params.channels == 1 && phys_in_ports[1] != NULL) { + cbjack_connect_port_out(stream, 0, phys_in_ports[1]); + } + + r = CUBEB_OK; + +skipplayback: + if (phys_out_ports == NULL || *phys_out_ports == NULL || + options & CBJACK_CP_OPTIONS_SKIP_INPUT) { + goto end; + } + // Connect inputs to capture + for (unsigned int c = 0; + c < stream->in_params.channels && phys_out_ports[c] != NULL; c++) { + cbjack_connect_port_in(stream, phys_out_ports[c], c); + } + r = CUBEB_OK; +end: + if (phys_out_ports) { + WRAP(jack_free)(phys_out_ports); + } + if (phys_in_ports) { + WRAP(jack_free)(phys_in_ports); + } + return r; +} + +static int +cbjack_xrun_callback(void * arg) +{ + cubeb * ctx = (cubeb *)arg; + + float delay = WRAP(jack_get_xrun_delayed_usecs)(ctx->jack_client); + float fragments = ceilf(((delay / 1000000.0) * ctx->jack_sample_rate) / + ctx->jack_buffer_size); + + ctx->jack_xruns += (unsigned int)fragments; + return 0; +} + +static int +cbjack_graph_order_callback(void * arg) +{ + cubeb * ctx = (cubeb *)arg; + int i; + jack_latency_range_t latency_range; + jack_nframes_t port_latency, max_latency = 0; + + for (int j = 0; j < MAX_STREAMS; j++) { + cubeb_stream * stm = &ctx->streams[j]; + + if (!stm->in_use) + continue; + if (!stm->ports_ready) + continue; + + for (i = 0; i < (int)stm->out_params.channels; ++i) { + WRAP(jack_port_get_latency_range) + (stm->output_ports[i], JackPlaybackLatency, &latency_range); + port_latency = latency_range.max; + if (port_latency > max_latency) + max_latency = port_latency; + } + /* Cap minimum latency to 128 frames */ + if (max_latency < 128) + max_latency = 128; + } + + ctx->jack_latency = max_latency; + + return 0; +} + +static int +cbjack_process(jack_nframes_t nframes, void * arg) +{ + cubeb * ctx = (cubeb *)arg; + unsigned int t_jack_xruns = ctx->jack_xruns; + int i; + + ctx->jack_xruns = 0; + + for (int j = 0; j < MAX_STREAMS; j++) { + cubeb_stream * stm = &ctx->streams[j]; + float * bufs_out[stm->out_params.channels]; + float * bufs_in[stm->in_params.channels]; + + if (!stm->in_use) + continue; + + // handle xruns by skipping audio that should have been played + stm->position += t_jack_xruns * ctx->fragment_size * stm->ratio; + + if (!stm->ports_ready) + continue; + + if (stm->devs & OUT_ONLY) { + // get jack output buffers + for (i = 0; i < (int)stm->out_params.channels; i++) + bufs_out[i] = + (float *)WRAP(jack_port_get_buffer)(stm->output_ports[i], nframes); + } + if (stm->devs & IN_ONLY) { + // get jack input buffers + for (i = 0; i < (int)stm->in_params.channels; i++) + bufs_in[i] = + (float *)WRAP(jack_port_get_buffer)(stm->input_ports[i], nframes); + } + if (stm->pause) { + // paused, play silence on output + if (stm->devs & OUT_ONLY) { + for (unsigned int c = 0; c < stm->out_params.channels; c++) { + float * buffer_out = bufs_out[c]; + if (buffer_out) { + for (long f = 0; f < nframes; f++) { + buffer_out[f] = 0.f; + } + } + } + } + if (stm->devs & IN_ONLY) { + // paused, capture silence + for (unsigned int c = 0; c < stm->in_params.channels; c++) { + float * buffer_in = bufs_in[c]; + if (buffer_in) { + for (long f = 0; f < nframes; f++) { + buffer_in[f] = 0.f; + } + } + } + } + } else { + + // try to lock stream mutex + if (pthread_mutex_trylock(&stm->mutex) == 0) { + + int16_t * in_s16ne = + stm->context->in_resampled_interleaved_buffer_s16ne; + float * in_float = stm->context->in_resampled_interleaved_buffer_float; + + // unpaused, play audio + if (stm->devs == DUPLEX) { + if (stm->out_params.format == CUBEB_SAMPLE_S16NE) { + cbjack_interleave_capture(stm, bufs_in, nframes, true); + cbjack_deinterleave_playback_refill_s16ne(stm, &in_s16ne, bufs_out, + nframes); + } else if (stm->out_params.format == CUBEB_SAMPLE_FLOAT32NE) { + cbjack_interleave_capture(stm, bufs_in, nframes, false); + cbjack_deinterleave_playback_refill_float(stm, &in_float, bufs_out, + nframes); + } + } else if (stm->devs == IN_ONLY) { + if (stm->in_params.format == CUBEB_SAMPLE_S16NE) { + cbjack_interleave_capture(stm, bufs_in, nframes, true); + cbjack_deinterleave_playback_refill_s16ne(stm, &in_s16ne, nullptr, + nframes); + } else if (stm->in_params.format == CUBEB_SAMPLE_FLOAT32NE) { + cbjack_interleave_capture(stm, bufs_in, nframes, false); + cbjack_deinterleave_playback_refill_float(stm, &in_float, nullptr, + nframes); + } + } else if (stm->devs == OUT_ONLY) { + if (stm->out_params.format == CUBEB_SAMPLE_S16NE) { + cbjack_deinterleave_playback_refill_s16ne(stm, nullptr, bufs_out, + nframes); + } else if (stm->out_params.format == CUBEB_SAMPLE_FLOAT32NE) { + cbjack_deinterleave_playback_refill_float(stm, nullptr, bufs_out, + nframes); + } + } + // unlock stream mutex + pthread_mutex_unlock(&stm->mutex); + + } else { + // could not lock mutex + // output silence + if (stm->devs & OUT_ONLY) { + for (unsigned int c = 0; c < stm->out_params.channels; c++) { + float * buffer_out = bufs_out[c]; + if (buffer_out) { + for (long f = 0; f < nframes; f++) { + buffer_out[f] = 0.f; + } + } + } + } + if (stm->devs & IN_ONLY) { + // capture silence + for (unsigned int c = 0; c < stm->in_params.channels; c++) { + float * buffer_in = bufs_in[c]; + if (buffer_in) { + for (long f = 0; f < nframes; f++) { + buffer_in[f] = 0.f; + } + } + } + } + } + } + } + return 0; +} + +static void +cbjack_deinterleave_playback_refill_float(cubeb_stream * stream, float ** in, + float ** bufs_out, + jack_nframes_t nframes) +{ + float * out_interleaved_buffer = nullptr; + + float * inptr = (in != NULL) ? *in : nullptr; + float * outptr = (bufs_out != NULL) ? *bufs_out : nullptr; + + long needed_frames = (bufs_out != NULL) ? nframes : 0; + long done_frames = 0; + long input_frames_count = (in != NULL) ? nframes : 0; + + done_frames = cubeb_resampler_fill( + stream->resampler, inptr, &input_frames_count, + (bufs_out != NULL) + ? stream->context->out_resampled_interleaved_buffer_float + : NULL, + needed_frames); + + out_interleaved_buffer = + stream->context->out_resampled_interleaved_buffer_float; + + if (outptr) { + // convert interleaved output buffers to contiguous buffers + for (unsigned int c = 0; c < stream->out_params.channels; c++) { + float * buffer = bufs_out[c]; + for (long f = 0; f < done_frames; f++) { + if (buffer) { + buffer[f] = + out_interleaved_buffer[(f * stream->out_params.channels) + c] * + stream->volume; + } + } + if (done_frames < needed_frames) { + // draining + for (long f = done_frames; f < needed_frames; f++) { + if (buffer) { + buffer[f] = 0.f; + } + } + } + if (done_frames == 0) { + // stop, but first zero out the existing buffer + for (long f = 0; f < needed_frames; f++) { + if (buffer) { + buffer[f] = 0.f; + } + } + } + } + } + + if (done_frames >= 0 && done_frames < needed_frames) { + // set drained + stream->state_callback(stream, stream->user_ptr, CUBEB_STATE_DRAINED); + // stop stream + cbjack_stream_stop(stream); + } + if (done_frames > 0 && done_frames <= needed_frames) { + // advance stream position + stream->position += done_frames * stream->ratio; + } + if (done_frames < 0 || done_frames > needed_frames) { + // stream error + stream->state_callback(stream, stream->user_ptr, CUBEB_STATE_ERROR); + } +} + +static void +cbjack_deinterleave_playback_refill_s16ne(cubeb_stream * stream, short ** in, + float ** bufs_out, + jack_nframes_t nframes) +{ + float * out_interleaved_buffer = nullptr; + + short * inptr = (in != NULL) ? *in : nullptr; + float * outptr = (bufs_out != NULL) ? *bufs_out : nullptr; + + long needed_frames = (bufs_out != NULL) ? nframes : 0; + long done_frames = 0; + long input_frames_count = (in != NULL) ? nframes : 0; + + done_frames = cubeb_resampler_fill( + stream->resampler, inptr, &input_frames_count, + (bufs_out != NULL) + ? stream->context->out_resampled_interleaved_buffer_s16ne + : NULL, + needed_frames); + + s16ne_to_float(stream->context->out_resampled_interleaved_buffer_float, + stream->context->out_resampled_interleaved_buffer_s16ne, + done_frames * stream->out_params.channels); + + out_interleaved_buffer = + stream->context->out_resampled_interleaved_buffer_float; + + if (outptr) { + // convert interleaved output buffers to contiguous buffers + for (unsigned int c = 0; c < stream->out_params.channels; c++) { + float * buffer = bufs_out[c]; + for (long f = 0; f < done_frames; f++) { + buffer[f] = + out_interleaved_buffer[(f * stream->out_params.channels) + c] * + stream->volume; + } + if (done_frames < needed_frames) { + // draining + for (long f = done_frames; f < needed_frames; f++) { + buffer[f] = 0.f; + } + } + if (done_frames == 0) { + // stop, but first zero out the existing buffer + for (long f = 0; f < needed_frames; f++) { + buffer[f] = 0.f; + } + } + } + } + + if (done_frames >= 0 && done_frames < needed_frames) { + // set drained + stream->state_callback(stream, stream->user_ptr, CUBEB_STATE_DRAINED); + // stop stream + cbjack_stream_stop(stream); + } + if (done_frames > 0 && done_frames <= needed_frames) { + // advance stream position + stream->position += done_frames * stream->ratio; + } + if (done_frames < 0 || done_frames > needed_frames) { + // stream error + stream->state_callback(stream, stream->user_ptr, CUBEB_STATE_ERROR); + } +} + +static void +cbjack_interleave_capture(cubeb_stream * stream, float ** in, + jack_nframes_t nframes, bool format_mismatch) +{ + float * in_buffer = stream->context->in_float_interleaved_buffer; + + for (unsigned int c = 0; c < stream->in_params.channels; c++) { + for (long f = 0; f < nframes; f++) { + in_buffer[(f * stream->in_params.channels) + c] = + in[c][f] * stream->volume; + } + } + if (format_mismatch) { + float_to_s16ne(stream->context->in_resampled_interleaved_buffer_s16ne, + in_buffer, nframes * stream->in_params.channels); + } else { + memset(stream->context->in_resampled_interleaved_buffer_float, 0, + (FIFO_SIZE * MAX_CHANNELS * 3) * sizeof(float)); + memcpy(stream->context->in_resampled_interleaved_buffer_float, in_buffer, + (FIFO_SIZE * MAX_CHANNELS * 2) * sizeof(float)); + } +} + +static void +silent_jack_error_callback(char const * /*msg*/) +{ +} + +/*static*/ int +jack_init(cubeb ** context, char const * context_name) +{ + int r; + + *context = NULL; + + cubeb * ctx = (cubeb *)calloc(1, sizeof(*ctx)); + if (ctx == NULL) { + return CUBEB_ERROR; + } + + r = load_jack_lib(ctx); + if (r != 0) { + cbjack_destroy(ctx); + return CUBEB_ERROR; + } + + WRAP(jack_set_error_function)(silent_jack_error_callback); + WRAP(jack_set_info_function)(silent_jack_error_callback); + + ctx->ops = &cbjack_ops; + + ctx->mutex = PTHREAD_MUTEX_INITIALIZER; + for (r = 0; r < MAX_STREAMS; r++) { + ctx->streams[r].mutex = PTHREAD_MUTEX_INITIALIZER; + } + + const char * jack_client_name = "cubeb"; + if (context_name) + jack_client_name = context_name; + + ctx->jack_client = + WRAP(jack_client_open)(jack_client_name, JackNoStartServer, NULL); + + if (ctx->jack_client == NULL) { + cbjack_destroy(ctx); + return CUBEB_ERROR; + } + + ctx->jack_xruns = 0; + + WRAP(jack_set_process_callback)(ctx->jack_client, cbjack_process, ctx); + WRAP(jack_set_xrun_callback)(ctx->jack_client, cbjack_xrun_callback, ctx); + WRAP(jack_set_graph_order_callback) + (ctx->jack_client, cbjack_graph_order_callback, ctx); + + if (WRAP(jack_activate)(ctx->jack_client)) { + cbjack_destroy(ctx); + return CUBEB_ERROR; + } + + ctx->jack_sample_rate = WRAP(jack_get_sample_rate)(ctx->jack_client); + ctx->jack_latency = 128 * 1000 / ctx->jack_sample_rate; + + ctx->active = true; + *context = ctx; + + return CUBEB_OK; +} + +static char const * +cbjack_get_backend_id(cubeb * /*context*/) +{ + return "jack"; +} + +static int +cbjack_get_max_channel_count(cubeb * /*ctx*/, uint32_t * max_channels) +{ + *max_channels = MAX_CHANNELS; + return CUBEB_OK; +} + +static int +cbjack_get_latency(cubeb_stream * stm, unsigned int * latency_ms) +{ + *latency_ms = stm->context->jack_latency; + return CUBEB_OK; +} + +static int +cbjack_get_min_latency(cubeb * ctx, cubeb_stream_params /*params*/, + uint32_t * latency_ms) +{ + *latency_ms = ctx->jack_latency; + return CUBEB_OK; +} + +static int +cbjack_get_preferred_sample_rate(cubeb * ctx, uint32_t * rate) +{ + if (!ctx->jack_client) { + jack_client_t * testclient = + WRAP(jack_client_open)("test-samplerate", JackNoStartServer, NULL); + if (!testclient) { + return CUBEB_ERROR; + } + + *rate = WRAP(jack_get_sample_rate)(testclient); + WRAP(jack_client_close)(testclient); + + } else { + *rate = WRAP(jack_get_sample_rate)(ctx->jack_client); + } + return CUBEB_OK; +} + +static void +cbjack_destroy(cubeb * context) +{ + context->active = false; + + if (context->jack_client != NULL) + WRAP(jack_client_close)(context->jack_client); +#ifndef DISABLE_LIBJACK_DLOPEN + if (context->libjack) + dlclose(context->libjack); +#endif + free(context); +} + +static cubeb_stream * +context_alloc_stream(cubeb * context, char const * stream_name) +{ + for (int i = 0; i < MAX_STREAMS; i++) { + if (!context->streams[i].in_use) { + cubeb_stream * stm = &context->streams[i]; + stm->in_use = true; + snprintf(stm->stream_name, 255, "%s_%u", stream_name, i); + return stm; + } + } + return NULL; +} + +static int +cbjack_stream_init(cubeb * context, cubeb_stream ** stream, + char const * stream_name, cubeb_devid input_device, + cubeb_stream_params * input_stream_params, + cubeb_devid output_device, + cubeb_stream_params * output_stream_params, + unsigned int /*latency_frames*/, + cubeb_data_callback data_callback, + cubeb_state_callback state_callback, void * user_ptr) +{ + int stream_actual_rate = 0; + int jack_rate = WRAP(jack_get_sample_rate)(context->jack_client); + + if (output_stream_params && + (output_stream_params->format != CUBEB_SAMPLE_FLOAT32NE && + output_stream_params->format != CUBEB_SAMPLE_S16NE)) { + return CUBEB_ERROR_INVALID_FORMAT; + } + + if (input_stream_params && + (input_stream_params->format != CUBEB_SAMPLE_FLOAT32NE && + input_stream_params->format != CUBEB_SAMPLE_S16NE)) { + return CUBEB_ERROR_INVALID_FORMAT; + } + + if ((input_device && input_device != JACK_DEFAULT_IN) || + (output_device && output_device != JACK_DEFAULT_OUT)) { + 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. + pthread_mutex_lock(&context->mutex); + cubeb_stream * stm = context_alloc_stream(context, stream_name); + + // No free stream? + if (stm == NULL) { + pthread_mutex_unlock(&context->mutex); + return CUBEB_ERROR; + } + + // unlock context mutex + pthread_mutex_unlock(&context->mutex); + + // Lock active stream + pthread_mutex_lock(&stm->mutex); + + stm->ports_ready = false; + stm->user_ptr = user_ptr; + stm->context = context; + stm->devs = NONE; + if (output_stream_params && !input_stream_params) { + stm->out_params = *output_stream_params; + stream_actual_rate = stm->out_params.rate; + stm->out_params.rate = jack_rate; + stm->devs = OUT_ONLY; + if (stm->out_params.format == CUBEB_SAMPLE_FLOAT32NE) { + context->output_bytes_per_frame = sizeof(float); + } else { + context->output_bytes_per_frame = sizeof(short); + } + } + if (input_stream_params && output_stream_params) { + stm->in_params = *input_stream_params; + stm->out_params = *output_stream_params; + stream_actual_rate = stm->out_params.rate; + stm->in_params.rate = jack_rate; + stm->out_params.rate = jack_rate; + stm->devs = DUPLEX; + if (stm->out_params.format == CUBEB_SAMPLE_FLOAT32NE) { + context->output_bytes_per_frame = sizeof(float); + stm->in_params.format = CUBEB_SAMPLE_FLOAT32NE; + } else { + context->output_bytes_per_frame = sizeof(short); + stm->in_params.format = CUBEB_SAMPLE_S16NE; + } + } else if (input_stream_params && !output_stream_params) { + stm->in_params = *input_stream_params; + stream_actual_rate = stm->in_params.rate; + stm->in_params.rate = jack_rate; + stm->devs = IN_ONLY; + if (stm->in_params.format == CUBEB_SAMPLE_FLOAT32NE) { + context->output_bytes_per_frame = sizeof(float); + } else { + context->output_bytes_per_frame = sizeof(short); + } + } + + stm->ratio = (float)stream_actual_rate / (float)jack_rate; + + stm->data_callback = data_callback; + stm->state_callback = state_callback; + stm->position = 0; + stm->volume = 1.0f; + context->jack_buffer_size = WRAP(jack_get_buffer_size)(context->jack_client); + context->fragment_size = context->jack_buffer_size; + + if (stm->devs == NONE) { + pthread_mutex_unlock(&stm->mutex); + return CUBEB_ERROR; + } + + stm->resampler = NULL; + + if (stm->devs == DUPLEX) { + stm->resampler = cubeb_resampler_create( + stm, &stm->in_params, &stm->out_params, stream_actual_rate, + stm->data_callback, stm->user_ptr, CUBEB_RESAMPLER_QUALITY_DESKTOP, + CUBEB_RESAMPLER_RECLOCK_NONE); + } else if (stm->devs == IN_ONLY) { + stm->resampler = cubeb_resampler_create( + stm, &stm->in_params, nullptr, stream_actual_rate, stm->data_callback, + stm->user_ptr, CUBEB_RESAMPLER_QUALITY_DESKTOP, + CUBEB_RESAMPLER_RECLOCK_NONE); + } else if (stm->devs == OUT_ONLY) { + stm->resampler = cubeb_resampler_create( + stm, nullptr, &stm->out_params, stream_actual_rate, stm->data_callback, + stm->user_ptr, CUBEB_RESAMPLER_QUALITY_DESKTOP, + CUBEB_RESAMPLER_RECLOCK_NONE); + } + + if (!stm->resampler) { + stm->in_use = false; + pthread_mutex_unlock(&stm->mutex); + return CUBEB_ERROR; + } + + if (stm->devs == DUPLEX || stm->devs == OUT_ONLY) { + for (unsigned int c = 0; c < stm->out_params.channels; c++) { + char portname[256]; + snprintf(portname, 255, "%s_out_%d", stm->stream_name, c); + stm->output_ports[c] = WRAP(jack_port_register)( + stm->context->jack_client, portname, JACK_DEFAULT_AUDIO_TYPE, + JackPortIsOutput, 0); + if (!(output_stream_params->prefs & + CUBEB_STREAM_PREF_JACK_NO_AUTO_CONNECT)) { + if (cbjack_connect_ports(stm, CBJACK_CP_OPTIONS_SKIP_INPUT) != + CUBEB_OK) { + pthread_mutex_unlock(&stm->mutex); + cbjack_stream_destroy(stm); + return CUBEB_ERROR; + } + } + } + } + + if (stm->devs == DUPLEX || stm->devs == IN_ONLY) { + for (unsigned int c = 0; c < stm->in_params.channels; c++) { + char portname[256]; + snprintf(portname, 255, "%s_in_%d", stm->stream_name, c); + stm->input_ports[c] = + WRAP(jack_port_register)(stm->context->jack_client, portname, + JACK_DEFAULT_AUDIO_TYPE, JackPortIsInput, 0); + if (!(input_stream_params->prefs & + CUBEB_STREAM_PREF_JACK_NO_AUTO_CONNECT)) { + if (cbjack_connect_ports(stm, CBJACK_CP_OPTIONS_SKIP_OUTPUT) != + CUBEB_OK) { + pthread_mutex_unlock(&stm->mutex); + cbjack_stream_destroy(stm); + return CUBEB_ERROR; + } + } + } + } + + *stream = stm; + + stm->ports_ready = true; + stm->pause = true; + pthread_mutex_unlock(&stm->mutex); + + return CUBEB_OK; +} + +static void +cbjack_stream_destroy(cubeb_stream * stream) +{ + pthread_mutex_lock(&stream->mutex); + stream->ports_ready = false; + + if (stream->devs == DUPLEX || stream->devs == OUT_ONLY) { + for (unsigned int c = 0; c < stream->out_params.channels; c++) { + if (stream->output_ports[c]) { + WRAP(jack_port_unregister) + (stream->context->jack_client, stream->output_ports[c]); + stream->output_ports[c] = NULL; + } + } + } + + if (stream->devs == DUPLEX || stream->devs == IN_ONLY) { + for (unsigned int c = 0; c < stream->in_params.channels; c++) { + if (stream->input_ports[c]) { + WRAP(jack_port_unregister) + (stream->context->jack_client, stream->input_ports[c]); + stream->input_ports[c] = NULL; + } + } + } + + if (stream->resampler) { + cubeb_resampler_destroy(stream->resampler); + stream->resampler = NULL; + } + stream->in_use = false; + pthread_mutex_unlock(&stream->mutex); +} + +static int +cbjack_stream_start(cubeb_stream * stream) +{ + stream->pause = false; + stream->state_callback(stream, stream->user_ptr, CUBEB_STATE_STARTED); + return CUBEB_OK; +} + +static int +cbjack_stream_stop(cubeb_stream * stream) +{ + stream->pause = true; + stream->state_callback(stream, stream->user_ptr, CUBEB_STATE_STOPPED); + return CUBEB_OK; +} + +static int +cbjack_stream_get_position(cubeb_stream * stream, uint64_t * position) +{ + *position = stream->position; + return CUBEB_OK; +} + +static int +cbjack_stream_set_volume(cubeb_stream * stm, float volume) +{ + stm->volume = volume; + return CUBEB_OK; +} + +static int +cbjack_stream_get_current_device(cubeb_stream * stm, + cubeb_device ** const device) +{ + *device = (cubeb_device *)calloc(1, sizeof(cubeb_device)); + if (*device == NULL) + return CUBEB_ERROR; + + const char * j_in = JACK_DEFAULT_IN; + const char * j_out = JACK_DEFAULT_OUT; + const char * empty = ""; + + if (stm->devs == DUPLEX) { + (*device)->input_name = strdup(j_in); + (*device)->output_name = strdup(j_out); + } else if (stm->devs == IN_ONLY) { + (*device)->input_name = strdup(j_in); + (*device)->output_name = strdup(empty); + } else if (stm->devs == OUT_ONLY) { + (*device)->input_name = strdup(empty); + (*device)->output_name = strdup(j_out); + } + + return CUBEB_OK; +} + +static int +cbjack_stream_device_destroy(cubeb_stream * /*stream*/, cubeb_device * device) +{ + if (device->input_name) + free(device->input_name); + if (device->output_name) + free(device->output_name); + free(device); + return CUBEB_OK; +} + +static int +cbjack_enumerate_devices(cubeb * context, cubeb_device_type type, + cubeb_device_collection * collection) +{ + if (!context) + return CUBEB_ERROR; + + uint32_t rate; + cbjack_get_preferred_sample_rate(context, &rate); + + 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) { + 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) { + 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->device = devices; + + return CUBEB_OK; +} + +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..73b568f5cd --- /dev/null +++ b/media/libcubeb/src/cubeb_log.cpp @@ -0,0 +1,233 @@ +/* + * 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 "cubeb_tracing.h" +#include <cstdarg> +#ifdef _WIN32 +#include <windows.h> +#else +#include <time.h> +#endif + +std::atomic<cubeb_log_level> g_cubeb_log_level; +std::atomic<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. */ +const size_t CUBEB_LOG_BATCH_PRINT_INTERVAL_MS = 10; + +void +cubeb_noop_log_callback(char const * /* fmt */, ...) +{ +} + +/** + * 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); + auto * owned_queue = msg_queue.load(); + // Check if the queue is being deallocated. If not, grab ownership. If yes, + // return, the message won't be logged. + if (!owned_queue || + !msg_queue.compare_exchange_strong(owned_queue, nullptr)) { + return; + } + owned_queue->enqueue(msg); + // Return ownership. + msg_queue.store(owned_queue); + } + void run() + { + assert(logging_thread.get_id() == std::thread::id()); + logging_thread = std::thread([this]() { + CUBEB_REGISTER_THREAD("cubeb_log"); + while (!shutdown_thread) { + cubeb_log_message msg; + while (msg_queue_consumer.load()->dequeue(&msg, 1)) { + cubeb_log_internal_no_format(msg.get()); + } + std::this_thread::sleep_for( + std::chrono::milliseconds(CUBEB_LOG_BATCH_PRINT_INTERVAL_MS)); + } + CUBEB_UNREGISTER_THREAD(); + }); + } + // 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() + { + if (msg_queue) { + msg_queue.load()->reset_thread_ids(); + } + } + void start() + { + auto * queue = + new lock_free_queue<cubeb_log_message>(CUBEB_LOG_MESSAGE_QUEUE_DEPTH); + msg_queue.store(queue); + msg_queue_consumer.store(queue); + shutdown_thread = false; + run(); + } + void stop() + { + assert(((g_cubeb_log_callback == cubeb_noop_log_callback) || + !g_cubeb_log_callback) && + "Only call stop after logging has been disabled."); + shutdown_thread = true; + if (logging_thread.get_id() != std::thread::id()) { + logging_thread.join(); + logging_thread = std::thread(); + auto * owned_queue = msg_queue.load(); + // Check if the queue is being used. If not, grab ownership. If yes, + // try again shortly. At this point, the logging thread has been joined, + // so nothing is going to dequeue. + // If there is a valid pointer here, then the real-time audio thread that + // logs won't attempt to write into the queue, and instead drop the + // message. + while (!msg_queue.compare_exchange_weak(owned_queue, nullptr)) { + } + delete owned_queue; + msg_queue_consumer.store(nullptr); + } + } + +private: + cubeb_async_logger() {} + ~cubeb_async_logger() + { + assert(logging_thread.get_id() == std::thread::id() && + (g_cubeb_log_callback == cubeb_noop_log_callback || + !g_cubeb_log_callback)); + if (msg_queue.load()) { + delete msg_queue.load(); + } + } + /** This is quite a big data structure, but is only instantiated if the + * asynchronous logger is used. The two pointers point to the same object, but + * the first one can be temporarily null when a message is being enqueued. */ + std::atomic<lock_free_queue<cubeb_log_message> *> msg_queue = {nullptr}; + + std::atomic<lock_free_queue<cubeb_log_message> *> msg_queue_consumer = { + nullptr}; + std::atomic<bool> shutdown_thread = {false}; + std::thread logging_thread; +}; + +void +cubeb_log_internal(char const * file, uint32_t line, char const * fmt, ...) +{ + va_list args; + va_start(args, fmt); + char msg[CUBEB_LOG_MESSAGE_MAX_SIZE]; + vsnprintf(msg, CUBEB_LOG_MESSAGE_MAX_SIZE, fmt, args); + va_end(args); + g_cubeb_log_callback.load()("%s:%d:%s", file, line, msg); +} + +void +cubeb_log_internal_no_format(const char * msg) +{ + g_cubeb_log_callback.load()(msg); +} + +void +cubeb_async_log(char const * fmt, ...) +{ + // 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(void) +{ + if (!g_cubeb_log_callback) { + return; + } + cubeb_async_logger::get().reset_producer_thread(); +} + +void +cubeb_log_set(cubeb_log_level log_level, cubeb_log_callback log_callback) +{ + g_cubeb_log_level = log_level; + // Once a callback has a been set, `g_cubeb_log_callback` is never set back to + // nullptr, to prevent a TOCTOU race between checking the pointer + if (log_callback && log_level != CUBEB_LOG_DISABLED) { + g_cubeb_log_callback = log_callback; + cubeb_async_logger::get().start(); + } else if (!log_callback || CUBEB_LOG_DISABLED) { + g_cubeb_log_callback = cubeb_noop_log_callback; + // This returns once the thread has joined. + cubeb_async_logger::get().stop(); + } else { + assert(false && "Incorrect parameters passed to cubeb_log_set"); + } +} + +cubeb_log_level +cubeb_log_get_level() +{ + return g_cubeb_log_level; +} + +cubeb_log_callback +cubeb_log_get_callback() +{ + if (g_cubeb_log_callback == cubeb_noop_log_callback) { + return nullptr; + } + return g_cubeb_log_callback; +} diff --git a/media/libcubeb/src/cubeb_log.h b/media/libcubeb/src/cubeb_log.h new file mode 100644 index 0000000000..fb3b719b17 --- /dev/null +++ b/media/libcubeb/src/cubeb_log.h @@ -0,0 +1,74 @@ +/* + * Copyright © 2016 Mozilla Foundation + * + * This program is made available under an ISC-style license. See the + * accompanying file LICENSE for details. + */ + +#ifndef CUBEB_LOG +#define CUBEB_LOG + +#include "cubeb/cubeb.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#if defined(__GNUC__) || defined(__clang__) +#define PRINTF_FORMAT(fmt, args) __attribute__((format(printf, fmt, args))) +#if defined(__FILE_NAME__) +#define __FILENAME__ __FILE_NAME__ +#else +#define __FILENAME__ \ + (__builtin_strrchr(__FILE__, '/') ? __builtin_strrchr(__FILE__, '/') + 1 \ + : __FILE__) +#endif +#else +#define PRINTF_FORMAT(fmt, args) +#include <string.h> +#define __FILENAME__ \ + (strrchr(__FILE__, '/') ? strrchr(__FILE__, '/') + 1 : __FILE__) +#endif + +void +cubeb_log_set(cubeb_log_level log_level, cubeb_log_callback log_callback); +cubeb_log_level +cubeb_log_get_level(void); +cubeb_log_callback +cubeb_log_get_callback(void); +void +cubeb_log_internal_no_format(const char * msg); +void +cubeb_log_internal(const char * filename, uint32_t line, const char * fmt, ...); +void +cubeb_async_log(const char * fmt, ...); +void +cubeb_async_log_reset_threads(void); + +#ifdef __cplusplus +} +#endif + +#define LOGV(msg, ...) LOG_INTERNAL(CUBEB_LOG_VERBOSE, msg, ##__VA_ARGS__) +#define LOG(msg, ...) LOG_INTERNAL(CUBEB_LOG_NORMAL, msg, ##__VA_ARGS__) + +#define LOG_INTERNAL(level, fmt, ...) \ + do { \ + if (cubeb_log_get_level() >= level && cubeb_log_get_callback()) { \ + cubeb_log_internal(__FILENAME__, __LINE__, fmt, ##__VA_ARGS__); \ + } \ + } while (0) + +#define ALOG_INTERNAL(level, fmt, ...) \ + do { \ + if (cubeb_log_get_level() >= level && cubeb_log_get_callback()) { \ + cubeb_async_log(fmt, ##__VA_ARGS__); \ + } \ + } while (0) + +/* Asynchronous logging macros to log in real-time callbacks. */ +/* Should not be used on android due to the use of global/static variables. */ +#define ALOGV(msg, ...) ALOG_INTERNAL(CUBEB_LOG_VERBOSE, msg, ##__VA_ARGS__) +#define ALOG(msg, ...) ALOG_INTERNAL(CUBEB_LOG_NORMAL, msg, ##__VA_ARGS__) + +#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..7f87571f56 --- /dev/null +++ b/media/libcubeb/src/cubeb_mixer.cpp @@ -0,0 +1,621 @@ +/* + * 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 "cubeb_mixer.h" +#include "cubeb-internal.h" +#include "cubeb_utils.h" +#include <algorithm> +#include <cassert> +#include <climits> +#include <cmath> +#include <cstdlib> +#include <memory> +#include <type_traits> + +#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; + double 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, decltype(operand(coeff1))>::value, + "function must return the same type as used by coeff1 and coeff2"); + 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, decltype(operand(coeff))>::value, + "function must return the same type as used by 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, decltype(aF(matrix_coeff[0][0]))>::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..1859dab467 --- /dev/null +++ b/media/libcubeb/src/cubeb_mixer.h @@ -0,0 +1,36 @@ +/* + * 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.cpp b/media/libcubeb/src/cubeb_opensl.cpp new file mode 100644 index 0000000000..f9914ea04c --- /dev/null +++ b/media/libcubeb/src/cubeb_opensl.cpp @@ -0,0 +1,1955 @@ +/* + * Copyright © 2012 Mozilla Foundation + * + * This program is made available under an ISC-style license. See the + * accompanying file LICENSE for details. + */ +#undef NDEBUG +#include <SLES/OpenSLES.h> +#include <assert.h> +#include <dlfcn.h> +#include <errno.h> +#include <math.h> +#include <pthread.h> +#include <stdlib.h> +#include <time.h> +#include <vector> +#if defined(__ANDROID__) +#include "android/sles_definitions.h" +#include <SLES/OpenSLES_Android.h> +#include <android/api-level.h> +#include <android/log.h> +#include <dlfcn.h> +#include <sys/system_properties.h> +#endif +#include "android/cubeb-output-latency.h" +#include "cubeb-internal.h" +#include "cubeb/cubeb.h" +#include "cubeb_android.h" +#include "cubeb_array_queue.h" +#include "cubeb_resampler.h" + +#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 + +#define DEFAULT_SAMPLE_RATE 48000 +#define DEFAULT_NUM_OF_FRAMES 480 + +extern cubeb_ops const opensl_ops; + +struct cubeb { + struct cubeb_ops const * ops; + void * lib; + 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 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; + void ** queuebuf; + uint32_t queuebuf_capacity; + uint32_t queuebuf_idx; + long queuebuf_len; + long bytespersec; + uint32_t framesize; + /* Total number of played frames. + * Synchronized by stream::mutex lock. */ + long written; + /* Flag indicating draining. Synchronized + * by stream::mutex lock. */ + int draining; + /* 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; + + cubeb_resampler * resampler; + 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_input; + int voice_output; + std::unique_ptr<cubeb_stream_params> input_params; + std::unique_ptr<cubeb_stream_params> output_params; + // A non-empty buffer means that f32 -> int16 conversion need to happen + std::vector<float> conversion_buffer_output; + std::vector<float> conversion_buffer_input; +}; + +/* 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 = static_cast<cubeb_stream *>(user_ptr); + assert(stm); + switch (event) { + case SL_PLAYEVENT_HEADATMARKER: + opensl_notify_drained(stm); + break; + default: + break; + } +} + +static void +recorder_marker_callback(SLRecordItf caller, void * pContext, SLuint32 event) +{ + cubeb_stream * stm = static_cast<cubeb_stream *>(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); + } + } + } +} + +// Returns a buffer suitable to write output data to. +void * +get_output_buffer(cubeb_stream * stm, void * output_buffer, + uint32_t sample_count) +{ + if (stm->conversion_buffer_output.empty()) { + return output_buffer; + } + if (stm->conversion_buffer_output.size() < sample_count) { + stm->conversion_buffer_output.resize(sample_count); + } + return stm->conversion_buffer_output.data(); +} + +void * +release_output_buffer(cubeb_stream * stm, void * original_output_buffer, + uint32_t sample_count) +{ + if (stm->conversion_buffer_output.empty()) { + return original_output_buffer; + } + int16_t * int16_buf = reinterpret_cast<int16_t *>(original_output_buffer); + for (uint32_t i = 0; i < sample_count; i++) { + float v = stm->conversion_buffer_output[i] * 32768.0f; + float clamped = std::max(-32768.0f, std::min(32767.0f, v)); + int16_buf[i] = static_cast<int16_t>(clamped); + } + return original_output_buffer; +} + +static void +bufferqueue_callback(SLBufferQueueItf caller, void * user_ptr) +{ + cubeb_stream * stm = static_cast<cubeb_stream *>(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) { + return; + } + + void * buf = stm->queuebuf[stm->queuebuf_idx]; + void * buf_original_ptr = buf; + uint32_t sample_count = + stm->output_params->channels * stm->queuebuf_len / stm->framesize; + 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) { + + buf = get_output_buffer(stm, buf, sample_count); + + written = cubeb_resampler_fill(stm->resampler, nullptr, nullptr, buf, + stm->queuebuf_len / stm->framesize); + + buf = release_output_buffer(stm, buf_original_ptr, sample_count); + + ALOGV("bufferqueue_callback: resampler fill returned %ld frames", written); + if (written < 0 || + written * stm->framesize > static_cast<uint32_t>(stm->queuebuf_len)) { + ALOGV("bufferqueue_callback: error, shutting down", written); + 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. + assert(static_cast<uint32_t>(stm->queuebuf_len) >= written * stm->framesize); + memset(reinterpret_cast<uint8_t *>(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 (written > 0) { + pthread_mutex_lock(&stm->mutex); + stm->written += written; + pthread_mutex_unlock(&stm->mutex); + } + + if (!draining && + written * stm->framesize < static_cast<uint32_t>(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; + } +} + +static int +opensl_enqueue_recorder(cubeb_stream * stm, void ** last_filled_buffer) +{ + assert(stm); + + int current_index = stm->input_buffer_index; + void * last_buffer = nullptr; + + 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 = + static_cast<int>((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; +} + +// If necessary, convert and returns an input buffer. +// Otherwise, just returns the pointer that has been passed in. +void * +convert_input_buffer_if_needed(cubeb_stream * stm, void * input_buffer, + uint32_t sample_count) +{ + // Perform conversion if needed + if (stm->conversion_buffer_input.empty()) { + return input_buffer; + } + if (stm->conversion_buffer_input.size() < sample_count) { + stm->conversion_buffer_input.resize(sample_count); + } + int16_t * int16_buf = reinterpret_cast<int16_t *>(input_buffer); + for (uint32_t i = 0; i < sample_count; i++) { + stm->conversion_buffer_input[i] = + static_cast<float>(int16_buf[i]) / 32768.f; + } + return stm->conversion_buffer_input.data(); +} + +// input data callback +void +recorder_callback(SLAndroidSimpleBufferQueueItf bq, void * context) +{ + assert(context); + cubeb_stream * stm = static_cast<cubeb_stream *>(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, nullptr); + assert(r == CUBEB_OK); + return; + } + + // Enqueue next available buffer and get the last filled buffer. + void * input_buffer = nullptr; + r = opensl_enqueue_recorder(stm, &input_buffer); + assert(r == CUBEB_OK); + assert(input_buffer); + + long input_frame_count = stm->input_buffer_length / stm->input_frame_size; + uint32_t sample_count = input_frame_count * stm->input_params->channels; + + input_buffer = + convert_input_buffer_if_needed(stm, input_buffer, sample_count); + + // Fill resampler with last input + long got = cubeb_resampler_fill(stm->resampler, input_buffer, + &input_frame_count, nullptr, 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 = static_cast<cubeb_stream *>(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, nullptr); + assert(r == CUBEB_OK); + return; + } + + // Enqueue next available buffer and get the last filled buffer. + void * input_buffer = nullptr; + 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) +{ + cubeb_stream * stm = static_cast<cubeb_stream *>(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 = nullptr; + r = pthread_mutex_lock(&stm->mutex); + assert(r == 0); + output_buffer = stm->queuebuf[stm->queuebuf_idx]; + void * output_buffer_original_ptr = output_buffer; + // 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 sample_count = input_frame_count * stm->input_params->channels; + long frames_needed = stm->queuebuf_len / stm->framesize; + uint32_t output_sample_count = + stm->output_params->channels * stm->queuebuf_len / stm->framesize; + + if (!input_buffer) { + LOG("Input hole set silent input buffer"); + input_buffer = stm->input_silent_buffer; + } + + input_buffer = + convert_input_buffer_if_needed(stm, input_buffer, sample_count); + + output_buffer = get_output_buffer(stm, output_buffer, output_sample_count); + + long written = 0; + // Trigger user callback through resampler + written = + cubeb_resampler_fill(stm->resampler, input_buffer, &input_frame_count, + output_buffer, frames_needed); + + output_buffer = + release_output_buffer(stm, output_buffer_original_ptr, sample_count); + + 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); +} + +static void +opensl_destroy(cubeb * ctx); + +#if defined(__ANDROID__) +#if (__ANDROID_API__ >= ANDROID_VERSION_LOLLIPOP) +using system_property_get = int(const char *, char *); + +static int +wrap_system_property_get(const char * name, char * value) +{ + void * libc = dlopen("libc.so", RTLD_LAZY); + if (!libc) { + LOG("Failed to open libc.so"); + return -1; + } + system_property_get * func = + (system_property_get *)dlsym(libc, "__system_property_get"); + int ret = -1; + if (func) { + ret = func(name, value); + } + dlclose(libc); + return ret; +} +#endif + +static int +get_android_version(void) +{ + char version_string[PROP_VALUE_MAX]; + + 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, nullptr, 10); + LOG("Android version %d", version); + return version; +} +#endif + +extern "C" { +int +opensl_init(cubeb ** context, char const * context_name) +{ + cubeb * ctx; + +#if defined(__ANDROID__) + int android_version = get_android_version(); + if (android_version > 0 && + android_version <= ANDROID_VERSION_GINGERBREAD_MR1) { + // Don't even attempt to run on Gingerbread and lower + LOG("Error: Android version too old, exiting."); + return CUBEB_ERROR; + } +#endif + + *context = nullptr; + + ctx = static_cast<cubeb *>(calloc(1, sizeof(*ctx))); + assert(ctx); + + ctx->ops = &opensl_ops; + + ctx->lib = dlopen("libOpenSLES.so", RTLD_LAZY); + if (!ctx->lib) { + LOG("Error: Couldn't find libOpenSLES.so, exiting"); + free(ctx); + return CUBEB_ERROR; + } + + typedef SLresult (*slCreateEngine_t)( + SLObjectItf *, SLuint32, const SLEngineOption *, SLuint32, + const SLInterfaceID *, const SLboolean *); + slCreateEngine_t f_slCreateEngine = + (slCreateEngine_t)dlsym(ctx->lib, "slCreateEngine"); + SLInterfaceID SL_IID_ENGINE = + *(SLInterfaceID *)dlsym(ctx->lib, "SL_IID_ENGINE"); + SLInterfaceID SL_IID_OUTPUTMIX = + *(SLInterfaceID *)dlsym(ctx->lib, "SL_IID_OUTPUTMIX"); + ctx->SL_IID_VOLUME = *(SLInterfaceID *)dlsym(ctx->lib, "SL_IID_VOLUME"); + 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_RECORD) { + LOG("Error: didn't find required symbols, exiting."); + opensl_destroy(ctx); + return CUBEB_ERROR; + } + + const SLEngineOption opt[] = {{SL_ENGINEOPTION_THREADSAFE, SL_BOOLEAN_TRUE}}; + + SLresult res; + res = f_slCreateEngine(&ctx->engObj, 1, opt, 0, nullptr, nullptr); + + if (res != SL_RESULT_SUCCESS) { + LOG("Error: slCreateEngine failure, exiting."); + opensl_destroy(ctx); + return CUBEB_ERROR; + } + + res = (*ctx->engObj)->Realize(ctx->engObj, SL_BOOLEAN_FALSE); + if (res != SL_RESULT_SUCCESS) { + LOG("Error: engine realization failure, exiting."); + opensl_destroy(ctx); + return CUBEB_ERROR; + } + + res = (*ctx->engObj)->GetInterface(ctx->engObj, SL_IID_ENGINE, &ctx->eng); + if (res != SL_RESULT_SUCCESS) { + LOG("Error: GetInterface(..., SL_IID_ENGINE, ...), exiting."); + opensl_destroy(ctx); + return CUBEB_ERROR; + } + + const SLInterfaceID idsom[] = {SL_IID_OUTPUTMIX}; + const SLboolean reqom[] = {SL_BOOLEAN_TRUE}; + res = + (*ctx->eng)->CreateOutputMix(ctx->eng, &ctx->outmixObj, 1, idsom, reqom); + if (res != SL_RESULT_SUCCESS) { + LOG("Error: CreateOutputMix failure, exiting."); + opensl_destroy(ctx); + return CUBEB_ERROR; + } + + res = (*ctx->outmixObj)->Realize(ctx->outmixObj, SL_BOOLEAN_FALSE); + if (res != SL_RESULT_SUCCESS) { + LOG("Error: Output mix object failure, exiting."); + opensl_destroy(ctx); + return CUBEB_ERROR; + } + + ctx->p_output_latency_function = + cubeb_output_latency_load_method(android_version); + if (!cubeb_output_latency_method_is_loaded(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; +} +} + +static char const * +opensl_get_backend_id(cubeb * ctx) +{ + return "opensl"; +} + +static int +opensl_get_max_channel_count(cubeb * ctx, uint32_t * max_channels) +{ + assert(ctx && max_channels); + /* The android mixer handles up to two channels, see + http://androidxref.com/4.2.2_r1/xref/frameworks/av/services/audioflinger/AudioFlinger.h#67 + */ + *max_channels = 2; + + return CUBEB_OK; +} + +static void +opensl_destroy(cubeb * ctx) +{ + if (ctx->outmixObj) { + (*ctx->outmixObj)->Destroy(ctx->outmixObj); + } + if (ctx->engObj) { + (*ctx->engObj)->Destroy(ctx->engObj); + } + dlclose(ctx->lib); + if (ctx->p_output_latency_function) { + cubeb_output_latency_unload_method(ctx->p_output_latency_function); + } + free(ctx); +} + +static void +opensl_stream_destroy(cubeb_stream * stm); + +#if defined(__ANDROID__) && (__ANDROID_API__ >= ANDROID_VERSION_LOLLIPOP) +static int +opensl_set_format_ext(SLAndroidDataFormat_PCM_EX * format, + cubeb_stream_params * params) +{ + assert(format); + assert(params); + + format->formatType = SL_ANDROID_DATAFORMAT_PCM_EX; + format->numChannels = params->channels; + // sampleRate is in milliHertz + format->sampleRate = params->rate * 1000; + 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->bitsPerSample = SL_PCMSAMPLEFORMAT_FIXED_16; + format->containerSize = SL_PCMSAMPLEFORMAT_FIXED_16; + format->representation = SL_ANDROID_PCM_REPRESENTATION_SIGNED_INT; + format->endianness = SL_BYTEORDER_LITTLEENDIAN; + break; + case CUBEB_SAMPLE_S16BE: + format->bitsPerSample = SL_PCMSAMPLEFORMAT_FIXED_16; + format->containerSize = SL_PCMSAMPLEFORMAT_FIXED_16; + format->representation = SL_ANDROID_PCM_REPRESENTATION_SIGNED_INT; + format->endianness = SL_BYTEORDER_BIGENDIAN; + break; + case CUBEB_SAMPLE_FLOAT32LE: + format->bitsPerSample = SL_PCMSAMPLEFORMAT_FIXED_32; + format->containerSize = SL_PCMSAMPLEFORMAT_FIXED_32; + format->representation = SL_ANDROID_PCM_REPRESENTATION_FLOAT; + format->endianness = SL_BYTEORDER_LITTLEENDIAN; + break; + case CUBEB_SAMPLE_FLOAT32BE: + format->bitsPerSample = SL_PCMSAMPLEFORMAT_FIXED_32; + format->containerSize = SL_PCMSAMPLEFORMAT_FIXED_32; + format->representation = SL_ANDROID_PCM_REPRESENTATION_FLOAT; + format->endianness = SL_BYTEORDER_BIGENDIAN; + break; + default: + return CUBEB_ERROR_INVALID_FORMAT; + } + return CUBEB_OK; +} +#endif + +static int +opensl_set_format(SLDataFormat_PCM * format, cubeb_stream_params * params) +{ + assert(format); + assert(params); + + // If this function is called, this backend has been compiled with an older + // version of Android, that doesn't support floating point audio IO. + // The stream is configured with int16 of the proper endianess, and conversion + // will happen during playback. + + 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: + case CUBEB_SAMPLE_FLOAT32LE: + format->endianness = SL_BYTEORDER_LITTLEENDIAN; + break; + case CUBEB_SAMPLE_S16BE: + case CUBEB_SAMPLE_FLOAT32BE: + format->endianness = SL_BYTEORDER_BIGENDIAN; + break; + default: + assert(false && "unhandled value"); + } + return CUBEB_OK; +} + +template <typename Function> +int +initialize_with_format(cubeb_stream * stm, cubeb_stream_params * params, + Function func) +{ + void * format = nullptr; + bool using_floats = false; + uint32_t * format_sample_rate; +#if defined(__ANDROID__) && (__ANDROID_API__ >= ANDROID_VERSION_LOLLIPOP) + SLAndroidDataFormat_PCM_EX pcm_ext_format; + if (get_android_version() >= ANDROID_VERSION_LOLLIPOP) { + if (opensl_set_format_ext(&pcm_ext_format, params) != CUBEB_OK) { + LOG("opensl_set_format_ext: error, exiting"); + return CUBEB_ERROR_INVALID_FORMAT; + } + format = &pcm_ext_format; + format_sample_rate = &pcm_ext_format.sampleRate; + using_floats = + pcm_ext_format.representation == SL_ANDROID_PCM_REPRESENTATION_FLOAT; + } +#endif + + SLDataFormat_PCM pcm_format; + if (!format) { + if (opensl_set_format(&pcm_format, params) != CUBEB_OK) { + LOG("opensl_set_format: error, exiting"); + return CUBEB_ERROR_INVALID_FORMAT; + } + format = &pcm_format; + format_sample_rate = &pcm_format.samplesPerSec; + } + + return func(format, format_sample_rate, using_floats); +} + +static int +opensl_configure_capture(cubeb_stream * stm, cubeb_stream_params * params) +{ + assert(stm); + assert(params); + + /* For now set device rate to params rate. */ + stm->input_device_rate = params->rate; + + int rv = initialize_with_format( + stm, params, + [=](void * format, uint32_t * format_sample_rate, + bool using_floats) -> int { + SLDataLocator_AndroidSimpleBufferQueue lDataLocatorOut; + lDataLocatorOut.locatorType = SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE; + lDataLocatorOut.numBuffers = NBUFS; + + SLDataSink dataSink; + dataSink.pLocator = &lDataLocatorOut; + dataSink.pFormat = format; + + SLDataLocator_IODevice dataLocatorIn; + dataLocatorIn.locatorType = SL_DATALOCATOR_IODEVICE; + dataLocatorIn.deviceType = SL_IODEVICE_AUDIOINPUT; + dataLocatorIn.deviceID = SL_DEFAULTDEVICEID_AUDIOINPUT; + dataLocatorIn.device = nullptr; + + SLDataSource dataSource; + dataSource.pLocator = &dataLocatorIn; + dataSource.pFormat = nullptr; + + 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, + &dataSource, &dataSink, + 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; + } + *format_sample_rate = stm->input_device_rate * 1000; + res = (*stm->context->eng) + ->CreateAudioRecorder( + stm->context->eng, &stm->recorderObj, &dataSource, + &dataSink, NELEMS(lSoundRecorderIIDs), + lSoundRecorderIIDs, lSoundRecorderReqs); + } + if (res != SL_RESULT_SUCCESS) { + LOG("Failed to create recorder, not trying other input" + " rate. Error code: %lu", + res); + return CUBEB_ERROR; + } + // It's always possible to use int16 regardless of the Android version. + // However if compiling for older Android version, it's possible to + // request f32 audio, but Android only supports int16, in which case a + // conversion need to happen. + if ((params->format == CUBEB_SAMPLE_FLOAT32NE || + params->format == CUBEB_SAMPLE_FLOAT32BE) && + !using_floats) { + // setup conversion from f32 to int16 + LOG("Input stream configured for using float, but not supported: a " + "conversion will be performed"); + stm->conversion_buffer_input.resize(1); + } + return CUBEB_OK; + }); + + if (rv != CUBEB_OK) { + LOG("Could not initialize recorder."); + return rv; + } + + SLresult res; + 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; + } + + // 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_input + ? SL_ANDROID_RECORDING_PRESET_VOICE_RECOGNITION + : SL_ANDROID_RECORDING_PRESET_CAMCORDER; + + res = + (*recorderConfig) + ->SetConfiguration(recorderConfig, SL_ANDROID_KEY_RECORDING_PRESET, + &streamType, sizeof(SLint32)); + + 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; + } + + 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; + } + + (*stm->recorderItf)->SetMarkerPosition(stm->recorderItf, (SLmillisecond)0); + + 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; + } + + // 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; + } + + // Calculate length of input buffer according to requested latency + uint32_t sample_size = 0; + if (params->format == CUBEB_SAMPLE_FLOAT32BE || + params->format == CUBEB_SAMPLE_FLOAT32NE) { + sample_size = sizeof(float); + } else { + sample_size = sizeof(int16_t); + } + stm->input_frame_size = params->channels * sample_size; + stm->input_buffer_length = (stm->input_frame_size * stm->buffer_size_frames); + + // 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); + } + + // 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); + } + + // Enqueue buffer to start rolling once recorder started + rv = opensl_enqueue_recorder(stm, nullptr); + if (rv != CUBEB_OK) { + return rv; + } + + LOG("Cubeb stream init recorder success"); + + 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->lastPosition = -1; + stm->lastPositionTimeStamp = 0; + stm->lastCompensativePosition = -1; + + int rv = initialize_with_format( + stm, params, + [=](void * format, uint32_t * format_sample_rate, bool using_floats) { + SLDataLocator_BufferQueue loc_bufq; + loc_bufq.locatorType = SL_DATALOCATOR_BUFFERQUEUE; + loc_bufq.numBuffers = NBUFS; + SLDataSource source; + source.pLocator = &loc_bufq; + source.pFormat = format; + SLDataLocator_OutputMix loc_outmix; + loc_outmix.locatorType = SL_DATALOCATOR_OUTPUTMIX; + loc_outmix.outputMix = stm->context->outmixObj; + + SLDataSink sink; + sink.pLocator = &loc_outmix; + sink.pFormat = nullptr; + +#if defined(__ANDROID__) + 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}; + const SLboolean req[] = {SL_BOOLEAN_TRUE, SL_BOOLEAN_TRUE}; +#endif + assert(NELEMS(ids) == NELEMS(req)); + + uint32_t preferred_sampling_rate = stm->user_output_rate; + SLresult res = SL_RESULT_CONTENT_UNSUPPORTED; + if (preferred_sampling_rate) { + 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 && + preferred_sampling_rate != DEFAULT_SAMPLE_RATE) { + preferred_sampling_rate = DEFAULT_SAMPLE_RATE; + *format_sample_rate = preferred_sampling_rate * 1000; + res = (*stm->context->eng) + ->CreateAudioPlayer(stm->context->eng, &stm->playerObj, + &source, &sink, NELEMS(ids), ids, req); + } + + if (res != SL_RESULT_SUCCESS) { + LOG("Failed to create audio player. Error code: %lu", res); + return CUBEB_ERROR; + } + stm->output_configured_rate = preferred_sampling_rate; + + // It's always possible to use int16 regardless of the Android version. + // However if compiling for older Android version, it's possible to + // request f32 audio, but Android only supports int16, in which case a + // conversion need to happen. + if ((params->format == CUBEB_SAMPLE_FLOAT32NE || + params->format == CUBEB_SAMPLE_FLOAT32BE) && + !using_floats) { + // setup conversion from f32 to int16 + LOG("Input stream configured for using float, but not supported: a " + "conversion will be performed"); + stm->conversion_buffer_output.resize(1); + } + + if (!using_floats) { + stm->framesize = params->channels * sizeof(int16_t); + } else { + stm->framesize = params->channels * sizeof(float); + } + return CUBEB_OK; + }); + + if (rv != CUBEB_OK) { + LOG("Couldn't set format on sink or source"); + return rv; + } + + 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; + // Allocate input arrays + 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]); + } + + SLAndroidConfigurationItf playerConfig = nullptr; + + SLresult res; + 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; + } + + SLint32 streamType = SL_ANDROID_STREAM_MEDIA; + if (stm->voice_output) { + 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); + } + + SLuint32 performanceMode = SL_ANDROID_PERFORMANCE_LATENCY; + if (stm->buffer_size_frames > POWERSAVE_LATENCY_FRAMES_THRESHOLD) { + LOG("Audio stream configured for power saving"); + performanceMode = SL_ANDROID_PERFORMANCE_POWER_SAVING; + } + + res = (*playerConfig) + ->SetConfiguration(playerConfig, SL_ANDROID_KEY_PERFORMANCE_MODE, + &performanceMode, sizeof(performanceMode)); + if (res != SL_RESULT_SUCCESS) { + LOG("Failed to set Android performance mode to %d Error code: %lu. This " + "is not fatal.", + performanceMode, res); + } + } + + res = (*stm->playerObj)->Realize(stm->playerObj, SL_BOOLEAN_FALSE); + if (res != SL_RESULT_SUCCESS) { + LOG("Failed to realize player object. Error code: %lu", res); + return CUBEB_ERROR; + } + + // 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) { + LOG("Failed to get play interface. Error code: %lu", res); + return CUBEB_ERROR; + } + + res = (*stm->playerObj) + ->GetInterface(stm->playerObj, stm->context->SL_IID_BUFFERQUEUE, + &stm->bufq); + if (res != SL_RESULT_SUCCESS) { + LOG("Failed to get bufferqueue interface. Error code: %lu", res); + return CUBEB_ERROR; + } + + res = (*stm->playerObj) + ->GetInterface(stm->playerObj, stm->context->SL_IID_VOLUME, + &stm->volume); + if (res != SL_RESULT_SUCCESS) { + 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) { + LOG("Failed to register play callback. Error code: %lu", res); + return CUBEB_ERROR; + } + + // Work around wilhelm/AudioTrack badness, bug 1221228 + (*stm->play)->SetMarkerPosition(stm->play, (SLmillisecond)0); + + res = (*stm->play) + ->SetCallbackEventsMask(stm->play, + (SLuint32)SL_PLAYEVENT_HEADATMARKER); + if (res != SL_RESULT_SUCCESS) { + LOG("Failed to set headatmarker event mask. Error code: %lu", res); + return CUBEB_ERROR; + } + + 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) { + LOG("Failed to register bufferqueue callback. Error code: %lu", res); + return CUBEB_ERROR; + } + + { + // Enqueue a silent frame so once the player becomes playing, the frame + // will be consumed and kick off the buffer queue callback. + // Note the duration of a single frame is less than 1ms. We don't bother + // adjusting the playback position. + uint8_t * buf = + reinterpret_cast<uint8_t *>(stm->queuebuf[stm->queuebuf_idx++]); + memset(buf, 0, stm->framesize); + res = (*stm->bufq)->Enqueue(stm->bufq, buf, stm->framesize); + assert(res == SL_RESULT_SUCCESS); + } + + LOG("Cubeb stream init playback success"); + return CUBEB_OK; +} + +static int +opensl_validate_stream_param(cubeb_stream_params * stream_params) +{ + if ((stream_params && + (stream_params->channels < 1 || stream_params->channels > 32))) { + return CUBEB_ERROR_INVALID_FORMAT; + } + if ((stream_params && (stream_params->prefs & CUBEB_STREAM_PREF_LOOPBACK))) { + LOG("Loopback is not supported"); + return CUBEB_ERROR_NOT_SUPPORTED; + } + return CUBEB_OK; +} + +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); +} + +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 = nullptr; + cubeb_async_log_reset_threads(); + + assert(ctx); + if (input_device || output_device) { + LOG("Device selection is not supported in Android. The default will be " + "used"); + } + + *stream = nullptr; + + 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 = reinterpret_cast<cubeb_stream *>(calloc(1, sizeof(*stm))); + assert(stm); + + if (input_stream_params) { + stm->input_params = + std::make_unique<cubeb_stream_params>(*input_stream_params); + } + if (output_stream_params) { + stm->output_params = + std::make_unique<cubeb_stream_params>(*output_stream_params); + } + + 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_input = + has_pref_set(input_stream_params, nullptr, CUBEB_STREAM_PREF_VOICE); + stm->voice_output = + has_pref_set(nullptr, output_stream_params, CUBEB_STREAM_PREF_VOICE); + + LOG("cubeb stream prefs: voice_input: %s voice_output: %s", + stm->voice_input ? "true" : "false", + stm->voice_output ? "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, nullptr); +#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) { + LOG("Error: playback-side configuration error, exiting."); + 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) { + LOG("Error: record-side configuration error, exiting."); + 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 : nullptr, + output_stream_params ? &output_params : nullptr, target_sample_rate, + data_callback, user_ptr, CUBEB_RESAMPLER_QUALITY_DEFAULT, + CUBEB_RESAMPLER_RECLOCK_NONE); + 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) +{ + 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_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) { + 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 = nullptr; + for (uint32_t i = 0; i < stm->input_array_capacity; ++i) { + free(stm->input_buffer_array[i]); + } + } + + (*stm->recorderObj)->Destroy(stm->recorderObj); + stm->recorderObj = nullptr; + stm->recorderItf = nullptr; + + 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 we're still draining at stream destroy time, pause the streams now so we + // can destroy them safely. + if (stm->draining) { + opensl_stream_stop(stm); + } + // Sleep for 10ms to give active streams time to pause so that no further + // buffer callbacks occur. Inspired by the same workaround (sleepBeforeClose) + // in liboboe. + usleep(10 * 1000); + + if (stm->playerObj) { + (*stm->playerObj)->Destroy(stm->playerObj); + stm->playerObj = nullptr; + stm->play = nullptr; + stm->bufq = nullptr; + 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; + uint32_t compensation_msec = 0; + SLresult res; + + res = (*stm->play)->GetPosition(stm->play, &msec); + if (res != SL_RESULT_SUCCESS) { + return CUBEB_ERROR; + } + + timespec t{}; + clock_gettime(CLOCK_MONOTONIC, &t); + if (stm->lastPosition == msec) { + compensation_msec = + (t.tv_sec * 1000000000LL + t.tv_nsec - stm->lastPositionTimeStamp) / + 1000000; + } else { + stm->lastPositionTimeStamp = t.tv_sec * 1000000000LL + t.tv_nsec; + stm->lastPosition = msec; + } + + 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->user_output_rate / + stm->output_configured_rate; + pthread_mutex_unlock(&stm->mutex); + assert(maximum_position >= 0); + + if (msec > output_latency) { + int64_t unadjusted_position; + if (stm->lastCompensativePosition > msec + compensation_msec) { + // Over compensation, use lastCompensativePosition. + unadjusted_position = + samplerate * (stm->lastCompensativePosition - output_latency) / 1000; + } else { + unadjusted_position = + samplerate * (msec - output_latency + compensation_msec) / 1000; + stm->lastCompensativePosition = msec + compensation_msec; + } + *position = unadjusted_position < maximum_position ? unadjusted_position + : maximum_position; + } else { + *position = 0; + } + return CUBEB_OK; +} + +static int +opensl_stream_get_latency(cubeb_stream * stm, uint32_t * latency) +{ + assert(stm); + assert(latency); + + uint32_t stream_latency_frames = + stm->user_output_rate * stm->output_latency_ms / 1000; + + *latency = static_cast<int>(stream_latency_frames + + cubeb_resampler_latency(stm->resampler)); + + return CUBEB_OK; +} + +int +opensl_stream_set_volume(cubeb_stream * stm, float volume) +{ + SLresult res; + SLmillibel max_level, millibels; + float unclamped_millibels; + + res = (*stm->volume)->GetMaxVolumeLevel(stm->volume, &max_level); + + if (res != SL_RESULT_SUCCESS) { + return CUBEB_ERROR; + } + + /* millibels are 100*dB, so the conversion from the volume's linear amplitude + * is 100 * 20 * log(volume). However we clamp the resulting value before + * passing it to lroundf() in order to prevent it from silently returning an + * erroneous value when the unclamped value exceeds the size of a long. */ + unclamped_millibels = 100.0f * 20.0f * log10f(fmaxf(volume, 0.0f)); + unclamped_millibels = fmaxf(unclamped_millibels, SL_MILLIBEL_MIN); + unclamped_millibels = fminf(unclamped_millibels, max_level); + + millibels = static_cast<SLmillibel>(lroundf(unclamped_millibels)); + + res = (*stm->volume)->SetVolumeLevel(stm->volume, millibels); + + if (res != SL_RESULT_SUCCESS) { + return CUBEB_ERROR; + } + return CUBEB_OK; +} + +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 = nullptr, + .get_preferred_sample_rate = nullptr, + .get_supported_input_processing_params = nullptr, + .enumerate_devices = nullptr, + .device_collection_destroy = nullptr, + .destroy = opensl_destroy, + .stream_init = opensl_stream_init, + .stream_destroy = opensl_stream_destroy, + .stream_start = opensl_stream_start, + .stream_stop = opensl_stream_stop, + .stream_get_position = opensl_stream_get_position, + .stream_get_latency = opensl_stream_get_latency, + .stream_get_input_latency = nullptr, + .stream_set_volume = opensl_stream_set_volume, + .stream_set_name = nullptr, + .stream_get_current_device = nullptr, + .stream_set_input_mute = nullptr, + .stream_set_input_processing_params = nullptr, + .stream_device_destroy = nullptr, + .stream_register_device_changed_callback = nullptr, + .register_device_collection_changed = nullptr}; diff --git a/media/libcubeb/src/cubeb_oss.c b/media/libcubeb/src/cubeb_oss.c new file mode 100644 index 0000000000..3d09ba4db2 --- /dev/null +++ b/media/libcubeb/src/cubeb_oss.c @@ -0,0 +1,1356 @@ +/* + * Copyright © 2019-2020 Nia Alarie <nia@NetBSD.org> + * Copyright © 2020 Ka Ho Ng <khng300@gmail.com> + * Copyright © 2020 The FreeBSD Foundation + * + * Portions of this software were developed by Ka Ho Ng + * under sponsorship from the FreeBSD Foundation. + * + * This program is made available under an ISC-style license. See the + * accompanying file LICENSE for details. + */ + +#include "cubeb-internal.h" +#include "cubeb/cubeb.h" +#include "cubeb_mixer.h" +#include "cubeb_strings.h" +#include "cubeb_tracing.h" +#include <assert.h> +#include <ctype.h> +#include <errno.h> +#include <fcntl.h> +#include <limits.h> +#include <poll.h> +#include <pthread.h> +#include <stdbool.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/ioctl.h> +#include <sys/soundcard.h> +#include <sys/types.h> +#include <unistd.h> + +/* Supported well by most hardware. */ +#ifndef OSS_PREFER_RATE +#define OSS_PREFER_RATE (48000) +#endif + +/* Standard acceptable minimum. */ +#ifndef OSS_LATENCY_MS +#define OSS_LATENCY_MS (8) +#endif + +#ifndef OSS_NFRAGS +#define OSS_NFRAGS (4) +#endif + +#ifndef OSS_DEFAULT_DEVICE +#define OSS_DEFAULT_DEVICE "/dev/dsp" +#endif + +#ifndef OSS_DEFAULT_MIXER +#define OSS_DEFAULT_MIXER "/dev/mixer" +#endif + +#define ENV_AUDIO_DEVICE "AUDIO_DEVICE" + +#ifndef OSS_MAX_CHANNELS +#if defined(__FreeBSD__) || defined(__DragonFly__) +/* + * The current maximum number of channels supported + * on FreeBSD is 8. + * + * Reference: FreeBSD 12.1-RELEASE + */ +#define OSS_MAX_CHANNELS (8) +#elif defined(__sun__) +/* + * The current maximum number of channels supported + * on Illumos is 16. + * + * Reference: PSARC 2008/318 + */ +#define OSS_MAX_CHANNELS (16) +#else +#define OSS_MAX_CHANNELS (2) +#endif +#endif + +#if defined(__FreeBSD__) || defined(__DragonFly__) +#define SNDSTAT_BEGIN_STR "Installed devices:" +#define SNDSTAT_USER_BEGIN_STR "Installed devices from userspace:" +#define SNDSTAT_FV_BEGIN_STR "File Versions:" +#endif + +static struct cubeb_ops const oss_ops; + +struct cubeb { + struct cubeb_ops const * ops; + + /* Our intern string store */ + pthread_mutex_t mutex; /* protects devid_strs */ + cubeb_strings * devid_strs; +}; + +struct oss_stream { + oss_devnode_t name; + int fd; + void * buf; + unsigned int bufframes; + unsigned int maxframes; + + struct stream_info { + int channels; + int sample_rate; + int fmt; + int precision; + } info; + + unsigned int frame_size; /* precision in bytes * channels */ + bool floating; +}; + +struct cubeb_stream { + struct cubeb * context; + void * user_ptr; + pthread_t thread; + bool doorbell; /* (m) */ + pthread_cond_t doorbell_cv; /* (m) */ + pthread_cond_t stopped_cv; /* (m) */ + pthread_mutex_t mtx; /* Members protected by this should be marked (m) */ + bool thread_created; /* (m) */ + bool running; /* (m) */ + bool destroying; /* (m) */ + cubeb_state state; /* (m) */ + float volume /* (m) */; + struct oss_stream play; + struct oss_stream record; + cubeb_data_callback data_cb; + cubeb_state_callback state_cb; + uint64_t frames_written /* (m) */; +}; + +static char const * +oss_cubeb_devid_intern(cubeb * context, char const * devid) +{ + char const * is; + pthread_mutex_lock(&context->mutex); + is = cubeb_strings_intern(context->devid_strs, devid); + pthread_mutex_unlock(&context->mutex); + return is; +} + +int +oss_init(cubeb ** context, char const * context_name) +{ + cubeb * c; + + (void)context_name; + if ((c = calloc(1, sizeof(cubeb))) == NULL) { + return CUBEB_ERROR; + } + + if (cubeb_strings_init(&c->devid_strs) == CUBEB_ERROR) { + goto fail; + } + + if (pthread_mutex_init(&c->mutex, NULL) != 0) { + goto fail; + } + + c->ops = &oss_ops; + *context = c; + return CUBEB_OK; + +fail: + cubeb_strings_destroy(c->devid_strs); + free(c); + return CUBEB_ERROR; +} + +static void +oss_destroy(cubeb * context) +{ + pthread_mutex_destroy(&context->mutex); + cubeb_strings_destroy(context->devid_strs); + free(context); +} + +static char const * +oss_get_backend_id(cubeb * context) +{ + return "oss"; +} + +static int +oss_get_preferred_sample_rate(cubeb * context, uint32_t * rate) +{ + (void)context; + + *rate = OSS_PREFER_RATE; + return CUBEB_OK; +} + +static int +oss_get_max_channel_count(cubeb * context, uint32_t * max_channels) +{ + (void)context; + + *max_channels = OSS_MAX_CHANNELS; + return CUBEB_OK; +} + +static int +oss_get_min_latency(cubeb * context, cubeb_stream_params params, + uint32_t * latency_frames) +{ + (void)context; + + *latency_frames = (OSS_LATENCY_MS * params.rate) / 1000; + return CUBEB_OK; +} + +static void +oss_free_cubeb_device_info_strings(cubeb_device_info * cdi) +{ + free((char *)cdi->device_id); + free((char *)cdi->friendly_name); + free((char *)cdi->group_id); + cdi->device_id = NULL; + cdi->friendly_name = NULL; + cdi->group_id = NULL; +} + +#if defined(__FreeBSD__) || defined(__DragonFly__) +/* + * Check if the specified DSP is okay for the purpose specified + * in type. Here type can only specify one operation each time + * this helper is called. + * + * Return 0 if OK, otherwise 1. + */ +static int +oss_probe_open(const char * dsppath, cubeb_device_type type, int * fdp, + oss_audioinfo * resai) +{ + oss_audioinfo ai; + int error; + int oflags = (type == CUBEB_DEVICE_TYPE_INPUT) ? O_RDONLY : O_WRONLY; + int dspfd = open(dsppath, oflags); + if (dspfd == -1) + return 1; + + ai.dev = -1; + error = ioctl(dspfd, SNDCTL_AUDIOINFO, &ai); + if (error < 0) { + close(dspfd); + return 1; + } + + if (resai) + *resai = ai; + if (fdp) + *fdp = dspfd; + else + close(dspfd); + return 0; +} + +struct sndstat_info { + oss_devnode_t devname; + const char * desc; + cubeb_device_type type; + int preferred; +}; + +static int +oss_sndstat_line_parse(char * line, int is_ud, struct sndstat_info * sinfo) +{ + char *matchptr = line, *n = NULL; + struct sndstat_info res; + + memset(&res, 0, sizeof(res)); + + n = strchr(matchptr, ':'); + if (n == NULL) + goto fail; + if (is_ud == 0) { + unsigned int devunit; + + if (sscanf(matchptr, "pcm%u: ", &devunit) < 1) + goto fail; + + if (snprintf(res.devname, sizeof(res.devname), "/dev/dsp%u", devunit) < 1) + goto fail; + } else { + if (n - matchptr >= (ssize_t)(sizeof(res.devname) - strlen("/dev/"))) + goto fail; + + strlcpy(res.devname, "/dev/", sizeof(res.devname)); + strncat(res.devname, matchptr, n - matchptr); + } + matchptr = n + 1; + + n = strchr(matchptr, '<'); + if (n == NULL) + goto fail; + matchptr = n + 1; + n = strrchr(matchptr, '>'); + if (n == NULL) + goto fail; + *n = 0; + res.desc = matchptr; + matchptr = n + 1; + + n = strchr(matchptr, '('); + if (n == NULL) + goto fail; + matchptr = n + 1; + n = strrchr(matchptr, ')'); + if (n == NULL) + goto fail; + *n = 0; + if (!isdigit(matchptr[0])) { + if (strstr(matchptr, "play") != NULL) + res.type |= CUBEB_DEVICE_TYPE_OUTPUT; + if (strstr(matchptr, "rec") != NULL) + res.type |= CUBEB_DEVICE_TYPE_INPUT; + } else { + int p, r; + if (sscanf(matchptr, "%dp:%*dv/%dr:%*dv", &p, &r) != 2) + goto fail; + if (p > 0) + res.type |= CUBEB_DEVICE_TYPE_OUTPUT; + if (r > 0) + res.type |= CUBEB_DEVICE_TYPE_INPUT; + } + matchptr = n + 1; + if (strstr(matchptr, "default") != NULL) + res.preferred = 1; + + *sinfo = res; + return 0; + +fail: + return 1; +} + +/* + * XXX: On FreeBSD we have to rely on SNDCTL_CARDINFO to get all + * the usable audio devices currently, as SNDCTL_AUDIOINFO will + * never return directly usable audio device nodes. + */ +static int +oss_enumerate_devices(cubeb * context, cubeb_device_type type, + cubeb_device_collection * collection) +{ + cubeb_device_info * devinfop = NULL; + char * line = NULL; + size_t linecap = 0; + FILE * sndstatfp = NULL; + int collection_cnt = 0; + int is_ud = 0; + int skipall = 0; + + devinfop = calloc(1, sizeof(cubeb_device_info)); + if (devinfop == NULL) + goto fail; + + sndstatfp = fopen("/dev/sndstat", "r"); + if (sndstatfp == NULL) + goto fail; + while (getline(&line, &linecap, sndstatfp) > 0) { + const char * devid = NULL; + struct sndstat_info sinfo; + oss_audioinfo ai; + + if (!strncmp(line, SNDSTAT_FV_BEGIN_STR, strlen(SNDSTAT_FV_BEGIN_STR))) { + skipall = 1; + continue; + } + if (!strncmp(line, SNDSTAT_BEGIN_STR, strlen(SNDSTAT_BEGIN_STR))) { + is_ud = 0; + skipall = 0; + continue; + } + if (!strncmp(line, SNDSTAT_USER_BEGIN_STR, + strlen(SNDSTAT_USER_BEGIN_STR))) { + is_ud = 1; + skipall = 0; + continue; + } + if (skipall || isblank(line[0])) + continue; + + if (oss_sndstat_line_parse(line, is_ud, &sinfo)) + continue; + + devinfop[collection_cnt].type = 0; + switch (sinfo.type) { + case CUBEB_DEVICE_TYPE_INPUT: + if (type & CUBEB_DEVICE_TYPE_OUTPUT) + continue; + break; + case CUBEB_DEVICE_TYPE_OUTPUT: + if (type & CUBEB_DEVICE_TYPE_INPUT) + continue; + break; + case 0: + continue; + } + + if (oss_probe_open(sinfo.devname, type, NULL, &ai)) + continue; + + devid = oss_cubeb_devid_intern(context, sinfo.devname); + if (devid == NULL) + continue; + + devinfop[collection_cnt].device_id = strdup(sinfo.devname); + asprintf((char **)&devinfop[collection_cnt].friendly_name, "%s: %s", + sinfo.devname, sinfo.desc); + devinfop[collection_cnt].group_id = strdup(sinfo.devname); + devinfop[collection_cnt].vendor_name = NULL; + if (devinfop[collection_cnt].device_id == NULL || + devinfop[collection_cnt].friendly_name == NULL || + devinfop[collection_cnt].group_id == NULL) { + oss_free_cubeb_device_info_strings(&devinfop[collection_cnt]); + continue; + } + + devinfop[collection_cnt].type = type; + devinfop[collection_cnt].devid = devid; + devinfop[collection_cnt].state = CUBEB_DEVICE_STATE_ENABLED; + devinfop[collection_cnt].preferred = + (sinfo.preferred) ? CUBEB_DEVICE_PREF_ALL : CUBEB_DEVICE_PREF_NONE; + devinfop[collection_cnt].format = CUBEB_DEVICE_FMT_S16NE; + devinfop[collection_cnt].default_format = CUBEB_DEVICE_FMT_S16NE; + devinfop[collection_cnt].max_channels = ai.max_channels; + devinfop[collection_cnt].default_rate = OSS_PREFER_RATE; + devinfop[collection_cnt].max_rate = ai.max_rate; + devinfop[collection_cnt].min_rate = ai.min_rate; + devinfop[collection_cnt].latency_lo = 0; + devinfop[collection_cnt].latency_hi = 0; + + collection_cnt++; + + void * newp = + reallocarray(devinfop, collection_cnt + 1, sizeof(cubeb_device_info)); + if (newp == NULL) + goto fail; + devinfop = newp; + } + + free(line); + fclose(sndstatfp); + + collection->count = collection_cnt; + collection->device = devinfop; + + return CUBEB_OK; + +fail: + free(line); + if (sndstatfp) + fclose(sndstatfp); + free(devinfop); + return CUBEB_ERROR; +} + +#else + +static int +oss_enumerate_devices(cubeb * context, cubeb_device_type type, + cubeb_device_collection * collection) +{ + oss_sysinfo si; + int error, i; + cubeb_device_info * devinfop = NULL; + int collection_cnt = 0; + int mixer_fd = -1; + + mixer_fd = open(OSS_DEFAULT_MIXER, O_RDWR); + if (mixer_fd == -1) { + LOG("Failed to open mixer %s. errno: %d", OSS_DEFAULT_MIXER, errno); + return CUBEB_ERROR; + } + + error = ioctl(mixer_fd, SNDCTL_SYSINFO, &si); + if (error) { + LOG("Failed to run SNDCTL_SYSINFO on mixer %s. errno: %d", + OSS_DEFAULT_MIXER, errno); + goto fail; + } + + devinfop = calloc(si.numaudios, sizeof(cubeb_device_info)); + if (devinfop == NULL) + goto fail; + + collection->count = 0; + for (i = 0; i < si.numaudios; i++) { + oss_audioinfo ai; + cubeb_device_info cdi = {0}; + const char * devid = NULL; + + ai.dev = i; + error = ioctl(mixer_fd, SNDCTL_AUDIOINFO, &ai); + if (error) + goto fail; + + assert(ai.dev < si.numaudios); + if (!ai.enabled) + continue; + + cdi.type = 0; + switch (ai.caps & DSP_CAP_DUPLEX) { + case DSP_CAP_INPUT: + if (type & CUBEB_DEVICE_TYPE_OUTPUT) + continue; + break; + case DSP_CAP_OUTPUT: + if (type & CUBEB_DEVICE_TYPE_INPUT) + continue; + break; + case 0: + continue; + } + cdi.type = type; + + devid = oss_cubeb_devid_intern(context, ai.devnode); + cdi.device_id = strdup(ai.name); + cdi.friendly_name = strdup(ai.name); + cdi.group_id = strdup(ai.name); + if (devid == NULL || cdi.device_id == NULL || cdi.friendly_name == NULL || + cdi.group_id == NULL) { + oss_free_cubeb_device_info_strings(&cdi); + continue; + } + + cdi.devid = devid; + cdi.vendor_name = NULL; + cdi.state = CUBEB_DEVICE_STATE_ENABLED; + cdi.preferred = CUBEB_DEVICE_PREF_NONE; + cdi.format = CUBEB_DEVICE_FMT_S16NE; + cdi.default_format = CUBEB_DEVICE_FMT_S16NE; + cdi.max_channels = ai.max_channels; + cdi.default_rate = OSS_PREFER_RATE; + cdi.max_rate = ai.max_rate; + cdi.min_rate = ai.min_rate; + cdi.latency_lo = 0; + cdi.latency_hi = 0; + + devinfop[collection_cnt++] = cdi; + } + + collection->count = collection_cnt; + collection->device = devinfop; + + if (mixer_fd != -1) + close(mixer_fd); + return CUBEB_OK; + +fail: + if (mixer_fd != -1) + close(mixer_fd); + free(devinfop); + return CUBEB_ERROR; +} + +#endif + +static int +oss_device_collection_destroy(cubeb * context, + cubeb_device_collection * collection) +{ + size_t i; + for (i = 0; i < collection->count; i++) { + oss_free_cubeb_device_info_strings(&collection->device[i]); + } + free(collection->device); + collection->device = NULL; + collection->count = 0; + return 0; +} + +static unsigned int +oss_chn_from_cubeb(cubeb_channel chn) +{ + switch (chn) { + case CHANNEL_FRONT_LEFT: + return CHID_L; + case CHANNEL_FRONT_RIGHT: + return CHID_R; + case CHANNEL_FRONT_CENTER: + return CHID_C; + case CHANNEL_LOW_FREQUENCY: + return CHID_LFE; + case CHANNEL_BACK_LEFT: + return CHID_LR; + case CHANNEL_BACK_RIGHT: + return CHID_RR; + case CHANNEL_SIDE_LEFT: + return CHID_LS; + case CHANNEL_SIDE_RIGHT: + return CHID_RS; + default: + return CHID_UNDEF; + } +} + +static unsigned long long +oss_cubeb_layout_to_chnorder(cubeb_channel_layout layout) +{ + unsigned int i, nchns = 0; + unsigned long long chnorder = 0; + + for (i = 0; layout; i++, layout >>= 1) { + unsigned long long chid = oss_chn_from_cubeb((layout & 1) << i); + if (chid == CHID_UNDEF) + continue; + + chnorder |= (chid & 0xf) << nchns * 4; + nchns++; + } + + return chnorder; +} + +static int +oss_copy_params(int fd, cubeb_stream * stream, cubeb_stream_params * params, + struct stream_info * sinfo) +{ + unsigned long long chnorder; + + sinfo->channels = params->channels; + sinfo->sample_rate = params->rate; + switch (params->format) { + case CUBEB_SAMPLE_S16LE: + sinfo->fmt = AFMT_S16_LE; + sinfo->precision = 16; + break; + case CUBEB_SAMPLE_S16BE: + sinfo->fmt = AFMT_S16_BE; + sinfo->precision = 16; + break; + case CUBEB_SAMPLE_FLOAT32NE: + sinfo->fmt = AFMT_S32_NE; + sinfo->precision = 32; + break; + default: + LOG("Unsupported format"); + return CUBEB_ERROR_INVALID_FORMAT; + } + if (ioctl(fd, SNDCTL_DSP_CHANNELS, &sinfo->channels) == -1) { + return CUBEB_ERROR; + } + if (ioctl(fd, SNDCTL_DSP_SETFMT, &sinfo->fmt) == -1) { + return CUBEB_ERROR; + } + if (ioctl(fd, SNDCTL_DSP_SPEED, &sinfo->sample_rate) == -1) { + return CUBEB_ERROR; + } + /* Mono layout is an exception */ + if (params->layout != CUBEB_LAYOUT_UNDEFINED && + params->layout != CUBEB_LAYOUT_MONO) { + chnorder = oss_cubeb_layout_to_chnorder(params->layout); + if (ioctl(fd, SNDCTL_DSP_SET_CHNORDER, &chnorder) == -1) + LOG("Non-fatal error %d occured when setting channel order.", errno); + } + return CUBEB_OK; +} + +static int +oss_stream_stop(cubeb_stream * s) +{ + pthread_mutex_lock(&s->mtx); + if (s->thread_created && s->running) { + s->running = false; + s->doorbell = false; + pthread_cond_wait(&s->stopped_cv, &s->mtx); + } + if (s->state != CUBEB_STATE_STOPPED) { + s->state = CUBEB_STATE_STOPPED; + pthread_mutex_unlock(&s->mtx); + s->state_cb(s, s->user_ptr, CUBEB_STATE_STOPPED); + } else { + pthread_mutex_unlock(&s->mtx); + } + return CUBEB_OK; +} + +static void +oss_stream_destroy(cubeb_stream * s) +{ + pthread_mutex_lock(&s->mtx); + if (s->thread_created) { + s->destroying = true; + s->doorbell = true; + pthread_cond_signal(&s->doorbell_cv); + } + pthread_mutex_unlock(&s->mtx); + pthread_join(s->thread, NULL); + + pthread_cond_destroy(&s->doorbell_cv); + pthread_cond_destroy(&s->stopped_cv); + pthread_mutex_destroy(&s->mtx); + if (s->play.fd != -1) { + close(s->play.fd); + } + if (s->record.fd != -1) { + close(s->record.fd); + } + free(s->play.buf); + free(s->record.buf); + free(s); +} + +static void +oss_float_to_linear32(void * buf, unsigned sample_count, float vol) +{ + float * in = buf; + int32_t * out = buf; + int32_t * tail = out + sample_count; + + while (out < tail) { + int64_t f = *(in++) * vol * 0x80000000LL; + if (f < -INT32_MAX) + f = -INT32_MAX; + else if (f > INT32_MAX) + f = INT32_MAX; + *(out++) = f; + } +} + +static void +oss_linear32_to_float(void * buf, unsigned sample_count) +{ + int32_t * in = buf; + float * out = buf; + float * tail = out + sample_count; + + while (out < tail) { + *(out++) = (1.0 / 0x80000000LL) * *(in++); + } +} + +static void +oss_linear16_set_vol(int16_t * buf, unsigned sample_count, float vol) +{ + unsigned i; + int32_t multiplier = vol * 0x8000; + + for (i = 0; i < sample_count; ++i) { + buf[i] = (buf[i] * multiplier) >> 15; + } +} + +static int +oss_get_rec_frames(cubeb_stream * s, unsigned int nframes) +{ + size_t rem = nframes * s->record.frame_size; + size_t read_ofs = 0; + while (rem > 0) { + ssize_t n; + if ((n = read(s->record.fd, (uint8_t *)s->record.buf + read_ofs, rem)) < + 0) { + if (errno == EINTR) + continue; + return CUBEB_ERROR; + } + read_ofs += n; + rem -= n; + } + return 0; +} + +static int +oss_put_play_frames(cubeb_stream * s, unsigned int nframes) +{ + size_t rem = nframes * s->play.frame_size; + size_t write_ofs = 0; + while (rem > 0) { + ssize_t n; + if ((n = write(s->play.fd, (uint8_t *)s->play.buf + write_ofs, rem)) < 0) { + if (errno == EINTR) + continue; + return CUBEB_ERROR; + } + pthread_mutex_lock(&s->mtx); + s->frames_written += n / s->play.frame_size; + pthread_mutex_unlock(&s->mtx); + write_ofs += n; + rem -= n; + } + return 0; +} + +static int +oss_wait_fds_for_space(cubeb_stream * s, long * nfrp) +{ + audio_buf_info bi; + struct pollfd pfds[2]; + long nfr, tnfr; + int i; + + assert(s->play.fd != -1 || s->record.fd != -1); + pfds[0].events = POLLOUT | POLLHUP; + pfds[0].revents = 0; + pfds[0].fd = s->play.fd; + pfds[1].events = POLLIN | POLLHUP; + pfds[1].revents = 0; + pfds[1].fd = s->record.fd; + +retry: + nfr = LONG_MAX; + + if (poll(pfds, 2, 1000) == -1) { + return CUBEB_ERROR; + } + + for (i = 0; i < 2; i++) { + if (pfds[i].revents & POLLHUP) { + return CUBEB_ERROR; + } + } + + if (s->play.fd != -1) { + if (ioctl(s->play.fd, SNDCTL_DSP_GETOSPACE, &bi) == -1) { + return CUBEB_STATE_ERROR; + } + tnfr = bi.bytes / s->play.frame_size; + if (tnfr <= 0) { + /* too little space - stop polling record, if any */ + pfds[0].fd = s->play.fd; + pfds[1].fd = -1; + goto retry; + } else if (tnfr > (long)s->play.maxframes) { + /* too many frames available - limit */ + tnfr = (long)s->play.maxframes; + } + if (nfr > tnfr) { + nfr = tnfr; + } + } + if (s->record.fd != -1) { + if (ioctl(s->record.fd, SNDCTL_DSP_GETISPACE, &bi) == -1) { + return CUBEB_STATE_ERROR; + } + tnfr = bi.bytes / s->record.frame_size; + if (tnfr <= 0) { + /* too little space - stop polling playback, if any */ + pfds[0].fd = -1; + pfds[1].fd = s->record.fd; + goto retry; + } else if (tnfr > (long)s->record.maxframes) { + /* too many frames available - limit */ + tnfr = (long)s->record.maxframes; + } + if (nfr > tnfr) { + nfr = tnfr; + } + } + + *nfrp = nfr; + return 0; +} + +/* 1 - Stopped by cubeb_stream_stop, otherwise 0 */ +static int +oss_audio_loop(cubeb_stream * s, cubeb_state * new_state) +{ + cubeb_state state = CUBEB_STATE_STOPPED; + int trig = 0, drain = 0; + const bool play_on = s->play.fd != -1, record_on = s->record.fd != -1; + long nfr = 0; + + if (record_on) { + if (ioctl(s->record.fd, SNDCTL_DSP_SETTRIGGER, &trig)) { + LOG("Error %d occured when setting trigger on record fd", errno); + state = CUBEB_STATE_ERROR; + goto breakdown; + } + + trig |= PCM_ENABLE_INPUT; + memset(s->record.buf, 0, s->record.bufframes * s->record.frame_size); + + if (ioctl(s->record.fd, SNDCTL_DSP_SETTRIGGER, &trig) == -1) { + LOG("Error %d occured when setting trigger on record fd", errno); + state = CUBEB_STATE_ERROR; + goto breakdown; + } + } + + if (!play_on && !record_on) { + /* + * Stop here if the stream is not play & record stream, + * play-only stream or record-only stream + */ + + goto breakdown; + } + + while (1) { + pthread_mutex_lock(&s->mtx); + if (!s->running || s->destroying) { + pthread_mutex_unlock(&s->mtx); + break; + } + pthread_mutex_unlock(&s->mtx); + + long got = 0; + if (nfr > 0) { + if (record_on) { + if (oss_get_rec_frames(s, nfr) == CUBEB_ERROR) { + state = CUBEB_STATE_ERROR; + goto breakdown; + } + if (s->record.floating) { + oss_linear32_to_float(s->record.buf, s->record.info.channels * nfr); + } + } + + got = s->data_cb(s, s->user_ptr, s->record.buf, s->play.buf, nfr); + if (got == CUBEB_ERROR) { + state = CUBEB_STATE_ERROR; + goto breakdown; + } + if (got < nfr) { + if (s->play.fd != -1) { + drain = 1; + } else { + /* + * This is a record-only stream and number of frames + * returned from data_cb() is smaller than number + * of frames required to read. Stop here. + */ + state = CUBEB_STATE_STOPPED; + goto breakdown; + } + } + + if (got > 0 && play_on) { + float vol; + + pthread_mutex_lock(&s->mtx); + vol = s->volume; + pthread_mutex_unlock(&s->mtx); + + if (s->play.floating) { + oss_float_to_linear32(s->play.buf, s->play.info.channels * got, vol); + } else { + oss_linear16_set_vol((int16_t *)s->play.buf, + s->play.info.channels * got, vol); + } + if (oss_put_play_frames(s, got) == CUBEB_ERROR) { + state = CUBEB_STATE_ERROR; + goto breakdown; + } + } + if (drain) { + state = CUBEB_STATE_DRAINED; + goto breakdown; + } + } + + if (oss_wait_fds_for_space(s, &nfr) != 0) { + state = CUBEB_STATE_ERROR; + goto breakdown; + } + } + + return 1; + +breakdown: + pthread_mutex_lock(&s->mtx); + *new_state = s->state = state; + s->running = false; + pthread_mutex_unlock(&s->mtx); + return 0; +} + +static void * +oss_io_routine(void * arg) +{ + cubeb_stream * s = arg; + cubeb_state new_state; + int stopped; + + CUBEB_REGISTER_THREAD("cubeb rendering thread"); + + do { + pthread_mutex_lock(&s->mtx); + if (s->destroying) { + pthread_mutex_unlock(&s->mtx); + break; + } + pthread_mutex_unlock(&s->mtx); + + stopped = oss_audio_loop(s, &new_state); + if (s->record.fd != -1) + ioctl(s->record.fd, SNDCTL_DSP_HALT_INPUT, NULL); + if (!stopped) + s->state_cb(s, s->user_ptr, new_state); + + pthread_mutex_lock(&s->mtx); + pthread_cond_signal(&s->stopped_cv); + if (s->destroying) { + pthread_mutex_unlock(&s->mtx); + break; + } + while (!s->doorbell) { + pthread_cond_wait(&s->doorbell_cv, &s->mtx); + } + s->doorbell = false; + pthread_mutex_unlock(&s->mtx); + } while (1); + + pthread_mutex_lock(&s->mtx); + s->thread_created = false; + pthread_mutex_unlock(&s->mtx); + + CUBEB_UNREGISTER_THREAD(); + + return NULL; +} + +static inline int +oss_calc_frag_shift(unsigned int frames, unsigned int frame_size) +{ + int n = 4; + int blksize = frames * frame_size; + while ((1 << n) < blksize) { + n++; + } + return n; +} + +static inline int +oss_get_frag_params(unsigned int shift) +{ + return (OSS_NFRAGS << 16) | shift; +} + +static int +oss_stream_init(cubeb * context, cubeb_stream ** stream, + char const * stream_name, cubeb_devid input_device, + cubeb_stream_params * input_stream_params, + cubeb_devid output_device, + cubeb_stream_params * output_stream_params, + unsigned int latency_frames, cubeb_data_callback data_callback, + cubeb_state_callback state_callback, void * user_ptr) +{ + int ret = CUBEB_OK; + cubeb_stream * s = NULL; + const char * defdsp; + + if (!(defdsp = getenv(ENV_AUDIO_DEVICE)) || *defdsp == '\0') + defdsp = OSS_DEFAULT_DEVICE; + + (void)stream_name; + if ((s = calloc(1, sizeof(cubeb_stream))) == NULL) { + ret = CUBEB_ERROR; + goto error; + } + s->state = CUBEB_STATE_STOPPED; + s->record.fd = s->play.fd = -1; + if (input_device != NULL) { + strlcpy(s->record.name, input_device, sizeof(s->record.name)); + } else { + strlcpy(s->record.name, defdsp, sizeof(s->record.name)); + } + if (output_device != NULL) { + strlcpy(s->play.name, output_device, sizeof(s->play.name)); + } else { + strlcpy(s->play.name, defdsp, sizeof(s->play.name)); + } + if (input_stream_params != NULL) { + unsigned int nb_channels; + uint32_t minframes; + + if (input_stream_params->prefs & CUBEB_STREAM_PREF_LOOPBACK) { + LOG("Loopback not supported"); + ret = CUBEB_ERROR_NOT_SUPPORTED; + goto error; + } + nb_channels = cubeb_channel_layout_nb_channels(input_stream_params->layout); + if (input_stream_params->layout != CUBEB_LAYOUT_UNDEFINED && + nb_channels != input_stream_params->channels) { + LOG("input_stream_params->layout does not match " + "input_stream_params->channels"); + ret = CUBEB_ERROR_INVALID_PARAMETER; + goto error; + } + if ((s->record.fd = open(s->record.name, O_RDONLY)) == -1) { + LOG("Audio device \"%s\" could not be opened as read-only", + s->record.name); + ret = CUBEB_ERROR_DEVICE_UNAVAILABLE; + goto error; + } + if ((ret = oss_copy_params(s->record.fd, s, input_stream_params, + &s->record.info)) != CUBEB_OK) { + LOG("Setting record params failed"); + goto error; + } + s->record.floating = + (input_stream_params->format == CUBEB_SAMPLE_FLOAT32NE); + s->record.frame_size = + s->record.info.channels * (s->record.info.precision / 8); + s->record.bufframes = latency_frames; + + oss_get_min_latency(context, *input_stream_params, &minframes); + if (s->record.bufframes < minframes) { + s->record.bufframes = minframes; + } + } + if (output_stream_params != NULL) { + unsigned int nb_channels; + uint32_t minframes; + + if (output_stream_params->prefs & CUBEB_STREAM_PREF_LOOPBACK) { + LOG("Loopback not supported"); + ret = CUBEB_ERROR_NOT_SUPPORTED; + goto error; + } + nb_channels = + cubeb_channel_layout_nb_channels(output_stream_params->layout); + if (output_stream_params->layout != CUBEB_LAYOUT_UNDEFINED && + nb_channels != output_stream_params->channels) { + LOG("output_stream_params->layout does not match " + "output_stream_params->channels"); + ret = CUBEB_ERROR_INVALID_PARAMETER; + goto error; + } + if ((s->play.fd = open(s->play.name, O_WRONLY)) == -1) { + LOG("Audio device \"%s\" could not be opened as write-only", + s->play.name); + ret = CUBEB_ERROR_DEVICE_UNAVAILABLE; + goto error; + } + if ((ret = oss_copy_params(s->play.fd, s, output_stream_params, + &s->play.info)) != CUBEB_OK) { + LOG("Setting play params failed"); + goto error; + } + s->play.floating = (output_stream_params->format == CUBEB_SAMPLE_FLOAT32NE); + s->play.frame_size = s->play.info.channels * (s->play.info.precision / 8); + s->play.bufframes = latency_frames; + + oss_get_min_latency(context, *output_stream_params, &minframes); + if (s->play.bufframes < minframes) { + s->play.bufframes = minframes; + } + } + if (s->play.fd != -1) { + int frag = oss_get_frag_params( + oss_calc_frag_shift(s->play.bufframes, s->play.frame_size)); + if (ioctl(s->play.fd, SNDCTL_DSP_SETFRAGMENT, &frag)) + LOG("Failed to set play fd with SNDCTL_DSP_SETFRAGMENT. frag: 0x%x", + frag); + audio_buf_info bi; + if (ioctl(s->play.fd, SNDCTL_DSP_GETOSPACE, &bi)) + LOG("Failed to get play fd's buffer info."); + else { + s->play.bufframes = (bi.fragsize * bi.fragstotal) / s->play.frame_size; + } + int lw; + + /* + * Force 32 ms service intervals at most, or when recording is + * active, use the recording service intervals as a reference. + */ + s->play.maxframes = (32 * output_stream_params->rate) / 1000; + if (s->record.fd != -1 || s->play.maxframes >= s->play.bufframes) { + lw = s->play.frame_size; /* Feed data when possible. */ + s->play.maxframes = s->play.bufframes; + } else { + lw = (s->play.bufframes - s->play.maxframes) * s->play.frame_size; + } + if (ioctl(s->play.fd, SNDCTL_DSP_LOW_WATER, &lw)) + LOG("Audio device \"%s\" (play) could not set trigger threshold", + s->play.name); + } + if (s->record.fd != -1) { + int frag = oss_get_frag_params( + oss_calc_frag_shift(s->record.bufframes, s->record.frame_size)); + if (ioctl(s->record.fd, SNDCTL_DSP_SETFRAGMENT, &frag)) + LOG("Failed to set record fd with SNDCTL_DSP_SETFRAGMENT. frag: 0x%x", + frag); + audio_buf_info bi; + if (ioctl(s->record.fd, SNDCTL_DSP_GETISPACE, &bi)) + LOG("Failed to get record fd's buffer info."); + else { + s->record.bufframes = + (bi.fragsize * bi.fragstotal) / s->record.frame_size; + } + + s->record.maxframes = s->record.bufframes; + int lw = s->record.frame_size; + if (ioctl(s->record.fd, SNDCTL_DSP_LOW_WATER, &lw)) + LOG("Audio device \"%s\" (record) could not set trigger threshold", + s->record.name); + } + s->context = context; + s->volume = 1.0; + s->state_cb = state_callback; + s->data_cb = data_callback; + s->user_ptr = user_ptr; + + if (pthread_mutex_init(&s->mtx, NULL) != 0) { + LOG("Failed to create mutex"); + goto error; + } + if (pthread_cond_init(&s->doorbell_cv, NULL) != 0) { + LOG("Failed to create cv"); + goto error; + } + if (pthread_cond_init(&s->stopped_cv, NULL) != 0) { + LOG("Failed to create cv"); + goto error; + } + s->doorbell = false; + + if (s->play.fd != -1) { + if ((s->play.buf = calloc(s->play.bufframes, s->play.frame_size)) == NULL) { + ret = CUBEB_ERROR; + goto error; + } + } + if (s->record.fd != -1) { + if ((s->record.buf = calloc(s->record.bufframes, s->record.frame_size)) == + NULL) { + ret = CUBEB_ERROR; + goto error; + } + } + + *stream = s; + return CUBEB_OK; +error: + if (s != NULL) { + oss_stream_destroy(s); + } + return ret; +} + +static int +oss_stream_thr_create(cubeb_stream * s) +{ + if (s->thread_created) { + s->doorbell = true; + pthread_cond_signal(&s->doorbell_cv); + return CUBEB_OK; + } + + if (pthread_create(&s->thread, NULL, oss_io_routine, s) != 0) { + LOG("Couldn't create thread"); + return CUBEB_ERROR; + } + + return CUBEB_OK; +} + +static int +oss_stream_start(cubeb_stream * s) +{ + s->state_cb(s, s->user_ptr, CUBEB_STATE_STARTED); + pthread_mutex_lock(&s->mtx); + /* Disallow starting an already started stream */ + assert(!s->running && s->state != CUBEB_STATE_STARTED); + if (oss_stream_thr_create(s) != CUBEB_OK) { + pthread_mutex_unlock(&s->mtx); + s->state_cb(s, s->user_ptr, CUBEB_STATE_ERROR); + return CUBEB_ERROR; + } + s->state = CUBEB_STATE_STARTED; + s->thread_created = true; + s->running = true; + pthread_mutex_unlock(&s->mtx); + return CUBEB_OK; +} + +static int +oss_stream_get_position(cubeb_stream * s, uint64_t * position) +{ + pthread_mutex_lock(&s->mtx); + *position = s->frames_written; + pthread_mutex_unlock(&s->mtx); + return CUBEB_OK; +} + +static int +oss_stream_get_latency(cubeb_stream * s, uint32_t * latency) +{ + int delay; + + if (ioctl(s->play.fd, SNDCTL_DSP_GETODELAY, &delay) == -1) { + return CUBEB_ERROR; + } + + /* Return number of frames there */ + *latency = delay / s->play.frame_size; + return CUBEB_OK; +} + +static int +oss_stream_set_volume(cubeb_stream * stream, float volume) +{ + if (volume < 0.0) + volume = 0.0; + else if (volume > 1.0) + volume = 1.0; + pthread_mutex_lock(&stream->mtx); + stream->volume = volume; + pthread_mutex_unlock(&stream->mtx); + return CUBEB_OK; +} + +static int +oss_get_current_device(cubeb_stream * stream, cubeb_device ** const device) +{ + *device = calloc(1, sizeof(cubeb_device)); + if (*device == NULL) { + return CUBEB_ERROR; + } + (*device)->input_name = + stream->record.fd != -1 ? strdup(stream->record.name) : NULL; + (*device)->output_name = + stream->play.fd != -1 ? strdup(stream->play.name) : NULL; + return CUBEB_OK; +} + +static int +oss_stream_device_destroy(cubeb_stream * stream, cubeb_device * device) +{ + (void)stream; + free(device->input_name); + free(device->output_name); + free(device); + return CUBEB_OK; +} + +static struct cubeb_ops const oss_ops = { + .init = oss_init, + .get_backend_id = oss_get_backend_id, + .get_max_channel_count = oss_get_max_channel_count, + .get_min_latency = oss_get_min_latency, + .get_preferred_sample_rate = oss_get_preferred_sample_rate, + .get_supported_input_processing_params = NULL, + .enumerate_devices = oss_enumerate_devices, + .device_collection_destroy = oss_device_collection_destroy, + .destroy = oss_destroy, + .stream_init = oss_stream_init, + .stream_destroy = oss_stream_destroy, + .stream_start = oss_stream_start, + .stream_stop = oss_stream_stop, + .stream_get_position = oss_stream_get_position, + .stream_get_latency = oss_stream_get_latency, + .stream_get_input_latency = NULL, + .stream_set_volume = oss_stream_set_volume, + .stream_set_name = NULL, + .stream_get_current_device = oss_get_current_device, + .stream_set_input_mute = NULL, + .stream_set_input_processing_params = NULL, + .stream_device_destroy = oss_stream_device_destroy, + .stream_register_device_changed_callback = NULL, + .register_device_collection_changed = NULL}; diff --git a/media/libcubeb/src/cubeb_osx_run_loop.c b/media/libcubeb/src/cubeb_osx_run_loop.c new file mode 100644 index 0000000000..0ba9536560 --- /dev/null +++ b/media/libcubeb/src/cubeb_osx_run_loop.c @@ -0,0 +1,11 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-*/ +/* 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/. */ + +#include "OSXRunLoopSingleton.h" + +void cubeb_set_coreaudio_notification_runloop() +{ + mozilla_set_coreaudio_notification_runloop_if_needed(); +} diff --git a/media/libcubeb/src/cubeb_osx_run_loop.h b/media/libcubeb/src/cubeb_osx_run_loop.h new file mode 100644 index 0000000000..8d88a37140 --- /dev/null +++ b/media/libcubeb/src/cubeb_osx_run_loop.h @@ -0,0 +1,23 @@ +/* + * Copyright © 2014 Mozilla Foundation + * + * This program is made available under an ISC-style license. See the + * accompanying file LICENSE for details. + */ + +/* On OSX 10.6 and after, the notification callbacks from the audio hardware are + * called on the main thread. Setting the kAudioHardwarePropertyRunLoop property + * to null tells the OSX to use a separate thread for that. + * + * This has to be called only once per process, so it is in a separate header + * for easy integration in other code bases. */ +#if defined(__cplusplus) +extern "C" { +#endif + +void +cubeb_set_coreaudio_notification_runloop(); + +#if defined(__cplusplus) +} +#endif diff --git a/media/libcubeb/src/cubeb_resampler.cpp b/media/libcubeb/src/cubeb_resampler.cpp new file mode 100644 index 0000000000..c31944b826 --- /dev/null +++ b/media/libcubeb/src/cubeb_resampler.cpp @@ -0,0 +1,373 @@ +/* + * Copyright © 2014 Mozilla Foundation + * + * This program is made available under an ISC-style license. See the + * accompanying file LICENSE for details. + */ +#ifndef NOMINMAX +#define NOMINMAX +#endif // NOMINMAX + +#include "cubeb_resampler.h" +#include "cubeb-speex-resampler.h" +#include "cubeb_resampler_internal.h" +#include "cubeb_utils.h" +#include <algorithm> +#include <cassert> +#include <cmath> +#include <cstddef> +#include <cstdio> +#include <cstring> + +int +to_speex_quality(cubeb_resampler_quality q) +{ + switch (q) { + case CUBEB_RESAMPLER_QUALITY_VOIP: + return SPEEX_RESAMPLER_QUALITY_VOIP; + case CUBEB_RESAMPLER_QUALITY_DEFAULT: + return SPEEX_RESAMPLER_QUALITY_DEFAULT; + case CUBEB_RESAMPLER_QUALITY_DESKTOP: + return SPEEX_RESAMPLER_QUALITY_DESKTOP; + default: + assert(false); + return 0XFFFFFFFF; + } +} + +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) || + (output_buffer && !input_buffer && + (!input_frames_count || *input_frames_count == 0)) || + (input_buffer && !output_buffer && output_frames == 0)); + + // When we have no pending input data and exactly as much input + // as output data, we don't need to copy it into the internal buffer + // and can directly forward it to the callback. + void * in_buf = input_buffer; + unsigned long pop_input_count = 0u; + if (input_buffer && !output_buffer) { + output_frames = *input_frames_count; + } else if (input_buffer) { + if (internal_input_buffer.length() != 0 || + *input_frames_count < output_frames) { + // If we have pending input data left and have to first append the input + // so we can pass it as one pointer to the callback. Or this is a glitch. + // It can happen when system's performance is poor. Audible silence is + // being pushed at the end of the short input buffer. An improvement for + // the future is to resample to the output number of frames, when that + // happens. + internal_input_buffer.push(static_cast<T *>(input_buffer), + frames_to_samples(*input_frames_count)); + if (internal_input_buffer.length() < frames_to_samples(output_frames)) { + // This is unxpected but it can happen when a glitch occurs. Fill the + // buffer with silence. First keep the actual number of input samples + // used without the silence. + pop_input_count = internal_input_buffer.length(); + internal_input_buffer.push_silence(frames_to_samples(output_frames) - + internal_input_buffer.length()); + } else { + pop_input_count = frames_to_samples(output_frames); + } + in_buf = internal_input_buffer.data(); + } else if (*input_frames_count > output_frames) { + // In this case we have more input that we need output and + // fill the overflowing input into internal_input_buffer + // Since we have no other pending data, we can nonetheless + // pass the current input data directly to the callback + assert(pop_input_count == 0); + unsigned long samples_off = frames_to_samples(output_frames); + internal_input_buffer.push( + static_cast<T *>(input_buffer) + samples_off, + frames_to_samples(*input_frames_count - output_frames)); + } + } + + long rv = + data_callback(stream, user_ptr, in_buf, output_buffer, output_frames); + + if (input_buffer) { + if (pop_input_count) { + internal_input_buffer.pop(nullptr, pop_input_count); + *input_frames_count = samples_to_frames(pop_input_count); + } else { + *input_frames_count = output_frames; + } + drop_audio_if_needed(); + } + + return rv; +} + +// Explicit instantiation of template class. +template class passthrough_resampler<float>; +template class passthrough_resampler<short>; + +template <typename T, typename InputProcessor, typename OutputProcessor> +cubeb_resampler_speex<T, InputProcessor, OutputProcessor>:: + cubeb_resampler_speex(InputProcessor * input_processor, + OutputProcessor * output_processor, cubeb_stream * s, + cubeb_data_callback cb, void * ptr) + : input_processor(input_processor), output_processor(output_processor), + stream(s), data_callback(cb), user_ptr(ptr) +{ + if (input_processor && output_processor) { + fill_internal = &cubeb_resampler_speex::fill_internal_duplex; + } else if (input_processor) { + fill_internal = &cubeb_resampler_speex::fill_internal_input; + } else if (output_processor) { + fill_internal = &cubeb_resampler_speex::fill_internal_output; + } +} + +template <typename T, typename InputProcessor, typename OutputProcessor> +cubeb_resampler_speex<T, InputProcessor, + OutputProcessor>::~cubeb_resampler_speex() +{ +} + +template <typename T, typename InputProcessor, typename OutputProcessor> +long +cubeb_resampler_speex<T, InputProcessor, OutputProcessor>::fill( + void * input_buffer, long * input_frames_count, void * output_buffer, + long output_frames_needed) +{ + /* Input and output buffers, typed */ + T * in_buffer = reinterpret_cast<T *>(input_buffer); + T * out_buffer = reinterpret_cast<T *>(output_buffer); + return (this->*fill_internal)(in_buffer, input_frames_count, out_buffer, + output_frames_needed); +} + +template <typename T, typename InputProcessor, typename OutputProcessor> +long +cubeb_resampler_speex<T, InputProcessor, OutputProcessor>::fill_internal_output( + T * input_buffer, long * input_frames_count, T * output_buffer, + long output_frames_needed) +{ + assert(!input_buffer && (!input_frames_count || *input_frames_count == 0) && + output_buffer && output_frames_needed); + + 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); + + out_unprocessed = + output_processor->input_buffer(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; + } + } + + output_processor->written(got); + } + + /* Process the output. If not enough frames have been returned from the + * callback, drain the processors. */ + return output_processor->output(output_buffer, output_frames_needed); +} + +template <typename T, typename InputProcessor, typename OutputProcessor> +long +cubeb_resampler_speex<T, InputProcessor, OutputProcessor>::fill_internal_input( + T * input_buffer, long * input_frames_count, T * output_buffer, + long /*output_frames_needed*/) +{ + assert(input_buffer && input_frames_count && *input_frames_count && + !output_buffer); + + /* The input data, after eventual resampling. This is passed to the callback. + */ + T * resampled_input = nullptr; + uint32_t resampled_frame_count = + input_processor->output_for_input(*input_frames_count); + + /* process the input, and present exactly `output_frames_needed` in the + * callback. */ + input_processor->input(input_buffer, *input_frames_count); + + /* resampled_frame_count == 0 happens if the resampler + * doesn't have enough input frames buffered to produce 1 resampled frame. */ + if (resampled_frame_count == 0) { + return *input_frames_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); + + /* Return the number of initial input frames or part of it. + * Since output_frames_needed == 0 in input scenario, the only + * available number outside resampler is the initial number of frames. */ + 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; + long output_frames_before_processing = 0; + /* The number of frames returned from the callback. */ + long got = 0; + + /* We need to determine how much frames to present to the consumer. + * - If we have a two way stream, but we're only resampling input, we resample + * the input to the number of output frames. + * - If we have a two way stream, but we're only resampling the output, we + * resize the input buffer of the output resampler to the number of input + * frames, and we resample it afterwards. + * - If we resample both ways, we resample the input to the number of frames + * we would need to pass down to the consumer (before resampling the output), + * get the output data, and resample it to the number of frames needed by the + * caller. */ + + 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 */ + out_unprocessed = + output_processor->input_buffer(output_frames_before_processing); + + if (in_buffer) { + /* 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, + &frames_resampled); + *input_frames_count = frames_resampled; + } else { + resampled_input = nullptr; + } + + got = data_callback(stream, user_ptr, resampled_input, out_unprocessed, + output_frames_before_processing); + + 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. */ + got = output_processor->output(out_buffer, output_frames_needed); + + output_processor->drop_audio_if_needed(); + + return got; +} + +/* Resampler C API */ + +cubeb_resampler * +cubeb_resampler_create(cubeb_stream * stream, + cubeb_stream_params * input_params, + cubeb_stream_params * output_params, + unsigned int target_rate, cubeb_data_callback callback, + void * user_ptr, cubeb_resampler_quality quality, + cubeb_resampler_reclock reclock) +{ + cubeb_sample_format format; + + assert(input_params || output_params); + + if (input_params) { + format = input_params->format; + } else { + format = output_params->format; + } + + switch (format) { + case CUBEB_SAMPLE_S16NE: + return cubeb_resampler_create_internal<short>( + stream, input_params, output_params, target_rate, callback, user_ptr, + quality, reclock); + case CUBEB_SAMPLE_FLOAT32NE: + return cubeb_resampler_create_internal<float>( + stream, input_params, output_params, target_rate, callback, user_ptr, + quality, reclock); + default: + assert(false); + return nullptr; + } +} + +long +cubeb_resampler_fill(cubeb_resampler * resampler, void * input_buffer, + long * input_frames_count, void * output_buffer, + long output_frames_needed) +{ + return resampler->fill(input_buffer, input_frames_count, output_buffer, + output_frames_needed); +} + +void +cubeb_resampler_destroy(cubeb_resampler * resampler) +{ + delete resampler; +} + +long +cubeb_resampler_latency(cubeb_resampler * resampler) +{ + return resampler->latency(); +} diff --git a/media/libcubeb/src/cubeb_resampler.h b/media/libcubeb/src/cubeb_resampler.h new file mode 100644 index 0000000000..711a3771d4 --- /dev/null +++ b/media/libcubeb/src/cubeb_resampler.h @@ -0,0 +1,91 @@ +/* + * Copyright © 2014 Mozilla Foundation + * + * This program is made available under an ISC-style license. See the + * accompanying file LICENSE for details. + */ +#ifndef CUBEB_RESAMPLER_H +#define CUBEB_RESAMPLER_H + +#include "cubeb/cubeb.h" + +#if defined(__cplusplus) +extern "C" { +#endif + +typedef struct cubeb_resampler cubeb_resampler; + +typedef enum { + CUBEB_RESAMPLER_QUALITY_VOIP, + CUBEB_RESAMPLER_QUALITY_DEFAULT, + CUBEB_RESAMPLER_QUALITY_DESKTOP +} cubeb_resampler_quality; + +typedef enum { + CUBEB_RESAMPLER_RECLOCK_NONE, + CUBEB_RESAMPLER_RECLOCK_INPUT +} cubeb_resampler_reclock; + +/** + * 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 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. + * @retval A non-null pointer if success. + */ +cubeb_resampler * +cubeb_resampler_create(cubeb_stream * stream, + cubeb_stream_params * input_params, + cubeb_stream_params * output_params, + unsigned int target_rate, cubeb_data_callback callback, + void * user_ptr, cubeb_resampler_quality quality, + cubeb_resampler_reclock reclock); + +/** + * Fill the buffer with frames acquired using the data callback. Resampling will + * happen if necessary. + * @param resampler A cubeb_resampler instance. + * @param input_buffer A buffer of input samples + * @param input_frame_count The size of the buffer. Returns the number of frames + * consumed. + * @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. + */ +long +cubeb_resampler_fill(cubeb_resampler * resampler, void * input_buffer, + long * input_frame_count, void * output_buffer, + long output_frames_needed); + +/** + * Destroy a cubeb_resampler. + * @param resampler A cubeb_resampler instance. + */ +void +cubeb_resampler_destroy(cubeb_resampler * resampler); + +/** + * Returns the latency, in frames, of the resampler. + * @param resampler A cubeb resampler instance. + * @retval The latency, in frames, induced by the resampler. + */ +long +cubeb_resampler_latency(cubeb_resampler * resampler); + +#if defined(__cplusplus) +} +#endif + +#endif /* CUBEB_RESAMPLER_H */ diff --git a/media/libcubeb/src/cubeb_resampler_internal.h b/media/libcubeb/src/cubeb_resampler_internal.h new file mode 100644 index 0000000000..285f24dd0b --- /dev/null +++ b/media/libcubeb/src/cubeb_resampler_internal.h @@ -0,0 +1,591 @@ +/* + * Copyright © 2016 Mozilla Foundation + * + * This program is made available under an ISC-style license. See the + * accompanying file LICENSE for details. + */ + +#if !defined(CUBEB_RESAMPLER_INTERNAL) +#define CUBEB_RESAMPLER_INTERNAL + +#include <algorithm> +#include <cassert> +#include <cmath> +#include <memory> +#ifdef CUBEB_GECKO_BUILD +#include "mozilla/UniquePtr.h" +// In libc++, symbols such as std::unique_ptr may be defined in std::__1. +// The _LIBCPP_BEGIN_NAMESPACE_STD and _LIBCPP_END_NAMESPACE_STD macros +// will expand to the correct namespace. +#ifdef _LIBCPP_BEGIN_NAMESPACE_STD +#define MOZ_BEGIN_STD_NAMESPACE _LIBCPP_BEGIN_NAMESPACE_STD +#define MOZ_END_STD_NAMESPACE _LIBCPP_END_NAMESPACE_STD +#else +#define MOZ_BEGIN_STD_NAMESPACE namespace std { +#define MOZ_END_STD_NAMESPACE } +#endif +MOZ_BEGIN_STD_NAMESPACE +using mozilla::DefaultDelete; +using mozilla::UniquePtr; +#define default_delete DefaultDelete +#define unique_ptr UniquePtr +MOZ_END_STD_NAMESPACE +#endif +#include "cubeb-speex-resampler.h" +#include "cubeb/cubeb.h" +#include "cubeb_log.h" +#include "cubeb_resampler.h" +#include "cubeb_utils.h" +#include <stdio.h> + +/* 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 { + virtual long fill(void * input_buffer, long * input_frames_count, + void * output_buffer, long frames_needed) = 0; + virtual long latency() = 0; + virtual ~cubeb_resampler() {} +}; + +/** Base class for processors. This is just used to share methods for now. */ +class processor { +public: + explicit processor(uint32_t channels) : channels(channels) {} + +protected: + size_t frames_to_samples(size_t frames) const { return frames * channels; } + size_t samples_to_frames(size_t samples) const + { + assert(!(samples % channels)); + return samples / channels; + } + /** The number of channel of the audio buffers to be resampled. */ + 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) { + ALOGV("Dropping %u frames", 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. */ +template <typename T, typename InputProcessing, typename OutputProcessing> +class cubeb_resampler_speex : public cubeb_resampler { +public: + cubeb_resampler_speex(InputProcessing * input_processor, + OutputProcessing * output_processor, cubeb_stream * s, + cubeb_data_callback cb, void * ptr); + + virtual ~cubeb_resampler_speex(); + + virtual long fill(void * input_buffer, long * input_frames_count, + void * output_buffer, long output_frames_needed); + + virtual long latency() + { + if (input_processor && output_processor) { + assert(input_processor->latency() == output_processor->latency()); + return input_processor->latency(); + } else if (input_processor) { + return input_processor->latency(); + } else { + return output_processor->latency(); + } + } + +private: + typedef long (cubeb_resampler_speex::*processing_callback)( + T * input_buffer, long * input_frames_count, T * output_buffer, + long output_frames_needed); + + long fill_internal_duplex(T * input_buffer, long * input_frames_count, + T * output_buffer, long output_frames_needed); + long fill_internal_input(T * input_buffer, long * input_frames_count, + T * output_buffer, long output_frames_needed); + long fill_internal_output(T * input_buffer, long * input_frames_count, + T * output_buffer, long output_frames_needed); + + std::unique_ptr<InputProcessing> input_processor; + std::unique_ptr<OutputProcessing> output_processor; + processing_callback fill_internal; + 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 + * audio buffers of type T. This class is designed so that the number of frames + * coming out of the resampler can be precisely controled. It manages its own + * input buffer, and can use the caller's output buffer, or allocate its own. */ +template <typename T> class cubeb_resampler_speex_one_way : public processor { +public: + /** The sample type of this resampler, either 16-bit integers or 32-bit + * floats. */ + typedef T sample_type; + /** Construct a resampler resampling from #source_rate to #target_rate, that + * can be arbitrary, strictly positive number. + * @parameter channels The number of channels this resampler will resample. + * @parameter source_rate The sample-rate of the audio input. + * @parameter target_rate The sample-rate of the audio output. + * @parameter quality A number between 0 (fast, low quality) and 10 (slow, + * high quality). */ + cubeb_resampler_speex_one_way(uint32_t channels, uint32_t source_rate, + uint32_t target_rate, int quality) + : processor(channels), + resampling_ratio(static_cast<float>(source_rate) / target_rate), + source_rate(source_rate), additional_latency(0), leftover_samples(0) + { + int r; + speex_resampler = + speex_resampler_init(channels, source_rate, target_rate, quality, &r); + assert(r == RESAMPLER_ERR_SUCCESS && "resampler allocation failure"); + + uint32_t input_latency = speex_resampler_get_input_latency(speex_resampler); + const size_t LATENCY_SAMPLES = 8192; + T input_buffer[LATENCY_SAMPLES] = {}; + T output_buffer[LATENCY_SAMPLES] = {}; + uint32_t input_frame_count = input_latency; + uint32_t output_frame_count = LATENCY_SAMPLES; + assert(input_latency * channels <= LATENCY_SAMPLES); + speex_resample(input_buffer, &input_frame_count, output_buffer, + &output_frame_count); + } + + /** Destructor, deallocate the resampler */ + virtual ~cubeb_resampler_speex_one_way() + { + speex_resampler_destroy(speex_resampler); + } + + /* Fill the resampler with `input_frame_count` frames. */ + void input(T * input_buffer, size_t input_frame_count) + { + resampling_in_buffer.push(input_buffer, + frames_to_samples(input_frame_count)); + } + + /** Outputs exactly `output_frame_count` into `output_buffer`. + * `output_buffer` has to be at least `output_frame_count` long. */ + size_t output(T * output_buffer, size_t output_frame_count) + { + uint32_t in_len = samples_to_frames(resampling_in_buffer.length()); + uint32_t out_len = output_frame_count; + + speex_resample(resampling_in_buffer.data(), &in_len, output_buffer, + &out_len); + + /* This shifts back any unresampled samples to the beginning of the input + buffer. */ + resampling_in_buffer.pop(nullptr, frames_to_samples(in_len)); + + return out_len; + } + + size_t output_for_input(uint32_t input_frames) + { + return (size_t)floorf( + (input_frames + samples_to_frames(resampling_in_buffer.length())) / + resampling_ratio); + } + + /** 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, 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)); + } + + uint32_t in_len = samples_to_frames(resampling_in_buffer.length()); + uint32_t out_len = output_frame_count; + + speex_resample(resampling_in_buffer.data(), &in_len, + resampling_out_buffer.data(), &out_len); + + if (out_len < output_frame_count) { + LOGV("underrun during resampling: got %u frames, expected %zu", + (unsigned)out_len, output_frame_count); + // silence the rightmost part + T * data = resampling_out_buffer.data(); + for (uint32_t i = frames_to_samples(out_len); + i < frames_to_samples(output_frame_count); i++) { + data[i] = 0; + } + } + + /* 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(); + } + + /** Get the latency of the resampler, in output frames. */ + uint32_t latency() const + { + /* The documentation of the resampler talks about "samples" here, but it + * only consider a single channel here so it's the same number of frames. */ + int latency = 0; + + latency = speex_resampler_get_output_latency(speex_resampler) + + additional_latency; + + assert(latency >= 0); + + return latency; + } + + /** Returns the number of frames to pass in the input of the resampler to have + * 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(int32_t output_frame_count) const + { + assert(output_frame_count >= 0); // Check overflow + int32_t unresampled_frames_left = + samples_to_frames(resampling_in_buffer.length()); + int32_t resampled_frames_left = + samples_to_frames(resampling_out_buffer.length()); + float input_frames_needed = + (output_frame_count - unresampled_frames_left) * resampling_ratio - + resampled_frames_left; + if (input_frames_needed < 0) { + return 0; + } + return (uint32_t)ceilf(input_frames_needed); + } + + /** Returns a pointer to the input buffer, that contains empty space for at + * least `frame_count` elements. This is useful so that consumer can directly + * write into the input buffer of the resampler. The pointer returned is + * adjusted so that leftover data are not overwritten. + */ + T * input_buffer(size_t frame_count) + { + leftover_samples = resampling_in_buffer.length(); + resampling_in_buffer.reserve(leftover_samples + + frames_to_samples(frame_count)); + return resampling_in_buffer.data() + leftover_samples; + } + + /** This method works with `input_buffer`, and allows to inform the processor + how much frames have been written in the provided buffer. */ + void written(size_t written_frames) + { + 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) { + ALOGV("Dropping %u frames", 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. */ + void speex_resample(float * input_buffer, uint32_t * input_frame_count, + float * output_buffer, uint32_t * output_frame_count) + { +#ifndef NDEBUG + int rv; + rv = +#endif + speex_resampler_process_interleaved_float( + speex_resampler, input_buffer, input_frame_count, output_buffer, + output_frame_count); + assert(rv == RESAMPLER_ERR_SUCCESS); + } + + void speex_resample(short * input_buffer, uint32_t * input_frame_count, + short * output_buffer, uint32_t * output_frame_count) + { +#ifndef NDEBUG + int rv; + rv = +#endif + speex_resampler_process_interleaved_int( + speex_resampler, input_buffer, input_frame_count, output_buffer, + output_frame_count); + assert(rv == RESAMPLER_ERR_SUCCESS); + } + /** The state for the speex resampler used internaly. */ + 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; + /* Storage for the resampled frames, to be passed back to the caller. */ + auto_array<T> resampling_out_buffer; + /** Additional latency inserted into the pipeline for synchronisation. */ + uint32_t additional_latency; + /** When `input_buffer` is called, this allows tracking the number of samples + that were in the buffer. */ + uint32_t leftover_samples; +}; + +/** This class allows delaying an audio stream by `frames` frames. */ +template <typename T> 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. + * @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); + } + /** Push some frames into the delay line. + * @parameter buffer the frames to push. + * @parameter frame_count the number of frames in #buffer. */ + void input(T * buffer, uint32_t frame_count) + { + delay_input_buffer.push(buffer, frames_to_samples(frame_count)); + } + /** Pop some frames from the internal buffer, into a internal output buffer. + * @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, size_t * input_frames_used) + { + if (delay_output_buffer.capacity() < frames_to_samples(frames_needed)) { + delay_output_buffer.reserve(frames_to_samples(frames_needed)); + } + + delay_output_buffer.clear(); + 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(); + } + /** Get a pointer to the first writable location in the input buffer> + * @parameter frames_needed the number of frames the user needs to write into + * the buffer. + * @returns a pointer to a location in the input buffer where #frames_needed + * can be writen. */ + T * input_buffer(uint32_t frames_needed) + { + leftover_samples = delay_input_buffer.length(); + delay_input_buffer.reserve(leftover_samples + + frames_to_samples(frames_needed)); + return delay_input_buffer.data() + leftover_samples; + } + /** This method works with `input_buffer`, and allows to inform the processor + how much frames have been written in the provided buffer. */ + void written(size_t frames_written) + { + delay_input_buffer.set_length(leftover_samples + + frames_to_samples(frames_written)); + } + /** Drains the delay line, emptying the buffer. + * @parameter output_buffer the buffer in which the frames are written. + * @parameter frames_needed the maximum number of frames to write. + * @return the actual number of frames written. */ + size_t output(T * output_buffer, uint32_t frames_needed) + { + uint32_t in_len = samples_to_frames(delay_input_buffer.length()); + uint32_t out_len = frames_needed; + + uint32_t to_pop = std::min(in_len, out_len); + + delay_input_buffer.pop(output_buffer, frames_to_samples(to_pop)); + + return to_pop; + } + /** Returns the number of frames one needs to input into the delay line to get + * #frames_needed frames back. + * @parameter frames_needed the number of frames one want to write into the + * delay_line + * @returns the number of frames one will get. */ + uint32_t input_needed_for_output(int32_t frames_needed) const + { + assert(frames_needed >= 0); // Check overflow + return frames_needed; + } + /** Returns the number of frames produces for `input_frames` frames in input + */ + size_t output_for_input(uint32_t input_frames) { return input_frames; } + /** The number of frames this delay line delays the stream by. + * @returns The number of frames of delay. */ + size_t latency() { 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) { + ALOGV("Dropping %u frames", 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; + /** When `input_buffer` is called, this allows tracking the number of samples + that where in the buffer. */ + uint32_t leftover_samples; + /** The input buffer, where the delay is applied. */ + auto_array<T> delay_input_buffer; + /** 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. */ +template <typename T> +cubeb_resampler * +cubeb_resampler_create_internal(cubeb_stream * stream, + cubeb_stream_params * input_params, + cubeb_stream_params * output_params, + unsigned int target_rate, + cubeb_data_callback callback, void * user_ptr, + cubeb_resampler_quality quality, + cubeb_resampler_reclock reclock) +{ + std::unique_ptr<cubeb_resampler_speex_one_way<T>> input_resampler = nullptr; + std::unique_ptr<cubeb_resampler_speex_one_way<T>> output_resampler = nullptr; + std::unique_ptr<delay_line<T>> input_delay = nullptr; + std::unique_ptr<delay_line<T>> output_delay = nullptr; + + assert((input_params || output_params) && + "need at least one valid parameter pointer."); + + /* All the streams we have have a sample rate that matches the target + sample rate, use a no-op resampler, that simply forwards the buffers to the + callback. */ + if (((input_params && input_params->rate == target_rate) && + (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))) { + LOG("Input and output sample-rate match, target rate of %dHz", target_rate); + 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 + resamplers. */ + if (output_params && (output_params->rate != target_rate)) { + output_resampler.reset(new cubeb_resampler_speex_one_way<T>( + output_params->channels, target_rate, output_params->rate, + to_speex_quality(quality))); + if (!output_resampler) { + return NULL; + } + } + + if (input_params && (input_params->rate != target_rate)) { + input_resampler.reset(new cubeb_resampler_speex_one_way<T>( + input_params->channels, input_params->rate, target_rate, + to_speex_quality(quality))); + if (!input_resampler) { + return NULL; + } + } + + /* If we resample only one direction but we have a duplex stream, insert a + * delay line with a length equal to the resampler latency of the + * 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->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, + output_params->rate)); + if (!input_delay) { + return NULL; + } + } + + if (input_resampler && output_resampler) { + LOG("Resampling input (%d) and output (%d) to target rate of %dHz", + input_params->rate, output_params->rate, target_rate); + return new cubeb_resampler_speex<T, cubeb_resampler_speex_one_way<T>, + cubeb_resampler_speex_one_way<T>>( + input_resampler.release(), output_resampler.release(), stream, callback, + user_ptr); + } else if (input_resampler) { + LOG("Resampling input (%d) to target and output rate of %dHz", + input_params->rate, target_rate); + return new cubeb_resampler_speex<T, cubeb_resampler_speex_one_way<T>, + delay_line<T>>(input_resampler.release(), + output_delay.release(), + stream, callback, user_ptr); + } else { + LOG("Resampling output (%dHz) to target and input rate of %dHz", + output_params->rate, target_rate); + return new cubeb_resampler_speex<T, delay_line<T>, + cubeb_resampler_speex_one_way<T>>( + input_delay.release(), output_resampler.release(), stream, callback, + user_ptr); + } +} + +#endif /* CUBEB_RESAMPLER_INTERNAL */ diff --git a/media/libcubeb/src/cubeb_ring_array.h b/media/libcubeb/src/cubeb_ring_array.h new file mode 100644 index 0000000000..331d0471c5 --- /dev/null +++ b/media/libcubeb/src/cubeb_ring_array.h @@ -0,0 +1,142 @@ +/* + * Copyright © 2016 Mozilla Foundation + * + * This program is made available under an ISC-style license. See the + * accompanying file LICENSE for details. + */ + +#ifndef CUBEB_RING_ARRAY_H +#define CUBEB_RING_ARRAY_H + +#include "cubeb_utils.h" +#include <CoreAudio/CoreAudioTypes.h> + +/** Ring array of pointers is used to hold buffers. In case that + asynchronous producer/consumer callbacks do not arrive in a + repeated order the ring array stores the buffers and fetch + them in the correct order. */ + +typedef struct { + AudioBuffer * buffer_array; /**< Array that hold pointers of the allocated + space for the buffers. */ + unsigned int tail; /**< Index of the last element (first to deliver). */ + unsigned int count; /**< Number of elements in the array. */ + unsigned int capacity; /**< Total length of the array. */ +} ring_array; + +static int +single_audiobuffer_init(AudioBuffer * buffer, uint32_t bytesPerFrame, + uint32_t channelsPerFrame, uint32_t frames) +{ + assert(buffer); + assert(bytesPerFrame > 0 && channelsPerFrame && frames > 0); + + size_t size = bytesPerFrame * frames; + buffer->mData = operator new(size); + if (buffer->mData == NULL) { + return CUBEB_ERROR; + } + PodZero(static_cast<char *>(buffer->mData), size); + + buffer->mNumberChannels = channelsPerFrame; + buffer->mDataByteSize = size; + + return CUBEB_OK; +} + +/** Initialize the ring array. + @param ra The ring_array pointer of allocated structure. + @retval 0 on success. */ +static int +ring_array_init(ring_array * ra, uint32_t capacity, uint32_t bytesPerFrame, + uint32_t channelsPerFrame, uint32_t framesPerBuffer) +{ + assert(ra); + if (capacity == 0 || bytesPerFrame == 0 || channelsPerFrame == 0 || + framesPerBuffer == 0) { + return CUBEB_ERROR_INVALID_PARAMETER; + } + ra->capacity = capacity; + ra->tail = 0; + ra->count = 0; + + ra->buffer_array = new AudioBuffer[ra->capacity]; + PodZero(ra->buffer_array, ra->capacity); + if (ra->buffer_array == NULL) { + return CUBEB_ERROR; + } + + for (unsigned int i = 0; i < ra->capacity; ++i) { + if (single_audiobuffer_init(&ra->buffer_array[i], bytesPerFrame, + channelsPerFrame, + framesPerBuffer) != CUBEB_OK) { + return CUBEB_ERROR; + } + } + + return CUBEB_OK; +} + +/** Destroy the ring array. + @param ra The ring_array pointer.*/ +static void +ring_array_destroy(ring_array * ra) +{ + assert(ra); + if (ra->buffer_array == NULL) { + return; + } + for (unsigned int i = 0; i < ra->capacity; ++i) { + if (ra->buffer_array[i].mData) { + operator delete(ra->buffer_array[i].mData); + } + } + delete[] ra->buffer_array; +} + +/** Get the allocated buffer to be stored with fresh data. + @param ra The ring_array pointer. + @retval Pointer of the allocated space to be stored with fresh data or NULL + if full. */ +static AudioBuffer * +ring_array_get_free_buffer(ring_array * ra) +{ + assert(ra && ra->buffer_array); + assert(ra->buffer_array[0].mData != NULL); + if (ra->count == ra->capacity) { + return NULL; + } + + assert(ra->count == 0 || (ra->tail + ra->count) % ra->capacity != ra->tail); + AudioBuffer * ret = &ra->buffer_array[(ra->tail + ra->count) % ra->capacity]; + + ++ra->count; + assert(ra->count <= ra->capacity); + + return ret; +} + +/** Get the next available buffer with data. + @param ra The ring_array pointer. + @retval Pointer of the next in order data buffer or NULL if empty. */ +static AudioBuffer * +ring_array_get_data_buffer(ring_array * ra) +{ + assert(ra && ra->buffer_array); + assert(ra->buffer_array[0].mData != NULL); + + if (ra->count == 0) { + return NULL; + } + AudioBuffer * ret = &ra->buffer_array[ra->tail]; + + ra->tail = (ra->tail + 1) % ra->capacity; + assert(ra->tail < ra->capacity); + + assert(ra->count > 0); + --ra->count; + + return ret; +} + +#endif // CUBEB_RING_ARRAY_H diff --git a/media/libcubeb/src/cubeb_ringbuffer.h b/media/libcubeb/src/cubeb_ringbuffer.h new file mode 100644 index 0000000000..f020351566 --- /dev/null +++ b/media/libcubeb/src/cubeb_ringbuffer.h @@ -0,0 +1,468 @@ +/* + * 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 wr_idx = write_index_.load(std::memory_order_relaxed); + int rd_idx = read_index_.load(std::memory_order_acquire); + + 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_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 rd_idx = read_index_.load(std::memory_order_relaxed); + int wr_idx = write_index_.load(std::memory_order_acquire); + + 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_release); + + 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_relaxed), + write_index_.load(std::memory_order_acquire)); + } + /** + * 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_acquire), + write_index_.load(std::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 new file mode 100644 index 0000000000..cd295bff56 --- /dev/null +++ b/media/libcubeb/src/cubeb_sndio.c @@ -0,0 +1,687 @@ +/* + * Copyright (c) 2011 Alexandre Ratchov <alex@caoua.org> + * + * This program is made available under an ISC-style license. See the + * accompanying file LICENSE for details. + */ +#include "cubeb-internal.h" +#include "cubeb/cubeb.h" +#include "cubeb_tracing.h" +#include <assert.h> +#include <dlfcn.h> +#include <inttypes.h> +#include <math.h> +#include <poll.h> +#include <pthread.h> +#include <sndio.h> +#include <stdbool.h> +#include <stdio.h> +#include <stdlib.h> + +#if defined(CUBEB_SNDIO_DEBUG) +#define DPR(...) fprintf(stderr, __VA_ARGS__); +#else +#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_nfds) \ + 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; + 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->s24 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 */ + float volume; /* current volume */ +}; + +static void +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_s24(void * ptr, long nsamp, float volume) +{ + int32_t * dst = ptr; + float * src = ptr; + float mult = volume * 8388608; + int s; + + while (nsamp-- > 0) { + s = lrintf(*(src++) * mult); + if (s < -8388608) + s = -8388608; + else if (s > 8388607) + s = 8388607; + *(dst++) = s; + } +} + +static void +s24_to_float(void * ptr, long nsamp) +{ + int32_t * src = ptr; + float * dst = ptr; + + src += nsamp; + dst += nsamp; + while (nsamp-- > 0) + *(--dst) = (1. / 8388608) * *(--src); +} + +static const char * +sndio_get_device() +{ +#ifdef __linux__ + /* + * On other platforms default to sndio devices, + * so cubebs other backends can be used instead. + */ + const char * dev = getenv("AUDIODEVICE"); + if (dev == NULL || *dev == '\0') + return "snd/0"; + return dev; +#else + return SIO_DEVANY; +#endif +} + +static void +sndio_onmove(void * arg, int delta) +{ + cubeb_stream * s = (cubeb_stream *)arg; + + s->hwpos += delta; +} + +static void * +sndio_mainloop(void * arg) +{ + struct pollfd * pfds; + cubeb_stream * s = arg; + int n, eof = 0, prime, nfds, events, revents, state = CUBEB_STATE_STARTED; + size_t pstart = 0, pend = 0, rstart = 0, rend = 0; + long nfr; + + CUBEB_REGISTER_THREAD("cubeb rendering thread"); + + nfds = WRAP(sio_nfds)(s->hdl); + pfds = calloc(nfds, sizeof(struct pollfd)); + if (pfds == NULL) { + CUBEB_UNREGISTER_THREAD(); + return NULL; + } + + DPR("sndio_mainloop()\n"); + s->state_cb(s, s->arg, CUBEB_STATE_STARTED); + pthread_mutex_lock(&s->mtx); + if (!WRAP(sio_start)(s->hdl)) { + pthread_mutex_unlock(&s->mtx); + free(pfds); + CUBEB_UNREGISTER_THREAD(); + return NULL; + } + DPR("sndio_mainloop(), started\n"); + + 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; + } + + /* 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) + s24_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, 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; + } + 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_s24(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; + } + + 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); + pthread_mutex_lock(&s->mtx); + if (n < 0) + continue; + } + + revents = WRAP(sio_revents)(s->hdl, pfds); + + if (revents & POLLHUP) { + state = CUBEB_STATE_ERROR; + break; + } + + if (revents & POLLOUT) { + 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; + } + 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; + } + WRAP(sio_stop)(s->hdl); + s->hwpos = s->swpos; + pthread_mutex_unlock(&s->mtx); + s->state_cb(s, s->arg, state); + free(pfds); + CUBEB_UNREGISTER_THREAD(); + return NULL; +} + +/*static*/ int +sndio_init(cubeb ** context, char const * context_name) +{ + void * libsndio = NULL; + struct sio_hdl * hdl; + + assert(context); + +#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 + + /* test if sndio works */ + hdl = WRAP(sio_open)(sndio_get_device(), SIO_PLAY, 1); + if (hdl == NULL) { + return CUBEB_ERROR; + } + WRAP(sio_close)(hdl); + + DPR("sndio_init(%s)\n", context_name); + *context = malloc(sizeof(**context)); + if (*context == NULL) + return CUBEB_ERROR; + (*context)->libsndio = libsndio; + (*context)->ops = &sndio_ops; + (void)context_name; + return CUBEB_OK; +} + +static char const * +sndio_get_backend_id(cubeb * context) +{ + return "sndio"; +} + +static void +sndio_destroy(cubeb * context) +{ + DPR("sndio_destroy()\n"); +#ifndef DISABLE_LIBSNDIO_DLOPEN + if (context->libsndio) + dlclose(context->libsndio); +#endif + free(context); +} + +static int +sndio_stream_init(cubeb * context, cubeb_stream ** stream, + char const * stream_name, cubeb_devid input_device, + cubeb_stream_params * input_stream_params, + cubeb_devid output_device, + cubeb_stream_params * output_stream_params, + unsigned int latency_frames, + cubeb_data_callback data_callback, + cubeb_state_callback state_callback, void * user_ptr) +{ + cubeb_stream * s; + struct sio_par wpar, rpar; + cubeb_sample_format format; + int rate; + size_t bps; + + 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 = WRAP(sio_open)(sndio_get_device(), s->mode, 1); + if (s->hdl == NULL) { + DPR("sndio_stream_init(), sio_open() failed\n"); + goto err; + } + WRAP(sio_initpar)(&wpar); + wpar.sig = 1; + switch (format) { + case CUBEB_SAMPLE_S16LE: + wpar.le = 1; + wpar.bits = 16; + break; + case CUBEB_SAMPLE_S16BE: + wpar.le = 0; + wpar.bits = 16; + break; + case CUBEB_SAMPLE_FLOAT32NE: + wpar.le = SIO_LE_NATIVE; + wpar.bits = 24; + wpar.msb = 0; + break; + default: + DPR("sndio_stream_init() unsupported format\n"); + goto err; + } + wpar.bps = SIO_BPS(wpar.bits); + 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 (!WRAP(sio_setpar)(s->hdl, &wpar) || !WRAP(sio_getpar)(s->hdl, &rpar)) { + DPR("sndio_stream_init(), sio_setpar() failed\n"); + goto err; + } + if (rpar.bits != wpar.bits || rpar.le != wpar.le || rpar.sig != wpar.sig || + rpar.bps != wpar.bps || + (wpar.bits < 8 * wpar.bps && rpar.msb != wpar.msb) || + rpar.rate != wpar.rate || + ((s->mode & SIO_REC) && rpar.rchan != wpar.rchan) || + ((s->mode & SIO_PLAY) && rpar.pchan != wpar.pchan)) { + DPR("sndio_stream_init() unsupported params\n"); + goto err; + } + WRAP(sio_onmove)(s->hdl, sndio_onmove, s); + s->active = 0; + s->nfr = rpar.round; + 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_t)PTHREAD_MUTEX_INITIALIZER; + s->hwpos = s->swpos = 0; + if (format == CUBEB_SAMPLE_FLOAT32LE) { + s->conv = 1; + bps = sizeof(float); + } else { + s->conv = 0; + bps = rpar.bps; + } + 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 +sndio_get_max_channel_count(cubeb * ctx, uint32_t * max_channels) +{ + assert(ctx && max_channels); + + *max_channels = 8; + + return CUBEB_OK; +} + +static int +sndio_get_preferred_sample_rate(cubeb * ctx, uint32_t * rate) +{ + /* + * 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) +{ + /* + * We've no device-independent minimum latency. + */ + *latency_frames = 2048; + + return CUBEB_OK; +} + +static void +sndio_stream_destroy(cubeb_stream * s) +{ + DPR("sndio_stream_destroy()\n"); + WRAP(sio_close)(s->hdl); + if (s->mode & SIO_PLAY) + free(s->pbuf); + if (s->mode & SIO_REC) + free(s->rbuf); + free(s); +} + +static int +sndio_stream_start(cubeb_stream * s) +{ + int err; + + DPR("sndio_stream_start()\n"); + s->active = 1; + err = pthread_create(&s->th, NULL, sndio_mainloop, s); + if (err) { + s->active = 0; + return CUBEB_ERROR; + } + return CUBEB_OK; +} + +static int +sndio_stream_stop(cubeb_stream * s) +{ + void * dummy; + + DPR("sndio_stream_stop()\n"); + if (s->active) { + s->active = 0; + pthread_join(s->th, &dummy); + } + return CUBEB_OK; +} + +static int +sndio_stream_get_position(cubeb_stream * s, uint64_t * p) +{ + pthread_mutex_lock(&s->mtx); + DPR("sndio_stream_get_position() %" PRId64 "\n", s->hwpos); + *p = s->hwpos; + pthread_mutex_unlock(&s->mtx); + return CUBEB_OK; +} + +static int +sndio_stream_set_volume(cubeb_stream * s, float volume) +{ + DPR("sndio_stream_set_volume(%f)\n", volume); + pthread_mutex_lock(&s->mtx); + if (volume < 0.) + volume = 0.; + else if (volume > 1.0) + volume = 1.; + s->volume = volume; + pthread_mutex_unlock(&s->mtx); + return CUBEB_OK; +} + +int +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->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 = (type == CUBEB_DEVICE_TYPE_INPUT) ? 2 : 8; + 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; +} + +static struct cubeb_ops const sndio_ops = { + .init = sndio_init, + .get_backend_id = sndio_get_backend_id, + .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, + .get_supported_input_processing_params = 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_get_position = sndio_stream_get_position, + .stream_get_latency = sndio_stream_get_latency, + .stream_set_volume = sndio_stream_set_volume, + .stream_set_name = NULL, + .stream_get_current_device = NULL, + .stream_set_input_mute = NULL, + .stream_set_input_processing_params = NULL, + .stream_device_destroy = NULL, + .stream_register_device_changed_callback = NULL, + .register_device_collection_changed = NULL}; diff --git a/media/libcubeb/src/cubeb_strings.c b/media/libcubeb/src/cubeb_strings.c new file mode 100644 index 0000000000..5fe5b791e2 --- /dev/null +++ b/media/libcubeb/src/cubeb_strings.c @@ -0,0 +1,154 @@ +/* + * 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..cfffbbc68a --- /dev/null +++ b/media/libcubeb/src/cubeb_strings.h @@ -0,0 +1,47 @@ +/* + * 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 new file mode 100644 index 0000000000..cae9eefe20 --- /dev/null +++ b/media/libcubeb/src/cubeb_sun.c @@ -0,0 +1,740 @@ +/* + * Copyright © 2019-2020 Nia Alarie <nia@NetBSD.org> + * + * This program is made available under an ISC-style license. See the + * accompanying file LICENSE for details. + */ +#include "cubeb-internal.h" +#include "cubeb/cubeb.h" +#include "cubeb_tracing.h" +#include <fcntl.h> +#include <limits.h> +#include <pthread.h> +#include <stdbool.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/audioio.h> +#include <sys/ioctl.h> +#include <unistd.h> + +/* 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_BUFFER_FRAMES +#define SUN_BUFFER_FRAMES (32) +#endif + +/* + * Supported on NetBSD regardless of hardware. + */ + +#ifndef SUN_MAX_CHANNELS +#ifdef __NetBSD__ +#define SUN_MAX_CHANNELS (12) +#else +#define SUN_MAX_CHANNELS (2) +#endif +#endif + +#ifndef SUN_MIN_RATE +#define SUN_MIN_RATE (1000) +#endif + +#ifndef SUN_MAX_RATE +#define SUN_MAX_RATE (192000) +#endif + +static struct cubeb_ops const sun_ops; + +struct cubeb { + struct cubeb_ops const * ops; +}; + +struct sun_stream { + char name[32]; + int fd; + void * buf; + struct audio_info info; + unsigned frame_size; /* precision in bytes * channels */ + bool floating; +}; + +struct cubeb_stream { + struct cubeb * context; + void * user_ptr; + pthread_t thread; + pthread_mutex_t mutex; /* protects running, volume, frames_written */ + bool running; + float volume; + struct sun_stream play; + struct sun_stream record; + cubeb_data_callback data_cb; + cubeb_state_callback state_cb; + 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 +sun_destroy(cubeb * context) +{ + free(context); +} + +static char const * +sun_get_backend_id(cubeb * context) +{ + return "sun"; +} + +static int +sun_get_preferred_sample_rate(cubeb * context, uint32_t * rate) +{ + (void)context; + + *rate = SUN_PREFER_RATE; + return CUBEB_OK; +} + +static int +sun_get_max_channel_count(cubeb * context, uint32_t * max_channels) +{ + (void)context; + + *max_channels = SUN_MAX_CHANNELS; + return CUBEB_OK; +} + +static int +sun_get_min_latency(cubeb * context, cubeb_stream_params params, + uint32_t * latency_frames) +{ + (void)context; + + *latency_frames = SUN_LATENCY_MS * params.rate / 1000; + return CUBEB_OK; +} + +static int +sun_get_hwinfo(const char * device, struct audio_info * format, int * props, + struct audio_device * dev) +{ + int fd = -1; + + 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; +} + +/* + * 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; +} + +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; + + for (i = 0; i < SUN_DEVICE_COUNT; ++i) { + if (i > 0) { + (void)snprintf(dev, sizeof(dev), "/dev/audio%u", i - 1); + } + 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; +} + +static int +sun_device_collection_destroy(cubeb * context, + cubeb_device_collection * collection) +{ + unsigned i; + + 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 +sun_copy_params(int fd, cubeb_stream * stream, cubeb_stream_params * params, + struct audio_info * info, struct audio_prinfo * prinfo) +{ + prinfo->channels = params->channels; + prinfo->sample_rate = params->rate; +#ifdef AUDIO_ENCODING_SLINEAR_LE + switch (params->format) { + case CUBEB_SAMPLE_S16LE: + prinfo->encoding = AUDIO_ENCODING_SLINEAR_LE; + prinfo->precision = 16; + break; + case CUBEB_SAMPLE_S16BE: + prinfo->encoding = AUDIO_ENCODING_SLINEAR_BE; + prinfo->precision = 16; + break; + case CUBEB_SAMPLE_FLOAT32NE: + prinfo->encoding = AUDIO_ENCODING_SLINEAR; + prinfo->precision = 32; + break; + default: + LOG("Unsupported format"); + return CUBEB_ERROR_INVALID_FORMAT; + } +#else + switch (params->format) { + case CUBEB_SAMPLE_S16NE: + prinfo->encoding = AUDIO_ENCODING_LINEAR; + prinfo->precision = 16; + break; + case CUBEB_SAMPLE_FLOAT32NE: + prinfo->encoding = AUDIO_ENCODING_LINEAR; + prinfo->precision = 32; + 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 int +sun_stream_stop(cubeb_stream * s) +{ + pthread_mutex_lock(&s->mutex); + if (s->running) { + s->running = false; + pthread_mutex_unlock(&s->mutex); + pthread_join(s->thread, NULL); + } else { + pthread_mutex_unlock(&s->mutex); + } + return CUBEB_OK; +} + +static void +sun_stream_destroy(cubeb_stream * s) +{ + sun_stream_stop(s); + pthread_mutex_destroy(&s->mutex); + if (s->play.fd != -1) { + close(s->play.fd); + } + if (s->record.fd != -1) { + close(s->record.fd); + } + free(s->play.buf); + free(s->record.buf); + free(s); +} + +static void +sun_float_to_linear32(void * buf, unsigned sample_count, float vol) +{ + float * in = buf; + int32_t * out = buf; + int32_t * tail = out + sample_count; + + while (out < tail) { + float f = *(in++) * vol; + if (f < -1.0) + f = -1.0; + else if (f > 1.0) + f = 1.0; + *(out++) = f * (float)INT32_MAX; + } +} + +static void +sun_linear32_to_float(void * buf, unsigned sample_count) +{ + int32_t * in = buf; + float * out = buf; + float * tail = out + sample_count; + + while (out < tail) { + *(out++) = (1.0 / 0x80000000) * *(in++); + } +} + +static void +sun_linear16_set_vol(int16_t * buf, unsigned sample_count, float vol) +{ + unsigned i; + int32_t multiplier = vol * 0x8000; + + for (i = 0; i < sample_count; ++i) { + buf[i] = (buf[i] * multiplier) >> 15; + } +} + +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; + + CUBEB_REGISTER_THREAD("cubeb rendering thread"); + + 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; + } + pthread_mutex_unlock(&s->mutex); + if (s->record.fd != -1 && s->record.floating) { + sun_linear32_to_float(s->record.buf, + s->record.info.record.channels * SUN_BUFFER_FRAMES); + } + 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) { + float vol; + + pthread_mutex_lock(&s->mutex); + vol = s->volume; + pthread_mutex_unlock(&s->mutex); + + if (s->play.floating) { + sun_float_to_linear32(s->play.buf, + s->play.info.play.channels * to_write, vol); + } else { + sun_linear16_set_vol(s->play.buf, s->play.info.play.channels * to_write, + vol); + } + } + if (to_write < SUN_BUFFER_FRAMES) { + drain = 1; + } + 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 = to_write * s->play.frame_size; + if ((n = write(s->play.fd, (uint8_t *)s->play.buf + write_ofs, bytes)) < + 0) { + state = CUBEB_STATE_ERROR; + break; + } + frames = n / s->play.frame_size; + pthread_mutex_lock(&s->mutex); + s->frames_written += frames; + pthread_mutex_unlock(&s->mutex); + to_write -= frames; + write_ofs += n; + } + if (to_read > 0) { + bytes = to_read * s->record.frame_size; + if ((n = read(s->record.fd, (uint8_t *)s->record.buf + read_ofs, + bytes)) < 0) { + state = CUBEB_STATE_ERROR; + break; + } + frames = n / s->record.frame_size; + to_read -= frames; + read_ofs += n; + } + } + if (drain && state != CUBEB_STATE_ERROR) { + state = CUBEB_STATE_DRAINED; + break; + } + } + s->state_cb(s, s->user_ptr, state); + CUBEB_UNREGISTER_THREAD(); + return NULL; +} + +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->record.name, sizeof(s->record.name), "/dev/audio%zu", + (uintptr_t)input_device - 1); + } else { + snprintf(s->record.name, sizeof(s->record.name), "%s", SUN_DEFAULT_DEVICE); + } + if (output_device != 0) { + snprintf(s->play.name, sizeof(s->play.name), "/dev/audio%zu", + (uintptr_t)output_device - 1); + } else { + snprintf(s->play.name, sizeof(s->play.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->record.name, O_RDONLY)) == -1) { + LOG("Audio device could not be opened as read-only"); + ret = CUBEB_ERROR_DEVICE_UNAVAILABLE; + goto error; + } + } + AUDIO_INITINFO(&s->record.info); +#ifdef AUMODE_RECORD + s->record.info.mode = AUMODE_RECORD; +#endif + if ((ret = sun_copy_params(s->record.fd, s, input_stream_params, + &s->record.info, &s->record.info.record)) != + CUBEB_OK) { + LOG("Setting record params failed"); + goto error; + } + s->record.floating = + (input_stream_params->format == CUBEB_SAMPLE_FLOAT32NE); + } + 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->play.name, O_WRONLY)) == -1) { + LOG("Audio device could not be opened as write-only"); + ret = CUBEB_ERROR_DEVICE_UNAVAILABLE; + goto error; + } + } + AUDIO_INITINFO(&s->play.info); +#ifdef AUMODE_PLAY + s->play.info.mode = AUMODE_PLAY; +#endif + if ((ret = sun_copy_params(s->play.fd, s, output_stream_params, + &s->play.info, &s->play.info.play)) != + CUBEB_OK) { + LOG("Setting play params failed"); + goto error; + } + s->play.floating = (output_stream_params->format == CUBEB_SAMPLE_FLOAT32NE); + } + s->context = context; + s->volume = 1.0; + s->state_cb = state_callback; + s->data_cb = data_callback; + s->user_ptr = user_ptr; + if (pthread_mutex_init(&s->mutex, NULL) != 0) { + LOG("Failed to create mutex"); + goto error; + } + s->play.frame_size = + s->play.info.play.channels * (s->play.info.play.precision / 8); + if (s->play.fd != -1 && + (s->play.buf = calloc(SUN_BUFFER_FRAMES, s->play.frame_size)) == NULL) { + ret = CUBEB_ERROR; + goto error; + } + s->record.frame_size = + s->record.info.record.channels * (s->record.info.record.precision / 8); + if (s->record.fd != -1 && + (s->record.buf = calloc(SUN_BUFFER_FRAMES, s->record.frame_size)) == + NULL) { + ret = CUBEB_ERROR; + goto error; + } + *stream = s; + return CUBEB_OK; +error: + if (s != NULL) { + sun_stream_destroy(s); + } + return ret; +} + +static int +sun_stream_start(cubeb_stream * s) +{ + s->running = true; + if (pthread_create(&s->thread, NULL, sun_io_routine, s) != 0) { + LOG("Couldn't create thread"); + return CUBEB_ERROR; + } + return CUBEB_OK; +} + +static int +sun_stream_get_position(cubeb_stream * s, uint64_t * position) +{ +#ifdef AUDIO_GETOOFFS + struct audio_offset offset; + + if (ioctl(s->play.fd, AUDIO_GETOOFFS, &offset) == -1) { + return CUBEB_ERROR; + } + s->blocks_written += offset.deltablks; + *position = (s->blocks_written * s->play.info.blocksize) / s->play.frame_size; + return CUBEB_OK; +#else + pthread_mutex_lock(&s->mutex); + *position = s->frames_written; + pthread_mutex_unlock(&s->mutex); + return CUBEB_OK; +#endif +} + +static int +sun_stream_get_latency(cubeb_stream * s, uint32_t * latency) +{ +#ifdef AUDIO_GETBUFINFO + struct audio_info info; + + if (ioctl(s->play.fd, AUDIO_GETBUFINFO, &info) == -1) { + return CUBEB_ERROR; + } + + *latency = (info.play.seek + info.blocksize) / s->play.frame_size; + return CUBEB_OK; +#else + cubeb_stream_params params; + + params.rate = s->play.info.play.sample_rate; + + return sun_get_min_latency(NULL, params, latency); +#endif +} + +static int +sun_stream_set_volume(cubeb_stream * stream, float volume) +{ + pthread_mutex_lock(&stream->mutex); + stream->volume = volume; + pthread_mutex_unlock(&stream->mutex); + return CUBEB_OK; +} + +static int +sun_get_current_device(cubeb_stream * stream, cubeb_device ** const device) +{ + *device = calloc(1, sizeof(cubeb_device)); + if (*device == NULL) { + return CUBEB_ERROR; + } + (*device)->input_name = + stream->record.fd != -1 ? strdup(stream->record.name) : NULL; + (*device)->output_name = + stream->play.fd != -1 ? strdup(stream->play.name) : NULL; + return CUBEB_OK; +} + +static int +sun_stream_device_destroy(cubeb_stream * stream, cubeb_device * device) +{ + (void)stream; + free(device->input_name); + free(device->output_name); + free(device); + return CUBEB_OK; +} + +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, + .get_supported_input_processing_params = NULL, + .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_get_position = sun_stream_get_position, + .stream_get_latency = sun_stream_get_latency, + .stream_get_input_latency = NULL, + .stream_set_volume = sun_stream_set_volume, + .stream_set_name = NULL, + .stream_get_current_device = sun_get_current_device, + .stream_set_input_mute = NULL, + .stream_set_input_processing_params = NULL, + .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_tracing.h b/media/libcubeb/src/cubeb_tracing.h new file mode 100644 index 0000000000..3056d2c6b3 --- /dev/null +++ b/media/libcubeb/src/cubeb_tracing.h @@ -0,0 +1,27 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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_TRACING_H +#define CUBEB_TRACING_H + +#include <MicroGeckoProfiler.h> + +#define CUBEB_REGISTER_THREAD(name) \ + do { \ + char stacktop; \ + uprofiler_register_thread(name, &stacktop); \ + } while (0) + +#define CUBEB_UNREGISTER_THREAD() uprofiler_unregister_thread() + +// Insert a tracing marker, with a particular name. +// Phase can be 'x': instant marker, start time but no duration +// 'b': beginning of a marker with a duration +// 'e': end of a marker with a duration +#define CUBEB_TRACE(name, phase) \ + uprofiler_simple_event_marker(name, phase, 0, NULL, NULL, NULL) + +#endif // CUBEB_TRACING_H diff --git a/media/libcubeb/src/cubeb_triple_buffer.h b/media/libcubeb/src/cubeb_triple_buffer.h new file mode 100644 index 0000000000..a5a5978fb4 --- /dev/null +++ b/media/libcubeb/src/cubeb_triple_buffer.h @@ -0,0 +1,80 @@ +/* + * Copyright © 2022 Mozilla Foundation + * + * This program is made available under an ISC-style license. See the + * accompanying file LICENSE for details. + */ + +/** + * Adapted and ported to C++ from https://crates.io/crates/triple_buffer + */ + +#ifndef CUBEB_TRIPLE_BUFFER +#define CUBEB_TRIPLE_BUFFER + +#include <atomic> + +// Single producer / single consumer wait-free triple buffering +// implementation, for when a producer wants to publish data to a consumer +// without blocking, but when a queue is wastefull, because it's OK for the +// consumer to miss data updates. +template <typename T> class triple_buffer { +public: + // Write a new value into the triple buffer. Returns true if a value was + // overwritten. + // Producer-side only. + bool write(T & input) + { + storage[input_idx] = input; + return publish(); + } + // Get the latest value from the triple buffer. + // Consumer-side only. + T & read() + { + update(); + return storage[output_idx]; + } + // Returns true if a new value has been published by the consumer without + // having been consumed yet. + // Consumer-side only. + bool updated() + { + return (shared_state.load(std::memory_order_relaxed) & BACK_DIRTY_BIT) != 0; + } + +private: + // Publish a value to the consumer. Returns true if the data was overwritten + // without having been read. + bool publish() + { + auto former_back_idx = shared_state.exchange(input_idx | BACK_DIRTY_BIT, + std::memory_order_acq_rel); + input_idx = former_back_idx & BACK_INDEX_MASK; + return (former_back_idx & BACK_DIRTY_BIT) != 0; + } + // Get a new value from the producer, if a new value has been produced. + bool update() + { + bool was_updated = updated(); + if (was_updated) { + auto former_back_idx = + shared_state.exchange(output_idx, std::memory_order_acq_rel); + output_idx = former_back_idx & BACK_INDEX_MASK; + } + return was_updated; + } + T storage[3]; + // Mask used to extract back-buffer index + const uint8_t BACK_INDEX_MASK = 0b11; + // Bit set by producer to signal updates + const uint8_t BACK_DIRTY_BIT = 0b100; + // Shared state: a dirty bit, and an index. + std::atomic<uint8_t> shared_state = {0}; + // Output index, private to the consumer. + uint8_t output_idx = 1; + // Input index, private to the producer. + uint8_t input_idx = 2; +}; + +#endif // CUBEB_TRIPLE_BUFFER diff --git a/media/libcubeb/src/cubeb_utils.cpp b/media/libcubeb/src/cubeb_utils.cpp new file mode 100644 index 0000000000..dd1aef7b82 --- /dev/null +++ b/media/libcubeb/src/cubeb_utils.cpp @@ -0,0 +1,25 @@ +/* + * 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); + return 0; + } +} diff --git a/media/libcubeb/src/cubeb_utils.h b/media/libcubeb/src/cubeb_utils.h new file mode 100644 index 0000000000..851d24de2c --- /dev/null +++ b/media/libcubeb/src/cubeb_utils.h @@ -0,0 +1,318 @@ +/* + * Copyright © 2016 Mozilla Foundation + * + * This program is made available under an ISC-style license. See the + * accompanying file LICENSE for details. + */ + +#if !defined(CUBEB_UTILS) +#define CUBEB_UTILS + +#include "cubeb/cubeb.h" + +#ifdef __cplusplus + +#include <assert.h> +#include <mutex> +#include <stdint.h> +#include <string.h> +#include <type_traits> +#if defined(_WIN32) +#include "cubeb_utils_win.h" +#else +#include "cubeb_utils_unix.h" +#endif + +/** Similar to memcpy, but accounts for the size of an element. */ +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)); +} + +/** Similar to memmove, but accounts for the size of an element. */ +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)); +} + +/** Similar to a memset to zero, but accounts for the size of an element. */ +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); +} +} // namespace + +/** + * 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); +} +} // namespace + +/** + * 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 { +public: + explicit auto_array(uint32_t capacity = 0) + : data_(capacity ? new T[capacity] : nullptr), capacity_(capacity), + length_(0) + { + } + + ~auto_array() { delete[] data_; } + + /** Get a constant pointer to the underlying data. */ + T * data() const { return data_; } + + T * end() const { return data_ + length_; } + + const T & at(size_t index) const + { + assert(index < length_ && "out of range"); + return data_[index]; + } + + T & at(size_t index) + { + assert(index < length_ && "out of range"); + return data_[index]; + } + + /** Get how much underlying storage this auto_array has. */ + size_t capacity() const { return capacity_; } + + /** Get how much elements this auto_array contains. */ + size_t length() const { return length_; } + + /** Keeps the storage, but removes all the elements from the array. */ + void clear() { length_ = 0; } + + /** Change the storage of this auto array, copying the elements to the new + * storage. + * @returns true in case of success + * @returns false if the new capacity is not big enough to accomodate for the + * elements in the array. + */ + bool reserve(size_t new_capacity) + { + if (new_capacity < length_) { + return false; + } + T * new_data = new T[new_capacity]; + if (data_ && length_) { + PodCopy(new_data, data_, length_); + } + capacity_ = new_capacity; + delete[] data_; + data_ = new_data; + + return true; + } + + /** Append `length` elements to the end of the array, resizing the array if + * needed. + * @parameter elements the elements to append to the array. + * @parameter length the number of elements to append to the array. + */ + void push(const T * elements, size_t length) + { + if (length_ + length > capacity_) { + reserve(length_ + length); + } + if (data_) { + PodCopy(data_ + length_, elements, length); + } + length_ += length; + } + + /** Append `length` zero-ed elements to the end of the array, resizing the + * array if needed. + * @parameter length the number of elements to append to the array. + */ + void push_silence(size_t length) + { + if (length_ + length > capacity_) { + reserve(length + length_); + } + if (data_) { + PodZero(data_ + length_, length); + } + length_ += length; + } + + /** Prepend `length` zero-ed elements to the front of the array, resizing and + * shifting the array if needed. + * @parameter length the number of elements to prepend to the array. + */ + void push_front_silence(size_t length) + { + if (length_ + length > capacity_) { + reserve(length + length_); + } + if (data_) { + PodMove(data_ + length, data_, length_); + PodZero(data_, length); + } + length_ += length; + } + + /** Return the number of free elements in the array. */ + size_t available() const { return capacity_ - length_; } + + /** Copies `length` elements to `elements` if it is not null, and shift + * the remaining elements of the `auto_array` to the beginning. + * @parameter elements a buffer to copy the elements to, or nullptr. + * @parameter length the number of elements to copy. + * @returns true in case of success. + * @returns false if the auto_array contains less than `length` elements. */ + bool pop(T * elements, size_t length) + { + if (length > length_) { + return false; + } + if (!data_) { + return true; + } + if (elements) { + PodCopy(elements, data_, length); + } + PodMove(data_, data_ + length, length_ - length); + + length_ -= length; + + return true; + } + + void set_length(size_t length) + { + assert(length <= capacity_); + length_ = length; + } + +private: + /** The underlying storage */ + T * data_; + /** The size, in number of elements, of the storage. */ + size_t capacity_; + /** The number of elements the array contains. */ + size_t length_; +}; + +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); + } + + 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: + 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 new file mode 100644 index 0000000000..b6618ca45e --- /dev/null +++ b/media/libcubeb/src/cubeb_utils_unix.h @@ -0,0 +1,88 @@ +/* + * Copyright © 2016 Mozilla Foundation + * + * This program is made available under an ISC-style license. See the + * accompanying file LICENSE for details. + */ + +#if !defined(CUBEB_UTILS_UNIX) +#define CUBEB_UTILS_UNIX + +#include <errno.h> +#include <pthread.h> +#include <stdio.h> + +/* This wraps a critical section to track the owner in debug mode. */ +class owned_critical_section { +public: + owned_critical_section() + { + pthread_mutexattr_t attr; + pthread_mutexattr_init(&attr); +#ifndef NDEBUG + pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_ERRORCHECK); +#else + pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_NORMAL); +#endif + +#ifndef NDEBUG + int r = +#endif + pthread_mutex_init(&mutex, &attr); +#ifndef NDEBUG + assert(r == 0); +#endif + + pthread_mutexattr_destroy(&attr); + } + + ~owned_critical_section() + { +#ifndef NDEBUG + int r = +#endif + pthread_mutex_destroy(&mutex); +#ifndef NDEBUG + assert(r == 0); +#endif + } + + void lock() + { +#ifndef NDEBUG + int r = +#endif + pthread_mutex_lock(&mutex); +#ifndef NDEBUG + assert(r == 0 && "Deadlock"); +#endif + } + + void unlock() + { +#ifndef NDEBUG + int r = +#endif + pthread_mutex_unlock(&mutex); +#ifndef NDEBUG + assert(r == 0 && "Unlocking unlocked mutex"); +#endif + } + + void assert_current_thread_owns() + { +#ifndef NDEBUG + int r = pthread_mutex_lock(&mutex); + assert(r == EDEADLK); +#endif + } + +private: + pthread_mutex_t mutex; + + // Disallow copy and assignment because pthread_mutex_t cannot be copied. + owned_critical_section(const owned_critical_section &); + owned_critical_section & operator=(const owned_critical_section &); +}; + +#endif /* CUBEB_UTILS_UNIX */ diff --git a/media/libcubeb/src/cubeb_utils_win.h b/media/libcubeb/src/cubeb_utils_win.h new file mode 100644 index 0000000000..48e7b1b6d0 --- /dev/null +++ b/media/libcubeb/src/cubeb_utils_win.h @@ -0,0 +1,67 @@ +/* + * Copyright © 2016 Mozilla Foundation + * + * This program is made available under an ISC-style license. See the + * accompanying file LICENSE for details. + */ + +#if !defined(CUBEB_UTILS_WIN) +#define CUBEB_UTILS_WIN + +#include "cubeb-internal.h" +#include <windows.h> + +/* This wraps an SRWLock to track the owner in debug mode, adapted from + NSPR and http://blogs.msdn.com/b/oldnewthing/archive/2013/07/12/10433554.aspx + */ +class owned_critical_section { +public: + owned_critical_section() + : srwlock(SRWLOCK_INIT) +#ifndef NDEBUG + , + owner(0) +#endif + { + } + + void lock() + { + AcquireSRWLockExclusive(&srwlock); +#ifndef NDEBUG + XASSERT(owner != GetCurrentThreadId() && "recursive locking"); + owner = GetCurrentThreadId(); +#endif + } + + void unlock() + { +#ifndef NDEBUG + /* GetCurrentThreadId cannot return 0: it is not a the valid thread id */ + owner = 0; +#endif + ReleaseSRWLockExclusive(&srwlock); + } + + /* This is guaranteed to have the good behaviour if it succeeds. The behaviour + is undefined otherwise. */ + void assert_current_thread_owns() + { +#ifndef NDEBUG + /* This implies owner != 0, because GetCurrentThreadId cannot return 0. */ + XASSERT(owner == GetCurrentThreadId()); +#endif + } + +private: + SRWLOCK srwlock; +#ifndef NDEBUG + DWORD owner; +#endif + + // Disallow copy and assignment because SRWLock cannot be copied. + owned_critical_section(const owned_critical_section &); + owned_critical_section & operator=(const owned_critical_section &); +}; + +#endif /* CUBEB_UTILS_WIN */ diff --git a/media/libcubeb/src/cubeb_wasapi.cpp b/media/libcubeb/src/cubeb_wasapi.cpp new file mode 100644 index 0000000000..01417a52e3 --- /dev/null +++ b/media/libcubeb/src/cubeb_wasapi.cpp @@ -0,0 +1,3585 @@ +/* + * Copyright © 2013 Mozilla Foundation + * + * This program is made available under an ISC-style license. See the + * accompanying file LICENSE for details. + */ +#define _WIN32_WINNT 0x0603 +#define NOMINMAX + +#include <algorithm> +#include <atomic> +#include <audioclient.h> +#include <avrt.h> +#include <cmath> +#include <devicetopology.h> +#include <initguid.h> +#include <limits> +#include <memory> +#include <mmdeviceapi.h> +#include <process.h> +#include <stdint.h> +#include <stdio.h> +#include <stdlib.h> +#include <vector> +#include <windef.h> +#include <windows.h> +/* clang-format off */ +/* These need to be included after windows.h */ +#include <mmsystem.h> +/* clang-format on */ + +#include "cubeb-internal.h" +#include "cubeb/cubeb.h" +#include "cubeb_mixer.h" +#include "cubeb_resampler.h" +#include "cubeb_strings.h" +#include "cubeb_tracing.h" +#include "cubeb_utils.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 +#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 +DEFINE_PROPERTYKEY(PKEY_Device_FriendlyName, 0xa45c254e, 0xdf1c, 0x4efd, 0x80, + 0x20, 0x67, 0xd1, 0x46, 0xa8, 0x50, 0xe0, + 14); // DEVPROP_TYPE_STRING +#endif +#ifndef PKEY_Device_InstanceId +DEFINE_PROPERTYKEY(PKEY_Device_InstanceId, 0x78c34fc8, 0x104a, 0x4aca, 0x9e, + 0xa4, 0x52, 0x4d, 0x52, 0x99, 0x6e, 0x57, + 0x00000100); // VT_LPWSTR +#endif + +namespace { + +const int64_t LATENCY_NOT_AVAILABLE_YET = -1; + +const DWORD DEVICE_CHANGE_DEBOUNCE_MS = 250; + +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]) +{ + return N; +} + +template <typename T> class no_addref_release : public T { + ULONG STDMETHODCALLTYPE AddRef() = 0; + ULONG STDMETHODCALLTYPE Release() = 0; +}; + +template <typename T> 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(); } + + 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; } + + 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: + void release() noexcept + { + T * temp = ptr; + + if (temp) { + ptr = nullptr; + temp->Release(); + } + } + + T * ptr = nullptr; +}; + +LONG +wasapi_stream_add_ref(cubeb_stream * stm); +LONG +wasapi_stream_release(cubeb_stream * stm); + +struct auto_stream_ref { + auto_stream_ref(cubeb_stream * stm_) : stm(stm_) + { + wasapi_stream_add_ref(stm); + } + ~auto_stream_ref() { wasapi_stream_release(stm); } + cubeb_stream * stm; +}; + +extern cubeb_ops const wasapi_ops; + +static com_heap_ptr<wchar_t> +wasapi_get_default_device_id(EDataFlow flow, ERole role, + IMMDeviceEnumerator * enumerator); + +struct wasapi_default_devices { + wasapi_default_devices(IMMDeviceEnumerator * enumerator) + : render_console_id( + wasapi_get_default_device_id(eRender, eConsole, enumerator)), + render_comms_id( + wasapi_get_default_device_id(eRender, eCommunications, enumerator)), + capture_console_id( + wasapi_get_default_device_id(eCapture, eConsole, enumerator)), + capture_comms_id( + wasapi_get_default_device_id(eCapture, eCommunications, enumerator)) + { + } + + bool is_default(EDataFlow flow, ERole role, wchar_t const * id) + { + wchar_t const * default_id = nullptr; + if (flow == eRender && role == eConsole) { + default_id = this->render_console_id.get(); + } else if (flow == eRender && role == eCommunications) { + default_id = this->render_comms_id.get(); + } else if (flow == eCapture && role == eConsole) { + default_id = this->capture_console_id.get(); + } else if (flow == eCapture && role == eCommunications) { + default_id = this->capture_comms_id.get(); + } + + return default_id && wcscmp(id, default_id) == 0; + } + +private: + com_heap_ptr<wchar_t> render_console_id; + com_heap_ptr<wchar_t> render_comms_id; + com_heap_ptr<wchar_t> capture_console_id; + com_heap_ptr<wchar_t> capture_comms_id; +}; + +struct AutoRegisterThread { + AutoRegisterThread(const char * name) { CUBEB_REGISTER_THREAD(name); } + ~AutoRegisterThread() { CUBEB_UNREGISTER_THREAD(); } +}; + +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); +ERole +pref_to_role(cubeb_stream_prefs param); +int +wasapi_create_device(cubeb * ctx, cubeb_device_info & ret, + IMMDeviceEnumerator * enumerator, IMMDevice * dev, + wasapi_default_devices * defaults); +void +wasapi_destroy_device(cubeb_device_info * device_info); +static int +wasapi_enumerate_devices_internal(cubeb * context, cubeb_device_type type, + cubeb_device_collection * out, + DWORD state_mask); +static int +wasapi_device_collection_destroy(cubeb * ctx, + cubeb_device_collection * collection); +static char const * +wstr_to_utf8(wchar_t const * str); +static std::unique_ptr<wchar_t const[]> +utf8_to_wstr(char const * str); + +} // namespace + +class wasapi_collection_notification_client; +class monitor_device_notifications; + +struct cubeb { + cubeb_ops const * ops = &wasapi_ops; + owned_critical_section lock; + 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; + UINT64 performance_counter_frequency; +}; + +class wasapi_endpoint_notification_client; + +/* We have three possible callbacks we can use with a stream: + * - input only + * - output only + * - synchronized input and output + * + * Returns true when we should continue to play, false otherwise. + */ +typedef bool (*wasapi_refill_callback)(cubeb_stream * stm); + +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_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_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; + /* True if this stream will transport voice-data. */ + bool voice; + /* True if the input device of this stream is using bluetooth handsfree. */ + bool input_bluetooth_handsfree; + /* The input and output device, or NULL for default. */ + std::unique_ptr<const wchar_t[]> input_device_id; + std::unique_ptr<const wchar_t[]> output_device_id; + com_ptr<IMMDevice> input_device; + com_ptr<IMMDevice> output_device; + /* The latency initially requested for this stream, in frames. */ + 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. + - The lifetime for device_enumerator and notification_client, resampler, + mix_buffer are the same as the cubeb_stream instance. */ + + /* Main handle on the WASAPI stream. */ + com_ptr<IAudioClient> output_client; + /* Interface pointer to use the event-driven interface. */ + com_ptr<IAudioRenderClient> render_client; +#ifdef CUBEB_WASAPI_USE_IAUDIOSTREAMVOLUME + /* Interface pointer to use the volume facilities. */ + com_ptr<IAudioStreamVolume> audio_stream_volume; +#endif + /* Interface pointer to use the stream 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 = 0; + /* Frames written to the (logical) stream since it was first + created. Updated on device change. Uses stream_params.rate. */ + 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 = 0; + /* Device enumerator to be able to be notified when the default + device change. */ + 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 */ + com_ptr<wasapi_endpoint_notification_client> notification_client; + /* Main andle to the WASAPI capture stream. */ + com_ptr<IAudioClient> input_client; + /* Interface to use the event driven capture interface */ + com_ptr<IAudioCaptureClient> capture_client; + /* This event is set by the stream_destroy function, so the render loop can + exit properly. */ + 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 = 0; + /* This is set by WASAPI when we should refill the stream. */ + 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 = 0; + /* Each cubeb_stream has its own 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 = 0; + /* Maximum number of frames that can be requested in a callback. */ + uint32_t output_buffer_frame_count = 0; + /* Resampler instance. Resampling will only happen if necessary. */ + 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. */ + 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 = 1.0; + /* True if the stream is draining. */ + bool draining = false; + /* This needs an active audio input stream to be known, and is updated in the + * first audio input callback. */ + std::atomic<int64_t> input_latency_hns{LATENCY_NOT_AVAILABLE_YET}; + /* Those attributes count the number of frames requested (resp. received) by + the OS, to be able to detect drifts. This is only used for logging for now. */ + size_t total_input_frames = 0; + size_t total_output_frames = 0; + /* This is set by the render loop thread once it has obtained a reference to + * COM and this stream object. */ + HANDLE thread_ready_event = 0; + /* Keep a ref count on this stream object. After both stream_destroy has been + * called and the render loop thread has exited, destroy this stream object. + */ + LONG ref_count = 0; + + /* True if the stream is active, false if inactive. */ + bool active = false; +}; + +class monitor_device_notifications { +public: + monitor_device_notifications(cubeb * context) : cubeb_context(context) + { + create_thread(); + } + + ~monitor_device_notifications() + { + SetEvent(begin_shutdown); + WaitForSingleObject(shutdown_complete, INFINITE); + CloseHandle(thread); + + CloseHandle(input_changed); + CloseHandle(output_changed); + CloseHandle(begin_shutdown); + CloseHandle(shutdown_complete); + } + + 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) + { + AutoRegisterThread raii("WASAPI device notification thread"); + XASSERT(args); + auto mdn = static_cast<monitor_device_notifications *>(args); + mdn->notification_thread_loop(); + SetEvent(mdn->shutdown_complete); + 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, + begin_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; + } + + begin_shutdown = CreateEvent(nullptr, 0, 0, nullptr); + if (!begin_shutdown) { + LOG("Failed to create begin_shutdown event."); + return; + } + + shutdown_complete = CreateEvent(nullptr, 0, 0, nullptr); + if (!shutdown_complete) { + LOG("Failed to create shutdown_complete 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 begin_shutdown = INVALID_HANDLE_VALUE; + HANDLE shutdown_complete = 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 { +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_endpoint_notification_client(HANDLE event, ERole role) + : ref_count(1), reconfigure_event(event), role(role), + last_device_change(timeGetTime()) + { + } + + virtual ~wasapi_endpoint_notification_client() {} + + HRESULT STDMETHODCALLTYPE OnDefaultDeviceChanged(EDataFlow flow, ERole role, + LPCWSTR device_id) + { + LOG("endpoint: Audio device default changed flow=%d role=%d " + "new_device_id=%ws.", + flow, role, device_id); + + /* we only support a single stream type for now. */ + if (flow != eRender || role != this->role) { + return S_OK; + } + + DWORD last_change_ms = timeGetTime() - last_device_change; + bool same_device = default_device_id && device_id && + wcscmp(default_device_id.get(), device_id) == 0; + LOG("endpoint: Audio device default changed last_change=%u same_device=%d", + last_change_ms, same_device); + if (last_change_ms > DEVICE_CHANGE_DEBOUNCE_MS || !same_device) { + if (device_id) { + default_device_id.reset(_wcsdup(device_id)); + } else { + default_device_id.reset(); + } + BOOL ok = SetEvent(reconfigure_event); + LOG("endpoint: Audio device default changed: trigger reconfig"); + if (!ok) { + LOG("endpoint: SetEvent on reconfigure_event failed: %lx", + GetLastError()); + } + } + + 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("endpoint: Audio device added."); + return S_OK; + }; + + HRESULT STDMETHODCALLTYPE OnDeviceRemoved(LPCWSTR device_id) + { + LOG("endpoint: Audio device removed."); + return S_OK; + } + + HRESULT STDMETHODCALLTYPE OnDeviceStateChanged(LPCWSTR device_id, + DWORD new_state) + { + LOG("endpoint: Audio device state changed."); + return S_OK; + } + + HRESULT STDMETHODCALLTYPE OnPropertyValueChanged(LPCWSTR device_id, + const PROPERTYKEY key) + { + // 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; + std::unique_ptr<const wchar_t[]> default_device_id; + DWORD last_device_change; +}; + +namespace { + +long +wasapi_data_callback(cubeb_stream * stm, void * user_ptr, + void const * input_buffer, void * output_buffer, + long nframes) +{ + return stm->data_callback(stm, user_ptr, input_buffer, output_buffer, + nframes); +} + +void +wasapi_state_callback(cubeb_stream * stm, void * user_ptr, cubeb_state state) +{ + return stm->state_callback(stm, user_ptr, state); +} + +char const * +intern_device_id(cubeb * ctx, wchar_t const * id) +{ + XASSERT(id); + + auto_lock lock(ctx->lock); + + 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; +} + +bool +has_output(cubeb_stream * stm) +{ + return stm->output_stream_params.rate != 0; +} + +double +stream_to_mix_samplerate_ratio(cubeb_stream_params & stream, + cubeb_stream_params & mixer) +{ + return double(stream.rate) / mixer.rate; +} + +/* 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 + */ + +cubeb_channel_layout +mask_to_channel_layout(WAVEFORMATEX const * fmt) +{ + 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) +{ + return has_input(stm) ? stm->input_stream_params.rate + : stm->output_stream_params.rate; +} + +uint32_t +hns_to_frames(uint32_t rate, REFERENCE_TIME hns) +{ + return std::ceil((hns - 1) / 10000000.0 * rate); +} + +uint32_t +hns_to_frames(cubeb_stream * stm, REFERENCE_TIME hns) +{ + return hns_to_frames(get_rate(stm), hns); +} + +REFERENCE_TIME +frames_to_hns(uint32_t rate, uint32_t frames) +{ + return std::ceil(frames * 10000000.0 / rate); +} + +/* This returns the size of a frame in the stream, before the eventual upmix + occurs. */ +static size_t +frames_to_bytes_before_mix(cubeb_stream * stm, size_t 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, 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. 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.get(), input_buffer, + &input_frames_count, dest, output_frames_needed); + if (out_frames < 0) { + ALOGV("Callback refill error: %d", out_frames); + wasapi_state_callback(stm, stm->user_ptr, CUBEB_STATE_ERROR); + return out_frames; + } + + float volume = 1.0; + { + auto_lock lock(stm->stream_reset_lock); + stm->frames_written += out_frames; + volume = stm->volume; + } + + /* 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) || stm->has_dummy_output); + +#ifndef CUBEB_WASAPI_USE_IAUDIOSTREAMVOLUME + if (has_output(stm) && !stm->has_dummy_output && volume != 1.0) { + // Adjust the output volume. + // Note: This could be integrated with the remixing below. + long out_samples = out_frames * stm->output_stream_params.channels; + if (volume == 0.0) { + memset(dest, 0, out_samples * stm->bytes_per_sample); + } else { + switch (stm->output_stream_params.format) { + case CUBEB_SAMPLE_FLOAT32NE: { + float * buf = static_cast<float *>(dest); + for (long i = 0; i < out_samples; ++i) { + buf[i] *= volume; + } + break; + } + case CUBEB_SAMPLE_S16NE: { + short * buf = static_cast<short *>(dest); + for (long i = 0; i < out_samples; ++i) { + buf[i] = static_cast<short>(static_cast<float>(buf[i]) * volume); + } + break; + } + default: + XASSERT(false); + } + } + } +#endif + + // 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; +} + +bool +trigger_async_reconfigure(cubeb_stream * stm) +{ + XASSERT(stm && stm->reconfigure_event); + LOG("Try reconfiguring the stream"); + BOOL ok = SetEvent(stm->reconfigure_event); + if (!ok) { + LOG("SetEvent on reconfigure_event failed: %lx", GetLastError()); + } + return static_cast<bool>(ok); +} + +/* This helper grabs all the frames available from a capture client, put them in + * the linear_input_buffer. This helper does not work with exclusive mode + * streams. */ +bool +get_input_buffer(cubeb_stream * stm) +{ + XASSERT(has_input(stm)); + + HRESULT hr; + BYTE * input_packet = NULL; + DWORD flags; + UINT64 dev_pos; + UINT64 pc_position; + UINT32 next; + /* Get input packets until we have captured enough frames, and put them in a + * contiguous buffer. */ + uint32_t offset = 0; + // 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("Input device invalidated error"); + // No need to reset device if user asks to use particular device, or + // switching is disabled. + if (stm->input_device_id || + (stm->input_stream_params.prefs & + CUBEB_STREAM_PREF_DISABLE_DEVICE_SWITCHING) || + !trigger_async_reconfigure(stm)) { + wasapi_state_callback(stm, stm->user_ptr, CUBEB_STATE_ERROR); + return false; + } + return true; + } + + if (FAILED(hr)) { + LOG("cannot get next packet size: %lx", hr); + return false; + } + + UINT32 frames; + hr = stm->capture_client->GetBuffer(&input_packet, &frames, &flags, + &dev_pos, &pc_position); + + if (FAILED(hr)) { + LOG("GetBuffer failed for capture: %lx", hr); + return false; + } + XASSERT(frames == next); + + if (stm->context->performance_counter_frequency) { + LARGE_INTEGER now; + UINT64 now_hns; + // See + // https://docs.microsoft.com/en-us/windows/win32/api/audioclient/nf-audioclient-iaudiocaptureclient-getbuffer, + // section "Remarks". + QueryPerformanceCounter(&now); + now_hns = + 10000000 * now.QuadPart / stm->context->performance_counter_frequency; + if (now_hns >= pc_position) { + stm->input_latency_hns = now_hns - pc_position; + } + } + + stm->total_input_frames += frames; + + 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", frames); + stm->linear_input_buffer->push_silence(input_stream_samples); + } else { + 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(input_packet, input_stream_samples); + } + } + hr = stm->capture_client->ReleaseBuffer(frames); + if (FAILED(hr)) { + LOG("FAILED to release intput buffer"); + return false; + } + offset += input_stream_samples; + } + + ALOGV("get_input_buffer: got %d frames", offset); + + 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, void *& buffer, size_t & frame_count) +{ + UINT32 padding_out; + HRESULT hr; + + 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("Output device invalidated error"); + // No need to reset device if user asks to use particular device, or + // switching is disabled. + if (stm->output_device_id || + (stm->output_stream_params.prefs & + CUBEB_STREAM_PREF_DISABLE_DEVICE_SWITCHING) || + !trigger_async_reconfigure(stm)) { + wasapi_state_callback(stm, stm->user_ptr, CUBEB_STATE_ERROR); + return false; + } + return true; + } + + if (FAILED(hr)) { + LOG("Failed to get padding: %lx", hr); + return false; + } + + XASSERT(padding_out <= stm->output_buffer_frame_count); + + if (stm->draining) { + if (padding_out == 0) { + LOG("Draining finished."); + wasapi_state_callback(stm, stm->user_ptr, CUBEB_STATE_DRAINED); + return false; + } + LOG("Draining."); + return true; + } + + frame_count = stm->output_buffer_frame_count - padding_out; + BYTE * output_buffer; + + hr = stm->render_client->GetBuffer(frame_count, &output_buffer); + if (FAILED(hr)) { + LOG("cannot get render buffer"); + return false; + } + + buffer = output_buffer; + + return true; +} + +/** + * This function gets input data from a input device, and pass it along with an + * output buffer to the resamplers. */ +bool +refill_callback_duplex(cubeb_stream * stm) +{ + HRESULT hr; + void * output_buffer = nullptr; + size_t output_frames = 0; + size_t input_frames; + bool rv; + + XASSERT(has_input(stm) && has_output(stm)); + + if (stm->input_stream_params.prefs & CUBEB_STREAM_PREF_LOOPBACK) { + HRESULT rv = get_input_buffer(stm); + if (FAILED(rv)) { + return rv; + } + } + + input_frames = + stm->linear_input_buffer->length() / stm->input_stream_params.channels; + + rv = get_output_buffer(stm, output_buffer, output_frames); + if (!rv) { + hr = stm->render_client->ReleaseBuffer(output_frames, 0); + return rv; + } + + /* This can only happen when debugging, and having breakpoints set in the + * callback in a way that it makes the stream underrun. */ + if (output_frames == 0) { + return true; + } + + /* Wait for draining is not important on duplex. */ + if (stm->draining) { + return false; + } + + stm->total_output_frames += output_frames; + + ALOGV("in: %zu, out: %zu, missing: %ld, ratio: %f", stm->total_input_frames, + stm->total_output_frames, + static_cast<long>(stm->total_output_frames) - stm->total_input_frames, + static_cast<float>(stm->total_output_frames) / stm->total_input_frames); + + long got; + 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) + got = + 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); + + got = refill(stm, stm->linear_input_buffer->data(), input_frames, + output_buffer, output_frames); + } + + stm->linear_input_buffer->clear(); + + 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: %lx", hr); + return false; + } + if (got < 0) { + return false; + } + return true; +} + +bool +refill_callback_input(cubeb_stream * stm) +{ + bool rv; + size_t input_frames; + + XASSERT(has_input(stm) && !has_output(stm)); + + rv = get_input_buffer(stm); + if (!rv) { + return rv; + } + + input_frames = + stm->linear_input_buffer->length() / stm->input_stream_params.channels; + if (!input_frames) { + return true; + } + + ALOGV("Input callback: input frames: %Iu", input_frames); + + long read = + refill(stm, stm->linear_input_buffer->data(), input_frames, nullptr, 0); + if (read < 0) { + return false; + } + + stm->linear_input_buffer->clear(); + + return !stm->draining; +} + +bool +refill_callback_output(cubeb_stream * stm) +{ + bool rv; + HRESULT hr; + void * output_buffer = nullptr; + size_t output_frames = 0; + + XASSERT(!has_input(stm) && has_output(stm)); + + rv = get_output_buffer(stm, output_buffer, output_frames); + if (!rv) { + return rv; + } + + if (stm->draining || output_frames == 0) { + return true; + } + + long got = refill(stm, nullptr, 0, output_buffer, output_frames); + + ALOGV("Output callback: output frames requested: %Iu, got %ld", output_frames, + got); + if (got < 0) { + return false; + } + XASSERT(size_t(got) == output_frames || stm->draining); + + hr = stm->render_client->ReleaseBuffer(got, 0); + if (FAILED(hr)) { + LOG("failed to release buffer: %lx", hr); + return false; + } + + return size_t(got) == output_frames || stm->draining; +} + +void +wasapi_stream_destroy(cubeb_stream * stm); + +static unsigned int __stdcall wasapi_stream_render_loop(LPVOID stream) +{ + AutoRegisterThread raii("cubeb rendering thread"); + cubeb_stream * stm = static_cast<cubeb_stream *>(stream); + + auto_stream_ref stream_ref(stm); + struct auto_com { + auto_com() + { + HRESULT hr = CoInitializeEx(nullptr, COINIT_MULTITHREADED); + XASSERT(SUCCEEDED(hr)); + } + ~auto_com() { CoUninitialize(); } + } com; + + bool is_playing = true; + HANDLE wait_array[4] = {stm->shutdown_event, stm->reconfigure_event, + stm->refill_event, stm->input_available_event}; + HANDLE mmcss_handle = NULL; + HRESULT hr = 0; + DWORD mmcss_task_index = 0; + + // Signal wasapi_stream_start that we've initialized COM and incremented + // the stream's ref_count. + BOOL ok = SetEvent(stm->thread_ready_event); + if (!ok) { + LOG("thread_ready SetEvent failed: %lx", GetLastError()); + return 0; + } + + /* We could consider using "Pro Audio" here for WebAudio and + maybe WebRTC. */ + 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: %lx", + GetLastError()); + } + + while (is_playing) { + DWORD waitResult = WaitForMultipleObjects(ARRAY_LENGTH(wait_array), + wait_array, FALSE, INFINITE); + switch (waitResult) { + case WAIT_OBJECT_0: { /* shutdown */ + is_playing = false; + /* We don't check if the drain is actually finished here, we just want to + shutdown. */ + if (stm->draining) { + wasapi_state_callback(stm, stm->user_ptr, CUBEB_STATE_DRAINED); + } + continue; + } + case WAIT_OBJECT_0 + 1: { /* reconfigure */ + auto_lock lock(stm->stream_reset_lock); + if (!stm->active) { + /* Avoid reconfiguring, stream start will handle it. */ + LOG("Stream is not active, ignoring reconfigure."); + continue; + } + XASSERT(stm->output_client || stm->input_client); + LOG("Reconfiguring the stream"); + /* Close the stream */ + bool was_running = false; + if (stm->output_client) { + was_running = stm->output_client->Stop() == S_OK; + LOG("Output stopped."); + } + if (stm->input_client) { + was_running = stm->input_client->Stop() == S_OK; + LOG("Input stopped."); + } + close_wasapi_stream(stm); + LOG("Stream closed."); + /* Reopen a stream and start it immediately. This will automatically + pick the new default device for this role. */ + int r = setup_wasapi_stream(stm); + if (r != CUBEB_OK) { + LOG("Error setting up the stream during reconfigure."); + /* Don't destroy the stream here, since we expect the caller to do + so after the error has propagated via the state callback. */ + is_playing = false; + hr = E_FAIL; + continue; + } + LOG("Stream setup successfuly."); + XASSERT(stm->output_client || stm->input_client); + if (was_running && stm->output_client) { + 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 (was_running && stm->input_client) { + 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))); + is_playing = stm->refill_callback(stm); + break; + case WAIT_OBJECT_0 + 3: { /* input available */ + HRESULT rv = get_input_buffer(stm); + if (FAILED(rv)) { + is_playing = false; + continue; + } + + if (!has_output(stm)) { + is_playing = stm->refill_callback(stm); + } + + break; + } + default: + LOG("case %lu not handled in render loop.", waitResult); + XASSERT(false); + } + } + + // Stop audio clients since this thread will no longer service + // the events. + if (stm->output_client) { + stm->output_client->Stop(); + } + if (stm->input_client) { + stm->input_client->Stop(); + } + + if (mmcss_handle) { + AvRevertMmThreadCharacteristics(mmcss_handle); + } + + if (FAILED(hr)) { + wasapi_state_callback(stm, stm->user_ptr, CUBEB_STATE_ERROR); + } + + return 0; +} + +void +wasapi_destroy(cubeb * context); + +HRESULT +register_notification_client(cubeb_stream * stm) +{ + XASSERT(stm->device_enumerator && !stm->notification_client); + + stm->notification_client.reset(new wasapi_endpoint_notification_client( + stm->reconfigure_event, stm->role)); + + HRESULT hr = stm->device_enumerator->RegisterEndpointNotificationCallback( + stm->notification_client.get()); + if (FAILED(hr)) { + LOG("Could not register endpoint notification callback: %lx", hr); + stm->notification_client = nullptr; + } + + return hr; +} + +HRESULT +unregister_notification_client(cubeb_stream * stm) +{ + XASSERT(stm->device_enumerator && stm->notification_client); + + HRESULT 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. + return S_OK; + } + + stm->notification_client = nullptr; + + return S_OK; +} + +HRESULT +get_endpoint(com_ptr<IMMDevice> & device, LPCWSTR devid) +{ + 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->GetDevice(devid, device.receive()); + if (FAILED(hr)) { + LOG("Could not get device: %lx", hr); + return hr; + } + + return S_OK; +} + +HRESULT +register_collection_notification_client(cubeb * context) +{ + context->lock.assert_current_thread_owns(); + XASSERT(!context->device_collection_enumerator && + !context->collection_notification_client); + HRESULT hr = CoCreateInstance( + __uuidof(MMDeviceEnumerator), NULL, CLSCTX_INPROC_SERVER, + IID_PPV_ARGS(context->device_collection_enumerator.receive())); + if (FAILED(hr)) { + LOG("Could not get device enumerator: %lx", hr); + return hr; + } + + 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) +{ + context->lock.assert_current_thread_owns(); + XASSERT(context->device_collection_enumerator && + context->collection_notification_client); + HRESULT hr = context->device_collection_enumerator + ->UnregisterEndpointNotificationCallback( + context->collection_notification_client.get()); + if (FAILED(hr)) { + return hr; + } + + 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; +} + +double +current_stream_delay(cubeb_stream * stm) +{ + stm->stream_reset_lock.assert_current_thread_owns(); + + /* If the default audio endpoint went away during playback and we weren't + able to configure a new one, it's possible the caller may call this + before the error callback has propogated back. */ + if (!stm->audio_clock) { + return 0; + } + + UINT64 freq; + HRESULT hr = stm->audio_clock->GetFrequency(&freq); + if (FAILED(hr)) { + LOG("GetFrequency failed: %lx", hr); + return 0; + } + + UINT64 pos; + hr = stm->audio_clock->GetPosition(&pos, NULL); + if (FAILED(hr)) { + LOG("GetPosition failed: %lx", hr); + return 0; + } + + double cur_pos = static_cast<double>(pos) / freq; + double max_pos = + static_cast<double>(stm->frames_written) / stm->output_mix_params.rate; + double delay = std::max(max_pos - cur_pos, 0.0); + + return delay; +} + +#ifdef CUBEB_WASAPI_USE_IAUDIOSTREAMVOLUME +int +stream_set_volume(cubeb_stream * stm, float volume) +{ + stm->stream_reset_lock.assert_current_thread_owns(); + + if (!stm->audio_stream_volume) { + return CUBEB_ERROR; + } + + uint32_t channels; + HRESULT hr = stm->audio_stream_volume->GetChannelCount(&channels); + if (FAILED(hr)) { + LOG("could not get the channel count: %lx", hr); + return CUBEB_ERROR; + } + + /* up to 9.1 for now */ + if (channels > 10) { + return CUBEB_ERROR_NOT_SUPPORTED; + } + + float volumes[10]; + for (uint32_t i = 0; i < channels; i++) { + volumes[i] = volume; + } + + hr = stm->audio_stream_volume->SetAllVolumes(channels, volumes); + if (FAILED(hr)) { + LOG("could not set the channels volume: %lx", hr); + return CUBEB_ERROR; + } + + return CUBEB_OK; +} +#endif +} // namespace + +extern "C" { +int +wasapi_init(cubeb ** context, char const * context_name) +{ + /* 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. */ + com_ptr<IMMDevice> device; + HRESULT hr = get_default_endpoint(device, eRender, eConsole); + if (FAILED(hr)) { + 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; + } + } + + cubeb * ctx = new cubeb(); + + ctx->ops = &wasapi_ops; + auto_lock lock(ctx->lock); + if (cubeb_strings_init(&ctx->device_ids) != CUBEB_OK) { + delete ctx; + return CUBEB_ERROR; + } + + LARGE_INTEGER frequency; + if (QueryPerformanceFrequency(&frequency)) { + ctx->performance_counter_frequency = frequency.QuadPart; + } else { + LOG("Failed getting performance counter frequency, latency reporting will " + "be inacurate"); + ctx->performance_counter_frequency = 0; + } + + *context = ctx; + + return CUBEB_OK; +} +} + +namespace { +enum ShutdownPhase { OnStop, OnDestroy }; + +bool +stop_and_join_render_thread(cubeb_stream * stm) +{ + LOG("%p: Stop and join render thread: %p", stm, stm->thread); + if (!stm->thread) { + return true; + } + + BOOL ok = SetEvent(stm->shutdown_event); + if (!ok) { + LOG("stop_and_join_render_thread: SetEvent failed: %lx", GetLastError()); + return false; + } + + /* Wait five seconds for the rendering thread to return. It's supposed to + * check its event loop very often, five seconds is rather conservative. + * Note: 5*1s loop to work around timer sleep issues on pre-Windows 8. */ + DWORD r; + for (int i = 0; i < 5; ++i) { + r = WaitForSingleObject(stm->thread, 1000); + if (r == WAIT_OBJECT_0) { + break; + } + } + if (r != WAIT_OBJECT_0) { + LOG("stop_and_join_render_thread: WaitForSingleObject on thread failed: " + "%lx, %lx", + r, GetLastError()); + return false; + } + + return true; +} + +void +wasapi_destroy(cubeb * context) +{ + { + auto_lock lock(context->lock); + XASSERT(!context->device_collection_enumerator && + !context->collection_notification_client); + + if (context->device_ids) { + cubeb_strings_destroy(context->device_ids); + } + } + + delete context; +} + +char const * +wasapi_get_backend_id(cubeb * context) +{ + return "wasapi"; +} + +int +wasapi_get_max_channel_count(cubeb * ctx, uint32_t * max_channels) +{ + XASSERT(ctx && max_channels); + + 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, + client.receive_vpp()); + if (FAILED(hr)) { + return CUBEB_ERROR; + } + + WAVEFORMATEX * tmp = nullptr; + hr = client->GetMixFormat(&tmp); + if (FAILED(hr)) { + return CUBEB_ERROR; + } + com_heap_ptr<WAVEFORMATEX> mix_format(tmp); + + *max_channels = mix_format->nChannels; + + return CUBEB_OK; +} + +int +wasapi_get_min_latency(cubeb * ctx, cubeb_stream_params params, + uint32_t * latency_frames) +{ + if (params.format != CUBEB_SAMPLE_FLOAT32NE && + params.format != CUBEB_SAMPLE_S16NE) { + return CUBEB_ERROR_INVALID_FORMAT; + } + + 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: %lx", hr); + return CUBEB_ERROR; + } + + com_ptr<IAudioClient> client; + hr = device->Activate(__uuidof(IAudioClient), CLSCTX_INPROC_SERVER, NULL, + client.receive_vpp()); + if (FAILED(hr)) { + LOG("Could not activate device for latency: %lx", hr); + return CUBEB_ERROR; + } + + REFERENCE_TIME minimum_period; + REFERENCE_TIME default_period; + hr = client->GetDevicePeriod(&default_period, &minimum_period); + if (FAILED(hr)) { + LOG("Could not get device period: %lx", hr); + return CUBEB_ERROR; + } + + LOG("default device period: %I64d, minimum device period: %I64d", + default_period, minimum_period); + + /* 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 + */ + + // #ifdef _WIN32_WINNT_WIN10 +#if 0 + *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); + + return CUBEB_OK; +} + +int +wasapi_get_preferred_sample_rate(cubeb * ctx, uint32_t * rate) +{ + 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, + client.receive_vpp()); + if (FAILED(hr)) { + return CUBEB_ERROR; + } + + WAVEFORMATEX * tmp = nullptr; + hr = client->GetMixFormat(&tmp); + if (FAILED(hr)) { + return CUBEB_ERROR; + } + com_heap_ptr<WAVEFORMATEX> mix_format(tmp); + + *rate = mix_format->nSamplesPerSec; + + LOG("Preferred sample rate for output: %u", *rate); + + return CUBEB_OK; +} + +static void +waveformatex_update_derived_properties(WAVEFORMATEX * format) +{ + 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) { + return; + } + + 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; + + /* 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 * tmp = nullptr; + HRESULT hr = audio_client->IsFormatSupported(AUDCLNT_SHAREMODE_SHARED, + mix_format.get(), &tmp); + com_heap_ptr<WAVEFORMATEX> closest(tmp); + if (hr == S_FALSE) { + /* 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.get()); + 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. */ + 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: %lx", hr); + } +} + +static int +initialize_iaudioclient2(com_ptr<IAudioClient> & audio_client) +{ + com_ptr<IAudioClient2> audio_client2; + audio_client->QueryInterface<IAudioClient2>(audio_client2.receive()); + if (!audio_client2) { + LOG("Could not get IAudioClient2 interface, not setting " + "AUDCLNT_STREAMOPTIONS_RAW."); + return CUBEB_OK; + } + AudioClientProperties properties = {0}; + properties.cbSize = sizeof(AudioClientProperties); +#ifndef __MINGW32__ + properties.Options |= AUDCLNT_STREAMOPTIONS_RAW; +#endif + HRESULT hr = audio_client2->SetClientProperties(&properties); + if (FAILED(hr)) { + LOG("IAudioClient2::SetClientProperties error: %lx", GetLastError()); + return CUBEB_ERROR; + } + return CUBEB_OK; +} + +#if 0 +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; + } + + // 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; +} +#endif + +#define DIRECTION_NAME (direction == eCapture ? "capture" : "render") + +template <typename T> +int +setup_wasapi_stream_one_side(cubeb_stream * stm, + cubeb_stream_params * stream_params, + wchar_t const * devid, EDataFlow direction, + REFIID riid, com_ptr<IAudioClient> & audio_client, + uint32_t * buffer_frame_count, HANDLE & event, + T & render_or_capture_client, + cubeb_stream_params * mix_params, + com_ptr<IMMDevice> & device) +{ + XASSERT(direction == eCapture || direction == eRender); + + 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(); + // If user doesn't specify a particular device, we can choose another one when + // the given devid is unavailable. + bool allow_fallback = + direction == eCapture ? !stm->input_device_id : !stm->output_device_id; + bool try_again = false; + // This loops until we find a device that works, or we've exhausted all + // possibilities. + do { + if (devid) { + hr = get_endpoint(device, devid); + if (FAILED(hr)) { + LOG("Could not get %s endpoint, error: %lx\n", DIRECTION_NAME, hr); + return CUBEB_ERROR; + } + } 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)) { + 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. */ +#if 0 // See https://bugzilla.mozilla.org/show_bug.cgi?id=1590902 + hr = device->Activate(__uuidof(IAudioClient3), + CLSCTX_INPROC_SERVER, + NULL, audio_client.receive_vpp()); + if (hr == E_NOINTERFACE) { +#endif + hr = device->Activate(__uuidof(IAudioClient), CLSCTX_INPROC_SERVER, NULL, + audio_client.receive_vpp()); +#if 0 + } +#endif + + if (FAILED(hr)) { + LOG("Could not activate the device to get an audio" + " 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 && allow_fallback) { + LOG("Trying again with the default %s audio device.", DIRECTION_NAME); + devid = nullptr; + device = nullptr; + try_again = true; + } else { + return CUBEB_ERROR; + } + } else { + try_again = false; + } + } while (try_again); + + /* We have to distinguish between the format the mixer uses, + * and the format the stream we want to play uses. */ + 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: %lx", + DIRECTION_NAME, hr); + return CUBEB_ERROR; + } + com_heap_ptr<WAVEFORMATEX> mix_format(tmp); + + mix_format->wBitsPerSample = stm->bytes_per_sample * 8; + if (mix_format->wFormatTag == WAVE_FORMAT_PCM || + mix_format->wFormatTag == WAVE_FORMAT_IEEE_FLOAT) { + switch (mix_format->wBitsPerSample) { + case 8: + case 16: + mix_format->wFormatTag = WAVE_FORMAT_PCM; + break; + case 32: + mix_format->wFormatTag = WAVE_FORMAT_IEEE_FLOAT; + break; + default: + LOG("%u bits per sample is incompatible with PCM wave formats", + mix_format->wBitsPerSample); + return CUBEB_ERROR; + } + } + + 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()); + + /* 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; + 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, + stream_params->layout, mix_params->format, mix_params->rate, + mix_params->channels, mix_params->layout); + + DWORD flags = 0; + + // 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; + } + + REFERENCE_TIME latency_hns = frames_to_hns(stream_params->rate, stm->latency); + + // Adjust input latency and check if input is using bluetooth handsfree + // protocol. + if (direction == eCapture) { + stm->input_bluetooth_handsfree = false; + + wasapi_default_devices default_devices(stm->device_enumerator.get()); + cubeb_device_info device_info; + if (wasapi_create_device(stm->context, device_info, + stm->device_enumerator.get(), device.get(), + &default_devices) == CUBEB_OK) { + if (device_info.latency_hi == 0) { + LOG("Input: could not query latency_hi to guess safe latency"); + wasapi_destroy_device(&device_info); + return CUBEB_ERROR; + } + // This multiplicator has been found empirically. + uint32_t latency_frames = device_info.latency_hi * 8; + LOG("Input: latency increased to %u frames from a default of %u", + latency_frames, device_info.latency_hi); + latency_hns = frames_to_hns(device_info.default_rate, latency_frames); + + const char * HANDSFREE_TAG = "BTHHFENUM"; + size_t len = sizeof(HANDSFREE_TAG); + if (strlen(device_info.group_id) >= len && + strncmp(device_info.group_id, HANDSFREE_TAG, len) == 0) { + LOG("Input device is using bluetooth handsfree protocol"); + stm->input_bluetooth_handsfree = true; + } + + wasapi_destroy_device(&device_info); + } else { + LOG("Could not get cubeb_device_info. Skip customizing input settings"); + } + } + + if (stream_params->prefs & CUBEB_STREAM_PREF_RAW) { + if (initialize_iaudioclient2(audio_client) != CUBEB_OK) { + LOG("Can't initialize an IAudioClient2, error: %lx", GetLastError()); + // This is not fatal. + } + } + +#if 0 // See https://bugzilla.mozilla.org/show_bug.cgi?id=1590902 + if (initialize_iaudioclient3(audio_client, stm, mix_format, flags, direction)) { + LOG("Initialized with IAudioClient3"); + } else { +#endif + hr = audio_client->Initialize(AUDCLNT_SHAREMODE_SHARED, flags, latency_hns, 0, + mix_format.get(), NULL); +#if 0 + } +#endif + if (FAILED(hr)) { + LOG("Unable to initialize audio client for %s: %lx.", DIRECTION_NAME, hr); + return CUBEB_ERROR; + } + + hr = audio_client->GetBufferSize(buffer_frame_count); + if (FAILED(hr)) { + LOG("Could not get the buffer size from the client" + " for %s %lx.", + DIRECTION_NAME, hr); + return CUBEB_ERROR; + } + + LOG("Buffer size is: %d for %s\n", *buffer_frame_count, DIRECTION_NAME); + + // 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, render_or_capture_client.receive_vpp()); + if (FAILED(hr)) { + LOG("Could not get the %s client %lx.", DIRECTION_NAME, hr); + return CUBEB_ERROR; + } + + return CUBEB_OK; +} + +#undef DIRECTION_NAME + +// Returns a non-null cubeb_devid if we find a matched device, or nullptr +// otherwise. +cubeb_devid +wasapi_find_bt_handsfree_output_device(cubeb_stream * stm) +{ + HRESULT hr; + cubeb_device_info * input_device = nullptr; + cubeb_device_collection collection; + + // Only try to match to an output device if the input device is a bluetooth + // device that is using the handsfree protocol + if (!stm->input_bluetooth_handsfree) { + return nullptr; + } + + wchar_t * tmp = nullptr; + hr = stm->input_device->GetId(&tmp); + if (FAILED(hr)) { + LOG("Couldn't get input device id in " + "wasapi_find_bt_handsfree_output_device"); + return nullptr; + } + com_heap_ptr<wchar_t> device_id(tmp); + cubeb_devid input_device_id = reinterpret_cast<cubeb_devid>( + intern_device_id(stm->context, device_id.get())); + if (!input_device_id) { + return nullptr; + } + + int rv = wasapi_enumerate_devices_internal( + stm->context, + (cubeb_device_type)(CUBEB_DEVICE_TYPE_INPUT | CUBEB_DEVICE_TYPE_OUTPUT), + &collection, DEVICE_STATE_ACTIVE); + if (rv != CUBEB_OK) { + return nullptr; + } + + // Find the input device, and then find the output device with the same group + // id and the same rate. + for (uint32_t i = 0; i < collection.count; i++) { + if (collection.device[i].devid == input_device_id) { + input_device = &collection.device[i]; + break; + } + } + + cubeb_devid matched_output = nullptr; + + if (input_device) { + for (uint32_t i = 0; i < collection.count; i++) { + cubeb_device_info & dev = collection.device[i]; + if (dev.type == CUBEB_DEVICE_TYPE_OUTPUT && dev.group_id && + !strcmp(dev.group_id, input_device->group_id) && + dev.default_rate == input_device->default_rate) { + LOG("Found matching device for %s: %s", input_device->friendly_name, + dev.friendly_name); + matched_output = dev.devid; + break; + } + } + } + + wasapi_device_collection_destroy(stm->context, &collection); + return matched_output; +} + +std::unique_ptr<wchar_t[]> +copy_wide_string(const wchar_t * src) +{ + XASSERT(src); + size_t len = wcslen(src); + std::unique_ptr<wchar_t[]> copy(new wchar_t[len + 1]); + if (wcsncpy_s(copy.get(), len + 1, src, len) != 0) { + return nullptr; + } + return copy; +} + +int +setup_wasapi_stream(cubeb_stream * stm) +{ + int rv; + + stm->stream_reset_lock.assert_current_thread_owns(); + + XASSERT((!stm->output_client || !stm->input_client) && + "WASAPI stream already setup, close it first."); + + std::unique_ptr<const wchar_t[]> selected_output_device_id; + if (stm->output_device_id) { + if (std::unique_ptr<wchar_t[]> tmp = + copy_wide_string(stm->output_device_id.get())) { + selected_output_device_id = std::move(tmp); + } else { + LOG("Failed to copy output device identifier."); + return CUBEB_ERROR; + } + } + + if (has_input(stm)) { + LOG("(%p) Setup capture: device=%p", stm, stm->input_device_id.get()); + rv = setup_wasapi_stream_one_side( + stm, &stm->input_stream_params, stm->input_device_id.get(), eCapture, + __uuidof(IAudioCaptureClient), stm->input_client, + &stm->input_buffer_frame_count, stm->input_available_event, + stm->capture_client, &stm->input_mix_params, stm->input_device); + 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 this is a bluetooth device, and the output device is the default + // device, and the default device is the same bluetooth device, pick the + // right output device, running at the same rate and with the same protocol + // as the input. + if (!selected_output_device_id) { + cubeb_devid matched = wasapi_find_bt_handsfree_output_device(stm); + if (matched) { + selected_output_device_id = + utf8_to_wstr(reinterpret_cast<char const *>(matched)); + } + } + } + + // 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_id) { + if (std::unique_ptr<wchar_t[]> tmp = + copy_wide_string(stm->input_device_id.get())) { + XASSERT(!selected_output_device_id); + selected_output_device_id = std::move(tmp); + } else { + LOG("Failed to copy device identifier while copying input stream " + "configuration to output stream configuration to drive loopback."); + return CUBEB_ERROR; + } + } + stm->has_dummy_output = true; + } + + if (has_output(stm)) { + LOG("(%p) Setup render: device=%p", stm, selected_output_device_id.get()); + rv = setup_wasapi_stream_one_side( + stm, &stm->output_stream_params, selected_output_device_id.get(), + eRender, __uuidof(IAudioRenderClient), stm->output_client, + &stm->output_buffer_frame_count, stm->refill_event, stm->render_client, + &stm->output_mix_params, stm->output_device); + if (rv != CUBEB_OK) { + LOG("Failure to open the output side."); + return rv; + } + + HRESULT hr = 0; +#ifdef CUBEB_WASAPI_USE_IAUDIOSTREAMVOLUME + hr = stm->output_client->GetService(__uuidof(IAudioStreamVolume), + stm->audio_stream_volume.receive_vpp()); + if (FAILED(hr)) { + LOG("Could not get the IAudioStreamVolume: %lx", hr); + return CUBEB_ERROR; + } +#endif + + XASSERT(stm->frames_written == 0); + hr = stm->output_client->GetService(__uuidof(IAudioClock), + stm->audio_clock.receive_vpp()); + if (FAILED(hr)) { + LOG("Could not get the IAudioClock: %lx", hr); + return CUBEB_ERROR; + } + +#ifdef CUBEB_WASAPI_USE_IAUDIOSTREAMVOLUME + /* Restore the stream volume over a device change. */ + if (stream_set_volume(stm, stm->volume) != CUBEB_OK) { + LOG("Could not set the volume."); + return CUBEB_ERROR; + } +#endif + } + + /* If we have both input and output, we resample to + * the highest sample rate available. */ + int32_t target_sample_rate; + if (has_input(stm) && has_output(stm)) { + 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; + } else { + XASSERT(has_output(stm)); + target_sample_rate = stm->output_stream_params.rate; + } + + LOG("Target sample rate: %d", target_sample_rate); + + /* If we are playing/capturing a mono stream, we only resample one channel, + and copy it over, so we are always resampling the number + of channels of the stream, not the number of channels + that WASAPI wants. */ + cubeb_stream_params input_params = stm->input_mix_params; + input_params.channels = stm->input_stream_params.channels; + cubeb_stream_params output_params = stm->output_mix_params; + output_params.channels = stm->output_stream_params.channels; + + stm->resampler.reset(cubeb_resampler_create( + stm, has_input(stm) ? &input_params : nullptr, + has_output(stm) && !stm->has_dummy_output ? &output_params : nullptr, + target_sample_rate, wasapi_data_callback, stm->user_ptr, + stm->voice ? CUBEB_RESAMPLER_QUALITY_VOIP + : CUBEB_RESAMPLER_QUALITY_DESKTOP, + CUBEB_RESAMPLER_RECLOCK_NONE)); + if (!stm->resampler) { + LOG("Could not get a resampler"); + return CUBEB_ERROR; + } + + XASSERT(has_input(stm) || has_output(stm)); + + if (has_input(stm) && has_output(stm)) { + stm->refill_callback = refill_callback_duplex; + } else if (has_input(stm)) { + stm->refill_callback = refill_callback_input; + } else if (has_output(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, 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 rv; + + XASSERT(context && stream && (input_stream_params || output_stream_params)); + + if (output_stream_params && input_stream_params && + output_stream_params->format != input_stream_params->format) { + return CUBEB_ERROR_INVALID_FORMAT; + } + + cubeb_stream * stm = new cubeb_stream(); + auto_stream_ref stream_ref(stm); + + stm->context = context; + stm->data_callback = data_callback; + stm->state_callback = state_callback; + stm->user_ptr = user_ptr; + stm->role = eConsole; + stm->input_bluetooth_handsfree = false; + + HRESULT hr = + CoCreateInstance(__uuidof(MMDeviceEnumerator), NULL, CLSCTX_INPROC_SERVER, + IID_PPV_ARGS(stm->device_enumerator.receive())); + if (FAILED(hr)) { + LOG("Could not get device enumerator: %lx", hr); + return hr; + } + + if (input_stream_params) { + stm->input_stream_params = *input_stream_params; + stm->input_device_id = + utf8_to_wstr(reinterpret_cast<char const *>(input_device)); + } + if (output_stream_params) { + stm->output_stream_params = *output_stream_params; + stm->output_device_id = + utf8_to_wstr(reinterpret_cast<char const *>(output_device)); + } + + if (stm->output_stream_params.prefs & CUBEB_STREAM_PREF_VOICE || + stm->input_stream_params.prefs & CUBEB_STREAM_PREF_VOICE) { + stm->voice = true; + } else { + stm->voice = false; + } + + 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; + } + + stm->latency = latency_frames; + + stm->reconfigure_event = CreateEvent(NULL, 0, 0, NULL); + if (!stm->reconfigure_event) { + 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: %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: %lx", GetLastError()); + return CUBEB_ERROR; + } + + stm->shutdown_event = CreateEvent(NULL, 0, 0, NULL); + if (!stm->shutdown_event) { + 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; + } + + { + /* 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); + } + if (rv != CUBEB_OK) { + return rv; + } + + // Follow the system default devices when not specifying devices explicitly + // and CUBEB_STREAM_PREF_DISABLE_DEVICE_SWITCHING is not set. + if ((!input_device && input_stream_params && + !(input_stream_params->prefs & + CUBEB_STREAM_PREF_DISABLE_DEVICE_SWITCHING)) || + (!output_device && output_stream_params && + !(output_stream_params->prefs & + CUBEB_STREAM_PREF_DISABLE_DEVICE_SWITCHING))) { + LOG("Follow the system default input or/and output devices"); + HRESULT 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, %lx", hr); + } + } + + 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 the wasapi_stream_render_loop thread to signal that COM has been + // initialized and the stream's ref_count has been incremented. + hr = WaitForSingleObject(stm->thread_ready_event, INFINITE); + XASSERT(hr == WAIT_OBJECT_0); + CloseHandle(stm->thread_ready_event); + stm->thread_ready_event = 0; + + wasapi_stream_add_ref(stm); + *stream = stm; + + LOG("Stream init successful (%p)", *stream); + return CUBEB_OK; +} + +void +close_wasapi_stream(cubeb_stream * stm) +{ + XASSERT(stm); + + stm->stream_reset_lock.assert_current_thread_owns(); + +#ifdef CUBEB_WASAPI_USE_IAUDIOSTREAMVOLUME + stm->audio_stream_volume = nullptr; +#endif + stm->audio_clock = nullptr; + stm->render_client = nullptr; + stm->output_client = nullptr; + stm->output_device = nullptr; + + stm->capture_client = nullptr; + stm->input_client = nullptr; + stm->input_device = 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; + + stm->resampler.reset(); + stm->output_mixer.reset(); + stm->input_mixer.reset(); + stm->mix_buffer.clear(); + if (stm->linear_input_buffer) { + stm->linear_input_buffer->clear(); + } +} + +LONG +wasapi_stream_add_ref(cubeb_stream * stm) +{ + XASSERT(stm); + LONG result = InterlockedIncrement(&stm->ref_count); + LOGV("Stream ref count incremented = %i (%p)", result, stm); + return result; +} + +LONG +wasapi_stream_release(cubeb_stream * stm) +{ + XASSERT(stm); + + LONG result = InterlockedDecrement(&stm->ref_count); + LOGV("Stream ref count decremented = %i (%p)", result, stm); + if (result == 0) { + LOG("Stream ref count hit zero, destroying (%p)", stm); + + if (stm->notification_client) { + unregister_notification_client(stm); + } + + CloseHandle(stm->shutdown_event); + CloseHandle(stm->reconfigure_event); + CloseHandle(stm->refill_event); + CloseHandle(stm->input_available_event); + + CloseHandle(stm->thread); + + // The variables intialized in wasapi_stream_init, + // must be destroyed in wasapi_stream_release. + stm->linear_input_buffer.reset(); + + { + auto_lock lock(stm->stream_reset_lock); + close_wasapi_stream(stm); + } + + delete stm; + } + + return result; +} + +void +wasapi_stream_destroy(cubeb_stream * stm) +{ + XASSERT(stm); + LOG("Stream destroy called, decrementing ref count (%p)", stm); + + stop_and_join_render_thread(stm); + wasapi_stream_release(stm); +} + +enum StreamDirection { OUTPUT, INPUT }; + +int +stream_start_one_side(cubeb_stream * stm, StreamDirection dir) +{ + XASSERT(stm); + XASSERT((dir == OUTPUT && stm->output_client) || + (dir == INPUT && stm->input_client)); + + HRESULT hr = + dir == OUTPUT ? stm->output_client->Start() : stm->input_client->Start(); + if (hr == AUDCLNT_E_DEVICE_INVALIDATED) { + LOG("audioclient invalidated for %s device, reconfiguring", + dir == OUTPUT ? "output" : "input"); + + BOOL ok = ResetEvent(stm->reconfigure_event); + if (!ok) { + LOG("resetting reconfig event failed for %s stream: %lx", + dir == OUTPUT ? "output" : "input", GetLastError()); + } + + close_wasapi_stream(stm); + int r = setup_wasapi_stream(stm); + if (r != CUBEB_OK) { + LOG("reconfigure failed"); + return r; + } + + HRESULT hr2 = dir == OUTPUT ? stm->output_client->Start() + : stm->input_client->Start(); + if (FAILED(hr2)) { + 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: %lx.", + dir == OUTPUT ? "output" : "input", hr); + return CUBEB_ERROR; + } + + return CUBEB_OK; +} + +int +wasapi_stream_start(cubeb_stream * stm) +{ + auto_lock lock(stm->stream_reset_lock); + + XASSERT(stm); + XASSERT(stm->output_client || stm->input_client); + + if (stm->output_client) { + int rv = stream_start_one_side(stm, OUTPUT); + if (rv != CUBEB_OK) { + return rv; + } + } + + if (stm->input_client) { + int rv = stream_start_one_side(stm, INPUT); + if (rv != CUBEB_OK) { + return rv; + } + } + + stm->active = true; + + stm->state_callback(stm, stm->user_ptr, CUBEB_STATE_STARTED); + + return CUBEB_OK; +} + +int +wasapi_stream_stop(cubeb_stream * stm) +{ + XASSERT(stm); + HRESULT hr; + + { + auto_lock lock(stm->stream_reset_lock); + + if (stm->output_client) { + hr = stm->output_client->Stop(); + if (FAILED(hr)) { + LOG("could not stop AudioClient (output)"); + return CUBEB_ERROR; + } + } + + if (stm->input_client) { + hr = stm->input_client->Stop(); + if (FAILED(hr)) { + LOG("could not stop AudioClient (input)"); + return CUBEB_ERROR; + } + } + + stm->active = false; + + wasapi_state_callback(stm, stm->user_ptr, CUBEB_STATE_STOPPED); + } + + return CUBEB_OK; +} + +int +wasapi_stream_get_position(cubeb_stream * stm, uint64_t * position) +{ + XASSERT(stm && position); + auto_lock lock(stm->stream_reset_lock); + + if (!has_output(stm)) { + return CUBEB_ERROR; + } + + /* Calculate how far behind the current stream head the playback cursor is. */ + uint64_t stream_delay = static_cast<uint64_t>(current_stream_delay(stm) * + stm->output_stream_params.rate); + + /* Calculate the logical stream head in frames at the stream sample rate. */ + uint64_t max_pos = + stm->total_frames_written + + static_cast<uint64_t>( + round(stm->frames_written * + stream_to_mix_samplerate_ratio(stm->output_stream_params, + stm->output_mix_params))); + + *position = max_pos; + if (stream_delay <= *position) { + *position -= stream_delay; + } + + if (*position < stm->prev_position) { + *position = stm->prev_position; + } + stm->prev_position = *position; + + return CUBEB_OK; +} + +int +wasapi_stream_get_latency(cubeb_stream * stm, uint32_t * latency) +{ + XASSERT(stm && latency); + + if (!has_output(stm)) { + return CUBEB_ERROR; + } + + auto_lock lock(stm->stream_reset_lock); + + /* The GetStreamLatency method only works if the + AudioClient has been initialized. */ + if (!stm->output_client) { + LOG("get_latency: No output_client."); + return CUBEB_ERROR; + } + + REFERENCE_TIME latency_hns; + HRESULT hr = stm->output_client->GetStreamLatency(&latency_hns); + if (FAILED(hr)) { + LOG("GetStreamLatency failed %lx.", hr); + return CUBEB_ERROR; + } + // This happens on windows 10: no error, but always 0 for latency. + if (latency_hns == 0) { + LOG("GetStreamLatency returned 0, using workaround."); + double delay_s = current_stream_delay(stm); + // convert to sample-frames + *latency = delay_s * stm->output_stream_params.rate; + } else { + *latency = hns_to_frames(stm, latency_hns); + } + + LOG("Output latency %u frames.", *latency); + + return CUBEB_OK; +} + +int +wasapi_stream_get_input_latency(cubeb_stream * stm, uint32_t * latency) +{ + XASSERT(stm && latency); + + if (!has_input(stm)) { + LOG("Input latency queried on an output-only stream."); + return CUBEB_ERROR; + } + + auto_lock lock(stm->stream_reset_lock); + + if (stm->input_latency_hns == LATENCY_NOT_AVAILABLE_YET) { + LOG("Input latency not available yet."); + return CUBEB_ERROR; + } + + *latency = hns_to_frames(stm, stm->input_latency_hns); + + return CUBEB_OK; +} + +int +wasapi_stream_set_volume(cubeb_stream * stm, float volume) +{ + auto_lock lock(stm->stream_reset_lock); + + if (!has_output(stm)) { + return CUBEB_ERROR; + } + +#ifdef CUBEB_WASAPI_USE_IAUDIOSTREAMVOLUME + if (stream_set_volume(stm, volume) != CUBEB_OK) { + return CUBEB_ERROR; + } +#endif + + stm->volume = volume; + + return CUBEB_OK; +} + +static char const * +wstr_to_utf8(LPCWSTR str) +{ + 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<wchar_t const[]> +utf8_to_wstr(char const * str) +{ + int size = ::MultiByteToWideChar(CP_UTF8, 0, str, -1, nullptr, 0); + if (size <= 0) { + return nullptr; + } + + std::unique_ptr<wchar_t[]> ret(new wchar_t[size]); + ::MultiByteToWideChar(CP_UTF8, 0, str, -1, ret.get(), size); + return ret; +} + +static com_ptr<IMMDevice> +wasapi_get_device_node(IMMDeviceEnumerator * enumerator, IMMDevice * dev) +{ + 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; + } + } + + return ret; +} + +static com_heap_ptr<wchar_t> +wasapi_get_default_device_id(EDataFlow flow, ERole role, + IMMDeviceEnumerator * enumerator) +{ + com_ptr<IMMDevice> dev; + + HRESULT hr = enumerator->GetDefaultAudioEndpoint(flow, role, dev.receive()); + if (SUCCEEDED(hr)) { + wchar_t * tmp = nullptr; + if (SUCCEEDED(dev->GetId(&tmp))) { + com_heap_ptr<wchar_t> devid(tmp); + return devid; + } + } + + return nullptr; +} + +/* `ret` must be deallocated with `wasapi_destroy_device`, iff the return value + * of this function is `CUBEB_OK`. */ +int +wasapi_create_device(cubeb * ctx, cubeb_device_info & ret, + IMMDeviceEnumerator * enumerator, IMMDevice * dev, + wasapi_default_devices * defaults) +{ + com_ptr<IMMEndpoint> endpoint; + com_ptr<IMMDevice> devnode; + com_ptr<IAudioClient> client; + EDataFlow flow; + DWORD state = DEVICE_STATE_NOTPRESENT; + com_ptr<IPropertyStore> propstore; + REFERENCE_TIME def_period, min_period; + HRESULT hr; + + XASSERT(enumerator && dev && defaults); + + // zero-out to be able to safely delete the pointers to friendly_name and + // group_id at all time in this function. + PodZero(&ret, 1); + + 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.receive())); + if (FAILED(hr)) { + wasapi_destroy_device(&ret); + return CUBEB_ERROR; + } + + hr = endpoint->GetDataFlow(&flow); + if (FAILED(hr)) { + wasapi_destroy_device(&ret); + return CUBEB_ERROR; + } + + wchar_t * tmp = nullptr; + hr = dev->GetId(&tmp); + if (FAILED(hr)) { + wasapi_destroy_device(&ret); + return CUBEB_ERROR; + } + com_heap_ptr<wchar_t> device_id(tmp); + + char const * device_id_intern = intern_device_id(ctx, device_id.get()); + if (!device_id_intern) { + wasapi_destroy_device(&ret); + return CUBEB_ERROR; + } + + hr = dev->OpenPropertyStore(STGM_READ, propstore.receive()); + if (FAILED(hr)) { + wasapi_destroy_device(&ret); + return CUBEB_ERROR; + } + + hr = dev->GetState(&state); + if (FAILED(hr)) { + wasapi_destroy_device(&ret); + return CUBEB_ERROR; + } + + 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) && namevar.vt == VT_LPWSTR) { + ret.friendly_name = wstr_to_utf8(namevar.pwszVal); + } + if (!ret.friendly_name) { + // This is not fatal, but a valid string is expected in all cases. + char * empty = new char[1]; + empty[0] = '\0'; + ret.friendly_name = empty; + } + + devnode = wasapi_get_device_node(enumerator, dev); + if (devnode) { + com_ptr<IPropertyStore> ps; + hr = devnode->OpenPropertyStore(STGM_READ, ps.receive()); + if (FAILED(hr)) { + wasapi_destroy_device(&ret); + return CUBEB_ERROR; + } + + prop_variant instancevar; + hr = ps->GetValue(PKEY_Device_InstanceId, &instancevar); + if (SUCCEEDED(hr) && instancevar.vt == VT_LPWSTR) { + ret.group_id = wstr_to_utf8(instancevar.pwszVal); + } + } + + if (!ret.group_id) { + // This is not fatal, but a valid string is expected in all cases. + char * empty = new char[1]; + empty[0] = '\0'; + ret.group_id = empty; + } + + ret.preferred = CUBEB_DEVICE_PREF_NONE; + if (defaults->is_default(flow, eConsole, device_id.get())) { + ret.preferred = + (cubeb_device_pref)(ret.preferred | CUBEB_DEVICE_PREF_MULTIMEDIA | + CUBEB_DEVICE_PREF_NOTIFICATION); + } else if (defaults->is_default(flow, eCommunications, device_id.get())) { + ret.preferred = + (cubeb_device_pref)(ret.preferred | CUBEB_DEVICE_PREF_VOICE); + } + + 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; + break; + case DEVICE_STATE_UNPLUGGED: + ret.state = CUBEB_DEVICE_STATE_UNPLUGGED; + break; + default: + ret.state = CUBEB_DEVICE_STATE_DISABLED; + break; + }; + + 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 (fmtvar.blob.cbSize >= sizeof(WAVEFORMATEX)) { + WAVEFORMATEX * wfx = + reinterpret_cast<WAVEFORMATEX *>(fmtvar.blob.pBlobData); + + 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; + } + } + } + + 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); + } else { + ret.latency_lo = 0; + ret.latency_hi = 0; + } + + XASSERT(ret.friendly_name && ret.group_id); + + return CUBEB_OK; +} + +void +wasapi_destroy_device(cubeb_device_info * device) +{ + delete[] device->friendly_name; + delete[] device->group_id; +} + +static int +wasapi_enumerate_devices_internal(cubeb * context, cubeb_device_type type, + cubeb_device_collection * out, + DWORD state_mask) +{ + com_ptr<IMMDeviceEnumerator> enumerator; + com_ptr<IMMDeviceCollection> collection; + HRESULT hr; + UINT cc, i; + EDataFlow flow; + + 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 CUBEB_ERROR; + } + + wasapi_default_devices default_devices(enumerator.get()); + + 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_OUTPUT)) { + flow = eAll; + } else { + return CUBEB_ERROR; + } + + hr = enumerator->EnumAudioEndpoints(flow, state_mask, collection.receive()); + if (FAILED(hr)) { + LOG("Could not enumerate audio endpoints: %lx", hr); + return CUBEB_ERROR; + } + + hr = collection->GetCount(&cc); + if (FAILED(hr)) { + LOG("IMMDeviceCollection::GetCount() failed: %lx", hr); + return CUBEB_ERROR; + } + cubeb_device_info * devices = new cubeb_device_info[cc]; + if (!devices) + return CUBEB_ERROR; + + PodZero(devices, cc); + out->count = 0; + for (i = 0; i < cc; i++) { + 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(), &default_devices) == CUBEB_OK) { + out->count += 1; + } + } + + out->device = devices; + return CUBEB_OK; +} + +static int +wasapi_enumerate_devices(cubeb * context, cubeb_device_type type, + cubeb_device_collection * out) +{ + return wasapi_enumerate_devices_internal( + context, type, out, + DEVICE_STATE_ACTIVE | DEVICE_STATE_DISABLED | DEVICE_STATE_UNPLUGGED); +} + +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]; + wasapi_destroy_device(&dev); + } + + 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) +{ + auto_lock lock(context->lock); + 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)) { + 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; + } + } + } + + return CUBEB_OK; +} + +cubeb_ops const wasapi_ops = { + /*.init =*/wasapi_init, + /*.get_backend_id =*/wasapi_get_backend_id, + /*.get_max_channel_count =*/wasapi_get_max_channel_count, + /*.get_min_latency =*/wasapi_get_min_latency, + /*.get_preferred_sample_rate =*/wasapi_get_preferred_sample_rate, + /*.get_supported_input_processing_params =*/NULL, + /*.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_get_position =*/wasapi_stream_get_position, + /*.stream_get_latency =*/wasapi_stream_get_latency, + /*.stream_get_input_latency =*/wasapi_stream_get_input_latency, + /*.stream_set_volume =*/wasapi_stream_set_volume, + /*.stream_set_name =*/NULL, + /*.stream_get_current_device =*/NULL, + /*.stream_set_input_mute =*/NULL, + /*.stream_set_input_processing_params =*/NULL, + /*.stream_device_destroy =*/NULL, + /*.stream_register_device_changed_callback =*/NULL, + /*.register_device_collection_changed =*/ + wasapi_register_device_collection_changed, +}; +} // namespace diff --git a/media/libcubeb/src/cubeb_winmm.c b/media/libcubeb/src/cubeb_winmm.c new file mode 100644 index 0000000000..b2234a9b52 --- /dev/null +++ b/media/libcubeb/src/cubeb_winmm.c @@ -0,0 +1,1213 @@ +/* + * Copyright © 2011 Mozilla Foundation + * + * This program is made available under an ISC-style license. See the + * accompanying file LICENSE for details. + */ +#undef WINVER +#define WINVER 0x0501 +#undef WIN32_LEAN_AND_MEAN + +#include "cubeb-internal.h" +#include "cubeb/cubeb.h" +#include <malloc.h> +#include <math.h> +#include <process.h> +#include <stdio.h> +#include <stdlib.h> +#include <windows.h> + +/* clang-format off */ +/* These need to be included after windows.h */ +#include <mmreg.h> +#include <mmsystem.h> +/* clang-format on */ + +/* This is missing from the MinGW headers. Use a safe fallback. */ +#if !defined(MEMORY_ALLOCATION_ALIGNMENT) +#define MEMORY_ALLOCATION_ALIGNMENT 16 +#endif + +/**This is also missing from the MinGW headers. It also appears to be + * undocumented by Microsoft.*/ +#ifndef WAVE_FORMAT_48M08 +#define WAVE_FORMAT_48M08 0x00001000 /* 48 kHz, Mono, 8-bit */ +#endif +#ifndef WAVE_FORMAT_48M16 +#define WAVE_FORMAT_48M16 0x00002000 /* 48 kHz, Mono, 16-bit */ +#endif +#ifndef WAVE_FORMAT_48S08 +#define WAVE_FORMAT_48S08 0x00004000 /* 48 kHz, Stereo, 8-bit */ +#endif +#ifndef WAVE_FORMAT_48S16 +#define WAVE_FORMAT_48S16 0x00008000 /* 48 kHz, Stereo, 16-bit */ +#endif +#ifndef WAVE_FORMAT_96M08 +#define WAVE_FORMAT_96M08 0x00010000 /* 96 kHz, Mono, 8-bit */ +#endif +#ifndef WAVE_FORMAT_96M16 +#define WAVE_FORMAT_96M16 0x00020000 /* 96 kHz, Mono, 16-bit */ +#endif +#ifndef WAVE_FORMAT_96S08 +#define WAVE_FORMAT_96S08 0x00040000 /* 96 kHz, Stereo, 8-bit */ +#endif +#ifndef WAVE_FORMAT_96S16 +#define WAVE_FORMAT_96S16 0x00080000 /* 96 kHz, Stereo, 16-bit */ +#endif + +/**Taken from winbase.h, also not in MinGW.*/ +#ifndef STACK_SIZE_PARAM_IS_A_RESERVATION +#define STACK_SIZE_PARAM_IS_A_RESERVATION 0x00010000 // Threads only +#endif + +#ifndef DRVM_MAPPER +#define DRVM_MAPPER (0x2000) +#endif +#ifndef DRVM_MAPPER_PREFERRED_GET +#define DRVM_MAPPER_PREFERRED_GET (DRVM_MAPPER + 21) +#endif +#ifndef DRVM_MAPPER_CONSOLEVOICECOM_GET +#define DRVM_MAPPER_CONSOLEVOICECOM_GET (DRVM_MAPPER + 23) +#endif + +#define CUBEB_STREAM_MAX 32 +#define NBUFS 4 + +struct cubeb_stream_item { + SLIST_ENTRY head; + cubeb_stream * stream; +}; + +static struct cubeb_ops const winmm_ops; + +struct cubeb { + struct cubeb_ops const * ops; + HANDLE event; + HANDLE thread; + int shutdown; + PSLIST_HEADER work; + CRITICAL_SECTION lock; + unsigned int active_streams; + unsigned int minimum_latency_ms; +}; + +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; + WAVEHDR buffers[NBUFS]; + size_t buffer_size; + int next_buffer; + int free_buffers; + int shutdown; + int draining; + int error; + HANDLE event; + HWAVEOUT waveout; + CRITICAL_SECTION lock; + uint64_t written; + /* number of frames written during preroll */ + uint64_t position_base; + float soft_volume; + /* For position wrap-around handling: */ + size_t frame_size; + DWORD prev_pos_lo_dword; + DWORD pos_hi_dword; +}; + +static size_t +bytes_per_frame(cubeb_stream_params params) +{ + size_t bytes; + + switch (params.format) { + case CUBEB_SAMPLE_S16LE: + bytes = sizeof(signed short); + break; + case CUBEB_SAMPLE_FLOAT32LE: + bytes = sizeof(float); + break; + default: + XASSERT(0); + } + + return bytes * params.channels; +} + +static WAVEHDR * +winmm_get_next_buffer(cubeb_stream * stm) +{ + WAVEHDR * hdr = NULL; + + XASSERT(stm->free_buffers > 0 && stm->free_buffers <= NBUFS); + hdr = &stm->buffers[stm->next_buffer]; + XASSERT(hdr->dwFlags & WHDR_PREPARED || + (hdr->dwFlags & WHDR_DONE && !(hdr->dwFlags & WHDR_INQUEUE))); + stm->next_buffer = (stm->next_buffer + 1) % NBUFS; + stm->free_buffers -= 1; + + return hdr; +} + +static long +preroll_callback(cubeb_stream * stream, void * user, const void * inputbuffer, + void * outputbuffer, long nframes) +{ + memset((uint8_t *)outputbuffer, 0, nframes * bytes_per_frame(stream->params)); + return nframes; +} + +static void +winmm_refill_stream(cubeb_stream * stm) +{ + WAVEHDR * hdr; + long got; + long wanted; + MMRESULT r; + + ALOG("winmm_refill_stream"); + + EnterCriticalSection(&stm->lock); + if (stm->error) { + LeaveCriticalSection(&stm->lock); + return; + } + stm->free_buffers += 1; + XASSERT(stm->free_buffers > 0 && stm->free_buffers <= NBUFS); + + if (stm->draining) { + LeaveCriticalSection(&stm->lock); + if (stm->free_buffers == NBUFS) { + ALOG("winmm_refill_stream draining"); + stm->state_callback(stm, stm->user_ptr, CUBEB_STATE_DRAINED); + } + SetEvent(stm->event); + return; + } + + if (stm->shutdown) { + LeaveCriticalSection(&stm->lock); + SetEvent(stm->event); + return; + } + + hdr = winmm_get_next_buffer(stm); + + wanted = (DWORD)stm->buffer_size / bytes_per_frame(stm->params); + + /* It is assumed that the caller is holding this lock. It must be dropped + during the callback to avoid deadlocks. */ + LeaveCriticalSection(&stm->lock); + got = stm->data_callback(stm, stm->user_ptr, NULL, hdr->lpData, wanted); + EnterCriticalSection(&stm->lock); + if (got < 0) { + stm->error = 1; + LeaveCriticalSection(&stm->lock); + SetEvent(stm->event); + stm->state_callback(stm, stm->user_ptr, CUBEB_STATE_ERROR); + return; + } else if (got < wanted) { + stm->draining = 1; + } + stm->written += got; + + XASSERT(hdr->dwFlags & WHDR_PREPARED); + + hdr->dwBufferLength = got * bytes_per_frame(stm->params); + XASSERT(hdr->dwBufferLength <= stm->buffer_size); + + if (stm->soft_volume != -1.0) { + if (stm->params.format == CUBEB_SAMPLE_FLOAT32NE) { + float * b = (float *)hdr->lpData; + uint32_t i; + for (i = 0; i < got * stm->params.channels; i++) { + b[i] *= stm->soft_volume; + } + } else { + short * b = (short *)hdr->lpData; + uint32_t i; + for (i = 0; i < got * stm->params.channels; i++) { + b[i] = (short)(b[i] * stm->soft_volume); + } + } + } + + r = waveOutWrite(stm->waveout, hdr, sizeof(*hdr)); + if (r != MMSYSERR_NOERROR) { + LeaveCriticalSection(&stm->lock); + stm->state_callback(stm, stm->user_ptr, CUBEB_STATE_ERROR); + return; + } + + ALOG("winmm_refill_stream %ld frames", got); + + LeaveCriticalSection(&stm->lock); +} + +static unsigned __stdcall winmm_buffer_thread(void * user_ptr) +{ + cubeb * ctx = (cubeb *)user_ptr; + XASSERT(ctx); + + for (;;) { + DWORD r; + PSLIST_ENTRY item; + + r = WaitForSingleObject(ctx->event, INFINITE); + XASSERT(r == WAIT_OBJECT_0); + + /* Process work items in batches so that a single stream can't + starve the others by continuously adding new work to the top of + the work item stack. */ + item = InterlockedFlushSList(ctx->work); + while (item != NULL) { + PSLIST_ENTRY tmp = item; + winmm_refill_stream(((struct cubeb_stream_item *)tmp)->stream); + item = item->Next; + _aligned_free(tmp); + } + + if (ctx->shutdown) { + break; + } + } + + return 0; +} + +static void CALLBACK +winmm_buffer_callback(HWAVEOUT waveout, UINT msg, DWORD_PTR user_ptr, + DWORD_PTR p1, DWORD_PTR p2) +{ + cubeb_stream * stm = (cubeb_stream *)user_ptr; + struct cubeb_stream_item * item; + + if (msg != WOM_DONE) { + return; + } + + item = _aligned_malloc(sizeof(struct cubeb_stream_item), + MEMORY_ALLOCATION_ALIGNMENT); + XASSERT(item); + item->stream = stm; + InterlockedPushEntrySList(stm->context->work, &item->head); + + SetEvent(stm->context->event); +} + +static unsigned int +calculate_minimum_latency(void) +{ + OSVERSIONINFOEX osvi; + DWORDLONG mask; + + /* Running under Terminal Services results in underruns with low latency. */ + if (GetSystemMetrics(SM_REMOTESESSION) == TRUE) { + return 500; + } + + /* Vista's WinMM implementation underruns when less than 200ms of audio is + * buffered. */ + memset(&osvi, 0, sizeof(OSVERSIONINFOEX)); + osvi.dwOSVersionInfoSize = sizeof(OSVERSIONINFOEX); + osvi.dwMajorVersion = 6; + osvi.dwMinorVersion = 0; + + mask = 0; + VER_SET_CONDITION(mask, VER_MAJORVERSION, VER_EQUAL); + VER_SET_CONDITION(mask, VER_MINORVERSION, VER_EQUAL); + + if (VerifyVersionInfo(&osvi, VER_MAJORVERSION | VER_MINORVERSION, mask) != + 0) { + return 200; + } + + return 100; +} + +static void +winmm_destroy(cubeb * ctx); + +/*static*/ int +winmm_init(cubeb ** context, char const * context_name) +{ + cubeb * ctx; + + XASSERT(context); + *context = NULL; + + /* Don't initialize a context if there are no devices available. */ + if (waveOutGetNumDevs() == 0) { + return CUBEB_ERROR; + } + + ctx = calloc(1, sizeof(*ctx)); + XASSERT(ctx); + + ctx->ops = &winmm_ops; + + ctx->work = _aligned_malloc(sizeof(*ctx->work), MEMORY_ALLOCATION_ALIGNMENT); + XASSERT(ctx->work); + InitializeSListHead(ctx->work); + + ctx->event = CreateEvent(NULL, FALSE, FALSE, NULL); + if (!ctx->event) { + winmm_destroy(ctx); + return CUBEB_ERROR; + } + + ctx->thread = + (HANDLE)_beginthreadex(NULL, 256 * 1024, winmm_buffer_thread, ctx, + STACK_SIZE_PARAM_IS_A_RESERVATION, NULL); + if (!ctx->thread) { + winmm_destroy(ctx); + return CUBEB_ERROR; + } + + SetThreadPriority(ctx->thread, THREAD_PRIORITY_TIME_CRITICAL); + + InitializeCriticalSection(&ctx->lock); + ctx->active_streams = 0; + + ctx->minimum_latency_ms = calculate_minimum_latency(); + + *context = ctx; + + return CUBEB_OK; +} + +static char const * +winmm_get_backend_id(cubeb * ctx) +{ + return "winmm"; +} + +static void +winmm_destroy(cubeb * ctx) +{ + DWORD r; + + XASSERT(ctx->active_streams == 0); + XASSERT(!InterlockedPopEntrySList(ctx->work)); + + DeleteCriticalSection(&ctx->lock); + + if (ctx->thread) { + ctx->shutdown = 1; + SetEvent(ctx->event); + r = WaitForSingleObject(ctx->thread, INFINITE); + XASSERT(r == WAIT_OBJECT_0); + CloseHandle(ctx->thread); + } + + if (ctx->event) { + CloseHandle(ctx->event); + } + + _aligned_free(ctx->work); + + free(ctx); +} + +static void +winmm_stream_destroy(cubeb_stream * stm); + +static int +winmm_stream_init(cubeb * context, cubeb_stream ** stream, + char const * stream_name, cubeb_devid input_device, + cubeb_stream_params * input_stream_params, + cubeb_devid output_device, + cubeb_stream_params * output_stream_params, + unsigned int latency_frames, + cubeb_data_callback data_callback, + cubeb_state_callback state_callback, void * user_ptr) +{ + MMRESULT r; + WAVEFORMATEXTENSIBLE wfx; + cubeb_stream * stm; + int i; + size_t bufsz; + + XASSERT(context); + XASSERT(stream); + XASSERT(output_stream_params); + + if (input_stream_params) { + /* Capture support not yet implemented. */ + return CUBEB_ERROR_NOT_SUPPORTED; + } + + if (input_device || output_device) { + /* Device selection not yet implemented. */ + 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)); + if (output_stream_params->channels > 2) { + wfx.Format.wFormatTag = WAVE_FORMAT_EXTENSIBLE; + wfx.Format.cbSize = sizeof(wfx) - sizeof(wfx.Format); + } else { + wfx.Format.wFormatTag = WAVE_FORMAT_PCM; + if (output_stream_params->format == CUBEB_SAMPLE_FLOAT32LE) { + wfx.Format.wFormatTag = WAVE_FORMAT_IEEE_FLOAT; + } + wfx.Format.cbSize = 0; + } + wfx.Format.nChannels = output_stream_params->channels; + wfx.Format.nSamplesPerSec = output_stream_params->rate; + + /* XXX fix channel mappings */ + wfx.dwChannelMask = SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT; + + switch (output_stream_params->format) { + case CUBEB_SAMPLE_S16LE: + wfx.Format.wBitsPerSample = 16; + wfx.SubFormat = KSDATAFORMAT_SUBTYPE_PCM; + break; + case CUBEB_SAMPLE_FLOAT32LE: + wfx.Format.wBitsPerSample = 32; + wfx.SubFormat = KSDATAFORMAT_SUBTYPE_IEEE_FLOAT; + break; + default: + return CUBEB_ERROR_INVALID_FORMAT; + } + + wfx.Format.nBlockAlign = + (wfx.Format.wBitsPerSample * wfx.Format.nChannels) / 8; + wfx.Format.nAvgBytesPerSec = + wfx.Format.nSamplesPerSec * wfx.Format.nBlockAlign; + wfx.Samples.wValidBitsPerSample = wfx.Format.wBitsPerSample; + + EnterCriticalSection(&context->lock); + /* CUBEB_STREAM_MAX is a horrible hack to avoid a situation where, when + many streams are active at once, a subset of them will not consume (via + playback) or release (via waveOutReset) their buffers. */ + if (context->active_streams >= CUBEB_STREAM_MAX) { + LeaveCriticalSection(&context->lock); + return CUBEB_ERROR; + } + context->active_streams += 1; + LeaveCriticalSection(&context->lock); + + stm = calloc(1, sizeof(*stm)); + XASSERT(stm); + + stm->context = context; + + stm->params = *output_stream_params; + + // Data callback is set to the user-provided data callback after + // the initialization and potential preroll callback calls are done, because + // cubeb users don't expect the data callback to be called during + // initialization. + stm->data_callback = preroll_callback; + stm->state_callback = state_callback; + stm->user_ptr = user_ptr; + stm->written = 0; + + uint32_t latency_ms = latency_frames * 1000 / output_stream_params->rate; + + if (latency_ms < context->minimum_latency_ms) { + latency_ms = context->minimum_latency_ms; + } + + bufsz = (size_t)(stm->params.rate / 1000.0 * latency_ms * + bytes_per_frame(stm->params) / NBUFS); + if (bufsz % bytes_per_frame(stm->params) != 0) { + bufsz += + bytes_per_frame(stm->params) - (bufsz % bytes_per_frame(stm->params)); + } + XASSERT(bufsz % bytes_per_frame(stm->params) == 0); + + stm->buffer_size = bufsz; + + InitializeCriticalSection(&stm->lock); + + stm->event = CreateEvent(NULL, FALSE, FALSE, NULL); + if (!stm->event) { + winmm_stream_destroy(stm); + return CUBEB_ERROR; + } + + stm->soft_volume = -1.0; + + /* winmm_buffer_callback will be called during waveOutOpen, so all + other initialization must be complete before calling it. */ + r = waveOutOpen(&stm->waveout, WAVE_MAPPER, &wfx.Format, + (DWORD_PTR)winmm_buffer_callback, (DWORD_PTR)stm, + CALLBACK_FUNCTION); + if (r != MMSYSERR_NOERROR) { + winmm_stream_destroy(stm); + return CUBEB_ERROR; + } + + r = waveOutPause(stm->waveout); + if (r != MMSYSERR_NOERROR) { + winmm_stream_destroy(stm); + return CUBEB_ERROR; + } + + for (i = 0; i < NBUFS; ++i) { + WAVEHDR * hdr = &stm->buffers[i]; + + hdr->lpData = calloc(1, bufsz); + XASSERT(hdr->lpData); + hdr->dwBufferLength = bufsz; + hdr->dwFlags = 0; + + r = waveOutPrepareHeader(stm->waveout, hdr, sizeof(*hdr)); + if (r != MMSYSERR_NOERROR) { + winmm_stream_destroy(stm); + return CUBEB_ERROR; + } + + winmm_refill_stream(stm); + } + + stm->frame_size = bytes_per_frame(stm->params); + stm->prev_pos_lo_dword = 0; + stm->pos_hi_dword = 0; + // Set the user data callback now that preroll has finished. + stm->data_callback = data_callback; + stm->position_base = 0; + + // Offset the position by the number of frames written during preroll. + stm->position_base = stm->written; + stm->written = 0; + + *stream = stm; + + LOG("winmm_stream_init OK"); + + return CUBEB_OK; +} + +static void +winmm_stream_destroy(cubeb_stream * stm) +{ + int i; + + if (stm->waveout) { + MMTIME time; + MMRESULT r; + int device_valid; + int enqueued; + + EnterCriticalSection(&stm->lock); + stm->shutdown = 1; + + waveOutReset(stm->waveout); + + /* Don't need this value, we just want the result to detect invalid + handle/no device errors than waveOutReset doesn't seem to report. */ + time.wType = TIME_SAMPLES; + r = waveOutGetPosition(stm->waveout, &time, sizeof(time)); + device_valid = !(r == MMSYSERR_INVALHANDLE || r == MMSYSERR_NODRIVER); + + enqueued = NBUFS - stm->free_buffers; + LeaveCriticalSection(&stm->lock); + + /* Wait for all blocks to complete. */ + while (device_valid && enqueued > 0 && !stm->error) { + DWORD rv = WaitForSingleObject(stm->event, INFINITE); + XASSERT(rv == WAIT_OBJECT_0); + + EnterCriticalSection(&stm->lock); + enqueued = NBUFS - stm->free_buffers; + LeaveCriticalSection(&stm->lock); + } + + EnterCriticalSection(&stm->lock); + + for (i = 0; i < NBUFS; ++i) { + if (stm->buffers[i].dwFlags & WHDR_PREPARED) { + waveOutUnprepareHeader(stm->waveout, &stm->buffers[i], + sizeof(stm->buffers[i])); + } + } + + waveOutClose(stm->waveout); + + LeaveCriticalSection(&stm->lock); + } + + if (stm->event) { + CloseHandle(stm->event); + } + + DeleteCriticalSection(&stm->lock); + + for (i = 0; i < NBUFS; ++i) { + free(stm->buffers[i].lpData); + } + + EnterCriticalSection(&stm->context->lock); + XASSERT(stm->context->active_streams >= 1); + stm->context->active_streams -= 1; + LeaveCriticalSection(&stm->context->lock); + + free(stm); +} + +static int +winmm_get_max_channel_count(cubeb * ctx, uint32_t * max_channels) +{ + XASSERT(ctx && max_channels); + + /* We don't support more than two channels in this backend. */ + *max_channels = 2; + + return CUBEB_OK; +} + +static int +winmm_get_min_latency(cubeb * ctx, cubeb_stream_params params, + uint32_t * latency) +{ + // 100ms minimum, if we are not in a bizarre configuration. + *latency = ctx->minimum_latency_ms * params.rate / 1000; + + return CUBEB_OK; +} + +static int +winmm_get_preferred_sample_rate(cubeb * ctx, uint32_t * rate) +{ + WAVEOUTCAPS woc; + MMRESULT r; + + r = waveOutGetDevCaps(WAVE_MAPPER, &woc, sizeof(WAVEOUTCAPS)); + if (r != MMSYSERR_NOERROR) { + return CUBEB_ERROR; + } + + /* Check if we support 48kHz, but not 44.1kHz. */ + if (!(woc.dwFormats & WAVE_FORMAT_4S16) && + woc.dwFormats & WAVE_FORMAT_48S16) { + *rate = 48000; + return CUBEB_OK; + } + /* Prefer 44.1kHz between 44.1kHz and 48kHz. */ + *rate = 44100; + + return CUBEB_OK; +} + +static int +winmm_stream_start(cubeb_stream * stm) +{ + MMRESULT r; + + EnterCriticalSection(&stm->lock); + r = waveOutRestart(stm->waveout); + LeaveCriticalSection(&stm->lock); + + if (r != MMSYSERR_NOERROR) { + return CUBEB_ERROR; + } + + stm->state_callback(stm, stm->user_ptr, CUBEB_STATE_STARTED); + + return CUBEB_OK; +} + +static int +winmm_stream_stop(cubeb_stream * stm) +{ + MMRESULT r; + + EnterCriticalSection(&stm->lock); + r = waveOutPause(stm->waveout); + LeaveCriticalSection(&stm->lock); + + if (r != MMSYSERR_NOERROR) { + return CUBEB_ERROR; + } + + stm->state_callback(stm, stm->user_ptr, CUBEB_STATE_STOPPED); + + return CUBEB_OK; +} + +/* +Microsoft wave audio docs say "samples are the preferred time format in which +to represent the current position", but relying on this causes problems on +Windows XP, the only OS cubeb_winmm is used on. + +While the wdmaud.sys driver internally tracks a 64-bit position and ensures no +backward movement, the WinMM API limits the position returned from +waveOutGetPosition() to a 32-bit DWORD (this applies equally to XP x64). The +higher 32 bits are chopped off, and to an API consumer the position can appear +to move backward. + +In theory, even a 32-bit TIME_SAMPLES position should provide plenty of +playback time for typical use cases before this pseudo wrap-around, e.g: + (2^32 - 1)/48000 = ~24:51:18 for 48.0 kHz stereo; + (2^32 - 1)/44100 = ~27:03:12 for 44.1 kHz stereo. +In reality, wdmaud.sys doesn't provide a TIME_SAMPLES position at all, only a +32-bit TIME_BYTES position, from which wdmaud.drv derives TIME_SAMPLES: + SamplePos = (BytePos * 8) / BitsPerFrame, + where BitsPerFrame = Channels * BitsPerSample, +Per dom\media\AudioSampleFormat.h, desktop builds always use 32-bit FLOAT32 +samples, so the maximum for TIME_SAMPLES should be: + (2^29 - 1)/48000 = ~03:06:25; + (2^29 - 1)/44100 = ~03:22:54. +This might still be OK for typical browser usage, but there's also a bug in the +formula above: BytePos * 8 (BytePos << 3) is done on a 32-bit BytePos, without +first casting it to 64 bits, so the highest 3 bits, if set, would get shifted +out, and the maximum possible TIME_SAMPLES drops unacceptably low: + (2^26 - 1)/48000 = ~00:23:18; + (2^26 - 1)/44100 = ~00:25:22. + +To work around these limitations, we just get the position in TIME_BYTES, +recover the 64-bit value, and do our own conversion to samples. +*/ + +/* Convert chopped 32-bit waveOutGetPosition() into 64-bit true position. */ +static uint64_t +update_64bit_position(cubeb_stream * stm, DWORD pos_lo_dword) +{ + /* Caller should be holding stm->lock. */ + if (pos_lo_dword < stm->prev_pos_lo_dword) { + stm->pos_hi_dword++; + LOG("waveOutGetPosition() has wrapped around: %#lx -> %#lx", + stm->prev_pos_lo_dword, pos_lo_dword); + LOG("Wrap-around count = %#lx", stm->pos_hi_dword); + LOG("Current 64-bit position = %#llx", + (((uint64_t)stm->pos_hi_dword) << 32) | ((uint64_t)pos_lo_dword)); + } + stm->prev_pos_lo_dword = pos_lo_dword; + + return (((uint64_t)stm->pos_hi_dword) << 32) | ((uint64_t)pos_lo_dword); +} + +static int +winmm_stream_get_position(cubeb_stream * stm, uint64_t * position) +{ + MMRESULT r; + MMTIME time; + + EnterCriticalSection(&stm->lock); + /* See the long comment above for why not just use TIME_SAMPLES here. */ + time.wType = TIME_BYTES; + r = waveOutGetPosition(stm->waveout, &time, sizeof(time)); + + if (r != MMSYSERR_NOERROR || time.wType != TIME_BYTES) { + LeaveCriticalSection(&stm->lock); + return CUBEB_ERROR; + } + + uint64_t position_not_adjusted = + update_64bit_position(stm, time.u.cb) / stm->frame_size; + + // Subtract the number of frames that were written while prerolling, during + // initialization. + if (position_not_adjusted < stm->position_base) { + *position = 0; + } else { + *position = position_not_adjusted - stm->position_base; + } + + LeaveCriticalSection(&stm->lock); + + return CUBEB_OK; +} + +static int +winmm_stream_get_latency(cubeb_stream * stm, uint32_t * latency) +{ + MMRESULT r; + MMTIME time; + uint64_t written, position; + + int rv = winmm_stream_get_position(stm, &position); + if (rv != CUBEB_OK) { + return rv; + } + + EnterCriticalSection(&stm->lock); + written = stm->written; + LeaveCriticalSection(&stm->lock); + + XASSERT((written - (position / stm->frame_size)) <= UINT32_MAX); + *latency = (uint32_t)(written - (position / stm->frame_size)); + + return CUBEB_OK; +} + +static int +winmm_stream_set_volume(cubeb_stream * stm, float volume) +{ + EnterCriticalSection(&stm->lock); + stm->soft_volume = volume; + LeaveCriticalSection(&stm->lock); + return CUBEB_OK; +} + +#define MM_11025HZ_MASK \ + (WAVE_FORMAT_1M08 | WAVE_FORMAT_1M16 | WAVE_FORMAT_1S08 | WAVE_FORMAT_1S16) +#define MM_22050HZ_MASK \ + (WAVE_FORMAT_2M08 | WAVE_FORMAT_2M16 | WAVE_FORMAT_2S08 | WAVE_FORMAT_2S16) +#define MM_44100HZ_MASK \ + (WAVE_FORMAT_4M08 | WAVE_FORMAT_4M16 | WAVE_FORMAT_4S08 | WAVE_FORMAT_4S16) +#define MM_48000HZ_MASK \ + (WAVE_FORMAT_48M08 | WAVE_FORMAT_48M16 | WAVE_FORMAT_48S08 | \ + WAVE_FORMAT_48S16) +#define MM_96000HZ_MASK \ + (WAVE_FORMAT_96M08 | WAVE_FORMAT_96M16 | WAVE_FORMAT_96S08 | \ + WAVE_FORMAT_96S16) +static void +winmm_calculate_device_rate(cubeb_device_info * info, DWORD formats) +{ + if (formats & MM_11025HZ_MASK) { + info->min_rate = 11025; + info->default_rate = 11025; + info->max_rate = 11025; + } + if (formats & MM_22050HZ_MASK) { + if (info->min_rate == 0) + info->min_rate = 22050; + info->max_rate = 22050; + info->default_rate = 22050; + } + if (formats & MM_44100HZ_MASK) { + if (info->min_rate == 0) + info->min_rate = 44100; + info->max_rate = 44100; + info->default_rate = 44100; + } + if (formats & MM_48000HZ_MASK) { + if (info->min_rate == 0) + info->min_rate = 48000; + info->max_rate = 48000; + info->default_rate = 48000; + } + if (formats & MM_96000HZ_MASK) { + if (info->min_rate == 0) { + info->min_rate = 96000; + info->default_rate = 96000; + } + info->max_rate = 96000; + } +} + +#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 +winmm_query_supported_formats(UINT devid, DWORD formats, + cubeb_device_fmt * supfmt, + cubeb_device_fmt * deffmt) +{ + WAVEFORMATEXTENSIBLE wfx; + + if (formats & MM_S16_MASK) + *deffmt = *supfmt = CUBEB_DEVICE_FMT_S16LE; + else + *deffmt = *supfmt = 0; + + ZeroMemory(&wfx, sizeof(WAVEFORMATEXTENSIBLE)); + wfx.Format.wFormatTag = WAVE_FORMAT_EXTENSIBLE; + wfx.Format.nChannels = 2; + wfx.Format.nSamplesPerSec = 44100; + wfx.Format.wBitsPerSample = 32; + wfx.Format.nBlockAlign = + (wfx.Format.wBitsPerSample * wfx.Format.nChannels) / 8; + wfx.Format.nAvgBytesPerSec = + wfx.Format.nSamplesPerSec * wfx.Format.nBlockAlign; + wfx.Format.cbSize = 22; + wfx.Samples.wValidBitsPerSample = wfx.Format.wBitsPerSample; + wfx.dwChannelMask = SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT; + wfx.SubFormat = KSDATAFORMAT_SUBTYPE_IEEE_FLOAT; + if (waveOutOpen(NULL, devid, &wfx.Format, 0, 0, WAVE_FORMAT_QUERY) == + MMSYSERR_NOERROR) + *supfmt = (cubeb_device_fmt)(*supfmt | CUBEB_DEVICE_FMT_F32LE); + + return (*deffmt != 0) ? CUBEB_OK : CUBEB_ERROR; +} + +static char * +guid_to_cstr(LPGUID guid) +{ + char * ret = malloc(40); + if (!ret) { + return NULL; + } + _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], guid->Data4[4], guid->Data4[5], + guid->Data4[6], guid->Data4[7]); + return ret; +} + +static cubeb_device_pref +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)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)WAVE_MAPPER, DRVM_MAPPER_CONSOLEVOICECOM_GET, + (DWORD_PTR)&compref, + (DWORD_PTR)&status) == MMSYSERR_NOERROR && + devid == compref) + ret |= CUBEB_DEVICE_PREF_VOICE; + + return ret; +} + +static char * +device_id_idx(UINT devid) +{ + char * ret = malloc(16); + if (!ret) { + return NULL; + } + _snprintf_s(ret, 16, _TRUNCATE, "%u", devid); + return ret; +} + +static void +winmm_create_device_from_outcaps2(cubeb_device_info * ret, LPWAVEOUTCAPS2A caps, + UINT 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); + ret->vendor_name = guid_to_cstr(&caps->ManufacturerGuid); + + ret->type = CUBEB_DEVICE_TYPE_OUTPUT; + ret->state = CUBEB_DEVICE_STATE_ENABLED; + ret->preferred = winmm_query_preferred_out_device(devid); + + ret->max_channels = caps->wChannels; + winmm_calculate_device_rate(ret, caps->dwFormats); + winmm_query_supported_formats(devid, caps->dwFormats, &ret->format, + &ret->default_format); + + /* Hardcoded latency estimates... */ + ret->latency_lo = 100 * ret->default_rate / 1000; + ret->latency_hi = 200 * ret->default_rate / 1000; +} + +static void +winmm_create_device_from_outcaps(cubeb_device_info * ret, LPWAVEOUTCAPSA caps, + UINT 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; + ret->vendor_name = NULL; + + ret->type = CUBEB_DEVICE_TYPE_OUTPUT; + ret->state = CUBEB_DEVICE_STATE_ENABLED; + ret->preferred = winmm_query_preferred_out_device(devid); + + ret->max_channels = caps->wChannels; + winmm_calculate_device_rate(ret, caps->dwFormats); + winmm_query_supported_formats(devid, caps->dwFormats, &ret->format, + &ret->default_format); + + /* Hardcoded latency estimates... */ + ret->latency_lo = 100 * ret->default_rate / 1000; + ret->latency_hi = 200 * ret->default_rate / 1000; +} + +static cubeb_device_pref +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)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)WAVE_MAPPER, DRVM_MAPPER_CONSOLEVOICECOM_GET, + (DWORD_PTR)&compref, + (DWORD_PTR)&status) == MMSYSERR_NOERROR && + devid == compref) + ret |= CUBEB_DEVICE_PREF_VOICE; + + return ret; +} + +static void +winmm_create_device_from_incaps2(cubeb_device_info * ret, LPWAVEINCAPS2A caps, + UINT 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); + ret->vendor_name = guid_to_cstr(&caps->ManufacturerGuid); + + ret->type = CUBEB_DEVICE_TYPE_INPUT; + ret->state = CUBEB_DEVICE_STATE_ENABLED; + ret->preferred = winmm_query_preferred_in_device(devid); + + ret->max_channels = caps->wChannels; + winmm_calculate_device_rate(ret, caps->dwFormats); + winmm_query_supported_formats(devid, caps->dwFormats, &ret->format, + &ret->default_format); + + /* Hardcoded latency estimates... */ + ret->latency_lo = 100 * ret->default_rate / 1000; + ret->latency_hi = 200 * ret->default_rate / 1000; +} + +static void +winmm_create_device_from_incaps(cubeb_device_info * ret, LPWAVEINCAPSA caps, + UINT 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; + ret->vendor_name = NULL; + + ret->type = CUBEB_DEVICE_TYPE_INPUT; + ret->state = CUBEB_DEVICE_STATE_ENABLED; + ret->preferred = winmm_query_preferred_in_device(devid); + + ret->max_channels = caps->wChannels; + winmm_calculate_device_rate(ret, caps->dwFormats); + winmm_query_supported_formats(devid, caps->dwFormats, &ret->format, + &ret->default_format); + + /* Hardcoded latency estimates... */ + ret->latency_lo = 100 * ret->default_rate / 1000; + ret->latency_hi = 200 * ret->default_rate / 1000; +} + +static int +winmm_enumerate_devices(cubeb * context, cubeb_device_type type, + cubeb_device_collection * collection) +{ + UINT i, incount, outcount, total; + cubeb_device_info * devices; + cubeb_device_info * dev; + + outcount = waveOutGetNumDevs(); + incount = waveInGetNumDevs(); + total = outcount + incount; + + devices = calloc(total, sizeof(cubeb_device_info)); + collection->count = 0; + + if (type & CUBEB_DEVICE_TYPE_OUTPUT) { + WAVEOUTCAPSA woc; + WAVEOUTCAPS2A woc2; + + ZeroMemory(&woc, sizeof(woc)); + ZeroMemory(&woc2, sizeof(woc2)); + + for (i = 0; i < outcount; i++) { + 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; + } + } + } + + if (type & CUBEB_DEVICE_TYPE_INPUT) { + WAVEINCAPSA wic; + WAVEINCAPS2A wic2; + + ZeroMemory(&wic, sizeof(wic)); + ZeroMemory(&wic2, sizeof(wic2)); + + for (i = 0; i < incount; i++) { + 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; +} + +static struct cubeb_ops const winmm_ops = { + /*.init =*/winmm_init, + /*.get_backend_id =*/winmm_get_backend_id, + /*.get_max_channel_count=*/winmm_get_max_channel_count, + /*.get_min_latency=*/winmm_get_min_latency, + /*.get_preferred_sample_rate =*/winmm_get_preferred_sample_rate, + /*.get_supported_input_processing_params =*/NULL, + /*.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_get_position =*/winmm_stream_get_position, + /*.stream_get_latency = */ winmm_stream_get_latency, + /*.stream_get_input_latency = */ NULL, + /*.stream_set_volume =*/winmm_stream_set_volume, + /*.stream_set_name =*/NULL, + /*.stream_get_current_device =*/NULL, + /*.stream_set_input_mute =*/NULL, + /*.stream_set_input_processing_params =*/NULL, + /*.stream_device_destroy =*/NULL, + /*.stream_register_device_changed_callback=*/NULL, + /*.register_device_collection_changed =*/NULL}; diff --git a/media/libcubeb/src/moz.build b/media/libcubeb/src/moz.build new file mode 100644 index 0000000000..d7d05b5867 --- /dev/null +++ b/media/libcubeb/src/moz.build @@ -0,0 +1,114 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +DEFINES['CUBEB_GECKO_BUILD'] = True + +Library('cubeb') + +SOURCES += [ + 'cubeb.c', + 'cubeb_log.cpp', + 'cubeb_mixer.cpp', + 'cubeb_strings.c', + 'cubeb_utils.cpp' +] + +if CONFIG['MOZ_ALSA']: + SOURCES += [ + 'cubeb_alsa.c', + ] + DEFINES['USE_ALSA'] = True + +if CONFIG['MOZ_SUNAUDIO']: + SOURCES += [ + 'cubeb_sun.c', + ] + DEFINES['USE_SUN'] = True + +if ( + CONFIG["MOZ_PULSEAUDIO"] + or CONFIG["MOZ_JACK"] + or CONFIG["MOZ_AAUDIO"] + or CONFIG["MOZ_OPENSL"] + or CONFIG["MOZ_AUDIOUNIT_RUST"] + or CONFIG["MOZ_WASAPI"] +): + SOURCES += [ + 'cubeb_resampler.cpp', + ] + +if CONFIG['MOZ_PULSEAUDIO']: + DEFINES['USE_PULSE_RUST'] = True + +if CONFIG['MOZ_JACK']: + SOURCES += [ + 'cubeb_jack.cpp', + ] + USE_LIBS += [ + 'speex', + ] + DEFINES['USE_JACK'] = True + +if CONFIG['MOZ_OSS']: + SOURCES += [ + 'cubeb_oss.c', + ] + DEFINES['USE_OSS'] = True + +if CONFIG['MOZ_SNDIO']: + SOURCES += [ + 'cubeb_sndio.c', + ] + DEFINES['USE_SNDIO'] = True + if CONFIG['OS_ARCH'] == 'OpenBSD': + DEFINES['DISABLE_LIBSNDIO_DLOPEN'] = True + +if CONFIG['MOZ_AUDIOUNIT_RUST']: + SOURCES += [ + 'cubeb_audiounit.cpp', + ] + if CONFIG['MOZ_WIDGET_TOOLKIT'] == 'cocoa': + SOURCES += [ + 'cubeb_osx_run_loop.c', + ] + DEFINES['USE_AUDIOUNIT'] = True + DEFINES['USE_AUDIOUNIT_RUST'] = True + +if CONFIG['MOZ_WASAPI']: + SOURCES += [ + 'cubeb_wasapi.cpp', + 'cubeb_winmm.c', + ] + DEFINES['UNICODE'] = True + DEFINES['USE_WINMM'] = True + DEFINES['USE_WASAPI'] = True + OS_LIBS += [ + "avrt", + "ksuser", + ] + +if CONFIG['MOZ_AAUDIO'] or CONFIG['MOZ_OPENSL']: + SOURCES += ['cubeb-jni.cpp'] + +if CONFIG['MOZ_AAUDIO']: + SOURCES += ['cubeb_aaudio.cpp'] + SOURCES['cubeb_aaudio.cpp'].flags += ['-D__ANDROID_UNAVAILABLE_SYMBOLS_ARE_WEAK__'] + DEFINES['USE_AAUDIO'] = True + +if CONFIG['MOZ_OPENSL']: + SOURCES += ['cubeb_opensl.cpp'] + DEFINES['USE_OPENSL'] = True + +FINAL_LIBRARY = 'gkmedias' + +if CONFIG['MOZ_ALSA']: + 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. +AllowCompilerWarnings() diff --git a/media/libcubeb/test/README.md b/media/libcubeb/test/README.md new file mode 100644 index 0000000000..7318263989 --- /dev/null +++ b/media/libcubeb/test/README.md @@ -0,0 +1,13 @@ +Notes on writing tests. + +The googletest submodule is currently at 1.6 rather than the latest, and should +only be updated to track the version used in Gecko to make test compatibility +easier. + +Always #include "gtest/gtest.h" before *anything* else. + +All tests should be part of the "cubeb" test case, e.g. TEST(cubeb, my_test). + +Tests are built stand-alone in cubeb, but built as a single unit in Gecko, so +you must use unique names for globally visible items in each test, e.g. rather +than state_cb use state_cb_my_test. diff --git a/media/libcubeb/test/common.h b/media/libcubeb/test/common.h new file mode 100644 index 0000000000..6b06f9fcf0 --- /dev/null +++ b/media/libcubeb/test/common.h @@ -0,0 +1,180 @@ +/* + * Copyright © 2013 Sebastien Alaiwan + * + * This program is made available under an ISC-style license. See the + * accompanying file LICENSE for details. + */ +#if !defined(TEST_COMMON) +#define TEST_COMMON + +#if defined(_WIN32) +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#include <objbase.h> +#include <windows.h> +#else +#include <unistd.h> +#endif + +#include "cubeb/cubeb.h" +#include "cubeb_mixer.h" +#include "gtest/gtest.h" +#include <cstdarg> +#include <cstdio> +#include <cstring> + +template <typename T, size_t N> +constexpr size_t +ARRAY_LENGTH(T (&)[N]) +{ + return N; +} + +inline void +delay(unsigned int ms) +{ +#if defined(_WIN32) + Sleep(ms); +#else + sleep(ms / 1000); + usleep(ms % 1000 * 1000); +#endif +} + +#if !defined(M_PI) +#define M_PI 3.14159265358979323846 +#endif + +typedef struct { + char const * name; + unsigned int const channels; + uint32_t const layout; +} layout_info; + +struct backend_caps { + const char * id; + const int input_capabilities; +}; + +// This static table allows knowing if a backend has audio input capabilities. +// We don't rely on opening a stream and checking if it works, because this +// would make the test skip the tests that make use of audio input, if a +// particular backend has a bug that causes a failure during audio input stream +// creation +static backend_caps backend_capabilities[] = { + {"sun", 1}, {"wasapi", 1}, {"kai", 1}, {"audiounit", 1}, + {"audiotrack", 0}, {"opensl", 1}, {"aaudio", 1}, {"jack", 1}, + {"pulse", 1}, {"sndio", 1}, {"oss", 1}, {"winmm", 0}, + {"alsa", 1}, +}; + +inline int +can_run_audio_input_test(cubeb * ctx) +{ + cubeb_device_collection devices; + int input_device_available = 0; + int r; + /* Bail out early if the host does not have input devices. */ + r = cubeb_enumerate_devices(ctx, CUBEB_DEVICE_TYPE_INPUT, &devices); + if (r != CUBEB_OK) { + fprintf(stderr, "error enumerating devices."); + return 0; + } + + if (devices.count == 0) { + fprintf(stderr, "no input device available, skipping test.\n"); + cubeb_device_collection_destroy(ctx, &devices); + return 0; + } + + for (uint32_t i = 0; i < devices.count; i++) { + input_device_available |= + (devices.device[i].state == CUBEB_DEVICE_STATE_ENABLED); + } + + if (!input_device_available) { + fprintf(stderr, "there are input devices, but they are not " + "available, skipping\n"); + } + + cubeb_device_collection_destroy(ctx, &devices); + + int backend_has_input_capabilities; + const char * backend_id = cubeb_get_backend_id(ctx); + for (uint32_t i = 0; i < sizeof(backend_capabilities) / sizeof(backend_caps); + i++) { + if (strcmp(backend_capabilities[i].id, backend_id) == 0) { + backend_has_input_capabilities = + backend_capabilities[i].input_capabilities; + } + } + + return !!input_device_available && !!backend_has_input_capabilities; +} + +inline void +print_log(const char * msg, ...) +{ + va_list args; + va_start(args, msg); + vprintf(msg, args); + va_end(args); +} + +/** Initialize cubeb with backend override. + * Create call cubeb_init passing value for CUBEB_BACKEND env var as + * override. */ +inline int +common_init(cubeb ** ctx, char const * ctx_name) +{ +#ifdef ENABLE_NORMAL_LOG + if (cubeb_set_log_callback(CUBEB_LOG_NORMAL, print_log) != CUBEB_OK) { + fprintf(stderr, "Set normal log callback failed\n"); + } +#endif + +#ifdef ENABLE_VERBOSE_LOG + if (cubeb_set_log_callback(CUBEB_LOG_VERBOSE, print_log) != CUBEB_OK) { + fprintf(stderr, "Set verbose log callback failed\n"); + } +#endif + + int r; + char const * backend; + char const * ctx_backend; + + backend = getenv("CUBEB_BACKEND"); + r = cubeb_init(ctx, ctx_name, backend); + if (r == CUBEB_OK && backend) { + ctx_backend = cubeb_get_backend_id(*ctx); + if (strcmp(backend, ctx_backend) != 0) { + fprintf(stderr, "Requested backend `%s', got `%s'\n", backend, + ctx_backend); + } + } + + return r; +} + +#if defined(_WIN32) +class TestEnvironment : public ::testing::Environment { +public: + void SetUp() override { hr = CoInitializeEx(nullptr, COINIT_MULTITHREADED); } + + void TearDown() override + { + if (SUCCEEDED(hr)) { + CoUninitialize(); + } + } + +private: + HRESULT hr; +}; + +::testing::Environment * const foo_env = + ::testing::AddGlobalTestEnvironment(new TestEnvironment); +#endif + +#endif /* TEST_COMMON */ diff --git a/media/libcubeb/test/test_audio.cpp b/media/libcubeb/test/test_audio.cpp new file mode 100644 index 0000000000..7c99a1814a --- /dev/null +++ b/media/libcubeb/test/test_audio.cpp @@ -0,0 +1,253 @@ +/* + * Copyright © 2013 Sebastien Alaiwan <sebastien.alaiwan@gmail.com> + * + * This program is made available under an ISC-style license. See the + * accompanying file LICENSE for details. + */ + +/* libcubeb api/function exhaustive test. Plays a series of tones in different + * conditions. */ +#include "gtest/gtest.h" +#if !defined(_XOPEN_SOURCE) +#define _XOPEN_SOURCE 600 +#endif +#include "cubeb/cubeb.h" +#include <math.h> +#include <memory> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <string> + +// #define ENABLE_NORMAL_LOG +// #define ENABLE_VERBOSE_LOG +#include "common.h" + +using namespace std; + +#define MAX_NUM_CHANNELS 32 +#define VOLUME 0.2 + +float +get_frequency(int channel_index) +{ + return 220.0f * (channel_index + 1); +} + +template <typename T> +T +ConvertSample(double input); +template <> +float +ConvertSample(double input) +{ + return input; +} +template <> +short +ConvertSample(double input) +{ + return short(input * 32767.0f); +} + +/* store the phase of the generated waveform */ +struct synth_state { + synth_state(int num_channels_, float sample_rate_) + : num_channels(num_channels_), sample_rate(sample_rate_) + { + for (int i = 0; i < MAX_NUM_CHANNELS; ++i) + phase[i] = 0.0f; + } + + template <typename T> void run(T * audiobuffer, long nframes) + { + for (int c = 0; c < num_channels; ++c) { + float freq = get_frequency(c); + float phase_inc = 2.0 * M_PI * freq / sample_rate; + for (long n = 0; n < nframes; ++n) { + audiobuffer[n * num_channels + c] = + ConvertSample<T>(sin(phase[c]) * VOLUME); + phase[c] += phase_inc; + } + } + } + +private: + int num_channels; + float phase[MAX_NUM_CHANNELS]; + float sample_rate; +}; + +template <typename T> +long +data_cb(cubeb_stream * /*stream*/, void * user, const void * /*inputbuffer*/, + void * outputbuffer, long nframes) +{ + synth_state * synth = (synth_state *)user; + synth->run((T *)outputbuffer, nframes); + return nframes; +} + +void +state_cb_audio(cubeb_stream * /*stream*/, void * /*user*/, + cubeb_state /*state*/) +{ +} + +/* Our android backends don't support float, only int16. */ +int +supports_float32(string backend_id) +{ + return backend_id != "opensl" && backend_id != "audiotrack"; +} + +/* Some backends don't have code to deal with more than mono or stereo. */ +int +supports_channel_count(string backend_id, int nchannels) +{ + return nchannels <= 2 || + (backend_id != "opensl" && backend_id != "audiotrack"); +} + +int +run_test(int num_channels, int sampling_rate, int is_float) +{ + int r = CUBEB_OK; + + cubeb * ctx = NULL; + + r = common_init(&ctx, "Cubeb audio test: channels"); + if (r != CUBEB_OK) { + fprintf(stderr, "Error initializing cubeb library\n"); + return r; + } + std::unique_ptr<cubeb, decltype(&cubeb_destroy)> cleanup_cubeb_at_exit( + ctx, cubeb_destroy); + + const char * backend_id = cubeb_get_backend_id(ctx); + + if ((is_float && !supports_float32(backend_id)) || + !supports_channel_count(backend_id, num_channels)) { + /* don't treat this as a test failure. */ + return CUBEB_OK; + } + + fprintf(stderr, "Testing %d channel(s), %d Hz, %s (%s)\n", num_channels, + sampling_rate, is_float ? "float" : "short", + cubeb_get_backend_id(ctx)); + + cubeb_stream_params params; + params.format = is_float ? CUBEB_SAMPLE_FLOAT32NE : CUBEB_SAMPLE_S16NE; + params.rate = sampling_rate; + params.channels = num_channels; + params.layout = CUBEB_LAYOUT_UNDEFINED; + params.prefs = CUBEB_STREAM_PREF_NONE; + + synth_state synth(params.channels, params.rate); + + cubeb_stream * stream = NULL; + r = cubeb_stream_init(ctx, &stream, "test tone", NULL, NULL, NULL, ¶ms, + 4096, is_float ? &data_cb<float> : &data_cb<short>, + state_cb_audio, &synth); + if (r != CUBEB_OK) { + fprintf(stderr, "Error initializing cubeb stream: %d\n", r); + return r; + } + + std::unique_ptr<cubeb_stream, decltype(&cubeb_stream_destroy)> + cleanup_stream_at_exit(stream, cubeb_stream_destroy); + + cubeb_stream_start(stream); + delay(200); + cubeb_stream_stop(stream); + + return r; +} + +int +run_volume_test(int is_float) +{ + int r = CUBEB_OK; + + cubeb * ctx = NULL; + + r = common_init(&ctx, "Cubeb audio test"); + if (r != CUBEB_OK) { + fprintf(stderr, "Error initializing cubeb library\n"); + return r; + } + + std::unique_ptr<cubeb, decltype(&cubeb_destroy)> cleanup_cubeb_at_exit( + ctx, cubeb_destroy); + + const char * backend_id = cubeb_get_backend_id(ctx); + + if ((is_float && !supports_float32(backend_id))) { + /* don't treat this as a test failure. */ + return CUBEB_OK; + } + + cubeb_stream_params params; + params.format = is_float ? CUBEB_SAMPLE_FLOAT32NE : CUBEB_SAMPLE_S16NE; + params.rate = 44100; + params.channels = 2; + params.layout = CUBEB_LAYOUT_STEREO; + params.prefs = CUBEB_STREAM_PREF_NONE; + + synth_state synth(params.channels, params.rate); + + cubeb_stream * stream = NULL; + r = cubeb_stream_init(ctx, &stream, "test tone", NULL, NULL, NULL, ¶ms, + 4096, is_float ? &data_cb<float> : &data_cb<short>, + state_cb_audio, &synth); + if (r != CUBEB_OK) { + fprintf(stderr, "Error initializing cubeb stream: %d\n", r); + return r; + } + + std::unique_ptr<cubeb_stream, decltype(&cubeb_stream_destroy)> + cleanup_stream_at_exit(stream, cubeb_stream_destroy); + + fprintf(stderr, "Testing: volume\n"); + for (int i = 0; i <= 4; ++i) { + fprintf(stderr, "Volume: %d%%\n", i * 25); + + cubeb_stream_set_volume(stream, i / 4.0f); + cubeb_stream_start(stream); + delay(400); + cubeb_stream_stop(stream); + delay(100); + } + + return r; +} + +TEST(cubeb, run_volume_test_short) { ASSERT_EQ(run_volume_test(0), CUBEB_OK); } + +TEST(cubeb, run_volume_test_float) { ASSERT_EQ(run_volume_test(1), CUBEB_OK); } + +TEST(cubeb, run_channel_rate_test) +{ + unsigned int channel_values[] = { + 1, 2, 3, 4, 6, + }; + + int freq_values[] = { + 16000, + 24000, + 44100, + 48000, + }; + + for (auto channels : channel_values) { + for (auto freq : freq_values) { + ASSERT_TRUE(channels < MAX_NUM_CHANNELS); + fprintf(stderr, "--------------------------\n"); + ASSERT_EQ(run_test(channels, freq, 0), CUBEB_OK); + ASSERT_EQ(run_test(channels, freq, 1), CUBEB_OK); + } + } +} + +#undef MAX_NUM_CHANNELS +#undef VOLUME diff --git a/media/libcubeb/test/test_callback_ret.cpp b/media/libcubeb/test/test_callback_ret.cpp new file mode 100644 index 0000000000..0737b60ae8 --- /dev/null +++ b/media/libcubeb/test/test_callback_ret.cpp @@ -0,0 +1,254 @@ +/* + * Copyright � 2017 Mozilla Foundation + * + * This program is made available under an ISC-style license. See the + * accompanying file LICENSE for details. + */ + +/* libcubeb api/function test. Test that different return values from user + specified callbacks are handled correctly. */ +#include "gtest/gtest.h" +#if !defined(_XOPEN_SOURCE) +#define _XOPEN_SOURCE 600 +#endif +#include "cubeb/cubeb.h" +#include <atomic> +#include <memory> +#include <string> + +// #define ENABLE_NORMAL_LOG +// #define ENABLE_VERBOSE_LOG +#include "common.h" + +const uint32_t SAMPLE_FREQUENCY = 48000; +const cubeb_sample_format SAMPLE_FORMAT = CUBEB_SAMPLE_S16NE; + +enum test_direction { INPUT_ONLY, OUTPUT_ONLY, DUPLEX }; + +// Structure which is used by data callbacks to track the total callbacks +// executed vs the number of callbacks expected. +struct user_state_callback_ret { + std::atomic<int> cb_count{0}; + std::atomic<int> expected_cb_count{0}; + std::atomic<int> error_state{0}; +}; + +// Data callback that always returns 0 +long +data_cb_ret_zero(cubeb_stream * stream, void * user, const void * inputbuffer, + void * outputbuffer, long nframes) +{ + user_state_callback_ret * u = (user_state_callback_ret *)user; + // If this is the first time the callback has been called set our expected + // callback count + if (u->cb_count == 0) { + u->expected_cb_count = 1; + } + u->cb_count++; + if (nframes < 1) { + // This shouldn't happen + EXPECT_TRUE(false) << "nframes should not be 0 in data callback!"; + } + return 0; +} + +// Data callback that always returns nframes - 1 +long +data_cb_ret_nframes_minus_one(cubeb_stream * stream, void * user, + const void * inputbuffer, void * outputbuffer, + long nframes) +{ + user_state_callback_ret * u = (user_state_callback_ret *)user; + // If this is the first time the callback has been called set our expected + // callback count + if (u->cb_count == 0) { + u->expected_cb_count = 1; + } + u->cb_count++; + if (nframes < 1) { + // This shouldn't happen + EXPECT_TRUE(false) << "nframes should not be 0 in data callback!"; + } + if (outputbuffer != NULL) { + // If we have an output buffer insert silence + short * ob = (short *)outputbuffer; + for (long i = 0; i < nframes - 1; i++) { + ob[i] = 0; + } + } + return nframes - 1; +} + +// Data callback that always returns nframes +long +data_cb_ret_nframes(cubeb_stream * stream, void * user, + const void * inputbuffer, void * outputbuffer, long nframes) +{ + user_state_callback_ret * u = (user_state_callback_ret *)user; + u->cb_count++; + // Every callback returns nframes, so every callback is expected + u->expected_cb_count++; + if (nframes < 1) { + // This shouldn't happen + EXPECT_TRUE(false) << "nframes should not be 0 in data callback!"; + } + if (outputbuffer != NULL) { + // If we have an output buffer insert silence + short * ob = (short *)outputbuffer; + for (long i = 0; i < nframes; i++) { + ob[i] = 0; + } + } + return nframes; +} + +// Data callback that always returns CUBEB_ERROR +long +data_cb_ret_error(cubeb_stream * stream, void * user, const void * inputbuffer, + void * outputbuffer, long nframes) +{ + user_state_callback_ret * u = (user_state_callback_ret *)user; + // If this is the first time the callback has been called set our expected + // callback count + if (u->cb_count == 0) { + u->expected_cb_count = 1; + } + u->cb_count++; + if (nframes < 1) { + // This shouldn't happen + EXPECT_TRUE(false) << "nframes should not be 0 in data callback!"; + } + return CUBEB_ERROR; +} + +void +state_cb_ret(cubeb_stream * stream, void * user, cubeb_state state) +{ + if (stream == NULL) + return; + user_state_callback_ret * u = (user_state_callback_ret *)user; + + switch (state) { + case CUBEB_STATE_STARTED: + fprintf(stderr, "stream started\n"); + break; + case CUBEB_STATE_STOPPED: + fprintf(stderr, "stream stopped\n"); + break; + case CUBEB_STATE_DRAINED: + fprintf(stderr, "stream drained\n"); + break; + case CUBEB_STATE_ERROR: + fprintf(stderr, "stream error\n"); + u->error_state.fetch_add(1); + break; + default: + fprintf(stderr, "unknown stream state %d\n", state); + } +} + +void +run_test_callback(test_direction direction, cubeb_data_callback data_cb, + const std::string & test_desc) +{ + cubeb * ctx; + cubeb_stream * stream; + cubeb_stream_params input_params; + cubeb_stream_params output_params; + int r; + user_state_callback_ret user_state; + uint32_t latency_frames = 0; + + r = common_init(&ctx, "Cubeb callback return value example"); + ASSERT_EQ(r, CUBEB_OK) << "Error initializing cubeb library"; + + std::unique_ptr<cubeb, decltype(&cubeb_destroy)> cleanup_cubeb_at_exit( + ctx, cubeb_destroy); + + if ((direction == INPUT_ONLY || direction == DUPLEX) && + !can_run_audio_input_test(ctx)) { + /* This test needs an available input device, skip it if this host does not + * have one or if the backend doesn't implement input. */ + return; + } + + // Setup all params, but only pass them later as required by direction + input_params.format = SAMPLE_FORMAT; + input_params.rate = SAMPLE_FREQUENCY; + input_params.channels = 1; + input_params.layout = CUBEB_LAYOUT_MONO; + input_params.prefs = CUBEB_STREAM_PREF_NONE; + output_params = input_params; + + r = cubeb_get_min_latency(ctx, &input_params, &latency_frames); + if (r != CUBEB_OK) { + // not fatal + latency_frames = 1024; + } + + switch (direction) { + case INPUT_ONLY: + r = cubeb_stream_init(ctx, &stream, "Cubeb callback ret input", NULL, + &input_params, NULL, NULL, latency_frames, data_cb, + state_cb_ret, &user_state); + break; + case OUTPUT_ONLY: + r = cubeb_stream_init(ctx, &stream, "Cubeb callback ret output", NULL, NULL, + NULL, &output_params, latency_frames, data_cb, + state_cb_ret, &user_state); + break; + case DUPLEX: + r = cubeb_stream_init(ctx, &stream, "Cubeb callback ret duplex", NULL, + &input_params, NULL, &output_params, latency_frames, + data_cb, state_cb_ret, &user_state); + break; + default: + ASSERT_TRUE(false) << "Unrecognized test direction!"; + } + EXPECT_EQ(r, CUBEB_OK) << "Error initializing cubeb stream"; + + std::unique_ptr<cubeb_stream, decltype(&cubeb_stream_destroy)> + cleanup_stream_at_exit(stream, cubeb_stream_destroy); + + cubeb_stream_start(stream); + delay(100); + cubeb_stream_stop(stream); + + ASSERT_EQ(user_state.expected_cb_count, user_state.cb_count) + << "Callback called unexpected number of times for " << test_desc << "!"; + // TODO: On some test configurations, the data_callback is never called. + if (data_cb == data_cb_ret_error && user_state.cb_count != 0) { + ASSERT_EQ(user_state.error_state, 1) << "Callback expected error state"; + } +} + +TEST(cubeb, test_input_callback) +{ + run_test_callback(INPUT_ONLY, data_cb_ret_zero, "input only, return 0"); + run_test_callback(INPUT_ONLY, data_cb_ret_nframes_minus_one, + "input only, return nframes - 1"); + run_test_callback(INPUT_ONLY, data_cb_ret_nframes, + "input only, return nframes"); + run_test_callback(INPUT_ONLY, data_cb_ret_error, + "input only, return CUBEB_ERROR"); +} + +TEST(cubeb, test_output_callback) +{ + run_test_callback(OUTPUT_ONLY, data_cb_ret_zero, "output only, return 0"); + run_test_callback(OUTPUT_ONLY, data_cb_ret_nframes_minus_one, + "output only, return nframes - 1"); + run_test_callback(OUTPUT_ONLY, data_cb_ret_nframes, + "output only, return nframes"); + run_test_callback(OUTPUT_ONLY, data_cb_ret_error, + "output only, return CUBEB_ERROR"); +} + +TEST(cubeb, test_duplex_callback) +{ + run_test_callback(DUPLEX, data_cb_ret_zero, "duplex, return 0"); + run_test_callback(DUPLEX, data_cb_ret_nframes_minus_one, + "duplex, return nframes - 1"); + run_test_callback(DUPLEX, data_cb_ret_nframes, "duplex, return nframes"); + run_test_callback(DUPLEX, data_cb_ret_error, "duplex, return CUBEB_ERROR"); +} diff --git a/media/libcubeb/test/test_device_changed_callback.cpp b/media/libcubeb/test/test_device_changed_callback.cpp new file mode 100644 index 0000000000..ddf810da65 --- /dev/null +++ b/media/libcubeb/test/test_device_changed_callback.cpp @@ -0,0 +1,128 @@ +/* + * Copyright © 2018 Mozilla Foundation + * + * This program is made available under an ISC-style license. See the + * accompanying file LICENSE for details. + */ + +/* libcubeb api/function test. Check behaviors of registering device changed + * callbacks for the streams. */ +#include "gtest/gtest.h" +#if !defined(_XOPEN_SOURCE) +#define _XOPEN_SOURCE 600 +#endif +#include "cubeb/cubeb.h" +#include <memory> +#include <stdio.h> + +// #define ENABLE_NORMAL_LOG +// #define ENABLE_VERBOSE_LOG +#include "common.h" + +#define SAMPLE_FREQUENCY 48000 +#define STREAM_FORMAT CUBEB_SAMPLE_FLOAT32LE +#define INPUT_CHANNELS 1 +#define INPUT_LAYOUT CUBEB_LAYOUT_MONO +#define OUTPUT_CHANNELS 2 +#define OUTPUT_LAYOUT CUBEB_LAYOUT_STEREO + +long +data_callback(cubeb_stream * stream, void * user, const void * inputbuffer, + void * outputbuffer, long nframes) +{ + return 0; +} + +void +state_callback(cubeb_stream * stream, void * user, cubeb_state state) +{ +} + +void +device_changed_callback(void * user) +{ + fprintf(stderr, "device changed callback\n"); + ASSERT_TRUE(false) << "Error: device changed callback" + " called without changing devices"; +} + +void +test_registering_null_callback_twice(cubeb_stream * stream) +{ + int r = cubeb_stream_register_device_changed_callback(stream, nullptr); + if (r == CUBEB_ERROR_NOT_SUPPORTED) { + return; + } + ASSERT_EQ(r, CUBEB_OK) << "Error registering null device changed callback"; + + r = cubeb_stream_register_device_changed_callback(stream, nullptr); + ASSERT_EQ(r, CUBEB_OK) + << "Error registering null device changed callback again"; +} + +void +test_registering_and_unregistering_callback(cubeb_stream * stream) +{ + int r = cubeb_stream_register_device_changed_callback( + stream, device_changed_callback); + if (r == CUBEB_ERROR_NOT_SUPPORTED) { + return; + } + ASSERT_EQ(r, CUBEB_OK) << "Error registering device changed callback"; + + r = cubeb_stream_register_device_changed_callback(stream, nullptr); + ASSERT_EQ(r, CUBEB_OK) << "Error unregistering device changed callback"; +} + +TEST(cubeb, device_changed_callbacks) +{ + cubeb * ctx; + cubeb_stream * stream; + cubeb_stream_params input_params; + cubeb_stream_params output_params; + int r = CUBEB_OK; + uint32_t latency_frames = 0; + + r = common_init(&ctx, "Cubeb duplex example with device change"); + ASSERT_EQ(r, CUBEB_OK) << "Error initializing cubeb library"; + + if (!can_run_audio_input_test(ctx)) { + return; + } + + std::unique_ptr<cubeb, decltype(&cubeb_destroy)> cleanup_cubeb_at_exit( + ctx, cubeb_destroy); + + /* typical user-case: mono input, stereo output, low latency. */ + input_params.format = STREAM_FORMAT; + input_params.rate = SAMPLE_FREQUENCY; + input_params.channels = INPUT_CHANNELS; + input_params.layout = INPUT_LAYOUT; + input_params.prefs = CUBEB_STREAM_PREF_NONE; + output_params.format = STREAM_FORMAT; + output_params.rate = SAMPLE_FREQUENCY; + output_params.channels = OUTPUT_CHANNELS; + output_params.layout = OUTPUT_LAYOUT; + output_params.prefs = CUBEB_STREAM_PREF_NONE; + + r = cubeb_get_min_latency(ctx, &output_params, &latency_frames); + ASSERT_EQ(r, CUBEB_OK) << "Could not get minimal latency"; + + r = cubeb_stream_init(ctx, &stream, "Cubeb duplex", NULL, &input_params, NULL, + &output_params, latency_frames, data_callback, + state_callback, nullptr); + ASSERT_EQ(r, CUBEB_OK) << "Error initializing cubeb stream"; + + test_registering_null_callback_twice(stream); + + test_registering_and_unregistering_callback(stream); + + cubeb_stream_destroy(stream); +} + +#undef SAMPLE_FREQUENCY +#undef STREAM_FORMAT +#undef INPUT_CHANNELS +#undef INPUT_LAYOUT +#undef OUTPUT_CHANNELS +#undef OUTPUT_LAYOUT diff --git a/media/libcubeb/test/test_devices.cpp b/media/libcubeb/test/test_devices.cpp new file mode 100644 index 0000000000..d0eab2da67 --- /dev/null +++ b/media/libcubeb/test/test_devices.cpp @@ -0,0 +1,264 @@ +/* + * Copyright © 2015 Haakon Sporsheim <haakon.sporsheim@telenordigital.com> + * + * This program is made available under an ISC-style license. See the + * accompanying file LICENSE for details. + */ + +/* libcubeb enumerate device test/example. + * Prints out a list of devices enumerated. */ +#include "cubeb/cubeb.h" +#include "gtest/gtest.h" +#include <memory> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +// #define ENABLE_NORMAL_LOG +// #define ENABLE_VERBOSE_LOG +#include "common.h" + +static long +data_cb_duplex(cubeb_stream * stream, void * user, const void * inputbuffer, + void * outputbuffer, long nframes) +{ + // noop, unused + return 0; +} + +static void +state_cb_duplex(cubeb_stream * stream, void * /*user*/, cubeb_state state) +{ + // noop, unused +} + +static void +print_device_info(cubeb_device_info * info, FILE * f) +{ + char devfmts[64] = ""; + const char *devtype, *devstate, *devdeffmt; + + switch (info->type) { + case CUBEB_DEVICE_TYPE_INPUT: + devtype = "input"; + break; + case CUBEB_DEVICE_TYPE_OUTPUT: + devtype = "output"; + break; + case CUBEB_DEVICE_TYPE_UNKNOWN: + default: + devtype = "unknown?"; + break; + }; + + switch (info->state) { + case CUBEB_DEVICE_STATE_DISABLED: + devstate = "disabled"; + break; + case CUBEB_DEVICE_STATE_UNPLUGGED: + devstate = "unplugged"; + break; + case CUBEB_DEVICE_STATE_ENABLED: + devstate = "enabled"; + break; + default: + devstate = "unknown?"; + break; + }; + + switch (info->default_format) { + case CUBEB_DEVICE_FMT_S16LE: + devdeffmt = "S16LE"; + break; + case CUBEB_DEVICE_FMT_S16BE: + devdeffmt = "S16BE"; + break; + case CUBEB_DEVICE_FMT_F32LE: + devdeffmt = "F32LE"; + break; + case CUBEB_DEVICE_FMT_F32BE: + devdeffmt = "F32BE"; + break; + default: + devdeffmt = "unknown?"; + break; + }; + + if (info->format & CUBEB_DEVICE_FMT_S16LE) + strcat(devfmts, " S16LE"); + if (info->format & CUBEB_DEVICE_FMT_S16BE) + strcat(devfmts, " S16BE"); + if (info->format & CUBEB_DEVICE_FMT_F32LE) + strcat(devfmts, " F32LE"); + if (info->format & CUBEB_DEVICE_FMT_F32BE) + strcat(devfmts, " F32BE"); + + fprintf(f, + "dev: \"%s\"%s\n" + "\tName: \"%s\"\n" + "\tGroup: \"%s\"\n" + "\tVendor: \"%s\"\n" + "\tType: %s\n" + "\tState: %s\n" + "\tCh: %u\n" + "\tFormat: %s (0x%x) (default: %s)\n" + "\tRate: %u - %u (default: %u)\n" + "\tLatency: lo %u frames, hi %u frames\n", + info->device_id, info->preferred ? " (PREFERRED)" : "", + info->friendly_name, info->group_id, info->vendor_name, devtype, + devstate, info->max_channels, + (devfmts[0] == '\0') ? devfmts : devfmts + 1, + (unsigned int)info->format, devdeffmt, info->min_rate, info->max_rate, + info->default_rate, info->latency_lo, info->latency_hi); +} + +static void +print_device_collection(cubeb_device_collection * collection, FILE * f) +{ + uint32_t i; + + for (i = 0; i < collection->count; i++) + print_device_info(&collection->device[i], f); +} + +TEST(cubeb, destroy_default_collection) +{ + int r; + cubeb * ctx = NULL; + cubeb_device_collection collection{nullptr, 0}; + + r = common_init(&ctx, "Cubeb audio test"); + ASSERT_EQ(r, CUBEB_OK) << "Error initializing cubeb library"; + + std::unique_ptr<cubeb, decltype(&cubeb_destroy)> cleanup_cubeb_at_exit( + ctx, cubeb_destroy); + + ASSERT_EQ(collection.device, nullptr); + ASSERT_EQ(collection.count, (size_t)0); + + r = cubeb_device_collection_destroy(ctx, &collection); + if (r != CUBEB_ERROR_NOT_SUPPORTED) { + ASSERT_EQ(r, CUBEB_OK); + ASSERT_EQ(collection.device, nullptr); + ASSERT_EQ(collection.count, (size_t)0); + } +} + +TEST(cubeb, enumerate_devices) +{ + int r; + cubeb * ctx = NULL; + cubeb_device_collection collection; + + r = common_init(&ctx, "Cubeb audio test"); + ASSERT_EQ(r, CUBEB_OK) << "Error initializing cubeb library"; + + std::unique_ptr<cubeb, decltype(&cubeb_destroy)> cleanup_cubeb_at_exit( + ctx, cubeb_destroy); + + fprintf(stdout, "Enumerating input devices for backend %s\n", + cubeb_get_backend_id(ctx)); + + r = cubeb_enumerate_devices(ctx, CUBEB_DEVICE_TYPE_INPUT, &collection); + if (r == CUBEB_ERROR_NOT_SUPPORTED) { + fprintf(stderr, "Device enumeration not supported" + " for this backend, skipping this test.\n"); + return; + } + ASSERT_EQ(r, CUBEB_OK) << "Error enumerating devices " << r; + + fprintf(stdout, "Found %zu input devices\n", collection.count); + print_device_collection(&collection, stdout); + cubeb_device_collection_destroy(ctx, &collection); + + fprintf(stdout, "Enumerating output devices for backend %s\n", + cubeb_get_backend_id(ctx)); + + r = cubeb_enumerate_devices(ctx, CUBEB_DEVICE_TYPE_OUTPUT, &collection); + ASSERT_EQ(r, CUBEB_OK) << "Error enumerating devices " << r; + + fprintf(stdout, "Found %zu output devices\n", collection.count); + print_device_collection(&collection, stdout); + cubeb_device_collection_destroy(ctx, &collection); + + uint32_t count_before_creating_duplex_stream; + r = cubeb_enumerate_devices(ctx, CUBEB_DEVICE_TYPE_OUTPUT, &collection); + ASSERT_EQ(r, CUBEB_OK) << "Error enumerating devices " << r; + count_before_creating_duplex_stream = collection.count; + cubeb_device_collection_destroy(ctx, &collection); + + if (!can_run_audio_input_test(ctx)) { + return; + } + cubeb_stream * stream; + cubeb_stream_params input_params; + cubeb_stream_params output_params; + + input_params.format = output_params.format = CUBEB_SAMPLE_FLOAT32NE; + input_params.rate = output_params.rate = 48000; + input_params.channels = output_params.channels = 1; + input_params.layout = output_params.layout = CUBEB_LAYOUT_MONO; + input_params.prefs = output_params.prefs = CUBEB_STREAM_PREF_NONE; + + r = cubeb_stream_init(ctx, &stream, "Cubeb duplex", NULL, &input_params, NULL, + &output_params, 1024, data_cb_duplex, state_cb_duplex, + nullptr); + + ASSERT_EQ(r, CUBEB_OK) << "Error initializing cubeb stream"; + + r = cubeb_enumerate_devices(ctx, CUBEB_DEVICE_TYPE_OUTPUT, &collection); + ASSERT_EQ(r, CUBEB_OK) << "Error enumerating devices " << r; + ASSERT_EQ(count_before_creating_duplex_stream, collection.count); + cubeb_device_collection_destroy(ctx, &collection); + + cubeb_stream_destroy(stream); +} + +TEST(cubeb, stream_get_current_device) +{ + cubeb * ctx = NULL; + int r = common_init(&ctx, "Cubeb audio test"); + ASSERT_EQ(r, CUBEB_OK) << "Error initializing cubeb library"; + + std::unique_ptr<cubeb, decltype(&cubeb_destroy)> cleanup_cubeb_at_exit( + ctx, cubeb_destroy); + + fprintf(stdout, "Getting current devices for backend %s\n", + cubeb_get_backend_id(ctx)); + + if (!can_run_audio_input_test(ctx)) { + return; + } + + cubeb_stream * stream = NULL; + cubeb_stream_params input_params; + cubeb_stream_params output_params; + + input_params.format = output_params.format = CUBEB_SAMPLE_FLOAT32NE; + input_params.rate = output_params.rate = 48000; + input_params.channels = output_params.channels = 1; + input_params.layout = output_params.layout = CUBEB_LAYOUT_MONO; + input_params.prefs = output_params.prefs = CUBEB_STREAM_PREF_NONE; + + r = cubeb_stream_init(ctx, &stream, "Cubeb duplex", NULL, &input_params, NULL, + &output_params, 1024, data_cb_duplex, state_cb_duplex, + nullptr); + ASSERT_EQ(r, CUBEB_OK) << "Error initializing cubeb stream"; + std::unique_ptr<cubeb_stream, decltype(&cubeb_stream_destroy)> + cleanup_stream_at_exit(stream, cubeb_stream_destroy); + + cubeb_device * device; + r = cubeb_stream_get_current_device(stream, &device); + if (r == CUBEB_ERROR_NOT_SUPPORTED) { + fprintf(stderr, "Getting current device is not supported" + " for this backend, skipping this test.\n"); + return; + } + ASSERT_EQ(r, CUBEB_OK) << "Error getting current devices"; + + fprintf(stdout, "Current output device: %s\n", device->output_name); + fprintf(stdout, "Current input device: %s\n", device->input_name); + + r = cubeb_stream_device_destroy(stream, device); + ASSERT_EQ(r, CUBEB_OK) << "Error destroying current devices"; +} diff --git a/media/libcubeb/test/test_duplex.cpp b/media/libcubeb/test/test_duplex.cpp new file mode 100644 index 0000000000..6a7ca98b1f --- /dev/null +++ b/media/libcubeb/test/test_duplex.cpp @@ -0,0 +1,380 @@ +/* + * Copyright © 2016 Mozilla Foundation + * + * This program is made available under an ISC-style license. See the + * accompanying file LICENSE for details. + */ + +/* libcubeb api/function test. Loops input back to output and check audio + * is flowing. */ +#include "gtest/gtest.h" +#if !defined(_XOPEN_SOURCE) +#define _XOPEN_SOURCE 600 +#endif +#include "cubeb/cubeb.h" +#include <atomic> +#include <math.h> +#include <memory> +#include <stdio.h> +#include <stdlib.h> + +#include "mozilla/gtest/MozHelpers.h" + +// #define ENABLE_NORMAL_LOG +// #define ENABLE_VERBOSE_LOG +#include "common.h" + +#define SAMPLE_FREQUENCY 48000 +#define STREAM_FORMAT CUBEB_SAMPLE_FLOAT32LE +#define INPUT_CHANNELS 1 +#define INPUT_LAYOUT CUBEB_LAYOUT_MONO +#define OUTPUT_CHANNELS 2 +#define OUTPUT_LAYOUT CUBEB_LAYOUT_STEREO + +struct user_state_duplex { + std::atomic<int> invalid_audio_value{0}; +}; + +long +data_cb_duplex(cubeb_stream * stream, void * user, const void * inputbuffer, + void * outputbuffer, long nframes) +{ + user_state_duplex * u = reinterpret_cast<user_state_duplex *>(user); + float * ib = (float *)inputbuffer; + float * ob = (float *)outputbuffer; + + if (stream == NULL || inputbuffer == NULL || outputbuffer == NULL) { + return CUBEB_ERROR; + } + + // Loop back: upmix the single input channel to the two output channels, + // checking if there is noise in the process. + long output_index = 0; + for (long i = 0; i < nframes; i++) { + if (ib[i] <= -1.0 || ib[i] >= 1.0) { + u->invalid_audio_value = 1; + } + ob[output_index] = ob[output_index + 1] = ib[i]; + output_index += 2; + } + + return nframes; +} + +void +state_cb_duplex(cubeb_stream * stream, void * /*user*/, cubeb_state state) +{ + if (stream == NULL) + return; + + switch (state) { + case CUBEB_STATE_STARTED: + fprintf(stderr, "stream started\n"); + break; + case CUBEB_STATE_STOPPED: + fprintf(stderr, "stream stopped\n"); + break; + case CUBEB_STATE_DRAINED: + fprintf(stderr, "stream drained\n"); + break; + default: + fprintf(stderr, "unknown stream state %d\n", state); + } + + return; +} + +TEST(cubeb, duplex) +{ + cubeb * ctx; + cubeb_stream * stream; + cubeb_stream_params input_params; + cubeb_stream_params output_params; + int r; + user_state_duplex stream_state; + uint32_t latency_frames = 0; + + r = common_init(&ctx, "Cubeb duplex example"); + ASSERT_EQ(r, CUBEB_OK) << "Error initializing cubeb library"; + + std::unique_ptr<cubeb, decltype(&cubeb_destroy)> cleanup_cubeb_at_exit( + ctx, cubeb_destroy); + + /* This test needs an available input device, skip it if this host does not + * have one. */ + if (!can_run_audio_input_test(ctx)) { + return; + } + + /* typical user-case: mono input, stereo output, low latency. */ + input_params.format = STREAM_FORMAT; + input_params.rate = SAMPLE_FREQUENCY; + input_params.channels = INPUT_CHANNELS; + input_params.layout = INPUT_LAYOUT; + input_params.prefs = CUBEB_STREAM_PREF_NONE; + output_params.format = STREAM_FORMAT; + output_params.rate = SAMPLE_FREQUENCY; + output_params.channels = OUTPUT_CHANNELS; + output_params.layout = OUTPUT_LAYOUT; + output_params.prefs = CUBEB_STREAM_PREF_NONE; + + r = cubeb_get_min_latency(ctx, &output_params, &latency_frames); + ASSERT_EQ(r, CUBEB_OK) << "Could not get minimal latency"; + + r = cubeb_stream_init(ctx, &stream, "Cubeb duplex", NULL, &input_params, NULL, + &output_params, latency_frames, data_cb_duplex, + state_cb_duplex, &stream_state); + ASSERT_EQ(r, CUBEB_OK) << "Error initializing cubeb stream"; + + std::unique_ptr<cubeb_stream, decltype(&cubeb_stream_destroy)> + cleanup_stream_at_exit(stream, cubeb_stream_destroy); + + cubeb_stream_start(stream); + delay(500); + cubeb_stream_stop(stream); + + ASSERT_FALSE(stream_state.invalid_audio_value.load()); +} + +void +device_collection_changed_callback(cubeb * context, void * user) +{ + fprintf(stderr, "collection changed callback\n"); + ASSERT_TRUE(false) << "Error: device collection changed callback" + " called when opening a stream"; +} + +void +duplex_collection_change_impl(cubeb * ctx) +{ + cubeb_stream * stream; + cubeb_stream_params input_params; + cubeb_stream_params output_params; + int r; + uint32_t latency_frames = 0; + + r = cubeb_register_device_collection_changed( + ctx, static_cast<cubeb_device_type>(CUBEB_DEVICE_TYPE_INPUT), + device_collection_changed_callback, nullptr); + ASSERT_EQ(r, CUBEB_OK) << "Error initializing cubeb stream"; + + /* typical user-case: mono input, stereo output, low latency. */ + input_params.format = STREAM_FORMAT; + input_params.rate = SAMPLE_FREQUENCY; + input_params.channels = INPUT_CHANNELS; + input_params.layout = INPUT_LAYOUT; + input_params.prefs = CUBEB_STREAM_PREF_NONE; + output_params.format = STREAM_FORMAT; + output_params.rate = SAMPLE_FREQUENCY; + output_params.channels = OUTPUT_CHANNELS; + output_params.layout = OUTPUT_LAYOUT; + output_params.prefs = CUBEB_STREAM_PREF_NONE; + + r = cubeb_get_min_latency(ctx, &output_params, &latency_frames); + ASSERT_EQ(r, CUBEB_OK) << "Could not get minimal latency"; + + r = cubeb_stream_init(ctx, &stream, "Cubeb duplex", NULL, &input_params, NULL, + &output_params, latency_frames, data_cb_duplex, + state_cb_duplex, nullptr); + ASSERT_EQ(r, CUBEB_OK) << "Error initializing cubeb stream"; + cubeb_stream_destroy(stream); +} + +TEST(cubeb, duplex_collection_change) +{ + cubeb * ctx; + int r; + + r = common_init(&ctx, "Cubeb duplex example with collection change"); + ASSERT_EQ(r, CUBEB_OK) << "Error initializing cubeb library"; + std::unique_ptr<cubeb, decltype(&cubeb_destroy)> cleanup_cubeb_at_exit( + ctx, cubeb_destroy); + + /* This test needs an available input device, skip it if this host does not + * have one. */ + if (!can_run_audio_input_test(ctx)) { + return; + } + + duplex_collection_change_impl(ctx); + r = cubeb_register_device_collection_changed( + ctx, static_cast<cubeb_device_type>(CUBEB_DEVICE_TYPE_INPUT), nullptr, + nullptr); + ASSERT_EQ(r, CUBEB_OK); +} + +TEST(cubeb, duplex_collection_change_no_unregister) +{ + cubeb * ctx; + int r; + + mozilla::gtest::DisableCrashReporter(); + + r = common_init(&ctx, "Cubeb duplex example with collection change"); + ASSERT_EQ(r, CUBEB_OK) << "Error initializing cubeb library"; + + /* This test needs an available input device, skip it if this host does not + * have one. */ + if (!can_run_audio_input_test(ctx)) { + cubeb_destroy(ctx); + return; + } + + std::unique_ptr<cubeb, decltype(&cubeb_destroy)> cleanup_cubeb_at_exit( + ctx, [](cubeb * p) noexcept { EXPECT_DEATH(cubeb_destroy(p), ""); }); + + duplex_collection_change_impl(ctx); +} + +long +data_cb_input(cubeb_stream * stream, void * user, const void * inputbuffer, + void * outputbuffer, long nframes) +{ + if (stream == NULL || inputbuffer == NULL || outputbuffer != NULL) { + return CUBEB_ERROR; + } + + return nframes; +} + +void +state_cb_input(cubeb_stream * stream, void * /*user*/, cubeb_state state) +{ + if (stream == NULL) + return; + + switch (state) { + case CUBEB_STATE_STARTED: + fprintf(stderr, "stream started\n"); + break; + case CUBEB_STATE_STOPPED: + fprintf(stderr, "stream stopped\n"); + break; + case CUBEB_STATE_DRAINED: + fprintf(stderr, "stream drained\n"); + break; + case CUBEB_STATE_ERROR: + fprintf(stderr, "stream runs into error state\n"); + break; + default: + fprintf(stderr, "unknown stream state %d\n", state); + } + + return; +} + +std::vector<cubeb_devid> +get_devices(cubeb * ctx, cubeb_device_type type) +{ + std::vector<cubeb_devid> devices; + + cubeb_device_collection collection; + int r = cubeb_enumerate_devices(ctx, type, &collection); + + if (r != CUBEB_OK) { + fprintf(stderr, "Failed to enumerate devices\n"); + return devices; + } + + for (uint32_t i = 0; i < collection.count; i++) { + if (collection.device[i].state == CUBEB_DEVICE_STATE_ENABLED) { + devices.emplace_back(collection.device[i].devid); + } + } + + cubeb_device_collection_destroy(ctx, &collection); + + return devices; +} + +TEST(cubeb, one_duplex_one_input) +{ + cubeb * ctx; + cubeb_stream * duplex_stream; + cubeb_stream_params input_params; + cubeb_stream_params output_params; + int r; + user_state_duplex duplex_stream_state; + uint32_t latency_frames = 0; + + r = common_init(&ctx, "Cubeb duplex example"); + ASSERT_EQ(r, CUBEB_OK) << "Error initializing cubeb library"; + + std::unique_ptr<cubeb, decltype(&cubeb_destroy)> cleanup_cubeb_at_exit( + ctx, cubeb_destroy); + + /* This test needs at least two available input devices. */ + std::vector<cubeb_devid> input_devices = + get_devices(ctx, CUBEB_DEVICE_TYPE_INPUT); + if (input_devices.size() < 2) { + return; + } + + /* This test needs at least one available output device. */ + std::vector<cubeb_devid> output_devices = + get_devices(ctx, CUBEB_DEVICE_TYPE_OUTPUT); + if (output_devices.size() < 1) { + return; + } + + cubeb_devid duplex_input = input_devices.front(); + cubeb_devid duplex_output = nullptr; // default device + cubeb_devid input_only = input_devices.back(); + + /* typical use-case: mono voice input, stereo output, low latency. */ + input_params.format = STREAM_FORMAT; + input_params.rate = SAMPLE_FREQUENCY; + input_params.channels = INPUT_CHANNELS; + input_params.layout = CUBEB_LAYOUT_UNDEFINED; + input_params.prefs = CUBEB_STREAM_PREF_VOICE; + + output_params.format = STREAM_FORMAT; + output_params.rate = SAMPLE_FREQUENCY; + output_params.channels = OUTPUT_CHANNELS; + output_params.layout = OUTPUT_LAYOUT; + output_params.prefs = CUBEB_STREAM_PREF_NONE; + + r = cubeb_get_min_latency(ctx, &output_params, &latency_frames); + ASSERT_EQ(r, CUBEB_OK) << "Could not get minimal latency"; + + r = cubeb_stream_init(ctx, &duplex_stream, "Cubeb duplex", duplex_input, + &input_params, duplex_output, &output_params, + latency_frames, data_cb_duplex, state_cb_duplex, + &duplex_stream_state); + ASSERT_EQ(r, CUBEB_OK) << "Error initializing duplex cubeb stream"; + + std::unique_ptr<cubeb_stream, decltype(&cubeb_stream_destroy)> + cleanup_stream_at_exit(duplex_stream, cubeb_stream_destroy); + + r = cubeb_stream_start(duplex_stream); + ASSERT_EQ(r, CUBEB_OK) << "Could not start duplex stream"; + delay(500); + + cubeb_stream * input_stream; + r = cubeb_stream_init(ctx, &input_stream, "Cubeb input", input_only, + &input_params, NULL, NULL, latency_frames, + data_cb_input, state_cb_input, nullptr); + ASSERT_EQ(r, CUBEB_OK) << "Error initializing input-only cubeb stream"; + + std::unique_ptr<cubeb_stream, decltype(&cubeb_stream_destroy)> + cleanup_input_stream_at_exit(input_stream, cubeb_stream_destroy); + + r = cubeb_stream_start(input_stream); + ASSERT_EQ(r, CUBEB_OK) << "Could not start input stream"; + delay(500); + + r = cubeb_stream_stop(duplex_stream); + ASSERT_EQ(r, CUBEB_OK) << "Could not stop duplex stream"; + + r = cubeb_stream_stop(input_stream); + ASSERT_EQ(r, CUBEB_OK) << "Could not stop input stream"; + + ASSERT_FALSE(duplex_stream_state.invalid_audio_value.load()); +} + +#undef SAMPLE_FREQUENCY +#undef STREAM_FORMAT +#undef INPUT_CHANNELS +#undef INPUT_LAYOUT +#undef OUTPUT_CHANNELS +#undef OUTPUT_LAYOUT diff --git a/media/libcubeb/test/test_latency.cpp b/media/libcubeb/test/test_latency.cpp new file mode 100644 index 0000000000..97f24ea498 --- /dev/null +++ b/media/libcubeb/test/test_latency.cpp @@ -0,0 +1,43 @@ +#include "cubeb/cubeb.h" +#include "gtest/gtest.h" +#include <memory> +#include <stdlib.h> +// #define ENABLE_NORMAL_LOG +// #define ENABLE_VERBOSE_LOG +#include "common.h" + +TEST(cubeb, latency) +{ + cubeb * ctx = NULL; + int r; + uint32_t max_channels; + uint32_t preferred_rate; + uint32_t latency_frames; + + r = common_init(&ctx, "Cubeb audio test"); + ASSERT_EQ(r, CUBEB_OK); + + std::unique_ptr<cubeb, decltype(&cubeb_destroy)> cleanup_cubeb_at_exit( + ctx, cubeb_destroy); + + r = cubeb_get_max_channel_count(ctx, &max_channels); + ASSERT_TRUE(r == CUBEB_OK || r == CUBEB_ERROR_NOT_SUPPORTED); + if (r == CUBEB_OK) { + ASSERT_GT(max_channels, 0u); + } + + r = cubeb_get_preferred_sample_rate(ctx, &preferred_rate); + ASSERT_TRUE(r == CUBEB_OK || r == CUBEB_ERROR_NOT_SUPPORTED); + if (r == CUBEB_OK) { + ASSERT_GT(preferred_rate, 0u); + } + + cubeb_stream_params params = {CUBEB_SAMPLE_FLOAT32NE, preferred_rate, + max_channels, CUBEB_LAYOUT_UNDEFINED, + CUBEB_STREAM_PREF_NONE}; + r = cubeb_get_min_latency(ctx, ¶ms, &latency_frames); + ASSERT_TRUE(r == CUBEB_OK || r == CUBEB_ERROR_NOT_SUPPORTED); + if (r == CUBEB_OK) { + ASSERT_GT(latency_frames, 0u); + } +} diff --git a/media/libcubeb/test/test_logging.cpp b/media/libcubeb/test/test_logging.cpp new file mode 100644 index 0000000000..54ff718152 --- /dev/null +++ b/media/libcubeb/test/test_logging.cpp @@ -0,0 +1,196 @@ +/* + * Copyright © 2016 Mozilla Foundation + * + * This program is made available under an ISC-style license. See the + * accompanying file LICENSE for details. + */ + +/* cubeb_logging test */ +#include "gtest/gtest.h" +#if !defined(_XOPEN_SOURCE) +#define _XOPEN_SOURCE 600 +#endif +#include "cubeb/cubeb.h" +#include "cubeb_log.h" +#include <atomic> +#include <math.h> +#include <memory> +#include <stdio.h> +#include <stdlib.h> +#include <thread> + +#include "common.h" + +#define PRINT_LOGS_TO_STDERR 0 + +std::atomic<uint32_t> log_statements_received = {0}; +std::atomic<uint32_t> data_callback_call_count = {0}; + +static void +test_logging_callback(char const * fmt, ...) +{ + log_statements_received++; +#if PRINT_LOGS_TO_STDERR == 1 + char buf[1024]; + va_list argslist; + va_start(argslist, fmt); + vsnprintf(buf, 1024, fmt, argslist); + fprintf(stderr, "%s\n", buf); + va_end(argslist); +#endif // PRINT_LOGS_TO_STDERR +} + +static long +data_cb_load(cubeb_stream * stream, void * user, const void * inputbuffer, + void * outputbuffer, long nframes) +{ + data_callback_call_count++; + return nframes; +} + +static void +state_cb(cubeb_stream * stream, void * /*user*/, cubeb_state state) +{ + if (stream == NULL) + return; + + switch (state) { + case CUBEB_STATE_STARTED: + fprintf(stderr, "stream started\n"); + break; + case CUBEB_STATE_STOPPED: + fprintf(stderr, "stream stopped\n"); + break; + case CUBEB_STATE_DRAINED: + fprintf(stderr, "stream drained\n"); + break; + default: + fprintf(stderr, "unknown stream state %d\n", state); + } + + return; +} + +// Waits for at least one audio callback to have occured. +void +wait_for_audio_callback() +{ + uint32_t audio_callback_index = + data_callback_call_count.load(std::memory_order_acquire); + while (audio_callback_index == + data_callback_call_count.load(std::memory_order_acquire)) { + delay(100); + } +} + +TEST(cubeb, logging) +{ + cubeb * ctx; + cubeb_stream * stream; + cubeb_stream_params output_params; + int r; + uint32_t latency_frames = 0; + + cubeb_set_log_callback(CUBEB_LOG_NORMAL, test_logging_callback); + + r = common_init(&ctx, "Cubeb logging test"); + ASSERT_EQ(r, CUBEB_OK) << "Error initializing cubeb library"; + + std::unique_ptr<cubeb, decltype(&cubeb_destroy)> cleanup_cubeb_at_exit( + ctx, cubeb_destroy); + + output_params.format = CUBEB_SAMPLE_FLOAT32LE; + output_params.rate = 48000; + output_params.channels = 2; + output_params.layout = CUBEB_LAYOUT_STEREO; + output_params.prefs = CUBEB_STREAM_PREF_NONE; + + r = cubeb_get_min_latency(ctx, &output_params, &latency_frames); + if (r != CUBEB_OK) { + // not fatal + latency_frames = 1024; + } + + r = cubeb_stream_init(ctx, &stream, "Cubeb logging", NULL, NULL, NULL, + &output_params, latency_frames, data_cb_load, state_cb, + NULL); + ASSERT_EQ(r, CUBEB_OK) << "Error initializing cubeb stream"; + + std::unique_ptr<cubeb_stream, decltype(&cubeb_stream_destroy)> + cleanup_stream_at_exit(stream, cubeb_stream_destroy); + + ASSERT_NE(log_statements_received.load(std::memory_order_acquire), 0u); + + cubeb_set_log_callback(CUBEB_LOG_DISABLED, nullptr); + log_statements_received.store(0, std::memory_order_release); + + // This is synchronous and we'll receive log messages on all backends that we + // test + cubeb_stream_start(stream); + + ASSERT_EQ(log_statements_received.load(std::memory_order_acquire), 0u); + + cubeb_set_log_callback(CUBEB_LOG_VERBOSE, test_logging_callback); + + wait_for_audio_callback(); + + ASSERT_NE(log_statements_received.load(std::memory_order_acquire), 0u); + + bool log_callback_set = true; + uint32_t iterations = 100; + while (iterations--) { + wait_for_audio_callback(); + + if (!log_callback_set) { + ASSERT_EQ(log_statements_received.load(std::memory_order_acquire), 0u); + // Set a logging callback, start logging + cubeb_set_log_callback(CUBEB_LOG_VERBOSE, test_logging_callback); + log_callback_set = true; + } else { + // Disable the logging callback, stop logging. + ASSERT_NE(log_statements_received.load(std::memory_order_acquire), 0u); + cubeb_set_log_callback(CUBEB_LOG_DISABLED, nullptr); + log_statements_received.store(0, std::memory_order_release); + // Disabling logging should flush any log message -- wait a bit and check + // that this is true. + ASSERT_EQ(log_statements_received.load(std::memory_order_acquire), 0u); + log_callback_set = false; + } + } + + cubeb_stream_stop(stream); +} + +TEST(cubeb, logging_stress) +{ + cubeb_set_log_callback(CUBEB_LOG_NORMAL, test_logging_callback); + + std::atomic<bool> thread_done = {false}; + + auto t = std::thread([&thread_done]() { + uint32_t count = 0; + do { + while (rand() % 10) { + ALOG("Log message #%d!", count++); + } + } while (count < 1e4); + thread_done.store(true); + }); + + bool enabled = true; + while (!thread_done.load()) { + if (enabled) { + cubeb_set_log_callback(CUBEB_LOG_DISABLED, nullptr); + enabled = false; + } else { + cubeb_set_log_callback(CUBEB_LOG_NORMAL, test_logging_callback); + enabled = true; + } + } + + cubeb_set_log_callback(CUBEB_LOG_DISABLED, nullptr); + + t.join(); + + ASSERT_TRUE(true); +} diff --git a/media/libcubeb/test/test_loopback.cpp b/media/libcubeb/test/test_loopback.cpp new file mode 100644 index 0000000000..7410bedb8b --- /dev/null +++ b/media/libcubeb/test/test_loopback.cpp @@ -0,0 +1,679 @@ +/* + * Copyright © 2017 Mozilla Foundation + * + * This program is made available under an ISC-style license. See the + * accompanying file LICENSE for details. + */ + +/* libcubeb api/function test. Requests a loopback device and checks that + output is being looped back to input. NOTE: Usage of output devices while + performing this test will cause flakey results! */ +#include "gtest/gtest.h" +#if !defined(_XOPEN_SOURCE) +#define _XOPEN_SOURCE 600 +#endif +#include "cubeb/cubeb.h" +#include <algorithm> +#include <math.h> +#include <memory> +#include <mutex> +#include <stdio.h> +#include <stdlib.h> +#include <string> +// #define ENABLE_NORMAL_LOG +// #define ENABLE_VERBOSE_LOG +#include "common.h" +const uint32_t SAMPLE_FREQUENCY = 48000; +const uint32_t TONE_FREQUENCY = 440; +const double OUTPUT_AMPLITUDE = 0.25; +const int32_t NUM_FRAMES_TO_OUTPUT = + SAMPLE_FREQUENCY / 20; /* play ~50ms of samples */ + +template <typename T> +T +ConvertSampleToOutput(double input); +template <> +float +ConvertSampleToOutput(double input) +{ + return float(input); +} +template <> +short +ConvertSampleToOutput(double input) +{ + return short(input * 32767.0f); +} + +template <typename T> +double +ConvertSampleFromOutput(T sample); +template <> +double +ConvertSampleFromOutput(float sample) +{ + return double(sample); +} +template <> +double +ConvertSampleFromOutput(short sample) +{ + return double(sample / 32767.0); +} + +/* Simple cross correlation to help find phase shift. Not a performant impl */ +std::vector<double> +cross_correlate(std::vector<double> & f, std::vector<double> & g, + size_t signal_length) +{ + /* the length we sweep our window through to find the cross correlation */ + size_t sweep_length = f.size() - signal_length + 1; + std::vector<double> correlation; + correlation.reserve(sweep_length); + for (size_t i = 0; i < sweep_length; i++) { + double accumulator = 0.0; + for (size_t j = 0; j < signal_length; j++) { + accumulator += f.at(j) * g.at(i + j); + } + correlation.push_back(accumulator); + } + return correlation; +} + +/* best effort discovery of phase shift between output and (looped) input*/ +size_t +find_phase(std::vector<double> & output_frames, + std::vector<double> & input_frames, size_t signal_length) +{ + std::vector<double> correlation = + cross_correlate(output_frames, input_frames, signal_length); + size_t phase = 0; + double max_correlation = correlation.at(0); + for (size_t i = 1; i < correlation.size(); i++) { + if (correlation.at(i) > max_correlation) { + max_correlation = correlation.at(i); + phase = i; + } + } + return phase; +} + +std::vector<double> +normalize_frames(std::vector<double> & frames) +{ + double max = abs( + *std::max_element(frames.begin(), frames.end(), + [](double a, double b) { return abs(a) < abs(b); })); + std::vector<double> normalized_frames; + normalized_frames.reserve(frames.size()); + for (const double frame : frames) { + normalized_frames.push_back(frame / max); + } + return normalized_frames; +} + +/* heuristic comparison of aligned output and input signals, gets flaky if + * TONE_FREQUENCY is too high */ +void +compare_signals(std::vector<double> & output_frames, + std::vector<double> & input_frames) +{ + ASSERT_EQ(output_frames.size(), input_frames.size()) + << "#Output frames != #input frames"; + size_t num_frames = output_frames.size(); + std::vector<double> normalized_output_frames = + normalize_frames(output_frames); + std::vector<double> normalized_input_frames = normalize_frames(input_frames); + + /* calculate mean absolute errors */ + /* mean absolute errors between output and input */ + double io_mas = 0.0; + /* mean absolute errors between output and silence */ + double output_silence_mas = 0.0; + /* mean absolute errors between input and silence */ + double input_silence_mas = 0.0; + for (size_t i = 0; i < num_frames; i++) { + io_mas += + abs(normalized_output_frames.at(i) - normalized_input_frames.at(i)); + output_silence_mas += abs(normalized_output_frames.at(i)); + input_silence_mas += abs(normalized_input_frames.at(i)); + } + io_mas /= num_frames; + output_silence_mas /= num_frames; + input_silence_mas /= num_frames; + + ASSERT_LT(io_mas, output_silence_mas) + << "Error between output and input should be less than output and " + "silence!"; + ASSERT_LT(io_mas, input_silence_mas) + << "Error between output and input should be less than output and " + "silence!"; + + /* make sure extrema are in (roughly) correct location */ + /* number of maxima + minama expected in the frames*/ + const long NUM_EXTREMA = + 2 * TONE_FREQUENCY * NUM_FRAMES_TO_OUTPUT / SAMPLE_FREQUENCY; + /* expected index of first maxima */ + const long FIRST_MAXIMUM_INDEX = SAMPLE_FREQUENCY / TONE_FREQUENCY / 4; + /* Threshold we expect all maxima and minima to be above or below. Ideally + the extrema would be 1 or -1, but particularly at the start of loopback + the values seen can be significantly lower. */ + const double THRESHOLD = 0.5; + + for (size_t i = 0; i < NUM_EXTREMA; i++) { + bool is_maximum = i % 2 == 0; + /* expected offset to current extreme: i * stide between extrema */ + size_t offset = i * SAMPLE_FREQUENCY / TONE_FREQUENCY / 2; + if (is_maximum) { + ASSERT_GT(normalized_output_frames.at(FIRST_MAXIMUM_INDEX + offset), + THRESHOLD) + << "Output frames have unexpected missing maximum!"; + ASSERT_GT(normalized_input_frames.at(FIRST_MAXIMUM_INDEX + offset), + THRESHOLD) + << "Input frames have unexpected missing maximum!"; + } else { + ASSERT_LT(normalized_output_frames.at(FIRST_MAXIMUM_INDEX + offset), + -THRESHOLD) + << "Output frames have unexpected missing minimum!"; + ASSERT_LT(normalized_input_frames.at(FIRST_MAXIMUM_INDEX + offset), + -THRESHOLD) + << "Input frames have unexpected missing minimum!"; + } + } +} + +struct user_state_loopback { + std::mutex user_state_mutex; + long position = 0; + /* track output */ + std::vector<double> output_frames; + /* track input */ + std::vector<double> input_frames; +}; + +template <typename T> +long +data_cb_loop_duplex(cubeb_stream * stream, void * user, + const void * inputbuffer, void * outputbuffer, long nframes) +{ + struct user_state_loopback * u = (struct user_state_loopback *)user; + T * ib = (T *)inputbuffer; + T * ob = (T *)outputbuffer; + + if (stream == NULL || inputbuffer == NULL || outputbuffer == NULL) { + return CUBEB_ERROR; + } + + std::lock_guard<std::mutex> lock(u->user_state_mutex); + /* generate our test tone on the fly */ + for (int i = 0; i < nframes; i++) { + double tone = 0.0; + if (u->position + i < NUM_FRAMES_TO_OUTPUT) { + /* generate sine wave */ + tone = + sin(2 * M_PI * (i + u->position) * TONE_FREQUENCY / SAMPLE_FREQUENCY); + tone *= OUTPUT_AMPLITUDE; + } + ob[i] = ConvertSampleToOutput<T>(tone); + u->output_frames.push_back(tone); + /* store any looped back output, may be silence */ + u->input_frames.push_back(ConvertSampleFromOutput(ib[i])); + } + + u->position += nframes; + + return nframes; +} + +template <typename T> +long +data_cb_loop_input_only(cubeb_stream * stream, void * user, + const void * inputbuffer, void * outputbuffer, + long nframes) +{ + struct user_state_loopback * u = (struct user_state_loopback *)user; + T * ib = (T *)inputbuffer; + + if (outputbuffer != NULL) { + // Can't assert as it needs to return, so expect to fail instead + EXPECT_EQ(outputbuffer, (void *)NULL) + << "outputbuffer should be null in input only callback"; + return CUBEB_ERROR; + } + + if (stream == NULL || inputbuffer == NULL) { + return CUBEB_ERROR; + } + + std::lock_guard<std::mutex> lock(u->user_state_mutex); + for (int i = 0; i < nframes; i++) { + u->input_frames.push_back(ConvertSampleFromOutput(ib[i])); + } + + return nframes; +} + +template <typename T> +long +data_cb_playback(cubeb_stream * stream, void * user, const void * inputbuffer, + void * outputbuffer, long nframes) +{ + struct user_state_loopback * u = (struct user_state_loopback *)user; + T * ob = (T *)outputbuffer; + + if (stream == NULL || outputbuffer == NULL) { + return CUBEB_ERROR; + } + + std::lock_guard<std::mutex> lock(u->user_state_mutex); + /* generate our test tone on the fly */ + for (int i = 0; i < nframes; i++) { + double tone = 0.0; + if (u->position + i < NUM_FRAMES_TO_OUTPUT) { + /* generate sine wave */ + tone = + sin(2 * M_PI * (i + u->position) * TONE_FREQUENCY / SAMPLE_FREQUENCY); + tone *= OUTPUT_AMPLITUDE; + } + ob[i] = ConvertSampleToOutput<T>(tone); + u->output_frames.push_back(tone); + } + + u->position += nframes; + + return nframes; +} + +void +state_cb_loop(cubeb_stream * stream, void * /*user*/, cubeb_state state) +{ + if (stream == NULL) + return; + + switch (state) { + case CUBEB_STATE_STARTED: + fprintf(stderr, "stream started\n"); + break; + case CUBEB_STATE_STOPPED: + fprintf(stderr, "stream stopped\n"); + break; + case CUBEB_STATE_DRAINED: + fprintf(stderr, "stream drained\n"); + break; + default: + fprintf(stderr, "unknown stream state %d\n", state); + } + + return; +} + +void +run_loopback_duplex_test(bool is_float) +{ + cubeb * ctx; + cubeb_stream * stream; + cubeb_stream_params input_params; + cubeb_stream_params output_params; + int r; + uint32_t latency_frames = 0; + + r = common_init(&ctx, "Cubeb loopback example: duplex stream"); + ASSERT_EQ(r, CUBEB_OK) << "Error initializing cubeb library"; + + std::unique_ptr<cubeb, decltype(&cubeb_destroy)> cleanup_cubeb_at_exit( + ctx, cubeb_destroy); + + /* This test needs an available input device, skip it if this host does not + * have one. */ + if (!can_run_audio_input_test(ctx)) { + return; + } + + input_params.format = is_float ? CUBEB_SAMPLE_FLOAT32NE : CUBEB_SAMPLE_S16LE; + input_params.rate = SAMPLE_FREQUENCY; + input_params.channels = 1; + input_params.layout = CUBEB_LAYOUT_MONO; + input_params.prefs = CUBEB_STREAM_PREF_LOOPBACK; + output_params.format = is_float ? CUBEB_SAMPLE_FLOAT32NE : CUBEB_SAMPLE_S16LE; + output_params.rate = SAMPLE_FREQUENCY; + output_params.channels = 1; + output_params.layout = CUBEB_LAYOUT_MONO; + output_params.prefs = CUBEB_STREAM_PREF_NONE; + + std::unique_ptr<user_state_loopback> user_data(new user_state_loopback()); + ASSERT_TRUE(!!user_data) << "Error allocating user data"; + + r = cubeb_get_min_latency(ctx, &output_params, &latency_frames); + ASSERT_EQ(r, CUBEB_OK) << "Could not get minimal latency"; + + /* setup a duplex stream with loopback */ + r = cubeb_stream_init(ctx, &stream, "Cubeb loopback", NULL, &input_params, + NULL, &output_params, latency_frames, + is_float ? data_cb_loop_duplex<float> + : data_cb_loop_duplex<short>, + state_cb_loop, user_data.get()); + ASSERT_EQ(r, CUBEB_OK) << "Error initializing cubeb stream"; + + std::unique_ptr<cubeb_stream, decltype(&cubeb_stream_destroy)> + cleanup_stream_at_exit(stream, cubeb_stream_destroy); + + cubeb_stream_start(stream); + delay(300); + cubeb_stream_stop(stream); + + /* access after stop should not happen, but lock just in case and to appease + * sanitization tools */ + std::lock_guard<std::mutex> lock(user_data->user_state_mutex); + std::vector<double> & output_frames = user_data->output_frames; + std::vector<double> & input_frames = user_data->input_frames; + ASSERT_EQ(output_frames.size(), input_frames.size()) + << "#Output frames != #input frames"; + + size_t phase = find_phase(user_data->output_frames, user_data->input_frames, + NUM_FRAMES_TO_OUTPUT); + + /* extract vectors of just the relevant signal from output and input */ + auto output_frames_signal_start = output_frames.begin(); + auto output_frames_signal_end = output_frames.begin() + NUM_FRAMES_TO_OUTPUT; + std::vector<double> trimmed_output_frames(output_frames_signal_start, + output_frames_signal_end); + auto input_frames_signal_start = input_frames.begin() + phase; + auto input_frames_signal_end = + input_frames.begin() + phase + NUM_FRAMES_TO_OUTPUT; + std::vector<double> trimmed_input_frames(input_frames_signal_start, + input_frames_signal_end); + + compare_signals(trimmed_output_frames, trimmed_input_frames); +} + +TEST(cubeb, loopback_duplex) +{ + run_loopback_duplex_test(true); + run_loopback_duplex_test(false); +} + +void +run_loopback_separate_streams_test(bool is_float) +{ + cubeb * ctx; + cubeb_stream * input_stream; + cubeb_stream * output_stream; + cubeb_stream_params input_params; + cubeb_stream_params output_params; + int r; + uint32_t latency_frames = 0; + + r = common_init(&ctx, "Cubeb loopback example: separate streams"); + ASSERT_EQ(r, CUBEB_OK) << "Error initializing cubeb library"; + + std::unique_ptr<cubeb, decltype(&cubeb_destroy)> cleanup_cubeb_at_exit( + ctx, cubeb_destroy); + + if (!can_run_audio_input_test(ctx)) { + return; + } + + input_params.format = is_float ? CUBEB_SAMPLE_FLOAT32NE : CUBEB_SAMPLE_S16LE; + input_params.rate = SAMPLE_FREQUENCY; + input_params.channels = 1; + input_params.layout = CUBEB_LAYOUT_MONO; + input_params.prefs = CUBEB_STREAM_PREF_LOOPBACK; + output_params.format = is_float ? CUBEB_SAMPLE_FLOAT32NE : CUBEB_SAMPLE_S16LE; + output_params.rate = SAMPLE_FREQUENCY; + output_params.channels = 1; + output_params.layout = CUBEB_LAYOUT_MONO; + output_params.prefs = CUBEB_STREAM_PREF_NONE; + + std::unique_ptr<user_state_loopback> user_data(new user_state_loopback()); + ASSERT_TRUE(!!user_data) << "Error allocating user data"; + + r = cubeb_get_min_latency(ctx, &output_params, &latency_frames); + ASSERT_EQ(r, CUBEB_OK) << "Could not get minimal latency"; + + /* setup an input stream with loopback */ + r = cubeb_stream_init(ctx, &input_stream, "Cubeb loopback input only", NULL, + &input_params, NULL, NULL, latency_frames, + is_float ? data_cb_loop_input_only<float> + : data_cb_loop_input_only<short>, + state_cb_loop, user_data.get()); + ASSERT_EQ(r, CUBEB_OK) << "Error initializing cubeb stream"; + + std::unique_ptr<cubeb_stream, decltype(&cubeb_stream_destroy)> + cleanup_input_stream_at_exit(input_stream, cubeb_stream_destroy); + + /* setup an output stream */ + r = cubeb_stream_init(ctx, &output_stream, "Cubeb loopback output only", NULL, + NULL, NULL, &output_params, latency_frames, + is_float ? data_cb_playback<float> + : data_cb_playback<short>, + state_cb_loop, user_data.get()); + ASSERT_EQ(r, CUBEB_OK) << "Error initializing cubeb stream"; + + std::unique_ptr<cubeb_stream, decltype(&cubeb_stream_destroy)> + cleanup_output_stream_at_exit(output_stream, cubeb_stream_destroy); + + cubeb_stream_start(input_stream); + cubeb_stream_start(output_stream); + delay(300); + cubeb_stream_stop(output_stream); + cubeb_stream_stop(input_stream); + + /* access after stop should not happen, but lock just in case and to appease + * sanitization tools */ + std::lock_guard<std::mutex> lock(user_data->user_state_mutex); + std::vector<double> & output_frames = user_data->output_frames; + std::vector<double> & input_frames = user_data->input_frames; + ASSERT_LE(output_frames.size(), input_frames.size()) + << "#Output frames should be less or equal to #input frames"; + + size_t phase = find_phase(user_data->output_frames, user_data->input_frames, + NUM_FRAMES_TO_OUTPUT); + + /* extract vectors of just the relevant signal from output and input */ + auto output_frames_signal_start = output_frames.begin(); + auto output_frames_signal_end = output_frames.begin() + NUM_FRAMES_TO_OUTPUT; + std::vector<double> trimmed_output_frames(output_frames_signal_start, + output_frames_signal_end); + auto input_frames_signal_start = input_frames.begin() + phase; + auto input_frames_signal_end = + input_frames.begin() + phase + NUM_FRAMES_TO_OUTPUT; + std::vector<double> trimmed_input_frames(input_frames_signal_start, + input_frames_signal_end); + + compare_signals(trimmed_output_frames, trimmed_input_frames); +} + +TEST(cubeb, loopback_separate_streams) +{ + run_loopback_separate_streams_test(true); + run_loopback_separate_streams_test(false); +} + +void +run_loopback_silence_test(bool is_float) +{ + cubeb * ctx; + cubeb_stream * input_stream; + cubeb_stream_params input_params; + int r; + uint32_t latency_frames = 0; + + r = common_init(&ctx, "Cubeb loopback example: record silence"); + ASSERT_EQ(r, CUBEB_OK) << "Error initializing cubeb library"; + + std::unique_ptr<cubeb, decltype(&cubeb_destroy)> cleanup_cubeb_at_exit( + ctx, cubeb_destroy); + + if (!can_run_audio_input_test(ctx)) { + return; + } + + input_params.format = is_float ? CUBEB_SAMPLE_FLOAT32NE : CUBEB_SAMPLE_S16LE; + input_params.rate = SAMPLE_FREQUENCY; + input_params.channels = 1; + input_params.layout = CUBEB_LAYOUT_MONO; + input_params.prefs = CUBEB_STREAM_PREF_LOOPBACK; + + std::unique_ptr<user_state_loopback> user_data(new user_state_loopback()); + ASSERT_TRUE(!!user_data) << "Error allocating user data"; + + r = cubeb_get_min_latency(ctx, &input_params, &latency_frames); + ASSERT_EQ(r, CUBEB_OK) << "Could not get minimal latency"; + + /* setup an input stream with loopback */ + r = cubeb_stream_init(ctx, &input_stream, "Cubeb loopback input only", NULL, + &input_params, NULL, NULL, latency_frames, + is_float ? data_cb_loop_input_only<float> + : data_cb_loop_input_only<short>, + state_cb_loop, user_data.get()); + ASSERT_EQ(r, CUBEB_OK) << "Error initializing cubeb stream"; + + std::unique_ptr<cubeb_stream, decltype(&cubeb_stream_destroy)> + cleanup_input_stream_at_exit(input_stream, cubeb_stream_destroy); + + cubeb_stream_start(input_stream); + delay(300); + cubeb_stream_stop(input_stream); + + /* access after stop should not happen, but lock just in case and to appease + * sanitization tools */ + std::lock_guard<std::mutex> lock(user_data->user_state_mutex); + std::vector<double> & input_frames = user_data->input_frames; + + /* expect to have at least ~50ms of frames */ + ASSERT_GE(input_frames.size(), SAMPLE_FREQUENCY / 20); + double EPISILON = 0.0001; + /* frames should be 0.0, but use epsilon to avoid possible issues with impls + that may use ~0.0 silence values. */ + for (double frame : input_frames) { + ASSERT_LT(abs(frame), EPISILON); + } +} + +TEST(cubeb, loopback_silence) +{ + run_loopback_silence_test(true); + run_loopback_silence_test(false); +} + +void +run_loopback_device_selection_test(bool is_float) +{ + cubeb * ctx; + cubeb_device_collection collection; + cubeb_stream * input_stream; + cubeb_stream * output_stream; + cubeb_stream_params input_params; + cubeb_stream_params output_params; + int r; + uint32_t latency_frames = 0; + + r = common_init(&ctx, + "Cubeb loopback example: device selection, separate streams"); + ASSERT_EQ(r, CUBEB_OK) << "Error initializing cubeb library"; + + std::unique_ptr<cubeb, decltype(&cubeb_destroy)> cleanup_cubeb_at_exit( + ctx, cubeb_destroy); + + if (!can_run_audio_input_test(ctx)) { + return; + } + + r = cubeb_enumerate_devices(ctx, CUBEB_DEVICE_TYPE_OUTPUT, &collection); + if (r == CUBEB_ERROR_NOT_SUPPORTED) { + fprintf(stderr, "Device enumeration not supported" + " for this backend, skipping this test.\n"); + return; + } + + ASSERT_EQ(r, CUBEB_OK) << "Error enumerating devices " << r; + /* get first preferred output device id */ + std::string device_id; + for (size_t i = 0; i < collection.count; i++) { + if (collection.device[i].preferred) { + device_id = collection.device[i].device_id; + break; + } + } + cubeb_device_collection_destroy(ctx, &collection); + if (device_id.empty()) { + fprintf(stderr, "Could not find preferred device, aborting test.\n"); + return; + } + + input_params.format = is_float ? CUBEB_SAMPLE_FLOAT32NE : CUBEB_SAMPLE_S16LE; + input_params.rate = SAMPLE_FREQUENCY; + input_params.channels = 1; + input_params.layout = CUBEB_LAYOUT_MONO; + input_params.prefs = CUBEB_STREAM_PREF_LOOPBACK; + output_params.format = is_float ? CUBEB_SAMPLE_FLOAT32NE : CUBEB_SAMPLE_S16LE; + output_params.rate = SAMPLE_FREQUENCY; + output_params.channels = 1; + output_params.layout = CUBEB_LAYOUT_MONO; + output_params.prefs = CUBEB_STREAM_PREF_NONE; + + std::unique_ptr<user_state_loopback> user_data(new user_state_loopback()); + ASSERT_TRUE(!!user_data) << "Error allocating user data"; + + r = cubeb_get_min_latency(ctx, &output_params, &latency_frames); + ASSERT_EQ(r, CUBEB_OK) << "Could not get minimal latency"; + + /* setup an input stream with loopback */ + r = cubeb_stream_init(ctx, &input_stream, "Cubeb loopback input only", + device_id.c_str(), &input_params, NULL, NULL, + latency_frames, + is_float ? data_cb_loop_input_only<float> + : data_cb_loop_input_only<short>, + state_cb_loop, user_data.get()); + ASSERT_EQ(r, CUBEB_OK) << "Error initializing cubeb stream"; + + std::unique_ptr<cubeb_stream, decltype(&cubeb_stream_destroy)> + cleanup_input_stream_at_exit(input_stream, cubeb_stream_destroy); + + /* setup an output stream */ + r = cubeb_stream_init(ctx, &output_stream, "Cubeb loopback output only", NULL, + NULL, device_id.c_str(), &output_params, latency_frames, + is_float ? data_cb_playback<float> + : data_cb_playback<short>, + state_cb_loop, user_data.get()); + ASSERT_EQ(r, CUBEB_OK) << "Error initializing cubeb stream"; + + std::unique_ptr<cubeb_stream, decltype(&cubeb_stream_destroy)> + cleanup_output_stream_at_exit(output_stream, cubeb_stream_destroy); + + cubeb_stream_start(input_stream); + cubeb_stream_start(output_stream); + delay(300); + cubeb_stream_stop(output_stream); + cubeb_stream_stop(input_stream); + + /* access after stop should not happen, but lock just in case and to appease + * sanitization tools */ + std::lock_guard<std::mutex> lock(user_data->user_state_mutex); + std::vector<double> & output_frames = user_data->output_frames; + std::vector<double> & input_frames = user_data->input_frames; + ASSERT_LE(output_frames.size(), input_frames.size()) + << "#Output frames should be less or equal to #input frames"; + + size_t phase = find_phase(user_data->output_frames, user_data->input_frames, + NUM_FRAMES_TO_OUTPUT); + + /* extract vectors of just the relevant signal from output and input */ + auto output_frames_signal_start = output_frames.begin(); + auto output_frames_signal_end = output_frames.begin() + NUM_FRAMES_TO_OUTPUT; + std::vector<double> trimmed_output_frames(output_frames_signal_start, + output_frames_signal_end); + auto input_frames_signal_start = input_frames.begin() + phase; + auto input_frames_signal_end = + input_frames.begin() + phase + NUM_FRAMES_TO_OUTPUT; + std::vector<double> trimmed_input_frames(input_frames_signal_start, + input_frames_signal_end); + + compare_signals(trimmed_output_frames, trimmed_input_frames); +} + +TEST(cubeb, loopback_device_selection) +{ + run_loopback_device_selection_test(true); + run_loopback_device_selection_test(false); +} diff --git a/media/libcubeb/test/test_overload_callback.cpp b/media/libcubeb/test/test_overload_callback.cpp new file mode 100644 index 0000000000..14c1ec3e65 --- /dev/null +++ b/media/libcubeb/test/test_overload_callback.cpp @@ -0,0 +1,108 @@ +/* + * Copyright © 2017 Mozilla Foundation + * + * This program is made available under an ISC-style license. See the + * accompanying file LICENSE for details. + */ + +#include "gtest/gtest.h" +#if !defined(_XOPEN_SOURCE) +#define _XOPEN_SOURCE 600 +#endif +#include "cubeb/cubeb.h" +#include <atomic> +#include <math.h> +#include <memory> +#include <stdio.h> +#include <stdlib.h> +// #define ENABLE_NORMAL_LOG +// #define ENABLE_VERBOSE_LOG +#include "common.h" + +#define SAMPLE_FREQUENCY 48000 +#define STREAM_FORMAT CUBEB_SAMPLE_S16LE + +std::atomic<bool> load_callback{false}; + +static long +data_cb(cubeb_stream * stream, void * user, const void * inputbuffer, + void * outputbuffer, long nframes) +{ + if (load_callback) { + fprintf(stderr, "Sleeping...\n"); + delay(100000); + fprintf(stderr, "Sleeping done\n"); + } + return nframes; +} + +static void +state_cb(cubeb_stream * stream, void * /*user*/, cubeb_state state) +{ + ASSERT_TRUE(!!stream); + + switch (state) { + case CUBEB_STATE_STARTED: + fprintf(stderr, "stream started\n"); + break; + case CUBEB_STATE_STOPPED: + fprintf(stderr, "stream stopped\n"); + break; + case CUBEB_STATE_DRAINED: + FAIL() << "this test is not supposed to drain"; + break; + case CUBEB_STATE_ERROR: + fprintf(stderr, "stream error\n"); + break; + default: + FAIL() << "this test is not supposed to have a weird state"; + break; + } +} + +TEST(cubeb, overload_callback) +{ + cubeb * ctx; + cubeb_stream * stream; + cubeb_stream_params output_params; + int r; + uint32_t latency_frames = 0; + + r = common_init(&ctx, "Cubeb callback overload"); + ASSERT_EQ(r, CUBEB_OK); + + std::unique_ptr<cubeb, decltype(&cubeb_destroy)> cleanup_cubeb_at_exit( + ctx, cubeb_destroy); + + // This test is specifically designed to test a behaviour of the WASAPI + // backend in a specific scenario. + if (strcmp(cubeb_get_backend_id(ctx), "wasapi") != 0) { + return; + } + + output_params.format = STREAM_FORMAT; + output_params.rate = 48000; + output_params.channels = 2; + output_params.layout = CUBEB_LAYOUT_STEREO; + output_params.prefs = CUBEB_STREAM_PREF_NONE; + + r = cubeb_get_min_latency(ctx, &output_params, &latency_frames); + ASSERT_EQ(r, CUBEB_OK); + + r = cubeb_stream_init(ctx, &stream, "Cubeb", NULL, NULL, NULL, &output_params, + latency_frames, data_cb, state_cb, NULL); + ASSERT_EQ(r, CUBEB_OK); + + std::unique_ptr<cubeb_stream, decltype(&cubeb_stream_destroy)> + cleanup_stream_at_exit(stream, cubeb_stream_destroy); + + cubeb_stream_start(stream); + delay(500); + // This causes the callback to sleep for a large number of seconds. + load_callback = true; + delay(500); + cubeb_stream_stop(stream); +} + +#undef SAMPLE_FREQUENCY +#undef STREAM_FORMAT diff --git a/media/libcubeb/test/test_record.cpp b/media/libcubeb/test/test_record.cpp new file mode 100644 index 0000000000..5b46b21020 --- /dev/null +++ b/media/libcubeb/test/test_record.cpp @@ -0,0 +1,126 @@ +/* + * Copyright © 2016 Mozilla Foundation + * + * This program is made available under an ISC-style license. See the + * accompanying file LICENSE for details. + */ + +/* libcubeb api/function test. Record the mic and check there is sound. */ +#include "gtest/gtest.h" +#if !defined(_XOPEN_SOURCE) +#define _XOPEN_SOURCE 600 +#endif +#include "cubeb/cubeb.h" +#include <atomic> +#include <math.h> +#include <memory> +#include <stdio.h> +#include <stdlib.h> + +// #define ENABLE_NORMAL_LOG +// #define ENABLE_VERBOSE_LOG +#include "common.h" + +#define SAMPLE_FREQUENCY 48000 +#define STREAM_FORMAT CUBEB_SAMPLE_FLOAT32LE + +struct user_state_record { + std::atomic<int> invalid_audio_value{0}; +}; + +long +data_cb_record(cubeb_stream * stream, void * user, const void * inputbuffer, + void * outputbuffer, long nframes) +{ + user_state_record * u = reinterpret_cast<user_state_record *>(user); + float * b = (float *)inputbuffer; + + if (stream == NULL || inputbuffer == NULL || outputbuffer != NULL) { + return CUBEB_ERROR; + } + + for (long i = 0; i < nframes; i++) { + if (b[i] <= -1.0 || b[i] >= 1.0) { + u->invalid_audio_value = 1; + break; + } + } + + return nframes; +} + +void +state_cb_record(cubeb_stream * stream, void * /*user*/, cubeb_state state) +{ + if (stream == NULL) + return; + + switch (state) { + case CUBEB_STATE_STARTED: + fprintf(stderr, "stream started\n"); + break; + case CUBEB_STATE_STOPPED: + fprintf(stderr, "stream stopped\n"); + break; + case CUBEB_STATE_DRAINED: + fprintf(stderr, "stream drained\n"); + break; + default: + fprintf(stderr, "unknown stream state %d\n", state); + } + + return; +} + +TEST(cubeb, record) +{ + if (cubeb_set_log_callback(CUBEB_LOG_DISABLED, nullptr /*print_log*/) != + CUBEB_OK) { + fprintf(stderr, "Set log callback failed\n"); + } + cubeb * ctx; + cubeb_stream * stream; + cubeb_stream_params params; + int r; + user_state_record stream_state; + + r = common_init(&ctx, "Cubeb record example"); + ASSERT_EQ(r, CUBEB_OK) << "Error initializing cubeb library"; + + std::unique_ptr<cubeb, decltype(&cubeb_destroy)> cleanup_cubeb_at_exit( + ctx, cubeb_destroy); + + /* This test needs an available input device, skip it if this host does not + * have one. */ + if (!can_run_audio_input_test(ctx)) { + return; + } + + params.format = STREAM_FORMAT; + params.rate = SAMPLE_FREQUENCY; + params.channels = 1; + params.layout = CUBEB_LAYOUT_UNDEFINED; + params.prefs = CUBEB_STREAM_PREF_NONE; + + r = cubeb_stream_init(ctx, &stream, "Cubeb record (mono)", NULL, ¶ms, + NULL, nullptr, 4096, data_cb_record, state_cb_record, + &stream_state); + ASSERT_EQ(r, CUBEB_OK) << "Error initializing cubeb stream"; + + std::unique_ptr<cubeb_stream, decltype(&cubeb_stream_destroy)> + cleanup_stream_at_exit(stream, cubeb_stream_destroy); + + cubeb_stream_start(stream); + delay(500); + cubeb_stream_stop(stream); + +#ifdef __linux__ + // user callback does not arrive in Linux, silence the error + fprintf(stderr, "Check is disabled in Linux\n"); +#else + ASSERT_FALSE(stream_state.invalid_audio_value.load()); +#endif +} + +#undef SAMPLE_FREQUENCY +#undef STREAM_FORMAT diff --git a/media/libcubeb/test/test_resampler.cpp b/media/libcubeb/test/test_resampler.cpp new file mode 100644 index 0000000000..adbcfd8c5e --- /dev/null +++ b/media/libcubeb/test/test_resampler.cpp @@ -0,0 +1,1164 @@ +/* + * Copyright © 2016 Mozilla Foundation + * + * This program is made available under an ISC-style license. See the + * accompanying file LICENSE for details. + */ +#ifndef NOMINMAX +#define NOMINMAX +#endif // NOMINMAX +#include "common.h" +#include "cubeb_resampler_internal.h" +#include "gtest/gtest.h" +#include <algorithm> +#include <iostream> +#include <stdio.h> + +/* Windows cmath USE_MATH_DEFINE thing... */ +const float PI = 3.14159265359f; + +/* Testing all sample rates is very long, so if THOROUGH_TESTING is not defined, + * only part of the test suite is ran. */ +#ifdef THOROUGH_TESTING +/* Some standard sample rates we're testing with. */ +const uint32_t sample_rates[] = {8000, 16000, 32000, 44100, + 48000, 88200, 96000, 192000}; +/* The maximum number of channels we're resampling. */ +const uint32_t max_channels = 2; +/* The minimum an maximum number of milliseconds we're resampling for. This is + * used to simulate the fact that the audio stream is resampled in chunks, + * because audio is delivered using callbacks. */ +const uint32_t min_chunks = 10; /* ms */ +const uint32_t max_chunks = 30; /* ms */ +const uint32_t chunk_increment = 1; + +#else + +const uint32_t sample_rates[] = { + 8000, + 44100, + 48000, +}; +const uint32_t max_channels = 2; +const uint32_t min_chunks = 10; /* ms */ +const uint32_t max_chunks = 30; /* ms */ +const uint32_t chunk_increment = 10; +#endif + +// #define DUMP_ARRAYS +#ifdef DUMP_ARRAYS +/** + * Files produced by dump(...) can be converted to .wave files using: + * + * sox -c <channel_count> -r <rate> -e float -b 32 file.raw file.wav + * + * for floating-point audio, or: + * + * sox -c <channel_count> -r <rate> -e unsigned -b 16 file.raw file.wav + * + * for 16bit integer audio. + */ + +/* Use the correct implementation of fopen, depending on the platform. */ +void +fopen_portable(FILE ** f, const char * name, const char * mode) +{ +#ifdef WIN32 + fopen_s(f, name, mode); +#else + *f = fopen(name, mode); +#endif +} + +template <typename T> +void +dump(const char * name, T * frames, size_t count) +{ + FILE * file; + fopen_portable(&file, name, "wb"); + + if (!file) { + fprintf(stderr, "error opening %s\n", name); + return; + } + + if (count != fwrite(frames, sizeof(T), count, file)) { + fprintf(stderr, "error writing to %s\n", name); + } + fclose(file); +} +#else +template <typename T> +void +dump(const char * name, T * frames, size_t count) +{ +} +#endif + +// The more the ratio is far from 1, the more we accept a big error. +float +epsilon_tweak_ratio(float ratio) +{ + return ratio >= 1 ? ratio : 1 / ratio; +} + +// Epsilon values for comparing resampled data to expected data. +// The bigger the resampling ratio is, the more lax we are about errors. +template <typename T> +T +epsilon(float ratio); + +template <> +float +epsilon(float ratio) +{ + return 0.08f * epsilon_tweak_ratio(ratio); +} + +template <> +int16_t +epsilon(float ratio) +{ + return static_cast<int16_t>(10 * epsilon_tweak_ratio(ratio)); +} + +void +test_delay_lines(uint32_t delay_frames, uint32_t channels, uint32_t chunk_ms) +{ + const size_t length_s = 2; + const size_t rate = 44100; + const size_t length_frames = rate * length_s; + delay_line<float> delay(delay_frames, channels, rate); + auto_array<float> input; + auto_array<float> output; + uint32_t chunk_length = channels * chunk_ms * rate / 1000; + uint32_t output_offset = 0; + uint32_t channel = 0; + + /** Generate diracs every 100 frames, and check they are delayed. */ + input.push_silence(length_frames * channels); + for (uint32_t i = 0; i < input.length() - 1; i += 100) { + input.data()[i + channel] = 0.5; + channel = (channel + 1) % channels; + } + dump("input.raw", input.data(), input.length()); + while (input.length()) { + uint32_t to_pop = + std::min<uint32_t>(input.length(), chunk_length * channels); + float * in = delay.input_buffer(to_pop / channels); + input.pop(in, to_pop); + delay.written(to_pop / channels); + output.push_silence(to_pop); + delay.output(output.data() + output_offset, to_pop / channels); + output_offset += to_pop; + } + + // Check the diracs have been shifted by `delay_frames` frames. + for (uint32_t i = 0; i < output.length() - delay_frames * channels + 1; + i += 100) { + ASSERT_EQ(output.data()[i + channel + delay_frames * channels], 0.5); + channel = (channel + 1) % channels; + } + + dump("output.raw", output.data(), output.length()); +} +/** + * This takes sine waves with a certain `channels` count, `source_rate`, and + * resample them, by chunk of `chunk_duration` milliseconds, to `target_rate`. + * Then a sample-wise comparison is performed against a sine wave generated at + * the correct rate. + */ +template <typename T> +void +test_resampler_one_way(uint32_t channels, uint32_t source_rate, + uint32_t target_rate, float chunk_duration) +{ + size_t chunk_duration_in_source_frames = + static_cast<uint32_t>(ceil(chunk_duration * source_rate / 1000.)); + float resampling_ratio = static_cast<float>(source_rate) / target_rate; + cubeb_resampler_speex_one_way<T> resampler(channels, source_rate, target_rate, + 3); + auto_array<T> source(channels * source_rate * 10); + auto_array<T> destination(channels * target_rate * 10); + auto_array<T> expected(channels * target_rate * 10); + uint32_t phase_index = 0; + uint32_t offset = 0; + const uint32_t buf_len = 2; /* seconds */ + + // generate a sine wave in each channel, at the source sample rate + source.push_silence(channels * source_rate * buf_len); + while (offset != source.length()) { + float p = phase_index++ / static_cast<float>(source_rate); + for (uint32_t j = 0; j < channels; j++) { + source.data()[offset++] = 0.5 * sin(440. * 2 * PI * p); + } + } + + dump("input.raw", source.data(), source.length()); + + expected.push_silence(channels * target_rate * buf_len); + // generate a sine wave in each channel, at the target sample rate. + // Insert silent samples at the beginning to account for the resampler + // latency. + offset = resampler.latency() * channels; + for (uint32_t i = 0; i < offset; i++) { + expected.data()[i] = 0.0f; + } + phase_index = 0; + while (offset != expected.length()) { + float p = phase_index++ / static_cast<float>(target_rate); + for (uint32_t j = 0; j < channels; j++) { + expected.data()[offset++] = 0.5 * sin(440. * 2 * PI * p); + } + } + + dump("expected.raw", expected.data(), expected.length()); + + // resample by chunk + uint32_t write_offset = 0; + destination.push_silence(channels * target_rate * buf_len); + while (write_offset < destination.length()) { + size_t output_frames = static_cast<uint32_t>( + floor(chunk_duration_in_source_frames / resampling_ratio)); + uint32_t input_frames = resampler.input_needed_for_output(output_frames); + resampler.input(source.data(), input_frames); + source.pop(nullptr, input_frames * channels); + resampler.output( + destination.data() + write_offset, + std::min(output_frames, + (destination.length() - write_offset) / channels)); + write_offset += output_frames * channels; + } + + dump("output.raw", destination.data(), expected.length()); + + // compare, taking the latency into account + bool fuzzy_equal = true; + for (uint32_t i = resampler.latency() + 1; i < expected.length(); i++) { + float diff = fabs(expected.data()[i] - destination.data()[i]); + if (diff > epsilon<T>(resampling_ratio)) { + fprintf(stderr, "divergence at %d: %f %f (delta %f)\n", i, + expected.data()[i], destination.data()[i], diff); + fuzzy_equal = false; + } + } + ASSERT_TRUE(fuzzy_equal); +} + +template <typename T> +cubeb_sample_format +cubeb_format(); + +template <> +cubeb_sample_format +cubeb_format<float>() +{ + return CUBEB_SAMPLE_FLOAT32NE; +} + +template <> +cubeb_sample_format +cubeb_format<short>() +{ + return CUBEB_SAMPLE_S16NE; +} + +struct osc_state { + osc_state() + : input_phase_index(0), output_phase_index(0), output_offset(0), + input_channels(0), output_channels(0) + { + } + uint32_t input_phase_index; + uint32_t max_output_phase_index; + uint32_t output_phase_index; + uint32_t output_offset; + uint32_t input_channels; + uint32_t output_channels; + uint32_t output_rate; + uint32_t target_rate; + auto_array<float> input; + auto_array<float> output; +}; + +uint32_t +fill_with_sine(float * buf, uint32_t rate, uint32_t channels, uint32_t frames, + uint32_t initial_phase) +{ + uint32_t offset = 0; + for (uint32_t i = 0; i < frames; i++) { + float p = initial_phase++ / static_cast<float>(rate); + for (uint32_t j = 0; j < channels; j++) { + buf[offset++] = 0.5 * sin(440. * 2 * PI * p); + } + } + return initial_phase; +} + +long +data_cb_resampler(cubeb_stream * /*stm*/, void * user_ptr, + const void * input_buffer, void * output_buffer, + long frame_count) +{ + osc_state * state = reinterpret_cast<osc_state *>(user_ptr); + const float * in = reinterpret_cast<const float *>(input_buffer); + float * out = reinterpret_cast<float *>(output_buffer); + + state->input.push(in, frame_count * state->input_channels); + + /* Check how much output frames we need to write */ + uint32_t remaining = + state->max_output_phase_index - state->output_phase_index; + uint32_t to_write = std::min<uint32_t>(remaining, frame_count); + state->output_phase_index = + fill_with_sine(out, state->target_rate, state->output_channels, to_write, + state->output_phase_index); + + return to_write; +} + +template <typename T> +bool +array_fuzzy_equal(const auto_array<T> & lhs, const auto_array<T> & rhs, T epsi) +{ + uint32_t len = std::min(lhs.length(), rhs.length()); + + for (uint32_t i = 0; i < len; i++) { + if (fabs(lhs.at(i) - rhs.at(i)) > epsi) { + std::cout << "not fuzzy equal at index: " << i << " lhs: " << lhs.at(i) + << " rhs: " << rhs.at(i) + << " delta: " << fabs(lhs.at(i) - rhs.at(i)) + << " epsilon: " << epsi << std::endl; + return false; + } + } + return true; +} + +template <typename T> +void +test_resampler_duplex(uint32_t input_channels, uint32_t output_channels, + uint32_t input_rate, uint32_t output_rate, + uint32_t target_rate, float chunk_duration) +{ + cubeb_stream_params input_params; + cubeb_stream_params output_params; + osc_state state; + + input_params.format = output_params.format = cubeb_format<T>(); + state.input_channels = input_params.channels = input_channels; + state.output_channels = output_params.channels = output_channels; + input_params.rate = input_rate; + state.output_rate = output_params.rate = output_rate; + state.target_rate = target_rate; + input_params.prefs = output_params.prefs = CUBEB_STREAM_PREF_NONE; + long got; + + cubeb_resampler * resampler = cubeb_resampler_create( + (cubeb_stream *)nullptr, &input_params, &output_params, target_rate, + data_cb_resampler, (void *)&state, CUBEB_RESAMPLER_QUALITY_VOIP, + CUBEB_RESAMPLER_RECLOCK_NONE); + + long latency = cubeb_resampler_latency(resampler); + + const uint32_t duration_s = 2; + int32_t duration_frames = duration_s * target_rate; + uint32_t input_array_frame_count = + ceil(chunk_duration * input_rate / 1000) + + ceilf(static_cast<float>(input_rate) / target_rate) * 2; + uint32_t output_array_frame_count = chunk_duration * output_rate / 1000; + auto_array<float> input_buffer(input_channels * input_array_frame_count); + auto_array<float> output_buffer(output_channels * output_array_frame_count); + auto_array<float> expected_resampled_input(input_channels * duration_frames); + auto_array<float> expected_resampled_output(output_channels * output_rate * + duration_s); + + state.max_output_phase_index = duration_s * target_rate; + + expected_resampled_input.push_silence(input_channels * duration_frames); + expected_resampled_output.push_silence(output_channels * output_rate * + duration_s); + + /* expected output is a 440Hz sine wave at 16kHz */ + fill_with_sine(expected_resampled_input.data() + latency, target_rate, + input_channels, duration_frames - latency, 0); + /* expected output is a 440Hz sine wave at 32kHz */ + fill_with_sine(expected_resampled_output.data() + latency, output_rate, + output_channels, output_rate * duration_s - latency, 0); + + while (state.output_phase_index != state.max_output_phase_index) { + uint32_t leftover_samples = input_buffer.length() * input_channels; + input_buffer.reserve(input_array_frame_count); + state.input_phase_index = fill_with_sine( + input_buffer.data() + leftover_samples, input_rate, input_channels, + input_array_frame_count - leftover_samples, state.input_phase_index); + long input_consumed = input_array_frame_count; + input_buffer.set_length(input_array_frame_count); + + got = cubeb_resampler_fill(resampler, input_buffer.data(), &input_consumed, + output_buffer.data(), output_array_frame_count); + + /* handle leftover input */ + if (input_array_frame_count != static_cast<uint32_t>(input_consumed)) { + input_buffer.pop(nullptr, input_consumed * input_channels); + } else { + input_buffer.clear(); + } + + state.output.push(output_buffer.data(), got * state.output_channels); + } + + dump("input_expected.raw", expected_resampled_input.data(), + expected_resampled_input.length()); + dump("output_expected.raw", expected_resampled_output.data(), + expected_resampled_output.length()); + dump("input.raw", state.input.data(), state.input.length()); + dump("output.raw", state.output.data(), state.output.length()); + + // This is disabled because the latency estimation in the resampler code is + // slightly off so we can generate expected vectors. + // See https://github.com/kinetiknz/cubeb/issues/93 + // ASSERT_TRUE(array_fuzzy_equal(state.input, expected_resampled_input, + // epsilon<T>(input_rate/target_rate))); + // ASSERT_TRUE(array_fuzzy_equal(state.output, expected_resampled_output, + // epsilon<T>(output_rate/target_rate))); + + cubeb_resampler_destroy(resampler); +} + +#define array_size(x) (sizeof(x) / sizeof(x[0])) + +TEST(cubeb, resampler_one_way) +{ + /* Test one way resamplers */ + for (uint32_t channels = 1; channels <= max_channels; channels++) { + for (uint32_t source_rate = 0; source_rate < array_size(sample_rates); + source_rate++) { + for (uint32_t dest_rate = 0; dest_rate < array_size(sample_rates); + dest_rate++) { + for (uint32_t chunk_duration = min_chunks; chunk_duration < max_chunks; + chunk_duration += chunk_increment) { + fprintf(stderr, + "one_way: channels: %d, source_rate: %d, dest_rate: %d, " + "chunk_duration: %d\n", + channels, sample_rates[source_rate], sample_rates[dest_rate], + chunk_duration); + test_resampler_one_way<float>(channels, sample_rates[source_rate], + sample_rates[dest_rate], + chunk_duration); + } + } + } + } +} + +TEST(cubeb, DISABLED_resampler_duplex) +{ + for (uint32_t input_channels = 1; input_channels <= max_channels; + input_channels++) { + for (uint32_t output_channels = 1; output_channels <= max_channels; + output_channels++) { + for (uint32_t source_rate_input = 0; + source_rate_input < array_size(sample_rates); source_rate_input++) { + for (uint32_t source_rate_output = 0; + source_rate_output < array_size(sample_rates); + source_rate_output++) { + for (uint32_t dest_rate = 0; dest_rate < array_size(sample_rates); + dest_rate++) { + for (uint32_t chunk_duration = min_chunks; + chunk_duration < max_chunks; + chunk_duration += chunk_increment) { + fprintf(stderr, + "input channels:%d output_channels:%d input_rate:%d " + "output_rate:%d target_rate:%d chunk_ms:%d\n", + input_channels, output_channels, + sample_rates[source_rate_input], + sample_rates[source_rate_output], sample_rates[dest_rate], + chunk_duration); + test_resampler_duplex<float>(input_channels, output_channels, + sample_rates[source_rate_input], + sample_rates[source_rate_output], + sample_rates[dest_rate], + chunk_duration); + } + } + } + } + } + } +} + +TEST(cubeb, resampler_delay_line) +{ + for (uint32_t channel = 1; channel <= 2; channel++) { + for (uint32_t delay_frames = 4; delay_frames <= 40; + delay_frames += chunk_increment) { + for (uint32_t chunk_size = 10; chunk_size <= 30; chunk_size++) { + fprintf(stderr, "channel: %d, delay_frames: %d, chunk_size: %d\n", + channel, delay_frames, chunk_size); + test_delay_lines(delay_frames, channel, chunk_size); + } + } + } +} + +long +test_output_only_noop_data_cb(cubeb_stream * /*stm*/, void * /*user_ptr*/, + const void * input_buffer, void * output_buffer, + long frame_count) +{ + EXPECT_TRUE(output_buffer); + EXPECT_TRUE(!input_buffer); + return frame_count; +} + +TEST(cubeb, resampler_output_only_noop) +{ + cubeb_stream_params output_params; + int target_rate; + + output_params.rate = 44100; + output_params.channels = 1; + output_params.format = CUBEB_SAMPLE_FLOAT32NE; + target_rate = output_params.rate; + + cubeb_resampler * resampler = cubeb_resampler_create( + (cubeb_stream *)nullptr, nullptr, &output_params, target_rate, + test_output_only_noop_data_cb, nullptr, CUBEB_RESAMPLER_QUALITY_VOIP, + CUBEB_RESAMPLER_RECLOCK_NONE); + const long out_frames = 128; + float out_buffer[out_frames]; + long got; + + got = + cubeb_resampler_fill(resampler, nullptr, nullptr, out_buffer, out_frames); + + ASSERT_EQ(got, out_frames); + + cubeb_resampler_destroy(resampler); +} + +long +test_drain_data_cb(cubeb_stream * /*stm*/, void * user_ptr, + const void * input_buffer, void * output_buffer, + long frame_count) +{ + EXPECT_TRUE(output_buffer); + EXPECT_TRUE(!input_buffer); + auto cb_count = static_cast<int *>(user_ptr); + (*cb_count)++; + return frame_count - 1; +} + +TEST(cubeb, resampler_drain) +{ + cubeb_stream_params output_params; + int target_rate; + + output_params.rate = 44100; + output_params.channels = 1; + output_params.format = CUBEB_SAMPLE_FLOAT32NE; + target_rate = 48000; + int cb_count = 0; + + cubeb_resampler * resampler = cubeb_resampler_create( + (cubeb_stream *)nullptr, nullptr, &output_params, target_rate, + test_drain_data_cb, &cb_count, CUBEB_RESAMPLER_QUALITY_VOIP, + CUBEB_RESAMPLER_RECLOCK_NONE); + + const long out_frames = 128; + float out_buffer[out_frames]; + long got; + + do { + got = cubeb_resampler_fill(resampler, nullptr, nullptr, out_buffer, + out_frames); + } while (got == out_frames); + + /* The callback should be called once but not again after returning < + * frame_count. */ + ASSERT_EQ(cb_count, 1); + + cubeb_resampler_destroy(resampler); +} + +// gtest does not support using ASSERT_EQ and friend in a function that returns +// a value. +void +check_output(const void * input_buffer, void * output_buffer, long frame_count) +{ + ASSERT_EQ(input_buffer, nullptr); + ASSERT_EQ(frame_count, 256); + ASSERT_TRUE(!!output_buffer); +} + +long +cb_passthrough_resampler_output(cubeb_stream * /*stm*/, void * /*user_ptr*/, + const void * input_buffer, void * output_buffer, + long frame_count) +{ + check_output(input_buffer, output_buffer, frame_count); + return frame_count; +} + +TEST(cubeb, resampler_passthrough_output_only) +{ + // Test that the passthrough resampler works when there is only an output + // stream. + cubeb_stream_params output_params; + + const size_t output_channels = 2; + output_params.channels = output_channels; + output_params.rate = 44100; + output_params.format = CUBEB_SAMPLE_FLOAT32NE; + int target_rate = output_params.rate; + + cubeb_resampler * resampler = cubeb_resampler_create( + (cubeb_stream *)nullptr, nullptr, &output_params, target_rate, + cb_passthrough_resampler_output, nullptr, CUBEB_RESAMPLER_QUALITY_VOIP, + CUBEB_RESAMPLER_RECLOCK_NONE); + + float output_buffer[output_channels * 256]; + + long got; + for (uint32_t i = 0; i < 30; i++) { + got = cubeb_resampler_fill(resampler, nullptr, nullptr, output_buffer, 256); + ASSERT_EQ(got, 256); + } + + cubeb_resampler_destroy(resampler); +} + +// gtest does not support using ASSERT_EQ and friend in a function that returns +// a value. +void +check_input(const void * input_buffer, void * output_buffer, long frame_count) +{ + ASSERT_EQ(output_buffer, nullptr); + ASSERT_EQ(frame_count, 256); + ASSERT_TRUE(!!input_buffer); +} + +long +cb_passthrough_resampler_input(cubeb_stream * /*stm*/, void * /*user_ptr*/, + const void * input_buffer, void * output_buffer, + long frame_count) +{ + check_input(input_buffer, output_buffer, frame_count); + return frame_count; +} + +TEST(cubeb, resampler_passthrough_input_only) +{ + // Test that the passthrough resampler works when there is only an output + // stream. + cubeb_stream_params input_params; + + const size_t input_channels = 2; + input_params.channels = input_channels; + input_params.rate = 44100; + input_params.format = CUBEB_SAMPLE_FLOAT32NE; + int target_rate = input_params.rate; + + cubeb_resampler * resampler = cubeb_resampler_create( + (cubeb_stream *)nullptr, &input_params, nullptr, target_rate, + cb_passthrough_resampler_input, nullptr, CUBEB_RESAMPLER_QUALITY_VOIP, + CUBEB_RESAMPLER_RECLOCK_NONE); + + float input_buffer[input_channels * 256]; + + long got; + for (uint32_t i = 0; i < 30; i++) { + long int frames = 256; + got = cubeb_resampler_fill(resampler, input_buffer, &frames, nullptr, 0); + ASSERT_EQ(got, 256); + } + + cubeb_resampler_destroy(resampler); +} + +template <typename T> +long +seq(T * array, int stride, long start, long count) +{ + uint32_t output_idx = 0; + for (int i = 0; i < count; i++) { + for (int j = 0; j < stride; j++) { + array[output_idx + j] = static_cast<T>(start + i); + } + output_idx += stride; + } + return start + count; +} + +template <typename T> +void +is_seq(T * array, int stride, long count, long expected_start) +{ + uint32_t output_index = 0; + for (long i = 0; i < count; i++) { + for (int j = 0; j < stride; j++) { + ASSERT_EQ(array[output_index + j], expected_start + i); + } + output_index += stride; + } +} + +template <typename T> +void +is_not_seq(T * array, int stride, long count, long expected_start) +{ + uint32_t output_index = 0; + for (long i = 0; i < count; i++) { + for (int j = 0; j < stride; j++) { + ASSERT_NE(array[output_index + j], expected_start + i); + } + output_index += stride; + } +} + +struct closure { + int input_channel_count; +}; + +// gtest does not support using ASSERT_EQ and friend in a function that returns +// a value. +template <typename T> +void +check_duplex(const T * input_buffer, T * output_buffer, long frame_count, + int input_channel_count) +{ + ASSERT_EQ(frame_count, 256); + // Silence scan-build warning. + ASSERT_TRUE(!!output_buffer); + assert(output_buffer); + ASSERT_TRUE(!!input_buffer); + assert(input_buffer); + + int output_index = 0; + int input_index = 0; + for (int i = 0; i < frame_count; i++) { + // output is two channels, input one or two channels. + if (input_channel_count == 1) { + output_buffer[output_index] = output_buffer[output_index + 1] = + input_buffer[i]; + } else if (input_channel_count == 2) { + output_buffer[output_index] = input_buffer[input_index]; + output_buffer[output_index + 1] = input_buffer[input_index + 1]; + } + output_index += 2; + input_index += input_channel_count; + } +} + +long +cb_passthrough_resampler_duplex(cubeb_stream * /*stm*/, void * user_ptr, + const void * input_buffer, void * output_buffer, + long frame_count) +{ + closure * c = reinterpret_cast<closure *>(user_ptr); + check_duplex<float>(static_cast<const float *>(input_buffer), + static_cast<float *>(output_buffer), frame_count, + c->input_channel_count); + return frame_count; +} + +TEST(cubeb, resampler_passthrough_duplex_callback_reordering) +{ + // Test that when pre-buffering on resampler creation, we can survive an input + // callback being delayed. + + cubeb_stream_params input_params; + cubeb_stream_params output_params; + + const int input_channels = 1; + const int output_channels = 2; + + input_params.channels = input_channels; + input_params.rate = 44100; + input_params.format = CUBEB_SAMPLE_FLOAT32NE; + + output_params.channels = output_channels; + output_params.rate = input_params.rate; + output_params.format = CUBEB_SAMPLE_FLOAT32NE; + + int target_rate = input_params.rate; + + closure c; + c.input_channel_count = input_channels; + + cubeb_resampler * resampler = cubeb_resampler_create( + (cubeb_stream *)nullptr, &input_params, &output_params, target_rate, + cb_passthrough_resampler_duplex, &c, CUBEB_RESAMPLER_QUALITY_VOIP, + CUBEB_RESAMPLER_RECLOCK_NONE); + + const long BUF_BASE_SIZE = 256; + float input_buffer_prebuffer[input_channels * BUF_BASE_SIZE * 2]; + float input_buffer_glitch[input_channels * BUF_BASE_SIZE * 2]; + float input_buffer_normal[input_channels * BUF_BASE_SIZE]; + float output_buffer[output_channels * BUF_BASE_SIZE]; + + long seq_idx = 0; + long output_seq_idx = 0; + + long prebuffer_frames = + ARRAY_LENGTH(input_buffer_prebuffer) / input_params.channels; + seq_idx = + seq(input_buffer_prebuffer, input_channels, seq_idx, prebuffer_frames); + + long got = + cubeb_resampler_fill(resampler, input_buffer_prebuffer, &prebuffer_frames, + output_buffer, BUF_BASE_SIZE); + + output_seq_idx += BUF_BASE_SIZE; + + // prebuffer_frames will hold the frames used by the resampler. + ASSERT_EQ(prebuffer_frames, BUF_BASE_SIZE); + ASSERT_EQ(got, BUF_BASE_SIZE); + + for (uint32_t i = 0; i < 300; i++) { + long int frames = BUF_BASE_SIZE; + // Simulate that sometimes, we don't have the input callback on time + if (i != 0 && (i % 100) == 0) { + long zero = 0; + got = + cubeb_resampler_fill(resampler, input_buffer_normal /* unused here */, + &zero, output_buffer, BUF_BASE_SIZE); + is_seq(output_buffer, 2, BUF_BASE_SIZE, output_seq_idx); + output_seq_idx += BUF_BASE_SIZE; + } else if (i != 0 && (i % 100) == 1) { + // if this is the case, the on the next iteration, we'll have twice the + // amount of input frames + seq_idx = + seq(input_buffer_glitch, input_channels, seq_idx, BUF_BASE_SIZE * 2); + frames = 2 * BUF_BASE_SIZE; + got = cubeb_resampler_fill(resampler, input_buffer_glitch, &frames, + output_buffer, BUF_BASE_SIZE); + is_seq(output_buffer, 2, BUF_BASE_SIZE, output_seq_idx); + output_seq_idx += BUF_BASE_SIZE; + } else { + // normal case + seq_idx = + seq(input_buffer_normal, input_channels, seq_idx, BUF_BASE_SIZE); + long normal_input_frame_count = 256; + got = cubeb_resampler_fill(resampler, input_buffer_normal, + &normal_input_frame_count, output_buffer, + BUF_BASE_SIZE); + is_seq(output_buffer, 2, BUF_BASE_SIZE, output_seq_idx); + output_seq_idx += BUF_BASE_SIZE; + } + ASSERT_EQ(got, BUF_BASE_SIZE); + } + + cubeb_resampler_destroy(resampler); +} + +// Artificially simulate output thread underruns, +// by building up artificial delay in the input. +// Check that the frame drop logic kicks in. +TEST(cubeb, resampler_drift_drop_data) +{ + for (uint32_t input_channels = 1; input_channels < 3; input_channels++) { + cubeb_stream_params input_params; + cubeb_stream_params output_params; + + const int output_channels = 2; + const int sample_rate = 44100; + + input_params.channels = input_channels; + input_params.rate = sample_rate; + input_params.format = CUBEB_SAMPLE_FLOAT32NE; + + output_params.channels = output_channels; + output_params.rate = sample_rate; + output_params.format = CUBEB_SAMPLE_FLOAT32NE; + + int target_rate = input_params.rate; + + closure c; + c.input_channel_count = input_channels; + + cubeb_resampler * resampler = cubeb_resampler_create( + (cubeb_stream *)nullptr, &input_params, &output_params, target_rate, + cb_passthrough_resampler_duplex, &c, CUBEB_RESAMPLER_QUALITY_VOIP, + CUBEB_RESAMPLER_RECLOCK_NONE); + + const long BUF_BASE_SIZE = 256; + + // The factor by which the deadline is missed. This is intentionally + // kind of large to trigger the frame drop quickly. In real life, multiple + // smaller under-runs would accumulate. + const long UNDERRUN_FACTOR = 10; + // Number buffer used for pre-buffering, that some backends do. + const long PREBUFFER_FACTOR = 2; + + std::vector<float> input_buffer_prebuffer(input_channels * BUF_BASE_SIZE * + PREBUFFER_FACTOR); + std::vector<float> input_buffer_glitch(input_channels * BUF_BASE_SIZE * + UNDERRUN_FACTOR); + std::vector<float> input_buffer_normal(input_channels * BUF_BASE_SIZE); + std::vector<float> output_buffer(output_channels * BUF_BASE_SIZE); + + long seq_idx = 0; + long output_seq_idx = 0; + + long prebuffer_frames = + input_buffer_prebuffer.size() / input_params.channels; + seq_idx = seq(input_buffer_prebuffer.data(), input_channels, seq_idx, + prebuffer_frames); + + long got = cubeb_resampler_fill(resampler, input_buffer_prebuffer.data(), + &prebuffer_frames, output_buffer.data(), + BUF_BASE_SIZE); + + output_seq_idx += BUF_BASE_SIZE; + + // prebuffer_frames will hold the frames used by the resampler. + ASSERT_EQ(prebuffer_frames, BUF_BASE_SIZE); + ASSERT_EQ(got, BUF_BASE_SIZE); + + for (uint32_t i = 0; i < 300; i++) { + long int frames = BUF_BASE_SIZE; + if (i != 0 && (i % 100) == 1) { + // Once in a while, the output thread misses its deadline. + // The input thread still produces data, so it ends up accumulating. + // Simulate this by providing a much bigger input buffer. Check that the + // sequence is now unaligned, meaning we've dropped data to keep + // everything in sync. + seq_idx = seq(input_buffer_glitch.data(), input_channels, seq_idx, + BUF_BASE_SIZE * UNDERRUN_FACTOR); + frames = BUF_BASE_SIZE * UNDERRUN_FACTOR; + got = + cubeb_resampler_fill(resampler, input_buffer_glitch.data(), &frames, + output_buffer.data(), BUF_BASE_SIZE); + is_seq(output_buffer.data(), 2, BUF_BASE_SIZE, output_seq_idx); + output_seq_idx += BUF_BASE_SIZE; + } else if (i != 0 && (i % 100) == 2) { + // On the next iteration, the sequence should be broken + seq_idx = seq(input_buffer_normal.data(), input_channels, seq_idx, + BUF_BASE_SIZE); + long normal_input_frame_count = 256; + got = cubeb_resampler_fill(resampler, input_buffer_normal.data(), + &normal_input_frame_count, + output_buffer.data(), BUF_BASE_SIZE); + is_not_seq(output_buffer.data(), output_channels, BUF_BASE_SIZE, + output_seq_idx); + // Reclock so that we can use is_seq again. + output_seq_idx = output_buffer[BUF_BASE_SIZE * output_channels - 1] + 1; + } else { + // normal case + seq_idx = seq(input_buffer_normal.data(), input_channels, seq_idx, + BUF_BASE_SIZE); + long normal_input_frame_count = 256; + got = cubeb_resampler_fill(resampler, input_buffer_normal.data(), + &normal_input_frame_count, + output_buffer.data(), BUF_BASE_SIZE); + is_seq(output_buffer.data(), output_channels, BUF_BASE_SIZE, + output_seq_idx); + output_seq_idx += BUF_BASE_SIZE; + } + ASSERT_EQ(got, BUF_BASE_SIZE); + } + + cubeb_resampler_destroy(resampler); + } +} + +static long +passthrough_resampler_fill_eq_input(cubeb_stream * stream, void * user_ptr, + void const * input_buffer, + void * output_buffer, long nframes) +{ + // gtest does not support using ASSERT_EQ and friends in a + // function that returns a value. + [nframes, input_buffer]() { + ASSERT_EQ(nframes, 32); + const float * input = static_cast<const float *>(input_buffer); + for (int i = 0; i < 64; ++i) { + ASSERT_FLOAT_EQ(input[i], 0.01 * i); + } + }(); + return nframes; +} + +TEST(cubeb, passthrough_resampler_fill_eq_input) +{ + uint32_t channels = 2; + uint32_t sample_rate = 44100; + passthrough_resampler<float> resampler = + passthrough_resampler<float>(nullptr, passthrough_resampler_fill_eq_input, + nullptr, channels, sample_rate); + + long input_frame_count = 32; + long output_frame_count = 32; + float input[64] = {}; + float output[64] = {}; + for (uint32_t i = 0; i < input_frame_count * channels; ++i) { + input[i] = 0.01 * i; + } + long got = + resampler.fill(input, &input_frame_count, output, output_frame_count); + ASSERT_EQ(got, output_frame_count); + // Input frames used must be equal to output frames. + ASSERT_EQ(input_frame_count, output_frame_count); +} + +static long +passthrough_resampler_fill_short_input(cubeb_stream * stream, void * user_ptr, + void const * input_buffer, + void * output_buffer, long nframes) +{ + // gtest does not support using ASSERT_EQ and friends in a + // function that returns a value. + [nframes, input_buffer]() { + ASSERT_EQ(nframes, 32); + const float * input = static_cast<const float *>(input_buffer); + // First part contains the input + for (int i = 0; i < 32; ++i) { + ASSERT_FLOAT_EQ(input[i], 0.01 * i); + } + // missing part contains silence + for (int i = 32; i < 64; ++i) { + ASSERT_FLOAT_EQ(input[i], 0.0); + } + }(); + return nframes; +} + +TEST(cubeb, passthrough_resampler_fill_short_input) +{ + uint32_t channels = 2; + uint32_t sample_rate = 44100; + passthrough_resampler<float> resampler = passthrough_resampler<float>( + nullptr, passthrough_resampler_fill_short_input, nullptr, channels, + sample_rate); + + long input_frame_count = 16; + long output_frame_count = 32; + float input[64] = {}; + float output[64] = {}; + for (uint32_t i = 0; i < input_frame_count * channels; ++i) { + input[i] = 0.01 * i; + } + long got = + resampler.fill(input, &input_frame_count, output, output_frame_count); + ASSERT_EQ(got, output_frame_count); + // Input frames used are less than the output frames due to glitch. + ASSERT_EQ(input_frame_count, output_frame_count - 16); +} + +static long +passthrough_resampler_fill_input_left(cubeb_stream * stream, void * user_ptr, + void const * input_buffer, + void * output_buffer, long nframes) +{ + // gtest does not support using ASSERT_EQ and friends in a + // function that returns a value. + int iteration = *static_cast<int *>(user_ptr); + if (iteration == 1) { + [nframes, input_buffer]() { + ASSERT_EQ(nframes, 32); + const float * input = static_cast<const float *>(input_buffer); + for (int i = 0; i < 64; ++i) { + ASSERT_FLOAT_EQ(input[i], 0.01 * i); + } + }(); + } else if (iteration == 2) { + [nframes, input_buffer]() { + ASSERT_EQ(nframes, 32); + const float * input = static_cast<const float *>(input_buffer); + for (int i = 0; i < 32; ++i) { + // First part contains the reamaining input samples from previous + // iteration (since they were more). + ASSERT_FLOAT_EQ(input[i], 0.01 * (i + 64)); + // next part contains the new buffer + ASSERT_FLOAT_EQ(input[i + 32], 0.01 * i); + } + }(); + } else if (iteration == 3) { + [nframes, input_buffer]() { + ASSERT_EQ(nframes, 32); + const float * input = static_cast<const float *>(input_buffer); + for (int i = 0; i < 32; ++i) { + // First part (16 frames) contains the reamaining input samples + // from previous iteration (since they were more). + ASSERT_FLOAT_EQ(input[i], 0.01 * (i + 32)); + } + for (int i = 0; i < 16; ++i) { + // next part (8 frames) contains the new input buffer. + ASSERT_FLOAT_EQ(input[i + 32], 0.01 * i); + // last part (8 frames) contains silence. + ASSERT_FLOAT_EQ(input[i + 32 + 16], 0.0); + } + }(); + } + return nframes; +} + +TEST(cubeb, passthrough_resampler_fill_input_left) +{ + const uint32_t channels = 2; + const uint32_t sample_rate = 44100; + int iteration = 0; + passthrough_resampler<float> resampler = passthrough_resampler<float>( + nullptr, passthrough_resampler_fill_input_left, &iteration, channels, + sample_rate); + + long input_frame_count = 48; // 32 + 16 + const long output_frame_count = 32; + float input[96] = {}; + float output[64] = {}; + for (uint32_t i = 0; i < input_frame_count * channels; ++i) { + input[i] = 0.01 * i; + } + + // 1st iteration, add the extra input. + iteration = 1; + long got = + resampler.fill(input, &input_frame_count, output, output_frame_count); + ASSERT_EQ(got, output_frame_count); + // Input frames used must be equal to output frames. + ASSERT_EQ(input_frame_count, output_frame_count); + + // 2st iteration, use the extra input from previous iteration, + // 16 frames are remaining in the input buffer. + input_frame_count = 32; // we need 16 input frames but we get more; + iteration = 2; + got = resampler.fill(input, &input_frame_count, output, output_frame_count); + ASSERT_EQ(got, output_frame_count); + // Input frames used must be equal to output frames. + ASSERT_EQ(input_frame_count, output_frame_count); + + // 3rd iteration, use the extra input from previous iteration. + // 16 frames are remaining in the input buffer. + input_frame_count = 16 - 8; // We need 16 more input frames but we only get 8. + iteration = 3; + got = resampler.fill(input, &input_frame_count, output, output_frame_count); + ASSERT_EQ(got, output_frame_count); + // Input frames used are less than the output frames due to glitch. + ASSERT_EQ(input_frame_count, output_frame_count - 8); +} + +TEST(cubeb, individual_methods) +{ + const uint32_t channels = 2; + const uint32_t sample_rate = 44100; + const uint32_t frames = 256; + + delay_line<float> dl(10, channels, sample_rate); + uint32_t frames_needed1 = dl.input_needed_for_output(0); + ASSERT_EQ(frames_needed1, 0u); + + cubeb_resampler_speex_one_way<float> one_way( + channels, sample_rate, sample_rate, CUBEB_RESAMPLER_QUALITY_DEFAULT); + float buffer[channels * frames] = {0.0}; + // Add all frames in the resampler's internal buffer. + one_way.input(buffer, frames); + // Ask for less than the existing frames, this would create a uint overlflow + // without the fix. + uint32_t frames_needed2 = one_way.input_needed_for_output(0); + ASSERT_EQ(frames_needed2, 0u); +} + +#undef NOMINMAX +#undef DUMP_ARRAYS diff --git a/media/libcubeb/test/test_ring_array.cpp b/media/libcubeb/test/test_ring_array.cpp new file mode 100644 index 0000000000..a364684c77 --- /dev/null +++ b/media/libcubeb/test/test_ring_array.cpp @@ -0,0 +1,72 @@ +#include "gtest/gtest.h" +#ifdef __APPLE__ +#include "cubeb/cubeb.h" +#include "cubeb_ring_array.h" +#include <CoreAudio/CoreAudioTypes.h> +#include <iostream> +#include <string.h> + +TEST(cubeb, ring_array) +{ + ring_array ra; + + ASSERT_EQ(ring_array_init(&ra, 0, 0, 1, 1), CUBEB_ERROR_INVALID_PARAMETER); + ASSERT_EQ(ring_array_init(&ra, 1, 0, 0, 1), CUBEB_ERROR_INVALID_PARAMETER); + + unsigned int capacity = 8; + ring_array_init(&ra, capacity, sizeof(int), 1, 1); + int verify_data[capacity]; // {1,2,3,4,5,6,7,8}; + AudioBuffer * p_data = NULL; + + for (unsigned int i = 0; i < capacity; ++i) { + verify_data[i] = i; // in case capacity change value + *(int *)ra.buffer_array[i].mData = i; + ASSERT_EQ(ra.buffer_array[i].mDataByteSize, sizeof(int)); + ASSERT_EQ(ra.buffer_array[i].mNumberChannels, 1u); + } + + /* Get store buffers*/ + for (unsigned int i = 0; i < capacity; ++i) { + p_data = ring_array_get_free_buffer(&ra); + ASSERT_NE(p_data, nullptr); + ASSERT_EQ(*(int *)p_data->mData, verify_data[i]); + } + /*Now array is full extra store should give NULL*/ + ASSERT_EQ(ring_array_get_free_buffer(&ra), nullptr); + /* Get fetch buffers*/ + for (unsigned int i = 0; i < capacity; ++i) { + p_data = ring_array_get_data_buffer(&ra); + ASSERT_NE(p_data, nullptr); + ASSERT_EQ(*(int *)p_data->mData, verify_data[i]); + } + /*Now array is empty extra fetch should give NULL*/ + ASSERT_EQ(ring_array_get_data_buffer(&ra), nullptr); + + p_data = NULL; + /* Repeated store fetch should can go for ever*/ + for (unsigned int i = 0; i < 2 * capacity; ++i) { + p_data = ring_array_get_free_buffer(&ra); + ASSERT_NE(p_data, nullptr); + ASSERT_EQ(ring_array_get_data_buffer(&ra), p_data); + } + + p_data = NULL; + /* Verify/modify buffer data*/ + for (unsigned int i = 0; i < capacity; ++i) { + p_data = ring_array_get_free_buffer(&ra); + ASSERT_NE(p_data, nullptr); + ASSERT_EQ(*((int *)p_data->mData), verify_data[i]); + (*((int *)p_data->mData))++; // Modify data + } + for (unsigned int i = 0; i < capacity; ++i) { + p_data = ring_array_get_data_buffer(&ra); + ASSERT_NE(p_data, nullptr); + ASSERT_EQ(*((int *)p_data->mData), + verify_data[i] + 1); // Verify modified data + } + + ring_array_destroy(&ra); +} +#else +TEST(cubeb, DISABLED_ring_array) {} +#endif diff --git a/media/libcubeb/test/test_ring_buffer.cpp b/media/libcubeb/test/test_ring_buffer.cpp new file mode 100644 index 0000000000..fffe1087e4 --- /dev/null +++ b/media/libcubeb/test/test_ring_buffer.cpp @@ -0,0 +1,229 @@ +/* + * Copyright © 2016 Mozilla Foundation + * + * This program is made available under an ISC-style license. See the + * accompanying file LICENSE for details. + */ + +#ifndef NOMINMAX +#define NOMINMAX +#endif + +#include "cubeb_ringbuffer.h" +#include "gtest/gtest.h" +#include <chrono> +#include <iostream> +#include <thread> + +/* Generate a monotonically increasing sequence of numbers. */ +template <typename T> class sequence_generator { +public: + sequence_generator(size_t channels) : channels(channels) {} + void get(T * elements, size_t frames) + { + for (size_t i = 0; i < frames; i++) { + for (size_t c = 0; c < channels; c++) { + elements[i * channels + c] = static_cast<T>(index_); + } + index_++; + } + } + void rewind(size_t frames) { index_ -= frames; } + +private: + size_t index_ = 0; + size_t channels = 0; +}; + +/* Checks that a sequence is monotonically increasing. */ +template <typename T> class sequence_verifier { +public: + sequence_verifier(size_t channels) : channels(channels) {} + void check(T * elements, size_t frames) + { + for (size_t i = 0; i < frames; i++) { + for (size_t c = 0; c < channels; c++) { + if (elements[i * channels + c] != static_cast<T>(index_)) { + std::cerr << "Element " << i << " is different. Expected " + << static_cast<T>(index_) << ", got " << elements[i] + << ". (channel count: " << channels << ")." << std::endl; + ASSERT_TRUE(false); + } + } + index_++; + } + } + +private: + size_t index_ = 0; + size_t channels = 0; +}; + +template <typename T> +void +test_ring(lock_free_audio_ring_buffer<T> & buf, int channels, + int capacity_frames) +{ + std::unique_ptr<T[]> seq(new T[capacity_frames * channels]); + sequence_generator<T> gen(channels); + sequence_verifier<T> checker(channels); + + int iterations = 1002; + + const int block_size = 128; + + while (iterations--) { + gen.get(seq.get(), block_size); + int rv = buf.enqueue(seq.get(), block_size); + ASSERT_EQ(rv, block_size); + PodZero(seq.get(), block_size); + rv = buf.dequeue(seq.get(), block_size); + ASSERT_EQ(rv, block_size); + checker.check(seq.get(), block_size); + } +} + +template <typename T> +void +test_ring_multi(lock_free_audio_ring_buffer<T> & buf, int channels, + int capacity_frames) +{ + sequence_verifier<T> checker(channels); + std::unique_ptr<T[]> out_buffer(new T[capacity_frames * channels]); + + const int block_size = 128; + + std::thread t([=, &buf] { + int iterations = 1002; + std::unique_ptr<T[]> in_buffer(new T[capacity_frames * channels]); + sequence_generator<T> gen(channels); + + while (iterations--) { + std::this_thread::yield(); + gen.get(in_buffer.get(), block_size); + int rv = buf.enqueue(in_buffer.get(), block_size); + ASSERT_TRUE(rv <= block_size); + if (rv != block_size) { + gen.rewind(block_size - rv); + } + } + }); + + int remaining = 1002; + + while (remaining--) { + std::this_thread::yield(); + int rv = buf.dequeue(out_buffer.get(), block_size); + ASSERT_TRUE(rv <= block_size); + checker.check(out_buffer.get(), rv); + } + + t.join(); +} + +template <typename T> +void +basic_api_test(T & ring) +{ + ASSERT_EQ(ring.capacity(), 128); + + ASSERT_EQ(ring.available_read(), 0); + ASSERT_EQ(ring.available_write(), 128); + + int rv = ring.enqueue_default(63); + + ASSERT_TRUE(rv == 63); + ASSERT_EQ(ring.available_read(), 63); + ASSERT_EQ(ring.available_write(), 65); + + rv = ring.enqueue_default(65); + + ASSERT_EQ(rv, 65); + ASSERT_EQ(ring.available_read(), 128); + ASSERT_EQ(ring.available_write(), 0); + + rv = ring.dequeue(nullptr, 63); + + ASSERT_EQ(ring.available_read(), 65); + ASSERT_EQ(ring.available_write(), 63); + + rv = ring.dequeue(nullptr, 65); + + ASSERT_EQ(ring.available_read(), 0); + ASSERT_EQ(ring.available_write(), 128); +} + +void +test_reset_api() +{ + const size_t ring_buffer_size = 128; + const size_t enqueue_size = ring_buffer_size / 2; + + lock_free_queue<float> ring(ring_buffer_size); + std::thread t([=, &ring] { + std::unique_ptr<float[]> in_buffer(new float[enqueue_size]); + ring.enqueue(in_buffer.get(), enqueue_size); + }); + + t.join(); + + ring.reset_thread_ids(); + + // Enqueue with a different thread. We have reset the thread ID + // in the ring buffer, this should work. + std::thread t2([=, &ring] { + std::unique_ptr<float[]> in_buffer(new float[enqueue_size]); + ring.enqueue(in_buffer.get(), enqueue_size); + }); + + t2.join(); + + ASSERT_TRUE(true); +} + +TEST(cubeb, ring_buffer) +{ + /* Basic API test. */ + const int min_channels = 1; + const int max_channels = 10; + const int min_capacity = 199; + const int max_capacity = 1277; + const int capacity_increment = 27; + + lock_free_queue<float> q1(128); + basic_api_test(q1); + lock_free_queue<short> q2(128); + basic_api_test(q2); + + for (size_t channels = min_channels; channels < max_channels; channels++) { + lock_free_audio_ring_buffer<float> q3(channels, 128); + basic_api_test(q3); + lock_free_audio_ring_buffer<short> q4(channels, 128); + basic_api_test(q4); + } + + /* Single thread testing. */ + /* Test mono to 9.1 */ + for (size_t channels = min_channels; channels < max_channels; channels++) { + /* Use non power-of-two numbers to catch edge-cases. */ + for (size_t capacity_frames = min_capacity; capacity_frames < max_capacity; + capacity_frames += capacity_increment) { + lock_free_audio_ring_buffer<float> ring(channels, capacity_frames); + test_ring(ring, channels, capacity_frames); + } + } + + /* Multi thread testing */ + for (size_t channels = min_channels; channels < max_channels; channels++) { + /* Use non power-of-two numbers to catch edge-cases. */ + for (size_t capacity_frames = min_capacity; capacity_frames < max_capacity; + capacity_frames += capacity_increment) { + lock_free_audio_ring_buffer<short> ring(channels, capacity_frames); + test_ring_multi(ring, channels, capacity_frames); + } + } + + test_reset_api(); +} + +#undef NOMINMAX diff --git a/media/libcubeb/test/test_sanity.cpp b/media/libcubeb/test/test_sanity.cpp new file mode 100644 index 0000000000..17d3c542b4 --- /dev/null +++ b/media/libcubeb/test/test_sanity.cpp @@ -0,0 +1,721 @@ +/* + * Copyright © 2011 Mozilla Foundation + * + * This program is made available under an ISC-style license. See the + * accompanying file LICENSE for details. + */ +#include "gtest/gtest.h" +#if !defined(_XOPEN_SOURCE) +#define _XOPEN_SOURCE 600 +#endif +#include "cubeb/cubeb.h" +#include <atomic> +#include <math.h> +#include <stdio.h> +#include <string.h> + +// #define ENABLE_NORMAL_LOG +// #define ENABLE_VERBOSE_LOG +#include "common.h" + +#define STREAM_RATE 44100 +#define STREAM_LATENCY 100 * STREAM_RATE / 1000 +#define STREAM_CHANNELS 1 +#define STREAM_LAYOUT CUBEB_LAYOUT_MONO +#define STREAM_FORMAT CUBEB_SAMPLE_S16LE + +int +is_windows_7() +{ +#ifdef __MINGW32__ + fprintf(stderr, + "Warning: this test was built with MinGW.\n" + "MinGW does not contain necessary version checking infrastructure. " + "Claiming to be Windows 7, even if we're not.\n"); + return 1; +#endif +#if (defined(_WIN32) || defined(__WIN32__)) && (!defined(__MINGW32__)) + OSVERSIONINFOEX osvi; + DWORDLONG condition_mask = 0; + + ZeroMemory(&osvi, sizeof(OSVERSIONINFOEX)); + osvi.dwOSVersionInfoSize = sizeof(OSVERSIONINFOEX); + + // NT 6.1 is Windows 7 + osvi.dwMajorVersion = 6; + osvi.dwMinorVersion = 1; + + VER_SET_CONDITION(condition_mask, VER_MAJORVERSION, VER_EQUAL); + VER_SET_CONDITION(condition_mask, VER_MINORVERSION, VER_GREATER_EQUAL); + + return VerifyVersionInfo(&osvi, VER_MAJORVERSION | VER_MINORVERSION, + condition_mask); +#else + return 0; +#endif +} + +static int dummy; +static std::atomic<uint64_t> total_frames_written; +static int delay_callback; + +static long +test_data_callback(cubeb_stream * stm, void * user_ptr, + const void * /*inputbuffer*/, void * outputbuffer, + long nframes) +{ + EXPECT_TRUE(stm && user_ptr == &dummy && outputbuffer && nframes > 0); + assert(outputbuffer); + memset(outputbuffer, 0, nframes * sizeof(short)); + + total_frames_written += nframes; + if (delay_callback) { + delay(10); + } + return nframes; +} + +void +test_state_callback(cubeb_stream * /*stm*/, void * /*user_ptr*/, + cubeb_state /*state*/) +{ +} + +TEST(cubeb, init_destroy_context) +{ + int r; + cubeb * ctx; + char const * backend_id; + + r = common_init(&ctx, "test_sanity"); + ASSERT_EQ(r, CUBEB_OK); + ASSERT_NE(ctx, nullptr); + + backend_id = cubeb_get_backend_id(ctx); + ASSERT_TRUE(backend_id); + + fprintf(stderr, "Backend: %s\n", backend_id); + + cubeb_destroy(ctx); +} + +TEST(cubeb, init_destroy_multiple_contexts) +{ + size_t i; + int r; + cubeb * ctx[4]; + int order[4] = {2, 0, 3, 1}; + ASSERT_EQ(ARRAY_LENGTH(ctx), ARRAY_LENGTH(order)); + + for (i = 0; i < ARRAY_LENGTH(ctx); ++i) { + r = common_init(&ctx[i], NULL); + ASSERT_EQ(r, CUBEB_OK); + ASSERT_NE(ctx[i], nullptr); + } + + /* destroy in a different order */ + for (i = 0; i < ARRAY_LENGTH(ctx); ++i) { + cubeb_destroy(ctx[order[i]]); + } +} + +TEST(cubeb, context_variables) +{ + int r; + cubeb * ctx; + uint32_t value; + cubeb_stream_params params; + + r = common_init(&ctx, "test_context_variables"); + ASSERT_EQ(r, CUBEB_OK); + ASSERT_NE(ctx, nullptr); + + params.channels = STREAM_CHANNELS; + params.format = STREAM_FORMAT; + params.rate = STREAM_RATE; + params.layout = STREAM_LAYOUT; + params.prefs = CUBEB_STREAM_PREF_NONE; + + r = cubeb_get_min_latency(ctx, ¶ms, &value); + ASSERT_TRUE(r == CUBEB_OK || r == CUBEB_ERROR_NOT_SUPPORTED); + if (r == CUBEB_OK) { + ASSERT_TRUE(value > 0); + } + + r = cubeb_get_preferred_sample_rate(ctx, &value); + ASSERT_TRUE(r == CUBEB_OK || r == CUBEB_ERROR_NOT_SUPPORTED); + if (r == CUBEB_OK) { + ASSERT_TRUE(value > 0); + } + + cubeb_destroy(ctx); +} + +TEST(cubeb, init_destroy_stream) +{ + int r; + cubeb * ctx; + cubeb_stream * stream; + cubeb_stream_params params; + + r = common_init(&ctx, "test_sanity"); + ASSERT_EQ(r, CUBEB_OK); + ASSERT_NE(ctx, nullptr); + + params.format = STREAM_FORMAT; + params.rate = STREAM_RATE; + params.channels = STREAM_CHANNELS; + params.layout = STREAM_LAYOUT; + params.prefs = CUBEB_STREAM_PREF_NONE; + + r = cubeb_stream_init(ctx, &stream, "test", NULL, NULL, NULL, ¶ms, + STREAM_LATENCY, test_data_callback, test_state_callback, + &dummy); + ASSERT_EQ(r, CUBEB_OK); + ASSERT_NE(stream, nullptr); + + cubeb_stream_destroy(stream); + cubeb_destroy(ctx); +} + +TEST(cubeb, init_destroy_multiple_streams) +{ + size_t i; + int r; + cubeb * ctx; + cubeb_stream * stream[8]; + cubeb_stream_params params; + + r = common_init(&ctx, "test_sanity"); + ASSERT_EQ(r, CUBEB_OK); + ASSERT_NE(ctx, nullptr); + + params.format = STREAM_FORMAT; + params.rate = STREAM_RATE; + params.channels = STREAM_CHANNELS; + params.layout = STREAM_LAYOUT; + params.prefs = CUBEB_STREAM_PREF_NONE; + + for (i = 0; i < ARRAY_LENGTH(stream); ++i) { + r = cubeb_stream_init(ctx, &stream[i], "test", NULL, NULL, NULL, ¶ms, + STREAM_LATENCY, test_data_callback, + test_state_callback, &dummy); + ASSERT_EQ(r, CUBEB_OK); + ASSERT_NE(stream[i], nullptr); + } + + for (i = 0; i < ARRAY_LENGTH(stream); ++i) { + cubeb_stream_destroy(stream[i]); + } + + cubeb_destroy(ctx); +} + +TEST(cubeb, configure_stream) +{ + int r; + cubeb * ctx; + cubeb_stream * stream; + cubeb_stream_params params; + + r = common_init(&ctx, "test_sanity"); + ASSERT_EQ(r, CUBEB_OK); + ASSERT_NE(ctx, nullptr); + + params.format = STREAM_FORMAT; + params.rate = STREAM_RATE; + params.channels = 2; + params.layout = CUBEB_LAYOUT_STEREO; + params.prefs = CUBEB_STREAM_PREF_NONE; + + r = cubeb_stream_init(ctx, &stream, "test", NULL, NULL, NULL, ¶ms, + STREAM_LATENCY, test_data_callback, test_state_callback, + &dummy); + ASSERT_EQ(r, CUBEB_OK); + ASSERT_NE(stream, nullptr); + + r = cubeb_stream_set_volume(stream, 1.0f); + ASSERT_TRUE(r == 0 || r == CUBEB_ERROR_NOT_SUPPORTED); + + r = cubeb_stream_set_name(stream, "test 2"); + ASSERT_TRUE(r == 0 || r == CUBEB_ERROR_NOT_SUPPORTED); + + cubeb_stream_destroy(stream); + cubeb_destroy(ctx); +} + +TEST(cubeb, configure_stream_undefined_layout) +{ + int r; + cubeb * ctx; + cubeb_stream * stream; + cubeb_stream_params params; + + r = common_init(&ctx, "test_sanity"); + ASSERT_EQ(r, CUBEB_OK); + ASSERT_NE(ctx, nullptr); + + params.format = STREAM_FORMAT; + params.rate = STREAM_RATE; + params.channels = 2; + params.layout = CUBEB_LAYOUT_UNDEFINED; + params.prefs = CUBEB_STREAM_PREF_NONE; + + r = cubeb_stream_init(ctx, &stream, "test", NULL, NULL, NULL, ¶ms, + STREAM_LATENCY, test_data_callback, test_state_callback, + &dummy); + ASSERT_EQ(r, CUBEB_OK); + ASSERT_NE(stream, nullptr); + + r = cubeb_stream_start(stream); + ASSERT_EQ(r, CUBEB_OK); + + delay(100); + + r = cubeb_stream_stop(stream); + ASSERT_EQ(r, CUBEB_OK); + + cubeb_stream_destroy(stream); + cubeb_destroy(ctx); +} + +static void +test_init_start_stop_destroy_multiple_streams(int early, int delay_ms) +{ + size_t i; + int r; + cubeb * ctx; + cubeb_stream * stream[8]; + cubeb_stream_params params; + + r = common_init(&ctx, "test_sanity"); + ASSERT_EQ(r, CUBEB_OK); + ASSERT_NE(ctx, nullptr); + + params.format = STREAM_FORMAT; + params.rate = STREAM_RATE; + params.channels = STREAM_CHANNELS; + params.layout = STREAM_LAYOUT; + params.prefs = CUBEB_STREAM_PREF_NONE; + + for (i = 0; i < ARRAY_LENGTH(stream); ++i) { + r = cubeb_stream_init(ctx, &stream[i], "test", NULL, NULL, NULL, ¶ms, + STREAM_LATENCY, test_data_callback, + test_state_callback, &dummy); + ASSERT_EQ(r, CUBEB_OK); + ASSERT_NE(stream[i], nullptr); + if (early) { + r = cubeb_stream_start(stream[i]); + ASSERT_EQ(r, CUBEB_OK); + } + } + + if (!early) { + for (i = 0; i < ARRAY_LENGTH(stream); ++i) { + r = cubeb_stream_start(stream[i]); + ASSERT_EQ(r, CUBEB_OK); + } + } + + if (delay_ms) { + delay(delay_ms); + } + + if (!early) { + for (i = 0; i < ARRAY_LENGTH(stream); ++i) { + r = cubeb_stream_stop(stream[i]); + ASSERT_EQ(r, CUBEB_OK); + } + } + + for (i = 0; i < ARRAY_LENGTH(stream); ++i) { + if (early) { + r = cubeb_stream_stop(stream[i]); + ASSERT_EQ(r, CUBEB_OK); + } + cubeb_stream_destroy(stream[i]); + } + + cubeb_destroy(ctx); +} + +TEST(cubeb, init_start_stop_destroy_multiple_streams) +{ + /* Sometimes, when using WASAPI on windows 7 (vista and 8 are okay), and + * calling Activate a lot on an AudioClient, 0x800700b7 is returned. This is + * the HRESULT value for "Cannot create a file when that file already exists", + * and is not documented as a possible return value for this call. Hence, we + * try to limit the number of streams we create in this test. */ + if (!is_windows_7()) { + delay_callback = 0; + test_init_start_stop_destroy_multiple_streams(0, 0); + test_init_start_stop_destroy_multiple_streams(1, 0); + test_init_start_stop_destroy_multiple_streams(0, 150); + test_init_start_stop_destroy_multiple_streams(1, 150); + delay_callback = 1; + test_init_start_stop_destroy_multiple_streams(0, 0); + test_init_start_stop_destroy_multiple_streams(1, 0); + test_init_start_stop_destroy_multiple_streams(0, 150); + test_init_start_stop_destroy_multiple_streams(1, 150); + } +} + +TEST(cubeb, init_destroy_multiple_contexts_and_streams) +{ + size_t i, j; + int r; + cubeb * ctx[2]; + cubeb_stream * stream[8]; + cubeb_stream_params params; + size_t streams_per_ctx = ARRAY_LENGTH(stream) / ARRAY_LENGTH(ctx); + ASSERT_EQ(ARRAY_LENGTH(ctx) * streams_per_ctx, ARRAY_LENGTH(stream)); + + /* Sometimes, when using WASAPI on windows 7 (vista and 8 are okay), and + * calling Activate a lot on an AudioClient, 0x800700b7 is returned. This is + * the HRESULT value for "Cannot create a file when that file already exists", + * and is not documented as a possible return value for this call. Hence, we + * try to limit the number of streams we create in this test. */ + if (is_windows_7()) + return; + + params.format = STREAM_FORMAT; + params.rate = STREAM_RATE; + params.channels = STREAM_CHANNELS; + params.layout = STREAM_LAYOUT; + params.prefs = CUBEB_STREAM_PREF_NONE; + + for (i = 0; i < ARRAY_LENGTH(ctx); ++i) { + r = common_init(&ctx[i], "test_sanity"); + ASSERT_EQ(r, CUBEB_OK); + ASSERT_NE(ctx[i], nullptr); + + for (j = 0; j < streams_per_ctx; ++j) { + r = cubeb_stream_init(ctx[i], &stream[i * streams_per_ctx + j], "test", + NULL, NULL, NULL, ¶ms, STREAM_LATENCY, + test_data_callback, test_state_callback, &dummy); + ASSERT_EQ(r, CUBEB_OK); + ASSERT_NE(stream[i * streams_per_ctx + j], nullptr); + } + } + + for (i = 0; i < ARRAY_LENGTH(ctx); ++i) { + for (j = 0; j < streams_per_ctx; ++j) { + cubeb_stream_destroy(stream[i * streams_per_ctx + j]); + } + cubeb_destroy(ctx[i]); + } +} + +TEST(cubeb, basic_stream_operations) +{ + int r; + cubeb * ctx; + cubeb_stream * stream; + cubeb_stream_params params; + uint64_t position; + uint32_t latency; + + r = common_init(&ctx, "test_sanity"); + ASSERT_EQ(r, CUBEB_OK); + ASSERT_NE(ctx, nullptr); + + params.format = STREAM_FORMAT; + params.rate = STREAM_RATE; + params.channels = STREAM_CHANNELS; + params.layout = STREAM_LAYOUT; + params.prefs = CUBEB_STREAM_PREF_NONE; + + r = cubeb_stream_init(ctx, &stream, "test", NULL, NULL, NULL, ¶ms, + STREAM_LATENCY, test_data_callback, test_state_callback, + &dummy); + ASSERT_EQ(r, CUBEB_OK); + ASSERT_NE(stream, nullptr); + + /* position and latency before stream has started */ + r = cubeb_stream_get_position(stream, &position); + ASSERT_EQ(r, CUBEB_OK); + ASSERT_EQ(position, 0u); + + r = cubeb_stream_get_latency(stream, &latency); + ASSERT_EQ(r, CUBEB_OK); + + r = cubeb_stream_start(stream); + ASSERT_EQ(r, CUBEB_OK); + + /* position and latency after while stream running */ + r = cubeb_stream_get_position(stream, &position); + ASSERT_EQ(r, CUBEB_OK); + + r = cubeb_stream_get_latency(stream, &latency); + ASSERT_EQ(r, CUBEB_OK); + + r = cubeb_stream_stop(stream); + ASSERT_EQ(r, CUBEB_OK); + + /* position and latency after stream has stopped */ + r = cubeb_stream_get_position(stream, &position); + ASSERT_EQ(r, CUBEB_OK); + + r = cubeb_stream_get_latency(stream, &latency); + ASSERT_EQ(r, CUBEB_OK); + + cubeb_stream_destroy(stream); + cubeb_destroy(ctx); +} + +TEST(cubeb, stream_position) +{ + size_t i; + int r; + cubeb * ctx; + cubeb_stream * stream; + cubeb_stream_params params; + uint64_t position, last_position; + + total_frames_written = 0; + + r = common_init(&ctx, "test_sanity"); + ASSERT_EQ(r, CUBEB_OK); + ASSERT_NE(ctx, nullptr); + + params.format = STREAM_FORMAT; + params.rate = STREAM_RATE; + params.channels = STREAM_CHANNELS; + params.layout = STREAM_LAYOUT; + params.prefs = CUBEB_STREAM_PREF_NONE; + + r = cubeb_stream_init(ctx, &stream, "test", NULL, NULL, NULL, ¶ms, + STREAM_LATENCY, test_data_callback, test_state_callback, + &dummy); + ASSERT_EQ(r, CUBEB_OK); + ASSERT_NE(stream, nullptr); + + /* stream position should not advance before starting playback */ + r = cubeb_stream_get_position(stream, &position); + ASSERT_EQ(r, CUBEB_OK); + ASSERT_EQ(position, 0u); + + delay(500); + + r = cubeb_stream_get_position(stream, &position); + ASSERT_EQ(r, CUBEB_OK); + ASSERT_EQ(position, 0u); + + /* stream position should advance during playback */ + r = cubeb_stream_start(stream); + ASSERT_EQ(r, CUBEB_OK); + + /* XXX let start happen */ + delay(500); + + /* stream should have prefilled */ + ASSERT_TRUE(total_frames_written.load() > 0); + + r = cubeb_stream_get_position(stream, &position); + ASSERT_EQ(r, CUBEB_OK); + last_position = position; + + delay(500); + + r = cubeb_stream_get_position(stream, &position); + ASSERT_EQ(r, CUBEB_OK); + ASSERT_GE(position, last_position); + last_position = position; + + /* stream position should not exceed total frames written */ + for (i = 0; i < 5; ++i) { + r = cubeb_stream_get_position(stream, &position); + ASSERT_EQ(r, CUBEB_OK); + ASSERT_GE(position, last_position); + ASSERT_LE(position, total_frames_written.load()); + last_position = position; + delay(500); + } + + /* test that the position is valid even when starting and + * stopping the stream. */ + for (i = 0; i < 5; ++i) { + r = cubeb_stream_stop(stream); + ASSERT_EQ(r, CUBEB_OK); + r = cubeb_stream_get_position(stream, &position); + ASSERT_EQ(r, CUBEB_OK); + ASSERT_TRUE(last_position < position); + last_position = position; + delay(500); + r = cubeb_stream_start(stream); + ASSERT_EQ(r, CUBEB_OK); + delay(500); + } + + ASSERT_NE(last_position, 0u); + + /* stream position should not advance after stopping playback */ + r = cubeb_stream_stop(stream); + ASSERT_EQ(r, CUBEB_OK); + + /* XXX allow stream to settle */ + delay(500); + + r = cubeb_stream_get_position(stream, &position); + ASSERT_EQ(r, CUBEB_OK); + last_position = position; + + delay(500); + + r = cubeb_stream_get_position(stream, &position); + ASSERT_EQ(r, CUBEB_OK); + // The OpenSL backend performs client-side interpolation for its position and + // its drain implementation isn't very accurate. + if (strcmp(cubeb_get_backend_id(ctx), "opensl")) { + ASSERT_EQ(position, last_position); + } + + cubeb_stream_destroy(stream); + cubeb_destroy(ctx); +} + +static std::atomic<int> do_drain; +static std::atomic<int> got_drain; + +static long +test_drain_data_callback(cubeb_stream * stm, void * user_ptr, + const void * /*inputbuffer*/, void * outputbuffer, + long nframes) +{ + EXPECT_TRUE(stm && user_ptr == &dummy && outputbuffer && nframes > 0); + assert(outputbuffer); + if (do_drain == 1) { + do_drain = 2; + return 0; + } + /* once drain has started, callback must never be called again */ + EXPECT_TRUE(do_drain != 2); + memset(outputbuffer, 0, nframes * sizeof(short)); + total_frames_written += nframes; + return nframes; +} + +void +test_drain_state_callback(cubeb_stream * /*stm*/, void * /*user_ptr*/, + cubeb_state state) +{ + if (state == CUBEB_STATE_DRAINED) { + ASSERT_TRUE(!got_drain); + got_drain = 1; + } +} + +TEST(cubeb, drain) +{ + int r; + cubeb * ctx; + cubeb_stream * stream; + cubeb_stream_params params; + uint64_t position; + + delay_callback = 0; + total_frames_written = 0; + + r = common_init(&ctx, "test_sanity"); + ASSERT_EQ(r, CUBEB_OK); + ASSERT_NE(ctx, nullptr); + + params.format = STREAM_FORMAT; + params.rate = STREAM_RATE; + params.channels = STREAM_CHANNELS; + params.layout = STREAM_LAYOUT; + params.prefs = CUBEB_STREAM_PREF_NONE; + + r = cubeb_stream_init(ctx, &stream, "test", NULL, NULL, NULL, ¶ms, + STREAM_LATENCY, test_drain_data_callback, + test_drain_state_callback, &dummy); + ASSERT_EQ(r, CUBEB_OK); + ASSERT_NE(stream, nullptr); + + r = cubeb_stream_start(stream); + ASSERT_EQ(r, CUBEB_OK); + + delay(5000); + + do_drain = 1; + + for (;;) { + r = cubeb_stream_get_position(stream, &position); + ASSERT_EQ(r, CUBEB_OK); + if (got_drain) { + break; + } else { + ASSERT_LE(position, total_frames_written.load()); + } + delay(500); + } + + r = cubeb_stream_get_position(stream, &position); + ASSERT_EQ(r, CUBEB_OK); + ASSERT_TRUE(got_drain); + + // Really, we should be able to rely on position reaching our final written + // frame, but for now let's make sure it doesn't continue beyond that point. + // ASSERT_LE(position, total_frames_written.load()); + + cubeb_stream_destroy(stream); + cubeb_destroy(ctx); + + got_drain = 0; + do_drain = 0; +} + +TEST(cubeb, DISABLED_eos_during_prefill) +{ + // This test needs to be implemented. +} + +TEST(cubeb, DISABLED_stream_destroy_pending_drain) +{ + // This test needs to be implemented. +} + +TEST(cubeb, stable_devid) +{ + /* Test that the devid field of cubeb_device_info is stable + * (ie. compares equal) over two invocations of + * cubeb_enumerate_devices(). */ + + int r; + cubeb * ctx; + cubeb_device_collection first; + cubeb_device_collection second; + cubeb_device_type all_devices = + (cubeb_device_type)(CUBEB_DEVICE_TYPE_INPUT | CUBEB_DEVICE_TYPE_OUTPUT); + size_t n; + + r = common_init(&ctx, "test_sanity"); + ASSERT_EQ(r, CUBEB_OK); + ASSERT_NE(ctx, nullptr); + + r = cubeb_enumerate_devices(ctx, all_devices, &first); + if (r == CUBEB_ERROR_NOT_SUPPORTED) + return; + + ASSERT_EQ(r, CUBEB_OK); + + r = cubeb_enumerate_devices(ctx, all_devices, &second); + ASSERT_EQ(r, CUBEB_OK); + + ASSERT_EQ(first.count, second.count); + for (n = 0; n < first.count; n++) { + ASSERT_EQ(first.device[n].devid, second.device[n].devid); + } + + r = cubeb_device_collection_destroy(ctx, &first); + ASSERT_EQ(r, CUBEB_OK); + r = cubeb_device_collection_destroy(ctx, &second); + ASSERT_EQ(r, CUBEB_OK); + cubeb_destroy(ctx); +} + +#undef STREAM_RATE +#undef STREAM_LATENCY +#undef STREAM_CHANNELS +#undef STREAM_LAYOUT +#undef STREAM_FORMAT diff --git a/media/libcubeb/test/test_tone.cpp b/media/libcubeb/test/test_tone.cpp new file mode 100644 index 0000000000..56ff8767ba --- /dev/null +++ b/media/libcubeb/test/test_tone.cpp @@ -0,0 +1,130 @@ +/* + * Copyright © 2011 Mozilla Foundation + * + * This program is made available under an ISC-style license. See the + * accompanying file LICENSE for details. + */ + +/* libcubeb api/function test. Plays a simple tone. */ +#include "gtest/gtest.h" +#if !defined(_XOPEN_SOURCE) +#define _XOPEN_SOURCE 600 +#endif +#include "cubeb/cubeb.h" +#include <atomic> +#include <limits.h> +#include <math.h> +#include <memory> +#include <stdio.h> +#include <stdlib.h> + +// #define ENABLE_NORMAL_LOG +// #define ENABLE_VERBOSE_LOG +#include "common.h" + +#define SAMPLE_FREQUENCY 48000 +#define STREAM_FORMAT CUBEB_SAMPLE_S16LE + +/* store the phase of the generated waveform */ +struct cb_user_data { + std::atomic<long> position; +}; + +long +data_cb_tone(cubeb_stream * stream, void * user, const void * /*inputbuffer*/, + void * outputbuffer, long nframes) +{ + struct cb_user_data * u = (struct cb_user_data *)user; + short * b = (short *)outputbuffer; + float t1, t2; + int i; + + if (stream == NULL || u == NULL) + return CUBEB_ERROR; + + /* generate our test tone on the fly */ + for (i = 0; i < nframes; i++) { + /* North American dial tone */ + t1 = sin(2 * M_PI * (i + u->position) * 350 / SAMPLE_FREQUENCY); + t2 = sin(2 * M_PI * (i + u->position) * 440 / SAMPLE_FREQUENCY); + b[i] = (SHRT_MAX / 2) * t1; + b[i] += (SHRT_MAX / 2) * t2; + /* European dial tone */ + /* + t1 = sin(2*M_PI*(i + u->position)*425/SAMPLE_FREQUENCY); + b[i] = SHRT_MAX * t1; + */ + } + /* remember our phase to avoid clicking on buffer transitions */ + /* we'll still click if position overflows */ + u->position += nframes; + + return nframes; +} + +void +state_cb_tone(cubeb_stream * stream, void * user, cubeb_state state) +{ + struct cb_user_data * u = (struct cb_user_data *)user; + + if (stream == NULL || u == NULL) + return; + + switch (state) { + case CUBEB_STATE_STARTED: + fprintf(stderr, "stream started\n"); + break; + case CUBEB_STATE_STOPPED: + fprintf(stderr, "stream stopped\n"); + break; + case CUBEB_STATE_DRAINED: + fprintf(stderr, "stream drained\n"); + break; + default: + fprintf(stderr, "unknown stream state %d\n", state); + } + + return; +} + +TEST(cubeb, tone) +{ + cubeb * ctx; + cubeb_stream * stream; + cubeb_stream_params params; + int r; + + r = common_init(&ctx, "Cubeb tone example"); + ASSERT_EQ(r, CUBEB_OK) << "Error initializing cubeb library"; + + std::unique_ptr<cubeb, decltype(&cubeb_destroy)> cleanup_cubeb_at_exit( + ctx, cubeb_destroy); + + params.format = STREAM_FORMAT; + params.rate = SAMPLE_FREQUENCY; + params.channels = 1; + params.layout = CUBEB_LAYOUT_MONO; + params.prefs = CUBEB_STREAM_PREF_NONE; + + std::unique_ptr<cb_user_data> user_data(new cb_user_data()); + ASSERT_TRUE(!!user_data) << "Error allocating user data"; + + user_data->position = 0; + + r = cubeb_stream_init(ctx, &stream, "Cubeb tone (mono)", NULL, NULL, NULL, + ¶ms, 4096, data_cb_tone, state_cb_tone, + user_data.get()); + ASSERT_EQ(r, CUBEB_OK) << "Error initializing cubeb stream"; + + std::unique_ptr<cubeb_stream, decltype(&cubeb_stream_destroy)> + cleanup_stream_at_exit(stream, cubeb_stream_destroy); + + cubeb_stream_start(stream); + delay(5000); + cubeb_stream_stop(stream); + + ASSERT_TRUE(user_data->position.load()); +} + +#undef SAMPLE_FREQUENCY +#undef STREAM_FORMAT diff --git a/media/libcubeb/test/test_triple_buffer.cpp b/media/libcubeb/test/test_triple_buffer.cpp new file mode 100644 index 0000000000..a6e0049b79 --- /dev/null +++ b/media/libcubeb/test/test_triple_buffer.cpp @@ -0,0 +1,67 @@ +/* + * Copyright © 2022 Mozilla Foundation + * + * This program is made available under an ISC-style license. See the + * accompanying file LICENSE for details. + */ + +/* cubeb_triple_buffer test */ +#include "gtest/gtest.h" +#if !defined(_XOPEN_SOURCE) +#define _XOPEN_SOURCE 600 +#endif +#include "cubeb/cubeb.h" +#include "cubeb_triple_buffer.h" +#include <atomic> +#include <math.h> +#include <memory> +#include <stdio.h> +#include <stdlib.h> +#include <thread> + +#include "common.h" + +TEST(cubeb, triple_buffer) +{ + struct AB { + uint64_t a; + uint64_t b; + }; + triple_buffer<AB> buffer; + + std::atomic<bool> finished = {false}; + + ASSERT_TRUE(!buffer.updated()); + + auto t = std::thread([&finished, &buffer] { + AB ab; + ab.a = 0; + ab.b = UINT64_MAX; + uint64_t counter = 0; + do { + buffer.write(ab); + ab.a++; + ab.b--; + } while (counter++ < 1e6 && ab.a <= UINT64_MAX && ab.b != 0); + finished.store(true); + }); + + AB ab; + AB old_ab; + old_ab.a = 0; + old_ab.b = UINT64_MAX; + + // Wait to have at least one value produced. + while (!buffer.updated()) { + } + + // Check that the values are increasing (resp. descreasing) monotonically. + while (!finished) { + ab = buffer.read(); + ASSERT_GE(ab.a, old_ab.a); + ASSERT_LE(ab.b, old_ab.b); + old_ab = ab; + } + + t.join(); +} diff --git a/media/libcubeb/test/test_utils.cpp b/media/libcubeb/test/test_utils.cpp new file mode 100644 index 0000000000..a6227d4d7a --- /dev/null +++ b/media/libcubeb/test/test_utils.cpp @@ -0,0 +1,70 @@ +#include "cubeb_utils.h" +#include "gtest/gtest.h" + +TEST(cubeb, auto_array) +{ + auto_array<uint32_t> array; + auto_array<uint32_t> array2(10); + uint32_t a[10]; + + ASSERT_EQ(array2.length(), 0u); + ASSERT_EQ(array2.capacity(), 10u); + + for (uint32_t i = 0; i < 10; i++) { + a[i] = i; + } + + ASSERT_EQ(array.capacity(), 0u); + ASSERT_EQ(array.length(), 0u); + + array.push(a, 10); + + ASSERT_TRUE(!array.reserve(9)); + + for (uint32_t i = 0; i < 10; i++) { + ASSERT_EQ(array.data()[i], i); + } + + ASSERT_EQ(array.capacity(), 10u); + ASSERT_EQ(array.length(), 10u); + + uint32_t b[10]; + + array.pop(b, 5); + + ASSERT_EQ(array.capacity(), 10u); + ASSERT_EQ(array.length(), 5u); + for (uint32_t i = 0; i < 5; i++) { + ASSERT_EQ(b[i], i); + ASSERT_EQ(array.data()[i], 5 + i); + } + uint32_t * bb = b + 5; + array.pop(bb, 5); + + ASSERT_EQ(array.capacity(), 10u); + ASSERT_EQ(array.length(), 0u); + for (uint32_t i = 0; i < 5; i++) { + ASSERT_EQ(bb[i], 5 + i); + } + + ASSERT_TRUE(!array.pop(nullptr, 1)); + + array.push(a, 10); + array.push(a, 10); + + for (uint32_t j = 0; j < 2; j++) { + for (uint32_t i = 0; i < 10; i++) { + ASSERT_EQ(array.data()[10 * j + i], i); + } + } + ASSERT_EQ(array.length(), 20u); + ASSERT_EQ(array.capacity(), 20u); + array.pop(nullptr, 5); + + for (uint32_t i = 0; i < 5; i++) { + ASSERT_EQ(array.data()[i], 5 + i); + } + + ASSERT_EQ(array.length(), 15u); + ASSERT_EQ(array.capacity(), 20u); +} |