Add BOOST, 3D depth, and gradient-angle knobs

- BOOST: global display level multiplier (0.75 - 2.5, 1.0 default)
- 3D: pseudo-3D bevel/shading on bars, LEDs, peaks and curve (0 = flat)
- G-ANG: 3D gradient angle (-180 - 180, 0 = top-lit)
- Apply CURVING rounded corners to bars and peaks in BAR mode
This commit is contained in:
Armin 2026-08-14 03:38:09 +02:00
commit aeb4b83281
7 changed files with 142 additions and 18 deletions

View file

@ -34,6 +34,9 @@ and a configurable display.
| GRID FADE | 0 1 | Fades inner grid lines; outer border stays | | GRID FADE | 0 1 | Fades inner grid lines; outer border stays |
| CURVING | 0 1 | LED corner radius (0 = square, 1 = rounded) | | CURVING | 0 1 | LED corner radius (0 = square, 1 = rounded) |
| TILT | -1 1 | Spectral tilt: + boosts highs / cuts lows, - does the reverse (0 = flat) | | TILT | -1 1 | Spectral tilt: + boosts highs / cuts lows, - does the reverse (0 = flat) |
| BOOST | 0.75 2.5 | Global display level multiplier (1.0 = unity) |
| 3D | 0 1 | Pseudo-3D bevel/shading on bars, LEDs, peaks and curve (0 = flat) |
| G-ANG | -180 180 | Gradient angle for the 3D shading (0 = top-lit, +/-90 = side-lit) |
## Building ## Building

View file

@ -105,7 +105,7 @@ void Analyser::computeFFT()
double norm = (db + tiltDb - minDb) / (maxDb - minDb); double norm = (db + tiltDb - minDb) / (maxDb - minDb);
norm = juce::jlimit (0.0, 1.0, norm); norm = juce::jlimit (0.0, 1.0, norm);
targets[b] = static_cast<float> (norm); targets[b] = static_cast<float> (norm * boost);
} }
} }

View file

@ -33,6 +33,8 @@ public:
void setNumBands (int n) noexcept; void setNumBands (int n) noexcept;
void setTilt (double amount) noexcept { tilt = amount; } void setTilt (double amount) noexcept { tilt = amount; }
double getTilt() const noexcept { return tilt; } double getTilt() const noexcept { return tilt; }
void setBoost (double amount) noexcept { boost = amount; }
double getBoost() const noexcept { return boost; }
double getGraceSeconds() const noexcept { return grace; } double getGraceSeconds() const noexcept { return grace; }
double getFalloffRate() const noexcept { return falloffRate; } double getFalloffRate() const noexcept { return falloffRate; }
@ -79,4 +81,8 @@ private:
// the centre of the log-frequency range. // the centre of the log-frequency range.
static constexpr double maxTiltDb = 18.0; static constexpr double maxTiltDb = 18.0;
double tilt = 0.0; double tilt = 0.0;
// Display boost: a global multiplier on the level the analyser shows
// (0.75 = quieter, 1.5 = louder, 1.0 = unity).
double boost = 1.0;
}; };

View file

@ -38,6 +38,8 @@ public:
const double gridZoomV = *params.getRawParameterValue ("gridzoom"); const double gridZoomV = *params.getRawParameterValue ("gridzoom");
const double gridFadeV = *params.getRawParameterValue ("gridfade"); const double gridFadeV = *params.getRawParameterValue ("gridfade");
const double curveV = *params.getRawParameterValue ("curving"); const double curveV = *params.getRawParameterValue ("curving");
const double depthV = *params.getRawParameterValue ("depth3d");
const double gradAngV = *params.getRawParameterValue ("gradangle");
analyser.setGraceSeconds (graceMs / 1000.0); analyser.setGraceSeconds (graceMs / 1000.0);
analyser.setFalloffRate (0.1 + falloff * 5.0); analyser.setFalloffRate (0.1 + falloff * 5.0);
@ -55,6 +57,8 @@ public:
gridZoom = static_cast<float> (gridZoomV); gridZoom = static_cast<float> (gridZoomV);
gridFade = static_cast<float> (gridFadeV); gridFade = static_cast<float> (gridFadeV);
curveAmount = static_cast<float> (curveV); curveAmount = static_cast<float> (curveV);
depthAmount = static_cast<float> (depthV);
gradAngle = static_cast<float> (gradAngV);
analyser.update (1.0 / 60.0); analyser.update (1.0 / 60.0);
repaint(); repaint();
@ -159,9 +163,49 @@ public:
} }
private: private:
// Build a top-lit 3D gradient across `bounds`. `angleDeg` rotates the
// gradient direction (0 = vertical/bright-top, +/-90 = horizontal), `bright`
// and `dark` scale the highlight/shadow strength (typically 0..1).
juce::ColourGradient make3DGradient (juce::Colour base, const juce::Rectangle<float>& bounds,
float angleDeg, float bright, float dark)
{
const float rad = juce::degreesToRadians (angleDeg);
const float dx = std::sin (rad);
const float dy = std::cos (rad);
const juce::Point<float> c = bounds.getCentre();
const float half = 0.5f * std::sqrt (bounds.getWidth() * bounds.getWidth()
+ bounds.getHeight() * bounds.getHeight());
const juce::Point<float> p1 = c - juce::Point<float> (dx, dy) * half;
const juce::Point<float> p2 = c + juce::Point<float> (dx, dy) * half;
juce::ColourGradient grad (base.brighter (bright), p1.x, p1.y,
base.darker (dark), p2.x, p2.y, false);
grad.addColour (0.5, base);
return grad;
}
// Fill a rounded rectangle with a top-lit 3D gradient. `depth` (0..1) blends
// between a flat solid fill and a fully raised/beveled look, angled by the
// G-ANG knob, so the two knobs control the 3D appearance.
void fill3D (juce::Graphics& g, juce::Colour base, const juce::Rectangle<float>& r,
float radius, float depth)
{
if (depth <= 0.001f)
{
g.setColour (base);
g.fillRoundedRectangle (r, radius);
return;
}
g.setGradientFill (make3DGradient (base, r, gradAngle, depth, depth));
g.fillRoundedRectangle (r, radius);
}
void drawBars (juce::Graphics& g, int n, const juce::Rectangle<float>& plot, void drawBars (juce::Graphics& g, int n, const juce::Rectangle<float>& plot,
float gap, float bandWidth, float baseY) float gap, float bandWidth, float baseY)
{ {
const float peakH = 3.0f;
const float peakRadius = curveAmount * juce::jmin (bandWidth, peakH) * 0.5f;
for (int i = 0; i < n; ++i) for (int i = 0; i < n; ++i)
{ {
const float level = analyser.getLevel (i); const float level = analyser.getLevel (i);
@ -171,12 +215,12 @@ private:
const float h = level * plot.getHeight(); const float h = level * plot.getHeight();
const float y = baseY - h; const float y = baseY - h;
g.setColour (lf.getTeal()); const float barRadius = curveAmount * juce::jmin (bandWidth, h, 8.0f) * 0.5f;
g.fillRect (x, y, bandWidth, h);
fill3D (g, lf.getTeal(), juce::Rectangle<float> (x, y, bandWidth, h), barRadius, depthAmount);
const float peakY = baseY - peak * plot.getHeight(); const float peakY = baseY - peak * plot.getHeight();
g.setColour (lf.getPeak()); fill3D (g, lf.getPeak(), juce::Rectangle<float> (x, peakY - 2.0f, bandWidth, peakH), peakRadius, depthAmount);
g.fillRect (x, peakY - 2.0f, bandWidth, 3.0f);
} }
} }
@ -237,14 +281,23 @@ private:
wave.lineTo (plot.getX(), baseY); wave.lineTo (plot.getX(), baseY);
wave.closeSubPath(); wave.closeSubPath();
// Brighter at the top, darker toward the bottom. // 3D: a top-lit gradient (bright edge fading to a darker underside),
juce::ColourGradient grad (lf.getTeal().brighter (0.35f), 0.0f, plot.getY(), // angled by the G-ANG knob. `depthAmount` blends from a flat fill (0)
lf.getTeal().darker (0.85f), 0.0f, plot.getBottom(), false); // to a fully raised look (1).
g.setGradientFill (grad); const juce::Colour base = lf.getTeal();
g.fillPath (wave); if (depthAmount <= 0.001f)
{
g.setColour (base);
g.fillPath (wave);
}
else
{
g.setGradientFill (make3DGradient (base, plot, gradAngle, 0.7f * depthAmount, depthAmount));
g.fillPath (wave);
}
// Bright outline tracing the waveform. // Bright top-edge outline (the "lit" highlight), stronger with depth.
g.setColour (lf.getTeal().brighter (0.5f)); g.setColour (base.brighter (0.3f + 0.5f * depthAmount));
g.strokePath (wave, juce::PathStrokeType (1.5f)); g.strokePath (wave, juce::PathStrokeType (1.5f));
// Smooth peak-hold wave: per-band peaks, smoothed with the same // Smooth peak-hold wave: per-band peaks, smoothed with the same
@ -287,7 +340,16 @@ private:
} }
peakWave.quadraticTo (ppts[n - 1], ppts[n - 1]); peakWave.quadraticTo (ppts[n - 1], ppts[n - 1]);
peakWave.lineTo (plot.getRight(), baseY - peaks[n - 1] * plot.getHeight()); peakWave.lineTo (plot.getRight(), baseY - peaks[n - 1] * plot.getHeight());
g.setColour (lf.getPeak()); // Peak wave gets the same top-lit 3D shading.
const juce::Colour pbase = lf.getPeak();
if (depthAmount <= 0.001f)
{
g.setColour (pbase);
}
else
{
g.setGradientFill (make3DGradient (pbase, plot, gradAngle, 0.7f * depthAmount, depthAmount));
}
g.strokePath (peakWave, juce::PathStrokeType (2.0f)); g.strokePath (peakWave, juce::PathStrokeType (2.0f));
} }
@ -319,16 +381,15 @@ private:
if (b < litCount) if (b < litCount)
{ {
const float frac = (b + 0.5f) / static_cast<float> (leds); const float frac = (b + 0.5f) / static_cast<float> (leds);
g.setColour (lf.getTeal().withMultipliedBrightness (0.35f + 0.65f * frac)); const juce::Colour base = lf.getTeal().withMultipliedBrightness (0.35f + 0.65f * frac);
g.fillRoundedRectangle (juce::Rectangle<float> (x, blockTop, bandWidth, blockLen), radius); fill3D (g, base, juce::Rectangle<float> (x, blockTop, bandWidth, blockLen), radius, depthAmount);
} }
// Peak honours the same discrete blocks: draw the peak block in the // Peak honours the same discrete blocks: draw the peak block in the
// peak colour (a single, fully-filled block). // peak colour (a single, fully-filled block).
if (b == peakBlock - 1) if (b == peakBlock - 1)
{ {
g.setColour (lf.getPeak()); fill3D (g, lf.getPeak(), juce::Rectangle<float> (x, blockTop, bandWidth, blockLen), radius, depthAmount);
g.fillRoundedRectangle (juce::Rectangle<float> (x, blockTop, bandWidth, blockLen), radius);
} }
} }
} }
@ -345,6 +406,8 @@ private:
float gridZoom = 1.0f; float gridZoom = 1.0f;
float gridFade = 0.0f; float gridFade = 0.0f;
float curveAmount = 0.0f; float curveAmount = 0.0f;
float depthAmount = 0.0f;
float gradAngle = 0.0f;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AnalyserComponent) JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AnalyserComponent)
}; };

View file

@ -41,6 +41,9 @@ BeamgridAudioProcessorEditor::BeamgridAudioProcessorEditor (BeamgridAudioProcess
setupKnob (gridfadeSlider, ""); setupKnob (gridfadeSlider, "");
setupKnob (curvingSlider, ""); setupKnob (curvingSlider, "");
setupKnob (tiltSlider, ""); setupKnob (tiltSlider, "");
setupKnob (boostSlider, "");
setupKnob (depth3dSlider, "");
setupKnob (gradangleSlider, "");
barsSlider.setNumDecimalPlacesToDisplay (0); barsSlider.setNumDecimalPlacesToDisplay (0);
modeSlider.setNumDecimalPlacesToDisplay (0); modeSlider.setNumDecimalPlacesToDisplay (0);
ledsSlider.setNumDecimalPlacesToDisplay (0); ledsSlider.setNumDecimalPlacesToDisplay (0);
@ -110,6 +113,21 @@ BeamgridAudioProcessorEditor::BeamgridAudioProcessorEditor (BeamgridAudioProcess
tiltLabel.setColour (juce::Label::textColourId, juce::Colours::lightgrey); tiltLabel.setColour (juce::Label::textColourId, juce::Colours::lightgrey);
addAndMakeVisible (tiltLabel); addAndMakeVisible (tiltLabel);
boostLabel.setText ("BOOST", juce::dontSendNotification);
boostLabel.setJustificationType (juce::Justification::centred);
boostLabel.setColour (juce::Label::textColourId, juce::Colours::lightgrey);
addAndMakeVisible (boostLabel);
depth3dLabel.setText ("3D", juce::dontSendNotification);
depth3dLabel.setJustificationType (juce::Justification::centred);
depth3dLabel.setColour (juce::Label::textColourId, juce::Colours::lightgrey);
addAndMakeVisible (depth3dLabel);
gradangleLabel.setText ("G-ANG", juce::dontSendNotification);
gradangleLabel.setJustificationType (juce::Justification::centred);
gradangleLabel.setColour (juce::Label::textColourId, juce::Colours::lightgrey);
addAndMakeVisible (gradangleLabel);
scaleBox.addItemList (juce::StringArray ("75%", "100%", "150%", "200%", "250%", "300%"), 1); scaleBox.addItemList (juce::StringArray ("75%", "100%", "150%", "200%", "250%", "300%"), 1);
scaleBox.setSelectedItemIndex (1); // 100% scaleBox.setSelectedItemIndex (1); // 100%
scaleBox.setColour (juce::ComboBox::backgroundColourId, juce::Colours::black.brighter (0.15f)); scaleBox.setColour (juce::ComboBox::backgroundColourId, juce::Colours::black.brighter (0.15f));
@ -167,6 +185,12 @@ BeamgridAudioProcessorEditor::BeamgridAudioProcessorEditor (BeamgridAudioProcess
processorRef.getParametersState(), "curving", curvingSlider); processorRef.getParametersState(), "curving", curvingSlider);
tiltAttachment = std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment>( tiltAttachment = std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment>(
processorRef.getParametersState(), "tilt", tiltSlider); processorRef.getParametersState(), "tilt", tiltSlider);
boostAttachment = std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment>(
processorRef.getParametersState(), "boost", boostSlider);
depth3dAttachment = std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment>(
processorRef.getParametersState(), "depth3d", depth3dSlider);
gradangleAttachment = std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment>(
processorRef.getParametersState(), "gradangle", gradangleSlider);
// Set these AFTER the attachments: SliderAttachment installs its own // Set these AFTER the attachments: SliderAttachment installs its own
// textFromValueFunction, so assigning afterwards makes ours take effect. // textFromValueFunction, so assigning afterwards makes ours take effect.
@ -259,7 +283,7 @@ void BeamgridAudioProcessorEditor::resized()
const int margin = 24; const int margin = 24;
const int gap = 14; const int gap = 14;
const int count = 13; const int count = 16;
const int titleW = 180; const int titleW = 180;
// Available width for the knob row (leave room on the right for the title). // Available width for the knob row (leave room on the right for the title).
@ -289,6 +313,9 @@ void BeamgridAudioProcessorEditor::resized()
{ gridfadeSlider, gridfadeLabel }, { gridfadeSlider, gridfadeLabel },
{ curvingSlider, curvingLabel }, { curvingSlider, curvingLabel },
{ tiltSlider, tiltLabel }, { tiltSlider, tiltLabel },
{ boostSlider, boostLabel },
{ depth3dSlider, depth3dLabel },
{ gradangleSlider, gradangleLabel },
}; };
const juce::Font labelFont (juce::FontOptions (11.0f)); const juce::Font labelFont (juce::FontOptions (11.0f));

View file

@ -35,6 +35,9 @@ private:
juce::Slider gridfadeSlider; juce::Slider gridfadeSlider;
juce::Slider curvingSlider; juce::Slider curvingSlider;
juce::Slider tiltSlider; juce::Slider tiltSlider;
juce::Slider boostSlider;
juce::Slider depth3dSlider;
juce::Slider gradangleSlider;
juce::Label graceLabel; juce::Label graceLabel;
juce::Label falloffLabel; juce::Label falloffLabel;
@ -49,6 +52,9 @@ private:
juce::Label gridfadeLabel; juce::Label gridfadeLabel;
juce::Label curvingLabel; juce::Label curvingLabel;
juce::Label tiltLabel; juce::Label tiltLabel;
juce::Label boostLabel;
juce::Label depth3dLabel;
juce::Label gradangleLabel;
juce::Label titleLabel; juce::Label titleLabel;
juce::Label statusLabel; juce::Label statusLabel;
@ -67,6 +73,9 @@ private:
std::unique_ptr<juce::AudioProcessorValueTreeState::SliderAttachment> gridfadeAttachment; std::unique_ptr<juce::AudioProcessorValueTreeState::SliderAttachment> gridfadeAttachment;
std::unique_ptr<juce::AudioProcessorValueTreeState::SliderAttachment> curvingAttachment; std::unique_ptr<juce::AudioProcessorValueTreeState::SliderAttachment> curvingAttachment;
std::unique_ptr<juce::AudioProcessorValueTreeState::SliderAttachment> tiltAttachment; std::unique_ptr<juce::AudioProcessorValueTreeState::SliderAttachment> tiltAttachment;
std::unique_ptr<juce::AudioProcessorValueTreeState::SliderAttachment> boostAttachment;
std::unique_ptr<juce::AudioProcessorValueTreeState::SliderAttachment> depth3dAttachment;
std::unique_ptr<juce::AudioProcessorValueTreeState::SliderAttachment> gradangleAttachment;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (BeamgridAudioProcessorEditor) JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (BeamgridAudioProcessorEditor)
}; };

View file

@ -71,6 +71,21 @@ BeamgridAudioProcessor::createParameterLayout()
juce::NormalisableRange<float> (-1.0f, 1.0f, 0.001f), 0.0f, juce::NormalisableRange<float> (-1.0f, 1.0f, 0.001f), 0.0f,
juce::AudioParameterFloatAttributes().withLabel (""))); juce::AudioParameterFloatAttributes().withLabel ("")));
params.push_back (std::make_unique<juce::AudioParameterFloat>(
"boost", "Boost",
juce::NormalisableRange<float> (0.75f, 2.5f, 0.001f), 1.0f,
juce::AudioParameterFloatAttributes().withLabel ("")));
params.push_back (std::make_unique<juce::AudioParameterFloat>(
"depth3d", "3D",
juce::NormalisableRange<float> (0.0f, 1.0f, 0.001f), 0.0f,
juce::AudioParameterFloatAttributes().withLabel ("")));
params.push_back (std::make_unique<juce::AudioParameterFloat>(
"gradangle", "Gradient Angle",
juce::NormalisableRange<float> (-180.0f, 180.0f, 0.5f), 0.0f,
juce::AudioParameterFloatAttributes().withLabel ("")));
juce::NormalisableRange<float> barsRange (8.0f, 128.0f, juce::NormalisableRange<float> barsRange (8.0f, 128.0f,
[] (float start, float end, float v) { return start + v * (end - start); }, // convertFrom0To1 [] (float start, float end, float v) { return start + v * (end - start); }, // convertFrom0To1
[] (float start, float end, float v) { return (v - start) / (end - start); }, // convertTo0To1 [] (float start, float end, float v) { return (v - start) / (end - start); }, // convertTo0To1
@ -150,6 +165,7 @@ void BeamgridAudioProcessor::processBlock (juce::AudioBuffer<float>& buffer,
juce::ScopedNoDenormals noDenormals; juce::ScopedNoDenormals noDenormals;
analyser.setTilt (*params.getRawParameterValue ("tilt")); analyser.setTilt (*params.getRawParameterValue ("tilt"));
analyser.setBoost (*params.getRawParameterValue ("boost"));
// Analyse the first channel; pass the audio through unchanged. // Analyse the first channel; pass the audio through unchanged.
if (buffer.getNumChannels() > 0) if (buffer.getNumChannels() > 0)