Skip to main content

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; auto re-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 events NoteEvent / 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 $FE is a silence marker.
  • Patterns — player bytecode: < $60 note (+ a control byte, with two inline argument bytes for a filter ($80 bit) or vibrato/arp ($20 bit) command); $60–$7F bare duration; $80–$9F instrument select (id = value & $1F); $A0–$FE long duration; $FF terminator.
  • 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 $10c3 JMP $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 ($129e accumulator, $1296 gate, $12a0 step, the $1265/$126a voice-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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pymusicassembler-0.1.0.tar.gz (38.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pymusicassembler-0.1.0-py3-none-any.whl (34.1 kB view details)

Uploaded Python 3

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

Hashes for pymusicassembler-0.1.0.tar.gz
Algorithm Hash digest
SHA256 659010d4dea62bb476481c9a4ce204f92bf80fbcbf2c7ed369c4bb0971d4e29d
MD5 1ff3c4e8eae2e740d21084bc39782715
BLAKE2b-256 d505c9a15bb5a42a43fe18300394ac63a7a1c350d2dc1ec66750322f74cc09e1

See more details on using hashes here.

Provenance

The following attestation bundles were made for pymusicassembler-0.1.0.tar.gz:

Publisher: release.yml on anarkiwi/pymusicassembler

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file pymusicassembler-0.1.0-py3-none-any.whl.

File metadata

File hashes

Hashes for pymusicassembler-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 574c2bfcb18060e75f8c74b9a8dcb1aca0f144d14cd486ca613beacae6e370ad
MD5 3fa5c98a7afc2afaa72b52c7a5966f2f
BLAKE2b-256 1303288fcd1365cd4957ea028cea07a231fdc24acc547d3c7cb49a6f5afdb55f

See more details on using hashes here.

Provenance

The following attestation bundles were made for pymusicassembler-0.1.0-py3-none-any.whl:

Publisher: release.yml on anarkiwi/pymusicassembler

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page