refactor: EqualizerWidget and the logic behind it.

convenience add: build.sh cai option (Clean AppImage)
This commit is contained in:
2026-06-19 18:54:20 +02:00
parent e62ab4626c
commit 66843e3ad2
8 changed files with 578 additions and 268 deletions
+4
View File
@@ -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
+14 -1
View File
@@ -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
+132
View File
@@ -0,0 +1,132 @@
#include "EqualizerPresetManager.h"
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
const QVector<EqualizerPresetManager::Preset> 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());
}
+33
View File
@@ -0,0 +1,33 @@
#pragma once
#include <QString>
#include <QVector>
#include <QPair>
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<Preset> s_builtins;
QVector<Preset> m_userPresets;
};
+167 -242
View File
@@ -1,50 +1,46 @@
#include "EqualizerWidget.h"
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QLabel>
#include <QMessageBox>
#include <QInputDialog>
#include <QFileDialog>
#include <QStyleFactory>
#include <QFile>
#include <QDir>
#include <QPainter>
#include <QPainterPath>
#include <QFile>
#include <QFileInfo>
#include <QFont>
#include <QHBoxLayout>
#include <QInputDialog>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
#include <algorithm>
#include <QLabel>
#include <QLineEdit>
#include <QMessageBox>
#include <QPushButton>
#include <QVBoxLayout>
#include <cmath>
static const inline QVector<QPair<QString,QString>> 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<int>::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; i<BANDS; i++) {
if (i) vals += ',';
vals += QString::number(m_sliders[i]->value()/10.f,'f',1);
}
m_userPresets.removeIf([&](const QPair<QString,QString> &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; i<BANDS; i++) {
for (int i = 0; i < BANDS; ++i) {
if (i) vals += ',';
vals += QString::number(m_sliders[i]->value() / 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; i<BANDS && i<parts.size(); i++) {
bool ok=false;
float db = parts[i].trimmed().toFloat(&ok);
if (ok) {
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::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; i<BANDS && i<parts.size(); i++) {
bool ok=false;
int raw = parts[i].trimmed().toInt(&ok);
if (ok) {
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));
}
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);
}
}
+20 -25
View File
@@ -4,42 +4,37 @@
#include <QSlider>
#include <QLabel>
#include <QPushButton>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
#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<QPair<QString,QString>> 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();
};
QComboBox *m_presetBox { nullptr };
QSlider *m_sliders[BANDS] {};
QLabel *m_dbLabels[BANDS] {};
FreqCurveWidget *m_curve { nullptr };
};
+186
View File
@@ -0,0 +1,186 @@
#include "FreqCurveWidget.h"
#include <QPainter>
#include <QPainterPath>
#include <QPen>
#include <QFont>
#include <cmath>
#include <algorithm>
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);
}
}
+22
View File
@@ -0,0 +1,22 @@
#pragma once
#include <QWidget>
#include <QSlider>
#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 };
};