#pragma once #include "JuceHeader.h" #include "Analyser.h" #include "BeamgridLookAndFeel.h" // Black spectrum visualizer: teal analyzer bands and peak-colored markers. class AnalyserComponent : public juce::Component, public juce::Timer { public: enum { ModeBars = 1, ModeWaveform = 2, ModeLED = 3 }; AnalyserComponent (Analyser& analyserToUse, juce::AudioProcessorValueTreeState& paramsToUse, BeamgridLookAndFeel& lookAndFeelToUse) : analyser (analyserToUse), params (paramsToUse), lf (lookAndFeelToUse) { startTimerHz (60); } ~AnalyserComponent() override { stopTimer(); } void timerCallback() override { const double graceMs = *params.getRawParameterValue ("grace"); const double falloff = *params.getRawParameterValue ("falloff"); const double barfall = *params.getRawParameterValue ("barfalloff"); const int bars = juce::roundToInt (params.getRawParameterValue ("bars")->load()); const double hue = *params.getRawParameterValue ("hue"); const int mode = juce::roundToInt (params.getRawParameterValue ("mode")->load()); const int leds = juce::roundToInt (params.getRawParameterValue ("leds")->load()); const double smooth = *params.getRawParameterValue ("smooth"); analyser.setGraceSeconds (graceMs / 1000.0); analyser.setFalloffRate (0.1 + falloff * 5.0); // Bar falloff: higher knob -> faster bar descent (smaller release tau). analyser.setBarReleaseTau (2.0 - barfall * 1.98); analyser.setNumBands (bars); // HUE knob: 12 o'clock (0.5) = no rotation; left/right rotate +/- 180 deg. lf.setHueTurns (static_cast (hue - 0.5)); displayMode = mode; ledCount = leds; smoothAmount = static_cast (smooth); analyser.update (1.0 / 60.0); repaint(); } void paint (juce::Graphics& g) override { const auto area = getLocalBounds().toFloat().reduced (8.0f); const int n = analyser.getNumBands(); // Reserve margins for the axis legends. juce::Rectangle plot = area; plot.setLeft (area.getX() + 40.0f); plot.setBottom (area.getBottom() - 20.0f); const float gap = 2.0f; const float bandWidth = (plot.getWidth() - gap * (n + 1)) / static_cast (n); const float baseY = plot.getBottom(); const juce::Colour gridColour = lf.transform (juce::Colours::grey.brighter (0.2f)).withAlpha (0.32f); const juce::Colour labelColour = lf.transform (juce::Colours::grey); // --- Gridlines (drawn first; labels drawn last so they stay readable) --- const double freqTicks[] = { 20, 50, 100, 200, 500, 1000, 2000, 5000, 10000, 20000 }; for (double f : freqTicks) { if (f < analyser.getMinFreq() || f > analyser.getMaxFreq()) continue; const float frac = analyser.freqToFraction (f); const float x = plot.getX() + gap + frac * (plot.getWidth() - 2.0f * gap); g.setColour (gridColour); g.drawVerticalLine (static_cast (x), plot.getY(), plot.getBottom()); } const double dbTicks[] = { 0.0, -6.0, -12.0, -24.0, -36.0, -48.0 }; for (double db : dbTicks) { const float frac = analyser.dbToFraction (db); const float y = baseY - frac * plot.getHeight(); g.setColour (gridColour); g.drawHorizontalLine (static_cast (y), plot.getX(), plot.getRight()); } // --- Spectrum content (mode dependent) --- if (displayMode == ModeWaveform) drawWaveform (g, n, plot, gap, bandWidth, baseY, smoothAmount); else if (displayMode == ModeLED) drawLED (g, n, plot, gap, bandWidth, baseY); else drawBars (g, n, plot, gap, bandWidth, baseY); // --- Axis labels (on top of everything) --- g.setFont (juce::FontOptions (10.0f)); g.setColour (labelColour); for (double f : freqTicks) { if (f < analyser.getMinFreq() || f > analyser.getMaxFreq()) continue; const float frac = analyser.freqToFraction (f); const float x = plot.getX() + gap + frac * (plot.getWidth() - 2.0f * gap); g.drawText (formatFreq (f), x - 18.0f, plot.getBottom() + 3.0f, 36.0f, 14.0f, juce::Justification::centredTop, false); } for (double db : dbTicks) { const float frac = analyser.dbToFraction (db); const float y = baseY - frac * plot.getHeight(); g.drawText (juce::String (db, 0), area.getX(), y - 7.0f, 36.0f, 14.0f, juce::Justification::centredRight, false); } } static juce::String formatFreq (double f) { if (f >= 1000.0) return juce::String (f / 1000.0, 1, false) + "k"; return juce::String (static_cast (f)); } private: void drawBars (juce::Graphics& g, int n, const juce::Rectangle& plot, float gap, float bandWidth, float baseY) { for (int i = 0; i < n; ++i) { const float level = analyser.getLevel (i); const float peak = analyser.getPeak (i); const float x = plot.getX() + gap + i * (bandWidth + gap); const float h = level * plot.getHeight(); const float y = baseY - h; g.setColour (lf.getTeal()); g.fillRect (x, y, bandWidth, h); const float peakY = baseY - peak * plot.getHeight(); g.setColour (lf.getPeak()); g.fillRect (x, peakY - 2.0f, bandWidth, 3.0f); } } void drawWaveform (juce::Graphics& g, int n, const juce::Rectangle& plot, float gap, float bandWidth, float baseY, float smooth) { // Gather the band levels and apply 1-2-1 smoothing passes. `smooth` // (0..1) blends each pass toward the averaged value, so 0 = untouched // (edgy/spiky) and 1 = heavily smoothed. const float t = juce::jlimit (0.0f, 1.0f, smooth); juce::HeapBlock levels (static_cast (n)); for (int i = 0; i < n; ++i) levels[i] = analyser.getLevel (i); for (int pass = 0; pass < 4; ++pass) { juce::HeapBlock tmp (static_cast (n)); for (int i = 0; i < n; ++i) { const float a = levels[juce::jmax (0, i - 1)]; const float b = levels[i]; const float c = levels[juce::jmin (n - 1, i + 1)]; const float avg = a * 0.25f + b * 0.5f + c * 0.25f; tmp[i] = b * (1.0f - t) + avg * t; } for (int i = 0; i < n; ++i) levels[i] = tmp[i]; } // Trace the curve. `smooth` controls how much the band levels are // averaged (above); the path itself is straight segments between band // centres, so at smooth = 0 it stays raw/edgy and at 1 it is rounded // by the heavy level smoothing. juce::Path wave; wave.startNewSubPath (plot.getX() + gap, baseY); for (int i = 0; i < n; ++i) { const float x = plot.getX() + gap + i * (bandWidth + gap) + bandWidth * 0.5f; const float y = baseY - levels[i] * plot.getHeight(); wave.lineTo (x, y); } wave.lineTo (plot.getRight() - gap, baseY); wave.closeSubPath(); // Brighter at the top, darker toward the bottom. juce::ColourGradient grad (lf.getTeal().brighter (0.35f), 0.0f, plot.getY(), lf.getTeal().darker (0.85f), 0.0f, plot.getBottom(), false); g.setGradientFill (grad); g.fillPath (wave); // Bright outline tracing the waveform. g.setColour (lf.getTeal().brighter (0.5f)); g.strokePath (wave, juce::PathStrokeType (1.5f)); // Smooth peak-hold wave: per-band peaks, smoothed with the same // amount as the main curve and traced as a matching spline. juce::HeapBlock peaks (static_cast (n)); for (int i = 0; i < n; ++i) peaks[i] = analyser.getPeak (i); for (int pass = 0; pass < 4; ++pass) { juce::HeapBlock tmp (static_cast (n)); for (int i = 0; i < n; ++i) { const float a = peaks[juce::jmax (0, i - 1)]; const float b = peaks[i]; const float c = peaks[juce::jmin (n - 1, i + 1)]; const float avg = a * 0.25f + b * 0.5f + c * 0.25f; tmp[i] = b * (1.0f - t) + avg * t; } for (int i = 0; i < n; ++i) peaks[i] = tmp[i]; } juce::HeapBlock> ppts (static_cast (n)); for (int i = 0; i < n; ++i) { const float x = plot.getX() + gap + i * (bandWidth + gap) + bandWidth * 0.5f; const float y = baseY - peaks[i] * plot.getHeight(); ppts[i] = { x, y }; } juce::Path peakWave; peakWave.startNewSubPath (ppts[0]); for (int i = 0; i < n - 1; ++i) { const juce::Point mid = { (ppts[i].x + ppts[i + 1].x) * 0.5f, (ppts[i].y + ppts[i + 1].y) * 0.5f }; peakWave.quadraticTo (ppts[i], mid); } peakWave.quadraticTo (ppts[n - 1], ppts[n - 1]); g.setColour (lf.getPeak()); g.strokePath (peakWave, juce::PathStrokeType (2.0f)); } void drawLED (juce::Graphics& g, int n, const juce::Rectangle& plot, float gap, float bandWidth, float baseY) { const int leds = juce::jmax (1, ledCount); const float segH = plot.getHeight() / static_cast (leds); const float blockLen = juce::jmax (1.0f, segH - 1.0f); for (int i = 0; i < n; ++i) { const float level = analyser.getLevel (i); const float peak = analyser.getPeak (i); const int litCount = juce::jlimit (0, leds, juce::roundToInt (level * leds)); const int peakBlock = juce::jlimit (0, leds, juce::roundToInt (peak * leds)); // top lit block (1-based) const float x = plot.getX() + gap + i * (bandWidth + gap); for (int b = 0; b < leds; ++b) { const float blockBottom = baseY - b * segH; const float blockTop = blockBottom - blockLen; if (b < litCount) { const float frac = (b + 0.5f) / static_cast (leds); g.setColour (lf.getTeal().withMultipliedBrightness (0.35f + 0.65f * frac)); g.fillRect (x, blockTop, bandWidth, blockLen); } // Peak honours the same discrete blocks: draw the peak block in the // peak colour (a single, fully-filled block). if (b == peakBlock - 1) { g.setColour (lf.getPeak()); g.fillRect (x, blockTop, bandWidth, blockLen); } } } } Analyser& analyser; juce::AudioProcessorValueTreeState& params; BeamgridLookAndFeel& lf; int displayMode = ModeBars; int ledCount = 16; float smoothAmount = 0.35f; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AnalyserComponent) };