OboeTester: add glitch test
diff --git a/apps/OboeTester/app/src/main/AndroidManifest.xml b/apps/OboeTester/app/src/main/AndroidManifest.xml
index ee3f838..d632de6 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="5"
-    android:versionName="1.3.01">
+    android:versionCode="6"
+    android:versionName="1.3.02">
     <!-- versionCode and versionName also have to be updated in build.gradle -->
 
     <uses-feature android:name="android.hardware.microphone" android:required="true" />
@@ -73,6 +73,12 @@
             android:screenOrientation="portrait">
         </activity>
 
+        <activity
+            android:name="com.google.sample.oboe.manualtest.GlitchActivity"
+            android:label="@string/title_activity_glitches"
+            android:screenOrientation="portrait">
+        </activity>
+
         <service
             android:name="com.google.sample.oboe.manualtest.AudioMidiTester"
             android:permission="android.permission.BIND_MIDI_DEVICE_SERVICE">
diff --git a/apps/OboeTester/app/src/main/cpp/FullDuplexAnalyzer.cpp b/apps/OboeTester/app/src/main/cpp/FullDuplexAnalyzer.cpp
new file mode 100644
index 0000000..428ce5a
--- /dev/null
+++ b/apps/OboeTester/app/src/main/cpp/FullDuplexAnalyzer.cpp
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "common/OboeDebug.h"
+#include "FullDuplexAnalyzer.h"
+
+oboe::Result  FullDuplexAnalyzer::start() {
+    getLoopbackProcessor()->reset();
+    return FullDuplexStream::start();
+}
+
+oboe::DataCallbackResult FullDuplexAnalyzer::onBothStreamsReady(
+        const void *inputData,
+        int   numInputFrames,
+        void *outputData,
+        int   numOutputFrames) {
+    // TODO Pull up into superclass
+    // reset analyzer if we miss some input data
+    if (numInputFrames < numOutputFrames) {
+        LOGD("numInputFrames (%4d) < numOutputFrames (%4d) so reset analyzer",
+             numInputFrames, numOutputFrames);
+        getLoopbackProcessor()->reset();
+    } else {
+        float *inputFloat = (float *) inputData;
+        float *outputFloat = (float *) outputData;
+        int32_t outputStride = getOutputStream()->getChannelCount();
+
+        (void) getLoopbackProcessor()->process(inputFloat, getInputStream()->getChannelCount(),
+                                       outputFloat, outputStride,
+                                       numOutputFrames);
+
+        // zero out remainder of output array
+        int32_t framesLeft = numOutputFrames - numInputFrames;
+        outputFloat += numOutputFrames * outputStride;
+
+        if (framesLeft > 0) {
+            memset(outputFloat, 0, framesLeft * getOutputStream()->getBytesPerFrame());
+        }
+    }
+    return oboe::DataCallbackResult::Continue;
+};
diff --git a/apps/OboeTester/app/src/main/cpp/FullDuplexAnalyzer.h b/apps/OboeTester/app/src/main/cpp/FullDuplexAnalyzer.h
new file mode 100644
index 0000000..0b7ce9f
--- /dev/null
+++ b/apps/OboeTester/app/src/main/cpp/FullDuplexAnalyzer.h
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef OBOETESTER_FULL_DUPLEX_ANALYZER_H
+#define OBOETESTER_FULL_DUPLEX_ANALYZER_H
+
+#include <unistd.h>
+#include <sys/types.h>
+
+#include "oboe/Oboe.h"
+#include "FullDuplexStream.h"
+#include "LatencyAnalyzer.h"
+
+class FullDuplexAnalyzer : public FullDuplexStream {
+public:
+    FullDuplexAnalyzer() {}
+
+    /**
+     * Called when data is available on both streams.
+     * Caller should override this method.
+     */
+    oboe::DataCallbackResult onBothStreamsReady(
+            const void *inputData,
+            int   numInputFrames,
+            void *outputData,
+            int   numOutputFrames
+    ) override;
+
+    oboe::Result start() override;
+
+    bool isDone() {
+        return false;
+    }
+
+    virtual LoopbackProcessor *getLoopbackProcessor() = 0;
+
+};
+
+
+#endif //OBOETESTER_FULL_DUPLEX_ANALYZER_H
+
diff --git a/apps/OboeTester/app/src/main/cpp/FullDuplexGlitches.cpp b/apps/OboeTester/app/src/main/cpp/FullDuplexGlitches.cpp
new file mode 100644
index 0000000..53cfa5b
--- /dev/null
+++ b/apps/OboeTester/app/src/main/cpp/FullDuplexGlitches.cpp
@@ -0,0 +1,18 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "common/OboeDebug.h"
+#include "FullDuplexGlitches.h"
diff --git a/apps/OboeTester/app/src/main/cpp/FullDuplexGlitches.h b/apps/OboeTester/app/src/main/cpp/FullDuplexGlitches.h
new file mode 100644
index 0000000..38fa057
--- /dev/null
+++ b/apps/OboeTester/app/src/main/cpp/FullDuplexGlitches.h
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef OBOETESTER_FULL_DUPLEX_GLITCHES_H
+#define OBOETESTER_FULL_DUPLEX_GLITCHES_H
+
+#include <unistd.h>
+#include <sys/types.h>
+
+#include "oboe/Oboe.h"
+#include "FullDuplexAnalyzer.h"
+#include "LatencyAnalyzer.h"
+
+class FullDuplexGlitches : public FullDuplexAnalyzer {
+public:
+    FullDuplexGlitches() {}
+
+    bool isDone() {
+        return false;
+    }
+
+    GlitchAnalyzer *getGlitchAnalyzer() {
+        return &mGlitchAnalyzer;
+    }
+
+    LoopbackProcessor *getLoopbackProcessor() override {
+        return (LoopbackProcessor *) &mGlitchAnalyzer;
+    }
+private:
+
+    GlitchAnalyzer  mGlitchAnalyzer;
+
+};
+
+
+#endif //OBOETESTER_FULL_DUPLEX_GLITCHES_H
+
diff --git a/apps/OboeTester/app/src/main/cpp/FullDuplexLatency.cpp b/apps/OboeTester/app/src/main/cpp/FullDuplexLatency.cpp
index edeb585..5bd7104 100644
--- a/apps/OboeTester/app/src/main/cpp/FullDuplexLatency.cpp
+++ b/apps/OboeTester/app/src/main/cpp/FullDuplexLatency.cpp
@@ -35,10 +35,12 @@
         int   numOutputFrames) {
     // reset analyzer if we miss some input data
     if (numInputFrames < numOutputFrames) {
-        LOGD("numInputFrames (%4d) < numOutputFrames (%4d) so reset analyzer",
-             numInputFrames, numOutputFrames);
+        LOGD("%s() numInputFrames (%4d) < numOutputFrames (%4d) so reset analyzer",
+             __func__, numInputFrames, numOutputFrames);
         mEchoAnalyzer.reset();
     } else {
+        LOGD("%s() numInputFrames = %4d, numOutputFrames = %4d",
+             __func__, numInputFrames, numOutputFrames);
         float *inputFloat = (float *) inputData;
         float *outputFloat = (float *) outputData;
         int32_t outputStride = getOutputStream()->getChannelCount();
diff --git a/apps/OboeTester/app/src/main/cpp/FullDuplexStream.cpp b/apps/OboeTester/app/src/main/cpp/FullDuplexStream.cpp
index f3d4c19..6f92cd4 100644
--- a/apps/OboeTester/app/src/main/cpp/FullDuplexStream.cpp
+++ b/apps/OboeTester/app/src/main/cpp/FullDuplexStream.cpp
@@ -33,8 +33,8 @@
         int32_t totalFramesRead = 0;
         do {
             oboe::ResultWithValue<int32_t> result = getInputStream()->read(mInputBuffer.get(),
-                                            numFrames,
-                                            0 /* timeout */);
+                                                                           numFrames,
+                                                                           0 /* timeout */);
             if (!result) {
                 // Ignore errors because input stream may not be started yet.
                 break;
@@ -54,8 +54,8 @@
     } else if (mCountCallbacksToDiscard > 0) {
         // Ignore. Allow the input to reach to equilibrium with the output.
         oboe::ResultWithValue<int32_t> result = getInputStream()->read(mInputBuffer.get(),
-                                        numFrames,
-                                        0 /* timeout */);
+                                                                       numFrames,
+                                                                       0 /* timeout */);
         if (!result) {
             LOGE("%s() read() returned %s\n", __func__, convertToText(result.error()));
             callbackResult = oboe::DataCallbackResult::Stop;
@@ -80,6 +80,10 @@
         }
     }
 
+    if (callbackResult == oboe::DataCallbackResult::Stop) {
+        getInputStream()->requestStop();
+    }
+
     return callbackResult;
 }
 
diff --git a/apps/OboeTester/app/src/main/cpp/LatencyAnalyzer.h b/apps/OboeTester/app/src/main/cpp/LatencyAnalyzer.h
index 05f8f45..d64a599 100644
--- a/apps/OboeTester/app/src/main/cpp/LatencyAnalyzer.h
+++ b/apps/OboeTester/app/src/main/cpp/LatencyAnalyzer.h
@@ -908,11 +908,39 @@
  * 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.
  */
-class SineAnalyzer : public LoopbackProcessor {
+class GlitchAnalyzer : public LoopbackProcessor {
 public:
 
+    int32_t getState() {
+        return mState;
+    }
+
+    int32_t getGlitchCount() {
+        return mGlitchCount;
+    }
+
+    double getSignalToNoiseDB() {
+        static const double threshold = 1.0e-10;
+        LOGD(LOOPBACK_RESULT_TAG "mMagnitude     = %8f", mMagnitude);
+        LOGD(LOOPBACK_RESULT_TAG "mPeakNoise     = %8f", mPeakNoise);
+        if (mMagnitude < threshold || mPeakNoise < threshold) {
+            return 0.0;
+        } else {
+            double amplitudeRatio = mMagnitude / mPeakNoise;
+            double signalToNoise = amplitudeRatio * amplitudeRatio;
+            double signalToNoiseDB = 10.0 * log(signalToNoise);
+            LOGD(LOOPBACK_RESULT_TAG "signalToNoiseDB     = %8f", signalToNoiseDB);
+            if (signalToNoiseDB < MIN_SNRATIO_DB) {
+                LOGD("ERROR - signal to noise ratio is too low! < %d dB. Adjust volume.",
+                     MIN_SNRATIO_DB);
+                setResult(ERROR_VOLUME_TOO_LOW);
+            }
+            return signalToNoiseDB;
+        }
+    }
+
     void analyze() override {
-        LOGD("SineAnalyzer ------------------");
+        LOGD("GlitchAnalyzer ------------------");
         LOGD(LOOPBACK_RESULT_TAG "peak.amplitude     = %8f", mPeakAmplitude);
         LOGD(LOOPBACK_RESULT_TAG "sine.magnitude     = %8f", mMagnitude);
         LOGD(LOOPBACK_RESULT_TAG "peak.noise         = %8f", mPeakNoise);
@@ -1051,12 +1079,8 @@
                     float absDiff = fabs(diff);
                     mMaxGlitchDelta = std::max(mMaxGlitchDelta, absDiff);
                     if (absDiff > mTolerance) {
-                        mGlitchCount++;
                         result = ERROR_GLITCHES;
-                        //LOGD("%5d: Got a glitch # %d, predicted = %f, actual = %f",
-                        //       mFrameCounter, mGlitchCount, predicted, sample);
-                        mState = STATE_IMMUNE;
-                        mDownCounter = mSinePeriod * PERIODS_IMMUNE;
+                        addGlitch();
                     }
 
                     // Track incoming signal and slowly adjust magnitude to account
@@ -1072,6 +1096,11 @@
                         // One pole averaging filter.
                         mMagnitude = (mMagnitude * (1.0 - coefficient)) + (magnitude * coefficient);
                         resetAccumulator();
+
+                        if (mMagnitude < mThreshold) {
+                            result = ERROR_GLITCHES;
+                            addGlitch();
+                        }
                     }
                 } break;
             }
@@ -1094,6 +1123,14 @@
         return result;
     }
 
+    void addGlitch() {
+        mGlitchCount++;
+//        LOGD("%5d: Got a glitch # %d, predicted = %f, actual = %f",
+//        mFrameCounter, mGlitchCount, predicted, sample);
+        mState = STATE_IMMUNE;
+        mDownCounter = mSinePeriod * PERIODS_IMMUNE;
+    }
+
     void resetAccumulator() {
         mFramesAccumulated = 0;
         mSinAccumulator = 0.0;
diff --git a/apps/OboeTester/app/src/main/cpp/NativeAudioContext.cpp b/apps/OboeTester/app/src/main/cpp/NativeAudioContext.cpp
index 2db3ffd..2cba670 100644
--- a/apps/OboeTester/app/src/main/cpp/NativeAudioContext.cpp
+++ b/apps/OboeTester/app/src/main/cpp/NativeAudioContext.cpp
@@ -520,3 +520,24 @@
         mFullDuplexLatency->setOutputStream(oboeStream);
     }
 }
+
+// ======================================================================= ActivityGlitches
+void ActivityGlitches::configureBuilder(bool isInput, oboe::AudioStreamBuilder &builder) {
+    ActivityFullDuplex::configureBuilder(isInput, builder);
+
+    if (mFullDuplexGlitches.get() == nullptr) {
+        mFullDuplexGlitches = std::make_unique<FullDuplexGlitches>();
+    }
+    if (!isInput) {
+        // only output uses a callback, input is polled
+        builder.setCallback(mFullDuplexGlitches.get());
+    }
+}
+
+void ActivityGlitches::finishOpen(bool isInput, oboe::AudioStream *oboeStream) {
+    if (isInput) {
+        mFullDuplexGlitches->setInputStream(oboeStream);
+    } else {
+        mFullDuplexGlitches->setOutputStream(oboeStream);
+    }
+}
diff --git a/apps/OboeTester/app/src/main/cpp/NativeAudioContext.h b/apps/OboeTester/app/src/main/cpp/NativeAudioContext.h
index 7983076..24db396 100644
--- a/apps/OboeTester/app/src/main/cpp/NativeAudioContext.h
+++ b/apps/OboeTester/app/src/main/cpp/NativeAudioContext.h
@@ -35,6 +35,7 @@
 #include "flowgraph/SawtoothOscillator.h"
 
 #include "FullDuplexEcho.h"
+#include "FullDuplexGlitches.h"
 #include "FullDuplexLatency.h"
 #include "FullDuplexStream.h"
 #include "InputStreamCallbackAnalyzer.h"
@@ -335,6 +336,9 @@
 
     void configureBuilder(bool isInput, oboe::AudioStreamBuilder &builder) override;
 
+    virtual int32_t getState() { return -1; }
+    virtual int32_t getResult() { return -1; }
+    virtual bool isAnalyzerDone() { return false; }
 };
 
 /**
@@ -378,7 +382,13 @@
         return mFullDuplexLatency->getLatencyAnalyzer();
     }
 
-    bool isAnalyzerDone() {
+    int32_t getState() override {
+        return getLatencyAnalyzer()->getState();
+    }
+    int32_t getResult() override {
+        return getLatencyAnalyzer()->getState();
+    }
+    bool isAnalyzerDone() override {
         return mFullDuplexLatency->isDone();
     }
 
@@ -390,6 +400,40 @@
 };
 
 /**
+ * Measure Glitches
+ */
+class ActivityGlitches : public ActivityFullDuplex {
+public:
+
+    oboe::Result startStreams() override {
+        return mFullDuplexGlitches->start();
+    }
+
+    void configureBuilder(bool isInput, oboe::AudioStreamBuilder &builder) override;
+
+    GlitchAnalyzer *getGlitchAnalyzer() {
+        LOGD("%s() mFullDuplexGlitches = %p", __func__, mFullDuplexGlitches.get());
+        return mFullDuplexGlitches->getGlitchAnalyzer();
+    }
+
+    int32_t getState() override {
+        return getGlitchAnalyzer()->getState();
+    }
+    int32_t getResult() override {
+        return getGlitchAnalyzer()->getResult();
+    }
+    bool isAnalyzerDone() override {
+        return mFullDuplexGlitches->isDone();
+    }
+
+protected:
+    void finishOpen(bool isInput, oboe::AudioStream *oboeStream) override;
+
+private:
+    std::unique_ptr<FullDuplexGlitches>   mFullDuplexGlitches{};
+};
+
+/**
  * Switch between various
  */
 class NativeAudioContext {
@@ -423,6 +467,9 @@
             case ActivityType::RoundTripLatency:
                 currentActivity = &mActivityRoundTripLatency;
                 break;
+            case ActivityType::Glitches:
+                currentActivity = &mActivityGlitches;
+                break;
         }
     }
 
@@ -436,6 +483,7 @@
     ActivityRecording            mActivityRecording;
     ActivityEcho                 mActivityEcho;
     ActivityRoundTripLatency     mActivityRoundTripLatency;
+    ActivityGlitches             mActivityGlitches;
 
 private:
 
@@ -448,6 +496,7 @@
         RecordPlay = 3,
         Echo = 4,
         RoundTripLatency = 5,
+        Glitches = 6,
     };
 
     ActivityType                 mActivityType = ActivityType::Undefined;
diff --git a/apps/OboeTester/app/src/main/cpp/jni-bridge.cpp b/apps/OboeTester/app/src/main/cpp/jni-bridge.cpp
index caf7950..ea31462 100644
--- a/apps/OboeTester/app/src/main/cpp/jni-bridge.cpp
+++ b/apps/OboeTester/app/src/main/cpp/jni-bridge.cpp
@@ -409,7 +409,7 @@
     engine.getCurrentActivity()->setChannelEnabled(channelIndex, enabled);
 }
 
-
+// ==========================================================================
 JNIEXPORT void JNICALL
 Java_com_google_sample_oboe_manualtest_TestAudioActivity_setActivityType(JNIEnv *env,
                                                                          jobject instance,
@@ -417,6 +417,7 @@
     engine.setActivityType(activityType);
 }
 
+// ==========================================================================
 JNIEXPORT void JNICALL
 Java_com_google_sample_oboe_manualtest_EchoActivity_setDelayTime(JNIEnv *env,
                                                                          jobject instance,
@@ -424,24 +425,13 @@
     engine.setDelayTime(delayTimeSeconds);
 }
 
+// ==========================================================================
 JNIEXPORT jint JNICALL
 Java_com_google_sample_oboe_manualtest_RoundTripLatencyActivity_getAnalyzerProgress(JNIEnv *env,
                                                                                     jobject instance) {
     return engine.mActivityRoundTripLatency.getLatencyAnalyzer()->getProgress();
 }
 
-JNIEXPORT jint JNICALL
-Java_com_google_sample_oboe_manualtest_RoundTripLatencyActivity_getAnalyzerState(JNIEnv *env,
-                                                                                    jobject instance) {
-    return engine.mActivityRoundTripLatency.getLatencyAnalyzer()->getState();
-}
-
-JNIEXPORT jboolean JNICALL
-Java_com_google_sample_oboe_manualtest_RoundTripLatencyActivity_isAnalyzerDone(JNIEnv *env,
-                                                                                    jobject instance) {
-    return engine.mActivityRoundTripLatency.isAnalyzerDone();
-}
-
 JNIEXPORT jdouble JNICALL
 Java_com_google_sample_oboe_manualtest_RoundTripLatencyActivity_getMeasuredLatency(JNIEnv *env,
                                                                                    jobject instance) {
@@ -454,10 +444,41 @@
     return engine.mActivityRoundTripLatency.getLatencyAnalyzer()->getMeasuredConfidence();
 }
 
+// ==========================================================================
 JNIEXPORT jint JNICALL
-Java_com_google_sample_oboe_manualtest_RoundTripLatencyActivity_getMeasuredResult(JNIEnv *env,
-                                                                                      jobject instance) {
+Java_com_google_sample_oboe_manualtest_AnalyzerActivity_getAnalyzerState(JNIEnv *env,
+                                                                         jobject instance) {
+    return ((ActivityFullDuplex *)engine.getCurrentActivity())->getState();
+}
+
+JNIEXPORT jboolean JNICALL
+Java_com_google_sample_oboe_manualtest_AnalyzerActivity_isAnalyzerDone(JNIEnv *env,
+                                                                       jobject instance) {
+    return ((ActivityFullDuplex *)engine.getCurrentActivity())->isAnalyzerDone();
+}
+
+JNIEXPORT jint JNICALL
+Java_com_google_sample_oboe_manualtest_AnalyzerActivity_getMeasuredResult(JNIEnv *env,
+                                                                          jobject instance) {
     return engine.mActivityRoundTripLatency.getLatencyAnalyzer()->getResult();
 }
 
+
+// ==========================================================================
+JNIEXPORT jint JNICALL
+Java_com_google_sample_oboe_manualtest_GlitchActivity_getGlitchCount(JNIEnv *env,
+                                                                     jobject instance) {
+    LOGD("%s() engine.mActivityGlitches.getGlitchAnalyzer() = %p",
+         __func__, engine.mActivityGlitches.getGlitchAnalyzer());
+    return engine.mActivityGlitches.getGlitchAnalyzer()->getGlitchCount();
 }
+
+JNIEXPORT jdouble JNICALL
+Java_com_google_sample_oboe_manualtest_GlitchActivity_getSignalToNoiseDB(JNIEnv *env,
+                                                                         jobject instance) {
+    LOGD("%s() engine.mActivityGlitches.getGlitchAnalyzer() = %p",
+         __func__, engine.mActivityGlitches.getGlitchAnalyzer());
+    return engine.mActivityGlitches.getGlitchAnalyzer()->getSignalToNoiseDB();
+}
+
+}
\ No newline at end of file
diff --git a/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/AnalyzerActivity.java b/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/AnalyzerActivity.java
new file mode 100644
index 0000000..1dc3f5e
--- /dev/null
+++ b/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/AnalyzerActivity.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2018 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.
+ */
+
+package com.google.sample.oboe.manualtest;
+
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.view.View;
+import android.widget.Button;
+import android.widget.TextView;
+
+/**
+ * Activity to measure latency on a full duplex stream.
+ */
+public class AnalyzerActivity extends TestInputActivity {
+
+    AudioOutputTester mAudioOutTester;
+
+    // Note that these string must match the enum result_code in LatencyAnalyzer.h
+    String resultCodeToString(int resultCode) {
+        switch (resultCode) {
+            case 0:
+                return "OK";
+            case -99:
+                return "ERROR_NOISY";
+            case -98:
+                return "ERROR_VOLUME_TOO_LOW";
+            case -97:
+                return "ERROR_VOLUME_TOO_HIGH";
+            case -96:
+                return "ERROR_CONFIDENCE";
+            case -95:
+                return "ERROR_INVALID_STATE";
+            case -94:
+                return "ERROR_GLITCHES";
+            case -93:
+                return "ERROR_NO_LOCK";
+            default:
+                return "UNKNOWN";
+        }
+    }
+
+    public native int getAnalyzerState();
+    public native boolean isAnalyzerDone();
+    public native int getMeasuredResult();
+
+}
diff --git a/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/GlitchActivity.java b/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/GlitchActivity.java
new file mode 100644
index 0000000..10bff7a
--- /dev/null
+++ b/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/GlitchActivity.java
@@ -0,0 +1,204 @@
+/*
+ * Copyright 2018 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.
+ */
+
+package com.google.sample.oboe.manualtest;
+
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.view.View;
+import android.widget.Button;
+import android.widget.TextView;
+
+/**
+ * Activity to measure the number of glitches.
+ */
+public class GlitchActivity extends AnalyzerActivity {
+    private TextView mAnalyzerView;
+    private Button mStartButton;
+    private Button mStopButton;
+
+    // These must match the values in LatencyAnalyzer.h
+    final static int STATE_IDLE = 0;
+    final static int STATE_MEASURE_NOISE =1;
+    final static int STATE_IMMUNE = 2;
+    final static int STATE_WAITING_FOR_SIGNAL = 3;
+    final static int STATE_WAITING_FOR_LOCK = 4;
+    final static int STATE_LOCKED = 5;
+
+
+    // Note that these string must match the enum result_code in LatencyAnalyzer.h
+    String stateToString(int resultCode) {
+        switch (resultCode) {
+            case STATE_IDLE:
+                return "IDLE";
+            case STATE_MEASURE_NOISE:
+                return "MEASURE_NOISE";
+            case STATE_IMMUNE:
+                return "IMMUNE";
+            case STATE_WAITING_FOR_SIGNAL:
+                return "WAITING_FOR_SIGNAL";
+            case STATE_WAITING_FOR_LOCK:
+                return "WAITING_FOR_LOCK";
+            case STATE_LOCKED:
+                return "RUNNING";
+            default:
+                return "UNKNOWN";
+        }
+    }
+
+    // Periodically query the status of the stream.
+    protected class GlitchSniffer {
+        public static final int SNIFFER_UPDATE_PERIOD_MSEC = 100;
+        public static final int SNIFFER_UPDATE_DELAY_MSEC = 200;
+        long timeStarted = 0;
+        long timeSinceLock = 0;
+        int lastGlitchCount = 0;
+        int previousState = STATE_IDLE;
+        boolean gotLock = false;
+
+        private Handler mHandler = new Handler(Looper.getMainLooper()); // UI thread
+
+        // Display status info for the stream.
+        private Runnable runnableCode = new Runnable() {
+            @Override
+            public void run() {
+                int state = getAnalyzerState();
+                int glitchCount = getGlitchCount();
+                double signalToNoiseDB = getSignalToNoiseDB();
+
+                boolean locked = (state == STATE_LOCKED);
+                if (locked && (previousState != STATE_LOCKED)) {
+                    timeSinceLock = System.currentTimeMillis();
+                }
+                previousState = state;
+                gotLock = gotLock || locked;
+                if (glitchCount > lastGlitchCount) {
+                    timeSinceLock = System.currentTimeMillis();
+                    lastGlitchCount = glitchCount;
+                }
+
+                long now = System.currentTimeMillis();
+                double totalSeconds = (now - timeStarted) / 1000.0;
+
+                StringBuffer message = new StringBuffer();
+                message.append("state = " + state + " = " + stateToString(state) + "\n");
+                message.append(String.format("signal to noise = %5.1f dB\n", signalToNoiseDB));
+                message.append(String.format("Time total = %8.2f seconds\n", totalSeconds));
+                double goodSeconds = (locked && (timeSinceLock > 0))
+                        ? ((now - timeSinceLock) / 1000.0)
+                        : 0.0;
+                message.append(String.format("Time without glitches = %8.2f seconds\n",
+                        goodSeconds));
+                if (gotLock) {
+                    message.append("glitchCount = " + glitchCount+ "\n");
+                }
+                setAnalyzerText(message.toString());
+
+                // Reschedule so this task repeats
+                mHandler.postDelayed(runnableCode, SNIFFER_UPDATE_PERIOD_MSEC);
+            }
+        };
+
+        private void startSniffer() {
+            // Start the initial runnable task by posting through the handler
+            mHandler.postDelayed(runnableCode, SNIFFER_UPDATE_DELAY_MSEC);
+            timeStarted = System.currentTimeMillis();
+            timeSinceLock = 0;
+            lastGlitchCount = 0;
+            gotLock = false;
+        }
+
+        private void stopSniffer() {
+            if (mHandler != null) {
+                mHandler.removeCallbacks(runnableCode);
+            }
+        }
+    }
+
+    private GlitchSniffer mGlitchSniffer = new GlitchSniffer();
+
+    native int getGlitchCount();
+    native double getSignalToNoiseDB();
+
+    private void setAnalyzerText(String s) {
+        mAnalyzerView.setText(s);
+    }
+
+    @Override
+    protected void inflateActivity() {
+        setContentView(R.layout.activity_glitches);
+    }
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        mStartButton = (Button) findViewById(R.id.button_start);
+        mStopButton = (Button) findViewById(R.id.button_stop);
+        mStopButton.setEnabled(false);
+        mAnalyzerView = (TextView) findViewById(R.id.text_analyzer_result);
+        updateEnabledWidgets();
+        mAudioOutTester = addAudioOutputTester();
+
+        hideSettingsViews();
+    }
+
+    @Override
+    protected void onStart() {
+        super.onStart();
+        setActivityType(ACTIVITY_GLITCHES);
+    }
+
+    @Override
+    protected void onStop() {
+        mGlitchSniffer.stopSniffer();
+        super.onStop();
+    }
+
+    public void onStartAudioTest(View view) {
+        openAudio();
+        startAudio();
+        mStartButton.setEnabled(false);
+        mStopButton.setEnabled(true);
+        mGlitchSniffer.startSniffer();
+    }
+
+    public void onCancel(View view) {
+        stopAudioTest();
+    }
+
+    // Call on UI thread
+    public void onStopAudioTest(View view) {
+        stopAudioTest();
+    }
+
+    public void stopAudioTest() {
+        mGlitchSniffer.stopSniffer();
+        mStartButton.setEnabled(true);
+        mStopButton.setEnabled(false);
+        stopAudio();
+        closeAudio();
+    }
+
+    @Override
+    boolean isOutput() {
+        return false;
+    }
+
+    @Override
+    public void setupEffects(int sessionId) {
+    }
+}
diff --git a/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/MainActivity.java b/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/MainActivity.java
index 6b7f0cb..5528b93 100644
--- a/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/MainActivity.java
+++ b/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/MainActivity.java
@@ -125,6 +125,12 @@
         startActivity(intent);
     }
 
+    public void onLaunchGlitchTest(View view) {
+        updateCallbackSize();
+        Intent intent = new Intent(this, GlitchActivity.class);
+        startActivity(intent);
+    }
+
     public void onUseCallbackClicked(View view) {
         CheckBox checkBox = (CheckBox) view;
         OboeAudioStream.setUseCallback(checkBox.isChecked());
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 a66a8bd..cba159b 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
@@ -27,39 +27,14 @@
 /**
  * Activity to measure latency on a full duplex stream.
  */
-public class RoundTripLatencyActivity extends TestInputActivity {
+public class RoundTripLatencyActivity extends AnalyzerActivity {
 
     private static final int STATE_GOT_DATA = 5; // Defined in LatencyAnalyzer.h
-    
-    AudioOutputTester mAudioOutTester;
+
     private TextView mAnalyzerView;
     private Button mMeasureButton;
     private Button mCancelButton;
 
-    // Note that these string must match the enum result_code in LatencyAnalyzer.h
-    String resultCodeToString(int resultCode) {
-        switch (resultCode) {
-            case 0:
-                return "OK";
-            case -99:
-                return "ERROR_NOISY";
-            case -98:
-                return "ERROR_VOLUME_TOO_LOW";
-            case -97:
-                return "ERROR_VOLUME_TOO_HIGH";
-            case -96:
-                return "ERROR_CONFIDENCE";
-            case -95:
-                return "ERROR_INVALID_STATE";
-            case -94:
-                return "ERROR_GLITCHES";
-            case -93:
-                return "ERROR_NO_LOCK";
-            default:
-                return "UNKNOWN";
-        }
-    }
-
     // Periodically query the status of the stream.
     protected class LatencySniffer {
         public static final int SNIFFER_UPDATE_PERIOD_MSEC = 150;
@@ -122,10 +97,7 @@
 
     private LatencySniffer mLatencySniffer = new LatencySniffer();
 
-    native int getAnalyzerState();
     native int getAnalyzerProgress();
-    native boolean isAnalyzerDone();
-    native int getMeasuredResult();
     native double getMeasuredLatency();
     native double getMeasuredConfidence();
 
@@ -146,6 +118,8 @@
         mAnalyzerView = (TextView) findViewById(R.id.text_analyzer_result);
         updateEnabledWidgets();
         mAudioOutTester = addAudioOutputTester();
+
+        hideSettingsViews();
     }
 
     @Override
@@ -175,10 +149,10 @@
     // Call on UI thread
     public void stopAudioTest() {
         mLatencySniffer.stopSniffer();
-        stopAudio();
-        closeAudio();
         mMeasureButton.setEnabled(true);
         mCancelButton.setEnabled(false);
+        stopAudio();
+        closeAudio();
     }
 
     @Override
diff --git a/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/StreamConfigurationView.java b/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/StreamConfigurationView.java
index 5d108c6..d4cc863 100644
--- a/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/StreamConfigurationView.java
+++ b/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/StreamConfigurationView.java
@@ -72,15 +72,23 @@
     private View.OnClickListener mToggleListener = new View.OnClickListener() {
         public void onClick(View v) {
             if (mOptionTable.isShown()) {
-                mOptionTable.setVisibility(View.GONE);
-                mOptionExpander.setText(mShowSettingsText);
+                hideSettingsView();
             } else {
-                mOptionTable.setVisibility(View.VISIBLE);
-                mOptionExpander.setText(mHideSettingsText);
+                showSettingsView();
             }
         }
     };
 
+    public void showSettingsView() {
+        mOptionTable.setVisibility(View.VISIBLE);
+        mOptionExpander.setText(mHideSettingsText);
+    }
+
+    public void hideSettingsView() {
+        mOptionTable.setVisibility(View.GONE);
+        mOptionExpander.setText(mShowSettingsText);
+    }
+
     public StreamConfigurationView(Context context) {
         super(context);
         initializeViews(context);
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 119ad1c..16dc642 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
@@ -54,6 +54,7 @@
     public static final int ACTIVITY_RECORD_PLAY = 3;
     public static final int ACTIVITY_ECHO = 4;
     public static final int ACTIVITY_RT_LATENCY = 5;
+    public static final int ACTIVITY_GLITCHES = 6;
 
     private int mState = STATE_CLOSED;
     protected String audioManagerSampleRate;
@@ -130,6 +131,12 @@
         findAudioCommon();
     }
 
+    public void hideSettingsViews() {
+        for (StreamContext streamContext : mStreamContexts) {
+            streamContext.configurationView.hideSettingsView();
+        }
+    }
+
     @Override
     protected void onStop() {
         Log.i(TAG, "onStop() called so stopping audio =========================");
diff --git a/apps/OboeTester/app/src/main/res/layout/activity_glitches.xml b/apps/OboeTester/app/src/main/res/layout/activity_glitches.xml
new file mode 100644
index 0000000..4b15153
--- /dev/null
+++ b/apps/OboeTester/app/src/main/res/layout/activity_glitches.xml
@@ -0,0 +1,58 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical"
+    android:paddingBottom="@dimen/activity_vertical_margin"
+    android:paddingLeft="@dimen/activity_horizontal_margin"
+    android:paddingRight="@dimen/activity_horizontal_margin"
+    android:paddingTop="@dimen/activity_vertical_margin"
+    tools:context="com.google.sample.oboe.manualtest.GlitchActivity">
+
+    <com.google.sample.oboe.manualtest.StreamConfigurationView
+        android:id="@+id/inputStreamConfiguration"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:gravity="center"
+        android:orientation="horizontal" />
+
+    <com.google.sample.oboe.manualtest.StreamConfigurationView
+        android:id="@+id/outputStreamConfiguration"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:gravity="center"
+        android:orientation="horizontal" />
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:orientation="horizontal">
+
+        <Button
+            android:id="@+id/button_start"
+            android:layout_width="0dp"
+            android:layout_weight="1"
+            android:layout_height="wrap_content"
+            android:onClick="onStartAudioTest"
+            android:text="@string/startAudio" />
+
+        <Button
+            android:id="@+id/button_stop"
+            android:layout_width="0dp"
+            android:layout_weight="1"
+            android:layout_height="wrap_content"
+            android:onClick="onStopAudioTest"
+            android:text="@string/stopAudio" />
+    </LinearLayout>
+
+    <TextView
+        android:id="@+id/text_analyzer_result"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:lines="8"
+        android:text="@string/use_loopback"
+        android:textSize="18sp"
+        android:textStyle="bold" />
+
+</LinearLayout>
diff --git a/apps/OboeTester/app/src/main/res/layout/activity_main.xml b/apps/OboeTester/app/src/main/res/layout/activity_main.xml
index fcd2864..a5392a8 100644
--- a/apps/OboeTester/app/src/main/res/layout/activity_main.xml
+++ b/apps/OboeTester/app/src/main/res/layout/activity_main.xml
@@ -58,6 +58,13 @@
         android:onClick="onLaunchRoundTripLatency"
         android:text="Round Trip Latency" />
 
+    <Button
+        android:id="@+id/button_glitches"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:onClick="onLaunchGlitchTest"
+        android:text="Test Glitches" />
+
     <LinearLayout
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
diff --git a/apps/OboeTester/app/src/main/res/values/strings.xml b/apps/OboeTester/app/src/main/res/values/strings.xml
index 9f5929f..f415920 100644
--- a/apps/OboeTester/app/src/main/res/values/strings.xml
+++ b/apps/OboeTester/app/src/main/res/values/strings.xml
@@ -19,7 +19,7 @@
     <string name="auto_select">Auto select</string>
     <string name="hint_hide_settings">Hide Settings</string>
     <string name="hint_show_settings">Show Settings</string>
-    <string name="use_loopback">Hide Settings to make more room.\nRequires a loopback adapter!</string>
+    <string name="use_loopback">Click Show Settings to configure stream.\nRequires a loopback adapter!</string>
 
     <string name="output_prompt">Choose an Output Mode</string>
     <string-array name="output_modes">
@@ -98,6 +98,7 @@
     <string name="title_activity_recorder">Recorder</string>
     <string name="title_activity_echo">Echo Input to Output</string>
     <string name="title_activity_rt_latency">Round Trip Latency</string>
+    <string name="title_activity_glitches">Test Glitches</string>
 
     <string name="need_record_audio_permission">"This app needs RECORD_AUDIO permission"</string>