Ilma records scheduled RTTY and WEFAX windows so it can re-decode them offline, away from the real-time pressure that makes the Raspberry Pi drop audio blocks during a live decode. The recordings used to be raw IQ at 256 kS/s. The digital modes now record decimated audio instead, because a 50-baud teleprinter recovers its own bit timing and gains nothing from raw phase.
That raised a question worth measuring rather than guessing: if 48 kHz audio is already overkill for tones near 1 kHz, how low can we go? A leaner stream would mean smaller files and, maybe, a stream the Pi could decode live without dropping anything.
Three rates, one recording
The test
One recording, decoded at three rates. The input never changes, only the decimation ratio, so quality differences can only come from the rate itself, not from a different capture.
The window is a real DWD broadcast: DDK9 on 10100.8 kHz, 18:00 UTC, 275 seconds, signal at +20 to +28 dB. It opens with the usual RYRY idle and a gale warning:
RYRYRYRYRYRYRYRYRYRYRYRYRYRYRYRYRYRYRYRY
ZCZC 646
WODL45 EDZW 201800
STRONG WIND, GALE AND STORM WARNINGS FOR SEA AREAS:
GERMAN BIGHT, WESTERN AND SOUTHERN BALTIC.

Decoded at 48 kHz, 8 kHz, and 4 kHz:
| rate | decimate | decode | total | disk |
|---|---|---|---|---|
| 48 kHz | 1.55 s | 2.06 s | 3.61 s | 96 KB/s |
| 8 kHz | 1.76 s | 0.14 s | 1.89 s | 16 KB/s |
| 4 kHz | 1.87 s | 0.07 s | 1.94 s | 8 KB/s |
(Times are from a laptop, not the Pi. The absolute numbers are not the point; the shape of the curve is.)
What it says
Quality holds. At 8 kHz and 4 kHz the transcript is byte-for-byte the same as at 48 kHz. The mark and space tones sit at 775 and 1225 Hz, comfortably under even the 2 kHz Nyquist limit of a 4 kHz stream, so nothing the decoder needs is thrown away. On a strong window there is no penalty for going lean.
The compute saving is not where intuition puts it. The obvious guess is that less audio means less work everywhere. Only half true. The decode itself collapses from 2.06 s to 0.07 s, roughly thirty times faster at 4 kHz, because it has thirty times fewer samples to chew through. But the decimation does not get cheaper. It gets slightly worse: 1.55 s at 48 kHz, 1.87 s at 4 kHz. The filter that turns 256 kS/s of IQ into audio has to be longer to roll off more steeply for a lower output rate, and it still has to read every one of the 256 kS/s input samples. The dominant cost is bound to the input rate, which never changes, so steeper decimation buys nothing and costs a little.

Disk is the clean win. 96, 16, 8 KB/s. Six times smaller at 8 kHz, twelve at 4 kHz. For the scheduled windows that is a rounding error, but it is exactly the lever that decides whether continuous recording of the primary mode is ever practical on an SD card.
Where this lands
8 kHz is the sweet spot. It takes almost the entire decode saving and a six-fold disk saving, and it keeps a comfortable margin above the tones. 4 kHz halves the disk again but adds no compute benefit and trims the Nyquist headroom, so it is only worth it if storage is the binding constraint and the quality holds on weaker signals than this one.
The honest caveat: this is one strong window. A faint or fading signal, with energy closer to the 4 kHz Nyquist edge and a tighter anti-alias transition, is the case that could separate the rates. That test wants more windows across a range of conditions before 4 kHz earns any trust.
The CIC filter: three times faster, at what cost?
The interesting residue is the decimation cost. It is now the fixed floor: it does not shrink with the audio rate, and on the Pi it competes for the same core as the live decode. Standard polyphase resampling cannot help, because its filter lengthens as fast as the savings would arrive. A CIC decimator can, in principle, because it replaces multiplies with running sums and reads the integer samples the dongle already hands us. The question is whether the theory survives contact with numpy and a real broadcast.
I bet it would be faster but that the win would not matter. Claude Code bet the other way. We were each half right.
What a CIC actually does
Three cascaded running sums at the full input rate, a downsample by the decimation factor, then three cascaded differences at the output rate. The integrators overflow almost immediately on a long stream, which sounds fatal and is not: the structure is linear, so as long as the arithmetic wraps consistently and the true answer fits in the word size, the comb unwinds the overflow exactly. A constant input pushed through four million samples drives the accumulators far past a 64-bit integer and the output still comes back as the constant. That invariant is the first thing the test suite checks, because without it the rest is noise.
The result
Same DDK9 window as before, 275 seconds at +20 to +28 dB, decimated to 8 kHz three ways and decoded:
| path | decimate | transcript vs production |
|---|---|---|
| polyphase, complex (production) | 1.59 s | reference |
| polyphase, real channel only | 1.15 s | identical |
| CIC, real channel | 0.50 s | 97.4% similar |

Two things to read off this.
The speed bet is settled: the CIC is 3.2 times faster than the production path. Part of that is unfair to the FIR. The production decimator resamples the complex signal and then keeps only the real part, which means it computes the imaginary channel and throws it away. Take the real channel first and the polyphase drops to 1.15 s, so the honest filter-against-filter number is 2.3 times. Either way the CIC wins on compute, comfortably, exactly as the operation count predicted. I win that round.
But the transcript is not identical, and that is the interesting part. The two polyphase variants agree to the byte, which proves the real-channel shortcut is not the culprit. The CIC alone diverges, and it diverges by getting things slightly wrong:
AMTLICHE -> AMTLCCHE
20.05.2026 -> 70.05.2026
CONTACT -> COMNAC
Scattered single-bit slips, eleven lines of seventy-two touched, on a signal strong enough that the sharp filter makes no errors at all. The CIC’s gentle sinc rolloff lets through a little more of the energy the polyphase filter rejects outright, and that is enough to flip the occasional marginal bit. Raising the filter order tightens the stopband but costs back the speed (the win falls to 2.0 times at order five) and does not close the gap. A passband-droop compensator does nothing, because droop was never the problem. The problem is stopband rejection, and a three-stage CIC simply has less of it than a proper FIR.
Where this lands
The CIC is a cheaper, slightly worse decimator.
For the job this code actually does, that trade is a loss. The offline re-decode runs about 150 times faster than real time already, so shaving the decimation from 1.6 s to 0.5 s on a five-minute window saves nothing anyone will notice, and it buys that nothing at the price of a noisier transcript. On a weaker signal, where bit decisions are already marginal, the gap would only widen. There is no reason to put it on the offline path.
The one place the speed could matter is live decoding on the Pi, where the decimator and the decoder fight for the same core in real time. Even there the case is weak. The live transcript is already the throwaway draft that the offline pass overwrites, so making it cheaper and noisier optimizes the copy nobody keeps. And the live drops were never purely about decimation cost; they are also about three Python threads contending for one interpreter lock, which a faster filter does not touch.
So the bet resolves the way bets between an optimist and a pessimist usually do. The CIC is genuinely faster, and I was right to back it on that. It is also genuinely worse, which I did not predict. And it is faster in a place where speed has no value and worse in a way that does, which is why the sharp, expensive, multiply-heavy FIR keeps its job.