aaudio: add glitch detection to loopback, improve latency check

Also improve automatic loop gain control for latency testing.
Use ArgParser to parse command line options.

Bug: 38178592
Test: this is a test
Change-Id: Idfe8f25544a3e7c74ee15be008bf34e3a0741455
diff --git a/media/libaaudio/examples/loopback/jni/Android.mk b/media/libaaudio/examples/loopback/jni/Android.mk
index dc933e3..d78f286 100644
--- a/media/libaaudio/examples/loopback/jni/Android.mk
+++ b/media/libaaudio/examples/loopback/jni/Android.mk
@@ -4,7 +4,8 @@
 LOCAL_MODULE_TAGS := tests
 LOCAL_C_INCLUDES := \
     $(call include-path-for, audio-utils) \
-    frameworks/av/media/libaaudio/include
+    frameworks/av/media/libaaudio/include \
+    frameworks/av/media/libaaudio/examples/utils
 
 # NDK recommends using this kind of relative path instead of an absolute path.
 LOCAL_SRC_FILES:= ../src/loopback.cpp
diff --git a/media/libaaudio/examples/loopback/src/LoopbackAnalyzer.h b/media/libaaudio/examples/loopback/src/LoopbackAnalyzer.h
new file mode 100644
index 0000000..21cf341
--- /dev/null
+++ b/media/libaaudio/examples/loopback/src/LoopbackAnalyzer.h
@@ -0,0 +1,794 @@
+/*
+ * Copyright (C) 2017 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.
+ */
+
+/**
+ * Tools for measuring latency and for detecting glitches.
+ * These classes are pure math and can be used with any audio system.
+ */
+
+#ifndef AAUDIO_EXAMPLES_LOOPBACK_ANALYSER_H
+#define AAUDIO_EXAMPLES_LOOPBACK_ANALYSER_H
+
+#include <algorithm>
+#include <assert.h>
+#include <cctype>
+#include <math.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+// Tag for machine readable results as property = value pairs
+#define LOOPBACK_RESULT_TAG      "RESULT: "
+#define LOOPBACK_SAMPLE_RATE     48000
+
+#define MILLIS_PER_SECOND        1000
+
+#define MAX_ZEROTH_PARTIAL_BINS  40
+
+static const float s_Impulse[] = {
+        0.0f, 0.0f, 0.0f, 0.0f, 0.2f, // silence on each side of the impulse
+        0.5f, 0.9999f, 0.0f, -0.9999, -0.5f, // bipolar
+        -0.2f, 0.0f, 0.0f, 0.0f, 0.0f
+};
+
+class PseudoRandom {
+public:
+    PseudoRandom() {}
+    PseudoRandom(int64_t seed)
+            :    mSeed(seed)
+    {}
+
+    /**
+     * Returns the next random double from -1.0 to 1.0
+     *
+     * @return value from -1.0 to 1.0
+     */
+     double nextRandomDouble() {
+        return nextRandomInteger() * (0.5 / (((int32_t)1) << 30));
+    }
+
+    /** Calculate random 32 bit number using linear-congruential method. */
+    int32_t nextRandomInteger() {
+        // Use values for 64-bit sequence from MMIX by Donald Knuth.
+        mSeed = (mSeed * (int64_t)6364136223846793005) + (int64_t)1442695040888963407;
+        return (int32_t) (mSeed >> 32); // The higher bits have a longer sequence.
+    }
+
+private:
+    int64_t mSeed = 99887766;
+};
+
+static double calculateCorrelation(const float *a,
+                                   const float *b,
+                                   int windowSize)
+{
+    double correlation = 0.0;
+    double sumProducts = 0.0;
+    double sumSquares = 0.0;
+
+    // Correlate a against b.
+    for (int i = 0; i < windowSize; i++) {
+        float s1 = a[i];
+        float s2 = b[i];
+        // Use a normalized cross-correlation.
+        sumProducts += s1 * s2;
+        sumSquares += ((s1 * s1) + (s2 * s2));
+    }
+
+    if (sumSquares >= 0.00000001) {
+        correlation = (float) (2.0 * sumProducts / sumSquares);
+    }
+    return correlation;
+}
+
+static int calculateCorrelations(const float *haystack, int haystackSize,
+                                 const float *needle, int needleSize,
+                                 float *results, int resultSize)
+{
+    int maxCorrelations = haystackSize - needleSize;
+    int numCorrelations = std::min(maxCorrelations, resultSize);
+
+    for (int ic = 0; ic < numCorrelations; ic++) {
+        double correlation = calculateCorrelation(&haystack[ic], needle, needleSize);
+        results[ic] = correlation;
+    }
+
+    return numCorrelations;
+}
+
+/*==========================================================================================*/
+/**
+ * Scan until we get a correlation of a single scan that goes over the tolerance level,
+ * peaks then drops back down.
+ */
+static double findFirstMatch(const float *haystack, int haystackSize,
+                             const float *needle, int needleSize, double threshold  )
+{
+    int ic;
+    // How many correlations can we calculate?
+    int numCorrelations = haystackSize - needleSize;
+    double maxCorrelation = 0.0;
+    int peakIndex = -1;
+    double location = -1.0;
+    const double backThresholdScaler = 0.5;
+
+    for (ic = 0; ic < numCorrelations; ic++) {
+        double correlation = calculateCorrelation(&haystack[ic], needle, needleSize);
+
+        if( (correlation > maxCorrelation) ) {
+            maxCorrelation = correlation;
+            peakIndex = ic;
+        }
+
+        //printf("PaQa_FindFirstMatch: ic = %4d, correlation = %8f, maxSum = %8f\n",
+        //    ic, correlation, maxSum );
+        // Are we past what we were looking for?
+        if((maxCorrelation > threshold) && (correlation < backThresholdScaler * maxCorrelation)) {
+            location = peakIndex;
+            break;
+        }
+    }
+
+    return location;
+}
+
+typedef struct LatencyReport_s {
+    double latencyInFrames;
+    double confidence;
+} LatencyReport;
+
+// Apply a technique similar to Harmonic Product Spectrum Analysis to find echo fundamental.
+// Using first echo instead of the original impulse for a better match.
+static int measureLatencyFromEchos(const float *haystack, int haystackSize,
+                            const float *needle, int needleSize,
+                            LatencyReport *report) {
+    const double threshold = 0.1;
+
+    // Find first peak
+    int first = (int) (findFirstMatch(haystack,
+                                      haystackSize,
+                                      needle,
+                                      needleSize,
+                                      threshold) + 0.5);
+
+    // Use first echo as the needle for the other echos because
+    // it will be more similar.
+    needle = &haystack[first];
+    int again = (int) (findFirstMatch(haystack,
+                                      haystackSize,
+                                      needle,
+                                      needleSize,
+                                      threshold) + 0.5);
+
+    printf("first = %d, again at %d\n", first, again);
+    first = again;
+
+    // Allocate results array
+    int remaining = haystackSize - first;
+    const int maxReasonableLatencyFrames = 48000 * 2; // arbitrary but generous value
+    int numCorrelations = std::min(remaining, maxReasonableLatencyFrames);
+    float *correlations = new float[numCorrelations];
+    float *harmonicSums = new float[numCorrelations](); // set to zero
+
+    // Generate correlation for every position.
+    numCorrelations = calculateCorrelations(&haystack[first], remaining,
+                                            needle, needleSize,
+                                            correlations, numCorrelations);
+
+    // Add higher harmonics mapped onto lower harmonics.
+    // This reinforces the "fundamental" echo.
+    const int numEchoes = 10;
+    for (int partial = 1; partial < numEchoes; partial++) {
+        for (int i = 0; i < numCorrelations; i++) {
+            harmonicSums[i / partial] += correlations[i] / partial;
+        }
+    }
+
+    // Find highest peak in correlation array.
+    float maxCorrelation = 0.0;
+    float sumOfPeaks = 0.0;
+    int peakIndex = 0;
+    const int skip = MAX_ZEROTH_PARTIAL_BINS; // skip low bins
+    for (int i = skip; i < numCorrelations; i++) {
+        if (harmonicSums[i] > maxCorrelation) {
+            maxCorrelation = harmonicSums[i];
+            sumOfPeaks += maxCorrelation;
+            peakIndex = i;
+            printf("maxCorrelation = %f at %d\n", maxCorrelation, peakIndex);
+        }
+    }
+
+    report->latencyInFrames = peakIndex;
+    if (sumOfPeaks < 0.0001) {
+        report->confidence = 0.0;
+    } else {
+        report->confidence = maxCorrelation / sumOfPeaks;
+    }
+
+    delete[] correlations;
+    delete[] harmonicSums;
+    return 0;
+}
+
+class AudioRecording
+{
+public:
+    AudioRecording() {
+    }
+    ~AudioRecording() {
+        delete[] mData;
+    }
+
+    void allocate(int maxFrames) {
+        delete[] mData;
+        mData = new float[maxFrames];
+        mMaxFrames = maxFrames;
+    }
+
+    // Write SHORT data from the first channel.
+    int write(int16_t *inputData, int inputChannelCount, int numFrames) {
+        // stop at end of buffer
+        if ((mFrameCounter + numFrames) > mMaxFrames) {
+            numFrames = mMaxFrames - mFrameCounter;
+        }
+        for (int i = 0; i < numFrames; i++) {
+            mData[mFrameCounter++] = inputData[i * inputChannelCount] * (1.0f / 32768);
+        }
+        return numFrames;
+    }
+
+    // Write FLOAT data from the first channel.
+    int write(float *inputData, int inputChannelCount, int numFrames) {
+        // stop at end of buffer
+        if ((mFrameCounter + numFrames) > mMaxFrames) {
+            numFrames = mMaxFrames - mFrameCounter;
+        }
+        for (int i = 0; i < numFrames; i++) {
+            mData[mFrameCounter++] = inputData[i * inputChannelCount];
+        }
+        return numFrames;
+    }
+
+    int size() {
+        return mFrameCounter;
+    }
+
+    float *getData() {
+        return mData;
+    }
+
+    int save(const char *fileName, bool writeShorts = true) {
+        int written = 0;
+        const int chunkSize = 64;
+        FILE *fid = fopen(fileName, "wb");
+        if (fid == NULL) {
+            return -errno;
+        }
+
+        if (writeShorts) {
+            int16_t buffer[chunkSize];
+            int32_t framesLeft = mFrameCounter;
+            int32_t cursor = 0;
+            while (framesLeft) {
+                int32_t framesToWrite = framesLeft < chunkSize ? framesLeft : chunkSize;
+                for (int i = 0; i < framesToWrite; i++) {
+                    buffer[i] = (int16_t) (mData[cursor++] * 32767);
+                }
+                written += fwrite(buffer, sizeof(int16_t), framesToWrite, fid);
+                framesLeft -= framesToWrite;
+            }
+        } else {
+            written = (int) fwrite(mData, sizeof(float), mFrameCounter, fid);
+        }
+        fclose(fid);
+        return written;
+    }
+
+private:
+    float  *mData = nullptr;
+    int32_t mFrameCounter = 0;
+    int32_t mMaxFrames = 0;
+};
+
+// ====================================================================================
+class LoopbackProcessor {
+public:
+    virtual ~LoopbackProcessor() = default;
+
+
+    virtual void reset() {}
+
+    virtual void process(float *inputData, int inputChannelCount,
+                 float *outputData, int outputChannelCount,
+                 int numFrames) = 0;
+
+
+    virtual void report() = 0;
+
+    virtual void printStatus() {};
+
+    virtual bool isDone() {
+        return false;
+    }
+
+    void setSampleRate(int32_t sampleRate) {
+        mSampleRate = sampleRate;
+    }
+
+    int32_t getSampleRate() {
+        return mSampleRate;
+    }
+
+    // Measure peak amplitude of buffer.
+    static float measurePeakAmplitude(float *inputData, int inputChannelCount, int numFrames) {
+        float peak = 0.0f;
+        for (int i = 0; i < numFrames; i++) {
+            float pos = fabs(*inputData);
+            if (pos > peak) {
+                peak = pos;
+            }
+            inputData += inputChannelCount;
+        }
+        return peak;
+    }
+
+
+private:
+    int32_t mSampleRate = LOOPBACK_SAMPLE_RATE;
+};
+
+class PeakDetector {
+public:
+    float process(float input) {
+        float output = mPrevious * mDecay;
+        if (input > output) {
+            output = input;
+        }
+        mPrevious = output;
+        return output;
+    }
+
+private:
+    float  mDecay = 0.99f;
+    float  mPrevious = 0.0f;
+};
+
+
+static void printAudioScope(float sample) {
+    const int maxStars = 80
+    ; // arbitrary, fits on one line
+    char c = '*';
+    if (sample < -1.0) {
+        sample = -1.0;
+        c = '$';
+    } else if (sample > 1.0) {
+        sample = 1.0;
+        c = '$';
+    }
+    int numSpaces = (int) (((sample + 1.0) * 0.5) * maxStars);
+    for (int i = 0; i < numSpaces; i++) {
+        putchar(' ');
+    }
+    printf("%c\n", c);
+}
+
+// ====================================================================================
+/**
+ * Measure latency given a loopback stream data.
+ * Uses a state machine to cycle through various stages including:
+ *
+ */
+class EchoAnalyzer : public LoopbackProcessor {
+public:
+
+    EchoAnalyzer() : LoopbackProcessor() {
+        audioRecorder.allocate(2 * LOOPBACK_SAMPLE_RATE);
+    }
+
+    void reset() override {
+        mDownCounter = 200;
+        mLoopCounter = 0;
+        mMeasuredLoopGain = 0.0f;
+        mEchoGain = 1.0f;
+        mState = STATE_INITIAL_SILENCE;
+    }
+
+    virtual bool isDone() {
+        return mState == STATE_DONE;
+    }
+
+    void setGain(float gain) {
+        mEchoGain = gain;
+    }
+
+    float getGain() {
+        return mEchoGain;
+    }
+
+    void report() override {
+
+        printf("EchoAnalyzer ---------------\n");
+        printf(LOOPBACK_RESULT_TAG "measured.gain          = %f\n", mMeasuredLoopGain);
+        printf(LOOPBACK_RESULT_TAG "echo.gain              = %f\n", mEchoGain);
+        printf(LOOPBACK_RESULT_TAG "frame.count            = %d\n", mFrameCounter);
+        printf(LOOPBACK_RESULT_TAG "test.state             = %d\n", mState);
+        if (mMeasuredLoopGain >= 0.9999) {
+            printf("   ERROR - clipping, turn down volume slightly\n");
+        } else {
+            const float *needle = s_Impulse;
+            int needleSize = (int) (sizeof(s_Impulse) / sizeof(float));
+            float *haystack = audioRecorder.getData();
+            int haystackSize = audioRecorder.size();
+            int result = measureLatencyFromEchos(haystack, haystackSize,
+                                                 needle, needleSize,
+                                                 &latencyReport);
+            if (latencyReport.confidence < 0.01) {
+                printf("   ERROR - confidence too low = %f\n", latencyReport.confidence);
+            } else {
+                double latencyMillis = 1000.0 * latencyReport.latencyInFrames / getSampleRate();
+                printf(LOOPBACK_RESULT_TAG "latency.frames        = %8.2f\n", latencyReport.latencyInFrames);
+                printf(LOOPBACK_RESULT_TAG "latency.msec          = %8.2f\n", latencyMillis);
+                printf(LOOPBACK_RESULT_TAG "latency.confidence    = %8.6f\n", latencyReport.confidence);
+            }
+        }
+
+        {
+#define ECHO_FILENAME "/data/oboe_echo.raw"
+            int written = audioRecorder.save(ECHO_FILENAME);
+            printf("Echo wrote %d mono samples to %s on Android device\n", written, ECHO_FILENAME);
+        }
+    }
+
+    void printStatus() override {
+        printf("state = %d, echo gain = %f ", mState, mEchoGain);
+    }
+
+    static void sendImpulse(float *outputData, int outputChannelCount) {
+        for (float sample : s_Impulse) {
+            *outputData = sample;
+            outputData += outputChannelCount;
+        }
+    }
+
+    void process(float *inputData, int inputChannelCount,
+                 float *outputData, int outputChannelCount,
+                 int numFrames) override {
+        int channelsValid = std::min(inputChannelCount, outputChannelCount);
+        float peak = 0.0f;
+        int numWritten;
+        int numSamples;
+
+        echo_state_t nextState = mState;
+
+        switch (mState) {
+            case STATE_INITIAL_SILENCE:
+                // Output silence at the beginning.
+                numSamples = numFrames * outputChannelCount;
+                for (int i = 0; i < numSamples; i++) {
+                    outputData[i] = 0;
+                }
+                if (mDownCounter-- <= 0) {
+                    nextState = STATE_MEASURING_GAIN;
+                    //printf("%5d: switch to STATE_MEASURING_GAIN\n", mLoopCounter);
+                    mDownCounter = 8;
+                }
+                break;
+
+            case STATE_MEASURING_GAIN:
+                sendImpulse(outputData, outputChannelCount);
+                peak = measurePeakAmplitude(inputData, inputChannelCount, numFrames);
+                // If we get several in a row then go to next state.
+                if (peak > mPulseThreshold) {
+                    if (mDownCounter-- <= 0) {
+                        nextState = STATE_WAITING_FOR_SILENCE;
+                        //printf("%5d: switch to STATE_WAITING_FOR_SILENCE, measured peak = %f\n",
+                        //       mLoopCounter, peak);
+                        mDownCounter = 8;
+                        mMeasuredLoopGain = peak;  // assumes original pulse amplitude is one
+                        // Calculate gain that will give us a nice decaying echo.
+                        mEchoGain = mDesiredEchoGain / mMeasuredLoopGain;
+                    }
+                } else {
+                    mDownCounter = 8;
+                }
+                break;
+
+            case STATE_WAITING_FOR_SILENCE:
+                // Output silence.
+                numSamples = numFrames * outputChannelCount;
+                for (int i = 0; i < numSamples; i++) {
+                    outputData[i] = 0;
+                }
+                peak = measurePeakAmplitude(inputData, inputChannelCount, numFrames);
+                // If we get several in a row then go to next state.
+                if (peak < mSilenceThreshold) {
+                    if (mDownCounter-- <= 0) {
+                        nextState = STATE_SENDING_PULSE;
+                        //printf("%5d: switch to STATE_SENDING_PULSE\n", mLoopCounter);
+                        mDownCounter = 8;
+                    }
+                } else {
+                    mDownCounter = 8;
+                }
+                break;
+
+            case STATE_SENDING_PULSE:
+                audioRecorder.write(inputData, inputChannelCount, numFrames);
+                sendImpulse(outputData, outputChannelCount);
+                nextState = STATE_GATHERING_ECHOS;
+                //printf("%5d: switch to STATE_GATHERING_ECHOS\n", mLoopCounter);
+                break;
+
+            case STATE_GATHERING_ECHOS:
+                numWritten = audioRecorder.write(inputData, inputChannelCount, numFrames);
+                peak = measurePeakAmplitude(inputData, inputChannelCount, numFrames);
+                if (peak > mMeasuredLoopGain) {
+                    mMeasuredLoopGain = peak;  // AGC might be raising gain so adjust it on the fly.
+                    // Recalculate gain that will give us a nice decaying echo.
+                    mEchoGain = mDesiredEchoGain / mMeasuredLoopGain;
+                }
+                // Echo input to output.
+                for (int i = 0; i < numFrames; i++) {
+                    int ic;
+                    for (ic = 0; ic < channelsValid; ic++) {
+                        outputData[ic] = inputData[ic] * mEchoGain;
+                    }
+                    for (; ic < outputChannelCount; ic++) {
+                        outputData[ic] = 0;
+                    }
+                    inputData += inputChannelCount;
+                    outputData += outputChannelCount;
+                }
+                if (numWritten  < numFrames) {
+                    nextState = STATE_DONE;
+                    //printf("%5d: switch to STATE_DONE\n", mLoopCounter);
+                }
+                break;
+
+            case STATE_DONE:
+            default:
+                break;
+        }
+
+        mState = nextState;
+        mLoopCounter++;
+    }
+
+private:
+
+    enum echo_state_t {
+        STATE_INITIAL_SILENCE,
+        STATE_MEASURING_GAIN,
+        STATE_WAITING_FOR_SILENCE,
+        STATE_SENDING_PULSE,
+        STATE_GATHERING_ECHOS,
+        STATE_DONE
+    };
+
+    int           mDownCounter = 500;
+    int           mLoopCounter = 0;
+    int           mLoopStart = 1000;
+    float         mPulseThreshold = 0.02f;
+    float         mSilenceThreshold = 0.002f;
+    float         mMeasuredLoopGain = 0.0f;
+    float         mDesiredEchoGain = 0.95f;
+    float         mEchoGain = 1.0f;
+    echo_state_t  mState = STATE_INITIAL_SILENCE;
+    int32_t       mFrameCounter = 0;
+
+    AudioRecording     audioRecorder;
+    LatencyReport      latencyReport;
+    PeakDetector       mPeakDetector;
+};
+
+
+// ====================================================================================
+/**
+ * Output a steady sinewave 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.
+ */
+class SineAnalyzer : public LoopbackProcessor {
+public:
+
+    void report() override {
+        printf("SineAnalyzer ------------------\n");
+        printf(LOOPBACK_RESULT_TAG "peak.amplitude     = %7.5f\n", mPeakAmplitude);
+        printf(LOOPBACK_RESULT_TAG "sine.magnitude     = %7.5f\n", mMagnitude);
+        printf(LOOPBACK_RESULT_TAG "phase.offset       = %7.5f\n", mPhaseOffset);
+        printf(LOOPBACK_RESULT_TAG "ref.phase          = %7.5f\n", mPhase);
+        printf(LOOPBACK_RESULT_TAG "frames.accumulated = %6d\n", mFramesAccumulated);
+        printf(LOOPBACK_RESULT_TAG "sine.period        = %6d\n", mPeriod);
+        printf(LOOPBACK_RESULT_TAG "test.state         = %6d\n", mState);
+        printf(LOOPBACK_RESULT_TAG "frame.count        = %6d\n", mFrameCounter);
+        // Did we ever get a lock?
+        bool gotLock = (mState == STATE_LOCKED) || (mGlitchCount > 0);
+        if (!gotLock) {
+            printf("ERROR - failed to lock on reference sine tone\n");
+        } else {
+            // Only print if meaningful.
+            printf(LOOPBACK_RESULT_TAG "glitch.count       = %6d\n", mGlitchCount);
+        }
+    }
+
+    void printStatus() override {
+        printf("  state = %d, glitches = %d,", mState, mGlitchCount);
+    }
+
+    double calculateMagnitude(double *phasePtr = NULL) {
+        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 );
+            *phasePtr = phase;
+        }
+        return magnitude;
+    }
+
+    /**
+     * @param inputData contains microphone data with sine signal feedback
+     * @param outputData contains the reference sine wave
+     */
+    void process(float *inputData, int inputChannelCount,
+                 float *outputData, int outputChannelCount,
+                 int numFrames) override {
+        float sample;
+        float peak = measurePeakAmplitude(inputData, inputChannelCount, numFrames);
+        if (peak > mPeakAmplitude) {
+            mPeakAmplitude = peak;
+        }
+
+        for (int i = 0; i < numFrames; i++) {
+            float sample = inputData[i * inputChannelCount];
+
+            float sinOut = sinf(mPhase);
+
+            switch (mState) {
+                case STATE_IMMUNE:
+                case STATE_WAITING_FOR_SIGNAL:
+                    break;
+                case STATE_WAITING_FOR_LOCK:
+                    mSinAccumulator += sample * sinOut;
+                    mCosAccumulator += sample * cosf(mPhase);
+                    mFramesAccumulated++;
+                    // Must be a multiple of the period or the calculation will not be accurate.
+                    if (mFramesAccumulated == mPeriod * 4) {
+                        mPhaseOffset = 0.0;
+                        mMagnitude = calculateMagnitude(&mPhaseOffset);
+                        if (mMagnitude > mThreshold) {
+                            if (fabs(mPreviousPhaseOffset - mPhaseOffset) < 0.001) {
+                                mState = STATE_LOCKED;
+                                //printf("%5d: switch to STATE_LOCKED\n", mFrameCounter);
+                            }
+                            mPreviousPhaseOffset = mPhaseOffset;
+                        }
+                        resetAccumulator();
+                    }
+                    break;
+
+                case STATE_LOCKED: {
+                    // Predict next sine value
+                    float predicted = sinf(mPhase + mPhaseOffset) * mMagnitude;
+                    // printf("    predicted = %f, actual = %f\n", predicted, sample);
+
+                    float diff = predicted - sample;
+                    if (fabs(diff) > mTolerance) {
+                        mGlitchCount++;
+                        //printf("%5d: Got a glitch # %d, predicted = %f, actual = %f\n",
+                        //       mFrameCounter, mGlitchCount, predicted, sample);
+                        mState = STATE_IMMUNE;
+                        //printf("%5d: switch to STATE_IMMUNE\n", mFrameCounter);
+                        mDownCounter = mPeriod;  // Set duration of IMMUNE state.
+                    }
+                } break;
+            }
+
+            // Output sine wave so we can measure it.
+            outputData[i * outputChannelCount] = (sinOut * mOutputAmplitude)
+                    + (mWhiteNoise.nextRandomDouble() * mNoiseAmplitude);
+            // printf("%5d: sin(%f) = %f, %f\n", i, mPhase, sinOut,  mPhaseIncrement);
+
+            // advance and wrap phase
+            mPhase += mPhaseIncrement;
+            if (mPhase > M_PI) {
+                mPhase -= (2.0 * M_PI);
+            }
+
+            mFrameCounter++;
+        }
+
+        // Do these once per buffer.
+        switch (mState) {
+            case STATE_IMMUNE:
+                mDownCounter -= numFrames;
+                if (mDownCounter <= 0) {
+                    mState = STATE_WAITING_FOR_SIGNAL;
+                    //printf("%5d: switch to STATE_WAITING_FOR_SIGNAL\n", mFrameCounter);
+                }
+                break;
+            case STATE_WAITING_FOR_SIGNAL:
+                if (peak > mThreshold) {
+                    mState = STATE_WAITING_FOR_LOCK;
+                    //printf("%5d: switch to STATE_WAITING_FOR_LOCK\n", mFrameCounter);
+                    resetAccumulator();
+                }
+                break;
+            case STATE_WAITING_FOR_LOCK:
+            case STATE_LOCKED:
+                break;
+        }
+
+    }
+
+    void resetAccumulator() {
+        mFramesAccumulated = 0;
+        mSinAccumulator = 0.0;
+        mCosAccumulator = 0.0;
+    }
+
+    void reset() override {
+        mGlitchCount = 0;
+        mState = STATE_IMMUNE;
+        mPhaseIncrement = 2.0 * M_PI / mPeriod;
+        printf("phaseInc = %f for period %d\n", mPhaseIncrement, mPeriod);
+        resetAccumulator();
+    }
+
+private:
+
+    enum sine_state_t {
+        STATE_IMMUNE,
+        STATE_WAITING_FOR_SIGNAL,
+        STATE_WAITING_FOR_LOCK,
+        STATE_LOCKED
+    };
+
+    int     mPeriod = 79;
+    double  mPhaseIncrement = 0.0;
+    double  mPhase = 0.0;
+    double  mPhaseOffset = 0.0;
+    double  mPreviousPhaseOffset = 0.0;
+    double  mMagnitude = 0.0;
+    double  mThreshold = 0.005;
+    double  mTolerance = 0.01;
+    int32_t mFramesAccumulated = 0;
+    double  mSinAccumulator = 0.0;
+    double  mCosAccumulator = 0.0;
+    int32_t mGlitchCount = 0;
+    double  mPeakAmplitude = 0.0;
+    int     mDownCounter = 4000;
+    int32_t mFrameCounter = 0;
+    float   mOutputAmplitude = 0.75;
+
+    int32_t mZeroCrossings = 0;
+
+    PseudoRandom  mWhiteNoise;
+    float   mNoiseAmplitude = 0.00; // Used to experiment with warbling caused by DRC.
+
+    sine_state_t  mState = STATE_IMMUNE;
+};
+
+
+#undef LOOPBACK_SAMPLE_RATE
+#undef LOOPBACK_RESULT_TAG
+
+#endif /* AAUDIO_EXAMPLES_LOOPBACK_ANALYSER_H */
diff --git a/media/libaaudio/examples/loopback/src/loopback.cpp b/media/libaaudio/examples/loopback/src/loopback.cpp
index 57d45cd..144c941 100644
--- a/media/libaaudio/examples/loopback/src/loopback.cpp
+++ b/media/libaaudio/examples/loopback/src/loopback.cpp
@@ -14,8 +14,7 @@
  * limitations under the License.
  */
 
-// Play an impulse and then record it.
-// Measure the round trip latency.
+// Audio loopback tests to measure the round trip latency and glitches.
 
 #include <algorithm>
 #include <assert.h>
@@ -28,460 +27,38 @@
 #include <aaudio/AAudio.h>
 #include <aaudio/AAudioTesting.h>
 
+#include "AAudioSimplePlayer.h"
+#include "AAudioSimpleRecorder.h"
+#include "AAudioExampleUtils.h"
+#include "LoopbackAnalyzer.h"
+
 // Tag for machine readable results as property = value pairs
 #define RESULT_TAG              "RESULT: "
 #define SAMPLE_RATE             48000
 #define NUM_SECONDS             5
 #define NUM_INPUT_CHANNELS      1
 #define FILENAME                "/data/oboe_input.raw"
-
-#define NANOS_PER_MICROSECOND ((int64_t)1000)
-#define NANOS_PER_MILLISECOND (NANOS_PER_MICROSECOND * 1000)
-#define MILLIS_PER_SECOND     1000
-#define NANOS_PER_SECOND      (NANOS_PER_MILLISECOND * MILLIS_PER_SECOND)
-
-#define MAX_ZEROTH_PARTIAL_BINS   40
-
-static const float s_Impulse[] = {
-        0.0f, 0.0f, 0.0f, 0.0f, 0.0f, // silence on each side of the impulse
-        0.5f, 0.9f, 0.0f, -0.9f, -0.5f, // bipolar
-        0.0f, 0.0f, 0.0f, 0.0f, 0.0f};
-
-
-static double calculateCorrelation(const float *a,
-                                   const float *b,
-                                   int windowSize)
-{
-    double correlation = 0.0;
-    double sumProducts = 0.0;
-    double sumSquares = 0.0;
-
-    // Correlate a against b.
-    for (int i = 0; i < windowSize; i++) {
-        float s1 = a[i];
-        float s2 = b[i];
-        // Use a normalized cross-correlation.
-        sumProducts += s1 * s2;
-        sumSquares += ((s1 * s1) + (s2 * s2));
-    }
-
-    if (sumSquares >= 0.00000001) {
-        correlation = (float) (2.0 * sumProducts / sumSquares);
-    }
-    return correlation;
-}
-
-static int calculateCorrelations(const float *haystack, int haystackSize,
-                                 const float *needle, int needleSize,
-                                 float *results, int resultSize)
-{
-    int ic;
-    int maxCorrelations = haystackSize - needleSize;
-    int numCorrelations = std::min(maxCorrelations, resultSize);
-
-    for (ic = 0; ic < numCorrelations; ic++) {
-        double correlation = calculateCorrelation(&haystack[ic], needle, needleSize);
-        results[ic] = correlation;
-    }
-
-    return numCorrelations;
-}
-
-/*==========================================================================================*/
-/**
- * Scan until we get a correlation of a single scan that goes over the tolerance level,
- * peaks then drops back down.
- */
-static double findFirstMatch(const float *haystack, int haystackSize,
-                             const float *needle, int needleSize, double threshold  )
-{
-    int ic;
-    // How many correlations can we calculate?
-    int numCorrelations = haystackSize - needleSize;
-    double maxCorrelation = 0.0;
-    int peakIndex = -1;
-    double location = -1.0;
-
-    for (ic = 0; ic < numCorrelations; ic++) {
-        double correlation = calculateCorrelation(&haystack[ic], needle, needleSize);
-
-        if( (correlation > maxCorrelation) ) {
-            maxCorrelation = correlation;
-            peakIndex = ic;
-        }
-
-        //printf("PaQa_FindFirstMatch: ic = %4d, correlation = %8f, maxSum = %8f\n",
-        //    ic, correlation, maxSum );
-        // Are we past what we were looking for?
-        if((maxCorrelation > threshold) && (correlation < 0.5 * maxCorrelation)) {
-            location = peakIndex;
-            break;
-        }
-    }
-
-    return location;
-}
-
-typedef struct LatencyReport_s {
-    double latencyInFrames;
-    double confidence;
-} LatencyReport;
-
-// Apply a technique similar to Harmonic Product Spectrum Analysis to find echo fundamental.
-// Using first echo instead of the original impulse for a better match.
-int measureLatencyFromEchos(const float *haystack, int haystackSize,
-                          const float *needle, int needleSize,
-                          LatencyReport *report) {
-    double threshold = 0.1;
-
-    // Find first peak
-    int first = (int) (findFirstMatch(haystack,
-                                      haystackSize,
-                                      needle,
-                                      needleSize,
-                                      threshold) + 0.5);
-
-    // Use first echo as the needle for the other echos because
-    // it will be more similar.
-    needle = &haystack[first];
-    int again = (int) (findFirstMatch(haystack,
-                                      haystackSize,
-                                      needle,
-                                      needleSize,
-                                      threshold) + 0.5);
-
-    printf("first = %d, again at %d\n", first, again);
-    first = again;
-
-    // Allocate results array
-    int remaining = haystackSize - first;
-    int generous = 48000 * 2;
-    int numCorrelations = std::min(remaining, generous);
-    float *correlations = new float[numCorrelations];
-    float *harmonicSums = new float[numCorrelations](); // cleared to zero
-
-    // Generate correlation for every position.
-    numCorrelations = calculateCorrelations(&haystack[first], remaining,
-                                            needle, needleSize,
-                                            correlations, numCorrelations);
-
-    // Add higher harmonics mapped onto lower harmonics.
-    // This reinforces the "fundamental" echo.
-    const int numEchoes = 10;
-    for (int partial = 1; partial < numEchoes; partial++) {
-        for (int i = 0; i < numCorrelations; i++) {
-            harmonicSums[i / partial] += correlations[i] / partial;
-        }
-    }
-
-    // Find highest peak in correlation array.
-    float maxCorrelation = 0.0;
-    float sumOfPeaks = 0.0;
-    int peakIndex = 0;
-    const int skip = MAX_ZEROTH_PARTIAL_BINS; // skip low bins
-    for (int i = skip; i < numCorrelations; i++) {
-        if (harmonicSums[i] > maxCorrelation) {
-            maxCorrelation = harmonicSums[i];
-            sumOfPeaks += maxCorrelation;
-            peakIndex = i;
-            printf("maxCorrelation = %f at %d\n", maxCorrelation, peakIndex);
-        }
-    }
-
-    report->latencyInFrames = peakIndex;
-    if (sumOfPeaks < 0.0001) {
-        report->confidence = 0.0;
-    } else {
-        report->confidence = maxCorrelation / sumOfPeaks;
-    }
-
-    delete[] correlations;
-    delete[] harmonicSums;
-    return 0;
-}
-
-class AudioRecording
-{
-public:
-    AudioRecording() {
-    }
-    ~AudioRecording() {
-        delete[] mData;
-    }
-
-    void allocate(int maxFrames) {
-        delete[] mData;
-        mData = new float[maxFrames];
-        mMaxFrames = maxFrames;
-    }
-
-    // Write SHORT data from the first channel.
-    int write(int16_t *inputData, int inputChannelCount, int numFrames) {
-        // stop at end of buffer
-        if ((mFrameCounter + numFrames) > mMaxFrames) {
-            numFrames = mMaxFrames - mFrameCounter;
-        }
-        for (int i = 0; i < numFrames; i++) {
-            mData[mFrameCounter++] = inputData[i * inputChannelCount] * (1.0f / 32768);
-        }
-        return numFrames;
-    }
-
-    // Write FLOAT data from the first channel.
-    int write(float *inputData, int inputChannelCount, int numFrames) {
-        // stop at end of buffer
-        if ((mFrameCounter + numFrames) > mMaxFrames) {
-            numFrames = mMaxFrames - mFrameCounter;
-        }
-        for (int i = 0; i < numFrames; i++) {
-            mData[mFrameCounter++] = inputData[i * inputChannelCount];
-        }
-        return numFrames;
-    }
-
-    int size() {
-        return mFrameCounter;
-    }
-
-    float *getData() {
-        return mData;
-    }
-
-    int save(const char *fileName, bool writeShorts = true) {
-        int written = 0;
-        const int chunkSize = 64;
-        FILE *fid = fopen(fileName, "wb");
-        if (fid == NULL) {
-            return -errno;
-        }
-
-        if (writeShorts) {
-            int16_t buffer[chunkSize];
-            int32_t framesLeft = mFrameCounter;
-            int32_t cursor = 0;
-            while (framesLeft) {
-                int32_t framesToWrite = framesLeft < chunkSize ? framesLeft : chunkSize;
-                for (int i = 0; i < framesToWrite; i++) {
-                    buffer[i] = (int16_t) (mData[cursor++] * 32767);
-                }
-                written += fwrite(buffer, sizeof(int16_t), framesToWrite, fid);
-                framesLeft -= framesToWrite;
-            }
-        } else {
-            written = fwrite(mData, sizeof(float), mFrameCounter, fid);
-        }
-        fclose(fid);
-        return written;
-    }
-
-private:
-    float  *mData = nullptr;
-    int32_t mFrameCounter = 0;
-    int32_t mMaxFrames = 0;
-};
-
-// ====================================================================================
-class LoopbackProcessor {
-public:
-    virtual ~LoopbackProcessor() = default;
-
-    virtual void process(float *inputData, int inputChannelCount,
-                 float *outputData, int outputChannelCount,
-                 int numFrames) = 0;
-
-
-    virtual void report() = 0;
-
-    void setSampleRate(int32_t sampleRate) {
-        mSampleRate = sampleRate;
-    }
-
-    int32_t getSampleRate() {
-        return mSampleRate;
-    }
-
-private:
-    int32_t mSampleRate = SAMPLE_RATE;
-};
-
-
-// ====================================================================================
-class EchoAnalyzer : public LoopbackProcessor {
-public:
-
-    EchoAnalyzer() : LoopbackProcessor() {
-        audioRecorder.allocate(NUM_SECONDS * SAMPLE_RATE);
-    }
-
-    void setGain(float gain) {
-        mGain = gain;
-    }
-
-    float getGain() {
-        return mGain;
-    }
-
-    void report() override {
-
-        const float *needle = s_Impulse;
-        int needleSize = (int)(sizeof(s_Impulse) / sizeof(float));
-        float *haystack = audioRecorder.getData();
-        int haystackSize = audioRecorder.size();
-        int result = measureLatencyFromEchos(haystack, haystackSize,
-                                              needle, needleSize,
-                                              &latencyReport);
-        if (latencyReport.confidence < 0.01) {
-            printf(" ERROR - confidence too low = %f\n", latencyReport.confidence);
-        } else {
-            double latencyMillis = 1000.0 * latencyReport.latencyInFrames / getSampleRate();
-            printf(RESULT_TAG "latency.frames     = %8.2f\n", latencyReport.latencyInFrames);
-            printf(RESULT_TAG "latency.msec       = %8.2f\n", latencyMillis);
-            printf(RESULT_TAG "latency.confidence = %8.6f\n", latencyReport.confidence);
-        }
-    }
-
-    void process(float *inputData, int inputChannelCount,
-                 float *outputData, int outputChannelCount,
-                 int numFrames) override {
-        int channelsValid = std::min(inputChannelCount, outputChannelCount);
-
-        audioRecorder.write(inputData, inputChannelCount, numFrames);
-
-        if (mLoopCounter < mLoopStart) {
-            // Output silence at the beginning.
-            for (int i = 0; i < numFrames; i++) {
-                int ic;
-                for (ic = 0; ic < outputChannelCount; ic++) {
-                    outputData[ic] = 0;
-                }
-                inputData += inputChannelCount;
-                outputData += outputChannelCount;
-            }
-        } else if (mLoopCounter == mLoopStart) {
-            // Send a bipolar impulse that we can easily detect.
-            for (float sample : s_Impulse) {
-                *outputData = sample;
-                outputData += outputChannelCount;
-            }
-        } else {
-            // Echo input to output.
-            for (int i = 0; i < numFrames; i++) {
-                int ic;
-                for (ic = 0; ic < channelsValid; ic++) {
-                    outputData[ic] = inputData[ic] * mGain;
-                }
-                for (; ic < outputChannelCount; ic++) {
-                    outputData[ic] = 0;
-                }
-                inputData += inputChannelCount;
-                outputData += outputChannelCount;
-            }
-        }
-
-        mLoopCounter++;
-    }
-
-private:
-    int   mLoopCounter = 0;
-    int   mLoopStart = 1000;
-    float mGain = 1.0f;
-
-    AudioRecording     audioRecorder;
-    LatencyReport      latencyReport;
-};
-
-
-// ====================================================================================
-class SineAnalyzer : public LoopbackProcessor {
-public:
-
-    void report() override {
-        double magnitude = calculateMagnitude();
-        printf("sine magnitude = %7.5f\n", magnitude);
-        printf("sine frames    = %7d\n", mFrameCounter);
-        printf("sine frequency = %7.1f Hz\n", mFrequency);
-    }
-
-    double calculateMagnitude(double *phasePtr = NULL) {
-        if (mFrameCounter == 0) {
-            return 0.0;
-        }
-        double sinMean = mSinAccumulator / mFrameCounter;
-        double cosMean = mCosAccumulator / mFrameCounter;
-        double magnitude = 2.0 * sqrt( (sinMean * sinMean) + (cosMean * cosMean ));
-        if( phasePtr != NULL )
-        {
-            double phase = atan2( sinMean, cosMean );
-            *phasePtr = phase;
-        }
-        return magnitude;
-    }
-
-    void process(float *inputData, int inputChannelCount,
-                 float *outputData, int outputChannelCount,
-                 int numFrames) override {
-        double phaseIncrement = 2.0 * M_PI * mFrequency / getSampleRate();
-
-        for (int i = 0; i < numFrames; i++) {
-            // Multiply input by sine/cosine
-            float sample = inputData[i * inputChannelCount];
-            float sinOut = sinf(mPhase);
-            mSinAccumulator += sample * sinOut;
-            mCosAccumulator += sample * cosf(mPhase);
-            // Advance and wrap phase
-            mPhase += phaseIncrement;
-            if (mPhase > (2.0 * M_PI)) {
-                mPhase -= (2.0 * M_PI);
-            }
-
-            // Output sine wave so we can measure it.
-            outputData[i * outputChannelCount] = sinOut;
-        }
-        mFrameCounter += numFrames;
-
-        double magnitude = calculateMagnitude();
-        if (mWaiting) {
-            if (magnitude < 0.001) {
-                // discard silence
-                mFrameCounter = 0;
-                mSinAccumulator = 0.0;
-                mCosAccumulator = 0.0;
-            } else {
-                mWaiting = false;
-            }
-        }
-    };
-
-    void setFrequency(int32_t frequency) {
-        mFrequency = frequency;
-    }
-
-    int32_t getFrequency() {
-        return mFrequency;
-    }
-
-private:
-    double  mFrequency = 300.0;
-    double  mPhase = 0.0;
-    int32_t mFrameCounter = 0;
-    double  mSinAccumulator = 0.0;
-    double  mCosAccumulator = 0.0;
-    bool    mWaiting = true;
-};
+#define APP_VERSION             "0.1.22"
 
-// TODO make this a class that manages its own buffer allocation
 struct LoopbackData {
     AAudioStream      *inputStream = nullptr;
     int32_t            inputFramesMaximum = 0;
     int16_t           *inputData = nullptr;
+    int16_t            peakShort = 0;
     float             *conversionBuffer = nullptr;
     int32_t            actualInputChannelCount = 0;
     int32_t            actualOutputChannelCount = 0;
     int32_t            inputBuffersToDiscard = 10;
+    int32_t            minNumFrames = INT32_MAX;
+    int32_t            maxNumFrames = 0;
+    bool               isDone = false;
 
-    aaudio_result_t    inputError;
+    aaudio_result_t    inputError = AAUDIO_OK;
+    aaudio_result_t    outputError = AAUDIO_OK;
+
     SineAnalyzer       sineAnalyzer;
     EchoAnalyzer       echoAnalyzer;
+    AudioRecording     audioRecorder;
     LoopbackProcessor *loopbackProcessor;
 };
 
@@ -517,6 +94,13 @@
         return AAUDIO_CALLBACK_RESULT_STOP;
     }
 
+    if (numFrames > myData->maxNumFrames) {
+        myData->maxNumFrames = numFrames;
+    }
+    if (numFrames < myData->minNumFrames) {
+        myData->minNumFrames = numFrames;
+    }
+
     if (myData->inputBuffersToDiscard > 0) {
         // Drain the input.
         do {
@@ -524,6 +108,7 @@
                                        numFrames, 0);
             if (framesRead < 0) {
                 myData->inputError = framesRead;
+                printf("ERROR in read = %d", framesRead);
                 result = AAUDIO_CALLBACK_RESULT_STOP;
             } else if (framesRead > 0) {
                 myData->inputBuffersToDiscard--;
@@ -534,9 +119,14 @@
                                        numFrames, 0);
         if (framesRead < 0) {
             myData->inputError = framesRead;
+            printf("ERROR in read = %d", framesRead);
             result = AAUDIO_CALLBACK_RESULT_STOP;
         } else if (framesRead > 0) {
 
+            myData->audioRecorder.write(myData->inputData,
+                                        myData->actualInputChannelCount,
+                                        numFrames);
+
             int32_t numSamples = framesRead * myData->actualInputChannelCount;
             convertPcm16ToFloat(myData->inputData, myData->conversionBuffer, numSamples);
 
@@ -545,12 +135,25 @@
                                               outputData,
                                               myData->actualOutputChannelCount,
                                               framesRead);
+            myData->isDone = myData->loopbackProcessor->isDone();
+            if (myData->isDone) {
+                result = AAUDIO_CALLBACK_RESULT_STOP;
+            }
         }
     }
 
     return result;
 }
 
+static void MyErrorCallbackProc(
+        AAudioStream *stream __unused,
+        void *userData __unused,
+        aaudio_result_t error)
+{
+    printf("Error Callback, error: %d\n",(int)error);
+    LoopbackData *myData = (LoopbackData *) userData;
+    myData->outputError = error;
+}
 
 static void usage() {
     printf("loopback: -n{numBursts} -p{outPerf} -P{inPerf} -t{test} -g{gain} -f{freq}\n");
@@ -571,7 +174,7 @@
 }
 
 static aaudio_performance_mode_t parsePerformanceMode(char c) {
-    aaudio_performance_mode_t mode = AAUDIO_PERFORMANCE_MODE_NONE;
+    aaudio_performance_mode_t mode = AAUDIO_ERROR_ILLEGAL_ARGUMENT;
     c = tolower(c);
     switch (c) {
         case 'n':
@@ -612,86 +215,117 @@
     return testMode;
 }
 
+void printAudioGraph(AudioRecording &recording, int numSamples) {
+    int32_t start = recording.size() / 2;
+    int32_t end = start + numSamples;
+    if (end >= recording.size()) {
+        end = recording.size() - 1;
+    }
+    float *data = recording.getData();
+    // Normalize data so we can see it better.
+    float maxSample = 0.01;
+    for (int32_t i = start; i < end; i++) {
+        float samplePos = fabs(data[i]);
+        if (samplePos > maxSample) {
+            maxSample = samplePos;
+        }
+    }
+    float gain = 0.98f / maxSample;
+    for (int32_t i = start; i < end; i++) {
+        float sample = data[i];
+        printf("%5.3f ", sample); // actual value
+        sample *= gain;
+        printAudioScope(sample);
+    }
+}
+
+
 // ====================================================================================
 // TODO break up this large main() function into smaller functions
 int main(int argc, const char **argv)
 {
-    aaudio_result_t result = AAUDIO_OK;
-    LoopbackData loopbackData;
-    AAudioStream *outputStream = nullptr;
 
-    int requestedInputChannelCount = NUM_INPUT_CHANNELS;
-    const int requestedOutputChannelCount = AAUDIO_UNSPECIFIED;
-    const int requestedSampleRate = SAMPLE_RATE;
-    int actualSampleRate = 0;
+    AAudioArgsParser     argParser;
+    AAudioSimplePlayer   player;
+    AAudioSimpleRecorder recorder;
+    LoopbackData         loopbackData;
+    AAudioStream        *outputStream = nullptr;
+
+    aaudio_result_t      result = AAUDIO_OK;
+    aaudio_sharing_mode_t requestedInputSharingMode     = AAUDIO_SHARING_MODE_SHARED;
+    int                   requestedInputChannelCount = NUM_INPUT_CHANNELS;
+    const int             requestedOutputChannelCount = AAUDIO_UNSPECIFIED;
+    int                   actualSampleRate = 0;
     const aaudio_format_t requestedInputFormat = AAUDIO_FORMAT_PCM_I16;
     const aaudio_format_t requestedOutputFormat = AAUDIO_FORMAT_PCM_FLOAT;
-    aaudio_format_t actualInputFormat;
-    aaudio_format_t actualOutputFormat;
+    aaudio_format_t       actualInputFormat;
+    aaudio_format_t       actualOutputFormat;
+    aaudio_performance_mode_t outputPerformanceLevel = AAUDIO_PERFORMANCE_MODE_LOW_LATENCY;
+    aaudio_performance_mode_t inputPerformanceLevel = AAUDIO_PERFORMANCE_MODE_LOW_LATENCY;
+
     int testMode = TEST_ECHO_LATENCY;
-    double frequency = 1000.0;
     double gain = 1.0;
 
-    const aaudio_sharing_mode_t requestedSharingMode = AAUDIO_SHARING_MODE_EXCLUSIVE;
-    //const aaudio_sharing_mode_t requestedSharingMode = AAUDIO_SHARING_MODE_SHARED;
-    aaudio_sharing_mode_t       actualSharingMode;
-
-    AAudioStreamBuilder  *builder = nullptr;
     aaudio_stream_state_t state = AAUDIO_STREAM_STATE_UNINITIALIZED;
     int32_t framesPerBurst = 0;
     float *outputData = NULL;
     double deviation;
     double latency;
-    aaudio_performance_mode_t outputPerformanceLevel = AAUDIO_PERFORMANCE_MODE_LOW_LATENCY;
-    aaudio_performance_mode_t inputPerformanceLevel = AAUDIO_PERFORMANCE_MODE_LOW_LATENCY;
-
     int32_t burstsPerBuffer = 1; // single buffered
 
+    // Make printf print immediately so that debug info is not stuck
+    // in a buffer if we hang or crash.
+    setvbuf(stdout, NULL, _IONBF, (size_t) 0);
+
+    printf("%s - Audio loopback using AAudio V" APP_VERSION "\n", argv[0]);
+
     for (int i = 1; i < argc; i++) {
         const char *arg = argv[i];
-        if (arg[0] == '-') {
-            char option = arg[1];
-            switch (option) {
-                case 'c':
-                    requestedInputChannelCount = atoi(&arg[2]);
-                    break;
-                case 'f':
-                    frequency = atof(&arg[2]);
-                    break;
-                case 'g':
-                    gain = atof(&arg[2]);
-                    break;
-                case 'm':
-                    AAudio_setMMapPolicy(AAUDIO_POLICY_AUTO);
-                    break;
-                case 'n':
-                    burstsPerBuffer = atoi(&arg[2]);
-                    break;
-                case 'p':
-                    outputPerformanceLevel = parsePerformanceMode(arg[2]);
-                    break;
-                case 'P':
-                    inputPerformanceLevel = parsePerformanceMode(arg[2]);
-                    break;
-                case 't':
-                    testMode = parseTestMode(arg[2]);
-                    break;
-                default:
-                    usage();
-                    exit(0);
-                    break;
+        if (argParser.parseArg(arg)) {
+            // Handle options that are not handled by the ArgParser
+            if (arg[0] == '-') {
+                char option = arg[1];
+                switch (option) {
+                    case 'C':
+                        requestedInputChannelCount = atoi(&arg[2]);
+                        break;
+                    case 'g':
+                        gain = atof(&arg[2]);
+                        break;
+                    case 'P':
+                        inputPerformanceLevel = parsePerformanceMode(arg[2]);
+                        break;
+                    case 'X':
+                        requestedInputSharingMode = AAUDIO_SHARING_MODE_EXCLUSIVE;
+                        break;
+                    case 't':
+                        testMode = parseTestMode(arg[2]);
+                        break;
+                    default:
+                        usage();
+                        exit(EXIT_FAILURE);
+                        break;
+                }
+            } else {
+                usage();
+                exit(EXIT_FAILURE);
+                break;
             }
-        } else {
-            usage();
-            exit(0);
-            break;
         }
+
     }
 
+    if (inputPerformanceLevel < 0) {
+        printf("illegal inputPerformanceLevel = %d\n", inputPerformanceLevel);
+        exit(EXIT_FAILURE);
+    }
+
+    int32_t requestedDuration = argParser.getDurationSeconds();
+    int32_t recordingDuration = std::min(60, requestedDuration);
+    loopbackData.audioRecorder.allocate(recordingDuration * SAMPLE_RATE);
 
     switch(testMode) {
         case TEST_SINE_MAGNITUDE:
-            loopbackData.sineAnalyzer.setFrequency(frequency);
             loopbackData.loopbackProcessor = &loopbackData.sineAnalyzer;
             break;
         case TEST_ECHO_LATENCY:
@@ -703,106 +337,44 @@
             break;
     }
 
-    // Make printf print immediately so that debug info is not stuck
-    // in a buffer if we hang or crash.
-    setvbuf(stdout, NULL, _IONBF, (size_t) 0);
-
-    printf("%s - Audio loopback using AAudio\n", argv[0]);
-
-    // Use an AAudioStreamBuilder to contain requested parameters.
-    result = AAudio_createStreamBuilder(&builder);
-    if (result < 0) {
-        goto finish;
-    }
-
-    // Request common stream properties.
-    AAudioStreamBuilder_setSampleRate(builder, requestedSampleRate);
-    AAudioStreamBuilder_setFormat(builder, requestedInputFormat);
-    AAudioStreamBuilder_setSharingMode(builder, requestedSharingMode);
-
-    // Open the input stream.
-    AAudioStreamBuilder_setDirection(builder, AAUDIO_DIRECTION_INPUT);
-    AAudioStreamBuilder_setPerformanceMode(builder, inputPerformanceLevel);
-    AAudioStreamBuilder_setChannelCount(builder, requestedInputChannelCount);
-
-    result = AAudioStreamBuilder_openStream(builder, &loopbackData.inputStream);
-    printf("AAudioStreamBuilder_openStream(input) returned %d = %s\n",
-           result, AAudio_convertResultToText(result));
-    if (result < 0) {
-        goto finish;
-    }
-
-    // Create an output stream using the Builder.
-    AAudioStreamBuilder_setDirection(builder, AAUDIO_DIRECTION_OUTPUT);
-    AAudioStreamBuilder_setFormat(builder, requestedOutputFormat);
-    AAudioStreamBuilder_setPerformanceMode(builder, outputPerformanceLevel);
-    AAudioStreamBuilder_setChannelCount(builder, requestedOutputChannelCount);
-    AAudioStreamBuilder_setDataCallback(builder, MyDataCallbackProc, &loopbackData);
-
-    result = AAudioStreamBuilder_openStream(builder, &outputStream);
-    printf("AAudioStreamBuilder_openStream(output) returned %d = %s\n",
-           result, AAudio_convertResultToText(result));
+    printf("OUTPUT stream ----------------------------------------\n");
+    argParser.setFormat(requestedOutputFormat);
+    result = player.open(argParser, MyDataCallbackProc, MyErrorCallbackProc, &loopbackData);
     if (result != AAUDIO_OK) {
+        fprintf(stderr, "ERROR -  player.open() returned %d\n", result);
         goto finish;
     }
+    outputStream = player.getStream();
+    argParser.compareWithStream(outputStream);
 
-    printf("Stream INPUT ---------------------\n");
-    loopbackData.actualInputChannelCount = AAudioStream_getChannelCount(loopbackData.inputStream);
-    printf("    channelCount: requested = %d, actual = %d\n", requestedInputChannelCount,
-           loopbackData.actualInputChannelCount);
-    printf("    framesPerBurst = %d\n", AAudioStream_getFramesPerBurst(loopbackData.inputStream));
-    printf("    bufferSize     = %d\n",
-           AAudioStream_getBufferSizeInFrames(loopbackData.inputStream));
-    printf("    bufferCapacity = %d\n",
-           AAudioStream_getBufferCapacityInFrames(loopbackData.inputStream));
+    actualOutputFormat = AAudioStream_getFormat(outputStream);
+    assert(actualOutputFormat == AAUDIO_FORMAT_PCM_FLOAT);
 
-    actualSharingMode = AAudioStream_getSharingMode(loopbackData.inputStream);
-    printf("    sharingMode: requested = %d, actual = %d\n",
-           requestedSharingMode, actualSharingMode);
-
-    actualInputFormat = AAudioStream_getFormat(loopbackData.inputStream);
-    printf("    dataFormat: requested = %d, actual = %d\n",
-           requestedInputFormat, actualInputFormat);
-    assert(actualInputFormat == AAUDIO_FORMAT_PCM_I16);
-
-    printf("    is MMAP used?         = %s\n", AAudioStream_isMMapUsed(loopbackData.inputStream)
-                                               ? "yes" : "no");
-
-
-    printf("Stream OUTPUT ---------------------\n");
-    // Check to see what kind of stream we actually got.
-    actualSampleRate = AAudioStream_getSampleRate(outputStream);
-    printf("    sampleRate: requested = %d, actual = %d\n", requestedSampleRate, actualSampleRate);
-    loopbackData.echoAnalyzer.setSampleRate(actualSampleRate);
-
-    loopbackData.actualOutputChannelCount = AAudioStream_getChannelCount(outputStream);
-    printf("    channelCount: requested = %d, actual = %d\n", requestedOutputChannelCount,
-           loopbackData.actualOutputChannelCount);
-
-    actualSharingMode = AAudioStream_getSharingMode(outputStream);
-    printf("    sharingMode: requested = %d, actual = %d\n",
-           requestedSharingMode, actualSharingMode);
+    printf("INPUT stream ----------------------------------------\n");
+    // Use different parameters for the input.
+    argParser.setNumberOfBursts(AAUDIO_UNSPECIFIED);
+    argParser.setFormat(requestedInputFormat);
+    argParser.setPerformanceMode(inputPerformanceLevel);
+    argParser.setChannelCount(requestedInputChannelCount);
+    argParser.setSharingMode(requestedInputSharingMode);
+    result = recorder.open(argParser);
+    if (result != AAUDIO_OK) {
+        fprintf(stderr, "ERROR -  recorder.open() returned %d\n", result);
+        goto finish;
+    }
+    loopbackData.inputStream = recorder.getStream();
+    argParser.compareWithStream(loopbackData.inputStream);
 
     // This is the number of frames that are read in one chunk by a DMA controller
     // or a DSP or a mixer.
     framesPerBurst = AAudioStream_getFramesPerBurst(outputStream);
-    printf("    framesPerBurst = %d\n", framesPerBurst);
 
-    result = AAudioStream_setBufferSizeInFrames(outputStream, burstsPerBuffer * framesPerBurst);
-    if (result < 0) { // may be positive buffer size
-        fprintf(stderr, "ERROR - AAudioStream_setBufferSize() returned %d\n", result);
-        goto finish;
-    }
-    printf("    bufferSize     = %d\n", AAudioStream_getBufferSizeInFrames(outputStream));
-    printf("    bufferCapacity = %d\n", AAudioStream_getBufferCapacityInFrames(outputStream));
+    actualInputFormat = AAudioStream_getFormat(outputStream);
+    assert(actualInputFormat == AAUDIO_FORMAT_PCM_I16);
 
-    actualOutputFormat = AAudioStream_getFormat(outputStream);
-    printf("    dataFormat: requested = %d, actual = %d\n",
-           requestedOutputFormat, actualOutputFormat);
-    assert(actualOutputFormat == AAUDIO_FORMAT_PCM_FLOAT);
 
-    printf("    is MMAP used?         = %s\n", AAudioStream_isMMapUsed(outputStream)
-                                               ? "yes" : "no");
+    loopbackData.actualInputChannelCount = recorder.getChannelCount();
+    loopbackData.actualOutputChannelCount = player.getChannelCount();
 
     // Allocate a buffer for the audio data.
     loopbackData.inputFramesMaximum = 32 * framesPerBurst;
@@ -813,49 +385,75 @@
     loopbackData.conversionBuffer = new float[loopbackData.inputFramesMaximum *
                                               loopbackData.actualInputChannelCount];
 
+    loopbackData.loopbackProcessor->reset();
 
-    // Start output first so input stream runs low.
-    result = AAudioStream_requestStart(outputStream);
+    result = recorder.start();
     if (result != AAUDIO_OK) {
-        fprintf(stderr, "ERROR - AAudioStream_requestStart(output) returned %d = %s\n",
-                result, AAudio_convertResultToText(result));
+        printf("ERROR - AAudioStream_requestStart(input) returned %d = %s\n",
+               result, AAudio_convertResultToText(result));
         goto finish;
     }
 
-    result = AAudioStream_requestStart(loopbackData.inputStream);
+    result = player.start();
     if (result != AAUDIO_OK) {
-        fprintf(stderr, "ERROR - AAudioStream_requestStart(input) returned %d = %s\n",
-                result, AAudio_convertResultToText(result));
+        printf("ERROR - AAudioStream_requestStart(output) returned %d = %s\n",
+               result, AAudio_convertResultToText(result));
         goto finish;
     }
 
     printf("------- sleep while the callback runs --------------\n");
     fflush(stdout);
-    sleep(NUM_SECONDS);
-
+    for (int i = requestedDuration; i > 0 ; i--) {
+        if (loopbackData.inputError != AAUDIO_OK) {
+            printf("  ERROR on input stream\n");
+            break;
+        } else if (loopbackData.outputError != AAUDIO_OK) {
+                printf("  ERROR on output stream\n");
+                break;
+        } else if (loopbackData.isDone) {
+                printf("  test says it is done!\n");
+                break;
+        } else {
+            sleep(1);
+            printf("%4d: ", i);
+            loopbackData.loopbackProcessor->printStatus();
+            int64_t framesWritten = AAudioStream_getFramesWritten(loopbackData.inputStream);
+            int64_t framesRead = AAudioStream_getFramesRead(loopbackData.inputStream);
+            printf(" input written = %lld, read %lld, xruns = %d\n",
+                   (long long) framesWritten,
+                   (long long) framesRead,
+                   AAudioStream_getXRunCount(outputStream)
+            );
+        }
+    }
 
     printf("input error = %d = %s\n",
                 loopbackData.inputError, AAudio_convertResultToText(loopbackData.inputError));
 
     printf("AAudioStream_getXRunCount %d\n", AAudioStream_getXRunCount(outputStream));
-    printf("framesRead    = %d\n", (int) AAudioStream_getFramesRead(outputStream));
-    printf("framesWritten = %d\n", (int) AAudioStream_getFramesWritten(outputStream));
+    printf("framesRead    = %8d\n", (int) AAudioStream_getFramesRead(outputStream));
+    printf("framesWritten = %8d\n", (int) AAudioStream_getFramesWritten(outputStream));
+    printf("min numFrames = %8d\n", (int) loopbackData.minNumFrames);
+    printf("max numFrames = %8d\n", (int) loopbackData.maxNumFrames);
 
-    loopbackData.loopbackProcessor->report();
+    if (loopbackData.inputError == AAUDIO_OK) {
+        if (testMode == TEST_SINE_MAGNITUDE) {
+            printAudioGraph(loopbackData.audioRecorder, 200);
+        }
+        loopbackData.loopbackProcessor->report();
+    }
 
-//    {
-//        int written = loopbackData.audioRecorder.save(FILENAME);
-//        printf("wrote %d mono samples to %s on Android device\n", written, FILENAME);
-//    }
-
+    {
+        int written = loopbackData.audioRecorder.save(FILENAME);
+        printf("main() wrote %d mono samples to %s on Android device\n", written, FILENAME);
+    }
 
 finish:
-    AAudioStream_close(outputStream);
-    AAudioStream_close(loopbackData.inputStream);
+    player.close();
+    recorder.close();
     delete[] loopbackData.conversionBuffer;
     delete[] loopbackData.inputData;
     delete[] outputData;
-    AAudioStreamBuilder_delete(builder);
 
     printf(RESULT_TAG "error = %d = %s\n", result, AAudio_convertResultToText(result));
     if ((result != AAUDIO_OK)) {
diff --git a/media/libaaudio/examples/loopback/src/loopback.sh b/media/libaaudio/examples/loopback/src/loopback.sh
new file mode 100644
index 0000000..bc63125
--- /dev/null
+++ b/media/libaaudio/examples/loopback/src/loopback.sh
@@ -0,0 +1,14 @@
+#!/system/bin/sh
+# Run a loopback test in the background after a delay.
+# To run the script enter:
+#    adb shell "nohup sh /data/loopback.sh &"
+
+SLEEP_TIME=10
+TEST_COMMAND="aaudio_loopback -pl -Pl -C1 -n2 -m2 -tm -d5"
+
+echo "Plug in USB Mir and Fun Plug."
+echo "Test will start in ${SLEEP_TIME} seconds: ${TEST_COMMAND}"
+sleep ${SLEEP_TIME}
+date > /data/loopreport.txt
+${TEST_COMMAND} >> /data/loopreport.txt
+date >> /data/loopreport.txt
diff --git a/media/libaaudio/examples/utils/AAudioArgsParser.h b/media/libaaudio/examples/utils/AAudioArgsParser.h
index 54217a5..46bc99e 100644
--- a/media/libaaudio/examples/utils/AAudioArgsParser.h
+++ b/media/libaaudio/examples/utils/AAudioArgsParser.h
@@ -121,7 +121,7 @@
     aaudio_sharing_mode_t      mSharingMode     = AAUDIO_SHARING_MODE_SHARED;
     aaudio_performance_mode_t  mPerformanceMode = AAUDIO_PERFORMANCE_MODE_NONE;
 
-    int32_t                    mNumberOfBursts = AAUDIO_UNSPECIFIED;
+    int32_t                    mNumberOfBursts  = AAUDIO_UNSPECIFIED;
 };
 
 class AAudioArgsParser : public AAudioParameters {
@@ -151,9 +151,13 @@
                 case 'd':
                     mDurationSeconds = atoi(&arg[2]);
                     break;
-                case 'm':
-                    AAudio_setMMapPolicy(AAUDIO_POLICY_AUTO);
-                    break;
+                case 'm': {
+                    aaudio_policy_t policy = AAUDIO_POLICY_AUTO;
+                    if (strlen(arg) > 2) {
+                        policy = atoi(&arg[2]);
+                    }
+                    AAudio_setMMapPolicy(policy);
+                } break;
                 case 'n':
                     setNumberOfBursts(atoi(&arg[2]));
                     break;
@@ -198,7 +202,11 @@
         printf("      -b{bufferCapacity} frames\n");
         printf("      -c{channels} for example 2 for stereo\n");
         printf("      -d{duration} in seconds, default is %d\n", DEFAULT_DURATION_SECONDS);
-        printf("      -m enable MMAP\n");
+        printf("      -m{0|1|2|3} set MMAP policy\n");
+        printf("          0 = _UNSPECIFIED, default\n");
+        printf("          1 = _NEVER\n");
+        printf("          2 = _AUTO, also if -m is used with no number\n");
+        printf("          3 = _ALWAYS\n");
         printf("      -n{numberOfBursts} for setBufferSize\n");
         printf("      -p{performanceMode} set output AAUDIO_PERFORMANCE_MODE*, default NONE\n");
         printf("          n for _NONE\n");
diff --git a/media/libaaudio/examples/utils/AAudioSimplePlayer.h b/media/libaaudio/examples/utils/AAudioSimplePlayer.h
index 19f8aff..4557f02 100644
--- a/media/libaaudio/examples/utils/AAudioSimplePlayer.h
+++ b/media/libaaudio/examples/utils/AAudioSimplePlayer.h
@@ -219,6 +219,8 @@
 typedef struct SineThreadedData_s {
     SineGenerator  sineOsc1;
     SineGenerator  sineOsc2;
+    int32_t        minNumFrames = INT32_MAX;
+    int32_t        maxNumFrames = 0;
     int            scheduler;
     bool           schedulerChecked;
 } SineThreadedData_t;
@@ -243,6 +245,13 @@
         sineData->schedulerChecked = true;
     }
 
+    if (numFrames > sineData->maxNumFrames) {
+        sineData->maxNumFrames = numFrames;
+    }
+    if (numFrames < sineData->minNumFrames) {
+        sineData->minNumFrames = numFrames;
+    }
+
     int32_t samplesPerFrame = AAudioStream_getChannelCount(stream);
     // This code only plays on the first one or two channels.
     // TODO Support arbitrary number of channels.
diff --git a/media/libaaudio/examples/utils/AAudioSimpleRecorder.h b/media/libaaudio/examples/utils/AAudioSimpleRecorder.h
index cb12fda..5ecac04 100644
--- a/media/libaaudio/examples/utils/AAudioSimpleRecorder.h
+++ b/media/libaaudio/examples/utils/AAudioSimpleRecorder.h
@@ -73,12 +73,20 @@
     /**
      * Only call this after open() has been called.
      */
-    int32_t getSamplesPerFrame() {
+    int32_t getChannelCount() {
         if (mStream == nullptr) {
             return AAUDIO_ERROR_INVALID_STATE;
         }
         return AAudioStream_getChannelCount(mStream);
     }
+
+    /**
+     * @deprecated use getChannelCount()
+     */
+    int32_t getSamplesPerFrame() {
+        return getChannelCount();
+    }
+
     /**
      * Only call this after open() has been called.
      */
diff --git a/media/libaaudio/examples/write_sine/src/write_sine_callback.cpp b/media/libaaudio/examples/write_sine/src/write_sine_callback.cpp
index 2211b72..01cf7e7 100644
--- a/media/libaaudio/examples/write_sine/src/write_sine_callback.cpp
+++ b/media/libaaudio/examples/write_sine/src/write_sine_callback.cpp
@@ -120,6 +120,9 @@
                SCHED_FIFO);
     }
 
+    printf("min numFrames = %8d\n", (int) myData.minNumFrames);
+    printf("max numFrames = %8d\n", (int) myData.maxNumFrames);
+
     printf("SUCCESS\n");
     return EXIT_SUCCESS;
 error: