From 18ada658c9fc0b0a8e29525d2fbf5d5ffe329661 Mon Sep 17 00:00:00 2001 From: JosJuice Date: Thu, 6 Jun 2024 13:32:26 +0200 Subject: [PATCH 1/2] Android: Get rid of OpenSLESStream's global state Not sure if we're ever going to want to have more than one of these at the same time, but these global variables are a code smell nonetheless. I'm also deleting the existing member variables because they were unused. --- Source/Core/AudioCommon/OpenSLESStream.cpp | 105 +++++++++------------ Source/Core/AudioCommon/OpenSLESStream.h | 28 +++++- 2 files changed, 70 insertions(+), 63 deletions(-) diff --git a/Source/Core/AudioCommon/OpenSLESStream.cpp b/Source/Core/AudioCommon/OpenSLESStream.cpp index a3f6308187..84831ceec6 100644 --- a/Source/Core/AudioCommon/OpenSLESStream.cpp +++ b/Source/Core/AudioCommon/OpenSLESStream.cpp @@ -14,34 +14,19 @@ #include "Common/Logging/Log.h" #include "Core/ConfigManager.h" -// engine interfaces -static SLObjectItf engineObject; -static SLEngineItf engineEngine; -static SLObjectItf outputMixObject; - -// buffer queue player interfaces -static SLObjectItf bqPlayerObject = nullptr; -static SLPlayItf bqPlayerPlay; -static SLAndroidSimpleBufferQueueItf bqPlayerBufferQueue; -static SLVolumeItf bqPlayerVolume; -static Mixer* g_mixer; -#define BUFFER_SIZE 512 -#define BUFFER_SIZE_IN_SAMPLES (BUFFER_SIZE / 2) - -// Double buffering. -static short buffer[2][BUFFER_SIZE]; -static int curBuffer = 0; - -static void bqPlayerCallback(SLAndroidSimpleBufferQueueItf bq, void* context) +void OpenSLESStream::BQPlayerCallback(SLAndroidSimpleBufferQueueItf bq, void* context) { - ASSERT(bq == bqPlayerBufferQueue); - ASSERT(nullptr == context); + reinterpret_cast(context)->PushSamples(bq); +} + +void OpenSLESStream::PushSamples(SLAndroidSimpleBufferQueueItf bq) +{ + ASSERT(bq == m_bq_player_buffer_queue); // Render to the fresh buffer - g_mixer->Mix(reinterpret_cast(buffer[curBuffer]), BUFFER_SIZE_IN_SAMPLES); - SLresult result = - (*bqPlayerBufferQueue)->Enqueue(bqPlayerBufferQueue, buffer[curBuffer], sizeof(buffer[0])); - curBuffer ^= 1; // Switch buffer + m_mixer->Mix(reinterpret_cast(m_buffer[m_current_buffer]), BUFFER_SIZE_IN_SAMPLES); + SLresult result = (*bq)->Enqueue(bq, m_buffer[m_current_buffer], sizeof(m_buffer[0])); + m_current_buffer ^= 1; // Switch buffer // Comment from sample code: // the most likely other result is SL_RESULT_BUFFER_INSUFFICIENT, @@ -53,15 +38,15 @@ bool OpenSLESStream::Init() { SLresult result; // create engine - result = slCreateEngine(&engineObject, 0, nullptr, 0, nullptr, nullptr); + result = slCreateEngine(&m_engine_object, 0, nullptr, 0, nullptr, nullptr); ASSERT(SL_RESULT_SUCCESS == result); - result = (*engineObject)->Realize(engineObject, SL_BOOLEAN_FALSE); + result = (*m_engine_object)->Realize(m_engine_object, SL_BOOLEAN_FALSE); ASSERT(SL_RESULT_SUCCESS == result); - result = (*engineObject)->GetInterface(engineObject, SL_IID_ENGINE, &engineEngine); + result = (*m_engine_object)->GetInterface(m_engine_object, SL_IID_ENGINE, &m_engine_engine); ASSERT(SL_RESULT_SUCCESS == result); - result = (*engineEngine)->CreateOutputMix(engineEngine, &outputMixObject, 0, 0, 0); + result = (*m_engine_engine)->CreateOutputMix(m_engine_engine, &m_output_mix_object, 0, 0, 0); ASSERT(SL_RESULT_SUCCESS == result); - result = (*outputMixObject)->Realize(outputMixObject, SL_BOOLEAN_FALSE); + result = (*m_output_mix_object)->Realize(m_output_mix_object, SL_BOOLEAN_FALSE); ASSERT(SL_RESULT_SUCCESS == result); SLDataLocator_AndroidSimpleBufferQueue loc_bufq = {SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE, 2}; @@ -76,36 +61,38 @@ bool OpenSLESStream::Init() SLDataSource audioSrc = {&loc_bufq, &format_pcm}; // configure audio sink - SLDataLocator_OutputMix loc_outmix = {SL_DATALOCATOR_OUTPUTMIX, outputMixObject}; + SLDataLocator_OutputMix loc_outmix = {SL_DATALOCATOR_OUTPUTMIX, m_output_mix_object}; SLDataSink audioSnk = {&loc_outmix, nullptr}; // create audio player const SLInterfaceID ids[2] = {SL_IID_BUFFERQUEUE, SL_IID_VOLUME}; const SLboolean req[2] = {SL_BOOLEAN_TRUE, SL_BOOLEAN_TRUE}; - result = - (*engineEngine) - ->CreateAudioPlayer(engineEngine, &bqPlayerObject, &audioSrc, &audioSnk, 2, ids, req); + result = (*m_engine_engine) + ->CreateAudioPlayer(m_engine_engine, &m_bq_player_object, &audioSrc, &audioSnk, 2, + ids, req); ASSERT(SL_RESULT_SUCCESS == result); - result = (*bqPlayerObject)->Realize(bqPlayerObject, SL_BOOLEAN_FALSE); + result = (*m_bq_player_object)->Realize(m_bq_player_object, SL_BOOLEAN_FALSE); ASSERT(SL_RESULT_SUCCESS == result); - result = (*bqPlayerObject)->GetInterface(bqPlayerObject, SL_IID_PLAY, &bqPlayerPlay); + result = (*m_bq_player_object)->GetInterface(m_bq_player_object, SL_IID_PLAY, &m_bq_player_play); + ASSERT(SL_RESULT_SUCCESS == result); + result = (*m_bq_player_object) + ->GetInterface(m_bq_player_object, SL_IID_BUFFERQUEUE, &m_bq_player_buffer_queue); ASSERT(SL_RESULT_SUCCESS == result); result = - (*bqPlayerObject)->GetInterface(bqPlayerObject, SL_IID_BUFFERQUEUE, &bqPlayerBufferQueue); + (*m_bq_player_object)->GetInterface(m_bq_player_object, SL_IID_VOLUME, &m_bq_player_volume); ASSERT(SL_RESULT_SUCCESS == result); - result = (*bqPlayerObject)->GetInterface(bqPlayerObject, SL_IID_VOLUME, &bqPlayerVolume); + result = (*m_bq_player_buffer_queue) + ->RegisterCallback(m_bq_player_buffer_queue, BQPlayerCallback, this); ASSERT(SL_RESULT_SUCCESS == result); - result = (*bqPlayerBufferQueue)->RegisterCallback(bqPlayerBufferQueue, bqPlayerCallback, nullptr); - ASSERT(SL_RESULT_SUCCESS == result); - result = (*bqPlayerPlay)->SetPlayState(bqPlayerPlay, SL_PLAYSTATE_PLAYING); + result = (*m_bq_player_play)->SetPlayState(m_bq_player_play, SL_PLAYSTATE_PLAYING); ASSERT(SL_RESULT_SUCCESS == result); // Render and enqueue a first buffer. - curBuffer ^= 1; - g_mixer = m_mixer.get(); + m_current_buffer ^= 1; - result = (*bqPlayerBufferQueue)->Enqueue(bqPlayerBufferQueue, buffer[0], sizeof(buffer[0])); + result = (*m_bq_player_buffer_queue) + ->Enqueue(m_bq_player_buffer_queue, m_buffer[0], sizeof(m_buffer[0])); if (SL_RESULT_SUCCESS != result) return false; @@ -114,39 +101,39 @@ bool OpenSLESStream::Init() OpenSLESStream::~OpenSLESStream() { - if (bqPlayerObject != nullptr) + if (m_bq_player_object != nullptr) { - (*bqPlayerObject)->Destroy(bqPlayerObject); - bqPlayerObject = nullptr; - bqPlayerPlay = nullptr; - bqPlayerBufferQueue = nullptr; - bqPlayerVolume = nullptr; + (*m_bq_player_object)->Destroy(m_bq_player_object); + m_bq_player_object = nullptr; + m_bq_player_play = nullptr; + m_bq_player_buffer_queue = nullptr; + m_bq_player_volume = nullptr; } - if (outputMixObject != nullptr) + if (m_output_mix_object != nullptr) { - (*outputMixObject)->Destroy(outputMixObject); - outputMixObject = nullptr; + (*m_output_mix_object)->Destroy(m_output_mix_object); + m_output_mix_object = nullptr; } - if (engineObject != nullptr) + if (m_engine_object != nullptr) { - (*engineObject)->Destroy(engineObject); - engineObject = nullptr; - engineEngine = nullptr; + (*m_engine_object)->Destroy(m_engine_object); + m_engine_object = nullptr; + m_engine_engine = nullptr; } } bool OpenSLESStream::SetRunning(bool running) { SLuint32 new_state = running ? SL_PLAYSTATE_PLAYING : SL_PLAYSTATE_PAUSED; - return (*bqPlayerPlay)->SetPlayState(bqPlayerPlay, new_state) == SL_RESULT_SUCCESS; + return (*m_bq_player_play)->SetPlayState(m_bq_player_play, new_state) == SL_RESULT_SUCCESS; } void OpenSLESStream::SetVolume(int volume) { const SLmillibel attenuation = volume <= 0 ? SL_MILLIBEL_MIN : static_cast(2000 * std::log10(volume / 100.0f)); - (*bqPlayerVolume)->SetVolumeLevel(bqPlayerVolume, attenuation); + (*m_bq_player_volume)->SetVolumeLevel(m_bq_player_volume, attenuation); } #endif // HAVE_OPENSL_ES diff --git a/Source/Core/AudioCommon/OpenSLESStream.h b/Source/Core/AudioCommon/OpenSLESStream.h index f22aaf9a0f..009d8d891a 100644 --- a/Source/Core/AudioCommon/OpenSLESStream.h +++ b/Source/Core/AudioCommon/OpenSLESStream.h @@ -3,10 +3,12 @@ #pragma once -#include +#ifdef HAVE_OPENSL_ES +#include +#include +#endif // HAVE_OPENSL_ES #include "AudioCommon/SoundStream.h" -#include "Common/Event.h" class OpenSLESStream final : public SoundStream { @@ -19,7 +21,25 @@ public: static bool IsValid() { return true; } private: - std::thread thread; - Common::Event soundSyncEvent; + static void BQPlayerCallback(SLAndroidSimpleBufferQueueItf bq, void* context); + void PushSamples(SLAndroidSimpleBufferQueueItf bq); + + // engine interfaces + SLObjectItf m_engine_object; + SLEngineItf m_engine_engine; + SLObjectItf m_output_mix_object; + + // buffer queue player interfaces + SLObjectItf m_bq_player_object = nullptr; + SLPlayItf m_bq_player_play; + SLAndroidSimpleBufferQueueItf m_bq_player_buffer_queue; + SLVolumeItf m_bq_player_volume; + + static constexpr int BUFFER_SIZE = 512; + static constexpr int BUFFER_SIZE_IN_SAMPLES = BUFFER_SIZE / 2; + + // Double buffering. + short m_buffer[2][BUFFER_SIZE]; + int m_current_buffer = 0; #endif // HAVE_OPENSL_ES }; From 489d0366d93b319de3b8fa24fc3c83a9d7fe7030 Mon Sep 17 00:00:00 2001 From: JosJuice Date: Thu, 6 Jun 2024 15:38:41 +0200 Subject: [PATCH 2/2] Android: Ask system for optimal audio buffer size and sample rate This can reduce audio latency according to https://developer.android.com/ndk/guides/audio/opensl/opensl-prog-notes#perform. Previously we were using the hardcoded values of 48000 Hz and 256 frames per buffer. The sample rate we use with this change is 48000 Hz on all devices I'm aware of, but the buffer size does vary across devices. Terminology note: The old code used the term "sample" to refer to what Android refers to as a "frame". "Frame" is a clearer term to use for this, so I've changed OpenSLESStream's terminology. One frame consists of one sample per channel. --- .../dolphinemu/dolphinemu/utils/AudioUtils.kt | 28 ++++++++++++++ Source/Android/jni/AndroidCommon/IDCache.cpp | 27 ++++++++++++++ Source/Android/jni/AndroidCommon/IDCache.h | 4 ++ Source/Core/AudioCommon/OpenSLESStream.cpp | 37 ++++++++++++++----- Source/Core/AudioCommon/OpenSLESStream.h | 9 +++-- 5 files changed, 92 insertions(+), 13 deletions(-) create mode 100644 Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/AudioUtils.kt diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/AudioUtils.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/AudioUtils.kt new file mode 100644 index 0000000000..68e3573fa5 --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/AudioUtils.kt @@ -0,0 +1,28 @@ +package org.dolphinemu.dolphinemu.utils + +import android.content.Context +import android.media.AudioManager +import androidx.annotation.Keep +import org.dolphinemu.dolphinemu.DolphinApplication + +object AudioUtils { + @JvmStatic @Keep + fun getSampleRate(): Int = + getAudioServiceProperty(AudioManager.PROPERTY_OUTPUT_SAMPLE_RATE, 48000) + + @JvmStatic @Keep + fun getFramesPerBuffer(): Int = + getAudioServiceProperty(AudioManager.PROPERTY_OUTPUT_FRAMES_PER_BUFFER, 256) + + private fun getAudioServiceProperty(property: String, fallback: Int): Int { + return try { + val context = DolphinApplication.getAppContext() + val am = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager + Integer.parseUnsignedInt(am.getProperty(property)) + } catch (e: NullPointerException) { + fallback + } catch (e: NumberFormatException) { + fallback + } + } +} diff --git a/Source/Android/jni/AndroidCommon/IDCache.cpp b/Source/Android/jni/AndroidCommon/IDCache.cpp index ed382745c0..9bf016f3a1 100644 --- a/Source/Android/jni/AndroidCommon/IDCache.cpp +++ b/Source/Android/jni/AndroidCommon/IDCache.cpp @@ -118,6 +118,10 @@ static jfieldID s_input_detector_pointer; static jmethodID s_runnable_run; +static jclass s_audio_utils_class; +static jmethodID s_audio_utils_get_sample_rate; +static jmethodID s_audio_utils_get_frames_per_buffer; + namespace IDCache { JNIEnv* GetEnvForThread() @@ -543,6 +547,21 @@ jmethodID GetRunnableRun() return s_runnable_run; } +jclass GetAudioUtilsClass() +{ + return s_audio_utils_class; +} + +jmethodID GetAudioUtilsGetSampleRate() +{ + return s_audio_utils_get_sample_rate; +} + +jmethodID GetAudioUtilsGetFramesPerBuffer() +{ + return s_audio_utils_get_frames_per_buffer; +} + } // namespace IDCache extern "C" { @@ -769,6 +788,13 @@ JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) s_runnable_run = env->GetMethodID(runnable_class, "run", "()V"); env->DeleteLocalRef(runnable_class); + const jclass audio_utils_class = env->FindClass("org/dolphinemu/dolphinemu/utils/AudioUtils"); + s_audio_utils_class = reinterpret_cast(env->NewGlobalRef(audio_utils_class)); + s_audio_utils_get_sample_rate = env->GetStaticMethodID(audio_utils_class, "getSampleRate", "()I"); + s_audio_utils_get_frames_per_buffer = + env->GetStaticMethodID(audio_utils_class, "getFramesPerBuffer", "()I"); + env->DeleteLocalRef(audio_utils_class); + return JNI_VERSION; } @@ -804,5 +830,6 @@ JNIEXPORT void JNI_OnUnload(JavaVM* vm, void* reserved) env->DeleteGlobalRef(s_core_device_class); env->DeleteGlobalRef(s_core_device_control_class); env->DeleteGlobalRef(s_input_detector_class); + env->DeleteGlobalRef(s_audio_utils_class); } } diff --git a/Source/Android/jni/AndroidCommon/IDCache.h b/Source/Android/jni/AndroidCommon/IDCache.h index 0b01d14b42..572b305777 100644 --- a/Source/Android/jni/AndroidCommon/IDCache.h +++ b/Source/Android/jni/AndroidCommon/IDCache.h @@ -117,4 +117,8 @@ jfieldID GetInputDetectorPointer(); jmethodID GetRunnableRun(); +jclass GetAudioUtilsClass(); +jmethodID GetAudioUtilsGetSampleRate(); +jmethodID GetAudioUtilsGetFramesPerBuffer(); + } // namespace IDCache diff --git a/Source/Core/AudioCommon/OpenSLESStream.cpp b/Source/Core/AudioCommon/OpenSLESStream.cpp index 84831ceec6..3fc71e75df 100644 --- a/Source/Core/AudioCommon/OpenSLESStream.cpp +++ b/Source/Core/AudioCommon/OpenSLESStream.cpp @@ -8,11 +8,13 @@ #include #include +#include #include "Common/Assert.h" #include "Common/CommonTypes.h" #include "Common/Logging/Log.h" #include "Core/ConfigManager.h" +#include "jni/AndroidCommon/IDCache.h" void OpenSLESStream::BQPlayerCallback(SLAndroidSimpleBufferQueueItf bq, void* context) { @@ -24,8 +26,8 @@ void OpenSLESStream::PushSamples(SLAndroidSimpleBufferQueueItf bq) ASSERT(bq == m_bq_player_buffer_queue); // Render to the fresh buffer - m_mixer->Mix(reinterpret_cast(m_buffer[m_current_buffer]), BUFFER_SIZE_IN_SAMPLES); - SLresult result = (*bq)->Enqueue(bq, m_buffer[m_current_buffer], sizeof(m_buffer[0])); + m_mixer->Mix(m_buffer[m_current_buffer].data(), m_frames_per_buffer); + SLresult result = (*bq)->Enqueue(bq, m_buffer[m_current_buffer].data(), m_bytes_per_buffer); m_current_buffer ^= 1; // Switch buffer // Comment from sample code: @@ -36,6 +38,23 @@ void OpenSLESStream::PushSamples(SLAndroidSimpleBufferQueueItf bq) bool OpenSLESStream::Init() { + JNIEnv* env = IDCache::GetEnvForThread(); + jclass audio_utils = IDCache::GetAudioUtilsClass(); + const SLuint32 sample_rate = + env->CallStaticIntMethod(audio_utils, IDCache::GetAudioUtilsGetSampleRate()); + m_frames_per_buffer = + env->CallStaticIntMethod(audio_utils, IDCache::GetAudioUtilsGetFramesPerBuffer()); + + INFO_LOG_FMT(AUDIO, "OpenSLES configuration: {} Hz, {} frames per buffer", sample_rate, + m_frames_per_buffer); + + constexpr SLuint32 channels = 2; + const SLuint32 samples_per_buffer = m_frames_per_buffer * channels; + m_bytes_per_buffer = m_frames_per_buffer * channels * sizeof(m_buffer[0][0]); + + for (std::vector& buffer : m_buffer) + buffer.resize(samples_per_buffer); + SLresult result; // create engine result = slCreateEngine(&m_engine_object, 0, nullptr, 0, nullptr, nullptr); @@ -50,13 +69,11 @@ bool OpenSLESStream::Init() ASSERT(SL_RESULT_SUCCESS == result); SLDataLocator_AndroidSimpleBufferQueue loc_bufq = {SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE, 2}; - SLDataFormat_PCM format_pcm = {SL_DATAFORMAT_PCM, - 2, - m_mixer->GetSampleRate() * 1000, - SL_PCMSAMPLEFORMAT_FIXED_16, - SL_PCMSAMPLEFORMAT_FIXED_16, - SL_SPEAKER_FRONT_LEFT | SL_SPEAKER_FRONT_RIGHT, - SL_BYTEORDER_LITTLEENDIAN}; + SLDataFormat_PCM format_pcm = { + SL_DATAFORMAT_PCM, channels, + sample_rate * 1000, SL_PCMSAMPLEFORMAT_FIXED_16, + SL_PCMSAMPLEFORMAT_FIXED_16, SL_SPEAKER_FRONT_LEFT | SL_SPEAKER_FRONT_RIGHT, + SL_BYTEORDER_LITTLEENDIAN}; SLDataSource audioSrc = {&loc_bufq, &format_pcm}; @@ -92,7 +109,7 @@ bool OpenSLESStream::Init() m_current_buffer ^= 1; result = (*m_bq_player_buffer_queue) - ->Enqueue(m_bq_player_buffer_queue, m_buffer[0], sizeof(m_buffer[0])); + ->Enqueue(m_bq_player_buffer_queue, m_buffer[0].data(), m_bytes_per_buffer); if (SL_RESULT_SUCCESS != result) return false; diff --git a/Source/Core/AudioCommon/OpenSLESStream.h b/Source/Core/AudioCommon/OpenSLESStream.h index 009d8d891a..9772f0b9c7 100644 --- a/Source/Core/AudioCommon/OpenSLESStream.h +++ b/Source/Core/AudioCommon/OpenSLESStream.h @@ -4,6 +4,9 @@ #pragma once #ifdef HAVE_OPENSL_ES +#include +#include + #include #include #endif // HAVE_OPENSL_ES @@ -35,11 +38,11 @@ private: SLAndroidSimpleBufferQueueItf m_bq_player_buffer_queue; SLVolumeItf m_bq_player_volume; - static constexpr int BUFFER_SIZE = 512; - static constexpr int BUFFER_SIZE_IN_SAMPLES = BUFFER_SIZE / 2; + SLuint32 m_frames_per_buffer; + SLuint32 m_bytes_per_buffer; // Double buffering. - short m_buffer[2][BUFFER_SIZE]; + std::array, 2> m_buffer; int m_current_buffer = 0; #endif // HAVE_OPENSL_ES };