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/SpectrogramWidget.cpp
|
||||||
src/gui/GradientDialog.cpp
|
src/gui/GradientDialog.cpp
|
||||||
src/gui/EqualizerWidget.cpp
|
src/gui/EqualizerWidget.cpp
|
||||||
|
src/gui/EqualizerPresetManager.cpp
|
||||||
|
src/gui/FreqCurveWidget.cpp
|
||||||
src/gui/TrackDelegate.cpp
|
src/gui/TrackDelegate.cpp
|
||||||
src/gui/MainWindow.cpp
|
src/gui/MainWindow.cpp
|
||||||
src/gui/NowPlayingPanel.cpp
|
src/gui/NowPlayingPanel.cpp
|
||||||
@@ -53,6 +55,8 @@ set(HEADERS
|
|||||||
src/gui/SpectrogramWidget.h
|
src/gui/SpectrogramWidget.h
|
||||||
src/gui/GradientDialog.h
|
src/gui/GradientDialog.h
|
||||||
src/gui/EqualizerWidget.h
|
src/gui/EqualizerWidget.h
|
||||||
|
src/gui/EqualizerPresetManager.h
|
||||||
|
src/gui/FreqCurveWidget.h
|
||||||
src/gui/TrackDelegate.h
|
src/gui/TrackDelegate.h
|
||||||
src/gui/MainWindow.h
|
src/gui/MainWindow.h
|
||||||
src/gui/NowPlayingPanel.h
|
src/gui/NowPlayingPanel.h
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ clean_build() {
|
|||||||
clean
|
clean
|
||||||
mkdir build
|
mkdir build
|
||||||
cd build
|
cd build
|
||||||
|
echo "doing a clean build"
|
||||||
cmake ..
|
cmake ..
|
||||||
make -j"$(nproc)"
|
make -j"$(nproc)"
|
||||||
}
|
}
|
||||||
@@ -24,10 +25,11 @@ clean_build() {
|
|||||||
release_build() {
|
release_build() {
|
||||||
if [ -d "build" ]; then
|
if [ -d "build" ]; then
|
||||||
rm -rf "build"
|
rm -rf "build"
|
||||||
|
echo "removing build directory"
|
||||||
fi
|
fi
|
||||||
mkdir "build"
|
mkdir "build"
|
||||||
|
|
||||||
cd build
|
cd build
|
||||||
|
echo "starting cmake and make processes"
|
||||||
cmake -DCMAKE_BUILD_TYPE=Release ..
|
cmake -DCMAKE_BUILD_TYPE=Release ..
|
||||||
make -j"$(nproc)"
|
make -j"$(nproc)"
|
||||||
}
|
}
|
||||||
@@ -36,6 +38,14 @@ run_executable() {
|
|||||||
./build/SubWave
|
./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
|
case "${1:-build}" in
|
||||||
clean|c)
|
clean|c)
|
||||||
clean
|
clean
|
||||||
@@ -52,6 +62,9 @@ case "${1:-build}" in
|
|||||||
release|rb)
|
release|rb)
|
||||||
release_build
|
release_build
|
||||||
;;
|
;;
|
||||||
|
cai|clean_appimage|cleanimg)
|
||||||
|
clean_appimage
|
||||||
|
;;
|
||||||
*)
|
*)
|
||||||
echo "Usage: $0 {build|clean|cleanbuild|cb}"
|
echo "Usage: $0 {build|clean|cleanbuild|cb}"
|
||||||
exit 1
|
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 "EqualizerWidget.h"
|
||||||
#include <QVBoxLayout>
|
|
||||||
#include <QHBoxLayout>
|
|
||||||
#include <QLabel>
|
|
||||||
#include <QMessageBox>
|
|
||||||
#include <QInputDialog>
|
|
||||||
#include <QFileDialog>
|
|
||||||
#include <QStyleFactory>
|
|
||||||
#include <QFile>
|
|
||||||
#include <QDir>
|
#include <QDir>
|
||||||
#include <QPainter>
|
#include <QFile>
|
||||||
#include <QPainterPath>
|
#include <QFileInfo>
|
||||||
|
#include <QFont>
|
||||||
|
#include <QHBoxLayout>
|
||||||
|
#include <QInputDialog>
|
||||||
#include <QJsonDocument>
|
#include <QJsonDocument>
|
||||||
#include <QJsonObject>
|
#include <QJsonObject>
|
||||||
#include <QJsonArray>
|
#include <QLabel>
|
||||||
#include <algorithm>
|
#include <QLineEdit>
|
||||||
|
#include <QMessageBox>
|
||||||
|
#include <QPushButton>
|
||||||
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
|
|
||||||
static const inline QVector<QPair<QString,QString>> BUILTIN_PRESETS = {
|
static const QString FREQ_LABELS[] = {
|
||||||
{"Flat", "0,0,0,0,0,0,0,0,0,0"},
|
"32Hz","64Hz","125Hz","250Hz","500Hz",
|
||||||
{"Bass Boost", "8,6,4,2,0,0,0,0,0,0"},
|
"1kHz","2kHz","4kHz","8kHz","16kHz"
|
||||||
{"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 inline QString FREQ_LABELS[] = {"32Hz","64Hz","125Hz","250Hz","500Hz","1kHz","2kHz","4kHz","8kHz","16kHz"};
|
QString EqualizerWidget::stateFilePath()
|
||||||
|
{
|
||||||
QString EqualizerWidget::presetFile() { return QDir::homePath() + "/.subwave/eq_presets.json"; }
|
return QDir::homePath() + "/.subwave/eq_state.json";
|
||||||
QString EqualizerWidget::stateFile() { return QDir::homePath() + "/.subwave/eq_state.json"; }
|
}
|
||||||
|
|
||||||
EqualizerWidget::EqualizerWidget(AudioEngine *engine, QWidget *parent)
|
EqualizerWidget::EqualizerWidget(AudioEngine *engine, QWidget *parent)
|
||||||
: QWidget(parent), m_engine(engine)
|
: QWidget(parent)
|
||||||
|
, m_engine(engine)
|
||||||
{
|
{
|
||||||
auto *vlay = new QVBoxLayout(this);
|
auto *vlay = new QVBoxLayout(this);
|
||||||
vlay->setSpacing(4);
|
vlay->setSpacing(4);
|
||||||
vlay->setContentsMargins(4,4,4,4);
|
vlay->setContentsMargins(4, 4, 4, 4);
|
||||||
|
|
||||||
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");
|
||||||
auto *resetBtn = new QPushButton("Reset");
|
auto *resetBtn = new QPushButton("Reset");
|
||||||
topBar->addWidget(saveBtn);
|
topBar->addWidget(saveBtn);
|
||||||
@@ -54,24 +50,26 @@ EqualizerWidget::EqualizerWidget(AudioEngine *engine, QWidget *parent)
|
|||||||
vlay->addLayout(topBar);
|
vlay->addLayout(topBar);
|
||||||
|
|
||||||
m_curve = new FreqCurveWidget(this);
|
m_curve = new FreqCurveWidget(this);
|
||||||
m_curve->setMinimumHeight(80);
|
m_curve->setMinimumHeight(100);
|
||||||
vlay->addWidget(m_curve);
|
vlay->addWidget(m_curve);
|
||||||
|
|
||||||
auto *sliderRow = new QHBoxLayout;
|
auto *sliderRow = new QHBoxLayout;
|
||||||
sliderRow->setSpacing(2);
|
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;
|
auto *col = new QVBoxLayout;
|
||||||
col->setSpacing(1);
|
col->setSpacing(1);
|
||||||
col->setAlignment(Qt::AlignHCenter);
|
col->setAlignment(Qt::AlignHCenter);
|
||||||
|
|
||||||
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);
|
||||||
// Fixed width and monospace to prevent left/right shifting
|
m_dbLabels[i]->setFont(monoFont);
|
||||||
QFont sf("Monospace", 8);
|
m_dbLabels[i]->setFixedWidth(50);
|
||||||
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);
|
||||||
@@ -81,246 +79,173 @@ EqualizerWidget::EqualizerWidget(AudioEngine *engine, QWidget *parent)
|
|||||||
|
|
||||||
auto *freqLbl = new QLabel(FREQ_LABELS[i]);
|
auto *freqLbl = new QLabel(FREQ_LABELS[i]);
|
||||||
freqLbl->setAlignment(Qt::AlignHCenter);
|
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_dbLabels[i]);
|
||||||
col->addWidget(m_sliders[i]);
|
col->addWidget(m_sliders[i]);
|
||||||
col->addWidget(freqLbl);
|
col->addWidget(freqLbl);
|
||||||
sliderRow->addLayout(col);
|
sliderRow->addLayout(col);
|
||||||
|
|
||||||
connect(m_sliders[i], &QSlider::valueChanged, this, [this, band](int val){
|
connect(m_sliders[i], &QSlider::valueChanged,
|
||||||
float db = val / 10.f;
|
this, [this, band](int raw) {
|
||||||
|
float db = raw / 10.f;
|
||||||
m_engine->setEqBand(band, db);
|
m_engine->setEqBand(band, db);
|
||||||
m_dbLabels[band]->setText(QString("%1%2 dB")
|
m_dbLabels[band]->setText(
|
||||||
.arg(db >= 0 ? "+" : "").arg(db, 0, 'f', 0));
|
QString("%1%2 dB")
|
||||||
|
.arg(db >= 0 ? "+" : "")
|
||||||
|
.arg(db, 0, 'f', 0));
|
||||||
m_curve->update();
|
m_curve->update();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
m_curve->setSliders(m_sliders);
|
m_curve->setSliders(m_sliders);
|
||||||
vlay->addLayout(sliderRow);
|
vlay->addLayout(sliderRow);
|
||||||
|
|
||||||
loadPresets();
|
|
||||||
populatePresetBox();
|
populatePresetBox();
|
||||||
loadState();
|
loadState();
|
||||||
|
|
||||||
connect(m_presetBox, QOverload<int>::of(&QComboBox::activated),
|
connect(m_presetBox, QOverload<int>::of(&QComboBox::activated),
|
||||||
this, [this](int idx) {
|
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]{
|
|
||||||
m_presetBox->hidePopup();
|
m_presetBox->hidePopup();
|
||||||
m_presetBox->setCurrentIndex(0);
|
|
||||||
m_presetBox->setEnabled(false);
|
m_presetBox->setEnabled(false);
|
||||||
QMetaObject::invokeMethod(this, [this]{
|
QMetaObject::invokeMethod(this, [this, idx] {
|
||||||
applyPresetAt(0);
|
applyPresetAt(idx);
|
||||||
m_presetBox->setEnabled(true);
|
m_presetBox->setEnabled(true);
|
||||||
}, Qt::QueuedConnection);
|
}, 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() {
|
void EqualizerWidget::populatePresetBox()
|
||||||
QDir().mkpath(QFileInfo(stateFile()).absolutePath());
|
{
|
||||||
QFile f(stateFile());
|
QSignalBlocker blk(m_presetBox);
|
||||||
if (!f.open(QIODevice::WriteOnly|QIODevice::Truncate)) return;
|
m_presetBox->clear();
|
||||||
QJsonObject obj;
|
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;
|
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 += ',';
|
if (i) vals += ',';
|
||||||
vals += QString::number(m_sliders[i]->value());
|
vals += QString::number(m_sliders[i]->value());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QJsonObject obj;
|
||||||
obj["bands"] = vals;
|
obj["bands"] = vals;
|
||||||
f.write(QJsonDocument(obj).toJson());
|
f.write(QJsonDocument(obj).toJson());
|
||||||
}
|
}
|
||||||
|
|
||||||
void EqualizerWidget::loadPresets() {
|
void EqualizerWidget::loadState()
|
||||||
QFile f(presetFile());
|
{
|
||||||
if (!f.open(QIODevice::ReadOnly)) return;
|
QFile f(stateFilePath());
|
||||||
|
if (!f.open(QIODevice::ReadOnly))
|
||||||
|
return;
|
||||||
|
|
||||||
QJsonDocument doc = QJsonDocument::fromJson(f.readAll());
|
QJsonDocument doc = QJsonDocument::fromJson(f.readAll());
|
||||||
m_userPresets.clear();
|
if (!doc.isObject())
|
||||||
if (!doc.isArray()) return;
|
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});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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();
|
QJsonObject obj = doc.object();
|
||||||
if (!obj.contains("bands")) return;
|
if (!obj.contains("bands"))
|
||||||
QString vals = obj["bands"].toString();
|
return;
|
||||||
QStringList parts = vals.split(',');
|
|
||||||
for (int i=0; i<BANDS && i<parts.size(); i++) {
|
const QStringList parts = obj["bands"].toString().split(',');
|
||||||
bool ok=false;
|
for (int i = 0; i < BANDS && i < parts.size(); ++i) {
|
||||||
int raw = parts[i].trimmed().toInt(&ok);
|
bool ok = false;
|
||||||
if (ok) {
|
int raw = parts[i].trimmed().toInt(&ok);
|
||||||
QSignalBlocker blk(m_sliders[i]);
|
if (!ok)
|
||||||
m_sliders[i]->setValue(raw);
|
continue;
|
||||||
float db = raw/10.f;
|
|
||||||
m_engine->setEqBand(i, db);
|
QSignalBlocker blk(m_sliders[i]);
|
||||||
m_dbLabels[i]->setText(QString("%1%2 dB")
|
m_sliders[i]->setValue(raw);
|
||||||
.arg(db>=0?"+":"").arg(db,0,'f',0));
|
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();
|
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 <QSlider>
|
||||||
#include <QLabel>
|
#include <QLabel>
|
||||||
#include <QPushButton>
|
#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
|
Q_OBJECT
|
||||||
static constexpr int BANDS = AudioEngine::EQ_BANDS;
|
static constexpr int BANDS = AudioEngine::EQ_BANDS;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit EqualizerWidget(AudioEngine *engine, QWidget *parent = nullptr);
|
explicit EqualizerWidget(AudioEngine *engine, QWidget *parent = nullptr);
|
||||||
|
|
||||||
void saveState();
|
void saveState();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void loadPresets();
|
|
||||||
void savePresets();
|
|
||||||
void populatePresetBox();
|
void populatePresetBox();
|
||||||
void applyPresetAt(int idx);
|
void applyPresetAt(int index);
|
||||||
|
|
||||||
void loadState();
|
void loadState();
|
||||||
|
static QString stateFilePath();
|
||||||
|
|
||||||
AudioEngine *m_engine;
|
void onSavePreset();
|
||||||
QComboBox *m_presetBox;
|
void onDeletePreset();
|
||||||
QSlider *m_sliders[BANDS] {};
|
void onResetToFlat();
|
||||||
QLabel *m_dbLabels[BANDS] {};
|
|
||||||
QVector<QPair<QString,QString>> m_userPresets;
|
|
||||||
|
|
||||||
class FreqCurveWidget : public QWidget {
|
AudioEngine *m_engine;
|
||||||
public:
|
EqualizerPresetManager m_presets;
|
||||||
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;
|
|
||||||
|
|
||||||
static QString presetFile();
|
QComboBox *m_presetBox { nullptr };
|
||||||
static QString stateFile();
|
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