/* * Copyright (c) 2022-2025, Gregory Bertilson * * SPDX-License-Identifier: BSD-2-Clause */ #include "PulseAudioWrappers.h" #include #include namespace Audio { static PulseAudioContext* s_pulse_audio_context; static Threading::Mutex s_pulse_audio_context_mutex; ErrorOr> PulseAudioContext::the() { auto instantiation_locker = Threading::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 strong_instance_pointer = RefPtr(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(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(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(-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(-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 = Threading::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 = Threading::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(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(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(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(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(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(user_data)->m_context->signal_to_wake(); \ }, \ (stream) ErrorOr> 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(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(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(min(target_latency_bytes, NumericLimits::max())); buffer_attributes.minreq = buffer_attributes.tlength / 4; buffer_attributes.fragsize = buffer_attributes.minreq; auto flags = static_cast(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(static_cast(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(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(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(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(NonnullRefPtr&& 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(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 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()).reinterpret(); 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 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 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 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(code)); return StringView { string, strlen(string) }; } ErrorOr 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 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 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 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(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::max()) { warnln("WARNING: Audio time is too large!"); time -= NumericLimits::max(); } return AK::Duration::from_microseconds(static_cast(time)); } ErrorOr 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 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(channel_map.channels) > Audio::ChannelMap::capacity()) return Error::from_string_literal("PulseAudio channel map had too many channels"); Vector 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 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(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; } }