Task 2 of 5

From Lag to Pitch

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.

the tallest sample is not the peak — the peak is between two of them
Goal: find the peak lag, refine it to a fraction of a sample with parabolic interpolation, convert it to hertz, and log both.

Requirements

Hint 1 — the parabola, in one line
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.

Hint 2 — which way does it move?

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.

Hint 3 — "but I want to hum into my laptop"

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.

Same idea elsewhere

Sub-sample peak refinement is everywhere in GPU work under other names: it is the same quadratic fit stereo matchers use to get sub-pixel disparity, that keypoint detectors use to place a corner between pixels, and that time-of-flight sensors use between range bins. The pattern is identical — the grid is coarse, the underlying function is smooth, so fit and solve rather than sample harder.

All tasks in Autocorrelation & Pitch

  1. Compare a Signal With Itself
  2. From Lag to Pitch
  3. Three Ways to Get It Wrong
  4. The Note That Isn't There
  5. Payoff: The Same Curve, via the FFT

This page is an interactive exercise — the editor, the GPU runner and your saved progress need JavaScript. The text above is the full brief.