Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion source/dsp/YinPitchDetector.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ void YinPitchDetector::prepare(double sampleRate)
{
analysisSR = sampleRate;

halfWindow = static_cast<int>(std::ceil(sampleRate / 70.0));
halfWindow = static_cast<int>(std::ceil(sampleRate / 60.0));
windowSize = 2 * halfWindow;
hopSize = static_cast<int>(std::ceil(sampleRate * 0.003));

Expand Down Expand Up @@ -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;

Copy link
Copy Markdown
Collaborator Author

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 minVal is below some secondary threshold (e.g. 0.5), rejecting truly ambiguous frames. But that's not needed now.

}
}
}

if (tauEstimate == 0)
{
lastResult = { 0.0f, 0.0f };
Expand Down
58 changes: 57 additions & 1 deletion tests/TestYinPitchDetector.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand All @@ -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;
Expand Down Expand Up @@ -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);

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The 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));
}
Loading