HART  0.2.0
High level Audio Regression and Testing
Loading...
Searching...
No Matches
hart_true_peak.hpp
Go to the documentation of this file.
1#pragma once
2
3#include <algorithm> // fill()
4#include <vector>
5
7#include "hart_exceptions.hpp" // hassert()
9#include "hart_slice.hpp"
10#include "hart_utils.hpp" // Oversampling, floatsEqual(), decibelsToRatio(), nan()
11
12namespace hart
13{
14
15/// @brief Multi-channel True peak estimator, based on ITU-R BS.1770 measuring recommendations
16/// @details Meant to be used as an implementation for `truePeak()` metric and `TruePeaksBelow` matcher.
17/// @private
18template<typename SampleType>
19class TruePeak
20{
21public:
22 /// @brief Number of taps of the internal poly-phase FIR filter
23 enum FilterQuality
24 {
25 low = 12, //!< 12 taps
26 medium = 24, //!< 24 taps
27 high = 96 //!< 96 taps
28 };
29
30 struct Result
31 {
32 const SampleType valueLinear;
33 const size_t channel;
34 const double frame;
35 };
36
37 TruePeak (Oversampling oversamplingRatio = Oversampling::x4, FilterQuality filterQuality = FilterQuality::low) :
38 m_oversamplingRatio (oversamplingRatio),
39 m_filterQuality (filterQuality),
40 m_maximumUnderReadLinear (calculateMaximumUnderReadLinear())
41 {
42 hassert (! floatsEqual (m_maximumUnderReadLinear, (SampleType) 0));
43 hassert (m_maximumUnderReadLinear <= (SampleType) 1);
44 }
45
46 /// @param numActiveChannels Number of channels that we want to measure, e. g. the channels
47 /// selected via Matcher::atChannels(). Not the total number of channels in the audio buffers.
48 void prepare (double sampleRateHz, size_t numActiveChannels)
49 {
50 // TODO: If instantiated by a matcher, we can skip allocating history
51 // for channels that should be skipped
52
53 m_history.assign (numActiveChannels, std::vector<SampleType> (getTapsPerPhase(), (SampleType) 0));
54 m_historyTapIndex = 0;
55 m_offsetFrames = 0;
56 m_TruePeakLinear = (SampleType) 0;
57 buildPhaseCoefficients();
58
59 m_sampleRateHz = sampleRateHz; // Just for readable failure details
60 }
61
62 void reset()
63 {
64 for (std::vector<SampleType>& channelHistory : m_history)
65 std::fill (channelHistory.begin(), channelHistory.end(), (SampleType) 0);
66
67 m_historyTapIndex = 0;
68 m_offsetFrames = 0;
69 m_TruePeakLinear = (SampleType) 0;
70 }
71
72 Result estimate (const AudioBuffer<SampleType>& observedOutputAudio, std::vector<size_t>&& channels, Slice slice = Slice::whole())
73 {
74 if (channels.empty() || slice.isEmpty())
75 return { hart::nan<SampleType>(), 0, hart::nan<double>()};
76
77 hassert (m_history.size() == channels.size());
78 hassert (m_history[0].size() == getTapsPerPhase());
79
80 const auto sliceFrameIndices = observedOutputAudio.getFrameIndices (slice);
81 const size_t sliceStart = sliceFrameIndices.first;
82 const size_t sliceStop = sliceFrameIndices.second;
83 hassert (sliceStop > sliceStart);
84 hassert (sliceStop - sliceStart != 0);
85 hassert (sliceStop <= observedOutputAudio.getNumFrames());
86
87 const size_t ratio = getRatio();
88
89 SampleType truePeakValueLinear = (SampleType) 0;
90 size_t truePeakChannel = 0;
91 double truePeakFrame = 0.0;
92
93 for (size_t frame = sliceStart; frame < sliceStop; ++frame)
94 {
95 size_t historyChannelIndex = 0;
96
97 for (size_t channel : channels)
98 {
99 hassert (channel < observedOutputAudio.getNumChannels());
100 hassert (historyChannelIndex < m_history.size())
101
102 m_history[historyChannelIndex][m_historyTapIndex] = observedOutputAudio[channel][frame];
103
104 for (size_t phase = 0; phase < ratio; ++phase)
105 {
106 const SampleType oversampledPeakLinear = evaluatePolyphaseFIR (historyChannelIndex, phase);
107 const SampleType rectifiedPeakLinear = std::abs (oversampledPeakLinear);
108
109 if (rectifiedPeakLinear > truePeakValueLinear)
110 {
111 truePeakValueLinear = rectifiedPeakLinear;
112 truePeakChannel = channel;
113 truePeakFrame =
114 static_cast<double> (m_offsetFrames) +
115 static_cast<double> (phase) / static_cast<double> (ratio);
116 }
117
118 }
119
120 ++historyChannelIndex;
121 }
122
123 m_historyTapIndex = (m_historyTapIndex + 1) % getTapsPerPhase();
124 ++m_offsetFrames;
125 }
126
127 return {truePeakValueLinear, truePeakChannel, truePeakFrame};
128 }
129
130 SampleType getMaximumUnderReadLinear() const
131 {
132 return m_maximumUnderReadLinear;
133 }
134
135 friend std::ostream& operator<< (std::ostream& os, FilterQuality filterQuality)
136 {
137 os << "FilterQuality::";
138
139 switch (filterQuality)
140 {
141 case FilterQuality::low : os << "low"; break;
142 case FilterQuality::medium : os << "medium"; break;
143 case FilterQuality::high : os << "high"; break;
144 }
145
146 return os;
147 }
148
149private:
150 static constexpr double m_fNorm = 0.45; // It's a ratio, not Hz
151 const Oversampling m_oversamplingRatio;
152 const FilterQuality m_filterQuality;
153 const SampleType m_maximumUnderReadLinear;
154 double m_sampleRateHz = hart::nan<double>();
155 SampleType m_TruePeakLinear = static_cast<SampleType>(0);
156
157 // Outer vector = channels
158 // Inner vector = ring buffer of previous samples
159 // History index is shared since all channels advance in lockstep
160 std::vector<std::vector<SampleType>> m_history;
161 size_t m_historyTapIndex = 0;
162 size_t m_offsetFrames = 0;
163
164 // TODO: Flatten m_phaseCoefficients and m_history to 1D vectors?
165 std::vector<std::vector<SampleType>> m_phaseCoefficients;
166
167 inline SampleType calculateMaximumUnderReadLinear() const
168 {
169 // ITU-R BS.1770-5, Attachement 1 to Annex 2, Page 21
170 // https://www.itu.int/dms_pubrec/itu-r/rec/bs/R-REC-BS.1770-5-202311-I!!PDF-E.pdf
171
172 return static_cast<SampleType> (std::cos (pi * m_fNorm / getRatio()));
173 }
174
175 inline size_t getRatio() const
176 {
177 return static_cast<size_t> (m_oversamplingRatio);
178 }
179
180 inline size_t getTapsPerPhase() const
181 {
182 return static_cast<size_t> (m_filterQuality);
183 }
184
185 SampleType evaluatePolyphaseFIR (size_t historyChannelIndex, size_t phase) const
186 {
187 const auto& history = m_history[historyChannelIndex];
188 const auto& coeffs = m_phaseCoefficients[phase];
189
190 AccurateSum<SampleType> sum;
191 const size_t size = history.size();
192
193 for (size_t tap = 0; tap < coeffs.size(); ++tap)
194 {
195 const size_t index = (m_historyTapIndex + size - tap) % size;
196 sum += history[index] * coeffs[tap];
197 }
198
199 return sum.getValue();
200 }
201
202 void buildPhaseCoefficients()
203 {
204 // Windowed-sinc polyphase FIR generator
205 // TODO: Implement coefficients caching?
206
207 const size_t ratio = getRatio();
208 const size_t tapsPerPhase = getTapsPerPhase();
209 m_phaseCoefficients.assign (ratio, std::vector<SampleType> (tapsPerPhase, (SampleType) 0));
210
211 const double center = static_cast<double> (tapsPerPhase - 1) / 2.0;
212
213 for (size_t phase = 0; phase < ratio; ++phase)
214 {
215 AccurateSum<SampleType> norm;
216 const double frac = static_cast<double> (phase) / static_cast<double> (ratio);
217
218 for (size_t tap = 0; tap < tapsPerPhase; ++tap)
219 {
220 const double x = static_cast<double> (tap) - center - frac;
221 const double sinc = floatsEqual (x, 0.0) ? 1.0 : std::sin (pi * x) / (pi * x);
222
223 // Hann window
224 const double window =
225 0.5 - 0.5 * std::cos (2.0 * pi * static_cast<double> (tap) / static_cast<double> (tapsPerPhase - 1));
226
227 const SampleType coeff = static_cast<SampleType> (sinc * window);
228 m_phaseCoefficients[phase][tap] = coeff;
229 norm += coeff;
230 }
231
232 const SampleType normValue = norm.getValue();
233
234 // Normalize each phase for unity DC gain
235 for (SampleType& c : m_phaseCoefficients[phase])
236 c /= normValue;
237 }
238 }
239
240};
241
242/// @brief Estimates true peak (inter-sample peak) level
243/// @details
244/// It checks inter-sample peaks by observing oversampled signal, following
245/// [ITU-R BS.1770-5](https://www.itu.int/rec/R-REC-BS.1770-5-202311-I/en)
246/// guidelines. Some of the implementation choices are exposed via arguments,
247/// such as oversampling factor and number of taps in the internal poly-phase
248/// FIR filter, as the standard does not specify the exact values.
249///
250/// Supports values in dB TP (`Unit::dB`) and linear domain (Unit::linear).
251/// Operating at default unit (`Unit::native`) will yield values in dB TP.
252///
253/// Shares the same implementation as `TruePeaksBelow` matcher, but lets you
254/// make more versatile expressions.
255///
256/// @param audioBuffer Buffer to estimate true peaks in
257/// @param oversamplingRatio Oversampling for the estimator. Higher OS ratios
258/// are expected to result in more accurate estimations.
259/// @param filterQuality Represent number of taps for the internal FIR filter.
260/// Higher will result in more accurate estimate. Note that even the highest
261/// filter quality is way lower than what is used in actual DAC oversamplers,
262/// but it's okay, since we're merely estimating here.
263/// @ingroup Metrics
264template <typename SampleType>
266 const AudioBuffer<SampleType>& audioBuffer,
267 Oversampling oversamplingRatio = Oversampling::x4,
268 typename TruePeak<SampleType>::FilterQuality filterQuality = TruePeak<SampleType>::FilterQuality::low
269 )
270{
271 // Lambda (estimator) will actually own the truePeakEstimator object,
272 // but for C++11 compatibility sake, it's easier to pass it by copy
273 // as a shared pointer.
274 auto truePeakEstimator = std::make_shared<TruePeak<SampleType>> (oversamplingRatio, filterQuality);
275 truePeakEstimator->prepare (
276 audioBuffer.getSampleRateHz(),
277 1 // Will estimate one channel at a time
278 );
279
280 MetricQuery<double>::SingleChannelMetricEvaluator evaluator =
281 [&audioBuffer, truePeakEstimator]
282 (size_t channel, Slice slice, Unit requestedUnit)
283 -> double
284 {
285 hassert (truePeakEstimator != nullptr);
286 truePeakEstimator->reset(); // This specific instance has 1 stateful internal channel for every measured audio channel
287 const typename TruePeak<SampleType>::Result estimatorResult = truePeakEstimator->estimate (audioBuffer, {{channel}}, slice);
288 const double truePeakLinear = static_cast<double> (estimatorResult.valueLinear);
289
290 switch (requestedUnit)
291 {
292 case Unit::linear: return truePeakLinear;
293
294 case Unit::native:
295 case Unit::dB: return hart::ratioToDecibels (truePeakLinear);
296
297 default: HART_THROW_OR_RETURN (hart::UnitError, "Unsupported unit", hart::nan<double>());
298 }
299 };
300
301 const size_t numChannels = audioBuffer.getNumChannels();
302 return MetricQuery<double> (
303 std::move (evaluator),
304 numChannels,
305 ChannelSubsets::allChannels (numChannels)
306 );
307}
308
309} // namespace hart
Container for audio data.
#define hassert(condition)
Triggers a HartAssertException if the condition is false
#define HART_THROW_OR_RETURN(ExceptionType, message, returnValue)
Throws an exception if HART_DO_NOT_THROW_EXCEPTIONS is set, prints a message and returns a specified ...
MetricQuery< double > truePeak(const AudioBuffer< SampleType > &audioBuffer, Oversampling oversamplingRatio=Oversampling::x4, typename TruePeak< SampleType >::FilterQuality filterQuality=TruePeak< SampleType >::FilterQuality::low)
Estimates true peak (inter-sample peak) level.
FloatType nan()
Returns a quiet NaN value for the given floating-point type.
constexpr double pi
pi
static SampleType floatsEqual(SampleType a, SampleType b, SampleType epsilon=(SampleType) 1e-8)
Compares two floating point numbers within a given tolerance.
Oversampling
Oversampling ratio.
Represents a slice of analysis data.
static Slice whole()
bool isEmpty() const
const SampleType valueLinear