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 '' % ( self.meta.version, self.meta.name, self.meta.author )