Adding loopback example ( echo ) sample
diff --git a/samples/build.gradle b/samples/build.gradle
index 06347cc..8e03641 100644
--- a/samples/build.gradle
+++ b/samples/build.gradle
@@ -1,5 +1,5 @@
 /*
- * Copyright 2015 The Android Open Source Project
+ * Copyright 2018 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.
diff --git a/samples/common/audio_common.cpp b/samples/common/audio_common.cpp
new file mode 100644
index 0000000..82dc8a4
--- /dev/null
+++ b/samples/common/audio_common.cpp
@@ -0,0 +1,120 @@
+/*
+ * Copyright 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.
+ */
+
+#include <string>
+#include "audio_common.h"
+#include <logging_macros.h>
+
+static const oboe::AudioFormat audioFormatEnum[] = {
+    oboe::AudioFormat::Invalid,
+    oboe::AudioFormat::Unspecified,
+    oboe::AudioFormat::I16,
+    oboe::AudioFormat::Float,
+};
+static const int32_t audioFormatCount = sizeof(audioFormatEnum)/
+                                        sizeof(audioFormatEnum[0]);
+
+static const uint32_t sampleFormatBPP[] = {
+    0xffff,
+    0xffff,
+    16, //I16
+    32, //FLOAT
+};
+uint16_t SampleFormatToBpp(oboe::AudioFormat format) {
+    for (int32_t i = 0; i < audioFormatCount; ++i) {
+      if (audioFormatEnum[i] == format)
+        return sampleFormatBPP[i];
+    }
+    return 0xffff;
+}
+static const char * audioFormatStr[] = {
+    "AAUDIO_FORMAT_INVALID", // = -1,
+    "AAUDIO_FORMAT_UNSPECIFIED", // = 0,
+    "AAUDIO_FORMAT_PCM_I16",
+    "AAUDIO_FORMAT_PCM_FLOAT",
+};
+const char* FormatToString(oboe::AudioFormat format) {
+    for (int32_t i = 0; i < audioFormatCount; ++i) {
+        if (audioFormatEnum[i] == format)
+            return audioFormatStr[i];
+    }
+    return "UNKNOW_AUDIO_FORMAT";
+}
+
+void PrintAudioStreamInfo(const oboe::AudioStream *stream) {
+#define STREAM_CALL(func) (stream)->func()
+    LOGI("StreamID: %p", stream);
+
+    LOGI("API type: %s", oboe::convertToText(stream->getAudioApi()));
+    LOGI("BufferCapacity: %d", STREAM_CALL(getBufferCapacityInFrames));
+    LOGI("BufferSize: %d", STREAM_CALL(getBufferSizeInFrames));
+//  Question: does this one have to non-constant function?
+//    LOGI("FramesPerBurst: %d", STREAM_CALL(getFramesPerBurst));
+    LOGI("FramesPerBurst: %d", const_cast<oboe::AudioStream* >(stream)->getFramesPerBurst());
+    LOGI("XRunCount: %d", STREAM_CALL(getXRunCount));
+    LOGI("SampleRate: %d", STREAM_CALL(getSampleRate));
+    LOGI("SamplesPerFrame: %d", STREAM_CALL(getChannelCount));
+    LOGI("DeviceId: %d", STREAM_CALL(getDeviceId));
+    LOGI("Format: %s",  FormatToString(STREAM_CALL(getFormat)));
+    LOGI("SharingMode: %s", (STREAM_CALL(getSharingMode)) == oboe::SharingMode::Exclusive?
+                          "EXCLUSIVE" : "SHARED");
+
+    oboe::PerformanceMode perfMode = STREAM_CALL(getPerformanceMode);
+    std::string perfModeDescription;
+    switch (perfMode){
+      case oboe::PerformanceMode ::None:
+        perfModeDescription = "NONE";
+        break;
+      case oboe::PerformanceMode::LowLatency:
+        perfModeDescription = "LOW_LATENCY";
+        break;
+      case oboe::PerformanceMode::PowerSaving:
+        perfModeDescription = "POWER_SAVING";
+        break;
+      default:
+        perfModeDescription = "UNKNOWN";
+        break;
+    }
+    LOGI("PerformanceMode: %s", perfModeDescription.c_str());
+
+    oboe::Direction  dir = STREAM_CALL(getDirection);
+    LOGI("Direction: %s", (dir == oboe::Direction ::Output ? "OUTPUT" : "INPUT"));
+    if (dir == oboe::Direction ::Output) {
+        LOGI("FramesReadByDevice: %d", (int32_t)STREAM_CALL(getFramesRead));
+        LOGI("FramesWriteByApp: %d", (int32_t)STREAM_CALL(getFramesWritten));
+    } else {
+        LOGI("FramesReadByApp: %d", (int32_t)STREAM_CALL(getFramesRead));
+        LOGI("FramesWriteByDevice: %d", (int32_t)STREAM_CALL(getFramesWritten));
+    }
+#undef STREAM_CALL
+}
+
+int64_t timestamp_to_nanoseconds(timespec ts){
+  return (ts.tv_sec * (int64_t) NANOS_PER_SECOND) + ts.tv_nsec;
+}
+
+int64_t get_time_nanoseconds(clockid_t clockid){
+  timespec ts;
+  clock_gettime(clockid, &ts);
+  return timestamp_to_nanoseconds(ts);
+}
+
+void ConvertMonoToStereo(int16_t *buffer, int32_t numFrames) {
+    for (int i = numFrames - 1; i >= 0; i--) {
+        buffer[i*2] = buffer[i];
+        buffer[(i*2)+1] = buffer[i];
+    }
+}
\ No newline at end of file
diff --git a/samples/common/audio_common.h b/samples/common/audio_common.h
new file mode 100644
index 0000000..36a4b5e
--- /dev/null
+++ b/samples/common/audio_common.h
@@ -0,0 +1,56 @@
+/*
+ * Copyright 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.
+ */
+
+
+#ifndef AUDIO_COMMON_H
+#define AUDIO_COMMON_H
+
+#include <chrono>
+#include <oboe/Oboe.h>
+
+// Time constants
+#define NANOS_PER_SECOND 1000000000L
+#define NANOS_PER_MILLISECOND 1000000L
+
+constexpr int kMonoChannelCount = 1;
+constexpr int kStereoChannelCount = 2;
+
+uint16_t SampleFormatToBpp(oboe::AudioFormat format);
+/*
+ * GetSystemTicks(void):  return the time in micro sec
+ */
+__inline__ uint64_t GetSystemTicks(void) {
+    struct timeval Time;
+    gettimeofday( &Time, NULL );
+
+    return (static_cast<uint64_t>(1000000) * Time.tv_sec + Time.tv_usec);
+}
+
+/*
+ * flag to enable file dumping
+ */
+// #define ENABLE_LOG  1
+
+void PrintAudioStreamInfo(const oboe::AudioStream *stream);
+
+int64_t timestamp_to_nanoseconds(timespec ts);
+
+int64_t get_time_nanoseconds(clockid_t clockid);
+
+// Note: buffer must be at least double the length of numFrames to accommodate the stereo data
+void ConvertMonoToStereo(int16_t *buffer, int32_t numFrames);
+
+#endif // AUDIO_COMMON_H
diff --git a/samples/echo/build.gradle b/samples/echo/build.gradle
new file mode 100644
index 0000000..b952cf4
--- /dev/null
+++ b/samples/echo/build.gradle
@@ -0,0 +1,39 @@
+apply plugin: 'com.android.application'
+
+android {
+    compileSdkVersion 27
+
+    defaultConfig {
+        applicationId 'com.google.sample.oboe.echo'
+        minSdkVersion 24
+        targetSdkVersion 27
+        versionCode 1
+        versionName '1.0'
+        ndk {
+            abiFilters 'armeabi-v7a', 'arm64-v8a'
+        }
+        externalNativeBuild {
+            cmake {
+                arguments '-DANDROID_STL=c++_shared', '-DANDROID_TOOLCHAIN=clang',
+                          '-DANDROID_PLATFORM=android-26'
+            }
+        }
+    }
+    buildTypes {
+        release {
+            minifyEnabled false
+            proguardFiles getDefaultProguardFile('proguard-android.txt')
+        }
+    }
+    externalNativeBuild {
+        cmake {
+            path 'src/main/cpp/CMakeLists.txt'
+        }
+    }
+}
+
+dependencies {
+    implementation 'com.android.support:appcompat-v7:26.1.0'
+    implementation 'com.android.support.constraint:constraint-layout:1.1.0'
+    implementation project(':audio-device')
+}
diff --git a/samples/echo/proguard-rules.pro b/samples/echo/proguard-rules.pro
new file mode 100644
index 0000000..7dc6c7f
--- /dev/null
+++ b/samples/echo/proguard-rules.pro
@@ -0,0 +1,17 @@
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in /Users/gfan/dev/android-sdk/tools/proguard/proguard-android.txt
+# You can edit the include path and order by changing the proguardFiles
+# directive in build.gradle.
+#
+# For more details, see
+#   http://developer.android.com/guide/developing/tools/proguard.html
+
+# Add any project specific keep options here:
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+#   public *;
+#}
diff --git a/samples/echo/screenshot.png b/samples/echo/screenshot.png
new file mode 100644
index 0000000..411af67
--- /dev/null
+++ b/samples/echo/screenshot.png
Binary files differ
diff --git a/samples/echo/src/main/AndroidManifest.xml b/samples/echo/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..bd55980
--- /dev/null
+++ b/samples/echo/src/main/AndroidManifest.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.google.sample.oboe.echo" >
+
+    <uses-feature android:name="android.hardware.microphone" android:required="true" />
+    <uses-feature android:name="android.hardware.audio.output" android:required="true" />
+    <uses-permission android:name="android.permission.RECORD_AUDIO" />
+    <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
+    <application
+        android:allowBackup="false"
+        android:fullBackupContent="false"
+        android:supportsRtl="true"
+        android:icon="@mipmap/ic_launcher"
+        android:label="@string/app_name"
+        android:theme="@style/AppTheme" >
+      <activity
+          android:name="com.google.sample.oboe.echo.MainActivity"
+          android:label="@string/app_name"
+          android:screenOrientation="portrait">
+        <intent-filter>
+          <action android:name="android.intent.action.MAIN" />
+          <category android:name="android.intent.category.LAUNCHER" />
+        </intent-filter>
+      </activity>
+    </application>
+</manifest>
diff --git a/samples/echo/src/main/assets/sample_s16_pcm_dc_48khz.raw b/samples/echo/src/main/assets/sample_s16_pcm_dc_48khz.raw
new file mode 100644
index 0000000..c63b397
--- /dev/null
+++ b/samples/echo/src/main/assets/sample_s16_pcm_dc_48khz.raw
Binary files differ
diff --git a/samples/echo/src/main/cpp/AudioEffect.cpp b/samples/echo/src/main/cpp/AudioEffect.cpp
new file mode 100644
index 0000000..3f7c842
--- /dev/null
+++ b/samples/echo/src/main/cpp/AudioEffect.cpp
@@ -0,0 +1,277 @@
+/*
+ * Copyright 2018 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 "AudioEffect.h"
+#include <logging_macros.h>
+#include <climits>
+#include <cstring>
+#include <audio_common.h>
+
+/*
+ * Mixing Audio in integer domain to avoid FP calculation
+ *   (FG * ( MixFactor * 16 ) + BG * ( (1.0f-MixFactor) * 16 )) / 16
+ */
+static const int32_t kFloatToIntMapFactor = 128;
+
+AudioMixer::AudioMixer() :
+    AudioFormat(48000, 2, oboe::AudioFormat::I16) {
+
+  busy_ = false;
+  bgMixFactorInt_ = (int32_t)
+                   (bgMixFactor_ * kFloatToIntMapFactor + 0.5f);
+  fgMixFactorInt_ = kFloatToIntMapFactor - bgMixFactorInt_;
+}
+
+/**
+ * destructor: release memory for audio samples
+ */
+AudioMixer::~AudioMixer() {
+}
+/**
+ * Set mix factor for the 2 streams( background and forground );
+ * blending:
+ *    recordedAudio * fgMix +
+ *    backgroundMusic * ( 1.0f - fgMix )
+ * @param fgMix is background music mixer
+ */
+void  AudioMixer::setBackgroundMixer(float mixer) {
+  if (mixer >= 0.0f && mixer <= 1.0f) {
+    bgMixFactor_ = mixer;
+    bgMixFactorInt_ = (int32_t)
+      (bgMixFactor_ * kFloatToIntMapFactor + 0.5f);
+    fgMixFactorInt_ = kFloatToIntMapFactor - bgMixFactorInt_;
+  }
+}
+
+/**
+ * Insert a raw PCM audio buffer to blend with live audio
+ *
+ * @param samples points to PCM audio buffer
+ * @param sampleCount is total samples pointed by samples
+ * @param channelCount channels for PCM audio pointed by samples
+ * @param freq is PCM audio frequency (48000hz for this sample)
+ */
+void  AudioMixer::addStream(std::unique_ptr<int16_t[]>samples, size_t sampleCount,
+          int32_t sampleRate, int32_t channelCount, oboe::AudioFormat format){
+  if (busy_) {
+    LOGW("filtering in progress, filter configuration is IGNORED");
+    return;
+  }
+  bgAudio_ = std::move(samples);
+  bgAudioSampleCount_ = sampleCount;
+  sampleRate_ = sampleRate;
+  format_ = format;
+  channelCount_ = channelCount;
+
+  curPosition_ = 0;
+}
+
+/**
+ * Adding audio processing into the live audio
+ * @param liveAudio is recorded audio
+ * @param samplesPerFrame is same as channelCount.
+ * @param numFrames represents frames pointed by liveAudio
+ *        total samples = numFrames * samplesPerFrame
+ */
+void AudioMixer::process(int16_t *liveAudio, int32_t channelCount,
+                          int32_t numFrames) {
+  assert(bgAudio_ && liveAudio);
+  if (numFrames > bgAudioSampleCount_ || channelCount != channelCount_ ||
+      bgMixFactorInt_ == 0) {
+    return;
+  }
+
+  busy_ = true;
+  int32_t curSample;
+  for (int i = 0; i < (numFrames * channelCount); i++) {
+    curSample = liveAudio[i];
+    curSample = curSample * fgMixFactorInt_ +
+                bgAudio_[curPosition_] * bgMixFactorInt_;
+    curSample /= kFloatToIntMapFactor;
+
+    curSample = (curSample > SHRT_MAX ? SHRT_MAX : curSample);
+    liveAudio[i] = (int16_t)(curSample < SHRT_MIN ? SHRT_MIN : curSample);
+    curPosition_ = (curPosition_ + 1 ) % bgAudioSampleCount_;
+  }
+  busy_ = false;
+}
+
+/**
+ * query for audio format supportability
+ */
+bool AudioMixer::AudioFormatSupported(int32_t frequency,
+                  int32_t channels, oboe::AudioFormat format) const {
+  return (frequency == sampleRate_ &&
+          channels == channelCount_ &&
+          format == format_);
+}
+
+/**
+ * Constructor for AudioDelay
+ * @param sampleRate
+ * @param channelCount
+ * @param format
+ * @param delay
+ * @param decay
+ */
+AudioDelay::AudioDelay(int32_t sampleRate,
+                       int32_t channelCount,
+                       oboe::AudioFormat format,
+                       float  delay,
+                       float  decay) :
+    AudioFormat(sampleRate, channelCount, format),
+    delay_(delay), decay_(decay) {
+
+  feedbackFactor_ = static_cast<int32_t>(decay_ * kFloatToIntMapFactor);
+  liveAudioFactor_ = kFloatToIntMapFactor - feedbackFactor_;
+  allocateBuffer();
+}
+
+/**
+ * Destructor
+ */
+AudioDelay::~AudioDelay() {
+  if(buffer_) delete static_cast<uint8_t*>(buffer_);
+}
+
+/**
+ * Configure for delay time ( in miliseconds ). It is possible to dynamically
+ * adjust the value
+ * @param delay in seconds
+ * @return true if delay time is set successfully
+ */
+bool AudioDelay::setDelay(float delay) {
+  float delta = delay - delay_;
+  if ( delta > -0.022f && delta < 0.022f) {
+    return true;
+  }
+
+  std::lock_guard<std::mutex> lock(lock_);
+
+  if(buffer_) {
+    delete static_cast<uint8_t*>(buffer_);
+    buffer_ = nullptr;
+  }
+
+  delay_  = delay;
+  allocateBuffer();
+  return buffer_ != nullptr;
+}
+
+/**
+ * Internal helper function to allocate buffer for the delay
+ *  - calculate the buffer size for the delay time
+ *  - allocate and zero out buffer (0 means silent audio)
+ *  - configure bufSize_ to be size of audioFrames
+ */
+void AudioDelay::allocateBuffer(void) {
+
+  float fNumFrames = sampleRate_ * delay_;
+
+  size_t sampleCount = static_cast<uint32_t>(fNumFrames + 0.5f) * channelCount_;
+
+  uint32_t bytePerSample = SampleFormatToBpp(format_) / 8;
+  assert(bytePerSample <= 4);
+
+  uint32_t bytePerFrame =  channelCount_ * bytePerSample;
+
+  // get bufCapacity in bytes
+  bufCapacity_ = sampleCount * bytePerSample;
+  bufCapacity_ = ((bufCapacity_ + bytePerFrame - 1) / bytePerFrame) * bytePerFrame;
+
+  buffer_ = new uint8_t[bufCapacity_];
+  assert(buffer_);
+
+  memset(buffer_, 0, bufCapacity_);
+  curPos_ = 0;
+
+  // bufSize_ is in Frames ( not samples, not bytes )
+  bufSize_ = bufCapacity_ / bytePerFrame;
+}
+
+float AudioDelay::getDelay(void) const {
+  return delay_;
+}
+
+/**
+ * SetFeedbackRatio(): set the decay factor
+ * ratio: value of 0.0 -- 1.0f;
+ *
+ * the calculation is in integer ( not in float )
+ * for performance purpose
+ */
+void AudioDelay::setDecay(float decay) {
+  if (decay > 0.0f && decay < 1.0f) {
+    float feedback = (decay * kFloatToIntMapFactor + 0.5f);
+    feedbackFactor_ = static_cast<int32_t>(feedback);
+    liveAudioFactor_ = kFloatToIntMapFactor - feedbackFactor_;
+  }
+}
+
+float AudioDelay::getDecay(float) const {
+  return decay_;
+}
+
+/**
+ * process() filter live audio with "echo" effect:
+ *   delay time is run-time adjustable
+ *   decay time could also be adjustable, but not used
+ *   in this sample, hardcoded to .5
+ *
+ * @param liveAudio is recorded audio stream
+ * @param channelCount for liveAudio, must be 2 for stereo
+ * @param numFrames is length of liveAudio in Frames ( not in byte )
+ */
+void AudioDelay::process(int16_t *liveAudio,
+                         int32_t channelCount,
+                         int32_t numFrames) {
+  if (feedbackFactor_ == 0 ||
+      channelCount != channelCount_ ||
+      bufSize_ < numFrames) {
+    return;
+  }
+
+  if(!lock_.try_lock()) {
+    return;
+  }
+
+  if (numFrames + curPos_  > bufSize_) {
+    curPos_ = 0;
+  }
+
+  // process every sample
+  int32_t sampleCount = channelCount * numFrames;
+  int16_t* samples =  & static_cast<int16_t*>(buffer_)[curPos_];
+  int32_t curSample;
+  for (size_t idx = 0; idx < sampleCount; idx++) {
+#if 1
+    curSample = (samples[idx] * feedbackFactor_ +
+                liveAudio[idx] * liveAudioFactor_) / kFloatToIntMapFactor;
+#else
+    curSample = (samples[idx] * feedbackFactor_) / kFloatToIntMapFactor +
+                 liveAudio[idx];
+#endif
+    if(curSample > SHRT_MAX)
+      curSample = SHRT_MAX;
+    else if (curSample < SHRT_MIN)
+      curSample = SHRT_MAX;
+    samples[idx] = static_cast<int16_t>(curSample);
+    liveAudio[idx] = samples[idx];
+  }
+
+  curPos_ += numFrames;
+  lock_.unlock();
+}
+
diff --git a/samples/echo/src/main/cpp/AudioEffect.h b/samples/echo/src/main/cpp/AudioEffect.h
new file mode 100644
index 0000000..9119507
--- /dev/null
+++ b/samples/echo/src/main/cpp/AudioEffect.h
@@ -0,0 +1,106 @@
+/*
+ * Copyright 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.
+ */
+
+#ifndef EFFECT_PROCESSOR_H
+#define EFFECT_PROCESSOR_H
+
+#include <cstdint>
+#include <atomic>
+#include <oboe/Oboe.h>
+#include <mutex>
+
+class AudioFormat {
+  protected:
+    int32_t sampleRate_ = 48000;
+    int32_t channelCount_ = 2;
+    oboe::AudioFormat format_ = oboe::AudioFormat::I16;
+
+    AudioFormat(int32_t sampleRate, int32_t channelCount,
+                oboe::AudioFormat format) :
+      sampleRate_(sampleRate), channelCount_(channelCount),
+      format_(format) {};
+    virtual ~AudioFormat() {}
+};
+
+/**
+ * An Audio Mixer that mixing input audio stream with a background
+ * music. Only works with:
+ *   - One background stream
+ *   - I16, 48000Hz, dual channel
+ *   - raw PCM format without headers
+ */
+class AudioMixer : public AudioFormat {
+  public:
+    AudioMixer();
+    ~AudioMixer();
+    void process(int16_t *liveAudio, int32_t channelCount,
+                 int32_t numFrames);
+    void addStream(std::unique_ptr<int16_t[]>samples, size_t sampleCount,
+                   int32_t sampleRate, int32_t channelCount,
+                   oboe::AudioFormat format);
+    void setBackgroundMixer(float bgMix);
+    bool AudioFormatSupported(int32_t sampleRate, int32_t channels,
+                              oboe::AudioFormat format) const;
+  private:
+    std::unique_ptr<int16_t[]> bgAudio_ = nullptr;
+    size_t bgAudioSampleCount_ = 0;
+    size_t curPosition_ = 0;
+    std::atomic_bool busy_;
+    float bgMixFactor_ = 0.5f;
+    int32_t  fgMixFactorInt_;
+    int32_t  bgMixFactorInt_;
+};
+
+/**
+ * An audio delay effect:
+ *   - decay is how strong echo should be
+ *   - delay time is how long to hear the first echo
+ *
+ *   It is a simple mixing:
+ *     new sample = newly_recorded_audio * ( 1 - decay ) +
+ *                  feedback_audio * decay
+ */
+class AudioDelay : public AudioFormat {
+  public:
+    ~AudioDelay();
+
+    explicit AudioDelay(int32_t sampleRate,
+                int32_t channelCount,
+                oboe::AudioFormat format,
+                float delay,
+                float decay);
+    void process(int16_t *liveAudio, int32_t channelCount,
+               int32_t numFrames);
+
+    bool   setDelay(float delayInSec);
+    void   setDecay(float delay);
+    float  getDelay(void) const;
+    float  getDecay(float)const;
+
+  private:
+    float delay_ = 0.0f;
+    float decay_ = 0.1f;
+    void *buffer_ = nullptr;
+    size_t bufCapacity_ = 0;
+    size_t bufSize_ = 0;
+    size_t curPos_ = 0;
+    std::mutex lock_;
+    int32_t feedbackFactor_;
+    int32_t liveAudioFactor_;
+    void allocateBuffer(void);
+
+};
+#endif  // EFFECT_PROCESSOR_H
diff --git a/samples/echo/src/main/cpp/CMakeLists.txt b/samples/echo/src/main/cpp/CMakeLists.txt
new file mode 100644
index 0000000..ed2764b
--- /dev/null
+++ b/samples/echo/src/main/cpp/CMakeLists.txt
@@ -0,0 +1,47 @@
+#
+# Copyright 2018 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.
+#
+cmake_minimum_required(VERSION 3.4.1)
+project(echo LANGUAGES C CXX)
+
+get_filename_component(SAMPLE_ROOT_DIR
+    ${CMAKE_CURRENT_SOURCE_DIR}/../../../.. ABSOLUTE)
+
+### INCLUDE OBOE LIBRARY ###
+set (OBOE_DIR ${SAMPLE_ROOT_DIR}/..)
+add_subdirectory(${OBOE_DIR} ./oboe-bin)
+
+add_library(echo
+    SHARED
+        EchoAudioEngine.cpp
+        jni_bridge.cpp
+        AudioEffect.cpp
+        ${SAMPLE_ROOT_DIR}/debug-utils/trace.cpp
+        ${SAMPLE_ROOT_DIR}/common/audio_common.cpp)
+target_include_directories(echo
+    PRIVATE
+        ${SAMPLE_ROOT_DIR}/common
+        ${SAMPLE_ROOT_DIR}/debug-utils
+        ${OBOE_DIR}/include)
+target_link_libraries(echo
+    PRIVATE
+        oboe
+        android
+        atomic
+        log)
+
+target_compile_options(echo
+    PRIVATE
+        -Wall)
diff --git a/samples/echo/src/main/cpp/EchoAudioEngine.cpp b/samples/echo/src/main/cpp/EchoAudioEngine.cpp
new file mode 100644
index 0000000..10267f8
--- /dev/null
+++ b/samples/echo/src/main/cpp/EchoAudioEngine.cpp
@@ -0,0 +1,406 @@
+/**
+ * Copyright 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.
+ */
+
+#include "EchoAudioEngine.h"
+#include <audio_common.h>
+#include <logging_macros.h>
+#include <climits>
+#include <assert.h>
+
+EchoAudioEngine::EchoAudioEngine() {
+  assert(outputChannelCount_ == inputChannelCount_);
+  mixerEffect_ = std::unique_ptr<AudioMixer>(new AudioMixer);
+}
+
+EchoAudioEngine::~EchoAudioEngine() {
+  stopStream(playStream_);
+  stopStream(recordingStream_);
+
+  closeStream(playStream_);
+  frameCallbackCount_ = 0;
+
+  closeStream(recordingStream_);
+}
+
+void EchoAudioEngine::setRecordingDeviceId(int32_t deviceId) {
+  recordingDeviceId_ = deviceId;
+}
+
+void EchoAudioEngine::setPlaybackDeviceId(int32_t deviceId) {
+  playbackDeviceId_ = deviceId;
+}
+
+bool EchoAudioEngine::isAAudioSupported() {
+  oboe::AudioStreamBuilder builder;
+  return builder.isAAudioSupported();
+}
+bool EchoAudioEngine::setAudioApi(oboe::AudioApi api) {
+  if (isEchoOn_)
+    return false;
+
+  audioApi_ = api;
+  return true;
+}
+void EchoAudioEngine::setEchoOn(bool isEchoOn) {
+  if (isEchoOn != isEchoOn_) {
+    isEchoOn_ = isEchoOn;
+
+    if (isEchoOn) {
+      openAllStreams();
+    } else {
+      closeAllStreams();
+    }
+  }
+}
+
+void EchoAudioEngine::openAllStreams() {
+  // Note: The order of stream creation is important. We create the playback
+  // stream first, then use properties from the playback stream
+  // (e.g. sample rate) to create the recording stream. By matching the
+  // properties we should get the lowest latency path
+  openPlaybackStream();
+  openRecordingStream();
+  // Now start the recording stream first so that we can read from it during the
+  // playback stream's dataCallback
+  if (recordingStream_ && playStream_) {
+    mixAudio_ = mixerEffect_->AudioFormatSupported(playStream_->getSampleRate(),
+      playStream_->getChannelCount(), playStream_->getFormat());
+
+    startStream(recordingStream_);
+    startStream(playStream_);
+  } else {
+    LOGE("Failed to create recording (%p) and/or playback (%p) stream",
+         recordingStream_, playStream_);
+    closeAllStreams();
+  }
+}
+
+/**
+ * Stops and closes the playback and recording streams.
+ */
+void EchoAudioEngine::closeAllStreams() {
+  /**
+   * Note: The order of events is important here.
+   * The playback stream must be closed before the recording stream. If the
+   * recording stream were to be closed first the playback stream's
+   * callback may attempt to read from the recording stream
+   * which would cause the app to crash since the recording stream would be
+   * null.
+   */
+
+  if (playStream_ != nullptr) {
+    closeStream(playStream_);  // Calling close will also stop the stream
+    playStream_ = nullptr;
+  }
+
+  if (recordingStream_ != nullptr) {
+    closeStream(recordingStream_);
+    recordingStream_ = nullptr;
+  }
+  mixAudio_ = false;
+}
+
+/**
+ * Creates an audio stream for recording. The audio device used will depend on
+ * recordingDeviceId_.
+ * If the value is set to oboe::Unspecified then the default recording device
+ * will be used.
+ */
+void EchoAudioEngine::openRecordingStream() {
+  // To create a stream we use a stream builder. This allows us to specify all
+  // the parameters for the stream prior to opening it
+  oboe::AudioStreamBuilder builder;
+
+  setupRecordingStreamParameters(&builder);
+
+  // Now that the parameters are set up we can open the stream
+  oboe::Result result = builder.openStream(&recordingStream_);
+  if (result == oboe::Result::OK && recordingStream_) {
+    assert(recordingStream_->getChannelCount() == inputChannelCount_);
+    assert(recordingStream_->getSampleRate() == sampleRate_);
+    assert(recordingStream_->getFormat() == oboe::AudioFormat::I16);
+
+    warnIfNotLowLatency(recordingStream_);
+    PrintAudioStreamInfo(recordingStream_);
+  } else {
+    LOGE("Failed to create recording stream. Error: %s", convertToText(result));
+  }
+}
+
+/**
+ * Creates an audio stream for playback. The audio device used will depend on
+ * playbackDeviceId_.
+ * If the value is set to oboe::Unspecified then the default playback device
+ * will be used.
+ */
+void EchoAudioEngine::openPlaybackStream() {
+  oboe::AudioStreamBuilder builder;
+
+  setupPlaybackStreamParameters(&builder);
+  oboe::Result result = builder.openStream(&playStream_);
+  if (result == oboe::Result::OK && playStream_) {
+    sampleRate_ = playStream_->getSampleRate();
+
+    assert(sampleRate_ == kLoopbackSampleRate);
+    assert(playStream_->getFormat() == oboe::AudioFormat::I16);
+    assert(outputChannelCount_ == playStream_->getChannelCount());
+
+    framesPerBurst_ = playStream_->getFramesPerBurst();
+
+    // Read blocking timeout value: half of the burst size
+    audioBlockingReadTimeout_ = static_cast<uint64_t>(.5f * framesPerBurst_
+                                          / sampleRate_ * NANOS_PER_SECOND);
+
+    latencyTuner_ = std::unique_ptr<oboe::LatencyTuner>
+                    (new oboe::LatencyTuner(*playStream_));
+
+    delayEffect_ = std::unique_ptr<AudioDelay>(new AudioDelay(
+      sampleRate_,outputChannelCount_, format_, echoDelay_, echoDecay_));
+    assert(delayEffect_ && mixerEffect_);
+
+    frameCallbackCount_ = 0;
+    warnIfNotLowLatency(playStream_);
+
+    PrintAudioStreamInfo(playStream_);
+  } else {
+    LOGE("Failed to create playback stream. Error: %s",
+         oboe::convertToText(result));
+  }
+}
+
+/**
+ * Sets the stream parameters which are specific to recording,
+ * including the sample rate which is determined from the
+ * playback stream.
+ *
+ * @param builder The recording stream builder
+ */
+oboe::AudioStreamBuilder *EchoAudioEngine::setupRecordingStreamParameters(
+    oboe::AudioStreamBuilder *builder) {
+  // This sample uses blocking read() by setting callback to null
+  builder->setCallback(nullptr)
+      ->setDeviceId(recordingDeviceId_)
+      ->setDirection(oboe::Direction::Input)
+      ->setSampleRate(sampleRate_)
+      ->setChannelCount(inputChannelCount_);
+  return setupCommonStreamParameters(builder);
+}
+
+/**
+ * Sets the stream parameters which are specific to playback, including device
+ * id and the dataCallback function, which must be set for low latency
+ * playback.
+ * @param builder The playback stream builder
+ */
+oboe::AudioStreamBuilder *EchoAudioEngine::setupPlaybackStreamParameters(
+    oboe::AudioStreamBuilder *builder) {
+  builder->setCallback(this)
+      ->setDeviceId(playbackDeviceId_)
+      ->setDirection(oboe::Direction::Output)
+      ->setChannelCount(outputChannelCount_)
+      ->setSampleRate(sampleRate_);
+
+  return setupCommonStreamParameters(builder);
+}
+
+/**
+ * Set the stream parameters which are common to both recording and playback
+ * streams.
+ * @param builder The playback or recording stream builder
+ */
+oboe::AudioStreamBuilder *EchoAudioEngine::setupCommonStreamParameters(
+    oboe::AudioStreamBuilder *builder) {
+  // We request EXCLUSIVE mode since this will give us the lowest possible
+  // latency.
+  // If EXCLUSIVE mode isn't available the builder will fall back to SHARED
+  // mode.
+  builder->setAudioApi(audioApi_)
+      ->setFormat(format_)
+      ->setSharingMode(oboe::SharingMode::Exclusive)
+      ->setPerformanceMode(oboe::PerformanceMode::LowLatency);
+  return builder;
+}
+
+void EchoAudioEngine::startStream(oboe::AudioStream *stream) {
+  assert(stream);
+  if (stream) {
+    oboe::Result result = stream->requestStart();
+    if (result != oboe::Result::OK) {
+      LOGE("Error starting stream. %s", convertToText(result));
+    }
+  }
+}
+
+void EchoAudioEngine::stopStream(oboe::AudioStream *stream) {
+  if (stream) {
+    oboe::Result result = stream->start(0L);
+    if (result != oboe::Result::OK) {
+      LOGE("Error stopping stream. %s", oboe::convertToText(result));
+    }
+  }
+}
+
+/**
+ * Close the stream. AudioStream::close() is a blocking call so
+ * the application does not need to add synchronization between
+ * onAudioReady() function and the thread calling close().
+ * [the closing thread is the UI thread in this sample].
+ * @param stream the stream to close
+ */
+void EchoAudioEngine::closeStream(oboe::AudioStream *stream) {
+  if (stream) {
+    oboe::Result result = stream->close();
+    if (result != oboe::Result::OK) {
+      LOGE("Error closing stream. %s", convertToText(result));
+    }
+  }
+}
+
+/**
+ * Restart the streams. During the restart operation subsequent calls to this
+ * method will output a warning.
+ */
+void EchoAudioEngine::restartStreams() {
+  LOGI("Restarting streams");
+
+  if (restartingLock_.try_lock()) {
+    closeAllStreams();
+    openAllStreams();
+    restartingLock_.unlock();
+  } else {
+    LOGW(
+        "Restart stream operation already in progress - ignoring this request");
+    // We were unable to obtain the restarting lock which means the restart
+    // operation is currently
+    // active. This is probably because we received successive "stream
+    // disconnected" events.
+    // Internal issue b/63087953
+  }
+}
+
+/**
+ * Warn in logcat if non-low latency stream is created
+ * @param stream: newly created stream
+ *
+ */
+void EchoAudioEngine::warnIfNotLowLatency(oboe::AudioStream *stream) {
+  if (stream->getPerformanceMode() != oboe::PerformanceMode::LowLatency) {
+    LOGW(
+        "Stream is NOT low latency."
+        "Check your requested format, sample rate and channel count");
+  }
+}
+
+/**
+ * Handles playback stream's audio request. In this sample, we simply block-read
+ * from the record stream for the required samples.
+ *
+ * @param oboeStream: the playback stream that requesting additional samples
+ * @param audioData:  the buffer to load audio samples for playback stream
+ * @param numFrames:  number of frames to load to audioData buffer
+ * @return: DataCallbackResult::Continue.
+ */
+oboe::DataCallbackResult EchoAudioEngine::onAudioReady(
+    oboe::AudioStream *oboeStream, void *audioData, int32_t numFrames) {
+
+  assert(oboeStream == playStream_);
+
+  if (frameCallbackCount_) {
+    latencyTuner_->tune();
+  }
+  frameCallbackCount_++;
+
+  // blocking read with timeout:
+  //     recorder may not have data ready, specifically
+  //     at the very beginning; in this case, simply play
+  //     silent audio. The timeout is equivalent to
+  //       framesPerBurst()/2
+  //     Do not make it too long, otherwise player would underrun
+  //     and if tuning is in process, player will increase
+  //     FramesPerBurst.
+  oboe::ErrorOrValue<int32_t> status =
+    recordingStream_->read(audioData, numFrames, audioBlockingReadTimeout_);
+
+  int32_t framesRead = (!status) ? 0 : status.value();
+  if (framesRead < numFrames) {
+    int32_t bytesPerFrame = recordingStream_->getChannelCount() *
+                            SampleFormatToBpp(oboeStream->getFormat()) / 8;
+    uint8_t *padPos = static_cast<uint8_t*>(audioData) +
+                      framesRead * bytesPerFrame;
+    memset(padPos, 0, (size_t)(numFrames - framesRead) * bytesPerFrame);
+  }
+
+  delayEffect_->process(static_cast<int16_t *>(audioData),
+                        outputChannelCount_, numFrames);
+  if (mixAudio_) {
+    mixerEffect_->process(static_cast<int16_t *>(audioData),
+                         outputChannelCount_, numFrames);
+  }
+  return oboe::DataCallbackResult::Continue;
+}
+
+/**
+ * Oboe notifies the application for "about to close the stream".
+ *
+ * @param oboeStream: the stream to close
+ * @param error: oboe's reason for closing the stream
+ */
+void EchoAudioEngine::onErrorBeforeClose(oboe::AudioStream *oboeStream,
+                                         oboe::Result error) {
+  LOGE("%s stream Error before close: %s",
+       oboe::convertToText(oboeStream->getDirection()),
+       oboe::convertToText(error));
+}
+
+/**
+ * Oboe notifies application that "the stream is closed"
+ *
+ * @param oboeStream
+ * @param error
+ */
+void EchoAudioEngine::onErrorAfterClose(oboe::AudioStream *oboeStream,
+                                        oboe::Result error) {
+  LOGE("%s stream Error after close: %s",
+       oboe::convertToText(oboeStream->getDirection()),
+       oboe::convertToText(error));
+}
+
+void EchoAudioEngine::setBackgroundStream(
+      std::unique_ptr<int16_t[]> samples, size_t sampleCount,
+      int32_t sampleRate, int32_t channelCount) {
+
+  mixerEffect_->addStream(std::move(samples), sampleCount, sampleRate,
+                         channelCount, oboe::AudioFormat::I16);
+}
+
+void EchoAudioEngine::setBackgroundMixer(float bgFactor) {
+  mixerEffect_->setBackgroundMixer(bgFactor);
+}
+
+/**
+ *  Configure echo delay and decay value
+ *  @param delay: delay in second
+ *  @param decay: decay in second
+ */
+void EchoAudioEngine::setEchoControls(float delay, float decay) {
+  echoDelay_ = delay;
+  echoDecay_ = decay;
+  if (delayEffect_) {
+    delayEffect_->setDelay(echoDelay_);
+    delayEffect_->setDecay(echoDecay_);
+  }
+}
+
diff --git a/samples/echo/src/main/cpp/EchoAudioEngine.h b/samples/echo/src/main/cpp/EchoAudioEngine.h
new file mode 100644
index 0000000..c937d6c
--- /dev/null
+++ b/samples/echo/src/main/cpp/EchoAudioEngine.h
@@ -0,0 +1,97 @@
+/*
+ * Copyright 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.
+ */
+
+#ifndef OBOE_ECHOAUDIOENGINE_H
+#define OBOE_ECHOAUDIOENGINE_H
+
+#include <thread>
+#include <jni.h>
+#include <string>
+#include "audio_common.h"
+#include "AudioEffect.h"
+
+const int32_t kLoopbackSampleRate = 48000;
+
+class EchoAudioEngine : public oboe::AudioStreamCallback {
+ public:
+  EchoAudioEngine();
+  ~EchoAudioEngine();
+  void setRecordingDeviceId(int32_t deviceId);
+  void setPlaybackDeviceId(int32_t deviceId);
+  void setEchoOn(bool isEchoOn);
+
+  /*
+   * oboe::AudioStreamCallback interface implementation
+   */
+  oboe::DataCallbackResult onAudioReady(oboe::AudioStream *oboeStream,
+                                        void *audioData, int32_t numFrames);
+  void onErrorBeforeClose(oboe::AudioStream *oboeStream, oboe::Result error);
+  void onErrorAfterClose(oboe::AudioStream *oboeStream, oboe::Result error);
+
+  /*
+   * handle fileStream
+   */
+  void setBackgroundStream(std::unique_ptr<int16_t[]>samples, size_t sampleCount,
+                           int32_t sampleRate, int32_t channelCount);
+  void setBackgroundMixer(float bgFactor);
+  void setEchoControls(float delay, float decay);
+  bool setAudioApi(oboe::AudioApi);
+  bool isAAudioSupported(void);
+
+ private:
+  bool isEchoOn_ = false;
+  uint64_t frameCallbackCount_ = 0;
+  int32_t recordingDeviceId_ = oboe::kUnspecified;
+  int32_t playbackDeviceId_ = oboe::kUnspecified;
+  oboe::AudioFormat format_ = oboe::AudioFormat::I16;
+  int32_t sampleRate_ = kLoopbackSampleRate;
+  int32_t inputChannelCount_ = kStereoChannelCount;
+  int32_t outputChannelCount_ = kStereoChannelCount;
+  oboe::AudioStream *recordingStream_ = nullptr;
+  oboe::AudioStream *playStream_ = nullptr;
+  int32_t framesPerBurst_;
+  std::mutex restartingLock_;
+  std::unique_ptr<AudioMixer> mixerEffect_ = nullptr;
+  std::unique_ptr<AudioDelay> delayEffect_ = nullptr;
+  uint64_t audioBlockingReadTimeout_ = NANOS_PER_MILLISECOND;
+  oboe::AudioApi audioApi_ = oboe::AudioApi::AAudio;
+  float echoDelay_ = 0.5f;
+  float echoDecay_ = 0.1f;
+
+  bool mixAudio_ = false;
+  std::unique_ptr<oboe::LatencyTuner> latencyTuner_;
+
+  void openRecordingStream();
+  void openPlaybackStream();
+
+  void startStream(oboe::AudioStream *stream);
+  void stopStream(oboe::AudioStream *stream);
+  void closeStream(oboe::AudioStream *stream);
+
+  void openAllStreams();
+  void closeAllStreams();
+  void restartStreams();
+
+  oboe::AudioStreamBuilder *setupCommonStreamParameters(
+      oboe::AudioStreamBuilder *builder);
+  oboe::AudioStreamBuilder *setupRecordingStreamParameters(
+      oboe::AudioStreamBuilder *builder);
+  oboe::AudioStreamBuilder *setupPlaybackStreamParameters(
+      oboe::AudioStreamBuilder *builder);
+  void warnIfNotLowLatency(oboe::AudioStream *stream);
+};
+
+#endif  // ECHO_ECHOAUDIOENGINE_H
diff --git a/samples/echo/src/main/cpp/jni_bridge.cpp b/samples/echo/src/main/cpp/jni_bridge.cpp
new file mode 100644
index 0000000..fe55fca
--- /dev/null
+++ b/samples/echo/src/main/cpp/jni_bridge.cpp
@@ -0,0 +1,174 @@
+/**
+ * Copyright 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.
+ */
+
+#include <jni.h>
+#include <logging_macros.h>
+#include "EchoAudioEngine.h"
+#include <android/asset_manager.h>
+#include <android/asset_manager_jni.h>
+
+static EchoAudioEngine *engine = nullptr;
+extern "C" {
+
+JNIEXPORT bool JNICALL
+Java_com_google_sample_oboe_echo_EchoEngine_create(JNIEnv *env, jclass) {
+  if (engine == nullptr) {
+    engine = new EchoAudioEngine();
+  }
+
+  return (engine != nullptr);
+}
+
+JNIEXPORT void JNICALL
+Java_com_google_sample_oboe_echo_EchoEngine_delete(JNIEnv *env, jclass) {
+  delete engine;
+  engine = nullptr;
+}
+
+JNIEXPORT void JNICALL Java_com_google_sample_oboe_echo_EchoEngine_setEchoOn(
+    JNIEnv *env, jclass, jboolean isEchoOn) {
+  if (engine == nullptr) {
+    LOGE(
+        "Engine is null, you must call createEngine before calling this "
+        "method");
+    return;
+  }
+
+  engine->setEchoOn(isEchoOn);
+}
+
+JNIEXPORT void JNICALL
+Java_com_google_sample_oboe_echo_EchoEngine_setRecordingDeviceId(
+    JNIEnv *env, jclass, jint deviceId) {
+  if (engine == nullptr) {
+    LOGE(
+        "Engine is null, you must call createEngine before calling this "
+        "method");
+    return;
+  }
+
+  engine->setRecordingDeviceId(deviceId);
+}
+
+JNIEXPORT void JNICALL
+Java_com_google_sample_oboe_echo_EchoEngine_setPlaybackDeviceId(
+    JNIEnv *env, jclass, jint deviceId) {
+  if (engine == nullptr) {
+    LOGE(
+        "Engine is null, you must call createEngine before calling this "
+        "method");
+    return;
+  }
+
+  engine->setPlaybackDeviceId(deviceId);
+}
+
+JNIEXPORT void JNICALL
+Java_com_google_sample_oboe_echo_EchoEngine_setStreamFile(
+  JNIEnv *env, jclass type, jobject assetMgr, jstring fileName,
+  jint channelCount, jint sampleRate) {
+
+  if (engine == nullptr) {
+    LOGE("Engine is null, you must call createEngine "
+         "before calling this method");
+    return;
+  }
+  const char *jniFileName = env->GetStringUTFChars(fileName, 0);
+
+  AAssetManager *jniAssetManager = AAssetManager_fromJava(env, assetMgr);
+  AAsset *sampleAsset = AAssetManager_open(jniAssetManager, jniFileName,
+                                           AASSET_MODE_UNKNOWN);
+  size_t sampleCount = static_cast<size_t>
+                       (AAsset_getLength(sampleAsset)/2);
+
+  // allocate memory to holds the full clip; the memory is released
+  // by the AudioMixer object when it is done.
+  std::unique_ptr<int16_t[]> samples =
+        std::unique_ptr<int16_t[]>(new int16_t[sampleCount]);
+  AAsset_read(sampleAsset, samples.get(), sampleCount * 2);
+
+  engine->setBackgroundStream(std::move(samples), sampleCount,
+                              sampleRate, channelCount);
+  AAsset_close(sampleAsset);
+
+  env->ReleaseStringUTFChars(fileName, jniFileName);
+}
+
+JNIEXPORT void JNICALL
+Java_com_google_sample_oboe_echo_EchoEngine_setMixer(
+      JNIEnv *env, jclass type, jfloat progress) {
+
+  if (engine == nullptr) {
+    LOGE("Engine is null, you must call createEngine "
+         "before calling this method");
+    return;
+  }
+  engine->setBackgroundMixer(progress);
+}
+
+JNIEXPORT void JNICALL
+Java_com_google_sample_oboe_echo_EchoEngine_setEchoControls(
+  JNIEnv *env, jclass type, jfloat delay, jfloat decay) {
+
+  if (engine == nullptr) {
+    LOGE("Engine is null, you must call createEngine "
+           "before calling this method");
+    return;
+  }
+  engine->setEchoControls(delay, decay);
+}
+
+
+static const int OBOE_API_AAUDIO = 0;
+static const int OBOE_API_OPENSL_ES = 1;
+
+JNIEXPORT jboolean JNICALL
+Java_com_google_sample_oboe_echo_EchoEngine_setAPI(JNIEnv *env, jclass type, jint apiType) {
+  if (engine == nullptr) {
+    LOGE("Engine is null, you must call createEngine "
+           "before calling this method");
+    return JNI_FALSE;
+  }
+
+  oboe::AudioApi audioApi;
+  switch (apiType) {
+    case OBOE_API_AAUDIO:
+      audioApi  = oboe::AudioApi::AAudio;
+      break;
+    case OBOE_API_OPENSL_ES:
+      audioApi = oboe::AudioApi::OpenSLES;
+      break;
+    default:
+      LOGE("Unknown API selection to setAPI() %d", apiType);
+      return JNI_FALSE;
+  }
+
+  return static_cast<jboolean>
+            (engine->setAudioApi(audioApi) ? JNI_TRUE : JNI_FALSE);
+}
+
+JNIEXPORT jboolean JNICALL
+Java_com_google_sample_oboe_echo_EchoEngine_isAAudioSupported(JNIEnv *env, jclass type) {
+  if (engine == nullptr) {
+    LOGE("Engine is null, you must call createEngine "
+           "before calling this method");
+    return JNI_FALSE;
+  }
+  return static_cast<jboolean >(engine->isAAudioSupported() ? JNI_TRUE : JNI_FALSE);
+}
+
+}
+
diff --git a/samples/echo/src/main/cpp/ndk-stl-config.cmake b/samples/echo/src/main/cpp/ndk-stl-config.cmake
new file mode 100644
index 0000000..72f0f8d
--- /dev/null
+++ b/samples/echo/src/main/cpp/ndk-stl-config.cmake
@@ -0,0 +1,40 @@
+# Copy shared STL files to Android Studio output directory so they can be
+# packaged in the APK.
+# Usage:
+#
+#   find_package(ndk-stl REQUIRED)
+#
+# or
+#
+#   find_package(ndk-stl REQUIRED PATHS ".")
+
+if(NOT ${ANDROID_STL} MATCHES "_shared")
+  return()
+endif()
+
+function(configure_shared_stl lib_path so_base)
+  message("Configuring STL ${so_base} for ${ANDROID_ABI}")
+  configure_file(
+    "${ANDROID_NDK}/sources/cxx-stl/${lib_path}/libs/${ANDROID_ABI}/lib${so_base}.so" 
+    "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}/lib${so_base}.so" 
+    COPYONLY)
+endfunction()
+
+if("${ANDROID_STL}" STREQUAL "libstdc++")
+  # The default minimal system C++ runtime library.
+elseif("${ANDROID_STL}" STREQUAL "gabi++_shared")
+  # The GAbi++ runtime (shared).
+  message(FATAL_ERROR "gabi++_shared was not configured by ndk-stl package")
+elseif("${ANDROID_STL}" STREQUAL "stlport_shared")
+  # The STLport runtime (shared).
+  configure_shared_stl("stlport" "stlport_shared")
+elseif("${ANDROID_STL}" STREQUAL "gnustl_shared")
+  # The GNU STL (shared).
+  configure_shared_stl("gnu-libstdc++/4.9" "gnustl_shared")
+elseif("${ANDROID_STL}" STREQUAL "c++_shared")
+  # The LLVM libc++ runtime (shared).
+  configure_shared_stl("llvm-libc++" "c++_shared")
+else()
+   message(FATAL_ERROR "STL configuration ANDROID_STL=${ANDROID_STL} is not supported")
+endif()
+
diff --git a/samples/echo/src/main/java/com/google/sample/oboe/echo/EchoEngine.java b/samples/echo/src/main/java/com/google/sample/oboe/echo/EchoEngine.java
new file mode 100644
index 0000000..58ddbb0
--- /dev/null
+++ b/samples/echo/src/main/java/com/google/sample/oboe/echo/EchoEngine.java
@@ -0,0 +1,46 @@
+package com.google.sample.oboe.echo;
+/*
+ * Copyright 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.
+ */
+
+import android.content.Context;
+import android.content.res.AssetManager;
+import android.media.AudioFormat;
+import android.media.AudioManager;
+import android.media.AudioRecord;
+import android.util.Log;
+
+public enum EchoEngine {
+
+    INSTANCE;
+
+    // Load native library
+    static {
+        System.loadLibrary("echo");
+    }
+
+    // Native methods
+    static native boolean create();
+    static native boolean isAAudioSupported();
+    static native boolean setAPI(int apiType);
+    static native void setStreamFile(AssetManager assetMgr, String fileName,
+                                         int channelCount, int frequency);
+    static native void setMixer(float progress);
+    static native boolean setEchoControls(float delay, float decay);
+    static native void setEchoOn(boolean isEchoOn);
+    static native void setRecordingDeviceId(int deviceId);
+    static native void setPlaybackDeviceId(int deviceId);
+    static native void delete();
+}
diff --git a/samples/echo/src/main/java/com/google/sample/oboe/echo/MainActivity.java b/samples/echo/src/main/java/com/google/sample/oboe/echo/MainActivity.java
new file mode 100644
index 0000000..09ab432
--- /dev/null
+++ b/samples/echo/src/main/java/com/google/sample/oboe/echo/MainActivity.java
@@ -0,0 +1,342 @@
+/*
+ * Copyright 2015 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.echo;
+
+import android.Manifest;
+import android.app.Activity;
+import android.content.pm.PackageManager;
+import android.content.res.AssetManager;
+import android.media.AudioManager;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.v4.app.ActivityCompat;
+import android.util.Log;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.Button;
+import android.widget.RadioButton;
+import android.widget.RadioGroup;
+import android.widget.SeekBar;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import com.google.sample.audio_device.AudioDeviceListEntry;
+import com.google.sample.audio_device.AudioDeviceSpinner;
+
+import java.io.IOException;
+
+/**
+ * TODO: Update README.md and go through and comment sample
+ */
+public class MainActivity extends Activity
+        implements ActivityCompat.OnRequestPermissionsResultCallback {
+
+    private static final String TAG = MainActivity.class.getName();
+    private static final int AUDIO_ECHO_REQUEST = 0;
+    private static final int OBOE_API_AAUDIO = 0;
+    private static final int OBOE_API_OPENSL_ES=1;
+
+    private TextView statusText;
+    private Button toggleEchoButton;
+    private AudioDeviceSpinner recordingDeviceSpinner;
+    private AudioDeviceSpinner playbackDeviceSpinner;
+    private boolean isPlaying = false;
+
+    private int apiSelection = OBOE_API_AAUDIO;
+    private boolean aaudioSupported = true;
+    private SeekBar mixerSeekbar, echoDelaySeekbar, echoDecaySeekbar;
+    private TextView echoDelayTV, echoDecayTV;
+    float   mixerProgress, echoDelayProgress, echoDecayProgress;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.activity_main);
+
+        statusText = findViewById(R.id.status_view_text);
+        toggleEchoButton = findViewById(R.id.button_toggle_echo);
+        toggleEchoButton.setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View view) {
+                toggleEcho();
+            }
+        });
+        toggleEchoButton.setText(getString(R.string.start_echo));
+
+        recordingDeviceSpinner = findViewById(R.id.recording_devices_spinner);
+        recordingDeviceSpinner.setDirectionType(AudioManager.GET_DEVICES_INPUTS);
+        recordingDeviceSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
+            @Override
+            public void onItemSelected(AdapterView<?> adapterView, View view, int i, long l) {
+                EchoEngine.setRecordingDeviceId(getRecordingDeviceId());
+            }
+
+            @Override
+            public void onNothingSelected(AdapterView<?> adapterView) {
+                // Do nothing
+            }
+        });
+
+        playbackDeviceSpinner = findViewById(R.id.playback_devices_spinner);
+        playbackDeviceSpinner.setDirectionType(AudioManager.GET_DEVICES_OUTPUTS);
+        playbackDeviceSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
+            @Override
+            public void onItemSelected(AdapterView<?> adapterView, View view, int i, long l) {
+                EchoEngine.setPlaybackDeviceId(getPlaybackDeviceId());
+            }
+
+            @Override
+            public void onNothingSelected(AdapterView<?> adapterView) {
+                // Do nothing
+            }
+        });
+
+        ((RadioGroup)findViewById(R.id.apiSelectionGroup)).check(R.id.aaudioButton);
+        findViewById(R.id.aaudioButton).setOnClickListener(new RadioButton.OnClickListener(){
+            @Override
+            public void onClick(View v) {
+                if (((RadioButton)v).isChecked()) {
+                    apiSelection = OBOE_API_AAUDIO;
+                }
+            }
+        });
+        findViewById(R.id.slesButton).setOnClickListener(new RadioButton.OnClickListener(){
+            @Override
+            public void onClick(View v) {
+                if (((RadioButton)v).isChecked()) {
+                    apiSelection = OBOE_API_OPENSL_ES;
+                }
+            }
+        });
+
+        mixerSeekbar = findViewById(R.id.mixerSeekBar);
+        mixerProgress = mixerSeekbar.getProgress()/((float)mixerSeekbar.getMax());
+        mixerSeekbar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
+            @Override
+            public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+                if (!fromUser) return;
+
+                Log.d(TAG, "mixerSeekBar value = " + progress);
+                mixerProgress = progress / (float)mixerSeekbar.getMax();
+                EchoEngine.setMixer(mixerProgress);
+            }
+
+            @Override
+            public void onStartTrackingTouch(SeekBar seekBar) {
+            }
+
+            @Override
+            public void onStopTrackingTouch(SeekBar seekBar) {
+            }
+        });
+
+        echoDelaySeekbar = findViewById(R.id.echoDelaySeekBar);
+        echoDelayTV = findViewById(R.id.echoCurDelay);
+        echoDelayProgress = (float)echoDelaySeekbar.getProgress()
+                             / echoDelaySeekbar.getMax();
+        echoDelaySeekbar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
+            @Override
+            public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+
+                float curVal = (float)progress/seekBar.getMax();
+                echoDelayTV.setText(String.format("%s", curVal));
+                setSeekBarPromptPosition(echoDelaySeekbar, echoDelayTV);
+
+                if (!fromUser) return;
+                echoDelayProgress = curVal;
+                EchoEngine.setEchoControls(echoDelayProgress, echoDecayProgress);
+            }
+
+            @Override
+            public void onStartTrackingTouch(SeekBar seekBar) {
+            }
+
+            @Override
+            public void onStopTrackingTouch(SeekBar seekBar) {
+            }
+        });
+        echoDelaySeekbar.post(new Runnable() {
+            @Override
+            public void run() {
+                setSeekBarPromptPosition(echoDelaySeekbar, echoDelayTV);
+            }
+        });
+
+        echoDecaySeekbar = findViewById(R.id.echoDecaySeekBar);
+        echoDecayTV = findViewById(R.id.echoCurDecay);
+        echoDecayProgress = (float)echoDecaySeekbar.getProgress() / echoDecaySeekbar.getMax();
+        echoDecaySeekbar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
+            @Override
+            public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+                float curVal = (float)progress / seekBar.getMax();
+                echoDecayTV.setText(String.format("%s", curVal));
+                setSeekBarPromptPosition(echoDecaySeekbar, echoDecayTV);
+                if (!fromUser)
+                    return;
+
+                echoDecayProgress = curVal;
+                EchoEngine.setEchoControls(echoDelayProgress, echoDecayProgress);
+            }
+            @Override
+            public void onStartTrackingTouch(SeekBar seekBar) {}
+            @Override
+            public void onStopTrackingTouch(SeekBar seekBar) {}
+        });
+        echoDecaySeekbar.post(new Runnable() {
+            @Override
+            public void run() {
+                setSeekBarPromptPosition(echoDecaySeekbar, echoDecayTV);
+            }
+        });
+        EchoEngine.create();
+        aaudioSupported = EchoEngine.isAAudioSupported();
+        EnableAudioApiUI(true);
+        EchoEngine.setAPI(apiSelection);
+
+        try {
+            AssetManager assetManager = getAssets();
+            String[] assets = assetManager.list("");
+            int channels = 2;
+            int frequency = 48000;
+            EchoEngine.setStreamFile(assetManager, assets[1], channels, frequency);
+            EchoEngine.setMixer(mixerProgress);
+        } catch (IOException e) {
+            Log.e(TAG, e.getMessage());
+        }
+        EchoEngine.setEchoControls(echoDelayProgress, echoDecayProgress);
+    }
+    private void setSeekBarPromptPosition(SeekBar seekBar, TextView label) {
+        float thumbX = (float)seekBar.getProgress()/ seekBar.getMax() *
+                seekBar.getWidth() + seekBar.getX();
+        label.setX(thumbX - label.getWidth()/2.0f);
+    }
+    private void EnableAudioApiUI(boolean enable) {
+        if(apiSelection == OBOE_API_AAUDIO && !aaudioSupported)
+        {
+            apiSelection = OBOE_API_OPENSL_ES;
+        }
+        findViewById(R.id.slesButton).setEnabled(enable);
+        if(!aaudioSupported) {
+          findViewById(R.id.aaudioButton).setEnabled(false);
+        } else {
+            findViewById(R.id.aaudioButton).setEnabled(enable);
+        }
+
+        ((RadioGroup)findViewById(R.id.apiSelectionGroup))
+          .check(apiSelection == OBOE_API_AAUDIO ? R.id.aaudioButton : R.id.slesButton);
+    }
+    @Override
+    protected void onStart() {
+        super.onStart();
+        setVolumeControlStream(AudioManager.STREAM_MUSIC);
+    }
+
+    @Override
+    protected void onDestroy() {
+        EchoEngine.delete();
+        super.onDestroy();
+    }
+
+    public void toggleEcho() {
+        if (isPlaying) {
+            stopEchoing();
+            EnableAudioApiUI(true);
+        } else {
+            EnableAudioApiUI(false);
+            EchoEngine.setAPI(apiSelection);
+            startEchoing();
+        }
+    }
+
+    private void startEchoing() {
+        Log.d(TAG, "Attempting to start");
+
+        if (!isRecordPermissionGranted()){
+            requestRecordPermission();
+            return;
+        }
+
+        setSpinnersEnabled(false);
+        EchoEngine.setEchoControls(echoDelayProgress, echoDecayProgress);
+        EchoEngine.setEchoOn(true);
+        statusText.setText(R.string.status_echoing);
+        toggleEchoButton.setText(R.string.stop_echo);
+        isPlaying = true;
+    }
+
+    private void stopEchoing() {
+        Log.d(TAG, "Playing, attempting to stop");
+        EchoEngine.setEchoOn(false);
+        resetStatusView();
+        toggleEchoButton.setText(R.string.start_echo);
+        isPlaying = false;
+        setSpinnersEnabled(true);
+    }
+
+    private void setSpinnersEnabled(boolean isEnabled){
+        recordingDeviceSpinner.setEnabled(isEnabled);
+        playbackDeviceSpinner.setEnabled(isEnabled);
+    }
+
+    private int getRecordingDeviceId(){
+        return ((AudioDeviceListEntry)recordingDeviceSpinner.getSelectedItem()).getId();
+    }
+
+    private int getPlaybackDeviceId(){
+        return ((AudioDeviceListEntry)playbackDeviceSpinner.getSelectedItem()).getId();
+    }
+
+    private boolean isRecordPermissionGranted() {
+        return (ActivityCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) ==
+                PackageManager.PERMISSION_GRANTED);
+    }
+
+    private void requestRecordPermission(){
+        ActivityCompat.requestPermissions(
+                this,
+                new String[]{Manifest.permission.RECORD_AUDIO},
+                AUDIO_ECHO_REQUEST);
+    }
+    private void resetStatusView() {
+        statusText.setText(R.string.status_warning);
+    }
+
+    @Override
+    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
+                                           @NonNull int[] grantResults) {
+
+        if (AUDIO_ECHO_REQUEST != requestCode) {
+            super.onRequestPermissionsResult(requestCode, permissions, grantResults);
+            return;
+        }
+
+        if (grantResults.length != 1 ||
+                grantResults[0] != PackageManager.PERMISSION_GRANTED) {
+
+            // User denied the permission, without this we cannot record audio
+            // Show a toast and update the status accordingly
+            statusText.setText(R.string.status_record_audio_denied);
+            Toast.makeText(getApplicationContext(),
+                    getString(R.string.need_record_audio_permission),
+                    Toast.LENGTH_SHORT)
+                    .show();
+        } else {
+            // Permission was granted, start echoing
+            toggleEcho();
+        }
+    }
+}
diff --git a/samples/echo/src/main/res/drawable/balance_seekbar.xml b/samples/echo/src/main/res/drawable/balance_seekbar.xml
new file mode 100644
index 0000000..127181e
--- /dev/null
+++ b/samples/echo/src/main/res/drawable/balance_seekbar.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android" >
+    <item android:id="@android:id/background">
+        <shape android:shape="rectangle" >
+            <solid
+                android:color="@color/colorBlue" />
+        </shape>
+    </item>
+    <item android:id="@android:id/progress">
+        <clip>
+            <shape android:shape="rectangle" >
+                <solid
+                    android:color="@color/colorBlue" />
+            </shape>
+        </clip>
+    </item>
+</layer-list>
\ No newline at end of file
diff --git a/samples/echo/src/main/res/layout/activity_main.xml b/samples/echo/src/main/res/layout/activity_main.xml
new file mode 100644
index 0000000..cbfc51f
--- /dev/null
+++ b/samples/echo/src/main/res/layout/activity_main.xml
@@ -0,0 +1,232 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ 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.
+  -->
+<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    tools:context="com.google.sample.oboe.echo.MainActivity"
+    tools:layout_editor_absoluteY="81dp">
+
+    <TextView
+        android:id="@+id/textView"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginStart="@dimen/activity_horizontal_margin"
+        android:layout_marginTop="@dimen/activity_vertical_margin"
+        android:text="@string/recording_device"
+        app:layout_constraintLeft_toLeftOf="parent"
+        app:layout_constraintTop_toTopOf="parent" />
+
+    <com.google.sample.audio_device.AudioDeviceSpinner
+        android:id="@+id/recording_devices_spinner"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginStart="@dimen/activity_horizontal_margin"
+        android:layout_marginTop="0dp"
+        app:layout_constraintLeft_toLeftOf="parent"
+        app:layout_constraintTop_toBottomOf="@+id/textView" />
+
+    <TextView
+        android:id="@+id/textView2"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginStart="@dimen/activity_horizontal_margin"
+        android:layout_marginTop="@dimen/activity_vertical_margin"
+        android:text="@string/playback_device"
+        app:layout_constraintLeft_toLeftOf="parent"
+        app:layout_constraintTop_toBottomOf="@+id/recording_devices_spinner" />
+
+    <com.google.sample.audio_device.AudioDeviceSpinner
+        android:id="@+id/playback_devices_spinner"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginStart="@dimen/activity_horizontal_margin"
+        android:layout_marginTop="0dp"
+        app:layout_constraintLeft_toLeftOf="parent"
+        app:layout_constraintTop_toBottomOf="@+id/textView2" />
+
+    <RadioGroup xmlns:android="http://schemas.android.com/apk/res/android"
+        android:id="@+id/apiSelectionGroup"
+        android:layout_width="fill_parent"
+        android:layout_height="wrap_content"
+        android:layout_marginStart="@dimen/activity_horizontal_margin"
+        android:layout_marginTop="@dimen/activity_vertical_margin"
+        android:orientation="horizontal"
+        app:layout_constraintTop_toBottomOf="@+id/playback_devices_spinner"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintEnd_toEndOf="parent">
+
+        <TextView
+            android:id="@+id/apiTextView"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="@string/apiSelection" />
+
+        <RadioButton
+            android:id="@+id/aaudioButton"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginStart="16dp"
+            android:text="@string/aaudio" />
+
+        <RadioButton
+            android:id="@+id/slesButton"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginStart="16dp"
+            android:text="@string/sles" />
+    </RadioGroup>
+
+    <TextView
+        android:id="@+id/voiceLabel"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginStart="@dimen/activity_horizontal_margin"
+        android:layout_marginTop="@dimen/activity_vertical_group_margin"
+        android:layout_marginEnd="0dp"
+        android:text="@string/voice_label"
+        android:visibility="visible"
+        app:layout_constraintTop_toBottomOf="@+id/apiSelectionGroup"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintVertical_bias="0.0" />
+
+    <SeekBar
+        android:id="@+id/mixerSeekBar"
+        android:layout_width="0dp"
+        android:layout_height="17dp"
+        android:layout_marginTop="5dp"
+        android:layout_marginStart="1dp"
+        android:layout_marginEnd="1dp"
+        android:maxHeight="3dp"
+        android:minHeight="3dp"
+        android:max="10"
+        android:min="0"
+        android:progress="5"
+        android:progressDrawable="@drawable/balance_seekbar"
+        app:layout_constraintStart_toEndOf="@+id/voiceLabel"
+        app:layout_constraintEnd_toStartOf="@+id/musicLabel"
+        app:layout_constraintTop_toTopOf="@+id/voiceLabel" />
+
+    <TextView
+        android:id="@+id/musicLabel"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="end"
+        android:text="@string/music_label"
+        android:visibility="visible"
+        app:layout_constraintRight_toRightOf="parent"
+        app:layout_constraintTop_toTopOf="@+id/voiceLabel"
+        app:layout_constraintVertical_bias="0.0" />
+
+    <TextView
+        android:id="@+id/echoCurDelay"
+        android:layout_gravity="center"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="@dimen/activity_vertical_group_margin"
+        android:text="@string/echo_init_delay"
+        android:visibility="visible"
+        app:layout_constraintTop_toBottomOf="@+id/voiceLabel"
+        app:layout_constraintStart_toEndOf="@+id/echoDelayLabel" />
+
+    <TextView
+        android:id="@+id/echoDelayLabel"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginStart="@dimen/activity_horizontal_margin"
+        android:text="@string/echo_delay_label"
+        android:visibility="visible"
+        app:layout_constraintTop_toBottomOf="@+id/echoCurDelay"
+        app:layout_constraintStart_toStartOf="parent" />
+
+    <SeekBar
+        android:id="@+id/echoDelaySeekBar"
+        android:layout_width="0dp"
+        android:layout_height="17dp"
+        android:layout_marginTop="5dp"
+        android:layout_marginEnd="@dimen/activity_horizontal_margin"
+        android:max="10"
+        android:min="0"
+        android:progress="5"
+        app:layout_constraintStart_toEndOf="@+id/echoDelayLabel"
+        app:layout_constraintTop_toTopOf="@+id/echoDelayLabel"
+        app:layout_constraintEnd_toEndOf="parent" />
+
+    <TextView
+        android:id="@+id/echoCurDecay"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center"
+        android:layout_marginTop="@dimen/activity_vertical_margin"
+        android:text="@string/echo_init_decay"
+        android:visibility="visible"
+        app:layout_constraintTop_toBottomOf="@+id/echoDelayLabel"
+        app:layout_constraintStart_toEndOf="@+id/echoDecayLabel"/>
+
+    <TextView
+        android:id="@+id/echoDecayLabel"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="start"
+        android:layout_marginStart="@dimen/activity_horizontal_margin"
+        android:text="@string/echo_decay_label"
+        android:visibility="visible"
+        app:layout_constraintTop_toBottomOf="@+id/echoCurDecay"
+        app:layout_constraintStart_toStartOf="parent"/>
+
+    <SeekBar
+        android:id="@+id/echoDecaySeekBar"
+        android:layout_width="0dp"
+        android:layout_height="17dp"
+        android:layout_marginTop="5dp"
+        android:layout_marginEnd="@dimen/activity_horizontal_margin"
+        android:max="10"
+        android:min="0"
+        android:progress="1"
+        app:layout_constraintTop_toTopOf="@+id/echoDecayLabel"
+        app:layout_constraintBottom_toBottomOf="@+id/echoDecayLabel"
+        app:layout_constraintStart_toEndOf="@+id/echoDecayLabel"
+        app:layout_constraintEnd_toEndOf="parent"/>
+
+    <TextView
+        android:id="@+id/status_view_text"
+        android:layout_width="0dp"
+        android:layout_height="60dp"
+        android:layout_marginStart="@dimen/activity_horizontal_margin"
+        android:layout_marginEnd="@dimen/activity_horizontal_margin"
+        android:layout_marginTop="@dimen/activity_vertical_group_margin"
+        android:lines="6"
+        android:text="@string/status_warning"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintTop_toBottomOf="@+id/echoDecayLabel" />
+
+    <Button
+        android:id="@+id/button_toggle_echo"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:gravity="center"
+        android:layout_marginTop="@dimen/activity_vertical_margin"
+        android:textAllCaps="false"
+        android:text="@string/start_echo"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintTop_toBottomOf="@+id/status_view_text" />
+
+</android.support.constraint.ConstraintLayout>
diff --git a/samples/echo/src/main/res/mipmap-hdpi/ic_launcher.png b/samples/echo/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..cde69bc
--- /dev/null
+++ b/samples/echo/src/main/res/mipmap-hdpi/ic_launcher.png
Binary files differ
diff --git a/samples/echo/src/main/res/mipmap-mdpi/ic_launcher.png b/samples/echo/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..c133a0c
--- /dev/null
+++ b/samples/echo/src/main/res/mipmap-mdpi/ic_launcher.png
Binary files differ
diff --git a/samples/echo/src/main/res/mipmap-xhdpi/ic_launcher.png b/samples/echo/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..bfa42f0
--- /dev/null
+++ b/samples/echo/src/main/res/mipmap-xhdpi/ic_launcher.png
Binary files differ
diff --git a/samples/echo/src/main/res/mipmap-xxhdpi/ic_launcher.png b/samples/echo/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..324e72c
--- /dev/null
+++ b/samples/echo/src/main/res/mipmap-xxhdpi/ic_launcher.png
Binary files differ
diff --git a/samples/echo/src/main/res/values-v21/styles.xml b/samples/echo/src/main/res/values-v21/styles.xml
new file mode 100644
index 0000000..dba3c41
--- /dev/null
+++ b/samples/echo/src/main/res/values-v21/styles.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <style name="AppTheme" parent="android:Theme.Material.Light">
+    </style>
+</resources>
diff --git a/samples/echo/src/main/res/values-w820dp/dimens.xml b/samples/echo/src/main/res/values-w820dp/dimens.xml
new file mode 100644
index 0000000..63fc816
--- /dev/null
+++ b/samples/echo/src/main/res/values-w820dp/dimens.xml
@@ -0,0 +1,6 @@
+<resources>
+    <!-- Example customization of dimensions originally defined in res/values/dimens.xml
+         (such as screen margins) for screens with more than 820dp of available width. This
+         would include 7" and 10" devices in landscape (~960dp and ~1280dp respectively). -->
+    <dimen name="activity_horizontal_margin">64dp</dimen>
+</resources>
diff --git a/samples/echo/src/main/res/values/colors.xml b/samples/echo/src/main/res/values/colors.xml
new file mode 100644
index 0000000..87b8786
--- /dev/null
+++ b/samples/echo/src/main/res/values/colors.xml
@@ -0,0 +1,3 @@
+<resources>
+    <color name="colorBlue">#4444CC</color>
+</resources>
\ No newline at end of file
diff --git a/samples/echo/src/main/res/values/dimens.xml b/samples/echo/src/main/res/values/dimens.xml
new file mode 100644
index 0000000..d6bea8d
--- /dev/null
+++ b/samples/echo/src/main/res/values/dimens.xml
@@ -0,0 +1,6 @@
+<resources>
+    <!-- Default screen margins, per the Android Design guidelines. -->
+    <dimen name="activity_horizontal_margin">16dp</dimen>
+    <dimen name="activity_vertical_margin">16dp</dimen>
+    <dimen name="activity_vertical_group_margin">32dp</dimen>
+</resources>
diff --git a/samples/echo/src/main/res/values/strings.xml b/samples/echo/src/main/res/values/strings.xml
new file mode 100644
index 0000000..9025491
--- /dev/null
+++ b/samples/echo/src/main/res/values/strings.xml
@@ -0,0 +1,24 @@
+<resources>
+    <string name="app_name">Oboe Echo</string>
+    <string name="action_settings">Settings</string>
+    <string name="start_echo">Start</string>
+    <string name="stop_echo">Stop</string>
+    <string name="need_record_audio_permission">"This sample needs RECORD_AUDIO permission"</string>
+    <string name="status_echoing">Engine Echoing ....</string>
+    <string name="status_record_audio_denied">Error: Permission for RECORD_AUDIO was denied</string>
+    <string name="status_touch_to_begin">RECORD_AUDIO permission granted, touch START to begin</string>
+    <string name="status_warning">Warning: If you run this sample using the built-in microphone
+        and speaker you may create a feedback loop which will not be pleasant to listen to.</string>
+    <string name="recording_device">Recording device</string>
+    <string name="playback_device">Playback device</string>
+
+    <string name="apiSelection">APIs</string>
+    <string name="aaudio">AAudio</string>
+    <string name="sles">OpenSL ES</string>
+    <string name="voice_label">Voice</string>
+    <string name="music_label">Music</string>
+    <string name="echo_init_delay">0.5</string>
+    <string name="echo_delay_label">Delay(sec)</string>
+    <string name="echo_init_decay">0.1</string>
+    <string name="echo_decay_label">Decay</string>
+</resources>
diff --git a/samples/echo/src/main/res/values/styles.xml b/samples/echo/src/main/res/values/styles.xml
new file mode 100644
index 0000000..ff6c9d2
--- /dev/null
+++ b/samples/echo/src/main/res/values/styles.xml
@@ -0,0 +1,8 @@
+<resources>
+
+    <!-- Base application theme. -->
+    <style name="AppTheme" parent="android:Theme.Holo.Light.DarkActionBar">
+        <!-- Customize your theme here. -->
+    </style>
+
+</resources>
diff --git a/samples/settings.gradle b/samples/settings.gradle
index 0e8521a..647caee 100644
--- a/samples/settings.gradle
+++ b/samples/settings.gradle
@@ -19,3 +19,5 @@
 include ':hello-oboe'
 include ':RhythmGame'
 include ':MegaDrone'
+include 'echo'
+