mirror of
https://codeberg.org/armin/gelyk.git
synced 2026-10-11 17:01:53 +02:00
init
This commit is contained in:
parent
b2b64fb26e
commit
517219e035
6 changed files with 733 additions and 110 deletions
58
.gitignore
vendored
58
.gitignore
vendored
|
|
@ -1,2 +1,58 @@
|
||||||
build/
|
# Build output (all dependencies live exclusively here)
|
||||||
|
/build/
|
||||||
|
|
||||||
|
# macOS
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
.DS_Store?
|
||||||
|
.AppleDouble
|
||||||
|
.LSOverride
|
||||||
|
._*
|
||||||
|
.AppleDB
|
||||||
|
|
||||||
|
# Vim swap files
|
||||||
|
*.sw[a-p]
|
||||||
|
*.swo
|
||||||
|
*.swn
|
||||||
|
*~
|
||||||
|
.*.sw[a-p]
|
||||||
|
|
||||||
|
# VS Code temp files
|
||||||
|
.vscode/
|
||||||
|
*.code-workspace
|
||||||
|
*.sublime-project
|
||||||
|
*.sublime-workspace
|
||||||
|
*.vscode*
|
||||||
|
|
||||||
|
# Editor / IDE backups & settings
|
||||||
|
.idea/
|
||||||
|
*.iml
|
||||||
|
*.orig
|
||||||
|
*.rej
|
||||||
|
|
||||||
|
# Compiled / object files
|
||||||
|
*.o
|
||||||
|
*.obj
|
||||||
|
*.a
|
||||||
|
*.so
|
||||||
|
*.dylib
|
||||||
|
*.dll
|
||||||
|
*.exe
|
||||||
|
*.lib
|
||||||
|
*.out
|
||||||
|
*.class
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# OS generated files
|
||||||
|
Thumbs.db
|
||||||
|
Desktop.ini
|
||||||
|
ehthumbs.db
|
||||||
|
|
||||||
|
# CMake internal clutter (kept out of the tree)
|
||||||
|
cmake-build-*/
|
||||||
|
CMakeCache.txt
|
||||||
|
CMakeFiles/
|
||||||
|
|
|
||||||
|
|
@ -55,6 +55,8 @@ void computeEQResponse(const FilterBand::Type* types,
|
||||||
|
|
||||||
void FilterBand::prepare(double sampleRate, int blockSize)
|
void FilterBand::prepare(double sampleRate, int blockSize)
|
||||||
{
|
{
|
||||||
|
this->sampleRate = sampleRate;
|
||||||
|
|
||||||
juce::dsp::ProcessSpec spec;
|
juce::dsp::ProcessSpec spec;
|
||||||
spec.sampleRate = sampleRate;
|
spec.sampleRate = sampleRate;
|
||||||
spec.maximumBlockSize = (juce::uint32) blockSize;
|
spec.maximumBlockSize = (juce::uint32) blockSize;
|
||||||
|
|
@ -63,19 +65,42 @@ void FilterBand::prepare(double sampleRate, int blockSize)
|
||||||
left.prepare(spec);
|
left.prepare(spec);
|
||||||
right.prepare(spec);
|
right.prepare(spec);
|
||||||
|
|
||||||
|
// Start from the current (target) parameters with no ramp.
|
||||||
|
targetGain = gainDb;
|
||||||
|
targetFreq = freq;
|
||||||
|
targetQ = q;
|
||||||
|
curGain = gainDb;
|
||||||
|
curFreq = freq;
|
||||||
|
curQ = q;
|
||||||
|
|
||||||
|
updateCoeffs();
|
||||||
|
}
|
||||||
|
|
||||||
|
void FilterBand::setTargets(float gain, float freq, float q)
|
||||||
|
{
|
||||||
|
gainDb = gain;
|
||||||
|
this->freq = freq;
|
||||||
|
this->q = q;
|
||||||
|
targetGain = gain;
|
||||||
|
targetFreq = freq;
|
||||||
|
targetQ = q;
|
||||||
|
}
|
||||||
|
|
||||||
|
void FilterBand::updateCoeffs()
|
||||||
|
{
|
||||||
|
const float db = juce::jlimit(-24.0f, 24.0f, curGain);
|
||||||
Coeffs::Ptr c;
|
Coeffs::Ptr c;
|
||||||
const float db = juce::jlimit(-24.0f, 24.0f, gainDb);
|
|
||||||
switch (type)
|
switch (type)
|
||||||
{
|
{
|
||||||
case Type::lowShelf:
|
case Type::lowShelf:
|
||||||
c = Coeffs::makeLowShelf(sampleRate, freq, q, juce::Decibels::decibelsToGain(db));
|
c = Coeffs::makeLowShelf(sampleRate, curFreq, curQ, juce::Decibels::decibelsToGain(db));
|
||||||
break;
|
break;
|
||||||
case Type::highShelf:
|
case Type::highShelf:
|
||||||
c = Coeffs::makeHighShelf(sampleRate, freq, q, juce::Decibels::decibelsToGain(db));
|
c = Coeffs::makeHighShelf(sampleRate, curFreq, curQ, juce::Decibels::decibelsToGain(db));
|
||||||
break;
|
break;
|
||||||
case Type::peak:
|
case Type::peak:
|
||||||
default:
|
default:
|
||||||
c = Coeffs::makePeakFilter(sampleRate, freq, q, juce::Decibels::decibelsToGain(db));
|
c = Coeffs::makePeakFilter(sampleRate, curFreq, curQ, juce::Decibels::decibelsToGain(db));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -91,14 +116,52 @@ void FilterBand::reset()
|
||||||
|
|
||||||
void FilterBand::process(juce::dsp::AudioBlock<float>& block)
|
void FilterBand::process(juce::dsp::AudioBlock<float>& block)
|
||||||
{
|
{
|
||||||
if (block.getNumChannels() < 2)
|
const int numSamples = (int) block.getNumSamples();
|
||||||
|
const auto numChannels = block.getNumChannels();
|
||||||
|
|
||||||
|
// One-pole (exponential) smoothing evaluated at block rate, using the real
|
||||||
|
// number of samples in this block so the ramp is independent of host buffer
|
||||||
|
// size. ~50 ms time constant: starts responding immediately and settles
|
||||||
|
// within ~200 ms, giving a gentle glide without the multi-second lag the old
|
||||||
|
// per-sample SmoothedValue ramp produced when only advanced once per block.
|
||||||
|
if (numSamples > 0 && numChannels > 0)
|
||||||
|
{
|
||||||
|
const float alpha = 1.0f - std::exp(-(float) numSamples / (sampleRate * 0.05f));
|
||||||
|
|
||||||
|
const float g = curGain + (targetGain - curGain) * alpha;
|
||||||
|
const float f = curFreq + (targetFreq - curFreq) * alpha;
|
||||||
|
const float qv = curQ + (targetQ - curQ) * alpha;
|
||||||
|
|
||||||
|
if (std::abs(g - curGain) > 0.001f ||
|
||||||
|
std::abs(f - curFreq) > 0.01f ||
|
||||||
|
std::abs(qv - curQ) > 0.001f)
|
||||||
|
{
|
||||||
|
curGain = g;
|
||||||
|
curFreq = f;
|
||||||
|
curQ = qv;
|
||||||
|
updateCoeffs();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (numChannels < 1)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// Smooth the coefficients to avoid zipper noise, then process.
|
// JUCE's IIR::Filter only processes mono (one channel pointer), so each
|
||||||
|
// instance must be run on a single channel's own block. Previously the whole
|
||||||
|
// (stereo) block was passed in, which made both filters operate on channel 0
|
||||||
|
// only: the left went through twice and the right not at all. Splitting the
|
||||||
|
// channels fixes the dead right channel and the doubled (rumble) low end.
|
||||||
left.snapToZero();
|
left.snapToZero();
|
||||||
right.snapToZero();
|
right.snapToZero();
|
||||||
left.process(juce::dsp::ProcessContextReplacing<float>(block));
|
|
||||||
right.process(juce::dsp::ProcessContextReplacing<float>(block));
|
auto leftBlock = block.getSingleChannelBlock(0);
|
||||||
|
left.process(juce::dsp::ProcessContextReplacing<float>(leftBlock));
|
||||||
|
|
||||||
|
if (numChannels > 1)
|
||||||
|
{
|
||||||
|
auto rightBlock = block.getSingleChannelBlock(1);
|
||||||
|
right.process(juce::dsp::ProcessContextReplacing<float>(rightBlock));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
@ -182,13 +245,17 @@ void SpectrumAnalyser::runAnalysis()
|
||||||
fft.performFrequencyOnlyForwardTransform(fftBuffer.data());
|
fft.performFrequencyOnlyForwardTransform(fftBuffer.data());
|
||||||
|
|
||||||
// Magnitude values (non-negative frequencies) are in the first size/2+1 entries.
|
// Magnitude values (non-negative frequencies) are in the first size/2+1 entries.
|
||||||
|
// Normalise so a full-scale sine reads as ~0 dB: JUCE's frequency-only FFT gives
|
||||||
|
// a magnitude of ~N/2 for a unit sine at its bin, and the Hann window halves that
|
||||||
|
// to N/4. Dividing by that reference keeps the -72..+12 dB display range meaningful.
|
||||||
|
const float norm = 0.25f * (float) fftSize;
|
||||||
const int numBins = juce::jmin((int) fftBuffer.size(), fftSize / 2 + 1);
|
const int numBins = juce::jmin((int) fftBuffer.size(), fftSize / 2 + 1);
|
||||||
juce::Array<float> mags;
|
juce::Array<float> mags;
|
||||||
mags.ensureStorageAllocated(numBins);
|
mags.ensureStorageAllocated(numBins);
|
||||||
for (int i = 0; i < numBins; ++i)
|
for (int i = 0; i < numBins; ++i)
|
||||||
{
|
{
|
||||||
float mag = fftBuffer[(size_t) i];
|
float mag = fftBuffer[(size_t) i];
|
||||||
float db = 20.0f * std::log10(mag + 1.0e-9f);
|
float db = 20.0f * std::log10(mag / norm + 1.0e-9f);
|
||||||
mags.add(db);
|
mags.add(db);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,12 +22,22 @@ struct FilterBand
|
||||||
|
|
||||||
void prepare(double sampleRate, int blockSize);
|
void prepare(double sampleRate, int blockSize);
|
||||||
void reset();
|
void reset();
|
||||||
|
// Sets the desired (target) values; the coefficients are then smoothed in
|
||||||
|
// `process` to avoid zipper noise when a slider is dragged.
|
||||||
|
void setTargets(float gain, float freq, float q);
|
||||||
void process(juce::dsp::AudioBlock<float>& block);
|
void process(juce::dsp::AudioBlock<float>& block);
|
||||||
|
|
||||||
using Filter = juce::dsp::IIR::Filter<float>;
|
using Filter = juce::dsp::IIR::Filter<float>;
|
||||||
using Coeffs = juce::dsp::IIR::Coefficients<float>;
|
using Coeffs = juce::dsp::IIR::Coefficients<float>;
|
||||||
|
|
||||||
Filter left, right;
|
Filter left, right;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void updateCoeffs();
|
||||||
|
|
||||||
|
double sampleRate = 44100.0;
|
||||||
|
float targetGain = 0.0f, targetFreq = 1000.0f, targetQ = 1.0f;
|
||||||
|
float curGain = 0.0f, curFreq = 1000.0f, curQ = 1.0f;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Computes the total summed magnitude response (in dB) of a series filter chain
|
// Computes the total summed magnitude response (in dB) of a series filter chain
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,11 @@
|
||||||
namespace gelyk
|
namespace gelyk
|
||||||
{
|
{
|
||||||
|
|
||||||
|
static constexpr float fontScale = 1.4f;
|
||||||
|
|
||||||
static juce::Font monoFont(float height)
|
static juce::Font monoFont(float height)
|
||||||
{
|
{
|
||||||
return juce::Font(juce::FontOptions(juce::Font::getDefaultMonospacedFontName(), height,
|
return juce::Font(juce::FontOptions(juce::Font::getDefaultMonospacedFontName(), height * fontScale,
|
||||||
juce::Font::plain));
|
juce::Font::plain));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -15,13 +17,18 @@ static juce::Font monoFont(float height)
|
||||||
|
|
||||||
NeonLookAndFeel::NeonLookAndFeel()
|
NeonLookAndFeel::NeonLookAndFeel()
|
||||||
{
|
{
|
||||||
setColour(juce::Slider::textBoxTextColourId, cyan);
|
refreshPalette();
|
||||||
|
}
|
||||||
|
|
||||||
|
void NeonLookAndFeel::refreshPalette()
|
||||||
|
{
|
||||||
|
setColour(juce::Slider::textBoxTextColourId, cyan());
|
||||||
setColour(juce::Slider::textBoxOutlineColourId, juce::Colours::transparentBlack);
|
setColour(juce::Slider::textBoxOutlineColourId, juce::Colours::transparentBlack);
|
||||||
setColour(juce::Slider::textBoxBackgroundColourId, panel);
|
setColour(juce::Slider::textBoxBackgroundColourId, panel());
|
||||||
setColour(juce::ComboBox::backgroundColourId, panel);
|
setColour(juce::ComboBox::backgroundColourId, panel());
|
||||||
setColour(juce::ComboBox::outlineColourId, line);
|
setColour(juce::ComboBox::outlineColourId, line());
|
||||||
setColour(juce::ComboBox::textColourId, cyan);
|
setColour(juce::ComboBox::textColourId, cyan());
|
||||||
setColour(juce::ComboBox::arrowColourId, cyan);
|
setColour(juce::ComboBox::arrowColourId, cyan());
|
||||||
}
|
}
|
||||||
|
|
||||||
void NeonLookAndFeel::drawLinearSlider(juce::Graphics& g, int x, int y, int width, int height,
|
void NeonLookAndFeel::drawLinearSlider(juce::Graphics& g, int x, int y, int width, int height,
|
||||||
|
|
@ -37,7 +44,7 @@ void NeonLookAndFeel::drawLinearSlider(juce::Graphics& g, int x, int y, int widt
|
||||||
const float tw = 2.0f;
|
const float tw = 2.0f;
|
||||||
juce::Path track;
|
juce::Path track;
|
||||||
track.addRoundedRectangle(cx - tw * 0.5f, (float) y, tw, (float) height, 1.0f);
|
track.addRoundedRectangle(cx - tw * 0.5f, (float) y, tw, (float) height, 1.0f);
|
||||||
g.setColour(line);
|
g.setColour(line());
|
||||||
g.fillPath(track);
|
g.fillPath(track);
|
||||||
|
|
||||||
// Filled region from centre (0 dB) down to the handle, glowing.
|
// Filled region from centre (0 dB) down to the handle, glowing.
|
||||||
|
|
@ -46,13 +53,13 @@ void NeonLookAndFeel::drawLinearSlider(juce::Graphics& g, int x, int y, int widt
|
||||||
const float bottom = juce::jmax(midY, sliderPos);
|
const float bottom = juce::jmax(midY, sliderPos);
|
||||||
juce::Path fill;
|
juce::Path fill;
|
||||||
fill.addRoundedRectangle(cx - 3.0f, top, 6.0f, juce::jmax(1.0f, bottom - top), 3.0f);
|
fill.addRoundedRectangle(cx - 3.0f, top, 6.0f, juce::jmax(1.0f, bottom - top), 3.0f);
|
||||||
g.setColour(cyan);
|
g.setColour(cyan());
|
||||||
g.fillPath(fill);
|
g.fillPath(fill);
|
||||||
|
|
||||||
// Handle.
|
// Handle.
|
||||||
g.setColour(slider.isMouseOverOrDragging(true) ? violet : cyan);
|
g.setColour(slider.isMouseOverOrDragging(true) ? violet() : cyan());
|
||||||
g.fillEllipse(cx - 5.0f, sliderPos - 5.0f, 10.0f, 10.0f);
|
g.fillEllipse(cx - 5.0f, sliderPos - 5.0f, 10.0f, 10.0f);
|
||||||
g.setColour(bg);
|
g.setColour(bg());
|
||||||
g.fillEllipse(cx - 2.0f, sliderPos - 2.0f, 4.0f, 4.0f);
|
g.fillEllipse(cx - 2.0f, sliderPos - 2.0f, 4.0f, 4.0f);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|
@ -60,9 +67,9 @@ void NeonLookAndFeel::drawLinearSlider(juce::Graphics& g, int x, int y, int widt
|
||||||
const float th = 2.0f;
|
const float th = 2.0f;
|
||||||
juce::Path track;
|
juce::Path track;
|
||||||
track.addRoundedRectangle((float) x, y + height * 0.5f - th * 0.5f, (float) width, th, 1.0f);
|
track.addRoundedRectangle((float) x, y + height * 0.5f - th * 0.5f, (float) width, th, 1.0f);
|
||||||
g.setColour(line);
|
g.setColour(line());
|
||||||
g.fillPath(track);
|
g.fillPath(track);
|
||||||
g.setColour(cyan);
|
g.setColour(cyan());
|
||||||
g.drawHorizontalLine((int) (y + height * 0.5f), sliderPos - 5.0f, sliderPos + 5.0f);
|
g.drawHorizontalLine((int) (y + height * 0.5f), sliderPos - 5.0f, sliderPos + 5.0f);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -78,17 +85,18 @@ void NeonLookAndFeel::drawRotarySlider(juce::Graphics& g, int x, int y, int widt
|
||||||
const float arcRadius = radius - lineW * 0.5f;
|
const float arcRadius = radius - lineW * 0.5f;
|
||||||
|
|
||||||
// Background disc.
|
// Background disc.
|
||||||
g.setColour(panel);
|
g.setColour(panel());
|
||||||
g.fillEllipse(cx - radius, cy - radius, radius * 2.0f, radius * 2.0f);
|
g.fillEllipse(cx - radius, cy - radius, radius * 2.0f, radius * 2.0f);
|
||||||
g.setColour(line);
|
g.setColour(line());
|
||||||
g.drawEllipse(cx - radius, cy - radius, radius * 2.0f, radius * 2.0f, 1.0f);
|
g.drawEllipse(cx - radius, cy - radius, radius * 2.0f, radius * 2.0f, 1.0f);
|
||||||
|
|
||||||
// Filled arc (progress).
|
// Filled arc (progress).
|
||||||
juce::Path filledArc;
|
juce::Path filledArc;
|
||||||
filledArc.addCentredArc(cx, cy, arcRadius, arcRadius, 0.0f,
|
filledArc.addCentredArc(cx, cy, arcRadius, arcRadius, 0.0f,
|
||||||
startAngle, toAngle, true);
|
startAngle + juce::MathConstants<float>::halfPi,
|
||||||
|
toAngle + juce::MathConstants<float>::halfPi, true);
|
||||||
juce::PathStrokeType stroke(lineW, juce::PathStrokeType::curved, juce::PathStrokeType::rounded);
|
juce::PathStrokeType stroke(lineW, juce::PathStrokeType::curved, juce::PathStrokeType::rounded);
|
||||||
g.setColour(slider.isMouseOverOrDragging(true) ? magenta : cyan);
|
g.setColour(slider.isMouseOverOrDragging(true) ? magenta() : cyan());
|
||||||
g.strokePath(filledArc, stroke);
|
g.strokePath(filledArc, stroke);
|
||||||
|
|
||||||
// Pointer line.
|
// Pointer line.
|
||||||
|
|
@ -97,13 +105,13 @@ void NeonLookAndFeel::drawRotarySlider(juce::Graphics& g, int x, int y, int widt
|
||||||
pointer.startNewSubPath(cx, cy);
|
pointer.startNewSubPath(cx, cy);
|
||||||
pointer.lineTo(cx + pointerLength * std::cos(toAngle),
|
pointer.lineTo(cx + pointerLength * std::cos(toAngle),
|
||||||
cy + pointerLength * std::sin(toAngle));
|
cy + pointerLength * std::sin(toAngle));
|
||||||
g.setColour(slider.isMouseOverOrDragging(true) ? violet : bg);
|
g.setColour(slider.isMouseOverOrDragging(true) ? violet() : cyan());
|
||||||
g.strokePath(pointer, juce::PathStrokeType(juce::jmax(1.5f, radius * 0.07f),
|
g.strokePath(pointer, juce::PathStrokeType(juce::jmax(1.5f, radius * 0.07f),
|
||||||
juce::PathStrokeType::curved,
|
juce::PathStrokeType::curved,
|
||||||
juce::PathStrokeType::rounded));
|
juce::PathStrokeType::rounded));
|
||||||
|
|
||||||
// Centre hub.
|
// Centre hub.
|
||||||
g.setColour(slider.isMouseOverOrDragging(true) ? cyan : line);
|
g.setColour(slider.isMouseOverOrDragging(true) ? cyan() : line());
|
||||||
g.fillEllipse(cx - 2.0f, cy - 2.0f, 4.0f, 4.0f);
|
g.fillEllipse(cx - 2.0f, cy - 2.0f, 4.0f, 4.0f);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -112,9 +120,9 @@ void NeonLookAndFeel::drawComboBox(juce::Graphics& g, int width, int height, boo
|
||||||
{
|
{
|
||||||
auto corner = 4.0f;
|
auto corner = 4.0f;
|
||||||
auto bounds = box.getLocalBounds().toFloat();
|
auto bounds = box.getLocalBounds().toFloat();
|
||||||
g.setColour(panel);
|
g.setColour(panel());
|
||||||
g.fillRoundedRectangle(bounds, corner);
|
g.fillRoundedRectangle(bounds, corner);
|
||||||
g.setColour(cyan);
|
g.setColour(cyan());
|
||||||
g.drawRoundedRectangle(bounds, corner, 1.0f);
|
g.drawRoundedRectangle(bounds, corner, 1.0f);
|
||||||
|
|
||||||
juce::Rectangle<int> arrowZone(width - 20, 0, 20, height);
|
juce::Rectangle<int> arrowZone(width - 20, 0, 20, height);
|
||||||
|
|
@ -122,14 +130,136 @@ void NeonLookAndFeel::drawComboBox(juce::Graphics& g, int width, int height, boo
|
||||||
p.startNewSubPath((float) arrowZone.getX() + 4.0f, (float) arrowZone.getCentreY() - 3.0f);
|
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.getCentreX(), (float) arrowZone.getCentreY() + 3.0f);
|
||||||
p.lineTo((float) arrowZone.getX() + 12.0f, (float) arrowZone.getCentreY() - 3.0f);
|
p.lineTo((float) arrowZone.getX() + 12.0f, (float) arrowZone.getCentreY() - 3.0f);
|
||||||
g.setColour(cyan);
|
g.setColour(cyan());
|
||||||
g.strokePath(p, juce::PathStrokeType(1.5f));
|
g.strokePath(p, juce::PathStrokeType(1.5f));
|
||||||
}
|
}
|
||||||
|
|
||||||
void NeonLookAndFeel::positionComboBoxText(juce::ComboBox& box, juce::Label& label)
|
void NeonLookAndFeel::positionComboBoxText(juce::ComboBox& box, juce::Label& label)
|
||||||
{
|
{
|
||||||
label.setBounds(2, 1, box.getWidth() - 22, box.getHeight() - 2);
|
label.setBounds(2, 1, box.getWidth() - 22, box.getHeight() - 2);
|
||||||
label.setFont(monoFont(13.0f));
|
label.setFont(monoFont(13.0f * uiScale));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// BandFader
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
void BandFader::paint(juce::Graphics& g)
|
||||||
|
{
|
||||||
|
const float cx = getWidth() * 0.5f;
|
||||||
|
const float y = 0.0f;
|
||||||
|
const float height = (float) getHeight();
|
||||||
|
|
||||||
|
const float value = getGain();
|
||||||
|
const auto start = (float) gainP->getNormalisableRange().start;
|
||||||
|
const auto end = (float) gainP->getNormalisableRange().end;
|
||||||
|
const float sliderPos = y + (end - value) / (end - start) * height;
|
||||||
|
|
||||||
|
// Track.
|
||||||
|
const float tw = 2.0f;
|
||||||
|
g.setColour(lf.line());
|
||||||
|
g.fillRect(cx - tw * 0.5f, y, tw, height);
|
||||||
|
|
||||||
|
// Filled region from centre (0 dB) down to the handle.
|
||||||
|
const float midY = y + height * 0.5f;
|
||||||
|
const float top = juce::jmin(midY, sliderPos);
|
||||||
|
const float bottom = juce::jmax(midY, sliderPos);
|
||||||
|
g.setColour(lf.cyan());
|
||||||
|
g.fillRect(cx - 3.0f, top, 6.0f, juce::jmax(1.0f, bottom - top));
|
||||||
|
|
||||||
|
// Handle.
|
||||||
|
g.setColour(hovered || dragging ? lf.violet() : lf.cyan());
|
||||||
|
g.fillEllipse(cx - 5.0f, sliderPos - 5.0f, 10.0f, 10.0f);
|
||||||
|
g.setColour(lf.bg());
|
||||||
|
g.fillEllipse(cx - 2.0f, sliderPos - 2.0f, 4.0f, 4.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
void BandFader::mouseDown(const juce::MouseEvent& e)
|
||||||
|
{
|
||||||
|
if (gainP == nullptr || freqP == nullptr)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (e.getNumberOfClicks() > 1)
|
||||||
|
{
|
||||||
|
*gainP = 0.0f;
|
||||||
|
repaint();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dragging = true;
|
||||||
|
startGain = (float) gainP->get();
|
||||||
|
startFreq = (float) freqP->get();
|
||||||
|
startX = e.getScreenX();
|
||||||
|
startY = e.getScreenY();
|
||||||
|
repaint();
|
||||||
|
}
|
||||||
|
|
||||||
|
void BandFader::mouseDrag(const juce::MouseEvent& e)
|
||||||
|
{
|
||||||
|
if (!dragging || gainP == nullptr || freqP == nullptr)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const float h = (float) juce::jmax(1, getHeight());
|
||||||
|
const float dy = (float) (startY - e.getScreenY());
|
||||||
|
const float span = (float) gainP->getNormalisableRange().end - (float) gainP->getNormalisableRange().start;
|
||||||
|
const float newGain = juce::jlimit((float) gainP->getNormalisableRange().start,
|
||||||
|
(float) gainP->getNormalisableRange().end,
|
||||||
|
startGain + dy / h * span);
|
||||||
|
if (gainP->get() != newGain)
|
||||||
|
*gainP = newGain;
|
||||||
|
|
||||||
|
const float w = dragWidth > 0.0f ? dragWidth : (float) juce::jmax(1, getWidth());
|
||||||
|
const float dx = (float) (e.getScreenX() - startX);
|
||||||
|
// Track 1:1 with the cursor over the full visualizer width: the fader's screen
|
||||||
|
// position moves by exactly the same number of pixels as the mouse.
|
||||||
|
const float startXpos = bandFreqToX(startFreq, dragX0, w);
|
||||||
|
float newFreq = bandXToFreq(startXpos + dx, dragX0, w);
|
||||||
|
newFreq = juce::jlimit(20.0f, 20000.0f, newFreq);
|
||||||
|
|
||||||
|
// Keep the band between its neighbours so they stay ordered left-to-right.
|
||||||
|
if (onGetFreqBounds != nullptr)
|
||||||
|
{
|
||||||
|
const auto [lo, hi] = onGetFreqBounds();
|
||||||
|
newFreq = juce::jlimit(lo, hi, newFreq);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (std::abs(newFreq - (float) freqP->get()) > 0.5f)
|
||||||
|
{
|
||||||
|
*freqP = newFreq;
|
||||||
|
if (onFreqChange != nullptr)
|
||||||
|
onFreqChange();
|
||||||
|
}
|
||||||
|
|
||||||
|
repaint();
|
||||||
|
}
|
||||||
|
|
||||||
|
void BandFader::mouseUp(const juce::MouseEvent&)
|
||||||
|
{
|
||||||
|
dragging = false;
|
||||||
|
repaint();
|
||||||
|
}
|
||||||
|
|
||||||
|
void BandFader::mouseEnter(const juce::MouseEvent&)
|
||||||
|
{
|
||||||
|
hovered = true;
|
||||||
|
repaint();
|
||||||
|
}
|
||||||
|
|
||||||
|
void BandFader::mouseExit(const juce::MouseEvent&)
|
||||||
|
{
|
||||||
|
hovered = false;
|
||||||
|
repaint();
|
||||||
|
}
|
||||||
|
|
||||||
|
void BandFader::mouseWheelMove(const juce::MouseEvent&, const juce::MouseWheelDetails& wheel)
|
||||||
|
{
|
||||||
|
if (qP == nullptr)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const float delta = (float) (wheel.deltaY * 0.3);
|
||||||
|
const float lo = (float) qP->getNormalisableRange().start;
|
||||||
|
const float hi = (float) qP->getNormalisableRange().end;
|
||||||
|
*qP = juce::jlimit(lo, hi, (float) qP->get() + delta);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
@ -143,9 +273,9 @@ GelykEQAudioProcessorEditor::GelykEQAudioProcessorEditor(GelykEQAudioProcessor&
|
||||||
setLookAndFeel(&lookAndFeel);
|
setLookAndFeel(&lookAndFeel);
|
||||||
setOpaque(true);
|
setOpaque(true);
|
||||||
|
|
||||||
titleLabel = std::make_unique<juce::Label>("title", "GELYKEQ");
|
titleLabel = std::make_unique<juce::Label>("title", "GELYK");
|
||||||
titleLabel->setFont(monoFont(26.0f));
|
titleLabel->setFont(monoFont(26.0f));
|
||||||
titleLabel->setColour(juce::Label::textColourId, lookAndFeel.cyan);
|
titleLabel->setColour(juce::Label::textColourId, lookAndFeel.cyan());
|
||||||
titleLabel->setJustificationType(juce::Justification::centredLeft);
|
titleLabel->setJustificationType(juce::Justification::centredLeft);
|
||||||
addAndMakeVisible(*titleLabel);
|
addAndMakeVisible(*titleLabel);
|
||||||
|
|
||||||
|
|
@ -160,10 +290,69 @@ GelykEQAudioProcessorEditor::GelykEQAudioProcessorEditor(GelykEQAudioProcessor&
|
||||||
scaleBox->onChange = [this] { scaleUI((float) scaleBox->getSelectedId()); };
|
scaleBox->onChange = [this] { scaleUI((float) scaleBox->getSelectedId()); };
|
||||||
addAndMakeVisible(*scaleBox);
|
addAndMakeVisible(*scaleBox);
|
||||||
|
|
||||||
|
const struct ColorControl
|
||||||
|
{
|
||||||
|
std::unique_ptr<juce::Label>* label;
|
||||||
|
std::unique_ptr<juce::Slider>* slider;
|
||||||
|
const char* text;
|
||||||
|
double min, max, def;
|
||||||
|
const char* tip;
|
||||||
|
} colorControls[] = {
|
||||||
|
{ &waveSensLabel, &waveSensSlider, "SENS", -12.0, 12.0, 0.0, "Live waveform sensitivity (dB)" },
|
||||||
|
{ &colorHueLabel, &colorHueSlider, "HUE", -0.5, 0.5, 0.0, "Master colour hue shift" },
|
||||||
|
{ &colorSatLabel, &colorSatSlider, "SAT", 0.0, 1.0, 1.0, "Master colour saturation" },
|
||||||
|
{ &colorBriLabel, &colorBriSlider, "BRI", 0.5, 1.5, 1.0, "Master colour brightness" },
|
||||||
|
{ &waveLabel, &waveSlider, "WAVE", 0.0, 1.0, 0.2, "Live waveform opacity" },
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const auto& cc : colorControls)
|
||||||
|
{
|
||||||
|
*cc.label = std::make_unique<juce::Label>("ccap", cc.text);
|
||||||
|
(*cc.label)->setFont(monoFont(8.0f));
|
||||||
|
(*cc.label)->setColour(juce::Label::textColourId, lookAndFeel.textDim());
|
||||||
|
(*cc.label)->setJustificationType(juce::Justification::centred);
|
||||||
|
addAndMakeVisible(**cc.label);
|
||||||
|
|
||||||
|
*cc.slider = std::make_unique<juce::Slider>(juce::Slider::LinearHorizontal, juce::Slider::NoTextBox);
|
||||||
|
(*cc.slider)->setLookAndFeel(&lookAndFeel);
|
||||||
|
(*cc.slider)->setSliderStyle(juce::Slider::LinearHorizontal);
|
||||||
|
(*cc.slider)->setRange(cc.min, cc.max, 0.01);
|
||||||
|
(*cc.slider)->setDoubleClickReturnValue(true, cc.def);
|
||||||
|
(*cc.slider)->setValue(cc.def, juce::dontSendNotification);
|
||||||
|
(*cc.slider)->setTooltip(cc.tip);
|
||||||
|
addAndMakeVisible(**cc.slider);
|
||||||
|
}
|
||||||
|
|
||||||
|
colorHueSlider->onValueChange = [this]
|
||||||
|
{
|
||||||
|
lookAndFeel.hueShift = (float) colorHueSlider->getValue();
|
||||||
|
refreshColors();
|
||||||
|
};
|
||||||
|
colorSatSlider->onValueChange = [this]
|
||||||
|
{
|
||||||
|
lookAndFeel.saturation = (float) colorSatSlider->getValue();
|
||||||
|
refreshColors();
|
||||||
|
};
|
||||||
|
colorBriSlider->onValueChange = [this]
|
||||||
|
{
|
||||||
|
lookAndFeel.brightness = (float) colorBriSlider->getValue();
|
||||||
|
refreshColors();
|
||||||
|
};
|
||||||
|
waveSlider->onValueChange = [this]
|
||||||
|
{
|
||||||
|
lookAndFeel.waveformOpacity = (float) waveSlider->getValue();
|
||||||
|
repaint();
|
||||||
|
};
|
||||||
|
waveSensSlider->onValueChange = [this]
|
||||||
|
{
|
||||||
|
lookAndFeel.waveSensitivity = (float) waveSensSlider->getValue();
|
||||||
|
repaint();
|
||||||
|
};
|
||||||
|
|
||||||
// Master gain (vertical fader, far right).
|
// Master gain (vertical fader, far right).
|
||||||
masterLabel = std::make_unique<juce::Label>("mlab", "MASTER");
|
masterLabel = std::make_unique<juce::Label>("mlab", "MASTER");
|
||||||
masterLabel->setFont(monoFont(11.0f));
|
masterLabel->setFont(monoFont(11.0f));
|
||||||
masterLabel->setColour(juce::Label::textColourId, lookAndFeel.magenta);
|
masterLabel->setColour(juce::Label::textColourId, lookAndFeel.magenta());
|
||||||
masterLabel->setJustificationType(juce::Justification::centred);
|
masterLabel->setJustificationType(juce::Justification::centred);
|
||||||
master = std::make_unique<juce::Slider>(juce::Slider::LinearVertical, juce::Slider::TextBoxBelow);
|
master = std::make_unique<juce::Slider>(juce::Slider::LinearVertical, juce::Slider::TextBoxBelow);
|
||||||
master->setLookAndFeel(&lookAndFeel);
|
master->setLookAndFeel(&lookAndFeel);
|
||||||
|
|
@ -173,15 +362,23 @@ GelykEQAudioProcessorEditor::GelykEQAudioProcessorEditor(GelykEQAudioProcessor&
|
||||||
master->setDoubleClickReturnValue(true, 0.0);
|
master->setDoubleClickReturnValue(true, 0.0);
|
||||||
addAndMakeVisible(*masterLabel);
|
addAndMakeVisible(*masterLabel);
|
||||||
addAndMakeVisible(*master);
|
addAndMakeVisible(*master);
|
||||||
|
|
||||||
|
masterPercent = std::make_unique<juce::Label>("mper", "100%");
|
||||||
|
masterPercent->setFont(monoFont(10.0f));
|
||||||
|
masterPercent->setColour(juce::Label::textColourId, lookAndFeel.cyan());
|
||||||
|
masterPercent->setJustificationType(juce::Justification::centred);
|
||||||
|
addAndMakeVisible(*masterPercent);
|
||||||
|
updateMasterPercent();
|
||||||
masterAttachment = std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment>(
|
masterAttachment = std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment>(
|
||||||
processor.getAPVTS(), "master", *master);
|
processor.getAPVTS(), "master", *master);
|
||||||
|
master->onValueChange = [this] { updateMasterPercent(); };
|
||||||
// Band faders.
|
// Band faders.
|
||||||
const char* const names[numBands] = { "LOW SHELF", "LOW", "LOW-MID", "MID", "HIGH-MID", "HIGH", "HIGH SHELF" };
|
const char* const names[numBands] = { "LOW SHELF", "LOW", "LOW-MID", "MID", "HIGH-MID", "HIGH", "HIGH SHELF" };
|
||||||
const char* const freqs[numBands] = { "60", "160", "400", "800", "1.6K", "4K", "10K" };
|
const char* const freqs[numBands] = { "60", "160", "400", "800", "1.6K", "4K", "10K" };
|
||||||
for (int i = 0; i < numBands; ++i)
|
for (int i = 0; i < numBands; ++i)
|
||||||
{
|
{
|
||||||
auto& w = bands[i];
|
auto& w = bands[i];
|
||||||
const juce::Colour accent = (i % 2 == 0) ? lookAndFeel.cyan : lookAndFeel.magenta;
|
const juce::Colour accent = (i % 2 == 0) ? lookAndFeel.cyan() : lookAndFeel.magenta();
|
||||||
|
|
||||||
w.freqLabel = std::make_unique<juce::Label>("freq", freqs[i]);
|
w.freqLabel = std::make_unique<juce::Label>("freq", freqs[i]);
|
||||||
w.freqLabel->setFont(monoFont(14.0f).boldened());
|
w.freqLabel->setFont(monoFont(14.0f).boldened());
|
||||||
|
|
@ -190,7 +387,7 @@ GelykEQAudioProcessorEditor::GelykEQAudioProcessorEditor(GelykEQAudioProcessor&
|
||||||
|
|
||||||
w.gainCaption = std::make_unique<juce::Label>("gname", names[i]);
|
w.gainCaption = std::make_unique<juce::Label>("gname", names[i]);
|
||||||
w.gainCaption->setFont(monoFont(10.0f));
|
w.gainCaption->setFont(monoFont(10.0f));
|
||||||
w.gainCaption->setColour(juce::Label::textColourId, lookAndFeel.textDim);
|
w.gainCaption->setColour(juce::Label::textColourId, lookAndFeel.textDim());
|
||||||
w.gainCaption->setJustificationType(juce::Justification::centred);
|
w.gainCaption->setJustificationType(juce::Justification::centred);
|
||||||
|
|
||||||
w.gainValue = std::make_unique<juce::Label>("gval", "0.0");
|
w.gainValue = std::make_unique<juce::Label>("gval", "0.0");
|
||||||
|
|
@ -200,23 +397,59 @@ GelykEQAudioProcessorEditor::GelykEQAudioProcessorEditor(GelykEQAudioProcessor&
|
||||||
|
|
||||||
w.qCaption = std::make_unique<juce::Label>("qcap", "Q");
|
w.qCaption = std::make_unique<juce::Label>("qcap", "Q");
|
||||||
w.qCaption->setFont(monoFont(10.0f));
|
w.qCaption->setFont(monoFont(10.0f));
|
||||||
w.qCaption->setColour(juce::Label::textColourId, lookAndFeel.textDim);
|
w.qCaption->setColour(juce::Label::textColourId, lookAndFeel.textDim());
|
||||||
w.qCaption->setJustificationType(juce::Justification::centred);
|
w.qCaption->setJustificationType(juce::Justification::centred);
|
||||||
|
|
||||||
w.qValue = std::make_unique<juce::Label>("qval", "1.00");
|
w.qValue = std::make_unique<juce::Label>("qval", "1.00");
|
||||||
w.qValue->setFont(monoFont(10.0f));
|
w.qValue->setFont(monoFont(10.0f));
|
||||||
w.qValue->setColour(juce::Label::textColourId, lookAndFeel.violet);
|
w.qValue->setColour(juce::Label::textColourId, lookAndFeel.violet());
|
||||||
w.qValue->setJustificationType(juce::Justification::centred);
|
w.qValue->setJustificationType(juce::Justification::centred);
|
||||||
|
|
||||||
w.gain = std::make_unique<juce::Slider>(juce::Slider::LinearVertical, juce::Slider::NoTextBox);
|
w.gain = std::make_unique<BandFader>(processor.getAPVTS(),
|
||||||
w.gain->setLookAndFeel(&lookAndFeel);
|
"gain" + juce::String(i),
|
||||||
w.gain->setSliderStyle(juce::Slider::LinearVertical);
|
"freq" + juce::String(i),
|
||||||
w.gain->setRange(-24.0, 24.0, 0.01);
|
"q" + juce::String(i),
|
||||||
w.gain->setDoubleClickReturnValue(true, 0.0);
|
lookAndFeel);
|
||||||
w.gain->setValue(0.0, juce::dontSendNotification);
|
w.gain->onFreqChange = [this]
|
||||||
w.gain->onValueChange = [this, i]
|
|
||||||
{
|
{
|
||||||
bands[i].gainValue->setText(juce::String(bands[i].gain->getValue(), 1), juce::dontSendNotification);
|
resized();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Keep bands in left-to-right frequency order: each band can only go up
|
||||||
|
// to its right-hand neighbour and down to its left-hand neighbour.
|
||||||
|
w.gain->onGetFreqBounds = [this, i]() -> std::pair<float, float>
|
||||||
|
{
|
||||||
|
auto& apvts = processor.getAPVTS();
|
||||||
|
float lo = 20.0f;
|
||||||
|
float hi = 20000.0f;
|
||||||
|
if (i > 0)
|
||||||
|
lo = apvts.getRawParameterValue("freq" + juce::String(i - 1))->load() * 1.01f;
|
||||||
|
if (i < numBands - 1)
|
||||||
|
hi = apvts.getRawParameterValue("freq" + juce::String(i + 1))->load() * 0.99f;
|
||||||
|
return { juce::jmax(20.0f, lo), juce::jmin(20000.0f, hi) };
|
||||||
|
};
|
||||||
|
|
||||||
|
w.levelValue = std::make_unique<juce::Label>("lval", "0.0");
|
||||||
|
w.levelValue->setFont(monoFont(10.0f));
|
||||||
|
w.levelValue->setColour(juce::Label::textColourId, accent);
|
||||||
|
w.levelValue->setJustificationType(juce::Justification::centred);
|
||||||
|
|
||||||
|
w.levelCaption = std::make_unique<juce::Label>("lcap", "LEVEL");
|
||||||
|
w.levelCaption->setFont(monoFont(10.0f));
|
||||||
|
w.levelCaption->setColour(juce::Label::textColourId, lookAndFeel.textDim());
|
||||||
|
w.levelCaption->setJustificationType(juce::Justification::centred);
|
||||||
|
|
||||||
|
w.level = std::make_unique<juce::Slider>(juce::Slider::RotaryVerticalDrag, juce::Slider::NoTextBox);
|
||||||
|
w.level->setLookAndFeel(&lookAndFeel);
|
||||||
|
w.level->setSliderStyle(juce::Slider::RotaryVerticalDrag);
|
||||||
|
w.level->setRotaryParameters(2.4f, 6.9f, true); // ~270° sweep
|
||||||
|
w.level->setRange(-24.0, 24.0, 0.01);
|
||||||
|
w.level->setDoubleClickReturnValue(true, 0.0);
|
||||||
|
w.level->setValue(0.0, juce::dontSendNotification);
|
||||||
|
w.level->setTooltip("Gain (level), also adjustable via the fader");
|
||||||
|
w.level->onValueChange = [this, i]
|
||||||
|
{
|
||||||
|
updateGainValue(i);
|
||||||
};
|
};
|
||||||
|
|
||||||
w.q = std::make_unique<juce::Slider>(juce::Slider::RotaryVerticalDrag, juce::Slider::NoTextBox);
|
w.q = std::make_unique<juce::Slider>(juce::Slider::RotaryVerticalDrag, juce::Slider::NoTextBox);
|
||||||
|
|
@ -233,12 +466,12 @@ GelykEQAudioProcessorEditor::GelykEQAudioProcessorEditor(GelykEQAudioProcessor&
|
||||||
|
|
||||||
w.freqCaption = std::make_unique<juce::Label>("fcap", "FREQ");
|
w.freqCaption = std::make_unique<juce::Label>("fcap", "FREQ");
|
||||||
w.freqCaption->setFont(monoFont(10.0f));
|
w.freqCaption->setFont(monoFont(10.0f));
|
||||||
w.freqCaption->setColour(juce::Label::textColourId, lookAndFeel.textDim);
|
w.freqCaption->setColour(juce::Label::textColourId, lookAndFeel.textDim());
|
||||||
w.freqCaption->setJustificationType(juce::Justification::centred);
|
w.freqCaption->setJustificationType(juce::Justification::centred);
|
||||||
|
|
||||||
w.freqValue = std::make_unique<juce::Label>("fval", "160");
|
w.freqValue = std::make_unique<juce::Label>("fval", "160");
|
||||||
w.freqValue->setFont(monoFont(10.0f));
|
w.freqValue->setFont(monoFont(10.0f));
|
||||||
w.freqValue->setColour(juce::Label::textColourId, lookAndFeel.cyan);
|
w.freqValue->setColour(juce::Label::textColourId, lookAndFeel.cyan());
|
||||||
w.freqValue->setJustificationType(juce::Justification::centred);
|
w.freqValue->setJustificationType(juce::Justification::centred);
|
||||||
|
|
||||||
w.freq = std::make_unique<juce::Slider>(juce::Slider::RotaryVerticalDrag, juce::Slider::NoTextBox);
|
w.freq = std::make_unique<juce::Slider>(juce::Slider::RotaryVerticalDrag, juce::Slider::NoTextBox);
|
||||||
|
|
@ -258,12 +491,16 @@ GelykEQAudioProcessorEditor::GelykEQAudioProcessorEditor(GelykEQAudioProcessor&
|
||||||
else
|
else
|
||||||
txt = juce::String((int) f);
|
txt = juce::String((int) f);
|
||||||
bands[i].freqValue->setText(txt, juce::dontSendNotification);
|
bands[i].freqValue->setText(txt, juce::dontSendNotification);
|
||||||
|
bands[i].freqLabel->setText(txt, juce::dontSendNotification);
|
||||||
};
|
};
|
||||||
|
|
||||||
addAndMakeVisible(*w.freqLabel);
|
addAndMakeVisible(*w.freqLabel);
|
||||||
addAndMakeVisible(*w.gainCaption);
|
addAndMakeVisible(*w.gainCaption);
|
||||||
addAndMakeVisible(*w.gainValue);
|
addAndMakeVisible(*w.gainValue);
|
||||||
addAndMakeVisible(*w.gain);
|
addAndMakeVisible(*w.gain);
|
||||||
|
addAndMakeVisible(*w.levelValue);
|
||||||
|
addAndMakeVisible(*w.levelCaption);
|
||||||
|
addAndMakeVisible(*w.level);
|
||||||
addAndMakeVisible(*w.freqCaption);
|
addAndMakeVisible(*w.freqCaption);
|
||||||
addAndMakeVisible(*w.freqValue);
|
addAndMakeVisible(*w.freqValue);
|
||||||
addAndMakeVisible(*w.freq);
|
addAndMakeVisible(*w.freq);
|
||||||
|
|
@ -271,8 +508,8 @@ GelykEQAudioProcessorEditor::GelykEQAudioProcessorEditor(GelykEQAudioProcessor&
|
||||||
addAndMakeVisible(*w.q);
|
addAndMakeVisible(*w.q);
|
||||||
addAndMakeVisible(*w.qValue);
|
addAndMakeVisible(*w.qValue);
|
||||||
|
|
||||||
gainAttachments[i] = std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment>(
|
levelAttachments[i] = std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment>(
|
||||||
processor.getAPVTS(), "gain" + juce::String(i), *w.gain);
|
processor.getAPVTS(), "gain" + juce::String(i), *w.level);
|
||||||
qAttachments[i] = std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment>(
|
qAttachments[i] = std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment>(
|
||||||
processor.getAPVTS(), "q" + juce::String(i), *w.q);
|
processor.getAPVTS(), "q" + juce::String(i), *w.q);
|
||||||
freqAttachments[i] = std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment>(
|
freqAttachments[i] = std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment>(
|
||||||
|
|
@ -282,13 +519,13 @@ GelykEQAudioProcessorEditor::GelykEQAudioProcessorEditor(GelykEQAudioProcessor&
|
||||||
// Status bar.
|
// Status bar.
|
||||||
statusGit = std::make_unique<juce::Label>("git", processor.getGitInfo());
|
statusGit = std::make_unique<juce::Label>("git", processor.getGitInfo());
|
||||||
statusGit->setFont(monoFont(11.0f));
|
statusGit->setFont(monoFont(11.0f));
|
||||||
statusGit->setColour(juce::Label::textColourId, lookAndFeel.textDim);
|
statusGit->setColour(juce::Label::textColourId, lookAndFeel.textDim());
|
||||||
statusGit->setJustificationType(juce::Justification::centredRight);
|
statusGit->setJustificationType(juce::Justification::centredRight);
|
||||||
|
|
||||||
statusBar = std::make_unique<juce::Label>("status",
|
statusBar = std::make_unique<juce::Label>("status",
|
||||||
"GELYKEQ v" + juce::String(ProjectInfo::versionString) + " | ANALYSER");
|
"GELYK v" + juce::String(ProjectInfo::versionString) + "");
|
||||||
statusBar->setFont(monoFont(11.0f));
|
statusBar->setFont(monoFont(11.0f));
|
||||||
statusBar->setColour(juce::Label::textColourId, lookAndFeel.cyan.withAlpha(0.8f));
|
statusBar->setColour(juce::Label::textColourId, lookAndFeel.cyan().withAlpha(0.8f));
|
||||||
statusBar->setJustificationType(juce::Justification::centredLeft);
|
statusBar->setJustificationType(juce::Justification::centredLeft);
|
||||||
addAndMakeVisible(*statusBar);
|
addAndMakeVisible(*statusBar);
|
||||||
addAndMakeVisible(*statusGit);
|
addAndMakeVisible(*statusGit);
|
||||||
|
|
@ -298,6 +535,7 @@ GelykEQAudioProcessorEditor::GelykEQAudioProcessorEditor(GelykEQAudioProcessor&
|
||||||
eqCurve.resize(128);
|
eqCurve.resize(128);
|
||||||
|
|
||||||
startTimerHz(30);
|
startTimerHz(30);
|
||||||
|
refreshColors();
|
||||||
scaleUI(100.0f);
|
scaleUI(100.0f);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -332,12 +570,95 @@ void GelykEQAudioProcessorEditor::rebuildFaders()
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void GelykEQAudioProcessorEditor::updateGainValue(int i)
|
||||||
|
{
|
||||||
|
const juce::String txt(juce::String(bands[i].level->getValue(), 1));
|
||||||
|
bands[i].gainValue->setText(txt, juce::dontSendNotification);
|
||||||
|
bands[i].levelValue->setText(txt, juce::dontSendNotification);
|
||||||
|
}
|
||||||
|
|
||||||
|
void GelykEQAudioProcessorEditor::updateMasterPercent()
|
||||||
|
{
|
||||||
|
const float db = (float) master->getValue();
|
||||||
|
const float percent = std::pow(10.0f, db / 20.0f) * 100.0f;
|
||||||
|
masterPercent->setText(juce::String(percent, 0) + "%", juce::dontSendNotification);
|
||||||
|
}
|
||||||
|
|
||||||
|
void GelykEQAudioProcessorEditor::refreshColors()
|
||||||
|
{
|
||||||
|
lookAndFeel.refreshPalette();
|
||||||
|
|
||||||
|
titleLabel->setColour(juce::Label::textColourId, lookAndFeel.cyan());
|
||||||
|
masterLabel->setColour(juce::Label::textColourId, lookAndFeel.magenta());
|
||||||
|
masterPercent->setColour(juce::Label::textColourId, lookAndFeel.cyan());
|
||||||
|
// The scale dropdown and the master value readout read their text colour from
|
||||||
|
// the LookAndFeel at construction, so they must be re-applied explicitly here
|
||||||
|
// for the saturation (and hue/brightness) faders to affect them.
|
||||||
|
scaleBox->setColour(juce::ComboBox::textColourId, lookAndFeel.cyan());
|
||||||
|
master->setColour(juce::Slider::textBoxTextColourId, lookAndFeel.cyan());
|
||||||
|
master->setColour(juce::Slider::textBoxBackgroundColourId, lookAndFeel.panel());
|
||||||
|
statusBar->setColour(juce::Label::textColourId, lookAndFeel.cyan().withAlpha(0.8f));
|
||||||
|
statusGit->setColour(juce::Label::textColourId, lookAndFeel.textDim());
|
||||||
|
colorHueLabel->setColour(juce::Label::textColourId, lookAndFeel.cyan());
|
||||||
|
colorSatLabel->setColour(juce::Label::textColourId, lookAndFeel.magenta());
|
||||||
|
colorBriLabel->setColour(juce::Label::textColourId, lookAndFeel.violet());
|
||||||
|
waveLabel->setColour(juce::Label::textColourId, lookAndFeel.textDim());
|
||||||
|
waveSensLabel->setColour(juce::Label::textColourId, lookAndFeel.textDim());
|
||||||
|
for (int i = 0; i < numBands; ++i)
|
||||||
|
{
|
||||||
|
auto& w = bands[i];
|
||||||
|
const juce::Colour accent = (i % 2 == 0) ? lookAndFeel.cyan() : lookAndFeel.magenta();
|
||||||
|
w.freqLabel->setColour(juce::Label::textColourId, accent);
|
||||||
|
w.gainCaption->setColour(juce::Label::textColourId, lookAndFeel.textDim());
|
||||||
|
w.gainValue->setColour(juce::Label::textColourId, accent);
|
||||||
|
w.levelCaption->setColour(juce::Label::textColourId, lookAndFeel.textDim());
|
||||||
|
w.levelValue->setColour(juce::Label::textColourId, accent);
|
||||||
|
w.qCaption->setColour(juce::Label::textColourId, lookAndFeel.textDim());
|
||||||
|
w.qValue->setColour(juce::Label::textColourId, lookAndFeel.violet());
|
||||||
|
w.freqCaption->setColour(juce::Label::textColourId, lookAndFeel.textDim());
|
||||||
|
w.freqValue->setColour(juce::Label::textColourId, lookAndFeel.cyan());
|
||||||
|
}
|
||||||
|
|
||||||
|
repaint();
|
||||||
|
}
|
||||||
|
|
||||||
void GelykEQAudioProcessorEditor::scaleUI(float percent)
|
void GelykEQAudioProcessorEditor::scaleUI(float percent)
|
||||||
{
|
{
|
||||||
const float s = percent / 100.0f;
|
const float s = percent / 100.0f;
|
||||||
scaling.store(s);
|
scaling.store(s);
|
||||||
|
lookAndFeel.uiScale = s;
|
||||||
setSize((int) (1180 * s), (int) (640 * s));
|
setSize((int) (1180 * s), (int) (640 * s));
|
||||||
resized();
|
resized();
|
||||||
|
refreshFonts(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
void GelykEQAudioProcessorEditor::refreshFonts(float s)
|
||||||
|
{
|
||||||
|
titleLabel->setFont(monoFont(26.0f * s));
|
||||||
|
masterLabel->setFont(monoFont(11.0f * s));
|
||||||
|
statusGit->setFont(monoFont(11.0f * s));
|
||||||
|
statusBar->setFont(monoFont(11.0f * s));
|
||||||
|
colorHueLabel->setFont(monoFont(8.0f * s));
|
||||||
|
colorSatLabel->setFont(monoFont(8.0f * s));
|
||||||
|
colorBriLabel->setFont(monoFont(8.0f * s));
|
||||||
|
waveLabel->setFont(monoFont(8.0f * s));
|
||||||
|
waveSensLabel->setFont(monoFont(8.0f * s));
|
||||||
|
|
||||||
|
for (int i = 0; i < numBands; ++i)
|
||||||
|
{
|
||||||
|
auto& w = bands[i];
|
||||||
|
w.freqLabel->setFont(monoFont(14.0f * s).boldened());
|
||||||
|
w.gainCaption->setFont(monoFont(10.0f * s));
|
||||||
|
w.gainValue->setFont(monoFont(11.0f * s));
|
||||||
|
w.levelCaption->setFont(monoFont(10.0f * s));
|
||||||
|
w.levelValue->setFont(monoFont(10.0f * s));
|
||||||
|
w.qCaption->setFont(monoFont(10.0f * s));
|
||||||
|
w.qValue->setFont(monoFont(10.0f * s));
|
||||||
|
w.freqCaption->setFont(monoFont(10.0f * s));
|
||||||
|
w.freqValue->setFont(monoFont(10.0f * s));
|
||||||
|
}
|
||||||
|
|
||||||
|
repaint();
|
||||||
}
|
}
|
||||||
|
|
||||||
void GelykEQAudioProcessorEditor::resized()
|
void GelykEQAudioProcessorEditor::resized()
|
||||||
|
|
@ -354,6 +675,18 @@ void GelykEQAudioProcessorEditor::resized()
|
||||||
titleLabel->setBounds(header.removeFromLeft((int) (180 * s)).reduced(pad, 0));
|
titleLabel->setBounds(header.removeFromLeft((int) (180 * s)).reduced(pad, 0));
|
||||||
scaleBox->setBounds(header.removeFromRight((int) (90 * s)).reduced((int)(6*s), (int)(9*s)));
|
scaleBox->setBounds(header.removeFromRight((int) (90 * s)).reduced((int)(6*s), (int)(9*s)));
|
||||||
|
|
||||||
|
// Colour controls, just left of the scaling dropdown.
|
||||||
|
juce::Rectangle<int> colorArea = header.removeFromRight((int) (360 * s)).reduced((int)(2*s), (int)(9*s));
|
||||||
|
std::unique_ptr<juce::Slider>* colorSliders[] = { &waveSensSlider, &colorHueSlider, &colorSatSlider, &colorBriSlider, &waveSlider };
|
||||||
|
std::unique_ptr<juce::Label>* colorLabels[] = { &waveSensLabel, &colorHueLabel, &colorSatLabel, &colorBriLabel, &waveLabel };
|
||||||
|
int cellW = colorArea.getWidth() / 5;
|
||||||
|
for (int i = 0; i < 5; ++i)
|
||||||
|
{
|
||||||
|
juce::Rectangle<int> cell = colorArea.removeFromLeft(cellW);
|
||||||
|
(*colorLabels[i])->setBounds(cell.removeFromTop((int)(13*s)));
|
||||||
|
(*colorSliders[i])->setBounds(cell);
|
||||||
|
}
|
||||||
|
|
||||||
// Footer.
|
// Footer.
|
||||||
juce::Rectangle<int> footer = area.removeFromBottom(footerH);
|
juce::Rectangle<int> footer = area.removeFromBottom(footerH);
|
||||||
footer.reduce(pad, 0);
|
footer.reduce(pad, 0);
|
||||||
|
|
@ -375,48 +708,66 @@ void GelykEQAudioProcessorEditor::resized()
|
||||||
for (int i = 0; i < numBands; ++i)
|
for (int i = 0; i < numBands; ++i)
|
||||||
{
|
{
|
||||||
auto& w = bands[i];
|
auto& w = bands[i];
|
||||||
|
const int captionH = (int) (20 * s);
|
||||||
|
const int valueH = (int) (17 * s);
|
||||||
|
const int freqH = (int) (23 * s);
|
||||||
|
const int knobStackH = (int) (16 * s) + (int)(48 * s) + (int)(12 * s); // value + knob + caption
|
||||||
|
|
||||||
|
// Fixed, evenly spaced column for all labels and the knob row.
|
||||||
juce::Rectangle<int> col(x0 + i * (colW + gap), y0, colW, height);
|
juce::Rectangle<int> col(x0 + i * (colW + gap), y0, colW, height);
|
||||||
|
|
||||||
const int captionH = (int) (18 * s);
|
|
||||||
const int valueH = (int) (15 * s);
|
|
||||||
const int freqH = (int) (20 * s);
|
|
||||||
const int knobStackH = (int) (14 * s) + (int)(58 * s) + (int)(12 * s); // value + knob + caption
|
|
||||||
|
|
||||||
w.gainCaption->setBounds(col.removeFromTop(captionH));
|
w.gainCaption->setBounds(col.removeFromTop(captionH));
|
||||||
w.gainValue->setBounds(col.removeFromTop(valueH));
|
w.gainValue->setBounds(col.removeFromTop(valueH));
|
||||||
|
|
||||||
// The gain fader fills the large analyser area (spectrum drawn behind it).
|
// Only the fader slides horizontally to the band's frequency on the shared
|
||||||
w.gain->setBounds(col.removeFromTop(col.getHeight() - freqH - knobStackH - (int)(6*s)));
|
// log axis; everything below stays anchored to the fixed column.
|
||||||
|
juce::Rectangle<int> body = col;
|
||||||
|
const int faderH = body.getHeight() - freqH - knobStackH - (int)(6 * s);
|
||||||
|
|
||||||
w.freqLabel->setBounds(col.removeFromTop(freqH));
|
const float f = processor.getAPVTS().getRawParameterValue("freq" + juce::String(i))->load();
|
||||||
|
const int cx = (int) bandFreqToX(f, (float) x0, (float) totalW);
|
||||||
|
const int faderX = juce::jlimit(x0, x0 + totalW - colW, cx - colW / 2);
|
||||||
|
w.gain->setBounds(faderX, body.getY(), colW, faderH);
|
||||||
|
w.gain->setDragSpace((float) x0, (float) totalW);
|
||||||
|
|
||||||
// Bottom control row: FREQ and Q knobs side by side.
|
juce::Rectangle<int> rest = col;
|
||||||
juce::Rectangle<int> knobRow = col;
|
rest.removeFromTop(faderH + (int)(6 * s));
|
||||||
|
w.freqLabel->setBounds(rest.removeFromTop(freqH));
|
||||||
|
|
||||||
|
// Bottom control row: LEVEL, FREQ and Q knobs side by side.
|
||||||
|
juce::Rectangle<int> knobRow = rest;
|
||||||
|
bandSections[i] = knobRow.toFloat();
|
||||||
int rowW = knobRow.getWidth();
|
int rowW = knobRow.getWidth();
|
||||||
int halfW = rowW / 2;
|
int thirdW = rowW / 3;
|
||||||
|
|
||||||
// FREQ (left half).
|
// LEVEL (left third).
|
||||||
juce::Rectangle<int> freqCol = knobRow.removeFromLeft(halfW);
|
juce::Rectangle<int> levelCol = knobRow.removeFromLeft(thirdW);
|
||||||
w.freqValue->setBounds(freqCol.removeFromTop((int)(14*s)));
|
w.levelValue->setBounds(levelCol.removeFromTop((int)(16*s)));
|
||||||
w.freq->setBounds(freqCol.removeFromTop((int)(58*s)).reduced((int)(colW*0.08f), 0));
|
w.level->setBounds(levelCol.removeFromTop((int)(48*s)).reduced((int)(colW*0.08f), 0));
|
||||||
|
w.levelCaption->setBounds(levelCol.removeFromTop((int)(12*s)));
|
||||||
|
|
||||||
|
// FREQ (middle third).
|
||||||
|
juce::Rectangle<int> freqCol = knobRow.removeFromLeft(thirdW);
|
||||||
|
w.freqValue->setBounds(freqCol.removeFromTop((int)(16*s)));
|
||||||
|
w.freq->setBounds(freqCol.removeFromTop((int)(48*s)).reduced((int)(colW*0.08f), 0));
|
||||||
w.freqCaption->setBounds(freqCol.removeFromTop((int)(12*s)));
|
w.freqCaption->setBounds(freqCol.removeFromTop((int)(12*s)));
|
||||||
|
|
||||||
// Q (right half).
|
// Q (right third).
|
||||||
w.qValue->setBounds(knobRow.removeFromTop((int)(14*s)));
|
w.qValue->setBounds(knobRow.removeFromTop((int)(16*s)));
|
||||||
w.q->setBounds(knobRow.removeFromTop((int)(58*s)).reduced((int)(colW*0.08f), 0));
|
w.q->setBounds(knobRow.removeFromTop((int)(48*s)).reduced((int)(colW*0.08f), 0));
|
||||||
w.qCaption->setBounds(knobRow.removeFromTop((int)(12*s)));
|
w.qCaption->setBounds(knobRow.removeFromTop((int)(12*s)));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Master column.
|
// Master column.
|
||||||
juce::Rectangle<int> msub = masterArea.reduced((int)(14*s), (int)(6*s));
|
juce::Rectangle<int> msub = masterArea.reduced((int)(14*s), (int)(6*s));
|
||||||
masterLabel->setBounds(msub.removeFromTop((int) (20 * s)));
|
masterLabel->setBounds(msub.removeFromTop((int) (22 * s)));
|
||||||
|
masterPercent->setBounds(msub.removeFromTop((int) (16 * s)));
|
||||||
master->setBounds(msub);
|
master->setBounds(msub);
|
||||||
}
|
}
|
||||||
|
|
||||||
void GelykEQAudioProcessorEditor::paint(juce::Graphics& g)
|
void GelykEQAudioProcessorEditor::paint(juce::Graphics& g)
|
||||||
{
|
{
|
||||||
const float s = scaling.load();
|
const float s = scaling.load();
|
||||||
g.fillAll(lookAndFeel.bg);
|
g.fillAll(lookAndFeel.bg());
|
||||||
|
|
||||||
juce::Rectangle<int> area = getLocalBounds();
|
juce::Rectangle<int> area = getLocalBounds();
|
||||||
const int headerH = (int) (46 * s);
|
const int headerH = (int) (46 * s);
|
||||||
|
|
@ -426,13 +777,18 @@ void GelykEQAudioProcessorEditor::paint(juce::Graphics& g)
|
||||||
mainArea.removeFromBottom(footerH);
|
mainArea.removeFromBottom(footerH);
|
||||||
mainArea.removeFromRight((int) (64 * s));
|
mainArea.removeFromRight((int) (64 * s));
|
||||||
|
|
||||||
const int topPad = (int) (20 * s);
|
// The visualizer is the gain-fader field, bounded above by the header controls
|
||||||
const int botPad = (int) (28 * s);
|
// and below by the LEVEL/FREQ/Q knob row. Keep the waveform/spectrum and grid
|
||||||
const float topY = (float) (mainArea.getY() + topPad);
|
// strictly inside it so they don't spill over the knobs.
|
||||||
const float botY = (float) (mainArea.getY() + mainArea.getHeight() - botPad);
|
const float captionH = 20.0f * s;
|
||||||
|
const float valueH = 17.0f * s;
|
||||||
|
const float freqH = 23.0f * s;
|
||||||
|
const float knobStackH = (16.0f + 48.0f + 12.0f) * s;
|
||||||
|
const float topY = (float) mainArea.getY() + captionH + valueH;
|
||||||
|
const float botY = (float) mainArea.getBottom() - freqH - knobStackH - 6.0f * s;
|
||||||
|
|
||||||
// Neon grid lines.
|
// Neon grid lines.
|
||||||
g.setColour(lookAndFeel.line);
|
g.setColour(lookAndFeel.line());
|
||||||
for (int i = 1; i <= 4; ++i)
|
for (int i = 1; i <= 4; ++i)
|
||||||
{
|
{
|
||||||
if (i == 3) continue; // skip the 0 dB centre (drawn separately)
|
if (i == 3) continue; // skip the 0 dB centre (drawn separately)
|
||||||
|
|
@ -442,21 +798,27 @@ void GelykEQAudioProcessorEditor::paint(juce::Graphics& g)
|
||||||
|
|
||||||
// 0 dB centre line (glowing cyan).
|
// 0 dB centre line (glowing cyan).
|
||||||
const float centreLineY = topY + (botY - topY) * 0.5f;
|
const float centreLineY = topY + (botY - topY) * 0.5f;
|
||||||
g.setColour(lookAndFeel.cyan.withAlpha(0.55f));
|
g.setColour(lookAndFeel.cyan().withAlpha(0.55f));
|
||||||
g.drawHorizontalLine((int) centreLineY, (float) mainArea.getX(), (float) mainArea.getRight());
|
g.drawHorizontalLine((int) centreLineY, (float) mainArea.getX(), (float) mainArea.getRight());
|
||||||
|
|
||||||
// Spectrum overlay.
|
// Spectrum overlay.
|
||||||
if (spectrum.size() > 2)
|
if (spectrum.size() > 2)
|
||||||
{
|
{
|
||||||
|
const float waveAlpha = lookAndFeel.waveformOpacity;
|
||||||
|
const float sensFactor = std::pow(10.0f, lookAndFeel.waveSensitivity / 20.0f);
|
||||||
juce::Path path;
|
juce::Path path;
|
||||||
const float x0 = (float) mainArea.getX();
|
const float x0 = (float) mainArea.getX();
|
||||||
const float width = (float) mainArea.getWidth();
|
const float width = (float) mainArea.getWidth();
|
||||||
|
|
||||||
for (int i = 0; i < spectrum.size(); ++i)
|
for (int i = 0; i < spectrum.size(); ++i)
|
||||||
{
|
{
|
||||||
float db = juce::jlimit(-72.0f, 12.0f, spectrum[i]);
|
float db = juce::Decibels::gainToDecibels(
|
||||||
|
juce::Decibels::decibelsToGain(spectrum[i]) * sensFactor);
|
||||||
|
db = juce::jlimit(-72.0f, 12.0f, db);
|
||||||
float norm = (db + 72.0f) / 84.0f; // 0..1 up
|
float norm = (db + 72.0f) / 84.0f; // 0..1 up
|
||||||
float xx = x0 + (float) i / (float) (spectrum.size() - 1) * width;
|
float xx = (i < freqBins.size())
|
||||||
|
? bandFreqToX(freqBins[i], x0, width)
|
||||||
|
: x0 + (float) i / (float) (spectrum.size() - 1) * width;
|
||||||
float yy = botY - norm * (botY - topY);
|
float yy = botY - norm * (botY - topY);
|
||||||
if (i == 0) path.startNewSubPath(xx, yy);
|
if (i == 0) path.startNewSubPath(xx, yy);
|
||||||
else path.lineTo(xx, yy);
|
else path.lineTo(xx, yy);
|
||||||
|
|
@ -467,12 +829,12 @@ void GelykEQAudioProcessorEditor::paint(juce::Graphics& g)
|
||||||
filled.lineTo((float) mainArea.getX(), botY);
|
filled.lineTo((float) mainArea.getX(), botY);
|
||||||
filled.closeSubPath();
|
filled.closeSubPath();
|
||||||
|
|
||||||
g.setGradientFill(juce::ColourGradient(lookAndFeel.magenta.withAlpha(0.30f),
|
g.setGradientFill(juce::ColourGradient(lookAndFeel.magenta().withAlpha(0.30f * waveAlpha),
|
||||||
(float) mainArea.getX(), topY,
|
(float) mainArea.getX(), topY,
|
||||||
lookAndFeel.magenta.withAlpha(0.05f),
|
lookAndFeel.magenta().withAlpha(0.05f * waveAlpha),
|
||||||
(float) mainArea.getX(), botY, false));
|
(float) mainArea.getX(), botY, false));
|
||||||
g.fillPath(filled);
|
g.fillPath(filled);
|
||||||
g.setColour(lookAndFeel.cyan.withAlpha(0.9f));
|
g.setColour(lookAndFeel.cyan().withAlpha(0.9f * waveAlpha));
|
||||||
g.strokePath(path, juce::PathStrokeType(1.6f * s));
|
g.strokePath(path, juce::PathStrokeType(1.6f * s));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -489,18 +851,31 @@ void GelykEQAudioProcessorEditor::paint(juce::Graphics& g)
|
||||||
{
|
{
|
||||||
float db = juce::jlimit(-24.0f, 24.0f, eqCurve[i]);
|
float db = juce::jlimit(-24.0f, 24.0f, eqCurve[i]);
|
||||||
float yy = centreY - (db / 24.0f) * halfH;
|
float yy = centreY - (db / 24.0f) * halfH;
|
||||||
float xx = x0 + (float) i / (float) (eqCurve.size() - 1) * width;
|
// eqCurve bins are log-spaced from 20 Hz to 20 kHz; use the same
|
||||||
|
// log-frequency mapping as the columns so peaks line up with bands.
|
||||||
|
const float f = 20.0f * std::pow(1000.0f, (float) i / (float) (eqCurve.size() - 1));
|
||||||
|
float xx = bandFreqToX(f, x0, width);
|
||||||
if (i == 0) curve.startNewSubPath(xx, yy);
|
if (i == 0) curve.startNewSubPath(xx, yy);
|
||||||
else curve.lineTo(xx, yy);
|
else curve.lineTo(xx, yy);
|
||||||
}
|
}
|
||||||
|
|
||||||
g.setColour(lookAndFeel.violet.withAlpha(0.20f));
|
g.setColour(lookAndFeel.violet().withAlpha(0.20f));
|
||||||
g.strokePath(curve, juce::PathStrokeType(7.0f * s, juce::PathStrokeType::curved,
|
g.strokePath(curve, juce::PathStrokeType(7.0f * s, juce::PathStrokeType::curved,
|
||||||
juce::PathStrokeType::rounded));
|
juce::PathStrokeType::rounded));
|
||||||
g.setColour(lookAndFeel.violet);
|
g.setColour(lookAndFeel.violet());
|
||||||
g.strokePath(curve, juce::PathStrokeType(1.8f * s, juce::PathStrokeType::curved,
|
g.strokePath(curve, juce::PathStrokeType(1.8f * s, juce::PathStrokeType::curved,
|
||||||
juce::PathStrokeType::rounded));
|
juce::PathStrokeType::rounded));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Very light rounded outline around each band's knob group (LEVEL/FREQ/Q).
|
||||||
|
g.setColour(lookAndFeel.line());
|
||||||
|
const float corner = 6.0f * s;
|
||||||
|
const float inset = 2.0f * s;
|
||||||
|
for (int i = 0; i < numBands; ++i)
|
||||||
|
{
|
||||||
|
juce::Rectangle<float> r = bandSections[i].reduced(inset);
|
||||||
|
g.drawRoundedRectangle(r, corner, 1.0f * s);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace gelyk
|
} // namespace gelyk
|
||||||
|
|
|
||||||
|
|
@ -21,13 +21,115 @@ public:
|
||||||
juce::ComboBox&) override;
|
juce::ComboBox&) override;
|
||||||
void positionComboBoxText(juce::ComboBox&, juce::Label&) override;
|
void positionComboBoxText(juce::ComboBox&, juce::Label&) override;
|
||||||
|
|
||||||
juce::Colour cyan { 0xff00e5ff };
|
juce::Colour cyan() const { return transform(baseCyan); }
|
||||||
juce::Colour magenta{ 0xffff2fd6 };
|
juce::Colour magenta() const { return transform(baseMagenta); }
|
||||||
juce::Colour violet { 0xff8a2be2 };
|
juce::Colour violet() const { return transform(baseViolet); }
|
||||||
juce::Colour bg { 0xff0b0f16 };
|
juce::Colour bg() const { return transform(baseBg); }
|
||||||
juce::Colour panel { 0xff121a24 };
|
juce::Colour panel() const { return transform(basePanel); }
|
||||||
juce::Colour line { 0xff1f2a38 };
|
juce::Colour line() const { return transform(baseLine); }
|
||||||
juce::Colour textDim{ 0xff7a8aa0 };
|
juce::Colour textDim() const { return transform(baseTextDim); }
|
||||||
|
|
||||||
|
void refreshPalette();
|
||||||
|
|
||||||
|
float hueShift = 0.0f;
|
||||||
|
float saturation = 1.0f;
|
||||||
|
float brightness = 1.0f;
|
||||||
|
float waveformOpacity = 0.2f;
|
||||||
|
float waveSensitivity = 0.0f; // in dB, added to the spectrum display level
|
||||||
|
float uiScale = 1.0f;
|
||||||
|
|
||||||
|
private:
|
||||||
|
juce::Colour transform(juce::Colour c) const
|
||||||
|
{
|
||||||
|
return c.withRotatedHue(hueShift)
|
||||||
|
.withMultipliedSaturation(saturation)
|
||||||
|
.withMultipliedBrightness(brightness);
|
||||||
|
}
|
||||||
|
|
||||||
|
const juce::Colour baseCyan { 0xff00e5ff };
|
||||||
|
const juce::Colour baseMagenta{ 0xffff2fd6 };
|
||||||
|
const juce::Colour baseViolet { 0xff8a2be2 };
|
||||||
|
const juce::Colour baseBg { 0xff0b0f16 };
|
||||||
|
const juce::Colour basePanel { 0xff121a24 };
|
||||||
|
const juce::Colour baseLine { 0xff1f2a38 };
|
||||||
|
const juce::Colour baseTextDim{ 0xff7a8aa0 };
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// Log-frequency horizontal mapping shared by the band columns and the EQ curve,
|
||||||
|
// so a band's position in the visualizer always lines up with the curve peak at
|
||||||
|
// the same frequency. 20 Hz sits at the left edge, 20 kHz at the right.
|
||||||
|
inline float bandLogNorm(float freq)
|
||||||
|
{
|
||||||
|
return (std::log10(freq) - std::log10(20.0f)) / (std::log10(20000.0f) - std::log10(20.0f));
|
||||||
|
}
|
||||||
|
inline float bandFreqToX(float freq, float x0, float width)
|
||||||
|
{
|
||||||
|
return x0 + bandLogNorm(freq) * width;
|
||||||
|
}
|
||||||
|
inline float bandXToFreq(float x, float x0, float width)
|
||||||
|
{
|
||||||
|
const float norm = (width > 0.0f) ? (x - x0) / width : 0.0f;
|
||||||
|
return std::pow(10.0f, std::log10(20.0f) + norm * (std::log10(20000.0f) - std::log10(20.0f)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// The large fader of each band. Dragging vertically changes the level, dragging
|
||||||
|
// horizontally changes the band's frequency, and scrolling the mouse wheel
|
||||||
|
// nudges the band's Q. All three write straight to the plugin's parameters.
|
||||||
|
class BandFader final : public juce::Component
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
BandFader(juce::AudioProcessorValueTreeState& apvts,
|
||||||
|
const juce::String& gainId, const juce::String& freqId,
|
||||||
|
const juce::String& qId, NeonLookAndFeel& lnf)
|
||||||
|
: gainP(dynamic_cast<juce::AudioParameterFloat*>(apvts.getParameter(gainId))),
|
||||||
|
freqP(dynamic_cast<juce::AudioParameterFloat*>(apvts.getParameter(freqId))),
|
||||||
|
qP(dynamic_cast<juce::AudioParameterFloat*>(apvts.getParameter(qId))),
|
||||||
|
lf(lnf)
|
||||||
|
{
|
||||||
|
setRepaintsOnMouseActivity(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
void paint(juce::Graphics&) override;
|
||||||
|
void mouseDown(const juce::MouseEvent&) override;
|
||||||
|
void mouseDrag(const juce::MouseEvent&) override;
|
||||||
|
void mouseUp(const juce::MouseEvent&) override;
|
||||||
|
void mouseEnter(const juce::MouseEvent&) override;
|
||||||
|
void mouseExit(const juce::MouseEvent&) override;
|
||||||
|
void mouseWheelMove(const juce::MouseEvent&, const juce::MouseWheelDetails&) override;
|
||||||
|
|
||||||
|
float getGain() const { return gainP != nullptr ? (float) gainP->get() : 0.0f; }
|
||||||
|
|
||||||
|
// The x-origin and width of the full visualizer area, used so a horizontal
|
||||||
|
// drag moves the fader 1:1 with the mouse cursor.
|
||||||
|
void setDragSpace(float x0, float width)
|
||||||
|
{
|
||||||
|
dragX0 = x0;
|
||||||
|
dragWidth = width;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ask the containing editor to redispose the columns after a frequency drag.
|
||||||
|
std::function<void()> onFreqChange;
|
||||||
|
|
||||||
|
// Returns the ordering bounds [minFreq, maxFreq] this band's frequency is
|
||||||
|
// allowed to take, so bands keep their left-to-right order.
|
||||||
|
std::function<std::pair<float, float>()> onGetFreqBounds;
|
||||||
|
|
||||||
|
private:
|
||||||
|
juce::AudioParameterFloat* gainP;
|
||||||
|
juce::AudioParameterFloat* freqP;
|
||||||
|
juce::AudioParameterFloat* qP;
|
||||||
|
NeonLookAndFeel& lf;
|
||||||
|
|
||||||
|
bool dragging = false;
|
||||||
|
bool hovered = false;
|
||||||
|
float startGain = 0.0f;
|
||||||
|
float startFreq = 1000.0f;
|
||||||
|
int startX = 0;
|
||||||
|
int startY = 0;
|
||||||
|
float dragX0 = 0.0f;
|
||||||
|
float dragWidth = 1.0f;
|
||||||
};
|
};
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
@ -46,6 +148,10 @@ public:
|
||||||
private:
|
private:
|
||||||
void rebuildFaders();
|
void rebuildFaders();
|
||||||
void scaleUI(float percent);
|
void scaleUI(float percent);
|
||||||
|
void refreshColors();
|
||||||
|
void refreshFonts(float s);
|
||||||
|
void updateGainValue(int i);
|
||||||
|
void updateMasterPercent();
|
||||||
|
|
||||||
GelykEQAudioProcessor& processor;
|
GelykEQAudioProcessor& processor;
|
||||||
|
|
||||||
|
|
@ -54,12 +160,15 @@ private:
|
||||||
static constexpr int numBands = 7;
|
static constexpr int numBands = 7;
|
||||||
struct BandWidget
|
struct BandWidget
|
||||||
{
|
{
|
||||||
std::unique_ptr<juce::Slider> gain;
|
std::unique_ptr<BandFader> gain;
|
||||||
|
std::unique_ptr<juce::Slider> level;
|
||||||
std::unique_ptr<juce::Slider> q;
|
std::unique_ptr<juce::Slider> q;
|
||||||
std::unique_ptr<juce::Slider> freq;
|
std::unique_ptr<juce::Slider> freq;
|
||||||
std::unique_ptr<juce::Label> freqLabel;
|
std::unique_ptr<juce::Label> freqLabel;
|
||||||
std::unique_ptr<juce::Label> gainCaption;
|
std::unique_ptr<juce::Label> gainCaption;
|
||||||
std::unique_ptr<juce::Label> gainValue;
|
std::unique_ptr<juce::Label> gainValue;
|
||||||
|
std::unique_ptr<juce::Label> levelCaption;
|
||||||
|
std::unique_ptr<juce::Label> levelValue;
|
||||||
std::unique_ptr<juce::Label> qCaption;
|
std::unique_ptr<juce::Label> qCaption;
|
||||||
std::unique_ptr<juce::Label> qValue;
|
std::unique_ptr<juce::Label> qValue;
|
||||||
std::unique_ptr<juce::Label> freqCaption;
|
std::unique_ptr<juce::Label> freqCaption;
|
||||||
|
|
@ -68,6 +177,7 @@ private:
|
||||||
BandWidget bands[numBands];
|
BandWidget bands[numBands];
|
||||||
|
|
||||||
std::unique_ptr<juce::Label> masterLabel;
|
std::unique_ptr<juce::Label> masterLabel;
|
||||||
|
std::unique_ptr<juce::Label> masterPercent;
|
||||||
std::unique_ptr<juce::Slider> master;
|
std::unique_ptr<juce::Slider> master;
|
||||||
|
|
||||||
std::unique_ptr<juce::ComboBox> scaleBox;
|
std::unique_ptr<juce::ComboBox> scaleBox;
|
||||||
|
|
@ -76,7 +186,19 @@ private:
|
||||||
std::unique_ptr<juce::Label> statusBar;
|
std::unique_ptr<juce::Label> statusBar;
|
||||||
std::unique_ptr<juce::Label> statusGit;
|
std::unique_ptr<juce::Label> statusGit;
|
||||||
|
|
||||||
std::unique_ptr<juce::AudioProcessorValueTreeState::SliderAttachment> gainAttachments[numBands];
|
std::unique_ptr<juce::Label> colorHueLabel;
|
||||||
|
std::unique_ptr<juce::Label> colorSatLabel;
|
||||||
|
std::unique_ptr<juce::Label> colorBriLabel;
|
||||||
|
std::unique_ptr<juce::Label> waveLabel;
|
||||||
|
std::unique_ptr<juce::Label> waveSensLabel;
|
||||||
|
|
||||||
|
std::unique_ptr<juce::Slider> colorHueSlider;
|
||||||
|
std::unique_ptr<juce::Slider> colorSatSlider;
|
||||||
|
std::unique_ptr<juce::Slider> colorBriSlider;
|
||||||
|
std::unique_ptr<juce::Slider> waveSlider;
|
||||||
|
std::unique_ptr<juce::Slider> waveSensSlider;
|
||||||
|
|
||||||
|
std::unique_ptr<juce::AudioProcessorValueTreeState::SliderAttachment> levelAttachments[numBands];
|
||||||
std::unique_ptr<juce::AudioProcessorValueTreeState::SliderAttachment> qAttachments[numBands];
|
std::unique_ptr<juce::AudioProcessorValueTreeState::SliderAttachment> qAttachments[numBands];
|
||||||
std::unique_ptr<juce::AudioProcessorValueTreeState::SliderAttachment> freqAttachments[numBands];
|
std::unique_ptr<juce::AudioProcessorValueTreeState::SliderAttachment> freqAttachments[numBands];
|
||||||
std::unique_ptr<juce::AudioProcessorValueTreeState::SliderAttachment> masterAttachment;
|
std::unique_ptr<juce::AudioProcessorValueTreeState::SliderAttachment> masterAttachment;
|
||||||
|
|
@ -84,6 +206,7 @@ private:
|
||||||
juce::Array<float> spectrum;
|
juce::Array<float> spectrum;
|
||||||
juce::Array<float> freqBins;
|
juce::Array<float> freqBins;
|
||||||
juce::Array<float> eqCurve;
|
juce::Array<float> eqCurve;
|
||||||
|
juce::Rectangle<float> bandSections[numBands]; // knob-group outlines
|
||||||
std::atomic<float> scaling { 1.0f }; // for resized()
|
std::atomic<float> scaling { 1.0f }; // for resized()
|
||||||
|
|
||||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(GelykEQAudioProcessorEditor)
|
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(GelykEQAudioProcessorEditor)
|
||||||
|
|
|
||||||
|
|
@ -135,21 +135,13 @@ void GelykEQAudioProcessor::processBlock(juce::AudioBuffer<float>& buffer, juce:
|
||||||
analyser.push(chans, numChannels > 1 ? 2 : 1, numSamples);
|
analyser.push(chans, numChannels > 1 ? 2 : 1, numSamples);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Refresh band coefficients (cheap) from smoothed params each block.
|
// Refresh band coefficient targets (cheap) from smoothed params each block.
|
||||||
for (int i = 0; i < numBands; ++i)
|
for (int i = 0; i < numBands; ++i)
|
||||||
{
|
{
|
||||||
float gain = apvts.getRawParameterValue("gain" + juce::String(i))->load();
|
float gain = apvts.getRawParameterValue("gain" + juce::String(i))->load();
|
||||||
float q = apvts.getRawParameterValue("q" + juce::String(i))->load();
|
float q = apvts.getRawParameterValue("q" + juce::String(i))->load();
|
||||||
float freq = apvts.getRawParameterValue("freq" + juce::String(i))->load();
|
float freq = apvts.getRawParameterValue("freq" + juce::String(i))->load();
|
||||||
if (std::abs(gain - bands[i].gainDb) > 0.001f ||
|
bands[i].setTargets(gain, freq, q);
|
||||||
std::abs(q - bands[i].q) > 0.001f ||
|
|
||||||
std::abs(freq - bands[i].freq) > 0.5f)
|
|
||||||
{
|
|
||||||
bands[i].gainDb = gain;
|
|
||||||
bands[i].q = q;
|
|
||||||
bands[i].freq = freq;
|
|
||||||
bands[i].prepare(currentSampleRate, numSamples);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
juce::dsp::AudioBlock<float> block(buffer);
|
juce::dsp::AudioBlock<float> block(buffer);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue