First working PCM decoder

This commit is contained in:
emeric
2026-02-17 23:15:43 +01:00
parent 275d3bb967
commit e0f1e4a1ec
15 changed files with 734 additions and 31 deletions
+289
View File
@@ -0,0 +1,289 @@
/*
* 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 "PcmDecoder.hpp"
#include <array>
extern "C"
{
#include <libavcodec/avcodec.h>
#include <libavcodec/packet.h>
#include <libavformat/avformat.h>
#include <libavutil/channel_layout.h>
#include <libavutil/samplefmt.h>
#include <libswresample/swresample.h>
}
#include "core/ILogger.hpp"
#include "audio/Exception.hpp"
#include "audio/IPcmDecoder.hpp"
#include "Exception.hpp"
namespace lms::audio
{
std::unique_ptr<IPcmDecoder> createPcmDecoder(const std::filesystem::path& filePath, const PcmDecoderParameters& parameters)
{
return std::make_unique<ffmpeg::PcmDecoder>(filePath, parameters);
}
} // namespace lms::audio
namespace lms::audio::ffmpeg
{
namespace
{
::AVSampleFormat toAvSampleFormat(PcmDecodeSampleType type, bool planar)
{
switch (type)
{
case PcmDecodeSampleType::Signed16:
return planar ? AV_SAMPLE_FMT_S16P : AV_SAMPLE_FMT_S16;
case PcmDecodeSampleType::Signed32:
return planar ? AV_SAMPLE_FMT_S32P : AV_SAMPLE_FMT_S32;
case PcmDecodeSampleType::Float32:
return planar ? AV_SAMPLE_FMT_FLTP : AV_SAMPLE_FMT_FLT;
case PcmDecodeSampleType::Float64:
return planar ? AV_SAMPLE_FMT_DBLP : AV_SAMPLE_FMT_DBL;
}
throw Exception("Unsupported PcmDecodeSampleType");
}
} // namespace
PcmDecoder::PcmDecoder(const std::filesystem::path& filePath, const PcmDecoderParameters& parameters)
: _parameters{ parameters }
{
if (_parameters.channelCount > AV_NUM_DATA_POINTERS)
throw Exception("Channel count exceeds maximum supported channels");
{
::AVFormatContext* context{};
int error{ ::avformat_open_input(&context, filePath.c_str(), nullptr, nullptr) };
if (error < 0)
{
LMS_LOG(AUDIO, ERROR, "Cannot open " << filePath << ": " << utils::averrorToString(error));
throw FFmpegException{ "Cannot open '" + filePath.string() + "'", error };
}
_context = AVFormatContextPtr{ context };
}
{
int error{ ::avformat_find_stream_info(_context.get(), nullptr) };
if (error < 0)
{
LMS_LOG(AUDIO, ERROR, "Cannot find stream information in " << filePath << ": " << utils::averrorToString(error));
throw FFmpegException{ "Cannot find stream information in '" + filePath.string() + "'", error };
}
}
const ::AVCodec* decoder{};
_inputStreamIndex = ::av_find_best_stream(_context.get(),
AVMEDIA_TYPE_AUDIO,
-1, // auto
-1, // auto
&decoder,
0);
if (_inputStreamIndex < 0)
{
LMS_LOG(AUDIO, ERROR, "Cannot find best audio stream in " << filePath << ": " << utils::averrorToString(_inputStreamIndex));
throw FFmpegException{ "Cannot find best audio stream in '" + filePath.string() + "'", _inputStreamIndex };
}
_decoderContext = AVCodecContextPtr{ ::avcodec_alloc_context3(decoder) };
if (!_decoderContext)
throw Exception{ "Cannot allocate decoder context" };
{
int error{ ::avcodec_parameters_to_context(_decoderContext.get(), _context->streams[_inputStreamIndex]->codecpar) };
if (error < 0)
throw FFmpegException{ "Cannot init decoder parameters", error };
}
{
int error{ ::avcodec_open2(_decoderContext.get(), decoder, nullptr) };
if (error < 0)
throw FFmpegException("Cannot open decoder", error);
}
_decodedFrame = AVFramePtr{ av_frame_alloc() };
if (!_decodedFrame)
throw Exception{ "Cannot allocate decoded frame" };
_inputPacket = AVPacketPtr{ ::av_packet_alloc() };
if (!_inputPacket)
throw Exception{ "Cannot allocate input packet" };
// Resampler
const ::AVSampleFormat outFmt{ toAvSampleFormat(_parameters.sampleType, _parameters.planar) };
AVChannelLayout outLayout;
::av_channel_layout_default(&outLayout, _parameters.channelCount);
{
::SwrContext* context{};
::swr_alloc_set_opts2(
&context, // existing context
&outLayout, // out layout
outFmt, // out format
static_cast<int>(_parameters.sampleRate), // out rate
&_decoderContext->ch_layout, // in layout
_decoderContext->sample_fmt, // in format
_decoderContext->sample_rate, // in rate
0, // log offset
nullptr);
::av_channel_layout_uninit(&outLayout);
if (!context)
throw Exception{ "Cannot allocate resampler context" };
_resampleContext = SwrContextPtr{ context };
}
{
int error{ ::swr_init(_resampleContext.get()) };
if (error < 0)
throw FFmpegException{ "Cannot initialize resampler", error };
}
}
PcmDecoder::~PcmDecoder() = default;
std::size_t PcmDecoder::readSamples(std::span<WritableBuffer> outputChannelBuffers, std::size_t maxSamplesPerChannel)
{
assert(outputChannelBuffers.size() <= AV_NUM_DATA_POINTERS);
if (_finished)
return 0;
if (_parameters.planar)
{
if (outputChannelBuffers.size() != _parameters.channelCount)
throw Exception{ "Expected " + std::to_string(_parameters.channelCount) + " buffers for planar output" };
}
else
{
if (outputChannelBuffers.size() != 1)
throw Exception{ "Expected a single buffer for interleaved output" };
}
std::array<uint8_t*, AV_NUM_DATA_POINTERS> outData{};
for (size_t i = 0; i < outputChannelBuffers.size(); ++i)
outData[i] = reinterpret_cast<uint8_t*>(outputChannelBuffers[i].data());
while (true)
{
if (!_eof)
feedDecoder();
// Try to receive a decoded frame
int recvErr{ ::avcodec_receive_frame(_decoderContext.get(), _decodedFrame.get()) };
if (recvErr == AVERROR(EAGAIN))
{
if (!_eof)
continue; // need more input
_draining = true;
}
else if (recvErr == AVERROR_EOF)
{
_draining = true;
}
else if (recvErr < 0)
{
throw FFmpegException{ "avcodec_receive_frame failed", recvErr };
}
else
{
// Resample decoded audio
const int outSampleCount{ ::swr_convert(
_resampleContext.get(),
outData.data(),
static_cast<int>(maxSamplesPerChannel),
(const uint8_t**)_decodedFrame->data,
_decodedFrame->nb_samples) };
::av_frame_unref(_decodedFrame.get());
if (outSampleCount < 0)
throw FFmpegException{ "swr_convert failed", outSampleCount };
if (outSampleCount > 0)
return static_cast<std::size_t>(outSampleCount);
continue; // Rare but legal: frame produced no output (delay accumulation)
}
// Drain resampler once decoder is drained
if (_draining)
{
const int outSampleCount = ::swr_convert(_resampleContext.get(),
outData.data(),
static_cast<int>(maxSamplesPerChannel),
nullptr,
0);
if (outSampleCount < 0)
throw FFmpegException{ "swr_convert (drain) failed", outSampleCount };
if (outSampleCount > 0)
return outSampleCount;
_finished = true;
return 0;
}
}
return 0;
}
bool PcmDecoder::finished() const
{
return _finished;
}
void PcmDecoder::feedDecoder()
{
assert(!_eof);
const int readError{ ::av_read_frame(_context.get(), _inputPacket.get()) };
if (readError == AVERROR_EOF)
{
_eof = true;
// flush decoder
::avcodec_send_packet(_decoderContext.get(), nullptr);
}
else if (readError < 0)
{
throw FFmpegException{ "av_read_frame failed", readError };
}
else
{
if (_inputPacket->stream_index == _inputStreamIndex)
{
const int sendError{ ::avcodec_send_packet(_decoderContext.get(), _inputPacket.get()) };
::av_packet_unref(_inputPacket.get());
if (sendError < 0)
throw FFmpegException{ "avcodec_send_packet failed", sendError };
}
else
::av_packet_unref(_inputPacket.get());
}
}
} // namespace lms::audio::ffmpeg