From eca418d2bc8e6a2a033656f635bbcb2a9b78d661 Mon Sep 17 00:00:00 2001 From: emeric Date: Sun, 21 Jun 2026 13:35:50 +0200 Subject: [PATCH] Fixed crash with some corrupted files, ref #857 --- src/libs/audio/impl/ffmpeg/PcmDecoder.cpp | 167 +++++++++++++--------- src/libs/audio/impl/ffmpeg/PcmDecoder.hpp | 13 ++ 2 files changed, 116 insertions(+), 64 deletions(-) diff --git a/src/libs/audio/impl/ffmpeg/PcmDecoder.cpp b/src/libs/audio/impl/ffmpeg/PcmDecoder.cpp index 499db0e2..035bf12e 100644 --- a/src/libs/audio/impl/ffmpeg/PcmDecoder.cpp +++ b/src/libs/audio/impl/ffmpeg/PcmDecoder.cpp @@ -67,6 +67,37 @@ namespace lms::audio::ffmpeg throw Exception("Unsupported PcmSampleType"); } + + SwrContextPtr createResampler(const PcmParameters& params, const AVChannelLayout& inLayout, AVSampleFormat inFmt, int inSampleRate) + { + const ::AVSampleFormat outFmt{ toAvSampleFormat(params.sampleType, params.planar) }; + AVChannelLayout outLayout; + ::av_channel_layout_default(&outLayout, static_cast(params.channelCount)); + + ::SwrContext* context{}; + const int err{ ::swr_alloc_set_opts2( + &context, + &outLayout, + outFmt, + static_cast(params.sampleRate), + &inLayout, + inFmt, + inSampleRate, + 0, + nullptr) }; + ::av_channel_layout_uninit(&outLayout); + + if (err < 0 || !context) + throw FFmpegException{ "Cannot allocate resampler context", err }; + + SwrContextPtr resampleContext{ context }; + + const int initErr{ ::swr_init(resampleContext.get()) }; + if (initErr < 0) + throw FFmpegException{ "Cannot initialize resampler", initErr }; + + return resampleContext; + } } // namespace PcmDecoder::PcmDecoder(const std::filesystem::path& filePath, std::chrono::microseconds offset, const PcmParameters& parameters) @@ -159,36 +190,14 @@ namespace lms::audio::ffmpeg 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(_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 }; - } + _resampleContext = createResampler(_parameters, _decoderContext->ch_layout, _decoderContext->sample_fmt, _decoderContext->sample_rate); + _resamplerInputConfig = { + _decoderContext->sample_rate, + static_cast(_decoderContext->sample_fmt), + _decoderContext->ch_layout.nb_channels, + static_cast(_decoderContext->ch_layout.order), + _decoderContext->ch_layout.u.mask, + }; } PcmDecoder::~PcmDecoder() = default; @@ -232,34 +241,28 @@ namespace lms::audio::ffmpeg } else { - std::array outData{}; - for (std::size_t i{}; i < outputChannelBuffers.size(); ++i) - outData[i] = reinterpret_cast(outputChannelBuffers[i].data()); - - // Resample decoded audio - const int outSampleCount{ ::swr_convert( - _resampleContext.get(), - outData.data(), - static_cast(maxSamplesPerChannel), - (const uint8_t**)_decodedFrame->data, - _decodedFrame->nb_samples) }; + const std::size_t outSampleCount{ resampleFrame(outputChannelBuffers, maxSamplesPerChannel, _decodedFrame.get()) }; ::av_frame_unref(_decodedFrame.get()); - if (outSampleCount < 0) - throw FFmpegException{ "swr_convert failed", outSampleCount }; - if (outSampleCount > 0) - return static_cast(outSampleCount); + return outSampleCount; - continue; // Rare but legal: frame produced no output (delay accumulation) + continue; // delay accumulation: resampler is buffering, no output yet } // Drain resampler once decoder is drained if (_draining) { - const std::size_t outSampleCount{ drainResampler(outputChannelBuffers, maxSamplesPerChannel) }; - if (outSampleCount > 0) - return outSampleCount; + try + { + const std::size_t outSampleCount{ drainResampler(outputChannelBuffers, maxSamplesPerChannel) }; + if (outSampleCount > 0) + return outSampleCount; + } + catch (const Exception& e) + { + LMS_LOG(AUDIO, ERROR, "Failed to drain resampler: " << e.what()); + } _finished = true; break; @@ -286,7 +289,6 @@ namespace lms::audio::ffmpeg if (outputChannelBuffers.size() != _parameters.channelCount) throw Exception{ "Expected " + std::to_string(_parameters.channelCount) + " buffers for planar output" }; - // Each planar buffer holds samples for one channel only const int bytesPerSample{ av_get_bytes_per_sample(toAvSampleFormat(_parameters.sampleType, true)) }; if (bytesPerSample <= 0) throw Exception{ "Invalid bytes per sample for output format" }; @@ -306,7 +308,6 @@ namespace lms::audio::ffmpeg if (bytesPerSample <= 0) throw Exception{ "Invalid bytes per sample for output format" }; - // Divide by (bytes per sample * number of channels) for interleaved const std::size_t sampleCount = outputChannelBuffers[0].size() / (bytesPerSample * _parameters.channelCount); return sampleCount; @@ -353,28 +354,66 @@ namespace lms::audio::ffmpeg } } - std::size_t PcmDecoder::drainResampler(std::span outputChannelBuffers, std::size_t maxSamplesPerChannel) + bool PcmDecoder::inputFormatChanged(const AVFrame* frame) const { - std::array outData{}; - for (std::size_t i{}; i < outputChannelBuffers.size(); ++i) - outData[i] = reinterpret_cast(outputChannelBuffers[i].data()); + const auto& c{ _resamplerInputConfig }; + if (frame->sample_rate != c.sampleRate + || frame->format != c.sampleFormat + || frame->ch_layout.nb_channels != c.nbChannels + || static_cast(frame->ch_layout.order) != c.channelOrder) + return true; + if (frame->ch_layout.order == AV_CHANNEL_ORDER_NATIVE) + return frame->ch_layout.u.mask != c.channelMask; + return false; + } - const int outSampleCount{ ::swr_convert(_resampleContext.get(), - outData.data(), - static_cast(maxSamplesPerChannel), - nullptr, - 0) }; + void PcmDecoder::reinitResamplerForFrame(const AVFrame* frame) + { + // The format change is caused by a corrupt/non-standard frame so the lost samples are likely garbled audio anyway. + _resampleContext = createResampler(_parameters, frame->ch_layout, static_cast(frame->format), frame->sample_rate); + _resamplerInputConfig = { + frame->sample_rate, + frame->format, + frame->ch_layout.nb_channels, + static_cast(frame->ch_layout.order), + frame->ch_layout.u.mask, + }; + } + + std::size_t PcmDecoder::resampleFrame(std::span outputChannelBuffers, std::size_t maxSamplesPerChannel, const AVFrame* inputFrame) + { + if (inputFrame && inputFormatChanged(inputFrame)) + { + LMS_LOG(AUDIO, DEBUG, "Input format changed"); + reinitResamplerForFrame(inputFrame); + } + + std::array outData{}; + for (std::size_t i{}; i < outputChannelBuffers.size(); ++i) + outData[i] = static_cast(static_cast(outputChannelBuffers[i].data())); + + const int outSampleCount{ ::swr_convert( + _resampleContext.get(), + outData.data(), + static_cast(maxSamplesPerChannel), + inputFrame ? reinterpret_cast(inputFrame->data) : nullptr, + inputFrame ? inputFrame->nb_samples : 0) }; if (outSampleCount < 0) - throw FFmpegException{ "swr_convert (drain) failed", outSampleCount }; + throw FFmpegException{ inputFrame ? "swr_convert failed" : "swr_convert (drain) failed", outSampleCount }; - return outSampleCount; + return static_cast(outSampleCount); + } + + std::size_t PcmDecoder::drainResampler(std::span outputChannelBuffers, std::size_t maxSamplesPerChannel) + { + return resampleFrame(outputChannelBuffers, maxSamplesPerChannel, nullptr); } std::size_t PcmDecoder::getEstimatedResamplerAvailableSamples() const { - const int64_t delayedInputSampleCount{ ::swr_get_delay(_resampleContext.get(), _decoderContext->sample_rate) }; - const int64_t sampleCount{ av_rescale_rnd(delayedInputSampleCount, _parameters.sampleRate, _decoderContext->sample_rate, AV_ROUND_UP) }; + const int64_t delayedInputSampleCount{ ::swr_get_delay(_resampleContext.get(), _resamplerInputConfig.sampleRate) }; + const int64_t sampleCount{ av_rescale_rnd(delayedInputSampleCount, _parameters.sampleRate, _resamplerInputConfig.sampleRate, AV_ROUND_UP) }; return sampleCount; } diff --git a/src/libs/audio/impl/ffmpeg/PcmDecoder.hpp b/src/libs/audio/impl/ffmpeg/PcmDecoder.hpp index 8c7d5c3f..a2515527 100644 --- a/src/libs/audio/impl/ffmpeg/PcmDecoder.hpp +++ b/src/libs/audio/impl/ffmpeg/PcmDecoder.hpp @@ -44,6 +44,9 @@ namespace lms::audio::ffmpeg std::size_t computeSampleCountPerChannel(std::span outputChannelBuffers) const; void feedDecoder(); + bool inputFormatChanged(const AVFrame* frame) const; + void reinitResamplerForFrame(const AVFrame* frame); + std::size_t resampleFrame(std::span outputChannelBuffers, std::size_t maxSamplesPerChannel, const AVFrame* inputFrame); std::size_t drainResampler(std::span outputChannelBuffers, std::size_t maxSamplesPerChannel); std::size_t getEstimatedResamplerAvailableSamples() const; @@ -60,5 +63,15 @@ namespace lms::audio::ffmpeg AVFramePtr _decodedFrame; AVPacketPtr _inputPacket; SwrContextPtr _resampleContext; + + struct ResamplerInputConfig + { + int sampleRate{}; + int sampleFormat{}; + int nbChannels{}; + int channelOrder{}; + std::uint64_t channelMask{}; + }; + ResamplerInputConfig _resamplerInputConfig; }; } // namespace lms::audio::ffmpeg \ No newline at end of file