OboeTester: add InterpolatingDelayLine

Cleanup AudioStreamGateway
Bump test to version 1.3.00
diff --git a/apps/OboeTester/app/build.gradle b/apps/OboeTester/app/build.gradle
index c32b8b5..97c5aac 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 3
-        versionName "1.2.02"
+        versionCode 4
+        versionName "1.3.00"
         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 c393f56..3ea2dfa 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="3"
-    android:versionName="1.2.02">
+    android:versionCode="4"
+    android:versionName="1.3.00">
     <!-- versionCode and versionName also have to be updated in build.gradle -->
 
     <uses-feature android:name="android.hardware.microphone" android:required="true" />
diff --git a/apps/OboeTester/app/src/main/cpp/AudioStreamGateway.cpp b/apps/OboeTester/app/src/main/cpp/AudioStreamGateway.cpp
index 6d21ebe..431db4d 100644
--- a/apps/OboeTester/app/src/main/cpp/AudioStreamGateway.cpp
+++ b/apps/OboeTester/app/src/main/cpp/AudioStreamGateway.cpp
@@ -22,13 +22,13 @@
 
 using namespace flowgraph;
 
-AudioStreamGateway::AudioStreamGateway(int samplesPerFrame)
-{
-}
-
-AudioStreamGateway::~AudioStreamGateway()
-{
-}
+//AudioStreamGateway::AudioStreamGateway(int samplesPerFrame)
+//{
+//}
+//
+//AudioStreamGateway::~AudioStreamGateway()
+//{
+//}
 
 int64_t AudioStreamGateway::mFramePosition = 0;
 
diff --git a/apps/OboeTester/app/src/main/cpp/AudioStreamGateway.h b/apps/OboeTester/app/src/main/cpp/AudioStreamGateway.h
index 8eda5d1..2192094 100644
--- a/apps/OboeTester/app/src/main/cpp/AudioStreamGateway.h
+++ b/apps/OboeTester/app/src/main/cpp/AudioStreamGateway.h
@@ -25,14 +25,14 @@
 using namespace flowgraph;
 
 /**
- * Bridge between an audio graph and an audio device.
+ * Bridge between an audio flowgraph and an audio device.
  * Pass in an AudioSink and then pass
  * this object to the AudioStreamBuilder as a callback.
  */
 class AudioStreamGateway : public oboe::AudioStreamCallback {
 public:
-    AudioStreamGateway(int samplesPerFrame);
-    virtual ~AudioStreamGateway();
+//    AudioStreamGateway(int samplesPerFrame);
+    virtual ~AudioStreamGateway() = default;
 
     void setAudioSink(std::shared_ptr<flowgraph::AudioSink>  sink) {
         mAudioSink = sink;
diff --git a/apps/OboeTester/app/src/main/cpp/FullDuplexEcho.cpp b/apps/OboeTester/app/src/main/cpp/FullDuplexEcho.cpp
index 3f37d8a..1b34692 100644
--- a/apps/OboeTester/app/src/main/cpp/FullDuplexEcho.cpp
+++ b/apps/OboeTester/app/src/main/cpp/FullDuplexEcho.cpp
@@ -18,9 +18,9 @@
 #include "FullDuplexEcho.h"
 
 oboe::Result  FullDuplexEcho::start() {
-    mDelaySizeFrames = 3 * getOutputStream()->getSampleRate();
-    static const int32_t maxDelaySamples = mDelaySizeFrames * getOutputStream()->getChannelCount();
-    mDelayLine = std::make_unique<float[]>(maxDelaySamples);
+    int32_t delaySize = 3 * getOutputStream()->getSampleRate();
+    mDelayFrames = delaySize - 2;
+    mDelayLine = std::make_unique<InterpolatingDelayLine>(delaySize);
     return FullDuplexStream::start();
 }
 
@@ -30,20 +30,31 @@
         void *outputData,
         int   numOutputFrames) {
     // FIXME only handles matching stream formats.
-//    LOGE("FullDuplexEcho::%s() called, numInputFrames = %d, numOutputFrames = %d",
-//            __func__, numInputFrames, numOutputFrames);
-    int32_t framesLeft = std::min(numInputFrames, numOutputFrames);
-    while (framesLeft > 0) {
-        float *delayAddress = mDelayLine.get() + (mCursorFrames * getOutputStream()->getChannelCount());
-        memcpy(outputData, delayAddress, getOutputStream()->getBytesPerFrame());
-        memcpy(delayAddress, inputData, getOutputStream()->getBytesPerFrame());
-        mCursorFrames++;
-        if (mCursorFrames >= mDelaySizeFrames) {
-            mCursorFrames = 0;
+    // TODO use array of delays
+    // TODO Add delay node
+    // TODO use flowgraph to handle format conversion
+    int32_t framesToEcho = std::min(numInputFrames, numOutputFrames);
+    float *inputFloat = (float *)inputData;
+    float *outputFloat = (float *)outputData;
+    int32_t inputStride = getInputStream()->getChannelCount();
+    int32_t outputStride = getOutputStream()->getChannelCount();
+    if (outputStride == 1) {
+        while (framesToEcho-- > 0) {
+            *outputFloat++ = mDelayLine->process(mDelayFrames, *inputFloat); // mono delay
+            inputFloat += inputStride;
         }
-        inputData = ((uint8_t *)inputData) + getOutputStream()->getBytesPerFrame();
-        outputData = ((uint8_t *)outputData) + getOutputStream()->getBytesPerFrame();
-        framesLeft--;
+    } else if (outputStride == 2) {
+        while (framesToEcho-- > 0) {
+            *outputFloat++ = mDelayLine->process(mDelayFrames, *inputFloat); // mono delay
+            *outputFloat++ = 0.0f; // FIXME *inputFloat; // mono
+            inputFloat += inputStride;
+        }
+    } // else TODO
+
+    // zero out remainder of output array
+    int32_t framesLeft = numOutputFrames - numInputFrames;
+    if (framesLeft > 0) {
+        memset(outputFloat, 0, framesLeft * getOutputStream()->getBytesPerFrame());
     }
     return oboe::DataCallbackResult::Continue;
 };
diff --git a/apps/OboeTester/app/src/main/cpp/FullDuplexEcho.h b/apps/OboeTester/app/src/main/cpp/FullDuplexEcho.h
index 668bf42..7a755ec 100644
--- a/apps/OboeTester/app/src/main/cpp/FullDuplexEcho.h
+++ b/apps/OboeTester/app/src/main/cpp/FullDuplexEcho.h
@@ -22,6 +22,7 @@
 
 #include "oboe/Oboe.h"
 #include "FullDuplexStream.h"
+#include "InterpolatingDelayLine.h"
 
 class FullDuplexEcho : public FullDuplexStream {
 public:
@@ -41,9 +42,8 @@
     oboe::Result start() override;
 
 private:
-    std::unique_ptr<float[]> mDelayLine;
-    int32_t mCursorFrames = 0;
-    int32_t mDelaySizeFrames = 0;
+    std::unique_ptr<InterpolatingDelayLine> mDelayLine;
+    float mDelayFrames = 0.0f;
 };
 
 
diff --git a/apps/OboeTester/app/src/main/cpp/InterpolatingDelayLine.cpp b/apps/OboeTester/app/src/main/cpp/InterpolatingDelayLine.cpp
new file mode 100644
index 0000000..e69c711
--- /dev/null
+++ b/apps/OboeTester/app/src/main/cpp/InterpolatingDelayLine.cpp
@@ -0,0 +1,42 @@
+/*
+ * 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 "InterpolatingDelayLine.h"
+
+InterpolatingDelayLine::InterpolatingDelayLine(int32_t delaySize) {
+    mDelaySize = delaySize;
+    mDelayLine = std::make_unique<float[]>(delaySize);
+}
+
+float InterpolatingDelayLine::process(float delay, float input) {
+    float *writeAddress = mDelayLine.get() + mCursor;
+    *writeAddress = input;
+    mDelayLine.get()[mCursor] = input;
+    int32_t delayInt = std::min(mDelaySize - 1, (int32_t) delay);
+    int32_t readIndex = mCursor - delayInt;
+    if (readIndex < 0) {
+        readIndex += mDelaySize;
+    }
+    // TODO interpolate
+    float *readAddress = mDelayLine.get() + readIndex;
+    float output = *readAddress;
+    mCursor++;
+    if (mCursor >= mDelaySize) {
+        mCursor = 0;
+    }
+    return output;
+};
diff --git a/apps/OboeTester/app/src/main/cpp/InterpolatingDelayLine.h b/apps/OboeTester/app/src/main/cpp/InterpolatingDelayLine.h
new file mode 100644
index 0000000..b3a510d
--- /dev/null
+++ b/apps/OboeTester/app/src/main/cpp/InterpolatingDelayLine.h
@@ -0,0 +1,48 @@
+/*
+ * 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_INTERPOLATING_DELAY_LINE_H
+#define OBOETESTER_INTERPOLATING_DELAY_LINE_H
+
+#include <memory>
+#include <unistd.h>
+#include <sys/types.h>
+
+#include "oboe/Oboe.h"
+#include "FullDuplexStream.h"
+
+/**
+ * Monophonic delay line.
+ */
+class InterpolatingDelayLine  {
+public:
+    explicit InterpolatingDelayLine(int32_t delaySize);
+
+    /**
+     * @param input sample to be written to the delay line
+     * @param delay number of samples to delay the output
+     * @return delayed value
+     */
+    float process(float delay, float input);
+
+private:
+    std::unique_ptr<float[]> mDelayLine;
+    int32_t mCursor = 0;
+    int32_t mDelaySize = 0;
+};
+
+
+#endif //OBOETESTER_INTERPOLATING_DELAY_LINE_H
diff --git a/apps/OboeTester/app/src/main/cpp/NativeAudioContext.cpp b/apps/OboeTester/app/src/main/cpp/NativeAudioContext.cpp
index de1124c..9a72bbd 100644
--- a/apps/OboeTester/app/src/main/cpp/NativeAudioContext.cpp
+++ b/apps/OboeTester/app/src/main/cpp/NativeAudioContext.cpp
@@ -48,7 +48,6 @@
     LOGD("%s() delete nodes", __func__);
     manyToMulti.reset(nullptr);
     monoToMulti.reset(nullptr);
-    audioStreamGateway.reset(nullptr);
     mSinkFloat.reset();
     mSinkI16.reset();
 }
@@ -328,15 +327,14 @@
 
         // We needed the proxy because we did not know the channelCount
         // when we setup the Builder.
-        audioStreamGateway = std::make_unique<AudioStreamGateway>(mChannelCount);
         if (outputStream->getFormat() == oboe::AudioFormat::I16) {
-            audioStreamGateway->setAudioSink(mSinkI16);
+            audioStreamGateway.setAudioSink(mSinkI16);
         } else if (outputStream->getFormat() == oboe::AudioFormat::Float) {
-            audioStreamGateway->setAudioSink(mSinkFloat);
+            audioStreamGateway.setAudioSink(mSinkFloat);
         }
 
         if (useCallback) {
-            oboeCallbackProxy.setCallback(audioStreamGateway.get());
+            oboeCallbackProxy.setCallback(&audioStreamGateway);
         }
 
         // Set starting size of buffer.
@@ -441,9 +439,9 @@
             callbackResult = mInputAnalyzer.onAudioReady(oboeStream,
                                                          dataBuffer.get(),
                                                          framesRead);
-        } else if (audioStreamGateway != nullptr) {  // OUTPUT?
+        } else {  // OUTPUT?
             // generate output by calling the callback
-            callbackResult = audioStreamGateway->onAudioReady(oboeStream,
+            callbackResult = audioStreamGateway.onAudioReady(oboeStream,
                                                               dataBuffer.get(),
                                                               framesPerBlock);
 
diff --git a/apps/OboeTester/app/src/main/cpp/NativeAudioContext.h b/apps/OboeTester/app/src/main/cpp/NativeAudioContext.h
index 6cec33b..ec0d3e9 100644
--- a/apps/OboeTester/app/src/main/cpp/NativeAudioContext.h
+++ b/apps/OboeTester/app/src/main/cpp/NativeAudioContext.h
@@ -100,10 +100,8 @@
     }
 
     void printScheduler() {
-        if (audioStreamGateway != nullptr) {
-            int scheduler = audioStreamGateway->getScheduler();
-            LOGI("scheduler = 0x%08x, SCHED_FIFO = 0x%08X\n", scheduler, SCHED_FIFO);
-        }
+        int scheduler = audioStreamGateway.getScheduler();
+        LOGI("scheduler = 0x%08x, SCHED_FIFO = 0x%08X\n", scheduler, SCHED_FIFO);
     }
 
     oboe::Result pause() {
@@ -237,7 +235,10 @@
         mActivityType = (ActivityType) activityType;
     }
 
-    InputStreamCallbackAnalyzer  mInputAnalyzer;
+    double getPeakLevel(int index) {
+        return mInputAnalyzer.getPeakLevel(index);
+    }
+
     bool                         useCallback = true;
     bool                         callbackReturnStop = false;
     int                          callbackSize = 0;
@@ -299,6 +300,9 @@
     std::thread                 *dataThread = nullptr;
 
     OboeStreamCallbackProxy      oboeCallbackProxy;
+    AudioStreamGateway           audioStreamGateway;
+    InputStreamCallbackAnalyzer  mInputAnalyzer;
+
     std::vector<SineOscillator>  sineOscillators;
     std::vector<SawtoothOscillator>  sawtoothOscillators;
 
@@ -311,7 +315,6 @@
     std::unique_ptr<MonoToMultiConverter>   monoToMulti;
     std::shared_ptr<flowgraph::SinkFloat>   mSinkFloat;
     std::shared_ptr<flowgraph::SinkI16>     mSinkI16;
-    std::unique_ptr<AudioStreamGateway>     audioStreamGateway{};
     std::unique_ptr<MultiChannelRecording>  mRecording{};
     std::unique_ptr<FullDuplexEcho>         mFullDuplexEcho{};
 
diff --git a/apps/OboeTester/app/src/main/cpp/jni-bridge.cpp b/apps/OboeTester/app/src/main/cpp/jni-bridge.cpp
index ce7b803..be75f63 100644
--- a/apps/OboeTester/app/src/main/cpp/jni-bridge.cpp
+++ b/apps/OboeTester/app/src/main/cpp/jni-bridge.cpp
@@ -363,7 +363,7 @@
 Java_com_google_sample_oboe_manualtest_AudioInputTester_getPeakLevel(JNIEnv *env,
                                                           jobject instance,
                                                           jint index) {
-    return engine.mInputAnalyzer.getPeakLevel(index);
+    return engine.getPeakLevel(index);
 }
 
 JNIEXPORT void JNICALL
diff --git a/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/TestAudioActivity.java b/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/TestAudioActivity.java
index bb537b6..560610c 100644
--- a/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/TestAudioActivity.java
+++ b/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/TestAudioActivity.java
@@ -88,7 +88,7 @@
                     AudioStreamBase.StreamStatus status = streamContext.tester.getCurrentAudioStream().getStreamStatus();
                     int framesPerBurst = streamContext.tester.getCurrentAudioStream().getFramesPerBurst();
                     final String msg = status.dump(framesPerBurst);
-                    mStreamContexts.get(0).configurationView.setStatusText(msg);
+                    streamContext.configurationView.setStatusText(msg);
                     updateStreamDisplay();
                 }
 
diff --git a/apps/OboeTester/app/src/main/res/layout/activity_recorder.xml b/apps/OboeTester/app/src/main/res/layout/activity_recorder.xml
index 7527fa0..a366a8f 100644
--- a/apps/OboeTester/app/src/main/res/layout/activity_recorder.xml
+++ b/apps/OboeTester/app/src/main/res/layout/activity_recorder.xml
@@ -55,48 +55,28 @@
         android:lines="3"
         android:text="@string/init_status" />
 
-    <TextView
-        android:id="@+id/volumeText0"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:text="\?" />
-
     <com.google.sample.oboe.manualtest.VolumeBarView
         android:id="@+id/volumeBar0"
         android:layout_width="fill_parent"
+        android:layout_marginBottom="4dp"
         android:layout_height="20dp" />
 
-    <TextView
-        android:id="@+id/volumeText1"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:text="\?" />
-
     <com.google.sample.oboe.manualtest.VolumeBarView
         android:id="@+id/volumeBar1"
         android:layout_width="fill_parent"
+        android:layout_marginBottom="4dp"
         android:layout_height="20dp" />
 
-    <TextView
-        android:id="@+id/volumeText2"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:text="\?" />
-
     <com.google.sample.oboe.manualtest.VolumeBarView
         android:id="@+id/volumeBar2"
         android:layout_width="fill_parent"
+        android:layout_marginBottom="4dp"
         android:layout_height="20dp" />
 
-    <TextView
-        android:id="@+id/volumeText3"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:text="\?" />
-
     <com.google.sample.oboe.manualtest.VolumeBarView
         android:id="@+id/volumeBar3"
         android:layout_width="fill_parent"
+        android:layout_marginBottom="4dp"
         android:layout_height="20dp" />
 
 </LinearLayout>
diff --git a/apps/OboeTester/build.gradle b/apps/OboeTester/build.gradle
index 4f94500..c78c3f9 100644
--- a/apps/OboeTester/build.gradle
+++ b/apps/OboeTester/build.gradle
@@ -6,7 +6,7 @@
         google()
     }
     dependencies {
-        classpath 'com.android.tools.build:gradle:3.3.1'
+        classpath 'com.android.tools.build:gradle:3.2.1'
     }
 }