This gives us a cheap way to wake up all PlaybackStream implementations when they are sleeping due to an underrun. The new function is meant to be very cheap so that it can be unconditionally called when new data is ready to be written. This will allow the audio sink to rely on streams to track the time in written audio frames instead of writing silence when data isn't ready. The WASAPI implementation was actually polling for new data every 10ms after hitting an underrun, so now it drains the buffers and stops when entering the underrun-paused state.
203 lines
6 KiB
C++
203 lines
6 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/Atomic.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);
|
|
|
|
void notify_data_available();
|
|
|
|
PulseAudioContext& context() { return *m_context; }
|
|
|
|
private:
|
|
friend class PulseAudioContext;
|
|
|
|
explicit PulseAudioStream(PulseAudioContext&, pa_stream*);
|
|
PulseAudioStream(PulseAudioStream const&) = delete;
|
|
|
|
ErrorOr<void> wait_for_operation(pa_operation*, StringView error_message);
|
|
|
|
void on_write_requested(size_t bytes_to_write);
|
|
void queue_a_write_while_locked();
|
|
|
|
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 };
|
|
|
|
enum CallbackState : u8 {
|
|
Parked,
|
|
Active,
|
|
ActiveWithFutureData,
|
|
};
|
|
|
|
Atomic<CallbackState> m_callback_state { CallbackState::Parked };
|
|
|
|
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&);
|
|
|
|
}
|