LibMedia: Provide new PlaybackStreams through promises

This allows us to avoid returning a PlaybackStream in cases where the
async initialization fails.

This is a step towards more graceful fallbacks when audio fails in
AudioMixingSink.
This commit is contained in:
Zaggy1024 2026-03-21 04:40:39 -05:00 committed by Gregory Bertilson
parent 041213a597
commit 39d865b403
11 changed files with 181 additions and 102 deletions

View file

@ -9,12 +9,14 @@
namespace Audio {
#if !defined(AK_OS_WINDOWS)
ErrorOr<NonnullRefPtr<PlaybackStream>> __attribute__((weak)) PlaybackStream::create(OutputState, u32, SampleSpecificationCallback&&, AudioDataRequestCallback&&)
NonnullRefPtr<PlaybackStream::CreatePromise> __attribute__((weak)) PlaybackStream::create(OutputState, u32, AudioDataRequestCallback&&)
#else
ErrorOr<NonnullRefPtr<PlaybackStream>> PlaybackStream::create(OutputState, u32, SampleSpecificationCallback&&, AudioDataRequestCallback&&)
NonnullRefPtr<PlaybackStream::CreatePromise> PlaybackStream::create(OutputState, u32, AudioDataRequestCallback&&)
#endif
{
return Error::from_string_literal("Audio output is not available for this platform");
auto promise = CreatePromise::construct();
promise->reject(Error::from_string_literal("Audio output is not available for this platform"));
return promise;
}
}

View file

@ -10,6 +10,7 @@
#include <AK/Function.h>
#include <AK/Time.h>
#include <LibCore/Forward.h>
#include <LibCore/Promise.h>
#include <LibCore/ThreadedPromise.h>
#include <LibMedia/Audio/SampleSpecification.h>
#include <LibMedia/Export.h>
@ -28,20 +29,22 @@ enum class OutputState {
// Timing information provided by the class should allow audio timestamps to be tracked with the best accuracy possible.
class MEDIA_API PlaybackStream : public AtomicRefCounted<PlaybackStream> {
public:
using SampleSpecificationCallback = Function<void(SampleSpecification)>;
using CreatePromise = Core::Promise<NonnullRefPtr<PlaybackStream>>;
using AudioDataRequestCallback = Function<ReadonlySpan<float>(Span<float> buffer)>;
// Creates a new audio Output class.
// Begins creating a new audio output and returns a promise that is resolved when it is ready.
//
// The initial_output_state parameter determines whether it will begin playback immediately.
//
// The SampleSpecificationCallback will be called when a SampleSpecification has been selected based on the
// default output device. It will always be called before the first data request.
// The returned promise will be resolved with the PlaybackStream if the audio output was successfully initialized,
// or rejected with an error if not.
//
// The AudioDataRequestCallback will be called when the Output needs more audio data to fill its buffers and
// The AudioDataRequestCallback will be called when the output needs more audio data to fill its buffers and
// continue playback. This callback will only be allowed to run on one thread at a time, to prevent any data
// race on the resource used by the callback.
static ErrorOr<NonnullRefPtr<PlaybackStream>> create(OutputState initial_output_state, u32 target_latency_ms, SampleSpecificationCallback&&, AudioDataRequestCallback&&);
static NonnullRefPtr<CreatePromise> create(OutputState initial_output_state, u32 target_latency_ms, AudioDataRequestCallback&&);
virtual SampleSpecification sample_specification() const = 0;
virtual ~PlaybackStream() = default;

View file

@ -158,7 +158,7 @@ static void check_audio_channel_layout_size(AudioChannelLayout& layout, u32 size
class AudioState : public RefCounted<AudioState> {
public:
static ErrorOr<NonnullRefPtr<AudioState>> create(PlaybackStream::SampleSpecificationCallback sample_specification_callback, PlaybackStream::AudioDataRequestCallback data_request_callback, OutputState initial_output_state)
static ErrorOr<NonnullRefPtr<AudioState>> create(PlaybackStream::AudioDataRequestCallback data_request_callback, OutputState initial_output_state)
{
auto state = TRY(adopt_nonnull_ref_or_enomem(new (nothrow) AudioState(move(data_request_callback), initial_output_state)));
@ -184,8 +184,6 @@ public:
auto channel_map = TRY(audio_channel_layout_to_channel_map(*layout));
state->m_sample_specification = SampleSpecification(static_cast<u32>(description->mSampleRate), channel_map);
sample_specification_callback(state->m_sample_specification);
AURenderCallbackStruct callbackStruct;
callbackStruct.inputProc = &AudioState::on_audio_unit_buffer_request;
callbackStruct.inputProcRefCon = state.ptr();
@ -217,6 +215,8 @@ public:
m_task_queue_is_empty = false;
}
SampleSpecification const& sample_specification() const { return m_sample_specification; }
AK::Duration last_sample_time() const
{
return AK::Duration::from_milliseconds(m_last_sample_time.load());
@ -319,15 +319,23 @@ private:
Atomic<i64> m_last_sample_time { 0 };
};
ErrorOr<NonnullRefPtr<PlaybackStream>> PlaybackStream::create(OutputState initial_output_state, u32 target_latency_ms, SampleSpecificationCallback&& sample_specification_callback, AudioDataRequestCallback&& data_request_callback)
NonnullRefPtr<PlaybackStream::CreatePromise> PlaybackStream::create(OutputState initial_output_state, u32 target_latency_ms, AudioDataRequestCallback&& data_request_callback)
{
return PlaybackStreamAudioUnit::create(initial_output_state, target_latency_ms, move(sample_specification_callback), move(data_request_callback));
return PlaybackStreamAudioUnit::create(initial_output_state, target_latency_ms, move(data_request_callback));
}
ErrorOr<NonnullRefPtr<PlaybackStream>> PlaybackStreamAudioUnit::create(OutputState initial_output_state, u32, SampleSpecificationCallback&& sample_specification_callback, AudioDataRequestCallback&& data_request_callback)
NonnullRefPtr<PlaybackStream::CreatePromise> PlaybackStreamAudioUnit::create(OutputState initial_output_state, u32, AudioDataRequestCallback&& data_request_callback)
{
auto state = TRY(AudioState::create(move(sample_specification_callback), move(data_request_callback), initial_output_state));
return TRY(adopt_nonnull_ref_or_enomem(new (nothrow) PlaybackStreamAudioUnit(move(state))));
auto promise = CreatePromise::construct();
auto state_or_error = AudioState::create(move(data_request_callback), initial_output_state);
if (state_or_error.is_error()) {
promise->reject(state_or_error.release_error());
return promise;
}
auto state = state_or_error.release_value();
auto stream = adopt_ref(*new PlaybackStreamAudioUnit(move(state)));
promise->resolve(stream);
return promise;
}
PlaybackStreamAudioUnit::PlaybackStreamAudioUnit(NonnullRefPtr<AudioState> impl)
@ -337,6 +345,11 @@ PlaybackStreamAudioUnit::PlaybackStreamAudioUnit(NonnullRefPtr<AudioState> impl)
PlaybackStreamAudioUnit::~PlaybackStreamAudioUnit() = default;
SampleSpecification PlaybackStreamAudioUnit::sample_specification() const
{
return m_state->sample_specification();
}
void PlaybackStreamAudioUnit::set_underrun_callback(Function<void()>)
{
// FIXME: Implement this.

View file

@ -18,7 +18,9 @@ class AudioState;
class PlaybackStreamAudioUnit final : public PlaybackStream {
public:
static ErrorOr<NonnullRefPtr<PlaybackStream>> create(OutputState initial_output_state, u32 target_latency_ms, SampleSpecificationCallback&&, AudioDataRequestCallback&&);
static NonnullRefPtr<CreatePromise> create(OutputState initial_output_state, u32 target_latency_ms, AudioDataRequestCallback&&);
virtual SampleSpecification sample_specification() const override;
virtual void set_underrun_callback(Function<void()>) override;

View file

@ -1,9 +1,10 @@
/*
* Copyright (c) 2023-2025, Gregory Bertilson <gregory@ladybird.org>
* 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>
@ -11,42 +12,55 @@
namespace Audio {
#define TRY_OR_EXIT_THREAD(expression) \
({ \
auto&& __temporary_result = (expression); \
if (__temporary_result.is_error()) [[unlikely]] { \
warnln("Failure in PulseAudio control thread: {}", __temporary_result.error().string_literal()); \
internal_state->exit(); \
return 1; \
} \
__temporary_result.release_value(); \
#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(); \
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(); \
})
ErrorOr<NonnullRefPtr<PlaybackStream>> PlaybackStream::create(OutputState initial_output_state, u32 target_latency_ms, SampleSpecificationCallback&& sample_specification_selected_callback, AudioDataRequestCallback&& data_request_callback)
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(sample_specification_selected_callback), move(data_request_callback));
return PlaybackStreamPulseAudio::create(initial_output_state, target_latency_ms, move(data_request_callback));
}
ErrorOr<NonnullRefPtr<PlaybackStream>> PlaybackStreamPulseAudio::create(OutputState initial_state, u32 target_latency_ms, SampleSpecificationCallback&& sample_specification_selected_callback, AudioDataRequestCallback&& 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 = TRY(adopt_nonnull_ref_or_enomem(new (nothrow) InternalState()));
auto playback_stream = TRY(adopt_nonnull_ref_or_enomem(new (nothrow) PlaybackStreamPulseAudio(internal_state)));
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 = TRY(Threading::Thread::try_create("Audio Control"sv, [=, sample_specification_selected_callback = move(sample_specification_selected_callback), data_request_callback = move(data_request_callback)]() mutable {
auto context = TRY_OR_EXIT_THREAD(PulseAudioContext::the());
internal_state->set_stream(TRY_OR_EXIT_THREAD(context->create_stream(initial_state, target_latency_ms, [data_request_callback = move(data_request_callback)](PulseAudioStream&, Span<float> buffer) {
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);
})));
sample_specification_selected_callback(internal_state->stream()->sample_specification());
// PulseAudio retains the last volume it sets for an application. We want to consistently
// start at 100% volume instead.
TRY_OR_EXIT_THREAD(internal_state->stream()->set_volume(1.0));
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] {
promise->resolve(playback_stream);
});
}
internal_state->thread_loop();
return 0;
@ -54,7 +68,7 @@ ErrorOr<NonnullRefPtr<PlaybackStream>> PlaybackStreamPulseAudio::create(OutputSt
thread->start();
thread->detach();
return playback_stream;
return promise;
}
PlaybackStreamPulseAudio::PlaybackStreamPulseAudio(NonnullRefPtr<InternalState> state)
@ -62,6 +76,11 @@ PlaybackStreamPulseAudio::PlaybackStreamPulseAudio(NonnullRefPtr<InternalState>
{
}
SampleSpecification PlaybackStreamPulseAudio::sample_specification() const
{
return m_state->stream()->sample_specification();
}
PlaybackStreamPulseAudio::~PlaybackStreamPulseAudio()
{
m_state->exit();

View file

@ -17,7 +17,9 @@ namespace Audio {
class PlaybackStreamPulseAudio final
: public PlaybackStream {
public:
static ErrorOr<NonnullRefPtr<PlaybackStream>> create(OutputState, u32 target_latency_ms, SampleSpecificationCallback&&, AudioDataRequestCallback&&);
static NonnullRefPtr<CreatePromise> create(OutputState, u32 target_latency_ms, AudioDataRequestCallback&&);
virtual SampleSpecification sample_specification() const override;
virtual void set_underrun_callback(Function<void()>) override;

View file

@ -50,13 +50,6 @@ using namespace Microsoft::WRL;
} \
})
#define TRY_HR(expression) \
({ \
AK_IGNORE_DIAGNOSTIC("-Wshadow", HRESULT&& _temporary_hr = (expression)); \
if (FAILED(_temporary_hr)) [[unlikely]] \
return Error::from_windows_error(_temporary_hr); \
})
// GUID for the playback session. That way all render streams have a single volume slider in the OS interface
constexpr GUID PlaybackSessionGUID = { // 22f2ca89-210a-492c-a0aa-f25b1d2f33a1
0x22f2ca89,
@ -158,9 +151,9 @@ PlaybackStreamWASAPI::~PlaybackStreamWASAPI()
SetEvent(m_state->buffer_event);
}
ErrorOr<NonnullRefPtr<PlaybackStream>> PlaybackStream::create(OutputState initial_output_state, u32 target_latency_ms, SampleSpecificationCallback&& specification_callback, AudioDataRequestCallback&& data_callback)
NonnullRefPtr<PlaybackStream::CreatePromise> PlaybackStream::create(OutputState initial_output_state, u32 target_latency_ms, AudioDataRequestCallback&& data_callback)
{
return PlaybackStreamWASAPI::create(initial_output_state, target_latency_ms, move(specification_callback), move(data_callback));
return PlaybackStreamWASAPI::create(initial_output_state, target_latency_ms, move(data_callback));
}
static void print_audio_format(WAVEFORMATEXTENSIBLE& format)
@ -228,8 +221,19 @@ static ErrorOr<ChannelMap> convert_bitmask_to_channel_map(u32 channel_bitmask)
return ChannelMap { channels };
}
ErrorOr<NonnullRefPtr<PlaybackStream>> PlaybackStreamWASAPI::create(OutputState initial_output_state, u32, SampleSpecificationCallback&& sample_specification_callback, AudioDataRequestCallback&& data_request_callback)
NonnullRefPtr<PlaybackStream::CreatePromise> PlaybackStreamWASAPI::create(OutputState initial_output_state, u32, AudioDataRequestCallback&& data_request_callback)
{
auto promise = CreatePromise::construct();
#define TRY_HR(expression) \
({ \
AK_IGNORE_DIAGNOSTIC("-Wshadow", HRESULT&& _temporary_hr = (expression)); \
if (FAILED(_temporary_hr)) [[unlikely]] { \
promise->reject(Error::from_windows_error(_temporary_hr)); \
return promise; \
} \
})
HRESULT hr;
if (!s_com_uninitializer.initialized) {
TRY_HR(CoInitializeEx(NULL, COINIT_MULTITHREADED));
@ -257,10 +261,6 @@ ErrorOr<NonnullRefPtr<PlaybackStream>> PlaybackStreamWASAPI::create(OutputState
VERIFY(popcount(device_format->dwChannelMask) == device_format->Format.nChannels);
auto channels = device_format->Format.nChannels;
ChannelMap channel_map = MUST(convert_bitmask_to_channel_map(device_format->dwChannelMask));
sample_specification_callback(SampleSpecification { device_format->Format.nSamplesPerSec, channel_map });
// Set up a 32bit float pcm stream with whatever sample rate and channels we were given.
auto block_align = channels * sizeof(float);
@ -279,8 +279,10 @@ ErrorOr<NonnullRefPtr<PlaybackStream>> PlaybackStreamWASAPI::create(OutputState
WAVEFORMATEXTENSIBLE* closest_match;
hr = state->audio_client->IsFormatSupported(AUDCLNT_SHAREMODE_SHARED, &state->wave_format.Format, reinterpret_cast<WAVEFORMATEX**>(&closest_match));
if (FAILED(hr))
return Error::from_windows_error(hr);
if (FAILED(hr)) {
promise->reject(Error::from_windows_error(hr));
return promise;
}
if (hr == S_FALSE) {
dbgln("Audio format not supported. Current format:\n");
print_audio_format(state->wave_format);
@ -300,8 +302,10 @@ ErrorOr<NonnullRefPtr<PlaybackStream>> PlaybackStreamWASAPI::create(OutputState
TRY_HR(state->audio_client->GetService(IID_PPV_ARGS(&state->clock)));
state->buffer_event = CreateEvent(NULL, FALSE, FALSE, NULL);
if (!state->buffer_event)
return Error::from_windows_error(hr);
if (!state->buffer_event) {
promise->reject(Error::from_windows_error(hr));
return promise;
}
TRY_HR(state->audio_client->SetEventHandle(state->buffer_event));
TRY_HR(state->clock->GetFrequency(&state->audio_client_clock_frequency));
@ -319,7 +323,17 @@ ErrorOr<NonnullRefPtr<PlaybackStream>> PlaybackStreamWASAPI::create(OutputState
audio_thread->start();
audio_thread->detach();
return TRY(adopt_nonnull_ref_or_enomem(new (nothrow) PlaybackStreamWASAPI(move(state))));
auto stream = adopt_ref(*new PlaybackStreamWASAPI(move(state)));
promise->resolve(stream);
return promise;
#undef TRY_HR
}
SampleSpecification PlaybackStreamWASAPI::sample_specification() const
{
auto channel_map = MUST(convert_bitmask_to_channel_map(m_state->wave_format.dwChannelMask));
return SampleSpecification { m_state->wave_format.Format.nSamplesPerSec, channel_map };
}
int PlaybackStreamWASAPI::AudioState::render_thread_loop(PlaybackStreamWASAPI::AudioState& state)

View file

@ -14,7 +14,9 @@ namespace Audio {
class PlaybackStreamWASAPI final : public PlaybackStream {
public:
static ErrorOr<NonnullRefPtr<PlaybackStream>> create(OutputState initial_output_state, u32 target_latency_ms, SampleSpecificationCallback&&, AudioDataRequestCallback&&);
static NonnullRefPtr<CreatePromise> create(OutputState initial_output_state, u32 target_latency_ms, AudioDataRequestCallback&&);
virtual SampleSpecification sample_specification() const override;
// The overrun callback must be realtime safe. The buffer size might be small.
virtual void set_underrun_callback(Function<void()>) override;

View file

@ -24,6 +24,12 @@ AudioMixingSink::AudioMixingSink(AudioMixingSinkWeakReference& weak_ref)
: m_main_thread_event_loop(Core::EventLoop::current())
, m_weak_self(weak_ref)
{
m_main_thread_event_loop.deferred_invoke([weak_self = m_weak_self] {
auto self = weak_self->take_strong();
if (!self)
return;
self->create_playback_stream();
});
}
AudioMixingSink::~AudioMixingSink()
@ -38,8 +44,6 @@ void AudioMixingSink::set_provider(Track const& track, RefPtr<AudioDataProvider>
if (provider == nullptr)
return;
create_playback_stream();
// The provider must have its output sample specification set before it starts decoding, or
// we'll drop some samples due to a mismatch.
m_track_mixing_datas.set(track, TrackMixingData(*provider));
@ -59,25 +63,11 @@ RefPtr<AudioDataProvider> AudioMixingSink::provider(Track const& track) const
void AudioMixingSink::create_playback_stream()
{
if (m_playback_stream != nullptr)
if (m_playback_stream != nullptr || m_creating_playback_stream)
return;
auto sample_specification_callback = [weak_self = m_weak_self](Audio::SampleSpecification sample_specification) {
auto self = weak_self->take_strong();
if (!self)
return;
m_creating_playback_stream = true;
Threading::MutexLocker locker { self->m_mutex };
self->m_sample_specification = sample_specification;
for (auto& [track, track_data] : self->m_track_mixing_datas) {
track_data.provider->set_output_sample_specification(sample_specification);
track_data.provider->start();
}
if (self->m_playing)
self->resume();
};
auto data_callback = [weak_self = m_weak_self](Span<float> buffer) -> ReadonlySpan<float> {
auto self = weak_self->take_strong();
if (!self)
@ -86,14 +76,39 @@ void AudioMixingSink::create_playback_stream()
};
constexpr u32 target_latency_ms = 100;
auto stream_or_error = Audio::PlaybackStream::create(Audio::OutputState::Suspended, target_latency_ms, move(sample_specification_callback), move(data_callback));
auto promise = Audio::PlaybackStream::create(Audio::OutputState::Suspended, target_latency_ms, move(data_callback));
if (!stream_or_error.is_error()) {
m_playback_stream = stream_or_error.value();
set_volume(m_volume);
} else {
dbgln("Failed to create playback stream: {}", stream_or_error.error());
}
promise->when_resolved([weak_self = m_weak_self](auto& stream) {
auto self = weak_self->take_strong();
if (!self)
return;
self->m_creating_playback_stream = false;
self->m_playback_stream = stream;
self->set_volume(self->m_volume);
if (self->m_temporary_time.has_value())
self->set_time(self->m_temporary_time.value());
Threading::MutexLocker locker { self->m_mutex };
self->m_sample_specification = stream->sample_specification();
for (auto& [track, track_data] : self->m_track_mixing_datas) {
track_data.provider->set_output_sample_specification(self->m_sample_specification);
track_data.provider->start();
}
if (self->m_playing)
self->resume();
});
promise->when_rejected([weak_self = m_weak_self](auto& error) {
auto self = weak_self->take_strong();
if (!self)
return;
self->m_creating_playback_stream = false;
dbgln("Failed to create playback stream: {}", error);
});
}
ReadonlySpan<float> AudioMixingSink::write_audio_data_to_playback_stream(Span<float> buffer)
@ -267,6 +282,9 @@ void AudioMixingSink::set_time(AK::Duration time)
}
m_temporary_time = time;
if (!m_playback_stream)
return;
m_playback_stream->drain_buffer_and_suspend()
->when_resolved([weak_self = m_weak_self, &playback_stream = *m_playback_stream]() {
auto self = weak_self->take_strong();

View file

@ -82,6 +82,7 @@ private:
Threading::Mutex m_mutex;
Threading::ConditionVariable m_wait_condition { m_mutex };
bool m_creating_playback_stream { false };
RefPtr<Audio::PlaybackStream> m_playback_stream;
Audio::SampleSpecification m_sample_specification;
bool m_playing { false };

View file

@ -28,30 +28,33 @@ TEST_CASE(create_and_destroy_playback_stream)
has_implementation = true;
# endif
{
auto stream_result = Audio::PlaybackStream::create(Audio::OutputState::Playing, 100, [](Audio::SampleSpecification) {}, [](Span<float> buffer) -> ReadonlySpan<float> { return buffer.trim(0); });
if (has_implementation) {
auto stream = stream_result.release_value();
Audio::PlaybackStream::create(Audio::OutputState::Playing, 100, [](Span<float> buffer) -> ReadonlySpan<float> { return buffer.trim(0); })
->when_resolved([&](auto& stream) {
if (!has_implementation)
VERIFY_NOT_REACHED();
EXPECT_EQ(stream->total_time_played(), AK::Duration::zero());
for (int i = 0; i < 5; i++) {
stream->resume()->when_rejected([](Error const&) { VERIFY_NOT_REACHED(); });
stream->drain_buffer_and_suspend()->when_rejected([](Error const&) { VERIFY_NOT_REACHED(); });
}
} else {
EXPECT(stream_result.is_error());
dbgln("Failed to create playback stream: {}", stream_result.error());
}
}
# if defined(HAVE_PULSEAUDIO)
// The PulseAudio context is kept alive by the PlaybackStream's control thread, which blocks on
// some operations, so it won't necessarily be destroyed immediately.
auto wait_start = MonotonicTime::now_coarse();
while (Audio::PulseAudioContext::is_connected()) {
if (MonotonicTime::now_coarse() - wait_start > AK::Duration::from_milliseconds(100))
VERIFY_NOT_REACHED();
}
// The PulseAudio context is kept alive by the PlaybackStream's control thread, which blocks on
// some operations, so it won't necessarily be destroyed immediately.
auto wait_start = MonotonicTime::now_coarse();
while (Audio::PulseAudioContext::is_connected()) {
if (MonotonicTime::now_coarse() - wait_start > AK::Duration::from_milliseconds(100))
VERIFY_NOT_REACHED();
}
# endif
})
.when_rejected([&](auto& error) {
if (has_implementation) {
dbgln("Failed to create playback stream: {}", error);
VERIFY_NOT_REACHED();
}
});
}
#endif