KAOS-FM is a comedy / prank-call radio station in the browser. Streams arrive as clean digital audio — HLS, Icecast, playlists — but we wanted the dial to feel like a cheap bedroom radio from the early 2000s, not a lossless CD player. That means AM and FM simulation in Web Audio: bandlimiting, quieting hiss, multipath, mains hash, and the occasional neighbor with a reciprocating saw.
This note is a field dump for other developers. We are not claiming RF perfection. We are claiming that if you research how real receivers fail, then schedule those failures as intermittent audio-graph events, the illusion gets surprisingly far — and that some “clever” effects should be deleted the moment they sound canny or digital.
Architecture that keeps noise honest
The spine is boring on purpose: source → makeup → radio effect → limiter → out. Inside the
radio effect we split two ideas that must never be fused:
- Program path — the station itself, bandlimited and gently compressed.
- Synth / noise path — hiss, hum, crackle, sferics, tool RFI, quieting splash.
Carrier fades and multipath dips duck program gain only. Noise stays up (or rises) so a weak signal sounds noisy, not “someone turned the master volume down.” That single rule is the difference between a radio and a volume LFO.
AM sessions also get a seeded character (Mulberry32 from a random 32-bit seed): sometimes hum, sometimes buzz, sometimes both, sometimes clean. Same code path, different roll of the dice — because real rooms and power grids are not identical every listen.
AM: what actually sounds like medium wave
Selective fading is not a volume fade
Skywave and multipath can notch the carrier harder than the sidebands. An envelope detector then behaves like the station is overmodulated — scratchy, fizzy, raucous — even when overall RF is still “there.” Listeners often describe it as mistuning or bit-erosion. The literature calls it selective fading; synchronous detectors exist largely to beat it.
Our sim keeps slow carrier dips on the program path, and on a seeded fraction of those dips we also: close a mistune-style lowpass (IF-skirt / off-tune), crossfade toward an asymmetric soft-clip “foldover” path, and bloom a fizzy demod hash. Quiet fades stay volume-only. Distorted fades get the rasp.
Mains, crackle, sferics
Classic AM beds: square-ish magnetic hum (50/60 Hz + harmonics), power-line arcing buzz with a 120/s (or 60/s) burst envelope (ARRL-style frying hash), sparse impulse crackle (thermostats, ignition, switching), and occasional sferic crashes. Levels are tuned to sit under the station — audible in the quiet, not a meme overlay.
Steep IF, not telephone EQ
Program audio is cascaded lowpassed into a steep ~4 kHz wall (classic MW IF character), with mono fold up front. Whistle / heterodyne inject after the program path so 9/10 kHz adjacent-channel leftovers stay hearable without smearing the voice band.
FM: multipath, quieting, and bandwidth discipline
15 kHz is a steep roll-off, not a brickwall
Stereo FM audio is limited to about 15 kHz so it stays clear of the 19 kHz pilot. Transmitters use stereo-generator lowpasses that are flat through the band and then fall off steeply — not a digital brickwall (brickwalls ring and sound unnatural in a browser sim). We landed on six cascaded Butterworth stages at 15 kHz (~−72 dB/oct), plus mild pre-emphasis and a de-emphasis-style highshelf. Soft clip and a gentle edge peak live inside that ceiling.
De-esser: LR4 spit control that does not scratch
Broadcast-style HF dynamics matter once you add pre-emphasis. We A/B’d a static “spit tame” peaking cut (too weak) against an aggressive split-band compressor (too scratchy / overmodulated on highs). The keeper is a Linkwitz–Riley 4th-order crossover with HF-only compression, dialed about a third gentler than the first LR4 pass — then forced on for FM (no settings toggle).
Three different “signal got weak” sounds
Real FM has several quieting modes. We schedule them as separate gain stages so they do not cancel each other into mush:
- Picket fencing — brief multipath nulls with a “shhp” hiss splash (BBC-style quieting).
- Antenna / tree fizz — rapid SNR flutter (whip shake, foliage chop) with denser pulses than picket.
- Antenna knock null — rare 300–1500 ms near-total duck: program almost gone, quieting hiss loud, then recover — the classic “I bumped the telescopic aerial” moment.
Multipath rasp and ignition crackle
Occasional harsh multipath scratch (longer than a click) and sparse impulse crackle for ignition / multipath pops. Live streams also get rare buffer-pop clicks. These are scheduled bursts, not continuous beds — continuous grit fatigues listeners in under a minute.
Brushed power-tool RFI
Universal motors (recip saws, circular saws, drills) spark at the commutator. That broadband hash couples through building wiring or radiates into a nearby radio. On FM it is rarer than on AM, but when it hits it is unmistakable: intermittent raspy buzz while the tool runs. We schedule sparse “work sessions” (a few on/off cuts) with a 120 Hz arc envelope and spark hash under load — not a continuous drone.
What we tried and deleted
Lab rule: if it sounds clever but not radio, kill it.
- Starlink-style tiled packet hiccups — delay-tap stutter with bit-fry hash. Fun on paper; read as a broken stream buffer, not terrestrial FM. Removed.
- HF-only “sibilance fuzz” spit path — meant to model multipath S/Z distortion; landed as canny intermittent phase / shrill hash. Removed after A/B.
- Static peaking “spit tame” — did almost nothing next to real ess energy. Replaced by the gentler LR4 de-esser.
Impulse crackle versus picket fence also took a flip-flop: one sounds like weak fuzz pops, the other like quieting dips. Keep both only if they stay out of each other’s cadence; otherwise pick the failure mode you actually hear on a car radio.
Practical notes for other developers
- Duck program, raise noise — never duck the master bus for “fading.”
- Schedule, don’t drone — intermittent multipath / RFI / sferics beat continuous beds.
- Seed character per graph build — same listener, different “radio in the room.”
- Steep lowpass ≠ brickwall — protect the 19 kHz story with slope, not a hard cut.
- A/B with ears, then delete — selective-fade foldover and commutator hash earned their keep; digital tile stutter did not.
- Live vs automation — some textures (buffer pops) only make sense on affiliate streams; keep beds clean for playlist automation if that is your product.
Implementation lives in KAOS-FM’s client audio module (js/kaosfm-audio.js):
buildAmRadioEffect / buildFmRadioEffect plus the various
scheduleFm* / scheduleAm* timers. Toggle AM/FM in settings to rebuild the graph
after changes — Web Audio does not hot-patch topology for free.
If you are building a radio toy, a nostalgia skin, or a stream player that wants terrestrial grit: start from how receivers fail, not from how synthesizers decorate. The physics is already the sound design.
Try it live on KAOS-FM — Audio Experience → AM or FM.