First working version for pulse audio output

This commit is contained in:
emeric
2026-02-17 23:16:52 +01:00
parent e1279c84ed
commit 49c8eacdc5
11 changed files with 1129 additions and 28 deletions
+7
View File
@@ -1,5 +1,6 @@
pkg_check_modules(LIBAV IMPORTED_TARGET libavcodec libavutil libavformat libswresample)
pkg_check_modules(Taglib REQUIRED IMPORTED_TARGET taglib)
pkg_check_modules(PulseAudio REQUIRED IMPORTED_TARGET libpulse)
add_library(lmsaudio STATIC
impl/ffmpeg/AudioFile.cpp
@@ -11,12 +12,17 @@ add_library(lmsaudio STATIC
impl/ffmpeg/TagReader.cpp
impl/ffmpeg/Transcoder.cpp
impl/ffmpeg/Utils.cpp
impl/pulseaudio/AudioOutput.cpp
impl/pulseaudio/AudioOutputStream.cpp
impl/pulseaudio/Exception.cpp
impl/pulseaudio/MainLoopScopedLock.cpp
impl/taglib/AudioFileInfo.cpp
impl/taglib/AudioFileInfoParser.cpp
impl/taglib/ImageReader.cpp
impl/taglib/TagReader.cpp
impl/taglib/Utils.cpp
impl/AudioFileInfoParser.cpp
impl/PcmTypes.cpp
impl/TagReader.cpp
)
@@ -39,4 +45,5 @@ target_link_libraries(lmsaudio PUBLIC
target_link_libraries(lmsaudio PRIVATE
PkgConfig::LIBAV
PkgConfig::Taglib
PkgConfig::PulseAudio
)
@@ -0,0 +1,161 @@
/*
* Copyright (C) 2025 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include "AudioOutput.hpp"
#include <boost/asio/post.hpp>
#include <pulse/context.h>
#include <pulse/def.h>
#include <pulse/error.h>
#include <pulse/thread-mainloop.h>
#include "core/ILogger.hpp"
#include "core/LiteralString.hpp"
#include "audio/PcmTypes.hpp"
#include "AudioOutputStream.hpp"
#include "Exception.hpp"
#include "MainLoopScopedLock.hpp"
namespace lms::audio
{
std::unique_ptr<IAudioOutputContext> createAudioOutputContext(boost::asio::io_context& ioContext, std::string_view name)
{
return std::make_unique<pulseaudio::AudioOutputContext>(ioContext, name);
}
} // namespace lms::audio
namespace lms::audio::pulseaudio
{
namespace
{
core::LiteralString contextStateToString(pa_context_state_t state)
{
switch (state)
{
case PA_CONTEXT_UNCONNECTED: // The context hasn't been connected yet
return "Unconnected";
case PA_CONTEXT_CONNECTING: // A connection is being established
return "Connecting";
case PA_CONTEXT_AUTHORIZING: // The client is authorizing itself to the daemon
return "Authorizing";
case PA_CONTEXT_SETTING_NAME: // The client is passing its application name to the daemon
return "Setting Name";
case PA_CONTEXT_READY: // The connection is established, the context is ready to execute operations
return "Ready";
case PA_CONTEXT_FAILED: // The connection failed or was disconnected
return "Failed";
case PA_CONTEXT_TERMINATED: // The connection was terminated cleanly
return "Terminated";
}
return "Unknown";
}
} // namespace
void PaContextDeleter::operator()(pa_context* ctx) const noexcept
{
::pa_context_unref(ctx);
}
void PaThreadedMainLoopDeleter::operator()(pa_threaded_mainloop* mainloop) const noexcept
{
::pa_threaded_mainloop_free(mainloop);
}
AudioOutputContext::AudioOutputContext(boost::asio::io_context& ioContext, std::string_view name)
: _ioContext{ ioContext }
{
_mainLoop = PaThreadedMainLoopPtr{ ::pa_threaded_mainloop_new() };
if (!_mainLoop)
throw Exception{ "pa_mainloop_new failed" };
::pa_mainloop_api* mainloop_api{ ::pa_threaded_mainloop_get_api(_mainLoop.get()) };
_context = PaContextPtr{ ::pa_context_new(mainloop_api, std::string{ name }.c_str()) };
if (!_context)
throw Exception{ "pa_context_new failed" };
::pa_context_set_state_callback(_context.get(), [](pa_context*, void* userData) { static_cast<AudioOutputContext*>(userData)->onStateChanged(); }, this);
{
const int error{ ::pa_context_connect(_context.get(), nullptr, PA_CONTEXT_NOFLAGS, nullptr) };
if (error < 0)
throw PaException("pa_context_connect failed", error);
}
{
const int error{ ::pa_threaded_mainloop_start(_mainLoop.get()) };
if (error < 0)
throw PaException("pa_threaded_mainloop_start failed", error);
}
}
AudioOutputContext::~AudioOutputContext()
{
::pa_threaded_mainloop_stop(_mainLoop.get());
}
void AudioOutputContext::asyncWaitReady(WaitReadyCallback cb)
{
MainLoopScopedLock lock{ _mainLoop.get() };
if (pa_context_get_state(_context.get()) == PA_CONTEXT_READY)
{
boost::asio::post(_ioContext, std::move(cb));
}
else
{
_ioContext.get_executor().on_work_started();
_waitReadyCallbacks.push_back(std::move(cb));
}
}
std::unique_ptr<IAudioOutputStream> AudioOutputContext::createOutputStream(std::string_view name, const PcmParameters& outputParameters)
{
return std::make_unique<AudioOutputStream>(_ioContext, _context.get(), _mainLoop.get(), name, outputParameters);
}
void AudioOutputContext::onStateChanged()
{
const pa_context_state_t state{ pa_context_get_state(_context.get()) };
LMS_LOG(AUDIO, DEBUG, "Context state changed to '" << contextStateToString(state) << "'");
switch (state)
{
case PA_CONTEXT_READY:
assert(pa_threaded_mainloop_in_thread(_mainLoop.get()));
LMS_LOG(AUDIO, INFO, "Context connected to server '" << pa_context_get_server(_context.get()) << "'");
for (auto& callback : _waitReadyCallbacks)
{
boost::asio::post(_ioContext, std::move(callback));
// callback();
_ioContext.get_executor().on_work_finished();
}
_waitReadyCallbacks.clear();
break;
default:
break;
}
}
} // namespace lms::audio::pulseaudio
@@ -0,0 +1,64 @@
/*
* Copyright (C) 2025 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include "audio/IAudioOutput.hpp"
extern "C"
{
struct pa_context;
struct pa_threaded_mainloop;
}
namespace lms::audio::pulseaudio
{
struct PaContextDeleter
{
void operator()(pa_context* ctx) const noexcept;
};
using PaContextPtr = std::unique_ptr<pa_context, PaContextDeleter>;
struct PaThreadedMainLoopDeleter
{
void operator()(pa_threaded_mainloop* mainloop) const noexcept;
};
using PaThreadedMainLoopPtr = std::unique_ptr<pa_threaded_mainloop, PaThreadedMainLoopDeleter>;
class AudioOutputContext : public IAudioOutputContext
{
public:
AudioOutputContext(boost::asio::io_context& ioContext, std::string_view name);
~AudioOutputContext() override;
AudioOutputContext(const AudioOutputContext&) = delete;
AudioOutputContext& operator=(const AudioOutputContext&) = delete;
private:
void asyncWaitReady(WaitReadyCallback cb) override;
std::unique_ptr<IAudioOutputStream> createOutputStream(std::string_view name, const PcmParameters& outputParameters) override;
void onStateChanged();
boost::asio::io_context& _ioContext;
std::vector<WaitReadyCallback> _waitReadyCallbacks;
PaThreadedMainLoopPtr _mainLoop;
PaContextPtr _context;
};
} // namespace lms::audio::pulseaudio
@@ -0,0 +1,407 @@
/*
* Copyright (C) 2025 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include "AudioOutputStream.hpp"
#include <boost/asio/post.hpp>
#include <pulse/context.h>
#include <pulse/def.h>
#include <pulse/error.h>
#include <pulse/operation.h>
#include <pulse/proplist.h>
#include <pulse/stream.h>
#include <pulse/thread-mainloop.h>
#include "audio/Exception.hpp"
#include "core/ILogger.hpp"
#include "core/LiteralString.hpp"
#include "audio/PcmTypes.hpp"
#include "Exception.hpp"
#include "MainLoopScopedLock.hpp"
namespace lms::audio::pulseaudio
{
namespace
{
core::LiteralString streamStateToString(pa_stream_state_t state)
{
switch (state)
{
case PA_STREAM_UNCONNECTED: // The stream is not yet connected to any sink or
return "Unconnected";
case PA_STREAM_CREATING: // The stream is being created
return "Creating";
case PA_STREAM_READY: // The stream is established, you may pass audio data to it now
return "Ready";
case PA_STREAM_FAILED: // An error occurred that made the stream invalid
return "Failed";
case PA_STREAM_TERMINATED: // The stream has been terminated cleanly
return "Terminated";
}
return "Unknown";
}
::pa_sample_format toPaSampleFormat(PcmSampleType sampleType, std::endian byteOrder)
{
switch (sampleType)
{
case PcmSampleType::Signed16:
return byteOrder == std::endian::little ? PA_SAMPLE_S16LE : PA_SAMPLE_S16BE;
case PcmSampleType::Signed32:
return byteOrder == std::endian::little ? PA_SAMPLE_S32LE : PA_SAMPLE_S32BE;
case PcmSampleType::Float32:
return byteOrder == std::endian::little ? PA_SAMPLE_FLOAT32LE : PA_SAMPLE_FLOAT32BE;
case PcmSampleType::Float64:
throw Exception{ "Float64 sample not supported" };
}
throw Exception{ "Unexpected sample type!" };
}
} // namespace
void PaPropListDeleter::operator()(pa_proplist* proplist) const noexcept
{
::pa_proplist_free(proplist);
}
void PaStreamDeleter::operator()(pa_stream* stream) const noexcept
{
LMS_LOG(AUDIO, DEBUG, "Unref stream " << stream);
::pa_stream_unref(stream);
}
AudioOutputStream::AudioOutputStream(boost::asio::io_context& ioContext, pa_context* context, pa_threaded_mainloop* mainLoop, std::string_view name, const PcmParameters& outputParameters)
: _ioContext{ ioContext }
, _context{ context }
, _mainLoop{ mainLoop }
, _outputParameters{ outputParameters }
{
if (_outputParameters.planar)
throw Exception{ "Planar output format not supported" };
::pa_sample_spec specs;
specs.channels = _outputParameters.channelCount;
specs.format = toPaSampleFormat(_outputParameters.sampleType, _outputParameters.byteOrder);
specs.rate = _outputParameters.sampleRate;
LMS_LOG(AUDIO, DEBUG, "channels = " << (int)specs.channels << ", format = " << specs.format << ", rate = " << specs.rate);
PaPropListPtr props{ ::pa_proplist_new() };
if (::pa_proplist_sets(props.get(), PA_PROP_MEDIA_ROLE, "music") != 0)
throw Exception{ "pa_proplist_sets failed" };
_stream = PaStreamPtr{ pa_stream_new_with_proplist(_context, std::string{ name }.c_str(), &specs, nullptr, props.get()) };
if (!_stream)
throw PaException{ "pa_stream_new_with_proplist failed", ::pa_context_errno(_context) };
::pa_stream_set_state_callback(_stream.get(), [](pa_stream*, void* userdata) { static_cast<AudioOutputStream*>(userdata)->onStateChanged(); }, this);
::pa_stream_set_write_callback(_stream.get(), [](pa_stream*, std::size_t nbytes, void* userdata) { static_cast<AudioOutputStream*>(userdata)->onWriteRequested(nbytes); }, this);
::pa_stream_set_started_callback(_stream.get(), [](pa_stream*, void*) { LMS_LOG(AUDIO, DEBUG, "Stream started!"); }, nullptr);
::pa_stream_set_overflow_callback(_stream.get(), [](pa_stream*, void*) { LMS_LOG(AUDIO, DEBUG, "Stream overflow!"); }, nullptr);
::pa_stream_set_underflow_callback(_stream.get(), [](pa_stream*, void*) { LMS_LOG(AUDIO, WARNING, "Stream underflow!"); }, nullptr);
connect();
};
AudioOutputStream::~AudioOutputStream()
{
LMS_LOG(AUDIO, DEBUG, "~AudioOutputStream()");
// We don't want to be notified for termination as this holder class will be destroyed
::pa_stream_set_state_callback(_stream.get(), NULL, NULL);
}
const PcmParameters& AudioOutputStream::getParameters() const
{
return _outputParameters;
}
void AudioOutputStream::asyncWaitReady(WaitReadyCallback cb)
{
MainLoopScopedLock lock{ _mainLoop };
if (_waitReadyCallback)
throw Exception{ "asyncWaitReady already called!" };
if (::pa_stream_get_state(_stream.get()) == PA_STREAM_READY)
{
boost::asio::post(_ioContext, std::move(cb));
}
else
{
_ioContext.get_executor().on_work_started();
_waitReadyCallback = std::move(cb);
}
}
void AudioOutputStream::asyncWrite(std::span<const std::byte> buffer, WriteCompletionCallback cb)
{
if (buffer.size() == 0)
throw Exception{ "Empty buffer!" };
MainLoopScopedLock lock{ _mainLoop };
if (_drainRequested)
throw Exception{ "asyncDrain already called!" };
WriteOperation* operation{ acquireWriteOperation() };
operation->buffer = buffer;
operation->callback = std::move(cb);
_ioContext.get_executor().on_work_started();
_pendingWriteOperations.push_back(operation);
if (_pendingWriteOperations.size() == 1 && _ongoingWriteOperationCount == 0 && ::pa_stream_get_state(_stream.get()) == PA_STREAM_READY)
{
LMS_LOG(AUDIO, DEBUG, "Audio buffer shortage? immediate write!");
writeSome(pa_stream_writable_size(_stream.get()));
}
}
void AudioOutputStream::asyncDrain(DrainCompletionCallback cb)
{
LMS_LOG(AUDIO, DEBUG, "asyncDrain called...");
MainLoopScopedLock lock{ _mainLoop };
if (_drainRequested)
throw Exception{ "asyncDrain already called! " };
_ioContext.get_executor().on_work_started();
_drainRequested = true;
_drainCallback = std::move(cb);
if (_pendingWriteOperations.empty() && ::pa_stream_get_state(_stream.get()) == PA_STREAM_READY)
{
LMS_LOG(AUDIO, DEBUG, "audio buffer shortage? immediate drain");
drain();
}
}
void AudioOutputStream::pause()
{
LMS_LOG(AUDIO, DEBUG, "Pausing stream");
MainLoopScopedLock lock{ _mainLoop };
pa_operation* op{ ::pa_stream_cork(_stream.get(), 1, nullptr, nullptr) };
if (!op)
throw PaException("pa_stream_cork (pause) failed", pa_context_errno(_context));
::pa_operation_unref(op);
}
void AudioOutputStream::resume()
{
LMS_LOG(AUDIO, DEBUG, "Resuming stream");
MainLoopScopedLock lock{ _mainLoop };
{
pa_operation* op{ ::pa_stream_cork(_stream.get(), 0, nullptr, nullptr) };
if (!op)
throw PaException("pa_stream_cork (resume) failed", pa_context_errno(_context));
::pa_operation_unref(op);
}
{
pa_operation* op{ ::pa_stream_trigger(_stream.get(), NULL, NULL) };
if (!op)
throw PaException("pa_stream_trigger failed", pa_context_errno(_context));
::pa_operation_unref(op);
}
}
bool AudioOutputStream::isPaused() const
{
MainLoopScopedLock lock{ _mainLoop };
return pa_stream_is_corked(_stream.get());
}
std::chrono::microseconds AudioOutputStream::getPlaybackTime() const
{
pa_usec_t duration{};
if (::pa_stream_get_time(_stream.get(), &duration) == -PA_ERR_NODATA)
duration = 0;
return std::chrono::microseconds{ duration };
}
void AudioOutputStream::connect()
{
constexpr pa_stream_flags_t flags{ static_cast<pa_stream_flags_t>(
PA_STREAM_START_CORKED | PA_STREAM_INTERPOLATE_TIMING | PA_STREAM_AUTO_TIMING_UPDATE) };
const int error{ pa_stream_connect_playback(
_stream.get(), // The stream to connect to a sink
NULL, // Name of the sink to connect to, or NULL to let the server decide
NULL, // Buffering attributes, or NULL for default
flags, // Additional flags, or 0 for default
NULL, // Initial volume, or NULL for default
NULL // Synchronize this stream with the specified one, or NULL for a standalone stream */
) };
if (error != 0)
{
LMS_LOG(AUDIO, DEBUG, "pa_stream_connect_playback failed: " << pa_strerror(error));
throw PaException{ "pa_stream_connect_playback failed", error };
}
}
void AudioOutputStream::onStateChanged()
{
const pa_stream_state_t state{ pa_stream_get_state(_stream.get()) };
LMS_LOG(AUDIO, DEBUG, "Stream state changed to '" << streamStateToString(state) << "'");
switch (state)
{
case PA_STREAM_READY:
LMS_LOG(AUDIO, INFO, "Stream connected to device '" << pa_stream_get_device_name(_stream.get()) << "'");
if (_waitReadyCallback)
{
boost::asio::post(_ioContext, std::move(_waitReadyCallback));
_ioContext.get_executor().on_work_finished();
}
break;
default:
break;
}
}
void AudioOutputStream::onWriteRequested(std::size_t writableSize)
{
writeSome(writableSize);
if (_drainRequested && !_drainDone && _pendingWriteOperations.empty())
drain();
}
void AudioOutputStream::writeSome(std::size_t writableSize)
{
while (writableSize > 0 && !_pendingWriteOperations.empty())
{
WriteOperation* writeOperation{ _pendingWriteOperations.front() };
const std::span<const std::byte> buffer{ writeOperation->buffer };
const std::size_t byteCountToWrite{ std::min(writableSize, buffer.size()) };
pa_free_cb_t freeCallback{};
void* freeCallbackArg{ writeOperation };
if (byteCountToWrite == buffer.size())
{
_pendingWriteOperations.pop_front();
freeCallback = [](void* userdata) {
WriteOperation* operation{ static_cast<WriteOperation*>(userdata) };
operation->stream->onWriteOperationComplete(operation);
};
}
else
{
freeCallback = [](void* userdata) {
WriteOperation* operation{ static_cast<WriteOperation*>(userdata) };
operation->stream->onPartialWriteOperationComplete(operation);
};
writeOperation->buffer = std::span<const std::byte>(buffer.data() + byteCountToWrite, buffer.size() - byteCountToWrite);
}
LMS_LOG(AUDIO, DEBUG, "Operation ID " << writeOperation->id << ", writing " << byteCountToWrite << " bytes");
_ongoingWriteOperationCount++;
const int error{
::pa_stream_write_ext_free(_stream.get(), // The stream to use
buffer.data(), // The data to write
byteCountToWrite, // The length of the data to write in bytes
freeCallback, // A cleanup routine for the data
freeCallbackArg, // Argument passed to free_cb function
0, // Offset for seeking
PA_SEEK_RELATIVE) // Seek mode
};
if (error != 0)
throw PaException{ "pa_stream_write_ext_free failed", error };
writableSize -= byteCountToWrite;
}
}
void AudioOutputStream::drain()
{
assert(_ongoingWriteOperationCount == 0);
assert(_pendingWriteOperations.empty());
assert(!_drainDone);
LMS_LOG(AUDIO, DEBUG, "Draining stream...");
_drainDone = true;
::pa_operation* op{ ::pa_stream_drain(_stream.get(), [](pa_stream*, int success, void* userdata) { static_cast<AudioOutputStream*>(userdata)->onDrainComplete(success); }, this) };
if (!op)
throw PaException("pa_stream_drain failed", pa_context_errno(_context));
::pa_operation_unref(op);
}
void AudioOutputStream::onDrainComplete(bool success)
{
{
int error{ ::pa_stream_disconnect(_stream.get()) };
if (error != 0)
throw PaException{ "pa_stream_disconnect failed", error };
}
LMS_LOG(AUDIO, DEBUG, "AudioOutputStream::onDrainComplete, success = " << success << ", posting CB");
boost::asio::post(_ioContext, std::move(_drainCallback));
_ioContext.get_executor().on_work_finished();
}
AudioOutputStream::WriteOperation* AudioOutputStream::acquireWriteOperation()
{
if (_freeOperations.empty())
{
WriteOperation* operation{ &_operations.emplace_back() };
_freeOperations.push_back(operation);
}
WriteOperation* operation{ _freeOperations.back() };
_freeOperations.pop_back();
operation->id = _nextWriteOperationId++;
operation->stream = this;
return operation;
}
void AudioOutputStream::onWriteOperationComplete(WriteOperation* operation)
{
LMS_LOG(AUDIO, DEBUG, "Operation ID " << operation->id << ", onWriteOperationComplete");
assert(_ongoingWriteOperationCount > 0);
_ongoingWriteOperationCount -= 1;
boost::asio::post(_ioContext, std::move(operation->callback));
_freeOperations.push_back(operation);
_ioContext.get_executor().on_work_finished();
}
void AudioOutputStream::onPartialWriteOperationComplete(WriteOperation* operation)
{
LMS_LOG(AUDIO, DEBUG, "Operation ID " << operation->id << ", onPartialWriteOperationComplete");
assert(_ongoingWriteOperationCount > 0);
_ongoingWriteOperationCount -= 1;
}
} // namespace lms::audio::pulseaudio
@@ -0,0 +1,108 @@
/*
* Copyright (C) 2025 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <deque>
#include <list>
#include <vector>
#include "audio/IAudioOutput.hpp"
extern "C"
{
struct pa_context;
struct pa_proplist;
struct pa_stream;
struct pa_threaded_mainloop;
}
namespace lms::audio::pulseaudio
{
struct PaPropListDeleter
{
void operator()(pa_proplist* proplist) const noexcept;
};
using PaPropListPtr = std::unique_ptr<pa_proplist, PaPropListDeleter>;
struct PaStreamDeleter
{
void operator()(pa_stream* stream) const noexcept;
};
using PaStreamPtr = std::unique_ptr<pa_stream, PaStreamDeleter>;
class AudioOutputStream : public IAudioOutputStream
{
public:
AudioOutputStream(boost::asio::io_context& ioContext, pa_context* context, pa_threaded_mainloop* mainLoop, std::string_view name, const PcmParameters& outputParameters);
~AudioOutputStream() override;
AudioOutputStream(AudioOutputStream&) = delete;
AudioOutputStream& operator=(AudioOutputStream&) = delete;
private:
const PcmParameters& getParameters() const override;
void asyncWaitReady(WaitReadyCallback cb) override;
void asyncWrite(std::span<const std::byte> buffer, WriteCompletionCallback cb) override;
void asyncDrain(DrainCompletionCallback cb) override;
void pause() override;
void resume() override;
bool isPaused() const override;
std::chrono::microseconds getPlaybackTime() const override;
void connect();
void onStateChanged();
void onWriteRequested(std::size_t writableSize);
void writeSome(std::size_t writableSize);
void drain();
void onDrainComplete(bool success);
boost::asio::io_context& _ioContext;
pa_context* _context;
pa_threaded_mainloop* _mainLoop;
const PcmParameters& _outputParameters;
PaStreamPtr _stream;
WaitReadyCallback _waitReadyCallback;
using WriteOperationId = std::size_t;
struct WriteOperation
{
WriteOperationId id{};
AudioOutputStream* stream{};
std::span<const std::byte> buffer;
WriteCompletionCallback callback;
};
WriteOperationId _nextWriteOperationId{};
std::list<WriteOperation> _operations; // we want obj addresses to be stable
std::vector<WriteOperation*> _freeOperations;
std::deque<WriteOperation*> _pendingWriteOperations;
std::size_t _ongoingWriteOperationCount{};
WriteOperation* acquireWriteOperation();
void onWriteOperationComplete(WriteOperation* operation);
void onPartialWriteOperationComplete(WriteOperation* operation);
bool _drainRequested{};
bool _drainDone{};
DrainCompletionCallback _drainCallback;
};
} // namespace lms::audio::pulseaudio
@@ -0,0 +1,41 @@
/*
* Copyright (C) 2025 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the imp::pa_strerror(errorlied warranty of
, _error{error}
{}
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include "Exception.hpp"
#include <pulse/error.h>
namespace lms::audio::pulseaudio
{
PaException::PaException(std::string_view msg, int error)
: Exception{ std::string{ msg } + ": " + ::pa_strerror(error) }
, _error{ error }
{
}
int PaException::getError() const
{
return _error;
}
} // namespace lms::audio::pulseaudio
@@ -0,0 +1,36 @@
/*
* Copyright (C) 2025 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include <string_view>
#include "audio/Exception.hpp"
namespace lms::audio::pulseaudio
{
class PaException : public Exception
{
public:
PaException(std::string_view msg, int error);
int getError() const;
private:
int _error;
};
} // namespace lms::audio::pulseaudio
@@ -0,0 +1,37 @@
/*
* Copyright (C) 2025 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include "MainLoopScopedLock.hpp"
#include <pulse/thread-mainloop.h>
namespace lms::audio::pulseaudio
{
MainLoopScopedLock::MainLoopScopedLock(pa_threaded_mainloop* mainLoop)
: _mainLoop{ mainLoop }
{
pa_threaded_mainloop_lock(_mainLoop);
}
MainLoopScopedLock::~MainLoopScopedLock()
{
pa_threaded_mainloop_unlock(_mainLoop);
}
} // namespace lms::audio::pulseaudio
@@ -0,0 +1,42 @@
/*
* Copyright (C) 2025 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
extern "C"
{
struct pa_threaded_mainloop;
}
namespace lms::audio::pulseaudio
{
class [[nodiscard]] MainLoopScopedLock
{
public:
MainLoopScopedLock(pa_threaded_mainloop* mainLoop);
~MainLoopScopedLock();
MainLoopScopedLock(const MainLoopScopedLock&) = delete;
MainLoopScopedLock& operator=(const MainLoopScopedLock&) = delete;
private:
pa_threaded_mainloop* _mainLoop;
};
} // namespace lms::audio::pulseaudio
@@ -0,0 +1,72 @@
/*
* Copyright (C) 2025 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <chrono>
#include <functional>
#include <memory>
#include <string_view>
#include <boost/asio/io_context.hpp>
#include "audio/PcmTypes.hpp"
namespace lms::audio
{
class IAudioOutputStream
{
public:
virtual ~IAudioOutputStream() = default;
virtual const PcmParameters& getParameters() const = 0;
using WaitReadyCallback = std::function<void()>;
virtual void asyncWaitReady(WaitReadyCallback cb) = 0;
using WriteCompletionCallback = std::function<void()>;
// Do not touch the buffer until cb is called
virtual void asyncWrite(std::span<const std::byte> buffer, WriteCompletionCallback cb) = 0;
using DrainCompletionCallback = std::function<void()>;
virtual void asyncDrain(DrainCompletionCallback cb) = 0; // can be called only once
virtual std::chrono::microseconds getPlaybackTime() const = 0;
virtual void pause() = 0;
virtual void resume() = 0;
virtual bool isPaused() const = 0;
};
class IAudioOutputContext
{
public:
virtual ~IAudioOutputContext() = default;
using WaitReadyCallback = std::function<void()>;
virtual void asyncWaitReady(WaitReadyCallback cb) = 0;
// Must be called once output context is ready
// the created stream is in pause state
// planar format is not accepted!
[[nodiscard]] virtual std::unique_ptr<IAudioOutputStream> createOutputStream(std::string_view name, const PcmParameters& pcmParameters) = 0;
};
std::unique_ptr<IAudioOutputContext> createAudioOutputContext(boost::asio::io_context& ioContext, std::string_view name);
} // namespace lms::audio