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 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 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 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 +Log_SetChannel(OpenSLESAudioStream); + +// Based off Dolphin's OpenSLESStream class. + +OpenSLESAudioStream::OpenSLESAudioStream() = default; + +OpenSLESAudioStream::~OpenSLESAudioStream() +{ + if (IsOpen()) + OpenSLESAudioStream::CloseDevice(); +} + +std::unique_ptr OpenSLESAudioStream::Create() +{ + return std::make_unique(); +} + +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 ap_interfaces = {{SL_IID_BUFFERQUEUE, SL_IID_VOLUME}}; + const std::array ap_interfaces_req = {{true, true}}; + res = (*m_engine_engine) + ->CreateAudioPlayer(m_engine_engine, &m_player, &dsrc, &dsink, static_cast(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(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(2000.0f * std::log10(static_cast(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(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 +#include +#include +#include + +class OpenSLESAudioStream final : public AudioStream +{ +public: + OpenSLESAudioStream(); + ~OpenSLESAudioStream(); + + static std::unique_ptr 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, 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 @@ analog_stick analog_sticks + + Null (No Output) + Cubeb + OpenSL ES (Recommended) + + + Null + Cubeb + OpenSLES + 1024 Frames (23.22ms) - 2048 Frames (46.44ms) - 3072 Frames (69.66ms, Recommended) + 2048 Frames (46.44ms, Recommended) + 3072 Frames (69.66ms) 4096 Frames (92.88ms) 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" /> + 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(ar)]; } -static std::array s_audio_backend_names = {{"Null", "Cubeb", "SDL"}}; -static std::array s_audio_backend_display_names = {{TRANSLATABLE("AudioBackend", "Null (No Output)"), - TRANSLATABLE("AudioBackend", "Cubeb"), - TRANSLATABLE("AudioBackend", "SDL")}}; +static std::array s_audio_backend_names = {{ + "Null", + "Cubeb", +#ifndef ANDROID + "SDL", +#else + "OpenSLES", +#endif +}}; +static std::array 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 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 };