mirror of
https://codeberg.org/armin/terion.git
synced 2026-10-11 16:41:53 +02:00
init
This commit is contained in:
parent
7e21684497
commit
5d4134966f
11 changed files with 996 additions and 0 deletions
50
Source/ArcKnob.cpp
Normal file
50
Source/ArcKnob.cpp
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
#include "ArcKnob.h"
|
||||
|
||||
ArcKnob::ArcKnob(juce::RangedAudioParameter& param)
|
||||
: juce::Slider(juce::Slider::RotaryHorizontalVerticalDrag,
|
||||
juce::Slider::NoTextBox),
|
||||
parameter(param)
|
||||
{
|
||||
}
|
||||
|
||||
void ArcKnob::paint(juce::Graphics& g)
|
||||
{
|
||||
auto bounds = getLocalBounds().toFloat();
|
||||
float diameter = juce::jmin(bounds.getWidth(), bounds.getHeight());
|
||||
float radius = diameter * 0.42f;
|
||||
float cx = bounds.getCentreX();
|
||||
float cy = bounds.getCentreY();
|
||||
|
||||
const float startAngle = juce::MathConstants<float>::pi * 1.25f;
|
||||
const float endAngle = juce::MathConstants<float>::pi * 2.75f;
|
||||
const float angleRange = endAngle - startAngle;
|
||||
|
||||
float rangeMin = static_cast<float>(getMinimum());
|
||||
float rangeMax = static_cast<float>(getMaximum());
|
||||
float value = static_cast<float>(getValue());
|
||||
float normalizedValue = (rangeMax > rangeMin)
|
||||
? juce::jlimit(0.0f, 1.0f, (value - rangeMin) / (rangeMax - rangeMin))
|
||||
: 0.0f;
|
||||
float valueAngle = startAngle + normalizedValue * angleRange;
|
||||
|
||||
// Background arc (dim)
|
||||
juce::Path bgArc;
|
||||
bgArc.addArc(cx - radius, cy - radius, radius * 2.0f, radius * 2.0f,
|
||||
startAngle, endAngle, true);
|
||||
g.setColour(juce::Colour(0xff333340));
|
||||
g.strokePath(bgArc, juce::PathStrokeType(1.5f));
|
||||
|
||||
// Value arc
|
||||
if (normalizedValue > 0.001f)
|
||||
{
|
||||
juce::Path valueArc;
|
||||
valueArc.addArc(cx - radius, cy - radius, radius * 2.0f, radius * 2.0f,
|
||||
startAngle, valueAngle, true);
|
||||
g.setColour(juce::Colour(0xff7ecfcf));
|
||||
g.strokePath(valueArc, juce::PathStrokeType(1.5f));
|
||||
}
|
||||
|
||||
// Center dot
|
||||
g.setColour(juce::Colour(0xff7ecfcf));
|
||||
g.fillEllipse(cx - 2.0f, cy - 2.0f, 4.0f, 4.0f);
|
||||
}
|
||||
17
Source/ArcKnob.h
Normal file
17
Source/ArcKnob.h
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
#pragma once
|
||||
#include <juce_audio_processors/juce_audio_processors.h>
|
||||
|
||||
class ArcKnob : public juce::Slider
|
||||
{
|
||||
public:
|
||||
explicit ArcKnob(juce::RangedAudioParameter& param);
|
||||
|
||||
juce::String getParameterName() const { return parameter.getName(10); }
|
||||
|
||||
void paint(juce::Graphics& g) override;
|
||||
|
||||
private:
|
||||
juce::RangedAudioParameter& parameter;
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(ArcKnob)
|
||||
};
|
||||
146
Source/PluginEditor.cpp
Normal file
146
Source/PluginEditor.cpp
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
#include "PluginEditor.h"
|
||||
|
||||
TerionProcessorEditor::TerionProcessorEditor(TerionProcessor& p)
|
||||
: AudioProcessorEditor(p),
|
||||
processorRef(p),
|
||||
scope(p),
|
||||
fadeKnob(*p.apvts.getParameter("fadeTime")),
|
||||
sensitivityKnob(*p.apvts.getParameter("sensitivity")),
|
||||
dotSizeKnob(*p.apvts.getParameter("dotSize")),
|
||||
smoothingKnob(*p.apvts.getParameter("smoothing"))
|
||||
{
|
||||
setSize(kBaseWidth, kBaseHeight);
|
||||
setResizable(true, true);
|
||||
setResizeLimits(kBaseWidth, kBaseHeight, 1200, 1200);
|
||||
|
||||
addAndMakeVisible(scope);
|
||||
|
||||
// Fade knob
|
||||
addAndMakeVisible(fadeKnob);
|
||||
fadeKnob.setRange(0.1, 10.0);
|
||||
fadeAttachment = std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment>(
|
||||
processorRef.apvts, "fadeTime", fadeKnob);
|
||||
|
||||
// Sensitivity knob
|
||||
addAndMakeVisible(sensitivityKnob);
|
||||
sensitivityKnob.setRange(-12.0, 24.0);
|
||||
sensitivityAttachment = std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment>(
|
||||
processorRef.apvts, "sensitivity", sensitivityKnob);
|
||||
|
||||
// Dot size knob
|
||||
addAndMakeVisible(dotSizeKnob);
|
||||
dotSizeKnob.setRange(0.5, 3.0);
|
||||
dotSizeAttachment = std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment>(
|
||||
processorRef.apvts, "dotSize", dotSizeKnob);
|
||||
|
||||
// Smoothing knob
|
||||
addAndMakeVisible(smoothingKnob);
|
||||
smoothingKnob.setRange(0.0, 0.99);
|
||||
smoothingAttachment = std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment>(
|
||||
processorRef.apvts, "smoothing", smoothingKnob);
|
||||
|
||||
// Freeze button
|
||||
addAndMakeVisible(freezeButton);
|
||||
freezeButton.setButtonText("FRZ");
|
||||
freezeButton.setColour(juce::ToggleButton::textColourId, juce::Colour(0xff888899));
|
||||
freezeButton.setColour(juce::ToggleButton::tickColourId, juce::Colour(0xff7ecfcf));
|
||||
freezeAttachment = std::make_unique<juce::AudioProcessorValueTreeState::ButtonAttachment>(
|
||||
processorRef.apvts, "freeze", freezeButton);
|
||||
|
||||
// Scale dropdown
|
||||
addAndMakeVisible(scaleBox);
|
||||
scaleBox.addItem("100%", 1);
|
||||
scaleBox.addItem("125%", 2);
|
||||
scaleBox.addItem("150%", 3);
|
||||
scaleBox.addItem("200%", 4);
|
||||
scaleBox.addItem("300%", 5);
|
||||
scaleBox.setSelectedId(2, juce::dontSendNotification);
|
||||
scaleBox.onChange = [this] { scaleChanged(); };
|
||||
scaleChanged();
|
||||
|
||||
// Knob labels (separate from knobs to ensure correct z-order)
|
||||
auto setupLabel = [this](juce::Label& label, const juce::String& text)
|
||||
{
|
||||
addAndMakeVisible(label);
|
||||
label.setText(text, juce::dontSendNotification);
|
||||
label.setJustificationType(juce::Justification::centredTop);
|
||||
label.setFont(juce::Font(juce::FontOptions(9.0f)));
|
||||
label.setColour(juce::Label::textColourId, juce::Colour(0xff888899));
|
||||
label.setInterceptsMouseClicks(false, false);
|
||||
};
|
||||
setupLabel(fadeLabel, fadeKnob.getParameterName());
|
||||
setupLabel(sensitivityLabel, sensitivityKnob.getParameterName());
|
||||
setupLabel(dotSizeLabel, dotSizeKnob.getParameterName());
|
||||
setupLabel(smoothingLabel, smoothingKnob.getParameterName());
|
||||
}
|
||||
|
||||
TerionProcessorEditor::~TerionProcessorEditor() {}
|
||||
|
||||
void TerionProcessorEditor::scaleChanged()
|
||||
{
|
||||
int id = scaleBox.getSelectedId();
|
||||
switch (id)
|
||||
{
|
||||
case 1: currentScale = 1.0f; break;
|
||||
case 2: currentScale = 1.25f; break;
|
||||
case 3: currentScale = 1.5f; break;
|
||||
case 4: currentScale = 2.0f; break;
|
||||
case 5: currentScale = 3.0f; break;
|
||||
default: currentScale = 1.25f; break;
|
||||
}
|
||||
|
||||
setTransform(juce::AffineTransform::scale(currentScale));
|
||||
setSize(static_cast<int>(kBaseWidth * currentScale),
|
||||
static_cast<int>(kBaseHeight * currentScale));
|
||||
}
|
||||
|
||||
void TerionProcessorEditor::paint(juce::Graphics& g)
|
||||
{
|
||||
g.fillAll(juce::Colour(0xff0a0a0f));
|
||||
|
||||
// Footer
|
||||
auto bounds = getLocalBounds();
|
||||
auto footer = bounds.removeFromBottom(kFooterHeight);
|
||||
|
||||
g.setColour(juce::Colour(0xff15151e));
|
||||
g.fillRect(footer);
|
||||
|
||||
g.setColour(juce::Colour(0xff555566));
|
||||
g.setFont(9.0f);
|
||||
|
||||
juce::String footerText = "Terion "
|
||||
+ juce::String(TERION_GIT_HASH)
|
||||
+ " | " + juce::String(TERION_BUILD_TIME)
|
||||
+ " | " + juce::String(TERION_GIT_LOCAL_COMMITS) + " local";
|
||||
|
||||
g.drawFittedText(footerText, footer.reduced(10, 0),
|
||||
juce::Justification::centredLeft, 1);
|
||||
}
|
||||
|
||||
void TerionProcessorEditor::resized()
|
||||
{
|
||||
auto bounds = getLocalBounds();
|
||||
auto footer = bounds.removeFromBottom(kFooterHeight);
|
||||
|
||||
// Footer: scale dropdown on the right
|
||||
scaleBox.setBounds(footer.getWidth() - 60, 1, 56, kFooterHeight - 2);
|
||||
|
||||
// Scope fills the remaining area
|
||||
scope.setBounds(bounds);
|
||||
|
||||
// Controls layout: top-right corner, horizontal row
|
||||
const int startX = bounds.getWidth() - (4 * (kKnobSize + kKnobMargin) + kKnobSize + kKnobMargin);
|
||||
const int startY = 10;
|
||||
|
||||
fadeKnob.setBounds(startX, startY, kKnobSize, kKnobSize);
|
||||
sensitivityKnob.setBounds(startX + (kKnobSize + kKnobMargin), startY, kKnobSize, kKnobSize);
|
||||
dotSizeKnob.setBounds(startX + 2 * (kKnobSize + kKnobMargin), startY, kKnobSize, kKnobSize);
|
||||
smoothingKnob.setBounds(startX + 3 * (kKnobSize + kKnobMargin), startY, kKnobSize, kKnobSize);
|
||||
freezeButton.setBounds(startX + 4 * (kKnobSize + kKnobMargin), startY + 10, kKnobSize, kKnobSize - 20);
|
||||
|
||||
const int labelY = startY + kKnobSize - 6;
|
||||
fadeLabel.setBounds(fadeKnob.getX(), labelY, kKnobSize, 12);
|
||||
sensitivityLabel.setBounds(sensitivityKnob.getX(), labelY, kKnobSize, 12);
|
||||
dotSizeLabel.setBounds(dotSizeKnob.getX(), labelY, kKnobSize, 12);
|
||||
smoothingLabel.setBounds(smoothingKnob.getX(), labelY, kKnobSize, 12);
|
||||
}
|
||||
44
Source/PluginEditor.h
Normal file
44
Source/PluginEditor.h
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
#pragma once
|
||||
#include <juce_audio_processors/juce_audio_processors.h>
|
||||
#include "PluginProcessor.h"
|
||||
#include "VectorScope.h"
|
||||
#include "ArcKnob.h"
|
||||
#include "Version.h"
|
||||
|
||||
class TerionProcessorEditor : public juce::AudioProcessorEditor
|
||||
{
|
||||
public:
|
||||
explicit TerionProcessorEditor(TerionProcessor&);
|
||||
~TerionProcessorEditor() override;
|
||||
|
||||
void paint(juce::Graphics&) override;
|
||||
void resized() override;
|
||||
|
||||
private:
|
||||
void scaleChanged();
|
||||
|
||||
TerionProcessor& processorRef;
|
||||
VectorScope scope;
|
||||
ArcKnob fadeKnob;
|
||||
ArcKnob sensitivityKnob;
|
||||
ArcKnob dotSizeKnob;
|
||||
ArcKnob smoothingKnob;
|
||||
juce::ToggleButton freezeButton;
|
||||
juce::ComboBox scaleBox;
|
||||
juce::Label fadeLabel, sensitivityLabel, dotSizeLabel, smoothingLabel;
|
||||
|
||||
std::unique_ptr<juce::AudioProcessorValueTreeState::SliderAttachment> fadeAttachment;
|
||||
std::unique_ptr<juce::AudioProcessorValueTreeState::SliderAttachment> sensitivityAttachment;
|
||||
std::unique_ptr<juce::AudioProcessorValueTreeState::SliderAttachment> dotSizeAttachment;
|
||||
std::unique_ptr<juce::AudioProcessorValueTreeState::SliderAttachment> smoothingAttachment;
|
||||
std::unique_ptr<juce::AudioProcessorValueTreeState::ButtonAttachment> freezeAttachment;
|
||||
|
||||
float currentScale = 1.0f;
|
||||
static constexpr int kBaseWidth = 420;
|
||||
static constexpr int kBaseHeight = 420;
|
||||
static constexpr int kFooterHeight = 28;
|
||||
static constexpr int kKnobSize = 54;
|
||||
static constexpr int kKnobMargin = 6;
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(TerionProcessorEditor)
|
||||
};
|
||||
212
Source/PluginProcessor.cpp
Normal file
212
Source/PluginProcessor.cpp
Normal file
|
|
@ -0,0 +1,212 @@
|
|||
#include "PluginProcessor.h"
|
||||
#include "PluginEditor.h"
|
||||
|
||||
TerionProcessor::TerionProcessor()
|
||||
: AudioProcessor(BusesProperties()
|
||||
.withInput("Input", juce::AudioChannelSet::stereo(), true)
|
||||
.withOutput("Output", juce::AudioChannelSet::stereo(), true)),
|
||||
apvts(*this, nullptr, "Parameters", {
|
||||
std::make_unique<juce::AudioParameterFloat>(
|
||||
juce::ParameterID{"fadeTime", 1},
|
||||
"Fade Time",
|
||||
juce::NormalisableRange<float>(0.1f, 10.0f, 0.01f, 0.4f),
|
||||
2.0f),
|
||||
std::make_unique<juce::AudioParameterFloat>(
|
||||
juce::ParameterID{"sensitivity", 2},
|
||||
"Sensitivity",
|
||||
juce::NormalisableRange<float>(-12.0f, 24.0f, 0.1f, 0.5f),
|
||||
0.0f),
|
||||
std::make_unique<juce::AudioParameterFloat>(
|
||||
juce::ParameterID{"dotSize", 3},
|
||||
"Dot Size",
|
||||
juce::NormalisableRange<float>(0.5f, 3.0f, 0.1f, 0.5f),
|
||||
1.0f),
|
||||
std::make_unique<juce::AudioParameterFloat>(
|
||||
juce::ParameterID{"smoothing", 4},
|
||||
"Smoothing",
|
||||
juce::NormalisableRange<float>(0.0f, 0.99f, 0.01f, 0.5f),
|
||||
0.0f),
|
||||
std::make_unique<juce::AudioParameterBool>(
|
||||
juce::ParameterID{"freeze", 5},
|
||||
"Freeze",
|
||||
false)
|
||||
})
|
||||
{
|
||||
points.resize(kMaxPoints);
|
||||
}
|
||||
|
||||
TerionProcessor::~TerionProcessor() {}
|
||||
|
||||
void TerionProcessor::prepareToPlay(double, int) {}
|
||||
|
||||
void TerionProcessor::releaseResources() {}
|
||||
|
||||
void TerionProcessor::processBlock(juce::AudioBuffer<float>& buffer, juce::MidiBuffer&)
|
||||
{
|
||||
juce::ScopedNoDenormals noDenormals;
|
||||
|
||||
const int numSamples = buffer.getNumSamples();
|
||||
const float* left = buffer.getReadPointer(0);
|
||||
const float* right = buffer.getNumChannels() > 1 ? buffer.getReadPointer(1) : left;
|
||||
|
||||
const float fadeTime = apvts.getRawParameterValue("fadeTime")->load();
|
||||
const float agePerSample = 1.0f / (fadeTime * static_cast<float>(getSampleRate()));
|
||||
const float blockSize = static_cast<float>(numSamples);
|
||||
const float sensGain = juce::Decibels::decibelsToGain(apvts.getRawParameterValue("sensitivity")->load());
|
||||
const float smoothCoef = apvts.getRawParameterValue("smoothing")->load();
|
||||
const float frozen = apvts.getRawParameterValue("freeze")->load();
|
||||
|
||||
// Reset metrics for this block
|
||||
sumMid = 0.0f;
|
||||
sumSide = 0.0f;
|
||||
sumMidSide = 0.0f;
|
||||
sumMid2 = 0.0f;
|
||||
sumSide2 = 0.0f;
|
||||
peakMid = 0.0f;
|
||||
peakSide = 0.0f;
|
||||
metricsCount = 0;
|
||||
|
||||
for (int i = 0; i < numSamples; ++i)
|
||||
{
|
||||
float mid = (left[i] + right[i]) * 0.5f * sensGain;
|
||||
float side = (left[i] - right[i]) * 0.5f * sensGain;
|
||||
|
||||
// Apply smoothing
|
||||
if (smoothCoef > 0.0f)
|
||||
{
|
||||
float alpha = std::exp(-1.0f / (smoothCoef * 0.05f * static_cast<float>(getSampleRate())));
|
||||
midSmoothed += alpha * (mid - midSmoothed);
|
||||
sideSmoothed += alpha * (side - sideSmoothed);
|
||||
mid = midSmoothed;
|
||||
side = sideSmoothed;
|
||||
}
|
||||
|
||||
// Update metrics accumulation
|
||||
updateMetrics(mid, side, left[i], right[i]);
|
||||
|
||||
// Add point if not frozen
|
||||
if (frozen < 0.5f)
|
||||
{
|
||||
addPoint(mid, side, agePerSample);
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate final metrics for this block
|
||||
if (metricsCount > 0)
|
||||
{
|
||||
float avgMid = sumMid / static_cast<float>(metricsCount);
|
||||
float avgSide = sumSide / static_cast<float>(metricsCount);
|
||||
float avgMidSide = sumMidSide / static_cast<float>(metricsCount);
|
||||
float avgMid2 = sumMid2 / static_cast<float>(metricsCount);
|
||||
float avgSide2 = sumSide2 / static_cast<float>(metricsCount);
|
||||
|
||||
// Correlation (Pearson coefficient)
|
||||
float denom = std::sqrt((avgMid2 - avgMid * avgMid) * (avgSide2 - avgSide * avgSide));
|
||||
metrics.correlation = (denom > 1e-6f) ? (avgMidSide - avgMid * avgSide) / denom : 0.0f;
|
||||
metrics.correlation = juce::jlimit(-1.0f, 1.0f, metrics.correlation);
|
||||
|
||||
// Stereo width (M/S ratio in dB)
|
||||
float midRms = std::sqrt(avgMid2);
|
||||
float sideRms = std::sqrt(avgSide2);
|
||||
metrics.midLevel = juce::Decibels::gainToDecibels(midRms, -100.0f);
|
||||
metrics.sideLevel = juce::Decibels::gainToDecibels(sideRms, -100.0f);
|
||||
metrics.stereoWidth = metrics.sideLevel - metrics.midLevel;
|
||||
|
||||
// Balance (L-R difference)
|
||||
float lRms = std::sqrt((metrics.rmsL * metrics.rmsL)); // Already in dB, need linear
|
||||
float rRms = std::sqrt((metrics.rmsR * metrics.rmsR));
|
||||
metrics.balance = metrics.rmsL - metrics.rmsR;
|
||||
|
||||
// Phase angle (degrees)
|
||||
float phaseRad = std::atan2(sideRms, midRms);
|
||||
metrics.phaseAngle = phaseRad * (180.0f / juce::MathConstants<float>::pi);
|
||||
|
||||
// Crest factor (peak vs RMS)
|
||||
float peakLevel = juce::jmax(peakMid, peakSide);
|
||||
float rmsLevel = std::sqrt((avgMid2 + avgSide2) * 0.5f);
|
||||
metrics.crestFactor = juce::Decibels::gainToDecibels(peakLevel / (rmsLevel + 1e-6f), 0.0f);
|
||||
}
|
||||
|
||||
// Age all points (only if not frozen)
|
||||
if (frozen < 0.5f)
|
||||
{
|
||||
for (auto& p : points)
|
||||
p.age += agePerSample * blockSize;
|
||||
}
|
||||
}
|
||||
|
||||
void TerionProcessor::updateMetrics(float mid, float side, float left, float right)
|
||||
{
|
||||
sumMid += mid;
|
||||
sumSide += side;
|
||||
sumMidSide += mid * side;
|
||||
sumMid2 += mid * mid;
|
||||
sumSide2 += side * side;
|
||||
|
||||
float absMid = std::abs(mid);
|
||||
float absSide = std::abs(side);
|
||||
peakMid = juce::jmax(peakMid, absMid);
|
||||
peakSide = juce::jmax(peakSide, absSide);
|
||||
|
||||
// Peak and RMS for L/R
|
||||
float absL = std::abs(left);
|
||||
float absR = std::abs(right);
|
||||
metrics.peakL = juce::jmax(metrics.peakL, absL);
|
||||
metrics.peakR = juce::jmax(metrics.peakR, absR);
|
||||
|
||||
// RMS accumulation (using running sum of squares)
|
||||
static thread_local float sumL2 = 0.0f;
|
||||
static thread_local float sumR2 = 0.0f;
|
||||
static thread_local int count = 0;
|
||||
|
||||
sumL2 += left * left;
|
||||
sumR2 += right * right;
|
||||
count++;
|
||||
|
||||
if (count >= 1024) // Update RMS every 1024 samples
|
||||
{
|
||||
metrics.rmsL = juce::Decibels::gainToDecibels(std::sqrt(sumL2 / static_cast<float>(count)), -100.0f);
|
||||
metrics.rmsR = juce::Decibels::gainToDecibels(std::sqrt(sumR2 / static_cast<float>(count)), -100.0f);
|
||||
sumL2 = 0.0f;
|
||||
sumR2 = 0.0f;
|
||||
count = 0;
|
||||
}
|
||||
|
||||
metricsCount++;
|
||||
}
|
||||
|
||||
void TerionProcessor::addPoint(float mid, float side, float age)
|
||||
{
|
||||
auto& p = points[writePos];
|
||||
p.mid = mid;
|
||||
p.side = side;
|
||||
p.age = age;
|
||||
|
||||
writePos = (writePos + 1) % kMaxPoints;
|
||||
if (writePos == 0)
|
||||
filled = true;
|
||||
}
|
||||
|
||||
juce::AudioProcessorEditor* TerionProcessor::createEditor()
|
||||
{
|
||||
return new TerionProcessorEditor(*this);
|
||||
}
|
||||
|
||||
void TerionProcessor::getStateInformation(juce::MemoryBlock& destData)
|
||||
{
|
||||
auto state = apvts.copyState();
|
||||
std::unique_ptr<juce::XmlElement> xml(state.createXml());
|
||||
copyXmlToBinary(*xml, destData);
|
||||
}
|
||||
|
||||
void TerionProcessor::setStateInformation(const void* data, int sizeInBytes)
|
||||
{
|
||||
std::unique_ptr<juce::XmlElement> xml(getXmlFromBinary(data, sizeInBytes));
|
||||
if (xml && xml->hasTagName(apvts.state.getType()))
|
||||
apvts.replaceState(juce::ValueTree::fromXml(*xml));
|
||||
}
|
||||
|
||||
juce::AudioProcessor* JUCE_CALLTYPE createPluginFilter()
|
||||
{
|
||||
return new TerionProcessor();
|
||||
}
|
||||
97
Source/PluginProcessor.h
Normal file
97
Source/PluginProcessor.h
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
#pragma once
|
||||
#include <juce_audio_processors/juce_audio_processors.h>
|
||||
#include <juce_dsp/juce_dsp.h>
|
||||
#include <atomic>
|
||||
#include <vector>
|
||||
|
||||
struct StereoSample
|
||||
{
|
||||
float mid = 0.0f;
|
||||
float side = 0.0f;
|
||||
float age = 0.0f;
|
||||
};
|
||||
|
||||
struct StereoMetrics
|
||||
{
|
||||
float correlation = 0.0f; // -1 to +1
|
||||
float stereoWidth = 0.0f; // dB
|
||||
float midLevel = 0.0f; // dB
|
||||
float sideLevel = 0.0f; // dB
|
||||
float balance = 0.0f; // dB (L-R)
|
||||
float phaseAngle = 0.0f; // degrees
|
||||
float crestFactor = 0.0f; // dB
|
||||
float peakL = 0.0f; // dB
|
||||
float peakR = 0.0f; // dB
|
||||
float rmsL = 0.0f; // dB
|
||||
float rmsR = 0.0f; // dB
|
||||
};
|
||||
|
||||
class TerionProcessor final : public juce::AudioProcessor
|
||||
{
|
||||
public:
|
||||
static constexpr int kMaxPoints = 8192;
|
||||
|
||||
TerionProcessor();
|
||||
~TerionProcessor() override;
|
||||
|
||||
void prepareToPlay(double sampleRate, int samplesPerBlock) override;
|
||||
void releaseResources() 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 "Terion"; }
|
||||
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;
|
||||
|
||||
const std::vector<StereoSample>& getPoints() const { return points; }
|
||||
const StereoMetrics& getMetrics() const { return metrics; }
|
||||
|
||||
juce::AudioProcessorValueTreeState apvts;
|
||||
|
||||
enum class ParameterID
|
||||
{
|
||||
fadeTime = 1,
|
||||
sensitivity = 2,
|
||||
dotSize = 3,
|
||||
smoothing = 4,
|
||||
freeze = 5
|
||||
};
|
||||
|
||||
private:
|
||||
std::vector<StereoSample> points;
|
||||
StereoMetrics metrics;
|
||||
int writePos = 0;
|
||||
bool filled = false;
|
||||
|
||||
// Smoothing filters
|
||||
float midSmoothed = 0.0f;
|
||||
float sideSmoothed = 0.0f;
|
||||
|
||||
// Metrics calculation state
|
||||
float sumMid = 0.0f;
|
||||
float sumSide = 0.0f;
|
||||
float sumMidSide = 0.0f;
|
||||
float sumMid2 = 0.0f;
|
||||
float sumSide2 = 0.0f;
|
||||
float peakMid = 0.0f;
|
||||
float peakSide = 0.0f;
|
||||
int metricsCount = 0;
|
||||
|
||||
void addPoint(float mid, float side, float age);
|
||||
void updateMetrics(float mid, float side, float left, float right);
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(TerionProcessor)
|
||||
};
|
||||
198
Source/VectorScope.cpp
Normal file
198
Source/VectorScope.cpp
Normal file
|
|
@ -0,0 +1,198 @@
|
|||
#include "VectorScope.h"
|
||||
|
||||
VectorScope::VectorScope(TerionProcessor& p)
|
||||
: processor(p)
|
||||
{
|
||||
startTimerHz(30);
|
||||
}
|
||||
|
||||
VectorScope::~VectorScope()
|
||||
{
|
||||
stopTimer();
|
||||
}
|
||||
|
||||
void VectorScope::timerCallback()
|
||||
{
|
||||
repaint();
|
||||
}
|
||||
|
||||
float VectorScope::getRadius() const
|
||||
{
|
||||
auto bounds = getLocalBounds();
|
||||
return juce::jmin(bounds.getWidth(), bounds.getHeight()) * 0.5f * 0.85f;
|
||||
}
|
||||
|
||||
void VectorScope::resized()
|
||||
{
|
||||
auto bounds = getLocalBounds();
|
||||
trail = juce::Image(juce::Image::ARGB, bounds.getWidth(), bounds.getHeight(), true);
|
||||
}
|
||||
|
||||
void VectorScope::paint(juce::Graphics& g)
|
||||
{
|
||||
auto bounds = getLocalBounds();
|
||||
const int w = bounds.getWidth();
|
||||
const int h = bounds.getHeight();
|
||||
const float cx = w * 0.5f;
|
||||
const float cy = h * 0.5f;
|
||||
const float radius = getRadius();
|
||||
const float dotSize = processor.apvts.getRawParameterValue("dotSize")->load();
|
||||
const float frozen = processor.apvts.getRawParameterValue("freeze")->load();
|
||||
|
||||
// Rebuild the trail from live points each frame (only if not frozen).
|
||||
// The trail is fully cleared first, then every point with age < fadeTime
|
||||
// is redrawn with its age-based alpha, so dots past fadeTime disappear
|
||||
// completely instead of leaving an asymptotic dark-grey residue.
|
||||
if (trail.isValid() && frozen < 0.5f)
|
||||
{
|
||||
juce::Graphics trailG(trail);
|
||||
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)
|
||||
continue;
|
||||
|
||||
float normAge = pt.age / fadeTime;
|
||||
float alpha = 1.0f - normAge;
|
||||
alpha = alpha * alpha;
|
||||
|
||||
float x = cx + pt.side * radius * 2.0f;
|
||||
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,
|
||||
alpha * 0.9f);
|
||||
|
||||
trailG.setColour(color);
|
||||
trailG.fillRect(x - dotSize * 0.5f, y - dotSize * 0.5f, dotSize, dotSize);
|
||||
}
|
||||
}
|
||||
|
||||
// Draw background
|
||||
g.fillAll(juce::Colour(0xff0a0a0f));
|
||||
|
||||
// Draw the trail
|
||||
if (trail.isValid())
|
||||
g.drawImageAt(trail, 0, 0);
|
||||
|
||||
// Concentric circles with dB labels
|
||||
g.setColour(juce::Colour(0xff1a1a25));
|
||||
const float dBValues[] = { -12.0f, -6.0f, 0.0f };
|
||||
const float dBFactors[] = { 0.25f, 0.5f, 1.0f };
|
||||
for (int i = 0; i < 3; ++i)
|
||||
{
|
||||
float r = radius * dBFactors[i];
|
||||
g.drawEllipse(cx - r, cy - r, r * 2.0f, r * 2.0f, 0.5f);
|
||||
|
||||
// dB label at top-right of each circle
|
||||
g.setColour(juce::Colour(0xff444455));
|
||||
g.setFont(9.0f);
|
||||
juce::String dbLabel = juce::String(dBValues[i], 0) + "dB";
|
||||
g.drawText(dbLabel, cx + r + 3, cy - r - 2, 40, 14, juce::Justification::centredLeft);
|
||||
g.setColour(juce::Colour(0xff1a1a25));
|
||||
}
|
||||
|
||||
// 45-degree lines (L=R and L=-R)
|
||||
g.setColour(juce::Colour(0xff181822));
|
||||
g.drawLine(cx - radius, cy - radius, cx + radius, cy + radius, 0.5f);
|
||||
g.drawLine(cx - radius, cy + radius, cx + radius, cy - radius, 0.5f);
|
||||
|
||||
// Hair cross (brighter, thin)
|
||||
drawHairCross(g, cx, cy);
|
||||
|
||||
// Legends
|
||||
drawLegends(g, cx, cy, radius);
|
||||
|
||||
// Metrics overlay
|
||||
drawMetricsOverlay(g, bounds);
|
||||
|
||||
// Border
|
||||
g.setColour(juce::Colour(0xff333340));
|
||||
g.drawRect(bounds, 1);
|
||||
}
|
||||
|
||||
void VectorScope::drawHairCross(juce::Graphics& g, float cx, float cy)
|
||||
{
|
||||
auto bounds = getLocalBounds().toFloat();
|
||||
|
||||
// Bright center cross — two short, bright strokes
|
||||
g.setColour(juce::Colour(0xff666680));
|
||||
|
||||
// Horizontal
|
||||
g.drawLine(bounds.getX() + 8.0f, cy, bounds.getRight() - 8.0f, cy, 0.5f);
|
||||
// Vertical
|
||||
g.drawLine(cx, bounds.getY() + 8.0f, cx, bounds.getBottom() - 8.0f, 0.5f);
|
||||
|
||||
// Tiny center dot
|
||||
g.setColour(juce::Colour(0xff8888aa));
|
||||
g.fillEllipse(cx - 2.0f, cy - 2.0f, 4.0f, 4.0f);
|
||||
}
|
||||
|
||||
void VectorScope::drawLegends(juce::Graphics& g, float cx, float cy, float radius)
|
||||
{
|
||||
g.setColour(juce::Colour(0xff666677));
|
||||
g.setFont(10.0f);
|
||||
|
||||
// Side label (X axis, right side)
|
||||
g.drawText("Side", cx + radius + 4.0f, cy + 4.0f, 40, 14, juce::Justification::centredLeft);
|
||||
|
||||
// Mid label (Y axis, top)
|
||||
g.drawText("Mid", cx + 4.0f, cy - radius - 16.0f, 40, 14, juce::Justification::centredLeft);
|
||||
|
||||
// Diagonal labels: in M/S space these are the pure-channel loci
|
||||
g.setColour(juce::Colour(0xff444455));
|
||||
g.setFont(8.0f);
|
||||
|
||||
// L = 0 (right channel only): line y = x through centre
|
||||
float diagX = cx + radius * 0.71f;
|
||||
g.drawText("R only", diagX + 4.0f, cy + radius * 0.71f, 30, 12, juce::Justification::centredLeft);
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
void VectorScope::drawMetricsOverlay(juce::Graphics& g, const juce::Rectangle<int>& bounds)
|
||||
{
|
||||
const auto& metrics = processor.getMetrics();
|
||||
const int margin = 10;
|
||||
const int lineHeight = 14;
|
||||
const int startY = bounds.getBottom() - margin - (7 * lineHeight);
|
||||
|
||||
// Background panel
|
||||
juce::Rectangle<int> panelRect(bounds.getX() + margin, startY - 5, 120, 7 * lineHeight + 10);
|
||||
g.setColour(juce::Colour(0xff0a0a0f).withAlpha(0.8f));
|
||||
g.fillRect(panelRect);
|
||||
g.setColour(juce::Colour(0xff333340));
|
||||
g.drawRect(panelRect, 1);
|
||||
|
||||
// Metrics text
|
||||
g.setFont(9.0f);
|
||||
g.setColour(juce::Colour(0xff888899));
|
||||
|
||||
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.setColour(juce::Colour(0xffccccdd));
|
||||
g.drawText(juce::String(value, 1) + unit, panelRect.getX() + 55, y, 60, lineHeight, juce::Justification::centredRight);
|
||||
g.setColour(juce::Colour(0xff888899));
|
||||
};
|
||||
|
||||
int y = startY;
|
||||
drawMetric("Corr", metrics.correlation, "", y); y += lineHeight;
|
||||
drawMetric("Width", metrics.stereoWidth, "dB", y); y += lineHeight;
|
||||
drawMetric("Mid", metrics.midLevel, "dB", y); y += lineHeight;
|
||||
drawMetric("Side", metrics.sideLevel, "dB", y); y += lineHeight;
|
||||
drawMetric("Balance", metrics.balance, "dB", y); y += lineHeight;
|
||||
drawMetric("Phase", metrics.phaseAngle, "\u00B0", y); y += lineHeight;
|
||||
drawMetric("Crest", metrics.crestFactor, "dB", y); y += lineHeight;
|
||||
}
|
||||
27
Source/VectorScope.h
Normal file
27
Source/VectorScope.h
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
#pragma once
|
||||
#include <juce_audio_processors/juce_audio_processors.h>
|
||||
#include <juce_opengl/juce_opengl.h>
|
||||
#include "PluginProcessor.h"
|
||||
|
||||
class VectorScope : public juce::Component, private juce::Timer
|
||||
{
|
||||
public:
|
||||
explicit VectorScope(TerionProcessor& p);
|
||||
~VectorScope() override;
|
||||
|
||||
void paint(juce::Graphics& g) override;
|
||||
void resized() override;
|
||||
void timerCallback() override;
|
||||
|
||||
float getRadius() const;
|
||||
|
||||
private:
|
||||
TerionProcessor& processor;
|
||||
juce::Image trail;
|
||||
|
||||
void drawLegends(juce::Graphics& g, float cx, float cy, float radius);
|
||||
void drawHairCross(juce::Graphics& g, float cx, float cy);
|
||||
void drawMetricsOverlay(juce::Graphics& g, const juce::Rectangle<int>& bounds);
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(VectorScope)
|
||||
};
|
||||
5
Source/Version.h.in
Normal file
5
Source/Version.h.in
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
#pragma once
|
||||
|
||||
#define TERION_GIT_HASH "@GIT_HASH@"
|
||||
#define TERION_BUILD_TIME "@BUILD_TIME@"
|
||||
#define TERION_GIT_LOCAL_COMMITS "@GIT_LOCAL_COMMITS@"
|
||||
Loading…
Add table
Add a link
Reference in a new issue