Disclosure before anything else. I built the free tool linked at the end, and I sell a paid version of it. I have no connection to Vocal Pitch Monitor or to the person who makes it.
Why I went looking
Google's autocomplete is a public endpoint. It answers with what people type rather than with what you assume they type, and it costs nothing to ask. Here is what came back this morning for "vocal pitch monitor".
vocal pitch monitor app
vocal pitch monitor online
vocal pitch monitor pc
vocal pitch monitor online free
vocal pitch monitor free
vocal pitch monitor apk
vocal pitch monitor download
vocal pitch monitor mod apk
Vocal Pitch Monitor is an Android app. It draws the note you are making as a line on a scrolling graph, and singers like it a lot. On Google Play it has more than a million downloads and 4.1 stars from 7,310 reviews.
So the app exists and is well liked. Yet four of those eight completions are people asking for it somewhere other than an Android phone. There is an iOS build by the same developer, released in 2014, last updated in January 2021, priced at 1.99 dollars, and rated 2.6 out of 5 from 42 ratings. Against 4.1 from 7,310 on Android. I have not used it and I am not going to review an app I have not used, but a gap that size between two versions of one product is the kind of thing that keeps a search query alive for years.
The Web Audio API hands a page raw microphone samples, so a tool like this no longer has to be an app at all. I built one. Then I went to check whether mine was actually correct, and it was not.
The bug
I stopped feeding my detector my own voice and started feeding it tones whose frequency I already knew. It was always sharp. Never flat, always sharp. That is the shape of a bug, not the shape of noise.
frequency what my detector reported
82.41 Hz 29.8 cents sharp
98 Hz 15.5 cents sharp
130.81 Hz 9.1 cents sharp
440 Hz 2.6 cents sharp
Thirty cents is about a third of a semitone. On a low E, my own tool was lying to anyone who checked it against a tuning fork. Three symptoms to explain, and they turned out to have one cause. The sign is always positive. The size grows as the note gets lower. And it is much worse on a pure sine than on a voice.
The cause
The detector is a YIN implementation. YIN builds a difference function
d(tau) = sum over j of ( x[j] - x[j+tau] )^2
and expanded that is
d(tau) = power of the head window
+ power of the tail window
- 2 * autocorrelation(tau)
The usual fast implementation computes the autocorrelation with an FFT, and then takes a shortcut. It assumes both power terms equal r0, the autocorrelation at lag zero, so it writes
d(tau) = 2*r0 - 2*acf[tau]
That shortcut is where it goes wrong, and it goes wrong for a reason that has nothing to do with sound.
FFT autocorrelation is biased. At lag tau only N - tau sample pairs actually overlap, but the result is still divided by N. So acf[tau] decays as tau grows, purely as an artefact of counting. Feed it a perfectly periodic signal and it still slopes downward.
Put that decaying acf into 2*r0 - 2*acf[tau] and the difference is inflated more and more at long lags. The minimum therefore slides toward shorter lags. A shorter lag means a higher frequency. Every reading comes out sharp.
Now all three symptoms fall out of it. The bias only ever pushes one way, so the sign is always positive. A low note means a long period means a large tau, where the taper is worst, so the error grows in the bass. And a pure sine has a wide shallow minimum with nothing to anchor it, while a harmonically rich voice has a sharp narrow one that resists being dragged, which is why my worst case was a sine and not a singer.
The fix
Do not correct the autocorrelation. Recompute the difference honestly, over a window of constant length, on a handful of lags around the first estimate.
const largeur = n >> 1; // same count for every lag
for (let tau = tauMin; tau <= tauMax; tau++) {
let somme = 0;
for (let j = 0; j < largeur; j++) {
const ecart = buf[j] - buf[j + tau];
somme += ecart * ecart;
}
d[tau - tauMin] = somme;
}
Constant length is the whole point. Every lag is judged on exactly the same number of terms, so the counting artefact cannot exist. Then take the local minimum and interpolate parabolically. It costs one narrow pass over half the buffer, which is nothing next to the FFT you already ran.
Same table afterwards.
82.41 Hz 0.0 cents
98 Hz 0.0 cents
130.81 Hz 0.0 cents
440 Hz 0.0 cents
I got the fix wrong first, and that part is worth more than the fix
My first version searched three samples either side of the first estimate. It changed nothing at 82 Hz. Gain of exactly zero, on the one case I had written it for.
A thirty cent error is about nine samples on a low E and less than one sample on a high A. So a fixed search width in samples missed precisely the cases that needed it, and helped only the cases that did not. The width had to be a fraction of the period.
const rayon = Math.max(3, Math.round(tauCentre * 0.04));
When the size of a bug scales with something, the fix has to scale with the same thing. A constant put up against a proportional defect works at exactly one point on the scale.
One more thing saved me here. My search returns the original estimate untouched if the minimum lands on the edge of the bracket, rather than interpolating against a missing neighbour. That guard is why the failure showed up as "no change" instead of as a plausible wrong number. A guard that hands back its input when it is unsure is worth more than one that always produces an answer.
Two things to check in any pitch detector
Feed it a frequency you already know. Not your voice. A test that uses your voice cannot separate your error from the tool's. Sixteen synthetic tones found a bug that months of singing at it did not.
Feed it a pure sine in the bass. It is the least forgiving input for autocorrelation methods and it is also, unhelpfully, the first thing a sceptical user reaches for.
And keep the two kinds of test apart. I have a bench that compares my port to its reference implementation to within a ten thousandth of a cent, and it stayed green through all of this, because both sides were wrong in the same way. A fidelity test tells you that you copied correctly. Only a known frequency tells you that you are right.
The tool
One page, runs in the tab, no account, nothing installed, and nothing you sing leaves the machine because there is no server to send it to.
https://thibaudlepan77-svg.github.io/steady-pitch/
It names the note and shows how many cents sharp or flat you are, in C D E or do re mi, with a settable reference A. There is a trainer on the same page that grades a held note. Level one is free and uses the same detector at the same precision. The full file is 9.99 dollars once, no subscription, and I will say plainly that the big singing apps have far more content than I do.
Top comments (0)