mirror of
https://codeberg.org/armin/terion.git
synced 2026-10-11 16:41:53 +02:00
Add 250% scale option, default 150%, widen base layout; polish trail scaling
This commit is contained in:
parent
d15e61b96f
commit
e4af0eb793
7 changed files with 123 additions and 67 deletions
|
|
@ -10,7 +10,7 @@ Terion visualizes the stereo field of any audio signal using a mid/side coordina
|
||||||
- **Phosphor trail** - configurable fade time from 0.1 to 10 seconds for persistent visual history
|
- **Phosphor trail** - configurable fade time from 0.1 to 10 seconds for persistent visual history
|
||||||
- **Real-time stereo metrics** - correlation, stereo width, mid/side levels, balance, phase angle, crest factor, and peak/RMS levels
|
- **Real-time stereo metrics** - correlation, stereo width, mid/side levels, balance, phase angle, crest factor, and peak/RMS levels
|
||||||
- **Freeze** - capture and hold the current display for inspection
|
- **Freeze** - capture and hold the current display for inspection
|
||||||
- **Resizable UI** - scales from 420×420 up to 1200×1200, with preset scale factors (100/125/150/200/300%)
|
- **Resizable UI** - scales from 630×420 up to 1890×1890, with preset scale factors (100/125/150/200/250/300%)
|
||||||
- **Custom arc knobs** - clean rotary controls with teal arc indicators
|
- **Custom arc knobs** - clean rotary controls with teal arc indicators
|
||||||
- **VST3 and Audio Unit** formats
|
- **VST3 and Audio Unit** formats
|
||||||
|
|
||||||
|
|
@ -59,8 +59,9 @@ make install # build, codesign, un-quarantine, install to ~/Library/Audio/Plu
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| Fade | 0.1 – 10 s | Trail persistence time |
|
| Fade | 0.1 – 10 s | Trail persistence time |
|
||||||
| Sensitivity | -12 – +24 dB | Input gain for M/S decode |
|
| Sensitivity | -12 – +24 dB | Input gain for M/S decode |
|
||||||
| Dot size | 0.5 – 3.0 px | Size of each plotted point |
|
| Dot size | 0.5 – 8.0 px | Size of each plotted point |
|
||||||
| Smoothing | 0 – 0.99 | One-pole smoothing on M/S signals |
|
| Smoothing | 0 – 0.99 | One-pole smoothing on M/S signals |
|
||||||
|
| Afterglow | 0 – 5 s | Extra lingers of the trail after the fade time |
|
||||||
| Freeze | on/off | Stop capturing new points |
|
| Freeze | on/off | Stop capturing new points |
|
||||||
|
|
||||||
## Project structure
|
## Project structure
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,8 @@ TerionProcessorEditor::TerionProcessorEditor(TerionProcessor& p)
|
||||||
fadeKnob(*p.apvts.getParameter("fadeTime")),
|
fadeKnob(*p.apvts.getParameter("fadeTime")),
|
||||||
sensitivityKnob(*p.apvts.getParameter("sensitivity")),
|
sensitivityKnob(*p.apvts.getParameter("sensitivity")),
|
||||||
dotSizeKnob(*p.apvts.getParameter("dotSize")),
|
dotSizeKnob(*p.apvts.getParameter("dotSize")),
|
||||||
smoothingKnob(*p.apvts.getParameter("smoothing"))
|
smoothingKnob(*p.apvts.getParameter("smoothing")),
|
||||||
|
afterglowKnob(*p.apvts.getParameter("afterglow"))
|
||||||
{
|
{
|
||||||
setSize(kBaseWidth, kBaseHeight);
|
setSize(kBaseWidth, kBaseHeight);
|
||||||
setResizable(true, true);
|
setResizable(true, true);
|
||||||
|
|
@ -28,7 +29,7 @@ TerionProcessorEditor::TerionProcessorEditor(TerionProcessor& p)
|
||||||
|
|
||||||
// Dot size knob
|
// Dot size knob
|
||||||
addAndMakeVisible(dotSizeKnob);
|
addAndMakeVisible(dotSizeKnob);
|
||||||
dotSizeKnob.setRange(0.5, 3.0);
|
dotSizeKnob.setRange(0.5, 8.0);
|
||||||
dotSizeAttachment = std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment>(
|
dotSizeAttachment = std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment>(
|
||||||
processorRef.apvts, "dotSize", dotSizeKnob);
|
processorRef.apvts, "dotSize", dotSizeKnob);
|
||||||
|
|
||||||
|
|
@ -38,6 +39,12 @@ TerionProcessorEditor::TerionProcessorEditor(TerionProcessor& p)
|
||||||
smoothingAttachment = std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment>(
|
smoothingAttachment = std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment>(
|
||||||
processorRef.apvts, "smoothing", smoothingKnob);
|
processorRef.apvts, "smoothing", smoothingKnob);
|
||||||
|
|
||||||
|
// Afterglow knob
|
||||||
|
addAndMakeVisible(afterglowKnob);
|
||||||
|
afterglowKnob.setRange(0.0, 5.0);
|
||||||
|
afterglowAttachment = std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment>(
|
||||||
|
processorRef.apvts, "afterglow", afterglowKnob);
|
||||||
|
|
||||||
// Freeze button
|
// Freeze button
|
||||||
addAndMakeVisible(freezeButton);
|
addAndMakeVisible(freezeButton);
|
||||||
freezeButton.setButtonText("FRZ");
|
freezeButton.setButtonText("FRZ");
|
||||||
|
|
@ -60,8 +67,9 @@ TerionProcessorEditor::TerionProcessorEditor(TerionProcessor& p)
|
||||||
scaleBox.addItem("125%", 2);
|
scaleBox.addItem("125%", 2);
|
||||||
scaleBox.addItem("150%", 3);
|
scaleBox.addItem("150%", 3);
|
||||||
scaleBox.addItem("200%", 4);
|
scaleBox.addItem("200%", 4);
|
||||||
scaleBox.addItem("300%", 5);
|
scaleBox.addItem("250%", 5);
|
||||||
scaleBox.setSelectedId(2, juce::dontSendNotification);
|
scaleBox.addItem("300%", 6);
|
||||||
|
scaleBox.setSelectedId(3, juce::dontSendNotification);
|
||||||
scaleBox.onChange = [this] { scaleChanged(); };
|
scaleBox.onChange = [this] { scaleChanged(); };
|
||||||
scaleChanged();
|
scaleChanged();
|
||||||
|
|
||||||
|
|
@ -75,7 +83,6 @@ TerionProcessorEditor::TerionProcessorEditor(TerionProcessor& p)
|
||||||
addAndMakeVisible(label);
|
addAndMakeVisible(label);
|
||||||
label.setText(text, juce::dontSendNotification);
|
label.setText(text, juce::dontSendNotification);
|
||||||
label.setJustificationType(juce::Justification::centredTop);
|
label.setJustificationType(juce::Justification::centredTop);
|
||||||
label.setFont(juce::Font(juce::FontOptions(9.0f)));
|
|
||||||
label.setColour(juce::Label::textColourId, juce::Colour(0xff888899));
|
label.setColour(juce::Label::textColourId, juce::Colour(0xff888899));
|
||||||
label.setInterceptsMouseClicks(false, false);
|
label.setInterceptsMouseClicks(false, false);
|
||||||
};
|
};
|
||||||
|
|
@ -83,6 +90,7 @@ TerionProcessorEditor::TerionProcessorEditor(TerionProcessor& p)
|
||||||
setupLabel(sensitivityLabel, sensitivityKnob.getParameterName());
|
setupLabel(sensitivityLabel, sensitivityKnob.getParameterName());
|
||||||
setupLabel(dotSizeLabel, dotSizeKnob.getParameterName());
|
setupLabel(dotSizeLabel, dotSizeKnob.getParameterName());
|
||||||
setupLabel(smoothingLabel, smoothingKnob.getParameterName());
|
setupLabel(smoothingLabel, smoothingKnob.getParameterName());
|
||||||
|
setupLabel(afterglowLabel, afterglowKnob.getParameterName());
|
||||||
}
|
}
|
||||||
|
|
||||||
TerionProcessorEditor::~TerionProcessorEditor() {}
|
TerionProcessorEditor::~TerionProcessorEditor() {}
|
||||||
|
|
@ -96,12 +104,14 @@ void TerionProcessorEditor::scaleChanged()
|
||||||
case 2: currentScale = 1.25f; break;
|
case 2: currentScale = 1.25f; break;
|
||||||
case 3: currentScale = 1.5f; break;
|
case 3: currentScale = 1.5f; break;
|
||||||
case 4: currentScale = 2.0f; break;
|
case 4: currentScale = 2.0f; break;
|
||||||
case 5: currentScale = 3.0f; break;
|
case 5: currentScale = 2.5f; break;
|
||||||
default: currentScale = 1.25f; break;
|
case 6: currentScale = 3.0f; break;
|
||||||
|
default: currentScale = 1.5f; break;
|
||||||
}
|
}
|
||||||
|
|
||||||
const int maxDim = static_cast<int>(kBaseWidth * kMaxScale);
|
const int maxDim = static_cast<int>(kBaseWidth * kMaxScale);
|
||||||
setResizeLimits(kBaseWidth, kBaseHeight, maxDim, maxDim);
|
setResizeLimits(kBaseWidth, kBaseHeight, maxDim, maxDim);
|
||||||
|
scope.setScale(currentScale);
|
||||||
setSize(static_cast<int>(kBaseWidth * currentScale),
|
setSize(static_cast<int>(kBaseWidth * currentScale),
|
||||||
static_cast<int>(kBaseHeight * currentScale));
|
static_cast<int>(kBaseHeight * currentScale));
|
||||||
}
|
}
|
||||||
|
|
@ -151,21 +161,32 @@ void TerionProcessorEditor::resized()
|
||||||
// Scope fills the remaining area
|
// Scope fills the remaining area
|
||||||
scope.setBounds(bounds);
|
scope.setBounds(bounds);
|
||||||
|
|
||||||
// Controls layout: top-right corner, horizontal row
|
// Controls layout: knob row sits 8px above the footer, FRZ stays top-right
|
||||||
const int startX = bounds.getWidth() - (4 * (knobs + margin) + knobs + margin);
|
const int gap = static_cast<int>(8.0f * s);
|
||||||
const int startY = static_cast<int>(10.0f * s);
|
const int numKnobs = 5;
|
||||||
|
const int rowH = knobs + static_cast<int>(12.0f * s) - static_cast<int>(6.0f * s);
|
||||||
|
const int startX = bounds.getWidth() - (numKnobs * (knobs + margin) + knobs + margin);
|
||||||
|
const int startY = bounds.getHeight() - rowH - gap;
|
||||||
|
|
||||||
fadeKnob.setBounds(startX, startY, knobs, knobs);
|
fadeKnob.setBounds(startX, startY, knobs, knobs);
|
||||||
sensitivityKnob.setBounds(startX + (knobs + margin), startY, knobs, knobs);
|
sensitivityKnob.setBounds(startX + (knobs + margin), startY, knobs, knobs);
|
||||||
dotSizeKnob.setBounds(startX + 2 * (knobs + margin), startY, knobs, knobs);
|
dotSizeKnob.setBounds(startX + 2 * (knobs + margin), startY, knobs, knobs);
|
||||||
smoothingKnob.setBounds(startX + 3 * (knobs + margin), startY, knobs, knobs);
|
smoothingKnob.setBounds(startX + 3 * (knobs + margin), startY, knobs, knobs);
|
||||||
freezeButton.setBounds(startX + 4 * (knobs + margin), startY + static_cast<int>(10.0f * s),
|
afterglowKnob.setBounds(startX + 4 * (knobs + margin), startY, knobs, knobs);
|
||||||
|
freezeButton.setBounds(bounds.getWidth() - knobs - margin, static_cast<int>(20.0f * s),
|
||||||
knobs, knobs - static_cast<int>(20.0f * s));
|
knobs, knobs - static_cast<int>(20.0f * s));
|
||||||
|
|
||||||
const int labelY = startY + knobs - static_cast<int>(6.0f * s);
|
const int labelY = startY + knobs - static_cast<int>(6.0f * s);
|
||||||
const int labelH = static_cast<int>(12.0f * s);
|
const int labelH = static_cast<int>(12.0f * s);
|
||||||
|
const float labelFont = 9.0f * s;
|
||||||
fadeLabel.setBounds(fadeKnob.getX(), labelY, knobs, labelH);
|
fadeLabel.setBounds(fadeKnob.getX(), labelY, knobs, labelH);
|
||||||
|
fadeLabel.setFont(juce::Font(juce::FontOptions(labelFont)));
|
||||||
sensitivityLabel.setBounds(sensitivityKnob.getX(), labelY, knobs, labelH);
|
sensitivityLabel.setBounds(sensitivityKnob.getX(), labelY, knobs, labelH);
|
||||||
|
sensitivityLabel.setFont(juce::Font(juce::FontOptions(labelFont)));
|
||||||
dotSizeLabel.setBounds(dotSizeKnob.getX(), labelY, knobs, labelH);
|
dotSizeLabel.setBounds(dotSizeKnob.getX(), labelY, knobs, labelH);
|
||||||
|
dotSizeLabel.setFont(juce::Font(juce::FontOptions(labelFont)));
|
||||||
smoothingLabel.setBounds(smoothingKnob.getX(), labelY, knobs, labelH);
|
smoothingLabel.setBounds(smoothingKnob.getX(), labelY, knobs, labelH);
|
||||||
|
smoothingLabel.setFont(juce::Font(juce::FontOptions(labelFont)));
|
||||||
|
afterglowLabel.setBounds(afterglowKnob.getX(), labelY, knobs, labelH);
|
||||||
|
afterglowLabel.setFont(juce::Font(juce::FontOptions(labelFont)));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,20 +23,22 @@ private:
|
||||||
ArcKnob sensitivityKnob;
|
ArcKnob sensitivityKnob;
|
||||||
ArcKnob dotSizeKnob;
|
ArcKnob dotSizeKnob;
|
||||||
ArcKnob smoothingKnob;
|
ArcKnob smoothingKnob;
|
||||||
|
ArcKnob afterglowKnob;
|
||||||
juce::ToggleButton freezeButton;
|
juce::ToggleButton freezeButton;
|
||||||
juce::ToggleButton monoButton;
|
juce::ToggleButton monoButton;
|
||||||
juce::ComboBox scaleBox;
|
juce::ComboBox scaleBox;
|
||||||
juce::Label fadeLabel, sensitivityLabel, dotSizeLabel, smoothingLabel;
|
juce::Label fadeLabel, sensitivityLabel, dotSizeLabel, smoothingLabel, afterglowLabel;
|
||||||
|
|
||||||
std::unique_ptr<juce::AudioProcessorValueTreeState::SliderAttachment> fadeAttachment;
|
std::unique_ptr<juce::AudioProcessorValueTreeState::SliderAttachment> fadeAttachment;
|
||||||
std::unique_ptr<juce::AudioProcessorValueTreeState::SliderAttachment> sensitivityAttachment;
|
std::unique_ptr<juce::AudioProcessorValueTreeState::SliderAttachment> sensitivityAttachment;
|
||||||
std::unique_ptr<juce::AudioProcessorValueTreeState::SliderAttachment> dotSizeAttachment;
|
std::unique_ptr<juce::AudioProcessorValueTreeState::SliderAttachment> dotSizeAttachment;
|
||||||
std::unique_ptr<juce::AudioProcessorValueTreeState::SliderAttachment> smoothingAttachment;
|
std::unique_ptr<juce::AudioProcessorValueTreeState::SliderAttachment> smoothingAttachment;
|
||||||
|
std::unique_ptr<juce::AudioProcessorValueTreeState::SliderAttachment> afterglowAttachment;
|
||||||
std::unique_ptr<juce::AudioProcessorValueTreeState::ButtonAttachment> freezeAttachment;
|
std::unique_ptr<juce::AudioProcessorValueTreeState::ButtonAttachment> freezeAttachment;
|
||||||
std::unique_ptr<juce::AudioProcessorValueTreeState::ButtonAttachment> monoAttachment;
|
std::unique_ptr<juce::AudioProcessorValueTreeState::ButtonAttachment> monoAttachment;
|
||||||
|
|
||||||
float currentScale = 1.0f;
|
float currentScale = 1.0f;
|
||||||
static constexpr int kBaseWidth = 420;
|
static constexpr int kBaseWidth = 630;
|
||||||
static constexpr int kBaseHeight = 420;
|
static constexpr int kBaseHeight = 420;
|
||||||
static constexpr int kFooterHeight = 28;
|
static constexpr int kFooterHeight = 28;
|
||||||
static constexpr int kKnobSize = 54;
|
static constexpr int kKnobSize = 54;
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ TerionProcessor::TerionProcessor()
|
||||||
std::make_unique<juce::AudioParameterFloat>(
|
std::make_unique<juce::AudioParameterFloat>(
|
||||||
juce::ParameterID{"dotSize", 3},
|
juce::ParameterID{"dotSize", 3},
|
||||||
"Dot Size",
|
"Dot Size",
|
||||||
juce::NormalisableRange<float>(0.5f, 3.0f, 0.1f, 0.5f),
|
juce::NormalisableRange<float>(0.5f, 8.0f, 0.1f, 0.5f),
|
||||||
1.0f),
|
1.0f),
|
||||||
std::make_unique<juce::AudioParameterFloat>(
|
std::make_unique<juce::AudioParameterFloat>(
|
||||||
juce::ParameterID{"smoothing", 4},
|
juce::ParameterID{"smoothing", 4},
|
||||||
|
|
@ -33,7 +33,12 @@ TerionProcessor::TerionProcessor()
|
||||||
std::make_unique<juce::AudioParameterBool>(
|
std::make_unique<juce::AudioParameterBool>(
|
||||||
juce::ParameterID{"mono", 6},
|
juce::ParameterID{"mono", 6},
|
||||||
"Mono",
|
"Mono",
|
||||||
false)
|
false),
|
||||||
|
std::make_unique<juce::AudioParameterFloat>(
|
||||||
|
juce::ParameterID{"afterglow", 7},
|
||||||
|
"Afterglow",
|
||||||
|
juce::NormalisableRange<float>(0.0f, 5.0f, 0.01f, 0.5f),
|
||||||
|
0.5f)
|
||||||
})
|
})
|
||||||
{
|
{
|
||||||
points.resize(kMaxPoints);
|
points.resize(kMaxPoints);
|
||||||
|
|
|
||||||
|
|
@ -68,7 +68,8 @@ public:
|
||||||
dotSize = 3,
|
dotSize = 3,
|
||||||
smoothing = 4,
|
smoothing = 4,
|
||||||
freeze = 5,
|
freeze = 5,
|
||||||
mono = 6
|
mono = 6,
|
||||||
|
afterglow = 7
|
||||||
};
|
};
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|
|
||||||
|
|
@ -39,42 +39,56 @@ void VectorScope::paint(juce::Graphics& g)
|
||||||
const float dotSize = processor.apvts.getRawParameterValue("dotSize")->load();
|
const float dotSize = processor.apvts.getRawParameterValue("dotSize")->load();
|
||||||
const float frozen = processor.apvts.getRawParameterValue("freeze")->load();
|
const float frozen = processor.apvts.getRawParameterValue("freeze")->load();
|
||||||
|
|
||||||
// Rebuild the trail from live points each frame (only if not frozen).
|
// Persistent glow trail: each frame the image itself is faded by a
|
||||||
// The trail is fully cleared first, then every point with age < fadeTime
|
// decay factor (so it keeps dying off even after audio stops), then the
|
||||||
// is redrawn with its age-based alpha, so dots past fadeTime disappear
|
// dots captured since the last frame are stamped in at full brightness.
|
||||||
// completely instead of leaving an asymptotic dark-grey residue.
|
// Older history is retained purely in the image, so lowering the level
|
||||||
if (trail.isValid() && frozen < 0.5f)
|
// never erases the trail prematurely.
|
||||||
|
if (trail.isValid())
|
||||||
{
|
{
|
||||||
juce::Graphics trailG(trail);
|
if (frozen < 0.5f)
|
||||||
trailG.setColour(juce::Colours::black);
|
|
||||||
trailG.fillRect(trail.getBounds().toFloat());
|
|
||||||
|
|
||||||
const auto& points = processor.getPoints();
|
|
||||||
const float fadeTime = processor.apvts.getRawParameterValue("fadeTime")->load();
|
|
||||||
|
|
||||||
for (const auto& pt : points)
|
|
||||||
{
|
{
|
||||||
if (pt.age > fadeTime)
|
const float fadeTime = processor.apvts.getRawParameterValue("fadeTime")->load();
|
||||||
continue;
|
const float afterglow = processor.apvts.getRawParameterValue("afterglow")->load();
|
||||||
|
const float totalLife = fadeTime + afterglow;
|
||||||
|
const float fps = 30.0f;
|
||||||
|
|
||||||
float normAge = pt.age / fadeTime;
|
if (totalLife > 0.0f)
|
||||||
float alpha = 1.0f - normAge;
|
{
|
||||||
alpha = alpha * alpha;
|
// Per-frame alpha multiplier: the trail falls to ~0.1% of its
|
||||||
|
// brightness over fadeTime + afterglow seconds.
|
||||||
|
const float decay = std::exp(-6.9f / (totalLife * fps));
|
||||||
|
trail.multiplyAllAlphas(decay);
|
||||||
|
}
|
||||||
|
|
||||||
float x = cx + pt.side * radius * 2.0f;
|
// Stamp only points captured within roughly the last frame and a
|
||||||
float y = cy - pt.mid * radius * 2.0f;
|
// half; everything older lives on in the decaying image above.
|
||||||
|
const float newAge = 1.5f / (fps * fadeTime);
|
||||||
|
const float newAlpha = 0.9f;
|
||||||
|
const auto& points = processor.getPoints();
|
||||||
|
|
||||||
float phase = std::abs(pt.side) / (std::abs(pt.mid) + 0.0001f);
|
juce::Graphics trailG(trail);
|
||||||
phase = juce::jmin(phase, 1.0f);
|
|
||||||
|
|
||||||
juce::Colour color = juce::Colour::fromFloatRGBA(
|
for (const auto& pt : points)
|
||||||
0.2f + phase * 0.8f,
|
{
|
||||||
1.0f - phase * 0.4f,
|
if (pt.age > newAge)
|
||||||
1.0f - phase * 0.6f,
|
continue;
|
||||||
alpha * 0.9f);
|
|
||||||
|
|
||||||
trailG.setColour(color);
|
float x = cx + pt.side * radius * 2.0f;
|
||||||
trailG.fillRect(x - dotSize * 0.5f, y - dotSize * 0.5f, dotSize, dotSize);
|
float y = cy - pt.mid * radius * 2.0f;
|
||||||
|
|
||||||
|
float phase = std::abs(pt.side) / (std::abs(pt.mid) + 0.0001f);
|
||||||
|
phase = juce::jmin(phase, 1.0f);
|
||||||
|
|
||||||
|
juce::Colour color = juce::Colour::fromFloatRGBA(
|
||||||
|
0.2f + phase * 0.8f,
|
||||||
|
1.0f - phase * 0.4f,
|
||||||
|
1.0f - phase * 0.6f,
|
||||||
|
newAlpha);
|
||||||
|
|
||||||
|
trailG.setColour(color);
|
||||||
|
trailG.fillRect(x - dotSize * 0.5f, y - dotSize * 0.5f, dotSize, dotSize);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -96,9 +110,10 @@ void VectorScope::paint(juce::Graphics& g)
|
||||||
|
|
||||||
// dB label at top-right of each circle
|
// dB label at top-right of each circle
|
||||||
g.setColour(juce::Colour(0xff444455));
|
g.setColour(juce::Colour(0xff444455));
|
||||||
g.setFont(9.0f);
|
g.setFont(juce::Font(juce::FontOptions(9.0f * currentScale)));
|
||||||
juce::String dbLabel = juce::String(dBValues[i], 0) + "dB";
|
juce::String dbLabel = juce::String(dBValues[i], 0) + "dB";
|
||||||
g.drawText(dbLabel, cx + r + 3, cy - r - 2, 40, 14, juce::Justification::centredLeft);
|
g.drawText(dbLabel, cx + r + 3.0f * currentScale, cy - r - 2.0f * currentScale,
|
||||||
|
40.0f * currentScale, 14.0f * currentScale, juce::Justification::centredLeft);
|
||||||
g.setColour(juce::Colour(0xff1a1a25));
|
g.setColour(juce::Colour(0xff1a1a25));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -126,7 +141,7 @@ void VectorScope::drawHairCross(juce::Graphics& g, float cx, float cy)
|
||||||
auto bounds = getLocalBounds().toFloat();
|
auto bounds = getLocalBounds().toFloat();
|
||||||
|
|
||||||
// Bright center cross — two short, bright strokes
|
// Bright center cross — two short, bright strokes
|
||||||
g.setColour(juce::Colour(0xff666680));
|
g.setColour(juce::Colour(0xff9999b3));
|
||||||
|
|
||||||
// Horizontal
|
// Horizontal
|
||||||
g.drawLine(bounds.getX() + 8.0f, cy, bounds.getRight() - 8.0f, cy, 0.5f);
|
g.drawLine(bounds.getX() + 8.0f, cy, bounds.getRight() - 8.0f, cy, 0.5f);
|
||||||
|
|
@ -140,50 +155,59 @@ void VectorScope::drawHairCross(juce::Graphics& g, float cx, float cy)
|
||||||
|
|
||||||
void VectorScope::drawLegends(juce::Graphics& g, float cx, float cy, float radius)
|
void VectorScope::drawLegends(juce::Graphics& g, float cx, float cy, float radius)
|
||||||
{
|
{
|
||||||
|
const float s = currentScale;
|
||||||
g.setColour(juce::Colour(0xff666677));
|
g.setColour(juce::Colour(0xff666677));
|
||||||
g.setFont(10.0f);
|
g.setFont(juce::Font(juce::FontOptions(10.0f * s)));
|
||||||
|
|
||||||
// Side label (X axis, right side)
|
// Side label (X axis, right side)
|
||||||
g.drawText("Side", cx + radius + 4.0f, cy + 4.0f, 40, 14, juce::Justification::centredLeft);
|
g.drawText("Side", cx + radius + 4.0f * s, cy + 4.0f * s, 40.0f * s, 14.0f * s,
|
||||||
|
juce::Justification::centredLeft);
|
||||||
|
|
||||||
// Mid label (Y axis, top)
|
// Mid label (Y axis, top)
|
||||||
g.drawText("Mid", cx + 4.0f, cy - radius - 16.0f, 40, 14, juce::Justification::centredLeft);
|
g.drawText("Mid", cx + 4.0f * s, cy - radius - 16.0f * s, 40.0f * s, 14.0f * s,
|
||||||
|
juce::Justification::centredLeft);
|
||||||
|
|
||||||
// Diagonal labels: in M/S space these are the pure-channel loci
|
// Diagonal labels: in M/S space these are the pure-channel loci
|
||||||
g.setColour(juce::Colour(0xff444455));
|
g.setColour(juce::Colour(0xff444455));
|
||||||
g.setFont(8.0f);
|
g.setFont(juce::Font(juce::FontOptions(8.0f * s)));
|
||||||
|
|
||||||
// L = 0 (right channel only): line y = x through centre
|
// L = 0 (right channel only): line y = x through centre
|
||||||
float diagX = cx + radius * 0.71f;
|
float diagX = cx + radius * 0.71f;
|
||||||
g.drawText("R only", diagX + 4.0f, cy + radius * 0.71f, 30, 12, juce::Justification::centredLeft);
|
g.drawText("R only", diagX + 4.0f * s, cy + radius * 0.71f, 30.0f * s, 12.0f * s,
|
||||||
|
juce::Justification::centredLeft);
|
||||||
|
|
||||||
// R = 0 (left channel only): line y = -x through centre
|
// R = 0 (left channel only): line y = -x through centre
|
||||||
g.drawText("L only", diagX + 4.0f, cy - radius * 0.71f - 12.0f, 30, 12, juce::Justification::centredLeft);
|
g.drawText("L only", diagX + 4.0f * s, cy - radius * 0.71f - 12.0f * s, 30.0f * s, 12.0f * s,
|
||||||
|
juce::Justification::centredLeft);
|
||||||
}
|
}
|
||||||
|
|
||||||
void VectorScope::drawMetricsOverlay(juce::Graphics& g, const juce::Rectangle<int>& bounds)
|
void VectorScope::drawMetricsOverlay(juce::Graphics& g, const juce::Rectangle<int>& bounds)
|
||||||
{
|
{
|
||||||
const auto& metrics = processor.getMetrics();
|
const auto& metrics = processor.getMetrics();
|
||||||
const int margin = 10;
|
const float s = currentScale;
|
||||||
const int lineHeight = 14;
|
const int margin = static_cast<int>(10.0f * s);
|
||||||
|
const int lineHeight = static_cast<int>(14.0f * s);
|
||||||
const int startY = bounds.getBottom() - margin - (7 * lineHeight);
|
const int startY = bounds.getBottom() - margin - (7 * lineHeight);
|
||||||
|
|
||||||
// Background panel
|
// Background panel
|
||||||
juce::Rectangle<int> panelRect(bounds.getX() + margin, startY - 5, 120, 7 * lineHeight + 10);
|
juce::Rectangle<int> panelRect(bounds.getX() + margin, startY - static_cast<int>(5.0f * s),
|
||||||
|
static_cast<int>(120.0f * s), 7 * lineHeight + static_cast<int>(10.0f * s));
|
||||||
g.setColour(juce::Colour(0xff0a0a0f).withAlpha(0.8f));
|
g.setColour(juce::Colour(0xff0a0a0f).withAlpha(0.8f));
|
||||||
g.fillRect(panelRect);
|
g.fillRect(panelRect);
|
||||||
g.setColour(juce::Colour(0xff333340));
|
g.setColour(juce::Colour(0xff333340));
|
||||||
g.drawRect(panelRect, 1);
|
g.drawRect(panelRect, 1);
|
||||||
|
|
||||||
// Metrics text
|
// Metrics text
|
||||||
g.setFont(9.0f);
|
g.setFont(juce::Font(juce::FontOptions(9.0f * s)));
|
||||||
g.setColour(juce::Colour(0xff888899));
|
g.setColour(juce::Colour(0xff888899));
|
||||||
|
|
||||||
auto drawMetric = [&](const juce::String& label, float value, const juce::String& unit, int y)
|
auto drawMetric = [&](const juce::String& label, float value, const juce::String& unit, int y)
|
||||||
{
|
{
|
||||||
g.drawText(label, panelRect.getX() + 5, y, 50, lineHeight, juce::Justification::centredLeft);
|
g.drawText(label, panelRect.getX() + static_cast<int>(5.0f * s), y,
|
||||||
|
static_cast<int>(50.0f * s), lineHeight, juce::Justification::centredLeft);
|
||||||
g.setColour(juce::Colour(0xffccccdd));
|
g.setColour(juce::Colour(0xffccccdd));
|
||||||
g.drawText(juce::String(value, 1) + unit, panelRect.getX() + 55, y, 60, lineHeight, juce::Justification::centredRight);
|
g.drawText(juce::String(value, 1) + unit, panelRect.getX() + static_cast<int>(55.0f * s), y,
|
||||||
|
static_cast<int>(60.0f * s), lineHeight, juce::Justification::centredRight);
|
||||||
g.setColour(juce::Colour(0xff888899));
|
g.setColour(juce::Colour(0xff888899));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,9 +14,11 @@ public:
|
||||||
void timerCallback() override;
|
void timerCallback() override;
|
||||||
|
|
||||||
float getRadius() const;
|
float getRadius() const;
|
||||||
|
void setScale(float s) { currentScale = s; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
TerionProcessor& processor;
|
TerionProcessor& processor;
|
||||||
|
float currentScale = 1.0f;
|
||||||
juce::Image trail;
|
juce::Image trail;
|
||||||
|
|
||||||
void drawLegends(juce::Graphics& g, float cx, float cy, float radius);
|
void drawLegends(juce::Graphics& g, float cx, float cy, float radius);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue