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'
+