refactor ffmpeg super class into more maintainable package, cmake is catigorized now ; removed old engine, replaced with new one

This commit is contained in:
2026-06-09 11:29:01 +02:00
parent 3c274b5cf8
commit 86df304adb
12 changed files with 541 additions and 270 deletions
+52 -21
View File
@@ -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()
+1 -1
View File
@@ -1,5 +1,5 @@
#include "AudioEngine.h"
#include "engine/FFmpegDecoder.h"
#include "engine/ffmpeg/FFmpegDecoder.h"
#include "engine/EqProcessor.h"
#include "engine/SpectrumAnalyzer.h"
-191
View File
@@ -1,191 +0,0 @@
#include "FFmpegDecoder.h"
#include <QDebug>
#include <cstring>
extern "C" {
#include <libavformat/avformat.h>
#include <libavcodec/avcodec.h>
#include <libavutil/avutil.h>
#include <libswresample/swresample.h>
}
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<qint64>(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<qint64>(fraction * static_cast<double>(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<std::size_t>(outSamples) * m_channels);
uint8_t *outPtr = reinterpret_cast<uint8_t *>(buf.samples.data() + offset);
int converted = swr_convert(m_swr.get(),
&outPtr, outSamples,
const_cast<const uint8_t **>(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<std::size_t>(converted) * m_channels);
gotSamples = true;
}
av_frame_unref(m_frame.get());
}
return gotSamples;
}
} // namespace engine
-57
View File
@@ -1,57 +0,0 @@
#pragma once
#include <QString>
#include <memory>
#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<AVFormatContext, FmtDeleter> m_fmt;
std::unique_ptr<AVCodecContext, CodecDeleter> m_codec;
std::unique_ptr<SwrContext, SwrDeleter> m_swr;
std::unique_ptr<AVPacket, PktDeleter> m_pkt;
std::unique_ptr<AVFrame, FrameDeleter> 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
+67
View File
@@ -0,0 +1,67 @@
#include "CodecContext.h"
#include <QDebug>
extern "C" {
#include <libavcodec/avcodec.h>
}
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
+38
View File
@@ -0,0 +1,38 @@
#pragma once
#include <memory>
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<AVCodecContext, Deleter> m_ctx;
};
} // namespace engine
+106
View File
@@ -0,0 +1,106 @@
#include "FFmpegDecoder.h"
#include <QDebug>
extern "C" {
#include <libavcodec/avcodec.h>
#include <libavutil/avutil.h>
}
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<qint64>(streamDur * tb * m_codec.sampleRate());
} else {
double durSecs = m_format.durationSecs();
if (durSecs > 0)
m_totalFrames = static_cast<qint64>(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
+41
View File
@@ -0,0 +1,41 @@
#pragma once
#include <QString>
#include <memory>
#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
+94
View File
@@ -0,0 +1,94 @@
#include "FormatContext.h"
#include <QDebug>
#include <algorithm>
extern "C" {
#include <libavformat/avformat.h>
#include <libavutil/avutil.h>
}
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<qint64>(fraction * static_cast<double>(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<double>(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
+42
View File
@@ -0,0 +1,42 @@
#pragma once
#include <QString>
#include <memory>
extern "C" {
#include <libavformat/avformat.h>
}
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<AVFormatContext, Deleter> m_fmt;
int m_audioIdx { -1 };
};
} // namespace engine
+68
View File
@@ -0,0 +1,68 @@
#include "Resampler.h"
#include <QDebug>
extern "C" {
#include <libavcodec/avcodec.h>
#include <libswresample/swresample.h>
#include <libavutil/channel_layout.h>
}
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<float> &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<std::size_t>(maxOutSamples) * m_channels);
uint8_t *outPtr = reinterpret_cast<uint8_t *>(out.data() + offset);
int converted = swr_convert(m_swr.get(),
&outPtr, maxOutSamples,
const_cast<const uint8_t **>(frame->data), frame->nb_samples);
if (converted <= 0) {
out.resize(offset); // roll back the reservation
return 0;
}
out.resize(offset + static_cast<std::size_t>(converted) * m_channels);
return converted;
}
} // namespace engine
+32
View File
@@ -0,0 +1,32 @@
#pragma once
#include <memory>
#include <vector>
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<float> &out) const;
bool isReady() const { return m_swr != nullptr; }
private:
struct Deleter { void operator()(SwrContext *p) const; };
std::unique_ptr<SwrContext, Deleter> m_swr;
int m_channels { 0 };
int m_sampleRate { 0 };
};
} // namespace engine