WEFAX — HF marine weather-fax decoder
The oldest image mode still on the air: brightness carried as the instantaneous frequency of an audio tone (1500 Hz black → 2300 Hz white), 120 lines per minute, IOC 576 — demodulated with a Hilbert-transform FM discriminator and painted line by line into a PNG.
Rafe project · app/radio/wefax.py · RX only · independent implementation (no third-party decoder); no automated test — validated by on-air use · free-running line sync (no start-tone/phasing lock yet, §4)
Abstract
Meteorological services (DWD, NOAA, JMA, Northwood…) still broadcast synoptic charts and satellite imagery as HF radiofax: an SSB transmission whose audio tone frequency is the pixel brightness. The standard marine parameters are 120 LPM (2 lines/second) and index of cooperation 576, which fixes the line geometry. Reception is USB; the decoder consumes 12 kHz mono audio from the radio chain and emits a PNG per completed page. It is a close sibling of the NOAA APT decoder — both are analog raster modes; APT amplitude-modulates its subcarrier where WEFAX frequency-modulates.
1. The exact numbers
| Parameter | Value | Code |
|---|---|---|
| Black / white tones | 1500 Hz / 2300 Hz | BLACK_HZ, WHITE_HZ |
| Brightness law | linear in instantaneous frequency between them, clipped 0–255 | _inst_bright |
| Sample rate | 12 000 Hz s16le mono | SR |
| Line rate | 120 LPM → 0.5 s/line | LPM, LINE_S |
| Index of cooperation | 576 → ⌊π·IOC⌋ = 1809 px/line | IOC, PIX_PER_LINE |
| Page bound | 1400 lines (~11.7 min) | MAX_LINES |
| Minimum save | 20 lines | save() |
(IOC is defined so that line length = π × IOC pixels — the classic drum-fax definition; 576 is the international marine standard, with 288 the half-resolution alternative not yet selectable here.)
2. Demodulation
Per audio block: convert s16le → float, take the analytic signal (Hilbert transform), unwrap its phase, differentiate — instantaneous frequency, the one-line FM discriminator of the maths guide (§1.1) — then map linearly to brightness:
\[b = 255 \cdot \mathrm{clip}\!\left(\frac{f_{\text{inst}} - 1500}{2300 - 1500},\ 0,\ 1\right)\]
Samples accumulate into a line buffer; every SR · 0.5 samples a line is closed, resampled by nearest-index decimation to exactly 1809 pixels, and appended to the page. A status callback reports line count every ~3 s.
3. Page assembly
save() (called at MAX_LINES or when the operator stops the decoder) stacks the byte rows into an (nlines, 1809) grayscale array, downscales to 800 px wide with Pillow for a sane file size, and writes rec-wefax-YYYYMMDD-HHMMSS.png into the recordings directory, invoking on_image(name).
4. Limitations (the honest list)
- Free-running line sync. Lines are cut purely by sample count at the nominal 0.5 s. There is no lock to the transmitted phasing lines or the start tone (the
START_TONE = 300constant is declared for the classic 675/300 Hz start/stop signalling but not yet acted on), so: a sound-card clock offset appears as image slant, and the left margin sits wherever reception began. Both are the standard first refinements of any fax decoder (correlate the phasing pulse per line, as APT does with its sync-A train). - No start/stop automation. Page boundaries are the 1400-line bound or a manual stop — the 300 Hz stop tone is not yet detected.
- Fixed parameters. 120 LPM / IOC 576 only; 60/90/240 LPM and IOC 288 variants exist in the wild and would be trivial parameterisations.
- No automated test. The module has no synthetic round-trip in the test suite (unique among the native decoders); validation is by on-air charts. A
synth → decode → comparetest in the APT style is the obvious gap.
Related: NOAA APT (the AM sibling, with the line-sync correlation this decoder still lacks), HamDRM (the digital successor for image traffic on HF), and the maths guide on instantaneous frequency.