v1.1 — Improved metering, transformer, and optical cell tuning
- 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
This commit is contained in:
@@ -2,28 +2,31 @@
|
||||
#include <JuceHeader.h>
|
||||
|
||||
// ============================================================
|
||||
// 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<float>::pi * 1.25f; // -225 deg
|
||||
float endAngle = juce::MathConstants<float>::pi * 1.75f; // -315 deg (sweep right)
|
||||
float startAngle = juce::MathConstants<float>::pi * 1.25f;
|
||||
float endAngle = juce::MathConstants<float>::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));
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user