OboeTester: add glitch test
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