aboutsummaryrefslogtreecommitdiff
path: root/externals/oboe/apps/OboeTester/app/src/main/cpp/NativeAudioContext.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'externals/oboe/apps/OboeTester/app/src/main/cpp/NativeAudioContext.cpp')
-rw-r--r--externals/oboe/apps/OboeTester/app/src/main/cpp/NativeAudioContext.cpp810
1 files changed, 810 insertions, 0 deletions
diff --git a/externals/oboe/apps/OboeTester/app/src/main/cpp/NativeAudioContext.cpp b/externals/oboe/apps/OboeTester/app/src/main/cpp/NativeAudioContext.cpp
new file mode 100644
index 0000000000..f4c3f6b957
--- /dev/null
+++ b/externals/oboe/apps/OboeTester/app/src/main/cpp/NativeAudioContext.cpp
@@ -0,0 +1,810 @@
+/*
+ * Copyright 2017 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.
+ */
+
+// Set to 1 for debugging race condition #1180 with mAAudioStream.
+// See also AudioStreamAAudio.cpp in Oboe.
+// This was left in the code so that we could test the fix again easily in the future.
+// We could not trigger the race condition without adding these get calls and the sleeps.
+#define DEBUG_CLOSE_RACE 0
+
+#include <fstream>
+#include <iostream>
+#if DEBUG_CLOSE_RACE
+#include <thread>
+#endif // DEBUG_CLOSE_RACE
+#include <vector>
+
+#include "oboe/AudioClock.h"
+#include "util/WaveFileWriter.h"
+
+#include "NativeAudioContext.h"
+
+using namespace oboe;
+
+static oboe::AudioApi convertNativeApiToAudioApi(int nativeApi) {
+ switch (nativeApi) {
+ default:
+ case NATIVE_MODE_UNSPECIFIED:
+ return oboe::AudioApi::Unspecified;
+ case NATIVE_MODE_AAUDIO:
+ return oboe::AudioApi::AAudio;
+ case NATIVE_MODE_OPENSLES:
+ return oboe::AudioApi::OpenSLES;
+ }
+}
+
+class MyOboeOutputStream : public WaveFileOutputStream {
+public:
+ void write(uint8_t b) override {
+ mData.push_back(b);
+ }
+
+ int32_t length() {
+ return (int32_t) mData.size();
+ }
+
+ uint8_t *getData() {
+ return mData.data();
+ }
+
+private:
+ std::vector<uint8_t> mData;
+};
+
+bool ActivityContext::mUseCallback = true;
+int ActivityContext::callbackSize = 0;
+
+std::shared_ptr<oboe::AudioStream> ActivityContext::getOutputStream() {
+ for (auto entry : mOboeStreams) {
+ std::shared_ptr<oboe::AudioStream> oboeStream = entry.second;
+ if (oboeStream->getDirection() == oboe::Direction::Output) {
+ return oboeStream;
+ }
+ }
+ return nullptr;
+}
+
+std::shared_ptr<oboe::AudioStream> ActivityContext::getInputStream() {
+ for (auto entry : mOboeStreams) {
+ std::shared_ptr<oboe::AudioStream> oboeStream = entry.second;
+ if (oboeStream != nullptr) {
+ if (oboeStream->getDirection() == oboe::Direction::Input) {
+ return oboeStream;
+ }
+ }
+ }
+ return nullptr;
+}
+
+void ActivityContext::freeStreamIndex(int32_t streamIndex) {
+ mOboeStreams[streamIndex].reset();
+ mOboeStreams.erase(streamIndex);
+}
+
+int32_t ActivityContext::allocateStreamIndex() {
+ return mNextStreamHandle++;
+}
+
+oboe::Result ActivityContext::release() {
+ oboe::Result result = oboe::Result::OK;
+ stopBlockingIOThread();
+ for (auto entry : mOboeStreams) {
+ std::shared_ptr<oboe::AudioStream> oboeStream = entry.second;
+ result = oboeStream->release();
+ }
+ return result;
+}
+
+void ActivityContext::close(int32_t streamIndex) {
+ stopBlockingIOThread();
+ std::shared_ptr<oboe::AudioStream> oboeStream = getStream(streamIndex);
+ if (oboeStream != nullptr) {
+ oboeStream->close();
+ LOGD("ActivityContext::%s() delete stream %d ", __func__, streamIndex);
+ freeStreamIndex(streamIndex);
+ }
+}
+
+bool ActivityContext::isMMapUsed(int32_t streamIndex) {
+ std::shared_ptr<oboe::AudioStream> oboeStream = getStream(streamIndex);
+ if (oboeStream == nullptr) return false;
+ if (oboeStream->getAudioApi() != AudioApi::AAudio) return false;
+ return AAudioExtensions::getInstance().isMMapUsed(oboeStream.get());
+}
+
+oboe::Result ActivityContext::pause() {
+ oboe::Result result = oboe::Result::OK;
+ stopBlockingIOThread();
+ for (auto entry : mOboeStreams) {
+ std::shared_ptr<oboe::AudioStream> oboeStream = entry.second;
+ result = oboeStream->requestPause();
+ }
+ return result;
+}
+
+oboe::Result ActivityContext::stopAllStreams() {
+ oboe::Result result = oboe::Result::OK;
+ stopBlockingIOThread();
+ for (auto entry : mOboeStreams) {
+ std::shared_ptr<oboe::AudioStream> oboeStream = entry.second;
+ result = oboeStream->requestStop();
+ }
+ return result;
+}
+
+void ActivityContext::configureBuilder(bool isInput, oboe::AudioStreamBuilder &builder) {
+ // We needed the proxy because we did not know the channelCount when we setup the Builder.
+ if (mUseCallback) {
+ builder.setDataCallback(&oboeCallbackProxy);
+ }
+}
+
+int ActivityContext::open(jint nativeApi,
+ jint sampleRate,
+ jint channelCount,
+ jint channelMask,
+ jint format,
+ jint sharingMode,
+ jint performanceMode,
+ jint inputPreset,
+ jint usage,
+ jint contentType,
+ jint bufferCapacityInFrames,
+ jint deviceId,
+ jint sessionId,
+ jboolean channelConversionAllowed,
+ jboolean formatConversionAllowed,
+ jint rateConversionQuality,
+ jboolean isMMap,
+ jboolean isInput,
+ jint spatializationBehavior) {
+ oboe::AudioApi audioApi = oboe::AudioApi::Unspecified;
+ switch (nativeApi) {
+ case NATIVE_MODE_UNSPECIFIED:
+ case NATIVE_MODE_AAUDIO:
+ case NATIVE_MODE_OPENSLES:
+ audioApi = convertNativeApiToAudioApi(nativeApi);
+ break;
+ default:
+ return (jint) oboe::Result::ErrorOutOfRange;
+ }
+
+ int32_t streamIndex = allocateStreamIndex();
+ if (streamIndex < 0) {
+ LOGE("ActivityContext::open() stream array full");
+ return (jint) oboe::Result::ErrorNoFreeHandles;
+ }
+
+ if (channelCount < 0 || channelCount > 256) {
+ LOGE("ActivityContext::open() channels out of range");
+ return (jint) oboe::Result::ErrorOutOfRange;
+ }
+
+ // Create an audio stream.
+ oboe::AudioStreamBuilder builder;
+ builder.setChannelCount(channelCount)
+ ->setDirection(isInput ? oboe::Direction::Input : oboe::Direction::Output)
+ ->setSharingMode((oboe::SharingMode) sharingMode)
+ ->setPerformanceMode((oboe::PerformanceMode) performanceMode)
+ ->setInputPreset((oboe::InputPreset)inputPreset)
+ ->setUsage((oboe::Usage)usage)
+ ->setContentType((oboe::ContentType)contentType)
+ ->setBufferCapacityInFrames(bufferCapacityInFrames)
+ ->setDeviceId(deviceId)
+ ->setSessionId((oboe::SessionId) sessionId)
+ ->setSampleRate(sampleRate)
+ ->setFormat((oboe::AudioFormat) format)
+ ->setChannelConversionAllowed(channelConversionAllowed)
+ ->setFormatConversionAllowed(formatConversionAllowed)
+ ->setSampleRateConversionQuality((oboe::SampleRateConversionQuality) rateConversionQuality)
+ ->setSpatializationBehavior((oboe::SpatializationBehavior) spatializationBehavior)
+ ;
+ if (channelMask != (jint) oboe::ChannelMask::Unspecified) {
+ // Set channel mask when it is specified.
+ builder.setChannelMask((oboe::ChannelMask) channelMask);
+ }
+ if (mUseCallback) {
+ builder.setFramesPerCallback(callbackSize);
+ }
+ configureBuilder(isInput, builder);
+
+ builder.setAudioApi(audioApi);
+
+ // Temporarily set the AAudio MMAP policy to disable MMAP or not.
+ bool oldMMapEnabled = AAudioExtensions::getInstance().isMMapEnabled();
+ AAudioExtensions::getInstance().setMMapEnabled(isMMap);
+
+ // Record time for opening.
+ if (isInput) {
+ mInputOpenedAt = oboe::AudioClock::getNanoseconds();
+ } else {
+ mOutputOpenedAt = oboe::AudioClock::getNanoseconds();
+ }
+ // Open a stream based on the builder settings.
+ std::shared_ptr<oboe::AudioStream> oboeStream;
+ Result result = builder.openStream(oboeStream);
+ AAudioExtensions::getInstance().setMMapEnabled(oldMMapEnabled);
+ if (result != Result::OK) {
+ freeStreamIndex(streamIndex);
+ streamIndex = -1;
+ } else {
+ mOboeStreams[streamIndex] = oboeStream; // save shared_ptr
+
+ mChannelCount = oboeStream->getChannelCount(); // FIXME store per stream
+ mFramesPerBurst = oboeStream->getFramesPerBurst();
+ mSampleRate = oboeStream->getSampleRate();
+
+ createRecording();
+
+ finishOpen(isInput, oboeStream);
+ }
+
+ if (!mUseCallback) {
+ int numSamples = getFramesPerBlock() * mChannelCount;
+ dataBuffer = std::make_unique<float[]>(numSamples);
+ }
+
+ if (result != Result::OK) {
+ return (int) result;
+ } else {
+ configureAfterOpen();
+ return streamIndex;
+ }
+}
+
+oboe::Result ActivityContext::start() {
+ oboe::Result result = oboe::Result::OK;
+ std::shared_ptr<oboe::AudioStream> inputStream = getInputStream();
+ std::shared_ptr<oboe::AudioStream> outputStream = getOutputStream();
+ if (inputStream == nullptr && outputStream == nullptr) {
+ LOGD("%s() - no streams defined", __func__);
+ return oboe::Result::ErrorInvalidState; // not open
+ }
+
+ audioStreamGateway.reset();
+ result = startStreams();
+
+ if (!mUseCallback && result == oboe::Result::OK) {
+ // Instead of using the callback, start a thread that writes the stream.
+ threadEnabled.store(true);
+ dataThread = new std::thread(threadCallback, this);
+ }
+
+#if DEBUG_CLOSE_RACE
+ // Also put a sleep for 400 msec in AudioStreamAAudio::updateFramesRead().
+ if (outputStream != nullptr) {
+ std::thread raceDebugger([outputStream]() {
+ while (outputStream->getState() != StreamState::Closed) {
+ int64_t framesRead = outputStream->getFramesRead();
+ LOGD("raceDebugger, framesRead = %d, state = %d",
+ (int) framesRead, (int) outputStream->getState());
+ }
+ });
+ raceDebugger.detach();
+ }
+#endif // DEBUG_CLOSE_RACE
+
+ return result;
+}
+
+oboe::Result ActivityContext::flush() {
+ oboe::Result result = oboe::Result::OK;
+ for (auto entry : mOboeStreams) {
+ std::shared_ptr<oboe::AudioStream> oboeStream = entry.second;
+ result = oboeStream->requestFlush();
+ }
+ return result;
+}
+
+int32_t ActivityContext::saveWaveFile(const char *filename) {
+ if (mRecording == nullptr) {
+ LOGW("ActivityContext::saveWaveFile(%s) but no recording!", filename);
+ return -1;
+ }
+ if (mRecording->getSizeInFrames() == 0) {
+ LOGW("ActivityContext::saveWaveFile(%s) but no frames!", filename);
+ return -2;
+ }
+ MyOboeOutputStream outStream;
+ WaveFileWriter writer(&outStream);
+ // You must setup the format before the first write().
+ writer.setFrameRate(mSampleRate);
+ writer.setSamplesPerFrame(mRecording->getChannelCount());
+ writer.setBitsPerSample(24);
+ writer.setFrameCount(mRecording->getSizeInFrames());
+ float buffer[mRecording->getChannelCount()];
+ // Read samples from start to finish.
+ mRecording->rewind();
+ for (int32_t frameIndex = 0; frameIndex < mRecording->getSizeInFrames(); frameIndex++) {
+ mRecording->read(buffer, 1 /* numFrames */);
+ for (int32_t i = 0; i < mRecording->getChannelCount(); i++) {
+ writer.write(buffer[i]);
+ }
+ }
+ writer.close();
+
+ if (outStream.length() > 0) {
+ auto myfile = std::ofstream(filename, std::ios::out | std::ios::binary);
+ myfile.write((char *) outStream.getData(), outStream.length());
+ myfile.close();
+ }
+
+ return outStream.length();
+}
+
+double ActivityContext::getTimestampLatency(int32_t streamIndex) {
+ std::shared_ptr<oboe::AudioStream> oboeStream = getStream(streamIndex);
+ if (oboeStream != nullptr) {
+ auto result = oboeStream->calculateLatencyMillis();
+ return (!result) ? -1.0 : result.value();
+ }
+ return -1.0;
+}
+
+// =================================================================== ActivityTestOutput
+void ActivityTestOutput::close(int32_t streamIndex) {
+ ActivityContext::close(streamIndex);
+ manyToMulti.reset(nullptr);
+ monoToMulti.reset(nullptr);
+ mVolumeRamp.reset();
+ mSinkFloat.reset();
+ mSinkI16.reset();
+ mSinkI24.reset();
+ mSinkI32.reset();
+ mSinkMemoryDirect.reset();
+}
+
+void ActivityTestOutput::setChannelEnabled(int channelIndex, bool enabled) {
+ if (manyToMulti == nullptr) {
+ return;
+ }
+ if (enabled) {
+ switch (mSignalType) {
+ case SignalType::Sine:
+ sineOscillators[channelIndex].frequency.disconnect();
+ sineOscillators[channelIndex].output.connect(manyToMulti->inputs[channelIndex].get());
+ break;
+ case SignalType::Sawtooth:
+ sawtoothOscillators[channelIndex].output.connect(manyToMulti->inputs[channelIndex].get());
+ break;
+ case SignalType::FreqSweep:
+ mLinearShape.output.connect(&sineOscillators[channelIndex].frequency);
+ sineOscillators[channelIndex].output.connect(manyToMulti->inputs[channelIndex].get());
+ break;
+ case SignalType::PitchSweep:
+ mExponentialShape.output.connect(&sineOscillators[channelIndex].frequency);
+ sineOscillators[channelIndex].output.connect(manyToMulti->inputs[channelIndex].get());
+ break;
+ case SignalType::WhiteNoise:
+ mWhiteNoise.output.connect(manyToMulti->inputs[channelIndex].get());
+ break;
+ default:
+ break;
+ }
+ } else {
+ manyToMulti->inputs[channelIndex]->disconnect();
+ }
+}
+
+void ActivityTestOutput::configureAfterOpen() {
+ manyToMulti = std::make_unique<ManyToMultiConverter>(mChannelCount);
+
+ std::shared_ptr<oboe::AudioStream> outputStream = getOutputStream();
+
+ mVolumeRamp = std::make_shared<RampLinear>(mChannelCount);
+ mVolumeRamp->setLengthInFrames(kRampMSec * outputStream->getSampleRate() /
+ MILLISECONDS_PER_SECOND);
+ mVolumeRamp->setTarget(mAmplitude);
+
+ mSinkFloat = std::make_shared<SinkFloat>(mChannelCount);
+ mSinkI16 = std::make_shared<SinkI16>(mChannelCount);
+ mSinkI24 = std::make_shared<SinkI24>(mChannelCount);
+ mSinkI32 = std::make_shared<SinkI32>(mChannelCount);
+ static constexpr int COMPRESSED_FORMAT_BYTES_PER_FRAME = 1;
+ mSinkMemoryDirect = std::make_shared<SinkMemoryDirect>(
+ mChannelCount, COMPRESSED_FORMAT_BYTES_PER_FRAME);
+
+ mTriangleOscillator.setSampleRate(outputStream->getSampleRate());
+ mTriangleOscillator.frequency.setValue(1.0/kSweepPeriod);
+ mTriangleOscillator.amplitude.setValue(1.0);
+ mTriangleOscillator.setPhase(-1.0);
+
+ mLinearShape.setMinimum(0.0);
+ mLinearShape.setMaximum(outputStream->getSampleRate() * 0.5); // Nyquist
+
+ mExponentialShape.setMinimum(110.0);
+ mExponentialShape.setMaximum(outputStream->getSampleRate() * 0.5); // Nyquist
+
+ mTriangleOscillator.output.connect(&(mLinearShape.input));
+ mTriangleOscillator.output.connect(&(mExponentialShape.input));
+ {
+ double frequency = 330.0;
+ // Go up by a minor third or a perfect fourth just intoned interval.
+ const float interval = (mChannelCount > 8) ? (6.0f / 5.0f) : (4.0f / 3.0f);
+ for (int i = 0; i < mChannelCount; i++) {
+ sineOscillators[i].setSampleRate(outputStream->getSampleRate());
+ sineOscillators[i].frequency.setValue(frequency);
+ sineOscillators[i].amplitude.setValue(AMPLITUDE_SINE / mChannelCount);
+ sawtoothOscillators[i].setSampleRate(outputStream->getSampleRate());
+ sawtoothOscillators[i].frequency.setValue(frequency);
+ sawtoothOscillators[i].amplitude.setValue(AMPLITUDE_SAWTOOTH / mChannelCount);
+
+ frequency *= interval; // each wave is at a higher frequency
+ setChannelEnabled(i, true);
+ }
+ }
+
+ mWhiteNoise.amplitude.setValue(0.5);
+
+ manyToMulti->output.connect(&(mVolumeRamp.get()->input));
+
+ mVolumeRamp->output.connect(&(mSinkFloat.get()->input));
+ mVolumeRamp->output.connect(&(mSinkI16.get()->input));
+ mVolumeRamp->output.connect(&(mSinkI24.get()->input));
+ mVolumeRamp->output.connect(&(mSinkI32.get()->input));
+
+ mSinkFloat->pullReset();
+ mSinkI16->pullReset();
+ mSinkI24->pullReset();
+ mSinkI32->pullReset();
+ mSinkMemoryDirect->pullReset();
+
+ configureStreamGateway();
+}
+
+void ActivityTestOutput::configureStreamGateway() {
+ std::shared_ptr<oboe::AudioStream> outputStream = getOutputStream();
+ if (outputStream->getFormat() == oboe::AudioFormat::I16) {
+ audioStreamGateway.setAudioSink(mSinkI16);
+ } else if (outputStream->getFormat() == oboe::AudioFormat::I24) {
+ audioStreamGateway.setAudioSink(mSinkI24);
+ } else if (outputStream->getFormat() == oboe::AudioFormat::I32) {
+ audioStreamGateway.setAudioSink(mSinkI32);
+ } else if (outputStream->getFormat() == oboe::AudioFormat::Float) {
+ audioStreamGateway.setAudioSink(mSinkFloat);
+ } else if (outputStream->getFormat() == oboe::AudioFormat::MP3) {
+ audioStreamGateway.setAudioSink(mSinkMemoryDirect);
+ }
+
+ if (mUseCallback) {
+ oboeCallbackProxy.setDataCallback(&audioStreamGateway);
+ }
+}
+
+void ActivityTestOutput::runBlockingIO() {
+ int32_t framesPerBlock = getFramesPerBlock();
+ oboe::DataCallbackResult callbackResult = oboe::DataCallbackResult::Continue;
+
+ std::shared_ptr<oboe::AudioStream> oboeStream = getOutputStream();
+ if (oboeStream == nullptr) {
+ LOGE("%s() : no stream found\n", __func__);
+ return;
+ }
+
+ while (threadEnabled.load()
+ && callbackResult == oboe::DataCallbackResult::Continue) {
+ // generate output by calling the callback
+ callbackResult = audioStreamGateway.onAudioReady(oboeStream.get(),
+ dataBuffer.get(),
+ framesPerBlock);
+
+ auto result = oboeStream->write(dataBuffer.get(),
+ framesPerBlock,
+ NANOS_PER_SECOND);
+
+ if (!result) {
+ LOGE("%s() returned %s\n", __func__, convertToText(result.error()));
+ break;
+ }
+ int32_t framesWritten = result.value();
+ if (framesWritten < framesPerBlock) {
+ LOGE("%s() : write() wrote %d of %d\n", __func__, framesWritten, framesPerBlock);
+ break;
+ }
+ }
+}
+
+oboe::Result ActivityTestOutput::startStreams() {
+ mSinkFloat->pullReset();
+ mSinkI16->pullReset();
+ mSinkI24->pullReset();
+ mSinkI32->pullReset();
+ mSinkMemoryDirect->pullReset();
+ if (mVolumeRamp != nullptr) {
+ mVolumeRamp->setTarget(mAmplitude);
+ }
+ return getOutputStream()->start();
+}
+
+void ActivityTestOutput::setupMemoryBuffer(std::unique_ptr<uint8_t[]> &buffer, int length) {
+ if (mSinkMemoryDirect != nullptr) {
+ mSinkMemoryDirect->setupMemoryBuffer(buffer, length);
+ }
+}
+
+// ======================================================================= ActivityTestInput
+void ActivityTestInput::configureAfterOpen() {
+ mInputAnalyzer.reset();
+ if (mUseCallback) {
+ oboeCallbackProxy.setDataCallback(&mInputAnalyzer);
+ }
+ mInputAnalyzer.setRecording(mRecording.get());
+}
+
+void ActivityTestInput::runBlockingIO() {
+ int32_t framesPerBlock = getFramesPerBlock();
+ oboe::DataCallbackResult callbackResult = oboe::DataCallbackResult::Continue;
+
+ std::shared_ptr<oboe::AudioStream> oboeStream = getInputStream();
+ if (oboeStream == nullptr) {
+ LOGE("%s() : no stream found\n", __func__);
+ return;
+ }
+
+ while (threadEnabled.load()
+ && callbackResult == oboe::DataCallbackResult::Continue) {
+
+ // Avoid glitches by waiting until there is extra data in the FIFO.
+ auto err = oboeStream->waitForAvailableFrames(mMinimumFramesBeforeRead, kNanosPerSecond);
+ if (!err) break;
+
+ // read from input
+ auto result = oboeStream->read(dataBuffer.get(),
+ framesPerBlock,
+ NANOS_PER_SECOND);
+ if (!result) {
+ LOGE("%s() : read() returned %s\n", __func__, convertToText(result.error()));
+ break;
+ }
+ int32_t framesRead = result.value();
+ if (framesRead < framesPerBlock) { // timeout?
+ LOGE("%s() : read() read %d of %d\n", __func__, framesRead, framesPerBlock);
+ break;
+ }
+
+ // analyze input
+ callbackResult = mInputAnalyzer.onAudioReady(oboeStream.get(),
+ dataBuffer.get(),
+ framesRead);
+ }
+}
+
+oboe::Result ActivityRecording::stopPlayback() {
+ oboe::Result result = oboe::Result::OK;
+ if (playbackStream != nullptr) {
+ result = playbackStream->requestStop();
+ playbackStream->close();
+ mPlayRecordingCallback.setRecording(nullptr);
+ delete playbackStream;
+ playbackStream = nullptr;
+ }
+ return result;
+}
+
+oboe::Result ActivityRecording::startPlayback() {
+ stop();
+ oboe::AudioStreamBuilder builder;
+ builder.setChannelCount(mChannelCount)
+ ->setSampleRate(mSampleRate)
+ ->setFormat(oboe::AudioFormat::Float)
+ ->setCallback(&mPlayRecordingCallback);
+ oboe::Result result = builder.openStream(&playbackStream);
+ if (result != oboe::Result::OK) {
+ delete playbackStream;
+ playbackStream = nullptr;
+ } else if (playbackStream != nullptr) {
+ if (mRecording != nullptr) {
+ mRecording->rewind();
+ mPlayRecordingCallback.setRecording(mRecording.get());
+ result = playbackStream->requestStart();
+ }
+ }
+ return result;
+}
+
+// ======================================================================= ActivityTapToTone
+void ActivityTapToTone::configureAfterOpen() {
+ monoToMulti = std::make_unique<MonoToMultiConverter>(mChannelCount);
+
+ mSinkFloat = std::make_shared<SinkFloat>(mChannelCount);
+ mSinkI16 = std::make_shared<SinkI16>(mChannelCount);
+ mSinkI24 = std::make_shared<SinkI24>(mChannelCount);
+ mSinkI32 = std::make_shared<SinkI32>(mChannelCount);
+
+ std::shared_ptr<oboe::AudioStream> outputStream = getOutputStream();
+ sawPingGenerator.setSampleRate(outputStream->getSampleRate());
+ sawPingGenerator.frequency.setValue(FREQUENCY_SAW_PING);
+ sawPingGenerator.amplitude.setValue(AMPLITUDE_SAW_PING);
+
+ sawPingGenerator.output.connect(&(monoToMulti->input));
+ monoToMulti->output.connect(&(mSinkFloat.get()->input));
+ monoToMulti->output.connect(&(mSinkI16.get()->input));
+ monoToMulti->output.connect(&(mSinkI24.get()->input));
+ monoToMulti->output.connect(&(mSinkI32.get()->input));
+
+ mSinkFloat->pullReset();
+ mSinkI16->pullReset();
+ mSinkI24->pullReset();
+ mSinkI32->pullReset();
+
+ configureStreamGateway();
+}
+
+// ======================================================================= ActivityFullDuplex
+void ActivityFullDuplex::configureBuilder(bool isInput, oboe::AudioStreamBuilder &builder) {
+ if (isInput) {
+ // Ideally the output streams should be opened first.
+ std::shared_ptr<oboe::AudioStream> outputStream = getOutputStream();
+ if (outputStream != nullptr) {
+ // The input and output buffers will run in sync with input empty
+ // and output full. So set the input capacity to match the output.
+ builder.setBufferCapacityInFrames(outputStream->getBufferCapacityInFrames());
+ }
+ }
+}
+
+// ======================================================================= ActivityEcho
+void ActivityEcho::configureBuilder(bool isInput, oboe::AudioStreamBuilder &builder) {
+ ActivityFullDuplex::configureBuilder(isInput, builder);
+
+ if (mFullDuplexEcho.get() == nullptr) {
+ mFullDuplexEcho = std::make_unique<FullDuplexEcho>();
+ }
+ // only output uses a callback, input is polled
+ if (!isInput) {
+ builder.setCallback((oboe::AudioStreamCallback *) &oboeCallbackProxy);
+ oboeCallbackProxy.setDataCallback(mFullDuplexEcho.get());
+ }
+}
+
+void ActivityEcho::finishOpen(bool isInput, std::shared_ptr<oboe::AudioStream> &oboeStream) {
+ if (isInput) {
+ mFullDuplexEcho->setSharedInputStream(oboeStream);
+ } else {
+ mFullDuplexEcho->setSharedOutputStream(oboeStream);
+ }
+}
+
+// ======================================================================= ActivityRoundTripLatency
+void ActivityRoundTripLatency::configureBuilder(bool isInput, oboe::AudioStreamBuilder &builder) {
+ ActivityFullDuplex::configureBuilder(isInput, builder);
+
+ if (mFullDuplexLatency.get() == nullptr) {
+ mFullDuplexLatency = std::make_unique<FullDuplexAnalyzer>(mLatencyAnalyzer.get());
+ }
+ if (!isInput) {
+ // only output uses a callback, input is polled
+ builder.setCallback((oboe::AudioStreamCallback *) &oboeCallbackProxy);
+ oboeCallbackProxy.setDataCallback(mFullDuplexLatency.get());
+ }
+}
+
+void ActivityRoundTripLatency::finishOpen(bool isInput, std::shared_ptr<oboe::AudioStream>
+ &oboeStream) {
+ if (isInput) {
+ mFullDuplexLatency->setSharedInputStream(oboeStream);
+ mFullDuplexLatency->setRecording(mRecording.get());
+ } else {
+ mFullDuplexLatency->setSharedOutputStream(oboeStream);
+ }
+}
+
+// The timestamp latency is the difference between the input
+// and output times for a specific frame.
+// Start with the position and time from an input timestamp.
+// Map the input position to the corresponding position in output
+// and calculate its time.
+// Use the difference between framesWritten and framesRead to
+// convert input positions to output positions.
+jdouble ActivityRoundTripLatency::measureTimestampLatency() {
+ if (!mFullDuplexLatency->isWriteReadDeltaValid()) return -1.0;
+
+ int64_t writeReadDelta = mFullDuplexLatency->getWriteReadDelta();
+ auto inputTimestampResult = mFullDuplexLatency->getInputStream()->getTimestamp(CLOCK_MONOTONIC);
+ if (!inputTimestampResult) return -1.0;
+ auto outputTimestampResult = mFullDuplexLatency->getOutputStream()->getTimestamp(CLOCK_MONOTONIC);
+ if (!outputTimestampResult) return -1.0;
+
+ int64_t inputPosition = inputTimestampResult.value().position;
+ int64_t inputTimeNanos = inputTimestampResult.value().timestamp;
+ int64_t ouputPosition = outputTimestampResult.value().position;
+ int64_t outputTimeNanos = outputTimestampResult.value().timestamp;
+
+ // Map input frame position to the corresponding output frame.
+ int64_t mappedPosition = inputPosition + writeReadDelta;
+ // Calculate when that frame will play.
+ int32_t sampleRate = mFullDuplexLatency->getOutputStream()->getSampleRate();
+ int64_t mappedTimeNanos = outputTimeNanos + ((mappedPosition - ouputPosition) * 1e9) / sampleRate;
+
+ // Latency is the difference in time between when a frame was recorded and
+ // when its corresponding echo was played.
+ return (mappedTimeNanos - inputTimeNanos) * 1.0e-6; // convert nanos to millis
+}
+
+// ======================================================================= ActivityGlitches
+void ActivityGlitches::configureBuilder(bool isInput, oboe::AudioStreamBuilder &builder) {
+ ActivityFullDuplex::configureBuilder(isInput, builder);
+
+ if (mFullDuplexGlitches.get() == nullptr) {
+ mFullDuplexGlitches = std::make_unique<FullDuplexAnalyzer>(&mGlitchAnalyzer);
+ }
+ if (!isInput) {
+ // only output uses a callback, input is polled
+ builder.setCallback((oboe::AudioStreamCallback *) &oboeCallbackProxy);
+ oboeCallbackProxy.setDataCallback(mFullDuplexGlitches.get());
+ }
+}
+
+void ActivityGlitches::finishOpen(bool isInput, std::shared_ptr<oboe::AudioStream> &oboeStream) {
+ if (isInput) {
+ mFullDuplexGlitches->setSharedInputStream(oboeStream);
+ mFullDuplexGlitches->setRecording(mRecording.get());
+ } else {
+ mFullDuplexGlitches->setSharedOutputStream(oboeStream);
+ }
+}
+
+// ======================================================================= ActivityDataPath
+void ActivityDataPath::configureBuilder(bool isInput, oboe::AudioStreamBuilder &builder) {
+ ActivityFullDuplex::configureBuilder(isInput, builder);
+
+ if (mFullDuplexDataPath.get() == nullptr) {
+ mFullDuplexDataPath = std::make_unique<FullDuplexAnalyzer>(&mDataPathAnalyzer);
+ }
+ if (!isInput) {
+ // only output uses a callback, input is polled
+ builder.setCallback((oboe::AudioStreamCallback *) &oboeCallbackProxy);
+ oboeCallbackProxy.setDataCallback(mFullDuplexDataPath.get());
+ }
+}
+
+void ActivityDataPath::finishOpen(bool isInput, std::shared_ptr<oboe::AudioStream> &oboeStream) {
+ if (isInput) {
+ mFullDuplexDataPath->setSharedInputStream(oboeStream);
+ mFullDuplexDataPath->setRecording(mRecording.get());
+ } else {
+ mFullDuplexDataPath->setSharedOutputStream(oboeStream);
+ }
+}
+
+// =================================================================== ActivityTestDisconnect
+void ActivityTestDisconnect::close(int32_t streamIndex) {
+ ActivityContext::close(streamIndex);
+ mSinkFloat.reset();
+}
+
+void ActivityTestDisconnect::configureAfterOpen() {
+ std::shared_ptr<oboe::AudioStream> outputStream = getOutputStream();
+ std::shared_ptr<oboe::AudioStream> inputStream = getInputStream();
+ if (outputStream) {
+ mSinkFloat = std::make_unique<SinkFloat>(mChannelCount);
+ sineOscillator = std::make_unique<SineOscillator>();
+ monoToMulti = std::make_unique<MonoToMultiConverter>(mChannelCount);
+
+ sineOscillator->setSampleRate(outputStream->getSampleRate());
+ sineOscillator->frequency.setValue(440.0);
+ sineOscillator->amplitude.setValue(AMPLITUDE_SINE);
+ sineOscillator->output.connect(&(monoToMulti->input));
+
+ monoToMulti->output.connect(&(mSinkFloat->input));
+ mSinkFloat->pullReset();
+ audioStreamGateway.setAudioSink(mSinkFloat);
+ } else if (inputStream) {
+ audioStreamGateway.setAudioSink(nullptr);
+ }
+ oboeCallbackProxy.setDataCallback(&audioStreamGateway);
+}
+