My three-year-old had a hearing test coming up. At that age it is done as a sound field test: tones played through a speaker in the room rather than through headphones, with the audiologist watching for a response. Unfamiliar room, unfamiliar person, unfamiliar sound - we wanted to take at least one of those out of the equation before the day.
The key to a calm appointment was not explaining the test to a three-year-old; it was making the sound boring before we got there. So I built a small warble tone generator, now living at warble.allanson.org. In the week before the appointment my kid pressed the start button roughly a hundred times, dragged the sliders around, and turned the whole business into a familiar game instead of a mystery. Playing existing recordings was considered, but the button is the point: pressing start yourself makes it your sound, not something done to you.
Why the tone warbles
The tones used in sound field testing are not steady pure tones. A fixed frequency bouncing around a room sets up standing waves, interference between the direct sound and the reflections off the walls, so exactly where a child sits determines whether a given frequency is loud or nearly absent. A warble tone sweeps the frequency back and forth around a centre value, smearing the peaks and nulls out.
The usual parameters are a warble rate of 4–6 Hz and a modulation depth of 10–20% of the centre frequency. The tool defaults to 5 Hz at 15%, the middle of both ranges, with the centre frequency adjustable from 125 Hz to 8,000 Hz.
Four nodes make the tone
This is the part that made me smile: the Web Audio API makes a warble tone
almost embarrassingly easy. An oscillator’s frequency is an AudioParam, and an
AudioParam accepts incoming audio as well as values. Feed a slow sine into a
carrier’s frequency input and you have frequency modulation, no DSP required. The
entire tone is this graph:
modulatorOscillator → modulatorGain → oscillator.frequency (FM)
oscillator → gainNode → destination (audio out)
The carrier is a sine at the centre frequency. The modulator is another sine
at the warble rate, scaled by modulatorGain to centre frequency × depth before
it lands on oscillator.frequency. The instantaneous frequency works out to
f(t) = f_centre + (f_centre × depth) × sin(2π × rate × t) - the textbook
definition of a warble tone, arrived at with four nodes.
Every slider writes straight into an AudioParam while the tone plays, which
mattered more than I expected: the test subject wants the frequency lever right
now, and waiting for a restart would have gone down badly.
One file, no build step
The whole tool is a single dependency-free index.html. No framework, no build
step, no server; it opens directly from disk, and it works on a phone - which
matters, because a three-year-old does not want your laptop, they want the thing
in your hand.
A framework was considered but rejected: nothing in a four-control page earns the weight of a build pipeline, and a single file will still open from a filesystem in ten years. The cost is real but bounded: the JavaScript is one long file with no types and no tests, which I would not accept at work and can live with here.
I spent a disproportionate amount of time on the styling. It is dressed in this site’s apollo theme, same fonts, same red, same dark mode toggle, so it reads as a small cousin of the blog rather than a random tool. Whether the intended user ever noticed the typography is not recorded.
The initial version was generated with Claude and iterated from there; a small, self-contained app on top of the Web Audio API turned out to be a good shape for that kind of collaboration.
What it is not
It is important to note that this is not an audiometer. Browser audio through whatever speaker happens to be nearby gives you relative volume and nothing more; real testing needs calibrated equipment at known levels. Keep the volume low, especially with little ears. The volume slider tops out at half gain for exactly this reason.
The goal was never measurement, it was familiarity. On that narrow goal it worked: the sound in the booth was one my kid had already summoned a hundred times at home, and the appointment went smoothly.
The generalisable version, such as it is: you cannot explain a clinical procedure to a three-year-old, but you can make it boring, and boring is achievable in an evening with four audio nodes. If it helps another family ahead of their own appointment, it has done its job.
The generator is live, and the source is on GitHub.