mirror of
https://github.com/LadybirdBrowser/ladybird
synced 2026-04-26 01:35:08 +02:00
210 lines
8.2 KiB
C++
210 lines
8.2 KiB
C++
/*
|
|
* Copyright (c) 2023-2026, Gregory Bertilson <gregory@ladybird.org>
|
|
*
|
|
* SPDX-License-Identifier: BSD-2-Clause
|
|
*/
|
|
|
|
#include <LibCore/EventLoop.h>
|
|
#include <LibCore/ThreadedPromise.h>
|
|
#include <LibThreading/Thread.h>
|
|
|
|
#include "PlaybackStreamPulseAudio.h"
|
|
|
|
namespace Audio {
|
|
|
|
#define TRY_OR_REJECT_AND_EXIT(expression) \
|
|
({ \
|
|
auto&& __temporary_result = (expression); \
|
|
if (__temporary_result.is_error()) [[unlikely]] { \
|
|
warnln("Failure in PulseAudio control thread: {}", __temporary_result.error().string_literal()); \
|
|
auto event_loop = main_thread_event_loop->take(); \
|
|
if (!event_loop) \
|
|
return 1; \
|
|
event_loop->deferred_invoke([promise = move(promise), error = __temporary_result.release_error()] mutable { \
|
|
promise->reject(move(error)); \
|
|
}); \
|
|
internal_state->exit(); \
|
|
return 1; \
|
|
} \
|
|
__temporary_result.release_value(); \
|
|
})
|
|
|
|
NonnullRefPtr<PlaybackStream::CreatePromise> PlaybackStream::create(OutputState initial_output_state, u32 target_latency_ms, AudioDataRequestCallback&& data_request_callback)
|
|
{
|
|
return PlaybackStreamPulseAudio::create(initial_output_state, target_latency_ms, move(data_request_callback));
|
|
}
|
|
|
|
NonnullRefPtr<PlaybackStream::CreatePromise> PlaybackStreamPulseAudio::create(OutputState initial_state, u32 target_latency_ms, AudioDataRequestCallback&& data_request_callback)
|
|
{
|
|
VERIFY(data_request_callback);
|
|
|
|
auto promise = CreatePromise::construct();
|
|
|
|
// Create an internal state for the control thread to hold on to.
|
|
auto internal_state = MUST(adopt_nonnull_ref_or_enomem(new (nothrow) InternalState()));
|
|
auto playback_stream = MUST(adopt_nonnull_ref_or_enomem(new (nothrow) PlaybackStreamPulseAudio(internal_state)));
|
|
|
|
// Create the control thread and start it.
|
|
auto thread = MUST(Threading::Thread::try_create("Audio Control"sv, [=, main_thread_event_loop = Core::EventLoop::current_weak(), data_request_callback = move(data_request_callback)]() mutable {
|
|
auto context = TRY_OR_REJECT_AND_EXIT(PulseAudioContext::the());
|
|
internal_state->set_stream(TRY_OR_REJECT_AND_EXIT(context->create_stream(initial_state, target_latency_ms, [data_request_callback = move(data_request_callback)](PulseAudioStream&, Span<float> buffer) {
|
|
return data_request_callback(buffer);
|
|
})));
|
|
|
|
// PulseAudio retains the last volume it sets for an application. We want to consistently
|
|
// start at 100% volume instead.
|
|
TRY_OR_REJECT_AND_EXIT(internal_state->stream()->set_volume(1.0));
|
|
|
|
{
|
|
auto event_loop = main_thread_event_loop->take();
|
|
if (!event_loop)
|
|
return 1;
|
|
event_loop->deferred_invoke([promise = move(promise), playback_stream = move(playback_stream)] mutable {
|
|
promise->resolve(move(playback_stream));
|
|
});
|
|
}
|
|
|
|
internal_state->thread_loop();
|
|
return 0;
|
|
}));
|
|
|
|
thread->start();
|
|
thread->detach();
|
|
return promise;
|
|
}
|
|
|
|
PlaybackStreamPulseAudio::PlaybackStreamPulseAudio(NonnullRefPtr<InternalState> state)
|
|
: m_state(move(state))
|
|
{
|
|
}
|
|
|
|
SampleSpecification PlaybackStreamPulseAudio::sample_specification() const
|
|
{
|
|
return m_state->stream()->sample_specification();
|
|
}
|
|
|
|
PlaybackStreamPulseAudio::~PlaybackStreamPulseAudio()
|
|
{
|
|
m_state->exit();
|
|
}
|
|
|
|
#define TRY_OR_REJECT(expression, ...) \
|
|
({ \
|
|
auto&& __temporary_result = (expression); \
|
|
if (__temporary_result.is_error()) [[unlikely]] { \
|
|
promise->reject(__temporary_result.release_error()); \
|
|
return __VA_ARGS__; \
|
|
} \
|
|
__temporary_result.release_value(); \
|
|
})
|
|
|
|
void PlaybackStreamPulseAudio::set_underrun_callback(Function<void()> callback)
|
|
{
|
|
m_state->enqueue([&state = *m_state, callback = move(callback)]() mutable {
|
|
state.stream()->set_underrun_callback(move(callback));
|
|
});
|
|
}
|
|
|
|
NonnullRefPtr<Core::ThreadedPromise<AK::Duration>> PlaybackStreamPulseAudio::resume()
|
|
{
|
|
auto promise = Core::ThreadedPromise<AK::Duration>::create();
|
|
TRY_OR_REJECT(m_state->check_is_running(), promise);
|
|
m_state->enqueue([&state = *m_state, promise]() {
|
|
TRY_OR_REJECT(state.stream()->resume());
|
|
promise->resolve(state.stream()->total_time_played());
|
|
});
|
|
return promise;
|
|
}
|
|
|
|
NonnullRefPtr<Core::ThreadedPromise<void>> PlaybackStreamPulseAudio::drain_buffer_and_suspend()
|
|
{
|
|
auto promise = Core::ThreadedPromise<void>::create();
|
|
TRY_OR_REJECT(m_state->check_is_running(), promise);
|
|
m_state->enqueue([&state = *m_state, promise]() {
|
|
TRY_OR_REJECT(state.stream()->drain_and_suspend());
|
|
promise->resolve();
|
|
});
|
|
return promise;
|
|
}
|
|
|
|
NonnullRefPtr<Core::ThreadedPromise<void>> PlaybackStreamPulseAudio::discard_buffer_and_suspend()
|
|
{
|
|
auto promise = Core::ThreadedPromise<void>::create();
|
|
TRY_OR_REJECT(m_state->check_is_running(), promise);
|
|
m_state->enqueue([&state = *m_state, promise]() {
|
|
TRY_OR_REJECT(state.stream()->flush_and_suspend());
|
|
promise->resolve();
|
|
});
|
|
return promise;
|
|
}
|
|
|
|
AK::Duration PlaybackStreamPulseAudio::total_time_played() const
|
|
{
|
|
if (m_state->stream() != nullptr)
|
|
return m_state->stream()->total_time_played();
|
|
return AK::Duration::zero();
|
|
}
|
|
|
|
NonnullRefPtr<Core::ThreadedPromise<void>> PlaybackStreamPulseAudio::set_volume(double volume)
|
|
{
|
|
auto promise = Core::ThreadedPromise<void>::create();
|
|
TRY_OR_REJECT(m_state->check_is_running(), promise);
|
|
m_state->enqueue([&state = *m_state, promise, volume]() {
|
|
TRY_OR_REJECT(state.stream()->set_volume(volume));
|
|
promise->resolve();
|
|
});
|
|
return promise;
|
|
}
|
|
|
|
ErrorOr<void> PlaybackStreamPulseAudio::InternalState::check_is_running()
|
|
{
|
|
if (m_exit)
|
|
return Error::from_string_literal("PulseAudio control thread loop is not running");
|
|
return {};
|
|
}
|
|
|
|
void PlaybackStreamPulseAudio::InternalState::set_stream(NonnullRefPtr<PulseAudioStream>&& stream)
|
|
{
|
|
m_stream = move(stream);
|
|
}
|
|
|
|
RefPtr<PulseAudioStream> const& PlaybackStreamPulseAudio::InternalState::stream()
|
|
{
|
|
return m_stream;
|
|
}
|
|
|
|
void PlaybackStreamPulseAudio::InternalState::enqueue(Function<void()>&& task)
|
|
{
|
|
Threading::MutexLocker locker { m_mutex };
|
|
m_tasks.enqueue(forward<Function<void()>>(task));
|
|
m_wake_condition.signal();
|
|
}
|
|
|
|
void PlaybackStreamPulseAudio::InternalState::thread_loop()
|
|
{
|
|
while (true) {
|
|
auto task = [this]() -> Function<void()> {
|
|
Threading::MutexLocker locker { m_mutex };
|
|
|
|
while (m_tasks.is_empty() && !m_exit)
|
|
m_wake_condition.wait();
|
|
if (m_exit)
|
|
return nullptr;
|
|
return m_tasks.dequeue();
|
|
}();
|
|
if (!task) {
|
|
VERIFY(m_exit);
|
|
break;
|
|
}
|
|
task();
|
|
}
|
|
}
|
|
|
|
void PlaybackStreamPulseAudio::InternalState::exit()
|
|
{
|
|
m_exit = true;
|
|
m_wake_condition.signal();
|
|
}
|
|
|
|
}
|