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
+![minimaloboe-screenshot](minimaloboe-screenshot.png)
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;