Initial Gelyk EQ plugin

This commit is contained in:
Gelyk 2026-08-29 13:17:11 +02:00
commit 13863f4522
9 changed files with 1080 additions and 0 deletions

121
CMakeLists.txt Normal file
View file

@ -0,0 +1,121 @@
cmake_minimum_required(VERSION 3.15)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
project(GelykEQ VERSION 1.0.0 LANGUAGES CXX)
if(NOT DEFINED JUCE_ROOT)
include(FetchContent)
set(FETCHCONTENT_QUIET OFF)
FetchContent_Declare(
JUCE
GIT_REPOSITORY https://github.com/juce-framework/JUCE.git
GIT_TAG 8.0.7
)
FetchContent_MakeAvailable(JUCE)
else()
add_subdirectory(${JUCE_ROOT} JUCE)
endif()
juce_add_plugin(GelykEQ
VENDOR_NAME "Gelyk Audio"
VENDOR_WEBSITE "https://gelyk.example"
VENDOR_EMAIL "dev@gelyk.example"
VENDOR_VERSION_CODE 1
PLUGIN_MANUFACTURER_CODE Gely
PLUGIN_CODE GkEQ
FORMATS AU VST3
PRODUCT_NAME "Gelyk EQ"
IS_SYNTH FALSE
IS_MIDI_EFFECT FALSE
NEEDS_MIDI_INPUT FALSE
NEEDS_MIDI_OUTPUT FALSE
EDITOR_WANTS_KEYBOARD_FOCUS TRUE
COPY_PLUGIN_AFTER_BUILD TRUE
INSTALL_AU TRUE
INSTALL_VST3 TRUE
BUNDLE_ID com.gelyk.eq
JucePlugin_Name "Gelyk EQ"
JucePlugin_Desc "A minimalist cyber/neon multiband equalizer"
)
juce_generate_juce_header(GelykEQ)
# --- Generate Git build info header -------------------------------------
find_package(Git QUIET)
set(GELYK_GIT_BRANCH "unknown")
set(GELYK_GIT_SHA "unknown")
set(GELYK_GIT_DESCRIBE "no-git")
set(GELYK_GIT_DIRTY "")
if(Git_FOUND)
execute_process(
COMMAND ${GIT_EXECUTABLE} rev-parse --abbrev-ref HEAD
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
OUTPUT_VARIABLE GELYK_GIT_BRANCH
OUTPUT_STRIP_TRAILING_WHITESPACE
ERROR_QUIET)
execute_process(
COMMAND ${GIT_EXECUTABLE} describe --always --tags --dirty
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
OUTPUT_VARIABLE GELYK_GIT_DESCRIBE
OUTPUT_STRIP_TRAILING_WHITESPACE
ERROR_QUIET)
execute_process(
COMMAND ${GIT_EXECUTABLE} rev-parse --short HEAD
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
OUTPUT_VARIABLE GELYK_GIT_SHA
OUTPUT_STRIP_TRAILING_WHITESPACE
ERROR_QUIET)
execute_process(
COMMAND ${GIT_EXECUTABLE} status --porcelain
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
OUTPUT_VARIABLE GELYK_GIT_DIRTY
ERROR_QUIET)
if(NOT GELYK_GIT_DIRTY STREQUAL "")
set(GELYK_GIT_DIRTY "dirty")
else()
set(GELYK_GIT_DIRTY "")
endif()
endif()
string(TIMESTAMP GELYK_BUILD_DATE "%Y-%m-%d %H:%M")
configure_file(
${CMAKE_CURRENT_SOURCE_DIR}/Source/GitInfo.h.in
${CMAKE_CURRENT_BINARY_DIR}/Source/GitInfo.h
@ONLY
)
target_sources(GelykEQ
PRIVATE
Source/PluginProcessor.cpp
Source/PluginEditor.cpp
Source/EqualizerDSP.cpp
)
target_include_directories(GelykEQ
PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/Source
)
target_compile_definitions(GelykEQ PRIVATE
JucePlugin_Build_VST=1
JucePlugin_Build_AU=1
JucePlugin_Build_AAX=0
JucePlugin_Build_Standalone=0
)
target_compile_options(GelykEQ PRIVATE
$<$<CXX_COMPILER_ID:AppleClang,Clang>:-Wall -Wextra>
)
option(GELYK_BUILD_STANDALONE "Build a standalone audio app target too" OFF)
if(GELYK_BUILD_STANDALONE)
target_sources(GelykEQ PRIVATE Source/Standalone.cpp)
add_executable(GelykEQStandalone ${CMAKE_CURRENT_BINARY_DIR}/JuceLibraryCode/Standalone.cpp)
target_link_libraries(GelykEQStandalone PRIVATE GelykEQ_impl juce_recommended_config_flags)
endif()
set_target_properties(GelykEQ PROPERTIES
OUTPUT_NAME GelykEQ
)

21
Makefile Normal file
View file

@ -0,0 +1,21 @@
BUILD_DIR := build
all: build
build: cmake-configure
cmake --build $(BUILD_DIR) --config Release -j$(shell sysctl -n hw.ncpu)
cmake-configure:
cmake -S . -B $(BUILD_DIR) -DCMAKE_BUILD_TYPE=Release
install: build
cmake --install $(BUILD_DIR)
# Install directly to the standard macOS user plugin folders (AU + VST3).
install-local: build
cmake --install $(BUILD_DIR) --prefix "$(HOME)"
clean:
rm -rf $(BUILD_DIR)
.PHONY: all build cmake-configure install install-local clean

203
Source/EqualizerDSP.cpp Normal file
View file

@ -0,0 +1,203 @@
#include "EqualizerDSP.h"
namespace gelyk
{
// ---------------------------------------------------------------------------
// FilterBand
// ---------------------------------------------------------------------------
void FilterBand::prepare(double sampleRate, int blockSize)
{
juce::dsp::ProcessSpec spec;
spec.sampleRate = sampleRate;
spec.maximumBlockSize = (juce::uint32) blockSize;
spec.numChannels = 2;
left.prepare(spec);
right.prepare(spec);
Coeffs::Ptr c;
const float db = juce::jlimit(-24.0f, 24.0f, gainDb);
switch (type)
{
case Type::lowShelf:
c = new Coeffs(Coeffs::makeLowShelf(sampleRate, freq, q, juce::Decibels::decibelsToGain(db)));
break;
case Type::highShelf:
c = new Coeffs(Coeffs::makeHighShelf(sampleRate, freq, q, juce::Decibels::decibelsToGain(db)));
break;
case Type::peak:
default:
c = new Coeffs(Coeffs::makePeakFilter(sampleRate, freq, q, juce::Decibels::decibelsToGain(db)));
break;
}
left.coefficients = c;
right.coefficients = c;
}
void FilterBand::reset()
{
left.reset();
right.reset();
}
void FilterBand::process(juce::dsp::AudioBlock<float>& block)
{
if (block.getNumChannels() < 2)
return;
// Smooth the coefficients to avoid zipper noise, then process.
left.snapToZero();
right.snapToZero();
left.process(juce::dsp::ProcessContextReplacing<float>(block));
right.process(juce::dsp::ProcessContextReplacing<float>(block));
}
// ---------------------------------------------------------------------------
// SpectrumAnalyser
// ---------------------------------------------------------------------------
SpectrumAnalyser::SpectrumAnalyser()
{
// The timer drives periodic background analysis.
startTimerHz(30);
}
void SpectrumAnalyser::prepare(double sr, int maxBlockSize)
{
const int size = 4096;
sampleRate = sr;
ring.setSize(2, size);
ring.clear();
ringFill = 0;
ringChannels = 2;
}
void SpectrumAnalyser::push(const float* const* channels, int numChannels, int numSamples)
{
if (!enabled.load() || ring.getNumSamples() == 0)
return;
const int n = (int) ring.getNumSamples();
const int c = juce::jmin(ringChannels, numChannels);
for (int i = 0; i < numSamples; ++i)
{
const int dst = (ringFill + i) % n;
for (int ch = 0; ch < c; ++ch)
ring.setSample(ch, dst, channels[ch][i]);
}
ringFill = (ringFill + numSamples) % n;
}
void SpectrumAnalyser::startBackground()
{
startTimerHz(30);
}
void SpectrumAnalyser::stopBackground()
{
stopTimer();
}
void SpectrumAnalyser::timerCallback()
{
if (!enabled.load())
return;
runAnalysis();
}
void SpectrumAnalyser::runAnalysis()
{
const int n = (int) ring.getNumSamples();
if (n == 0)
return;
juce::AudioBuffer<float> block(1, n);
// Mesa of the two channels and window with a Hann.
block.copyFrom(0, 0, ring, 0, 0, n);
if (ringChannels > 1)
block.addFrom(0, 0, ring, 1, 0, n);
juce::dsp::FFT fft(12); // 4096 point
juce::HeapBlock<std::complex<float>> fftData;
fftData.calloc((size_t) n);
for (int i = 0; i < n; ++i)
{
float v = block.getSample(0, i) * 0.5f;
const float win = 0.5f * (1.0f - std::cos(2.0f * juce::MathConstants<float>::pi * i / (n - 1)));
fftData[i] = std::complex<float>(v * win, 0.0f);
}
fft.perform((juce::dsp::Complex<float>*) fftData.getData(), false);
const int numBins = n / 2;
juce::Array<float> mags;
mags.ensureStorageAllocated(numBins);
for (int i = 1; i < numBins; ++i)
{
float re = fftData[i].real();
float im = fftData[i].imag();
float mag = std::sqrt(re * re + im * im);
float db = 20.0f * std::log10(mag + 1.0e-9f);
mags.add(db);
}
{
juce::ScopedLock sl(spectrumLock);
readySpectrum = mags;
}
}
int SpectrumAnalyser::copySpectrum(juce::Array<float>& dest, juce::Array<float>& freqBins,
int maxBins) const
{
juce::Array<float> snapshot;
{
juce::ScopedLock sl(spectrumLock);
snapshot = readySpectrum;
}
if (snapshot.size() == 0)
{
dest.clear();
freqBins.clear();
return 0;
}
// Log-spaced aggregation from ~20 Hz to 20 kHz.
const float fMin = 20.0f;
const float fMax = 20000.0f;
const double nfft = (double)(snapshot.size() * 2);
const float binFreq = (float)(sampleRate / nfft);
dest.clear();
freqBins.clear();
const int bands = juce::jmin(maxBins, snapshot.size());
for (int b = 0; b < bands; ++b)
{
const float f = fMin * std::pow(fMax / fMin, (float) b / (float) (bands - 1));
freqBins.add(f);
const int startBin = juce::jmax(1, (int) std::floor(f / binFreq));
const int endBin = juce::jmin(snapshot.size() - 1, (int) std::ceil((f * 1.5f) / binFreq));
float total = 0.0f;
int count = 0;
for (int i = startBin; i <= endBin; ++i)
{
total += snapshot[i];
++count;
}
dest.add(count > 0 ? total / count : -96.0f);
}
return dest.size();
}
void SpectrumAnalyser::setEnabled(bool e)
{
enabled.store(e);
}
} // namespace gelyk

70
Source/EqualizerDSP.h Normal file
View file

@ -0,0 +1,70 @@
#pragma once
#include <JuceHeader.h>
namespace gelyk
{
// A single peaking/shelf filter band with smoothed coefficients.
struct FilterBand
{
enum class Type
{
lowShelf,
peak,
highShelf
};
Type type = Type::peak;
float gainDb = 0.0f; // target gain in dB
float freq = 1000.0f; // centre/corner frequency
float q = 1.0f;
void prepare(double sampleRate, int blockSize);
void reset();
void process(juce::dsp::AudioBlock<float>& block);
using Filter = juce::dsp::IIR::Filter<float>;
using Coeffs = juce::dsp::IIR::Coefficients<float>;
Filter left, right;
};
// Simple background "realtime" analyser: it keeps a circular ring buffer
// of the most recent input, computes an FFT magnitude spectrum periodically
// in a background thread, and exposes the result thread-safely for the UI.
class SpectrumAnalyser : public juce::Timer
{
public:
SpectrumAnalyser();
void prepare(double sampleRate, int maxBlockSize);
void push(const float* const* channels, int numChannels, int numSamples);
void startBackground();
void stopBackground();
// Copies the latest spectrum into `dest` (size destSize) with equal
// log-spaced bands, returning the number of bins written.
int copySpectrum(juce::Array<float>& dest, juce::Array<float>& freqBins,
int maxBins) const;
void setEnabled(bool enabled);
private:
void timerCallback() override;
void runAnalysis();
mutable juce::CriticalSection spectrumLock;
juce::Array<float> readySpectrum; // dB magnitudes per bin
juce::AudioBuffer<float> ring;
int ringFill = 0;
int ringChannels = 0;
double sampleRate = 44100.0;
std::atomic<bool> enabled { true };
};
} // namespace gelyk

9
Source/GitInfo.h.in Normal file
View file

@ -0,0 +1,9 @@
#pragma once
// This file is generated by CMake at configure time from GitInfo.h.in
#define GELYK_GIT_BRANCH "@GELYK_GIT_BRANCH@"
#define GELYK_GIT_SHA "@GELYK_GIT_SHA@"
#define GELYK_GIT_VERSION "@GELYK_GIT_DESCRIBE@"
#define GELYK_GIT_DIRTY_STR "@GELYK_GIT_DIRTY@"
#define GELYK_BUILD_DATE_STR "@GELYK_BUILD_DATE@"

348
Source/PluginEditor.cpp Normal file
View file

@ -0,0 +1,348 @@
#include "PluginEditor.h"
namespace gelyk
{
// ---------------------------------------------------------------------------
// NeonLookAndFeel
// ---------------------------------------------------------------------------
NeonLookAndFeel::NeonLookAndFeel()
{
setColour(juce::Slider::textBoxTextColourId, cyan);
setColour(juce::Slider::textBoxOutlineColourId, juce::Colours::transparentBlack);
setColour(juce::Slider::textBoxBackgroundColourId, panel);
setColour(juce::ComboBox::backgroundColourId, panel);
setColour(juce::ComboBox::outlineColourId, line);
setColour(juce::ComboBox::textColourId, cyan);
setColour(juce::ComboBox::arrowColourId, cyan);
}
void NeonLookAndFeel::drawLinearSlider(juce::Graphics& g, int x, int y, int width, int height,
float sliderPos, float, float,
const juce::Slider::SliderStyle style, juce::Slider& slider)
{
const bool vertical = (style == juce::Slider::LinearVertical);
const float cx = x + width * 0.5f;
// Track
juce::Path track;
if (vertical)
{
const float tw = 2.0f;
track.addRoundedRectangle(cx - tw * 0.5f, (float) y, tw, (float) height, 1.0f);
}
else
{
const float th = 2.0f;
track.addRoundedRectangle((float) x, y + height * 0.5f - th * 0.5f, (float) width, th, 1.0f);
}
g.setColour(line);
g.fillPath(track);
// Filled region from centre (0 dB) down to the handle, glowing.
if (vertical)
{
const float midY = y + height * 0.5f;
const float top = juce::jmin(midY, sliderPos);
const float bottom = juce::jmax(midY, sliderPos);
juce::Path fill;
fill.addRoundedRectangle(cx - 3.0f, top, 6.0f, juce::jmax(1.0f, bottom - top), 3.0f);
g.setColour(cyan);
g.fillPath(fill);
// Handle.
g.setColour(slider.isEnabled() && isMouseOverOrDragging(true) ? violet : cyan);
g.fillEllipse(cx - 5.0f, sliderPos - 5.0f, 10.0f, 10.0f);
g.setColour(bg);
g.fillEllipse(cx - 2.0f, sliderPos - 2.0f, 4.0f, 4.0f);
}
else
{
g.setColour(cyan);
g.fillRect(x, y + height * 0.5f - 3.0f, (float) width, 6.0f);
}
}
void NeonLookAndFeel::drawComboBox(juce::Graphics& g, int width, int height, bool,
int, int, int, int, juce::ComboBox& box)
{
auto corner = 4.0f;
auto bounds = box.getLocalBounds().toFloat();
g.setColour(panel);
g.fillRoundedRectangle(bounds, corner);
g.setColour(cyan);
g.drawRoundedRectangle(bounds, corner, 1.0f);
juce::Rectangle<int> arrowZone(width - 20, 0, 20, height);
juce::Path p;
p.startNewSubPath((float) arrowZone.getX() + 4.0f, (float) arrowZone.getCentreY() - 3.0f);
p.lineTo((float) arrowZone.getCentreX(), (float) arrowZone.getCentreY() + 3.0f);
p.lineTo((float) arrowZone.getX() + 12.0f, (float) arrowZone.getCentreY() - 3.0f);
g.setColour(cyan);
g.strokePath(p, juce::PathStrokeType(1.5f));
}
void NeonLookAndFeel::positionComboBoxText(juce::ComboBox& box, juce::Label& label)
{
label.setBounds(2, 1, box.getWidth() - 22, box.getHeight() - 2);
label.setFont(juce::FontOptions(13.0f).setTypefaceName(juce::Font::getDefaultMonospacedFontName()));
}
// ---------------------------------------------------------------------------
// GelykEQAudioProcessorEditor
// ---------------------------------------------------------------------------
GelykEQAudioProcessorEditor::GelykEQAudioProcessorEditor(GelykEQAudioProcessor& p)
: AudioProcessorEditor(&p),
processor(p),
analyserAttachment(processor.getAPVTS().parameterState,
"analyserEnabled",
[this](float v) { processor.getAnalyser().setEnabled(v > 0.5f); })
{
setLookAndFeel(&lookAndFeel);
setOpaque(true);
titleLabel = std::make_unique<juce::Label>("title", "GELYKEQ");
titleLabel->setFont(juce::FontOptions(26.0f).setTypefaceName(juce::Font::getDefaultMonospacedFontName()));
titleLabel->setColour(juce::Label::textColourId, cyan);
titleLabel->setJustificationType(juce::Justification::centredLeft);
addAndMakeVisible(*titleLabel);
scaleBox = std::make_unique<juce::ComboBox>("scale");
scaleBox->addItem("100%", 100);
scaleBox->addItem("125%", 125);
scaleBox->addItem("150%", 150);
scaleBox->addItem("175%", 175);
scaleBox->addItem("200%", 200);
scaleBox->setSelectedId(100, juce::dontSendNotification);
scaleBox->onChange = [this] { scaleUI((float) scaleBox->getSelectedId()); };
addAndMakeVisible(*scaleBox);
// Master gain (vertical fader, far right).
masterLabel = std::make_unique<juce::Label>("mlab", "MASTER");
masterLabel->setFont(juce::FontOptions(11.0f).setTypefaceName(juce::Font::getDefaultMonospacedFontName()));
masterLabel->setColour(juce::Label::textColourId, magenta);
masterLabel->setJustificationType(juce::Justification::centred);
master = std::make_unique<juce::Slider>(juce::Slider::LinearVertical,
juce::Slider::TextBoxBelow);
master->setLookAndFeel(&lookAndFeel);
master->setSliderStyle(juce::Slider::LinearVertical);
master->setTextBoxStyle(juce::Slider::TextBoxBelow, false, 40, 14);
master->setRange(-24.0, 24.0, 0.01);
addAndMakeVisible(*masterLabel);
addAndMakeVisible(*master);
masterAttachment = std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment>(
processor.getAPVTS(), "master", *master);
// Band faders.
const char* const names[numBands] = { "LOW", "LOW-MID", "MID", "HIGH-MID", "HIGH" };
const char* const freqs[numBands] = { "60", "200", "1K", "4K", "10K" };
for (int i = 0; i < numBands; ++i)
{
auto& w = bands[i];
w.freqLabel = std::make_unique<juce::Label>("freq", freqs[i]);
w.freqLabel->setFont(juce::FontOptions(13.0f).setTypefaceName(juce::Font::getDefaultMonospacedFontName()));
w.freqLabel->setColour(juce::Label::textColourId, textDim);
w.freqLabel->setJustificationType(juce::Justification::centred);
w.gainLabel = std::make_unique<juce::Label>("gname", names[i]);
w.gainLabel->setFont(juce::FontOptions(11.0f).setTypefaceName(juce::Font::getDefaultMonospacedFontName()));
w.gainLabel->setColour(juce::Label::textColourId, i % 2 == 0 ? cyan : magenta);
w.gainLabel->setJustificationType(juce::Justification::centred);
w.gain = std::make_unique<juce::Slider>(juce::Slider::LinearVertical, juce::Slider::TextBoxBelow);
w.gain->setLookAndFeel(&lookAndFeel);
w.gain->setSliderStyle(juce::Slider::LinearVertical);
w.gain->setTextBoxStyle(juce::Slider::TextBoxBelow, false, 40, 14);
w.gain->setRange(-24.0, 24.0, 0.01);
w.gain->setDoubleClickReturnValue(true, 0.0);
w.q = std::make_unique<juce::Slider>(juce::Slider::LinearHorizontal, juce::Slider::NoTextBox);
w.q->setLookAndFeel(&lookAndFeel);
w.q->setRange(0.1, 6.0, 0.01);
w.q->setDoubleClickReturnValue(true, 1.0);
addAndMakeVisible(*w.freqLabel);
addAndMakeVisible(*w.gainLabel);
addAndMakeVisible(*w.gain);
addAndMakeVisible(*w.q);
gainAttachments[i] = std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment>(
processor.getAPVTS(), "gain" + juce::String(i), *w.gain);
qAttachments[i] = std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment>(
processor.getAPVTS(), "q" + juce::String(i), *w.q);
}
// Status bar.
statusGit = std::make_unique<juce::Label>("git", processor.getGitInfo());
statusGit->setFont(juce::FontOptions(11.0f).setTypefaceName(juce::Font::getDefaultMonospacedFontName()));
statusGit->setColour(juce::Label::textColourId, textDim);
statusGit->setJustificationType(juce::Justification::centredRight);
statusBar = std::make_unique<juce::Label>("status",
"GELYKEQ v" + juce::String(ProjectInfo::versionString) + " | AU / VST3 ");
statusBar->setFont(juce::FontOptions(11.0f).setTypefaceName(juce::Font::getDefaultMonospacedFontName()));
statusBar->setColour(juce::Label::textColourId, textDim);
statusBar->setJustificationType(juce::Justification::centredLeft);
addAndMakeVisible(*statusBar);
addAndMakeVisible(*statusGit);
spectrum.ensureStorageAllocated(128);
freqBins.ensureStorageAllocated(128);
startTimerHz(30);
rebuildFaders();
scaleUI(100.0f);
}
GelykEQAudioProcessorEditor::~GelykEQAudioProcessorEditor()
{
stopTimer();
setLookAndFeel(nullptr);
}
void GelykEQAudioProcessorEditor::timerCallback()
{
processor.getAnalyser().copySpectrum(spectrum, freqBins, 128);
repaint();
}
void GelykEQAudioProcessorEditor::rebuildFaders()
{
// Nothing needed at construction; sliders are attachments.
}
void GelykEQAudioProcessorEditor::scaleUI(float percent)
{
const float s = percent / 100.0f;
scaling.store(s);
setSize((int) (920 * s), (int) (540 * s));
resized();
}
void GelykEQAudioProcessorEditor::resized()
{
juce::Rectangle<int> area = getLocalBounds();
const float s = scaling.load();
const int headerH = (int) (46 * s);
const int footerH = (int) (26 * s);
const int pad = (int) (12 * s);
// Header.
juce::Rectangle<int> header = area.removeFromTop(headerH);
titleLabel->setBounds(header.removeFromLeft((int) (160 * s)).reduced(pad, 0));
scaleBox->setBounds(header.removeFromRight((int) (90 * s)).reduced((int)(6*s), (int)(9*s)));
// Footer.
juce::Rectangle<int> footer = area.removeFromBottom(footerH);
footer.reduce(pad, 0);
statusBar->setBounds(footer.removeFromLeft(footer.getWidth() / 2));
statusGit->setBounds(footer);
// Main grid.
const int masterW = (int) (64 * s);
juce::Rectangle<int> mainArea = area;
juce::Rectangle<int> masterArea = mainArea.removeFromRight(masterW);
const int x0 = mainArea.getX();
const int y0 = mainArea.getY();
const int totalW = mainArea.getWidth();
const int gap = (int) (10 * s);
const int colW = (totalW - gap * (numBands - 1)) / numBands;
for (int i = 0; i < numBands; ++i)
{
auto& w = bands[i];
juce::Rectangle<int> col(x0 + i * (colW + gap), y0, colW, mainArea.getHeight());
const int topLabelH = (int) (18 * s);
const int freqH = (int) (16 * s);
const int gainH = (int) (14 * s);
juce::Rectangle<int> sub = col.reduced((int)(8*s), 0);
w.gainLabel->setBounds(sub.removeFromTop(topLabelH));
juce::Rectangle<int> analyserArea = sub.removeFromTop(sub.getHeight() - freqH - gainH - (int)(18*s) - (int)(10*s));
w.freqLabel->setBounds(sub.removeFromTop(freqH));
w.gain->setBounds(sub.removeFromTop(gainH + (int)(34*s)));
w.q->setBounds(col.removeFromBottom((int)(18*s)).reduced((int)(8*s), 0));
}
// Master column.
juce::Rectangle<int> msub = masterArea.reduced((int)(14*s), 0);
masterLabel->setBounds(msub.removeFromTop((int) (18 * s)));
master->setBounds(msub);
}
void GelykEQAudioProcessorEditor::paint(juce::Graphics& g)
{
const float s = scaling.load();
g.fillAll(bg);
// Analyser canvas area matches fader columns.
juce::Rectangle<int> area = getLocalBounds();
const int headerH = (int) (46 * s);
const int footerH = (int) (26 * s);
juce::Rectangle<int> mainArea = area;
mainArea.removeFromTop(headerH);
mainArea.removeFromBottom(footerH);
mainArea.removeFromRight((int) (64 * s));
// Neon grid lines behind faders.
g.setColour(line);
for (int i = 0; i < 4; ++i)
{
float f = (i + 1) / 5.0f;
// 0 dB line.
if (i == 2)
continue;
float yy = mainArea.getY() + 18.0f * s + (mainArea.getHeight() - 18.0f * s - 60.0f * s) * f;
g.drawHorizontalLine((int) yy + (int)(18*s), (float) mainArea.getX(), (float) mainArea.getRight());
}
// The 0 dB centre line (glowing).
const float centreLineY = mainArea.getY() + (int)(18*s) +
(mainArea.getHeight() - (int)(18*s) - (int)(60*s)) * 0.5f;
g.setColour(cyan.withAlpha(0.55f));
g.drawHorizontalLine((int) centreLineY, (float) mainArea.getX(), (float) mainArea.getRight());
// Spectrum overlay.
if (spectrum.size() > 2)
{
juce::Path path;
const int x0 = mainArea.getX();
const int width = mainArea.getWidth();
const float topY = mainArea.getY() + 18.0f * s;
const float botY = mainArea.getY() + (int)(8*s) + (mainArea.getHeight() - (int)(18*s) - (int)(60*s)) * 0.92f;
for (int i = 0; i < spectrum.size(); ++i)
{
float db = juce::jlimit(-72.0f, 12.0f, spectrum[i]);
float norm = (db + 72.0f) / 84.0f; // 0..1 up
float xx = (float) x0 + (float) i / (float) (spectrum.size() - 1) * width;
float yy = botY - norm * (botY - topY);
if (i == 0) path.startNewSubPath(xx, yy);
else path.lineTo(xx, yy);
}
juce::Path filled = path;
filled.lineTo((float) mainArea.getRight(), botY);
filled.lineTo((float) mainArea.getX(), botY);
filled.closeSubPath();
g.setGradientFill(juce::ColourGradient(magenta.withAlpha(0.35f), (float) mainArea.getX(), topY,
magenta.withAlpha(0.05f), (float) mainArea.getX(), botY,
false));
g.fillPath(filled);
g.setColour(cyan.withAlpha(0.9f));
g.strokePath(path, juce::PathStrokeType(1.6f * s));
}
}
} // namespace gelyk

83
Source/PluginEditor.h Normal file
View file

@ -0,0 +1,83 @@
#pragma once
#include <JuceHeader.h>
#include "PluginProcessor.h"
namespace gelyk
{
class NeonLookAndFeel : public juce::LookAndFeel_V4
{
public:
NeonLookAndFeel();
void drawLinearSlider(juce::Graphics&, int x, int y, int width, int height,
float sliderPos, float minSliderPos, float maxSliderPos,
const juce::Slider::SliderStyle, juce::Slider&) override;
void drawComboBox(juce::Graphics&, int width, int height, bool isButtonDown,
int buttonX, int buttonY, int buttonW, int buttonH,
juce::ComboBox&) override;
void positionComboBoxText(juce::ComboBox&, juce::Label&) override;
juce::Colour cyan { 0xff00e5ff };
juce::Colour magenta{ 0xffff2fd6 };
juce::Colour violet { 0xff8a2be2 };
juce::Colour bg { 0xff0b0f16 };
juce::Colour panel { 0xff121a24 };
juce::Colour line { 0xff1f2a38 };
juce::Colour textDim{ 0xff7a8aa0 };
};
// ---------------------------------------------------------------------------
class GelykEQAudioProcessorEditor final : public juce::AudioProcessorEditor,
public juce::Timer
{
public:
explicit GelykEQAudioProcessorEditor(GelykEQAudioProcessor&);
~GelykEQAudioProcessorEditor() override;
void paint(juce::Graphics&) override;
void resized() override;
void timerCallback() override;
private:
void rebuildFaders();
void scaleUI(float percent);
GelykEQAudioProcessor& processor;
NeonLookAndFeel lookAndFeel;
static constexpr int numBands = 5;
struct BandWidget
{
std::unique_ptr<juce::Slider> gain;
std::unique_ptr<juce::Slider> q;
std::unique_ptr<juce::Label> freqLabel;
std::unique_ptr<juce::Label> gainLabel;
};
BandWidget bands[numBands];
std::unique_ptr<juce::Label> masterLabel;
std::unique_ptr<juce::Slider> master;
std::unique_ptr<juce::ComboBox> scaleBox;
std::unique_ptr<juce::Label> titleLabel;
std::unique_ptr<juce::Label> statusBar;
std::unique_ptr<juce::Label> statusGit;
juce::AudioProcessorValueTreeState::SliderAttachment gainAttachments[numBands];
juce::AudioProcessorValueTreeState::SliderAttachment qAttachments[numBands];
std::unique_ptr<juce::AudioProcessorValueTreeState::SliderAttachment> masterAttachment;
juce::AudioProcessorValueTreeState::ParameterAttachment analyserAttachment;
juce::Array<float> spectrum;
juce::Array<float> freqBins;
std::atomic<float> scaling { 1.0f }; // for resized()
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(GelykEQAudioProcessorEditor)
};
} // namespace gelyk

166
Source/PluginProcessor.cpp Normal file
View file

@ -0,0 +1,166 @@
#include "PluginProcessor.h"
#include "PluginEditor.h"
#include "GitInfo.h"
namespace gelyk
{
namespace
{
juce::AudioProcessorValueTreeState::ParameterLayout createParameterLayout()
{
juce::AudioProcessorValueTreeState::ParameterLayout layout;
auto addGain = [&](juce::String id, juce::String name, float def)
{
layout.add(std::make_unique<juce::AudioParameterFloat>(
id, name, juce::NormalisableRange<float>(-24.0f, 24.0f, 0.01f), def));
};
auto addQ = [&](juce::String id, juce::String name, float def)
{
layout.add(std::make_unique<juce::AudioParameterFloat>(
id, name, juce::NormalisableRange<float>(0.1f, 6.0f, 0.01f), def));
};
addGain("gain0", "Low Shelf Gain", 0.0f);
addGain("gain1", "Low-Mid Gain", 0.0f);
addGain("gain2", "Mid Gain", 0.0f);
addGain("gain3", "High-Mid Gain", 0.0f);
addGain("gain4", "High Shelf Gain", 0.0f);
addQ("q0", "Low Shelf Q", 0.71f);
addQ("q1", "Low-Mid Q", 1.0f);
addQ("q2", "Mid Q", 1.0f);
addQ("q3", "High-Mid Q", 1.0f);
addQ("q4", "High Shelf Q", 0.71f);
layout.add(std::make_unique<juce::AudioParameterFloat>(
"master", "Master", juce::NormalisableRange<float>(-24.0f, 24.0f, 0.01f), 0.0f));
layout.add(std::make_unique<juce::AudioParameterBool>(
"analyserEnabled", "Analyser", true));
return layout;
}
const char* const bandTypes[5] = { "lowShelf", "peak", "peak", "peak", "highShelf" };
const float bandFreqs[5] = { 60.0f, 200.0f, 1000.0f, 4000.0f, 10000.0f };
} // namespace
GelykEQAudioProcessor::GelykEQAudioProcessor()
: AudioProcessor(BusesProperties()
.withInput("Input", juce::AudioChannelSet::stereo(), true)
.withOutput("Output", juce::AudioChannelSet::stereo(), true)),
apvts(*this, nullptr, "Parameters", createParameterLayout())
{
buildVersion = ProjectInfo::versionString;
gitInfo = juce::String(GELYK_GIT_BRANCH) + " @ " + juce::String(GELYK_GIT_SHA) +
(juce::String(GELYK_GIT_DIRTY_STR).isNotEmpty() ? " (dirty)" : "") +
" | " + juce::String(GELYK_BUILD_DATE_STR);
}
void GelykEQAudioProcessor::prepareToPlay(double sampleRate, int samplesPerBlock)
{
currentSampleRate = sampleRate;
for (int i = 0; i < numBands; ++i)
{
bands[i].type = (FilterBand::Type)
(juce::String(bandTypes[i]) == "lowShelf" ? 0 :
(juce::String(bandTypes[i]) == "highShelf" ? 2 : 1));
bands[i].freq = bandFreqs[i];
bands[i].prepare(sampleRate, samplesPerBlock);
bands[i].reset();
}
analyser.prepare(sampleRate, samplesPerBlock);
analyser.startBackground();
rebuildCoefficients();
}
void GelykEQAudioProcessor::releaseResources()
{
analyser.stopBackground();
}
void GelykEQAudioProcessor::rebuildCoefficients()
{
for (int i = 0; i < numBands; ++i)
{
float gain = apvts.getRawParameterValue("gain" + juce::String(i))->load();
float q = apvts.getRawParameterValue("q" + juce::String(i))->load();
bands[i].gainDb = gain;
bands[i].q = q;
bands[i].prepare(currentSampleRate, 512);
}
}
void GelykEQAudioProcessor::processBlock(juce::AudioBuffer<float>& buffer, juce::MidiBuffer&)
{
juce::ScopedNoDenormals noDenormals;
const int numChannels = buffer.getNumChannels();
const int numSamples = buffer.getNumSamples();
if (numChannels < 1)
return;
// Feed the analyser with the input.
if (apvts.getRawParameterValue("analyserEnabled")->load() > 0.5f)
{
const float* chans[2] = { buffer.getReadPointer(0),
numChannels > 1 ? buffer.getReadPointer(1) : buffer.getReadPointer(0) };
analyser.push(chans, numChannels > 1 ? 2 : 1, numSamples);
}
// Refresh band coefficients (cheap) from smoothed params each block.
for (int i = 0; i < numBands; ++i)
{
float gain = apvts.getRawParameterValue("gain" + juce::String(i))->load();
float q = apvts.getRawParameterValue("q" + juce::String(i))->load();
if (std::abs(gain - bands[i].gainDb) > 0.001f || std::abs(q - bands[i].q) > 0.001f)
{
bands[i].gainDb = gain;
bands[i].q = q;
bands[i].prepare(currentSampleRate, numSamples);
}
}
juce::dsp::AudioBlock<float> block(buffer);
for (int i = 0; i < numBands; ++i)
bands[i].process(block);
float master = apvts.getRawParameterValue("master")->load();
if (std::abs(master) > 0.001f)
buffer.applyGain(juce::Decibels::decibelsToGain(master));
}
void GelykEQAudioProcessor::getStateInformation(juce::MemoryBlock& destData)
{
auto state = apvts.copyState();
std::unique_ptr<juce::XmlElement> xml(state.createXml());
copyXmlToBinary(*xml, destData);
}
void GelykEQAudioProcessor::setStateInformation(const void* data, int sizeInBytes)
{
std::unique_ptr<juce::XmlElement> xml(getXmlFromBinary(data, sizeInBytes));
if (xml != nullptr && xml->hasTagName(apvts.state.getType()))
apvts.replaceState(juce::ValueTree::fromXml(*xml));
}
juce::AudioProcessorEditor* GelykEQAudioProcessor::createEditor()
{
return new GelykEQAudioProcessorEditor(*this);
}
} // namespace gelyk
juce::AudioProcessor* JUCE_CALLTYPE createPluginFilter()
{
return new gelyk::GelykEQAudioProcessor();
}

59
Source/PluginProcessor.h Normal file
View file

@ -0,0 +1,59 @@
#pragma once
#include <JuceHeader.h>
#include "EqualizerDSP.h"
namespace gelyk
{
class GelykEQAudioProcessor final : public juce::AudioProcessor
{
public:
GelykEQAudioProcessor();
~GelykEQAudioProcessor() override = default;
void prepareToPlay(double sampleRate, int samplesPerBlock) override;
void releaseResources() 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 "Gelyk EQ"; }
bool acceptsMidi() const override { return false; }
bool producesMidi() 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*, int) override;
juce::AudioProcessorValueTreeState& getAPVTS() { return apvts; }
// Exposed for the editor / analyser.
SpectrumAnalyser& getAnalyser() { return analyser; }
// Returns human-readable Git build info.
const juce::String& getGitInfo() const { return gitInfo; }
private:
juce::AudioProcessorValueTreeState apvts;
static constexpr int numBands = 5;
FilterBand bands[numBands];
double currentSampleRate = 44100.0;
SpectrumAnalyser analyser;
juce::String buildVersion;
juce::String gitInfo;
void rebuildCoefficients();
};
} // namespace gelyk