chore: gitignore build-viz/, remove accidentally committed build artifacts
This commit is contained in:
@@ -1,163 +0,0 @@
|
||||
#include "spectrumwidget.hpp"
|
||||
|
||||
#include <QPainter>
|
||||
#include <QTimerEvent>
|
||||
#include <cmath>
|
||||
#include <algorithm>
|
||||
|
||||
static constexpr float M_PI_F = static_cast<float>(M_PI);
|
||||
|
||||
SpectrumWidget::SpectrumWidget(QobuzBackend *backend, QWidget *parent)
|
||||
: QWidget(parent)
|
||||
, m_backend(backend)
|
||||
, m_pcmBuf(FFT_SIZE * 2, 0.0f) // enough for stereo
|
||||
, m_fftReal(FFT_SIZE, 0.0f)
|
||||
, m_fftImag(FFT_SIZE, 0.0f)
|
||||
, m_bars(NUM_BARS, 0.0f)
|
||||
, m_window(FFT_SIZE)
|
||||
{
|
||||
// Build Hann window
|
||||
for (int i = 0; i < FFT_SIZE; ++i)
|
||||
m_window[i] = 0.5f * (1.0f - std::cos(2.0f * M_PI_F * i / (FFT_SIZE - 1)));
|
||||
|
||||
setMinimumHeight(48);
|
||||
setAttribute(Qt::WA_OpaquePaintEvent);
|
||||
m_timerId = startTimer(33); // ~30 fps
|
||||
}
|
||||
|
||||
// ---- Cooley-Tukey in-place radix-2 DIT FFT --------------------------------
|
||||
|
||||
void SpectrumWidget::fft(std::vector<float> &re, std::vector<float> &im)
|
||||
{
|
||||
const int n = static_cast<int>(re.size());
|
||||
|
||||
// Bit-reversal permutation
|
||||
for (int i = 1, j = 0; i < n; ++i) {
|
||||
int bit = n >> 1;
|
||||
for (; j & bit; bit >>= 1)
|
||||
j ^= bit;
|
||||
j ^= bit;
|
||||
if (i < j) {
|
||||
std::swap(re[i], re[j]);
|
||||
std::swap(im[i], im[j]);
|
||||
}
|
||||
}
|
||||
|
||||
// Butterfly stages
|
||||
for (int len = 2; len <= n; len <<= 1) {
|
||||
const float ang = -2.0f * M_PI_F / len;
|
||||
const float wRe = std::cos(ang);
|
||||
const float wIm = std::sin(ang);
|
||||
for (int i = 0; i < n; i += len) {
|
||||
float curRe = 1.0f, curIm = 0.0f;
|
||||
for (int k = 0; k < len / 2; ++k) {
|
||||
const float uRe = re[i + k];
|
||||
const float uIm = im[i + k];
|
||||
const float vRe = re[i + k + len/2] * curRe - im[i + k + len/2] * curIm;
|
||||
const float vIm = re[i + k + len/2] * curIm + im[i + k + len/2] * curRe;
|
||||
re[i + k] = uRe + vRe;
|
||||
im[i + k] = uIm + vIm;
|
||||
re[i + k + len/2] = uRe - vRe;
|
||||
im[i + k + len/2] = uIm - vIm;
|
||||
const float nextRe = curRe * wRe - curIm * wIm;
|
||||
curIm = curRe * wIm + curIm * wRe;
|
||||
curRe = nextRe;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Sample processing -----------------------------------------------------
|
||||
|
||||
void SpectrumWidget::processNewSamples()
|
||||
{
|
||||
const quint32 channels = m_backend->vizChannels();
|
||||
if (channels == 0) return;
|
||||
|
||||
const quint32 n = m_backend->vizRead(m_pcmBuf.data(),
|
||||
static_cast<quint32>(m_pcmBuf.size()));
|
||||
if (n == 0) return;
|
||||
|
||||
// Mix to mono into the FFT real buffer (wrap-around ring fill)
|
||||
const int ch = static_cast<int>(channels);
|
||||
const int frames = static_cast<int>(n) / ch;
|
||||
for (int f = 0; f < frames && f < FFT_SIZE; ++f) {
|
||||
float mono = 0.0f;
|
||||
for (int c = 0; c < ch; ++c)
|
||||
mono += m_pcmBuf[f * ch + c];
|
||||
m_fftReal[f] = mono / ch * m_window[f];
|
||||
}
|
||||
// Zero-pad if we got fewer frames than FFT_SIZE
|
||||
for (int f = frames; f < FFT_SIZE; ++f)
|
||||
m_fftReal[f] = 0.0f;
|
||||
std::fill(m_fftImag.begin(), m_fftImag.end(), 0.0f);
|
||||
|
||||
fft(m_fftReal, m_fftImag);
|
||||
|
||||
// Map FFT bins to bars using logarithmic frequency spacing
|
||||
// Bins 1..FFT_SIZE/2 cover 0..Nyquist; skip DC (bin 0)
|
||||
const int halfBins = FFT_SIZE / 2;
|
||||
// Log-spaced bin boundaries: bin = 1 * (halfBins)^(i/NUM_BARS)
|
||||
for (int b = 0; b < NUM_BARS; ++b) {
|
||||
const float lo = std::pow(static_cast<float>(halfBins), static_cast<float>(b) / NUM_BARS);
|
||||
const float hi = std::pow(static_cast<float>(halfBins), static_cast<float>(b + 1) / NUM_BARS);
|
||||
const int binLo = std::max(1, static_cast<int>(lo));
|
||||
const int binHi = std::min(halfBins - 1, static_cast<int>(hi));
|
||||
|
||||
float peak = 0.0f;
|
||||
for (int k = binLo; k <= binHi; ++k) {
|
||||
const float mag = std::sqrt(m_fftReal[k] * m_fftReal[k] + m_fftImag[k] * m_fftImag[k]);
|
||||
if (mag > peak) peak = mag;
|
||||
}
|
||||
|
||||
// Convert to dB-ish (log scale), normalised to [0,1]
|
||||
float level = std::log1p(peak * 5.0f) / std::log1p(5.0f * FFT_SIZE / 4.0f);
|
||||
level = std::clamp(level, 0.0f, 1.0f);
|
||||
|
||||
// Smooth: fast attack, slow decay
|
||||
const float alpha = level > m_bars[b] ? 0.6f : 0.15f;
|
||||
m_bars[b] = m_bars[b] * (1.0f - alpha) + level * alpha;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Qt overrides ----------------------------------------------------------
|
||||
|
||||
void SpectrumWidget::timerEvent(QTimerEvent *event)
|
||||
{
|
||||
if (event->timerId() == m_timerId) {
|
||||
processNewSamples();
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
void SpectrumWidget::paintEvent(QPaintEvent *)
|
||||
{
|
||||
QPainter p(this);
|
||||
p.fillRect(rect(), palette().window());
|
||||
|
||||
const int w = width();
|
||||
const int h = height();
|
||||
if (w <= 0 || h <= 0) return;
|
||||
|
||||
const QColor barColor = palette().highlight().color();
|
||||
const QColor peakColor = barColor.lighter(150);
|
||||
|
||||
const float barW = static_cast<float>(w) / NUM_BARS;
|
||||
const float gap = std::max(1.0f, barW * 0.15f);
|
||||
|
||||
for (int b = 0; b < NUM_BARS; ++b) {
|
||||
const int x = static_cast<int>(b * barW);
|
||||
const int bw = static_cast<int>(barW - gap);
|
||||
const int bh = static_cast<int>(m_bars[b] * h);
|
||||
if (bh <= 0) continue;
|
||||
|
||||
// Gradient-ish: draw in two colours split at 70% height
|
||||
const int split = static_cast<int>(h * 0.7f);
|
||||
if (bh <= split) {
|
||||
p.fillRect(x, h - bh, bw, bh, barColor);
|
||||
} else {
|
||||
p.fillRect(x, h - split, bw, split, barColor);
|
||||
p.fillRect(x, h - bh, bw, bh - split, peakColor);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "../backend/qobuzbackend.hpp"
|
||||
|
||||
#include <QWidget>
|
||||
#include <QTimer>
|
||||
#include <vector>
|
||||
|
||||
/// Dock-friendly spectrum bar visualizer.
|
||||
/// Reads PCM from the Rust viz ring buffer, runs a simple FFT,
|
||||
/// and draws frequency bars with QPainter.
|
||||
class SpectrumWidget : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit SpectrumWidget(QobuzBackend *backend, QWidget *parent = nullptr);
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent *event) override;
|
||||
void timerEvent(QTimerEvent *event) override;
|
||||
|
||||
private:
|
||||
void processNewSamples();
|
||||
|
||||
QobuzBackend *m_backend = nullptr;
|
||||
int m_timerId = 0;
|
||||
|
||||
// FFT size — power of two
|
||||
static constexpr int FFT_SIZE = 1024;
|
||||
static constexpr int NUM_BARS = 32;
|
||||
|
||||
std::vector<float> m_pcmBuf; // raw interleaved PCM from ring
|
||||
std::vector<float> m_fftReal; // FFT input (mono mix)
|
||||
std::vector<float> m_fftImag;
|
||||
std::vector<float> m_bars; // smoothed bar heights [0..1]
|
||||
|
||||
// Simple Hann window
|
||||
std::vector<float> m_window;
|
||||
|
||||
static void fft(std::vector<float> &re, std::vector<float> &im);
|
||||
};
|
||||
Reference in New Issue
Block a user