aluf/src/VisualizerWidget.cpp

781 lines
26 KiB
C++

#include "VisualizerWidget.h"
#include <QApplication>
#include <QDateTime>
#include <QFile>
#include <algorithm>
#include <cmath>
#include <numeric>
#ifndef M_PI
#define M_PI 3.14159265358979323846
#endif
static QShader loadShader(const QString &name) {
QFile f(name);
if (f.open(QIODevice::ReadOnly))
return QShader::fromSerialized(f.readAll());
qWarning() << "Failed to load shader:" << name;
return {};
}
VisualizerWidget::VisualizerWidget(QWidget *parent) : QRhiWidget(parent) {
setSampleCount(4);
setNumBins(26);
}
void VisualizerWidget::mouseReleaseEvent(QMouseEvent *event) {
if (event->button() == Qt::LeftButton)
emit tapDetected();
QRhiWidget::mouseReleaseEvent(event);
}
void VisualizerWidget::setNumBins(int n) {
m_customBins.clear();
float minFreq = 40.0f;
float maxFreq = 11000.0f;
for (int i = 0; i <= n; ++i) {
float f = minFreq * std::pow(maxFreq / minFreq, (float)i / n);
m_customBins.push_back(f);
}
m_channels.clear();
}
void VisualizerWidget::setTargetFps(int fps) {
m_targetFps = std::max(15, std::min(120, fps));
}
void VisualizerWidget::setParams(bool glass, bool albumColors,
bool mirrored, bool inverted, float hue,
float contrast, float brightness,
float entropy) {
m_glass = glass;
m_useAlbumColors = albumColors;
m_mirrored = mirrored;
m_inverted = inverted;
m_hueFactor = hue;
m_contrast = contrast;
m_brightness = brightness;
m_entropyStrength = entropy;
update();
}
void VisualizerWidget::setAlbumPalette(const std::vector<QColor> &palette) {
m_albumPalette.clear();
int targetLen = static_cast<int>(m_customBins.size()) - 1;
if (palette.empty())
return;
for (int i = 0; i < targetLen; ++i) {
int idx = (i * static_cast<int>(palette.size() - 1)) / (targetLen - 1);
m_albumPalette.push_back(palette[idx]);
}
}
float VisualizerWidget::getX(float freq) {
float logMin = std::log10(20.0f);
float logMax = std::log10(20000.0f);
if (freq <= 0)
return 0;
return (std::log10(std::max(freq, 1e-9f)) - logMin) / (logMax - logMin);
}
QColor VisualizerWidget::applyModifiers(QColor c) {
float h, s, l;
c.getHslF(&h, &s, &l);
s = std::clamp(s * m_hueFactor, 0.0f, 1.0f);
float v = c.valueF();
v = std::clamp(v * (m_contrast * 0.5f + 0.5f), 0.0f, 1.0f);
return QColor::fromHsvF(c.hsvHueF(), s, v);
}
float VisualizerWidget::calculateEntropy(const std::deque<float> &history) {
if (history.size() < 4)
return 0.0f;
int N = static_cast<int>(history.size());
// Forward DFT (O(N²) for N≈30 is ~900 ops — trivial, no FFTW needed)
std::vector<std::complex<double>> X(N);
for (int k = 0; k < N; ++k) {
double re = 0, im = 0;
for (int n = 0; n < N; ++n) {
double angle = -2.0 * M_PI * k * n / N;
re += history[n] * std::cos(angle);
im += history[n] * std::sin(angle);
}
X[k] = {re, im};
}
// Analytic signal: zero negative freqs, double positive freqs
for (int k = N / 2 + 1; k < N; ++k)
X[k] = 0;
for (int k = 1; k < (N + 1) / 2; ++k)
X[k] *= 2.0;
// Inverse DFT — imaginary part only (zero-centered, phase-aligned)
float sqSum = 0.0f;
for (int n = 0; n < N; ++n) {
double im = 0;
for (int k = 0; k < N; ++k) {
double angle = 2.0 * M_PI * k * n / N;
im += X[k].real() * std::sin(angle) + X[k].imag() * std::cos(angle);
}
im /= N;
sqSum += static_cast<float>(im * im);
}
// RMS of zero-centered signal, normalized: 10dB RMS = max chaos
return std::clamp(std::sqrt(sqSum / N) / 10.0f, 0.0f, 1.0f);
}
// ===== Spectrum Processing =====
void VisualizerWidget::updateData(
const std::vector<AudioAnalyzer::FrameData> &data) {
if (!isVisible())
return;
m_data = data;
m_dataDirty = true;
qint64 now = QDateTime::currentMSecsSinceEpoch();
if (now - m_lastFrameTime < (1000 / m_targetFps))
return;
m_lastFrameTime = now;
if (m_channels.size() != data.size())
m_channels.resize(data.size());
// --- 1. Unified Glass Color ---
if (m_glass && !m_data.empty()) {
size_t midIdx = m_data[0].freqs.size() / 2;
float frameMidFreq =
(midIdx < m_data[0].freqs.size()) ? m_data[0].freqs[midIdx] : 1000.0f;
float sumDb = 0;
for (float v : m_data[0].db)
sumDb += v;
float frameMeanDb =
m_data[0].db.empty() ? -80.0f : sumDb / m_data[0].db.size();
float logMin = std::log10(20.0f);
float logMax = std::log10(20000.0f);
float frameFreqNorm =
(std::log10(std::max(frameMidFreq, 1e-9f)) - logMin) /
(logMax - logMin);
float frameAmpNorm =
std::clamp((frameMeanDb + 80.0f) / 80.0f, 0.0f, 1.0f);
float frameAmpWeight = 1.0f / std::pow(frameFreqNorm + 1e-4f, 5.0f);
frameAmpWeight = std::clamp(frameAmpWeight * 2.0f, 0.5f, 6.0f);
float frameHue = std::fmod(
frameFreqNorm + frameAmpNorm * frameAmpWeight * m_hueFactor, 1.0f);
if (m_mirrored)
frameHue = 1.0f - frameHue;
if (frameHue < 0)
frameHue += 1.0f;
float angle = frameHue * 2.0f * M_PI;
float cosVal = std::cos(angle);
float sinVal = std::sin(angle);
m_hueHistory.push_back({cosVal, sinVal});
m_hueSumCos += cosVal;
m_hueSumSin += sinVal;
if (m_hueHistory.size() > 40) {
auto old = m_hueHistory.front();
m_hueSumCos -= old.first;
m_hueSumSin -= old.second;
m_hueHistory.pop_front();
}
float smoothedAngle = std::atan2(m_hueSumSin, m_hueSumCos);
float smoothedHue = smoothedAngle / (2.0f * M_PI);
if (smoothedHue < 0.0f)
smoothedHue += 1.0f;
m_unifiedColor = QColor::fromHsvF(smoothedHue, 1.0, 1.0);
} else {
m_unifiedColor = Qt::white;
}
// --- 2. Process Channels & Bins ---
for (size_t ch = 0; ch < data.size(); ++ch) {
const auto &db = data[ch].db;
const auto &primaryDb = data[ch].primaryDb;
size_t numBins = db.size();
auto &bins = m_channels[ch].bins;
if (bins.size() != numBins)
bins.resize(numBins);
std::vector<float> vertexEnergy(numBins);
float globalMax = 0.001f;
bool useEntropy = m_entropyStrength > -2.0f;
// Pass 1: compute per-bin entropy from output history (self-referencing)
std::vector<float> binEntropy(numBins, 0.0f);
if (useEntropy) {
for (size_t i = 0; i < numBins; ++i)
binEntropy[i] = calculateEntropy(bins[i].history);
}
// Find the midpoint — median entropy across all bins
float medianEntropy = 0.0f;
if (useEntropy) {
std::vector<float> sorted = binEntropy;
std::nth_element(sorted.begin(), sorted.begin() + numBins / 2,
sorted.end());
medianEntropy = sorted[numBins / 2];
}
// Pass 2: apply entropy-relative multiplier and update visual state
for (size_t i = 0; i < numBins; ++i) {
auto &bin = bins[i];
float rawVal = db[i];
float primaryVal = (i < primaryDb.size()) ? primaryDb[i] : rawVal;
float change = rawVal - bin.visualDb;
if (useEntropy) {
// Position relative to midpoint: >0 = more stable, <0 = more chaotic
float relative = medianEntropy - binEntropy[i];
// Slider controls ratio of reward (stable bins) to penalty (chaotic bins)
// At 0: equal gains → balanced/neutral. +1.5: all reward. -1.5: all penalty.
float base = 1.5f;
float rewardGain = base + m_entropyStrength; // 0..3
float penaltyGain = base - m_entropyStrength; // 3..0
float gain = (relative >= 0.0f) ? rewardGain : penaltyGain;
float multiplier = 1.0f + relative * gain * 2.0f;
multiplier = std::clamp(multiplier, 0.05f, 4.0f);
bin.visualDb += change * multiplier;
// Feed output back into history — the compressor sees its own work
bin.history.push_back(bin.visualDb);
if (bin.history.size() > 30)
bin.history.pop_front();
} else {
float responsiveness = 0.2f;
bin.visualDb =
(bin.visualDb * (1.0f - responsiveness)) + (rawVal * responsiveness);
}
float patternResp = 0.1f;
bin.primaryVisualDb = (bin.primaryVisualDb * (1.0f - patternResp)) +
(primaryVal * patternResp);
vertexEnergy[i] =
std::clamp((bin.primaryVisualDb + 80.0f) / 80.0f, 0.0f, 1.0f);
}
size_t splitIdx = numBins / 2;
float maxLow = 0.01f;
float maxHigh = 0.01f;
for (size_t j = 0; j < splitIdx; ++j)
maxLow = std::max(maxLow, vertexEnergy[j]);
for (size_t j = splitIdx; j < numBins; ++j)
maxHigh = std::max(maxHigh, vertexEnergy[j]);
float trebleBoost = std::clamp(maxLow / maxHigh, 1.0f, 40.0f);
for (size_t j = 0; j < numBins; ++j) {
if (j >= splitIdx) {
float t = (float)(j - splitIdx) / (numBins - splitIdx);
vertexEnergy[j] *= 1.0f + (trebleBoost - 1.0f) * t;
}
float compressed = std::tanh(vertexEnergy[j]);
vertexEnergy[j] = compressed;
if (compressed > globalMax)
globalMax = compressed;
}
for (float &v : vertexEnergy)
v = std::clamp(v / globalMax, 0.0f, 1.0f);
// --- 3. Procedural Pattern ---
for (auto &b : bins) {
b.brightMod = 0.0f;
b.alphaMod = 0.0f;
}
for (size_t i = 1; i < vertexEnergy.size() - 1; ++i) {
float curr = vertexEnergy[i];
float prev = vertexEnergy[i - 1];
float next = vertexEnergy[i + 1];
if (curr > prev && curr > next) {
bool leftDominant = (prev > next);
float sharpness = std::min(curr - prev, curr - next);
float entropyFactor = useEntropy ? std::max(0.1f, std::abs(m_entropyStrength)) : 1.0f;
float peakIntensity =
std::clamp(std::pow(sharpness * 10.0f * entropyFactor, 0.3f), 0.0f, 1.0f);
float decayBase = 0.65f - std::clamp(sharpness * 3.0f, 0.0f, 0.35f);
auto applyPattern = [&](int dist, bool isBrightSide, int direction) {
int segIdx = (direction == -1) ? (static_cast<int>(i) - dist)
: (static_cast<int>(i) + dist - 1);
if (segIdx < 0 || segIdx >= static_cast<int>(bins.size()))
return;
int cycle = (dist - 1) / 3;
int step = (dist - 1) % 3;
float decay = std::pow(decayBase, cycle);
float intensity = peakIntensity * decay;
if (intensity < 0.01f)
return;
int type = step;
if (isBrightSide)
type = (type + 2) % 3;
switch (type) {
case 0:
bins[segIdx].brightMod += 0.8f * intensity;
bins[segIdx].alphaMod -= 0.8f * intensity;
break;
case 1:
bins[segIdx].brightMod -= 0.8f * intensity;
bins[segIdx].alphaMod += 0.2f * intensity;
break;
case 2:
bins[segIdx].brightMod += 0.8f * intensity;
bins[segIdx].alphaMod += 0.2f * intensity;
break;
}
};
for (int d = 1; d <= 12; ++d) {
applyPattern(d, leftDominant, -1);
applyPattern(d, !leftDominant, 1);
}
}
}
// --- 4. Pre-calculate Colors ---
for (size_t i = 0; i < numBins; ++i) {
auto &b = bins[i];
QColor binColor;
if (m_useAlbumColors && !m_albumPalette.empty()) {
int palIdx = static_cast<int>(i);
if (m_mirrored)
palIdx = static_cast<int>(m_albumPalette.size()) - 1 -
static_cast<int>(i);
palIdx =
std::clamp(palIdx, 0, static_cast<int>(m_albumPalette.size()) - 1);
binColor = m_albumPalette[palIdx];
binColor = applyModifiers(binColor);
} else {
float hue = (float)i / (numBins - 1);
if (m_mirrored)
hue = 1.0f - hue;
binColor = QColor::fromHsvF(hue, 1.0f, 1.0f);
}
b.cachedColor = binColor;
}
}
// --- 5. Cepstral Thread Smoothing (mirrored mode only) ---
if (m_mirrored && !data.empty() && !data[0].cepstrum.empty()) {
const auto &raw = data[0].cepstrum;
if (m_smoothedCepstrum.size() != raw.size())
m_smoothedCepstrum.assign(raw.size(), 0.0f);
for (size_t i = 0; i < raw.size(); ++i)
m_smoothedCepstrum[i] = 0.15f * raw[i] + 0.85f * m_smoothedCepstrum[i];
}
update();
}
// ===== QRhiWidget GPU Rendering =====
void VisualizerWidget::initialize(QRhiCommandBuffer *cb) {
if (m_rhi != rhi()) {
m_fillPipeline.reset();
m_rhi = rhi();
}
if (!m_fillPipeline) {
m_ubufAlign = m_rhi->ubufAlignment();
// Vertex buffer: dynamic, sized for worst case
m_vbuf.reset(m_rhi->newBuffer(QRhiBuffer::Dynamic,
QRhiBuffer::VertexBuffer,
2048 * 6 * sizeof(float)));
m_vbuf->create();
// Uniform buffer: single MVP matrix (mirroring baked into vertices)
m_ubuf.reset(m_rhi->newBuffer(QRhiBuffer::Dynamic,
QRhiBuffer::UniformBuffer,
m_ubufAlign * 5));
m_ubuf->create();
// Shader resource bindings with dynamic UBO offset
m_srb.reset(m_rhi->newShaderResourceBindings());
m_srb->setBindings({QRhiShaderResourceBinding::uniformBufferWithDynamicOffset(
0, QRhiShaderResourceBinding::VertexStage, m_ubuf.get(), 64)});
m_srb->create();
// Load compiled shaders
QShader vs = loadShader(QStringLiteral(":/shaders/visualizer.vert.qsb"));
QShader fs = loadShader(QStringLiteral(":/shaders/visualizer.frag.qsb"));
// Vertex layout: [x, y, r, g, b, a] = 6 floats, 24 bytes stride
QRhiVertexInputLayout inputLayout;
inputLayout.setBindings({{6 * sizeof(float)}});
inputLayout.setAttributes(
{{0, 0, QRhiVertexInputAttribute::Float2, 0},
{0, 1, QRhiVertexInputAttribute::Float4, 2 * sizeof(float)}});
// Alpha blending
QRhiGraphicsPipeline::TargetBlend blend;
blend.enable = true;
blend.srcColor = QRhiGraphicsPipeline::SrcAlpha;
blend.dstColor = QRhiGraphicsPipeline::OneMinusSrcAlpha;
blend.srcAlpha = QRhiGraphicsPipeline::One;
blend.dstAlpha = QRhiGraphicsPipeline::OneMinusSrcAlpha;
// Fill pipeline (triangles)
m_fillPipeline.reset(m_rhi->newGraphicsPipeline());
m_fillPipeline->setShaderStages(
{{QRhiShaderStage::Vertex, vs}, {QRhiShaderStage::Fragment, fs}});
m_fillPipeline->setVertexInputLayout(inputLayout);
m_fillPipeline->setShaderResourceBindings(m_srb.get());
m_fillPipeline->setRenderPassDescriptor(renderTarget()->renderPassDescriptor());
m_fillPipeline->setTopology(QRhiGraphicsPipeline::Triangles);
m_fillPipeline->setTargetBlends({blend});
m_fillPipeline->setSampleCount(sampleCount());
m_fillPipeline->create();
// Line pipeline (same shader, line topology)
m_linePipeline.reset(m_rhi->newGraphicsPipeline());
m_linePipeline->setShaderStages(
{{QRhiShaderStage::Vertex, vs}, {QRhiShaderStage::Fragment, fs}});
m_linePipeline->setVertexInputLayout(inputLayout);
m_linePipeline->setShaderResourceBindings(m_srb.get());
m_linePipeline->setRenderPassDescriptor(renderTarget()->renderPassDescriptor());
m_linePipeline->setTopology(QRhiGraphicsPipeline::Lines);
m_linePipeline->setTargetBlends({blend});
m_linePipeline->setSampleCount(sampleCount());
m_linePipeline->create();
}
}
void VisualizerWidget::render(QRhiCommandBuffer *cb) {
if (!m_fillPipeline)
return;
const QSize outputSize = renderTarget()->pixelSize();
int w = width();
int h = height();
// Rebuild vertices when new data arrives OR when the widget has been resized
bool sizeChanged = (w != m_lastBuildW || h != m_lastBuildH);
if (m_dataDirty || sizeChanged) {
m_dataDirty = false;
m_lastBuildW = w;
m_lastBuildH = h;
if (m_mirrored) {
buildVertices(w * 0.55f, h / 2);
// Mirror into 4 quadrants directly (avoids multi-pass dynamic UBO
// issues on Android GPU drivers)
{
int fillFloats = m_fillVertexCount * 6;
std::vector<float> baseFill(m_vertices.begin(),
m_vertices.begin() + fillFloats);
std::vector<float> baseLine(m_vertices.begin() + fillFloats,
m_vertices.end());
m_vertices.clear();
auto mir = [](const std::vector<float> &src, std::vector<float> &dst,
float sx, float sy, float tx, float ty) {
for (size_t j = 0; j < src.size(); j += 6) {
dst.push_back(src[j] * sx + tx);
dst.push_back(src[j + 1] * sy + ty);
dst.insert(dst.end(), src.begin() + j + 2, src.begin() + j + 6);
}
};
mir(baseFill, m_vertices, 1, 1, 0, 0);
mir(baseFill, m_vertices, -1, 1, (float)w, 0);
mir(baseFill, m_vertices, 1, -1, 0, (float)h);
mir(baseFill, m_vertices, -1, -1, (float)w, (float)h);
m_fillVertexCount *= 4;
mir(baseLine, m_vertices, 1, 1, 0, 0);
mir(baseLine, m_vertices, -1, 1, (float)w, 0);
mir(baseLine, m_vertices, 1, -1, 0, (float)h);
mir(baseLine, m_vertices, -1, -1, (float)w, (float)h);
m_lineVertexCount *= 4;
}
buildCepstrumVertices(w, h);
} else {
buildVertices(w, h);
m_cepstrumVertexCount = 0;
}
}
// Prepare resource updates
QRhiResourceUpdateBatch *u = m_rhi->nextResourceUpdateBatch();
// Upload vertex data (main + cepstrum appended)
{
int mainSize = static_cast<int>(m_vertices.size() * sizeof(float));
int cepSize = static_cast<int>(m_cepstrumVerts.size() * sizeof(float));
int totalSize = mainSize + cepSize;
if (totalSize > 0) {
if (totalSize > m_vbuf->size()) {
m_vbuf->setSize(totalSize);
m_vbuf->create();
}
if (mainSize > 0)
u->updateDynamicBuffer(m_vbuf.get(), 0, mainSize, m_vertices.data());
if (cepSize > 0)
u->updateDynamicBuffer(m_vbuf.get(), mainSize, cepSize, m_cepstrumVerts.data());
}
}
// Single full-screen ortho MVP — mirroring is baked into vertex positions
QMatrix4x4 correction = m_rhi->clipSpaceCorrMatrix();
QMatrix4x4 proj;
proj.ortho(0, (float)w, (float)h, 0, -1, 1);
QMatrix4x4 mvp = correction * proj;
u->updateDynamicBuffer(m_ubuf.get(), 0, 64, mvp.constData());
// Begin render pass
cb->beginPass(renderTarget(), QColor(0, 0, 0, 255), {1.0f, 0}, u);
cb->setViewport({0, 0, (float)outputSize.width(),
(float)outputSize.height()});
const QRhiCommandBuffer::VertexInput vbufBinding(m_vbuf.get(), 0);
QRhiCommandBuffer::DynamicOffset dynOfs(0, 0);
if (m_fillVertexCount > 0) {
cb->setGraphicsPipeline(m_fillPipeline.get());
cb->setShaderResources(m_srb.get(), 1, &dynOfs);
cb->setVertexInput(0, 1, &vbufBinding);
cb->draw(m_fillVertexCount);
}
if (m_lineVertexCount > 0) {
cb->setGraphicsPipeline(m_linePipeline.get());
cb->setShaderResources(m_srb.get(), 1, &dynOfs);
cb->setVertexInput(0, 1, &vbufBinding);
cb->draw(m_lineVertexCount, 1, m_fillVertexCount, 0);
}
if (m_cepstrumVertexCount > 0) {
cb->setGraphicsPipeline(m_linePipeline.get());
cb->setShaderResources(m_srb.get(), 1, &dynOfs);
cb->setVertexInput(0, 1, &vbufBinding);
cb->draw(m_cepstrumVertexCount, 1, m_fillVertexCount + m_lineVertexCount, 0);
}
cb->endPass();
update();
}
void VisualizerWidget::releaseResources() {
m_linePipeline.reset();
m_fillPipeline.reset();
m_srb.reset();
m_ubuf.reset();
m_vbuf.reset();
}
// ===== Cepstral Thread Vertex Building =====
void VisualizerWidget::buildCepstrumVertices(int w, int h) {
m_cepstrumVerts.clear();
m_cepstrumVertexCount = 0;
if (m_smoothedCepstrum.empty())
return;
// Quefrency range: indices 12-600 (~80Hz to ~4000Hz pitch at 48kHz)
int qStart = 12;
int qEnd = std::min(600, (int)m_smoothedCepstrum.size());
if (qEnd <= qStart)
return;
// Find peak magnitude for normalization
float peak = 0.0f;
for (int i = qStart; i < qEnd; ++i)
peak = std::max(peak, std::abs(m_smoothedCepstrum[i]));
if (peak < 1e-7f)
return; // silence — don't draw
float invPeak = 1.0f / peak;
float maxDisp = w * 0.06f;
float cx = w * 0.5f;
// Color: unified color desaturated slightly, alpha ~0.45
float cr, cg, cb;
{
float ch, cs, cv;
m_unifiedColor.getHsvF(&ch, &cs, &cv);
cs *= 0.7f; // desaturate
QColor c = QColor::fromHsvF(ch, cs, cv);
cr = c.redF();
cg = c.greenF();
cb = c.blueF();
}
float ca = 0.45f;
// Build line segments with top/bottom edge fade
float fadeMargin = 0.08f; // fade over 8% of height at each end
float prevX = cx + m_smoothedCepstrum[qStart] * invPeak * maxDisp;
float prevY = 0.0f;
float prevT = 0.0f;
for (int i = qStart + 1; i < qEnd; ++i) {
float t = (float)(i - qStart) / (qEnd - qStart);
float y = t * h;
float x = cx + m_smoothedCepstrum[i] * invPeak * maxDisp;
// Fade alpha near top and bottom edges
auto edgeFade = [&](float tt) -> float {
if (tt < fadeMargin) return tt / fadeMargin;
if (tt > 1.0f - fadeMargin) return (1.0f - tt) / fadeMargin;
return 1.0f;
};
float a0 = ca * edgeFade(prevT);
float a1 = ca * edgeFade(t);
m_cepstrumVerts.insert(m_cepstrumVerts.end(),
{prevX, prevY, cr, cg, cb, a0,
x, y, cr, cg, cb, a1});
prevX = x;
prevY = y;
prevT = t;
}
m_cepstrumVertexCount = (int)m_cepstrumVerts.size() / 6;
}
// ===== Vertex Building (identical logic to old drawContent) =====
void VisualizerWidget::buildVertices(int w, int h) {
m_vertices.clear();
m_fillVertexCount = 0;
m_lineVertexCount = 0;
std::vector<float> lineVerts;
for (size_t ch = 0; ch < m_channels.size(); ++ch) {
if (ch >= m_data.size())
break;
const auto &freqs = m_data[ch].freqs;
const auto &bins = m_channels[ch].bins;
if (bins.empty() || freqs.size() < 2)
continue;
float xOffset = (ch == 1 && m_data.size() > 1) ? 1.005f : 1.0f;
size_t numBins = std::min(freqs.size(), bins.size());
for (size_t i = 0; i + 1 < numBins; ++i) {
// When inverted, read bin data in reverse order (highs left, lows right)
size_t di = m_inverted ? (numBins - 1 - i) : i;
size_t diN = m_inverted ? (numBins - 2 - i) : (i + 1);
const auto &b = bins[di];
const auto &bNext = bins[diN];
// --- Brightness ---
float avgEnergy =
std::clamp((b.primaryVisualDb + 80.0f) / 80.0f, 0.0f, 1.0f);
float baseBrightness = std::pow(avgEnergy, 0.5f);
float bMod = b.brightMod;
float bMult =
(bMod >= 0) ? (1.0f + bMod) : (1.0f / (1.0f - bMod * 2.0f));
float finalBrightness =
std::clamp(baseBrightness * bMult * m_brightness, 0.0f, 1.0f);
// --- Color ---
QColor dynamicBinColor = b.cachedColor;
float h_val, s, v, a;
dynamicBinColor.getHsvF(&h_val, &s, &v, &a);
dynamicBinColor = QColor::fromHsvF(
h_val, s, std::clamp(v * finalBrightness, 0.0f, 1.0f));
QColor fillColor, lineColor;
if (m_glass) {
float uh, us, uv, ua;
m_unifiedColor.getHsvF(&uh, &us, &uv, &ua);
fillColor = QColor::fromHsvF(
uh, us, std::clamp(uv * finalBrightness, 0.0f, 1.0f));
lineColor = dynamicBinColor;
} else {
fillColor = dynamicBinColor;
lineColor = dynamicBinColor;
}
// --- Alpha ---
float aMod = b.alphaMod;
float aMult = (aMod >= 0) ? (1.0f + aMod * 0.5f) : (1.0f + aMod);
if (aMult < 0.1f)
aMult = 0.1f;
float alpha = 0.4f + (avgEnergy - 0.5f) * m_contrast;
alpha = std::clamp(alpha * aMult, 0.0f, 1.0f);
// --- Edge fade: taper last bins to transparent near center gap ---
if (m_mirrored) {
int fadeBins = 4;
int fromEnd = (int)(numBins - 2) - (int)i;
if (fromEnd < fadeBins) {
float fade = (float)(fromEnd + 1) / (float)(fadeBins + 1);
fade = fade * fade; // ease-in for smoother taper
alpha *= fade;
}
}
fillColor.setAlphaF(alpha);
lineColor.setAlphaF(std::min(0.9f, alpha));
// --- Channel 1 tint ---
if (ch == 1 && m_data.size() > 1) {
int r, g, b_val, a_val;
fillColor.getRgb(&r, &g, &b_val, &a_val);
fillColor.setRgb(std::max(0, r - 40), std::max(0, g - 40),
std::min(255, b_val + 40), a_val);
lineColor.getRgb(&r, &g, &b_val, &a_val);
lineColor.setRgb(std::max(0, r - 40), std::max(0, g - 40),
std::min(255, b_val + 40), a_val);
}
// --- Geometry ---
float x1 = getX(freqs[i] * xOffset) * w;
float x2 = getX(freqs[i + 1] * xOffset) * w;
float barH1 =
std::clamp((b.visualDb + 80.0f) / 80.0f * h, 0.0f, (float)h);
float barH2 =
std::clamp((bNext.visualDb + 80.0f) / 80.0f * h, 0.0f, (float)h);
float anchorY = h;
float y1 = h - barH1;
float y2 = h - barH2;
float fr = fillColor.redF(), fg = fillColor.greenF(),
fb = fillColor.blueF(), fa = fillColor.alphaF();
// Triangle 1
m_vertices.insert(m_vertices.end(),
{x1, anchorY, fr, fg, fb, fa, x1, y1, fr, fg, fb, fa,
x2, y2, fr, fg, fb, fa});
// Triangle 2
m_vertices.insert(m_vertices.end(),
{x1, anchorY, fr, fg, fb, fa, x2, y2, fr, fg, fb, fa,
x2, anchorY, fr, fg, fb, fa});
m_fillVertexCount += 6;
float lr = lineColor.redF(), lg = lineColor.greenF(),
lb = lineColor.blueF(), la = lineColor.alphaF();
// Left edge
lineVerts.insert(lineVerts.end(),
{x1, anchorY, lr, lg, lb, la, x1, y1, lr, lg, lb, la});
// Right edge (last bin only)
if (i + 2 == freqs.size()) {
lineVerts.insert(
lineVerts.end(),
{x2, anchorY, lr, lg, lb, la, x2, y2, lr, lg, lb, la});
}
}
}
m_lineVertexCount = static_cast<int>(lineVerts.size()) / 6;
m_vertices.insert(m_vertices.end(), lineVerts.begin(), lineVerts.end());
}