From 68127588e0e21d97137c112b9cab6d0aad83b3af Mon Sep 17 00:00:00 2001 From: Michael Fisher Date: Tue, 21 Jul 2026 14:15:27 -0400 Subject: [PATCH] meters: use a stable rms time window in node processing. meters: apply calibration db so -20db test tone reads -20db --- include/element/processor.hpp | 21 +++++++++++---- src/engine/graphbuilder.cpp | 4 +-- src/engine/processor.cpp | 50 ++++++++++++++++++----------------- src/ui/simplemeter.cpp | 6 ++++- 4 files changed, 49 insertions(+), 32 deletions(-) diff --git a/include/element/processor.hpp b/include/element/processor.hpp index 3fc4a2402..106b07910 100644 --- a/include/element/processor.hpp +++ b/include/element/processor.hpp @@ -263,10 +263,10 @@ class Processor : public juce::ReferenceCountedObject { */ GraphNode* getParentGraph() const; - void setInputRMS (int chan, float val); - float getInputRMS (int chan) const { return (chan < inRMS.size()) ? inRMS.getUnchecked (chan)->get() : 0.0f; } - void setOutputRMS (int chan, float val); - float getOutputRMS (int chan) const { return (chan < outRMS.size()) ? outRMS.getUnchecked (chan)->get() : 0.0f; } + void setInputRMS (int chan, float rms, int numSamples); + float getInputRMS (int chan) const { return (chan < inRMS.size()) ? inRMS.getUnchecked (chan)->value.get() : 0.0f; } + void setOutputRMS (int chan, float rms, int numSamples); + float getOutputRMS (int chan) const { return (chan < outRMS.size()) ? outRMS.getUnchecked (chan)->value.get() : 0.0f; } //========================================================================= /** Connect this node's output audio to another node's input audio */ @@ -553,7 +553,18 @@ class Processor : public juce::ReferenceCountedObject { PatchParameterArray _patches; juce::Atomic gain, lastGain, inputGain, lastInputGain; - juce::OwnedArray> inRMS, outRMS; + + /** Per-channel RMS meter integrator. Accumulates the block mean-square with a + fixed time constant so the metered level is independent of the host's + buffer size (see updateRMSMeter). */ + struct RMSMeter { + float meanSquare = 0.0f; // running mean-square, audio thread only + AtomicValue value; // smoothed linear RMS handed to the UI thread + }; + juce::OwnedArray inRMS, outRMS; + + /** Integrates a block's RMS into the given meter with a fixed time window. */ + void updateRMSMeter (RMSMeter& meter, float blockRMS, int numSamples); juce::Atomic keyRangeLow { 0 }; juce::Atomic keyRangeHigh { 127 }; diff --git a/src/engine/graphbuilder.cpp b/src/engine/graphbuilder.cpp index 80060b46a..0ed94f845 100644 --- a/src/engine/graphbuilder.cpp +++ b/src/engine/graphbuilder.cpp @@ -279,7 +279,7 @@ class ProcessBufferOp : public GraphOp } for (int i = numAudioIns; --i >= 0;) - node->setInputRMS (i, buffer.getRMSLevel (i, 0, numSamples)); + node->setInputRMS (i, buffer.getRMSLevel (i, 0, numSamples), numSamples); // Begin MIDI filters { @@ -448,7 +448,7 @@ class ProcessBufferOp : public GraphOp lastMute = muted; for (int i = 0; i < numAudioOuts; ++i) - node->setOutputRMS (i, buffer.getRMSLevel (i, 0, numSamples)); + node->setOutputRMS (i, buffer.getRMSLevel (i, 0, numSamples), numSamples); } const ProcessorPtr node; diff --git a/src/engine/processor.cpp b/src/engine/processor.cpp index 6eff01e83..0926493ce 100644 --- a/src/engine/processor.cpp +++ b/src/engine/processor.cpp @@ -205,16 +205,34 @@ bool Processor::isMidiDeviceNode() const int Processor::getNumAudioInputs() const { return ports.size (PortType::Audio, true); } int Processor::getNumAudioOutputs() const { return ports.size (PortType::Audio, false); } -void Processor::setInputRMS (int chan, float val) +void Processor::updateRMSMeter (RMSMeter& meter, float blockRMS, int numSamples) +{ + // Integrate the block mean-square with a one-pole filter whose coefficient + // scales with the block length. This yields a fixed integration window + // regardless of the host buffer size, so the metered level (and its + // ballistics) no longer change when the audio device block size changes. + static constexpr double integrationSeconds = 0.3; + + if (numSamples <= 0) + return; + + const double fs = sampleRate > 0.0 ? sampleRate : 44100.0; + const float ms = blockRMS * blockRMS; + const float alpha = (float) (1.0 - std::exp (-(double) numSamples / (integrationSeconds * fs))); + meter.meanSquare += (ms - meter.meanSquare) * alpha; + meter.value.set (std::sqrt (meter.meanSquare)); +} + +void Processor::setInputRMS (int chan, float rms, int numSamples) { if (chan < inRMS.size()) - inRMS.getUnchecked (chan)->set (val); + updateRMSMeter (*inRMS.getUnchecked (chan), rms, numSamples); } -void Processor::setOutputRMS (int chan, float val) +void Processor::setOutputRMS (int chan, float rms, int numSamples) { if (chan < outRMS.size()) - outRMS.getUnchecked (chan)->set (val); + updateRMSMeter (*outRMS.getUnchecked (chan), rms, numSamples); } bool Processor::isSuspended() const @@ -339,19 +357,11 @@ void Processor::prepare (const double newSampleRate, inRMS.clearQuick (true); for (int i = 0; i < getNumAudioInputs(); ++i) - { - AtomicValue* avf = new AtomicValue(); - avf->set (0); - inRMS.add (avf); - } + inRMS.add (new RMSMeter()); outRMS.clearQuick (true); for (int i = 0; i < getNumAudioOutputs(); ++i) - { - AtomicValue* avf = new AtomicValue(); - avf->set (0); - outRMS.add (avf); - } + outRMS.add (new RMSMeter()); } } @@ -778,18 +788,10 @@ void Processor::setPorts (const PortList& newPorts) if (isPrepared) { for (int i = inRMS.size(); i < getNumAudioInputs(); ++i) - { - auto* avf = new AtomicValue(); - avf->set (0); - inRMS.add (avf); - } + inRMS.add (new RMSMeter()); for (int i = outRMS.size(); i < getNumAudioOutputs(); ++i) - { - auto* avf = new AtomicValue(); - avf->set (0); - outRMS.add (avf); - } + outRMS.add (new RMSMeter()); } } diff --git a/src/ui/simplemeter.cpp b/src/ui/simplemeter.cpp index 58d350a95..46fdf4206 100644 --- a/src/ui/simplemeter.cpp +++ b/src/ui/simplemeter.cpp @@ -6,6 +6,10 @@ // Meter level limits (in dB). #define DIGITAL_METER_MAX_DB (+4.0f) #define DIGITAL_METER_MIN_DB (-70.0f) +// AES-17 dBFS calibration: the incoming value is a linear RMS amplitude, whose +// level for a sine sits 3.01 dB below its peak. Adding this offset defines 0 dBFS +// as a full-scale sine, so a -20 dBFS sine reads -20 dB rather than -23 dB. +#define DIGITAL_METER_RMS_CAL_DB (+3.0103f) // The decay rates (magic goes here :). // - value decay rate (faster) #define DIGITAL_METER_DECAY_RATE1 (1.0f - 3E-2f) @@ -78,7 +82,7 @@ void SimpleMeterValue::paint (Graphics& g) float dB = DIGITAL_METER_MIN_DB; if (value > 0.0f) - dB = 20.0f * log10f (value); + dB = 20.0f * log10f (value) + DIGITAL_METER_RMS_CAL_DB; if (dB < DIGITAL_METER_MIN_DB) dB = DIGITAL_METER_MIN_DB;