refactor ffmpeg super class into more maintainable package, cmake is catigorized now ; removed old engine, replaced with new one
This commit is contained in:
+52
-21
@@ -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,5 +1,5 @@
|
||||
#include "AudioEngine.h"
|
||||
#include "engine/FFmpegDecoder.h"
|
||||
#include "engine/ffmpeg/FFmpegDecoder.h"
|
||||
#include "engine/EqProcessor.h"
|
||||
#include "engine/SpectrumAnalyzer.h"
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user