The other day I found myself getting up unusually early to attend a ham radio market. 50% off everything really makes me do the most unlikely things. the haul ended up being 3 “METEOSAT Wefax” decoders and a Cisco 2950.
Now, 2 of those decoders ended up being broken; in fact one of them was completely empty. One of them though, a GRUNDIG MST 100 (Manual included!), was still fully functional it seems. It turned on and showed what seems to have been a corrupted image, probably due to the volatile memory used for storing pictures.

The modem has an RF input for the downconverted bands of the two METEOSAT channels (137.2-137.8 MHz, 140.7-141.3 MHz) as well as an audio input and output for recording/replaying images from tape. Images are directly displayed from either an AV or CVBS port.
I had an old, very long recording of METEOSAT 7 wefax I had found somewhere a long time ago and played that into the tape input of the modem and it worked perfectly:

Now, as readers may or may not know, this format of WEFAX is not used anywhere in the world anymore. The closest relatives of this still in use would be NOAA-APT on Noaa 15 and 19 and HF-Weatherfax for marine use, though both have some differences that make them incompatible with this modem.
So, with some help from our AI overlords I made an encoder that turns any 800×800 image into a METEOSAT Wefax transmission (without the final FM modulation). After a bunch of bugfixes, here’s the result

Apart from the image being mirrored, pretty good I’d say. This could enable using this modem as some vintage looking display. if you attached a microphone to it, it could allow for image transmission through soundwaves maybe. Here’s the audio (volume warning!):
And the code to generate this in python:
import numpy as np
import wave
from PIL import Image
# Constants
LINE_DURATION = 0.25 # 250 ms
SYNC_PIXELS = 28 # 14 white + 14 black
IMAGE_WIDTH = 800
SAMPLE_RATE = 11025 # Audio sample rate in Hz
SUBCARRIER_FREQ = 2400 # Hz
MOD_DEPTH = 0.8 # 80% modulation
def generate_sync_line():
"""Generates a sync pulse: alternating 2 white and 2 black pixels, 7 times, with 2 leading and 10 trailing white pixels."""
pattern = np.array([255] * 2 + [0] * 2, dtype=np.uint8)
sync_core = np.tile(pattern, 7)
sync_line = np.concatenate(([255] * 2, sync_core, [255] * 10))
return sync_line
def amplitude_modulate(pixels, sample_rate, duration, freq, mod_depth):
"""AM modulate a signal based on pixel brightness."""
num_samples = int(sample_rate * duration)
t = np.linspace(0, duration, num_samples, endpoint=False)
signal = np.interp(
np.linspace(0, len(pixels), num_samples, endpoint=False),
np.arange(len(pixels)),
pixels / 255.0
)
carrier = np.sin(2 * np.pi * freq * t)
modulated = (1 + mod_depth * (signal - 0.5) * 2) * carrier
return modulated
def encode_ascii_header(text):
"""Encodes 50-character ASCII text into a WEFAX header line."""
text = text.ljust(50)[:50] # Ensure exactly 50 characters
bits = ''.join(f'{ord(c):08b}' for c in text)
pixels = [255 if bit == '1' else 0 for bit in bits for _ in range(2)] # 2 pixels per bit
return np.array(pixels, dtype=np.uint8)
def encode_image_to_wefax(image_path, output_wave, header_text="MET5 IR 15 MAY 03 1215 VIS01"):
image = Image.open(image_path).convert('L')
image = image.resize((IMAGE_WIDTH, IMAGE_WIDTH))
img_array = np.array(image, dtype=np.uint8)
audio = []
# Add 3s of alternating black/white pixels at 300Hz modulating the carrier
num_cycles = int(3 * 300) # 300 Hz for 3 seconds
samples_per_half_cycle = int(SAMPLE_RATE / (300 * 2))
pattern = np.array([0, 255] * num_cycles, dtype=np.uint8)
pattern = np.repeat(pattern, samples_per_half_cycle // 2)
start_signal = amplitude_modulate(pattern, SAMPLE_RATE, 3, SUBCARRIER_FREQ, MOD_DEPTH)
audio.extend(start_signal)
# Add 5s phasing signal (each 250ms: 12.5ms black + 237.5ms white)
for _ in range(20):
black_duration = int(IMAGE_WIDTH * (12.5 / 250.0))
white_duration = IMAGE_WIDTH - black_duration
blk_black = np.zeros(black_duration, dtype=np.uint8)
blk_white = np.ones(white_duration, dtype=np.uint8) * 255
blk = np.concatenate((blk_black, blk_white))
phasing = amplitude_modulate(blk, SAMPLE_RATE, LINE_DURATION, SUBCARRIER_FREQ, MOD_DEPTH)
audio.extend(phasing)
# Encode 2 header lines
header_pixels = encode_ascii_header(header_text)
for _ in range(2):
sync = generate_sync_line()
line = np.concatenate((sync, header_pixels))
modulated = amplitude_modulate(line, SAMPLE_RATE, LINE_DURATION, SUBCARRIER_FREQ, MOD_DEPTH)
audio.extend(modulated)
# Encode image lines in reverse (bottom-up)
for row in reversed(img_array):
sync = generate_sync_line()
line = np.concatenate((sync, row))
modulated = amplitude_modulate(line, SAMPLE_RATE, LINE_DURATION, SUBCARRIER_FREQ, MOD_DEPTH)
audio.extend(modulated)
# Add 5s of alternating black/white pixels at 450Hz modulating the carrier (end tone)
num_cycles = int(5 * 450) # 450 Hz for 5 seconds
samples_per_half_cycle = int(SAMPLE_RATE / (450 * 2))
pattern = np.array([0, 255] * num_cycles, dtype=np.uint8)
pattern = np.repeat(pattern, samples_per_half_cycle // 2)
end_signal = amplitude_modulate(pattern, SAMPLE_RATE, 5, SUBCARRIER_FREQ, MOD_DEPTH)
audio.extend(end_signal)
audio_np = np.array(audio)
audio_pcm = np.int16(audio_np / np.max(np.abs(audio_np)) * 32767)
with wave.open(output_wave, 'w') as f:
f.setnchannels(1)
f.setsampwidth(2)
f.setframerate(SAMPLE_RATE)
f.writeframes(audio_pcm.tobytes())
encode_image_to_wefax("example.png", "output.wav", header_text="MET5 IR 15 MAY 03 1215 VIS01")
Wow this is so cool mr. Cho