HART  0.2.0
High level Audio Regression and Testing
Loading...
Searching...
No Matches
hart_rt60.hpp
Go to the documentation of this file.
1#pragma once
2
3#include <vector>
4
8#include "metrics/hart_metric_query.hpp"
9#include "metrics/hart_metrics_common.hpp" // ChannelSubsets
10#include "hart_slice.hpp"
11#include "hart_units.hpp" // Unit
12#include "hart_utils.hpp" // nan(), floatsEqual()
13
14namespace hart
15{
16
17namespace RT60
18{
19 /// @brief RT60 estimation method.
20 /// @details
21 /// All methods estimate the time required for the impulse response energy
22 /// to decay by 60 dB. They differ only in which part of the energy decay
23 /// curve is used for the linear fit.
24 ///
25 /// Used as `hart::rt60()` metric argument.
26 enum class Method
27 {
28 /// Early Decay Time.
29 /// Fits the decay from 0 dB to -10 dB and extrapolates the fitted slope
30 /// to a 60 dB decay. EDT is influenced primarily by the early part of
31 /// the reverberation tail.
32 /// See ISO 3382, Annex A for more in-depth description of EDT.
33 edt,
34
35 /// T20 reverberation time estimate.
36 /// Fits the decay from -5 dB to -25 dB and extrapolates the fitted slope
37 /// to a 60 dB decay.
38 t20,
39
40 /// T30 reverberation time estimate.
41 /// Fits the decay from -5 dB to -35 dB and extrapolates the fitted slope
42 /// to a 60 dB decay. T30 uses a larger portion of the decay than T20 and
43 /// therefore requires a sufficiently long impulse response.
44 t30
45 };
46} // namespace RT60
47
48/// @brief Estimates the RT60 reverberation time of an impulse response.
49///
50/// RT60 is the time required for reverberant energy to decay by 60 dB.
51/// The metric is calculated from the impulse response using Schroeder backward
52/// integration, followed by linear regression over the range specified by
53/// @p method. The fitted decay slope is then extrapolated to 60 dB.
54///
55/// EDT, T20 and T30, specified by @p method, are different estimators of the
56/// same RT60 quantity. For an exponential decay they're expected to produce
57/// identical results; for more complex decay curves you can use combination
58/// of those.
59///
60/// Supported units are `Unit::seconds`, `Unit::native` (same as seconds) and
61/// `Unit::frames`. If `Unit::frames` is requested, the result will be a fractional
62/// value.
63///
64/// This metric is based on ISO 3382 standard.
65/// @note Slices aren't yet supported by this metric, and will be ignored.
66/// @attention
67/// The supplied impulse response is assumed to represent a decaying response.
68/// If the requested decay range cannot be observed, or a valid decay slope
69/// cannot be estimated, the result will be `NaN`. Also, note that DSPs that
70/// produce no decay or ringing at all (e. g., a system that just applies linear
71/// gain, or a stateless waveshaper) will result in `NaN`, and not zero.
72///
73/// Also, make sure that provided IR is long enough to contain a portion of slope
74/// specified by @p method, otherwise the estimation will result `NaN`.
75/// See @ref RT60::Method options documentation for details, and ISO 3382 for
76/// a more in-depth description.
77/// @tparam SampleType Floating-point sample type of the impulse response.
78/// @param ir Impulse response to analyze
79/// @param method RT60 estimation method
80///
81/// @return A MetricQuery containing the estimated decay time for each channel.
82/// Either in seconds, or in frames, depending on requested unit. Can be `NaN`.
83///
84/// @see RT60::Method
85/// @ingroup Metrics
86template <typename SampleType>
87MetricQuery<double> rt60 (const ImpulseResponse<SampleType>& ir, RT60::Method method = RT60::Method::edt)
88{
89 MetricQuery<double>::SingleChannelMetricEvaluator evaluator =
90 [&ir, method]
91 (size_t channel, Slice /* slice */, Unit requestedUnit)
92 -> double
93 {
94 hassert (channel < ir.getNumChannels());
95 const double nan = hart::nan<double>();
96
97 // TODO: Handle Slice for impulse responses in a way that makes sense
98
99 const size_t numFrames = ir.getNumFrames();
100
101 if (numFrames == 0)
102 return nan;
103
104 const SampleType* irChannelData = ir[channel];
105
106 // Schroeder backward integration
107 std::vector<double> energy (numFrames);
108 AccurateSum<double> accumulatedEnergy;
109
110 for (size_t i = numFrames; i-- > 0;)
111 {
112 const double sample = static_cast<double> (irChannelData[i]);
113 accumulatedEnergy += sample * sample;
114 energy[i] = accumulatedEnergy.getValue();
115 }
116
117 const auto totalEnergy = energy[0];
118
119 if (totalEnergy <= 0.0)
120 return nan;
121
122 const double sampleRateHz = ir.getSampleRateHz();
123 hassert (sampleRateHz > 0.0);
124
125 struct FitRange
126 {
127 double upperDb;
128 double lowerDb;
129
130 FitRange (RT60::Method method)
131 {
132 switch (method)
133 {
135 upperDb = -5.0;
136 lowerDb = -25.0;
137 break;
139 upperDb = -5.0;
140 lowerDb = -35.0;
141 break;
142 default: // Rt60::Method::edt
143 upperDb = 0.0;
144 lowerDb = -10.0;
145 }
146 }
147 };
148
149 const FitRange fitRange (method);
150
151 // Linear regression:
152 // y = slope * x + intercept
153 // where:
154 // x is time in seconds,
155 // y is Schroeder energy decay in dB
156
157 AccurateSum<double> sumX;
158 AccurateSum<double> sumY;
159 AccurateSum<double> sumXX;
160 AccurateSum<double> sumXY;
161 std::size_t numPoints = 0;
162 bool lowerBoundWasReached = false;
163
164 for (std::size_t i = 0; i < numFrames; ++i)
165 {
166 if (energy[i] <= 0.0)
167 break;
168
169 const double decayDb = 10.0 * std::log10 (energy[i] / totalEnergy);
170
171 if (decayDb > fitRange.upperDb)
172 continue;
173
174 if (decayDb <= fitRange.lowerDb)
175 {
176 lowerBoundWasReached = true;
177 break;
178 }
179
180 const double timeSeconds = static_cast<double> (i) / sampleRateHz;
181
182 sumX += timeSeconds;
183 sumY += decayDb;
184 sumXX += timeSeconds * timeSeconds;
185 sumXY += timeSeconds * decayDb;
186
187 ++numPoints;
188 }
189
190 if (! lowerBoundWasReached)
191 {
192 // Signal (IR) didn't reach the appropriate decay point.
193 // Consider using a different RT60::Method, or supply a longer IR.
194 return nan;
195 }
196
197 if (numPoints < 2)
198 return nan;
199
200 const double n = static_cast<double> (numPoints);
201 const double denominator = n * sumXX.getValue() - sumX.getValue() * sumX.getValue();
202
203 if (floatsEqual (denominator, 0.0))
204 return nan;
205
206 const auto slopeDbPerSecond = (n * sumXY.getValue() - sumX.getValue() * sumY.getValue()) / denominator;
207 hassert (slopeDbPerSecond < 0.0); // A valid Schroeder decay fit must have a negative slope
208
209 const double sixtyDbDecayTimeSeconds = -60.0 / slopeDbPerSecond;
210
211 switch (requestedUnit)
212 {
213 case Unit::native:
214 case Unit::seconds: return sixtyDbDecayTimeSeconds;
215
216 case Unit::frames: return sixtyDbDecayTimeSeconds * sampleRateHz;
217
218 default: HART_THROW_OR_RETURN (hart::UnitError, "Unsupported unit", hart::nan<double>());
219 }
220 };
221
222 const size_t numChannels = ir.getNumChannels();
223 return MetricQuery<double> (
224 std::move (evaluator),
225 numChannels,
227 );
228}
229
230} // namespace hart
Implements Kahan algorithm for floating point accumulations.
SampleType getValue() const
AccurateSum & operator+=(SampleType value)
Adds a value to a sum, tracking the potential floating point error.
Container for representing an impulse response (IR)
Manages the metrics calculations.
MetricQuery(SingleChannelMetricEvaluator evaluator, size_t totalNumChannels, std::vector< size_t > &&defaultChannelsToProcess)
Create a metric query object for a metric that operates on one channel at a time.
Thrown when some metric is requested to return a value in an unsupported unit.
#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 > rt60(const ImpulseResponse< SampleType > &ir, RT60::Method method=RT60::Method::edt)
Estimates the RT60 reverberation time of an impulse response.
Definition hart_rt60.hpp:87
FloatType nan()
Returns a quiet NaN value for the given floating-point type.
static SampleType floatsEqual(SampleType a, SampleType b, SampleType epsilon=(SampleType) 1e-8)
Compares two floating point numbers within a given tolerance.
Method
RT60 estimation method.
Definition hart_rt60.hpp:27
Unit
Represents a physical unit.
@ seconds
Time stamps, intervals, durations.
@ native
Default (native) unit of whatever returns some value.
@ frames
Value of something in frames (samples)
Helpers to generate common default channel subsets.
static std::vector< size_t > allChannels(size_t numChannels)
Represents a slice of analysis data.