From 6b88e43b3bd74f67e71aa82cc01a9a98595c73b9 Mon Sep 17 00:00:00 2001 From: Olekoop Date: Tue, 16 Jul 2024 15:02:16 +0200 Subject: [PATCH] LibAudio: Implement PlaybackStream for Android using Oboe https://github.com/google/oboe There are many ways to implement audio for Android, however this is the recommended way to do it. --- Ladybird/Android/build.gradle.kts | 3 + .../src/main/cpp/WebContentService.cpp | 3 +- Userland/Libraries/LibAudio/CMakeLists.txt | 6 + .../Libraries/LibAudio/PlaybackStream.cpp | 4 + .../Libraries/LibAudio/PlaybackStreamOboe.cpp | 158 ++++++++++++++++++ .../Libraries/LibAudio/PlaybackStreamOboe.h | 36 ++++ 6 files changed, 208 insertions(+), 2 deletions(-) create mode 100644 Userland/Libraries/LibAudio/PlaybackStreamOboe.cpp create mode 100644 Userland/Libraries/LibAudio/PlaybackStreamOboe.h diff --git a/Ladybird/Android/build.gradle.kts b/Ladybird/Android/build.gradle.kts index 8abcdd8bfee..38633dbc8eb 100644 --- a/Ladybird/Android/build.gradle.kts +++ b/Ladybird/Android/build.gradle.kts @@ -38,6 +38,7 @@ android { cppFlags += "-std=c++2b" arguments += listOf( "-DLagomTools_DIR=$buildDir/lagom-tools-install/share/LagomTools", + "-DANDROID_STL=c++_shared", "-DSERENITY_CACHE_DIR=$cacheDir", "-DVCPKG_ROOT=$sourceDir/Toolchain/Tarballs/vcpkg", "-DVCPKG_TARGET_ANDROID=ON" @@ -76,6 +77,7 @@ android { buildFeatures { viewBinding = true + prefab = true } } @@ -89,4 +91,5 @@ dependencies { androidTestImplementation("androidx.test.ext:junit:1.1.5") androidTestImplementation("androidx.test.ext:junit-ktx:1.1.5") androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") + implementation("com.google.oboe:oboe:1.9.0") } diff --git a/Ladybird/Android/src/main/cpp/WebContentService.cpp b/Ladybird/Android/src/main/cpp/WebContentService.cpp index 1240cc81012..dabcc89961c 100644 --- a/Ladybird/Android/src/main/cpp/WebContentService.cpp +++ b/Ladybird/Android/src/main/cpp/WebContentService.cpp @@ -56,8 +56,7 @@ ErrorOr service_main(int ipc_socket) Web::Platform::ImageCodecPlugin::install(*new Ladybird::ImageCodecPlugin(move(image_decoder_client))); Web::Platform::AudioCodecPlugin::install_creation_hook([](auto loader) { - (void)loader; - return Error::from_string_literal("Don't know how to initialize audio in this configuration!"); + return Web::Platform::AudioCodecPluginAgnostic::create(move(loader)); }); auto request_server_client = TRY(bind_request_server_service()); diff --git a/Userland/Libraries/LibAudio/CMakeLists.txt b/Userland/Libraries/LibAudio/CMakeLists.txt index f6e1fdcf9af..0081052f1d0 100644 --- a/Userland/Libraries/LibAudio/CMakeLists.txt +++ b/Userland/Libraries/LibAudio/CMakeLists.txt @@ -36,3 +36,9 @@ if (APPLE AND NOT IOS) find_library(AUDIO_UNIT AudioUnit REQUIRED) target_link_libraries(LibAudio PRIVATE ${AUDIO_UNIT}) endif() + +if (ANDROID) + target_sources(LibAudio PRIVATE PlaybackStreamOboe.cpp) + find_package(oboe REQUIRED CONFIG) + target_link_libraries(LibAudio PRIVATE log oboe::oboe) +endif() diff --git a/Userland/Libraries/LibAudio/PlaybackStream.cpp b/Userland/Libraries/LibAudio/PlaybackStream.cpp index 168e7ba88f7..94e7e625b09 100644 --- a/Userland/Libraries/LibAudio/PlaybackStream.cpp +++ b/Userland/Libraries/LibAudio/PlaybackStream.cpp @@ -13,6 +13,8 @@ # include #elif defined(AK_OS_MACOS) # include +#elif defined(AK_OS_ANDROID) +# include #endif namespace Audio { @@ -25,6 +27,8 @@ ErrorOr> PlaybackStream::create(OutputState initia return PlaybackStreamPulseAudio::create(initial_output_state, sample_rate, channels, target_latency_ms, move(data_request_callback)); #elif defined(AK_OS_MACOS) return PlaybackStreamAudioUnit::create(initial_output_state, sample_rate, channels, target_latency_ms, move(data_request_callback)); +#elif defined(AK_OS_ANDROID) + return PlaybackStreamOboe::create(initial_output_state, sample_rate, channels, target_latency_ms, move(data_request_callback)); #else (void)initial_output_state, (void)sample_rate, (void)channels, (void)target_latency_ms; return Error::from_string_literal("Audio output is not available for this platform"); diff --git a/Userland/Libraries/LibAudio/PlaybackStreamOboe.cpp b/Userland/Libraries/LibAudio/PlaybackStreamOboe.cpp new file mode 100644 index 00000000000..d619fd68399 --- /dev/null +++ b/Userland/Libraries/LibAudio/PlaybackStreamOboe.cpp @@ -0,0 +1,158 @@ +/* + * Copyright (c) 2024, Olekoop + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#define AK_DONT_REPLACE_STD +#include +#include +#include +#include +#include +#include + +#include + +namespace Audio { + +class OboeCallback : public oboe::AudioStreamDataCallback { +public: + virtual oboe::DataCallbackResult onAudioReady(oboe::AudioStream* oboeStream, void* audioData, int32_t numFrames) + { + Bytes output_buffer { + reinterpret_cast(audioData), + static_cast(numFrames * oboeStream->getChannelCount() * sizeof(float)) + }; + auto written_bytes = m_data_request_callback(output_buffer, PcmSampleFormat::Float32, numFrames); + if (written_bytes.is_empty()) + return oboe::DataCallbackResult::Stop; + + auto timestamp = oboeStream->getTimestamp(CLOCK_MONOTONIC); + if (timestamp == oboe::Result::OK) { + m_number_of_samples_enqueued = timestamp.value().position; + } else { + // Fallback for OpenSLES + m_number_of_samples_enqueued += numFrames; + } + auto last_sample_time = static_cast(m_number_of_samples_enqueued / oboeStream->getSampleRate()); + m_last_sample_time.store(last_sample_time); + + float* output = (float*)audioData; + for (int frames = 0; frames < numFrames; frames++) { + for (int channels = 0; channels < oboeStream->getChannelCount(); channels++) { + *output++ *= m_volume.load(); + } + } + return oboe::DataCallbackResult::Continue; + } + OboeCallback(PlaybackStream::AudioDataRequestCallback data_request_callback) + : m_data_request_callback(move(data_request_callback)) + { + } + Duration last_sample_time() const + { + return Duration::from_seconds(m_last_sample_time.load()); + } + void set_volume(float volume) + { + m_volume.store(volume); + } + +private: + PlaybackStream::AudioDataRequestCallback m_data_request_callback; + Atomic m_last_sample_time { 0 }; + size_t m_number_of_samples_enqueued { 0 }; + Atomic m_volume { 1.0 }; +}; + +class PlaybackStreamOboe::Storage : public RefCounted { +public: + Storage(std::shared_ptr stream, std::shared_ptr oboe_callback) + : m_stream(move(stream)) + , m_oboe_callback(move(oboe_callback)) + { + } + std::shared_ptr stream() const { return m_stream; } + std::shared_ptr oboe_callback() const { return m_oboe_callback; } + +private: + std::shared_ptr m_stream; + std::shared_ptr m_oboe_callback; +}; + +PlaybackStreamOboe::PlaybackStreamOboe(NonnullRefPtr storage) + : m_storage(move(storage)) +{ +} + +ErrorOr> PlaybackStreamOboe::create(OutputState initial_output_state, u32 sample_rate, u8 channels, u32, AudioDataRequestCallback&& data_request_callback) +{ + std::shared_ptr stream; + auto oboe_callback = std::make_shared(move(data_request_callback)); + oboe::AudioStreamBuilder builder; + auto result = builder.setSharingMode(oboe::SharingMode::Shared) + ->setPerformanceMode(oboe::PerformanceMode::LowLatency) + ->setFormat(oboe::AudioFormat::Float) + ->setDataCallback(oboe_callback) + ->setChannelCount(channels) + ->setSampleRate(sample_rate) + ->openStream(stream); + + if (result != oboe::Result::OK) + return Error::from_string_literal("Oboe failed to start"); + + if (initial_output_state == OutputState::Playing) + stream->requestStart(); + + auto storage = TRY(adopt_nonnull_ref_or_enomem(new PlaybackStreamOboe::Storage(move(stream), move(oboe_callback)))); + return TRY(adopt_nonnull_ref_or_enomem(new (nothrow) PlaybackStreamOboe(move(storage)))); +} + +PlaybackStreamOboe::~PlaybackStreamOboe() = default; + +void PlaybackStreamOboe::set_underrun_callback(Function) +{ + // FIXME: Implement this. +} + +NonnullRefPtr> PlaybackStreamOboe::resume() +{ + auto promise = Core::ThreadedPromise::create(); + auto time = MUST(total_time_played()); + m_storage->stream()->start(); + promise->resolve(move(time)); + return promise; +} + +NonnullRefPtr> PlaybackStreamOboe::drain_buffer_and_suspend() +{ + auto promise = Core::ThreadedPromise::create(); + m_storage->stream()->stop(); + promise->resolve(); + return promise; +} + +NonnullRefPtr> PlaybackStreamOboe::discard_buffer_and_suspend() +{ + auto promise = Core::ThreadedPromise::create(); + m_storage->stream()->pause(); + m_storage->stream()->flush(); + promise->resolve(); + return promise; +} + +ErrorOr PlaybackStreamOboe::total_time_played() +{ + return m_storage->oboe_callback()->last_sample_time(); +} + +NonnullRefPtr> PlaybackStreamOboe::set_volume(double volume) +{ + auto promise = Core::ThreadedPromise::create(); + m_storage->oboe_callback()->set_volume(volume); + promise->resolve(); + return promise; +} + +} diff --git a/Userland/Libraries/LibAudio/PlaybackStreamOboe.h b/Userland/Libraries/LibAudio/PlaybackStreamOboe.h new file mode 100644 index 00000000000..be2c810130e --- /dev/null +++ b/Userland/Libraries/LibAudio/PlaybackStreamOboe.h @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2024, Olekoop + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include + +namespace Audio { + +class PlaybackStreamOboe final : public PlaybackStream { +public: + static ErrorOr> create(OutputState initial_output_state, u32 sample_rate, u8 channels, u32 target_latency_ms, AudioDataRequestCallback&& data_request_callback); + + virtual void set_underrun_callback(Function) override; + + virtual NonnullRefPtr> resume() override; + virtual NonnullRefPtr> drain_buffer_and_suspend() override; + virtual NonnullRefPtr> discard_buffer_and_suspend() override; + + virtual ErrorOr total_time_played() override; + + virtual NonnullRefPtr> set_volume(double) override; + +private: + class Storage; + explicit PlaybackStreamOboe(NonnullRefPtr); + ~PlaybackStreamOboe(); + RefPtr m_storage; +}; + +}