Clone
92960c4 Rewrite the README, and carry attribution and license in the HTML commits
README

Noise Bench

Continuous colored noise in a single HTML file — white, pink, brown, blue, violet, gray and green. Generated in the browser, level-matched across colors, with a spectrum analyzer that plots the measured curve against the theoretical slope so you can check the −3 dB/octave rather than take it on faith.

No build step, no dependencies, no network requests, nothing recorded. public/index.html is entirely self-contained — fonts included — and is the whole application.

The colors

Five are power laws: spectral density falls off as 1/f^α, and the color name just encodes α. Two are not, and it's worth knowing which.

Color Slope α What it is
White 0 dB/oct 0 Flat power per hertz. Bright and hissy, because equal power per hertz puts far more energy in the top octave than the bottom.
Pink −3 dB/oct 1 Equal power per octave, which matches how hearing is organized. The usual choice for masking and sleep, and what most "white noise" apps actually play.
Brown −6 dB/oct 2 Integrated white noise — a random walk, so Brownian rather than a color. Deep and rumbling.
Blue +3 dB/oct −1 Pink inverted. Used in dithering, rarely for listening.
Violet +6 dB/oct −2 Differentiated white noise. Sometimes used in tinnitus therapy.
Gray contoured — Not a power law. White shaped by an inverse equal-loudness contour, so it reads as equally loud at every frequency — cutting 2–5 kHz where hearing is most sensitive and lifting both ends. Approximated here with four biquads.
Green −3 dB + 500 Hz — Not a standard at all. No IEEE or ANSI definition exists; it's a de-facto term for mid-weighted noise. Implemented as pink with a broad lift around 500 Hz and the top rolled off.

Gray and green are approximations of informal targets, and the page says so rather than presenting them as settled.

Controls

Control Effect
Low cut / High cut 20 Hz–2 kHz and 500 Hz–20 kHz. Narrowing the band is the most effective way to make any color less fatiguing.
Waves A slow amplitude swell, roughly the cadence of surf, with an adjustable 4–40 s period.
Volume Ignored on iOS in Lock screen mode — see below.
Fade out after 15 minutes to 8 hours, then a ramped stop.
Mono Collapses to one correlated source. Off by default: independent left and right noise is wider and markedly less fatiguing on headphones.
Keep awake Holds a Screen Wake Lock. Needs a secure context, so it works over HTTPS but not plain http://.
Lock screen Switches playback path. Defaults on for touch devices.

Space toggles playback; 1–7 select a color.

How it works

One seamlessly-looping stereo white buffer feeds seven parallel filter chains, crossfaded on switch — instant color changes, and one buffer in memory instead of seven. Pink is an IIRFilterNode running the standard four-tap approximation; brown is a one-pole leaky integrator followed by a 22 Hz high-pass to kill DC; violet is a one-sample differentiator; blue is pink through that same differentiator.

Levels are measured, not guessed. On first play each chain is rendered through an OfflineAudioContext, its RMS measured, and a makeup gain applied to bring it to a common target. Unnormalized, brown and violet differ by tens of decibels — enough to be a hazard at headphone volume when switching.

Two playback paths

iOS suspends an AudioContext when the tab backgrounds or the screen locks, but keeps a media element playing. Those can't be reconciled in one signal path — routing a media element back through createMediaElementSource puts it under the AudioContext again — so the page carries both and lets you choose.

Web Audio Lock screen
Survives screen lock Android only Both platforms
Lock-screen controls No Yes, with artwork
Live analyzer Yes Theoretical curve only
Changing color or band Instant Re-renders, about a second
Volume slider Yes Ignored on iOS
Default Desktop Touch devices

HTMLMediaElement.volume is read-only on iOS, so in Lock screen mode there you use the hardware buttons.

Making the loop inaudible

A looping media element is not sample accurate — browsers insert a small gap when one wraps, which on brown noise reads as a thump every time round. Lock screen mode therefore doesn't rely on gapless looping at all.

It renders 24 seconds of the selected color, discards a 3-second pre-roll so every filter starts in steady state, and bakes a raised-sine envelope sin(πt/L) into the result so the file fades to silence at both ends. Two copies then play half a period apart. Because sin² + cos² = 1 they sum to constant power — and each file's loop point now sits at zero amplitude, so whatever the browser does when it wraps is inaudible: that copy contributes nothing at that instant while the other is at full level.

Three details make that actually work:

  • The offset comes from starting the second copy late, never from seeking. Setting currentTime races the element's load, and a seek that silently fails leaves both copies in phase — the envelope becomes 2·sin() rather than sin/cos and the output falls to zero once per loop. A phone speaker's low-frequency rolloff and AGC hide that completely; headphones expose it at once. Starting late makes the offset true by construction, and both elements then advance on the same audio clock, so they don't drift.
  • Each element gets its own Blob and object URL. Sharing one resource between two media elements is contended on some browsers, and the second simply refuses to play.
  • There is no compressor in the render. Its gain reduction hasn't settled at sample zero, which left the head of the file louder than the tail. Peak is measured afterward and scaled only if it exceeds −6 dBFS, which preserves the RMS level-matching between colors instead of peak-normalizing them.

A health check runs every two seconds and measures what is actually happening — whether the second copy is advancing, and whether it is genuinely half a period behind. The measured offset appears in the Path readout as media · 2 players · Δ12.0s. If the offset can't be held, the page falls back to a single player with a crossfade-sealed loop and says so.

The wave LFO, when enabled, is fitted to an integer number of cycles per half loop, so both offset copies carry the same envelope value at any instant and it doesn't partially cancel.

Deploying

The whole app is public/index.html. Any static host will serve it.

npx wrangler deploy          # Cloudflare Workers, per wrangler.toml

Check wrangler.toml against your wrangler version — the static-assets config key has changed across releases.

Content-Security-Policy

Everything is inlined, so a default-deny policy will break the page silently. It needs three allowances:

default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; font-src data:; base-uri 'none'; form-action 'none'

If 'unsafe-inline' is unacceptable, hash the script and style blocks instead — the hashes need recomputing on every edit.

A note on file size

The three typefaces are embedded as base64 woff2, which is roughly 130 KB of the file. Base64 adds 33% to already-compressed woff2 and Brotli can't win much of it back. For a page opened once and left running that's irrelevant; on a high-traffic site, serve the fonts as separate cacheable files instead.

Browser support

Needs AudioContext, OfflineAudioContext and IIRFilterNode — Chrome 49+, Firefox 50+, Safari 14.1+. Without IIRFilterNode the page falls back to cruder biquad approximations rather than failing. Screen Wake Lock is optional and the button disables itself where it's unavailable.

License

Public domain, under The Unlicense. Do whatever you like with it.

The embedded typefaces are third-party and keep their own licenses — OFL 1.1 for Chivo and Public Sans, Apache 2.0 for Roboto Mono. See NOTICE; the same notices ride along inside public/index.html beside the @font-face rules, so they travel with the file wherever it's served.