Merge branch 'master' into dturner-patch-1
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 4f65c2a..533ada1 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -18,6 +18,8 @@
         src/opensles/EngineOpenSLES.cpp
         src/opensles/OpenSLESUtilities.cpp
         src/opensles/OutputMixerOpenSLES.cpp
+        src/common/StabilizedCallback.cpp
+		src/common/Trace.cpp
         )
 
 add_library(oboe STATIC ${oboe_sources})
diff --git a/apps/OboeTester/app/src/main/cpp/AudioProcessorBase.cpp b/apps/OboeTester/app/src/main/cpp/AudioProcessorBase.cpp
index ebb1d9d..3d9d012 100644
--- a/apps/OboeTester/app/src/main/cpp/AudioProcessorBase.cpp
+++ b/apps/OboeTester/app/src/main/cpp/AudioProcessorBase.cpp
@@ -32,7 +32,7 @@
 AudioFloatPort::AudioFloatPort(AudioProcessorBase &parent, int samplesPerFrame)
         : AudioPort(parent, samplesPerFrame), mFloatBuffer(NULL) {
     int numFloats = MAX_BLOCK_SIZE * mSamplesPerFrame;
-    mFloatBuffer = new float[numFloats];
+    mFloatBuffer = new float[numFloats]{};
 }
 
 AudioFloatPort::~AudioFloatPort() {
diff --git a/apps/OboeTester/app/src/main/cpp/FifoProcessor.h b/apps/OboeTester/app/src/main/cpp/FifoProcessor.h
index 37de256..a562b79 100644
--- a/apps/OboeTester/app/src/main/cpp/FifoProcessor.h
+++ b/apps/OboeTester/app/src/main/cpp/FifoProcessor.h
@@ -44,7 +44,7 @@
 
     AudioResult onProcess(
             uint64_t framePosition,
-            int numFrames) {
+            int numFrames)  override {
         float *buffer = output.getFloatBuffer(numFrames);
         return mFifoBuffer.readNow(buffer, numFrames);
     }
diff --git a/apps/OboeTester/app/src/main/cpp/ManyToMultiConverter.cpp b/apps/OboeTester/app/src/main/cpp/ManyToMultiConverter.cpp
new file mode 100644
index 0000000..ba5ad9f
--- /dev/null
+++ b/apps/OboeTester/app/src/main/cpp/ManyToMultiConverter.cpp
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <unistd.h>
+#include "AudioProcessorBase.h"
+#include "ManyToMultiConverter.h"
+
+ManyToMultiConverter::ManyToMultiConverter(int32_t channelCount)
+        : inputs(channelCount)
+        , output(*this, channelCount) {
+    for (int i = 0; i < channelCount; i++) {
+        inputs[i] = std::make_unique<AudioInputPort>(*this, 1);
+    }
+}
+
+AudioResult ManyToMultiConverter::onProcess(
+        uint64_t framePosition,
+        int numFrames) {
+    int32_t channelCount = output.getSamplesPerFrame();
+
+    for (int i = 0; i < channelCount; i++) {
+        inputs[i]->pullData(framePosition, numFrames);
+    }
+
+
+    for (int ch = 0; ch < channelCount; ch++) {
+        const float *inputBuffer = inputs[ch]->getFloatBuffer(numFrames);
+        float *outputBuffer = output.getFloatBuffer(numFrames) + ch;
+
+        for (int i = 0; i < numFrames; i++) {
+            // read one, write into the proper interleaved output channel
+            float sample = *inputBuffer++;
+            *outputBuffer = sample;
+            outputBuffer += channelCount; // advance to next multichannel frame
+        }
+    }
+    return AUDIO_RESULT_SUCCESS;
+}
+
diff --git a/apps/OboeTester/app/src/main/cpp/ManyToMultiConverter.h b/apps/OboeTester/app/src/main/cpp/ManyToMultiConverter.h
new file mode 100644
index 0000000..3cdf376
--- /dev/null
+++ b/apps/OboeTester/app/src/main/cpp/ManyToMultiConverter.h
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef OBOETESTER_MANY_TO_MULTI_CONVERTER_H
+#define OBOETESTER_MANY_TO_MULTI_CONVERTER_H
+
+#include <unistd.h>
+#include <sys/types.h>
+#include <vector>
+
+#include "AudioProcessorBase.h"
+
+/**
+ * Combine multiple mono inputs into one multi-channel output.
+ */
+class ManyToMultiConverter : public AudioProcessorBase {
+public:
+    explicit ManyToMultiConverter(int32_t channelCount);
+
+    virtual ~ManyToMultiConverter() = default;
+
+    AudioResult onProcess(
+            uint64_t framePosition,
+            int numFrames) override;
+
+    void setEnabled(bool enabled) {};
+
+    std::vector<std::unique_ptr<AudioInputPort> > inputs;
+    AudioOutputPort output;
+
+private:
+};
+
+#endif //OBOETESTER_MANY_TO_MULTI_CONVERTER_H
diff --git a/apps/OboeTester/app/src/main/cpp/MonoToMultiConverter.cpp b/apps/OboeTester/app/src/main/cpp/MonoToMultiConverter.cpp
index 0f761dc..53746b4 100644
--- a/apps/OboeTester/app/src/main/cpp/MonoToMultiConverter.cpp
+++ b/apps/OboeTester/app/src/main/cpp/MonoToMultiConverter.cpp
@@ -23,8 +23,6 @@
         , output(*this, channelCount) {
 }
 
-MonoToMultiConverter::~MonoToMultiConverter() { }
-
 AudioResult MonoToMultiConverter::onProcess(
         uint64_t framePosition,
         int numFrames) {
diff --git a/apps/OboeTester/app/src/main/cpp/MonoToMultiConverter.h b/apps/OboeTester/app/src/main/cpp/MonoToMultiConverter.h
index 6bae467..9132fa1 100644
--- a/apps/OboeTester/app/src/main/cpp/MonoToMultiConverter.h
+++ b/apps/OboeTester/app/src/main/cpp/MonoToMultiConverter.h
@@ -14,23 +14,26 @@
  * limitations under the License.
  */
 
-#ifndef NATIVEOBOE_MONO_TO_MULTI_CONVERTER_H
-#define NATIVEOBOE_MONO_TO_MULTI_CONVERTER_H
+#ifndef OBOETESTER_MONO_TO_MULTI_CONVERTER_H
+#define OBOETESTER_MONO_TO_MULTI_CONVERTER_H
 
 #include <unistd.h>
 #include <sys/types.h>
 
 #include "AudioProcessorBase.h"
 
+/**
+ * Duplicate a single mono input across multiple channels of output.
+ */
 class MonoToMultiConverter : AudioProcessorBase {
 public:
     explicit MonoToMultiConverter(int32_t channelCount);
 
-    virtual ~MonoToMultiConverter();
+    virtual ~MonoToMultiConverter() = default;
 
     AudioResult onProcess(
             uint64_t framePosition,
-            int numFrames);
+            int numFrames) override;
 
     void setEnabled(bool enabled) {};
 
@@ -40,5 +43,4 @@
 private:
 };
 
-
-#endif //NATIVEOBOE_MONO_TO_MULTI_CONVERTER_H
+#endif //OBOETESTER_MONO_TO_MULTI_CONVERTER_H
diff --git a/apps/OboeTester/app/src/main/cpp/NativeAudioContext.cpp b/apps/OboeTester/app/src/main/cpp/NativeAudioContext.cpp
index 93d573e..207c636 100644
--- a/apps/OboeTester/app/src/main/cpp/NativeAudioContext.cpp
+++ b/apps/OboeTester/app/src/main/cpp/NativeAudioContext.cpp
@@ -18,6 +18,11 @@
 
 #define SECONDS_TO_RECORD   10
 
+
+NativeAudioContext::NativeAudioContext()
+    : sineGenerators(MAX_SINE_OSCILLATORS) {
+}
+
 void NativeAudioContext::close() {
     stopBlockingIOThread();
 
@@ -26,8 +31,9 @@
     }
     delete oboeStream;
     oboeStream = nullptr;
-    delete monoToMulti;
-    monoToMulti = nullptr;
+    manyToMulti.reset(nullptr);
+    monoToMulti.reset(nullptr);
+    audioStreamGateway.reset(nullptr);
 }
 
 bool NativeAudioContext::isMMapUsed() {
@@ -58,18 +64,34 @@
         LOGI("%s() mToneType = %d", __func__, mToneType);
         switch (mToneType) {
             case ToneType::SawPing:
-                sawPingGenerator.output.connect(&monoToMulti->input);
+                sawPingGenerator.output.connect(&(monoToMulti->input));
+                monoToMulti->output.connect(&(audioStreamGateway.get()->input));
                 break;
             case ToneType::Sine:
-                sineGenerator.output.connect(&monoToMulti->input);
+                for (int i = 0; i < mChannelCount; i++) {
+                    sineGenerators[i].output.connect(manyToMulti->inputs[i].get());
+                }
+                manyToMulti->output.connect(&(audioStreamGateway.get()->input));
                 break;
             case ToneType::Impulse:
-                impulseGenerator.output.connect(&monoToMulti->input);
+                impulseGenerator.output.connect(&(monoToMulti->input));
+                monoToMulti->output.connect(&(audioStreamGateway.get()->input));
                 break;
         }
     }
 }
 
+void NativeAudioContext::setChannelEnabled(int channelIndex, bool enabled) {
+    if (manyToMulti == nullptr) {
+        return;
+    }
+    if (enabled) {
+        sineGenerators[channelIndex].output.connect(manyToMulti->inputs[channelIndex].get());
+    } else {
+        manyToMulti->inputs[channelIndex]->disconnect();
+    }
+}
+
 int NativeAudioContext::open(jint sampleRate,
          jint channelCount,
          jint format,
@@ -128,10 +150,13 @@
                                                                  SECONDS_TO_RECORD * mSampleRate);
             mInputAnalyzer.setRecording(mRecording.get());
         } else {
-
-            sineGenerator.setSampleRate(oboeStream->getSampleRate());
-            sineGenerator.frequency.setValue(440.0);
-            sineGenerator.amplitude.setValue(AMPLITUDE_SINE);
+            double frequency = 440.0;
+            for (int i = 0; i < mChannelCount; i++) {
+                sineGenerators[i].setSampleRate(oboeStream->getSampleRate());
+                sineGenerators[i].frequency.setValue(frequency);
+                frequency *= 4.0 / 3.0; // each sine is a higher frequency
+                sineGenerators[i].amplitude.setValue(AMPLITUDE_SINE);
+            }
 
             impulseGenerator.setSampleRate(oboeStream->getSampleRate());
             impulseGenerator.frequency.setValue(440.0);
@@ -141,20 +166,19 @@
             sawPingGenerator.frequency.setValue(FREQUENCY_SAW_PING);
             sawPingGenerator.amplitude.setValue(AMPLITUDE_SAW_PING);
 
-            monoToMulti = new MonoToMultiConverter(mChannelCount);
-            connectTone();
+            manyToMulti = std::make_unique<ManyToMultiConverter>(mChannelCount);
+            monoToMulti = std::make_unique<MonoToMultiConverter>(mChannelCount);
 
             // We needed the proxy because we did not know the channelCount
             // when we setup the Builder.
             audioStreamGateway = std::make_unique<AudioStreamGateway>(mChannelCount);
 
+            connectTone();
+
             if (useCallback) {
                 oboeCallbackProxy.setCallback(audioStreamGateway.get());
             }
 
-            // Input will get connected by setToneType()
-            monoToMulti->output.connect(&(audioStreamGateway.get()->input));
-
             // Set starting size of buffer.
             constexpr int kDefaultNumBursts = 2; // "double buffer"
             int32_t numBursts = kDefaultNumBursts;
diff --git a/apps/OboeTester/app/src/main/cpp/NativeAudioContext.h b/apps/OboeTester/app/src/main/cpp/NativeAudioContext.h
index ccc9f46..e1324ca 100644
--- a/apps/OboeTester/app/src/main/cpp/NativeAudioContext.h
+++ b/apps/OboeTester/app/src/main/cpp/NativeAudioContext.h
@@ -19,6 +19,7 @@
 
 #include <dlfcn.h>
 #include <thread>
+#include <vector>
 
 #include "common/OboeDebug.h"
 #include "oboe/Oboe.h"
@@ -26,6 +27,7 @@
 #include "AudioStreamGateway.h"
 #include "ImpulseGenerator.h"
 #include "InputStreamCallbackAnalyzer.h"
+#include "ManyToMultiConverter.h"
 #include "MonoToMultiConverter.h"
 #include "MultiChannelRecording.h"
 #include "OboeStreamCallbackProxy.h"
@@ -33,8 +35,9 @@
 #include "SawPingGenerator.h"
 #include "SineGenerator.h"
 
+#define MAX_SINE_OSCILLATORS     8
 #define AMPLITUDE_SINE           1.0
-#define FREQUENCY_SAW_PING       1200.0
+#define FREQUENCY_SAW_PING       800.0
 #define AMPLITUDE_SAW_PING       1.0
 #define AMPLITUDE_IMPULSE        0.7
 
@@ -53,6 +56,8 @@
 class NativeAudioContext {
 public:
 
+    NativeAudioContext();
+
     void close();
 
     bool isMMapUsed();
@@ -121,7 +126,9 @@
         oboe::Result result1 = stopPlayback();
         oboe::Result result2 = stopAudio();
 
-        sineGenerator.stop();
+        for (int i = 0; i < mChannelCount; i++) {
+            sineGenerators[i].stop();
+        }
         impulseGenerator.stop();
         sawPingGenerator.stop();
         if (audioStreamGateway != nullptr) {
@@ -137,8 +144,8 @@
         builder.setChannelCount(mChannelCount)
                 ->setSampleRate(mSampleRate)
                 ->setFormat(oboe::AudioFormat::Float)
-                ->setCallback(&mPlayRecordingCallback);
-          //      ->setAudioApi(oboe::AudioApi::OpenSLES);
+                ->setCallback(&mPlayRecordingCallback)
+                ->setAudioApi(oboe::AudioApi::OpenSLES);
         oboe::Result result = builder.openStream(&playbackStream);
         LOGD("NativeAudioContext::startPlayback() openStream() returned %d", result);
         if (result != oboe::Result::OK) {
@@ -163,7 +170,9 @@
     oboe::Result start() {
         stop();
 
-        sineGenerator.start();
+        for (int i = 0; i < mChannelCount; i++) {
+            sineGenerators[i].start();
+        }
         impulseGenerator.start();
         sawPingGenerator.start();
         if (audioStreamGateway != nullptr) {
@@ -199,11 +208,15 @@
 
     void setAmplitude(double amplitude) {
         LOGD("%s(%f)", __func__, amplitude);
-        sineGenerator.amplitude.setValue(amplitude);
+        for (int i = 0; i < mChannelCount; i++) {
+            sineGenerators[i].amplitude.setValue(amplitude);
+        }
         sawPingGenerator.amplitude.setValue(amplitude);
         impulseGenerator.amplitude.setValue(amplitude);
     }
 
+    void setChannelEnabled(int channelIndex, bool enabled);
+
     oboe::AudioStream           *oboeStream = nullptr;
     InputStreamCallbackAnalyzer  mInputAnalyzer;
     bool                         useCallback = true;
@@ -244,14 +257,16 @@
     std::atomic<bool>            threadEnabled{false};
     std::thread                 *dataThread = nullptr;
 
-    OboeStreamCallbackProxy     oboeCallbackProxy;
-    SineGenerator               sineGenerator;
-    ImpulseGenerator            impulseGenerator;
-    SawPingGenerator            sawPingGenerator;
-    MonoToMultiConverter        *monoToMulti = nullptr;
-    oboe::AudioStream           *playbackStream = nullptr;
-    std::unique_ptr<float>       dataBuffer{};
+    OboeStreamCallbackProxy      oboeCallbackProxy;
+    std::vector<SineGenerator>   sineGenerators;
 
+    ImpulseGenerator             impulseGenerator;
+    SawPingGenerator             sawPingGenerator;
+    oboe::AudioStream           *playbackStream = nullptr;
+
+    std::unique_ptr<float>                  dataBuffer{};
+    std::unique_ptr<ManyToMultiConverter>            manyToMulti;
+    std::unique_ptr<MonoToMultiConverter>   monoToMulti;
     std::unique_ptr<AudioStreamGateway>     audioStreamGateway{};
     std::unique_ptr<MultiChannelRecording>  mRecording{};
 
diff --git a/apps/OboeTester/app/src/main/cpp/jni-bridge.cpp b/apps/OboeTester/app/src/main/cpp/jni-bridge.cpp
index a47f31c..7e32478 100644
--- a/apps/OboeTester/app/src/main/cpp/jni-bridge.cpp
+++ b/apps/OboeTester/app/src/main/cpp/jni-bridge.cpp
@@ -392,4 +392,10 @@
 
 }
 
+JNIEXPORT void JNICALL
+Java_com_google_sample_oboe_manualtest_OboeAudioOutputStream_setChannelEnabled(
+        JNIEnv *env, jobject, jint channelIndex, jboolean enabled) {
+    engine.setChannelEnabled(channelIndex, enabled);
+}
+
 }
diff --git a/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/AudioOutputTester.java b/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/AudioOutputTester.java
index 4ac3cde..2d49ae8 100644
--- a/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/AudioOutputTester.java
+++ b/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/AudioOutputTester.java
@@ -61,4 +61,7 @@
         mCurrentAudioStream.setAmplitude(amplitude);
     }
 
+    public void setChannelEnabled(int channelIndex, boolean enabled)  {
+        mOboeAudioOutputStream.setChannelEnabled(channelIndex, enabled);
+    }
 }
diff --git a/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/OboeAudioOutputStream.java b/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/OboeAudioOutputStream.java
index e63fff6..763e4c3 100644
--- a/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/OboeAudioOutputStream.java
+++ b/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/OboeAudioOutputStream.java
@@ -31,11 +31,12 @@
         return false;
     }
 
-    public native void setToneEnabled(boolean b);
+    public native void setToneEnabled(boolean enabled);
 
     public native void setToneType(int index);
 
     @Override
     public native void setAmplitude(double amplitude);
 
+    public native void setChannelEnabled(int channelIndex, boolean enabled);
 }
diff --git a/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/TestOutputActivity.java b/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/TestOutputActivity.java
index 47d61e5..f0e5f64 100644
--- a/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/TestOutputActivity.java
+++ b/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/TestOutputActivity.java
@@ -17,13 +17,18 @@
 package com.google.sample.oboe.manualtest;
 
 import android.os.Bundle;
+import android.view.View;
+import android.widget.CheckBox;
 
 import com.google.sample.oboe.manualtest.R;
 
 /**
  * Base class for output test activities
  */
-public class TestOutputActivity extends TestOutputActivityBase {
+public final class TestOutputActivity extends TestOutputActivityBase {
+
+    public static final int MAX_CHANNEL_BOXES = 8;
+    private CheckBox[] mChannelBoxes;
 
     @Override
     protected void onCreate(Bundle savedInstanceState) {
@@ -34,6 +39,31 @@
         updateEnabledWidgets();
 
         mAudioStreamTester = mAudioOutTester = AudioOutputTester.getInstance();
+
+        mChannelBoxes = new CheckBox[MAX_CHANNEL_BOXES];
+        int ic = 0;
+        mChannelBoxes[ic++] = (CheckBox) findViewById(R.id.channelBox0);
+        mChannelBoxes[ic++] = (CheckBox) findViewById(R.id.channelBox1);
+        mChannelBoxes[ic++] = (CheckBox) findViewById(R.id.channelBox2);
+        mChannelBoxes[ic++] = (CheckBox) findViewById(R.id.channelBox3);
+        mChannelBoxes[ic++] = (CheckBox) findViewById(R.id.channelBox4);
+        mChannelBoxes[ic++] = (CheckBox) findViewById(R.id.channelBox5);
+        mChannelBoxes[ic++] = (CheckBox) findViewById(R.id.channelBox6);
+        mChannelBoxes[ic++] = (CheckBox) findViewById(R.id.channelBox7);
+        configureChannelBoxes(0);
+    }
+
+    public void openAudio() {
+        super.openAudio();
+        int channelCount = mAudioOutTester.getCurrentAudioStream().getChannelCount();
+        configureChannelBoxes(channelCount);
+    }
+
+    private void configureChannelBoxes(int channelCount) {
+        for (int i = 0; i < mChannelBoxes.length; i++) {
+            mChannelBoxes[i].setChecked(i < channelCount);
+            mChannelBoxes[i].setEnabled(i < channelCount);
+        }
     }
 
     public void startAudio() {
@@ -47,4 +77,15 @@
         super.stopAudio();
     }
 
+    public void closeAudio() {
+        configureChannelBoxes(0);
+        super.closeAudio();
+    }
+
+    public void onChannelBoxClicked(View view) {
+        CheckBox checkBox = (CheckBox) view;
+        String text = (String) checkBox.getText();
+        int channelIndex = Integer.parseInt(text);
+        mAudioOutTester.setChannelEnabled(channelIndex, checkBox.isChecked());
+    }
 }
diff --git a/apps/OboeTester/app/src/main/res/layout/activity_test_output.xml b/apps/OboeTester/app/src/main/res/layout/activity_test_output.xml
index ff9f58b..63d0d48 100644
--- a/apps/OboeTester/app/src/main/res/layout/activity_test_output.xml
+++ b/apps/OboeTester/app/src/main/res/layout/activity_test_output.xml
@@ -12,4 +12,72 @@
 
     <include layout="@layout/merge_audio_common"/>
 
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:orientation="horizontal">
+
+        <TextView
+            android:id="@+id/channelText"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="Channels:" />
+
+        <CheckBox
+            android:id="@+id/channelBox0"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:onClick="onChannelBoxClicked"
+            android:text="0" />
+
+        <CheckBox
+            android:id="@+id/channelBox1"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:onClick="onChannelBoxClicked"
+            android:text="1" />
+
+        <CheckBox
+            android:id="@+id/channelBox2"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:onClick="onChannelBoxClicked"
+            android:text="2" />
+
+        <CheckBox
+            android:id="@+id/channelBox3"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:onClick="onChannelBoxClicked"
+            android:text="3" />
+
+        <CheckBox
+            android:id="@+id/channelBox4"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:onClick="onChannelBoxClicked"
+            android:text="4" />
+
+        <CheckBox
+            android:id="@+id/channelBox5"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:onClick="onChannelBoxClicked"
+            android:text="5" />
+
+        <CheckBox
+            android:id="@+id/channelBox6"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:onClick="onChannelBoxClicked"
+            android:text="6" />
+
+        <CheckBox
+            android:id="@+id/channelBox7"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:onClick="onChannelBoxClicked"
+            android:text="7" />
+
+    </LinearLayout>
 </LinearLayout>
diff --git a/docs/AppsUsingOboe.md b/docs/AppsUsingOboe.md
index ebfac25..af56804 100644
--- a/docs/AppsUsingOboe.md
+++ b/docs/AppsUsingOboe.md
@@ -67,3 +67,5 @@
 FluidSynth - [App on Play Store](https://play.google.com/store/apps/details?id=net.volcanomobile.fluidsynthmidi)
 
 OPL3 MIDI Synth FM - [App on Play Store](https://play.google.com/store/apps/details?id=net.volcanomobile.opl3midisynth)
+
+MIDI Sequencer app is very handy for testing. [App on Play Store](https://play.google.com/store/apps/details?id=net.volcanomobile.midisequencer)
diff --git a/docs/ChangeLog.md b/docs/ChangeLog.md
index cc2bf64..d206afa 100644
--- a/docs/ChangeLog.md
+++ b/docs/ChangeLog.md
@@ -1,6 +1,8 @@
 # Changelog
 
-## 1.0
+**This changelog is deprecated**. See the [Oboe releases page](https://github.com/google/oboe/releases) for the contents of each release. 
+
+## [1.0.0](https://github.com/google/oboe/releases/tag/1.0.0)
 #### 2nd October 2018
 First production ready version
 
diff --git a/docs/FAQ.md b/docs/FAQ.md
index 710dea9..5ae2fe3 100644
--- a/docs/FAQ.md
+++ b/docs/FAQ.md
@@ -12,3 +12,17 @@
 Oboe requires a callback to get a low latency stream and that does not work well with Java.
 
 Note that [`AudioTrack.PERFORMANCE_MODE_LOW_LATENCY`](https://developer.android.com/reference/android/media/AudioTrack#PERFORMANCE_MODE_LOW_LATENCY) was added in API 26, For API 24 or 25 use [`AudioAttributes.FLAG_LOW_LATENCY`](https://developer.android.com/reference/kotlin/android/media/AudioAttributes#flag_low_latency). That was deprecated but will still work with later APIs.
+
+## Can I use Oboe to play compressed audio files, such as MP3 or AAC?
+Oboe only works with PCM data. It does not include any extraction or decoding classes. For this you can use:
+
+1) [FFmpeg](https://www.ffmpeg.org/) - very fast decoding speeds, but can be difficult to configure and compile. [There's a good article on compiling FFmpeg 4.0 here](https://medium.com/@karthikcodes1999/cross-compiling-ffmpeg-4-0-for-android-b988326f16f2).
+2) [The NDK media classes](https://developer.android.com/ndk/reference/group/media), specifically `NdkMediaExtractor` and `NdkMediaCodec` - they're approximately 10X slower than FFmpeg but ship with Android. [Code sample here](https://github.com/googlesamples/android-ndk/tree/master/native-codec). 
+
+If you don't need the lowest possible audio latency you may want to investigate using the following Java/Kotlin APIs which support playback of compressed audio files: 
+
+- [MediaPlayer](https://developer.android.com/reference/android/media/MediaPlayer)
+- [SoundPool](https://developer.android.com/reference/android/media/SoundPool)
+
+## My question isn't listed, where can I ask it?
+Please ask questions on [Stack Overflow](https://stackoverflow.com/questions/ask) with the [Oboe tag](https://stackoverflow.com/tags/oboe). 
\ No newline at end of file
diff --git a/docs/GettingStarted.md b/docs/GettingStarted.md
index 857bb32..2674e24 100644
--- a/docs/GettingStarted.md
+++ b/docs/GettingStarted.md
@@ -4,9 +4,9 @@
 ## Adding Oboe to your project
 
 ### 1. Clone the github repository 
-Start by cloning the Oboe repository: 
+Start by cloning the [latest stable release](https://github.com/google/oboe/releases/) of the Oboe repository, for example: 
 
-    git clone https://github.com/google/oboe
+    git clone -b 1.1-stable https://github.com/google/oboe
 
 **Make a note of the path which you cloned oboe into - you will need it shortly**
 
diff --git a/include/oboe/Oboe.h b/include/oboe/Oboe.h
index e59fa89..c3a0bca 100644
--- a/include/oboe/Oboe.h
+++ b/include/oboe/Oboe.h
@@ -32,5 +32,6 @@
 #include "oboe/AudioStreamBuilder.h"
 #include "oboe/Utilities.h"
 #include "oboe/Version.h"
+#include "oboe/StabilizedCallback.h"
 
 #endif //OBOE_OBOE_H
diff --git a/include/oboe/StabilizedCallback.h b/include/oboe/StabilizedCallback.h
new file mode 100644
index 0000000..3f1a689
--- /dev/null
+++ b/include/oboe/StabilizedCallback.h
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef OBOE_STABILIZEDCALLBACK_H
+#define OBOE_STABILIZEDCALLBACK_H
+
+#include <cstdint>
+#include "oboe/AudioStream.h"
+
+namespace oboe {
+
+class StabilizedCallback : public AudioStreamCallback {
+
+public:
+    explicit StabilizedCallback(AudioStreamCallback *callback);
+
+    DataCallbackResult
+    onAudioReady(AudioStream *oboeStream, void *audioData, int32_t numFrames) override;
+
+    void onErrorBeforeClose(AudioStream *oboeStream, Result error) override {
+        return mCallback->onErrorBeforeClose(oboeStream, error);
+    }
+
+    void onErrorAfterClose(AudioStream *oboeStream, Result error) override {
+
+        // Reset all fields now that the stream has been closed
+        mFrameCount = 0;
+        mEpochTimeNanos = 0;
+        mOpsPerNano = 1;
+        return mCallback->onErrorAfterClose(oboeStream, error);
+    }
+
+private:
+
+    AudioStreamCallback *mCallback = nullptr;
+    int64_t mFrameCount = 0;
+    int64_t mEpochTimeNanos = 0;
+    double  mOpsPerNano = 1;
+
+    void generateLoad(int64_t durationNanos);
+};
+
+/**
+ * cpu_relax is an architecture specific method of telling the CPU that you don't want it to
+ * do much work. asm volatile keeps the compiler from optimising these instructions out.
+ */
+#if defined(__i386__) || defined(__x86_64__)
+#define cpu_relax() asm volatile("rep; nop" ::: "memory");
+
+#elif defined(__arm__) || defined(__mips__)
+    #define cpu_relax() asm volatile("":::"memory")
+
+#elif defined(__aarch64__)
+#define cpu_relax() asm volatile("yield" ::: "memory")
+
+#else
+#error "cpu_relax is not defined for this architecture"
+#endif
+
+}
+
+#endif //OBOE_STABILIZEDCALLBACK_H
diff --git a/samples/MegaDrone/README.md b/samples/MegaDrone/README.md
index ae72768..5865fdb 100644
--- a/samples/MegaDrone/README.md
+++ b/samples/MegaDrone/README.md
@@ -12,6 +12,8 @@
 4) Setting the buffer size to 2 bursts
 5) Using the `-Ofast` compiler optimization flag, even when building the `Debug` variant
 6) Using [`getExclusiveCores`](https://developer.android.com/reference/android/os/Process#getExclusiveCores()) (API 24+) and thread affinity to bind the audio thread to the best available CPU core(s)
+7) Using a `StabilizedCallback` which aims to spend a fixed percentage of the callback time to avoid CPU frequency scaling ([video explanation](https://www.youtube.com/watch?v=C0BPXZIvG-Q&feature=youtu.be&t=1158))
+
 
 This code was presented at [AES Milan](http://www.aes.org/events/144/) and [Droidcon Berlin](https://www.de.droidcon.com/) as part of a talk on Oboe.
 
diff --git a/samples/MegaDrone/src/main/cpp/AudioEngine.cpp b/samples/MegaDrone/src/main/cpp/AudioEngine.cpp
index 0471a12..2a19fde 100644
--- a/samples/MegaDrone/src/main/cpp/AudioEngine.cpp
+++ b/samples/MegaDrone/src/main/cpp/AudioEngine.cpp
@@ -25,7 +25,9 @@
 
     mCpuIds = cpuIds;
     AudioStreamBuilder builder;
-    builder.setCallback(this);
+
+    mStabilizedCallback = new StabilizedCallback(this);
+    builder.setCallback(mStabilizedCallback);
     builder.setPerformanceMode(PerformanceMode::LowLatency);
     builder.setSharingMode(SharingMode::Exclusive);
 
diff --git a/samples/MegaDrone/src/main/cpp/AudioEngine.h b/samples/MegaDrone/src/main/cpp/AudioEngine.h
index e6e2cb8..98aa0c9 100644
--- a/samples/MegaDrone/src/main/cpp/AudioEngine.h
+++ b/samples/MegaDrone/src/main/cpp/AudioEngine.h
@@ -38,6 +38,7 @@
 
 private:
 
+    StabilizedCallback *mStabilizedCallback = nullptr;
     AudioStream *mStream = nullptr;
     std::unique_ptr<ISynth> mSynth;
     std::vector<int> mCpuIds; // IDs of CPU cores which the audio callback should be bound to
diff --git a/samples/RhythmGame/CMakeLists.txt b/samples/RhythmGame/CMakeLists.txt
index f8bc1fb..c5ae34e 100644
--- a/samples/RhythmGame/CMakeLists.txt
+++ b/samples/RhythmGame/CMakeLists.txt
@@ -12,7 +12,8 @@
              src/main/cpp/Game.cpp
 
              # audio engine
-             src/main/cpp/audio/SoundRecording.cpp
+             src/main/cpp/audio/AAssetDataSource.cpp
+             src/main/cpp/audio/Player.cpp
              src/main/cpp/audio/Mixer.cpp
 
              # UI engine
diff --git a/samples/RhythmGame/src/main/cpp/Game.cpp b/samples/RhythmGame/src/main/cpp/Game.cpp
index 7d1798a..b5ee621 100644
--- a/samples/RhythmGame/src/main/cpp/Game.cpp
+++ b/samples/RhythmGame/src/main/cpp/Game.cpp
@@ -19,14 +19,29 @@
 
 #include "Game.h"
 
-Game::Game(AAssetManager *assetManager): mAssetManager(assetManager) {
+Game::Game(AAssetManager &assetManager): mAssetManager(assetManager) {
 }
 
 void Game::start() {
 
     // Load the RAW PCM data files for both the clap sound and backing track into memory.
-    mClap = SoundRecording::loadFromAssets(mAssetManager, "CLAP.raw");
-    mBackingTrack = SoundRecording::loadFromAssets(mAssetManager, "FUNKY_HOUSE.raw" );
+    std::shared_ptr<AAssetDataSource> mClapSource(AAssetDataSource::newFromAssetManager(mAssetManager,
+        "CLAP.raw",
+        oboe::ChannelCount::Stereo));
+    if (mClapSource == nullptr){
+        LOGE("Could not load source data for clap sound");
+        return;
+    }
+    mClap = std::make_shared<Player>(mClapSource);
+
+    std::shared_ptr<AAssetDataSource> mBackingTrackSource(AAssetDataSource::newFromAssetManager(mAssetManager,
+        "FUNKY_HOUSE.raw",
+        oboe::ChannelCount::Stereo));
+    if (mBackingTrackSource == nullptr){
+        LOGE("Could not load source data for backing track");
+        return;
+    }
+    mBackingTrack = std::make_shared<Player>(mBackingTrackSource);
     mBackingTrack->setPlaying(true);
     mBackingTrack->setLooping(true);
 
@@ -76,6 +91,15 @@
     }
 }
 
+void Game::stop(){
+
+    if (mAudioStream != nullptr){
+        mAudioStream->close();
+        delete mAudioStream;
+        mAudioStream = nullptr;
+    }
+}
+
 void Game::tap(int64_t eventTimeAsUptime) {
     mClap->setPlaying(true);
 
diff --git a/samples/RhythmGame/src/main/cpp/Game.h b/samples/RhythmGame/src/main/cpp/Game.h
index d707124..3ec955d 100644
--- a/samples/RhythmGame/src/main/cpp/Game.h
+++ b/samples/RhythmGame/src/main/cpp/Game.h
@@ -21,7 +21,8 @@
 #include <oboe/Oboe.h>
 
 #include "audio/Mixer.h"
-#include "audio/SoundRecording.h"
+#include "audio/Player.h"
+#include "audio/AAssetDataSource.h"
 #include "ui/OpenGLFunctions.h"
 #include "utils/LockFreeQueue.h"
 #include "utils/UtilityFunctions.h"
@@ -31,9 +32,10 @@
 
 class Game : public AudioStreamCallback {
 public:
-    explicit Game(AAssetManager *assetManager);
+    explicit Game(AAssetManager&);
 
     void start();
+    void stop();
     void onSurfaceCreated();
     void onSurfaceDestroyed();
     void onSurfaceChanged(int widthInPixels, int heightInPixels);
@@ -45,10 +47,10 @@
     onAudioReady(AudioStream *oboeStream, void *audioData, int32_t numFrames) override;
 
 private:
-    AAssetManager *mAssetManager{nullptr};
+    AAssetManager& mAssetManager;
     AudioStream *mAudioStream{nullptr};
-    SoundRecording *mClap{nullptr};
-    SoundRecording *mBackingTrack{nullptr};
+    std::shared_ptr<Player> mClap;
+    std::shared_ptr<Player> mBackingTrack;
     Mixer mMixer;
 
     LockFreeQueue<int64_t, kMaxQueueItems> mClapEvents;
@@ -56,6 +58,7 @@
     LockFreeQueue<int64_t, kMaxQueueItems> mClapWindows;
     LockFreeQueue<TapResult, kMaxQueueItems> mUiEvents;
     std::atomic<int64_t> mLastUpdateTime { 0 };
+
 };
 
 
diff --git a/samples/RhythmGame/src/main/cpp/audio/AAssetDataSource.cpp b/samples/RhythmGame/src/main/cpp/audio/AAssetDataSource.cpp
new file mode 100644
index 0000000..0d6dbc4
--- /dev/null
+++ b/samples/RhythmGame/src/main/cpp/audio/AAssetDataSource.cpp
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+#include <utils/logging.h>
+#include "AAssetDataSource.h"
+
+
+AAssetDataSource* AAssetDataSource::newFromAssetManager(AAssetManager &assetManager,
+                                                        const char *filename,
+                                                        const int32_t channelCount) {
+
+    // Load the backing track
+    AAsset* asset = AAssetManager_open(&assetManager, filename, AASSET_MODE_BUFFER);
+
+    if (asset == nullptr){
+        LOGE("Failed to open track, filename %s", filename);
+        return nullptr;
+    }
+
+    // Get the length of the track (we assume it is stereo 48kHz)
+    off_t trackSizeInBytes = AAsset_getLength(asset);
+
+    // Load it into memory
+    auto *audioBuffer = static_cast<const int16_t*>(AAsset_getBuffer(asset));
+
+    if (audioBuffer == nullptr){
+        LOGE("Could not get buffer for track");
+        return nullptr;
+    }
+
+    auto numFrames = static_cast<int32_t>(trackSizeInBytes / (sizeof(int16_t) * channelCount));
+    LOGD("Opened audio data source, bytes: %ld frames: %d", trackSizeInBytes, numFrames);
+
+    return new AAssetDataSource(asset, audioBuffer, numFrames, channelCount);
+}
diff --git a/samples/RhythmGame/src/main/cpp/audio/AAssetDataSource.h b/samples/RhythmGame/src/main/cpp/audio/AAssetDataSource.h
new file mode 100644
index 0000000..dd64650
--- /dev/null
+++ b/samples/RhythmGame/src/main/cpp/audio/AAssetDataSource.h
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef RHYTHMGAME_AASSETDATASOURCE_H
+#define RHYTHMGAME_AASSETDATASOURCE_H
+
+#include <android/asset_manager.h>
+#include "DataSource.h"
+
+class AAssetDataSource : public DataSource {
+
+public:
+
+    ~AAssetDataSource(){
+
+        // Note that this will also delete the data at mBuffer
+        AAsset_close(mAsset);
+    }
+
+    int32_t getTotalFrames() const override { return mTotalFrames; } ;
+    int32_t getChannelCount() const override { return mChannelCount; } ;
+    const int16_t* getData() const override { return mBuffer;	};
+
+    static AAssetDataSource* newFromAssetManager(AAssetManager&, const char *, const int32_t);
+
+private:
+
+    AAssetDataSource(AAsset *asset, const int16_t *data, int32_t frames,
+                     int32_t channelCount)
+            : mAsset(asset)
+            , mBuffer(data)
+            , mTotalFrames(frames)
+            , mChannelCount(channelCount) {
+    };
+
+    AAsset *mAsset = nullptr;
+    const int16_t* mBuffer;
+    const int32_t mTotalFrames;
+    const int32_t mChannelCount;
+
+};
+#endif //RHYTHMGAME_AASSETDATASOURCE_H
diff --git a/samples/RhythmGame/src/main/cpp/audio/DataSource.h b/samples/RhythmGame/src/main/cpp/audio/DataSource.h
new file mode 100644
index 0000000..6694399
--- /dev/null
+++ b/samples/RhythmGame/src/main/cpp/audio/DataSource.h
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef RHYTHMGAME_AUDIOSOURCE_H
+#define RHYTHMGAME_AUDIOSOURCE_H
+
+#include <cstdint>
+
+class DataSource {
+public:
+    virtual ~DataSource(){};
+    virtual int32_t getTotalFrames() const = 0;
+    virtual int32_t getChannelCount() const  = 0;
+    virtual const int16_t* getData() const = 0;
+};
+
+
+#endif //RHYTHMGAME_AUDIOSOURCE_H
diff --git a/samples/RhythmGame/src/main/cpp/audio/Mixer.cpp b/samples/RhythmGame/src/main/cpp/audio/Mixer.cpp
index 0279617..b6f0028 100644
--- a/samples/RhythmGame/src/main/cpp/audio/Mixer.cpp
+++ b/samples/RhythmGame/src/main/cpp/audio/Mixer.cpp
@@ -24,7 +24,7 @@
     }
 
     for (int i = 0; i < mNextFreeTrackIndex; ++i) {
-        mTracks[i]->renderAudio(mixingBuffer, numFrames);
+        mTracks[i]->renderAudio(mixingBuffer.data(), numFrames);
 
         for (int j = 0; j < numFrames * kChannelCount; ++j) {
             audioData[j] += mixingBuffer[j];
@@ -32,6 +32,9 @@
     }
 }
 
-void Mixer::addTrack(RenderableAudio *renderer){
+void Mixer::addTrack(std::shared_ptr<RenderableAudio> renderer){
     mTracks[mNextFreeTrackIndex++] = renderer;
+    // If we've reached our track limit then overwrite the first track
+    if (mNextFreeTrackIndex >= kMaxTracks)
+        mNextFreeTrackIndex = 0;
 };
\ No newline at end of file
diff --git a/samples/RhythmGame/src/main/cpp/audio/Mixer.h b/samples/RhythmGame/src/main/cpp/audio/Mixer.h
index a101f62..b558390 100644
--- a/samples/RhythmGame/src/main/cpp/audio/Mixer.h
+++ b/samples/RhythmGame/src/main/cpp/audio/Mixer.h
@@ -18,7 +18,7 @@
 #define RHYTHMGAME_MIXER_H
 
 
-#include "SoundRecording.h"
+#include "Player.h"
 #include "RenderableAudio.h"
 
 constexpr int32_t kBufferSize = 192*10; // Temporary buffer is used for mixing
@@ -28,15 +28,13 @@
 class Mixer : public RenderableAudio {
 
 public:
-    void addTrack(RenderableAudio *renderer);
+    void addTrack(std::shared_ptr<RenderableAudio> renderer);
     void renderAudio(int16_t *audioData, int32_t numFrames);
 
 private:
-
-    int16_t *mixingBuffer = new int16_t[kBufferSize]; // TODO: smart pointer
-    RenderableAudio *mTracks[kMaxTracks]; // TODO: this might be better as a linked list for easy track removal
+    std::array<int16_t, kBufferSize> mixingBuffer;
+    std::shared_ptr<RenderableAudio> mTracks[kMaxTracks]; // TODO: this might be better as a linked list for easy track removal
     uint8_t mNextFreeTrackIndex = 0;
 };
 
-
 #endif //RHYTHMGAME_MIXER_H
diff --git a/samples/RhythmGame/src/main/cpp/audio/Player.cpp b/samples/RhythmGame/src/main/cpp/audio/Player.cpp
new file mode 100644
index 0000000..2ecd616
--- /dev/null
+++ b/samples/RhythmGame/src/main/cpp/audio/Player.cpp
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "Player.h"
+#include "utils/logging.h"
+
+void Player::renderAudio(int16_t *targetData, int32_t numFrames){
+
+    const int32_t channelCount = mSource->getChannelCount();
+
+    if (mIsPlaying){
+
+        int32_t framesToRenderFromData = numFrames;
+        int32_t totalSourceFrames = mSource->getTotalFrames();
+        const int16_t *data = mSource->getData();
+
+        // Check whether we're about to reach the end of the recording
+        if (!mIsLooping && mReadFrameIndex + numFrames >= totalSourceFrames){
+            framesToRenderFromData = totalSourceFrames - mReadFrameIndex;
+            mIsPlaying = false;
+        }
+
+        for (int i = 0; i < framesToRenderFromData; ++i) {
+            for (int j = 0; j < channelCount; ++j) {
+                targetData[(i*channelCount)+j] = data[(mReadFrameIndex*channelCount)+j];
+            }
+
+            // Increment and handle wraparound
+            if (++mReadFrameIndex >= totalSourceFrames) mReadFrameIndex = 0;
+        }
+
+        if (framesToRenderFromData < numFrames){
+            // fill the rest of the buffer with silence
+            renderSilence(&targetData[framesToRenderFromData], numFrames * channelCount);
+        }
+
+    } else {
+        renderSilence(targetData, numFrames * channelCount);
+    }
+}
+
+void Player::renderSilence(int16_t *start, int32_t numSamples){
+    for (int i = 0; i < numSamples; ++i) {
+        start[i] = 0;
+    }
+}
\ No newline at end of file
diff --git a/samples/RhythmGame/src/main/cpp/audio/SoundRecording.h b/samples/RhythmGame/src/main/cpp/audio/Player.h
similarity index 72%
rename from samples/RhythmGame/src/main/cpp/audio/SoundRecording.h
rename to samples/RhythmGame/src/main/cpp/audio/Player.h
index f80f972..5ccdcd7 100644
--- a/samples/RhythmGame/src/main/cpp/audio/SoundRecording.h
+++ b/samples/RhythmGame/src/main/cpp/audio/Player.h
@@ -27,29 +27,34 @@
 #include <android/asset_manager.h>
 
 #include "RenderableAudio.h"
+#include "DataSource.h"
 
-class SoundRecording : public RenderableAudio{
+class Player : public RenderableAudio{
 
 public:
-    SoundRecording(const int16_t *sourceData, int32_t numFrames)
-            : mData(sourceData)
-            , mTotalFrames(numFrames)
+    /**
+     * Construct a new Player from the given DataSource. Players can share the same data source.
+     * For example, you could play two identical sounds concurrently by creating 2 Players with the
+     * same data source.
+     *
+     * @param source
+     */
+    Player(std::shared_ptr<DataSource> source)
+        : mSource(source)
     {};
+
     void renderAudio(int16_t *targetData, int32_t numFrames);
     void resetPlayHead() { mReadFrameIndex = 0; };
     void setPlaying(bool isPlaying) { mIsPlaying = isPlaying; resetPlayHead(); };
     void setLooping(bool isLooping) { mIsLooping = isLooping; };
 
-    static SoundRecording * loadFromAssets(AAssetManager *assetManager, const char * filename);
-
 private:
-    int32_t mChannelCount = 2; // TODO: move this into a konstant and maybe add as parameter to ctor
     int32_t mReadFrameIndex = 0;
-    const int16_t* mData = nullptr;
-    int32_t mTotalFrames = 0;
     std::atomic<bool> mIsPlaying { false };
     std::atomic<bool> mIsLooping { false };
+    std::shared_ptr<DataSource> mSource;
 
+    void renderSilence(int16_t*, int32_t);
 };
 
 #endif //RHYTHMGAME_SOUNDRECORDING_H
diff --git a/samples/RhythmGame/src/main/cpp/audio/SoundRecording.cpp b/samples/RhythmGame/src/main/cpp/audio/SoundRecording.cpp
deleted file mode 100644
index 62e3af5..0000000
--- a/samples/RhythmGame/src/main/cpp/audio/SoundRecording.cpp
+++ /dev/null
@@ -1,74 +0,0 @@
-/*
- * Copyright 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-#include "SoundRecording.h"
-#include "utils/logging.h"
-
-void SoundRecording::renderAudio(int16_t *targetData, int32_t numFrames){
-
-    if (mIsPlaying){
-
-        // Check whether we're about to reach the end of the recording
-        if (!mIsLooping && mReadFrameIndex + numFrames >= mTotalFrames){
-            numFrames = mTotalFrames - mReadFrameIndex;
-            mIsPlaying = false;
-        }
-
-        for (int i = 0; i < numFrames; ++i) {
-            for (int j = 0; j < mChannelCount; ++j) {
-                targetData[(i*mChannelCount)+j] = mData[(mReadFrameIndex*mChannelCount)+j];
-            }
-
-            // Increment and handle wraparound
-            if (++mReadFrameIndex >= mTotalFrames) mReadFrameIndex = 0;
-        }
-
-    } else {
-        // fill with zeros to output silence
-        for (int i = 0; i < numFrames * mChannelCount; ++i) {
-            targetData[i] = 0;
-        }
-    }
-}
-
-SoundRecording * SoundRecording::loadFromAssets(AAssetManager *assetManager, const char *filename) {
-
-    // Load the backing track
-    AAsset* asset = AAssetManager_open(assetManager, filename, AASSET_MODE_BUFFER);
-
-    if (asset == nullptr){
-        LOGE("Failed to open track, filename %s", filename);
-        return nullptr;
-    }
-
-    // Get the length of the track (we assume it is stereo 48kHz)
-    off_t trackLength = AAsset_getLength(asset);
-
-    // Load it into memory
-    const int16_t *audioBuffer = static_cast<const int16_t*>(AAsset_getBuffer(asset));
-
-    if (audioBuffer == nullptr){
-        LOGE("Could not get buffer for track");
-        return nullptr;
-    }
-
-    // There are 4 bytes per frame because
-    // each sample is 2 bytes and
-    // it's a stereo recording which has 2 samples per frame.
-    int32_t numFrames = static_cast<int32_t>(trackLength / 4);
-    LOGD("Opened backing track, bytes: %ld frames: %d", trackLength, numFrames);
-    return new SoundRecording(audioBuffer, numFrames);
-}
diff --git a/samples/RhythmGame/src/main/cpp/native-lib.cpp b/samples/RhythmGame/src/main/cpp/native-lib.cpp
index b7859ce..ecd941f 100644
--- a/samples/RhythmGame/src/main/cpp/native-lib.cpp
+++ b/samples/RhythmGame/src/main/cpp/native-lib.cpp
@@ -27,41 +27,58 @@
 std::unique_ptr<Game> game;
 
 JNIEXPORT void JNICALL
-Java_com_google_oboe_sample_rhythmgame_MainActivity_native_1onCreate(JNIEnv *env, jobject instance,
-                                                            jobject jAssetManager) {
+Java_com_google_oboe_sample_rhythmgame_MainActivity_native_1onStart(JNIEnv *env, jobject instance,
+                                                                     jobject jAssetManager) {
 
     AAssetManager *assetManager = AAssetManager_fromJava(env, jAssetManager);
-    game = std::make_unique<Game>(assetManager);
+    if (assetManager == nullptr) {
+        LOGE("Could not obtain the AAssetManager");
+        return;
+    }
+
+    game = std::make_unique<Game>(*assetManager);
     game->start();
 }
 
 JNIEXPORT void JNICALL
-Java_com_google_oboe_sample_rhythmgame_RendererWrapper_native_1onSurfaceCreated(JNIEnv *env, jobject instance) {
+Java_com_google_oboe_sample_rhythmgame_RendererWrapper_native_1onSurfaceCreated(JNIEnv *env,
+                                                                                jobject instance) {
     game->onSurfaceCreated();
 }
 
 JNIEXPORT void JNICALL
-Java_com_google_oboe_sample_rhythmgame_RendererWrapper_native_1onSurfaceChanged(JNIEnv *env, jclass type,
-                                                                   jint width, jint height) {
+Java_com_google_oboe_sample_rhythmgame_RendererWrapper_native_1onSurfaceChanged(JNIEnv *env,
+                                                                                jclass type,
+                                                                                jint width,
+                                                                                jint height) {
     game->onSurfaceChanged(width, height);
 }
 
 JNIEXPORT void JNICALL
-Java_com_google_oboe_sample_rhythmgame_RendererWrapper_native_1onDrawFrame(JNIEnv *env, jclass type) {
+Java_com_google_oboe_sample_rhythmgame_RendererWrapper_native_1onDrawFrame(JNIEnv *env,
+                                                                           jclass type) {
     game->tick();
 }
 
 JNIEXPORT void JNICALL
-Java_com_google_oboe_sample_rhythmgame_GameSurfaceView_native_1onTouchInput(JNIEnv *env, jclass type,
-                                                           jint event_type,
-                                                           jlong time_since_boot_ms,
-                                                           jint pixel_x, jint pixel_y) {
+Java_com_google_oboe_sample_rhythmgame_GameSurfaceView_native_1onTouchInput(JNIEnv *env,
+                                                                            jclass type,
+                                                                            jint event_type,
+                                                                            jlong time_since_boot_ms,
+                                                                            jint pixel_x,
+                                                                            jint pixel_y) {
     game->tap(time_since_boot_ms);
 }
 
 JNIEXPORT void JNICALL
-Java_com_google_oboe_sample_rhythmgame_GameSurfaceView_native_1surfaceDestroyed__(JNIEnv *env, jclass type) {
+Java_com_google_oboe_sample_rhythmgame_GameSurfaceView_native_1surfaceDestroyed__(JNIEnv *env,
+                                                                                  jclass type) {
     game->onSurfaceDestroyed();
 }
 
+JNIEXPORT void JNICALL
+Java_com_google_oboe_sample_rhythmgame_MainActivity_native_1onStop(JNIEnv *env, jobject instance) {
+
+    game->stop();
+}
 }
\ No newline at end of file
diff --git a/samples/RhythmGame/src/main/java/com/google/oboe/sample/rhythmgame/MainActivity.java b/samples/RhythmGame/src/main/java/com/google/oboe/sample/rhythmgame/MainActivity.java
index 57d9a4a..266ec48 100644
--- a/samples/RhythmGame/src/main/java/com/google/oboe/sample/rhythmgame/MainActivity.java
+++ b/samples/RhythmGame/src/main/java/com/google/oboe/sample/rhythmgame/MainActivity.java
@@ -33,8 +33,18 @@
         super.onCreate(savedInstanceState);
         setContentView(R.layout.activity_main);
         getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
-        native_onCreate(getAssets());
     }
 
-    private native void native_onCreate(AssetManager assetManager);
+    protected void onStart(){
+        super.onStart();
+        native_onStart(getAssets());
+    }
+
+    protected void onStop(){
+        super.onStop();
+        native_onStop();
+    }
+
+    private native void native_onStart(AssetManager assetManager);
+    private native void native_onStop();
 }
diff --git a/src/common/StabilizedCallback.cpp b/src/common/StabilizedCallback.cpp
new file mode 100644
index 0000000..7a0adba
--- /dev/null
+++ b/src/common/StabilizedCallback.cpp
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "oboe/StabilizedCallback.h"
+#include "common/AudioClock.h"
+#include "common/Trace.h"
+
+constexpr int32_t kLoadGenerationStepSizeNanos = 20000;
+constexpr float kPercentageOfCallbackToUse = 0.8;
+
+using namespace oboe;
+
+StabilizedCallback::StabilizedCallback(AudioStreamCallback *callback) : mCallback(callback){
+    Trace::initialize();
+}
+
+/**
+ * An audio callback which attempts to do work for a fixed amount of time.
+ *
+ * @param oboeStream
+ * @param audioData
+ * @param numFrames
+ * @return
+ */
+DataCallbackResult
+StabilizedCallback::onAudioReady(AudioStream *oboeStream, void *audioData, int32_t numFrames) {
+
+    int64_t startTimeNanos = AudioClock::getNanoseconds();
+
+    if (mFrameCount == 0){
+        mEpochTimeNanos = startTimeNanos;
+    }
+
+    int64_t durationSinceEpochNanos = startTimeNanos - mEpochTimeNanos;
+
+    // In an ideal world the callback start time will be exactly the same as the duration of the
+    // frames already read/written into the stream. In reality the callback can start early
+    // or late. By finding the delta we can calculate the target duration for our stabilized
+    // callback.
+    int64_t idealStartTimeNanos = (mFrameCount * kNanosPerSecond) / oboeStream->getSampleRate();
+    int64_t lateStartNanos = durationSinceEpochNanos - idealStartTimeNanos;
+
+    if (lateStartNanos < 0){
+        // This was an early start which indicates that our previous epoch was a late callback.
+        // Update our epoch to this more accurate time.
+        mEpochTimeNanos = startTimeNanos;
+        mFrameCount = 0;
+    }
+
+    int64_t numFramesAsNanos = (numFrames * kNanosPerSecond) / oboeStream->getSampleRate();
+    int64_t targetDurationNanos = (int64_t)
+            (numFramesAsNanos * kPercentageOfCallbackToUse) - lateStartNanos;
+
+    Trace::beginSection("Actual load");
+    DataCallbackResult result = mCallback->onAudioReady(oboeStream, audioData, numFrames);
+    Trace::endSection();
+
+    int64_t executionDurationNanos = AudioClock::getNanoseconds() - startTimeNanos;
+    int64_t stabilizingLoadDurationNanos = targetDurationNanos - executionDurationNanos;
+
+    Trace::beginSection("Stabilized load for %lldns", stabilizingLoadDurationNanos);
+    generateLoad(stabilizingLoadDurationNanos);
+    Trace::endSection();
+
+    // Wraparound: At 48000 frames per second mFrameCount wraparound will occur after 6m years,
+    // significantly longer than the average lifetime of an Android phone.
+    mFrameCount += numFrames;
+    return result;
+}
+
+void StabilizedCallback::generateLoad(int64_t durationNanos) {
+
+    int64_t currentTimeNanos = AudioClock::getNanoseconds();
+    int64_t deadlineTimeNanos = currentTimeNanos + durationNanos;
+
+    // opsPerStep gives us an estimated number of operations which need to be run to fully utilize
+    // the CPU for a fixed amount of time (specified by kLoadGenerationStepSizeNanos).
+    // After each step the opsPerStep value is re-calculated based on the actual time taken to
+    // execute those operations.
+    auto opsPerStep = (int)(mOpsPerNano * kLoadGenerationStepSizeNanos);
+    int64_t stepDurationNanos = 0;
+    int64_t previousTimeNanos = 0;
+
+    while (currentTimeNanos <= deadlineTimeNanos){
+
+        for (int i = 0; i < opsPerStep; i++) cpu_relax();
+
+        previousTimeNanos = currentTimeNanos;
+        currentTimeNanos = AudioClock::getNanoseconds();
+        stepDurationNanos = currentTimeNanos - previousTimeNanos;
+
+        // Calculate exponential moving average to smooth out values, this acts as a low pass filter.
+        // @see https://en.wikipedia.org/wiki/Moving_average#Exponential_moving_average
+        static const float kFilterCoefficient = 0.1;
+        auto measuredOpsPerNano = (double) opsPerStep / stepDurationNanos;
+        mOpsPerNano = kFilterCoefficient * measuredOpsPerNano + (1.0 - kFilterCoefficient) * mOpsPerNano;
+        opsPerStep = (int) (mOpsPerNano * kLoadGenerationStepSizeNanos);
+    }
+}
\ No newline at end of file
diff --git a/src/common/Trace.cpp b/src/common/Trace.cpp
new file mode 100644
index 0000000..5ed445b
--- /dev/null
+++ b/src/common/Trace.cpp
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <dlfcn.h>
+#include <cstdio>
+#include "Trace.h"
+#include "OboeDebug.h"
+
+static char buffer[256];
+
+// Tracing functions
+static void *(*ATrace_beginSection)(const char *sectionName);
+
+static void *(*ATrace_endSection)();
+
+typedef void *(*fp_ATrace_beginSection)(const char *sectionName);
+
+typedef void *(*fp_ATrace_endSection)();
+
+bool Trace::mIsTracingSupported = false;
+
+void Trace::beginSection(const char *format, ...){
+
+    if (mIsTracingSupported) {
+        va_list va;
+        va_start(va, format);
+        vsprintf(buffer, format, va);
+        ATrace_beginSection(buffer);
+        va_end(va);
+    } else {
+        LOGE("Tracing is either not initialized (call Trace::initialize()) "
+             "or not supported on this device");
+    }
+}
+
+void Trace::endSection() {
+
+    if (mIsTracingSupported) {
+        ATrace_endSection();
+    }
+}
+
+void Trace::initialize() {
+
+    // Using dlsym allows us to use tracing on API 21+ without needing android/trace.h which wasn't
+    // published until API 23
+    void *lib = dlopen("libandroid.so", RTLD_NOW | RTLD_LOCAL);
+    if (lib == nullptr) {
+        LOGE("Could not open libandroid.so to dynamically load tracing symbols");
+    } else {
+        ATrace_beginSection =
+                reinterpret_cast<fp_ATrace_beginSection >(
+                        dlsym(lib, "ATrace_beginSection"));
+        ATrace_endSection =
+                reinterpret_cast<fp_ATrace_endSection >(
+                        dlsym(lib, "ATrace_endSection"));
+
+        if (ATrace_beginSection != nullptr && ATrace_endSection != nullptr){
+            mIsTracingSupported = true;
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/common/Trace.h b/src/common/Trace.h
new file mode 100644
index 0000000..c7965f9
--- /dev/null
+++ b/src/common/Trace.h
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef OBOE_TRACE_H
+#define OBOE_TRACE_H
+
+class Trace {
+
+public:
+    static void beginSection(const char *format, ...);
+    static void endSection();
+    static void initialize();
+
+private:
+    static bool mIsTracingSupported;
+};
+
+#endif //OBOE_TRACE_H
\ No newline at end of file