Skip to main content

ZX Spectrum AY music toolkit: MIDI to PT3, software-AY audition, self-playing .tap/.sna packaging, and PT3 back to MIDI.

Project description

spectrumizer

PyPI CI License: MIT Python Live demos

Generate ZX Spectrum AY music (.pt3) from MIDI — and get the notes back out of a .pt3 into MIDI too. The output is a standard Vortex Tracker / Sergey Bulba PT3 module, so anything it produces drops straight into a Spectrum game that ships a PT3 replayer.

Instead of typing every arrangement note-by-note in Python, you feed a source file and spectrumizer arranges it down to the AY's 3 channels (+ noise).

▶ Hear it in your browser: demo page · or the Demos section below.

⚠️ Licence: spectrumizer does not launder licences. The licence of the SOURCE governs the OUTPUT — a .pt3 from a copyrighted MIDI is still copyrighted. Only bundle public-domain or your own music into a release. Read LICENSING.md.

The toolkit

Four commands, one round trip:

Command Does Details
spectrumizer MIDI → .pt3 — arranges any MIDI down to the AY's 3 channels: faithful or chiptune style, velocity dynamics, GM drums, chord arpeggios, echo, vibrato, buzzer bass, auto-transpose. Use
spectrumizer-play .pt3 → your speakers — auditions any module through a built-in software AY (stereo WAV; no Spectrum, no extra software). Listen
spectrumizer-pack .pt3.tap / .sna — a self-playing tape or 128K snapshot for an emulator or real hardware, with a title screen. Package
spectrumizer-export .pt3 → MIDI — the reverse pipeline: recovers the notes from a module (yours or anyone's) into a DAW-ready file. Export

Together they close the loop compose → hear → ship → recover: a .pt3 you generated, downloaded or recovered can be auditioned, packaged for hardware, exported back to notes, edited, and re-spectrumized.

Install

pip install spectrumizer    # from PyPI — installs spectrumizer / spectrumizer-play / spectrumizer-pack / spectrumizer-export

The spectrumizer, spectrumizer-play and spectrumizer-export commands are pure-Python. spectrumizer-pack (package a .pt3 for an emulator) additionally needs sjasmplus on PATH to assemble the player.

Or from a clone (for development):

pip install -e .                                                    # editable install
# ...or without installing the package:
python3 -m venv .venv && . .venv/bin/activate && pip install -r requirements.txt

All deps are pure-Python (mido), so the same wheels work on Intel & Apple Silicon — no native build step.

Use

# faithful 3-voice reduction
spectrumizer song.mid -o song.pt3                 # or: python -m spectrumizer song.mid -o song.pt3

# chiptune flavour: octave-doubled leads + a kick/snare/hi-hat groove when the
# source has no drum track
spectrumizer song.mid -o song.pt3 --style chiptune

# tune the AY octave by ear, change grid/tempo
spectrumizer song.mid --transpose -12 --rows-per-beat 4 --speed 6

# ...or let it fit the register itself: whole-octave shift (key preserved) into
# the AY sweet spot; any --transpose is applied on top
spectrumizer song.mid --auto-transpose

# module metadata: stored in the PT3 header, read back by trackers, the
# audition player and spectrumizer-pack's title screen
spectrumizer song.mid --name "MY THEME" --author "ME"

# where to restart after the last pattern (a position index, e.g. skip an intro)
spectrumizer song.mid --loop-pos 2

# dynamics: MIDI velocity drives per-note volume (on by default)
spectrumizer song.mid -o song.pt3 --no-dynamics      # ...or flat per-channel volume

# buzzer bass: drive the bass through the AY hardware envelope
spectrumizer song.mid --bass envelope        # pure buzzer — envelope is the oscillator (deep, coarse pitch)
spectrumizer song.mid --bass envelope-tone   # tone keeps the pitch, envelope adds the buzz (any register)

# chord arpeggios: channel C plays each chord's root and cycles the matching
# interval ornament — one channel implies the whole chord (the classic AY trick).
# Recognises major/minor triads, dominant/major/minor sevenths and sus2/sus4.
spectrumizer song.mid --arps
spectrumizer song.mid --arps --arp-speed 2   # hold each tone 2 frames: audible ripple

# echo: channel C repeats the lead half a beat later, quieter (the other classic)
spectrumizer song.mid --echo

# delayed vibrato on the lead (sub-semitone, encoded inside the PT3 sample)
spectrumizer song.mid --vibrato

# generate and immediately hear it (renders through a software AY, then plays)
spectrumizer song.mid -o song.pt3 --play

Run spectrumizer --help for all flags. No MIDI at hand? python examples/make_example_midi.py regenerates four public-domain test pieces in examples/ (Ode to Joy, Pachelbel's Canon, Korobeiniki, Greensleeves).

The flags compose--style chiptune --vibrato --bass envelope-tone --arps is a valid (and good) combination. The only contention is channel C, which is one channel: real GM drums in the source always take it (the harmony fills the gaps between hits); otherwise --arps / --echo (mutually exclusive) replace its default voice; otherwise chiptune puts synth drums there and faithful the harmony. Every run prints what it decided:

spectrumizer: greensleeves.mid -> song.pt3
  style=faithful  speed=7  tempo~107.1bpm (source 110.0)  patterns=3  bytes=989
  A=lead(36)  B=bass(16)  C=arp(36)

speed is whole frames per row at 50 Hz, so the playable tempo is quantised — the stats show the tempo the Spectrum will actually play, with the source tempo alongside when they differ.

From Python

The CLIs are thin wrappers — every stage is a library call:

from spectrumizer.inputs.midi import load_midi
from spectrumizer.arrange import arrange

pt3, stats = arrange(load_midi("song.mid"), style="chiptune", arps=True)
with open("song.pt3", "wb") as f:
    f.write(pt3)

…and the reverse direction:

from spectrumizer.pt3.player import parse_module
from spectrumizer.export import module_to_song, write_midi

with open("song.pt3", "rb") as f:
    song = module_to_song(parse_module(f.read()))
write_midi(song, "song.mid")

How it works

MIDI ─(inputs/midi.py)→ IR ─(arrange/)→ 3 AY channels ─(pt3/)→ .pt3
.pt3 ─(pt3/player.py)→ rows ─(export.py)→ IR ─(mido)→ MIDI        ← the reverse
  • spectrumizer/inputs/ — MIDI → IR (via mido). Tempo changes are folded into one fixed grid (PT3 has a single global speed): the tempo heard longest becomes the reference and other sections are time-scaled onto it, so wall-clock timing is preserved.
  • spectrumizer/pt3/ — the proven PT3 emitter (note encoding, channel packer, samples, ornaments, file writer). The byte format is verified against the real player; don't change it blindly.
  • spectrumizer/arrange/ — the hard part:
    • quantize — map time to PT3's row grid (derive speed from tempo).
    • reduce — peel the source polyphony into ≤3 monophonic lines (lead / bass / harmony) via a greedy high/low "skyline".
    • embellish — extra voices: octave leads + synth drums (a kick/snare backbeat with closed hats on the off-eighths and an open hat closing each bar; chiptune style), chord arpeggios (--arps) — each source chord becomes its root note plus the matching interval ornament cycling at frame rate, so one AY channel implies the full chord (arrange/chords.py recognises major/minor triads, dominant/major/minor sevenths and sus2/sus4; --arp-speed N holds each tone N frames, turning the 50 Hz blur into an audible ripple) — and echo (--echo) — the lead repeated half a beat later, quieter, on channel C.
    • dynamics — MIDI velocity → per-note AY volume, normalised so the piece's loudest note hits each channel's ceiling (on by default; --no-dynamics).
    • auto-transpose (--auto-transpose) — shift the piece by whole octaves (key preserved) so the duration-weighted bulk of its notes sits in the AY's comfortable register (up to PT3 octave 6 — clear of the coarse-pitch top octaves — and off the format floor), instead of tuning --transpose by ear. A well-registered piece shifts 0.
    • vibrato (--vibrato) — the lead's sustain wobbles the tone period (±3 units at 6.25 Hz, delayed past the attack). PT3 samples carry a signed per-tick tone offset, so the vibrato lives inside the instrument and costs nothing in the patterns; an --echo inherits it.
    • buzzer bass — --bass envelope routes channel B through the AY hardware envelope at each note's pitch (the deep AY buzzer; pitch is coarse, best low). --bass envelope-tone keeps the tone for exact pitch and uses the envelope only for the buzz.
    • Channel allocation: A = lead, B = bass, C = real drums if present (the harmony fills the rows between hits — drums and chords time-share the channel), else chord arps (--arps), else echo (--echo), else synth drums (chiptune), else harmony (faithful). GM kits map to the AY noise drums: kick, snare, and hi-hats — closed/pedal hats and rides tick short and quiet, open hats and crashes ring a sizzling tail; simultaneous hits collapse to the strongest (kick > snare > cymbals).
    • pattern dedup — identical 64-row patterns are stored once and replayed through the PT3 position list (repeats cost 1 byte, not a pattern).
  • spectrumizer/export.py — the reverse arranger: module_to_song rebuilds the IR from a decoded module (percussion recognised by noise colour, arp ornaments expanded back into chords) and write_midi serialises it as a type-1 MIDI file.
  • spectrumizer/ir.py — the source-agnostic note model in the middle: input adapters produce it, the arranger consumes it, the export rebuilds it.

PT3 invariants baked in (from the player source)

The player ends a pattern when channel A hits its 0x00 terminator and then resets all three channels — so every channel of a pattern encodes exactly ROWS_PER_PATTERN (64) rows, and row 0 is never an empty rest (the packer drops leading rests). arrange/model.py enforces both.

Listen to it (no Spectrum needed)

spectrumizer ships its own playback path: a small software AY-3-8910 that renders a .pt3 to a stereo .wav (classic ABC panning — A left, B centre, C right) and plays it through your system audio player (afplay on macOS; ffplay / aplay / paplay / sox elsewhere).

spectrumizer-play song.pt3            # render song.wav and play it
spectrumizer-play song.pt3 --no-play  # just write the .wav
spectrumizer-play song.pt3 --loops 3      # play the loop section three times
spectrumizer-play song.pt3 --seconds 30   # cap length, looping the song's tail
spectrumizer-play song.pt3 --rate 22050   # faster render (lower fidelity)
spectrumizer-play song.pt3 --tuning equal # equal-tempered instead of the PT3 table
spectrumizer-play song.pt3 --stereo mono  # mono (default abc = A-left/B-centre/C-right)
spectrumizer-play song.pt3 --separation 1.0  # stereo width 0..1 (default 0.7)
spectrumizer-play song.pt3 --noise-period 5  # force a noise period (default: the module's real one)

The synth (spectrumizer/audio.py) plus the PT3 interpreter (spectrumizer/pt3/player.py, the inverse of the encoder) only implement the subset of PT3 this tool emits — notes, OFF, sample/ornament/volume, NtSkip, and the AY hardware envelope (all 16 R13 shapes, so buzzer-bass modules audition too). Pointing it at a foreign module (full Vortex Tracker output) is detected, not silent: it warns about tokens outside that subset and about a non-default tone table. Pitch uses the exact PT3 tone table (the table-1 periods from the real Bulba player, so notes land where the chip puts them; pass --tuning equal for the old equal-tempered approximation). Treat it as a faithful audition, not a cycle-exact emulation — for the real chip, package the .pt3 for an emulator (below).

Hear it on a real Spectrum / emulator

A .pt3 is only music data. spectrumizer-pack wraps it with Sergey Bulba's PT3 replayer + a tiny loader and assembles a self-playing tape or snapshot you can load in Fuse / ZEsarUX (or on real hardware):

spectrumizer-pack song.pt3 -o song.tap     # autoloading tape (BASIC + CODE)
spectrumizer-pack song.pt3 --sna song.sna  # 128K snapshot (boots straight into the tune)
spectrumizer-pack song.pt3 --tap a.tap --sna a.sna   # both at once
spectrumizer-pack song.pt3 --name MYTUNE   # tape/CODE block name (<=10 chars)

The music uses the AY, so it needs a 128K machine: the .sna is a 128K snapshot and just plays; the .tap must be loaded in 128K / 128-BASIC mode (the 48K loader has no AY). Needs sjasmplus on PATH to assemble the player. The bundled player is Bulba's, under its own terms — see LICENSING.md.

While it plays, the program shows a small title screen — a colour-cycling spectrumizer logo plus the module's title and author, read straight from the PT3 header, with the ROM font on black.

Get the notes back out (PT3 → MIDI)

The pipeline also runs in reverse: spectrumizer-export decodes a .pt3 (with the same interpreter the audition uses) and writes a standard MIDI file — to study a module, edit it in a DAW, or re-spectrumize it after changes.

spectrumizer-export song.pt3              # → song.mid
spectrumizer-export song.pt3 --no-merge   # keep pattern-boundary re-attacks
spectrumizer-export song.pt3 --rows-per-beat 8  # halve the tempo (barlines only,
                                                # PT3 doesn't store the grid)

# the full circle: recover a module, edit it in a DAW, re-spectrumize it
spectrumizer-export old-module.pt3 -o draft.mid
spectrumizer draft.mid --style chiptune --arps -o new-module.pt3 --play

What you get: channels A/B/C as three tracks at the module's effective tempo, velocities from the AY volumes, percussive samples (one-shot noise bursts) as GM drums — kick / snare / closed / open hat by noise colour — and chord-arp ornaments expanded back into the chords they fake (an --arps module exports real Am7 stacks, not a lone root). Notes the encoder re-attacked at pattern boundaries are merged back into one held note — at the PT3 level a re-attack and a genuinely repeated note are the same bytes there, so a repeated note landing exactly on a boundary merges too; --no-merge keeps every attack.

What you don't: timbre. Samples, buzzer bass and noise periods have no MIDI analogue, and a spectrumizer-made module returns the 3-channel arrangement, not your original source (the reduction is lossy by design; echo and octave embellishments export as the notes they play). Foreign modules audition-grade only: tokens outside the decoded subset are skipped with a warning. And the usual reminder: exporting somebody's module to MIDI does not clear its licence.

Demos

Hear every mode in your browser on the demo page (GitHub Pages, nothing to install) — or click a clip to play it in GitHub's file viewer. All clips are the bundled public-domain examples rendered through the built-in software AY: ode-to-joy.mid for most, pachelbel-canon.mid where a low ground bass or chords shine (buzzer, arps), korobeiniki.mid (the Tetris folk tune, with a real GM drum track) for the drums clip, and greensleeves.mid (the traditional tune, harmonised with 7th/sus chords) for the arps-v2 clip. Regenerate with pip install -e ".[demos]" && python examples/make_demos.py.

Demo What it shows
Faithful 3-voice reduction
Chiptune octave lead + synth drums (off-beat hi-hats included)
Chord arpeggios triads faked on one channel via 50 Hz ornaments (--arps)
Seventh & sus arpeggios Greensleeves: Am7 / Fmaj7 / G7 / Esus4 — four-note chords from one channel
Echo the lead repeated half a beat later, quieter (--echo)
Vibrato delayed sub-semitone vibrato, encoded inside the sample (--vibrato)
Real drums + harmony a GM drum track (kick/snare/hi-hats) and the chords time-sharing channel C
Buzzer (pure) bass = the AY hardware envelope, tone off (--bass envelope)
Buzzer (tone+env) envelope buzz, tone keeps the pitch (--bass envelope-tone)
No dynamics flat volume — vs the velocity dynamics
Equal-tempered vs the exact PT3 tone table
Mono vs the default ABC stereo
Everything at once the flags compose: octave lead + vibrato + buzzer bass + rippling arps (--arp-speed 2), an octave down

Every demo also ships as an executable 128K snapshot in docs/audio/ (<demo>.sna, made with spectrumizer-pack) — load it in any Spectrum emulator to hear the real Z80 player instead of the software AY. Demos that differ only in playback flags (--tuning, --stereo) share chiptune.sna: those are audition-player options, not part of the module.

Tests

pip install -e ".[dev]"     # installs pytest
pytest -q

Status

  • Generate: MIDI → PT3, faithful + chiptune, velocity-driven dynamics, hi-hat percussion (GM cymbals mapped, off-beat hats in the synth groove), chord arpeggios with the full triad/7th/sus vocabulary (--arps, --arp-speed), echo (--echo), lead vibrato (--vibrato), auto-transpose into the AY register (--auto-transpose), and buzzer bass through the AY hardware envelope (--bass envelope / envelope-tone).
  • Audition: built-in software-AY playback to a stereo WAV — exact PT3 tone table, real per-frame noise period, ABC panning, and the AY hardware envelope generator (spectrumizer-play / --play).
  • Package: wrap a .pt3 (+ Bulba's replayer) into a self-playing .tap / 128K .sna for an emulator or real hardware (spectrumizer-pack).
  • Export: PT3 → MIDI — the reverse pipeline: decode a module back into notes and take them to a DAW (spectrumizer-export).

The feature set is complete — the project is maintained, not growing.

Origin

spectrumizer grew out of hand-written, per-track PT3 composer scripts for a ZX Spectrum game, generalising them into a single reusable arranger. It is now a standalone, game-agnostic tool.

Credits

  • Sergey Bulba — the PT3 module format and the Vortex Tracker / PT3 replayer this tool targets, including the NoteTableCreator tone-table data the audition synth uses for exact Spectrum pitches.
  • Ivan Roshin — NoteTableCreator, the source of those packed AY tone tables.

These credits acknowledge the format and reference data; spectrumizer's encoder, decoder and synth are independent implementations (see LICENSE).

Licence

MIT © Miguel Ángel Esteve Marco. Note: the MIT licence covers spectrumizer's own code, not the music you run through it — see LICENSING.md.

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

spectrumizer-0.2.1.tar.gz (81.6 kB view details)

Uploaded Source

Built Distribution

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

spectrumizer-0.2.1-py3-none-any.whl (68.2 kB view details)

Uploaded Python 3

File details

Details for the file spectrumizer-0.2.1.tar.gz.

File metadata

  • Download URL: spectrumizer-0.2.1.tar.gz
  • Upload date:
  • Size: 81.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for spectrumizer-0.2.1.tar.gz
Algorithm Hash digest
SHA256 8509aa8f1c7b088be39a278941aaa92f5e4088368f66d0215e95ac3aa72cd7fc
MD5 71bfb862a1bfe028be670a047a94f8a7
BLAKE2b-256 7c633234b65c2c75ae95ff934a743dc2e42eebd8782e9cae3540a62a92d8962b

See more details on using hashes here.

Provenance

The following attestation bundles were made for spectrumizer-0.2.1.tar.gz:

Publisher: publish.yml on revengator/spectrumizer

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

File details

Details for the file spectrumizer-0.2.1-py3-none-any.whl.

File metadata

  • Download URL: spectrumizer-0.2.1-py3-none-any.whl
  • Upload date:
  • Size: 68.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for spectrumizer-0.2.1-py3-none-any.whl
Algorithm Hash digest
SHA256 89fd199a2d9c1ac09f58023f497c7874e041b3ba30784125181b89300ba92ff0
MD5 5cf0b8b04888096ccdbad3970671e370
BLAKE2b-256 c4699c48911eac4519b1e84f76455ffa1dac1db17bc8393f3e6a374175344b92

See more details on using hashes here.

Provenance

The following attestation bundles were made for spectrumizer-0.2.1-py3-none-any.whl:

Publisher: publish.yml on revengator/spectrumizer

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