HART  0.2.0
High level Audio Regression and Testing
Loading...
Searching...
No Matches
hart_fundamental_equals.hpp
Go to the documentation of this file.
1#pragma once
2
3#include <complex>
4#include <vector>
5#include <cmath>
6#include <sstream>
7
9#include "matchers/hart_matcher.hpp"
11#include "hart_utils.hpp"
12
13namespace hart
14{
15
16/// @brief Checks the fundamental frequency of the signal
17/// @details Uses full-buffer zero-padded FFT + parabolic interpolation on the strongest bin.
18/// Works correctly on anything with a strong fundamental.
19/// If multiple channels are enabled for this matcher, it will check the mono sum of the signal.
20/// If you require every channel to match this frequency, do multiple per-channel assertions,
21/// using @ref hart::Matcher::atChannel().
22/// @ingroup Matchers
23template<typename SampleType>
25 public Matcher<SampleType, FundamentalEquals<SampleType>>
26{
27public:
28 /// @brief Creates a matcher for a specific fundamental frequency
29 /// @param expectedFundamentalHz Expected fundamental frequency in Hz
30 /// @param toleranceCents Tolerance in cents
31 FundamentalEquals (double expectedFundamentalHz, double toleranceCents = 1.0) :
32 m_expectedFundamentalHz (expectedFundamentalHz),
33 m_toleranceCents (toleranceCents)
34 {
35 if (expectedFundamentalHz <= 0.0)
36 HART_THROW (hart::ValueError, "Target frequency must be > 0");
37 }
38
39 void prepare (double sampleRateHz, size_t /* numChannels */, size_t /* maxBlockSizeFrames */) override
40 {
41 m_sampleRateHz = sampleRateHz;
42 }
43
44 bool match (const AudioBuffer<SampleType>& observedAudio) override
45 {
46 const size_t numFrames = observedAudio.getNumFrames();
47
48 if (numFrames < 64)
49 HART_THROW_OR_RETURN (hart::SizeError, "Audio is too short for fundamental detection", false);
50
51 if (! this->m_channelsToMatch.anyTrue())
52 return true; // Nothing to check
53
54 // If multiple channels are to be checked, sum them to mono
55 // Switching to double here, to make things more simple (and precise)
56 std::vector<double> observedAudioMono (numFrames, 0.0);
57 const double numChannelsSelected = static_cast<double> (this->m_channelsToMatch.numTrue());
58
59 for (size_t channel = 0; channel < observedAudio.getNumChannels(); ++channel)
60 {
61 if (! this->appliesToChannel (channel))
62 continue;
63
64 for (size_t frame = 0; frame < numFrames; ++frame)
65 observedAudioMono[frame] += static_cast<double> (observedAudio[channel][frame]) / numChannelsSelected;
66 }
67
68 // Next power of 2 after numFrames
69 size_t fftSize = 1;
70
71 while (fftSize < numFrames)
72 fftSize <<= 1;
73
74 // Zero-pad and FFT
75 std::vector<std::complex<double>> spectrum (fftSize, 0.0);
76
77 for (size_t i = 0; i < numFrames; ++i)
78 spectrum[i] = observedAudioMono[i];
79
80 calculateFFTInPlace (spectrum);
81
82 // Find strongest bin (skip DC and Nyquist frequency)
83 double maxPower = -1.0;
84 size_t strongestBin = 0;
85
86 for (size_t bin = 2; bin < fftSize / 2; ++bin)
87 {
88 const double power = std::norm (spectrum[bin]);
89
90 if (power > maxPower)
91 {
92 maxPower = power;
93 strongestBin = bin;
94 }
95 }
96
97 // Parabolic interpolation on magnitude
98 const double ym1 = std::abs (spectrum[strongestBin - 1]);
99 const double y0 = std::abs (spectrum[strongestBin]);
100 const double yp1 = std::abs (spectrum[strongestBin + 1]);
101
102 const double delta = 0.5 * (ym1 - yp1) / (ym1 - 2.0 * y0 + yp1 + 1e-30);
103 const double preciseBin = static_cast<double> (strongestBin) + delta;
104
105 m_observedHz = preciseBin * m_sampleRateHz / static_cast<double> (fftSize);
106 const double deviationCents = 1200.0 * std::log2 (m_observedHz / m_expectedFundamentalHz);
107
108 if (std::abs (deviationCents) > m_toleranceCents)
109 {
110 m_centsError = deviationCents;
111 return false;
112 }
113
114 return true;
115 }
116
117 bool canOperatePerBlock() override
118 {
119 return false;
120 }
121
122 void reset() override {}
123
125 {
126 std::stringstream stream;
127 stream << "Observed fundamental "
128 << hzPrecision << m_observedHz << " Hz ("
129 << centsPrecision << m_centsError << " cents off)";
130
131 MatcherFailureDetails details;
132 details.frame = 0; // Actually, more like a whole buffer is off
133 details.channel = 0; // All the channels, actually
134 details.description = stream.str();
135 return details;
136 }
137
138 void represent (std::ostream& s) const override
139 {
140 s << "FundamentalEquals ("
141 << hzPrecision << m_expectedFundamentalHz << "_Hz, "
142 << centsPrecision << m_toleranceCents << "_cents)";
143 }
144
145private:
146 const double m_expectedFundamentalHz;
147 const double m_toleranceCents;
148 double m_sampleRateHz = 44100.0;
149 double m_observedHz = 0.0;
150 double m_centsError = 0.0;
151
152 static void calculateFFTInPlace (std::vector<std::complex<double>>& spectrum)
153 {
154 // Bit reversal
155 size_t log2n = 0;
156
157 for (size_t temp = spectrum.size(); temp > 1; temp >>= 1)
158 ++log2n;
159
160 for (size_t i = 0; i < spectrum.size(); ++i)
161 {
162 size_t rev = 0;
163
164 for (size_t j = 0; j < log2n; ++j)
165 if (i & (size_t (1) << j))
166 rev |= size_t (1) << (log2n - 1 - j);
167
168 if (i < rev)
169 std::swap (spectrum[i], spectrum[rev]);
170 }
171
172 // Butterflies
173 for (size_t len = 2; len <= spectrum.size(); len <<= 1)
174 {
175 const double angleRadians = -hart::twoPi / len;
176 const std::complex<double> wlen (std::cos (angleRadians), std::sin (angleRadians));
177
178 for (size_t i = 0; i < spectrum.size(); i += len)
179 {
180 std::complex<double> w (1.0);
181 for (size_t j = 0; j < len / 2; ++j)
182 {
183 std::complex<double> u = spectrum[i + j];
184 std::complex<double> v = spectrum[i + j + len / 2] * w;
185 spectrum[i + j] = u + v;
186 spectrum[i + j + len / 2] = u - v;
187 w *= wlen;
188 }
189 }
190 }
191 }
192};
193
195
196} // namespace hart
Checks the fundamental frequency of the signal.
void represent(std::ostream &s) const override
Makes a text representation of this Matcher for test failure outputs.
MatcherFailureDetails getFailureDetails() const override
Returns a description of why the match has failed.
bool match(const AudioBuffer< SampleType > &observedAudio) override
Tells the host if the piece of audio satisfies Matcher's condition or not.
FundamentalEquals(double expectedFundamentalHz, double toleranceCents=1.0)
Creates a matcher for a specific fundamental frequency.
void prepare(double sampleRateHz, size_t, size_t) override
Prepare for processing It is guaranteed that all subsequent process() calls will be in line with the ...
bool canOperatePerBlock() override
Tells the host if it can operate on a block-by-block basis.
void reset() override
Resets the matcher to its initial state.
Base for audio matchers.
std::ostream & centsPrecision(std::ostream &stream)
Sets number of decimal places for values in cents (frequency deviation)
std::ostream & hzPrecision(std::ostream &stream)
Sets number of decimal places for values in hertz.
constexpr double twoPi
2 * pi
#define HART_THROW_OR_RETURN(ExceptionType, message, returnValue)
#define HART_THROW(ExceptionType, message)
#define HART_MATCHER_DECLARE_ALIASES_FOR(ClassName)
size_t channel
Index of channel at which the failure was detected.
std::string description
Readable description of why the match has failed.
size_t frame
Index of frame at which the match has failed.