├── .git-blame-ignore-revs ├── .github ├── mkdocs.sh └── workflows │ └── deploy-docs.yml ├── .gitignore ├── .python-version ├── LICENSE.txt ├── chipchune ├── __init__.py ├── _util.py ├── deflemask │ └── __init__.py ├── famitracker │ └── __init__.py ├── furnace │ ├── __init__.py │ ├── data_types.py │ ├── enums.py │ ├── instrument.py │ ├── module.py │ ├── sample.py │ └── wavetable.py ├── interchange │ ├── __init__.py │ ├── enums.py │ └── furnace.py └── utils │ ├── __init__.py │ └── conversion.py ├── examples └── furnace │ ├── files │ └── rocky_mountain.197.fur │ └── fur2smps.py ├── mypy.ini ├── setup.py └── tests ├── __init__.py ├── samples ├── README.md └── furnace │ ├── atlantis_pv1000.144.fur │ ├── atlantis_pv1000.144x.fur │ ├── bass.new.fui │ ├── blank.143.fur │ ├── blank.143.uncompressed.fur │ ├── blank.143.uncompressed.no_patch_bay.fur │ ├── dppt_youngster.70.fur │ ├── funnybones.127.fur │ ├── lawnstring.new.fui │ ├── macros.70.fur │ ├── mahbod-intro.143.fur │ ├── mahbod-intro.181.fur │ ├── map04.140.fur │ ├── map04.140.uncompressed.fur │ ├── opl1_brass.new.fui │ ├── opl1_brass.old.fui │ ├── opldrums.70.fur │ ├── r32.127.fur │ ├── skate_or_die.143.fur │ ├── skate_or_die.143.uncompressed.fur │ ├── skate_or_die.181.fur │ ├── skate_or_die.181.uncompressed.fur │ ├── skate_or_die.181.wave.1.fuw │ ├── skate_or_die.70.fur │ ├── skate_or_die.70.uncompressed.fur │ ├── spooky_birthday.181.fur │ ├── spooky_birthday.181.uncompressed.fur │ ├── tsu.new.fui │ ├── tsu.old.fui │ ├── viridian.127.fur │ ├── viridian.181.fur │ ├── viridian.70.fur │ ├── waveta.new.fui │ └── waveta.old.fui └── test_furnace.py /.git-blame-ignore-revs: -------------------------------------------------------------------------------- 1 | # Reformat via Black 2 | 7f2fcdf3e0e19b22ca149ad67c9ec1af84a86866 -------------------------------------------------------------------------------- /.github/mkdocs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -eu 4 | 5 | BUILDROOT="docbuild" 6 | 7 | pdoc --no-show-source -o "$BUILDROOT" chipchune 8 | 9 | [ "$GH_PASSWORD" ] || exit 12 10 | 11 | git clone -b gh-pages "https://zoomten:$GH_PASSWORD@github.com/$GITHUB_REPOSITORY.git" gh-pages 12 | 13 | cp -R docbuild/* gh-pages/ 14 | 15 | cd gh-pages 16 | 17 | git add * 18 | 19 | if git diff --staged --quiet; then 20 | echo "$0: No changes to commit." 21 | exit 0 22 | fi 23 | 24 | if ! git config user.name; then 25 | git config user.name 'github-actions' 26 | git config user.email '41898282+github-actions[bot]@users.noreply.github.com' 27 | fi 28 | 29 | git commit -a -m "CI: Update docs" 30 | git push 31 | -------------------------------------------------------------------------------- /.github/workflows/deploy-docs.yml: -------------------------------------------------------------------------------- 1 | name: Deploy docs 2 | on: 3 | push: 4 | branches: ["master"] 5 | 6 | jobs: 7 | deploy: 8 | name: Deploy 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - name: Set up Python 13 | uses: actions/setup-python@v2 14 | with: 15 | python-version: 3.8 16 | 17 | - uses: actions/cache@v2 18 | name: Set up caches 19 | with: 20 | path: ~/.cache/pip 21 | key: ${{ runner.os }} 22 | 23 | - name: Checkout repo 24 | uses: actions/checkout@v2 25 | with: 26 | fetch-depth: 3 27 | 28 | - name: Install dependencies 29 | run: | 30 | pip install -U pip 31 | pip install pdoc 32 | 33 | - name: Build and deploy docs 34 | env: 35 | GH_PASSWORD: ${{ secrets.UPDATE_DOCS_TOKEN }} 36 | run: .github/mkdocs.sh 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | **__pycache__** 3 | .coverage 4 | build/ 5 | *.egg-info/ 6 | -------------------------------------------------------------------------------- /.python-version: -------------------------------------------------------------------------------- 1 | 3.8.13 2 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2023 Zumi Daxuya 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /chipchune/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | :mod:`chipchune` is a Python library for manipulating several 3 | different kinds of chiptune music files. 4 | 5 | Currently supports: 6 | - Furnace (:mod:`chipchune.furnace`) (almost!) 7 | 8 | Plans to support: 9 | - DefleMask (:mod:`chipchune.deflemask`) 10 | - FamiTracker (:mod:`chipchune.famitracker`) 11 | 12 | ### Installation 13 | 14 | `pip install git+https://github.com/ZoomTen/chipchune@master` 15 | 16 | """ 17 | 18 | __version__ = "0.0.1" 19 | -------------------------------------------------------------------------------- /chipchune/_util.py: -------------------------------------------------------------------------------- 1 | """ 2 | This is for internal use; not to be confused with 3 | `chipchune.utils`. 4 | """ 5 | 6 | import struct 7 | from enum import Enum 8 | from typing import BinaryIO, Any, cast 9 | import io 10 | 11 | known_sizes = { 12 | "c": 1, 13 | "b": 1, 14 | "B": 1, 15 | "?": 1, 16 | "h": 2, 17 | "H": 2, 18 | "i": 4, 19 | "I": 4, 20 | "l": 4, 21 | "L": 4, 22 | "q": 8, 23 | "Q": 8, 24 | "e": 2, 25 | "f": 4, 26 | "d": 8, 27 | } 28 | 29 | 30 | class EnumShowNameOnly(Enum): 31 | """ 32 | Just an Enum, except its string repr is 33 | just the enum's name 34 | """ 35 | 36 | def __repr__(self) -> str: 37 | return self.name 38 | 39 | def __str__(self) -> str: 40 | return self.__repr__() 41 | 42 | 43 | class EnumValueEquals(Enum): 44 | """ 45 | Enum that can be compared to its raw value. 46 | """ 47 | 48 | def __eq__(self, other: Any) -> bool: 49 | return cast(bool, self.value == other) 50 | 51 | 52 | def truthy_to_boolbyte(value: Any) -> bytes: 53 | """ 54 | If value is truthy, output b'\x01'. Else output b'\x00'. 55 | 56 | :param value: anything 57 | """ 58 | if value: 59 | return b"\x01" 60 | else: 61 | return b"\x00" 62 | 63 | 64 | # these are just to make the typehinter happy 65 | # cast(dolphin, foobar) should've been named trust_me_bro_im_a(dolphin, foobar) 66 | 67 | 68 | def read_int(file: BinaryIO, signed: bool = False) -> int: 69 | """ 70 | 4 bytes 71 | """ 72 | if signed: 73 | return cast(int, struct.unpack(" int: 78 | """ 79 | 2 bytes 80 | """ 81 | if signed: 82 | return cast(int, struct.unpack(" int: 87 | """ 88 | 1 bytes 89 | """ 90 | if signed: 91 | return cast(int, struct.unpack(" float: 96 | """ 97 | 4 bytes 98 | """ 99 | return cast(float, struct.unpack(" str: 103 | """ 104 | variable string (ends in \\x00) 105 | """ 106 | buffer = bytearray() 107 | char = file.read(1) 108 | while char != b"\x00": 109 | buffer += char 110 | char = file.read(1) 111 | return buffer.decode("utf-8") 112 | -------------------------------------------------------------------------------- /chipchune/deflemask/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | soon! 3 | """ 4 | -------------------------------------------------------------------------------- /chipchune/famitracker/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | soon! 3 | """ 4 | -------------------------------------------------------------------------------- /chipchune/furnace/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tools to manipulate Furnace .fur files. 3 | 4 | - :mod:`chipchune.furnace.module`: Tools to inspect and manipulate module files. 5 | - :mod:`chipchune.furnace.instrument`: Tools to inspect and manipulate instrument data from within or without the module. 6 | - :mod:`chipchune.furnace.sample`: Tools to inspect and manipulate sample data (might be merged with inst?) 7 | - :mod:`chipchune.furnace.wavetable`: Tools to inspect and manipulate wavetable data 8 | - :mod:`chipchune.furnace.enums`: Various constants that apply to Furnace. 9 | - :mod:`chipchune.furnace.data_types`: Various data types that apply to Furnace. 10 | 11 | 12 | ### Example 13 | 14 | from chipchune.furnace.module import FurnaceModule 15 | 16 | module = FurnaceModule("tests/samples/furnace/skate_or_die.143.fur") 17 | 18 | pattern = module.get_pattern(0, 0, 0) 19 | 20 | print(pattern.as_clipboard()) 21 | 22 | for row in pattern.data: 23 | print(row) 24 | """ 25 | -------------------------------------------------------------------------------- /chipchune/furnace/data_types.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass, field 2 | from typing import Tuple, List, TypedDict, Any, Union, Dict 3 | 4 | from .enums import ( 5 | ChipType, 6 | LinearPitch, 7 | LoopModality, 8 | DelayBehavior, 9 | JumpTreatment, 10 | InputPortSet, 11 | OutputPortSet, 12 | InstrumentType, 13 | MacroCode, 14 | OpMacroCode, 15 | MacroType, 16 | MacroItem, 17 | GBHwCommand, 18 | WaveFX, 19 | ESFilterMode, 20 | SNESSusMode, 21 | GainMode, 22 | Note, 23 | ) 24 | 25 | 26 | # modules 27 | @dataclass 28 | class ChipInfo: 29 | """ 30 | Information on a single chip. 31 | """ 32 | 33 | type: ChipType 34 | #: shall be a simple dict, no enums needed 35 | flags: Dict[str, Any] = field(default_factory=dict) 36 | panning: float = 0.0 37 | surround: float = 0.0 38 | """ 39 | Chip front/rear balance. 40 | """ 41 | volume: float = 1.0 42 | 43 | 44 | @dataclass 45 | class ModuleMeta: 46 | """ 47 | Module metadata. 48 | """ 49 | 50 | name: str = "" 51 | name_jp: str = "" 52 | author: str = "" 53 | author_jp: str = "" 54 | album: str = "" 55 | """ 56 | Can also be the game name or container name. 57 | """ 58 | album_jp: str = "" 59 | sys_name: str = "Sega Genesis/Mega Drive" 60 | sys_name_jp: str = "" 61 | comment: str = "" 62 | version: int = 0 63 | tuning: float = 440.0 64 | 65 | 66 | @dataclass 67 | class TimingInfo: 68 | """ 69 | Timing information for a single subsong. 70 | """ 71 | 72 | arp_speed = 1 73 | clock_speed = 60.0 74 | highlight: Tuple[int, int] = (4, 16) 75 | speed: Tuple[int, int] = (0, 0) 76 | timebase = 1 77 | virtual_tempo: Tuple[int, int] = (150, 150) 78 | 79 | 80 | @dataclass 81 | class ChipList: 82 | """ 83 | Information about chips used in the module. 84 | """ 85 | 86 | list: List[ChipInfo] = field(default_factory=list) 87 | master_volume: float = 2.0 88 | 89 | 90 | @dataclass(repr=False) 91 | class ChannelDisplayInfo: 92 | """ 93 | Relating to channel display in Pattern and Order windows. 94 | """ 95 | 96 | name: str = "" 97 | abbreviation: str = "" 98 | collapsed: bool = False 99 | shown: bool = True 100 | 101 | def __repr__(self) -> str: 102 | return ( 103 | "ChannelDisplayInfo(name='%s', abbreviation='%s', collapsed=%s, shown=%s)" 104 | % (self.name, self.abbreviation, self.collapsed, self.shown) 105 | ) 106 | 107 | 108 | @dataclass 109 | class ModuleCompatFlags: 110 | """ 111 | Module compatibility flags, a.k.a. "The Motherload" 112 | 113 | Default values correspond with fileOps.cpp in the furnace src. 114 | """ 115 | 116 | # compat 1 117 | 118 | limit_slides: bool = False 119 | linear_pitch: LinearPitch = field(default_factory=lambda: LinearPitch.FULL_LINEAR) 120 | loop_modality: LoopModality = field(default_factory=lambda: LoopModality.DO_NOTHING) 121 | proper_noise_layout: bool = True 122 | wave_duty_is_volume: bool = False 123 | reset_macro_on_porta: bool = False 124 | legacy_volume_slides: bool = False 125 | compatible_arpeggio: bool = False 126 | note_off_resets_slides: bool = True 127 | target_resets_slides: bool = True 128 | arpeggio_inhibits_portamento: bool = False 129 | wack_algorithm_macro: bool = False 130 | broken_shortcut_slides: bool = False 131 | ignore_duplicates_slides: bool = False 132 | stop_portamento_on_note_off: bool = False 133 | continuous_vibrato: bool = False 134 | broken_dac_mode: bool = False 135 | one_tick_cut: bool = False 136 | instrument_change_allowed_in_porta: bool = True 137 | reset_note_base_on_arpeggio_stop: bool = True 138 | 139 | # compat 2 (>= dev70) 140 | 141 | broken_speed_selection: bool = False 142 | no_slides_on_first_tick: bool = False 143 | next_row_reset_arp_pos: bool = False 144 | ignore_jump_at_end: bool = False 145 | buggy_portamento_after_slide: bool = False 146 | gb_ins_affects_env: bool = True 147 | shared_extch_state: bool = True 148 | ignore_outside_dac_mode_change: bool = False 149 | e1e2_takes_priority: bool = False 150 | new_sega_pcm: bool = True 151 | weird_fnum_pitch_slides: bool = False 152 | sn_duty_resets_phase: bool = False 153 | linear_pitch_macro: bool = True 154 | pitch_slide_speed_in_linear: int = 4 155 | old_octave_boundary: bool = False 156 | disable_opn2_dac_volume_control: bool = False 157 | new_volume_scaling: bool = True 158 | volume_macro_lingers: bool = True 159 | broken_out_vol: bool = False 160 | e1e2_stop_on_same_note: bool = False 161 | broken_porta_after_arp: bool = False 162 | sn_no_low_periods: bool = False 163 | cut_delay_effect_policy: DelayBehavior = field( 164 | default_factory=lambda: DelayBehavior.LAX 165 | ) 166 | jump_treatment: JumpTreatment = field( 167 | default_factory=lambda: JumpTreatment.ALL_JUMPS 168 | ) 169 | auto_sys_name: bool = True 170 | disable_sample_macro: bool = False 171 | broken_out_vol_2: bool = False 172 | old_arp_strategy: bool = False 173 | 174 | # not-a-compat (>= dev135) 175 | 176 | auto_patchbay: bool = True 177 | 178 | # compat 3 (>= dev138) 179 | 180 | broken_porta_during_legato: bool = False 181 | 182 | broken_fm_off: bool = False 183 | pre_note_no_effect: bool = False 184 | old_dpcm: bool = False 185 | reset_arp_phase_on_new_note: bool = False 186 | ceil_volume_scaling: bool = False 187 | old_always_set_volume: bool = False 188 | old_sample_offset: bool = False 189 | 190 | 191 | @dataclass 192 | class SubSong: 193 | """ 194 | Information on a single subsong. 195 | """ 196 | 197 | name: str = "" 198 | comment: str = "" 199 | speed_pattern: List[int] = field(default_factory=lambda: [6]) 200 | """ 201 | Maximum 16 entries. 202 | """ 203 | grooves: List[List[int]] = field(default_factory=list) 204 | timing: TimingInfo = field(default_factory=TimingInfo) 205 | pattern_length = 64 206 | order: Dict[int, List[int]] = field( 207 | default_factory=lambda: { 208 | 0: [0], 209 | 1: [0], 210 | 2: [0], 211 | 3: [0], 212 | 4: [0], 213 | 5: [0], 214 | 6: [0], 215 | 7: [0], 216 | 8: [0], 217 | 9: [0], 218 | } 219 | ) 220 | effect_columns: List[int] = field( 221 | default_factory=lambda: [ 222 | 1 for _ in range(ChipType.YM2612.channels + ChipType.SMS.channels) 223 | ] 224 | ) 225 | channel_display: List[ChannelDisplayInfo] = field( 226 | default_factory=lambda: [ 227 | ChannelDisplayInfo() 228 | for _ in range(ChipType.YM2612.channels + ChipType.SMS.channels) 229 | ] 230 | ) 231 | 232 | 233 | @dataclass 234 | class FurnaceRow: 235 | """ 236 | Represents a single row in a pattern. 237 | """ 238 | 239 | note: Note 240 | octave: int 241 | instrument: int 242 | volume: int 243 | effects: List[Tuple[int, int]] = field(default_factory=list) 244 | 245 | def as_clipboard(self) -> str: 246 | """ 247 | Renders the selected row in Furnace clipboard format (without header!) 248 | 249 | :return: Furnace clipboard data (str) 250 | """ 251 | note_maps = { 252 | Note.Cs: "C#", 253 | Note.D_: "D-", 254 | Note.Ds: "D#", 255 | Note.E_: "E-", 256 | Note.F_: "F-", 257 | Note.Fs: "F#", 258 | Note.G_: "G-", 259 | Note.Gs: "G#", 260 | Note.A_: "A-", 261 | Note.As: "A#", 262 | Note.B_: "B-", 263 | Note.C_: "C-", 264 | } 265 | if self.note == Note.OFF: 266 | note_str = "OFF" 267 | elif self.note == Note.OFF_REL: 268 | note_str = "===" 269 | elif self.note == Note.REL: 270 | note_str = "REL" 271 | elif self.note == Note.__: 272 | note_str = "..." 273 | else: 274 | note_str = "%s%d" % (note_maps[self.note], self.octave) 275 | 276 | vol = ".." if self.volume == 0xFFFF else "%02X" % self.volume 277 | ins = ".." if self.instrument == 0xFFFF else "%02X" % self.instrument 278 | 279 | rep_str = "%s%s%s" 280 | 281 | for fx in self.effects: 282 | cmd, val = fx 283 | cmd_str = ".." if cmd == 0xFFFF else "%02X" % cmd 284 | val_str = ".." if val == 0xFFFF else "%02X" % val 285 | rep_str += "%s%s" % (cmd_str, val_str) 286 | 287 | return rep_str % (note_str, ins, vol) + "|" 288 | 289 | def __str__(self) -> str: 290 | if self.note == Note.OFF: 291 | note_str = "OFF" 292 | elif self.note == Note.OFF_REL: 293 | note_str = "===" 294 | elif self.note == Note.REL: 295 | note_str = "///" 296 | elif self.note == Note.__: 297 | note_str = "---" 298 | else: 299 | note_str = "%s%d" % (self.note, self.octave) 300 | 301 | vol = "--" if self.volume == 0xFFFF else "%02x" % self.volume 302 | ins = "--" if self.instrument == 0xFFFF else "%02x" % self.instrument 303 | 304 | rep_str = "row data: %s %s %s" 305 | 306 | for fx in self.effects: 307 | cmd, val = fx 308 | cmd_str = "--" if cmd == 0xFFFF else "%02x" % cmd 309 | val_str = "--" if val == 0xFFFF else "%02x" % val 310 | rep_str += " %s%s" % (cmd_str, val_str) 311 | 312 | return "<" + rep_str % (note_str, ins, vol) + ">" 313 | 314 | 315 | @dataclass 316 | class FurnacePattern: 317 | """ 318 | Represents one pattern in a module. 319 | """ 320 | 321 | channel: int = 0 322 | index: int = 0 323 | subsong: int = 0 324 | data: List[FurnaceRow] = field(default_factory=list) # yeah... 325 | name: str = "" 326 | 327 | def as_clipboard(self) -> str: 328 | """ 329 | Renders the selected pattern in Furnace clipboard format. 330 | 331 | :return: Furnace clipboard data 332 | """ 333 | return "org.tildearrow.furnace - Pattern Data\n0\n" + "\n".join( 334 | [x.as_clipboard() for x in self.data] 335 | ) 336 | 337 | def __str__(self) -> str: 338 | return "" % ( 339 | self.name if len(self.name) > 0 else "%02x" % self.index, 340 | self.channel, 341 | self.subsong, 342 | ) 343 | 344 | 345 | class InputPatchBayEntry(TypedDict): 346 | """ 347 | A patch that has an "input" connector. 348 | """ 349 | 350 | set: InputPortSet 351 | """ 352 | The set that the patch belongs to. 353 | """ 354 | port: int 355 | """ 356 | Which port to connect to. 357 | """ 358 | 359 | 360 | class OutputPatchBayEntry(TypedDict): 361 | """ 362 | A patch that has an "output" connector. 363 | """ 364 | 365 | set: OutputPortSet 366 | """ 367 | The set that the patch belongs to. 368 | """ 369 | port: int 370 | """ 371 | Which port to connect from. 372 | """ 373 | 374 | 375 | @dataclass 376 | class PatchBay: 377 | """ 378 | A single patchbay connection. 379 | """ 380 | 381 | source: OutputPatchBayEntry 382 | dest: InputPatchBayEntry 383 | 384 | 385 | # instruments 386 | @dataclass 387 | class InsFeatureAbstract: 388 | """ 389 | Base class for all InsFeature* classes. Not really to be used. 390 | """ 391 | 392 | _code: str = field(init=False) 393 | 394 | def __post_init__(self) -> None: 395 | if len(self._code) != 2: 396 | raise ValueError("No code defined for this instrument feature") 397 | 398 | # def serialize(self) -> bytes: 399 | # raise Exception('Method serialize() has not been overridden...') 400 | 401 | 402 | @dataclass 403 | class InsFeatureName(InsFeatureAbstract, str): 404 | """ 405 | Instrument's name block. Can be used as a string. 406 | """ 407 | 408 | _code = "NA" 409 | name: str = "" 410 | 411 | def __str__(self) -> str: 412 | return self.name 413 | 414 | 415 | @dataclass 416 | class InsMeta: 417 | version: int = 143 418 | type: InstrumentType = InstrumentType.FM_4OP 419 | 420 | 421 | @dataclass 422 | class InsFMOperator: 423 | am: bool = False 424 | ar: int = 0 425 | dr: int = 0 426 | mult: int = 0 427 | rr: int = 0 428 | sl: int = 0 429 | tl: int = 0 430 | dt2: int = 0 431 | rs: int = 0 432 | dt: int = 0 433 | d2r: int = 0 434 | ssg_env: int = 0 435 | dam: int = 0 436 | dvb: int = 0 437 | egt: bool = False 438 | ksl: int = 0 439 | sus: bool = False 440 | vib: bool = False 441 | ws: int = 0 442 | ksr: bool = False 443 | enable: bool = True 444 | kvs: int = 2 445 | 446 | 447 | @dataclass 448 | class InsFeatureFM(InsFeatureAbstract): 449 | _code = "FM" 450 | alg: int = 0 451 | fb: int = 4 452 | fms: int = 0 453 | ams: int = 0 454 | fms2: int = 0 455 | ams2: int = 0 456 | ops: int = 2 457 | opll_preset: int = 0 458 | op_list: List[InsFMOperator] = field( 459 | default_factory=lambda: [ 460 | InsFMOperator(tl=42, ar=31, dr=8, sl=15, rr=3, mult=5, dt=5), 461 | InsFMOperator(tl=48, ar=31, dr=4, sl=11, rr=1, mult=1, dt=5), 462 | InsFMOperator(tl=18, ar=31, dr=10, sl=15, rr=4, mult=1, dt=0), 463 | InsFMOperator(tl=2, ar=31, dr=9, sl=15, rr=9, mult=1, dt=0), 464 | ] 465 | ) 466 | 467 | 468 | @dataclass 469 | class SingleMacro: 470 | kind: Union[MacroCode, OpMacroCode] = field(default_factory=lambda: MacroCode.VOL) 471 | mode: int = 0 472 | type: MacroType = field(default_factory=lambda: MacroType.SEQUENCE) 473 | delay: int = 0 474 | speed: int = 1 475 | open: bool = False 476 | data: List[Union[int, MacroItem]] = field(default_factory=list) 477 | 478 | 479 | @dataclass 480 | class InsFeatureMacro(InsFeatureAbstract): 481 | _code = "MA" 482 | macros: List[SingleMacro] = field(default_factory=lambda: [SingleMacro()]) 483 | 484 | 485 | @dataclass 486 | class InsFeatureOpr1Macro(InsFeatureMacro): 487 | _code = "O1" 488 | 489 | 490 | @dataclass 491 | class InsFeatureOpr2Macro(InsFeatureMacro): 492 | _code = "O2" 493 | 494 | 495 | @dataclass 496 | class InsFeatureOpr3Macro(InsFeatureMacro): 497 | _code = "O3" 498 | 499 | 500 | @dataclass 501 | class InsFeatureOpr4Macro(InsFeatureMacro): 502 | _code = "O4" 503 | 504 | 505 | @dataclass 506 | class GBHwSeq: 507 | command: GBHwCommand 508 | data: List[int] = field(default_factory=lambda: [0, 0]) 509 | 510 | 511 | @dataclass 512 | class InsFeatureGB(InsFeatureAbstract): 513 | _code = "GB" 514 | env_vol: int = 15 515 | env_dir: int = 0 516 | env_len: int = 2 517 | sound_len: int = 0 518 | soft_env: bool = False 519 | always_init: bool = False 520 | hw_seq: List[GBHwSeq] = field(default_factory=list) 521 | 522 | 523 | @dataclass 524 | class GenericADSR: 525 | a: int = 0 526 | d: int = 0 527 | s: int = 0 528 | r: int = 0 529 | 530 | 531 | @dataclass 532 | class InsFeatureC64(InsFeatureAbstract): 533 | _code = "64" 534 | tri_on: bool = False 535 | saw_on: bool = True 536 | pulse_on: bool = False 537 | noise_on: bool = False 538 | envelope: GenericADSR = field( 539 | default_factory=lambda: GenericADSR(a=0, d=8, s=0, r=0) 540 | ) 541 | duty: int = 2048 542 | ring_mod: int = 0 543 | osc_sync: int = 0 544 | to_filter: bool = False 545 | vol_is_cutoff: bool = False 546 | init_filter: bool = False 547 | duty_is_abs: bool = False 548 | filter_is_abs: bool = False 549 | no_test: bool = False 550 | res: int = 0 551 | cut: int = 0 552 | hp: bool = False 553 | lp: bool = False 554 | bp: bool = False 555 | ch3_off: bool = False 556 | 557 | 558 | @dataclass 559 | class SampleMap: 560 | freq: int = 0 561 | sample_index: int = 0 562 | 563 | 564 | @dataclass 565 | class DPCMMap: 566 | pitch: int = 0 567 | delta: int = 0 568 | 569 | 570 | @dataclass 571 | class InsFeatureAmiga(InsFeatureAbstract): # Sample data 572 | _code = "SM" 573 | init_sample: int = 0 574 | use_note_map: bool = False 575 | use_sample: bool = False 576 | use_wave: bool = False 577 | wave_len: int = 31 578 | sample_map: List[SampleMap] = field( 579 | default_factory=lambda: [SampleMap() for _ in range(120)] 580 | ) 581 | 582 | 583 | @dataclass 584 | class InsFeatureDPCMMap(InsFeatureAbstract): # DPCM sample data 585 | _code = "NE" 586 | use_map: bool = False 587 | sample_map: List[DPCMMap] = field( 588 | default_factory=lambda: [DPCMMap() for _ in range(120)] 589 | ) 590 | 591 | 592 | @dataclass 593 | class InsFeatureX1010(InsFeatureAbstract): 594 | _code = "X1" 595 | bank_slot: int = 0 596 | 597 | 598 | @dataclass 599 | class InsFeaturePowerNoise(InsFeatureAbstract): 600 | _code = "PN" 601 | octave: int = 0 602 | 603 | 604 | @dataclass 605 | class InsFeatureSID2(InsFeatureAbstract): 606 | _code = "S2" 607 | noise_mode: int = 0 608 | wave_mix: int = 0 609 | volume: int = 0 610 | 611 | 612 | @dataclass 613 | class InsFeatureN163(InsFeatureAbstract): 614 | _code = "N1" 615 | wave: int = -1 616 | wave_pos: int = 0 617 | wave_len: int = 32 618 | wave_mode: int = 3 619 | 620 | 621 | @dataclass 622 | class InsFeatureFDS(InsFeatureAbstract): # Virtual Boy 623 | _code = "FD" 624 | mod_speed: int = 0 625 | mod_depth: int = 0 626 | init_table_with_first_wave: bool = False # compat 627 | mod_table: List[int] = field(default_factory=lambda: [0 for i in range(32)]) 628 | 629 | 630 | @dataclass 631 | class InsFeatureMultiPCM(InsFeatureAbstract): 632 | _code = "MP" 633 | ar: int = 15 634 | d1r: int = 15 635 | dl: int = 0 636 | d2r: int = 0 637 | rr: int = 15 638 | rc: int = 15 639 | lfo: int = 0 640 | vib: int = 0 641 | am: int = 0 642 | 643 | 644 | @dataclass 645 | class InsFeatureWaveSynth(InsFeatureAbstract): 646 | _code = "WS" 647 | wave_indices: List[int] = field(default_factory=lambda: [0, 0]) 648 | rate_divider: int = 1 649 | effect: WaveFX = WaveFX.NONE 650 | enabled: bool = False 651 | global_effect: bool = False 652 | speed: int = 0 653 | params: List[int] = field(default_factory=lambda: [0, 0, 0, 0]) 654 | one_shot: bool = False # not read? 655 | 656 | 657 | @dataclass 658 | class InsFeatureSoundUnit(InsFeatureAbstract): 659 | _code = "SU" 660 | switch_roles: bool = False 661 | 662 | 663 | @dataclass 664 | class InsFeatureES5506(InsFeatureAbstract): 665 | _code = "ES" 666 | filter_mode: ESFilterMode = ESFilterMode.LPK2_LPK1 667 | k1: int = 0xFFFF 668 | k2: int = 0xFFFF 669 | env_count: int = 0 670 | left_volume_ramp: int = 0 671 | right_volume_ramp: int = 0 672 | k1_ramp: int = 0 673 | k2_ramp: int = 0 674 | k1_slow: int = 0 675 | k2_slow: int = 0 676 | 677 | 678 | @dataclass 679 | class InsFeatureSNES(InsFeatureAbstract): 680 | _code = "SN" 681 | use_env: bool = True 682 | sus: SNESSusMode = SNESSusMode.DIRECT 683 | gain_mode: GainMode = GainMode.DIRECT 684 | gain: int = 127 685 | d2: int = 0 686 | envelope: GenericADSR = field( 687 | default_factory=lambda: GenericADSR(a=15, d=7, s=7, r=0) 688 | ) 689 | 690 | 691 | @dataclass 692 | class InsFeatureOPLDrums(InsFeatureAbstract): 693 | _code = "LD" 694 | fixed_drums: bool = False 695 | kick_freq: int = 1312 696 | snare_hat_freq: int = 1360 697 | tom_top_freq: int = 448 698 | 699 | 700 | @dataclass 701 | class _InsFeaturePointerAbstract(InsFeatureAbstract): 702 | """ 703 | Also not really to be used. Container for all "list" features. 704 | """ 705 | 706 | _code = "LL" 707 | pointers: Dict[int, int] = field(default_factory=dict) 708 | 709 | 710 | @dataclass 711 | class InsFeatureSampleList(_InsFeaturePointerAbstract): 712 | """ 713 | List of pointers to all samples used by this instrument. 714 | """ 715 | 716 | _code = "SL" 717 | 718 | 719 | @dataclass 720 | class InsFeatureWaveList(_InsFeaturePointerAbstract): 721 | """ 722 | List of pointers to all wave tables used by this instrument. 723 | """ 724 | 725 | _code = "WL" 726 | 727 | 728 | @dataclass 729 | class WavetableMeta: 730 | name: str = "" 731 | width: int = 32 732 | height: int = 32 733 | 734 | 735 | @dataclass 736 | class SampleMeta: 737 | name: str = "" 738 | length: int = 0 739 | bitdepth: int = 0 740 | loop_start: int = 0 741 | loop_end: int = 0 742 | -------------------------------------------------------------------------------- /chipchune/furnace/enums.py: -------------------------------------------------------------------------------- 1 | from chipchune._util import EnumShowNameOnly, EnumValueEquals 2 | from typing import Tuple 3 | 4 | 5 | class LinearPitch(EnumShowNameOnly, EnumValueEquals): 6 | """ 7 | Options for :attr:`chipchune.furnace.data_types.ModuleCompatFlags.linear_pitch`. 8 | """ 9 | 10 | NON_LINEAR = 0 11 | ONLY_PITCH_CHANGE = 1 12 | FULL_LINEAR = 2 13 | 14 | 15 | class LoopModality(EnumShowNameOnly, EnumValueEquals): 16 | """ 17 | Options for :attr:`chipchune.furnace.data_types.ModuleCompatFlags.loop_modality`. 18 | """ 19 | 20 | HARD_RESET_CHANNELS = 0 21 | SOFT_RESET_CHANNELS = 1 22 | DO_NOTHING = 2 23 | 24 | 25 | class DelayBehavior(EnumShowNameOnly, EnumValueEquals): 26 | """ 27 | Options for :attr:`chipchune.furnace.data_types.ModuleCompatFlags.cut_delay_effect_policy`. 28 | """ 29 | 30 | STRICT = 0 31 | BROKEN = 1 32 | LAX = 2 33 | 34 | 35 | class JumpTreatment(EnumShowNameOnly, EnumValueEquals): 36 | """ 37 | Options for :attr:`chipchune.furnace.data_types.ModuleCompatFlags.jump_treatment`. 38 | """ 39 | 40 | ALL_JUMPS = 0 41 | FIRST_JUMP_ONLY = 1 42 | ROW_JUMP_PRIORITY = 2 43 | 44 | 45 | class Note(EnumShowNameOnly): 46 | """ 47 | All notes recognized by Furnace 48 | """ 49 | 50 | __ = 0 51 | Cs = 1 52 | D_ = 2 53 | Ds = 3 54 | E_ = 4 55 | F_ = 5 56 | Fs = 6 57 | G_ = 7 58 | Gs = 8 59 | A_ = 9 60 | As = 10 61 | B_ = 11 62 | C_ = 12 63 | OFF = 100 64 | OFF_REL = 101 65 | REL = 102 66 | 67 | 68 | class MacroItem(EnumShowNameOnly): 69 | """ 70 | Special values used only in this parser, to allow data editing similar to that 71 | of Furnace itself. 72 | """ 73 | 74 | LOOP = 0 75 | RELEASE = 1 76 | 77 | 78 | class MacroCode(EnumShowNameOnly, EnumValueEquals): 79 | """ 80 | Marks what aspect of an instrument does a macro change. 81 | """ 82 | 83 | VOL = 0 84 | """ 85 | Also: 86 | - C64 cutoff 87 | """ 88 | 89 | ARP = 1 90 | """ 91 | Not applicable to MSM6258 and MSM6295. 92 | """ 93 | 94 | DUTY = 2 95 | """ 96 | Also: 97 | - AY noise freq 98 | - POKEY audctl 99 | - Mikey duty/int 100 | - MSM5232 group ctrl 101 | - Beeper/Pokemon Mini pulse width 102 | - T6W28 noise type 103 | - Virtual Boy noise length 104 | - PC Engine/Namco/WonderSwan noise type 105 | - SNES noise freq 106 | - Namco 163 waveform pos. 107 | - ES5506 filter mode 108 | - MSM6258/MSM6295 freq. divider 109 | - ADPCMA global volume 110 | - QSound echo level 111 | """ 112 | 113 | WAVE = 3 114 | """ 115 | Also: 116 | - OPLL patch 117 | - OPZ/OPM lfo1 shape 118 | """ 119 | 120 | PITCH = 4 121 | 122 | EX1 = 5 123 | """ 124 | - OPZ/OPM am depth 125 | - C64 filter mode 126 | - SAA1099 envelope 127 | - X1-010 env. mode 128 | - Namco 163 wave length 129 | - FDS mod depth 130 | - TSU cutoff 131 | - ES5506 filter k1 132 | - MSM6258 clk divider 133 | - QSound echo feedback 134 | - SNES special 135 | - MSM5232 group attack 136 | - AY8930 duty? 137 | """ 138 | 139 | EX2 = 6 140 | """ 141 | - C64 resonance 142 | - Namco 163 wave update 143 | - FDS mod speed 144 | - TSU resonance 145 | - ES5506 filter k2 146 | - QSound echo length 147 | - SNES gain 148 | - MSM5232 group decay 149 | - AY3/AY8930 envelope 150 | """ 151 | 152 | EX3 = 7 153 | """ 154 | - C64 special 155 | - AY/AY8930 autoenv num 156 | - X1-010 autoenv num 157 | - Namco 163 waveload wave 158 | - FDS mod position 159 | - TSU control 160 | - MSM5232 noise 161 | """ 162 | 163 | ALG = 8 164 | """ 165 | Also: 166 | - AY/AY8930 autoenv den 167 | - X1-010 autoenv den 168 | - Namco 163 waveload pos 169 | - ES5506 control 170 | """ 171 | 172 | FB = 9 173 | """ 174 | Also: 175 | - AY8930 noise & mask 176 | - Namco 163 waveload len 177 | - ES5506 outputs 178 | """ 179 | 180 | FMS = 10 181 | """ 182 | Also: 183 | - AY8930 noise | mask 184 | - Namco 163 waveload trigger 185 | """ 186 | 187 | AMS = 11 188 | 189 | PAN_L = 12 190 | 191 | PAN_R = 13 192 | 193 | PHASE_RESET = 14 194 | 195 | EX4 = 15 196 | """ 197 | - C64 test/gate 198 | - TSU phase reset timer 199 | - FM/OPM opmask 200 | """ 201 | 202 | EX5 = 16 203 | """ 204 | - OPZ am depth 2 205 | """ 206 | 207 | EX6 = 17 208 | """ 209 | - OPZ pm depth 2 210 | """ 211 | 212 | EX7 = 18 213 | """ 214 | - OPZ lfo2 speed 215 | """ 216 | 217 | EX8 = 19 218 | """ 219 | - OPZ lfo2 shape 220 | """ 221 | 222 | STOP = 255 223 | """ 224 | Marks end of macro reading. 225 | """ 226 | 227 | 228 | class OpMacroCode(EnumShowNameOnly, EnumValueEquals): 229 | """ 230 | Controls which FM parameter a macro should change. 231 | """ 232 | 233 | AM = 0 234 | AR = 1 235 | DR = 2 236 | MULT = 3 237 | RR = 4 238 | SL = 5 239 | TL = 6 240 | DT2 = 7 241 | RS = 8 242 | DT = 9 243 | D2R = 10 244 | SSG_EG = 11 245 | DAM = 12 246 | DVB = 13 247 | EGT = 14 248 | KSL = 15 249 | SUS = 16 250 | VIB = 17 251 | WS = 18 252 | KSR = 19 253 | 254 | 255 | class MacroType(EnumShowNameOnly): 256 | """ 257 | Instrument macro type (version 120+). 258 | """ 259 | 260 | SEQUENCE = 0 261 | ADSR = 1 262 | LFO = 2 263 | 264 | 265 | class MacroSize(EnumShowNameOnly): 266 | """ 267 | Type of value stored in the instrument file. 268 | """ 269 | 270 | _value_: int 271 | num_bytes: int 272 | signed: bool 273 | 274 | UINT8: Tuple[int, int, bool] = (0, 1, False) 275 | INT8: Tuple[int, int, bool] = (1, 1, True) 276 | INT16: Tuple[int, int, bool] = (2, 2, True) 277 | INT32: Tuple[int, int, bool] = (3, 4, True) 278 | 279 | def __new__(cls, id: int, num_bytes: int, signed: bool): # type: ignore[no-untyped-def] 280 | member = object.__new__(cls) 281 | member._value_ = id 282 | setattr(member, "num_bytes", num_bytes) 283 | setattr(member, "signed", signed) 284 | return member 285 | 286 | 287 | class GBHwCommand(EnumShowNameOnly): 288 | """ 289 | Game Boy hardware envelope commands. 290 | """ 291 | 292 | ENVELOPE = 0 293 | SWEEP = 1 294 | WAIT = 2 295 | WAIT_REL = 3 296 | LOOP = 4 297 | LOOP_REL = 5 298 | 299 | 300 | class SampleType(EnumShowNameOnly): 301 | """ 302 | Sample types used in Furnace 303 | """ 304 | 305 | ZX_DRUM = 0 306 | NES_DPCM = 1 307 | QSOUND_ADPCM = 4 308 | ADPCM_A = 5 309 | ADPCM_B = 6 310 | X68K_ADPCM = 7 311 | PCM_8 = 8 312 | SNES_BRR = 9 313 | VOX = 10 314 | PCM_16 = 16 315 | 316 | 317 | class InstrumentType(EnumShowNameOnly): 318 | """ 319 | Instrument types currently available as of version 144. 320 | """ 321 | 322 | STANDARD = 0 323 | FM_4OP = 1 324 | GB = 2 325 | C64 = 3 326 | AMIGA = 4 327 | PCE = 5 328 | SSG = 6 329 | AY8930 = 7 330 | TIA = 8 331 | SAA1099 = 9 332 | VIC = 10 333 | PET = 11 334 | VRC6 = 12 335 | FM_OPLL = 13 336 | FM_OPL = 14 337 | FDS = 15 338 | VB = 16 339 | N163 = 17 340 | KONAMI_SCC = 18 341 | FM_OPZ = 19 342 | POKEY = 20 343 | PC_BEEPER = 21 344 | WONDERSWAN = 22 345 | LYNX = 23 346 | VERA = 24 347 | X1010 = 25 348 | VRC6_SAW = 26 349 | ES5506 = 27 350 | MULTIPCM = 28 351 | SNES = 29 352 | TSU = 30 353 | NAMCO_WSG = 31 354 | OPL_DRUMS = 32 355 | FM_OPM = 33 356 | NES = 34 357 | MSM6258 = 35 358 | MSM6295 = 36 359 | ADPCM_A = 37 360 | ADPCM_B = 38 361 | SEGAPCM = 39 362 | QSOUND = 40 363 | YMZ280B = 41 364 | RF5C68 = 42 365 | MSM5232 = 43 366 | T6W28 = 44 367 | K007232 = 45 368 | GA20 = 46 369 | POKEMON_MINI = 47 370 | SM8521 = 48 371 | PV1000 = 49 372 | 373 | 374 | class ChipType(EnumShowNameOnly): 375 | """ 376 | Furnace chip database, either planned or implemented. 377 | Contains console name, chip ID and number of channels. 378 | """ 379 | 380 | _value_: int 381 | channels: int 382 | 383 | YMU759 = (0x01, 17) 384 | GENESIS = (0x02, 10) # YM2612 + SN76489 385 | SMS = (0x03, 4) # SN76489 386 | GB = (0x04, 4) # LR53902 387 | PCE = (0x05, 6) # HuC6280 388 | NES = (0x06, 5) # RP2A03 389 | C64_8580 = (0x07, 3) # SID r8580 390 | SEGA_ARCADE = (0x08, 13) # YM2151 + SegaPCM 391 | NEO_GEO_CD = (0x09, 13) 392 | 393 | GENESIS_EX = (0x42, 13) # YM2612 + SN76489 394 | SMS_JP = (0x43, 13) # SN76489 + YM2413 395 | NES_VRC7 = (0x46, 11) # RP2A03 + YM2413 396 | C64_6581 = (0x47, 3) # SID r6581 397 | NEO_GEO_CD_EX = (0x49, 16) 398 | 399 | AY38910 = (0x80, 3) 400 | AMIGA = (0x81, 4) # Paula 401 | YM2151 = (0x82, 8) # YM2151 402 | YM2612 = (0x83, 6) # YM2612 403 | TIA = (0x84, 2) 404 | VIC20 = (0x85, 4) 405 | PET = (0x86, 1) 406 | SNES = (0x87, 8) # SPC700 407 | VRC6 = (0x88, 3) 408 | OPLL = (0x89, 9) # YM2413 409 | FDS = (0x8A, 1) 410 | MMC5 = (0x8B, 3) 411 | N163 = (0x8C, 8) 412 | OPN = (0x8D, 6) # YM2203 413 | PC98 = (0x8E, 16) # YM2608 414 | OPL = (0x8F, 9) # YM3526 415 | 416 | OPL2 = (0x90, 9) # YM3812 417 | OPL3 = (0x91, 18) # YMF262 418 | MULTIPCM = (0x92, 24) 419 | PC_SPEAKER = (0x93, 1) # Intel 8253 420 | POKEY = (0x94, 4) 421 | RF5C68 = (0x95, 8) 422 | WONDERSWAN = (0x96, 4) 423 | SAA1099 = (0x97, 6) 424 | OPZ = (0x98, 8) 425 | POKEMON_MINI = (0x99, 1) 426 | AY8930 = (0x9A, 3) 427 | SEGAPCM = (0x9B, 16) 428 | VIRTUAL_BOY = (0x9C, 6) 429 | VRC7 = (0x9D, 6) 430 | YM2610B = (0x9E, 16) 431 | ZX_BEEPER = (0x9F, 6) # tildearrow's engine 432 | 433 | YM2612_EX = (0xA0, 9) 434 | SCC = (0xA1, 5) 435 | OPL_DRUMS = (0xA2, 11) 436 | OPL2_DRUMS = (0xA3, 11) 437 | OPL3_DRUMS = (0xA4, 20) 438 | NEO_GEO = (0xA5, 14) 439 | NEO_GEO_EX = (0xA6, 17) 440 | OPLL_DRUMS = (0xA7, 11) 441 | LYNX = (0xA8, 4) 442 | SEGAPCM_DMF = (0xA9, 5) 443 | MSM6295 = (0xAA, 4) 444 | MSM6258 = (0xAB, 1) 445 | COMMANDER_X16 = (0xAC, 17) # VERA 446 | BUBBLE_SYSTEM_WSG = (0xAD, 2) 447 | OPL4 = (0xAE, 42) 448 | OPL4_DRUMS = (0xAF, 44) 449 | 450 | SETA = (0xB0, 16) # Allumer X1-010 451 | ES5506 = (0xB1, 32) 452 | Y8950 = (0xB2, 10) 453 | Y8950_DRUMS = (0xB3, 12) 454 | SCC_PLUS = (0xB4, 5) 455 | TSU = (0xB5, 8) 456 | YM2203_EX = (0xB6, 9) 457 | YM2608_EX = (0xB7, 19) 458 | YMZ280B = (0xB8, 8) 459 | NAMCO = (0xB9, 3) # Namco WSG 460 | N15XX = (0xBA, 8) # Namco 15xx 461 | CUS30 = (0xBB, 8) # Namco CUS30 462 | MSM5232 = (0xBC, 8) 463 | YM2612_PLUS_EX = (0xBD, 11) 464 | YM2612_PLUS = (0xBE, 7) 465 | T6W28 = (0xBF, 4) 466 | 467 | PCM_DAC = (0xC0, 1) 468 | YM2612_CSM = (0xC1, 10) 469 | NEO_GEO_CSM = (0xC2, 18) # YM2610 CSM 470 | YM2203_CSM = (0xC3, 10) 471 | YM2608_CSM = (0xC4, 20) 472 | YM2610B_CSM = (0xC5, 20) 473 | K007232 = (0xC6, 2) 474 | GA20 = (0xC7, 4) 475 | SM8521 = (0xC8, 3) 476 | M114S = (0xC9, 16) 477 | ZX_BEEPER_QUADTONE = (0xCA, 5) # Natt Akuma's engine 478 | PV_1000 = (0xCB, 3) # NEC D65010G031 479 | K053260 = (0xCC, 4) 480 | TED = (0xCD, 2) 481 | NAMCO_C140 = (0xCE, 24) 482 | NAMCO_C219 = (0xCF, 16) 483 | 484 | NAMCO_C352 = (0xD0, 32) 485 | ESFM = (0xD1, 18) 486 | ES5503 = (0xD2, 32) 487 | POWERNOISE = (0xD4, 4) 488 | DAVE = (0xD5, 6) 489 | NDS = (0xD6, 16) 490 | GBA = (0xD7, 2) 491 | GBA_MINMOD = (0xD8, 16) 492 | BIFURCATOR = (0xD9, 4) 493 | YM2610B_EX = (0xDE, 19) 494 | 495 | QSOUND = (0xE0, 19) 496 | 497 | SID2 = (0xF0, 3) # SID2 498 | FIVEE01 = (0xF1, 5) # 5E01 499 | PONG = (0xFC, 1) 500 | DUMMY = (0xFD, 1) 501 | RESERVED_1 = (0xFE, 1) 502 | RESERVED_2 = (0xFF, 1) 503 | 504 | def __new__(cls, id: int, channels: int): # type: ignore[no-untyped-def] 505 | member = object.__new__(cls) 506 | member._value_ = id 507 | setattr(member, "channels", channels) 508 | return member 509 | 510 | def __repr__(self) -> str: 511 | # repr abuse 512 | # about as stupid as "mapping for the renderer"... 513 | return "%s (0x%02x), %d channel%s" % ( 514 | self.name, 515 | self._value_, 516 | self.channels, 517 | "s" if self.channels != 1 else "", 518 | ) 519 | 520 | 521 | class InputPortSet(EnumShowNameOnly): 522 | """ 523 | Devices which contain an "input" port. 524 | """ 525 | 526 | SYSTEM = 0 527 | NULL = 0xFFF 528 | 529 | 530 | class OutputPortSet(EnumShowNameOnly): 531 | """ 532 | Devices which contain an "output" port. 533 | """ 534 | 535 | CHIP_1 = 0 536 | CHIP_2 = 1 537 | CHIP_3 = 2 538 | CHIP_4 = 3 539 | CHIP_5 = 4 540 | CHIP_6 = 5 541 | CHIP_7 = 6 542 | CHIP_8 = 7 543 | CHIP_9 = 8 544 | CHIP_10 = 9 545 | CHIP_11 = 10 546 | CHIP_12 = 11 547 | CHIP_13 = 12 548 | CHIP_14 = 13 549 | CHIP_15 = 14 550 | CHIP_16 = 15 551 | CHIP_17 = 16 552 | CHIP_18 = 17 553 | CHIP_19 = 18 554 | CHIP_20 = 19 555 | CHIP_21 = 20 556 | CHIP_22 = 21 557 | CHIP_23 = 22 558 | CHIP_24 = 23 559 | CHIP_25 = 24 560 | CHIP_26 = 25 561 | CHIP_27 = 26 562 | CHIP_28 = 27 563 | CHIP_29 = 28 564 | CHIP_30 = 29 565 | CHIP_31 = 30 566 | CHIP_32 = 31 567 | PREVIEW = 0xFFD 568 | METRONOME = 0xFFE 569 | NULL = 0xFFF 570 | 571 | 572 | class WaveFX(EnumShowNameOnly): 573 | """ 574 | Used in :attr:`chipchune.furnace.data_types.InsFeatureWaveSynth.effect`. 575 | """ 576 | 577 | NONE = 0 578 | 579 | # single waveform 580 | INVERT = 1 581 | ADD = 2 582 | SUBTRACT = 3 583 | AVERAGE = 4 584 | PHASE = 5 585 | CHORUS = 6 586 | 587 | # double waveform 588 | NONE_DUAL = 128 589 | WIPE = 129 590 | FADE = 130 591 | PING_PONG = 131 592 | OVERLAY = 132 593 | NEGATIVE_OVERLAY = 133 594 | SLIDE = 134 595 | MIX = 135 596 | PHASE_MOD = 136 597 | 598 | 599 | class ESFilterMode(EnumShowNameOnly): 600 | """ 601 | Used in :attr:`chipchune.furnace.data_types.InsFeatureES5506.filter_mode`. 602 | """ 603 | 604 | HPK2_HPK2 = 0 605 | HPK2_LPK1 = 1 606 | LPK2_LPK2 = 2 607 | LPK2_LPK1 = 3 608 | 609 | 610 | class GainMode(EnumShowNameOnly): 611 | """ 612 | Used in :attr:`chipchune.furnace.data_types.InsFeatureSNES.gain_mode`. 613 | """ 614 | 615 | DIRECT = 0 616 | DEC_LINEAR = 4 617 | DEC_LOG = 5 618 | INC_LINEAR = 6 619 | INC_INVLOG = 7 620 | 621 | 622 | class SNESSusMode(EnumShowNameOnly): 623 | """ 624 | Used in :attr:`chipchune.furnace.data_types.InsFeatureSNES.sus`. 625 | """ 626 | 627 | DIRECT = 0 628 | SUS_WITH_DEC = 1 629 | SUS_WITH_EXP = 2 630 | SUS_WITH_REL = 3 631 | 632 | 633 | class _FurInsImportType(EnumShowNameOnly, EnumValueEquals): 634 | """ 635 | Also only used in this parser to differentiate between different types of instrument formats. 636 | """ 637 | 638 | # Old format 639 | FORMAT_0_FILE = 0 640 | FORMAT_0_EMBED = 1 641 | 642 | # Dev127 format 643 | FORMAT_1_FILE = 2 644 | FORMAT_1_EMBED = 3 645 | 646 | 647 | class _FurWavetableImportType(EnumShowNameOnly, EnumValueEquals): 648 | """ 649 | Also only used in this parser to differentiate between different types of wavetable formats. 650 | """ 651 | 652 | FILE = 0 653 | EMBED = 1 654 | 655 | 656 | class _FurSampleType(EnumShowNameOnly, EnumValueEquals): 657 | """ 658 | Also only used in this parser to differentiate between different types of sample formats. 659 | """ 660 | 661 | PCM_1_BIT = 0 662 | DPCM = 1 663 | YMZ = 3 664 | QSOUND = 4 665 | ADPCM_A = 5 666 | ADPCM_B = 6 667 | K05_ADPCM = 7 668 | PCM_8_BIT = 8 669 | BRR = 9 670 | VOX = 10 671 | ULAW = 11 672 | C219 = 12 673 | IMA = 13 674 | PCM_16_BIT = 16 675 | -------------------------------------------------------------------------------- /chipchune/furnace/instrument.py: -------------------------------------------------------------------------------- 1 | from io import BytesIO 2 | from typing import Optional, Union, BinaryIO, TypeVar, Type, List, Dict, cast 3 | 4 | from chipchune._util import read_byte, read_short, read_int, read_str 5 | from .data_types import ( 6 | InsFeatureAbstract, 7 | InsFeatureMacro, 8 | InsMeta, 9 | InstrumentType, 10 | InsFeatureName, 11 | InsFeatureFM, 12 | InsFeatureOpr1Macro, 13 | InsFeatureOpr2Macro, 14 | InsFeatureOpr3Macro, 15 | InsFeatureOpr4Macro, 16 | InsFeatureC64, 17 | InsFeatureGB, 18 | GBHwSeq, 19 | SingleMacro, 20 | InsFeatureAmiga, 21 | InsFeatureOPLDrums, 22 | InsFeatureSNES, 23 | GainMode, 24 | InsFeatureN163, 25 | InsFeatureFDS, 26 | InsFeatureWaveSynth, 27 | _InsFeaturePointerAbstract, 28 | InsFeatureSampleList, 29 | InsFeatureWaveList, 30 | InsFeatureMultiPCM, 31 | InsFeatureSoundUnit, 32 | InsFeatureES5506, 33 | InsFeatureX1010, 34 | GenericADSR, 35 | InsFeatureDPCMMap, 36 | InsFeaturePowerNoise, 37 | InsFeatureSID2, 38 | ) 39 | from .enums import ( 40 | _FurInsImportType, 41 | MacroCode, 42 | OpMacroCode, 43 | MacroItem, 44 | MacroType, 45 | GBHwCommand, 46 | SNESSusMode, 47 | WaveFX, 48 | ESFilterMode, 49 | MacroSize, 50 | ) 51 | 52 | FILE_MAGIC_STR = b"-Furnace instr.-" 53 | DEV127_FILE_MAGIC_STR = b"FINS" 54 | 55 | EMBED_MAGIC_STR = b"INST" 56 | DEV127_EMBED_MAGIC_STR = b"INS2" 57 | 58 | T_MACRO = TypeVar( 59 | "T_MACRO", bound=InsFeatureMacro 60 | ) # T_MACRO must be subclass of InsFeatureMacro 61 | T_POINTERS = TypeVar("T_POINTERS", bound=_InsFeaturePointerAbstract) 62 | 63 | 64 | class FurnaceInstrument: 65 | def __init__( 66 | self, file_name: Optional[str] = None, protocol_version: Optional[int] = 1 67 | ) -> None: 68 | """ 69 | Creates or opens a new Furnace instrument as a Python object. 70 | 71 | :param file_name: (Optional) 72 | If specified, then it will parse a file as a FurnaceInstrument. If file name (str) is 73 | given, it will load that file. 74 | 75 | Defaults to None. 76 | 77 | :param protocol_version: (Optional) 78 | If specified, it will determine which format the instrument is to be serialized (exported) 79 | to. It is ignored if loading up a file. 80 | 81 | Defaults to 2 (dev127+ ins. format) 82 | """ 83 | self.file_name: Optional[str] = None 84 | """ 85 | Original file name, if the object was initialized with one. 86 | """ 87 | self.protocol_version: Optional[int] = protocol_version 88 | """ 89 | Instrument file "protocol" version. Currently: 90 | - 0: The "unified" instrument format up to Furnace version 126. 91 | - 1: The new "featural" instrument format introduced in version 127. 92 | """ 93 | self.features: List[InsFeatureAbstract] = [] 94 | """ 95 | List of features, regardless of protocol version. 96 | """ 97 | self.meta: InsMeta = InsMeta() 98 | """ 99 | Instrument metadata. 100 | """ 101 | 102 | # self.wavetables: list[] = [] 103 | # self.samples: list[] = [] 104 | 105 | self.__map_to_fn = { 106 | b"NA": self.__load_na_block, 107 | b"FM": self.__load_fm_block, 108 | b"MA": self.__load_ma_block, 109 | b"64": self.__load_c64_block, 110 | b"GB": self.__load_gb_block, 111 | b"SM": self.__load_sm_block, 112 | b"O1": self.__load_o1_block, 113 | b"O2": self.__load_o2_block, 114 | b"O3": self.__load_o3_block, 115 | b"O4": self.__load_o4_block, 116 | b"LD": self.__load_ld_block, 117 | b"SN": self.__load_sn_block, 118 | b"N1": self.__load_n1_block, 119 | b"FD": self.__load_fd_block, 120 | b"WS": self.__load_ws_block, 121 | b"SL": self.__load_sl_block, 122 | b"WL": self.__load_wl_block, 123 | b"MP": self.__load_mp_block, 124 | b"SU": self.__load_su_block, 125 | b"ES": self.__load_es_block, 126 | b"X1": self.__load_x1_block, 127 | b"NE": self.__load_ne_block, 128 | # TODO: No documentation? 129 | # b'EF': self.__load_ef_block, 130 | b"PN": self.__load_pn_block, 131 | b"S2": self.__load_s2_block, 132 | } 133 | 134 | if isinstance(file_name, str): 135 | self.load_from_file(file_name) 136 | 137 | def load_from_file(self, file_name: Optional[str] = None) -> None: 138 | if isinstance(file_name, str): 139 | self.file_name = file_name 140 | if self.file_name is None: 141 | raise RuntimeError( 142 | "No file name set, either set self.file_name or pass file_name to the function" 143 | ) 144 | 145 | # since we're loading from an uncompressed file, we can just check the file magic number 146 | with open(self.file_name, "rb") as f: 147 | detect_magic = f.peek(len(FILE_MAGIC_STR))[: len(FILE_MAGIC_STR)] 148 | if detect_magic == FILE_MAGIC_STR: 149 | return self.load_from_stream(f, _FurInsImportType.FORMAT_0_FILE) 150 | elif detect_magic[: len(DEV127_FILE_MAGIC_STR)] == DEV127_FILE_MAGIC_STR: 151 | return self.load_from_stream(f, _FurInsImportType.FORMAT_1_FILE) 152 | else: # uncompressed for sure 153 | raise ValueError("No recognized file type magic") 154 | 155 | def load_from_bytes( 156 | self, data: bytes, import_as: Union[int, _FurInsImportType] 157 | ) -> None: 158 | """ 159 | Load an instrument from a series of bytes. 160 | 161 | :param data: Bytes 162 | :param import_as: int 163 | see :method:`FurnaceInstrument.load_from_stream` 164 | 165 | """ 166 | return self.load_from_stream(BytesIO(data), import_as) 167 | 168 | def load_from_stream( 169 | self, stream: BinaryIO, import_as: Union[int, _FurInsImportType] 170 | ) -> None: 171 | """ 172 | Load a module from an **uncompressed** stream. 173 | 174 | :param stream: File-like object containing the uncompressed module. 175 | :param import_as: int 176 | - 0 = old format instrument file 177 | - 1 = old format, embedded in module 178 | - 2 = new format instrument file 179 | - 3 = new format, embedded in module 180 | """ 181 | if import_as == _FurInsImportType.FORMAT_0_FILE: 182 | if stream.read(len(FILE_MAGIC_STR)) != FILE_MAGIC_STR: 183 | raise ValueError("Bad magic value for a format 1 file") 184 | self.protocol_version = 0 185 | self.meta.version = read_short(stream) 186 | read_short(stream) # reserved 187 | ins_data_ptr = read_int(stream) 188 | num_waves = read_short(stream) 189 | num_samples = read_short(stream) 190 | read_int(stream) # reserved 191 | 192 | # these don't exist for format 1 instrs. 193 | self.__wavetable_ptr = [read_int(stream) for _ in range(num_waves)] 194 | self.__sample_ptr = [read_int(stream) for _ in range(num_samples)] 195 | 196 | stream.seek(ins_data_ptr) 197 | self.__load_format_0_embed(stream) 198 | # TODO: load wavetables and samples 199 | 200 | elif import_as == _FurInsImportType.FORMAT_0_EMBED: 201 | self.protocol_version = 0 202 | return self.__load_format_0_embed(stream) 203 | 204 | elif import_as == _FurInsImportType.FORMAT_1_FILE: 205 | if stream.read(len(DEV127_FILE_MAGIC_STR)) != DEV127_FILE_MAGIC_STR: 206 | raise ValueError("Bad magic value for a format 1 file") 207 | self.protocol_version = 1 208 | self.__load_format_1(stream) 209 | # TODO: load wavetables and samples 210 | 211 | elif import_as == _FurInsImportType.FORMAT_1_EMBED: 212 | if stream.read(len(DEV127_EMBED_MAGIC_STR)) != DEV127_EMBED_MAGIC_STR: 213 | raise ValueError("Bad magic value for a format 1 embed") 214 | self.protocol_version = 1 215 | ins_data = BytesIO(stream.read(read_int(stream))) 216 | return self.__load_format_1(ins_data) 217 | 218 | else: 219 | raise ValueError("Invalid import type") 220 | 221 | def __str__(self) -> str: 222 | return '' % (self.get_name(), self.meta.type) 223 | 224 | def __load_format_1(self, stream: BinaryIO) -> None: 225 | # skip headers and magic 226 | self.meta.version = read_short(stream) 227 | self.meta.type = InstrumentType(read_short(stream)) 228 | self.features.clear() 229 | 230 | # add all the features 231 | feat = self.__read_format_1_feature(stream) 232 | while isinstance(feat, InsFeatureAbstract): 233 | self.features.append(feat) 234 | feat = self.__read_format_1_feature(stream) 235 | 236 | def __read_format_1_feature( 237 | self, stream: BinaryIO 238 | ) -> Optional[object]: # subclass InsFeatureAbstract 239 | code = stream.read(2) 240 | if code == b"EN" or code == b"": # eof 241 | return None 242 | 243 | len_block = read_short(stream) 244 | feature_block = BytesIO(stream.read(len_block)) 245 | 246 | # if this fails it might be a malformed file 247 | return self.__map_to_fn[code](feature_block) 248 | 249 | def get_name(self) -> str: 250 | """ 251 | Shortcut to fetch the instrument name. 252 | 253 | :return: Instrument name 254 | """ 255 | name = "" 256 | for i in self.features: 257 | if isinstance(i, InsFeatureName): 258 | name = i # InsFeatureName also subclasses 'str' so it's fine 259 | return name 260 | 261 | # format 1 features 262 | 263 | def __load_na_block(self, stream: BytesIO) -> InsFeatureName: 264 | return InsFeatureName(read_str(stream)) 265 | 266 | def __load_fm_block(self, stream: BytesIO) -> InsFeatureFM: 267 | fm = InsFeatureFM() 268 | 269 | # read base data 270 | data = [read_byte(stream) for _ in range(4)] 271 | 272 | current = data.pop(0) 273 | ops = current & 0b1111 274 | fm.op_list[0].enable = bool(current & 16) 275 | fm.op_list[1].enable = bool(current & 32) 276 | fm.op_list[2].enable = bool(current & 64) 277 | fm.op_list[3].enable = bool(current & 128) 278 | 279 | current = data.pop(0) 280 | fm.alg = (current >> 4) & 0b111 281 | fm.fb = current & 0b111 282 | 283 | current = data.pop(0) 284 | fm.fms2 = (current >> 5) & 0b111 285 | fm.ams = (current >> 3) & 0b11 286 | fm.fms = current & 0b111 287 | 288 | current = data.pop(0) 289 | fm.ams2 = (current >> 6) & 0b11 290 | if current & 32: 291 | fm.ops = 4 292 | else: 293 | fm.ops = 2 294 | fm.opll_preset = current & 31 295 | 296 | # read operators 297 | for op in range(ops): 298 | data = [read_byte(stream) for _ in range(8)] 299 | 300 | current = data.pop(0) 301 | fm.op_list[op].ksr = bool(current & 128) 302 | fm.op_list[op].dt = (current >> 4) & 7 303 | fm.op_list[op].mult = current & 15 304 | 305 | current = data.pop(0) 306 | fm.op_list[op].sus = bool(current & 128) 307 | fm.op_list[op].tl = current & 127 308 | 309 | current = data.pop(0) 310 | fm.op_list[op].rs = (current >> 6) & 3 311 | fm.op_list[op].vib = bool(current & 32) 312 | fm.op_list[op].ar = current & 31 313 | 314 | current = data.pop(0) 315 | fm.op_list[op].am = bool(current & 128) 316 | fm.op_list[op].ksl = (current >> 5) & 3 317 | fm.op_list[op].dr = current & 31 318 | 319 | current = data.pop(0) 320 | fm.op_list[op].egt = bool(current & 128) 321 | fm.op_list[op].kvs = (current >> 5) & 3 322 | fm.op_list[op].d2r = current & 31 323 | 324 | current = data.pop(0) 325 | fm.op_list[op].sl = (current >> 4) & 15 326 | fm.op_list[op].rr = current & 15 327 | 328 | current = data.pop(0) 329 | fm.op_list[op].dvb = (current >> 4) & 15 330 | fm.op_list[op].ssg_env = current & 15 331 | 332 | current = data.pop(0) 333 | fm.op_list[op].dam = (current >> 5) & 7 334 | fm.op_list[op].dt2 = (current >> 3) & 3 335 | fm.op_list[op].ws = current & 7 336 | 337 | return fm 338 | 339 | def __common_ma_block(self, stream: BytesIO, macro_class: Type[T_MACRO]) -> T_MACRO: 340 | ma = macro_class() 341 | ma.macros.clear() 342 | read_short(stream) # header size 343 | 344 | target_code: Union[MacroCode, OpMacroCode] 345 | 346 | if macro_class in [ 347 | InsFeatureOpr1Macro, 348 | InsFeatureOpr2Macro, 349 | InsFeatureOpr3Macro, 350 | InsFeatureOpr4Macro, 351 | ]: 352 | target_code = OpMacroCode(read_byte(stream)) 353 | else: 354 | target_code = MacroCode(read_byte(stream)) 355 | 356 | while target_code != MacroCode.STOP: 357 | new_macro = SingleMacro(kind=target_code) 358 | 359 | length = read_byte(stream) 360 | loop = read_byte(stream) 361 | release = read_byte(stream) 362 | 363 | new_macro.mode = read_byte(stream) 364 | flags = read_byte(stream) 365 | 366 | word_size = MacroSize(flags >> 6 & 0b11) # type: ignore 367 | new_macro.type = MacroType(flags >> 1 & 0b11) 368 | new_macro.open = bool(flags & 1) 369 | new_macro.delay = read_byte(stream) 370 | new_macro.speed = read_byte(stream) 371 | 372 | # adsr and lfo will simply be kept as a list 373 | macro_content: List[Union[int, MacroItem]] = [ 374 | int.from_bytes( 375 | stream.read(word_size.num_bytes), 376 | byteorder="little", 377 | signed=word_size.signed, 378 | ) 379 | for _ in range(length) 380 | ] 381 | 382 | if loop != 0xFF: # hard limit in new macro 383 | macro_content.insert(loop, MacroItem.LOOP) 384 | 385 | if release != 0xFF: # ^ 386 | macro_content.insert(release, MacroItem.RELEASE) 387 | 388 | new_macro.data = macro_content 389 | 390 | ma.macros.append(new_macro) 391 | 392 | if macro_class in [ 393 | InsFeatureOpr1Macro, 394 | InsFeatureOpr2Macro, 395 | InsFeatureOpr3Macro, 396 | InsFeatureOpr4Macro, 397 | ]: 398 | target_code = OpMacroCode(read_byte(stream)) 399 | else: 400 | target_code = MacroCode(read_byte(stream)) 401 | 402 | return ma 403 | 404 | def __load_ma_block(self, stream: BytesIO) -> InsFeatureMacro: 405 | return self.__common_ma_block(stream, InsFeatureMacro) 406 | 407 | def __load_o1_block(self, stream: BytesIO) -> InsFeatureOpr1Macro: 408 | return self.__common_ma_block(stream, InsFeatureOpr1Macro) 409 | 410 | def __load_o2_block(self, stream: BytesIO) -> InsFeatureOpr2Macro: 411 | return self.__common_ma_block(stream, InsFeatureOpr2Macro) 412 | 413 | def __load_o3_block(self, stream: BytesIO) -> InsFeatureOpr3Macro: 414 | return self.__common_ma_block(stream, InsFeatureOpr3Macro) 415 | 416 | def __load_o4_block(self, stream: BytesIO) -> InsFeatureOpr4Macro: 417 | return self.__common_ma_block(stream, InsFeatureOpr4Macro) 418 | 419 | def __load_c64_block(self, stream: BytesIO) -> InsFeatureC64: 420 | c64 = InsFeatureC64() 421 | 422 | data = [read_byte(stream) for _ in range(4)] 423 | 424 | current = data.pop(0) 425 | c64.duty_is_abs = bool((current >> 7) & 1) 426 | c64.init_filter = bool((current >> 6) & 1) 427 | c64.vol_is_cutoff = bool((current >> 5) & 1) 428 | c64.to_filter = bool((current >> 4) & 1) 429 | c64.noise_on = bool((current >> 3) & 1) 430 | c64.pulse_on = bool((current >> 2) & 1) 431 | c64.saw_on = bool((current >> 1) & 1) 432 | c64.tri_on = bool(current & 1) 433 | 434 | current = data.pop(0) 435 | c64.osc_sync = bool((current >> 7) & 1) 436 | c64.ring_mod = bool((current >> 6) & 1) 437 | c64.no_test = bool((current >> 5) & 1) 438 | c64.filter_is_abs = bool((current >> 4) & 1) 439 | c64.ch3_off = bool((current >> 3) & 1) 440 | c64.bp = bool((current >> 2) & 1) 441 | c64.hp = bool((current >> 1) & 1) 442 | c64.lp = bool(current & 1) 443 | 444 | current = data.pop(0) 445 | c64.envelope.a = (current >> 4) & 0b1111 446 | c64.envelope.d = current & 0b1111 447 | 448 | current = data.pop(0) 449 | c64.envelope.s = (current >> 4) & 0b1111 450 | c64.envelope.r = current & 0b1111 451 | 452 | c64.duty = read_short(stream) 453 | 454 | c_r = read_short(stream) 455 | c64.cut = c_r & 0b1111111111 456 | c64.res = (c_r >> 12) & 0b1111 457 | 458 | return c64 459 | 460 | def __load_gb_block(self, stream: BytesIO) -> InsFeatureGB: 461 | gb = InsFeatureGB() 462 | 463 | data = [read_byte(stream) for _ in range(4)] 464 | 465 | current = data.pop(0) 466 | gb.env_vol = current & 0b1111 467 | gb.env_dir = (current >> 4) & 1 468 | gb.env_len = (current >> 5) & 0b111 469 | 470 | gb.sound_len = data.pop(0) 471 | 472 | current = data.pop(0) 473 | gb.soft_env = bool(current & 1) 474 | gb.always_init = bool((current >> 1) & 1) 475 | 476 | hw_seq_len = data.pop(0) 477 | for i in range(hw_seq_len): 478 | seq_entry = GBHwSeq(GBHwCommand(read_byte(stream))) 479 | seq_entry.data = [read_byte(stream), read_byte(stream)] 480 | gb.hw_seq.append(seq_entry) 481 | 482 | return gb 483 | 484 | def __load_sm_block(self, stream: BytesIO) -> InsFeatureAmiga: 485 | sm = InsFeatureAmiga() 486 | 487 | sm.init_sample = read_short(stream) 488 | 489 | current = read_byte(stream) 490 | sm.use_wave = bool((current >> 2) & 1) 491 | sm.use_sample = bool((current >> 1) & 1) 492 | sm.use_note_map = bool(current & 1) 493 | 494 | sm.wave_len = read_byte(stream) 495 | 496 | if sm.use_note_map: 497 | for i in range(len(sm.sample_map)): 498 | sm.sample_map[i].freq = read_short(stream) 499 | sm.sample_map[i].sample_index = read_short(stream) 500 | 501 | return sm 502 | 503 | def __load_ld_block(self, stream: BytesIO) -> InsFeatureOPLDrums: 504 | return InsFeatureOPLDrums( 505 | fixed_drums=bool(read_byte(stream) & 1), 506 | kick_freq=read_short(stream), 507 | snare_hat_freq=read_short(stream), 508 | tom_top_freq=read_short(stream), 509 | ) 510 | 511 | def __load_sn_block(self, stream: BytesIO) -> InsFeatureSNES: 512 | sn = InsFeatureSNES() 513 | 514 | data = [read_byte(stream) for _ in range(4)] 515 | 516 | current = data.pop(0) 517 | sn.envelope.d = (current >> 4) & 0b1111 518 | sn.envelope.a = current & 0b1111 519 | 520 | current = data.pop(0) 521 | sn.envelope.s = (current >> 4) & 0b1111 522 | sn.envelope.r = current & 0b1111 523 | 524 | current = data.pop(0) 525 | sn.use_env = bool((current >> 4) & 1) 526 | sn.sus = SNESSusMode((current >> 3) & 1) 527 | 528 | gain_mode = current & 0b111 529 | if current < 4: 530 | gain_mode = 0 531 | sn.gain_mode = GainMode(gain_mode) 532 | 533 | sn.gain = data.pop(0) 534 | 535 | if self.meta.version >= 131: 536 | d2s = read_byte(stream) 537 | sn.sus = SNESSusMode((d2s >> 5 & 0b11)) 538 | sn.d2 = d2s & 31 539 | 540 | return sn 541 | 542 | def __load_n1_block(self, stream: BytesIO) -> InsFeatureN163: 543 | return InsFeatureN163( 544 | wave=read_int(stream), 545 | wave_pos=read_byte(stream), 546 | wave_len=read_byte(stream), 547 | wave_mode=read_byte(stream), 548 | ) 549 | 550 | def __load_fd_block(self, stream: BytesIO) -> InsFeatureFDS: 551 | fd = InsFeatureFDS( 552 | mod_speed=read_int(stream), 553 | mod_depth=read_int(stream), 554 | init_table_with_first_wave=bool(read_byte(stream)), 555 | ) 556 | for i in range(32): 557 | fd.mod_table[i] = read_byte(stream) 558 | return fd 559 | 560 | def __load_ws_block(self, stream: BytesIO) -> InsFeatureWaveSynth: 561 | return InsFeatureWaveSynth( 562 | wave_indices=[read_int(stream), read_int(stream)], 563 | rate_divider=read_byte(stream), 564 | effect=WaveFX(read_byte(stream)), 565 | enabled=bool(read_byte(stream) & 1), 566 | global_effect=bool(read_byte(stream) & 1), 567 | speed=read_byte(stream), 568 | params=[ 569 | read_byte(stream), 570 | read_byte(stream), 571 | read_byte(stream), 572 | read_byte(stream), 573 | ], 574 | ) 575 | 576 | def __common_pointers_block( 577 | self, stream: BytesIO, ptr_class: Type[T_POINTERS] 578 | ) -> T_POINTERS: 579 | pt = ptr_class() 580 | num_entries = read_byte(stream) 581 | 582 | for _ in range(num_entries): 583 | pt.pointers[read_byte(stream)] = -1 584 | 585 | for i in pt.pointers: 586 | pt.pointers[i] = read_int(stream) 587 | 588 | return pt 589 | 590 | def __load_sl_block(self, stream: BytesIO) -> InsFeatureSampleList: 591 | return self.__common_pointers_block(stream, InsFeatureSampleList) 592 | 593 | def __load_wl_block(self, stream: BytesIO) -> InsFeatureWaveList: 594 | return self.__common_pointers_block(stream, InsFeatureWaveList) 595 | 596 | def __load_mp_block(self, stream: BytesIO) -> InsFeatureMultiPCM: 597 | return InsFeatureMultiPCM( 598 | ar=read_byte(stream), 599 | d1r=read_byte(stream), 600 | dl=read_byte(stream), 601 | d2r=read_byte(stream), 602 | rr=read_byte(stream), 603 | rc=read_byte(stream), 604 | lfo=read_byte(stream), 605 | vib=read_byte(stream), 606 | am=read_byte(stream), 607 | ) 608 | 609 | def __load_su_block(self, stream: BytesIO) -> InsFeatureSoundUnit: 610 | return InsFeatureSoundUnit(switch_roles=bool(read_byte(stream))) 611 | 612 | def __load_es_block(self, stream: BytesIO) -> InsFeatureES5506: 613 | return InsFeatureES5506( 614 | filter_mode=ESFilterMode(read_byte(stream)), 615 | k1=read_short(stream), 616 | k2=read_short(stream), 617 | env_count=read_short(stream), 618 | left_volume_ramp=read_byte(stream), 619 | right_volume_ramp=read_byte(stream), 620 | k1_ramp=read_byte(stream), 621 | k2_ramp=read_byte(stream), 622 | k1_slow=read_byte(stream), 623 | k2_slow=read_byte(stream), 624 | ) 625 | 626 | def __load_x1_block(self, stream: BytesIO) -> InsFeatureX1010: 627 | return InsFeatureX1010(bank_slot=read_int(stream)) 628 | 629 | def __load_ne_block(self, stream: BytesIO) -> InsFeatureDPCMMap: 630 | sm = InsFeatureDPCMMap() 631 | 632 | sm.use_map = bool(read_byte(stream) & 1) 633 | 634 | if sm.use_map: 635 | for i in range(len(sm.sample_map)): 636 | sm.sample_map[i].pitch = read_byte(stream) 637 | sm.sample_map[i].delta = read_byte(stream) 638 | 639 | return sm 640 | 641 | # TODO: No documentation? 642 | # def __load_ef_block(self, stream: BytesIO) -> InsFeatureESFM: 643 | # pass 644 | 645 | def __load_pn_block(self, stream: BytesIO) -> InsFeaturePowerNoise: 646 | return InsFeaturePowerNoise(octave=read_byte(stream)) 647 | 648 | def __load_s2_block(self, stream: BytesIO) -> InsFeatureSID2: 649 | current_byte = read_byte(stream) 650 | return InsFeatureSID2( 651 | volume=current_byte & 0b1111, 652 | wave_mix=(current_byte >> 4) & 0b11, 653 | noise_mode=(current_byte >> 6) & 0b11, 654 | ) 655 | 656 | # format 0; also used for file because it includes the "INST" header too 657 | 658 | def __load_format_0_embed(self, stream: BinaryIO) -> None: 659 | # load format 0 as a series of format 1 feature blocks 660 | 661 | # aux function... 662 | def add_to_macro_data( 663 | macro: List[Union[int, MacroItem]], 664 | loop: Optional[int] = 0xFFFFFFFF, 665 | release: Optional[int] = 0xFFFFFFFF, 666 | data: Optional[List[int]] = None, 667 | ) -> None: 668 | if data is not None: 669 | macro.extend(data) 670 | if ( 671 | loop is not None and loop != 0xFFFFFFFF 672 | ): # old macros have a 4-byte length 673 | macro.insert(loop, MacroItem.LOOP) 674 | if release is not None and release != 0xFFFFFFFF: 675 | macro.insert(release, MacroItem.RELEASE) 676 | 677 | # we check the header here 678 | if stream.read(len(EMBED_MAGIC_STR)) != EMBED_MAGIC_STR: 679 | raise RuntimeError("Bad magic value for a format 0 embed") 680 | 681 | blk_size = read_int(stream) 682 | if blk_size > 0: 683 | ins_data: Union[BytesIO, BinaryIO] = BytesIO(stream.read(blk_size)) 684 | else: 685 | ins_data = stream 686 | 687 | self.meta.version = read_short(ins_data) # overwrites the file header version 688 | self.meta.type = InstrumentType(read_byte(ins_data)) 689 | 690 | read_byte(ins_data) 691 | 692 | # read all features in one go! 693 | self.features.clear() 694 | 695 | # name, insert immediately 696 | self.features.append(InsFeatureName(read_str(ins_data))) 697 | 698 | # fm 699 | if True: 700 | fm = InsFeatureFM( 701 | alg=read_byte(ins_data), 702 | fb=read_byte(ins_data), 703 | fms=read_byte(ins_data), 704 | ams=read_byte(ins_data), 705 | ops=read_byte(ins_data), 706 | opll_preset=read_byte(ins_data), 707 | ) 708 | read_short(ins_data) 709 | for i in range(4): 710 | fm.op_list[i].am = bool(read_byte(ins_data)) 711 | fm.op_list[i].ar = read_byte(ins_data) 712 | fm.op_list[i].dr = read_byte(ins_data) 713 | fm.op_list[i].mult = read_byte(ins_data) 714 | fm.op_list[i].rr = read_byte(ins_data) 715 | fm.op_list[i].sl = read_byte(ins_data) 716 | fm.op_list[i].tl = read_byte(ins_data) 717 | fm.op_list[i].dt2 = read_byte(ins_data) 718 | fm.op_list[i].rs = read_byte(ins_data) 719 | fm.op_list[i].dt = read_byte(ins_data) 720 | fm.op_list[i].d2r = read_byte(ins_data) 721 | fm.op_list[i].ssg_env = read_byte(ins_data) 722 | fm.op_list[i].dam = read_byte(ins_data) 723 | fm.op_list[i].dvb = read_byte(ins_data) 724 | fm.op_list[i].egt = bool(read_byte(ins_data)) 725 | fm.op_list[i].ksl = read_byte(ins_data) 726 | fm.op_list[i].sus = bool(read_byte(ins_data)) 727 | fm.op_list[i].vib = bool(read_byte(ins_data)) 728 | fm.op_list[i].ws = read_byte(ins_data) 729 | fm.op_list[i].ksr = bool(read_byte(ins_data)) 730 | en = read_byte(ins_data) 731 | if self.meta.version >= 114: 732 | fm.op_list[i].enable = bool(en) 733 | kvs = read_byte(ins_data) 734 | if self.meta.version >= 115: 735 | fm.op_list[i].kvs = kvs 736 | ins_data.read(10) 737 | self.features.append(fm) 738 | 739 | # gameboy 740 | if True: 741 | gb = InsFeatureGB( 742 | env_vol=read_byte(ins_data), 743 | env_dir=read_byte(ins_data), 744 | env_len=read_byte(ins_data), 745 | sound_len=read_byte(ins_data), 746 | ) 747 | self.features.append(gb) 748 | 749 | # c64 750 | if True: 751 | c64 = InsFeatureC64( 752 | tri_on=bool(read_byte(ins_data)), 753 | saw_on=bool(read_byte(ins_data)), 754 | pulse_on=bool(read_byte(ins_data)), 755 | noise_on=bool(read_byte(ins_data)), 756 | duty=read_short(ins_data), 757 | ring_mod=read_byte(ins_data), 758 | osc_sync=read_byte(ins_data), 759 | to_filter=bool(read_byte(ins_data)), 760 | init_filter=bool(read_byte(ins_data)), 761 | vol_is_cutoff=bool(read_byte(ins_data)), 762 | res=read_byte(ins_data), 763 | lp=bool(read_byte(ins_data)), 764 | bp=bool(read_byte(ins_data)), 765 | hp=bool(read_byte(ins_data)), 766 | ch3_off=bool(read_byte(ins_data)), 767 | cut=read_short(ins_data), 768 | duty_is_abs=bool(read_byte(ins_data)), 769 | filter_is_abs=bool(read_byte(ins_data)), 770 | ) 771 | c64.envelope = GenericADSR( 772 | a=read_byte(ins_data), 773 | d=read_byte(ins_data), 774 | s=read_byte(ins_data), 775 | r=read_byte(ins_data), 776 | ) 777 | self.features.append(c64) 778 | 779 | # amiga 780 | if True: 781 | amiga = InsFeatureAmiga(init_sample=read_short(ins_data)) 782 | 783 | wave = read_byte(ins_data) 784 | wavelen = read_byte(ins_data) 785 | if self.meta.version >= 82: 786 | amiga.use_wave = bool(wave) 787 | amiga.wave_len = wavelen 788 | 789 | for _ in range(12): 790 | read_byte(ins_data) # reserved 791 | 792 | self.features.append(amiga) 793 | 794 | # standard 795 | if True: 796 | mac = InsFeatureMacro() 797 | 798 | vol_mac = SingleMacro(kind=MacroCode.VOL) 799 | arp_mac = SingleMacro(kind=MacroCode.ARP) 800 | duty_mac = SingleMacro(kind=MacroCode.DUTY) 801 | wave_mac = SingleMacro(kind=MacroCode.WAVE) 802 | 803 | vol_mac.data.clear() 804 | arp_mac.data.clear() 805 | duty_mac.data.clear() 806 | wave_mac.data.clear() 807 | 808 | mac_list: List[SingleMacro] = [vol_mac, arp_mac, duty_mac, wave_mac] 809 | mac.macros = mac_list 810 | 811 | vol_mac_len = read_int(ins_data) 812 | arp_mac_len = read_int(ins_data) 813 | duty_mac_len = read_int(ins_data) 814 | wave_mac_len = read_int(ins_data) 815 | 816 | if self.meta.version >= 17: 817 | pitch_mac = SingleMacro(kind=MacroCode.PITCH) 818 | x1_mac = SingleMacro(kind=MacroCode.EX1) 819 | x2_mac = SingleMacro(kind=MacroCode.EX2) 820 | x3_mac = SingleMacro(kind=MacroCode.EX3) 821 | 822 | pitch_mac.data.clear() 823 | x1_mac.data.clear() 824 | x2_mac.data.clear() 825 | x3_mac.data.clear() 826 | 827 | mac_list.extend([pitch_mac, x1_mac, x2_mac, x3_mac]) 828 | 829 | pitch_mac_len = read_int(ins_data) 830 | x1_mac_len = read_int(ins_data) 831 | x2_mac_len = read_int(ins_data) 832 | x3_mac_len = read_int(ins_data) 833 | 834 | vol_mac_loop = read_int(ins_data) 835 | arp_mac_loop = read_int(ins_data) 836 | duty_mac_loop = read_int(ins_data) 837 | wave_mac_loop = read_int(ins_data) 838 | 839 | if self.meta.version >= 17: 840 | pitch_mac_loop = read_int(ins_data) 841 | x1_mac_loop = read_int(ins_data) 842 | x2_mac_loop = read_int(ins_data) 843 | x3_mac_loop = read_int(ins_data) 844 | 845 | arp_mac_mode = read_byte(ins_data) 846 | old_vol_height = read_byte(ins_data) 847 | old_duty_height = read_byte(ins_data) 848 | 849 | read_byte(ins_data) 850 | 851 | add_to_macro_data( 852 | vol_mac.data, 853 | loop=vol_mac_loop, 854 | release=None, 855 | data=[read_int(ins_data) for _ in range(vol_mac_len)], 856 | ) 857 | 858 | add_to_macro_data( 859 | arp_mac.data, 860 | loop=arp_mac_loop, 861 | release=None, 862 | data=[read_int(ins_data) for _ in range(arp_mac_len)], 863 | ) 864 | 865 | add_to_macro_data( 866 | duty_mac.data, 867 | loop=duty_mac_loop, 868 | release=None, 869 | data=[read_int(ins_data) for _ in range(duty_mac_len)], 870 | ) 871 | 872 | add_to_macro_data( 873 | wave_mac.data, 874 | loop=wave_mac_loop, 875 | release=None, 876 | data=[read_int(ins_data) for _ in range(wave_mac_len)], 877 | ) 878 | 879 | # adjust values 880 | if self.meta.version < 31: 881 | if arp_mac_mode == 0: 882 | for j in range(len(arp_mac.data)): 883 | if isinstance(arp_mac.data[j], int): 884 | arp_mac.data[j] = cast(int, arp_mac.data[j]) - 12 885 | if self.meta.version < 87: 886 | if c64.vol_is_cutoff and not c64.filter_is_abs: 887 | for j in range(len(vol_mac.data)): 888 | if isinstance(vol_mac.data[j], int): 889 | vol_mac.data[j] = cast(int, vol_mac.data[j]) - 18 890 | if c64.duty_is_abs: # TODO 891 | for j in range(len(duty_mac.data)): 892 | if isinstance(duty_mac.data[j], int): 893 | duty_mac.data[j] = cast(int, duty_mac.data[j]) - 12 894 | if self.meta.version < 112: 895 | if arp_mac_mode == 1: # fixed arp! 896 | for i in range(len(arp_mac.data)): 897 | if isinstance(arp_mac.data[i], int): 898 | arp_mac.data[i] = cast(int, arp_mac.data[i]) | (1 << 30) 899 | if len(arp_mac.data) > 0: 900 | if arp_mac_loop != 0xFFFFFFFF: 901 | if arp_mac_loop == arp_mac_len + 1: 902 | arp_mac.data[-1] = 0 903 | arp_mac.data.append(MacroItem.LOOP) 904 | elif arp_mac_loop == arp_mac_len: 905 | arp_mac.data.append(0) 906 | else: 907 | arp_mac.data.append(0) 908 | 909 | # read more macros 910 | if self.meta.version >= 17: 911 | add_to_macro_data( 912 | pitch_mac.data, 913 | loop=pitch_mac_loop, 914 | release=None, 915 | data=[read_int(ins_data) for _ in range(pitch_mac_len)], 916 | ) 917 | 918 | add_to_macro_data( 919 | x1_mac.data, 920 | loop=x1_mac_loop, 921 | release=None, 922 | data=[read_int(ins_data) for _ in range(x1_mac_len)], 923 | ) 924 | 925 | add_to_macro_data( 926 | x2_mac.data, 927 | loop=x2_mac_loop, 928 | release=None, 929 | data=[read_int(ins_data) for _ in range(x2_mac_len)], 930 | ) 931 | 932 | add_to_macro_data( 933 | x3_mac.data, 934 | loop=x3_mac_loop, 935 | release=None, 936 | data=[read_int(ins_data) for _ in range(x3_mac_len)], 937 | ) 938 | else: 939 | if self.meta.type == InstrumentType.STANDARD: 940 | if old_vol_height == 31: 941 | self.meta.type = InstrumentType.PCE 942 | elif old_duty_height == 31: 943 | self.meta.type = InstrumentType.SSG 944 | 945 | self.features.append(mac) 946 | 947 | # fm macros 948 | if True: 949 | if self.meta.version >= 29: 950 | alg_mac = SingleMacro(kind=MacroCode.ALG) 951 | fb_mac = SingleMacro(kind=MacroCode.FB) 952 | fms_mac = SingleMacro(kind=MacroCode.FMS) 953 | ams_mac = SingleMacro(kind=MacroCode.AMS) 954 | mac_list.extend([alg_mac, fb_mac, fms_mac, ams_mac]) 955 | 956 | alg_mac.data.clear() 957 | fb_mac.data.clear() 958 | fms_mac.data.clear() 959 | ams_mac.data.clear() 960 | 961 | alg_mac_len = read_int(ins_data) 962 | fb_mac_len = read_int(ins_data) 963 | fms_mac_len = read_int(ins_data) 964 | ams_mac_len = read_int(ins_data) 965 | 966 | alg_mac_loop = read_int(ins_data) 967 | fb_mac_loop = read_int(ins_data) 968 | fms_mac_loop = read_int(ins_data) 969 | ams_mac_loop = read_int(ins_data) 970 | 971 | vol_mac.open = bool(read_byte(ins_data)) 972 | arp_mac.open = bool(read_byte(ins_data)) 973 | duty_mac.open = bool(read_byte(ins_data)) 974 | wave_mac.open = bool(read_byte(ins_data)) 975 | pitch_mac.open = bool(read_byte(ins_data)) 976 | x1_mac.open = bool(read_byte(ins_data)) 977 | x2_mac.open = bool(read_byte(ins_data)) 978 | x3_mac.open = bool(read_byte(ins_data)) 979 | 980 | alg_mac.open = bool(read_byte(ins_data)) 981 | fb_mac.open = bool(read_byte(ins_data)) 982 | fms_mac.open = bool(read_byte(ins_data)) 983 | ams_mac.open = bool(read_byte(ins_data)) 984 | 985 | add_to_macro_data( 986 | alg_mac.data, 987 | loop=alg_mac_loop, 988 | release=None, 989 | data=[read_int(ins_data) for _ in range(alg_mac_len)], 990 | ) 991 | 992 | add_to_macro_data( 993 | fb_mac.data, 994 | loop=fb_mac_loop, 995 | release=None, 996 | data=[read_int(ins_data) for _ in range(fb_mac_len)], 997 | ) 998 | 999 | add_to_macro_data( 1000 | fms_mac.data, 1001 | loop=fms_mac_loop, 1002 | release=None, 1003 | data=[read_int(ins_data) for _ in range(fms_mac_len)], 1004 | ) 1005 | 1006 | add_to_macro_data( 1007 | ams_mac.data, 1008 | loop=ams_mac_loop, 1009 | release=None, 1010 | data=[read_int(ins_data) for _ in range(ams_mac_len)], 1011 | ) 1012 | 1013 | # fm op macros 1014 | if True: 1015 | if self.meta.version >= 29: 1016 | new_ops: Dict[int, InsFeatureMacro] = {} # actual ops 1017 | 1018 | ops_types: Dict[int, Type[InsFeatureMacro]] = { # classes 1019 | 0: InsFeatureOpr1Macro, 1020 | 1: InsFeatureOpr2Macro, 1021 | 2: InsFeatureOpr3Macro, 1022 | 3: InsFeatureOpr4Macro, 1023 | } 1024 | 1025 | ops: Dict[int, Dict[str, Union[int, bool]]] = { # params 1026 | 0: {}, 1027 | 1: {}, 1028 | 2: {}, 1029 | 3: {}, 1030 | } 1031 | 1032 | for opi in ops: 1033 | ops[opi]["am_mac_len"] = read_int(ins_data) 1034 | ops[opi]["ar_mac_len"] = read_int(ins_data) 1035 | ops[opi]["dr_mac_len"] = read_int(ins_data) 1036 | ops[opi]["mult_mac_len"] = read_int(ins_data) 1037 | ops[opi]["rr_mac_len"] = read_int(ins_data) 1038 | ops[opi]["sl_mac_len"] = read_int(ins_data) 1039 | ops[opi]["tl_mac_len"] = read_int(ins_data) 1040 | ops[opi]["dt2_mac_len"] = read_int(ins_data) 1041 | ops[opi]["rs_mac_len"] = read_int(ins_data) 1042 | ops[opi]["dt_mac_len"] = read_int(ins_data) 1043 | ops[opi]["d2r_mac_len"] = read_int(ins_data) 1044 | ops[opi]["ssg_mac_len"] = read_int(ins_data) 1045 | 1046 | ops[opi]["am_mac_loop"] = read_int(ins_data) 1047 | ops[opi]["ar_mac_loop"] = read_int(ins_data) 1048 | ops[opi]["dr_mac_loop"] = read_int(ins_data) 1049 | ops[opi]["mult_mac_loop"] = read_int(ins_data) 1050 | ops[opi]["rr_mac_loop"] = read_int(ins_data) 1051 | ops[opi]["sl_mac_loop"] = read_int(ins_data) 1052 | ops[opi]["tl_mac_loop"] = read_int(ins_data) 1053 | ops[opi]["dt2_mac_loop"] = read_int(ins_data) 1054 | ops[opi]["rs_mac_loop"] = read_int(ins_data) 1055 | ops[opi]["dt_mac_loop"] = read_int(ins_data) 1056 | ops[opi]["d2r_mac_loop"] = read_int(ins_data) 1057 | ops[opi]["ssg_mac_loop"] = read_int(ins_data) 1058 | 1059 | ops[opi]["am_mac_open"] = read_byte(ins_data) 1060 | ops[opi]["ar_mac_open"] = read_byte(ins_data) 1061 | ops[opi]["dr_mac_open"] = read_byte(ins_data) 1062 | ops[opi]["mult_mac_open"] = read_byte(ins_data) 1063 | ops[opi]["rr_mac_open"] = read_byte(ins_data) 1064 | ops[opi]["sl_mac_open"] = read_byte(ins_data) 1065 | ops[opi]["tl_mac_open"] = read_byte(ins_data) 1066 | ops[opi]["dt2_mac_open"] = read_byte(ins_data) 1067 | ops[opi]["rs_mac_open"] = read_byte(ins_data) 1068 | ops[opi]["dt_mac_open"] = read_byte(ins_data) 1069 | ops[opi]["d2r_mac_open"] = read_byte(ins_data) 1070 | ops[opi]["ssg_mac_open"] = read_byte(ins_data) 1071 | 1072 | for opi in ops: 1073 | new_op = ops_types[opi]() 1074 | new_op.macros = [] 1075 | 1076 | am_mac = SingleMacro(kind=OpMacroCode.AM) 1077 | am_mac.open = bool(ops[opi]["am_mac_open"]) 1078 | am_mac.data.clear() 1079 | add_to_macro_data( 1080 | am_mac.data, 1081 | loop=ops[opi]["am_mac_loop"], 1082 | release=None, 1083 | data=[ 1084 | read_int(ins_data) for _ in range(ops[opi]["am_mac_len"]) 1085 | ], 1086 | ) 1087 | 1088 | ar_mac = SingleMacro(kind=OpMacroCode.AR) 1089 | ar_mac.open = bool(ops[opi]["ar_mac_open"]) 1090 | ar_mac.data.clear() 1091 | add_to_macro_data( 1092 | ar_mac.data, 1093 | loop=ops[opi]["ar_mac_loop"], 1094 | release=None, 1095 | data=[ 1096 | read_int(ins_data) for _ in range(ops[opi]["ar_mac_len"]) 1097 | ], 1098 | ) 1099 | 1100 | dr_mac = SingleMacro(kind=OpMacroCode.DR) 1101 | dr_mac.open = bool(ops[opi]["dr_mac_open"]) 1102 | dr_mac.data.clear() 1103 | add_to_macro_data( 1104 | dr_mac.data, 1105 | loop=ops[opi]["dr_mac_loop"], 1106 | release=None, 1107 | data=[ 1108 | read_int(ins_data) for _ in range(ops[opi]["dr_mac_len"]) 1109 | ], 1110 | ) 1111 | 1112 | mult_mac = SingleMacro(kind=OpMacroCode.MULT) 1113 | mult_mac.open = bool(ops[opi]["mult_mac_open"]) 1114 | mult_mac.data.clear() 1115 | add_to_macro_data( 1116 | mult_mac.data, 1117 | loop=ops[opi]["mult_mac_loop"], 1118 | release=None, 1119 | data=[ 1120 | read_int(ins_data) for _ in range(ops[opi]["mult_mac_len"]) 1121 | ], 1122 | ) 1123 | 1124 | rr_mac = SingleMacro(kind=OpMacroCode.RR) 1125 | rr_mac.open = bool(ops[opi]["rr_mac_open"]) 1126 | rr_mac.data.clear() 1127 | add_to_macro_data( 1128 | rr_mac.data, 1129 | loop=ops[opi]["rr_mac_loop"], 1130 | release=None, 1131 | data=[ 1132 | read_int(ins_data) for _ in range(ops[opi]["rr_mac_len"]) 1133 | ], 1134 | ) 1135 | 1136 | sl_mac = SingleMacro(kind=OpMacroCode.SL) 1137 | sl_mac.open = bool(ops[opi]["sl_mac_open"]) 1138 | sl_mac.data.clear() 1139 | add_to_macro_data( 1140 | sl_mac.data, 1141 | loop=ops[opi]["sl_mac_loop"], 1142 | release=None, 1143 | data=[ 1144 | read_int(ins_data) for _ in range(ops[opi]["sl_mac_len"]) 1145 | ], 1146 | ) 1147 | 1148 | tl_mac = SingleMacro(kind=OpMacroCode.TL) 1149 | tl_mac.open = bool(ops[opi]["tl_mac_open"]) 1150 | tl_mac.data.clear() 1151 | add_to_macro_data( 1152 | tl_mac.data, 1153 | loop=ops[opi]["tl_mac_loop"], 1154 | release=None, 1155 | data=[ 1156 | read_int(ins_data) for _ in range(ops[opi]["tl_mac_len"]) 1157 | ], 1158 | ) 1159 | 1160 | dt2_mac = SingleMacro(kind=OpMacroCode.DT2) 1161 | dt2_mac.open = bool(ops[opi]["dt2_mac_open"]) 1162 | dt2_mac.data.clear() 1163 | add_to_macro_data( 1164 | dt2_mac.data, 1165 | loop=ops[opi]["dt2_mac_loop"], 1166 | release=None, 1167 | data=[ 1168 | read_int(ins_data) for _ in range(ops[opi]["dt2_mac_len"]) 1169 | ], 1170 | ) 1171 | 1172 | rs_mac = SingleMacro(kind=OpMacroCode.RS) 1173 | rs_mac.open = bool(ops[opi]["rs_mac_open"]) 1174 | rs_mac.data.clear() 1175 | add_to_macro_data( 1176 | rs_mac.data, 1177 | loop=ops[opi]["rs_mac_loop"], 1178 | release=None, 1179 | data=[ 1180 | read_int(ins_data) for _ in range(ops[opi]["rs_mac_len"]) 1181 | ], 1182 | ) 1183 | 1184 | dt_mac = SingleMacro(kind=OpMacroCode.DT) 1185 | dt_mac.open = bool(ops[opi]["dt_mac_open"]) 1186 | dt_mac.data.clear() 1187 | add_to_macro_data( 1188 | dt_mac.data, 1189 | loop=ops[opi]["dt_mac_loop"], 1190 | release=None, 1191 | data=[ 1192 | read_int(ins_data) for _ in range(ops[opi]["dt_mac_len"]) 1193 | ], 1194 | ) 1195 | 1196 | d2r_mac = SingleMacro(kind=OpMacroCode.D2R) 1197 | d2r_mac.open = bool(ops[opi]["d2r_mac_open"]) 1198 | d2r_mac.data.clear() 1199 | add_to_macro_data( 1200 | d2r_mac.data, 1201 | loop=ops[opi]["d2r_mac_loop"], 1202 | release=None, 1203 | data=[ 1204 | read_int(ins_data) for _ in range(ops[opi]["d2r_mac_len"]) 1205 | ], 1206 | ) 1207 | 1208 | ssg_mac = SingleMacro(kind=OpMacroCode.SSG_EG) 1209 | ssg_mac.open = bool(ops[opi]["ssg_mac_open"]) 1210 | ssg_mac.data.clear() 1211 | add_to_macro_data( 1212 | ssg_mac.data, 1213 | loop=ops[opi]["ssg_mac_loop"], 1214 | release=None, 1215 | data=[ 1216 | read_int(ins_data) for _ in range(ops[opi]["ssg_mac_len"]) 1217 | ], 1218 | ) 1219 | 1220 | new_op.macros.extend( 1221 | [ 1222 | am_mac, 1223 | ar_mac, 1224 | dr_mac, 1225 | mult_mac, 1226 | rr_mac, 1227 | sl_mac, 1228 | tl_mac, 1229 | dt2_mac, 1230 | rs_mac, 1231 | dt_mac, 1232 | d2r_mac, 1233 | ssg_mac, 1234 | ] 1235 | ) # must be in order!! 1236 | 1237 | new_ops[opi] = new_op 1238 | 1239 | # release points 1240 | if True: 1241 | if self.meta.version >= 44: 1242 | add_to_macro_data(vol_mac.data, None, read_int(ins_data), None) 1243 | add_to_macro_data(arp_mac.data, None, read_int(ins_data), None) 1244 | add_to_macro_data(duty_mac.data, None, read_int(ins_data), None) 1245 | add_to_macro_data(wave_mac.data, None, read_int(ins_data), None) 1246 | add_to_macro_data(pitch_mac.data, None, read_int(ins_data), None) 1247 | add_to_macro_data(x1_mac.data, None, read_int(ins_data), None) 1248 | add_to_macro_data(x2_mac.data, None, read_int(ins_data), None) 1249 | add_to_macro_data(x3_mac.data, None, read_int(ins_data), None) 1250 | add_to_macro_data(alg_mac.data, None, read_int(ins_data), None) 1251 | add_to_macro_data(fb_mac.data, None, read_int(ins_data), None) 1252 | add_to_macro_data(fms_mac.data, None, read_int(ins_data), None) 1253 | add_to_macro_data(ams_mac.data, None, read_int(ins_data), None) 1254 | 1255 | for opi in new_ops: 1256 | for i in range(12): 1257 | add_to_macro_data( 1258 | new_ops[opi].macros[i].data, None, read_int(ins_data), None 1259 | ) 1260 | 1261 | # extended op macros 1262 | if True: 1263 | if self.meta.version >= 61: 1264 | for op in new_ops: 1265 | dam_mac = SingleMacro(kind=OpMacroCode.DAM) 1266 | dvb_mac = SingleMacro(kind=OpMacroCode.DVB) 1267 | egt_mac = SingleMacro(kind=OpMacroCode.EGT) 1268 | ksl_mac = SingleMacro(kind=OpMacroCode.KSL) 1269 | sus_mac = SingleMacro(kind=OpMacroCode.SUS) 1270 | vib_mac = SingleMacro(kind=OpMacroCode.VIB) 1271 | ws_mac = SingleMacro(kind=OpMacroCode.WS) 1272 | ksr_mac = SingleMacro(kind=OpMacroCode.KSR) 1273 | 1274 | dam_mac_len = read_int(ins_data) 1275 | dvb_mac_len = read_int(ins_data) 1276 | egt_mac_len = read_int(ins_data) 1277 | ksl_mac_len = read_int(ins_data) 1278 | sus_mac_len = read_int(ins_data) 1279 | vib_mac_len = read_int(ins_data) 1280 | ws_mac_len = read_int(ins_data) 1281 | ksr_mac_len = read_int(ins_data) 1282 | 1283 | dam_mac_loop = read_int(ins_data) 1284 | dvb_mac_loop = read_int(ins_data) 1285 | egt_mac_loop = read_int(ins_data) 1286 | ksl_mac_loop = read_int(ins_data) 1287 | sus_mac_loop = read_int(ins_data) 1288 | vib_mac_loop = read_int(ins_data) 1289 | ws_mac_loop = read_int(ins_data) 1290 | ksr_mac_loop = read_int(ins_data) 1291 | 1292 | dam_mac_rel = read_int(ins_data) 1293 | dvb_mac_rel = read_int(ins_data) 1294 | egt_mac_rel = read_int(ins_data) 1295 | ksl_mac_rel = read_int(ins_data) 1296 | sus_mac_rel = read_int(ins_data) 1297 | vib_mac_rel = read_int(ins_data) 1298 | ws_mac_rel = read_int(ins_data) 1299 | ksr_mac_rel = read_int(ins_data) 1300 | 1301 | dam_mac.open = bool(read_byte(ins_data)) 1302 | dvb_mac.open = bool(read_byte(ins_data)) 1303 | egt_mac.open = bool(read_byte(ins_data)) 1304 | ksl_mac.open = bool(read_byte(ins_data)) 1305 | sus_mac.open = bool(read_byte(ins_data)) 1306 | vib_mac.open = bool(read_byte(ins_data)) 1307 | ws_mac.open = bool(read_byte(ins_data)) 1308 | ksr_mac.open = bool(read_byte(ins_data)) 1309 | 1310 | dam_mac.data.clear() 1311 | dvb_mac.data.clear() 1312 | egt_mac.data.clear() 1313 | ksl_mac.data.clear() 1314 | sus_mac.data.clear() 1315 | vib_mac.data.clear() 1316 | ws_mac.data.clear() 1317 | ksr_mac.data.clear() 1318 | 1319 | add_to_macro_data( 1320 | dam_mac.data, 1321 | dam_mac_loop, 1322 | dam_mac_rel, 1323 | [read_byte(ins_data) for _ in range(dam_mac_len)], 1324 | ) 1325 | add_to_macro_data( 1326 | dvb_mac.data, 1327 | dvb_mac_loop, 1328 | dvb_mac_rel, 1329 | [read_byte(ins_data) for _ in range(dvb_mac_len)], 1330 | ) 1331 | add_to_macro_data( 1332 | egt_mac.data, 1333 | egt_mac_loop, 1334 | egt_mac_rel, 1335 | [read_byte(ins_data) for _ in range(egt_mac_len)], 1336 | ) 1337 | add_to_macro_data( 1338 | ksl_mac.data, 1339 | ksl_mac_loop, 1340 | ksl_mac_rel, 1341 | [read_byte(ins_data) for _ in range(ksl_mac_len)], 1342 | ) 1343 | add_to_macro_data( 1344 | sus_mac.data, 1345 | sus_mac_loop, 1346 | sus_mac_rel, 1347 | [read_byte(ins_data) for _ in range(sus_mac_len)], 1348 | ) 1349 | add_to_macro_data( 1350 | vib_mac.data, 1351 | vib_mac_loop, 1352 | vib_mac_rel, 1353 | [read_byte(ins_data) for _ in range(vib_mac_len)], 1354 | ) 1355 | add_to_macro_data( 1356 | ws_mac.data, 1357 | ws_mac_loop, 1358 | ws_mac_rel, 1359 | [read_byte(ins_data) for _ in range(ws_mac_len)], 1360 | ) 1361 | add_to_macro_data( 1362 | ksr_mac.data, 1363 | ksr_mac_loop, 1364 | ksr_mac_rel, 1365 | [read_byte(ins_data) for _ in range(ksr_mac_len)], 1366 | ) 1367 | 1368 | new_ops[op].macros.extend( 1369 | [ 1370 | dam_mac, 1371 | dvb_mac, 1372 | egt_mac, 1373 | ksl_mac, 1374 | sus_mac, 1375 | vib_mac, 1376 | ws_mac, 1377 | ksr_mac, 1378 | ] 1379 | ) 1380 | 1381 | # opl drum data 1382 | if True: 1383 | if self.meta.version >= 63: 1384 | opl_drum = InsFeatureOPLDrums(fixed_drums=bool(read_byte(ins_data))) 1385 | read_byte(ins_data) 1386 | opl_drum.kick_freq = read_short(ins_data) 1387 | opl_drum.snare_hat_freq = read_short(ins_data) 1388 | opl_drum.tom_top_freq = read_short(ins_data) 1389 | self.features.append(opl_drum) 1390 | 1391 | # clear macros 1392 | if True: 1393 | if self.meta.version < 63 and self.meta.type == InstrumentType.PCE: 1394 | duty_mac.data.clear() 1395 | if self.meta.version < 70 and self.meta.type == InstrumentType.FM_OPLL: 1396 | wave_mac.data.clear() 1397 | 1398 | # sample map 1399 | if True: 1400 | if self.meta.version >= 67: 1401 | note_map = InsFeatureAmiga() 1402 | note_map.use_note_map = bool(read_byte(ins_data)) 1403 | if note_map.use_note_map: 1404 | for i in range(len(note_map.sample_map)): 1405 | note_map.sample_map[i].freq = read_int(ins_data) 1406 | for i in range(len(note_map.sample_map)): 1407 | note_map.sample_map[i].sample_index = read_short(ins_data) 1408 | self.features.append(note_map) 1409 | 1410 | # n163 1411 | if True: 1412 | if self.meta.version >= 73: 1413 | n163 = InsFeatureN163( 1414 | wave=read_int(ins_data), 1415 | wave_pos=read_byte(ins_data), 1416 | wave_len=read_byte(ins_data), 1417 | wave_mode=read_byte(ins_data), 1418 | ) 1419 | read_byte(ins_data) # reserved 1420 | self.features.append(n163) 1421 | 1422 | # moar macroes 1423 | if True: 1424 | if self.meta.version >= 76: 1425 | pan_l_mac = SingleMacro(kind=MacroCode.PAN_L) 1426 | pan_r_mac = SingleMacro(kind=MacroCode.PAN_R) 1427 | phase_res_mac = SingleMacro(kind=MacroCode.PHASE_RESET) 1428 | x4_mac = SingleMacro(kind=MacroCode.EX4) 1429 | x5_mac = SingleMacro(kind=MacroCode.EX5) 1430 | x6_mac = SingleMacro(kind=MacroCode.EX6) 1431 | x7_mac = SingleMacro(kind=MacroCode.EX7) 1432 | x8_mac = SingleMacro(kind=MacroCode.EX8) 1433 | 1434 | pan_l_mac.data.clear() 1435 | pan_r_mac.data.clear() 1436 | phase_res_mac.data.clear() 1437 | x4_mac.data.clear() 1438 | x5_mac.data.clear() 1439 | x6_mac.data.clear() 1440 | x7_mac.data.clear() 1441 | x8_mac.data.clear() 1442 | 1443 | pan_l_mac_len = read_int(ins_data) 1444 | pan_r_mac_len = read_int(ins_data) 1445 | phase_res_mac_len = read_int(ins_data) 1446 | x4_mac_len = read_int(ins_data) 1447 | x5_mac_len = read_int(ins_data) 1448 | x6_mac_len = read_int(ins_data) 1449 | x7_mac_len = read_int(ins_data) 1450 | x8_mac_len = read_int(ins_data) 1451 | 1452 | pan_l_mac_loop = read_int(ins_data) 1453 | pan_r_mac_loop = read_int(ins_data) 1454 | phase_res_mac_loop = read_int(ins_data) 1455 | x4_mac_loop = read_int(ins_data) 1456 | x5_mac_loop = read_int(ins_data) 1457 | x6_mac_loop = read_int(ins_data) 1458 | x7_mac_loop = read_int(ins_data) 1459 | x8_mac_loop = read_int(ins_data) 1460 | 1461 | pan_l_mac_rel = read_int(ins_data) 1462 | pan_r_mac_rel = read_int(ins_data) 1463 | phase_res_mac_rel = read_int(ins_data) 1464 | x4_mac_rel = read_int(ins_data) 1465 | x5_mac_rel = read_int(ins_data) 1466 | x6_mac_rel = read_int(ins_data) 1467 | x7_mac_rel = read_int(ins_data) 1468 | x8_mac_rel = read_int(ins_data) 1469 | 1470 | pan_l_mac.open = bool(read_byte(ins_data)) 1471 | pan_r_mac.open = bool(read_byte(ins_data)) 1472 | phase_res_mac.open = bool(read_byte(ins_data)) 1473 | x4_mac.open = bool(read_byte(ins_data)) 1474 | x5_mac.open = bool(read_byte(ins_data)) 1475 | x6_mac.open = bool(read_byte(ins_data)) 1476 | x7_mac.open = bool(read_byte(ins_data)) 1477 | x8_mac.open = bool(read_byte(ins_data)) 1478 | 1479 | add_to_macro_data( 1480 | pan_l_mac.data, 1481 | pan_l_mac_loop, 1482 | pan_l_mac_rel, 1483 | [read_int(ins_data) for _ in range(pan_l_mac_len)], 1484 | ) 1485 | add_to_macro_data( 1486 | pan_r_mac.data, 1487 | pan_r_mac_loop, 1488 | pan_r_mac_rel, 1489 | [read_int(ins_data) for _ in range(pan_r_mac_len)], 1490 | ) 1491 | add_to_macro_data( 1492 | phase_res_mac.data, 1493 | phase_res_mac_loop, 1494 | phase_res_mac_rel, 1495 | [read_int(ins_data) for _ in range(phase_res_mac_len)], 1496 | ) 1497 | add_to_macro_data( 1498 | x4_mac.data, 1499 | x4_mac_loop, 1500 | x4_mac_rel, 1501 | [read_int(ins_data) for _ in range(x4_mac_len)], 1502 | ) 1503 | add_to_macro_data( 1504 | x5_mac.data, 1505 | x5_mac_loop, 1506 | x5_mac_rel, 1507 | [read_int(ins_data) for _ in range(x5_mac_len)], 1508 | ) 1509 | add_to_macro_data( 1510 | x6_mac.data, 1511 | x6_mac_loop, 1512 | x6_mac_rel, 1513 | [read_int(ins_data) for _ in range(x6_mac_len)], 1514 | ) 1515 | add_to_macro_data( 1516 | x7_mac.data, 1517 | x7_mac_loop, 1518 | x7_mac_rel, 1519 | [read_int(ins_data) for _ in range(x7_mac_len)], 1520 | ) 1521 | add_to_macro_data( 1522 | x8_mac.data, 1523 | x8_mac_loop, 1524 | x8_mac_rel, 1525 | [read_int(ins_data) for _ in range(x8_mac_len)], 1526 | ) 1527 | 1528 | mac_list.extend( 1529 | [ 1530 | pan_l_mac, 1531 | pan_r_mac, 1532 | phase_res_mac, 1533 | x4_mac, 1534 | x5_mac, 1535 | x6_mac, 1536 | x7_mac, 1537 | x8_mac, 1538 | ] 1539 | ) 1540 | 1541 | # fds 1542 | if True: 1543 | if self.meta.version >= 76: 1544 | fds = InsFeatureFDS( 1545 | mod_speed=read_int(ins_data), 1546 | mod_depth=read_int(ins_data), 1547 | init_table_with_first_wave=bool(read_byte(ins_data)), 1548 | ) 1549 | read_byte(ins_data) # reserved 1550 | read_byte(ins_data) 1551 | read_byte(ins_data) 1552 | fds.mod_table = [read_byte(ins_data) for _ in range(32)] 1553 | self.features.append(fds) 1554 | 1555 | # opz 1556 | if True: 1557 | if self.meta.version >= 77: 1558 | fm.fms2 = read_byte(ins_data) 1559 | fm.ams2 = read_byte(ins_data) 1560 | 1561 | # wave synth 1562 | if True: 1563 | if self.meta.version >= 79: 1564 | ws = InsFeatureWaveSynth( 1565 | wave_indices=[read_int(ins_data), read_int(ins_data)], 1566 | rate_divider=read_byte(ins_data), 1567 | effect=WaveFX(read_byte(ins_data)), 1568 | enabled=bool(read_byte(ins_data)), 1569 | global_effect=bool(read_byte(ins_data)), 1570 | speed=read_byte(ins_data), 1571 | params=[read_byte(ins_data) for _ in range(4)], 1572 | ) 1573 | self.features.append(ws) 1574 | 1575 | # macro moads 1576 | if True: 1577 | if self.meta.version >= 84: 1578 | vol_mac.mode = read_byte(ins_data) 1579 | duty_mac.mode = read_byte(ins_data) 1580 | wave_mac.mode = read_byte(ins_data) 1581 | pitch_mac.mode = read_byte(ins_data) 1582 | x1_mac.mode = read_byte(ins_data) 1583 | x2_mac.mode = read_byte(ins_data) 1584 | x3_mac.mode = read_byte(ins_data) 1585 | alg_mac.mode = read_byte(ins_data) 1586 | fb_mac.mode = read_byte(ins_data) 1587 | fms_mac.mode = read_byte(ins_data) 1588 | ams_mac.mode = read_byte(ins_data) 1589 | pan_l_mac.mode = read_byte(ins_data) 1590 | pan_r_mac.mode = read_byte(ins_data) 1591 | phase_res_mac.mode = read_byte(ins_data) 1592 | x4_mac.mode = read_byte(ins_data) 1593 | x5_mac.mode = read_byte(ins_data) 1594 | x6_mac.mode = read_byte(ins_data) 1595 | x7_mac.mode = read_byte(ins_data) 1596 | x8_mac.mode = read_byte(ins_data) 1597 | 1598 | # c64 no test 1599 | if True: 1600 | if self.meta.version >= 89: 1601 | c64.no_test = bool(read_byte(ins_data)) 1602 | 1603 | # multipcm 1604 | if True: 1605 | if self.meta.version >= 93: 1606 | mp = InsFeatureMultiPCM( 1607 | ar=read_byte(ins_data), 1608 | d1r=read_byte(ins_data), 1609 | dl=read_byte(ins_data), 1610 | d2r=read_byte(ins_data), 1611 | rr=read_byte(ins_data), 1612 | rc=read_byte(ins_data), 1613 | lfo=read_byte(ins_data), 1614 | vib=read_byte(ins_data), 1615 | am=read_byte(ins_data), 1616 | ) 1617 | for _ in range(23): # reserved 1618 | read_byte(ins_data) 1619 | self.features.append(mp) 1620 | 1621 | # sound unit 1622 | if True: 1623 | if self.meta.version >= 104: 1624 | amiga.use_sample = bool(read_byte(ins_data)) 1625 | su = InsFeatureSoundUnit(switch_roles=bool(read_byte(ins_data))) 1626 | self.features.append(su) 1627 | 1628 | # gb hw seq 1629 | if True: 1630 | if self.meta.version >= 105: 1631 | gb_hwseq_len = read_byte(ins_data) 1632 | gb.hw_seq.clear() 1633 | for i in range(gb_hwseq_len): 1634 | gb.hw_seq.append( 1635 | GBHwSeq( 1636 | command=GBHwCommand(read_byte(ins_data)), 1637 | data=[read_byte(ins_data), read_byte(ins_data)], 1638 | ) 1639 | ) 1640 | 1641 | # additional gb 1642 | if True: 1643 | if self.meta.version >= 106: 1644 | gb.soft_env = bool(read_byte(ins_data)) 1645 | gb.always_init = bool(read_byte(ins_data)) 1646 | 1647 | # es5506 1648 | if True: 1649 | if self.meta.version >= 107: 1650 | es = InsFeatureES5506( 1651 | filter_mode=ESFilterMode(read_byte(ins_data)), 1652 | k1=read_short(ins_data), 1653 | k2=read_short(ins_data), 1654 | env_count=read_short(ins_data), 1655 | left_volume_ramp=read_byte(ins_data), 1656 | right_volume_ramp=read_byte(ins_data), 1657 | k1_ramp=read_byte(ins_data), 1658 | k2_ramp=read_byte(ins_data), 1659 | k1_slow=read_byte(ins_data), 1660 | k2_slow=read_byte(ins_data), 1661 | ) 1662 | self.features.append(es) 1663 | 1664 | # snes 1665 | if True: 1666 | if self.meta.version >= 109: 1667 | snes = InsFeatureSNES() 1668 | snes.use_env = bool(read_byte(ins_data)) 1669 | if self.meta.version >= 118: 1670 | snes.gain_mode = GainMode(read_byte(ins_data)) 1671 | snes.gain = read_byte(ins_data) 1672 | else: 1673 | read_byte(ins_data) 1674 | read_byte(ins_data) 1675 | snes.envelope.a = read_byte(ins_data) 1676 | snes.envelope.d = read_byte(ins_data) 1677 | snes_env_s = read_byte(ins_data) 1678 | snes.envelope.s = snes_env_s & 0b111 1679 | snes.envelope.r = read_byte(ins_data) 1680 | snes.sus = SNESSusMode((snes_env_s >> 3) & 1) # ??? 1681 | self.features.append(snes) 1682 | 1683 | # macro speed delay 1684 | if True: 1685 | if self.meta.version >= 111: 1686 | vol_mac.speed = read_byte(ins_data) 1687 | arp_mac.speed = read_byte(ins_data) 1688 | duty_mac.speed = read_byte(ins_data) 1689 | wave_mac.speed = read_byte(ins_data) 1690 | pitch_mac.speed = read_byte(ins_data) 1691 | x1_mac.speed = read_byte(ins_data) 1692 | x2_mac.speed = read_byte(ins_data) 1693 | x3_mac.speed = read_byte(ins_data) 1694 | alg_mac.speed = read_byte(ins_data) 1695 | fb_mac.speed = read_byte(ins_data) 1696 | fms_mac.speed = read_byte(ins_data) 1697 | ams_mac.speed = read_byte(ins_data) 1698 | pan_l_mac.speed = read_byte(ins_data) 1699 | pan_r_mac.speed = read_byte(ins_data) 1700 | phase_res_mac.speed = read_byte(ins_data) 1701 | x4_mac.speed = read_byte(ins_data) 1702 | x5_mac.speed = read_byte(ins_data) 1703 | x6_mac.speed = read_byte(ins_data) 1704 | x7_mac.speed = read_byte(ins_data) 1705 | x8_mac.speed = read_byte(ins_data) 1706 | 1707 | vol_mac.delay = read_byte(ins_data) 1708 | arp_mac.delay = read_byte(ins_data) 1709 | duty_mac.delay = read_byte(ins_data) 1710 | wave_mac.delay = read_byte(ins_data) 1711 | pitch_mac.delay = read_byte(ins_data) 1712 | x1_mac.delay = read_byte(ins_data) 1713 | x2_mac.delay = read_byte(ins_data) 1714 | x3_mac.delay = read_byte(ins_data) 1715 | alg_mac.delay = read_byte(ins_data) 1716 | fb_mac.delay = read_byte(ins_data) 1717 | fms_mac.delay = read_byte(ins_data) 1718 | ams_mac.delay = read_byte(ins_data) 1719 | pan_l_mac.delay = read_byte(ins_data) 1720 | pan_r_mac.delay = read_byte(ins_data) 1721 | phase_res_mac.delay = read_byte(ins_data) 1722 | x4_mac.delay = read_byte(ins_data) 1723 | x5_mac.delay = read_byte(ins_data) 1724 | x6_mac.delay = read_byte(ins_data) 1725 | x7_mac.delay = read_byte(ins_data) 1726 | x8_mac.delay = read_byte(ins_data) 1727 | 1728 | for op in ops: 1729 | for i in range(20): 1730 | new_ops[op].macros[i].speed = read_byte(ins_data) 1731 | for i in range(20): 1732 | new_ops[op].macros[i].delay = read_byte(ins_data) 1733 | 1734 | # old arp mac format 1735 | if True: 1736 | if self.meta.version < 112: 1737 | if arp_mac.mode != 0: 1738 | arp_mac.mode = 0 1739 | for i in range(len(arp_mac.data)): 1740 | if isinstance(arp_mac.data[i], int): 1741 | arp_mac.data[i] = cast(int, arp_mac.data[i]) ^ 0x40000000 1742 | 1743 | # add ops macros at the end 1744 | if True: 1745 | if self.meta.version >= 29: 1746 | for _, op_contents in new_ops.items(): 1747 | self.features.append(op_contents) 1748 | -------------------------------------------------------------------------------- /chipchune/furnace/module.py: -------------------------------------------------------------------------------- 1 | import re 2 | import zlib 3 | from io import BytesIO, BufferedReader 4 | from typing import BinaryIO, Optional, Literal, Union, Dict, List, Callable 5 | 6 | from chipchune._util import read_byte, read_short, read_int, read_float, read_str 7 | from .data_types import ( 8 | ModuleMeta, 9 | ChipList, 10 | ModuleCompatFlags, 11 | SubSong, 12 | PatchBay, 13 | ChannelDisplayInfo, 14 | InputPatchBayEntry, 15 | OutputPatchBayEntry, 16 | ChipInfo, 17 | FurnacePattern, 18 | FurnaceRow, 19 | ) 20 | from .enums import ( 21 | ChipType, 22 | LinearPitch, 23 | InputPortSet, 24 | OutputPortSet, 25 | LoopModality, 26 | DelayBehavior, 27 | JumpTreatment, 28 | _FurInsImportType, 29 | _FurWavetableImportType, 30 | Note, 31 | ) 32 | from .instrument import FurnaceInstrument 33 | from .wavetable import FurnaceWavetable 34 | from .sample import FurnaceSample 35 | 36 | MAGIC_STR = b"-Furnace module-" 37 | MAX_CHIPS = 32 38 | 39 | 40 | class FurnaceModule: 41 | """ 42 | Represents a Furnace .fur file. 43 | 44 | When possible, instrument objects etc. will use the latest format as its internal 45 | representation. For example, old instruments will internally be converted into the 46 | "new" instrument-feature-list format. 47 | """ 48 | 49 | def __init__( 50 | self, file_name_or_stream: Optional[Union[BufferedReader, str]] = None 51 | ) -> None: 52 | """ 53 | Creates or opens a new Furnace module as a Python object. 54 | 55 | :param file_name_or_stream: (Optional) 56 | If specified, then it will parse a file as a FurnaceModule. If file name (str) is 57 | given, it will load that file. If a stream (BufferedReader) instead is given, 58 | it will parse it from the stream. 59 | 60 | Defaults to None. 61 | """ 62 | self.file_name: Optional[str] = None 63 | """ 64 | Original file name, if the object was initialized with one. 65 | """ 66 | self.meta: ModuleMeta = ModuleMeta() 67 | """ 68 | Metadata concerning the module. 69 | """ 70 | self.chips: ChipList = ChipList() 71 | """ 72 | List of chips used in the module. 73 | """ 74 | self.compat_flags: ModuleCompatFlags = ModuleCompatFlags() 75 | """ 76 | Compat flags settings within the module. 77 | """ 78 | self.subsongs: List[SubSong] = [SubSong()] 79 | """ 80 | Subsongs contained within the module. Although the first subsong 81 | and the others are internally stored separately, they're organized 82 | into a list here for convenience. 83 | """ 84 | self.patchbay: List[PatchBay] = [] 85 | """ 86 | List of patchbay connections. 87 | """ 88 | self.instruments: List[FurnaceInstrument] = [] 89 | """ 90 | List of all instruments in the module. 91 | """ 92 | self.patterns: List[FurnacePattern] = [] 93 | """ 94 | List of all patterns in the module. 95 | """ 96 | self.wavetables: List[FurnaceWavetable] = [] 97 | 98 | self.samples: List[FurnaceWavetable] = [] 99 | 100 | if isinstance(file_name_or_stream, BufferedReader): 101 | self.load_from_stream(file_name_or_stream) 102 | elif isinstance(file_name_or_stream, str): 103 | self.load_from_file(file_name_or_stream) 104 | 105 | def load_from_file(self, file_name: Optional[str] = None) -> None: 106 | """ 107 | Load a module from a file name. The file may either be compressed or uncompressed. 108 | 109 | :param file_name: If not specified, it will grab from self.file_name instead. 110 | """ 111 | if isinstance(file_name, str): 112 | self.file_name = file_name 113 | if self.file_name is None: 114 | raise RuntimeError( 115 | "No file name set, either set self.file_name or pass file_name to the function" 116 | ) 117 | with open(self.file_name, "rb") as f: 118 | detect_magic = f.peek(len(MAGIC_STR))[: len(MAGIC_STR)] 119 | if ( 120 | detect_magic != MAGIC_STR 121 | ): # this is probably compressed, so try decompressing it first 122 | return self.load_from_bytes(zlib.decompress(f.read())) 123 | else: # uncompressed for sure 124 | return self.load_from_stream(f) 125 | 126 | @staticmethod 127 | def decompress_to_file(in_name: str, out_name: str) -> int: 128 | """ 129 | Simple zlib wrapper. Decompresses a zlib-compressed .fur 130 | from in_name to out_name. Does not need instantiation. 131 | 132 | :param in_name: input file name 133 | :param out_name: output file name 134 | :return: Results of file.write(). 135 | """ 136 | with open(in_name, "rb") as fi: 137 | with open(out_name, "wb") as fo: 138 | return fo.write(zlib.decompress(fi.read())) 139 | 140 | def load_from_bytes(self, data: bytes) -> None: 141 | """ 142 | Load a module from a series of bytes. 143 | 144 | :param data: Bytes 145 | """ 146 | return self.load_from_stream(BytesIO(data)) 147 | 148 | def load_from_stream(self, stream: BinaryIO) -> None: 149 | """ 150 | Load a module from an **uncompressed** stream. 151 | 152 | :param stream: File-like object containing the uncompressed module. 153 | """ 154 | # assumes uncompressed stream 155 | if stream.read(len(MAGIC_STR)) != MAGIC_STR: 156 | raise RuntimeError( 157 | "Bad magic value; this is not a Furnace file or is corrupt" 158 | ) 159 | 160 | # clear defaults 161 | self.chips.list.clear() 162 | self.patchbay.clear() 163 | self.subsongs[0].order.clear() 164 | self.subsongs[0].speed_pattern.clear() 165 | 166 | self.__read_header(stream) 167 | self.__init_compat_flags() 168 | self.__read_info(stream) 169 | if self.meta.version >= 119: 170 | self.__read_dev119_chip_flags(stream) 171 | self.__read_instruments(stream) 172 | self.__read_wavetables(stream) 173 | self.__read_samples(stream) 174 | if self.meta.version >= 95: 175 | self.__read_subsongs(stream) 176 | self.__read_patterns(stream) 177 | 178 | def get_num_channels(self) -> int: 179 | """ 180 | Retrieve the number of total channels in the module. 181 | 182 | :return: Channel sum across all chips. 183 | """ 184 | num_channels = 0 185 | for chip in self.chips.list: 186 | num_channels += chip.type.channels 187 | return num_channels 188 | 189 | def get_pattern( 190 | self, channel: int, index: int, subsong: int = 0 191 | ) -> Optional[FurnacePattern]: 192 | """ 193 | Gets one pattern object from a module. 194 | 195 | :param channel: Which channel to use (zero-indexed), e.g. to get VRC6 196 | in a NES+VRC6 module, use `5`. 197 | :param index: The index of the pattern within the subsong. 198 | :param subsong: The subsong number. 199 | :return: FurnacePattern object or None if no such pattern exists. 200 | """ 201 | try: 202 | return next( 203 | filter( 204 | lambda x: x.channel == channel 205 | and x.index == index 206 | and x.subsong == subsong, 207 | self.patterns, 208 | ) 209 | ) 210 | except StopIteration: 211 | return None 212 | 213 | def __init_compat_flags(self) -> None: 214 | """ 215 | Initializes appropriate compat flags based on module version 216 | """ 217 | if self.meta.version < 37: 218 | self.compat_flags.limit_slides = True 219 | self.compat_flags.linear_pitch = LinearPitch.ONLY_PITCH_CHANGE 220 | self.compat_flags.loop_modality = LoopModality.HARD_RESET_CHANNELS 221 | if self.meta.version < 43: 222 | self.compat_flags.proper_noise_layout = False 223 | self.compat_flags.wave_duty_is_volume = False 224 | if self.meta.version < 45: 225 | self.compat_flags.reset_macro_on_porta = True 226 | self.compat_flags.legacy_volume_slides = True 227 | self.compat_flags.compatible_arpeggio = True 228 | self.compat_flags.note_off_resets_slides = True 229 | self.compat_flags.target_resets_slides = True 230 | if self.meta.version < 46: 231 | self.compat_flags.arpeggio_inhibits_portamento = True 232 | self.compat_flags.wack_algorithm_macro = True 233 | if self.meta.version < 49: 234 | self.compat_flags.broken_shortcut_slides = True 235 | if self.meta.version < 50: 236 | self.compat_flags.ignore_duplicates_slides = False 237 | if self.meta.version < 62: 238 | self.compat_flags.stop_portamento_on_note_off = True 239 | if self.meta.version < 64: 240 | self.compat_flags.broken_dac_mode = False 241 | if self.meta.version < 65: 242 | self.compat_flags.one_tick_cut = False 243 | if self.meta.version < 66: 244 | self.compat_flags.instrument_change_allowed_in_porta = False 245 | if self.meta.version < 69: 246 | self.compat_flags.reset_note_base_on_arpeggio_stop = False 247 | if self.meta.version < 71: 248 | self.compat_flags.no_slides_on_first_tick = False 249 | self.compat_flags.next_row_reset_arp_pos = False 250 | self.compat_flags.ignore_jump_at_end = True 251 | if self.meta.version < 72: 252 | self.compat_flags.buggy_portamento_after_slide = True 253 | self.compat_flags.gb_ins_affects_env = False 254 | if self.meta.version < 78: 255 | self.compat_flags.shared_extch_state = False 256 | if self.meta.version < 83: 257 | self.compat_flags.ignore_outside_dac_mode_change = True 258 | self.compat_flags.e1e2_takes_priority = False 259 | if self.meta.version < 84: 260 | self.compat_flags.new_sega_pcm = False 261 | if self.meta.version < 85: 262 | self.compat_flags.weird_fnum_pitch_slides = True 263 | if self.meta.version < 86: 264 | self.compat_flags.sn_duty_resets_phase = True 265 | if self.meta.version < 90: 266 | self.compat_flags.linear_pitch_macro = False 267 | if self.meta.version < 97: 268 | self.compat_flags.old_octave_boundary = True 269 | self.compat_flags.disable_opn2_dac_volume_control = True # dev98 270 | if self.meta.version < 99: 271 | self.compat_flags.new_volume_scaling = False 272 | self.compat_flags.volume_macro_lingers = False 273 | self.compat_flags.broken_out_vol = True 274 | if self.meta.version < 100: 275 | self.compat_flags.e1e2_stop_on_same_note = False 276 | if self.meta.version < 101: 277 | self.compat_flags.broken_porta_after_arp = True 278 | if self.meta.version < 108: 279 | self.compat_flags.sn_no_low_periods = True 280 | if self.meta.version < 110: 281 | self.compat_flags.cut_delay_effect_policy = DelayBehavior.BROKEN 282 | if self.meta.version < 113: 283 | self.compat_flags.jump_treatment = JumpTreatment.FIRST_JUMP_ONLY 284 | if self.meta.version < 115: 285 | self.compat_flags.auto_sys_name = True 286 | if self.meta.version < 117: 287 | self.compat_flags.disable_sample_macro = True 288 | if self.meta.version < 121: 289 | self.compat_flags.broken_out_vol_2 = False 290 | if self.meta.version < 130: 291 | self.compat_flags.old_arp_strategy = True 292 | if self.meta.version < 138: 293 | self.compat_flags.broken_porta_during_legato = True 294 | if self.meta.version < 155: 295 | self.compat_flags.broken_fm_off = True 296 | if self.meta.version < 168: 297 | self.compat_flags.pre_note_no_effect = True 298 | if self.meta.version < 183: 299 | self.compat_flags.old_dpcm = True 300 | if self.meta.version < 184: 301 | self.compat_flags.reset_arp_phase_on_new_note = False 302 | if self.meta.version < 188: 303 | self.compat_flags.ceil_volume_scaling = False 304 | if self.meta.version < 191: 305 | self.compat_flags.old_always_set_volume = True 306 | if self.meta.version < 200: 307 | self.compat_flags.old_sample_offset = True 308 | 309 | # XXX: update my signature whenever a new compat flag block is added 310 | def __read_compat_flags(self, stream: BinaryIO, phase: Literal[1, 2, 3]) -> None: 311 | """ 312 | Reads the set compat flags in the module 313 | """ 314 | if phase == 1: 315 | compat_flags_to_skip = 20 316 | if self.meta.version < 37: 317 | self.compat_flags.limit_slides = True 318 | self.compat_flags.linear_pitch = LinearPitch.ONLY_PITCH_CHANGE 319 | self.compat_flags.loop_modality = LoopModality.HARD_RESET_CHANNELS 320 | else: # >= 37 321 | self.compat_flags.limit_slides = bool(read_byte(stream)) 322 | self.compat_flags.linear_pitch = LinearPitch(read_byte(stream)) 323 | self.compat_flags.loop_modality = LoopModality(read_byte(stream)) 324 | compat_flags_to_skip -= 3 325 | 326 | if self.meta.version >= 43: 327 | self.compat_flags.proper_noise_layout = bool(read_byte(stream)) 328 | self.compat_flags.wave_duty_is_volume = bool(read_byte(stream)) 329 | compat_flags_to_skip -= 2 330 | 331 | if self.meta.version >= 45: 332 | self.compat_flags.reset_macro_on_porta = bool(read_byte(stream)) 333 | self.compat_flags.legacy_volume_slides = bool(read_byte(stream)) 334 | self.compat_flags.compatible_arpeggio = bool(read_byte(stream)) 335 | self.compat_flags.note_off_resets_slides = bool(read_byte(stream)) 336 | self.compat_flags.target_resets_slides = bool(read_byte(stream)) 337 | compat_flags_to_skip -= 5 338 | 339 | if self.meta.version >= 47: 340 | self.compat_flags.arpeggio_inhibits_portamento = bool( 341 | read_byte(stream) 342 | ) 343 | self.compat_flags.wack_algorithm_macro = bool(read_byte(stream)) 344 | compat_flags_to_skip -= 2 345 | 346 | if self.meta.version >= 49: 347 | self.compat_flags.broken_shortcut_slides = bool(read_byte(stream)) 348 | compat_flags_to_skip -= 1 349 | 350 | if self.meta.version >= 50: 351 | self.compat_flags.ignore_duplicates_slides = bool(read_byte(stream)) 352 | compat_flags_to_skip -= 1 353 | 354 | if self.meta.version >= 62: 355 | self.compat_flags.stop_portamento_on_note_off = bool( 356 | read_byte(stream) 357 | ) 358 | self.compat_flags.continuous_vibrato = bool(read_byte(stream)) 359 | compat_flags_to_skip -= 2 360 | 361 | if self.meta.version >= 64: 362 | self.compat_flags.broken_dac_mode = bool(read_byte(stream)) 363 | compat_flags_to_skip -= 1 364 | 365 | if self.meta.version >= 65: 366 | self.compat_flags.one_tick_cut = bool(read_byte(stream)) 367 | compat_flags_to_skip -= 1 368 | 369 | if self.meta.version >= 66: 370 | self.compat_flags.instrument_change_allowed_in_porta = bool( 371 | read_byte(stream) 372 | ) 373 | compat_flags_to_skip -= 1 374 | 375 | if self.meta.version >= 69: 376 | self.compat_flags.reset_note_base_on_arpeggio_stop = bool( 377 | read_byte(stream) 378 | ) 379 | compat_flags_to_skip -= 1 380 | elif phase == 2: 381 | compat_flags_to_skip = 28 382 | if self.meta.version >= 70: 383 | self.compat_flags.broken_speed_selection = bool(read_byte(stream)) 384 | compat_flags_to_skip -= 1 385 | 386 | if self.meta.version >= 71: 387 | self.compat_flags.no_slides_on_first_tick = bool(read_byte(stream)) 388 | self.compat_flags.next_row_reset_arp_pos = bool(read_byte(stream)) 389 | self.compat_flags.ignore_jump_at_end = bool(read_byte(stream)) 390 | compat_flags_to_skip -= 3 391 | 392 | if self.meta.version >= 72: 393 | self.compat_flags.buggy_portamento_after_slide = bool( 394 | read_byte(stream) 395 | ) 396 | self.compat_flags.gb_ins_affects_env = bool(read_byte(stream)) 397 | compat_flags_to_skip -= 2 398 | 399 | if self.meta.version >= 78: 400 | self.compat_flags.shared_extch_state = bool(read_byte(stream)) 401 | compat_flags_to_skip -= 1 402 | 403 | if self.meta.version >= 83: 404 | self.compat_flags.ignore_outside_dac_mode_change = bool( 405 | read_byte(stream) 406 | ) 407 | self.compat_flags.e1e2_takes_priority = bool(read_byte(stream)) 408 | compat_flags_to_skip -= 2 409 | 410 | if self.meta.version >= 84: 411 | self.compat_flags.new_sega_pcm = bool(read_byte(stream)) 412 | compat_flags_to_skip -= 1 413 | 414 | if self.meta.version >= 85: 415 | self.compat_flags.weird_fnum_pitch_slides = bool(read_byte(stream)) 416 | compat_flags_to_skip -= 1 417 | 418 | if self.meta.version >= 86: 419 | self.compat_flags.sn_duty_resets_phase = bool(read_byte(stream)) 420 | compat_flags_to_skip -= 1 421 | 422 | if self.meta.version >= 90: 423 | self.compat_flags.linear_pitch_macro = bool(read_byte(stream)) 424 | compat_flags_to_skip -= 1 425 | 426 | if self.meta.version >= 94: 427 | self.compat_flags.pitch_slide_speed_in_linear = read_byte(stream) 428 | compat_flags_to_skip -= 1 429 | 430 | if self.meta.version >= 97: 431 | self.compat_flags.old_octave_boundary = bool(read_byte(stream)) 432 | compat_flags_to_skip -= 1 433 | 434 | if self.meta.version >= 98: 435 | self.compat_flags.disable_opn2_dac_volume_control = bool( 436 | read_byte(stream) 437 | ) 438 | compat_flags_to_skip -= 1 439 | 440 | if self.meta.version >= 99: 441 | self.compat_flags.new_volume_scaling = bool(read_byte(stream)) 442 | self.compat_flags.volume_macro_lingers = bool(read_byte(stream)) 443 | self.compat_flags.broken_out_vol = bool(read_byte(stream)) 444 | compat_flags_to_skip -= 3 445 | 446 | if self.meta.version >= 100: 447 | self.compat_flags.e1e2_stop_on_same_note = bool(read_byte(stream)) 448 | compat_flags_to_skip -= 1 449 | 450 | if self.meta.version >= 101: 451 | self.compat_flags.broken_porta_after_arp = bool(read_byte(stream)) 452 | compat_flags_to_skip -= 1 453 | 454 | if self.meta.version >= 108: 455 | self.compat_flags.sn_no_low_periods = bool(read_byte(stream)) 456 | compat_flags_to_skip -= 1 457 | 458 | if self.meta.version >= 110: 459 | self.compat_flags.cut_delay_effect_policy = DelayBehavior( 460 | read_byte(stream) 461 | ) 462 | compat_flags_to_skip -= 1 463 | 464 | if self.meta.version >= 113: 465 | self.compat_flags.jump_treatment = JumpTreatment(read_byte(stream)) 466 | compat_flags_to_skip -= 1 467 | 468 | if self.meta.version >= 115: 469 | self.compat_flags.auto_sys_name = bool(read_byte(stream)) 470 | compat_flags_to_skip -= 1 471 | 472 | if self.meta.version >= 117: 473 | self.compat_flags.disable_sample_macro = bool(read_byte(stream)) 474 | compat_flags_to_skip -= 1 475 | 476 | if self.meta.version >= 121: 477 | self.compat_flags.broken_out_vol_2 = bool(read_byte(stream)) 478 | compat_flags_to_skip -= 1 479 | 480 | if self.meta.version >= 130: 481 | self.compat_flags.old_arp_strategy = bool(read_byte(stream)) 482 | compat_flags_to_skip -= 1 483 | 484 | elif phase == 3: 485 | compat_flags_to_skip = 8 486 | if self.meta.version >= 138: 487 | self.compat_flags.broken_porta_during_legato = bool(read_byte(stream)) 488 | compat_flags_to_skip -= 1 489 | 490 | if self.meta.version >= 155: 491 | self.compat_flags.broken_fm_off = bool(read_byte(stream)) 492 | compat_flags_to_skip -= 1 493 | 494 | if self.meta.version >= 168: 495 | self.compat_flags.pre_note_no_effect = bool(read_byte(stream)) 496 | compat_flags_to_skip -= 1 497 | 498 | if self.meta.version >= 183: 499 | self.compat_flags.old_dpcm = bool(read_byte(stream)) 500 | compat_flags_to_skip -= 1 501 | 502 | if self.meta.version >= 184: 503 | self.compat_flags.reset_arp_phase_on_new_note = bool(read_byte(stream)) 504 | compat_flags_to_skip -= 1 505 | 506 | if self.meta.version >= 188: 507 | self.compat_flags.ceil_volume_scaling = bool(read_byte(stream)) 508 | compat_flags_to_skip -= 1 509 | 510 | if self.meta.version >= 191: 511 | self.compat_flags.old_always_set_volume = bool(read_byte(stream)) 512 | compat_flags_to_skip -= 1 513 | else: 514 | raise ValueError("Compat flag phase must be in between: 1, 2, 3") 515 | stream.read(compat_flags_to_skip) 516 | 517 | def __read_dev119_chip_flags(self, stream: BinaryIO) -> None: 518 | for i in range(len(self.chips.list)): 519 | # skip if this chip doesn't have flags 520 | if self.__chip_flag_ptr[i] == 0: 521 | continue 522 | stream.seek(self.__chip_flag_ptr[i]) 523 | 524 | if stream.read(4) != b"FLAG": 525 | raise ValueError('No "FLAG" magic') 526 | 527 | # i assume this will grow, you never know 528 | blk_size = read_int(stream) 529 | flag_blk = BytesIO(stream.read(blk_size)) 530 | 531 | # read entries in FLAG 532 | for entry in [flag.split("=") for flag in read_str(flag_blk).split()]: 533 | key = entry[0] 534 | value = entry[1] 535 | # cast by regex 536 | if re.match(r"true", value): 537 | self.chips.list[i].flags[key] = True 538 | elif re.match(r"false", value): 539 | self.chips.list[i].flags[key] = False 540 | elif re.match(r"\d+$", value): 541 | self.chips.list[i].flags[key] = int(value) 542 | elif re.match(r"\d+\.\d+", value): 543 | self.chips.list[i].flags[key] = float(value) 544 | else: # all other values should be treated as a string 545 | self.chips.list[i].flags[key] = value 546 | 547 | @staticmethod 548 | def __convert_old_chip_flags( 549 | chip: ChipType, flag: int 550 | ) -> Dict[str, Union[bool, int]]: 551 | """ 552 | Convert pre-v119 binary chip flags to the newer dict-style form. 553 | 554 | :param chip: ChipType 555 | :param flag: flag value as a 32-bit number 556 | :return: dictionary containing the flag's equivalent values 557 | """ 558 | n = {} 559 | 560 | if chip in [ChipType.GENESIS, ChipType.GENESIS_EX]: 561 | n["clockSel"] = flag & 2147483647 # bits 0-30 562 | n["ladderEffect"] = bool((flag >> 31) & 1) 563 | elif chip == ChipType.SMS: 564 | cs = flag & 0xFF03 565 | if cs > 0x100: 566 | cs = cs - 252 # 0x100 + 4 567 | n["clockSel"] = cs 568 | ct = (flag & 0xCC) // 4 569 | if ct >= 32: 570 | ct -= 24 571 | elif ct >= 16: 572 | ct -= 12 573 | n["chipType"] = ct 574 | n["noPhaseReset"] = flag >> 4 575 | elif chip == ChipType.GB: 576 | n["chipType"] = flag & 0b11 577 | n["noAntiClick"] = bool((flag >> 3) & 1) 578 | elif chip == ChipType.PCE: 579 | n["clockSel"] = flag & 1 580 | n["chipType"] = (flag >> 2) & 1 581 | n["noAntiClick"] = bool((flag >> 3) & 1) 582 | elif chip in [ChipType.NES, ChipType.VRC6, ChipType.FDS, ChipType.MMC5]: 583 | n["clockSel"] = flag & 0b11 584 | elif chip in [ChipType.C64_8580, ChipType.C64_6581]: 585 | n["clockSel"] = flag & 0b1111 586 | elif chip == ChipType.SEGA_ARCADE: 587 | n["clockSel"] = flag & 0b11111111 588 | elif chip in [ 589 | ChipType.NEO_GEO_CD, 590 | ChipType.NEO_GEO, 591 | ChipType.NEO_GEO_EX, 592 | ChipType.NEO_GEO_CD_EX, 593 | ChipType.YM2610B, 594 | ChipType.YM2610B_EX, 595 | ]: 596 | n["clockSel"] = flag & 0b11111111 597 | elif chip == ChipType.AY38910: 598 | n["clockSel"] = flag & 0b1111 599 | n["chipType"] = (flag >> 4) & 0b11 600 | n["stereo"] = bool((flag >> 6) & 1) 601 | n["halfClock"] = bool((flag >> 7) & 1) 602 | n["stereoSep"] = (flag >> 8) & 0b11111111 603 | elif chip == ChipType.AMIGA: 604 | n["clockSel"] = flag & 1 605 | n["chipType"] = (flag >> 1) & 1 606 | n["bypassLimits"] = bool((flag >> 2) & 1) 607 | n["stereoSep"] = (flag >> 8) & 0b1111111 608 | elif chip == ChipType.YM2151: 609 | n["clockSel"] = flag & 0b11111111 610 | elif chip in [ 611 | ChipType.YM2612, 612 | ChipType.YM2612_EX, 613 | ChipType.YM2612_PLUS, 614 | ChipType.YM2612_PLUS_EX, 615 | ]: 616 | n["clockSel"] = flag & 2147483647 # bits 0-30 617 | n["ladderEffect"] = bool((flag >> 31) & 1) 618 | elif chip == ChipType.TIA: 619 | n["clockSel"] = flag & 1 620 | n["mixingType"] = (flag >> 1) & 0b11 621 | elif chip == ChipType.VIC20: 622 | n["clockSel"] = flag & 1 623 | elif chip == ChipType.SNES: 624 | n["volScaleL"] = flag & 0b1111111 625 | n["volScaleR"] = (flag >> 8) & 0b1111111 626 | elif chip in [ChipType.OPLL, ChipType.OPLL_DRUMS]: 627 | n["clockSel"] = flag & 0b1111 628 | n["patchSet"] = flag >> 4 # safe 629 | elif chip == ChipType.N163: 630 | n["clockSel"] = flag & 0b1111 631 | n["channels"] = (flag >> 4) & 0b111 632 | n["multiplex"] = bool((flag >> 7) & 1) 633 | elif chip in [ChipType.OPN, ChipType.YM2203_EX]: 634 | n["clockSel"] = flag & 0b11111 635 | n["prescale"] = (flag >> 5) & 0b11 636 | elif chip in [ 637 | ChipType.OPL, 638 | ChipType.OPL_DRUMS, 639 | ChipType.OPL2, 640 | ChipType.OPL2_DRUMS, 641 | ChipType.Y8950, 642 | ChipType.Y8950_DRUMS, 643 | ]: 644 | n["clockSel"] = flag & 0b11111111 645 | elif chip in [ChipType.OPL3, ChipType.OPL3_DRUMS]: 646 | n["clockSel"] = flag & 0b11111111 647 | elif chip == ChipType.PC_SPEAKER: 648 | n["speakerType"] = flag & 0b11 649 | elif chip == ChipType.RF5C68: 650 | n["clockSel"] = flag & 0b1111 651 | n["chipType"] = flag >> 4 # safe 652 | elif chip in [ChipType.SAA1099, ChipType.OPZ]: 653 | n["clockSel"] = flag & 0b11 654 | elif chip == ChipType.AY8930: 655 | n["clockSel"] = flag & 0b1111 656 | n["stereo"] = bool((flag >> 6) & 1) 657 | n["halfClock"] = bool((flag >> 7) & 1) 658 | n["stereoSep"] = (flag >> 8) & 0b11111111 659 | elif chip == ChipType.VRC7: 660 | n["clockSel"] = flag & 0b11 661 | elif chip == ChipType.ZX_BEEPER: 662 | n["clockSel"] = flag & 1 663 | elif chip in [ChipType.SCC, ChipType.SCC_PLUS]: 664 | n["clockSel"] = flag & 0b11 665 | elif chip == ChipType.MSM6295: 666 | n["clockSel"] = flag & 0b1111111 667 | n["rateSel"] = bool((flag >> 7) & 1) 668 | elif chip == ChipType.MSM6258: 669 | n["clockSel"] = flag & 0b11 670 | elif chip in [ChipType.OPL4, ChipType.OPL4_DRUMS]: 671 | n["clockSel"] = flag & 0b11111111 672 | elif chip == ChipType.SETA: 673 | n["clockSel"] = flag & 0b1111 674 | n["stereo"] = bool((flag >> 4) & 1) 675 | elif chip == ChipType.ES5506: 676 | n["channels"] = flag & 0b11111 677 | elif chip == ChipType.TSU: 678 | n["clockSel"] = flag & 1 679 | n["echo"] = bool((flag >> 2) & 1) 680 | n["swapEcho"] = bool((flag >> 3) & 1) 681 | n["sampleMemSize"] = (flag >> 4) & 1 682 | n["pdm"] = bool((flag >> 5) & 1) 683 | n["echoDelay"] = (flag >> 8) & 0b111111 684 | n["echoFeedback"] = (flag >> 16) & 0b1111 685 | n["echoResolution"] = (flag >> 20) & 0b1111 686 | n["echoVol"] = (flag >> 24) & 0b11111111 687 | elif chip == ChipType.YMZ280B: 688 | n["clockSel"] = flag & 0b11111111 689 | elif chip == ChipType.PCM_DAC: 690 | n["rate"] = (flag & 0b1111111111111111) + 1 691 | n["outDepth"] = (flag >> 16) & 0b1111 692 | n["stereo"] = bool((flag >> 20) & 1) 693 | elif chip == ChipType.QSOUND: 694 | n["echoDelay"] = flag & 0b1111111111111 695 | n["echoFeedback"] = (flag >> 16) & 0b11111111 696 | return n 697 | 698 | def __read_header(self, stream: BinaryIO) -> None: 699 | # assuming we passed the magic number check 700 | self.meta.version = read_short(stream) 701 | stream.read(2) # RESERVED 702 | self.__song_info_ptr = read_int(stream) 703 | stream.read(8) # RESERVED 704 | 705 | def __read_info(self, stream: BinaryIO) -> None: 706 | stream.seek(self.__song_info_ptr) 707 | if stream.read(4) != b"INFO": 708 | raise ValueError('No "INFO" magic') 709 | 710 | if self.meta.version < 100: # don't read size prior to 0.6pre1 711 | stream.read(4) 712 | info_blk = stream 713 | else: 714 | blk_size = read_int(stream) 715 | info_blk = BytesIO(stream.read(blk_size)) 716 | 717 | # info of first subsong 718 | self.subsongs[0].timing.timebase = read_byte(info_blk) + 1 719 | self.subsongs[0].timing.speed = (read_byte(info_blk), read_byte(info_blk)) 720 | self.subsongs[0].timing.arp_speed = read_byte(info_blk) 721 | self.subsongs[0].timing.clock_speed = read_float(info_blk) 722 | self.subsongs[0].pattern_length = read_short(info_blk) 723 | len_orders = read_short(info_blk) 724 | self.subsongs[0].timing.highlight = (read_byte(info_blk), read_byte(info_blk)) 725 | 726 | # global 727 | num_insts = read_short(info_blk) 728 | num_waves = read_short(info_blk) 729 | num_samples = read_short(info_blk) 730 | num_patterns = read_int(info_blk) 731 | 732 | # fetch chip list 733 | for chip_id in info_blk.read(MAX_CHIPS): 734 | if chip_id == 0: 735 | break # seek position is after chips here 736 | self.chips.list.append(ChipInfo(ChipType(chip_id))) # type: ignore 737 | 738 | # fetch volume 739 | for i in range(MAX_CHIPS): 740 | vol = read_byte(info_blk, True) / 64.0 741 | if i >= len(self.chips.list): # cut here 742 | continue 743 | self.chips.list[i].volume = vol 744 | 745 | for i in range(MAX_CHIPS): 746 | pan = read_byte(info_blk, True) / 128.0 747 | if i >= len(self.chips.list): # cut here 748 | continue 749 | self.chips.list[i].panning = pan 750 | 751 | if self.meta.version >= 119: 752 | self.__chip_flag_ptr: List[int] = [ 753 | read_int(info_blk) for _ in range(MAX_CHIPS) 754 | ] 755 | else: 756 | for i in range(MAX_CHIPS): 757 | flag = read_int(info_blk) 758 | if i < len(self.chips.list): 759 | self.chips.list[i].flags.update( 760 | self.__convert_old_chip_flags(self.chips.list[i].type, flag) 761 | ) 762 | 763 | self.meta.name = read_str(info_blk) 764 | self.meta.author = read_str(info_blk) 765 | self.meta.tuning = read_float(info_blk) 766 | 767 | # Compat flags, part I 768 | self.__read_compat_flags(info_blk, 1) 769 | 770 | self.__instrument_ptr = [read_int(info_blk) for _ in range(num_insts)] 771 | 772 | self.__wavetable_ptr = [read_int(info_blk) for _ in range(num_waves)] 773 | 774 | self.__sample_ptr = [read_int(info_blk) for _ in range(num_samples)] 775 | 776 | self.__pattern_ptr = [read_int(info_blk) for _ in range(num_patterns)] 777 | 778 | num_channels = self.get_num_channels() 779 | 780 | for channel in range(self.get_num_channels()): 781 | self.subsongs[0].order[channel] = [ 782 | read_byte(info_blk) for _ in range(len_orders) 783 | ] 784 | 785 | self.subsongs[0].effect_columns = [ 786 | read_byte(info_blk) for _ in range(num_channels) 787 | ] 788 | 789 | # set up channels display info 790 | self.subsongs[0].channel_display = [ 791 | ChannelDisplayInfo() for _ in range(num_channels) 792 | ] 793 | 794 | for i in range(num_channels): 795 | self.subsongs[0].channel_display[i].shown = bool(read_byte(info_blk)) 796 | 797 | for i in range(num_channels): 798 | self.subsongs[0].channel_display[i].collapsed = bool(read_byte(info_blk)) 799 | 800 | for i in range(num_channels): 801 | self.subsongs[0].channel_display[i].name = read_str(info_blk) 802 | 803 | for i in range(num_channels): 804 | self.subsongs[0].channel_display[i].abbreviation = read_str(info_blk) 805 | 806 | self.meta.comment = read_str(info_blk) 807 | 808 | # Master volume 809 | if self.meta.version >= 59: 810 | self.chips.master_volume = read_float(info_blk) 811 | 812 | # Compat flags, part II 813 | if self.meta.version >= 70: 814 | self.__read_compat_flags(info_blk, 2) 815 | if self.meta.version >= 96: 816 | self.subsongs[0].timing.virtual_tempo = ( 817 | read_short(info_blk), 818 | read_short(info_blk), 819 | ) 820 | else: 821 | info_blk.read(4) # reserved in self.meta.version < 96 822 | 823 | # Subsongs 824 | if self.meta.version >= 95: 825 | self.subsongs[0].name = read_str(info_blk) 826 | self.subsongs[0].comment = read_str(info_blk) 827 | num_extra_subsongs = read_byte(info_blk) 828 | info_blk.read(3) # reserved 829 | self.__subsong_ptr = [read_int(info_blk) for _ in range(num_extra_subsongs)] 830 | 831 | # Extra metadata 832 | if self.meta.version >= 103: 833 | self.meta.sys_name = read_str(info_blk) 834 | self.meta.album = read_str(info_blk) 835 | # TODO: need to take encoding into account 836 | self.meta.name_jp = read_str(info_blk) 837 | self.meta.author_jp = read_str(info_blk) 838 | self.meta.sys_name_jp = read_str(info_blk) 839 | self.meta.album_jp = read_str(info_blk) 840 | 841 | # New chip mixer and patchbay 842 | if self.meta.version >= 135: 843 | for i in range(len(self.chips.list)): 844 | # new chip volume/panning format takes precedence over the legacy one 845 | # if you save a .fur with this, legacy and new volume/panning formats 846 | # have the same value. different values shouldn't be possible 847 | self.chips.list[i].volume = read_float(info_blk) 848 | self.chips.list[i].panning = read_float(info_blk) 849 | self.chips.list[i].surround = read_float(info_blk) 850 | num_patchbay_connections = read_int(info_blk) 851 | for _ in range(num_patchbay_connections): 852 | src = read_short(info_blk) 853 | dst = read_short(info_blk) 854 | self.patchbay.append( 855 | PatchBay( 856 | dest=InputPatchBayEntry( 857 | set=InputPortSet(src >> 4), port=src & 0b1111 858 | ), 859 | source=OutputPatchBayEntry( 860 | set=OutputPortSet(dst >> 4), port=dst & 0b1111 861 | ), 862 | ) 863 | ) 864 | 865 | if self.meta.version >= 136: 866 | self.compat_flags.auto_patchbay = bool(read_byte(info_blk)) 867 | 868 | # Compat flags, part III 869 | if self.meta.version >= 138: 870 | self.__read_compat_flags(info_blk, 3) 871 | 872 | # Speed patterns and grooves 873 | if self.meta.version >= 139: 874 | # speed pattern 875 | len_speed_pattern = read_byte(info_blk) 876 | if (len_speed_pattern < 0) or (len_speed_pattern > 16): 877 | raise ValueError("Invalid speed pattern length value") 878 | self.subsongs[0].speed_pattern = [ 879 | read_byte(info_blk) for _ in range(len_speed_pattern) 880 | ] 881 | info_blk.read( 882 | 16 - len_speed_pattern 883 | ) # skip that many bytes, because it's always 0x06 884 | 885 | # groove 886 | len_groove_list = read_byte(info_blk) 887 | for _ in range(len_groove_list): 888 | len_groove = read_byte(info_blk) 889 | self.subsongs[0].grooves.append( 890 | [read_byte(info_blk) for _ in range(len_groove)] 891 | ) 892 | info_blk.read( 893 | 16 - len_groove 894 | ) # TODO: i assume the same as above. i hope i'm right 895 | 896 | def __read_instruments(self, stream: BinaryIO) -> None: 897 | for i in self.__instrument_ptr: 898 | if i == 0: 899 | break 900 | stream.seek(i) 901 | new_ins = FurnaceInstrument() 902 | if self.meta.version < 127: # i trust this not to screw up 903 | new_ins.load_from_stream(stream, _FurInsImportType.FORMAT_0_EMBED) 904 | else: 905 | new_ins.load_from_stream(stream, _FurInsImportType.FORMAT_1_EMBED) 906 | self.instruments.append(new_ins) 907 | 908 | def __read_wavetables(self, stream: BinaryIO) -> None: 909 | for i in self.__wavetable_ptr: 910 | if i == 0: 911 | break 912 | stream.seek(i) 913 | new_wt = FurnaceWavetable() 914 | new_wt.load_from_stream(stream, _FurWavetableImportType.EMBED) 915 | self.wavetables.append(new_wt) 916 | 917 | def __read_samples(self, stream: BinaryIO) -> None: 918 | for i in self.__sample_ptr: 919 | if i == 0: 920 | break 921 | stream.seek(i) 922 | new_wt = FurnaceSample() 923 | new_wt.load_from_stream(stream) 924 | self.samples.append(new_wt) 925 | 926 | def __read_patterns(self, stream: BinaryIO) -> None: 927 | for i in self.__pattern_ptr: 928 | if i == 0: 929 | break 930 | stream.seek(i) 931 | 932 | # Old pattern 933 | if self.meta.version < 157: 934 | if stream.read(4) != b"PATR": 935 | raise ValueError('No "PATR" magic') 936 | sz = read_int(stream) 937 | if sz == 0: 938 | patr_blk = stream 939 | else: 940 | patr_blk = BytesIO(stream.read(sz)) 941 | 942 | new_patr = FurnacePattern() 943 | new_patr.channel = read_short(patr_blk) 944 | new_patr.index = read_short(patr_blk) 945 | new_patr.subsong = read_short(patr_blk) 946 | if self.meta.version < 95: 947 | assert new_patr.subsong == 0 948 | read_short(patr_blk) # reserved 949 | 950 | num_rows = self.subsongs[new_patr.subsong].pattern_length 951 | 952 | for _ in range(num_rows): 953 | row = FurnaceRow( 954 | note=Note(read_short(patr_blk)), 955 | octave=read_short(patr_blk), 956 | instrument=read_short(patr_blk), 957 | volume=read_short(patr_blk), 958 | ) 959 | row.octave += 1 if row.note == Note.C_ else 0 960 | effect_columns = self.subsongs[new_patr.subsong].effect_columns[ 961 | new_patr.channel 962 | ] 963 | row.effects = [ 964 | (read_short(patr_blk), read_short(patr_blk)) 965 | for _ in range(effect_columns) 966 | ] 967 | new_patr.data.append(row) 968 | 969 | if self.meta.version >= 51: 970 | new_patr.name = read_str(patr_blk) 971 | 972 | # New pattern 973 | else: 974 | if stream.read(4) != b"PATN": 975 | raise ValueError('No "PATN" magic') 976 | sz = read_int(stream) 977 | if sz == 0: 978 | patr_blk = stream 979 | else: 980 | patr_blk = BytesIO(stream.read(sz)) 981 | 982 | new_patr = FurnacePattern() 983 | new_patr.subsong = read_byte(patr_blk) 984 | new_patr.channel = read_byte(patr_blk) 985 | new_patr.index = read_short(patr_blk) 986 | new_patr.name = read_str(patr_blk) 987 | 988 | num_rows = self.subsongs[new_patr.subsong].pattern_length 989 | effect_columns = self.subsongs[new_patr.subsong].effect_columns[ 990 | new_patr.channel 991 | ] 992 | 993 | empty_row: Callable[[], FurnaceRow] = lambda: FurnaceRow( 994 | Note.__, 0, 0xFFFF, 0xFFFF, [(0xFFFF, 0xFFFF)] * effect_columns 995 | ) 996 | 997 | row_idx = 0 998 | while row_idx < num_rows: 999 | char = read_byte(patr_blk) 1000 | # end of pattern 1001 | if char == 0xFF: 1002 | break 1003 | # skip N+2 rows 1004 | if char & 0x80: 1005 | skip = (char & 0x7F) + 2 1006 | row_idx += skip 1007 | for _ in range(skip): 1008 | new_patr.data.append(empty_row()) 1009 | continue 1010 | # check if some values present 1011 | effect_present_list = [False] * 8 1012 | effect_val_present_list = [False] * 8 1013 | note_present = bool(char & 0x01) 1014 | ins_present = bool(char & 0x02) 1015 | volume_present = bool(char & 0x04) 1016 | effect_present_list[0] = bool(char & 0x08) 1017 | effect_val_present_list[0] = bool(char & 0x10) 1018 | effect_0_3_present = bool(char & 0x20) 1019 | effect_4_7_present = bool(char & 0x40) 1020 | if effect_0_3_present: 1021 | char = read_byte(patr_blk) 1022 | assert effect_present_list[0] == bool(char & 0x01) 1023 | assert effect_val_present_list[0] == bool(char & 0x02) 1024 | effect_present_list[1] = bool(char & 0x04) 1025 | effect_val_present_list[1] = bool(char & 0x08) 1026 | effect_present_list[2] = bool(char & 0x10) 1027 | effect_val_present_list[2] = bool(char & 0x20) 1028 | effect_present_list[3] = bool(char & 0x40) 1029 | effect_val_present_list[3] = bool(char & 0x80) 1030 | if effect_4_7_present: 1031 | char = read_byte(patr_blk) 1032 | effect_present_list[4] = bool(char & 0x01) 1033 | effect_val_present_list[4] = bool(char & 0x02) 1034 | effect_present_list[5] = bool(char & 0x04) 1035 | effect_val_present_list[5] = bool(char & 0x08) 1036 | effect_present_list[6] = bool(char & 0x10) 1037 | effect_val_present_list[6] = bool(char & 0x20) 1038 | effect_present_list[7] = bool(char & 0x40) 1039 | effect_val_present_list[7] = bool(char & 0x80) 1040 | 1041 | # actually read present values 1042 | note, octave = Note(0), 0 1043 | if note_present: 1044 | raw_note = read_byte(patr_blk) 1045 | if raw_note == 180: 1046 | note = Note.OFF 1047 | elif raw_note == 181: 1048 | note = Note.OFF_REL 1049 | elif raw_note == 182: 1050 | note = Note.REL 1051 | else: 1052 | note_val = raw_note % 12 1053 | note_val = 12 if note_val == 0 else note_val 1054 | note = Note(note_val) 1055 | octave = -5 + raw_note // 12 1056 | 1057 | ins, volume = 0xFFFF, 0xFFFF 1058 | if ins_present: 1059 | ins = read_byte(patr_blk) 1060 | if volume_present: 1061 | volume = read_byte(patr_blk) 1062 | 1063 | row = FurnaceRow( 1064 | note=note, octave=octave, instrument=ins, volume=volume 1065 | ) 1066 | 1067 | row.effects = [(0xFFFF, 0xFFFF)] * effect_columns 1068 | for i, fx_presents in enumerate( 1069 | zip(effect_present_list, effect_val_present_list) 1070 | ): 1071 | if i >= effect_columns: 1072 | break 1073 | fx_cmd, fx_val = 0xFFFF, 0xFFFF 1074 | if fx_presents[0]: 1075 | fx_cmd = read_byte(patr_blk) 1076 | if fx_presents[1]: 1077 | fx_val = read_byte(patr_blk) 1078 | row.effects[i] = (fx_cmd, fx_val) 1079 | 1080 | new_patr.data.append(row) 1081 | row_idx += 1 1082 | 1083 | # fill the rest of the pattern with EMPTY 1084 | while row_idx < num_rows: 1085 | new_patr.data.append(empty_row()) 1086 | row_idx += 1 1087 | 1088 | self.patterns.append(new_patr) 1089 | 1090 | def __read_subsongs(self, stream: BinaryIO) -> None: 1091 | for i in self.__subsong_ptr: 1092 | if i == 0: 1093 | break 1094 | stream.seek(i) 1095 | if stream.read(4) != b"SONG": 1096 | raise ValueError('No "SONG" magic') 1097 | subsong_blk = BytesIO(stream.read(read_int(stream))) 1098 | new_subsong = SubSong() 1099 | new_subsong.order.clear() 1100 | new_subsong.speed_pattern.clear() 1101 | 1102 | new_subsong.timing.timebase = read_byte(subsong_blk) 1103 | new_subsong.timing.speed = (read_byte(subsong_blk), read_byte(subsong_blk)) 1104 | new_subsong.timing.arp_speed = read_byte(subsong_blk) 1105 | new_subsong.timing.clock_speed = read_float(subsong_blk) 1106 | new_subsong.pattern_length = read_short(subsong_blk) 1107 | new_subsong_len_orders = read_short(subsong_blk) 1108 | new_subsong.timing.highlight = ( 1109 | read_byte(subsong_blk), 1110 | read_byte(subsong_blk), 1111 | ) 1112 | new_subsong.timing.virtual_tempo = ( 1113 | read_short(subsong_blk), 1114 | read_short(subsong_blk), 1115 | ) 1116 | new_subsong.name = read_str(subsong_blk) 1117 | new_subsong.comment = read_str(subsong_blk) 1118 | 1119 | num_channels = self.get_num_channels() 1120 | 1121 | for channel in range(self.get_num_channels()): 1122 | new_subsong.order[channel] = [ 1123 | read_byte(subsong_blk) for _ in range(new_subsong_len_orders) 1124 | ] 1125 | 1126 | new_subsong.effect_columns = [ 1127 | read_byte(subsong_blk) for _ in range(num_channels) 1128 | ] 1129 | 1130 | # set up channels display info 1131 | new_subsong.channel_display = [ 1132 | ChannelDisplayInfo() for _ in range(num_channels) 1133 | ] 1134 | 1135 | for i in range(num_channels): 1136 | new_subsong.channel_display[i].shown = bool(read_byte(subsong_blk)) 1137 | 1138 | for i in range(num_channels): 1139 | new_subsong.channel_display[i].collapsed = bool(read_byte(subsong_blk)) 1140 | 1141 | for i in range(num_channels): 1142 | new_subsong.channel_display[i].name = read_str(subsong_blk) 1143 | 1144 | for i in range(num_channels): 1145 | new_subsong.channel_display[i].abbreviation = read_str(subsong_blk) 1146 | 1147 | # Speed patterns and grooves 1148 | if self.meta.version >= 139: 1149 | # speed pattern 1150 | len_speed_pattern = read_byte(subsong_blk) 1151 | if (len_speed_pattern < 0) or (len_speed_pattern > 16): 1152 | raise ValueError("Invalid speed pattern length value") 1153 | new_subsong.speed_pattern = [ 1154 | read_byte(subsong_blk) for _ in range(len_speed_pattern) 1155 | ] 1156 | 1157 | self.subsongs.append(new_subsong) 1158 | 1159 | def __str__(self) -> str: 1160 | return '' % ( 1161 | self.meta.version, 1162 | self.meta.name, 1163 | self.meta.author, 1164 | ) 1165 | -------------------------------------------------------------------------------- /chipchune/furnace/sample.py: -------------------------------------------------------------------------------- 1 | from io import BytesIO 2 | from typing import Optional, Union, BinaryIO, List 3 | 4 | from chipchune._util import read_short, read_int, read_str 5 | from .data_types import SampleMeta 6 | from .enums import _FurSampleType 7 | 8 | FILE_MAGIC_STR = b"SMP2" 9 | 10 | 11 | class FurnaceSample: 12 | def __init__(self) -> None: 13 | self.meta: SampleMeta = SampleMeta() 14 | """ 15 | Sample metadata. 16 | """ 17 | self.data: bytearray = b"" 18 | """ 19 | Sample data. 20 | """ 21 | 22 | def load_from_stream(self, stream: BinaryIO) -> None: 23 | """ 24 | Load a sample from an **uncompressed** stream. 25 | 26 | :param stream: File-like object containing the uncompressed sample. 27 | """ 28 | if stream.read(len(FILE_MAGIC_STR)) != FILE_MAGIC_STR: 29 | raise ValueError("Bad magic value for a sample") 30 | blk_size = read_int(stream) 31 | if blk_size > 0: 32 | smp_data = BytesIO(stream.read(blk_size)) 33 | else: 34 | smp_data = stream 35 | 36 | self.meta.name = read_str(smp_data) 37 | self.meta.length = read_int(smp_data) 38 | read_int(smp_data) # compatablity rate 39 | read_int(smp_data) # C-4 rate 40 | self.meta.depth = int(smp_data.read(1)[0]) 41 | smp_data.read(1) # loop direction 42 | smp_data.read(1) # flags 43 | smp_data.read(1) # flags 2 44 | self.meta.loop_start = read_int(smp_data) 45 | self.meta.loop_end = read_int(smp_data) 46 | smp_data.read(16) # sample presence bitfields 47 | self.data = smp_data.read(self.meta.length) 48 | -------------------------------------------------------------------------------- /chipchune/furnace/wavetable.py: -------------------------------------------------------------------------------- 1 | from io import BytesIO 2 | from typing import Optional, Union, BinaryIO, List 3 | 4 | from chipchune._util import read_short, read_int, read_str 5 | from .data_types import WavetableMeta 6 | from .enums import _FurWavetableImportType 7 | 8 | FILE_MAGIC_STR = b"-Furnace waveta-" 9 | EMBED_MAGIC_STR = b"WAVE" 10 | 11 | 12 | class FurnaceWavetable: 13 | def __init__(self, file_name: Optional[str] = None) -> None: 14 | """ 15 | Creates or opens a new Furnace wavetable as a Python object. 16 | 17 | :param file_name: (Optional) 18 | If specified, then it will parse a file as a FurnaceWavetable. If file name (str) is 19 | given, it will load that file. 20 | 21 | Defaults to None. 22 | """ 23 | self.file_name: Optional[str] = None 24 | """ 25 | Original file name, if the object was initialized with one. 26 | """ 27 | self.meta: WavetableMeta = WavetableMeta() 28 | """ 29 | Wavetable metadata. 30 | """ 31 | self.data: List[int] = [] 32 | """ 33 | Wavetable data. 34 | """ 35 | 36 | if isinstance(file_name, str): 37 | self.load_from_file(file_name) 38 | 39 | def load_from_file(self, file_name: Optional[str] = None) -> None: 40 | if isinstance(file_name, str): 41 | self.file_name = file_name 42 | if self.file_name is None: 43 | raise RuntimeError( 44 | "No file name set, either set self.file_name or pass file_name to the function" 45 | ) 46 | 47 | # since we're loading from an uncompressed file, we can just check the file magic number 48 | with open(self.file_name, "rb") as f: 49 | detect_magic = f.peek(len(FILE_MAGIC_STR))[: len(FILE_MAGIC_STR)] 50 | if detect_magic == FILE_MAGIC_STR: 51 | return self.load_from_stream(f, _FurWavetableImportType.FILE) 52 | else: # uncompressed for sure 53 | raise ValueError("No recognized file type magic") 54 | 55 | def load_from_bytes( 56 | self, data: bytes, import_as: Union[int, _FurWavetableImportType] 57 | ) -> None: 58 | """ 59 | Load a wavetable from a series of bytes. 60 | 61 | :param data: Bytes 62 | """ 63 | return self.load_from_stream(BytesIO(data), import_as) 64 | 65 | def load_from_stream( 66 | self, stream: BinaryIO, import_as: Union[int, _FurWavetableImportType] 67 | ) -> None: 68 | """ 69 | Load a wavetable from an **uncompressed** stream. 70 | 71 | :param stream: File-like object containing the uncompressed wavetable. 72 | :param import_as: int 73 | - 0 = wavetable file 74 | - 1 = wavetable embedded in module 75 | """ 76 | if import_as == _FurWavetableImportType.FILE: 77 | if stream.read(len(FILE_MAGIC_STR)) != FILE_MAGIC_STR: 78 | raise ValueError("Bad magic value for a wavetable file") 79 | version = read_short(stream) 80 | read_short(stream) # reserved 81 | self.__load_embed(stream) 82 | 83 | elif import_as == _FurWavetableImportType.EMBED: 84 | return self.__load_embed(stream) 85 | 86 | else: 87 | raise ValueError("Invalid import type") 88 | 89 | def __load_embed(self, stream: BinaryIO) -> None: 90 | if stream.read(len(EMBED_MAGIC_STR)) != EMBED_MAGIC_STR: 91 | raise RuntimeError("Bad magic value for a wavetable embed") 92 | 93 | blk_size = read_int(stream) 94 | if blk_size > 0: 95 | wt_data: Union[BytesIO, BinaryIO] = BytesIO(stream.read(blk_size)) 96 | else: 97 | wt_data = stream 98 | 99 | self.meta.name = read_str(wt_data) 100 | self.meta.width = read_int(wt_data) 101 | read_int(wt_data) # reserved 102 | self.meta.height = ( 103 | read_int(wt_data) + 1 104 | ) # serialized height is 1 lower than actual value 105 | 106 | self.data = [read_int(wt_data) for _ in range(self.meta.width)] 107 | -------------------------------------------------------------------------------- /chipchune/interchange/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Generic format for manipulating to, from, and between tracker formats. 3 | 4 | - :mod:`chipchune.interchange.enums`: Various constants. 5 | - :mod:`chipchune.interchange.furnace`: Adapters for Furnace. 6 | 7 | """ 8 | -------------------------------------------------------------------------------- /chipchune/interchange/enums.py: -------------------------------------------------------------------------------- 1 | import enum 2 | from chipchune._util import EnumShowNameOnly 3 | 4 | 5 | class InterNote(EnumShowNameOnly): 6 | """ 7 | Common note interchange format. 8 | """ 9 | 10 | __ = enum.auto() # Signifies a blank space in tracker 11 | C_ = enum.auto() 12 | Cs = enum.auto() 13 | D_ = enum.auto() 14 | Ds = enum.auto() 15 | E_ = enum.auto() 16 | F_ = enum.auto() 17 | Fs = enum.auto() 18 | G_ = enum.auto() 19 | Gs = enum.auto() 20 | A_ = enum.auto() 21 | As = enum.auto() 22 | B_ = enum.auto() 23 | Off = enum.auto() 24 | OffRel = enum.auto() 25 | Rel = enum.auto() 26 | Echo = enum.auto() 27 | -------------------------------------------------------------------------------- /chipchune/interchange/furnace.py: -------------------------------------------------------------------------------- 1 | from chipchune.furnace.enums import Note as FurnaceNote 2 | from chipchune.interchange.enums import InterNote 3 | 4 | 5 | def furnace_note_to_internote(note: FurnaceNote) -> InterNote: 6 | """ 7 | Convert a Furnace note into an InterNote. 8 | 9 | Raises: 10 | - Exception: If the supplied note is out of range. 11 | """ 12 | if note == FurnaceNote.__: 13 | return InterNote.__ 14 | elif note == FurnaceNote.C_: 15 | return InterNote.C_ 16 | elif note == FurnaceNote.Cs: 17 | return InterNote.Cs 18 | elif note == FurnaceNote.D_: 19 | return InterNote.D_ 20 | elif note == FurnaceNote.Ds: 21 | return InterNote.Ds 22 | elif note == FurnaceNote.E_: 23 | return InterNote.E_ 24 | elif note == FurnaceNote.F_: 25 | return InterNote.F_ 26 | elif note == FurnaceNote.Fs: 27 | return InterNote.Fs 28 | elif note == FurnaceNote.G_: 29 | return InterNote.G_ 30 | elif note == FurnaceNote.Gs: 31 | return InterNote.Gs 32 | elif note == FurnaceNote.A_: 33 | return InterNote.A_ 34 | elif note == FurnaceNote.As: 35 | return InterNote.As 36 | elif note == FurnaceNote.B_: 37 | return InterNote.B_ 38 | elif note == FurnaceNote.OFF: 39 | return InterNote.Off 40 | elif note == FurnaceNote.OFF_REL: 41 | return InterNote.OffRel 42 | elif note == FurnaceNote.REL: 43 | return InterNote.Rel 44 | else: 45 | raise Exception("Invalid note value %s" % note) 46 | 47 | 48 | def internote_to_furnace_note(note: InterNote) -> FurnaceNote: 49 | """ 50 | Convert an InterNote into a Furnace note. If the equivalent 51 | value is unable to be determined, a blank note `__` is returned. 52 | """ 53 | if note == InterNote.__: 54 | return FurnaceNote.__ 55 | elif note == InterNote.C_: 56 | return FurnaceNote.C_ 57 | elif note == InterNote.Cs: 58 | return FurnaceNote.Cs 59 | elif note == InterNote.D_: 60 | return FurnaceNote.D_ 61 | elif note == InterNote.Ds: 62 | return FurnaceNote.Ds 63 | elif note == InterNote.E_: 64 | return FurnaceNote.E_ 65 | elif note == InterNote.F_: 66 | return FurnaceNote.F_ 67 | elif note == InterNote.Fs: 68 | return FurnaceNote.Fs 69 | elif note == InterNote.G_: 70 | return FurnaceNote.G_ 71 | elif note == InterNote.Gs: 72 | return FurnaceNote.Gs 73 | elif note == InterNote.A_: 74 | return FurnaceNote.A_ 75 | elif note == InterNote.As: 76 | return FurnaceNote.As 77 | elif note == InterNote.B_: 78 | return FurnaceNote.B_ 79 | elif note == InterNote.Off: 80 | return FurnaceNote.OFF 81 | elif note == InterNote.OffRel: 82 | return FurnaceNote.OFF_REL 83 | elif note == InterNote.Rel: 84 | return FurnaceNote.REL 85 | else: 86 | return FurnaceNote.__ 87 | -------------------------------------------------------------------------------- /chipchune/utils/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Utilities for manipulating, converting, etc. tracker data. 3 | 4 | This is for library users; it's not to be confused with `_util.py`, 5 | which is for internal use. 6 | 7 | - :mod:`chipchune.utils.conversion`: Conversion tools. 8 | """ 9 | -------------------------------------------------------------------------------- /chipchune/utils/conversion.py: -------------------------------------------------------------------------------- 1 | from chipchune.furnace.module import FurnacePattern 2 | from chipchune.interchange.enums import InterNote 3 | from chipchune.interchange.furnace import furnace_note_to_internote 4 | from typing import Union, List, Tuple 5 | from dataclasses import dataclass, field 6 | 7 | 8 | @dataclass 9 | class SequenceEntry: 10 | """ 11 | A representation of a row in note-length format. Such a format is commonly 12 | used across different sound engines and sequenced data. 13 | 14 | A pattern can be turned into a list of SequenceEntries, which should be easier 15 | to convert into a format of your choice. 16 | """ 17 | 18 | note: InterNote 19 | length: int 20 | volume: int 21 | """ 22 | Tracker-defined volume; although this should be -1 for undefined values. 23 | """ 24 | octave: int 25 | """ 26 | Tracker-defined octave; although this should be -1 for undefined values. 27 | """ 28 | instrument: int 29 | """ 30 | Tracker-defined instrument number; although this should be -1 for undefined values. 31 | """ 32 | effects: List[Tuple[int, int]] = field(default_factory=list) 33 | """ 34 | Tracker-defined effects list; if undefined, this should be empty. 35 | """ 36 | 37 | 38 | def pattern_to_sequence(pattern: Union[FurnacePattern, None]) -> List[SequenceEntry]: 39 | """ 40 | Interface to convert a pattern from tracker rows to a "sequence", which is 41 | really a list of SequenceEntries. 42 | 43 | :param pattern: 44 | A pattern object. Supported types at the moment: `FurnacePattern`. 45 | Anything outside of the supported types will throw a `TypeError`. 46 | """ 47 | if isinstance(pattern, FurnacePattern): 48 | return furnace_pattern_to_sequence(pattern) 49 | else: 50 | raise TypeError("Invalid pattern type; must be one of: FurnacePattern") 51 | 52 | 53 | def furnace_pattern_to_sequence(pattern: FurnacePattern) -> List[SequenceEntry]: 54 | converted: List[SequenceEntry] = [] 55 | last_volume = -1 56 | for i in pattern.data: 57 | note = furnace_note_to_internote(i.note) 58 | effects = i.effects 59 | volume = i.volume 60 | instrument = i.instrument 61 | 62 | if effects == [(65535, 65535)]: 63 | effects = [] 64 | 65 | if volume == 65535: 66 | volume = last_volume 67 | else: 68 | last_volume = volume 69 | 70 | if instrument == 65535: 71 | instrument = -1 72 | 73 | if note == InterNote.__: 74 | if len(converted) == 0: 75 | converted.append( 76 | SequenceEntry( 77 | note=InterNote.__, 78 | length=1, 79 | volume=volume, 80 | octave=i.octave, 81 | instrument=instrument, 82 | effects=effects, 83 | ) 84 | ) 85 | else: 86 | converted[-1].length += 1 87 | else: 88 | converted.append( 89 | SequenceEntry( 90 | note=note, 91 | length=1, 92 | volume=volume, 93 | octave=i.octave, 94 | instrument=instrument, 95 | effects=effects, 96 | ) 97 | ) 98 | return converted 99 | -------------------------------------------------------------------------------- /examples/furnace/files/rocky_mountain.197.fur: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZoomTen/chipchune/975e4b2c1922f58aa22f191bff99bcd563e45002/examples/furnace/files/rocky_mountain.197.fur -------------------------------------------------------------------------------- /examples/furnace/fur2smps.py: -------------------------------------------------------------------------------- 1 | from chipchune.furnace.module import FurnaceModule, FurnacePattern 2 | from chipchune.furnace.enums import ChipType 3 | from chipchune.interchange.enums import InterNote 4 | import chipchune.utils.conversion as convert 5 | from typing import cast, List 6 | 7 | inter_note_to_smps = { 8 | InterNote.__: "nRst", 9 | InterNote.Off: "nRst", 10 | InterNote.OffRel: "nRst", 11 | InterNote.Rel: "nRst", 12 | InterNote.C_: "nC", 13 | InterNote.Cs: "nCs", 14 | InterNote.D_: "nD", 15 | InterNote.Ds: "nEb", 16 | InterNote.E_: "nE", 17 | InterNote.F_: "nF", 18 | InterNote.Fs: "nFs", 19 | InterNote.G_: "nG", 20 | InterNote.Gs: "nAb", 21 | InterNote.A_: "nA", 22 | InterNote.As: "nBb", 23 | InterNote.B_: "nB", 24 | } 25 | 26 | 27 | def fur2smps(module: FurnaceModule, title: str) -> List[str]: 28 | if not ( 29 | len(module.chips.list) > 1 30 | and module.chips.list[0].type == ChipType.YM2612 31 | and module.chips.list[1].type == ChipType.SMS 32 | ): 33 | raise Exception("Not a (Furnace) Genesis module") 34 | 35 | lines: List[str] = [] 36 | 37 | lines += [ 38 | "%s_Header:" % title, 39 | # Sonic 3&K track 40 | "\tsmpsHeaderStartSong 3", 41 | "\tsmpsHeaderVoice S3_UVB", 42 | "\tsmpsHeaderChan 6, 3", 43 | # TODO 44 | "\tsmpsHeaderTempo 6, $38", 45 | "\tsmpsHeaderDAC %s_DAC, 0, $a" % title, 46 | "\tsmpsHeaderFM %s_FM0, 0, 0" % title, 47 | "\tsmpsHeaderFM %s_FM1, 0, 0" % title, 48 | "\tsmpsHeaderFM %s_FM2, 0, 0" % title, 49 | "\tsmpsHeaderFM %s_FM3, 0, 0" % title, 50 | "\tsmpsHeaderFM %s_FM4, 0, 0" % title, 51 | "\tsmpsHeaderPSG %s_PSG6, 0, 0, 0, sTone_0C" % title, 52 | "\tsmpsHeaderPSG %s_PSG7, 0, 0, 0, sTone_0C" % title, 53 | "\tsmpsHeaderPSG %s_PSG9, 0, 0, 0, sTone_0C" % title, 54 | ] 55 | 56 | for i in range(module.get_num_channels()): 57 | order = module.subsongs[0].order[i] 58 | if i == 8: # ignored 59 | pass 60 | else: 61 | lines += ["", ""] 62 | if i < 5: # FM channels 63 | lines += ["%s_FM%d:" % (title, i)] 64 | lines += [ 65 | "\tsmpsCall %s_FM%d_%02x" % (title, i, ord_num) for ord_num in order 66 | ] 67 | elif i == 5: # DAC 68 | lines += ["%s_DAC:" % (title)] 69 | lines += [ 70 | "\tsmpsCall %s_DAC_%02x" % (title, ord_num) for ord_num in order 71 | ] 72 | else: # PSG channels 73 | lines += ["%s_PSG%d:" % (title, i)] 74 | lines += [ 75 | "\tsmpsCall %s_PSG%d_%02x" % (title, i, ord_num) 76 | for ord_num in order 77 | ] 78 | lines += ["\tsmpsStop"] 79 | 80 | avail_patterns = filter( 81 | lambda x: (x.channel == i and x.subsong == 0), module.patterns 82 | ) 83 | for p in avail_patterns: 84 | lines += [""] 85 | if i < 5: # FM channels 86 | lines += ["%s_FM%d_%02x:" % (title, i, p.index)] 87 | elif i == 5: # DAC 88 | lines += ["%s_DAC_%02x:" % (title, p.index)] 89 | else: # PSG channels 90 | lines += ["%s_PSG%d_%02x:" % (title, i, p.index)] 91 | sequence = convert.pattern_to_sequence(p) 92 | cur_instrument = -1 93 | cur_volume = -1 94 | # print(p) 95 | for s in sequence: 96 | if s.volume != cur_volume: 97 | cur_volume = s.volume 98 | lines += ["\tsmpsSetVol %d" % (cur_volume - 10)] 99 | if s.instrument != -1 and s.instrument != cur_instrument: 100 | cur_instrument = s.instrument 101 | lines += ["\tsmpsSetvoice %d" % cur_instrument] 102 | 103 | note = inter_note_to_smps[s.note] 104 | if note != "nRst": 105 | note += str(s.octave) 106 | 107 | lines += [ 108 | # "\t;%s" % str(s), 109 | "\tdc.b %s, %d" 110 | % (note, s.length) 111 | ] 112 | lines += ["\tsmpsReturn"] 113 | return lines 114 | 115 | 116 | module = FurnaceModule("files/rocky_mountain.197.fur") 117 | print("\n".join(fur2smps(module, "RockyMtn1"))) 118 | 119 | # pattern = module.get_pattern(3, 1, 0) 120 | 121 | # for i in convert.pattern_to_sequence(pattern): 122 | # print(i) 123 | -------------------------------------------------------------------------------- /mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | disallow_untyped_defs = True 3 | disallow_untyped_calls = True 4 | disallow_any_unimported = True 5 | disallow_incomplete_defs = True 6 | disallow_untyped_decorators = True 7 | check_untyped_defs = True 8 | 9 | disallow_any_generics = True 10 | disallow_subclassing_any = True 11 | warn_return_any = True 12 | 13 | warn_redundant_casts = True 14 | warn_unused_ignores = True 15 | warn_unused_configs = True 16 | warn_unreachable = True 17 | show_error_codes = True 18 | 19 | no_implicit_optional = True 20 | 21 | [mypy-*.tests.*] 22 | ; pytest decorators are not typed 23 | disallow_untyped_decorators = False -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # /usr/bin/env python 2 | 3 | import os 4 | from setuptools import setup 5 | 6 | 7 | def get_version(): 8 | import importlib.util 9 | 10 | spec = importlib.util.spec_from_file_location( 11 | "__meta__", 12 | os.path.join(os.path.dirname("__file__"), "chipchune", "__init__.py"), 13 | ) 14 | meta = importlib.util.module_from_spec(spec) 15 | spec.loader.exec_module(meta) 16 | return meta.__version__ 17 | 18 | 19 | setup( 20 | name="chipchune", 21 | version=get_version(), 22 | url="https://github.com/zoomten/chipchune", 23 | description="Library to manipulate various chiptune tracker formats.", 24 | author="Zumi Daxuya", 25 | author_email="daxuya.zumi+chipchune@proton.me", 26 | packages=[ 27 | "chipchune", 28 | "chipchune.deflemask", 29 | "chipchune.famitracker", 30 | "chipchune.furnace", 31 | "chipchune.interchange", 32 | ], 33 | license="MIT", 34 | python_requires=">=3.8", 35 | extras_require={"testing": ["coverage", "pytest", "mypy"]}, 36 | classifiers=[ 37 | "Development Status :: 2 - Pre-Alpha", 38 | "License :: OSI Approved :: MIT License", 39 | "Operating System :: OS Independent", 40 | "Programming Language :: Python", 41 | "Programming Language :: Python :: 3", 42 | "Programming Language :: Python :: 3.8", 43 | "Programming Language :: Python :: 3.9", 44 | "Programming Language :: Python :: 3.10", 45 | "Programming Language :: Python :: 3.11", 46 | "Programming Language :: Python :: 3.12", 47 | "Topic :: Multimedia :: Sound/Audio :: Editors", 48 | "Topic :: Multimedia :: Sound/Audio :: Conversion", 49 | ], 50 | ) 51 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZoomTen/chipchune/975e4b2c1922f58aa22f191bff99bcd563e45002/tests/__init__.py -------------------------------------------------------------------------------- /tests/samples/README.md: -------------------------------------------------------------------------------- 1 | Sample files. All rights reserved. 2 | -------------------------------------------------------------------------------- /tests/samples/furnace/atlantis_pv1000.144.fur: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZoomTen/chipchune/975e4b2c1922f58aa22f191bff99bcd563e45002/tests/samples/furnace/atlantis_pv1000.144.fur -------------------------------------------------------------------------------- /tests/samples/furnace/atlantis_pv1000.144x.fur: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZoomTen/chipchune/975e4b2c1922f58aa22f191bff99bcd563e45002/tests/samples/furnace/atlantis_pv1000.144x.fur -------------------------------------------------------------------------------- /tests/samples/furnace/bass.new.fui: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZoomTen/chipchune/975e4b2c1922f58aa22f191bff99bcd563e45002/tests/samples/furnace/bass.new.fui -------------------------------------------------------------------------------- /tests/samples/furnace/blank.143.fur: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZoomTen/chipchune/975e4b2c1922f58aa22f191bff99bcd563e45002/tests/samples/furnace/blank.143.fur -------------------------------------------------------------------------------- /tests/samples/furnace/blank.143.uncompressed.fur: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZoomTen/chipchune/975e4b2c1922f58aa22f191bff99bcd563e45002/tests/samples/furnace/blank.143.uncompressed.fur -------------------------------------------------------------------------------- /tests/samples/furnace/blank.143.uncompressed.no_patch_bay.fur: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZoomTen/chipchune/975e4b2c1922f58aa22f191bff99bcd563e45002/tests/samples/furnace/blank.143.uncompressed.no_patch_bay.fur -------------------------------------------------------------------------------- /tests/samples/furnace/dppt_youngster.70.fur: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZoomTen/chipchune/975e4b2c1922f58aa22f191bff99bcd563e45002/tests/samples/furnace/dppt_youngster.70.fur -------------------------------------------------------------------------------- /tests/samples/furnace/funnybones.127.fur: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZoomTen/chipchune/975e4b2c1922f58aa22f191bff99bcd563e45002/tests/samples/furnace/funnybones.127.fur -------------------------------------------------------------------------------- /tests/samples/furnace/lawnstring.new.fui: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZoomTen/chipchune/975e4b2c1922f58aa22f191bff99bcd563e45002/tests/samples/furnace/lawnstring.new.fui -------------------------------------------------------------------------------- /tests/samples/furnace/macros.70.fur: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZoomTen/chipchune/975e4b2c1922f58aa22f191bff99bcd563e45002/tests/samples/furnace/macros.70.fur -------------------------------------------------------------------------------- /tests/samples/furnace/mahbod-intro.143.fur: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZoomTen/chipchune/975e4b2c1922f58aa22f191bff99bcd563e45002/tests/samples/furnace/mahbod-intro.143.fur -------------------------------------------------------------------------------- /tests/samples/furnace/mahbod-intro.181.fur: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZoomTen/chipchune/975e4b2c1922f58aa22f191bff99bcd563e45002/tests/samples/furnace/mahbod-intro.181.fur -------------------------------------------------------------------------------- /tests/samples/furnace/map04.140.fur: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZoomTen/chipchune/975e4b2c1922f58aa22f191bff99bcd563e45002/tests/samples/furnace/map04.140.fur -------------------------------------------------------------------------------- /tests/samples/furnace/map04.140.uncompressed.fur: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZoomTen/chipchune/975e4b2c1922f58aa22f191bff99bcd563e45002/tests/samples/furnace/map04.140.uncompressed.fur -------------------------------------------------------------------------------- /tests/samples/furnace/opl1_brass.new.fui: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZoomTen/chipchune/975e4b2c1922f58aa22f191bff99bcd563e45002/tests/samples/furnace/opl1_brass.new.fui -------------------------------------------------------------------------------- /tests/samples/furnace/opl1_brass.old.fui: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZoomTen/chipchune/975e4b2c1922f58aa22f191bff99bcd563e45002/tests/samples/furnace/opl1_brass.old.fui -------------------------------------------------------------------------------- /tests/samples/furnace/opldrums.70.fur: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZoomTen/chipchune/975e4b2c1922f58aa22f191bff99bcd563e45002/tests/samples/furnace/opldrums.70.fur -------------------------------------------------------------------------------- /tests/samples/furnace/r32.127.fur: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZoomTen/chipchune/975e4b2c1922f58aa22f191bff99bcd563e45002/tests/samples/furnace/r32.127.fur -------------------------------------------------------------------------------- /tests/samples/furnace/skate_or_die.143.fur: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZoomTen/chipchune/975e4b2c1922f58aa22f191bff99bcd563e45002/tests/samples/furnace/skate_or_die.143.fur -------------------------------------------------------------------------------- /tests/samples/furnace/skate_or_die.143.uncompressed.fur: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZoomTen/chipchune/975e4b2c1922f58aa22f191bff99bcd563e45002/tests/samples/furnace/skate_or_die.143.uncompressed.fur -------------------------------------------------------------------------------- /tests/samples/furnace/skate_or_die.181.fur: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZoomTen/chipchune/975e4b2c1922f58aa22f191bff99bcd563e45002/tests/samples/furnace/skate_or_die.181.fur -------------------------------------------------------------------------------- /tests/samples/furnace/skate_or_die.181.uncompressed.fur: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZoomTen/chipchune/975e4b2c1922f58aa22f191bff99bcd563e45002/tests/samples/furnace/skate_or_die.181.uncompressed.fur -------------------------------------------------------------------------------- /tests/samples/furnace/skate_or_die.181.wave.1.fuw: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZoomTen/chipchune/975e4b2c1922f58aa22f191bff99bcd563e45002/tests/samples/furnace/skate_or_die.181.wave.1.fuw -------------------------------------------------------------------------------- /tests/samples/furnace/skate_or_die.70.fur: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZoomTen/chipchune/975e4b2c1922f58aa22f191bff99bcd563e45002/tests/samples/furnace/skate_or_die.70.fur -------------------------------------------------------------------------------- /tests/samples/furnace/skate_or_die.70.uncompressed.fur: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZoomTen/chipchune/975e4b2c1922f58aa22f191bff99bcd563e45002/tests/samples/furnace/skate_or_die.70.uncompressed.fur -------------------------------------------------------------------------------- /tests/samples/furnace/spooky_birthday.181.fur: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZoomTen/chipchune/975e4b2c1922f58aa22f191bff99bcd563e45002/tests/samples/furnace/spooky_birthday.181.fur -------------------------------------------------------------------------------- /tests/samples/furnace/spooky_birthday.181.uncompressed.fur: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZoomTen/chipchune/975e4b2c1922f58aa22f191bff99bcd563e45002/tests/samples/furnace/spooky_birthday.181.uncompressed.fur -------------------------------------------------------------------------------- /tests/samples/furnace/tsu.new.fui: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZoomTen/chipchune/975e4b2c1922f58aa22f191bff99bcd563e45002/tests/samples/furnace/tsu.new.fui -------------------------------------------------------------------------------- /tests/samples/furnace/tsu.old.fui: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZoomTen/chipchune/975e4b2c1922f58aa22f191bff99bcd563e45002/tests/samples/furnace/tsu.old.fui -------------------------------------------------------------------------------- /tests/samples/furnace/viridian.127.fur: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZoomTen/chipchune/975e4b2c1922f58aa22f191bff99bcd563e45002/tests/samples/furnace/viridian.127.fur -------------------------------------------------------------------------------- /tests/samples/furnace/viridian.181.fur: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZoomTen/chipchune/975e4b2c1922f58aa22f191bff99bcd563e45002/tests/samples/furnace/viridian.181.fur -------------------------------------------------------------------------------- /tests/samples/furnace/viridian.70.fur: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZoomTen/chipchune/975e4b2c1922f58aa22f191bff99bcd563e45002/tests/samples/furnace/viridian.70.fur -------------------------------------------------------------------------------- /tests/samples/furnace/waveta.new.fui: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZoomTen/chipchune/975e4b2c1922f58aa22f191bff99bcd563e45002/tests/samples/furnace/waveta.new.fui -------------------------------------------------------------------------------- /tests/samples/furnace/waveta.old.fui: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZoomTen/chipchune/975e4b2c1922f58aa22f191bff99bcd563e45002/tests/samples/furnace/waveta.old.fui -------------------------------------------------------------------------------- /tests/test_furnace.py: -------------------------------------------------------------------------------- 1 | from chipchune.furnace.module import ( 2 | FurnaceModule, 3 | PatchBay, 4 | InputPatchBayEntry, 5 | OutputPatchBayEntry, 6 | ) 7 | from chipchune.furnace.instrument import FurnaceInstrument 8 | from chipchune.furnace.wavetable import FurnaceWavetable 9 | from chipchune.furnace.data_types import InsFeatureFM, InsFeatureMacro, SingleMacro 10 | from chipchune.furnace.enums import MacroCode 11 | from typing import Union 12 | 13 | import pytest 14 | 15 | from chipchune.furnace.wavetable import FurnaceWavetable 16 | 17 | # pytest --cov=chipchune 18 | 19 | 20 | @pytest.fixture 21 | def dev_70() -> FurnaceModule: 22 | return FurnaceModule("samples/furnace/skate_or_die.70.fur") 23 | 24 | 25 | @pytest.fixture 26 | def dev_143() -> FurnaceModule: 27 | return FurnaceModule("samples/furnace/skate_or_die.143.fur") 28 | 29 | 30 | @pytest.fixture 31 | def dev_181() -> FurnaceModule: 32 | return FurnaceModule("samples/furnace/skate_or_die.181.fur") 33 | 34 | 35 | @pytest.fixture 36 | def dev_140() -> FurnaceModule: 37 | return FurnaceModule("samples/furnace/map04.140.fur") 38 | 39 | 40 | @pytest.fixture 41 | def new_ins() -> FurnaceInstrument: 42 | return FurnaceInstrument("samples/furnace/opl1_brass.new.fui") 43 | 44 | 45 | @pytest.fixture 46 | def new_ins_2() -> FurnaceInstrument: 47 | return FurnaceInstrument("samples/furnace/bass.new.fui") 48 | 49 | 50 | @pytest.fixture 51 | def old_ins() -> FurnaceInstrument: 52 | return FurnaceInstrument("samples/furnace/opl1_brass.old.fui") 53 | 54 | 55 | @pytest.fixture 56 | def wav_181() -> FurnaceWavetable: 57 | return FurnaceWavetable("samples/furnace/skate_or_die.181.wave.1.fuw") 58 | 59 | 60 | def test_meta(dev_70: FurnaceModule, dev_143: FurnaceModule) -> None: 61 | # verify correct metadata 62 | assert dev_70.meta.name == "Skate or Die - Title Theme" 63 | assert dev_70.meta.author == "Rob Hubbard '87, cv:Zumi '22" 64 | assert dev_143.meta.album == "" 65 | assert dev_143.meta.sys_name == "PC Engine/TurboGrafx-16 + AY-3-8910 + AY-3-8910" 66 | 67 | # verify integrity 68 | assert dev_70.meta.name == dev_143.meta.name 69 | assert dev_70.meta.author == dev_143.meta.author 70 | 71 | 72 | def test_info(dev_70: FurnaceModule, dev_143: FurnaceModule) -> None: 73 | # verify info 74 | assert dev_70.meta.tuning == 440.0 75 | assert dev_70.subsongs[0].pattern_length == 64 76 | 77 | # verify integrity 78 | assert dev_70.meta.tuning == dev_143.meta.tuning 79 | assert dev_70.subsongs[0].pattern_length == dev_143.subsongs[0].pattern_length 80 | 81 | 82 | def test_compat_flags(dev_70: FurnaceModule, dev_143: FurnaceModule) -> None: 83 | # make sure default compat flags match 84 | for k, v in dev_70.compat_flags.__dict__.items(): 85 | assert v == dev_143.compat_flags.__dict__[k], ( 86 | "compat_flags.%s does not match" % k 87 | ) 88 | 89 | 90 | def test_old2new_chip_flag_convert( 91 | dev_70: FurnaceModule, dev_143: FurnaceModule 92 | ) -> None: 93 | # make sure old chip flags are correctly converted to new ones 94 | for i in range(len(dev_70.chips.list)): 95 | for k, v in dev_70.chips.list[i].flags.items(): 96 | if k in dev_143.chips.list[i].flags.keys(): 97 | assert ( 98 | v == dev_143.chips.list[i].flags[k] 99 | ), 'chips.list[%d].flags["%s"] does not match' % (i, k) 100 | 101 | 102 | @pytest.mark.skip 103 | def test_patchbay(dev_143: FurnaceModule) -> None: 104 | pass 105 | 106 | 107 | # panning and volume values are not tested due to negligible rounding errors 108 | 109 | 110 | def test_timing(dev_70: FurnaceModule, dev_143: FurnaceModule) -> None: 111 | # verify dev70 info 112 | assert dev_70.subsongs[0].timing.clock_speed == 60.0 113 | assert dev_70.subsongs[0].timing.speed == (3, 3) 114 | assert dev_70.subsongs[0].timing.timebase == 1 115 | assert dev_70.subsongs[0].timing.highlight == (8, 32) 116 | 117 | # verify dev143 info 118 | assert dev_143.subsongs[0].speed_pattern == [3, 3] 119 | assert len(dev_143.subsongs[0].grooves) == 0 120 | assert dev_143.subsongs[0].timing.virtual_tempo == (150, 150) 121 | 122 | # verify integrity 123 | assert ( 124 | dev_70.subsongs[0].timing.clock_speed == dev_143.subsongs[0].timing.clock_speed 125 | ) 126 | assert dev_70.subsongs[0].timing.speed == dev_143.subsongs[0].timing.speed 127 | assert dev_70.subsongs[0].timing.timebase == dev_143.subsongs[0].timing.timebase 128 | assert dev_70.subsongs[0].timing.highlight == dev_143.subsongs[0].timing.highlight 129 | 130 | 131 | def test_dev140_module(dev_140: FurnaceModule) -> None: 132 | assert dev_140.meta.name == 'MAP04 "Between Levels"' 133 | assert dev_140.meta.author == "Bobby Prince '94, cv: Zumi '23" 134 | assert dev_140.meta.album == "Doom II" 135 | assert dev_140.meta.sys_name == "tildearrow Sound Unit" 136 | assert dev_140.compat_flags.auto_sys_name is True 137 | assert dev_140.meta.tuning == 444.0 138 | 139 | 140 | def test_new_ins_name(new_ins: FurnaceInstrument) -> None: 141 | assert new_ins.get_name() == "Brass Lead" 142 | 143 | 144 | def test_new_ins_has_fm(new_ins: FurnaceInstrument) -> None: 145 | # check for existence of fm 146 | fm = None 147 | for i in new_ins.features: 148 | if isinstance(i, InsFeatureFM): 149 | fm = i 150 | assert isinstance(fm, InsFeatureFM) 151 | assert fm.fb == 7 152 | assert fm.alg == 0 153 | assert fm.ops == 2 154 | 155 | 156 | def test_new_ins_has_macro(new_ins_2: FurnaceInstrument) -> None: 157 | assert new_ins_2.get_name() == "bass" 158 | mac = None 159 | vol = None 160 | res = None 161 | for i in new_ins_2.features: 162 | if isinstance(i, InsFeatureMacro): 163 | mac = i 164 | assert isinstance(mac, InsFeatureMacro) 165 | for j in mac.macros: 166 | if j.kind == MacroCode.VOL: 167 | vol = j 168 | elif j.kind == MacroCode.EX1: 169 | res = j 170 | assert vol is not None 171 | assert vol.data == [1254, 921, 819, 729, 652, 576, 537, 486] 172 | assert res.data == [1] 173 | 174 | 175 | def test_if_old_instr_the_same(dev_70: FurnaceModule, dev_143: FurnaceModule) -> None: 176 | assert len(dev_70.instruments) == len(dev_143.instruments) 177 | inslens = len(dev_70.instruments) 178 | 179 | # get macro from A 180 | for e in range(inslens): 181 | a = dev_70.instruments[e] 182 | b = dev_143.instruments[e] 183 | assert a.get_name() == b.get_name() 184 | assert a.meta.type == b.meta.type 185 | try: 186 | for i in a.features: 187 | if type(i) is InsFeatureMacro: 188 | a_vol = next(filter(lambda x: x.kind == MacroCode.VOL, i.macros)) 189 | a_arp = next(filter(lambda x: x.kind == MacroCode.ARP, i.macros)) 190 | a_duty = next(filter(lambda x: x.kind == MacroCode.DUTY, i.macros)) 191 | a_wave = next(filter(lambda x: x.kind == MacroCode.WAVE, i.macros)) 192 | 193 | for i in b.features: 194 | if type(i) is InsFeatureMacro: 195 | b_vol = next(filter(lambda x: x.kind == MacroCode.VOL, i.macros)) 196 | b_arp = next(filter(lambda x: x.kind == MacroCode.ARP, i.macros)) 197 | b_duty = next(filter(lambda x: x.kind == MacroCode.DUTY, i.macros)) 198 | b_wave = next(filter(lambda x: x.kind == MacroCode.WAVE, i.macros)) 199 | 200 | assert a_vol == b_vol 201 | assert a_arp == b_arp 202 | assert a_duty == b_duty 203 | assert a_wave == b_wave 204 | except StopIteration: 205 | continue 206 | 207 | 208 | def test_old_new_pettern_match(dev_143: FurnaceModule, dev_181: FurnaceModule) -> None: 209 | # make sure old & new patterns match 210 | assert dev_143.patterns == dev_181.patterns 211 | 212 | 213 | def test_wavetables(dev_181: FurnaceModule, wav_181: FurnaceWavetable) -> None: 214 | # verify wavetables 215 | assert len(dev_181.wavetables) == 7 216 | assert dev_181.wavetables[1].data == wav_181.data 217 | 218 | assert wav_181.meta.width == 32 219 | assert wav_181.meta.height == 32 220 | assert wav_181.data == [ 221 | 7, 222 | 19, 223 | 31, 224 | 25, 225 | 23, 226 | 21, 227 | 19, 228 | 16, 229 | 7, 230 | 1, 231 | 0, 232 | 0, 233 | 16, 234 | 14, 235 | 25, 236 | 24, 237 | 23, 238 | 21, 239 | 20, 240 | 20, 241 | 17, 242 | 31, 243 | 6, 244 | 9, 245 | 11, 246 | 13, 247 | 16, 248 | 19, 249 | 20, 250 | 22, 251 | 25, 252 | 30, 253 | ] 254 | --------------------------------------------------------------------------------