mirror of
https://codeberg.org/armin/beamgrid.git
synced 2026-09-01 04:10:47 +02:00
add LICENSE, update README, update source
This commit is contained in:
parent
6ab142e450
commit
e0177ed26f
9 changed files with 395 additions and 53 deletions
14
.gitignore
vendored
14
.gitignore
vendored
|
|
@ -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
23
LICENSE
Normal 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.
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -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.
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- **Three display modes** (top-right `MODE` knob):
|
- **Three display modes** (top-right `MODE` knob):
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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));
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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() };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue