Rust implementation of an SSTV (Slow Scan Television) audio encoder. This tool converts images to audio signals that can be transmitted over radio and decoded by SSTV receivers worldwide.
- 12 SSTV Modes: Martin 1/2, Scottie 1/2/DX, Robot 36/72, PD 120/180/290, BW 8/12
- Real-time Audio Playback: Stream directly to your sound card
- WAV File Export: Generate standard WAV files for later transmission
- Automatic Image Resizing: Lanczos3 resampling for optimal quality
- Sample Rate Independent: Works with 44.1kHz, 48kHz, or any sample rate
- Library + CLI: Use as a library in your projects or as a command-line tool
git clone https://github.com/ktauchathuranga/sstv.git
cd sstv
cargo build --releaseThe binary will be available at ./target/release/sstv
# Generate SSTV audio and play through speakers
sstv input.jpg
# Save to WAV file
sstv input.jpg output.wav
# Specify mode and sample rate
sstv input.jpg output.wav -m scottie1 -s 48000
# Save without playing
sstv input.jpg output.wav --no-play# Generate WAV files for all supported SSTV modes
for mode in martin1 martin2 scottie1 scottie2 scottiedx robot36 robot72 pd120 pd180 pd290 bw8 bw12; do
echo "Generating $mode..."
sstv input.jpg output_${mode}.wav -m $mode --no-play
doneUSAGE:
sstv [OPTIONS] <INPUT> [OUTPUT]
ARGS:
<INPUT> Input image file (PNG, JPEG, etc.)
[OUTPUT] Output WAV file (optional, plays audio if omitted)
OPTIONS:
-m, --mode <MODE> SSTV mode [default: martin1]
-s, --sample-rate <RATE> Audio sample rate [default: 48000]
--no-play Don't play audio
-h, --help Print help information
-V, --version Print version information
| Mode | VIS Code | Resolution | Time | Color Space |
|---|---|---|---|---|
martin1 |
0xAC | 320×256 | 114.3s | RGB (GBR) |
martin2 |
0x28 | 320×256 | 58.1s | RGB (GBR) |
scottie1 |
0x3C | 320×256 | 109.6s | RGB (GBR) |
scottie2 |
0xB8 | 320×256 | 71.1s | RGB (GBR) |
scottiedx |
0xCC | 320×256 | 268.9s | RGB (GBR) |
robot36 |
0x88 | 320×240 | 36.0s | YUV |
robot72 |
0x0C | 320×240 | 72.0s | YUV |
pd120 |
0x5F | 640×496 | 126.1s | YUV |
pd180 |
0x60 | 640×496 | 187.1s | YUV |
pd290 |
0xDE | 800×616 | 288.7s | YUV |
bw8 |
0x82 | 160×120 | 8.0s | Grayscale |
bw12 |
0x86 | 160×120 | 12.0s | Grayscale |
SSTV (Slow Scan Television) is a method of transmitting still images over radio using audio frequency modulation. Unlike fast-scan television (broadcast TV), SSTV transmits one frame over several seconds to minutes, allowing it to fit within the narrow bandwidth of amateur radio voice channels (approximately 3 kHz).
SSTV uses Frequency Modulation (FM) within the audio band to encode image brightness:
┌─────────────────────────────────────────────────────────────┐
│ SSTV Frequency Mapping │
├─────────────────────────────────────────────────────────────┤
│ Signal Type │ Frequency (Hz) │ Purpose │
├─────────────────────┼──────────────────┼────────────────────┤
│ Sync Pulse │ 1200 │ Line/frame sync │
│ Black Level │ 1500 │ Minimum brightness│
│ White Level │ 2300 │ Maximum brightness│
│ VIS Bit 1 │ 1100 │ Logic high │
│ VIS Bit 0 │ 1300 │ Logic low │
└─────────────────────┴──────────────────┴────────────────────┘
The relationship between pixel brightness and frequency is linear:
frequency = 1500 + (pixel_value × 800 / 255)
Where pixel_value ranges from 0 (black) to 255 (white).
Every SSTV transmission follows this structure:
┌──────────────────────────────────────────────────────────────────────────┐
│ SSTV Transmission Structure │
├──────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────┐ ┌─────────┐ ┌──────────────────────────────────────────┐ │
│ │Preamble │→ │VIS Code │→ │ Image Data │ │
│ │ ~1.4s │ │ ~0.3s │ │ (mode-dependent) │ │
│ └─────────┘ └─────────┘ └──────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────────────────┘
The VIS code identifies the SSTV mode being transmitted. It allows receivers to automatically detect and configure themselves for the incoming image format.
┌────────────────────────────────────────────────────────────────────────┐
│ VIS Code Structure │
├────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────┐ ┌─────────┐ ┌──────────┐ ┌─────────┐ ┌─────────────────┐ │
│ │ Leader │→│ Break │→│ Leader │→│Start Bit│→│ 8 Data Bits │ │
│ │ 300ms │ │ 10ms │ │ 300ms │ │ 30ms │ │ 30ms × 8 │ │
│ │ 1900 Hz │ │ 1200 Hz │ │ 1900 Hz │ │ 1200 Hz │ │ 1100/1300 Hz │ │
│ └──────────┘ └─────────┘ └──────────┘ └─────────┘ └─────────────────┘ │
│ │ │
│ ↓ │
│ ┌─────────┐ ┌─────────┐ │
│ │Stop Bit │←─────────────────────────────────────│ Parity │ │
│ │ 30ms │ │ (bit 7) │ │
│ │ 1200 Hz │ └─────────┘ │
│ └─────────┘ │
│ │
└────────────────────────────────────────────────────────────────────────┘
- Logic 1: 1100 Hz for 30ms
- Logic 0: 1300 Hz for 30ms
- Bit Order: LSB first
Example: Martin 1 (VIS = 0xAC = 10101100 binary)
Start → 0 → 0 → 1 → 1 → 0 → 1 → 0 → 1 → Stop
1300 1300 1100 1100 1300 1100 1300 1100
This encoder uses a phase accumulator approach for generating FM audio, ensuring sample-rate independence and smooth frequency transitions:
// Phase accumulator formula (from QSSTV synthes.cpp)
fn next_sample(&mut self, freq: f64) -> f64 {
let phase_increment = (freq / sample_rate) * SINE_TABLE_LEN;
self.phase = (self.phase + phase_increment) % SINE_TABLE_LEN;
self.sine_table[self.phase as usize]
}To maintain accurate timing across different sample rates, the encoder uses an adjustment accumulator:
// Duration adjustment for precise timing
let num_samples = ((duration + adjust) * sample_rate + 0.5) as usize;
adjust += duration - (num_samples as f64 / sample_rate);This ensures that timing errors don't accumulate over the transmission.
Add to your Cargo.toml:
[dependencies]
libsstv = { path = "path/to/libsstv" }use libsstv::{Encoder, ImageData, Mode, RgbPixel};
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Create test image (320x256 for Martin 1)
let pixels: Vec<RgbPixel> = (0..320*256)
.map(|i| RgbPixel::new((i % 256) as u8, 128, 64))
.collect();
let image = ImageData::new(320, 256, pixels);
// Create encoder for Martin 1 at 48kHz
let mut encoder = Encoder::new(Mode::Martin1, 48000)?;
// Encode to audio samples
let samples = encoder.encode(&image)?;
// Convert to i16 for WAV output
let i16_samples = Encoder::samples_to_i16(&samples);
println!("Generated {} samples", i16_samples.len());
Ok(())
}Generated WAV files can be decoded with:
- QSSTV (Linux) - The reference implementation
- MMSSTV (Windows) - Popular Windows decoder
- Robot36 (Android) - Mobile decoder app
- CQ SSTV (iOS) - iPhone/iPad decoder
MIT License - See LICENSE for details.
