terion/Source/PluginProcessor.cpp

235 lines
8 KiB
C++

#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, 8.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),
std::make_unique<juce::AudioParameterBool>(
juce::ParameterID{"mono", 6},
"Mono",
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);
}
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();
float* leftOut = buffer.getWritePointer(0);
float* rightOut = buffer.getNumChannels() > 1 ? buffer.getWritePointer(1) : leftOut;
const float* left = leftOut;
const float* right = rightOut;
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();
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;
}
}
// 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();
}