469 lines
17 KiB
C++
469 lines
17 KiB
C++
// src/VisualizerWidget.cpp
|
|
|
|
#include "VisualizerWidget.h"
|
|
#include <QPainter>
|
|
#include <QPainterPath>
|
|
#include <QMouseEvent>
|
|
#include <QApplication>
|
|
#include <QLinearGradient>
|
|
#include <cmath>
|
|
#include <algorithm>
|
|
#include <numeric>
|
|
|
|
#ifndef M_PI
|
|
#define M_PI 3.14159265358979323846
|
|
#endif
|
|
|
|
VisualizerWidget::VisualizerWidget(QWidget* parent) : QWidget(parent) {
|
|
setAttribute(Qt::WA_OpaquePaintEvent);
|
|
setNumBins(26);
|
|
}
|
|
|
|
void VisualizerWidget::mouseReleaseEvent(QMouseEvent* event) {
|
|
if (event->button() == Qt::LeftButton) {
|
|
emit tapDetected();
|
|
}
|
|
QWidget::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::setParams(bool glass, bool focus, bool trails, bool albumColors, bool shadow, bool mirrored, float hue, float contrast, float brightness) {
|
|
m_glass = glass;
|
|
m_focus = focus;
|
|
m_trailsEnabled = trails;
|
|
m_useAlbumColors = albumColors;
|
|
m_shadowMode = shadow;
|
|
m_mirrored = mirrored;
|
|
m_hueFactor = hue;
|
|
m_contrast = contrast;
|
|
m_brightness = brightness;
|
|
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);
|
|
}
|
|
|
|
void VisualizerWidget::updateData(const std::vector<AudioEngine::FrameData>& data) {
|
|
if (QApplication::applicationState() != Qt::ApplicationActive || !isVisible()) return;
|
|
m_data = data;
|
|
|
|
if (m_channels.size() != data.size()) m_channels.resize(data.size());
|
|
|
|
for (size_t ch = 0; ch < data.size(); ++ch) {
|
|
const auto& db = data[ch].db;
|
|
const auto& primaryDb = data[ch].primaryDb; // Access Primary DB
|
|
|
|
size_t numBins = db.size();
|
|
auto& bins = m_channels[ch].bins;
|
|
if (bins.size() != numBins) bins.resize(numBins);
|
|
|
|
for (size_t i = 0; i < numBins; ++i) {
|
|
auto& bin = bins[i];
|
|
float rawVal = db[i];
|
|
float primaryVal = (i < primaryDb.size()) ? primaryDb[i] : rawVal;
|
|
|
|
// 1. Calculate Responsiveness (Simplified Physics)
|
|
float responsiveness = 0.2f;
|
|
|
|
// 2. Update Visual Bar Height (Mixed Signal)
|
|
bin.visualDb = (bin.visualDb * (1.0f - responsiveness)) + (rawVal * responsiveness);
|
|
|
|
// 3. Update Primary Visual DB (Steady Signal for Pattern)
|
|
float patternResp = 0.1f;
|
|
bin.primaryVisualDb = (bin.primaryVisualDb * (1.0f - patternResp)) + (primaryVal * patternResp);
|
|
|
|
// 4. Trail Physics
|
|
bin.trailLife = std::max(bin.trailLife - 0.02f, 0.0f);
|
|
|
|
float flux = rawVal - bin.lastRawDb;
|
|
bin.lastRawDb = rawVal;
|
|
|
|
if (flux > 0) {
|
|
float jumpTarget = bin.visualDb + (flux * 1.5f);
|
|
|
|
if (jumpTarget > bin.trailDb) {
|
|
bin.trailDb = jumpTarget;
|
|
bin.trailLife = 1.0f;
|
|
bin.trailThickness = 2.0f;
|
|
}
|
|
}
|
|
|
|
if (bin.trailDb < bin.visualDb) bin.trailDb = bin.visualDb;
|
|
}
|
|
}
|
|
update();
|
|
}
|
|
|
|
void VisualizerWidget::paintEvent(QPaintEvent*) {
|
|
if (QApplication::applicationState() != Qt::ApplicationActive || !isVisible()) return;
|
|
|
|
QPainter p(this);
|
|
p.fillRect(rect(), Qt::black);
|
|
p.setRenderHint(QPainter::Antialiasing);
|
|
|
|
if (m_data.empty()) return;
|
|
|
|
int w = width();
|
|
int h = height();
|
|
|
|
if (m_mirrored) {
|
|
int hw = w / 2;
|
|
int hh = h / 2;
|
|
|
|
// Top-Left
|
|
p.save();
|
|
drawContent(p, hw, hh);
|
|
p.restore();
|
|
|
|
// Top-Right (Mirror X)
|
|
p.save();
|
|
p.translate(w, 0);
|
|
p.scale(-1, 1);
|
|
drawContent(p, hw, hh);
|
|
p.restore();
|
|
|
|
// Bottom-Left (Mirror Y)
|
|
p.save();
|
|
p.translate(0, h);
|
|
p.scale(1, -1);
|
|
drawContent(p, hw, hh);
|
|
p.restore();
|
|
|
|
// Bottom-Right (Mirror XY)
|
|
p.save();
|
|
p.translate(w, h);
|
|
p.scale(-1, -1);
|
|
drawContent(p, hw, hh);
|
|
p.restore();
|
|
} else {
|
|
drawContent(p, w, h);
|
|
}
|
|
}
|
|
|
|
void VisualizerWidget::drawContent(QPainter& p, int w, int h) {
|
|
auto getScreenY = [&](float normY) {
|
|
float screenH = normY * h;
|
|
return m_shadowMode ? screenH : h - screenH;
|
|
};
|
|
|
|
// --- Unified Glass Color Logic ---
|
|
QColor unifiedColor = Qt::white;
|
|
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; // Invert hue for mirrored mode
|
|
if (frameHue < 0) frameHue += 1.0f;
|
|
|
|
// --- MWA Filter for Hue ---
|
|
float angle = frameHue * 2.0f * M_PI;
|
|
m_hueHistory.push_back({std::cos(angle), std::sin(angle)});
|
|
if (m_hueHistory.size() > 40) m_hueHistory.pop_front(); // ~0.6s smoothing
|
|
|
|
float avgCos = 0.0f;
|
|
float avgSin = 0.0f;
|
|
for (const auto& pair : m_hueHistory) {
|
|
avgCos += pair.first;
|
|
avgSin += pair.second;
|
|
}
|
|
|
|
float smoothedAngle = std::atan2(avgSin, avgCos);
|
|
float smoothedHue = smoothedAngle / (2.0f * M_PI);
|
|
if (smoothedHue < 0.0f) smoothedHue += 1.0f;
|
|
|
|
unifiedColor = QColor::fromHsvF(smoothedHue, 1.0, 1.0);
|
|
}
|
|
|
|
// --- Draw Trails First (Behind) ---
|
|
if (m_trailsEnabled) {
|
|
for (size_t ch = 0; ch < m_channels.size(); ++ch) {
|
|
const auto& freqs = m_data[ch].freqs;
|
|
const auto& bins = m_channels[ch].bins;
|
|
if (bins.empty()) continue;
|
|
|
|
float xOffset = (ch == 1 && m_data.size() > 1) ? 1.002f : 1.0f;
|
|
|
|
for (size_t i = 0; i < freqs.size() - 1; ++i) {
|
|
const auto& b = bins[i];
|
|
const auto& bNext = bins[i+1];
|
|
|
|
if (b.trailLife < 0.01f) continue;
|
|
|
|
float saturation = 1.0f - std::sqrt(b.trailLife);
|
|
float alpha = b.trailLife * 0.6f;
|
|
|
|
QColor c;
|
|
if (m_useAlbumColors && !m_albumPalette.empty()) {
|
|
int palIdx = i;
|
|
if (m_mirrored) palIdx = m_albumPalette.size() - 1 - i;
|
|
palIdx = std::clamp(palIdx, 0, (int)m_albumPalette.size() - 1);
|
|
|
|
QColor base = m_albumPalette[palIdx];
|
|
base = applyModifiers(base);
|
|
float h_val, s, v, a;
|
|
base.getHsvF(&h_val, &s, &v, &a);
|
|
c = QColor::fromHsvF(h_val, s * saturation, v, alpha);
|
|
} else {
|
|
float hue = (float)i / freqs.size();
|
|
if (m_mirrored) hue = 1.0f - hue;
|
|
c = QColor::fromHsvF(hue, saturation, 1.0f, alpha);
|
|
}
|
|
|
|
float x1 = getX(freqs[i] * xOffset) * w;
|
|
float x2 = getX(freqs[i+1] * xOffset) * w;
|
|
|
|
float y1 = getScreenY((b.trailDb + 80.0f) / 80.0f);
|
|
float y2 = getScreenY((bNext.trailDb + 80.0f) / 80.0f);
|
|
|
|
p.setPen(QPen(c, b.trailThickness));
|
|
p.drawLine(QPointF(x1, y1), QPointF(x2, y2));
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- Draw Bars (Trapezoids) ---
|
|
for (size_t ch = 0; ch < m_channels.size(); ++ch) {
|
|
const auto& freqs = m_data[ch].freqs;
|
|
const auto& bins = m_channels[ch].bins;
|
|
if (bins.empty()) continue;
|
|
|
|
// 1. Calculate Raw Energy (Using Primary DB for Pattern)
|
|
std::vector<float> rawEnergy(bins.size());
|
|
for (size_t j = 0; j < bins.size(); ++j) {
|
|
// Use primaryVisualDb for the pattern calculation to keep it stable
|
|
rawEnergy[j] = std::clamp((bins[j].primaryVisualDb + 80.0f) / 80.0f, 0.0f, 1.0f);
|
|
}
|
|
|
|
// 2. Auto-Balance Highs vs Lows (Dynamic Normalization)
|
|
size_t splitIdx = bins.size() / 2;
|
|
float maxLow = 0.01f;
|
|
float maxHigh = 0.01f;
|
|
|
|
for (size_t j = 0; j < splitIdx; ++j) maxLow = std::max(maxLow, rawEnergy[j]);
|
|
for (size_t j = splitIdx; j < bins.size(); ++j) maxHigh = std::max(maxHigh, rawEnergy[j]);
|
|
|
|
float trebleBoost = maxLow / maxHigh;
|
|
trebleBoost = std::clamp(trebleBoost, 1.0f, 40.0f);
|
|
|
|
std::vector<float> vertexEnergy(bins.size());
|
|
float globalMax = 0.001f;
|
|
|
|
for (size_t j = 0; j < bins.size(); ++j) {
|
|
float val = rawEnergy[j];
|
|
|
|
if (j >= splitIdx) {
|
|
float t = (float)(j - splitIdx) / (bins.size() - splitIdx);
|
|
float boost = 1.0f + (trebleBoost - 1.0f) * t;
|
|
val *= boost;
|
|
}
|
|
|
|
float compressed = std::tanh(val);
|
|
vertexEnergy[j] = compressed;
|
|
if (compressed > globalMax) globalMax = compressed;
|
|
}
|
|
|
|
// 3. Global Normalization
|
|
for (float& v : vertexEnergy) {
|
|
v = std::clamp(v / globalMax, 0.0f, 1.0f);
|
|
}
|
|
|
|
// 4. Calculate Segment Modifiers (Procedural Pattern)
|
|
std::vector<float> brightMods(freqs.size() - 1, 0.0f);
|
|
std::vector<float> alphaMods(freqs.size() - 1, 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];
|
|
|
|
// Is this vertex a local peak?
|
|
if (curr > prev && curr > next) {
|
|
bool leftDominant = (prev > next);
|
|
float sharpness = std::min(curr - prev, curr - next);
|
|
|
|
float peakIntensity = std::clamp(std::pow(sharpness * 10.0f, 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) ? (i - dist) : (i + dist - 1);
|
|
if (segIdx < 0 || segIdx >= (int)brightMods.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: // Ghost (Bright + Trans)
|
|
brightMods[segIdx] += 0.8f * intensity;
|
|
alphaMods[segIdx] -= 0.8f * intensity;
|
|
break;
|
|
case 1: // Shadow (Dark + Opaque)
|
|
brightMods[segIdx] -= 0.8f * intensity;
|
|
alphaMods[segIdx] += 0.2f * intensity;
|
|
break;
|
|
case 2: // Highlight (Bright + Opaque)
|
|
brightMods[segIdx] += 0.8f * intensity;
|
|
alphaMods[segIdx] += 0.2f * intensity;
|
|
break;
|
|
}
|
|
};
|
|
|
|
for (int d = 1; d <= 12; ++d) {
|
|
applyPattern(d, leftDominant, -1);
|
|
applyPattern(d, !leftDominant, 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
float xOffset = (ch == 1 && m_data.size() > 1) ? 1.005f : 1.0f;
|
|
|
|
for (size_t i = 0; i < freqs.size() - 1; ++i) {
|
|
const auto& b = bins[i];
|
|
const auto& bNext = bins[i+1];
|
|
|
|
QColor binColor;
|
|
if (m_useAlbumColors && !m_albumPalette.empty()) {
|
|
int palIdx = i;
|
|
if (m_mirrored) palIdx = m_albumPalette.size() - 1 - i;
|
|
palIdx = std::clamp(palIdx, 0, (int)m_albumPalette.size() - 1);
|
|
|
|
binColor = m_albumPalette[palIdx];
|
|
binColor = applyModifiers(binColor);
|
|
} else {
|
|
float hue = (float)i / (freqs.size() - 1);
|
|
if (m_mirrored) hue = 1.0f - hue;
|
|
binColor = QColor::fromHsvF(hue, 1.0f, 1.0f);
|
|
}
|
|
|
|
// Base Brightness from Energy (Using Primary for stability)
|
|
float avgEnergy = (vertexEnergy[i] + vertexEnergy[i+1]) / 2.0f;
|
|
float baseBrightness = std::pow(avgEnergy, 0.5f);
|
|
|
|
// Apply Brightness Modifier
|
|
float bMod = brightMods[i];
|
|
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);
|
|
|
|
float h_val, s, v, a;
|
|
binColor.getHsvF(&h_val, &s, &v, &a);
|
|
v = std::clamp(v * finalBrightness, 0.0f, 1.0f);
|
|
QColor dynamicBinColor = QColor::fromHsvF(h_val, s, v);
|
|
|
|
QColor fillColor, lineColor;
|
|
if (m_glass) {
|
|
float uh, us, uv, ua;
|
|
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;
|
|
}
|
|
|
|
// Apply Alpha Modifier
|
|
float aMod = alphaMods[i];
|
|
float aMult = (aMod >= 0) ? (1.0f + aMod * 0.5f) : (1.0f + aMod);
|
|
if (aMult < 0.1f) aMult = 0.1f;
|
|
|
|
float intensity = avgEnergy;
|
|
float alpha = 0.4f + (intensity - 0.5f) * m_contrast;
|
|
alpha = std::clamp(alpha * aMult, 0.0f, 1.0f);
|
|
|
|
fillColor.setAlphaF(alpha);
|
|
lineColor.setAlphaF(0.9f);
|
|
|
|
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);
|
|
}
|
|
|
|
float x1 = getX(freqs[i] * xOffset) * w;
|
|
float x2 = getX(freqs[i+1] * xOffset) * w;
|
|
|
|
// Use visualDb (Mixed) for Height
|
|
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 y1, y2, anchorY;
|
|
if (m_shadowMode) {
|
|
anchorY = 0;
|
|
y1 = barH1;
|
|
y2 = barH2;
|
|
} else {
|
|
anchorY = h;
|
|
y1 = h - barH1;
|
|
y2 = h - barH2;
|
|
}
|
|
|
|
QPainterPath fillPath;
|
|
fillPath.moveTo(x1, anchorY);
|
|
fillPath.lineTo(x1, y1);
|
|
fillPath.lineTo(x2, y2);
|
|
fillPath.lineTo(x2, anchorY);
|
|
fillPath.closeSubpath();
|
|
p.fillPath(fillPath, fillColor);
|
|
|
|
p.setPen(QPen(lineColor, 1));
|
|
p.drawLine(QPointF(x1, anchorY), QPointF(x1, y1));
|
|
if (i == freqs.size() - 2) {
|
|
p.drawLine(QPointF(x2, anchorY), QPointF(x2, y2));
|
|
}
|
|
}
|
|
}
|
|
} |