trackmod 0.2.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.
Files changed (343) hide show
  1. trackmod/__init__.py +79 -0
  2. trackmod/binary/__init__.py +0 -0
  3. trackmod/binary/bits.py +35 -0
  4. trackmod/binary/cursor.py +101 -0
  5. trackmod/binary/layout.py +12 -0
  6. trackmod/binary/nibble.py +46 -0
  7. trackmod/binary/pcm/__init__.py +0 -0
  8. trackmod/binary/pcm/blocks.py +20 -0
  9. trackmod/binary/pcm/codec.py +53 -0
  10. trackmod/binary/pcm/encoding.py +13 -0
  11. trackmod/binary/pcm/quantize.py +19 -0
  12. trackmod/binary/pcm/sign.py +39 -0
  13. trackmod/binary/records/__init__.py +0 -0
  14. trackmod/binary/records/field.py +50 -0
  15. trackmod/binary/records/record.py +99 -0
  16. trackmod/binary/records/values.py +44 -0
  17. trackmod/binary/text.py +31 -0
  18. trackmod/binary/volume.py +114 -0
  19. trackmod/binary/warnings.py +40 -0
  20. trackmod/core/__init__.py +0 -0
  21. trackmod/core/effects/__init__.py +0 -0
  22. trackmod/core/effects/catalog.py +35 -0
  23. trackmod/core/effects/effect.py +17 -0
  24. trackmod/core/envelopes/__init__.py +0 -0
  25. trackmod/core/envelopes/curve.py +85 -0
  26. trackmod/core/envelopes/envelope.py +43 -0
  27. trackmod/core/envelopes/kind.py +10 -0
  28. trackmod/core/envelopes/point.py +16 -0
  29. trackmod/core/envelopes/repair.py +85 -0
  30. trackmod/core/envelopes/span.py +22 -0
  31. trackmod/core/instruments/__init__.py +0 -0
  32. trackmod/core/instruments/behavior.py +30 -0
  33. trackmod/core/instruments/fade.py +61 -0
  34. trackmod/core/instruments/instrument.py +122 -0
  35. trackmod/core/instruments/keymap.py +38 -0
  36. trackmod/core/instruments/repair.py +49 -0
  37. trackmod/core/instruments/transfer.py +95 -0
  38. trackmod/core/instruments/unit.py +42 -0
  39. trackmod/core/notes/__init__.py +0 -0
  40. trackmod/core/notes/checks.py +33 -0
  41. trackmod/core/notes/codec.py +24 -0
  42. trackmod/core/notes/command.py +20 -0
  43. trackmod/core/notes/pitch.py +93 -0
  44. trackmod/core/patterns/__init__.py +0 -0
  45. trackmod/core/patterns/builder.py +59 -0
  46. trackmod/core/patterns/cell.py +22 -0
  47. trackmod/core/patterns/codec.py +38 -0
  48. trackmod/core/patterns/column.py +19 -0
  49. trackmod/core/patterns/grid.py +148 -0
  50. trackmod/core/patterns/repair.py +36 -0
  51. trackmod/core/repairs/__init__.py +0 -0
  52. trackmod/core/repairs/report.py +47 -0
  53. trackmod/core/samples/__init__.py +0 -0
  54. trackmod/core/samples/depth.py +36 -0
  55. trackmod/core/samples/loop.py +58 -0
  56. trackmod/core/samples/repair.py +86 -0
  57. trackmod/core/samples/sample.py +140 -0
  58. trackmod/core/samples/vibrato.py +22 -0
  59. trackmod/core/songs/__init__.py +0 -0
  60. trackmod/core/songs/order.py +37 -0
  61. trackmod/core/songs/playback.py +17 -0
  62. trackmod/core/songs/repair.py +20 -0
  63. trackmod/core/songs/song.py +67 -0
  64. trackmod/core/timing/__init__.py +0 -0
  65. trackmod/core/timing/clock.py +50 -0
  66. trackmod/core/timing/lattice.py +122 -0
  67. trackmod/core/timing/timing.py +14 -0
  68. trackmod/core/timing/timings.py +54 -0
  69. trackmod/core/voices/__init__.py +0 -0
  70. trackmod/core/voices/convert.py +110 -0
  71. trackmod/core/voices/voices.py +68 -0
  72. trackmod/core/volumes/__init__.py +0 -0
  73. trackmod/core/volumes/checks.py +41 -0
  74. trackmod/core/volumes/codec.py +37 -0
  75. trackmod/core/volumes/command.py +60 -0
  76. trackmod/limits/__init__.py +0 -0
  77. trackmod/limits/bound.py +42 -0
  78. trackmod/limits/capability.py +45 -0
  79. trackmod/limits/capacity.py +41 -0
  80. trackmod/limits/checklist.py +30 -0
  81. trackmod/limits/compliance.py +22 -0
  82. trackmod/limits/error.py +31 -0
  83. trackmod/limits/guard.py +13 -0
  84. trackmod/limits/reach.py +35 -0
  85. trackmod/limits/table.py +99 -0
  86. trackmod/limits/tier.py +14 -0
  87. trackmod/limits/violation.py +40 -0
  88. trackmod/module/__init__.py +0 -0
  89. trackmod/module/instrument.py +96 -0
  90. trackmod/module/protocol.py +112 -0
  91. trackmod/module/provenance.py +39 -0
  92. trackmod/module/reaching.py +44 -0
  93. trackmod/module/size.py +33 -0
  94. trackmod/module/storage.py +106 -0
  95. trackmod/py.typed +0 -0
  96. trackmod/schema/__init__.py +0 -0
  97. trackmod/schema/array.py +52 -0
  98. trackmod/schema/config.py +6 -0
  99. trackmod/schema/scalars.py +39 -0
  100. trackmod/spec/__init__.py +0 -0
  101. trackmod/spec/application.py +13 -0
  102. trackmod/spec/clock.py +7 -0
  103. trackmod/spec/grid.py +14 -0
  104. trackmod/spec/levels.py +17 -0
  105. trackmod/spec/pitch.py +24 -0
  106. trackmod/spec/text.py +5 -0
  107. trackmod/spec/volumes.py +8 -0
  108. trackmod/spec/width.py +15 -0
  109. trackmod/trackers/amiga/__init__.py +0 -0
  110. trackmod/trackers/amiga/checks.py +65 -0
  111. trackmod/trackers/amiga/layout/__init__.py +0 -0
  112. trackmod/trackers/amiga/layout/file.py +10 -0
  113. trackmod/trackers/amiga/layout/sample.py +17 -0
  114. trackmod/trackers/amiga/note.py +122 -0
  115. trackmod/trackers/amiga/patterns/__init__.py +0 -0
  116. trackmod/trackers/amiga/patterns/packer.py +94 -0
  117. trackmod/trackers/amiga/patterns/parser.py +109 -0
  118. trackmod/trackers/amiga/patterns/sizing.py +11 -0
  119. trackmod/trackers/amiga/reading.py +140 -0
  120. trackmod/trackers/amiga/samples/__init__.py +0 -0
  121. trackmod/trackers/amiga/samples/parser.py +87 -0
  122. trackmod/trackers/amiga/samples/writer.py +122 -0
  123. trackmod/trackers/amiga/sizing.py +32 -0
  124. trackmod/trackers/amiga/spec/__init__.py +0 -0
  125. trackmod/trackers/amiga/spec/cells.py +19 -0
  126. trackmod/trackers/amiga/spec/defaults.py +7 -0
  127. trackmod/trackers/amiga/spec/periods.py +34 -0
  128. trackmod/trackers/amiga/spec/ranges.py +21 -0
  129. trackmod/trackers/amiga/spec/sizes.py +11 -0
  130. trackmod/trackers/amiga/spec/storage.py +9 -0
  131. trackmod/trackers/amiga/tuning.py +31 -0
  132. trackmod/trackers/amiga/writing.py +50 -0
  133. trackmod/trackers/it/__init__.py +0 -0
  134. trackmod/trackers/it/addressing.py +29 -0
  135. trackmod/trackers/it/checks.py +120 -0
  136. trackmod/trackers/it/detection.py +15 -0
  137. trackmod/trackers/it/effects/__init__.py +0 -0
  138. trackmod/trackers/it/effects/catalog.py +115 -0
  139. trackmod/trackers/it/effects/command.py +59 -0
  140. trackmod/trackers/it/extensions.py +118 -0
  141. trackmod/trackers/it/fade.py +19 -0
  142. trackmod/trackers/it/instrument_file.py +182 -0
  143. trackmod/trackers/it/instruments/__init__.py +0 -0
  144. trackmod/trackers/it/instruments/envelope.py +109 -0
  145. trackmod/trackers/it/instruments/keymap.py +53 -0
  146. trackmod/trackers/it/instruments/parser.py +100 -0
  147. trackmod/trackers/it/instruments/writer.py +82 -0
  148. trackmod/trackers/it/layout/__init__.py +0 -0
  149. trackmod/trackers/it/layout/envelope.py +51 -0
  150. trackmod/trackers/it/layout/file.py +38 -0
  151. trackmod/trackers/it/layout/instrument.py +54 -0
  152. trackmod/trackers/it/layout/pattern.py +14 -0
  153. trackmod/trackers/it/layout/sample.py +34 -0
  154. trackmod/trackers/it/limits.py +8 -0
  155. trackmod/trackers/it/message.py +50 -0
  156. trackmod/trackers/it/module.py +229 -0
  157. trackmod/trackers/it/note.py +43 -0
  158. trackmod/trackers/it/panning.py +12 -0
  159. trackmod/trackers/it/parser.py +309 -0
  160. trackmod/trackers/it/patterns/__init__.py +0 -0
  161. trackmod/trackers/it/patterns/cost.py +16 -0
  162. trackmod/trackers/it/patterns/encoded.py +9 -0
  163. trackmod/trackers/it/patterns/memory.py +19 -0
  164. trackmod/trackers/it/patterns/packer.py +178 -0
  165. trackmod/trackers/it/patterns/parser.py +211 -0
  166. trackmod/trackers/it/patterns/sizing.py +117 -0
  167. trackmod/trackers/it/patterns/width.py +36 -0
  168. trackmod/trackers/it/samples/__init__.py +0 -0
  169. trackmod/trackers/it/samples/compression.py +159 -0
  170. trackmod/trackers/it/samples/parser.py +288 -0
  171. trackmod/trackers/it/samples/writer.py +97 -0
  172. trackmod/trackers/it/settings.py +68 -0
  173. trackmod/trackers/it/sizing.py +55 -0
  174. trackmod/trackers/it/spec/__init__.py +0 -0
  175. trackmod/trackers/it/spec/capacities.py +122 -0
  176. trackmod/trackers/it/spec/cells.py +52 -0
  177. trackmod/trackers/it/spec/defaults.py +24 -0
  178. trackmod/trackers/it/spec/effects.py +19 -0
  179. trackmod/trackers/it/spec/extensions.py +13 -0
  180. trackmod/trackers/it/spec/flags.py +62 -0
  181. trackmod/trackers/it/spec/identity.py +14 -0
  182. trackmod/trackers/it/spec/orders.py +5 -0
  183. trackmod/trackers/it/spec/ranges.py +56 -0
  184. trackmod/trackers/it/spec/sizes.py +20 -0
  185. trackmod/trackers/it/spec/storage.py +27 -0
  186. trackmod/trackers/it/spec/volume.py +51 -0
  187. trackmod/trackers/it/timing.py +10 -0
  188. trackmod/trackers/it/version.py +100 -0
  189. trackmod/trackers/it/writer.py +133 -0
  190. trackmod/trackers/mod/__init__.py +0 -0
  191. trackmod/trackers/mod/detection.py +11 -0
  192. trackmod/trackers/mod/dialect.py +60 -0
  193. trackmod/trackers/mod/effects/__init__.py +0 -0
  194. trackmod/trackers/mod/effects/catalog.py +106 -0
  195. trackmod/trackers/mod/effects/command.py +63 -0
  196. trackmod/trackers/mod/layout/__init__.py +0 -0
  197. trackmod/trackers/mod/layout/file.py +17 -0
  198. trackmod/trackers/mod/limits.py +8 -0
  199. trackmod/trackers/mod/module.py +261 -0
  200. trackmod/trackers/mod/parser.py +102 -0
  201. trackmod/trackers/mod/settings.py +27 -0
  202. trackmod/trackers/mod/sizing.py +9 -0
  203. trackmod/trackers/mod/spec/__init__.py +0 -0
  204. trackmod/trackers/mod/spec/capacities.py +58 -0
  205. trackmod/trackers/mod/spec/dialects.py +34 -0
  206. trackmod/trackers/mod/spec/effects.py +19 -0
  207. trackmod/trackers/mod/spec/identity.py +12 -0
  208. trackmod/trackers/mod/spec/ranges.py +21 -0
  209. trackmod/trackers/mod/spec/sizes.py +13 -0
  210. trackmod/trackers/mod/spec/storage.py +17 -0
  211. trackmod/trackers/mod/tag.py +79 -0
  212. trackmod/trackers/mod/timing.py +6 -0
  213. trackmod/trackers/mod/writer.py +58 -0
  214. trackmod/trackers/registry.py +277 -0
  215. trackmod/trackers/s3m/__init__.py +0 -0
  216. trackmod/trackers/s3m/channels.py +38 -0
  217. trackmod/trackers/s3m/checks.py +91 -0
  218. trackmod/trackers/s3m/detection.py +10 -0
  219. trackmod/trackers/s3m/effects/__init__.py +0 -0
  220. trackmod/trackers/s3m/effects/catalog.py +105 -0
  221. trackmod/trackers/s3m/effects/command.py +54 -0
  222. trackmod/trackers/s3m/layout/__init__.py +0 -0
  223. trackmod/trackers/s3m/layout/file.py +36 -0
  224. trackmod/trackers/s3m/layout/instrument.py +28 -0
  225. trackmod/trackers/s3m/layout/pattern.py +10 -0
  226. trackmod/trackers/s3m/limits.py +8 -0
  227. trackmod/trackers/s3m/module.py +252 -0
  228. trackmod/trackers/s3m/note.py +101 -0
  229. trackmod/trackers/s3m/panning.py +22 -0
  230. trackmod/trackers/s3m/parapointers.py +41 -0
  231. trackmod/trackers/s3m/parser.py +233 -0
  232. trackmod/trackers/s3m/patterns/__init__.py +0 -0
  233. trackmod/trackers/s3m/patterns/packer.py +103 -0
  234. trackmod/trackers/s3m/patterns/parser.py +144 -0
  235. trackmod/trackers/s3m/patterns/sizing.py +37 -0
  236. trackmod/trackers/s3m/placement.py +89 -0
  237. trackmod/trackers/s3m/samples/__init__.py +0 -0
  238. trackmod/trackers/s3m/samples/parser.py +211 -0
  239. trackmod/trackers/s3m/samples/writer.py +102 -0
  240. trackmod/trackers/s3m/settings.py +67 -0
  241. trackmod/trackers/s3m/sizing.py +44 -0
  242. trackmod/trackers/s3m/spec/__init__.py +0 -0
  243. trackmod/trackers/s3m/spec/capacities.py +100 -0
  244. trackmod/trackers/s3m/spec/cells.py +45 -0
  245. trackmod/trackers/s3m/spec/defaults.py +17 -0
  246. trackmod/trackers/s3m/spec/effects.py +19 -0
  247. trackmod/trackers/s3m/spec/flags.py +57 -0
  248. trackmod/trackers/s3m/spec/identity.py +18 -0
  249. trackmod/trackers/s3m/spec/keys.py +16 -0
  250. trackmod/trackers/s3m/spec/orders.py +4 -0
  251. trackmod/trackers/s3m/spec/ranges.py +58 -0
  252. trackmod/trackers/s3m/spec/sizes.py +20 -0
  253. trackmod/trackers/s3m/spec/storage.py +35 -0
  254. trackmod/trackers/s3m/spec/volume.py +24 -0
  255. trackmod/trackers/s3m/timing.py +10 -0
  256. trackmod/trackers/s3m/version.py +98 -0
  257. trackmod/trackers/s3m/writer.py +176 -0
  258. trackmod/trackers/st/__init__.py +0 -0
  259. trackmod/trackers/st/detection.py +36 -0
  260. trackmod/trackers/st/effects/__init__.py +0 -0
  261. trackmod/trackers/st/effects/catalog.py +110 -0
  262. trackmod/trackers/st/effects/command.py +25 -0
  263. trackmod/trackers/st/layout/__init__.py +0 -0
  264. trackmod/trackers/st/layout/file.py +15 -0
  265. trackmod/trackers/st/limits.py +8 -0
  266. trackmod/trackers/st/module.py +244 -0
  267. trackmod/trackers/st/parser.py +95 -0
  268. trackmod/trackers/st/settings.py +20 -0
  269. trackmod/trackers/st/sizing.py +9 -0
  270. trackmod/trackers/st/spec/__init__.py +0 -0
  271. trackmod/trackers/st/spec/capacities.py +42 -0
  272. trackmod/trackers/st/spec/defaults.py +3 -0
  273. trackmod/trackers/st/spec/effects.py +10 -0
  274. trackmod/trackers/st/spec/identity.py +3 -0
  275. trackmod/trackers/st/spec/ranges.py +21 -0
  276. trackmod/trackers/st/spec/sizes.py +14 -0
  277. trackmod/trackers/st/spec/storage.py +17 -0
  278. trackmod/trackers/st/timing.py +10 -0
  279. trackmod/trackers/st/writer.py +51 -0
  280. trackmod/trackers/xm/__init__.py +0 -0
  281. trackmod/trackers/xm/addressing.py +22 -0
  282. trackmod/trackers/xm/checks.py +122 -0
  283. trackmod/trackers/xm/detection.py +20 -0
  284. trackmod/trackers/xm/effects/__init__.py +0 -0
  285. trackmod/trackers/xm/effects/catalog.py +113 -0
  286. trackmod/trackers/xm/effects/command.py +62 -0
  287. trackmod/trackers/xm/fade.py +19 -0
  288. trackmod/trackers/xm/instrument_file.py +188 -0
  289. trackmod/trackers/xm/instruments/__init__.py +0 -0
  290. trackmod/trackers/xm/instruments/envelope.py +118 -0
  291. trackmod/trackers/xm/instruments/group.py +45 -0
  292. trackmod/trackers/xm/instruments/grouping.py +100 -0
  293. trackmod/trackers/xm/instruments/keymap.py +26 -0
  294. trackmod/trackers/xm/instruments/parser.py +83 -0
  295. trackmod/trackers/xm/instruments/writer.py +144 -0
  296. trackmod/trackers/xm/layout/__init__.py +0 -0
  297. trackmod/trackers/xm/layout/envelope.py +75 -0
  298. trackmod/trackers/xm/layout/file.py +30 -0
  299. trackmod/trackers/xm/layout/instrument.py +79 -0
  300. trackmod/trackers/xm/layout/pattern.py +15 -0
  301. trackmod/trackers/xm/layout/sample.py +21 -0
  302. trackmod/trackers/xm/limits.py +8 -0
  303. trackmod/trackers/xm/module.py +246 -0
  304. trackmod/trackers/xm/note.py +83 -0
  305. trackmod/trackers/xm/parser.py +166 -0
  306. trackmod/trackers/xm/patterns/__init__.py +0 -0
  307. trackmod/trackers/xm/patterns/encoded.py +32 -0
  308. trackmod/trackers/xm/patterns/packer.py +80 -0
  309. trackmod/trackers/xm/patterns/parser.py +163 -0
  310. trackmod/trackers/xm/patterns/sizing.py +37 -0
  311. trackmod/trackers/xm/samples/__init__.py +0 -0
  312. trackmod/trackers/xm/samples/parser.py +139 -0
  313. trackmod/trackers/xm/samples/writer.py +73 -0
  314. trackmod/trackers/xm/settings.py +23 -0
  315. trackmod/trackers/xm/sizing.py +52 -0
  316. trackmod/trackers/xm/spec/__init__.py +0 -0
  317. trackmod/trackers/xm/spec/capacities.py +110 -0
  318. trackmod/trackers/xm/spec/cells.py +36 -0
  319. trackmod/trackers/xm/spec/defaults.py +13 -0
  320. trackmod/trackers/xm/spec/effects.py +19 -0
  321. trackmod/trackers/xm/spec/flags.py +37 -0
  322. trackmod/trackers/xm/spec/identity.py +13 -0
  323. trackmod/trackers/xm/spec/ranges.py +70 -0
  324. trackmod/trackers/xm/spec/sizes.py +32 -0
  325. trackmod/trackers/xm/spec/storage.py +27 -0
  326. trackmod/trackers/xm/spec/tuning.py +12 -0
  327. trackmod/trackers/xm/spec/volume.py +67 -0
  328. trackmod/trackers/xm/timing.py +10 -0
  329. trackmod/trackers/xm/tuning.py +73 -0
  330. trackmod/trackers/xm/writer.py +62 -0
  331. trackmod/utils/__init__.py +0 -0
  332. trackmod/utils/arithmetic.py +47 -0
  333. trackmod/wave/__init__.py +0 -0
  334. trackmod/wave/chunks.py +77 -0
  335. trackmod/wave/layout.py +88 -0
  336. trackmod/wave/parser.py +225 -0
  337. trackmod/wave/settings.py +25 -0
  338. trackmod/wave/spec.py +64 -0
  339. trackmod/wave/writer.py +213 -0
  340. trackmod-0.2.0.dist-info/METADATA +170 -0
  341. trackmod-0.2.0.dist-info/RECORD +343 -0
  342. trackmod-0.2.0.dist-info/WHEEL +4 -0
  343. trackmod-0.2.0.dist-info/licenses/LICENSE +21 -0
trackmod/__init__.py ADDED
@@ -0,0 +1,79 @@
1
+ import importlib.metadata
2
+
3
+ from trackmod.core.instruments.instrument import Instrument
4
+ from trackmod.core.instruments.transfer import combine, extract, units
5
+ from trackmod.core.instruments.unit import InstrumentUnit
6
+ from trackmod.core.samples.depth import BitDepth
7
+ from trackmod.core.samples.loop import Loop, LoopMode
8
+ from trackmod.core.samples.sample import Sample
9
+ from trackmod.core.songs.song import Song
10
+ from trackmod.core.voices.convert import flattened, raised
11
+ from trackmod.core.voices.voices import InstrumentVoices, SampleVoices, Voices
12
+ from trackmod.limits.compliance import Compliance
13
+ from trackmod.limits.error import LimitError
14
+ from trackmod.limits.violation import Violation
15
+ from trackmod.module.instrument import InstrumentFile
16
+ from trackmod.module.protocol import TrackerModule
17
+ from trackmod.trackers.it.instrument_file import ITInstrumentFile
18
+ from trackmod.trackers.it.module import ITModule
19
+ from trackmod.trackers.mod.module import MODModule
20
+ from trackmod.trackers.registry import (
21
+ EXTENSIONS,
22
+ INSTRUMENT_EXTENSIONS,
23
+ MODULE_EXTENSIONS,
24
+ detected,
25
+ load_module,
26
+ load_voices,
27
+ parse_module,
28
+ parse_voices,
29
+ )
30
+ from trackmod.trackers.s3m.module import S3MModule
31
+ from trackmod.trackers.st.module import STModule
32
+ from trackmod.trackers.xm.instrument_file import XMInstrumentFile
33
+ from trackmod.trackers.xm.module import XMModule
34
+ from trackmod.wave.parser import load_sample, parse_sample
35
+ from trackmod.wave.writer import save_sample, write_sample
36
+
37
+ __version__ = importlib.metadata.version("trackmod")
38
+
39
+ __all__ = [
40
+ "EXTENSIONS",
41
+ "INSTRUMENT_EXTENSIONS",
42
+ "MODULE_EXTENSIONS",
43
+ "BitDepth",
44
+ "Compliance",
45
+ "ITInstrumentFile",
46
+ "ITModule",
47
+ "Instrument",
48
+ "InstrumentFile",
49
+ "InstrumentUnit",
50
+ "InstrumentVoices",
51
+ "LimitError",
52
+ "Loop",
53
+ "LoopMode",
54
+ "MODModule",
55
+ "S3MModule",
56
+ "STModule",
57
+ "Sample",
58
+ "SampleVoices",
59
+ "Song",
60
+ "TrackerModule",
61
+ "Violation",
62
+ "Voices",
63
+ "XMInstrumentFile",
64
+ "XMModule",
65
+ "combine",
66
+ "detected",
67
+ "extract",
68
+ "flattened",
69
+ "load_module",
70
+ "load_sample",
71
+ "load_voices",
72
+ "parse_module",
73
+ "parse_sample",
74
+ "parse_voices",
75
+ "raised",
76
+ "save_sample",
77
+ "units",
78
+ "write_sample",
79
+ ]
File without changes
@@ -0,0 +1,35 @@
1
+ from trackmod.spec.width import BITS_PER_BYTE
2
+
3
+
4
+ class BitReader:
5
+ """Reads variable-width fields from a byte stream, least significant bit first.
6
+
7
+ A compressed waveform packs its values at a width that changes as it runs, so the reader keeps a
8
+ running buffer of whole bytes and hands out whatever width is asked for next. Bytes are drawn in as
9
+ the buffer runs dry, which is what makes one field cost a shift rather than a walk over its bits.
10
+ """
11
+
12
+ def __init__(self, data: bytes) -> None:
13
+ self._data = data
14
+ self._index = 0
15
+ self._buffer = 0
16
+ self._available = 0
17
+
18
+ def take(self, width: int) -> int:
19
+ """The next ``width`` bits, as an unsigned integer.
20
+
21
+ Raises:
22
+ ValueError: when the stream holds fewer bits than were asked for.
23
+ """
24
+ while self._available < width:
25
+ if self._index >= len(self._data):
26
+ raise ValueError(f"the stream holds {self._available} bits, short of the {width} asked for")
27
+
28
+ self._buffer |= self._data[self._index] << self._available
29
+ self._index += 1
30
+ self._available += BITS_PER_BYTE
31
+
32
+ value = self._buffer & ((1 << width) - 1)
33
+ self._buffer >>= width
34
+ self._available -= width
35
+ return value
@@ -0,0 +1,101 @@
1
+ from trackmod.binary.records.record import Record
2
+ from trackmod.binary.records.values import ArrayValue, FieldValue
3
+
4
+
5
+ class Cursor:
6
+ """Walks a module's bytes from front to back, which is how every format here is laid out.
7
+
8
+ A parser reads sections in the order the file stores them, so a cursor that remembers where it is
9
+ keeps every offset out of the parsing code.
10
+ """
11
+
12
+ def __init__(self, data: bytes) -> None:
13
+ self._data = data
14
+ self._position = 0
15
+
16
+ @property
17
+ def position(self) -> int:
18
+ """How many bytes have been consumed."""
19
+ return self._position
20
+
21
+ @property
22
+ def remaining(self) -> int:
23
+ """How many bytes are left."""
24
+ return len(self._data) - self._position
25
+
26
+ @property
27
+ def at_end(self) -> bool:
28
+ """Whether every byte has been consumed."""
29
+ return self.remaining == 0
30
+
31
+ def take(self, count: int) -> bytes:
32
+ """Consume and return the next ``count`` bytes.
33
+
34
+ Raises:
35
+ ValueError: when fewer than ``count`` bytes remain.
36
+ """
37
+ chunk = self.peek(count)
38
+ self._position += count
39
+ return chunk
40
+
41
+ def byte(self) -> int:
42
+ """Consume and return the next byte, which is what a stream of markers and columns is read in.
43
+
44
+ Raises:
45
+ ValueError: when the data is spent.
46
+ """
47
+ return self.take(1)[0]
48
+
49
+ def peek(self, count: int) -> bytes:
50
+ """Return the next ``count`` bytes without consuming them.
51
+
52
+ Raises:
53
+ ValueError: when fewer than ``count`` bytes remain.
54
+ """
55
+ if count < 0:
56
+ raise ValueError(f"cannot read {count} bytes")
57
+
58
+ if count > self.remaining:
59
+ raise ValueError(f"needed {count} bytes at offset {self._position}, {self.remaining} remain")
60
+
61
+ return self._data[self._position : self._position + count]
62
+
63
+ def take_at_most(self, count: int) -> bytes:
64
+ """Consume and return up to ``count`` bytes, stopping where the data ends.
65
+
66
+ A file may state a section longer than it goes on to hold, and a player sounds the part that is
67
+ there rather than refusing the file, so a reader following one needs to do the same.
68
+ """
69
+ return self.take(min(count, self.remaining))
70
+
71
+ def peek_padded(self, count: int) -> bytes:
72
+ """Return the next ``count`` bytes, padded with zeroes where the data ends.
73
+
74
+ This reads a fixed record out of a file that stops inside it: the fields the data reaches are
75
+ the ones it stated, and the rest read as the zeroes an empty record holds.
76
+ """
77
+ held = self.peek(min(count, self.remaining))
78
+ return held + bytes(count - len(held))
79
+
80
+ def skip(self, count: int) -> None:
81
+ """Consume ``count`` bytes without returning them.
82
+
83
+ Raises:
84
+ ValueError: when fewer than ``count`` bytes remain.
85
+ """
86
+ self.take(count)
87
+
88
+ def seek(self, position: int) -> None:
89
+ """Move to an absolute byte offset.
90
+
91
+ Raises:
92
+ ValueError: when the offset falls outside the data.
93
+ """
94
+ if not 0 <= position <= len(self._data):
95
+ raise ValueError(f"offset {position} falls outside {len(self._data)} bytes")
96
+
97
+ self._position = position
98
+
99
+ def read(self, record: Record) -> dict[str, FieldValue | ArrayValue]:
100
+ """Consume one record's worth of bytes and unpack its described fields."""
101
+ return record.unpack(self.take(record.size))
@@ -0,0 +1,12 @@
1
+ from collections.abc import Sequence
2
+
3
+
4
+ def offsets(blobs: Sequence[bytes], start: int) -> list[int]:
5
+ """Where each blob lands when they are laid end to end from ``start``."""
6
+ positions = []
7
+ position = start
8
+ for blob in blobs:
9
+ positions.append(position)
10
+ position += len(blob)
11
+
12
+ return positions
@@ -0,0 +1,46 @@
1
+ from trackmod.spec.width import (
2
+ BYTE_MAX,
3
+ DECIMAL_BYTE_MAX,
4
+ DECIMAL_RADIX,
5
+ NIBBLE_BITS,
6
+ NIBBLE_MAX,
7
+ )
8
+
9
+
10
+ def split_nibbles(value: int) -> tuple[int, int]:
11
+ """Split a byte into its high and low nibbles.
12
+
13
+ Raises:
14
+ ValueError: when ``value`` does not fit in a byte.
15
+ """
16
+ if not 0 <= value <= BYTE_MAX:
17
+ raise ValueError(f"{value} does not fit in a byte")
18
+
19
+ return value >> NIBBLE_BITS, value & NIBBLE_MAX
20
+
21
+
22
+ def join_nibbles(high: int, low: int) -> int:
23
+ """Combine two nibbles into a byte.
24
+
25
+ Raises:
26
+ ValueError: when either nibble exceeds four bits.
27
+ """
28
+ if not 0 <= high <= NIBBLE_MAX or not 0 <= low <= NIBBLE_MAX:
29
+ raise ValueError(f"nibbles {high}, {low} exceed four bits")
30
+
31
+ return (high << NIBBLE_BITS) | low
32
+
33
+
34
+ def decimal_byte(value: int) -> int:
35
+ """The byte a tracker reads back as the decimal number ``value``, a digit to each nibble.
36
+
37
+ Amiga ProTracker read one parameter as though its two hexadecimal digits were decimal ones, and the
38
+ trackers that followed it kept the reading, so 16 is stored as ``0x16``.
39
+
40
+ Raises:
41
+ ValueError: when ``value`` needs more than two decimal digits.
42
+ """
43
+ if not 0 <= value <= DECIMAL_BYTE_MAX:
44
+ raise ValueError(f"{value} needs more than two decimal digits")
45
+
46
+ return join_nibbles(*divmod(value, DECIMAL_RADIX))
File without changes
@@ -0,0 +1,20 @@
1
+ from trackmod.core.samples.depth import BitDepth
2
+ from trackmod.core.samples.sample import STEREO_CHANNELS
3
+
4
+
5
+ def whole_frames(block: bytes, *, depth: BitDepth) -> bytes:
6
+ """The head of a stored block that holds whole frames, which is as far as a cut-short block sounds."""
7
+ return block[: len(block) - len(block) % depth.bytes_per_frame]
8
+
9
+
10
+ def paired_channels(data: bytes, *, block: int, depth: BitDepth) -> tuple[bytes, bytes]:
11
+ """The two channel blocks of a stereo waveform, each read as far as both of them reach.
12
+
13
+ A stereo waveform holds each channel's frames in full, the left before the right, so a file stopping
14
+ inside the block holds all of the left channel and a part of the right. One frame is the pair of
15
+ amplitudes a player sounds together, so both channels are read to the length they share.
16
+ """
17
+ left = whole_frames(data[:block], depth=depth)
18
+ right = whole_frames(data[block : block * STEREO_CHANNELS], depth=depth)
19
+ held = min(len(left), len(right))
20
+ return left[:held], right[:held]
@@ -0,0 +1,53 @@
1
+ import numpy as np
2
+ from numpy.typing import NDArray
3
+
4
+ from trackmod.binary.pcm.encoding import PcmEncoding
5
+ from trackmod.binary.pcm.quantize import dequantize, quantize
6
+ from trackmod.binary.pcm.sign import PcmSign, bias, dtype_for
7
+ from trackmod.core.samples.depth import BitDepth
8
+
9
+
10
+ def encode_pcm(
11
+ pcm: NDArray[np.float64],
12
+ *,
13
+ depth: BitDepth,
14
+ encoding: PcmEncoding,
15
+ sign: PcmSign,
16
+ ) -> bytes:
17
+ """Serialize float PCM in ``[-1, 1]`` to stored frames.
18
+
19
+ The amplitudes are placed in the stored range before they are differenced, which is the order the
20
+ two axes compose in: a player integrates in the width it reads, so the differences it sums are
21
+ between the values that width holds.
22
+
23
+ Deltas are taken in a width wider than the store and cast back, so a difference that overshoots the
24
+ stored range wraps exactly as the player's running sum unwraps it. The first stored delta is taken
25
+ against zero, which makes it the waveform's first stored amplitude.
26
+ """
27
+ amplitudes = quantize(pcm, depth) + bias(depth, sign=sign)
28
+ match encoding:
29
+ case PcmEncoding.ABSOLUTE:
30
+ stored = amplitudes
31
+ case PcmEncoding.DELTA:
32
+ stored = np.diff(amplitudes, prepend=0)
33
+
34
+ return stored.astype(dtype_for(depth, sign=sign)).tobytes()
35
+
36
+
37
+ def decode_pcm(
38
+ data: bytes,
39
+ *,
40
+ depth: BitDepth,
41
+ encoding: PcmEncoding,
42
+ sign: PcmSign,
43
+ ) -> NDArray[np.float64]:
44
+ """Read stored frames back as float PCM in ``[-1, 1]``."""
45
+ dtype = dtype_for(depth, sign=sign)
46
+ stored = np.frombuffer(data, dtype=dtype)
47
+ match encoding:
48
+ case PcmEncoding.ABSOLUTE:
49
+ frames = stored
50
+ case PcmEncoding.DELTA:
51
+ frames = np.cumsum(stored, dtype=dtype)
52
+
53
+ return dequantize(frames.astype(np.int64) - bias(depth, sign=sign), depth)
@@ -0,0 +1,13 @@
1
+ from enum import StrEnum, unique
2
+
3
+
4
+ @unique
5
+ class PcmEncoding(StrEnum):
6
+ """How a format lays a waveform's frames out on disk.
7
+
8
+ Absolute storage writes each frame's amplitude; delta storage writes successive differences and the
9
+ player integrates them with a running sum in the stored width.
10
+ """
11
+
12
+ ABSOLUTE = "absolute"
13
+ DELTA = "delta"
@@ -0,0 +1,19 @@
1
+ import numpy as np
2
+ from numpy.typing import NDArray
3
+
4
+ from trackmod.core.samples.depth import BitDepth
5
+
6
+
7
+ def quantize(pcm: NDArray[np.float64], depth: BitDepth) -> NDArray[np.int64]:
8
+ """Map float PCM in ``[-1, 1]`` onto the signed integer range of ``depth``."""
9
+ scale = depth.scale
10
+ return np.clip(
11
+ np.round(np.asarray(pcm, dtype=np.float64) * scale),
12
+ -scale,
13
+ scale - 1,
14
+ ).astype(np.int64)
15
+
16
+
17
+ def dequantize(frames: NDArray[np.int64], depth: BitDepth) -> NDArray[np.float64]:
18
+ """Map stored integer frames back onto float PCM in ``[-1, 1]``."""
19
+ return np.asarray(frames, dtype=np.float64) / depth.scale
@@ -0,0 +1,39 @@
1
+ from enum import StrEnum, unique
2
+ from typing import Final
3
+
4
+ from trackmod.core.samples.depth import BitDepth
5
+
6
+ NO_BIAS: Final = 0
7
+
8
+
9
+ @unique
10
+ class PcmSign(StrEnum):
11
+ """Which stored value a frame of silence sits on.
12
+
13
+ Signed storage centers silence on zero, so a frame runs from ``-scale`` to ``scale - 1``. Unsigned
14
+ storage carries the same amplitudes shifted up by ``scale``, so silence sits in the middle of the
15
+ range and every stored value is positive.
16
+ """
17
+
18
+ SIGNED = "signed"
19
+ UNSIGNED = "unsigned"
20
+
21
+
22
+ STORED_DTYPES: Final[dict[PcmSign, dict[BitDepth, str]]] = {
23
+ PcmSign.SIGNED: {BitDepth.EIGHT: "<i1", BitDepth.SIXTEEN: "<i2"},
24
+ PcmSign.UNSIGNED: {BitDepth.EIGHT: "<u1", BitDepth.SIXTEEN: "<u2"},
25
+ }
26
+
27
+
28
+ def dtype_for(depth: BitDepth, *, sign: PcmSign) -> str:
29
+ """The little-endian numpy dtype one stored frame occupies."""
30
+ return STORED_DTYPES[sign][depth]
31
+
32
+
33
+ def bias(depth: BitDepth, *, sign: PcmSign) -> int:
34
+ """The amount that separates a stored frame from the signed amplitude it carries."""
35
+ match sign:
36
+ case PcmSign.SIGNED:
37
+ return NO_BIAS
38
+ case PcmSign.UNSIGNED:
39
+ return int(depth.scale)
File without changes
@@ -0,0 +1,50 @@
1
+ import struct
2
+
3
+ from pydantic import BaseModel
4
+ from pydantic import Field as ModelField
5
+
6
+ from trackmod.schema.config import FROZEN
7
+
8
+
9
+ class Field(BaseModel):
10
+ """One fixed field of a record: a :mod:`struct` value written at a byte offset.
11
+
12
+ ``code`` is a ``struct`` format for a single value, such as ``"<I"`` for a 32-bit word, ``"B"`` for a
13
+ byte, ``"b"`` for a signed byte, or ``"26s"`` for a fixed-length block the caller has already padded.
14
+ """
15
+
16
+ model_config = FROZEN
17
+
18
+ name: str = ModelField(min_length=1)
19
+ offset: int = ModelField(ge=0)
20
+ code: str = ModelField(min_length=1)
21
+
22
+ @property
23
+ def size(self) -> int:
24
+ """How many bytes the field occupies."""
25
+ return struct.calcsize(self.code)
26
+
27
+
28
+ class ArrayField(BaseModel):
29
+ """A contiguous run of fixed-stride elements, such as a keyboard note map.
30
+
31
+ ``code`` is the ``struct`` format for one element and its packed size is the stride; each value is a
32
+ sequence unpacked into that element.
33
+ """
34
+
35
+ model_config = FROZEN
36
+
37
+ name: str = ModelField(min_length=1)
38
+ offset: int = ModelField(ge=0)
39
+ count: int = ModelField(ge=0)
40
+ code: str = ModelField(min_length=1)
41
+
42
+ @property
43
+ def stride(self) -> int:
44
+ """How many bytes one element occupies."""
45
+ return struct.calcsize(self.code)
46
+
47
+ @property
48
+ def size(self) -> int:
49
+ """How many bytes the whole run occupies."""
50
+ return self.stride * self.count
@@ -0,0 +1,99 @@
1
+ from __future__ import annotations
2
+
3
+ import struct
4
+
5
+ from pydantic import BaseModel
6
+ from pydantic import Field as ModelField
7
+ from pydantic import model_validator
8
+
9
+ from trackmod.binary.records.field import ArrayField, Field
10
+ from trackmod.binary.records.values import ArrayValue, FieldValue, RecordValues
11
+ from trackmod.schema.config import FROZEN
12
+
13
+
14
+ class Record(BaseModel):
15
+ """A fixed-size binary record described as data: a byte size plus the fields laid out within it.
16
+
17
+ Stating the layout declaratively keeps every byte offset in one place, so the serializers supply
18
+ field values and never arithmetic. Offsets a record leaves undescribed are reserved and stay zero
19
+ when packing.
20
+ """
21
+
22
+ model_config = FROZEN
23
+
24
+ size: int = ModelField(ge=0)
25
+ fields: tuple[Field, ...]
26
+ arrays: tuple[ArrayField, ...] = ()
27
+
28
+ @model_validator(mode="after")
29
+ def _contained(self) -> Record:
30
+ for field in self.fields:
31
+ if field.offset + field.size > self.size:
32
+ raise ValueError(f"field {field.name!r} runs past the {self.size}-byte record")
33
+
34
+ for array in self.arrays:
35
+ if array.offset + array.size > self.size:
36
+ raise ValueError(f"array {array.name!r} runs past the {self.size}-byte record")
37
+
38
+ return self
39
+
40
+ def pack(self, values: RecordValues) -> bytes:
41
+ """Serialize ``values`` into exactly :attr:`size` bytes.
42
+
43
+ Raises:
44
+ KeyError: when a described field has no value.
45
+ TypeError: when an array field is given a single value rather than a sequence.
46
+ """
47
+ buffer = bytearray(self.size)
48
+ for field in self.fields:
49
+ struct.pack_into(field.code, buffer, field.offset, values[field.name])
50
+
51
+ for array in self.arrays:
52
+ rows = values[array.name]
53
+ if isinstance(rows, (int, bytes)):
54
+ raise TypeError(f"array {array.name!r} needs a sequence of elements, got {type(rows).__name__}")
55
+
56
+ for index, row in enumerate(rows):
57
+ struct.pack_into(
58
+ array.code,
59
+ buffer,
60
+ array.offset + index * array.stride,
61
+ *row,
62
+ )
63
+
64
+ return bytes(buffer)
65
+
66
+ def unpack_at(self, data: bytes, offset: int) -> dict[str, FieldValue | ArrayValue]:
67
+ """Read this record from ``offset``, filling it out with zeroes where the data stops inside it.
68
+
69
+ A file states where each of its records sits, and one stating an offset it never reaches is read
70
+ to the end of what it holds -- the fields the data covers are the ones it stated, and the rest
71
+ read as the zeroes an empty record holds.
72
+ """
73
+ return self.unpack(data[offset : offset + self.size].ljust(self.size, b"\0"))
74
+
75
+ def unpack(self, data: bytes) -> dict[str, FieldValue | ArrayValue]:
76
+ """Read every described field out of ``data``.
77
+
78
+ Raises:
79
+ ValueError: when ``data`` is shorter than the record.
80
+ """
81
+ if len(data) < self.size:
82
+ raise ValueError(f"record needs {self.size} bytes, got {len(data)}")
83
+
84
+ values: dict[str, FieldValue | ArrayValue] = {}
85
+ for field in self.fields:
86
+ (value,) = struct.unpack_from(field.code, data, field.offset)
87
+ values[field.name] = value
88
+
89
+ for array in self.arrays:
90
+ values[array.name] = tuple(
91
+ struct.unpack_from(
92
+ array.code,
93
+ data,
94
+ array.offset + index * array.stride,
95
+ )
96
+ for index in range(array.count)
97
+ )
98
+
99
+ return values
@@ -0,0 +1,44 @@
1
+ from collections.abc import Mapping, Sequence
2
+
3
+ FieldValue = int | bytes
4
+ ArrayValue = Sequence[Sequence[int]]
5
+ RecordValues = Mapping[str, FieldValue | ArrayValue]
6
+
7
+
8
+ def read_int(values: RecordValues, name: str) -> int:
9
+ """Read one unpacked field as an integer.
10
+
11
+ Raises:
12
+ TypeError: when the field holds a block of bytes or a run of elements instead.
13
+ """
14
+ value = values[name]
15
+ if not isinstance(value, int):
16
+ raise TypeError(f"field {name!r} holds {type(value).__name__}, expected an integer")
17
+
18
+ return value
19
+
20
+
21
+ def read_bytes(values: RecordValues, name: str) -> bytes:
22
+ """Read one unpacked field as a fixed-length byte block.
23
+
24
+ Raises:
25
+ TypeError: when the field holds an integer or a run of elements instead.
26
+ """
27
+ value = values[name]
28
+ if not isinstance(value, bytes):
29
+ raise TypeError(f"field {name!r} holds {type(value).__name__}, expected a byte block")
30
+
31
+ return value
32
+
33
+
34
+ def read_rows(values: RecordValues, name: str) -> ArrayValue:
35
+ """Read one unpacked array field as its run of elements.
36
+
37
+ Raises:
38
+ TypeError: when the field holds a single value instead.
39
+ """
40
+ value = values[name]
41
+ if isinstance(value, (int, bytes)):
42
+ raise TypeError(f"field {name!r} holds {type(value).__name__}, expected a run of elements")
43
+
44
+ return value
@@ -0,0 +1,31 @@
1
+ from trackmod.spec.text import ENCODING, PADDING, REPLACEMENT
2
+
3
+
4
+ def encode_name(text: str, length: int) -> bytes:
5
+ """Encode ``text`` to exactly ``length`` bytes, null-padded, truncating anything longer.
6
+
7
+ Tracker name fields are fixed-width and a player reads the whole width, so a name is padded rather
8
+ than terminated. Characters outside the tracker character set become ``?``.
9
+ """
10
+ raw = text.encode(ENCODING, errors=REPLACEMENT)[:length]
11
+ return raw + PADDING * (length - len(raw))
12
+
13
+
14
+ def decode_name(raw: bytes) -> str:
15
+ """Read a fixed-width name field back, dropping the padding a writer added."""
16
+ return raw.split(PADDING, 1)[0].decode(ENCODING, errors=REPLACEMENT)
17
+
18
+
19
+ def encode_text(text: str) -> bytes:
20
+ """Encode ``text`` as a block of its own length, closed by the terminator a reader stops at.
21
+
22
+ A block a header points at with an offset and a length is as long as its content, so the terminator
23
+ is what states where the text ends within the length the header reserves for it. Characters outside
24
+ the tracker character set become ``?``.
25
+ """
26
+ return text.encode(ENCODING, errors=REPLACEMENT) + PADDING
27
+
28
+
29
+ def decode_text(raw: bytes) -> str:
30
+ """Read a terminated text block back, up to where its terminator closes it."""
31
+ return raw.split(PADDING, 1)[0].decode(ENCODING, errors=REPLACEMENT)