#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::ParameterID{"fadeTime", 1}, "Fade Time", juce::NormalisableRange(0.1f, 10.0f, 0.01f, 0.4f), 2.0f), std::make_unique( juce::ParameterID{"sensitivity", 2}, "Sensitivity", juce::NormalisableRange(-12.0f, 24.0f, 0.1f, 0.5f), 0.0f), std::make_unique( juce::ParameterID{"dotSize", 3}, "Dot Size", juce::NormalisableRange(0.5f, 8.0f, 0.1f, 0.5f), 1.0f), std::make_unique( juce::ParameterID{"smoothing", 4}, "Smoothing", juce::NormalisableRange(0.0f, 0.99f, 0.01f, 0.5f), 0.0f), std::make_unique( juce::ParameterID{"freeze", 5}, "Freeze", false), std::make_unique( juce::ParameterID{"mono", 6}, "Mono", false), std::make_unique( juce::ParameterID{"afterglow", 7}, "Afterglow", juce::NormalisableRange(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& 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(getSampleRate())); const float blockSize = static_cast(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(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(metricsCount); float avgSide = sumSide / static_cast(metricsCount); float avgMidSide = sumMidSide / static_cast(metricsCount); float avgMid2 = sumMid2 / static_cast(metricsCount); float avgSide2 = sumSide2 / static_cast(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::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(count)), -100.0f); metrics.rmsR = juce::Decibels::gainToDecibels(std::sqrt(sumR2 / static_cast(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 xml(state.createXml()); copyXmlToBinary(*xml, destData); } void TerionProcessor::setStateInformation(const void* data, int sizeInBytes) { std::unique_ptr 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(); }