aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHaibo Huang <hhb@google.com>2020-09-10 22:59:40 +0000
committerAutomerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>2020-09-10 22:59:40 +0000
commitc9056fde849c9deb1d647fa92a72da66a4a8442f (patch)
tree8386d0a708feb788939d45dab1083babca1574c0
parent32f16966e88170df96c65d970c3e564b93e6aed1 (diff)
parent3128bb814235c96f19c41ec7299b522c75827bb7 (diff)
downloadoboe-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
-rw-r--r--METADATA12
-rw-r--r--MODULE_LICENSE_APACHE20
-rw-r--r--NOTICE202
-rw-r--r--apps/OboeTester/app/CMakeLists.txt2
-rw-r--r--apps/OboeTester/app/build.gradle4
-rw-r--r--apps/OboeTester/app/src/main/AndroidManifest.xml4
-rw-r--r--apps/OboeTester/app/src/main/cpp/AudioStreamGateway.cpp9
-rw-r--r--apps/OboeTester/app/src/main/cpp/AudioStreamGateway.h9
-rw-r--r--apps/OboeTester/app/src/main/cpp/FullDuplexAnalyzer.cpp2
-rw-r--r--apps/OboeTester/app/src/main/cpp/InputStreamCallbackAnalyzer.cpp2
-rw-r--r--apps/OboeTester/app/src/main/cpp/InputStreamCallbackAnalyzer.h4
-rw-r--r--apps/OboeTester/app/src/main/cpp/NativeAudioContext.cpp3
-rw-r--r--apps/OboeTester/app/src/main/cpp/NativeAudioContext.h7
-rw-r--r--apps/OboeTester/app/src/main/cpp/OboeTesterStreamCallback.cpp40
-rw-r--r--apps/OboeTester/app/src/main/cpp/OboeTesterStreamCallback.h41
-rw-r--r--apps/OboeTester/app/src/main/cpp/analyzer/GlitchAnalyzer.h145
-rw-r--r--apps/OboeTester/app/src/main/cpp/analyzer/LatencyAnalyzer.h153
-rw-r--r--apps/OboeTester/app/src/main/cpp/analyzer/ManchesterEncoder.h8
-rw-r--r--apps/OboeTester/app/src/main/cpp/analyzer/PeakDetector.h26
-rw-r--r--apps/OboeTester/app/src/main/cpp/analyzer/PseudoRandom.h8
-rw-r--r--apps/OboeTester/app/src/main/cpp/analyzer/RandomPulseGenerator.h4
-rw-r--r--apps/OboeTester/app/src/main/cpp/analyzer/RoundedManchesterEncoder.h39
-rw-r--r--apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/AutoGlitchActivity.java30
-rw-r--r--apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/RoundTripLatencyActivity.java51
-rw-r--r--apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/TestAudioActivity.java29
-rw-r--r--apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/TestInputActivity.java5
-rw-r--r--apps/OboeTester/app/src/main/res/layout/activity_rt_latency.xml2
-rw-r--r--include/oboe/AudioStreamBuilder.h6
-rw-r--r--samples/drumthumper/src/main/cpp/DrumPlayerJNI.cpp17
-rw-r--r--samples/drumthumper/src/main/java/com/plausibleaudio/drumthumper/DrumPlayer.kt15
-rw-r--r--samples/drumthumper/src/main/java/com/plausibleaudio/drumthumper/DrumThumperActivity.kt18
-rw-r--r--samples/iolib/src/main/cpp/CMakeLists.txt1
-rw-r--r--samples/iolib/src/main/cpp/player/SampleBuffer.cpp76
-rw-r--r--samples/iolib/src/main/cpp/player/SampleBuffer.h2
-rw-r--r--samples/iolib/src/main/cpp/player/SimpleMultiPlayer.cpp18
-rw-r--r--samples/iolib/src/main/cpp/player/SimpleMultiPlayer.h7
-rw-r--r--src/aaudio/AudioStreamAAudio.cpp11
-rw-r--r--src/aaudio/AudioStreamAAudio.h3
-rw-r--r--src/common/DataConversionFlowGraph.cpp12
-rw-r--r--src/common/DataConversionFlowGraph.h2
-rw-r--r--src/flowgraph/FlowGraphNode.cpp2
-rw-r--r--src/flowgraph/SampleRateConverter.cpp10
-rw-r--r--src/flowgraph/SampleRateConverter.h10
-rw-r--r--src/flowgraph/resampler/README.md2
-rw-r--r--tests/testStreamOpen.cpp69
-rw-r--r--tests/testUtilities.cpp3
46 files changed, 834 insertions, 291 deletions
diff --git a/METADATA b/METADATA
index b003168f..ab5c8ac7 100644
--- a/METADATA
+++ b/METADATA
@@ -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
diff --git a/NOTICE b/NOTICE
new file mode 100644
index 00000000..d6456956
--- /dev/null
+++ b/NOTICE
@@ -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){