diff --git a/apps/OboeTester/app/src/main/AndroidManifest.xml b/apps/OboeTester/app/src/main/AndroidManifest.xml
index ba91a8a3d..c5b5cfe7f 100644
--- a/apps/OboeTester/app/src/main/AndroidManifest.xml
+++ b/apps/OboeTester/app/src/main/AndroidManifest.xml
@@ -94,6 +94,10 @@
android:name=".TestDataPathsActivity"
android:label="@string/title_data_paths"
android:screenOrientation="portrait" />
+
mSinkFloat;
};
+/**
+ * Test multiple streams.
+ */
+class ActivityTestMultiStream : public ActivityContext {
+public:
+ class MultiStreamCallback : public oboe::AudioStreamDataCallback {
+ public:
+ MultiStreamCallback() {
+ for (int i = 0; i < 8; i++) mPeakLevels[i] = 0.0;
+ }
+
+ oboe::DataCallbackResult onAudioReady(oboe::AudioStream *audioStream, void *audioData, int32_t numFrames) override {
+ int channelCount = audioStream->getChannelCount();
+ if (channelCount > 8) channelCount = 8;
+
+ if (audioStream->getDirection() == oboe::Direction::Output) {
+ int channelCount = audioStream->getChannelCount();
+ float phaseIncrement = 440.0f * M_PI * 2 / (float)audioStream->getSampleRate();
+ if (audioStream->getFormat() == oboe::AudioFormat::Float) {
+ float *floatData = (float *) audioData;
+ for (int i = 0; i < numFrames; i++) {
+ float sample = sinf(mPhase) * 0.5f;
+ mPhase += phaseIncrement;
+ if (mPhase > M_PI * 2) mPhase -= (float)(M_PI * 2);
+ for (int c = 0; c < channelCount; c++) {
+ *floatData++ = sample;
+ }
+ }
+ } else if (audioStream->getFormat() == oboe::AudioFormat::I16) {
+ int16_t *shortData = (int16_t *) audioData;
+ for (int i = 0; i < numFrames; i++) {
+ float sample = sinf(mPhase) * 0.5f;
+ mPhase += phaseIncrement;
+ if (mPhase > M_PI * 2) mPhase -= (float)(M_PI * 2);
+ for (int c = 0; c < channelCount; c++) {
+ *shortData++ = (int16_t)(sample * 32767);
+ }
+ }
+ } else if (audioStream->getFormat() == oboe::AudioFormat::I24) {
+ uint8_t *byteData = (uint8_t *) audioData;
+ for (int i = 0; i < numFrames; i++) {
+ float sample = sinf(mPhase) * 0.5f;
+ mPhase += phaseIncrement;
+ if (mPhase > M_PI * 2) mPhase -= (float)(M_PI * 2);
+ int32_t sample24 = (int32_t)(sample * 8388607);
+ for (int c = 0; c < channelCount; c++) {
+ *byteData++ = (uint8_t)(sample24 & 0xFF);
+ *byteData++ = (uint8_t)((sample24 >> 8) & 0xFF);
+ *byteData++ = (uint8_t)((sample24 >> 16) & 0xFF);
+ }
+ }
+ }
+ }
+
+ // Measure peak level for both input and output
+ if (audioStream->getFormat() == oboe::AudioFormat::Float) {
+ float *floatData = (float *) audioData;
+ for (int i = 0; i < numFrames; i++) {
+ for (int c = 0; c < channelCount; c++) {
+ float sample = std::abs(*floatData++);
+ if (sample > mPeakLevels[c]) mPeakLevels[c] = sample;
+ }
+ }
+ } else if (audioStream->getFormat() == oboe::AudioFormat::I16) {
+ int16_t *shortData = (int16_t *) audioData;
+ for (int i = 0; i < numFrames; i++) {
+ for (int c = 0; c < channelCount; c++) {
+ float sample = std::abs(*shortData++) / 32768.0f;
+ if (sample > mPeakLevels[c]) mPeakLevels[c] = sample;
+ }
+ }
+ } else if (audioStream->getFormat() == oboe::AudioFormat::I24) {
+ uint8_t *byteData = (uint8_t *) audioData;
+ for (int i = 0; i < numFrames; i++) {
+ for (int c = 0; c < channelCount; c++) {
+ int32_t sample24 = (byteData[0]) | (byteData[1] << 8) | (byteData[2] << 16);
+ if (sample24 & 0x800000) sample24 |= ~0xFFFFFF; // sign extend
+ float sample = std::abs(sample24) / 8388608.0f;
+ byteData += 3;
+ if (sample > mPeakLevels[c]) mPeakLevels[c] = sample;
+ }
+ }
+ }
+
+ return oboe::DataCallbackResult::Continue;
+ }
+
+ double getPeakLevel(int index) {
+ if (index < 0 || index >= 8) return 0.0;
+ double peak = mPeakLevels[index];
+ mPeakLevels[index] = 0.0;
+ return peak;
+ }
+ private:
+ float mPhase = 0.0f;
+ double mPeakLevels[8];
+ };
+
+ ActivityTestMultiStream() = default;
+
+ virtual ~ActivityTestMultiStream() = default;
+
+ void configureBuilder(bool isInput, oboe::AudioStreamBuilder &builder) override {
+ std::shared_ptr callback = std::make_shared();
+ mCallbacks.push_back(callback);
+ builder.setDataCallback(callback);
+ }
+
+ oboe::Result startStreams() override {
+ oboe::Result result = oboe::Result::OK;
+ for (auto entry : mOboeStreams) {
+ std::shared_ptr oboeStream = entry.second;
+ if (oboeStream) {
+ result = oboeStream->requestStart();
+ if (result != oboe::Result::OK) break;
+ }
+ }
+ return result;
+ }
+
+ double getPeakLevel(int streamIndex, int channelIndex) override {
+ std::shared_ptr oboeStream = getStream(streamIndex);
+ if (oboeStream) {
+ oboe::AudioStreamDataCallback *callback = oboeStream->getDataCallback();
+ if (callback) {
+ MultiStreamCallback *myCallback = static_cast(callback);
+ return myCallback->getPeakLevel(channelIndex);
+ }
+ }
+ return 0.0;
+ }
+
+private:
+ std::vector> mCallbacks;
+};
+
/**
* Global context for native tests.
* Switch between various ActivityContexts.
@@ -910,6 +1050,9 @@ class NativeAudioContext {
case ActivityType::DataPath:
currentActivity = &mActivityDataPath;
break;
+ case ActivityType::TestMultiStream:
+ currentActivity = &mActivityTestMultiStream;
+ break;
}
}
@@ -926,6 +1069,7 @@ class NativeAudioContext {
ActivityGlitches mActivityGlitches;
ActivityDataPath mActivityDataPath;
ActivityTestDisconnect mActivityTestDisconnect;
+ ActivityTestMultiStream mActivityTestMultiStream;
private:
@@ -941,6 +1085,8 @@ class NativeAudioContext {
Glitches = 6,
TestDisconnect = 7,
DataPath = 8,
+ DynamicWorkload = 9,
+ TestMultiStream = 10,
};
ActivityType mActivityType = ActivityType::Undefined;
diff --git a/apps/OboeTester/app/src/main/cpp/jni-bridge.cpp b/apps/OboeTester/app/src/main/cpp/jni-bridge.cpp
index 359f535cf..32a679bd6 100644
--- a/apps/OboeTester/app/src/main/cpp/jni-bridge.cpp
+++ b/apps/OboeTester/app/src/main/cpp/jni-bridge.cpp
@@ -85,6 +85,14 @@ Java_com_mobileer_oboetester_OboeAudioStream_openNative(JNIEnv *env, jobject,
jint spatializationBehavior,
jstring packageName,
jstring attributionTag);
+JNIEXPORT jint JNICALL
+Java_com_mobileer_oboetester_OboeAudioStream_startNative(JNIEnv *env, jobject, jint);
+JNIEXPORT jint JNICALL
+Java_com_mobileer_oboetester_OboeAudioStream_pauseNative(JNIEnv *env, jobject, jint);
+JNIEXPORT jint JNICALL
+Java_com_mobileer_oboetester_OboeAudioStream_stopNative(JNIEnv *env, jobject, jint);
+JNIEXPORT jint JNICALL
+Java_com_mobileer_oboetester_OboeAudioStream_flushNative(JNIEnv *env, jobject, jint);
JNIEXPORT void JNICALL
Java_com_mobileer_oboetester_OboeAudioStream_close(JNIEnv *env, jobject, jint);
@@ -313,6 +321,39 @@ Java_com_mobileer_oboetester_OboeAudioStream_startPlaybackNative(JNIEnv *env, jo
return (jint) engine.getCurrentActivity()->startPlayback();
}
+JNIEXPORT jint JNICALL
+Java_com_mobileer_oboetester_OboeAudioStream_startNative(JNIEnv *env, jobject, jint streamIndex) {
+ std::shared_ptr oboeStream = engine.getCurrentActivity()->getStream(streamIndex);
+ if (oboeStream) return (jint) oboeStream->requestStart();
+ return (jint) oboe::Result::ErrorNull;
+}
+
+JNIEXPORT jint JNICALL
+Java_com_mobileer_oboetester_OboeAudioStream_pauseNative(JNIEnv *env, jobject, jint streamIndex) {
+ std::shared_ptr oboeStream = engine.getCurrentActivity()->getStream(streamIndex);
+ if (oboeStream) return (jint) oboeStream->requestPause();
+ return (jint) oboe::Result::ErrorNull;
+}
+
+JNIEXPORT jint JNICALL
+Java_com_mobileer_oboetester_OboeAudioStream_stopNative(JNIEnv *env, jobject, jint streamIndex) {
+ std::shared_ptr oboeStream = engine.getCurrentActivity()->getStream(streamIndex);
+ if (oboeStream) return (jint) oboeStream->requestStop();
+ return (jint) oboe::Result::ErrorNull;
+}
+
+JNIEXPORT jint JNICALL
+Java_com_mobileer_oboetester_OboeAudioStream_flushNative(JNIEnv *env, jobject, jint streamIndex) {
+ std::shared_ptr oboeStream = engine.getCurrentActivity()->getStream(streamIndex);
+ if (oboeStream) return (jint) oboeStream->requestFlush();
+ return (jint) oboe::Result::ErrorNull;
+}
+
+JNIEXPORT jdouble JNICALL
+Java_com_mobileer_oboetester_OboeAudioStream_getPeakLevelNative(JNIEnv *env, jobject, jint streamIndex, jint channelIndex) {
+ return engine.getCurrentActivity()->getPeakLevel(streamIndex, channelIndex);
+}
+
JNIEXPORT void JNICALL
Java_com_mobileer_oboetester_OboeAudioStream_close(JNIEnv *env, jobject, jint streamIndex) {
engine.getCurrentActivity()->close(streamIndex);
diff --git a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/AudioInputTester.java b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/AudioInputTester.java
index a03824bf8..397964db3 100644
--- a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/AudioInputTester.java
+++ b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/AudioInputTester.java
@@ -21,7 +21,7 @@
class AudioInputTester extends AudioStreamTester{
private static AudioInputTester mInstance;
- private AudioInputTester() {
+ public AudioInputTester() {
super();
Log.i(TapToToneActivity.TAG, "create OboeAudioStream ---------");
@@ -30,10 +30,7 @@ private AudioInputTester() {
}
public static synchronized AudioInputTester getInstance() {
- if (mInstance == null) {
- mInstance = new AudioInputTester();
- }
- return mInstance;
+ return new AudioInputTester();
}
public native double getPeakLevel(int i);
diff --git a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/AudioOutputTester.java b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/AudioOutputTester.java
index 04c3f2e35..83d6b9f51 100644
--- a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/AudioOutputTester.java
+++ b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/AudioOutputTester.java
@@ -25,13 +25,10 @@ public class AudioOutputTester extends AudioStreamTester {
private static AudioOutputTester mInstance;
public static synchronized AudioOutputTester getInstance() {
- if (mInstance == null) {
- mInstance = new AudioOutputTester();
- }
- return mInstance;
+ return new AudioOutputTester();
}
- private AudioOutputTester() {
+ public AudioOutputTester() {
super();
Log.i(TapToToneActivity.TAG, "create OboeAudioOutputStream ---------");
mOboeAudioOutputStream = new OboeAudioOutputStream();
diff --git a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/ExtraTestsActivity.java b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/ExtraTestsActivity.java
index c6a2ec8f1..262dab00b 100644
--- a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/ExtraTestsActivity.java
+++ b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/ExtraTestsActivity.java
@@ -54,4 +54,8 @@ public void onLaunchAudioWorkloadTestRunner(View view) {
public void onLaunchReverseJniTest(View view) {
launchTestActivity(ReverseJniActivity.class);
}
+
+ public void onLaunchTestMultiStream(View view) {
+ launchTestActivity(TestMultiStreamActivity.class);
+ }
}
diff --git a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/OboeAudioStream.java b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/OboeAudioStream.java
index a151af09d..5eafba8b2 100644
--- a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/OboeAudioStream.java
+++ b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/OboeAudioStream.java
@@ -145,6 +145,21 @@ public void close() {
}
public native void close(int streamIndex);
+ public int start() { return startNative(mStreamIndex); }
+ public native int startNative(int streamIndex);
+
+ public int pause() { return pauseNative(mStreamIndex); }
+ public native int pauseNative(int streamIndex);
+
+ public int stop() { return stopNative(mStreamIndex); }
+ public native int stopNative(int streamIndex);
+
+ public int flush() { return flushNative(mStreamIndex); }
+ public native int flushNative(int streamIndex);
+
+ public double getPeakLevel(int channelIndex) { return getPeakLevelNative(mStreamIndex, channelIndex); }
+ public native double getPeakLevelNative(int streamIndex, int channelIndex);
+
@Override
public int getBufferCapacityInFrames() {
return getBufferCapacityInFrames(mStreamIndex);
diff --git a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestAudioActivity.java b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestAudioActivity.java
index 83abd871c..33c04b8ab 100644
--- a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestAudioActivity.java
+++ b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestAudioActivity.java
@@ -94,6 +94,7 @@ abstract class TestAudioActivity extends AppCompatActivity implements AudioManag
public static final int ACTIVITY_TEST_DISCONNECT = 7;
public static final int ACTIVITY_DATA_PATHS = 8;
public static final int ACTIVITY_DYNAMIC_WORKLOAD = 9;
+ public static final int ACTIVITY_TEST_MULTI_STREAM = 10;
private static final int MP3_RES_ID = R.raw.sine441stereo;
private static final AudioConfig MP3_FILE_CONFIG =
@@ -301,6 +302,9 @@ public int getServiceType() {
| ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE;
case ACTIVITY_DYNAMIC_WORKLOAD:
return ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK;
+ case ACTIVITY_TEST_MULTI_STREAM:
+ return ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK
+ | ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE;
default:
Log.i(TAG, "getServiceType() called on unknown activity type " + getActivityType());
return 0;
diff --git a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestMultiStreamActivity.java b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestMultiStreamActivity.java
new file mode 100644
index 000000000..4bcd4c2e7
--- /dev/null
+++ b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestMultiStreamActivity.java
@@ -0,0 +1,236 @@
+/*
+ * Copyright 2026 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.mobileer.oboetester;
+
+import android.os.Bundle;
+import android.view.View;
+import android.widget.Button;
+import java.io.IOException;
+
+public class TestMultiStreamActivity extends TestAudioActivity {
+
+ private class MultiStreamTester {
+ AudioStreamTester tester;
+ StreamConfigurationView configView;
+ View container;
+
+ int mAudioState = AUDIO_STATE_CLOSED;
+ Button btnOpen, btnStart, btnPause, btnFlush, btnStop, btnClose;
+ VolumeBarView[] mVolumeBars = new VolumeBarView[2];
+
+ MultiStreamTester(View container, boolean isInput) {
+ this.container = container;
+ this.tester = isInput ? new AudioInputTester() : new AudioOutputTester();
+
+ this.configView = container.findViewById(R.id.streamConfiguration);
+ this.configView.setOutput(!isInput);
+
+ btnOpen = container.findViewById(R.id.button_open);
+ btnStart = container.findViewById(R.id.button_start);
+ btnPause = container.findViewById(R.id.button_pause);
+ btnFlush = container.findViewById(R.id.button_flush);
+ btnStop = container.findViewById(R.id.button_stop);
+ btnClose = container.findViewById(R.id.button_close);
+
+ mVolumeBars[0] = container.findViewById(R.id.volumeBar0);
+ mVolumeBars[1] = container.findViewById(R.id.volumeBar1);
+
+ btnOpen.setOnClickListener(v -> openStream());
+ btnStart.setOnClickListener(v -> startStream());
+ btnPause.setOnClickListener(v -> pauseStream());
+ btnFlush.setOnClickListener(v -> flushStream());
+ btnStop.setOnClickListener(v -> stopStream());
+ btnClose.setOnClickListener(v -> closeStream());
+
+ updateButtons();
+ }
+
+ void updateButtons() {
+ btnOpen.setBackgroundColor(mAudioState == AUDIO_STATE_OPEN ? COLOR_ACTIVE : COLOR_IDLE);
+ btnStart.setBackgroundColor(mAudioState == AUDIO_STATE_STARTED ? COLOR_ACTIVE : COLOR_IDLE);
+ btnPause.setBackgroundColor(mAudioState == AUDIO_STATE_PAUSED ? COLOR_ACTIVE : COLOR_IDLE);
+ btnFlush.setBackgroundColor(mAudioState == AUDIO_STATE_FLUSHED ? COLOR_ACTIVE : COLOR_IDLE);
+ btnStop.setBackgroundColor(mAudioState == AUDIO_STATE_STOPPED ? COLOR_ACTIVE : COLOR_IDLE);
+ btnClose.setBackgroundColor(mAudioState == AUDIO_STATE_CLOSED ? COLOR_ACTIVE : COLOR_IDLE);
+ configView.setChildrenEnabled(mAudioState == AUDIO_STATE_CLOSED);
+ }
+
+ void openStream() {
+ try {
+ configView.applyToModel(tester.requestedConfiguration);
+ tester.open();
+ mAudioState = AUDIO_STATE_OPEN;
+ configView.updateDisplay(tester.actualConfiguration);
+ updateButtons();
+ } catch (IOException e) {
+ showErrorToast(e.getMessage());
+ }
+ }
+
+ void startStream() {
+ OboeAudioStream stream = (OboeAudioStream) tester.getCurrentAudioStream();
+ if (stream != null) {
+ stream.start();
+ mAudioState = AUDIO_STATE_STARTED;
+ updateButtons();
+ }
+ }
+
+ void pauseStream() {
+ OboeAudioStream stream = (OboeAudioStream) tester.getCurrentAudioStream();
+ if (stream != null) {
+ stream.pause();
+ mAudioState = AUDIO_STATE_PAUSED;
+ updateButtons();
+ }
+ }
+
+ void flushStream() {
+ OboeAudioStream stream = (OboeAudioStream) tester.getCurrentAudioStream();
+ if (stream != null) {
+ stream.flush();
+ mAudioState = AUDIO_STATE_FLUSHED;
+ updateButtons();
+ }
+ }
+
+ void stopStream() {
+ OboeAudioStream stream = (OboeAudioStream) tester.getCurrentAudioStream();
+ if (stream != null) {
+ stream.stop();
+ mAudioState = AUDIO_STATE_STOPPED;
+ updateButtons();
+ }
+ }
+
+ void closeStream() {
+ if (tester.getCurrentAudioStream() != null) {
+ tester.close();
+ mAudioState = AUDIO_STATE_CLOSED;
+ configView.updateDisplay(tester.actualConfiguration);
+ updateButtons();
+ }
+ }
+ }
+
+ private MultiStreamTester mOut1;
+ private MultiStreamTester mOut2;
+ private MultiStreamTester mIn1;
+ private MultiStreamTester mIn2;
+
+ private android.os.Handler mStatusHandler = new android.os.Handler(android.os.Looper.getMainLooper());
+ private Runnable mStatusRunnable = new Runnable() {
+ @Override
+ public void run() {
+ updateStreamStatus(mOut1);
+ updateStreamStatus(mOut2);
+ updateStreamStatus(mIn1);
+ updateStreamStatus(mIn2);
+ mStatusHandler.postDelayed(this, 200);
+ }
+ };
+
+ private void updateStreamStatus(MultiStreamTester testerContext) {
+ if (testerContext == null || testerContext.mAudioState == AUDIO_STATE_CLOSED || testerContext.tester.getCurrentAudioStream() == null) return;
+ AudioStreamBase stream = testerContext.tester.getCurrentAudioStream();
+ AudioStreamBase.StreamStatus status = stream.getStreamStatus();
+ AudioStreamBase.DoubleStatistics latencyStatistics = stream.getLatencyStatistics();
+ int errorCode = stream.getLastErrorCallbackResult();
+
+ int framesPerBurst = stream.getFramesPerBurst();
+ status.framesPerCallback = 0;
+ String msg = "";
+ msg += "timestamp.latency = " + latencyStatistics.dump() + "\n";
+ msg += "lastErrorCallbackResult = " + StreamConfiguration.convertErrorToText(errorCode) + "\n";
+ msg += status.dump(framesPerBurst);
+ testerContext.configView.setStatusText(msg);
+
+ if (testerContext.mAudioState == AUDIO_STATE_STARTED) {
+ int numChannels = stream.getChannelCount();
+ if (numChannels > 2) numChannels = 2;
+ for (int i = 0; i < numChannels; i++) {
+ if (testerContext.mVolumeBars[i] != null) {
+ double level = ((OboeAudioStream) stream).getPeakLevel(i);
+ testerContext.mVolumeBars[i].setAmplitude((float) level);
+ }
+ }
+ } else {
+ for (int i = 0; i < 2; i++) {
+ if (testerContext.mVolumeBars[i] != null) {
+ testerContext.mVolumeBars[i].setAmplitude(0.0f);
+ }
+ }
+ }
+ }
+
+ @Override
+ protected void inflateActivity() {
+ setContentView(R.layout.activity_test_multi_stream);
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ mOut1 = new MultiStreamTester(findViewById(R.id.output1), false);
+ mOut2 = new MultiStreamTester(findViewById(R.id.output2), false);
+ mIn1 = new MultiStreamTester(findViewById(R.id.input1), true);
+ mIn2 = new MultiStreamTester(findViewById(R.id.input2), true);
+
+ mCommunicationDeviceView = (CommunicationDeviceView) findViewById(R.id.comm_device_view);
+
+ updateEnabledWidgets();
+ }
+
+ @Override
+ int getActivityType() {
+ return ACTIVITY_TEST_MULTI_STREAM;
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ mStatusHandler.post(mStatusRunnable);
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ mStatusHandler.removeCallbacks(mStatusRunnable);
+ }
+
+ @Override
+ boolean isOutput() {
+ return true; // Contains outputs
+ }
+
+ @Override
+ protected void findAudioCommon() {
+ // Initialize this so TestAudioActivity doesn't crash in setConfigViewsEnabled
+ mStreamContexts = new java.util.ArrayList<>();
+ // Do not call super.findAudioCommon() so TestAudioActivity doesn't hijack the first stream's buttons!
+ }
+
+ @Override
+ protected void resetConfiguration() {
+ super.resetConfiguration();
+ mOut1.tester.reset();
+ mOut2.tester.reset();
+ mIn1.tester.reset();
+ mIn2.tester.reset();
+ }
+}
diff --git a/apps/OboeTester/app/src/main/res/layout/activity_extra_tests.xml b/apps/OboeTester/app/src/main/res/layout/activity_extra_tests.xml
index 206b87697..b8f6ccfa6 100644
--- a/apps/OboeTester/app/src/main/res/layout/activity_extra_tests.xml
+++ b/apps/OboeTester/app/src/main/res/layout/activity_extra_tests.xml
@@ -146,5 +146,15 @@
android:backgroundTint="@color/button_tint"
android:onClick="onLaunchReverseJniTest"
android:text="Reverse JNI" />
+
+
diff --git a/apps/OboeTester/app/src/main/res/layout/activity_test_multi_stream.xml b/apps/OboeTester/app/src/main/res/layout/activity_test_multi_stream.xml
new file mode 100644
index 000000000..0ab984019
--- /dev/null
+++ b/apps/OboeTester/app/src/main/res/layout/activity_test_multi_stream.xml
@@ -0,0 +1,85 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/OboeTester/app/src/main/res/layout/layout_multi_stream_config.xml b/apps/OboeTester/app/src/main/res/layout/layout_multi_stream_config.xml
new file mode 100644
index 000000000..b2e0d75c0
--- /dev/null
+++ b/apps/OboeTester/app/src/main/res/layout/layout_multi_stream_config.xml
@@ -0,0 +1,93 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+