From d750716608b5d37e5c2defc78757de46ca87ef48 Mon Sep 17 00:00:00 2001 From: hariel1985 Date: Fri, 27 Mar 2026 17:46:25 +0100 Subject: [PATCH] =?UTF-8?q?v1.1=20=E2=80=94=20Improved=20metering,=20trans?= =?UTF-8?q?former,=20and=20optical=20cell=20tuning?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Needle VU meters with spring-mass-damper physics (analog inertia) - Swappable meter modes: GR needles + input bars, or input needles + GR bars - GR bar meters fill right-to-left (0dB=empty, -30dB=full) - Input bar meters fill left-to-right with green color - Optical cell: normalized parameters (eta=50) for proper audio-level response - Transformer: removed 3-band crossover artifacts, simplified waveshaping with dry/wet mix - Nickel/Iron/Steel with distinct but subtle harmonic character - Layout: optical left, discrete right, meters center, transformer+output bottom center --- CMakeLists.txt | 2 +- Source/CompressorEngine.cpp | 7 ++ Source/CompressorEngine.h | 2 + Source/GRMeter.h | 57 ++++++++--- Source/NeedleVuMeter.h | 182 ++++++++++++++++++++++-------------- Source/PluginEditor.cpp | 101 ++++++++++++++------ Source/PluginEditor.h | 16 ++-- 7 files changed, 244 insertions(+), 123 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 3a8bc8d..15944d7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.22) -project(InstaShadow VERSION 1.0.0) +project(InstaShadow VERSION 1.1.0) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) diff --git a/Source/CompressorEngine.cpp b/Source/CompressorEngine.cpp index 75a17f3..7541350 100644 --- a/Source/CompressorEngine.cpp +++ b/Source/CompressorEngine.cpp @@ -34,6 +34,13 @@ void CompressorEngine::processBlock (juce::AudioBuffer& buffer) if (globalBypass.load() || numChannels == 0) return; + // Measure input level BEFORE any processing + inputLevelL.store (buffer.getMagnitude (0, 0, numSamples)); + if (numChannels > 1) + inputLevelR.store (buffer.getMagnitude (1, 0, numSamples)); + else + inputLevelR.store (inputLevelL.load()); + // Read parameters once per block float optoThresh = optoThresholdDb.load(); float optoGain = optoGainDb.load(); diff --git a/Source/CompressorEngine.h b/Source/CompressorEngine.h index 561fb77..31235c2 100644 --- a/Source/CompressorEngine.h +++ b/Source/CompressorEngine.h @@ -37,6 +37,8 @@ public: // --- Metering (audio → GUI) --- std::atomic optoGrDb { 0.0f }; std::atomic vcaGrDb { 0.0f }; + std::atomic inputLevelL { 0.0f }; + std::atomic inputLevelR { 0.0f }; std::atomic outputLevelL { 0.0f }; std::atomic outputLevelR { 0.0f }; diff --git a/Source/GRMeter.h b/Source/GRMeter.h index 32027c4..24d333e 100644 --- a/Source/GRMeter.h +++ b/Source/GRMeter.h @@ -6,11 +6,27 @@ class GRMeter : public juce::Component public: void setGainReduction (float grDb) { + // 0dB GR = 0.0 (empty), -30dB GR = 1.0 (full bar) + // Bar fills from RIGHT to LEFT showing how much GR 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; + float normalised = -clamped / 30.0f; // 0dB→0.0, -30dB→1.0 + currentLevel = std::max (normalised, currentLevel * 0.92f); + if (normalised > peakLevel) peakLevel = normalised; + else peakLevel *= 0.998f; + leftToRight = false; // right-to-left + repaint(); + } + + // Input level meter (left-to-right, linear level 0..1) + void setInputLevel (float linearLevel) + { + float db = (linearLevel > 0.0001f) ? 20.0f * std::log10 (linearLevel) : -60.0f; + // Map -30..0 dB to 0..1 + float normalised = juce::jlimit (0.0f, 1.0f, (db + 30.0f) / 30.0f); + currentLevel = std::max (normalised, currentLevel * 0.92f); + if (normalised > peakLevel) peakLevel = normalised; + else peakLevel *= 0.998f; + leftToRight = true; repaint(); } @@ -21,22 +37,28 @@ public: { 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); + float w = bounds.getWidth() * currentLevel; + + juce::Rectangle filled; + if (leftToRight) + filled = bounds.withWidth (w); // left to right for input level + else + filled = bounds.withLeft (bounds.getRight() - w); // right to left for GR + g.setColour (barColour); g.fillRoundedRectangle (filled, 2.0f); // Peak hold line - if (peakGr > 0.01f) + if (peakLevel > 0.01f) { - float peakX = bounds.getRight() - bounds.getWidth() * peakGr; + float peakX = leftToRight + ? bounds.getX() + bounds.getWidth() * peakLevel + : bounds.getRight() - bounds.getWidth() * peakLevel; g.setColour (juce::Colours::white.withAlpha (0.8f)); - g.fillRect (peakX, bounds.getY(), 1.5f, bounds.getHeight()); + g.fillRect (peakX - 0.75f, bounds.getY(), 1.5f, bounds.getHeight()); } // Label @@ -45,15 +67,20 @@ public: g.drawText (label, bounds.reduced (4, 0), juce::Justification::centredLeft); // dB readout - float dbVal = -currentGr * 30.0f; - if (currentGr > 0.001f) + if (currentLevel > 0.001f) + { + float dbVal = leftToRight + ? (currentLevel * 30.0f - 30.0f) // input: -30..0 dB + : (-currentLevel * 30.0f); // GR: 0..-30 dB g.drawText (juce::String (dbVal, 1) + " dB", bounds.reduced (4, 0), juce::Justification::centredRight); + } } private: - float currentGr = 0.0f; - float peakGr = 0.0f; + float currentLevel = 0.0f; + float peakLevel = 0.0f; + bool leftToRight = false; juce::Colour barColour { 0xffff8833 }; juce::String label; }; diff --git a/Source/NeedleVuMeter.h b/Source/NeedleVuMeter.h index 527547a..1b7b4a3 100644 --- a/Source/NeedleVuMeter.h +++ b/Source/NeedleVuMeter.h @@ -2,28 +2,31 @@ #include // ============================================================ -// Analog-style needle VU meter (semicircular, like Shadow Hills) +// Analog-style needle meter (semicircular) +// Two modes: VU (level) and GR (gain reduction) // ============================================================ class NeedleVuMeter : public juce::Component { public: + enum Mode { VU, GR }; + + void setMode (Mode m) { mode = m; repaint(); } + 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); + applyNeedlePhysics (target); + } - // Smooth needle movement (ballistic) - if (target > needlePos) - needlePos += (target - needlePos) * 0.07f; // slow attack (inertia) - else - needlePos += (target - needlePos) * 0.05f; // moderate release - - repaint(); + // For GR mode: pass negative dB value (e.g. -6.0 = 6dB reduction) + // Standard VU scale, needle rests at 0dB mark, moves LEFT with compression + void setGainReduction (float grDb) + { + float target = juce::jlimit (0.0f, 1.0f, (grDb + 20.0f) / 23.0f); + applyNeedlePhysics (target); } void setLabel (const juce::String& text) { label = text; } @@ -31,13 +34,12 @@ public: 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); + // Dark background g.setColour (juce::Colour (0xff1a1a22)); g.fillRoundedRectangle (bounds, 4.0f); @@ -50,61 +52,17 @@ public: 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::pi * 1.25f; // -225 deg - float endAngle = juce::MathConstants::pi * 1.75f; // -315 deg (sweep right) + float startAngle = juce::MathConstants::pi * 1.25f; + float endAngle = juce::MathConstants::pi * 1.75f; - // 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)); - } + // Always use VU scale — in GR mode the needle just starts at 0 and goes left + drawVuScale (g, cx, cy, radius, startAngle, endAngle); // Needle { @@ -112,35 +70,115 @@ public: 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); + 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); + 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 + // Label 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 + Mode mode = VU; + float needlePos = 0.0f; + float needleVelocity = 0.0f; juce::String label; + + void applyNeedlePhysics (float target) + { + constexpr float spring = 0.35f; + constexpr float damping = 0.55f; + + float force = spring * (target - needlePos); + needleVelocity = needleVelocity * (1.0f - damping) + force; + needlePos += needleVelocity; + needlePos = juce::jlimit (0.0f, 1.05f, needlePos); + + repaint(); + } + + void drawVuScale (juce::Graphics& g, float cx, float cy, float radius, + float startAngle, float endAngle) + { + const float dbValues[] = { -20, -10, -7, -5, -3, -1, 0, 1, 2, 3 }; + + for (int i = 0; i < 10; ++i) + { + float norm = (dbValues[i] + 20.0f) / 23.0f; + float angle = startAngle + norm * (endAngle - startAngle); + float cosA = std::cos (angle), sinA = std::sin (angle); + + float innerR = radius * 0.82f, outerR = radius * 0.95f; + bool isMajor = (dbValues[i] == -20 || dbValues[i] == -10 || dbValues[i] == -5 + || dbValues[i] == 0 || dbValues[i] == 3); + + g.setColour (dbValues[i] >= 0 ? juce::Colour (0xffcc3333) : juce::Colour (0xff333333)); + g.drawLine (cx + cosA * (isMajor ? innerR * 0.9f : innerR), cy + sinA * (isMajor ? innerR * 0.9f : innerR), + cx + cosA * outerR, cy + sinA * outerR, isMajor ? 1.5f : 0.8f); + + if (isMajor) + { + float lx = cx + cosA * radius * 0.7f, ly = cy + sinA * radius * 0.7f; + 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 + 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)); + } + + void drawGrScale (juce::Graphics& g, float cx, float cy, float radius, + float startAngle, float endAngle) + { + // GR scale: 0 (left, rest) to -20 (right, max compression) + const float grValues[] = { 0, -2, -4, -6, -8, -10, -14, -20 }; + + for (int i = 0; i < 8; ++i) + { + float norm = -grValues[i] / 20.0f; // 0→0.0, -20→1.0 + float angle = startAngle + norm * (endAngle - startAngle); + float cosA = std::cos (angle), sinA = std::sin (angle); + + float innerR = radius * 0.82f, outerR = radius * 0.95f; + bool isMajor = (grValues[i] == 0 || grValues[i] == -6 || grValues[i] == -10 || grValues[i] == -20); + + g.setColour (grValues[i] <= -10 ? juce::Colour (0xffcc3333) : juce::Colour (0xff333333)); + g.drawLine (cx + cosA * (isMajor ? innerR * 0.9f : innerR), cy + sinA * (isMajor ? innerR * 0.9f : innerR), + cx + cosA * outerR, cy + sinA * outerR, isMajor ? 1.5f : 0.8f); + + if (isMajor) + { + float lx = cx + cosA * radius * 0.7f, ly = cy + sinA * radius * 0.7f; + juce::String txt = juce::String ((int) grValues[i]); + g.setColour (grValues[i] <= -10 ? juce::Colour (0xffcc3333) : juce::Colour (0xff444444)); + g.drawText (txt, (int) (lx - 12), (int) (ly - 6), 24, 12, juce::Justification::centred); + } + } + + // Warning zone arc (-10 to -20 dB GR) + float warnStart = startAngle + (10.0f / 20.0f) * (endAngle - startAngle); + juce::Path warnArc; + warnArc.addCentredArc (cx, cy, radius * 0.92f, radius * 0.92f, 0, warnStart, endAngle, true); + g.setColour (juce::Colour (0x33ff3333)); + g.strokePath (warnArc, juce::PathStrokeType (radius * 0.08f)); + } }; diff --git a/Source/PluginEditor.cpp b/Source/PluginEditor.cpp index f550cd7..5cb529d 100644 --- a/Source/PluginEditor.cpp +++ b/Source/PluginEditor.cpp @@ -41,20 +41,50 @@ InstaShadowEditor::InstaShadowEditor (InstaShadowProcessor& p) addAndMakeVisible (transformerPanel); addAndMakeVisible (outputPanel); - // Needle VU meters - vuMeterL.setLabel ("L"); - addAndMakeVisible (vuMeterL); - vuMeterR.setLabel ("R"); - addAndMakeVisible (vuMeterR); + // Needle meters (default: GR) + needleMeterL.setLabel ("OPTICAL GR"); + needleMeterL.setMode (NeedleVuMeter::GR); + addAndMakeVisible (needleMeterL); + needleMeterR.setLabel ("DISCRETE GR"); + needleMeterR.setMode (NeedleVuMeter::GR); + addAndMakeVisible (needleMeterR); - // GR meters (compact bars) - optoGrMeter.setLabel ("OPTICAL GR"); - optoGrMeter.setBarColour (juce::Colour (0xffff8833)); - addAndMakeVisible (optoGrMeter); + // Bar meters (default: input level) + barMeterL.setLabel ("INPUT L"); + barMeterL.setBarColour (juce::Colour (0xff00cc44)); + addAndMakeVisible (barMeterL); + barMeterR.setLabel ("INPUT R"); + barMeterR.setBarColour (juce::Colour (0xff00cc44)); + addAndMakeVisible (barMeterR); - vcaGrMeter.setLabel ("DISCRETE GR"); - vcaGrMeter.setBarColour (juce::Colour (0xff4488ff)); - addAndMakeVisible (vcaGrMeter); + // Meter swap button + meterSwapButton.onClick = [this] + { + metersSwapped = ! metersSwapped; + if (metersSwapped) + { + needleMeterL.setLabel ("INPUT L"); + needleMeterL.setMode (NeedleVuMeter::VU); + needleMeterR.setLabel ("INPUT R"); + needleMeterR.setMode (NeedleVuMeter::VU); + barMeterL.setLabel ("OPTICAL GR"); + barMeterL.setBarColour (juce::Colour (0xffff8833)); + barMeterR.setLabel ("DISCRETE GR"); + barMeterR.setBarColour (juce::Colour (0xff4488ff)); + } + else + { + needleMeterL.setLabel ("OPTICAL GR"); + needleMeterL.setMode (NeedleVuMeter::GR); + needleMeterR.setLabel ("DISCRETE GR"); + needleMeterR.setMode (NeedleVuMeter::GR); + barMeterL.setLabel ("INPUT L"); + barMeterL.setBarColour (juce::Colour (0xff00cc44)); + barMeterR.setLabel ("INPUT R"); + barMeterR.setBarColour (juce::Colour (0xff00cc44)); + } + }; + addAndMakeVisible (meterSwapButton); syncKnobsToEngine(); startTimerHz (30); @@ -116,13 +146,22 @@ void InstaShadowEditor::timerCallback() 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()); + if (! metersSwapped) + { + // Default: needles = GR, bars = input + needleMeterL.setGainReduction (eng.optoGrDb.load()); + needleMeterR.setGainReduction (eng.vcaGrDb.load()); + barMeterL.setInputLevel (eng.inputLevelL.load()); + barMeterR.setInputLevel (eng.inputLevelR.load()); + } + else + { + // Swapped: needles = input, bars = GR + needleMeterL.setLevel (eng.inputLevelL.load()); + needleMeterR.setLevel (eng.inputLevelR.load()); + barMeterL.setGainReduction (eng.optoGrDb.load()); + barMeterR.setGainReduction (eng.vcaGrDb.load()); + } // Output panel VU outputPanel.vuMeter.setLevel (eng.outputLevelL.load(), eng.outputLevelR.load()); @@ -185,20 +224,24 @@ void InstaShadowEditor::resized() // Center column: VU meters, GR bars, Transformer, Output — all stacked auto centerArea = mainRow; - // Two needle VU meters side by side (~30%) + // Two needle 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); + auto needleRow = centerArea.removeFromTop (vuH); + int needleW = (needleRow.getWidth() - pad) / 2; + needleMeterL.setBounds (needleRow.removeFromLeft (needleW)); + needleRow.removeFromLeft (pad); + needleMeterR.setBounds (needleRow); centerArea.removeFromTop (pad); - // Two GR meter bars (~15%) - int grBarH = (int) (centerArea.getHeight() * 0.12f); - optoGrMeter.setBounds (centerArea.removeFromTop (grBarH)); + // Swap button (compact, between needles and bars) + meterSwapButton.setBounds (centerArea.removeFromTop (20).reduced (centerArea.getWidth() / 4, 0)); centerArea.removeFromTop (pad); - vcaGrMeter.setBounds (centerArea.removeFromTop (grBarH)); + + // Two bar meters (~10%) + int barH = (int) (centerArea.getHeight() * 0.10f); + barMeterL.setBounds (centerArea.removeFromTop (barH)); + centerArea.removeFromTop (pad); + barMeterR.setBounds (centerArea.removeFromTop (barH)); centerArea.removeFromTop (pad); // Transformer + Output side by side in remaining center space diff --git a/Source/PluginEditor.h b/Source/PluginEditor.h index 44441e3..c437e37 100644 --- a/Source/PluginEditor.h +++ b/Source/PluginEditor.h @@ -9,7 +9,7 @@ #include "GRMeter.h" #include "NeedleVuMeter.h" -static constexpr const char* kInstaShadowVersion = "v1.0"; +static constexpr const char* kInstaShadowVersion = "v1.1"; class InstaShadowEditor : public juce::AudioProcessorEditor, public juce::Timer @@ -38,11 +38,15 @@ private: OpticalPanel opticalPanel; DiscretePanel discretePanel; - // Center: needle VU meters + GR bars - NeedleVuMeter vuMeterL; - NeedleVuMeter vuMeterR; - GRMeter optoGrMeter; - GRMeter vcaGrMeter; + // Center: needle meters + bar meters (swappable) + NeedleVuMeter needleMeterL; + NeedleVuMeter needleMeterR; + GRMeter barMeterL; + GRMeter barMeterR; + + // Meter swap toggle + juce::TextButton meterSwapButton { "GR / INPUT" }; + bool metersSwapped = false; // false: needle=GR, bar=input | true: needle=input, bar=GR // Bottom panels TransformerPanel transformerPanel;