Capture

A capture is a file the machine made. Emu198x hands you the frame its video path painted, the samples its sound chip emitted, or both muxed together — written to disk by the same headless run that produced them, with no window, no screen grabber and nobody watching.

The still image is the everyday case, and it is the one this site runs on. Every boot picture on the system matrix came out of a capture: thirty-four of them from a --screenshot flag, and the thirteen Spectrum models — the one family whose binary has no such flag — from a save_screenshot step. The ZX80 and ZX81 take the flag, so the split is not a Sinclair one. Both routes are below, and the example further down writes the NES one byte for byte.

The mental model

What may be published

Worth knowing before the captures pile up, because it shapes which of them you want. Software this project did not write is illustrated with stills — one to three frames, each attached to a claim the surrounding writing is making, never a gallery. Where a still cannot carry the point, because the point is a raster split or a scroll or sprite flicker, the published form is a silent animated WebP at the machine's own field rate. MP4 files and audio captures stay local: a muxed container carries a sound track, and a captured tune is a whole composition by its own author rather than an extract of anything. Capturing any of it to check your work is a separate question; this is about what goes on a public page.

What works today

All 30 machines carry the capture steps, because they live in the shared session every binary is built on. What differs is the command-line shorthand around them.

NeedStepsFlag
Still imagesave_screenshot--screenshot PATH, on every machine but the ZX Spectrum, ZX80 and ZX81
Soundstart_audio_recording, stop_audio_recording, save_audio_capture, clear_audio_capture--audio-capture PATH, on every machine but the Spectrum and the C64
Moving picturestart_video_recording, stop_video_recordingNone
Frame sequencesave_screenshot, once per frameNone
Did anything arrive?query on capture.has_frame or capture.has_audioNone

One format per medium, and the file name does not choose it. A still is a PNG: 8-bit RGBA, lossless, no palette indexing. Audio is a 16-bit PCM WAV at the machine's own rate — 48 kHz on a C64 or a Master System, 44.1 kHz on a Spectrum — with the channel count the machine emits. Video is an H.264 MP4, and the audio track is muxed in when the run captured any.

The video encode is deliberate about dither, and it took two goes to get there. Retro framebuffers carry checkerboards at single-pixel frequency, and libx264's defaults — CRF rate control over 4:2:0 chroma — average that into flat colour. The encoder now runs a constant quantiser at QP 12 over full 4:4:4 chroma, which holds the pattern and holds it equally on the first frame as on the hundredth. The earlier CRF setting drifted over the opening frames, which on a short loop reads as a flash at every restart.

What a frame comes out as

MachinePNG size
ZX Spectrum352 × 296
Commodore 64416 × 312
Amstrad CPC832 × 288
Sega Master System280 × 240
NES256 × 240
Game Boy160 × 144

Those are the emitted frames, not the advertised display modes. A Spectrum is a 256 × 192 screen inside a border the hardware also draws, and the capture keeps both. The CPC number is wide because its pixel clock is, not because anything was scaled.

Capturing a still

The shortest run that needs no firmware and no software you have to find. From a source checkout, this boots a NES on a synthetic cartridge built in this repository, runs 300 frames and writes the frame it stopped on:

cargo run --release -p emu198x-nes -- \
  --rom test-data/synthetic-cartridges/nintendo-nes-logo.nes \
  --frames 300 \
  --screenshot boot.png
NES runtime: time=53426560 cartridge_loaded=true

boot.png is a 256 × 240 picture of the Emu198x wordmark drawn in the machine's own tiles. It is the same file the NES page shows, down to the checksum, which is the useful property: the picture and the command that made it travel together. A release binary takes the same flags.

One machine has no --screenshot flag — the Spectrum — and the step covers it. Put this in shot.json:

[
  {"action":"wait_for_boot","max_frames":300},
  {"action":"save_screenshot","path":"boot.png"},
  {"action":"query","path":"capture.has_frame"}
]
./emu198x-spectrum --script shot.json

This one needs the machine's firmware, since a Spectrum paints nothing without it. The report comes back on standard output:

{"kind":"wait_for_boot","frames":87,"reached":24321024,"reason":"found copyright banner on row 23","row":23}
{"kind":"query","result":{"path":"capture.has_frame","value":true}}

Two steps ran and three are in the file, because save_screenshot reports nothing at all — it either writes the file or ends the run with an error. That is why the capture.has_frame query is worth the third line: it is the machine confirming there was a frame to write, in the report you keep.

A frame sequence

Moving pictures for publication start as numbered PNGs, and stay that way on disk. There is no frame-dump flag, so a sequence is one save_screenshot per frame with the machine advanced between them. Sixty frames of settling, then three consecutive fields:

[
  {"action":"run_frames","frames":60},
  {"action":"save_screenshot","path":"frames/0001.png"},
  {"action":"run_frames","frames":1},
  {"action":"save_screenshot","path":"frames/0002.png"},
  {"action":"run_frames","frames":1},
  {"action":"save_screenshot","path":"frames/0003.png"}
]

Save that as frames.json and point a machine at a moving cartridge. The directory has to exist first — a capture writes a file and will not build a path to put it in:

mkdir frames
cargo run --release -p emu198x-sega-master-system -- \
  --cart test-data/sega/synthetic-cart/master-system-raster.sms \
  --script frames.json

Keep those frames. They are the honest artefact — someone can look at what was captured instead of trusting an encoder — and the animation is built from them as a separate step, with a tool of your own. Emu198x ships no WebP encoder; the reference one from the libwebp project reads a sequence directly:

img2webp -loop 0 -d 17 -lossless frames/*.png -o clip.webp

-d is the per-frame duration in whole milliseconds, and it is the number to get right. A 60Hz machine wants 17, a 50Hz machine 20. Do not halve the rate to save bytes: a scroll survives it, but flicker does not, and where the effect is the flicker — sprite multiplexing, interlace, two-frame colour mixing — a halved rate shows a reader something the hardware never did. These are small pictures from machines with a handful of colours on screen, so the saving would be tiny anyway.

Sound and video

Audio comes out as WAV, either from a flag over a whole run or from a bracketed pair of steps that captures the window between them. The flag form:

cargo run --release -p emu198x-sega-master-system -- \
  --cart test-data/sega/synthetic-cart/master-system.sms \
  --frames 150 \
  --audio-capture tone.wav

That writes a 16-bit mono WAV at 48 kHz, and it writes silence: the synthetic cartridges paint a frame and never touch the sound chip. The capture path is what the run proves. To hear a machine you need software that plays something, which is yours to supply.

Video is one MP4 per recording, bracketed the same way. The machine has to have painted a frame before the recorder starts, because the frame is where it gets its width and height:

[
  {"action":"run_frames","frames":1},
  {"action":"start_video_recording","path":"clip.mp4"},
  {"action":"run_frames","frames":100},
  {"action":"stop_video_recording"}
]
{"duration_ms":1666,"frames":100,"has_audio":true,"kind":"stop_video_recording","path":"clip.mp4"}

Only the stop step reports, and it reports what landed: 100 frames, 1.666 seconds of machine time, and a muxed sound track. Every frame the machine emits between the two steps is teed into the recorder, so the clip's frame rate is the machine's field rate and not a setting.

No flag turns the sound track off, but a clear_audio_capture step immediately before the stop empties the buffer the muxer reads, and the recording finishes with "has_audio":false and one video stream. That is the whole mechanism, and it is worth knowing: a container with a sound track in it is the reason a clip stays local.

What you need that we cannot give you

Firmware and software, the same as any other way of running these machines. Emu198x ships no ROMs, no BIOS images, and no disks, tapes or cartridges; the downloads page sets out the position on that. The synthetic cartridges used above are the exception, and a narrow one: a few machines declare no firmware — the NES, Game Boy, Master System, Game Gear, SG-1000 and Atari 2600 among them — so a cartridge built in this repository is enough to reach a frame. Every other machine needs its manufacturer's ROM before there is anything to photograph.

ffmpeg, for video. The recorder streams raw frames to an ffmpeg subprocess and shells out a second time to mux audio, so a machine without it on PATH refuses the recording rather than writing a broken file. Stills and WAVs need nothing beyond the binary.

An encoder, for animation. Turning a PNG sequence into an animated WebP happens outside Emu198x, with whatever tool you prefer.

What does not work yet