| e8555c89 Andy McCall |
1 |
#include "catch.hpp" |
| 2 |
#include "yin.hh" |
| 3 |
|
| 4 |
#include <cmath> |
| 5 |
#include <cstdlib> |
| 6 |
#include <numbers> |
| 7 |
#include <random> |
| 8 |
#include <vector> |
| 9 |
|
| 10 |
static constexpr float PI = std::numbers::pi_v<float>; |
| 11 |
static constexpr float TAU = 2.0f * PI; |
| 12 |
|
| 13 |
static constexpr float SAMPLE_RATE = 44100.0f; |
| 14 |
static constexpr size_t BUFFER_SIZE = 4096; |
| 15 |
static constexpr float MIN_FREQ = 50.0f; |
| 16 |
static constexpr float MAX_FREQ = 2000.0f; |
| 17 |
|
| 18 |
[[nodiscard]] static bool approx_rel(float value, float expected, float rel_err) |
| 19 |
{ |
| 20 |
return std::abs(value - expected) < (expected * rel_err); |
| 21 |
} |
| 22 |
[[nodiscard]] static bool approx_abs(float value, float expected, float abs_err) |
| 23 |
{ |
| 24 |
return std::abs(value - expected) < abs_err; |
| 25 |
} |
| 26 |
|
| 27 |
static void addSine(std::span<float> signal, float frequency, float amplitude = 1.0f, size_t start = 0, size_t end = size_t(-1)) |
| 28 |
{ |
| 29 |
if (end == size_t(-1)) end = signal.size(); |
| 30 |
auto C = TAU * frequency / SAMPLE_RATE; |
| 31 |
for (size_t i = start; i < end; ++i) { |
| 32 |
signal[i] += amplitude * std::sin(float(i) * C); |
| 33 |
} |
| 34 |
}; |
| 35 |
static void addNoise(std::span<float> signal, float amplitude) |
| 36 |
{ |
| 37 |
std::mt19937 rng(42); |
| 38 |
std::normal_distribution<float> dist(0.0f, 1.0f); |
| 39 |
for (size_t i = 0; i < signal.size(); ++i) { |
| 40 |
signal[i] += amplitude * dist(rng); |
| 41 |
} |
| 42 |
}; |
| 43 |
|
| 44 |
[[nodiscard]] static auto run(std::span<const float> signal) |
| 45 |
{ |
| 46 |
return yin::detectPitch(signal, SAMPLE_RATE, MIN_FREQ, MAX_FREQ); |
| 47 |
} |
| 48 |
|
| 49 |
TEST_CASE("YIN: silence") |
| 50 |
{ |
| 51 |
auto signal = std::vector<float>(BUFFER_SIZE, 0.0f); |
| 52 |
|
| 53 |
auto [freq, err] = run(signal); |
| 54 |
CHECK(approx_abs(freq, 0.0f, 1.0f)); |
| 55 |
CHECK(approx_rel(err, 1.0f, 0.05f)); |
| 56 |
} |
| 57 |
|
| 58 |
TEST_CASE("YIN: sine") |
| 59 |
{ |
| 60 |
auto generateSine = [](float frequency) { |
| 61 |
std::vector<float> signal(BUFFER_SIZE); |
| 62 |
addSine(signal, frequency); |
| 63 |
return signal; |
| 64 |
}; |
| 65 |
SECTION("50Hz") { |
| 66 |
auto [freq, err] = run(generateSine(50.0f)); |
| 67 |
CHECK(approx_rel(freq, 50.0f, 0.01f)); |
| 68 |
CHECK(err < 0.01f); |
| 69 |
} |
| 70 |
SECTION("100Hz") { |
| 71 |
auto [freq, err] = run(generateSine(100.0f)); |
| 72 |
CHECK(approx_rel(freq, 100.0f, 0.01f)); |
| 73 |
CHECK(err < 0.01f); |
| 74 |
} |
| 75 |
SECTION("440Hz") { |
| 76 |
auto [freq, err] = run(generateSine(440.0f)); |
| 77 |
CHECK(approx_rel(freq, 440.0f, 0.01f)); |
| 78 |
CHECK(err < 0.01f); |
| 79 |
} |
| 80 |
SECTION("1000Hz") { |
| 81 |
auto [freq, err] = run(generateSine(1000.0f)); |
| 82 |
CHECK(approx_rel(freq, 1000.0f, 0.01f)); |
| 83 |
CHECK(err < 0.01f); |
| 84 |
} |
| 85 |
SECTION("2000Hz") { |
| 86 |
auto [freq, err] = run(generateSine(2000.0f)); |
| 87 |
CHECK(approx_rel(freq, 2000.0f, 0.01f)); |
| 88 |
CHECK(err < 0.01f); |
| 89 |
} |
| 90 |
} |
| 91 |
|
| 92 |
TEST_CASE("YIN: square") |
| 93 |
{ |
| 94 |
auto generateSquare = [](float frequency) { |
| 95 |
|
| 96 |
|
| 97 |
std::vector<float> signal(BUFFER_SIZE, 0.0f); |
| 98 |
for (int harmonic = 1; harmonic <= 11; harmonic += 2) { |
| 99 |
float amplitude = 4.0f / (PI * harmonic); |
| 100 |
addSine(signal, frequency * harmonic, amplitude); |
| 101 |
} |
| 102 |
return signal; |
| 103 |
}; |
| 104 |
auto [freq, err] = run(generateSquare(321.0f)); |
| 105 |
CHECK(approx_rel(freq, 321.0f, 0.01f)); |
| 106 |
CHECK(err < 0.01f); |
| 107 |
} |
| 108 |
|
| 109 |
TEST_CASE("YIN: triangle") |
| 110 |
{ |
| 111 |
auto generateTriangle = [](float frequency) { |
| 112 |
|
| 113 |
std::vector<float> signal(BUFFER_SIZE, 0.0f); |
| 114 |
for (int harmonic = 1; harmonic <= 11; harmonic += 2) { |
| 115 |
float amplitude = 8.0f / (PI * PI * harmonic * harmonic); |
| 116 |
if ((harmonic - 1) / 2 % 2 == 1) amplitude = -amplitude; |
| 117 |
addSine(signal, frequency * harmonic, amplitude); |
| 118 |
} |
| 119 |
return signal; |
| 120 |
}; |
| 121 |
auto [freq, err] = run(generateTriangle(555.0f)); |
| 122 |
CHECK(approx_rel(freq, 555.0f, 0.01f)); |
| 123 |
CHECK(err < 0.01f); |
| 124 |
} |
| 125 |
|
| 126 |
TEST_CASE("YIN: sawtooth") |
| 127 |
{ |
| 128 |
auto generateSawtooth = [](float frequency) { |
| 129 |
|
| 130 |
std::vector<float> signal(BUFFER_SIZE, 0.0f); |
| 131 |
for (int harmonic = 1; harmonic <= 20; ++harmonic) { |
| 132 |
float amplitude = 2.0f / (PI * harmonic); |
| 133 |
addSine(signal, frequency * harmonic, amplitude); |
| 134 |
} |
| 135 |
return signal; |
| 136 |
}; |
| 137 |
auto [freq, err] = run(generateSawtooth(222.0f)); |
| 138 |
CHECK(approx_rel(freq, 222.0f, 0.01f)); |
| 139 |
CHECK(err < 0.01f); |
| 140 |
} |
| 141 |
|
| 142 |
TEST_CASE("YIN: sine with harmonics") |
| 143 |
{ |
| 144 |
SECTION("Sine 440 Hz + decaying harmonics (2nd, 3rd)") { |
| 145 |
std::vector<float> signal(BUFFER_SIZE, 0.0f); |
| 146 |
addSine(signal, 1.0f * 440.0f, 1.0f); |
| 147 |
addSine(signal, 2.0f * 440.0f, 0.5f); |
| 148 |
addSine(signal, 3.0f * 440.0f, 0.25f); |
| 149 |
|
| 150 |
auto [freq, err] = run(signal); |
| 151 |
CHECK(approx_rel(freq, 440.0f, 0.01f)); |
| 152 |
CHECK(err < 0.01f); |
| 153 |
} |
| 154 |
SECTION("Sine 440 Hz + strong harmonics") { |
| 155 |
std::vector<float> signal(BUFFER_SIZE, 0.0f); |
| 156 |
addSine(signal, 1.0f * 440.0f, 1.0f); |
| 157 |
addSine(signal, 2.0f * 440.0f, 2.0f); |
| 158 |
addSine(signal, 3.0f * 440.0f, 1.5f); |
| 159 |
|
| 160 |
auto [freq, err] = run(signal); |
| 161 |
CHECK(approx_rel(freq, 440.0f, 0.01f)); |
| 162 |
CHECK(err < 0.01f); |
| 163 |
} |
| 164 |
SECTION("Sine 440 Hz + 880Hz equally strong") { |
| 165 |
std::vector<float> signal(BUFFER_SIZE, 0.0f); |
| 166 |
addSine(signal, 1.0f * 440.0f, 1.0f); |
| 167 |
addSine(signal, 2.0f * 440.0f, 1.0f); |
| 168 |
|
| 169 |
auto [freq, err] = run(signal); |
| 170 |
CHECK(approx_rel(freq, 440.0f, 0.01f)); |
| 171 |
CHECK(err < 0.01f); |
| 172 |
} |
| 173 |
} |
| 174 |
|
| 175 |
TEST_CASE("YIN: noise") |
| 176 |
{ |
| 177 |
std::mt19937 rng(12345); |
| 178 |
SECTION("uniform") { |
| 179 |
std::uniform_real_distribution<float> dist(-1.0f, 1.0f); |
| 180 |
std::vector<float> signal(BUFFER_SIZE); |
| 181 |
std::ranges::generate(signal, [&]{ return dist(rng); }); |
| 182 |
|
| 183 |
auto [freq, err] = run(signal); |
| 184 |
|
| 185 |
CHECK(err > 0.8f); |
| 186 |
} |
| 187 |
SECTION("normal") { |
| 188 |
std::normal_distribution<float> dist(0.0f, 1.0f); |
| 189 |
std::vector<float> signal(BUFFER_SIZE); |
| 190 |
std::ranges::generate(signal, [&]{ return dist(rng); }); |
| 191 |
|
| 192 |
auto [freq, err] = run(signal); |
| 193 |
|
| 194 |
CHECK(err > 0.8f); |
| 195 |
} |
| 196 |
} |
| 197 |
|
| 198 |
TEST_CASE("YIN: chirp") |
| 199 |
{ |
| 200 |
auto generateChirp = [](float startFreq, float endFreq) { |
| 201 |
std::vector<float> signal(BUFFER_SIZE); |
| 202 |
for (size_t i = 0; i < BUFFER_SIZE; ++i) { |
| 203 |
float t = float(i) / SAMPLE_RATE; |
| 204 |
float freq = startFreq + (endFreq - startFreq) * t / (float(BUFFER_SIZE) / SAMPLE_RATE); |
| 205 |
signal[i] = std::sin(TAU * freq * t); |
| 206 |
} |
| 207 |
return signal; |
| 208 |
}; |
| 209 |
auto [freq, err] = run(generateChirp(100.0f, 1000.0f)); |
| 210 |
|
| 211 |
CHECK(err > 0.8f); |
| 212 |
} |
| 213 |
|
| 214 |
TEST_CASE("YIN: sine decaying in amplitude") |
| 215 |
{ |
| 216 |
|
| 217 |
auto generateDecayingSine = [](float frequency, float decayTime) { |
| 218 |
std::vector<float> signal(BUFFER_SIZE); |
| 219 |
float decayConstant = -std::log(0.01f) / decayTime; |
| 220 |
for (size_t i = 0; i < BUFFER_SIZE; ++i) { |
| 221 |
float t = float(i) / SAMPLE_RATE; |
| 222 |
float envelope = std::exp(-decayConstant * t); |
| 223 |
signal[i] = envelope * std::sin(TAU * frequency * t); |
| 224 |
} |
| 225 |
return signal; |
| 226 |
}; |
| 227 |
auto [freq, err] = run(generateDecayingSine(440.0f, 0.05f)); |
| 228 |
CHECK(approx_rel(freq, 440.0f, 0.01f)); |
| 229 |
CHECK(err < 0.05f); |
| 230 |
} |
| 231 |
|
| 232 |
TEST_CASE("YIN: split frequency") |
| 233 |
{ |
| 234 |
|
| 235 |
std::vector<float> signal(BUFFER_SIZE); |
| 236 |
addSine(signal, 100.0f, 1.0f, 0, BUFFER_SIZE / 2); |
| 237 |
addSine(signal, 123.0f, 1.0f, BUFFER_SIZE / 2); |
| 238 |
|
| 239 |
auto [freq, err] = run(signal); |
| 240 |
CHECK(approx_rel(freq, 113.0f, 0.1f)); |
| 241 |
CHECK(err < 0.25f); |
| 242 |
} |
| 243 |
|
| 244 |
TEST_CASE("YIN: sine with noise") |
| 245 |
{ |
| 246 |
auto generateSineWithNoise = [](float freq, float noiseAmplitude) { |
| 247 |
std::vector<float> signal(BUFFER_SIZE); |
| 248 |
addSine(signal, freq); |
| 249 |
addNoise(signal, noiseAmplitude); |
| 250 |
return signal; |
| 251 |
}; |
| 252 |
SECTION("0.1") { |
| 253 |
auto [freq, err] = run(generateSineWithNoise(440.0f, 0.1f)); |
| 254 |
CHECK(approx_rel(freq, 440.0f, 0.01f)); |
| 255 |
CHECK(err < 0.02f); |
| 256 |
} |
| 257 |
SECTION("0.2") { |
| 258 |
auto [freq, err] = run(generateSineWithNoise(440.0f, 0.2f)); |
| 259 |
CHECK(approx_rel(freq, 440.0f, 0.01f)); |
| 260 |
CHECK(err < 0.1f); |
| 261 |
} |
| 262 |
SECTION("0.3") { |
| 263 |
auto [freq, err] = run(generateSineWithNoise(440.0f, 0.3f)); |
| 264 |
|
| 265 |
CHECK(approx_rel(freq, 220.0f, 0.01f)); |
| 266 |
CHECK(err > 0.14f); |
| 267 |
} |
| 268 |
SECTION("0.4") { |
| 269 |
auto [freq, err] = run(generateSineWithNoise(440.0f, 0.4f)); |
| 270 |
|
| 271 |
CHECK(err > 0.2f); |
| 272 |
} |
| 273 |
SECTION("0.5") { |
| 274 |
auto [freq, err] = run(generateSineWithNoise(440.0f, 0.5f)); |
| 275 |
|
| 276 |
CHECK(err > 0.2f); |
| 277 |
} |
| 278 |
} |