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>& /* inputAudio */, const AudioBuffer<SampleType>& observedOutputAudio) override
45 {
46 const size_t numFrames = observedOutputAudio.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 < observedOutputAudio.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> (observedOutputAudio[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() const 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
Container for audio data.
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.
bool match(const AudioBuffer< SampleType > &, const AudioBuffer< SampleType > &observedOutputAudio) override
Tells the host if the piece of audio satisfies Matcher's condition or not.
MatcherFailureDetails getFailureDetails() const override
Returns a description of why the match has failed.
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() const 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.
Thrown when an unexpected container size is encountered.
Thrown when an inappropriate value is encountered.
#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.
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_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.