From 956aec5c92060cb3425a384f097638bf30f91ded Mon Sep 17 00:00:00 2001 From: user1303836 Date: Mon, 16 Feb 2026 15:03:56 -0500 Subject: [PATCH] Add best-guess fallback for low-note pitch detection When the CMNDF threshold search fails (no dip below 0.15), fall back to the global CMNDF minimum instead of returning silence. This ensures harmonically complex signals like distorted guitar still trigger the ring modulator with a rough pitch estimate. Also widen analysis window from sampleRate/70 to sampleRate/60 for better low-frequency coverage (~5ms additional latency). --- source/dsp/YinPitchDetector.cpp | 15 ++++++++- tests/TestYinPitchDetector.cpp | 58 ++++++++++++++++++++++++++++++++- 2 files changed, 71 insertions(+), 2 deletions(-) diff --git a/source/dsp/YinPitchDetector.cpp b/source/dsp/YinPitchDetector.cpp index 8230cda..678083d 100644 --- a/source/dsp/YinPitchDetector.cpp +++ b/source/dsp/YinPitchDetector.cpp @@ -6,7 +6,7 @@ void YinPitchDetector::prepare(double sampleRate) { analysisSR = sampleRate; - halfWindow = static_cast(std::ceil(sampleRate / 70.0)); + halfWindow = static_cast(std::ceil(sampleRate / 60.0)); windowSize = 2 * halfWindow; hopSize = static_cast(std::ceil(sampleRate * 0.003)); @@ -120,6 +120,19 @@ void YinPitchDetector::analyse() } } + if (tauEstimate == 0) + { + float minVal = 1.0f; + for (size_t tau = 2; tau < n; ++tau) + { + if (cmndf[tau] < minVal) + { + minVal = cmndf[tau]; + tauEstimate = tau; + } + } + } + if (tauEstimate == 0) { lastResult = { 0.0f, 0.0f }; diff --git a/tests/TestYinPitchDetector.cpp b/tests/TestYinPitchDetector.cpp index fcd74fb..d823ee7 100644 --- a/tests/TestYinPitchDetector.cpp +++ b/tests/TestYinPitchDetector.cpp @@ -7,7 +7,7 @@ static constexpr double twoPi = 6.283185307179586476925; static int computeWindowSize(double sampleRate) { - int halfWindow = static_cast(std::ceil(sampleRate / 70.0)); + int halfWindow = static_cast(std::ceil(sampleRate / 60.0)); return 2 * halfWindow; } @@ -29,6 +29,23 @@ static void feedSine(YinPitchDetector& yin, double sampleRate, float freq, int n } } +static void feedHarmonicComplex(YinPitchDetector& yin, double sampleRate, float freq, int numSamples, float amplitude) +{ + double phase = 0.0; + double inc = twoPi * static_cast(freq) / sampleRate; + + for (int i = 0; i < numSamples; ++i) + { + double s = std::sin(phase) + + 0.8 * std::sin(2.0 * phase) + + 0.6 * std::sin(3.0 * phase) + + 0.5 * std::sin(4.0 * phase) + + 0.3 * std::sin(5.0 * phase); + yin.feedSample(static_cast(s * static_cast(amplitude))); + phase += inc; + } +} + static int feedSineUntilDetection(YinPitchDetector& yin, double sampleRate, float freq, int maxSamples) { double phase = 0.0; @@ -219,3 +236,42 @@ TEST_CASE("YIN: tracks frequency sweep across hop intervals") REQUIRE_THAT(static_cast(resultAfter.frequency), Catch::Matchers::WithinRel(880.0, 0.05)); } + +TEST_CASE("YIN: fallback detects harmonically complex low signal") +{ + YinPitchDetector yin; + yin.prepare(44100.0); + + feedHarmonicComplex(yin, 44100.0, 82.4f, 44100, 0.15f); + + auto result = yin.getResult(); + REQUIRE(result.frequency > 0.0f); + REQUIRE(result.frequency > 40.0f); + REQUIRE(result.frequency < 200.0f); +} + +TEST_CASE("YIN: silence still returns zero with fallback") +{ + YinPitchDetector yin; + yin.prepare(44100.0); + + for (int i = 0; i < 44100; ++i) + yin.feedSample(0.0f); + + auto result = yin.getResult(); + REQUIRE(result.frequency == 0.0f); + REQUIRE(result.confidence == 0.0f); +} + +TEST_CASE("YIN: threshold path still preferred for clean signals") +{ + YinPitchDetector yin; + yin.prepare(44100.0); + + feedSine(yin, 44100.0, 440.0f, 44100); + + auto result = yin.getResult(); + REQUIRE(result.confidence > 0.8f); + REQUIRE_THAT(static_cast(result.frequency), + Catch::Matchers::WithinRel(440.0, 0.01)); +}