ys2-intro/loader/samples/minexample/furC64/chipchune/furnace/module.py

1112 lines
46 KiB
Python
Raw Normal View History

2025-11-26 07:42:34 -05:00
import re
import zlib
from io import BytesIO, BufferedReader
from typing import BinaryIO, Optional, Literal, Union, Dict, List
from chipchune._util import read_byte, read_short, read_int, read_float, read_str
from .data_types import (
ModuleMeta, ChipList, ModuleCompatFlags, SubSong, PatchBay, ChannelDisplayInfo,
InputPatchBayEntry, OutputPatchBayEntry, ChipInfo, FurnacePattern, FurnaceRow
)
from .enums import (
ChipType, LinearPitch, InputPortSet, OutputPortSet, LoopModality,
DelayBehavior, JumpTreatment, _FurInsImportType, _FurWavetableImportType, Note
)
from .instrument import FurnaceInstrument
from .wavetable import FurnaceWavetable
from .sample import FurnaceSample
MAGIC_STR = b'-Furnace module-'
MAX_CHIPS = 32
class FurnaceModule:
"""
Represents a Furnace .fur file.
When possible, instrument objects etc. will use the latest format as its internal
representation. For example, old instruments will internally be converted into the
"new" instrument-feature-list format.
"""
def __init__(self, file_name_or_stream: Optional[Union[BufferedReader, str]] = None) -> None:
"""
Creates or opens a new Furnace module as a Python object.
:param file_name_or_stream: (Optional)
If specified, then it will parse a file as a FurnaceModule. If file name (str) is
given, it will load that file. If a stream (BufferedReader) instead is given,
it will parse it from the stream.
Defaults to None.
"""
self.file_name: Optional[str] = None
"""
Original file name, if the object was initialized with one.
"""
self.meta: ModuleMeta = ModuleMeta()
"""
Metadata concerning the module.
"""
self.chips: ChipList = ChipList()
"""
List of chips used in the module.
"""
self.compat_flags: ModuleCompatFlags = ModuleCompatFlags()
"""
Compat flags settings within the module.
"""
self.subsongs: List[SubSong] = [SubSong()]
"""
Subsongs contained within the module. Although the first subsong
and the others are internally stored separately, they're organized
into a list here for convenience.
"""
self.patchbay: List[PatchBay] = []
"""
List of patchbay connections.
"""
self.instruments: List[FurnaceInstrument] = []
"""
List of all instruments in the module.
"""
self.patterns: List[FurnacePattern] = []
"""
List of all patterns in the module.
"""
self.wavetables: List[FurnaceWavetable] = []
self.samples: List[FurnaceWavetable] = []
if isinstance(file_name_or_stream, BufferedReader):
self.load_from_stream(file_name_or_stream)
elif isinstance(file_name_or_stream, str):
self.load_from_file(file_name_or_stream)
def load_from_file(self, file_name: Optional[str] = None) -> None:
"""
Load a module from a file name. The file may either be compressed or uncompressed.
:param file_name: If not specified, it will grab from self.file_name instead.
"""
if isinstance(file_name, str):
self.file_name = file_name
if self.file_name is None:
raise RuntimeError('No file name set, either set self.file_name or pass file_name to the function')
with open(self.file_name, 'rb') as f:
detect_magic = f.peek(len(MAGIC_STR))[:len(MAGIC_STR)]
if detect_magic != MAGIC_STR: # this is probably compressed, so try decompressing it first
return self.load_from_bytes(
zlib.decompress(f.read())
)
else: # uncompressed for sure
return self.load_from_stream(f)
@staticmethod
def decompress_to_file(in_name: str, out_name: str) -> int:
"""
Simple zlib wrapper. Decompresses a zlib-compressed .fur
from in_name to out_name. Does not need instantiation.
:param in_name: input file name
:param out_name: output file name
:return: Results of file.write().
"""
with open(in_name, 'rb') as fi:
with open(out_name, 'wb') as fo:
return fo.write(zlib.decompress(fi.read()))
def load_from_bytes(self, data: bytes) -> None:
"""
Load a module from a series of bytes.
:param data: Bytes
"""
return self.load_from_stream(
BytesIO(data)
)
def load_from_stream(self, stream: BinaryIO) -> None:
"""
Load a module from an **uncompressed** stream.
:param stream: File-like object containing the uncompressed module.
"""
# assumes uncompressed stream
if stream.read(len(MAGIC_STR)) != MAGIC_STR:
raise RuntimeError('Bad magic value; this is not a Furnace file or is corrupt')
# clear defaults
self.chips.list.clear()
self.patchbay.clear()
self.subsongs[0].order.clear()
self.subsongs[0].speed_pattern.clear()
self.__read_header(stream)
self.__init_compat_flags()
self.__read_info(stream)
if self.meta.version >= 119:
self.__read_dev119_chip_flags(stream)
self.__read_instruments(stream)
self.__read_wavetables(stream)
self.__read_samples(stream)
if self.meta.version >= 95:
self.__read_subsongs(stream)
self.__read_patterns(stream)
def get_num_channels(self) -> int:
"""
Retrieve the number of total channels in the module.
:return: Channel sum across all chips.
"""
num_channels = 0
for chip in self.chips.list:
num_channels += chip.type.channels
return num_channels
def get_pattern(self, channel: int, index: int, subsong: int=0) -> Optional[FurnacePattern]:
"""
Gets one pattern object from a module.
:param channel: Which channel to use (zero-indexed), e.g. to get VRC6
in a NES+VRC6 module, use `5`.
:param index: The index of the pattern within the subsong.
:param subsong: The subsong number.
:return: FurnacePattern object or None if no such pattern exists.
"""
try:
return next(
filter(lambda x: x.channel==channel and x.index==index and x.subsong==subsong, self.patterns)
)
except StopIteration:
return None
def __init_compat_flags(self) -> None:
"""
Initializes appropriate compat flags based on module version
"""
if self.meta.version < 37:
self.compat_flags.limit_slides = True
self.compat_flags.linear_pitch = LinearPitch.ONLY_PITCH_CHANGE
self.compat_flags.loop_modality = LoopModality.HARD_RESET_CHANNELS
if self.meta.version < 43:
self.compat_flags.proper_noise_layout = False
self.compat_flags.wave_duty_is_volume = False
if self.meta.version < 45:
self.compat_flags.reset_macro_on_porta = True
self.compat_flags.legacy_volume_slides = True
self.compat_flags.compatible_arpeggio = True
self.compat_flags.note_off_resets_slides = True
self.compat_flags.target_resets_slides = True
if self.meta.version < 46:
self.compat_flags.arpeggio_inhibits_portamento = True
self.compat_flags.wack_algorithm_macro = True
if self.meta.version < 49:
self.compat_flags.broken_shortcut_slides = True
if self.meta.version < 50:
self.compat_flags.ignore_duplicates_slides = False
if self.meta.version < 62:
self.compat_flags.stop_portamento_on_note_off = True
if self.meta.version < 64:
self.compat_flags.broken_dac_mode = False
if self.meta.version < 65:
self.compat_flags.one_tick_cut = False
if self.meta.version < 66:
self.compat_flags.instrument_change_allowed_in_porta = False
if self.meta.version < 69:
self.compat_flags.reset_note_base_on_arpeggio_stop = False
if self.meta.version < 71:
self.compat_flags.no_slides_on_first_tick = False
self.compat_flags.next_row_reset_arp_pos = False
self.compat_flags.ignore_jump_at_end = True
if self.meta.version < 72:
self.compat_flags.buggy_portamento_after_slide = True
self.compat_flags.gb_ins_affects_env = False
if self.meta.version < 78:
self.compat_flags.shared_extch_state = False
if self.meta.version < 83:
self.compat_flags.ignore_outside_dac_mode_change = True
self.compat_flags.e1e2_takes_priority = False
if self.meta.version < 84:
self.compat_flags.new_sega_pcm = False
if self.meta.version < 85:
self.compat_flags.weird_fnum_pitch_slides = True
if self.meta.version < 86:
self.compat_flags.sn_duty_resets_phase = True
if self.meta.version < 90:
self.compat_flags.linear_pitch_macro = False
if self.meta.version < 97:
self.compat_flags.old_octave_boundary = True
self.compat_flags.disable_opn2_dac_volume_control = True # dev98
if self.meta.version < 99:
self.compat_flags.new_volume_scaling = False
self.compat_flags.volume_macro_lingers = False
self.compat_flags.broken_out_vol = True
if self.meta.version < 100:
self.compat_flags.e1e2_stop_on_same_note = False
if self.meta.version < 101:
self.compat_flags.broken_porta_after_arp = True
if self.meta.version < 108:
self.compat_flags.sn_no_low_periods = True
if self.meta.version < 110:
self.compat_flags.cut_delay_effect_policy = DelayBehavior.BROKEN
if self.meta.version < 113:
self.compat_flags.jump_treatment = JumpTreatment.FIRST_JUMP_ONLY
if self.meta.version < 115:
self.compat_flags.auto_sys_name = True
if self.meta.version < 117:
self.compat_flags.disable_sample_macro = True
if self.meta.version < 121:
self.compat_flags.broken_out_vol_2 = False
if self.meta.version < 130:
self.compat_flags.old_arp_strategy = True
if self.meta.version < 138:
self.compat_flags.broken_porta_during_legato = True
if self.meta.version < 155:
self.compat_flags.broken_fm_off = True
if self.meta.version < 168:
self.compat_flags.pre_note_no_effect = True
if self.meta.version < 183:
self.compat_flags.old_dpcm = True
if self.meta.version < 184:
self.compat_flags.reset_arp_phase_on_new_note = False
if self.meta.version < 188:
self.compat_flags.ceil_volume_scaling = False
if self.meta.version < 191:
self.compat_flags.old_always_set_volume = True
if self.meta.version < 200:
self.compat_flags.old_sample_offset = True
# XXX: update my signature whenever a new compat flag block is added
def __read_compat_flags(self, stream: BinaryIO, phase: Literal[1, 2, 3]) -> None:
"""
Reads the set compat flags in the module
"""
if phase == 1:
compat_flags_to_skip = 20
if self.meta.version < 37:
self.compat_flags.limit_slides = True
self.compat_flags.linear_pitch = LinearPitch.ONLY_PITCH_CHANGE
self.compat_flags.loop_modality = LoopModality.HARD_RESET_CHANNELS
else: # >= 37
self.compat_flags.limit_slides = bool(read_byte(stream))
self.compat_flags.linear_pitch = LinearPitch(read_byte(stream))
self.compat_flags.loop_modality = LoopModality(read_byte(stream))
compat_flags_to_skip -= 3
if self.meta.version >= 43:
self.compat_flags.proper_noise_layout = bool(read_byte(stream))
self.compat_flags.wave_duty_is_volume = bool(read_byte(stream))
compat_flags_to_skip -= 2
if self.meta.version >= 45:
self.compat_flags.reset_macro_on_porta = bool(read_byte(stream))
self.compat_flags.legacy_volume_slides = bool(read_byte(stream))
self.compat_flags.compatible_arpeggio = bool(read_byte(stream))
self.compat_flags.note_off_resets_slides = bool(read_byte(stream))
self.compat_flags.target_resets_slides = bool(read_byte(stream))
compat_flags_to_skip -= 5
if self.meta.version >= 47:
self.compat_flags.arpeggio_inhibits_portamento = bool(read_byte(stream))
self.compat_flags.wack_algorithm_macro = bool(read_byte(stream))
compat_flags_to_skip -= 2
if self.meta.version >= 49:
self.compat_flags.broken_shortcut_slides = bool(read_byte(stream))
compat_flags_to_skip -= 1
if self.meta.version >= 50:
self.compat_flags.ignore_duplicates_slides = bool(read_byte(stream))
compat_flags_to_skip -= 1
if self.meta.version >= 62:
self.compat_flags.stop_portamento_on_note_off = bool(read_byte(stream))
self.compat_flags.continuous_vibrato = bool(read_byte(stream))
compat_flags_to_skip -= 2
if self.meta.version >= 64:
self.compat_flags.broken_dac_mode = bool(read_byte(stream))
compat_flags_to_skip -= 1
if self.meta.version >= 65:
self.compat_flags.one_tick_cut = bool(read_byte(stream))
compat_flags_to_skip -= 1
if self.meta.version >= 66:
self.compat_flags.instrument_change_allowed_in_porta = bool(read_byte(stream))
compat_flags_to_skip -= 1
if self.meta.version >= 69:
self.compat_flags.reset_note_base_on_arpeggio_stop = bool(read_byte(stream))
compat_flags_to_skip -= 1
elif phase == 2:
compat_flags_to_skip = 28
if self.meta.version >= 70:
self.compat_flags.broken_speed_selection = bool(read_byte(stream))
compat_flags_to_skip -= 1
if self.meta.version >= 71:
self.compat_flags.no_slides_on_first_tick = bool(read_byte(stream))
self.compat_flags.next_row_reset_arp_pos = bool(read_byte(stream))
self.compat_flags.ignore_jump_at_end = bool(read_byte(stream))
compat_flags_to_skip -= 3
if self.meta.version >= 72:
self.compat_flags.buggy_portamento_after_slide = bool(read_byte(stream))
self.compat_flags.gb_ins_affects_env = bool(read_byte(stream))
compat_flags_to_skip -= 2
if self.meta.version >= 78:
self.compat_flags.shared_extch_state = bool(read_byte(stream))
compat_flags_to_skip -= 1
if self.meta.version >= 83:
self.compat_flags.ignore_outside_dac_mode_change = bool(read_byte(stream))
self.compat_flags.e1e2_takes_priority = bool(read_byte(stream))
compat_flags_to_skip -= 2
if self.meta.version >= 84:
self.compat_flags.new_sega_pcm = bool(read_byte(stream))
compat_flags_to_skip -= 1
if self.meta.version >= 85:
self.compat_flags.weird_fnum_pitch_slides = bool(read_byte(stream))
compat_flags_to_skip -= 1
if self.meta.version >= 86:
self.compat_flags.sn_duty_resets_phase = bool(read_byte(stream))
compat_flags_to_skip -= 1
if self.meta.version >= 90:
self.compat_flags.linear_pitch_macro = bool(read_byte(stream))
compat_flags_to_skip -= 1
if self.meta.version >= 94:
self.compat_flags.pitch_slide_speed_in_linear = read_byte(stream)
compat_flags_to_skip -= 1
if self.meta.version >= 97:
self.compat_flags.old_octave_boundary = bool(read_byte(stream))
compat_flags_to_skip -= 1
if self.meta.version >= 98:
self.compat_flags.disable_opn2_dac_volume_control = bool(read_byte(stream))
compat_flags_to_skip -= 1
if self.meta.version >= 99:
self.compat_flags.new_volume_scaling = bool(read_byte(stream))
self.compat_flags.volume_macro_lingers = bool(read_byte(stream))
self.compat_flags.broken_out_vol = bool(read_byte(stream))
compat_flags_to_skip -= 3
if self.meta.version >= 100:
self.compat_flags.e1e2_stop_on_same_note = bool(read_byte(stream))
compat_flags_to_skip -= 1
if self.meta.version >= 101:
self.compat_flags.broken_porta_after_arp = bool(read_byte(stream))
compat_flags_to_skip -= 1
if self.meta.version >= 108:
self.compat_flags.sn_no_low_periods = bool(read_byte(stream))
compat_flags_to_skip -= 1
if self.meta.version >= 110:
self.compat_flags.cut_delay_effect_policy = DelayBehavior(read_byte(stream))
compat_flags_to_skip -= 1
if self.meta.version >= 113:
self.compat_flags.jump_treatment = JumpTreatment(read_byte(stream))
compat_flags_to_skip -= 1
if self.meta.version >= 115:
self.compat_flags.auto_sys_name = bool(read_byte(stream))
compat_flags_to_skip -= 1
if self.meta.version >= 117:
self.compat_flags.disable_sample_macro = bool(read_byte(stream))
compat_flags_to_skip -= 1
if self.meta.version >= 121:
self.compat_flags.broken_out_vol_2 = bool(read_byte(stream))
compat_flags_to_skip -= 1
if self.meta.version >= 130:
self.compat_flags.old_arp_strategy = bool(read_byte(stream))
compat_flags_to_skip -= 1
elif phase == 3:
compat_flags_to_skip = 8
if self.meta.version >= 138:
self.compat_flags.broken_porta_during_legato = bool(read_byte(stream))
compat_flags_to_skip -= 1
if self.meta.version >= 155:
self.compat_flags.broken_fm_off = bool(read_byte(stream))
compat_flags_to_skip -= 1
if self.meta.version >= 168:
self.compat_flags.pre_note_no_effect = bool(read_byte(stream))
compat_flags_to_skip -= 1
if self.meta.version >= 183:
self.compat_flags.old_dpcm = bool(read_byte(stream))
compat_flags_to_skip -= 1
if self.meta.version >= 184:
self.compat_flags.reset_arp_phase_on_new_note = bool(read_byte(stream))
compat_flags_to_skip -= 1
if self.meta.version >= 188:
self.compat_flags.ceil_volume_scaling = bool(read_byte(stream))
compat_flags_to_skip -= 1
if self.meta.version >= 191:
self.compat_flags.old_always_set_volume = bool(read_byte(stream))
compat_flags_to_skip -= 1
else:
raise ValueError(
'Compat flag phase must be in between: 1, 2, 3'
)
stream.read(compat_flags_to_skip)
def __read_dev119_chip_flags(self, stream: BinaryIO) -> None:
for i in range(len(self.chips.list)):
# skip if this chip doesn't have flags
if self.__chip_flag_ptr[i] == 0:
continue
stream.seek(self.__chip_flag_ptr[i])
if stream.read(4) != b'FLAG':
raise ValueError('No "FLAG" magic')
# i assume this will grow, you never know
blk_size = read_int(stream)
flag_blk = BytesIO(stream.read(blk_size))
# read entries in FLAG
for entry in [flag.split('=') for flag in read_str(flag_blk).split()]:
key = entry[0]
value = entry[1]
# cast by regex
if re.match(r'true', value):
self.chips.list[i].flags[key] = True
elif re.match(r'false', value):
self.chips.list[i].flags[key] = False
elif re.match(r'\d+$', value):
self.chips.list[i].flags[key] = int(value)
elif re.match(r'\d+\.\d+', value):
self.chips.list[i].flags[key] = float(value)
else: # all other values should be treated as a string
self.chips.list[i].flags[key] = value
@staticmethod
def __convert_old_chip_flags(chip: ChipType, flag: int) -> Dict[str, Union[bool, int]]:
"""
Convert pre-v119 binary chip flags to the newer dict-style form.
:param chip: ChipType
:param flag: flag value as a 32-bit number
:return: dictionary containing the flag's equivalent values
"""
n = {}
if chip in [ChipType.GENESIS, ChipType.GENESIS_EX]:
n['clockSel'] = flag & 2147483647 # bits 0-30
n['ladderEffect'] = bool((flag >> 31) & 1)
elif chip == ChipType.SMS:
cs = flag & 0xff03
if cs > 0x100:
cs = cs - 252 # 0x100 + 4
n['clockSel'] = cs
ct = (flag & 0xcc) // 4
if ct >= 32:
ct -= 24
elif ct >= 16:
ct -= 12
n['chipType'] = ct
n['noPhaseReset'] = flag >> 4
elif chip == ChipType.GB:
n['chipType'] = flag & 0b11
n['noAntiClick'] = bool((flag >> 3) & 1)
elif chip == ChipType.PCE:
n['clockSel'] = flag & 1
n['chipType'] = (flag >> 2) & 1
n['noAntiClick'] = bool((flag >> 3) & 1)
elif chip in [ChipType.NES, ChipType.VRC6, ChipType.FDS, ChipType.MMC5]:
n['clockSel'] = flag & 0b11
elif chip in [ChipType.C64_8580, ChipType.C64_6581]:
n['clockSel'] = flag & 0b1111
elif chip == ChipType.SEGA_ARCADE:
n['clockSel'] = flag & 0b11111111
elif chip in [ChipType.NEO_GEO_CD, ChipType.NEO_GEO, ChipType.NEO_GEO_EX,
ChipType.NEO_GEO_CD_EX, ChipType.YM2610B, ChipType.YM2610B_EX]:
n['clockSel'] = flag & 0b11111111
elif chip == ChipType.AY38910:
n['clockSel'] = flag & 0b1111
n['chipType'] = (flag >> 4) & 0b11
n['stereo'] = bool((flag >> 6) & 1)
n['halfClock'] = bool((flag >> 7) & 1)
n['stereoSep'] = (flag >> 8) & 0b11111111
elif chip == ChipType.AMIGA:
n['clockSel'] = flag & 1
n['chipType'] = (flag >> 1) & 1
n['bypassLimits'] = bool((flag >> 2) & 1)
n['stereoSep'] = (flag >> 8) & 0b1111111
elif chip == ChipType.YM2151:
n['clockSel'] = flag & 0b11111111
elif chip in [ChipType.YM2612, ChipType.YM2612_EX, ChipType.YM2612_PLUS,
ChipType.YM2612_PLUS_EX]:
n['clockSel'] = flag & 2147483647 # bits 0-30
n['ladderEffect'] = bool((flag >> 31) & 1)
elif chip == ChipType.TIA:
n['clockSel'] = flag & 1
n['mixingType'] = (flag >> 1) & 0b11
elif chip == ChipType.VIC20:
n['clockSel'] = flag & 1
elif chip == ChipType.SNES:
n['volScaleL'] = flag & 0b1111111
n['volScaleR'] = (flag >> 8) & 0b1111111
elif chip in [ChipType.OPLL, ChipType.OPLL_DRUMS]:
n['clockSel'] = flag & 0b1111
n['patchSet'] = flag >> 4 # safe
elif chip == ChipType.N163:
n['clockSel'] = flag & 0b1111
n['channels'] = (flag >> 4) & 0b111
n['multiplex'] = bool((flag >> 7) & 1)
elif chip in [ChipType.OPN, ChipType.YM2203_EX]:
n['clockSel'] = flag & 0b11111
n['prescale'] = (flag >> 5) & 0b11
elif chip in [ChipType.OPL, ChipType.OPL_DRUMS, ChipType.OPL2, ChipType.OPL2_DRUMS,
ChipType.Y8950, ChipType.Y8950_DRUMS]:
n['clockSel'] = flag & 0b11111111
elif chip in [ChipType.OPL3, ChipType.OPL3_DRUMS]:
n['clockSel'] = flag & 0b11111111
elif chip == ChipType.PC_SPEAKER:
n['speakerType'] = flag & 0b11
elif chip == ChipType.RF5C68:
n['clockSel'] = flag & 0b1111
n['chipType'] = flag >> 4 # safe
elif chip in [ChipType.SAA1099, ChipType.OPZ]:
n['clockSel'] = flag & 0b11
elif chip == ChipType.AY8930:
n['clockSel'] = flag & 0b1111
n['stereo'] = bool((flag >> 6) & 1)
n['halfClock'] = bool((flag >> 7) & 1)
n['stereoSep'] = (flag >> 8) & 0b11111111
elif chip == ChipType.VRC7:
n['clockSel'] = flag & 0b11
elif chip == ChipType.ZX_BEEPER:
n['clockSel'] = flag & 1
elif chip in [ChipType.SCC, ChipType.SCC_PLUS]:
n['clockSel'] = flag & 0b11
elif chip == ChipType.MSM6295:
n['clockSel'] = flag & 0b1111111
n['rateSel'] = bool((flag >> 7) & 1)
elif chip == ChipType.MSM6258:
n['clockSel'] = flag & 0b11
elif chip in [ChipType.OPL4, ChipType.OPL4_DRUMS]:
n['clockSel'] = flag & 0b11111111
elif chip == ChipType.SETA:
n['clockSel'] = flag & 0b1111
n['stereo'] = bool((flag >> 4) & 1)
elif chip == ChipType.ES5506:
n['channels'] = flag & 0b11111
elif chip == ChipType.TSU:
n['clockSel'] = flag & 1
n['echo'] = bool((flag >> 2) & 1)
n['swapEcho'] = bool((flag >> 3) & 1)
n['sampleMemSize'] = (flag >> 4) & 1
n['pdm'] = bool((flag >> 5) & 1)
n['echoDelay'] = (flag >> 8) & 0b111111
n['echoFeedback'] = (flag >> 16) & 0b1111
n['echoResolution'] = (flag >> 20) & 0b1111
n['echoVol'] = (flag >> 24) & 0b11111111
elif chip == ChipType.YMZ280B:
n['clockSel'] = flag & 0b11111111
elif chip == ChipType.PCM_DAC:
n['rate'] = (flag & 0b1111111111111111) + 1
n['outDepth'] = (flag >> 16) & 0b1111
n['stereo'] = bool((flag >> 20) & 1)
elif chip == ChipType.QSOUND:
n['echoDelay'] = flag & 0b1111111111111
n['echoFeedback'] = (flag >> 16) & 0b11111111
return n
def __read_header(self, stream: BinaryIO) -> None:
# assuming we passed the magic number check
self.meta.version = read_short(stream)
stream.read(2) # RESERVED
self.__song_info_ptr = read_int(stream)
stream.read(8) # RESERVED
def __read_info(self, stream: BinaryIO) -> None:
stream.seek(self.__song_info_ptr)
if stream.read(4) != b'INFO':
raise ValueError('No "INFO" magic')
if self.meta.version < 100: # don't read size prior to 0.6pre1
stream.read(4)
info_blk = stream
else:
blk_size = read_int(stream)
info_blk = BytesIO(stream.read(blk_size))
# info of first subsong
self.subsongs[0].timing.timebase = (read_byte(info_blk) + 1)
self.subsongs[0].timing.speed = (
read_byte(info_blk),
read_byte(info_blk)
)
self.subsongs[0].timing.arp_speed = read_byte(info_blk)
self.subsongs[0].timing.clock_speed = read_float(info_blk)
self.subsongs[0].pattern_length = read_short(info_blk)
len_orders = read_short(info_blk)
self.subsongs[0].timing.highlight = (
read_byte(info_blk),
read_byte(info_blk)
)
# global
num_insts = read_short(info_blk)
num_waves = read_short(info_blk)
num_samples = read_short(info_blk)
num_patterns = read_int(info_blk)
# fetch chip list
for chip_id in info_blk.read(MAX_CHIPS):
if chip_id == 0:
break # seek position is after chips here
self.chips.list.append(
ChipInfo(ChipType(chip_id)) # type: ignore
)
# fetch volume
for i in range(MAX_CHIPS):
vol = read_byte(info_blk, True) / 64.0
if i >= len(self.chips.list): # cut here
continue
self.chips.list[i].volume = vol
for i in range(MAX_CHIPS):
pan = read_byte(info_blk, True) / 128.0
if i >= len(self.chips.list): # cut here
continue
self.chips.list[i].panning = pan
if self.meta.version >= 119:
self.__chip_flag_ptr: List[int] = [
read_int(info_blk) for _ in range(MAX_CHIPS)
]
else:
for i in range(MAX_CHIPS):
flag = read_int(info_blk)
if i < len(self.chips.list):
self.chips.list[i].flags.update(
self.__convert_old_chip_flags(self.chips.list[i].type, flag)
)
self.meta.name = read_str(info_blk)
self.meta.author = read_str(info_blk)
self.meta.tuning = read_float(info_blk)
# Compat flags, part I
self.__read_compat_flags(info_blk, 1)
self.__instrument_ptr = [
read_int(info_blk) for _ in range(num_insts)
]
self.__wavetable_ptr = [
read_int(info_blk) for _ in range(num_waves)
]
self.__sample_ptr = [
read_int(info_blk) for _ in range(num_samples)
]
self.__pattern_ptr = [
read_int(info_blk) for _ in range(num_patterns)
]
num_channels = self.get_num_channels()
for channel in range(self.get_num_channels()):
self.subsongs[0].order[channel] = [
read_byte(info_blk) for _ in range(len_orders)
]
self.subsongs[0].effect_columns = [
read_byte(info_blk) for _ in range(num_channels)
]
# set up channels display info
self.subsongs[0].channel_display = [
ChannelDisplayInfo() for _ in range(num_channels)
]
for i in range(num_channels):
self.subsongs[0].channel_display[i].shown = bool(read_byte(info_blk))
for i in range(num_channels):
self.subsongs[0].channel_display[i].collapsed = bool(read_byte(info_blk))
for i in range(num_channels):
self.subsongs[0].channel_display[i].name = read_str(info_blk)
for i in range(num_channels):
self.subsongs[0].channel_display[i].abbreviation = read_str(info_blk)
self.meta.comment = read_str(info_blk)
# Master volume
if self.meta.version >= 59:
self.chips.master_volume = read_float(info_blk)
# Compat flags, part II
if self.meta.version >= 70:
self.__read_compat_flags(info_blk, 2)
if self.meta.version >= 96:
self.subsongs[0].timing.virtual_tempo = (
read_short(info_blk), read_short(info_blk)
)
else:
info_blk.read(4) # reserved in self.meta.version < 96
# Subsongs
if self.meta.version >= 95:
self.subsongs[0].name = read_str(info_blk)
self.subsongs[0].comment = read_str(info_blk)
num_extra_subsongs = read_byte(info_blk)
info_blk.read(3) # reserved
self.__subsong_ptr = [
read_int(info_blk) for _ in range(num_extra_subsongs)
]
# Extra metadata
if self.meta.version >= 103:
self.meta.sys_name = read_str(info_blk)
self.meta.album = read_str(info_blk)
# TODO: need to take encoding into account
self.meta.name_jp = read_str(info_blk)
self.meta.author_jp = read_str(info_blk)
self.meta.sys_name_jp = read_str(info_blk)
self.meta.album_jp = read_str(info_blk)
# New chip mixer and patchbay
if self.meta.version >= 135:
for i in range(len(self.chips.list)):
# new chip volume/panning format takes precedence over the legacy one
# if you save a .fur with this, legacy and new volume/panning formats
# have the same value. different values shouldn't be possible
self.chips.list[i].volume = read_float(info_blk)
self.chips.list[i].panning = read_float(info_blk)
self.chips.list[i].surround = read_float(info_blk)
num_patchbay_connections = read_int(info_blk)
for _ in range(num_patchbay_connections):
src = read_short(info_blk)
dst = read_short(info_blk)
self.patchbay.append(
PatchBay(
dest=InputPatchBayEntry(
set=InputPortSet(src >> 4),
port=src & 0b1111
),
source=OutputPatchBayEntry(
set=OutputPortSet(dst >> 4),
port=dst & 0b1111
)
)
)
if self.meta.version >= 136:
self.compat_flags.auto_patchbay = bool(read_byte(info_blk))
# Compat flags, part III
if self.meta.version >= 138:
self.__read_compat_flags(info_blk, 3)
# Speed patterns and grooves
if self.meta.version >= 139:
# speed pattern
len_speed_pattern = read_byte(info_blk)
if (len_speed_pattern < 0) or (len_speed_pattern > 16):
raise ValueError('Invalid speed pattern length value')
self.subsongs[0].speed_pattern = [
read_byte(info_blk) for _ in range(len_speed_pattern)
]
info_blk.read(16 - len_speed_pattern) # skip that many bytes, because it's always 0x06
# groove
len_groove_list = read_byte(info_blk)
for _ in range(len_groove_list):
len_groove = read_byte(info_blk)
self.subsongs[0].grooves.append([
read_byte(info_blk) for _ in range(len_groove)
])
info_blk.read(16 - len_groove) # TODO: i assume the same as above. i hope i'm right
def __read_instruments(self, stream: BinaryIO) -> None:
for i in self.__instrument_ptr:
if i == 0:
break
stream.seek(i)
new_ins = FurnaceInstrument()
if self.meta.version < 127: # i trust this not to screw up
new_ins.load_from_stream(stream, _FurInsImportType.FORMAT_0_EMBED)
else:
new_ins.load_from_stream(stream, _FurInsImportType.FORMAT_1_EMBED)
self.instruments.append(new_ins)
def __read_wavetables(self, stream: BinaryIO) -> None:
for i in self.__wavetable_ptr:
if i == 0:
break
stream.seek(i)
new_wt = FurnaceWavetable()
new_wt.load_from_stream(stream, _FurWavetableImportType.EMBED)
self.wavetables.append(new_wt)
def __read_samples(self, stream: BinaryIO) -> None:
for i in self.__sample_ptr:
if i == 0:
break
stream.seek(i)
new_wt = FurnaceSample()
new_wt.load_from_stream(stream)
self.samples.append(new_wt)
def __read_patterns(self, stream: BinaryIO) -> None:
for i in self.__pattern_ptr:
if i == 0:
break
stream.seek(i)
# Old pattern
if self.meta.version < 157:
if stream.read(4) != b'PATR':
raise ValueError('No "PATR" magic')
sz = read_int(stream)
if sz == 0:
patr_blk = stream
else:
patr_blk = BytesIO(stream.read(sz))
new_patr = FurnacePattern()
new_patr.channel = read_short(patr_blk)
new_patr.index = read_short(patr_blk)
new_patr.subsong = read_short(patr_blk)
if self.meta.version < 95:
assert new_patr.subsong == 0
read_short(patr_blk) # reserved
num_rows = self.subsongs[new_patr.subsong].pattern_length
for _ in range(num_rows):
row = FurnaceRow(
note=Note(read_short(patr_blk)),
octave=read_short(patr_blk),
instrument=read_short(patr_blk),
volume=read_short(patr_blk)
)
row.octave += (1 if row.note == Note.C_ else 0)
effect_columns = self.subsongs[new_patr.subsong].effect_columns[new_patr.channel]
row.effects = [
(read_short(patr_blk), read_short(patr_blk)) for _ in range(effect_columns)
]
new_patr.data.append(row)
if self.meta.version >= 51:
new_patr.name = read_str(patr_blk)
# New pattern
else:
if stream.read(4) != b'PATN':
raise ValueError('No "PATN" magic')
sz = read_int(stream)
if sz == 0:
patr_blk = stream
else:
patr_blk = BytesIO(stream.read(sz))
new_patr = FurnacePattern()
new_patr.subsong = read_byte(patr_blk)
new_patr.channel = read_byte(patr_blk)
new_patr.index = read_short(patr_blk)
new_patr.name = read_str(patr_blk)
num_rows = self.subsongs[new_patr.subsong].pattern_length
effect_columns = self.subsongs[new_patr.subsong].effect_columns[new_patr.channel]
empty_row = lambda: FurnaceRow(Note.__, 0, 0xffff, 0xffff, [(0xffff,0xffff)] * effect_columns)
row_idx = 0
while row_idx < num_rows:
char = read_byte(patr_blk)
# end of pattern
if char == 0xff:
break
# skip N+2 rows
if char & 0x80:
skip = (char & 0x7f) + 2
row_idx += skip
for _ in range(skip):
new_patr.data.append(empty_row())
continue
# check if some values present
effect_present_list = [False] * 8
effect_val_present_list = [False] * 8
note_present = bool(char & 0x01)
ins_present = bool(char & 0x02)
volume_present = bool(char & 0x04)
effect_present_list[0] = bool(char & 0x08)
effect_val_present_list[0] = bool(char & 0x10)
effect_0_3_present = bool(char & 0x20)
effect_4_7_present = bool(char & 0x40)
if effect_0_3_present:
char = read_byte(patr_blk)
assert effect_present_list[0] == bool(char & 0x01)
assert effect_val_present_list[0] == bool(char & 0x02)
effect_present_list[1] = bool(char & 0x04)
effect_val_present_list[1] = bool(char & 0x08)
effect_present_list[2] = bool(char & 0x10)
effect_val_present_list[2] = bool(char & 0x20)
effect_present_list[3] = bool(char & 0x40)
effect_val_present_list[3] = bool(char & 0x80)
if effect_4_7_present:
char = read_byte(patr_blk)
effect_present_list[4] = bool(char & 0x01)
effect_val_present_list[4] = bool(char & 0x02)
effect_present_list[5] = bool(char & 0x04)
effect_val_present_list[5] = bool(char & 0x08)
effect_present_list[6] = bool(char & 0x10)
effect_val_present_list[6] = bool(char & 0x20)
effect_present_list[7] = bool(char & 0x40)
effect_val_present_list[7] = bool(char & 0x80)
# actually read present values
note, octave = Note(0), 0
if note_present:
raw_note = read_byte(patr_blk)
if raw_note == 180:
note = Note.OFF
elif raw_note == 181:
note = Note.OFF_REL
elif raw_note == 182:
note = Note.REL
else:
note = raw_note % 12
note = 12 if note == 0 else note
note = Note(note)
octave = -5 + raw_note // 12
ins, volume = 0xffff, 0xffff
if ins_present:
ins = read_byte(patr_blk)
if volume_present:
volume = read_byte(patr_blk)
row = FurnaceRow(
note=note,
octave=octave,
instrument=ins,
volume=volume
)
row.effects = [(0xffff,0xffff)] * effect_columns
for i, fx_presents in enumerate(zip(effect_present_list, effect_val_present_list)):
if i >= effect_columns:
break
fx_cmd, fx_val = 0xffff, 0xffff
if fx_presents[0]:
fx_cmd = read_byte(patr_blk)
if fx_presents[1]:
fx_val = read_byte(patr_blk)
row.effects[i] = (fx_cmd, fx_val)
new_patr.data.append(row)
row_idx += 1
# fill the rest of the pattern with EMPTY
while row_idx < num_rows:
new_patr.data.append(empty_row())
row_idx += 1
self.patterns.append(new_patr)
def __read_subsongs(self, stream: BinaryIO) -> None:
for i in self.__subsong_ptr:
if i == 0:
break
stream.seek(i)
if stream.read(4) != b'SONG':
raise ValueError('No "SONG" magic')
subsong_blk = BytesIO(stream.read(read_int(stream)))
new_subsong = SubSong()
new_subsong.order.clear()
new_subsong.speed_pattern.clear()
new_subsong.timing.timebase = read_byte(subsong_blk)
new_subsong.timing.speed = (
read_byte(subsong_blk), read_byte(subsong_blk)
)
new_subsong.timing.arp_speed = read_byte(subsong_blk)
new_subsong.timing.clock_speed = read_float(subsong_blk)
new_subsong.pattern_length = read_short(subsong_blk)
new_subsong_len_orders = read_short(subsong_blk)
new_subsong.timing.highlight = (
read_byte(subsong_blk), read_byte(subsong_blk)
)
new_subsong.timing.virtual_tempo = (
read_short(subsong_blk), read_short(subsong_blk)
)
new_subsong.name = read_str(subsong_blk)
new_subsong.comment = read_str(subsong_blk)
num_channels = self.get_num_channels()
for channel in range(self.get_num_channels()):
new_subsong.order[channel] = [
read_byte(subsong_blk) for _ in range(new_subsong_len_orders)
]
new_subsong.effect_columns = [
read_byte(subsong_blk) for _ in range(num_channels)
]
# set up channels display info
new_subsong.channel_display = [
ChannelDisplayInfo() for _ in range(num_channels)
]
for i in range(num_channels):
new_subsong.channel_display[i].shown = bool(read_byte(subsong_blk))
for i in range(num_channels):
new_subsong.channel_display[i].collapsed = bool(read_byte(subsong_blk))
for i in range(num_channels):
new_subsong.channel_display[i].name = read_str(subsong_blk)
for i in range(num_channels):
new_subsong.channel_display[i].abbreviation = read_str(subsong_blk)
# Speed patterns and grooves
if self.meta.version >= 139:
# speed pattern
len_speed_pattern = read_byte(subsong_blk)
if (len_speed_pattern < 0) or (len_speed_pattern > 16):
raise ValueError('Invalid speed pattern length value')
new_subsong.speed_pattern = [
read_byte(subsong_blk) for _ in range(len_speed_pattern)
]
self.subsongs.append(new_subsong)
def __str__(self) -> str:
return '<Furnace ver. %d module "%s" by %s>' % (
self.meta.version, self.meta.name, self.meta.author
)