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();
}
1 Like

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