About

Test Audio With AnalyserNode, Not the Screen — Why All 17 Demos Looked Silent in a Background Tab

The drawing stops, the sound doesn't. Measuring the peak also catches clipping

Test audio code with an AnalyserNode, not the screen. In a background tab the drawing stops but the sound keeps going. Measure the peak and you learn not just "is there sound" but also "is it clipping." The ten-line snippet below is enough to check audio demos automatically.

All 17 came back silent

I built 17 runnable boxes for a learning app and tried to confirm with browser automation that each one made sound. At first I read the oscilloscope canvas and checked "was a waveform drawn." Result: all 17 silent. Clicking them by hand, every one of them played.

The automated tab had document.visibilityState = "hidden", a background tab. Here's what happens there.

In a background tab
requestAnimationFrame paused, so the canvas oscilloscope never draws
Turbo.visit page swap waits on rAF, so turbo:load never fires
Tone.getDraw().schedule rAF based, doesn't run
setInterval slowed to about once per second
AudioContext keeps running. Sound plays

Only the drawing side stopped. The sound side was fine. Judging from the screen was always going to be wrong.

document.visibilityState = "hidden" AudioContext: running canvas oscilloscope drawn by requestAnimationFrame: paused reads "silent" (wrong) AnalyserNode getFloatTimeDomainData() peak 0.2 = gain 0.2 01.0 silentsound, OK clipping: 1.17, 1.23

The check

Intercept the analyser the page creates, hold on to it, and read the largest absolute value in its time-domain data.

const orig = BaseAudioContext.prototype.createAnalyser
BaseAudioContext.prototype.createAnalyser = function () {
  const a = orig.call(this)
  window.__lastAnalyser = a
  return a
}

const peak = () => {
  const buf = new Float32Array(__lastAnalyser.fftSize)
  __lastAnalyser.getFloatTimeDomainData(buf)
  return buf.reduce((m, v) => Math.max(m, Math.abs(v)), 0)
}
// 0 means silence, above 1 means clipping

If the page has no analyser, create one for testing and insert it before the output. Inject this before the page's own scripts run, or there's nothing to intercept.

What the numbers showed

Local Chrome, background tab (2026-09-19). By canvas, all 17 were silent (wrong). By AnalyserNode, the measured level matched the volume in the code: gain 0.2 → peak 0.2, 0.3 → 0.3, 0.15 → 0.15.

As a bonus, three cases of clipping showed up. None of them were obvious by ear.

  • Hand-written 16-step drum machine: 1.17. Kick 0.8 + snare 0.4 + hat 0.2 landed on the same beat

  • Tone.js Synth at default volume (0dB): 0.99. Right on the edge

  • Tone.js bass + kick: 1.23

All fixed by turning the volume down. In digital audio anything beyond ±1 gets cut off, and that's the crackle you hear. Drums, where several sounds hit at the same instant, go over especially easily.

Compared with other ways

Method Background tab Clipping Note
A person listens – easy to miss 0.99 vs 1.17 is hard to hear
Read the canvas waveform wrong can't tell rAF stops
AnalyserNode peak works catches it page needs an analyser, or you insert one

When you first write an E2E test for sound, screenshot comparison is the obvious idea. Audio should be measured on the audio side.

Not verified yet

The AudioContext was running even though the button was clicked by script. Probably because a real click had happened earlier (sticky activation: once the user has interacted, the permission sticks). I haven't checked whether it reaches running without any real click, whether the same check works in headless Playwright, or whether rendering with OfflineAudioContext could put this into CI.

If you want automated tests around a web feature that plays audio, feel free to talk to negabaro. This check caught three real bugs for us.

Key Points

1

In a background tab rAF pauses, so judging by a canvas waveform reads as silence

2

Intercept createAnalyser and read the max absolute value from getFloatTimeDomainData

3

Peak 0 is silence, above 1 is clipping. Overlapping hits like drums trigger it often

Use Cases

Automated checks for audio demos and screens with sound effects Checking during development whether the mix goes over 1
← "Left Ear Only" Means Two Things — StereoPanner Moves, ChannelSplitter Picks Strudel Is Great for Beat Sketches, Weak for Finished Tracks — and Lazy Importing It Silently Drops Effects →