From 86df304adb6ad904670714aa08ff5f95a87aa765 Mon Sep 17 00:00:00 2001 From: rattatwinko Date: Tue, 9 Jun 2026 11:29:01 +0200 Subject: [PATCH] refactor ffmpeg super class into more maintainable package, cmake is catigorized now ; removed old engine, replaced with new one --- CMakeLists.txt | 73 ++++++++--- src/engine/AudioEngine.cpp | 2 +- src/engine/FFmpegDecoder.cpp | 191 ---------------------------- src/engine/FFmpegDecoder.h | 57 --------- src/engine/ffmpeg/CodecContext.cpp | 67 ++++++++++ src/engine/ffmpeg/CodecContext.h | 38 ++++++ src/engine/ffmpeg/FFmpegDecoder.cpp | 106 +++++++++++++++ src/engine/ffmpeg/FFmpegDecoder.h | 41 ++++++ src/engine/ffmpeg/FormatContext.cpp | 94 ++++++++++++++ src/engine/ffmpeg/FormatContext.h | 42 ++++++ src/engine/ffmpeg/Resampler.cpp | 68 ++++++++++ src/engine/ffmpeg/Resampler.h | 32 +++++ 12 files changed, 541 insertions(+), 270 deletions(-) delete mode 100644 src/engine/FFmpegDecoder.cpp delete mode 100644 src/engine/FFmpegDecoder.h create mode 100644 src/engine/ffmpeg/CodecContext.cpp create mode 100644 src/engine/ffmpeg/CodecContext.h create mode 100644 src/engine/ffmpeg/FFmpegDecoder.cpp create mode 100644 src/engine/ffmpeg/FFmpegDecoder.h create mode 100644 src/engine/ffmpeg/FormatContext.cpp create mode 100644 src/engine/ffmpeg/FormatContext.h create mode 100644 src/engine/ffmpeg/Resampler.cpp create mode 100644 src/engine/ffmpeg/Resampler.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 28e3600..b432039 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,81 +4,107 @@ project(SubWave LANGUAGES CXX) set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) -find_package(Qt6 REQUIRED COMPONENTS Widgets Multimedia OpenGL OpenGLWidgets Core) +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTORCC ON) + +# dependencies +find_package(Qt6 REQUIRED COMPONENTS + Core Widgets Multimedia OpenGL OpenGLWidgets +) -# require ffmpeg find_package(PkgConfig REQUIRED) + pkg_check_modules(FFMPEG REQUIRED IMPORTED_TARGET libavformat libavcodec libavutil libswresample ) -# require fftw (fourier transform) + pkg_check_modules(FFTW REQUIRED IMPORTED_TARGET fftw3) -set(CMAKE_AUTOMOC ON) -set(CMAKE_AUTORCC ON) - -# source files +# sources set(SOURCES src/main.cpp src/MetadataReader.cpp src/Config.cpp - src/engine/AudioEngine.cpp - src/engine/FFmpegDecoder.cpp - src/engine/EqProcessor.cpp - src/engine/SpectrumAnalyzer.cpp src/MusicLibrary.cpp src/CoverArtWidget.cpp src/SpectrogramWidget.cpp src/GradientDialog.cpp src/EqualizerWidget.cpp src/TrackDelegate.cpp + src/MainWindow.cpp + + # engine + src/engine/AudioEngine.cpp + src/engine/EqProcessor.cpp + src/engine/SpectrumAnalyzer.cpp + + # ffmpeg + src/engine/ffmpeg/FormatContext.cpp + src/engine/ffmpeg/CodecContext.cpp + src/engine/ffmpeg/Resampler.cpp + src/engine/ffmpeg/FFmpegDecoder.cpp + + # playlists src/playlists/PlaylistModel.cpp src/playlists/PlaylistManager.cpp src/playlists/PlaylistCreatorDialog.cpp src/playlists/PlaylistBrowserDialog.cpp - src/MainWindow.cpp ) set(HEADERS src/Track.h src/MetadataReader.h src/Config.h - src/engine/AudioEngine.h src/MusicLibrary.h src/CoverArtWidget.h src/SpectrogramWidget.h src/GradientDialog.h src/EqualizerWidget.h src/TrackDelegate.h + src/MainWindow.h + + # engine + src/engine/AudioEngine.h + src/engine/EqProcessor.h + src/engine/SpectrumAnalyzer.h + src/engine/AudioBuffer.h + + # ffmpeg wrapper + src/engine/ffmpeg/FormatContext.h + src/engine/ffmpeg/CodecContext.h + src/engine/ffmpeg/Resampler.h + src/engine/ffmpeg/FFmpegDecoder.h + + # playlists src/playlists/PlaylistModel.h src/playlists/PlaylistManager.h src/playlists/PlaylistCreatorDialog.h src/playlists/PlaylistBrowserDialog.h - src/MainWindow.h ) -set(RESOURCES +# qrc resources +set(RESOURCES src/shaders.qrc src/application.qrc ) -# normal platforms + +# target add_executable(SubWave ${SOURCES} ${HEADERS} ${RESOURCES}) -# For windows stupid ahh -if(WIN32) - set_target_properties(SubWave PROPERTIES WIN32_EXECUTABLE TRUE) -endif() - +# headers target_include_directories(SubWave PRIVATE src src/engine + src/engine/ffmpeg src/playlists ) +# link to qt and ffmpeg and fftw target_link_libraries(SubWave PRIVATE + Qt6::Core Qt6::Widgets Qt6::Multimedia Qt6::OpenGL @@ -86,3 +112,8 @@ target_link_libraries(SubWave PRIVATE PkgConfig::FFMPEG PkgConfig::FFTW ) + +# For windows stupid ahh +if(WIN32) + set_target_properties(SubWave PROPERTIES WIN32_EXECUTABLE TRUE) +endif() diff --git a/src/engine/AudioEngine.cpp b/src/engine/AudioEngine.cpp index 2aa2914..7f7d12d 100644 --- a/src/engine/AudioEngine.cpp +++ b/src/engine/AudioEngine.cpp @@ -1,5 +1,5 @@ #include "AudioEngine.h" -#include "engine/FFmpegDecoder.h" +#include "engine/ffmpeg/FFmpegDecoder.h" #include "engine/EqProcessor.h" #include "engine/SpectrumAnalyzer.h" diff --git a/src/engine/FFmpegDecoder.cpp b/src/engine/FFmpegDecoder.cpp deleted file mode 100644 index 28f9ac8..0000000 --- a/src/engine/FFmpegDecoder.cpp +++ /dev/null @@ -1,191 +0,0 @@ -#include "FFmpegDecoder.h" -#include -#include - -extern "C" { -#include -#include -#include -#include -} - -namespace engine { - -void FFmpegDecoder::FmtDeleter::operator()(AVFormatContext *p) const { - avformat_close_input(&p); -} -void FFmpegDecoder::CodecDeleter::operator()(AVCodecContext *p) const { - avcodec_free_context(&p); -} -void FFmpegDecoder::SwrDeleter::operator()(SwrContext *p) const { - swr_free(&p); -} -void FFmpegDecoder::PktDeleter::operator()(AVPacket *p) const { - av_packet_free(&p); -} -void FFmpegDecoder::FrameDeleter::operator()(AVFrame *p) const { - av_frame_free(&p); -} - -FFmpegDecoder::FFmpegDecoder() = default; -FFmpegDecoder::~FFmpegDecoder() { close(); } - -bool FFmpegDecoder::open(const QString &filePath) { - close(); - - AVFormatContext *rawFmt = nullptr; - AVDictionary *opts = nullptr; - av_dict_set(&opts, "probesize", "5000000", 0); - av_dict_set(&opts, "analyzeduration", "5000000", 0); - - if (avformat_open_input(&rawFmt, filePath.toUtf8().constData(), - nullptr, &opts) < 0) { - av_dict_free(&opts); - qWarning() << "[FFmpegDecoder] Cannot open:" << filePath; - return false; - } - av_dict_free(&opts); - m_fmt.reset(rawFmt); - - avformat_find_stream_info(m_fmt.get(), nullptr); - - if (m_fmt->duration != AV_NOPTS_VALUE && m_fmt->duration > 0) - m_durationSecs = m_fmt->duration / AV_TIME_BASE; - - m_audioIdx = av_find_best_stream(m_fmt.get(), AVMEDIA_TYPE_AUDIO, - -1, -1, nullptr, 0); - if (m_audioIdx < 0) { - qWarning() << "[FFmpegDecoder] No audio stream in:" << filePath; - close(); - return false; - } - - AVStream *stream = m_fmt->streams[m_audioIdx]; - const AVCodec *dec = avcodec_find_decoder(stream->codecpar->codec_id); - if (!dec) { - qWarning() << "[FFmpegDecoder] No decoder for codec"; - close(); - return false; - } - - AVCodecContext *rawCodec = avcodec_alloc_context3(dec); - avcodec_parameters_to_context(rawCodec, stream->codecpar); - if (avcodec_open2(rawCodec, dec, nullptr) < 0) { - avcodec_free_context(&rawCodec); - qWarning() << "[FFmpegDecoder] Cannot open codec"; - close(); - return false; - } - m_codec.reset(rawCodec); - - m_sampleRate = m_codec->sample_rate; - m_channels = m_codec->ch_layout.nb_channels; - - // Derive total frames from stream duration where possible - if (stream->duration != AV_NOPTS_VALUE) { - double tb = av_q2d(stream->time_base); - m_totalFrames = static_cast(stream->duration * tb * m_sampleRate); - } else if (m_durationSecs > 0) { - m_totalFrames = m_durationSecs * m_sampleRate; - } - - AVChannelLayout outLayout{}; - av_channel_layout_default(&outLayout, m_channels); - - SwrContext *rawSwr = nullptr; - swr_alloc_set_opts2(&rawSwr, - &outLayout, AV_SAMPLE_FMT_FLT, m_sampleRate, - &m_codec->ch_layout, m_codec->sample_fmt, m_sampleRate, - 0, nullptr); - swr_init(rawSwr); - m_swr.reset(rawSwr); - - m_pkt.reset(av_packet_alloc()); - m_frame.reset(av_frame_alloc()); - - return true; -} - -void FFmpegDecoder::close() { - m_frame.reset(); - m_pkt.reset(); - m_swr.reset(); - m_codec.reset(); - m_fmt.reset(); - m_audioIdx = -1; - m_sampleRate = 0; - m_channels = 0; - m_durationSecs = -1; - m_totalFrames = 0; -} - -void FFmpegDecoder::seek(double fraction) { - if (!m_fmt) return; - fraction = std::clamp(fraction, 0.0, 1.0); - - qint64 durUs = (m_fmt->duration != AV_NOPTS_VALUE && m_fmt->duration > 0) - ? m_fmt->duration - : (m_durationSecs * AV_TIME_BASE); - - if (durUs <= 0) return; - - qint64 targetUs = static_cast(fraction * static_cast(durUs)); - av_seek_frame(m_fmt.get(), -1, targetUs, AVSEEK_FLAG_BACKWARD); - avcodec_flush_buffers(m_codec.get()); -} - -bool FFmpegDecoder::decodeNext(AudioBuffer &buf) { - if (!m_fmt || !m_codec || !m_swr) return false; - - buf.sampleRate = m_sampleRate; - buf.channels = m_channels; - buf.samples.clear(); - - while (true) { - int ret = av_read_frame(m_fmt.get(), m_pkt.get()); - if (ret < 0) return false; // EOF or error - - if (m_pkt->stream_index != m_audioIdx) { - av_packet_unref(m_pkt.get()); - continue; - } - - avcodec_send_packet(m_codec.get(), m_pkt.get()); - av_packet_unref(m_pkt.get()); - - if (receiveFrames(buf)) - return true; - } -} - -bool FFmpegDecoder::receiveFrames(AudioBuffer &buf) { - bool gotSamples = false; - - while (true) { - int ret = avcodec_receive_frame(m_codec.get(), m_frame.get()); - if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) break; - if (ret < 0) break; - - int outSamples = swr_get_out_samples(m_swr.get(), m_frame->nb_samples); - std::size_t offset = buf.samples.size(); - buf.samples.resize(offset + static_cast(outSamples) * m_channels); - - uint8_t *outPtr = reinterpret_cast(buf.samples.data() + offset); - int converted = swr_convert(m_swr.get(), - &outPtr, outSamples, - const_cast(m_frame->data), m_frame->nb_samples); - - if (converted <= 0) { - buf.samples.resize(offset); // roll back the reservation - } else { - buf.samples.resize(offset + static_cast(converted) * m_channels); - gotSamples = true; - } - - av_frame_unref(m_frame.get()); - } - - return gotSamples; -} - -} // namespace engine \ No newline at end of file diff --git a/src/engine/FFmpegDecoder.h b/src/engine/FFmpegDecoder.h deleted file mode 100644 index 3926fa4..0000000 --- a/src/engine/FFmpegDecoder.h +++ /dev/null @@ -1,57 +0,0 @@ -#pragma once -#include -#include -#include "AudioBuffer.h" - -struct AVFormatContext; -struct AVCodecContext; -struct SwrContext; -struct AVPacket; -struct AVFrame; - -namespace engine { - -class FFmpegDecoder { -public: - FFmpegDecoder(); - ~FFmpegDecoder(); - - FFmpegDecoder(const FFmpegDecoder &) = delete; - FFmpegDecoder &operator=(const FFmpegDecoder &) = delete; - - bool open(const QString &filePath); - - void close(); - - bool decodeNext(AudioBuffer &buf); - - void seek(double fraction); - - int sampleRate() const { return m_sampleRate; } - int channels() const { return m_channels; } - qint64 durationSeconds() const { return m_durationSecs; } - qint64 totalFrames() const { return m_totalFrames; } - -private: - struct FmtDeleter { void operator()(AVFormatContext *p) const; }; - struct CodecDeleter { void operator()(AVCodecContext *p) const; }; - struct SwrDeleter { void operator()(SwrContext *p) const; }; - struct PktDeleter { void operator()(AVPacket *p) const; }; - struct FrameDeleter { void operator()(AVFrame *p) const; }; - - std::unique_ptr m_fmt; - std::unique_ptr m_codec; - std::unique_ptr m_swr; - std::unique_ptr m_pkt; - std::unique_ptr m_frame; - - int m_audioIdx { -1 }; - int m_sampleRate { 0 }; - int m_channels { 0 }; - qint64 m_durationSecs{ -1 }; - qint64 m_totalFrames { 0 }; - - bool receiveFrames(AudioBuffer &buf); -}; - -} // namespace engine \ No newline at end of file diff --git a/src/engine/ffmpeg/CodecContext.cpp b/src/engine/ffmpeg/CodecContext.cpp new file mode 100644 index 0000000..48b6740 --- /dev/null +++ b/src/engine/ffmpeg/CodecContext.cpp @@ -0,0 +1,67 @@ +#include "CodecContext.h" +#include + +extern "C" { +#include +} + +namespace engine { + +void CodecContext::Deleter::operator()(AVCodecContext *p) const { + avcodec_free_context(&p); +} + +bool CodecContext::open(const AVCodecParameters *par) { + close(); + + const AVCodec *dec = avcodec_find_decoder(par->codec_id); + if (!dec) { + qWarning() << "[CodecContext] No decoder found for codec_id" << par->codec_id; + return false; + } + + AVCodecContext *raw = avcodec_alloc_context3(dec); + if (!raw) { + qWarning() << "[CodecContext] Failed to allocate codec context"; + return false; + } + + avcodec_parameters_to_context(raw, par); + + if (avcodec_open2(raw, dec, nullptr) < 0) { + avcodec_free_context(&raw); + qWarning() << "[CodecContext] Cannot open codec"; + return false; + } + + m_ctx.reset(raw); + return true; +} + +void CodecContext::close() { + m_ctx.reset(); +} + +bool CodecContext::sendPacket(const AVPacket *pkt) const { + return m_ctx && avcodec_send_packet(m_ctx.get(), pkt) >= 0; +} + +bool CodecContext::receiveFrame(AVFrame *frame) const { + if (!m_ctx) return false; + int ret = avcodec_receive_frame(m_ctx.get(), frame); + return ret == 0; +} + +void CodecContext::flush() { + if (m_ctx) avcodec_flush_buffers(m_ctx.get()); +} + +int CodecContext::sampleRate() const { + return m_ctx ? m_ctx->sample_rate : 0; +} + +int CodecContext::channels() const { + return m_ctx ? m_ctx->ch_layout.nb_channels : 0; +} + +} // namespace engine diff --git a/src/engine/ffmpeg/CodecContext.h b/src/engine/ffmpeg/CodecContext.h new file mode 100644 index 0000000..c189aa6 --- /dev/null +++ b/src/engine/ffmpeg/CodecContext.h @@ -0,0 +1,38 @@ +#pragma once +#include + +struct AVCodecContext; +struct AVCodecParameters; +struct AVPacket; +struct AVFrame; + +namespace engine { + +class CodecContext { +public: + CodecContext() = default; + ~CodecContext() = default; + CodecContext(const CodecContext &) = delete; + CodecContext &operator=(const CodecContext &) = delete; + + bool open(const AVCodecParameters *par); + void close(); + + bool sendPacket(const AVPacket *pkt) const; + + bool receiveFrame(AVFrame *frame) const; + + void flush(); + + bool isOpen() const { return m_ctx != nullptr; } + int sampleRate()const; + int channels() const; + + const AVCodecContext *get() const { return m_ctx.get(); } + +private: + struct Deleter { void operator()(AVCodecContext *p) const; }; + std::unique_ptr m_ctx; +}; + +} // namespace engine diff --git a/src/engine/ffmpeg/FFmpegDecoder.cpp b/src/engine/ffmpeg/FFmpegDecoder.cpp new file mode 100644 index 0000000..e963946 --- /dev/null +++ b/src/engine/ffmpeg/FFmpegDecoder.cpp @@ -0,0 +1,106 @@ +#include "FFmpegDecoder.h" +#include + +extern "C" { +#include +#include +} + +namespace engine { + +FFmpegDecoder::FFmpegDecoder() = default; + +bool FFmpegDecoder::open(const QString &filePath) { + close(); + + if (!m_format.open(filePath)) + return false; + + const AVCodecParameters *par = m_format.audioCodecPar(); + const AVRational streamTb = m_format.audioTimeBase(); + const qint64 streamDur = m_format.audioStreamDuration(); + + if (!par) { + qWarning() << "[FFmpegDecoder] Could not retrieve codec parameters"; + close(); + return false; + } + + if (!m_codec.open(par)) { + close(); + return false; + } + + if (!m_resampler.init(m_codec.get())) { + close(); + return false; + } + + // Compute total frame count + if (streamDur != AV_NOPTS_VALUE) { + double tb = av_q2d(streamTb); + m_totalFrames = static_cast(streamDur * tb * m_codec.sampleRate()); + } else { + double durSecs = m_format.durationSecs(); + if (durSecs > 0) + m_totalFrames = static_cast(durSecs * m_codec.sampleRate()); + } + + return true; +} + +void FFmpegDecoder::close() { + m_resampler.close(); + m_codec.close(); + m_format.close(); + m_totalFrames = 0; +} + +void FFmpegDecoder::seek(double fraction) { + m_format.seek(fraction); + m_codec.flush(); +} + +bool FFmpegDecoder::decodeNext(AudioBuffer &buf) { + if (!m_format.isOpen()) return false; + + buf.sampleRate = sampleRate(); + buf.channels = channels(); + buf.samples.clear(); + + AVPacket *pkt = av_packet_alloc(); + + while (m_format.readPacket(pkt)) { + if (pkt->stream_index != m_format.audioIndex()) { + av_packet_unref(pkt); + continue; + } + + m_codec.sendPacket(pkt); + av_packet_unref(pkt); + + if (drainCodec(buf)) { + av_packet_free(&pkt); + return true; + } + } + + av_packet_free(&pkt); + return false; +} + +bool FFmpegDecoder::drainCodec(AudioBuffer &buf) const { + bool gotSamples = false; + + AVFrame *frame = av_frame_alloc(); + while (m_codec.receiveFrame(frame)) { + if (m_resampler.convert(frame, buf.samples) > 0) + gotSamples = true; + av_frame_unref(frame); + } + av_frame_free(&frame); + + return gotSamples; +} + +} // namespace engine diff --git a/src/engine/ffmpeg/FFmpegDecoder.h b/src/engine/ffmpeg/FFmpegDecoder.h new file mode 100644 index 0000000..44ebe81 --- /dev/null +++ b/src/engine/ffmpeg/FFmpegDecoder.h @@ -0,0 +1,41 @@ +#pragma once +#include +#include +#include "AudioBuffer.h" +#include "FormatContext.h" +#include "CodecContext.h" +#include "Resampler.h" + +namespace engine { + +// Thin orchestrator: ties FormatContext, CodecContext, and Resampler together +// to expose a simple open / seek / decodeNext interface. +class FFmpegDecoder { +public: + FFmpegDecoder(); + ~FFmpegDecoder() = default; + FFmpegDecoder(const FFmpegDecoder &) = delete; + FFmpegDecoder &operator=(const FFmpegDecoder &) = delete; + + bool open(const QString &filePath); + void close(); + bool decodeNext(AudioBuffer &buf); + void seek(double fraction); + + int sampleRate() const { return m_codec.sampleRate(); } + int channels() const { return m_codec.channels(); } + double durationSeconds() const { return m_format.durationSecs(); } + qint64 totalFrames() const { return m_totalFrames; } + +private: + FormatContext m_format; + CodecContext m_codec; + Resampler m_resampler; + + qint64 m_totalFrames { 0 }; + + // Drains all frames currently buffered in the codec into buf. + bool drainCodec(AudioBuffer &buf) const; +}; + +} // namespace engine diff --git a/src/engine/ffmpeg/FormatContext.cpp b/src/engine/ffmpeg/FormatContext.cpp new file mode 100644 index 0000000..309654b --- /dev/null +++ b/src/engine/ffmpeg/FormatContext.cpp @@ -0,0 +1,94 @@ +#include "FormatContext.h" +#include +#include + +extern "C" { +#include +#include +} + +namespace engine { + +void FormatContext::Deleter::operator()(AVFormatContext *p) const { + avformat_close_input(&p); +} + +bool FormatContext::open(const QString &filePath) { + close(); + + AVFormatContext *raw = nullptr; + AVDictionary *opts = nullptr; + av_dict_set(&opts, "probesize", "5000000", 0); + av_dict_set(&opts, "analyzeduration", "5000000", 0); + + int err = avformat_open_input(&raw, filePath.toUtf8().constData(), + nullptr, &opts); + av_dict_free(&opts); + + if (err < 0) { + qWarning() << "[FormatContext] Cannot open:" << filePath; + return false; + } + m_fmt.reset(raw); + + avformat_find_stream_info(m_fmt.get(), nullptr); + + m_audioIdx = av_find_best_stream(m_fmt.get(), AVMEDIA_TYPE_AUDIO, + -1, -1, nullptr, 0); + if (m_audioIdx < 0) { + qWarning() << "[FormatContext] No audio stream in:" << filePath; + close(); + return false; + } + + return true; +} + +void FormatContext::close() { + m_fmt.reset(); + m_audioIdx = -1; +} + +void FormatContext::seek(double fraction) { + if (!m_fmt) return; + fraction = std::clamp(fraction, 0.0, 1.0); + + qint64 durUs = durationUs(); + if (durUs <= 0) return; + + qint64 targetUs = static_cast(fraction * static_cast(durUs)); + av_seek_frame(m_fmt.get(), -1, targetUs, AVSEEK_FLAG_BACKWARD); +} + +bool FormatContext::readPacket(AVPacket *pkt) const { + return m_fmt && av_read_frame(m_fmt.get(), pkt) >= 0; +} + +qint64 FormatContext::durationUs() const { + if (!m_fmt) return AV_NOPTS_VALUE; + return (m_fmt->duration != AV_NOPTS_VALUE && m_fmt->duration > 0) + ? m_fmt->duration + : AV_NOPTS_VALUE; +} + +double FormatContext::durationSecs() const { + qint64 us = durationUs(); + return (us != AV_NOPTS_VALUE) ? static_cast(us) / AV_TIME_BASE : -1.0; +} + +const AVCodecParameters *FormatContext::audioCodecPar() const { + if (!m_fmt || m_audioIdx < 0) return nullptr; + return m_fmt->streams[m_audioIdx]->codecpar; +} + +AVRational FormatContext::audioTimeBase() const { + if (!m_fmt || m_audioIdx < 0) return {0, 1}; + return m_fmt->streams[m_audioIdx]->time_base; +} + +qint64 FormatContext::audioStreamDuration() const { + if (!m_fmt || m_audioIdx < 0) return AV_NOPTS_VALUE; + return m_fmt->streams[m_audioIdx]->duration; +} + +} // namespace engine diff --git a/src/engine/ffmpeg/FormatContext.h b/src/engine/ffmpeg/FormatContext.h new file mode 100644 index 0000000..20fbec0 --- /dev/null +++ b/src/engine/ffmpeg/FormatContext.h @@ -0,0 +1,42 @@ +#pragma once +#include +#include + +extern "C" { +#include +} + +struct AVPacket; + +namespace engine { + +class FormatContext { +public: + FormatContext() = default; + ~FormatContext() = default; + FormatContext(const FormatContext &) = delete; + FormatContext &operator=(const FormatContext &) = delete; + + bool open(const QString &filePath); + void close(); + + void seek(double fraction); + + bool readPacket(AVPacket *pkt) const; + + bool isOpen() const { return m_fmt != nullptr; } + int audioIndex() const { return m_audioIdx; } + qint64 durationUs() const; // AV_NOPTS_VALUE if unknown + double durationSecs()const; // -1 if unknown + + const AVCodecParameters *audioCodecPar() const; + AVRational audioTimeBase() const; + qint64 audioStreamDuration() const; // AV_NOPTS_VALUE if unknown + +private: + struct Deleter { void operator()(AVFormatContext *p) const; }; + std::unique_ptr m_fmt; + int m_audioIdx { -1 }; +}; + +} // namespace engine diff --git a/src/engine/ffmpeg/Resampler.cpp b/src/engine/ffmpeg/Resampler.cpp new file mode 100644 index 0000000..0be6ade --- /dev/null +++ b/src/engine/ffmpeg/Resampler.cpp @@ -0,0 +1,68 @@ +#include "Resampler.h" +#include + +extern "C" { +#include +#include +#include +} + +namespace engine { + +void Resampler::Deleter::operator()(SwrContext *p) const { + swr_free(&p); +} + +bool Resampler::init(const AVCodecContext *ctx) { + close(); + + m_channels = ctx->ch_layout.nb_channels; + m_sampleRate = ctx->sample_rate; + + AVChannelLayout outLayout{}; + av_channel_layout_default(&outLayout, m_channels); + + SwrContext *raw = nullptr; + int err = swr_alloc_set_opts2(&raw, + &outLayout, AV_SAMPLE_FMT_FLT, m_sampleRate, + &ctx->ch_layout, ctx->sample_fmt, m_sampleRate, + 0, nullptr); + + if (err < 0 || swr_init(raw) < 0) { + swr_free(&raw); + qWarning() << "[Resampler] Failed to initialise SwrContext"; + return false; + } + + m_swr.reset(raw); + return true; +} + +void Resampler::close() { + m_swr.reset(); + m_channels = 0; + m_sampleRate = 0; +} + +int Resampler::convert(const AVFrame *frame, std::vector &out) const { + if (!m_swr) return 0; + + int maxOutSamples = swr_get_out_samples(m_swr.get(), frame->nb_samples); + std::size_t offset = out.size(); + out.resize(offset + static_cast(maxOutSamples) * m_channels); + + uint8_t *outPtr = reinterpret_cast(out.data() + offset); + int converted = swr_convert(m_swr.get(), + &outPtr, maxOutSamples, + const_cast(frame->data), frame->nb_samples); + + if (converted <= 0) { + out.resize(offset); // roll back the reservation + return 0; + } + + out.resize(offset + static_cast(converted) * m_channels); + return converted; +} + +} // namespace engine diff --git a/src/engine/ffmpeg/Resampler.h b/src/engine/ffmpeg/Resampler.h new file mode 100644 index 0000000..b24db23 --- /dev/null +++ b/src/engine/ffmpeg/Resampler.h @@ -0,0 +1,32 @@ +#pragma once +#include +#include + +struct SwrContext; +struct AVCodecContext; +struct AVFrame; + +namespace engine { + +class Resampler { +public: + Resampler() = default; + ~Resampler() = default; + Resampler(const Resampler &) = delete; + Resampler &operator=(const Resampler &) = delete; + + bool init(const AVCodecContext *ctx); + void close(); + + int convert(const AVFrame *frame, std::vector &out) const; + + bool isReady() const { return m_swr != nullptr; } + +private: + struct Deleter { void operator()(SwrContext *p) const; }; + std::unique_ptr m_swr; + int m_channels { 0 }; + int m_sampleRate { 0 }; +}; + +} // namespace engine