Move dead submodules in-tree

Signed-off-by: swurl <swurl@swurl.xyz>
This commit is contained in:
swurl 2025-05-31 02:33:02 -04:00
parent c0cceff365
commit 6c655321e6
No known key found for this signature in database
GPG key ID: A5A7629F109C8FD1
4081 changed files with 1185566 additions and 45 deletions

View file

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-feature android:name="android.hardware.microphone" android:required="true" />
<uses-feature android:name="android.hardware.audio.output" android:required="true" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" />
<application
android:allowBackup="false"
android:fullBackupContent="false"
android:supportsRtl="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme" >
<activity
android:name="com.google.oboe.samples.liveEffect.MainActivity"
android:label="@string/app_name"
android:screenOrientation="portrait"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<service
android:name=".DuplexStreamForegroundService"
android:foregroundServiceType="mediaPlayback|microphone"
android:exported="false">
</service>
</application>
</manifest>

View file

@ -0,0 +1,46 @@
#
# Copyright 2018 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
cmake_minimum_required(VERSION 3.4.1)
project(liveEffect LANGUAGES C CXX)
get_filename_component(SAMPLE_ROOT_DIR
${CMAKE_CURRENT_SOURCE_DIR}/../../../.. ABSOLUTE)
### INCLUDE OBOE LIBRARY ###
set (OBOE_DIR ${SAMPLE_ROOT_DIR}/..)
add_subdirectory(${OBOE_DIR} ./oboe-bin)
add_library(liveEffect
SHARED
LiveEffectEngine.cpp
jni_bridge.cpp
${SAMPLE_ROOT_DIR}/debug-utils/trace.cpp)
target_include_directories(liveEffect
PRIVATE
${SAMPLE_ROOT_DIR}/debug-utils
${OBOE_DIR}/include)
target_link_libraries(liveEffect
PRIVATE
oboe
android
atomic
log)
target_link_options(liveEffect PRIVATE "-Wl,-z,max-page-size=16384")
# Enable optimization flags: if having problems with source level debugging,
# disable -Ofast ( and debug ), re-enable it after done debugging.
target_compile_options(liveEffect PRIVATE -Wall -Werror "$<$<CONFIG:RELEASE>:-Ofast>")

View file

@ -0,0 +1,54 @@
/*
* 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 SAMPLES_FULLDUPLEXPASS_H
#define SAMPLES_FULLDUPLEXPASS_H
class FullDuplexPass : public oboe::FullDuplexStream {
public:
virtual oboe::DataCallbackResult
onBothStreamsReady(
const void *inputData,
int numInputFrames,
void *outputData,
int numOutputFrames) {
// Copy the input samples to the output with a little arbitrary gain change.
// This code assumes the data format for both streams is Float.
const float *inputFloats = static_cast<const float *>(inputData);
float *outputFloats = static_cast<float *>(outputData);
// It also assumes the channel count for each stream is the same.
int32_t samplesPerFrame = getOutputStream()->getChannelCount();
int32_t numInputSamples = numInputFrames * samplesPerFrame;
int32_t numOutputSamples = numOutputFrames * samplesPerFrame;
// It is possible that there may be fewer input than output samples.
int32_t samplesToProcess = std::min(numInputSamples, numOutputSamples);
for (int32_t i = 0; i < samplesToProcess; i++) {
*outputFloats++ = *inputFloats++ * 0.95; // do some arbitrary processing
}
// If there are fewer input samples then clear the rest of the buffer.
int32_t samplesLeft = numOutputSamples - numInputSamples;
for (int32_t i = 0; i < samplesLeft; i++) {
*outputFloats++ = 0.0; // silence
}
return oboe::DataCallbackResult::Continue;
}
};
#endif //SAMPLES_FULLDUPLEXPASS_H

View file

@ -0,0 +1,245 @@
/**
* 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 <cassert>
#include <logging_macros.h>
#include "LiveEffectEngine.h"
LiveEffectEngine::LiveEffectEngine() {
assert(mOutputChannelCount == mInputChannelCount);
}
void LiveEffectEngine::setRecordingDeviceId(int32_t deviceId) {
mRecordingDeviceId = deviceId;
}
void LiveEffectEngine::setPlaybackDeviceId(int32_t deviceId) {
mPlaybackDeviceId = deviceId;
}
bool LiveEffectEngine::isAAudioRecommended() {
return oboe::AudioStreamBuilder::isAAudioRecommended();
}
bool LiveEffectEngine::setAudioApi(oboe::AudioApi api) {
if (mIsEffectOn) return false;
mAudioApi = api;
return true;
}
bool LiveEffectEngine::setEffectOn(bool isOn) {
bool success = true;
if (isOn != mIsEffectOn) {
if (isOn) {
success = openStreams() == oboe::Result::OK;
if (success) {
mIsEffectOn = isOn;
}
} else {
closeStreams();
mIsEffectOn = isOn;
}
}
return success;
}
void LiveEffectEngine::closeStreams() {
/*
* 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.
*/
mDuplexStream->stop();
closeStream(mPlayStream);
closeStream(mRecordingStream);
mDuplexStream.reset();
}
oboe::Result LiveEffectEngine::openStreams() {
// 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
oboe::AudioStreamBuilder inBuilder, outBuilder;
setupPlaybackStreamParameters(&outBuilder);
oboe::Result result = outBuilder.openStream(mPlayStream);
if (result != oboe::Result::OK) {
LOGE("Failed to open output stream. Error %s", oboe::convertToText(result));
mSampleRate = oboe::kUnspecified;
return result;
} else {
// The input stream needs to run at the same sample rate as the output.
mSampleRate = mPlayStream->getSampleRate();
}
warnIfNotLowLatency(mPlayStream);
setupRecordingStreamParameters(&inBuilder, mSampleRate);
result = inBuilder.openStream(mRecordingStream);
if (result != oboe::Result::OK) {
LOGE("Failed to open input stream. Error %s", oboe::convertToText(result));
closeStream(mPlayStream);
return result;
}
warnIfNotLowLatency(mRecordingStream);
mDuplexStream = std::make_unique<FullDuplexPass>();
mDuplexStream->setSharedInputStream(mRecordingStream);
mDuplexStream->setSharedOutputStream(mPlayStream);
mDuplexStream->start();
return 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
* @param sampleRate The desired sample rate of the recording stream
*/
oboe::AudioStreamBuilder *LiveEffectEngine::setupRecordingStreamParameters(
oboe::AudioStreamBuilder *builder, int32_t sampleRate) {
// This sample uses blocking read() because we don't specify a callback
builder->setDeviceId(mRecordingDeviceId)
->setDirection(oboe::Direction::Input)
->setSampleRate(sampleRate)
->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->setDataCallback(this)
->setErrorCallback(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)
->setFormatConversionAllowed(true)
->setSharingMode(oboe::SharingMode::Exclusive)
->setPerformanceMode(oboe::PerformanceMode::LowLatency);
return builder;
}
/**
* 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(std::shared_ptr<oboe::AudioStream> &stream) {
if (stream) {
oboe::Result result = stream->stop();
if (result != oboe::Result::OK) {
LOGW("Error stopping stream: %s", oboe::convertToText(result));
}
result = stream->close();
if (result != oboe::Result::OK) {
LOGE("Error closing stream: %s", oboe::convertToText(result));
} else {
LOGW("Successfully closed streams");
}
stream.reset();
}
}
/**
* Warn in logcat if non-low latency stream is created
* @param stream: newly created stream
*
*/
void LiveEffectEngine::warnIfNotLowLatency(std::shared_ptr<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) {
return mDuplexStream->onAudioReady(oboeStream, audioData, numFrames);
}
/**
* 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));
closeStreams();
// Restart the stream if the error is a disconnect.
if (error == oboe::Result::ErrorDisconnected) {
LOGI("Restarting AudioStream");
openStreams();
}
}

View file

@ -0,0 +1,83 @@
/*
* 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>
#include "FullDuplexPass.h"
class LiveEffectEngine : public oboe::AudioStreamCallback {
public:
LiveEffectEngine();
void setRecordingDeviceId(int32_t deviceId);
void setPlaybackDeviceId(int32_t deviceId);
/**
* @param isOn
* @return true if it succeeds
*/
bool setEffectOn(bool isOn);
/*
* oboe::AudioStreamDataCallback interface implementation
*/
oboe::DataCallbackResult onAudioReady(oboe::AudioStream *oboeStream,
void *audioData, int32_t numFrames) override;
/*
* oboe::AudioStreamErrorCallback interface implementation
*/
void onErrorBeforeClose(oboe::AudioStream *oboeStream, oboe::Result error) override;
void onErrorAfterClose(oboe::AudioStream *oboeStream, oboe::Result error) override;
bool setAudioApi(oboe::AudioApi);
bool isAAudioRecommended(void);
private:
bool mIsEffectOn = false;
int32_t mRecordingDeviceId = oboe::kUnspecified;
int32_t mPlaybackDeviceId = oboe::kUnspecified;
const oboe::AudioFormat mFormat = oboe::AudioFormat::Float; // for easier processing
oboe::AudioApi mAudioApi = oboe::AudioApi::AAudio;
int32_t mSampleRate = oboe::kUnspecified;
const int32_t mInputChannelCount = oboe::ChannelCount::Stereo;
const int32_t mOutputChannelCount = oboe::ChannelCount::Stereo;
std::unique_ptr<FullDuplexPass> mDuplexStream;
std::shared_ptr<oboe::AudioStream> mRecordingStream;
std::shared_ptr<oboe::AudioStream> mPlayStream;
oboe::Result openStreams();
void closeStreams();
void closeStream(std::shared_ptr<oboe::AudioStream> &stream);
oboe::AudioStreamBuilder *setupCommonStreamParameters(
oboe::AudioStreamBuilder *builder);
oboe::AudioStreamBuilder *setupRecordingStreamParameters(
oboe::AudioStreamBuilder *builder, int32_t sampleRate);
oboe::AudioStreamBuilder *setupPlaybackStreamParameters(
oboe::AudioStreamBuilder *builder);
void warnIfNotLowLatency(std::shared_ptr<oboe::AudioStream> &stream);
};
#endif // OBOE_LIVEEFFECTENGINE_H

View file

@ -0,0 +1,134 @@
/**
* 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 jboolean JNICALL
Java_com_google_oboe_samples_liveEffect_LiveEffectEngine_create(JNIEnv *env,
jclass) {
if (engine == nullptr) {
engine = new LiveEffectEngine();
}
return (engine != nullptr) ? JNI_TRUE : JNI_FALSE;
}
JNIEXPORT void JNICALL
Java_com_google_oboe_samples_liveEffect_LiveEffectEngine_delete(JNIEnv *env,
jclass) {
if (engine) {
engine->setEffectOn(false);
delete engine;
engine = nullptr;
}
}
JNIEXPORT jboolean JNICALL
Java_com_google_oboe_samples_liveEffect_LiveEffectEngine_setEffectOn(
JNIEnv *env, jclass, jboolean isEffectOn) {
if (engine == nullptr) {
LOGE(
"Engine is null, you must call createEngine before calling this "
"method");
return JNI_FALSE;
}
return engine->setEffectOn(isEffectOn) ? JNI_TRUE : JNI_FALSE;
}
JNIEXPORT void JNICALL
Java_com_google_oboe_samples_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_oboe_samples_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_oboe_samples_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 engine->setAudioApi(audioApi) ? JNI_TRUE : JNI_FALSE;
}
JNIEXPORT jboolean JNICALL
Java_com_google_oboe_samples_liveEffect_LiveEffectEngine_isAAudioRecommended(
JNIEnv *env, jclass type) {
if (engine == nullptr) {
LOGE(
"Engine is null, you must call createEngine "
"before calling this method");
return JNI_FALSE;
}
return engine->isAAudioRecommended() ? JNI_TRUE : JNI_FALSE;
}
JNIEXPORT void JNICALL
Java_com_google_oboe_samples_liveEffect_LiveEffectEngine_native_1setDefaultStreamValues(JNIEnv *env,
jclass type,
jint sampleRate,
jint framesPerBurst) {
oboe::DefaultStreamValues::SampleRate = (int32_t) sampleRate;
oboe::DefaultStreamValues::FramesPerBurst = (int32_t) framesPerBurst;
}
} // extern "C"

View file

@ -0,0 +1,40 @@
# Copy shared STL files to Android Studio output directory so they can be
# packaged in the APK.
# Usage:
#
# find_package(ndk-stl REQUIRED)
#
# or
#
# find_package(ndk-stl REQUIRED PATHS ".")
if(NOT ${ANDROID_STL} MATCHES "_shared")
return()
endif()
function(configure_shared_stl lib_path so_base)
message("Configuring STL ${so_base} for ${ANDROID_ABI}")
configure_file(
"${ANDROID_NDK}/sources/cxx-stl/${lib_path}/libs/${ANDROID_ABI}/lib${so_base}.so"
"${CMAKE_LIBRARY_OUTPUT_DIRECTORY}/lib${so_base}.so"
COPYONLY)
endfunction()
if("${ANDROID_STL}" STREQUAL "libstdc++")
# The default minimal system C++ runtime library.
elseif("${ANDROID_STL}" STREQUAL "gabi++_shared")
# The GAbi++ runtime (shared).
message(FATAL_ERROR "gabi++_shared was not configured by ndk-stl package")
elseif("${ANDROID_STL}" STREQUAL "stlport_shared")
# The STLport runtime (shared).
configure_shared_stl("stlport" "stlport_shared")
elseif("${ANDROID_STL}" STREQUAL "gnustl_shared")
# The GNU STL (shared).
configure_shared_stl("gnu-libstdc++/4.9" "gnustl_shared")
elseif("${ANDROID_STL}" STREQUAL "c++_shared")
# The LLVM libc++ runtime (shared).
configure_shared_stl("llvm-libc++" "c++_shared")
else()
message(FATAL_ERROR "STL configuration ANDROID_STL=${ANDROID_STL} is not supported")
endif()

View file

@ -0,0 +1,92 @@
/*
* Copyright 2024 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.oboe.samples.liveEffect;
import android.app.ForegroundServiceStartNotAllowedException;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ServiceInfo;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.IBinder;
import android.os.Looper;
import android.os.Message;
import android.util.Log;
import android.widget.Toast;
import androidx.core.app.NotificationCompat;
import androidx.core.app.ServiceCompat;
import androidx.core.content.ContextCompat;
public class DuplexStreamForegroundService extends Service {
private static final String TAG = "DuplexStreamFS";
public static final String ACTION_START = "ACTION_START";
public static final String ACTION_STOP = "ACTION_STOP";
@Override
public IBinder onBind(Intent intent) {
// We don't provide binding, so return null
return null;
}
private Notification buildNotification() {
NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
manager.createNotificationChannel(new NotificationChannel(
"all",
"All Notifications",
NotificationManager.IMPORTANCE_NONE));
return new Notification.Builder(this, "all")
.setContentTitle("Playing/recording audio")
.setContentText("playing/recording...")
.setSmallIcon(R.mipmap.ic_launcher)
.build();
}
return null;
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Log.i(TAG, "Receive onStartCommand" + intent);
switch (intent.getAction()) {
case ACTION_START:
Log.i(TAG, "Receive ACTION_START" + intent.getExtras());
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
startForeground(1, buildNotification(),
ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK
| ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE);
}
break;
case ACTION_STOP:
Log.i(TAG, "Receive ACTION_STOP" + intent.getExtras());
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
stopForeground(STOP_FOREGROUND_REMOVE);
}
break;
}
return START_NOT_STICKY;
}
}

View file

@ -0,0 +1,52 @@
package com.google.oboe.samples.liveEffect;
/*
* 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.
*/
import android.content.Context;
import android.media.AudioManager;
import android.os.Build;
public enum LiveEffectEngine {
INSTANCE;
// Load native library
static {
System.loadLibrary("liveEffect");
}
// Native methods
static native boolean create();
static native boolean isAAudioRecommended();
static native boolean setAPI(int apiType);
static native boolean setEffectOn(boolean isEffectOn);
static native void setRecordingDeviceId(int deviceId);
static native void setPlaybackDeviceId(int deviceId);
static native void delete();
static native void native_setDefaultStreamValues(int defaultSampleRate, int defaultFramesPerBurst);
static void setDefaultStreamValues(Context context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1){
AudioManager myAudioMgr = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
String sampleRateStr = myAudioMgr.getProperty(AudioManager.PROPERTY_OUTPUT_SAMPLE_RATE);
int defaultSampleRate = Integer.parseInt(sampleRateStr);
String framesPerBurstStr = myAudioMgr.getProperty(AudioManager.PROPERTY_OUTPUT_FRAMES_PER_BUFFER);
int defaultFramesPerBurst = Integer.parseInt(framesPerBurstStr);
native_setDefaultStreamValues(defaultSampleRate, defaultFramesPerBurst);
}
}
}

View file

@ -0,0 +1,299 @@
/*
* 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.oboe.samples.liveEffect;
import static com.google.oboe.samples.liveEffect.DuplexStreamForegroundService.ACTION_START;
import static com.google.oboe.samples.liveEffect.DuplexStreamForegroundService.ACTION_STOP;
import android.Manifest;
import android.app.Activity;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.media.AudioManager;
import android.os.Build;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.core.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.oboe.samples.audio_device.AudioDeviceListEntry;
import com.google.oboe.samples.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 mAAudioRecommended = 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;
setSpinnersEnabled(true);
}
}
});
findViewById(R.id.slesButton).setOnClickListener(new RadioButton.OnClickListener(){
@Override
public void onClick(View v) {
if (((RadioButton)v).isChecked()) {
apiSelection = OBOE_API_OPENSL_ES;
setSpinnersEnabled(false);
}
}
});
LiveEffectEngine.setDefaultStreamValues(this);
setVolumeControlStream(AudioManager.STREAM_MUSIC);
if (!isRecordPermissionGranted()){
requestRecordPermission();
} else {
startForegroundService();
}
onStartTest();
}
private void EnableAudioApiUI(boolean enable) {
if(apiSelection == OBOE_API_AAUDIO && !mAAudioRecommended)
{
apiSelection = OBOE_API_OPENSL_ES;
}
findViewById(R.id.slesButton).setEnabled(enable);
if(!mAAudioRecommended) {
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);
setSpinnersEnabled(enable);
}
@Override
protected void onStart() {
super.onStart();
}
@Override
protected void onResume() {
super.onResume();
}
@Override
protected void onPause() {
super.onPause();
}
@Override
protected void onDestroy() {
onStopTest();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
Intent serviceIntent = new Intent(ACTION_STOP, null, this,
DuplexStreamForegroundService.class);
startForegroundService(serviceIntent);
}
super.onDestroy();
}
private void onStartTest() {
LiveEffectEngine.create();
mAAudioRecommended = LiveEffectEngine.isAAudioRecommended();
EnableAudioApiUI(true);
LiveEffectEngine.setAPI(apiSelection);
}
private void onStopTest() {
stopEffect();
LiveEffectEngine.delete();
}
public void toggleEffect() {
if (isPlaying) {
stopEffect();
} else {
LiveEffectEngine.setAPI(apiSelection);
startEffect();
}
}
private void startEffect() {
Log.d(TAG, "Attempting to start");
boolean success = LiveEffectEngine.setEffectOn(true);
if (success) {
statusText.setText(R.string.status_playing);
toggleEffectButton.setText(R.string.stop_effect);
isPlaying = true;
EnableAudioApiUI(false);
} else {
statusText.setText(R.string.status_open_failed);
isPlaying = false;
}
}
private void stopEffect() {
Log.d(TAG, "Playing, attempting to stop");
LiveEffectEngine.setEffectOn(false);
resetStatusView();
toggleEffectButton.setText(R.string.start_effect);
isPlaying = false;
EnableAudioApiUI(true);
}
private void setSpinnersEnabled(boolean isEnabled){
if (((RadioButton)findViewById(R.id.slesButton)).isChecked())
{
isEnabled = false;
playbackDeviceSpinner.setSelection(0);
recordingDeviceSpinner.setSelection(0);
}
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);
}
private void startForegroundService() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
Intent serviceIntent = new Intent(ACTION_START, null, this,
DuplexStreamForegroundService.class);
startForegroundService(serviceIntent);
}
}
@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();
EnableAudioApiUI(false);
toggleEffectButton.setEnabled(false);
} else {
// Permission was granted, start foreground service.
startForegroundService();
}
}
}

View file

@ -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.
-->
<androidx.constraintlayout.widget.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.oboe.samples.liveEffect.MainActivity"
tools:layout_editor_absoluteY="81dp">
<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_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="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>
<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_toBottomOf="@+id/apiSelectionGroup"/>
<com.google.oboe.samples.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.oboe.samples.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" />
<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/playback_devices_spinner" />
<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" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,126 @@
<?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.
-->
<androidx.constraintlayout.widget.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.oboe.samples.liveEffect.MainActivity"
tools:layout_editor_absoluteY="81dp">
<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_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="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>
<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_toBottomOf="@+id/apiSelectionGroup"/>
<com.google.oboe.samples.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.oboe.samples.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" />
<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_constraintTop_toBottomOf="@+id/playback_devices_spinner" />
<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" />
</androidx.constraintlayout.widget.ConstraintLayout>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="AppTheme" parent="android:Theme.Material.Light">
</style>
</resources>

View file

@ -0,0 +1,6 @@
<resources>
<!-- Example customization of dimensions originally defined in res/values/dimens.xml
(such as screen margins) for screens with more than 820dp of available width. This
would include 7" and 10" devices in landscape (~960dp and ~1280dp respectively). -->
<dimen name="activity_horizontal_margin">64dp</dimen>
</resources>

View file

@ -0,0 +1,3 @@
<resources>
<color name="colorBlue">#4444CC</color>
</resources>

View file

@ -0,0 +1,6 @@
<resources>
<!-- Default screen margins, per the Android Design guidelines. -->
<dimen name="activity_horizontal_margin">16dp</dimen>
<dimen name="activity_vertical_margin">16dp</dimen>
<dimen name="activity_vertical_group_margin">32dp</dimen>
</resources>

View file

@ -0,0 +1,19 @@
<resources>
<string name="app_name">LiveEffect</string>
<string name="action_settings">Settings</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_playing">Engine Playing ....</string>
<string name="status_open_failed">Engine Failed to Open Streams!</string>
<string name="status_record_audio_denied">Error: Permission for RECORD_AUDIO was denied</string>
<string name="status_touch_to_begin">RECORD_AUDIO permission granted, touch START to begin</string>
<string name="status_warning">Warning: If you run this sample using the built-in microphone
and speaker you may create a feedback loop which will not be pleasant to listen to.</string>
<string name="recording_device">Recording device</string>
<string name="playback_device">Playback device</string>
<string name="apiSelection">APIs</string>
<string name="aaudio">AAudio</string>
<string name="sles">OpenSL ES</string>
</resources>

View file

@ -0,0 +1,8 @@
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="android:Theme.Holo.Light.DarkActionBar">
<!-- Customize your theme here. -->
</style>
</resources>