Task 1 of 5

One Thread, One Sample

A signal, to a computer, is a list of numbers: signal[i] is how big the thing was at moment i. Nothing else survives. Which moment is that? Sample i happens at t = i / sampleRate seconds — and forgetting that division is the single most common bug in this whole field, because the code still runs and the answer is only wrong by a factor of the sample rate.

Building samples is the friendliest shape a GPU has: every sample is computed from its own index and nothing else, so 256 threads do 256 independent sums. No neighbours, no ordering, no coordination — the easiest possible parallel problem, and it produces the input every other task in this module runs on.

Your tones input is three components, each an [amplitude, frequency in Hz, phase in radians] triple. Add them up at this thread's time:

signal[i] = Σ  A · sin(2π · f · t + φ)        with t = i / sampleRate

Two things that bite. Math.sin counts radians, so a frequency in Hz has to be multiplied by 2π before it goes in. And a real signal like this one is just a plain [n] array of floats — the paired output: [n, 2] shape this track uses for complex signals arrives later, with the DFT.

One boundary, stated plainly: your code runs in a Web Worker, which has no AudioContext, no OfflineAudioContext and no navigator.mediaDevices. Nothing here can record or play anything. Outside the sandbox the samples would arrive like this, and the kernel would not know the difference:

const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
const audio = new AudioContext({ sampleRate: 48000 });
const analyser = audio.createAnalyser();
audio.createMediaStreamSource(stream).connect(analyser);

const buffer = new Float32Array(analyser.fftSize);
analyser.getFloatTimeDomainData(buffer);   // ← 2048 real samples
kernel(buffer);
sample 64 is not second 64 — the whole field runs on one division
Goal: fill 256 samples of a one-second signal — each thread turns its own index into a time and sums the three components of tones at that time.

Requirements

Hint 1 — reading a component

tones is a 3×3 nested array. Component c is tones[c][0] (amplitude), tones[c][1] (frequency, Hz) and tones[c][2] (phase, radians).

Hint 2 — the time of this sample

With sampleRate = 256, thread 0 is at t = 0 s, thread 64 is at t = 0.25 s, and thread 255 is at t = 0.996 s:

const t = this.thread.x / this.constants.sampleRate;
Hint 3 — the loop body
s += tones[c][0] * Math.sin(2 * Math.PI * tones[c][1] * t + tones[c][2]);

Same idea elsewhere

Per-sample synthesis is the "embarrassingly parallel" case every platform opens with: a CUDA kernel with one thread per sample, a WebGPU compute shader whose global_invocation_id.x is the sample index, a Metal kernel over a 1D grid. It is also what an AudioWorkletProcessor does per render quantum on the CPU — the same arithmetic, 128 samples at a time instead of all of them at once.

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.