ladybird/Libraries/LibMedia/Audio/PulseAudioWrappers.cpp
R-Goc 50be9493d2 LibSync: Abstract away Mutex implementation
This commit adds in-place pimpl to abstract away the implementation of
Mutex. It also adds policies to configure the type of mutex desired.

Because of the tight integration between mutex and condition variables
they also needed to be reworked and the changes have to be in one commit
to retain atomicity.

A win32 and pthread implemenation is provided to make sure the api
works with both.
2026-05-08 18:58:35 -05:00

664 lines
26 KiB
C++

/*
* Copyright (c) 2022-2025, Gregory Bertilson <gregory@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include "PulseAudioWrappers.h"
#include <LibMedia/Audio/SampleSpecification.h>
#include <LibSync/Mutex.h>
namespace Audio {
static PulseAudioContext* s_pulse_audio_context;
static Sync::RecursiveMutex s_pulse_audio_context_mutex;
ErrorOr<NonnullRefPtr<PulseAudioContext>> PulseAudioContext::the()
{
auto instantiation_locker = Sync::MutexLocker(s_pulse_audio_context_mutex);
// Lock and unlock the mutex to ensure that the mutex is fully unlocked at application
// exit.
static bool registered_atexit_callback = false;
if (!registered_atexit_callback) {
auto atexit_result = atexit([]() {
s_pulse_audio_context_mutex.lock();
s_pulse_audio_context_mutex.unlock();
});
if (atexit_result)
return Error::from_string_literal("Unable to set PulseAudioContext atexit action");
registered_atexit_callback = true;
}
RefPtr<PulseAudioContext> strong_instance_pointer = RefPtr<PulseAudioContext>(s_pulse_audio_context);
if (strong_instance_pointer == nullptr) {
auto* main_loop = pa_threaded_mainloop_new();
if (main_loop == nullptr)
return Error::from_string_literal("Failed to create PulseAudio main loop");
auto* api = pa_threaded_mainloop_get_api(main_loop);
if (api == nullptr)
return Error::from_string_literal("Failed to get PulseAudio API");
auto* context = pa_context_new(api, "Ladybird");
if (context == nullptr)
return Error::from_string_literal("Failed to get PulseAudio connection context");
strong_instance_pointer = make_ref_counted<PulseAudioContext>(main_loop, api, context);
// Set a callback to signal ourselves to wake when the state changes, so that we can
// synchronously wait for the connection.
pa_context_set_state_callback(
context, [](pa_context*, void* user_data) {
static_cast<PulseAudioContext*>(user_data)->signal_to_wake();
},
strong_instance_pointer.ptr());
if (auto error = pa_context_connect(context, nullptr, PA_CONTEXT_NOFLAGS, nullptr); error < 0) {
warnln("Starting PulseAudio context connection failed with error: {}", pulse_audio_error_to_string(static_cast<PulseAudioErrorCode>(-error)));
return Error::from_string_literal("Error while starting PulseAudio daemon connection");
}
if (auto error = pa_threaded_mainloop_start(main_loop); error < 0) {
warnln("Starting PulseAudio main loop failed with error: {}", pulse_audio_error_to_string(static_cast<PulseAudioErrorCode>(-error)));
return Error::from_string_literal("Failed to start PulseAudio main loop");
}
{
auto locker = strong_instance_pointer->main_loop_locker();
while (true) {
bool is_ready = false;
switch (strong_instance_pointer->get_connection_state()) {
case PulseAudioContextState::Connecting:
case PulseAudioContextState::Authorizing:
case PulseAudioContextState::SettingName:
break;
case PulseAudioContextState::Ready:
is_ready = true;
break;
case PulseAudioContextState::Failed:
warnln("PulseAudio server connection failed with error: {}", pulse_audio_error_to_string(strong_instance_pointer->get_last_error()));
return Error::from_string_literal("Failed to connect to PulseAudio server");
case PulseAudioContextState::Unconnected:
case PulseAudioContextState::Terminated:
VERIFY_NOT_REACHED();
break;
}
if (is_ready)
break;
strong_instance_pointer->wait_for_signal();
}
pa_context_set_state_callback(context, nullptr, nullptr);
strong_instance_pointer->request_device_sample_specification();
while (!strong_instance_pointer->m_device_sample_specification.is_valid() && strong_instance_pointer->connection_is_good())
strong_instance_pointer->wait_for_signal();
VERIFY(strong_instance_pointer->m_device_sample_specification.is_valid());
// FIXME: We should hook up a callback to update the device sample specification
// when it changes and somehow reinitialize any existing streams.
}
s_pulse_audio_context = strong_instance_pointer;
}
return strong_instance_pointer.release_nonnull();
}
bool PulseAudioContext::is_connected()
{
auto locker = Sync::MutexLocker(s_pulse_audio_context_mutex);
return s_pulse_audio_context != nullptr;
}
PulseAudioContext::PulseAudioContext(pa_threaded_mainloop* main_loop, pa_mainloop_api* api, pa_context* context)
: m_main_loop(main_loop)
, m_api(api)
, m_context(context)
{
}
PulseAudioContext::~PulseAudioContext()
{
auto locker = Sync::MutexLocker(s_pulse_audio_context_mutex);
{
auto loop_locker = main_loop_locker();
pa_context_disconnect(m_context);
pa_context_unref(m_context);
}
pa_threaded_mainloop_stop(m_main_loop);
pa_threaded_mainloop_free(m_main_loop);
s_pulse_audio_context = nullptr;
}
bool PulseAudioContext::current_thread_is_main_loop_thread()
{
return static_cast<bool>(pa_threaded_mainloop_in_thread(m_main_loop));
}
void PulseAudioContext::lock_main_loop()
{
if (!current_thread_is_main_loop_thread())
pa_threaded_mainloop_lock(m_main_loop);
}
void PulseAudioContext::unlock_main_loop()
{
if (!current_thread_is_main_loop_thread())
pa_threaded_mainloop_unlock(m_main_loop);
}
void PulseAudioContext::wait_for_signal()
{
pa_threaded_mainloop_wait(m_main_loop);
}
void PulseAudioContext::signal_to_wake()
{
pa_threaded_mainloop_signal(m_main_loop, 0);
}
PulseAudioContextState PulseAudioContext::get_connection_state()
{
return static_cast<PulseAudioContextState>(pa_context_get_state(m_context));
}
bool PulseAudioContext::connection_is_good()
{
return PA_CONTEXT_IS_GOOD(pa_context_get_state(m_context));
}
PulseAudioErrorCode PulseAudioContext::get_last_error()
{
return static_cast<PulseAudioErrorCode>(pa_context_errno(m_context));
}
void PulseAudioContext::request_device_sample_specification()
{
VERIFY(pa_context_get_state(m_context) == PA_CONTEXT_READY);
static constexpr auto set_default_sample_specification = [](PulseAudioContext& context) {
context.m_device_sample_specification = SampleSpecification(44100, ChannelMap::stereo());
context.signal_to_wake();
};
m_device_sample_specification = {};
auto* operation = pa_context_get_server_info(
m_context, [](pa_context*, pa_server_info const* info, void* user_data) {
auto& context = *reinterpret_cast<PulseAudioContext*>(user_data);
if (info->default_sink_name == nullptr) {
set_default_sample_specification(context);
return;
}
auto* operation = pa_context_get_sink_info_by_name(
context.m_context, info->default_sink_name, [](pa_context*, pa_sink_info const* info, int status, void* user_data) {
auto& context = *reinterpret_cast<PulseAudioContext*>(user_data);
if (status != 0) {
if (!context.m_device_sample_specification.is_valid()) {
warnln("PulseAudio was unable to find the default sink by name.");
set_default_sample_specification(context);
}
return;
}
auto channel_map_result = pulse_audio_channel_map_to_channel_map(info->channel_map);
if (channel_map_result.is_error()) {
warnln("Failed to convert the PulseAudio's default sink's channel map to ChannelMap.");
set_default_sample_specification(context);
return;
}
context.m_device_sample_specification = SampleSpecification(info->sample_spec.rate, channel_map_result.release_value());
context.signal_to_wake();
},
&context);
VERIFY(operation != nullptr);
pa_operation_unref(operation);
},
this);
VERIFY(operation != nullptr);
pa_operation_unref(operation);
}
#define STREAM_SIGNAL_CALLBACK(stream) \
[](auto*, int, void* user_data) { \
static_cast<PulseAudioStream*>(user_data)->m_context->signal_to_wake(); \
}, \
(stream)
ErrorOr<NonnullRefPtr<PulseAudioStream>> PulseAudioContext::create_stream(OutputState initial_state, u32 target_latency_ms, PulseAudioDataRequestCallback write_callback)
{
auto locker = main_loop_locker();
VERIFY(get_connection_state() == PulseAudioContextState::Ready);
pa_sample_spec sample_specification {
PA_SAMPLE_FLOAT32LE,
m_device_sample_specification.sample_rate(),
m_device_sample_specification.channel_map().channel_count(),
};
// Check the sample specification and channel map here. These are also checked by stream_new(),
// but we can return a more accurate error if we check beforehand.
if (pa_sample_spec_valid(&sample_specification) == 0)
return Error::from_string_literal("PulseAudio sample specification is invalid");
pa_channel_map pa_channel_map = TRY(channel_map_to_pulse_audio_channel_map(m_device_sample_specification.channel_map()));
if (!pa_channel_map_valid(&pa_channel_map)) {
warnln("Channel map is incompatible with PulseAudio: {}", m_device_sample_specification.channel_map());
return Error::from_string_literal("Channel map is incompatible with PulseAudio");
}
// Create the stream object and set a callback to signal ourselves to wake when the stream changes states,
// allowing us to wait synchronously for it to become Ready or Failed.
auto* stream = pa_stream_new_with_proplist(m_context, "Audio Stream", &sample_specification, &pa_channel_map, nullptr);
if (stream == nullptr) {
warnln("Instantiating PulseAudio stream failed with error: {}", pulse_audio_error_to_string(get_last_error()));
return Error::from_string_literal("Failed to create PulseAudio stream");
}
pa_stream_set_state_callback(
stream, [](pa_stream*, void* user_data) {
static_cast<PulseAudioContext*>(user_data)->signal_to_wake();
},
this);
auto stream_wrapper = TRY(adopt_nonnull_ref_or_enomem(new (nothrow) PulseAudioStream(*this, stream)));
stream_wrapper->m_write_callback = move(write_callback);
pa_stream_set_write_callback(
stream, [](pa_stream* stream, size_t bytes_to_write, void* user_data) {
auto& stream_wrapper = *static_cast<PulseAudioStream*>(user_data);
VERIFY(stream_wrapper.m_stream == stream);
stream_wrapper.on_write_requested(bytes_to_write);
},
stream_wrapper.ptr());
// Borrowing logic from cubeb to set reasonable buffer sizes for a target latency:
// https://searchfox.org/mozilla-central/rev/3b707c8fd7e978eebf24279ee51ccf07895cfbcb/third_party/rust/cubeb-sys/libcubeb/src/cubeb_pulse.c#910-927
pa_buffer_attr buffer_attributes;
buffer_attributes.maxlength = -1;
buffer_attributes.prebuf = -1;
u64 const target_latency_frames = target_latency_ms * sample_specification.rate / 1000u;
u64 const target_latency_bytes = target_latency_frames * pa_frame_size(&sample_specification);
buffer_attributes.tlength = static_cast<u32>(min<u64>(target_latency_bytes, NumericLimits<u32>::max()));
buffer_attributes.minreq = buffer_attributes.tlength / 4;
buffer_attributes.fragsize = buffer_attributes.minreq;
auto flags = static_cast<pa_stream_flags>(PA_STREAM_AUTO_TIMING_UPDATE | PA_STREAM_INTERPOLATE_TIMING | PA_STREAM_ADJUST_LATENCY | PA_STREAM_RELATIVE_VOLUME);
if (initial_state == OutputState::Suspended) {
stream_wrapper->m_suspended = true;
flags = static_cast<pa_stream_flags>(static_cast<u32>(flags) | PA_STREAM_START_CORKED);
}
// This is a workaround for an issue with starting the stream corked, see PulseAudioStream::total_time_played().
pa_stream_set_started_callback(
stream, [](pa_stream* stream, void* user_data) {
static_cast<PulseAudioStream*>(user_data)->m_started_playback = true;
pa_stream_set_started_callback(stream, nullptr, nullptr);
},
stream_wrapper.ptr());
pa_stream_set_underflow_callback(
stream, [](pa_stream*, void* user_data) {
auto& stream = *static_cast<PulseAudioStream*>(user_data);
if (stream.m_underrun_callback)
stream.m_underrun_callback();
},
stream_wrapper.ptr());
if (auto error = pa_stream_connect_playback(stream, nullptr, &buffer_attributes, flags, nullptr, nullptr); error != 0) {
warnln("Failed to start PulseAudio stream connection with error: {}", pulse_audio_error_to_string(static_cast<PulseAudioErrorCode>(error)));
return Error::from_string_literal("Error while connecting the PulseAudio stream");
}
while (true) {
bool is_ready = false;
switch (stream_wrapper->get_connection_state()) {
case PulseAudioStreamState::Creating:
break;
case PulseAudioStreamState::Ready:
is_ready = true;
break;
case PulseAudioStreamState::Failed:
warnln("PulseAudio stream connection failed with error: {}", pulse_audio_error_to_string(get_last_error()));
return Error::from_string_literal("Failed to connect to PulseAudio daemon");
case PulseAudioStreamState::Unconnected:
case PulseAudioStreamState::Terminated:
VERIFY_NOT_REACHED();
break;
}
if (is_ready)
break;
wait_for_signal();
}
pa_stream_set_state_callback(stream, nullptr, nullptr);
return stream_wrapper;
}
PulseAudioStream::PulseAudioStream(PulseAudioContext& context, pa_stream* stream)
: m_context(context)
, m_stream(stream)
{
}
PulseAudioStream::~PulseAudioStream()
{
auto locker = m_context->main_loop_locker();
pa_stream_set_write_callback(m_stream, nullptr, nullptr);
pa_stream_set_underflow_callback(m_stream, nullptr, nullptr);
pa_stream_set_started_callback(m_stream, nullptr, nullptr);
pa_stream_disconnect(m_stream);
pa_stream_unref(m_stream);
}
PulseAudioStreamState PulseAudioStream::get_connection_state()
{
return static_cast<PulseAudioStreamState>(pa_stream_get_state(m_stream));
}
bool PulseAudioStream::connection_is_good()
{
return PA_STREAM_IS_GOOD(pa_stream_get_state(m_stream));
}
void PulseAudioStream::set_underrun_callback(Function<void()> callback)
{
auto locker = m_context->main_loop_locker();
m_underrun_callback = move(callback);
}
SampleSpecification PulseAudioStream::sample_specification()
{
auto const* pa_sample_specification = pa_stream_get_sample_spec(m_stream);
auto const* pa_channel_map = pa_stream_get_channel_map(m_stream);
VERIFY(pa_sample_specification->format == PA_SAMPLE_FLOAT32LE);
auto channel_map = MUST(pulse_audio_channel_map_to_channel_map(*pa_channel_map));
return SampleSpecification(pa_sample_specification->rate, channel_map);
}
u32 PulseAudioStream::sample_rate()
{
return pa_stream_get_sample_spec(m_stream)->rate;
}
size_t PulseAudioStream::sample_size()
{
return pa_sample_size(pa_stream_get_sample_spec(m_stream));
}
size_t PulseAudioStream::frame_size()
{
return pa_frame_size(pa_stream_get_sample_spec(m_stream));
}
u8 PulseAudioStream::channel_count()
{
return pa_stream_get_sample_spec(m_stream)->channels;
}
void PulseAudioStream::on_write_requested(size_t bytes_to_write)
{
VERIFY(m_write_callback);
if (m_suspended)
return;
while (bytes_to_write > 0) {
auto buffer = begin_write(bytes_to_write).release_value_but_fixme_should_propagate_errors();
auto frame_size = this->frame_size();
VERIFY(buffer.size() % frame_size == 0);
auto written_buffer = m_write_callback(*this, buffer.reinterpret<float>()).reinterpret<u8 const>();
if (written_buffer.size() == 0) {
cancel_write().release_value_but_fixme_should_propagate_errors();
break;
}
bytes_to_write -= written_buffer.size();
write(written_buffer).release_value_but_fixme_should_propagate_errors();
}
}
ErrorOr<Bytes> PulseAudioStream::begin_write(size_t bytes_to_write)
{
void* data_pointer;
size_t data_size = bytes_to_write;
if (pa_stream_begin_write(m_stream, &data_pointer, &data_size) != 0 || data_pointer == nullptr)
return Error::from_string_literal("Failed to get the playback stream's write buffer from PulseAudio");
return Bytes { data_pointer, data_size };
}
ErrorOr<void> PulseAudioStream::write(ReadonlyBytes data)
{
if (pa_stream_write(m_stream, data.data(), data.size(), nullptr, 0, PA_SEEK_RELATIVE) != 0)
return Error::from_string_literal("Failed to write data to PulseAudio playback stream");
return {};
}
ErrorOr<void> PulseAudioStream::cancel_write()
{
if (pa_stream_cancel_write(m_stream) != 0)
return Error::from_string_literal("Failed to get the playback stream's write buffer from PulseAudio");
return {};
}
bool PulseAudioStream::is_suspended() const
{
return m_suspended;
}
StringView pulse_audio_error_to_string(PulseAudioErrorCode code)
{
if (code < PulseAudioErrorCode::OK || code >= PulseAudioErrorCode::Sentinel)
return "Unknown error code"sv;
char const* string = pa_strerror(static_cast<int>(code));
return StringView { string, strlen(string) };
}
ErrorOr<void> PulseAudioStream::wait_for_operation(pa_operation* operation, StringView error_message)
{
while (pa_operation_get_state(operation) == PA_OPERATION_RUNNING)
m_context->wait_for_signal();
if (!m_context->connection_is_good() || !this->connection_is_good()) {
auto pulse_audio_error_name = pulse_audio_error_to_string(m_context->get_last_error());
warnln("Encountered stream error: {}", pulse_audio_error_name);
return Error::from_string_view(error_message);
}
pa_operation_unref(operation);
return {};
}
ErrorOr<void> PulseAudioStream::drain_and_suspend()
{
auto locker = m_context->main_loop_locker();
if (m_suspended)
return {};
m_suspended = true;
if (pa_stream_is_corked(m_stream) > 0)
return {};
TRY(wait_for_operation(pa_stream_drain(m_stream, STREAM_SIGNAL_CALLBACK(this)), "Draining PulseAudio stream failed"sv));
TRY(wait_for_operation(pa_stream_cork(m_stream, 1, STREAM_SIGNAL_CALLBACK(this)), "Corking PulseAudio stream after drain failed"sv));
return {};
}
ErrorOr<void> PulseAudioStream::flush_and_suspend()
{
auto locker = m_context->main_loop_locker();
if (m_suspended)
return {};
m_suspended = true;
if (pa_stream_is_corked(m_stream) > 0)
return {};
TRY(wait_for_operation(pa_stream_flush(m_stream, STREAM_SIGNAL_CALLBACK(this)), "Flushing PulseAudio stream failed"sv));
TRY(wait_for_operation(pa_stream_cork(m_stream, 1, STREAM_SIGNAL_CALLBACK(this)), "Corking PulseAudio stream after flush failed"sv));
return {};
}
ErrorOr<void> PulseAudioStream::resume()
{
auto locker = m_context->main_loop_locker();
if (!m_suspended)
return {};
m_suspended = false;
TRY(wait_for_operation(pa_stream_cork(m_stream, 0, STREAM_SIGNAL_CALLBACK(this)), "Uncorking PulseAudio stream failed"sv));
// Defer a write to the playback buffer on the PulseAudio main loop. Otherwise, playback will not
// begin again, despite the fact that we uncorked.
// NOTE: We ref here and then unref in the callback so that this stream will not be deleted until
// it finishes.
ref();
pa_mainloop_api_once(
m_context->m_api, [](pa_mainloop_api*, void* user_data) {
auto& stream = *static_cast<PulseAudioStream*>(user_data);
// NOTE: writable_size() returns -1 in case of an error. However, the value is still safe
// since begin_write() will interpret -1 as a default parameter and choose a good size.
auto bytes_to_write = pa_stream_writable_size(stream.m_stream);
stream.on_write_requested(bytes_to_write);
stream.unref();
},
this);
return {};
}
AK::Duration PulseAudioStream::total_time_played() const
{
auto locker = m_context->main_loop_locker();
// NOTE: This is a workaround for a PulseAudio issue. When a stream is started corked,
// the time smoother doesn't seem to be aware of it, so it will return the time
// since the stream was connected. Once the playback actually starts, the time
// resets back to zero. However, since we request monotonically-increasing time,
// this means that the smoother will register that it had a larger time before,
// and return that time instead, until we reach a timestamp greater than the
// last-returned time. If we never call pa_stream_get_time() until after giving
// the stream its first samples, the issue never occurs.
if (!m_started_playback)
return AK::Duration::zero();
pa_usec_t time = 0;
auto error = pa_stream_get_time(m_stream, &time);
if (error)
return AK::Duration::zero();
if (error != 0) {
warnln("Unexpected error in pa_stream_get_time(): {}", error);
return AK::Duration::zero();
}
if (time > NumericLimits<i64>::max()) {
warnln("WARNING: Audio time is too large!");
time -= NumericLimits<i64>::max();
}
return AK::Duration::from_microseconds(static_cast<i64>(time));
}
ErrorOr<void> PulseAudioStream::set_volume(double volume)
{
auto locker = m_context->main_loop_locker();
auto index = pa_stream_get_index(m_stream);
if (index == PA_INVALID_INDEX)
return Error::from_string_literal("Failed to get PulseAudio stream index while setting volume");
auto pulse_volume = pa_sw_volume_from_linear(volume);
pa_cvolume per_channel_volumes;
pa_cvolume_set(&per_channel_volumes, channel_count(), pulse_volume);
auto* operation = pa_context_set_sink_input_volume(m_context->m_context, index, &per_channel_volumes, STREAM_SIGNAL_CALLBACK(this));
return wait_for_operation(operation, "Failed to set PulseAudio stream volume"sv);
}
#define ENUMERATE_CHANNEL_POSITIONS(C) \
C(Audio::Channel::FrontLeft, PA_CHANNEL_POSITION_FRONT_LEFT) \
C(Audio::Channel::FrontRight, PA_CHANNEL_POSITION_FRONT_RIGHT) \
C(Audio::Channel::FrontCenter, PA_CHANNEL_POSITION_FRONT_CENTER) \
C(Audio::Channel::LowFrequency, PA_CHANNEL_POSITION_LFE) \
C(Audio::Channel::BackLeft, PA_CHANNEL_POSITION_REAR_LEFT) \
C(Audio::Channel::BackRight, PA_CHANNEL_POSITION_REAR_RIGHT) \
C(Audio::Channel::FrontLeftOfCenter, PA_CHANNEL_POSITION_FRONT_LEFT_OF_CENTER) \
C(Audio::Channel::FrontRightOfCenter, PA_CHANNEL_POSITION_FRONT_RIGHT_OF_CENTER) \
C(Audio::Channel::BackCenter, PA_CHANNEL_POSITION_REAR_CENTER) \
C(Audio::Channel::SideLeft, PA_CHANNEL_POSITION_SIDE_LEFT) \
C(Audio::Channel::SideRight, PA_CHANNEL_POSITION_SIDE_RIGHT) \
C(Audio::Channel::TopCenter, PA_CHANNEL_POSITION_TOP_CENTER) \
C(Audio::Channel::TopFrontLeft, PA_CHANNEL_POSITION_TOP_FRONT_LEFT) \
C(Audio::Channel::TopFrontCenter, PA_CHANNEL_POSITION_TOP_FRONT_CENTER) \
C(Audio::Channel::TopFrontRight, PA_CHANNEL_POSITION_TOP_FRONT_RIGHT) \
C(Audio::Channel::TopBackLeft, PA_CHANNEL_POSITION_TOP_REAR_LEFT) \
C(Audio::Channel::TopBackCenter, PA_CHANNEL_POSITION_TOP_REAR_CENTER) \
C(Audio::Channel::TopBackRight, PA_CHANNEL_POSITION_TOP_REAR_RIGHT)
ErrorOr<Audio::ChannelMap> pulse_audio_channel_map_to_channel_map(pa_channel_map const& channel_map)
{
if (channel_map.channels <= 0)
return Error::from_string_literal("PulseAudio channel map had no channels");
if (static_cast<size_t>(channel_map.channels) > Audio::ChannelMap::capacity())
return Error::from_string_literal("PulseAudio channel map had too many channels");
Vector<Audio::Channel, Audio::ChannelMap::capacity()> channels;
channels.resize(channel_map.channels);
#define PA_CHANNEL_POSITION_TO_AUDIO_CHANNEL(audio_channel, pa_channel_position) \
case pa_channel_position: \
return audio_channel;
for (int i = 0; i < channel_map.channels; i++) {
auto channel = [&] {
switch (channel_map.map[i]) {
ENUMERATE_CHANNEL_POSITIONS(PA_CHANNEL_POSITION_TO_AUDIO_CHANNEL);
default:
return Audio::Channel::Unknown;
}
}();
channels[i] = channel;
}
return Audio::ChannelMap(channels);
}
ErrorOr<pa_channel_map> channel_map_to_pulse_audio_channel_map(Audio::ChannelMap const& channel_map)
{
static_assert(sizeof(pa_channel_map::map) >= PA_CHANNELS_MAX * sizeof(*pa_channel_map::map));
if (static_cast<size_t>(channel_map.channel_count()) > PA_CHANNELS_MAX)
return Error::from_string_literal("PulseAudio channel map had too many channels");
pa_channel_map map;
map.channels = channel_map.channel_count();
#define AUDIO_CHANNEL_TO_PA_CHANNEL_POSITION(audio_channel, pa_channel_position) \
case audio_channel: \
return pa_channel_position;
u32 i = 0;
while (i < channel_map.channel_count()) {
auto channel = [&] {
switch (channel_map.channel_at(i)) {
ENUMERATE_CHANNEL_POSITIONS(AUDIO_CHANNEL_TO_PA_CHANNEL_POSITION);
default:
return PA_CHANNEL_POSITION_INVALID;
}
}();
map.map[i++] = channel;
}
while (i < PA_CHANNELS_MAX)
map.map[i++] = PA_CHANNEL_POSITION_INVALID;
return map;
}
}