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:
parent
172556db74
commit
ad4634d0ed
Notes:
github-actions[bot]
2025-05-26 09:24:05 +00:00
Author: https://github.com/trflynn89
Commit: ad4634d0ed
Pull-request: https://github.com/LadybirdBrowser/ladybird/pull/4873
1 changed files with 36 additions and 39 deletions
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue