True Peak Measuring: Is it really that simple?

According to this ITU guide (page 16 onwards)
https://www.itu.int/dms_pubrec/itu-r/rec/bs/R-REC-BS.1770-4-201510-I!!PDF-E.pdf

measuring true peak would need the following steps:

  • attenuate the signal by 12.04dB if integer arithmetic is used (not needed with floats in JUCE)
  • oversampling up to 4x to reach 192kHz (or 176.4kHz @ 44.1 sample rate)
  • filtering the oversampled signal with a FIR (see coeffs in the guide)
  • reading the absolute value of the result
  • convert into dB
  • raise the gain by 12.04dB (see above)

I’ve built a minimal example plug-in with juce::dsp::Oversampling and using the included stock FIR filtering.

The result seems to be correct comparing with other commercial plugins.

Is that really all I need to do? That would be too good to be true.

Off topic: in dsp::Oversampling class, you have to define the num of channels during construction, so I’m using a std::unique_ptr instead of a stack variable and instantiate it in prepareToPlay. Is there any issues or performance penalty doing it that way?

Here is my code (any optimisation advice are welcome :wink:)

Thanks
Stefan

PluginEditor.h:

#pragma once

#include <JuceHeader.h>

#include "PluginProcessor.h"

//==============================================================================

/**

*/

class TruePeakMeteringAudioProcessorEditor : public juce::AudioProcessorEditor, public Timer

{

public :

TruePeakMeteringAudioProcessorEditor (TruePeakMeteringAudioProcessor&);

~TruePeakMeteringAudioProcessorEditor() override;

//==============================================================================

void paint (juce::Graphics&) override;

**void** resized() **override** ;

**void** timerCallback() **override**;

**private** :

// This reference is provided as a quick way for your editor to

// access the processor object that created it.

TruePeakMeteringAudioProcessor& audioProcessor;

Label headline;

Label truePeakLabel;

JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (TruePeakMeteringAudioProcessorEditor)

};

PluginEditor.cpp:


/*
  ==============================================================================

    This file contains the basic framework code for a JUCE plugin editor.

  ==============================================================================
*/

#include "PluginProcessor.h"
#include "PluginEditor.h"

//==============================================================================
TruePeakMeteringAudioProcessorEditor::TruePeakMeteringAudioProcessorEditor (TruePeakMeteringAudioProcessor& p)
    : AudioProcessorEditor (&p), audioProcessor (p)
{
 
    addAndMakeVisible(headline);
    headline.setText("Max True Peak", dontSendNotification);
    headline.setFont(Font(30));
    headline.setJustificationType(Justification::centred);
    
    addAndMakeVisible(truePeakLabel);
    truePeakLabel.setFont(Font(60));
    truePeakLabel.setJustificationType(Justification::centred);
    truePeakLabel.setText("---", dontSendNotification);
    
    startTimerHz(30);
    
    setSize (400, 300);
}

TruePeakMeteringAudioProcessorEditor::~TruePeakMeteringAudioProcessorEditor()
{
}

//==============================================================================
void TruePeakMeteringAudioProcessorEditor::paint (juce::Graphics& g)
{
    // (Our component is opaque, so we must completely fill the background with a solid colour)
    g.fillAll (getLookAndFeel().findColour (juce::ResizableWindow::backgroundColourId));

}

void TruePeakMeteringAudioProcessorEditor::resized()
{

    auto area = getLocalBounds().removeFromBottom(250);
    
    auto headlineArea = area.removeFromTop(50);
    headline.setBounds(headlineArea);
    
    truePeakLabel.setBounds(area);
    
}


void TruePeakMeteringAudioProcessorEditor::timerCallback() {
    
    float dbValue = Decibels::gainToDecibels(audioProcessor.getMaxTruePeak());
    auto str = String(dbValue, 1) + "\n(" + String(dbValue) + ")";
    truePeakLabel.setText(str, dontSendNotification);
}

PluginProcessor.h:

/*

==============================================================================

This file contains the basic framework code for a JUCE plugin processor.

==============================================================================

*/

#pragma once

#include <JuceHeader.h>

//==============================================================================

/**

*/

**class** TruePeakMeteringAudioProcessor : **public** juce::AudioProcessor

#if JucePlugin_Enable_ARA

, **public** juce::AudioProcessorARAExtension

#endif

{

**public** :

//==============================================================================

TruePeakMeteringAudioProcessor();

~TruePeakMeteringAudioProcessor() **override**;

//==============================================================================

**void** prepareToPlay (**double** sampleRate, **int** samplesPerBlock) **override**;

**void** releaseResources() **override**;

#ifndef JucePlugin_PreferredChannelConfigurations

**bool** isBusesLayoutSupported (**const** BusesLayout& layouts) **const** **override**;

#endif

**void** processBlock (juce::AudioBuffer<**float**>&, juce::MidiBuffer&) **override**;

//==============================================================================

juce::AudioProcessorEditor* createEditor() **override**;

**bool** hasEditor() **const** **override** ;

//==============================================================================

**const** juce::String getName() **const** **override** ;

**bool** acceptsMidi() **const** **override** ;

**bool** producesMidi() **const** **override** ;

**bool** isMidiEffect() **const** **override** ;

**double** getTailLengthSeconds() **const** **override**;

//==============================================================================

**int** getNumPrograms() **override**;

**int** getCurrentProgram() **override**;

**void** setCurrentProgram (**int** index) **override**;

**const** juce::String getProgramName (**int** index) **override**;

**void** changeProgramName (**int** index, **const** juce::String& newName) **override**;

//==============================================================================

**void** getStateInformation (juce::MemoryBlock& destData) **override**;

**void** setStateInformation (**const** **void*** data, **int** sizeInBytes) **override**;

**float** getMaxTruePeak();

**private** :

std::unique_ptr<dsp::Oversampling<float>> oversamplingProcessor;

Atomic<**float**> maxTruePeak;

//==============================================================================

JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (TruePeakMeteringAudioProcessor)

};

PluginProcessor.cpp:

#include "PluginProcessor.h"
#include "PluginEditor.h"

//==============================================================================
TruePeakMeteringAudioProcessor::TruePeakMeteringAudioProcessor()
#ifndef JucePlugin_PreferredChannelConfigurations
     : AudioProcessor (BusesProperties()
                     #if ! JucePlugin_IsMidiEffect
                      #if ! JucePlugin_IsSynth
                       .withInput  ("Input",  juce::AudioChannelSet::stereo(), true)
                      #endif
                       .withOutput ("Output", juce::AudioChannelSet::stereo(), true)
                     #endif
                       )
#endif
{
}

TruePeakMeteringAudioProcessor::~TruePeakMeteringAudioProcessor()
{
}


// ...

//==============================================================================
void TruePeakMeteringAudioProcessor::prepareToPlay (double sampleRate, int samplesPerBlock)
{
    int numInputChannels = getTotalNumInputChannels();
    
    /// TRUE PEAK (OVERSAMPLING)
    //============================================================================================
    /// see https://www.itu.int/dms_pubrec/itu-r/rec/bs/R-REC-BS.1770-4-201510-I!!PDF-E.pdf ; page 16
    /// 192kHz -> no oversampling; 96/88.2kHz -> 2x oversampling; 48/44.1kHz -> 4x oversampling
    int oversamplingFactor = 1 / (sampleRate / 192000);
    if (oversamplingFactor > 1) {
        int order = std::log2(oversamplingFactor);
        oversamplingProcessor.reset(new dsp::Oversampling<float>(numInputChannels, order, dsp::Oversampling<float>::FilterType::filterHalfBandFIREquiripple));
        oversamplingProcessor->initProcessing(samplesPerBlock);
        oversamplingProcessor->reset();
    }
    else oversamplingProcessor.reset();
    
}


void TruePeakMeteringAudioProcessor::processBlock (juce::AudioBuffer<float>& buffer, juce::MidiBuffer& midiMessages)
{
    juce::ScopedNoDenormals noDenormals;
    auto totalNumInputChannels  = getTotalNumInputChannels();
    auto totalNumOutputChannels = getTotalNumOutputChannels();
    

    auto audioBlock = dsp::AudioBlock<float>(buffer);
//    auto osBlock = dsp::AudioBlock<float>(buffer);
    
    
    /// TRUE PEAK (OVERSAMPLING)
    //============================================================================================
    auto truePeak = 0.0f;
    auto readTruePeak = [&truePeak] (dsp::AudioBlock<float>& block) {
        for (int i = 0; i < block.getNumChannels(); i++)
            for (int j = 0; j < block.getNumSamples(); j++)
                truePeak = jmax(truePeak, std::abs(block.getSample(i, j)));
    };
    
    if (oversamplingProcessor != nullptr) {
        auto osBlock = oversamplingProcessor->processSamplesUp(audioBlock);
        readTruePeak(osBlock);
    }
    /// sampleRate already 192kHz
    else readTruePeak(audioBlock);
    
    maxTruePeak = jmax(truePeak, maxTruePeak.get());
    


    // In case we have more outputs than inputs, this code clears any output
    // channels that didn't contain input data, (because these aren't
    // guaranteed to be empty - they may contain garbage).
    // This is here to avoid people getting screaming feedback
    // when they first compile a plugin, but obviously you don't need to keep
    // this code if your algorithm always overwrites all the output channels.
    for (auto i = totalNumInputChannels; i < totalNumOutputChannels; ++i)
        buffer.clear (i, 0, buffer.getNumSamples());

}


// ...


float TruePeakMeteringAudioProcessor::getMaxTruePeak() {
    return maxTruePeak.get();
}
2 Likes

Since no one answered so far: Yes, it is that simple :wink:

However, a bit explanation on the steps mentioned in the ITU paper you linked, as I find them a bit confusing on the first read.

  • As you already found out, with floating point arithmetic there is no need to attenuate and then amplify the signal again by 12.04dB since you have plenty of headroom with float numbers left.
  • The filtering mentioned in the paper is a usual thing to do when oversampling.
    Typical non-fractional oversampling implementations insert some zeros, low pass filter that signal and then apply a makeup gain on the filtered signal (the gain can also already be part of the filter coefficients). If you use the juce oversampling implementation, it will hand you an already filtered block of samples. There is no need to add another explicit filter after the JUCE oversampler. Note that the JUCE oversampling class gives you the choice between efficient but less precise IIR filters and a more precise but also more computationally expensive FIR filter. The FIR filter is the right choice here.

So in the JUCE world, all you have to do is

  • Sample the signal up to approx 196 kHz
  • Find the max absolute value in the upsampled audio block

I had a quick look at your code, you seem to do both anyway.

Using a unique ptr here is totally fine and you shouldn’t expect any performance penalty here when accessing it once per process callback.

A tiny bit of room for optimisation can be found in the readTruePeak lambda. I’d use a AudioBlock::findMinAndMax here, which probably uses some accelerated vector function to iterate over the channel vectors. Also in performance critical code, it’s a good idea to assign the target value in a for loop to a local variable and always use a pre-increment operator.

auto readTruePeak = [] (dsp::AudioBlock<float>& block) 
{
    auto minMax = block.findMinAndMax();
    return = std::max (std::abs (minMax.getStart()), std::abs (minMax.getEnd()));
};
2 Likes

Awesome, thank you very much for your feedback! Much appreciated

Maybe a bit nitpicking. Does the filter of the juce oversampling exactly match the specification, I guess not :wink:

You might be right here indeed, but adding the specified filter on an signal already filtered by the juce oversampler would also not make it any more correct :wink:

Tbh. I don’t know what the ITU specified filter exactly looks like, but my guess is that it won’t matter that much in real world. In any case, the JUCE oversampling implementation using cascaded resampling stages with halfband filters is probably computationally quite a bit more efficient as they save quite a lot multiplications without sacrifying any precision – at least that’s what we found out during some extensive benchmarking of resampling implementation topologies. We never had issues computing correct true peak values in our projects using roughly that approach.

I think that the described method is given as an example of how you might calculate a true peak, not as a definition. By that I mean you can have a different method as long as the results are similar to the true true peak (which one can define as approximately the same thing but with an oversampling ratio of infinity).

3 Likes

Thanks, but my actual point was that I’m not able to figure out the exact filter response from just looking at the coefficient table :wink:

Indeed, also because the specification or better recommendation is a bit vague.

“coefficients … that would satisfy the requirements would be as follows”

But I don’t find this quite convincing. It is not the sense of such a document, that just different measuring instruments also measure exactly the same? I think the effects can be quite dramatic, for example, if the measurement is used for loudness normalisation, from a technical perspective.

Especially when the material includes a lot of high frequency content, slightly different curves can result in very different true peak values.

I haven’t went over this specific document, but some loudness specs also come with an appendix which points to a set of audio examples and give a valid range for the measurement results for each one (i.e -20 [+/- 0.1] LKFS).

Specifically on true peak, for example Sound Radix POWAIR includes true peak metering but does it in a method different than the one mentioned above (we use a more efficient method).
When testing it and comparing with various other meters we saw both that the results are good and also that other various meters don’t all give the exact same measurement.

1 Like

For the EBU R128 standard there is a suite of test signals to pass to be complient. Also for dBTP.

5 Likes

bringing this one up again.
Got everything up and running :+1:
However, the oversampling is eating up a lot of CPU cycles.

I tried dsp::Oversampling and (presumably very efficient) r8brain (https://github.com/avaneev/r8brain-free-src)

I think both are overkill for only reading the peak value of each block. Does anybody has a more lightweight approach for that?

@yairadix: you mentioned using a more efficient method.

Thanks
Stefan

I’ve seen people mention only oversampling a narrowed down data set. So before oversampling, you can set some kind of gate, and look for a samples that go over a certain value. When such a value comes up, grab a window of samples either side of it, then oversample that range. Then you might be only oversampling and computing the true peak on 100 samples every few blocks.

2 Likes

interesting approach :thinking:
so when looking for max true peak, I could first check if the block’s magnitude is higher than, say 8dB below current max true peak. if so, finding the sample(s) that are above this threshold and oversample around them…
i’ll try that.

thanks!

By the way, the filter coefficients in the document are given as an example of what could work, but the specification doesn’t require you to use them. Anything that does the job is fine.

1 Like

:+1:

So far, the result with your coeffs (FIR and even IIR) seemed correct.

I did, and elaborating on it in detail would require looking at and deciphering old code and would take some effort. But I can say from my memory that the general idea of both our method and the method described in the ITU guide is simply to get enough sub-sample measurements to get a close enough peek at the true peak, and there are more ways to get sub-sample interpolations, including several built-in in JUCE iiuc.

1 Like

I’ve made some more tests.

I tried that approach but did not get good results. Not sure though if it was my implementation or the whole approach of only oversampling/reading around a peaking sample.

What did help quite a bit is first looking if the magnitude of the buffer is above a certain threshold (in my case 5dB under max true peak) and only apply oversampling to those buffers. But the efficiency of course would be highly dependent on the overall loudness of the audio (how often is the buffer near max true peak).

thanks yairadix for pointing me to that direction. Instead of oversampling, I tried JUCE’s Lagrange interpolator and the CPU load is a lot better than oversampling. The results seems to be okay too for the first tests. Measuring with EBU 3341 testfiles (the phase shift ones are interesting) it is not that accurate as the oversampling version but still compliant. On real life tests there was no difference to the usual suspects on the market. But I’ll have to do more tests.

BUT: all approaches don’t cope well with white noise. I got different results when comparing the measurements at 192kHz (aka no processing) comparing to 48kHz. When feeding -20dB of white noise, the true peak reading is also -20dBTP at 192kHz
But at 48kHz I got readings 2.5dB higher when interpolating and 7.5dB higher when oversampling. The problem is, I compared against several other true peak meters (Youlean, Avid ProLim, …) and they also give different results on both tests.

:man_shrugging:

1 Like

That’s an interesting experiment. Note that the noises are a bit different as the high sample rate one has all the extra high frequencies in it.
I wonder what would happen if you took your 192kHz white noise, down-sample it to 48kHz and then check the levels on that.

quick and dirty test

white noise -20dB at 192kHz converted to 48kHz (Pro Tools SRC):
-18.7dBTP (interpolating) -17.3dBTP (oversampling) -18.8dBTP (Avid Pro Lim)
white noise -20dB at 192kHz played back in 48kHz (no SRC, played back slower):
-17.5dBTP (interpolating) -12.3dBTP (oversampling) -15dBTP (Avid Pro Lim)

white noise -20dB at 48kHz played back in 48kHz:
-17.5dBTP (interpolating) -12.7dBTP (oversampling) -14.9dBTP (Avid Pro Lim)

white noise -20dB at 48kHz converted to 192kHz (Pro Tools SRC):
-13.7dbTP (interpolating) -13.7dBTP (oversampling) -13.7dBTP (Avid Pro Lim)
white noise -20dB at 48kHz played back in 192kHz (no SRC, played back faster):
-17.3dBTP (interpolating) -20.0dBTP (oversampling) -20dBTP (Avid Pro Lim)

white noise -20dB at 192kHz played back in 192kHz:
-17.5 dBTP (interpolating) -20dBTP (oversampling) -20.0 dBTP (Avid Pro Lim)

interpolating is almost always 2.5dB higher but constant. All other readings give complete different results …