Task 3 of 5

What Happened Between the Samples?

You have 64 numbers. The signal they came from existed at every instant in between. Can you get it back?

The obvious answer — join the dots with straight lines — is wrong, and not by a little. A sinusoid does not lie on the chords between its own samples, so linear interpolation shaves every peak; on the signal below it misses by about 0.026 RMS. The right answer is stranger and much better: if nothing in the signal was above Nyquist, the samples determine it completely, and the formula that rebuilds it sums a sinc centred on every sample:

x(t) = Σ  x[k] · sinc(t · fs − k)          sinc(u) = sin(πu) / (πu),  sinc(0) = 1

sinc is 1 at its own sample and exactly 0 at every other whole offset, so the curve threads every sample point and interpolates smoothly between them. In principle the sum runs over all k; in practice you stop after a handful of taps and window what is left, or the abrupt cut rings. The Lanczos window is the classic: multiply by sinc(u / a), with a the half-width in samples.

On a GPU this is a gather, the shape "Thinking in Parallel" makes the case for: each output point pulls the 17 samples around it and weights them. Nothing is written anywhere but this thread's own cell, so 256 output points cost one pass, no coordination and no ordering.

The ends are honestly ragged — near the edges the window runs off the array and the sum is missing terms, so the tests only judge the interior. Real resamplers pad, mirror or taper the edges; the middle is where the idea lives.

every sample gets a sinc; the sum threads all of them
Goal: reconstruct the signal at 4× the sample rate — 256 output points from 64 samples — with a 17-tap Lanczos-windowed sinc gather.

Requirements

Hint 1 — which samples are mine?

p is generally between two samples. Take centre = Math.floor(p) and walk k from centre - a to centre + a — that is this.constants.width = 17 taps. Guard each one: if (k >= 0 && k < this.constants.n).

Hint 2 — the weight, carefully

x = p - k is exactly 0 when the output point lands on a sample, and sin(πx) / (πx) is 0/0 there — special-case it to 1. Outside ±a the window is 0, so those taps contribute nothing.

Hint 3 — the whole weight
const px = Math.PI * x;
w = (Math.sin(px) / px) * (Math.sin(px / this.constants.a) / (px / this.constants.a));

— the first factor is the sinc, the second is the Lanczos taper that lets you stop after 17 taps.

Same idea elsewhere

Windowed-sinc resampling is what every audio SRC (libsamplerate, SoX, WebAudio's own playbackRate) and every image resizer's Lanczos option actually does. On a GPU it is the same gather a separable convolution uses — CUDA reads the taps through the texture cache, WebGPU binds the sample buffer as read-only storage, and a fragment shader gets a cheap 2-tap version for free in textureSample's bilinear filter, which is precisely the straight-line guess this task rejects.

All tasks in Sampling & Aliasing

  1. One Thread, One Sample
  2. Above Nyquist
  3. What Happened Between the Samples?
  4. The Other Axis Is Discrete Too
  5. Payoff: Keep One Sample in Four

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