pygoattracker 0.1.2__tar.gz → 0.1.4__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.
- {pygoattracker-0.1.2 → pygoattracker-0.1.4}/PKG-INFO +1 -1
- {pygoattracker-0.1.2 → pygoattracker-0.1.4}/pyproject.toml +1 -1
- {pygoattracker-0.1.2 → pygoattracker-0.1.4}/src/pygoattracker/player.py +70 -1
- {pygoattracker-0.1.2 → pygoattracker-0.1.4}/src/pygoattracker.egg-info/PKG-INFO +1 -1
- {pygoattracker-0.1.2 → pygoattracker-0.1.4}/tests/test_player.py +69 -0
- {pygoattracker-0.1.2 → pygoattracker-0.1.4}/LICENSE +0 -0
- {pygoattracker-0.1.2 → pygoattracker-0.1.4}/README.md +0 -0
- {pygoattracker-0.1.2 → pygoattracker-0.1.4}/setup.cfg +0 -0
- {pygoattracker-0.1.2 → pygoattracker-0.1.4}/src/pygoattracker/__init__.py +0 -0
- {pygoattracker-0.1.2 → pygoattracker-0.1.4}/src/pygoattracker/audio.py +0 -0
- {pygoattracker-0.1.2 → pygoattracker-0.1.4}/src/pygoattracker/cli.py +0 -0
- {pygoattracker-0.1.2 → pygoattracker-0.1.4}/src/pygoattracker/constants.py +0 -0
- {pygoattracker-0.1.2 → pygoattracker-0.1.4}/src/pygoattracker/convert.py +0 -0
- {pygoattracker-0.1.2 → pygoattracker-0.1.4}/src/pygoattracker/errors.py +0 -0
- {pygoattracker-0.1.2 → pygoattracker-0.1.4}/src/pygoattracker/legacy.py +0 -0
- {pygoattracker-0.1.2 → pygoattracker-0.1.4}/src/pygoattracker/model.py +0 -0
- {pygoattracker-0.1.2 → pygoattracker-0.1.4}/src/pygoattracker/ninja.py +0 -0
- {pygoattracker-0.1.2 → pygoattracker-0.1.4}/src/pygoattracker/reader.py +0 -0
- {pygoattracker-0.1.2 → pygoattracker-0.1.4}/src/pygoattracker/reglog.py +0 -0
- {pygoattracker-0.1.2 → pygoattracker-0.1.4}/src/pygoattracker/writer.py +0 -0
- {pygoattracker-0.1.2 → pygoattracker-0.1.4}/src/pygoattracker.egg-info/SOURCES.txt +0 -0
- {pygoattracker-0.1.2 → pygoattracker-0.1.4}/src/pygoattracker.egg-info/dependency_links.txt +0 -0
- {pygoattracker-0.1.2 → pygoattracker-0.1.4}/src/pygoattracker.egg-info/entry_points.txt +0 -0
- {pygoattracker-0.1.2 → pygoattracker-0.1.4}/src/pygoattracker.egg-info/requires.txt +0 -0
- {pygoattracker-0.1.2 → pygoattracker-0.1.4}/src/pygoattracker.egg-info/top_level.txt +0 -0
- {pygoattracker-0.1.2 → pygoattracker-0.1.4}/tests/test_audio.py +0 -0
- {pygoattracker-0.1.2 → pygoattracker-0.1.4}/tests/test_cli.py +0 -0
- {pygoattracker-0.1.2 → pygoattracker-0.1.4}/tests/test_convert.py +0 -0
- {pygoattracker-0.1.2 → pygoattracker-0.1.4}/tests/test_coverage.py +0 -0
- {pygoattracker-0.1.2 → pygoattracker-0.1.4}/tests/test_examples.py +0 -0
- {pygoattracker-0.1.2 → pygoattracker-0.1.4}/tests/test_legacy.py +0 -0
- {pygoattracker-0.1.2 → pygoattracker-0.1.4}/tests/test_lint.py +0 -0
- {pygoattracker-0.1.2 → pygoattracker-0.1.4}/tests/test_model.py +0 -0
- {pygoattracker-0.1.2 → pygoattracker-0.1.4}/tests/test_ninja.py +0 -0
- {pygoattracker-0.1.2 → pygoattracker-0.1.4}/tests/test_reader.py +0 -0
- {pygoattracker-0.1.2 → pygoattracker-0.1.4}/tests/test_reglog.py +0 -0
- {pygoattracker-0.1.2 → pygoattracker-0.1.4}/tests/test_roundtrip.py +0 -0
- {pygoattracker-0.1.2 → pygoattracker-0.1.4}/tests/test_writer.py +0 -0
|
@@ -89,9 +89,31 @@ class Player:
|
|
|
89
89
|
adparam: int = constants.DEFAULT_ADPARAM,
|
|
90
90
|
optimize_pulse: bool = True,
|
|
91
91
|
optimize_realtime: bool = True,
|
|
92
|
+
freq_table=None,
|
|
93
|
+
simplepulse: bool = False,
|
|
92
94
|
):
|
|
93
95
|
if not 0 <= subtune < len(song.subtunes):
|
|
94
96
|
raise GoatTrackerError(f"no such subtune: {subtune}")
|
|
97
|
+
# The editor playroutine (gplay.c) reads a fixed note-frequency table
|
|
98
|
+
# zero-padded to 128 entries. The PACKED player (gt2reloc/player.s) lays
|
|
99
|
+
# the table out as ``freqtbllo[firstnote..lastnote]`` followed
|
|
100
|
+
# immediately by ``freqtblhi[...]`` with NO padding, then the songtable;
|
|
101
|
+
# an out-of-range note (a wavetable relative step past the last note)
|
|
102
|
+
# therefore overruns into the adjacent relocated memory and yields an
|
|
103
|
+
# image-specific frequency. ``freq_table`` lets a caller (the packed-SID
|
|
104
|
+
# decompiler) supply that exact 128-entry table read from the image so
|
|
105
|
+
# the render reproduces the packed player's overrun frequencies. When
|
|
106
|
+
# None the editor's zero-padded ``constants.FREQ_TABLE`` is used.
|
|
107
|
+
self._freq_table = (
|
|
108
|
+
constants.FREQ_TABLE if freq_table is None else tuple(freq_table)
|
|
109
|
+
)
|
|
110
|
+
# ``simplepulse`` selects the packed player's SIMPLEPULSE pulse code
|
|
111
|
+
# path (greloc's one-byte pulse optimization), which computes pulse
|
|
112
|
+
# width differently from the editor player. The pulse table must then
|
|
113
|
+
# be the PACKED table (greloc's set-pulse byte + swapped speed); see
|
|
114
|
+
# ``_pulse_exec_simple``. Default False = the editor's full-mod path,
|
|
115
|
+
# so .SNG / editor playback is unchanged.
|
|
116
|
+
self.simplepulse = simplepulse
|
|
95
117
|
self.adparam = adparam & 0xFFFF
|
|
96
118
|
self.optimize_pulse = optimize_pulse
|
|
97
119
|
self.optimize_realtime = optimize_realtime
|
|
@@ -201,7 +223,7 @@ class Player:
|
|
|
201
223
|
return self._rtable[table][(ptr - 1) & 0xFF]
|
|
202
224
|
|
|
203
225
|
def _freq(self, note: int) -> int:
|
|
204
|
-
return
|
|
226
|
+
return self._freq_table[note & 0x7F]
|
|
205
227
|
|
|
206
228
|
def _speed_value(self, idx: int, chan: _Channel) -> int:
|
|
207
229
|
"""16-bit speedtable value; high bit selects realtime calculation."""
|
|
@@ -537,6 +559,9 @@ class Player:
|
|
|
537
559
|
self._toneporta(chan, chan.cmddata)
|
|
538
560
|
|
|
539
561
|
def _pulse_exec(self, chan: _Channel) -> None:
|
|
562
|
+
if self.simplepulse:
|
|
563
|
+
self._pulse_exec_simple(chan)
|
|
564
|
+
return
|
|
540
565
|
ptbl = constants.PTBL
|
|
541
566
|
if self._lt(ptbl, chan.pulse_table_ptr) == constants.TABLEJUMP:
|
|
542
567
|
chan.pulse_table_ptr = self._rt(ptbl, chan.pulse_table_ptr)
|
|
@@ -558,6 +583,50 @@ class Player:
|
|
|
558
583
|
if not chan.pulsetime:
|
|
559
584
|
chan.pulse_table_ptr = (chan.pulse_table_ptr + 1) & 0xFF
|
|
560
585
|
|
|
586
|
+
def _pulse_exec_simple(self, chan: _Channel) -> None:
|
|
587
|
+
"""SIMPLEPULSE pulse executor (player.s mt_setpulse/mt_pulsemod,
|
|
588
|
+
``.IF SIMPLEPULSE != 0``).
|
|
589
|
+
|
|
590
|
+
greloc's SIMPLEPULSE optimization (greloc.c ~888/1302) packs the
|
|
591
|
+
pulse table so a SET-PULSE step is ONE byte ``(pulsehi & 0x0f) |
|
|
592
|
+
(pulselo & 0xf0)`` and the modulation speed is pre-swapped
|
|
593
|
+
(``swapnybbles``). The packed player keeps a single ghost pulse byte:
|
|
594
|
+
mt_setpulse stores that one packed byte to BOTH ghostpulselo and
|
|
595
|
+
ghostpulsehi (no separate hi store, no ``& 0x0F``), and mt_pulsemod
|
|
596
|
+
does an 8-bit ``lo = lo + speed + carry`` accumulate writing the same
|
|
597
|
+
byte to both lo and hi. The SID then masks the high pulse nibble to 12
|
|
598
|
+
bits. The editor player (``_pulse_exec``) instead computes
|
|
599
|
+
``pulse = (left & 0x0f) << 8 | right`` giving pulse-hi 0 where this
|
|
600
|
+
packed player gives pulse-hi = the packed byte's low nibble.
|
|
601
|
+
|
|
602
|
+
``chan.pulse`` carries that single packed byte in its low 8 bits; the
|
|
603
|
+
12-bit SID pulse is ``((byte & 0x0f) << 8) | byte``.
|
|
604
|
+
"""
|
|
605
|
+
ptbl = constants.PTBL
|
|
606
|
+
if self._lt(ptbl, chan.pulse_table_ptr) == constants.TABLEJUMP:
|
|
607
|
+
chan.pulse_table_ptr = self._rt(ptbl, chan.pulse_table_ptr)
|
|
608
|
+
if not chan.pulse_table_ptr:
|
|
609
|
+
return
|
|
610
|
+
byte = chan.pulse & 0xFF
|
|
611
|
+
if not chan.pulsetime:
|
|
612
|
+
left = self._lt(ptbl, chan.pulse_table_ptr)
|
|
613
|
+
if left >= 0x80:
|
|
614
|
+
# Set pulse: one packed byte -> both lo and hi ghost registers.
|
|
615
|
+
byte = self._rt(ptbl, chan.pulse_table_ptr)
|
|
616
|
+
chan.pulse = ((byte & 0x0F) << 8) | byte
|
|
617
|
+
chan.pulse_table_ptr = (chan.pulse_table_ptr + 1) & 0xFF
|
|
618
|
+
return
|
|
619
|
+
chan.pulsetime = left
|
|
620
|
+
if chan.pulsetime:
|
|
621
|
+
# mt_pulsemod: lo = lo + speed + carry-out; hi = lo (same byte).
|
|
622
|
+
speed = self._rt(ptbl, chan.pulse_table_ptr)
|
|
623
|
+
total = byte + speed
|
|
624
|
+
byte = (total + (total >> 8)) & 0xFF
|
|
625
|
+
chan.pulse = ((byte & 0x0F) << 8) | byte
|
|
626
|
+
chan.pulsetime -= 1
|
|
627
|
+
if not chan.pulsetime:
|
|
628
|
+
chan.pulse_table_ptr = (chan.pulse_table_ptr + 1) & 0xFF
|
|
629
|
+
|
|
561
630
|
def _get_new_notes(self, channel: int, chan: _Channel) -> None:
|
|
562
631
|
pattern = self._patterns[chan.pattnum]
|
|
563
632
|
newnote = pattern[chan.pattptr]
|
|
@@ -89,6 +89,24 @@ def test_single_note_timeline(song):
|
|
|
89
89
|
assert (constants.CONTROL_REG, 0x09) in frames[55]
|
|
90
90
|
|
|
91
91
|
|
|
92
|
+
def test_freq_table_override(song):
|
|
93
|
+
"""A per-instance ``freq_table`` overrides the editor's fixed note table so
|
|
94
|
+
the packed-SID decompiler can reproduce the packed player's overrun
|
|
95
|
+
frequencies (it lays the table out unpadded, so out-of-range notes read
|
|
96
|
+
adjacent image bytes). Default (None) keeps the editor table."""
|
|
97
|
+
# The default render emits C-4 at the standard table frequency.
|
|
98
|
+
base = play(Player(song), 60)
|
|
99
|
+
assert (constants.FREQ_LO_REG, C4_FREQ & 0xFF) in base[8]
|
|
100
|
+
assert (constants.FREQ_HI_REG, C4_FREQ >> 8) in base[8]
|
|
101
|
+
# Override note 48 (C-4) to an arbitrary image-specific value; the render
|
|
102
|
+
# now emits that frequency for the same note, nothing else changes.
|
|
103
|
+
table = list(constants.FREQ_TABLE)
|
|
104
|
+
table[48] = 0xBEEF
|
|
105
|
+
frames = play(Player(song, freq_table=table), 60)
|
|
106
|
+
assert (constants.FREQ_LO_REG, 0xEF) in frames[8]
|
|
107
|
+
assert (constants.FREQ_HI_REG, 0xBE) in frames[8]
|
|
108
|
+
|
|
109
|
+
|
|
92
110
|
def test_keyoff_keyon(song):
|
|
93
111
|
song.patterns[0].rows[4] = Row(note=constants.KEYOFF)
|
|
94
112
|
song.patterns[0].rows[5] = Row(note=constants.KEYON)
|
|
@@ -323,6 +341,57 @@ def test_pulse_low_byte_unmasked(song):
|
|
|
323
341
|
assert player.regs[constants.PULSE_LO_REG] == 0x01
|
|
324
342
|
|
|
325
343
|
|
|
344
|
+
def test_simplepulse_set_writes_packed_byte_to_both_nibbles(song):
|
|
345
|
+
# greloc's SIMPLEPULSE optimization packs a set-pulse step into one byte and
|
|
346
|
+
# the packed player stores it to BOTH ghostpulselo and ghostpulsehi. For a
|
|
347
|
+
# packed set byte $88 the SID pulse is ((0x88 & 0x0f) << 8) | 0x88 == $888
|
|
348
|
+
# (pulse-hi nibble = the byte's low nibble), whereas the editor player
|
|
349
|
+
# computes (left & 0x0f) << 8 | right == $088 (pulse-hi 0).
|
|
350
|
+
song.pulsetable.left = [0x80, 0xFF]
|
|
351
|
+
song.pulsetable.right = [0x88, 0x00]
|
|
352
|
+
song.instruments[0].pulse_ptr = 1
|
|
353
|
+
editor = Player(song)
|
|
354
|
+
play(editor, 10)
|
|
355
|
+
assert (editor.regs[3] << 8) | editor.regs[2] == 0x088
|
|
356
|
+
|
|
357
|
+
packed = Player(song, simplepulse=True)
|
|
358
|
+
play(packed, 10)
|
|
359
|
+
assert (packed.regs[3] << 8) | packed.regs[2] == 0x888
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def test_simplepulse_modulation_accumulates_one_byte(song):
|
|
363
|
+
# SIMPLEPULSE modulation does lo = lo + speed + carry and mirrors the byte
|
|
364
|
+
# into pulse-hi. Start at packed byte $08, then a 16-step modulation of $10:
|
|
365
|
+
# the byte walks $08,$18,$28,... and pulse-hi tracks its low nibble ($8).
|
|
366
|
+
song.pulsetable.left = [0x80, 0x10, 0xFF]
|
|
367
|
+
song.pulsetable.right = [0x08, 0x10, 0x00]
|
|
368
|
+
song.instruments[0].pulse_ptr = 1
|
|
369
|
+
player = Player(song, simplepulse=True)
|
|
370
|
+
play(player, 10)
|
|
371
|
+
history = []
|
|
372
|
+
for _ in range(20):
|
|
373
|
+
player.play_frame()
|
|
374
|
+
history.append((player.regs[3], player.regs[2])) # (hi, lo)
|
|
375
|
+
# The defining SIMPLEPULSE invariant: the one ghost byte feeds both pulse
|
|
376
|
+
# registers, so pulse-hi is ALWAYS the low nibble of pulse-lo (the editor's
|
|
377
|
+
# 12-bit pulse keeps an independent hi byte and would not hold this).
|
|
378
|
+
assert all(hi == (lo & 0x0F) for hi, lo in history)
|
|
379
|
+
# While in the $10-per-step ramp the low byte climbs by $10 (low nibble
|
|
380
|
+
# fixed at $8) until a low-nibble carry advances the table.
|
|
381
|
+
assert history[0] == (0x08, 0x28)
|
|
382
|
+
assert history[2] == (0x08, 0x38)
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def test_simplepulse_default_off_matches_editor(song):
|
|
386
|
+
# Default simplepulse=False leaves the editor pulse path byte-identical.
|
|
387
|
+
song.pulsetable.left = [0x88, 0x10, 0xFF]
|
|
388
|
+
song.pulsetable.right = [0x00, 0x40, 0x00]
|
|
389
|
+
song.instruments[0].pulse_ptr = 1
|
|
390
|
+
a = reg_history(song, 40, constants.PULSE_HI_REG)
|
|
391
|
+
b = reg_history(song, 40, constants.PULSE_HI_REG, simplepulse=False)
|
|
392
|
+
assert a == b
|
|
393
|
+
|
|
394
|
+
|
|
326
395
|
def test_filter_program():
|
|
327
396
|
# A long pattern so the looping song does not retrigger the note
|
|
328
397
|
# (and with it the filter program) during the measurement.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|