diff --git a/samples/LiveEffect/src/main/cpp/CMakeLists.txt b/samples/LiveEffect/src/main/cpp/CMakeLists.txt index 916edc0cf..29d66b96f 100644 --- a/samples/LiveEffect/src/main/cpp/CMakeLists.txt +++ b/samples/LiveEffect/src/main/cpp/CMakeLists.txt @@ -42,5 +42,5 @@ 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>") +target_compile_options(liveEffect PRIVATE -Wall -Werror "$<$<CONFIG:RELEASE>:-O3 -ffast-math>") diff --git a/samples/MegaDrone/src/main/cpp/CMakeLists.txt b/samples/MegaDrone/src/main/cpp/CMakeLists.txt index 658eb076d..cb101761a 100644 --- a/samples/MegaDrone/src/main/cpp/CMakeLists.txt +++ b/samples/MegaDrone/src/main/cpp/CMakeLists.txt @@ -26,4 +26,4 @@ target_link_options(megadrone 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(megadrone PRIVATE -Wall -Werror -Ofast) +target_compile_options(megadrone PRIVATE -Wall -Werror -O3 -ffast-math) diff --git a/samples/SoundBoard/src/main/cpp/CMakeLists.txt b/samples/SoundBoard/src/main/cpp/CMakeLists.txt index 6e997e557..992c33c8f 100644 --- a/samples/SoundBoard/src/main/cpp/CMakeLists.txt +++ b/samples/SoundBoard/src/main/cpp/CMakeLists.txt @@ -26,4 +26,4 @@ target_link_options(soundboard 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(soundboard PRIVATE -Wall -Werror -Ofast) +target_compile_options(soundboard PRIVATE -Wall -Werror -O3 -ffast-math) diff --git a/samples/drumthumper/src/main/cpp/CMakeLists.txt b/samples/drumthumper/src/main/cpp/CMakeLists.txt index 75fe8652d..273c45442 100644 --- a/samples/drumthumper/src/main/cpp/CMakeLists.txt +++ b/samples/drumthumper/src/main/cpp/CMakeLists.txt @@ -56,7 +56,7 @@ add_library(drumthumper SHARED # Enable optimization flags: if having problems with source level debugging, # disable -Ofast ( and debug ), re-enable after done debugging. -target_compile_options(drumthumper PRIVATE -Wall -Werror "$<$<CONFIG:RELEASE>:-Ofast>") +target_compile_options(drumthumper PRIVATE -Wall -Werror "$<$<CONFIG:RELEASE>:-O3 -ffast-math>") target_link_libraries( # Specifies the target library. drumthumper diff --git a/samples/drumthumper/src/main/cpp/DrumPlayerJNI.cpp b/samples/drumthumper/src/main/cpp/DrumPlayerJNI.cpp index 90eb36ff3..e44005ab4 100644 --- a/samples/drumthumper/src/main/cpp/DrumPlayerJNI.cpp +++ b/samples/drumthumper/src/main/cpp/DrumPlayerJNI.cpp @@ -47,7 +47,7 @@ static SimpleMultiPlayer sDTPlayer; JNIEXPORT void JNICALL Java_com_plausiblesoftware_drumthumper_DrumPlayer_setupAudioStreamNative( JNIEnv* env, jobject, jint numChannels) { __android_log_print(ANDROID_LOG_INFO, TAG, "%s", "init()"); - sDTPlayer.setupAudioStream(numChannels); + sDTPlayer.setupAudioStream(numChannels, oboe::PerformanceMode::LowLatency); } JNIEXPORT void JNICALL @@ -106,7 +106,7 @@ JNIEXPORT void JNICALL Java_com_plausiblesoftware_drumthumper_DrumPlayer_unloadW * Native (JNI) implementation of DrumPlayer.trigger() */ JNIEXPORT void JNICALL Java_com_plausiblesoftware_drumthumper_DrumPlayer_trigger(JNIEnv* env, jobject, jint index) { - sDTPlayer.triggerDown(index); + sDTPlayer.triggerDown(index, oboe::PerformanceMode::LowLatency); } /** @@ -135,7 +135,7 @@ JNIEXPORT void JNICALL Java_com_plausiblesoftware_drumthumper_DrumPlayer_clearOu */ JNIEXPORT void JNICALL Java_com_plausiblesoftware_drumthumper_DrumPlayer_restartStream(JNIEnv*, jobject) { sDTPlayer.resetAll(); - if (sDTPlayer.openStream() && sDTPlayer.startStream()){ + if (sDTPlayer.openStream(oboe::PerformanceMode::LowLatency) && sDTPlayer.startStream()){ __android_log_print(ANDROID_LOG_INFO, TAG, "openStream successful"); } else { __android_log_print(ANDROID_LOG_ERROR, TAG, "openStream failed"); diff --git a/samples/gradle.properties b/samples/gradle.properties index 3afe4fd2a..cfe2a55be 100644 --- a/samples/gradle.properties +++ b/samples/gradle.properties @@ -38,3 +38,4 @@ android.nonFinalResIds=false android.nonTransitiveRClass=false android.useAndroidX=true org.gradle.configuration-cache=true +org.gradle.jvmargs=-Xmx4g -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 \ No newline at end of file diff --git a/samples/hello-oboe/src/main/cpp/CMakeLists.txt b/samples/hello-oboe/src/main/cpp/CMakeLists.txt index 48fc94ef5..bc2bda953 100644 --- a/samples/hello-oboe/src/main/cpp/CMakeLists.txt +++ b/samples/hello-oboe/src/main/cpp/CMakeLists.txt @@ -57,4 +57,4 @@ target_link_options(hello-oboe PRIVATE "-Wl,-z,max-page-size=16384") # Enable optimization flags: if having problems with source level debugging, # disable -Ofast ( and debug ), re-enable after done debugging. -target_compile_options(hello-oboe PRIVATE -Wall -Werror "$<$<CONFIG:RELEASE>:-Ofast>") +target_compile_options(hello-oboe PRIVATE -Wall -Werror "$<$<CONFIG:RELEASE>:-O3 -ffast-math>") diff --git a/samples/iolib/src/main/cpp/player/SampleSource.h b/samples/iolib/src/main/cpp/player/SampleSource.h index 54edbf1f9..633c776fe 100644 --- a/samples/iolib/src/main/cpp/player/SampleSource.h +++ b/samples/iolib/src/main/cpp/player/SampleSource.h @@ -45,7 +45,12 @@ class SampleSource: public DataSource { virtual ~SampleSource() {} void setPlayMode() { mCurSampleIndex = 0; mIsPlaying = true; } - void setStopMode() { mIsPlaying = false; mCurSampleIndex = 0; } + void setStopMode(bool isPause = false) { + mIsPlaying = false; + if (!isPause) { + mCurSampleIndex = 0; + } + } void setLoopMode(bool isLoopMode) { mIsLoopMode = isLoopMode; } diff --git a/samples/iolib/src/main/cpp/player/SimpleMultiPlayer.cpp b/samples/iolib/src/main/cpp/player/SimpleMultiPlayer.cpp index a30725877..8d938bfc8 100644 --- a/samples/iolib/src/main/cpp/player/SimpleMultiPlayer.cpp +++ b/samples/iolib/src/main/cpp/player/SimpleMultiPlayer.cpp @@ -24,201 +24,226 @@ #include "OneShotSampleSource.h" #include "SimpleMultiPlayer.h" -static const char* TAG = "SimpleMultiPlayer"; +static const char *TAG = "SimpleMultiPlayer"; using namespace oboe; using namespace parselib; namespace iolib { -constexpr int32_t kBufferSizeInBursts = 2; // Use 2 bursts as the buffer size (double buffer) + constexpr int32_t kBufferSizeInBursts = 2; // Use 2 bursts as the buffer size (double buffer) -SimpleMultiPlayer::SimpleMultiPlayer() - : mChannelCount(0), mOutputReset(false), mSampleRate(0), mNumSampleBuffers(0) -{} + SimpleMultiPlayer::SimpleMultiPlayer() + : mChannelCount(0), mOutputReset(false), mSampleRate(0), mNumSampleBuffers(0) {} -DataCallbackResult SimpleMultiPlayer::MyDataCallback::onAudioReady(AudioStream *oboeStream, - void *audioData, - int32_t numFrames) { + DataCallbackResult SimpleMultiPlayer::MyDataCallback::onAudioReady(AudioStream *oboeStream, + void *audioData, + int32_t numFrames) { - StreamState streamState = oboeStream->getState(); - if (streamState != StreamState::Open && streamState != StreamState::Started) { - __android_log_print(ANDROID_LOG_ERROR, TAG, " streamState:%d", streamState); - } - if (streamState == StreamState::Disconnected) { - __android_log_print(ANDROID_LOG_ERROR, TAG, " streamState::Disconnected"); + StreamState streamState = oboeStream->getState(); + if (streamState != StreamState::Open && streamState != StreamState::Started) { + __android_log_print(ANDROID_LOG_ERROR, TAG, " streamState:%d", streamState); + } + if (streamState == StreamState::Disconnected) { + __android_log_print(ANDROID_LOG_ERROR, TAG, " streamState::Disconnected"); + } + + memset(audioData, 0, static_cast<size_t>(numFrames) * static_cast<size_t> + (mParent->mChannelCount) * sizeof(float)); + + // OneShotSampleSource* sources = mSampleSources.get(); + for (int32_t index = 0; index < mParent->mNumSampleBuffers; index++) { + if (mParent->mSampleSources[index]->isPlaying()) { + mParent->mSampleSources[index]->mixAudio((float *) audioData, mParent->mChannelCount, + numFrames); + } + } + + return DataCallbackResult::Continue; } - memset(audioData, 0, static_cast<size_t>(numFrames) * static_cast<size_t> - (mParent->mChannelCount) * sizeof(float)); + void SimpleMultiPlayer::MyErrorCallback::onErrorAfterClose(AudioStream *oboeStream, Result error) { + __android_log_print(ANDROID_LOG_INFO, TAG, "==== onErrorAfterClose() error:%d", error); - // OneShotSampleSource* sources = mSampleSources.get(); - for(int32_t index = 0; index < mParent->mNumSampleBuffers; index++) { - if (mParent->mSampleSources[index]->isPlaying()) { - mParent->mSampleSources[index]->mixAudio((float*)audioData, mParent->mChannelCount, - numFrames); + mParent->resetAll(); + if (mParent->openStream(PerformanceMode::None) && mParent->startStream()) { + mParent->mOutputReset = true; } } - return DataCallbackResult::Continue; -} + void SimpleMultiPlayer::MyPresentationCallback::onPresentationEnded(oboe::AudioStream *oboeStream) { + __android_log_print(ANDROID_LOG_INFO, TAG, "==== MyPresentationCallback() called"); + } -void SimpleMultiPlayer::MyErrorCallback::onErrorAfterClose(AudioStream *oboeStream, Result error) { - __android_log_print(ANDROID_LOG_INFO, TAG, "==== onErrorAfterClose() error:%d", error); + bool SimpleMultiPlayer::openStream(PerformanceMode performanceMode) { + __android_log_print(ANDROID_LOG_INFO, TAG, "openStream()"); + + // Use shared_ptr to prevent use of a deleted callback. + mDataCallback = std::make_shared<MyDataCallback>(this); + mErrorCallback = std::make_shared<MyErrorCallback>(this); + + // Create an audio stream + AudioStreamBuilder builder; + builder.setChannelCount(mChannelCount); + // we will resample source data to device rate, so take default sample rate + builder.setDataCallback(mDataCallback); + + builder.setFormat(AudioFormat::Float); + builder.setSampleRate(48000); + + builder.setErrorCallback(mErrorCallback); + builder.setPresentationCallback(mPresentationCallback); + builder.setPerformanceMode(performanceMode); + builder.setFramesPerDataCallback(128); + builder.setSharingMode(SharingMode::Exclusive); + builder.setSampleRateConversionQuality(SampleRateConversionQuality::Medium); + + Result result = builder.openStream(mAudioStream); + if (result != Result::OK) { + __android_log_print( + ANDROID_LOG_ERROR, + TAG, + "openStream failed. Error: %s", convertToText(result)); + return false; + } + + // Reduce stream latency by setting the buffer size to a multiple of the burst size + // Note: this will fail with ErrorUnimplemented if we are using a callback with OpenSL ES + // See oboe::AudioStreamBuffered::setBufferSizeInFrames + result = mAudioStream->setBufferSizeInFrames( + mAudioStream->getFramesPerBurst() * kBufferSizeInBursts); + if (result != Result::OK) { + __android_log_print( + ANDROID_LOG_WARN, + TAG, + "setBufferSizeInFrames failed. Error: %s", convertToText(result)); + } - mParent->resetAll(); - if (mParent->openStream() && mParent->startStream()) { - mParent->mOutputReset = true; + mSampleRate = mAudioStream->getSampleRate(); + + return true; } -} -bool SimpleMultiPlayer::openStream() { - __android_log_print(ANDROID_LOG_INFO, TAG, "openStream()"); - - // Use shared_ptr to prevent use of a deleted callback. - mDataCallback = std::make_shared<MyDataCallback>(this); - mErrorCallback = std::make_shared<MyErrorCallback>(this); - - // Create an audio stream - AudioStreamBuilder builder; - builder.setChannelCount(mChannelCount); - // we will resample source data to device rate, so take default sample rate - builder.setDataCallback(mDataCallback); - builder.setErrorCallback(mErrorCallback); - builder.setPerformanceMode(PerformanceMode::LowLatency); - builder.setSharingMode(SharingMode::Exclusive); - builder.setSampleRateConversionQuality(SampleRateConversionQuality::Medium); - - Result result = builder.openStream(mAudioStream); - if (result != Result::OK){ - __android_log_print( - ANDROID_LOG_ERROR, - TAG, - "openStream failed. Error: %s", convertToText(result)); + bool SimpleMultiPlayer::startStream() { + int tryCount = 0; + while (tryCount < 3) { + bool wasOpenSuccessful = true; + // Assume that openStream() was called successfully before startStream() call. + if (tryCount > 0) { + usleep(20 * 1000); // Sleep between tries to give the system time to settle. + wasOpenSuccessful = openStream( + PerformanceMode::None); // Try to open the stream again after the first try. + } + if (wasOpenSuccessful) { + Result result = mAudioStream->requestStart(); + if (result != Result::OK) { + __android_log_print( + ANDROID_LOG_ERROR, + TAG, + "requestStart failed. Error: %s", convertToText(result)); + mAudioStream->close(); + mAudioStream.reset(); + } else { + return true; + } + } + tryCount++; + } + return false; } - // Reduce stream latency by setting the buffer size to a multiple of the burst size - // Note: this will fail with ErrorUnimplemented if we are using a callback with OpenSL ES - // See oboe::AudioStreamBuffered::setBufferSizeInFrames - result = mAudioStream->setBufferSizeInFrames( - mAudioStream->getFramesPerBurst() * kBufferSizeInBursts); - if (result != Result::OK) { - __android_log_print( - ANDROID_LOG_WARN, - TAG, - "setBufferSizeInFrames failed. Error: %s", convertToText(result)); + void SimpleMultiPlayer::setupAudioStream(int32_t channelCount, oboe::PerformanceMode performanceMode) { + __android_log_print(ANDROID_LOG_INFO, TAG, "setupAudioStream()"); + mChannelCount = channelCount; + + openStream(performanceMode); } - mSampleRate = mAudioStream->getSampleRate(); + void SimpleMultiPlayer::teardownAudioStream() { + __android_log_print(ANDROID_LOG_INFO, TAG, "teardownAudioStream()"); + // tear down the player + if (mAudioStream) { + mAudioStream->stop(); + mAudioStream->close(); + mAudioStream.reset(); + } + } - return true; -} + void SimpleMultiPlayer::addSampleSource(SampleSource *source, SampleBuffer *buffer) { + buffer->resampleData(mSampleRate); -bool SimpleMultiPlayer::startStream() { - int tryCount = 0; - while (tryCount < 3) { - bool wasOpenSuccessful = true; - // Assume that openStream() was called successfully before startStream() call. - if (tryCount > 0) { - usleep(20 * 1000); // Sleep between tries to give the system time to settle. - wasOpenSuccessful = openStream(); // Try to open the stream again after the first try. - } - if (wasOpenSuccessful) { - Result result = mAudioStream->requestStart(); - if (result != Result::OK){ - __android_log_print( - ANDROID_LOG_ERROR, - TAG, - "requestStart failed. Error: %s", convertToText(result)); - mAudioStream->close(); - mAudioStream.reset(); - } else { - return true; - } - } - tryCount++; + mSampleBuffers.push_back(buffer); + mSampleSources.push_back(source); + mNumSampleBuffers++; } - return false; -} + void SimpleMultiPlayer::unloadSampleData() { + __android_log_print(ANDROID_LOG_INFO, TAG, "unloadSampleData()"); + resetAll(); -void SimpleMultiPlayer::setupAudioStream(int32_t channelCount) { - __android_log_print(ANDROID_LOG_INFO, TAG, "setupAudioStream()"); - mChannelCount = channelCount; + for (int32_t bufferIndex = 0; bufferIndex < mNumSampleBuffers; bufferIndex++) { + delete mSampleBuffers[bufferIndex]; + delete mSampleSources[bufferIndex]; + } - openStream(); -} + mSampleBuffers.clear(); + mSampleSources.clear(); -void SimpleMultiPlayer::teardownAudioStream() { - __android_log_print(ANDROID_LOG_INFO, TAG, "teardownAudioStream()"); - // tear down the player - if (mAudioStream) { - mAudioStream->stop(); - mAudioStream->close(); - mAudioStream.reset(); + mNumSampleBuffers = 0; } -} -void SimpleMultiPlayer::addSampleSource(SampleSource* source, SampleBuffer* buffer) { - buffer->resampleData(mSampleRate); + void SimpleMultiPlayer::triggerDown(int32_t index, oboe::PerformanceMode performanceMode) { + if (index < mNumSampleBuffers) { + mSampleSources[index]->setPlayMode(); + } + + if (!mAudioStream) setupAudioStream(mChannelCount, performanceMode); - mSampleBuffers.push_back(buffer); - mSampleSources.push_back(source); - mNumSampleBuffers++; -} + auto state = mAudioStream->getState(); + if (mAudioStream && state == StreamState::Closed) { + openStream(performanceMode); + startStream(); + } -void SimpleMultiPlayer::unloadSampleData() { - __android_log_print(ANDROID_LOG_INFO, TAG, "unloadSampleData()"); - resetAll(); + if (mAudioStream && state == StreamState::Open) startStream(); + if (mAudioStream && state == StreamState::Paused) mAudioStream->requestStart(); - for (int32_t bufferIndex = 0; bufferIndex < mNumSampleBuffers; bufferIndex++) { - delete mSampleBuffers[bufferIndex]; - delete mSampleSources[bufferIndex]; } - mSampleBuffers.clear(); - mSampleSources.clear(); + void SimpleMultiPlayer::triggerUp(int32_t index) { +// if (index < mNumSampleBuffers) { +// mSampleSources[index]->setStopMode(true); +// } - mNumSampleBuffers = 0; -} - -void SimpleMultiPlayer::triggerDown(int32_t index) { - if (index < mNumSampleBuffers) { - mSampleSources[index]->setPlayMode(); + mAudioStream->pause(); } -} -void SimpleMultiPlayer::triggerUp(int32_t index) { - if (index < mNumSampleBuffers) { - mSampleSources[index]->setStopMode(); + void SimpleMultiPlayer::resetAll() { + for (int32_t bufferIndex = 0; bufferIndex < mNumSampleBuffers; bufferIndex++) { + mSampleSources[bufferIndex]->setStopMode(); + } } -} -void SimpleMultiPlayer::resetAll() { - for (int32_t bufferIndex = 0; bufferIndex < mNumSampleBuffers; bufferIndex++) { - mSampleSources[bufferIndex]->setStopMode(); + void SimpleMultiPlayer::setPan(int index, float pan) { + mSampleSources[index]->setPan(pan); } -} -void SimpleMultiPlayer::setPan(int index, float pan) { - mSampleSources[index]->setPan(pan); -} + float SimpleMultiPlayer::getPan(int index) { + return mSampleSources[index]->getPan(); + } -float SimpleMultiPlayer::getPan(int index) { - return mSampleSources[index]->getPan(); -} + void SimpleMultiPlayer::setGain(int index, float gain) { + mSampleSources[index]->setGain(gain); + } -void SimpleMultiPlayer::setGain(int index, float gain) { - mSampleSources[index]->setGain(gain); -} + float SimpleMultiPlayer::getGain(int index) { + return mSampleSources[index]->getGain(); + } -float SimpleMultiPlayer::getGain(int index) { - return mSampleSources[index]->getGain(); -} + void SimpleMultiPlayer::setLoopMode(int index, bool isLoopMode) { + mSampleSources[index]->setLoopMode(isLoopMode); + } -void SimpleMultiPlayer::setLoopMode(int index, bool isLoopMode) { - mSampleSources[index]->setLoopMode(isLoopMode); -} } diff --git a/samples/iolib/src/main/cpp/player/SimpleMultiPlayer.h b/samples/iolib/src/main/cpp/player/SimpleMultiPlayer.h index 43a6dd6cd..c53663b1d 100644 --- a/samples/iolib/src/main/cpp/player/SimpleMultiPlayer.h +++ b/samples/iolib/src/main/cpp/player/SimpleMultiPlayer.h @@ -33,10 +33,10 @@ class SimpleMultiPlayer { public: SimpleMultiPlayer(); - void setupAudioStream(int32_t channelCount); + void setupAudioStream(int32_t channelCount, oboe::PerformanceMode performanceMode); void teardownAudioStream(); - bool openStream(); + bool openStream(oboe::PerformanceMode performanceMode); bool startStream(); int getSampleRate() { return mSampleRate; } @@ -54,7 +54,7 @@ class SimpleMultiPlayer { */ void unloadSampleData(); - void triggerDown(int32_t index); + void triggerDown(int32_t index, oboe::PerformanceMode performanceMode); void triggerUp(int32_t index); void resetAll(); @@ -97,6 +97,18 @@ class SimpleMultiPlayer { SimpleMultiPlayer *mParent; }; + class MyPresentationCallback : public oboe::AudioStreamPresentationCallback { + public: + MyPresentationCallback(SimpleMultiPlayer *parent) : mParent(parent) {} + + virtual ~MyPresentationCallback() { + } + + void onPresentationEnded(oboe::AudioStream *oboeStream) override; + private: + SimpleMultiPlayer *mParent; + }; + // Oboe Audio Stream std::shared_ptr<oboe::AudioStream> mAudioStream; @@ -113,6 +125,8 @@ class SimpleMultiPlayer { std::shared_ptr<MyDataCallback> mDataCallback; std::shared_ptr<MyErrorCallback> mErrorCallback; + std::shared_ptr<MyPresentationCallback> mPresentationCallback; + }; } diff --git a/samples/minimaloboe/src/main/cpp/CMakeLists.txt b/samples/minimaloboe/src/main/cpp/CMakeLists.txt index 027b27bce..a62226281 100644 --- a/samples/minimaloboe/src/main/cpp/CMakeLists.txt +++ b/samples/minimaloboe/src/main/cpp/CMakeLists.txt @@ -41,7 +41,7 @@ add_library(minimaloboe SHARED # Enable optimization flags: if having problems with source level debugging, # disable -Ofast ( and debug ), re-enable after done debugging. -target_compile_options(minimaloboe PRIVATE -Wall -Werror "$<$<CONFIG:RELEASE>:-Ofast>") +target_compile_options(minimaloboe PRIVATE -Wall -Werror "$<$<CONFIG:RELEASE>:-O3 -ffast-math>") target_link_libraries( # Specifies the target library. minimaloboe diff --git a/samples/powerplay/.gitignore b/samples/powerplay/.gitignore new file mode 100644 index 000000000..796b96d1c --- /dev/null +++ b/samples/powerplay/.gitignore @@ -0,0 +1 @@ +/build diff --git a/samples/powerplay/README.md b/samples/powerplay/README.md new file mode 100644 index 000000000..cd8f68e31 --- /dev/null +++ b/samples/powerplay/README.md @@ -0,0 +1,19 @@ +# Minimal Oboe + +## Overview + +This app is a very simple demonstration of turning on audio from buttons. +It uses a low-latency Oboe stream. + +## Implementation + +The app is written using Kotlin and Jetpack Compose. + +The app state is maintained by subclassing DefaultLifecycleObserver. +Oboe is called through an external native function. + +This app uses shared_ptr for passing callbacks to Oboe. +When the stream is disconnected, it starts a new stream. + +## Screenshots + diff --git a/samples/powerplay/build.gradle b/samples/powerplay/build.gradle new file mode 100644 index 000000000..a34adf817 --- /dev/null +++ b/samples/powerplay/build.gradle @@ -0,0 +1,86 @@ +plugins { + id 'com.android.application' + id 'org.jetbrains.kotlin.android' + id 'org.jetbrains.kotlin.plugin.compose' +} + +android { + defaultConfig { + applicationId "com.example.powerplay" + minSdkVersion 26 + targetSdkVersion 35 + compileSdkVersion 35 + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + vectorDrawables { + useSupportLibrary true + } + externalNativeBuild { + cmake { + cppFlags "-std=c++17" + arguments '-DANDROID_STL=c++_static' + // armeabi and mips are deprecated in NDK r16 so we don't want to build for them + abiFilters 'arm64-v8a', 'x86', 'x86_64' + } + } + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_18 + targetCompatibility JavaVersion.VERSION_18 + } + kotlinOptions { + jvmTarget = '18' + } + buildFeatures { + compose true + } + composeOptions { + kotlinCompilerExtensionVersion compose_version + } + + ndkVersion '29.0.13113456 rc1' // Requires NDK r29 for PCM Offload + externalNativeBuild { + cmake { + path 'src/main/cpp/CMakeLists.txt' + version '3.31.6' + } + } + packagingOptions { + resources { + excludes += '/META-INF/{AL2.0,LGPL2.1}' + } + } + namespace 'com.example.powerplay' +} + +dependencies { + implementation "androidx.core:core-ktx:$core_version" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.1" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.1" + implementation "androidx.activity:activity-ktx:1.10.1" + implementation "androidx.lifecycle:lifecycle-process:$lifecycle_version" + implementation "androidx.lifecycle:lifecycle-viewmodel:$lifecycle_version" + implementation "androidx.lifecycle:lifecycle-viewmodel-compose:$lifecycle_version" + implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version" + implementation "androidx.lifecycle:lifecycle-runtime-compose:$lifecycle_version" + implementation "androidx.compose.ui:ui:$compose_version" + implementation "androidx.compose.material:material:$compose_version" + implementation "androidx.compose.ui:ui-tooling-preview:$compose_version" + implementation "androidx.compose.material3:material3:1.3.1" + implementation 'androidx.activity:activity-compose:1.10.1' + implementation 'androidx.appcompat:appcompat:1.7.0' + testImplementation 'junit:junit:4.13.2' + androidTestImplementation 'androidx.test.ext:junit:1.2.1' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1' + androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version" + debugImplementation "androidx.compose.ui:ui-tooling:$compose_version" + debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_version" +} diff --git a/samples/powerplay/proguard-rules.pro b/samples/powerplay/proguard-rules.pro new file mode 100644 index 000000000..f1b424510 --- /dev/null +++ b/samples/powerplay/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/samples/powerplay/src/main/AndroidManifest.xml b/samples/powerplay/src/main/AndroidManifest.xml new file mode 100644 index 000000000..9f629864d --- /dev/null +++ b/samples/powerplay/src/main/AndroidManifest.xml @@ -0,0 +1,32 @@ +<?xml version="1.0" encoding="utf-8"?> +<manifest xmlns:android="http://schemas.android.com/apk/res/android"> + + <uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> + <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" /> + + <application + android:allowBackup="true" + android:icon="@mipmap/ic_launcher" + android:label="@string/app_name" + android:roundIcon="@mipmap/ic_launcher_round" + android:supportsRtl="true" + android:theme="@style/Theme.Samples"> + <activity + android:name=".MainActivity" + android:exported="true" + android:theme="@style/Theme.Samples"> + <intent-filter> + <action android:name="android.intent.action.MAIN" /> + + <category android:name="android.intent.category.LAUNCHER" /> + </intent-filter> + </activity> + + + <service + android:name=".engine.AudioForegroundService" + android:exported="false" + android:foregroundServiceType="mediaPlayback" /> + </application> + +</manifest> diff --git a/samples/powerplay/src/main/assets/song1.wav b/samples/powerplay/src/main/assets/song1.wav new file mode 100644 index 000000000..2c773f1d5 Binary files /dev/null and b/samples/powerplay/src/main/assets/song1.wav differ diff --git a/samples/powerplay/src/main/cpp/CMakeLists.txt b/samples/powerplay/src/main/cpp/CMakeLists.txt new file mode 100644 index 000000000..5f41eb6ec --- /dev/null +++ b/samples/powerplay/src/main/cpp/CMakeLists.txt @@ -0,0 +1,66 @@ +# +# Copyright 2022 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.31.6) + +# Pull in required libs +set(PARSELIB_DIR ../../../../parselib) +set(IOLIB_DIR ../../../../iolib) +set(OBOE_DIR ../../../../../) + +add_subdirectory(${OBOE_DIR} ./oboe-bin) + +# include folders +include_directories( + ${OBOE_DIR}/include + ${CMAKE_CURRENT_LIST_DIR} +) + +include(${PARSELIB_DIR}/src/main/cpp/CMakeLists.txt) +include(${IOLIB_DIR}/src/main/cpp/CMakeLists.txt) + +# App specific sources +set(APP_SOURCES + PowerPlayJNI.cpp +) + +# Build the powerplay (native) library +add_library( + powerplay SHARED ${APP_SOURCES} +) + +# Enable optimization flags: if having problems with source level debugging, +# disable -Ofast ( and debug ), re-enable after done debugging. +target_compile_options(powerplay PRIVATE -Wall -Werror "$<$<CONFIG:RELEASE>:-O3 -ffast-math>") + +target_link_libraries( + # Specifies the target library. + powerplay + + # iolib & parselib + -Wl,--whole-archive + iolib + parselib + -Wl,--no-whole-archive + + oboe + + # Links the target library to the log library + # included in the NDK. + log +) + +target_link_options(powerplay PRIVATE "-Wl,-z,max-page-size=16384") diff --git a/samples/powerplay/src/main/cpp/PowerPlayJNI.cpp b/samples/powerplay/src/main/cpp/PowerPlayJNI.cpp new file mode 100644 index 000000000..744718811 --- /dev/null +++ b/samples/powerplay/src/main/cpp/PowerPlayJNI.cpp @@ -0,0 +1,183 @@ +/* + * Copyright 2025 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 <sys/types.h> +#include <sys/stat.h> +#include <fcntl.h> +#include <unistd.h> + +#include <android/log.h> + +// parselib includes +#include <stream/MemInputStream.h> +#include <wav/WavStreamReader.h> + +#include <player/OneShotSampleSource.h> +#include <player/SimpleMultiPlayer.h> + +static const char *TAG = "PowerPlayJNI"; + +// JNI functions are "C" calling convention +#ifdef __cplusplus +extern "C" { +#endif + +using namespace iolib; +using namespace parselib; +using namespace oboe; + +static SimpleMultiPlayer sDTPlayer; + +/** + * Native (JNI) implementation of PowerPlayAudioEngine.setupAudioStreamNative() + */ +JNIEXPORT void JNICALL +Java_com_example_powerplay_engine_PowerPlayAudioPlayer_setupAudioStreamNative( + JNIEnv *env, + jobject, + jint channels +) { + __android_log_print(ANDROID_LOG_INFO, TAG, "%s", "setupAudioStreamNative()"); + + // TODO - Dynamically Set Performance Mode + sDTPlayer.setupAudioStream(channels, oboe::PerformanceMode::None); +} + +/** + * Native (JNI) implementation of PowerPlayAudioEngine.startAudioStreamNative() + */ +JNIEXPORT jint JNICALL +Java_com_example_powerplay_engine_PowerPlayAudioPlayer_startAudioStreamNative( + JNIEnv *, + jobject +) { + __android_log_print(ANDROID_LOG_INFO, TAG, "%s", "startAudioStreamNative()"); + return (jint) sDTPlayer.startStream(); +} + +/** + * Native (JNI) implementation of PowerPlayAudioEngine.teardownAudioStreamNative() + */ +JNIEXPORT jint JNICALL +Java_com_example_powerplay_engine_PowerPlayAudioPlayer_teardownAudioStreamNative( + JNIEnv *, + jobject +) { + __android_log_print(ANDROID_LOG_INFO, TAG, "%s", "teardownAudioStreamNative()"); + sDTPlayer.teardownAudioStream(); + + //TODO - Actually handle a return here. + return true; +} + +/** + * Native (JNI) implementation of PowerPlayAudioEngine.loadAssetNative() + */ +JNIEXPORT void JNICALL Java_com_example_powerplay_engine_PowerPlayAudioPlayer_loadAssetNative( + JNIEnv *env, + jobject, + jbyteArray bytearray, + jint index +) { + const int32_t len = env->GetArrayLength(bytearray); + auto *buf = new unsigned char[len]; + + env->GetByteArrayRegion(bytearray, 0, len, reinterpret_cast<jbyte *>(buf)); + + MemInputStream stream(buf, len); + WavStreamReader reader(&stream); + reader.parse(); + reader.getNumChannels(); + + auto *sampleBuffer = new SampleBuffer(); + sampleBuffer->loadSampleData(&reader); + + const auto source = new OneShotSampleSource(sampleBuffer, 0); + sDTPlayer.addSampleSource(source, sampleBuffer); + + delete[] buf; +} + +/** + * Native (JNI) implementation of DrumPlayer.unloadWavAssetsNative() + */ +JNIEXPORT void JNICALL Java_com_example_powerplay_engine_PowerPlayAudioPlayer_unloadAssetsNative( + JNIEnv *env, + jobject +) { + sDTPlayer.unloadSampleData(); +} + +/** + * Native (JNI) implementation of DrumPlayer.getOutputReset() + */ +JNIEXPORT jboolean JNICALL +Java_com_example_powerplay_engine_PowerPlayAudioPlayer_getOutputResetNative( + JNIEnv *, + jobject +) { + return sDTPlayer.getOutputReset(); +} + +/** + * Native (JNI) implementation of DrumPlayer.clearOutputReset() + */ +JNIEXPORT void JNICALL +Java_com_example_powerplay_engine_PowerPlayAudioPlayer_clearOutputResetNative( + JNIEnv *, + jobject +) { + sDTPlayer.clearOutputReset(); +} + +/** + * Native (JNI) implementation of DrumPlayer.trigger() + */ +JNIEXPORT void JNICALL +Java_com_example_powerplay_engine_PowerPlayAudioPlayer_startPlayingNative(JNIEnv *env, jobject, + jint index, + jint offload) { + auto performanceMode = + offload == 0 ? PerformanceMode::None + : offload == 1 ? PerformanceMode::LowLatency + : offload == 2 ? PerformanceMode::PowerSaving + : PerformanceMode::POWER_SAVING_OFFLOADED; + sDTPlayer.triggerDown(index, performanceMode); +} + +/** + * Native (JNI) implementation of DrumPlayer.trigger() + */ +JNIEXPORT void JNICALL +Java_com_example_powerplay_engine_PowerPlayAudioPlayer_stopPlayingNative(JNIEnv *env, jobject, + jint index) { + sDTPlayer.triggerUp(index); +} + +/** + * Native (JNI) implementation of DrumPlayer.trigger() + */ +JNIEXPORT void JNICALL +Java_com_example_powerplay_engine_PowerPlayAudioPlayer_setLoopingNative(JNIEnv *env, jobject, + jint index, + jboolean looping) { + sDTPlayer.setLoopMode(index, looping); +} + +#ifdef __cplusplus +} +#endif diff --git a/samples/powerplay/src/main/kotlin/com/example/powerplay/MainActivity.kt b/samples/powerplay/src/main/kotlin/com/example/powerplay/MainActivity.kt new file mode 100644 index 000000000..eb5010acd --- /dev/null +++ b/samples/powerplay/src/main/kotlin/com/example/powerplay/MainActivity.kt @@ -0,0 +1,602 @@ +package com.example.powerplay + +import android.content.Intent +import android.media.AudioAttributes +import android.media.AudioFormat +import android.media.AudioManager +import android.os.Build +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.ExperimentalAnimationApi +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.LinearOutSlowInEasing +import androidx.compose.animation.core.RepeatMode +import androidx.compose.animation.core.infiniteRepeatable +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.scaleIn +import androidx.compose.animation.scaleOut +import androidx.compose.animation.with +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.PageSize +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.foundation.selection.selectable +import androidx.compose.foundation.selection.selectableGroup +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Checkbox +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.RadioButton +import androidx.compose.material3.Slider +import androidx.compose.material3.SliderDefaults +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableLongStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.rotate +import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Outline +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.graphics.PathOperation +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.example.powerplay.engine.AudioForegroundService +import com.example.powerplay.engine.PlayerState +import com.example.powerplay.engine.PowerPlayAudioPlayer +import com.example.powerplay.ui.theme.MusicPlayerTheme + +class MainActivity : ComponentActivity() { + + private lateinit var player: PowerPlayAudioPlayer + private lateinit var serviceIntent: Intent + private var isOffloadSupported: Boolean = false + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setUpPowerPlayAudioPlayer() + + val format = AudioFormat.Builder() + .setEncoding(AudioFormat.ENCODING_PCM_FLOAT) + .setSampleRate(48000) + .setChannelMask(AudioFormat.CHANNEL_OUT_STEREO) + .build() + + val attributes = + AudioAttributes.Builder() + .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC) + .setUsage(AudioAttributes.USAGE_MEDIA) + .build() + + serviceIntent = Intent(this, AudioForegroundService::class.java) + isOffloadSupported = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + AudioManager.isOffloadedPlaybackSupported(format, attributes) + } else { + false + } + + setContent { + MusicPlayerTheme { + Surface( + modifier = Modifier.fillMaxSize(), + color = MaterialTheme.colorScheme.background + ) { + SongScreen() + } + } + } + } + + override fun onDestroy() { + super.onDestroy() + player.stopPlaying(0) + player.teardownAudioStream() + } + + private fun setUpPowerPlayAudioPlayer() { + player = PowerPlayAudioPlayer() + player.setupAudioStream() + } + + /*** + * Brings together all UI elements for the player + */ + @OptIn(ExperimentalAnimationApi::class) + @Preview + @Composable + fun SongScreen() { + val playList = getPlayList() + val pagerState = rememberPagerState(pageCount = { playList.count() }) + val playingSongIndex = remember { + mutableIntStateOf(0) + } + val offload = remember { + mutableIntStateOf(0) + } + + LaunchedEffect(pagerState.currentPage) { + playingSongIndex.intValue = pagerState.currentPage + // player.seekTo(pagerState.currentPage, 0) + } + +// LaunchedEffect(player.currentMediaItemIndex) { +// playingSongIndex.intValue = player.currentMediaItemIndex +// pagerState.animateScrollToPage( +// playingSongIndex.intValue, +// animationSpec = tween(500) +// ) +// } + + LaunchedEffect(Unit) { + playList.forEachIndexed { index, it -> + player.loadFile(assets, it.fileName, index) + player.setLooping(index, true) + } + } + + val isPlaying = remember { + mutableStateOf(false) + } + + val currentPosition = remember { + mutableLongStateOf(0) + } + + val sliderPosition = remember { + mutableLongStateOf(0) + } + + val totalDuration = remember { + mutableLongStateOf(0) + } + + +// LaunchedEffect(key1 = player.currentPosition, key2 = player.isPlaying) { +// delay(1000) +// currentPosition.longValue = player.currentPosition +// } +// +// LaunchedEffect(currentPosition.longValue) { +// sliderPosition.longValue = currentPosition.longValue +// } +// +// LaunchedEffect(player.duration) { +// if (player.duration > 0) { +// totalDuration.longValue = player.duration +// } +// } + + Box( + modifier = Modifier + .fillMaxSize(), contentAlignment = Alignment.Center + ) { + val configuration = LocalConfiguration.current + + Column(horizontalAlignment = Alignment.CenterHorizontally) { + /*** + * Animated texts includes song name and its artist + * Animates when the song is switching + */ + AnimatedContent(targetState = playingSongIndex.intValue, transitionSpec = { + (scaleIn() + fadeIn()) with (scaleOut() + fadeOut()) + }, label = "") { + Text( + text = playList[it].name, fontSize = 24.sp, + color = Color.Black, + style = TextStyle(fontWeight = FontWeight.ExtraBold) + ) + } + Spacer(modifier = Modifier.height(8.dp)) + AnimatedContent(targetState = playingSongIndex.intValue, transitionSpec = { + (scaleIn() + fadeIn()) with (scaleOut() + fadeOut()) + }, label = "") { + Text( + text = playList[it].artist, fontSize = 12.sp, color = Color.Black, + style = TextStyle(fontWeight = FontWeight.Bold) + ) + } + + Spacer(modifier = Modifier.height(16.dp)) + + + /*** + * Includes animated song album cover + */ + HorizontalPager( + modifier = Modifier.fillMaxWidth(), + state = pagerState, + pageSize = PageSize.Fixed((configuration.screenWidthDp / (1.7)).dp), + contentPadding = PaddingValues(horizontal = 85.dp) + ) { page -> + + val painter = painterResource(id = playList[page].cover) + + if (page == pagerState.currentPage) { + VinylAlbumCoverAnimation(isSongPlaying = isPlaying.value, painter = painter) + } else { + VinylAlbumCoverAnimation(isSongPlaying = false, painter = painter) + } + } + Spacer(modifier = Modifier.height(54.dp)) + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 32.dp), + ) { + +// TrackSlider( +// value = sliderPosition.longValue.toFloat(), +// onValueChange = { +// sliderPosition.longValue = it.toLong() +// }, +// onValueChangeFinished = { +// currentPosition.longValue = sliderPosition.longValue +// //player.seekTo(sliderPosition.longValue) +// }, +// songDuration = totalDuration.longValue.toFloat() +// ) +// Row( +// modifier = Modifier.fillMaxWidth(), +// ) { +// +// Text( +// text = (currentPosition.longValue).convertToText(), +// modifier = Modifier +// .weight(1f) +// .padding(8.dp), +// color = Color.Black, +// style = TextStyle(fontWeight = FontWeight.Bold) +// ) +// +// val remainTime = totalDuration.longValue - currentPosition.longValue +// Text( +// text = if (remainTime >= 0) remainTime.convertToText() else "", +// modifier = Modifier +// .padding(8.dp), +// color = Color.Black, +// style = TextStyle(fontWeight = FontWeight.Bold) +// ) +// } + } + Spacer(modifier = Modifier.height(8.dp)) + Text( + "Performance Modes" + ) + Spacer(modifier = Modifier.height(8.dp)) + Column { + val radioOptions = mutableListOf("None", "Low Latency", "Power Saving") + if (isOffloadSupported) radioOptions.add("PCM Offload") + + val (selectedOption, onOptionSelected) = remember { + mutableStateOf(radioOptions[0]) + } + radioOptions.forEachIndexed { index, text -> + Row( + Modifier + .height(32.dp) + .selectable( + selected = (text == selectedOption), + onClick = { + onOptionSelected(text) + offload.intValue = index + player.teardownAudioStream() + }, + role = Role.RadioButton + ) + .padding(horizontal = 4.dp), + verticalAlignment = Alignment.CenterVertically + ) { + RadioButton( + selected = (text == selectedOption), + onClick = null, // null recommended for accessibility with screen readers + enabled = !isPlaying.value + ) + Text( + text = text, + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier.padding(start = 8.dp) + ) + } + } + } + Spacer(modifier = Modifier.height(8.dp)) + Text( + when (offload.intValue) { + 0 -> "Performance Mode: None" + 1 -> "Performance Mode: Low Latency" + 2 -> "Performance Mode: Power Saving" + else -> "Performance Mode: PCM Offload" + } + ) + Spacer(modifier = Modifier.height(24.dp)) + Row( + horizontalArrangement = Arrangement.SpaceEvenly, + verticalAlignment = Alignment.CenterVertically + ) { +// ControlButton(icon = R.drawable.ic_previous, size = 40.dp, onClick = { +// //player.seekToPreviousMediaItem() +// }) + Spacer(modifier = Modifier.width(20.dp)) + ControlButton( + icon = if (isPlaying.value) R.drawable.ic_pause else R.drawable.ic_play, + size = 100.dp, + onClick = { + when (isPlaying.value) { + true -> player.stopPlaying(playingSongIndex.intValue) + false -> player.startPlaying( + playingSongIndex.intValue, + offload.intValue + ) + } + + isPlaying.value = + player.getPlayerStateLive().value == PlayerState.Playing + }) + Spacer(modifier = Modifier.width(20.dp)) +// ControlButton(icon = R.drawable.ic_next, size = 40.dp, onClick = { +// //player.seekToNextMediaItem() +// }) + } + } + } + } + + /** + * Tracks and visualizes the song playing actions. + */ + @Composable + fun TrackSlider( + value: Float, + onValueChange: (newValue: Float) -> Unit, + onValueChangeFinished: () -> Unit, + songDuration: Float + ) { + Slider( + value = value, + onValueChange = { + onValueChange(it) + }, + onValueChangeFinished = { + + onValueChangeFinished() + + }, + valueRange = 0f..songDuration, + colors = SliderDefaults.colors( + thumbColor = Color.Black, + activeTrackColor = Color.DarkGray, + inactiveTrackColor = Color.Gray, + ) + ) + } + + /*** + * Player control button + */ + @Composable + fun ControlButton(icon: Int, size: Dp, onClick: () -> Unit) { + Box( + modifier = Modifier + .size(size) + .clip(CircleShape) + .clickable { + onClick() + }, contentAlignment = Alignment.Center + ) { + Icon( + modifier = Modifier.size(size / 1.5f), + painter = painterResource(id = icon), + tint = Color.Black, + contentDescription = null + ) + } + } + + @Composable + fun VinylAlbumCoverAnimation( + modifier: Modifier = Modifier, + isSongPlaying: Boolean = true, + painter: Painter + ) { + var currentRotation by remember { + mutableFloatStateOf(0f) + } + + val rotation = remember { + Animatable(currentRotation) + } + + LaunchedEffect(isSongPlaying) { + if (isSongPlaying) { + rotation.animateTo( + targetValue = currentRotation + 360f, + animationSpec = infiniteRepeatable( + animation = tween(3000, easing = LinearEasing), + repeatMode = RepeatMode.Restart + ) + ) { + currentRotation = value + } + } else { + if (currentRotation > 0f) { + rotation.animateTo( + targetValue = currentRotation + 50, + animationSpec = tween( + 1250, + easing = LinearOutSlowInEasing + ) + ) { + currentRotation = value + } + } + } + } + + VinylAlbumCover( + painter = painter, + rotationDegrees = rotation.value + ) + } + + @Composable + fun VinylAlbumCover( + modifier: Modifier = Modifier, + rotationDegrees: Float = 0f, + painter: Painter + ) { + + /** + * Creates a custom outline for a rounded shape + */ + val roundedShape = object : Shape { + override fun createOutline( + size: Size, + layoutDirection: LayoutDirection, + density: Density + ): Outline { + val p1 = Path().apply { + addOval(Rect(4f, 3f, size.width - 1, size.height - 1)) + } + val thickness = size.height / 2.10f + val p2 = Path().apply { + addOval( + Rect( + thickness, + thickness, + size.width - thickness, + size.height - thickness + ) + ) + } + val p3 = Path() + p3.op(p1, p2, PathOperation.Difference) + + return Outline.Generic(p3) + } + } + + /** + * Container defining the layout for a vinyl-themed UI element. + */ + Box( + modifier = modifier + .aspectRatio(1.0f) + .clip(roundedShape) + ) { + + /** + * Vinyl background image + */ + Image( + modifier = Modifier + .fillMaxSize() + .rotate(rotationDegrees), + painter = painterResource(id = R.drawable.vinyl_background), + contentDescription = "Vinyl Background" + ) + + + /** + * Song album cover image overlaid on the vinyl background image + */ + Image( + modifier = Modifier + .fillMaxSize(0.5f) + .rotate(rotationDegrees) + .aspectRatio(1.0f) + .align(Alignment.Center) + .clip(roundedShape), + painter = painter, + contentDescription = "Song cover" + ) + } + } + + /*** + * Convert the millisecond to String text + */ + private fun Long.convertToText(): String { + val sec = this / 1000 + val minutes = sec / 60 + val seconds = sec % 60 + + val minutesString = if (minutes < 10) { + "0$minutes" + } else { + minutes.toString() + } + val secondsString = if (seconds < 10) { + "0$seconds" + } else { + seconds.toString() + } + return "$minutesString:$secondsString" + } + + + /*** + * Return a play list of type Music data class + */ + private fun getPlayList(): List<Music> { + return listOf( + Music( + name = "Chemical Reaction", + artist = "Momo Oboe", + cover = R.drawable.album_art_1, + fileName = "song1.wav", + ), + ) + } + + /*** + * Data class to represent a music in the list + */ + data class Music( + val name: String, + val artist: String, + val fileName: String, + val cover: Int, + ) +} \ No newline at end of file diff --git a/samples/powerplay/src/main/kotlin/com/example/powerplay/engine/AudioForegroundService.kt b/samples/powerplay/src/main/kotlin/com/example/powerplay/engine/AudioForegroundService.kt new file mode 100644 index 000000000..e18832a25 --- /dev/null +++ b/samples/powerplay/src/main/kotlin/com/example/powerplay/engine/AudioForegroundService.kt @@ -0,0 +1,104 @@ +package com.example.powerplay.engine + +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.app.Service +import android.content.Context +import android.content.Intent +import android.media.AudioAttributes +import android.media.AudioFocusRequest +import android.media.AudioManager +import android.os.IBinder +import androidx.core.app.NotificationCompat +import com.example.powerplay.MainActivity +import com.example.powerplay.R + +class AudioForegroundService : Service() { + + private lateinit var audioManager: AudioManager + private lateinit var audioFocusRequest: AudioFocusRequest + private val audioFocusChangeListener = AudioManager.OnAudioFocusChangeListener { focusChange -> + when (focusChange) { + AudioManager.AUDIOFOCUS_GAIN -> { + + } + + AudioManager.AUDIOFOCUS_LOSS, AudioManager.AUDIOFOCUS_LOSS_TRANSIENT, AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> { + } + } + } + + override fun onCreate() { + super.onCreate() + audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager + val audioAttributes = AudioAttributes.Builder() + .setUsage(AudioAttributes.USAGE_MEDIA) + .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC) + .build() + + audioFocusRequest = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN) + .setAudioAttributes(audioAttributes) + .setAcceptsDelayedFocusGain(true) + .setOnAudioFocusChangeListener(audioFocusChangeListener) + .build() + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + val notification = createNotification() + startForeground(NOTIFICATION_ID, notification) + + val result = audioManager.requestAudioFocus(audioFocusRequest) + if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { + // Start playback only when audio focus is granted + // ... + } + + return START_STICKY + } + + override fun onDestroy() { + super.onDestroy() + audioManager.abandonAudioFocusRequest(audioFocusRequest) + } + + private fun createNotification(): Notification { + val channelId = createNotificationChannel() + val notificationIntent = + Intent(this, MainActivity::class.java) // Replace with your main activity + val pendingIntent = PendingIntent.getActivity( + this, + 0, + notificationIntent, + PendingIntent.FLAG_IMMUTABLE + ) + + return NotificationCompat.Builder(this, channelId) + .setContentTitle("Audio Playing") + .setContentText("Audio is playing in the background") + .setSmallIcon(R.drawable.ic_play) // Replace with your icon + .setContentIntent(pendingIntent) + .setPriority(NotificationCompat.PRIORITY_LOW) + .build() + } + + private fun createNotificationChannel(): String { + val channelId = "audio_playback_channel" + val channelName = "Audio Playback" + val importance = NotificationManager.IMPORTANCE_LOW + val channel = NotificationChannel(channelId, channelName, importance) + val notificationManager = + getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + notificationManager.createNotificationChannel(channel) + return channelId + } + + override fun onBind(intent: Intent?): IBinder? { + return null // Not using binding in this example + } + + companion object { + private const val NOTIFICATION_ID = 1 + } +} \ No newline at end of file diff --git a/samples/powerplay/src/main/kotlin/com/example/powerplay/engine/PowerPlayAudioPlayer.kt b/samples/powerplay/src/main/kotlin/com/example/powerplay/engine/PowerPlayAudioPlayer.kt new file mode 100644 index 000000000..522742fb2 --- /dev/null +++ b/samples/powerplay/src/main/kotlin/com/example/powerplay/engine/PowerPlayAudioPlayer.kt @@ -0,0 +1,108 @@ +package com.example.powerplay.engine + +import android.content.res.AssetManager +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.asLiveData +import com.example.powerplay.MainActivity +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import kotlinx.coroutines.plus + +class PowerPlayAudioPlayer() : DefaultLifecycleObserver { + /** + * + */ + private var _playerState = MutableStateFlow<PlayerState>(PlayerState.NoResultYet) + fun getPlayerStateLive() = _playerState.asLiveData() + + /** + * Native passthrough functions + */ + fun setupAudioStream() { + setupAudioStreamNative(NUM_PLAY_CHANNELS) + + // TODO - Handle real response from native code + _playerState.update { PlayerState.Initialized } + } + + fun startPlaying(index: Int, offload: Int) { + startPlayingNative(index, offload) + + // TODO - Handle real response from native code + _playerState.update { PlayerState.Playing } + } + + fun stopPlaying(index: Int) { + stopPlayingNative(index) + + // TODO - Handle real response from native code + _playerState.update { PlayerState.Stopped } + } + + fun setLooping(index: Int, looping: Boolean) = setLoopingNative(index, looping) + fun teardownAudioStream() = teardownAudioStreamNative() + fun unloadAssets() = unloadAssetsNative() + + /** + * Loads the file into memory + */ + fun loadFile(assetMgr: AssetManager, filename: String, id: Int) { + val assetFD = assetMgr.openFd(filename) + val stream = assetFD.createInputStream() + val len = assetFD.getLength().toInt() + val bytes = ByteArray(len) + + stream.read(bytes, 0, len) + loadAssetNative(bytes, id) + assetFD.close() + } + + /** + * Native functions. + * Load the library containing the native code including the JNI functions. + */ + init { + System.loadLibrary("powerplay") + } + + private external fun setupAudioStreamNative(numChannels: Int) + private external fun startAudioStreamNative(): Int + private external fun teardownAudioStreamNative(): Int + private external fun loadAssetNative(wavBytes: ByteArray, index: Int) + private external fun unloadAssetsNative() + private external fun getOutputResetNative(): Boolean + private external fun clearOutputResetNative() + private external fun setLoopingNative(index: Int, looping: Boolean) + private external fun startPlayingNative(index: Int, mode: Int) + private external fun stopPlayingNative(index: Int) + + /** + * Companion + */ + companion object { + /** + * Logging Tag + */ + const val TAG: String = "PowerPlayAudioEngine" + + /** + * The number of channels in the player Stream. 2 for Stereo Playback, set to 1 for Mono playback. + */ + const val NUM_PLAY_CHANNELS: Int = 2 + } +} + +sealed interface PlayerState { + object NoResultYet : PlayerState + object Initialized : PlayerState + object StreamStarted : PlayerState + object Playing : PlayerState + object Stopped : PlayerState + data class Unknown(val resultCode: Int) : PlayerState +} diff --git a/samples/powerplay/src/main/kotlin/com/example/powerplay/ui/theme/Color.kt b/samples/powerplay/src/main/kotlin/com/example/powerplay/ui/theme/Color.kt new file mode 100644 index 000000000..fecb91ad1 --- /dev/null +++ b/samples/powerplay/src/main/kotlin/com/example/powerplay/ui/theme/Color.kt @@ -0,0 +1,21 @@ +package com.example.powerplay.ui.theme + +import androidx.compose.ui.graphics.Color + +val Purple80 = Color(0xFFD0BCFF) +val PurpleGrey80 = Color(0xFFCCC2DC) +val Pink80 = Color(0xFFEFB8C8) + +val Purple40 = Color(0xFF6650a4) +val PurpleGrey40 = Color(0xFF625b71) +val Pink40 = Color(0xFF7D5260) + +val md_theme_light_primary = Color(0xFF476810) +val md_theme_light_onPrimary = Color(0xFFFFFFFF) +val md_theme_light_primaryContainer = Color(0xFFC7F089) +// .. +// .. + +val md_theme_dark_primary = Color(0xFFACD370) +val md_theme_dark_onPrimary = Color(0xFF213600) +val md_theme_dark_primaryContainer = Color(0xFF324F00) \ No newline at end of file diff --git a/samples/powerplay/src/main/kotlin/com/example/powerplay/ui/theme/Shape.kt b/samples/powerplay/src/main/kotlin/com/example/powerplay/ui/theme/Shape.kt new file mode 100644 index 000000000..f40fc87c2 --- /dev/null +++ b/samples/powerplay/src/main/kotlin/com/example/powerplay/ui/theme/Shape.kt @@ -0,0 +1,11 @@ +package com.example.powerplay.ui.theme + +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Shapes +import androidx.compose.ui.unit.dp + +val Shapes = Shapes( + small = RoundedCornerShape(4.dp), + medium = RoundedCornerShape(4.dp), + large = RoundedCornerShape(0.dp) +) diff --git a/samples/powerplay/src/main/kotlin/com/example/powerplay/ui/theme/Theme.kt b/samples/powerplay/src/main/kotlin/com/example/powerplay/ui/theme/Theme.kt new file mode 100644 index 000000000..7de95ae3c --- /dev/null +++ b/samples/powerplay/src/main/kotlin/com/example/powerplay/ui/theme/Theme.kt @@ -0,0 +1,18 @@ +package com.example.powerplay.ui.theme + +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable + +private val LightColorScheme = lightColorScheme( + primary = md_theme_light_primary, + onPrimary = md_theme_light_onPrimary, + primaryContainer = md_theme_light_primaryContainer, +) + +@Composable +fun MusicPlayerTheme(content: @Composable () -> Unit) { + MaterialTheme( + colorScheme = LightColorScheme, typography = Typography, content = content + ) +} \ No newline at end of file diff --git a/samples/powerplay/src/main/kotlin/com/example/powerplay/ui/theme/Type.kt b/samples/powerplay/src/main/kotlin/com/example/powerplay/ui/theme/Type.kt new file mode 100644 index 000000000..c4753cb06 --- /dev/null +++ b/samples/powerplay/src/main/kotlin/com/example/powerplay/ui/theme/Type.kt @@ -0,0 +1,17 @@ +package com.example.powerplay.ui.theme + +import androidx.compose.material3.Typography +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp + +val Typography = Typography( + bodyLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.5.sp + ) +) \ No newline at end of file diff --git a/samples/powerplay/src/main/res/drawable-v24/ic_launcher_foreground.xml b/samples/powerplay/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 000000000..2b068d114 --- /dev/null +++ b/samples/powerplay/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:aapt="http://schemas.android.com/aapt" + android:width="108dp" + android:height="108dp" + android:viewportWidth="108" + android:viewportHeight="108"> + <path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z"> + <aapt:attr name="android:fillColor"> + <gradient + android:endX="85.84757" + android:endY="92.4963" + android:startX="42.9492" + android:startY="49.59793" + android:type="linear"> + <item + android:color="#44000000" + android:offset="0.0" /> + <item + android:color="#00000000" + android:offset="1.0" /> + </gradient> + </aapt:attr> + </path> + <path + android:fillColor="#FFFFFF" + android:fillType="nonZero" + android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z" + android:strokeWidth="1" + android:strokeColor="#00000000" /> +</vector> \ No newline at end of file diff --git a/samples/powerplay/src/main/res/drawable/album_art_1.jpeg b/samples/powerplay/src/main/res/drawable/album_art_1.jpeg new file mode 100644 index 000000000..6b5a77624 Binary files /dev/null and b/samples/powerplay/src/main/res/drawable/album_art_1.jpeg differ diff --git a/samples/powerplay/src/main/res/drawable/ic_launcher_background.xml b/samples/powerplay/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 000000000..07d5da9cb --- /dev/null +++ b/samples/powerplay/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ +<?xml version="1.0" encoding="utf-8"?> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="108dp" + android:height="108dp" + android:viewportWidth="108" + android:viewportHeight="108"> + <path + android:fillColor="#3DDC84" + android:pathData="M0,0h108v108h-108z" /> + <path + android:fillColor="#00000000" + android:pathData="M9,0L9,108" + android:strokeWidth="0.8" + android:strokeColor="#33FFFFFF" /> + <path + android:fillColor="#00000000" + android:pathData="M19,0L19,108" + android:strokeWidth="0.8" + android:strokeColor="#33FFFFFF" /> + <path + android:fillColor="#00000000" + android:pathData="M29,0L29,108" + android:strokeWidth="0.8" + android:strokeColor="#33FFFFFF" /> + <path + android:fillColor="#00000000" + android:pathData="M39,0L39,108" + android:strokeWidth="0.8" + android:strokeColor="#33FFFFFF" /> + <path + android:fillColor="#00000000" + android:pathData="M49,0L49,108" + android:strokeWidth="0.8" + android:strokeColor="#33FFFFFF" /> + <path + android:fillColor="#00000000" + android:pathData="M59,0L59,108" + android:strokeWidth="0.8" + android:strokeColor="#33FFFFFF" /> + <path + android:fillColor="#00000000" + android:pathData="M69,0L69,108" + android:strokeWidth="0.8" + android:strokeColor="#33FFFFFF" /> + <path + android:fillColor="#00000000" + android:pathData="M79,0L79,108" + android:strokeWidth="0.8" + android:strokeColor="#33FFFFFF" /> + <path + android:fillColor="#00000000" + android:pathData="M89,0L89,108" + android:strokeWidth="0.8" + android:strokeColor="#33FFFFFF" /> + <path + android:fillColor="#00000000" + android:pathData="M99,0L99,108" + android:strokeWidth="0.8" + android:strokeColor="#33FFFFFF" /> + <path + android:fillColor="#00000000" + android:pathData="M0,9L108,9" + android:strokeWidth="0.8" + android:strokeColor="#33FFFFFF" /> + <path + android:fillColor="#00000000" + android:pathData="M0,19L108,19" + android:strokeWidth="0.8" + android:strokeColor="#33FFFFFF" /> + <path + android:fillColor="#00000000" + android:pathData="M0,29L108,29" + android:strokeWidth="0.8" + android:strokeColor="#33FFFFFF" /> + <path + android:fillColor="#00000000" + android:pathData="M0,39L108,39" + android:strokeWidth="0.8" + android:strokeColor="#33FFFFFF" /> + <path + android:fillColor="#00000000" + android:pathData="M0,49L108,49" + android:strokeWidth="0.8" + android:strokeColor="#33FFFFFF" /> + <path + android:fillColor="#00000000" + android:pathData="M0,59L108,59" + android:strokeWidth="0.8" + android:strokeColor="#33FFFFFF" /> + <path + android:fillColor="#00000000" + android:pathData="M0,69L108,69" + android:strokeWidth="0.8" + android:strokeColor="#33FFFFFF" /> + <path + android:fillColor="#00000000" + android:pathData="M0,79L108,79" + android:strokeWidth="0.8" + android:strokeColor="#33FFFFFF" /> + <path + android:fillColor="#00000000" + android:pathData="M0,89L108,89" + android:strokeWidth="0.8" + android:strokeColor="#33FFFFFF" /> + <path + android:fillColor="#00000000" + android:pathData="M0,99L108,99" + android:strokeWidth="0.8" + android:strokeColor="#33FFFFFF" /> + <path + android:fillColor="#00000000" + android:pathData="M19,29L89,29" + android:strokeWidth="0.8" + android:strokeColor="#33FFFFFF" /> + <path + android:fillColor="#00000000" + android:pathData="M19,39L89,39" + android:strokeWidth="0.8" + android:strokeColor="#33FFFFFF" /> + <path + android:fillColor="#00000000" + android:pathData="M19,49L89,49" + android:strokeWidth="0.8" + android:strokeColor="#33FFFFFF" /> + <path + android:fillColor="#00000000" + android:pathData="M19,59L89,59" + android:strokeWidth="0.8" + android:strokeColor="#33FFFFFF" /> + <path + android:fillColor="#00000000" + android:pathData="M19,69L89,69" + android:strokeWidth="0.8" + android:strokeColor="#33FFFFFF" /> + <path + android:fillColor="#00000000" + android:pathData="M19,79L89,79" + android:strokeWidth="0.8" + android:strokeColor="#33FFFFFF" /> + <path + android:fillColor="#00000000" + android:pathData="M29,19L29,89" + android:strokeWidth="0.8" + android:strokeColor="#33FFFFFF" /> + <path + android:fillColor="#00000000" + android:pathData="M39,19L39,89" + android:strokeWidth="0.8" + android:strokeColor="#33FFFFFF" /> + <path + android:fillColor="#00000000" + android:pathData="M49,19L49,89" + android:strokeWidth="0.8" + android:strokeColor="#33FFFFFF" /> + <path + android:fillColor="#00000000" + android:pathData="M59,19L59,89" + android:strokeWidth="0.8" + android:strokeColor="#33FFFFFF" /> + <path + android:fillColor="#00000000" + android:pathData="M69,19L69,89" + android:strokeWidth="0.8" + android:strokeColor="#33FFFFFF" /> + <path + android:fillColor="#00000000" + android:pathData="M79,19L79,89" + android:strokeWidth="0.8" + android:strokeColor="#33FFFFFF" /> +</vector> diff --git a/samples/powerplay/src/main/res/drawable/ic_next.png b/samples/powerplay/src/main/res/drawable/ic_next.png new file mode 100755 index 000000000..97e93cc4c Binary files /dev/null and b/samples/powerplay/src/main/res/drawable/ic_next.png differ diff --git a/samples/powerplay/src/main/res/drawable/ic_pause.png b/samples/powerplay/src/main/res/drawable/ic_pause.png new file mode 100755 index 000000000..3c877cc4e Binary files /dev/null and b/samples/powerplay/src/main/res/drawable/ic_pause.png differ diff --git a/samples/powerplay/src/main/res/drawable/ic_play.png b/samples/powerplay/src/main/res/drawable/ic_play.png new file mode 100755 index 000000000..82d116f81 Binary files /dev/null and b/samples/powerplay/src/main/res/drawable/ic_play.png differ diff --git a/samples/powerplay/src/main/res/drawable/ic_previous.png b/samples/powerplay/src/main/res/drawable/ic_previous.png new file mode 100755 index 000000000..5d1ce27ac Binary files /dev/null and b/samples/powerplay/src/main/res/drawable/ic_previous.png differ diff --git a/samples/powerplay/src/main/res/drawable/vinyl_background.png b/samples/powerplay/src/main/res/drawable/vinyl_background.png new file mode 100755 index 000000000..bf03bcca6 Binary files /dev/null and b/samples/powerplay/src/main/res/drawable/vinyl_background.png differ diff --git a/samples/powerplay/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/samples/powerplay/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 000000000..eca70cfe5 --- /dev/null +++ b/samples/powerplay/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> + <background android:drawable="@drawable/ic_launcher_background" /> + <foreground android:drawable="@drawable/ic_launcher_foreground" /> +</adaptive-icon> \ No newline at end of file diff --git a/samples/powerplay/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/samples/powerplay/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 000000000..eca70cfe5 --- /dev/null +++ b/samples/powerplay/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> + <background android:drawable="@drawable/ic_launcher_background" /> + <foreground android:drawable="@drawable/ic_launcher_foreground" /> +</adaptive-icon> \ No newline at end of file diff --git a/samples/powerplay/src/main/res/mipmap-hdpi/ic_launcher.webp b/samples/powerplay/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 000000000..c209e78ec Binary files /dev/null and b/samples/powerplay/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/samples/powerplay/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/samples/powerplay/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 000000000..b2dfe3d1b Binary files /dev/null and b/samples/powerplay/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/samples/powerplay/src/main/res/mipmap-mdpi/ic_launcher.webp b/samples/powerplay/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 000000000..4f0f1d64e Binary files /dev/null and b/samples/powerplay/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/samples/powerplay/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/samples/powerplay/src/main/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 000000000..62b611da0 Binary files /dev/null and b/samples/powerplay/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/samples/powerplay/src/main/res/mipmap-xhdpi/ic_launcher.webp b/samples/powerplay/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 000000000..948a3070f Binary files /dev/null and b/samples/powerplay/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/samples/powerplay/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/samples/powerplay/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 000000000..1b9a6956b Binary files /dev/null and b/samples/powerplay/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/samples/powerplay/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/samples/powerplay/src/main/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 000000000..28d4b77f9 Binary files /dev/null and b/samples/powerplay/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/samples/powerplay/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/samples/powerplay/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 000000000..9287f5083 Binary files /dev/null and b/samples/powerplay/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/samples/powerplay/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/samples/powerplay/src/main/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 000000000..aa7d6427e Binary files /dev/null and b/samples/powerplay/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/samples/powerplay/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/samples/powerplay/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 000000000..9126ae37c Binary files /dev/null and b/samples/powerplay/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/samples/powerplay/src/main/res/values/colors.xml b/samples/powerplay/src/main/res/values/colors.xml new file mode 100644 index 000000000..ca1931bca --- /dev/null +++ b/samples/powerplay/src/main/res/values/colors.xml @@ -0,0 +1,10 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <color name="purple_200">#FFBB86FC</color> + <color name="purple_500">#FF6200EE</color> + <color name="purple_700">#FF3700B3</color> + <color name="teal_200">#FF03DAC5</color> + <color name="teal_700">#FF018786</color> + <color name="black">#FF000000</color> + <color name="white">#FFFFFFFF</color> +</resources> diff --git a/samples/powerplay/src/main/res/values/strings.xml b/samples/powerplay/src/main/res/values/strings.xml new file mode 100644 index 000000000..3cde2fc60 --- /dev/null +++ b/samples/powerplay/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ +<resources> + <string name="app_name">PowerPlay</string> +</resources> diff --git a/samples/powerplay/src/main/res/values/themes.xml b/samples/powerplay/src/main/res/values/themes.xml new file mode 100644 index 000000000..0cbbe65ab --- /dev/null +++ b/samples/powerplay/src/main/res/values/themes.xml @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + + <style name="Theme.Samples" parent="android:Theme.Material.Light.NoActionBar"> + <item name="android:statusBarColor">@color/purple_700</item> + </style> +</resources> diff --git a/samples/settings.gradle b/samples/settings.gradle index ddec58ebb..a7711210f 100644 --- a/samples/settings.gradle +++ b/samples/settings.gradle @@ -25,3 +25,4 @@ include ':drumthumper' include ':parselib' include ':iolib' include ':minimaloboe' +include 'powerplay' \ No newline at end of file diff --git a/src/aaudio/AAudioLoader.cpp b/src/aaudio/AAudioLoader.cpp index 80e45a885..ae79d4f6d 100644 --- a/src/aaudio/AAudioLoader.cpp +++ b/src/aaudio/AAudioLoader.cpp @@ -559,7 +559,7 @@ AAudioLoader::signature_I_PSPIPI AAudioLoader::load_I_PSPIPI(const char *functio // The aaudio device type and aaudio policy were added in NDK 28, // which is the first version to support Android W (API 36). -#if __NDK_MAJOR__ >= 29 +#if __NDK_MAJOR__ >= 30 ASSERT_INT32(AAudio_DeviceType); static_assert((int32_t)DeviceType::BuiltinEarpiece == AAUDIO_DEVICE_BUILTIN_EARPIECE, ERRMSG); diff --git a/src/aaudio/AAudioLoader.h b/src/aaudio/AAudioLoader.h index 0e77613ff..9dc2fb14d 100644 --- a/src/aaudio/AAudioLoader.h +++ b/src/aaudio/AAudioLoader.h @@ -106,7 +106,7 @@ typedef void (*AAudioStream_presentationEndCallback)( #define __ANDROID_API_B__ 36 #endif -#if OBOE_USING_NDK && __NDK_MAJOR__ < 29 +#if OBOE_USING_NDK && __NDK_MAJOR__ < 30 // These were defined in Android B typedef int32_t AAudio_DeviceType; typedef int32_t aaudio_policy_t;