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 "metrics/hart_interpolated_peak_frequency.hpp"
10#include "matchers/hart_matcher.hpp"
12#include "hart_reducers.hpp" // mean()
13#include "hart_utils.hpp"
14
15namespace hart
16{
17
18/// @brief Checks the fundamental frequency of the signal
19/// @details Uses full-buffer zero-padded FFT + parabolic interpolation on the strongest bin.
20/// Works correctly on anything with a strong fundamental.
21/// If multiple channels are enabled for this matcher, it will check the mono sum of the signal.
22/// If you require every channel to match this frequency, do multiple per-channel assertions,
23/// using @ref hart::Matcher::atChannel().
24/// @ingroup Matchers
25template<typename SampleType>
27 public Matcher<SampleType, FundamentalEquals<SampleType>>
28{
29public:
30 /// @brief Creates a matcher for a specific fundamental frequency
31 /// @param expectedFundamentalHz Expected fundamental frequency in Hz
32 /// @param toleranceCents Tolerance in cents
33 FundamentalEquals (double expectedFundamentalHz, double toleranceCents = 1.0) :
34 m_expectedFundamentalHz (expectedFundamentalHz),
35 m_toleranceCents (toleranceCents)
36 {
37 if (expectedFundamentalHz <= 0.0)
38 HART_THROW (hart::ValueError, "Target frequency must be > 0");
39 }
40
41 void prepare (double sampleRateHz, size_t /* numInputChannels */, size_t /* numOutputChannels */, size_t /* maxBlockSizeFrames */) override
42 {
43 m_sampleRateHz = sampleRateHz;
44 }
45
46 bool match (AnalysisContext<SampleType> context) override
47 {
48 if (context.outputAudio().getNumFrames() < 64)
49 HART_THROW_OR_RETURN (hart::SizeError, "Audio is too short for fundamental detection", false);
50
51 ChannelFlags channelsToCheck = this->getChannelFlags();
52 channelsToCheck.resize (context.outputAudio().getNumChannels()); // TODO: Do it in the parent class implementation
53
54 if (! channelsToCheck.anyTrue())
55 return true; // Nothing to check
56
57 // TODO: It's not the best fundamental frequency estimator. Implement something like McLeod at some point for it.
58 m_observedHz = interpolatedPeakFrequency (context.outputSpectrum()).ch (channelsToCheck).get (mean());
59 const double deviationCents = 1200.0 * std::log2 (m_observedHz / m_expectedFundamentalHz);
60
61 if (std::abs (deviationCents) > m_toleranceCents)
62 {
63 m_centsError = deviationCents;
64 return false;
65 }
66
67 return true;
68 }
69
70 bool canOperatePerBlock() const override
71 {
72 return false;
73 }
74
75 void reset() override {}
76
78 {
79 std::stringstream stream;
80 stream << "Observed fundamental "
81 << hzPrecision << m_observedHz << " Hz ("
82 << centsPrecision << m_centsError << " cents off)";
83
85 details.frame = 0; // Actually, more like a whole buffer is off
86 details.channel = 0; // All the channels, actually
87 details.description = stream.str();
88 return details;
89 }
90
91 void represent (std::ostream& s) const override
92 {
93 s << "FundamentalEquals ("
94 << hzPrecision << m_expectedFundamentalHz << "_Hz, "
95 << centsPrecision << m_toleranceCents << "_cents)";
96 }
97
98private:
99 const double m_expectedFundamentalHz;
100 const double m_toleranceCents;
101 double m_sampleRateHz = 44100.0;
102 double m_observedHz = 0.0;
103 double m_centsError = 0.0;
104
105 static void calculateFFTInPlace (std::vector<std::complex<double>>& spectrum)
106 {
107 // Bit reversal
108 size_t log2n = 0;
109
110 for (size_t temp = spectrum.size(); temp > 1; temp >>= 1)
111 ++log2n;
112
113 for (size_t i = 0; i < spectrum.size(); ++i)
114 {
115 size_t rev = 0;
116
117 for (size_t j = 0; j < log2n; ++j)
118 if (i & (size_t (1) << j))
119 rev |= size_t (1) << (log2n - 1 - j);
120
121 if (i < rev)
122 std::swap (spectrum[i], spectrum[rev]);
123 }
124
125 // Butterflies
126 for (size_t len = 2; len <= spectrum.size(); len <<= 1)
127 {
128 const double angleRadians = -hart::twoPi / len;
129 const std::complex<double> wlen (std::cos (angleRadians), std::sin (angleRadians));
130
131 for (size_t i = 0; i < spectrum.size(); i += len)
132 {
133 std::complex<double> w (1.0);
134 for (size_t j = 0; j < len / 2; ++j)
135 {
136 std::complex<double> u = spectrum[i + j];
137 std::complex<double> v = spectrum[i + j + len / 2] * w;
138 spectrum[i + j] = u + v;
139 spectrum[i + j + len / 2] = u - v;
140 w *= wlen;
141 }
142 }
143 }
144 }
145};
146
148
149} // namespace hart
Contains audio-related artefacts useful for analysis by matchers.
A set of boolean flags mapped to each audio channel.
void resize(size_t newNumChannels)
Resizes the container.
bool anyTrue() const noexcept
Checks if any of the flags is set to true
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.
FundamentalEquals(double expectedFundamentalHz, double toleranceCents=1.0)
Creates a matcher for a specific fundamental frequency.
bool match(AnalysisContext< SampleType > context) override
Tells the host if the piece of audio satisfies Matcher's condition or not.
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.
void prepare(double sampleRateHz, size_t, size_t, size_t) override
Prepare for processing It is guaranteed that all subsequent process() calls will be in line with the ...
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.
Returns the arithmetic mean of all elements in the range.