pysidwizard 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.
- pysidwizard/__init__.py +74 -0
- pysidwizard/constants.py +163 -0
- pysidwizard/errors.py +9 -0
- pysidwizard/model.py +613 -0
- pysidwizard/player.py +2296 -0
- pysidwizard/reader.py +223 -0
- pysidwizard/writer.py +218 -0
- pysidwizard-0.1.0.dist-info/METADATA +550 -0
- pysidwizard-0.1.0.dist-info/RECORD +12 -0
- pysidwizard-0.1.0.dist-info/WHEEL +5 -0
- pysidwizard-0.1.0.dist-info/licenses/LICENSE +201 -0
- pysidwizard-0.1.0.dist-info/top_level.txt +1 -0
pysidwizard/__init__.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Pure-Python reader, writer, and player for SID-Wizard SWM modules.
|
|
2
|
+
|
|
3
|
+
SID-Wizard is a Commodore 64 tracker by Hermit (Mihaly Horvath). This
|
|
4
|
+
library implements its SWM module format (documented in
|
|
5
|
+
``native/sources/SWM-spec.src`` of the SID-Wizard source distribution)
|
|
6
|
+
from first principles — no native dependencies, no extension modules.
|
|
7
|
+
|
|
8
|
+
Three public surfaces:
|
|
9
|
+
|
|
10
|
+
* :func:`read_swm` / :func:`parse_swm` — parse an SWM file or byte
|
|
11
|
+
string into a :class:`SWMFile`.
|
|
12
|
+
* :func:`write_swm` / :func:`build_swm` — serialise an :class:`SWMFile`
|
|
13
|
+
back to disk or bytes; round-trips byte-exactly.
|
|
14
|
+
* :class:`SWMPlayer` (in :mod:`pysidwizard.player`) — per-frame
|
|
15
|
+
emulation of SID-Wizard's 6502 player IRQ. Output matches a real
|
|
16
|
+
SID-Wizard running inside ``asid-vice`` byte-for-byte, every frame ×
|
|
17
|
+
every SID register — verified by the integration test suite on
|
|
18
|
+
every PR.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from .constants import Waveform, attack_decay, straight_tempo, sustain_release
|
|
22
|
+
from .errors import SWMError, SWMFormatError
|
|
23
|
+
from .model import (
|
|
24
|
+
End,
|
|
25
|
+
Instrument,
|
|
26
|
+
Loop,
|
|
27
|
+
Pattern,
|
|
28
|
+
PlayPattern,
|
|
29
|
+
RawSequenceByte,
|
|
30
|
+
Row,
|
|
31
|
+
SequenceCommand,
|
|
32
|
+
SWMFile,
|
|
33
|
+
TempoOverride,
|
|
34
|
+
Transpose,
|
|
35
|
+
decode_sequence,
|
|
36
|
+
encode_sequence,
|
|
37
|
+
pack_pattern,
|
|
38
|
+
unpack_pattern,
|
|
39
|
+
)
|
|
40
|
+
from .player import SWMPlayer, iter_writes, render_wav, write_csv
|
|
41
|
+
from .reader import parse_swm, read_swm
|
|
42
|
+
from .writer import build_swm, write_swm
|
|
43
|
+
|
|
44
|
+
__all__ = [
|
|
45
|
+
"End",
|
|
46
|
+
"Instrument",
|
|
47
|
+
"Loop",
|
|
48
|
+
"Pattern",
|
|
49
|
+
"PlayPattern",
|
|
50
|
+
"RawSequenceByte",
|
|
51
|
+
"Row",
|
|
52
|
+
"SWMError",
|
|
53
|
+
"SWMFile",
|
|
54
|
+
"SWMFormatError",
|
|
55
|
+
"SWMPlayer",
|
|
56
|
+
"SequenceCommand",
|
|
57
|
+
"TempoOverride",
|
|
58
|
+
"Transpose",
|
|
59
|
+
"Waveform",
|
|
60
|
+
"attack_decay",
|
|
61
|
+
"build_swm",
|
|
62
|
+
"decode_sequence",
|
|
63
|
+
"encode_sequence",
|
|
64
|
+
"iter_writes",
|
|
65
|
+
"pack_pattern",
|
|
66
|
+
"parse_swm",
|
|
67
|
+
"read_swm",
|
|
68
|
+
"render_wav",
|
|
69
|
+
"straight_tempo",
|
|
70
|
+
"sustain_release",
|
|
71
|
+
"unpack_pattern",
|
|
72
|
+
"write_csv",
|
|
73
|
+
"write_swm",
|
|
74
|
+
]
|
pysidwizard/constants.py
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"""SWM file-format constants.
|
|
2
|
+
|
|
3
|
+
The numeric constants below mirror those in the upstream SID-Wizard source
|
|
4
|
+
file ``native/sources/SWM-spec.src``. They are reproduced here so the library
|
|
5
|
+
can be used without the C source distribution.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from enum import IntFlag
|
|
9
|
+
|
|
10
|
+
# Magic identifiers
|
|
11
|
+
SWM_MAGIC = b"SWM1"
|
|
12
|
+
SWS_MAGIC = b"SWMS"
|
|
13
|
+
|
|
14
|
+
# Header layout (offsets are relative to the SWM data start, i.e. *after* any
|
|
15
|
+
# 2-byte PRG load address that may prefix the file on disk).
|
|
16
|
+
TUNE_HEADER_SIZE = 0x40 # 64 bytes
|
|
17
|
+
MAGIC_POS = 0
|
|
18
|
+
FRAMESPEED_POS = 0x04
|
|
19
|
+
HIGHLIGHT_POS = 0x05
|
|
20
|
+
AUTO_POS = 0x06 # obsolete in SWM1
|
|
21
|
+
CONFIG_BITS_POS = 0x07 # obsolete in SWM1
|
|
22
|
+
MUTE_POS = 0x08 # 3 bytes: 0x08..0x0A
|
|
23
|
+
DEFAULT_PATTERN_LEN_POS = 0x0B
|
|
24
|
+
SEQUENCE_AMOUNT_POS = 0x0C
|
|
25
|
+
PATTERN_AMOUNT_POS = 0x0D
|
|
26
|
+
INSTRUMENT_AMOUNT_POS = 0x0E
|
|
27
|
+
CHORD_LENGTH_POS = 0x0F
|
|
28
|
+
TEMPO_LENGTH_POS = 0x10
|
|
29
|
+
COLOR_THEME_POS = 0x11 # obsolete in SWM1
|
|
30
|
+
KEYBOARD_TYPE_POS = 0x12 # obsolete in SWM1
|
|
31
|
+
DRIVER_TYPE_POS = 0x13
|
|
32
|
+
TUNING_TYPE_POS = 0x14
|
|
33
|
+
RESERVED_POS = 0x15 # bytes 0x15..0x17
|
|
34
|
+
AUTHOR_POS = 0x18
|
|
35
|
+
AUTHOR_LEN = TUNE_HEADER_SIZE - AUTHOR_POS # 40 bytes
|
|
36
|
+
|
|
37
|
+
# Hardware limits (used for validation, not enforced strictly in parsing).
|
|
38
|
+
SID_CHANNELS = 3
|
|
39
|
+
INSTRUMENT_NAME_LEN = 8
|
|
40
|
+
MAX_PATTERN_LEN = 249 # 0xF9
|
|
41
|
+
MAX_INSTRUMENT_SIZE = 128 # 0x80
|
|
42
|
+
MAX_SEQUENCE_LEN = 126 # 0x7E
|
|
43
|
+
MAX_PATTERN_AMOUNT = 100
|
|
44
|
+
MAX_INSTRUMENT_AMOUNT = 37
|
|
45
|
+
|
|
46
|
+
# Default PRG load address used by SID-Wizard's SWM export.
|
|
47
|
+
DEFAULT_LOAD_ADDRESS = 0x1FF8
|
|
48
|
+
|
|
49
|
+
# Pattern NOP-packing range. Each packed byte ``PACKED_MIN+k`` expands to
|
|
50
|
+
# ``k+2`` consecutive zero bytes when preceded by a zero or another packed
|
|
51
|
+
# byte in the source stream.
|
|
52
|
+
PACKED_MIN = 0x70
|
|
53
|
+
PACKED_MAX = 0x77
|
|
54
|
+
|
|
55
|
+
# Pattern note-column effect-value boundaries (informational).
|
|
56
|
+
NOTE_MAX = 0x5F
|
|
57
|
+
VIBRATO_FX = 0x60
|
|
58
|
+
PORTAMENTO_FX = 0x78
|
|
59
|
+
SYNC_ON_FX = 0x79
|
|
60
|
+
SYNC_OFF_FX = 0x7A
|
|
61
|
+
RING_ON_FX = 0x7B
|
|
62
|
+
RING_OFF_FX = 0x7C
|
|
63
|
+
GATE_ON_FX = 0x7D
|
|
64
|
+
GATE_OFF_FX = 0x7E
|
|
65
|
+
|
|
66
|
+
# Universal terminator used in instrument WF / PW / filter tables. On disk,
|
|
67
|
+
# the *final* filter-table terminator is replaced with the instrument's
|
|
68
|
+
# size byte; the writer handles that translation automatically.
|
|
69
|
+
TABLE_END = 0xFF
|
|
70
|
+
|
|
71
|
+
# Pattern row flag. The note and instrument columns of a row both use
|
|
72
|
+
# bit 7 to signal "the next column byte follows me". A row that consists
|
|
73
|
+
# of just a note (no instrument or fx) leaves the bit clear.
|
|
74
|
+
PATTERN_NEXT_COLUMN_FLAG = 0x80
|
|
75
|
+
|
|
76
|
+
# SWM note-column reference value. SID-Wizard's converter treats SWM
|
|
77
|
+
# note 49 (0x31) as the equivalent of MIDI middle C.
|
|
78
|
+
SWM_C5_NOTE = 49
|
|
79
|
+
|
|
80
|
+
# Sequence (orderlist) commands. Anything ``< 0x80`` in a sequence is a
|
|
81
|
+
# pattern reference; the values below are top-bit-set commands recognised
|
|
82
|
+
# by the player and by SID-Wizard's exporters.
|
|
83
|
+
SEQUENCE_END = 0xFE # exit subtune without looping
|
|
84
|
+
SEQUENCE_END_WITH_LOOP = 0xFF # exit, followed by a 1-byte loop position
|
|
85
|
+
SEQUENCE_TRANSPOSE_BASE = 0x90 # 0x90..0x9F sets a -16..+15 semitone shift
|
|
86
|
+
SEQUENCE_TEMPO_BASE = 0xB0 # 0xB0..0xFD overrides the subtune tempo
|
|
87
|
+
|
|
88
|
+
# Subtune funktempo: bit 7 of a tempo byte means "use the low seven bits
|
|
89
|
+
# straight, do not average with the partner byte".
|
|
90
|
+
TEMPO_STRAIGHT_FLAG = 0x80
|
|
91
|
+
|
|
92
|
+
# Instrument layout. These offsets index the bytes *before* the 8-byte
|
|
93
|
+
# instrument name on disk. ``INST_WF_TABLE_POS`` doubles as the size of
|
|
94
|
+
# the fixed instrument header; the variable-length WF / PW / Filter
|
|
95
|
+
# tables start there.
|
|
96
|
+
INST_HEADER_SIZE = 0x10
|
|
97
|
+
INST_CONTROL_POS = 0x00
|
|
98
|
+
INST_HR_AD_POS = 0x01 # hard-restart attack/decay
|
|
99
|
+
INST_HR_SR_POS = 0x02 # hard-restart sustain/release
|
|
100
|
+
INST_AD_POS = 0x03 # note-start attack/decay
|
|
101
|
+
INST_SR_POS = 0x04 # note-start sustain/release
|
|
102
|
+
INST_VIBRATO_POS = 0x05 # vibrato freq (low nibble) + amp (high)
|
|
103
|
+
INST_VIBRATO_DELAY_POS = 0x06
|
|
104
|
+
INST_ARP_SPEED_POS = 0x07
|
|
105
|
+
INST_DEFAULT_CHORD_POS = 0x08
|
|
106
|
+
INST_OCTAVE_POS = 0x09 # signed 2's-complement semitone shift
|
|
107
|
+
INST_PW_TABLE_PTR_POS = 0x0A # PW-table offset, relative to instbase
|
|
108
|
+
INST_FILTER_TABLE_PTR_POS = 0x0B
|
|
109
|
+
INST_GATEOFF_WF_POS = 0x0C
|
|
110
|
+
INST_GATEOFF_PW_POS = 0x0D
|
|
111
|
+
INST_GATEOFF_FILT_POS = 0x0E
|
|
112
|
+
INST_FIRST_WAVEFORM_POS = 0x0F # 1st-frame waveform (see :class:`Waveform`)
|
|
113
|
+
INST_WF_TABLE_POS = INST_HEADER_SIZE
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class Waveform(IntFlag):
|
|
117
|
+
"""SID waveform bits as encoded by SID-Wizard.
|
|
118
|
+
|
|
119
|
+
Each flag corresponds to one of the SID's four waveform generators.
|
|
120
|
+
Combine with ``|`` to mix waveforms (e.g. ``Waveform.TRIANGLE |
|
|
121
|
+
Waveform.PULSE`` selects a tied triangle+pulse output). A zero value
|
|
122
|
+
selects silence.
|
|
123
|
+
"""
|
|
124
|
+
|
|
125
|
+
TRIANGLE = 0x01
|
|
126
|
+
SAWTOOTH = 0x02
|
|
127
|
+
PULSE = 0x04
|
|
128
|
+
NOISE = 0x08
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def attack_decay(attack: int, decay: int) -> int:
|
|
132
|
+
"""Pack an Attack/Decay nibble pair into a single ADSR-register byte.
|
|
133
|
+
|
|
134
|
+
Both nibbles are in the SID's 0..15 range. Used to assemble the
|
|
135
|
+
:data:`INST_AD_POS` and :data:`INST_HR_AD_POS` instrument bytes.
|
|
136
|
+
"""
|
|
137
|
+
if not (0 <= attack <= 0xF and 0 <= decay <= 0xF):
|
|
138
|
+
raise ValueError("attack and decay must each be 0..15")
|
|
139
|
+
return (attack << 4) | decay
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def sustain_release(sustain: int, release: int) -> int:
|
|
143
|
+
"""Pack a Sustain/Release nibble pair into a single ADSR-register byte.
|
|
144
|
+
|
|
145
|
+
Both nibbles are in the SID's 0..15 range. Used to assemble the
|
|
146
|
+
:data:`INST_SR_POS` and :data:`INST_HR_SR_POS` instrument bytes.
|
|
147
|
+
"""
|
|
148
|
+
if not (0 <= sustain <= 0xF and 0 <= release <= 0xF):
|
|
149
|
+
raise ValueError("sustain and release must each be 0..15")
|
|
150
|
+
return (sustain << 4) | release
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def straight_tempo(frames_per_row: int) -> int:
|
|
154
|
+
"""Encode a "straight" (non-funky) subtune-tempo byte.
|
|
155
|
+
|
|
156
|
+
``frames_per_row`` is the player's row delay in PAL frames (1..127);
|
|
157
|
+
the returned byte sets :data:`TEMPO_STRAIGHT_FLAG` so the runtime uses
|
|
158
|
+
it directly instead of averaging with its partner byte in a funktempo
|
|
159
|
+
pair.
|
|
160
|
+
"""
|
|
161
|
+
if not (1 <= frames_per_row <= 0x7F):
|
|
162
|
+
raise ValueError("frames_per_row must be 1..127")
|
|
163
|
+
return TEMPO_STRAIGHT_FLAG | frames_per_row
|
pysidwizard/errors.py
ADDED