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" /> + +