this feature will not last very long, its too hard to maintain, sometimes segfaults when encoder is finised or stops ?

moved into seperate folder, and cleaned up some components
This commit is contained in:
2026-06-25 09:23:01 +02:00
parent d605509588
commit 51421ffc2b
11 changed files with 757 additions and 4 deletions
+12 -3
View File
@@ -38,7 +38,10 @@ set(SOURCES
src/engine/SpectrumAnalyzer.cpp
src/engine/MusicLibrary.cpp
src/engine/MetadataReader.cpp
src/engine/AlbumReencoder.cpp
# engine/encoder
src/engine/encoder/AlbumReencoder.cpp
src/engine/encoder/TrackEncoder.cpp
src/engine/encoder/StageBuffer.cpp
# ffmpeg
src/engine/ffmpeg/FormatContext.cpp
src/engine/ffmpeg/CodecContext.cpp
@@ -74,8 +77,13 @@ set(HEADERS
src/engine/Track.h
src/engine/MetadataReader.h
src/engine/MusicLibrary.h
src/engine/ReencodeJob.h
src/engine/AlbumReencoder.h
# engine/encoder
src/engine/encoder/ReencodeJob.h
src/engine/encoder/AlbumReencoder.h
src/engine/encoder/TrackEncoder.h
src/engine/encoder/StageBuffer.h
src/engine/encoder/FfmpegGuards.h
src/engine/encoder/SampleRateUtils.h
# ffmpeg wrapper
src/engine/ffmpeg/FormatContext.h
src/engine/ffmpeg/CodecContext.h
@@ -111,6 +119,7 @@ target_include_directories(SubWave PRIVATE
src/config
src/controllers
src/engine
src/engine/encoder
src/engine/ffmpeg
src/playlists
src/gui
+84
View File
@@ -0,0 +1,84 @@
#include "AlbumReencoder.h"
#include "TrackEncoder.h"
namespace engine {
AlbumReencoder::AlbumReencoder(QObject *parent) : QObject(parent) {}
AlbumReencoder::~AlbumReencoder()
{
requestStop();
if (m_thread) {
m_thread->wait(8000);
}
}
void AlbumReencoder::setJobs(const QList<ReencodeJob> &jobs)
{
QMutexLocker lk(&m_mtx);
m_jobs = jobs;
}
QList<ReencodeJob> AlbumReencoder::jobs() const
{
QMutexLocker lk(&m_mtx);
return m_jobs;
}
bool AlbumReencoder::isRunning() const
{
return m_thread && m_thread->isRunning();
}
void AlbumReencoder::requestStop()
{
m_stopReq = true;
}
void AlbumReencoder::start()
{
if (isRunning()) return;
m_stopReq = false;
m_thread = QThread::create([this]{ workerRun(); });
m_thread->setObjectName("subwave-reencode");
connect(m_thread, &QThread::finished, m_thread, &QObject::deleteLater);
m_thread->start(QThread::LowPriority);
}
void AlbumReencoder::workerRun()
{
TrackEncoder encoder(m_stopReq);
int succeeded = 0, failed = 0;
QMutexLocker lk(&m_mtx);
int total = m_jobs.size();
lk.unlock();
for (int i = 0; i < total; ++i) {
if (m_stopReq) break;
lk.relock();
ReencodeJob &job = m_jobs[i];
job.status = ReencodeJob::Status::Running;
lk.unlock();
emit trackStarted(i, job.track.displayTitle());
bool ok = encoder.encode(job);
lk.relock();
job.status = ok ? ReencodeJob::Status::Done : ReencodeJob::Status::Failed;
lk.unlock();
if (ok) ++succeeded; else ++failed;
emit trackFinished(i, ok, job.outputPath, job.errorString);
}
emit allFinished(succeeded, failed);
}
} // namespace engine
+47
View File
@@ -0,0 +1,47 @@
#pragma once
#include "ReencodeJob.h"
#include <QList>
#include <QMutex>
#include <QObject>
#include <QThread>
#include <atomic>
namespace engine {
class AlbumReencoder : public QObject
{
Q_OBJECT
public:
explicit AlbumReencoder(QObject *parent = nullptr);
~AlbumReencoder() override;
void setJobs(const QList<ReencodeJob> &jobs);
QList<ReencodeJob> jobs() const;
bool isRunning() const;
public slots:
void start();
void requestStop();
signals:
void trackStarted (int index, const QString &title);
void trackFinished(int index, bool ok,
const QString &outputPath,
const QString &errorString);
void allFinished (int succeeded, int failed);
private:
void workerRun();
mutable QMutex m_mtx;
QList<ReencodeJob> m_jobs;
QThread *m_thread = nullptr;
std::atomic<bool> m_stopReq{false};
};
} // namespace engine
+47
View File
@@ -0,0 +1,47 @@
#pragma once
extern "C" {
#include <libavformat/avformat.h>
#include <libavcodec/avcodec.h>
#include <libswresample/swresample.h>
}
namespace engine {
struct FmtCtxIn {
AVFormatContext *p = nullptr;
~FmtCtxIn() { if (p) avformat_close_input(&p); }
};
struct FmtCtxOut {
AVFormatContext *p = nullptr;
~FmtCtxOut() {
if (p) {
if (!(p->oformat->flags & AVFMT_NOFILE))
avio_closep(&p->pb);
avformat_free_context(p);
}
}
};
struct CodecCtxGuard {
AVCodecContext *p = nullptr;
~CodecCtxGuard() { if (p) avcodec_free_context(&p); }
};
struct SwrGuard {
SwrContext *p = nullptr;
~SwrGuard() { if (p) swr_free(&p); }
};
struct FrameGuard {
AVFrame *p = av_frame_alloc();
~FrameGuard() { av_frame_free(&p); }
};
struct PacketGuard {
AVPacket *p = av_packet_alloc();
~PacketGuard() { av_packet_free(&p); }
};
} // namespace engine
+35
View File
@@ -0,0 +1,35 @@
#pragma once
extern "C" {
#include <libavcodec/avcodec.h>
}
namespace engine {
inline int pickSampleRate(const AVCodec *encoder, int sourceRate)
{
if (!encoder->supported_samplerates)
return sourceRate;
for (const int *r = encoder->supported_samplerates; *r != 0; ++r)
if (*r == sourceRate)
return sourceRate;
int best = 0;
for (const int *r = encoder->supported_samplerates; *r != 0; ++r) {
if (*r >= sourceRate) {
if (best == 0 || *r < best)
best = *r;
}
}
if (best != 0)
return best;
for (const int *r = encoder->supported_samplerates; *r != 0; ++r)
if (*r > best)
best = *r;
return best;
}
} // namespace engine
+128
View File
@@ -0,0 +1,128 @@
// StageBuffer.cpp
// "America doesn't have health insurance" - Joe Biden
// ^^ may god help you
#include "StageBuffer.h"
#include <algorithm>
#include <cstring>
extern "C" {
#include <libavutil/avutil.h>
}
namespace engine {
StageBuffer::StageBuffer(SwrContext *swr,
AVFrame *encFrame,
AVSampleFormat fmt,
int channels,
int frameSize,
SendFrameFn sendFrame)
: m_swr(swr)
, m_encFrame(encFrame)
, m_fmt(fmt)
, m_channels(channels)
, m_frameSize(frameSize)
, m_bytesPerSample(av_get_bytes_per_sample(fmt))
, m_isPlanar(av_sample_fmt_is_planar(fmt))
, m_numPlanes(m_isPlanar ? channels : 1)
, m_sendFrame(std::move(sendFrame))
, m_bufs(m_numPlanes)
{}
int StageBuffer::bytesForSamples(int samples, int /*plane*/) const
{
return samples * m_bytesPerSample * (m_isPlanar ? 1 : m_channels);
}
/**
* @brief Resample and append audio to the staging buffer
*
* Input samples are converted using the swrcontext and append
* to the accumulation buffers. No encoder frames are emitted by
* this function; callers should invoke flush() to emit output frames
*
* @param srcData Input audio buffers in the format expected by
* the swrcontext
* @param nbSamples Number of input samples per channel
*
* @return true if conversion succeeded ; false if swr_convert() failed
* @note The number of samples may differ from the number of input samples due
* to resampling
*
* @author rattatwinko
* @date 25/06/2026
*/
bool StageBuffer::push(const uint8_t * const *srcData, int nbSamples)
{
int maxOut = swr_get_out_samples(m_swr, nbSamples);
if (maxOut <= 0) return true; // nothing to do
std::vector<uint8_t *> dstPtrs(m_numPlanes);
for (int pl = 0; pl < m_numPlanes; ++pl) {
int prevSz = static_cast<int>(m_bufs[pl].size());
m_bufs[pl].resize(prevSz + bytesForSamples(maxOut, pl));
dstPtrs[pl] = m_bufs[pl].data() + prevSz;
}
int converted = swr_convert(m_swr,
dstPtrs.data(), maxOut,
srcData, nbSamples);
for (int pl = 0; pl < m_numPlanes; ++pl) {
int keepSz = static_cast<int>(m_bufs[pl].size())
- bytesForSamples(maxOut - std::max(converted, 0), pl);
m_bufs[pl].resize(std::max(keepSz, 0));
}
return converted >= 0;
}
/**
* @brief Emit buffered samples as a encoder frame
*
* In normal mode (drain=false), frames are emitted only when least
* framesize samples are available.
*
* In drain mode (drain=true), all remaining samples are emitted,
* including a final partial frame. this is typically used at eos
*
* foreach emitted frame:
* - the encoder frame is made writeable.
* - samples are copied from the buffers
* - the callback supplied at construction is invoked
*
* @param drain Wether to flush partial frames.
* @return true if the frames callback reports failure, true otherwise
*
* @author rattatwinko
* @date 25/06/2026
*/
bool StageBuffer::flush(bool drain)
{
while (true) {
int avail = static_cast<int>(m_bufs[0].size())
/ (m_bytesPerSample * (m_isPlanar ? 1 : m_channels));
if (avail < (drain ? 1 : m_frameSize))
break;
int use = drain ? avail : m_frameSize;
av_frame_make_writable(m_encFrame);
m_encFrame->nb_samples = use;
for (int pl = 0; pl < m_numPlanes; ++pl) {
int bytes = bytesForSamples(use, pl);
std::memcpy(m_encFrame->data[pl], m_bufs[pl].data(), bytes);
m_bufs[pl].erase(m_bufs[pl].begin(), m_bufs[pl].begin() + bytes);
}
if (!m_sendFrame(m_encFrame))
return false;
}
return true;
}
} // namespace engine
+78
View File
@@ -0,0 +1,78 @@
#pragma once
#include <cstdint>
#include <functional>
#include <vector>
extern "C" {
#include <libavutil/samplefmt.h>
#include <libswresample/swresample.h>
}
namespace engine {
/**
* @brief Buffers resampled audio and emits encoder-sized frames
*
* The Buffer sits between a SWRContext resampler and a audio encoder.
* Incoming audio blocks may contain an arbitrary number of samples and
* resampling may change the sample count. This class accumulates the converted
* output until it has enough samples to fill an encoder frame,
* then it invokes a callback to send the frame to the encoder.
*
* Ownership:
* - The SWRContext and AVFrame are not owned by StageBuffer and must
* remain valid for the lifetime of the object.
* - The callback is invoked syncronously from the flush()
*
* Typical Usage:
* @code
* StageBuffer buffer(swr,
* encFrame,
* encoderFmt,
* channels,
* encoderFrameSize,
* sendFrame
* );
* buffer.push(srcDatam nbSamples);
* buffer.flush(false); // emit any full frames
*
* buffer.flush(true); // emit remaining samples
* @endcode
*
* @author rattatwinko
* @date 25/06/2026
*/
class StageBuffer
{
public:
using SendFrameFn = std::function<bool(AVFrame *)>;
StageBuffer(SwrContext *swr,
AVFrame *encFrame,
AVSampleFormat fmt,
int channels,
int frameSize,
SendFrameFn sendFrame);
bool push(const uint8_t * const *srcData, int nbSamples);
bool flush(bool drain);
private:
int bytesForSamples(int samples, int plane) const;
SwrContext *m_swr;
AVFrame *m_encFrame;
AVSampleFormat m_fmt;
int m_channels;
int m_frameSize;
int m_bytesPerSample;
bool m_isPlanar;
int m_numPlanes;
SendFrameFn m_sendFrame;
std::vector<std::vector<uint8_t>> m_bufs;
};
} // namespace engine
+306
View File
@@ -0,0 +1,306 @@
#include "TrackEncoder.h"
#include "FfmpegGuards.h"
#include "StageBuffer.h"
#include "SampleRateUtils.h"
#include <QDir>
#include <QFileInfo>
extern "C" {
#include <libavformat/avformat.h>
#include <libavcodec/avcodec.h>
#include <libavutil/avutil.h>
#include <libavutil/opt.h>
#include <libavutil/channel_layout.h>
#include <libswresample/swresample.h>
}
namespace engine {
namespace {
QString buildOutputPath(const ReencodeJob &job)
{
QString outDir = job.outputDir;
if (outDir.isEmpty())
outDir = QFileInfo(job.track.filePath).absolutePath();
QDir().mkpath(outDir);
QString base = QFileInfo(job.track.filePath).completeBaseName();
return outDir + QDir::separator() + base + "." + job.preset.extension;
}
AVSampleFormat pickSampleFormat(const AVCodec *encoder, const QString &codecName)
{
if (!encoder->sample_fmts)
return AV_SAMPLE_FMT_FLTP;
auto supports = [&](AVSampleFormat fmt) -> bool {
for (int i = 0; encoder->sample_fmts[i] != AV_SAMPLE_FMT_NONE; ++i)
if (encoder->sample_fmts[i] == fmt) return true;
return false;
};
if (codecName == "pcm_s16le")
return supports(AV_SAMPLE_FMT_S16) ? AV_SAMPLE_FMT_S16
: encoder->sample_fmts[0];
if (codecName == "pcm_s24le" || codecName == "pcm_s32le")
return supports(AV_SAMPLE_FMT_S32) ? AV_SAMPLE_FMT_S32
: encoder->sample_fmts[0];
if (supports(AV_SAMPLE_FMT_FLTP)) return AV_SAMPLE_FMT_FLTP;
return encoder->sample_fmts[0];
}
int pickChannelCount(const QString &codecName, int inChannels)
{
if (codecName == "libopus")
return std::min(inChannels, 2);
if (codecName == "libvorbis")
return std::min(inChannels, 8);
return inChannels;
}
int64_t clampBitrate(const QString &codecName, int64_t requestedKbps, int channels)
{
if (requestedKbps <= 0) return -1;
if (codecName == "libopus") {
// opus: 6510 kbps per channel
int64_t minTotal = 6LL * channels;
int64_t maxTotal = 510LL * channels;
return std::clamp(requestedKbps, minTotal, maxTotal);
}
if (codecName == "libmp3lame") {
return std::clamp(requestedKbps, INT64_C(8), INT64_C(320));
}
return requestedKbps;
}
} // anonymous namespace
TrackEncoder::TrackEncoder(const std::atomic<bool> &stopFlag)
: m_stop(stopFlag)
{}
bool TrackEncoder::encode(ReencodeJob &job) const
{
job.outputPath = buildOutputPath(job);
job.errorString = {};
FmtCtxIn in;
{
AVDictionary *opts = nullptr;
av_dict_set(&opts, "probesize", "5000000", 0);
av_dict_set(&opts, "analyzeduration", "5000000", 0);
int err = avformat_open_input(&in.p,
job.track.filePath.toUtf8().constData(),
nullptr, &opts);
av_dict_free(&opts);
if (err < 0) { job.errorString = "Cannot open input file"; return false; }
}
if (avformat_find_stream_info(in.p, nullptr) < 0) {
job.errorString = "Cannot read stream info"; return false;
}
int audioIdx = av_find_best_stream(in.p, AVMEDIA_TYPE_AUDIO, -1, -1, nullptr, 0);
if (audioIdx < 0) { job.errorString = "No audio stream found"; return false; }
AVStream *inStream = in.p->streams[audioIdx];
const AVCodec *decoder = avcodec_find_decoder(inStream->codecpar->codec_id);
if (!decoder) { job.errorString = "No decoder for input codec"; return false; }
CodecCtxGuard decCtx;
decCtx.p = avcodec_alloc_context3(decoder);
if (!decCtx.p) { job.errorString = "OOM: decoder context"; return false; }
avcodec_parameters_to_context(decCtx.p, inStream->codecpar);
decCtx.p->pkt_timebase = inStream->time_base;
if (avcodec_open2(decCtx.p, decoder, nullptr) < 0) {
job.errorString = "Cannot open decoder"; return false;
}
const int inChannels = decCtx.p->ch_layout.nb_channels;
const int inSampleRate = decCtx.p->sample_rate;
const AVCodec *encoder = avcodec_find_encoder_by_name(
job.preset.codecName.toUtf8().constData());
if (!encoder) {
job.errorString = QString("Encoder not found: %1").arg(job.preset.codecName);
return false;
}
// opus only accepts 8/12/16/24/48 kHz; 44100 must become 48000.
const int outSampleRate = pickSampleRate(encoder, inSampleRate);
const int outChannels = pickChannelCount(job.preset.codecName, inChannels);
const AVSampleFormat encFmt = pickSampleFormat(encoder, job.preset.codecName);
CodecCtxGuard encCtx;
encCtx.p = avcodec_alloc_context3(encoder);
if (!encCtx.p) { job.errorString = "OOM: encoder context"; return false; }
AVChannelLayout outLayout{};
av_channel_layout_default(&outLayout, outChannels);
av_channel_layout_copy(&encCtx.p->ch_layout, &outLayout);
encCtx.p->sample_rate = outSampleRate;
encCtx.p->sample_fmt = encFmt;
encCtx.p->time_base = { 1, outSampleRate };
{
int64_t clamped = clampBitrate(job.preset.codecName,
job.preset.bitrate, outChannels);
if (clamped > 0)
encCtx.p->bit_rate = clamped * 1000;
}
if (job.preset.vbrQuality >= 0) {
if (job.preset.codecName == "libmp3lame") {
av_opt_set_int(encCtx.p->priv_data, "q", job.preset.vbrQuality, 0);
encCtx.p->flags |= AV_CODEC_FLAG_QSCALE;
} else if (job.preset.codecName == "libvorbis") {
av_opt_set_double(encCtx.p->priv_data, "q", job.preset.vbrQuality, 0);
}
}
if (job.preset.compression > 0 && job.preset.codecName == "flac")
av_opt_set_int(encCtx.p->priv_data, "compression_level",
job.preset.compression, 0);
if (avcodec_open2(encCtx.p, encoder, nullptr) < 0) {
job.errorString = "Cannot open encoder"; return false;
}
FmtCtxOut out;
avformat_alloc_output_context2(&out.p, nullptr, nullptr,
job.outputPath.toUtf8().constData());
if (!out.p) { job.errorString = "Cannot allocate output context"; return false; }
AVStream *outStream = avformat_new_stream(out.p, nullptr);
if (!outStream) { job.errorString = "Cannot create output stream"; return false; }
avcodec_parameters_from_context(outStream->codecpar, encCtx.p);
outStream->time_base = encCtx.p->time_base;
av_dict_copy(&out.p->metadata, in.p->metadata, 0);
if (!(out.p->oformat->flags & AVFMT_NOFILE)) {
if (avio_open(&out.p->pb, job.outputPath.toUtf8().constData(),
AVIO_FLAG_WRITE) < 0) {
job.errorString = "Cannot open output file for writing"; return false;
}
}
if (avformat_write_header(out.p, nullptr) < 0) {
job.errorString = "Cannot write output header"; return false;
}
SwrGuard swr;
{
AVChannelLayout inLayout{};
av_channel_layout_copy(&inLayout, &decCtx.p->ch_layout);
int err = swr_alloc_set_opts2(&swr.p,
&outLayout, encFmt, outSampleRate,
&inLayout, decCtx.p->sample_fmt, inSampleRate,
0, nullptr);
av_channel_layout_uninit(&inLayout);
if (err < 0 || swr_init(swr.p) < 0) {
job.errorString = "Cannot initialise resampler"; return false;
}
}
av_channel_layout_uninit(&outLayout);
const int frameSize = (encCtx.p->frame_size > 0) ? encCtx.p->frame_size : 1152;
FrameGuard encFrame;
encFrame.p->format = encFmt;
encFrame.p->nb_samples = frameSize;
encFrame.p->sample_rate = outSampleRate;
av_channel_layout_copy(&encFrame.p->ch_layout, &encCtx.p->ch_layout);
if (av_frame_get_buffer(encFrame.p, 0) < 0) {
job.errorString = "Cannot allocate encode frame buffer"; return false;
}
qint64 ptsOut = 0;
auto sendFrameToEncoder = [&](AVFrame *f) -> bool {
if (f) {
f->pts = ptsOut;
ptsOut += f->nb_samples;
}
if (avcodec_send_frame(encCtx.p, f) < 0) return false;
PacketGuard outPkt;
int ret;
while ((ret = avcodec_receive_packet(encCtx.p, outPkt.p)) == 0) {
av_packet_rescale_ts(outPkt.p, encCtx.p->time_base, outStream->time_base);
outPkt.p->stream_index = 0;
av_interleaved_write_frame(out.p, outPkt.p);
av_packet_unref(outPkt.p);
}
return ret == AVERROR(EAGAIN) || ret == AVERROR_EOF;
};
StageBuffer stage(swr.p, encFrame.p, encFmt, outChannels, frameSize,
sendFrameToEncoder);
PacketGuard inPkt;
FrameGuard decFrame;
while (!m_stop) {
int ret = av_read_frame(in.p, inPkt.p);
if (ret < 0) break;
if (inPkt.p->stream_index != audioIdx) {
av_packet_unref(inPkt.p);
continue;
}
av_packet_rescale_ts(inPkt.p, inStream->time_base, decCtx.p->pkt_timebase);
avcodec_send_packet(decCtx.p, inPkt.p);
av_packet_unref(inPkt.p);
while (avcodec_receive_frame(decCtx.p, decFrame.p) == 0) {
if (!stage.push(const_cast<const uint8_t **>(decFrame.p->data),
decFrame.p->nb_samples)) {
job.errorString = "SWR convert error";
return false;
}
av_frame_unref(decFrame.p);
if (!stage.flush(false)) {
job.errorString = "Encode error during conversion";
return false;
}
}
}
if (m_stop) { job.errorString = "Cancelled"; return false; }
avcodec_send_packet(decCtx.p, nullptr);
while (avcodec_receive_frame(decCtx.p, decFrame.p) == 0) {
stage.push(const_cast<const uint8_t **>(decFrame.p->data),
decFrame.p->nb_samples);
av_frame_unref(decFrame.p);
stage.flush(false);
}
stage.push(nullptr, 0);
stage.flush(true);
sendFrameToEncoder(nullptr);
av_write_trailer(out.p);
return true;
}
} // namespace engine
+19
View File
@@ -0,0 +1,19 @@
#pragma once
#include "ReencodeJob.h"
#include <atomic>
namespace engine {
class TrackEncoder
{
public:
explicit TrackEncoder(const std::atomic<bool> &stopFlag);
bool encode(ReencodeJob &job) const;
private:
const std::atomic<bool> &m_stop;
};
} // namespace engine
+1 -1
View File
@@ -2,7 +2,7 @@
#include <QDialog>
#include <QList>
#include "engine/Track.h"
#include "engine/ReencodeJob.h"
#include "engine/encoder/ReencodeJob.h"
class QComboBox;
class QLineEdit;