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:
+12
-3
@@ -38,7 +38,10 @@ set(SOURCES
|
|||||||
src/engine/SpectrumAnalyzer.cpp
|
src/engine/SpectrumAnalyzer.cpp
|
||||||
src/engine/MusicLibrary.cpp
|
src/engine/MusicLibrary.cpp
|
||||||
src/engine/MetadataReader.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
|
# ffmpeg
|
||||||
src/engine/ffmpeg/FormatContext.cpp
|
src/engine/ffmpeg/FormatContext.cpp
|
||||||
src/engine/ffmpeg/CodecContext.cpp
|
src/engine/ffmpeg/CodecContext.cpp
|
||||||
@@ -74,8 +77,13 @@ set(HEADERS
|
|||||||
src/engine/Track.h
|
src/engine/Track.h
|
||||||
src/engine/MetadataReader.h
|
src/engine/MetadataReader.h
|
||||||
src/engine/MusicLibrary.h
|
src/engine/MusicLibrary.h
|
||||||
src/engine/ReencodeJob.h
|
# engine/encoder
|
||||||
src/engine/AlbumReencoder.h
|
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
|
# ffmpeg wrapper
|
||||||
src/engine/ffmpeg/FormatContext.h
|
src/engine/ffmpeg/FormatContext.h
|
||||||
src/engine/ffmpeg/CodecContext.h
|
src/engine/ffmpeg/CodecContext.h
|
||||||
@@ -111,6 +119,7 @@ target_include_directories(SubWave PRIVATE
|
|||||||
src/config
|
src/config
|
||||||
src/controllers
|
src/controllers
|
||||||
src/engine
|
src/engine
|
||||||
|
src/engine/encoder
|
||||||
src/engine/ffmpeg
|
src/engine/ffmpeg
|
||||||
src/playlists
|
src/playlists
|
||||||
src/gui
|
src/gui
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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: 6–510 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
|
||||||
@@ -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
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
#include <QDialog>
|
#include <QDialog>
|
||||||
#include <QList>
|
#include <QList>
|
||||||
#include "engine/Track.h"
|
#include "engine/Track.h"
|
||||||
#include "engine/ReencodeJob.h"
|
#include "engine/encoder/ReencodeJob.h"
|
||||||
|
|
||||||
class QComboBox;
|
class QComboBox;
|
||||||
class QLineEdit;
|
class QLineEdit;
|
||||||
|
|||||||
Reference in New Issue
Block a user