ladybird/Userland/Libraries/LibAudio/PulseAudioWrappers.h
Zaggy1024 50c73d02f0 LibAudio: Add a test for creating and destructing a PlaybackStream
This will ensure that we don't leak any memory while playing back
audio.

There is an expectation value in the test that is only set to true when
PulseAudio is present for the moment. When any new implementation is
added for other libraries/platforms, we should hopefully get a CI
failure due to unexpected success in creating the `PlaybackStream`.

To ensure that we clean up our PulseAudio connection whenever audio
output is not needed, add `PulseAudioContext::weak_instance()` to allow
us to check whether an instance exists without creating one.
2023-08-07 10:40:34 -06:00

186 lines
5.5 KiB
C++

/*
* Copyright (c) 2023, Gregory Bertilson <zaggy1024@gmail.com>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/AtomicRefCounted.h>
#include <AK/Error.h>
#include <AK/NonnullRefPtr.h>
#include <AK/Time.h>
#include <LibAudio/Forward.h>
#include <LibAudio/PlaybackStream.h>
#include <LibAudio/SampleFormats.h>
#include <LibThreading/Thread.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<ReadonlyBytes(PulseAudioStream&, Bytes buffer, size_t sample_count)>;
// A wrapper around the PulseAudio main loop and context structs.
// Generally, only one instance of this should be needed for a single process.
class PulseAudioContext
: public AtomicRefCounted<PulseAudioContext>
, public Weakable<PulseAudioContext> {
public:
static AK::WeakPtr<PulseAudioContext> weak_instance();
static ErrorOr<NonnullRefPtr<PulseAudioContext>> instance();
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();
ErrorOr<NonnullRefPtr<PulseAudioStream>> create_stream(OutputState initial_state, u32 sample_rate, u8 channels, u32 target_latency_ms, PulseAudioDataRequestCallback write_callback);
private:
friend class PulseAudioStream;
pa_threaded_mainloop* m_main_loop { nullptr };
pa_mainloop_api* m_api { nullptr };
pa_context* m_context;
};
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()>);
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();
ErrorOr<Duration> total_time_played();
ErrorOr<void> set_volume(double volume);
PulseAudioContext& context() { return *m_context; }
private:
friend class PulseAudioContext;
explicit PulseAudioStream(NonnullRefPtr<PulseAudioContext>&& context, pa_stream* stream)
: m_context(context)
, m_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 };
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);
}