DSPark 1.8.0
Header-only C++20 DSP for real-time and offline audio
Loading...
Searching...
No Matches
LoudnessNormalizer.h
1// DSPark - Professional Audio DSP Framework
2// Copyright (c) 2026 Cristian Moresi - MIT License
3
4#pragma once
5
44#include "../Core/AudioBuffer.h"
45#include "../Core/DenormalGuard.h"
46#include "../Core/DspMath.h"
47#include "../Core/TruePeakDetector.h"
48#include "LoudnessMeter.h"
49
50#include <algorithm>
51#include <atomic>
52#include <cmath>
53#include <cstdint>
54#include <limits>
55
56namespace dspark {
57
69template <FloatType T>
71{
72public:
74 enum class Status : std::uint8_t
75 {
76 Success,
82 };
83
85 struct Result
86 {
88 T measuredLUFS = T(-100);
89 T requestedGainDb = T(0);
90 T appliedGainDb = T(0);
91 T outLUFS = T(-100);
92 T outTruePeakDb = T(-100);
93 bool targetReached = false;
94 bool ceilingLimited = false;
95 };
96
97 // -- Parameters (control thread, between runs) ----------------------------
98
100 void setTargetLUFS(T target) noexcept
101 {
102 if (std::isfinite(target))
103 targetLUFS_.store(target, std::memory_order_relaxed);
104 }
105
107 [[nodiscard]] T getTargetLUFS() const noexcept
108 {
109 return targetLUFS_.load(std::memory_order_relaxed);
110 }
111
118 void setTruePeakCeilingDb(T ceilingDb) noexcept
119 {
120 if (std::isfinite(ceilingDb))
121 ceilingDb_.store(std::clamp(ceilingDb, T(-40), T(0)),
122 std::memory_order_relaxed);
123 }
124
126 [[nodiscard]] T getTruePeakCeilingDb() const noexcept
127 {
128 return ceilingDb_.load(std::memory_order_relaxed);
129 }
130
131 // -- Offline processing ---------------------------------------------------
132
144 template <int MaxChannels>
146 double sampleRate)
147 {
148 DenormalGuard guard;
149
150 Result out;
151 const int numChannels = audio.getNumChannels();
152 const int numSamples = audio.getNumSamples();
153 if (numChannels <= 0 || numSamples <= 0)
154 {
156 return out;
157 }
158 if (!std::isfinite(sampleRate) || sampleRate <= 0.0)
159 {
161 return out;
162 }
163
164 T maxAbsInput = T(0);
165 for (int channel = 0; channel < numChannels; ++channel)
166 {
167 const T* samples = audio.getChannel(channel);
168 for (int i = 0; i < numSamples; ++i)
169 {
170 const T sample = samples[i];
171 if (!std::isfinite(sample))
172 {
174 return out;
175 }
176 maxAbsInput = std::max(maxAbsInput, std::abs(sample));
177 }
178 }
179
180 const T measured = measureIntegrated(audio, sampleRate);
181 if (!meter_.isMeasurementValid())
182 {
184 return out;
185 }
186 const T inputTruePeak = measureTruePeakLinear(audio);
187 if (!std::isfinite(measured) || !std::isfinite(inputTruePeak)
188 || inputTruePeak < T(0))
189 {
191 return out;
192 }
193
194 if (measured <= kMeterFloorLUFS || !(inputTruePeak > T(0)))
195 {
196 out.measuredLUFS = measured;
198 out.outLUFS = measured;
199 out.outTruePeakDb = linearToDecibels(inputTruePeak, T(-100));
200 return out;
201 }
202
203 const T target = targetLUFS_.load(std::memory_order_relaxed);
204 const T ceilingDb = ceilingDb_.load(std::memory_order_relaxed);
205 const T requestedGainDb = target - measured;
206 const T requestedLinear = decibelsToLinear(requestedGainDb);
207 const T ceilingLinear = decibelsToLinear(ceilingDb);
208 const T ceilingSafeLinear = static_cast<T>(
209 static_cast<double>(ceilingLinear) * (1.0 - kCeilingAimMargin)
210 / static_cast<double>(inputTruePeak));
211 const T appliedLinear = std::min(requestedLinear, ceilingSafeLinear);
212 const T appliedGainDb = linearToDecibels(appliedLinear, T(0));
213
214 if (!std::isfinite(requestedGainDb) || !std::isfinite(requestedLinear)
215 || !std::isfinite(ceilingLinear)
216 || !std::isfinite(ceilingSafeLinear)
217 || !std::isfinite(appliedLinear) || !(appliedLinear > T(0))
218 || !std::isfinite(appliedGainDb)
219 || static_cast<long double>(maxAbsInput)
220 > static_cast<long double>(std::numeric_limits<T>::max())
221 / static_cast<long double>(appliedLinear))
222 {
224 return out;
225 }
226
227 out.measuredLUFS = measured;
228 out.requestedGainDb = requestedGainDb;
229 out.appliedGainDb = appliedGainDb;
230 out.ceilingLimited = ceilingSafeLinear < requestedLinear;
231
232 // This is the only caller-buffer mutation in the operation.
233 for (int channel = 0; channel < numChannels; ++channel)
234 {
235 T* samples = audio.getChannel(channel);
236 for (int i = 0; i < numSamples; ++i)
237 samples[i] = static_cast<T>(samples[i] * appliedLinear);
238 }
239
240 out.outLUFS = measureIntegrated(audio, sampleRate);
241 out.outTruePeakDb = linearToDecibels(measureTruePeakLinear(audio), T(-100));
242 out.targetReached = std::abs(out.outLUFS - target) <= kTargetToleranceLU;
244 return out;
245 }
246
247private:
248 static constexpr T kMeterFloorLUFS = T(-99);
249 static constexpr T kTargetToleranceLU = T(0.1);
250 static constexpr double kCeilingAimMargin = 1.0e-4;
251
252 [[nodiscard]] static T decibelsToLinear(T decibels) noexcept
253 {
254 return static_cast<T>(
255 std::pow(10.0, static_cast<double>(decibels) / 20.0));
256 }
257
258 [[nodiscard]] static T linearToDecibels(T gain, T zeroSentinel) noexcept
259 {
260 return gain > T(0)
261 ? static_cast<T>(20.0 * std::log10(static_cast<double>(gain)))
262 : zeroSentinel;
263 }
264
265 template <int MaxChannels>
266 [[nodiscard]] T measureIntegrated(const AudioBuffer<T, MaxChannels>& audio,
267 double sampleRate) noexcept
268 {
269 const int numChannels = audio.getNumChannels();
270 meter_.prepare(sampleRate, std::min(numChannels, 2));
271 meter_.reset();
272 if (numChannels >= 2)
273 meter_.process(audio.getChannel(0), audio.getChannel(1),
274 audio.getNumSamples());
275 else
276 meter_.process(audio.getChannel(0), audio.getNumSamples());
277 return meter_.getIntegratedLUFS();
278 }
279
280 template <int MaxChannels>
281 [[nodiscard]] static T measureTruePeakLinear(
282 const AudioBuffer<T, MaxChannels>& audio) noexcept
283 {
284 TruePeakDetector<T, MaxChannels> detector;
285 detector.reset();
286 T peak = T(0);
287 for (int channel = 0; channel < audio.getNumChannels(); ++channel)
288 {
289 const T* samples = audio.getChannel(channel);
290 for (int i = 0; i < audio.getNumSamples(); ++i)
291 peak = std::max(peak, detector.processSample(samples[i], channel));
292 for (int i = 0; i < TruePeakDetector<T, MaxChannels>::getTaps() - 1; ++i)
293 peak = std::max(peak, detector.processSample(T(0), channel));
294 }
295 return peak;
296 }
297
298 std::atomic<T> targetLUFS_ { T(-23) };
299 std::atomic<T> ceilingDb_ { T(-1) };
300 LoudnessMeter<T> meter_;
301};
302
303} // namespace dspark
Owning audio buffer with contiguous, 32-byte aligned storage.
int getNumChannels() const noexcept
Returns the number of active channels.
T * getChannel(int ch) noexcept
Returns a pointer to the sample data.
int getNumSamples() const noexcept
Returns the number of samples per channel.
RAII scope guard to disable denormalised (subnormal) floating-point numbers.
Offline waveform-preserving LUFS normalizer with a hard true-peak ceiling.
void setTruePeakCeilingDb(T ceilingDb) noexcept
Sets the true-peak ceiling (default -1 dBTP).
void setTargetLUFS(T target) noexcept
Sets the target integrated loudness (default -23 LUFS).
T getTruePeakCeilingDb() const noexcept
Result normalize(AudioBuffer< T, MaxChannels > &audio, double sampleRate)
Normalizes audio in place with one validated constant gain.
Status
Completion or conservative no-mutation outcome.
Main namespace for the DSPark framework.
Measurements and the constant-gain decision for one run.
T appliedGainDb
One gain applied to every sample.
T outTruePeakDb
All-channel output true peak.
bool targetReached
Output is within 0.1 LU of target.
bool ceilingLimited
The ceiling selected the lower gain.
T outLUFS
Re-measured output loudness.
T measuredLUFS
Integrated loudness of the input.
T requestedGainDb
Gain needed to reach the target.