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:
hariel1985
2026-03-27 16:03:24 +01:00
commit a587a43ff9
33 fájl változott, egészen pontosan 2417 új sor hozzáadva és 0 régi sor törölve

145
Source/CompressorEngine.cpp Normal file
Fájl megtekintése

@@ -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
Fájl megtekintése

@@ -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
Fájl megtekintése

@@ -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
Fájl megtekintése

@@ -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
Fájl megtekintése

@@ -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
Fájl megtekintése

@@ -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
Fájl megtekintése

@@ -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
Fájl megtekintése

@@ -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
Fájl megtekintése

@@ -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
Fájl megtekintése

@@ -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
Fájl megtekintése

@@ -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
Fájl megtekintése

@@ -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
Fájl megtekintése

@@ -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
Fájl megtekintése

@@ -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
Fájl megtekintése

@@ -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
Fájl megtekintése

@@ -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)
};

Fájl megtekintése

@@ -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
Fájl megtekintése

@@ -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)
};

Fájl megtekintése

@@ -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
Fájl megtekintése

@@ -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)
};

Fájl megtekintése

@@ -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)));
}
}
}

Fájl megtekintése

@@ -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
Fájl megtekintése

@@ -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
Fájl megtekintése

@@ -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
Fájl megtekintése

@@ -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);
}
}
};