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:
parent
041213a597
commit
39d865b403
11 changed files with 181 additions and 102 deletions
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue