mirror of
https://git.eden-emu.dev/eden-emu/eden.git
synced 2025-07-21 05:15:47 +00:00
Move dead submodules in-tree
Signed-off-by: swurl <swurl@swurl.xyz>
This commit is contained in:
parent
c0cceff365
commit
6c655321e6
4081 changed files with 1185566 additions and 45 deletions
36
externals/oboe/samples/LiveEffect/README.md
vendored
Normal file
36
externals/oboe/samples/LiveEffect/README.md
vendored
Normal file
|
@ -0,0 +1,36 @@
|
|||
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
|
||||
|
||||
### Customizing the App
|
||||
|
||||
If you want to customize the effects processing then modify the
|
||||
onBothStreamsReady() method in "src/main/cpp/FullDuplexPass.h"
|
||||
|
||||
### Caveats
|
||||
OpenES SL does not allow setting the recording or playback device.
|
||||
|
||||
Synchronizing input and output streams for full-duplex operation is tricky.
|
||||
|
||||
Input and output have different startup times. The input side may have to charge up the microphone circuit.
|
||||
Also the initial timing for the output callback may be bursty as it fills the buffer up.
|
||||
So when the output stream makes its first callback, the input buffer may be overflowing or empty or partially full.
|
||||
|
||||
In order to get into sync we go through a few phases.
|
||||
|
||||
* In Phase 1 we always drain the input buffer as much as possible, more than the output callback asks for. When we have done this for a while, we move to phase 2.
|
||||
* In Phase 2 we optionally skip reading the input once to allow it to fill up with one burst. This makes it less likely to underflow on future reads.
|
||||
* In Phase 3 we should be in a stable situation where the output is nearly full and the input is nearly empty. You should be able to run for hours like this with no glitches.
|
43
externals/oboe/samples/LiveEffect/build.gradle
vendored
Normal file
43
externals/oboe/samples/LiveEffect/build.gradle
vendored
Normal file
|
@ -0,0 +1,43 @@
|
|||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
|
||||
android {
|
||||
compileSdkVersion 35
|
||||
defaultConfig {
|
||||
applicationId 'com.google.oboe.samples.liveeffect'
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 35
|
||||
versionCode 1
|
||||
versionName '1.0'
|
||||
ndk {
|
||||
abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
|
||||
}
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
arguments '-DANDROID_TOOLCHAIN=clang'
|
||||
abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
|
||||
}
|
||||
}
|
||||
}
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_18
|
||||
targetCompatibility JavaVersion.VERSION_18
|
||||
}
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
path 'src/main/cpp/CMakeLists.txt'
|
||||
}
|
||||
}
|
||||
namespace 'com.google.oboe.samples.liveEffect'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'androidx.appcompat:appcompat:1.7.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.2.1'
|
||||
implementation project(':audio-device')
|
||||
}
|
BIN
externals/oboe/samples/LiveEffect/screenshot.png
vendored
Normal file
BIN
externals/oboe/samples/LiveEffect/screenshot.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 35 KiB |
35
externals/oboe/samples/LiveEffect/src/main/AndroidManifest.xml
vendored
Normal file
35
externals/oboe/samples/LiveEffect/src/main/AndroidManifest.xml
vendored
Normal 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>
|
46
externals/oboe/samples/LiveEffect/src/main/cpp/CMakeLists.txt
vendored
Normal file
46
externals/oboe/samples/LiveEffect/src/main/cpp/CMakeLists.txt
vendored
Normal 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>")
|
||||
|
54
externals/oboe/samples/LiveEffect/src/main/cpp/FullDuplexPass.h
vendored
Normal file
54
externals/oboe/samples/LiveEffect/src/main/cpp/FullDuplexPass.h
vendored
Normal 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
|
245
externals/oboe/samples/LiveEffect/src/main/cpp/LiveEffectEngine.cpp
vendored
Normal file
245
externals/oboe/samples/LiveEffect/src/main/cpp/LiveEffectEngine.cpp
vendored
Normal 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();
|
||||
}
|
||||
}
|
83
externals/oboe/samples/LiveEffect/src/main/cpp/LiveEffectEngine.h
vendored
Normal file
83
externals/oboe/samples/LiveEffect/src/main/cpp/LiveEffectEngine.h
vendored
Normal 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
|
134
externals/oboe/samples/LiveEffect/src/main/cpp/jni_bridge.cpp
vendored
Normal file
134
externals/oboe/samples/LiveEffect/src/main/cpp/jni_bridge.cpp
vendored
Normal 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"
|
40
externals/oboe/samples/LiveEffect/src/main/cpp/ndk-stl-config.cmake
vendored
Normal file
40
externals/oboe/samples/LiveEffect/src/main/cpp/ndk-stl-config.cmake
vendored
Normal 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()
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
299
externals/oboe/samples/LiveEffect/src/main/java/com/google/oboe/samples/liveEffect/MainActivity.java
vendored
Normal file
299
externals/oboe/samples/LiveEffect/src/main/java/com/google/oboe/samples/liveEffect/MainActivity.java
vendored
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
128
externals/oboe/samples/LiveEffect/src/main/res/layout-v21/activity_main.xml
vendored
Normal file
128
externals/oboe/samples/LiveEffect/src/main/res/layout-v21/activity_main.xml
vendored
Normal 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>
|
126
externals/oboe/samples/LiveEffect/src/main/res/layout/activity_main.xml
vendored
Normal file
126
externals/oboe/samples/LiveEffect/src/main/res/layout/activity_main.xml
vendored
Normal 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>
|
BIN
externals/oboe/samples/LiveEffect/src/main/res/mipmap-hdpi/ic_launcher.png
vendored
Normal file
BIN
externals/oboe/samples/LiveEffect/src/main/res/mipmap-hdpi/ic_launcher.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.3 KiB |
BIN
externals/oboe/samples/LiveEffect/src/main/res/mipmap-mdpi/ic_launcher.png
vendored
Normal file
BIN
externals/oboe/samples/LiveEffect/src/main/res/mipmap-mdpi/ic_launcher.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.2 KiB |
BIN
externals/oboe/samples/LiveEffect/src/main/res/mipmap-xhdpi/ic_launcher.png
vendored
Normal file
BIN
externals/oboe/samples/LiveEffect/src/main/res/mipmap-xhdpi/ic_launcher.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.7 KiB |
BIN
externals/oboe/samples/LiveEffect/src/main/res/mipmap-xxhdpi/ic_launcher.png
vendored
Normal file
BIN
externals/oboe/samples/LiveEffect/src/main/res/mipmap-xxhdpi/ic_launcher.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 7.5 KiB |
5
externals/oboe/samples/LiveEffect/src/main/res/values-v21/styles.xml
vendored
Normal file
5
externals/oboe/samples/LiveEffect/src/main/res/values-v21/styles.xml
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<style name="AppTheme" parent="android:Theme.Material.Light">
|
||||
</style>
|
||||
</resources>
|
6
externals/oboe/samples/LiveEffect/src/main/res/values-w820dp/dimens.xml
vendored
Normal file
6
externals/oboe/samples/LiveEffect/src/main/res/values-w820dp/dimens.xml
vendored
Normal 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>
|
3
externals/oboe/samples/LiveEffect/src/main/res/values/colors.xml
vendored
Normal file
3
externals/oboe/samples/LiveEffect/src/main/res/values/colors.xml
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
<resources>
|
||||
<color name="colorBlue">#4444CC</color>
|
||||
</resources>
|
6
externals/oboe/samples/LiveEffect/src/main/res/values/dimens.xml
vendored
Normal file
6
externals/oboe/samples/LiveEffect/src/main/res/values/dimens.xml
vendored
Normal 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>
|
19
externals/oboe/samples/LiveEffect/src/main/res/values/strings.xml
vendored
Normal file
19
externals/oboe/samples/LiveEffect/src/main/res/values/strings.xml
vendored
Normal 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>
|
8
externals/oboe/samples/LiveEffect/src/main/res/values/styles.xml
vendored
Normal file
8
externals/oboe/samples/LiveEffect/src/main/res/values/styles.xml
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
<resources>
|
||||
|
||||
<!-- Base application theme. -->
|
||||
<style name="AppTheme" parent="android:Theme.Holo.Light.DarkActionBar">
|
||||
<!-- Customize your theme here. -->
|
||||
</style>
|
||||
|
||||
</resources>
|
Loading…
Add table
Add a link
Reference in a new issue