spectrumizer 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. spectrumizer-0.1.0/LICENSE +21 -0
  2. spectrumizer-0.1.0/PKG-INFO +195 -0
  3. spectrumizer-0.1.0/README.md +166 -0
  4. spectrumizer-0.1.0/pyproject.toml +49 -0
  5. spectrumizer-0.1.0/setup.cfg +4 -0
  6. spectrumizer-0.1.0/spectrumizer/__init__.py +13 -0
  7. spectrumizer-0.1.0/spectrumizer/__main__.py +6 -0
  8. spectrumizer-0.1.0/spectrumizer/arrange/__init__.py +126 -0
  9. spectrumizer-0.1.0/spectrumizer/arrange/embellish.py +38 -0
  10. spectrumizer-0.1.0/spectrumizer/arrange/model.py +74 -0
  11. spectrumizer-0.1.0/spectrumizer/arrange/quantize.py +36 -0
  12. spectrumizer-0.1.0/spectrumizer/arrange/reduce.py +48 -0
  13. spectrumizer-0.1.0/spectrumizer/audio.py +195 -0
  14. spectrumizer-0.1.0/spectrumizer/cli.py +102 -0
  15. spectrumizer-0.1.0/spectrumizer/inputs/__init__.py +1 -0
  16. spectrumizer-0.1.0/spectrumizer/inputs/midi.py +81 -0
  17. spectrumizer-0.1.0/spectrumizer/inputs/musicxml.py +21 -0
  18. spectrumizer-0.1.0/spectrumizer/ir.py +51 -0
  19. spectrumizer-0.1.0/spectrumizer/play.py +86 -0
  20. spectrumizer-0.1.0/spectrumizer/pt3/__init__.py +24 -0
  21. spectrumizer-0.1.0/spectrumizer/pt3/encode.py +159 -0
  22. spectrumizer-0.1.0/spectrumizer/pt3/ornaments.py +51 -0
  23. spectrumizer-0.1.0/spectrumizer/pt3/player.py +292 -0
  24. spectrumizer-0.1.0/spectrumizer/pt3/samples.py +82 -0
  25. spectrumizer-0.1.0/spectrumizer/pt3/writer.py +113 -0
  26. spectrumizer-0.1.0/spectrumizer.egg-info/PKG-INFO +195 -0
  27. spectrumizer-0.1.0/spectrumizer.egg-info/SOURCES.txt +33 -0
  28. spectrumizer-0.1.0/spectrumizer.egg-info/dependency_links.txt +1 -0
  29. spectrumizer-0.1.0/spectrumizer.egg-info/entry_points.txt +3 -0
  30. spectrumizer-0.1.0/spectrumizer.egg-info/requires.txt +7 -0
  31. spectrumizer-0.1.0/spectrumizer.egg-info/top_level.txt +1 -0
  32. spectrumizer-0.1.0/tests/test_arrange.py +107 -0
  33. spectrumizer-0.1.0/tests/test_midi.py +35 -0
  34. spectrumizer-0.1.0/tests/test_player.py +108 -0
  35. spectrumizer-0.1.0/tests/test_pt3_writer.py +55 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Miguel Ángel Esteve Marco
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,195 @@
1
+ Metadata-Version: 2.4
2
+ Name: spectrumizer
3
+ Version: 0.1.0
4
+ Summary: Generate ZX Spectrum AY (PT3) music from MIDI and other sources.
5
+ Author-email: Miguel Ángel Esteve Marco <esteve@telemaco.es>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/revengator/spectrumizer
8
+ Project-URL: Repository, https://github.com/revengator/spectrumizer
9
+ Project-URL: Issues, https://github.com/revengator/spectrumizer/issues
10
+ Keywords: zx-spectrum,ay-3-8910,pt3,vortex-tracker,chiptune,midi
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3 :: Only
18
+ Classifier: Topic :: Multimedia :: Sound/Audio :: Conversion
19
+ Classifier: Topic :: Multimedia :: Sound/Audio :: MIDI
20
+ Requires-Python: >=3.10
21
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE
23
+ Requires-Dist: mido<2.0,>=1.3
24
+ Provides-Extra: dev
25
+ Requires-Dist: pytest>=7; extra == "dev"
26
+ Provides-Extra: demos
27
+ Requires-Dist: lameenc>=1.5; extra == "demos"
28
+ Dynamic: license-file
29
+
30
+ # spectrumizer
31
+
32
+ [![CI](https://github.com/revengator/spectrumizer/actions/workflows/ci.yml/badge.svg)](https://github.com/revengator/spectrumizer/actions/workflows/ci.yml)
33
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
34
+ [![Python](https://img.shields.io/badge/python-3.10%2B-blue.svg)](pyproject.toml)
35
+ [![Live demos](https://img.shields.io/badge/%E2%96%B6-live%20demos-brightgreen.svg)](https://revengator.github.io/spectrumizer/)
36
+
37
+ Generate **ZX Spectrum AY music (`.pt3`)** from public sources (MIDI now;
38
+ MusicXML/scores planned). The output is a standard Vortex Tracker / Sergey
39
+ Bulba PT3 module, so anything it produces drops straight into a Spectrum game
40
+ that ships a PT3 replayer.
41
+
42
+ Instead of typing every arrangement note-by-note in Python, you feed a source
43
+ file and spectrumizer arranges it down to the AY's 3 channels (+ noise).
44
+
45
+ **▶ Hear it in your browser:** [demo page](https://revengator.github.io/spectrumizer/)
46
+ · or the [Demos](#demos) section below.
47
+
48
+ > ⚠️ **Licence:** spectrumizer does **not** launder licences. The licence of the
49
+ > SOURCE governs the OUTPUT — a `.pt3` from a copyrighted MIDI is still
50
+ > copyrighted. Only bundle **public-domain or your own** music into a release.
51
+ > Read [`LICENSING.md`](LICENSING.md).
52
+
53
+ ## Install
54
+
55
+ ```bash
56
+ pip install -e . # from a clone — installs the `spectrumizer` command + deps
57
+ ```
58
+
59
+ Or, without installing the package:
60
+
61
+ ```bash
62
+ python3 -m venv .venv && . .venv/bin/activate
63
+ pip install -r requirements.txt
64
+ ```
65
+
66
+ All deps are pure-Python (`mido`), so the same wheels work on Intel & Apple
67
+ Silicon — no native build step.
68
+
69
+ ## Use
70
+
71
+ ```bash
72
+ # faithful 3-voice reduction
73
+ spectrumizer song.mid -o song.pt3 # or: python -m spectrumizer song.mid -o song.pt3
74
+
75
+ # chiptune flavour: octave-doubled leads + synth drums when the source has none
76
+ spectrumizer song.mid -o song.pt3 --style chiptune
77
+
78
+ # tune the AY octave by ear, change grid/tempo
79
+ spectrumizer song.mid --transpose -12 --rows-per-beat 4 --speed 6 \
80
+ --name "MY THEME" --author "ME"
81
+
82
+ # dynamics: MIDI velocity drives per-note volume (on by default)
83
+ spectrumizer song.mid -o song.pt3 --no-dynamics # ...or flat per-channel volume
84
+
85
+ # generate and immediately hear it (renders through a software AY, then plays)
86
+ spectrumizer song.mid -o song.pt3 --play
87
+ ```
88
+
89
+ Run `spectrumizer --help` for all flags.
90
+
91
+ ## How it works
92
+
93
+ ```
94
+ MIDI ─(inputs/midi.py, mido)→ IR ─(arrange/)→ 3 AY channels ─(pt3/)→ .pt3
95
+ ```
96
+
97
+ - **`spectrumizer/pt3/`** — the proven PT3 emitter (note encoding, channel packer,
98
+ samples, ornaments, file writer). The byte format is verified against the real
99
+ player; don't change it blindly.
100
+ - **`spectrumizer/arrange/`** — the hard part:
101
+ - `quantize` — map time to PT3's row grid (derive `speed` from tempo).
102
+ - `reduce` — peel the source polyphony into ≤3 monophonic lines (lead / bass /
103
+ harmony) via a greedy high/low "skyline".
104
+ - `embellish` — chiptune passes (octave leads, synth drums; chord arps planned).
105
+ - dynamics — MIDI velocity → per-note AY volume, normalised so the piece's
106
+ loudest note hits each channel's ceiling (on by default; `--no-dynamics`).
107
+ - Channel allocation: **A = lead, B = bass, C =** real drums if present, else
108
+ synth drums (chiptune), else harmony (faithful).
109
+ - **`spectrumizer/ir.py`** — the source-agnostic note model both inputs target.
110
+
111
+ ### PT3 invariants baked in (from the player source)
112
+
113
+ The player ends a pattern when **channel A** hits its `0x00` terminator and then
114
+ resets all three channels — so every channel of a pattern encodes **exactly**
115
+ `ROWS_PER_PATTERN` (64) rows, and row 0 is never an empty rest (the packer drops
116
+ leading rests). `arrange/model.py` enforces both.
117
+
118
+ ## Listen to it (no Spectrum needed)
119
+
120
+ spectrumizer ships its own playback path: a small software **AY-3-8910** that
121
+ renders a `.pt3` to a stereo `.wav` (classic ABC panning — A left, B centre,
122
+ C right) and plays it through your system audio player (`afplay` on macOS;
123
+ `ffplay` / `aplay` / `paplay` / `sox` elsewhere).
124
+
125
+ ```bash
126
+ spectrumizer-play song.pt3 # render song.wav and play it
127
+ spectrumizer-play song.pt3 --no-play # just write the .wav
128
+ spectrumizer-play song.pt3 --seconds 30 # cap length, looping the song's tail
129
+ spectrumizer-play song.pt3 --rate 22050 # faster render (lower fidelity)
130
+ spectrumizer-play song.pt3 --tuning equal # equal-tempered instead of the PT3 table
131
+ spectrumizer-play song.pt3 --stereo mono # mono (default abc = A-left/B-centre/C-right)
132
+ spectrumizer-play song.pt3 --noise-period 5 # force a noise period (default: the module's real one)
133
+ ```
134
+
135
+ The synth (`spectrumizer/audio.py`) plus the PT3 interpreter
136
+ (`spectrumizer/pt3/player.py`, the inverse of the encoder) only implement the
137
+ subset of PT3 this tool emits — notes, OFF, sample/ornament/volume, NtSkip. Pitch
138
+ uses the **exact PT3 tone table** (the table-1 periods from the real Bulba player,
139
+ so notes land where the chip puts them; pass `--tuning equal` for the old
140
+ equal-tempered approximation). Treat it as a faithful **audition**, not a
141
+ cycle-exact emulation. For the real thing, drop the `.pt3` into the PT3 slot of a
142
+ 128K Spectrum build running a Bulba/Vortex replayer, rebuild, and run it in an
143
+ emulator (e.g. ZEsarUX).
144
+
145
+ ## Demos
146
+
147
+ Hear every mode in your browser on the **[demo page](https://revengator.github.io/spectrumizer/)**
148
+ (GitHub Pages, nothing to install) — or click a clip to play it in GitHub's file
149
+ viewer. All are `examples/ode-to-joy.mid` rendered through the built-in software
150
+ AY; regenerate with `pip install -e ".[demos]" && python examples/make_demos.py`.
151
+
152
+ | Demo | What it shows |
153
+ |---|---|
154
+ | ▶ [Faithful](docs/audio/faithful.mp3) | 3-voice reduction |
155
+ | ▶ [Chiptune](docs/audio/chiptune.mp3) | octave lead + synth drums |
156
+ | ▶ [No dynamics](docs/audio/chiptune-flat.mp3) | flat volume — vs the velocity dynamics |
157
+ | ▶ [Equal-tempered](docs/audio/chiptune-equal.mp3) | vs the exact PT3 tone table |
158
+ | ▶ [Mono](docs/audio/chiptune-mono.mp3) | vs the default ABC stereo |
159
+
160
+ ## Tests
161
+
162
+ ```bash
163
+ pip install -e ".[dev]" # installs pytest
164
+ pytest -q
165
+ ```
166
+
167
+ ## Status
168
+
169
+ - **Generate:** MIDI → PT3, faithful + chiptune, with velocity-driven dynamics.
170
+ - **Audition:** built-in software-AY playback to a stereo WAV — exact PT3 tone
171
+ table, real per-frame noise period, ABC panning (`spectrumizer-play` / `--play`).
172
+ - **Planned:** MusicXML (music21) input, chord arpeggios, AY hardware envelopes
173
+ (buzzer bass), general PT3 playback (envelope/slides), raw-AY/`.vtx` export.
174
+
175
+ ## Origin
176
+
177
+ spectrumizer grew out of hand-written, per-track PT3 composer scripts for a ZX
178
+ Spectrum game, generalising them into a single reusable arranger. It is now a
179
+ standalone, game-agnostic tool.
180
+
181
+ ## Credits
182
+
183
+ - **Sergey Bulba** — the PT3 module format and the Vortex Tracker / PT3 replayer
184
+ this tool targets, including the NoteTableCreator tone-table data the audition
185
+ synth uses for exact Spectrum pitches.
186
+ - **Ivan Roshin** — NoteTableCreator, the source of those packed AY tone tables.
187
+
188
+ These credits acknowledge the format and reference data; spectrumizer's encoder,
189
+ decoder and synth are independent implementations (see [`LICENSE`](LICENSE)).
190
+
191
+ ## Licence
192
+
193
+ [MIT](LICENSE) © Miguel Ángel Esteve Marco. Note: the MIT licence covers
194
+ **spectrumizer's own code**, not the music you run through it — see
195
+ [`LICENSING.md`](LICENSING.md).
@@ -0,0 +1,166 @@
1
+ # spectrumizer
2
+
3
+ [![CI](https://github.com/revengator/spectrumizer/actions/workflows/ci.yml/badge.svg)](https://github.com/revengator/spectrumizer/actions/workflows/ci.yml)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
5
+ [![Python](https://img.shields.io/badge/python-3.10%2B-blue.svg)](pyproject.toml)
6
+ [![Live demos](https://img.shields.io/badge/%E2%96%B6-live%20demos-brightgreen.svg)](https://revengator.github.io/spectrumizer/)
7
+
8
+ Generate **ZX Spectrum AY music (`.pt3`)** from public sources (MIDI now;
9
+ MusicXML/scores planned). The output is a standard Vortex Tracker / Sergey
10
+ Bulba PT3 module, so anything it produces drops straight into a Spectrum game
11
+ that ships a PT3 replayer.
12
+
13
+ Instead of typing every arrangement note-by-note in Python, you feed a source
14
+ file and spectrumizer arranges it down to the AY's 3 channels (+ noise).
15
+
16
+ **▶ Hear it in your browser:** [demo page](https://revengator.github.io/spectrumizer/)
17
+ · or the [Demos](#demos) section below.
18
+
19
+ > ⚠️ **Licence:** spectrumizer does **not** launder licences. The licence of the
20
+ > SOURCE governs the OUTPUT — a `.pt3` from a copyrighted MIDI is still
21
+ > copyrighted. Only bundle **public-domain or your own** music into a release.
22
+ > Read [`LICENSING.md`](LICENSING.md).
23
+
24
+ ## Install
25
+
26
+ ```bash
27
+ pip install -e . # from a clone — installs the `spectrumizer` command + deps
28
+ ```
29
+
30
+ Or, without installing the package:
31
+
32
+ ```bash
33
+ python3 -m venv .venv && . .venv/bin/activate
34
+ pip install -r requirements.txt
35
+ ```
36
+
37
+ All deps are pure-Python (`mido`), so the same wheels work on Intel & Apple
38
+ Silicon — no native build step.
39
+
40
+ ## Use
41
+
42
+ ```bash
43
+ # faithful 3-voice reduction
44
+ spectrumizer song.mid -o song.pt3 # or: python -m spectrumizer song.mid -o song.pt3
45
+
46
+ # chiptune flavour: octave-doubled leads + synth drums when the source has none
47
+ spectrumizer song.mid -o song.pt3 --style chiptune
48
+
49
+ # tune the AY octave by ear, change grid/tempo
50
+ spectrumizer song.mid --transpose -12 --rows-per-beat 4 --speed 6 \
51
+ --name "MY THEME" --author "ME"
52
+
53
+ # dynamics: MIDI velocity drives per-note volume (on by default)
54
+ spectrumizer song.mid -o song.pt3 --no-dynamics # ...or flat per-channel volume
55
+
56
+ # generate and immediately hear it (renders through a software AY, then plays)
57
+ spectrumizer song.mid -o song.pt3 --play
58
+ ```
59
+
60
+ Run `spectrumizer --help` for all flags.
61
+
62
+ ## How it works
63
+
64
+ ```
65
+ MIDI ─(inputs/midi.py, mido)→ IR ─(arrange/)→ 3 AY channels ─(pt3/)→ .pt3
66
+ ```
67
+
68
+ - **`spectrumizer/pt3/`** — the proven PT3 emitter (note encoding, channel packer,
69
+ samples, ornaments, file writer). The byte format is verified against the real
70
+ player; don't change it blindly.
71
+ - **`spectrumizer/arrange/`** — the hard part:
72
+ - `quantize` — map time to PT3's row grid (derive `speed` from tempo).
73
+ - `reduce` — peel the source polyphony into ≤3 monophonic lines (lead / bass /
74
+ harmony) via a greedy high/low "skyline".
75
+ - `embellish` — chiptune passes (octave leads, synth drums; chord arps planned).
76
+ - dynamics — MIDI velocity → per-note AY volume, normalised so the piece's
77
+ loudest note hits each channel's ceiling (on by default; `--no-dynamics`).
78
+ - Channel allocation: **A = lead, B = bass, C =** real drums if present, else
79
+ synth drums (chiptune), else harmony (faithful).
80
+ - **`spectrumizer/ir.py`** — the source-agnostic note model both inputs target.
81
+
82
+ ### PT3 invariants baked in (from the player source)
83
+
84
+ The player ends a pattern when **channel A** hits its `0x00` terminator and then
85
+ resets all three channels — so every channel of a pattern encodes **exactly**
86
+ `ROWS_PER_PATTERN` (64) rows, and row 0 is never an empty rest (the packer drops
87
+ leading rests). `arrange/model.py` enforces both.
88
+
89
+ ## Listen to it (no Spectrum needed)
90
+
91
+ spectrumizer ships its own playback path: a small software **AY-3-8910** that
92
+ renders a `.pt3` to a stereo `.wav` (classic ABC panning — A left, B centre,
93
+ C right) and plays it through your system audio player (`afplay` on macOS;
94
+ `ffplay` / `aplay` / `paplay` / `sox` elsewhere).
95
+
96
+ ```bash
97
+ spectrumizer-play song.pt3 # render song.wav and play it
98
+ spectrumizer-play song.pt3 --no-play # just write the .wav
99
+ spectrumizer-play song.pt3 --seconds 30 # cap length, looping the song's tail
100
+ spectrumizer-play song.pt3 --rate 22050 # faster render (lower fidelity)
101
+ spectrumizer-play song.pt3 --tuning equal # equal-tempered instead of the PT3 table
102
+ spectrumizer-play song.pt3 --stereo mono # mono (default abc = A-left/B-centre/C-right)
103
+ spectrumizer-play song.pt3 --noise-period 5 # force a noise period (default: the module's real one)
104
+ ```
105
+
106
+ The synth (`spectrumizer/audio.py`) plus the PT3 interpreter
107
+ (`spectrumizer/pt3/player.py`, the inverse of the encoder) only implement the
108
+ subset of PT3 this tool emits — notes, OFF, sample/ornament/volume, NtSkip. Pitch
109
+ uses the **exact PT3 tone table** (the table-1 periods from the real Bulba player,
110
+ so notes land where the chip puts them; pass `--tuning equal` for the old
111
+ equal-tempered approximation). Treat it as a faithful **audition**, not a
112
+ cycle-exact emulation. For the real thing, drop the `.pt3` into the PT3 slot of a
113
+ 128K Spectrum build running a Bulba/Vortex replayer, rebuild, and run it in an
114
+ emulator (e.g. ZEsarUX).
115
+
116
+ ## Demos
117
+
118
+ Hear every mode in your browser on the **[demo page](https://revengator.github.io/spectrumizer/)**
119
+ (GitHub Pages, nothing to install) — or click a clip to play it in GitHub's file
120
+ viewer. All are `examples/ode-to-joy.mid` rendered through the built-in software
121
+ AY; regenerate with `pip install -e ".[demos]" && python examples/make_demos.py`.
122
+
123
+ | Demo | What it shows |
124
+ |---|---|
125
+ | ▶ [Faithful](docs/audio/faithful.mp3) | 3-voice reduction |
126
+ | ▶ [Chiptune](docs/audio/chiptune.mp3) | octave lead + synth drums |
127
+ | ▶ [No dynamics](docs/audio/chiptune-flat.mp3) | flat volume — vs the velocity dynamics |
128
+ | ▶ [Equal-tempered](docs/audio/chiptune-equal.mp3) | vs the exact PT3 tone table |
129
+ | ▶ [Mono](docs/audio/chiptune-mono.mp3) | vs the default ABC stereo |
130
+
131
+ ## Tests
132
+
133
+ ```bash
134
+ pip install -e ".[dev]" # installs pytest
135
+ pytest -q
136
+ ```
137
+
138
+ ## Status
139
+
140
+ - **Generate:** MIDI → PT3, faithful + chiptune, with velocity-driven dynamics.
141
+ - **Audition:** built-in software-AY playback to a stereo WAV — exact PT3 tone
142
+ table, real per-frame noise period, ABC panning (`spectrumizer-play` / `--play`).
143
+ - **Planned:** MusicXML (music21) input, chord arpeggios, AY hardware envelopes
144
+ (buzzer bass), general PT3 playback (envelope/slides), raw-AY/`.vtx` export.
145
+
146
+ ## Origin
147
+
148
+ spectrumizer grew out of hand-written, per-track PT3 composer scripts for a ZX
149
+ Spectrum game, generalising them into a single reusable arranger. It is now a
150
+ standalone, game-agnostic tool.
151
+
152
+ ## Credits
153
+
154
+ - **Sergey Bulba** — the PT3 module format and the Vortex Tracker / PT3 replayer
155
+ this tool targets, including the NoteTableCreator tone-table data the audition
156
+ synth uses for exact Spectrum pitches.
157
+ - **Ivan Roshin** — NoteTableCreator, the source of those packed AY tone tables.
158
+
159
+ These credits acknowledge the format and reference data; spectrumizer's encoder,
160
+ decoder and synth are independent implementations (see [`LICENSE`](LICENSE)).
161
+
162
+ ## Licence
163
+
164
+ [MIT](LICENSE) © Miguel Ángel Esteve Marco. Note: the MIT licence covers
165
+ **spectrumizer's own code**, not the music you run through it — see
166
+ [`LICENSING.md`](LICENSING.md).
@@ -0,0 +1,49 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "spectrumizer"
7
+ description = "Generate ZX Spectrum AY (PT3) music from MIDI and other sources."
8
+ readme = "README.md"
9
+ requires-python = ">=3.10"
10
+ license = { text = "MIT" }
11
+ authors = [{ name = "Miguel Ángel Esteve Marco", email = "esteve@telemaco.es" }]
12
+ keywords = ["zx-spectrum", "ay-3-8910", "pt3", "vortex-tracker", "chiptune", "midi"]
13
+ classifiers = [
14
+ "Development Status :: 3 - Alpha",
15
+ "Environment :: Console",
16
+ "Intended Audience :: Developers",
17
+ "License :: OSI Approved :: MIT License",
18
+ "Operating System :: OS Independent",
19
+ "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3 :: Only",
21
+ "Topic :: Multimedia :: Sound/Audio :: Conversion",
22
+ "Topic :: Multimedia :: Sound/Audio :: MIDI",
23
+ ]
24
+ dynamic = ["version"]
25
+ dependencies = ["mido>=1.3,<2.0"]
26
+
27
+ [project.optional-dependencies]
28
+ dev = ["pytest>=7"]
29
+ demos = ["lameenc>=1.5"] # MP3 encoder for examples/make_demos.py (pip wheel, no system ffmpeg)
30
+
31
+ [project.scripts]
32
+ spectrumizer = "spectrumizer.cli:main"
33
+ spectrumizer-play = "spectrumizer.play:main"
34
+
35
+ [project.urls]
36
+ Homepage = "https://github.com/revengator/spectrumizer"
37
+ Repository = "https://github.com/revengator/spectrumizer"
38
+ Issues = "https://github.com/revengator/spectrumizer/issues"
39
+
40
+ [tool.setuptools]
41
+ packages = [
42
+ "spectrumizer",
43
+ "spectrumizer.arrange",
44
+ "spectrumizer.inputs",
45
+ "spectrumizer.pt3",
46
+ ]
47
+
48
+ [tool.setuptools.dynamic]
49
+ version = { attr = "spectrumizer.__version__" }
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,13 @@
1
+ """spectrumizer — generate ZX Spectrum AY (PT3) music from public sources.
2
+
3
+ Pipeline: source -> IR (ir.Song) -> arrange (3 AY channels) -> PT3 bytes.
4
+
5
+ LICENCE GUARDRAIL: spectrumizer does NOT launder licences. The licence of the
6
+ SOURCE governs the OUTPUT. Only public-domain or your own material is safe to
7
+ bundle into a game. See LICENSING.md.
8
+ """
9
+
10
+ from .ir import Song, Note
11
+
12
+ __all__ = ["Song", "Note"]
13
+ __version__ = "0.1.0"
@@ -0,0 +1,6 @@
1
+ """Enable `python -m spectrumizer ...`."""
2
+
3
+ from .cli import main
4
+
5
+ if __name__ == "__main__":
6
+ raise SystemExit(main())
@@ -0,0 +1,126 @@
1
+ """Arranger: IR Song -> PT3 bytes.
2
+
3
+ Channel allocation (3 AY channels, A governs pattern length so it gets the lead):
4
+ A = lead (top voice)
5
+ B = bass (bottom voice)
6
+ C = real drums if the source has them; else synth drums in chiptune style;
7
+ else the harmony (middle voice) in faithful style.
8
+
9
+ `--style faithful|chiptune` selects which passes run — same engine, composable
10
+ passes, not two code paths.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from ..ir import Song, Note
16
+ from ..pt3 import (
17
+ midi_to_pt3_byte, NOTE_TO_BYTE, build_pt3,
18
+ DEFAULT_SAMPLES, DEFAULT_ORNAMENTS,
19
+ S_LEAD, S_BASS, S_HARMONY, S_SNARE, S_KICK, ORN_EMPTY,
20
+ )
21
+ from .model import Placed, rasterize, pack_patterns, ROWS_PER_PATTERN
22
+ from .quantize import plan_grid, note_rows
23
+ from .reduce import assign_voices
24
+ from .embellish import octave_short_lead, synth_drums
25
+
26
+ # GM percussion keys we treat as a kick (everything else on ch10 -> snare).
27
+ GM_KICK_KEYS = {35, 36}
28
+ DRUM_NOTE_BYTE = NOTE_TO_BYTE['C-4'] # dummy pitch; drum samples mute tone
29
+
30
+
31
+ def vol_from_velocity(velocity: int, ceil: int, vmax: int) -> int:
32
+ """Map a MIDI velocity to an AY volume in 1..ceil, scaled so the piece's
33
+ loudest note reaches the channel's ceiling. `vmax <= 0` disables dynamics
34
+ (returns the ceiling), so a flat-velocity source stays at full volume."""
35
+ if vmax <= 0:
36
+ return ceil
37
+ return max(1, min(ceil, round(ceil * velocity / vmax)))
38
+
39
+
40
+ def _line_to_placed(line: list[Note], rows_per_beat: int, total_rows: int,
41
+ transpose: int, ceil_vol: int = 15, vmax: int = 0) -> list[Placed]:
42
+ out: list[Placed] = []
43
+ for n in line:
44
+ s, e = note_rows(n.start, n.end, rows_per_beat, total_rows)
45
+ if s >= total_rows:
46
+ continue
47
+ opts = {} if vmax <= 0 else {'vol': vol_from_velocity(n.velocity, ceil_vol, vmax)}
48
+ out.append(Placed(s, e, midi_to_pt3_byte(n.pitch, transpose), opts))
49
+ return out
50
+
51
+
52
+ def _drums_to_placed(drums: list[Note], rows_per_beat: int, total_rows: int,
53
+ ceil_vol: int = 13, vmax: int = 0) -> list[Placed]:
54
+ out: list[Placed] = []
55
+ for n in drums:
56
+ s, _ = note_rows(n.start, n.end, rows_per_beat, total_rows)
57
+ if s >= total_rows:
58
+ continue
59
+ sample = S_KICK if (n.pitch in GM_KICK_KEYS or n.pitch <= 36) else S_SNARE
60
+ opts = {'sample': sample}
61
+ if vmax > 0:
62
+ opts['vol'] = vol_from_velocity(n.velocity, ceil_vol, vmax)
63
+ out.append(Placed(s, s + 1, DRUM_NOTE_BYTE, opts))
64
+ return out
65
+
66
+
67
+ def arrange(song: Song, *, style: str = 'faithful', rows_per_beat: int = 4,
68
+ speed: int | None = None, transpose: int = 0,
69
+ name: str | None = None, author: str = 'SPECTRUMIZER',
70
+ loop_pos: int = 0, dynamics: bool = True) -> tuple[bytes, dict]:
71
+ """Arrange `song` and return (pt3_bytes, stats).
72
+
73
+ `dynamics`: map MIDI velocity to per-note AY volume (on by default); the
74
+ loudest note in the piece sits at each channel's ceiling. False = flat
75
+ per-channel volume.
76
+ """
77
+ speed_v, total_rows = plan_grid(song, rows_per_beat, speed)
78
+
79
+ use_drum_channel = song.has_drums or style == 'chiptune'
80
+ n_pitched = 2 if use_drum_channel else 3
81
+ lead, bass, harmony = assign_voices(song.notes, n_pitched)
82
+
83
+ vmax = max((n.velocity for n in song.notes), default=0) if dynamics else 0
84
+ lead_p = _line_to_placed(lead, rows_per_beat, total_rows, transpose, 15, vmax)
85
+ bass_p = _line_to_placed(bass, rows_per_beat, total_rows, transpose, 14, vmax)
86
+
87
+ if song.has_drums:
88
+ dmax = max((n.velocity for n in song.drums), default=0) if dynamics else 0
89
+ c_p = _drums_to_placed(song.drums, rows_per_beat, total_rows, 13, dmax)
90
+ c_sample, c_vol, c_kind = S_SNARE, 13, 'drums'
91
+ elif style == 'chiptune':
92
+ c_p = synth_drums(total_rows, rows_per_beat, DRUM_NOTE_BYTE, S_SNARE, S_KICK)
93
+ c_sample, c_vol, c_kind = S_SNARE, 13, 'synth-drums'
94
+ else:
95
+ c_p = _line_to_placed(harmony, rows_per_beat, total_rows, transpose, 10, vmax)
96
+ c_sample, c_vol, c_kind = S_HARMONY, 10, 'harmony'
97
+
98
+ if style == 'chiptune':
99
+ octave_short_lead(lead_p, short_thresh_rows=rows_per_beat)
100
+
101
+ specs = [
102
+ (rasterize(lead_p, total_rows), S_LEAD, 15, ORN_EMPTY),
103
+ (rasterize(bass_p, total_rows), S_BASS, 14, ORN_EMPTY),
104
+ (rasterize(c_p, total_rows), c_sample, c_vol, ORN_EMPTY),
105
+ ]
106
+ patterns = pack_patterns(specs, total_rows)
107
+
108
+ pt3 = build_pt3(patterns, dict(DEFAULT_SAMPLES), dict(DEFAULT_ORNAMENTS),
109
+ name=name or song.name or "SPECTRUMIZED",
110
+ author=author, speed=speed_v, loop_pos=loop_pos)
111
+
112
+ stats = {
113
+ 'style': style,
114
+ 'dynamics': dynamics,
115
+ 'speed': speed_v,
116
+ 'rows_per_beat': rows_per_beat,
117
+ 'total_rows': total_rows,
118
+ 'patterns': len(patterns),
119
+ 'tempo_bpm': round(song.tempo_bpm, 1),
120
+ 'voices': {'lead': len(lead), 'bass': len(bass),
121
+ 'channel_c': c_kind,
122
+ 'harmony': len(harmony) if c_kind == 'harmony' else 0,
123
+ 'drums': len(song.drums)},
124
+ 'bytes': len(pt3),
125
+ }
126
+ return pt3, stats
@@ -0,0 +1,38 @@
1
+ """Chiptune embellishers (only run for --style chiptune).
2
+
3
+ MVP passes:
4
+ * octave_short_lead — octave-double SHORT lead notes (the classic AY brightener;
5
+ held notes stay single-voice so they don't warble).
6
+ * synth_drums — a backbeat (kick on beats 1&3, snare on 2&4) when the
7
+ source has no drum track, so chiptune output still has percussive drive.
8
+
9
+ Planned next: chord arpeggios via ORN_MAJOR/ORN_MINOR (the ornament builders are
10
+ already wired in pt3.ornaments) to fake polyphony on a single channel.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from .model import Placed
16
+ from ..pt3 import ORN_OCTAVE, ORN_EMPTY
17
+
18
+
19
+ def octave_short_lead(lead: list[Placed], short_thresh_rows: int) -> None:
20
+ """In place: octave ornament on notes shorter than the threshold."""
21
+ for p in lead:
22
+ p.opts['ornament'] = ORN_OCTAVE if (p.end - p.start) < short_thresh_rows else ORN_EMPTY
23
+
24
+
25
+ def synth_drums(total_rows: int, rows_per_beat: int, drum_byte: int,
26
+ snare_sample: int, kick_sample: int) -> list[Placed]:
27
+ """A simple 4/4 backbeat occupying one channel (drums = noise, tone off)."""
28
+ placed: list[Placed] = []
29
+ n_beats = total_rows // rows_per_beat
30
+ for b in range(n_beats):
31
+ row = b * rows_per_beat
32
+ if row >= total_rows:
33
+ break
34
+ if b % 4 in (0, 2):
35
+ placed.append(Placed(row, row + 1, drum_byte, {'sample': kick_sample}))
36
+ else:
37
+ placed.append(Placed(row, row + 1, drum_byte, {'sample': snare_sample}))
38
+ return placed