Initial BEAMGRID VST3/AU spectrum analyzer

This commit is contained in:
opencode 2026-08-13 23:15:38 +02:00
commit 0a3fe6d853
15 changed files with 908 additions and 0 deletions

4
.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
build/
*.o
*.obj
.DS_Store

86
CMakeLists.txt Normal file
View file

@ -0,0 +1,86 @@
cmake_minimum_required(VERSION 3.22)
project(BEAMGRID VERSION 0.1.0)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
include(FetchContent)
FetchContent_Declare(
juce
GIT_REPOSITORY https://github.com/juce-framework/JUCE.git
GIT_TAG 8.0.6
)
FetchContent_MakeAvailable(juce)
juce_add_plugin(BEAMGRID
NAME "BEAMGRID"
COMPANY_NAME "Beamgrid"
PLUGIN_MANUFACTURER_CODE Bmgr
PLUGIN_CODE Bmgr
FORMATS VST3 AU
PRODUCT_NAME "BEAMGRID"
IS_SYNTH FALSE
NEEDS_MIDI_INPUT FALSE
NEEDS_MIDI_OUTPUT FALSE
IS_MIDI_EFFECT FALSE
EDITOR_WANTS_KEYBOARD_FOCUS FALSE
COPY_PLUGIN_AFTER_BUILD TRUE
)
target_sources(BEAMGRID PRIVATE
Source/JuceHeader.h
Source/Analyser.h
Source/Analyser.cpp
Source/BeamgridLookAndFeel.h
Source/BeamgridLookAndFeel.cpp
Source/AnalyserComponent.h
Source/AnalyserComponent.cpp
Source/PluginProcessor.h
Source/PluginProcessor.cpp
Source/PluginEditor.h
Source/PluginEditor.cpp
)
target_compile_definitions(BEAMGRID PUBLIC
JUCE_VST3_CAN_REPLACE_VST2=0
JUCE_USE_CAMERA_PERMISSIONS=0
)
target_link_libraries(BEAMGRID PRIVATE
juce::juce_audio_basics
juce::juce_audio_devices
juce::juce_audio_processors
juce::juce_audio_utils
juce::juce_core
juce::juce_data_structures
juce::juce_dsp
juce::juce_events
juce::juce_graphics
juce::juce_gui_basics
juce::juce_gui_extra
)
if(APPLE)
set_target_properties(BEAMGRID PROPERTIES
CMAKE_OSX_DEPLOYMENT_TARGET "10.15"
)
endif()
# Generate a version header with the current Git commit, dirty state and a
# fresh build timestamp. Runs on every build via a custom target.
set(GENERATED_DIR ${CMAKE_BINARY_DIR}/generated)
file(MAKE_DIRECTORY ${GENERATED_DIR})
set(GIT_VERSION_HEADER ${GENERATED_DIR}/GitVersion.h)
add_custom_target(generate_version ALL
COMMAND ${CMAKE_COMMAND}
-D SRC_DIR=${CMAKE_SOURCE_DIR}
-D TEMPLATE=${CMAKE_SOURCE_DIR}/Source/GitVersion.h.in
-D OUTPUT=${GIT_VERSION_HEADER}
-P ${CMAKE_SOURCE_DIR}/cmake/generate_version.cmake
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
COMMENT "Generating BEAMGRID version header"
)
target_include_directories(BEAMGRID PRIVATE ${GENERATED_DIR})
add_dependencies(BEAMGRID generate_version)

154
Source/Analyser.cpp Normal file
View file

@ -0,0 +1,154 @@
#include "Analyser.h"
#include <cmath>
Analyser::Analyser() = default;
void Analyser::prepare (double newSampleRate, int /*blockSize*/)
{
sampleRate = newSampleRate > 0.0 ? newSampleRate : 44100.0;
minFreq = 20.0;
maxFreq = sampleRate * 0.5;
computeBandEdges();
}
void Analyser::computeBandEdges()
{
const int n = numBands;
// Log-spaced band edges mapped into FFT bins.
for (int b = 0; b <= n; ++b)
{
const double freq = minFreq * std::pow (maxFreq / minFreq,
static_cast<double> (b) / n);
int bin = static_cast<int> (std::round (freq * fftSize / sampleRate));
bin = juce::jlimit (1, static_cast<int> (fftBins), bin);
bandBinEdges[b] = bin;
}
// Guarantee strictly increasing edges.
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
{
numBands = juce::jlimit (1, maxBands, n);
computeBandEdges();
}
void Analyser::push (const float* channelData, int numSamples)
{
if (channelData == nullptr)
return;
for (int i = 0; i < numSamples; ++i)
pushSample (channelData[i]);
}
void Analyser::pushSample (float sample)
{
ring[writePos] = sample;
writePos = (writePos + 1) % fftSize;
++totalSamples;
// Only start transforming once the window has filled at least once,
// then recompute overlapped every `hopSize` samples.
if (totalSamples >= fftSize && ++samplesSinceFFT >= hopSize)
{
samplesSinceFFT = 0;
computeFFT();
}
}
void Analyser::computeFFT()
{
// Gather the most recent fftSize samples in order (oldest -> newest).
const int start = writePos;
for (int i = 0; i < fftSize; ++i)
fftData[i] = ring[(start + i) % fftSize];
window.multiplyWithWindowingTable (fftData.data(), static_cast<size_t> (fftSize));
juce::FloatVectorOperations::clear (fftData.data() + fftSize, fftSize);
fft.performFrequencyOnlyForwardTransform (fftData.data());
for (int b = 0; b < numBands; ++b)
{
const int lo = bandBinEdges[b];
const int hi = bandBinEdges[b + 1];
double sum = 0.0;
int count = 0;
for (int k = lo; k < hi && k < fftBins; ++k)
{
sum += fftData[k];
++count;
}
const double avg = count > 0 ? sum / count : 0.0;
// Normalize the bin magnitude by the FFT size so "0 dB" corresponds
// to ~full scale, otherwise the raw magnitudes sit ~66 dB too hot.
const double normalized = avg / static_cast<double> (fftSize);
const double db = 20.0 * std::log10 (normalized + 1e-9);
double norm = (db - minDb) / (maxDb - minDb);
norm = juce::jlimit (0.0, 1.0, norm);
targets[b] = static_cast<float> (norm);
}
}
void Analyser::update (double dt)
{
// Live bar level eases toward the latest FFT target with a fast attack
// and a knob-controlled release (bar falloff).
const double attackCoef = 1.0 - std::exp (-dt / 0.005);
const double releaseCoef = 1.0 - std::exp (-dt / juce::jmax (0.001, barReleaseTau));
for (int b = 0; b < numBands; ++b)
{
const float target = targets[b];
if (target >= levels[b])
levels[b] += (target - levels[b]) * static_cast<float> (attackCoef);
else
levels[b] += (target - levels[b]) * static_cast<float> (releaseCoef);
if (levels[b] >= peaks[b])
{
peaks[b] = levels[b];
peakTimers[b] = 0.0f;
}
else
{
peakTimers[b] += static_cast<float> (dt);
if (peakTimers[b] >= grace)
{
peaks[b] -= static_cast<float> (falloffRate * dt);
if (peaks[b] < levels[b])
peaks[b] = levels[b];
if (peaks[b] < 0.0f)
peaks[b] = 0.0f;
}
}
}
}
float Analyser::freqToFraction (double freq) const noexcept
{
const double f = juce::jlimit (minFreq, maxFreq, freq);
return static_cast<float> (std::log (f / minFreq) / std::log (maxFreq / minFreq));
}
float Analyser::dbToFraction (double db) const noexcept
{
return static_cast<float> (juce::jlimit (0.0, 1.0, (db - minDb) / (maxDb - minDb)));
}

74
Source/Analyser.h Normal file
View file

@ -0,0 +1,74 @@
#pragma once
#include "JuceHeader.h"
// Beamgrid analyser: log-spaced spectrum bands with peak-hold markers.
// Each band tracks a current level and a peak that holds for a definable
// "grace" time before falling off at a definable rate.
class Analyser
{
public:
static constexpr int maxBands = 256;
enum { fftOrder = 11, fftSize = 1 << fftOrder, fftBins = fftSize / 2 };
Analyser();
void prepare (double sampleRate, int blockSize);
void push (const float* channelData, int numSamples);
void update (double dt);
int getNumBands() const noexcept { return numBands; }
float getLevel (int band) const noexcept { return band < numBands ? juce::jmin (1.0f, levels[band]) : 0.0f; }
float getPeak (int band) const noexcept { return band < numBands ? juce::jmin (1.0f, peaks[band]) : 0.0f; }
// Helpers for drawing axis legends.
double getMinFreq() const noexcept { return minFreq; }
double getMaxFreq() const noexcept { return maxFreq; }
float freqToFraction (double freq) const noexcept;
float dbToFraction (double db) const noexcept;
void setGraceSeconds (double seconds) noexcept { grace = seconds; }
void setFalloffRate (double rate) noexcept { falloffRate = rate; }
void setBarReleaseTau (double seconds) noexcept { barReleaseTau = seconds; }
void setNumBands (int n) noexcept;
double getGraceSeconds() const noexcept { return grace; }
double getFalloffRate() const noexcept { return falloffRate; }
private:
void pushSample (float sample);
void computeFFT();
void computeBandEdges();
juce::dsp::FFT fft { fftOrder };
juce::dsp::WindowingFunction<float> window { fftSize, juce::dsp::WindowingFunction<float>::hann, true };
// Sliding time-domain window so the FFT is recomputed every `hopSize`
// samples (overlapped) instead of only once per full window. This keeps
// the visualization temporally smooth.
static constexpr int hopSize = 256;
std::array<float, fftSize> ring {};
std::array<float, 2 * fftSize> fftData {};
int writePos = 0;
int samplesSinceFFT = 0;
int totalSamples = 0;
int numBands = 64;
std::array<float, maxBands> levels {};
std::array<float, maxBands> targets {};
std::array<float, maxBands> peaks {};
std::array<float, maxBands> peakTimers {};
// First/last FFT bin index covered by each band (log spaced).
std::array<int, maxBands + 1> bandBinEdges {};
double minFreq = 20.0;
double maxFreq = 22050.0;
double sampleRate = 44100.0;
double grace = 0.8; // seconds the peak is held before falling
double falloffRate = 0.8; // normalized units per second while falling
double barReleaseTau = 0.2; // seconds for the live bar to fall (release)
double minDb = -48.0; // dBFS-equivalent floor (bands below this -> 0)
double maxDb = -6.0; // dBFS-equivalent ceiling (bands at/above -> 1)
};

View file

@ -0,0 +1,3 @@
#include "AnalyserComponent.h"
// (Implementation is entirely inline in the header for this small component.)

129
Source/AnalyserComponent.h Normal file
View file

@ -0,0 +1,129 @@
#pragma once
#include "JuceHeader.h"
#include "Analyser.h"
#include "BeamgridLookAndFeel.h"
// Black spectrum visualizer: teal analyzer bands and peak-colored markers.
class AnalyserComponent : public juce::Component,
public juce::Timer
{
public:
AnalyserComponent (Analyser& analyserToUse,
juce::AudioProcessorValueTreeState& paramsToUse,
BeamgridLookAndFeel& lookAndFeelToUse)
: analyser (analyserToUse), params (paramsToUse), lf (lookAndFeelToUse)
{
startTimerHz (60);
}
~AnalyserComponent() override
{
stopTimer();
}
void timerCallback() override
{
const double graceMs = *params.getRawParameterValue ("grace");
const double falloff = *params.getRawParameterValue ("falloff");
const double barfall = *params.getRawParameterValue ("barfalloff");
const int bars = juce::roundToInt (params.getRawParameterValue ("bars")->load());
const double hue = *params.getRawParameterValue ("hue");
analyser.setGraceSeconds (graceMs / 1000.0);
analyser.setFalloffRate (0.1 + falloff * 5.0);
// Bar falloff: higher knob -> faster bar descent (smaller release tau).
analyser.setBarReleaseTau (2.0 - barfall * 1.98);
analyser.setNumBands (bars);
// HUE knob: 12 o'clock (0.5) = no rotation; left/right rotate +/- 180 deg.
lf.setHueTurns (static_cast<float> (hue - 0.5));
analyser.update (1.0 / 60.0);
repaint();
}
void paint (juce::Graphics& g) override
{
const auto area = getLocalBounds().toFloat().reduced (8.0f);
const int n = analyser.getNumBands();
// Reserve margins for the axis legends.
juce::Rectangle<float> plot = area;
plot.setLeft (area.getX() + 40.0f);
plot.setBottom (area.getBottom() - 20.0f);
const float gap = 2.0f;
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 labelColour = lf.transform (juce::Colours::grey);
// Frequency gridlines + labels (log spaced).
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())
continue;
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);
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).
for (int i = 0; i < n; ++i)
{
const float level = analyser.getLevel (i);
const float peak = analyser.getPeak (i);
const float x = plot.getX() + gap + i * (bandWidth + gap);
const float h = level * plot.getHeight();
const float y = baseY - h;
g.setColour (lf.getTeal());
g.fillRect (x, y, bandWidth, h);
const float peakY = baseY - peak * plot.getHeight();
g.setColour (lf.getPeak());
g.fillRect (x, peakY - 2.0f, bandWidth, 3.0f);
}
}
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:
Analyser& analyser;
juce::AudioProcessorValueTreeState& params;
BeamgridLookAndFeel& lf;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AnalyserComponent)
};

View file

@ -0,0 +1,4 @@
#include "BeamgridLookAndFeel.h"
const juce::Colour BeamgridLookAndFeel::tealBase { 0xff558899 };
const juce::Colour BeamgridLookAndFeel::peakBase { 0xff996655 };

View file

@ -0,0 +1,69 @@
#pragma once
#include "JuceHeader.h"
// Visual styling for BEAMGRID: black UI with teal accents. A single HUE knob
// rotates the hue of every saturated colour on the UI via a shared LookAndFeel.
class BeamgridLookAndFeel : public juce::LookAndFeel_V4
{
public:
BeamgridLookAndFeel()
{
setColour (juce::Slider::rotarySliderFillColourId, tealBase);
setColour (juce::Slider::rotarySliderOutlineColourId, juce::Colours::black.brighter (0.18f));
setColour (juce::Slider::textBoxTextColourId, juce::Colours::lightgrey);
setColour (juce::Slider::textBoxOutlineColourId, juce::Colours::transparentBlack);
setColour (juce::Label::textColourId, juce::Colours::lightgrey);
}
// Base palette (used at 12 o'clock / zero hue rotation).
static const juce::Colour tealBase;
static const juce::Colour peakBase;
// Hue rotation in turns (-0.5 .. +0.5); 0 = original colours.
float hueTurns = 0.0f;
void setHueTurns (float turns) noexcept { hueTurns = turns; }
juce::Colour transform (juce::Colour c) const noexcept
{
return c.withRotatedHue (hueTurns);
}
juce::Colour getTeal() const noexcept { return transform (tealBase); }
juce::Colour getPeak() const noexcept { return transform (peakBase); }
void drawRotarySlider (juce::Graphics& g, int x, int y, int w, int h,
float pos, float startAngle, float endAngle,
juce::Slider& slider) override
{
const auto bounds = juce::Rectangle<int> (x, y, w, h).toFloat().reduced (6.0f);
const auto centre = bounds.getCentre();
const auto radius = juce::jmin (bounds.getWidth(), bounds.getHeight()) * 0.5f;
const float thickness = 5.0f;
// Background track arc.
juce::Path track;
track.addArc (centre.x - radius, centre.y - radius, radius * 2.0f, radius * 2.0f,
startAngle, endAngle, thickness);
g.setColour (slider.findColour (juce::Slider::rotarySliderOutlineColourId));
g.strokePath (track, juce::PathStrokeType (thickness));
// Filled value arc (teal, hue-rotated).
const float angle = startAngle + pos * (endAngle - startAngle);
juce::Path value;
value.addArc (centre.x - radius, centre.y - radius, radius * 2.0f, radius * 2.0f,
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));
}
};

5
Source/GitVersion.h.in Normal file
View file

@ -0,0 +1,5 @@
#pragma once
#define BEAMGRID_GIT_COMMIT "@GIT_COMMIT@"
#define BEAMGRID_GIT_DIRTY @GIT_DIRTY@
#define BEAMGRID_BUILD_DATE "@BUILD_DATE@"

13
Source/JuceHeader.h Normal file
View file

@ -0,0 +1,13 @@
#pragma once
#include <juce_core/juce_core.h>
#include <juce_data_structures/juce_data_structures.h>
#include <juce_events/juce_events.h>
#include <juce_audio_basics/juce_audio_basics.h>
#include <juce_audio_devices/juce_audio_devices.h>
#include <juce_audio_processors/juce_audio_processors.h>
#include <juce_audio_utils/juce_audio_utils.h>
#include <juce_dsp/juce_dsp.h>
#include <juce_graphics/juce_graphics.h>
#include <juce_gui_basics/juce_gui_basics.h>
#include <juce_gui_extra/juce_gui_extra.h>

134
Source/PluginEditor.cpp Normal file
View file

@ -0,0 +1,134 @@
#include "PluginEditor.h"
#include "GitVersion.h"
BeamgridAudioProcessorEditor::BeamgridAudioProcessorEditor (BeamgridAudioProcessor& p)
: AudioProcessorEditor (&p),
processorRef (p),
analyserComponent (p.getAnalyser(), p.getParametersState(), lookAndFeel)
{
setLookAndFeel (&lookAndFeel);
setSize (820, 460);
// Make the editor freely resizable (with a corner resizer). The plugin
// reports IPlugView::canResize() == true to the host, which is what FL
// Studio (and other hosts) use to enable their native maximize button.
setResizable (true, true);
setResizeLimits (420, 260, 4000, 4000);
analyserComponent.setLookAndFeel (&lookAndFeel);
addAndMakeVisible (analyserComponent);
auto setupKnob = [&] (juce::Slider& s, const juce::String& name)
{
s.setSliderStyle (juce::Slider::RotaryHorizontalVerticalDrag);
s.setTextBoxStyle (juce::Slider::TextBoxBelow, false, 80, 18);
s.setColour (juce::Slider::textBoxTextColourId, juce::Colours::lightgrey);
s.setTextValueSuffix (" " + name);
s.setLookAndFeel (&lookAndFeel);
addAndMakeVisible (s);
};
setupKnob (graceSlider, "ms");
setupKnob (falloffSlider, "");
setupKnob (barfalloffSlider, "");
setupKnob (barsSlider, "");
setupKnob (hueSlider, "");
barsSlider.setNumDecimalPlacesToDisplay (0);
graceLabel.setText ("PEAK GRACE", juce::dontSendNotification);
graceLabel.setJustificationType (juce::Justification::centred);
graceLabel.setColour (juce::Label::textColourId, juce::Colours::lightgrey);
addAndMakeVisible (graceLabel);
falloffLabel.setText ("PEAK FALLOFF", juce::dontSendNotification);
falloffLabel.setJustificationType (juce::Justification::centred);
falloffLabel.setColour (juce::Label::textColourId, juce::Colours::lightgrey);
addAndMakeVisible (falloffLabel);
barfalloffLabel.setText ("BAR FALLOFF", juce::dontSendNotification);
barfalloffLabel.setJustificationType (juce::Justification::centred);
barfalloffLabel.setColour (juce::Label::textColourId, juce::Colours::lightgrey);
addAndMakeVisible (barfalloffLabel);
barsLabel.setText ("BARS", juce::dontSendNotification);
barsLabel.setJustificationType (juce::Justification::centred);
barsLabel.setColour (juce::Label::textColourId, juce::Colours::lightgrey);
addAndMakeVisible (barsLabel);
hueLabel.setText ("HUE", juce::dontSendNotification);
hueLabel.setJustificationType (juce::Justification::centred);
hueLabel.setColour (juce::Label::textColourId, juce::Colours::lightgrey);
addAndMakeVisible (hueLabel);
titleLabel.setText ("BEAMGRID", juce::dontSendNotification);
titleLabel.setJustificationType (juce::Justification::centredRight);
titleLabel.setFont (juce::FontOptions (20.0f, juce::Font::bold));
titleLabel.setColour (juce::Label::textColourId, lookAndFeel.getTeal());
addAndMakeVisible (titleLabel);
statusLabel.setFont (juce::FontOptions (11.0f));
statusLabel.setColour (juce::Label::textColourId, juce::Colours::grey);
statusLabel.setJustificationType (juce::Justification::centredLeft);
juce::String status;
status << "git " << juce::String (BEAMGRID_GIT_COMMIT).substring (0, 7);
status << " [" << (BEAMGRID_GIT_DIRTY ? "dirty" : "clean") << "]";
status << " build " << BEAMGRID_BUILD_DATE;
statusLabel.setText (status, juce::dontSendNotification);
addAndMakeVisible (statusLabel);
graceAttachment = std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment>(
processorRef.getParametersState(), "grace", graceSlider);
falloffAttachment = std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment>(
processorRef.getParametersState(), "falloff", falloffSlider);
barfalloffAttachment = std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment>(
processorRef.getParametersState(), "barfalloff", barfalloffSlider);
barsAttachment = std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment>(
processorRef.getParametersState(), "bars", barsSlider);
hueAttachment = std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment>(
processorRef.getParametersState(), "hue", hueSlider);
}
BeamgridAudioProcessorEditor::~BeamgridAudioProcessorEditor()
{
setLookAndFeel (nullptr);
}
void BeamgridAudioProcessorEditor::paint (juce::Graphics& g)
{
g.fillAll (juce::Colours::black);
// Keep the title colour in sync with the HUE knob.
titleLabel.setColour (juce::Label::textColourId, lookAndFeel.getTeal());
}
void BeamgridAudioProcessorEditor::resized()
{
const int panelH = 114;
const int knob = 64;
const int y = getHeight() - panelH + 14;
analyserComponent.setBounds (0, 0, getWidth(), getHeight() - panelH);
const int margin = 32;
const int spacing = knob + 24;
barsSlider.setBounds (margin, y, knob, knob);
barsLabel.setBounds (margin, y + knob + 2, knob, 18);
graceSlider.setBounds (margin + spacing, y, knob, knob);
graceLabel.setBounds (margin + spacing, y + knob + 2, knob, 18);
barfalloffSlider.setBounds (margin + spacing * 2, y, knob, knob);
barfalloffLabel.setBounds (margin + spacing * 2, y + knob + 2, knob, 18);
falloffSlider.setBounds (margin + spacing * 3, y, knob, knob);
falloffLabel.setBounds (margin + spacing * 3, y + knob + 2, knob, 18);
hueSlider.setBounds (margin + spacing * 4, y, knob, knob);
hueLabel.setBounds (margin + spacing * 4, y + knob + 2, knob, 18);
titleLabel.setBounds (getWidth() - 262, y - 6, 210, knob + 24);
statusLabel.setBounds (8, getHeight() - 16, getWidth() - 16, 14);
}

44
Source/PluginEditor.h Normal file
View file

@ -0,0 +1,44 @@
#pragma once
#include "JuceHeader.h"
#include "PluginProcessor.h"
#include "AnalyserComponent.h"
#include "BeamgridLookAndFeel.h"
class BeamgridAudioProcessorEditor : public juce::AudioProcessorEditor
{
public:
explicit BeamgridAudioProcessorEditor (BeamgridAudioProcessor&);
~BeamgridAudioProcessorEditor() override;
void paint (juce::Graphics&) override;
void resized() override;
private:
BeamgridAudioProcessor& processorRef;
BeamgridLookAndFeel lookAndFeel;
AnalyserComponent analyserComponent;
juce::Slider graceSlider;
juce::Slider falloffSlider;
juce::Slider barfalloffSlider;
juce::Slider barsSlider;
juce::Slider hueSlider;
juce::Label graceLabel;
juce::Label falloffLabel;
juce::Label barfalloffLabel;
juce::Label barsLabel;
juce::Label hueLabel;
juce::Label titleLabel;
juce::Label statusLabel;
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;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (BeamgridAudioProcessorEditor)
};

116
Source/PluginProcessor.cpp Normal file
View file

@ -0,0 +1,116 @@
#include "PluginProcessor.h"
#include "PluginEditor.h"
BeamgridAudioProcessor::BeamgridAudioProcessor()
: AudioProcessor (BusesProperties()
.withInput ("Input", juce::AudioChannelSet::stereo(), true)
.withOutput ("Output", juce::AudioChannelSet::stereo(), true)),
params (*this, nullptr, "Parameters", createParameterLayout())
{
}
BeamgridAudioProcessor::~BeamgridAudioProcessor() = default;
juce::AudioProcessor* JUCE_CALLTYPE createPluginFilter()
{
return new BeamgridAudioProcessor();
}
juce::AudioProcessorValueTreeState::ParameterLayout
BeamgridAudioProcessor::createParameterLayout()
{
std::vector<std::unique_ptr<juce::RangedAudioParameter>> params;
params.push_back (std::make_unique<juce::AudioParameterFloat>(
"grace", "Peak Grace (ms)",
juce::NormalisableRange<float> (0.0f, 2000.0f, 1.0f), 800.0f,
juce::AudioParameterFloatAttributes().withLabel ("ms")));
params.push_back (std::make_unique<juce::AudioParameterFloat>(
"falloff", "Peak Falloff",
juce::NormalisableRange<float> (0.0f, 1.0f, 0.001f), 0.4f,
juce::AudioParameterFloatAttributes().withLabel ("")));
params.push_back (std::make_unique<juce::AudioParameterFloat>(
"barfalloff", "Bar Falloff",
juce::NormalisableRange<float> (0.0f, 1.0f, 0.001f), 0.5f,
juce::AudioParameterFloatAttributes().withLabel ("")));
params.push_back (std::make_unique<juce::AudioParameterFloat>(
"hue", "Hue",
juce::NormalisableRange<float> (0.0f, 1.0f, 0.001f), 0.5f,
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
{
const float steps[] = { 8.0f, 16.0f, 32.0f, 64.0f, 96.0f, 128.0f };
float best = steps[0];
float bestDist = std::abs (v - best);
for (float s : steps)
{
const float dist = std::abs (v - s);
if (dist < bestDist)
{
bestDist = dist;
best = s;
}
}
return best;
});
params.push_back (std::make_unique<juce::AudioParameterFloat>(
"bars", "Bars", barsRange, 64.0f));
return { params.begin(), params.end() };
}
void BeamgridAudioProcessor::prepareToPlay (double sampleRate, int samplesPerBlock)
{
analyser.prepare (sampleRate, samplesPerBlock);
}
void BeamgridAudioProcessor::releaseResources() {}
bool BeamgridAudioProcessor::isBusesLayoutSupported (const BusesLayout& layouts) const
{
if (layouts.getMainInputChannels() == 0 ||
layouts.getMainOutputChannels() == 0)
return false;
if (layouts.getMainInputChannels() != layouts.getMainOutputChannels())
return false;
return true;
}
void BeamgridAudioProcessor::processBlock (juce::AudioBuffer<float>& buffer,
juce::MidiBuffer& /*midi*/)
{
juce::ScopedNoDenormals noDenormals;
// Analyse the first channel; pass the audio through unchanged.
if (buffer.getNumChannels() > 0)
analyser.push (buffer.getReadPointer (0), buffer.getNumSamples());
}
juce::AudioProcessorEditor* BeamgridAudioProcessor::createEditor()
{
return new BeamgridAudioProcessorEditor (*this);
}
void BeamgridAudioProcessor::getStateInformation (juce::MemoryBlock& dest)
{
auto state = params.copyState();
std::unique_ptr<juce::XmlElement> xml (state.createXml());
copyXmlToBinary (*xml, dest);
}
void BeamgridAudioProcessor::setStateInformation (const void* data, int sizeInBytes)
{
std::unique_ptr<juce::XmlElement> xml (getXmlFromBinary (data, sizeInBytes));
if (xml && xml->hasTagName (params.state.getType()))
params.replaceState (juce::ValueTree::fromXml (*xml));
}

45
Source/PluginProcessor.h Normal file
View file

@ -0,0 +1,45 @@
#pragma once
#include "JuceHeader.h"
#include "Analyser.h"
class BeamgridAudioProcessor : public juce::AudioProcessor
{
public:
BeamgridAudioProcessor();
~BeamgridAudioProcessor() override;
void prepareToPlay (double sampleRate, int samplesPerBlock) override;
void releaseResources() override;
bool isBusesLayoutSupported (const BusesLayout& layouts) const override;
void processBlock (juce::AudioBuffer<float>&, juce::MidiBuffer&) override;
juce::AudioProcessorEditor* createEditor() override;
bool hasEditor() const override { return true; }
const juce::String getName() const override { return JucePlugin_Name; }
bool acceptsMidi() const override { return false; }
bool producesMidi() const override { return false; }
bool isMidiEffect() const override { return false; }
double getTailLengthSeconds() const override { return 0.0; }
int getNumPrograms() override { return 1; }
int getCurrentProgram() override { return 0; }
void setCurrentProgram (int) override {}
const juce::String getProgramName (int) override { return {}; }
void changeProgramName (int, const juce::String&) override {}
void getStateInformation (juce::MemoryBlock&) override;
void setStateInformation (const void* data, int sizeInBytes) override;
Analyser& getAnalyser() { return analyser; }
juce::AudioProcessorValueTreeState& getParametersState() { return params; }
private:
juce::AudioProcessorValueTreeState::ParameterLayout createParameterLayout();
juce::AudioProcessorValueTreeState params;
Analyser analyser;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (BeamgridAudioProcessor)
};

View file

@ -0,0 +1,28 @@
# Generates Source/GitVersion.h from Source/GitVersion.h.in using the current
# Git state and a fresh build timestamp. Invoked via `cmake -P` at build time.
execute_process(COMMAND git rev-parse HEAD
WORKING_DIRECTORY ${SRC_DIR}
OUTPUT_VARIABLE GIT_COMMIT
OUTPUT_STRIP_TRAILING_WHITESPACE
ERROR_QUIET)
if(NOT GIT_COMMIT)
set(GIT_COMMIT "unknown")
endif()
execute_process(COMMAND git status --porcelain
WORKING_DIRECTORY ${SRC_DIR}
OUTPUT_VARIABLE GIT_STATUS
OUTPUT_STRIP_TRAILING_WHITESPACE
ERROR_QUIET)
if(GIT_STATUS STREQUAL "")
set(GIT_DIRTY 0)
else()
set(GIT_DIRTY 1)
endif()
string(TIMESTAMP BUILD_DATE "%Y-%m-%d__%H-%M-%S")
configure_file(${TEMPLATE} ${OUTPUT} @ONLY)