HART  0.2.0
High level Audio Regression and Testing
Loading...
Searching...
No Matches
hart_true_peaks_below.hpp
Go to the documentation of this file.
1#pragma once
2
3#include <cmath> // abs()
4#include <sstream>
5#include <vector>
6
8#include "matchers/hart_matcher.hpp"
9#include "metrics/hart_true_peak.hpp"
10#include "hart_str.hpp"
11#include "hart_utils.hpp" // decibelsToRatio(), nan()
12
13namespace hart
14{
15
16/// @brief Checks whether the audio true-peaks below specific level
17/// @details
18/// It checks inter-sample peaks by observing oversampled signal, following
19/// [ITU-R BS.1770-5](https://www.itu.int/rec/R-REC-BS.1770-5-202311-I/en)
20/// guidelines. Some of the implementation choices are exposed by ctor,
21/// such as oversampling factor and number of taps in the internal poly-phase
22/// IIR filter, as the standard does not specify the exact values.
23/// Also, you may pick whether to leave enough headroom due to potential
24/// measuring under-shoot via setting `Strictness` option.
25///
26/// For more versatile expressions, you may also check `truePeak()` metric.
27///
28/// @ingroup Matchers
29template<typename SampleType>
31 public Matcher<SampleType, TruePeaksBelow<SampleType>>
32{
33public:
34 /// @brief Number of taps of the internal poly-phase IIR filter
36 {
37 low = 12, //!< 12 taps
38 medium = 24, //!< 24 taps
39 high = 96 //!< 96 taps
40 };
41
42 /// @brief Strictness to when it comes to decision on whether the signal is below a specific dB TP target
43 enum class Strictness
44 {
45 relaxed, //!< Numeric tolerance only
46 strict //!< Numeric tolerance + estimated under-read
47 };
48
49 /// @brief Creates a matcher for a specific true peak level
50 /// @param thresholdDbTP Expected true peak threshold in decibels true peak ("dB TP")
51 /// @param oversampling Oversampling ratio, see @ref Oversampling
52 /// @param filterQuality Resolution of the internal IIR filters.
53 /// "low" is in line with ITU-R BS.1770 measuring recommendations, "high" a bit closer to what mass-produced
54 /// DACs implement (although still a rough approximate), "medium" is somewhere in between.
55 /// @param strictness Whether to take estimated true peaks at face value, or also take potential
56 /// under-read in consideration. Under-read estimation is calculated according to ITU-R BS.1770-5, Annex 2,
57 /// Attachement 1
58 /// ([See page 21 here](https://www.itu.int/dms_pubrec/itu-r/rec/bs/R-REC-BS.1770-5-202311-I!!PDF-E.pdf)).
59 /// For "strict" setting, worst case will be assumed, and additional headroom equal to maximum under-shoot
60 /// at $$f_norm = 0.45$$ will be required for the audio to pass this matcher's requirement.
61 /// @param numericToleranceLinear Absolute tolerance for comparing frames, in linear domain (not decibels)
63 double thresholdDbTP,
64 Oversampling oversamplingRatio = Oversampling::x4,
65 FilterQuality filterQuality = FilterQuality::low,
66 Strictness strictness = Strictness::relaxed,
67 double numericToleranceLinear = 1e-3
68 ) :
69
70 m_thresholdDbTP (thresholdDbTP),
71 m_thresholdLinear (static_cast<SampleType> (decibelsToRatio (thresholdDbTP) + numericToleranceLinear)),
72 m_oversamplingRatio (oversamplingRatio),
73 m_filterQuality (static_cast<typename TruePeak<SampleType>::FilterQuality> (filterQuality)),
74 m_strictness (strictness),
75 m_numericToleranceLinear (static_cast<SampleType> (numericToleranceLinear)),
76 truePeakEstimator (oversamplingRatio, m_filterQuality)
77 {
78 }
79
80 void prepare (double sampleRateHz, size_t /* numInputChannels */, size_t numOutputChannels, size_t /* maxBlockSizeFrames */) override
81 {
82 const size_t numActiveChannels = this->getChannelFlags().numTrue();
83 hassert (numActiveChannels > 0); // Zero active channels is technically still valid, but definitely weird
84 hassert (numActiveChannels <= numOutputChannels);
85
86 truePeakEstimator.prepare (sampleRateHz, numActiveChannels);
87 m_sampleRateHz = sampleRateHz; // Just for readable failure details
88 }
89
90 void reset() override
91 {
92 m_TruePeakLinear = (SampleType) 0;
93 truePeakEstimator.reset();
94 }
95
96 bool match (AnalysisContext<SampleType> context) override
97 {
98 const AudioBuffer<SampleType>& observedOutputAudio = context.outputAudio();
99
100 const ChannelFlags channelFlags = this->getChannelFlags();
101 const size_t numChannels = observedOutputAudio.getNumChannels();
102 // hassert (numChannels <= observedOutputAudio.getNumFrames());
103
104 std::vector<size_t> channels;
105 channels.reserve (channelFlags.numTrue());
106
107 for (size_t channel = 0; channel < numChannels; ++channel)
108 if (channelFlags[channel] == true)
109 channels.push_back (channel);
110
111 const typename TruePeak<SampleType>::Result estimatorResult = truePeakEstimator.estimate (observedOutputAudio, std::move (channels));
112 m_TruePeakLinear = estimatorResult.valueLinear;
113 m_failedChannel = estimatorResult.channel;
114 m_failedFrame = estimatorResult.frame;
115
116 const bool matcherResult = ! isAboveRequiredThreshold (m_TruePeakLinear);
117 m_hasPassed &= matcherResult;
118 return matcherResult;
119 }
120
121 bool canOperatePerBlock() const override
122 {
123 // It's more useful to display the highest peak of
124 // the entire audio clip, when the matcher fails
125 return false;
126 }
127
129 {
130 const double maximumUnderReadLinear = static_cast<double> (truePeakEstimator.getMaximumUnderReadLinear());
131 hassert (! floatsEqual (maximumUnderReadLinear, 0.0));
132 hassert (maximumUnderReadLinear <= 1.0);
133 const double maximumUnderReadDb = ratioToDecibels (maximumUnderReadLinear);
134
135 const double observedPeakDbTP = ratioToDecibels (m_TruePeakLinear);
136 const double maximumMormalizedFrequencyHz = m_fNorm * m_sampleRateHz / 2;
137
138 std::ostringstream detailsStream;
139 detailsStream
140 << "Observed audio true peak is "
141 << linPrecision << m_TruePeakLinear << " ("
142 << dbPrecision << observedPeakDbTP << " dB TP), at frame "
143 << std::setprecision (4) << m_failedFrame
144 << "\nMaximum under-read at f_norm = " << std::setprecision (2) << m_fNorm
145 << hzPrecision << " (" << maximumMormalizedFrequencyHz << " Hz) is " << maximumUnderReadDb << " dB, "
146 << (m_strictness == Strictness::relaxed ? "not " : "") << "taken into account";
147
148 MatcherFailureDetails details;
149 details.frame = hart::roundToSizeT (m_failedFrame); // TODO: Report floating point value
150 details.channel = m_failedChannel;
151 details.description = detailsStream.str();
152
153 return details;
154 }
155
156 void represent (std::ostream& stream) const override
157 {
158 stream << "TruePeaksBelow ("
159 << dbPrecision << m_thresholdDbTP << "_dBTP, "
160 << m_oversamplingRatio << ", "
161 << m_filterQuality << ", "
162 << m_strictness << ", "
163 << linPrecision << m_numericToleranceLinear << ')';
164 }
165
166 friend std::ostream& operator<< (std::ostream& os, Strictness strictness)
167 {
168 return os << "Strictness::" << (strictness == Strictness::relaxed ? "relaxed" : "strict");
169 }
170
171 friend std::ostream& operator<< (std::ostream& os, FilterQuality filterQuality)
172 {
173 os << "FilterQuality::";
174
175 switch (filterQuality)
176 {
177 case FilterQuality::low : os << "low"; break;
178 case FilterQuality::medium : os << "medium"; break;
179 case FilterQuality::high : os << "high"; break;
180 }
181
182 return os;
183 }
184
185private:
186 static constexpr double m_fNorm = 0.45; // It's a ratio, not Hz
187 const double m_thresholdDbTP;
188 const SampleType m_thresholdLinear;
189 const Oversampling m_oversamplingRatio;
190 const typename TruePeak<SampleType>::FilterQuality m_filterQuality;
191 const Strictness m_strictness;
192 const SampleType m_numericToleranceLinear;
193 double m_sampleRateHz = hart::nan<double>();
194 SampleType m_TruePeakLinear = static_cast<SampleType>(0);
195 TruePeak<SampleType> truePeakEstimator;
196
197 bool m_hasPassed = true;
198 double m_failedFrame = hart::nan<double>();
199 size_t m_failedChannel = 0;
200
201 // Outer vector = channels
202 // Inner vector = ring buffer of previous samples
203 // History index is shared since all channels advance in lockstep
204 std::vector<std::vector<SampleType>> m_history;
205 size_t m_historyIndex = 0;
206 size_t m_offsetFrames = 0;
207
208 // TODO: Flatten m_phaseCoefficients and m_history to 1D vectors?
209 std::vector<std::vector<SampleType>> m_phaseCoefficients;
210
211 inline bool isAboveRequiredThreshold (SampleType rectifiedPeakLinear)
212 {
213 return m_strictness == Strictness::strict
214 ? rectifiedPeakLinear / truePeakEstimator.getMaximumUnderReadLinear() > m_thresholdLinear
215 : rectifiedPeakLinear > m_thresholdLinear;
216 }
217};
218
220
221} // namespace hart
Contains audio-related artefacts useful for analysis by matchers.
Container for audio data.
A set of boolean flags mapped to each audio channel.
bool operator[](size_t channel) const
Access the flag value for a specific channel.
size_t numTrue() const noexcept
Checks how many channels are marked with true
Base for audio matchers.
Checks whether the audio true-peaks below specific level.
void prepare(double sampleRateHz, size_t, size_t numOutputChannels, size_t) override
Prepare for processing It is guaranteed that all subsequent process() calls will be in line with the ...
MatcherFailureDetails getFailureDetails() const override
Returns a description of why the match has failed.
Strictness
Strictness to when it comes to decision on whether the signal is below a specific dB TP target.
@ strict
Numeric tolerance + estimated under-read.
@ relaxed
Numeric tolerance only.
FilterQuality
Number of taps of the internal poly-phase IIR filter.
void represent(std::ostream &stream) const override
Makes a text representation of this Matcher for test failure outputs.
bool match(AnalysisContext< SampleType > context) override
Tells the host if the piece of audio satisfies Matcher's condition or not.
bool canOperatePerBlock() const override
Tells the host if it can operate on a block-by-block basis.
void reset() override
Resets the matcher to its initial state.
TruePeaksBelow(double thresholdDbTP, Oversampling oversamplingRatio=Oversampling::x4, FilterQuality filterQuality=FilterQuality::low, Strictness strictness=Strictness::relaxed, double numericToleranceLinear=1e-3)
Creates a matcher for a specific true peak level.
#define hassert(condition)
Triggers a HartAssertException if the condition is false
std::ostream & linPrecision(std::ostream &stream)
Sets number of decimal places for linear (sample) values.
std::ostream & hzPrecision(std::ostream &stream)
Sets number of decimal places for values in hertz.
std::ostream & dbPrecision(std::ostream &stream)
Sets number of decimal places for values in decibels.
FloatType nan()
Returns a quiet NaN value for the given floating-point type.
static size_t roundToSizeT(SampleType x)
Rounds a floating point value to a size_t value.
static SampleType ratioToDecibels(SampleType valueLinear)
Converts linear value (ratio) to dB.
static SampleType decibelsToRatio(SampleType valueDb)
Converts dB to linear value (ratio)
static SampleType floatsEqual(SampleType a, SampleType b, SampleType epsilon=(SampleType) 1e-8)
Compares two floating point numbers within a given tolerance.
Oversampling
Oversampling ratio.
#define HART_MATCHER_DECLARE_ALIASES_FOR(ClassName)
size_t channel
Index of channel at which the failure was detected.
std::string description
Readable description of why the match has failed.
size_t frame
Index of frame at which the match has failed.