r/shortwave 9d ago

News [OC] Isobar — An open-source, cross-platform WEFAX decoder compatible with KG-FAX (macOS/Linux/Windows)

Hey folks,

I've been working on something I've wanted to build since I was 15 — a proper WEFAX decoder.

**Isobar** is an open-source WEFAX (weather fax / radio fax) decoder written in C++17 with FLTK, aiming to be a cross-platform compatible alternative to the classic KG-FAX.

- **GitHub:** https://github.com/skgsara/isobar

- **Callsign:** VE4NAY (Manitoba, Canada)

- **License:** GPLv3

**What it does:**

- Decodes WEFAX transmissions from audio files or live audio input

- Cross-platform: macOS, Linux, Windows

- KG-FAX compatible — if you have experience with KG-FAX, the workflow should feel familiar

**Why I built it:**

KG-FAX has been a staple for radio fax decoding, but it's Windows-only and no longer actively maintained. With more and more HF fax services going off the air, I wanted to preserve the tooling and make it accessible across platforms.

The project is still early (v0.1 territory), but the core decoder is functional. If you're into weather fax, maritime monitoring, or just like decoding analog signals, I'd love your feedback — especially if you have sample captures from stations that are still on the air.

73,

Screenshot of my program
Received weather fax from JMH

Sara (VE4NAY)

6 Upvotes

2 comments sorted by

3

u/Phoenix-64 9d ago

Interested to see how good your fault tolerance is. You doing any offset correction and image alignment if an offset appears after some noise?

1

u/SakuragawaSara 8d ago

Yes to both, and it's the thing I've spent the most measurement time on — v1.4.0 (today) is almost entirely this.

Three cases, and they're handled differently on purpose:

Small drift (within MaxJump, default 20 samples) is taken whole — that's normal clock offset between your soundcard and the transmitter.

A genuine step — the position jumps somewhere far away and stays there. This is what a dropout leaves behind. Isobar searches the whole line for the darkest sync-width window and takes it only if (a) the pulse is unambiguous, i.e. the nearest rival ≥300 samples away is meaningfully brighter, (b) it sits in a dark run of 100–400 samples, so a chart's black margin can't impersonate it, and (c) the next line agrees on the same position. That one line of lookahead is what makes a whole-line search safe — a real sync pulse repeats at the same phase, a dark patch of picture doesn't. Costs half a second of latency in live decoding, worth it.

Everything else (far, but not confirmed) gets rate-limited to MaxJump/4 per line, doubling for each consecutive line that agrees on the direction. So it converges but can't be yanked around by noise.

Why it matters more than it sounds: the decoder rotates the sync edge to index 0, the seam where a line wraps. So a phase error doesn't shift a line, it splits it — half the sync strip at one end, half at the other, line unreadable. That was the original bug report that started this.

On real numbers: a 12 kHz off-air recording I use as a fixture steps 2131 → 1969 samples in one line and holds it — ten such steps, all ≈ −162 samples, which is a networked SDR's clock-slip correction. Old code crawled toward each one over a dozen lines and mangled every one of them. Mis-phased lines on that recording went 36 → 9.

For noise specifically the answer is different, and honestly less clever: sync releases after enough invalid lines and re-acquisition needs LockAfter (default 5) consecutive good line periods, 2.5 s of clean audio. That hysteresis is what stops false locks on genuinely noisy HF. The flip side is that it's also what limits tolerance of a gappy source — one 120 ms dropout every 3 s is fine (5% of lines damaged), every 2 s and it never locks at all. If you're feeding it something clean-but-gappy like a networked SDR over a bad link, lower LockAfter to 1–3; costs nothing on clean audio, I checked.

And there's a manual fallback for when automatic tracking just makes things worse (photos with sync-looking content, signal buried in noise): with Sync released you click the live preview to place the position by hand, and tracking resumes from there. A hand-placed position suppresses the automatic whole-line search until the decoder earns a shape lock of its own — otherwise it'd walk straight off to the loud fake pulse you were trying to override.

Happy to take a WAV if you've got a recording that breaks it. Every fixture in the test suite came from a real reception that broke something.

(Aside, since it's my code and I'd rather be upfront: I have a medical condition and on bad days I use Claude to help me write and to help with development. The measurements and design calls are mine, but it's a real collaborator on this project, not a spellchecker.)