2026-09-16 17:47:45 +02:00
|
|
|
#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},
|
2026-09-17 11:29:01 +02:00
|
|
|
"Fade",
|
2026-09-16 17:47:45 +02:00
|
|
|
juce::NormalisableRange<float>(0.1f, 10.0f, 0.01f, 0.4f),
|
|
|
|
|
2.0f),
|
|
|
|
|
std::make_unique<juce::AudioParameterFloat>(
|
|
|
|
|
juce::ParameterID{"sensitivity", 2},
|
2026-09-17 11:29:01 +02:00
|
|
|
"Response",
|
2026-09-16 17:47:45 +02:00
|
|
|
juce::NormalisableRange<float>(-12.0f, 24.0f, 0.1f, 0.5f),
|
|
|
|
|
0.0f),
|
|
|
|
|
std::make_unique<juce::AudioParameterFloat>(
|
|
|
|
|
juce::ParameterID{"dotSize", 3},
|
2026-09-17 11:29:01 +02:00
|
|
|
"Size",
|
2026-09-16 21:16:37 +02:00
|
|
|
juce::NormalisableRange<float>(0.5f, 8.0f, 0.1f, 0.5f),
|
2026-09-16 17:47:45 +02:00
|
|
|
1.0f),
|
|
|
|
|
std::make_unique<juce::AudioParameterFloat>(
|
|
|
|
|
juce::ParameterID{"smoothing", 4},
|
2026-09-17 11:29:01 +02:00
|
|
|
"Smooth",
|
2026-09-16 17:47:45 +02:00
|
|
|
juce::NormalisableRange<float>(0.0f, 0.99f, 0.01f, 0.5f),
|
|
|
|
|
0.0f),
|
|
|
|
|
std::make_unique<juce::AudioParameterBool>(
|
|
|
|
|
juce::ParameterID{"freeze", 5},
|
|
|
|
|
"Freeze",
|
2026-09-16 18:38:35 +02:00
|
|
|
false),
|
|
|
|
|
std::make_unique<juce::AudioParameterBool>(
|
|
|
|
|
juce::ParameterID{"mono", 6},
|
|
|
|
|
"Mono",
|
2026-09-16 21:16:37 +02:00
|
|
|
false),
|
|
|
|
|
std::make_unique<juce::AudioParameterFloat>(
|
|
|
|
|
juce::ParameterID{"afterglow", 7},
|
2026-09-17 11:29:01 +02:00
|
|
|
"Glow",
|
2026-09-16 21:16:37 +02:00
|
|
|
juce::NormalisableRange<float>(0.0f, 5.0f, 0.01f, 0.5f),
|
|
|
|
|
0.5f)
|
2026-09-16 17:47:45 +02:00
|
|
|
})
|
|
|
|
|
{
|
|
|
|
|
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();
|
2026-09-16 18:38:35 +02:00
|
|
|
float* leftOut = buffer.getWritePointer(0);
|
|
|
|
|
float* rightOut = buffer.getNumChannels() > 1 ? buffer.getWritePointer(1) : leftOut;
|
|
|
|
|
const float* left = leftOut;
|
|
|
|
|
const float* right = rightOut;
|
2026-09-16 17:47:45 +02:00
|
|
|
|
|
|
|
|
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();
|
2026-09-16 18:38:35 +02:00
|
|
|
const float mono = apvts.getRawParameterValue("mono")->load();
|
|
|
|
|
|
|
|
|
|
// Collapse to mono: sum L/R and duplicate into both channels
|
|
|
|
|
if (mono > 0.5f && buffer.getNumChannels() > 1)
|
|
|
|
|
{
|
|
|
|
|
for (int i = 0; i < numSamples; ++i)
|
|
|
|
|
{
|
|
|
|
|
float m = (left[i] + right[i]) * 0.5f;
|
|
|
|
|
leftOut[i] = m;
|
|
|
|
|
rightOut[i] = m;
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-09-16 17:47:45 +02:00
|
|
|
|
|
|
|
|
// 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();
|
|
|
|
|
}
|