#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: 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"); 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)); 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); // Frequency gridlines + labels (log spaced). const double freqTicks[] = { 20, 50, 100, 200, 500, 1000, 2000, 5000, 10000, 20000 }; g.setFont (juce::FontOptions (10.0f)); 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()); g.setColour (labelColour); g.drawText (formatFreq (f), x - 18.0f, plot.getBottom() + 3.0f, 36.0f, 14.0f, juce::Justification::centredTop, false); } // Level gridlines + labels (dB). 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()); g.setColour (labelColour); g.drawText (juce::String (db, 0), area.getX(), y - 7.0f, 36.0f, 14.0f, juce::Justification::centredRight, false); } // Analyzer bands (teal) and peak markers (peak color). 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); } } 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: Analyser& analyser; juce::AudioProcessorValueTreeState& params; BeamgridLookAndFeel& lf; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AnalyserComponent) };