1
0
Fork 0
mirror of https://github.com/LadybirdBrowser/ladybird.git synced 2025-06-08 05:27:14 +09:00

LibMedia: Use a simple locked vector to handle audio commands on macOS

The SharedSingleProducerCircularQueue used here has dubious value, This
queue is used to pass commands to the audio thread, such as play/pause/
seek/volume change/etc. We can make do with a simple locked vector, as
we were blocking to enqueue tasks anyways. We can also use an atomic
bool to tell the audio thread when it needs to take a lock on the task
queue, to keep the thread lock-free most of the time.
This commit is contained in:
Timothy Flynn 2025-05-24 09:06:44 -04:00 committed by Tim Flynn
parent 172556db74
commit ad4634d0ed
Notes: github-actions[bot] 2025-05-26 09:24:05 +00:00

View file

@ -1,15 +1,16 @@
/* /*
* Copyright (c) 2023, Andrew Kaster <akaster@serenityos.org> * Copyright (c) 2023, Andrew Kaster <akaster@serenityos.org>
* Copyright (c) 2023, Tim Flynn <trflynn89@serenityos.org> * Copyright (c) 2023-2025, Tim Flynn <trflynn89@ladybird.org>
* *
* SPDX-License-Identifier: BSD-2-Clause * SPDX-License-Identifier: BSD-2-Clause
*/ */
#include "PlaybackStreamAudioUnit.h"
#include <AK/Atomic.h> #include <AK/Atomic.h>
#include <AK/SourceLocation.h> #include <AK/SourceLocation.h>
#include <LibCore/SharedCircularQueue.h> #include <AK/Vector.h>
#include <LibCore/ThreadedPromise.h> #include <LibCore/ThreadedPromise.h>
#include <LibMedia/Audio/PlaybackStreamAudioUnit.h>
#include <LibThreading/Mutex.h>
#include <AudioUnit/AudioUnit.h> #include <AudioUnit/AudioUnit.h>
@ -67,12 +68,9 @@ struct AudioTask {
class AudioState : public RefCounted<AudioState> { class AudioState : public RefCounted<AudioState> {
public: public:
using AudioTaskQueue = Core::SharedSingleProducerCircularQueue<AudioTask>;
static ErrorOr<NonnullRefPtr<AudioState>> create(AudioStreamBasicDescription description, PlaybackStream::AudioDataRequestCallback data_request_callback, OutputState initial_output_state) static ErrorOr<NonnullRefPtr<AudioState>> create(AudioStreamBasicDescription description, PlaybackStream::AudioDataRequestCallback data_request_callback, OutputState initial_output_state)
{ {
auto task_queue = TRY(AudioTaskQueue::create()); auto state = TRY(adopt_nonnull_ref_or_enomem(new (nothrow) AudioState(description, move(data_request_callback), initial_output_state)));
auto state = TRY(adopt_nonnull_ref_or_enomem(new (nothrow) AudioState(description, move(task_queue), move(data_request_callback), initial_output_state)));
AudioComponentDescription component_description; AudioComponentDescription component_description;
component_description.componentType = kAudioUnitType_Output; component_description.componentType = kAudioUnitType_Output;
@ -116,11 +114,11 @@ public:
AudioOutputUnitStop(m_audio_unit); AudioOutputUnitStop(m_audio_unit);
} }
ErrorOr<void> queue_task(AudioTask task) void queue_task(AudioTask task)
{ {
return m_task_queue.blocking_enqueue(move(task), []() { Threading::MutexLocker lock(m_task_queue_mutex);
usleep(10'000); m_task_queue.append(move(task));
}); m_task_queue_is_empty = false;
} }
AK::Duration last_sample_time() const AK::Duration last_sample_time() const
@ -129,14 +127,26 @@ public:
} }
private: private:
AudioState(AudioStreamBasicDescription description, AudioTaskQueue task_queue, PlaybackStream::AudioDataRequestCallback data_request_callback, OutputState initial_output_state) AudioState(AudioStreamBasicDescription description, PlaybackStream::AudioDataRequestCallback data_request_callback, OutputState initial_output_state)
: m_description(description) : m_description(description)
, m_task_queue(move(task_queue))
, m_paused(initial_output_state == OutputState::Playing ? Paused::No : Paused::Yes) , m_paused(initial_output_state == OutputState::Playing ? Paused::No : Paused::Yes)
, m_data_request_callback(move(data_request_callback)) , m_data_request_callback(move(data_request_callback))
{ {
} }
Optional<AudioTask> dequeue_task()
{
// OPTIMIZATION: We can avoid taking a lock in the audio decoder thread if there are no queued commands, which
// will be the case most of the time.
if (m_task_queue_is_empty.load())
return {};
Threading::MutexLocker lock(m_task_queue_mutex);
m_task_queue_is_empty = m_task_queue.size() == 1;
return m_task_queue.take_first();
}
static OSStatus on_audio_unit_buffer_request(void* user_data, AudioUnitRenderActionFlags*, AudioTimeStamp const* time_stamp, UInt32 element, UInt32 frames_to_render, AudioBufferList* output_buffer_list) static OSStatus on_audio_unit_buffer_request(void* user_data, AudioUnitRenderActionFlags*, AudioTimeStamp const* time_stamp, UInt32 element, UInt32 frames_to_render, AudioBufferList* output_buffer_list)
{ {
VERIFY(element == AUDIO_UNIT_OUTPUT_BUS); VERIFY(element == AUDIO_UNIT_OUTPUT_BUS);
@ -150,13 +160,10 @@ private:
auto last_sample_time = static_cast<i64>(sample_time_seconds * 1000.0); auto last_sample_time = static_cast<i64>(sample_time_seconds * 1000.0);
state.m_last_sample_time.store(last_sample_time); state.m_last_sample_time.store(last_sample_time);
if (auto result = state.m_task_queue.dequeue(); result.is_error()) { if (auto task = state.dequeue_task(); task.has_value()) {
VERIFY(result.error() == AudioTaskQueue::QueueStatus::Empty);
} else {
auto task = result.release_value();
OSStatus error = noErr; OSStatus error = noErr;
switch (task.type) { switch (task->type) {
case AudioTask::Type::Play: case AudioTask::Type::Play:
state.m_paused = Paused::No; state.m_paused = Paused::No;
break; break;
@ -171,15 +178,15 @@ private:
break; break;
case AudioTask::Type::Volume: case AudioTask::Type::Volume:
VERIFY(task.data.has_value()); VERIFY(task->data.has_value());
error = AudioUnitSetParameter(state.m_audio_unit, kHALOutputParam_Volume, kAudioUnitScope_Global, 0, static_cast<float>(*task.data), 0); error = AudioUnitSetParameter(state.m_audio_unit, kHALOutputParam_Volume, kAudioUnitScope_Global, 0, static_cast<float>(*task->data), 0);
break; break;
} }
if (error == noErr) if (error == noErr)
task.resolve(AK::Duration::from_milliseconds(last_sample_time)); task->resolve(AK::Duration::from_milliseconds(last_sample_time));
else else
task.reject(error); task->reject(error);
} }
Bytes output_buffer { Bytes output_buffer {
@ -203,7 +210,9 @@ private:
AudioComponentInstance m_audio_unit { nullptr }; AudioComponentInstance m_audio_unit { nullptr };
AudioStreamBasicDescription m_description {}; AudioStreamBasicDescription m_description {};
AudioTaskQueue m_task_queue; Threading::Mutex m_task_queue_mutex;
Vector<AudioTask, 4> m_task_queue;
Atomic<bool> m_task_queue_is_empty { true };
enum class Paused { enum class Paused {
Yes, Yes,
@ -251,10 +260,7 @@ void PlaybackStreamAudioUnit::set_underrun_callback(Function<void()>)
NonnullRefPtr<Core::ThreadedPromise<AK::Duration>> PlaybackStreamAudioUnit::resume() NonnullRefPtr<Core::ThreadedPromise<AK::Duration>> PlaybackStreamAudioUnit::resume()
{ {
auto promise = Core::ThreadedPromise<AK::Duration>::create(); auto promise = Core::ThreadedPromise<AK::Duration>::create();
AudioTask task { AudioTask::Type::Play, promise }; m_state->queue_task({ AudioTask::Type::Play, promise });
if (auto result = m_state->queue_task(move(task)); result.is_error())
promise->reject(result.release_error());
return promise; return promise;
} }
@ -262,10 +268,7 @@ NonnullRefPtr<Core::ThreadedPromise<AK::Duration>> PlaybackStreamAudioUnit::resu
NonnullRefPtr<Core::ThreadedPromise<void>> PlaybackStreamAudioUnit::drain_buffer_and_suspend() NonnullRefPtr<Core::ThreadedPromise<void>> PlaybackStreamAudioUnit::drain_buffer_and_suspend()
{ {
auto promise = Core::ThreadedPromise<void>::create(); auto promise = Core::ThreadedPromise<void>::create();
AudioTask task { AudioTask::Type::Pause, promise }; m_state->queue_task({ AudioTask::Type::Pause, promise });
if (auto result = m_state->queue_task(move(task)); result.is_error())
promise->reject(result.release_error());
return promise; return promise;
} }
@ -273,10 +276,7 @@ NonnullRefPtr<Core::ThreadedPromise<void>> PlaybackStreamAudioUnit::drain_buffer
NonnullRefPtr<Core::ThreadedPromise<void>> PlaybackStreamAudioUnit::discard_buffer_and_suspend() NonnullRefPtr<Core::ThreadedPromise<void>> PlaybackStreamAudioUnit::discard_buffer_and_suspend()
{ {
auto promise = Core::ThreadedPromise<void>::create(); auto promise = Core::ThreadedPromise<void>::create();
AudioTask task { AudioTask::Type::PauseAndDiscard, promise }; m_state->queue_task({ AudioTask::Type::PauseAndDiscard, promise });
if (auto result = m_state->queue_task(move(task)); result.is_error())
promise->reject(result.release_error());
return promise; return promise;
} }
@ -289,10 +289,7 @@ ErrorOr<AK::Duration> PlaybackStreamAudioUnit::total_time_played()
NonnullRefPtr<Core::ThreadedPromise<void>> PlaybackStreamAudioUnit::set_volume(double volume) NonnullRefPtr<Core::ThreadedPromise<void>> PlaybackStreamAudioUnit::set_volume(double volume)
{ {
auto promise = Core::ThreadedPromise<void>::create(); auto promise = Core::ThreadedPromise<void>::create();
AudioTask task { AudioTask::Type::Volume, promise, volume }; m_state->queue_task({ AudioTask::Type::Volume, promise, volume });
if (auto result = m_state->queue_task(move(task)); result.is_error())
promise->reject(result.release_error());
return promise; return promise;
} }