Pure-Python reader, writer, and player for SID-Wizard SWM modules
Project description
pysidwizard
Pure-Python reader, writer, and bit-exact player for SID-Wizard SWM modules. No native dependencies.
SID-Wizard is a Commodore 64 music tracker by Hermit (Mihaly Horvath). pysidwizard implements its SWM file format and player IRQ from first principles.
The player is verified against a live capture of SID-Wizard running
inside asid-vice: every frame × every SID register, on every PR.
See Player correctness below.
Install
pip install pysidwizard
Read an SWM
from pysidwizard import read_swm
swm = read_swm("tune.swm")
print(swm.author_str(), swm.frame_speed, swm.highlight)
# Pattern rows are typed; no byte poking required.
for row in swm.patterns[0].rows[:4]:
print(row)
# Sequences (one per SID channel) are lists of typed commands.
for cmd in swm.sequences[0][:6]:
print(cmd)
# Instruments expose fixed fields by name and tables as bytes.
ins = swm.instruments[0]
print(ins.name_str(), ins.attack, ins.decay, ins.sustain, ins.release)
print(ins.wf_table.hex(), ins.pw_table.hex(), ins.filter_table.hex())
read_swm auto-detects both bare SWM payloads and PRG-wrapped files
(2-byte little-endian load address, then the SWM1 magic at offset
2). The writer emits the PRG wrapper by default; set
swm.load_address = None for a bare payload.
Build an SWM from scratch
from pysidwizard import (
End,
Instrument,
Pattern,
PlayPattern,
Row,
SWMFile,
Waveform,
build_swm,
straight_tempo,
write_swm,
)
from pysidwizard.constants import GATE_OFF_FX, SWM_C5_NOTE
instrument = Instrument(
name=b"LEAD ",
control=0x1A, # hard-restart timer on, tied PW/CTF off
hr_attack=0, hr_decay=0xF,
hr_sustain=0xF, hr_release=0,
attack=0, decay=0,
sustain=0xF, release=0,
default_chord=1,
first_waveform=Waveform.PULSE,
)
pattern = Pattern(
rows=[
Row(note=SWM_C5_NOTE, instrument=1),
Row(), Row(), Row(), Row(), Row(), Row(),
Row(note=GATE_OFF_FX),
],
length=8,
)
swm = SWMFile(
author=b"PYSIDWIZARD DEMO",
default_pattern_length=8,
sequences=[[PlayPattern(1), End()]] * 3,
patterns=[pattern],
instruments=[instrument],
subtune_tempos=[(straight_tempo(6), straight_tempo(3))],
)
write_swm(swm, "demo.swm") # ready to load in SID-Wizard 1.x
assert build_swm(read_swm("demo.swm")) == build_swm(swm) # byte-exact roundtrip
The writer rejects modules that reference patterns or instruments
that don't exist, or sequences missing an End() / Loop()
terminator. NOP packing, table terminators, and on-disk pointer
arithmetic are handled automatically — you describe content, not
bytes.
Sequence command types
| Command | Meaning |
|---|---|
PlayPattern(n) |
Play pattern n (1-based; 0 is the reserved empty slot). |
Transpose(semitones) |
Per-channel transpose, -16..+15 semitones. |
TempoOverride(value) |
Switch to row-delay value until the next override. |
End() |
Terminate the sequence without looping. |
Loop(position) |
Terminate and jump back to position within the sequence. |
RawSequenceByte(b) |
Opaque single byte preserved for byte-exact round-trip. |
Row fields
Every column of a Row is None by default and contributes nothing.
-
note: a pitch (0..0x5F) or a note-column effect:Code Constant Meaning 0x78PORTAMENTO_FXPre-arm tone portamento for the next row 0x79SYNC_ON_FXEnable hard sync 0x7ASYNC_OFF_FXDisable hard sync 0x7BRING_ON_FXEnable ring modulation 0x7CRING_OFF_FXDisable ring modulation 0x7DGATE_ON_FXForce gate on 0x7EGATE_OFF_FXForce gate off -
instrument: a1..0x3Einstrument index, or an instrument-column effect — value< 0x40selects an instrument,>= 0x40runs a small effect. The same small-effect dispatch is available in thefxcolumn (any value>= 0x20):Range Effect 0x20..0x2FSet attack nibble of post-HR ADSR 0x30..0x3FSet decay nibble of post-HR ADSR; 0x3Fis legato0x40..0x4FSet waveform nibble (not yet modelled) 0x50..0x5FSet sustain ("note volume") 0x60..0x6FSet release 0x70..0x7FSet chord ( 0x7n→ chordn, looked up inchord_table) -
fx: a1..0xFFeffect code. Values< 0x20are big-FX and require anfx_value:Code Big-FX 0x01Pitch slide up (value = step per frame) 0x02Pitch slide down 0x03Tone portamento to the row's note (value = slide speed) 0x08Set vibrato amp + freq 0x0BJump to filter-table row (value = row index) 0x10Override row delay (= live tempo change) Values
>= 0x20are small-FX, same as the table above.
Play an SWM
from pysidwizard import SWMPlayer, read_swm
swm = read_swm("tune.swm")
player = SWMPlayer(swm)
# Each play_frame() returns the SID register writes the real
# SID-Wizard player would emit on the C64 this frame, in the same
# order (channel 3 -> 2 -> 1, then global filter + volume).
for _ in range(100):
for reg, value in player.play_frame():
print(f"${reg:04X} = ${value & 0xFF:02X}")
Render to WAV
from pysidwizard import render_wav
render_wav("tune.swm", "tune.wav", seconds=60.0, model_name="MOS8580")
Or from the command line:
python -m pysidwizard.player tune.swm tune.wav --seconds 60 --model MOS8580
render_wav drives pyresidfp
under the hood (install with pip install pyresidfp).
Iterate raw writes
For SID-state inspection or feeding a different SID emulator:
from pysidwizard import iter_writes
for frame, reg, value in iter_writes(swm, n_frames=1500):
...
iter_writes deduplicates consecutive identical writes to the same
register by default — matching the schema sidwizard-driver produces
for ground-truth captures.
SWM at a glance
An SWM module is a tracker module split into typed pieces. You don't need to know the byte layout to use this library, but a high-level picture of the moving parts is useful when reading or writing patterns:
Orderlist (sequences). Three independent sequences, one per
SID channel. Each sequence is a list of typed commands
(PlayPattern, Transpose, TempoOverride, End, Loop).
Channels advance independently; if v0 is playing a long pattern and
v1 finishes early, v1 just stays silent until its sequence loops.
Patterns (patterns). A list of rows; each row may set
note / instrument / fx / fx_value columns. The player runs
one row every tempo frames (or alternating funktempo left/right
values), or whatever the current TempoOverride says.
Instruments (instruments). Each instrument is a fixed
ADSR / vibrato / hard-restart header plus three per-instrument
"tables" that the player walks once per frame while the instrument
is active:
- Waveform / arp table (
wf_table). Three-byte rows(waveform, arp_pitch, detune). Drives the SIDCTRLregister and the per-note pitch offset. Arp byte semantics:$00..$7E: relative pitch up — add this many semitones to the note.$7F: take the next pitch from the active chord table.$80: NOP — keep the previous pitch.$81..$DF: absolute pitch — the low 7 bits become the discrete pitch.$E0..$FF: relative pitch down.$FE(in the waveform column): jump to the row whose offset is in the next byte.$FF: end of table; the walker freezes here.
- Pulse-width table (
pw_table). Three-byte rows. Either set mode (high byte$80..$FD, plus a low byte -> SID pulse- width register), or sweep mode (low cycle-count byte, signed delta, key-track byte). - Filter table (
filter_table). Three-byte rows controlling the global SID filter while this instrument owns it (only one voice at a time controls the filter). Set mode encodes the band switches (LP/BP/HP) + resonance + cutoff hi; sweep mode drifts the cutoff with a signed delta over a cycle count.
Chord table (chord_table). A flat byte array indexed by
chord number. When the waveform table's arp byte is $7F, the
next chord pitch is looked up here. Chords loop until the
instrument's waveform table moves past the chord-trigger row.
Each table terminates on $FF. Jumps inside a table use $FE
followed by an offset. pysidwizard handles all of this for you
when serialising — pass the table contents only.
Player correctness
The four reference tunes in tests/fixtures/ (flashitback,
bronkosaurus, euphoria, rain8580) collectively exercise the full
1-SID feature surface: pitched notes, gate-off note-FX, chord
tables, multispeed (frame_speed=2), funktempo, BIGFX portamento,
vibrato, filter walking, instrument inheritance across F1, the
WRPITCH detune-with-carry chain.
For each tune, every frame's full SID-register state agrees
byte-for-byte with the reference captured from real SID-Wizard
running inside asid-vice
via sidwizard-driver.
tests/test_player_reference.py runs that comparison on every
push.
The integration suite in tests/integration/ goes one step
further: every PR re-derives a fresh reference CSV from the real
SID-Wizard binary and asserts pysidwizard still matches. If the
binary changes, the integration suite fails before merge.
Out of scope: multi-SID, SFX, slowdown, and non-440 Hz tuning tables.
Tests
Unit tests (fast, no Docker):
pip install -e ".[dev]"
python -m pytest
Integration tests (slow, requires Docker; pulls
anarkiwi/headlessvice):
pip install -e ".[integration]"
python -m pytest -m integration tests/integration/
The four SWM test tunes are not tracked in this repo — they're
SID-Wizard binary artifacts. tests/_swm_cache.py fetches them on
demand from the SID-Wizard 1.94 source tarball via
sidwizard-driver's cache and SHA-256 verifies each one.
Continuous integration
| Workflow | What it runs |
|---|---|
.github/workflows/test.yml |
ruff + black + pytest (matrix: Python 3.10-3.13 on Linux + 3.12 on macOS/Windows) |
.github/workflows/integration.yml |
Fresh capture from real SID-Wizard inside asid-vice; asserts pysidwizard matches |
.github/workflows/publish.yml |
Builds + uploads to PyPI via trusted publishing on release |
The integration workflow runs on every push, every PR, and weekly
on cron — so SID-Wizard binary / sidwizard-driver / headlessvice
drift is caught even when no PR has landed.
License
Apache 2.0 — 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 pysidwizard-0.1.0.tar.gz.
File metadata
- Download URL: pysidwizard-0.1.0.tar.gz
- Upload date:
- Size: 87.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
82836e1e314a88e558c67df3d02e800333fd82ce2bf6a4d3bf9cc8b527182f68
|
|
| MD5 |
fa0c1553495aab5e87ce0d9f138d84a7
|
|
| BLAKE2b-256 |
27f806e4645fa9a3a111dfe0e8b7f1489c2540a24b1772201865d8957d19c0dd
|
Provenance
The following attestation bundles were made for pysidwizard-0.1.0.tar.gz:
Publisher:
publish.yml on anarkiwi/pysidwizard
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pysidwizard-0.1.0.tar.gz -
Subject digest:
82836e1e314a88e558c67df3d02e800333fd82ce2bf6a4d3bf9cc8b527182f68 - Sigstore transparency entry: 1611753734
- Sigstore integration time:
-
Permalink:
anarkiwi/pysidwizard@b8eb7dbc0fb9c27d473e2ffd00a797cc39932238 -
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:
publish.yml@b8eb7dbc0fb9c27d473e2ffd00a797cc39932238 -
Trigger Event:
release
-
Statement type:
File details
Details for the file pysidwizard-0.1.0-py3-none-any.whl.
File metadata
- Download URL: pysidwizard-0.1.0-py3-none-any.whl
- Upload date:
- Size: 63.9 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 |
712e3d7067d770c4d2a6dd35e1b74b7d46869b5b59b98c892df91bfea78c5317
|
|
| MD5 |
95cda466d9e0720117b5589a434b8abb
|
|
| BLAKE2b-256 |
2546ad19b53d9b639d9fb9948b4e1a8c87cac35dc42581f0a5022c8bb6675f11
|
Provenance
The following attestation bundles were made for pysidwizard-0.1.0-py3-none-any.whl:
Publisher:
publish.yml on anarkiwi/pysidwizard
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pysidwizard-0.1.0-py3-none-any.whl -
Subject digest:
712e3d7067d770c4d2a6dd35e1b74b7d46869b5b59b98c892df91bfea78c5317 - Sigstore transparency entry: 1611754141
- Sigstore integration time:
-
Permalink:
anarkiwi/pysidwizard@b8eb7dbc0fb9c27d473e2ffd00a797cc39932238 -
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:
publish.yml@b8eb7dbc0fb9c27d473e2ffd00a797cc39932238 -
Trigger Event:
release
-
Statement type: