Repository navigation
Add best-guess fallback for low-note detection #30
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,7 +7,7 @@ static constexpr double twoPi = 6.283185307179586476925; | |
|
|
||
| static int computeWindowSize(double sampleRate) | ||
| { | ||
| int halfWindow = static_cast<int>(std::ceil(sampleRate / 70.0)); | ||
| int halfWindow = static_cast<int>(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<double>(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<float>(s * static_cast<double>(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<double>(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); | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The 40-200 Hz tolerance band is very wide (over 2 octaves) for an 82.4 Hz fundamental. This is intentional given the fallback's subharmonic ambiguity, but it means this test mainly verifies "something was detected in a plausible range" rather than "the fundamental was correctly identified." That's a reasonable stance for a fallback path — just calling it out for clarity. |
||
| 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<double>(result.frequency), | ||
| Catch::Matchers::WithinRel(440.0, 0.01)); | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The fallback scans the entire CMNDF for the global minimum, which for harmonically complex signals may land on a subharmonic (e.g. octave below) rather than the fundamental. This is fine for the ring modulator use case since subharmonic detection still produces a musically related modulation frequency, and the confidence value from the CMNDF minimum will naturally be lower, letting the PitchSmoother's sensitivity gate attenuate the effect.
Worth noting: if accuracy requirements tighten in the future, a refinement would be to only accept the fallback when
minValis below some secondary threshold (e.g. 0.5), rejecting truly ambiguous frames. But that's not needed now.