aluf/src/VisualizerWidget.cpp

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));
}
}
}
}