This commit is contained in:
Armin 2026-08-10 12:55:02 +02:00
commit 6c0d072465
19 changed files with 2212 additions and 0 deletions

218
Source/HorizontEngine.cpp Normal file
View file

@ -0,0 +1,218 @@
#include "HorizontEngine.h"
HorizontEngine::HorizontEngine()
{
prepare (44100.0);
}
void HorizontEngine::prepare (double sr)
{
sampleRate = sr;
const float srScale = (float) (sr / 44100.0);
for (int i = 0; i < 8; ++i)
{
baseLen[(size_t) i] = baseLen441[(size_t) i] * srScale;
baseLenR[(size_t) i] = (baseLen441[(size_t) i] + rightOffset[(size_t) i]) * srScale;
const int maxL = (int) std::ceil (baseLen441[(size_t) i] * 2.0f * srScale) + 8;
combL[(size_t) i].prepare (maxL);
combR[(size_t) i].prepare (maxL);
}
diffL1.prepare ((int) (221.0f * srScale));
diffL2.prepare ((int) (113.0f * srScale));
diffR1.prepare ((int) (229.0f * srScale));
diffR2.prepare ((int) (107.0f * srScale));
shiftL.prepare (sr);
shiftR.prepare (sr);
lowCutG.reset (sr, 0.04);
setLowCut (lowCutHz);
lowCutG.setCurrentAndTargetValue (lowCutG.getTargetValue());
highCutG.reset (sr, 0.04);
setHighCut (highCutHz);
highCutG.setCurrentAndTargetValue (highCutG.getTargetValue());
decayS.reset (sr, 0.06);
setDecay (decaySec);
decayS.setCurrentAndTargetValue (decayS.getTargetValue());
sizeS.reset (sr, 0.06);
setSize (sizeVal);
sizeS.setCurrentAndTargetValue (sizeS.getTargetValue());
diffS.reset (sr, 0.05);
setDiffusion (diffusionVal);
diffS.setCurrentAndTargetValue (diffS.getTargetValue());
dampS.reset (sr, 0.05);
setBrightness (brightnessVal);
dampS.setCurrentAndTargetValue (dampS.getTargetValue());
feedbackS.reset (sr, 0.06);
setFeedback (feedbackVal);
feedbackS.setCurrentAndTargetValue (feedbackS.getTargetValue());
ratioS.reset (sr, 0.08);
setPitch (pitchVal);
ratioS.setCurrentAndTargetValue (ratioS.getTargetValue());
wetS.reset (sr, 0.03);
setDryWet (dryWetVal);
wetS.setCurrentAndTargetValue (wetS.getTargetValue());
reset();
}
void HorizontEngine::reset()
{
for (auto& c : combL)
c.reset();
for (auto& c : combR)
c.reset();
diffL1.reset();
diffL2.reset();
diffR1.reset();
diffR2.reset();
hpL.reset();
lpL.reset();
hpR.reset();
lpR.reset();
dcL.reset();
dcR.reset();
shiftL.reset();
shiftR.reset();
lastWetL = 0.0f;
lastWetR = 0.0f;
}
void HorizontEngine::setLowCut (float hz)
{
lowCutHz = hz;
hz = juce::jmin (hz, (float) (sampleRate * 0.48));
lowCutG.setTargetValue (std::tan (juce::MathConstants<float>::pi * hz / (float) sampleRate));
}
void HorizontEngine::setHighCut (float hz)
{
highCutHz = hz;
hz = juce::jmin (hz, (float) (sampleRate * 0.48));
highCutG.setTargetValue (std::tan (juce::MathConstants<float>::pi * hz / (float) sampleRate));
}
void HorizontEngine::setDecay (float seconds)
{
decaySec = seconds;
decayS.setTargetValue (juce::jmax (0.1f, seconds));
}
void HorizontEngine::setSize (float size)
{
sizeVal = size;
sizeS.setTargetValue (juce::jlimit (0.2f, 2.5f, size));
}
void HorizontEngine::setDiffusion (float diffusion)
{
diffusionVal = diffusion;
diffS.setTargetValue (0.72f * juce::jlimit (0.0f, 1.0f, diffusion));
}
void HorizontEngine::setBrightness (float brightness)
{
brightnessVal = brightness;
const float b = juce::jlimit (0.0f, 1.0f, brightness);
float fc = 500.0f + 19500.0f * std::pow (b, 1.5f);
fc = juce::jmin (fc, (float) (sampleRate * 0.48f));
dampS.setTargetValue (1.0f - std::exp (-2.0f * juce::MathConstants<float>::pi * fc / (float) sampleRate));
}
void HorizontEngine::setFeedback (float feedback)
{
feedbackVal = feedback;
feedbackS.setTargetValue (juce::jlimit (0.0f, 0.99f, feedback));
}
void HorizontEngine::setPitch (float semitones)
{
pitchVal = semitones;
ratioS.setTargetValue (std::pow (2.0f, semitones / 12.0f));
}
void HorizontEngine::setDryWet (float dryWet)
{
dryWetVal = dryWet;
wetS.setTargetValue (juce::jlimit (0.0f, 1.0f, dryWet));
}
void HorizontEngine::processSample (float inL, float inR, float& outL, float& outR)
{
const float sizeCur = sizeS.getNextValue();
const float rtCur = decayS.getNextValue();
const float dampC = dampS.getNextValue();
const float diffG = diffS.getNextValue();
const float fbGain = feedbackS.getNextValue();
const float ratio = ratioS.getNextValue();
const float wetVal = wetS.getNextValue();
const float gLow = lowCutG.getNextValue();
const float gHigh = highCutG.getNextValue();
hpL.setG (gLow);
hpL.process (inL);
lpL.setG (gHigh);
lpL.process (hpL.high);
hpR.setG (gLow);
hpR.process (inR);
lpR.setG (gHigh);
lpR.process (hpR.high);
const float fl = lpL.low;
const float fr = lpR.low;
const float invRt = 1.0f / (rtCur * sizeCur * (float) sampleRate);
const float loopInvRt = invRt * (1.0f - 0.7f * fbGain);
const float gMax = std::exp (-6.907755f * baseLen[0] * sizeCur * loopInvRt);
const float fbL = fl + fbGain * 0.5f * (1.0f - gMax) * shiftL.process (lastWetL, ratio);
const float fbR = fr + fbGain * 0.5f * (1.0f - gMax) * shiftR.process (lastWetR, ratio);
const float dL = diffL2.process (diffL1.process (fbL, diffG), diffG);
const float dR = diffR2.process (diffR1.process (fbR, diffG), diffG);
float wetL = 0.0f;
float wetR = 0.0f;
for (int i = 0; i < 8; ++i)
{
const float lenL = baseLen[(size_t) i] * sizeCur;
const float gainL = std::exp (-6.907755f * lenL * loopInvRt);
wetL += 0.25f * combL[(size_t) i].process (dL, lenL, dampC, gainL);
const float lenR = baseLenR[(size_t) i] * sizeCur;
const float gainR = std::exp (-6.907755f * lenR * loopInvRt);
wetR += 0.25f * combR[(size_t) i].process (dR, lenR, dampC, gainR);
}
wetL = dcL.process (wetL);
wetR = dcR.process (wetR);
wetL = std::tanh (wetL);
wetR = std::tanh (wetR);
lastWetL = wetL;
lastWetR = wetR;
outL = inL * (1.0f - wetVal) + wetL * wetVal;
outR = inR * (1.0f - wetVal) + wetR * wetVal;
}

218
Source/HorizontEngine.h Normal file
View file

@ -0,0 +1,218 @@
#pragma once
#include <JuceHeader.h>
#include <array>
#include "PitchShifter.h"
class HorizontEngine
{
public:
HorizontEngine();
void prepare (double sampleRate);
void reset();
void setLowCut (float hz);
void setHighCut (float hz);
void setDecay (float seconds);
void setSize (float size);
void setDiffusion (float diffusion);
void setBrightness (float brightness);
void setFeedback (float feedback);
void setPitch (float semitones);
void setDryWet (float dryWet);
void processSample (float inL, float inR, float& outL, float& outR);
float getLastWetL() const noexcept { return lastWetL; }
float getLastWetR() const noexcept { return lastWetR; }
private:
struct SVF
{
void reset()
{
ic1 = 0.0f;
ic2 = 0.0f;
g = 0.0f;
}
void setG (float gIn)
{
g = gIn;
a1 = 1.0f / (1.0f + g * (g + k));
a2 = g * a1;
}
void process (float x)
{
const float v3 = x - ic2;
const float v1 = a1 * ic1 + a2 * v3;
const float v2 = ic2 + g * v1;
ic1 = 2.0f * v1 - ic1;
ic2 = 2.0f * v2 - ic2;
low = v2;
high = x - k * v1 - v2;
}
float g = 0.0f;
float k = 1.41421356f;
float a1 = 0.0f;
float a2 = 0.0f;
float ic1 = 0.0f;
float ic2 = 0.0f;
float low = 0.0f;
float high = 0.0f;
};
struct Allpass
{
void prepare (int length)
{
len = length;
xb.assign ((size_t) len + 1, 0.0f);
yb.assign ((size_t) len + 1, 0.0f);
writePos = 0;
}
void reset()
{
std::fill (xb.begin(), xb.end(), 0.0f);
std::fill (yb.begin(), yb.end(), 0.0f);
writePos = 0;
}
float process (float x, float g)
{
int rp = writePos - len;
if (rp < 0)
rp += len + 1;
const float xd = xb[(size_t) rp];
const float yd = yb[(size_t) rp];
xb[(size_t) writePos] = x;
const float y = -g * x + xd + g * yd;
yb[(size_t) writePos] = y;
++writePos;
if (writePos > len)
writePos = 0;
return y;
}
std::vector<float> xb;
std::vector<float> yb;
int len = 0;
int writePos = 0;
};
struct Comb
{
void prepare (int maxLength)
{
buffer.assign ((size_t) maxLength + 4, 0.0f);
maxLen = maxLength;
writePos = 0;
dampState = 0.0f;
}
void reset()
{
std::fill (buffer.begin(), buffer.end(), 0.0f);
dampState = 0.0f;
writePos = 0;
}
float process (float input, float delayLen, float dampCoef, float gain)
{
const float rp = (float) writePos - delayLen;
int i0 = (int) std::floor (rp);
const float frac = rp - (float) i0;
i0 %= maxLen;
if (i0 < 0)
i0 += maxLen;
int i1 = i0 + 1;
if (i1 >= maxLen)
i1 -= maxLen;
const float y = buffer[(size_t) i0] + frac * (buffer[(size_t) i1] - buffer[(size_t) i0]);
buffer[(size_t) writePos] = input + dampState * gain;
dampState += dampCoef * (y - dampState);
++writePos;
if (writePos >= maxLen)
writePos = 0;
return dampState;
}
std::vector<float> buffer;
int maxLen = 0;
int writePos = 0;
float dampState = 0.0f;
};
struct DCBlock
{
void reset()
{
xm = 0.0f;
ym = 0.0f;
}
float process (float x)
{
const float y = x - xm + 0.999f * ym;
xm = x;
ym = y;
return y;
}
float xm = 0.0f;
float ym = 0.0f;
};
static constexpr std::array<float, 8> baseLen441 = { 1553.f, 1787.f, 2017.f, 2237.f, 2437.f, 2657.f, 2879.f, 3073.f };
static constexpr std::array<float, 8> rightOffset = { 79.f, 67.f, 53.f, 41.f, 29.f, 23.f, 13.f, 7.f };
double sampleRate = 44100.0;
juce::SmoothedValue<float, juce::ValueSmoothingTypes::Linear> lowCutG;
juce::SmoothedValue<float, juce::ValueSmoothingTypes::Linear> highCutG;
juce::SmoothedValue<float, juce::ValueSmoothingTypes::Linear> decayS;
juce::SmoothedValue<float, juce::ValueSmoothingTypes::Linear> sizeS;
juce::SmoothedValue<float, juce::ValueSmoothingTypes::Linear> diffS;
juce::SmoothedValue<float, juce::ValueSmoothingTypes::Linear> dampS;
juce::SmoothedValue<float, juce::ValueSmoothingTypes::Linear> feedbackS;
juce::SmoothedValue<float, juce::ValueSmoothingTypes::Linear> ratioS;
juce::SmoothedValue<float, juce::ValueSmoothingTypes::Linear> wetS;
SVF hpL, lpL, hpR, lpR;
DCBlock dcL, dcR;
Allpass diffL1, diffL2, diffR1, diffR2;
PitchShifter shiftL, shiftR;
std::array<Comb, 8> combL;
std::array<Comb, 8> combR;
std::array<float, 8> baseLen;
std::array<float, 8> baseLenR;
float lowCutHz = 40.0f;
float highCutHz = 14000.0f;
float decaySec = 3.5f;
float sizeVal = 1.0f;
float diffusionVal = 0.6f;
float brightnessVal = 0.75f;
float feedbackVal = 0.6f;
float pitchVal = 0.0f;
float dryWetVal = 0.35f;
float lastWetL = 0.0f;
float lastWetR = 0.0f;
};

View file

@ -0,0 +1,201 @@
#include "HorizontLookAndFeel.h"
namespace
{
const juce::Colour tealAccent (0xFF3CE0C8);
const juce::Colour textMain (0xFFE8F6F4);
const juce::Colour textDim (0xFFB8CCCC);
const juce::Colour charcoal (0xFF0B0E10);
}
HorizontLookAndFeel::HorizontLookAndFeel()
{
setColour (juce::ComboBox::outlineColourId, juce::Colour (0xFF2A3A3D));
setColour (juce::ComboBox::textColourId, textMain);
setColour (juce::ComboBox::arrowColourId, tealAccent);
setColour (juce::PopupMenu::backgroundColourId, juce::Colour (0xFF141B1E));
setColour (juce::PopupMenu::highlightedBackgroundColourId, juce::Colour (0xFF1E3A3C));
setColour (juce::PopupMenu::textColourId, textMain);
setColour (juce::TooltipWindow::textColourId, textMain);
setColour (juce::TooltipWindow::backgroundColourId, juce::Colour (0xFF122A2B));
setColour (juce::TooltipWindow::outlineColourId, tealAccent);
}
void HorizontLookAndFeel::drawRotarySlider (juce::Graphics& g, int x, int y, int width, int height,
float sliderPos, float rotaryStartAngle, float rotaryEndAngle,
juce::Slider&)
{
const auto bounds = juce::Rectangle<float> ((float) x, (float) y, (float) width, (float) height);
const auto centre = bounds.getCentre();
const float radius = jmin (bounds.getWidth(), bounds.getHeight()) * 0.5f;
const float angle = rotaryStartAngle + sliderPos * (rotaryEndAngle - rotaryStartAngle);
{
auto shadow = bounds.translated (0.0f, radius * 0.10f);
auto grad = juce::ColourGradient (juce::Colour (0x80000000),
centre.withY (centre.getY() + radius * 0.45f),
juce::Colour (0x00000000),
centre.withY (centre.getY() + radius * 0.45f).translated (radius * 1.1f, 0.0f),
true);
grad.addColour (0.55f, juce::Colour (0x30000000));
g.setGradientFill (grad);
g.fillEllipse (shadow);
}
{
auto disc = bounds.reduced (radius * 0.05f);
auto grad = juce::ColourGradient (juce::Colour (0xFFA9BBC2),
centre.translated (-radius * 0.35f, -radius * 0.45f),
juce::Colour (0xFF171B1E),
centre.translated (-radius * 0.35f, -radius * 0.45f).translated (radius * 1.25f, 0.0f),
true);
grad.addColour (0.35f, juce::Colour (0xFF6E7C84));
grad.addColour (0.75f, juce::Colour (0xFF3A4247));
g.setGradientFill (grad);
g.fillEllipse (disc);
}
{
auto rim = bounds.reduced (radius * 0.03f);
g.setColour (charcoal);
g.drawEllipse (rim, jmax (1.0f, radius * 0.05f));
}
{
auto face = bounds.reduced (radius * 0.24f);
auto grad = juce::ColourGradient (juce::Colour (0xFF333C43),
centre.translated (0.0f, -radius * 0.10f),
juce::Colour (0xFF101316),
centre.translated (0.0f, -radius * 0.10f).translated (radius * 0.85f, 0.0f),
true);
grad.addColour (0.55f, juce::Colour (0xFF20262B));
g.setGradientFill (grad);
g.fillEllipse (face);
g.setColour (juce::Colour (0x383CE0C8));
g.drawEllipse (face, 1.0f);
}
{
const float tipDist = radius * 0.72f;
const float baseDist = radius * 0.34f;
const float halfWidth = radius * 0.085f;
const auto tip = centre.getPointOnCircumference (tipDist, angle);
const auto base = centre.getPointOnCircumference (baseDist, angle);
const float perp = angle + juce::MathConstants<float>::halfPi;
const auto b1 = base.translated (std::cos (perp) * halfWidth, std::sin (perp) * halfWidth);
const auto b2 = base.translated (-std::cos (perp) * halfWidth, -std::sin (perp) * halfWidth);
juce::Path notch;
notch.addTriangle (b1, b2, tip);
notch.closeSubPath();
g.setColour (tealAccent);
g.fillPath (notch);
g.setColour (tealAccent);
g.strokePath (notch, juce::PathStrokeType (1.0f));
}
{
auto hl = juce::Rectangle<float> (radius * 0.9f, radius * 0.5f)
.withCentre (centre.translated (-radius * 0.28f, -radius * 0.32f));
auto grad = juce::ColourGradient (juce::Colour (0x30FFFFFF), hl.getCentre(),
juce::Colour (0x00FFFFFF), hl.getCentre().translated (radius * 0.6f, 0.0f),
true);
g.setGradientFill (grad);
g.fillEllipse (hl);
}
{
auto dot = juce::Rectangle<float> (radius * 0.16f, radius * 0.16f).withCentre (centre);
auto grad = juce::ColourGradient (juce::Colour (0xFF5A666D),
centre.translated (-radius * 0.03f, -radius * 0.03f),
juce::Colour (0xFF171B1E),
centre.translated (-radius * 0.03f, -radius * 0.03f).translated (radius * 0.14f, 0.0f),
true);
g.setGradientFill (grad);
g.fillEllipse (dot);
}
}
void HorizontLookAndFeel::drawComboBox (juce::Graphics& g, int width, int height, bool,
int buttonX, int buttonY, int buttonW, int buttonH, juce::ComboBox&)
{
auto bounds = juce::Rectangle<float> (0, 0, (float) width, (float) height);
auto grad = juce::ColourGradient (juce::Colour (0xFF22292E), { 0.0f, 0.0f },
juce::Colour (0xFF0E1215), { 0.0f, (float) height },
false);
g.setGradientFill (grad);
g.fillRoundedRectangle (bounds, 4.0f);
g.setColour (juce::Colour (0xFF3A4A4D));
g.drawRoundedRectangle (bounds.reduced (0.5f), 4.0f, 1.0f);
auto arrowArea = juce::Rectangle<float> ((float) buttonX, (float) buttonY, (float) buttonW, (float) buttonH);
juce::Path arrow;
arrow.startNewSubPath (arrowArea.getX() + arrowArea.getWidth() * 0.35f, arrowArea.getCentreY() - 2.0f);
arrow.lineTo (arrowArea.getCentreX(), arrowArea.getCentreY() + 3.0f);
arrow.lineTo (arrowArea.getX() + arrowArea.getWidth() * 0.65f, arrowArea.getCentreY() - 2.0f);
g.setColour (tealAccent);
g.strokePath (arrow, juce::PathStrokeType (1.6f,
juce::PathStrokeType::JointStyle::beveled,
juce::PathStrokeType::EndCapStyle::rounded));
}
void HorizontLookAndFeel::drawPopupMenuBackground (juce::Graphics& g, int width, int height)
{
g.setColour (juce::Colour (0xF0141B1E));
g.fillRoundedRectangle (0.0f, 0.0f, (float) width, (float) height, 6.0f);
g.setColour (juce::Colour (0xFF3A4A4D));
g.drawRoundedRectangle (0.5f, 0.5f, (float) width - 1.0f, (float) height - 1.0f, 6.0f, 1.0f);
}
void HorizontLookAndFeel::drawPopupMenuItem (juce::Graphics& g, const juce::Rectangle<int>& area,
bool isSeparator, bool isActive, bool isHighlighted, bool isTicked,
bool, const juce::String& text, const juce::String&,
const juce::Drawable*, const juce::Colour* textColour)
{
if (isSeparator)
{
auto r = area.reduced (5, 0);
g.setColour (juce::Colour (0x334A5A5D));
g.fillRect (r.removeFromBottom (1));
return;
}
if (isHighlighted && isActive)
{
g.setColour (juce::Colour (0xFF1E3A3C));
g.fillRoundedRectangle (area.toFloat().reduced (2.0f), 4.0f);
}
auto textArea = area.reduced (12, 0);
if (isTicked)
textArea.removeFromLeft (12);
g.setColour (isActive ? (textColour != nullptr ? *textColour : textMain) : textDim);
g.setFont (juce::Font (juce::FontOptions (13.0f)));
g.drawText (text, textArea, juce::Justification::centredLeft, false);
if (isTicked)
{
auto dot = juce::Rectangle<float> (4.0f, 4.0f)
.withCentre (juce::Point<float> ((float) (area.getX() + 10), (float) area.getCentreY()));
g.setColour (tealAccent);
g.fillEllipse (dot);
}
}
void HorizontLookAndFeel::drawTooltip (juce::Graphics& g, const juce::String& text, int width, int height)
{
g.setColour (juce::Colour (0xF0122A2B));
g.fillRoundedRectangle (0.0f, 0.0f, (float) width, (float) height, 4.0f);
g.setColour (tealAccent);
g.drawRoundedRectangle (0.5f, 0.5f, (float) width - 1.0f, (float) height - 1.0f, 4.0f, 1.0f);
g.setColour (textMain);
g.setFont (juce::Font (juce::FontOptions (12.0f)));
g.drawText (text, juce::Rectangle<int> (6, 0, width - 12, height), juce::Justification::centred, true);
}

View file

@ -0,0 +1,25 @@
#pragma once
#include <JuceHeader.h>
class HorizontLookAndFeel : public juce::LookAndFeel_V4
{
public:
HorizontLookAndFeel();
void drawRotarySlider (juce::Graphics&, int x, int y, int width, int height,
float sliderPos, float rotaryStartAngle, float rotaryEndAngle,
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 drawPopupMenuBackground (juce::Graphics&, int width, int height) override;
void drawPopupMenuItem (juce::Graphics&, const juce::Rectangle<int>& area,
bool isSeparator, bool isActive, bool isHighlighted, bool isTicked,
bool hasSubMenu, const juce::String& text, const juce::String& shortcutKeyText,
const juce::Drawable* icon, const juce::Colour* textColour) override;
void drawTooltip (juce::Graphics&, const juce::String& text, int width, int height) override;
};

81
Source/Knob.cpp Normal file
View file

@ -0,0 +1,81 @@
#include "Knob.h"
Knob::Knob (const juce::String& title,
juce::AudioProcessorValueTreeState& apvts,
const juce::String& paramID,
float defaultValue,
std::function<juce::String (float)> formatFn,
const juce::String& tooltip)
: format (std::move (formatFn))
{
nameLabel.setText (title, juce::dontSendNotification);
nameLabel.setJustificationType (juce::Justification::centred);
nameLabel.setFont (juce::Font (juce::FontOptions (11.0f, juce::Font::bold)));
nameLabel.setColour (juce::Label::textColourId, juce::Colour (0xFFB8CCCC));
nameLabel.setInterceptsMouseClicks (false, false);
addAndMakeVisible (nameLabel);
valueLabel.setJustificationType (juce::Justification::centred);
valueLabel.setFont (juce::Font (juce::FontOptions (11.0f, juce::Font::bold)));
valueLabel.setColour (juce::Label::textColourId, juce::Colour (0xFF3CE0C8));
valueLabel.setInterceptsMouseClicks (false, false);
addAndMakeVisible (valueLabel);
slider.setSliderStyle (juce::Slider::RotaryHorizontalVerticalDrag);
slider.setTextBoxStyle (juce::Slider::NoTextBox, true, 0, 0);
slider.setDoubleClickReturnValue (true, defaultValue);
slider.setWantsKeyboardFocus (true);
slider.setScrollWheelEnabled (true);
slider.setTooltip (tooltip);
slider.onValueChange = [this] { updateValueLabel(); };
addAndMakeVisible (slider);
attachment = std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment> (apvts, paramID, slider);
updateValueLabel();
}
void Knob::setScaleFactor (float scale)
{
scaleFactor = scale;
resized();
}
void Knob::updateValueLabel()
{
valueLabel.setText (format (slider.getValue()), juce::dontSendNotification);
}
void Knob::paint (juce::Graphics& g)
{
auto b = slider.getBounds().toFloat();
auto shadow = b.translated (0.0f, b.getHeight() * 0.05f).expanded (b.getWidth() * 0.05f);
auto shadowCentre = shadow.getCentre().withY (shadow.getCentre().getY() + b.getHeight() * 0.4f);
auto grad = juce::ColourGradient (juce::Colour (0x60000000), shadowCentre,
juce::Colour (0x00000000), shadowCentre.translated (shadow.getWidth() * 0.7f, 0.0f),
true);
grad.addColour (0.55f, juce::Colour (0x20000000));
g.setGradientFill (grad);
g.fillEllipse (shadow);
}
void Knob::resized()
{
auto bounds = getLocalBounds();
const float s = scaleFactor;
const int nameH = juce::roundToInt (18.0f * s);
const int valueH = juce::roundToInt (16.0f * s);
const int maxKnobSize = juce::roundToInt (84.0f * s);
auto nameArea = bounds.removeFromTop (nameH);
auto valueArea = bounds.removeFromBottom (valueH);
nameLabel.setBounds (nameArea);
valueLabel.setBounds (valueArea);
nameLabel.setFont (juce::Font (juce::FontOptions (juce::roundToInt (11.0f * s), juce::Font::bold)));
valueLabel.setFont (juce::Font (juce::FontOptions (juce::roundToInt (11.0f * s), juce::Font::bold)));
const int knobSize = jmin (bounds.getWidth(), bounds.getHeight(), maxKnobSize);
slider.setBounds (bounds.getCentreX() - knobSize / 2, bounds.getCentreY() - knobSize / 2, knobSize, knobSize);
}

31
Source/Knob.h Normal file
View file

@ -0,0 +1,31 @@
#pragma once
#include <JuceHeader.h>
class Knob : public juce::Component
{
public:
Knob (const juce::String& title,
juce::AudioProcessorValueTreeState& apvts,
const juce::String& paramID,
float defaultValue,
std::function<juce::String (float)> format,
const juce::String& tooltip);
void paint (juce::Graphics&) override;
void resized() override;
void setScaleFactor (float scale);
private:
void updateValueLabel();
std::function<juce::String (float)> format;
juce::Label nameLabel;
juce::Label valueLabel;
juce::Slider slider;
std::unique_ptr<juce::AudioProcessorValueTreeState::SliderAttachment> attachment;
float scaleFactor = 1.0f;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Knob)
};

80
Source/PitchShifter.cpp Normal file
View file

@ -0,0 +1,80 @@
#include "PitchShifter.h"
#include <JuceHeader.h>
void PitchShifter::prepare (double sampleRate)
{
window = (float) std::max (256.0, sampleRate * 0.035);
baseDelay = window;
bufferSize = (int) (baseDelay + window + 16.0f);
buffer.assign ((size_t) bufferSize, 0.0f);
writePos = 0;
o = 0.0f;
}
void PitchShifter::reset()
{
std::fill (buffer.begin(), buffer.end(), 0.0f);
writePos = 0;
o = 0.0f;
}
float PitchShifter::at (int index) const
{
index %= bufferSize;
if (index < 0)
index += bufferSize;
return buffer[(size_t) index];
}
float PitchShifter::readCubic (float pos) const
{
const int i = (int) std::floor (pos);
const float frac = pos - (float) i;
const float x0 = at (i - 1);
const float x1 = at (i);
const float x2 = at (i + 1);
const float x3 = at (i + 2);
const float a0 = -0.5f * x0 + 1.5f * x1 - 1.5f * x2 + 0.5f * x3;
const float a1 = x0 - 2.5f * x1 + 2.0f * x2 - 0.5f * x3;
const float a2 = -0.5f * x0 + 0.5f * x2;
const float a3 = x1;
return ((a0 * frac + a1) * frac + a2) * frac + a3;
}
float PitchShifter::process (float input, float ratio)
{
buffer[(size_t) writePos] = input;
if (std::fabs (ratio - 1.0f) < 1e-4f)
return input;
const float theta = juce::MathConstants<float>::pi * o / window;
const float g1 = std::sin (theta) * std::sin (theta);
const float g2 = std::cos (theta) * std::cos (theta);
float o2 = o + window * 0.5f;
if (o2 >= window)
o2 -= window;
const float pos1 = (float) writePos - (baseDelay - o);
const float pos2 = (float) writePos - (baseDelay - o2);
const float result = g1 * readCubic (pos1) + g2 * readCubic (pos2);
o += (ratio - 1.0f);
if (o >= window)
o -= window;
else if (o < 0.0f)
o += window;
++writePos;
if (writePos >= bufferSize)
writePos = 0;
return result;
}

23
Source/PitchShifter.h Normal file
View file

@ -0,0 +1,23 @@
#pragma once
#include <vector>
class PitchShifter
{
public:
void prepare (double sampleRate);
void reset();
float process (float input, float ratio);
private:
float at (int index) const;
float readCubic (float pos) const;
std::vector<float> buffer;
int bufferSize = 0;
int writePos = 0;
float o = 0.0f;
float baseDelay = 0.0f;
float window = 0.0f;
};

349
Source/PluginEditor.cpp Normal file
View file

@ -0,0 +1,349 @@
#include "PluginEditor.h"
#include <cmath>
#include <limits>
namespace
{
juce::String formatHz (float v)
{
return juce::String (juce::roundToInt (v)) + " Hz";
}
juce::String formatSec (float v)
{
return juce::String (v, 1) + " s";
}
juce::String formatSize (float v)
{
return juce::String (v, 2) + "x";
}
juce::String formatPct (float v)
{
return juce::String (juce::roundToInt (v * 100.0f)) + " %";
}
juce::String formatSemis (float v)
{
const int semis = juce::roundToInt (v);
if (semis > 0)
return "+" + juce::String (semis) + " st";
if (semis < 0)
return juce::String (semis) + " st";
return "0 st";
}
juce::StringArray presetNames()
{
juce::StringArray names;
for (const auto& p : getPresets())
names.add (p.name);
return names;
}
constexpr float scalePresets[] = { 0.75f, 0.9f, 1.0f, 1.25f, 1.5f, 1.75f, 2.0f };
constexpr int numScalePresets = (int) (sizeof (scalePresets) / sizeof (scalePresets[0]));
juce::StringArray scaleNames()
{
juce::StringArray names;
for (float v : scalePresets)
names.add (juce::String (juce::roundToInt (v * 100.0f)) + " %");
return names;
}
juce::String buildFooterText()
{
#ifndef HORIZONT_VERSION
#define HORIZONT_VERSION "0.0.0"
#endif
#ifndef HORIZONT_GIT_REVISION
#define HORIZONT_GIT_REVISION "unknown"
#endif
#ifndef HORIZONT_BUILD_TIMESTAMP
#define HORIZONT_BUILD_TIMESTAMP "unknown"
#endif
juce::String text ("HORIZONT v" + juce::String (HORIZONT_VERSION));
text << " · git " << HORIZONT_GIT_REVISION;
text << " · build " << HORIZONT_BUILD_TIMESTAMP;
return text;
}
}
HorizontAudioProcessorEditor::HorizontAudioProcessorEditor (HorizontAudioProcessor& p,
juce::AudioProcessorValueTreeState& s)
: AudioProcessorEditor (&p),
processor (p),
apvts (s),
display (p)
{
lookAndFeel = std::make_unique<HorizontLookAndFeel>();
setLookAndFeel (lookAndFeel.get());
titleLabel.setText ("HORIZONT", juce::dontSendNotification);
titleLabel.setFont (juce::Font (juce::FontOptions (22.0f, juce::Font::bold)));
titleLabel.setColour (juce::Label::textColourId, juce::Colour (0xFFE8F6F4));
addAndMakeVisible (titleLabel);
subtitleLabel.setText ("Algorithmic Spatial Processor", juce::dontSendNotification);
subtitleLabel.setFont (juce::Font (juce::FontOptions (11.0f)));
subtitleLabel.setColour (juce::Label::textColourId, juce::Colour (0xFF5E8688));
addAndMakeVisible (subtitleLabel);
presetBox.addItemList (presetNames(), 1);
presetBox.setSelectedItemIndex (0);
presetBox.addListener (this);
presetBox.setTooltip ("Recall a factory preset");
addAndMakeVisible (presetBox);
scaleBox.addItemList (scaleNames(), 1);
scaleBox.setSelectedItemIndex (2);
scaleBox.addListener (this);
scaleBox.setTooltip ("Set the UI scale");
addAndMakeVisible (scaleBox);
footerLabel.setText (buildFooterText(), juce::dontSendNotification);
footerLabel.setJustificationType (juce::Justification::centred);
footerLabel.setInterceptsMouseClicks (false, false);
addAndMakeVisible (footerLabel);
addAndMakeVisible (display);
auto makeKnob = [this] (const juce::String& title, const juce::String& id,
const juce::String& tip, std::function<juce::String (float)> fmt,
float defVal) -> std::unique_ptr<Knob>
{
return std::make_unique<Knob> (title, apvts, id, defVal, fmt, tip);
};
knobs.push_back (makeKnob ("LOW CUT", "lowCut", "Input high-pass filter (20 Hz - 500 Hz)", formatHz, 40.0f));
knobs.push_back (makeKnob ("HIGH CUT", "highCut", "Input low-pass filter (800 Hz - 20 kHz)", formatHz, 14000.0f));
knobs.push_back (makeKnob ("DECAY", "decay", "Length of the reverb tail (RT60)", formatSec, 3.5f));
knobs.push_back (makeKnob ("SIZE", "size", "Room size scaling", formatSize, 1.0f));
knobs.push_back (makeKnob ("DIFFUSION", "diffusion", "Echo diffusion and smearing", formatPct, 0.6f));
knobs.push_back (makeKnob ("BRIGHTNESS", "brightness", "High-frequency damping of the tail", formatPct, 0.75f));
knobs.push_back (makeKnob ("FEEDBACK", "feedback", "Recirculation through the pitch shifter", formatPct, 0.6f));
knobs.push_back (makeKnob ("PITCH", "pitch", "Pitch shift applied per feedback pass", formatSemis, 0.0f));
knobs.push_back (makeKnob ("DRY / WET", "dryWet", "Dry / wet mix", formatPct, 0.35f));
for (auto& k : knobs)
addAndMakeVisible (k.get());
setSize (juce::roundToInt (baseWidth), juce::roundToInt (baseHeight));
setResizeLimits (juce::roundToInt (baseWidth * 0.75f), juce::roundToInt (baseHeight * 0.75f),
juce::roundToInt (baseWidth * 2.0f), juce::roundToInt (baseHeight * 2.0f));
}
HorizontAudioProcessorEditor::~HorizontAudioProcessorEditor()
{
}
void HorizontAudioProcessorEditor::setScaleFactor (float newScale)
{
newScale = juce::jlimit (0.6f, 2.0f, newScale);
setSize (juce::roundToInt (baseWidth * newScale),
juce::roundToInt (baseHeight * newScale));
}
void HorizontAudioProcessorEditor::updateScaleFactor()
{
const float newScale = juce::jmin ((float) getWidth() / baseWidth,
(float) getHeight() / baseHeight);
if (juce::approximatelyEqual (scaleFactor, newScale))
return;
scaleFactor = newScale;
for (auto& k : knobs)
k->setScaleFactor (scaleFactor);
display.setScaleFactor (scaleFactor);
}
void HorizontAudioProcessorEditor::comboBoxChanged (juce::ComboBox* box)
{
if (box == &presetBox)
{
const int idx = presetBox.getSelectedItemIndex();
const auto& presets = processor.getPresets();
if (idx >= 0 && idx < (int) presets.size())
processor.applyPreset (presets[(size_t) idx]);
}
else if (box == &scaleBox)
{
const int idx = scaleBox.getSelectedItemIndex();
if (idx >= 0 && idx < numScalePresets)
setScaleFactor (scalePresets[idx]);
}
}
void HorizontAudioProcessorEditor::updateScaleSelection()
{
int best = 0;
float bestDiff = std::numeric_limits<float>::max();
for (int i = 0; i < numScalePresets; ++i)
{
const float diff = std::abs (scaleFactor - scalePresets[i]);
if (diff < bestDiff)
{
bestDiff = diff;
best = i;
}
}
scaleBox.setSelectedItemIndex (best, juce::dontSendNotification);
}
void HorizontAudioProcessorEditor::layoutKnobRow (juce::Rectangle<int> area, int startIndex, int count)
{
juce::FlexBox fb;
fb.flexDirection = juce::FlexBox::Direction::row;
fb.justifyContent = juce::FlexBox::JustifyContent::center;
fb.alignItems = juce::FlexBox::AlignItems::stretch;
const float minW = 80.0f * scaleFactor;
const float minH = 96.0f * scaleFactor;
for (int i = startIndex; i < startIndex + count; ++i)
fb.items.add (juce::FlexItem (*knobs[(size_t) i]).withMinWidth (minW).withMinHeight (minH).withFlex (1.0f));
fb.performLayout (area);
}
void HorizontAudioProcessorEditor::mouseWheelMove (const juce::MouseEvent&, const juce::MouseWheelDetails& wheel)
{
if (wheel.deltaY != 0.0f)
{
const float zoomStep = 0.1f;
setScaleFactor (scaleFactor - wheel.deltaY * zoomStep);
}
}
void HorizontAudioProcessorEditor::resized()
{
updateScaleFactor();
updateScaleSelection();
auto area = getLocalBounds();
const float s = scaleFactor;
const int headerH = juce::roundToInt (64.0f * s);
const int footerH = juce::roundToInt (26.0f * s);
const int presetBoxW = juce::roundToInt (180.0f * s);
const int scaleBoxW = juce::roundToInt (72.0f * s);
const int boxGap = juce::roundToInt (10.0f * s);
const int presetBoxMargin = juce::roundToInt (20.0f * s);
const int titleH = juce::roundToInt (32.0f * s);
const int titleMarginH = juce::roundToInt (18.0f * s);
const int titleMarginV = juce::roundToInt (4.0f * s);
const int subtitleMarginH = juce::roundToInt (18.0f * s);
const int subtitleMarginV = juce::roundToInt (2.0f * s);
const int rowsMarginH = juce::roundToInt (16.0f * s);
const int rowsMarginV = juce::roundToInt (4.0f * s);
const int gap = juce::roundToInt (10.0f * s);
footerArea = area.removeFromBottom (footerH);
footerLabel.setBounds (footerArea.reduced (rowsMarginH, 0));
footerLabel.setFont (juce::Font (juce::FontOptions (juce::roundToInt (9.0f * s))));
footerLabel.setColour (juce::Label::textColourId, juce::Colour (0xFF5E8688));
auto header = area.removeFromTop (headerH);
const int comboW = presetBoxW + boxGap + scaleBoxW;
auto comboArea = header.removeFromRight (comboW).reduced (0, presetBoxMargin);
auto scaleArea = comboArea.removeFromLeft (scaleBoxW);
scaleBox.setBounds (scaleArea);
presetBox.setBounds (comboArea);
auto titleArea = header.removeFromTop (titleH).reduced (titleMarginH, titleMarginV);
titleLabel.setBounds (titleArea);
subtitleLabel.setBounds (header.reduced (subtitleMarginH, subtitleMarginV));
titleLabel.setFont (juce::Font (juce::FontOptions (juce::roundToInt (22.0f * s), juce::Font::bold)));
subtitleLabel.setFont (juce::Font (juce::FontOptions (juce::roundToInt (11.0f * s))));
auto modules = area.reduced (rowsMarginH, rowsMarginV);
const int panelH = (modules.getHeight() - 2 * gap) / 3;
auto displayPanel = modules.removeFromTop (panelH);
display.setBounds (displayPanel);
knobRow1Area = modules.removeFromTop (panelH + gap);
knobRow2Area = modules.removeFromTop (panelH + gap);
layoutKnobRow (knobRow1Area, 0, 5);
layoutKnobRow (knobRow2Area, 5, 4);
}
void HorizontAudioProcessorEditor::paint (juce::Graphics& g)
{
const auto bounds = getLocalBounds();
g.setGradientFill (juce::ColourGradient (juce::Colour (0xFF0E2A2E), { 0.0f, 0.0f },
juce::Colour (0xFF060B0D), { 0.0f, (float) bounds.getHeight() },
false));
g.fillRect (bounds);
g.setColour (juce::Colour (0x1AFFFFFF));
g.fillRect (0, 0, bounds.getWidth(), 1);
g.setColour (juce::Colour (0x14000000));
g.fillRect (0, bounds.getHeight() - 1, bounds.getWidth(), 1);
if (footerArea.getHeight() > 0)
{
g.setColour (juce::Colour (0x1AFFFFFF));
g.fillRect (0, footerArea.getY(), bounds.getWidth(), 1);
g.setColour (juce::Colour (0x14000000));
g.fillRect (0, footerArea.getY() + 1, bounds.getWidth(), 1);
}
auto drawModule = [&g] (juce::Rectangle<int> moduleArea)
{
const auto r = moduleArea.toFloat();
auto shRect = r.expanded (4.0f, 3.0f).translated (0.0f, 3.0f);
auto shadowGrad = juce::ColourGradient (juce::Colour (0x00000000), { 0.0f, shRect.getY() },
juce::Colour (0x00000000), { 0.0f, shRect.getBottom() },
false);
shadowGrad.addColour (0.45f, juce::Colour (0x70000000));
g.setGradientFill (shadowGrad);
g.fillRoundedRectangle (shRect, 13.0f);
g.setGradientFill (juce::ColourGradient (juce::Colour (0xFF24323A), { 0.0f, r.getY() },
juce::Colour (0xFF0B1216), { 0.0f, r.getBottom() },
false));
g.fillRoundedRectangle (r, 12.0f);
g.setColour (juce::Colour (0x2A3CE0C8));
g.drawRoundedRectangle (r.expanded (1.0f), 13.0f, 1.0f);
g.setColour (juce::Colour (0x38FFFFFF));
g.drawHorizontalLine ((int) r.getY() + 1, (int) r.getX() + 3, (int) r.getRight() - 3);
g.drawVerticalLine ((int) r.getX() + 1, (int) r.getY() + 3, (int) r.getBottom() - 3);
g.setColour (juce::Colour (0x50000000));
g.drawHorizontalLine ((int) r.getBottom() - 1, (int) r.getX() + 3, (int) r.getRight() - 3);
g.drawVerticalLine ((int) r.getRight() - 1, (int) r.getY() + 3, (int) r.getBottom() - 3);
g.saveState();
g.reduceClipRegion (r.toNearestInt().reduced (1));
auto refl = juce::ColourGradient (juce::Colour (0x28FFFFFF), { 0.0f, r.getY() },
juce::Colour (0x00FFFFFF), { 0.0f, r.getY() + r.getHeight() * 0.45f },
false);
g.setGradientFill (refl);
g.fillRect (r);
g.setColour (juce::Colour (0x45FFFFFF));
g.fillRect (r.getX() + 2.0f, r.getY() + 1.0f, r.getWidth() - 4.0f, 1.0f);
g.restoreState();
};
drawModule (knobRow1Area);
drawModule (knobRow2Area);
}

54
Source/PluginEditor.h Normal file
View file

@ -0,0 +1,54 @@
#pragma once
#include <JuceHeader.h>
#include <vector>
#include "Knob.h"
#include "PluginProcessor.h"
#include "HorizontLookAndFeel.h"
#include "SpectrumDisplay.h"
class HorizontAudioProcessorEditor : public juce::AudioProcessorEditor,
private juce::ComboBox::Listener
{
public:
HorizontAudioProcessorEditor (HorizontAudioProcessor&, juce::AudioProcessorValueTreeState&);
~HorizontAudioProcessorEditor() override;
void paint (juce::Graphics&) override;
void resized() override;
void mouseWheelMove (const juce::MouseEvent&, const juce::MouseWheelDetails&) override;
float getScaleFactor() const noexcept { return scaleFactor; }
private:
void comboBoxChanged (juce::ComboBox* boxThatHasChanged) override;
void layoutKnobRow (juce::Rectangle<int> area, int startIndex, int count);
void setScaleFactor (float newScale);
void updateScaleFactor();
void updateScaleSelection();
HorizontAudioProcessor& processor;
juce::AudioProcessorValueTreeState& apvts;
std::unique_ptr<HorizontLookAndFeel> lookAndFeel;
juce::Label titleLabel;
juce::Label subtitleLabel;
juce::ComboBox presetBox;
juce::ComboBox scaleBox;
juce::Label footerLabel;
SpectrumDisplay display;
std::vector<std::unique_ptr<Knob>> knobs;
juce::Rectangle<int> knobRow1Area;
juce::Rectangle<int> knobRow2Area;
juce::Rectangle<int> footerArea;
float scaleFactor = 1.0f;
static constexpr float baseWidth = 860.0f;
static constexpr float baseHeight = 606.0f;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (HorizontAudioProcessorEditor)
};

314
Source/PluginProcessor.cpp Normal file
View file

@ -0,0 +1,314 @@
#include "PluginProcessor.h"
#include "PluginEditor.h"
HorizontAudioProcessor::HorizontAudioProcessor()
: AudioProcessor (BusesProperties()
.withInput ("Input", juce::AudioChannelSet::stereo(), true)
.withOutput ("Output", juce::AudioChannelSet::stereo(), true)),
apvts (*this, nullptr, "Parameters", createParameterLayout()),
presets (getPresets())
{
lowCutParam = apvts.getRawParameterValue ("lowCut");
highCutParam = apvts.getRawParameterValue ("highCut");
decayParam = apvts.getRawParameterValue ("decay");
sizeParam = apvts.getRawParameterValue ("size");
diffusionParam = apvts.getRawParameterValue ("diffusion");
brightnessParam = apvts.getRawParameterValue ("brightness");
feedbackParam = apvts.getRawParameterValue ("feedback");
pitchParam = apvts.getRawParameterValue ("pitch");
dryWetParam = apvts.getRawParameterValue ("dryWet");
}
HorizontAudioProcessor::~HorizontAudioProcessor()
{
}
juce::AudioProcessorValueTreeState::ParameterLayout HorizontAudioProcessor::createParameterLayout()
{
juce::AudioProcessorValueTreeState::ParameterLayout layout;
layout.add (std::make_unique<juce::AudioParameterFloat> ("lowCut", "Low Cut",
juce::NormalisableRange<float> (20.0f, 500.0f, 0.0f, 0.35f), 40.0f));
layout.add (std::make_unique<juce::AudioParameterFloat> ("highCut", "High Cut",
juce::NormalisableRange<float> (800.0f, 20000.0f, 0.0f, 0.25f), 14000.0f));
layout.add (std::make_unique<juce::AudioParameterFloat> ("decay", "Decay",
juce::NormalisableRange<float> (0.2f, 10.0f, 0.0f, 0.5f), 3.5f));
layout.add (std::make_unique<juce::AudioParameterFloat> ("size", "Size",
juce::NormalisableRange<float> (0.4f, 2.0f, 0.0f, 0.5f), 1.0f));
layout.add (std::make_unique<juce::AudioParameterFloat> ("diffusion", "Diffusion",
juce::NormalisableRange<float> (0.0f, 1.0f, 0.0f), 0.6f));
layout.add (std::make_unique<juce::AudioParameterFloat> ("brightness", "Brightness",
juce::NormalisableRange<float> (0.0f, 1.0f, 0.0f), 0.75f));
layout.add (std::make_unique<juce::AudioParameterFloat> ("feedback", "Feedback",
juce::NormalisableRange<float> (0.0f, 1.0f, 0.0f), 0.6f));
layout.add (std::make_unique<juce::AudioParameterFloat> ("pitch", "Pitch",
juce::NormalisableRange<float> (-12.0f, 12.0f, 0.0f), 0.0f));
layout.add (std::make_unique<juce::AudioParameterFloat> ("dryWet", "Dry/Wet",
juce::NormalisableRange<float> (0.0f, 1.0f, 0.0f), 0.35f));
return layout;
}
void HorizontAudioProcessor::prepareToPlay (double sampleRate, int samplesPerBlock)
{
wetScratch.resize ((size_t) juce::jmax (samplesPerBlock, 1));
dryScratch.resize ((size_t) juce::jmax (samplesPerBlock, 1));
wetFifo.reset();
engine.prepare (sampleRate);
updateParameters();
}
void HorizontAudioProcessor::releaseResources()
{
engine.reset();
}
bool HorizontAudioProcessor::isBusesLayoutSupported (const BusesLayout& layouts) const
{
if (layouts.getMainOutputChannelSet().isDisabled())
return false;
const int outCh = layouts.getMainOutputChannelSet().size();
const int inCh = layouts.getMainInputChannelSet().size();
if (inCh == 0)
return outCh <= 2;
return inCh <= 2 && outCh <= 2;
}
void HorizontAudioProcessor::updateParameters()
{
engine.setLowCut (lowCutParam->load());
engine.setHighCut (highCutParam->load());
engine.setDecay (decayParam->load());
engine.setSize (sizeParam->load());
engine.setDiffusion (diffusionParam->load());
engine.setBrightness (brightnessParam->load());
engine.setFeedback (feedbackParam->load());
engine.setPitch (pitchParam->load());
engine.setDryWet (dryWetParam->load());
}
void HorizontAudioProcessor::processBlock (juce::AudioBuffer<float>& buffer, juce::MidiBuffer&)
{
juce::ScopedNoDenormals noDenormals;
const int numSamples = buffer.getNumSamples();
if (numSamples == 0)
return;
const int numOut = juce::jmin (buffer.getNumChannels(), 2);
if (numOut == 0)
return;
for (int ch = numOut; ch < buffer.getNumChannels(); ++ch)
buffer.clear (ch, 0, numSamples);
updateParameters();
const int inputChannels = getChannelCountOfBus (true, 0);
const bool haveLeft = buffer.getNumChannels() > 0 && inputChannels > 0;
const bool haveRight = buffer.getNumChannels() > 1 && inputChannels > 1;
const bool canCaptureWet = wetScratch.size() >= (size_t) numSamples;
for (int s = 0; s < numSamples; ++s)
{
const float inL = haveLeft ? buffer.getSample (0, s) : 0.0f;
const float inR = haveRight ? buffer.getSample (1, s) : inL;
float outL = 0.0f;
float outR = 0.0f;
engine.processSample (inL, inR, outL, outR);
buffer.setSample (0, s, outL);
if (numOut > 1)
buffer.setSample (1, s, outR);
if (canCaptureWet)
{
wetScratch[(size_t) s] = engine.getLastWetL();
dryScratch[(size_t) s] = inL;
}
}
if (canCaptureWet)
{
pushWetSamples (wetScratch.data(), numSamples);
appendDrySamples (dryScratch.data(), numSamples);
}
}
void HorizontAudioProcessor::pushWetSamples (const float* src, int numToWrite)
{
if (numToWrite <= 0)
return;
numToWrite = juce::jmin (numToWrite, wetFifo.getFreeSpace());
if (numToWrite <= 0)
return;
int i = 0;
auto handle = wetFifo.write (numToWrite);
handle.forEach ([&] (int idx) { wetRing.setSample (0, idx, src[i++]); });
}
int HorizontAudioProcessor::readWetSamples (float* dest, int numToRead)
{
const int avail = wetFifo.getNumReady();
if (avail <= 0)
return 0;
numToRead = juce::jmin (numToRead, avail);
int i = 0;
auto handle = wetFifo.read (numToRead);
handle.forEach ([&] (int idx) { dest[i++] = wetRing.getSample (0, idx); });
return numToRead;
}
void HorizontAudioProcessor::appendDrySamples (const float* src, int numToWrite)
{
if (numToWrite <= 0)
return;
size_t w = dryWritePos.load (std::memory_order_relaxed);
for (int i = 0; i < numToWrite; ++i)
{
dryRingLive[w] = src[i];
w = (w + 1) % (size_t) dryRingSize;
}
dryWritePos.store (w, std::memory_order_release);
dryTotalWritten.fetch_add ((size_t) numToWrite, std::memory_order_release);
}
int HorizontAudioProcessor::copyRecentDrySamples (float* dest, int numToRead)
{
numToRead = juce::jmin (numToRead, dryRingSize);
const size_t written = dryTotalWritten.load (std::memory_order_acquire);
const size_t wp = dryWritePos.load (std::memory_order_acquire);
if (written < (size_t) numToRead)
{
const int avail = (int) written;
const int lead = numToRead - avail;
std::fill (dest, dest + lead, 0.0f);
for (int i = 0; i < avail; ++i)
{
const size_t idx = (wp + (size_t) dryRingSize - (size_t) avail + (size_t) i) % (size_t) dryRingSize;
dest[lead + i] = dryRingLive[idx];
}
return numToRead;
}
for (int i = 0; i < numToRead; ++i)
{
const size_t idx = (wp + (size_t) dryRingSize - (size_t) numToRead + (size_t) i) % (size_t) dryRingSize;
dest[i] = dryRingLive[idx];
}
return numToRead;
}
juce::AudioProcessorEditor* HorizontAudioProcessor::createEditor()
{
return new HorizontAudioProcessorEditor (*this, apvts);
}
bool HorizontAudioProcessor::hasEditor() const
{
return true;
}
const juce::String HorizontAudioProcessor::getName() const
{
return "Horizont";
}
bool HorizontAudioProcessor::acceptsMidi() const
{
return false;
}
bool HorizontAudioProcessor::producesMidi() const
{
return false;
}
double HorizontAudioProcessor::getTailLengthSeconds() const
{
return 10.0;
}
int HorizontAudioProcessor::getNumPrograms()
{
return (int) presets.size();
}
int HorizontAudioProcessor::getCurrentProgram()
{
return currentProgram;
}
void HorizontAudioProcessor::setCurrentProgram (int index)
{
if (index >= 0 && index < (int) presets.size())
{
currentProgram = index;
applyPreset (presets[(size_t) index]);
}
}
const juce::String HorizontAudioProcessor::getProgramName (int index)
{
if (index >= 0 && index < (int) presets.size())
return presets[(size_t) index].name;
return "Default";
}
void HorizontAudioProcessor::changeProgramName (int, const juce::String&)
{
}
void HorizontAudioProcessor::applyPreset (const Preset& preset)
{
for (const auto& v : preset.values)
{
if (auto* param = apvts.getParameter (v.id))
param->setValueNotifyingHost (param->convertTo0to1 (v.value));
}
}
void HorizontAudioProcessor::getStateInformation (juce::MemoryBlock& destData)
{
auto state = apvts.copyState();
std::unique_ptr<juce::XmlElement> xml (state.createXml());
copyXmlToBinary (*xml, destData);
}
void HorizontAudioProcessor::setStateInformation (const void* data, int sizeInBytes)
{
std::unique_ptr<juce::XmlElement> xml (getXmlFromBinary (data, sizeInBytes));
if (xml != nullptr)
apvts.replaceState (juce::ValueTree::fromXml (*xml));
updateParameters();
}
juce::AudioProcessor* JUCE_CALLTYPE createPluginFilter()
{
return new HorizontAudioProcessor();
}

81
Source/PluginProcessor.h Normal file
View file

@ -0,0 +1,81 @@
#pragma once
#include <JuceHeader.h>
#include <atomic>
#include "Presets.h"
#include "HorizontEngine.h"
class HorizontAudioProcessor : public juce::AudioProcessor
{
public:
HorizontAudioProcessor();
~HorizontAudioProcessor() override;
void prepareToPlay (double sampleRate, int samplesPerBlock) override;
void releaseResources() override;
bool isBusesLayoutSupported (const BusesLayout& layouts) const override;
void processBlock (juce::AudioBuffer<float>&, juce::MidiBuffer&) override;
juce::AudioProcessorEditor* createEditor() override;
bool hasEditor() const override;
const juce::String getName() const override;
bool acceptsMidi() const override;
bool producesMidi() const override;
double getTailLengthSeconds() const override;
int getNumPrograms() override;
int getCurrentProgram() override;
void setCurrentProgram (int index) override;
const juce::String getProgramName (int index) override;
void changeProgramName (int index, const juce::String& newName) override;
void getStateInformation (juce::MemoryBlock& destData) override;
void setStateInformation (const void* data, int sizeInBytes) override;
juce::AudioProcessorValueTreeState::ParameterLayout createParameterLayout();
void applyPreset (const Preset& preset);
const std::vector<Preset>& getPresets() const noexcept { return presets; }
void pushWetSamples (const float* data, int numToWrite);
int readWetSamples (float* dest, int numToRead);
void appendDrySamples (const float* data, int numToWrite);
int copyRecentDrySamples (float* dest, int numToRead);
juce::AudioProcessorValueTreeState apvts;
private:
void updateParameters();
HorizontEngine engine;
std::vector<Preset> presets;
int currentProgram = 0;
juce::AbstractFifo wetFifo { 2048 };
juce::AudioBuffer<float> wetRing { 1, 2048 };
std::vector<float> wetScratch;
std::vector<float> dryScratch;
static constexpr int dryRingSize = 4096;
std::vector<float> dryRingLive = std::vector<float> (dryRingSize, 0.0f);
std::atomic<size_t> dryWritePos { 0 };
std::atomic<size_t> dryTotalWritten { 0 };
std::atomic<float>* lowCutParam = nullptr;
std::atomic<float>* highCutParam = nullptr;
std::atomic<float>* decayParam = nullptr;
std::atomic<float>* sizeParam = nullptr;
std::atomic<float>* diffusionParam = nullptr;
std::atomic<float>* brightnessParam = nullptr;
std::atomic<float>* feedbackParam = nullptr;
std::atomic<float>* pitchParam = nullptr;
std::atomic<float>* dryWetParam = nullptr;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (HorizontAudioProcessor)
};

54
Source/Presets.h Normal file
View file

@ -0,0 +1,54 @@
#pragma once
#include <vector>
struct Preset
{
const char* name;
struct ParamValue
{
const char* id;
float value;
};
std::vector<ParamValue> values;
};
inline const std::vector<Preset>& getPresets()
{
static const std::vector<Preset> presets =
{
{ "Studio Plate",
{ { "lowCut", 30.0f }, { "highCut", 16000.0f }, { "decay", 2.5f },
{ "size", 0.75f }, { "diffusion", 0.70f }, { "brightness", 0.80f },
{ "feedback", 0.25f }, { "pitch", 0.0f }, { "dryWet", 0.40f } } },
{ "Warm Hall",
{ { "lowCut", 60.0f }, { "highCut", 8000.0f }, { "decay", 4.5f },
{ "size", 1.25f }, { "diffusion", 0.60f }, { "brightness", 0.45f },
{ "feedback", 0.50f }, { "pitch", 0.0f }, { "dryWet", 0.45f } } },
{ "Big Cathedral",
{ { "lowCut", 40.0f }, { "highCut", 12000.0f }, { "decay", 8.5f },
{ "size", 1.9f }, { "diffusion", 0.75f }, { "brightness", 0.60f },
{ "feedback", 0.70f }, { "pitch", 0.0f }, { "dryWet", 0.55f } } },
{ "Shimmer Space",
{ { "lowCut", 45.0f }, { "highCut", 15000.0f }, { "decay", 6.0f },
{ "size", 1.5f }, { "diffusion", 0.65f }, { "brightness", 0.70f },
{ "feedback", 0.90f }, { "pitch", 7.0f }, { "dryWet", 0.60f } } },
{ "Dark Pulse",
{ { "lowCut", 80.0f }, { "highCut", 7000.0f }, { "decay", 3.0f },
{ "size", 0.9f }, { "diffusion", 0.50f }, { "brightness", 0.35f },
{ "feedback", 0.85f }, { "pitch", -5.0f }, { "dryWet", 0.50f } } },
{ "Tight Room",
{ { "lowCut", 90.0f }, { "highCut", 16000.0f }, { "decay", 0.6f },
{ "size", 0.5f }, { "diffusion", 0.55f }, { "brightness", 0.85f },
{ "feedback", 0.20f }, { "pitch", 0.0f }, { "dryWet", 0.30f } } }
};
return presets;
}

273
Source/SpectrumDisplay.cpp Normal file
View file

@ -0,0 +1,273 @@
#include "SpectrumDisplay.h"
namespace
{
constexpr float minDb = -66.0f;
constexpr float topDb = -6.0f;
constexpr float dbRange = topDb - minDb;
const juce::Colour wetLineCol (0xDD9DF5E9);
const juce::Colour wetFillCol (0xE63CE0C8);
const juce::Colour wetPeakCol (0x883CE0C8);
const juce::Colour dryLineCol (0xCCF0A03C);
const juce::Colour dryFillCol (0x22F0A03C);
const juce::Colour dryPeakCol (0x55E89B3A);
juce::String formatFreq (float hz)
{
if (hz >= 1000.0f)
return juce::String (juce::roundToInt (hz / 1000.0f)) + "k";
return juce::String (juce::roundToInt (hz));
}
}
SpectrumDisplay::SpectrumDisplay (HorizontAudioProcessor& p)
: processor (p)
{
window.resize ((size_t) fftSize);
for (int i = 0; i < fftSize; ++i)
window[(size_t) i] = 0.5f * (1.0f - std::cos (2.0f * juce::MathConstants<float>::pi * (float) i / (float) (fftSize - 1)));
startTimerHz (30);
}
SpectrumDisplay::~SpectrumDisplay()
{
stopTimer();
}
void SpectrumDisplay::setScaleFactor (float scale)
{
scaleFactor = juce::jlimit (0.6f, 2.0f, scale);
repaint();
}
void SpectrumDisplay::computeLevels (const float* samples, int got, std::array<float, numBins>& out)
{
for (auto& l : out)
l = -200.0f;
if (got <= 0)
return;
for (int i = 0; i < got; ++i)
fftData[(size_t) i] = samples[i] * window[(size_t) i];
for (int i = got; i < fftSize; ++i)
fftData[(size_t) i] = 0.0f;
std::fill (fftData.begin() + fftSize, fftData.end(), 0.0f);
fft.performRealOnlyForwardTransform (fftData.data(), true);
const float logNyquist = std::log ((float) (fftSize / 2));
const float binScale = 2.0f / (float) fftSize;
for (int i = 0; i < numBins; ++i)
{
const int kLo = jmax (1, (int) std::floor (std::exp (logNyquist * (float) i / (float) numBins)));
const int kHi = jmin (fftSize / 2 - 1, (int) std::ceil (std::exp (logNyquist * (float) (i + 1) / (float) numBins)));
for (int k = kLo; k <= kHi; ++k)
{
const float re = fftData[(size_t) (2 * k)];
const float im = fftData[(size_t) (2 * k + 1)];
const float mag = std::sqrt (re * re + im * im) * binScale;
const float db = 20.0f * std::log10 (mag + 1e-6f);
out[(size_t) i] = jmax (out[(size_t) i], db);
}
}
}
void SpectrumDisplay::timerCallback()
{
juce::AudioBuffer<float> wetBuf (1, fftSize);
juce::AudioBuffer<float> dryBuf (1, fftSize);
const int got = processor.readWetSamples (wetBuf.getWritePointer (0), fftSize);
processor.copyRecentDrySamples (dryBuf.getWritePointer (0), fftSize);
computeLevels (wetBuf.getReadPointer (0), got, level);
computeLevels (dryBuf.getReadPointer (0), fftSize, dryLevel);
for (int i = 0; i < numBins; ++i)
{
const int lo = jmax (0, i - 1);
const int hi = jmin (numBins - 1, i + 1);
const float wetSmooth = 0.25f * level[(size_t) lo] + 0.5f * level[(size_t) i] + 0.25f * level[(size_t) hi];
const float wetNorm = (juce::jlimit (minDb, topDb, wetSmooth) - minDb) / dbRange;
float& wd = display[(size_t) i];
wd += (wetNorm - wd) * (wetNorm > wd ? 0.45f : 0.15f);
peak[(size_t) i] = jmax (peak[(size_t) i] * 0.93f, display[(size_t) i]);
const float drySmooth = 0.25f * dryLevel[(size_t) lo] + 0.5f * dryLevel[(size_t) i] + 0.25f * dryLevel[(size_t) hi];
const float dryNorm = (juce::jlimit (minDb, topDb, drySmooth) - minDb) / dbRange;
float& dd = dryDisplay[(size_t) i];
dd += (dryNorm - dd) * (dryNorm > dd ? 0.45f : 0.15f);
dryPeak[(size_t) i] = jmax (dryPeak[(size_t) i] * 0.93f, dryDisplay[(size_t) i]);
}
repaint();
}
juce::Path SpectrumDisplay::makeCurve (const juce::Rectangle<float>& graph, const std::array<float, numBins>& data) const
{
juce::Path p;
for (int i = 0; i < numBins; ++i)
{
const float x = graph.getX() + graph.getWidth() * (float) (i + 0.5f) / (float) numBins;
const float y = graph.getBottom() - data[(size_t) i] * graph.getHeight();
if (i == 0)
p.startNewSubPath (x, y);
else
p.lineTo (x, y);
}
return p;
}
void SpectrumDisplay::paint (juce::Graphics& g)
{
const auto bounds = getLocalBounds().toFloat();
const float s = scaleFactor;
auto shRect = bounds.expanded (6.0f * s, 4.0f * s).translated (0.0f, 5.0f * s);
auto shadowGrad = juce::ColourGradient (juce::Colour (0x00000000), { 0.0f, shRect.getY() },
juce::Colour (0x00000000), { 0.0f, shRect.getBottom() },
false);
shadowGrad.addColour (0.45f, juce::Colour (0x70000000));
g.setGradientFill (shadowGrad);
g.fillRoundedRectangle (shRect, 14.0f * s);
auto bodyGrad = juce::ColourGradient (juce::Colour (0xFF24323A), { 0.0f, bounds.getY() },
juce::Colour (0xFF0B1216), { 0.0f, bounds.getBottom() },
false);
g.setGradientFill (bodyGrad);
g.fillRoundedRectangle (bounds, 12.0f * s);
g.setColour (juce::Colour (0x3A3CE0C8));
g.drawRoundedRectangle (bounds.expanded (1.0f * s), 13.0f * s, 1.0f * s);
g.setColour (juce::Colour (0x38FFFFFF));
g.drawHorizontalLine (juce::roundToInt (bounds.getY() + 1.0f * s), juce::roundToInt (bounds.getX() + 3.0f * s), juce::roundToInt (bounds.getRight() - 3.0f * s));
g.drawVerticalLine (juce::roundToInt (bounds.getX() + 1.0f * s), juce::roundToInt (bounds.getY() + 3.0f * s), juce::roundToInt (bounds.getBottom() - 3.0f * s));
g.setColour (juce::Colour (0x50000000));
g.drawHorizontalLine (juce::roundToInt (bounds.getBottom() - 1.0f * s), juce::roundToInt (bounds.getX() + 3.0f * s), juce::roundToInt (bounds.getRight() - 3.0f * s));
g.drawVerticalLine (juce::roundToInt (bounds.getRight() - 1.0f * s), juce::roundToInt (bounds.getY() + 3.0f * s), juce::roundToInt (bounds.getBottom() - 3.0f * s));
g.saveState();
g.reduceClipRegion (bounds.toNearestInt().reduced (juce::roundToInt (1.0f * s)));
auto refl = juce::ColourGradient (juce::Colour (0x28FFFFFF), { 0.0f, bounds.getY() },
juce::Colour (0x00FFFFFF), { 0.0f, bounds.getY() + bounds.getHeight() * 0.45f },
false);
g.setGradientFill (refl);
g.fillRect (bounds);
g.setColour (juce::Colour (0x45FFFFFF));
g.fillRect (bounds.getX() + 2.0f * s, bounds.getY() + 1.0f * s, bounds.getWidth() - 4.0f * s, 1.0f * s);
g.restoreState();
const auto area = bounds.reduced (14.0f * s, 8.0f * s);
const float dbAxisW = 30.0f * s;
const float graphAreaBottom = area.getBottom() - 16.0f * s;
const float graphH = graphAreaBottom - area.getY();
const auto graph = area.withHeight (graphH).withLeft (area.getX() + dbAxisW);
g.setFont (juce::Font (juce::FontOptions (9.0f * s)));
for (int i = 0; i < 4; ++i)
{
const float db = -6.0f - (float) i * 15.0f;
const float y = graph.getBottom() - ((db - minDb) / dbRange) * graph.getHeight();
g.setColour (juce::Colour (0x223A4A4D));
g.drawHorizontalLine (juce::roundToInt (y), juce::roundToInt (graph.getX()), juce::roundToInt (graph.getRight()));
g.setColour (juce::Colour (0x55FFFFFF));
g.drawText (juce::String (juce::roundToInt (db)), juce::Rectangle<float> (graph.getX() - dbAxisW, y - 6.0f * s, dbAxisW - 6.0f * s, 12.0f * s),
juce::Justification::right, false);
}
const float sr = juce::jmax (1000.0f, (float) processor.getSampleRate());
const float logNyquist = std::log ((float) (fftSize / 2));
const float freqs[] = { 50.0f, 100.0f, 200.0f, 500.0f, 1000.0f, 2000.0f, 5000.0f, 10000.0f, 20000.0f };
for (float f : freqs)
{
const float k = f * (float) fftSize / sr;
if (k < 1.0f || k > (float) (fftSize / 2))
continue;
const float x = graph.getX() + (std::log (k) / logNyquist) * graph.getWidth();
g.setColour (juce::Colour (0x223A4A4D));
g.drawVerticalLine (juce::roundToInt (x), juce::roundToInt (graph.getY()), juce::roundToInt (graph.getBottom()));
g.setColour (juce::Colour (0x55FFFFFF));
g.drawText (formatFreq (f), juce::Rectangle<float> (x - 14.0f * s, graph.getBottom() + 3.0f * s, 28.0f * s, 12.0f * s),
juce::Justification::centred, false);
}
g.setColour (juce::Colour (0x33FFFFFF));
g.drawRect (graph, 0.6f * s);
if (graph.getWidth() > 2.0f * s && graph.getHeight() > 2.0f * s)
{
auto curve = makeCurve (graph, display);
auto dry = makeCurve (graph, dryDisplay);
auto wetFill = juce::Path (curve);
wetFill.lineTo (graph.getRight(), graph.getBottom());
wetFill.lineTo (graph.getX(), graph.getBottom());
wetFill.closeSubPath();
auto dryFill = juce::Path (dry);
dryFill.lineTo (graph.getRight(), graph.getBottom());
dryFill.lineTo (graph.getX(), graph.getBottom());
dryFill.closeSubPath();
auto wetGrad = juce::ColourGradient (wetFillCol, { 0.0f, graph.getY() },
juce::Colour (0x063CE0C8), { 0.0f, graph.getBottom() },
false);
g.setGradientFill (wetGrad);
g.fillPath (wetFill);
auto dryGrad = juce::ColourGradient (dryFillCol, { 0.0f, graph.getY() },
juce::Colour (0x00FFFFFF), { 0.0f, graph.getBottom() },
false);
g.setGradientFill (dryGrad);
g.fillPath (dryFill);
g.setColour (wetPeakCol);
g.strokePath (makeCurve (graph, peak), juce::PathStrokeType (1.0f * s));
g.setColour (dryLineCol);
g.strokePath (dry, juce::PathStrokeType (1.2f * s,
juce::PathStrokeType::JointStyle::curved,
juce::PathStrokeType::EndCapStyle::rounded));
g.setColour (dryPeakCol);
g.strokePath (makeCurve (graph, dryPeak), juce::PathStrokeType (0.8f * s));
g.setColour (wetLineCol);
g.strokePath (curve, juce::PathStrokeType (1.4f * s,
juce::PathStrokeType::JointStyle::curved,
juce::PathStrokeType::EndCapStyle::rounded));
}
g.setColour (juce::Colour (0x66FFFFFF));
g.setFont (juce::Font (juce::FontOptions (10.0f * s, juce::Font::bold)));
g.drawText ("REVERB SPECTRUM", graph.withTop (graph.getY() - 2.0f * s).withBottom (graph.getY() + 14.0f * s),
juce::Justification::bottomLeft, false);
const float legendY = graph.getY() + 4.0f * s;
const float legendX = graph.getRight() - 2.0f * s;
const auto legendRow = juce::Rectangle<float> (legendX - 84.0f * s, legendY - 9.0f * s, 84.0f * s, 12.0f * s);
g.setFont (juce::Font (juce::FontOptions (9.0f * s)));
g.setColour (wetPeakCol);
g.drawHorizontalLine (juce::roundToInt (legendRow.getY() + 8.0f * s), juce::roundToInt (legendRow.getX()), juce::roundToInt (legendRow.getX() + 11.0f * s));
g.setColour (juce::Colour (0x88FFFFFF));
g.drawText ("WET", legendRow.withX (legendRow.getX() + 14.0f * s).withWidth (28.0f * s),
juce::Justification::left, false);
g.setColour (dryPeakCol);
g.drawHorizontalLine (juce::roundToInt (legendRow.getY() + 8.0f * s), juce::roundToInt (legendRow.getX() + 42.0f * s), juce::roundToInt (legendRow.getX() + 53.0f * s));
g.setColour (juce::Colour (0x88FFFFFF));
g.drawText ("DRY", legendRow.withX (legendRow.getX() + 56.0f * s).withWidth (28.0f * s),
juce::Justification::left, false);
}

43
Source/SpectrumDisplay.h Normal file
View file

@ -0,0 +1,43 @@
#pragma once
#include <JuceHeader.h>
#include <array>
#include "PluginProcessor.h"
class SpectrumDisplay : public juce::Component, private juce::Timer
{
public:
explicit SpectrumDisplay (HorizontAudioProcessor&);
~SpectrumDisplay() override;
void paint (juce::Graphics&) override;
void setScaleFactor (float scale);
private:
void timerCallback() override;
HorizontAudioProcessor& processor;
static constexpr int fftOrder = 10;
static constexpr int fftSize = 1 << fftOrder;
static constexpr int numBins = 48;
void computeLevels (const float* samples, int got, std::array<float, numBins>& out);
juce::Path makeCurve (const juce::Rectangle<float>& graph, const std::array<float, numBins>& data) const;
juce::dsp::FFT fft { fftOrder };
std::vector<float> fftData = std::vector<float> (2 * fftSize, 0.0f);
std::vector<float> window;
std::array<float, numBins> level {};
std::array<float, numBins> display {};
std::array<float, numBins> peak {};
std::array<float, numBins> dryLevel {};
std::array<float, numBins> dryDisplay {};
std::array<float, numBins> dryPeak {};
float scaleFactor = 1.0f;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (SpectrumDisplay)
};