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;