summaryrefslogtreecommitdiffstats
path: root/media/libcubeb/src/cubeb_aaudio.cpp
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 19:33:14 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 19:33:14 +0000
commit36d22d82aa202bb199967e9512281e9a53db42c9 (patch)
tree105e8c98ddea1c1e4784a60a5a6410fa416be2de /media/libcubeb/src/cubeb_aaudio.cpp
parentInitial commit. (diff)
downloadfirefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.tar.xz
firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.zip
Adding upstream version 115.7.0esr.upstream/115.7.0esr
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'media/libcubeb/src/cubeb_aaudio.cpp')
-rw-r--r--media/libcubeb/src/cubeb_aaudio.cpp1747
1 files changed, 1747 insertions, 0 deletions
diff --git a/media/libcubeb/src/cubeb_aaudio.cpp b/media/libcubeb/src/cubeb_aaudio.cpp
new file mode 100644
index 0000000000..d5fa09d013
--- /dev/null
+++ b/media/libcubeb/src/cubeb_aaudio.cpp
@@ -0,0 +1,1747 @@
+/* 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 <time.h>
+
+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)
+
+// not needed or added later on
+// X(AAudioStreamBuilder_setFramesPerDataCallback) \
+ // 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);
+
+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::unique_ptr<char[]> 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.");
+ 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);
+ ALOGV("aaudio duplex data cb on stream %p: state %ld (in: %d, out: %d), "
+ "num_frames: %ld",
+ (void *)stm, state, istate, 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;
+ }
+
+ // 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.get(), num_frames, 0);
+ if (in_num_frames < 0) { // error
+ stm->state.store(stream_state::ERROR);
+ LOG("AAudioStream_read: %s",
+ WRAP(AAudio_convertResultToText)(in_num_frames));
+ return AAUDIO_CALLBACK_RESULT_STOP;
+ }
+
+ 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;
+ char * buf = stm->in_buf.get() + 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.get(), &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);
+
+ 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);
+ int res_err = realize_stream(sb, stm->output_stream_params.get(),
+ &stm->ostream, &frame_size);
+ if (res_err) {
+ return res_err;
+ }
+
+ 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);
+ int res_err = realize_stream(sb, stm->input_stream_params.get(),
+ &stm->istream, &frame_size);
+ if (res_err) {
+ return res_err;
+ }
+
+ 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.reset(new char[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,
+ /*.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_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;
+}