OboeTester: add FullDuplexStream and FullDuplexEcho
diff --git a/apps/OboeTester/app/CMakeLists.txt b/apps/OboeTester/app/CMakeLists.txt
index 2390007..2bca76c 100644
--- a/apps/OboeTester/app/CMakeLists.txt
+++ b/apps/OboeTester/app/CMakeLists.txt
@@ -28,6 +28,7 @@
 
 ### END OBOE INCLUDE SECTION ###
 
+
 # link to oboe
 target_link_libraries(oboetester log oboe atomic)
 
diff --git a/apps/OboeTester/app/src/main/cpp/FullDuplexEcho.cpp b/apps/OboeTester/app/src/main/cpp/FullDuplexEcho.cpp
new file mode 100644
index 0000000..3f37d8a
--- /dev/null
+++ b/apps/OboeTester/app/src/main/cpp/FullDuplexEcho.cpp
@@ -0,0 +1,49 @@
+/*
+ * 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 "FullDuplexEcho.h"
+
+oboe::Result  FullDuplexEcho::start() {
+    mDelaySizeFrames = 3 * getOutputStream()->getSampleRate();
+    static const int32_t maxDelaySamples = mDelaySizeFrames * getOutputStream()->getChannelCount();
+    mDelayLine = std::make_unique<float[]>(maxDelaySamples);
+    return FullDuplexStream::start();
+}
+
+oboe::DataCallbackResult FullDuplexEcho::onBothStreamsReady(
+        const void *inputData,
+        int   numInputFrames,
+        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;
+        }
+        inputData = ((uint8_t *)inputData) + getOutputStream()->getBytesPerFrame();
+        outputData = ((uint8_t *)outputData) + getOutputStream()->getBytesPerFrame();
+        framesLeft--;
+    }
+    return oboe::DataCallbackResult::Continue;
+};
diff --git a/apps/OboeTester/app/src/main/cpp/FullDuplexEcho.h b/apps/OboeTester/app/src/main/cpp/FullDuplexEcho.h
new file mode 100644
index 0000000..668bf42
--- /dev/null
+++ b/apps/OboeTester/app/src/main/cpp/FullDuplexEcho.h
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef OBOETESTER_FULL_DUPLEX_ECHO_H
+#define OBOETESTER_FULL_DUPLEX_ECHO_H
+
+#include <unistd.h>
+#include <sys/types.h>
+
+#include "oboe/Oboe.h"
+#include "FullDuplexStream.h"
+
+class FullDuplexEcho : public FullDuplexStream {
+public:
+    FullDuplexEcho() {}
+
+    /**
+     * Called when data is available on both streams.
+     * Caller should override this method.
+     */
+    oboe::DataCallbackResult onBothStreamsReady(
+            const void *inputData,
+            int   numInputFrames,
+            void *outputData,
+            int   numOutputFrames
+    ) override;
+
+    oboe::Result start() override;
+
+private:
+    std::unique_ptr<float[]> mDelayLine;
+    int32_t mCursorFrames = 0;
+    int32_t mDelaySizeFrames = 0;
+};
+
+
+#endif //OBOETESTER_FULL_DUPLEX_ECHO_H
diff --git a/apps/OboeTester/app/src/main/cpp/FullDuplexStream.cpp b/apps/OboeTester/app/src/main/cpp/FullDuplexStream.cpp
new file mode 100644
index 0000000..4d31226
--- /dev/null
+++ b/apps/OboeTester/app/src/main/cpp/FullDuplexStream.cpp
@@ -0,0 +1,73 @@
+/*
+ * 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 "FullDuplexStream.h"
+
+oboe::DataCallbackResult FullDuplexStream::onAudioReady(
+        oboe::AudioStream *outputStream,
+        void *audioData,
+        int numFrames) {
+    oboe::DataCallbackResult callbackResult = oboe::DataCallbackResult::Continue;
+
+//    LOGE("FullDuplexStream::%s() called, numFrames = %d, buf = %p", __func__, numFrames, mInputBuffer.get());
+    // Process in multiple bursts to fit in the buffer.
+    int32_t framesLeft = numFrames;
+    while(framesLeft > 0) {
+        // Read data into input buffer.
+        int32_t framesToRead = std::min(framesLeft, outputStream->getFramesPerBurst());
+        oboe::ResultWithValue<int32_t> result = getInputStream()->read(mInputBuffer.get(),
+                framesToRead,
+                0 /* timeout */);
+
+        if (!result) {
+            LOGE("%s() : read() returned %s\n", __func__, convertToText(result.error()));
+            break;
+        }
+        int32_t framesRead = result.value();
+
+        callbackResult = onBothStreamsReady(
+                mInputBuffer.get(), framesRead,
+                audioData, framesToRead
+                );
+        if (callbackResult != oboe::DataCallbackResult::Continue) {
+            break;
+        }
+
+        framesLeft -= framesToRead;
+        audioData = ((uint8_t *)audioData) +
+                (framesToRead * outputStream->getChannelCount() * outputStream->getBytesPerSample());
+    }
+
+    return callbackResult;
+}
+
+oboe::Result FullDuplexStream::start() {
+    int32_t bufferSize = getOutputStream()->getFramesPerBurst()
+            * getOutputStream()->getChannelCount();
+    if (bufferSize > mBufferSize) {
+        LOGE("FullDuplexStream::%s() allocating bufferSize = %d", __func__, bufferSize);
+        mInputBuffer = std::make_unique<float[]>(bufferSize);
+        mBufferSize = bufferSize;
+    }
+    getInputStream()->requestStart();
+    return getOutputStream()->requestStart();
+}
+
+oboe::Result FullDuplexStream::stop() {
+    getOutputStream()->requestStop(); // TODO result?
+    return getInputStream()->requestStop();
+}
diff --git a/apps/OboeTester/app/src/main/cpp/FullDuplexStream.h b/apps/OboeTester/app/src/main/cpp/FullDuplexStream.h
new file mode 100644
index 0000000..d892016
--- /dev/null
+++ b/apps/OboeTester/app/src/main/cpp/FullDuplexStream.h
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef OBOETESTER_FULL_DUPLEX_STREAM_H
+#define OBOETESTER_FULL_DUPLEX_STREAM_H
+
+#include <unistd.h>
+#include <sys/types.h>
+
+#include "oboe/Oboe.h"
+
+class FullDuplexStream : public oboe::AudioStreamCallback {
+public:
+    FullDuplexStream() {}
+    virtual ~FullDuplexStream() = default;
+
+    void setInputStream(oboe::AudioStream *stream) {
+        mInputStream = stream;
+    }
+
+    oboe::AudioStream *getInputStream() {
+        return mInputStream;
+    }
+
+    void setOutputStream(oboe::AudioStream *stream) {
+        mOutputStream = stream;
+    }
+    oboe::AudioStream *getOutputStream() {
+        return mOutputStream;
+    }
+
+    virtual oboe::Result start();
+    virtual oboe::Result stop();
+
+    /**
+     * Called when data is available on both streams.
+     * Caller should override this method.
+     */
+    virtual oboe::DataCallbackResult onBothStreamsReady(
+            const void *inputData,
+            int   numInputFrames,
+            void *outputData,
+            int   numOutputFrames
+            ) = 0;
+
+    /**
+     * Called by Oboe when the stream is ready to process audio.
+     */
+    oboe::DataCallbackResult onAudioReady(
+            oboe::AudioStream *audioStream,
+            void *audioData,
+            int numFrames) override;
+
+private:
+    oboe::AudioStream *mInputStream = nullptr;
+    oboe::AudioStream *mOutputStream = nullptr;
+    std::unique_ptr<float[]> mInputBuffer;
+    int32_t            mBufferSize = 0;
+};
+
+
+#endif //OBOETESTER_FULL_DUPLEX_STREAM_H
diff --git a/apps/OboeTester/app/src/main/cpp/NativeAudioContext.cpp b/apps/OboeTester/app/src/main/cpp/NativeAudioContext.cpp
index a9e3501..de1124c 100644
--- a/apps/OboeTester/app/src/main/cpp/NativeAudioContext.cpp
+++ b/apps/OboeTester/app/src/main/cpp/NativeAudioContext.cpp
@@ -189,12 +189,22 @@
             ->setSampleRate(sampleRate)
             ->setFormat((oboe::AudioFormat) format);
 
-    // We needed the proxy because we did not know the channelCount when we setup the Builder.
-    if (useCallback) {
-        LOGD("NativeAudioContext::open() set callback to use oboeCallbackProxy, size = %d",
-             callbackSize);
-        builder.setCallback(&oboeCallbackProxy);
-        builder.setFramesPerCallback(callbackSize);
+    if (mActivityType == ActivityType::Echo) {
+        if (mFullDuplexEcho.get() == nullptr) {
+            mFullDuplexEcho = std::make_unique<FullDuplexEcho>();
+        }
+        // only output uses a callback, input is polled
+        if (!isInput) {
+            builder.setCallback(mFullDuplexEcho.get());
+        }
+    } else {
+        // We needed the proxy because we did not know the channelCount when we setup the Builder.
+        if (useCallback) {
+            LOGD("NativeAudioContext::open() set callback to use oboeCallbackProxy, size = %d",
+                 callbackSize);
+            builder.setCallback(&oboeCallbackProxy);
+            builder.setFramesPerCallback(callbackSize);
+        }
     }
 
     if (audioApi == oboe::AudioApi::OpenSLES) {
@@ -217,6 +227,14 @@
         mChannelCount = oboeStream->getChannelCount(); // FIXME store per stream
         mFramesPerBurst = oboeStream->getFramesPerBurst();
         mSampleRate = oboeStream->getSampleRate();
+
+        if (mActivityType == ActivityType::Echo) {
+            if (isInput) {
+                mFullDuplexEcho->setInputStream(oboeStream);
+            } else {
+                mFullDuplexEcho->setOutputStream(oboeStream);
+            }
+        }
     }
 
     return ((int)result < 0) ? (int)result : streamIndex;
@@ -377,10 +395,7 @@
             break;
 
         case ActivityType::Echo:
-            inputStream = getInputStream();
-            outputStream = getOutputStream();
-            result = inputStream->requestStart(); // FIXME use full duplex object
-            result = outputStream->requestStart();
+            result = mFullDuplexEcho->start();
             break;
     }
 
diff --git a/apps/OboeTester/app/src/main/cpp/NativeAudioContext.h b/apps/OboeTester/app/src/main/cpp/NativeAudioContext.h
index 718ae1c..6cec33b 100644
--- a/apps/OboeTester/app/src/main/cpp/NativeAudioContext.h
+++ b/apps/OboeTester/app/src/main/cpp/NativeAudioContext.h
@@ -33,6 +33,9 @@
 #include "flowgraph/SinkI16.h"
 #include "flowgraph/SineOscillator.h"
 #include "flowgraph/SawtoothOscillator.h"
+
+#include "FullDuplexEcho.h"
+#include "FullDuplexStream.h"
 #include "InputStreamCallbackAnalyzer.h"
 #include "MultiChannelRecording.h"
 #include "OboeStreamCallbackProxy.h"
@@ -310,6 +313,7 @@
     std::shared_ptr<flowgraph::SinkI16>     mSinkI16;
     std::unique_ptr<AudioStreamGateway>     audioStreamGateway{};
     std::unique_ptr<MultiChannelRecording>  mRecording{};
+    std::unique_ptr<FullDuplexEcho>         mFullDuplexEcho{};
 
     PlayRecordingCallback        mPlayRecordingCallback;
 
diff --git a/apps/OboeTester/app/src/main/cpp/flowgraph/AudioProcessorBase.cpp b/apps/OboeTester/app/src/main/cpp/flowgraph/AudioProcessorBase.cpp
index 8148195..c35547a 100644
--- a/apps/OboeTester/app/src/main/cpp/flowgraph/AudioProcessorBase.cpp
+++ b/apps/OboeTester/app/src/main/cpp/flowgraph/AudioProcessorBase.cpp
@@ -23,8 +23,10 @@
 /***************************************************************************/
 int32_t AudioProcessorBase::pullData(int64_t framePosition, int32_t numFrames) {
     int32_t frameCount = numFrames;
+    // Prevent recursion and multiple execution of nodes.
     if (framePosition > mLastFramePosition) {
         mLastFramePosition = framePosition;
+        // Pull from all the upstream nodes.
         for (auto &port : mInputPorts) {
             frameCount = port.get().pullData(framePosition, frameCount);
         }