781 lines
26 KiB
C++
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());
|
|
}
|