From 66111e067c3970262f993dbc2fcfbb511399bac5 Mon Sep 17 00:00:00 2001 From: Robert Wu <85952307+robertwu1@users.noreply.github.com> Date: Thu, 12 Oct 2023 10:36:45 -0700 Subject: [PATCH] Add Full Duplex Stream to Oboe library (#1920) --- apps/OboeTester/app/CMakeLists.txt | 2 +- .../app/src/main/cpp/FullDuplexAnalyzer.cpp | 6 +- .../app/src/main/cpp/FullDuplexAnalyzer.h | 8 +- .../app/src/main/cpp/FullDuplexEcho.cpp | 8 +- .../app/src/main/cpp/FullDuplexEcho.h | 8 +- .../app/src/main/cpp/FullDuplexStream.cpp | 152 -------- .../app/src/main/cpp/FullDuplexStream.h | 119 ------- .../cpp/FullDuplexStreamWithConversion.cpp | 60 ++++ .../main/cpp/FullDuplexStreamWithConversion.h | 61 ++++ .../app/src/main/cpp/InterpolatingDelayLine.h | 1 - .../app/src/main/cpp/NativeAudioContext.cpp | 14 +- .../app/src/main/cpp/NativeAudioContext.h | 1 - .../src/main/cpp/OboeStreamCallbackProxy.h | 4 +- .../app/src/main/res/values/strings.xml | 2 + include/oboe/FullDuplexStream.h | 324 ++++++++++++++++++ include/oboe/Oboe.h | 1 + .../LiveEffect/src/main/cpp/CMakeLists.txt | 1 - .../LiveEffect/src/main/cpp/FullDuplexPass.h | 8 +- .../src/main/cpp/FullDuplexStream.cpp | 128 ------- .../src/main/cpp/FullDuplexStream.h | 102 ------ .../src/main/cpp/LiveEffectEngine.cpp | 20 +- tests/CMakeLists.txt | 1 + tests/testFullDuplexStream.cpp | 208 +++++++++++ 23 files changed, 702 insertions(+), 537 deletions(-) delete mode 100644 apps/OboeTester/app/src/main/cpp/FullDuplexStream.cpp delete mode 100644 apps/OboeTester/app/src/main/cpp/FullDuplexStream.h create mode 100644 apps/OboeTester/app/src/main/cpp/FullDuplexStreamWithConversion.cpp create mode 100644 apps/OboeTester/app/src/main/cpp/FullDuplexStreamWithConversion.h create mode 100644 include/oboe/FullDuplexStream.h delete mode 100644 samples/LiveEffect/src/main/cpp/FullDuplexStream.cpp delete mode 100644 samples/LiveEffect/src/main/cpp/FullDuplexStream.h create mode 100644 tests/testFullDuplexStream.cpp diff --git a/apps/OboeTester/app/CMakeLists.txt b/apps/OboeTester/app/CMakeLists.txt index 358a8dc67..8361d8823 100644 --- a/apps/OboeTester/app/CMakeLists.txt +++ b/apps/OboeTester/app/CMakeLists.txt @@ -6,7 +6,7 @@ set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -O3") link_directories(${CMAKE_CURRENT_LIST_DIR}/..) -# Increment this number when adding files to OboeTester => 103 +# Increment this number when adding files to OboeTester => 104 # The change in this file will help Android Studio resync # and generate new build files that reference the new code. file(GLOB_RECURSE app_native_sources src/main/cpp/*) diff --git a/apps/OboeTester/app/src/main/cpp/FullDuplexAnalyzer.cpp b/apps/OboeTester/app/src/main/cpp/FullDuplexAnalyzer.cpp index 58252f5ac..04c7086f9 100644 --- a/apps/OboeTester/app/src/main/cpp/FullDuplexAnalyzer.cpp +++ b/apps/OboeTester/app/src/main/cpp/FullDuplexAnalyzer.cpp @@ -21,10 +21,10 @@ oboe::Result FullDuplexAnalyzer::start() { getLoopbackProcessor()->setSampleRate(getOutputStream()->getSampleRate()); getLoopbackProcessor()->prepareToTest(); mWriteReadDeltaValid = false; - return FullDuplexStream::start(); + return FullDuplexStreamWithConversion::start(); } -oboe::DataCallbackResult FullDuplexAnalyzer::onBothStreamsReady( +oboe::DataCallbackResult FullDuplexAnalyzer::onBothStreamsReadyFloat( const float *inputData, int numInputFrames, float *outputData, @@ -32,7 +32,7 @@ oboe::DataCallbackResult FullDuplexAnalyzer::onBothStreamsReady( int32_t inputStride = getInputStream()->getChannelCount(); int32_t outputStride = getOutputStream()->getChannelCount(); - const float *inputFloat = inputData; + auto *inputFloat = static_cast(inputData); float *outputFloat = outputData; // Get atomic snapshot of the relative frame positions so they diff --git a/apps/OboeTester/app/src/main/cpp/FullDuplexAnalyzer.h b/apps/OboeTester/app/src/main/cpp/FullDuplexAnalyzer.h index 0473d3d4d..4163aa82e 100644 --- a/apps/OboeTester/app/src/main/cpp/FullDuplexAnalyzer.h +++ b/apps/OboeTester/app/src/main/cpp/FullDuplexAnalyzer.h @@ -21,22 +21,22 @@ #include #include "oboe/Oboe.h" -#include "FullDuplexStream.h" #include "analyzer/LatencyAnalyzer.h" +#include "FullDuplexStreamWithConversion.h" #include "MultiChannelRecording.h" -class FullDuplexAnalyzer : public FullDuplexStream { +class FullDuplexAnalyzer : public FullDuplexStreamWithConversion { public: FullDuplexAnalyzer(LoopbackProcessor *processor) : mLoopbackProcessor(processor) { - setMNumInputBurstsCushion(1); + setNumInputBurstsCushion(1); } /** * Called when data is available on both streams. * Caller should override this method. */ - oboe::DataCallbackResult onBothStreamsReady( + oboe::DataCallbackResult onBothStreamsReadyFloat( const float *inputData, int numInputFrames, float *outputData, diff --git a/apps/OboeTester/app/src/main/cpp/FullDuplexEcho.cpp b/apps/OboeTester/app/src/main/cpp/FullDuplexEcho.cpp index 6a8389e58..d54f669d2 100644 --- a/apps/OboeTester/app/src/main/cpp/FullDuplexEcho.cpp +++ b/apps/OboeTester/app/src/main/cpp/FullDuplexEcho.cpp @@ -23,7 +23,7 @@ oboe::Result FullDuplexEcho::start() { // Use peak detector for input streams mNumChannels = getInputStream()->getChannelCount(); mPeakDetectors = std::make_unique(mNumChannels); - return FullDuplexStream::start(); + return FullDuplexStreamWithConversion::start(); } double FullDuplexEcho::getPeakLevel(int index) { @@ -37,14 +37,14 @@ double FullDuplexEcho::getPeakLevel(int index) { return mPeakDetectors[index].getLevel(); } -oboe::DataCallbackResult FullDuplexEcho::onBothStreamsReady( +oboe::DataCallbackResult FullDuplexEcho::onBothStreamsReadyFloat( const float *inputData, int numInputFrames, float *outputData, int numOutputFrames) { int32_t framesToEcho = std::min(numInputFrames, numOutputFrames); - float *inputFloat = (float *)inputData; - float *outputFloat = (float *)outputData; + auto *inputFloat = const_cast(inputData); + float *outputFloat = outputData; // zero out entire output array memset(outputFloat, 0, static_cast(numOutputFrames) * static_cast(getOutputStream()->getBytesPerFrame())); diff --git a/apps/OboeTester/app/src/main/cpp/FullDuplexEcho.h b/apps/OboeTester/app/src/main/cpp/FullDuplexEcho.h index 65e1ca2e4..a88e51ef8 100644 --- a/apps/OboeTester/app/src/main/cpp/FullDuplexEcho.h +++ b/apps/OboeTester/app/src/main/cpp/FullDuplexEcho.h @@ -21,21 +21,21 @@ #include #include "oboe/Oboe.h" -#include "FullDuplexStream.h" #include "analyzer/LatencyAnalyzer.h" +#include "FullDuplexStreamWithConversion.h" #include "InterpolatingDelayLine.h" -class FullDuplexEcho : public FullDuplexStream { +class FullDuplexEcho : public FullDuplexStreamWithConversion { public: FullDuplexEcho() { - setMNumInputBurstsCushion(0); + setNumInputBurstsCushion(0); } /** * Called when data is available on both streams. * Caller should override this method. */ - oboe::DataCallbackResult onBothStreamsReady( + oboe::DataCallbackResult onBothStreamsReadyFloat( const float *inputData, int numInputFrames, float *outputData, diff --git a/apps/OboeTester/app/src/main/cpp/FullDuplexStream.cpp b/apps/OboeTester/app/src/main/cpp/FullDuplexStream.cpp deleted file mode 100644 index 52e762e60..000000000 --- a/apps/OboeTester/app/src/main/cpp/FullDuplexStream.cpp +++ /dev/null @@ -1,152 +0,0 @@ -/* - * Copyright 2019 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "common/OboeDebug.h" -#include "FullDuplexStream.h" - -oboe::ResultWithValue FullDuplexStream::readInput(int32_t numFrames) { - oboe::ResultWithValue result = getInputStream()->read( - mInputConverter->getInputBuffer(), - numFrames, - 0 /* timeout */); - if (result == oboe::Result::OK) { - int32_t numSamples = result.value() * getInputStream()->getChannelCount(); - mInputConverter->convertInternalBuffers(numSamples); - } - return result; -} - -oboe::DataCallbackResult FullDuplexStream::onAudioReady( - oboe::AudioStream *outputStream, - void *audioData, - int numFrames) { - oboe::DataCallbackResult callbackResult = oboe::DataCallbackResult::Continue; - int32_t actualFramesRead = 0; - - // Silence the output. - int32_t numBytes = numFrames * outputStream->getBytesPerFrame(); - memset(audioData, 0 /* value */, numBytes); - - if (mCountCallbacksToDrain > 0) { - // Drain the input. - int32_t totalFramesRead = 0; - do { - oboe::ResultWithValue result = readInput(numFrames); - if (!result) { - // Ignore errors because input stream may not be started yet. - break; - } - actualFramesRead = result.value(); - totalFramesRead += actualFramesRead; - } while (actualFramesRead > 0); - // Only counts if we actually got some data. - if (totalFramesRead > 0) { - mCountCallbacksToDrain--; - } - - } else if (mCountInputBurstsCushion > 0) { - // Let the input fill up a bit so we are not so close to the write pointer. - mCountInputBurstsCushion--; - - } else if (mCountCallbacksToDiscard > 0) { - mCountCallbacksToDiscard--; - // Ignore. Allow the input to reach to equilibrium with the output. - oboe::ResultWithValue resultAvailable = getInputStream()->getAvailableFrames(); - if (!resultAvailable) { - LOGE("%s() getAvailableFrames() returned %s\n", - __func__, convertToText(resultAvailable.error())); - callbackResult = oboe::DataCallbackResult::Stop; - } else { - int32_t framesAvailable = resultAvailable.value(); - if (framesAvailable >= mMinimumFramesBeforeRead) { - oboe::ResultWithValue resultRead = readInput(numFrames); - if (!resultRead) { - LOGE("%s() read() returned %s\n", __func__, convertToText(resultRead.error())); - callbackResult = oboe::DataCallbackResult::Stop; - } - } - } - } else { - int32_t framesRead = 0; - oboe::ResultWithValue resultAvailable = getInputStream()->getAvailableFrames(); - if (!resultAvailable) { - LOGE("%s() getAvailableFrames() returned %s\n", __func__, convertToText(resultAvailable.error())); - callbackResult = oboe::DataCallbackResult::Stop; - } else { - int32_t framesAvailable = resultAvailable.value(); - if (framesAvailable >= mMinimumFramesBeforeRead) { - // Read data into input buffer. - oboe::ResultWithValue resultRead = readInput(numFrames); - if (!resultRead) { - LOGE("%s() read() returned %s\n", __func__, convertToText(resultRead.error())); - callbackResult = oboe::DataCallbackResult::Stop; - } else { - framesRead = resultRead.value(); - } - } - } - - if (callbackResult == oboe::DataCallbackResult::Continue) { - callbackResult = onBothStreamsReady( - (const float *) mInputConverter->getOutputBuffer(), - framesRead, - (float *) mOutputConverter->getInputBuffer(), numFrames); - mOutputConverter->convertFromInternalInput( audioData, - numFrames * getOutputStream()->getChannelCount()); - } - } - - if (callbackResult == oboe::DataCallbackResult::Stop) { - getInputStream()->requestStop(); - } - - return callbackResult; -} - -oboe::Result FullDuplexStream::start() { - mCountCallbacksToDrain = kNumCallbacksToDrain; - mCountInputBurstsCushion = mNumInputBurstsCushion; - mCountCallbacksToDiscard = kNumCallbacksToDiscard; - - // Determine maximum size that could possibly be called. - int32_t bufferSize = getOutputStream()->getBufferCapacityInFrames() - * getOutputStream()->getChannelCount(); - mInputConverter = std::make_unique(bufferSize, - getInputStream()->getFormat(), - oboe::AudioFormat::Float); - mOutputConverter = std::make_unique(bufferSize, - oboe::AudioFormat::Float, - getOutputStream()->getFormat()); - - oboe::Result result = getInputStream()->requestStart(); - if (result != oboe::Result::OK) { - return result; - } - return getOutputStream()->requestStart(); -} - -oboe::Result FullDuplexStream::stop() { - getOutputStream()->requestStop(); // TODO result? - return getInputStream()->requestStop(); -} - -int32_t FullDuplexStream::getMNumInputBurstsCushion() const { - return mNumInputBurstsCushion; -} - -void FullDuplexStream::setMNumInputBurstsCushion(int32_t numBursts) { - FullDuplexStream::mNumInputBurstsCushion = numBursts; -} diff --git a/apps/OboeTester/app/src/main/cpp/FullDuplexStream.h b/apps/OboeTester/app/src/main/cpp/FullDuplexStream.h deleted file mode 100644 index dcfb6ea44..000000000 --- a/apps/OboeTester/app/src/main/cpp/FullDuplexStream.h +++ /dev/null @@ -1,119 +0,0 @@ -/* - * Copyright 2019 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#ifndef OBOETESTER_FULL_DUPLEX_STREAM_H -#define OBOETESTER_FULL_DUPLEX_STREAM_H - -#include -#include - -#include "oboe/Oboe.h" - -#include "FormatConverterBox.h" - -class FullDuplexStream : public oboe::AudioStreamCallback { -public: - FullDuplexStream() {} - virtual ~FullDuplexStream() = default; - - void setInputStream(oboe::AudioStream *stream) { - mInputStream = stream; - } - - oboe::AudioStream *getInputStream() { - return mInputStream; - } - - void setOutputStream(oboe::AudioStream *stream) { - mOutputStream = stream; - } - oboe::AudioStream *getOutputStream() { - return mOutputStream; - } - - virtual oboe::Result start(); - - virtual oboe::Result stop(); - - oboe::ResultWithValue readInput(int32_t numFrames); - - /** - * Called when data is available on both streams. - * Caller should override this method. - */ - virtual oboe::DataCallbackResult onBothStreamsReady( - const float *inputData, - int numInputFrames, - float *outputData, - int numOutputFrames - ) = 0; - - /** - * Called by Oboe when the stream is ready to process audio. - */ - oboe::DataCallbackResult onAudioReady( - oboe::AudioStream *audioStream, - void *audioData, - int numFrames) override; - - int32_t getMNumInputBurstsCushion() const; - - /** - * Number of bursts to leave in the input buffer as a cushion. - * Typically 0 for latency measurements - * or 1 for glitch tests. - * - * @param mNumInputBurstsCushion - */ - void setMNumInputBurstsCushion(int32_t mNumInputBurstsCushion); - - void setMinimumFramesBeforeRead(int32_t numFrames) { - mMinimumFramesBeforeRead = numFrames; - } - - int32_t getMinimumFramesBeforeRead() const { - return mMinimumFramesBeforeRead; - } - -private: - - // TODO add getters and setters - static constexpr int32_t kNumCallbacksToDrain = 20; - static constexpr int32_t kNumCallbacksToDiscard = 30; - - // let input fill back up, usually 0 or 1 - int32_t mNumInputBurstsCushion = 0; - int32_t mMinimumFramesBeforeRead = 0; - - // We want to reach a state where the input buffer is empty and - // the output buffer is full. - // These are used in order. - // Drain several callback so that input is empty. - int32_t mCountCallbacksToDrain = kNumCallbacksToDrain; - // Let the input fill back up slightly so we don't run dry. - int32_t mCountInputBurstsCushion = mNumInputBurstsCushion; - // Discard some callbacks so the input and output reach equilibrium. - int32_t mCountCallbacksToDiscard = kNumCallbacksToDiscard; - - oboe::AudioStream *mInputStream = nullptr; - oboe::AudioStream *mOutputStream = nullptr; - - std::unique_ptr mInputConverter; - std::unique_ptr mOutputConverter; -}; - - -#endif //OBOETESTER_FULL_DUPLEX_STREAM_H diff --git a/apps/OboeTester/app/src/main/cpp/FullDuplexStreamWithConversion.cpp b/apps/OboeTester/app/src/main/cpp/FullDuplexStreamWithConversion.cpp new file mode 100644 index 000000000..a64b8d962 --- /dev/null +++ b/apps/OboeTester/app/src/main/cpp/FullDuplexStreamWithConversion.cpp @@ -0,0 +1,60 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "common/OboeDebug.h" +#include "FullDuplexStreamWithConversion.h" + +oboe::Result FullDuplexStreamWithConversion::start() { + // Determine maximum size that could possibly be called. + int32_t bufferSize = getOutputStream()->getBufferCapacityInFrames() + * getOutputStream()->getChannelCount(); + mInputConverter = std::make_unique(bufferSize, + getInputStream()->getFormat(), + oboe::AudioFormat::Float); + mOutputConverter = std::make_unique(bufferSize, + oboe::AudioFormat::Float, + getOutputStream()->getFormat()); + return FullDuplexStream::start(); +} + +oboe::ResultWithValue FullDuplexStreamWithConversion::readInput(int32_t numFrames) { + oboe::ResultWithValue result = getInputStream()->read( + mInputConverter->getInputBuffer(), + numFrames, + 0 /* timeout */); + if (result == oboe::Result::OK) { + int32_t numSamples = result.value() * getInputStream()->getChannelCount(); + mInputConverter->convertInternalBuffers(numSamples); + } + return result; +} + +oboe::DataCallbackResult FullDuplexStreamWithConversion::onBothStreamsReady( + const void *inputData, + int numInputFrames, + void *outputData, + int numOutputFrames +) { + oboe::DataCallbackResult callbackResult = oboe::DataCallbackResult::Continue; + callbackResult = onBothStreamsReadyFloat( + static_cast(mInputConverter->getOutputBuffer()), + numInputFrames, + static_cast(mOutputConverter->getInputBuffer()), + numOutputFrames); + mOutputConverter->convertFromInternalInput( outputData, + numOutputFrames * getOutputStream()->getChannelCount()); + return callbackResult; +} diff --git a/apps/OboeTester/app/src/main/cpp/FullDuplexStreamWithConversion.h b/apps/OboeTester/app/src/main/cpp/FullDuplexStreamWithConversion.h new file mode 100644 index 000000000..63c2242e5 --- /dev/null +++ b/apps/OboeTester/app/src/main/cpp/FullDuplexStreamWithConversion.h @@ -0,0 +1,61 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef OBOETESTER_FULL_DUPLEX_STREAM_WITH_CONVERSION_H +#define OBOETESTER_FULL_DUPLEX_STREAM_WITH_CONVERSION_H + +#include +#include + +#include "oboe/Oboe.h" +#include "FormatConverterBox.h" + +class FullDuplexStreamWithConversion : public oboe::FullDuplexStream { +public: + /** + * Called when data is available on both streams. + * Caller must override this method. + */ + virtual oboe::DataCallbackResult onBothStreamsReadyFloat( + const float *inputData, + int numInputFrames, + float *outputData, + int numOutputFrames + ) = 0; + + /** + * Overrides the default onBothStreamsReady by converting to floats and then calling + * onBothStreamsReadyFloat(). + */ + oboe::DataCallbackResult onBothStreamsReady( + const void *inputData, + int numInputFrames, + void *outputData, + int numOutputFrames + ) override; + + oboe::ResultWithValue readInput(int32_t numFrames) override; + + virtual oboe::Result start() override; + +private: + std::unique_ptr mInputConverter; + std::unique_ptr mOutputConverter; + +}; + + +#endif //OBOETESTER_FULL_DUPLEX_STREAM_WITH_CONVERSION_H diff --git a/apps/OboeTester/app/src/main/cpp/InterpolatingDelayLine.h b/apps/OboeTester/app/src/main/cpp/InterpolatingDelayLine.h index b3a510da9..bb90f2407 100644 --- a/apps/OboeTester/app/src/main/cpp/InterpolatingDelayLine.h +++ b/apps/OboeTester/app/src/main/cpp/InterpolatingDelayLine.h @@ -22,7 +22,6 @@ #include #include "oboe/Oboe.h" -#include "FullDuplexStream.h" /** * Monophonic delay line. diff --git a/apps/OboeTester/app/src/main/cpp/NativeAudioContext.cpp b/apps/OboeTester/app/src/main/cpp/NativeAudioContext.cpp index 334f7119a..69c348f9b 100644 --- a/apps/OboeTester/app/src/main/cpp/NativeAudioContext.cpp +++ b/apps/OboeTester/app/src/main/cpp/NativeAudioContext.cpp @@ -469,7 +469,7 @@ void ActivityTestOutput::configureStreamGateway() { } if (mUseCallback) { - oboeCallbackProxy.setCallback(&audioStreamGateway); + oboeCallbackProxy.setDataCallback(&audioStreamGateway); } } @@ -521,7 +521,7 @@ oboe::Result ActivityTestOutput::startStreams() { void ActivityTestInput::configureAfterOpen() { mInputAnalyzer.reset(); if (mUseCallback) { - oboeCallbackProxy.setCallback(&mInputAnalyzer); + oboeCallbackProxy.setDataCallback(&mInputAnalyzer); } mInputAnalyzer.setRecording(mRecording.get()); } @@ -648,7 +648,7 @@ void ActivityEcho::configureBuilder(bool isInput, oboe::AudioStreamBuilder &buil // only output uses a callback, input is polled if (!isInput) { builder.setCallback((oboe::AudioStreamCallback *) &oboeCallbackProxy); - oboeCallbackProxy.setCallback(mFullDuplexEcho.get()); + oboeCallbackProxy.setDataCallback(mFullDuplexEcho.get()); } } @@ -670,7 +670,7 @@ void ActivityRoundTripLatency::configureBuilder(bool isInput, oboe::AudioStreamB if (!isInput) { // only output uses a callback, input is polled builder.setCallback((oboe::AudioStreamCallback *) &oboeCallbackProxy); - oboeCallbackProxy.setCallback(mFullDuplexLatency.get()); + oboeCallbackProxy.setDataCallback(mFullDuplexLatency.get()); } } @@ -725,7 +725,7 @@ void ActivityGlitches::configureBuilder(bool isInput, oboe::AudioStreamBuilder & if (!isInput) { // only output uses a callback, input is polled builder.setCallback((oboe::AudioStreamCallback *) &oboeCallbackProxy); - oboeCallbackProxy.setCallback(mFullDuplexGlitches.get()); + oboeCallbackProxy.setDataCallback(mFullDuplexGlitches.get()); } } @@ -748,7 +748,7 @@ void ActivityDataPath::configureBuilder(bool isInput, oboe::AudioStreamBuilder & if (!isInput) { // only output uses a callback, input is polled builder.setCallback((oboe::AudioStreamCallback *) &oboeCallbackProxy); - oboeCallbackProxy.setCallback(mFullDuplexDataPath.get()); + oboeCallbackProxy.setDataCallback(mFullDuplexDataPath.get()); } } @@ -786,6 +786,6 @@ void ActivityTestDisconnect::configureAfterOpen() { } else if (inputStream) { audioStreamGateway.setAudioSink(nullptr); } - oboeCallbackProxy.setCallback(&audioStreamGateway); + oboeCallbackProxy.setDataCallback(&audioStreamGateway); } diff --git a/apps/OboeTester/app/src/main/cpp/NativeAudioContext.h b/apps/OboeTester/app/src/main/cpp/NativeAudioContext.h index ed40ed27e..94ae680d1 100644 --- a/apps/OboeTester/app/src/main/cpp/NativeAudioContext.h +++ b/apps/OboeTester/app/src/main/cpp/NativeAudioContext.h @@ -46,7 +46,6 @@ #include "FullDuplexAnalyzer.h" #include "FullDuplexEcho.h" -#include "FullDuplexStream.h" #include "analyzer/GlitchAnalyzer.h" #include "analyzer/DataPathAnalyzer.h" #include "InputStreamCallbackAnalyzer.h" diff --git a/apps/OboeTester/app/src/main/cpp/OboeStreamCallbackProxy.h b/apps/OboeTester/app/src/main/cpp/OboeStreamCallbackProxy.h index 51b3ff5e3..157c0817c 100644 --- a/apps/OboeTester/app/src/main/cpp/OboeStreamCallbackProxy.h +++ b/apps/OboeTester/app/src/main/cpp/OboeStreamCallbackProxy.h @@ -137,7 +137,7 @@ class SynthWorkload { class OboeStreamCallbackProxy : public OboeTesterStreamCallback { public: - void setCallback(oboe::AudioStreamCallback *callback) { + void setDataCallback(oboe::AudioStreamDataCallback *callback) { mCallback = callback; setCallbackCount(0); mStatistics.clear(); @@ -251,7 +251,7 @@ class OboeStreamCallbackProxy : public OboeTesterStreamCallback { SynthWorkload mSynthWorkload; bool mHearWorkload = false; - oboe::AudioStreamCallback *mCallback = nullptr; + oboe::AudioStreamDataCallback *mCallback = nullptr; static bool mCallbackReturnStop; int64_t mCallbackCount = 0; diff --git a/apps/OboeTester/app/src/main/res/values/strings.xml b/apps/OboeTester/app/src/main/res/values/strings.xml index bdfea6055..d3157b3a0 100644 --- a/apps/OboeTester/app/src/main/res/values/strings.xml +++ b/apps/OboeTester/app/src/main/res/values/strings.xml @@ -166,6 +166,8 @@ 1024 2048 4096 + 8192 + 16384 diff --git a/include/oboe/FullDuplexStream.h b/include/oboe/FullDuplexStream.h new file mode 100644 index 000000000..15d2489a5 --- /dev/null +++ b/include/oboe/FullDuplexStream.h @@ -0,0 +1,324 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef OBOE_FULL_DUPLEX_STREAM_ +#define OBOE_FULL_DUPLEX_STREAM_ + +#include +#include "oboe/Definitions.h" +#include "oboe/AudioStream.h" +#include "oboe/AudioStreamCallback.h" + +namespace oboe { + +/** + * FullDuplexStream can be used to synchronize an input and output stream. + * + * For the builder of the output stream, call setDataCallback() with this object. + * + * When both streams are ready, onAudioReady() of the output stream will call onBothStreamsReady(). + * Callers must override onBothStreamsReady(). + * + * To ensure best results, open an output stream before the input stream. + * Call inputBuilder.setBufferCapacityInFrames(mOutputStream->getBufferCapacityInFrames() * 2). + * Also, call inputBuilder.setSampleRate(mOutputStream->getSampleRate()). + * + * Callers must call setInputStream() and setOutputStream(). + * Call start() to start both streams and stop() to stop both streams. + * Caller is responsible for closing both streams. + * + * Callers should handle error callbacks with setErrorCallback() for the output stream. + * When an error callback occurs for the output stream, Oboe will stop and close the output stream. + * The caller is responsible for stopping and closing the input stream. + * The caller should also reopen and restart both streams when the error callback is ErrorDisconnected. + * See the LiveEffect sample as an example of this. + * + */ +class FullDuplexStream : public AudioStreamDataCallback { +public: + FullDuplexStream() {} + virtual ~FullDuplexStream() = default; + + /** + * Sets the input stream. Calling this is mandatory. + * + * @param stream the output stream + */ + void setInputStream(AudioStream *stream) { + mInputStream = stream; + } + + /** + * Gets the input stream + * + * @return the input stream + */ + AudioStream *getInputStream() { + return mInputStream; + } + + /** + * Sets the output stream. Calling this is mandatory. + * + * @param stream the output stream + */ + void setOutputStream(AudioStream *stream) { + mOutputStream = stream; + } + + /** + * Gets the output stream + * + * @return the output stream + */ + AudioStream *getOutputStream() { + return mOutputStream; + } + + /** + * Attempts to start both streams. Please call setInputStream() and setOutputStream() before + * calling this function. + * + * @return result of the operation + */ + virtual Result start() { + mCountCallbacksToDrain = kNumCallbacksToDrain; + mCountInputBurstsCushion = mNumInputBurstsCushion; + mCountCallbacksToDiscard = kNumCallbacksToDiscard; + + // Determine maximum size that could possibly be called. + int32_t bufferSize = getOutputStream()->getBufferCapacityInFrames() + * getOutputStream()->getChannelCount(); + if (bufferSize > mBufferSize) { + mInputBuffer = std::make_unique(bufferSize); + mBufferSize = bufferSize; + } + + oboe::Result result = getInputStream()->requestStart(); + if (result != oboe::Result::OK) { + return result; + } + return getOutputStream()->requestStart(); + } + + /** + * Stops both streams. Returns Result::OK if neither stream had an error during close. + * + * @return result of the operation + */ + virtual Result stop() { + Result outputResult = Result::OK; + Result inputResult = Result::OK; + if (getOutputStream()) { + outputResult = mOutputStream->requestStop(); + } + if (getInputStream()) { + inputResult = mInputStream->requestStop(); + } + if (outputResult != Result::OK) { + return outputResult; + } else { + return inputResult; + } + } + + /** + * Reads input from the input stream. Callers should not call this directly as this is called + * in onAudioReady(). + * + * @param numFrames + * @return result of the operation + */ + virtual ResultWithValue readInput(int32_t numFrames) { + return getInputStream()->read(mInputBuffer.get(), numFrames, 0 /* timeout */); + } + + /** + * Called when data is available on both streams. + * Caller should override this method. + * numInputFrames and numOutputFrames may be zero. + * + * @param inputData buffer containing input data + * @param numInputFrames number of input frames + * @param outputData a place to put output data + * @param numOutputFrames number of output frames + * @return DataCallbackResult::Continue or DataCallbackResult::Stop + */ + virtual DataCallbackResult onBothStreamsReady( + const void *inputData, + int numInputFrames, + void *outputData, + int numOutputFrames + ) = 0; + + /** + * Called when the output stream is ready to process audio. + * This in return calls onBothStreamsReady() when data is available on both streams. + * Callers should call this function when the output stream is ready. + * Callers must override onBothStreamsReady(). + * + * @param audioStream pointer to the associated stream + * @param audioData a place to put output data + * @param numFrames number of frames to be processed + * @return DataCallbackResult::Continue or DataCallbackResult::Stop + * + */ + DataCallbackResult onAudioReady( + AudioStream *audioStream, + void *audioData, + int numFrames) { + DataCallbackResult callbackResult = DataCallbackResult::Continue; + int32_t actualFramesRead = 0; + + // Silence the output. + int32_t numBytes = numFrames * getOutputStream()->getBytesPerFrame(); + memset(audioData, 0 /* value */, numBytes); + + if (mCountCallbacksToDrain > 0) { + // Drain the input. + int32_t totalFramesRead = 0; + do { + ResultWithValue result = readInput(numFrames); + if (!result) { + // Ignore errors because input stream may not be started yet. + break; + } + actualFramesRead = result.value(); + totalFramesRead += actualFramesRead; + } while (actualFramesRead > 0); + // Only counts if we actually got some data. + if (totalFramesRead > 0) { + mCountCallbacksToDrain--; + } + + } else if (mCountInputBurstsCushion > 0) { + // Let the input fill up a bit so we are not so close to the write pointer. + mCountInputBurstsCushion--; + + } else if (mCountCallbacksToDiscard > 0) { + mCountCallbacksToDiscard--; + // Ignore. Allow the input to reach to equilibrium with the output. + ResultWithValue resultAvailable = getInputStream()->getAvailableFrames(); + if (!resultAvailable) { + callbackResult = DataCallbackResult::Stop; + } else { + int32_t framesAvailable = resultAvailable.value(); + if (framesAvailable >= mMinimumFramesBeforeRead) { + ResultWithValue resultRead = readInput(numFrames); + if (!resultRead) { + callbackResult = DataCallbackResult::Stop; + } + } + } + } else { + int32_t framesRead = 0; + ResultWithValue resultAvailable = getInputStream()->getAvailableFrames(); + if (!resultAvailable) { + callbackResult = DataCallbackResult::Stop; + } else { + int32_t framesAvailable = resultAvailable.value(); + if (framesAvailable >= mMinimumFramesBeforeRead) { + // Read data into input buffer. + ResultWithValue resultRead = readInput(numFrames); + if (!resultRead) { + callbackResult = DataCallbackResult::Stop; + } else { + framesRead = resultRead.value(); + } + } + } + + if (callbackResult == DataCallbackResult::Continue) { + callbackResult = onBothStreamsReady(mInputBuffer.get(), framesRead, + audioData, numFrames); + } + } + + if (callbackResult == DataCallbackResult::Stop) { + getInputStream()->requestStop(); + } + + return callbackResult; + } + + /** + * + * This is a cushion between the DSP and the application processor cursors to prevent collisions. + * Typically 0 for latency measurements or 1 for glitch tests. + * + * @param numBursts number of bursts to leave in the input buffer as a cushion + */ + void setNumInputBurstsCushion(int32_t numBursts) { + mNumInputBurstsCushion = numBursts; + } + + /** + * Get the number of bursts left in the input buffer as a cushion. + * + * @return number of bursts in the input buffer as a cushion + */ + int32_t getNumInputBurstsCushion() const { + return mNumInputBurstsCushion; + } + + /** + * Minimum number of frames in the input stream buffer before calling readInput(). + * + * @param numFrames number of bursts in the input buffer as a cushion + */ + void setMinimumFramesBeforeRead(int32_t numFrames) { + mMinimumFramesBeforeRead = numFrames; + } + + /** + * Gets the minimum number of frames in the input stream buffer before calling readInput(). + * + * @return minimum number of frames before reading + */ + int32_t getMinimumFramesBeforeRead() const { + return mMinimumFramesBeforeRead; + } + +private: + + // TODO add getters and setters + static constexpr int32_t kNumCallbacksToDrain = 20; + static constexpr int32_t kNumCallbacksToDiscard = 30; + + // let input fill back up, usually 0 or 1 + int32_t mNumInputBurstsCushion = 0; + int32_t mMinimumFramesBeforeRead = 0; + + // We want to reach a state where the input buffer is empty and + // the output buffer is full. + // These are used in order. + // Drain several callback so that input is empty. + int32_t mCountCallbacksToDrain = kNumCallbacksToDrain; + // Let the input fill back up slightly so we don't run dry. + int32_t mCountInputBurstsCushion = mNumInputBurstsCushion; + // Discard some callbacks so the input and output reach equilibrium. + int32_t mCountCallbacksToDiscard = kNumCallbacksToDiscard; + + AudioStream *mInputStream = nullptr; + AudioStream *mOutputStream = nullptr; + + int32_t mBufferSize = 0; + std::unique_ptr mInputBuffer; +}; + +} // namespace oboe + +#endif //OBOE_FULL_DUPLEX_STREAM_ diff --git a/include/oboe/Oboe.h b/include/oboe/Oboe.h index ea595af69..b9c948af8 100644 --- a/include/oboe/Oboe.h +++ b/include/oboe/Oboe.h @@ -35,5 +35,6 @@ #include "oboe/StabilizedCallback.h" #include "oboe/FifoBuffer.h" #include "oboe/OboeExtensions.h" +#include "oboe/FullDuplexStream.h" #endif //OBOE_OBOE_H diff --git a/samples/LiveEffect/src/main/cpp/CMakeLists.txt b/samples/LiveEffect/src/main/cpp/CMakeLists.txt index fbf9becfe..4f16621c9 100644 --- a/samples/LiveEffect/src/main/cpp/CMakeLists.txt +++ b/samples/LiveEffect/src/main/cpp/CMakeLists.txt @@ -26,7 +26,6 @@ add_subdirectory(${OBOE_DIR} ./oboe-bin) add_library(liveEffect SHARED LiveEffectEngine.cpp - FullDuplexStream.cpp jni_bridge.cpp ${SAMPLE_ROOT_DIR}/debug-utils/trace.cpp) target_include_directories(liveEffect diff --git a/samples/LiveEffect/src/main/cpp/FullDuplexPass.h b/samples/LiveEffect/src/main/cpp/FullDuplexPass.h index 6f5d05ffa..4c8bbd6db 100644 --- a/samples/LiveEffect/src/main/cpp/FullDuplexPass.h +++ b/samples/LiveEffect/src/main/cpp/FullDuplexPass.h @@ -17,16 +17,12 @@ #ifndef SAMPLES_FULLDUPLEXPASS_H #define SAMPLES_FULLDUPLEXPASS_H -#include "FullDuplexStream.h" - -class FullDuplexPass : public FullDuplexStream { +class FullDuplexPass : public oboe::FullDuplexStream { public: virtual oboe::DataCallbackResult onBothStreamsReady( - std::shared_ptr inputStream, const void *inputData, int numInputFrames, - std::shared_ptr outputStream, void *outputData, int numOutputFrames) { // Copy the input samples to the output with a little arbitrary gain change. @@ -36,7 +32,7 @@ class FullDuplexPass : public FullDuplexStream { float *outputFloats = static_cast(outputData); // It also assumes the channel count for each stream is the same. - int32_t samplesPerFrame = outputStream->getChannelCount(); + int32_t samplesPerFrame = getOutputStream()->getChannelCount(); int32_t numInputSamples = numInputFrames * samplesPerFrame; int32_t numOutputSamples = numOutputFrames * samplesPerFrame; diff --git a/samples/LiveEffect/src/main/cpp/FullDuplexStream.cpp b/samples/LiveEffect/src/main/cpp/FullDuplexStream.cpp deleted file mode 100644 index e38bb5f72..000000000 --- a/samples/LiveEffect/src/main/cpp/FullDuplexStream.cpp +++ /dev/null @@ -1,128 +0,0 @@ -/* - * Copyright 2019 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "FullDuplexStream.h" - -oboe::DataCallbackResult FullDuplexStream::onAudioReady( - oboe::AudioStream *outputStream, - void *audioData, - int numFrames) { - oboe::DataCallbackResult callbackResult = oboe::DataCallbackResult::Continue; - int32_t actualFramesRead = 0; - - // Silence the output. - int32_t numBytes = numFrames * outputStream->getBytesPerFrame(); - memset(audioData, 0 /* value */, numBytes); - - if (mCountCallbacksToDrain > 0) { - // Drain the input. - int32_t totalFramesRead = 0; - do { - oboe::ResultWithValue result = mInputStream->read(mInputBuffer.get(), - numFrames, - 0 /* timeout */); - if (!result) { - // Ignore errors because input stream may not be started yet. - break; - } - actualFramesRead = result.value(); - totalFramesRead += actualFramesRead; - } while (actualFramesRead > 0); - // Only counts if we actually got some data. - if (totalFramesRead > 0) { - mCountCallbacksToDrain--; - } - - } else if (mCountInputBurstsCushion > 0) { - // Let the input fill up a bit so we are not so close to the write pointer. - mCountInputBurstsCushion--; - - } else if (mCountCallbacksToDiscard > 0) { - // Ignore. Allow the input to reach to equilibrium with the output. - oboe::ResultWithValue result = mInputStream->read(mInputBuffer.get(), - numFrames, - 0 /* timeout */); - if (!result) { - callbackResult = oboe::DataCallbackResult::Stop; - } - mCountCallbacksToDiscard--; - - } else { - // Read data into input buffer. - oboe::ResultWithValue result = mInputStream->read(mInputBuffer.get(), - numFrames, - 0 /* timeout */); - if (!result) { - callbackResult = oboe::DataCallbackResult::Stop; - } else { - int32_t framesRead = result.value(); - - callbackResult = onBothStreamsReady( - mInputStream, mInputBuffer.get(), framesRead, - mOutputStream, audioData, numFrames - ); - } - } - - if (callbackResult == oboe::DataCallbackResult::Stop) { - mInputStream->requestStop(); - } - - return callbackResult; -} - -oboe::Result FullDuplexStream::start() { - mCountCallbacksToDrain = kNumCallbacksToDrain; - mCountInputBurstsCushion = mNumInputBurstsCushion; - mCountCallbacksToDiscard = kNumCallbacksToDiscard; - - // Determine maximum size that could possibly be called. - int32_t bufferSize = mOutputStream->getBufferCapacityInFrames() - * mOutputStream->getChannelCount(); - if (bufferSize > mBufferSize) { - mInputBuffer = std::make_unique(bufferSize); - mBufferSize = bufferSize; - } - oboe::Result result = mInputStream->requestStart(); - if (result != oboe::Result::OK) { - return result; - } - return mOutputStream->requestStart(); -} - -oboe::Result FullDuplexStream::stop() { - oboe::Result outputResult = oboe::Result::OK; - oboe::Result inputResult = oboe::Result::OK; - if (mOutputStream) { - outputResult = mOutputStream->requestStop(); - } - if (mInputStream) { - inputResult = mInputStream->requestStop(); - } - if (outputResult != oboe::Result::OK) { - return outputResult; - } else { - return inputResult; - } -} - -int32_t FullDuplexStream::getNumInputBurstsCushion() const { - return mNumInputBurstsCushion; -} - -void FullDuplexStream::setNumInputBurstsCushion(int32_t numBursts) { - FullDuplexStream::mNumInputBurstsCushion = numBursts; -} diff --git a/samples/LiveEffect/src/main/cpp/FullDuplexStream.h b/samples/LiveEffect/src/main/cpp/FullDuplexStream.h deleted file mode 100644 index c30bc04a8..000000000 --- a/samples/LiveEffect/src/main/cpp/FullDuplexStream.h +++ /dev/null @@ -1,102 +0,0 @@ -/* - * Copyright 2019 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#ifndef OBOE_FULL_DUPLEX_STREAM_H -#define OBOE_FULL_DUPLEX_STREAM_H - -#include -#include - -#include "oboe/Oboe.h" - -class FullDuplexStream : public oboe::AudioStreamCallback { -public: - FullDuplexStream() {} - virtual ~FullDuplexStream() = default; - - void setInputStream(std::shared_ptr stream) { - mInputStream = stream; - } - - void setOutputStream(std::shared_ptr stream) { - mOutputStream = stream; - } - - virtual oboe::Result start(); - - virtual oboe::Result stop(); - - /** - * Called when data is available on both streams. - * App should override this method. - */ - virtual oboe::DataCallbackResult onBothStreamsReady( - std::shared_ptr inputStream, - const void *inputData, - int numInputFrames, - std::shared_ptr outputStream, - void *outputData, - int numOutputFrames - ) = 0; - - /** - * Called by Oboe when the stream is ready to process audio. - * This implements the stream synchronization. App should NOT override this method. - */ - oboe::DataCallbackResult onAudioReady( - oboe::AudioStream *audioStream, - void *audioData, - int numFrames) override; - - int32_t getNumInputBurstsCushion() const; - - /** - * Number of bursts to leave in the input buffer as a cushion. - * Typically 0 for latency measurements - * or 1 for glitch tests. - * - * @param mNumInputBurstsCushion - */ - void setNumInputBurstsCushion(int32_t numInputBurstsCushion); - -private: - - // TODO add getters and setters - static constexpr int32_t kNumCallbacksToDrain = 20; - static constexpr int32_t kNumCallbacksToDiscard = 30; - - // let input fill back up, usually 0 or 1 - int32_t mNumInputBurstsCushion = 1; - - // We want to reach a state where the input buffer is empty and - // the output buffer is full. - // These are used in order. - // Drain several callback so that input is empty. - int32_t mCountCallbacksToDrain = kNumCallbacksToDrain; - // Let the input fill back up slightly so we don't run dry. - int32_t mCountInputBurstsCushion = mNumInputBurstsCushion; - // Discard some callbacks so the input and output reach equilibrium. - int32_t mCountCallbacksToDiscard = kNumCallbacksToDiscard; - - std::shared_ptr mInputStream; - std::shared_ptr mOutputStream; - - int32_t mBufferSize = 0; - std::unique_ptr mInputBuffer; -}; - - -#endif //OBOE_FULL_DUPLEX_STREAM_H diff --git a/samples/LiveEffect/src/main/cpp/LiveEffectEngine.cpp b/samples/LiveEffect/src/main/cpp/LiveEffectEngine.cpp index e02e00906..140dd3284 100644 --- a/samples/LiveEffect/src/main/cpp/LiveEffectEngine.cpp +++ b/samples/LiveEffect/src/main/cpp/LiveEffectEngine.cpp @@ -102,8 +102,8 @@ oboe::Result LiveEffectEngine::openStreams() { } warnIfNotLowLatency(mRecordingStream); - mFullDuplexPass.setInputStream(mRecordingStream); - mFullDuplexPass.setOutputStream(mPlayStream); + mFullDuplexPass.setInputStream(mRecordingStream.get()); + mFullDuplexPass.setOutputStream(mPlayStream.get()); return result; } @@ -235,4 +235,20 @@ void LiveEffectEngine::onErrorAfterClose(oboe::AudioStream *oboeStream, LOGE("%s stream Error after close: %s", oboe::convertToText(oboeStream->getDirection()), oboe::convertToText(error)); + + // Stop the Full Duplex stream. + // Since the error callback occurs only for the output stream, close the input stream. + mFullDuplexPass.stop(); + mFullDuplexPass.setOutputStream(nullptr); + closeStream(mRecordingStream); + mFullDuplexPass.setInputStream(nullptr); + + // Restart the stream if the error is a disconnect. + if (error == oboe::Result::ErrorDisconnected) { + LOGI("Restarting AudioStream"); + oboe::Result result = openStreams(); + if (result == oboe::Result::OK) { + mFullDuplexPass.start(); + } + } } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index ddbff2d5e..45b1f8c74 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -28,6 +28,7 @@ add_executable( testOboe testAAudio.cpp testFlowgraph.cpp + testFullDuplexStream.cpp testResampler.cpp testReturnStop.cpp testStreamClosedMethods.cpp diff --git a/tests/testFullDuplexStream.cpp b/tests/testFullDuplexStream.cpp new file mode 100644 index 000000000..b3b96f2ab --- /dev/null +++ b/tests/testFullDuplexStream.cpp @@ -0,0 +1,208 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#include + +#include + +using namespace oboe; + +static constexpr int kTimeToSleepMicros = 5 * 1000 * 1000; // 5 s + +using TestFullDuplexStreamParams = std::tuple; + +class TestFullDuplexStream : public ::testing::Test, + public ::testing::WithParamInterface, + public FullDuplexStream { +public: + DataCallbackResult onBothStreamsReady( + const void *inputData, + int numInputFrames, + void *outputData, + int numOutputFrames) override { + mCallbackCount++; + if (numInputFrames == numOutputFrames) { + mGoodCallbackCount++; + } + return DataCallbackResult::Continue; + } + +protected: + + void openStream(AudioApi inputAudioApi, PerformanceMode inputPerfMode, + AudioApi outputAudioApi, PerformanceMode outputPerfMode) { + mOutputBuilder.setDirection(Direction::Output); + if (mOutputBuilder.isAAudioRecommended()) { + mOutputBuilder.setAudioApi(outputAudioApi); + } + mOutputBuilder.setPerformanceMode(outputPerfMode); + mOutputBuilder.setChannelCount(1); + mOutputBuilder.setFormat(AudioFormat::Float); + mOutputBuilder.setDataCallback(this); + + Result r = mOutputBuilder.openStream(&mOutputStream); + ASSERT_EQ(r, Result::OK) << "Failed to open output stream " << convertToText(r); + + mInputBuilder.setDirection(Direction::Input); + if (mInputBuilder.isAAudioRecommended()) { + mInputBuilder.setAudioApi(inputAudioApi); + } + mInputBuilder.setPerformanceMode(inputPerfMode); + mInputBuilder.setChannelCount(1); + mInputBuilder.setFormat(AudioFormat::Float); + mInputBuilder.setBufferCapacityInFrames(mOutputStream->getBufferCapacityInFrames() * 2); + mInputBuilder.setSampleRate(mOutputStream->getSampleRate()); + + r = mInputBuilder.openStream(&mInputStream); + ASSERT_EQ(r, Result::OK) << "Failed to open input stream " << convertToText(r); + + setInputStream(mInputStream); + setOutputStream(mOutputStream); + } + + void startStream() { + Result r = start(); + ASSERT_EQ(r, Result::OK) << "Failed to start streams " << convertToText(r); + } + + void stopStream() { + Result r = stop(); + ASSERT_EQ(r, Result::OK) << "Failed to stop streams " << convertToText(r); + } + + void closeStream() { + Result r = mOutputStream->close(); + ASSERT_EQ(r, Result::OK) << "Failed to close output stream " << convertToText(r); + setOutputStream(nullptr); + r = mInputStream->close(); + ASSERT_EQ(r, Result::OK) << "Failed to close input stream " << convertToText(r); + setInputStream(nullptr); + } + + void checkXRuns() { + // Expect few xRuns with the use of full duplex stream + EXPECT_LT(mInputStream->getXRunCount().value(), 10); + EXPECT_LT(mOutputStream->getXRunCount().value(), 10); + } + + void checkInputAndOutputBufferSizesMatch() { + // Expect the large majority of callbacks to have the same sized input and output + EXPECT_GE(mGoodCallbackCount, mCallbackCount * 9 / 10); + } + + AudioStreamBuilder mInputBuilder; + AudioStreamBuilder mOutputBuilder; + AudioStream *mInputStream = nullptr; + AudioStream *mOutputStream = nullptr; + std::atomic mCallbackCount{0}; + std::atomic mGoodCallbackCount{0}; +}; + +TEST_P(TestFullDuplexStream, VerifyFullDuplexStream) { + const AudioApi inputAudioApi = std::get<0>(GetParam()); + const PerformanceMode inputPerformanceMode = std::get<1>(GetParam()); + const AudioApi outputAudioApi = std::get<2>(GetParam()); + const PerformanceMode outputPerformanceMode = std::get<3>(GetParam()); + + openStream(inputAudioApi, inputPerformanceMode, outputAudioApi, outputPerformanceMode); + startStream(); + usleep(kTimeToSleepMicros); + checkXRuns(); + checkInputAndOutputBufferSizesMatch(); + stopStream(); + closeStream(); +} + +INSTANTIATE_TEST_SUITE_P( + TestFullDuplexStreamTest, + TestFullDuplexStream, + ::testing::Values( + TestFullDuplexStreamParams({AudioApi::AAudio, PerformanceMode::LowLatency, + AudioApi::AAudio, PerformanceMode::LowLatency}), + TestFullDuplexStreamParams({AudioApi::AAudio, PerformanceMode::LowLatency, + AudioApi::AAudio, PerformanceMode::None}), + TestFullDuplexStreamParams({AudioApi::AAudio, PerformanceMode::LowLatency, + AudioApi::AAudio, PerformanceMode::PowerSaving}), + TestFullDuplexStreamParams({AudioApi::AAudio, PerformanceMode::LowLatency, + AudioApi::OpenSLES, PerformanceMode::LowLatency}), + TestFullDuplexStreamParams({AudioApi::AAudio, PerformanceMode::LowLatency, + AudioApi::OpenSLES, PerformanceMode::None}), + TestFullDuplexStreamParams({AudioApi::AAudio, PerformanceMode::LowLatency, + AudioApi::OpenSLES, PerformanceMode::PowerSaving}), + TestFullDuplexStreamParams({AudioApi::AAudio, PerformanceMode::None, + AudioApi::AAudio, PerformanceMode::LowLatency}), + TestFullDuplexStreamParams({AudioApi::AAudio, PerformanceMode::None, + AudioApi::AAudio, PerformanceMode::None}), + TestFullDuplexStreamParams({AudioApi::AAudio, PerformanceMode::None, + AudioApi::AAudio, PerformanceMode::PowerSaving}), + TestFullDuplexStreamParams({AudioApi::AAudio, PerformanceMode::None, + AudioApi::OpenSLES, PerformanceMode::LowLatency}), + TestFullDuplexStreamParams({AudioApi::AAudio, PerformanceMode::None, + AudioApi::OpenSLES, PerformanceMode::None}), + TestFullDuplexStreamParams({AudioApi::AAudio, PerformanceMode::None, + AudioApi::OpenSLES, PerformanceMode::PowerSaving}), + TestFullDuplexStreamParams({AudioApi::AAudio, PerformanceMode::PowerSaving, + AudioApi::AAudio, PerformanceMode::LowLatency}), + TestFullDuplexStreamParams({AudioApi::AAudio, PerformanceMode::PowerSaving, + AudioApi::AAudio, PerformanceMode::None}), + TestFullDuplexStreamParams({AudioApi::AAudio, PerformanceMode::PowerSaving, + AudioApi::AAudio, PerformanceMode::PowerSaving}), + TestFullDuplexStreamParams({AudioApi::AAudio, PerformanceMode::PowerSaving, + AudioApi::OpenSLES, PerformanceMode::LowLatency}), + TestFullDuplexStreamParams({AudioApi::AAudio, PerformanceMode::PowerSaving, + AudioApi::OpenSLES, PerformanceMode::None}), + TestFullDuplexStreamParams({AudioApi::AAudio, PerformanceMode::PowerSaving, + AudioApi::OpenSLES, PerformanceMode::PowerSaving}), + TestFullDuplexStreamParams({AudioApi::OpenSLES, PerformanceMode::LowLatency, + AudioApi::AAudio, PerformanceMode::LowLatency}), + TestFullDuplexStreamParams({AudioApi::OpenSLES, PerformanceMode::LowLatency, + AudioApi::AAudio, PerformanceMode::None}), + TestFullDuplexStreamParams({AudioApi::OpenSLES, PerformanceMode::LowLatency, + AudioApi::AAudio, PerformanceMode::PowerSaving}), + TestFullDuplexStreamParams({AudioApi::OpenSLES, PerformanceMode::LowLatency, + AudioApi::OpenSLES, PerformanceMode::LowLatency}), + TestFullDuplexStreamParams({AudioApi::OpenSLES, PerformanceMode::LowLatency, + AudioApi::OpenSLES, PerformanceMode::None}), + TestFullDuplexStreamParams({AudioApi::OpenSLES, PerformanceMode::LowLatency, + AudioApi::OpenSLES, PerformanceMode::PowerSaving}), + TestFullDuplexStreamParams({AudioApi::OpenSLES, PerformanceMode::None, + AudioApi::AAudio, PerformanceMode::LowLatency}), + TestFullDuplexStreamParams({AudioApi::OpenSLES, PerformanceMode::None, + AudioApi::AAudio, PerformanceMode::None}), + TestFullDuplexStreamParams({AudioApi::OpenSLES, PerformanceMode::None, + AudioApi::AAudio, PerformanceMode::PowerSaving}), + TestFullDuplexStreamParams({AudioApi::OpenSLES, PerformanceMode::None, + AudioApi::OpenSLES, PerformanceMode::LowLatency}), + TestFullDuplexStreamParams({AudioApi::OpenSLES, PerformanceMode::None, + AudioApi::OpenSLES, PerformanceMode::None}), + TestFullDuplexStreamParams({AudioApi::OpenSLES, PerformanceMode::None, + AudioApi::OpenSLES, PerformanceMode::PowerSaving}), + TestFullDuplexStreamParams({AudioApi::OpenSLES, PerformanceMode::PowerSaving, + AudioApi::AAudio, PerformanceMode::LowLatency}), + TestFullDuplexStreamParams({AudioApi::OpenSLES, PerformanceMode::PowerSaving, + AudioApi::AAudio, PerformanceMode::None}), + TestFullDuplexStreamParams({AudioApi::OpenSLES, PerformanceMode::PowerSaving, + AudioApi::AAudio, PerformanceMode::PowerSaving}), + TestFullDuplexStreamParams({AudioApi::OpenSLES, PerformanceMode::PowerSaving, + AudioApi::OpenSLES, PerformanceMode::LowLatency}), + TestFullDuplexStreamParams({AudioApi::OpenSLES, PerformanceMode::PowerSaving, + AudioApi::OpenSLES, PerformanceMode::None}), + TestFullDuplexStreamParams({AudioApi::OpenSLES, PerformanceMode::PowerSaving, + AudioApi::OpenSLES, PerformanceMode::PowerSaving}) + ) +);