HART  0.2.0
High level Audio Regression and Testing
Loading...
Searching...
No Matches
hart_interpolated_peak_frequency.hpp
Go to the documentation of this file.
1#pragma once
2
3#include <algorithm> // max()
4#include <complex> // complex, norm()
5
7#include "metrics/hart_metric_query.hpp"
8#include "metrics/hart_metrics_common.hpp" // ChannelSubsets
9#include "hart_slice.hpp"
11#include "hart_units.hpp" // Unit
12#include "hart_utils.hpp" // nan(), floatsEqual(), floatsNotEqual()
13
14namespace hart
15{
16
17/// @brief Returns the center frequency of the loudest FFT bin
18/// @details
19/// Finds the maximum-magnitude FFT bin independently for each channel.
20/// Use reducers to combine multi-channel results (see @ref Reducers).
21///
22/// Supports `Unit::Hz` (native/default) unit, so requesting a unit
23/// explicitly via `MetricQuery::as()` is not required.
24///
25/// This metric operates on FFT bins exactly as stored in the Spectrum.
26/// In a not-so-likely event where multiple bins have exactly the same
27/// magnitube, the lowest frequency will be returned.
28/// This is not intended for precise pitch tracking, but still useful
29/// for some types of tests, based around looking for peaks in a band
30/// of frequencies, but not a specific frequency.
31///
32/// For more precise peak frequency estimation, consider using
33/// Quinn's second estimator (`hart::quinns2()` metric) instead.
34///
35/// Usage examples:
36/// @code
37/// // Loudest bin frequency in Hz
38/// const double loudestFreqHz = loudestBinFrequency (monoSpectrum).get();
39///
40/// // Loudest bin frequency, but only inside a frequency range
41/// const double loudestMidBanFreqHz =
42/// loudestBinFrequency (monoSpectrum)
43/// .at (Slice::frequency (500_Hz, 2000_Hz))
44/// .get();
45///
46/// // Loudest bin frequency, but only inside a specific channel subset.
47/// Calculated per chanel, then averaged.
48/// const double loudestFreqHz =
49/// loudestBinMagnitude (multiChannelSpectrum)
50/// .ch ({0, 2, 4})
51/// .get (mean());
52///
53/// @endcode
54///
55/// @param spectrum Input frequency-domain spectrum
56/// @throws hart::UnitError if unsupported unit is requested
57/// @ingroup Metrics
58inline MetricQuery<double> interpolatedPeakFrequency (const Spectrum& spectrum)
59{
60 if (spectrum.getFFTSize() == 0)
61 HART_THROW_OR_RETURN (hart::SizeError, "Cannot estimate peak frequency, if FFT size is zero", {});
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
70 if (requestedUnit != Unit::native && requestedUnit != Unit::Hz)
71 HART_THROW_OR_RETURN (hart::UnitError, "Unsupported unit", hart::nan<double>());
72
73 const double sampleRateHz = spectrum.getSampleRateHz();
74
75 if (floatsEqual (sampleRateHz, 0.0))
76 return hart::nan<double>();
77
78 const std::pair<size_t, size_t> binIndices = spectrum.getBinIndices (slice);
79 const size_t startBin = binIndices.first;
80 const size_t stopBin = binIndices.second;
81
82 if (slice.isEmpty() || stopBin - startBin == 0)
83 return hart::nan<double>();
84
85 hassert (startBin < stopBin);
86 hassert (stopBin <= spectrum.getNumBins());
87
88 const std::complex<double>* bins = spectrum[channel];
89 double maxSquaredMagnitude = 0.0;
90 size_t strongestBin = 0;
91
92 for (size_t currentBin = startBin; currentBin < stopBin; ++currentBin)
93 {
94 const double currentSquaredMagnitude = std::norm (bins[currentBin]);
95
96 if (currentSquaredMagnitude > maxSquaredMagnitude)
97 {
98 maxSquaredMagnitude = currentSquaredMagnitude;
99 strongestBin = currentBin;
100 }
101 }
102
103 // Not defined for boundary bins
104 if (strongestBin == 0 || strongestBin == spectrum.getNumBins() - 1)
105 return hart::nan<double>();
106
107 // Parabolic interpolation on magnitude
108 const double ym1 = spectrum.getBinMagnitude (channel, strongestBin - 1);
109 const double y0 = spectrum.getBinMagnitude (channel, strongestBin);
110 const double yp1 = spectrum.getBinMagnitude (channel, strongestBin + 1);
111
112 const double delta = 0.5 * (ym1 - yp1) / (ym1 - 2.0 * y0 + yp1 + 1e-30);
113 const double preciseBin = static_cast<double> (strongestBin) + delta;
114
115 const double fftSize = static_cast<double> (spectrum.getFFTSize());
116 hassert (floatsNotEqual (fftSize, 0.0));
117 return preciseBin * sampleRateHz / static_cast<double> (fftSize);
118 };
119
120 const size_t numChannels = spectrum.getNumChannels();
121 return MetricQuery<double> (
122 std::move (evaluator),
123 numChannels,
125 );
126}
127
128} // 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 an unexpected container size is encountered.
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.
double getBinMagnitude(size_t channel, size_t binIndex) const
Returns magnitude of a frequency bin, by bin index.
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 > interpolatedPeakFrequency(const Spectrum &spectrum)
Returns the center frequency of the loudest FFT bin.
FloatType nan()
Returns a quiet NaN value for the given floating-point type.
static SampleType floatsNotEqual(SampleType a, SampleType b, SampleType epsilon=(SampleType) 1e-8)
Compares two floating point numbers within a given tolerance.
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