c64cast turns a real Commodore 64 — driven over the network through an
Ultimate 64 or
TeensyROM+ — into a programmable
display and audio device. It runs a playlist of scenes on the real
hardware: play videos and images, stream a live webcam, visualize SID music
on a 3-voice oscilloscope, synthesize a MIDI keyboard or an ASID stream
through the real SID chip, render reactive generative visuals, or hand the
machine over to a native game or demo. Frames from any source are quantized
in real time to a VIC-II display mode (PETSCII, MCM, hi-res bitmap, multicolor
hi-res); audio plays through the SID's $D418 DAC or the hi-fi Ultimate Audio
PCM sampler. Stackable overlays decorate any scene with scrolling text,
spectrum analyzers, clocks, weather, RSS, logos, and more — and ensemble
mode drives a whole wall of C64s at once.
Every row below is a runnable, single-scene demo — pass it to
--config and it loops forever until you Ctrl+C. Point it at your
hardware with -u (see Quick start).
docs/usage.md documents every option for these
scenes and overlays.
| I want to… | Try | Reference |
|---|---|---|
| Play a video (or YouTube URL) | c64cast clip.mp4 · scene-video.toml |
Quick playback |
| Show a live webcam as C64 art | scene-webcam-petscii.toml |
Scenes |
| Visualize a SID tune (oscilloscope) | scene-waveform.toml |
Scenes |
| Play a SID from a MIDI keyboard | scene-midi.toml |
Scenes |
| Stream from DeepSID / SIDFactory II | scene-asid.toml |
Scenes |
| Slideshow of images | scene-slideshow.toml |
Scenes |
| Generative / music-reactive visuals | scene-generative-plasma.toml |
Scenes |
Run a native .prg/.crt game or demo |
scene-launcher.toml |
Scenes |
| An info board (clock/weather/RSS) | overlay-clock.toml |
Overlays |
| Drive multiple C64s as one video wall | ensemble/master.toml |
Ensemble mode |
See config/examples/README.md for the
full demo index (one TOML per scene type and per overlay).
Scenes — a TOML playlist runs any mix of these on the real C64, each for a set duration, with an "UP NEXT" interstitial between them:
- Video — MP4/MKV/etc. (and YouTube/other URLs via yt-dlp), soundtrack and all, keyed off the audio clock so A/V can't drift.
- Webcam — live capture quantized to any display mode in real time.
- Slideshow — still images from a directory/glob, aspect-fit.
- SID waveform — plays a
.sidnatively on the C64 (via a small player PRG, not the firmware's own runner) with a per-voice oscilloscope. Handles multi-SID tunes — up to 8 chips using the U64's UltiSIDs. - MIDI → SID — bridge a live MIDI source (USB controller, DAW) into
the real SID and visualize each voice (
midiextra). - ASID client — receive an ASID stream (DeepSID in a browser,
SIDFactory II, Plogue chipsynth C64, …) and play it on the real SID with
the same 3-voice scope (
midiextra). - Generative — procedural plasma/tunnel/fire sources, optionally music-reactive and with pixel effects (trails, pulse, RGB shift).
- Launcher — hand the machine over to a native
.prg/.crtgame or demo, then reclaim it. - Blank — a solid PETSCII canvas for title cards + overlays.
Display + audio — six VIC-II display modes (hires, hires_edges,
mhires, petscii, mcm, blank), each with its own vectorized
quantizer (~30 fps bitmap, 50/60 fps char over a LAN). Audio plays through
the SID's 4-bit $D418 DAC or, on the U64, the high-fidelity Ultimate
Audio FPGA PCM sampler.
Overlays — stack on any compatible scene: scrolling text, marquee, RSS ticker, PETSCII spectrum analyzer, clock, weather, callsign, countdown, network info, multi-line logo, demo-scene big text, OBS Studio status.
Ensemble mode — one process drives N Ultimate 64s at once as a
video wall, with cross-system orchestration (e.g. a big_text message
scrolling across every screen as a single canvas).
Control surfaces — the C64 keyboard itself (C= pauses, CTRL skips,
SHIFT cycles the style), an on-C64 SPACE menu for live scene tweaks, webcam
hand gestures (vision extra), a FastAPI control plane (/pause,
/resume, /skip, /reload), MIDI CC control, and SIGHUP to reload the
config.
Quick playback — skip the config file entirely and pass media straight
on the command line: c64cast clip.mp4 tune.sid pics/ plays each in turn.
Preview + recording — an optional pygame mirror of what the C64 is showing, plus cv2-based recording to MP4.
git clone https://github.com/kfox/c64cast
cd c64cast
# Installing [uv](https://github.com/astral-sh/uv) is recommended.
# Optionally, use mise + direnv; direnv activates .venv for you.
# Hard deps + every optional extra + dev tooling, into a uv-managed .venv.
uv sync --all-extras
# Plain-pip alternative (no uv): runtime extras only — the dev tools are a
# PEP 735 dependency-group, installed separately:
# pip install -e .[all] && pip install --group dev
# "Hello world": scrolls big text across a solid canvas. Requires a
# reachable U64/TR+ — no webcam, mic, SID, or video files. Edit the URL at the
# top of the file to point at your U64/TR+, then run it. Ctrl-C to exit.
python -m c64cast --config config/examples/hello.toml
# Override the connection target without editing the file:
python -m c64cast --config config/examples/hello.toml -u u64://192.168.1.64-u/--url is a scheme-aware target that picks the backend + endpoint:
u64://HOST or http(s)://HOST (Ultimate 64 / II+), tr:// (TeensyROM+ over
auto-detected USB serial), tr:///dev/cu.usbmodem* / tr://COM3 (a specific
serial device), or tr://HOST (TeensyROM+ over TCP). $C64CAST_URL is the env
fallback.
hello.toml is the gentlest starting point. From there:
# Try a single feature in isolation — one TOML per scene type / overlay:
python -m c64cast --config config/examples/scene-webcam-petscii.toml
python -m c64cast --config config/examples/overlay-clock.toml
# Build your own config the easy way — the interactive wizard walks you
# through either a single scene or a multi-scene playlist (with the "UP NEXT"
# interstitial, video interleaving, and loop control) and writes a c64cast.toml
# (needs the 'wizard' extra; auto-loaded when no --config is given):
python -m c64cast --init
# ...or by hand: config/c64cast.example.toml is a fully-annotated
# reference exercising every scene + overlay; copy the bits you want.
cp config/c64cast.example.toml c64cast.toml && $EDITOR c64cast.toml
python -m c64cast
# Validate any config + check which optional extras are installed without
# touching the U64 (skip the connectivity probe to keep it offline):
python -m c64cast --doctor --config c64cast.toml --skip-probeEach file in config/examples/ is a runnable
single-scene demo. See config/examples/README.md
for the file index.
python -m c64cast -h lists every CLI flag grouped by section
(connection, quick playback, video input, audio, vision input,
playlist, introspection, debug).
Pass media files/directories/globs/URLs as positional arguments to play them
once, in order, without writing a TOML (mutually exclusive with --config).
Audio is on by default; --no-audio mutes.
# A video, a SID tune, then a folder of pictures, on an Ultimate 64:
python -m c64cast -u u64://192.168.1.64 clip.mp4 tune.sid assets/pictures/
# A clip on a TeensyROM+ over auto-detected USB serial:
python -m c64cast -u tr:// clip.mp4
# A YouTube URL (needs the 'yt' extra: uv sync --extra yt):
python -m c64cast 'https://youtu.be/dQw4w9WgXcQ'scripts/c64cast.sh is a thin convenience wrapper
around python -m c64cast. It cds to the repo root and forwards every
argument, running through uv run when uv is on your PATH (so the
project .venv is always used) and falling back to a bare python
otherwise. Handy when invoking c64cast from another directory or from a
context where direnv hasn't activated .venv (cron, systemd, an ssh
one-liner):
scripts/c64cast.sh --config config/examples/hello.toml
scripts/c64cast.sh --doctor --skip-probeAnywhere this README shows python -m c64cast ..., scripts/c64cast.sh ...
is an equivalent drop-in.
A config is a single TOML file (--config PATH, else ./c64cast.toml,
else built-in defaults) that defines the playlist and every overridable
option. Three ways to author one, plus tooling to discover and validate it —
none of which needs the U64:
# Build one interactively (single scene or multi-scene playlist):
python -m c64cast --init # needs the 'wizard' extra
# Discover the config surface straight from the code (always in sync):
python -m c64cast --list-scenes # scene types
python -m c64cast --list-overlays # overlays + their restrictions
python -m c64cast --list-modes # display modes
python -m c64cast --describe overlay:clock # full reference for one thing
python -m c64cast --compat # overlay × display-mode matrix
python -m c64cast --print-schema # JSON Schema for editor autocomplete
# Validate a config (and check which extras are installed) without hardware:
python -m c64cast --doctor --config c64cast.toml --skip-probeThe discovery output and the JSON schema are generated from the same field
metadata the loader runs on, so they can't drift from the code.
config/c64cast.example.toml is the fully-annotated
reference; see docs/usage.md for the complete config
walkthrough.
While the stream is running, you control it from the C64 keyboard itself
(c64cast polls $028D, the kernal's keyboard-modifier scratch byte, at
10 Hz):
| Key on the C64 | What it does |
|---|---|
| Commodore (C=) — tap | Pause: scene + overlays tear down, screen clears, audio stops |
| Commodore (C=) — hold 3 s while paused | Resume: re-sets-up the same scene (audio + polling threads all come back) |
| CTRL — tap while playing | Skip: advance to the next interstitial after the current frame |
| SHIFT — tap while playing | Cycle the current scene's display style (palette mode / edge variant / waveform subtune / etc.) |
The C= + CTRL chord pressed on the same poll tick prefers pause — skip is suppressed. SHIFT held alongside C= or CTRL is dropped so a thumb resting on shift doesn't phantom-cycle the style. Cycled style persists across single-scene loop iterations and across pause/resume, but resets to the configured default on a real scene boundary (multi-scene transitions construct fresh display_mode instances).
Same actions are exposed over HTTP when [control] enabled = true:
curl -X POST http://127.0.0.1:8765/pause
curl -X POST http://127.0.0.1:8765/resume
curl -X POST http://127.0.0.1:8765/skip
curl -X POST http://127.0.0.1:8765/reload # re-read config from diskOr send SIGHUP to the process to trigger a config reload from the
shell.
- docs/usage.md — full config reference, scene/overlay catalog with options, suggested setups
- docs/caveats.md — known quirks (6502 emulator scope, char ROM substitution, U64 endpoint variance, licensing of SIDs / videos)
- docs/troubleshooting.md — symptom-first index for "I saw X, what now?"
- docs/extending.md — how to add a new Scene, Overlay, DisplayMode, or interstitial Background
- CLAUDE.md — architecture notes for code-spelunking AI assistants and for humans who like dense reference material
One of the following:
- An Ultimate 64 — confirmed with Elite I, Elite II, Ultimate II+ cartridge, or Commodore 64 Ultimate. Best results will be obtained from using the Elite II or the Commodore 64 Ultimate. Under F2 → Network Settings, enable Ultimate DMA Service, Command Interface (TCP port 64 — the Command Interface toggle gates command dispatch even when the socket is open), and Ultimate Audio for streaming PCM audio. The REST API is used for the few operations that have no DMA equivalent.
- A TeensyROM+ Multi-Capable Cartridge for C64/128 plugged into an original Commodore 64 or one of the above modern "ultimate" equivalents.
Depending on how you use it, you'll also want some of these things:
- Any C64 video output path supported by a U64/C64.
- A webcam (any cv2-compatible USB device) for live capture scenes.
- A microphone for live audio; otherwise the audio path can sit idle or play a video's soundtrack via PyAV.
- A MIDI controller if you want to use MIDI scenes or control playlists/scenes via MIDI CC messages.
- An HDMI capture device if you want to capture output directly from a U64 or C64 equipped with a Kawari Large. Example capture devices include the Elgato Cam Link 4K or the Genki ShadowCast.
There is no software emulator path for the streaming side — c64cast
writes directly to U64 memory/registers over the Ultimate DMA Service
(TCP port 64), with REST used only for the few non-DMA operations. SID
playback is
driven by a small player PRG uploaded into C64 RAM so the real 6510
calls PLAY at IRQ time (the U64 firmware's runners:sidplay runner is
deliberately avoided because it hijacks the HDMI output with its own
UI); see docs/caveats.md for the PSID-only limitation.
An HDMI capture device (see above) is highly recommended for development. There are some diagnostic scripts in the scripts/diags subdirectory that can make use of an attached capture device, if present.
uv sync --all-extras # or: pip install -e .[all] && pip install --group dev
pre-commit install # ensure ruff + tests run before every commitCI runs the same lint + tests on every push and pull request — see .github/workflows/ci.yml.
There's a Makefile available that offers a few development targets:
⮑ make
targets:
lint ruff check
test unittest discover
coverage coverage report + HTML
typecheck mypy --strict (api/audio/playlist) + pyright (whole tree)
bench scripts/bench.py — async write pipeline
schema regenerate c64cast.schema.json from the config metadata
check lint + typecheck + test
clean remove build artifacts- Gideon Zweijtzer for the Ultimate 64 hardware and firmware.
- Travis Smith for the TeensyROM+ - including cartridge, firmware, hands-on testing, and suggestions.
- Bo Zimmerman for his excellent online and physical collections of all things Commodore.
- The HVSC team for the SID archive and the Songlengths database.
- Pex 'Mahoney' Tufvesson for the 8-bit
$D418DAC technique (his "Musings in the Key of C64" white paper) behind the optionaldac_curve = "mahoney_ultisid"audio path. - Jürgen Wothke (webSID / Tiny'R'Sid) for
documenting the
$D418filter-bit "almost 8-bit" playback approach behind Mahoney's technique. - Antonio Savona for the
48 kHz
$D418write-up. - CodeBase64 for the extensive reference material.
- Many open source contributors for all of the many Python packages that make this app possible. <3
MIT — see LICENSE.
