diff --git a/.gitignore b/.gitignore index f6a011e..85dd8f8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,18 @@ +# Prerequisites build/ *.o *.obj .DS_Store + +# Vim swap / undo files +*.swp +*.swo +*.sw? +*.un~ +*~ +.*.swp +.*.swo + +# Krita autosave / backup files +*.kra~ +*.kra.autosave diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..59b8b99 --- /dev/null +++ b/LICENSE @@ -0,0 +1,23 @@ +MIT License + +Copyright (c) 2026 Armin Jenewein - https://codeberg.org/armin/beamgrid + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + diff --git a/README.md b/README.md index f41e843..7c0bb4b 100644 --- a/README.md +++ b/README.md @@ -3,8 +3,6 @@ A JUCE-based VST3 / AU spectrum analyzer with a fully black UI, teal accents, and a configurable display. -![build](https://img.shields.io/badge/build-rolling-blue) - ## Features - **Three display modes** (top-right `MODE` knob): diff --git a/Source/Analyser.cpp b/Source/Analyser.cpp index 581f235..d15ce9d 100644 --- a/Source/Analyser.cpp +++ b/Source/Analyser.cpp @@ -9,7 +9,7 @@ void Analyser::prepare (double newSampleRate, int /*blockSize*/) sampleRate = newSampleRate > 0.0 ? newSampleRate : 44100.0; minFreq = 20.0; - maxFreq = sampleRate * 0.5; + maxFreq = juce::jmin (20000.0, sampleRate * 0.5); computeBandEdges(); } @@ -32,8 +32,6 @@ void Analyser::computeBandEdges() for (int b = 1; b <= n; ++b) if (bandBinEdges[b] <= bandBinEdges[b - 1]) bandBinEdges[b] = juce::jmin (static_cast (fftBins), bandBinEdges[b - 1] + 1); - - bandBinEdges[n] = fftBins; } void Analyser::setNumBands (int n) noexcept diff --git a/Source/AnalyserComponent.h b/Source/AnalyserComponent.h index 764158d..3b45f66 100644 --- a/Source/AnalyserComponent.h +++ b/Source/AnalyserComponent.h @@ -9,6 +9,8 @@ class AnalyserComponent : public juce::Component, public juce::Timer { public: + enum { ModeBars = 1, ModeWaveform = 2, ModeLED = 3 }; + AnalyserComponent (Analyser& analyserToUse, juce::AudioProcessorValueTreeState& paramsToUse, BeamgridLookAndFeel& lookAndFeelToUse) @@ -29,6 +31,9 @@ public: 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); @@ -39,6 +44,10 @@ public: // 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(); } @@ -57,12 +66,11 @@ public: 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 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). + // --- Gridlines (drawn first; labels drawn last so they stay readable) --- 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()) @@ -70,31 +78,61 @@ public: 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); + // --- 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); } + } - // Analyzer bands (teal) and peak markers (peak color). + 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); @@ -113,17 +151,147 @@ public: } } - static juce::String formatFreq (double f) + void drawWaveform (juce::Graphics& g, int n, const juce::Rectangle& plot, + float gap, float bandWidth, float baseY, float smooth) { - if (f >= 1000.0) - return juce::String (f / 1000.0, 1, false) + "k"; - return juce::String (static_cast (f)); + // 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); + } + } + } } -private: 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) }; diff --git a/Source/BeamgridLookAndFeel.h b/Source/BeamgridLookAndFeel.h index a2b93d2..2c51810 100644 --- a/Source/BeamgridLookAndFeel.h +++ b/Source/BeamgridLookAndFeel.h @@ -56,14 +56,5 @@ public: startAngle, angle, thickness); g.setColour (getTeal()); g.strokePath (value, juce::PathStrokeType (thickness)); - - // Pointer line. - juce::Path pointer; - const float pointerLen = radius * 0.78f; - pointer.addLineSegment (juce::Line (centre.x, centre.y, - centre.x + std::sin (angle) * pointerLen, - centre.y - std::cos (angle) * pointerLen), 2.0f); - g.setColour (juce::Colours::white); - g.strokePath (pointer, juce::PathStrokeType (2.5f)); } }; diff --git a/Source/PluginEditor.cpp b/Source/PluginEditor.cpp index 77bc4ca..d699832 100644 --- a/Source/PluginEditor.cpp +++ b/Source/PluginEditor.cpp @@ -7,7 +7,7 @@ BeamgridAudioProcessorEditor::BeamgridAudioProcessorEditor (BeamgridAudioProcess analyserComponent (p.getAnalyser(), p.getParametersState(), lookAndFeel) { setLookAndFeel (&lookAndFeel); - setSize (820, 460); + setSize (980, 480); // Make the editor freely resizable (with a corner resizer). The plugin // reports IPlugView::canResize() == true to the host, which is what FL @@ -33,7 +33,12 @@ BeamgridAudioProcessorEditor::BeamgridAudioProcessorEditor (BeamgridAudioProcess setupKnob (barfalloffSlider, ""); setupKnob (barsSlider, ""); setupKnob (hueSlider, ""); + setupKnob (modeSlider, ""); + setupKnob (ledsSlider, ""); + setupKnob (smoothSlider, ""); barsSlider.setNumDecimalPlacesToDisplay (0); + modeSlider.setNumDecimalPlacesToDisplay (0); + ledsSlider.setNumDecimalPlacesToDisplay (0); graceLabel.setText ("PEAK GRACE", juce::dontSendNotification); graceLabel.setJustificationType (juce::Justification::centred); @@ -60,6 +65,35 @@ BeamgridAudioProcessorEditor::BeamgridAudioProcessorEditor (BeamgridAudioProcess hueLabel.setColour (juce::Label::textColourId, juce::Colours::lightgrey); addAndMakeVisible (hueLabel); + modeLabel.setText ("MODE", juce::dontSendNotification); + modeLabel.setJustificationType (juce::Justification::centred); + modeLabel.setColour (juce::Label::textColourId, juce::Colours::lightgrey); + addAndMakeVisible (modeLabel); + + ledsLabel.setText ("LEDS", juce::dontSendNotification); + ledsLabel.setJustificationType (juce::Justification::centred); + ledsLabel.setColour (juce::Label::textColourId, juce::Colours::lightgrey); + addAndMakeVisible (ledsLabel); + + smoothLabel.setText ("SMOOTH", juce::dontSendNotification); + smoothLabel.setJustificationType (juce::Justification::centred); + smoothLabel.setColour (juce::Label::textColourId, juce::Colours::lightgrey); + addAndMakeVisible (smoothLabel); + + 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)); + scaleBox.setColour (juce::ComboBox::textColourId, juce::Colours::lightgrey); + scaleBox.setColour (juce::ComboBox::arrowColourId, juce::Colours::lightgrey); + scaleBox.onChange = [this] + { + const double pct = scaleBox.getText().retainCharacters ("0123456789.").getDoubleValue(); + const double scale = juce::jlimit (0.5, 3.0, pct / 100.0); + setScaleFactor (scale); + processorRef.getParametersState().state.setProperty ("uiScale", scale, nullptr); + }; + addAndMakeVisible (scaleBox); + titleLabel.setText ("BEAMGRID", juce::dontSendNotification); titleLabel.setJustificationType (juce::Justification::centredRight); titleLabel.setFont (juce::FontOptions (20.0f, juce::Font::bold)); @@ -68,7 +102,7 @@ BeamgridAudioProcessorEditor::BeamgridAudioProcessorEditor (BeamgridAudioProcess statusLabel.setFont (juce::FontOptions (11.0f)); statusLabel.setColour (juce::Label::textColourId, juce::Colours::grey); - statusLabel.setJustificationType (juce::Justification::centredLeft); + statusLabel.setJustificationType (juce::Justification::centredRight); juce::String status; status << "git " << juce::String (BEAMGRID_GIT_COMMIT).substring (0, 7); @@ -87,13 +121,69 @@ BeamgridAudioProcessorEditor::BeamgridAudioProcessorEditor (BeamgridAudioProcess processorRef.getParametersState(), "bars", barsSlider); hueAttachment = std::make_unique( processorRef.getParametersState(), "hue", hueSlider); + modeAttachment = std::make_unique( + processorRef.getParametersState(), "mode", modeSlider); + ledsAttachment = std::make_unique( + processorRef.getParametersState(), "leds", ledsSlider); + smoothAttachment = std::make_unique( + processorRef.getParametersState(), "smooth", smoothSlider); + + // Set these AFTER the attachments: SliderAttachment installs its own + // textFromValueFunction, so assigning afterwards makes ours take effect. + barsSlider.textFromValueFunction = [] (double v) + { + return juce::String (juce::roundToInt (v)); + }; + + modeSlider.textFromValueFunction = [] (double v) + { + const int m = juce::roundToInt (v); + return m == 2 ? juce::String ("SPECTRUM") + : m == 3 ? juce::String ("LED") + : juce::String ("BARS"); + }; + + // Force an immediate repaint of the text boxes now that our formatters + // are installed (otherwise they keep the attachment's initial text). + barsSlider.updateText(); + modeSlider.updateText(); + + processorRef.getParametersState().addParameterListener ("hue", this); + + // Restore the saved UI size + scale from the plugin state (the host saves + // this state with the project), so the window reopens the same way. + auto& uiState = processorRef.getParametersState().state; + const double storedScale = uiState.getProperty ("uiScale", 1.0); + + const double scales[] = { 0.75, 1.0, 1.5, 2.0, 2.5, 3.0 }; + int scaleIdx = 1; + for (int i = 0; i < 6; ++i) + if (std::abs (scales[i] - storedScale) < 0.001) + scaleIdx = i; + scaleBox.setSelectedItemIndex (scaleIdx); // triggers onChange -> setScaleFactor + + const int storedW = uiState.getProperty ("uiWidth", getWidth()); + const int storedH = uiState.getProperty ("uiHeight", getHeight()); + setSize (storedW, storedH); } BeamgridAudioProcessorEditor::~BeamgridAudioProcessorEditor() { + processorRef.getParametersState().removeParameterListener ("hue", this); setLookAndFeel (nullptr); } +void BeamgridAudioProcessorEditor::parameterChanged (const juce::String& parameterID, float newValue) +{ + if (parameterID == "hue") + { + // Update the shared LookAndFeel immediately and repaint the whole + // editor so every rotary knob's arc (and the title) recolours. + lookAndFeel.setHueTurns (newValue - 0.5f); + repaint(); + } +} + void BeamgridAudioProcessorEditor::paint (juce::Graphics& g) { g.fillAll (juce::Colours::black); @@ -108,27 +198,63 @@ void BeamgridAudioProcessorEditor::resized() const int knob = 64; const int y = getHeight() - panelH + 14; - analyserComponent.setBounds (0, 0, getWidth(), getHeight() - panelH); + // 32px blank bar across the very top of the plugin (the dropdown sits + // 16px below the top edge so it isn't glued to it). 30px blank strip on + // the right so the spectrum doesn't touch the right border. + const int topBarH = 32; + const int rightPad = 30; + analyserComponent.setBounds (0, topBarH, getWidth() - rightPad, getHeight() - panelH - topBarH); - const int margin = 32; - const int spacing = knob + 24; + const int margin = 24; + const int gap = 14; + const int count = 8; + const int titleW = 220; - barsSlider.setBounds (margin, y, knob, knob); - barsLabel.setBounds (margin, y + knob + 2, knob, 18); + // Available width for the knob row (leave room on the right for the title). + const int avail = getWidth() - margin * 2 - titleW; + int spacing = knob + gap; + const int minSpacing = knob + 4; + if ((count - 1) * spacing + knob > avail) + spacing = juce::jmax (minSpacing, (avail - knob) / (count - 1)); - graceSlider.setBounds (margin + spacing, y, knob, knob); - graceLabel.setBounds (margin + spacing, y + knob + 2, knob, 18); + struct KnobRow { juce::Slider& s; juce::Label& l; }; + const KnobRow knobs[] = { + { graceSlider, graceLabel }, + { falloffSlider, falloffLabel }, + { barfalloffSlider, barfalloffLabel }, + { barsSlider, barsLabel }, + { hueSlider, hueLabel }, + { modeSlider, modeLabel }, + { ledsSlider, ledsLabel }, + { smoothSlider, smoothLabel }, + }; - barfalloffSlider.setBounds (margin + spacing * 2, y, knob, knob); - barfalloffLabel.setBounds (margin + spacing * 2, y + knob + 2, knob, 18); + const juce::Font labelFont (juce::FontOptions (11.0f)); - falloffSlider.setBounds (margin + spacing * 3, y, knob, knob); - falloffLabel.setBounds (margin + spacing * 3, y + knob + 2, knob, 18); + // Knobs are drawn 16px higher than the panel baseline; the title and + // status bar below stay anchored to the original baseline. + const int knobY = y - 16; - hueSlider.setBounds (margin + spacing * 4, y, knob, knob); - hueLabel.setBounds (margin + spacing * 4, y + knob + 2, knob, 18); + for (int i = 0; i < count; ++i) + { + const int x = margin + i * spacing; + knobs[i].s.setBounds (x, knobY, knob, knob); - titleLabel.setBounds (getWidth() - 262, y - 6, 210, knob + 24); + // Let the label span the full spacing (wider than the knob) so longer + // names like "BAR FALLOFF" aren't clipped, centred under the knob. + const int labelW = spacing; + const int labelX = x + (knob - labelW) / 2; + knobs[i].l.setFont (labelFont); + knobs[i].l.setBounds (labelX, knobY + knob + 2, labelW, 18); + } - statusLabel.setBounds (8, getHeight() - 16, getWidth() - 16, 14); + titleLabel.setBounds (getWidth() - titleW - margin, y - 6, titleW, knob + 24); + statusLabel.setBounds (16, getHeight() - 16, getWidth() - 42, 14); + + // UI scaling dropdown, top-right (16px below the top edge). + scaleBox.setBounds (getWidth() - 96 - margin, 16, 96, 16); + + // Remember the current window size so it can be restored with the project. + processorRef.getParametersState().state.setProperty ("uiWidth", getWidth(), nullptr); + processorRef.getParametersState().state.setProperty ("uiHeight", getHeight(), nullptr); } diff --git a/Source/PluginEditor.h b/Source/PluginEditor.h index 6ca3522..d1d3674 100644 --- a/Source/PluginEditor.h +++ b/Source/PluginEditor.h @@ -5,7 +5,8 @@ #include "AnalyserComponent.h" #include "BeamgridLookAndFeel.h" -class BeamgridAudioProcessorEditor : public juce::AudioProcessorEditor +class BeamgridAudioProcessorEditor : public juce::AudioProcessorEditor, + public juce::AudioProcessorValueTreeState::Listener { public: explicit BeamgridAudioProcessorEditor (BeamgridAudioProcessor&); @@ -13,6 +14,7 @@ public: void paint (juce::Graphics&) override; void resized() override; + void parameterChanged (const juce::String& parameterID, float newValue) override; private: BeamgridAudioProcessor& processorRef; @@ -25,20 +27,31 @@ private: juce::Slider barfalloffSlider; juce::Slider barsSlider; juce::Slider hueSlider; + juce::Slider modeSlider; + juce::Slider ledsSlider; + juce::Slider smoothSlider; juce::Label graceLabel; juce::Label falloffLabel; juce::Label barfalloffLabel; juce::Label barsLabel; juce::Label hueLabel; + juce::Label modeLabel; + juce::Label ledsLabel; + juce::Label smoothLabel; juce::Label titleLabel; juce::Label statusLabel; + juce::ComboBox scaleBox; + std::unique_ptr graceAttachment; std::unique_ptr falloffAttachment; std::unique_ptr barfalloffAttachment; std::unique_ptr barsAttachment; std::unique_ptr hueAttachment; + std::unique_ptr modeAttachment; + std::unique_ptr ledsAttachment; + std::unique_ptr smoothAttachment; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (BeamgridAudioProcessorEditor) }; diff --git a/Source/PluginProcessor.cpp b/Source/PluginProcessor.cpp index 12bb616..3142105 100644 --- a/Source/PluginProcessor.cpp +++ b/Source/PluginProcessor.cpp @@ -41,12 +41,17 @@ BeamgridAudioProcessor::createParameterLayout() juce::NormalisableRange (0.0f, 1.0f, 0.001f), 0.5f, juce::AudioParameterFloatAttributes().withLabel (""))); + params.push_back (std::make_unique( + "smooth", "Smoothing", + juce::NormalisableRange (0.0f, 1.0f, 0.001f), 0.35f, + juce::AudioParameterFloatAttributes().withLabel (""))); + juce::NormalisableRange barsRange (8.0f, 128.0f, - [] (float, float, float v) { return v; }, // convertFrom0To1 - [] (float, float, float v) { return v; }, // convertTo0To1 - [] (float, float, float v) // snapToLegalValue + [] (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, float, float v) // snapToLegalValue { - const float steps[] = { 8.0f, 16.0f, 32.0f, 64.0f, 96.0f, 128.0f }; + const float steps[] = { 8.0f, 16.0f, 32.0f, 64.0f, 128.0f }; float best = steps[0]; float bestDist = std::abs (v - best); for (float s : steps) @@ -64,6 +69,12 @@ BeamgridAudioProcessor::createParameterLayout() params.push_back (std::make_unique( "bars", "Bars", barsRange, 64.0f)); + params.push_back (std::make_unique( + "mode", "Mode", 1, 3, 1)); + + params.push_back (std::make_unique( + "leds", "LEDs/Bar", 4, 48, 16)); + return { params.begin(), params.end() }; }