Simplified helper, recycle buffers across tracks

This commit is contained in:
emeric
2026-02-17 23:16:52 +01:00
parent 3963969cec
commit 4e10c63dff
6 changed files with 99 additions and 80 deletions
+38 -28
View File
@@ -21,26 +21,25 @@
#include <boost/asio/post.hpp> #include <boost/asio/post.hpp>
#include "audio/Exception.hpp"
#include "core/ILogger.hpp" #include "core/ILogger.hpp"
#include "audio/Exception.hpp"
#include "audio/IAudioOutput.hpp" #include "audio/IAudioOutput.hpp"
#include "audio/IPcmDecoder.hpp" #include "audio/IPcmDecoder.hpp"
namespace lms::audio::utils namespace lms::audio::utils
{ {
std::shared_ptr<IPcmDecodeStreamer> createPcmDecodeStreamer(boost::asio::io_context& ioContext, const PcmDecodeStreamerParameters& parameters) std::unique_ptr<IPcmDecodeStreamer> createPcmDecodeStreamer(boost::asio::io_context& ioContext, const PcmDecodeStreamerParameters& parameters)
{ {
return std::make_shared<PcmDecodeStreamer>(ioContext, parameters); return std::make_unique<PcmDecodeStreamer>(ioContext, parameters);
} }
PcmDecodeStreamer::PcmDecodeStreamer(boost::asio::io_context& ioContext, const PcmDecodeStreamerParameters& parameters) PcmDecodeStreamer::PcmDecodeStreamer(boost::asio::io_context& ioContext, const PcmDecodeStreamerParameters& parameters)
: _ioContext{ ioContext } : _ioContext{ ioContext }
, _strand{ _ioContext } , _strand{ _ioContext }
, _outputStream{ parameters.outputStream } , _outputStream{ parameters.outputStream }
, _pcmDecoder{ audio::createPcmDecoder(parameters.file, parameters.offset, parameters.pcmParameters) }
{ {
prepareBuffers(); prepareBuffers(parameters.bufferCount, parameters.bufferDuration);
} }
PcmDecodeStreamer::~PcmDecodeStreamer() PcmDecodeStreamer::~PcmDecodeStreamer()
@@ -48,42 +47,51 @@ namespace lms::audio::utils
assert(!isWritePending()); assert(!isWritePending());
} }
void PcmDecodeStreamer::start(DecodeCompleteCallback cb) void PcmDecodeStreamer::start(const std::filesystem::path& path, std::chrono::microseconds offset, DecodeCompleteCallback cb)
{ {
assert(cb); assert(cb);
assert(!_decodeCompleteCallback); assert(isComplete()); // previous job must be finished or cancelled
_pcmDecoder = audio::createPcmDecoder(path, offset, getPcmParameters()); // may throw
_aborted = false;
_eofReached = false;
_ioContext.get_executor().on_work_started(); _ioContext.get_executor().on_work_started();
_decodeCompleteCallback = std::move(cb); _decodeCompleteCallback = std::move(cb);
boost::asio::post(_strand, [self = shared_from_this()] { boost::asio::post(_strand, [this] {
self->decodeSome(); decodeSome();
}); });
} }
void PcmDecodeStreamer::abort() void PcmDecodeStreamer::abort()
{ {
boost::asio::post(_strand, [self = shared_from_this()] { if (isComplete())
LMS_LOG(AUDIO, DEBUG, "Processing abort"); return;
self->_aborted = true;
self->_outputStream.flush();
if (!self->isWritePending()) boost::asio::post(_strand, [this] {
self->notifyDecodeComplete(); LMS_LOG(AUDIO, DEBUG, "Processing abort");
_aborted = true;
_outputStream.flush();
if (!isWritePending())
notifyDecodeComplete();
}); });
} }
bool PcmDecodeStreamer::isComplete() const
{
return !_pcmDecoder;
}
const audio::PcmParameters& PcmDecodeStreamer::getPcmParameters() const const audio::PcmParameters& PcmDecodeStreamer::getPcmParameters() const
{ {
return _pcmDecoder->getParameters(); return _outputStream.getParameters();
} }
void PcmDecodeStreamer::prepareBuffers() void PcmDecodeStreamer::prepareBuffers(std::size_t bufferCount, std::chrono::microseconds bufferDuration)
{ {
constexpr std::chrono::milliseconds bufferDuration{ 100 }; const std::size_t sampleCountPerBuffer{ static_cast<std::size_t>(std::chrono::duration_cast<std::chrono::microseconds>(bufferDuration).count() * getPcmParameters().sampleRate / std::chrono::microseconds::period::den) };
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) }; const std::size_t bufferSize{ sampleCountToByteCount(sampleCountPerBuffer) };
_buffers.resize(bufferCount); _buffers.resize(bufferCount);
@@ -127,11 +135,11 @@ namespace lms::audio::utils
bufferDesc.isWritePending = true; bufferDesc.isWritePending = true;
buffer = { buffer.data(), sampleCountToByteCount(sampleCount) }; buffer = { buffer.data(), sampleCountToByteCount(sampleCount) };
_outputStream.asyncWrite(buffer, [self = shared_from_this(), bufferIndex] { _outputStream.asyncWrite(buffer, [this, bufferIndex] {
boost::asio::post(self->_strand, [self, bufferIndex] { self->onBufferWriteComplete(bufferIndex); }); boost::asio::post(_strand, [this, bufferIndex] { onBufferWriteComplete(bufferIndex); });
}); });
boost::asio::post(_strand, [self = shared_from_this()] { self->decodeSome(); }); boost::asio::post(_strand, [this] { decodeSome(); });
} }
} }
@@ -185,10 +193,12 @@ namespace lms::audio::utils
void PcmDecodeStreamer::notifyDecodeComplete() void PcmDecodeStreamer::notifyDecodeComplete()
{ {
boost::asio::post(_ioContext, [self = shared_from_this(), cb = std::move(_decodeCompleteCallback)] { LMS_LOG(AUDIO, DEBUG, "Decode complete notification");
LMS_LOG(AUDIO, DEBUG, "Decode complete notification");
cb(self->_aborted); boost::asio::post(_ioContext, [this, cb = std::move(_decodeCompleteCallback)] {
self->_ioContext.get_executor().on_work_finished(); _pcmDecoder.reset();
cb(_aborted);
_ioContext.get_executor().on_work_finished();
}); });
} }
@@ -19,14 +19,12 @@
#pragma once #pragma once
#include <filesystem> #include <cstddef>
#include <memory>
#include <vector> #include <vector>
#include <boost/asio/io_context.hpp> #include <boost/asio/io_context.hpp>
#include <boost/asio/io_context_strand.hpp> #include <boost/asio/io_context_strand.hpp>
#include "audio/PcmTypes.hpp"
#include "audio/utils/IPcmDecodeStreamer.hpp" #include "audio/utils/IPcmDecodeStreamer.hpp"
namespace lms::audio namespace lms::audio
@@ -37,7 +35,7 @@ namespace lms::audio
namespace lms::audio::utils namespace lms::audio::utils
{ {
class PcmDecodeStreamer : public IPcmDecodeStreamer, public std::enable_shared_from_this<PcmDecodeStreamer> class PcmDecodeStreamer : public IPcmDecodeStreamer
{ {
public: public:
PcmDecodeStreamer(boost::asio::io_context& ioContext, const PcmDecodeStreamerParameters& parameters); PcmDecodeStreamer(boost::asio::io_context& ioContext, const PcmDecodeStreamerParameters& parameters);
@@ -47,12 +45,13 @@ namespace lms::audio::utils
PcmDecodeStreamer& operator=(const PcmDecodeStreamer&) = delete; PcmDecodeStreamer& operator=(const PcmDecodeStreamer&) = delete;
private: private:
void start(DecodeCompleteCallback cb) override; void start(const std::filesystem::path& path, std::chrono::microseconds offset, DecodeCompleteCallback cb) override;
void abort() override; // will call DecodeCompleteCallback once done void abort() override;
bool isComplete() const override;
const audio::PcmParameters& getPcmParameters() const; const audio::PcmParameters& getPcmParameters() const;
void prepareBuffers(); void prepareBuffers(std::size_t bufferCount, std::chrono::microseconds bufferDuration);
bool isWritePending() const; bool isWritePending() const;
void decodeSome(); void decodeSome();
std::size_t readSamples(std::span<std::byte> buffer); std::size_t readSamples(std::span<std::byte> buffer);
@@ -72,7 +71,6 @@ namespace lms::audio::utils
audio::IAudioOutputStream& _outputStream; audio::IAudioOutputStream& _outputStream;
std::unique_ptr<audio::IPcmDecoder> _pcmDecoder; std::unique_ptr<audio::IPcmDecoder> _pcmDecoder;
static constexpr std::size_t bufferCount{ 4 };
std::vector<BufferDesc> _buffers; std::vector<BufferDesc> _buffers;
std::size_t _nextBufferIndex{}; std::size_t _nextBufferIndex{};
bool _eofReached{}; bool _eofReached{};
@@ -33,22 +33,26 @@ namespace lms::audio
namespace lms::audio::utils namespace lms::audio::utils
{ {
// helper class to decode files to PCM samples, fed into the provided output stream
class IPcmDecodeStreamer class IPcmDecodeStreamer
{ {
public: public:
virtual ~IPcmDecodeStreamer() = default; virtual ~IPcmDecodeStreamer() = default;
using DecodeCompleteCallback = std::function<void(bool aborted)>; using DecodeCompleteCallback = std::function<void(bool aborted)>;
virtual void start(DecodeCompleteCallback cb) = 0;
virtual void abort() = 0; // will call DecodeCompleteCallback once done // DecodeCompleteCallback is fired once the file has been fully decoded (but still buffered in output)
// You can start a new file only if the previous one is finished (i.e. once the callback is fired)
virtual void start(const std::filesystem::path& path, std::chrono::microseconds offset, DecodeCompleteCallback cb) = 0;
virtual void abort() = 0; // will call DecodeCompleteCallback once aborted
virtual bool isComplete() const = 0;
}; };
struct PcmDecodeStreamerParameters struct PcmDecodeStreamerParameters
{ {
audio::IAudioOutputStream& outputStream; audio::IAudioOutputStream& outputStream;
std::filesystem::path file; std::size_t bufferCount;
std::chrono::microseconds offset; std::chrono::milliseconds bufferDuration;
audio::PcmParameters pcmParameters;
}; };
std::shared_ptr<IPcmDecodeStreamer> createPcmDecodeStreamer(boost::asio::io_context& ioContext, const PcmDecodeStreamerParameters& params); std::unique_ptr<IPcmDecodeStreamer> createPcmDecodeStreamer(boost::asio::io_context& ioContext, const PcmDecodeStreamerParameters& parameters);
} // namespace lms::audio::utils } // namespace lms::audio::utils
@@ -20,7 +20,9 @@
#include "JukeboxService.hpp" #include "JukeboxService.hpp"
#include <chrono> #include <chrono>
#include <cstdlib>
#include <format> #include <format>
#include <thread>
#include <boost/asio/io_context.hpp> #include <boost/asio/io_context.hpp>
#include <boost/asio/post.hpp> #include <boost/asio/post.hpp>
@@ -59,9 +61,6 @@ namespace lms::jukebox
if (_decoder) if (_decoder)
_decoder->abort(); _decoder->abort();
if (_outputStream)
_outputStream->flush();
_ioContextRunner.wait(); _ioContextRunner.wait();
LMS_LOG(JUKEBOX, INFO, "Service stopped!"); LMS_LOG(JUKEBOX, INFO, "Service stopped!");
} }
@@ -72,7 +71,6 @@ namespace lms::jukebox
std::unique_lock lock{ _mutex }; std::unique_lock lock{ _mutex };
deleteDecoder();
if (trackIndex >= _tracks.size()) if (trackIndex >= _tracks.size())
{ {
LMS_LOG(JUKEBOX, INFO, "Requested track index out of bound: stopping"); LMS_LOG(JUKEBOX, INFO, "Requested track index out of bound: stopping");
@@ -80,12 +78,15 @@ namespace lms::jukebox
return; return;
} }
if (createDecoder(trackIndex, offset)) if (!_outputStream)
return;
abortDecoder();
if (startDecoder(trackIndex, offset))
{ {
_currentTrackIndex = trackIndex; _currentTrackIndex = trackIndex;
_currentTrackPlaybackTimeOffset = _outputStream->getPlaybackTime(); _currentTrackPlaybackTimeOffset = _outputStream->getPlaybackTime();
_currentTrackStartTimeOffset = offset; _currentTrackStartTimeOffset = offset;
startDecoder();
_outputStream->resume(); _outputStream->resume();
} }
// TODO if failure, switch to the next song? // TODO if failure, switch to the next song?
@@ -202,10 +203,20 @@ namespace lms::jukebox
void JukeboxService::onStreamReady() void JukeboxService::onStreamReady()
{ {
audio::utils::PcmDecodeStreamerParameters params{
.outputStream = *_outputStream,
.bufferCount = 50,
.bufferDuration = std::chrono::milliseconds{ 1000 },
};
_decoder = audio::utils::createPcmDecodeStreamer(_ioContext, params);
} }
bool JukeboxService::createDecoder(std::size_t trackIndex, std::chrono::microseconds offset) bool JukeboxService::startDecoder(std::size_t trackIndex, std::chrono::microseconds offset)
{ {
if (!_decoder)
return false;
std::filesystem::path trackPath; std::filesystem::path trackPath;
{ {
auto& session{ _db.getTLSSession() }; auto& session{ _db.getTLSSession() };
@@ -223,48 +234,42 @@ namespace lms::jukebox
try try
{ {
audio::utils::PcmDecodeStreamerParameters params{ _decoder->start(trackPath, offset, [this](bool aborted) {
.outputStream = *_outputStream, onDecodeFinished(aborted);
.file = trackPath, });
.offset = offset,
.pcmParameters = _pcmParams,
};
_decoder = audio::utils::createPcmDecodeStreamer(_ioContext, params);
} }
catch (const audio::Exception& e) catch (const audio::Exception& e)
{ {
LMS_LOG(JUKEBOX, ERROR, "Failed to create PCM decoder for track " << trackPath); LMS_LOG(JUKEBOX, ERROR, "Failed to start PCM decoder for track " << trackPath);
return false; return false;
} }
return true; return true;
} }
void JukeboxService::deleteDecoder() void JukeboxService::abortDecoder()
{ {
// Must not be called from within owned io_context
if (_decoder) if (_decoder)
{ {
_decoder->abort(); _decoder->abort();
_decoder.reset();
}
}
void JukeboxService::startDecoder() // Should be hopefully short since flushing/aborting
{ while (!_decoder->isComplete())
_decoder->start([this](bool aborted) { std::this_thread::yield(); // TODO: execute some io_context stuff?
onDecodeFinished(aborted); }
});
} }
void JukeboxService::onDecodeFinished(bool aborted) void JukeboxService::onDecodeFinished(bool aborted)
{ {
if (aborted) if (aborted)
return; // already setup to play next song return; // already setup to play next song, if needed
std::unique_lock lock{ _mutex }; std::unique_lock lock{ _mutex };
_decoder.reset(); _currentTrackPlaybackTimeOffset = {};
_currentTrackStartTimeOffset = {};
if (!_currentTrackIndex) if (!_currentTrackIndex)
{ {
_outputStream->pause(); _outputStream->pause();
@@ -274,15 +279,19 @@ namespace lms::jukebox
if (++(*_currentTrackIndex) >= _tracks.size()) if (++(*_currentTrackIndex) >= _tracks.size())
{ {
_currentTrackIndex.reset(); _currentTrackIndex.reset();
// let the output stream run out of data
return; return;
} }
if (createDecoder(*_currentTrackIndex)) if (startDecoder(*_currentTrackIndex))
{ {
startDecoder();
_currentTrackPlaybackTimeOffset = _outputStream->getPlaybackTime() + _outputStream->getLatency(); _currentTrackPlaybackTimeOffset = _outputStream->getPlaybackTime() + _outputStream->getLatency();
_currentTrackStartTimeOffset = {}; _currentTrackStartTimeOffset = {};
} }
else
{
_currentTrackIndex.reset();
}
// TODO if failure, switch to the next song? // TODO if failure, switch to the next song?
} }
} // namespace lms::jukebox } // namespace lms::jukebox
@@ -64,9 +64,8 @@ namespace lms::jukebox
void onContextReady(); void onContextReady();
void onStreamReady(); void onStreamReady();
bool createDecoder(std::size_t trackIndex, std::chrono::microseconds offset = {}); bool startDecoder(std::size_t trackIndex, std::chrono::microseconds offset = {});
void deleteDecoder(); void abortDecoder();
void startDecoder();
void onDecodeFinished(bool aborted); void onDecodeFinished(bool aborted);
// TODO: make configurable or use detected output params // TODO: make configurable or use detected output params
@@ -91,6 +90,6 @@ namespace lms::jukebox
std::unique_ptr<audio::IAudioOutputContext> _outputContext; std::unique_ptr<audio::IAudioOutputContext> _outputContext;
std::unique_ptr<audio::IAudioOutputStream> _outputStream; std::unique_ptr<audio::IAudioOutputStream> _outputStream;
std::shared_ptr<audio::utils::IPcmDecodeStreamer> _decoder; std::unique_ptr<audio::utils::IPcmDecodeStreamer> _decoder;
}; };
} // namespace lms::jukebox } // namespace lms::jukebox
+4 -5
View File
@@ -72,13 +72,12 @@ namespace lms
{ {
audio::utils::PcmDecodeStreamerParameters params{ audio::utils::PcmDecodeStreamerParameters params{
.outputStream = *_outputStream, .outputStream = *_outputStream,
.file = _filePath, .bufferCount = 2,
.offset = _offset, .bufferDuration = std::chrono::milliseconds{ 100 },
.pcmParameters = _pcmParams,
}; };
_fileStreamer = audio::utils::createPcmDecodeStreamer(_ioContext, params); _fileStreamer = audio::utils::createPcmDecodeStreamer(_ioContext, params);
_fileStreamer->start([this](bool aborted) { _fileStreamer->start(_filePath, _offset, [this](bool aborted) {
if (aborted) if (aborted)
std::cerr << "Playback aborted!" << std::endl; std::cerr << "Playback aborted!" << std::endl;
@@ -98,7 +97,7 @@ namespace lms
}); });
// Gives some time for the buffer to fill in // Gives some time for the buffer to fill in
_playTimer.expires_from_now(std::chrono::milliseconds{ 50 }); _playTimer.expires_after(std::chrono::milliseconds{ 50 });
_playTimer.async_wait([this](const boost::system::error_code& ec) { _playTimer.async_wait([this](const boost::system::error_code& ec) {
if (ec) if (ec)
return; return;