From 66843e3ad249a40ed85110cc9b416ccb84a13c5e Mon Sep 17 00:00:00 2001 From: rattatwinko Date: Fri, 19 Jun 2026 18:54:20 +0200 Subject: [PATCH] refactor: EqualizerWidget and the logic behind it. convenience add: build.sh cai option (Clean AppImage) --- CMakeLists.txt | 4 + build.sh | 15 +- src/gui/EqualizerPresetManager.cpp | 132 ++++++++++ src/gui/EqualizerPresetManager.h | 33 +++ src/gui/EqualizerWidget.cpp | 409 ++++++++++++----------------- src/gui/EqualizerWidget.h | 45 ++-- src/gui/FreqCurveWidget.cpp | 186 +++++++++++++ src/gui/FreqCurveWidget.h | 22 ++ 8 files changed, 578 insertions(+), 268 deletions(-) create mode 100644 src/gui/EqualizerPresetManager.cpp create mode 100644 src/gui/EqualizerPresetManager.h create mode 100644 src/gui/FreqCurveWidget.cpp create mode 100644 src/gui/FreqCurveWidget.h diff --git a/CMakeLists.txt b/CMakeLists.txt index b745b2c..01e7535 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -24,6 +24,8 @@ set(SOURCES src/gui/SpectrogramWidget.cpp src/gui/GradientDialog.cpp src/gui/EqualizerWidget.cpp + src/gui/EqualizerPresetManager.cpp + src/gui/FreqCurveWidget.cpp src/gui/TrackDelegate.cpp src/gui/MainWindow.cpp src/gui/NowPlayingPanel.cpp @@ -53,6 +55,8 @@ set(HEADERS src/gui/SpectrogramWidget.h src/gui/GradientDialog.h src/gui/EqualizerWidget.h + src/gui/EqualizerPresetManager.h + src/gui/FreqCurveWidget.h src/gui/TrackDelegate.h src/gui/MainWindow.h src/gui/NowPlayingPanel.h diff --git a/build.sh b/build.sh index f371ed0..47d6623 100755 --- a/build.sh +++ b/build.sh @@ -17,6 +17,7 @@ clean_build() { clean mkdir build cd build + echo "doing a clean build" cmake .. make -j"$(nproc)" } @@ -24,10 +25,11 @@ clean_build() { release_build() { if [ -d "build" ]; then rm -rf "build" + echo "removing build directory" fi mkdir "build" - cd build + echo "starting cmake and make processes" cmake -DCMAKE_BUILD_TYPE=Release .. make -j"$(nproc)" } @@ -36,6 +38,14 @@ run_executable() { ./build/SubWave } +clean_appimage() { + if [ -e "SubWave-x86_64.AppImage" ]; then + rm -rf SubWave-x86_64.AppImage + echo "Removed AppImage" + fi + echo "No AppImage found, aborting" +} + case "${1:-build}" in clean|c) clean @@ -52,6 +62,9 @@ case "${1:-build}" in release|rb) release_build ;; + cai|clean_appimage|cleanimg) + clean_appimage + ;; *) echo "Usage: $0 {build|clean|cleanbuild|cb}" exit 1 diff --git a/src/gui/EqualizerPresetManager.cpp b/src/gui/EqualizerPresetManager.cpp new file mode 100644 index 0000000..0b25a18 --- /dev/null +++ b/src/gui/EqualizerPresetManager.cpp @@ -0,0 +1,132 @@ +#include "EqualizerPresetManager.h" + +#include +#include +#include +#include +#include +#include + +const QVector EqualizerPresetManager::s_builtins = { + { "Flat", "0,0,0,0,0,0,0,0,0,0" }, + { "Bass Boost", "8,6,4,2,0,0,0,0,0,0" }, + { "Treble Boost", "0,0,0,0,0,0,2,4,6,8" }, + { "V-Shape", "6,4,2,-1,-3,-3,-1,2,4,6" }, + { "Rock", "4,3,1,0,-1,0,2,3,3,2" }, + { "Jazz", "3,2,1,2,0,-1,-1,0,1,2" }, + { "Classical", "0,0,0,0,0,0,-2,-3,-3,-2" }, + { "Electronic", "5,3,0,-1,-3,0,1,2,4,5" }, +}; + +EqualizerPresetManager::EqualizerPresetManager() +{ + load(); +} + +int EqualizerPresetManager::builtinCount() const +{ + return s_builtins.size(); +} + +int EqualizerPresetManager::totalCount() const +{ + return s_builtins.size() + m_userPresets.size(); +} + +bool EqualizerPresetManager::isBuiltin(int index) const +{ + return index >= 0 && index < s_builtins.size(); +} + +QString EqualizerPresetManager::name(int index) const +{ + if (isBuiltin(index)) + return s_builtins[index].name; + + int ui = index - s_builtins.size(); + if (ui >= 0 && ui < m_userPresets.size()) + return m_userPresets[ui].name + " ★"; + + return {}; +} + +QString EqualizerPresetManager::bands(int index) const +{ + if (isBuiltin(index)) + return s_builtins[index].bands; + + int ui = index - s_builtins.size(); + if (ui >= 0 && ui < m_userPresets.size()) + return m_userPresets[ui].bands; + + return {}; +} + +void EqualizerPresetManager::upsert(const QString &name, const QString &bandsValue) +{ + for (auto &p : m_userPresets) { + if (p.name == name) { + p.bands = bandsValue; + save(); + return; + } + } + m_userPresets.append({ name, bandsValue }); + save(); +} + +void EqualizerPresetManager::remove(int index) +{ + if (isBuiltin(index)) + return; + + int ui = index - s_builtins.size(); + if (ui >= 0 && ui < m_userPresets.size()) { + m_userPresets.removeAt(ui); + save(); + } +} + +QString EqualizerPresetManager::presetFilePath() +{ + return QDir::homePath() + "/.subwave/eq_presets.json"; +} + +void EqualizerPresetManager::load() +{ + QFile f(presetFilePath()); + if (!f.open(QIODevice::ReadOnly)) + return; + + QJsonDocument doc = QJsonDocument::fromJson(f.readAll()); + if (!doc.isArray()) + return; + + m_userPresets.clear(); + for (const QJsonValue &v : doc.array()) { + QJsonObject obj = v.toObject(); + QString n = obj["name"].toString(); + QString b = obj["bands"].toString(); + if (!n.isEmpty() && !b.isEmpty()) + m_userPresets.append({ n, b }); + } +} + +void EqualizerPresetManager::save() const +{ + const QString path = presetFilePath(); + QDir().mkpath(QFileInfo(path).absolutePath()); + + QFile f(path); + if (!f.open(QIODevice::WriteOnly | QIODevice::Truncate)) + return; + + QJsonArray arr; + for (const auto &pr : m_userPresets) { + QJsonObject obj; + obj["name"] = pr.name; + obj["bands"] = pr.bands; + arr.append(obj); + } + f.write(QJsonDocument(arr).toJson()); +} diff --git a/src/gui/EqualizerPresetManager.h b/src/gui/EqualizerPresetManager.h new file mode 100644 index 0000000..6a42403 --- /dev/null +++ b/src/gui/EqualizerPresetManager.h @@ -0,0 +1,33 @@ +#pragma once +#include +#include +#include + +class EqualizerPresetManager +{ +public: + EqualizerPresetManager(); + + int totalCount() const; + int builtinCount() const; + bool isBuiltin(int index) const; + + QString name(int index) const; + + QString bands(int index) const; + + void upsert(const QString &name, const QString &bandsValue); + + void remove(int index); + + void load(); + void save() const; + +private: + static QString presetFilePath(); + + struct Preset { QString name; QString bands; }; + + static const QVector s_builtins; + QVector m_userPresets; +}; diff --git a/src/gui/EqualizerWidget.cpp b/src/gui/EqualizerWidget.cpp index 14623e8..87e088d 100644 --- a/src/gui/EqualizerWidget.cpp +++ b/src/gui/EqualizerWidget.cpp @@ -1,50 +1,46 @@ #include "EqualizerWidget.h" -#include -#include -#include -#include -#include -#include -#include -#include + #include -#include -#include +#include +#include +#include +#include +#include #include #include -#include -#include +#include +#include +#include +#include +#include + #include -static const inline QVector> BUILTIN_PRESETS = { - {"Flat", "0,0,0,0,0,0,0,0,0,0"}, - {"Bass Boost", "8,6,4,2,0,0,0,0,0,0"}, - {"Treble Boost", "0,0,0,0,0,0,2,4,6,8"}, - {"V-Shape", "6,4,2,-1,-3,-3,-1,2,4,6"}, - {"Rock", "4,3,1,0,-1,0,2,3,3,2"}, - {"Jazz", "3,2,1,2,0,-1,-1,0,1,2"}, - {"Classical", "0,0,0,0,0,0,-2,-3,-3,-2"}, - {"Electronic", "5,3,0,-1,-3,0,1,2,4,5"}, +static const QString FREQ_LABELS[] = { + "32Hz","64Hz","125Hz","250Hz","500Hz", + "1kHz","2kHz","4kHz","8kHz","16kHz" }; -static const inline QString FREQ_LABELS[] = {"32Hz","64Hz","125Hz","250Hz","500Hz","1kHz","2kHz","4kHz","8kHz","16kHz"}; - -QString EqualizerWidget::presetFile() { return QDir::homePath() + "/.subwave/eq_presets.json"; } -QString EqualizerWidget::stateFile() { return QDir::homePath() + "/.subwave/eq_state.json"; } +QString EqualizerWidget::stateFilePath() +{ + return QDir::homePath() + "/.subwave/eq_state.json"; +} EqualizerWidget::EqualizerWidget(AudioEngine *engine, QWidget *parent) - : QWidget(parent), m_engine(engine) + : QWidget(parent) + , m_engine(engine) { auto *vlay = new QVBoxLayout(this); vlay->setSpacing(4); - vlay->setContentsMargins(4,4,4,4); + vlay->setContentsMargins(4, 4, 4, 4); auto *topBar = new QHBoxLayout; topBar->addWidget(new QLabel("Preset:")); + m_presetBox = new QComboBox; - m_presetBox->setStyle(QStyleFactory::create("Fusion")); topBar->addWidget(m_presetBox); - auto *saveBtn = new QPushButton("Save..."); + + auto *saveBtn = new QPushButton("Save…"); auto *deleteBtn = new QPushButton("Delete"); auto *resetBtn = new QPushButton("Reset"); topBar->addWidget(saveBtn); @@ -54,24 +50,26 @@ EqualizerWidget::EqualizerWidget(AudioEngine *engine, QWidget *parent) vlay->addLayout(topBar); m_curve = new FreqCurveWidget(this); - m_curve->setMinimumHeight(80); + m_curve->setMinimumHeight(100); vlay->addWidget(m_curve); auto *sliderRow = new QHBoxLayout; sliderRow->setSpacing(2); - for (int i = 0; i < BANDS; i++) { - const int band = i; + + QFont monoFont("Monospace", 8); + monoFont.setStyleHint(QFont::Monospace); + + for (int i = 0; i < BANDS; ++i) { + const int band = i; + auto *col = new QVBoxLayout; col->setSpacing(1); col->setAlignment(Qt::AlignHCenter); m_dbLabels[i] = new QLabel("0 dB"); m_dbLabels[i]->setAlignment(Qt::AlignHCenter); - // 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_dbLabels[i]->setFont(monoFont); + m_dbLabels[i]->setFixedWidth(50); m_sliders[i] = new QSlider(Qt::Vertical); m_sliders[i]->setRange(-120, 120); @@ -81,246 +79,173 @@ EqualizerWidget::EqualizerWidget(AudioEngine *engine, QWidget *parent) auto *freqLbl = new QLabel(FREQ_LABELS[i]); freqLbl->setAlignment(Qt::AlignHCenter); - QFont ff = freqLbl->font(); ff.setPointSizeF(7); freqLbl->setFont(ff); + QFont ff = freqLbl->font(); + ff.setPointSizeF(7.0); + freqLbl->setFont(ff); col->addWidget(m_dbLabels[i]); col->addWidget(m_sliders[i]); col->addWidget(freqLbl); sliderRow->addLayout(col); - connect(m_sliders[i], &QSlider::valueChanged, this, [this, band](int val){ - float db = val / 10.f; + connect(m_sliders[i], &QSlider::valueChanged, + this, [this, band](int raw) { + float db = raw / 10.f; m_engine->setEqBand(band, db); - m_dbLabels[band]->setText(QString("%1%2 dB") - .arg(db >= 0 ? "+" : "").arg(db, 0, 'f', 0)); + m_dbLabels[band]->setText( + QString("%1%2 dB") + .arg(db >= 0 ? "+" : "") + .arg(db, 0, 'f', 0)); m_curve->update(); }); } + m_curve->setSliders(m_sliders); vlay->addLayout(sliderRow); - loadPresets(); populatePresetBox(); loadState(); connect(m_presetBox, QOverload::of(&QComboBox::activated), - 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]{ - bool ok = false; - QString name = QInputDialog::getText(this, "Save Preset", "Preset name:", QLineEdit::Normal, "", &ok); - if (!ok || name.trimmed().isEmpty()) return; - QString vals; - for (int i=0; ivalue()/10.f,'f',1); - } - m_userPresets.removeIf([&](const QPair &p){ return p.first == name; }); - m_userPresets.append({name, vals}); - savePresets(); - populatePresetBox(); - }); - connect(deleteBtn, &QPushButton::clicked, this, [this]{ - int idx = m_presetBox->currentIndex(); - int builtinCount = BUILTIN_PRESETS.size(); - if (idx < builtinCount) { - QMessageBox::information(this, "SubWave", "Built-in presets cannot be deleted."); - return; - } - int userIdx = idx - builtinCount; - if (userIdx < 0 || userIdx >= m_userPresets.size()) return; - m_userPresets.removeAt(userIdx); - savePresets(); - populatePresetBox(); - }); - connect(resetBtn, &QPushButton::clicked, this, [this]{ + this, [this](int idx) { m_presetBox->hidePopup(); - m_presetBox->setCurrentIndex(0); m_presetBox->setEnabled(false); - QMetaObject::invokeMethod(this, [this]{ - applyPresetAt(0); + QMetaObject::invokeMethod(this, [this, idx] { + applyPresetAt(idx); m_presetBox->setEnabled(true); }, Qt::QueuedConnection); }); + + // connect actions + connect(saveBtn, &QPushButton::clicked, this, &EqualizerWidget::onSavePreset); + connect(deleteBtn, &QPushButton::clicked, this, &EqualizerWidget::onDeletePreset); + connect(resetBtn, &QPushButton::clicked, this, &EqualizerWidget::onResetToFlat); } -void EqualizerWidget::saveState() { - QDir().mkpath(QFileInfo(stateFile()).absolutePath()); - QFile f(stateFile()); - if (!f.open(QIODevice::WriteOnly|QIODevice::Truncate)) return; - QJsonObject obj; +void EqualizerWidget::populatePresetBox() +{ + QSignalBlocker blk(m_presetBox); + m_presetBox->clear(); + for (int i = 0; i < m_presets.totalCount(); ++i) + m_presetBox->addItem(m_presets.name(i)); +} + +void EqualizerWidget::applyPresetAt(int index) +{ + if (index < 0) + return; + + const QString vals = m_presets.bands(index); + if (vals.isEmpty()) + return; + + const QStringList parts = vals.split(','); + for (int i = 0; i < BANDS && i < parts.size(); ++i) { + bool ok = false; + float db = parts[i].trimmed().toFloat(&ok); + if (!ok) + continue; + + QSignalBlocker blk(m_sliders[i]); + m_sliders[i]->setValue(qRound(db * 10)); + m_engine->setEqBand(i, db); + m_dbLabels[i]->setText( + QString("%1%2 dB") + .arg(db >= 0 ? "+" : "") + .arg(db, 0, 'f', 0)); + } + m_curve->update(); +} + +void EqualizerWidget::onSavePreset() +{ + bool ok = false; + const QString name = QInputDialog::getText( + this, "Save Preset", "Preset name:", QLineEdit::Normal, {}, &ok); + if (!ok || name.trimmed().isEmpty()) + return; + QString vals; - for (int i=0; ivalue() / 10.f, 'f', 1); + } + m_presets.upsert(name.trimmed(), vals); + populatePresetBox(); +} + +void EqualizerWidget::onDeletePreset() +{ + const int idx = m_presetBox->currentIndex(); + if (m_presets.isBuiltin(idx)) { + QMessageBox::information(this, "SubWave", + "Built-in presets cannot be deleted."); + return; + } + m_presets.remove(idx); + populatePresetBox(); +} + +void EqualizerWidget::onResetToFlat() +{ + 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() +{ + const QString path = stateFilePath(); + QDir().mkpath(QFileInfo(path).absolutePath()); + + QFile f(path); + if (!f.open(QIODevice::WriteOnly | QIODevice::Truncate)) + return; + + QString vals; + for (int i = 0; i < BANDS; ++i) { if (i) vals += ','; vals += QString::number(m_sliders[i]->value()); } + + QJsonObject obj; obj["bands"] = vals; f.write(QJsonDocument(obj).toJson()); } -void EqualizerWidget::loadPresets() { - QFile f(presetFile()); - if (!f.open(QIODevice::ReadOnly)) return; +void EqualizerWidget::loadState() +{ + QFile f(stateFilePath()); + if (!f.open(QIODevice::ReadOnly)) + return; + QJsonDocument doc = QJsonDocument::fromJson(f.readAll()); - m_userPresets.clear(); - if (!doc.isArray()) return; - for (const QJsonValue &v : doc.array()) { - QJsonObject obj = v.toObject(); - QString name = obj["name"].toString(); - QString bands = obj["bands"].toString(); - if (!name.isEmpty() && !bands.isEmpty()) - m_userPresets.append({name, bands}); - } -} + if (!doc.isObject()) + return; -void EqualizerWidget::savePresets() { - QDir().mkpath(QFileInfo(presetFile()).absolutePath()); - QFile f(presetFile()); - if (!f.open(QIODevice::WriteOnly|QIODevice::Truncate)) return; - QJsonArray arr; - for (const auto &pr : m_userPresets) { - QJsonObject obj; - obj["name"] = pr.first; - obj["bands"] = pr.second; - arr.append(obj); - } - f.write(QJsonDocument(arr).toJson()); -} - -void EqualizerWidget::populatePresetBox() { - QSignalBlocker blk(m_presetBox); - m_presetBox->clear(); - for (auto &pr : BUILTIN_PRESETS) m_presetBox->addItem(pr.first); - for (auto &pr : m_userPresets) m_presetBox->addItem(pr.first + " ★"); -} - -void EqualizerWidget::applyPresetAt(int idx) { - if (idx < 0) return; - QString vals; - if (idx < BUILTIN_PRESETS.size()) - vals = BUILTIN_PRESETS[idx].second; - else { - int ui = idx - BUILTIN_PRESETS.size(); - if (ui < 0 || ui >= m_userPresets.size()) return; - vals = m_userPresets[ui].second; - } - QStringList parts = vals.split(','); - for (int i=0; isetValue(qRound(db*10)); - m_engine->setEqBand(i, db); - m_dbLabels[i]->setText(QString("%1%2 dB") - .arg(db>=0?"+":"").arg(db,0,'f',0)); - } - } - m_curve->update(); -} - -void EqualizerWidget::loadState() { - QFile f(stateFile()); - if (!f.open(QIODevice::ReadOnly)) return; - QJsonDocument doc = QJsonDocument::fromJson(f.readAll()); - if (!doc.isObject()) return; QJsonObject obj = doc.object(); - if (!obj.contains("bands")) return; - QString vals = obj["bands"].toString(); - QStringList parts = vals.split(','); - for (int i=0; isetValue(raw); - float db = raw/10.f; - m_engine->setEqBand(i, db); - m_dbLabels[i]->setText(QString("%1%2 dB") - .arg(db>=0?"+":"").arg(db,0,'f',0)); - } + if (!obj.contains("bands")) + return; + + const QStringList parts = obj["bands"].toString().split(','); + for (int i = 0; i < BANDS && i < parts.size(); ++i) { + bool ok = false; + int raw = parts[i].trimmed().toInt(&ok); + if (!ok) + continue; + + QSignalBlocker blk(m_sliders[i]); + m_sliders[i]->setValue(raw); + float db = raw / 10.f; + m_engine->setEqBand(i, db); + m_dbLabels[i]->setText( + QString("%1%2 dB") + .arg(db >= 0 ? "+" : "") + .arg(db, 0, 'f', 0)); } m_curve->update(); } - -void EqualizerWidget::FreqCurveWidget::paintEvent(QPaintEvent *) { - if (!m_sliders) return; - QPainter p(this); - p.setRenderHint(QPainter::Antialiasing); - int w = width(), h = height(); - const int labelArea = 20; - int graphH = h - labelArea; - - p.fillRect(rect(), palette().base()); - - float logMin = std::log10(20.f); - float logMax = std::log10(20000.f); - auto freqToX = [&](float f) -> float { - return (std::log10(f) - logMin) / (logMax - logMin) * w; - }; - - // 0 dB line - float midY = graphH / 2.f; - p.setPen(QColor(180, 180, 180)); - p.drawLine(0, int(midY), w, int(midY)); - - // Draw frequency axis ticks and labels - const float tickFreqs[] = {20, 50, 100, 200, 500, 1000, 2000, 5000, 10000, 20000}; - QFont tickFont = font(); - tickFont.setPointSizeF(7); - p.setFont(tickFont); - p.setPen(palette().windowText().color()); - for (float fq : tickFreqs) { - float x = freqToX(fq); - if (x < 0 || x > w) continue; - // Tick mark - p.drawLine(QPointF(x, graphH), QPointF(x, graphH + 3)); - // Label - QString label; - if (fq >= 1000) label = QString::number(fq / 1000, 'f', 0) + "k"; - else label = QString::number(fq, 'f', 0); - QRectF textRect(x - 20, graphH + 3, 40, labelArea - 3); - p.drawText(textRect, Qt::AlignHCenter | Qt::AlignTop, label); - } - - // 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); - - // Draw control points (band handles) - p.setPen(Qt::NoPen); - p.setBrush(QColor(60, 120, 255)); - for (int b = 0; b < BANDS; ++b) { - float f0 = AudioEngine::EQ_FREQS[b]; - float db = m_sliders[b]->value() / 10.f; - float nx = freqToX(f0); - float ny = midY - (db / 12.f * graphH / 2 * 0.9f); - ny = std::clamp(ny, 0.f, float(graphH)); - p.drawEllipse(QPointF(nx, ny), 3, 3); - } -} \ No newline at end of file diff --git a/src/gui/EqualizerWidget.h b/src/gui/EqualizerWidget.h index a49da4f..0aa9733 100644 --- a/src/gui/EqualizerWidget.h +++ b/src/gui/EqualizerWidget.h @@ -4,42 +4,37 @@ #include #include #include -#include -#include -#include -#include "AudioEngine.h" -class EqualizerWidget : public QWidget { +#include "AudioEngine.h" +#include "EqualizerPresetManager.h" +#include "FreqCurveWidget.h" + +class EqualizerWidget : public QWidget +{ Q_OBJECT static constexpr int BANDS = AudioEngine::EQ_BANDS; public: explicit EqualizerWidget(AudioEngine *engine, QWidget *parent = nullptr); + void saveState(); private: - void loadPresets(); - void savePresets(); void populatePresetBox(); - void applyPresetAt(int idx); + void applyPresetAt(int index); + void loadState(); + static QString stateFilePath(); - AudioEngine *m_engine; - QComboBox *m_presetBox; - QSlider *m_sliders[BANDS] {}; - QLabel *m_dbLabels[BANDS] {}; - QVector> m_userPresets; + void onSavePreset(); + void onDeletePreset(); + void onResetToFlat(); - class FreqCurveWidget : public QWidget { - public: - explicit FreqCurveWidget(QWidget *p) : QWidget(p) {} - void setSliders(QSlider *sliders[BANDS]) { m_sliders = sliders; } - protected: - void paintEvent(QPaintEvent *) override; - private: - QSlider **m_sliders { nullptr }; - } *m_curve; + AudioEngine *m_engine; + EqualizerPresetManager m_presets; - static QString presetFile(); - static QString stateFile(); -}; \ No newline at end of file + QComboBox *m_presetBox { nullptr }; + QSlider *m_sliders[BANDS] {}; + QLabel *m_dbLabels[BANDS] {}; + FreqCurveWidget *m_curve { nullptr }; +}; diff --git a/src/gui/FreqCurveWidget.cpp b/src/gui/FreqCurveWidget.cpp new file mode 100644 index 0000000..ee01e7d --- /dev/null +++ b/src/gui/FreqCurveWidget.cpp @@ -0,0 +1,186 @@ +#include "FreqCurveWidget.h" + +#include +#include +#include +#include + +#include +#include + +static constexpr double SAMPLE_RATE = 48000.0; +static constexpr double DB_RANGE = 12.0; +static constexpr double FREQ_MIN = 20.0; +static constexpr double FREQ_MAX = 20000.0; +static constexpr double LOG_MIN = /* log10(20) */ 1.30103; +static constexpr double LOG_MAX = /* log10(20000) */ 4.30103; + +FreqCurveWidget::FreqCurveWidget(QWidget *parent) + : QWidget(parent) +{} + +void FreqCurveWidget::setSliders(QSlider *sliders[BANDS]) +{ + m_sliders = sliders; +} + +double FreqCurveWidget::peakMagnitudeDb(double f, double f0, + double gainDb, double Q) +{ + if (std::abs(gainDb) < 1e-6) + return 0.0; + + const double A = std::pow(10.0, gainDb / 40.0); // sqrt(10^(dB/20)) + const double w0 = 2.0 * M_PI * f0 / SAMPLE_RATE; + const double alpha = std::sin(w0) / (2.0 * Q); + + // biquad coeff. + const double b0 = 1.0 + alpha * A; + const double b1 = -2.0 * std::cos(w0); + const double b2 = 1.0 - alpha * A; + const double a0 = 1.0 + alpha / A; + const double a1 = -2.0 * std::cos(w0); // same as b1 + const double a2 = 1.0 - alpha / A; + + const double w = 2.0 * M_PI * f / SAMPLE_RATE; + const double cw = std::cos(w); + const double c2w = std::cos(2.0 * w); + + auto polyMagSq = [&](double p0, double p1, double p2) -> double { + return p0*p0 + p1*p1 + p2*p2 + + 2.0 * cw * (p0*p1 + p1*p2) + + 2.0 * c2w * p0*p2; + }; + + const double numSq = polyMagSq(b0, b1, b2); + const double denSq = polyMagSq(a0, a1, a2); + + if (denSq < 1e-30) + return 0.0; + + return 10.0 * std::log10(numSq / denSq); // = 20xlog10(|H|) +} + +void FreqCurveWidget::paintEvent(QPaintEvent *) +{ + if (!m_sliders) + return; + + QPainter p(this); + p.setRenderHint(QPainter::Antialiasing); + + const int w = width(); + const int h = height(); + const int labelH = 18; + const int graphH = h - labelH; + const double midY = graphH / 2.0; + const double scaleY = midY * 0.88 / DB_RANGE; + + p.fillRect(rect(), palette().base()); + + { + QPen gridPen(palette().mid().color(), 0.5, Qt::DotLine); + p.setPen(gridPen); + for (int db : {-12, -9, -6, -3, 3, 6, 9, 12}) { + double y = midY - db * scaleY; + if (y < 0 || y > graphH) continue; + p.drawLine(QPointF(0, y), QPointF(w, y)); + } + } + + { + p.setPen(QPen(palette().mid().color(), 1.0)); + p.drawLine(QPointF(0, midY), QPointF(w, midY)); + } + + { + QFont lf = font(); + lf.setPointSizeF(7.0); + p.setFont(lf); + + for (int b = 0; b < BANDS; ++b) { + const double fq = AudioEngine::EQ_FREQS[b]; + const double x = (std::log10(fq) - LOG_MIN) / (LOG_MAX - LOG_MIN) * w; + + p.setPen(QPen(palette().mid().color(), 0.5, Qt::DotLine)); + p.drawLine(QPointF(x, 0), QPointF(x, graphH)); + + p.setPen(palette().windowText().color()); + p.drawLine(QPointF(x, graphH), QPointF(x, graphH + 3)); + + QString label; + if (fq >= 1000.0) + label = QString::number(fq / 1000.0, 'g', 2) + "k"; + else + label = QString::number(fq, 'g', 3); + + QRectF tr(x - 20, graphH + 3, 40, labelH - 3); + p.drawText(tr, Qt::AlignHCenter | Qt::AlignTop, label); + } + } + + struct Band { float f0; float gainDb; }; + Band bands[BANDS]; + for (int b = 0; b < BANDS; ++b) { + bands[b].f0 = AudioEngine::EQ_FREQS[b]; + bands[b].gainDb = m_sliders[b]->value() / 10.f; + } + + const double Q = M_SQRT2; // Q ~= 1.4142 (approx. ; with a few "zerquetschte") + + QPainterPath path; + for (int px = 0; px < w; ++px) { + double f = std::pow(10.0, LOG_MIN + (LOG_MAX - LOG_MIN) * px / w); + + double totalDb = 0.0; + for (int b = 0; b < BANDS; ++b) { + if (std::abs(bands[b].gainDb) < 0.05f) + continue; + totalDb += peakMagnitudeDb(f, bands[b].f0, bands[b].gainDb, Q); + } + + double py = midY - totalDb * scaleY; + py = std::clamp(py, 0.0, (double)graphH); + + if (px == 0) path.moveTo(px, py); + else path.lineTo(px, py); + } + + { + QPainterPath filled = path; + filled.lineTo(w, midY); + filled.lineTo(0, midY); + filled.closeSubpath(); + + QColor fillColor = palette().highlight().color(); + fillColor.setAlphaF(0.12); + p.fillPath(filled, fillColor); + } + + p.setPen(QPen(palette().highlight().color(), 1.8)); + p.drawPath(path); + + p.setPen(Qt::NoPen); + for (int b = 0; b < BANDS; ++b) { + double f0 = bands[b].f0; + double gainDb = bands[b].gainDb; + + double totalDb = 0.0; + for (int bb = 0; bb < BANDS; ++bb) { + if (std::abs(bands[bb].gainDb) < 0.05f) continue; + totalDb += peakMagnitudeDb(f0, bands[bb].f0, bands[bb].gainDb, Q); + } + + double nx = (std::log10(f0) - LOG_MIN) / (LOG_MAX - LOG_MIN) * w; + double ny = midY - totalDb * scaleY; + ny = std::clamp(ny, 0.0, (double)graphH); + + p.setBrush(palette().base()); + p.drawEllipse(QPointF(nx, ny), 5.0, 5.0); + QColor dot = (gainDb >= 0) + ? palette().highlight().color() + : QColor(220, 80, 60); + p.setBrush(dot); + p.drawEllipse(QPointF(nx, ny), 3.5, 3.5); + } +} diff --git a/src/gui/FreqCurveWidget.h b/src/gui/FreqCurveWidget.h new file mode 100644 index 0000000..52033e1 --- /dev/null +++ b/src/gui/FreqCurveWidget.h @@ -0,0 +1,22 @@ +#pragma once +#include +#include +#include "AudioEngine.h" + +class FreqCurveWidget : public QWidget +{ + Q_OBJECT + static constexpr int BANDS = AudioEngine::EQ_BANDS; + +public: + explicit FreqCurveWidget(QWidget *parent = nullptr); + void setSliders(QSlider *sliders[BANDS]); + +protected: + void paintEvent(QPaintEvent *) override; + +private: + static double peakMagnitudeDb(double f, double f0, double gainDb, double Q); + + QSlider **m_sliders { nullptr }; +};