HART  0.2.0
High level Audio Regression and Testing
Loading...
Searching...
No Matches
hart_quinns2.hpp
Go to the documentation of this file.
1#pragma once
2
3#include <algorithm> // max()
4#include <cmath> // log(), sqrt(), isnan()
5#include <complex> // complex, norm()
6
8#include "metrics/hart_metric_query.hpp"
9#include "metrics/hart_metrics_common.hpp" // ChannelSubsets
10#include "hart_slice.hpp"
12#include "hart_units.hpp" // Unit
13#include "hart_utils.hpp" // nan(), floatsEqual()
14
15namespace hart
16{
17
18/// @brief Returns somewhat accurate loudest frequency in the spectrum
19/// @details
20/// Implements algorithm commonly referred to as "Quinn's Second Estimator",
21/// described by B. G. Quinn in "Estimating frequency by interpolation
22/// using Fourier coefficients", IEEE Transactions on Signal Processing,
23/// Vol. 42, No. 5, pp. 1264-1268.
24///
25/// It's provides a quite accurate way if interpolating frequency value,
26/// that is somewhere in between FFT bin centers. Note that it's undefined
27/// near DC and Nyquist frequencies, and it will return NaN for those bins.
28/// For those edge cases, consider using a more simple
29/// `hart::loudestBinFrequency()` metric instead.
30///
31/// Supports `Unit::Hz` (native/default) unit, so requesting a unit
32/// explicitly via `MetricQuery::as()` is not required.
33///
34/// This metric operates on FFT bins exactly as stored in the Spectrum.
35/// In a not-so-likely event where multiple bins have exactly the same
36/// magnitube, the lowest frequency will be returned.
37///
38/// Usage examples:
39/// @code
40/// // Estimated loudest frequency in Hz
41/// const double loudestFreqHz = quinns2 (monoSpectrum).get();
42///
43/// // Estimated loudest frequency inside a frequency band
44/// const double loudestMidBandFreqHz =
45/// quinns2 (monoSpectrum)
46/// .at (Slice::frequency (500_Hz, 2000_Hz))
47/// .get();
48///
49/// // Loudest frequency, but only inside a specific channel subset.
50/// Calculated per chanel, then averaged.
51/// const double loudestFreqHz =
52/// quinns2 (multiChannelSpectrum)
53/// .ch ({0, 2, 4})
54/// .get (mean());
55///
56/// @endcode
57///
58/// @param spectrum Input frequency-domain spectrum
59/// @throws hart::UnitError if unsupported unit is requested
60/// @ingroup Metrics
61inline MetricQuery<double> quinns2 (const Spectrum& spectrum)
62{
63 typename MetricQuery<double>::SingleChannelMetricEvaluator evaluator =
64 [&spectrum]
65 (size_t channel, const Slice& slice, Unit requestedUnit)
66 -> double
67 {
68 hassert (channel < spectrum.getNumChannels());
69 hassert (! std::isnan (spectrum.getSampleRateHz()));
70
71 if (requestedUnit != Unit::native && requestedUnit != Unit::Hz)
72 HART_THROW_OR_RETURN (hart::UnitError, "Unsupported unit", hart::nan<double>());
73
74 if (floatsEqual (spectrum.getSampleRateHz(), 0.0))
75 HART_THROW_OR_RETURN (hart::SampleRateError, "Sample rate of spectrum must not be zero", hart::nan<double>());
76
77 const std::pair<size_t, size_t> binIndices = spectrum.getBinIndices (slice);
78 const size_t startBin = binIndices.first;
79 const size_t stopBin = binIndices.second;
80
81 if (slice.isEmpty() || stopBin - startBin == 0)
82 return hart::nan<double>();
83
84 hassert (startBin < stopBin);
85 hassert (stopBin <= spectrum.getNumBins());
86
87 const std::complex<double>* bins = spectrum[channel];
88 double maxSquaredMagnitude = 0.0;
89 size_t binOfMaxSquaredMagnitude = 0;
90
91 for (size_t currentBin = startBin; currentBin < stopBin; ++currentBin)
92 {
93 const double currentSquaredMagnitude = std::norm (bins[currentBin]);
94
95 if (currentSquaredMagnitude > maxSquaredMagnitude)
96 {
97 maxSquaredMagnitude = currentSquaredMagnitude;
98 binOfMaxSquaredMagnitude = currentBin;
99 }
100 }
101
102 // Quinn's 2nd estimate doesn't seem to be defined for boundary bins
103 if (binOfMaxSquaredMagnitude == 0 || binOfMaxSquaredMagnitude == spectrum.getNumBins() - 1)
104 return hart::nan<double>();
105
106 // Euclidian norm of max value is to be used as a denominator, so undefines if zero
107 if (floatsEqual (maxSquaredMagnitude, 0.0))
108 return hart::nan<double>();
109
110 // Based on this implementation:
111 // https://gist.github.com/hiromorozumi/f74fd4d5592a7f79028560cb2922d05f
112
113 // Helper function used for frequency estimation
114 const auto tau = [] (double x) -> double
115 {
116 constexpr double sqrtTwoThirds = 0.816496580927726; // sqrt (2.0 / 3.0)
117 constexpr double sqrtSixBy24 = 0.10206207261596574; // sqrt (6.0) / 24.0
118
119 const double p1 = std::log (3 * x * x + 6 * x + 1);
120 const double part1 = x + 1 - sqrtTwoThirds;
121 const double part2 = x + 1 + sqrtTwoThirds;
122 const double p2 = std::log (part1 / part2);
123
124 return (0.25 * p1 - sqrtSixBy24 * p2);
125 };
126
127 const size_t k = binOfMaxSquaredMagnitude;
128 const double n = static_cast<double> (spectrum.getFFTSize());
129 const double sampleRateHz = spectrum.getSampleRateHz();
130
131 const double divider = std::norm (bins[k]);
132 const double ap = (bins[k + 1].real() * bins[k].real() + bins[k + 1].imag() * bins[k].imag()) / divider;
133 const double dp = -ap / (1.0 - ap);
134 const double am = (bins[k-1].real() * bins[k].real() + bins[k-1].imag() * bins[k].imag()) / divider;
135 const double dm = am / (1.0 - am);
136 const double d = 0.5 * (dp + dm) + tau (dp * dp) - tau (dm * dm);
137
138 const double adjustedBinLocation = static_cast<double> (k) + d;
139 const double peakFreqAdjusted = (sampleRateHz * adjustedBinLocation / n);
140
141 return peakFreqAdjusted;
142 };
143
144 const size_t numChannels = spectrum.getNumChannels();
145 return MetricQuery<double> (
146 std::move (evaluator),
147 numChannels,
149 );
150}
151
152} // namespace hart
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 sample rate is mismatched.
Frequency-domain representation of a multi-channel audio signal.
const std::complex< double > * operator[](size_t channel) const
Returns pointer to read-only magnitudes of a specific channel.
size_t getFFTSize() const
Returns FFT size.
double getSampleRateHz() const
Returns sample rate in Hz.
std::pair< size_t, size_t > getBinIndices(const Slice &slice) const
Returns a pair of indices representing a provided slice.
size_t getNumBins() const
Returns number of frequency bins per channel.
size_t getNumChannels() const
Returns number of channels.
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 > quinns2(const Spectrum &spectrum)
Returns somewhat accurate loudest frequency in the spectrum.
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.
Unit
Represents a physical unit.
@ native
Default (native) unit of whatever returns some value.
@ Hz
Hertz.
Helpers to generate common default channel subsets.
static std::vector< size_t > allChannels(size_t numChannels)
Represents a slice of analysis data.
bool isEmpty() const