Task 2 of 5
A lag is a count of samples; a pitch is a count of hertz. One division bridges
them, and it is easy to write upside down. The peak lag is the period, so the
period in seconds is lag / sampleRate and the frequency is its reciprocal:
sampleRate / lag. Put those the wrong way round and the
answer is out by four or five orders of magnitude, which at least fails loudly.
Now the real problem. Lags are integers. At 8192 Hz, lag 18 means 455.1 Hz, lag 19 means 431.2 Hz and lag 20 means 409.6 Hz — steps of more than 20 Hz, which up here is nearly a semitone. The signal below is concert A, 440 Hz, whose true period is 18.618 samples. The best integer lag is 19, and a tuner built on it would report 431 Hz: 35 cents flat, audibly wrong, and no amount of GPU makes the grid any finer.
Three numbers fix it. Near its top the correlation curve is smooth, so fit a parabola through the peak sample and its two neighbours and take the apex — the peak between the samples. This is the one part of the module that belongs on the CPU: it is three numbers and a divide, done once.
8 for the peak lag p (the kernel is already written for you)δ = ½(r[p−1] − r[p+1]) / (r[p−1] − 2·r[p] + r[p+1]), and the refined lag is p + δconsole.log('refined lag:', p + delta)console.log('pitch:', sampleRate / (p + delta), 'Hz')const a = r[p - 1];
const b = r[p];
const c = r[p + 1];
const delta = (0.5 * (a - c)) / (a - 2 * b + c);
delta always lands between −0.5 and +0.5 — it is a nudge, not a jump.
Here the true peak is to the left of the sampled one, so
delta comes out negative and the refined lag is smaller
than p. If your refinement pushes the answer further from 440 Hz than
the raw integer lag did, the two terms in the numerator are the wrong way round: it is
r[p - 1] - r[p + 1], left minus right.
So would we. In a browser page the wiring is short:
const mic = await navigator.mediaDevices
.getUserMedia({ audio: true });
const audio = new AudioContext({ sampleRate: 8192 });
const analyser = audio.createAnalyser();
analyser.fftSize = 512;
audio.createMediaStreamSource(mic).connect(analyser);
const frame = new Float32Array(analyser.fftSize);
analyser.getFloatTimeDomainData(frame);
const r = correlate(frame); // the same kernel, real audio
None of that runs here, and not for a policy reason: your code executes inside a Web
Worker, and a Worker has no AudioContext, no
OfflineAudioContext and no navigator.mediaDevices. So the
signals in this module are synthesised in the course's own source, which has the
consolation that every expected answer is exact.
This page is an interactive exercise — the editor, the GPU runner and your saved progress need JavaScript. The text above is the full brief.