# The Other Axis Is Discrete Too

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

Sampling chops up *time*. Storing the samples chops up **amplitude**:
with `bits` bits there are 2bits code values to cover the whole −1…+1
range, so the step between neighbouring codes is `2 / 2^bits` and every sample gets
nudged to the nearest one.

```js
step = 2 / Math.pow(2, bits);
q    = Math.round(value / step) * step;      // NOT Math.floor
error = q - value;                           //  in −step/2 … +step/2
```

`Math.round` is load-bearing. Rounding *down* quantises just as
successfully, but every error then has the same sign, so instead of jitter centred on zero you
get a DC offset of −step/2 riding on the signal — and you measurably throw away a whole bit.
(Run this task with `Math.floor` if you want to watch it happen: every SNR drops by
about 6 dB.)

Which is the rule worth having. Signal-to-noise ratio is
`10·log10(signal power / error power)`, and halving the step — one more bit —
quarters the error power, which is 10·log10(4) ≈ **6 dB**. Do not take that on
faith: the loop below runs 4, 6, 8, 10 and 12 bits and you can read the slope straight off the
console.

The kernel writes two planes again — `output: [256, 2]`, plane 0 the quantised
signal and plane 1 the error — so one pass gives you both the result and the thing you want to
measure. That is fusion, the same trick "Reductions" ends on: compute it where you already have
the value in hand rather than paying for a second pass.

## Goal

**Goal:** quantise `signal` to `bits` bits into plane 0
and the rounding error into plane 1, then log the SNR at each of the five depths.

## Requirements

- Take `bits` as a kernel argument and derive `step = 2 / Math.pow(2, bits)`
- Plane 0 (`this.thread.y === 0`) holds `Math.round(value / step) * step`
- Plane 1 holds the error, `quantised - value`
- `console.log` the SNR in dB for each of 4, 6, 8, 10 and 12 bits

## Hint 1 — one value, two planes

Both planes are computed from the same sample, so read it once and branch at the
end:

```js
const v = signal[this.thread.x];
const step = 2 / Math.pow(2, bits);
const q = Math.round(v / step) * step;
let out = q - v;
if (this.thread.y === 0) {
  out = q;
}
return out;
```

## Hint 2 — the SNR

Sum the squares of the samples and the squares of plane 1, then

```js
const snr = 10 * Math.log10(sigPower / errPower);
```

— and print it with the bit depth beside it so the ~6 dB step is readable.

## Same idea elsewhere

The bits-versus-noise trade is the same everywhere it shows up: 16-bit audio buys about
98 dB, 8-bit textures about 50, and a GPU's `f16` half-precision path is this exact
bargain — half the bandwidth and half the memory for a coarser step. Modern inference hardware
pushes it much further with int8 and int4 kernels, and the reason quantisation-aware training
works at all is that the error stays small, zero-mean noise instead of a bias.

## Starter code

```js
// Amplitude is discrete too. Two planes: the quantised signal, and the error.
const gpu = new GPU({ mode });

const quantise = gpu.createKernel(function (signal, bits) {
  const v = signal[this.thread.x];
  // TODO: step = 2 / 2^bits, round v to the nearest step (Math.round, not floor),
  //       return the quantised value in plane 0 and (quantised - v) in plane 1.
  return v;
}, {
  output: [256, 2],
});

for (const bits of [4, 6, 8, 10, 12]) {
  const planes = quantise(signal, bits);
  let sigPower = 0;
  let errPower = 0;
  for (let i = 0; i < signal.length; i++) {
    sigPower += signal[i] * signal[i];
    errPower += planes[1][i] * planes[1][i];
  }
  const snr = 10 * Math.log10(sigPower / errPower);
  console.log(bits + ' bits: SNR ' + snr.toFixed(2) + ' dB');
}
```

---

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

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