From a5c90b61df46cbc101aea4c7db1dc073e10d16ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Sat, 24 Oct 2026 23:43:46 +0200 Subject: [PATCH] Android: Use OpenSL ES for audio on TVs, add audio API and MMAP settings An Android 11 TV played nothing over a Bluetooth headset since the switch to Oboe, while its speakers worked. Testing on it showed AAudio with MMAP is silent there and AAudio without MMAP works, as does OpenSL ES (also when the headset is connected mid-game), whatever the stream usage. MMAP should never be used for a Bluetooth route, so it's a firmware bug. Audio settings get an Audio API choice and a Low-latency audio (MMAP) checkbox, with hints, so other devices with the same kind of bug can be worked around. The API defaults to OpenSL ES on TVs and before Android 8.1, where the choice isn't offered, and AAudio elsewhere. Changing them reopens the streams right away. System Information shows the output stream that got opened (API, MMAP or not, rate, buffers, device) and any audio error. Co-Authored-By: Claude Opus 5.5 (1M context) --- Core/Config.cpp | 16 ++++++++++ Core/Config.h | 3 ++ Core/ConfigValues.h | 5 ++++ UI/GameSettingsScreen.cpp | 17 +++++++++++ UI/SystemInfoScreen.cpp | 13 ++++++++ android/jni/AndroidAudio.cpp | 58 ++++++++++++++++++++++++++++++------ android/jni/AndroidAudio.h | 5 ++++ android/jni/OboeContext.cpp | 24 +++++++++++++-- 8 files changed, 129 insertions(+), 12 deletions(-) diff --git a/Core/Config.cpp b/Core/Config.cpp index dd14d9bf9d..93063deec8 100644 --- a/Core/Config.cpp +++ b/Core/Config.cpp @@ -879,9 +879,25 @@ bool DefaultAudioMixWithOthers() { #endif } +static int DefaultAndroidAudioApi() { + // AAudio isn't stable before Android 8.1 (API 27), Oboe's own cutoff. + if (System_GetPropertyInt(SYSPROP_SYSTEMVERSION) < 27) { + return (int)AndroidAudioApi::OPENSLES; + } + // An Android 11 TV played nothing over Bluetooth when AAudio used MMAP, which should never + // be used for a Bluetooth route. OpenSL ES worked, also when the headset was connected mid-game. + // Latency on a TV is mostly the TV's own anyway. + if (System_GetPropertyInt(SYSPROP_DEVICE_TYPE) == DEVICE_TYPE_TV) { + return (int)AndroidAudioApi::OPENSLES; + } + return (int)AndroidAudioApi::AAUDIO; +} + static constexpr ConfigSetting soundSettings[] = { ConfigSetting("Enable", SETTING(g_Config, bEnableSound), true, CfgFlag::PER_GAME), ConfigSetting("ExtraAudioBuffering", SETTING(g_Config, bExtraAudioBuffering), false, CfgFlag::DEFAULT), + ConfigSetting("AndroidAudioBackend", SETTING(g_Config, iAndroidAudioApi), &DefaultAndroidAudioApi, CfgFlag::DEFAULT), + ConfigSetting("AndroidAudioMMap", SETTING(g_Config, bAndroidAudioMMap), true, CfgFlag::DEFAULT), ConfigSetting("AudioBufferSize", SETTING(g_Config, iSDLAudioBufferSize), 256, CfgFlag::DEFAULT), ConfigSetting("FillAudioGaps", SETTING(g_Config, bFillAudioGaps), true, CfgFlag::DEFAULT), diff --git a/Core/Config.h b/Core/Config.h index 9c43205bae..2edda05e08 100644 --- a/Core/Config.h +++ b/Core/Config.h @@ -447,6 +447,9 @@ public: int iAltSpeedVolume; bool bExtraAudioBuffering; // For bluetooth + // Android only. + int iAndroidAudioApi; // AndroidAudioApi + bool bAndroidAudioMMap; std::string sAudioDevice; bool bAutoSwitchAudioDevice; bool bUseOldAtrac; diff --git a/Core/ConfigValues.h b/Core/ConfigValues.h index f06afd1f4c..15217eccd6 100644 --- a/Core/ConfigValues.h +++ b/Core/ConfigValues.h @@ -153,6 +153,11 @@ enum class AudioSyncMode { CLASSIC_PITCH = 1, }; +enum class AndroidAudioApi { + AAUDIO = 0, + OPENSLES = 1, +}; + // TODO: We can make this more fine-grained. enum class RestoreSettingsBits : int { SETTINGS = 1, diff --git a/UI/GameSettingsScreen.cpp b/UI/GameSettingsScreen.cpp index 4a01b83634..0066033e07 100644 --- a/UI/GameSettingsScreen.cpp +++ b/UI/GameSettingsScreen.cpp @@ -792,6 +792,23 @@ void GameSettingsScreen::CreateAudioSettings(UI::ViewGroup *audioSettings) { #if PPSSPP_PLATFORM(ANDROID) CheckBox *extraAudio = audioBackendSettings->Add(new CheckBox(&g_Config.bExtraAudioBuffering, a->T("AudioBufferingForBluetooth", "Bluetooth-friendly buffer (slower)"))); + auto restartAudio = [](UI::EventParams &) { + AndroidAudio_Restart(g_audioState); + }; + // AAudio isn't stable before Android 8.1, so there it's always OpenSL ES. + if (System_GetPropertyInt(SYSPROP_SYSTEMVERSION) >= 27) { + static const char *audioApis[] = { "AAudio", "OpenSL ES" }; + PopupMultiChoice *audioApi = audioBackendSettings->Add(new PopupMultiChoice(&g_Config.iAndroidAudioApi, a->T("Audio API"), audioApis, 0, ARRAY_SIZE(audioApis), I18NCat::AUDIO, screenManager())); + audioApi->OnChoice.Add(restartAudio); + audioBackendSettings->Add(new SettingHint(a->T("AudioApiHint", "Try the other one if you have no sound or other audio problems."), audioApi)); + + CheckBox *mmap = audioBackendSettings->Add(new CheckBox(&g_Config.bAndroidAudioMMap, a->T("Low-latency audio (MMAP)"))); + mmap->OnClick.Add(restartAudio); + mmap->SetEnabledFunc([]() { + return g_Config.iAndroidAudioApi == (int)AndroidAudioApi::AAUDIO; + }); + audioBackendSettings->Add(new SettingHint(a->T("AudioMMapHint", "Lowest audio delay. On some devices, it causes silence over Bluetooth or other connected audio devices. Turn it off if you get no sound."), mmap)); + } // Show Oboe debug info const std::string audioErrorStr = AndroidAudio_GetErrorString(g_audioState); if (!audioErrorStr.empty()) { diff --git a/UI/SystemInfoScreen.cpp b/UI/SystemInfoScreen.cpp index fdef8aa57e..ed6637ef5c 100644 --- a/UI/SystemInfoScreen.cpp +++ b/UI/SystemInfoScreen.cpp @@ -30,6 +30,12 @@ #include "UI/OnScreenDisplay.h" #include "android/jni/app-android.h" +#if PPSSPP_PLATFORM(ANDROID) +#include "android/jni/AndroidAudio.h" + +extern AndroidAudioState *g_audioState; +#endif + void SystemInfoScreen::update() { UITabbedBaseDialogScreen::update(); g_OSD.NudgeIngameNotifications(); @@ -433,6 +439,13 @@ void SystemInfoScreen::CreateAudioInfoTab(UI::LinearLayout *audio) { audioInformation->Add(new InfoItem(a->T("Audio Error"), error.empty() ? di->T("None") : error)); } else { audioInformation->Add(new InfoItem(si->T("Sample rate"), StringFromFormat(si->T_cstr("%d Hz"), System_GetPropertyInt(SYSPROP_AUDIO_SAMPLE_RATE)))); +#if PPSSPP_PLATFORM(ANDROID) + // What actually got opened: the API, whether it's MMAP, the device's rate and buffers. + const std::string streamInfo = AndroidAudio_GetInfoString(g_audioState); + audioInformation->Add(new InfoItem(si->T("Stream format"), streamInfo.empty() ? di->T("None") : streamInfo)); + const std::string error = AndroidAudio_GetErrorString(g_audioState); + audioInformation->Add(new InfoItem(a->T("Audio Error"), error.empty() ? di->T("None") : error)); +#endif } int framesPerBuffer = System_GetPropertyInt(SYSPROP_AUDIO_FRAMES_PER_BUFFER); if (framesPerBuffer > 0) { diff --git a/android/jni/AndroidAudio.cpp b/android/jni/AndroidAudio.cpp index 9930060c85..27f0327054 100644 --- a/android/jni/AndroidAudio.cpp +++ b/android/jni/AndroidAudio.cpp @@ -6,8 +6,12 @@ #include "android/jni/OboeContext.h" std::string g_error; +std::string g_info; std::mutex g_errorMutex; +// Pause and resume come from the Java UI thread, a restart from the settings screen. +static std::mutex g_ctxMutex; + // From the input stream's callback to the CPU thread. 0.7 s at 44.1 kHz. static SampleRing<32768> g_micRing; @@ -22,6 +26,7 @@ AudioContext::AudioContext(AndroidAudioCallback cb, int _FramesPerBuffer, int _S sampleRate = _SampleRate; g_error = ""; + g_info = ""; } void AudioContext::SetErrorString(const std::string &error) { @@ -29,6 +34,11 @@ void AudioContext::SetErrorString(const std::string &error) { g_error = error; } +void AudioContext::SetInfoString(const std::string &info) { + std::unique_lock lock(g_errorMutex); + g_info = info; +} + struct AndroidAudioState { AudioContext *ctx = nullptr; AndroidAudioCallback callback = nullptr; @@ -63,6 +73,7 @@ bool AndroidAudio_Recording_Start(AndroidAudioState *state) { ERROR_LOG(Log::Audio, "AndroidAudioState not initialized, cannot start recording!"); return false; } + std::lock_guard guard(g_ctxMutex); state->input_enable = 1; if (!state->ctx) { ERROR_LOG(Log::Audio, "Audio context not initialized, cannot start recording!"); @@ -78,6 +89,7 @@ bool AndroidAudio_Recording_Stop(AndroidAudioState *state) { ERROR_LOG(Log::Audio, "AndroidAudioState not initialized, cannot stop recording!"); return false; } + std::lock_guard guard(g_ctxMutex); // Clear these even while paused, or resume would start the mic again. state->input_enable = 0; state->input_sample_rate = 0; @@ -103,30 +115,50 @@ void AndroidAudio_Recording_Clear() { g_micRing.Clear(); } +static bool StartContext(AndroidAudioState *state) { + state->ctx = new OboeContext(state->callback, state->frames_per_buffer, state->sample_rate); + bool init_retval = state->ctx->Init(); + if (!init_retval) { + delete state->ctx; + state->ctx = nullptr; + } else if (state->input_enable) { + state->ctx->AudioRecord_Start(state->input_sample_rate); + } + return init_retval; +} + bool AndroidAudio_Resume(AndroidAudioState *state) { if (!state) { ERROR_LOG(Log::Audio, "Audio was shutdown, cannot resume!"); return false; } + std::lock_guard guard(g_ctxMutex); if (!state->ctx) { - state->ctx = new OboeContext(state->callback, state->frames_per_buffer, state->sample_rate); - bool init_retval = state->ctx->Init(); - if (!init_retval) { - delete state->ctx; - state->ctx = nullptr; - } else if (state->input_enable) { - state->ctx->AudioRecord_Start(state->input_sample_rate); - } - return init_retval; + return StartContext(state); } return false; } +bool AndroidAudio_Restart(AndroidAudioState *state) { + if (!state) { + return false; + } + std::lock_guard guard(g_ctxMutex); + // While paused, there's nothing to restart. Resume picks up the settings. + if (!state->ctx) { + return false; + } + delete state->ctx; + state->ctx = nullptr; + return StartContext(state); +} + bool AndroidAudio_Pause(AndroidAudioState *state) { if (!state) { ERROR_LOG(Log::Audio, "Audio was shutdown, cannot pause!"); return false; } + std::lock_guard guard(g_ctxMutex); if (state->ctx) { delete state->ctx; state->ctx = nullptr; @@ -156,3 +188,11 @@ const std::string AndroidAudio_GetErrorString(AndroidAudioState *state) { std::unique_lock lock(g_errorMutex); return g_error; } + +const std::string AndroidAudio_GetInfoString(AndroidAudioState *state) { + if (!state) { + return ""; + } + std::unique_lock lock(g_errorMutex); + return g_info; +} diff --git a/android/jni/AndroidAudio.h b/android/jni/AndroidAudio.h index bf53156c54..4f174dc14b 100644 --- a/android/jni/AndroidAudio.h +++ b/android/jni/AndroidAudio.h @@ -19,6 +19,7 @@ public: protected: static void SetErrorString(const std::string &error); + static void SetInfoString(const std::string &info); AndroidAudioCallback audioCallback; int framesPerBuffer; @@ -42,5 +43,9 @@ void AndroidAudio_Recording_Poll(); void AndroidAudio_Recording_Clear(); bool AndroidAudio_Pause(AndroidAudioState *state); bool AndroidAudio_Resume(AndroidAudioState *state); +// Closes the streams and opens them again, to apply changed settings. +bool AndroidAudio_Restart(AndroidAudioState *state); bool AndroidAudio_Shutdown(AndroidAudioState *state); const std::string AndroidAudio_GetErrorString(AndroidAudioState *state); +// Describes the output stream that's open. +const std::string AndroidAudio_GetInfoString(AndroidAudioState *state); diff --git a/android/jni/OboeContext.cpp b/android/jni/OboeContext.cpp index c897baea0e..f45866f404 100644 --- a/android/jni/OboeContext.cpp +++ b/android/jni/OboeContext.cpp @@ -6,6 +6,9 @@ #include #include "Common/Log.h" +#include "Common/System/System.h" +#include "Core/Config.h" +#include "Core/ConfigValues.h" #include "android/jni/OboeContext.h" // Owns the streams. Held by shared_ptr since after a disconnect, Oboe calls the error @@ -13,7 +16,14 @@ class OboeContext::Streams : public oboe::AudioStreamDataCallback, public oboe::AudioStreamErrorCallback, public std::enable_shared_from_this { public: Streams(AndroidAudioCallback cb, int framesPerBuffer, int sampleRate) - : audioCallback_(cb), framesPerBuffer_(framesPerBuffer), sampleRate_(sampleRate) {} + : audioCallback_(cb), framesPerBuffer_(framesPerBuffer), sampleRate_(sampleRate) { + // The setting isn't offered before Android 8.1, where AAudio isn't stable. + if (g_Config.iAndroidAudioApi == (int)AndroidAudioApi::AAUDIO && oboe::AudioStreamBuilder::isAAudioRecommended()) { + audioApi_ = oboe::AudioApi::AAudio; + } + // Global in Oboe, and only read when an AAudio stream is opened. + oboe::OboeExtensions::setMMapEnabled(g_Config.bAndroidAudioMMap); + } bool StartOutput() { std::lock_guard guard(mutex_); @@ -85,6 +95,7 @@ private: bool OpenOutput() { oboe::AudioStreamBuilder builder; builder.setDirection(oboe::Direction::Output) + ->setAudioApi(audioApi_) ->setPerformanceMode(oboe::PerformanceMode::LowLatency) ->setSharingMode(oboe::SharingMode::Shared) ->setUsage(oboe::Usage::Game) @@ -109,16 +120,22 @@ private: CloseStream(output_); return Fail("Failed to start output stream", result); } - INFO_LOG(Log::Audio, "Oboe: Output stream open: %s, %d Hz, %d frames per burst, buffer %d frames, %s, %s", + char info[256]; + snprintf(info, sizeof(info), "%s%s, %d Hz, burst %d, buffer %d, %s, device %d", oboe::convertToText(output_->getAudioApi()), + // Only ask AAudio. An OpenSL ES stream has no AAudio stream underneath. + output_->getAudioApi() == oboe::AudioApi::AAudio && oboe::OboeExtensions::isMMapUsed(output_.get()) ? " (MMAP)" : "", output_->getSampleRate(), output_->getFramesPerBurst(), output_->getBufferSizeInFrames(), - oboe::convertToText(output_->getPerformanceMode()), oboe::convertToText(output_->getSharingMode())); + oboe::convertToText(output_->getPerformanceMode()), output_->getDeviceId()); + INFO_LOG(Log::Audio, "Oboe: Output stream open: %s", info); + OboeContext::SetInfoString(info); return true; } bool OpenInput() { oboe::AudioStreamBuilder builder; builder.setDirection(oboe::Direction::Input) + ->setAudioApi(audioApi_) ->setSharingMode(oboe::SharingMode::Shared) ->setFormat(oboe::AudioFormat::I16) ->setFormatConversionAllowed(true) @@ -159,6 +176,7 @@ private: AndroidAudioCallback audioCallback_; int framesPerBuffer_; int sampleRate_; + oboe::AudioApi audioApi_ = oboe::AudioApi::OpenSLES; std::mutex mutex_; bool shutdown_ = false;