diff --git a/dep/cubeb/CMakeLists.txt b/dep/cubeb/CMakeLists.txt index 5e48b7f89..08e65e9e4 100644 --- a/dep/cubeb/CMakeLists.txt +++ b/dep/cubeb/CMakeLists.txt @@ -22,7 +22,10 @@ endif() set(CMAKE_CXX_WARNING_LEVEL 4) if(NOT MSVC) set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -Wextra -Wno-unused-parameter") - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -Wno-unused-parameter") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -Wno-unused-parameter -fno-exceptions -fno-rtti") +else() + string(REPLACE "/GR" "" CMAKE_CXX_FLAGS ${CMAKE_CXX_FLAGS}) # Disable RTTI + string(REPLACE "/EHsc" "" CMAKE_CXX_FLAGS ${CMAKE_CXX_FLAGS}) # Disable Exceptions endif() add_library(cubeb @@ -122,6 +125,26 @@ if(HAVE_SYS_SOUNDCARD_H) endif() endif() +check_include_files(aaudio/AAudio.h USE_AAUDIO) +if(USE_AAUDIO) + target_sources(cubeb PRIVATE + src/cubeb_aaudio.cpp) + target_compile_definitions(cubeb PRIVATE USE_AAUDIO) + + # set this definition to enable low latency mode. Possibly bad for battery + target_compile_definitions(cubeb PRIVATE CUBEB_AAUDIO_LOW_LATENCY) + + # set this definition to enable power saving mode. Possibly resulting + # in high latency + # target_compile_definitions(cubeb PRIVATE CUBEB_AAUDIO_LOW_POWER_SAVING) + + # set this mode to make the backend use an exclusive stream. + # will decrease latency. + # target_compile_definitions(cubeb PRIVATE CUBEB_AAUDIO_EXCLUSIVE_STREAM) + + target_link_libraries(cubeb PRIVATE ${CMAKE_DL_LIBS}) +endif() + check_include_files(android/log.h USE_AUDIOTRACK) if(USE_AUDIOTRACK) target_sources(cubeb PRIVATE diff --git a/dep/cubeb/include/cubeb/cubeb.h b/dep/cubeb/include/cubeb/cubeb.h index e88536f0a..afe7c8eeb 100644 --- a/dep/cubeb/include/cubeb/cubeb.h +++ b/dep/cubeb/include/cubeb/cubeb.h @@ -509,7 +509,9 @@ CUBEB_EXPORT void cubeb_destroy(cubeb * context); 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. + 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 diff --git a/dep/cubeb/src/cubeb.c b/dep/cubeb/src/cubeb.c index 74c17dbe6..87574407f 100644 --- a/dep/cubeb/src/cubeb.c +++ b/dep/cubeb/src/cubeb.c @@ -63,6 +63,9 @@ int opensl_init(cubeb ** context, char const * context_name); #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 @@ -172,6 +175,10 @@ cubeb_init(cubeb ** context, char const * context_name, char const * backend_nam } 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) @@ -227,6 +234,11 @@ cubeb_init(cubeb ** context, char const * context_name, char const * backend_nam #endif #if defined(USE_OPENSL) opensl_init, +#endif + // TODO: should probably be preferred over OpenSLES when available. + // Initialization will fail on old android devices. +#if defined(USE_AAUDIO) + aaudio_init, #endif #if defined(USE_AUDIOTRACK) audiotrack_init, diff --git a/dep/cubeb/src/cubeb_aaudio.cpp b/dep/cubeb/src/cubeb_aaudio.cpp new file mode 100644 index 000000000..998f895e4 --- /dev/null +++ b/dep/cubeb/src/cubeb_aaudio.cpp @@ -0,0 +1,1488 @@ +/* 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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) \ + +#define MAKE_TYPEDEF(x) static decltype(x) * cubeb_##x; +LIBAAUDIO_API_VISIT(MAKE_TYPEDEF) +#undef MAKE_TYPEDEF +#endif + +const uint8_t MAX_STREAMS = 16; + +using unique_lock = std::unique_lock; +using lock_guard = std::lock_guard; + +enum class stream_state { + INIT = 0, + STOPPED, + STOPPING, + STARTED, + STARTING, + DRAINING, + ERROR, + SHUTDOWN, +}; + +struct cubeb_stream { + /* Note: Must match cubeb_stream layout in cubeb.c. */ + cubeb * context{}; + void * user_ptr{}; + + std::atomic in_use{false}; + std::atomic state{stream_state::INIT}; + + 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. + std::mutex mutex; + + std::unique_ptr in_buf; + unsigned in_frame_size{}; // size of one input frame + + cubeb_sample_format out_format{}; + std::atomic volume{1.f}; + unsigned out_channels{}; + unsigned out_frame_size{}; + int64_t latest_output_latency = 0; + int64_t latest_input_latency = 0; + bool voice_input; + bool voice_output; +}; + +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 join{false}; + std::atomic waiting{false}; + } state; + + // streams[i].in_use signals whether a stream is used + struct cubeb_stream streams[MAX_STREAMS]; +}; + +// Only allowed from state thread, while mutex on stm is locked +static void +shutdown(cubeb_stream * stm) +{ + if (stm->istream) { + WRAP(AAudioStream_requestStop)(stm->istream); + } + if (stm->ostream) { + WRAP(AAudioStream_requestStop)(stm->ostream); + } + + 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(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) { + const char * name = WRAP(AAudio_convertStreamStateToText)(istate); + LOG("Unexpected android input stream state %s", name); + shutdown(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) { + const char * name = WRAP(AAudio_convertStreamStateToText)(istate); + LOG("Unexpected android output stream state %s", name); + shutdown(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 (unsigned i = 0u; i < MAX_STREAMS; ++i) { + cubeb_stream * stm = &ctx->streams[i]; + 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 (unsigned i = 0u; i < MAX_STREAMS; ++i) { + assert(!ctx->streams[i].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(); + } + + if (ctx->libaaudio) { + dlclose(ctx->libaaudio); + } + 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: + for (uint32_t i = 0u; i < num_frames * stm->out_channels; ++i) { + (static_cast(audio_data))[i] *= volume; + } + break; + case CUBEB_SAMPLE_FLOAT32NE: + for (uint32_t i = 0u; i < num_frames * stm->out_channels; ++i) { + (static_cast(audio_data))[i] *= volume; + } + break; + default: + assert(false && "Unreachable: invalid stream out_format"); + } +} + +// 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; + 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; + } + + // 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; + } else 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(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; + 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", + // (void*) 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; + } + + long done_frames = + cubeb_resampler_fill(stm->resampler, NULL, NULL, 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; + } else 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(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; + 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", + // (void*) 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; + } + + long input_frame_count = num_frames; + long done_frames = cubeb_resampler_fill(stm->resampler, audio_data, + &input_frame_count, NULL, 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; + } else 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 +aaudio_error_cb(AAudioStream * astream, void * user_data, aaudio_result_t error) +{ + cubeb_stream * stm = static_cast(user_data); + assert(stm->ostream == astream || stm->istream == astream); + 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, params->rate); + WRAP(AAudioStreamBuilder_setChannelCount)(sb, 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; + } else 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); + 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 = NULL; + } + + 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 = NULL; + } + + if (stm->resampler) { + cubeb_resampler_destroy(stm->resampler); + stm->resampler = NULL; + } + + stm->in_buf = {}; + stm->in_frame_size = {}; + stm->out_format = {}; + stm->out_channels = {}; + stm->out_frame_size = {}; + + stm->state.store(stream_state::INIT); + stm->in_use.store(false); +} + +static int +aaudio_stream_init_impl(cubeb_stream * stm, cubeb_devid input_device, + cubeb_stream_params * input_stream_params, + cubeb_devid output_device, + cubeb_stream_params * output_stream_params, + unsigned int latency_frames) +{ + assert(stm->state.load() == stream_state::INIT); + stm->in_use.store(true); + + 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 sbPtr(sb); + + WRAP(AAudioStreamBuilder_setErrorCallback)(sb, aaudio_error_cb, stm); + WRAP(AAudioStreamBuilder_setBufferCapacityInFrames)(sb, latency_frames); + + AAudioStream_dataCallback in_data_callback{}; + AAudioStream_dataCallback out_data_callback{}; + if (output_stream_params && input_stream_params) { + out_data_callback = aaudio_duplex_data_cb; + in_data_callback = NULL; + } else if (input_stream_params) { + in_data_callback = aaudio_input_data_cb; + } else if (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 (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 + uint32_t target_sample_rate = 0; + cubeb_stream_params out_params; + if (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, output_stream_params, &stm->ostream, &frame_size); + if (res_err) { + return res_err; + } + + // output debug information + aaudio_sharing_mode_t sm = WRAP(AAudioStream_getSharingMode)(stm->ostream); + aaudio_performance_mode_t pm = + WRAP(AAudioStream_getPerformanceMode)(stm->ostream); + int bcap = WRAP(AAudioStream_getBufferCapacityInFrames)(stm->ostream); + int bsize = WRAP(AAudioStream_getBufferSizeInFrames)(stm->ostream); + int rate = WRAP(AAudioStream_getSampleRate)(stm->ostream); + LOG("AAudio output stream sharing mode: %d", sm); + LOG("AAudio output stream performance mode: %d", pm); + LOG("AAudio output stream buffer capacity: %d", bcap); + LOG("AAudio output stream buffer size: %d", bsize); + LOG("AAudio output stream buffer rate: %d", rate); + + target_sample_rate = output_stream_params->rate; + out_params = *output_stream_params; + out_params.rate = rate; + + stm->out_channels = output_stream_params->channels; + stm->out_format = output_stream_params->format; + stm->out_frame_size = frame_size; + stm->volume.store(1.f); + } + + // input + cubeb_stream_params in_params; + if (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, input_stream_params, &stm->istream, &frame_size); + if (res_err) { + return res_err; + } + + // output debug information + aaudio_sharing_mode_t sm = WRAP(AAudioStream_getSharingMode)(stm->istream); + aaudio_performance_mode_t pm = + WRAP(AAudioStream_getPerformanceMode)(stm->istream); + int bcap = WRAP(AAudioStream_getBufferCapacityInFrames)(stm->istream); + int bsize = WRAP(AAudioStream_getBufferSizeInFrames)(stm->istream); + int rate = WRAP(AAudioStream_getSampleRate)(stm->istream); + LOG("AAudio input stream sharing mode: %d", sm); + LOG("AAudio input stream performance mode: %d", pm); + LOG("AAudio input stream buffer capacity: %d", bcap); + LOG("AAudio input stream buffer size: %d", bsize); + LOG("AAudio input stream buffer rate: %d", rate); + + stm->in_buf.reset(new char[bcap * frame_size]()); + assert(!target_sample_rate || + target_sample_rate == input_stream_params->rate); + + target_sample_rate = input_stream_params->rate; + in_params = *input_stream_params; + in_params.rate = rate; + stm->in_frame_size = frame_size; + } + + // initialize resampler + stm->resampler = cubeb_resampler_create( + stm, input_stream_params ? &in_params : NULL, + output_stream_params ? &out_params : NULL, target_sample_rate, + stm->data_callback, stm->user_ptr, CUBEB_RESAMPLER_QUALITY_DEFAULT); + + 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 = NULL; + unique_lock lock; + for (unsigned i = 0u; i < MAX_STREAMS; ++i) { + // This check is only an optimization, we don't strictly need it + // since we check again after locking the mutex. + if (ctx->streams[i].in_use.load()) { + continue; + } + + // if this fails, another thread initialized this stream + // between our check of in_use and this. + lock = unique_lock(ctx->streams[i].mutex, std::try_to_lock); + if (!lock.owns_lock()) { + continue; + } + + if (ctx->streams[i].in_use.load()) { + lock = {}; + continue; + } + + stm = &ctx->streams[i]; + break; + } + + if (!stm) { + LOG("Error: maximum number of streams reached"); + return CUBEB_ERROR; + } + + 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); + + LOG("cubeb stream prefs: voice_input: %s voice_output: %s", + stm->voice_input ? "true" : "false", + stm->voice_output ? "true" : "false"); + + int err = aaudio_stream_init_impl(stm, input_device, input_stream_params, + output_device, output_stream_params, + latency_frames); + if (err != CUBEB_OK) { + // This is needed since aaudio_stream_destroy will lock the mutex again. + // It's no problem that there is a gap in between as the stream isn't + // actually in u se. + lock.unlock(); + aaudio_stream_destroy(stm); + return err; + } + + *stream = stm; + return CUBEB_OK; +} + +static int +aaudio_stream_start(cubeb_stream * stm) +{ + assert(stm && stm->in_use.load()); + lock_guard lock(stm->mutex); + + 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); + + 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("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); + return CUBEB_OK; + case stream_state::INIT: + assert(false && "Invalid stream"); + return CUBEB_ERROR; + case stream_state::STARTED: + case stream_state::STARTING: + break; + } + + int64_t pos; + int64_t ns; + aaudio_result_t res; + res = WRAP(AAudioStream_getTimestamp)(stream, CLOCK_MONOTONIC, &pos, &ns); + if (res != AAUDIO_OK) { + // when we are in 'STARTING' state we try it and hope that the stream + // has internally started and gives us a valid timestamp. + // If that is not the case (invalid_state is returned) we simply + // fall back to the method we use for non-playing streams. + if (res == AAUDIO_ERROR_INVALID_STATE && state == stream_state::STARTING) { + *position = WRAP(AAudioStream_getFramesRead)(stream); + return CUBEB_OK; + } + + LOG("AAudioStream_getTimestamp: %s", WRAP(AAudio_convertResultToText)(res)); + return CUBEB_ERROR; + } + + *position = pos; + return CUBEB_OK; +} + +static int +aaudio_stream_get_latency(cubeb_stream * stm, uint32_t * latency) +{ + int64_t pos; + int64_t ns; + aaudio_result_t res; + + if (!stm->ostream) { + LOG("error: aaudio_stream_get_latency on input-only stream"); + return CUBEB_ERROR; + } + + res = + WRAP(AAudioStream_getTimestamp)(stm->ostream, CLOCK_MONOTONIC, &pos, &ns); + if (res != AAUDIO_OK) { + LOG("aaudio_stream_get_latency, AAudioStream_getTimestamp: %s, returning " + "memoized value", + WRAP(AAudio_convertResultToText)(res)); + // Expected when the stream is paused. + *latency = stm->latest_output_latency; + return CUBEB_OK; + } + + int64_t read = WRAP(AAudioStream_getFramesRead)(stm->ostream); + + *latency = stm->latest_output_latency = read - pos; + LOG("aaudio_stream_get_latency, %u", *latency); + + return CUBEB_OK; +} + +static int +aaudio_stream_get_input_latency(cubeb_stream * stm, uint32_t * latency) +{ + int64_t pos; + int64_t ns; + aaudio_result_t res; + + if (!stm->istream) { + LOG("error: aaudio_stream_get_input_latency on an ouput-only stream"); + return CUBEB_ERROR; + } + + res = + WRAP(AAudioStream_getTimestamp)(stm->istream, CLOCK_MONOTONIC, &pos, &ns); + if (res != AAUDIO_OK) { + // Expected when the stream is paused. + LOG("aaudio_stream_get_input_latency, AAudioStream_getTimestamp: %s, " + "returning memoized value", + WRAP(AAudio_convertResultToText)(res)); + *latency = stm->latest_input_latency; + return CUBEB_OK; + } + + int64_t written = WRAP(AAudioStream_getFramesWritten)(stm->istream); + + *latency = stm->latest_input_latency = written - pos; + LOG("aaudio_stream_get_input_latency, %u", *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 =*/NULL, + /*.device_collection_destroy =*/NULL, + /*.destroy =*/aaudio_destroy, + /*.stream_init =*/aaudio_stream_init, + /*.stream_destroy =*/aaudio_stream_destroy, + /*.stream_start =*/aaudio_stream_start, + /*.stream_stop =*/aaudio_stream_stop, + /*.stream_reset_default_device =*/NULL, + /*.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 =*/NULL, + /*.stream_get_current_device =*/NULL, + /*.stream_device_destroy =*/NULL, + /*.stream_register_device_changed_callback =*/NULL, + /*.register_device_collection_changed =*/NULL}; + +extern "C" /*static*/ int +aaudio_init(cubeb ** context, char const * /* context_name */) +{ + // load api + void * libaaudio = NULL; +#ifndef DISABLE_LIBAAUDIO_DLOPEN + libaaudio = dlopen("libaaudio.so", RTLD_NOW); + if (!libaaudio) { + return CUBEB_ERROR; + } + +#define LOAD(x) \ + { \ + WRAP(x) = (decltype(WRAP(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/dep/cubeb/src/cubeb_android.h b/dep/cubeb/src/cubeb_android.h new file mode 100644 index 000000000..c21a941ab --- /dev/null +++ b/dep/cubeb/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/dep/cubeb/src/cubeb_log.h b/dep/cubeb/src/cubeb_log.h index 446e29a5c..413919622 100644 --- a/dep/cubeb/src/cubeb_log.h +++ b/dep/cubeb/src/cubeb_log.h @@ -46,6 +46,7 @@ void cubeb_async_log_reset_threads(); } while(0) /* Asynchronous verbose logging, to log in real-time callbacks. */ +/* Should not be used on android due to the use of global/static variables. */ #define ALOGV(fmt, ...) \ do { \ cubeb_async_log(fmt, ##__VA_ARGS__); \ diff --git a/dep/cubeb/src/cubeb_opensl.c b/dep/cubeb/src/cubeb_opensl.c index f34ab7ac5..a6800197f 100644 --- a/dep/cubeb/src/cubeb_opensl.c +++ b/dep/cubeb/src/cubeb_opensl.c @@ -27,6 +27,7 @@ #include "cubeb-sles.h" #include "cubeb_array_queue.h" #include "android/cubeb-output-latency.h" +#include "cubeb_android.h" #if defined(__ANDROID__) #ifdef LOG @@ -65,11 +66,6 @@ #define DEFAULT_SAMPLE_RATE 48000 #define DEFAULT_NUM_OF_FRAMES 480 -// If the latency requested is above this threshold, this stream is considered -// intended for playback (vs. real-time). Tell Android it should favor saving -// power over performance or latency. -// This is around 100ms at 44100 or 48000 -#define POWERSAVE_LATENCY_FRAMES_THRESHOLD 4000 static struct cubeb_ops const opensl_ops; @@ -1702,7 +1698,7 @@ opensl_stream_get_latency(cubeb_stream * stm, uint32_t * latency) assert(latency); uint32_t stream_latency_frames = - stm->user_output_rate * (stm->output_latency_ms / 1000); + stm->user_output_rate * stm->output_latency_ms / 1000; return stream_latency_frames + cubeb_resampler_latency(stm->resampler); } diff --git a/dep/cubeb/src/cubeb_oss.c b/dep/cubeb/src/cubeb_oss.c index 3348cdc3d..32a4bf6de 100644 --- a/dep/cubeb/src/cubeb_oss.c +++ b/dep/cubeb/src/cubeb_oss.c @@ -741,6 +741,46 @@ oss_linear16_set_vol(int16_t * buf, unsigned sample_count, float vol) } } +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; +} + /* 1 - Stopped by cubeb_stream_stop, otherwise 0 */ static int oss_audio_loop(cubeb_stream * s, cubeb_state *new_state) @@ -748,18 +788,10 @@ oss_audio_loop(cubeb_stream * s, cubeb_state *new_state) cubeb_state state = CUBEB_STATE_STOPPED; int trig = 0; int drain = 0; - struct pollfd pfds[2]; - unsigned int ppending, rpending; + const bool play_on = s->play.fd != -1, record_on = s->record.fd != -1; + long nfr = s->bufframes; - pfds[0].fd = s->play.fd; - pfds[0].events = POLLOUT; - pfds[1].fd = s->record.fd; - pfds[1].events = POLLIN; - - ppending = 0; - rpending = s->bufframes; - - if (s->record.fd != -1) { + 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; @@ -771,43 +803,35 @@ oss_audio_loop(cubeb_stream * s, cubeb_state *new_state) state = CUBEB_STATE_ERROR; goto breakdown; } + memset(s->record.buf, 0, s->bufframes * s->record.frame_size); } - while (1) { - long nfr = 0; + 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); - if (s->play.fd == -1 && s->record.fd == -1) { - /* - * Stop here if the stream is not play & record stream, - * play-only stream or record-only stream - */ - goto breakdown; - } - - while ((s->bufframes - ppending) >= s->nfr && rpending >= s->nfr) { - long n = ((s->bufframes - ppending) < rpending) ? s->bufframes - ppending : rpending; - char *rptr = NULL, *pptr = NULL; - if (s->record.fd != -1) - rptr = (char *)s->record.buf; - if (s->play.fd != -1) - pptr = (char *)s->play.buf + ppending * s->play.frame_size; - if (s->record.fd != -1 && s->record.floating) { - oss_linear32_to_float(s->record.buf, s->record.info.channels * n); - } - nfr = s->data_cb(s, s->user_ptr, rptr, pptr, n); - if (nfr == CUBEB_ERROR) { + long got = 0; + if (nfr > 0) { + 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 (pptr) { + if (play_on) { float vol; pthread_mutex_lock(&s->mtx); @@ -815,25 +839,15 @@ oss_audio_loop(cubeb_stream * s, cubeb_state *new_state) pthread_mutex_unlock(&s->mtx); if (s->play.floating) { - oss_float_to_linear32(pptr, s->play.info.channels * nfr, vol); + oss_float_to_linear32(s->play.buf, s->play.info.channels * got, vol); } else { - oss_linear16_set_vol((int16_t *)pptr, s->play.info.channels * nfr, vol); + oss_linear16_set_vol((int16_t *)s->play.buf, + s->play.info.channels * got, vol); } } - if (pptr) { - ppending += nfr; - assert(ppending <= s->bufframes); - } - if (rptr) { - assert(rpending >= nfr); - rpending -= nfr; - memmove(rptr, rptr + nfr * s->record.frame_size, - (s->bufframes - nfr) * s->record.frame_size); - } - if (nfr < n) { + if (got < nfr) { if (s->play.fd != -1) { drain = 1; - break; } else { /* * This is a record-only stream and number of frames @@ -845,74 +859,48 @@ oss_audio_loop(cubeb_stream * s, cubeb_state *new_state) goto breakdown; } } + nfr = 0; } - ssize_t n, frames; - int nfds; - - pfds[0].revents = 0; - pfds[1].revents = 0; - - nfds = poll(pfds, 2, 1000); - if (nfds == -1) { - if (errno == EINTR) - continue; - LOG("Error %d occured when polling playback and record fd", errno); - state = CUBEB_STATE_ERROR; - goto breakdown; - } else if (nfds == 0) - continue; - - if ((pfds[0].revents & (POLLERR | POLLHUP)) || - (pfds[1].revents & (POLLERR | POLLHUP))) { - LOG("Error occured on playback, record fds"); - state = CUBEB_STATE_ERROR; - goto breakdown; - } - - if (pfds[0].revents) { - while (ppending > 0) { - size_t bytes = ppending * s->play.frame_size; - if ((n = write(s->play.fd, (uint8_t *)s->play.buf, bytes)) < 0) { - if (errno == EINTR) - continue; - if (errno == EAGAIN) { - if (drain) - continue; - break; - } + if (got > 0) { + if (play_on && oss_put_play_frames(s, got) < 0) { state = CUBEB_STATE_ERROR; goto breakdown; - } - frames = n / s->play.frame_size; - pthread_mutex_lock(&s->mtx); - s->frames_written += frames; - pthread_mutex_unlock(&s->mtx); - ppending -= frames; - memmove(s->play.buf, (uint8_t *)s->play.buf + n, - (s->bufframes - frames) * s->play.frame_size); - } - } - if (pfds[1].revents) { - while (s->bufframes - rpending > 0) { - size_t bytes = (s->bufframes - rpending) * s->record.frame_size; - size_t read_ofs = rpending * s->record.frame_size; - if ((n = read(s->record.fd, (uint8_t *)s->record.buf + read_ofs, bytes)) < 0) { - if (errno == EINTR) - continue; - if (errno == EAGAIN) - break; - state = CUBEB_STATE_ERROR; - goto breakdown; - } - frames = n / s->record.frame_size; - rpending += frames; } } if (drain) { state = CUBEB_STATE_DRAINED; goto breakdown; } + + audio_buf_info bi; + if (play_on) { + if (ioctl(s->play.fd, SNDCTL_DSP_GETOSPACE, &bi)) { + state = CUBEB_STATE_ERROR; + goto breakdown; + } + /* + * In duplex mode, playback direction drives recording direction to + * prevent building up latencies. + */ + nfr = bi.fragsize * bi.fragments / s->play.frame_size; + if (nfr > s->bufframes) { + nfr = s->bufframes; + } + } + + if (record_on) { + if (nfr == 0) { + nfr = s->nfr; + } + 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); + } + } } return 1; @@ -1035,7 +1023,7 @@ oss_stream_init(cubeb * context, goto error; } if (s->record.fd == -1) { - if ((s->record.fd = open(s->record.name, O_RDONLY | O_NONBLOCK)) == -1) { + 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; @@ -1066,7 +1054,7 @@ oss_stream_init(cubeb * context, goto error; } if (s->play.fd == -1) { - if ((s->play.fd = open(s->play.name, O_WRONLY | O_NONBLOCK)) == -1) { + 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; @@ -1082,22 +1070,40 @@ oss_stream_init(cubeb * context, s->play.frame_size = s->play.info.channels * (s->play.info.precision / 8); playnfr = (1 << oss_calc_frag_shift(s->nfr, s->play.frame_size)) / s->play.frame_size; } - /* Use the largest nframes among playing and recording streams */ + /* + * Use the largest nframes among playing and recording streams to set OSS buffer size. + * After that, use the smallest allocated nframes among both direction to allocate our + * temporary buffers. + */ s->nfr = (playnfr > recnfr) ? playnfr : recnfr; s->nfrags = OSS_NFRAGS; - s->bufframes = s->nfr * s->nfrags; if (s->play.fd != -1) { int frag = oss_get_frag_params(oss_calc_frag_shift(s->nfr, s->play.frame_size)); - if (ioctl(s->record.fd, SNDCTL_DSP_SETFRAGMENT, &frag)) - LOG("Failed to set record fd with SNDCTL_DSP_SETFRAGMENT. frag: 0x%x", + 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 { + if (bi.fragsize / s->play.frame_size < s->nfr) + s->nfr = bi.fragsize / s->play.frame_size; + } } if (s->record.fd != -1) { int frag = oss_get_frag_params(oss_calc_frag_shift(s->nfr, 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 { + if (bi.fragsize / s->record.frame_size < s->nfr) + s->nfr = bi.fragsize / s->record.frame_size; + } } + s->bufframes = s->nfr * s->nfrags; s->context = context; s->volume = 1.0; s->state_cb = state_callback; diff --git a/dep/cubeb/src/cubeb_wasapi.cpp b/dep/cubeb/src/cubeb_wasapi.cpp index 66d9c1d8c..a17ea21a2 100644 --- a/dep/cubeb/src/cubeb_wasapi.cpp +++ b/dep/cubeb/src/cubeb_wasapi.cpp @@ -2030,6 +2030,23 @@ int setup_wasapi_stream_one_side(cubeb_stream * stm, com_heap_ptr 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(mix_format.get()); format_pcm->SubFormat = stm->waveformatextensible_sub_format; @@ -2085,9 +2102,11 @@ int setup_wasapi_stream_one_side(cubeb_stream * stm, cubeb_device_info device_info; int rv = wasapi_create_device(stm->context, device_info, stm->device_enumerator.get(), device.get()); if (rv == CUBEB_OK) { - const char* HANDSFREE_TAG = "BTHHFEENUM"; + const char* HANDSFREE_TAG = "BTHHFENUM"; size_t len = sizeof(HANDSFREE_TAG); - if (direction == eCapture && strncmp(device_info.group_id, HANDSFREE_TAG, len) == 0) { + if (direction == eCapture && + strlen(device_info.group_id) >= len && + strncmp(device_info.group_id, HANDSFREE_TAG, len) == 0) { // Rather high-latency to prevent constant under-runs in this particular // case of an input device using bluetooth handsfree. uint32_t default_period_frames = hns_to_frames(device_info.default_rate, default_period); @@ -2202,7 +2221,7 @@ void wasapi_find_matching_output_device(cubeb_stream * stm) { 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 && input_device && !strcmp(dev.group_id, input_device->group_id) && + 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); stm->output_device_id = utf8_to_wstr(reinterpret_cast(dev.devid));