mirror of
https://codeberg.org/armin/beamgrid.git
synced 2026-09-01 04:10:47 +02:00
Initial BEAMGRID VST3/AU spectrum analyzer
This commit is contained in:
commit
0a3fe6d853
15 changed files with 908 additions and 0 deletions
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
build/
|
||||
*.o
|
||||
*.obj
|
||||
.DS_Store
|
||||
86
CMakeLists.txt
Normal file
86
CMakeLists.txt
Normal 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
154
Source/Analyser.cpp
Normal 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
74
Source/Analyser.h
Normal 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)
|
||||
};
|
||||
3
Source/AnalyserComponent.cpp
Normal file
3
Source/AnalyserComponent.cpp
Normal 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
129
Source/AnalyserComponent.h
Normal 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)
|
||||
};
|
||||
4
Source/BeamgridLookAndFeel.cpp
Normal file
4
Source/BeamgridLookAndFeel.cpp
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
#include "BeamgridLookAndFeel.h"
|
||||
|
||||
const juce::Colour BeamgridLookAndFeel::tealBase { 0xff558899 };
|
||||
const juce::Colour BeamgridLookAndFeel::peakBase { 0xff996655 };
|
||||
69
Source/BeamgridLookAndFeel.h
Normal file
69
Source/BeamgridLookAndFeel.h
Normal 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
5
Source/GitVersion.h.in
Normal 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
13
Source/JuceHeader.h
Normal 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
134
Source/PluginEditor.cpp
Normal 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
44
Source/PluginEditor.h
Normal 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
116
Source/PluginProcessor.cpp
Normal 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
45
Source/PluginProcessor.h
Normal 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)
|
||||
};
|
||||
28
cmake/generate_version.cmake
Normal file
28
cmake/generate_version.cmake
Normal 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)
|
||||
Loading…
Add table
Add a link
Reference in a new issue