// src/VisualizerWidget.cpp #include "VisualizerWidget.h" #include #include #include #include #include #include #include #include #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& palette) { m_albumPalette.clear(); int targetLen = static_cast(m_customBins.size()) - 1; if (palette.empty()) return; for (int i = 0; i < targetLen; ++i) { int idx = (i * static_cast(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& 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 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 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 brightMods(freqs.size() - 1, 0.0f); std::vector 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)); } } } }