A free, open toolkit for turning everyday assets — MIDI/MML/MOD music, PNG images, and TTF fonts — into SNES-ready data for PVSnesLib games.
Writing a Super Nintendo game means feeding the hardware data in formats from 1990: planar tiles, BGR555 palettes, Impulse Tracker modules built from tiny chiptune voices. These tools do those conversions for you. They're small, dependency-light Python and Bash scripts, each with a proper command-line interface — drop them into any PVSnesLib project and point them at your art and music.
Extracted and generalised from the tooling that built Deadfall, a complete from-scratch SNES game.
This toolkit is released into the public domain (see License). Use it for anything — personal, commercial, closed-source, whatever — with no attribution required and no permission needed. If it saves you time and you'd like to say thanks, you can buy me a coffee on Ko-fi. Entirely optional. The tools are yours either way.
- Why this exists
- Requirements & install
- Quick start
- The tools
- Using these in a PVSnesLib project
- Notes & gotchas
- FAQ
- License
- Support
- Credits
The SNES is a wonderful but unforgiving target:
- Music plays through snesmod, whose converter smconv only accepts
Impulse Tracker
.itmodules. The console has a single audio chip (the APU) shared by music and sound effects, so you can't just upload a ripped.spc— that would replace the sound driver and silence your SFX. The audio tools here solve this by keeping the real notes and timing of your song but rebuilding it from a handful of synthesised chiptune voices that fit in ARAM next to your effects. - Graphics must be planar tiles with packed BGR555 palettes.
png2bg8turns an ordinary PNG into a tile set + tilemap + palette, deduplicating repeated tiles (including mirrored ones) automatically. - Text needs a tile-based font.
ttf2fontrasterises any TrueType/OpenType font into 8×8 2bpp glyph tiles.
Every tool is standalone — there's no framework, no config files, no build
system to adopt. Run them by hand or wire them into your Makefile.
- Python 3.8+
- numpy (audio tools) and Pillow (graphics tools)
- The toolchain helpers are Bash and macOS-specific
git clone https://github.com/TheMarco/pvsneslib-toolkit.git
cd pvsneslib-toolkit
pip install -r requirements.txt # numpy, PillowThat's it — the scripts run in place. There's nothing to build or install beyond the two Python packages.
# A MIDI tune -> a SNES music module (+ a .wav you can listen to first)
python3 audio/mid2it.py mysong.mid -o res/bgm.it
# A PNG -> an 8bpp background (tiles + map + palette)
python3 graphics/png2bg8.py title.png -n title -d res
# A font -> an 8x8 text font for your HUD / menus
python3 graphics/ttf2font.py myfont.ttf 14 -o res/font.picEach audio tool also drops a *_preview.wav next to its output so you can
audition the result before it ever goes in the ROM.
All three converters produce an Impulse Tracker .it module that snesmod's
smconv can compile, built from the same bank of small synthesised voices
(triangle / square / pulse / saw leads + kick / snare / hat / crash drums). They
keep your song's real pitches and timing; the timbre is a chiptune
approximation, so always check the preview WAV.
A General MIDI file is pure composition (notes + timing, no audio), which makes it the ideal SNES music source. Each MIDI channel becomes a chiptune voice; channel 10 (GM percussion) is routed to drum one-shots.
python3 audio/mid2it.py song.mid # -> ./song.it + ./song_preview.wav
python3 audio/mid2it.py song.mid -o res/bgm.it # explicit output path
python3 audio/mid2it.py song.mid --bpm 130 # force the whole song to 130 BPMCombine mode — merge two MIDIs into one module as two independently
looping sections. In-game you switch between them with a single snesmod position
jump (spcPlay) — no blocking module reload — perfect for calm/intense variants
of a level theme:
python3 audio/mid2it.py --combine calm.mid intense.mid -o res/level.it --bpm 130
# spcPlay(0) plays section A (calm); spcPlay(<b_start>) plays section B (intense)
# the script prints the b_start order index to use.| Option | Default | Meaning |
|---|---|---|
src |
— | input .mid (first section in --combine) |
second |
— | second .mid (only with --combine) |
-o, --out |
<src>.it |
output .it path |
-d, --outdir |
. |
output directory when -o is omitted |
-t, --title |
filename | IT song title (max 25 chars) |
--bpm N |
keep MIDI's own | force one tempo for the whole song |
--combine |
off | merge two MIDIs into two looping sections |
Many SNES music archives ship AddmusicK MML scores (the text source behind a
.spc). This parses the MML directly — notes, lengths, ties, octaves, tempo,
volume, instruments, loops [..]n (incl. nesting), labelled-loop recalls
(N)[..], and NAME=... macros — and renders the same chiptune module.
python3 audio/mml2it.py score.txt -o res/bgm.it
python3 audio/mml2it.py score.txt --amk-scale 2.0 # retune AMK-tempo -> BPM
python3 audio/mml2it.py score.txt --bpm 150 # force an exact tempo| Option | Default | Meaning |
|---|---|---|
src |
— | input MML score (.txt / .mml) |
-o, --out |
<src>.it |
output .it path |
-d, --outdir |
. |
output directory when -o is omitted |
-t, --title |
filename | IT song title |
--bpm N |
from score | force this exact tempo, bypassing the AMK conversion |
--amk-scale F |
2.385 |
AddmusicK-tempo → BPM scale factor |
For when you have real sampled music. Parses a 4/6/8-channel ProTracker .mod
and re-emits it as .it, translating samples, notes, volumes and the structural
effects. An optional KB budget shrinks sample-heavy modules to fit ARAM by
decimating samples (pitch preserved).
python3 audio/mod2it.py song.mod res/song.it # straight conversion
python3 audio/mod2it.py song.mod res/song.it 32 # fit samples into ~32 KB
python3 audio/mod2it.py --selftest # build + convert a tiny test mod| Argument | Meaning |
|---|---|
in.mod |
source ProTracker module |
out.it |
output Impulse Tracker module |
KB (optional) |
sample-data budget in KB; downsamples to fit |
The minimal .it writer the three converters share. You can also import it
directly to build a sound-effects bank — an .it whose samples are your
SFX, played in-game by index via spcLoadEffect / spcEffect:
from make_it import write_it
write_it("res/sfx.it", "SFX", samples) # samples = list of {name, pcm, rate, loop}See the docstring at the top of the file for the sample-dict format.
Converts an indexed PNG into a Mode 3, 8bpp background. 8bpp means 256 colours with no per-tile palette limit, so your source colours are preserved exactly 1:1 (a 4bpp/Mode-1 background is capped at 16 colours per 8×8 tile). Identical tiles — including horizontal/vertical mirrors — are deduplicated into a 32×32 tilemap.
python3 graphics/png2bg8.py title.png -n title -d resProduces three files in the output directory:
| File | Contents |
|---|---|
<name>.pic |
deduplicated 8bpp tiles (64 bytes each) |
<name>.map |
32×32 tilemap (16-bit little-endian tile indices) |
<name>.pal |
palette, BGR555 little-endian |
| Option | Default | Meaning |
|---|---|---|
image |
— | source image (any Pillow-readable format) |
-n, --name |
image stem | output basename |
-d, --outdir |
. |
output directory |
Rasterises a TrueType/OpenType font into a flat run of 8×8, 2bpp glyph tiles —
the layout most SNES text engines (including PVSnesLib's consoleDrawText)
expect. Each glyph is baseline-aligned and ink-centred, so a proportional font
still lines up as a tidy fixed-width run.
python3 graphics/ttf2font.py myfont.ttf 14 -o res/font.pic # transparent bg
python3 graphics/ttf2font.py myfont.ttf 14 --bg 2 -o res/hud.pic # opaque bg (solid HUD)
python3 graphics/ttf2font.py myfont.ttf 14 --preview "HELLO 0123" # ASCII-art, no file| Option | Default | Meaning |
|---|---|---|
font |
— | TrueType/OpenType font file |
size |
14 |
pixel size to rasterise at |
-o, --out |
<font>.pic |
output .pic path |
--bg N |
0 |
background colour index: 0 transparent, 2 opaque |
--first N |
32 |
first code point (32 = space) |
--count N |
64 |
number of glyphs (64 → ASCII 32..95) |
--preview [TEXT] |
off | print the font as ASCII art instead of writing a file |
The letter is colour 1; the cell background is --bg. Glyphs the font lacks
are emitted blank and reported, and glyphs wider than 8px are reported as
overflow (lower size if too many overflow).
PVSnesLib's snes_rules uses GNU-style sed -i SCRIPT FILE, which BSD/macOS
sed rejects (it reads the next argument as a backup suffix). This shim
intercepts only the -i in-place form and passes everything else straight
through to the system sed, so you never have to modify the installed
toolchain. Put it on PATH ahead of /usr/bin in your build (see
below).
Downloads the latest PVSnesLib release, extracts it to ~/pvsneslib, clears the
macOS quarantine bit on the binaries, detects and persists PVSNESLIB_HOME to
your ~/.zshrc, and writes a .pvsneslib_home marker into your project so its
build can find the toolchain.
bash toolchain/install_pvsneslib.sh [PROJECT_DIR] # PROJECT_DIR default: $PWDA typical project keeps converted assets in res/ and .incbins them. Two
practical tips:
1. The macOS sed shim. Run your build with the shim on PATH so
snes_rules works:
all:
PATH="$(CURDIR)/toolchain/bin:$$PATH" $(MAKE) rom2. snesmod module ordering. smconv assigns the MOD_* indices you pass to
spcPlay() in the order modules appear in your AUDIOFILES list, and the SFX
bank must be first. Keep that list stable, or your in-game music indices
will shift.
A regeneration target might look like:
res/bgm.it: music/song.mid
python3 tools/mid2it.py $< -o $@
res/title.pic: art/title.png
python3 tools/png2bg8.py $< -n title -d resHard-won lessons baked into these tools:
- numpy
uint8overflow when bit-packing. When packing palettes/pixels into BGR555 or shifting bytes, cast numpyuint8tointbefore the math.uint8arithmetic wraps silently and produces garbage — the classic symptom is an all-red palette. (The tools already handle this; worth knowing if you extend them.) - Chiptune timbre is an approximation. The audio tools reproduce the real
notes and rhythm but synthesise the instruments, so they won't sound like the
source's patches. Always listen to the
*_preview.wav. - Font baseline clipping.
ttf2fontaligns glyphs to a baseline inside the 8×8 cell, so deep descenders and the underscore can clip, and wide glyphs may overflow 8px. Both are reported; reduce--sizeif needed. - ARAM is small. SNES audio RAM is 64 KB shared by the driver, samples and
patterns. If a
.modwon't fit, usemod2it's KB-budget argument; chiptune modules frommid2it/mml2itare tiny by design.
Does this depend on Deadfall or any specific game? No. The tools were extracted from Deadfall but have no game-specific assumptions — output paths, titles and filenames are all command-line options.
Do I have to use the whole toolkit? No — every script is standalone. Grab just the one you need.
Will the music sound exactly like my MIDI/MML? The notes and timing will match; the instruments are synthesised chiptune voices, so the timbre differs. That's the trade-off for fitting in ARAM alongside your sound effects. Preview WAVs let you check before building.
Windows / Linux?
The Python converters (audio/*, graphics/*) are cross-platform. The
toolchain/ helpers are macOS-specific — Linux users generally don't need the
sed shim, and the installer targets macOS.
Can I use this in a commercial game? Yes. It's public domain — do anything you like, no permission or credit required.
Released into the public domain under The Unlicense.
You may copy, modify, publish, use, compile, sell, or distribute it — for any
purpose, commercial or not — with no attribution and no permission required.
See the LICENSE file for the full text.
These tools are, and always will be, completely free. If they helped you ship something and you'd like to support more work like this, a coffee is hugely appreciated — but never expected:
Built for and extracted from Deadfall. Targets PVSnesLib by alekmaul — the open-source SDK that makes SNES homebrew in C possible.