The Whistle You Cannot Blow
A browser will hand you a 96 kHz audio context and let you synthesise a 35 kHz dog whistle without complaint. Three ceilings stand between that and a dog, and the code only clears one of them.
What it is
A ladder. The page plays a tone and you say whether you heard it. Heard it and the tone goes up, missed it and it comes down, and each change of direction halves the step until it closes in on the highest frequency you can hear. Eight turns and it has a number for you, which is probably between 14 and 17 kHz if you’re over thirty.
Then it keeps climbing past you.
The bars are the three ceilings that stand between a line of code and a dog’s ear: what the audio context can represent, what your speaker actually emits, and what you could hear even if it did. measure this device plays a sweep and listens to it come back through your microphone, so the middle bar is your own hardware rather than an assumption. Speakers rather than headphones for that one.
howl is the thing that actually works, and it’s nowhere near ultrasonic.
How it works
The ladder is a transformed up-down staircase, stepping in octaves rather than in hertz because a fixed 500 Hz step is enormous at the bottom of the range and invisible at the top. The step halves at every reversal down to a floor, and the threshold is the geometric mean of the last six turns. Geometric, because the walk happened in log frequency and an arithmetic mean would bias the answer upward.
Tested against a simulated ear with a hard cutoff, it lands within 6 percent of the right answer at 12, 15 and 17.5 kHz. Against a probabilistic ear it still finds the region.
The important detail isn’t in the staircase at all, it’s in the envelope. Gating a 19 kHz tone on and off abruptly makes a click, and a click has energy all the way down the spectrum. A listener who can’t hear 19 kHz hears the click perfectly well, says yes, and the ladder cheerfully measures the switch instead of the ear.
So I measured it rather than assuming. Rendering the same 19 kHz tone twice offline, once hard gated and once with a 50 ms raised-cosine ramp, and lowpassing both to keep only what a person could hear:
| RMS below 2 kHz | RMS below 5 kHz | |
|---|---|---|
| hard gate | -83.5 dB | -79.0 dB |
| 50 ms ramp | -154.8 dB | -141.9 dB |
71.3 dB apart. The carrier sits at -17.1 dB, so the hard gate’s audible leakage is 66 dB below the tone it’s supposed to be hiding behind. Play a 19 kHz tone loud enough to be near somebody’s threshold and that puts the click near their threshold too, and anybody chasing a tone they can’t quite hear turns the volume up, which raises both together.
Every tone on this page has that ramp on both ends for exactly this reason and no other.
Now the ceilings.
One, what the code can represent. A dog whistle is sold at 23 to 54 kHz. At CD rate the Nyquist limit is 22.05 kHz, so 35 kHz can’t exist. Ask a browser for a 96 kHz context, though, and it says yes. Ask for 192 kHz and it says yes to that as well. So the page can synthesise 35 kHz, report success, and be telling the truth about a number that no part of the rest of the chain will honour.
Two, what the speaker emits. This is the one that decides it, and it’s the one nobody measures. The page plays a six second exponential sweep from 200 Hz upward and keeps the loudest thing every FFT bin ever saw, which recovers the response because the sweep visits each frequency exactly once. Then it finds the cliff: the lowest frequency above the passband where the level has dropped 20 dB and stays down.
Walking down from the top instead would stop at the first isolated loud bin, and there is always one.
The microphone constraints matter more than the sweep does. Echo cancellation, noise suppression and automatic gain control are all on by default, and every one of them would flatten or gate the thing being measured. All three are turned off explicitly.
What comes back is the speaker, the room and the microphone together, not the speaker alone, and that’s the honest quantity. If nothing in that chain carries 30 kHz then neither does the air the dog is standing in.
Three, you. That’s the ladder, and it’s the least interesting of the three, because your ceiling being below the whistle was never in doubt. It’s on the chart so the other two have something to be compared against.
Then the howl, which is the part that works. Dogs answer things that sound like a howl, which is why sirens and harmonicas set them off. A howl is a long fairly pure glide: this one rises from 320 to 610 Hz over half a second, sags 6 percent through a second and a half of sustain, and falls back to 380, with 28 cents of vibrato at 5.2 Hz on top. Every one of those numbers is inside human hearing, comfortably.
It’s synthesised as a sawtooth through three bandpass formants at 780, 1600 and 2900 Hz, which are higher than a human vowel’s because a dog’s vocal tract is shorter than ours.
What surprised me
The browser says yes to everything. I expected to have to fight for a high sample rate, or to get a rejection I could write about. Chrome handed over 96 kHz, then 192 kHz, then 384 kHz for offline rendering, without a murmur. There is no warning anywhere in the API that the device on the other end will not carry it.
That’s worth sitting with. Any page that claims to be a dog whistle can be written in about four lines, will run without error, will pass every test you think to write, and will do nothing at all. The failure is entirely outside the part of the system that reports anything.
The click leaks 71 dB more than I expected to be able to justify. I put the ramp in from the start, because the arithmetic is obvious once stated: a step edge is broadband, and broadband includes the part you can hear. What I didn’t expect was the size of it.
66 dB below the carrier sounds like a comfortable margin until you remember what the ladder is for. It presents tones near the edge of what you can hear, which means the carrier is already close to your threshold, which puts its click there as well. A ladder without that ramp wouldn’t be slightly optimistic, it would be measuring a different thing entirely and reporting it in the same units.
That’s the same failure this whole page is about, one layer down. The code says 19 kHz, the code is correct, and what arrives at the ear is a click.
Both my test failures were the test, again. One put a synthetic loud bin on the very top band, so there was nothing above it to confirm the cliff with and the function correctly reported nothing. The other asserted the howl glides slower than an arbitrary number I had guessed at, which its onset legitimately exceeds. I replaced the second with a real continuity test: halve the time step and the largest jump should halve, which it does to within a few percent.
That’s five days running where the first thing to break was the instrument.
What I would do next
I can’t measure the thing the page is named after. Whether a dog howls back isn’t something a browser can find out, and I am not going to pretend the acoustic parameters matching published howls is the same as a dog answering.
So the honest next step is data I don’t have. The page could ask, after the howl button, whether the dog joined in, and collect breed and age alongside it. That’s a survey rather than a measurement, and it would need to be one, but the effect either exists at scale or it doesn’t.
The other gap is that the device measurement is the whole chain and can’t separate the speaker from the microphone. Two devices in the same room, one playing and one listening, would split them, and the page already has everything it needs except a way for two browsers to agree on when to start.