Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@

#include <limits>

#include "TruePeak.h"

MeasureEBU128::MeasureEBU128(const double sampleRate,
const juce::AudioChannelSet& channelSet)
: loudnessMeter_(), kSampleRate_(sampleRate), playbackLayout_(channelSet) {
Expand All @@ -28,12 +30,10 @@ MeasureEBU128::MeasureEBU128(const double sampleRate,
MeasureEBU128::LoudnessStats MeasureEBU128::measureLoudness(
const juce::AudioChannelSet& currPlaybackLayout,
const juce::AudioBuffer<float>& buffer) {
// If the playback layout has changed or the buffer isn't sized as expected
// reconfigure, reset internal loudness stats.
// If the playback layout has changed, reconfigure and reset internal loudness
// stats.
if (buffer.getNumChannels() != currPlaybackLayout.size() ||
playbackLayout_ != currPlaybackLayout ||
upsampledBuffer_.getNumSamples() <
buffer.getNumSamples() * upsampleRatio_) {
playbackLayout_ != currPlaybackLayout) {
reset(currPlaybackLayout, buffer);
LOG_INFO(
0, "measureLoudness: Mismatch between provided layout and buffer size");
Expand All @@ -60,28 +60,11 @@ MeasureEBU128::LoudnessStats MeasureEBU128::measureLoudness(
void MeasureEBU128::reset(const juce::AudioChannelSet& currPlaybackLayout,
const juce::AudioBuffer<float>& buffer) {
playbackLayout_ = currPlaybackLayout;
upsampleRatio_ = 192e3 / kSampleRate_; // ITU 1770-5 Annex 2.
loudnessMeter_.prepareToPlay(kSampleRate_, playbackLayout_.size(),
buffer.getNumSamples(), 1);

int numChannels = playbackLayout_.size();
perChannelResamplers_.clear();
for (int i = 0; i < numChannels; ++i) {
perChannelResamplers_.emplace_back(juce::Interpolators::Lagrange());
}
upsampledBuffer_.setSize(numChannels,
buffer.getNumSamples() * upsampleRatio_);

lpf_.block = juce::dsp::AudioBlock<float>(upsampledBuffer_);
lpf_.filter.state =
juce::dsp::FilterDesign<float>::designFIRLowpassWindowMethod(
20e3, upsampleRatio_ * kSampleRate_, 49,
juce::dsp::WindowingFunction<float>::hann);
juce::dsp::ProcessSpec spec{upsampleRatio_ * kSampleRate_,
(unsigned)upsampledBuffer_.getNumSamples(),
(unsigned)upsampledBuffer_.getNumChannels()};
lpf_.filter.prepare(spec);
lpf_.filter.reset();
// Initialize true peak meter
truePeakMeter_.reset(kSampleRate_, currPlaybackLayout);

loudnessStats_ = {-std::numeric_limits<float>::infinity(),
-std::numeric_limits<float>::infinity(),
Expand All @@ -91,38 +74,10 @@ void MeasureEBU128::reset(const juce::AudioChannelSet& currPlaybackLayout,
-std::numeric_limits<float>::infinity()};
}

// ITU 1770-5 Annex 2.
// ITU-R BS.1770-4 True Peak using polyphase FIR filter.
float MeasureEBU128::calculateTruePeakLevel(
const juce::AudioBuffer<float>& buffer) {
// Upsample buffer. LPF.
for (int i = 0; i < buffer.getNumChannels(); ++i) {
// Skip LFE channel.
if (playbackLayout_.getTypeOfChannel(i) == juce::AudioChannelSet::LFE) {
upsampledBuffer_.clear(i, 0, upsampledBuffer_.getNumSamples());
continue;
}
perChannelResamplers_[i].process(
1.0f / upsampleRatio_, buffer.getReadPointer(i),
upsampledBuffer_.getWritePointer(i), upsampledBuffer_.getNumSamples());
}

// LPF.
juce::dsp::ProcessContextReplacing<float> ctx(lpf_.block);
lpf_.filter.process(ctx);

// Max absolute value over all channels.
float truePeak =
upsampledBuffer_.getMagnitude(0, upsampledBuffer_.getNumSamples());

// Convert to dB TP
float truePeakdB = 20.0f * std::log10(truePeak);

// Do some sanitation here as resampling can introduce unreasonably high
// values.
if (truePeakdB > 15.f) {
return std::numeric_limits<float>::quiet_NaN();
}
return truePeakdB;
return truePeakMeter_.compute(buffer);
}

// digital_peak specifies the the digital (sampled) peak of the audio signal.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
#include <logger/logger.h>

#include "EBU128LoudnessMeter.h"
#include "TruePeak.h"

class MeasureEBU128 {
public:
Expand All @@ -32,10 +33,6 @@ class MeasureEBU128 {
/**
* @brief Create a loudness measurement object for a given sample rate and
* rendering layout.
* NOTE:
* Filter coefficients are hard-coded from ITU 1770 for a sample rate of
* 48kHz, so calculations at other sample rates are currently expected to be
* inaccurate.
* @param sampleRate
* @param chData Playback layout for which to measure loudness.
*/
Expand Down Expand Up @@ -65,24 +62,15 @@ class MeasureEBU128 {

float calculateDigitalPeak(const juce::AudioBuffer<float>& buffer);

struct LPF {
juce::dsp::AudioBlock<float> block;
juce::dsp::ProcessorDuplicator<juce::dsp::FIR::Filter<float>,
juce::dsp::FIR::Coefficients<float>>
filter;
};
// Playback information
const double kSampleRate_;
juce::AudioChannelSet playbackLayout_;

// Library for calculating loudness and range values
Ebu128LoudnessMeter loudnessMeter_;

// Resamplers for true peak calculation.
int upsampleRatio_ = 4; // Upsampling ratio for true peak calculation.
juce::AudioBuffer<float> upsampledBuffer_; // Larger buffer to upsample into.
std::vector<juce::Interpolators::Lagrange> perChannelResamplers_;
LPF lpf_;
// True peak calculator
TruePeak truePeakMeter_;

// Internal copy of calculated loudness statistics to return when
// loudnesses' are queried between measurement periods.
Expand Down
134 changes: 134 additions & 0 deletions common/processors/mix_monitoring/loudness_standards/TruePeak.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#include "TruePeak.h"

void TruePeak::reset(const double sampleRate,
const juce::AudioChannelSet& channelSet) {
channelSet_ = channelSet;
// Initialize history arrays for each channel
channelHistory_.clear();
channelHistory_.resize(channelSet.size());
for (auto& history : channelHistory_) {
history.fill(0.0f);
}
currentTruePeak_ = -std::numeric_limits<float>::infinity();
}

float TruePeak::compute(const juce::AudioBuffer<float>& buffer) {
if (channelSet_.isDisabled()) {
return -1000;
}
if (buffer.getNumChannels() != channelSet_.size()) {
return -1000;
}

currentTruePeak_ = -std::numeric_limits<float>::infinity();

// Process each channel
for (int ch = 0; ch < buffer.getNumChannels(); ++ch) {
// Skip LFE channel
if (channelSet_.getTypeOfChannel(ch) == juce::AudioChannelSet::LFE) {
continue;
}

const float* inputData = buffer.getReadPointer(ch);
int numSamples = buffer.getNumSamples();
auto& history = channelHistory_[ch];

// Phase 1: Process the first 11 samples (requires history)
int overlapCount = std::min(numSamples, kHistorySize);
for (int i = 0; i < overlapCount; ++i) {
processSingleSampleWithHistory(inputData, i, history);
}

// Phase 2: Process the rest of the block
for (int i = kHistorySize; i < numSamples; ++i) {
processSingleSampleLinear(inputData, i);
}

// Phase 3: Save the last 11 samples for the next block's history
if (numSamples >= kHistorySize) {
for (int i = 0; i < kHistorySize; ++i) {
history[i] = inputData[numSamples - kHistorySize + i];
}
} else {
// Edge case: Block size is smaller than 11
// Shift history left and append new samples
int shift = kHistorySize - numSamples;
for (int i = 0; i < shift; ++i) {
history[i] = history[i + numSamples];
}
for (int i = 0; i < numSamples; ++i) {
history[shift + i] = inputData[i];
}
}
}

// Convert to dB and sanitize outputs
if (currentTruePeak_ > 0.0f) {
float truePeakdB = 20.0f * std::log10(currentTruePeak_);
if (truePeakdB > 15.0f) {
return std::numeric_limits<float>::quiet_NaN();
}
return truePeakdB;
}

return -std::numeric_limits<float>::infinity();
}

void TruePeak::processSingleSampleLinear(const float* data, int currentIndex) {
// Calculate the 4 upsampled points directly from the linear buffer
float out0 = 0.0f, out1 = 0.0f, out2 = 0.0f, out3 = 0.0f;

for (int m = 0; m < kTapsPerPhase; ++m) {
float sample = data[currentIndex - m];
out0 += kPhase0[m] * sample;
out1 += kPhase1[m] * sample;
out2 += kPhase2[m] * sample;
out3 += kPhase3[m] * sample;
}

updatePeak(out0, out1, out2, out3);
}

void TruePeak::processSingleSampleWithHistory(
const float* data, int currentIndex,
const std::array<float, kHistorySize>& history) {
// Calculate the 4 upsampled points using history when necessary
float out0 = 0.0f, out1 = 0.0f, out2 = 0.0f, out3 = 0.0f;

for (int m = 0; m < kTapsPerPhase; ++m) {
float sample;
int index = currentIndex - m;
if (index >= 0) {
sample = data[index];
} else {
// Read from history array
sample = history[kHistorySize + index];
}

out0 += kPhase0[m] * sample;
out1 += kPhase1[m] * sample;
out2 += kPhase2[m] * sample;
out3 += kPhase3[m] * sample;
}

updatePeak(out0, out1, out2, out3);
}

void TruePeak::updatePeak(float o0, float o1, float o2, float o3) {
currentTruePeak_ = std::max({currentTruePeak_, std::abs(o0), std::abs(o1),
std::abs(o2), std::abs(o3)});
}
73 changes: 73 additions & 0 deletions common/processors/mix_monitoring/loudness_standards/TruePeak.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#pragma once

#include <juce_audio_basics/juce_audio_basics.h>

#include <vector>

/**
* @brief Computes the `true peak` measurement described in ITU-R BS.1770-4.
* Uses a polyphase filter approach similar to the FFmpeg.
*
*/

class TruePeak {
public:
float compute(const juce::AudioBuffer<float>& buffer);
void reset(const double sampleRate, const juce::AudioChannelSet& channelSet);

private:
// ITU-R BS.1770-4 48-tap FIR filter coefficients split into 4 phases of 12
// taps
static constexpr int kHistorySize = 11;
static constexpr int kTapsPerPhase = 12;
static constexpr int kNumPhases = 4;

// The 4 polyphase filter coefficient arrays (12 taps each)
static constexpr float kPhase0[kTapsPerPhase] = {
0.0017089843750f, 0.0109863281250f, -0.0196533203125f,
0.0332031250000f, -0.0594482421875f, 0.1373291015625f,
0.9721679687500f, -0.1022949218750f, 0.0476074218750f,
-0.0266113281250f, 0.0148925781250f, -0.0083007812500f};

static constexpr float kPhase1[kTapsPerPhase] = {
-0.0291748046875f, 0.0292968750000f, -0.0517578125000f,
0.0891113281250f, -0.1665039062500f, 0.4650878906250f,
0.7797851562500f, -0.2003173828125f, 0.1015625000000f,
-0.0582275390625f, 0.0330810546875f, -0.0189208984375f};

static constexpr float kPhase2[kTapsPerPhase] = {
-0.0189208984375f, 0.0330810546875f, -0.0582275390625f,
0.1015625000000f, -0.2003173828125f, 0.7797851562500f,
0.4650878906250f, -0.1665039062500f, 0.0891113281250f,
-0.0517578125000f, 0.0292968750000f, -0.0291748046875f};

static constexpr float kPhase3[kTapsPerPhase] = {
-0.0083007812500f, 0.0148925781250f, -0.0266113281250f,
0.0476074218750f, -0.1022949218750f, 0.9721679687500f,
0.1373291015625f, -0.0594482421875f, 0.0332031250000f,
-0.0196533203125f, 0.0109863281250f, 0.0017089843750f};

void processSingleSampleLinear(const float* data, int currentIndex);
void processSingleSampleWithHistory(
const float* data, int currentIndex,
const std::array<float, kHistorySize>& history);
void updatePeak(float o0, float o1, float o2, float o3);

juce::AudioChannelSet channelSet_ = juce::AudioChannelSet::disabled();
std::vector<std::array<float, kHistorySize>> channelHistory_;
float currentTruePeak_ = -std::numeric_limits<float>::infinity();
};
1 change: 1 addition & 0 deletions common/processors/processors.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
#include "mix_monitoring/MixMonitorProcessor.cpp"
#include "mix_monitoring/TrackMonitorProcessor.cpp"
#include "mix_monitoring/loudness_standards/MeasureEBU128.cpp"
#include "mix_monitoring/loudness_standards/TruePeak.cpp"
#include "panner/Panner3DProcessor.cpp"
#include "remapping/RemappingProcessor.cpp"
#include "render/RenderProcessor.cpp"
Expand Down
8 changes: 0 additions & 8 deletions third_party/LUFSMeter/src/EBU128LoudnessMeter.cpp
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These debug logs were adding a lot of unhelpful noise so I'm just removing them (which I believe is permissible under this license?)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, this is fine

Original file line number Diff line number Diff line change
Expand Up @@ -89,9 +89,6 @@ Ebu128LoudnessMeter::Ebu128LoudnessMeter()
loudnessRangeEnd(minimalReturnValue),
freezeLoudnessRangeOnSilence(false),
currentBlockIsSilent(false) {
DBG("The longest possible measurement until a buffer overflow = " +
juce::String(INT_MAX / 10. / 3600. / 365.) + " years");

// If this class is used without caution and processBlock
// is called before prepareToPlay, divisions by zero
// might occure. E.g. if numberOfSamplesInAllBins = 0.
Expand Down Expand Up @@ -143,8 +140,6 @@ void Ebu128LoudnessMeter::prepareToPlay(double sampleRate,
}
}

DBG("expectedRequestRate = " + juce::String(expectedRequestRate));

// Figure out how many bins are needed.
const int timeOfAccumulationForShortTerm =
3; // seconds.
Expand All @@ -154,9 +149,7 @@ void Ebu128LoudnessMeter::prepareToPlay(double sampleRate,
numberOfSamplesInAllBins = numberOfBins * numberOfSamplesPerBin;

numberOfBinsToCover100ms = int(0.1 * expectedRequestRate);
DBG("numberOfBinsToCover100ms = " + juce::String(numberOfBinsToCover100ms));
numberOfBinsToCover400ms = int(0.4 * expectedRequestRate);
DBG("numberOfBinsToCover400ms = " + juce::String(numberOfBinsToCover400ms));
numberOfSamplesIn400ms = numberOfBinsToCover400ms * numberOfSamplesPerBin;

currentBin = 0;
Expand Down Expand Up @@ -205,7 +198,6 @@ void Ebu128LoudnessMeter::processBlock(const juce::AudioSampleBuffer& buffer) {
const float magnitude = buffer.getMagnitude(0, buffer.getNumSamples());
if (magnitude < silenceThreshold) {
currentBlockIsSilent = true;
DBG("Silence detected.");
} else
currentBlockIsSilent = false;
}
Expand Down