Initial release — InstaShadow mastering compressor v1.0
Dual-stage compressor (optical + VCA) with output transformer saturation. - Port-Hamiltonian T4B opto-cell model with implicit trapezoidal integration - Feed-forward VCA with 7 ratios, 6 attack/release presets, Dual release mode - 3 transformer types (Nickel/Iron/Steel) with 4x oversampled waveshaping - Analog-style needle VU meters, horizontal GR meters - Sidechain HPF, stereo link, independent section bypass - Full state save/restore, CI/CD for Windows/macOS/Linux
This commit is contained in:
145
Source/CompressorEngine.cpp
Normal file
145
Source/CompressorEngine.cpp
Normal file
@@ -0,0 +1,145 @@
|
||||
#include "CompressorEngine.h"
|
||||
|
||||
CompressorEngine::CompressorEngine() {}
|
||||
|
||||
void CompressorEngine::prepare (double sampleRate, int samplesPerBlock)
|
||||
{
|
||||
currentSampleRate = sampleRate;
|
||||
|
||||
for (auto& cell : optoCells) cell.prepare (sampleRate);
|
||||
for (auto& comp : vcaComps) comp.prepare (sampleRate);
|
||||
transformer.prepare (sampleRate, samplesPerBlock);
|
||||
|
||||
// Init sidechain HPF
|
||||
lastScHpfFreq = 0.0f;
|
||||
updateScHpf (90.0f);
|
||||
|
||||
for (auto& f : scHpfFilters) f.reset();
|
||||
}
|
||||
|
||||
void CompressorEngine::updateScHpf (float freqHz)
|
||||
{
|
||||
if (std::abs (freqHz - lastScHpfFreq) < 0.1f) return;
|
||||
lastScHpfFreq = freqHz;
|
||||
|
||||
auto coeffs = juce::dsp::IIR::Coefficients<float>::makeHighPass (currentSampleRate, freqHz);
|
||||
for (auto& f : scHpfFilters)
|
||||
f.coefficients = coeffs;
|
||||
}
|
||||
|
||||
void CompressorEngine::processBlock (juce::AudioBuffer<float>& buffer)
|
||||
{
|
||||
const int numSamples = buffer.getNumSamples();
|
||||
const int numChannels = std::min (buffer.getNumChannels(), 2);
|
||||
|
||||
if (globalBypass.load() || numChannels == 0) return;
|
||||
|
||||
// Read parameters once per block
|
||||
float optoThresh = optoThresholdDb.load();
|
||||
float optoGain = optoGainDb.load();
|
||||
float scHpf = optoScHpfHz.load();
|
||||
bool optoBp = optoBypass.load();
|
||||
|
||||
float vcaThresh = vcaThresholdDb.load();
|
||||
float vcaGain = vcaGainDb.load();
|
||||
int ratioIdx = std::clamp (vcaRatioIndex.load(), 0, numRatios - 1);
|
||||
int attackIdx = std::clamp (vcaAttackIndex.load(), 0, numAttacks - 1);
|
||||
int releaseIdx = std::clamp (vcaReleaseIndex.load(), 0, numReleases - 1);
|
||||
bool vcaBp = vcaBypass.load();
|
||||
|
||||
int xfmrType = transformerType.load();
|
||||
float outGain = std::pow (10.0f, outputGainDb.load() / 20.0f);
|
||||
bool linked = stereoLink.load();
|
||||
|
||||
float ratio = ratios[ratioIdx];
|
||||
float attackSec = attacks[attackIdx];
|
||||
float releaseSec = releases[releaseIdx];
|
||||
bool dualRel = (releaseSec < 0.0f);
|
||||
if (dualRel) releaseSec = 0.5f; // fallback (not used in dual mode)
|
||||
|
||||
float optoThreshLin = std::pow (10.0f, optoThresh / 20.0f);
|
||||
float optoGainLin = std::pow (10.0f, optoGain / 20.0f);
|
||||
float vcaGainLin = std::pow (10.0f, vcaGain / 20.0f);
|
||||
|
||||
// Update sidechain HPF
|
||||
updateScHpf (scHpf);
|
||||
|
||||
// Per-sample processing
|
||||
float peakOptoGr = 0.0f;
|
||||
float peakVcaGr = 0.0f;
|
||||
|
||||
for (int i = 0; i < numSamples; ++i)
|
||||
{
|
||||
// Get input samples
|
||||
float inL = buffer.getSample (0, i);
|
||||
float inR = (numChannels > 1) ? buffer.getSample (1, i) : inL;
|
||||
|
||||
// Sidechain: HPF filtered input for detection
|
||||
float scL = scHpfFilters[0].processSample (inL);
|
||||
float scR = (numChannels > 1) ? scHpfFilters[1].processSample (inR) : scL;
|
||||
|
||||
// Sidechain level (rectified)
|
||||
float scLevelL = std::abs (scL);
|
||||
float scLevelR = std::abs (scR);
|
||||
|
||||
if (linked)
|
||||
{
|
||||
float scMono = (scLevelL + scLevelR) * 0.5f;
|
||||
scLevelL = scMono;
|
||||
scLevelR = scMono;
|
||||
}
|
||||
|
||||
// --- Optical compressor ---
|
||||
float optoGainL = 1.0f, optoGainR = 1.0f;
|
||||
if (! optoBp)
|
||||
{
|
||||
optoGainL = optoCells[0].processSample (scLevelL, optoThreshLin);
|
||||
optoGainR = linked ? optoGainL : optoCells[1].processSample (scLevelR, optoThreshLin);
|
||||
|
||||
inL *= optoGainL * optoGainLin;
|
||||
inR *= optoGainR * optoGainLin;
|
||||
|
||||
float gr = optoCells[0].getGainReductionDb();
|
||||
if (gr < peakOptoGr) peakOptoGr = gr;
|
||||
}
|
||||
|
||||
// --- VCA compressor ---
|
||||
if (! vcaBp)
|
||||
{
|
||||
// Convert to dB for VCA
|
||||
float scDbL = 20.0f * std::log10 (std::max (1.0e-6f, std::abs (inL)));
|
||||
float scDbR = 20.0f * std::log10 (std::max (1.0e-6f, std::abs (inR)));
|
||||
|
||||
if (linked) scDbL = scDbR = std::max (scDbL, scDbR);
|
||||
|
||||
float vcaGainL = vcaComps[0].processSample (scDbL, vcaThresh, ratio, attackSec, releaseSec, dualRel);
|
||||
float vcaGainR = linked ? vcaGainL : vcaComps[1].processSample (scDbR, vcaThresh, ratio, attackSec, releaseSec, dualRel);
|
||||
|
||||
inL *= vcaGainL * vcaGainLin;
|
||||
inR *= vcaGainR * vcaGainLin;
|
||||
|
||||
float gr = vcaComps[0].getGainReductionDb();
|
||||
if (gr < peakVcaGr) peakVcaGr = gr;
|
||||
}
|
||||
|
||||
// Write back (transformer and output gain applied later as block ops)
|
||||
buffer.setSample (0, i, inL);
|
||||
if (numChannels > 1) buffer.setSample (1, i, inR);
|
||||
}
|
||||
|
||||
// --- Transformer saturation (block-level, 4x oversampled) ---
|
||||
if (xfmrType > 0 && xfmrType <= 3)
|
||||
transformer.processBlock (buffer, (TransformerSaturation::Type) xfmrType);
|
||||
|
||||
// --- Output gain ---
|
||||
buffer.applyGain (outGain);
|
||||
|
||||
// --- Metering ---
|
||||
optoGrDb.store (peakOptoGr);
|
||||
vcaGrDb.store (peakVcaGr);
|
||||
outputLevelL.store (buffer.getMagnitude (0, 0, numSamples));
|
||||
if (numChannels > 1)
|
||||
outputLevelR.store (buffer.getMagnitude (1, 0, numSamples));
|
||||
else
|
||||
outputLevelR.store (outputLevelL.load());
|
||||
}
|
||||
64
Source/CompressorEngine.h
Normal file
64
Source/CompressorEngine.h
Normal file
@@ -0,0 +1,64 @@
|
||||
#pragma once
|
||||
#include <JuceHeader.h>
|
||||
#include "OpticalCell.h"
|
||||
#include "VCACompressor.h"
|
||||
#include "TransformerSaturation.h"
|
||||
|
||||
class CompressorEngine
|
||||
{
|
||||
public:
|
||||
CompressorEngine();
|
||||
|
||||
void prepare (double sampleRate, int samplesPerBlock);
|
||||
void processBlock (juce::AudioBuffer<float>& buffer);
|
||||
|
||||
// --- Optical section (GUI → audio) ---
|
||||
std::atomic<float> optoThresholdDb { -20.0f };
|
||||
std::atomic<float> optoGainDb { 0.0f };
|
||||
std::atomic<float> optoScHpfHz { 90.0f };
|
||||
std::atomic<bool> optoBypass { false };
|
||||
|
||||
// --- Discrete VCA section ---
|
||||
std::atomic<float> vcaThresholdDb { -20.0f };
|
||||
std::atomic<float> vcaGainDb { 0.0f };
|
||||
std::atomic<int> vcaRatioIndex { 1 };
|
||||
std::atomic<int> vcaAttackIndex { 2 };
|
||||
std::atomic<int> vcaReleaseIndex { 2 };
|
||||
std::atomic<bool> vcaBypass { false };
|
||||
|
||||
// --- Transformer ---
|
||||
std::atomic<int> transformerType { 0 }; // 0=Off, 1=Nickel, 2=Iron, 3=Steel
|
||||
|
||||
// --- Output / Global ---
|
||||
std::atomic<float> outputGainDb { 0.0f };
|
||||
std::atomic<bool> stereoLink { true };
|
||||
std::atomic<bool> globalBypass { false };
|
||||
|
||||
// --- Metering (audio → GUI) ---
|
||||
std::atomic<float> optoGrDb { 0.0f };
|
||||
std::atomic<float> vcaGrDb { 0.0f };
|
||||
std::atomic<float> outputLevelL { 0.0f };
|
||||
std::atomic<float> outputLevelR { 0.0f };
|
||||
|
||||
// Lookup tables
|
||||
static constexpr float ratios[] = { 1.2f, 2.0f, 3.0f, 4.0f, 6.0f, 10.0f, 20.0f };
|
||||
static constexpr float attacks[] = { 0.0001f, 0.0005f, 0.001f, 0.005f, 0.01f, 0.03f };
|
||||
static constexpr float releases[] = { 0.1f, 0.25f, 0.5f, 0.8f, 1.2f, -1.0f }; // -1 = Dual
|
||||
static constexpr int numRatios = 7;
|
||||
static constexpr int numAttacks = 6;
|
||||
static constexpr int numReleases = 6;
|
||||
|
||||
private:
|
||||
double currentSampleRate = 44100.0;
|
||||
|
||||
// Per-channel DSP
|
||||
std::array<OpticalCell, 2> optoCells;
|
||||
std::array<VCACompressor, 2> vcaComps;
|
||||
TransformerSaturation transformer;
|
||||
|
||||
// Sidechain HPF (2nd order, per channel)
|
||||
std::array<juce::dsp::IIR::Filter<float>, 2> scHpfFilters;
|
||||
float lastScHpfFreq = 0.0f;
|
||||
|
||||
void updateScHpf (float freqHz);
|
||||
};
|
||||
129
Source/DiscretePanel.cpp
Normal file
129
Source/DiscretePanel.cpp
Normal file
@@ -0,0 +1,129 @@
|
||||
#include "DiscretePanel.h"
|
||||
|
||||
DiscretePanel::DiscretePanel()
|
||||
{
|
||||
titleLabel.setText ("DISCRETE", juce::dontSendNotification);
|
||||
titleLabel.setJustificationType (juce::Justification::centred);
|
||||
titleLabel.setColour (juce::Label::textColourId, InstaShadowLookAndFeel::textPrimary);
|
||||
addAndMakeVisible (titleLabel);
|
||||
|
||||
auto setupKnob = [this] (juce::Slider& knob, juce::Label& label, const juce::String& name,
|
||||
double min, double max, double def, const juce::String& suffix)
|
||||
{
|
||||
knob.setSliderStyle (juce::Slider::RotaryHorizontalVerticalDrag);
|
||||
knob.setTextBoxStyle (juce::Slider::TextBoxBelow, false, 60, 14);
|
||||
knob.setRange (min, max);
|
||||
knob.setValue (def);
|
||||
knob.setTextValueSuffix (suffix);
|
||||
knob.getProperties().set (InstaShadowLookAndFeel::knobTypeProperty, "orange");
|
||||
addAndMakeVisible (knob);
|
||||
|
||||
label.setText (name, juce::dontSendNotification);
|
||||
label.setJustificationType (juce::Justification::centred);
|
||||
label.setColour (juce::Label::textColourId, InstaShadowLookAndFeel::textSecondary);
|
||||
addAndMakeVisible (label);
|
||||
};
|
||||
|
||||
setupKnob (thresholdKnob, threshLabel, "Threshold", -40.0, 0.0, -20.0, " dB");
|
||||
setupKnob (gainKnob, gainLabel, "Gain", 0.0, 20.0, 0.0, " dB");
|
||||
|
||||
ratioBox.addItem ("1.2:1", 1);
|
||||
ratioBox.addItem ("2:1", 2);
|
||||
ratioBox.addItem ("3:1", 3);
|
||||
ratioBox.addItem ("4:1", 4);
|
||||
ratioBox.addItem ("6:1", 5);
|
||||
ratioBox.addItem ("10:1", 6);
|
||||
ratioBox.addItem ("Flood", 7);
|
||||
ratioBox.setSelectedId (2);
|
||||
addAndMakeVisible (ratioBox);
|
||||
ratioLabel.setText ("Ratio", juce::dontSendNotification);
|
||||
ratioLabel.setJustificationType (juce::Justification::centred);
|
||||
ratioLabel.setColour (juce::Label::textColourId, InstaShadowLookAndFeel::textSecondary);
|
||||
addAndMakeVisible (ratioLabel);
|
||||
|
||||
attackBox.addItem ("0.1 ms", 1);
|
||||
attackBox.addItem ("0.5 ms", 2);
|
||||
attackBox.addItem ("1 ms", 3);
|
||||
attackBox.addItem ("5 ms", 4);
|
||||
attackBox.addItem ("10 ms", 5);
|
||||
attackBox.addItem ("30 ms", 6);
|
||||
attackBox.setSelectedId (3);
|
||||
addAndMakeVisible (attackBox);
|
||||
attackLabel.setText ("Attack", juce::dontSendNotification);
|
||||
attackLabel.setJustificationType (juce::Justification::centred);
|
||||
attackLabel.setColour (juce::Label::textColourId, InstaShadowLookAndFeel::textSecondary);
|
||||
addAndMakeVisible (attackLabel);
|
||||
|
||||
releaseBox.addItem ("100 ms", 1);
|
||||
releaseBox.addItem ("250 ms", 2);
|
||||
releaseBox.addItem ("500 ms", 3);
|
||||
releaseBox.addItem ("800 ms", 4);
|
||||
releaseBox.addItem ("1.2 s", 5);
|
||||
releaseBox.addItem ("Dual", 6);
|
||||
releaseBox.setSelectedId (3);
|
||||
addAndMakeVisible (releaseBox);
|
||||
releaseLabel.setText ("Release", juce::dontSendNotification);
|
||||
releaseLabel.setJustificationType (juce::Justification::centred);
|
||||
releaseLabel.setColour (juce::Label::textColourId, InstaShadowLookAndFeel::textSecondary);
|
||||
addAndMakeVisible (releaseLabel);
|
||||
|
||||
bypassToggle.setButtonText ("");
|
||||
addAndMakeVisible (bypassToggle);
|
||||
bypassLabel.setText ("Bypass", juce::dontSendNotification);
|
||||
bypassLabel.setJustificationType (juce::Justification::centred);
|
||||
bypassLabel.setColour (juce::Label::textColourId, InstaShadowLookAndFeel::textSecondary);
|
||||
addAndMakeVisible (bypassLabel);
|
||||
}
|
||||
|
||||
void DiscretePanel::resized()
|
||||
{
|
||||
auto bounds = getLocalBounds().reduced (4);
|
||||
titleLabel.setBounds (bounds.removeFromTop (18));
|
||||
|
||||
int availH = bounds.getHeight() - 22; // reserve for bypass
|
||||
|
||||
// Top: 2 knobs stacked vertically (~55%)
|
||||
int knobSectionH = (int) (availH * 0.55f);
|
||||
int singleKnobH = knobSectionH / 2;
|
||||
|
||||
auto k1 = bounds.removeFromTop (singleKnobH);
|
||||
thresholdKnob.setBounds (k1.withTrimmedBottom (14).reduced (4, 0));
|
||||
threshLabel.setBounds (k1.getX(), k1.getBottom() - 14, k1.getWidth(), 14);
|
||||
|
||||
auto k2 = bounds.removeFromTop (singleKnobH);
|
||||
gainKnob.setBounds (k2.withTrimmedBottom (14).reduced (4, 0));
|
||||
gainLabel.setBounds (k2.getX(), k2.getBottom() - 14, k2.getWidth(), 14);
|
||||
|
||||
// Gap between knobs and combos
|
||||
bounds.removeFromTop (6);
|
||||
|
||||
// Middle: 3 combos stacked (~30%)
|
||||
int comboSectionH = (int) (availH * 0.30f);
|
||||
int comboH = comboSectionH / 3;
|
||||
|
||||
auto r1 = bounds.removeFromTop (comboH);
|
||||
ratioLabel.setBounds (r1.removeFromLeft (r1.getWidth() / 3));
|
||||
ratioBox.setBounds (r1.reduced (2, 1));
|
||||
|
||||
auto r2 = bounds.removeFromTop (comboH);
|
||||
attackLabel.setBounds (r2.removeFromLeft (r2.getWidth() / 3));
|
||||
attackBox.setBounds (r2.reduced (2, 1));
|
||||
|
||||
auto r3 = bounds.removeFromTop (comboH);
|
||||
releaseLabel.setBounds (r3.removeFromLeft (r3.getWidth() / 3));
|
||||
releaseBox.setBounds (r3.reduced (2, 1));
|
||||
|
||||
// Bottom: bypass
|
||||
auto bpRow = bounds;
|
||||
bypassLabel.setBounds (bpRow.removeFromLeft (bpRow.getWidth() / 2));
|
||||
bypassToggle.setBounds (bpRow.reduced (4, 2));
|
||||
}
|
||||
|
||||
void DiscretePanel::paint (juce::Graphics& g)
|
||||
{
|
||||
auto bounds = getLocalBounds().toFloat();
|
||||
g.setColour (InstaShadowLookAndFeel::bgMedium.withAlpha (0.5f));
|
||||
g.fillRoundedRectangle (bounds, 4.0f);
|
||||
g.setColour (InstaShadowLookAndFeel::bgLight.withAlpha (0.3f));
|
||||
g.drawRoundedRectangle (bounds, 4.0f, 1.0f);
|
||||
}
|
||||
24
Source/DiscretePanel.h
Normal file
24
Source/DiscretePanel.h
Normal file
@@ -0,0 +1,24 @@
|
||||
#pragma once
|
||||
#include <JuceHeader.h>
|
||||
#include "LookAndFeel.h"
|
||||
|
||||
class DiscretePanel : public juce::Component
|
||||
{
|
||||
public:
|
||||
DiscretePanel();
|
||||
void resized() override;
|
||||
void paint (juce::Graphics& g) override;
|
||||
|
||||
juce::Slider thresholdKnob;
|
||||
juce::Slider gainKnob;
|
||||
juce::ComboBox ratioBox;
|
||||
juce::ComboBox attackBox;
|
||||
juce::ComboBox releaseBox;
|
||||
juce::ToggleButton bypassToggle;
|
||||
|
||||
private:
|
||||
juce::Label titleLabel;
|
||||
juce::Label threshLabel, gainLabel, ratioLabel, attackLabel, releaseLabel, bypassLabel;
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (DiscretePanel)
|
||||
};
|
||||
59
Source/GRMeter.h
Normal file
59
Source/GRMeter.h
Normal file
@@ -0,0 +1,59 @@
|
||||
#pragma once
|
||||
#include <JuceHeader.h>
|
||||
|
||||
class GRMeter : public juce::Component
|
||||
{
|
||||
public:
|
||||
void setGainReduction (float grDb)
|
||||
{
|
||||
float clamped = juce::jlimit (-30.0f, 0.0f, grDb);
|
||||
float normalised = -clamped / 30.0f;
|
||||
currentGr = std::max (normalised, currentGr * 0.92f);
|
||||
if (normalised > peakGr) peakGr = normalised;
|
||||
else peakGr *= 0.998f;
|
||||
repaint();
|
||||
}
|
||||
|
||||
void setBarColour (juce::Colour c) { barColour = c; }
|
||||
void setLabel (const juce::String& text) { label = text; }
|
||||
|
||||
void paint (juce::Graphics& g) override
|
||||
{
|
||||
auto bounds = getLocalBounds().toFloat().reduced (1);
|
||||
|
||||
// Background
|
||||
g.setColour (juce::Colour (0xff111122));
|
||||
g.fillRoundedRectangle (bounds, 2.0f);
|
||||
|
||||
// GR bar (fills from right to left)
|
||||
float w = bounds.getWidth() * currentGr;
|
||||
auto filled = bounds.withLeft (bounds.getRight() - w);
|
||||
g.setColour (barColour);
|
||||
g.fillRoundedRectangle (filled, 2.0f);
|
||||
|
||||
// Peak hold line
|
||||
if (peakGr > 0.01f)
|
||||
{
|
||||
float peakX = bounds.getRight() - bounds.getWidth() * peakGr;
|
||||
g.setColour (juce::Colours::white.withAlpha (0.8f));
|
||||
g.fillRect (peakX, bounds.getY(), 1.5f, bounds.getHeight());
|
||||
}
|
||||
|
||||
// Label
|
||||
g.setColour (juce::Colour (0xffe0e0e0).withAlpha (0.7f));
|
||||
g.setFont (11.0f);
|
||||
g.drawText (label, bounds.reduced (4, 0), juce::Justification::centredLeft);
|
||||
|
||||
// dB readout
|
||||
float dbVal = -currentGr * 30.0f;
|
||||
if (currentGr > 0.001f)
|
||||
g.drawText (juce::String (dbVal, 1) + " dB", bounds.reduced (4, 0),
|
||||
juce::Justification::centredRight);
|
||||
}
|
||||
|
||||
private:
|
||||
float currentGr = 0.0f;
|
||||
float peakGr = 0.0f;
|
||||
juce::Colour barColour { 0xffff8833 };
|
||||
juce::String label;
|
||||
};
|
||||
372
Source/LookAndFeel.cpp
Normal file
372
Source/LookAndFeel.cpp
Normal file
@@ -0,0 +1,372 @@
|
||||
#include "LookAndFeel.h"
|
||||
#include "BinaryData.h"
|
||||
|
||||
InstaShadowLookAndFeel::InstaShadowLookAndFeel()
|
||||
{
|
||||
typefaceRegular = juce::Typeface::createSystemTypefaceFor (
|
||||
BinaryData::RajdhaniRegular_ttf, BinaryData::RajdhaniRegular_ttfSize);
|
||||
typefaceMedium = juce::Typeface::createSystemTypefaceFor (
|
||||
BinaryData::RajdhaniMedium_ttf, BinaryData::RajdhaniMedium_ttfSize);
|
||||
typefaceBold = juce::Typeface::createSystemTypefaceFor (
|
||||
BinaryData::RajdhaniBold_ttf, BinaryData::RajdhaniBold_ttfSize);
|
||||
|
||||
setColour (juce::ResizableWindow::backgroundColourId, bgDark);
|
||||
setColour (juce::Label::textColourId, textPrimary);
|
||||
setColour (juce::TextButton::buttonColourId, bgMedium);
|
||||
setColour (juce::TextButton::textColourOffId, textPrimary);
|
||||
setColour (juce::ComboBox::backgroundColourId, bgMedium);
|
||||
setColour (juce::ComboBox::textColourId, textPrimary);
|
||||
setColour (juce::ComboBox::outlineColourId, bgLight);
|
||||
setColour (juce::PopupMenu::backgroundColourId, bgMedium);
|
||||
setColour (juce::PopupMenu::textColourId, textPrimary);
|
||||
setColour (juce::PopupMenu::highlightedBackgroundColourId, bgLight);
|
||||
|
||||
generateNoiseTexture();
|
||||
}
|
||||
|
||||
juce::Typeface::Ptr InstaShadowLookAndFeel::getTypefaceForFont (const juce::Font& font)
|
||||
{
|
||||
if (font.isBold())
|
||||
return typefaceBold;
|
||||
return typefaceRegular;
|
||||
}
|
||||
|
||||
juce::Font InstaShadowLookAndFeel::getRegularFont (float height) const
|
||||
{
|
||||
return juce::Font (juce::FontOptions (typefaceRegular).withHeight (height));
|
||||
}
|
||||
|
||||
juce::Font InstaShadowLookAndFeel::getMediumFont (float height) const
|
||||
{
|
||||
return juce::Font (juce::FontOptions (typefaceMedium).withHeight (height));
|
||||
}
|
||||
|
||||
juce::Font InstaShadowLookAndFeel::getBoldFont (float height) const
|
||||
{
|
||||
return juce::Font (juce::FontOptions (typefaceBold).withHeight (height));
|
||||
}
|
||||
|
||||
void InstaShadowLookAndFeel::generateNoiseTexture()
|
||||
{
|
||||
const int texW = 256, texH = 256;
|
||||
noiseTexture = juce::Image (juce::Image::ARGB, texW, texH, true);
|
||||
|
||||
juce::Random rng (42);
|
||||
|
||||
for (int y = 0; y < texH; ++y)
|
||||
{
|
||||
for (int x = 0; x < texW; ++x)
|
||||
{
|
||||
float noise = rng.nextFloat() * 0.06f;
|
||||
bool crossA = ((x + y) % 4 == 0);
|
||||
bool crossB = ((x - y + 256) % 4 == 0);
|
||||
float pattern = (crossA || crossB) ? 0.03f : 0.0f;
|
||||
float alpha = noise + pattern;
|
||||
noiseTexture.setPixelAt (x, y, juce::Colour::fromFloatRGBA (1.0f, 1.0f, 1.0f, alpha));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void InstaShadowLookAndFeel::drawBackgroundTexture (juce::Graphics& g, juce::Rectangle<int> area)
|
||||
{
|
||||
for (int y = area.getY(); y < area.getBottom(); y += noiseTexture.getHeight())
|
||||
for (int x = area.getX(); x < area.getRight(); x += noiseTexture.getWidth())
|
||||
g.drawImageAt (noiseTexture, x, y);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Rotary slider (3D metal knob)
|
||||
// ============================================================
|
||||
|
||||
void InstaShadowLookAndFeel::drawRotarySlider (juce::Graphics& g, int x, int y, int width, int height,
|
||||
float sliderPos, float rotaryStartAngle,
|
||||
float rotaryEndAngle, juce::Slider& slider)
|
||||
{
|
||||
float knobSize = std::min ((float) width, (float) height);
|
||||
float s = knobSize / 60.0f;
|
||||
float margin = std::max (4.0f, 6.0f * s);
|
||||
|
||||
auto bounds = juce::Rectangle<int> (x, y, width, height).toFloat().reduced (margin);
|
||||
auto radius = std::min (bounds.getWidth(), bounds.getHeight()) / 2.0f;
|
||||
auto cx = bounds.getCentreX();
|
||||
auto cy = bounds.getCentreY();
|
||||
auto angle = rotaryStartAngle + sliderPos * (rotaryEndAngle - rotaryStartAngle);
|
||||
|
||||
auto knobType = slider.getProperties() [knobTypeProperty].toString();
|
||||
bool isDark = (knobType == "dark");
|
||||
|
||||
juce::Colour arcColour = isDark ? juce::Colour (0xff4488ff) : juce::Colour (0xffff8833);
|
||||
juce::Colour arcBgColour = isDark ? juce::Colour (0xff1a2a44) : juce::Colour (0xff2a1a0a);
|
||||
juce::Colour bodyTop = isDark ? juce::Colour (0xff3a3a4a) : juce::Colour (0xff5a4a3a);
|
||||
juce::Colour bodyBottom = isDark ? juce::Colour (0xff1a1a2a) : juce::Colour (0xff2a1a0a);
|
||||
juce::Colour rimColour = isDark ? juce::Colour (0xff555566) : juce::Colour (0xff886644);
|
||||
juce::Colour highlightCol = isDark ? juce::Colour (0x33aabbff) : juce::Colour (0x44ffcc88);
|
||||
juce::Colour pointerColour = isDark ? juce::Colour (0xff66aaff) : juce::Colour (0xffffaa44);
|
||||
|
||||
float arcW = std::max (1.5f, 2.5f * s);
|
||||
float glowW1 = std::max (3.0f, 10.0f * s);
|
||||
float hotW = std::max (0.8f, 1.2f * s);
|
||||
float ptrW = std::max (1.2f, 2.0f * s);
|
||||
float bodyRadius = radius * 0.72f;
|
||||
|
||||
// 1. Drop shadow
|
||||
g.setColour (juce::Colours::black.withAlpha (0.35f));
|
||||
g.fillEllipse (cx - bodyRadius + 1, cy - bodyRadius + 2, bodyRadius * 2, bodyRadius * 2);
|
||||
|
||||
// 2. Outer arc track
|
||||
{
|
||||
juce::Path arcBg;
|
||||
arcBg.addCentredArc (cx, cy, radius - 1, radius - 1, 0.0f,
|
||||
rotaryStartAngle, rotaryEndAngle, true);
|
||||
g.setColour (arcBgColour);
|
||||
g.strokePath (arcBg, juce::PathStrokeType (arcW, juce::PathStrokeType::curved,
|
||||
juce::PathStrokeType::rounded));
|
||||
}
|
||||
|
||||
// 3. Outer arc value with glow
|
||||
if (sliderPos > 0.01f)
|
||||
{
|
||||
juce::Path arcVal;
|
||||
arcVal.addCentredArc (cx, cy, radius - 1, radius - 1, 0.0f,
|
||||
rotaryStartAngle, angle, true);
|
||||
|
||||
const int numGlowLayers = 8;
|
||||
for (int i = 0; i < numGlowLayers; ++i)
|
||||
{
|
||||
float t = (float) i / (float) (numGlowLayers - 1);
|
||||
float layerWidth = glowW1 * (1.0f - t * 0.7f);
|
||||
float layerAlpha = 0.03f + t * t * 0.35f;
|
||||
g.setColour (arcColour.withAlpha (layerAlpha));
|
||||
g.strokePath (arcVal, juce::PathStrokeType (layerWidth, juce::PathStrokeType::curved,
|
||||
juce::PathStrokeType::rounded));
|
||||
}
|
||||
|
||||
g.setColour (arcColour);
|
||||
g.strokePath (arcVal, juce::PathStrokeType (arcW, juce::PathStrokeType::curved,
|
||||
juce::PathStrokeType::rounded));
|
||||
|
||||
g.setColour (arcColour.brighter (0.6f).withAlpha (0.5f));
|
||||
g.strokePath (arcVal, juce::PathStrokeType (hotW, juce::PathStrokeType::curved,
|
||||
juce::PathStrokeType::rounded));
|
||||
}
|
||||
|
||||
// 4. Knob body
|
||||
{
|
||||
juce::ColourGradient bodyGrad (bodyTop, cx, cy - bodyRadius * 0.5f,
|
||||
bodyBottom, cx, cy + bodyRadius, true);
|
||||
g.setGradientFill (bodyGrad);
|
||||
g.fillEllipse (cx - bodyRadius, cy - bodyRadius, bodyRadius * 2, bodyRadius * 2);
|
||||
}
|
||||
|
||||
// 5. Rim
|
||||
g.setColour (rimColour.withAlpha (0.6f));
|
||||
g.drawEllipse (cx - bodyRadius, cy - bodyRadius, bodyRadius * 2, bodyRadius * 2, std::max (0.8f, 1.2f * s));
|
||||
|
||||
// 6. Inner shadow
|
||||
{
|
||||
float innerR = bodyRadius * 0.85f;
|
||||
juce::ColourGradient innerGrad (juce::Colours::black.withAlpha (0.15f), cx, cy - innerR * 0.3f,
|
||||
juce::Colours::transparentBlack, cx, cy + innerR, true);
|
||||
g.setGradientFill (innerGrad);
|
||||
g.fillEllipse (cx - innerR, cy - innerR, innerR * 2, innerR * 2);
|
||||
}
|
||||
|
||||
// 7. Top highlight
|
||||
{
|
||||
float hlRadius = bodyRadius * 0.55f;
|
||||
float hlY = cy - bodyRadius * 0.35f;
|
||||
juce::ColourGradient hlGrad (highlightCol, cx, hlY - hlRadius * 0.5f,
|
||||
juce::Colours::transparentBlack, cx, hlY + hlRadius, true);
|
||||
g.setGradientFill (hlGrad);
|
||||
g.fillEllipse (cx - hlRadius, hlY - hlRadius * 0.6f, hlRadius * 2, hlRadius * 1.2f);
|
||||
}
|
||||
|
||||
// 8. Pointer with glow
|
||||
{
|
||||
float pointerLen = bodyRadius * 0.75f;
|
||||
|
||||
for (int i = 0; i < 4; ++i)
|
||||
{
|
||||
float t = (float) i / 3.0f;
|
||||
float gw = ptrW * (2.0f - t * 1.5f);
|
||||
float alpha = 0.02f + t * t * 0.15f;
|
||||
|
||||
juce::Path glowLayer;
|
||||
glowLayer.addRoundedRectangle (-gw, -pointerLen, gw * 2, pointerLen * 0.55f, gw * 0.5f);
|
||||
glowLayer.applyTransform (juce::AffineTransform::rotation (angle).translated (cx, cy));
|
||||
g.setColour (pointerColour.withAlpha (alpha));
|
||||
g.fillPath (glowLayer);
|
||||
}
|
||||
|
||||
{
|
||||
juce::Path pointer;
|
||||
pointer.addRoundedRectangle (-ptrW * 0.5f, -pointerLen, ptrW, pointerLen * 0.55f, ptrW * 0.5f);
|
||||
pointer.applyTransform (juce::AffineTransform::rotation (angle).translated (cx, cy));
|
||||
g.setColour (pointerColour);
|
||||
g.fillPath (pointer);
|
||||
}
|
||||
|
||||
{
|
||||
juce::Path hotCenter;
|
||||
float hw = ptrW * 0.3f;
|
||||
hotCenter.addRoundedRectangle (-hw, -pointerLen, hw * 2, pointerLen * 0.5f, hw);
|
||||
hotCenter.applyTransform (juce::AffineTransform::rotation (angle).translated (cx, cy));
|
||||
g.setColour (pointerColour.brighter (0.7f).withAlpha (0.6f));
|
||||
g.fillPath (hotCenter);
|
||||
}
|
||||
}
|
||||
|
||||
// 9. Center cap
|
||||
{
|
||||
float capR = bodyRadius * 0.18f;
|
||||
juce::ColourGradient capGrad (rimColour.brighter (0.3f), cx, cy - capR,
|
||||
bodyBottom, cx, cy + capR, false);
|
||||
g.setGradientFill (capGrad);
|
||||
g.fillEllipse (cx - capR, cy - capR, capR * 2, capR * 2);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Button style
|
||||
// ============================================================
|
||||
|
||||
void InstaShadowLookAndFeel::drawButtonBackground (juce::Graphics& g, juce::Button& button,
|
||||
const juce::Colour& backgroundColour,
|
||||
bool shouldDrawButtonAsHighlighted,
|
||||
bool shouldDrawButtonAsDown)
|
||||
{
|
||||
auto bounds = button.getLocalBounds().toFloat().reduced (0.5f);
|
||||
|
||||
auto baseColour = backgroundColour;
|
||||
if (shouldDrawButtonAsDown)
|
||||
baseColour = baseColour.brighter (0.2f);
|
||||
else if (shouldDrawButtonAsHighlighted)
|
||||
baseColour = baseColour.brighter (0.1f);
|
||||
|
||||
juce::ColourGradient grad (baseColour.brighter (0.05f), 0, bounds.getY(),
|
||||
baseColour.darker (0.1f), 0, bounds.getBottom(), false);
|
||||
g.setGradientFill (grad);
|
||||
g.fillRoundedRectangle (bounds, 4.0f);
|
||||
|
||||
g.setColour (bgLight.withAlpha (shouldDrawButtonAsHighlighted ? 0.8f : 0.5f));
|
||||
g.drawRoundedRectangle (bounds, 4.0f, 1.0f);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Toggle button — glowing switch
|
||||
// ============================================================
|
||||
|
||||
void InstaShadowLookAndFeel::drawToggleButton (juce::Graphics& g, juce::ToggleButton& button,
|
||||
bool shouldDrawButtonAsHighlighted,
|
||||
bool /*shouldDrawButtonAsDown*/)
|
||||
{
|
||||
auto bounds = button.getLocalBounds().toFloat();
|
||||
float h = std::min (bounds.getHeight() * 0.6f, 14.0f);
|
||||
float w = h * 1.8f;
|
||||
float trackR = h * 0.5f;
|
||||
|
||||
float sx = bounds.getX() + (bounds.getWidth() - w) * 0.5f;
|
||||
float sy = bounds.getCentreY() - h * 0.5f;
|
||||
auto trackBounds = juce::Rectangle<float> (sx, sy, w, h);
|
||||
|
||||
bool isOn = button.getToggleState();
|
||||
auto onColour = accent;
|
||||
auto offColour = bgLight;
|
||||
|
||||
float targetPos = isOn ? 1.0f : 0.0f;
|
||||
float animPos = (float) button.getProperties().getWithDefault ("animPos", targetPos);
|
||||
animPos += (targetPos - animPos) * 0.25f;
|
||||
if (std::abs (animPos - targetPos) < 0.01f) animPos = targetPos;
|
||||
button.getProperties().set ("animPos", animPos);
|
||||
|
||||
if (std::abs (animPos - targetPos) > 0.005f)
|
||||
button.repaint();
|
||||
|
||||
float thumbR = h * 0.4f;
|
||||
float thumbX = sx + trackR + animPos * (w - trackR * 2);
|
||||
float thumbY = sy + h * 0.5f;
|
||||
float glowIntensity = animPos;
|
||||
|
||||
if (glowIntensity > 0.01f)
|
||||
{
|
||||
for (int i = 0; i < 3; ++i)
|
||||
{
|
||||
float t = (float) i / 2.0f;
|
||||
float expand = (1.0f - t) * 1.5f;
|
||||
float alpha = (0.04f + t * t * 0.1f) * glowIntensity;
|
||||
g.setColour (onColour.withAlpha (alpha));
|
||||
g.fillRoundedRectangle (trackBounds.expanded (expand), trackR + expand);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
juce::Colour offCol = offColour.withAlpha (0.3f);
|
||||
juce::Colour onCol = onColour.withAlpha (0.35f);
|
||||
juce::Colour trackCol = offCol.interpolatedWith (onCol, glowIntensity);
|
||||
if (shouldDrawButtonAsHighlighted)
|
||||
trackCol = trackCol.brighter (0.15f);
|
||||
g.setColour (trackCol);
|
||||
g.fillRoundedRectangle (trackBounds, trackR);
|
||||
|
||||
g.setColour (offColour.withAlpha (0.4f).interpolatedWith (onColour.withAlpha (0.5f), glowIntensity));
|
||||
g.drawRoundedRectangle (trackBounds, trackR, 0.8f);
|
||||
}
|
||||
|
||||
if (glowIntensity > 0.01f)
|
||||
{
|
||||
for (int i = 0; i < 3; ++i)
|
||||
{
|
||||
float t = (float) i / 2.0f;
|
||||
float r = thumbR * (1.5f - t * 0.5f);
|
||||
float alpha = (0.05f + t * t * 0.12f) * glowIntensity;
|
||||
g.setColour (onColour.withAlpha (alpha));
|
||||
g.fillEllipse (thumbX - r, thumbY - r, r * 2, r * 2);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
juce::Colour thumbTopOff (0xff555566), thumbBotOff (0xff333344);
|
||||
juce::Colour thumbTopOn = onColour.brighter (0.3f), thumbBotOn = onColour.darker (0.2f);
|
||||
juce::ColourGradient thumbGrad (
|
||||
thumbTopOff.interpolatedWith (thumbTopOn, glowIntensity), thumbX, thumbY - thumbR,
|
||||
thumbBotOff.interpolatedWith (thumbBotOn, glowIntensity), thumbX, thumbY + thumbR, false);
|
||||
g.setGradientFill (thumbGrad);
|
||||
g.fillEllipse (thumbX - thumbR, thumbY - thumbR, thumbR * 2, thumbR * 2);
|
||||
|
||||
g.setColour (juce::Colour (0xff666677).withAlpha (0.5f).interpolatedWith (onColour.withAlpha (0.6f), glowIntensity));
|
||||
g.drawEllipse (thumbX - thumbR, thumbY - thumbR, thumbR * 2, thumbR * 2, 0.8f);
|
||||
|
||||
float hlR = thumbR * 0.4f;
|
||||
g.setColour (juce::Colours::white.withAlpha (0.1f + 0.15f * glowIntensity));
|
||||
g.fillEllipse (thumbX - hlR, thumbY - thumbR * 0.6f - hlR * 0.3f, hlR * 2, hlR * 1.2f);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// ComboBox style
|
||||
// ============================================================
|
||||
|
||||
void InstaShadowLookAndFeel::drawComboBox (juce::Graphics& g, int width, int height, bool /*isButtonDown*/,
|
||||
int /*buttonX*/, int /*buttonY*/, int /*buttonW*/, int /*buttonH*/,
|
||||
juce::ComboBox& box)
|
||||
{
|
||||
auto bounds = juce::Rectangle<float> (0, 0, (float) width, (float) height);
|
||||
|
||||
g.setColour (bgMedium);
|
||||
g.fillRoundedRectangle (bounds, 4.0f);
|
||||
|
||||
g.setColour (bgLight.withAlpha (0.5f));
|
||||
g.drawRoundedRectangle (bounds.reduced (0.5f), 4.0f, 1.0f);
|
||||
|
||||
// Arrow
|
||||
float arrowSize = height * 0.3f;
|
||||
float arrowX = (float) width - height * 0.6f;
|
||||
float arrowY = ((float) height - arrowSize) * 0.5f;
|
||||
|
||||
juce::Path arrow;
|
||||
arrow.addTriangle (arrowX, arrowY,
|
||||
arrowX + arrowSize, arrowY,
|
||||
arrowX + arrowSize * 0.5f, arrowY + arrowSize);
|
||||
g.setColour (textSecondary);
|
||||
g.fillPath (arrow);
|
||||
}
|
||||
54
Source/LookAndFeel.h
Normal file
54
Source/LookAndFeel.h
Normal file
@@ -0,0 +1,54 @@
|
||||
#pragma once
|
||||
#include <JuceHeader.h>
|
||||
|
||||
class InstaShadowLookAndFeel : public juce::LookAndFeel_V4
|
||||
{
|
||||
public:
|
||||
// Colour palette
|
||||
static inline const juce::Colour bgDark { 0xff1a1a2e };
|
||||
static inline const juce::Colour bgMedium { 0xff16213e };
|
||||
static inline const juce::Colour bgLight { 0xff0f3460 };
|
||||
static inline const juce::Colour textPrimary { 0xffe0e0e0 };
|
||||
static inline const juce::Colour textSecondary { 0xff888899 };
|
||||
static inline const juce::Colour accent { 0xff00ff88 };
|
||||
|
||||
// Knob type property key
|
||||
static constexpr const char* knobTypeProperty = "knobType";
|
||||
|
||||
InstaShadowLookAndFeel();
|
||||
|
||||
void drawRotarySlider (juce::Graphics& g, int x, int y, int width, int height,
|
||||
float sliderPosProportional, float rotaryStartAngle,
|
||||
float rotaryEndAngle, juce::Slider& slider) override;
|
||||
|
||||
void drawButtonBackground (juce::Graphics& g, juce::Button& button,
|
||||
const juce::Colour& backgroundColour,
|
||||
bool shouldDrawButtonAsHighlighted,
|
||||
bool shouldDrawButtonAsDown) override;
|
||||
|
||||
void drawToggleButton (juce::Graphics& g, juce::ToggleButton& button,
|
||||
bool shouldDrawButtonAsHighlighted,
|
||||
bool shouldDrawButtonAsDown) override;
|
||||
|
||||
void drawComboBox (juce::Graphics& g, int width, int height, bool isButtonDown,
|
||||
int buttonX, int buttonY, int buttonW, int buttonH,
|
||||
juce::ComboBox& box) override;
|
||||
|
||||
// Custom fonts
|
||||
juce::Font getRegularFont (float height) const;
|
||||
juce::Font getMediumFont (float height) const;
|
||||
juce::Font getBoldFont (float height) const;
|
||||
|
||||
// Background texture
|
||||
void drawBackgroundTexture (juce::Graphics& g, juce::Rectangle<int> area);
|
||||
|
||||
juce::Typeface::Ptr getTypefaceForFont (const juce::Font& font) override;
|
||||
|
||||
private:
|
||||
juce::Typeface::Ptr typefaceRegular;
|
||||
juce::Typeface::Ptr typefaceMedium;
|
||||
juce::Typeface::Ptr typefaceBold;
|
||||
|
||||
juce::Image noiseTexture;
|
||||
void generateNoiseTexture();
|
||||
};
|
||||
146
Source/NeedleVuMeter.h
Normal file
146
Source/NeedleVuMeter.h
Normal file
@@ -0,0 +1,146 @@
|
||||
#pragma once
|
||||
#include <JuceHeader.h>
|
||||
|
||||
// ============================================================
|
||||
// Analog-style needle VU meter (semicircular, like Shadow Hills)
|
||||
// ============================================================
|
||||
class NeedleVuMeter : public juce::Component
|
||||
{
|
||||
public:
|
||||
void setLevel (float linearLevel)
|
||||
{
|
||||
// Convert to dB, map to needle position
|
||||
float db = (linearLevel > 0.0001f)
|
||||
? 20.0f * std::log10 (linearLevel)
|
||||
: -60.0f;
|
||||
|
||||
// VU range: -20 to +3 dB → 0.0 to 1.0
|
||||
float target = juce::jlimit (0.0f, 1.0f, (db + 20.0f) / 23.0f);
|
||||
|
||||
// Smooth needle movement (ballistic)
|
||||
if (target > needlePos)
|
||||
needlePos += (target - needlePos) * 0.07f; // slow attack (inertia)
|
||||
else
|
||||
needlePos += (target - needlePos) * 0.05f; // moderate release
|
||||
|
||||
repaint();
|
||||
}
|
||||
|
||||
void setLabel (const juce::String& text) { label = text; }
|
||||
|
||||
void paint (juce::Graphics& g) override
|
||||
{
|
||||
auto bounds = getLocalBounds().toFloat().reduced (2);
|
||||
float w = bounds.getWidth();
|
||||
float h = bounds.getHeight();
|
||||
|
||||
// Meter face background (warm cream)
|
||||
float arcH = h * 0.85f;
|
||||
auto faceRect = bounds.withHeight (arcH);
|
||||
|
||||
g.setColour (juce::Colour (0xff1a1a22));
|
||||
g.fillRoundedRectangle (bounds, 4.0f);
|
||||
|
||||
// Cream arc area
|
||||
auto arcArea = faceRect.reduced (6, 4);
|
||||
{
|
||||
juce::ColourGradient grad (juce::Colour (0xfff0e8d0), arcArea.getCentreX(), arcArea.getY(),
|
||||
juce::Colour (0xffd8d0b8), arcArea.getCentreX(), arcArea.getBottom(), false);
|
||||
g.setGradientFill (grad);
|
||||
g.fillRoundedRectangle (arcArea, 3.0f);
|
||||
}
|
||||
|
||||
// Arc center point (bottom center of arc area)
|
||||
float cx = arcArea.getCentreX();
|
||||
float cy = arcArea.getBottom() - 4.0f;
|
||||
float radius = std::min (arcArea.getWidth() * 0.45f, arcArea.getHeight() * 0.8f);
|
||||
|
||||
// Scale markings
|
||||
float startAngle = juce::MathConstants<float>::pi * 1.25f; // -225 deg
|
||||
float endAngle = juce::MathConstants<float>::pi * 1.75f; // -315 deg (sweep right)
|
||||
|
||||
// Draw scale ticks and labels
|
||||
g.setFont (std::max (6.0f, h * 0.045f));
|
||||
const float dbValues[] = { -20, -10, -7, -5, -3, -1, 0, 1, 2, 3 };
|
||||
const int numTicks = 10;
|
||||
|
||||
for (int i = 0; i < numTicks; ++i)
|
||||
{
|
||||
float norm = (dbValues[i] + 20.0f) / 23.0f;
|
||||
float angle = startAngle + norm * (endAngle - startAngle);
|
||||
|
||||
float cosA = std::cos (angle);
|
||||
float sinA = std::sin (angle);
|
||||
|
||||
float innerR = radius * 0.82f;
|
||||
float outerR = radius * 0.95f;
|
||||
bool isMajor = (dbValues[i] == -20 || dbValues[i] == -10 || dbValues[i] == -5
|
||||
|| dbValues[i] == 0 || dbValues[i] == 3);
|
||||
|
||||
// Tick line
|
||||
g.setColour (dbValues[i] >= 0 ? juce::Colour (0xffcc3333) : juce::Colour (0xff333333));
|
||||
float tickInner = isMajor ? innerR * 0.9f : innerR;
|
||||
g.drawLine (cx + cosA * tickInner, cy + sinA * tickInner,
|
||||
cx + cosA * outerR, cy + sinA * outerR,
|
||||
isMajor ? 1.5f : 0.8f);
|
||||
|
||||
// Label for major ticks
|
||||
if (isMajor)
|
||||
{
|
||||
float labelR = radius * 0.7f;
|
||||
float lx = cx + cosA * labelR;
|
||||
float ly = cy + sinA * labelR;
|
||||
juce::String txt = (dbValues[i] > 0 ? "+" : "") + juce::String ((int) dbValues[i]);
|
||||
g.setColour (dbValues[i] >= 0 ? juce::Colour (0xffcc3333) : juce::Colour (0xff444444));
|
||||
g.drawText (txt, (int) (lx - 12), (int) (ly - 6), 24, 12, juce::Justification::centred);
|
||||
}
|
||||
}
|
||||
|
||||
// Red zone arc (0 to +3 dB)
|
||||
{
|
||||
float redStart = startAngle + (20.0f / 23.0f) * (endAngle - startAngle);
|
||||
juce::Path redArc;
|
||||
redArc.addCentredArc (cx, cy, radius * 0.92f, radius * 0.92f, 0,
|
||||
redStart, endAngle, true);
|
||||
g.setColour (juce::Colour (0x33ff3333));
|
||||
g.strokePath (redArc, juce::PathStrokeType (radius * 0.08f));
|
||||
}
|
||||
|
||||
// Needle
|
||||
{
|
||||
float angle = startAngle + needlePos * (endAngle - startAngle);
|
||||
float cosA = std::cos (angle);
|
||||
float sinA = std::sin (angle);
|
||||
|
||||
// Needle shadow
|
||||
g.setColour (juce::Colours::black.withAlpha (0.3f));
|
||||
g.drawLine (cx + 1, cy + 1,
|
||||
cx + cosA * radius * 0.88f + 1, cy + sinA * radius * 0.88f + 1,
|
||||
2.0f);
|
||||
|
||||
// Needle
|
||||
g.setColour (juce::Colour (0xff222222));
|
||||
g.drawLine (cx, cy,
|
||||
cx + cosA * radius * 0.88f, cy + sinA * radius * 0.88f,
|
||||
1.5f);
|
||||
|
||||
// Needle pivot dot
|
||||
g.setColour (juce::Colour (0xff333333));
|
||||
g.fillEllipse (cx - 3, cy - 3, 6, 6);
|
||||
}
|
||||
|
||||
// Label below
|
||||
g.setColour (juce::Colour (0xffaaaaaa));
|
||||
g.setFont (std::max (7.0f, h * 0.05f));
|
||||
g.drawText (label, bounds.getX(), bounds.getBottom() - h * 0.18f,
|
||||
bounds.getWidth(), h * 0.15f, juce::Justification::centred);
|
||||
|
||||
// Border
|
||||
g.setColour (juce::Colour (0xff333344));
|
||||
g.drawRoundedRectangle (bounds, 4.0f, 1.0f);
|
||||
}
|
||||
|
||||
private:
|
||||
float needlePos = 0.0f; // 0..1 mapped to -20..+3 dB
|
||||
juce::String label;
|
||||
};
|
||||
96
Source/OpticalCell.cpp
Normal file
96
Source/OpticalCell.cpp
Normal file
@@ -0,0 +1,96 @@
|
||||
#include "OpticalCell.h"
|
||||
|
||||
OpticalCell::OpticalCell() {}
|
||||
|
||||
void OpticalCell::prepare (double sampleRate)
|
||||
{
|
||||
sr = sampleRate;
|
||||
dt = 1.0 / (2.0 * sampleRate); // 2x oversampled
|
||||
reset();
|
||||
}
|
||||
|
||||
void OpticalCell::reset()
|
||||
{
|
||||
q_el = 0.0;
|
||||
R_cds = R_cds_dark;
|
||||
lightOutput = 0.0;
|
||||
lightHistory = 0.0;
|
||||
currentGain = 1.0;
|
||||
prevInput = 0.0f;
|
||||
}
|
||||
|
||||
void OpticalCell::solveImplicitStep (double inputLevel)
|
||||
{
|
||||
// Drive from sidechain level (rectified, normalized 0..1+)
|
||||
double i_drive = std::max (0.0, inputLevel);
|
||||
|
||||
// Store old state for trapezoidal rule
|
||||
double q_old = q_el;
|
||||
double f_old = (i_drive - q_old / C_el) / R_el;
|
||||
|
||||
// Initial guess (forward Euler)
|
||||
double q_new = q_old + dt * f_old;
|
||||
|
||||
// Newton-Raphson iteration (implicit trapezoidal)
|
||||
for (int iter = 0; iter < maxNewtonIter; ++iter)
|
||||
{
|
||||
double f_new = (i_drive - q_new / C_el) / R_el;
|
||||
double residual = q_new - q_old - (dt / 2.0) * (f_old + f_new);
|
||||
double jacobian = 1.0 + dt / (2.0 * R_el * C_el);
|
||||
|
||||
double delta = residual / jacobian;
|
||||
q_new -= delta;
|
||||
|
||||
if (std::abs (delta) < newtonTol)
|
||||
break;
|
||||
}
|
||||
|
||||
q_el = q_new;
|
||||
|
||||
// Light output proportional to STORED ENERGY (voltage^2)
|
||||
// This ensures continuous light output at steady state
|
||||
double voltage = q_el / C_el;
|
||||
lightOutput = eta_el * voltage * voltage;
|
||||
|
||||
// Update memory (illumination history) — explicit Euler
|
||||
lightHistory += dt * (lightOutput - lightHistory) / memoryTau;
|
||||
lightHistory = std::max (0.0, lightHistory);
|
||||
|
||||
// CdS resistance: R = k * (L_eff)^(-gamma)
|
||||
// Memory effect: effective light includes accumulated history
|
||||
double L_eff = lightOutput + memoryAlpha * lightHistory;
|
||||
double epsilon = 1.0e-10;
|
||||
|
||||
R_cds = k_cds * std::pow (L_eff + epsilon, -gamma);
|
||||
R_cds = std::clamp (R_cds, R_cds_min, R_cds_dark);
|
||||
}
|
||||
|
||||
void OpticalCell::updateGain()
|
||||
{
|
||||
// Shunt attenuator: high R_cds = pass, low R_cds = attenuate
|
||||
currentGain = R_cds / (R_fixed + R_cds);
|
||||
}
|
||||
|
||||
float OpticalCell::processSample (float sidechainLevel, float thresholdLinear)
|
||||
{
|
||||
// Envelope above threshold
|
||||
float envelope = std::max (0.0f, sidechainLevel - thresholdLinear);
|
||||
|
||||
// 2x oversampling: interpolate and process two sub-samples
|
||||
float mid = (prevInput + envelope) * 0.5f;
|
||||
prevInput = envelope;
|
||||
|
||||
solveImplicitStep ((double) mid);
|
||||
solveImplicitStep ((double) envelope);
|
||||
|
||||
updateGain();
|
||||
|
||||
return (float) currentGain;
|
||||
}
|
||||
|
||||
float OpticalCell::getGainReductionDb() const
|
||||
{
|
||||
if (currentGain <= 0.0)
|
||||
return -60.0f;
|
||||
return (float) (20.0 * std::log10 (currentGain));
|
||||
}
|
||||
71
Source/OpticalCell.h
Normal file
71
Source/OpticalCell.h
Normal file
@@ -0,0 +1,71 @@
|
||||
#pragma once
|
||||
#include <cmath>
|
||||
#include <algorithm>
|
||||
|
||||
// ============================================================
|
||||
// Port-Hamiltonian T4B Electro-Optical Compressor Model
|
||||
//
|
||||
// Models the EL panel (capacitive energy store) coupled to a
|
||||
// CdS photoresistor (nonlinear dissipation) using implicit
|
||||
// trapezoidal integration with Newton-Raphson iteration.
|
||||
//
|
||||
// All parameters are normalized for audio-level (0..1) operation.
|
||||
// ============================================================
|
||||
class OpticalCell
|
||||
{
|
||||
public:
|
||||
OpticalCell();
|
||||
|
||||
void prepare (double sampleRate);
|
||||
void reset();
|
||||
|
||||
// Process one sample: sidechain input (linear), returns gain (0..1)
|
||||
float processSample (float sidechainLevel, float thresholdLinear);
|
||||
|
||||
// Current GR in dB (for metering, always <= 0)
|
||||
float getGainReductionDb() const;
|
||||
|
||||
private:
|
||||
double sr = 44100.0;
|
||||
double dt = 1.0 / 88200.0; // 2x oversampled time step
|
||||
|
||||
// ---- Port-Hamiltonian State (normalized units) ----
|
||||
|
||||
// EL panel: capacitive energy store
|
||||
double q_el = 0.0; // charge state
|
||||
double C_el = 0.01; // capacitance (~10ms attack with R_el=1)
|
||||
double R_el = 1.0; // series resistance (normalized)
|
||||
double eta_el = 50.0; // electro-optical efficiency (scaled for audio levels)
|
||||
|
||||
// CdS photoresistor: nonlinear dissipation
|
||||
double R_cds = 100.0; // current resistance (normalized)
|
||||
double R_cds_dark = 100.0; // dark resistance (no light → no compression)
|
||||
double R_cds_min = 0.01; // minimum resistance (max compression)
|
||||
double k_cds = 1.0; // CdS scaling factor
|
||||
double gamma = 0.7; // CdS nonlinearity exponent
|
||||
|
||||
// Memory effect (CdS illumination history)
|
||||
double lightHistory = 0.0;
|
||||
double memoryTau = 2.0; // memory time constant (seconds)
|
||||
double memoryAlpha = 0.3; // history influence factor
|
||||
|
||||
// Light state
|
||||
double lightOutput = 0.0;
|
||||
|
||||
// Signal path impedance for gain (normalized)
|
||||
double R_fixed = 1.0;
|
||||
|
||||
// Current gain (linear)
|
||||
double currentGain = 1.0;
|
||||
|
||||
// Implicit solver
|
||||
static constexpr int maxNewtonIter = 5;
|
||||
static constexpr double newtonTol = 1.0e-8;
|
||||
|
||||
// 2x oversampling
|
||||
float prevInput = 0.0f;
|
||||
|
||||
// Core methods
|
||||
void solveImplicitStep (double inputLevel);
|
||||
void updateGain();
|
||||
};
|
||||
73
Source/OpticalPanel.cpp
Normal file
73
Source/OpticalPanel.cpp
Normal file
@@ -0,0 +1,73 @@
|
||||
#include "OpticalPanel.h"
|
||||
|
||||
OpticalPanel::OpticalPanel()
|
||||
{
|
||||
titleLabel.setText ("OPTICAL", juce::dontSendNotification);
|
||||
titleLabel.setJustificationType (juce::Justification::centred);
|
||||
titleLabel.setColour (juce::Label::textColourId, InstaShadowLookAndFeel::textPrimary);
|
||||
addAndMakeVisible (titleLabel);
|
||||
|
||||
auto setupKnob = [this] (juce::Slider& knob, juce::Label& label, const juce::String& name,
|
||||
double min, double max, double def, const juce::String& type,
|
||||
const juce::String& suffix = "")
|
||||
{
|
||||
knob.setSliderStyle (juce::Slider::RotaryHorizontalVerticalDrag);
|
||||
knob.setTextBoxStyle (juce::Slider::TextBoxBelow, false, 60, 14);
|
||||
knob.setRange (min, max);
|
||||
knob.setValue (def);
|
||||
knob.setTextValueSuffix (suffix);
|
||||
knob.getProperties().set (InstaShadowLookAndFeel::knobTypeProperty, type);
|
||||
addAndMakeVisible (knob);
|
||||
|
||||
label.setText (name, juce::dontSendNotification);
|
||||
label.setJustificationType (juce::Justification::centred);
|
||||
label.setColour (juce::Label::textColourId, InstaShadowLookAndFeel::textSecondary);
|
||||
addAndMakeVisible (label);
|
||||
};
|
||||
|
||||
setupKnob (thresholdKnob, threshLabel, "Threshold", -40.0, 0.0, -20.0, "orange", " dB");
|
||||
setupKnob (gainKnob, gainLabel, "Gain", 0.0, 20.0, 0.0, "orange", " dB");
|
||||
setupKnob (scHpfKnob, hpfLabel, "SC HPF", 20.0, 500.0, 90.0, "dark", " Hz");
|
||||
scHpfKnob.setSkewFactorFromMidPoint (100.0);
|
||||
|
||||
bypassToggle.setButtonText ("");
|
||||
addAndMakeVisible (bypassToggle);
|
||||
bypassLabel.setText ("Bypass", juce::dontSendNotification);
|
||||
bypassLabel.setJustificationType (juce::Justification::centred);
|
||||
bypassLabel.setColour (juce::Label::textColourId, InstaShadowLookAndFeel::textSecondary);
|
||||
addAndMakeVisible (bypassLabel);
|
||||
}
|
||||
|
||||
void OpticalPanel::resized()
|
||||
{
|
||||
auto bounds = getLocalBounds().reduced (4);
|
||||
titleLabel.setBounds (bounds.removeFromTop (18));
|
||||
|
||||
int availH = bounds.getHeight() - 22;
|
||||
int knobH = availH / 3;
|
||||
|
||||
auto k1 = bounds.removeFromTop (knobH);
|
||||
thresholdKnob.setBounds (k1.withTrimmedBottom (14).reduced (4, 0));
|
||||
threshLabel.setBounds (k1.getX(), k1.getBottom() - 14, k1.getWidth(), 14);
|
||||
|
||||
auto k2 = bounds.removeFromTop (knobH);
|
||||
gainKnob.setBounds (k2.withTrimmedBottom (14).reduced (4, 0));
|
||||
gainLabel.setBounds (k2.getX(), k2.getBottom() - 14, k2.getWidth(), 14);
|
||||
|
||||
auto k3 = bounds.removeFromTop (knobH);
|
||||
scHpfKnob.setBounds (k3.withTrimmedBottom (14).reduced (4, 0));
|
||||
hpfLabel.setBounds (k3.getX(), k3.getBottom() - 14, k3.getWidth(), 14);
|
||||
|
||||
auto bpRow = bounds;
|
||||
bypassLabel.setBounds (bpRow.removeFromLeft (bpRow.getWidth() / 2));
|
||||
bypassToggle.setBounds (bpRow.reduced (4, 2));
|
||||
}
|
||||
|
||||
void OpticalPanel::paint (juce::Graphics& g)
|
||||
{
|
||||
auto bounds = getLocalBounds().toFloat();
|
||||
g.setColour (InstaShadowLookAndFeel::bgMedium.withAlpha (0.5f));
|
||||
g.fillRoundedRectangle (bounds, 4.0f);
|
||||
g.setColour (InstaShadowLookAndFeel::bgLight.withAlpha (0.3f));
|
||||
g.drawRoundedRectangle (bounds, 4.0f, 1.0f);
|
||||
}
|
||||
22
Source/OpticalPanel.h
Normal file
22
Source/OpticalPanel.h
Normal file
@@ -0,0 +1,22 @@
|
||||
#pragma once
|
||||
#include <JuceHeader.h>
|
||||
#include "LookAndFeel.h"
|
||||
|
||||
class OpticalPanel : public juce::Component
|
||||
{
|
||||
public:
|
||||
OpticalPanel();
|
||||
void resized() override;
|
||||
void paint (juce::Graphics& g) override;
|
||||
|
||||
juce::Slider thresholdKnob;
|
||||
juce::Slider gainKnob;
|
||||
juce::Slider scHpfKnob;
|
||||
juce::ToggleButton bypassToggle;
|
||||
|
||||
private:
|
||||
juce::Label titleLabel;
|
||||
juce::Label threshLabel, gainLabel, hpfLabel, bypassLabel;
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (OpticalPanel)
|
||||
};
|
||||
52
Source/OutputPanel.cpp
Normal file
52
Source/OutputPanel.cpp
Normal file
@@ -0,0 +1,52 @@
|
||||
#include "OutputPanel.h"
|
||||
|
||||
OutputPanel::OutputPanel()
|
||||
{
|
||||
titleLabel.setText ("OUTPUT", juce::dontSendNotification);
|
||||
titleLabel.setJustificationType (juce::Justification::centredLeft);
|
||||
titleLabel.setColour (juce::Label::textColourId, InstaShadowLookAndFeel::textPrimary);
|
||||
addAndMakeVisible (titleLabel);
|
||||
|
||||
outputGainKnob.setSliderStyle (juce::Slider::RotaryHorizontalVerticalDrag);
|
||||
outputGainKnob.setTextBoxStyle (juce::Slider::TextBoxBelow, false, 60, 14);
|
||||
outputGainKnob.setRange (-12.0, 12.0);
|
||||
outputGainKnob.setValue (0.0);
|
||||
outputGainKnob.setTextValueSuffix (" dB");
|
||||
outputGainKnob.getProperties().set (InstaShadowLookAndFeel::knobTypeProperty, "orange");
|
||||
addAndMakeVisible (outputGainKnob);
|
||||
|
||||
gainLabel.setText ("Gain", juce::dontSendNotification);
|
||||
gainLabel.setJustificationType (juce::Justification::centred);
|
||||
gainLabel.setColour (juce::Label::textColourId, InstaShadowLookAndFeel::textSecondary);
|
||||
addAndMakeVisible (gainLabel);
|
||||
|
||||
addAndMakeVisible (vuMeter);
|
||||
}
|
||||
|
||||
void OutputPanel::resized()
|
||||
{
|
||||
auto bounds = getLocalBounds().reduced (4);
|
||||
titleLabel.setBounds (bounds.removeFromTop (20));
|
||||
|
||||
int halfW = bounds.getWidth() / 2;
|
||||
|
||||
auto knobArea = bounds.removeFromLeft (halfW);
|
||||
auto knobRect = knobArea.withTrimmedBottom (16);
|
||||
outputGainKnob.setBounds (knobRect.reduced (2));
|
||||
gainLabel.setBounds (knobArea.getX(), knobRect.getBottom() - 2, knobArea.getWidth(), 16);
|
||||
|
||||
// Small VU meter, limited height
|
||||
auto vuArea = bounds.reduced (8, 4);
|
||||
int maxVuH = std::min (vuArea.getHeight(), (int) (knobRect.getHeight() * 0.6f));
|
||||
int maxVuW = std::min (vuArea.getWidth(), 40);
|
||||
vuMeter.setBounds (vuArea.withSizeKeepingCentre (maxVuW, maxVuH));
|
||||
}
|
||||
|
||||
void OutputPanel::paint (juce::Graphics& g)
|
||||
{
|
||||
auto bounds = getLocalBounds().toFloat();
|
||||
g.setColour (InstaShadowLookAndFeel::bgMedium.withAlpha (0.5f));
|
||||
g.fillRoundedRectangle (bounds, 4.0f);
|
||||
g.setColour (InstaShadowLookAndFeel::bgLight.withAlpha (0.3f));
|
||||
g.drawRoundedRectangle (bounds, 4.0f, 1.0f);
|
||||
}
|
||||
21
Source/OutputPanel.h
Normal file
21
Source/OutputPanel.h
Normal file
@@ -0,0 +1,21 @@
|
||||
#pragma once
|
||||
#include <JuceHeader.h>
|
||||
#include "LookAndFeel.h"
|
||||
#include "VuMeter.h"
|
||||
|
||||
class OutputPanel : public juce::Component
|
||||
{
|
||||
public:
|
||||
OutputPanel();
|
||||
void resized() override;
|
||||
void paint (juce::Graphics& g) override;
|
||||
|
||||
juce::Slider outputGainKnob;
|
||||
VuMeter vuMeter;
|
||||
|
||||
private:
|
||||
juce::Label titleLabel;
|
||||
juce::Label gainLabel;
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (OutputPanel)
|
||||
};
|
||||
210
Source/PluginEditor.cpp
Normal file
210
Source/PluginEditor.cpp
Normal file
@@ -0,0 +1,210 @@
|
||||
#include "PluginEditor.h"
|
||||
|
||||
InstaShadowEditor::InstaShadowEditor (InstaShadowProcessor& p)
|
||||
: AudioProcessorEditor (&p), processor (p)
|
||||
{
|
||||
setLookAndFeel (&lookAndFeel);
|
||||
setSize (1000, 600);
|
||||
setResizable (true, true);
|
||||
setResizeLimits (800, 500, 1400, 900);
|
||||
|
||||
// Title
|
||||
titleLabel.setText ("INSTASHADOW", juce::dontSendNotification);
|
||||
titleLabel.setJustificationType (juce::Justification::centredLeft);
|
||||
titleLabel.setColour (juce::Label::textColourId, InstaShadowLookAndFeel::accent);
|
||||
addAndMakeVisible (titleLabel);
|
||||
|
||||
versionLabel.setText (kInstaShadowVersion, juce::dontSendNotification);
|
||||
versionLabel.setJustificationType (juce::Justification::centredLeft);
|
||||
versionLabel.setColour (juce::Label::textColourId, InstaShadowLookAndFeel::textSecondary);
|
||||
addAndMakeVisible (versionLabel);
|
||||
|
||||
// Link
|
||||
linkToggle.setToggleState (true, juce::dontSendNotification);
|
||||
addAndMakeVisible (linkToggle);
|
||||
linkLabel.setText ("LINK", juce::dontSendNotification);
|
||||
linkLabel.setJustificationType (juce::Justification::centredRight);
|
||||
linkLabel.setColour (juce::Label::textColourId, InstaShadowLookAndFeel::textSecondary);
|
||||
addAndMakeVisible (linkLabel);
|
||||
|
||||
// Bypass
|
||||
bypassToggle.setToggleState (false, juce::dontSendNotification);
|
||||
addAndMakeVisible (bypassToggle);
|
||||
bypassLabel.setText ("BYPASS", juce::dontSendNotification);
|
||||
bypassLabel.setJustificationType (juce::Justification::centredRight);
|
||||
bypassLabel.setColour (juce::Label::textColourId, InstaShadowLookAndFeel::textSecondary);
|
||||
addAndMakeVisible (bypassLabel);
|
||||
|
||||
// Panels
|
||||
addAndMakeVisible (opticalPanel);
|
||||
addAndMakeVisible (discretePanel);
|
||||
addAndMakeVisible (transformerPanel);
|
||||
addAndMakeVisible (outputPanel);
|
||||
|
||||
// Needle VU meters
|
||||
vuMeterL.setLabel ("L");
|
||||
addAndMakeVisible (vuMeterL);
|
||||
vuMeterR.setLabel ("R");
|
||||
addAndMakeVisible (vuMeterR);
|
||||
|
||||
// GR meters (compact bars)
|
||||
optoGrMeter.setLabel ("OPTICAL GR");
|
||||
optoGrMeter.setBarColour (juce::Colour (0xffff8833));
|
||||
addAndMakeVisible (optoGrMeter);
|
||||
|
||||
vcaGrMeter.setLabel ("DISCRETE GR");
|
||||
vcaGrMeter.setBarColour (juce::Colour (0xff4488ff));
|
||||
addAndMakeVisible (vcaGrMeter);
|
||||
|
||||
syncKnobsToEngine();
|
||||
startTimerHz (30);
|
||||
}
|
||||
|
||||
InstaShadowEditor::~InstaShadowEditor()
|
||||
{
|
||||
setLookAndFeel (nullptr);
|
||||
stopTimer();
|
||||
}
|
||||
|
||||
void InstaShadowEditor::syncKnobsToEngine()
|
||||
{
|
||||
auto& eng = processor.getEngine();
|
||||
|
||||
opticalPanel.thresholdKnob.setValue (eng.optoThresholdDb.load(), juce::dontSendNotification);
|
||||
opticalPanel.gainKnob.setValue (eng.optoGainDb.load(), juce::dontSendNotification);
|
||||
opticalPanel.scHpfKnob.setValue (eng.optoScHpfHz.load(), juce::dontSendNotification);
|
||||
opticalPanel.bypassToggle.setToggleState (eng.optoBypass.load(), juce::dontSendNotification);
|
||||
|
||||
discretePanel.thresholdKnob.setValue (eng.vcaThresholdDb.load(), juce::dontSendNotification);
|
||||
discretePanel.gainKnob.setValue (eng.vcaGainDb.load(), juce::dontSendNotification);
|
||||
discretePanel.ratioBox.setSelectedId (eng.vcaRatioIndex.load() + 1, juce::dontSendNotification);
|
||||
discretePanel.attackBox.setSelectedId (eng.vcaAttackIndex.load() + 1, juce::dontSendNotification);
|
||||
discretePanel.releaseBox.setSelectedId (eng.vcaReleaseIndex.load() + 1, juce::dontSendNotification);
|
||||
discretePanel.bypassToggle.setToggleState (eng.vcaBypass.load(), juce::dontSendNotification);
|
||||
|
||||
transformerPanel.setSelectedType (eng.transformerType.load());
|
||||
outputPanel.outputGainKnob.setValue (eng.outputGainDb.load(), juce::dontSendNotification);
|
||||
linkToggle.setToggleState (eng.stereoLink.load(), juce::dontSendNotification);
|
||||
bypassToggle.setToggleState (eng.globalBypass.load(), juce::dontSendNotification);
|
||||
}
|
||||
|
||||
void InstaShadowEditor::syncEngineFromKnobs()
|
||||
{
|
||||
auto& eng = processor.getEngine();
|
||||
|
||||
eng.optoThresholdDb.store ((float) opticalPanel.thresholdKnob.getValue());
|
||||
eng.optoGainDb.store ((float) opticalPanel.gainKnob.getValue());
|
||||
eng.optoScHpfHz.store ((float) opticalPanel.scHpfKnob.getValue());
|
||||
eng.optoBypass.store (opticalPanel.bypassToggle.getToggleState());
|
||||
|
||||
eng.vcaThresholdDb.store ((float) discretePanel.thresholdKnob.getValue());
|
||||
eng.vcaGainDb.store ((float) discretePanel.gainKnob.getValue());
|
||||
eng.vcaRatioIndex.store (discretePanel.ratioBox.getSelectedId() - 1);
|
||||
eng.vcaAttackIndex.store (discretePanel.attackBox.getSelectedId() - 1);
|
||||
eng.vcaReleaseIndex.store (discretePanel.releaseBox.getSelectedId() - 1);
|
||||
eng.vcaBypass.store (discretePanel.bypassToggle.getToggleState());
|
||||
|
||||
eng.transformerType.store (transformerPanel.getSelectedType());
|
||||
eng.outputGainDb.store ((float) outputPanel.outputGainKnob.getValue());
|
||||
eng.stereoLink.store (linkToggle.getToggleState());
|
||||
eng.globalBypass.store (bypassToggle.getToggleState());
|
||||
}
|
||||
|
||||
void InstaShadowEditor::timerCallback()
|
||||
{
|
||||
syncEngineFromKnobs();
|
||||
|
||||
auto& eng = processor.getEngine();
|
||||
|
||||
// Needle VU meters
|
||||
vuMeterL.setLevel (eng.outputLevelL.load());
|
||||
vuMeterR.setLevel (eng.outputLevelR.load());
|
||||
|
||||
// GR meters
|
||||
optoGrMeter.setGainReduction (eng.optoGrDb.load());
|
||||
vcaGrMeter.setGainReduction (eng.vcaGrDb.load());
|
||||
|
||||
// Output panel VU
|
||||
outputPanel.vuMeter.setLevel (eng.outputLevelL.load(), eng.outputLevelR.load());
|
||||
}
|
||||
|
||||
void InstaShadowEditor::paint (juce::Graphics& g)
|
||||
{
|
||||
g.fillAll (InstaShadowLookAndFeel::bgDark);
|
||||
lookAndFeel.drawBackgroundTexture (g, getLocalBounds());
|
||||
|
||||
float scale = (float) getHeight() / 600.0f;
|
||||
int topBarH = (int) (36.0f * scale);
|
||||
|
||||
g.setColour (InstaShadowLookAndFeel::bgMedium.withAlpha (0.7f));
|
||||
g.fillRect (0, 0, getWidth(), topBarH);
|
||||
g.setColour (InstaShadowLookAndFeel::bgLight.withAlpha (0.3f));
|
||||
g.drawHorizontalLine (topBarH, 0, (float) getWidth());
|
||||
}
|
||||
|
||||
void InstaShadowEditor::resized()
|
||||
{
|
||||
auto bounds = getLocalBounds();
|
||||
float scale = (float) getHeight() / 600.0f;
|
||||
|
||||
int topBarH = (int) (36.0f * scale);
|
||||
int pad = (int) (6.0f * scale);
|
||||
|
||||
// ===== TOP BAR =====
|
||||
auto topBar = bounds.removeFromTop (topBarH).reduced (pad, 0);
|
||||
titleLabel.setFont (lookAndFeel.getBoldFont (20.0f * scale));
|
||||
titleLabel.setBounds (topBar.removeFromLeft ((int) (140 * scale)));
|
||||
versionLabel.setFont (lookAndFeel.getRegularFont (13.0f * scale));
|
||||
versionLabel.setBounds (topBar.removeFromLeft ((int) (50 * scale)));
|
||||
|
||||
auto bypassArea = topBar.removeFromRight ((int) (30 * scale));
|
||||
bypassToggle.setBounds (bypassArea);
|
||||
bypassLabel.setFont (lookAndFeel.getRegularFont (11.0f * scale));
|
||||
bypassLabel.setBounds (topBar.removeFromRight ((int) (55 * scale)));
|
||||
topBar.removeFromRight (pad);
|
||||
auto linkArea = topBar.removeFromRight ((int) (30 * scale));
|
||||
linkToggle.setBounds (linkArea);
|
||||
linkLabel.setFont (lookAndFeel.getRegularFont (11.0f * scale));
|
||||
linkLabel.setBounds (topBar.removeFromRight ((int) (40 * scale)));
|
||||
|
||||
auto content = bounds.reduced (pad);
|
||||
|
||||
// ===== MAIN: 3 columns =====
|
||||
int sideW = (int) (content.getWidth() * 0.22f);
|
||||
auto mainRow = content;
|
||||
|
||||
// Left: Optical
|
||||
opticalPanel.setBounds (mainRow.removeFromLeft (sideW));
|
||||
mainRow.removeFromLeft (pad);
|
||||
|
||||
// Right: Discrete
|
||||
auto rightCol = mainRow.removeFromRight (sideW);
|
||||
discretePanel.setBounds (rightCol);
|
||||
mainRow.removeFromRight (pad);
|
||||
|
||||
// Center column: VU meters, GR bars, Transformer, Output — all stacked
|
||||
auto centerArea = mainRow;
|
||||
|
||||
// Two needle VU meters side by side (~30%)
|
||||
int vuH = (int) (centerArea.getHeight() * 0.30f);
|
||||
auto vuRow = centerArea.removeFromTop (vuH);
|
||||
int vuW = (vuRow.getWidth() - pad) / 2;
|
||||
vuMeterL.setBounds (vuRow.removeFromLeft (vuW));
|
||||
vuRow.removeFromLeft (pad);
|
||||
vuMeterR.setBounds (vuRow);
|
||||
centerArea.removeFromTop (pad);
|
||||
|
||||
// Two GR meter bars (~15%)
|
||||
int grBarH = (int) (centerArea.getHeight() * 0.12f);
|
||||
optoGrMeter.setBounds (centerArea.removeFromTop (grBarH));
|
||||
centerArea.removeFromTop (pad);
|
||||
vcaGrMeter.setBounds (centerArea.removeFromTop (grBarH));
|
||||
centerArea.removeFromTop (pad);
|
||||
|
||||
// Transformer + Output side by side in remaining center space
|
||||
auto botCenter = centerArea;
|
||||
int botHalf = botCenter.getWidth() / 2;
|
||||
transformerPanel.setBounds (botCenter.removeFromLeft (botHalf - pad / 2));
|
||||
botCenter.removeFromLeft (pad);
|
||||
outputPanel.setBounds (botCenter);
|
||||
}
|
||||
55
Source/PluginEditor.h
Normal file
55
Source/PluginEditor.h
Normal file
@@ -0,0 +1,55 @@
|
||||
#pragma once
|
||||
#include <JuceHeader.h>
|
||||
#include "PluginProcessor.h"
|
||||
#include "LookAndFeel.h"
|
||||
#include "OpticalPanel.h"
|
||||
#include "DiscretePanel.h"
|
||||
#include "TransformerPanel.h"
|
||||
#include "OutputPanel.h"
|
||||
#include "GRMeter.h"
|
||||
#include "NeedleVuMeter.h"
|
||||
|
||||
static constexpr const char* kInstaShadowVersion = "v1.0";
|
||||
|
||||
class InstaShadowEditor : public juce::AudioProcessorEditor,
|
||||
public juce::Timer
|
||||
{
|
||||
public:
|
||||
explicit InstaShadowEditor (InstaShadowProcessor&);
|
||||
~InstaShadowEditor() override;
|
||||
|
||||
void paint (juce::Graphics&) override;
|
||||
void resized() override;
|
||||
void timerCallback() override;
|
||||
|
||||
private:
|
||||
InstaShadowProcessor& processor;
|
||||
InstaShadowLookAndFeel lookAndFeel;
|
||||
|
||||
// Top bar
|
||||
juce::Label titleLabel;
|
||||
juce::Label versionLabel;
|
||||
juce::ToggleButton linkToggle;
|
||||
juce::Label linkLabel;
|
||||
juce::ToggleButton bypassToggle;
|
||||
juce::Label bypassLabel;
|
||||
|
||||
// Side panels
|
||||
OpticalPanel opticalPanel;
|
||||
DiscretePanel discretePanel;
|
||||
|
||||
// Center: needle VU meters + GR bars
|
||||
NeedleVuMeter vuMeterL;
|
||||
NeedleVuMeter vuMeterR;
|
||||
GRMeter optoGrMeter;
|
||||
GRMeter vcaGrMeter;
|
||||
|
||||
// Bottom panels
|
||||
TransformerPanel transformerPanel;
|
||||
OutputPanel outputPanel;
|
||||
|
||||
void syncKnobsToEngine();
|
||||
void syncEngineFromKnobs();
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (InstaShadowEditor)
|
||||
};
|
||||
87
Source/PluginProcessor.cpp
Normal file
87
Source/PluginProcessor.cpp
Normal file
@@ -0,0 +1,87 @@
|
||||
#include "PluginProcessor.h"
|
||||
#include "PluginEditor.h"
|
||||
|
||||
InstaShadowProcessor::InstaShadowProcessor()
|
||||
: AudioProcessor (BusesProperties()
|
||||
.withInput ("Input", juce::AudioChannelSet::stereo(), true)
|
||||
.withOutput ("Output", juce::AudioChannelSet::stereo(), true))
|
||||
{
|
||||
}
|
||||
|
||||
InstaShadowProcessor::~InstaShadowProcessor() {}
|
||||
|
||||
void InstaShadowProcessor::prepareToPlay (double sampleRate, int samplesPerBlock)
|
||||
{
|
||||
engine.prepare (sampleRate, samplesPerBlock);
|
||||
}
|
||||
|
||||
void InstaShadowProcessor::releaseResources() {}
|
||||
|
||||
bool InstaShadowProcessor::isBusesLayoutSupported (const BusesLayout& layouts) const
|
||||
{
|
||||
if (layouts.getMainOutputChannelSet() != juce::AudioChannelSet::stereo())
|
||||
return false;
|
||||
if (layouts.getMainInputChannelSet() != juce::AudioChannelSet::stereo())
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
void InstaShadowProcessor::processBlock (juce::AudioBuffer<float>& buffer, juce::MidiBuffer&)
|
||||
{
|
||||
juce::ScopedNoDenormals noDenormals;
|
||||
engine.processBlock (buffer);
|
||||
}
|
||||
|
||||
juce::AudioProcessorEditor* InstaShadowProcessor::createEditor()
|
||||
{
|
||||
return new InstaShadowEditor (*this);
|
||||
}
|
||||
|
||||
void InstaShadowProcessor::getStateInformation (juce::MemoryBlock& destData)
|
||||
{
|
||||
auto xml = std::make_unique<juce::XmlElement> ("InstaShadowState");
|
||||
|
||||
xml->setAttribute ("optoThreshold", (double) engine.optoThresholdDb.load());
|
||||
xml->setAttribute ("optoGain", (double) engine.optoGainDb.load());
|
||||
xml->setAttribute ("optoScHpf", (double) engine.optoScHpfHz.load());
|
||||
xml->setAttribute ("optoBypass", engine.optoBypass.load());
|
||||
xml->setAttribute ("vcaThreshold", (double) engine.vcaThresholdDb.load());
|
||||
xml->setAttribute ("vcaGain", (double) engine.vcaGainDb.load());
|
||||
xml->setAttribute ("vcaRatio", engine.vcaRatioIndex.load());
|
||||
xml->setAttribute ("vcaAttack", engine.vcaAttackIndex.load());
|
||||
xml->setAttribute ("vcaRelease", engine.vcaReleaseIndex.load());
|
||||
xml->setAttribute ("vcaBypass", engine.vcaBypass.load());
|
||||
xml->setAttribute ("transformerType", engine.transformerType.load());
|
||||
xml->setAttribute ("outputGain", (double) engine.outputGainDb.load());
|
||||
xml->setAttribute ("stereoLink", engine.stereoLink.load());
|
||||
xml->setAttribute ("globalBypass", engine.globalBypass.load());
|
||||
|
||||
copyXmlToBinary (*xml, destData);
|
||||
}
|
||||
|
||||
void InstaShadowProcessor::setStateInformation (const void* data, int sizeInBytes)
|
||||
{
|
||||
auto xml = getXmlFromBinary (data, sizeInBytes);
|
||||
if (xml == nullptr || ! xml->hasTagName ("InstaShadowState"))
|
||||
return;
|
||||
|
||||
engine.optoThresholdDb.store ((float) xml->getDoubleAttribute ("optoThreshold", -20.0));
|
||||
engine.optoGainDb.store ((float) xml->getDoubleAttribute ("optoGain", 0.0));
|
||||
engine.optoScHpfHz.store ((float) xml->getDoubleAttribute ("optoScHpf", 90.0));
|
||||
engine.optoBypass.store (xml->getBoolAttribute ("optoBypass", false));
|
||||
engine.vcaThresholdDb.store ((float) xml->getDoubleAttribute ("vcaThreshold", -20.0));
|
||||
engine.vcaGainDb.store ((float) xml->getDoubleAttribute ("vcaGain", 0.0));
|
||||
engine.vcaRatioIndex.store (xml->getIntAttribute ("vcaRatio", 1));
|
||||
engine.vcaAttackIndex.store (xml->getIntAttribute ("vcaAttack", 2));
|
||||
engine.vcaReleaseIndex.store (xml->getIntAttribute ("vcaRelease", 2));
|
||||
engine.vcaBypass.store (xml->getBoolAttribute ("vcaBypass", false));
|
||||
engine.transformerType.store (xml->getIntAttribute ("transformerType", 0));
|
||||
engine.outputGainDb.store ((float) xml->getDoubleAttribute ("outputGain", 0.0));
|
||||
engine.stereoLink.store (xml->getBoolAttribute ("stereoLink", true));
|
||||
engine.globalBypass.store (xml->getBoolAttribute ("globalBypass", false));
|
||||
}
|
||||
|
||||
juce::AudioProcessor* JUCE_CALLTYPE createPluginFilter()
|
||||
{
|
||||
return new InstaShadowProcessor();
|
||||
}
|
||||
41
Source/PluginProcessor.h
Normal file
41
Source/PluginProcessor.h
Normal file
@@ -0,0 +1,41 @@
|
||||
#pragma once
|
||||
#include <JuceHeader.h>
|
||||
#include "CompressorEngine.h"
|
||||
|
||||
class InstaShadowProcessor : public juce::AudioProcessor
|
||||
{
|
||||
public:
|
||||
InstaShadowProcessor();
|
||||
~InstaShadowProcessor() 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 { return true; }
|
||||
|
||||
const juce::String getName() const override { return JucePlugin_Name; }
|
||||
bool acceptsMidi() const override { return false; }
|
||||
bool producesMidi() const override { return false; }
|
||||
bool isMidiEffect() const override { return false; }
|
||||
double getTailLengthSeconds() const override { return 0.0; }
|
||||
|
||||
int getNumPrograms() override { return 1; }
|
||||
int getCurrentProgram() override { return 0; }
|
||||
void setCurrentProgram (int) override {}
|
||||
const juce::String getProgramName (int) override { return {}; }
|
||||
void changeProgramName (int, const juce::String&) override {}
|
||||
|
||||
void getStateInformation (juce::MemoryBlock& destData) override;
|
||||
void setStateInformation (const void* data, int sizeInBytes) override;
|
||||
|
||||
CompressorEngine& getEngine() { return engine; }
|
||||
|
||||
private:
|
||||
CompressorEngine engine;
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (InstaShadowProcessor)
|
||||
};
|
||||
84
Source/TransformerPanel.cpp
Normal file
84
Source/TransformerPanel.cpp
Normal file
@@ -0,0 +1,84 @@
|
||||
#include "TransformerPanel.h"
|
||||
|
||||
TransformerPanel::TransformerPanel()
|
||||
{
|
||||
titleLabel.setText ("TRANSFORMER", juce::dontSendNotification);
|
||||
titleLabel.setJustificationType (juce::Justification::centredLeft);
|
||||
titleLabel.setColour (juce::Label::textColourId, InstaShadowLookAndFeel::textPrimary);
|
||||
addAndMakeVisible (titleLabel);
|
||||
|
||||
auto setupButton = [this] (juce::TextButton& btn, int typeId)
|
||||
{
|
||||
btn.setClickingTogglesState (false);
|
||||
btn.onClick = [this, typeId, &btn]
|
||||
{
|
||||
if (currentType == typeId)
|
||||
currentType = 0; // deselect → Off
|
||||
else
|
||||
currentType = typeId;
|
||||
updateButtonStates();
|
||||
};
|
||||
addAndMakeVisible (btn);
|
||||
};
|
||||
|
||||
setupButton (nickelButton, 1);
|
||||
setupButton (ironButton, 2);
|
||||
setupButton (steelButton, 3);
|
||||
|
||||
updateButtonStates();
|
||||
}
|
||||
|
||||
void TransformerPanel::setSelectedType (int type)
|
||||
{
|
||||
currentType = std::clamp (type, 0, 3);
|
||||
updateButtonStates();
|
||||
}
|
||||
|
||||
void TransformerPanel::updateButtonStates()
|
||||
{
|
||||
auto setColour = [this] (juce::TextButton& btn, bool active)
|
||||
{
|
||||
if (active)
|
||||
{
|
||||
btn.setColour (juce::TextButton::buttonColourId, InstaShadowLookAndFeel::accent.withAlpha (0.4f));
|
||||
btn.setColour (juce::TextButton::textColourOffId, InstaShadowLookAndFeel::accent);
|
||||
}
|
||||
else
|
||||
{
|
||||
btn.setColour (juce::TextButton::buttonColourId, InstaShadowLookAndFeel::bgMedium);
|
||||
btn.setColour (juce::TextButton::textColourOffId, InstaShadowLookAndFeel::textSecondary);
|
||||
}
|
||||
};
|
||||
|
||||
setColour (nickelButton, currentType == 1);
|
||||
setColour (ironButton, currentType == 2);
|
||||
setColour (steelButton, currentType == 3);
|
||||
|
||||
repaint();
|
||||
}
|
||||
|
||||
void TransformerPanel::resized()
|
||||
{
|
||||
auto bounds = getLocalBounds().reduced (4);
|
||||
titleLabel.setBounds (bounds.removeFromTop (20));
|
||||
|
||||
auto btnArea = bounds.reduced (8, 4);
|
||||
int btnH = std::min ((btnArea.getHeight() - 8) / 3, 28);
|
||||
int totalH = btnH * 3 + 8;
|
||||
auto centred = btnArea.withSizeKeepingCentre (std::min (btnArea.getWidth(), 120), totalH);
|
||||
|
||||
nickelButton.setBounds (centred.removeFromTop (btnH).reduced (0, 1));
|
||||
centred.removeFromTop (4);
|
||||
ironButton.setBounds (centred.removeFromTop (btnH).reduced (0, 1));
|
||||
centred.removeFromTop (4);
|
||||
steelButton.setBounds (centred.removeFromTop (btnH).reduced (0, 1));
|
||||
}
|
||||
|
||||
void TransformerPanel::paint (juce::Graphics& g)
|
||||
{
|
||||
auto bounds = getLocalBounds().toFloat();
|
||||
g.setColour (InstaShadowLookAndFeel::bgMedium.withAlpha (0.5f));
|
||||
g.fillRoundedRectangle (bounds, 4.0f);
|
||||
g.setColour (InstaShadowLookAndFeel::bgLight.withAlpha (0.3f));
|
||||
g.drawRoundedRectangle (bounds, 4.0f, 1.0f);
|
||||
}
|
||||
26
Source/TransformerPanel.h
Normal file
26
Source/TransformerPanel.h
Normal file
@@ -0,0 +1,26 @@
|
||||
#pragma once
|
||||
#include <JuceHeader.h>
|
||||
#include "LookAndFeel.h"
|
||||
|
||||
class TransformerPanel : public juce::Component
|
||||
{
|
||||
public:
|
||||
TransformerPanel();
|
||||
void resized() override;
|
||||
void paint (juce::Graphics& g) override;
|
||||
|
||||
int getSelectedType() const { return currentType; }
|
||||
void setSelectedType (int type);
|
||||
|
||||
juce::TextButton nickelButton { "NICKEL" };
|
||||
juce::TextButton ironButton { "IRON" };
|
||||
juce::TextButton steelButton { "STEEL" };
|
||||
|
||||
private:
|
||||
juce::Label titleLabel;
|
||||
int currentType = 0; // 0=Off
|
||||
|
||||
void updateButtonStates();
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (TransformerPanel)
|
||||
};
|
||||
108
Source/TransformerSaturation.cpp
Normal file
108
Source/TransformerSaturation.cpp
Normal file
@@ -0,0 +1,108 @@
|
||||
#include "TransformerSaturation.h"
|
||||
|
||||
TransformerSaturation::TransformerSaturation() {}
|
||||
|
||||
void TransformerSaturation::prepare (double sampleRate, int samplesPerBlock)
|
||||
{
|
||||
sr = sampleRate;
|
||||
oversampler.initProcessing ((size_t) samplesPerBlock);
|
||||
|
||||
// Iron tonestack: very subtle low shelf +0.2dB at 110Hz
|
||||
auto ironCoeffs = juce::dsp::IIR::Coefficients<float>::makeLowShelf (
|
||||
sampleRate, 110.0f, 0.707f, juce::Decibels::decibelsToGain (0.2f));
|
||||
ironBoostL.coefficients = ironCoeffs;
|
||||
ironBoostR.coefficients = ironCoeffs;
|
||||
|
||||
// Steel tonestack: subtle low shelf +0.4dB at 40Hz
|
||||
auto steelCoeffs = juce::dsp::IIR::Coefficients<float>::makeLowShelf (
|
||||
sampleRate, 40.0f, 0.707f, juce::Decibels::decibelsToGain (0.4f));
|
||||
steelBoostL.coefficients = steelCoeffs;
|
||||
steelBoostR.coefficients = steelCoeffs;
|
||||
}
|
||||
|
||||
void TransformerSaturation::reset()
|
||||
{
|
||||
oversampler.reset();
|
||||
ironBoostL.reset(); ironBoostR.reset();
|
||||
steelBoostL.reset(); steelBoostR.reset();
|
||||
}
|
||||
|
||||
void TransformerSaturation::processBlock (juce::AudioBuffer<float>& buffer, Type type)
|
||||
{
|
||||
if (type == Off) return;
|
||||
|
||||
const int numChannels = std::min (buffer.getNumChannels(), 2);
|
||||
const int numSamples = buffer.getNumSamples();
|
||||
|
||||
// Select parameters per type:
|
||||
// drive: how hard the tanh clips (1.0 = no effect)
|
||||
// even: 2nd harmonic amount (asymmetric warmth)
|
||||
// odd: 3rd harmonic amount (edge/presence)
|
||||
// mix: wet/dry blend (keeps effect very subtle)
|
||||
float drive, even, odd, mix;
|
||||
|
||||
switch (type)
|
||||
{
|
||||
case Nickel:
|
||||
drive = 1.05f; even = 0.002f; odd = 0.0005f; mix = 0.3f;
|
||||
break;
|
||||
case Iron:
|
||||
drive = 1.15f; even = 0.008f; odd = 0.002f; mix = 0.5f;
|
||||
break;
|
||||
case Steel:
|
||||
drive = 1.3f; even = 0.012f; odd = 0.008f; mix = 0.6f;
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
// 4x upsample
|
||||
juce::dsp::AudioBlock<float> block (buffer);
|
||||
auto oversampledBlock = oversampler.processSamplesUp (block);
|
||||
|
||||
const int osNumSamples = (int) oversampledBlock.getNumSamples();
|
||||
const int osNumChannels = std::min ((int) oversampledBlock.getNumChannels(), 2);
|
||||
|
||||
// Apply waveshaping at oversampled rate
|
||||
for (int ch = 0; ch < osNumChannels; ++ch)
|
||||
{
|
||||
float* data = oversampledBlock.getChannelPointer ((size_t) ch);
|
||||
|
||||
for (int i = 0; i < osNumSamples; ++i)
|
||||
{
|
||||
float x = data[i];
|
||||
float dry = x;
|
||||
|
||||
// Soft-clip: tanh(drive*x)/drive — unity gain at DC
|
||||
float shaped = std::tanh (drive * x) / drive;
|
||||
|
||||
// Add subtle harmonic content
|
||||
shaped += even * x * std::abs (x); // 2nd harmonic (even-order)
|
||||
shaped -= odd * x * x * x; // 3rd harmonic (odd-order)
|
||||
|
||||
// Blend dry/wet to keep the effect subtle
|
||||
data[i] = dry + (shaped - dry) * mix;
|
||||
}
|
||||
}
|
||||
|
||||
// 4x downsample
|
||||
oversampler.processSamplesDown (block);
|
||||
|
||||
// Post-saturation tonestack (very subtle EQ coloration)
|
||||
if (type == Iron)
|
||||
{
|
||||
for (int i = 0; i < numSamples; ++i)
|
||||
{
|
||||
if (numChannels > 0) buffer.setSample (0, i, ironBoostL.processSample (buffer.getSample (0, i)));
|
||||
if (numChannels > 1) buffer.setSample (1, i, ironBoostR.processSample (buffer.getSample (1, i)));
|
||||
}
|
||||
}
|
||||
else if (type == Steel)
|
||||
{
|
||||
for (int i = 0; i < numSamples; ++i)
|
||||
{
|
||||
if (numChannels > 0) buffer.setSample (0, i, steelBoostL.processSample (buffer.getSample (0, i)));
|
||||
if (numChannels > 1) buffer.setSample (1, i, steelBoostR.processSample (buffer.getSample (1, i)));
|
||||
}
|
||||
}
|
||||
}
|
||||
34
Source/TransformerSaturation.h
Normal file
34
Source/TransformerSaturation.h
Normal file
@@ -0,0 +1,34 @@
|
||||
#pragma once
|
||||
#include <JuceHeader.h>
|
||||
|
||||
class TransformerSaturation
|
||||
{
|
||||
public:
|
||||
enum Type { Off = 0, Nickel = 1, Iron = 2, Steel = 3 };
|
||||
|
||||
TransformerSaturation();
|
||||
|
||||
void prepare (double sampleRate, int samplesPerBlock);
|
||||
void reset();
|
||||
|
||||
// Process buffer in-place with 4x oversampling
|
||||
void processBlock (juce::AudioBuffer<float>& buffer, Type type);
|
||||
|
||||
private:
|
||||
double sr = 44100.0;
|
||||
|
||||
// 4x oversampler (stereo)
|
||||
juce::dsp::Oversampling<float> oversampler { 2, 2,
|
||||
juce::dsp::Oversampling<float>::filterHalfBandPolyphaseIIR };
|
||||
|
||||
// Subtle tonestack (post-saturation)
|
||||
juce::dsp::IIR::Filter<float> ironBoostL, ironBoostR;
|
||||
juce::dsp::IIR::Filter<float> steelBoostL, steelBoostR;
|
||||
|
||||
// Simple pre-emphasis for frequency-dependent saturation
|
||||
// Boost lows before waveshaper, compensate after
|
||||
juce::dsp::IIR::Filter<float> preEmphL, preEmphR;
|
||||
juce::dsp::IIR::Filter<float> deEmphL, deEmphR;
|
||||
|
||||
void setupPreEmphasis (double sampleRate, float boostDb);
|
||||
};
|
||||
100
Source/VCACompressor.cpp
Normal file
100
Source/VCACompressor.cpp
Normal file
@@ -0,0 +1,100 @@
|
||||
#include "VCACompressor.h"
|
||||
|
||||
VCACompressor::VCACompressor() {}
|
||||
|
||||
void VCACompressor::prepare (double sampleRate)
|
||||
{
|
||||
sr = sampleRate;
|
||||
reset();
|
||||
}
|
||||
|
||||
void VCACompressor::reset()
|
||||
{
|
||||
smoothedGrDb = 0.0;
|
||||
dualFastEnv = 0.0;
|
||||
dualSlowEnv = 0.0;
|
||||
wasCompressing = false;
|
||||
currentGrDb = 0.0f;
|
||||
}
|
||||
|
||||
double VCACompressor::makeCoeff (double timeSec) const
|
||||
{
|
||||
if (timeSec <= 0.0) return 0.0;
|
||||
return std::exp (-1.0 / (sr * timeSec));
|
||||
}
|
||||
|
||||
float VCACompressor::computeGainReduction (float inputDb, float thresholdDb, float ratio) const
|
||||
{
|
||||
// Soft-knee gain computer
|
||||
float halfKnee = kneeWidthDb * 0.5f;
|
||||
|
||||
if (inputDb < thresholdDb - halfKnee)
|
||||
{
|
||||
return 0.0f; // below threshold — no compression
|
||||
}
|
||||
else if (inputDb > thresholdDb + halfKnee)
|
||||
{
|
||||
// Above knee — full compression
|
||||
float compressed = thresholdDb + (inputDb - thresholdDb) / ratio;
|
||||
return compressed - inputDb; // negative dB value
|
||||
}
|
||||
else
|
||||
{
|
||||
// In knee region — quadratic interpolation
|
||||
float x = inputDb - thresholdDb + halfKnee;
|
||||
return ((1.0f / ratio) - 1.0f) * x * x / (2.0f * kneeWidthDb);
|
||||
}
|
||||
}
|
||||
|
||||
float VCACompressor::processSample (float sidechainDb, float thresholdDb, float ratio,
|
||||
float attackSec, float releaseSec, bool dualRelease)
|
||||
{
|
||||
// Compute desired gain reduction
|
||||
float desiredGrDb = computeGainReduction (sidechainDb, thresholdDb, ratio);
|
||||
|
||||
if (! dualRelease)
|
||||
{
|
||||
// Standard attack/release smoothing
|
||||
double coeff;
|
||||
if (desiredGrDb < smoothedGrDb)
|
||||
coeff = makeCoeff ((double) attackSec); // attacking (GR going more negative)
|
||||
else
|
||||
coeff = makeCoeff ((double) releaseSec); // releasing
|
||||
|
||||
smoothedGrDb = coeff * smoothedGrDb + (1.0 - coeff) * (double) desiredGrDb;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Dual release mode: two-stage release mimicking optical behavior
|
||||
double attackCoeff = makeCoeff ((double) attackSec);
|
||||
|
||||
if (desiredGrDb < smoothedGrDb)
|
||||
{
|
||||
// Attacking
|
||||
smoothedGrDb = attackCoeff * smoothedGrDb + (1.0 - attackCoeff) * (double) desiredGrDb;
|
||||
dualFastEnv = smoothedGrDb;
|
||||
dualSlowEnv = smoothedGrDb;
|
||||
wasCompressing = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Releasing: blend fast (~60ms) and slow (~2s) release
|
||||
double fastCoeff = makeCoeff (0.06); // 60ms — first 50-80% of recovery
|
||||
double slowCoeff = makeCoeff (2.0); // 2s — remaining recovery
|
||||
|
||||
dualFastEnv = fastCoeff * dualFastEnv + (1.0 - fastCoeff) * (double) desiredGrDb;
|
||||
dualSlowEnv = slowCoeff * dualSlowEnv + (1.0 - slowCoeff) * (double) desiredGrDb;
|
||||
|
||||
// Blend: use the DEEPER (more negative) of the two
|
||||
// This naturally creates the two-stage behavior:
|
||||
// fast env recovers quickly, slow env holds longer
|
||||
smoothedGrDb = std::min (dualFastEnv, dualSlowEnv);
|
||||
wasCompressing = false;
|
||||
}
|
||||
}
|
||||
|
||||
currentGrDb = (float) smoothedGrDb;
|
||||
|
||||
// Convert GR dB to linear gain
|
||||
return std::pow (10.0f, currentGrDb / 20.0f);
|
||||
}
|
||||
41
Source/VCACompressor.h
Normal file
41
Source/VCACompressor.h
Normal file
@@ -0,0 +1,41 @@
|
||||
#pragma once
|
||||
#include <cmath>
|
||||
#include <algorithm>
|
||||
|
||||
class VCACompressor
|
||||
{
|
||||
public:
|
||||
VCACompressor();
|
||||
|
||||
void prepare (double sampleRate);
|
||||
void reset();
|
||||
|
||||
// Process one sample: sidechainDb = input level in dB
|
||||
// Returns gain in linear scale (0..1)
|
||||
float processSample (float sidechainDb, float thresholdDb, float ratio,
|
||||
float attackSec, float releaseSec, bool dualRelease);
|
||||
|
||||
float getGainReductionDb() const { return currentGrDb; }
|
||||
|
||||
private:
|
||||
double sr = 44100.0;
|
||||
|
||||
// Gain smoothing state (dB domain)
|
||||
double smoothedGrDb = 0.0;
|
||||
|
||||
// Dual release state
|
||||
double dualFastEnv = 0.0; // fast release envelope
|
||||
double dualSlowEnv = 0.0; // slow release envelope
|
||||
bool wasCompressing = false;
|
||||
|
||||
float currentGrDb = 0.0f;
|
||||
|
||||
// Soft-knee width
|
||||
static constexpr float kneeWidthDb = 6.0f;
|
||||
|
||||
// Gain computer: returns desired GR in dB (negative value)
|
||||
float computeGainReduction (float inputDb, float thresholdDb, float ratio) const;
|
||||
|
||||
// Coefficient helpers
|
||||
double makeCoeff (double timeSec) const;
|
||||
};
|
||||
62
Source/VuMeter.h
Normal file
62
Source/VuMeter.h
Normal file
@@ -0,0 +1,62 @@
|
||||
#pragma once
|
||||
#include <JuceHeader.h>
|
||||
|
||||
class VuMeter : public juce::Component
|
||||
{
|
||||
public:
|
||||
void setLevel (float left, float right)
|
||||
{
|
||||
if (left > peakL) peakL = left;
|
||||
else peakL *= 0.995f;
|
||||
if (right > peakR) peakR = right;
|
||||
else peakR *= 0.995f;
|
||||
|
||||
levelL = std::max (left, levelL * 0.85f);
|
||||
levelR = std::max (right, levelR * 0.85f);
|
||||
repaint();
|
||||
}
|
||||
|
||||
void paint (juce::Graphics& g) override
|
||||
{
|
||||
auto bounds = getLocalBounds().toFloat().reduced (1);
|
||||
float barGap = 2.0f;
|
||||
float halfW = (bounds.getWidth() - barGap) / 2.0f;
|
||||
auto leftBar = bounds.removeFromLeft (halfW);
|
||||
bounds.removeFromLeft (barGap);
|
||||
auto rightBar = bounds;
|
||||
|
||||
drawBar (g, leftBar, levelL, peakL);
|
||||
drawBar (g, rightBar, levelR, peakR);
|
||||
}
|
||||
|
||||
private:
|
||||
float levelL = 0.0f, levelR = 0.0f;
|
||||
float peakL = 0.0f, peakR = 0.0f;
|
||||
|
||||
void drawBar (juce::Graphics& g, juce::Rectangle<float> bar, float level, float peak)
|
||||
{
|
||||
g.setColour (juce::Colour (0xff111122));
|
||||
g.fillRoundedRectangle (bar, 2.0f);
|
||||
|
||||
float displayLevel = std::pow (juce::jlimit (0.0f, 1.0f, level), 0.5f);
|
||||
float h = bar.getHeight() * displayLevel;
|
||||
auto filled = bar.withTop (bar.getBottom() - h);
|
||||
|
||||
if (displayLevel < 0.6f)
|
||||
g.setColour (juce::Colour (0xff00cc44));
|
||||
else if (displayLevel < 0.85f)
|
||||
g.setColour (juce::Colour (0xffcccc00));
|
||||
else
|
||||
g.setColour (juce::Colour (0xffff3333));
|
||||
|
||||
g.fillRoundedRectangle (filled, 2.0f);
|
||||
|
||||
float displayPeak = std::pow (juce::jlimit (0.0f, 1.0f, peak), 0.5f);
|
||||
if (displayPeak > 0.01f)
|
||||
{
|
||||
float peakY = bar.getBottom() - bar.getHeight() * displayPeak;
|
||||
g.setColour (juce::Colours::white.withAlpha (0.8f));
|
||||
g.fillRect (bar.getX(), peakY, bar.getWidth(), 1.5f);
|
||||
}
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user