add LICENSE, update README, update source

This commit is contained in:
Armin 2026-08-14 01:33:20 +02:00
commit e0177ed26f
9 changed files with 395 additions and 53 deletions

14
.gitignore vendored
View file

@ -1,4 +1,18 @@
# Prerequisites
build/ build/
*.o *.o
*.obj *.obj
.DS_Store .DS_Store
# Vim swap / undo files
*.swp
*.swo
*.sw?
*.un~
*~
.*.swp
.*.swo
# Krita autosave / backup files
*.kra~
*.kra.autosave

23
LICENSE Normal file
View file

@ -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.

View file

@ -3,8 +3,6 @@
A JUCE-based VST3 / AU spectrum analyzer with a fully black UI, teal accents, A JUCE-based VST3 / AU spectrum analyzer with a fully black UI, teal accents,
and a configurable display. and a configurable display.
![build](https://img.shields.io/badge/build-rolling-blue)
## Features ## Features
- **Three display modes** (top-right `MODE` knob): - **Three display modes** (top-right `MODE` knob):

View file

@ -9,7 +9,7 @@ void Analyser::prepare (double newSampleRate, int /*blockSize*/)
sampleRate = newSampleRate > 0.0 ? newSampleRate : 44100.0; sampleRate = newSampleRate > 0.0 ? newSampleRate : 44100.0;
minFreq = 20.0; minFreq = 20.0;
maxFreq = sampleRate * 0.5; maxFreq = juce::jmin (20000.0, sampleRate * 0.5);
computeBandEdges(); computeBandEdges();
} }
@ -32,8 +32,6 @@ void Analyser::computeBandEdges()
for (int b = 1; b <= n; ++b) for (int b = 1; b <= n; ++b)
if (bandBinEdges[b] <= bandBinEdges[b - 1]) if (bandBinEdges[b] <= bandBinEdges[b - 1])
bandBinEdges[b] = juce::jmin (static_cast<int> (fftBins), bandBinEdges[b - 1] + 1); bandBinEdges[b] = juce::jmin (static_cast<int> (fftBins), bandBinEdges[b - 1] + 1);
bandBinEdges[n] = fftBins;
} }
void Analyser::setNumBands (int n) noexcept void Analyser::setNumBands (int n) noexcept

View file

@ -9,6 +9,8 @@ class AnalyserComponent : public juce::Component,
public juce::Timer public juce::Timer
{ {
public: public:
enum { ModeBars = 1, ModeWaveform = 2, ModeLED = 3 };
AnalyserComponent (Analyser& analyserToUse, AnalyserComponent (Analyser& analyserToUse,
juce::AudioProcessorValueTreeState& paramsToUse, juce::AudioProcessorValueTreeState& paramsToUse,
BeamgridLookAndFeel& lookAndFeelToUse) BeamgridLookAndFeel& lookAndFeelToUse)
@ -29,6 +31,9 @@ public:
const double barfall = *params.getRawParameterValue ("barfalloff"); const double barfall = *params.getRawParameterValue ("barfalloff");
const int bars = juce::roundToInt (params.getRawParameterValue ("bars")->load()); const int bars = juce::roundToInt (params.getRawParameterValue ("bars")->load());
const double hue = *params.getRawParameterValue ("hue"); 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.setGraceSeconds (graceMs / 1000.0);
analyser.setFalloffRate (0.1 + falloff * 5.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. // HUE knob: 12 o'clock (0.5) = no rotation; left/right rotate +/- 180 deg.
lf.setHueTurns (static_cast<float> (hue - 0.5)); lf.setHueTurns (static_cast<float> (hue - 0.5));
displayMode = mode;
ledCount = leds;
smoothAmount = static_cast<float> (smooth);
analyser.update (1.0 / 60.0); analyser.update (1.0 / 60.0);
repaint(); repaint();
} }
@ -60,9 +69,8 @@ public:
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); 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 }; const double freqTicks[] = { 20, 50, 100, 200, 500, 1000, 2000, 5000, 10000, 20000 };
g.setFont (juce::FontOptions (10.0f));
for (double f : freqTicks) for (double f : freqTicks)
{ {
if (f < analyser.getMinFreq() || f > analyser.getMaxFreq()) if (f < analyser.getMinFreq() || f > analyser.getMaxFreq())
@ -70,31 +78,61 @@ public:
const float frac = analyser.freqToFraction (f); const float frac = analyser.freqToFraction (f);
const float x = plot.getX() + gap + frac * (plot.getWidth() - 2.0f * gap); const float x = plot.getX() + gap + frac * (plot.getWidth() - 2.0f * gap);
g.setColour (gridColour); g.setColour (gridColour);
g.drawVerticalLine (static_cast<int> (x), plot.getY(), plot.getBottom()); g.drawVerticalLine (static_cast<int> (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 }; const double dbTicks[] = { 0.0, -6.0, -12.0, -24.0, -36.0, -48.0 };
for (double db : dbTicks) for (double db : dbTicks)
{ {
const float frac = analyser.dbToFraction (db); const float frac = analyser.dbToFraction (db);
const float y = baseY - frac * plot.getHeight(); const float y = baseY - frac * plot.getHeight();
g.setColour (gridColour); g.setColour (gridColour);
g.drawHorizontalLine (static_cast<int> (y), plot.getX(), plot.getRight()); g.drawHorizontalLine (static_cast<int> (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); 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, g.drawText (juce::String (db, 0), area.getX(), y - 7.0f, 36.0f, 14.0f,
juce::Justification::centredRight, false); 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<int> (f));
}
private:
void drawBars (juce::Graphics& g, int n, const juce::Rectangle<float>& plot,
float gap, float bandWidth, float baseY)
{
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);
@ -113,17 +151,147 @@ public:
} }
} }
static juce::String formatFreq (double f) void drawWaveform (juce::Graphics& g, int n, const juce::Rectangle<float>& plot,
float gap, float bandWidth, float baseY, float smooth)
{ {
if (f >= 1000.0) // Gather the band levels and apply 1-2-1 smoothing passes. `smooth`
return juce::String (f / 1000.0, 1, false) + "k"; // (0..1) blends each pass toward the averaged value, so 0 = untouched
return juce::String (static_cast<int> (f)); // (edgy/spiky) and 1 = heavily smoothed.
const float t = juce::jlimit (0.0f, 1.0f, smooth);
juce::HeapBlock<float> levels (static_cast<size_t> (n));
for (int i = 0; i < n; ++i)
levels[i] = analyser.getLevel (i);
for (int pass = 0; pass < 4; ++pass)
{
juce::HeapBlock<float> tmp (static_cast<size_t> (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<float> peaks (static_cast<size_t> (n));
for (int i = 0; i < n; ++i)
peaks[i] = analyser.getPeak (i);
for (int pass = 0; pass < 4; ++pass)
{
juce::HeapBlock<float> tmp (static_cast<size_t> (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<juce::Point<float>> ppts (static_cast<size_t> (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<float> 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<float>& plot,
float gap, float bandWidth, float baseY)
{
const int leds = juce::jmax (1, ledCount);
const float segH = plot.getHeight() / static_cast<float> (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<float> (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; Analyser& analyser;
juce::AudioProcessorValueTreeState& params; juce::AudioProcessorValueTreeState& params;
BeamgridLookAndFeel& lf; BeamgridLookAndFeel& lf;
int displayMode = ModeBars;
int ledCount = 16;
float smoothAmount = 0.35f;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AnalyserComponent) JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AnalyserComponent)
}; };

View file

@ -56,14 +56,5 @@ public:
startAngle, angle, thickness); startAngle, angle, thickness);
g.setColour (getTeal()); g.setColour (getTeal());
g.strokePath (value, juce::PathStrokeType (thickness)); g.strokePath (value, juce::PathStrokeType (thickness));
// Pointer line.
juce::Path pointer;
const float pointerLen = radius * 0.78f;
pointer.addLineSegment (juce::Line<float> (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));
} }
}; };

View file

@ -7,7 +7,7 @@ BeamgridAudioProcessorEditor::BeamgridAudioProcessorEditor (BeamgridAudioProcess
analyserComponent (p.getAnalyser(), p.getParametersState(), lookAndFeel) analyserComponent (p.getAnalyser(), p.getParametersState(), lookAndFeel)
{ {
setLookAndFeel (&lookAndFeel); setLookAndFeel (&lookAndFeel);
setSize (820, 460); setSize (980, 480);
// Make the editor freely resizable (with a corner resizer). The plugin // Make the editor freely resizable (with a corner resizer). The plugin
// reports IPlugView::canResize() == true to the host, which is what FL // reports IPlugView::canResize() == true to the host, which is what FL
@ -33,7 +33,12 @@ BeamgridAudioProcessorEditor::BeamgridAudioProcessorEditor (BeamgridAudioProcess
setupKnob (barfalloffSlider, ""); setupKnob (barfalloffSlider, "");
setupKnob (barsSlider, ""); setupKnob (barsSlider, "");
setupKnob (hueSlider, ""); setupKnob (hueSlider, "");
setupKnob (modeSlider, "");
setupKnob (ledsSlider, "");
setupKnob (smoothSlider, "");
barsSlider.setNumDecimalPlacesToDisplay (0); barsSlider.setNumDecimalPlacesToDisplay (0);
modeSlider.setNumDecimalPlacesToDisplay (0);
ledsSlider.setNumDecimalPlacesToDisplay (0);
graceLabel.setText ("PEAK GRACE", juce::dontSendNotification); graceLabel.setText ("PEAK GRACE", juce::dontSendNotification);
graceLabel.setJustificationType (juce::Justification::centred); graceLabel.setJustificationType (juce::Justification::centred);
@ -60,6 +65,35 @@ BeamgridAudioProcessorEditor::BeamgridAudioProcessorEditor (BeamgridAudioProcess
hueLabel.setColour (juce::Label::textColourId, juce::Colours::lightgrey); hueLabel.setColour (juce::Label::textColourId, juce::Colours::lightgrey);
addAndMakeVisible (hueLabel); 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.setText ("BEAMGRID", juce::dontSendNotification);
titleLabel.setJustificationType (juce::Justification::centredRight); titleLabel.setJustificationType (juce::Justification::centredRight);
titleLabel.setFont (juce::FontOptions (20.0f, juce::Font::bold)); titleLabel.setFont (juce::FontOptions (20.0f, juce::Font::bold));
@ -68,7 +102,7 @@ BeamgridAudioProcessorEditor::BeamgridAudioProcessorEditor (BeamgridAudioProcess
statusLabel.setFont (juce::FontOptions (11.0f)); statusLabel.setFont (juce::FontOptions (11.0f));
statusLabel.setColour (juce::Label::textColourId, juce::Colours::grey); statusLabel.setColour (juce::Label::textColourId, juce::Colours::grey);
statusLabel.setJustificationType (juce::Justification::centredLeft); statusLabel.setJustificationType (juce::Justification::centredRight);
juce::String status; juce::String status;
status << "git " << juce::String (BEAMGRID_GIT_COMMIT).substring (0, 7); status << "git " << juce::String (BEAMGRID_GIT_COMMIT).substring (0, 7);
@ -87,13 +121,69 @@ BeamgridAudioProcessorEditor::BeamgridAudioProcessorEditor (BeamgridAudioProcess
processorRef.getParametersState(), "bars", barsSlider); processorRef.getParametersState(), "bars", barsSlider);
hueAttachment = std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment>( hueAttachment = std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment>(
processorRef.getParametersState(), "hue", hueSlider); processorRef.getParametersState(), "hue", hueSlider);
modeAttachment = std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment>(
processorRef.getParametersState(), "mode", modeSlider);
ledsAttachment = std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment>(
processorRef.getParametersState(), "leds", ledsSlider);
smoothAttachment = std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment>(
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() BeamgridAudioProcessorEditor::~BeamgridAudioProcessorEditor()
{ {
processorRef.getParametersState().removeParameterListener ("hue", this);
setLookAndFeel (nullptr); 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) void BeamgridAudioProcessorEditor::paint (juce::Graphics& g)
{ {
g.fillAll (juce::Colours::black); g.fillAll (juce::Colours::black);
@ -108,27 +198,63 @@ void BeamgridAudioProcessorEditor::resized()
const int knob = 64; const int knob = 64;
const int y = getHeight() - panelH + 14; 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 margin = 24;
const int spacing = knob + 24; const int gap = 14;
const int count = 8;
const int titleW = 220;
barsSlider.setBounds (margin, y, knob, knob); // Available width for the knob row (leave room on the right for the title).
barsLabel.setBounds (margin, y + knob + 2, knob, 18); 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); struct KnobRow { juce::Slider& s; juce::Label& l; };
graceLabel.setBounds (margin + spacing, y + knob + 2, knob, 18); 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); const juce::Font labelFont (juce::FontOptions (11.0f));
barfalloffLabel.setBounds (margin + spacing * 2, y + knob + 2, knob, 18);
falloffSlider.setBounds (margin + spacing * 3, y, knob, knob); // Knobs are drawn 16px higher than the panel baseline; the title and
falloffLabel.setBounds (margin + spacing * 3, y + knob + 2, knob, 18); // status bar below stay anchored to the original baseline.
const int knobY = y - 16;
hueSlider.setBounds (margin + spacing * 4, y, knob, knob); for (int i = 0; i < count; ++i)
hueLabel.setBounds (margin + spacing * 4, y + knob + 2, knob, 18); {
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);
} }

View file

@ -5,7 +5,8 @@
#include "AnalyserComponent.h" #include "AnalyserComponent.h"
#include "BeamgridLookAndFeel.h" #include "BeamgridLookAndFeel.h"
class BeamgridAudioProcessorEditor : public juce::AudioProcessorEditor class BeamgridAudioProcessorEditor : public juce::AudioProcessorEditor,
public juce::AudioProcessorValueTreeState::Listener
{ {
public: public:
explicit BeamgridAudioProcessorEditor (BeamgridAudioProcessor&); explicit BeamgridAudioProcessorEditor (BeamgridAudioProcessor&);
@ -13,6 +14,7 @@ public:
void paint (juce::Graphics&) override; void paint (juce::Graphics&) override;
void resized() override; void resized() override;
void parameterChanged (const juce::String& parameterID, float newValue) override;
private: private:
BeamgridAudioProcessor& processorRef; BeamgridAudioProcessor& processorRef;
@ -25,20 +27,31 @@ private:
juce::Slider barfalloffSlider; juce::Slider barfalloffSlider;
juce::Slider barsSlider; juce::Slider barsSlider;
juce::Slider hueSlider; juce::Slider hueSlider;
juce::Slider modeSlider;
juce::Slider ledsSlider;
juce::Slider smoothSlider;
juce::Label graceLabel; juce::Label graceLabel;
juce::Label falloffLabel; juce::Label falloffLabel;
juce::Label barfalloffLabel; juce::Label barfalloffLabel;
juce::Label barsLabel; juce::Label barsLabel;
juce::Label hueLabel; juce::Label hueLabel;
juce::Label modeLabel;
juce::Label ledsLabel;
juce::Label smoothLabel;
juce::Label titleLabel; juce::Label titleLabel;
juce::Label statusLabel; juce::Label statusLabel;
juce::ComboBox scaleBox;
std::unique_ptr<juce::AudioProcessorValueTreeState::SliderAttachment> graceAttachment; std::unique_ptr<juce::AudioProcessorValueTreeState::SliderAttachment> graceAttachment;
std::unique_ptr<juce::AudioProcessorValueTreeState::SliderAttachment> falloffAttachment; std::unique_ptr<juce::AudioProcessorValueTreeState::SliderAttachment> falloffAttachment;
std::unique_ptr<juce::AudioProcessorValueTreeState::SliderAttachment> barfalloffAttachment; std::unique_ptr<juce::AudioProcessorValueTreeState::SliderAttachment> barfalloffAttachment;
std::unique_ptr<juce::AudioProcessorValueTreeState::SliderAttachment> barsAttachment; std::unique_ptr<juce::AudioProcessorValueTreeState::SliderAttachment> barsAttachment;
std::unique_ptr<juce::AudioProcessorValueTreeState::SliderAttachment> hueAttachment; std::unique_ptr<juce::AudioProcessorValueTreeState::SliderAttachment> hueAttachment;
std::unique_ptr<juce::AudioProcessorValueTreeState::SliderAttachment> modeAttachment;
std::unique_ptr<juce::AudioProcessorValueTreeState::SliderAttachment> ledsAttachment;
std::unique_ptr<juce::AudioProcessorValueTreeState::SliderAttachment> smoothAttachment;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (BeamgridAudioProcessorEditor) JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (BeamgridAudioProcessorEditor)
}; };

View file

@ -41,12 +41,17 @@ BeamgridAudioProcessor::createParameterLayout()
juce::NormalisableRange<float> (0.0f, 1.0f, 0.001f), 0.5f, juce::NormalisableRange<float> (0.0f, 1.0f, 0.001f), 0.5f,
juce::AudioParameterFloatAttributes().withLabel (""))); juce::AudioParameterFloatAttributes().withLabel ("")));
params.push_back (std::make_unique<juce::AudioParameterFloat>(
"smooth", "Smoothing",
juce::NormalisableRange<float> (0.0f, 1.0f, 0.001f), 0.35f,
juce::AudioParameterFloatAttributes().withLabel ("")));
juce::NormalisableRange<float> barsRange (8.0f, 128.0f, juce::NormalisableRange<float> barsRange (8.0f, 128.0f,
[] (float, float, float v) { return v; }, // convertFrom0To1 [] (float start, float end, float v) { return start + v * (end - start); }, // convertFrom0To1
[] (float, float, float v) { return v; }, // convertTo0To1 [] (float start, float end, float v) { return (v - start) / (end - start); }, // convertTo0To1
[] (float, float, float v) // snapToLegalValue [] (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 best = steps[0];
float bestDist = std::abs (v - best); float bestDist = std::abs (v - best);
for (float s : steps) for (float s : steps)
@ -64,6 +69,12 @@ BeamgridAudioProcessor::createParameterLayout()
params.push_back (std::make_unique<juce::AudioParameterFloat>( params.push_back (std::make_unique<juce::AudioParameterFloat>(
"bars", "Bars", barsRange, 64.0f)); "bars", "Bars", barsRange, 64.0f));
params.push_back (std::make_unique<juce::AudioParameterInt>(
"mode", "Mode", 1, 3, 1));
params.push_back (std::make_unique<juce::AudioParameterInt>(
"leds", "LEDs/Bar", 4, 48, 16));
return { params.begin(), params.end() }; return { params.begin(), params.end() };
} }