radiocli Home Blog GitHub

All posts

Both Sides Cancel

Recording the scanner looked like the easy one. radiocli could already hear the radio: a package that opens a sound card, cuts the stream into 20 ms frames, folds it down to mono and hands it to anything that asks. A WAV file is a 44 byte header and then the samples. I had already sketched the interface for a squelch gate into that package, with a comment saying so. This was supposed to be an afternoon. Everything else about this radio has taken longer than it looked, from its protocol to its menus, so I should have known better.

The first real recording came back as sixteen seconds of silence. The one after that sounded like the dispatcher was talking through a kazoo.

Both were mine, and both were the same mistake wearing different clothes. It boils down to this: I had a number, the number looked like evidence, and I never asked what it was evidence of.

Sixteen seconds of nothing

The design was the obvious one. Watch the level of the incoming audio, track the noise floor so nobody has to set a threshold by hand, and call it a transmission when the sound rises above that floor by a comfortable margin. Buffer ten seconds so the start never gets clipped. Ask the radio what channel it stopped on, purely to label the file.

Then the recorder wrote a 16.44 second file with nothing audible in it, and the description beside it said "samples": 1. That field counts how many times the scanner was asked what it was hearing during the recording. The radio gets polled three times a second, so a sixteen second transmission should have around fifty. One means the radio said it was receiving exactly once, and I recorded for another fifteen seconds after it stopped.

Measuring the file explained it. The transmission was a single 20 ms blip at -43 dBFS. Before it the line sat at -88 dBFS, and after it the line sat at -77 dBFS and stayed there. A scanner’s audio output has two idle levels, not one, and they were eleven decibels apart. My floor had settled on the quieter of the two, my margin put the trigger between them, and the louder idle level read as speech for as long as it lasted.

I could have widened the margin. That fixes this radio, this cable, this evening, and it is a guess wearing a lab coat: the gap between those two levels belongs to somebody’s particular hardware, not to audio in general. The better answer was sitting in the other half of the program the whole time. The scanner reports whether its own audio gate is open. It is not inferring that from loudness, it knows. So the radio decides whether a transmission is happening, the audio decides exactly when it started and stopped, and neither one is asked to do the other’s job.

A voice with the body taken out of it

The kazoo took longer, because my first answer was wrong and had good evidence behind it.

The transmission was on a P25 trunked system, and P25 does not send the sound of a voice. It sends a compact description of one, a few thousand bits a second, and the receiving radio builds a voice back out of that description. It is genuinely clever engineering, and it is also famous among scanner listeners for sounding robotic on hard consonants and marginal signal. I measured the recording’s spectrum and found it band-limited: ninety-nine percent of the energy below 3.4 kHz, which is where a voice codec’s output stops. Case closed, I thought. Nothing to fix.

Then I ran the same measurement on NOAA weather radio through the same cable. That is analog FM, no codec within a mile of it, and it had the same complaint. So the codec was innocent, and I owed it an apology.

What I found instead, measuring one side of the cable against the other on continuous analog voice, was this. The left channel peaked at -9.5 dBFS. The right channel peaked at -8.2 dBFS. Averaging the two together peaked at -19.5 dBFS.

Two channels carrying the same mono audio average with no loss at all. Losing eleven decibels means they are fighting each other. And the level was the smaller half of the damage. Comparing the spectrum of one side against the average, on the same voice, the energy between 300 Hz and 1 kHz fell from sixty-seven percent to thirteen. Low frequencies are the most alike between two channels, so they cancel the most completely, and what survives is a thin reedy voice with its body scooped out. That is the kazoo. It is not a codec artifact and it is not a fault in the radio’s receiver. It is arithmetic.

The headphone jack on the SDS100 and SDS150 is wired out of phase. My notes have the menu in them, from mapping every screen behind the MENU key, but nothing anywhere says what it is for; that came from another owner who had read the firmware release notes. Uniden addressed it in firmware rather than in hardware, by adding a menu that inverts one side. Given the alternative was a recall, that is a reasonable recovery and I would probably have made the same call. The consequence is that there are now two populations of these radios in the wild, identical except for a setting most owners have never opened, and any software reading that jack has to work on both.

Mono compatibility was a solved problem in 1961

Broadcast engineers hit this before I was born, and they solved it properly.

When the FCC approved stereo FM in April 1961, every FM radio already in every kitchen in America was mono, and none of them were going to be thrown away. So the design constraint was mono compatibility from the first line: the main channel carries left plus right, the sum, and a 38 kHz subcarrier carries left minus right, the difference. A mono set hears the sum and never knows the difference channel exists. A stereo set adds and subtracts to recover the two sides.

The sum is the signal you can always trust. The difference is the fragile one. They put the sum on the wire, on purpose, in 1961, so that nothing downstream would ever have to reconstruct it and get it wrong.

Sixty-five years later I wrote code that reconstructed mono by averaging two channels I had never verified were in phase, and got a difference signal for my trouble.

The comment that was true when it was written

The part that stings is that the bug was not really in the arithmetic. It was in a default.

The code that decides how to fold stereo to mono compares the two channels and picks a side when one of them is dead. If neither is dead, it mixes. And if the input stays quiet for thirty seconds, it gives up waiting and fixes the answer at mix, permanently, with a comment explaining why: mix is the fold that is never silent.

That comment was true when I wrote it. It is untrue for a cable whose two sides cancel, and I did not know such a cable existed. A scanner is quiet most of the time, so on any ordinary evening that deadline expired long before the first transmission and locked in the one answer that destroys the audio, for the entire run. The recorder was not occasionally unlucky. It was reliably wrong, and the mechanism that made it reliably wrong was a safety default.

Both fixes came out the same shape in the end. Stop trusting the proxy and measure the outcome. The fold is now judged by what it actually produces, so an average that comes out quieter than either side alone is treated as destruction rather than combination, whichever way the radio’s menu happens to be set, and whether the culprit is the firmware or a badly wired adapter. And nothing settles on no evidence any more. Silence is not a data point.

I wrote the whole thing up in oddities.md, with the measurements, next to the mislabels and the typos and the one command that locks the radio up. I called that file the best one in the repo back in “Return Prevous Mode” and it keeps earning it.

Two signals that look identical on any meter and add up to nothing. That was the cable. It was also, for about a day, the way I was reasoning about it.