mirror of
https://codeberg.org/armin/gelyk.git
synced 2026-10-11 17:01:53 +02:00
Initial Gelyk EQ plugin
This commit is contained in:
commit
13863f4522
9 changed files with 1080 additions and 0 deletions
203
Source/EqualizerDSP.cpp
Normal file
203
Source/EqualizerDSP.cpp
Normal 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
70
Source/EqualizerDSP.h
Normal 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
9
Source/GitInfo.h.in
Normal 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
348
Source/PluginEditor.cpp
Normal 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
83
Source/PluginEditor.h
Normal 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
166
Source/PluginProcessor.cpp
Normal 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
59
Source/PluginProcessor.h
Normal 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
|
||||
Loading…
Add table
Add a link
Reference in a new issue