Upgrade oboe to 386f057966b9640a25b63e7ac552d30d66b4e49d

Test: make
Change-Id: I89a4f2207c9fc93473f1fc68f206bdacd375eb17
diff --git a/METADATA b/METADATA
index b003168..ab5c8ac 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 0000000..e69de29
--- /dev/null
+++ b/MODULE_LICENSE_APACHE2
diff --git a/NOTICE b/NOTICE
new file mode 100644
index 0000000..d645695
--- /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 f2c81c1..8de21f9 100644
--- a/apps/OboeTester/app/CMakeLists.txt
+++ b/apps/OboeTester/app/CMakeLists.txt
@@ -31,4 +31,4 @@
 # 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 51911fe..e3aea74 100644
--- a/apps/OboeTester/app/build.gradle
+++ b/apps/OboeTester/app/build.gradle
@@ -7,8 +7,8 @@
         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 9bbc5a6..37a75b9 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 7dd374f..c473540 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 @@
         void *audioData,
         int numFrames) {
 
-    if (!mSchedulerChecked) {
-        mScheduler = sched_getscheduler(gettid());
-        mSchedulerChecked = true;
-    }
+    printScheduler();
 
     if (mAudioSink != nullptr) {
         mAudioSink->read(audioData, numFrames);
@@ -39,6 +37,3 @@
     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 982d099..0aaf429 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 @@
  * 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 @@
             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 c8a99ac..2da2dfd 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 d04f09a..f9290e1 100644
--- a/apps/OboeTester/app/src/main/cpp/InputStreamCallbackAnalyzer.cpp
+++ b/apps/OboeTester/app/src/main/cpp/InputStreamCallbackAnalyzer.cpp
@@ -23,6 +23,8 @@
         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 42ffbfb..fc26b1f 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 0026610..3b51177 100644
--- a/apps/OboeTester/app/src/main/cpp/NativeAudioContext.cpp
+++ b/apps/OboeTester/app/src/main/cpp/NativeAudioContext.cpp
@@ -111,7 +111,6 @@
     for (auto entry : mOboeStreams) {
         oboe::AudioStream *oboeStream = entry.second.get();
         result = oboeStream->requestPause();
-        printScheduler();
     }
     return result;
 }
@@ -122,7 +121,6 @@
     for (auto entry : mOboeStreams) {
         oboe::AudioStream *oboeStream = entry.second.get();
         result = oboeStream->requestStop();
-        printScheduler();
     }
     return result;
 }
@@ -237,6 +235,7 @@
 
     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 d5cb994..bd445d1 100644
--- a/apps/OboeTester/app/src/main/cpp/NativeAudioContext.h
+++ b/apps/OboeTester/app/src/main/cpp/NativeAudioContext.h
@@ -224,13 +224,6 @@
 
     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 0000000..aab60ab
--- /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 0000000..ec01fe5
--- /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 7a496f3..4d1bd6f 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 @@
             : 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 @@
         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 @@
         } 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 @@
      * @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 @@
             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 @@
                 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 @@
 
             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 @@
                         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 @@
             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 @@
             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 @@
 
     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 @@
     }
 
     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 @@
     }
 
     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 @@
         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 @@
         }
     }
 
-
     int32_t getLastGlitch(float *buffer, int32_t length) {
         return mInfiniteRecording.readFrom(buffer, mLastGlitchPosition - 32, length);
     }
@@ -382,10 +402,10 @@
 
     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 @@
     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 @@
 };
 
 
-#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 d0d1aa8..3178c6e 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 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 @@
         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 @@
     }
 
     if (sumSquares >= 1.0e-9) {
-        correlation = (float) (2.0 * sumProducts / sumSquares);
+        correlation = 2.0 * sumProducts / sumSquares;
     }
     return correlation;
 }
@@ -92,15 +101,9 @@
 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 @@
         // 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 @@
      * 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 @@
     }
 
 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 @@
 
 static int measureLatencyFromPulse(AudioRecording &recorded,
                                    AudioRecording &pulse,
-                                   int32_t framesPerEncodedBit,
                                    LatencyReport *report) {
 
     report->latencyInFrames = 0;
@@ -205,7 +208,7 @@
 
     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 @@
         }
     }
     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 @@
 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 @@
         ERROR_NO_LOCK
     };
 
-    virtual void onStartTest() {
+    virtual void prepareToTest() {
         reset();
     }
 
@@ -269,7 +282,7 @@
     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 @@
         }
     }
 
-    virtual void analyze() = 0;
+    virtual std::string analyze() = 0;
 
     virtual void printStatus() {};
 
@@ -320,11 +333,11 @@
         mSampleRate = sampleRate;
     }
 
-    int32_t getSampleRate() {
+    int32_t getSampleRate() const {
         return mSampleRate;
     }
 
-    int32_t getResetCount() {
+    int32_t getResetCount() const {
         return mResetCount;
     }
 
@@ -348,7 +361,7 @@
     LatencyAnalyzer() : LoopbackProcessor() {}
     virtual ~LatencyAnalyzer() = default;
 
-    virtual int32_t getProgress() = 0;
+    virtual int32_t getProgress() const = 0;
 
     virtual int getState() = 0;
 
@@ -380,7 +393,6 @@
                 / (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 @@
 
     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 @@
         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 @@
         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 @@
         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 @@
                     mBackgroundRMS = sqrtf(mBackgroundSumSquare / mBackgroundSumCount);
                     nextState = STATE_IN_PULSE;
                     mPulseCursor = 0;
-                    LOGD("LatencyAnalyzer state => STATE_SENDING_PULSE");
                 }
                 break;
 
@@ -517,7 +533,6 @@
                 // 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 @@
     };
 
     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 @@
 
     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 b3d12b3..0a4bd5b 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 @@
             , mCursor(samplesPerPulse) {
     }
 
+    virtual ~ManchesterEncoder() = default;
+
     /**
      * This will be called when the next byte is needed.
      * @return
@@ -64,10 +66,10 @@
     /**
      * 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 9139e42..4b3b4e7 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 @@
     }
 
     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 4aedbe0..1c4938c 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 @@
         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 @@
     }
 
 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 f0623cc..030050b 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 @@
     : 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 b1ba949..f2eba84 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 @@
         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 @@
         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 62f6f80..f86368b 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 @@
             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 782950a..7edcd38 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 @@
     // 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 @@
         // 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 @@
             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 8aa6434..bd7fff5 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 @@
     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 @@
         }
     }
 
+    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 @@
         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) {
-            streamContext.tester.close();
+            if (!streamContext.isInput()) {
+                streamContext.tester.close();
+            }
+        }
+        for (StreamContext streamContext : mStreamContexts) {
+            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 6ad0480..273ca4a 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 @@
         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 5cf9124..de3ca26 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 a1ea0a8..80c5da4 100644
--- a/include/oboe/AudioStreamBuilder.h
+++ b/include/oboe/AudioStreamBuilder.h
@@ -198,10 +198,11 @@
 
 
     /**
-     * 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 @@
     }
 
     /**
-     * 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 5ae41f7..b1dcf41 100644
--- a/samples/drumthumper/src/main/cpp/DrumPlayerJNI.cpp
+++ b/samples/drumthumper/src/main/cpp/DrumPlayerJNI.cpp
@@ -45,11 +45,17 @@
  * 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 @@
  * 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 @@
     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_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 8d50510..62c8dbf 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 @@
                                         // 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 @@
     }
 
     fun setupAudioStream() {
-        setupAudioStreamNative(SAMPLE_RATE, NUM_PLAY_CHANNELS)
+        setupAudioStreamNative(NUM_PLAY_CHANNELS)
+    }
+
+    fun startAudioStream() {
+        startAudioStreamNative();
     }
 
     fun teardownAudioStream() {
@@ -89,7 +91,7 @@
             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 @@
         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 eb90cb8..2bdd6c9 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 @@
 
         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 @@
                     Toast.LENGTH_LONG)
             toast.show()
         }
-    }
-
-    override fun onStart() {
-        super.onStart()
-
-        mDrumPlayer.setupAudioStream()
+        mDrumPlayer.startAudioStream()
 
         if (mUseDeviceChangeFallback) {
             mAudioMgr!!.registerAudioDeviceCallback(mDeviceListener, null)
@@ -232,11 +233,12 @@
 
         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 2c3d923..dd8c626 100644
--- a/samples/iolib/src/main/cpp/CMakeLists.txt
+++ b/samples/iolib/src/main/cpp/CMakeLists.txt
@@ -29,6 +29,7 @@
 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 ca3a905..a882b3a 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 @@
     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 9d61f28..c92ee78 100644
--- a/samples/iolib/src/main/cpp/player/SampleBuffer.h
+++ b/samples/iolib/src/main/cpp/player/SampleBuffer.h
@@ -38,6 +38,8 @@
     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 6110b29..2cbfae7 100644
--- a/samples/iolib/src/main/cpp/player/SimpleMultiPlayer.cpp
+++ b/samples/iolib/src/main/cpp/player/SimpleMultiPlayer.cpp
@@ -34,7 +34,7 @@
 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 @@
     // 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 @@
                 "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 @@
     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::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 3e0a1c7..d8c9877 100644
--- a/samples/iolib/src/main/cpp/player/SimpleMultiPlayer.h
+++ b/samples/iolib/src/main/cpp/player/SimpleMultiPlayer.h
@@ -41,10 +41,13 @@
     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 @@
     // 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 9f493ea..96f0c39 100644
--- a/src/aaudio/AudioStreamAAudio.cpp
+++ b/src/aaudio/AudioStreamAAudio.cpp
@@ -118,7 +118,7 @@
     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 @@
     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 @@
     // 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 6267328..0df224a 100644
--- a/src/aaudio/AudioStreamAAudio.h
+++ b/src/aaudio/AudioStreamAAudio.h
@@ -110,6 +110,9 @@
 
 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 7ddc0e6..6f0ee5b 100644
--- a/src/common/DataConversionFlowGraph.cpp
+++ b/src/common/DataConversionFlowGraph.cpp
@@ -98,8 +98,8 @@
                                 ? 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 @@
         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 @@
     }
     lastOutput->connect(&mSink->input);
 
-    mFramePosition = 0;
-
     return Result::OK;
 }
 
@@ -210,7 +208,6 @@
         mSourceCaller->setTimeoutNanos(timeoutNanos);
     }
     int32_t numRead = mSink->read(buffer, numFrames);
-    mFramePosition += numRead;
     return numRead;
 }
 
@@ -221,7 +218,6 @@
     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 5b8d3f6..0cde1f3 100644
--- a/src/common/DataConversionFlowGraph.h
+++ b/src/common/DataConversionFlowGraph.h
@@ -80,8 +80,6 @@
     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 c7e3ff9..9a62d7d 100644
--- a/src/flowgraph/FlowGraphNode.cpp
+++ b/src/flowgraph/FlowGraphNode.cpp
@@ -111,4 +111,4 @@
 
 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 0c92d7f..b1ae4bd 100644
--- a/src/flowgraph/SampleRateConverter.cpp
+++ b/src/flowgraph/SampleRateConverter.cpp
@@ -25,11 +25,17 @@
     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 5fb5c65..534df49 100644
--- a/src/flowgraph/SampleRateConverter.h
+++ b/src/flowgraph/SampleRateConverter.h
@@ -38,6 +38,8 @@
         return "SampleRateConverter";
     }
 
+    void reset() override;
+
 private:
 
     // Return true if there is a sample available.
@@ -48,9 +50,11 @@
 
     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 2026773..ecf030f 100644
--- a/src/flowgraph/resampler/README.md
+++ b/src/flowgraph/resampler/README.md
@@ -20,7 +20,7 @@
             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 e3bf7e8..8b2fa6d 100644
--- a/tests/testStreamOpen.cpp
+++ b/tests/testStreamOpen.cpp
@@ -24,18 +24,20 @@
 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 @@
         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 @@
     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 @@
         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 9945145..6b101be 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){