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
121
CMakeLists.txt
Normal file
121
CMakeLists.txt
Normal 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
21
Makefile
Normal 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
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