Pure-Python reader, writer, and player for Music Assembler SID songs
Project description
pymusicassembler
A standalone, pure-Python reader, writer, and player for Music Assembler C64 SID songs (the Richard Bayliss / Eric Campbell era player). It parses a Music Assembler tune into a typed song model, runs the playroutine to produce byte-exact per-frame SID register output, renders WAV through an emulated SID, and writes songs back to a byte-identical tune image.
Read/write/play/register-log is pure stdlib; only WAV rendering needs the
optional audio extra (pyresidfp).
pip install pymusicassembler # reader/writer/player/reglog
pip install pymusicassembler[audio] # + WAV rendering via pyresidfp
Quick start
import pymusicassembler as ma
song = ma.read("tune.sid") # PSID/.sid or bare .prg
# Per-frame SID register writes (changed registers only, after frame 0).
for writes in ma.iter_frames(song, max_frames=50 * 60):
... # writes: list[(register, value)]
# Forward-filled 25-register-per-frame snapshot grid (the oracle form).
grid = ma.render_grid(song, nframes=400)
# Register log (clock reg val triples), and WAV via an emulated SID.
ma.write_reglog(ma.iter_register_writes(song, max_frames=2500), "tune.reglog")
ma.render_wav(song, "tune.wav", seconds=30) # needs the audio extra
# Edit and re-emit, byte-stable.
song.patterns[0].data[0] = ma.constants.DUR_MIN # tweak a pattern byte
ma.write(song, "tune-edited.sid")
CLI:
pymusicassembler info tune.sid
pymusicassembler reglog tune.sid tune.reglog --seconds 30
pymusicassembler wav tune.sid tune.wav --seconds 30 --model 8580
pymusicassembler resave tune.sid out.sid # byte-identical round-trip
Public API
read(src) -> Song/parse(bytes) -> Song— read a.sid/.prg.write(song, dst, container="auto"|"psid"|"prg")— write a tune image;autore-emits the original container byte-identically.build(song) -> bytes— the raw C64 image;validate(song).Player(song)—.play_frame() -> list[(reg, val)],.regs.iter_frames(song, max_frames)— per-frame writes.render_grid(song, nframes) -> list[list[int]]— forward-filled grid.render_samples/render_wav/write_wav— audio (pyresidfp extra).iter_register_writes/read_reglog/write_reglog/RegWrite.- Model:
Song,Orderlist,OrderEntry,Pattern,Instrument, and the decoded pattern eventsNoteEvent/InstrumentEvent/DurationEvent(Pattern.events()). - Errors:
MusicAssemblerError,SidParseError,SongValidationError.
The Music Assembler format
Three independent per-voice tracks. The player binary is identical across tunes; only its DATA and the addresses that data lives at differ, so the reader discovers each table base from the player code's 16-bit operands (relocation-safe — no fixed-address assumption):
- Orderlists — one per voice, 2-byte entries
(pattern_id, transpose << 4 | repeat), terminated by$FF(loop to start); pattern id$FEis a silence marker. - Patterns — player bytecode:
< $60note (+ a control byte, with two inline argument bytes for a filter ($80bit) or vibrato/arp ($20bit) command);$60–$7Fbare duration;$80–$9Finstrument select (id = value & $1F);$A0–$FElong duration;$FFterminator. - Instruments — 8-byte records: attack/decay, sustain/release, waveform, pulse-width init + sweep step, arp speed + step, porta/effect flags.
- Per-frame DSP — tempo divider, 16-bit pulse-width sweep, vibrato/arp frequency accumulators, the gate/retrigger idiom, and a self-modified filter-cutoff sweep.
Notes vs the decompiled reference (decompile.c)
The player was recovered from its disassembly and validated byte-exact against
the preframr-sidtrace oracle. Two facts a naive read of the Ghidra
decompile (decompile.c) gets wrong, recovered from the raw disassembly:
- The bare-duration token (
$60–$7F) DOES advance the pattern cursor (the$10c3JMP $1187); the decompile's inlining made it look like an early return. - Init does not clear most work RAM. The init routine zeroes only
$1081–$1090(per-voice cursors / pattern id) and reloads pattern id / transpose / repeat; every other per-voice work byte AND the self-modified filter envelope ($129eaccumulator,$1296gate,$12a0step, the$1265/$126avoice-0 trigger-reset immediates) keep their loaded-image values. A tune saved mid-playback bakes live state in, so the player seeds from those image bytes — otherwise its first frames diverge.
Byte-exact verdict
Validated against the preframr-sidtrace register oracle (PAL, 19656
cycles/frame, forward-filled, one leading silent play-call aligned over):
| Tune | Author | Frames | Residual |
|---|---|---|---|
| Space Invadarz on Vacation | Richard Bayliss | 828 | 0 (byte-exact) |
| 7 Years | Eric Campbell | 828 | 0 (byte-exact) |
| 3LUX Intro | — | 827 | 0 (byte-exact) |
Round-trip: build(read(image)) == image and the full .sid container
re-emits byte-identically for every tune; read(write(song)) == song.
Tests
Test tunes are HVSC copyright works and are never committed. They are
fetched on demand (python scripts/fetch_tunes.py) into a gitignored cache;
the byte-exact tests validate against the live preframr-sidtrace binary when
$SIDTRACE_BIN is set, else against the committed frozen oracle grids in
tests/fixtures/, so CI passes with neither the binary nor the tunes.
pip install -e ".[dev]"
./run_tests.sh
SIDTRACE_BIN=/path/to/sidtrace ./run_tests.sh # validate live, too
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 pymusicassembler-0.1.0.tar.gz.
File metadata
- Download URL: pymusicassembler-0.1.0.tar.gz
- Upload date:
- Size: 38.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 |
659010d4dea62bb476481c9a4ce204f92bf80fbcbf2c7ed369c4bb0971d4e29d
|
|
| MD5 |
1ff3c4e8eae2e740d21084bc39782715
|
|
| BLAKE2b-256 |
d505c9a15bb5a42a43fe18300394ac63a7a1c350d2dc1ec66750322f74cc09e1
|
Provenance
The following attestation bundles were made for pymusicassembler-0.1.0.tar.gz:
Publisher:
release.yml on anarkiwi/pymusicassembler
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pymusicassembler-0.1.0.tar.gz -
Subject digest:
659010d4dea62bb476481c9a4ce204f92bf80fbcbf2c7ed369c4bb0971d4e29d - Sigstore transparency entry: 1961260583
- Sigstore integration time:
-
Permalink:
anarkiwi/pymusicassembler@3ea0203fb93827e7738c39dd4c797053c2265eb4 -
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:
release.yml@3ea0203fb93827e7738c39dd4c797053c2265eb4 -
Trigger Event:
release
-
Statement type:
File details
Details for the file pymusicassembler-0.1.0-py3-none-any.whl.
File metadata
- Download URL: pymusicassembler-0.1.0-py3-none-any.whl
- Upload date:
- Size: 34.1 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 |
574c2bfcb18060e75f8c74b9a8dcb1aca0f144d14cd486ca613beacae6e370ad
|
|
| MD5 |
3fa5c98a7afc2afaa72b52c7a5966f2f
|
|
| BLAKE2b-256 |
1303288fcd1365cd4957ea028cea07a231fdc24acc547d3c7cb49a6f5afdb55f
|
Provenance
The following attestation bundles were made for pymusicassembler-0.1.0-py3-none-any.whl:
Publisher:
release.yml on anarkiwi/pymusicassembler
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pymusicassembler-0.1.0-py3-none-any.whl -
Subject digest:
574c2bfcb18060e75f8c74b9a8dcb1aca0f144d14cd486ca613beacae6e370ad - Sigstore transparency entry: 1961261013
- Sigstore integration time:
-
Permalink:
anarkiwi/pymusicassembler@3ea0203fb93827e7738c39dd4c797053c2265eb4 -
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:
release.yml@3ea0203fb93827e7738c39dd4c797053c2265eb4 -
Trigger Event:
release
-
Statement type: