diff --git a/README.md b/README.md index 4fa54ba..9ee74dd 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,9 @@ and a configurable display. | GRID FADE | 0 – 1 | Fades inner grid lines; outer border stays | | CURVING | 0 – 1 | LED corner radius (0 = square, 1 = rounded) | | 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 diff --git a/Source/Analyser.cpp b/Source/Analyser.cpp index a6766d7..884d6de 100644 --- a/Source/Analyser.cpp +++ b/Source/Analyser.cpp @@ -105,7 +105,7 @@ void Analyser::computeFFT() double norm = (db + tiltDb - minDb) / (maxDb - minDb); norm = juce::jlimit (0.0, 1.0, norm); - targets[b] = static_cast (norm); + targets[b] = static_cast (norm * boost); } } diff --git a/Source/Analyser.h b/Source/Analyser.h index 0f878b9..8138229 100644 --- a/Source/Analyser.h +++ b/Source/Analyser.h @@ -33,6 +33,8 @@ public: void setNumBands (int n) noexcept; void setTilt (double amount) noexcept { tilt = amount; } 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 getFalloffRate() const noexcept { return falloffRate; } @@ -79,4 +81,8 @@ private: // the centre of the log-frequency range. static constexpr double maxTiltDb = 18.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; }; diff --git a/Source/AnalyserComponent.h b/Source/AnalyserComponent.h index 042a078..6a4efa1 100644 --- a/Source/AnalyserComponent.h +++ b/Source/AnalyserComponent.h @@ -38,6 +38,8 @@ public: const double gridZoomV = *params.getRawParameterValue ("gridzoom"); const double gridFadeV = *params.getRawParameterValue ("gridfade"); const double curveV = *params.getRawParameterValue ("curving"); + const double depthV = *params.getRawParameterValue ("depth3d"); + const double gradAngV = *params.getRawParameterValue ("gradangle"); analyser.setGraceSeconds (graceMs / 1000.0); analyser.setFalloffRate (0.1 + falloff * 5.0); @@ -55,6 +57,8 @@ public: gridZoom = static_cast (gridZoomV); gridFade = static_cast (gridFadeV); curveAmount = static_cast (curveV); + depthAmount = static_cast (depthV); + gradAngle = static_cast (gradAngV); analyser.update (1.0 / 60.0); repaint(); @@ -159,9 +163,49 @@ public: } 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& 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 c = bounds.getCentre(); + const float half = 0.5f * std::sqrt (bounds.getWidth() * bounds.getWidth() + + bounds.getHeight() * bounds.getHeight()); + const juce::Point p1 = c - juce::Point (dx, dy) * half; + const juce::Point p2 = c + juce::Point (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& 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& plot, 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) { const float level = analyser.getLevel (i); @@ -171,12 +215,12 @@ private: const float h = level * plot.getHeight(); const float y = baseY - h; - g.setColour (lf.getTeal()); - g.fillRect (x, y, bandWidth, h); + const float barRadius = curveAmount * juce::jmin (bandWidth, h, 8.0f) * 0.5f; + + fill3D (g, lf.getTeal(), juce::Rectangle (x, y, bandWidth, h), barRadius, depthAmount); const float peakY = baseY - peak * plot.getHeight(); - g.setColour (lf.getPeak()); - g.fillRect (x, peakY - 2.0f, bandWidth, 3.0f); + fill3D (g, lf.getPeak(), juce::Rectangle (x, peakY - 2.0f, bandWidth, peakH), peakRadius, depthAmount); } } @@ -237,14 +281,23 @@ private: wave.lineTo (plot.getX(), 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); + // 3D: a top-lit gradient (bright edge fading to a darker underside), + // angled by the G-ANG knob. `depthAmount` blends from a flat fill (0) + // to a fully raised look (1). + const juce::Colour base = lf.getTeal(); + 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. - g.setColour (lf.getTeal().brighter (0.5f)); + // Bright top-edge outline (the "lit" highlight), stronger with depth. + g.setColour (base.brighter (0.3f + 0.5f * depthAmount)); g.strokePath (wave, juce::PathStrokeType (1.5f)); // 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.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)); } @@ -319,16 +381,15 @@ private: if (b < litCount) { const float frac = (b + 0.5f) / static_cast (leds); - g.setColour (lf.getTeal().withMultipliedBrightness (0.35f + 0.65f * frac)); - g.fillRoundedRectangle (juce::Rectangle (x, blockTop, bandWidth, blockLen), radius); + const juce::Colour base = lf.getTeal().withMultipliedBrightness (0.35f + 0.65f * frac); + fill3D (g, base, juce::Rectangle (x, blockTop, bandWidth, blockLen), radius, depthAmount); } // 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.fillRoundedRectangle (juce::Rectangle (x, blockTop, bandWidth, blockLen), radius); + fill3D (g, lf.getPeak(), juce::Rectangle (x, blockTop, bandWidth, blockLen), radius, depthAmount); } } } @@ -345,6 +406,8 @@ private: float gridZoom = 1.0f; float gridFade = 0.0f; float curveAmount = 0.0f; + float depthAmount = 0.0f; + float gradAngle = 0.0f; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AnalyserComponent) }; diff --git a/Source/PluginEditor.cpp b/Source/PluginEditor.cpp index 112267d..2466644 100644 --- a/Source/PluginEditor.cpp +++ b/Source/PluginEditor.cpp @@ -41,6 +41,9 @@ BeamgridAudioProcessorEditor::BeamgridAudioProcessorEditor (BeamgridAudioProcess setupKnob (gridfadeSlider, ""); setupKnob (curvingSlider, ""); setupKnob (tiltSlider, ""); + setupKnob (boostSlider, ""); + setupKnob (depth3dSlider, ""); + setupKnob (gradangleSlider, ""); barsSlider.setNumDecimalPlacesToDisplay (0); modeSlider.setNumDecimalPlacesToDisplay (0); ledsSlider.setNumDecimalPlacesToDisplay (0); @@ -110,6 +113,21 @@ BeamgridAudioProcessorEditor::BeamgridAudioProcessorEditor (BeamgridAudioProcess tiltLabel.setColour (juce::Label::textColourId, juce::Colours::lightgrey); 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.setSelectedItemIndex (1); // 100% scaleBox.setColour (juce::ComboBox::backgroundColourId, juce::Colours::black.brighter (0.15f)); @@ -167,6 +185,12 @@ BeamgridAudioProcessorEditor::BeamgridAudioProcessorEditor (BeamgridAudioProcess processorRef.getParametersState(), "curving", curvingSlider); tiltAttachment = std::make_unique( processorRef.getParametersState(), "tilt", tiltSlider); + boostAttachment = std::make_unique( + processorRef.getParametersState(), "boost", boostSlider); + depth3dAttachment = std::make_unique( + processorRef.getParametersState(), "depth3d", depth3dSlider); + gradangleAttachment = std::make_unique( + processorRef.getParametersState(), "gradangle", gradangleSlider); // Set these AFTER the attachments: SliderAttachment installs its own // textFromValueFunction, so assigning afterwards makes ours take effect. @@ -259,7 +283,7 @@ void BeamgridAudioProcessorEditor::resized() const int margin = 24; const int gap = 14; - const int count = 13; + const int count = 16; const int titleW = 180; // Available width for the knob row (leave room on the right for the title). @@ -289,6 +313,9 @@ void BeamgridAudioProcessorEditor::resized() { gridfadeSlider, gridfadeLabel }, { curvingSlider, curvingLabel }, { tiltSlider, tiltLabel }, + { boostSlider, boostLabel }, + { depth3dSlider, depth3dLabel }, + { gradangleSlider, gradangleLabel }, }; const juce::Font labelFont (juce::FontOptions (11.0f)); diff --git a/Source/PluginEditor.h b/Source/PluginEditor.h index f3be7a4..2acb2a4 100644 --- a/Source/PluginEditor.h +++ b/Source/PluginEditor.h @@ -35,6 +35,9 @@ private: juce::Slider gridfadeSlider; juce::Slider curvingSlider; juce::Slider tiltSlider; + juce::Slider boostSlider; + juce::Slider depth3dSlider; + juce::Slider gradangleSlider; juce::Label graceLabel; juce::Label falloffLabel; @@ -49,6 +52,9 @@ private: juce::Label gridfadeLabel; juce::Label curvingLabel; juce::Label tiltLabel; + juce::Label boostLabel; + juce::Label depth3dLabel; + juce::Label gradangleLabel; juce::Label titleLabel; juce::Label statusLabel; @@ -67,6 +73,9 @@ private: std::unique_ptr gridfadeAttachment; std::unique_ptr curvingAttachment; std::unique_ptr tiltAttachment; + std::unique_ptr boostAttachment; + std::unique_ptr depth3dAttachment; + std::unique_ptr gradangleAttachment; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (BeamgridAudioProcessorEditor) }; diff --git a/Source/PluginProcessor.cpp b/Source/PluginProcessor.cpp index 58118ca..8790642 100644 --- a/Source/PluginProcessor.cpp +++ b/Source/PluginProcessor.cpp @@ -71,6 +71,21 @@ BeamgridAudioProcessor::createParameterLayout() juce::NormalisableRange (-1.0f, 1.0f, 0.001f), 0.0f, juce::AudioParameterFloatAttributes().withLabel (""))); + params.push_back (std::make_unique( + "boost", "Boost", + juce::NormalisableRange (0.75f, 2.5f, 0.001f), 1.0f, + juce::AudioParameterFloatAttributes().withLabel (""))); + + params.push_back (std::make_unique( + "depth3d", "3D", + juce::NormalisableRange (0.0f, 1.0f, 0.001f), 0.0f, + juce::AudioParameterFloatAttributes().withLabel (""))); + + params.push_back (std::make_unique( + "gradangle", "Gradient Angle", + juce::NormalisableRange (-180.0f, 180.0f, 0.5f), 0.0f, + juce::AudioParameterFloatAttributes().withLabel (""))); + juce::NormalisableRange barsRange (8.0f, 128.0f, [] (float start, float end, float v) { return start + v * (end - start); }, // convertFrom0To1 [] (float start, float end, float v) { return (v - start) / (end - start); }, // convertTo0To1 @@ -150,6 +165,7 @@ void BeamgridAudioProcessor::processBlock (juce::AudioBuffer& buffer, juce::ScopedNoDenormals noDenormals; analyser.setTilt (*params.getRawParameterValue ("tilt")); + analyser.setBoost (*params.getRawParameterValue ("boost")); // Analyse the first channel; pass the audio through unchanged. if (buffer.getNumChannels() > 0)