implement new audio engine

This commit is contained in:
2026-06-05 20:30:12 +02:00
parent 16efeb2fde
commit 61eb9a8be5
24 changed files with 1021 additions and 440 deletions
+6 -1
View File
@@ -55,4 +55,9 @@ compile_commands.json
*_qmlcache.qrc *_qmlcache.qrc
./build ./build
./dist
AppDir
*.AppImage
*.html
+16 -8
View File
@@ -4,10 +4,9 @@ project(SubWave LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_STANDARD_REQUIRED ON)
# ── Find Qt6 ─────────────────────────────────────────────────────────────────
find_package(Qt6 REQUIRED COMPONENTS Widgets Multimedia OpenGL OpenGLWidgets) find_package(Qt6 REQUIRED COMPONENTS Widgets Multimedia OpenGL OpenGLWidgets)
# ── Find FFmpeg via pkg-config ─────────────────────────────────────────────── # require ffmpeg
find_package(PkgConfig REQUIRED) find_package(PkgConfig REQUIRED)
pkg_check_modules(FFMPEG REQUIRED IMPORTED_TARGET pkg_check_modules(FFMPEG REQUIRED IMPORTED_TARGET
libavformat libavformat
@@ -15,17 +14,21 @@ pkg_check_modules(FFMPEG REQUIRED IMPORTED_TARGET
libavutil libavutil
libswresample libswresample
) )
# require fftw (fourier transform)
pkg_check_modules(FFTW REQUIRED IMPORTED_TARGET fftw3)
# Auto-generate MOC, and now also RCC for resource files
set(CMAKE_AUTOMOC ON) set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON) set(CMAKE_AUTORCC ON)
# ── Sources ────────────────────────────────────────────────────────────────── # source files
set(SOURCES set(SOURCES
src/main.cpp src/main.cpp
src/MetadataReader.cpp src/MetadataReader.cpp
src/Config.cpp src/Config.cpp
src/AudioEngine.cpp src/engine/AudioEngine.cpp
src/engine/FFmpegDecoder.cpp
src/engine/EqProcessor.cpp
src/engine/SpectrumAnalyzer.cpp
src/MusicLibrary.cpp src/MusicLibrary.cpp
src/CoverArtWidget.cpp src/CoverArtWidget.cpp
src/SpectrogramWidget.cpp src/SpectrogramWidget.cpp
@@ -40,7 +43,7 @@ set(HEADERS
src/Track.h src/Track.h
src/MetadataReader.h src/MetadataReader.h
src/Config.h src/Config.h
src/AudioEngine.h src/engine/AudioEngine.h
src/MusicLibrary.h src/MusicLibrary.h
src/CoverArtWidget.h src/CoverArtWidget.h
src/SpectrogramWidget.h src/SpectrogramWidget.h
@@ -51,19 +54,24 @@ set(HEADERS
src/MainWindow.h src/MainWindow.h
) )
set(RESOURCES src/shaders.qrc) set(RESOURCES
src/shaders.qrc
src/application.qrc
)
add_executable(SubWave ${SOURCES} ${HEADERS} ${RESOURCES}) add_executable(SubWave ${SOURCES} ${HEADERS} ${RESOURCES})
target_include_directories(SubWave PRIVATE src src/engine)
target_link_libraries(SubWave PRIVATE target_link_libraries(SubWave PRIVATE
Qt6::Widgets Qt6::Widgets
Qt6::Multimedia Qt6::Multimedia
Qt6::OpenGL Qt6::OpenGL
Qt6::OpenGLWidgets Qt6::OpenGLWidgets
PkgConfig::FFMPEG PkgConfig::FFMPEG
PkgConfig::FFTW
) )
# ── Copy FFmpeg shared libs next to the binary on Windows ────────────────────
if(WIN32) if(WIN32)
add_custom_command(TARGET SubWave POST_BUILD add_custom_command(TARGET SubWave POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different COMMAND ${CMAKE_COMMAND} -E copy_if_different
Executable
+6
View File
@@ -0,0 +1,6 @@
rm -rf build
mkdir build
cd build
cmake ..
make -j$(nproc)
./SubWave
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 648 B

Executable
+86
View File
@@ -0,0 +1,86 @@
#!/usr/bin/env bash
set -e
APP_NAME="SubWave"
BUILD_DIR="build"
APPDIR="AppDir"
DIST_DIR="dist"
ICON_SRC_PNG="ico/wavy_png.png"
ICON_SRC_ICO="ico/wavy_ico.ico"
echo "==> Cleaning old build"
rm -rf "$BUILD_DIR" "$APPDIR" "$DIST_DIR"
mkdir -p "$DIST_DIR"
echo "==> Configuring project"
cmake -B "$BUILD_DIR" -DCMAKE_BUILD_TYPE=Release
echo "==> Building project"
cmake --build "$BUILD_DIR" -j$(nproc)
echo "==> Installing into AppDir"
cmake --install "$BUILD_DIR" --prefix "$APPDIR/usr"
echo "==> Ensuring binary location"
mkdir -p "$APPDIR/usr/bin"
if [ ! -f "$APPDIR/usr/bin/$APP_NAME" ]; then
cp "$BUILD_DIR/$APP_NAME" "$APPDIR/usr/bin/" || true
fi
echo "==> Creating desktop file (REQUIRED for AppImage)"
mkdir -p "$APPDIR/usr/share/applications"
cat > "$APPDIR/usr/share/applications/$APP_NAME.desktop" <<EOF
[Desktop Entry]
Type=Application
Name=SubWave
Exec=SubWave
Icon=subwave
Categories=AudioVideo;Audio;Player;
Terminal=false
EOF
echo "==> Installing icon"
ICON_DEST_DIR="$APPDIR/usr/share/icons/hicolor/256x256/apps"
mkdir -p "$ICON_DEST_DIR"
if [ -f "$ICON_SRC_PNG" ]; then
cp "$ICON_SRC_PNG" "$ICON_DEST_DIR/subwave.png"
elif [ -f "$ICON_SRC_ICO" ]; then
if command -v convert >/dev/null 2>&1; then
convert "$ICON_SRC_ICO[0]" "$ICON_DEST_DIR/subwave.png"
else
echo "WARNING: ImageMagick not installed, copying .ico as fallback"
cp "$ICON_SRC_ICO" "$ICON_DEST_DIR/subwave.ico"
fi
else
echo "WARNING: No icon found, build will still work but no icon will be shown"
fi
echo "==> Downloading linuxdeploy if needed"
if [ ! -f linuxdeploy-x86_64.AppImage ]; then
wget -q https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage
chmod +x linuxdeploy-x86_64.AppImage
fi
echo "==> Building AppImage"
export ARCH=x86_64
./linuxdeploy-x86_64.AppImage \
--appdir "$APPDIR" \
--output appimage
echo "==> Moving output"
mkdir -p "$DIST_DIR"
mv *.AppImage "$DIST_DIR/$APP_NAME.AppImage"
echo "==> DONE"
echo "Run:"
echo " chmod +x dist/$APP_NAME.AppImage"
echo " ./dist/$APP_NAME.AppImage"
+42
View File
@@ -0,0 +1,42 @@
#!/usr/bin/env bash
set -e
echo "Installing build tools..."
sudo apt install -y \
build-essential \
cmake \
ninja-build \
pkg-config \
git
echo "Installing Qt6 dependencies..."
sudo apt install -y \
qt6-base-dev \
qt6-base-dev-tools \
qt6-multimedia-dev \
qt6-tools-dev \
qt6-tools-dev-tools \
libqt6opengl6-dev \
libqt6openglwidgets6
echo "Installing FFmpeg development libraries..."
sudo apt install -y \
libavformat-dev \
libavcodec-dev \
libavutil-dev \
libswresample-dev \
libswscale-dev \
ffmpeg
echo "Installing FFTW3..."
sudo apt install -y \
libfftw3-dev
echo "Installing OpenGL dependencies..."
sudo apt install -y \
libgl1-mesa-dev \
libglu1-mesa-dev
echo "Verifying pkg-config modules..."
pkg-config --modversion libavformat || echo "FFmpeg pkg-config missing"
pkg-config --modversion fftw3 || echo "FFTW pkg-config missing"
-355
View File
@@ -1,355 +0,0 @@
#include "AudioEngine.h"
#include <cmath>
#include <numbers>
#include <algorithm>
#include <QThread>
#include <QMediaDevices>
#include <QAudioDevice>
#include <QAudioFormat>
extern "C" {
#include <libavformat/avformat.h>
#include <libavcodec/avcodec.h>
#include <libavutil/avutil.h>
#include <libswresample/swresample.h>
}
AudioEngine::AudioEngine(QObject *parent) : QObject(parent) {
initEq();
}
AudioEngine::~AudioEngine() { stop(); }
AudioEngine::State AudioEngine::state() const { return m_state.load(); }
qint64 AudioEngine::positionFrames() const { return m_posFrames.load(); }
qint64 AudioEngine::totalFrames() const { return m_totalFrames.load(); }
int AudioEngine::sampleRate() const { return m_sampleRate.load(); }
void AudioEngine::setVolume(float v) {
m_volume = std::clamp(v, 0.f, 1.f);
if (m_sink) m_sink->setVolume(m_volume);
}
void AudioEngine::play(const Track &track) {
stop();
m_stopReq = false;
m_pauseReq = false;
m_track = track;
m_thread = QThread::create([this]{ playbackLoop(); });
m_thread->setObjectName("subwave-audio");
m_thread->start(QThread::HighestPriority);
}
void AudioEngine::togglePause() {
if (m_state == State::Playing) {
m_pauseReq = true;
m_state = State::Paused;
} else if (m_state == State::Paused) {
m_pauseReq = false;
m_state = State::Playing;
m_pauseCv.wakeAll();
}
}
void AudioEngine::stop() {
m_stopReq = true;
m_pauseReq = false;
m_pauseCv.wakeAll();
if (m_thread) {
m_thread->quit();
m_thread->wait(3000);
delete m_thread;
m_thread = nullptr;
}
m_state = State::Stopped;
m_posFrames = 0;
}
void AudioEngine::seekFraction(double fraction) {
m_seekFraction = std::clamp(fraction, 0.0, 1.0);
m_seekPending = true;
}
void AudioEngine::setEqBand(int band, float db) {
if (band < 0 || band >= EQ_BANDS) return;
m_eqGainDb[band] = db;
recomputeBiquad(band);
}
float AudioEngine::eqBand(int band) const {
return (band >= 0 && band < EQ_BANDS) ? m_eqGainDb[band] : 0.f;
}
void AudioEngine::initEq() {
for (int b = 0; b < EQ_BANDS; b++) { m_eqGainDb[b] = 0.f; recomputeBiquad(b); }
}
void AudioEngine::recomputeBiquad(int band) {
float sr = static_cast<float>(m_sampleRate.load());
float f0 = EQ_FREQS[band];
float dBg = m_eqGainDb[band];
float Q = 1.41421356f;
float A = std::pow(10.f, dBg / 40.f);
double w0 = 2.0 * std::numbers::pi * f0 / sr;
double cosW = std::cos(w0), sinW = std::sin(w0);
double alpha= sinW / (2.0 * Q);
double b0 = 1+alpha*A, b1 = -2*cosW, b2 = 1-alpha*A;
double a0 = 1+alpha/A, a1 = -2*cosW, a2 = 1-alpha/A;
m_bqCoeff[band][0] = float(b0/a0); m_bqCoeff[band][1] = float(b1/a0);
m_bqCoeff[band][2] = float(b2/a0); m_bqCoeff[band][3] = float(a1/a0);
m_bqCoeff[band][4] = float(a2/a0);
}
void AudioEngine::applyEq(float *samples, int count, int channels) {
for (int band = 0; band < EQ_BANDS; band++) {
if (std::abs(m_eqGainDb[band]) < 0.01f) continue;
float b0=m_bqCoeff[band][0], b1=m_bqCoeff[band][1], b2=m_bqCoeff[band][2];
float a1=m_bqCoeff[band][3], a2=m_bqCoeff[band][4];
for (int i = 0; i < count; i++) {
int ch = i % channels;
float x = samples[i];
float z1 = m_bqState[band][ch][0], z2 = m_bqState[band][ch][1];
float y = b0*x + z1;
m_bqState[band][ch][0] = b1*x - a1*y + z2;
m_bqState[band][ch][1] = b2*x - a2*y;
samples[i] = y;
}
}
}
void AudioEngine::resetBiquadState() {
for (auto &b : m_bqState) for (auto &c : b) { c[0]=0; c[1]=0; }
}
void AudioEngine::feedFft(const float *samples, int count, int channels) {
for (int i = 0; i < count; i += channels) {
float mono = 0;
for (int c = 0; c < channels; c++) mono += samples[i+c];
mono /= channels;
m_fftBuf[m_fftPos++] = mono;
if (m_fftPos == FFT_SIZE) {
QVector<float> mag = computeFFT(m_fftBuf, FFT_SIZE);
emit fftReady(mag);
std::copy(m_fftBuf + FFT_SIZE/2, m_fftBuf + FFT_SIZE, m_fftBuf);
m_fftPos = FFT_SIZE / 2;
}
}
}
QVector<float> AudioEngine::computeFFT(const float *input, int n) {
std::vector<float> re(input, input + n), im(n, 0.f);
for (int i = 0; i < n; i++)
re[i] *= 0.5f * (1.f - std::cos(2.f * std::numbers::pi_v<float> * i / (n-1)));
for (int j=1, i=0; j<n; j++) {
int bit = n>>1;
for (; (i&bit)!=0; bit>>=1) i^=bit;
i^=bit;
if (j<i) { std::swap(re[j],re[i]); std::swap(im[j],im[i]); }
}
for (int len=2; len<=n; len<<=1) {
double angle = -2.0*std::numbers::pi/len;
float wRe = std::cos(angle), wIm = std::sin(angle);
for (int i=0; i<n; i+=len) {
float cRe=1, cIm=0;
for (int j=0; j<len/2; j++) {
float uRe=re[i+j], uIm=im[i+j];
float vRe=re[i+j+len/2]*cRe - im[i+j+len/2]*cIm;
float vIm=re[i+j+len/2]*cIm + im[i+j+len/2]*cRe;
re[i+j]=uRe+vRe; im[i+j]=uIm+vIm;
re[i+j+len/2]=uRe-vRe; im[i+j+len/2]=uIm-vIm;
float nRe=cRe*wRe-cIm*wIm; cIm=cRe*wIm+cIm*wRe; cRe=nRe;
}
}
}
QVector<float> mag(n/2);
for (int i=0; i<n/2; i++)
mag[i] = std::sqrt(re[i]*re[i]+im[i]*im[i]) / n;
return mag;
}
void AudioEngine::playbackLoop() {
m_state = State::Playing;
AVFormatContext *fmt = nullptr;
AVCodecContext *codec = nullptr;
SwrContext *swr = nullptr;
AVPacket *pkt = av_packet_alloc();
AVFrame *frame = av_frame_alloc();
auto cleanup = [&]{
av_frame_free(&frame);
av_packet_free(&pkt);
swr_free(&swr);
if (codec) avcodec_free_context(&codec);
if (fmt) avformat_close_input(&fmt);
QAudioSink *sinkToDelete = m_sink;
m_sink = nullptr;
m_sinkIO = nullptr;
if (sinkToDelete) {
QMetaObject::invokeMethod(this, [sinkToDelete]{
sinkToDelete->stop();
delete sinkToDelete;
}, Qt::QueuedConnection);
}
m_state = State::Stopped;
};
AVDictionary *opts = nullptr;
av_dict_set(&opts, "probesize", "5000000", 0);
av_dict_set(&opts, "analyzeduration", "5000000", 0);
if (avformat_open_input(&fmt, m_track.filePath.toUtf8().constData(),
nullptr, &opts) < 0) {
av_dict_free(&opts);
qWarning() << "[SubWave] Cannot open:" << m_track.filePath;
cleanup(); return;
}
av_dict_free(&opts);
avformat_find_stream_info(fmt, nullptr);
if (fmt->duration != AV_NOPTS_VALUE && fmt->duration > 0) {
qint64 secs = fmt->duration / AV_TIME_BASE;
if (m_track.durationSecs == 0) m_track.durationSecs = secs;
emit durationKnown(m_track.durationSecs);
}
int audioIdx = av_find_best_stream(fmt, AVMEDIA_TYPE_AUDIO, -1, -1, nullptr, 0);
if (audioIdx < 0) {
qWarning() << "[SubWave] No audio stream:" << m_track.filePath;
cleanup(); return;
}
AVStream *stream = fmt->streams[audioIdx];
const AVCodec *dec = avcodec_find_decoder(stream->codecpar->codec_id);
codec = avcodec_alloc_context3(dec);
avcodec_parameters_to_context(codec, stream->codecpar);
if (avcodec_open2(codec, dec, nullptr) < 0) {
qWarning() << "[SubWave] Cannot open codec";
cleanup(); return;
}
const int srcRate = codec->sample_rate;
const int outRate = srcRate;
const int channels = codec->ch_layout.nb_channels;
m_sampleRate = outRate;
if (m_totalFrames == 0 && m_track.durationSecs > 0)
m_totalFrames = m_track.durationSecs * outRate;
AVChannelLayout chLayout {};
av_channel_layout_default(&chLayout, channels);
swr_alloc_set_opts2(&swr,
&chLayout, AV_SAMPLE_FMT_FLT, outRate,
&codec->ch_layout, codec->sample_fmt, srcRate,
0, nullptr);
swr_init(swr);
QAudioFormat af;
af.setSampleRate(outRate);
af.setChannelCount(channels);
af.setSampleFormat(QAudioFormat::Float);
QSemaphore sinkReady(0);
QMetaObject::invokeMethod(this, [&]{
QAudioDevice dev = QMediaDevices::defaultAudioOutput();
m_sink = new QAudioSink(dev, af);
m_sink->setVolume(m_volume);
m_sinkIO = m_sink->start();
sinkReady.release();
}, Qt::QueuedConnection);
sinkReady.acquire();
if (!m_sinkIO) {
qWarning() << "[SubWave] QAudioSink failed to start";
cleanup(); return;
}
resetBiquadState();
m_posFrames = 0;
std::vector<float> outBuf;
while (!m_stopReq) {
{
QMutexLocker lk(&m_pauseMtx);
while (m_pauseReq && !m_stopReq)
m_pauseCv.wait(&m_pauseMtx, 50);
}
if (m_stopReq) break;
if (m_seekPending) {
m_seekPending = false;
double frac = m_seekFraction.load();
qint64 durUs = (fmt->duration != AV_NOPTS_VALUE && fmt->duration > 0)
? fmt->duration
: (m_track.durationSecs * AV_TIME_BASE);
if (durUs > 0) {
qint64 targetUs = static_cast<qint64>(frac * durUs);
av_seek_frame(fmt, -1, targetUs, AVSEEK_FLAG_BACKWARD);
avcodec_flush_buffers(codec);
m_posFrames = static_cast<qint64>(frac * m_totalFrames.load());
}
resetBiquadState();
continue;
}
int ret = av_read_frame(fmt, pkt);
if (ret < 0) break;
if (pkt->stream_index != audioIdx) {
av_packet_unref(pkt);
continue;
}
avcodec_send_packet(codec, pkt);
av_packet_unref(pkt);
while (!m_stopReq) {
ret = avcodec_receive_frame(codec, frame);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) break;
if (ret < 0) break;
int outSamples = swr_get_out_samples(swr, frame->nb_samples);
outBuf.resize(static_cast<size_t>(outSamples) * channels);
uint8_t *outPtr = reinterpret_cast<uint8_t*>(outBuf.data());
int converted = swr_convert(swr,
&outPtr, outSamples,
const_cast<const uint8_t**>(frame->data), frame->nb_samples);
if (converted <= 0) { av_frame_unref(frame); continue; }
int count = converted * channels;
outBuf.resize(static_cast<size_t>(count));
applyEq(outBuf.data(), count, channels);
feedFft(outBuf.data(), count, channels);
const char *src = reinterpret_cast<const char*>(outBuf.data());
qsizetype bytes = count * sizeof(float);
qsizetype written = 0;
while (written < bytes && !m_stopReq) {
qsizetype n = m_sinkIO->write(src + written, bytes - written);
if (n <= 0) QThread::msleep(1);
else written += n;
}
m_posFrames += converted;
emit positionChanged(m_posFrames.load());
if (m_totalFrames == 0 && stream->duration != AV_NOPTS_VALUE) {
double tb = av_q2d(stream->time_base);
m_totalFrames = static_cast<qint64>(stream->duration * tb * outRate);
}
av_frame_unref(frame);
}
}
bool natural = !m_stopReq;
cleanup();
if (natural) emit trackEnded();
}
+111 -74
View File
@@ -5,13 +5,14 @@
#include <QMessageBox> #include <QMessageBox>
#include <QInputDialog> #include <QInputDialog>
#include <QFileDialog> #include <QFileDialog>
#include <QStyleFactory>
#include <QFile> #include <QFile>
#include <QDir> #include <QDir>
#include <QPainter>
#include <QPainterPath>
#include <QJsonDocument> #include <QJsonDocument>
#include <QJsonObject> #include <QJsonObject>
#include <QJsonArray> #include <QJsonArray>
#include <QPainter>
#include <QPainterPath>
#include <algorithm> #include <algorithm>
#include <cmath> #include <cmath>
@@ -41,6 +42,7 @@ EqualizerWidget::EqualizerWidget(AudioEngine *engine, QWidget *parent)
auto *topBar = new QHBoxLayout; auto *topBar = new QHBoxLayout;
topBar->addWidget(new QLabel("Preset:")); topBar->addWidget(new QLabel("Preset:"));
m_presetBox = new QComboBox; m_presetBox = new QComboBox;
m_presetBox->setStyle(QStyleFactory::create("Fusion"));
topBar->addWidget(m_presetBox); topBar->addWidget(m_presetBox);
auto *saveBtn = new QPushButton("Save..."); auto *saveBtn = new QPushButton("Save...");
auto *deleteBtn = new QPushButton("Delete"); auto *deleteBtn = new QPushButton("Delete");
@@ -65,7 +67,11 @@ EqualizerWidget::EqualizerWidget(AudioEngine *engine, QWidget *parent)
m_dbLabels[i] = new QLabel("0 dB"); m_dbLabels[i] = new QLabel("0 dB");
m_dbLabels[i]->setAlignment(Qt::AlignHCenter); m_dbLabels[i]->setAlignment(Qt::AlignHCenter);
QFont sf = m_dbLabels[i]->font(); sf.setPointSizeF(8); m_dbLabels[i]->setFont(sf); // Fixed width and monospace to prevent left/right shifting
QFont sf("Monospace", 8);
sf.setStyleHint(QFont::Monospace);
m_dbLabels[i]->setFont(sf);
m_dbLabels[i]->setFixedWidth(50); // ample room for "-12 dB"
m_sliders[i] = new QSlider(Qt::Vertical); m_sliders[i] = new QSlider(Qt::Vertical);
m_sliders[i]->setRange(-120, 120); m_sliders[i]->setRange(-120, 120);
@@ -97,8 +103,16 @@ EqualizerWidget::EqualizerWidget(AudioEngine *engine, QWidget *parent)
populatePresetBox(); populatePresetBox();
loadState(); loadState();
connect(m_presetBox, QOverload<int>::of(&QComboBox::currentIndexChanged), connect(m_presetBox, QOverload<int>::of(&QComboBox::activated),
this, &EqualizerWidget::applyPresetAt); this, [this](int idx) {
m_presetBox->hidePopup(); // force-close before anything else
m_presetBox->setEnabled(false);
QMetaObject::invokeMethod(this, [this, idx] {
applyPresetAt(idx);
m_presetBox->setEnabled(true);
}, Qt::QueuedConnection);
});
connect(saveBtn, &QPushButton::clicked, this, [this]{ connect(saveBtn, &QPushButton::clicked, this, [this]{
bool ok = false; bool ok = false;
QString name = QInputDialog::getText(this, "Save Preset", "Preset name:", QLineEdit::Normal, "", &ok); QString name = QInputDialog::getText(this, "Save Preset", "Preset name:", QLineEdit::Normal, "", &ok);
@@ -126,44 +140,43 @@ EqualizerWidget::EqualizerWidget(AudioEngine *engine, QWidget *parent)
savePresets(); savePresets();
populatePresetBox(); populatePresetBox();
}); });
connect(resetBtn, &QPushButton::clicked, this, [this]{ applyPresetAt(0); }); connect(resetBtn, &QPushButton::clicked, this, [this]{
m_presetBox->hidePopup();
m_presetBox->setCurrentIndex(0);
m_presetBox->setEnabled(false);
QMetaObject::invokeMethod(this, [this]{
applyPresetAt(0);
m_presetBox->setEnabled(true);
}, Qt::QueuedConnection);
});
} }
void EqualizerWidget::saveState() { void EqualizerWidget::saveState() {
QDir().mkpath(QFileInfo(stateFile()).absolutePath()); QDir().mkpath(QFileInfo(stateFile()).absolutePath());
QFile f(stateFile()); QFile f(stateFile());
if (!f.open(QIODevice::WriteOnly|QIODevice::Truncate)) return; if (!f.open(QIODevice::WriteOnly|QIODevice::Truncate)) return;
QJsonObject obj;
QString vals; QString vals;
for (int i=0; i<BANDS; i++) { for (int i=0; i<BANDS; i++) {
if (i) vals += ','; if (i) vals += ',';
vals += QString::number(m_sliders[i]->value()); vals += QString::number(m_sliders[i]->value());
} }
f.write(("{\n \"bands\": \"" + vals + "\"\n}\n").toUtf8()); obj["bands"] = vals;
f.write(QJsonDocument(obj).toJson());
} }
void EqualizerWidget::loadPresets() { void EqualizerWidget::loadPresets() {
m_userPresets.clear();
QFile f(presetFile()); QFile f(presetFile());
if (!f.open(QIODevice::ReadOnly)) return; if (!f.open(QIODevice::ReadOnly)) return;
QString json = QString::fromUtf8(f.readAll()); QJsonDocument doc = QJsonDocument::fromJson(f.readAll());
int pos = 0; m_userPresets.clear();
while (pos < json.size()) { if (!doc.isArray()) return;
int ns = json.indexOf("\"name\"", pos); if (ns<0) break; for (const QJsonValue &v : doc.array()) {
int nq1= json.indexOf('"', ns+7); if (nq1<0) break; QJsonObject obj = v.toObject();
int nq2= nq1+1; QString name = obj["name"].toString();
while (nq2<json.size() && json[nq2]!='"') { QString bands = obj["bands"].toString();
if (json[nq2]=='\\') nq2++; nq2++; if (!name.isEmpty() && !bands.isEmpty())
} m_userPresets.append({name, bands});
QString name = json.mid(nq1+1, nq2-nq1-1);
int bs = json.indexOf("\"bands\"", nq2); if (bs<0) break;
int bq1= json.indexOf('"', bs+8); if (bq1<0) break;
int bq2= bq1+1;
while (bq2<json.size() && json[bq2]!='"') bq2++;
QString bands = json.mid(bq1+1, bq2-bq1-1);
m_userPresets.append({name, bands});
pos = bq2+1;
} }
} }
@@ -171,18 +184,14 @@ void EqualizerWidget::savePresets() {
QDir().mkpath(QFileInfo(presetFile()).absolutePath()); QDir().mkpath(QFileInfo(presetFile()).absolutePath());
QFile f(presetFile()); QFile f(presetFile());
if (!f.open(QIODevice::WriteOnly|QIODevice::Truncate)) return; if (!f.open(QIODevice::WriteOnly|QIODevice::Truncate)) return;
QString json = "[\n"; QJsonArray arr;
for (int i=0; i<m_userPresets.size(); i++) { for (const auto &pr : m_userPresets) {
auto escq = [](const QString &s){ QJsonObject obj;
return QString(s).replace("\\","\\\\").replace("\"","\\\""); obj["name"] = pr.first;
}; obj["bands"] = pr.second;
json += " {\"name\":\"" + escq(m_userPresets[i].first) arr.append(obj);
+ "\",\"bands\":\"" + m_userPresets[i].second + "\"}";
if (i+1<m_userPresets.size()) json += ",";
json += "\n";
} }
json += "]\n"; f.write(QJsonDocument(arr).toJson());
f.write(json.toUtf8());
} }
void EqualizerWidget::populatePresetBox() { void EqualizerWidget::populatePresetBox() {
@@ -215,18 +224,16 @@ void EqualizerWidget::applyPresetAt(int idx) {
} }
} }
m_curve->update(); m_curve->update();
m_presetBox->hidePopup();
} }
void EqualizerWidget::loadState() { void EqualizerWidget::loadState() {
QFile f(stateFile()); QFile f(stateFile());
if (!f.open(QIODevice::ReadOnly)) return; if (!f.open(QIODevice::ReadOnly)) return;
QString json = QString::fromUtf8(f.readAll()); QJsonDocument doc = QJsonDocument::fromJson(f.readAll());
int bi = json.indexOf("\"bands\""); if (bi<0) return; if (!doc.isObject()) return;
int q1 = json.indexOf('"', bi+8); if (q1<0) return; QJsonObject obj = doc.object();
int q2 = q1+1; if (!obj.contains("bands")) return;
while (q2<json.size() && json[q2]!='"') q2++; QString vals = obj["bands"].toString();
QString vals = json.mid(q1+1, q2-q1-1);
QStringList parts = vals.split(','); QStringList parts = vals.split(',');
for (int i=0; i<BANDS && i<parts.size(); i++) { for (int i=0; i<BANDS && i<parts.size(); i++) {
bool ok=false; bool ok=false;
@@ -247,43 +254,73 @@ void EqualizerWidget::FreqCurveWidget::paintEvent(QPaintEvent *) {
if (!m_sliders) return; if (!m_sliders) return;
QPainter p(this); QPainter p(this);
p.setRenderHint(QPainter::Antialiasing); p.setRenderHint(QPainter::Antialiasing);
int w=width(), h=height(); int w = width(), h = height();
const int labelArea = 20;
int graphH = h - labelArea;
p.fillRect(rect(), palette().base()); p.fillRect(rect(), palette().base());
float midY = h/2.f; float logMin = std::log10(20.f);
p.setPen(QColor(180,180,180)); float logMax = std::log10(20000.f);
p.drawLine(0,int(midY),w,int(midY)); auto freqToX = [&](float f) -> float {
return (std::log10(f) - logMin) / (logMax - logMin) * w;
};
float logMin = std::log10(20.f), logMax = std::log10(20000.f); // 0 dB line
float midY = graphH / 2.f;
p.setPen(QColor(180, 180, 180));
p.drawLine(0, int(midY), w, int(midY));
QPainterPath path; // Draw frequency axis ticks and labels
for (int i=0; i<w; i++) { const float tickFreqs[] = {20, 50, 100, 200, 500, 1000, 2000, 5000, 10000, 20000};
float f = std::pow(10.f, logMin+(logMax-logMin)*i/w); QFont tickFont = font();
double totalDb = 0; tickFont.setPointSizeF(7);
for (int b=0; b<BANDS; b++) { p.setFont(tickFont);
float db = m_sliders[b]->value()/10.f; p.setPen(palette().windowText().color());
if (std::abs(db)<0.01f) continue; for (float fq : tickFreqs) {
float f0 = AudioEngine::EQ_FREQS[b], Q=1.41421356f; float x = freqToX(fq);
double ratio = f/f0; if (x < 0 || x > w) continue;
double lrSq = (ratio-1.0/ratio)*(ratio-1.0/ratio); // Tick mark
double denom = 1+lrSq/(Q*Q*(ratio+1.0/ratio)*(ratio+1.0/ratio)); p.drawLine(QPointF(x, graphH), QPointF(x, graphH + 3));
totalDb += db*denom; // Label
} QString label;
float py = midY - float(totalDb/12.0*h/2*0.9); if (fq >= 1000) label = QString::number(fq / 1000, 'f', 0) + "k";
py = std::clamp(py, 0.f, float(h)); else label = QString::number(fq, 'f', 0);
if (i==0) path.moveTo(i,py); else path.lineTo(i,py); QRectF textRect(x - 20, graphH + 3, 40, labelArea - 3);
p.drawText(textRect, Qt::AlignHCenter | Qt::AlignTop, label);
} }
p.setPen(QPen(palette().windowText().color(), 1.5f));
// Draw EQ curve
QPainterPath path;
for (int i = 0; i < w; ++i) {
float f = std::pow(10.f, logMin + (logMax - logMin) * i / w);
double totalDb = 0.0;
for (int b = 0; b < BANDS; ++b) {
float db = m_sliders[b]->value() / 10.f;
if (std::abs(db) < 0.01f) continue;
float f0 = AudioEngine::EQ_FREQS[b], Q = 1.41421356f;
double ratio = f / f0;
double lrSq = (ratio - 1.0 / ratio) * (ratio - 1.0 / ratio);
double denom = 1.0 + lrSq / (Q * Q * (ratio + 1.0 / ratio) * (ratio + 1.0 / ratio));
totalDb += db * denom;
}
float py = midY - float(totalDb / 12.0 * graphH / 2 * 0.9);
py = std::clamp(py, 0.f, float(graphH));
if (i == 0) path.moveTo(i, py);
else path.lineTo(i, py);
}
p.setPen(QPen(palette().windowText().color(), 1.5));
p.drawPath(path); p.drawPath(path);
// Draw control points (band handles)
p.setPen(Qt::NoPen); p.setPen(Qt::NoPen);
p.setBrush(QColor(60,120,255)); p.setBrush(QColor(60, 120, 255));
for (int b=0; b<BANDS; b++) { for (int b = 0; b < BANDS; ++b) {
float f0 = AudioEngine::EQ_FREQS[b]; float f0 = AudioEngine::EQ_FREQS[b];
float db = m_sliders[b]->value()/10.f; float db = m_sliders[b]->value() / 10.f;
float nx = (std::log10(f0)-logMin)/(logMax-logMin)*w; float nx = freqToX(f0);
float ny = midY-(db/12.f*h/2*0.9f); float ny = midY - (db / 12.f * graphH / 2 * 0.9f);
ny = std::clamp(ny,0.f,float(h)); ny = std::clamp(ny, 0.f, float(graphH));
p.drawEllipse(QPointF(nx,ny),3,3); p.drawEllipse(QPointF(nx, ny), 3, 3);
} }
} }
+3
View File
@@ -4,6 +4,9 @@
#include <QSlider> #include <QSlider>
#include <QLabel> #include <QLabel>
#include <QPushButton> #include <QPushButton>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
#include "AudioEngine.h" #include "AudioEngine.h"
class EqualizerWidget : public QWidget { class EqualizerWidget : public QWidget {
+6
View File
@@ -0,0 +1,6 @@
<RCC>
<qresource prefix="/icons">
<file>ico/wavy_ico.ico</file>
<file>ico/wavy_png.png</file>
</qresource>
</RCC>
+29
View File
@@ -0,0 +1,29 @@
#pragma once
#include <vector>
#include <cstdint>
namespace engine {
struct AudioBuffer {
std::vector<float> samples;
int sampleRate { 0 };
int channels { 0 };
int frameCount() const {
return channels > 0 ? static_cast<int>(samples.size()) / channels : 0;
}
bool empty() const { return samples.empty(); }
void resize(int frames) {
samples.resize(static_cast<std::size_t>(frames) * channels);
}
void truncate(int frames) {
samples.resize(static_cast<std::size_t>(frames) * channels);
}
int sampleCount() const { return static_cast<int>(samples.size()); }
};
} // namespace engine
+201
View File
@@ -0,0 +1,201 @@
#include "AudioEngine.h"
#include "engine/FFmpegDecoder.h"
#include "engine/EqProcessor.h"
#include "engine/SpectrumAnalyzer.h"
#include <cmath>
#include <numbers>
#include <algorithm>
#include <QThread>
#include <QMediaDevices>
#include <QAudioDevice>
#include <QAudioFormat>
#include <QSemaphore>
#include <QMutexLocker>
AudioEngine::AudioEngine(QObject *parent) : QObject(parent) {
initEq();
}
AudioEngine::~AudioEngine() { stop(); }
AudioEngine::State AudioEngine::state() const { return m_state.load(); }
qint64 AudioEngine::positionFrames() const { return m_posFrames.load(); }
qint64 AudioEngine::totalFrames() const { return m_totalFrames.load(); }
int AudioEngine::sampleRate() const { return m_sampleRate.load(); }
void AudioEngine::setVolume(float v) {
m_volume = std::clamp(v, 0.f, 1.f);
if (m_sink) m_sink->setVolume(m_volume);
}
void AudioEngine::play(const Track &track) {
stop();
m_stopReq = false;
m_pauseReq = false;
m_seekPending = false;
m_track = track;
m_thread = QThread::create([this]{ playbackLoop(); });
m_thread->setObjectName("subwave-audio");
m_thread->start(QThread::HighestPriority);
}
void AudioEngine::togglePause() {
if (m_state == State::Playing) {
m_pauseReq = true;
m_state = State::Paused;
} else if (m_state == State::Paused) {
m_pauseReq = false;
m_state = State::Playing;
m_pauseCv.wakeAll();
}
}
void AudioEngine::stop() {
m_stopReq = true;
m_pauseReq = false;
m_pauseCv.wakeAll();
if (m_thread) {
m_thread->quit();
m_thread->wait(3000);
delete m_thread;
m_thread = nullptr;
}
m_state = State::Stopped;
m_posFrames = 0;
}
void AudioEngine::seekFraction(double fraction) {
m_seekFraction = std::clamp(fraction, 0.0, 1.0);
m_seekPending = true;
}
void AudioEngine::setEqBand(int band, float db) {
if (band < 0 || band >= EQ_BANDS) return;
m_eq.setBand(band, db);
}
float AudioEngine::eqBand(int band) const {
return m_eq.band(band);
}
void AudioEngine::initEq() {
for (int b = 0; b < EQ_BANDS; ++b)
m_eq.setBand(b, 0.f);
}
void AudioEngine::playbackLoop() {
m_state = State::Playing;
engine::FFmpegDecoder decoder;
if (!decoder.open(m_track.filePath)) {
qWarning() << "[AudioEngine] Cannot open:" << m_track.filePath;
m_state = State::Stopped;
return;
}
if (decoder.sampleRate() <= 0 || decoder.channels() <= 0) {
qWarning() << "[AudioEngine] Invalid stream format:" << decoder.sampleRate() << "Hz" << decoder.channels() << "channels";
m_state = State::Stopped;
return;
}
m_sampleRate = decoder.sampleRate();
m_eq.setSampleRate(m_sampleRate);
m_eq.reset();
if (m_track.durationSecs == 0 && decoder.durationSeconds() > 0) {
m_track.durationSecs = decoder.durationSeconds();
}
emit durationKnown(m_track.durationSecs);
m_totalFrames = decoder.totalFrames();
QAudioFormat af;
af.setSampleRate(m_sampleRate);
af.setChannelCount(decoder.channels());
af.setSampleFormat(QAudioFormat::Float);
QSemaphore sinkReady(0);
QMetaObject::invokeMethod(this, [this, &af, &sinkReady] {
QAudioDevice dev = QMediaDevices::defaultAudioOutput();
m_sink = new QAudioSink(dev, af);
m_sink->setVolume(m_volume);
m_sinkIO = m_sink->start();
sinkReady.release();
}, Qt::QueuedConnection);
sinkReady.acquire();
if (!m_sinkIO) {
qWarning() << "[AudioEngine] QAudioSink failed to start";
m_state = State::Stopped;
return;
}
engine::SpectrumAnalyzer spectrumAnalyzer;
connect(&spectrumAnalyzer, &engine::SpectrumAnalyzer::fftReady,
this, &AudioEngine::fftReady, Qt::QueuedConnection);
auto cleanup = [&] {
QAudioSink *sinkToDelete = m_sink;
m_sink = nullptr;
m_sinkIO = nullptr;
if (sinkToDelete) {
QMetaObject::invokeMethod(this, [sinkToDelete] {
sinkToDelete->stop();
delete sinkToDelete;
}, Qt::QueuedConnection);
}
m_state = State::Stopped;
};
m_posFrames = 0;
engine::AudioBuffer buffer;
while (!m_stopReq) {
{
QMutexLocker lock(&m_pauseMtx);
while (m_pauseReq && !m_stopReq)
m_pauseCv.wait(&m_pauseMtx, 50);
}
if (m_stopReq) break;
if (m_seekPending) {
m_seekPending = false;
double frac = m_seekFraction.load();
decoder.seek(frac);
if (m_totalFrames > 0)
m_posFrames = static_cast<qint64>(frac * m_totalFrames.load());
m_eq.reset();
continue;
}
if (!decoder.decodeNext(buffer))
break;
if (buffer.empty())
continue;
m_eq.process(buffer);
spectrumAnalyzer.feed(buffer);
const char *src = reinterpret_cast<const char *>(buffer.samples.data());
qsizetype bytes = static_cast<qsizetype>(buffer.sampleCount()) * sizeof(float);
qsizetype written = 0;
while (written < bytes && !m_stopReq) {
qsizetype n = m_sinkIO->write(src + written, bytes - written);
if (n <= 0)
QThread::msleep(1);
else
written += n;
}
m_posFrames += buffer.frameCount();
emit positionChanged(m_posFrames.load());
}
bool natural = !m_stopReq;
cleanup();
if (natural)
emit trackEnded();
}
@@ -7,7 +7,9 @@
#include <QSemaphore> #include <QSemaphore>
#include <atomic> #include <atomic>
#include <vector> #include <vector>
#include <QVector>
#include "Track.h" #include "Track.h"
#include "engine/EqProcessor.h"
class AudioEngine : public QObject { class AudioEngine : public QObject {
Q_OBJECT Q_OBJECT
@@ -67,6 +69,7 @@ private:
QWaitCondition m_pauseCv; QWaitCondition m_pauseCv;
Track m_track; Track m_track;
engine::EqProcessor m_eq;
float m_eqGainDb[EQ_BANDS] {}; float m_eqGainDb[EQ_BANDS] {};
float m_bqCoeff[EQ_BANDS][5] {}; float m_bqCoeff[EQ_BANDS][5] {};
@@ -75,4 +78,4 @@ private:
static constexpr int FFT_SIZE = 4096; static constexpr int FFT_SIZE = 4096;
float m_fftBuf[FFT_SIZE] {}; float m_fftBuf[FFT_SIZE] {};
int m_fftPos { 0 }; int m_fftPos { 0 };
}; };
+83
View File
@@ -0,0 +1,83 @@
#include "EqProcessor.h"
#include <cmath>
#include <numbers>
#include <algorithm>
#include <cstring>
namespace engine {
EqProcessor::EqProcessor() {
for (int b = 0; b < BANDS; ++b) {
m_gainDb[b] = 0.f;
recomputeBiquad(b);
}
}
void EqProcessor::setSampleRate(int sr) {
m_sampleRate = sr;
for (int b = 0; b < BANDS; ++b)
recomputeBiquad(b);
}
void EqProcessor::setBand(int band, float dB) {
if (band < 0 || band >= BANDS) return;
m_gainDb[band] = dB;
recomputeBiquad(band);
}
float EqProcessor::band(int band) const {
return (band >= 0 && band < BANDS) ? m_gainDb[band] : 0.f;
}
void EqProcessor::reset() {
std::memset(m_state, 0, sizeof(m_state));
}
void EqProcessor::process(AudioBuffer &buf) {
const int channels = buf.channels;
const int count = buf.sampleCount();
float *samples = buf.samples.data();
for (int band = 0; band < BANDS; ++band) {
if (std::abs(m_gainDb[band]) < 0.01f) continue;
const float b0 = m_coeff[band][0], b1 = m_coeff[band][1], b2 = m_coeff[band][2];
const float a1 = m_coeff[band][3], a2 = m_coeff[band][4];
for (int i = 0; i < count; ++i) {
const int ch = i % channels;
float &z1 = m_state[band][ch][0];
float &z2 = m_state[band][ch][1];
float x = samples[i];
float y = b0 * x + z1;
z1 = b1 * x - a1 * y + z2;
z2 = b2 * x - a2 * y;
samples[i] = y;
}
}
}
void EqProcessor::recomputeBiquad(int band) {
const float sr = static_cast<float>(m_sampleRate);
const float f0 = FREQS[band];
const float dBg = m_gainDb[band];
const float Q = 1.41421356f; // sqrt(2) , Butterworth Q
const float A = std::pow(10.f, dBg / 40.f);
const double w0 = 2.0 * std::numbers::pi * f0 / sr;
const double cW = std::cos(w0);
const double sW = std::sin(w0);
const double alp = sW / (2.0 * Q);
// Peaking EQ biquad
const double b0 = 1 + alp * A, b1 = -2 * cW, b2 = 1 - alp * A;
const double a0 = 1 + alp / A, a1 = -2 * cW, a2 = 1 - alp / A;
m_coeff[band][0] = float(b0 / a0);
m_coeff[band][1] = float(b1 / a0);
m_coeff[band][2] = float(b2 / a0);
m_coeff[band][3] = float(a1 / a0);
m_coeff[band][4] = float(a2 / a0);
}
} // namespace engine
+37
View File
@@ -0,0 +1,37 @@
#pragma once
#include <array>
#include "AudioBuffer.h"
namespace engine {
class EqProcessor {
public:
static constexpr int BANDS = 10;
static constexpr float FREQS[BANDS] = {
32, 64, 125, 250, 500, 1000, 2000, 4000, 8000, 16000
};
EqProcessor();
void setBand(int band, float dB);
float band(int band) const;
void process(AudioBuffer &buf);
void reset();
void setSampleRate(int sr);
private:
void recomputeBiquad(int band);
int m_sampleRate { 44100 };
float m_gainDb[BANDS] {};
float m_coeff[BANDS][5] {};
static constexpr int MAX_CH = 8;
float m_state[BANDS][MAX_CH][2] {};
};
} // namespace engine
+191
View File
@@ -0,0 +1,191 @@
#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
@@ -0,0 +1,57 @@
#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
+21
View File
@@ -0,0 +1,21 @@
#pragma once
#include <atomic>
namespace engine {
enum class State { Stopped, Playing, Paused };
struct PlaybackState {
std::atomic<State> state { State::Stopped };
std::atomic<qint64> posFrames { 0 };
std::atomic<qint64> totalFrames { 0 };
std::atomic<int> sampleRate { 44100 };
void reset() {
state = State::Stopped;
posFrames = 0;
totalFrames = 0;
}
};
} // namespace engine
+84
View File
@@ -0,0 +1,84 @@
#include "SpectrumAnalyzer.h"
#include <cmath>
#include <numbers>
#include <algorithm>
#include <stdexcept>
namespace engine {
SpectrumAnalyzer::SpectrumAnalyzer(QObject *parent)
: QObject(parent)
{
m_fftIn = fftw_alloc_real(FFT_SIZE);
m_fftOut = fftw_alloc_complex(FFT_SIZE / 2 + 1);
if (!m_fftIn || !m_fftOut)
throw std::bad_alloc();
m_plan = fftw_plan_dft_r2c_1d(FFT_SIZE, m_fftIn, m_fftOut, FFTW_ESTIMATE);
if (!m_plan)
throw std::runtime_error("FFTW plan creation failed");
reset();
}
SpectrumAnalyzer::~SpectrumAnalyzer()
{
if (m_plan) fftw_destroy_plan(m_plan);
if (m_fftIn) fftw_free(m_fftIn);
if (m_fftOut) fftw_free(m_fftOut);
}
void SpectrumAnalyzer::reset()
{
std::fill(std::begin(m_buf), std::end(m_buf), 0.f);
m_pos = FFT_SIZE / 2;
}
void SpectrumAnalyzer::feed(const AudioBuffer &buf)
{
const int channels = buf.channels;
const int count = buf.sampleCount();
const float *src = buf.samples.data();
for (int i = 0; i < count; i += channels) {
float mono = 0.f;
for (int c = 0; c < channels; ++c)
mono += src[i + c];
mono /= static_cast<float>(channels);
m_buf[m_pos++] = mono;
if (m_pos == FFT_SIZE) {
emit fftReady(computeFFT(m_buf));
// 50 % overlap
std::copy(m_buf + FFT_SIZE / 2, m_buf + FFT_SIZE, m_buf);
m_pos = FFT_SIZE / 2;
}
}
}
QVector<float> SpectrumAnalyzer::computeFFT(const float *input)
{
// Copy input and apply Hann window
const double invN1 = 1.0 / static_cast<double>(FFT_SIZE - 1);
for (int i = 0; i < FFT_SIZE; ++i) {
const double w = 0.5 * (1.0 - std::cos(2.0 * std::numbers::pi * i * invN1));
m_fftIn[i] = static_cast<double>(input[i]) * w;
}
// Execute FFTW plan
fftw_execute(m_plan);
// Compute magnitude spectrum (bins 0 .. FFT_SIZE/2 - 1)
const int bins = FFT_SIZE / 2;
QVector<float> mag(bins);
const double invN = 1.0 / static_cast<double>(FFT_SIZE);
for (int i = 0; i < bins; ++i) {
const double re = m_fftOut[i][0];
const double im = m_fftOut[i][1];
mag[i] = static_cast<float>(std::sqrt(re * re + im * im) * invN);
}
return mag;
}
} // namespace engine
+35
View File
@@ -0,0 +1,35 @@
#pragma once
#include <QObject>
#include <QVector>
#include "AudioBuffer.h"
#include <fftw3.h>
namespace engine {
class SpectrumAnalyzer : public QObject {
Q_OBJECT
public:
static constexpr int FFT_SIZE = 4096;
explicit SpectrumAnalyzer(QObject *parent = nullptr);
~SpectrumAnalyzer() override;
void feed(const AudioBuffer &buf);
void reset();
signals:
void fftReady(QVector<float> magnitudes);
private:
QVector<float> computeFFT(const float *input);
float m_buf[FFT_SIZE] {};
int m_pos { 0 };
// FFTW members
fftw_plan m_plan = nullptr;
double *m_fftIn = nullptr;
fftw_complex *m_fftOut = nullptr;
};
} // namespace engine
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 648 B

+3 -1
View File
@@ -8,8 +8,10 @@ int main(int argc, char *argv[])
QApplication app(argc, argv); QApplication app(argc, argv);
app.setApplicationName("SubWave"); app.setApplicationName("SubWave");
app.setOrganizationName("SubWave"); app.setOrganizationName("SubWave");
app.setWindowIcon(QIcon(":/icons/ico/wavy_png.png"));
qDebug() << QResource(":/shaders/bars.vert").isValid(); qDebug() << "AppIcon there?: " << QResource(":/icons/ico/wavy_png.png").isValid();
qDebug() << "Shaders there?: " << QResource(":/shaders/bars.vert").isValid();
MainWindow w; MainWindow w;
w.show(); w.show();