Converts a folder of video into 640x480 SD MP4 for FieldStation42 on a Raspberry Pi driving a composite CRT.
Point it at a folder and walk away.
cd ~/Desktop/RetroConvert
./retro-convert.sh "/Volumes/SD/Next Wave"You get a live progress bar with a running count, rate and ETA:
[############..................] 41% 59/144 ok:59 fail:0 2.6/min eta 32m free 178GB
| What you want | Command |
|---|---|
| Convert a card, free space as it goes | ./retro-convert.sh /Volumes/SD |
| Same, but stay usable while you work | ./retro-convert.sh /Volumes/SD --jobs 4 --nice 19 |
| Full speed, machine left alone | ./retro-convert.sh /Volumes/SD --jobs 12 |
| Keep originals, write elsewhere | ./retro-convert.sh ~/Desktop/Show --out ~/Desktop/done --keep-sources |
| Just tell me what you'd do | ./retro-convert.sh /Volumes/SD --dry-run |
--help lists everything.
Always start with --dry-run on an unfamiliar folder. It prints the
counts and changes nothing.
- Skips anything already 640x480 or smaller. Re-encoding those costs a generation of quality and saves nothing. In one wave this was 375 of 519 files left untouched.
- Never upscales. A 320x240 source stays 320x240.
- Fills the tube. Widescreen is scaled to cover 640x480 then cropped, not letterboxed — a CRT is 4:3 and black bars waste a third of the picture. Extreme aspects are padded instead.
- Deletes a source only after its output passes every check: duration within 2%, ≤640x480, even dimensions, no upscale. Anything that fails keeps its source and is listed by name at the end.
- Never deletes an unreadable source. Broken files are reported for you to re-copy.
- Resumable. Ctrl-C is safe; re-run the same command and finished files are skipped.
- Keeps the Mac awake for the whole run, and releases it on exit.
- Sweeps
._*sidecars at the end. FieldStation42 globs*.mp4and would otherwise catalogue every sidecar as an episode.
Encoder settings are fixed and deliberate: libx264, CRF 23, main/3.1,
-preset veryfast, yuv420p, -g 60, AAC stereo 128k 48kHz, subtitles and data
streams dropped. CRF not fixed bitrate — a fixed bitrate inflated files. Never
VideoToolbox — no throughput gain, and concurrent jobs die with
-22 Could not open encoder.
Each run writes runs/<timestamp>/:
| File | Contents |
|---|---|
status.tsv |
one line per file: OK/FAIL, sizes, reason |
failed.txt |
just the failures, with why |
skipped.txt |
what was left alone, and why (already SD / broken) |
build.log |
raw ffmpeg errors |
The script checks ~/.local/bin first, then PATH, and prints install
instructions if neither has it. It has vanished twice before — once with a
deleted Desktop folder, once when /tmp was purged — which is why it now lives
in ~/.local/bin.
Several things in the script look like they could be simplified. They can't. Each one cost real time to find:
wait -ndoes not work here. macOS ships bash 3.2;wait -narrived in 4.3. In 3.2 it fails instantly and does not block, so the parallelism gate silently stops holding and every job launches at once — 190 concurrent ffmpegs, observed. Thejobs -rcount gate is deliberate.-threadsmust sit in the output options. Before-iit only limits the decoder; x264 then free-runs across every core and a few concurrent jobs make the machine unusable.- No "if the encode grew, remux instead" fallback. A remux copies the video stream untouched, so for an oversized source it returns the original oversized frame — smaller on disk, unplayable on the Pi. 56 short clips came back at full width that way before this was removed.
- The upscale check tests the picture, not the frame. A letterboxed output is a 640x480 frame holding smaller content; comparing frame height to source height falsely failed two correct 720x400 encodes.
- An unreadable probe must fail loudly. Shell numeric tests against an empty string pass, so treating a failed probe as "fine" means the upscale check never ran at all.
- Parse dimensions with
cut, not${d%x*}. Some files make ffprobe emit a trailing delimiter (3840x2160x); the naive form reads that as junk and silently skips a real 4K file. - Sweep
._*after the copy, not just during.COPYFILE_DISABLE=1plus an rsync--excludeis not enough — rsync preserves xattrs and exFAT materialises them as sidecars at the destination. - Kill the whole process tree. Killing the driver leaves ffmpeg children reparented to PID 1, still burning CPU and still holding disk blocks for files you already deleted.
RetroConvert.command converts the folder it sits in, plus every
subfolder — so a shows folder with Season 01/, Season 02/… is handled in
one go.
- Copy
RetroConvert.commandandretro-convert.shinto the folder (or the root of the SD card). - Double-click
RetroConvert.command. - It scans, shows you the counts, and waits for you to type
yes. Anything else cancels without touching a file.
It looks for retro-convert.sh beside itself first, then falls back to
~/Desktop/RetroConvert/, so copying just the .command also works as long as
this folder stays put.
The confirmation prompt is deliberate: double-clicking is easy to do by accident, and the default behaviour deletes each original once its replacement verifies.