A Rust command-line tool that encodes standard images (JPEG/PNG) into NOAA APT (Automatic Picture Transmission) audio signals. Supports dual-channel encoding (separate images for Channel A and B) and camera capture mode for encoding webcam frames into APT signals.
Sample APT signal decoded with SatDump
- File Mode: Encode images to WAV files for testing and offline use
- Dual-Channel: Use separate images for Channel A (visible) and Channel B (infrared)
- Camera Capture Mode: Capture frames from webcam(s) and encode to WAV or stream to stdout
- Direct SDR Streaming: Pipe raw PCM directly to HackRF or other SDR tools with
--stdout - Continuous Capture: Timelapse-style continuous frame capture mode
- Graceful Shutdown: Ctrl+C saves the WAV file properly without corruption
- Standards Compliant: Output compatible with WXtoImg, noaa-apt, and other APT decoders
- Quick Start
- What is APT?
- History of APT
- Technical Deep Dive
- Installation
- Usage
- How This Encoder Works
- Signal Specifications
- Decoding the Output
- License
# Build with camera support
cargo build --release --features input-v4l
# Encode an image to WAV
./target/release/apt-encoder photo.jpg -o output.wav
# Capture from webcam to WAV (same camera for both channels)
./target/release/apt-encoder --capture --cam-b-idx 0 -o output.wav
# Stream webcam directly to HackRF at 433 MHz (requires gnuradio, gr-osmosdr)
./target/release/apt-encoder --capture --stdout --continuous --cam-b-idx 0 2>/dev/null | \
python3 fm_stream.py --freq 433000000 --gain 0APT (Automatic Picture Transmission) is an analog image transmission system developed in the 1960s for weather satellites. It allows the transmission of grayscale weather imagery as audio signals that can be received and decoded with relatively simple equipment.
APT is used by the NOAA POES (Polar Operational Environmental Satellites) series, specifically:
- NOAA-15 (137.620 MHz) - Launched 1998
- NOAA-18 (137.9125 MHz) - Launched 2005
- NOAA-19 (137.100 MHz) - Launched 2009
These satellites orbit Earth in sun-synchronous polar orbits at approximately 850 km altitude, completing an orbit every ~102 minutes. During a typical pass overhead, you can receive 10-15 minutes of imagery.
| Year | Milestone |
|---|---|
| 1960 | TIROS-1, first weather satellite, launched with primitive imaging |
| 1963 | APT system first proposed by NASA |
| 1964 | APT first demonstrated on TIROS-8 |
| 1966 | ESSA-1 and ESSA-2 launched with operational APT |
| 1970s | NOAA satellite series begins with improved APT |
| 1978 | TIROS-N introduces the modern APT format still used today |
| 1998 | NOAA-15 launched, oldest currently operational APT satellite |
| 2009 | NOAA-19 launched, the last satellite with APT capability |
| 2030s | APT expected to be phased out as NOAA POES satellites reach end-of-life |
APT transmits two simultaneous image channels:
- Channel A: Visible light (daytime) or 3.7 µm IR (nighttime)
- Channel B: 10.8 µm thermal infrared (always)
The signal is transmitted at 137 MHz VHF using FM modulation with a 2400 Hz subcarrier that is amplitude modulated with the image data.
┌──────────────────────────────────────────────────────────────────────┐
│ APT Signal Chain │
├──────────────────────────────────────────────────────────────────────┤
│ │
│ Image Data ──► AM Modulation ──► 2400 Hz ──► FM Modulation ──► RF │
│ (on subcarrier) Subcarrier (±17 kHz dev) 137MHz │
│ │
└──────────────────────────────────────────────────────────────────────┘
Each APT line takes exactly 0.5 seconds to transmit, resulting in 2 lines per second (2 LPS). Each line consists of 2080 "words" (pixels), giving a word rate of 4160 words per second.
◄──────────────────────────── 2080 words (0.5 seconds) ───────────────────────────────►
┌───────┬────────┬─────────────┬───────────┬───────┬────────┬─────────────┬───────────┐
│Sync A │Space A │ Image A │Telemetry A│Sync B │Space B │ Image B │Telemetry B│
│ 39 │ 47 │ 909 │ 45 │ 39 │ 47 │ 909 │ 45 │
└───────┴────────┴─────────────┴───────────┴───────┴────────┴─────────────┴───────────┘
│◄── Channel A (1040 words) ──►│ │◄── Channel B (1040 words) ──►│
Word Rate: 4160 words/second
Line Rate: 2 lines/second
Each frame (128 lines) contains calibration data in the telemetry sections:
Telemetry Frame Structure (16 wedges per frame):
Wedge │ Lines │ Function
───────┼────────┼──────────────────────────────
1-8 │ 1-64 │ Grayscale calibration (black to white)
9-10 │ 65-80 │ Thermistor temperature
11-12 │ 81-96 │ Patch temperature
13-16 │ 97-128 │ Channel ID wedge
- Rust 1.70 or later
- Cargo (comes with Rust)
# Clone the repository
git clone https://github.com/ktauchathuranga/apt-encoder.git
cd apt-encoder
# Build without camera support
cargo build --release
# Build with camera support (Linux only, requires V4L2)
cargo build --release --features input-v4l
# The binary will be at: ./target/release/apt-encodercargo test# Basic: encode single image (duplicated to both channels)
apt-encoder photo.jpg -o output.wav
# Dual-channel: separate images for Channel A and B
apt-encoder visible.png -o output.wav --input-b infrared.png
# Repeat the image 3 times for a longer transmission
apt-encoder photo.jpg -o output.wav --repeat 3
# With custom telemetry settings
apt-encoder photo.jpg -o output.wav \
--thermistor-temp 0.3 \
--patch-temp 0.7 \
--channel-a-id 1 \
--channel-b-id 4Capture frames from webcam(s) and encode to APT. Each frame is encoded line-by-line (like real NOAA satellites), producing decodable images.
Note: Requires building with camera support:
cargo build --release --features input-v4l
# Single frame capture (same camera for both channels)
apt-encoder --capture --cam-b-idx 0 -o output.wav
# Single frame with separate cameras
apt-encoder --capture --cam-a-idx 0 --cam-b-idx 1 -o output.wav
# Continuous capture (timelapse) - press Ctrl+C to stop
apt-encoder --capture --continuous --cam-b-idx 0 -o output.wavUse --stdout to output raw 16-bit signed PCM (48kHz) directly to stdout for piping to SDR tools.
Note: For proper FM transmission (like real NOAA satellites), use
fm_stream.pywhich applies WBFM modulation before transmission.
The included fm_stream.py script reads PCM audio, applies proper WBFM modulation (±17 kHz deviation), and transmits via HackRF:
# Install dependencies (if not already installed)
# Requires: gnuradio, gr-osmosdr
# Stream camera to HackRF at 433 MHz (minimum power, safe for testing)
apt-encoder --capture --stdout --continuous --cam-b-idx 0 2>/dev/null | \
python3 fm_stream.py --freq 433000000 --gain 0
# Single frame transmission
apt-encoder --capture --stdout --cam-b-idx 0 2>/dev/null | \
python3 fm_stream.py
# Custom frequency and gain
apt-encoder --capture --stdout --continuous --cam-b-idx 0 2>/dev/null | \
python3 fm_stream.py --freq 433500000 --gain 20fm_stream.py options:
| Flag | Description | Default |
|---|---|---|
--freq, -f |
Transmit frequency (Hz) | 433000000 |
--gain, -g |
TX gain (0-47 dB) | 0 |
--amp-on |
Enable 14dB amplifier | off |
--amp-off |
Disable amplifier | default |
A pre-configured flowgraph is included in the repository for transmitting WAV files:
# Open the included GNU Radio Companion project
gnuradio-companion gnu-radio-companion/apt-encoder.grcOr create your own flowgraph:
[Wav File Source] ► [WBFM Transmit] ► [Rational Resampler] ► [osmocom Sink]
Block settings:
- Wav File Source: Your output.wav, Repeat: Yes
- WBFM Transmit: Audio Rate=48000, Quad Rate=480000, Max Dev=17000, Tau=75e-6
- Rational Resampler: Type=Complex (Real Taps), Interpolation=25, Decimation=6
- osmocom Sink: Device=hackrf=0, Sample Rate=2000000, Frequency=433000000
Tip: Edit the WAV file path in the flowgraph to point to your generated
output.wavfile.
--cam-a-idx: Camera index for Channel A (default: 0)--cam-b-idx: Camera index for Channel B (default: 1)- Same index for both = shared mode (one camera, same image for both channels)
- Different indices = dual camera mode (separate cameras)
Find your camera indices with:
ls /dev/video*
# or
v4l2-ctl --list-devicesUSAGE:
apt-encoder [OPTIONS] [INPUT_IMAGE]
ARGUMENTS:
[INPUT_IMAGE] Input image file for Channel A (JPEG/PNG) - required for file mode
OPTIONS:
Output:
-o, --output <FILE> Output WAV file
Channel Options:
--input-b <FILE> Secondary image for Channel B (IR channel)
If not provided, Channel A is duplicated
Camera Capture Mode:
--capture Enable camera capture mode
--stdout Output raw PCM to stdout (for piping to SDR)
--continuous Continuously capture frames (Ctrl+C to stop)
--cam-a-idx <N> Camera index for Channel A [default: 0]
--cam-b-idx <N> Camera index for Channel B [default: 1]
Telemetry Options:
--thermistor-temp <F> Thermistor temperature (0.0-1.0) [default: 0.5]
--patch-temp <F> Patch temperature (0.0-1.0) [default: 0.5]
--channel-a-id <N> Channel A identifier (1-6) [default: 2]
--channel-b-id <N> Channel B identifier (1-6) [default: 4]
General Options:
-r, --repeat <N> Number of times to repeat the image [default: 1]
-h, --help Print help information
-V, --version Print version information
| Use Case | Command |
|---|---|
| Image to WAV | apt-encoder photo.jpg -o output.wav |
| Dual-channel | apt-encoder a.jpg -o out.wav --input-b b.jpg |
| Camera to WAV (single) | apt-encoder --capture --cam-b-idx 0 -o out.wav |
| Camera to WAV (continuous) | apt-encoder --capture --continuous --cam-b-idx 0 -o out.wav |
| Camera to HackRF | apt-encoder --capture --stdout --continuous --cam-b-idx 0 2>/dev/null | python3 fm_stream.py |
| Parameter | Value |
|---|---|
| Sample Rate | 48,000 Hz |
| Bit Depth | 16-bit |
| Channels | 1 (Mono) |
| Format | PCM WAV or raw PCM (with --stdout) |
| Parameter | Value |
|---|---|
| Subcarrier Frequency | 2400 Hz |
| Line Rate | 2 lines/second |
| Word Rate | 4160 words/second |
| Samples per Line | 24,000 |
| Sync A Frequency | 1040 Hz |
| Sync B Frequency | 832 Hz |
| Modulation Depth | 87% (0.05 - 0.92) |
noaa-apt is an open-source APT decoder.
noaa-apt output.wav -o decoded.pngWXtoImg is a classic APT decoder (Windows/Linux/macOS).
- Open WXtoImg
- File ► Open Audio File
- Select your generated WAV file
- The image should decode automatically
SatDump is a modern satellite data processor.
- noaa-apt - Open-source APT decoder in Rust
- rtl-sdr - Software-defined radio for receiving real APT
- SatDump - General purpose satellite data processing
Bringing 1960s space technology to your command line