HART  0.2.0
High level Audio Regression and Testing
Loading...
Searching...
No Matches
hart_thd.hpp
Go to the documentation of this file.
1#pragma once
2
3#include <complex> // norm()
4#include <cmath> // floor(), round(), sqrt()
5
8#include "hart_utils.hpp" // nextPowerOfTwo(), roundToSizeT(), floatsNotEqual(), clamp()
9
10namespace hart
11{
12
13namespace THD
14{
15
16/// @brief A tuned setup for optimal THD measurement
17///
18/// Intended to be used for `hart::thd() metric`.
19///
20/// Contains the tuned experiment parameters produced by `ExperimentSetupTuner`.
21/// The frequency and duration are selected so that the rendered signal contains
22/// a power-of-two number of frames and the fundamental frequency falls exactly
23/// in the centre of FFT bin, avoiding FFT spectral leakage.
24///
25/// Use hart::THD::ExperimentSetupTuner::tune() to instantiate it.
26///
27/// Use `frequencyHz` and `durationSeconds` to configure the input signal and
28/// render duration, then pass the same `ExperimentSetup` instance to `hart::thd()`.
30{
31 /// @brief Optimized frequency of the input sine wave
32 /// @details
33 /// Should be close to desired fundamental, snapped to the centre of FFT bin.
34 const double frequencyHz;
35
36 /// @brief Optimized duration of audio for FFT with no padding
37 /// @details
38 /// Tuned in a way that signal will have a number of frames equal to power of 2.
39 /// Guaranteed to be no more that desired duration, specified in the `ExperimentSetupTuner`.
40 ///
41 /// Same as `durationFrames`, but in seconds.
42 const double durationSeconds;
43
44 /// @brief Optimized duration of audio for FFT with no padding
45 /// @details Guaranteed to be power of two.
46 ///
47 /// Same as `durationSeconds`, but in frames.
48 const size_t durationFrames;
49
50 /// @brief Optimized highest harmonic for THD calculation
51 /// @details Based on desired max harmonic specified in the `ExperimentSetupTuner`,
52 /// limited to the maximum possible amount, with FFT size in mind.
53 const int maxHarmonic;
54
55 ExperimentSetup (const ExperimentSetup&) = default;
59 ~ExperimentSetup() = default;
60
61private:
62 /// @brief The builder class used for instantiating this structure
64
65 /// @brief Creates the `ExperimentSetup` structure, filled with the provided values
66 /// @note Supposed to be constructed only via ExperimentSetupTuner
67 ExperimentSetup (double frequencyHz_, double durationSeconds_, size_t durationFrames_, int maxHarmonic_) :
68 frequencyHz (frequencyHz_),
69 durationSeconds (durationSeconds_),
70 durationFrames (durationFrames_),
71 maxHarmonic (maxHarmonic_)
72 {
73 hassert (frequencyHz > 0.0);
77 hassert (maxHarmonic >= 2);
78 }
79
80 /// @brief Creates invalid default-constructed structure
81 /// @note Supposed to be constructed only when builder encouters any runtime errors
82 ExperimentSetup() :
86 maxHarmonic (0)
87 {
88 }
89};
90
91/// @brief Configures and tunes an experiment setup for accurate THD measurement.
92///
93/// Intended to be used for `hart::thd() metric`.
94///
95/// THD measurement requires a pure sine, whose fundamental frequency should fall
96/// exactly on an FFT bin. The analysed signal should also contain exactly the
97/// same number of frames as the FFT, without zero padding. Otherwise, spectral
98/// leakage may appear as harmonic energy and artificially increase the
99/// measured THD.
100///
101/// This class takes the desired experiment parameters and tunes them
102/// to satisfy these requirements for you. The `tune()` method:
103///
104/// - Snaps the requested duration to a number of frames that is a power of two
105/// - Snaps the requested fundamental frequency to centre of the nearest valid FFT bin
106/// - Limits the number of harmonics to those measurable below Nyquist frequency bin
107///
108/// Examples:
109///
110/// @code
111/// // 1. Tune the desired experiment parameters
112/// const hart::THD::ExperimentSetup setup =
113/// hart::THD::ExperimentSetupTuner()
114/// .withFrequency (1000_Hz)
115/// .withSampleRate (44100_Hz)
116/// .withDuration (100_ms) // or withNumFrames(n)
117/// .withMaxHarmonic (10)
118/// .tune();
119///
120/// // 2. Run the experiment (render the audio), using the tuned values...
121/// processAudioWith (SomeDSP())
122/// .withInputSignal (SineWave (setup.frequencyHz)) // ...here...
123/// .withDuration (setup.durationSeconds) // ...and here.
124/// .expectTrue (
125/// [setup] (const auto& output)
126/// {
127/// return HART_EXPECT_LT (
128/// // 3. Call the thd() metric, passing the same setup structure to it
129/// hart::thd (hart::Spectrum (output), setup).get(),
130/// 0.01);
131/// },
132/// "THD < 0.1"
133/// )
134/// .process();
135/// @endcode
136///
137/// If the desired duration is already expressed in frames, withNumFrames()
138/// can be used instead:
139///
140/// @code
141/// const hart::THD::ExperimentSetup setup = hart::THD::ExperimentSetupTuner()
142/// .withFrequency (1000_Hz)
143/// .withNumFrames (4096)
144/// .tune();
145///
146/// // (Then run the test)
147/// @endcode
148///
149/// The most minimal scenario can look like this:
150///
151/// @code
152/// const auto setup = hart::THD::ExperimentSetupTuner()
153/// .withFrequency (1000_Hz)
154/// .tune();
155///
156/// // (Then run the test)
157/// @endcode
158///
159/// In that case, the duration and sample rate will be pulled `from hart::CLIConfig`,
160/// and be the same as the defaults in the usual `hart::AudioTestBuilder`.
162{
163public:
164 /// @brief Sets the desired frequency of the input signal
165 /// @details It will be tuned to fall exactly in the centre of an FFT bin.
166 ///
167 /// If omitted, this desired frequency will be assumed to be 1 kHz.
168 /// @param desiredFrequencyHz Your desired frequency of a sine wave in the input signal
169 /// @return A chainable builder
170 ExperimentSetupTuner& withFrequency (double desiredFrequencyHz)
171 {
172 if (desiredFrequencyHz < 0.0 || floatsEqual (desiredFrequencyHz, 0.0) || std::isnan (desiredFrequencyHz))
173 HART_THROW_OR_RETURN (hart::ValueError, "Invalid fundamental frequency", *this);
174
175 m_desiredFrequencyHz = desiredFrequencyHz;
176 return *this;
177 }
178
179 /// @brief Sample rate of the test case
180 /// @details If omitted, the global default sample rate will be pulled from `hart::CLIConfig`
181 /// @param sampleRateHz Sample rate in Herts
182 /// @return A chainable builder
184 {
185 if (sampleRateHz <= 0.0 || floatsEqual (sampleRateHz, 0.0) || std::isnan (sampleRateHz))
186 HART_THROW_OR_RETURN (hart::SampleRateError, "Invalid sample rate", *this);
187
188 m_sampleRateHz = sampleRateHz;
189 return *this;
190 }
191
192 /// @brief Sets desired render time of the test case in seconds
193 /// @details Alternatively, you can set this duration in frames, using `withNumFrames()`.
194 /// There's no need to call both seconds and frames setters in one chained builder, use
195 /// no more than one - either this one, or `withNumFrames()`.
196 ///
197 /// If both seconds and frames setters are omitted, the global default duration will be
198 /// pulled from `hart::CLIConfig`.
199 ///
200 /// This value be tuned in a way that resulting rendered audio will have number of frames
201 /// that is a power of two, so that no FFT zero padding would be required.
202 /// @param desiredDurationSeconds Desired render time in seconds
203 /// @return A chainable builder
204 ExperimentSetupTuner& withDuration (double desiredDurationSeconds)
205 {
206 if (desiredDurationSeconds < 0.0 || floatsEqual (desiredDurationSeconds, 0.0) || std::isnan (desiredDurationSeconds))
207 HART_THROW_OR_RETURN (hart::ValueError, "Invalid duration", *this);
208
209 m_desiredDurationSeconds = desiredDurationSeconds;
210 m_durationSpecifiedInFrames = false;
211 return *this;
212 }
213
214 /// @brief Sets desired render time of the test case in frames
215 /// @details Alternatively, you can set this duration in seconds, using `withDuration()`.
216 /// There's no need to call both seconds and frames setters in one chained builder, use
217 /// no more than one - either this one, or `withDuration()`.
218 ///
219 /// If both seconds and frames setters are omitted, the global default duration will be
220 /// pulled from `hart::CLIConfig`.
221 ///
222 /// This value be tuned in a way that resulting rendered audio will have number of frames
223 /// that is a power of two, so that no FFT zero padding would be required.
224 /// @param desiredDurationFrames Desired size of rendered audio in frames
225 /// @return A chainable builder
226 ExperimentSetupTuner& withNumFrames (size_t desiredDurationFrames)
227 {
228 if (desiredDurationFrames == 0)
229 HART_THROW_OR_RETURN (hart::ValueError, "Invalid duration", *this);
230
231 m_desiredDurationFrames = desiredDurationFrames;
232 m_durationSpecifiedInFrames = true;
233 return *this;
234 }
235
236 /// @brief Sets desired number of harmonics for the THD measurement
237 /// @details This value will be limited in a way it's always under the Nyquist
238 /// bin in the resulting FFT.
239 /// @param desiredMaxHarmonic Desired highest harmonic used for the THD calculation
240 /// (fundamental is number 1)
241 /// @return
242 ExperimentSetupTuner& withMaxHarmonic (int desiredMaxHarmonic)
243 {
244 if (desiredMaxHarmonic < 2)
245 HART_THROW (hart::ValueError, "Invalid max harmonic number");
246
247 m_desiredMaxHarmonic = desiredMaxHarmonic;
248 return *this;
249 }
250
251 /// @brief Call it at the end of the builder chain to produce a `hart::THD::ExperimentSetup` instance
252 /// @details This will give you a structure with tunes experiment setup values inside.
253 ///
254 /// You're expected to use those values for the test case, namely sine wave frequency and render duration,
255 /// instead of your desired values used in this builder. The values will be different from the desired
256 /// ones (except for rare lucky cases), but they'll be optimized for an accurate THD reading, with no FFT
257 /// spills. You're also expected to pass the same instance of experiment setup to the `hart::thd()` metric,
258 /// so that it has all experiment context it needs.
259 /// @return A `hart::THD::ExperimentSetup` instance containing tuned values for the THD measurement
260 /// experiment.
262 {
263 const size_t requestedDurationFrames =
264 m_durationSpecifiedInFrames
265 ? m_desiredDurationFrames
266 : roundToSizeT (m_desiredDurationSeconds * m_sampleRateHz);
267
268 // TODO: Make closestPowerOfTwo()?
269 const size_t tunedDurationFrames = hart::previousPowerOfTwo (requestedDurationFrames);
270
271 if (tunedDurationFrames < 8)
272 HART_THROW_OR_RETURN (hart::ValueError, "Experiment duration is too short for THD measurement", {});
273
274 const double tunedDurationSeconds = static_cast<double> (tunedDurationFrames) / m_sampleRateHz;
275 const size_t nyquistBin = tunedDurationFrames / 2;
276
277 // At least bins 1..numHarmonics must fit strictly below Nyquist.
278 const int maxValidNumHarmonics = static_cast<int> (nyquistBin) - 1;
279
280 const int tunedMaxHarmonic = std::min (m_desiredMaxHarmonic, maxValidNumHarmonics);
281 hassert (tunedMaxHarmonic >= 2);
282
283 const double tunedFrequencyHz =
284 closestCoherentFrequencyHz (
285 m_desiredFrequencyHz,
286 tunedDurationFrames,
287 m_sampleRateHz,
288 tunedMaxHarmonic
289 );
290
291 return {
292 tunedFrequencyHz,
293 tunedDurationSeconds,
294 tunedDurationFrames,
295 tunedMaxHarmonic
296 };
297 }
298
299private:
300 double m_desiredFrequencyHz = 1000.0;
302 double m_desiredDurationSeconds = CLIConfig::getInstance().getDefaultRenderDurationSeconds();
303 size_t m_desiredDurationFrames = 0;
304 int m_desiredMaxHarmonic = 10;
305 bool m_durationSpecifiedInFrames = false;
306
307 static double closestCoherentFrequencyHz (
308 double desiredFrequencyHz,
309 size_t fftSizeFrames,
310 double sampleRateHz,
311 int maxHarmonic = 10
312 )
313 {
314 const double nan = hart::nan<double>();
315
316 if (! isPowerOfTwo (fftSizeFrames))
317 HART_THROW_OR_RETURN (hart::SizeError, "FFT size is expected to be a power of 2", nan);
318
319 if (sampleRateHz < 0.0 || floatsEqual (sampleRateHz, 0.0) || std::isnan (sampleRateHz))
320 HART_THROW_OR_RETURN (hart::SampleRateError, "Invalid sample rate", nan);
321
322 if (desiredFrequencyHz < 0.0 || floatsEqual (desiredFrequencyHz, 0.0) || std::isnan (desiredFrequencyHz))
323 HART_THROW_OR_RETURN (hart::ValueError, "Invalid input fundamental frequency", nan);
324
325 if (maxHarmonic < 2)
326 HART_THROW_OR_RETURN (hart::ValueError, "Invalid max harmonic number", nan);
327
328 const double binWidthHz = sampleRateHz / static_cast<double> (fftSizeFrames);
329 const size_t nyquistBin = fftSizeFrames / 2;
330
331 if (static_cast<size_t> (maxHarmonic) >= nyquistBin)
332 HART_THROW_OR_RETURN (hart::ValueError, "FFT size is too small for the requested number of harmonics", nan);
333
334 const size_t maxFundamentalBin = (nyquistBin - 1) / static_cast<size_t> (maxHarmonic);
335
336 hassert (maxFundamentalBin >= 1);
337 hassert (maxFundamentalBin * static_cast<size_t> (maxHarmonic) < nyquistBin);
338
339 const size_t fundamentalBin = hart::clamp (
340 roundToSizeT (desiredFrequencyHz / binWidthHz),
341 (size_t) 1,
342 maxFundamentalBin
343 );
344
345 const double fundamentalFrequencyHz = static_cast<double> (fundamentalBin) * binWidthHz;
346 hassert (floatsNotEqual (fundamentalFrequencyHz, 0.0));
347 return fundamentalFrequencyHz;
348 }
349};
350
351} // namespace hart::THD
352
353/// @brief Calculates the total harmonic distortion (THD) of a spectrum.
354///
355/// THD is calculated as the square root of the summed harmonic power
356/// divided by the fundamental bin power:
357///
358/// @f[
359/// \mathrm{THD}
360/// = \sqrt{
361/// \frac{\sum_{h=2}^{H} |X[h k_1]|^2}
362/// {|X[k_1]|^2}
363/// }
364/// @f]
365///
366/// (THD = sqrt(sum(norm(harmonic bins)) / norm(fundamental bin))),
367///
368/// where @f$ k_1 @f$ is the FFT bin containing the fundamental, and @f$ H @f$
369/// is the maximum harmonic number requested. Harmonics at or above the
370/// Nyquist frequency are ignored.
371///
372/// For an accurate measurement, the input should be a pure sine whose
373/// frequency lies exactly at the centre of an FFT bin. The analysed signal
374/// should also contain exactly the same number of frames as the FFT, without
375/// zero padding. Otherwise, truncation and zero padding cause spectral
376/// leakage, which may appear as harmonic energy and artificially increase
377/// the measured THD.
378///
379/// To ensure those conditions are met, you're expected to obtain a "tuned"
380/// experiment setup, obtained through hart::THD::ExperimentSetupTuner,
381/// which will snap all of your desired experiment parameters to the values
382/// optimized for no-spill FFT. Those will be the values you're supposed to
383/// run the entire test render with.
384///
385/// Example:
386///
387/// @code
388/// // Assuming you have specific render duration and input signal frequency in mind:
389/// constexpr double desiredRenderDurationSeconds = 100_ms; // Or any other duration you want
390/// constexpr double desiredFrequencyHz = 1_kHz; // Or any other frequency you want
391///
392/// // Snap the desired parameters to optimal values
393/// const hart::THD::ExperimentSetup setup = hart::THD::ExperimentSetupTuner()
394/// .withFrequency (frequencyHz)
395/// .withDuration (desiredRenderDurationSeconds)
396/// .tune();
397///
398/// processAudioWith (SomeDSP())
399/// .withInputSignal (SineWave (setup.frequencyHz)) // Tuned frequency...
400/// .withDuration (setup.durationSeconds) // ...and tuned render duration
401/// .expectTrue (
402/// [setup] (const hart::AudioBuffer<float>& output)
403/// {
404/// return HART_FLOAT_EQ (
405/// hart::thd (hart::Spectrum (output), setup).get(),
406/// 0.0,
407/// 1e-8
408/// );
409/// },
410/// "THD ~= 0")
411/// .process();
412/// @endcode
413///
414/// Supported units:
415/// - `hart::Unit::ratio` - as a ratio
416/// - `hart::Unit::native` - default unit, same as `ratio`
417/// - `hart::Unit::dB` - as decibels, using a "ratio" (not "power") variety of decibels
418/// - `hart::Unit::percent` - as percentage - just ratio multipled by 100, may be fractional
419///
420/// @see hart::THD::ExperimentSetup
421/// @see hart::THD::ExperimentSetupTuner
422/// @param spectrum Spectrum of the output signal to analyse, assuming the input
423/// was a pure sine wave
424/// @param experimentSetup Optimized experiment setup values obtained through
425/// hart::THD::ExperimentSetupTuner().
426/// @return A MetricQuery containing THD as a linear amplitude ratio. May return `NaN`.
427/// @ingroup Metrics
428inline MetricQuery<double> thd (const Spectrum& spectrum, THD::ExperimentSetup experimentSetup)
429{
430 MetricQuery<double>::SingleChannelMetricEvaluator evaluator =
431 [&spectrum, experimentSetup]
432 (size_t channel, const Slice& slice, Unit requestedUnit)
433 -> double
434 {
435 hassert (channel < spectrum.getNumChannels());
436 hassert (! std::isnan (spectrum.getSampleRateHz()));
437
438 const double nan = hart::nan<double>();
439 const double fundamentalFrequencyHz = experimentSetup.frequencyHz;
440
441 // Make sure your test's render time is exactly experimentSetup.durationSeconds (or experimentSetup.durationFrames)
442 if (spectrum.getFFTSize() != experimentSetup.durationFrames)
443 HART_THROW_OR_RETURN (hart::ValueError, "FFT size doesn't match duration in the provided experiment setup", nan);
444
445 if (experimentSetup.durationFrames == 0 || floatsEqual (experimentSetup.durationSeconds, 0.0))
446 HART_THROW_OR_RETURN (hart::SizeError, "Experiment setup should not have duration of zero - nothing to analyze", nan);
447
448 const double experimentSetupSampleRateHz = static_cast<double> (experimentSetup.durationFrames) / experimentSetup.durationSeconds;
449
450 // The duration of input signal should be exactly experimentSetup.durationSeconds and experimentSetup.durationFrames
451 if (floatsNotEqual (spectrum.getSampleRateHz(), experimentSetupSampleRateHz))
452 HART_THROW_OR_RETURN (hart::ValueError, "Spectrum's sample rate doesn't match one derived from the provided experiment setup instance", nan);
453
454 if (slice.type != Slice::Type::whole)
455 HART_THROW_OR_RETURN (hart::ValueError, "Cannot calculate THD of a portion of spectrum", nan);
456
457 const size_t fundamentalBin = spectrum.findClosestBin (fundamentalFrequencyHz);
458
459 // The input signal in the experiment should be a sine wave at exactly experimentSetup.frequencyHz
460 if (floatsNotEqual (fundamentalFrequencyHz, spectrum.getBinFrequencyHz (fundamentalBin)))
461 HART_THROW_OR_RETURN (hart::ValueError, "Fundamental frequency in the provided spectrum doesn't match one in experiment setup", nan);
462
463 const double fundamentalPower = std::norm (spectrum.getBinValue (channel, fundamentalBin));
464
465 if (fundamentalPower < 1e-15)
466 return std::numeric_limits<double>::infinity();
467
468 const double nyquistFrequencyHz = spectrum.getSampleRateHz() / 2.0;
469 AccurateSum<double> harmonicPowerSum;
470
471 const int maxHarmonic = experimentSetup.maxHarmonic;
472 hassert (maxHarmonic >= 2);
473
474 for (int harmonic = 2; harmonic <= maxHarmonic; ++harmonic)
475 {
476 const double harmonicFrequencyHz = fundamentalFrequencyHz * harmonic;
477
478 if (harmonicFrequencyHz >= nyquistFrequencyHz)
479 break;
480
481 const size_t harmonicBin = fundamentalBin * harmonic;
482 harmonicPowerSum += std::norm (spectrum.getBinValue (channel, harmonicBin));
483 }
484
485 const double thdRatio = std::sqrt (harmonicPowerSum / fundamentalPower);
486
487 switch (requestedUnit)
488 {
489 case Unit::native:
490 case Unit::ratio: return thdRatio;
491 case Unit::dB: return hart::ratioToDecibels (thdRatio);
492 case Unit::percent: return thdRatio * 100.0;
493 default: HART_THROW_OR_RETURN (hart::UnitError, "Unsupported unit", hart::nan<double>());
494 }
495 };
496
497 const size_t numChannels = spectrum.getNumChannels();
498 return MetricQuery<double> (
499 std::move (evaluator),
500 numChannels,
501 ChannelSubsets::allChannels (numChannels)
502 );
503}
504
505} // namespace hart
Thrown when sample rate is mismatched or invalid.
Thrown when an unexpected container size is encountered.
Configures and tunes an experiment setup for accurate THD measurement.
Definition hart_thd.hpp:162
ExperimentSetupTuner & withDuration(double desiredDurationSeconds)
Sets desired render time of the test case in seconds.
Definition hart_thd.hpp:204
ExperimentSetupTuner & withMaxHarmonic(int desiredMaxHarmonic)
Sets desired number of harmonics for the THD measurement.
Definition hart_thd.hpp:242
ExperimentSetupTuner & withSampleRate(double sampleRateHz)
Sample rate of the test case.
Definition hart_thd.hpp:183
ExperimentSetupTuner & withFrequency(double desiredFrequencyHz)
Sets the desired frequency of the input signal.
Definition hart_thd.hpp:170
ExperimentSetupTuner & withNumFrames(size_t desiredDurationFrames)
Sets desired render time of the test case in frames.
Definition hart_thd.hpp:226
ExperimentSetup tune() const
Call it at the end of the builder chain to produce a hart::THD::ExperimentSetup instance.
Definition hart_thd.hpp:261
Thrown when an inappropriate value is encountered.
#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 ...
#define HART_THROW(ExceptionType, message)
Throws an exception if HART_DO_NOT_THROW_EXCEPTIONS is set, prints a message otherwise.
MetricQuery< double > thd(const Spectrum &spectrum, THD::ExperimentSetup experimentSetup)
Calculates the total harmonic distortion (THD) of a spectrum.
Definition hart_thd.hpp:428
FloatType nan()
Returns a quiet NaN value for the given floating-point type.
static bool isPowerOfTwo(size_t x)
static size_t roundToSizeT(SampleType x)
Rounds a floating point value to a size_t value.
static SampleType floatsNotEqual(SampleType a, SampleType b, SampleType epsilon=(SampleType) 1e-8)
Compares two floating point numbers within a given tolerance.
NumericType clamp(const NumericType &value, const NumericType &low, const NumericType &high)
std::clamp() replacement for C++11
static size_t previousPowerOfTwo(size_t x)
Finds previous power of 2 after a non-negative number x.
static SampleType floatsEqual(SampleType a, SampleType b, SampleType epsilon=(SampleType) 1e-8)
Compares two floating point numbers within a given tolerance.
Holds values set by the user via CLI interface.
double getDefaultRenderDurationSeconds() const
double getDefaultSampleRateHz() const
static CLIConfig & getInstance()
Get the singleton instance.
A tuned setup for optimal THD measurement.
Definition hart_thd.hpp:30
const double durationSeconds
Optimized duration of audio for FFT with no padding.
Definition hart_thd.hpp:42
const int maxHarmonic
Optimized highest harmonic for THD calculation.
Definition hart_thd.hpp:53
ExperimentSetup & operator=(ExperimentSetup &&)=delete
ExperimentSetup & operator=(const ExperimentSetup &)=delete
ExperimentSetup(ExperimentSetup &&)=default
ExperimentSetup(const ExperimentSetup &)=default
const size_t durationFrames
Optimized duration of audio for FFT with no padding.
Definition hart_thd.hpp:48
const double frequencyHz
Optimized frequency of the input sine wave.
Definition hart_thd.hpp:34