diff options
author | Haibo Huang <hhb@google.com> | 2020-09-10 22:59:40 +0000 |
---|---|---|
committer | Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com> | 2020-09-10 22:59:40 +0000 |
commit | c9056fde849c9deb1d647fa92a72da66a4a8442f (patch) | |
tree | 8386d0a708feb788939d45dab1083babca1574c0 | |
parent | 32f16966e88170df96c65d970c3e564b93e6aed1 (diff) | |
parent | 3128bb814235c96f19c41ec7299b522c75827bb7 (diff) | |
download | oboe-c9056fde849c9deb1d647fa92a72da66a4a8442f.tar.gz |
Upgrade oboe to 386f057966b9640a25b63e7ac552d30d66b4e49d am: e7b0cde384 am: e98ef114b1 am: c60bdcc1a2 am: 3128bb8142
Original change: https://android-review.googlesource.com/c/platform/external/oboe/+/1424149
Change-Id: I8e05f1059de15fa33645ab5007ab78453a990313
46 files changed, 834 insertions, 291 deletions
@@ -1,12 +1,14 @@ name: "Oboe" -description: - "Native audio API for Android that calls AAudio or OpenSL ES." - +description: "Native audio API for Android that calls AAudio or OpenSL ES." third_party { url { type: GIT value: "https://github.com/google/oboe" } - version: "c4d7e73c8fcf5ab2edc85ee48330a43edd5f52fc" - last_upgrade_date { year: 2020 month: 2 day: 6 } + version: "386f057966b9640a25b63e7ac552d30d66b4e49d" + last_upgrade_date { + year: 2020 + month: 9 + day: 9 + } } diff --git a/MODULE_LICENSE_APACHE2 b/MODULE_LICENSE_APACHE2 new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/MODULE_LICENSE_APACHE2 @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. diff --git a/apps/OboeTester/app/CMakeLists.txt b/apps/OboeTester/app/CMakeLists.txt index f2c81c15..8de21f9b 100644 --- a/apps/OboeTester/app/CMakeLists.txt +++ b/apps/OboeTester/app/CMakeLists.txt @@ -31,4 +31,4 @@ include_directories( # link to oboe target_link_libraries(oboetester log oboe atomic) -# bump 2 to resync CMake +# bump 3 to resync CMake diff --git a/apps/OboeTester/app/build.gradle b/apps/OboeTester/app/build.gradle index 51911fe3..e3aea743 100644 --- a/apps/OboeTester/app/build.gradle +++ b/apps/OboeTester/app/build.gradle @@ -7,8 +7,8 @@ android { minSdkVersion 23 targetSdkVersion 28 // Also update the versions in the AndroidManifest.xml file. - versionCode 33 - versionName "1.5.25" + versionCode 35 + versionName "1.5.27" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" externalNativeBuild { cmake { diff --git a/apps/OboeTester/app/src/main/AndroidManifest.xml b/apps/OboeTester/app/src/main/AndroidManifest.xml index 9bbc5a64..37a75b9a 100644 --- a/apps/OboeTester/app/src/main/AndroidManifest.xml +++ b/apps/OboeTester/app/src/main/AndroidManifest.xml @@ -1,8 +1,8 @@ <?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.google.sample.oboe.manualtest" - android:versionCode="33" - android:versionName="1.5.25"> + android:versionCode="35" + android:versionName="1.5.27"> <!-- versionCode and versionName also have to be updated in build.gradle --> <uses-feature android:name="android.hardware.microphone" android:required="true" /> diff --git a/apps/OboeTester/app/src/main/cpp/AudioStreamGateway.cpp b/apps/OboeTester/app/src/main/cpp/AudioStreamGateway.cpp index 7dd374f8..c4735405 100644 --- a/apps/OboeTester/app/src/main/cpp/AudioStreamGateway.cpp +++ b/apps/OboeTester/app/src/main/cpp/AudioStreamGateway.cpp @@ -17,6 +17,7 @@ #include <cstring> #include <sched.h> +#include "common/OboeDebug.h" #include "oboe/Oboe.h" #include "AudioStreamGateway.h" @@ -27,10 +28,7 @@ oboe::DataCallbackResult AudioStreamGateway::onAudioReady( void *audioData, int numFrames) { - if (!mSchedulerChecked) { - mScheduler = sched_getscheduler(gettid()); - mSchedulerChecked = true; - } + printScheduler(); if (mAudioSink != nullptr) { mAudioSink->read(audioData, numFrames); @@ -39,6 +37,3 @@ oboe::DataCallbackResult AudioStreamGateway::onAudioReady( return oboe::DataCallbackResult::Continue; } -int AudioStreamGateway::getScheduler() { - return mScheduler; -} diff --git a/apps/OboeTester/app/src/main/cpp/AudioStreamGateway.h b/apps/OboeTester/app/src/main/cpp/AudioStreamGateway.h index 982d0992..0aaf429a 100644 --- a/apps/OboeTester/app/src/main/cpp/AudioStreamGateway.h +++ b/apps/OboeTester/app/src/main/cpp/AudioStreamGateway.h @@ -21,6 +21,7 @@ #include "flowgraph/FlowGraphNode.h" #include "oboe/Oboe.h" +#include "OboeTesterStreamCallback.h" using namespace oboe::flowgraph; @@ -29,9 +30,8 @@ using namespace oboe::flowgraph; * Pass in an AudioSink and then pass * this object to the AudioStreamBuilder as a callback. */ -class AudioStreamGateway : public oboe::AudioStreamCallback { +class AudioStreamGateway : public OboeTesterStreamCallback { public: -// AudioStreamGateway(int samplesPerFrame); virtual ~AudioStreamGateway() = default; void setAudioSink(std::shared_ptr<oboe::flowgraph::FlowGraphSink> sink) { @@ -46,11 +46,8 @@ public: void *audioData, int numFrames) override; - int getScheduler(); - private: - bool mSchedulerChecked = false; - int mScheduler; + std::shared_ptr<oboe::flowgraph::FlowGraphSink> mAudioSink; }; diff --git a/apps/OboeTester/app/src/main/cpp/FullDuplexAnalyzer.cpp b/apps/OboeTester/app/src/main/cpp/FullDuplexAnalyzer.cpp index c8a99acf..2da2dfd4 100644 --- a/apps/OboeTester/app/src/main/cpp/FullDuplexAnalyzer.cpp +++ b/apps/OboeTester/app/src/main/cpp/FullDuplexAnalyzer.cpp @@ -19,7 +19,7 @@ oboe::Result FullDuplexAnalyzer::start() { getLoopbackProcessor()->setSampleRate(getOutputStream()->getSampleRate()); - getLoopbackProcessor()->onStartTest(); + getLoopbackProcessor()->prepareToTest(); return FullDuplexStream::start(); } diff --git a/apps/OboeTester/app/src/main/cpp/InputStreamCallbackAnalyzer.cpp b/apps/OboeTester/app/src/main/cpp/InputStreamCallbackAnalyzer.cpp index d04f09af..f9290e12 100644 --- a/apps/OboeTester/app/src/main/cpp/InputStreamCallbackAnalyzer.cpp +++ b/apps/OboeTester/app/src/main/cpp/InputStreamCallbackAnalyzer.cpp @@ -23,6 +23,8 @@ oboe::DataCallbackResult InputStreamCallbackAnalyzer::onAudioReady( int numFrames) { int32_t channelCount = audioStream->getChannelCount(); + printScheduler(); + if (audioStream->getFormat() == oboe::AudioFormat::I16) { int16_t *shortData = (int16_t *) audioData; if (mRecording != nullptr) { diff --git a/apps/OboeTester/app/src/main/cpp/InputStreamCallbackAnalyzer.h b/apps/OboeTester/app/src/main/cpp/InputStreamCallbackAnalyzer.h index 42ffbfbf..fc26b1f8 100644 --- a/apps/OboeTester/app/src/main/cpp/InputStreamCallbackAnalyzer.h +++ b/apps/OboeTester/app/src/main/cpp/InputStreamCallbackAnalyzer.h @@ -24,17 +24,19 @@ // TODO #include "flowgraph/FlowGraph.h" #include "oboe/Oboe.h" #include "MultiChannelRecording.h" +#include "OboeTesterStreamCallback.h" #include "analyzer/PeakDetector.h" constexpr int kMaxInputChannels = 8; -class InputStreamCallbackAnalyzer : public oboe::AudioStreamCallback { +class InputStreamCallbackAnalyzer : public OboeTesterStreamCallback { public: void reset() { for (auto detector : mPeakDetectors) { detector.reset(); } + OboeTesterStreamCallback::reset(); } /** diff --git a/apps/OboeTester/app/src/main/cpp/NativeAudioContext.cpp b/apps/OboeTester/app/src/main/cpp/NativeAudioContext.cpp index 00266107..3b51177b 100644 --- a/apps/OboeTester/app/src/main/cpp/NativeAudioContext.cpp +++ b/apps/OboeTester/app/src/main/cpp/NativeAudioContext.cpp @@ -111,7 +111,6 @@ oboe::Result ActivityContext::pause() { for (auto entry : mOboeStreams) { oboe::AudioStream *oboeStream = entry.second.get(); result = oboeStream->requestPause(); - printScheduler(); } return result; } @@ -122,7 +121,6 @@ oboe::Result ActivityContext::stopAllStreams() { for (auto entry : mOboeStreams) { oboe::AudioStream *oboeStream = entry.second.get(); result = oboeStream->requestStop(); - printScheduler(); } return result; } @@ -237,6 +235,7 @@ oboe::Result ActivityContext::start() { configureForStart(); + audioStreamGateway.reset(); result = startStreams(); if (!mUseCallback && result == oboe::Result::OK) { diff --git a/apps/OboeTester/app/src/main/cpp/NativeAudioContext.h b/apps/OboeTester/app/src/main/cpp/NativeAudioContext.h index d5cb994c..bd445d1d 100644 --- a/apps/OboeTester/app/src/main/cpp/NativeAudioContext.h +++ b/apps/OboeTester/app/src/main/cpp/NativeAudioContext.h @@ -224,13 +224,6 @@ public: virtual void close(int32_t streamIndex); - void printScheduler() { -#if OBOE_ENABLE_LOGGING - int scheduler = audioStreamGateway.getScheduler(); -#endif - LOGI("scheduler = 0x%08x, SCHED_FIFO = 0x%08X\n", scheduler, SCHED_FIFO); - } - virtual void configureForStart() {} oboe::Result start(); diff --git a/apps/OboeTester/app/src/main/cpp/OboeTesterStreamCallback.cpp b/apps/OboeTester/app/src/main/cpp/OboeTesterStreamCallback.cpp new file mode 100644 index 00000000..aab60abe --- /dev/null +++ b/apps/OboeTester/app/src/main/cpp/OboeTesterStreamCallback.cpp @@ -0,0 +1,40 @@ +/* + * Copyright 2020 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 "AudioStreamGateway.h" +#include "oboe/Oboe.h" +#include "common/OboeDebug.h" +#include <sched.h> +#include <cstring> +#include "OboeTesterStreamCallback.h" + +// Print if scheduler changes. +void OboeTesterStreamCallback::printScheduler() { +#if OBOE_ENABLE_LOGGING + int scheduler = sched_getscheduler(gettid()); + if (scheduler != mPreviousScheduler) { + int schedulerType = scheduler & 0xFFFF; // mask off high flags + LOGD("callback CPU scheduler = 0x%08x = %s", + scheduler, + ((schedulerType == SCHED_FIFO) ? "SCHED_FIFO" : + ((schedulerType == SCHED_OTHER) ? "SCHED_OTHER" : + ((schedulerType == SCHED_RR) ? "SCHED_RR" : "UNKNOWN"))) + ); + mPreviousScheduler = scheduler; + } +#endif +}
\ No newline at end of file diff --git a/apps/OboeTester/app/src/main/cpp/OboeTesterStreamCallback.h b/apps/OboeTester/app/src/main/cpp/OboeTesterStreamCallback.h new file mode 100644 index 00000000..ec01fe5c --- /dev/null +++ b/apps/OboeTester/app/src/main/cpp/OboeTesterStreamCallback.h @@ -0,0 +1,41 @@ +/* + * Copyright 2020 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_STREAM_CALLBACK_H +#define OBOETESTER_STREAM_CALLBACK_H + +#include <unistd.h> +#include <sys/types.h> +#include "flowgraph/FlowGraphNode.h" +#include "oboe/Oboe.h" + +class OboeTesterStreamCallback : public oboe::AudioStreamCallback { +public: + virtual ~OboeTesterStreamCallback() = default; + + // Call this before starting. + void reset() { + mPreviousScheduler = -1; + } + +protected: + void printScheduler(); + + int mPreviousScheduler = -1; +}; + + +#endif //OBOETESTER_STREAM_CALLBACK_H diff --git a/apps/OboeTester/app/src/main/cpp/analyzer/GlitchAnalyzer.h b/apps/OboeTester/app/src/main/cpp/analyzer/GlitchAnalyzer.h index 7a496f3b..4d1bd6f0 100644 --- a/apps/OboeTester/app/src/main/cpp/analyzer/GlitchAnalyzer.h +++ b/apps/OboeTester/app/src/main/cpp/analyzer/GlitchAnalyzer.h @@ -14,17 +14,20 @@ * limitations under the License. */ -#ifndef OBOETESTER_GLITCHANALYZER_H -#define OBOETESTER_GLITCHANALYZER_H +#ifndef ANALYZER_GLITCH_ANALYZER_H +#define ANALYZER_GLITCH_ANALYZER_H +#include <algorithm> #include <cctype> +#include <iomanip> +#include <iostream> -#include "PseudoRandom.h" -#include "LatencyAnalyzer.h" #include "InfiniteRecording.h" +#include "LatencyAnalyzer.h" +#include "PseudoRandom.h" /** - * Output a steady sinewave and analyze the return signal. + * Output a steady sine wave and analyze the return signal. * * Use a cosine transform to measure the predicted magnitude and relative phase of the * looped back sine wave. Then generate a predicted signal and compare with the actual signal. @@ -36,19 +39,19 @@ public: : LoopbackProcessor() , mInfiniteRecording(64 * 1024) {} - int32_t getState() { + int32_t getState() const { return mState; } - float getPeakAmplitude() { + double getPeakAmplitude() const { return mPeakFollower.getLevel(); } - float getTolerance() { + double getTolerance() { return mTolerance; } - void setTolerance(float tolerance) { + void setTolerance(double tolerance) { mTolerance = tolerance; mScaledTolerance = mMagnitude * mTolerance; } @@ -58,11 +61,11 @@ public: mScaledTolerance = mMagnitude * mTolerance; } - int32_t getGlitchCount() { + int32_t getGlitchCount() const { return mGlitchCount; } - int32_t getStateFrameCount(int state) { + int32_t getStateFrameCount(int state) const { return mStateFrameCounters[state]; } @@ -73,55 +76,72 @@ public: } else { double signalToNoise = mMeanSquareSignal / mMeanSquareNoise; // power ratio double signalToNoiseDB = 10.0 * log(signalToNoise); - if (signalToNoiseDB < MIN_SNRATIO_DB) { - LOGD("ERROR - signal to noise ratio is too low! < %d dB. Adjust volume.", - MIN_SNRATIO_DB); + if (signalToNoiseDB < MIN_SNR_DB) { + ALOGD("ERROR - signal to noise ratio is too low! < %d dB. Adjust volume.", + MIN_SNR_DB); setResult(ERROR_VOLUME_TOO_LOW); } return signalToNoiseDB; } } - void analyze() override { - LOGD("GlitchAnalyzer ------------------"); - LOGD(LOOPBACK_RESULT_TAG "peak.amplitude = %8f", getPeakAmplitude()); - LOGD(LOOPBACK_RESULT_TAG "sine.magnitude = %8f", mMagnitude); - LOGD(LOOPBACK_RESULT_TAG "rms.noise = %8f", mMeanSquareNoise); - LOGD(LOOPBACK_RESULT_TAG "signal.to.noise.db = %8.2f", getSignalToNoiseDB()); - LOGD(LOOPBACK_RESULT_TAG "frames.accumulated = %8d", mFramesAccumulated); - LOGD(LOOPBACK_RESULT_TAG "sine.period = %8d", mSinePeriod); - LOGD(LOOPBACK_RESULT_TAG "test.state = %8d", mState); - LOGD(LOOPBACK_RESULT_TAG "frame.count = %8d", mFrameCounter); + std::string analyze() override { + std::stringstream report; + report << "GlitchAnalyzer ------------------\n"; + report << LOOPBACK_RESULT_TAG "peak.amplitude = " << std::setw(8) + << getPeakAmplitude() << "\n"; + report << LOOPBACK_RESULT_TAG "sine.magnitude = " << std::setw(8) + << mMagnitude << "\n"; + report << LOOPBACK_RESULT_TAG "rms.noise = " << std::setw(8) + << mMeanSquareNoise << "\n"; + report << LOOPBACK_RESULT_TAG "signal.to.noise.db = " << std::setw(8) + << getSignalToNoiseDB() << "\n"; + report << LOOPBACK_RESULT_TAG "frames.accumulated = " << std::setw(8) + << mFramesAccumulated << "\n"; + report << LOOPBACK_RESULT_TAG "sine.period = " << std::setw(8) + << mSinePeriod << "\n"; + report << LOOPBACK_RESULT_TAG "test.state = " << std::setw(8) + << mState << "\n"; + report << LOOPBACK_RESULT_TAG "frame.count = " << std::setw(8) + << mFrameCounter << "\n"; // Did we ever get a lock? bool gotLock = (mState == STATE_LOCKED) || (mGlitchCount > 0); if (!gotLock) { - LOGD("ERROR - failed to lock on reference sine tone"); + report << "ERROR - failed to lock on reference sine tone.\n"; setResult(ERROR_NO_LOCK); } else { // Only print if meaningful. - LOGD(LOOPBACK_RESULT_TAG "glitch.count = %8d", mGlitchCount); - LOGD(LOOPBACK_RESULT_TAG "max.glitch = %8f", mMaxGlitchDelta); + report << LOOPBACK_RESULT_TAG "glitch.count = " << std::setw(8) + << mGlitchCount << "\n"; + report << LOOPBACK_RESULT_TAG "max.glitch = " << std::setw(8) + << mMaxGlitchDelta << "\n"; if (mGlitchCount > 0) { - LOGD("ERROR - number of glitches > 0"); + report << "ERROR - number of glitches > 0\n"; setResult(ERROR_GLITCHES); } } + return report.str(); } void printStatus() override { - LOGD("st = %d, #gl = %3d,", mState, mGlitchCount); + ALOGD("st = %d, #gl = %3d,", mState, mGlitchCount); } - - double calculateMagnitude(double *phasePtr = NULL) { + /** + * Calculate the magnitude of the component of the input signal + * that matches the analysis frequency. + * Also calculate the phase that we can use to create a + * signal that matches that component. + * The phase will be between -PI and +PI. + */ + double calculateMagnitude(double *phasePtr = nullptr) { if (mFramesAccumulated == 0) { return 0.0; } double sinMean = mSinAccumulator / mFramesAccumulated; double cosMean = mCosAccumulator / mFramesAccumulated; - double magnitude = 2.0 * sqrt( (sinMean * sinMean) + (cosMean * cosMean )); - if( phasePtr != NULL ) - { - double phase = M_PI_2 - atan2( sinMean, cosMean ); + double magnitude = 2.0 * sqrt((sinMean * sinMean) + (cosMean * cosMean)); + if (phasePtr != nullptr) { + double phase = M_PI_2 - atan2(sinMean, cosMean); *phasePtr = phase; } return magnitude; @@ -131,19 +151,20 @@ public: * @param frameData contains microphone data with sine signal feedback * @param channelCount */ - result_code processInputFrame(float *frameData, int channelCount) override { + result_code processInputFrame(float *frameData, int /* channelCount */) override { result_code result = RESULT_OK; float sample = frameData[0]; float peak = mPeakFollower.process(sample); mInfiniteRecording.write(sample); - // Force a periodic glitch! + // Force a periodic glitch to test the detector! if (mForceGlitchDuration > 0) { if (mForceGlitchCounter == 0) { - LOGE("%s: force a glitch!!", __func__); + ALOGE("%s: force a glitch!!", __func__); mForceGlitchCounter = getSampleRate(); } else if (mForceGlitchCounter <= mForceGlitchDuration) { + // Force an abrupt offset. sample += (sample > 0.0) ? -0.5f : 0.5f; } --mForceGlitchCounter; @@ -172,7 +193,7 @@ public: case STATE_WAITING_FOR_SIGNAL: if (peak > mThreshold) { mState = STATE_WAITING_FOR_LOCK; - //LOGD("%5d: switch to STATE_WAITING_FOR_LOCK", mFrameCounter); + //ALOGD("%5d: switch to STATE_WAITING_FOR_LOCK", mFrameCounter); resetAccumulator(); } break; @@ -185,12 +206,12 @@ public: if (mFramesAccumulated == mSinePeriod * PERIODS_NEEDED_FOR_LOCK) { double phaseOffset = 0.0; setMagnitude(calculateMagnitude(&phaseOffset)); -// LOGD("%s() mag = %f, offset = %f, prev = %f", +// ALOGD("%s() mag = %f, offset = %f, prev = %f", // __func__, mMagnitude, mPhaseOffset, mPreviousPhaseOffset); if (mMagnitude > mThreshold) { if (abs(phaseOffset) < kMaxPhaseError) { mState = STATE_LOCKED; -// LOGD("%5d: switch to STATE_LOCKED", mFrameCounter); +// ALOGD("%5d: switch to STATE_LOCKED", mFrameCounter); } // Adjust mInputPhase to match measured phase mInputPhase += phaseOffset; @@ -202,9 +223,9 @@ public: case STATE_LOCKED: { // Predict next sine value - float predicted = sinf(mInputPhase) * mMagnitude; - float diff = predicted - sample; - float absDiff = fabs(diff); + double predicted = sinf(mInputPhase) * mMagnitude; + double diff = predicted - sample; + double absDiff = fabs(diff); mMaxGlitchDelta = std::max(mMaxGlitchDelta, absDiff); if (absDiff > mScaledTolerance) { result = ERROR_GLITCHES; @@ -233,11 +254,11 @@ public: if (abs(phaseOffset) > kMaxPhaseError) { result = ERROR_GLITCHES; onGlitchStart(); - LOGD("phase glitch detected, phaseOffset = %g", phaseOffset); + ALOGD("phase glitch detected, phaseOffset = %g", phaseOffset); } else if (mMagnitude < mThreshold) { result = ERROR_GLITCHES; onGlitchStart(); - LOGD("magnitude glitch detected, mMagnitude = %g", mMagnitude); + ALOGD("magnitude glitch detected, mMagnitude = %g", mMagnitude); } } } @@ -247,9 +268,9 @@ public: case STATE_GLITCHING: { // Predict next sine value mGlitchLength++; - float predicted = sinf(mInputPhase) * mMagnitude; - float diff = predicted - sample; - float absDiff = fabs(diff); + double predicted = sinf(mInputPhase) * mMagnitude; + double diff = predicted - sample; + double absDiff = fabs(diff); mMaxGlitchDelta = std::max(mMaxGlitchDelta, absDiff); if (absDiff < mScaledTolerance) { // close enough? // If we get a full sine period of non-glitch samples in a row then consider the glitch over. @@ -303,7 +324,7 @@ public: incrementOutputPhase(); output = (sinOut * mOutputAmplitude) + (mWhiteNoise.nextRandomDouble() * kNoiseAmplitude); - // LOGD("%5d: sin(%f) = %f, %f", i, mPhase, sinOut, mPhaseIncrement); + // ALOGD("sin(%f) = %f, %f\n", mOutputPhase, sinOut, mPhaseIncrement); } frameData[0] = output; for (int i = 1; i < channelCount; i++) { @@ -314,7 +335,7 @@ public: void onGlitchStart() { mGlitchCount++; -// LOGD("%5d: STARTED a glitch # %d", mFrameCounter, mGlitchCount); +// ALOGD("%5d: STARTED a glitch # %d", mFrameCounter, mGlitchCount); mState = STATE_GLITCHING; mGlitchLength = 1; mNonGlitchCount = 0; @@ -322,7 +343,7 @@ public: } void onGlitchEnd() { -// LOGD("%5d: ENDED a glitch # %d, length = %d", mFrameCounter, mGlitchCount, mGlitchLength); +// ALOGD("%5d: ENDED a glitch # %d, length = %d", mFrameCounter, mGlitchCount, mGlitchLength); mState = STATE_LOCKED; resetAccumulator(); } @@ -337,7 +358,7 @@ public: } void relock() { -// LOGD("relock: %d because of a very long %d glitch", mFrameCounter, mGlitchLength); +// ALOGD("relock: %d because of a very long %d glitch", mFrameCounter, mGlitchLength); mState = STATE_WAITING_FOR_LOCK; resetAccumulator(); } @@ -349,8 +370,8 @@ public: resetAccumulator(); } - void onStartTest() override { - LoopbackProcessor::onStartTest(); + void prepareToTest() override { + LoopbackProcessor::prepareToTest(); mSinePeriod = getSampleRate() / kTargetGlitchFrequency; mOutputPhase = 0.0f; mInverseSinePeriod = 1.0 / mSinePeriod; @@ -362,7 +383,6 @@ public: } } - int32_t getLastGlitch(float *buffer, int32_t length) { return mInfiniteRecording.readFrom(buffer, mLastGlitchPosition - 32, length); } @@ -382,10 +402,10 @@ private: enum constants { // Arbitrary durations, assuming 48000 Hz - IDLE_FRAME_COUNT = 48 * 100, + IDLE_FRAME_COUNT = 48 * 100, IMMUNE_FRAME_COUNT = 48 * 100, PERIODS_NEEDED_FOR_LOCK = 8, - MIN_SNRATIO_DB = 65 + MIN_SNR_DB = 65 }; static constexpr float kNoiseAmplitude = 0.00; // Used to experiment with warbling caused by DRC. @@ -406,14 +426,15 @@ private: int32_t mFramesAccumulated = 0; double mSinAccumulator = 0.0; double mCosAccumulator = 0.0; - float mMaxGlitchDelta = 0.0f; + double mMaxGlitchDelta = 0.0; int32_t mGlitchCount = 0; int32_t mNonGlitchCount = 0; int32_t mGlitchLength = 0; - float mScaledTolerance = 0.0; + // This is used for processing every frame so we cache it here. + double mScaledTolerance = 0.0; int mDownCounter = IDLE_FRAME_COUNT; int32_t mFrameCounter = 0; - float mOutputAmplitude = 0.75; + double mOutputAmplitude = 0.75; int32_t mForceGlitchDuration = 0; // if > 0 then force a glitch for debugging int32_t mForceGlitchCounter = 4 * 48000; // count down and trigger at zero @@ -435,4 +456,4 @@ private: }; -#endif //OBOETESTER_GLITCHANALYZER_H +#endif //ANALYZER_GLITCH_ANALYZER_H diff --git a/apps/OboeTester/app/src/main/cpp/analyzer/LatencyAnalyzer.h b/apps/OboeTester/app/src/main/cpp/analyzer/LatencyAnalyzer.h index d0d1aa81..3178c6e0 100644 --- a/apps/OboeTester/app/src/main/cpp/analyzer/LatencyAnalyzer.h +++ b/apps/OboeTester/app/src/main/cpp/analyzer/LatencyAnalyzer.h @@ -25,16 +25,26 @@ #include <algorithm> #include <assert.h> #include <cctype> +#include <iomanip> +#include <iostream> #include <math.h> #include <memory> +#include <sstream> #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <vector> -#include "RandomPulseGenerator.h" #include "PeakDetector.h" #include "PseudoRandom.h" +#include "RandomPulseGenerator.h" + +// This is used when the code is in Oboe. +#ifndef ALOGD +#define ALOGD LOGD +#define ALOGE LOGE +#define ALOGW LOGW +#endif #define LOOPBACK_RESULT_TAG "RESULT: " @@ -43,7 +53,7 @@ static constexpr int32_t kMillisPerSecond = 1000; static constexpr int32_t kMaxLatencyMillis = 700; // arbitrary and generous static constexpr double kMinimumConfidence = 0.2; -typedef struct LatencyReport_s { +struct LatencyReport { int32_t latencyInFrames = 0.0; double confidence = 0.0; @@ -51,13 +61,12 @@ typedef struct LatencyReport_s { latencyInFrames = 0; confidence = 0.0; } -} LatencyReport; +}; // Calculate a normalized cross correlation. static double calculateNormalizedCorrelation(const float *a, const float *b, - int windowSize) -{ + int windowSize) { double correlation = 0.0; double sumProducts = 0.0; double sumSquares = 0.0; @@ -72,7 +81,7 @@ static double calculateNormalizedCorrelation(const float *a, } if (sumSquares >= 1.0e-9) { - correlation = (float) (2.0 * sumProducts / sumSquares); + correlation = 2.0 * sumProducts / sumSquares; } return correlation; } @@ -92,15 +101,9 @@ static double calculateRootMeanSquare(float *data, int32_t numSamples) { class AudioRecording { public: - AudioRecording() { - } - ~AudioRecording() { - delete[] mData; - } void allocate(int maxFrames) { - delete[] mData; - mData = new float[maxFrames]; + mData = std::make_unique<float[]>(maxFrames); mMaxFrames = maxFrames; } @@ -133,30 +136,31 @@ public: // stop at end of buffer if (mFrameCounter < mMaxFrames) { mData[mFrameCounter++] = sample; + return 1; } - return 1; + return 0; } void clear() { mFrameCounter = 0; } - int32_t size() { + int32_t size() const { return mFrameCounter; } - bool isFull() { + bool isFull() const { return mFrameCounter >= mMaxFrames; } - float *getData() { - return mData; + float *getData() const { + return mData.get(); } void setSampleRate(int32_t sampleRate) { mSampleRate = sampleRate; } - int32_t getSampleRate() { + int32_t getSampleRate() const { return mSampleRate; } @@ -164,9 +168,9 @@ public: * Square the samples so they are all positive and so the peaks are emphasized. */ void square() { + float *x = mData.get(); for (int i = 0; i < mFrameCounter; i++) { - const float sample = mData[i]; - mData[i] = sample * sample; + x[i] *= x[i]; } } @@ -189,7 +193,7 @@ public: } private: - float *mData = nullptr; + std::unique_ptr<float[]> mData; int32_t mFrameCounter = 0; int32_t mMaxFrames = 0; int32_t mSampleRate = kDefaultSampleRate; // common default @@ -197,7 +201,6 @@ private: static int measureLatencyFromPulse(AudioRecording &recorded, AudioRecording &pulse, - int32_t framesPerEncodedBit, LatencyReport *report) { report->latencyInFrames = 0; @@ -205,7 +208,7 @@ static int measureLatencyFromPulse(AudioRecording &recorded, int numCorrelations = recorded.size() - pulse.size(); if (numCorrelations < 10) { - LOGE("%s() recording too small = %d frames", __func__, recorded.size()); + ALOGE("%s() recording too small = %d frames\n", __func__, recorded.size()); return -1; } std::unique_ptr<float[]> correlations= std::make_unique<float[]>(numCorrelations); @@ -229,9 +232,20 @@ static int measureLatencyFromPulse(AudioRecording &recorded, } } if (peakIndex < 0) { - LOGE("%s() no signal for correlation", __func__); + ALOGE("%s() no signal for correlation\n", __func__); return -2; } +#if 0 + // Dump correlation data for charting. + else { + const int margin = 50; + int startIndex = std::max(0, peakIndex - margin); + int endIndex = std::min(numCorrelations - 1, peakIndex + margin); + for (int index = startIndex; index < endIndex; index++) { + ALOGD("Correlation, %d, %f", index, correlations[index]); + } + } +#endif report->latencyInFrames = peakIndex; report->confidence = peakCorrelation; @@ -244,7 +258,6 @@ class LoopbackProcessor { public: virtual ~LoopbackProcessor() = default; - // Note that these values must match the switch in RoundTripLatencyActivity.h enum result_code { RESULT_OK = 0, ERROR_NOISY = -99, @@ -256,7 +269,7 @@ public: ERROR_NO_LOCK }; - virtual void onStartTest() { + virtual void prepareToTest() { reset(); } @@ -269,7 +282,7 @@ public: virtual result_code processOutputFrame(float *frameData, int channelCount) = 0; void process(float *inputData, int inputChannelCount, int numInputFrames, - float *outputData, int outputChannelCount, int numOutputFrames) { + float *outputData, int outputChannelCount, int numOutputFrames) { int numBoth = std::min(numInputFrames, numOutputFrames); // Process one frame at a time. for (int i = 0; i < numBoth; i++) { @@ -290,7 +303,7 @@ public: } } - virtual void analyze() = 0; + virtual std::string analyze() = 0; virtual void printStatus() {}; @@ -320,11 +333,11 @@ public: mSampleRate = sampleRate; } - int32_t getSampleRate() { + int32_t getSampleRate() const { return mSampleRate; } - int32_t getResetCount() { + int32_t getResetCount() const { return mResetCount; } @@ -348,7 +361,7 @@ public: LatencyAnalyzer() : LoopbackProcessor() {} virtual ~LatencyAnalyzer() = default; - virtual int32_t getProgress() = 0; + virtual int32_t getProgress() const = 0; virtual int getState() = 0; @@ -380,7 +393,6 @@ public: / (kFramesPerEncodedBit * kMillisPerSecond); int32_t pulseLength = numPulseBits * kFramesPerEncodedBit; mFramesToRecord = pulseLength + maxLatencyFrames; - LOGD("PulseLatencyAnalyzer: allocate recording with %d frames", mFramesToRecord); mAudioRecording.allocate(mFramesToRecord); mAudioRecording.setSampleRate(getSampleRate()); generateRandomPulse(pulseLength); @@ -405,7 +417,8 @@ public: void reset() override { LoopbackProcessor::reset(); - mDownCounter = getSampleRate() / 2; + mState = STATE_MEASURE_BACKGROUND; + mDownCounter = (int32_t) (getSampleRate() * kBackgroundMeasurementLengthSeconds); mLoopCounter = 0; mPulseCursor = 0; @@ -414,8 +427,6 @@ public: mBackgroundRMS = 0.0f; mSignalRMS = 0.0f; - LOGD("state reset to STATE_MEASURE_BACKGROUND"); - mState = STATE_MEASURE_BACKGROUND; mAudioRecording.clear(); mLatencyReport.reset(); } @@ -428,51 +439,53 @@ public: return mState == STATE_DONE; } - int32_t getProgress() override { + int32_t getProgress() const override { return mAudioRecording.size(); } - void analyze() override { - LOGD("PulseLatencyAnalyzer ---------------"); - LOGD(LOOPBACK_RESULT_TAG "test.state = %8d", mState); - LOGD(LOOPBACK_RESULT_TAG "test.state.name = %8s", convertStateToText(mState)); - LOGD(LOOPBACK_RESULT_TAG "background.rms = %8f", mBackgroundRMS); + std::string analyze() override { + std::stringstream report; + report << "PulseLatencyAnalyzer ---------------\n"; + report << LOOPBACK_RESULT_TAG "test.state = " + << std::setw(8) << mState << "\n"; + report << LOOPBACK_RESULT_TAG "test.state.name = " + << convertStateToText(mState) << "\n"; + report << LOOPBACK_RESULT_TAG "background.rms = " + << std::setw(8) << mBackgroundRMS << "\n"; int32_t newResult = RESULT_OK; if (mState != STATE_GOT_DATA) { - LOGD("WARNING - Bad state. Check volume on device."); + report << "WARNING - Bad state. Check volume on device.\n"; // setResult(ERROR_INVALID_STATE); } else { - LOGD("Please wait several seconds for cross-correlation to complete."); float gain = mAudioRecording.normalize(1.0f); measureLatencyFromPulse(mAudioRecording, mPulse, - kFramesPerEncodedBit, &mLatencyReport); if (mLatencyReport.confidence < kMinimumConfidence) { - LOGD(" ERROR - confidence too low!"); + report << " ERROR - confidence too low!"; newResult = ERROR_CONFIDENCE; } else { mSignalRMS = calculateRootMeanSquare( &mAudioRecording.getData()[mLatencyReport.latencyInFrames], mPulse.size()) / gain; } -#if OBOE_ENABLE_LOGGING double latencyMillis = kMillisPerSecond * (double) mLatencyReport.latencyInFrames / getSampleRate(); -#endif - LOGD(LOOPBACK_RESULT_TAG "latency.frames = %8d", - mLatencyReport.latencyInFrames); - LOGD(LOOPBACK_RESULT_TAG "latency.msec = %8.2f", - latencyMillis); - LOGD(LOOPBACK_RESULT_TAG "latency.confidence = %8.6f", - mLatencyReport.confidence); + report << LOOPBACK_RESULT_TAG "latency.frames = " << std::setw(8) + << mLatencyReport.latencyInFrames << "\n"; + report << LOOPBACK_RESULT_TAG "latency.msec = " << std::setw(8) + << latencyMillis << "\n"; + report << LOOPBACK_RESULT_TAG "latency.confidence = " << std::setw(8) + << mLatencyReport.confidence << "\n"; } mState = STATE_DONE; if (getResult() == RESULT_OK) { setResult(newResult); } + + return report.str(); } int32_t getMeasuredLatency() override { @@ -491,8 +504,12 @@ public: return mSignalRMS; } + bool isRecordingComplete() { + return mState == STATE_GOT_DATA; + } + void printStatus() override { - LOGD("st = %d", mState); + ALOGD("latency: st = %d = %s", mState, convertStateToText(mState)); } result_code processInputFrame(float *frameData, int channelCount) override { @@ -509,7 +526,6 @@ public: mBackgroundRMS = sqrtf(mBackgroundSumSquare / mBackgroundSumCount); nextState = STATE_IN_PULSE; mPulseCursor = 0; - LOGD("LatencyAnalyzer state => STATE_SENDING_PULSE"); } break; @@ -517,7 +533,6 @@ public: // Record input until the mAudioRecording is full. mAudioRecording.write(frameData, channelCount, 1); if (hasEnoughData()) { - LOGD("LatencyAnalyzer state => STATE_GOT_DATA"); nextState = STATE_GOT_DATA; } break; @@ -570,22 +585,17 @@ private: }; const char *convertStateToText(echo_state state) { - const char *result = "Unknown"; - switch(state) { + switch (state) { case STATE_MEASURE_BACKGROUND: - result = "INIT"; - break; + return "INIT"; case STATE_IN_PULSE: - result = "PULSE"; - break; + return "PULSE"; case STATE_GOT_DATA: - result = "GOT_DATA"; - break; + return "GOT_DATA"; case STATE_DONE: - result = "DONE"; - break; + return "DONE"; } - return result; + return "UNKNOWN"; } int32_t mDownCounter = 500; @@ -594,14 +604,15 @@ private: static constexpr int32_t kFramesPerEncodedBit = 8; // multiple of 2 static constexpr int32_t kPulseLengthMillis = 500; + static constexpr double kBackgroundMeasurementLengthSeconds = 0.5; AudioRecording mPulse; int32_t mPulseCursor = 0; - float mBackgroundSumSquare = 0.0f; + double mBackgroundSumSquare = 0.0; int32_t mBackgroundSumCount = 0; - float mBackgroundRMS = 0.0f; - float mSignalRMS = 0.0f; + double mBackgroundRMS = 0.0; + double mSignalRMS = 0.0; int32_t mFramesToRecord = 0; AudioRecording mAudioRecording; // contains only the input after starting the pulse diff --git a/apps/OboeTester/app/src/main/cpp/analyzer/ManchesterEncoder.h b/apps/OboeTester/app/src/main/cpp/analyzer/ManchesterEncoder.h index b3d12b31..0a4bd5b2 100644 --- a/apps/OboeTester/app/src/main/cpp/analyzer/ManchesterEncoder.h +++ b/apps/OboeTester/app/src/main/cpp/analyzer/ManchesterEncoder.h @@ -41,6 +41,8 @@ public: , mCursor(samplesPerPulse) { } + virtual ~ManchesterEncoder() = default; + /** * This will be called when the next byte is needed. * @return @@ -64,10 +66,10 @@ protected: /** * This will be called when a new bit is ready to be encoded. * It can be used to prepare the encoded samples. - * @param current + * @param current */ - virtual void onNextBit(bool current) {}; - + virtual void onNextBit(bool /* current */) {}; + void advanceSample() { // Are we ready for a new bit? if (++mCursor >= mSamplesPerPulse) { diff --git a/apps/OboeTester/app/src/main/cpp/analyzer/PeakDetector.h b/apps/OboeTester/app/src/main/cpp/analyzer/PeakDetector.h index 9139e429..4b3b4e71 100644 --- a/apps/OboeTester/app/src/main/cpp/analyzer/PeakDetector.h +++ b/apps/OboeTester/app/src/main/cpp/analyzer/PeakDetector.h @@ -19,6 +19,11 @@ #include <math.h> +/** + * Measure a peak envelope by rising with the peaks, + * and decaying exponentially after each peak. + * The absolute value of the input signal is used. + */ class PeakDetector { public: @@ -27,20 +32,35 @@ public: } double process(double input) { - mLevel *= mDecay; + mLevel *= mDecay; // exponential decay input = fabs(input); + // never fall below the input signal if (input > mLevel) { mLevel = input; } return mLevel; } - double getLevel() { + double getLevel() const { return mLevel; } + double getDecay() const { + return mDecay; + } + + /** + * Multiply the level by this amount on every iteration. + * This provides an exponential decay curve. + * A value just under 1.0 is best, for example, 0.99; + * @param decay scale level for each input + */ + void setDecay(double decay) { + mDecay = decay; + } + private: - static constexpr float kDefaultDecay = 0.99f; + static constexpr double kDefaultDecay = 0.99f; double mLevel = 0.0; double mDecay = kDefaultDecay; diff --git a/apps/OboeTester/app/src/main/cpp/analyzer/PseudoRandom.h b/apps/OboeTester/app/src/main/cpp/analyzer/PseudoRandom.h index 4aedbe0c..1c4938cb 100644 --- a/apps/OboeTester/app/src/main/cpp/analyzer/PseudoRandom.h +++ b/apps/OboeTester/app/src/main/cpp/analyzer/PseudoRandom.h @@ -22,8 +22,7 @@ class PseudoRandom { public: - PseudoRandom() {} - PseudoRandom(int64_t seed) + PseudoRandom(int64_t seed = 99887766) : mSeed(seed) {} @@ -36,7 +35,8 @@ public: return nextRandomInteger() * (0.5 / (((int32_t)1) << 30)); } - /** Calculate random 32 bit number using linear-congruential method. + /** Calculate random 32 bit number using linear-congruential method + * with known real-time performance. */ int32_t nextRandomInteger() { #if __has_builtin(__builtin_mul_overflow) && __has_builtin(__builtin_add_overflow) @@ -51,7 +51,7 @@ public: } private: - int64_t mSeed = 99887766; + int64_t mSeed; }; #endif //ANALYZER_PSEUDORANDOM_H diff --git a/apps/OboeTester/app/src/main/cpp/analyzer/RandomPulseGenerator.h b/apps/OboeTester/app/src/main/cpp/analyzer/RandomPulseGenerator.h index f0623ccc..030050b4 100644 --- a/apps/OboeTester/app/src/main/cpp/analyzer/RandomPulseGenerator.h +++ b/apps/OboeTester/app/src/main/cpp/analyzer/RandomPulseGenerator.h @@ -29,12 +29,14 @@ public: : RoundedManchesterEncoder(samplesPerPulse) { } + virtual ~RandomPulseGenerator() = default; + /** * This will be called when the next byte is needed. * @return random byte */ uint8_t onNextByte() override { - return static_cast<uint8_t>(rand() & 0x00FF); + return static_cast<uint8_t>(rand()); } }; diff --git a/apps/OboeTester/app/src/main/cpp/analyzer/RoundedManchesterEncoder.h b/apps/OboeTester/app/src/main/cpp/analyzer/RoundedManchesterEncoder.h index b1ba949e..f2eba840 100644 --- a/apps/OboeTester/app/src/main/cpp/analyzer/RoundedManchesterEncoder.h +++ b/apps/OboeTester/app/src/main/cpp/analyzer/RoundedManchesterEncoder.h @@ -35,30 +35,30 @@ public: mZeroAfterZero = std::make_unique<float[]>(samplesPerPulse); mZeroAfterOne = std::make_unique<float[]>(samplesPerPulse); - int i = 0; - for (int j = 0; j < rampSize; j++) { - float phase = (j + 1) * M_PI / rampSize; + int sampleIndex = 0; + for (int rampIndex = 0; rampIndex < rampSize; rampIndex++) { + float phase = (rampIndex + 1) * M_PI / rampSize; float sample = -cosf(phase); - mZeroAfterZero[i] = sample; - mZeroAfterOne[i] = 1.0f; - i++; + mZeroAfterZero[sampleIndex] = sample; + mZeroAfterOne[sampleIndex] = 1.0f; + sampleIndex++; } - for (int j = 0; j < rampSize; j++) { - mZeroAfterZero[i] = 1.0f; - mZeroAfterOne[i] = 1.0f; - i++; + for (int rampIndex = 0; rampIndex < rampSize; rampIndex++) { + mZeroAfterZero[sampleIndex] = 1.0f; + mZeroAfterOne[sampleIndex] = 1.0f; + sampleIndex++; } - for (int j = 0; j < rampSize; j++) { - float phase = (j + 1) * M_PI / rampSize; + for (int rampIndex = 0; rampIndex < rampSize; rampIndex++) { + float phase = (rampIndex + 1) * M_PI / rampSize; float sample = cosf(phase); - mZeroAfterZero[i] = sample; - mZeroAfterOne[i] = sample; - i++; + mZeroAfterZero[sampleIndex] = sample; + mZeroAfterOne[sampleIndex] = sample; + sampleIndex++; } - for (int j = 0; j < rampSize; j++) { - mZeroAfterZero[i] = -1.0f; - mZeroAfterOne[i] = -1.0f; - i++; + for (int rampIndex = 0; rampIndex < rampSize; rampIndex++) { + mZeroAfterZero[sampleIndex] = -1.0f; + mZeroAfterOne[sampleIndex] = -1.0f; + sampleIndex++; } } @@ -70,7 +70,6 @@ public: mPreviousBit = current; } - float nextFloat() override { advanceSample(); float output = mCurrentSamples[mCursor]; diff --git a/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/AutoGlitchActivity.java b/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/AutoGlitchActivity.java index 62f6f802..f86368be 100644 --- a/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/AutoGlitchActivity.java +++ b/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/AutoGlitchActivity.java @@ -307,21 +307,23 @@ public class AutoGlitchActivity extends GlitchActivity implements Runnable { e.printStackTrace(); } finally { super.stopAudioTest(); - log("\n==== SUMMARY ========"); - if (mFailCount > 0) { - log(mPassCount + " passed. " + mFailCount + " failed."); - log("These tests FAILED:"); - log(mFailedSummary.toString()); - } else { - log("All tests PASSED."); - } - log("== FINISHED at " + new Date()); - runOnUiThread(new Runnable() { - @Override - public void run() { - onTestFinished(); + if (mThreadEnabled) { + log("\n==== SUMMARY ========"); + if (mFailCount > 0) { + log(mPassCount + " passed. " + mFailCount + " failed."); + log("These tests FAILED:"); + log(mFailedSummary.toString()); + } else { + log("All tests PASSED."); } - }); + log("== FINISHED at " + new Date()); + runOnUiThread(new Runnable() { + @Override + public void run() { + onTestFinished(); + } + }); + } } } diff --git a/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/RoundTripLatencyActivity.java b/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/RoundTripLatencyActivity.java index 782950a7..7edcd38f 100644 --- a/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/RoundTripLatencyActivity.java +++ b/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/RoundTripLatencyActivity.java @@ -51,8 +51,10 @@ public class RoundTripLatencyActivity extends AnalyzerActivity { // Run the test several times and report the acverage latency. protected class LatencyAverager { private final static int AVERAGE_TEST_DELAY_MSEC = 1000; // arbitrary - private static final int AVERAGE_MAX_ITERATIONS = 10; // arbitrary - private int mCount = 0; + private static final int GOOD_RUNS_REQUIRED = 10; // arbitrary + private static final int MAX_BAD_RUNS_ALLOWED = 10; // arbitrary + private int mBadCount = 0; // number of bad measurements + private int mGoodCount = 0; // number of good measurements private double mWeightedLatencySum; private double mLatencyMin; @@ -64,51 +66,63 @@ public class RoundTripLatencyActivity extends AnalyzerActivity { // Called on UI thread. String onAnalyserDone() { String message; + boolean reschedule = false; if (!mActive) { message = ""; } else if (getMeasuredResult() != 0) { - cancel(); - updateButtons(false); - message = "averaging cancelled due to error\n"; + mBadCount++; + if (mBadCount > MAX_BAD_RUNS_ALLOWED) { + cancel(); + updateButtons(false); + message = "averaging cancelled due to error\n"; + } else { + message = "skipping this bad run, " + + mBadCount + " of " + MAX_BAD_RUNS_ALLOWED + " max\n"; + reschedule = true; + } } else { - mCount++; + mGoodCount++; double latency = getMeasuredLatencyMillis(); double confidence = getMeasuredConfidence(); mWeightedLatencySum += latency * confidence; // weighted average based on confidence mConfidenceSum += confidence; mLatencyMin = Math.min(mLatencyMin, latency); mLatencyMax = Math.max(mLatencyMax, latency); - if (mCount < AVERAGE_MAX_ITERATIONS) { - mHandler.postDelayed(new Runnable() { - @Override - public void run() { - measureSingleLatency(); - } - }, AVERAGE_TEST_DELAY_MSEC); + if (mGoodCount < GOOD_RUNS_REQUIRED) { + reschedule = true; } else { mActive = false; updateButtons(false); } message = reportAverage(); } + if (reschedule) { + mHandler.postDelayed(new Runnable() { + @Override + public void run() { + measureSingleLatency(); + } + }, AVERAGE_TEST_DELAY_MSEC); + } return message; } private String reportAverage() { String message; - if (mCount == 0 || mConfidenceSum == 0.0) { - message = "num.iterations = " + mCount + "\n"; + if (mGoodCount == 0 || mConfidenceSum == 0.0) { + message = "num.iterations = " + mGoodCount + "\n"; } else { // When I use 5.3g I only get one digit after the decimal point! final double averageLatency = mWeightedLatencySum / mConfidenceSum; - final double mAverageConfidence = mConfidenceSum / mCount; + final double mAverageConfidence = mConfidenceSum / mGoodCount; message = "average.latency.msec = " + String.format(LATENCY_FORMAT, averageLatency) + "\n" + "average.confidence = " + String.format(CONFIDENCE_FORMAT, mAverageConfidence) + "\n" + "min.latency.msec = " + String.format(LATENCY_FORMAT, mLatencyMin) + "\n" + "max.latency.msec = " + String.format(LATENCY_FORMAT, mLatencyMax) + "\n" - + "num.iterations = " + mCount + "\n"; + + "num.iterations = " + mGoodCount + "\n"; } + message += "num.failed = " + mBadCount + "\n"; mLastReport = message; return message; } @@ -119,7 +133,8 @@ public class RoundTripLatencyActivity extends AnalyzerActivity { mConfidenceSum = 0.0; mLatencyMax = Double.MIN_VALUE; mLatencyMin = Double.MAX_VALUE; - mCount = 0; + mBadCount = 0; + mGoodCount = 0; mActive = true; mLastReport = ""; measureSingleLatency(); diff --git a/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/TestAudioActivity.java b/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/TestAudioActivity.java index 8aa6434c..bd7fff50 100644 --- a/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/TestAudioActivity.java +++ b/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/TestAudioActivity.java @@ -49,7 +49,8 @@ abstract class TestAudioActivity extends Activity { public static final int AUDIO_STATE_STARTED = 1; public static final int AUDIO_STATE_PAUSED = 2; public static final int AUDIO_STATE_STOPPED = 3; - public static final int AUDIO_STATE_CLOSED = 4; + public static final int AUDIO_STATE_CLOSING = 4; + public static final int AUDIO_STATE_CLOSED = 5; public static final int COLOR_ACTIVE = 0xFFD0D0A0; public static final int COLOR_IDLE = 0xFFD0D0D0; @@ -467,10 +468,14 @@ abstract class TestAudioActivity extends Activity { } } + protected void toastPauseError(int result) { + showErrorToast("Pause failed with " + result); + } + public void pauseAudio() { int result = pauseNative(); if (result < 0) { - showErrorToast("Pause failed with " + result); + toastPauseError(result); } else { mAudioState = AUDIO_STATE_PAUSED; updateEnabledWidgets(); @@ -493,10 +498,26 @@ abstract class TestAudioActivity extends Activity { updateEnabledWidgets(); } - public void closeAudio() { + // Make synchronized so we don't close from two streams at the same time. + public synchronized void closeAudio() { + if (mAudioState >= AUDIO_STATE_CLOSING) { + Log.d(TAG, "closeAudio() already closing"); + return; + } + mAudioState = AUDIO_STATE_CLOSING; + mStreamSniffer.stopStreamSniffer(); + // Close output streams first because legacy callbacks may still be active + // and an output stream may be calling the input stream. + for (StreamContext streamContext : mStreamContexts) { + if (!streamContext.isInput()) { + streamContext.tester.close(); + } + } for (StreamContext streamContext : mStreamContexts) { - streamContext.tester.close(); + if (streamContext.isInput()) { + streamContext.tester.close(); + } } if (mScoStarted) { diff --git a/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/TestInputActivity.java b/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/TestInputActivity.java index 6ad04808..273ca4ad 100644 --- a/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/TestInputActivity.java +++ b/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/TestInputActivity.java @@ -143,6 +143,11 @@ public class TestInputActivity extends TestAudioActivity resetVolumeBars(); } + @Override + protected void toastPauseError(int result) { + showToast("Pause not implemented. Returned " + result); + } + private boolean isRecordPermissionGranted() { return (ActivityCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED); diff --git a/apps/OboeTester/app/src/main/res/layout/activity_rt_latency.xml b/apps/OboeTester/app/src/main/res/layout/activity_rt_latency.xml index 5cf91243..de3ca261 100644 --- a/apps/OboeTester/app/src/main/res/layout/activity_rt_latency.xml +++ b/apps/OboeTester/app/src/main/res/layout/activity_rt_latency.xml @@ -88,7 +88,7 @@ android:id="@+id/text_status" android:layout_width="match_parent" android:layout_height="wrap_content" - android:lines="15" + android:lines="16" android:text="@string/use_loopback" android:textSize="18sp" android:textStyle="bold" /> diff --git a/include/oboe/AudioStreamBuilder.h b/include/oboe/AudioStreamBuilder.h index a1ea0a85..80c5da42 100644 --- a/include/oboe/AudioStreamBuilder.h +++ b/include/oboe/AudioStreamBuilder.h @@ -198,10 +198,11 @@ public: /** - * Set the intended use case for the stream. + * Set the intended use case for an output stream. * * The system will use this information to optimize the behavior of the stream. * This could, for example, affect how volume and focus is handled for the stream. + * The usage is ignored for input streams. * * The default, if you do not call this function, is Usage::Media. * @@ -215,10 +216,11 @@ public: } /** - * Set the type of audio data that the stream will carry. + * Set the type of audio data that an output stream will carry. * * The system will use this information to optimize the behavior of the stream. * This could, for example, affect whether a stream is paused when a notification occurs. + * The contentType is ignored for input streams. * * The default, if you do not call this function, is ContentType::Music. * diff --git a/samples/drumthumper/src/main/cpp/DrumPlayerJNI.cpp b/samples/drumthumper/src/main/cpp/DrumPlayerJNI.cpp index 5ae41f73..b1dcf41a 100644 --- a/samples/drumthumper/src/main/cpp/DrumPlayerJNI.cpp +++ b/samples/drumthumper/src/main/cpp/DrumPlayerJNI.cpp @@ -45,11 +45,17 @@ static SimpleMultiPlayer sDTPlayer; * Native (JNI) implementation of DrumPlayer.setupAudioStreamNative() */ JNIEXPORT void JNICALL Java_com_plausiblesoftware_drumthumper_DrumPlayer_setupAudioStreamNative( - JNIEnv* env, jobject, jint sampleRate, jint numChannels) { + JNIEnv* env, jobject, jint numChannels) { __android_log_print(ANDROID_LOG_INFO, TAG, "%s", "init()"); // we know in this case that the sample buffers are all 1-channel, 41K - sDTPlayer.setupAudioStream(sampleRate, numChannels); + sDTPlayer.setupAudioStream(numChannels); +} + +JNIEXPORT void JNICALL +Java_com_plausiblesoftware_drumthumper_DrumPlayer_startAudioStreamNative( + JNIEnv *env, jobject thiz) { + sDTPlayer.startStream(); } /** @@ -69,7 +75,7 @@ JNIEXPORT void JNICALL Java_com_plausiblesoftware_drumthumper_DrumPlayer_teardow * Native (JNI) implementation of DrumPlayer.loadWavAssetNative() */ JNIEXPORT jboolean JNICALL Java_com_plausiblesoftware_drumthumper_DrumPlayer_loadWavAssetNative( - JNIEnv* env, jobject, jbyteArray bytearray, jint index, jfloat pan, jint rate, jint channels) { + JNIEnv* env, jobject, jbyteArray bytearray, jint index, jfloat pan, jint channels) { int len = env->GetArrayLength (bytearray); unsigned char* buf = new unsigned char[len]; @@ -80,8 +86,7 @@ JNIEXPORT jboolean JNICALL Java_com_plausiblesoftware_drumthumper_DrumPlayer_loa WavStreamReader reader(&stream); reader.parse(); - jboolean isFormatValid = - (reader.getSampleRate() == rate) && (reader.getNumChannels() == channels); + jboolean isFormatValid = reader.getNumChannels() == channels; SampleBuffer* sampleBuffer = new SampleBuffer(); sampleBuffer->loadSampleData(&reader); @@ -127,7 +132,7 @@ JNIEXPORT void JNICALL Java_com_plausiblesoftware_drumthumper_DrumPlayer_clearOu */ JNIEXPORT void JNICALL Java_com_plausiblesoftware_drumthumper_DrumPlayer_restartStream(JNIEnv*, jobject) { sDTPlayer.resetAll(); - if (sDTPlayer.openStream()){ + if (sDTPlayer.openStream() && sDTPlayer.startStream()){ __android_log_print(ANDROID_LOG_INFO, TAG, "openStream successful"); } else { __android_log_print(ANDROID_LOG_ERROR, TAG, "openStream failed"); diff --git a/samples/drumthumper/src/main/java/com/plausibleaudio/drumthumper/DrumPlayer.kt b/samples/drumthumper/src/main/java/com/plausibleaudio/drumthumper/DrumPlayer.kt index 8d50510a..62c8dbfe 100644 --- a/samples/drumthumper/src/main/java/com/plausibleaudio/drumthumper/DrumPlayer.kt +++ b/samples/drumthumper/src/main/java/com/plausibleaudio/drumthumper/DrumPlayer.kt @@ -27,8 +27,6 @@ class DrumPlayer { // This IS NOT the channel format of the source samples // (which must be mono). val NUM_SAMPLE_CHANNELS: Int = 1; // All WAV resource must be mono - val SAMPLE_RATE: Int = 44100 // All the input samples are assumed to BE 44.1K - // All the input samples are assumed to be mono. // Sample Buffer IDs val BASSDRUM: Int = 0 @@ -55,7 +53,11 @@ class DrumPlayer { } fun setupAudioStream() { - setupAudioStreamNative(SAMPLE_RATE, NUM_PLAY_CHANNELS) + setupAudioStreamNative(NUM_PLAY_CHANNELS) + } + + fun startAudioStream() { + startAudioStreamNative(); } fun teardownAudioStream() { @@ -89,7 +91,7 @@ class DrumPlayer { var dataLen = assetFD.getLength().toInt() var dataBytes: ByteArray = ByteArray(dataLen) dataStream.read(dataBytes, 0, dataLen) - returnVal = loadWavAssetNative(dataBytes, index, pan, SAMPLE_RATE, NUM_SAMPLE_CHANNELS) + returnVal = loadWavAssetNative(dataBytes, index, pan, NUM_SAMPLE_CHANNELS) assetFD.close() } catch (ex: IOException) { Log.i(TAG, "IOException" + ex) @@ -98,11 +100,12 @@ class DrumPlayer { return returnVal } - external fun setupAudioStreamNative(sampleRate: Int, numChannels: Int) + external fun setupAudioStreamNative(numChannels: Int) + external fun startAudioStreamNative(); external fun teardownAudioStreamNative() external fun loadWavAssetNative( - wavBytes: ByteArray, index: Int, pan: Float, rate: Int, channels: Int) : Boolean + wavBytes: ByteArray, index: Int, pan: Float, channels: Int) : Boolean external fun unloadWavAssetsNative() external fun trigger(drumIndex: Int) diff --git a/samples/drumthumper/src/main/java/com/plausibleaudio/drumthumper/DrumThumperActivity.kt b/samples/drumthumper/src/main/java/com/plausibleaudio/drumthumper/DrumThumperActivity.kt index eb90cb8e..2bdd6c9f 100644 --- a/samples/drumthumper/src/main/java/com/plausibleaudio/drumthumper/DrumThumperActivity.kt +++ b/samples/drumthumper/src/main/java/com/plausibleaudio/drumthumper/DrumThumperActivity.kt @@ -161,7 +161,13 @@ class DrumThumperActivity : AppCompatActivity(), mAudioMgr = getSystemService(Context.AUDIO_SERVICE) as AudioManager - // mDrumPlayer.allocSampleData() + } + + override fun onStart() { + super.onStart() + + mDrumPlayer.setupAudioStream() + var allAssetsValid = mDrumPlayer.loadWavAssets(getAssets()) if (!allAssetsValid) { @@ -171,12 +177,7 @@ class DrumThumperActivity : AppCompatActivity(), Toast.LENGTH_LONG) toast.show() } - } - - override fun onStart() { - super.onStart() - - mDrumPlayer.setupAudioStream() + mDrumPlayer.startAudioStream() if (mUseDeviceChangeFallback) { mAudioMgr!!.registerAudioDeviceCallback(mDeviceListener, null) @@ -232,11 +233,12 @@ class DrumThumperActivity : AppCompatActivity(), mDrumPlayer.teardownAudioStream() + mDrumPlayer.unloadWavAssets() + super.onStop() } override fun onDestroy() { - mDrumPlayer.unloadWavAssets(); super.onDestroy() } diff --git a/samples/iolib/src/main/cpp/CMakeLists.txt b/samples/iolib/src/main/cpp/CMakeLists.txt index 2c3d9232..dd8c6263 100644 --- a/samples/iolib/src/main/cpp/CMakeLists.txt +++ b/samples/iolib/src/main/cpp/CMakeLists.txt @@ -29,6 +29,7 @@ set (PARSELIB_DIR ../../../../parselib) include_directories(
${PARSELIB_DIR}/src/main/cpp
${OBOE_DIR}/include
+ ${OBOE_DIR}/src/flowgraph
${CMAKE_CURRENT_LIST_DIR}
../../../../shared)
diff --git a/samples/iolib/src/main/cpp/player/SampleBuffer.cpp b/samples/iolib/src/main/cpp/player/SampleBuffer.cpp index ca3a9058..a882b3a4 100644 --- a/samples/iolib/src/main/cpp/player/SampleBuffer.cpp +++ b/samples/iolib/src/main/cpp/player/SampleBuffer.cpp @@ -16,8 +16,13 @@ #include "SampleBuffer.h" +// Resampler Includes +#include <resampler/MultiChannelResampler.h> + #include "wav/WavStreamReader.h" +using namespace resampler; + namespace iolib { void SampleBuffer::loadSampleData(parselib::WavStreamReader* reader) { @@ -41,4 +46,75 @@ void SampleBuffer::unloadSampleData() { mNumSamples = 0; } +class ResampleBlock { +public: + int32_t mSampleRate; + float* mBuffer; + int32_t mNumFrames; +}; + +void resampleData(const ResampleBlock& input, ResampleBlock* output) { + // Calculate output buffer size + double temp = + ((double)input.mNumFrames * (double)output->mSampleRate) / (double)input.mSampleRate; + + // round up + int32_t numOutFrames = (int32_t)(temp + 0.5); + // We iterate thousands of times through the loop. Roundoff error could accumulate + // so add a few more frames for padding + numOutFrames += 8; + + const int channelCount = 1; // 1 for mono, 2 for stereo + MultiChannelResampler *resampler = MultiChannelResampler::make( + channelCount, // channel count + input.mSampleRate, // input sampleRate + output->mSampleRate, // output sampleRate + MultiChannelResampler::Quality::Medium); // conversion quality + + float *inputBuffer = input.mBuffer;; // multi-channel buffer to be consumed + float *outputBuffer = new float[numOutFrames]; // multi-channel buffer to be filled + output->mBuffer = outputBuffer; + + int numOutputFrames = 0; + int inputFramesLeft = input.mNumFrames; + while (inputFramesLeft > 0) { + if(resampler->isWriteNeeded()) { + resampler->writeNextFrame(inputBuffer); + inputBuffer += channelCount; + inputFramesLeft--; + } else { + resampler->readNextFrame(outputBuffer); + outputBuffer += channelCount; + numOutputFrames++; + } + } + output->mNumFrames = numOutputFrames; + + delete resampler; } + +void SampleBuffer::resampleData(int sampleRate) { + if (mAudioProperties.sampleRate == sampleRate) { + // nothing to do + return; + } + + ResampleBlock inputBlock; + inputBlock.mBuffer = mSampleData; + inputBlock.mNumFrames = mNumSamples; + inputBlock.mSampleRate = mAudioProperties.sampleRate; + + ResampleBlock outputBlock; + outputBlock.mSampleRate = sampleRate; + iolib::resampleData(inputBlock, &outputBlock); + + // delete previous samples + delete[] mSampleData; + + // install the resampled data + mSampleData = outputBlock.mBuffer; + mNumSamples = outputBlock.mNumFrames; + mAudioProperties.sampleRate = outputBlock.mSampleRate; +} + +} // namespace iolib diff --git a/samples/iolib/src/main/cpp/player/SampleBuffer.h b/samples/iolib/src/main/cpp/player/SampleBuffer.h index 9d61f28d..c92ee78f 100644 --- a/samples/iolib/src/main/cpp/player/SampleBuffer.h +++ b/samples/iolib/src/main/cpp/player/SampleBuffer.h @@ -38,6 +38,8 @@ public: void loadSampleData(parselib::WavStreamReader* reader); void unloadSampleData(); + void resampleData(int sampleRate); + virtual AudioProperties getProperties() const { return mAudioProperties; } float* getSampleData() { return mSampleData; } diff --git a/samples/iolib/src/main/cpp/player/SimpleMultiPlayer.cpp b/samples/iolib/src/main/cpp/player/SimpleMultiPlayer.cpp index 6110b290..2cbfae77 100644 --- a/samples/iolib/src/main/cpp/player/SimpleMultiPlayer.cpp +++ b/samples/iolib/src/main/cpp/player/SimpleMultiPlayer.cpp @@ -34,7 +34,7 @@ namespace iolib { constexpr int32_t kBufferSizeInBursts = 2; // Use 2 bursts as the buffer size (double buffer) SimpleMultiPlayer::SimpleMultiPlayer() - : mChannelCount(0), mSampleRate(0), mOutputReset(false) + : mChannelCount(0), mOutputReset(false) {} DataCallbackResult SimpleMultiPlayer::onAudioReady(AudioStream *oboeStream, void *audioData, @@ -78,7 +78,7 @@ bool SimpleMultiPlayer::openStream() { // Create an audio stream AudioStreamBuilder builder; builder.setChannelCount(mChannelCount); - builder.setSampleRate(mSampleRate); + // we will resample source data to device rate, so take default sample rate builder.setCallback(this); builder.setPerformanceMode(PerformanceMode::LowLatency); builder.setSharingMode(SharingMode::Exclusive); @@ -105,7 +105,13 @@ bool SimpleMultiPlayer::openStream() { "setBufferSizeInFrames failed. Error: %s", convertToText(result)); } - result = mAudioStream->requestStart(); + mSampleRate = mAudioStream->getSampleRate(); + + return true; +} + +bool SimpleMultiPlayer::startStream() { + Result result = mAudioStream->requestStart(); if (result != Result::OK){ __android_log_print( ANDROID_LOG_ERROR, @@ -117,11 +123,9 @@ bool SimpleMultiPlayer::openStream() { return true; } -void SimpleMultiPlayer::setupAudioStream(int32_t sampleRate, int32_t channelCount) { +void SimpleMultiPlayer::setupAudioStream(int32_t channelCount) { __android_log_print(ANDROID_LOG_INFO, TAG, "setupAudioStream()"); mChannelCount = channelCount; - mSampleRate = sampleRate; - mSampleRate = sampleRate; openStream(); } @@ -135,6 +139,8 @@ void SimpleMultiPlayer::teardownAudioStream() { } void SimpleMultiPlayer::addSampleSource(SampleSource* source, SampleBuffer* buffer) { + buffer->resampleData(mSampleRate); + mSampleBuffers.push_back(buffer); mSampleSources.push_back(source); mNumSampleBuffers++; diff --git a/samples/iolib/src/main/cpp/player/SimpleMultiPlayer.h b/samples/iolib/src/main/cpp/player/SimpleMultiPlayer.h index 3e0a1c77..d8c9877c 100644 --- a/samples/iolib/src/main/cpp/player/SimpleMultiPlayer.h +++ b/samples/iolib/src/main/cpp/player/SimpleMultiPlayer.h @@ -41,10 +41,13 @@ public: virtual void onErrorAfterClose(oboe::AudioStream *oboeStream, oboe::Result error) override; virtual void onErrorBeforeClose(oboe::AudioStream * oboeStream, oboe::Result error) override; - void setupAudioStream(int32_t sampleRate, int32_t channelCount); + void setupAudioStream(int32_t channelCount); void teardownAudioStream(); bool openStream(); + bool startStream(); + + int getSampleRate() { return mSampleRate; } // Wave Sample Loading... /** @@ -77,7 +80,7 @@ private: // Oboe Audio Stream oboe::ManagedStream mAudioStream; - // Audio attributes + // Playback Audio attributes int32_t mChannelCount; int32_t mSampleRate; diff --git a/src/aaudio/AudioStreamAAudio.cpp b/src/aaudio/AudioStreamAAudio.cpp index 9f493ea2..96f0c392 100644 --- a/src/aaudio/AudioStreamAAudio.cpp +++ b/src/aaudio/AudioStreamAAudio.cpp @@ -118,7 +118,7 @@ void AudioStreamAAudio::internalErrorCallback( if (oboeStream->wasErrorCallbackCalled()) { // block extra error callbacks LOGE("%s() multiple error callbacks called!", __func__); } else if (stream != oboeStream->getUnderlyingStream()) { - LOGW("%s() stream already closed", __func__); // can happen if there are bugs + LOGW("%s() stream already closed or closing", __func__); // can happen if there are bugs } else if (sharedStream) { // Handle error on a separate thread using shared pointer. std::thread t(oboe_aaudio_error_thread_proc_shared, sharedStream, @@ -258,7 +258,6 @@ Result AudioStreamAAudio::open() { mBufferCapacityInFrames = mLibLoader->stream_getBufferCapacity(mAAudioStream); mBufferSizeInFrames = mLibLoader->stream_getBufferSize(mAAudioStream); - // These were added in P so we have to check for the function pointer. if (mLibLoader->stream_getUsage != nullptr) { mUsage = static_cast<Usage>(mLibLoader->stream_getUsage(mAAudioStream)); @@ -298,6 +297,14 @@ Result AudioStreamAAudio::close() { // This will delete the AAudio stream object so we need to null out the pointer. AAudioStream *stream = mAAudioStream.exchange(nullptr); if (stream != nullptr) { + // Sometimes a callback can occur shortly after a stream has been stopped and + // even after a close. If the stream has been closed then the callback + // can access memory that has been freed. That causes a crash. + // Two milliseconds may be enough but 10 msec is even safer. + // This seems to be more likely in P or earlier. But it can also occur in later versions. + if (OboeGlobals::areWorkaroundsEnabled()) { + usleep(kDelayBeforeCloseMillis * 1000); + } return static_cast<Result>(mLibLoader->stream_close(stream)); } else { return Result::ErrorClosed; diff --git a/src/aaudio/AudioStreamAAudio.h b/src/aaudio/AudioStreamAAudio.h index 6267328b..0df224ae 100644 --- a/src/aaudio/AudioStreamAAudio.h +++ b/src/aaudio/AudioStreamAAudio.h @@ -110,6 +110,9 @@ protected: private: + // Time to sleep in order to prevent a race condition with a callback after a close(). + static constexpr int kDelayBeforeCloseMillis = 10; + std::atomic<bool> mCallbackThreadEnabled; // pointer to the underlying AAudio stream, valid if open, null if closed diff --git a/src/common/DataConversionFlowGraph.cpp b/src/common/DataConversionFlowGraph.cpp index 7ddc0e60..6f0ee5b8 100644 --- a/src/common/DataConversionFlowGraph.cpp +++ b/src/common/DataConversionFlowGraph.cpp @@ -98,8 +98,8 @@ Result DataConversionFlowGraph::configure(AudioStream *sourceStream, AudioStream ? sourceStream->getFramesPerBurst() : sourceStream->getFramesPerCallback(); // Source - // If OUTPUT and using a callback then call back to the app using a SourceCaller. - // If INPUT and NOT using a callback then read from the child stream using a SourceCaller. + // IF OUTPUT and using a callback then call back to the app using a SourceCaller. + // OR IF INPUT and NOT using a callback then read from the child stream using a SourceCaller. if ((sourceStream->getCallback() != nullptr && isOutput) || (sourceStream->getCallback() == nullptr && isInput)) { switch (sourceFormat) { @@ -118,8 +118,8 @@ Result DataConversionFlowGraph::configure(AudioStream *sourceStream, AudioStream mSourceCaller->setStream(sourceStream); lastOutput = &mSourceCaller->output; } else { - // If OUTPUT and NOT using a callback then write to the child stream using a BlockWriter. - // If INPUT and using a callback then write to the app using a BlockWriter. + // IF OUTPUT and NOT using a callback then write to the child stream using a BlockWriter. + // OR IF INPUT and using a callback then write to the app using a BlockWriter. switch (sourceFormat) { case AudioFormat::Float: mSource = std::make_unique<SourceFloat>(sourceChannelCount); @@ -200,8 +200,6 @@ Result DataConversionFlowGraph::configure(AudioStream *sourceStream, AudioStream } lastOutput->connect(&mSink->input); - mFramePosition = 0; - return Result::OK; } @@ -210,7 +208,6 @@ int32_t DataConversionFlowGraph::read(void *buffer, int32_t numFrames, int64_t t mSourceCaller->setTimeoutNanos(timeoutNanos); } int32_t numRead = mSink->read(buffer, numFrames); - mFramePosition += numRead; return numRead; } @@ -221,7 +218,6 @@ int32_t DataConversionFlowGraph::write(void *inputBuffer, int32_t numFrames) { while (true) { // Pull and read some data in app format into a small buffer. int32_t framesRead = mSink->read(mAppBuffer.get(), flowgraph::kDefaultBufferSize); - mFramePosition += framesRead; if (framesRead <= 0) break; // Write to a block adapter, which will call the destination whenever it has enough data. int32_t bytesRead = mBlockWriter.write(mAppBuffer.get(), diff --git a/src/common/DataConversionFlowGraph.h b/src/common/DataConversionFlowGraph.h index 5b8d3f66..0cde1f35 100644 --- a/src/common/DataConversionFlowGraph.h +++ b/src/common/DataConversionFlowGraph.h @@ -80,8 +80,6 @@ private: DataCallbackResult mCallbackResult = DataCallbackResult::Continue; AudioStream *mFilterStream = nullptr; std::unique_ptr<uint8_t[]> mAppBuffer; - - int64_t mFramePosition = 0; }; } diff --git a/src/flowgraph/FlowGraphNode.cpp b/src/flowgraph/FlowGraphNode.cpp index c7e3ff9d..9a62d7d8 100644 --- a/src/flowgraph/FlowGraphNode.cpp +++ b/src/flowgraph/FlowGraphNode.cpp @@ -111,4 +111,4 @@ float *FlowGraphPortFloatInput::getBuffer() { int32_t FlowGraphSink::pullData(int32_t numFrames) { return FlowGraphNode::pullData(numFrames, getLastCallCount() + 1); -}
\ No newline at end of file +} diff --git a/src/flowgraph/SampleRateConverter.cpp b/src/flowgraph/SampleRateConverter.cpp index 0c92d7fd..b1ae4bd8 100644 --- a/src/flowgraph/SampleRateConverter.cpp +++ b/src/flowgraph/SampleRateConverter.cpp @@ -25,11 +25,17 @@ SampleRateConverter::SampleRateConverter(int32_t channelCount, MultiChannelResam setDataPulledAutomatically(false); } +void SampleRateConverter::reset() { + FlowGraphNode::reset(); + mInputCursor = kInitialCallCount; +} + // Return true if there is a sample available. bool SampleRateConverter::isInputAvailable() { + // If we have consumed all of the input data then go out and get some more. if (mInputCursor >= mNumValidInputFrames) { - mNumValidInputFrames = input.pullData(mInputFramePosition, input.getFramesPerBuffer()); - mInputFramePosition += mNumValidInputFrames; + mInputCallCount++; + mNumValidInputFrames = input.pullData(mInputCallCount, input.getFramesPerBuffer()); mInputCursor = 0; } return (mInputCursor < mNumValidInputFrames); diff --git a/src/flowgraph/SampleRateConverter.h b/src/flowgraph/SampleRateConverter.h index 5fb5c650..534df49a 100644 --- a/src/flowgraph/SampleRateConverter.h +++ b/src/flowgraph/SampleRateConverter.h @@ -38,6 +38,8 @@ public: return "SampleRateConverter"; } + void reset() override; + private: // Return true if there is a sample available. @@ -48,9 +50,11 @@ private: resampler::MultiChannelResampler &mResampler; - int32_t mInputCursor = 0; - int32_t mNumValidInputFrames = 0; - int64_t mInputFramePosition = 0; // monotonic counter of input frames used for pullData + int32_t mInputCursor = 0; // offset into the input port buffer + int32_t mNumValidInputFrames = 0; // number of valid frames currently in the input port buffer + // We need our own callCount for upstream calls because calls occur at a different rate. + // This means we cannot have cyclic graphs or merges that contain an SRC. + int64_t mInputCallCount = 0; }; diff --git a/src/flowgraph/resampler/README.md b/src/flowgraph/resampler/README.md index 2026773a..ecf030ff 100644 --- a/src/flowgraph/resampler/README.md +++ b/src/flowgraph/resampler/README.md @@ -20,7 +20,7 @@ Only do this once, when you open your stream. Then use the sample resampler to p 2, // channel count 44100, // input sampleRate 48000, // output sampleRate - MultiChannelResampler::Medium); // conversion quality + MultiChannelResampler::Quality::Medium); // conversion quality Possible values for quality include { Fastest, Low, Medium, High, Best }. Higher quality levels will sound better but consume more CPU because they have more taps in the filter. diff --git a/tests/testStreamOpen.cpp b/tests/testStreamOpen.cpp index e3bf7e83..8b2fa6d3 100644 --- a/tests/testStreamOpen.cpp +++ b/tests/testStreamOpen.cpp @@ -24,18 +24,20 @@ class CallbackSizeMonitor : public AudioStreamCallback { public: DataCallbackResult onAudioReady(AudioStream *oboeStream, void *audioData, int32_t numFrames) override { framesPerCallback = numFrames; + callbackCount++; return DataCallbackResult::Continue; } // This is exposed publicly so that the number of frames per callback can be tested. std::atomic<int32_t> framesPerCallback{0}; + std::atomic<int32_t> callbackCount{0}; }; class StreamOpen : public ::testing::Test { protected: - bool openStream(){ + bool openStream() { Result r = mBuilder.openStream(&mStream); EXPECT_EQ(r, Result::OK) << "Failed to open stream " << convertToText(r); EXPECT_EQ(0, openCount) << "Should start with a fresh object every time."; @@ -43,19 +45,45 @@ protected: return (r == Result::OK); } - void closeStream(){ - if (mStream != nullptr){ + void closeStream() { + if (mStream != nullptr) { Result r = mStream->close(); - if (r != Result::OK){ + if (r != Result::OK) { FAIL() << "Failed to close stream. " << convertToText(r); } } usleep(500 * 1000); // give previous stream time to settle } + void checkSampleRateConversionAdvancing(Direction direction) { + CallbackSizeMonitor callback; + + mBuilder.setDirection(direction); + mBuilder.setAudioApi(AudioApi::AAudio); + mBuilder.setCallback(&callback); + mBuilder.setPerformanceMode(PerformanceMode::LowLatency); + mBuilder.setSampleRate(44100); + mBuilder.setSampleRateConversionQuality(SampleRateConversionQuality::Medium); + + openStream(); + + ASSERT_EQ(mStream->requestStart(), Result::OK); + int timeout = 20; + while (callback.framesPerCallback == 0 && timeout > 0) { + usleep(50 * 1000); + timeout--; + } + ASSERT_GT(callback.callbackCount, 0); + ASSERT_GT(callback.framesPerCallback, 0); + ASSERT_EQ(mStream->requestStop(), Result::OK); + + closeStream(); + } + AudioStreamBuilder mBuilder; AudioStream *mStream = nullptr; int32_t openCount = 0; + }; TEST_F(StreamOpen, ForOpenSLESDefaultSampleRateIsUsed){ @@ -110,6 +138,29 @@ TEST_F(StreamOpen, InputForOpenSLESPerformanceModeShouldBeNone){ closeStream(); } +TEST_F(StreamOpen, ForOpenSlesIllegalFormatRejectedOutput) { + mBuilder.setAudioApi(AudioApi::OpenSLES); + mBuilder.setPerformanceMode(PerformanceMode::LowLatency); + mBuilder.setFormat(static_cast<AudioFormat>(666)); + Result r = mBuilder.openStream(&mStream); + EXPECT_NE(r, Result::OK) << "Should not open stream " << convertToText(r); + if (mStream != nullptr) { + mStream->close(); // just in case it accidentally opened + } +} + +TEST_F(StreamOpen, ForOpenSlesIllegalFormatRejectedInput) { + mBuilder.setAudioApi(AudioApi::OpenSLES); + mBuilder.setPerformanceMode(PerformanceMode::LowLatency); + mBuilder.setDirection(Direction::Input); + mBuilder.setFormat(static_cast<AudioFormat>(666)); + Result r = mBuilder.openStream(&mStream); + EXPECT_NE(r, Result::OK) << "Should not open stream " << convertToText(r); + if (mStream != nullptr) { + mStream->close(); // just in case it accidentally opened + } +} + // Make sure the callback is called with the requested FramesPerCallback TEST_F(StreamOpen, OpenSLESFramesPerCallback) { const int kRequestedFramesPerCallback = 417; @@ -309,3 +360,13 @@ TEST_F(StreamOpen, LowLatencyStreamHasSmallBufferSize){ ASSERT_LE(bufferSize, burst * 3); } } + +// See if sample rate conversion by Oboe is calling the callback. +TEST_F(StreamOpen, AAudioOutputSampleRate44100) { + checkSampleRateConversionAdvancing(Direction::Output); +} + +// See if sample rate conversion by Oboe is calling the callback. +TEST_F(StreamOpen, AAudioInputSampleRate44100) { + checkSampleRateConversionAdvancing(Direction::Input); +}
\ No newline at end of file diff --git a/tests/testUtilities.cpp b/tests/testUtilities.cpp index 99451453..6b101be0 100644 --- a/tests/testUtilities.cpp +++ b/tests/testUtilities.cpp @@ -27,9 +27,6 @@ using namespace oboe; class UtilityFunctions : public ::testing::Test { - - - }; TEST_F(UtilityFunctions, Converts16BitIntegerToSizeOf2Bytes){ |