DEV Community

The Audio Stuff
The Audio Stuff

Posted on

Building a browser tone generator with the Web Audio API

I built a free online tone generator for The Audio Stuff: sine, square, triangle and sawtooth tones, five colours of noise, frequency sweeps, a left/right channel test, a polarity click, and WAV export. Everything runs in the browser with the Web Audio API, no server and no libraries. Here are the building blocks, simplified, in case you want to make something similar.

1. Create the AudioContext on a user gesture

Browsers block audio until the user interacts with the page, so create (or resume) the context inside the click handler rather than on page load.

let ctx = null;

function audio() {
  ctx ??= new AudioContext();
  if (ctx.state === "suspended") ctx.resume();
  return ctx;
}
Enter fullscreen mode Exit fullscreen mode

2. Tones: one OscillatorNode

The four classic waveforms are built in. Put a GainNode after the oscillator so you can control the level, and keep the level modest: a full-scale square wave is loud.

function playTone(freq, type = "sine", level = 0.2) {
  const a = audio();
  const osc = a.createOscillator();
  const gain = a.createGain();
  osc.type = type;            // "sine" | "square" | "triangle" | "sawtooth"
  osc.frequency.value = freq;
  gain.gain.value = level;
  osc.connect(gain).connect(a.destination);
  osc.start();
  return osc;                 // call osc.stop() later
}
Enter fullscreen mode Exit fullscreen mode

3. Noise: fill a buffer and loop it

White noise is just random samples. Generate a couple of seconds into an AudioBuffer and loop it with an AudioBufferSourceNode.

function noiseBuffer(seconds = 2) {
  const a = audio();
  const buf = a.createBuffer(1, a.sampleRate * seconds, a.sampleRate);
  const data = buf.getChannelData(0);
  for (let i = 0; i < data.length; i++) data[i] = Math.random() * 2 - 1;
  return buf;
}
Enter fullscreen mode Exit fullscreen mode

Pink noise (equal energy per octave, falling 3 dB per octave) needs filtering. A well-known approach is Paul Kellet's method: a handful of one-pole filters at different rates, summed. Brown noise (falling 6 dB per octave) is a leaky integrator over white noise. Both are a few lines inside the same loop, plus a gain correction so the levels stay comparable.

4. Sweeps: let the AudioParam do the work

You do not need a timer to sweep frequency. AudioParam automation is sample-accurate. An exponential ramp gives a logarithmic sweep, which spends equal time in each octave and is what you usually want for finding rattles and resonances.

function sweep(from = 20, to = 20000, seconds = 20) {
  const a = audio();
  const osc = a.createOscillator();
  const t = a.currentTime;
  osc.frequency.setValueAtTime(from, t);
  osc.frequency.exponentialRampToValueAtTime(to, t + seconds);
  osc.connect(a.destination);
  osc.start(t);
  osc.stop(t + seconds);
}
Enter fullscreen mode Exit fullscreen mode

Use linearRampToValueAtTime instead if you want a linear sweep.

5. Left only, right only

To send a mono signal to one side, split and merge channels: route the signal into input 0 or 1 of a ChannelMergerNode.

function toSide(node, side) {          // side: "left" | "right"
  const a = audio();
  const merger = a.createChannelMerger(2);
  node.connect(merger, 0, side === "left" ? 0 : 1);
  merger.connect(a.destination);
}
Enter fullscreen mode Exit fullscreen mode

6. A polarity click

A polarity test needs an asymmetric signal, so that a speaker wired in reverse is distinguishable. A short buffer with a strong positive step followed by a smaller decaying negative tail works well: about 20 ms is enough.

7. Visualise with an AnalyserNode

An AnalyserNode in the chain gives you time-domain or frequency data every animation frame for a scope or a spectrum display. An fftSize of 2048 is a reasonable default.

8. Export a WAV with OfflineAudioContext

To let people download a test tone, render it faster than real time with an OfflineAudioContext, then write a 44-byte RIFF header and 16-bit PCM samples with a DataView.

async function renderTone(freq, type, seconds = 5, rate = 44100) {
  const off = new OfflineAudioContext(1, rate * seconds, rate);
  const osc = off.createOscillator();
  osc.type = type;
  osc.frequency.value = freq;
  osc.connect(off.destination);
  osc.start();
  const rendered = await off.startRendering();
  return rendered.getChannelData(0);   // Float32Array, then encode as WAV
}
Enter fullscreen mode Exit fullscreen mode

Things to watch out for

  • Clicks at start and stop. Starting or stopping an oscillator mid-cycle produces a click. A few milliseconds of gain ramp fixes it.
  • Safety. Test tones at high level can damage tweeters and hearing. Default to a low level and make users turn it up.
  • Square and sawtooth alias. The built-in oscillators are band-limited, but if you generate waveforms yourself at high frequencies, aliasing appears quickly.

You can try the finished tool here: Online Tone Generator. It sits alongside a speaker test, an ABX blind test, an FFT spectrum checker and a set of audio calculators, all free and client-side, at theaudiostuff.com/tools/.

Jakub Charkiewicz, The Audio Stuff

Top comments (0)