67 ExperimentSetup (
double frequencyHz_,
double durationSeconds_, size_t durationFrames_,
int maxHarmonic_) :
172 if (desiredFrequencyHz < 0.0 ||
floatsEqual (desiredFrequencyHz
, 0.0
) || std::isnan (desiredFrequencyHz))
175 m_desiredFrequencyHz = desiredFrequencyHz;
185 if (sampleRateHz <= 0.0 ||
floatsEqual (sampleRateHz
, 0.0
) || std::isnan (sampleRateHz))
188 m_sampleRateHz = sampleRateHz;
206 if (desiredDurationSeconds < 0.0 ||
floatsEqual (desiredDurationSeconds
, 0.0
) || std::isnan (desiredDurationSeconds))
209 m_desiredDurationSeconds = desiredDurationSeconds;
210 m_durationSpecifiedInFrames =
false;
228 if (desiredDurationFrames == 0)
231 m_desiredDurationFrames = desiredDurationFrames;
232 m_durationSpecifiedInFrames =
true;
244 if (desiredMaxHarmonic < 2)
247 m_desiredMaxHarmonic = desiredMaxHarmonic;
263 const size_t requestedDurationFrames =
264 m_durationSpecifiedInFrames
265 ? m_desiredDurationFrames
271 if (tunedDurationFrames < 8)
274 const double tunedDurationSeconds =
static_cast<
double> (tunedDurationFrames) / m_sampleRateHz;
275 const size_t nyquistBin = tunedDurationFrames / 2;
278 const int maxValidNumHarmonics =
static_cast<
int> (nyquistBin) - 1;
280 const int tunedMaxHarmonic = std::min (m_desiredMaxHarmonic, maxValidNumHarmonics);
281 hassert (tunedMaxHarmonic >= 2);
283 const double tunedFrequencyHz =
284 closestCoherentFrequencyHz (
285 m_desiredFrequencyHz,
293 tunedDurationSeconds,
300 double m_desiredFrequencyHz = 1000.0;
303 size_t m_desiredDurationFrames = 0;
304 int m_desiredMaxHarmonic = 10;
305 bool m_durationSpecifiedInFrames =
false;
307 static double closestCoherentFrequencyHz (
308 double desiredFrequencyHz,
309 size_t fftSizeFrames,
319 if (sampleRateHz < 0.0 ||
floatsEqual (sampleRateHz
, 0.0
) || std::isnan (sampleRateHz))
322 if (desiredFrequencyHz < 0.0 ||
floatsEqual (desiredFrequencyHz
, 0.0
) || std::isnan (desiredFrequencyHz))
328 const double binWidthHz = sampleRateHz /
static_cast<
double> (fftSizeFrames);
329 const size_t nyquistBin = fftSizeFrames / 2;
331 if (
static_cast<size_t> (maxHarmonic) >= nyquistBin)
334 const size_t maxFundamentalBin = (nyquistBin - 1) /
static_cast<size_t> (maxHarmonic);
336 hassert (maxFundamentalBin >= 1);
337 hassert (maxFundamentalBin *
static_cast<size_t> (maxHarmonic) < nyquistBin);
345 const double fundamentalFrequencyHz =
static_cast<
double> (fundamentalBin) * binWidthHz;
347 return fundamentalFrequencyHz;
430 MetricQuery<
double>::SingleChannelMetricEvaluator evaluator =
431 [&spectrum, experimentSetup]
432 (size_t channel,
const Slice& slice, Unit requestedUnit)
435 hassert (channel < spectrum.getNumChannels());
436 hassert (! std::isnan (spectrum.getSampleRateHz()));
438 const double nan = hart::nan<
double>();
439 const double fundamentalFrequencyHz = experimentSetup.frequencyHz;
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);
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);
448 const double experimentSetupSampleRateHz =
static_cast<
double> (experimentSetup.durationFrames) / experimentSetup.durationSeconds;
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);
454 if (slice.type != Slice::Type::whole)
457 const size_t fundamentalBin = spectrum.findClosestBin (fundamentalFrequencyHz);
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);
463 const double fundamentalPower = std::norm (spectrum.getBinValue (channel, fundamentalBin));
465 if (fundamentalPower < 1e-15)
466 return std::numeric_limits<
double>::infinity();
468 const double nyquistFrequencyHz = spectrum.getSampleRateHz() / 2.0;
469 AccurateSum<
double> harmonicPowerSum;
471 const int maxHarmonic = experimentSetup.maxHarmonic;
474 for (
int harmonic = 2; harmonic <= maxHarmonic; ++harmonic)
476 const double harmonicFrequencyHz = fundamentalFrequencyHz * harmonic;
478 if (harmonicFrequencyHz >= nyquistFrequencyHz)
481 const size_t harmonicBin = fundamentalBin * harmonic;
482 harmonicPowerSum += std::norm (spectrum.getBinValue (channel, harmonicBin));
485 const double thdRatio = std::sqrt (harmonicPowerSum / fundamentalPower);
487 switch (requestedUnit)
490 case Unit::ratio:
return thdRatio;
491 case Unit::dB:
return hart::ratioToDecibels (thdRatio);
492 case Unit::percent:
return thdRatio * 100.0;
497 const size_t numChannels = spectrum.getNumChannels();
498 return MetricQuery<
double> (
499 std::move (evaluator),
501 ChannelSubsets::allChannels (numChannels)
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.
ExperimentSetupTuner & withDuration(double desiredDurationSeconds)
Sets desired render time of the test case in seconds.
ExperimentSetupTuner & withMaxHarmonic(int desiredMaxHarmonic)
Sets desired number of harmonics for the THD measurement.
ExperimentSetupTuner & withSampleRate(double sampleRateHz)
Sample rate of the test case.
ExperimentSetupTuner & withFrequency(double desiredFrequencyHz)
Sets the desired frequency of the input signal.
ExperimentSetupTuner & withNumFrames(size_t desiredDurationFrames)
Sets desired render time of the test case in frames.
ExperimentSetup tune() const
Call it at the end of the builder chain to produce a hart::THD::ExperimentSetup instance.
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.
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.
const double durationSeconds
Optimized duration of audio for FFT with no padding.
const int maxHarmonic
Optimized highest harmonic for THD calculation.
ExperimentSetup & operator=(ExperimentSetup &&)=delete
ExperimentSetup & operator=(const ExperimentSetup &)=delete
~ExperimentSetup()=default
ExperimentSetup(ExperimentSetup &&)=default
ExperimentSetup(const ExperimentSetup &)=default
const size_t durationFrames
Optimized duration of audio for FFT with no padding.
const double frequencyHz
Optimized frequency of the input sine wave.