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, Tim Flynn <trflynn89@serenityos.org>
* Copyright (c) 2023-2025, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include "PlaybackStreamAudioUnit.h"
#include <AK/Atomic.h>
#include <AK/SourceLocation.h>
#include <LibCore/SharedCircularQueue.h>
#include <AK/Vector.h>
#include <LibCore/ThreadedPromise.h>
#include <LibMedia/Audio/PlaybackStreamAudioUnit.h>
#include <LibThreading/Mutex.h>
#include <AudioUnit/AudioUnit.h>
@ -67,12 +68,9 @@ struct AudioTask {
class AudioState : public RefCounted<AudioState> {
public:
using AudioTaskQueue = Core::SharedSingleProducerCircularQueue<AudioTask>;
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(task_queue), move(data_request_callback), initial_output_state)));
auto state = TRY(adopt_nonnull_ref_or_enomem(new (nothrow) AudioState(description, move(data_request_callback), initial_output_state)));
AudioComponentDescription component_description;
component_description.componentType = kAudioUnitType_Output;
@ -116,11 +114,11 @@ public:
AudioOutputUnitStop(m_audio_unit);
}
ErrorOr<void> queue_task(AudioTask task)
void queue_task(AudioTask task)
{
return m_task_queue.blocking_enqueue(move(task), []() {
usleep(10'000);
});
Threading::MutexLocker lock(m_task_queue_mutex);
m_task_queue.append(move(task));
m_task_queue_is_empty = false;
}
AK::Duration last_sample_time() const
@ -129,14 +127,26 @@ public:
}
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_task_queue(move(task_queue))
, m_paused(initial_output_state == OutputState::Playing ? Paused::No : Paused::Yes)
, 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)
{
VERIFY(element == AUDIO_UNIT_OUTPUT_BUS);
@ -150,13 +160,10 @@ private:
auto last_sample_time = static_cast<i64>(sample_time_seconds * 1000.0);
state.m_last_sample_time.store(last_sample_time);
if (auto result = state.m_task_queue.dequeue(); result.is_error()) {
VERIFY(result.error() == AudioTaskQueue::QueueStatus::Empty);
} else {
auto task = result.release_value();
if (auto task = state.dequeue_task(); task.has_value()) {
OSStatus error = noErr;
switch (task.type) {
switch (task->type) {
case AudioTask::Type::Play:
state.m_paused = Paused::No;
break;
@ -171,15 +178,15 @@ private:
break;
case AudioTask::Type::Volume:
VERIFY(task.data.has_value());
error = AudioUnitSetParameter(state.m_audio_unit, kHALOutputParam_Volume, kAudioUnitScope_Global, 0, static_cast<float>(*task.data), 0);
VERIFY(task->data.has_value());
error = AudioUnitSetParameter(state.m_audio_unit, kHALOutputParam_Volume, kAudioUnitScope_Global, 0, static_cast<float>(*task->data), 0);
break;
}
if (error == noErr)
task.resolve(AK::Duration::from_milliseconds(last_sample_time));
task->resolve(AK::Duration::from_milliseconds(last_sample_time));
else
task.reject(error);
task->reject(error);
}
Bytes output_buffer {
@ -203,7 +210,9 @@ private:
AudioComponentInstance m_audio_unit { nullptr };
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 {
Yes,
@ -251,10 +260,7 @@ void PlaybackStreamAudioUnit::set_underrun_callback(Function<void()>)
NonnullRefPtr<Core::ThreadedPromise<AK::Duration>> PlaybackStreamAudioUnit::resume()
{
auto promise = Core::ThreadedPromise<AK::Duration>::create();
AudioTask task { AudioTask::Type::Play, promise };
if (auto result = m_state->queue_task(move(task)); result.is_error())
promise->reject(result.release_error());
m_state->queue_task({ AudioTask::Type::Play, promise });
return promise;
}
@ -262,10 +268,7 @@ NonnullRefPtr<Core::ThreadedPromise<AK::Duration>> PlaybackStreamAudioUnit::resu
NonnullRefPtr<Core::ThreadedPromise<void>> PlaybackStreamAudioUnit::drain_buffer_and_suspend()
{
auto promise = Core::ThreadedPromise<void>::create();
AudioTask task { AudioTask::Type::Pause, promise };
if (auto result = m_state->queue_task(move(task)); result.is_error())
promise->reject(result.release_error());
m_state->queue_task({ AudioTask::Type::Pause, promise });
return promise;
}
@ -273,10 +276,7 @@ NonnullRefPtr<Core::ThreadedPromise<void>> PlaybackStreamAudioUnit::drain_buffer
NonnullRefPtr<Core::ThreadedPromise<void>> PlaybackStreamAudioUnit::discard_buffer_and_suspend()
{
auto promise = Core::ThreadedPromise<void>::create();
AudioTask task { AudioTask::Type::PauseAndDiscard, promise };
if (auto result = m_state->queue_task(move(task)); result.is_error())
promise->reject(result.release_error());
m_state->queue_task({ AudioTask::Type::PauseAndDiscard, promise });
return promise;
}
@ -289,10 +289,7 @@ ErrorOr<AK::Duration> PlaybackStreamAudioUnit::total_time_played()
NonnullRefPtr<Core::ThreadedPromise<void>> PlaybackStreamAudioUnit::set_volume(double volume)
{
auto promise = Core::ThreadedPromise<void>::create();
AudioTask task { AudioTask::Type::Volume, promise, volume };
if (auto result = m_state->queue_task(move(task)); result.is_error())
promise->reject(result.release_error());
m_state->queue_task({ AudioTask::Type::Volume, promise, volume });
return promise;
}