OboeTester: share WAVE from glitch test

Toggle buttons.
MultiChannelRecording wraps and save last recorded audio.
diff --git a/apps/OboeTester/app/build.gradle b/apps/OboeTester/app/build.gradle
index 642e484..c85dc88 100644
--- a/apps/OboeTester/app/build.gradle
+++ b/apps/OboeTester/app/build.gradle
@@ -6,8 +6,8 @@
         applicationId = "com.google.sample.oboe.manualtest"
         minSdkVersion 26
         targetSdkVersion 26
-        versionCode 10
-        versionName "1.4.01"
+        versionCode 11
+        versionName "1.4.02"
         testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
         externalNativeBuild {
             cmake {
diff --git a/apps/OboeTester/app/src/main/AndroidManifest.xml b/apps/OboeTester/app/src/main/AndroidManifest.xml
index a629a40..89a825d 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="10"
-    android:versionName="1.4.01">
+    android:versionCode="10=1"
+    android:versionName="1.4.02">
     <!-- versionCode and versionName also have to be updated in build.gradle -->
 
     <uses-feature android:name="android.hardware.microphone" android:required="true" />
diff --git a/apps/OboeTester/app/src/main/cpp/FullDuplexAnalyzer.cpp b/apps/OboeTester/app/src/main/cpp/FullDuplexAnalyzer.cpp
index 428ce5a..130479f 100644
--- a/apps/OboeTester/app/src/main/cpp/FullDuplexAnalyzer.cpp
+++ b/apps/OboeTester/app/src/main/cpp/FullDuplexAnalyzer.cpp
@@ -27,6 +27,10 @@
         int   numInputFrames,
         void *outputData,
         int   numOutputFrames) {
+
+    int32_t inputStride = getInputStream()->getChannelCount();
+    int32_t outputStride = getOutputStream()->getChannelCount();
+
     // TODO Pull up into superclass
     // reset analyzer if we miss some input data
     if (numInputFrames < numOutputFrames) {
@@ -36,9 +40,8 @@
     } else {
         float *inputFloat = (float *) inputData;
         float *outputFloat = (float *) outputData;
-        int32_t outputStride = getOutputStream()->getChannelCount();
 
-        (void) getLoopbackProcessor()->process(inputFloat, getInputStream()->getChannelCount(),
+        (void) getLoopbackProcessor()->process(inputFloat, inputStride,
                                        outputFloat, outputStride,
                                        numOutputFrames);
 
@@ -50,5 +53,23 @@
             memset(outputFloat, 0, framesLeft * getOutputStream()->getBytesPerFrame());
         }
     }
+
+    // write the first channel of output and input to the recorder
+    if (mRecording != nullptr) {
+        float buffer[2];
+        float *inputFloat = (float *) inputData;
+        float *outputFloat = (float *) outputData;
+        for (int i = 0; i < numOutputFrames; i++) {
+            buffer[0] = *outputFloat;
+            outputFloat += outputStride;
+            if (i < numInputFrames) {
+                buffer[1] = *inputFloat;
+                inputFloat += inputStride;
+            } else {
+                buffer[1] = 0.0f;
+            }
+            mRecording->write(buffer, 1);
+        }
+    }
     return oboe::DataCallbackResult::Continue;
 };
diff --git a/apps/OboeTester/app/src/main/cpp/FullDuplexAnalyzer.h b/apps/OboeTester/app/src/main/cpp/FullDuplexAnalyzer.h
index 0b7ce9f..b323ffb 100644
--- a/apps/OboeTester/app/src/main/cpp/FullDuplexAnalyzer.h
+++ b/apps/OboeTester/app/src/main/cpp/FullDuplexAnalyzer.h
@@ -23,6 +23,7 @@
 #include "oboe/Oboe.h"
 #include "FullDuplexStream.h"
 #include "LatencyAnalyzer.h"
+#include "MultiChannelRecording.h"
 
 class FullDuplexAnalyzer : public FullDuplexStream {
 public:
@@ -47,6 +48,13 @@
 
     virtual LoopbackProcessor *getLoopbackProcessor() = 0;
 
+    void setRecording(MultiChannelRecording *recording) {
+        mRecording = recording;
+    }
+
+private:
+    MultiChannelRecording  *mRecording = nullptr;
+
 };
 
 
diff --git a/apps/OboeTester/app/src/main/cpp/MultiChannelRecording.h b/apps/OboeTester/app/src/main/cpp/MultiChannelRecording.h
index 09d2acb..c2acd4a 100644
--- a/apps/OboeTester/app/src/main/cpp/MultiChannelRecording.h
+++ b/apps/OboeTester/app/src/main/cpp/MultiChannelRecording.h
@@ -21,6 +21,13 @@
 #include <unistd.h>
 #include <sys/types.h>
 
+/**
+ * Store multi-channel audio data in float format.
+ * The most recent data will be saved.
+ * Old data may be overwritten.
+ *
+ * Note that this is not thread safe. Do not read and write from separate threads.
+ */
 class MultiChannelRecording {
 public:
     MultiChannelRecording(int32_t channelCount, int32_t maxFrames)
@@ -34,7 +41,12 @@
     }
 
     void rewind() {
-        mCursorSample = 0;
+        mReadCursorFrames = mWriteCursorFrames - getSizeInFrames();
+    }
+
+    void clear() {
+        mReadCursorFrames = 0;
+        mWriteCursorFrames = 0;
     }
 
     int32_t getChannelCount() {
@@ -42,14 +54,14 @@
     }
 
     int32_t getSizeInFrames() {
-        return mValidFrames;
+        return (int32_t) std::min(mWriteCursorFrames, static_cast<int64_t>(mMaxFrames));
     }
 
-    /**
-     * @return pointer to data owned by this object
-     */
-    float *getData() {
-        return mData;
+    int32_t getReadIndex() {
+        return mReadCursorFrames % mMaxFrames;
+    }
+    int32_t getWriteIndex() {
+        return mWriteCursorFrames % mMaxFrames;
     }
 
     /**
@@ -61,46 +73,52 @@
      * @return number of frames actually written.
      */
     int32_t write(int16_t *buffer, int32_t numFrames) {
-        int32_t framesEmpty = mMaxFrames - mValidFrames;
-        if (numFrames > framesEmpty) {
-            numFrames = framesEmpty;
-        }
-        if (numFrames > 0) {
-            int32_t numSamples = numFrames * mChannelCount;
-            int32_t index = mValidFrames * mChannelCount;
+        int32_t framesLeft = numFrames;
+        while (framesLeft > 0) {
+            int32_t indexFrame = getWriteIndex();
+            // contiguous writes
+            int32_t framesToEnd = mMaxFrames - indexFrame;
+            int32_t framesNow = std::min(framesLeft, framesToEnd);
+            int32_t numSamples = framesNow * mChannelCount;
+            int32_t sampleIndex = indexFrame * mChannelCount;
+
             for (int i = 0; i < numSamples; i++) {
-                mData[index++] = buffer[i * mChannelCount] * (1.0f / 32768);
+                mData[sampleIndex++] = buffer[i * mChannelCount] * (1.0f / 32768);
             }
-            mValidFrames += numFrames;
+
+            mWriteCursorFrames += framesNow;
+            framesLeft -= framesNow;
         }
         return numFrames;
     }
 
     /**
-     * Write numFrames from the float buffer into the recording, if there is room.
+     * Write all numFrames from the float buffer into the recording.
+     * Overwrite old data if full.
      * @param buffer
      * @param numFrames
      * @return number of frames actually written.
      */
     int32_t write(float *buffer, int32_t numFrames) {
-        int32_t framesEmpty = mMaxFrames - mValidFrames;
-        if (numFrames > framesEmpty) {
-            numFrames = framesEmpty;
-        }
-        if (numFrames > 0) {
-            int32_t numSamples = numFrames * mChannelCount;
-            memcpy(mData + (mValidFrames * mChannelCount),
+        int32_t framesLeft = numFrames;
+        while (framesLeft > 0) {
+            int32_t indexFrame = getWriteIndex();
+            // contiguous writes
+            int32_t framesToEnd = mMaxFrames - indexFrame;
+            int32_t framesNow = std::min(framesLeft, framesToEnd);
+            int32_t numSamples = framesNow * mChannelCount;
+            int32_t sampleIndex = indexFrame * mChannelCount;
+
+            memcpy(&mData[sampleIndex],
                    buffer,
                    (numSamples * sizeof(float)));
-            mValidFrames += numFrames;
+
+            mWriteCursorFrames += framesNow;
+            framesLeft -= framesNow;
         }
         return numFrames;
     }
 
-    float read() {
-        return mData[mCursorSample++];
-    }
-
     /**
      * Read numFrames from the recording into the buffer, if there is enough data.
      * Start at the cursor position, aligned up to the next frame.
@@ -109,26 +127,32 @@
      * @return number of frames actually read.
      */
     int32_t read(float *buffer, int32_t numFrames) {
-        // round up to nearest frame
-        mCursorSample += mChannelCount - (mCursorSample % mChannelCount);
-        int32_t framesLeft = mValidFrames - mCursorSample;
-        if (numFrames > framesLeft) {
-            numFrames = framesLeft;
-        }
-        if (numFrames > 0) {
-            int32_t numSamples = numFrames * mChannelCount;
+        int32_t framesRead = 0;
+        int32_t framesLeft = std::min(numFrames,
+                std::min(mMaxFrames, (int32_t)(mWriteCursorFrames - mReadCursorFrames)));
+        while (framesLeft > 0) {
+            int32_t indexFrame = getReadIndex();
+            // contiguous reads
+            int32_t framesToEnd = mMaxFrames - indexFrame;
+            int32_t framesNow = std::min(framesLeft, framesToEnd);
+            int32_t numSamples = framesNow * mChannelCount;
+            int32_t sampleIndex = indexFrame * mChannelCount;
+
             memcpy(buffer,
-                   &mData[mCursorSample],
+                   &mData[sampleIndex],
                    (numSamples * sizeof(float)));
-            mCursorSample += numSamples;
+
+            mReadCursorFrames += framesNow;
+            framesLeft -= framesNow;
+            framesRead += framesNow;
         }
-        return numFrames;
+        return framesRead;
     }
 
 private:
     float          *mData = nullptr;
-    int32_t         mValidFrames = 0;
-    int32_t         mCursorSample = 0;
+    int64_t         mReadCursorFrames = 0;
+    int64_t         mWriteCursorFrames = 0; // monotonically increasing
     const int32_t   mChannelCount;
     const int32_t   mMaxFrames;
 };
diff --git a/apps/OboeTester/app/src/main/cpp/NativeAudioContext.cpp b/apps/OboeTester/app/src/main/cpp/NativeAudioContext.cpp
index 3a64eb5..baf46d8 100644
--- a/apps/OboeTester/app/src/main/cpp/NativeAudioContext.cpp
+++ b/apps/OboeTester/app/src/main/cpp/NativeAudioContext.cpp
@@ -22,8 +22,6 @@
 
 #include "NativeAudioContext.h"
 
-#define SECONDS_TO_RECORD   10
-
 static oboe::AudioApi convertNativeApiToAudioApi(int nativeApi) {
     switch (nativeApi) {
         default:
@@ -229,7 +227,10 @@
         mFramesPerBurst = oboeStream->getFramesPerBurst();
         mSampleRate = oboeStream->getSampleRate();
 
+        createRecording();
+
         finishOpen(isInput, oboeStream);
+
     }
 
     if (!useCallback) {
@@ -237,8 +238,6 @@
         dataBuffer = std::make_unique<float[]>(numSamples);
     }
 
-    mRecording = std::make_unique<MultiChannelRecording>(mChannelCount,
-                                                         SECONDS_TO_RECORD * mSampleRate);
 
     return ((int)result < 0) ? (int)result : streamIndex;
 }
@@ -279,16 +278,19 @@
     writer.setFrameRate(mSampleRate);
     writer.setSamplesPerFrame(mRecording->getChannelCount());
     writer.setBitsPerSample(24);
-    int32_t numSamples = mRecording->getSizeInFrames() * mRecording->getChannelCount();
+    float buffer[mRecording->getChannelCount()];
     // Read samples from start to finish.
     mRecording->rewind();
-    for (int32_t i = 0; i < numSamples; i++) {
-        writer.write(mRecording->read());
+    for (int32_t frameIndex = 0; frameIndex < mRecording->getSizeInFrames(); frameIndex++) {
+        mRecording->read(buffer, 1 /* numFrames */);
+        for (int32_t i = 0; i < mRecording->getChannelCount(); i++) {
+            writer.write(buffer[i]);
+        }
     }
     writer.close();
 
     auto myfile = std::ofstream(filename, std::ios::out | std::ios::binary);
-    myfile.write((char *)outStream.getData(), outStream.length());
+    myfile.write((char *) outStream.getData(), outStream.length());
     myfile.close();
 
     return outStream.length();
@@ -575,6 +577,7 @@
 void ActivityGlitches::finishOpen(bool isInput, oboe::AudioStream *oboeStream) {
     if (isInput) {
         mFullDuplexGlitches->setInputStream(oboeStream);
+        mFullDuplexGlitches->setRecording(mRecording.get());
     } 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 5c895ee..9aadbb0 100644
--- a/apps/OboeTester/app/src/main/cpp/NativeAudioContext.h
+++ b/apps/OboeTester/app/src/main/cpp/NativeAudioContext.h
@@ -64,6 +64,8 @@
 #define LIB_AAUDIO_NAME          "libaaudio.so"
 #define FUNCTION_IS_MMAP         "AAudioStream_isMMapUsed"
 
+#define SECONDS_TO_RECORD        10
+
 typedef struct AAudioStreamStruct         AAudioStream;
 
 /**
@@ -178,6 +180,11 @@
     int32_t allocateStreamIndex();
     void freeStreamIndex(int32_t streamIndex);
 
+    virtual void createRecording() {
+        mRecording = std::make_unique<MultiChannelRecording>(mChannelCount,
+                                                             SECONDS_TO_RECORD * mSampleRate);
+    }
+
     virtual void finishOpen(bool isInput, oboe::AudioStream *oboeStream) {}
 
     virtual oboe::Result startStreams() = 0;
@@ -356,6 +363,12 @@
     int32_t getResetCount() {
         return getFullDuplexAnalyzer()->getLoopbackProcessor()->getResetCount();
     }
+
+protected:
+    void createRecording() override {
+        mRecording = std::make_unique<MultiChannelRecording>(2, // output and input
+                                                             SECONDS_TO_RECORD * mSampleRate);
+    }
 };
 
 /**
diff --git a/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/AutoGlitchActivity.java b/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/AutoGlitchActivity.java
index b1d8a57..7a4a848 100644
--- a/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/AutoGlitchActivity.java
+++ b/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/AutoGlitchActivity.java
@@ -1,3 +1,19 @@
+/*
+ * 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.
+ */
+
 package com.google.sample.oboe.manualtest;
 
 import android.content.Intent;
diff --git a/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/BufferSizeView.java b/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/BufferSizeView.java
index d9d8e39..293bfd7 100644
--- a/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/BufferSizeView.java
+++ b/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/BufferSizeView.java
@@ -1,3 +1,19 @@
+/*
+ * 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.
+ */
+
 package com.google.sample.oboe.manualtest;
 
 import android.content.Context;
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
index 718c44f..01e3f30 100644
--- 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
@@ -27,9 +27,10 @@
  * Activity to measure the number of glitches.
  */
 public class GlitchActivity extends AnalyzerActivity {
-    TextView mAnalyzerTextView;
-    Button mStartButton;
-    Button mStopButton;
+    private TextView mAnalyzerTextView;
+    private Button mStartButton;
+    private Button mStopButton;
+    private Button mShareButton;
 
     // These must match the values in LatencyAnalyzer.h
     final static int STATE_IDLE = 0;
@@ -186,6 +187,8 @@
         mStartButton = (Button) findViewById(R.id.button_start);
         mStopButton = (Button) findViewById(R.id.button_stop);
         mStopButton.setEnabled(false);
+        mShareButton = (Button) findViewById(R.id.button_share);
+        mShareButton.setEnabled(false);
         mAnalyzerTextView = (TextView) findViewById(R.id.text_analyzer_result);
         updateEnabledWidgets();
         hideSettingsViews();
@@ -197,6 +200,7 @@
         setActivityType(ACTIVITY_GLITCHES);
         mStartButton.setEnabled(true);
         mStopButton.setEnabled(false);
+        mShareButton.setEnabled(false);
     }
 
     @Override
@@ -210,6 +214,7 @@
         startAudioTest();
         mStartButton.setEnabled(false);
         mStopButton.setEnabled(true);
+        mShareButton.setEnabled(false);
     }
 
     public void startAudioTest() {
@@ -233,6 +238,7 @@
     public void onTestFinished() {
         mStartButton.setEnabled(true);
         mStopButton.setEnabled(false);
+        mShareButton.setEnabled(true);
     }
 
     public void stopAudioTest() {
@@ -257,4 +263,9 @@
     public int getLastGlitchCount() {
         return mGlitchSniffer.geMLastGlitchCount();
     }
+
+    @Override
+    String getWaveTag() {
+        return "glitches";
+    }
 }
diff --git a/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/ManualGlitchActivity.java b/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/ManualGlitchActivity.java
index 0b59181..a988b38 100644
--- a/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/ManualGlitchActivity.java
+++ b/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/ManualGlitchActivity.java
@@ -1,3 +1,19 @@
+/*
+ * 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.
+ */
+
 package com.google.sample.oboe.manualtest;
 
 public class ManualGlitchActivity extends GlitchActivity {
diff --git a/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/RecorderActivity.java b/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/RecorderActivity.java
index f050c70..a6336d7 100644
--- a/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/RecorderActivity.java
+++ b/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/RecorderActivity.java
@@ -91,7 +91,9 @@
 
     }
 
-    public void onShareFile(View view) {
-        shareWaveFile();
+    @Override
+    String getWaveTag() {
+        return "recording";
     }
+
 }
diff --git a/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/TestInputActivity.java b/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/TestInputActivity.java
index 1f4e506..077a4ab 100644
--- a/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/TestInputActivity.java
+++ b/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/TestInputActivity.java
@@ -27,6 +27,7 @@
 import android.support.annotation.NonNull;
 import android.support.v4.app.ActivityCompat;
 import android.support.v4.content.FileProvider;
+import android.view.View;
 import android.widget.TextView;
 import android.widget.Toast;
 
@@ -156,11 +157,15 @@
         return result;
     }
 
+    String getWaveTag() {
+        return "input";
+    }
+
     @NonNull
     private File createFileName() {
         // Get directory and filename
         File dir = getExternalFilesDir(Environment.DIRECTORY_MUSIC);
-        return new File(dir, "oboe_recording.wav");
+        return new File(dir, "oboe_" +  getWaveTag() + ".wav");
     }
 
     public void shareWaveFile() {
@@ -170,7 +175,7 @@
         if (result > 0) {
             Intent sharingIntent = new Intent(android.content.Intent.ACTION_SEND);
             sharingIntent.setType("audio/wav");
-            String subjectText = "OboeTester recording at " + getTimestampString();
+            String subjectText = "OboeTester " +  getWaveTag() + " at " + getTimestampString();
             sharingIntent.putExtra(android.content.Intent.EXTRA_SUBJECT, subjectText);
             Uri uri = FileProvider.getUriForFile(this,
                     BuildConfig.APPLICATION_ID + ".provider",
@@ -181,4 +186,7 @@
         }
     }
 
+    public void onShareFile(View view) {
+        shareWaveFile();
+    }
 }