pymusicassembler 0.1.0__py3-none-any.whl
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.
- pymusicassembler/__init__.py +59 -0
- pymusicassembler/audio.py +97 -0
- pymusicassembler/cli.py +88 -0
- pymusicassembler/constants.py +121 -0
- pymusicassembler/errors.py +13 -0
- pymusicassembler/model.py +231 -0
- pymusicassembler/player.py +513 -0
- pymusicassembler/reader.py +258 -0
- pymusicassembler/reglog.py +91 -0
- pymusicassembler/writer.py +151 -0
- pymusicassembler-0.1.0.dist-info/METADATA +364 -0
- pymusicassembler-0.1.0.dist-info/RECORD +16 -0
- pymusicassembler-0.1.0.dist-info/WHEEL +5 -0
- pymusicassembler-0.1.0.dist-info/entry_points.txt +2 -0
- pymusicassembler-0.1.0.dist-info/licenses/LICENSE +201 -0
- pymusicassembler-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,513 @@
|
|
|
1
|
+
"""Music Assembler playroutine in Python.
|
|
2
|
+
|
|
3
|
+
A faithful, byte-exact reimplementation of the Music Assembler player's
|
|
4
|
+
per-frame DSP, recovered from the player's disassembly
|
|
5
|
+
(``decompile.c``) and validated byte-exact against the
|
|
6
|
+
``preframr-sidtrace`` register oracle. The control flow -- tempo
|
|
7
|
+
divider, orderlist + pattern walk, the 16-bit pulse-width sweep, the
|
|
8
|
+
vibrato/arp frequency accumulators, the gate/retrigger idiom and the
|
|
9
|
+
filter-cutoff sweep -- is the player's own algorithm; the per-tune
|
|
10
|
+
ADDRESSES it operates on are discovered by the reader, so a relocated
|
|
11
|
+
tune of the same player plays unchanged.
|
|
12
|
+
|
|
13
|
+
Each :meth:`Player.play_frame` call runs one frame and returns the SID
|
|
14
|
+
register writes for that frame as ``(register, value)`` pairs in
|
|
15
|
+
ascending register order (changed registers only, after the first
|
|
16
|
+
frame), mirroring :class:`pymusicassembler.model.Song`'s output.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from dataclasses import dataclass
|
|
20
|
+
from typing import List, Optional
|
|
21
|
+
|
|
22
|
+
from pymusicassembler import constants
|
|
23
|
+
from pymusicassembler.model import Song
|
|
24
|
+
|
|
25
|
+
FREQ_LO, FREQ_HI, PW_LO, PW_HI, CTRL, AD, SR = 0, 1, 2, 3, 4, 5, 6
|
|
26
|
+
FC_HI, RES_FILT, MODE_VOL = (
|
|
27
|
+
constants.FC_HI_REG,
|
|
28
|
+
constants.RES_FILT_REG,
|
|
29
|
+
constants.MODE_VOL_REG,
|
|
30
|
+
)
|
|
31
|
+
SID_OFFSET = constants.SID_OFFSET
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class _Voice:
|
|
36
|
+
"""Per-voice work state (mirrors the player's ``$..,X`` work arrays)."""
|
|
37
|
+
|
|
38
|
+
# pylint: disable=too-many-instance-attributes # mirrors the player state
|
|
39
|
+
pat_cursor: int = 0 # $1081
|
|
40
|
+
ctrl: int = 0 # $1084 waveform/CTRL
|
|
41
|
+
order_cursor: int = 0 # $1087 orderlist cursor
|
|
42
|
+
dur: int = 0 # $108a note-duration counter
|
|
43
|
+
pattern_id: int = 0 # $108d
|
|
44
|
+
note_index: int = 0 # $10c9
|
|
45
|
+
freq_lo: int = 0 # $10cc plain FREQ_LO
|
|
46
|
+
freq_hi: int = 0 # $10cf plain FREQ_HI
|
|
47
|
+
arp_substep: int = 0 # $10dd
|
|
48
|
+
pw_substep: int = 0 # $10e0
|
|
49
|
+
pw_dir: int = 0 # $10e3
|
|
50
|
+
transrep: int = 0 # $10e6 transpose(hi)/repeat(lo)
|
|
51
|
+
repeat_ctr: int = 0 # $10e9
|
|
52
|
+
ctrl_mask: int = 0xFF # $1031 gate/CTRL AND-mask
|
|
53
|
+
note_ctl: int = 0 # $1141 note control byte
|
|
54
|
+
instr_cursor: int = 0 # $1144
|
|
55
|
+
vib_lo_step: int = 0 # $1147
|
|
56
|
+
vib_hi_step: int = 0 # $114a
|
|
57
|
+
porta_ctr: int = 0 # $114d
|
|
58
|
+
vib_freq_hi: int = 0 # $12b6 vibrato FREQ_HI accumulator
|
|
59
|
+
vib_toggle: int = 0 # $12b9
|
|
60
|
+
arp_phase: int = 0 # $12bd
|
|
61
|
+
instr_x8: int = 0 # $13d9 instrument id * 8 (record byte offset)
|
|
62
|
+
pw_lo: int = 0 # $13dc PW_LO accumulator
|
|
63
|
+
pw_hi: int = 0 # $13df PW_HI accumulator
|
|
64
|
+
vib_freq_lo: int = 0 # $13e2 vibrato FREQ_LO accumulator
|
|
65
|
+
flags: int = 0 # $00fd effect flags
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@dataclass
|
|
69
|
+
class _Tables:
|
|
70
|
+
"""Resolved per-tune table base addresses (absolute C64 addresses)."""
|
|
71
|
+
|
|
72
|
+
# pylint: disable=too-many-instance-attributes # one field per table base
|
|
73
|
+
order_lo: int
|
|
74
|
+
order_hi: int
|
|
75
|
+
pattern_lo: int
|
|
76
|
+
pattern_hi: int
|
|
77
|
+
instr: int
|
|
78
|
+
freq_lo: int
|
|
79
|
+
freq_hi: int
|
|
80
|
+
inner_lo: int
|
|
81
|
+
inner_hi: int
|
|
82
|
+
arp: int
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class Player:
|
|
86
|
+
"""Play a :class:`~pymusicassembler.model.Song` one frame at a time."""
|
|
87
|
+
|
|
88
|
+
# pylint: disable=too-many-instance-attributes # full player machine state
|
|
89
|
+
def __init__(self, song: Song):
|
|
90
|
+
self._song = song
|
|
91
|
+
self._image = song.image
|
|
92
|
+
self._load = song.load_addr
|
|
93
|
+
self._tables = _resolve_tables(song)
|
|
94
|
+
self.regs = [0] * constants.SID_REGISTERS
|
|
95
|
+
self._voices = [_Voice() for _ in range(constants.VOICES)]
|
|
96
|
+
self._tempo = 0 # $1090 (init 0; underflows on frame 0 -> advance)
|
|
97
|
+
self._filter_owner = 0 # $1262
|
|
98
|
+
self._fc_acc = 0 # $129e accumulator (self-modified LDA immediate)
|
|
99
|
+
self._fc_ctr = 0 # $1296 gate counter (self-modified LDA immediate)
|
|
100
|
+
self._fc_step = 0 # $12a0 sweep step (self-modified ADC immediate)
|
|
101
|
+
self._last_regs: Optional[List[int]] = None
|
|
102
|
+
self._init()
|
|
103
|
+
|
|
104
|
+
# -- raw image access ------------------------------------------------
|
|
105
|
+
def _byte(self, abs_addr: int) -> int:
|
|
106
|
+
idx = abs_addr - self._load
|
|
107
|
+
if 0 <= idx < len(self._image):
|
|
108
|
+
return self._image[idx]
|
|
109
|
+
return 0
|
|
110
|
+
|
|
111
|
+
def _orderlist_addr(self, voice: int) -> int:
|
|
112
|
+
tab = self._tables
|
|
113
|
+
return self._byte(tab.order_lo + voice) | (
|
|
114
|
+
self._byte(tab.order_hi + voice) << 8
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
def _pattern_addr(self, pattern_id: int) -> int:
|
|
118
|
+
tab = self._tables
|
|
119
|
+
lo = self._byte(tab.pattern_lo + pattern_id)
|
|
120
|
+
hi = self._byte(tab.pattern_hi + pattern_id)
|
|
121
|
+
return lo | (hi << 8)
|
|
122
|
+
|
|
123
|
+
def _inner_addr(self, flags: int) -> int:
|
|
124
|
+
tab = self._tables
|
|
125
|
+
return self._byte(tab.inner_lo + flags) | (
|
|
126
|
+
self._byte(tab.inner_hi + flags) << 8
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
def _instr_byte(self, idx: int, byte_off: int) -> int:
|
|
130
|
+
return self._byte(
|
|
131
|
+
self._tables.instr + idx * constants.INSTR_RECORD_SIZE + byte_off
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
def _freq_lo(self, note_index: int) -> int:
|
|
135
|
+
return self._byte(self._tables.freq_lo + note_index)
|
|
136
|
+
|
|
137
|
+
def _freq_hi(self, note_index: int) -> int:
|
|
138
|
+
return self._byte(self._tables.freq_hi + note_index)
|
|
139
|
+
|
|
140
|
+
def _res_preset(self, v: int) -> int:
|
|
141
|
+
return constants.RES_PRESETS[v]
|
|
142
|
+
|
|
143
|
+
# -- init ($1048) ----------------------------------------------------
|
|
144
|
+
def _init(self) -> None:
|
|
145
|
+
self.regs[MODE_VOL] = 0x1F
|
|
146
|
+
self.regs[RES_FILT] = 0xF0
|
|
147
|
+
# The init routine zeroes only $1081..$1090 (pat_cursor / ctrl /
|
|
148
|
+
# order_cursor / dur / pattern_id) and the filter-owner flag; every
|
|
149
|
+
# other work-RAM byte keeps its loaded-image value (a tune saved
|
|
150
|
+
# mid-playback bakes live state in). Seed those from the image so the
|
|
151
|
+
# player's first frames match the chip's, then apply the init reloads.
|
|
152
|
+
# The filter-sweep state ($1262 owner, $129e acc, $1296 gate, $12a0
|
|
153
|
+
# step) is self-modified player code; init does not clear it, so it
|
|
154
|
+
# keeps the loaded-image immediates until a filter command / voice-0
|
|
155
|
+
# trigger overwrites them.
|
|
156
|
+
self._filter_owner = self._byte(constants.WORK_FILTER_OWNER)
|
|
157
|
+
self._fc_acc = self._byte(0x129E)
|
|
158
|
+
self._fc_ctr = self._byte(0x1296)
|
|
159
|
+
self._fc_step = self._byte(0x12A0)
|
|
160
|
+
# Voice-0 trigger reset values ($1265/$126a immediates) -- per-tune
|
|
161
|
+
# constants baked into the player code region (the player customises
|
|
162
|
+
# its filter-sweep envelope per tune).
|
|
163
|
+
self._fc_acc_reset = self._byte(0x1266)
|
|
164
|
+
self._fc_ctr_reset = self._byte(0x126B)
|
|
165
|
+
self._tempo = 0 # $1090 cleared by the init loop
|
|
166
|
+
for v, voice in enumerate(self._voices):
|
|
167
|
+
voice.pat_cursor = 0
|
|
168
|
+
voice.ctrl = 0
|
|
169
|
+
voice.order_cursor = 0
|
|
170
|
+
voice.dur = 0
|
|
171
|
+
voice.note_index = self._byte(constants.WORK_NOTE_INDEX + v)
|
|
172
|
+
voice.freq_lo = self._byte(constants.WORK_FREQ_LO + v)
|
|
173
|
+
voice.freq_hi = self._byte(constants.WORK_FREQ_HI + v)
|
|
174
|
+
voice.arp_substep = self._byte(constants.WORK_ARP_SUBSTEP + v)
|
|
175
|
+
voice.pw_substep = self._byte(constants.WORK_PW_SUBSTEP + v)
|
|
176
|
+
voice.pw_dir = self._byte(constants.WORK_PW_DIR + v)
|
|
177
|
+
voice.note_ctl = self._byte(constants.WORK_NOTE_CTL + v)
|
|
178
|
+
voice.instr_cursor = self._byte(constants.WORK_INSTR_CURSOR + v)
|
|
179
|
+
voice.vib_lo_step = self._byte(constants.WORK_VIB_LO_STEP + v)
|
|
180
|
+
voice.vib_hi_step = self._byte(constants.WORK_VIB_HI_STEP + v)
|
|
181
|
+
voice.porta_ctr = self._byte(constants.WORK_PORTA_CTR + v)
|
|
182
|
+
voice.vib_freq_hi = self._byte(constants.WORK_VIB_FREQ_HI + v)
|
|
183
|
+
voice.vib_toggle = self._byte(constants.WORK_VIB_TOGGLE + v)
|
|
184
|
+
voice.arp_phase = self._byte(constants.WORK_ARP_PHASE + v)
|
|
185
|
+
voice.instr_x8 = self._byte(constants.WORK_INSTR_X8 + v)
|
|
186
|
+
voice.pw_lo = self._byte(constants.WORK_PW_LO + v)
|
|
187
|
+
voice.pw_hi = self._byte(constants.WORK_PW_HI + v)
|
|
188
|
+
voice.vib_freq_lo = self._byte(constants.WORK_VIB_FREQ_LO + v)
|
|
189
|
+
voice.ctrl_mask = self._byte(constants.WORK_CTRL_MASK + v)
|
|
190
|
+
voice.flags = self._byte(constants.WORK_FLAGS + v)
|
|
191
|
+
# init reload: pattern_id / transrep / repeat from the orderlist.
|
|
192
|
+
order = self._orderlist_addr(v)
|
|
193
|
+
voice.pattern_id = self._byte(order)
|
|
194
|
+
second = self._byte(order + 1)
|
|
195
|
+
voice.transrep = second
|
|
196
|
+
voice.repeat_ctr = second & 0x0F
|
|
197
|
+
|
|
198
|
+
# -- one play call ($1021) ------------------------------------------
|
|
199
|
+
def play_frame(self) -> List[tuple]:
|
|
200
|
+
"""Run one player tick; return this frame's SID register writes.
|
|
201
|
+
|
|
202
|
+
The first frame returns all 25 registers; later frames return
|
|
203
|
+
only registers whose value changed.
|
|
204
|
+
"""
|
|
205
|
+
self._frame()
|
|
206
|
+
if self._last_regs is None:
|
|
207
|
+
writes = list(enumerate(self.regs))
|
|
208
|
+
else:
|
|
209
|
+
writes = [
|
|
210
|
+
(reg, value)
|
|
211
|
+
for reg, (value, last) in enumerate(zip(self.regs, self._last_regs))
|
|
212
|
+
if value != last
|
|
213
|
+
]
|
|
214
|
+
self._last_regs = list(self.regs)
|
|
215
|
+
return writes
|
|
216
|
+
|
|
217
|
+
def _frame(self) -> None:
|
|
218
|
+
self._tempo = (self._tempo - 1) & 0xFF
|
|
219
|
+
advance = self._tempo >= 0x80 # signed < 0
|
|
220
|
+
if advance:
|
|
221
|
+
self._tempo = constants.TEMPO_DIVIDER
|
|
222
|
+
for v in range(constants.VOICES):
|
|
223
|
+
voice = self._voices[v]
|
|
224
|
+
if not advance:
|
|
225
|
+
self._output(v)
|
|
226
|
+
continue
|
|
227
|
+
voice.dur = (voice.dur - 1) & 0xFF
|
|
228
|
+
if voice.dur >= 0x80: # signed < 0 -> new row
|
|
229
|
+
self._advance_row(v)
|
|
230
|
+
else:
|
|
231
|
+
self._output(v)
|
|
232
|
+
|
|
233
|
+
# -- orderlist + pattern parse ($1091) ------------------------------
|
|
234
|
+
def _advance_row(self, v: int) -> None:
|
|
235
|
+
voice = self._voices[v]
|
|
236
|
+
if voice.pattern_id == constants.SILENCE_PATTERN:
|
|
237
|
+
voice.ctrl &= 0xFE
|
|
238
|
+
return
|
|
239
|
+
pat = self._pattern_addr(voice.pattern_id)
|
|
240
|
+
cursor = voice.pat_cursor
|
|
241
|
+
val = self._byte(pat + cursor)
|
|
242
|
+
if val >= constants.INSTR_MIN:
|
|
243
|
+
if val >= constants.LONGDUR_MIN: # long-duration token
|
|
244
|
+
voice.dur = val & constants.CTL_DUR_MASK
|
|
245
|
+
self._after_row(v, pat, cursor)
|
|
246
|
+
return
|
|
247
|
+
# instrument select: id*8 kept in one byte, ASL wraps at 8 bits.
|
|
248
|
+
voice.instr_x8 = (val << 3) & 0xFF
|
|
249
|
+
cursor += 1
|
|
250
|
+
val = self._byte(pat + cursor)
|
|
251
|
+
if val >= constants.DUR_MIN:
|
|
252
|
+
voice.dur = val & constants.CTL_DUR_MASK
|
|
253
|
+
self._after_row(v, pat, cursor)
|
|
254
|
+
return
|
|
255
|
+
# else fall to note
|
|
256
|
+
elif val >= constants.DUR_MIN: # bare duration token ($60..$7F)
|
|
257
|
+
# $10b6: dur = val & 0x1f, gate-off mask, gate off, then advance
|
|
258
|
+
# the cursor / check the pattern end (the $10c3 JMP $1187).
|
|
259
|
+
voice.dur = val & constants.CTL_DUR_MASK
|
|
260
|
+
voice.ctrl_mask = 0xFE
|
|
261
|
+
voice.ctrl &= 0xFE
|
|
262
|
+
self._after_row(v, pat, cursor)
|
|
263
|
+
return
|
|
264
|
+
self._parse_note(v, pat, cursor, val)
|
|
265
|
+
|
|
266
|
+
def _parse_note(self, v: int, pat: int, cursor: int, note: int) -> None:
|
|
267
|
+
voice = self._voices[v]
|
|
268
|
+
idx = (voice.transrep >> 4) + note
|
|
269
|
+
voice.note_index = idx & 0xFF
|
|
270
|
+
voice.freq_lo = self._freq_lo(voice.note_index)
|
|
271
|
+
voice.vib_freq_lo = voice.freq_lo
|
|
272
|
+
voice.freq_hi = self._freq_hi(voice.note_index)
|
|
273
|
+
voice.vib_freq_hi = voice.freq_hi
|
|
274
|
+
cursor += 1
|
|
275
|
+
ctl = self._byte(pat + cursor)
|
|
276
|
+
voice.note_ctl = ctl
|
|
277
|
+
voice.dur = ctl & constants.CTL_DUR_MASK
|
|
278
|
+
if ctl >= constants.CTL_FILTER: # filter command attached ($1150)
|
|
279
|
+
self._filter_owner = v
|
|
280
|
+
cursor += 1
|
|
281
|
+
param = self._byte(pat + cursor)
|
|
282
|
+
# step = (param & 0xf) * 2 - 0x10 (self-modified ADC immediate).
|
|
283
|
+
self._fc_step = (((param & 0x0F) << 1) - 0x10) & 0xFF
|
|
284
|
+
cursor += 1
|
|
285
|
+
third = self._byte(pat + cursor)
|
|
286
|
+
if third == 0:
|
|
287
|
+
self.regs[RES_FILT] = 0xF0
|
|
288
|
+
else:
|
|
289
|
+
self.regs[RES_FILT] = self._res_preset(v)
|
|
290
|
+
elif ctl & constants.CTL_VIBARP: # vibrato/arp params attached
|
|
291
|
+
cursor += 1
|
|
292
|
+
voice.vib_lo_step = self._byte(pat + cursor)
|
|
293
|
+
cursor += 1
|
|
294
|
+
voice.vib_hi_step = self._byte(pat + cursor)
|
|
295
|
+
voice.ctrl_mask = 0xFF
|
|
296
|
+
voice.vib_toggle = 0xFF
|
|
297
|
+
voice.arp_substep = 0
|
|
298
|
+
voice.arp_phase = 0
|
|
299
|
+
self._after_row(v, pat, cursor)
|
|
300
|
+
|
|
301
|
+
def _after_row(self, v: int, pat: int, cursor: int) -> None:
|
|
302
|
+
voice = self._voices[v]
|
|
303
|
+
cursor += 1
|
|
304
|
+
nb = self._byte(pat + cursor)
|
|
305
|
+
if nb == constants.PATTERN_END:
|
|
306
|
+
voice.repeat_ctr = (voice.repeat_ctr - 1) & 0xFF
|
|
307
|
+
if voice.repeat_ctr < 0x80: # signed >= 0: replay pattern
|
|
308
|
+
voice.pat_cursor = 0
|
|
309
|
+
return
|
|
310
|
+
order = self._orderlist_addr(v)
|
|
311
|
+
ocur = voice.order_cursor + 2
|
|
312
|
+
if self._byte(order + ocur) == constants.ORDER_END:
|
|
313
|
+
ocur = 0
|
|
314
|
+
voice.order_cursor = ocur
|
|
315
|
+
voice.pattern_id = self._byte(order + ocur)
|
|
316
|
+
voice.transrep = self._byte(order + ocur + 1)
|
|
317
|
+
voice.repeat_ctr = voice.transrep & 0x0F
|
|
318
|
+
voice.pat_cursor = 0
|
|
319
|
+
else:
|
|
320
|
+
voice.pat_cursor = cursor
|
|
321
|
+
|
|
322
|
+
# -- per-frame output + generators ----------------------------------
|
|
323
|
+
def _output(self, v: int) -> None:
|
|
324
|
+
voice = self._voices[v]
|
|
325
|
+
ofs = SID_OFFSET[v]
|
|
326
|
+
triggered = voice.note_ctl & constants.CTL_TRIGGERED
|
|
327
|
+
if not triggered: # NOTE-TRIGGER frame
|
|
328
|
+
voice.instr_cursor = 0
|
|
329
|
+
idx = voice.instr_x8 >> 3
|
|
330
|
+
self.regs[ofs + SR] = self._instr_byte(idx, constants.INSTR_SR)
|
|
331
|
+
self.regs[ofs + AD] = self._instr_byte(idx, constants.INSTR_AD)
|
|
332
|
+
self.regs[ofs + CTRL] = voice.ctrl & 0xFE
|
|
333
|
+
voice.ctrl = self._instr_byte(idx, constants.INSTR_WAVEFORM)
|
|
334
|
+
pw_init = self._instr_byte(idx, constants.INSTR_PULSE_INIT)
|
|
335
|
+
voice.pw_lo = pw_init
|
|
336
|
+
voice.pw_hi = pw_init
|
|
337
|
+
if v == 0:
|
|
338
|
+
self._fc_acc = self._fc_acc_reset
|
|
339
|
+
self._fc_ctr = self._fc_ctr_reset
|
|
340
|
+
voice.pw_substep = 0
|
|
341
|
+
voice.pw_dir = 0
|
|
342
|
+
voice.porta_ctr = self._instr_byte(idx, constants.INSTR_ARP_SPEED) >> 3
|
|
343
|
+
voice.note_ctl |= constants.CTL_TRIGGERED
|
|
344
|
+
voice.flags = self._instr_byte(idx, constants.INSTR_PORTA)
|
|
345
|
+
self._sid_final(v)
|
|
346
|
+
return
|
|
347
|
+
# SUSTAIN frame. Filter-cutoff sweep ($1290): only the owner voice
|
|
348
|
+
# runs it; while the gate counter ($1296) is nonzero, decrement it,
|
|
349
|
+
# add the step ($12a0) to the accumulator ($129e) and write FC_HI.
|
|
350
|
+
if v == self._filter_owner and self._fc_ctr != 0:
|
|
351
|
+
self._fc_ctr = (self._fc_ctr - 1) & 0xFF
|
|
352
|
+
self._fc_acc = (self._fc_acc + self._fc_step) & 0xFF
|
|
353
|
+
self.regs[FC_HI] = self._fc_acc
|
|
354
|
+
flags = voice.flags & 0x0F
|
|
355
|
+
if flags != 0:
|
|
356
|
+
self._instrument_tick(v, flags)
|
|
357
|
+
elif (voice.note_ctl & constants.CTL_VIBARP) == 0 and (voice.flags & 0x10) != 0:
|
|
358
|
+
self._arp_walk(v)
|
|
359
|
+
self._pw_gen(v)
|
|
360
|
+
self._sid_final(v)
|
|
361
|
+
|
|
362
|
+
def _arp_walk(self, v: int) -> None:
|
|
363
|
+
voice = self._voices[v]
|
|
364
|
+
voice.porta_ctr = (voice.porta_ctr - 1) & 0xFF
|
|
365
|
+
if voice.porta_ctr < 0x80: # signed >= 0: not yet
|
|
366
|
+
return
|
|
367
|
+
voice.porta_ctr = (voice.porta_ctr + 1) & 0xFF
|
|
368
|
+
idx = voice.instr_x8 >> 3
|
|
369
|
+
step = self._instr_byte(idx, constants.INSTR_ARP_STEP)
|
|
370
|
+
phase = voice.arp_phase & 3
|
|
371
|
+
ascend = self._byte(self._tables.arp + phase)
|
|
372
|
+
if ascend == 0: # descending
|
|
373
|
+
lo = voice.freq_lo - step
|
|
374
|
+
voice.freq_lo = lo & 0xFF
|
|
375
|
+
if lo < 0:
|
|
376
|
+
voice.freq_hi = (voice.freq_hi - 1) & 0xFF
|
|
377
|
+
else: # ascending
|
|
378
|
+
lo = voice.freq_lo + step
|
|
379
|
+
voice.freq_lo = lo & 0xFF
|
|
380
|
+
if lo > 0xFF:
|
|
381
|
+
voice.freq_hi = (voice.freq_hi + 1) & 0xFF
|
|
382
|
+
voice.arp_substep = (voice.arp_substep + 1) & 0xFF
|
|
383
|
+
arpspeed = self._instr_byte(idx, constants.INSTR_ARP_SPEED) & 0x0F
|
|
384
|
+
if arpspeed == voice.arp_substep:
|
|
385
|
+
voice.arp_substep = 0
|
|
386
|
+
voice.arp_phase = (voice.arp_phase + 1) & 0xFF
|
|
387
|
+
|
|
388
|
+
def _instrument_tick(self, v: int, flags: int) -> None:
|
|
389
|
+
voice = self._voices[v]
|
|
390
|
+
base = self._inner_addr(flags)
|
|
391
|
+
cur = voice.instr_cursor
|
|
392
|
+
ctrl = self._byte(base + cur) & voice.ctrl_mask
|
|
393
|
+
voice.ctrl = ctrl
|
|
394
|
+
pitch = self._byte(base + cur + 1)
|
|
395
|
+
if pitch < 0x80:
|
|
396
|
+
pitch = (pitch + voice.note_index) & 0xFF
|
|
397
|
+
pitch &= 0x7F
|
|
398
|
+
cutoff = self._byte(base + cur + 2)
|
|
399
|
+
if cutoff != 0:
|
|
400
|
+
self._fc_acc = cutoff
|
|
401
|
+
marker = self._byte(base + cur + 3)
|
|
402
|
+
if marker >= 0xFE:
|
|
403
|
+
if marker == 0xFE: # stop the effect
|
|
404
|
+
voice.flags &= 0xF0
|
|
405
|
+
cur = cur + 3
|
|
406
|
+
else: # $FF: loop to start
|
|
407
|
+
cur = 0
|
|
408
|
+
else:
|
|
409
|
+
cur = cur + 3
|
|
410
|
+
voice.instr_cursor = cur
|
|
411
|
+
voice.freq_lo = self._freq_lo(pitch)
|
|
412
|
+
voice.freq_hi = self._freq_hi(pitch)
|
|
413
|
+
|
|
414
|
+
def _pw_gen(self, v: int) -> None:
|
|
415
|
+
voice = self._voices[v]
|
|
416
|
+
idx = voice.instr_x8 >> 3
|
|
417
|
+
speed = self._instr_byte(idx, constants.INSTR_PULSE_STEP)
|
|
418
|
+
flags = voice.flags
|
|
419
|
+
if flags & 0x40: # linear add
|
|
420
|
+
lo = voice.pw_lo + speed
|
|
421
|
+
voice.pw_lo = lo & 0xFF
|
|
422
|
+
voice.pw_hi = (voice.pw_hi + speed + (lo >> 8)) & 0xFF
|
|
423
|
+
elif flags & 0x20: # triangle up/down
|
|
424
|
+
if voice.pw_dir:
|
|
425
|
+
lo = voice.pw_lo + speed
|
|
426
|
+
voice.pw_lo = lo & 0xFF
|
|
427
|
+
if lo > 0xFF:
|
|
428
|
+
voice.pw_hi = (voice.pw_hi + 1) & 0xFF
|
|
429
|
+
else:
|
|
430
|
+
lo = voice.pw_lo - speed
|
|
431
|
+
voice.pw_lo = lo & 0xFF
|
|
432
|
+
if lo < 0:
|
|
433
|
+
voice.pw_hi = (voice.pw_hi - 1) & 0xFF
|
|
434
|
+
voice.pw_substep = (voice.pw_substep + 1) & 0xFF
|
|
435
|
+
if (speed & 0x0F) == voice.pw_substep:
|
|
436
|
+
voice.pw_substep = 0
|
|
437
|
+
voice.pw_dir ^= 1
|
|
438
|
+
|
|
439
|
+
def _sid_final(self, v: int) -> None:
|
|
440
|
+
voice = self._voices[v]
|
|
441
|
+
ofs = SID_OFFSET[v]
|
|
442
|
+
self.regs[ofs + CTRL] = voice.ctrl
|
|
443
|
+
self.regs[ofs + PW_HI] = voice.pw_hi & 0x0F
|
|
444
|
+
self.regs[ofs + PW_LO] = voice.pw_lo
|
|
445
|
+
if voice.note_ctl & constants.CTL_VIBARP: # vibrato/arp-freq mode
|
|
446
|
+
if voice.vib_lo_step & 1:
|
|
447
|
+
voice.vib_toggle ^= 0xFF
|
|
448
|
+
if voice.vib_toggle != 0:
|
|
449
|
+
self.regs[ofs + FREQ_LO] = voice.freq_lo
|
|
450
|
+
self.regs[ofs + FREQ_HI] = voice.freq_hi
|
|
451
|
+
return
|
|
452
|
+
lo = voice.vib_freq_lo + voice.vib_lo_step
|
|
453
|
+
voice.vib_freq_lo = lo & 0xFF
|
|
454
|
+
self.regs[ofs + FREQ_LO] = voice.vib_freq_lo
|
|
455
|
+
voice.vib_freq_hi = (
|
|
456
|
+
voice.vib_freq_hi + voice.vib_hi_step + (lo >> 8)
|
|
457
|
+
) & 0xFF
|
|
458
|
+
self.regs[ofs + FREQ_HI] = voice.vib_freq_hi
|
|
459
|
+
return
|
|
460
|
+
self.regs[ofs + FREQ_LO] = voice.freq_lo
|
|
461
|
+
self.regs[ofs + FREQ_HI] = voice.freq_hi
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
def _resolve_tables(song: Song) -> _Tables:
|
|
465
|
+
"""Resolve the per-tune table base addresses from the song's image.
|
|
466
|
+
|
|
467
|
+
The Music Assembler player binary is identical across tunes, so each
|
|
468
|
+
pointer-table base lives at a fixed player-code offset as a 16-bit
|
|
469
|
+
operand; reading those operands recovers the bases for any (relocated)
|
|
470
|
+
tune of the same player.
|
|
471
|
+
"""
|
|
472
|
+
image = song.image
|
|
473
|
+
|
|
474
|
+
def op16(off: int) -> int:
|
|
475
|
+
if off + 1 < len(image):
|
|
476
|
+
return image[off] | (image[off + 1] << 8)
|
|
477
|
+
return 0
|
|
478
|
+
|
|
479
|
+
return _Tables(
|
|
480
|
+
order_lo=op16(constants.OP_ORDER_LO),
|
|
481
|
+
order_hi=op16(constants.OP_ORDER_HI),
|
|
482
|
+
pattern_lo=op16(constants.OP_PATTERN_LO),
|
|
483
|
+
pattern_hi=op16(constants.OP_PATTERN_HI),
|
|
484
|
+
instr=op16(constants.OP_INSTR),
|
|
485
|
+
freq_lo=op16(constants.OP_FREQ_LO),
|
|
486
|
+
freq_hi=op16(constants.OP_FREQ_HI),
|
|
487
|
+
inner_lo=op16(constants.OP_INNER_LO),
|
|
488
|
+
inner_hi=op16(constants.OP_INNER_HI),
|
|
489
|
+
arp=op16(constants.OP_ARP),
|
|
490
|
+
)
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
def iter_frames(song: Song, max_frames: Optional[int] = None):
|
|
494
|
+
"""Yield per-frame register write lists for ``song``.
|
|
495
|
+
|
|
496
|
+
Stops after ``max_frames`` frames (required for a non-looping render,
|
|
497
|
+
since the Music Assembler player loops forever).
|
|
498
|
+
"""
|
|
499
|
+
player = Player(song)
|
|
500
|
+
frame = 0
|
|
501
|
+
while max_frames is None or frame < max_frames:
|
|
502
|
+
yield player.play_frame()
|
|
503
|
+
frame += 1
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
def render_grid(song: Song, nframes: int) -> List[List[int]]:
|
|
507
|
+
"""Render ``nframes`` of forward-filled per-frame register snapshots."""
|
|
508
|
+
player = Player(song)
|
|
509
|
+
rows: List[List[int]] = []
|
|
510
|
+
for _ in range(nframes):
|
|
511
|
+
player.play_frame()
|
|
512
|
+
rows.append(player.regs[:])
|
|
513
|
+
return rows
|