# A Tone Buried in Noise

*Task 4 of 5 · [Filtering in the Frequency Domain](https://gpu.rocks/learn/frequency-filtering-8c225e10.md) · GPU.js Learn*

Noise is broadband; a tone is not. In the time domain they are hopelessly mixed —
`noisy` looks like grass. In the frequency domain they come apart: three tones
put nearly all their energy into three bins, while the noise spreads its energy thinly over
all 256. Concentration versus dilution is the entire trick, and it is the best argument
anyone has ever made for paying the cost of a transform.

The numbers here: the three tones land at magnitudes **124**,
**71** and **42**, and the loudest noise bin reaches
**19.5**. Anything between about 22 and 40 separates them; the gate is set to
**30**, comfortably in the middle of that gap.

A convenience falls out of the conjugate symmetry that made task 2 painful. Bin
`k` and bin `n − k` are conjugates, and conjugates have
*equal magnitude* — so a gate that reads magnitude keeps or drops both mirrors
together, automatically, and the result comes back real without you doing anything about
it. Magnitude needs both planes though:

```js
const mag = Math.sqrt(re * re + im * im);
```

The starter gates on `Math.abs(re)` alone, which throws away any tone whose
energy happens to sit in the imaginary part. Run it and watch the SNR go the wrong way.

You are given `clean` as well as `noisy` — you would not have it
in real life, and that is the point: it is here so the improvement can be a number instead
of an impression.

## Goal

**Goal:** finish `gate` so every bin quieter than
`this.constants.gate` is zeroed on both planes, then read the two SNR figures
off the console. Before is about **4.8 dB**; after should be over
**20 dB**.

## Requirements

- Keep `output: [256, 2]`; the spectrum arrives as `spec[0][k]` / `spec[1][k]`
- Compute the bin's magnitude from **both** planes: `Math.sqrt(re * re + im * im)`
- Return `0` on both planes when that magnitude is below `this.constants.gate`; otherwise pass the bin through unchanged
- Do not fold the index and do not special-case the mirrors — equal magnitudes take care of them

## Hint 1 — read both planes, decide once

Every thread of a bin has to reach the same verdict, so read both planes
whichever one you are on:

```js
const k = this.thread.x;
const re = spec[0][k];
const im = spec[1][k];
const mag = Math.sqrt(re * re + im * im);
if (mag < this.constants.gate) return 0;
```

Dropping the `Math.sqrt` and comparing `re*re + im*im` against
30 is not the same test — it is a gate at magnitude 5.5, and 171 of the 256 bins
clear it.

## Hint 2 — the SNR arithmetic

Signal-to-noise in dB is ten times the log of a power ratio: the clean
signal's energy over the energy of whatever you got wrong.

```js
let signal = 0;
let error = 0;
for (let i = 0; i < 256; i++) {
  signal += clean[i] * clean[i];
  error += (test[i] - clean[i]) * (test[i] - clean[i]);
}
const db = 10 * Math.log10(signal / error);
```

## Same idea elsewhere

Spectral gating is the backbone of noise reduction: Audacity's Noise Reduction,
RNNoise, and the "spectral subtraction" every phone applies to your voice are all this idea
with a smarter threshold — estimated per band, from a stretch of silence, instead of one
number. The same shape reappears as thresholding in wavelet denoising, and as top-k pruning
of a weight matrix. Keeping the few coefficients that carry the energy is one idea wearing
several hats.

## Starter code

```js
// Three tones, a lot of noise, and a gate in the frequency domain.
const gpu = new GPU({ mode });

// GIVEN — the forward transform. A real signal in, a complex spectrum
// out: with output [n, 2] the result is indexed spec[p][k], where plane 0
// holds the real part of bin k and plane 1 the imaginary part.
const dft = gpu.createKernel(function (sig) {
  const k = this.thread.x;
  let re = 0;
  let im = 0;
  for (let t = 0; t < this.constants.n; t++) {
    const angle = (-2 * Math.PI * k * t) / this.constants.n;
    re += sig[t] * Math.cos(angle);
    im += sig[t] * Math.sin(angle);
  }
  if (this.thread.y === 0) return re;
  return im;
}, { output: [256, 2], constants: { n: 256 } });

// GIVEN — the inverse. A complex spectrum in as spec[0][k] / spec[1][k], a
// complex signal out in the same two planes. The 1/n lives HERE and nowhere
// else: divide a second time "to be safe" and everything comes back n times
// too small.
const idft = gpu.createKernel(function (spec) {
  const t = this.thread.x;
  let re = 0;
  let im = 0;
  for (let k = 0; k < this.constants.n; k++) {
    const angle = (2 * Math.PI * k * t) / this.constants.n;
    const c = Math.cos(angle);
    const s = Math.sin(angle);
    re += spec[0][k] * c - spec[1][k] * s;
    im += spec[0][k] * s + spec[1][k] * c;
  }
  if (this.thread.y === 0) return re / this.constants.n;
  return im / this.constants.n;
}, { output: [256, 2], constants: { n: 256 } });

// YOUR JOB — zero the bins where noise dominates.
const gate = gpu.createKernel(function (spec) {
  const k = this.thread.x;
  const re = spec[0][k];
  const im = spec[1][k];

  // TODO: a bin's magnitude needs BOTH planes — Math.sqrt(re*re + im*im).
  // This one reads the real part only, so a tone whose energy sits in the
  // imaginary part is thrown away with the noise.
  const mag = Math.abs(re);

  if (mag < this.constants.gate) return 0;

  if (this.thread.y === 0) return re;
  return im;
}, { output: [256, 2], constants: { gate: 30 } });

const cleaned = idft(gate(dft(noisy)));

function snr(test) {
  let signal = 0;
  let error = 0;
  for (let i = 0; i < 256; i++) {
    signal += clean[i] * clean[i];
    error += (test[i] - clean[i]) * (test[i] - clean[i]);
  }
  return 10 * Math.log10(signal / error);
}

console.log('SNR before:', snr(noisy).toFixed(2), 'dB');
console.log('SNR after:', snr(cleaned[0]).toFixed(2), 'dB');
```

---

Interactive version: https://gpu.rocks/learn/frequency-filtering-8c225e10/4

[Previous task](https://gpu.rocks/learn/frequency-filtering-8c225e10/3.md) · [Next task](https://gpu.rocks/learn/frequency-filtering-8c225e10/5.md)
