Task 3 of 5
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.
j sits at sample position p = j / this.constants.upthis.constants.width taps centred on Math.floor(p), skipping indices outside the arrayk by sinc(p − k) · sinc((p − k) / a), and by 1 when p − k is 0output: [256]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).
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.
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.
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.
This page is an interactive exercise — the editor, the GPU runner and your saved progress need JavaScript. The text above is the full brief.