Task 1 of 5
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);
tones at that time.this.thread.x / this.constants.sampleRatethis.constants.parts components and accumulate A * Math.sin(2 * Math.PI * f * t + phase)output: [256], one thread per sampletones is a 3×3 nested array. Component c is
tones[c][0] (amplitude), tones[c][1] (frequency, Hz) and
tones[c][2] (phase, radians).
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;s += tones[c][0] * Math.sin(2 * Math.PI * tones[c][1] * t + tones[c][2]);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.
This page is an interactive exercise — the editor, the GPU runner and your saved progress need JavaScript. The text above is the full brief.