Encode still images and video into NBTV (Narrow-Bandwidth Television) WAV signals
Project description
nbtvencoder
Encode still images and video into an NBTV (Narrow-Bandwidth Television) signal packaged as a WAV file — and emulate a televisor by decoding that signal back into a PNG or MP4.
NBTV is the modern continuation of John Logie Baird's 32-line mechanical television format. Playing the generated WAV into an NBTV televisor displays the picture. This encoder is known to work with the Mindsets televisor kit.
This is a Python/OpenCV (cv2) reimplementation of the original Perl proof of
concept, which is preserved in legacy/.
Demo
The alphabet A–Z, run through the encoder into an NBTV signal and then decoded back by the emulator — what a televisor would show. Reproduce it:
pip install "nbtvencoder[demo]"
python examples/alphabet_demo.py # writes alphabet.wav, alphabet.mp4, alphabet.apng
The full pipeline is in examples/alphabet_demo.py (or the copy/paste shell version, examples/alphabet_demo.sh):
# 1. generate the alphabet as test frames, then encode them to one NBTV signal
nbtvencoder letters/*.png -o alphabet.wav --frames 3 --seed 1
# 2. emulate the televisor: render the signal back to video
nbtv-emulate alphabet.wav -o alphabet.mp4 --scale 6
Install
pip install nbtvencoder
This pulls in numpy and opencv-python-headless. If you already use the full
opencv-python (with GUI support), that works too — the encoder only needs
OpenCV's decoding and image-processing routines.
Command-line usage
# Encode a still image, held for 10 seconds (the default):
nbtvencoder photo.jpg -o nbtv.wav
# Encode a video — frames are resampled to the NBTV frame rate (~12.7 fps):
nbtvencoder clip.mp4 -o nbtv.wav
# Hold a still for 30 seconds:
nbtvencoder photo.jpg -d 30 -o nbtv.wav
# Combine several inputs into one signal (a slideshow / playlist):
nbtvencoder intro.png clip.mp4 outro.png -o show.wav
# Reproducible output (fixes the dithering noise):
nbtvencoder photo.jpg --seed 1 -o nbtv.wav
Image vs. video is detected from the file extension; pass --video to force
every input through the video decoder. Run nbtvencoder --help for the full
option list (geometry, sample rate, channels, frame count, …).
You can also run it as a module:
python -m nbtvencoder photo.jpg -o nbtv.wav
Emulator (signal → PNG / MP4)
nbtv-emulate decodes an NBTV WAV the way a televisor would and renders the
picture to an image or a video. The output format is chosen from the extension:
# Render the first frame of a signal to a PNG still:
nbtv-emulate nbtv.wav -o frame.png
# Render the whole signal to an MP4 at the NBTV frame rate:
nbtv-emulate nbtv.wav -o out.mp4
# Pick a frame, and upscale the chunky 32-line picture more:
nbtv-emulate nbtv.wav -o frame.png --frame 10 --scale 12
The decoder separates the line/frame sync pulses, slices each scan line, maps
sample levels back to brightness, and undoes the scan geometry — so encoding an
image and emulating it back round-trips to a recognisable picture (at the
format's native 32×102 resolution). Run nbtv-emulate --help for all options.
Python API
from nbtvencoder import NBTVEncoder, NBTVDecoder, encode_to_wav, emulate_wav
# One-liner: encode any mix of images/videos straight to a WAV file.
encode_to_wav(["photo.jpg", "clip.mp4"], "nbtv.wav", seed=1)
# Emulate a televisor: decode the WAV back to a PNG still or an MP4.
emulate_wav("nbtv.wav", "frame.png")
emulate_wav("nbtv.wav", "out.mp4")
# Or work with the samples directly (a NumPy int16 array).
encoder = NBTVEncoder(seed=1)
samples = encoder.encode_still(cv2_image, duration=10.0) # still image
samples = encoder.encode_frames(frame_iterable) # any frame source
grids = NBTVDecoder().decode_grids(samples) # intensity frames back out
Key entry points:
| Function / class | Purpose |
|---|---|
NBTVEncoder |
Core encoder: encode_image_array, encode_still, encode_frames. |
NBTVDecoder |
Emulator: decode_grids, render_frame, decode_images. |
NBTVConfig |
Frozen dataclass of all timing/level parameters. |
encode_file / encode_files |
Encode file(s) to a sample array (auto-detects video). |
encode_to_wav |
Encode input(s) and write a WAV in one call. |
emulate_wav |
Decode a WAV and render a PNG still or a video. |
read_wav / write_wav |
Read/write a 16-bit PCM WAV as an int16 array. |
iter_video_frames |
Iterate a video's frames resampled to a target fps. |
How it works
For each frame the encoder emits a 16-bit PCM stream:
[ frame sync ][ line 0 dots ][ line sync ] ... [ line 31 dots ][ line sync ]
The image is converted to grayscale, flipped, rotated, and resized to
dots × lines (102 × 32 by default) — NBTV scans vertically, so each image row
becomes one scan line. Pixel intensity is inverted and scaled into the sample
range, with small dithering noise, exactly as the original encoder did. At
44.1 kHz this produces roughly 12.7 frames per second.
For video, the source is resampled to that frame rate so playback runs at real time; for a still image the single frame is repeated to fill the requested duration.
The emulator runs this in reverse: it detects the line-sync pulses (the
strongly negative end-of-line samples), groups them into frames using the wider
gap left by the frame sync, reads the dots samples before each sync as one
scan line, maps the levels back to brightness, and applies the inverse of the
scan geometry to produce an upright picture.
Development
git clone https://github.com/anarkiwi/nbtvencoder
cd nbtvencoder
pip install -e ".[dev]"
pytest # run the test suite
ruff check . # lint
ruff format . # format
CI runs the linter, the test suite across Python 3.9–3.13, and a package build on every push and pull request. Dependencies are kept current by Dependabot.
Releasing
Publishing a GitHub Release builds and uploads the package to PyPI via PyPI Trusted Publishing (OIDC — no stored tokens). See RELEASING.md for the one-time trusted publisher setup and the per-release steps.
License
MIT — see LICENSE.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file nbtvencoder-0.1.0.tar.gz.
File metadata
- Download URL: nbtvencoder-0.1.0.tar.gz
- Upload date:
- Size: 23.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
be2372db4c8475abf46599dca48aa80e79a2f1fddb1e47fb20af6f7e943f35a0
|
|
| MD5 |
76fca0bcc150f77f5973b454e32f6d14
|
|
| BLAKE2b-256 |
3707b1892a4dfe0319d510df34a1382898fffe16c3497fd2ddec85562980c458
|
Provenance
The following attestation bundles were made for nbtvencoder-0.1.0.tar.gz:
Publisher:
release.yml on anarkiwi/nbtvencoder
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
nbtvencoder-0.1.0.tar.gz -
Subject digest:
be2372db4c8475abf46599dca48aa80e79a2f1fddb1e47fb20af6f7e943f35a0 - Sigstore transparency entry: 1820040333
- Sigstore integration time:
-
Permalink:
anarkiwi/nbtvencoder@0cbdca29e39d8409271fe4541e2044b9237a05bc -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/anarkiwi
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@0cbdca29e39d8409271fe4541e2044b9237a05bc -
Trigger Event:
release
-
Statement type:
File details
Details for the file nbtvencoder-0.1.0-py3-none-any.whl.
File metadata
- Download URL: nbtvencoder-0.1.0-py3-none-any.whl
- Upload date:
- Size: 19.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
fc241e72f19500e2ec45b6e2a8eae019cefccb20770602a9c37a9fb347a40659
|
|
| MD5 |
4c0f149bb978ff40f6db571b236ace3a
|
|
| BLAKE2b-256 |
9251b2dc69f63b1834dac72f1a388a1cd9f9a40b87fe695df3ebaa50fe2eba19
|
Provenance
The following attestation bundles were made for nbtvencoder-0.1.0-py3-none-any.whl:
Publisher:
release.yml on anarkiwi/nbtvencoder
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
nbtvencoder-0.1.0-py3-none-any.whl -
Subject digest:
fc241e72f19500e2ec45b6e2a8eae019cefccb20770602a9c37a9fb347a40659 - Sigstore transparency entry: 1820040365
- Sigstore integration time:
-
Permalink:
anarkiwi/nbtvencoder@0cbdca29e39d8409271fe4541e2044b9237a05bc -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/anarkiwi
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@0cbdca29e39d8409271fe4541e2044b9237a05bc -
Trigger Event:
release
-
Statement type: