refactor: EqualizerWidget and the logic behind it.
convenience add: build.sh cai option (Clean AppImage)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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 };
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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 };
|
||||
};
|
||||
Reference in New Issue
Block a user