# What Happened Between the Samples?

*Task 3 of 5 · [Sampling & Aliasing](https://gpu.rocks/learn/sampling-and-aliasing-ad14836c.md) · GPU.js Learn*

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:

```js
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.

## Figures

- **every sample gets a sinc; the sum threads all of them**

## Goal

**Goal:** reconstruct the signal at 4× the sample rate — 256 output points from
64 samples — with a 17-tap Lanczos-windowed sinc gather.

## Requirements

- Output point `j` sits at sample position `p = j / this.constants.up`
- Gather `this.constants.width` taps centred on `Math.floor(p)`, skipping indices outside the array
- Weight tap `k` by `sinc(p − k) · sinc((p − k) / a)`, and by `1` when `p − k` is 0
- Return the weighted sum — `output: [256]`

## 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

```js
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.

## Starter code

```js
// 64 samples in, 256 reconstructed points out. A gather: pull, never push.
const gpu = new GPU({ mode });

const rebuild = gpu.createKernel(function (samples) {
  const p = this.thread.x / this.constants.up;
  const centre = Math.floor(p);

  // TODO: replace this straight-line guess with a 17-tap windowed-sinc sum.
  const frac = p - centre;
  let next = samples[centre];
  if (centre + 1 < this.constants.n) {
    next = samples[centre + 1];
  }
  return samples[centre] + (next - samples[centre]) * frac;
}, {
  output: [256],
  constants: { up: 4, n: 64, a: 8, width: 17 },
});

const curve = rebuild(samples);
console.log('reconstructed points:', curve.length);
console.log('on top of sample 10:', curve[40], 'vs', samples[10]);
```

---

Interactive version: https://gpu.rocks/learn/sampling-and-aliasing-ad14836c/3

[Previous task](https://gpu.rocks/learn/sampling-and-aliasing-ad14836c/2.md) · [Next task](https://gpu.rocks/learn/sampling-and-aliasing-ad14836c/4.md)
