Skip to content

Latest commit

 

History

2 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

sstv-decoder

🇬🇧 English | 🇹🇷 Türkçe

A pure-Python SSTV decoder that turns a WAV recording from the QO-100 satellite into a picture — and teaches you the SSTV format so thoroughly you could design your own mode by the end.

No hardware, no MMSSTV, no black boxes: numpy, scipy, and a WebSDR recording are all you need.

Audio (WAV) ──► band-pass + Hilbert ──► instantaneous-frequency curve
            ──► leader detect (1900 Hz) + frequency-offset estimate
            ──► VIS code (mode ID, parity checked)
            ──► line-sync fit (linear regression, slant correction)
            ──► pixel sampling (1500 Hz = black … 2300 Hz = white) ──► PNG

Contents

  1. What is SSTV?
  2. SSTV on QO-100
  3. How to record (WebSDR, step by step)
  4. Usage and outputs
  5. The SSTV format — deep dive
  6. How the decoder works
  7. Files, supported modes, limitations
  8. References

1. What is SSTV?

SSTV (Slow-Scan Television) is a way of sending still pictures over a plain voice radio channel. It dates back to 1958 — ham radio operators wanted to exchange images long before the internet, using nothing but a 3 kHz audio channel. NASA even used a variant to send back pictures from early Moon missions.

The core idea is beautifully simple:

Brightness becomes pitch. A dark pixel is transmitted as a low audio tone, a bright pixel as a high tone. The transmitter scans the image line by line, turning each line into a little melody; the receiver listens and paints what it hears.

The standard frequency mapping used by every common mode:

1200 Hz   sync pulse ("start a new line")
1500 Hz   black
  ...     greys / colour intensities in between (linear)
2300 Hz   white
1900 Hz   calibration leader tone (header)

That's why SSTV has that unmistakable "R2-D2 singing" sound: you are literally hearing the picture. One image takes anywhere from 8 seconds to 2 minutes depending on the mode — hence slow-scan.

Because the entire signal lives inside an ordinary audio channel, any receiver that produces audio can feed a decoder — including a WebSDR in your browser. That's what this project exploits.

2. SSTV on QO-100

QO-100 (Es'hail-2) is the first geostationary amateur-radio satellite; its narrow-band transponder relays signals from half the planet 24/7 (see our sister project qo100-beacon-decoder for the full introduction).

The band plan reserves an image segment around 10489.620–10489.640 MHz (downlink) for SSTV and similar picture modes. Activity is sporadic — an operator somewhere between Brazil and Thailand decides to send a picture and anyone pointing at the satellite (or at a WebSDR) can receive it.

On a waterfall an SSTV transmission is easy to spot: a single trace that wobbles rhythmically inside a ~1 kHz corridor for one or two minutes — each wobble cycle is one image line.

3. How to record

3.1 Via WebSDR (no antenna needed)

Connect to a QO-100 WebSDR, for example http://websdr.is0grb.it:8901/ (IS0GRB, Sardinia), and:

Setting Value Why
Frequency around 10489620.00 kHz (tune ±10 kHz across the image segment) SSTV lives in 10489.620–640; tune so the signal's tones land in 500–2800 Hz audio
Mode USB (USB2.7 or the dedicated SSTV filter) SSTV is transmitted as USB audio
DSP Noise Reduction Disabled it warps the tone frequencies — the decoder reads frequency, don't touch it
Autonotch Off SSTV tones look like carriers to it; it would eat the picture
Squelch / Mute / High Boost Off nothing may gate or shape the audio
Audio AGC Auto fine
Volume high, no clipping recording SNR

Screenshots of a real recording session (the wobbling trace on the waterfall is the picture being painted, line by line):

WebSDR SSTV recording — 1

WebSDR SSTV recording — 2

Then: wait until you actually see a transmission on the waterfall (SSTV is not continuous!), press Audio recording: start, let the whole transmission pass (Martin M1 takes ~114 s), press stop, download the WAV.

Don't worry about tuning precision: the decoder measures the frequency offset from the header and corrects for it automatically (our sample was off by −109.4 Hz and decoded perfectly).

3.2 Any other SSTV WAV

The decoder doesn't care where the audio came from. A shortwave receiver on 14.230 MHz (the classic HF SSTV frequency), an ISS pass, a phone recording of another radio's speaker, or a WAV generated by an SSTV encoder app — if it contains an SSTV transmission with its header, it will decode. Mono WAV, any sample rate ≥ 8 kHz.

4. Usage and outputs

pip3 install numpy scipy matplotlib
python3 sstv_decoder.py misc/sstv_sample.wav

Real output from the included sample (recorded from QO-100 via IS0GRB):

kayit: 158.7s @ 8000 Hz
t=34.0s: lider bulundu, ofset -109.4 Hz, VIS=44 -> Martin M1
  256/256 satir sync ile hizalandi; 256 satir cozuldu

kayit : misc/sstv_sample.wav
sure  : 158.7 s @ 8000 Hz
yayin : 1 baslik bulundu, 1 resim cozuldu

-- yayin @ t=34.0s --
  frekans ofseti : -109.4 Hz
  VIS            : 44 (parite OK) -> Martin M1
  boyut          : 320x256, renk GBR
  satir suresi   : nominal 446.446 ms, olculen 446.446 ms (egiklik -0.000%)
  hizalama       : 256/256 satir sync ile hizalandi
  video araligi  : t=34.9 - 149.2 s
  cikti          : data/sstv_sample_sstv.png
  frekans analizi: data/sstv_sample_freq_analysis.png

uretilen dosyalar:
  data/sstv_sample_meta.txt
  data/sstv_sample_sstv.png
  data/sstv_sample_freq_analysis.png

Every run writes three files under data/, named after the input WAV:

File Content
data/<name>_meta.txt everything the console printed: offset, VIS, mode, slant, timings
data/<name>_sstv.png the decoded picture
data/<name>_freq_analysis.png spectrogram with leader/VIS/video boundaries and the offset-corrected reference tones marked

(If one WAV contains several transmissions, outputs are numbered _sstv_01.png, _sstv_02.png, …)

The decoded picture from the sample — received from a satellite 36,000 km up, through a browser:

Decoded SSTV image

And its frequency analysis — you can literally see the anatomy of the transmission (leader, VIS, then 256 lines of video between the black and white reference lines):

Frequency analysis

5. The SSTV format

This section is the heart of the README. Read it to the end and you will know the format well enough to invent a mode of your own.

5.1 One curve carries everything

Forget bits and packets. Analog SSTV is a single continuous curve: instantaneous frequency over time. Every feature of the transmission — header, mode ID, line syncs, every single pixel — is a shape on that curve. Decoding SSTV = reading that curve.

5.2 The header: leader + VIS

Every transmission starts with a fixed overture so receivers can find it and identify the mode:

freq
2300 ┤
1900 ┤ ████████████ ██ ████████████                          ← leader tones
1500 ┤
1300 ┤                              ▄▄  ▄▄     ▄▄            ← VIS "0" bits
1200 ┤             ▓▓             ▓▓                    ▓▓   ← break, start, stop
1100 ┤                                    ▄▄▄▄               ← VIS "1" bits
     └──────────────────────────────────────────────────────► time
       300 ms      10    300        30 ms per bit
       leader     break  leader     VIS: start + 7 data + parity + stop
  • Leader: 1900 Hz held for ~300 ms, a 10 ms break at 1200 Hz, then another ~300 ms of 1900 Hz. Purpose: "an SSTV picture is coming", and — crucially for us — a frequency reference. If the receiver is mistuned by X Hz, the leader appears at 1900+X; measuring it gives us X, and every later tone can be corrected by it.
  • VIS code (Vertical Interval Signaling): a 1200 Hz start bit, then 7 data bits, LSB first (1100 Hz = 1, 1300 Hz = 0, 30 ms each), one even-parity bit, and a 1200 Hz stop bit. The 7-bit value identifies the mode: 44 = Martin M1, 40 = Martin M2, 60 = Scottie S1, 76 = Scottie DX, 95 = PD120, 8 = Robot36

The VIS is the only "digital" thing in the whole transmission — 8 bits of 1970s-style FSK, and even it comes with a parity check.

5.3 The line: sync + colour scans

After the header the image flows as nlines identical lines. Here is one Martin M1 line (VIS 44 — the mode in our sample, and the most popular on QO-100):

│sync │ G channel scan      │ B channel scan      │ R channel scan      │
│1200 │ 146.432 ms          │ 146.432 ms          │ 146.432 ms          │
│4.862│ 1500–2300 Hz        │ 1500–2300 Hz        │ 1500–2300 Hz        │
└─────┴─────────────────────┴─────────────────────┴─────────────────────┘
◄──────────────────────── 446.446 ms per line ────────────────────────►
                        × 256 lines = 114.3 s per image
  • The sync pulse (1200 Hz, below black — unmistakable) marks the start of each line. It exists so the receiver can re-align 256 times per image; without it, a clock error of 0.01% would shear the picture into a slant.
  • Each colour scan sweeps the line's pixels for that colour channel: during the G scan, the instantaneous frequency at time t IS the green value of the pixel at position x = t/146.432ms × width.
  • Note the order: Martin (and Scottie) transmit G, then B, then R — GBR, not RGB. Green first because eyes are most sensitive to it.

5.4 Where do resolution and colour come from?

Two facts that surprise everyone:

Horizontal resolution is not in the signal. The scan is continuous — an analog sweep, like a record groove. "320 pixels" is just the convention for how finely receivers sample the sweep. Our decoder chops each scan into 320 slices and averages each slice; a decoder that sliced it into 640 would get a (softer) 640-pixel-wide image from the same signal. Vertical resolution IS fixed by the mode: 256 transmitted lines is 256 lines, full stop.

Colour is a convention layered on the same grey mechanism. The signal only ever says "this bright, now this bright…". Modes agree on how to interpret consecutive scans:

  • Martin/Scottie (GBR sequential): 3 scans per line = the G, B, R planes of that line.
  • PD family (YCrCb): 4 scans per transmitted line — Y0, Cr, Cb, Y1 — carrying two image lines at once: Y0 and Y1 are the brightness of two consecutive lines, Cr/Cb their shared colour information (exactly the chroma subsampling trick JPEG and every video codec use — invented here out of the same need: brightness matters more than colour, so spend airtime on it).
  • Scottie's quirk: the sync pulse sits in the middle of the line (before the R scan), not at the start — G and B are transmitted before their own line's sync. Every decoder has a special case for this; ours handles it with a single sync_pos_ms parameter.

5.5 Design your own mode (you can, now)

An SSTV mode is completely defined by a handful of numbers — look at the MODES table at the top of sstv_decoder.py and you'll recognise every column:

1. Pick a free VIS code            → say, 57
2. Line time                       → say, 300 ms
3. Sync: 1200 Hz, 5 ms, at line start
4. Scans: G at 6 ms (94 ms), B at 103 ms (94 ms), R at 200 ms (94 ms)
5. Lines: 240
   => a 320x240 colour image in 72 s. Congratulations: "Levent L1".

Add one dict entry to MODES and this decoder would decode it. That's the entire spec surface of an SSTV mode — timing plus interpretation, riding on three fixed tones (1200/1500/2300) and a shared header. This is also exactly how the real modes proliferated: Martin Emmerson (G3OQD) tweaked timings and made the Martin family; Eddie Murphy (GM3SBC) made Scottie; each got a VIS code and the ecosystem absorbed them.

6. How the decoder works

The chain in sstv_decoder.py, layer by layer:

  1. Instantaneous frequency (inst_freq): band-pass 850–2700 Hz, then the Hilbert transform turns the real audio into an analytic signal whose phase derivative is the momentary frequency. A 7-tap median filter kills single-sample glitches. From here on, the decoder never touches audio again — only this curve.
  2. Header search (find_header): scan for ≥200 ms of stable tone near 1900 Hz (the leader) → the deviation from 1900.0 is the frequency offset; then find the VIS start bit (a ≥20 ms dip to ~1200 Hz within the next second), sample the 8 bit centres, check parity, look the value up in MODES.
  3. Line-sync fit (fit_sync): for each expected line, hunt the 1200 Hz dip near its predicted position, then fit a straight line sync_center = a + b·line_number by linear regression (two passes with outlier rejection). The slope b is the measured line period; its deviation from nominal is the slant that plagues analog SSTV — here it is measured and cancelled exactly. Our sample: 256/256 syncs found, slant −0.000%.
  4. Pixel sampling (decode_image): for every line and colour scan, average the frequency curve over each pixel's time slice (done in O(1) per pixel with a cumulative sum), then map 1500 Hz → 0, 2300 Hz → 255 (offset-corrected). PD modes additionally convert YCrCb → RGB (BT.601 coefficients).
  5. Reporting: meta text, the PNG, and a spectrogram with every detected landmark drawn on it — so you can see what the decoder saw.

Health indicators to read in the output: frequency offset (dial error — harmless, corrected), parity OK (VIS read correctly), N/256 lines sync-aligned (how much of the sync train was found), slant % (clock mismatch — measured and corrected).

7. Files and modes

File Role
sstv_decoder.py the whole decoder (single file, ~460 lines, numpy/scipy only)
misc/sstv_sample.wav, misc/sstv_sample_2.wav real QO-100 recordings (IS0GRB WebSDR)
misc/websdr_sstv_01.png, misc/websdr_sstv_02.png recording-session screenshots
data/ outputs: <name>_meta.txt, <name>_sstv.png, <name>_freq_analysis.png

Supported modes: Martin M1/M2, Scottie S1/S2/DX, PD50/PD90/PD120. Recognised but not yet decoded: Robot36/72 (their line interleaves luma and chroma differently — a nice future exercise). Unknown VIS codes are reported and skipped.

8. References

About

Pure-Python SSTV decoder — turns WAV audio (e.g. recorded from the QO-100 satellite via WebSDR) into images: leader/VIS detection, frequency-offset and slant correction, Martin/Scottie/PD modes. numpy/scipy only.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages