ladybird/Libraries/LibMedia/Audio/PlaybackStreamPulseAudio.cpp
R-Goc 02bb892d7a LibThreading/LibSync: Split out sync primitives
This commit splits out synchronization primitives from LibThreading into
LibSync. This is because LibThreading depends on LibCore, while LibCore
needs the synchronization primitives from LibThreading. This worked
while they were header only, but when I tried to add an implementation
file it ran into the circular dependency. To abstract away the pthread
implementation using cpp files is necessary so the synchronization
primitives were moved to a separate library.
2026-05-08 18:58:35 -05:00

209 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)
{
Sync::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()> {
Sync::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();
}
}