Encode images into WEFAX (Weather Facsimile) HF radiofax audio WAV files, and decode WEFAX recordings back into images per WMO standards.
WEFAX is fundamentally different from telephone fax (Group 3 ITU-T T.4). While Group 3 fax uses DPSK (Differential Phase Shift Keying) modulation over telephone lines at 2400 bps with complex Huffman compression, WEFAX uses simple FM (Frequency Modulation) over HF radio with no compression—each pixel's value is directly encoded as a frequency.
- Encode: Convert images (PNG, JPEG, etc.) to WEFAX WAV files
- Decode: Recover images from WEFAX transmissions (recordings)
- Standard compliant: WMO standards, IOC 576/288, RPM 120/60
- FM modulation/demodulation: Continuous-phase FM with proper frequency mapping
- Auto-detection: Automatically detects and skips start/stop tones and phasing signal
- Flexible: CLI tools for integration into workflows or GUIs
- NOAA (National Oceanic and Atmospheric Administration) — Broadcasts synoptic weather charts on HF
- JMH (Japan Meteorological Agency / Kishōchō) — Japanese maritime weather charts
- DWD (Deutscher Wetterdienst) — German weather service
- Other national meteorological services — China, Russia, Australia, etc.
pip install numpy Pillow
cd scripts/python3 wefax_encode.py weather_map.png output.wav -vOptions:
--ioc 576(default) or--ioc 288— Resolution (Index of Cooperation)--rpm 120(default) or--rpm 60— Transmission speed--no-start-stop— Omit start/stop tones and phasing (image data only)-v/--verbose— Print progress
python3 wefax_decode.py recording.wav weather_map.png -vOptions:
--ioc 576(default) or--ioc 288— Must match encoding--rpm 120(default) or--rpm 60— Must match encoding--no-auto-detect— Don't auto-detect start/stop tones (use all audio)-v/--verbose— Print progress
A complete WEFAX transmission:
[300 Hz] [Black/White Bars] [FM Image Data] [450 Hz]
5 sec 30 sec variable duration 5 sec
(Start) (Phasing signal) (Scan lines) (Stop)
FM Modulation:
Frequency (Hz) = 1500 + (PixelValue / 255) × 800
Black (0) → 1500 Hz
Gray (127) → 1900 Hz (center)
White (255) → 2300 Hz
| Parameter | Standard | Alternative |
|---|---|---|
| IOC | 576 (1809 px/line) | 288 (904 px/line) |
| RPM | 120 | 60 |
| Line Period | 0.5 sec | 1.0 sec |
| Black Freq | 1500 Hz | — |
| Center Freq | 1900 Hz | — |
| White Freq | 2300 Hz | — |
| FM Deviation | ±400 Hz | — |
| Start Tone | 300 Hz (5 sec) | — |
| Stop Tone | 450 Hz (5 sec) | — |
| Phasing | 30 sec (black/white) | — |
| Sample Rate | 44,100 Hz | — |
# Standard settings (IOC 576, 120 RPM)
python3 wefax_encode.py weather_map.png output.wav
# Low resolution (half the detail, shorter transmission)
python3 wefax_encode.py weather_map.png output_lowres.wav --ioc 288
# Slow transmission (better for weak signals)
python3 wefax_encode.py weather_map.png output_slow.wav --rpm 60
# Just the image data, no tones
python3 wefax_encode.py weather_map.png output_noheader.wav --no-start-stop# Standard settings
python3 wefax_decode.py recording.wav decoded_map.png
# With full debugging
python3 wefax_decode.py recording.wav decoded_map.png -v
# Don't auto-detect tones (use all audio as image)
python3 wefax_decode.py recording.wav decoded_map.png --no-auto-detect
# Match encoding parameters
python3 wefax_decode.py recording_lowres.wav map.png --ioc 288
python3 wefax_decode.py recording_slow.wav map.png --rpm 60# Encode all PNGs in a folder
for img in weather/*.png; do
python3 wefax_encode.py "$img" "${img%.png}.wav"
done
# Decode all WAV files
for wav in broadcasts/*.wav; do
python3 wefax_decode.py "$wav" "${wav%.wav}.png" -v
done- Load image (any PIL format) and convert to grayscale (0–255)
- Resize to IOC-appropriate width:
- IOC 576 @ 120 RPM: 1809 pixels wide
- IOC 288 @ 120 RPM: 904 pixels wide
- Generate audio stream:
- Start tone: Pure 300 Hz sine, 5 seconds
- Phasing signal: Repeating black/white pattern, ~30 seconds
- Image data: FM-modulate each pixel to a frequency, one line per 0.5 seconds
- Stop tone: Pure 450 Hz sine, 5 seconds
- Write 16-bit mono WAV file at 44,100 Hz sample rate
FM Modulation Detail:
- Each pixel value is mapped linearly to a frequency (0→1500 Hz, 255→2300 Hz)
- The modulation maintains continuous phase (no jumps), simulating real FM radio
- Phase is integrated from instantaneous frequency:
phase(n) = 2π ∑ freq[i] / fs
- Read WAV file (automatically resamples to 44,100 Hz if needed)
- Auto-detect transmission boundaries (optional):
- Locate start tone (300 Hz) and skip
- Skip phasing signal (~30 sec)
- Locate stop tone (450 Hz) and truncate
- FM demodulate to recover instantaneous frequency:
- Use analytic signal (Hilbert transform) to extract phase
- Compute phase derivative:
freq[n] = phase'[n] × fs / (2π) - Low-pass filter to reduce noise
- Downsample to pixel rate
- Map frequencies back to pixel values:
pixel = (freq − 1500) / 800 × 255 - Reshape into image and save as PNG
- numpy — Signal processing, numeric arrays
- scipy — Scientific computing (Hilbert transform for FM demodulation)
- Pillow (PIL) — Image loading/saving
- wave — Standard library WAV file I/O (no install needed)
Install:
pip install numpy scipy Pillow| Aspect | WEFAX | Group 3 Fax | APT | SSTV |
|---|---|---|---|---|
| Domain | HF Radio | Telephone | Satellites | Amateur Radio |
| Modulation | FM | DPSK | AM | FM |
| Compression | None | Huffman (Modified) | None | Various |
| Resolution | 1809×N px | 1728×2376 px | 480×2400 px | Mode-dependent |
| Speed | ~0.5 sec/line | ~3 sec/page | 12.5 min (full pass) | ~3 min (Martin M1) |
| Use Case | Weather charts | Office documents | Satellite imagery | Amateur images |
- WMO 49 (Vol. II.1) — Manual on Codes (Radiofax services)
- ITU-R Rec. 625 — Analogue picture transmission standard
- NOAA radiofax schedules — https://www.nws.noaa.gov/os/marine/radiofax.pdf
- Single-image only — WEFAX transmits one image per transmission (no multiple images per WAV)
- Grayscale only — WEFAX is inherently monochrome
- Line synchronization — Assumes perfect timing; very long lines may drift
- Noise sensitivity — FM demodulation requires reasonable SNR; very noisy recordings may decode poorly
- Frequency drift — Real HF transmissions may drift slightly; decoder assumes stable frequency
A roundtrip test is included in wefax_common.py:
python3 wefax_common.pyThis tests:
- FM modulation and demodulation
- Pixel recovery accuracy
- Configuration parsing
Example output:
Roundtrip test results:
Test pixels: 1809
MSE: 45.23 (0.07%)
Max error: 3 levels
PASS
wefax/
├── scripts/
│ ├── wefax_common.py # Shared constants, FM modulation/demodulation, utilities
│ ├── wefax_encode.py # CLI encoder: image → WAV
│ └── wefax_decode.py # CLI decoder: WAV → image
├── SKILL.md # Skill description (for Claude integration)
└── README.md # This file
Q: Decoded image is all black or all white
- The FM demodulation may not be finding the right frequency range
- Verify the audio sample rate (should be 44,100 Hz)
- Check IOC and RPM settings
- Try
--no-auto-detectto skip tone detection
Q: Image is stretched or wrong dimensions
- IOC determines width; height is determined by how many lines of audio you have
- If you want a specific aspect ratio, pre-crop your source image
Q: Audio playback sounds like "wavy" tones
- That's correct! FM of 1500–2300 Hz will sound like a descending or ascending tone as the frequency changes
- If you play the audio on an SSB (single-sideband) radio, it will demodulate back to the image
Q: Decoding is slow
- Frequency estimation involves Fourier transforms for each window
- A 30-second transmission might take 10–30 seconds to decode
- This is normal and depends on your CPU
MIT License — Feel free to use, modify, and distribute.
- APT Codec — Decode NOAA satellite pictures (APT format)
- SSTV Codec — Amateur radio image transmission (Martin/Scottie/Robot modes)
- RTTY Codec — Radioteletype text (Baudot/ITA2)
- Data Modem — FSK binary file transmission
- Fax Codec — ITU-T T.4 telephone fax (Group 3)
See SKILL.md for detailed usage, encoding/decoding steps, and technical details.