Przeglądaj źródła

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.
Olekoop 1 rok temu
rodzic
commit
6b88e43b3b

+ 3 - 0
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")
 }

+ 1 - 2
Ladybird/Android/src/main/cpp/WebContentService.cpp

@@ -56,8 +56,7 @@ ErrorOr<int> 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());

+ 6 - 0
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()

+ 4 - 0
Userland/Libraries/LibAudio/PlaybackStream.cpp

@@ -13,6 +13,8 @@
 #    include <LibAudio/PlaybackStreamPulseAudio.h>
 #elif defined(AK_OS_MACOS)
 #    include <LibAudio/PlaybackStreamAudioUnit.h>
+#elif defined(AK_OS_ANDROID)
+#    include <LibAudio/PlaybackStreamOboe.h>
 #endif
 
 namespace Audio {
@@ -25,6 +27,8 @@ ErrorOr<NonnullRefPtr<PlaybackStream>> 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");

+ 158 - 0
Userland/Libraries/LibAudio/PlaybackStreamOboe.cpp

@@ -0,0 +1,158 @@
+/*
+ * Copyright (c) 2024, Olekoop <mlglol360xd@gmail.com>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#define AK_DONT_REPLACE_STD
+#include <AK/Atomic.h>
+#include <AK/SourceLocation.h>
+#include <LibAudio/PlaybackStreamOboe.h>
+#include <LibCore/SharedCircularQueue.h>
+#include <LibCore/ThreadedPromise.h>
+#include <memory>
+
+#include <oboe/Oboe.h>
+
+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<u8*>(audioData),
+            static_cast<size_t>(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<i64>(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<i64> m_last_sample_time { 0 };
+    size_t m_number_of_samples_enqueued { 0 };
+    Atomic<float> m_volume { 1.0 };
+};
+
+class PlaybackStreamOboe::Storage : public RefCounted<PlaybackStreamOboe::Storage> {
+public:
+    Storage(std::shared_ptr<oboe::AudioStream> stream, std::shared_ptr<OboeCallback> oboe_callback)
+        : m_stream(move(stream))
+        , m_oboe_callback(move(oboe_callback))
+    {
+    }
+    std::shared_ptr<oboe::AudioStream> stream() const { return m_stream; }
+    std::shared_ptr<OboeCallback> oboe_callback() const { return m_oboe_callback; }
+
+private:
+    std::shared_ptr<oboe::AudioStream> m_stream;
+    std::shared_ptr<OboeCallback> m_oboe_callback;
+};
+
+PlaybackStreamOboe::PlaybackStreamOboe(NonnullRefPtr<Storage> storage)
+    : m_storage(move(storage))
+{
+}
+
+ErrorOr<NonnullRefPtr<PlaybackStream>> PlaybackStreamOboe::create(OutputState initial_output_state, u32 sample_rate, u8 channels, u32, AudioDataRequestCallback&& data_request_callback)
+{
+    std::shared_ptr<oboe::AudioStream> stream;
+    auto oboe_callback = std::make_shared<OboeCallback>(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<void()>)
+{
+    // FIXME: Implement this.
+}
+
+NonnullRefPtr<Core::ThreadedPromise<Duration>> PlaybackStreamOboe::resume()
+{
+    auto promise = Core::ThreadedPromise<Duration>::create();
+    auto time = MUST(total_time_played());
+    m_storage->stream()->start();
+    promise->resolve(move(time));
+    return promise;
+}
+
+NonnullRefPtr<Core::ThreadedPromise<void>> PlaybackStreamOboe::drain_buffer_and_suspend()
+{
+    auto promise = Core::ThreadedPromise<void>::create();
+    m_storage->stream()->stop();
+    promise->resolve();
+    return promise;
+}
+
+NonnullRefPtr<Core::ThreadedPromise<void>> PlaybackStreamOboe::discard_buffer_and_suspend()
+{
+    auto promise = Core::ThreadedPromise<void>::create();
+    m_storage->stream()->pause();
+    m_storage->stream()->flush();
+    promise->resolve();
+    return promise;
+}
+
+ErrorOr<Duration> PlaybackStreamOboe::total_time_played()
+{
+    return m_storage->oboe_callback()->last_sample_time();
+}
+
+NonnullRefPtr<Core::ThreadedPromise<void>> PlaybackStreamOboe::set_volume(double volume)
+{
+    auto promise = Core::ThreadedPromise<void>::create();
+    m_storage->oboe_callback()->set_volume(volume);
+    promise->resolve();
+    return promise;
+}
+
+}

+ 36 - 0
Userland/Libraries/LibAudio/PlaybackStreamOboe.h

@@ -0,0 +1,36 @@
+/*
+ * Copyright (c) 2024, Olekoop <mlglol360xd@gmail.com>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#pragma once
+
+#include <AK/Error.h>
+#include <AK/NonnullRefPtr.h>
+#include <LibAudio/PlaybackStream.h>
+
+namespace Audio {
+
+class PlaybackStreamOboe final : public PlaybackStream {
+public:
+    static ErrorOr<NonnullRefPtr<PlaybackStream>> create(OutputState initial_output_state, u32 sample_rate, u8 channels, u32 target_latency_ms, AudioDataRequestCallback&& data_request_callback);
+
+    virtual void set_underrun_callback(Function<void()>) override;
+
+    virtual NonnullRefPtr<Core::ThreadedPromise<Duration>> resume() override;
+    virtual NonnullRefPtr<Core::ThreadedPromise<void>> drain_buffer_and_suspend() override;
+    virtual NonnullRefPtr<Core::ThreadedPromise<void>> discard_buffer_and_suspend() override;
+
+    virtual ErrorOr<Duration> total_time_played() override;
+
+    virtual NonnullRefPtr<Core::ThreadedPromise<void>> set_volume(double) override;
+
+private:
+    class Storage;
+    explicit PlaybackStreamOboe(NonnullRefPtr<Storage>);
+    ~PlaybackStreamOboe();
+    RefPtr<Storage> m_storage;
+};
+
+}