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

View file

@ -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<int> (fftBins), bandBinEdges[b - 1] + 1);
bandBinEdges[n] = fftBins;
}
void Analyser::setNumBands (int n) noexcept

View file

@ -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<float> (hue - 0.5));
displayMode = mode;
ledCount = leds;
smoothAmount = static_cast<float> (smooth);
analyser.update (1.0 / 60.0);
repaint();
}
@ -57,12 +66,11 @@ public:
const float bandWidth = (plot.getWidth() - gap * (n + 1)) / static_cast<float> (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<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 };
for (double db : dbTicks)
{
const float frac = analyser.dbToFraction (db);
const float y = baseY - frac * plot.getHeight();
g.setColour (gridColour);
g.drawHorizontalLine (static_cast<int> (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<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)
{
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)
return juce::String (f / 1000.0, 1, false) + "k";
return juce::String (static_cast<int> (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<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;
juce::AudioProcessorValueTreeState& params;
BeamgridLookAndFeel& lf;
int displayMode = ModeBars;
int ledCount = 16;
float smoothAmount = 0.35f;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AnalyserComponent)
};

View file

@ -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<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)
{
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<juce::AudioProcessorValueTreeState::SliderAttachment>(
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()
{
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);
}

View file

@ -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<juce::AudioProcessorValueTreeState::SliderAttachment> graceAttachment;
std::unique_ptr<juce::AudioProcessorValueTreeState::SliderAttachment> falloffAttachment;
std::unique_ptr<juce::AudioProcessorValueTreeState::SliderAttachment> barfalloffAttachment;
std::unique_ptr<juce::AudioProcessorValueTreeState::SliderAttachment> barsAttachment;
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)
};

View file

@ -41,12 +41,17 @@ BeamgridAudioProcessor::createParameterLayout()
juce::NormalisableRange<float> (0.0f, 1.0f, 0.001f), 0.5f,
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,
[] (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<juce::AudioParameterFloat>(
"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() };
}