From 962f3407b479e99e4fab73ae2184e72ad0ce7006 Mon Sep 17 00:00:00 2001 From: Connor McLaughlin <stenzek@gmail.com> Date: Tue, 13 Oct 2020 23:11:28 +1000 Subject: [PATCH] Android: Add OpenSL ES audio backend --- android/app/src/cpp/CMakeLists.txt | 10 ++ .../app/src/cpp/android_host_interface.cpp | 14 ++ android/app/src/cpp/android_host_interface.h | 1 + android/app/src/cpp/opensles_audio_stream.cpp | 167 ++++++++++++++++++ android/app/src/cpp/opensles_audio_stream.h | 48 +++++ android/app/src/main/res/values/arrays.xml | 14 +- .../app/src/main/res/xml/root_preferences.xml | 10 +- src/core/settings.cpp | 22 ++- src/core/settings.h | 7 + src/core/types.h | 4 + 10 files changed, 290 insertions(+), 7 deletions(-) create mode 100644 android/app/src/cpp/opensles_audio_stream.cpp create mode 100644 android/app/src/cpp/opensles_audio_stream.h diff --git a/android/app/src/cpp/CMakeLists.txt b/android/app/src/cpp/CMakeLists.txt index 94f6e71ea..d8408d25c 100644 --- a/android/app/src/cpp/CMakeLists.txt +++ b/android/app/src/cpp/CMakeLists.txt @@ -7,3 +7,13 @@ set(SRCS add_library(duckstation-native SHARED ${SRCS}) target_link_libraries(duckstation-native PRIVATE android frontend-common core common glad imgui) + +find_package(OpenSLES) +if(OPENSLES_FOUND) + message("Enabling OpenSL ES audio stream") + target_sources(duckstation-native PRIVATE + opensles_audio_stream.cpp + opensles_audio_stream.h) + target_link_libraries(duckstation-native PRIVATE OpenSLES::OpenSLES) + target_compile_definitions(duckstation-native PRIVATE "-DUSE_OPENSLES=1") +endif() diff --git a/android/app/src/cpp/android_host_interface.cpp b/android/app/src/cpp/android_host_interface.cpp index a3ef5f223..c0e41baa1 100644 --- a/android/app/src/cpp/android_host_interface.cpp +++ b/android/app/src/cpp/android_host_interface.cpp @@ -20,6 +20,10 @@ #include <imgui.h> Log_SetChannel(AndroidHostInterface); +#ifdef USE_OPENSLES +#include "opensles_audio_stream.h" +#endif + static JavaVM* s_jvm; static jclass s_AndroidHostInterface_class; static jmethodID s_AndroidHostInterface_constructor; @@ -394,6 +398,16 @@ void AndroidHostInterface::ReleaseHostDisplay() m_display.reset(); } +std::unique_ptr<AudioStream> AndroidHostInterface::CreateAudioStream(AudioBackend backend) +{ +#ifdef USE_OPENSLES + if (backend == AudioBackend::OpenSLES) + return OpenSLESAudioStream::Create(); +#endif + + return CommonHostInterface::CreateAudioStream(backend); +} + void AndroidHostInterface::OnSystemDestroyed() { CommonHostInterface::OnSystemDestroyed(); diff --git a/android/app/src/cpp/android_host_interface.h b/android/app/src/cpp/android_host_interface.h index 4c8af0dad..a2ae84012 100644 --- a/android/app/src/cpp/android_host_interface.h +++ b/android/app/src/cpp/android_host_interface.h @@ -59,6 +59,7 @@ protected: bool AcquireHostDisplay() override; void ReleaseHostDisplay() override; + std::unique_ptr<AudioStream> CreateAudioStream(AudioBackend backend) override; void OnSystemDestroyed() override; void OnRunningGameChanged() override; diff --git a/android/app/src/cpp/opensles_audio_stream.cpp b/android/app/src/cpp/opensles_audio_stream.cpp new file mode 100644 index 000000000..92baf69b8 --- /dev/null +++ b/android/app/src/cpp/opensles_audio_stream.cpp @@ -0,0 +1,167 @@ +#include "opensles_audio_stream.h" +#include "common/assert.h" +#include "common/log.h" +#include <cmath> +Log_SetChannel(OpenSLESAudioStream); + +// Based off Dolphin's OpenSLESStream class. + +OpenSLESAudioStream::OpenSLESAudioStream() = default; + +OpenSLESAudioStream::~OpenSLESAudioStream() +{ + if (IsOpen()) + OpenSLESAudioStream::CloseDevice(); +} + +std::unique_ptr<AudioStream> OpenSLESAudioStream::Create() +{ + return std::make_unique<OpenSLESAudioStream>(); +} + +bool OpenSLESAudioStream::OpenDevice() +{ + DebugAssert(!IsOpen()); + + SLresult res = slCreateEngine(&m_engine, 0, nullptr, 0, nullptr, nullptr); + if (res != SL_RESULT_SUCCESS) + { + Log_ErrorPrintf("slCreateEngine failed: %d", res); + return false; + } + + res = (*m_engine)->Realize(m_engine, SL_BOOLEAN_FALSE); + if (res == SL_RESULT_SUCCESS) + res = (*m_engine)->GetInterface(m_engine, SL_IID_ENGINE, &m_engine_engine); + if (res == SL_RESULT_SUCCESS) + res = (*m_engine_engine)->CreateOutputMix(m_engine_engine, &m_output_mix, 0, 0, 0); + if (res == SL_RESULT_SUCCESS) + res = (*m_output_mix)->Realize(m_output_mix, SL_BOOLEAN_FALSE); + if (res != SL_RESULT_SUCCESS) + { + Log_ErrorPrintf("Failed to create engine/output mix"); + CloseDevice(); + return false; + } + + SLDataLocator_AndroidSimpleBufferQueue dloc_bq{SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE, NUM_BUFFERS}; + SLDataFormat_PCM format = {SL_DATAFORMAT_PCM, + m_channels, + m_output_sample_rate * 1000u, + SL_PCMSAMPLEFORMAT_FIXED_16, + SL_PCMSAMPLEFORMAT_FIXED_16, + SL_SPEAKER_FRONT_LEFT | SL_SPEAKER_FRONT_RIGHT, + SL_BYTEORDER_LITTLEENDIAN}; + SLDataSource dsrc{&dloc_bq, &format}; + SLDataLocator_OutputMix dloc_outputmix{SL_DATALOCATOR_OUTPUTMIX, m_output_mix}; + SLDataSink dsink{&dloc_outputmix, nullptr}; + + const std::array<SLInterfaceID, 2> ap_interfaces = {{SL_IID_BUFFERQUEUE, SL_IID_VOLUME}}; + const std::array<SLboolean, 2> ap_interfaces_req = {{true, true}}; + res = (*m_engine_engine) + ->CreateAudioPlayer(m_engine_engine, &m_player, &dsrc, &dsink, static_cast<u32>(ap_interfaces.size()), + ap_interfaces.data(), ap_interfaces_req.data()); + if (res != SL_RESULT_SUCCESS) + { + Log_ErrorPrintf("Failed to create audio player: %d", res); + CloseDevice(); + return false; + } + + res = (*m_player)->Realize(m_player, SL_BOOLEAN_FALSE); + if (res == SL_RESULT_SUCCESS) + res = (*m_player)->GetInterface(m_player, SL_IID_PLAY, &m_play_interface); + if (res == SL_RESULT_SUCCESS) + res = (*m_player)->GetInterface(m_player, SL_IID_BUFFERQUEUE, &m_buffer_queue_interface); + if (res == SL_RESULT_SUCCESS) + res = (*m_player)->GetInterface(m_player, SL_IID_VOLUME, &m_volume_interface); + if (res != SL_RESULT_SUCCESS) + { + Log_ErrorPrintf("Failed to get player interfaces: %d", res); + CloseDevice(); + return false; + } + + res = (*m_buffer_queue_interface)->RegisterCallback(m_buffer_queue_interface, BufferCallback, this); + if (res != SL_RESULT_SUCCESS) + { + Log_ErrorPrintf("Failed to register callback: %d", res); + CloseDevice(); + return false; + } + + for (u32 i = 0; i < NUM_BUFFERS; i++) + m_buffers[i] = std::make_unique<SampleType[]>(m_buffer_size * m_channels); + + return true; +} + +void OpenSLESAudioStream::PauseDevice(bool paused) +{ + if (m_paused == paused) + return; + + (*m_play_interface)->SetPlayState(m_play_interface, paused ? SL_PLAYSTATE_PAUSED : SL_PLAYSTATE_PLAYING); + + if (!paused && !m_buffer_enqueued) + { + m_buffer_enqueued = true; + EnqueueBuffer(); + } + + m_paused = paused; +} + +void OpenSLESAudioStream::CloseDevice() +{ + m_buffers = {}; + m_current_buffer = 0; + m_paused = true; + m_buffer_enqueued = false; + + if (m_player) + { + (*m_player)->Destroy(m_player); + m_volume_interface = {}; + m_buffer_queue_interface = {}; + m_play_interface = {}; + m_player = {}; + } + if (m_output_mix) + { + (*m_output_mix)->Destroy(m_output_mix); + m_output_mix = {}; + } + (*m_engine)->Destroy(m_engine); + m_engine_engine = {}; + m_engine = {}; +} + +void OpenSLESAudioStream::SetOutputVolume(u32 volume) +{ + const SLmillibel attenuation = (volume == 0) ? + SL_MILLIBEL_MIN : + static_cast<SLmillibel>(2000.0f * std::log10(static_cast<float>(volume) / 100.0f)); + (*m_volume_interface)->SetVolumeLevel(m_volume_interface, attenuation); +} + +void OpenSLESAudioStream::EnqueueBuffer() +{ + SampleType* samples = m_buffers[m_current_buffer].get(); + ReadFrames(samples, m_buffer_size, false); + + SLresult res = (*m_buffer_queue_interface) + ->Enqueue(m_buffer_queue_interface, samples, m_buffer_size * m_channels * sizeof(SampleType)); + if (res != SL_RESULT_SUCCESS) + Log_ErrorPrintf("Enqueue buffer failed: %d", res); + + m_current_buffer = (m_current_buffer + 1) % NUM_BUFFERS; +} + +void OpenSLESAudioStream::BufferCallback(SLAndroidSimpleBufferQueueItf buffer_queue, void* context) +{ + OpenSLESAudioStream* const this_ptr = static_cast<OpenSLESAudioStream*>(context); + this_ptr->EnqueueBuffer(); +} + +void OpenSLESAudioStream::FramesAvailable() {} diff --git a/android/app/src/cpp/opensles_audio_stream.h b/android/app/src/cpp/opensles_audio_stream.h new file mode 100644 index 000000000..83c54aa6e --- /dev/null +++ b/android/app/src/cpp/opensles_audio_stream.h @@ -0,0 +1,48 @@ +#pragma once +#include "common/audio_stream.h" +#include <SLES/OpenSLES.h> +#include <SLES/OpenSLES_Android.h> +#include <array> +#include <memory> + +class OpenSLESAudioStream final : public AudioStream +{ +public: + OpenSLESAudioStream(); + ~OpenSLESAudioStream(); + + static std::unique_ptr<AudioStream> Create(); + + void SetOutputVolume(u32 volume) override; + +protected: + enum : u32 + { + NUM_BUFFERS = 2 + }; + + ALWAYS_INLINE bool IsOpen() const { return (m_engine != nullptr); } + + bool OpenDevice() override; + void PauseDevice(bool paused) override; + void CloseDevice() override; + void FramesAvailable() override; + + void EnqueueBuffer(); + + static void BufferCallback(SLAndroidSimpleBufferQueueItf buffer_queue, void* context); + + SLObjectItf m_engine{}; + SLEngineItf m_engine_engine{}; + SLObjectItf m_output_mix{}; + + SLObjectItf m_player{}; + SLPlayItf m_play_interface{}; + SLAndroidSimpleBufferQueueItf m_buffer_queue_interface{}; + SLVolumeItf m_volume_interface{}; + + std::array<std::unique_ptr<SampleType[]>, NUM_BUFFERS> m_buffers; + u32 m_current_buffer = 0; + bool m_paused = true; + bool m_buffer_enqueued = false; +}; diff --git a/android/app/src/main/res/values/arrays.xml b/android/app/src/main/res/values/arrays.xml index e50d31fc8..3fa092f53 100644 --- a/android/app/src/main/res/values/arrays.xml +++ b/android/app/src/main/res/values/arrays.xml @@ -178,10 +178,20 @@ <item>analog_stick</item> <item>analog_sticks</item> </string-array> + <string-array name="settings_audio_backend_entries"> + <item>Null (No Output)</item> + <item>Cubeb</item> + <item>OpenSL ES (Recommended)</item> + </string-array> + <string-array name="settings_audio_backend_values"> + <item>Null</item> + <item>Cubeb</item> + <item>OpenSLES</item> + </string-array> <string-array name="settings_audio_buffer_size_entries"> <item>1024 Frames (23.22ms)</item> - <item>2048 Frames (46.44ms)</item> - <item>3072 Frames (69.66ms, Recommended)</item> + <item>2048 Frames (46.44ms, Recommended)</item> + <item>3072 Frames (69.66ms)</item> <item>4096 Frames (92.88ms)</item> </string-array> <string-array name="settings_audio_buffer_size_values"> diff --git a/android/app/src/main/res/xml/root_preferences.xml b/android/app/src/main/res/xml/root_preferences.xml index d6b2ffabb..a28be012d 100644 --- a/android/app/src/main/res/xml/root_preferences.xml +++ b/android/app/src/main/res/xml/root_preferences.xml @@ -300,12 +300,20 @@ app:defaultValue="false" app:summary="Forcibly mutes both CD-DA and XA audio from the CD-ROM. Can be used to disable background music in some games." app:iconSpaceReserved="false" /> + <ListPreference + app:key="Audio/Backend" + app:title="Audio Backend" + app:entries="@array/settings_audio_backend_entries" + app:entryValues="@array/settings_audio_backend_values" + app:defaultValue="OpenSLES" + app:useSimpleSummaryProvider="true" + app:iconSpaceReserved="false"/> <ListPreference app:key="Audio/BufferSize" app:title="Audio Buffer Size" app:entries="@array/settings_audio_buffer_size_entries" app:entryValues="@array/settings_audio_buffer_size_values" - app:defaultValue="3072" + app:defaultValue="2048" app:summary="Determines the latency between audio being generated and output to speakers. Smaller values reduce latency, but variations in emulation speed will cause hitches." app:useSimpleSummaryProvider="true" app:iconSpaceReserved="false" /> diff --git a/src/core/settings.cpp b/src/core/settings.cpp index 2e2ca2bfa..af5a1c949 100644 --- a/src/core/settings.cpp +++ b/src/core/settings.cpp @@ -592,10 +592,24 @@ float Settings::GetDisplayAspectRatioValue(DisplayAspectRatio ar) return s_display_aspect_ratio_values[static_cast<int>(ar)]; } -static std::array<const char*, 3> s_audio_backend_names = {{"Null", "Cubeb", "SDL"}}; -static std::array<const char*, 3> s_audio_backend_display_names = {{TRANSLATABLE("AudioBackend", "Null (No Output)"), - TRANSLATABLE("AudioBackend", "Cubeb"), - TRANSLATABLE("AudioBackend", "SDL")}}; +static std::array<const char*, 3> s_audio_backend_names = {{ + "Null", + "Cubeb", +#ifndef ANDROID + "SDL", +#else + "OpenSLES", +#endif +}}; +static std::array<const char*, 3> s_audio_backend_display_names = {{ + TRANSLATABLE("AudioBackend", "Null (No Output)"), + TRANSLATABLE("AudioBackend", "Cubeb"), +#ifndef ANDROID + TRANSLATABLE("AudioBackend", "SDL"), +#else + TRANSLATABLE("AudioBackend", "OpenSL ES"), +#endif +}}; std::optional<AudioBackend> Settings::ParseAudioBackend(const char* str) { diff --git a/src/core/settings.h b/src/core/settings.h index d5d589c44..d1f3786c2 100644 --- a/src/core/settings.h +++ b/src/core/settings.h @@ -251,8 +251,15 @@ struct Settings #endif static constexpr GPUTextureFilter DEFAULT_GPU_TEXTURE_FILTER = GPUTextureFilter::Nearest; static constexpr ConsoleRegion DEFAULT_CONSOLE_REGION = ConsoleRegion::Auto; + static constexpr CPUExecutionMode DEFAULT_CPU_EXECUTION_MODE = CPUExecutionMode::Recompiler; + +#ifndef ANDROID static constexpr AudioBackend DEFAULT_AUDIO_BACKEND = AudioBackend::Cubeb; +#else + static constexpr AudioBackend DEFAULT_AUDIO_BACKEND = AudioBackend::OpenSLES; +#endif + static constexpr DisplayCropMode DEFAULT_DISPLAY_CROP_MODE = DisplayCropMode::Overscan; static constexpr DisplayAspectRatio DEFAULT_DISPLAY_ASPECT_RATIO = DisplayAspectRatio::R4_3; static constexpr ControllerType DEFAULT_CONTROLLER_1_TYPE = ControllerType::DigitalController; diff --git a/src/core/types.h b/src/core/types.h index 0fb6bdd33..a8834eef6 100644 --- a/src/core/types.h +++ b/src/core/types.h @@ -97,7 +97,11 @@ enum class AudioBackend : u8 { Null, Cubeb, +#ifndef ANDROID SDL, +#else + OpenSLES, +#endif Count };