First working PCM decoder
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user