Task 1 of 5

Compare a Signal With Itself

"What note is this?" sounds like a question about frequency. Mostly it isn't. Ask a different one: how much does this signal look like itself, shifted? Slide a copy along by lag samples, multiply it against the original point by point, and total the products. Land the shift on a whole period and peaks meet peaks — a big positive score. Land it half a period out and peaks meet troughs — a big negative one. The periodicity falls straight out as a row of peaks.

That is autocorrelation, and it is not a new idea if you have done Template Matching: it is that module's cross-correlation with the template replaced by the signal itself. It also parallelises without an argument — one thread per lag, every thread reading the same array and writing only its own cell. No atomics, no scatter, nothing to coordinate.

Below is signal: 512 samples of a plucked string recorded at 8192 Hz — 62.5 milliseconds, a fundamental plus three harmonics, decaying. Its period is in there. Go and find it.

slide it half a period and it argues with itself; slide it a whole one and it agrees
Goal: score signal against a shifted copy of itself at every lag from 0 to 255, then find the lag of the biggest peak and log the period.

Requirements

Hint 1 — which lag is mine?

With output: [256] there are 256 threads, and thread this.thread.x owns exactly one lag. Read it into a const lag on the first line and the rest of the kernel reads like the formula.

Hint 2 — the loop, and its guard

A kernel loop needs a bound known at compile time, so loop over the whole signal and skip the samples whose partner has fallen off the end:

for (let i = 0; i < this.constants.n; i++) {
  if (i + lag < this.constants.n) {
    sum += signal[i] * signal[i + lag];
  }
}

Long lags therefore add up fewer terms than short ones. Remember that — task 3 sends the bill.

Hint 3 — finding the peak in JavaScript

256 numbers is nothing; finish in plain JS.

let bestLag = 8;
for (let lag = 8; lag < 256; lag++) {
  if (r[lag] > r[bestLag]) bestLag = lag;
}

Start at 8, not 0. Lag 0 is the signal against an unshifted copy of itself: it wins every time and means nothing.

Same idea elsewhere

Correlation-by-lag is a stock primitive everywhere — NPP and cuFFT on CUDA, vDSP's conv/corr on Apple silicon, a WGSL compute shader indexed by global_invocation_id in WebGPU. The guarded inner loop is the same edge handling every stencil kernel needs, and the read pattern (all threads sweeping the same array in step) is the one caches and texture units are built to serve.

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.