First working jukebox using Subsonic API

This commit is contained in:
emeric
2026-02-17 23:16:52 +01:00
parent c338521843
commit 3963969cec
39 changed files with 1464 additions and 277 deletions
+8 -17
View File
@@ -28,29 +28,20 @@
namespace lms::audio
{
consteval core::EnumSet<AudioOutputBackend> buildAudioOutputBackends()
{
core::EnumSet<AudioOutputBackend> res;
#if LMS_HAVE_ALSA
res.insert(AudioOutputBackend::ALSA);
#endif
#if LMS_HAVE_PULSEAUDIO
res.insert(AudioOutputBackend::PulseAudio);
#endif
return res;
}
core::EnumSet<AudioOutputBackend> getAudioOutputBackends()
{
return buildAudioOutputBackends();
}
std::unique_ptr<IAudioOutputContext> createAudioOutputContext([[maybe_unused]] boost::asio::io_context& ioContext, [[maybe_unused]] std::string_view name, AudioOutputBackend backend)
{
std::unique_ptr<IAudioOutputContext> context;
switch (backend)
{
case AudioOutputBackend::Auto:
#if LMS_HAVE_PULSEAUDIO
context = std::make_unique<pulseaudio::AudioOutputContext>(ioContext, name);
#elif LMS_HAVE_ALSA
context = std::make_unique<alsa::AudioOutputContext>(ioContext, name);
#endif
break;
case AudioOutputBackend::ALSA:
#if LMS_HAVE_ALSA
context = std::make_unique<alsa::AudioOutputContext>(ioContext, name);
+59 -8
View File
@@ -57,7 +57,10 @@ namespace lms::audio::alsa
void SndPcmDeleter::operator()(snd_pcm_t* pcm) const noexcept
{
const int error{ ::snd_pcm_close(pcm) };
LMS_LOG_IF(AUDIO, ERROR, error != 0, "snd_pcm_close failed: " << ::snd_strerror(error));
LMS_LOG_IF(AUDIO_OUTPUT_STREAM, ERROR, error != 0, "snd_pcm_close failed: " << ::snd_strerror(error));
// TODO move this
::snd_config_update_free_global();
}
class AlsaException : public Exception
@@ -106,7 +109,7 @@ namespace lms::audio::alsa
if (error < 0)
throw AlsaException{ "snd_pcm_hw_params_set_buffer_size failed", error };
LMS_LOG(AUDIO, DEBUG, ::snd_pcm_name(_pcm.get()) << ", buffer time set to " << bufferDuration << " mus");
LMS_LOG(AUDIO_OUTPUT_STREAM, DEBUG, ::snd_pcm_name(_pcm.get()) << ", buffer time set to " << bufferDuration << " mus");
}
{
@@ -117,7 +120,7 @@ namespace lms::audio::alsa
if (error < 0)
throw AlsaException{ "snd_pcm_hw_params_set_period_size failed", error };
LMS_LOG(AUDIO, DEBUG, ::snd_pcm_name(_pcm.get()) << ", period time set to " << periodDuration << " mus");
LMS_LOG(AUDIO_OUTPUT_STREAM, DEBUG, ::snd_pcm_name(_pcm.get()) << ", period time set to " << periodDuration << " mus");
}
const int error{ ::snd_pcm_hw_params(_pcm.get(), hw_params) };
@@ -242,7 +245,7 @@ namespace lms::audio::alsa
const int error{ ::snd_pcm_delay(_pcm.get(), &delayFrames) };
if (error != 0)
{
LMS_LOG(AUDIO, WARNING, "snd_pcm_delay failed: " << snd_strerror(error));
LMS_LOG(AUDIO_OUTPUT_STREAM, WARNING, "snd_pcm_delay failed: " << snd_strerror(error));
delayFrames = 0;
}
@@ -252,6 +255,54 @@ namespace lms::audio::alsa
return std::chrono::microseconds{ playedFrameCount * std::chrono::microseconds::period::den / _outputParameters.sampleRate };
}
std::chrono::microseconds AudioOutputStream::getLatency() const
{
::snd_pcm_sframes_t delayFrames{};
const int error{ ::snd_pcm_delay(_pcm.get(), &delayFrames) };
if (error != 0)
{
LMS_LOG(AUDIO_OUTPUT_STREAM, WARNING, "snd_pcm_delay failed: " << snd_strerror(error));
delayFrames = 0;
}
return std::chrono::microseconds{ delayFrames * std::chrono::microseconds::period::den / _outputParameters.sampleRate };
}
void AudioOutputStream::flush()
{
boost::asio::post(_strand, [this] {
assert(_strand.running_in_this_thread());
LMS_LOG(AUDIO_OUTPUT_STREAM, DEBUG, "Flushing output");
if (_drainRequested)
throw Exception{ "asyncDrain already called!" };
#if 0
{
const int error{ ::snd_pcm_drop(_pcm.get()) };
if (error < 0)
throw AlsaException{ "snd_pcm_drop failed", error };
}
{
const int error{ ::snd_pcm_prepare(_pcm.get()) };
if (error < 0)
throw AlsaException{ "snd_pcm_prepare failed", error };
}
#endif
while (!_operations.empty())
{
WriteOperation& operation{ _operations.front() };
boost::asio::post(_ioContext, std::move(operation.callback));
_ioContext.get_executor().on_work_finished();
_operations.pop_front();
}
});
}
void AudioOutputStream::stop()
{
releaseAllDescriptors();
@@ -298,7 +349,7 @@ namespace lms::audio::alsa
for (std::size_t i{}; i < _fileDescriptors.size(); ++i)
{
if (_fileDescriptors[i].events & (POLLOUT || POLLIN))
if (_fileDescriptors[i].events & (POLLOUT | POLLIN))
_streamDescriptors[i].cancel();
}
}
@@ -313,14 +364,14 @@ namespace lms::audio::alsa
if (ec)
{
LMS_LOG(AUDIO, ERROR, "Poll failed: " << ec);
LMS_LOG(AUDIO_OUTPUT_STREAM, ERROR, "Poll failed: " << ec);
throw Exception{ "poll failed: " + ec.message() };
}
unsigned short revents{};
::snd_pcm_poll_descriptors_revents(_pcm.get(), _fileDescriptors.data(), _fileDescriptors.size(), &revents);
if (revents & POLLERR)
LMS_LOG(AUDIO, ERROR, "ERROR");
LMS_LOG(AUDIO_OUTPUT_STREAM, ERROR, "snd_pcm_poll_descriptors_revents raied error!");
if (revents & POLLOUT)
writeSomeFrames();
@@ -360,7 +411,7 @@ namespace lms::audio::alsa
const ::snd_pcm_sframes_t writtenFrameCount{ ::snd_pcm_writei(_pcm.get(), operation.buffer.data(), frameCount) };
if (writtenFrameCount < 0)
{
LMS_LOG(AUDIO, WARNING, ::snd_pcm_name(_pcm.get()) << ", recovery needed! error = " << snd_strerror(writtenFrameCount) << ", pcm state = " << ::snd_pcm_state_name(::snd_pcm_state(_pcm.get())));
LMS_LOG(AUDIO_OUTPUT_STREAM, WARNING, ::snd_pcm_name(_pcm.get()) << ", recovery needed! error = " << snd_strerror(writtenFrameCount) << ", pcm state = " << ::snd_pcm_state_name(::snd_pcm_state(_pcm.get())));
const int error{ ::snd_pcm_recover(_pcm.get(), static_cast<int>(writtenFrameCount), 1) };
if (error)
throw AlsaException{ "Unrecoverable error", error };
@@ -57,6 +57,9 @@ namespace lms::audio::alsa
bool isPaused() const override;
std::chrono::microseconds getPlaybackTime() const override;
std::chrono::microseconds getLatency() const override;
void flush() override;
void stop();
void setupAllDescriptors();
+1 -52
View File
@@ -19,7 +19,6 @@
#include "AudioFile.hpp"
#include <array>
#include <cstdio>
#include <unordered_map>
@@ -27,7 +26,6 @@ extern "C"
{
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libavutil/log.h>
}
#include "core/ILogger.hpp"
@@ -174,54 +172,6 @@ namespace lms::audio::ffmpeg
return std::nullopt;
}
}
core::LiteralString avLogLevelToStr(int level)
{
switch (level)
{
case AV_LOG_TRACE:
return "trace";
case AV_LOG_DEBUG:
return "debug";
case AV_LOG_VERBOSE:
return "verbose";
case AV_LOG_INFO:
return "info";
case AV_LOG_WARNING:
return "warning";
case AV_LOG_ERROR:
return "error";
case AV_LOG_FATAL:
return "fatal";
case AV_LOG_PANIC:
return "panic";
default:
return "unknown";
}
}
void avLogCallback(void*, int level, const char* fmt, va_list vl)
{
if (!core::Service<core::logging::ILogger>::get()->isSeverityActive(core::logging::Severity::DEBUG))
return;
if (level > AV_LOG_WARNING)
return;
std::array<char, 256> buffer{ 0 };
std::vsnprintf(buffer.data(), buffer.size(), fmt, vl);
LMS_LOG(AUDIO, DEBUG, "FFmpeg [" << avLogLevelToStr(level) << "] " << buffer.data());
}
class AvInitializer
{
public:
AvInitializer()
{
::av_log_set_callback(avLogCallback);
}
};
} // namespace
AudioFile::AudioFile(const std::filesystem::path& p)
@@ -229,8 +179,7 @@ namespace lms::audio::ffmpeg
{
LMS_SCOPED_TRACE_DETAILED("MetaData", "FFmpegParseFile");
// TODO move this
static AvInitializer init;
utils::init();
int error{ avformat_open_input(&_context, _p.c_str(), nullptr, nullptr) };
if (error < 0)
+21 -3
View File
@@ -41,9 +41,9 @@ extern "C"
namespace lms::audio
{
std::unique_ptr<IPcmDecoder> createPcmDecoder(const std::filesystem::path& filePath, const PcmParameters& parameters)
std::unique_ptr<IPcmDecoder> createPcmDecoder(const std::filesystem::path& filePath, std::chrono::microseconds offset, const PcmParameters& parameters)
{
return std::make_unique<ffmpeg::PcmDecoder>(filePath, parameters);
return std::make_unique<ffmpeg::PcmDecoder>(filePath, offset, parameters);
}
} // namespace lms::audio
@@ -69,12 +69,15 @@ namespace lms::audio::ffmpeg
}
} // namespace
PcmDecoder::PcmDecoder(const std::filesystem::path& filePath, const PcmParameters& parameters)
PcmDecoder::PcmDecoder(const std::filesystem::path& filePath, std::chrono::microseconds offset, const PcmParameters& parameters)
: _parameters{ parameters }
{
if (_parameters.channelCount > AV_NUM_DATA_POINTERS)
throw Exception("Channel count exceeds maximum supported channels");
utils::init();
// TODO: use AudioFile wrapper?
{
::AVFormatContext* context{};
int error{ ::avformat_open_input(&context, filePath.c_str(), nullptr, nullptr) };
@@ -109,6 +112,21 @@ namespace lms::audio::ffmpeg
throw FFmpegException{ "Cannot find best audio stream in '" + filePath.string() + "'", _inputStreamIndex };
}
if (offset.count() > 0)
{
const AVStream* stream{ _context->streams[_inputStreamIndex] };
using OffsetPeriod = decltype(offset)::period;
constexpr AVRational offsetTimebase{ static_cast<int>(OffsetPeriod::num), static_cast<int>(OffsetPeriod::den) };
const int64_t targetTimestamp{ static_cast<int64_t>(av_rescale_q(offset.count(), offsetTimebase, stream->time_base)) };
const int seekError{ ::av_seek_frame(_context.get(), _inputStreamIndex, targetTimestamp, AVSEEK_FLAG_BACKWARD) };
if (seekError < 0)
{
LMS_LOG(AUDIO, WARNING, "Failed to seek to offset: " << utils::averrorToString(seekError));
}
}
_decoderContext = AVCodecContextPtr{ ::avcodec_alloc_context3(decoder) };
if (!_decoderContext)
throw Exception{ "Cannot allocate decoder context" };
+2 -2
View File
@@ -28,14 +28,14 @@ namespace lms::audio::ffmpeg
class PcmDecoder : public IPcmDecoder
{
public:
PcmDecoder(const std::filesystem::path& filePath, const PcmParameters& parameters);
PcmDecoder(const std::filesystem::path& filePath, std::chrono::microseconds offset, const PcmParameters& parameters);
~PcmDecoder() override;
PcmDecoder(const PcmDecoder&) = delete;
PcmDecoder& operator=(const PcmDecoder&) = delete;
private:
const PcmParameters& getParameters() const;
const PcmParameters& getParameters() const override;
std::size_t readSamples(std::span<WritableBuffer> outputChannelBuffers) override;
bool finished() const override;
+68
View File
@@ -18,14 +18,77 @@
*/
#include "Utils.hpp"
#include "core/String.hpp"
#include <array>
extern "C"
{
#include <libavutil/error.h>
#include <libavutil/log.h>
}
#include "core/ILogger.hpp"
#include "core/LiteralString.hpp"
namespace lms::audio::ffmpeg::utils
{
namespace
{
core::LiteralString avLogLevelToStr(int level)
{
switch (level)
{
case AV_LOG_TRACE:
return "trace";
case AV_LOG_DEBUG:
return "debug";
case AV_LOG_VERBOSE:
return "verbose";
case AV_LOG_INFO:
return "info";
case AV_LOG_WARNING:
return "warning";
case AV_LOG_ERROR:
return "error";
case AV_LOG_FATAL:
return "fatal";
case AV_LOG_PANIC:
return "panic";
default:
return "unknown";
}
}
void avLogCallback(void*, int level, const char* fmt, va_list vl)
{
if (!core::Service<core::logging::ILogger>::get()->isSeverityActive(core::logging::Severity::DEBUG))
return;
if (level > AV_LOG_WARNING)
return;
std::array<char, 256> buffer{ 0 };
if (std::vsnprintf(buffer.data(), buffer.size(), fmt, vl) > 0)
{
std::string_view str{ buffer.data() };
str = core::stringUtils::stringTrimEnd(str, " \t\r\n");
// TODO translate levels?
LMS_LOG(AUDIO, DEBUG, "[FFmpeg] [" << avLogLevelToStr(level) << "] " << str);
}
}
class AvInitializer
{
public:
AvInitializer()
{
::av_log_set_callback(avLogCallback);
}
};
} // namespace
std::string averrorToString(int error)
{
std::array<char, 128> buf{ 0 };
@@ -62,4 +125,9 @@ namespace lms::audio::ffmpeg::utils
};
return fileExtensions;
}
void init()
{
static AvInitializer init;
}
} // namespace lms::audio::ffmpeg::utils
+2
View File
@@ -27,4 +27,6 @@ namespace lms::audio::ffmpeg::utils
std::string averrorToString(int error);
std::span<const std::filesystem::path> getSupportedExtensions();
void init();
} // namespace lms::audio::ffmpeg::utils
@@ -84,7 +84,6 @@ namespace lms::audio::pulseaudio
void PaStreamDeleter::operator()(pa_stream* stream) const noexcept
{
LMS_LOG(AUDIO, DEBUG, "Unref stream " << stream);
::pa_stream_unref(stream);
}
@@ -102,7 +101,7 @@ namespace lms::audio::pulseaudio
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);
LMS_LOG(AUDIO_OUTPUT_STREAM, 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)
@@ -115,16 +114,18 @@ namespace lms::audio::pulseaudio
::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);
::pa_stream_set_started_callback(_stream.get(), [](pa_stream*, void*) { LMS_LOG(AUDIO_OUTPUT_STREAM, DEBUG, "Stream started!"); }, nullptr);
::pa_stream_set_overflow_callback(_stream.get(), [](pa_stream*, void*) { LMS_LOG(AUDIO_OUTPUT_STREAM, DEBUG, "Stream overflow!"); }, nullptr);
::pa_stream_set_underflow_callback(_stream.get(), [](pa_stream*, void*) { LMS_LOG(AUDIO_OUTPUT_STREAM, WARNING, "Stream underflow!"); }, nullptr);
connect();
};
AudioOutputStream::~AudioOutputStream()
{
LMS_LOG(AUDIO, DEBUG, "~AudioOutputStream()");
assert(_pendingWriteOperations.empty());
assert(_ongoingWriteOperationCount == 0);
// 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);
}
@@ -170,14 +171,14 @@ namespace lms::audio::pulseaudio
_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!");
LMS_LOG(AUDIO_OUTPUT_STREAM, DEBUG, "Audio buffer shortage? immediate write!");
writeSome(pa_stream_writable_size(_stream.get()));
}
}
void AudioOutputStream::asyncDrain(DrainCompletionCallback cb)
{
LMS_LOG(AUDIO, DEBUG, "asyncDrain called...");
LMS_LOG(AUDIO_OUTPUT_STREAM, DEBUG, "asyncDrain called...");
MainLoopScopedLock lock{ _mainLoop };
@@ -189,14 +190,14 @@ namespace lms::audio::pulseaudio
_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");
LMS_LOG(AUDIO_OUTPUT_STREAM, DEBUG, "audio buffer shortage? immediate drain");
drain();
}
}
void AudioOutputStream::pause()
{
LMS_LOG(AUDIO, DEBUG, "Pausing stream");
LMS_LOG(AUDIO_OUTPUT_STREAM, DEBUG, "Pausing stream");
MainLoopScopedLock lock{ _mainLoop };
@@ -209,7 +210,7 @@ namespace lms::audio::pulseaudio
void AudioOutputStream::resume()
{
LMS_LOG(AUDIO, DEBUG, "Resuming stream");
LMS_LOG(AUDIO_OUTPUT_STREAM, DEBUG, "Resuming stream");
MainLoopScopedLock lock{ _mainLoop };
@@ -244,6 +245,40 @@ namespace lms::audio::pulseaudio
return std::chrono::microseconds{ duration };
}
std::chrono::microseconds AudioOutputStream::getLatency() const
{
pa_usec_t latency{};
int negative{};
if (::pa_stream_get_latency(_stream.get(), &latency, &negative) == -PA_ERR_NODATA)
latency = 0;
return std::chrono::microseconds{ latency };
}
void AudioOutputStream::flush()
{
MainLoopScopedLock lock{ _mainLoop };
assert(_stream);
{
pa_operation* op{ ::pa_stream_flush(_stream.get(), nullptr, nullptr) };
if (!op)
throw PaException("pa_stream_flush failed", pa_context_errno(_context));
::pa_operation_unref(op);
}
// post all pending writes
while (!_pendingWriteOperations.empty())
{
WriteOperation* writeOperation{ _pendingWriteOperations.front() };
_pendingWriteOperations.pop_front();
onWriteOperationCancelled(writeOperation);
}
}
void AudioOutputStream::connect()
{
constexpr pa_stream_flags_t flags{ static_cast<pa_stream_flags_t>(
@@ -260,7 +295,7 @@ namespace lms::audio::pulseaudio
if (error != 0)
{
LMS_LOG(AUDIO, DEBUG, "pa_stream_connect_playback failed: " << pa_strerror(error));
LMS_LOG(AUDIO_OUTPUT_STREAM, DEBUG, "pa_stream_connect_playback failed: " << pa_strerror(error));
throw PaException{ "pa_stream_connect_playback failed", error };
}
}
@@ -268,12 +303,12 @@ namespace lms::audio::pulseaudio
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) << "'");
LMS_LOG(AUDIO_OUTPUT_STREAM, 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()) << "'");
LMS_LOG(AUDIO_OUTPUT_STREAM, INFO, "Stream connected to device '" << pa_stream_get_device_name(_stream.get()) << "'");
if (_waitReadyCallback)
{
@@ -323,7 +358,7 @@ namespace lms::audio::pulseaudio
writeOperation->buffer = std::span<const std::byte>(buffer.data() + byteCountToWrite, buffer.size() - byteCountToWrite);
}
LMS_LOG(AUDIO, DEBUG, "Operation ID " << writeOperation->id << ", writing " << byteCountToWrite << " bytes");
LMS_LOG(AUDIO_OUTPUT_STREAM, DEBUG, "Operation ID " << writeOperation->id << ", writing " << byteCountToWrite << " bytes");
_ongoingWriteOperationCount++;
const int error{
@@ -348,8 +383,12 @@ namespace lms::audio::pulseaudio
assert(_pendingWriteOperations.empty());
assert(!_drainDone);
LMS_LOG(AUDIO, DEBUG, "Draining stream...");
LMS_LOG(AUDIO_OUTPUT_STREAM, DEBUG, "Draining stream...");
_drainDone = true;
// We still receive underflow notifications while draining => discarding
::pa_stream_set_underflow_callback(_stream.get(), nullptr, nullptr);
::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));
@@ -365,7 +404,7 @@ namespace lms::audio::pulseaudio
throw PaException{ "pa_stream_disconnect failed", error };
}
LMS_LOG(AUDIO, DEBUG, "AudioOutputStream::onDrainComplete, success = " << success << ", posting CB");
LMS_LOG(AUDIO_OUTPUT_STREAM, DEBUG, "AudioOutputStream::onDrainComplete, success = " << success << ", posting CB");
boost::asio::post(_ioContext, std::move(_drainCallback));
_ioContext.get_executor().on_work_finished();
}
@@ -386,22 +425,38 @@ namespace lms::audio::pulseaudio
return operation;
}
void AudioOutputStream::releaseWriteOperation(WriteOperation* operation)
{
_freeOperations.push_back(operation);
}
void AudioOutputStream::onWriteOperationComplete(WriteOperation* operation)
{
LMS_LOG(AUDIO, DEBUG, "Operation ID " << operation->id << ", onWriteOperationComplete");
LMS_LOG(AUDIO_OUTPUT_STREAM, 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();
releaseWriteOperation(operation);
}
void AudioOutputStream::onPartialWriteOperationComplete(WriteOperation* operation)
{
LMS_LOG(AUDIO, DEBUG, "Operation ID " << operation->id << ", onPartialWriteOperationComplete");
LMS_LOG(AUDIO_OUTPUT_STREAM, DEBUG, "Operation ID " << operation->id << ", onPartialWriteOperationComplete");
assert(_ongoingWriteOperationCount > 0);
_ongoingWriteOperationCount -= 1;
}
void AudioOutputStream::onWriteOperationCancelled(WriteOperation* operation)
{
LMS_LOG(AUDIO_OUTPUT_STREAM, DEBUG, "Operation ID " << operation->id << ", onWriteOperationCancelled");
boost::asio::post(_ioContext, std::move(operation->callback));
_ioContext.get_executor().on_work_finished();
releaseWriteOperation(operation);
}
} // namespace lms::audio::pulseaudio
@@ -67,6 +67,9 @@ namespace lms::audio::pulseaudio
bool isPaused() const override;
std::chrono::microseconds getPlaybackTime() const override;
std::chrono::microseconds getLatency() const override;
void flush() override;
void connect();
void onStateChanged();
@@ -98,8 +101,11 @@ namespace lms::audio::pulseaudio
std::size_t _ongoingWriteOperationCount{};
WriteOperation* acquireWriteOperation();
void releaseWriteOperation(WriteOperation* operation);
void onWriteOperationComplete(WriteOperation* operation);
void onPartialWriteOperationComplete(WriteOperation* operation);
void onWriteOperationCancelled(WriteOperation* operation);
bool _drainRequested{};
bool _drainDone{};
+4 -4
View File
@@ -259,14 +259,14 @@ namespace lms::audio::taglib
AudioFileInfo::AudioFileInfo(const std::filesystem::path& filePath, const AudioFileInfoParseOptions& parseOptions)
: _filePath{ filePath }
, _file{ utils::parseFile(filePath, parseOptions.audioPropertiesReadStyle) }
, _audioProperties{ computeAudioProperties(*_file, filePath) }
, _fileDesc{ utils::parseFile(filePath, parseOptions.audioPropertiesReadStyle) }
, _audioProperties{ computeAudioProperties(*_fileDesc.file, filePath) }
{
if (parseOptions.readTags)
_tagReader = std::make_unique<TagReader>(*_file, parseOptions.enableExtraDebugLogs);
_tagReader = std::make_unique<TagReader>(*_fileDesc.file, parseOptions.enableExtraDebugLogs);
if (parseOptions.readImages)
_imageReader = std::make_unique<ImageReader>(*_file);
_imageReader = std::make_unique<ImageReader>(*_fileDesc.file);
}
AudioFileInfo::~AudioFileInfo() = default;
+2 -1
View File
@@ -22,6 +22,7 @@
#include <filesystem>
#include <optional>
#include "Utils.hpp"
#include "audio/AudioProperties.hpp"
#include "audio/IAudioFileInfo.hpp"
#include "audio/IAudioFileInfoParser.hpp"
@@ -53,7 +54,7 @@ namespace lms::audio::taglib
const ITagReader* getTagReader() const override;
const std::filesystem::path _filePath;
std::unique_ptr<::TagLib::File> _file;
utils::FileDesc _fileDesc;
std::optional<AudioProperties> _audioProperties;
std::unique_ptr<TagReader> _tagReader;
std::unique_ptr<ImageReader> _imageReader;
+9 -7
View File
@@ -104,7 +104,7 @@ namespace lms::audio::taglib::utils
throw Exception{ "Cannot convert read style" };
}
TagLib::FileStream createFileStream(const std::filesystem::path& p)
std::unique_ptr<TagLib::FileStream> createFileStream(const std::filesystem::path& p)
{
FILE* file{ std::fopen(p.c_str(), "r") };
if (!file)
@@ -122,7 +122,7 @@ namespace lms::audio::taglib::utils
throw IOFileException{ p, "fileno failed", ec };
}
return TagLib::FileStream{ fd, true };
return std::make_unique<TagLib::FileStream>(fd, true);
}
std::unique_ptr<TagLib::File> parseFileByExtension(TagLib::FileStream* stream, const std::filesystem::path& extension, TagLib::AudioProperties::ReadStyle audioPropertiesStyle)
@@ -236,17 +236,19 @@ namespace lms::audio::taglib::utils
return file;
}
std::unique_ptr<TagLib::File> parseFile(const std::filesystem::path& p, AudioFileInfoParseOptions::AudioPropertiesReadStyle readStyle)
FileDesc parseFile(const std::filesystem::path& p, AudioFileInfoParseOptions::AudioPropertiesReadStyle readStyle)
{
LMS_SCOPED_TRACE_DETAILED("MetaData", "TagLibParseFile");
const ::TagLib::AudioProperties::ReadStyle tagLibReadStyle{ readStyleToTagLibReadStyle(readStyle) };
TagLib::FileStream fileStream{ createFileStream(p) };
std::unique_ptr<TagLib::File> file{ parseFileByExtension(&fileStream, p.extension(), tagLibReadStyle) };
std::unique_ptr<TagLib::FileStream> fileStream{ createFileStream(p) };
assert(fileStream);
std::unique_ptr<TagLib::File> file{ parseFileByExtension(fileStream.get(), p.extension(), tagLibReadStyle) };
if (!file)
{
LMS_LOG(METADATA, DEBUG, "File " << p << ": failed to parse by extension");
file = parseFileByContent(&fileStream, tagLibReadStyle);
file = parseFileByContent(fileStream.get(), tagLibReadStyle);
if (!file)
LMS_LOG(METADATA, DEBUG, "File " << p << ": failed to parse by content");
}
@@ -254,6 +256,6 @@ namespace lms::audio::taglib::utils
if (!file)
throw Exception{ "Parsing failed" };
return file;
return FileDesc{ .fileStream = std::move(fileStream), .file = std::move(file) };
}
} // namespace lms::audio::taglib::utils
+8 -1
View File
@@ -24,11 +24,18 @@
#include <span>
#include <taglib/tfile.h>
#include <taglib/tfilestream.h>
#include "audio/IAudioFileInfoParser.hpp"
namespace lms::audio::taglib::utils
{
std::span<const std::filesystem::path> getSupportedExtensions();
std::unique_ptr<::TagLib::File> parseFile(const std::filesystem::path& p, AudioFileInfoParseOptions::AudioPropertiesReadStyle readStyle);
struct FileDesc
{
std::unique_ptr<::TagLib::FileStream> fileStream;
std::unique_ptr<::TagLib::File> file;
};
FileDesc parseFile(const std::filesystem::path& p, AudioFileInfoParseOptions::AudioPropertiesReadStyle readStyle);
} // namespace lms::audio::taglib::utils
@@ -0,0 +1,199 @@
/*
* Copyright (C) 2026 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 "PcmDecodeStreamer.hpp"
#include <boost/asio/post.hpp>
#include "audio/Exception.hpp"
#include "core/ILogger.hpp"
#include "audio/IAudioOutput.hpp"
#include "audio/IPcmDecoder.hpp"
namespace lms::audio::utils
{
std::shared_ptr<IPcmDecodeStreamer> createPcmDecodeStreamer(boost::asio::io_context& ioContext, const PcmDecodeStreamerParameters& parameters)
{
return std::make_shared<PcmDecodeStreamer>(ioContext, parameters);
}
PcmDecodeStreamer::PcmDecodeStreamer(boost::asio::io_context& ioContext, const PcmDecodeStreamerParameters& parameters)
: _ioContext{ ioContext }
, _strand{ _ioContext }
, _outputStream{ parameters.outputStream }
, _pcmDecoder{ audio::createPcmDecoder(parameters.file, parameters.offset, parameters.pcmParameters) }
{
prepareBuffers();
}
PcmDecodeStreamer::~PcmDecodeStreamer()
{
assert(!isWritePending());
}
void PcmDecodeStreamer::start(DecodeCompleteCallback cb)
{
assert(cb);
assert(!_decodeCompleteCallback);
_ioContext.get_executor().on_work_started();
_decodeCompleteCallback = std::move(cb);
boost::asio::post(_strand, [self = shared_from_this()] {
self->decodeSome();
});
}
void PcmDecodeStreamer::abort()
{
boost::asio::post(_strand, [self = shared_from_this()] {
LMS_LOG(AUDIO, DEBUG, "Processing abort");
self->_aborted = true;
self->_outputStream.flush();
if (!self->isWritePending())
self->notifyDecodeComplete();
});
}
const audio::PcmParameters& PcmDecodeStreamer::getPcmParameters() const
{
return _pcmDecoder->getParameters();
}
void PcmDecodeStreamer::prepareBuffers()
{
constexpr std::chrono::milliseconds bufferDuration{ 100 };
const audio::PcmParameters& pcmParams{ getPcmParameters() };
std::size_t sampleCountPerBuffer{ static_cast<std::size_t>(std::chrono::duration_cast<std::chrono::microseconds>(bufferDuration).count() * pcmParams.sampleRate / std::chrono::microseconds::period::den) };
const std::size_t bufferSize{ sampleCountToByteCount(sampleCountPerBuffer) };
_buffers.resize(bufferCount);
for (BufferDesc& bufferDesc : _buffers)
bufferDesc.buffer.resize(bufferSize);
}
bool PcmDecodeStreamer::isWritePending() const
{
return std::any_of(std::cbegin(_buffers), std::cend(_buffers), [](const BufferDesc& bufferDesc) {
return bufferDesc.isWritePending;
});
}
void PcmDecodeStreamer::decodeSome()
{
assert(_strand.running_in_this_thread());
while (!_eofReached && !_aborted)
{
BufferDesc& bufferDesc{ _buffers[_nextBufferIndex] };
if (bufferDesc.isWritePending)
break;
const std::size_t bufferIndex{ _nextBufferIndex };
if (++_nextBufferIndex >= _buffers.size())
_nextBufferIndex = 0;
std::span<std::byte> buffer{ bufferDesc.buffer };
const std::size_t sampleCount{ readSamples(buffer) };
if (sampleCount == 0) // EOF
{
LMS_LOG(AUDIO, DEBUG, "EOF reached");
_eofReached = true;
if (!isWritePending())
notifyDecodeComplete();
break;
}
bufferDesc.isWritePending = true;
buffer = { buffer.data(), sampleCountToByteCount(sampleCount) };
_outputStream.asyncWrite(buffer, [self = shared_from_this(), bufferIndex] {
boost::asio::post(self->_strand, [self, bufferIndex] { self->onBufferWriteComplete(bufferIndex); });
});
boost::asio::post(_strand, [self = shared_from_this()] { self->decodeSome(); });
}
}
std::size_t PcmDecodeStreamer::readSamples(std::span<std::byte> buffer)
{
assert(_strand.running_in_this_thread());
try
{
std::size_t totalSampleCount{};
// Buffer must be multiple of sample
assert(buffer.size() % (audio::getSampleSize(getPcmParameters().sampleType) * getPcmParameters().channelCount) == 0);
while (!buffer.empty())
{
std::array outputBuffers{ audio::IPcmDecoder::WritableBuffer{ buffer } };
const std::size_t sampleCount{ _pcmDecoder->readSamples(outputBuffers) };
if (sampleCount == 0)
break;
const std::size_t offset{ sampleCountToByteCount(sampleCount) };
buffer = std::span<std::byte>{ buffer.data() + offset, buffer.size() - offset };
totalSampleCount += sampleCount;
}
return totalSampleCount;
}
catch (const audio::Exception& e)
{
LMS_LOG(AUDIO, ERROR, "Failed to read pcm samples: " << e.what());
return 0;
}
}
void PcmDecodeStreamer::onBufferWriteComplete(std::size_t bufferIndex)
{
assert(_strand.running_in_this_thread());
BufferDesc& bufferDesc{ _buffers[bufferIndex] };
assert(bufferDesc.isWritePending);
bufferDesc.isWritePending = false;
if (!_aborted && !_eofReached)
decodeSome();
else if (!isWritePending())
notifyDecodeComplete();
}
void PcmDecodeStreamer::notifyDecodeComplete()
{
boost::asio::post(_ioContext, [self = shared_from_this(), cb = std::move(_decodeCompleteCallback)] {
LMS_LOG(AUDIO, DEBUG, "Decode complete notification");
cb(self->_aborted);
self->_ioContext.get_executor().on_work_finished();
});
}
std::size_t PcmDecodeStreamer::sampleCountToByteCount(std::size_t sampleCount) const
{
return sampleCount * audio::getSampleSize(getPcmParameters().sampleType) * getPcmParameters().channelCount;
}
} // namespace lms::audio::utils
@@ -0,0 +1,83 @@
/*
* Copyright (C) 2026 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 <filesystem>
#include <memory>
#include <vector>
#include <boost/asio/io_context.hpp>
#include <boost/asio/io_context_strand.hpp>
#include "audio/PcmTypes.hpp"
#include "audio/utils/IPcmDecodeStreamer.hpp"
namespace lms::audio
{
class IAudioOutputStream;
class IPcmDecoder;
} // namespace lms::audio
namespace lms::audio::utils
{
class PcmDecodeStreamer : public IPcmDecodeStreamer, public std::enable_shared_from_this<PcmDecodeStreamer>
{
public:
PcmDecodeStreamer(boost::asio::io_context& ioContext, const PcmDecodeStreamerParameters& parameters);
~PcmDecodeStreamer() override;
PcmDecodeStreamer(const PcmDecodeStreamer&) = delete;
PcmDecodeStreamer& operator=(const PcmDecodeStreamer&) = delete;
private:
void start(DecodeCompleteCallback cb) override;
void abort() override; // will call DecodeCompleteCallback once done
const audio::PcmParameters& getPcmParameters() const;
void prepareBuffers();
bool isWritePending() const;
void decodeSome();
std::size_t readSamples(std::span<std::byte> buffer);
void onBufferWriteComplete(std::size_t bufferIndex);
void notifyDecodeComplete();
std::size_t sampleCountToByteCount(std::size_t sampleCount) const;
struct BufferDesc
{
using Buffer = std::vector<std::byte>;
Buffer buffer;
bool isWritePending{};
};
boost::asio::io_context& _ioContext;
boost::asio::io_context::strand _strand;
audio::IAudioOutputStream& _outputStream;
std::unique_ptr<audio::IPcmDecoder> _pcmDecoder;
static constexpr std::size_t bufferCount{ 4 };
std::vector<BufferDesc> _buffers;
std::size_t _nextBufferIndex{};
bool _eofReached{};
bool _aborted{};
DecodeCompleteCallback _decodeCompleteCallback;
};
} // namespace lms::audio::utils