Files
ladybird/Libraries/LibMedia/Audio/PulseAudioWrappers.h
Zaggy1024 c75284591f LibMedia: Make PlaybackStream use the output's sample specification
Instead of specifying the sample rate, channel count/map, etc. to the
PlaybackStream, we'll now use the output device's sample specification
whenever possible. If necessary, the stream will fall back to sane
default.

This hugely simplifies AudioMixingSink, since it no longer has to take
care of reinitializing the stream with a new sample specification when
it encounters a track with a higher sample rate or more channels. We
wouldn't be likely to benefit from this anyway, since it turns out that
at least Windows's virtual surround doesn't work through WASAPI at all,
and WASAPI likely wouldn't support downmixing.

This commit breaks playback of audio files that don't match the system
default audio device's sample rate and channel count. The next commit
introduces a converter into the pipeline to allow mixing of any sample
specification.
2025-12-13 08:58:26 +01:00

192 lines
5.8 KiB
C++

/*
* Copyright (c) 2023-2025, Gregory Bertilson <gregory@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include "Forward.h"
#include "PlaybackStream.h"
#include <AK/AtomicRefCounted.h>
#include <AK/Error.h>
#include <AK/NonnullRefPtr.h>
#include <AK/Time.h>
#include <LibMedia/Audio/SampleSpecification.h>
#include <LibMedia/Export.h>
#include <pulse/pulseaudio.h>
namespace Audio {
class PulseAudioStream;
enum class PulseAudioContextState {
Unconnected = PA_CONTEXT_UNCONNECTED,
Connecting = PA_CONTEXT_CONNECTING,
Authorizing = PA_CONTEXT_AUTHORIZING,
SettingName = PA_CONTEXT_SETTING_NAME,
Ready = PA_CONTEXT_READY,
Failed = PA_CONTEXT_FAILED,
Terminated = PA_CONTEXT_TERMINATED,
};
enum class PulseAudioErrorCode;
using PulseAudioDataRequestCallback = Function<ReadonlySpan<float>(PulseAudioStream&, Span<float> buffer)>;
// A wrapper around the PulseAudio main loop and context structs.
// Generally, only one instance of this should be needed for a single process.
class MEDIA_API PulseAudioContext
: public AtomicRefCounted<PulseAudioContext> {
public:
static ErrorOr<NonnullRefPtr<PulseAudioContext>> the();
static bool is_connected();
explicit PulseAudioContext(pa_threaded_mainloop*, pa_mainloop_api*, pa_context*);
PulseAudioContext(PulseAudioContext const& other) = delete;
~PulseAudioContext();
bool current_thread_is_main_loop_thread();
void lock_main_loop();
void unlock_main_loop();
[[nodiscard]] auto main_loop_locker()
{
lock_main_loop();
return ScopeGuard([this]() { unlock_main_loop(); });
}
// Waits for signal_to_wake() to be called.
// This must be called with the main loop locked.
void wait_for_signal();
// Signals to wake all threads from calls to signal_to_wake()
void signal_to_wake();
PulseAudioContextState get_connection_state();
bool connection_is_good();
PulseAudioErrorCode get_last_error();
void request_device_sample_specification();
ErrorOr<NonnullRefPtr<PulseAudioStream>> create_stream(OutputState, u32 target_latency_ms, PulseAudioDataRequestCallback);
private:
friend class PulseAudioStream;
PulseAudioContext*& nullable_instance();
pa_threaded_mainloop* m_main_loop { nullptr };
pa_mainloop_api* m_api { nullptr };
pa_context* m_context;
SampleSpecification m_device_sample_specification;
};
enum class PulseAudioStreamState {
Unconnected = PA_STREAM_UNCONNECTED,
Creating = PA_STREAM_CREATING,
Ready = PA_STREAM_READY,
Failed = PA_STREAM_FAILED,
Terminated = PA_STREAM_TERMINATED,
};
class PulseAudioStream : public AtomicRefCounted<PulseAudioStream> {
public:
static constexpr bool start_corked = true;
~PulseAudioStream();
PulseAudioStreamState get_connection_state();
bool connection_is_good();
// Sets the callback to be run when the server consumes more of the buffer than
// has been written yet.
void set_underrun_callback(Function<void()>);
SampleSpecification sample_specification();
u32 sample_rate();
size_t sample_size();
size_t frame_size();
u8 channel_count();
// Gets a data buffer that can be written to and then passed back to PulseAudio through
// the write() function. This avoids a copy vs directly calling write().
ErrorOr<Bytes> begin_write(size_t bytes_to_write = NumericLimits<size_t>::max());
// Writes a data buffer to the playback stream.
ErrorOr<void> write(ReadonlyBytes data);
// Cancels the previous begin_write() call.
ErrorOr<void> cancel_write();
bool is_suspended() const;
// Plays back all buffered data and corks the stream. Until resume() is called, no data
// will be written to the stream.
ErrorOr<void> drain_and_suspend();
// Drops all buffered data and corks the stream. Until resume() is called, no data will
// be written to the stream.
ErrorOr<void> flush_and_suspend();
// Uncorks the stream and forces data to be written to the buffers to force playback to
// resume as soon as possible.
ErrorOr<void> resume();
AK::Duration total_time_played() const;
ErrorOr<void> set_volume(double volume);
PulseAudioContext& context() { return *m_context; }
private:
friend class PulseAudioContext;
explicit PulseAudioStream(NonnullRefPtr<PulseAudioContext>&& context, pa_stream* stream);
PulseAudioStream(PulseAudioStream const& other) = delete;
ErrorOr<void> wait_for_operation(pa_operation*, StringView error_message);
void on_write_requested(size_t bytes_to_write);
NonnullRefPtr<PulseAudioContext> m_context;
pa_stream* m_stream { nullptr };
SampleSpecification m_sample_specification;
bool m_started_playback { false };
PulseAudioDataRequestCallback m_write_callback { nullptr };
// Determines whether we will allow the write callback to run. This should only be true
// if the stream is becoming or is already corked.
bool m_suspended { false };
Function<void()> m_underrun_callback;
};
enum class PulseAudioErrorCode {
OK = 0,
AccessFailure,
UnknownCommand,
InvalidArgument,
EntityExists,
NoSuchEntity,
ConnectionRefused,
ProtocolError,
Timeout,
NoAuthenticationKey,
InternalError,
ConnectionTerminated,
EntityKilled,
InvalidServer,
NoduleInitFailed,
BadState,
NoData,
IncompatibleProtocolVersion,
DataTooLarge,
NotSupported,
Unknown,
NoExtension,
Obsolete,
NotImplemented,
CalledFromFork,
IOError,
Busy,
Sentinel
};
StringView pulse_audio_error_to_string(PulseAudioErrorCode code);
ErrorOr<Audio::ChannelMap> pulse_audio_channel_map_to_channel_map(pa_channel_map const&);
ErrorOr<pa_channel_map> channel_map_to_pulse_audio_channel_map(Audio::ChannelMap const&);
}