Under the Hood
Webaudio

Samples, sample rate, and what an AudioBuffer holds

A sound wave is continuous, but every node in the Web Audio graph only ever pushes around plain arrays of numbers — this lesson shows how sampling, sample rate, and bit depth turn a wave into that array, and what an AudioBuffer physically stores.

Samples, sample rate, and what an AudioBuffer holds

The previous lesson described the render thread producing "samples" every ~2.9 milliseconds without ever saying what a sample actually is. That gap is worth closing before anything else in this module, because the answer isn't an abstraction — it's the literal data every node passes to the next one. Zoom all the way in, past the graph, past the nodes, down to the numbers themselves.

Here's the whole idea in one sentence: a continuous sound wave is measured at a fixed rate, each measurement is stored as a number with finite precision, and the resulting array of numbers — one per channel — is the raw material every audio node reads and writes. Everything below just unpacks that sentence.

Sound as a wave, and what "measuring" it means

A sound is a pressure wave: something vibrates, it pushes air molecules back and forth, and those pressure fluctuations arrive at a microphone (or your eardrum) as a continuous, smoothly-varying signal — at any instant in time, the wave has some amplitude. A microphone converts that pressure into a continuous voltage that traces the same shape.

Computers cannot store a continuous anything — infinite precision at infinite time resolution is not a thing a finite machine can hold. So digital audio makes a deal: instead of keeping the whole continuous curve, it takes a sample — a single amplitude reading — at regular, fixed intervals, and keeps only that list of readings. Sampling is this act of measuring the wave's height over and over, tick after tick, and throwing away everything in between the ticks.

Picture the smooth curve of a wave with vertical tick marks dropped onto it at even spacing — each tick marks an instant where you read off the curve's height and keep only that number. Do that thousands of times a second and the curve turns into a plain array. That array, once taken, is a complete stand-in for the sound as far as digital audio is concerned — nothing else about the original wave survives.

Sample rate: how often you measure

The sample rate is how many of those readings you take per second, measured in Hertz. Web Audio's standard rate is 44,100 samples per second (44.1 kHz) — meaning the render thread's speaker needs a fresh number 44,100 times every second, which is exactly the cadence the render-thread lesson described as unforgiving.

The rate isn't arbitrary, and it isn't just "more is better." It follows from a hard mathematical limit called the Nyquist theorem: to faithfully reconstruct a wave of a given frequency from its samples, you must sample at more than twice that frequency. Flip that around and it tells you what a sample rate can represent: at sample rate R, the highest frequency you can faithfully capture is R / 2 — the Nyquist frequency. Sample too slowly relative to a wave's frequency and you don't get a fuzzy version of it — you get aliasing, where the samples describe an entirely different, lower "phantom" frequency that was never in the original signal, because too few points were taken to pin down how fast the real wave was oscillating.

Bit depth: how precisely you measure

Sample rate answers how often. Bit depth answers a separate question: how precisely is each individual reading stored? A sample isn't a mathematically exact real number — it's rounded to fit a fixed amount of storage, a process called quantization. More bits means more possible amplitude values to round to, and a smaller rounding error per sample.

CDs store samples as 16-bit integers — 65,536 possible amplitude levels. The rounding error this introduces is called quantization noise: a faint, unavoidable hiss that comes from every real value being nudged to the nearest representable step. Web Audio, though, does its internal processing as 32-bit floating-point numbers, normalized to roughly the range -1.0 to +1.0 (with 0.0 as silence). Floats give enormously more precision and headroom than a 16-bit integer format — which matters because audio nodes chain together, and each stage's rounding error would otherwise compound with the next.

This uncompressed, sampled, quantized representation — a plain array of amplitude numbers at a known rate and precision — is called PCM (pulse-code modulation). It's the lowest common substrate underneath every format: a .wav file is close to raw PCM with a small header; an .mp3 is PCM run through a lossy compression scheme and has to be decoded back into PCM before anything can play it.

The AudioBuffer: PCM you can hold in your hand

An AudioBuffer is Web Audio's object for exactly this: a block of decoded PCM samples, one plain array per channel, sitting in memory ready to be fed into the graph. Its properties are the concepts above, named directly:

// Suppose `buffer` is an AudioBuffer we already have
console.log(buffer.sampleRate);       // e.g. 44100 — samples per second
console.log(buffer.length);           // total samples per channel
console.log(buffer.duration);         // buffer.length / buffer.sampleRate, in seconds
console.log(buffer.numberOfChannels); // 1 = mono, 2 = stereo, etc.

const channelData = buffer.getChannelData(0); // Float32Array, one number per sample
console.log(channelData[0], channelData[1]);  // the first two raw amplitude values

getChannelData(ch) is the part worth sitting with: it hands you a Float32Array — the literal samples for one channel, laid out one after another, each value roughly in -1.0..+1.0. A stereo buffer holds two of these arrays, one for the left channel and one for the right, each sampled and quantized independently; a mono buffer holds one. There is no hidden structure beyond this — an AudioBuffer is these arrays plus the bookkeeping (sampleRate, numberOfChannels) needed to know how to interpret them.

Getting PCM from a compressed file

Files you actually load — an .mp3, an .ogg — are compressed, not raw PCM. Before the graph can touch them, something has to decode them back into that raw sample array, which is exactly what decodeAudioData does:

async function loadBuffer(context, url) {
  const response = await fetch(url);
  const arrayBuffer = await response.arrayBuffer();       // raw compressed bytes
  const audioBuffer = await context.decodeAudioData(arrayBuffer); // -> AudioBuffer
  return audioBuffer; // now sampleRate/length/getChannelData all apply
}

decodeAudioData takes the compressed bytes, runs the codec's decompression, resamples if needed to the context's sample rate, and hands back a fully-formed AudioBuffer — uncompressed PCM, one Float32Array per channel, ready to be handed to a source node and connected into the graph.

Back to the render thread, in these terms

Recall the render thread's render quantum of 128 samples from the previous lesson. Now that "sample" has a concrete meaning, that number stops being an arbitrary constant: it's 128 individual amplitude readings per channel, computed fresh by every node in the graph, every ~2.9 milliseconds, forever. A GainNode processing a render quantum is doing nothing more exotic than multiplying 128 floating-point numbers by its gain value; an OscillatorNode is computing 128 fresh points along a waveform. The graph moves arrays of samples; this lesson is what's actually inside them.

Where this goes next

Sources are where these sample arrays get produced in the first place — synthesized from nothing by an oscillator, or read out of an AudioBuffer like the one above. Source nodes: oscillators, buffers, and the microphone covers both, plus the one-shot lifecycle that governs every source in the graph.

Go deeper

Check yourself

Answer out loud, as if an interviewer asked. If you hand-wave, reread that section.

  1. What is a sample, mechanically — what two things happen to a continuous wave to produce one number in the array?
  2. State the Nyquist theorem and use it to explain why 44.1 kHz sampling cannot faithfully represent a 30 kHz tone.
  3. Why was 44.1 kHz specifically chosen as Web Audio's standard sample rate, rather than some lower rate closer to 40 kHz?
  4. What is quantization noise, and why does it exist even with a very high bit depth?
  5. Why does Web Audio use 32-bit floats internally instead of 16-bit integers like a CD, given that both can represent silence-to-full-volume amplitude?
  6. Given an AudioBuffer `buf`, what does buf.getChannelData(1) return, and what would calling it on a mono buffer with argument 1 imply about the buffer?
  7. What does decodeAudioData actually do to an mp3's bytes, and why can't a source node play those compressed bytes directly?