add shaders for bars, remove waterfall
This commit is contained in:
+12
-6
@@ -5,7 +5,7 @@ set(CMAKE_CXX_STANDARD 20)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
# ── Find Qt6 ─────────────────────────────────────────────────────────────────
|
||||
find_package(Qt6 REQUIRED COMPONENTS Widgets Multimedia)
|
||||
find_package(Qt6 REQUIRED COMPONENTS Widgets Multimedia OpenGL OpenGLWidgets)
|
||||
|
||||
# ── Find FFmpeg via pkg-config ───────────────────────────────────────────────
|
||||
find_package(PkgConfig REQUIRED)
|
||||
@@ -16,10 +16,11 @@ pkg_check_modules(FFMPEG REQUIRED IMPORTED_TARGET
|
||||
libswresample
|
||||
)
|
||||
|
||||
# Auto-generate MOC files from Q_OBJECT headers
|
||||
# Auto-generate MOC, and now also RCC for resource files
|
||||
set(CMAKE_AUTOMOC ON)
|
||||
set(CMAKE_AUTORCC ON)
|
||||
|
||||
# ── Sources (all located in src/ relative to this file) ──────────────────────
|
||||
# ── Sources ──────────────────────────────────────────────────────────────────
|
||||
set(SOURCES
|
||||
src/main.cpp
|
||||
src/MetadataReader.cpp
|
||||
@@ -28,6 +29,7 @@ set(SOURCES
|
||||
src/MusicLibrary.cpp
|
||||
src/CoverArtWidget.cpp
|
||||
src/SpectrogramWidget.cpp
|
||||
src/GradientDialog.cpp
|
||||
src/EqualizerWidget.cpp
|
||||
src/TrackDelegate.cpp
|
||||
src/PlaylistModel.cpp
|
||||
@@ -42,22 +44,26 @@ set(HEADERS
|
||||
src/MusicLibrary.h
|
||||
src/CoverArtWidget.h
|
||||
src/SpectrogramWidget.h
|
||||
src/GradientDialog.h
|
||||
src/EqualizerWidget.h
|
||||
src/TrackDelegate.h
|
||||
src/PlaylistModel.h
|
||||
src/MainWindow.h
|
||||
)
|
||||
|
||||
add_executable(SubWave ${SOURCES} ${HEADERS})
|
||||
set(RESOURCES src/shaders.qrc)
|
||||
|
||||
add_executable(SubWave ${SOURCES} ${HEADERS} ${RESOURCES})
|
||||
|
||||
# ── Link libraries ───────────────────────────────────────────────────────────
|
||||
target_link_libraries(SubWave PRIVATE
|
||||
Qt6::Widgets
|
||||
Qt6::Multimedia
|
||||
Qt6::OpenGL
|
||||
Qt6::OpenGLWidgets
|
||||
PkgConfig::FFMPEG
|
||||
)
|
||||
|
||||
# ── (Optional) Copy FFmpeg shared libs next to the binary on Windows ─────────
|
||||
# ── Copy FFmpeg shared libs next to the binary on Windows ────────────────────
|
||||
if(WIN32)
|
||||
add_custom_command(TARGET SubWave POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
#include "GradientDialog.h"
|
||||
#include <QHBoxLayout>
|
||||
#include <QVBoxLayout>
|
||||
#include <QPushButton>
|
||||
#include <QColorDialog>
|
||||
#include <QLabel>
|
||||
#include <QPainter>
|
||||
#include <QPixmap>
|
||||
|
||||
GradientDialog::GradientDialog(const QVector<QColor> &stops,
|
||||
QWidget *parent)
|
||||
: QDialog(parent), m_stops(stops)
|
||||
{
|
||||
setWindowTitle("Bar Colour Gradient");
|
||||
setMinimumSize(420, 180);
|
||||
|
||||
auto *mainLay = new QVBoxLayout(this);
|
||||
|
||||
// Preview
|
||||
m_preview = new QLabel;
|
||||
m_preview->setFixedHeight(40);
|
||||
m_preview->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
|
||||
mainLay->addWidget(m_preview);
|
||||
|
||||
// Colour stop buttons
|
||||
auto *btnLay = new QHBoxLayout;
|
||||
QStringList labels = { "Low", "Mid", "High", "Peak" };
|
||||
for (int i = 0; i < m_stops.size(); ++i) {
|
||||
auto *btn = new QPushButton(labels.value(i, QString::number(i)));
|
||||
btn->setFixedSize(70, 30);
|
||||
connect(btn, &QPushButton::clicked, this, [this, i]{ onStopClicked(i); });
|
||||
btnLay->addWidget(btn);
|
||||
m_buttons.append(btn);
|
||||
}
|
||||
mainLay->addLayout(btnLay);
|
||||
|
||||
// Action buttons
|
||||
auto *dialogBtns = new QHBoxLayout;
|
||||
auto *resetBtn = new QPushButton("Reset to Default");
|
||||
connect(resetBtn, &QPushButton::clicked, this, &GradientDialog::onResetToDefault);
|
||||
dialogBtns->addWidget(resetBtn);
|
||||
dialogBtns->addStretch();
|
||||
auto *ok = new QPushButton("Apply");
|
||||
connect(ok, &QPushButton::clicked, this, &QDialog::accept);
|
||||
dialogBtns->addWidget(ok);
|
||||
mainLay->addLayout(dialogBtns);
|
||||
|
||||
updatePreview();
|
||||
}
|
||||
|
||||
void GradientDialog::onStopClicked(int index)
|
||||
{
|
||||
QColor c = QColorDialog::getColor(m_stops[index], this,
|
||||
QString("Choose %1 colour").arg(index));
|
||||
if (c.isValid()) {
|
||||
m_stops[index] = c;
|
||||
updatePreview();
|
||||
}
|
||||
}
|
||||
|
||||
void GradientDialog::onResetToDefault()
|
||||
{
|
||||
m_stops = {
|
||||
QColor("#078D70"), // Low
|
||||
QColor("#26CEAA"), // Mid
|
||||
QColor("#98E8C1"), // High
|
||||
QColor("#FFFFFF") // Peak
|
||||
};
|
||||
updatePreview();
|
||||
}
|
||||
|
||||
void GradientDialog::updatePreview()
|
||||
{
|
||||
QPixmap pix(m_preview->size());
|
||||
if (pix.width() == 0) pix = QPixmap(400, 40); // fallback size
|
||||
pix.fill(Qt::transparent);
|
||||
QPainter p(&pix);
|
||||
QLinearGradient grad(0, 0, pix.width(), 0);
|
||||
for (int i = 0; i < m_stops.size(); ++i) {
|
||||
qreal pos = (m_stops.size() == 1) ? 0.5
|
||||
: qreal(i) / (m_stops.size() - 1);
|
||||
grad.setColorAt(pos, m_stops[i]);
|
||||
}
|
||||
p.setBrush(grad);
|
||||
p.drawRect(pix.rect());
|
||||
p.end();
|
||||
m_preview->setPixmap(pix);
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
#ifndef GRADIENTDIALOG_H
|
||||
#define GRADIENTDIALOG_H
|
||||
|
||||
#include <QDialog>
|
||||
#include <QColor>
|
||||
#include <QVector>
|
||||
|
||||
class QPushButton;
|
||||
class QLabel;
|
||||
|
||||
class GradientDialog : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit GradientDialog(const QVector<QColor> &stops,
|
||||
QWidget *parent = nullptr);
|
||||
|
||||
QVector<QColor> stops() const { return m_stops; }
|
||||
|
||||
private slots:
|
||||
void onStopClicked(int index);
|
||||
void onResetToDefault();
|
||||
|
||||
private:
|
||||
QVector<QColor> m_stops;
|
||||
QVector<QPushButton*> m_buttons;
|
||||
QLabel *m_preview;
|
||||
void updatePreview();
|
||||
};
|
||||
|
||||
#endif
|
||||
+255
-129
@@ -1,166 +1,292 @@
|
||||
#include "SpectrogramWidget.h"
|
||||
#include <QPainter>
|
||||
#include <algorithm>
|
||||
#include "GradientDialog.h"
|
||||
#include <QSurfaceFormat>
|
||||
#include <QFile>
|
||||
#include <QMouseEvent>
|
||||
#include <QColorDialog>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
#include <QDir>
|
||||
#include <QStandardPaths>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
|
||||
SpectrogramWidget::SpectrogramWidget(QWidget *parent)
|
||||
: QOpenGLWidget(parent)
|
||||
{
|
||||
setAttribute(Qt::WA_OpaquePaintEvent);
|
||||
|
||||
QSurfaceFormat fmt;
|
||||
fmt.setVersion(3, 3);
|
||||
fmt.setProfile(QSurfaceFormat::CoreProfile);
|
||||
fmt.setSwapInterval(1);
|
||||
fmt.setSwapBehavior(QSurfaceFormat::DoubleBuffer);
|
||||
setFormat(fmt);
|
||||
|
||||
SpectrogramWidget::SpectrogramWidget(QWidget *parent) : QWidget(parent) {
|
||||
setMinimumSize(400, 200);
|
||||
setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
|
||||
buildHeatmap();
|
||||
m_renderTimer.setInterval(16);
|
||||
connect(&m_renderTimer, &QTimer::timeout, this, [this]{
|
||||
bool updated = false;
|
||||
QVector<float> mag;
|
||||
{
|
||||
QMutexLocker lk(&m_queueMtx);
|
||||
while (!m_fftQueue.isEmpty()) {
|
||||
mag = m_fftQueue.dequeue();
|
||||
updated = true;
|
||||
}
|
||||
}
|
||||
if (updated) {
|
||||
m_currentMag = mag;
|
||||
scrollWaterfall();
|
||||
update();
|
||||
}
|
||||
});
|
||||
m_renderTimer.start();
|
||||
|
||||
m_barHeights .resize(BAR_COUNT);
|
||||
m_barPeaksVec.resize(BAR_COUNT);
|
||||
|
||||
loadBarColors();
|
||||
}
|
||||
|
||||
void SpectrogramWidget::receiveFft(const QVector<float> &mag) {
|
||||
SpectrogramWidget::~SpectrogramWidget()
|
||||
{
|
||||
makeCurrent();
|
||||
glDeleteTextures(1, &m_heatmapTex);
|
||||
glDeleteVertexArrays(1, &m_vaoBars);
|
||||
doneCurrent();
|
||||
}
|
||||
|
||||
void SpectrogramWidget::receiveFft(const QVector<float> &mag)
|
||||
{
|
||||
QMutexLocker lk(&m_queueMtx);
|
||||
if (m_fftQueue.size() < 16) m_fftQueue.enqueue(mag);
|
||||
}
|
||||
|
||||
void SpectrogramWidget::reset() {
|
||||
m_currentMag.fill(0.f, 2048);
|
||||
m_barSmoothed.fill(0.f, BAR_COUNT);
|
||||
m_barPeak.fill(0.f, BAR_COUNT);
|
||||
m_peakTimer.fill(0, BAR_COUNT);
|
||||
m_waterfall = QImage{};
|
||||
while (m_fftQueue.size() >= 4)
|
||||
m_fftQueue.dequeue();
|
||||
m_fftQueue.enqueue(mag);
|
||||
update();
|
||||
}
|
||||
|
||||
void SpectrogramWidget::paintEvent(QPaintEvent *) {
|
||||
QPainter p(this);
|
||||
int w = width(), h = height();
|
||||
if (w < 8 || h < 8) return;
|
||||
int wfH = h / 2;
|
||||
int barH = h - wfH;
|
||||
if (!m_waterfall.isNull())
|
||||
p.drawImage(0, 0, m_waterfall);
|
||||
p.setPen(QColor(100,100,100));
|
||||
p.drawLine(0, wfH-1, w, wfH-1);
|
||||
drawSpectrum(p, 0, wfH, w, barH);
|
||||
void SpectrogramWidget::reset()
|
||||
{
|
||||
{
|
||||
QMutexLocker lk(&m_queueMtx);
|
||||
m_fftQueue.clear();
|
||||
}
|
||||
m_currentMag .fill(0.f);
|
||||
m_barSmoothed.fill(0.f);
|
||||
m_barPeak .fill(0.f);
|
||||
m_peakTimer .fill(0);
|
||||
update();
|
||||
}
|
||||
|
||||
void SpectrogramWidget::resizeEvent(QResizeEvent *) {
|
||||
m_waterfall = QImage{};
|
||||
void SpectrogramWidget::initializeGL()
|
||||
{
|
||||
initializeOpenGLFunctions();
|
||||
initShaders();
|
||||
buildHeatmapTexture();
|
||||
|
||||
glGenVertexArrays(1, &m_vaoBars);
|
||||
glDisable(GL_BLEND);
|
||||
}
|
||||
|
||||
void SpectrogramWidget::buildHeatmap() {
|
||||
struct Stop { int r,g,b; };
|
||||
Stop stops[] = {{0,0,0},{0,0,180},{0,255,255},{255,255,0},{255,0,0}};
|
||||
int nSegs = 4;
|
||||
void SpectrogramWidget::initShaders()
|
||||
{
|
||||
auto load = [](const QString &path) -> QByteArray {
|
||||
QFile f(path);
|
||||
if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||||
qWarning() << "Cannot open shader:" << path;
|
||||
return {};
|
||||
}
|
||||
return f.readAll();
|
||||
};
|
||||
|
||||
if (!m_barsProg.addShaderFromSourceCode(QOpenGLShader::Vertex, load(":/shaders/bars.vert")))
|
||||
qWarning() << "Bars vert error:" << m_barsProg.log();
|
||||
if (!m_barsProg.addShaderFromSourceCode(QOpenGLShader::Fragment, load(":/shaders/bars.frag")))
|
||||
qWarning() << "Bars frag error:" << m_barsProg.log();
|
||||
if (!m_barsProg.link())
|
||||
qWarning() << "Bars link error:" << m_barsProg.log();
|
||||
|
||||
m_uBarHeights = m_barsProg.uniformLocation("barHeights");
|
||||
m_uBarPeaks = m_barsProg.uniformLocation("barPeaks");
|
||||
m_uBarCount = m_barsProg.uniformLocation("barCount");
|
||||
m_uBarWidth = m_barsProg.uniformLocation("barWidth");
|
||||
m_uGap = m_barsProg.uniformLocation("gap");
|
||||
m_uBottomY = m_barsProg.uniformLocation("bottomY");
|
||||
m_uTopClip = m_barsProg.uniformLocation("topClip");
|
||||
m_uHeatmap = m_barsProg.uniformLocation("heatmap");
|
||||
}
|
||||
|
||||
void SpectrogramWidget::buildHeatmapTexture()
|
||||
{
|
||||
updateHeatmapTexture(); // use current m_stops
|
||||
}
|
||||
|
||||
void SpectrogramWidget::updateHeatmapTexture()
|
||||
{
|
||||
if (m_heatmapTex) {
|
||||
glDeleteTextures(1, &m_heatmapTex);
|
||||
m_heatmapTex = 0;
|
||||
}
|
||||
|
||||
if (m_stops.size() < 2)
|
||||
m_stops = { QColor(0,0,0), QColor(255,255,255) };
|
||||
|
||||
const int nSegs = m_stops.size() - 1;
|
||||
|
||||
auto lerp = [](int x, int y, float f) {
|
||||
return std::clamp(int(x + (y - x) * f), 0, 255);
|
||||
};
|
||||
|
||||
for (int i = 0; i < 256; i++) {
|
||||
float t = float(i)/255.f * nSegs;
|
||||
int seg = std::min(int(t), nSegs-1);
|
||||
float t = float(i) / 255.f * nSegs;
|
||||
int seg = std::min(int(t), nSegs - 1);
|
||||
float f = t - seg;
|
||||
auto a = stops[seg], b = stops[seg+1];
|
||||
auto lerp = [](int x, int y, float f){ return std::clamp(int(x+(y-x)*f),0,255); };
|
||||
m_heatmap[i] = qRgb(lerp(a.r,b.r,f), lerp(a.g,b.g,f), lerp(a.b,b.b,f));
|
||||
QColor a = m_stops[seg], b = m_stops[seg+1];
|
||||
m_heatmap[i] = qRgb(lerp(a.red(), b.red(), f),
|
||||
lerp(a.green(), b.green(), f),
|
||||
lerp(a.blue(), b.blue(), f));
|
||||
}
|
||||
}
|
||||
|
||||
void SpectrogramWidget::ensureWaterfall(int w, int wfH) {
|
||||
if (m_waterfall.width() == w && m_waterfall.height() == wfH) return;
|
||||
QImage fresh(w, std::max(1,wfH), QImage::Format_RGB32);
|
||||
fresh.fill(Qt::black);
|
||||
if (!m_waterfall.isNull())
|
||||
QPainter(&fresh).drawImage(0,0,m_waterfall);
|
||||
m_waterfall = std::move(fresh);
|
||||
}
|
||||
|
||||
void SpectrogramWidget::scrollWaterfall() {
|
||||
int w = width(), h = height();
|
||||
int wfH = h / 2;
|
||||
if (w < 2 || wfH < 2) return;
|
||||
ensureWaterfall(w, wfH);
|
||||
|
||||
memmove(m_waterfall.bits(),
|
||||
m_waterfall.bits() + m_waterfall.bytesPerLine(),
|
||||
static_cast<size_t>((wfH-1) * m_waterfall.bytesPerLine()));
|
||||
|
||||
computeBars();
|
||||
|
||||
int fftLen = m_currentMag.size();
|
||||
double logMin = std::log10(20.0), logMax = std::log10(22050.0);
|
||||
QRgb *row = reinterpret_cast<QRgb*>(
|
||||
m_waterfall.bits() + (wfH-1)*m_waterfall.bytesPerLine());
|
||||
|
||||
for (int px = 0; px < w; px++) {
|
||||
double freq = std::pow(10.0, logMin + (logMax-logMin)*px/w);
|
||||
int bin = std::min(fftLen-1, int(freq/22050.0*fftLen));
|
||||
int binL = std::max(0, bin-1), binR = std::min(fftLen-1, bin+1);
|
||||
float mag = 0.f;
|
||||
for (int k=binL; k<=binR; k++) mag += m_currentMag[k];
|
||||
mag /= (binR-binL+1);
|
||||
float db = mag>0 ? 20.f*std::log10(mag) : DB_MIN;
|
||||
float val = std::clamp((db-DB_MIN)/(DB_MAX-DB_MIN), 0.f, 1.f);
|
||||
row[px] = m_heatmap[int(val*255)];
|
||||
glGenTextures(1, &m_heatmapTex);
|
||||
glBindTexture(GL_TEXTURE_1D, m_heatmapTex);
|
||||
std::vector<GLubyte> td(256 * 3);
|
||||
for (int i = 0; i < 256; i++) {
|
||||
td[3*i+0] = quint8(qRed (m_heatmap[i]));
|
||||
td[3*i+1] = quint8(qGreen(m_heatmap[i]));
|
||||
td[3*i+2] = quint8(qBlue (m_heatmap[i]));
|
||||
}
|
||||
glTexImage1D(GL_TEXTURE_1D, 0, GL_RGB, 256, 0,
|
||||
GL_RGB, GL_UNSIGNED_BYTE, td.data());
|
||||
glTexParameteri(GL_TEXTURE_1D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_1D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_1D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||
}
|
||||
|
||||
void SpectrogramWidget::computeBars() {
|
||||
int fftLen = m_currentMag.size();
|
||||
double logMin = std::log10(20.0), logMax = std::log10(22050.0);
|
||||
void SpectrogramWidget::setStops(const QVector<QColor> &stops)
|
||||
{
|
||||
if (stops.size() < 2) return;
|
||||
m_stops = stops;
|
||||
makeCurrent();
|
||||
updateHeatmapTexture();
|
||||
doneCurrent();
|
||||
update();
|
||||
saveBarColors();
|
||||
}
|
||||
|
||||
void SpectrogramWidget::loadBarColors()
|
||||
{
|
||||
QString configDir = QStandardPaths::writableLocation(QStandardPaths::HomeLocation) + "/.subwave";
|
||||
QDir().mkpath(configDir);
|
||||
QString path = configDir + "/barcolors.json";
|
||||
QFile f(path);
|
||||
if (!f.open(QIODevice::ReadOnly))
|
||||
return;
|
||||
|
||||
QJsonDocument doc = QJsonDocument::fromJson(f.readAll());
|
||||
f.close();
|
||||
if (!doc.isArray())
|
||||
return;
|
||||
|
||||
QJsonArray arr = doc.array();
|
||||
QVector<QColor> loaded;
|
||||
for (const auto &v : arr) {
|
||||
QJsonObject obj = v.toObject();
|
||||
int r = obj["r"].toInt(0);
|
||||
int g = obj["g"].toInt(0);
|
||||
int b = obj["b"].toInt(0);
|
||||
loaded.append(QColor(r, g, b));
|
||||
}
|
||||
if (loaded.size() >= 2)
|
||||
m_stops = loaded;
|
||||
}
|
||||
|
||||
void SpectrogramWidget::saveBarColors()
|
||||
{
|
||||
QString configDir = QStandardPaths::writableLocation(QStandardPaths::HomeLocation) + "/.subwave";
|
||||
QDir().mkpath(configDir);
|
||||
QString path = configDir + "/barcolors.json";
|
||||
QFile f(path);
|
||||
if (!f.open(QIODevice::WriteOnly))
|
||||
return;
|
||||
|
||||
QJsonArray arr;
|
||||
for (const QColor &c : m_stops) {
|
||||
QJsonObject obj;
|
||||
obj["r"] = c.red();
|
||||
obj["g"] = c.green();
|
||||
obj["b"] = c.blue();
|
||||
arr.append(obj);
|
||||
}
|
||||
QJsonDocument doc(arr);
|
||||
f.write(doc.toJson());
|
||||
f.close();
|
||||
}
|
||||
|
||||
void SpectrogramWidget::mousePressEvent(QMouseEvent *e)
|
||||
{
|
||||
if (e->button() == Qt::LeftButton) {
|
||||
GradientDialog dlg(m_stops, this);
|
||||
if (dlg.exec() == QDialog::Accepted) {
|
||||
setStops(dlg.stops());
|
||||
}
|
||||
}
|
||||
QOpenGLWidget::mousePressEvent(e);
|
||||
}
|
||||
|
||||
void SpectrogramWidget::computeBars()
|
||||
{
|
||||
const int fftLen = m_currentMag.size();
|
||||
const double logMin = std::log10(20.0), logMax = std::log10(22050.0);
|
||||
QVector<float> bars(BAR_COUNT, 0.f);
|
||||
|
||||
for (int b = 0; b < BAR_COUNT; b++) {
|
||||
double f1 = std::pow(10.0, logMin+(logMax-logMin)* b /BAR_COUNT);
|
||||
double f2 = std::pow(10.0, logMin+(logMax-logMin)*(b+1)/BAR_COUNT);
|
||||
int bin1 = std::max(0, std::min(fftLen-1, int(f1/22050.0*fftLen)));
|
||||
int bin2 = std::max(bin1, std::min(fftLen-1, int(f2/22050.0*fftLen)));
|
||||
float sum = 0.f; int cnt = 0;
|
||||
for (int k=bin1; k<=bin2; k++) { sum+=m_currentMag[k]; cnt++; }
|
||||
bars[b] = cnt>0 ? sum/cnt : 0.f;
|
||||
double f1 = std::pow(10.0, logMin + (logMax-logMin)* b /BAR_COUNT);
|
||||
double f2 = std::pow(10.0, logMin + (logMax-logMin)*(b+1)/BAR_COUNT);
|
||||
int bin1 = std::clamp(int(f1/22050.0*fftLen), 0, fftLen-1);
|
||||
int bin2 = std::clamp(int(f2/22050.0*fftLen), bin1, fftLen-1);
|
||||
float sum = 0.f; int cnt = 0;
|
||||
for (int k = bin1; k <= bin2; k++) { sum += m_currentMag[k]; cnt++; }
|
||||
bars[b] = cnt > 0 ? sum/cnt : 0.f;
|
||||
}
|
||||
for (int b=0; b<BAR_COUNT; b++) {
|
||||
float db = bars[b]>0 ? 20.f*std::log10(bars[b]) : DB_MIN;
|
||||
bars[b] = std::clamp((db-DB_MIN)/(DB_MAX-DB_MIN), 0.f, 1.f);
|
||||
for (int b = 0; b < BAR_COUNT; b++) {
|
||||
float db = bars[b] > 0 ? 20.f*std::log10(bars[b]) : DB_MIN;
|
||||
bars[b] = std::clamp((db - DB_MIN) / (DB_MAX - DB_MIN), 0.f, 1.f);
|
||||
}
|
||||
for (int b=0; b<BAR_COUNT; b++) {
|
||||
for (int b = 0; b < BAR_COUNT; b++) {
|
||||
m_barSmoothed[b] = m_barSmoothed[b]*0.65f + bars[b]*0.35f;
|
||||
if (m_barSmoothed[b] > m_barPeak[b]) {
|
||||
m_barPeak[b] = m_barSmoothed[b]; m_peakTimer[b] = 25;
|
||||
if (m_barSmoothed[b] >= m_barPeak[b]) {
|
||||
m_barPeak[b] = m_barSmoothed[b];
|
||||
m_peakTimer[b] = 25;
|
||||
} else if (m_peakTimer[b] > 0) {
|
||||
m_peakTimer[b]--;
|
||||
} else {
|
||||
m_barPeak[b] = std::max(0.f, m_barPeak[b]-0.006f);
|
||||
m_barPeak[b] = std::max(0.f, m_barPeak[b] - 0.006f);
|
||||
}
|
||||
m_barHeights [b] = m_barSmoothed[b];
|
||||
m_barPeaksVec[b] = m_barPeak[b];
|
||||
}
|
||||
}
|
||||
|
||||
void SpectrogramWidget::drawSpectrum(QPainter &p, int x, int y, int w, int h) {
|
||||
int barW = std::max(1, w/BAR_COUNT);
|
||||
int gap = std::max(1, barW/5);
|
||||
for (int i=0; i<BAR_COUNT; i++) {
|
||||
float val = m_barSmoothed[i];
|
||||
int bx = x + i*barW;
|
||||
int bh = int(val*(h-12));
|
||||
int ci = int(val*255);
|
||||
p.fillRect(bx+gap/2, y+h-12-bh, barW-gap, bh, QColor::fromRgb(m_heatmap[ci]));
|
||||
if (m_barPeak[i] > 0.01f) {
|
||||
int ph = int(m_barPeak[i]*(h-12));
|
||||
p.fillRect(bx+gap/2, y+h-12-ph-1, barW-gap, 2, Qt::white);
|
||||
}
|
||||
void SpectrogramWidget::paintGL()
|
||||
{
|
||||
{
|
||||
QMutexLocker lk(&m_queueMtx);
|
||||
while (!m_fftQueue.isEmpty())
|
||||
m_currentMag = m_fftQueue.dequeue();
|
||||
}
|
||||
p.setPen(palette().windowText().color());
|
||||
QFont f = p.font(); f.setPointSizeF(7); p.setFont(f);
|
||||
const char *lbls[] = {"32","125","500","2k","8k","16k"};
|
||||
float freqs[]= {32,125,500,2000,8000,16000};
|
||||
double logMin=std::log10(20.0), logMax=std::log10(22050.0);
|
||||
for (int i=0; i<6; i++) {
|
||||
double nx = (std::log10(freqs[i])-logMin)/(logMax-logMin)*w;
|
||||
p.drawText(x+int(nx), y+h-1, lbls[i]);
|
||||
|
||||
computeBars();
|
||||
|
||||
QColor bg = palette().color(QPalette::Window);
|
||||
glClearColor(bg.redF(), bg.greenF(), bg.blueF(), 1.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
|
||||
if (m_barsProg.isLinked()) {
|
||||
const float barWidth = 2.0f / BAR_COUNT;
|
||||
const float gap = barWidth * 0.2f;
|
||||
const float drawWidth = barWidth - gap;
|
||||
|
||||
m_barsProg.bind();
|
||||
glUniform1fv(m_uBarHeights, BAR_COUNT, m_barHeights .constData());
|
||||
glUniform1fv(m_uBarPeaks, BAR_COUNT, m_barPeaksVec.constData());
|
||||
glUniform1i (m_uBarCount, BAR_COUNT);
|
||||
glUniform1f (m_uBarWidth, drawWidth);
|
||||
glUniform1f (m_uGap, gap);
|
||||
glUniform1f (m_uBottomY, -1.0f);
|
||||
glUniform1f (m_uTopClip, 1.0f);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
glBindTexture(GL_TEXTURE_1D, m_heatmapTex);
|
||||
glUniform1i(m_uHeatmap, 0);
|
||||
glBindVertexArray(m_vaoBars);
|
||||
glDrawArraysInstanced(GL_TRIANGLES, 0, 6, BAR_COUNT);
|
||||
glBindVertexArray(0);
|
||||
m_barsProg.release();
|
||||
}
|
||||
|
||||
update();
|
||||
}
|
||||
+52
-22
@@ -1,38 +1,68 @@
|
||||
#pragma once
|
||||
#include <QWidget>
|
||||
#include <QTimer>
|
||||
#include <QOpenGLWidget>
|
||||
#include <QOpenGLFunctions_3_3_Core>
|
||||
#include <QOpenGLShaderProgram>
|
||||
#include <QMutex>
|
||||
#include <QQueue>
|
||||
#include <QImage>
|
||||
#include <array>
|
||||
#include <QColor>
|
||||
#include <QVector>
|
||||
|
||||
class SpectrogramWidget : public QWidget {
|
||||
class SpectrogramWidget : public QOpenGLWidget, protected QOpenGLFunctions_3_3_Core
|
||||
{
|
||||
Q_OBJECT
|
||||
static constexpr int BAR_COUNT = 96;
|
||||
static constexpr float DB_MIN = -80.f, DB_MAX = 0.f;
|
||||
|
||||
public:
|
||||
explicit SpectrogramWidget(QWidget *parent = nullptr);
|
||||
~SpectrogramWidget();
|
||||
|
||||
public slots:
|
||||
void receiveFft(const QVector<float> &mag);
|
||||
void reset();
|
||||
protected:
|
||||
void paintEvent(QPaintEvent *) override;
|
||||
void resizeEvent(QResizeEvent *) override;
|
||||
private:
|
||||
void buildHeatmap();
|
||||
void ensureWaterfall(int w, int wfH);
|
||||
void scrollWaterfall();
|
||||
void computeBars();
|
||||
void drawSpectrum(QPainter &p, int x, int y, int w, int h);
|
||||
|
||||
QTimer m_renderTimer;
|
||||
QMutex m_queueMtx;
|
||||
QQueue<QVector<float>> m_fftQueue;
|
||||
QVector<float> m_currentMag = QVector<float>(2048, 0.f);
|
||||
QVector<float> m_barSmoothed = QVector<float>(BAR_COUNT, 0.f);
|
||||
QVector<float> m_barPeak = QVector<float>(BAR_COUNT, 0.f);
|
||||
QVector<int> m_peakTimer = QVector<int>(BAR_COUNT, 0);
|
||||
QImage m_waterfall;
|
||||
std::array<QRgb,256> m_heatmap {};
|
||||
protected:
|
||||
void initializeGL() override;
|
||||
void paintGL() override;
|
||||
void mousePressEvent(QMouseEvent *e) override;
|
||||
|
||||
private:
|
||||
void initShaders();
|
||||
void buildHeatmapTexture();
|
||||
void updateHeatmapTexture();
|
||||
void setStops(const QVector<QColor> &stops);
|
||||
void loadBarColors();
|
||||
void saveBarColors();
|
||||
void computeBars();
|
||||
|
||||
QMutex m_queueMtx;
|
||||
QQueue<QVector<float>> m_fftQueue;
|
||||
QVector<float> m_currentMag = QVector<float>(2048, 0.f);
|
||||
|
||||
QVector<float> m_barSmoothed = QVector<float>(BAR_COUNT, 0.f);
|
||||
QVector<float> m_barPeak = QVector<float>(BAR_COUNT, 0.f);
|
||||
QVector<int> m_peakTimer = QVector<int> (BAR_COUNT, 0);
|
||||
QVector<float> m_barHeights;
|
||||
QVector<float> m_barPeaksVec;
|
||||
|
||||
QVector<QColor> m_stops = { QColor("#078D70"),
|
||||
QColor("#26CEAA"),
|
||||
QColor("#98E8C1"),
|
||||
QColor("#FFFFFF") };
|
||||
|
||||
std::array<QRgb, 256> m_heatmap{};
|
||||
|
||||
QOpenGLShaderProgram m_barsProg;
|
||||
GLuint m_heatmapTex = 0;
|
||||
GLuint m_vaoBars = 0;
|
||||
|
||||
int m_uBarHeights = -1;
|
||||
int m_uBarPeaks = -1;
|
||||
int m_uBarCount = -1;
|
||||
int m_uBarWidth = -1;
|
||||
int m_uGap = -1;
|
||||
int m_uBottomY = -1;
|
||||
int m_uTopClip = -1;
|
||||
int m_uHeatmap = -1;
|
||||
};
|
||||
@@ -1,4 +1,6 @@
|
||||
#include <QApplication>
|
||||
#include <QResource>
|
||||
#include <QDebug>
|
||||
#include "MainWindow.h"
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
@@ -7,7 +9,10 @@ int main(int argc, char *argv[])
|
||||
app.setApplicationName("SubWave");
|
||||
app.setOrganizationName("SubWave");
|
||||
|
||||
qDebug() << QResource(":/shaders/bars.vert").isValid();
|
||||
|
||||
MainWindow w;
|
||||
w.show();
|
||||
|
||||
return app.exec();
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
<RCC>
|
||||
<qresource prefix="/">
|
||||
<file>shaders/bars.vert</file>
|
||||
<file>shaders/bars.frag</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
@@ -0,0 +1,12 @@
|
||||
#version 330 core
|
||||
|
||||
in float vVal;
|
||||
out vec4 fragColor;
|
||||
|
||||
uniform sampler1D heatmap;
|
||||
|
||||
void main()
|
||||
{
|
||||
vec3 col = texture(heatmap, vVal).rgb;
|
||||
fragColor = vec4(col, 1.0);
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
#version 330 core
|
||||
|
||||
uniform float barHeights[96];
|
||||
uniform int barCount;
|
||||
uniform float barWidth;
|
||||
uniform float gap;
|
||||
uniform float bottomY;
|
||||
uniform float topClip;
|
||||
|
||||
out float vVal;
|
||||
|
||||
void main()
|
||||
{
|
||||
int bar = gl_InstanceID;
|
||||
int vertex = gl_VertexID;
|
||||
|
||||
float h = barHeights[bar];
|
||||
|
||||
float xLeft = -1.0 + float(bar) * (barWidth + gap);
|
||||
float xRight = xLeft + barWidth;
|
||||
|
||||
float regionH = topClip - bottomY;
|
||||
float yTop = bottomY + h * regionH;
|
||||
|
||||
vec2 corners[6];
|
||||
corners[0] = vec2(xLeft, bottomY);
|
||||
corners[1] = vec2(xRight, bottomY);
|
||||
corners[2] = vec2(xRight, yTop);
|
||||
corners[3] = vec2(xLeft, bottomY);
|
||||
corners[4] = vec2(xRight, yTop);
|
||||
corners[5] = vec2(xLeft, yTop);
|
||||
|
||||
vec2 pos = corners[vertex];
|
||||
|
||||
vVal = (pos.y - bottomY) / regionH;
|
||||
|
||||
gl_Position = vec4(pos, 0.0, 1.0);
|
||||
}
|
||||
Reference in New Issue
Block a user