JavaScript Web Audio Table
| Piece | What it does | Field note |
|---|---|---|
new AudioContext() | The audio graph root | Starts SUSPENDED until a gesture - resume() in the click handler |
createOscillator() | A tone generator | square/sine/saw - frequency + gain = the beep primitive |
GainNode | Volume stage | setTargetAtTime for clicks-free fades; setValueAtTime pops |
connect() chains | Routing graph | osc โ gain โ destination - nodes are cheap, graphs are the design |
createBufferSource | Play samples | decodeAudioData(mp3) then loop/offset - the sample player |
AnalyserNode | Read the wave | getByteFrequencyData - the visualizer feed |
BiquadFilterNode | Tone shaping | lowpass/highpass/peaking - the EQ primitive |
autoplay policy | Audio needs a gesture | Like video: no sound before user interaction - the beeps-after-load bug |
Web Audio is a GRAPH, not a player: an AudioContext routes signals through nodes - oscillators, gains, filters, analysers - into the destination. Synthesis, effects and analysis are the same wiring arranged differently, and every node is cheap enough that the graph IS the design.
Bottom line: two entry laws decide whether your audio works at all. A fresh AudioContext starts SUSPENDED until a user gesture - call resume() inside the click handler or your first beep silently never happens. And direct gain jumps CLICK: setValueAtTime snaps the waveform mid-cycle, while setTargetAtTime's exponential approach is the fade that sounds professional.
The honest part: oscillator + gain is the entire beep primitive - most UI audio needs nothing more. The graph grows when sound grows: decodeAudioData for samples, BiquadFilter for tone, AnalyserNode to read the wave back out for visualization.
How to use
- Unlock on the first gesture: click โ ctx.resume() โ schedule the sound - an AudioContext created at page load stays suspended until then, by policy.
- Fade instead of snap: gain.gain.setTargetAtTime(0.0001, ctx.currentTime, 0.05) before stopping - exponential release kills the click at note end.
- Wire the chain: oscillator.connect(gain).connect(analyser).connect(ctx.destination) - insert nodes anywhere in the chain; each is one more effect stage.
Frequently asked questions
Why does my AudioContext produce no sound until the user clicks?
The autoplay policy, audio edition. Browsers create AudioContexts in the SUSPENDED state unless the creation happened inside a user gesture - a page that builds its audio graph at load time holds a suspended context, and every scheduled sound waits forever. The fix is an unlock pattern: on the first click (anywhere, or on your sound toggle), call ctx.resume(), and from then on the context runs. The same policy mutes background tabs, which is usually desired - but it also means 'beep when the request finishes' fails if no gesture ever happened: audio that matters must be part of an interaction story.
Why do my gain changes click and pop?
Discontinuity. setValueAtTime jumps the amplitude instantly, and an instantaneous change in a waveform is a click by definition - the speaker cone snaps between values. The professional pattern is setTargetAtTime(target, startTime, timeConstant), which approaches the target exponentially - a fade whose curve ears accept. The same principle covers stopping oscillators: ramp the gain toward zero FIRST, then stop the oscillator after the fade completes. Zero-crossing scheduling (starting notes where the wave crosses silence) is the specialist version; the exponential fade is the 90% solution.
When do I use oscillators versus decoded audio buffers?
Oscillators synthesize: pure tones, sweeps, chiptune-style effects - mathematically generated on the fly, zero assets, infinite duration. Buffers PLAY recordings: decodeAudioData(arrayBuffer) turns an MP3 into an AudioBuffer, and a BufferSource plays it with loop, offset and playbackRate control. The split: UI blips and alarms are oscillator territory (2 nodes, no downloads); music, voice and any recorded sound are buffers. Hybrid is normal too - a buffer loop under oscillator stabs, routed through the same gain and filter stages. What buffers cannot do cheaply is pitch-shift arbitrarily; what oscillators cannot do at all is carry timbre you did not synthesize.
How does the AnalyserNode turn audio into a visualizer?
AnalyserNode is a tap in the graph that exposes the signal as arrays: getByteFrequencyData gives the FFT (frequency buckets - the classic bar visualizer), getByteTimeDomainData gives the raw waveform (the oscilloscope look). Insert it anywhere in the chain (usually just before destination), requestAnimationFrame a loop that pulls the array and draws to canvas - the two APIs compose into every audio visualizer on the web. Two practical notes: the FFT size (fftSize property) trades resolution for smoothness, and the analyser is read-only - tapping it does not alter the audio, so one analyser can feed multiple visualizations.