Addressing more code reviews
diff --git a/.ci_tools/build_samples.sh b/.ci_tools/build_samples.sh
index 4c300a6..0f28978 100755
--- a/.ci_tools/build_samples.sh
+++ b/.ci_tools/build_samples.sh
@@ -18,12 +18,11 @@
# Check the apks that all get built fine
-# TBD: add echo after it is merged
-# samples/echo/build/outputs/apk/debug/echo-debug.apk
declare apks=(
samples/hello-oboe/build/outputs/apk/debug/hello-oboe-debug.apk
samples/MegaDrone/build/outputs/apk/debug/MegaDrone-debug.apk
samples/RhythmGame/build/outputs/apk/debug/RhythmGame-debug.apk
+ samples/LiveEffect/build/outputs/apk/debug/LiveEffect-debug.apk
)
rm -fr ${BUILD_RESULT_FILE}
diff --git a/samples/LiveEffect/README.md b/samples/LiveEffect/README.md
new file mode 100644
index 0000000..f699ccf
--- /dev/null
+++ b/samples/LiveEffect/README.md
@@ -0,0 +1,23 @@
+LiveEffect Sample
+============
+
+This sample simply loops audio from input stream to output stream to demonstrate
+the usage of the 2 stream interfaces.
+
+Screenshots
+-----------
+
+
+
+
+### Stream Configurations
+- 48kHz
+- oboe::I16
+- stereo or mono
+
+
+### Caveats
+When first time starting audio devices, the stream may not be stable.
+The symptom is the strange callback pattern. This sample waits half a second
+for audio system to stablize. It is an estimate, it would vary on different platforms.
+
diff --git a/samples/echo/build.gradle b/samples/LiveEffect/build.gradle
similarity index 60%
rename from samples/echo/build.gradle
rename to samples/LiveEffect/build.gradle
index b952cf4..c612f3b 100644
--- a/samples/echo/build.gradle
+++ b/samples/LiveEffect/build.gradle
@@ -1,28 +1,26 @@
apply plugin: 'com.android.application'
android {
- compileSdkVersion 27
+ compileSdkVersion 26
defaultConfig {
- applicationId 'com.google.sample.oboe.echo'
- minSdkVersion 24
- targetSdkVersion 27
+ applicationId 'com.google.sample.oboe.liveeffect'
+ minSdkVersion 16
+ targetSdkVersion 26
versionCode 1
versionName '1.0'
ndk {
- abiFilters 'armeabi-v7a', 'arm64-v8a'
+ abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
}
externalNativeBuild {
cmake {
- arguments '-DANDROID_STL=c++_shared', '-DANDROID_TOOLCHAIN=clang',
- '-DANDROID_PLATFORM=android-26'
+ arguments '-DANDROID_TOOLCHAIN=clang'
}
}
}
buildTypes {
release {
minifyEnabled false
- proguardFiles getDefaultProguardFile('proguard-android.txt')
}
}
externalNativeBuild {
diff --git a/samples/LiveEffect/screenshot.png b/samples/LiveEffect/screenshot.png
new file mode 100644
index 0000000..f64a883
--- /dev/null
+++ b/samples/LiveEffect/screenshot.png
Binary files differ
diff --git a/samples/echo/src/main/AndroidManifest.xml b/samples/LiveEffect/src/main/AndroidManifest.xml
similarity index 89%
rename from samples/echo/src/main/AndroidManifest.xml
rename to samples/LiveEffect/src/main/AndroidManifest.xml
index bd55980..04cba7e 100644
--- a/samples/echo/src/main/AndroidManifest.xml
+++ b/samples/LiveEffect/src/main/AndroidManifest.xml
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
- package="com.google.sample.oboe.echo" >
+ package="com.google.sample.oboe.liveEffect" >
<uses-feature android:name="android.hardware.microphone" android:required="true" />
<uses-feature android:name="android.hardware.audio.output" android:required="true" />
@@ -14,7 +14,7 @@
android:label="@string/app_name"
android:theme="@style/AppTheme" >
<activity
- android:name="com.google.sample.oboe.echo.MainActivity"
+ android:name="com.google.sample.oboe.liveEffect.MainActivity"
android:label="@string/app_name"
android:screenOrientation="portrait">
<intent-filter>
diff --git a/samples/echo/src/main/cpp/CMakeLists.txt b/samples/LiveEffect/src/main/cpp/CMakeLists.txt
similarity index 76%
rename from samples/echo/src/main/cpp/CMakeLists.txt
rename to samples/LiveEffect/src/main/cpp/CMakeLists.txt
index fe6db7c..a5a3e96 100644
--- a/samples/echo/src/main/cpp/CMakeLists.txt
+++ b/samples/LiveEffect/src/main/cpp/CMakeLists.txt
@@ -14,7 +14,7 @@
# limitations under the License.
#
cmake_minimum_required(VERSION 3.4.1)
-project(echo LANGUAGES C CXX)
+project(liveEffect LANGUAGES C CXX)
get_filename_component(SAMPLE_ROOT_DIR
${CMAKE_CURRENT_SOURCE_DIR}/../../../.. ABSOLUTE)
@@ -23,25 +23,22 @@
set (OBOE_DIR ${SAMPLE_ROOT_DIR}/..)
add_subdirectory(${OBOE_DIR} ./oboe-bin)
-add_library(echo
+add_library(liveEffect
SHARED
- EchoAudioEngine.cpp
+ LiveEffectEngine.cpp
jni_bridge.cpp
- AudioEffect.cpp
- ${SAMPLE_ROOT_DIR}/debug-utils/trace.cpp
- ${SAMPLE_ROOT_DIR}/common/audio_common.cpp)
-target_include_directories(echo
+ ${SAMPLE_ROOT_DIR}/debug-utils/trace.cpp)
+target_include_directories(liveEffect
PRIVATE
- ${SAMPLE_ROOT_DIR}/common
${SAMPLE_ROOT_DIR}/debug-utils
${OBOE_DIR}/include)
-target_link_libraries(echo
+target_link_libraries(liveEffect
PRIVATE
oboe
android
atomic
log)
-target_compile_options(echo
+target_compile_options(liveEffect
PRIVATE
-Wall -Werror)
diff --git a/samples/LiveEffect/src/main/cpp/LiveEffectEngine.cpp b/samples/LiveEffect/src/main/cpp/LiveEffectEngine.cpp
new file mode 100644
index 0000000..b83081b
--- /dev/null
+++ b/samples/LiveEffect/src/main/cpp/LiveEffectEngine.cpp
@@ -0,0 +1,373 @@
+/**
+ * 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 "LiveEffectEngine.h"
+#include <assert.h>
+#include <logging_macros.h>
+#include <climits>
+
+/**
+ * Duplex is not very stable right after starting up:
+ * the callbacks may not be happening at the right times
+ * The time to get it stable varies on different systems. Half second
+ * is used for this sample, during the time this sample plays silence.
+ */
+const float kSystemWarmupTime = 0.5f;
+
+LiveEffectEngine::LiveEffectEngine() {
+ assert(mOutputChannelCount == mInputChannelCount);
+}
+
+LiveEffectEngine::~LiveEffectEngine() {
+ stopStream(mPlayStream);
+ stopStream(mRecordingStream);
+
+ closeStream(mPlayStream);
+
+ closeStream(mRecordingStream);
+}
+
+void LiveEffectEngine::setRecordingDeviceId(int32_t deviceId) {
+ mRecordingDeviceId = deviceId;
+}
+
+void LiveEffectEngine::setPlaybackDeviceId(int32_t deviceId) {
+ mPlaybackDeviceId = deviceId;
+}
+
+bool LiveEffectEngine::isAAudioSupported() {
+ oboe::AudioStreamBuilder builder;
+ return builder.isAAudioSupported();
+}
+bool LiveEffectEngine::setAudioApi(oboe::AudioApi api) {
+ if (mIsEffectOn) return false;
+
+ mAudioApi = api;
+ return true;
+}
+void LiveEffectEngine::setEffectOn(bool isOn) {
+ if (isOn != mIsEffectOn) {
+ mIsEffectOn = isOn;
+
+ if (isOn) {
+ openAllStreams();
+ } else {
+ closeAllStreams();
+ }
+ }
+}
+
+void LiveEffectEngine::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 (mRecordingStream && mPlayStream) {
+ startStream(mRecordingStream);
+ startStream(mPlayStream);
+ } else {
+ LOGE("Failed to create recording (%p) and/or playback (%p) stream",
+ mRecordingStream, mPlayStream);
+ closeAllStreams();
+ }
+}
+
+/**
+ * Stops and closes the playback and recording streams.
+ */
+void LiveEffectEngine::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 (mPlayStream != nullptr) {
+ closeStream(mPlayStream); // Calling close will also stop the stream
+ mPlayStream = nullptr;
+ }
+
+ if (mRecordingStream != nullptr) {
+ closeStream(mRecordingStream);
+ mRecordingStream = nullptr;
+ }
+}
+
+/**
+ * Creates an audio stream for recording. The audio device used will depend on
+ * mRecordingDeviceId.
+ * If the value is set to oboe::Unspecified then the default recording device
+ * will be used.
+ */
+void LiveEffectEngine::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(&mRecordingStream);
+ if (result == oboe::Result::OK && mRecordingStream) {
+ assert(mRecordingStream->getChannelCount() == mInputChannelCount);
+ assert(mRecordingStream->getSampleRate() == mSampleRate);
+ assert(mRecordingStream->getFormat() == oboe::AudioFormat::I16);
+
+ warnIfNotLowLatency(mRecordingStream);
+ } else {
+ LOGE("Failed to create recording stream. Error: %s",
+ oboe::convertToText(result));
+ }
+}
+
+/**
+ * Creates an audio stream for playback. The audio device used will depend on
+ * mPlaybackDeviceId.
+ * If the value is set to oboe::Unspecified then the default playback device
+ * will be used.
+ */
+void LiveEffectEngine::openPlaybackStream() {
+ oboe::AudioStreamBuilder builder;
+
+ setupPlaybackStreamParameters(&builder);
+ oboe::Result result = builder.openStream(&mPlayStream);
+ if (result == oboe::Result::OK && mPlayStream) {
+ mSampleRate = mPlayStream->getSampleRate();
+
+ assert(mPlayStream->getFormat() == oboe::AudioFormat::I16);
+ assert(mOutputChannelCount == mPlayStream->getChannelCount());
+
+ mSystemStartupFrames =
+ static_cast<uint64_t>(mSampleRate * kSystemWarmupTime);
+ mProcessedFrameCount = 0;
+
+ warnIfNotLowLatency(mPlayStream);
+
+ } 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 *LiveEffectEngine::setupRecordingStreamParameters(
+ oboe::AudioStreamBuilder *builder) {
+ // This sample uses blocking read() by setting callback to null
+ builder->setCallback(nullptr)
+ ->setDeviceId(mRecordingDeviceId)
+ ->setDirection(oboe::Direction::Input)
+ ->setSampleRate(mSampleRate)
+ ->setChannelCount(mInputChannelCount);
+ 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 *LiveEffectEngine::setupPlaybackStreamParameters(
+ oboe::AudioStreamBuilder *builder) {
+ builder->setCallback(this)
+ ->setDeviceId(mPlaybackDeviceId)
+ ->setDirection(oboe::Direction::Output)
+ ->setChannelCount(mOutputChannelCount);
+
+ 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 *LiveEffectEngine::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(mAudioApi)
+ ->setFormat(mFormat)
+ ->setSharingMode(oboe::SharingMode::Exclusive)
+ ->setPerformanceMode(oboe::PerformanceMode::LowLatency);
+ return builder;
+}
+
+void LiveEffectEngine::startStream(oboe::AudioStream *stream) {
+ assert(stream);
+ if (stream) {
+ oboe::Result result = stream->requestStart();
+ if (result != oboe::Result::OK) {
+ LOGE("Error starting stream. %s", oboe::convertToText(result));
+ }
+ }
+}
+
+void LiveEffectEngine::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 LiveEffectEngine::closeStream(oboe::AudioStream *stream) {
+ if (stream) {
+ oboe::Result result = stream->close();
+ if (result != oboe::Result::OK) {
+ LOGE("Error closing stream. %s", oboe::convertToText(result));
+ }
+ }
+}
+
+/**
+ * Restart the streams. During the restart operation subsequent calls to this
+ * method will output a warning.
+ */
+void LiveEffectEngine::restartStreams() {
+ LOGI("Restarting streams");
+
+ if (mRestartingLock.try_lock()) {
+ closeAllStreams();
+ openAllStreams();
+ mRestartingLock.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 LiveEffectEngine::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 LiveEffectEngine::onAudioReady(
+ oboe::AudioStream *oboeStream, void *audioData, int32_t numFrames) {
+ assert(oboeStream == mPlayStream);
+
+ int32_t prevFrameRead = 0, framesRead = 0;
+ if (mProcessedFrameCount < mSystemStartupFrames) {
+ do {
+ // Drain the audio for the starting up period, half second for
+ // this sample.
+ prevFrameRead = framesRead;
+
+ oboe::ResultWithValue<int32_t> status =
+ mRecordingStream->read(audioData, numFrames, 0);
+ framesRead = (!status) ? 0 : status.value();
+ if (framesRead == 0) break;
+
+ } while (framesRead);
+
+ framesRead = prevFrameRead;
+ } else {
+ oboe::ResultWithValue<int32_t> status =
+ mRecordingStream->read(audioData, numFrames, 0);
+ if (!status) {
+ LOGE("input stream read error: %s",
+ oboe::convertToText(status.error()));
+ return oboe::DataCallbackResult ::Stop;
+ }
+ framesRead = status.value();
+ }
+
+ if (framesRead < numFrames) {
+ int32_t bytesPerFrame = mRecordingStream->getChannelCount() *
+ oboeStream->getBytesPerSample();
+ uint8_t *padPos =
+ static_cast<uint8_t *>(audioData) + framesRead * bytesPerFrame;
+ memset(padPos, 0, (size_t)(numFrames - framesRead) * bytesPerFrame);
+ }
+
+ // add your audio processing here
+
+ mProcessedFrameCount += 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 LiveEffectEngine::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 LiveEffectEngine::onErrorAfterClose(oboe::AudioStream *oboeStream,
+ oboe::Result error) {
+ LOGE("%s stream Error after close: %s",
+ oboe::convertToText(oboeStream->getDirection()),
+ oboe::convertToText(error));
+}
diff --git a/samples/LiveEffect/src/main/cpp/LiveEffectEngine.h b/samples/LiveEffect/src/main/cpp/LiveEffectEngine.h
new file mode 100644
index 0000000..9faff7d
--- /dev/null
+++ b/samples/LiveEffect/src/main/cpp/LiveEffectEngine.h
@@ -0,0 +1,82 @@
+/*
+ * 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.
+ */
+
+#ifndef OBOE_LIVEEFFECTENGINE_H
+#define OBOE_LIVEEFFECTENGINE_H
+
+#include <jni.h>
+#include <oboe/Oboe.h>
+#include <string>
+#include <thread>
+
+constexpr int kMonoChannelCount = 1;
+constexpr int kStereoChannelCount = 2;
+
+class LiveEffectEngine : public oboe::AudioStreamCallback {
+ public:
+ LiveEffectEngine();
+ ~LiveEffectEngine();
+ void setRecordingDeviceId(int32_t deviceId);
+ void setPlaybackDeviceId(int32_t deviceId);
+ void setEffectOn(bool isOn);
+
+ /*
+ * 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);
+
+ bool setAudioApi(oboe::AudioApi);
+ bool isAAudioSupported(void);
+
+ private:
+ bool mIsEffectOn = false;
+ uint64_t mProcessedFrameCount = 0;
+ uint64_t mSystemStartupFrames = 0;
+ int32_t mRecordingDeviceId = oboe::kUnspecified;
+ int32_t mPlaybackDeviceId = oboe::kUnspecified;
+ oboe::AudioFormat mFormat = oboe::AudioFormat::I16;
+ int32_t mSampleRate = oboe::kUnspecified;
+ int32_t mInputChannelCount = kStereoChannelCount;
+ int32_t mOutputChannelCount = kStereoChannelCount;
+ oboe::AudioStream *mRecordingStream = nullptr;
+ oboe::AudioStream *mPlayStream = nullptr;
+ std::mutex mRestartingLock;
+ oboe::AudioApi mAudioApi = oboe::AudioApi::AAudio;
+
+ 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 // OBOE_LIVEEFFECTENGINE_H
diff --git a/samples/LiveEffect/src/main/cpp/jni_bridge.cpp b/samples/LiveEffect/src/main/cpp/jni_bridge.cpp
new file mode 100644
index 0000000..ec5bfae
--- /dev/null
+++ b/samples/LiveEffect/src/main/cpp/jni_bridge.cpp
@@ -0,0 +1,124 @@
+/**
+ * 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 <jni.h>
+#include <logging_macros.h>
+#include "LiveEffectEngine.h"
+
+static const int kOboeApiAAudio = 0;
+static const int kOboeApiOpenSLES = 1;
+
+static LiveEffectEngine *engine = nullptr;
+
+extern "C" {
+
+JNIEXPORT bool JNICALL
+Java_com_google_sample_oboe_liveEffect_LiveEffectEngine_create(JNIEnv *env,
+ jclass) {
+ if (engine == nullptr) {
+ engine = new LiveEffectEngine();
+ }
+
+ return (engine != nullptr);
+}
+
+JNIEXPORT void JNICALL
+Java_com_google_sample_oboe_liveEffect_LiveEffectEngine_delete(JNIEnv *env,
+ jclass) {
+ delete engine;
+ engine = nullptr;
+}
+
+JNIEXPORT void JNICALL
+Java_com_google_sample_oboe_liveEffect_LiveEffectEngine_setEffectOn(
+ JNIEnv *env, jclass, jboolean isEffectOn) {
+ if (engine == nullptr) {
+ LOGE(
+ "Engine is null, you must call createEngine before calling this "
+ "method");
+ return;
+ }
+
+ engine->setEffectOn(isEffectOn);
+}
+
+JNIEXPORT void JNICALL
+Java_com_google_sample_oboe_liveEffect_LiveEffectEngine_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_liveEffect_LiveEffectEngine_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 jboolean JNICALL
+Java_com_google_sample_oboe_liveEffect_LiveEffectEngine_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 kOboeApiAAudio:
+ audioApi = oboe::AudioApi::AAudio;
+ break;
+ case kOboeApiOpenSLES:
+ 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_liveEffect_LiveEffectEngine_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/LiveEffect/src/main/cpp/ndk-stl-config.cmake
similarity index 100%
rename from samples/echo/src/main/cpp/ndk-stl-config.cmake
rename to samples/LiveEffect/src/main/cpp/ndk-stl-config.cmake
diff --git a/samples/echo/src/main/java/com/google/sample/oboe/echo/EchoEngine.java b/samples/LiveEffect/src/main/java/com/google/sample/oboe/liveEffect/LiveEffectEngine.java
similarity index 70%
rename from samples/echo/src/main/java/com/google/sample/oboe/echo/EchoEngine.java
rename to samples/LiveEffect/src/main/java/com/google/sample/oboe/liveEffect/LiveEffectEngine.java
index 58ddbb0..227f5d7 100644
--- a/samples/echo/src/main/java/com/google/sample/oboe/echo/EchoEngine.java
+++ b/samples/LiveEffect/src/main/java/com/google/sample/oboe/liveEffect/LiveEffectEngine.java
@@ -1,6 +1,6 @@
-package com.google.sample.oboe.echo;
+package com.google.sample.oboe.liveEffect;
/*
- * Copyright 2017 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.
@@ -22,24 +22,20 @@
import android.media.AudioRecord;
import android.util.Log;
-public enum EchoEngine {
+public enum LiveEffectEngine {
INSTANCE;
// Load native library
static {
- System.loadLibrary("echo");
+ System.loadLibrary("liveEffect");
}
// 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 setEffectOn(boolean isEffectOn);
static native void setRecordingDeviceId(int deviceId);
static native void setPlaybackDeviceId(int deviceId);
static native void delete();
diff --git a/samples/LiveEffect/src/main/java/com/google/sample/oboe/liveEffect/MainActivity.java b/samples/LiveEffect/src/main/java/com/google/sample/oboe/liveEffect/MainActivity.java
new file mode 100644
index 0000000..136f1b9
--- /dev/null
+++ b/samples/LiveEffect/src/main/java/com/google/sample/oboe/liveEffect/MainActivity.java
@@ -0,0 +1,244 @@
+/*
+ * 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.
+ */
+
+package com.google.sample.oboe.liveEffect;
+
+import android.Manifest;
+import android.app.Activity;
+import android.content.pm.PackageManager;
+import android.media.AudioManager;
+import android.os.Build;
+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.TextView;
+import android.widget.Toast;
+
+import com.google.sample.audio_device.AudioDeviceListEntry;
+import com.google.sample.audio_device.AudioDeviceSpinner;
+
+/**
+ * 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_EFFECT_REQUEST = 0;
+ private static final int OBOE_API_AAUDIO = 0;
+ private static final int OBOE_API_OPENSL_ES=1;
+
+ private TextView statusText;
+ private Button toggleEffectButton;
+ private AudioDeviceSpinner recordingDeviceSpinner;
+ private AudioDeviceSpinner playbackDeviceSpinner;
+ private boolean isPlaying = false;
+
+ private int apiSelection = OBOE_API_AAUDIO;
+ private boolean aaudioSupported = true;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_main);
+
+ statusText = findViewById(R.id.status_view_text);
+ toggleEffectButton = findViewById(R.id.button_toggle_effect);
+ toggleEffectButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ toggleEffect();
+ }
+ });
+ toggleEffectButton.setText(getString(R.string.start_effect));
+
+ recordingDeviceSpinner = findViewById(R.id.recording_devices_spinner);
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ recordingDeviceSpinner.setDirectionType(AudioManager.GET_DEVICES_INPUTS);
+ recordingDeviceSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
+ @Override
+ public void onItemSelected(AdapterView<?> adapterView, View view, int i, long l) {
+ LiveEffectEngine.setRecordingDeviceId(getRecordingDeviceId());
+ }
+
+ @Override
+ public void onNothingSelected(AdapterView<?> adapterView) {
+ // Do nothing
+ }
+ });
+ }
+
+ playbackDeviceSpinner = findViewById(R.id.playback_devices_spinner);
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ playbackDeviceSpinner.setDirectionType(AudioManager.GET_DEVICES_OUTPUTS);
+ playbackDeviceSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
+ @Override
+ public void onItemSelected(AdapterView<?> adapterView, View view, int i, long l) {
+ LiveEffectEngine.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;
+ }
+ }
+ });
+
+ LiveEffectEngine.create();
+ aaudioSupported = LiveEffectEngine.isAAudioSupported();
+ EnableAudioApiUI(true);
+ LiveEffectEngine.setAPI(apiSelection);
+ }
+
+ 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() {
+ LiveEffectEngine.delete();
+ super.onDestroy();
+ }
+
+ public void toggleEffect() {
+ if (isPlaying) {
+ stopEffect();
+ EnableAudioApiUI(true);
+ } else {
+ EnableAudioApiUI(false);
+ LiveEffectEngine.setAPI(apiSelection);
+ startEffect();
+ }
+ }
+
+ private void startEffect() {
+ Log.d(TAG, "Attempting to start");
+
+ if (!isRecordPermissionGranted()){
+ requestRecordPermission();
+ return;
+ }
+
+ setSpinnersEnabled(false);
+ LiveEffectEngine.setEffectOn(true);
+ statusText.setText(R.string.status_playing);
+ toggleEffectButton.setText(R.string.stop_effect);
+ isPlaying = true;
+ }
+
+ private void stopEffect() {
+ Log.d(TAG, "Playing, attempting to stop");
+ LiveEffectEngine.setEffectOn(false);
+ resetStatusView();
+ toggleEffectButton.setText(R.string.start_effect);
+ 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_EFFECT_REQUEST);
+ }
+ private void resetStatusView() {
+ statusText.setText(R.string.status_warning);
+ }
+
+ @Override
+ public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
+ @NonNull int[] grantResults) {
+
+ if (AUDIO_EFFECT_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 live effect
+ toggleEffect();
+ }
+ }
+}
diff --git a/samples/echo/src/main/res/drawable/balance_seekbar.xml b/samples/LiveEffect/src/main/res/drawable/balance_seekbar.xml
similarity index 100%
rename from samples/echo/src/main/res/drawable/balance_seekbar.xml
rename to samples/LiveEffect/src/main/res/drawable/balance_seekbar.xml
diff --git a/samples/LiveEffect/src/main/res/layout-v21/activity_main.xml b/samples/LiveEffect/src/main/res/layout-v21/activity_main.xml
new file mode 100644
index 0000000..4ecd2e3
--- /dev/null
+++ b/samples/LiveEffect/src/main/res/layout-v21/activity_main.xml
@@ -0,0 +1,129 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 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.
+ -->
+<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.liveEffect.MainActivity"
+ tools:layout_editor_absoluteY="81dp">
+
+ <TextView
+ android:id="@+id/recDeviceLabel"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="@dimen/activity_horizontal_margin"
+ android:layout_marginLeft="@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_marginLeft="@dimen/activity_horizontal_margin"
+ android:layout_marginTop="0dp"
+ app:layout_constraintLeft_toLeftOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/recDeviceLabel" />
+
+ <TextView
+ android:id="@+id/playDeviceLabel"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="@dimen/activity_horizontal_margin"
+ android:layout_marginLeft="@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_marginLeft="@dimen/activity_horizontal_margin"
+ android:layout_marginTop="0dp"
+ app:layout_constraintLeft_toLeftOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/playDeviceLabel" />
+
+ <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_marginLeft="@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:layout_marginLeft="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:layout_marginLeft="16dp"
+ android:text="@string/sles" />
+ </RadioGroup>
+
+ <Button
+ android:id="@+id/button_toggle_effect"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="72dp"
+ android:gravity="center"
+ android:text="@string/start_effect"
+ android:textAllCaps="false"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintHorizontal_bias="0.53"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/apiSelectionGroup" />
+
+ <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/button_toggle_effect"
+ app:layout_constraintBottom_toBottomOf="parent" />
+
+</android.support.constraint.ConstraintLayout>
diff --git a/samples/LiveEffect/src/main/res/layout/activity_main.xml b/samples/LiveEffect/src/main/res/layout/activity_main.xml
new file mode 100644
index 0000000..e7a9e32
--- /dev/null
+++ b/samples/LiveEffect/src/main/res/layout/activity_main.xml
@@ -0,0 +1,128 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 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.
+ -->
+<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.liveEffect.MainActivity"
+ tools:layout_editor_absoluteY="81dp">
+
+ <TextView
+ android:id="@+id/recDeviceLabel"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="@dimen/activity_horizontal_margin"
+ android:layout_marginLeft="@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_marginLeft="@dimen/activity_horizontal_margin"
+ android:layout_marginTop="0dp"
+ app:layout_constraintLeft_toLeftOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/recDeviceLabel" />
+
+ <TextView
+ android:id="@+id/playDeviceLabel"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="@dimen/activity_horizontal_margin"
+ android:layout_marginLeft="@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_marginLeft="@dimen/activity_horizontal_margin"
+ android:layout_marginTop="0dp"
+ app:layout_constraintLeft_toLeftOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/playDeviceLabel" />
+
+ <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_marginLeft="@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:layout_marginLeft="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:layout_marginLeft="16dp"
+ android:text="@string/sles" />
+ </RadioGroup>
+
+ <Button
+ android:id="@+id/button_toggle_effect"
+ 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_effect"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/apiSelectionGroup" />
+
+ <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/button_toggle_effect"
+ app:layout_constraintBottom_toBottomOf="parent" />
+
+</android.support.constraint.ConstraintLayout>
diff --git a/samples/echo/src/main/res/mipmap-hdpi/ic_launcher.png b/samples/LiveEffect/src/main/res/mipmap-hdpi/ic_launcher.png
similarity index 100%
rename from samples/echo/src/main/res/mipmap-hdpi/ic_launcher.png
rename to samples/LiveEffect/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/LiveEffect/src/main/res/mipmap-mdpi/ic_launcher.png
similarity index 100%
rename from samples/echo/src/main/res/mipmap-mdpi/ic_launcher.png
rename to samples/LiveEffect/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/LiveEffect/src/main/res/mipmap-xhdpi/ic_launcher.png
similarity index 100%
rename from samples/echo/src/main/res/mipmap-xhdpi/ic_launcher.png
rename to samples/LiveEffect/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/LiveEffect/src/main/res/mipmap-xxhdpi/ic_launcher.png
similarity index 100%
rename from samples/echo/src/main/res/mipmap-xxhdpi/ic_launcher.png
rename to samples/LiveEffect/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/LiveEffect/src/main/res/values-v21/styles.xml
similarity index 100%
rename from samples/echo/src/main/res/values-v21/styles.xml
rename to samples/LiveEffect/src/main/res/values-v21/styles.xml
diff --git a/samples/echo/src/main/res/values-w820dp/dimens.xml b/samples/LiveEffect/src/main/res/values-w820dp/dimens.xml
similarity index 100%
rename from samples/echo/src/main/res/values-w820dp/dimens.xml
rename to samples/LiveEffect/src/main/res/values-w820dp/dimens.xml
diff --git a/samples/echo/src/main/res/values/colors.xml b/samples/LiveEffect/src/main/res/values/colors.xml
similarity index 100%
rename from samples/echo/src/main/res/values/colors.xml
rename to samples/LiveEffect/src/main/res/values/colors.xml
diff --git a/samples/echo/src/main/res/values/dimens.xml b/samples/LiveEffect/src/main/res/values/dimens.xml
similarity index 100%
rename from samples/echo/src/main/res/values/dimens.xml
rename to samples/LiveEffect/src/main/res/values/dimens.xml
diff --git a/samples/echo/src/main/res/values/strings.xml b/samples/LiveEffect/src/main/res/values/strings.xml
similarity index 62%
rename from samples/echo/src/main/res/values/strings.xml
rename to samples/LiveEffect/src/main/res/values/strings.xml
index 9025491..a1a97b6 100644
--- a/samples/echo/src/main/res/values/strings.xml
+++ b/samples/LiveEffect/src/main/res/values/strings.xml
@@ -1,10 +1,10 @@
<resources>
- <string name="app_name">Oboe Echo</string>
+ <string name="app_name">LiveEffect</string>
<string name="action_settings">Settings</string>
- <string name="start_echo">Start</string>
- <string name="stop_echo">Stop</string>
+ <string name="start_effect">Start</string>
+ <string name="stop_effect">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_playing">Engine Playing ....</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
@@ -15,10 +15,4 @@
<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/LiveEffect/src/main/res/values/styles.xml
similarity index 100%
rename from samples/echo/src/main/res/values/styles.xml
rename to samples/LiveEffect/src/main/res/values/styles.xml
diff --git a/samples/README.md b/samples/README.md
index 20431c5..7b1706d 100644
--- a/samples/README.md
+++ b/samples/README.md
@@ -6,6 +6,7 @@
sine wave when you tap the screen
1. [RhythmGame](RhythmGame): A simple rhythm game where you copy the clap patterns you hear by tapping on the screen
1. [MegaDrone](MegaDrone): A one hundred oscillator synthesizer, demonstrates low latency and CPU performance
+1. [LiveEffect](LiveEffect): loops audio from input stream to output stream to demonstrate duplex capability
Pre-requisites
-------------
diff --git a/samples/common/audio_common.cpp b/samples/common/audio_common.cpp
deleted file mode 100644
index 3d296af..0000000
--- a/samples/common/audio_common.cpp
+++ /dev/null
@@ -1,73 +0,0 @@
-/*
- * 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 "audio_common.h"
-#include <logging_macros.h>
-#include <string>
-#include <cinttypes>
-
-uint16_t SampleFormatToBpp(oboe::AudioFormat format) {
- switch (format) {
- case oboe::AudioFormat::I16:
- return 16;
- case oboe::AudioFormat::Float:
- return 32;
- default:
- return 0;
- }
-}
-
-void PrintAudioStreamInfo(const oboe::AudioStream *stream) {
- LOGI("StreamID: %p", stream);
- oboe::Direction dir = stream->getDirection();
- LOGI("Direction: %s", oboe::convertToText(dir));
- LOGI("API type: %s", oboe::convertToText(stream->getAudioApi()));
- LOGI("BufferCapacity: %d", stream->getBufferCapacityInFrames());
- LOGI("BufferSize: %d", stream->getBufferSizeInFrames());
- LOGI("FramesPerBurst: %d", stream->getFramesPerBurst());
- LOGI("XRunCount: %d", stream->getXRunCount().value());
- LOGI("SampleRate: %d", stream->getSampleRate());
- LOGI("SamplesPerFrame: %d", stream->getChannelCount());
- LOGI("DeviceId: %d", stream->getDeviceId());
- LOGI("Format: %s", oboe::convertToText(stream->getFormat()));
- LOGI("SharingMode: %s", oboe::convertToText(stream->getSharingMode()));
- LOGI("PerformanceMode: %s", oboe::convertToText(stream->getPerformanceMode()));
-
- if (dir == oboe::Direction ::Output) {
- LOGI("FramesReadByDevice: %" PRIx64, stream->getFramesRead());
- LOGI("FramesWriteByApp: %d", (int32_t)stream->getFramesWritten());
- } else {
- LOGI("FramesReadByApp: %d", (int32_t)stream->getFramesRead());
- LOGI("FramesWriteByDevice: %d", (int32_t)stream->getFramesWritten());
- }
-}
-
-int64_t timestamp_to_nanoseconds(timespec ts) {
- return (ts.tv_sec * oboe::kNanosPerSecond) + 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
deleted file mode 100644
index 83d3fda..0000000
--- a/samples/common/audio_common.h
+++ /dev/null
@@ -1,53 +0,0 @@
-/*
- * 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.
- */
-
-
-#ifndef AUDIO_COMMON_H
-#define AUDIO_COMMON_H
-
-#include <chrono>
-#include <oboe/Oboe.h>
-
-
-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>(oboe::kNanosPerMillisecond) * 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/proguard-rules.pro b/samples/echo/proguard-rules.pro
deleted file mode 100644
index 7dc6c7f..0000000
--- a/samples/echo/proguard-rules.pro
+++ /dev/null
@@ -1,17 +0,0 @@
-# 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
deleted file mode 100644
index 411af67..0000000
--- a/samples/echo/screenshot.png
+++ /dev/null
Binary files differ
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
deleted file mode 100644
index c63b397..0000000
--- a/samples/echo/src/main/assets/sample_s16_pcm_dc_48khz.raw
+++ /dev/null
Binary files differ
diff --git a/samples/echo/src/main/cpp/AudioEffect.cpp b/samples/echo/src/main/cpp/AudioEffect.cpp
deleted file mode 100644
index 3080237..0000000
--- a/samples/echo/src/main/cpp/AudioEffect.cpp
+++ /dev/null
@@ -1,308 +0,0 @@
-/*
- * 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>
-
-#define CLAMP_FOR_I16(x) ((x)>SHRT_MAX ? SHRT_MAX : \
- ((x)<SHRT_MIN ? SHRT_MIN : (x)))
-
-/*
- * 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) {
-
- 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;
- if (bgMixFactor_ < 0.05f) {
- bgMixFactor_ = 0.0f;
- bgMixFactorInt_ = 0;
- } else {
- 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)
- */
-bool AudioMixer::addStream(std::unique_ptr<int16_t[]>samples, size_t sampleCount,
- int32_t sampleRate, int32_t channelCount, oboe::AudioFormat format) {
- // Wait for lock, from user context.
- std::lock_guard<std::mutex> lock(lock_);
- bgAudio_ = std::move(samples);
- bgAudioSampleCount_ = sampleCount;
- sampleRate_ = sampleRate;
- format_ = format;
- channelCount_ = channelCount;
-
- curPosition_ = 0;
-
- return true;
-}
-
-/**
- * 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) {
- if(!bgAudio_ || !liveAudio) {
- return;
- }
-
- if ((numFrames * channelCount) > bgAudioSampleCount_ ||
- channelCount != channelCount_ ||
- bgMixFactorInt_ == 0) {
- return;
- }
-
- if (!lock_.try_lock()) {
- // UI thread still updating the stream, skip blending
- return;
- }
-
- size_t sampleCount = numFrames * channelCount;
- int32_t curSample;
- for (int i = 0; i < sampleCount ; i++) {
- curSample = liveAudio[i];
- curSample = curSample * fgMixFactorInt_ +
- bgAudio_[curPosition_] * bgMixFactorInt_;
- curSample /= kFloatToIntMapFactor;
-
- curSample = CLAMP_FOR_I16(curSample);
- liveAudio[i] = (int16_t)curSample;
- curPosition_ = (curPosition_ + 1 ) % bgAudioSampleCount_;
- }
-
- lock_.unlock();
-}
-
-/**
- * query for audio format supportability
- */
-bool AudioMixer::AudioFormatSupported(int32_t sampleRate,
- int32_t channels, oboe::AudioFormat format) {
- if (sampleRate != sampleRate_ || format != format_) {
- return false;
- }
-
- if (channels == channelCount_ ) {
- return true;
- }
-
- if(channelCount_ == channels * 2) {
- size_t dst = 0, src = 0;
- size_t totalFrames = bgAudioSampleCount_ / channelCount_;
- for(size_t frame = 0; frame < totalFrames; frame++) {
- for (int32_t c = 0; c < channelCount_; c += 2) {
- int32_t sample = bgAudio_[src] + bgAudio_[src + 1];
- src += 2;
- sample /= 2;
- bgAudio_[dst++] = static_cast<int16_t>(CLAMP_FOR_I16(sample));
- }
- }
- channelCount_ >>= 1;
- bgAudioSampleCount_ >>= 1;
- return true;
- }
-
- return false;
-}
-
-/**
- * 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() {
- delete buffer_;
-}
-
-/**
- * Configure for delay time ( in second ). 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_);
-
- delete (buffer_);
-
- 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_);
-
- if (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 (!buffer_ || !liveAudio ||
- 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 = & reinterpret_cast<int16_t*>(buffer_)[curPos_ * channelCount_];
- for (size_t idx = 0; idx < sampleCount; idx++) {
- int32_t curSample = (samples[idx] * feedbackFactor_ +
- liveAudio[idx] * liveAudioFactor_) / kFloatToIntMapFactor;
- CLAMP_FOR_I16(curSample);
- liveAudio[idx] = samples[idx];
- samples[idx] = static_cast<int16_t>(curSample);
- }
-
- curPos_ += numFrames;
- lock_.unlock();
-}
-
diff --git a/samples/echo/src/main/cpp/AudioEffect.h b/samples/echo/src/main/cpp/AudioEffect.h
deleted file mode 100644
index 8ca5769..0000000
--- a/samples/echo/src/main/cpp/AudioEffect.h
+++ /dev/null
@@ -1,106 +0,0 @@
-/*
- * 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.
- */
-
-#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);
- bool 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);
- private:
- std::unique_ptr<int16_t[]> bgAudio_;
- size_t bgAudioSampleCount_ = 0;
- size_t curPosition_ = 0;
- float bgMixFactor_ = 0.5f;
- int32_t fgMixFactorInt_;
- int32_t bgMixFactorInt_;
- std::mutex lock_;
-};
-
-/**
- * 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;
- uint8_t *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/EchoAudioEngine.cpp b/samples/echo/src/main/cpp/EchoAudioEngine.cpp
deleted file mode 100644
index d2775b2..0000000
--- a/samples/echo/src/main/cpp/EchoAudioEngine.cpp
+++ /dev/null
@@ -1,420 +0,0 @@
-/**
- * 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 "EchoAudioEngine.h"
-#include <audio_common.h>
-#include <logging_macros.h>
-#include <climits>
-#include <assert.h>
-
-/**
- * Duplex is not very stable right after starting up:
- * the callbacks may not be happening at the right times
- * The time to get it stable varies on different systems. Half second
- * is used for this sample, during the time this sample plays silence.
- */
-const float kSystemWarmupTime = 0.5f;
-
-EchoAudioEngine::EchoAudioEngine() {
- assert(outputChannelCount_ == inputChannelCount_);
- mixerEffect_ = std::unique_ptr<AudioMixer>(new AudioMixer);
-}
-
-EchoAudioEngine::~EchoAudioEngine() {
- stopStream(playStream_);
- stopStream(recordingStream_);
-
- closeStream(playStream_);
-
- 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());
-
- systemStartupFrames_ = static_cast<uint64_t>
- (sampleRate_ * kSystemWarmupTime);
- processedFrameCount_ = 0;
-
- framesPerBurst_ = playStream_->getFramesPerBurst();
-
- delayEffect_ = std::unique_ptr<AudioDelay>(new AudioDelay(
- sampleRate_,outputChannelCount_, format_, echoDelay_, echoDecay_));
- assert(delayEffect_ && mixerEffect_);
-
- 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_)
- ->setFramesPerCallback(framesPerBurst_);
-
- 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_);
-
- int32_t prevFrameRead = 0, framesRead = 0;
- if (processedFrameCount_ < systemStartupFrames_) {
- do {
- // Drain the audio for the starting up period, half second for
- // this sample.
- prevFrameRead = framesRead;
-
- oboe::ResultWithValue<int32_t> status =
- recordingStream_->read(audioData, numFrames, 0);
- framesRead = (!status) ? 0 : status.value();
- if (framesRead == 0)
- break;
-
- } while (framesRead);
-
- framesRead = prevFrameRead;
- } else {
- oboe::ResultWithValue<int32_t> status =
- recordingStream_->read(audioData, numFrames, 0);
-
- 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);
- }
-
- // Processing audio: padded silence audio treated as valid audio
- // glitch would be felt by turning off mixer
- delayEffect_->process(static_cast<int16_t *>(audioData),
- outputChannelCount_, numFrames);
- if (mixAudio_) {
- mixerEffect_->process(static_cast<int16_t *>(audioData),
- outputChannelCount_, numFrames);
- }
- processedFrameCount_ += 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
deleted file mode 100644
index 6a7a628..0000000
--- a/samples/echo/src/main/cpp/EchoAudioEngine.h
+++ /dev/null
@@ -1,96 +0,0 @@
-/*
- * 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.
- */
-
-#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 processedFrameCount_ = 0;
- uint64_t systemStartupFrames_ = 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_;
- std::unique_ptr<AudioDelay> delayEffect_;
- oboe::AudioApi audioApi_ = oboe::AudioApi::AAudio;
- float echoDelay_ = 0.5f;
- float echoDecay_ = 0.1f;
-
- bool mixAudio_ = false;
-
- 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
deleted file mode 100644
index 742b381..0000000
--- a/samples/echo/src/main/cpp/jni_bridge.cpp
+++ /dev/null
@@ -1,175 +0,0 @@
-/**
- * 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 <jni.h>
-#include <logging_macros.h>
-#include "EchoAudioEngine.h"
-#include <android/asset_manager.h>
-#include <android/asset_manager_jni.h>
-
-static EchoAudioEngine *engine = nullptr;
-
-static const int OBOE_API_AAUDIO = 0;
-static const int OBOE_API_OPENSL_ES = 1;
-
-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);
-}
-
-
-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/java/com/google/sample/oboe/echo/MainActivity.java b/samples/echo/src/main/java/com/google/sample/oboe/echo/MainActivity.java
deleted file mode 100644
index 09ab432..0000000
--- a/samples/echo/src/main/java/com/google/sample/oboe/echo/MainActivity.java
+++ /dev/null
@@ -1,342 +0,0 @@
-/*
- * 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/layout/activity_main.xml b/samples/echo/src/main/res/layout/activity_main.xml
deleted file mode 100644
index de5b51b..0000000
--- a/samples/echo/src/main/res/layout/activity_main.xml
+++ /dev/null
@@ -1,232 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- ~ Copyright (C) 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.
- -->
-<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/settings.gradle b/samples/settings.gradle
index 647caee..2024e54 100644
--- a/samples/settings.gradle
+++ b/samples/settings.gradle
@@ -19,5 +19,5 @@
include ':hello-oboe'
include ':RhythmGame'
include ':MegaDrone'
-include 'echo'
+include ':LiveEffect'