Merge pull request #821 from google/deleterace

Add openSharedStream() to prevent deletion while executing onError callbacks
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
index dec50cd..e27ff1a 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.md
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -7,18 +7,19 @@
 
 ---
 
-Android Version(s):
-Android Device(s):
-Oboe Version:
+Android version(s):
+Android device(s):
+Oboe version:
+App name used for testing (please try to reproduce the issue using the Oboe samples and apps): 
 
-**Short Description**
+**Short description**
 
-**Steps To Reproduce**
+**Steps to reproduce**
 
 **Expected behavior**
 
 **Actual behavior**
 
-**Additional context**
+**Any additional context**
 
 If applicable, please attach a recording of the sound.
diff --git a/README.md b/README.md
index b61e543..3507bff 100755
--- a/README.md
+++ b/README.md
@@ -34,8 +34,10 @@
 - [Winning on Android](https://www.youtube.com/watch?v=tWBojmBpS74) - How to optimize an Android audio app. (ADC '18)
 - [Real-Time Processing on Android](https://youtu.be/hY9BrS2uX-c) (ADC '19)
 
-## Sample code
-Sample apps can be found in the [samples directory](samples). Also check out the [Rhythm Game codelab](https://codelabs.developers.google.com/codelabs/musicalgame-using-oboe/index.html#0).
+## Sample code and apps
+- Sample apps can be found in the [samples directory](samples). 
+- A complete "effects processor" app called FXLab can  be found in the [apps/fxlab folder](apps/fxlab). 
+- Also check out the [Rhythm Game codelab](https://codelabs.developers.google.com/codelabs/musicalgame-using-oboe/index.html#0).
 
 ### Third party sample code
 - [Ableton Link integration demo](https://github.com/jbloit/AndroidLinkAudio) (author: jbloit)
diff --git a/apps/fxlab/README.md b/apps/fxlab/README.md
index 0743da6..4c377c7 100644
--- a/apps/fxlab/README.md
+++ b/apps/fxlab/README.md
@@ -18,6 +18,8 @@
 
 Tap the plus button to add various effects. The current list of effects being applied to the input audio will be shown on the main screen (in order from top to bottom). Use the drag handles on the right of the effect to re-order the effects. Swiping the effect near the title to the right or left will remove the effect. Use the sliders to modify the parameters of each audio effect. Some effect combinations or parameters might lead to unpleasant sounds!
 
+By default the sound output is off when you start the app (this is to avoid a feedback loop if you're not using headphones).  Tap the "Unmute" button in the top right corner to enable sound output.
+
 ## Development
 
 A guide to the code, as well as a pdf of a slideshow discussing the code in depth are in the [docs](docs) folder.
diff --git a/apps/fxlab/app/src/main/cpp/DuplexCallback.h b/apps/fxlab/app/src/main/cpp/DuplexCallback.h
index 13b1dde..1be1bdd 100644
--- a/apps/fxlab/app/src/main/cpp/DuplexCallback.h
+++ b/apps/fxlab/app/src/main/cpp/DuplexCallback.h
@@ -35,10 +35,12 @@
 
 
     oboe::DataCallbackResult
-    onAudioReady(oboe::AudioStream *, void *audioData, int32_t numFrames) override {
+    onAudioReady(oboe::AudioStream *outputStream, void *audioData, int32_t numFrames) override {
         auto *outputData = static_cast<numeric_type *>(audioData);
+        auto outputChannelCount = outputStream->getChannelCount();
+
         // Silence first to simplify glitch detection
-        std::fill(outputData, outputData + numFrames * kChannelCount, 0);
+        std::fill(outputData, outputData + numFrames * outputChannelCount, 0);
         oboe::ResultWithValue<int32_t> result = inRef.read(inputBuffer.get(), numFrames, 0);
         int32_t framesRead = result.value();
         if (!result) {
@@ -51,7 +53,7 @@
         }
         f(inputBuffer.get(), inputBuffer.get() + framesRead);
         for (int i = 0; i < framesRead; i++) {
-            for (size_t j = 0; j < kChannelCount; j++) {
+            for (size_t j = 0; j < outputChannelCount; j++) {
                 *outputData++ = inputBuffer[i];
             }
         }
@@ -68,7 +70,6 @@
 
 private:
     int mSpinUpCallbacks = 10; // We will let the streams sync for the first few valid frames
-    static constexpr size_t kChannelCount = 2;
     const size_t kBufferSize;
     oboe::AudioStream &inRef;
     std::function<void(numeric_type *, numeric_type *)> f;
diff --git a/apps/fxlab/app/src/main/java/com/mobileer/androidfxlab/MainActivity.kt b/apps/fxlab/app/src/main/java/com/mobileer/androidfxlab/MainActivity.kt
index 0659404..3370edb 100644
--- a/apps/fxlab/app/src/main/java/com/mobileer/androidfxlab/MainActivity.kt
+++ b/apps/fxlab/app/src/main/java/com/mobileer/androidfxlab/MainActivity.kt
@@ -176,9 +176,9 @@
             NativeInterface.enable(isAudioEnabled)
 
             if (isAudioEnabled) {
-                item.setIcon(R.drawable.ic_baseline_volume_off_24)
+                item.setIcon(R.drawable.ic_baseline_audio_is_enabled_24)
             } else {
-                item.setIcon(R.drawable.ic_baseline_volume_up_24)
+                item.setIcon(R.drawable.ic_baseline_audio_is_disabled_24)
             }
             true
         }
diff --git a/apps/fxlab/app/src/main/java/com/mobileer/androidfxlab/NativeInterface.kt b/apps/fxlab/app/src/main/java/com/mobileer/androidfxlab/NativeInterface.kt
index deb18db..fb373b4 100644
--- a/apps/fxlab/app/src/main/java/com/mobileer/androidfxlab/NativeInterface.kt
+++ b/apps/fxlab/app/src/main/java/com/mobileer/androidfxlab/NativeInterface.kt
@@ -78,7 +78,7 @@
     }
 
     fun enable(enable: Boolean) {
-        Log.d("INTERFACE", "Enabling effects")
+        Log.d("INTERFACE", "Enabling effects: $enable")
         enablePassthroughNative(enable)
     }
 
diff --git a/apps/fxlab/app/src/main/res/drawable/ic_baseline_volume_off_24.xml b/apps/fxlab/app/src/main/res/drawable/ic_baseline_audio_is_disabled_24.xml
similarity index 100%
rename from apps/fxlab/app/src/main/res/drawable/ic_baseline_volume_off_24.xml
rename to apps/fxlab/app/src/main/res/drawable/ic_baseline_audio_is_disabled_24.xml
diff --git a/apps/fxlab/app/src/main/res/drawable/ic_baseline_volume_up_24.xml b/apps/fxlab/app/src/main/res/drawable/ic_baseline_audio_is_enabled_24.xml
similarity index 100%
rename from apps/fxlab/app/src/main/res/drawable/ic_baseline_volume_up_24.xml
rename to apps/fxlab/app/src/main/res/drawable/ic_baseline_audio_is_enabled_24.xml
diff --git a/apps/fxlab/app/src/main/res/menu/toolbar_menu.xml b/apps/fxlab/app/src/main/res/menu/toolbar_menu.xml
index 424bf9d..b745be1 100644
--- a/apps/fxlab/app/src/main/res/menu/toolbar_menu.xml
+++ b/apps/fxlab/app/src/main/res/menu/toolbar_menu.xml
@@ -5,7 +5,7 @@
     <!-- Add a mute toggle action to our Toolbar-->
     <item
         android:id="@+id/action_toggle_mute"
-        android:icon="@drawable/ic_baseline_volume_off_24"
+        android:icon="@drawable/ic_baseline_audio_is_disabled_24"
         android:title="@string/action_toggle_mute"
         app:showAsAction="ifRoom"
         />
diff --git a/apps/fxlab/screenshot.png b/apps/fxlab/screenshot.png
index eb0a410..013b36b 100644
--- a/apps/fxlab/screenshot.png
+++ b/apps/fxlab/screenshot.png
Binary files differ
diff --git a/docs/AndroidAudioHistory.md b/docs/AndroidAudioHistory.md
index c6ed7e8..d07c69d 100644
--- a/docs/AndroidAudioHistory.md
+++ b/docs/AndroidAudioHistory.md
@@ -4,7 +4,7 @@
 A list of important audio features, bugs, fixes and workarounds for various Android versions. 

 

 ### 10.0 Q - API 29

-- Fixed: Setting capacity of Legacy input streams < 4096 can prevent use of FAST path. https://github.com/google/oboe/issues/183

+- Fixed: Setting capacity of Legacy input streams < 4096 can prevent use of FAST path. https://github.com/google/oboe/issues/183. also ag/7116429

 - Add InputPreset:VoicePerformance for low latency recording.

 

 ### 9.0 Pie - API 28 (August 6, 2018)

diff --git a/samples/build.gradle b/samples/build.gradle
index d522839..d51e478 100644
--- a/samples/build.gradle
+++ b/samples/build.gradle
@@ -27,7 +27,7 @@
         jcenter()
     }
     dependencies {
-        classpath 'com.android.tools.build:gradle:3.5.3'
+        classpath 'com.android.tools.build:gradle:3.6.3'
         // NOTE: Do not place your application dependencies here; they belong
         // in the individual module build.gradle files
         classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
diff --git a/samples/drumthumper/src/main/cpp/DrumPlayerJNI.cpp b/samples/drumthumper/src/main/cpp/DrumPlayerJNI.cpp
index 6fb7425..665acf9 100644
--- a/samples/drumthumper/src/main/cpp/DrumPlayerJNI.cpp
+++ b/samples/drumthumper/src/main/cpp/DrumPlayerJNI.cpp
@@ -22,6 +22,10 @@
 
 #include <android/log.h>
 
+// parselib includes
+#include <io/stream/MemInputStream.h>
+#include <io/wav/WavStreamReader.h>
+
 #include <player/OneShotSampleSource.h>
 #include <player/SimpleMultiPlayer.h>
 
@@ -33,6 +37,7 @@
 #endif
 
 using namespace iolib;
+using namespace parselib;
 
 static SimpleMultiPlayer sDTPlayer;
 
@@ -60,23 +65,26 @@
 /**
  * Native (JNI) implementation of DrumPlayer.allocSampleDataNative()
  */
-JNIEXPORT void JNICALL Java_com_google_oboe_sample_drumthumper_DrumPlayer_allocSampleDataNative(
-        JNIEnv* env, jobject, jint numSampleBuffers) {
-    __android_log_print(ANDROID_LOG_INFO, TAG, "%s", "allocSampleDataNative()");
-
-    // we know in this case that the sample buffers are all 1-channel, 44.1K
-    sDTPlayer.allocSampleData(numSampleBuffers);
-}
-
 /**
  * Native (JNI) implementation of DrumPlayer.loadWavAssetNative()
  */
-JNIEXPORT void JNICALL Java_com_google_oboe_sample_drumthumper_DrumPlayer_loadWavAssetNative(JNIEnv* env, jobject, jbyteArray bytearray, jint index) {
+JNIEXPORT void JNICALL Java_com_google_oboe_sample_drumthumper_DrumPlayer_loadWavAssetNative(JNIEnv* env, jobject, jbyteArray bytearray, jint index, jfloat pan) {
     int len = env->GetArrayLength (bytearray);
 
     unsigned char* buf = new unsigned char[len];
     env->GetByteArrayRegion (bytearray, 0, len, reinterpret_cast<jbyte*>(buf));
-    sDTPlayer.loadSampleDataFromAsset(buf, len, index);
+
+    MemInputStream stream(buf, len);
+
+    WavStreamReader reader(&stream);
+    reader.parse();
+
+    SampleBuffer* sampleBuffer = new SampleBuffer();
+    sampleBuffer->loadSampleData(&reader);
+
+    OneShotSampleSource* source = new OneShotSampleSource(sampleBuffer, pan);
+    sDTPlayer.addSampleSource(source, sampleBuffer);
+
     delete[] buf;
 }
 
@@ -97,21 +105,21 @@
 /**
  * Native (JNI) implementation of DrumPlayer.getOutputReset()
  */
-JNIEXPORT jboolean JNICALL Java_com_google_oboe_sample_drumthumper_DrumPlayer_getOutputReset() {
+JNIEXPORT jboolean JNICALL Java_com_google_oboe_sample_drumthumper_DrumPlayer_getOutputReset(JNIEnv*, jobject) {
     return sDTPlayer.getOutputReset();
 }
 
 /**
  * Native (JNI) implementation of DrumPlayer.clearOutputReset()
  */
-JNIEXPORT void JNICALL Java_com_google_oboe_sample_drumthumper_DrumPlayer_clearOutputReset() {
+JNIEXPORT void JNICALL Java_com_google_oboe_sample_drumthumper_DrumPlayer_clearOutputReset(JNIEnv*, jobject) {
     sDTPlayer.clearOutputReset();
 }
 
 /**
  * Native (JNI) implementation of DrumPlayer.restartStream()
  */
-JNIEXPORT void JNICALL Java_com_google_oboe_sample_drumthumper_DrumPlayer_restartStream() {
+JNIEXPORT void JNICALL Java_com_google_oboe_sample_drumthumper_DrumPlayer_restartStream(JNIEnv*, jobject) {
     sDTPlayer.resetAll();
     if (sDTPlayer.openStream()){
         __android_log_print(ANDROID_LOG_INFO, TAG, "openStream successful");
@@ -120,6 +128,26 @@
     }
 }
 
+JNIEXPORT void JNICALL Java_com_google_oboe_sample_drumthumper_DrumPlayer_setPan(
+        JNIEnv *env, jobject thiz, jint index, jfloat pan) {
+    sDTPlayer.setPan(index, pan);
+}
+
+JNIEXPORT jfloat JNICALL Java_com_google_oboe_sample_drumthumper_DrumPlayer_getPan(
+        JNIEnv *env, jobject thiz, jint  index) {
+    return sDTPlayer.getPan(index);
+}
+
+JNIEXPORT void JNICALL Java_com_google_oboe_sample_drumthumper_DrumPlayer_setGain(
+        JNIEnv *env, jobject thiz, jint  index, jfloat gain) {
+    sDTPlayer.setGain(index, gain);
+}
+
+JNIEXPORT jfloat JNICALL Java_com_google_oboe_sample_drumthumper_DrumPlayer_getGain(
+        JNIEnv *env, jobject thiz, jint index) {
+    return sDTPlayer.getGain(index);
+}
+
 #ifdef __cplusplus
 }
 #endif
diff --git a/samples/drumthumper/src/main/java/com/google/oboe/sample/drumthumper/DrumPlayer.kt b/samples/drumthumper/src/main/java/com/google/oboe/sample/drumthumper/DrumPlayer.kt
index f171313..598766c 100644
--- a/samples/drumthumper/src/main/java/com/google/oboe/sample/drumthumper/DrumPlayer.kt
+++ b/samples/drumthumper/src/main/java/com/google/oboe/sample/drumthumper/DrumPlayer.kt
@@ -22,11 +22,10 @@
 class DrumPlayer {
     companion object {
         // Sample attributes
-        val NUM_CHANNELS: Int = 1
-        val SAMPLE_RATE: Int = 44100
+        val NUM_CHANNELS: Int = 2       // Stereo Playback, set to 1 for Mono playback
+        val SAMPLE_RATE: Int = 44100    // All the input samples are 44.1K
 
         // Sample Buffer IDs
-        val NUM_SAMPLES: Int = 8
         val BASSDRUM: Int = 0
         val SNAREDRUM: Int = 1
         val CRASHCYMBAL: Int = 2
@@ -36,6 +35,16 @@
         val HIHATOPEN: Int = 6
         val HIHATCLOSED: Int = 7
 
+        // Pan position for each drum sample
+        val PAN_BASSDRUM: Float = 0f         // Dead Center
+        val PAN_SNAREDRUM: Float = 0.25f     // A little Right
+        val PAN_CRASHCYMBAL: Float = -0.75f  // Mostly Left
+        val PAN_RIDECYMBAL: Float = 1.0f     // Hard Right
+        val PAN_MIDTOM: Float = -0.75f       // Mostly Left
+        val PAN_LOWTOM: Float = 0.75f        // Mostly Right
+        val PAN_HIHATOPEN: Float = -1.0f     // Hard Left
+        val PAN_HIHATCLOSED: Float = -1.0f   // Hard Left
+
         // Logging Tag
         val TAG: String = "DrumPlayer"
     }
@@ -49,33 +58,29 @@
     }
 
     // asset-based samples
-    fun allocSampleData() {
-        allocSampleDataNative(NUM_SAMPLES)
-    }
-
     fun loadWavAssets(assetMgr: AssetManager) {
-        loadWavAsset(assetMgr, "KickDrum.wav", BASSDRUM)
-        loadWavAsset(assetMgr, "SnareDrum.wav", SNAREDRUM)
-        loadWavAsset(assetMgr, "CrashCymbal.wav", CRASHCYMBAL)
-        loadWavAsset(assetMgr, "RideCymbal.wav", RIDECYMBAL)
-        loadWavAsset(assetMgr, "MidTom.wav", MIDTOM)
-        loadWavAsset(assetMgr, "LowTom.wav", LOWTOM)
-        loadWavAsset(assetMgr, "HiHat_Open.wav", HIHATOPEN)
-        loadWavAsset(assetMgr, "HiHat_Closed.wav", HIHATCLOSED)
+        loadWavAsset(assetMgr, "KickDrum.wav", BASSDRUM, PAN_BASSDRUM)
+        loadWavAsset(assetMgr, "SnareDrum.wav", SNAREDRUM, PAN_SNAREDRUM)
+        loadWavAsset(assetMgr, "CrashCymbal.wav", CRASHCYMBAL, PAN_CRASHCYMBAL)
+        loadWavAsset(assetMgr, "RideCymbal.wav", RIDECYMBAL, PAN_RIDECYMBAL)
+        loadWavAsset(assetMgr, "MidTom.wav", MIDTOM, PAN_MIDTOM)
+        loadWavAsset(assetMgr, "LowTom.wav", LOWTOM, PAN_LOWTOM)
+        loadWavAsset(assetMgr, "HiHat_Open.wav", HIHATOPEN, PAN_HIHATOPEN)
+        loadWavAsset(assetMgr, "HiHat_Closed.wav", HIHATCLOSED, PAN_HIHATCLOSED)
     }
 
     fun unloadWavAssets() {
         unloadWavAssetsNative()
     }
 
-    fun loadWavAsset(assetMgr: AssetManager, assetName: String, index: Int) {
+    fun loadWavAsset(assetMgr: AssetManager, assetName: String, index: Int, pan: Float) {
         try {
             val assetFD = assetMgr.openFd(assetName)
             val dataStream = assetFD.createInputStream();
             var dataLen = assetFD.getLength().toInt()
             var dataBytes: ByteArray = ByteArray(dataLen)
             dataStream.read(dataBytes, 0, dataLen)
-            loadWavAssetNative(dataBytes, index)
+            loadWavAssetNative(dataBytes, index, pan)
             assetFD.close()
         } catch (ex: IOException) {
             Log.i(TAG, "IOException" + ex)
@@ -85,12 +90,17 @@
     external fun setupAudioStreamNative(numChannels: Int, sampleRate: Int)
     external fun teardownAudioStreamNative()
 
-    external fun allocSampleDataNative(numSampleBuffers: Int)
-    external fun loadWavAssetNative(wavBytes: ByteArray, index: Int)
+    external fun loadWavAssetNative(wavBytes: ByteArray, index: Int, pan: Float)
     external fun unloadWavAssetsNative()
 
     external fun trigger(drumIndex: Int)
 
+    external fun setPan(index: Int, pan: Float)
+    external fun getPan(index: Int): Float
+
+    external fun setGain(index: Int, gain: Float)
+    external fun getGain(index: Int): Float
+
     external fun getOutputReset() : Boolean
     external fun clearOutputReset()
 
diff --git a/samples/drumthumper/src/main/java/com/google/oboe/sample/drumthumper/DrumThumperActivity.kt b/samples/drumthumper/src/main/java/com/google/oboe/sample/drumthumper/DrumThumperActivity.kt
index 3ab87db..c575e85 100644
--- a/samples/drumthumper/src/main/java/com/google/oboe/sample/drumthumper/DrumThumperActivity.kt
+++ b/samples/drumthumper/src/main/java/com/google/oboe/sample/drumthumper/DrumThumperActivity.kt
@@ -21,6 +21,10 @@
 import android.media.AudioManager
 import android.os.Bundle
 import android.util.Log
+import android.view.View
+import android.widget.Button
+import android.widget.LinearLayout
+import android.widget.SeekBar
 import android.widget.Toast
 
 import androidx.appcompat.app.AppCompatActivity
@@ -30,8 +34,12 @@
 import java.time.LocalDateTime;
 
 import kotlin.concurrent.schedule
+import kotlin.math.roundToInt
 
-class DrumThumperActivity : AppCompatActivity(), TriggerPad.DrumPadTriggerListener {
+class DrumThumperActivity : AppCompatActivity(),
+        TriggerPad.DrumPadTriggerListener,
+        SeekBar.OnSeekBarChangeListener,
+        View.OnClickListener {
     private val TAG = "DrumThumperActivity"
 
     private var mAudioMgr: AudioManager? = null
@@ -45,6 +53,8 @@
 
     private var mDeviceListener: DeviceListener = DeviceListener()
 
+    private var mMixControlsShowing = false;
+
     init {
         // Load the library containing the a native code including the JNI  functions
         System.loadLibrary("drumthumper")
@@ -99,12 +109,64 @@
         }
     }
 
+    //
+    // UI Helpers
+    //
+    val GAIN_FACTOR = 100.0f;
+    val MAX_PAN_POSITION = 200.0f;
+    val HALF_PAN_POSITION = MAX_PAN_POSITION / 2.0f
+
+    fun gainPosToGainVal(pos: Int) : Float {
+        // map 0 -> 200 to 0.0f -> 2.0f
+        return pos.toFloat() / GAIN_FACTOR
+    }
+
+    fun gainValToGainPos(value: Float) : Int {
+        return (value * GAIN_FACTOR).toInt()
+    }
+
+    fun panPosToPanVal(pos: Int) : Float {
+        // map 0 -> 200 to -1.0f -> 1..0f
+        return (pos.toFloat() - HALF_PAN_POSITION) / HALF_PAN_POSITION
+    }
+
+    fun panValToPanPos(value: Float) : Int {
+        // map -1.0f -> 1.0f to 0 -> 200
+        return ((value * HALF_PAN_POSITION) + HALF_PAN_POSITION).toInt()
+    }
+
+    fun showMixControls(show : Boolean) {
+        mMixControlsShowing = show;
+        var showFlag = if (mMixControlsShowing) View.VISIBLE else View.GONE;
+        findViewById<LinearLayout>(R.id.kickMixControls).setVisibility(showFlag)
+        findViewById<LinearLayout>(R.id.snareMixControls).setVisibility(showFlag)
+        findViewById<LinearLayout>(R.id.hihatOpenMixControls).setVisibility(showFlag)
+        findViewById<LinearLayout>(R.id.hihatClosedMixControls).setVisibility(showFlag)
+        findViewById<LinearLayout>(R.id.midTomMixControls).setVisibility(showFlag)
+        findViewById<LinearLayout>(R.id.lowTomMixControls).setVisibility(showFlag)
+        findViewById<LinearLayout>(R.id.rideMixControls).setVisibility(showFlag)
+        findViewById<LinearLayout>(R.id.crashMixControls).setVisibility(showFlag)
+
+        findViewById<Button>(R.id.mixCtrlBtn).setText(
+                if (mMixControlsShowing) "Hide Mix Controls" else "Show Mix Controls")
+    }
+
+    fun connectMixSliders(panSliderId : Int, gainSliderId : Int, drumIndex : Int) {
+        var panSeekbar = findViewById<SeekBar>(panSliderId)
+        panSeekbar.setOnSeekBarChangeListener(this)
+        panSeekbar.setProgress(panValToPanPos(mDrumPlayer.getPan(drumIndex)))
+
+        var gainSeekbar = findViewById<SeekBar>(gainSliderId)
+        gainSeekbar.setOnSeekBarChangeListener(this)
+        gainSeekbar.setProgress(gainValToGainPos(mDrumPlayer.getGain(drumIndex)))
+    }
+
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
 
         mAudioMgr = getSystemService(Context.AUDIO_SERVICE) as AudioManager
 
-        mDrumPlayer.allocSampleData()
+        // mDrumPlayer.allocSampleData()
         mDrumPlayer.loadWavAssets(getAssets())
     }
 
@@ -124,47 +186,40 @@
         // UI
         setContentView(R.layout.drumthumper_activity)
 
-        // hookup the UI
-        run {
-            var pad: TriggerPad = findViewById(R.id.kickPad)
-            pad.addListener(this)
-        }
+        // "Kick" drum
+        findViewById<TriggerPad>(R.id.kickPad).addListener(this)
+        connectMixSliders(R.id.kickPan, R.id.kickGain, DrumPlayer.BASSDRUM)
 
-        run {
-            var pad: TriggerPad = findViewById(R.id.snarePad)
-            pad.addListener(this)
-        }
+        // Snare drum
+        findViewById<TriggerPad>(R.id.snarePad).addListener(this)
+        connectMixSliders(R.id.snarePan, R.id.snareGain, DrumPlayer.SNAREDRUM)
 
-        run {
-            var pad: TriggerPad = findViewById(R.id.midTomPad)
-            pad.addListener(this)
-        }
+        // Mid tom
+        findViewById<TriggerPad>(R.id.midTomPad).addListener(this)
+        connectMixSliders(R.id.midTomPan, R.id.midTomGain, DrumPlayer.MIDTOM)
 
-        run {
-            var pad: TriggerPad = findViewById(R.id.lowTomPad)
-            pad.addListener(this)
-        }
+        // Low tom
+        findViewById<TriggerPad>(R.id.lowTomPad).addListener(this)
+        connectMixSliders(R.id.lowTomPan, R.id.lowTomGain, DrumPlayer.LOWTOM)
 
-        run {
-            var pad: TriggerPad = findViewById(R.id.hihatOpenPad)
-            pad.addListener(this)
-        }
+        // Open hihat
+        findViewById<TriggerPad>(R.id.hihatOpenPad).addListener(this)
+        connectMixSliders(R.id.hihatOpenPan, R.id.hihatOpenGain, DrumPlayer.HIHATOPEN)
 
-        run {
-            var pad: TriggerPad = findViewById(R.id.hihatClosedPad)
-            pad.addListener(this)
-        }
+        // Closed hihat
+        findViewById<TriggerPad>(R.id.hihatClosedPad).addListener(this)
+        connectMixSliders(R.id.hihatClosedPan, R.id.hihatClosedGain, DrumPlayer.HIHATCLOSED)
 
-        run {
-            var pad: TriggerPad = findViewById(R.id.ridePad)
-            pad.addListener(this)
-        }
+        // Ride cymbal
+        findViewById<TriggerPad>(R.id.ridePad).addListener(this)
+        connectMixSliders(R.id.ridePan, R.id.rideGain, DrumPlayer.RIDECYMBAL)
 
-        run {
-            var pad: TriggerPad = findViewById(R.id.crashPad)
-            pad.addListener(this)
-        }
+        // Crash cymbal
+        findViewById<TriggerPad>(R.id.crashPad).addListener(this)
+        connectMixSliders(R.id.crashPan, R.id.crashGain, DrumPlayer.CRASHCYMBAL)
 
+        findViewById<Button>(R.id.mixCtrlBtn).setOnClickListener(this)
+        showMixControls(false);
     }
 
     override fun onPause() {
@@ -206,4 +261,56 @@
     override fun triggerUp(pad: TriggerPad) {
         // NOP
     }
+
+    //
+    // SeekBar.OnSeekBarChangeListener
+    //
+    override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
+        when (seekBar!!.id) {
+            // BASSDRUM
+            R.id.kickGain -> mDrumPlayer.setGain(DrumPlayer.BASSDRUM, gainPosToGainVal(progress))
+            R.id.kickPan -> mDrumPlayer.setPan(DrumPlayer.BASSDRUM, panPosToPanVal(progress))
+
+            // SNAREDRUM
+            R.id.snareGain -> mDrumPlayer.setGain(DrumPlayer.SNAREDRUM, gainPosToGainVal(progress))
+            R.id.snarePan -> mDrumPlayer.setPan(DrumPlayer.SNAREDRUM, panPosToPanVal(progress))
+
+            // MIDTOM
+            R.id.midTomGain -> mDrumPlayer.setGain(DrumPlayer.MIDTOM, gainPosToGainVal(progress))
+            R.id.midTomPan -> mDrumPlayer.setPan(DrumPlayer.MIDTOM, panPosToPanVal(progress))
+
+            // LOWTOM
+            R.id.lowTomGain -> mDrumPlayer.setGain(DrumPlayer.LOWTOM, gainPosToGainVal(progress))
+            R.id.lowTomPan -> mDrumPlayer.setPan(DrumPlayer.LOWTOM, panPosToPanVal(progress))
+
+            // HIHATOPEN
+            R.id.hihatOpenGain -> mDrumPlayer.setGain(DrumPlayer.HIHATOPEN, gainPosToGainVal(progress))
+            R.id.hihatOpenPan -> mDrumPlayer.setPan(DrumPlayer.HIHATOPEN, panPosToPanVal(progress))
+
+            // HIHATCLOSED
+            R.id.hihatClosedGain -> mDrumPlayer.setGain(DrumPlayer.HIHATCLOSED, gainPosToGainVal(progress))
+            R.id.hihatClosedPan -> mDrumPlayer.setPan(DrumPlayer.HIHATCLOSED, panPosToPanVal(progress))
+
+            // RIDECYMBAL
+            R.id.rideGain -> mDrumPlayer.setGain(DrumPlayer.RIDECYMBAL, gainPosToGainVal(progress))
+            R.id.ridePan -> mDrumPlayer.setPan(DrumPlayer.RIDECYMBAL, panPosToPanVal(progress))
+
+            // CRASHCYMBAL
+            R.id.crashGain -> mDrumPlayer.setGain(DrumPlayer.CRASHCYMBAL, gainPosToGainVal(progress))
+            R.id.crashPan -> mDrumPlayer.setPan(DrumPlayer.CRASHCYMBAL, panPosToPanVal(progress))
+        }
+    }
+
+    override fun onStartTrackingTouch(seekBar: SeekBar?) {
+        // NOP
+    }
+
+    override fun onStopTrackingTouch(seekBar: SeekBar?) {
+        // NOP
+    }
+
+    override fun onClick(v: View?) {
+        showMixControls(!mMixControlsShowing)
+    }
+
 }
diff --git a/samples/drumthumper/src/main/res/layout-land/drumthumper_activity.xml b/samples/drumthumper/src/main/res/layout-land/drumthumper_activity.xml
index ea0a665..bcc7a49 100644
--- a/samples/drumthumper/src/main/res/layout-land/drumthumper_activity.xml
+++ b/samples/drumthumper/src/main/res/layout-land/drumthumper_activity.xml
@@ -13,44 +13,249 @@
         android:layout_height="wrap_content"
         android:gravity="center"
         android:text="DrumThumper"
-        android:textSize="22pt" />
+        android:textSize="14pt" />
 
     <LinearLayout
         android:layout_width="match_parent"
         android:layout_height="128dp"
         android:orientation="horizontal">
 
-        <com.google.oboe.sample.drumthumper.TriggerPad
-            android:id="@+id/kickPad"
-            android:layout_width="match_parent"
-            android:layout_height="match_parent"
-            android:layout_weight="1"
-            android:padding="5dp"
-            android:text="Kick" />
-
-        <com.google.oboe.sample.drumthumper.TriggerPad
-            android:id="@+id/snarePad"
+        <LinearLayout
             android:layout_width="wrap_content"
-            android:layout_height="match_parent"
-            android:layout_weight="1"
-            android:padding="5dp"
-            android:text="Snare" />
+            android:layout_height="128dp"
+            android:layout_weight=".5"
+            android:orientation="vertical">
 
-        <com.google.oboe.sample.drumthumper.TriggerPad
-            android:id="@+id/midTomPad"
-            android:layout_width="match_parent"
-            android:layout_height="match_parent"
-            android:layout_weight="1"
-            android:padding="5dp"
-            android:text="Mid Tom" />
+            <com.google.oboe.sample.drumthumper.TriggerPad
+                android:id="@+id/kickPad"
+                android:layout_width="match_parent"
+                android:layout_height="match_parent"
+                android:layout_weight="1"
+                android:padding="5dp"
+                android:text="Kick" />
 
-        <com.google.oboe.sample.drumthumper.TriggerPad
-            android:id="@+id/lowTomPad"
-            android:layout_width="match_parent"
-            android:layout_height="match_parent"
-            android:layout_weight="1"
-            android:padding="5dp"
-            android:text="Low Tom" />
+            <LinearLayout
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:orientation="vertical"
+                android:id="@+id/kickMixControls">
+                <LinearLayout
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:orientation="horizontal">
+
+                    <TextView
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:text="P:"/>
+
+                    <SeekBar
+                        android:id="@+id/kickPan"
+                        style="@android:style/Widget.DeviceDefault.SeekBar"
+                        android:layout_width="match_parent"
+                        android:layout_height="20dp"
+                        android:layout_marginTop="5dp"
+                        android:max="200" />
+                </LinearLayout>
+
+                <LinearLayout
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:orientation="horizontal">
+
+                    <TextView
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:text="G:"/>
+
+                    <SeekBar
+                        android:id="@+id/kickGain"
+                        style="@android:style/Widget.DeviceDefault.SeekBar"
+                        android:layout_width="match_parent"
+                        android:layout_height="20dp"
+                        android:layout_marginTop="5dp"
+                        android:max="200" />
+                </LinearLayout>
+            </LinearLayout>
+        </LinearLayout>
+
+        <LinearLayout
+            android:layout_width="wrap_content"
+            android:layout_height="128dp"
+            android:layout_weight=".5"
+            android:orientation="vertical">
+
+            <com.google.oboe.sample.drumthumper.TriggerPad
+                android:id="@+id/snarePad"
+                android:layout_width="wrap_content"
+                android:layout_height="match_parent"
+                android:layout_weight="1"
+                android:padding="5dp"
+                android:text="Snare" />
+
+            <LinearLayout
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:orientation="vertical"
+                android:id="@+id/snareMixControls">
+                <LinearLayout
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:orientation="horizontal">
+
+                    <TextView
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:text="P:"/>
+
+                    <SeekBar
+                        android:id="@+id/snarePan"
+                        style="@android:style/Widget.DeviceDefault.SeekBar"
+                        android:layout_width="match_parent"
+                        android:layout_height="20dp"
+                        android:layout_marginTop="5dp"
+                        android:max="200" />
+                </LinearLayout>
+
+                <LinearLayout
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:orientation="horizontal">
+
+                    <TextView
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:text="G:"/>
+
+                    <SeekBar
+                        android:id="@+id/snareGain"
+                        style="@android:style/Widget.DeviceDefault.SeekBar"
+                        android:layout_width="match_parent"
+                        android:layout_height="20dp"
+                        android:layout_marginTop="5dp"
+                        android:max="200" />
+
+                </LinearLayout>
+            </LinearLayout>
+        </LinearLayout>
+
+        <LinearLayout
+            android:layout_width="wrap_content"
+            android:layout_height="128dp"
+            android:layout_weight=".5"
+            android:orientation="vertical">
+
+            <com.google.oboe.sample.drumthumper.TriggerPad
+                android:id="@+id/hihatOpenPad"
+                android:layout_width="match_parent"
+                android:layout_height="match_parent"
+                android:layout_weight="1"
+                android:padding="5dp"
+                android:text="Open Hat" />
+
+            <LinearLayout
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:orientation="vertical"
+                android:id="@+id/hihatOpenMixControls">
+                <LinearLayout
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:orientation="horizontal">
+
+                    <TextView
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:text="P:"/>
+
+                    <SeekBar
+                        android:id="@+id/hihatOpenPan"
+                        style="@android:style/Widget.DeviceDefault.SeekBar"
+                        android:layout_width="match_parent"
+                        android:layout_height="20dp"
+                        android:layout_marginTop="5dp"
+                        android:max="200" />
+                </LinearLayout>
+
+                <LinearLayout
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:orientation="horizontal">
+
+                    <TextView
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:text="G:"/>
+
+                    <SeekBar
+                        android:id="@+id/hihatOpenGain"
+                        style="@android:style/Widget.DeviceDefault.SeekBar"
+                        android:layout_width="match_parent"
+                        android:layout_height="20dp"
+                        android:layout_marginTop="5dp"
+                        android:max="200" />
+                </LinearLayout>
+            </LinearLayout>
+        </LinearLayout>
+
+        <LinearLayout
+            android:layout_width="wrap_content"
+            android:layout_height="128dp"
+            android:layout_weight=".5"
+            android:orientation="vertical">
+
+            <com.google.oboe.sample.drumthumper.TriggerPad
+                android:id="@+id/hihatClosedPad"
+                android:layout_width="match_parent"
+                android:layout_height="match_parent"
+                android:layout_weight="1"
+                android:padding="5dp"
+                android:text="Closed Hat" />
+
+            <LinearLayout
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:orientation="vertical"
+                android:id="@+id/hihatClosedMixControls">
+                <LinearLayout
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:orientation="horizontal">
+
+                    <TextView
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:text="P:"/>
+
+                    <SeekBar
+                        android:id="@+id/hihatClosedPan"
+                        style="@android:style/Widget.DeviceDefault.SeekBar"
+                        android:layout_width="match_parent"
+                        android:layout_height="20dp"
+                        android:layout_marginTop="5dp"
+                        android:max="200" />
+                </LinearLayout>
+
+                <LinearLayout
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:orientation="horizontal">
+
+                    <TextView
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:text="G:"/>
+
+                    <SeekBar
+                        android:id="@+id/hihatClosedGain"
+                        style="@android:style/Widget.DeviceDefault.SeekBar"
+                        android:layout_width="match_parent"
+                        android:layout_height="20dp"
+                        android:layout_marginTop="5dp"
+                        android:max="200" />
+                </LinearLayout>
+            </LinearLayout>
+        </LinearLayout>
     </LinearLayout>
 
     <LinearLayout
@@ -58,38 +263,245 @@
         android:layout_height="128dp"
         android:orientation="horizontal">
 
-        <com.google.oboe.sample.drumthumper.TriggerPad
-            android:id="@+id/hihatOpenPad"
-            android:layout_width="match_parent"
-            android:layout_height="match_parent"
-            android:layout_weight="1"
-            android:padding="5dp"
-            android:text="Open Hat" />
+        <LinearLayout
+            android:layout_width="wrap_content"
+            android:layout_height="128dp"
+            android:layout_weight=".5"
+            android:orientation="vertical">
 
-        <com.google.oboe.sample.drumthumper.TriggerPad
-            android:id="@+id/hihatClosedPad"
-            android:layout_width="match_parent"
-            android:layout_height="match_parent"
-            android:layout_weight="1"
-            android:padding="5dp"
-            android:text="Closed Hat" />
+            <com.google.oboe.sample.drumthumper.TriggerPad
+                android:id="@+id/midTomPad"
+                android:layout_width="match_parent"
+                android:layout_height="match_parent"
+                android:layout_weight="1"
+                android:padding="5dp"
+                android:text="Mid Tom" />
 
-        <com.google.oboe.sample.drumthumper.TriggerPad
-            android:id="@+id/ridePad"
-            android:layout_width="match_parent"
-            android:layout_height="match_parent"
-            android:layout_weight="1"
-            android:padding="5dp"
-            android:text="Ride" />
+            <LinearLayout
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:orientation="vertical"
+                android:id="@+id/midTomMixControls">
+                <LinearLayout
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:orientation="horizontal">
 
-        <com.google.oboe.sample.drumthumper.TriggerPad
-            android:id="@+id/crashPad"
-            android:layout_width="match_parent"
-            android:layout_height="match_parent"
-            android:layout_weight="1"
-            android:padding="5dp"
-            android:text="Crash" />
+                    <TextView
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:text="P:"/>
+
+                    <SeekBar
+                        android:id="@+id/midTomPan"
+                        style="@android:style/Widget.DeviceDefault.SeekBar"
+                        android:layout_width="match_parent"
+                        android:layout_height="20dp"
+                        android:layout_marginTop="5dp"
+                        android:max="200" />
+                </LinearLayout>
+
+                <LinearLayout
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:orientation="horizontal">
+
+                    <TextView
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:text="G:"/>
+
+                    <SeekBar
+                        android:id="@+id/midTomGain"
+                        style="@android:style/Widget.DeviceDefault.SeekBar"
+                        android:layout_width="match_parent"
+                        android:layout_height="20dp"
+                        android:layout_marginTop="5dp"
+                        android:max="200" />
+                </LinearLayout>
+            </LinearLayout>
+        </LinearLayout>
+
+        <LinearLayout
+            android:layout_width="wrap_content"
+            android:layout_height="128dp"
+            android:layout_weight=".5"
+            android:orientation="vertical">
+
+            <com.google.oboe.sample.drumthumper.TriggerPad
+                android:id="@+id/lowTomPad"
+                android:layout_width="match_parent"
+                android:layout_height="match_parent"
+                android:layout_weight="1"
+                android:padding="5dp"
+                android:text="Low Tom" />
+
+            <LinearLayout
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:orientation="vertical"
+                android:id="@+id/lowTomMixControls">
+                <LinearLayout
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:orientation="horizontal">
+
+                    <TextView
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:text="P:"/>
+
+                    <SeekBar
+                        android:id="@+id/lowTomPan"
+                        style="@android:style/Widget.DeviceDefault.SeekBar"
+                        android:layout_width="match_parent"
+                        android:layout_height="20dp"
+                        android:layout_marginTop="5dp"
+                        android:max="200" />
+                </LinearLayout>
+
+                <LinearLayout
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:orientation="horizontal">
+
+                    <TextView
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:text="G:"/>
+
+                    <SeekBar
+                        android:id="@+id/lowTomGain"
+                        style="@android:style/Widget.DeviceDefault.SeekBar"
+                        android:layout_width="match_parent"
+                        android:layout_height="20dp"
+                        android:layout_marginTop="5dp"
+                        android:max="200" />
+                </LinearLayout>
+            </LinearLayout>
+        </LinearLayout>
+
+        <LinearLayout
+            android:layout_width="wrap_content"
+            android:layout_height="128dp"
+            android:layout_weight=".5"
+            android:orientation="vertical">
+
+            <com.google.oboe.sample.drumthumper.TriggerPad
+                android:id="@+id/ridePad"
+                android:layout_width="match_parent"
+                android:layout_height="match_parent"
+                android:layout_weight="1"
+                android:padding="5dp"
+                android:text="Ride" />
+
+            <LinearLayout
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:orientation="vertical"
+                android:id="@+id/rideMixControls">
+                <LinearLayout
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:orientation="horizontal">
+
+                    <TextView
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:text="P:"/>
+
+                    <SeekBar
+                        android:id="@+id/ridePan"
+                        style="@android:style/Widget.DeviceDefault.SeekBar"
+                        android:layout_width="match_parent"
+                        android:layout_height="20dp"
+                        android:layout_marginTop="5dp"
+                        android:max="200" />
+                </LinearLayout>
+
+                <LinearLayout
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:orientation="horizontal">
+
+                    <TextView
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:text="G:"/>
+
+                    <SeekBar
+                        android:id="@+id/rideGain"
+                        style="@android:style/Widget.DeviceDefault.SeekBar"
+                        android:layout_width="match_parent"
+                        android:layout_height="20dp"
+                        android:layout_marginTop="5dp"
+                        android:max="200" />
+                </LinearLayout>
+            </LinearLayout>
+        </LinearLayout>
+
+        <LinearLayout
+            android:layout_width="wrap_content"
+            android:layout_height="128dp"
+            android:layout_weight=".5"
+            android:orientation="vertical">
+
+            <com.google.oboe.sample.drumthumper.TriggerPad
+                android:id="@+id/crashPad"
+                android:layout_width="match_parent"
+                android:layout_height="match_parent"
+                android:layout_weight="1"
+                android:padding="5dp"
+                android:text="Crash" />
+
+            <LinearLayout
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:orientation="vertical"
+                android:id="@+id/crashMixControls">
+                <LinearLayout
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:orientation="horizontal">
+
+                    <TextView
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:text="P:"/>
+
+                    <SeekBar
+                        android:id="@+id/crashPan"
+                        style="@android:style/Widget.DeviceDefault.SeekBar"
+                        android:layout_width="match_parent"
+                        android:layout_height="20dp"
+                        android:layout_marginTop="5dp"
+                        android:max="200" />
+                </LinearLayout>
+
+                <LinearLayout
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:orientation="horizontal">
+
+                    <TextView
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:text="G:"/>
+
+                    <SeekBar
+                        android:id="@+id/crashGain"
+                        style="@android:style/Widget.DeviceDefault.SeekBar"
+                        android:layout_width="match_parent"
+                        android:layout_height="20dp"
+                        android:layout_marginTop="5dp"
+                        android:max="200" />
+                </LinearLayout>
+            </LinearLayout>
+        </LinearLayout>
     </LinearLayout>
-
-
+    <Button
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:text="Hide/Show Mix Controls"
+        android:id="@+id/mixCtrlBtn"/>
 </LinearLayout>
diff --git a/samples/drumthumper/src/main/res/layout/drumthumper_activity.xml b/samples/drumthumper/src/main/res/layout/drumthumper_activity.xml
index 2ff2392..e5b3c49 100644
--- a/samples/drumthumper/src/main/res/layout/drumthumper_activity.xml
+++ b/samples/drumthumper/src/main/res/layout/drumthumper_activity.xml
@@ -20,21 +20,124 @@
         android:layout_height="128dp"
         android:orientation="horizontal">
 
-        <com.google.oboe.sample.drumthumper.TriggerPad
-            android:id="@+id/kickPad"
-            android:layout_width="match_parent"
-            android:layout_height="match_parent"
-            android:layout_weight="1"
-            android:padding="5dp"
-            android:text="Kick" />
-
-        <com.google.oboe.sample.drumthumper.TriggerPad
-            android:id="@+id/snarePad"
+        <LinearLayout
             android:layout_width="wrap_content"
-            android:layout_height="match_parent"
-            android:layout_weight="1"
-            android:padding="5dp"
-            android:text="Snare" />
+            android:layout_height="128dp"
+            android:layout_weight=".5"
+            android:orientation="vertical">
+
+            <com.google.oboe.sample.drumthumper.TriggerPad
+                android:id="@+id/kickPad"
+                android:layout_width="match_parent"
+                android:layout_height="match_parent"
+                android:layout_weight="1"
+                android:padding="5dp"
+                android:text="Kick" />
+
+            <LinearLayout
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:orientation="vertical"
+                android:id="@+id/kickMixControls">
+                <LinearLayout
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:orientation="horizontal">
+
+                    <TextView
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:text="P:"/>
+
+                    <SeekBar
+                        android:id="@+id/kickPan"
+                        style="@android:style/Widget.DeviceDefault.SeekBar"
+                        android:layout_width="match_parent"
+                        android:layout_height="20dp"
+                        android:layout_marginTop="5dp"
+                        android:max="200" />
+                </LinearLayout>
+
+                <LinearLayout
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:orientation="horizontal">
+
+                    <TextView
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:text="G:"/>
+
+                    <SeekBar
+                        android:id="@+id/kickGain"
+                        style="@android:style/Widget.DeviceDefault.SeekBar"
+                        android:layout_width="match_parent"
+                        android:layout_height="20dp"
+                        android:layout_marginTop="5dp"
+                        android:max="200" />
+                </LinearLayout>
+            </LinearLayout>
+        </LinearLayout>
+
+        <LinearLayout
+            android:layout_width="wrap_content"
+            android:layout_height="128dp"
+            android:layout_weight=".5"
+            android:orientation="vertical">
+
+            <com.google.oboe.sample.drumthumper.TriggerPad
+                android:id="@+id/snarePad"
+                android:layout_width="wrap_content"
+                android:layout_height="match_parent"
+                android:layout_weight="1"
+                android:padding="5dp"
+                android:text="Snare" />
+
+            <LinearLayout
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:orientation="vertical"
+                android:id="@+id/snareMixControls">
+                <LinearLayout
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:orientation="horizontal">
+
+                    <TextView
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:text="P:"/>
+
+                    <SeekBar
+                        android:id="@+id/snarePan"
+                        style="@android:style/Widget.DeviceDefault.SeekBar"
+                        android:layout_width="match_parent"
+                        android:layout_height="20dp"
+                        android:layout_marginTop="5dp"
+                        android:max="200" />
+                </LinearLayout>
+
+                <LinearLayout
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:orientation="horizontal">
+
+                    <TextView
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:text="G:"/>
+
+                    <SeekBar
+                        android:id="@+id/snareGain"
+                        style="@android:style/Widget.DeviceDefault.SeekBar"
+                        android:layout_width="match_parent"
+                        android:layout_height="20dp"
+                        android:layout_marginTop="5dp"
+                        android:max="200" />
+
+                </LinearLayout>
+            </LinearLayout>
+        </LinearLayout>
     </LinearLayout>
 
     <LinearLayout
@@ -42,21 +145,123 @@
         android:layout_height="128dp"
         android:orientation="horizontal">
 
-        <com.google.oboe.sample.drumthumper.TriggerPad
-            android:id="@+id/hihatOpenPad"
-            android:layout_width="match_parent"
-            android:layout_height="match_parent"
-            android:layout_weight="1"
-            android:padding="5dp"
-            android:text="Open Hat" />
+        <LinearLayout
+            android:layout_width="wrap_content"
+            android:layout_height="128dp"
+            android:layout_weight=".5"
+            android:orientation="vertical">
 
-        <com.google.oboe.sample.drumthumper.TriggerPad
-            android:id="@+id/hihatClosedPad"
-            android:layout_width="match_parent"
-            android:layout_height="match_parent"
-            android:layout_weight="1"
-            android:padding="5dp"
-            android:text="Closed Hat" />
+            <com.google.oboe.sample.drumthumper.TriggerPad
+                android:id="@+id/hihatOpenPad"
+                android:layout_width="match_parent"
+                android:layout_height="match_parent"
+                android:layout_weight="1"
+                android:padding="5dp"
+                android:text="Open Hat" />
+
+            <LinearLayout
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:orientation="vertical"
+                android:id="@+id/hihatOpenMixControls">
+                <LinearLayout
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:orientation="horizontal">
+
+                    <TextView
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:text="P:"/>
+
+                    <SeekBar
+                        android:id="@+id/hihatOpenPan"
+                        style="@android:style/Widget.DeviceDefault.SeekBar"
+                        android:layout_width="match_parent"
+                        android:layout_height="20dp"
+                        android:layout_marginTop="5dp"
+                        android:max="200" />
+                </LinearLayout>
+
+                <LinearLayout
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:orientation="horizontal">
+
+                    <TextView
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:text="G:"/>
+
+                    <SeekBar
+                        android:id="@+id/hihatOpenGain"
+                        style="@android:style/Widget.DeviceDefault.SeekBar"
+                        android:layout_width="match_parent"
+                        android:layout_height="20dp"
+                        android:layout_marginTop="5dp"
+                        android:max="200" />
+                </LinearLayout>
+            </LinearLayout>
+        </LinearLayout>
+
+        <LinearLayout
+            android:layout_width="wrap_content"
+            android:layout_height="128dp"
+            android:layout_weight=".5"
+            android:orientation="vertical">
+
+            <com.google.oboe.sample.drumthumper.TriggerPad
+                android:id="@+id/hihatClosedPad"
+                android:layout_width="match_parent"
+                android:layout_height="match_parent"
+                android:layout_weight="1"
+                android:padding="5dp"
+                android:text="Closed Hat" />
+
+            <LinearLayout
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:orientation="vertical"
+                android:id="@+id/hihatClosedMixControls">
+                <LinearLayout
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:orientation="horizontal">
+
+                    <TextView
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:text="P:"/>
+
+                    <SeekBar
+                        android:id="@+id/hihatClosedPan"
+                        style="@android:style/Widget.DeviceDefault.SeekBar"
+                        android:layout_width="match_parent"
+                        android:layout_height="20dp"
+                        android:layout_marginTop="5dp"
+                        android:max="200" />
+                </LinearLayout>
+
+                <LinearLayout
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:orientation="horizontal">
+
+                    <TextView
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:text="G:"/>
+
+                    <SeekBar
+                        android:id="@+id/hihatClosedGain"
+                        style="@android:style/Widget.DeviceDefault.SeekBar"
+                        android:layout_width="match_parent"
+                        android:layout_height="20dp"
+                        android:layout_marginTop="5dp"
+                        android:max="200" />
+            </LinearLayout>
+            </LinearLayout>
+        </LinearLayout>
     </LinearLayout>
 
     <LinearLayout
@@ -64,21 +269,123 @@
         android:layout_height="128dp"
         android:orientation="horizontal">
 
-        <com.google.oboe.sample.drumthumper.TriggerPad
-            android:id="@+id/midTomPad"
-            android:layout_width="match_parent"
-            android:layout_height="match_parent"
-            android:layout_weight="1"
-            android:padding="5dp"
-            android:text="Mid Tom" />
+        <LinearLayout
+            android:layout_width="wrap_content"
+            android:layout_height="128dp"
+            android:layout_weight=".5"
+            android:orientation="vertical">
 
-        <com.google.oboe.sample.drumthumper.TriggerPad
-            android:id="@+id/lowTomPad"
-            android:layout_width="match_parent"
-            android:layout_height="match_parent"
-            android:layout_weight="1"
-            android:padding="5dp"
-            android:text="Low Tom" />
+            <com.google.oboe.sample.drumthumper.TriggerPad
+                android:id="@+id/midTomPad"
+                android:layout_width="match_parent"
+                android:layout_height="match_parent"
+                android:layout_weight="1"
+                android:padding="5dp"
+                android:text="Mid Tom" />
+
+            <LinearLayout
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:orientation="vertical"
+                android:id="@+id/midTomMixControls">
+                <LinearLayout
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:orientation="horizontal">
+
+                    <TextView
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:text="P:"/>
+
+                    <SeekBar
+                        android:id="@+id/midTomPan"
+                        style="@android:style/Widget.DeviceDefault.SeekBar"
+                        android:layout_width="match_parent"
+                        android:layout_height="20dp"
+                        android:layout_marginTop="5dp"
+                        android:max="200" />
+                </LinearLayout>
+
+                <LinearLayout
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:orientation="horizontal">
+
+                    <TextView
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:text="G:"/>
+
+                    <SeekBar
+                        android:id="@+id/midTomGain"
+                        style="@android:style/Widget.DeviceDefault.SeekBar"
+                        android:layout_width="match_parent"
+                        android:layout_height="20dp"
+                        android:layout_marginTop="5dp"
+                        android:max="200" />
+                </LinearLayout>
+            </LinearLayout>
+        </LinearLayout>
+
+        <LinearLayout
+            android:layout_width="wrap_content"
+            android:layout_height="128dp"
+            android:layout_weight=".5"
+            android:orientation="vertical">
+
+            <com.google.oboe.sample.drumthumper.TriggerPad
+                android:id="@+id/lowTomPad"
+                android:layout_width="match_parent"
+                android:layout_height="match_parent"
+                android:layout_weight="1"
+                android:padding="5dp"
+                android:text="Low Tom" />
+
+            <LinearLayout
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:orientation="vertical"
+                android:id="@+id/lowTomMixControls">
+                <LinearLayout
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:orientation="horizontal">
+
+                    <TextView
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:text="P:"/>
+
+                    <SeekBar
+                        android:id="@+id/lowTomPan"
+                        style="@android:style/Widget.DeviceDefault.SeekBar"
+                        android:layout_width="match_parent"
+                        android:layout_height="20dp"
+                        android:layout_marginTop="5dp"
+                        android:max="200" />
+                </LinearLayout>
+
+                <LinearLayout
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:orientation="horizontal">
+
+                    <TextView
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:text="G:"/>
+
+                    <SeekBar
+                        android:id="@+id/lowTomGain"
+                        style="@android:style/Widget.DeviceDefault.SeekBar"
+                        android:layout_width="match_parent"
+                        android:layout_height="20dp"
+                        android:layout_marginTop="5dp"
+                        android:max="200" />
+                </LinearLayout>
+            </LinearLayout>
+        </LinearLayout>
     </LinearLayout>
 
     <LinearLayout
@@ -86,22 +393,127 @@
         android:layout_height="128dp"
         android:orientation="horizontal">
 
-        <com.google.oboe.sample.drumthumper.TriggerPad
-            android:id="@+id/ridePad"
-            android:layout_width="match_parent"
-            android:layout_height="match_parent"
-            android:layout_weight="1"
-            android:padding="5dp"
-            android:text="Ride" />
+        <LinearLayout
+            android:layout_width="wrap_content"
+            android:layout_height="128dp"
+            android:layout_weight=".5"
+            android:orientation="vertical">
 
-        <com.google.oboe.sample.drumthumper.TriggerPad
-            android:id="@+id/crashPad"
-            android:layout_width="match_parent"
-            android:layout_height="match_parent"
-            android:layout_weight="1"
-            android:padding="5dp"
-            android:text="Crash" />
+            <com.google.oboe.sample.drumthumper.TriggerPad
+                android:id="@+id/ridePad"
+                android:layout_width="match_parent"
+                android:layout_height="match_parent"
+                android:layout_weight="1"
+                android:padding="5dp"
+                android:text="Ride" />
+
+            <LinearLayout
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:orientation="vertical"
+                android:id="@+id/rideMixControls">
+                <LinearLayout
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:orientation="horizontal">
+
+                    <TextView
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:text="P:"/>
+
+                    <SeekBar
+                        android:id="@+id/ridePan"
+                        style="@android:style/Widget.DeviceDefault.SeekBar"
+                        android:layout_width="match_parent"
+                        android:layout_height="20dp"
+                        android:layout_marginTop="5dp"
+                        android:max="200" />
+                </LinearLayout>
+
+                <LinearLayout
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:orientation="horizontal">
+
+                    <TextView
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:text="G:"/>
+
+                    <SeekBar
+                        android:id="@+id/rideGain"
+                        style="@android:style/Widget.DeviceDefault.SeekBar"
+                        android:layout_width="match_parent"
+                        android:layout_height="20dp"
+                        android:layout_marginTop="5dp"
+                        android:max="200" />
+                </LinearLayout>
+            </LinearLayout>
+        </LinearLayout>
+
+        <LinearLayout
+            android:layout_width="wrap_content"
+            android:layout_height="128dp"
+            android:layout_weight=".5"
+            android:orientation="vertical">
+
+            <com.google.oboe.sample.drumthumper.TriggerPad
+                android:id="@+id/crashPad"
+                android:layout_width="match_parent"
+                android:layout_height="match_parent"
+                android:layout_weight="1"
+                android:padding="5dp"
+                android:text="Crash" />
+
+            <LinearLayout
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:orientation="vertical"
+                android:id="@+id/crashMixControls">
+                <LinearLayout
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:orientation="horizontal">
+
+                    <TextView
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:text="P:"/>
+
+                    <SeekBar
+                        android:id="@+id/crashPan"
+                        style="@android:style/Widget.DeviceDefault.SeekBar"
+                        android:layout_width="match_parent"
+                        android:layout_height="20dp"
+                        android:layout_marginTop="5dp"
+                        android:max="200" />
+                </LinearLayout>
+
+                <LinearLayout
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:orientation="horizontal">
+
+                    <TextView
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:text="G:"/>
+
+                    <SeekBar
+                        android:id="@+id/crashGain"
+                        style="@android:style/Widget.DeviceDefault.SeekBar"
+                        android:layout_width="match_parent"
+                        android:layout_height="20dp"
+                        android:layout_marginTop="5dp"
+                        android:max="200" />
+                </LinearLayout>
+            </LinearLayout>
+        </LinearLayout>
     </LinearLayout>
-
-
+    <Button
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:text="Hide/Show Mix Controls"
+        android:id="@+id/mixCtrlBtn"/>
 </LinearLayout>
diff --git a/samples/gradle/wrapper/gradle-wrapper.properties b/samples/gradle/wrapper/gradle-wrapper.properties
index 5aeee97..f6a94fe 100644
--- a/samples/gradle/wrapper/gradle-wrapper.properties
+++ b/samples/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
-#Mon Jan 13 08:54:33 MST 2020
+#Wed Apr 15 10:44:13 MDT 2020
 distributionBase=GRADLE_USER_HOME
 distributionPath=wrapper/dists
 zipStoreBase=GRADLE_USER_HOME
 zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip
diff --git a/samples/iolib/src/main/cpp/player/DataSource.h b/samples/iolib/src/main/cpp/player/DataSource.h
index b5f9c9a..5f5cefe 100644
--- a/samples/iolib/src/main/cpp/player/DataSource.h
+++ b/samples/iolib/src/main/cpp/player/DataSource.h
@@ -28,7 +28,7 @@
 public:
     virtual ~DataSource() {};
 
-    virtual void mixAudio(float* outBuff, int numFrames) = 0;
+    virtual void mixAudio(float* outBuff, int numChannels, int numFrames) = 0;
 };
 
 }
diff --git a/samples/iolib/src/main/cpp/player/OneShotSampleSource.cpp b/samples/iolib/src/main/cpp/player/OneShotSampleSource.cpp
index c637dfe..ec1bf11 100644
--- a/samples/iolib/src/main/cpp/player/OneShotSampleSource.cpp
+++ b/samples/iolib/src/main/cpp/player/OneShotSampleSource.cpp
@@ -22,7 +22,7 @@
 
 namespace iolib {
 
-void OneShotSampleSource::mixAudio(float* outBuff, int32_t numFrames) {
+void OneShotSampleSource::mixAudio(float* outBuff, int numChannels, int32_t numFrames) {
     int32_t numSampleFrames = mSampleBuffer->getNumSampleFrames();
     int32_t numWriteFrames = mIsPlaying
                          ? std::min(numFrames, numSampleFrames - mCurFrameIndex)
@@ -30,12 +30,21 @@
 
     if (numWriteFrames != 0) {
         // Mix in the samples
-        int32_t lastIndex = mCurFrameIndex + numWriteFrames;
 
-        // investigate unrolling this loop...
+        // investigate unrolling these loops...
         const float* data  = mSampleBuffer->getSampleData();
-        for(int32_t index = 0; index < numWriteFrames; index++) {
-            outBuff[index] += data[mCurFrameIndex++];
+        if (numChannels == 1) {
+            // MONO output
+            for (int32_t frameIndex = 0; frameIndex < numWriteFrames; frameIndex++) {
+                outBuff[frameIndex] += data[mCurFrameIndex++] * mGain;
+            }
+        } else if (numChannels == 2) {
+            // STEREO output
+            int dstSampleIndex = 0;
+            for (int32_t frameIndex = 0; frameIndex < numWriteFrames; frameIndex++) {
+                outBuff[dstSampleIndex++] += data[mCurFrameIndex] * mLeftGain;
+                outBuff[dstSampleIndex++] += data[mCurFrameIndex++] * mRightGain;
+            }
         }
 
         if (mCurFrameIndex >= numSampleFrames) {
diff --git a/samples/iolib/src/main/cpp/player/OneShotSampleSource.h b/samples/iolib/src/main/cpp/player/OneShotSampleSource.h
index b78a8b8..a08a704 100644
--- a/samples/iolib/src/main/cpp/player/OneShotSampleSource.h
+++ b/samples/iolib/src/main/cpp/player/OneShotSampleSource.h
@@ -22,14 +22,15 @@
 namespace iolib {
 
 /**
- * Provides audio data which will play through once when triggered
+ * Provides audio data which will play through ONCE when triggered
+ * Currently the sample data is assumed to be MONO
  */
 class OneShotSampleSource: public SampleSource {
 public:
-    OneShotSampleSource(SampleBuffer *sampleBuffer) : SampleSource(sampleBuffer) {};
+    OneShotSampleSource(SampleBuffer *sampleBuffer, float pan) : SampleSource(sampleBuffer, pan) {};
     virtual ~OneShotSampleSource() {};
 
-    virtual void mixAudio(float* outBuff, int32_t numFrames);
+    virtual void mixAudio(float* outBuff, int numChannels, int32_t numFrames);
 };
 
 } // namespace iolib
diff --git a/samples/iolib/src/main/cpp/player/SampleBuffer.cpp b/samples/iolib/src/main/cpp/player/SampleBuffer.cpp
index 464c92f..9808ac8 100644
--- a/samples/iolib/src/main/cpp/player/SampleBuffer.cpp
+++ b/samples/iolib/src/main/cpp/player/SampleBuffer.cpp
@@ -21,6 +21,7 @@
 namespace iolib {
 
 void SampleBuffer::loadSampleData(parselib::WavStreamReader* reader) {
+    // Although we read this in, at this time we know a-priori that the data is mono
     mAudioProperties.channelCount = reader->getNumChannels();
     mAudioProperties.sampleRate = reader->getSampleRate();
 
@@ -33,7 +34,10 @@
 }
 
 void SampleBuffer::unloadSampleData() {
-    delete[] mSampleData;
+    if (mSampleData != nullptr) {
+        delete[] mSampleData;
+        mSampleData = nullptr;
+    }
     mNumSamples = 0;
 }
 
diff --git a/samples/iolib/src/main/cpp/player/SampleSource.h b/samples/iolib/src/main/cpp/player/SampleSource.h
index 367438d..69dc57b 100644
--- a/samples/iolib/src/main/cpp/player/SampleSource.h
+++ b/samples/iolib/src/main/cpp/player/SampleSource.h
@@ -28,24 +28,74 @@
 /**
  * Defines an interface for audio data provided to a player object.
  * Concrete examples include OneShotSampleBuffer. One could imagine a LoopingSampleBuffer.
+ * Supports stereo position via mPan member.
  */
 class SampleSource: public DataSource {
 public:
-    SampleSource(SampleBuffer *sampleBuffer)
-     : mSampleBuffer(sampleBuffer), mCurFrameIndex(0), mIsPlaying(false) {};
-    virtual ~SampleSource() {};
+    // Pan position of the audio in a stereo mix
+    // [left:-1.0f] <- [center: 0.0f] -> -[right: 1.0f]
+    static constexpr float PAN_HARDLEFT = -1.0f;
+    static constexpr float PAN_HARDRIGHT = 1.0f;
+    static constexpr float PAN_CENTER = 0.0f;
+
+    SampleSource(SampleBuffer *sampleBuffer, float pan)
+     : mSampleBuffer(sampleBuffer), mCurFrameIndex(0), mIsPlaying(false), mGain(1.0f) {
+        setPan(pan);
+    }
+    virtual ~SampleSource() {}
 
     void setPlayMode() { mCurFrameIndex = 0; mIsPlaying = true; }
     void setStopMode() { mIsPlaying = false; mCurFrameIndex = 0; }
 
     bool isPlaying() { return mIsPlaying; }
 
+    void setPan(float pan) {
+        if (pan < PAN_HARDLEFT) {
+            mPan = PAN_HARDLEFT;
+        } else if (pan > PAN_HARDRIGHT) {
+            mPan = PAN_HARDRIGHT;
+        } else {
+            mPan = pan;
+        }
+        calcGainFactors();
+    }
+
+    float getPan() {
+        return mPan;
+    }
+
+    void setGain(float gain) {
+        mGain = gain;
+        calcGainFactors();
+    }
+
+    float getGain() {
+        return mGain;
+    }
+
 protected:
     SampleBuffer    *mSampleBuffer;
 
     int32_t mCurFrameIndex;
 
     bool mIsPlaying;
+
+    // Logical pan value
+    float mPan;
+
+    // precomputed channel gains for pan
+    float mLeftGain;
+    float mRightGain;
+
+    // Overall gain
+    float mGain;
+
+private:
+    void calcGainFactors() {
+        // useful panning information: http://www.cs.cmu.edu/~music/icm-online/readings/panlaws/
+        float rightPan = (mPan * 0.5) + 0.5;
+        mRightGain = rightPan * mGain;
+        mLeftGain = (1.0 - rightPan) * mGain;    }
 };
 
 } // namespace wavlib
diff --git a/samples/iolib/src/main/cpp/player/SimpleMultiPlayer.cpp b/samples/iolib/src/main/cpp/player/SimpleMultiPlayer.cpp
index 2d6edbf..b9a6859 100644
--- a/samples/iolib/src/main/cpp/player/SimpleMultiPlayer.cpp
+++ b/samples/iolib/src/main/cpp/player/SimpleMultiPlayer.cpp
@@ -48,12 +48,12 @@
         __android_log_print(ANDROID_LOG_ERROR, TAG, "  streamState::Disconnected");
     }
 
-    memset(audioData, 0, numFrames * sizeof(float));
+    memset(audioData, 0, numFrames * mChannelCount * sizeof(float));
 
     // OneShotSampleSource* sources = mSampleSources.get();
     for(int32_t index = 0; index < mNumSampleBuffers; index++) {
         if (mSampleSources[index]->isPlaying()) {
-            mSampleSources[index]->mixAudio((float*)audioData, numFrames);
+            mSampleSources[index]->mixAudio((float*)audioData, mChannelCount, numFrames);
         }
     }
 
@@ -134,31 +134,17 @@
     }
 }
 
-void SimpleMultiPlayer::allocSampleData(int32_t numSampleBuffers) {
-    mNumSampleBuffers = numSampleBuffers;
-
-    for(int index = 0; index < numSampleBuffers; index++) {
-        SampleBuffer* buffer = new SampleBuffer();
-        OneShotSampleSource* source = new OneShotSampleSource(buffer);
-        mSampleBuffers.push_back(buffer);
-        mSampleSources.push_back(source);
-    }
-}
-
-void SimpleMultiPlayer::loadSampleDataFromAsset(byte* dataBytes, int32_t dataLen, int32_t index) {
-    __android_log_print(ANDROID_LOG_INFO, TAG, "loadSampleDataFromAsset()");
-    MemInputStream stream(dataBytes, dataLen);
-
-    WavStreamReader reader(&stream);
-    reader.parse();
-
-    mSampleBuffers[index]->loadSampleData(&reader);
+void SimpleMultiPlayer::addSampleSource(SampleSource* source, SampleBuffer* buffer) {
+    mSampleBuffers.push_back(buffer);
+    mSampleSources.push_back(source);
+    mNumSampleBuffers++;
 }
 
 void SimpleMultiPlayer::unloadSampleData() {
     __android_log_print(ANDROID_LOG_INFO, TAG, "unloadSampleData()");
+    resetAll();
+
     for (int32_t bufferIndex = 0; bufferIndex < mNumSampleBuffers; bufferIndex++) {
-        mSampleBuffers[bufferIndex]->unloadSampleData();
         delete mSampleBuffers[bufferIndex];
         delete mSampleSources[bufferIndex];
     }
@@ -187,4 +173,20 @@
     }
 }
 
+void SimpleMultiPlayer::setPan(int index, float pan) {
+    mSampleSources[index]->setPan(pan);
+}
+
+float SimpleMultiPlayer::getPan(int index) {
+    return mSampleSources[index]->getPan();
+}
+
+void SimpleMultiPlayer::setGain(int index, float gain) {
+    mSampleSources[index]->setGain(gain);
+}
+
+float SimpleMultiPlayer::getGain(int index) {
+    return mSampleSources[index]->getGain();
+}
+
 }
diff --git a/samples/iolib/src/main/cpp/player/SimpleMultiPlayer.h b/samples/iolib/src/main/cpp/player/SimpleMultiPlayer.h
index 8371cb0..455bc57 100644
--- a/samples/iolib/src/main/cpp/player/SimpleMultiPlayer.h
+++ b/samples/iolib/src/main/cpp/player/SimpleMultiPlayer.h
@@ -47,8 +47,16 @@
     bool openStream();
 
     // Wave Sample Loading...
-    void allocSampleData(int32_t numSampleBuffers);
-    void loadSampleDataFromAsset(byte* dataBytes, int32_t dataLen, int32_t index);
+    /**
+     * Adds the SampleSource/Samplebuffer pair to the list of source channels.
+     * Transfers ownership of those objects so that they can be deleted/unloaded.
+     * The indexes associated with each source channel is the order in which they
+     * are added.
+     */
+    void addSampleSource(SampleSource* source, SampleBuffer* buffer);
+    /**
+     * Deallocates and deletes all added source/buffer (see addSampleSource()).
+     */
     void unloadSampleData();
 
     void triggerDown(int32_t index);
@@ -59,18 +67,24 @@
     bool getOutputReset() { return mOutputReset; }
     void clearOutputReset() { mOutputReset = false; }
 
+    void setPan(int index, float pan);
+    float getPan(int index);
+
+    void setGain(int index, float gain);
+    float getGain(int index);
+
 private:
     // Oboe Audio Stream
     oboe::ManagedStream mAudioStream;
 
-    // Audio attributs
+    // Audio attributes
     int32_t mChannelCount;
     int32_t mSampleRate;
 
     // Sample Data
     int32_t mNumSampleBuffers;
-    std::vector<SampleBuffer*> mSampleBuffers;
-    std::vector<OneShotSampleSource*>   mSampleSources;
+    std::vector<SampleBuffer*>  mSampleBuffers;
+    std::vector<SampleSource*>  mSampleSources;
 
     bool    mOutputReset;
 };
diff --git a/samples/parselib/src/main/cpp/io/stream/MemInputStream.h b/samples/parselib/src/main/cpp/io/stream/MemInputStream.h
index 5999249..9608c10 100644
--- a/samples/parselib/src/main/cpp/io/stream/MemInputStream.h
+++ b/samples/parselib/src/main/cpp/io/stream/MemInputStream.h
@@ -26,7 +26,7 @@
 class MemInputStream : public InputStream {

 public:

     /** constructor. Caller is presumed to have allocated and filled the memory buffer */

-    MemInputStream(unsigned char *buff, int32_t len) : mBuffer(buff), mPos(0), mBufferLen(len) {}

+    MemInputStream(unsigned char *buff, int32_t len) : mBuffer(buff), mBufferLen(len), mPos(0) {}

     virtual ~MemInputStream() {}

 

     virtual int32_t read(void *buff, int32_t numBytes);