640 lines
		
	
	
		
			20 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			640 lines
		
	
	
		
			20 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
#!/usr/bin/env python3
 | 
						|
 | 
						|
# i (shoofle) have heavily modified this script to output a totally different format
 | 
						|
# in order to use it with my tarot project. much credit to the original author, 
 | 
						|
# whose intro title/copyright block is preserved below.
 | 
						|
# sourced this from https://github.com/AntonioND/gbt-player/tree/master/gba/s3m2gbt
 | 
						|
# in april of 2025
 | 
						|
 | 
						|
# s3m2gbt v4.4.1 (Part of GBT Player)
 | 
						|
#
 | 
						|
# SPDX-License-Identifier: MIT
 | 
						|
#
 | 
						|
# Copyright (c) 2022 Antonio Niño Díaz <antonio_nd@outlook.com>
 | 
						|
 | 
						|
"""
 | 
						|
 SAMPLE PERIOD LUT - MOD values
 | 
						|
           C    C#   D    D#   E    F    F#   G    G#   A    A#   B
 | 
						|
Octave 0:1712,1616,1525,1440,1357,1281,1209,1141,1077,1017, 961, 907 // C3 to B3
 | 
						|
Octave 1: 856, 808, 762, 720, 678, 640, 604, 570, 538, 508, 480, 453 // C4 to B4
 | 
						|
Octave 2: 428, 404, 381, 360, 339, 320, 302, 285, 269, 254, 240, 226 // C5 to B5
 | 
						|
Octave 3: 214, 202, 190, 180, 170, 160, 151, 143, 135, 127, 120, 113 // C6 to B6
 | 
						|
Octave 4: 107, 101,  95,  90,  85,  80,  76,  71,  67,  64,  60,  57 // C7 to B7
 | 
						|
Octave 5:  53,  50,  47,  45,  42,  40,  37,  35,  33,  31,  30,  28 // C8 to B8
 | 
						|
 | 
						|
//From C3 to B8  |  A5 = 1750 = 440.00Hz  |  C5 = 1546
 | 
						|
const UWORD GB_frequencies[] = {
 | 
						|
      44, 156, 262, 363, 457, 547, 631, 710, 786, 854, 923, 986, // C3 to B3
 | 
						|
    1046,1102,1155,1205,1253,1297,1339,1379,1417,1452,1486,1517, // C4 to B4
 | 
						|
    1546,1575,1602,1627,1650,1673,1694,1714,1732,1750,1767,1783, // C5 to B5
 | 
						|
    1798,1812,1825,1837,1849,1860,1871,1881,1890,1899,1907,1915, // C6 to B6
 | 
						|
    1923,1930,1936,1943,1949,1954,1959,1964,1969,1974,1978,1982, // C7 to B7
 | 
						|
    1985,1988,1992,1995,1998,2001,2004,2006,2009,2011,2013,2015  // C8 to B8
 | 
						|
};
 | 
						|
 | 
						|
"""
 | 
						|
 | 
						|
class RowConversionError(Exception):
 | 
						|
    def __init__(self, message, pattern = -1, row = -1, channel = -1):
 | 
						|
        self.pattern = pattern
 | 
						|
        self.row = row
 | 
						|
        self.channel = channel + 1
 | 
						|
        self.message = message
 | 
						|
 | 
						|
    def __str__(self):
 | 
						|
        return f"Pattern {self.pattern} | Row {self.row} | Channel {self.channel} | {self.message}"
 | 
						|
 | 
						|
class S3MFormatError(Exception):
 | 
						|
    pass
 | 
						|
 | 
						|
class S3MFormatReader:
 | 
						|
 | 
						|
    def read_u8(self):
 | 
						|
        offset = self.read_ptr
 | 
						|
        self.read_ptr += 1
 | 
						|
        return int(self.data[offset])
 | 
						|
 | 
						|
    def read_u16(self):
 | 
						|
        offset = self.read_ptr
 | 
						|
        self.read_ptr += 2
 | 
						|
        return int((self.data[offset + 1] << 8) | self.data[offset])
 | 
						|
 | 
						|
    def read_memseg(self):
 | 
						|
        offset = self.read_ptr
 | 
						|
        self.read_ptr += 3
 | 
						|
        part1 = self.data[offset + 0]
 | 
						|
        part2 = self.data[offset + 1]
 | 
						|
        part3 = self.data[offset + 2]
 | 
						|
        return int((part1 << 16) | (part3 << 8) | part2)
 | 
						|
 | 
						|
    def read_string(self, size):
 | 
						|
        offset = self.read_ptr
 | 
						|
        self.read_ptr += size
 | 
						|
        return self.data[offset:offset+size]
 | 
						|
 | 
						|
class S3MFileInstrument(S3MFormatReader):
 | 
						|
 | 
						|
    def __init__(self, data, offset):
 | 
						|
        self.data = data
 | 
						|
        self.read_ptr = offset
 | 
						|
 | 
						|
        instrument_type = self.read_u8()
 | 
						|
        if instrument_type != 1:
 | 
						|
            self.exists = False
 | 
						|
            return
 | 
						|
        self.exists = True
 | 
						|
 | 
						|
        self.dos_filename = self.read_string(12).decode("utf-8")
 | 
						|
 | 
						|
        self.sample_data_offset = self.read_memseg() * 16
 | 
						|
 | 
						|
        self.length = self.read_u16()
 | 
						|
        self.length |= self.read_u16() << 16
 | 
						|
 | 
						|
        self.read_ptr += 4 + 4 # Skip loop begin and loop end
 | 
						|
 | 
						|
        self.default_volume = self.read_u8()
 | 
						|
 | 
						|
        self.read_ptr = offset + 0x30
 | 
						|
 | 
						|
        self.sample_name = self.read_string(28).decode("utf-8")
 | 
						|
 | 
						|
        if self.read_string(4) != b'SCRS':
 | 
						|
            raise S3MFormatError("Invalid magic string in instrument")
 | 
						|
 | 
						|
        start = self.sample_data_offset
 | 
						|
        end = start + self.length
 | 
						|
        self.sample_data = self.data[start:end]
 | 
						|
 | 
						|
class S3MFilePatternCell():
 | 
						|
 | 
						|
    def __init__(self, header, channel, note, instrument, volume,
 | 
						|
                 effect, effect_args):
 | 
						|
 | 
						|
        if header == 0:
 | 
						|
            self.empty = True
 | 
						|
            return
 | 
						|
 | 
						|
        self.empty = False
 | 
						|
 | 
						|
        self.channel = channel
 | 
						|
 | 
						|
        if (note != None) or (instrument != None):
 | 
						|
            self.has_note_and_instrument = True
 | 
						|
            self.note = note
 | 
						|
            self.instrument = instrument
 | 
						|
        else:
 | 
						|
            self.has_note_and_instrument = False
 | 
						|
 | 
						|
        if volume != None:
 | 
						|
            self.has_volume = True
 | 
						|
            self.volume = volume
 | 
						|
        else:
 | 
						|
            self.has_volume = False
 | 
						|
 | 
						|
        if (effect != None) or (effect_args != None):
 | 
						|
            self.has_effect = True
 | 
						|
            self.effect = effect
 | 
						|
            self.effect_args = effect_args
 | 
						|
        else:
 | 
						|
            self.has_effect = False
 | 
						|
 | 
						|
class S3MFilePattern(S3MFormatReader):
 | 
						|
 | 
						|
    def __init__(self, data, offset):
 | 
						|
 | 
						|
        # Check if we have asked to generate an empty pattern
 | 
						|
        if data == None:
 | 
						|
            cell = S3MFilePatternCell(0, 0, 0, 0, 0, 0, 0)
 | 
						|
            self.cells = []
 | 
						|
            for i in range(0, 64):
 | 
						|
                self.cells.append(cell)
 | 
						|
            return
 | 
						|
 | 
						|
        self.data = data
 | 
						|
        self.read_ptr = offset
 | 
						|
 | 
						|
        length = self.read_u16() - 2
 | 
						|
 | 
						|
        self.cells = []
 | 
						|
 | 
						|
        while length > 0:
 | 
						|
            header = self.read_u8()
 | 
						|
            length -= 1
 | 
						|
 | 
						|
            channel = header & 31
 | 
						|
 | 
						|
            note = None
 | 
						|
            instrument = None
 | 
						|
            volume = None
 | 
						|
            effect = None
 | 
						|
            effect_args = None
 | 
						|
 | 
						|
            if (header & (1 << 5)) != 0: # Has note and instrument
 | 
						|
                note = self.read_u8()
 | 
						|
                instrument = self.read_u8()
 | 
						|
                length -= 2
 | 
						|
 | 
						|
            if (header & (1 << 6)) != 0: # Has volume
 | 
						|
                volume = self.read_u8()
 | 
						|
                length -= 1
 | 
						|
 | 
						|
            if (header & (1 << 7)) != 0: # Has effect
 | 
						|
                effect = self.read_u8()
 | 
						|
                effect_args = self.read_u8()
 | 
						|
                length -= 2
 | 
						|
 | 
						|
            cell = S3MFilePatternCell(header, channel, note, instrument, volume,
 | 
						|
                                      effect, effect_args)
 | 
						|
            self.cells.append(cell)
 | 
						|
 | 
						|
class S3MFile(S3MFormatReader):
 | 
						|
 | 
						|
    def __init__(self, data):
 | 
						|
 | 
						|
        # Save data for now
 | 
						|
 | 
						|
        self.data = data
 | 
						|
        self.read_ptr = 0
 | 
						|
 | 
						|
        self.name = self.read_string(28).decode("utf-8")
 | 
						|
        print(f"Song Name: '{self.name}'")
 | 
						|
 | 
						|
        self.read_ptr += 1 + 1 + 2 # Ignore fields
 | 
						|
 | 
						|
        self.song_length = self.read_u16()
 | 
						|
        print(f"Song Length: {self.song_length}")
 | 
						|
 | 
						|
        self.num_instruments = self.read_u16()
 | 
						|
        self.num_patterns = self.read_u16()
 | 
						|
 | 
						|
        self.read_ptr += 6 # Ignore fields
 | 
						|
 | 
						|
        if self.read_string(4) != b'SCRM':
 | 
						|
            raise S3MFormatError("Invalid magic string in file")
 | 
						|
 | 
						|
        self.read_ptr += 1 # Ignore global volume
 | 
						|
 | 
						|
        self.initial_speed = self.read_u8()
 | 
						|
 | 
						|
        if self.read_u8() != 150:
 | 
						|
            raise S3MFormatError("Invalid tempo: It must be 150")
 | 
						|
 | 
						|
        self.read_ptr += 2 # Ignore master volume and ultraclick removal
 | 
						|
 | 
						|
        # Save this for later
 | 
						|
        has_custom_pan = False
 | 
						|
        if self.read_u8() == 252:
 | 
						|
            has_custom_pan = True
 | 
						|
 | 
						|
        self.read_ptr = 0x40
 | 
						|
        channel_settings = self.read_string(4)
 | 
						|
        if channel_settings[0] >= 16 or channel_settings[1] >= 16 or \
 | 
						|
           channel_settings[2] >= 16 or channel_settings[3] >= 16:
 | 
						|
            raise S3MFormatError("Invalid channel settings: Channels 0-3 must be enabled")
 | 
						|
 | 
						|
        # Read orders
 | 
						|
 | 
						|
        self.read_ptr = 0x60
 | 
						|
 | 
						|
        self.song_orders = self.read_string(self.song_length)
 | 
						|
        if self.song_length % 2 == 1:
 | 
						|
            self.read_ptr += 1 # Align to 2
 | 
						|
 | 
						|
        # Read instrument parapointers
 | 
						|
 | 
						|
        self.instrument_offsets = [None] * self.num_instruments
 | 
						|
        for i in range(0, self.num_instruments):
 | 
						|
            self.instrument_offsets[i] = self.read_u16() * 16
 | 
						|
 | 
						|
        # Read pattern parapointers
 | 
						|
 | 
						|
        self.pattern_offsets = [None] * self.num_patterns
 | 
						|
        for i in range(0, self.num_patterns):
 | 
						|
            self.pattern_offsets[i] = self.read_u16() * 16
 | 
						|
 | 
						|
        # Read default panning
 | 
						|
 | 
						|
        if has_custom_pan:
 | 
						|
            self.channel_pan = [b & 0xF for b in self.read_string(4)]
 | 
						|
        else:
 | 
						|
            self.channel_pan = [8, 8, 8, 8]
 | 
						|
 | 
						|
        # Load instruments
 | 
						|
 | 
						|
        self.instruments = [None] * self.num_instruments
 | 
						|
        for i in range(0, len(self.instrument_offsets)):
 | 
						|
            offset = self.instrument_offsets[i]
 | 
						|
            if offset != 0:
 | 
						|
                instr = S3MFileInstrument(self.data, offset)
 | 
						|
                if instr.exists:
 | 
						|
                    self.instruments[i] = instr
 | 
						|
 | 
						|
        # Load patterns
 | 
						|
 | 
						|
        self.patterns = [None] * self.num_patterns
 | 
						|
        for i in range(0, len(self.pattern_offsets)):
 | 
						|
            offset = self.pattern_offsets[i]
 | 
						|
            if offset != 0:
 | 
						|
                self.patterns[i] = S3MFilePattern(self.data, offset)
 | 
						|
            else:
 | 
						|
                # A NULL pointer means that the pattern is empty
 | 
						|
                self.patterns[i] = S3MFilePattern(None, 0)
 | 
						|
 | 
						|
        # The file data is no longer needed
 | 
						|
 | 
						|
        self.data = []
 | 
						|
 | 
						|
# Channels 1, 2, 4
 | 
						|
def s3m_volume_to_gb(s3m_vol):
 | 
						|
    if s3m_vol >= 64:
 | 
						|
        return 15
 | 
						|
    else:
 | 
						|
        print(f"found a decreased volume: {s3m_vol}, returning {s3m_vol>>2}")
 | 
						|
        return s3m_vol >> 2;
 | 
						|
 | 
						|
# Channel 3
 | 
						|
def s3m_volume_to_gb_ch3(s3m_vol):
 | 
						|
    vol = s3m_volume_to_gb(s3m_vol)
 | 
						|
 | 
						|
    if vol >= 0 and vol <= 3:
 | 
						|
        return 0<<1 # 0%
 | 
						|
    elif vol >= 4 and vol <= 6:
 | 
						|
        return 3<<1 # 25%
 | 
						|
    elif vol >= 7 and vol <= 9:
 | 
						|
        return 2<<1 # 50%
 | 
						|
    elif vol >= 10 and vol <= 12:
 | 
						|
        return 1<<1 # 75%
 | 
						|
    elif vol >= 13 and vol <= 15:
 | 
						|
        return 1<<1 # 100%
 | 
						|
    else:
 | 
						|
        return 0
 | 
						|
 | 
						|
def s3m_note_to_gb(note):
 | 
						|
    # Note cut with ^^
 | 
						|
    if note == 0xFE:
 | 
						|
        print(f"note cut found: {note}")
 | 
						|
        return 0xFE
 | 
						|
    if note == 0xFF:
 | 
						|
        print(f"note cut found: {note}")
 | 
						|
        return 0xFF
 | 
						|
 | 
						|
    # Note off and ^^ note cut should be handled before reaching this point
 | 
						|
    #note = note & 0x7F
 | 
						|
    if note > 0x7F or note < 0:
 | 
						|
        print(note)
 | 
						|
    assert note <= 0x7F
 | 
						|
 | 
						|
    note -= 32
 | 
						|
    if note < 0:
 | 
						|
        raise RowConversionError("Note too low")
 | 
						|
    elif note > 32 + 16 * 6:
 | 
						|
        raise RowConversionError("Note too high")
 | 
						|
 | 
						|
    note = (note & 0xF) + ((note & 0xF0) >> 4) * 12
 | 
						|
    return note
 | 
						|
 | 
						|
def s3m_pan_to_gb(pan, channel):
 | 
						|
    left = False
 | 
						|
    right = False
 | 
						|
 | 
						|
    if pan >= 0 and pan <= 3:
 | 
						|
        left = True
 | 
						|
    elif pan >= 4 and pan <= 11:
 | 
						|
        left = True
 | 
						|
        right = True
 | 
						|
    elif pan >= 12 and pan <= 15:
 | 
						|
        right = True
 | 
						|
 | 
						|
    val = 0
 | 
						|
    if left:
 | 
						|
        val |= 1 << (3 + channel)
 | 
						|
    if right:
 | 
						|
        val |= 1 << (channel - 1)
 | 
						|
 | 
						|
    return val
 | 
						|
 | 
						|
# masks for how to define an effect
 | 
						|
EFFECT_PAN              = 0x40
 | 
						|
EFFECT_ARPEGGIO         = 0x50
 | 
						|
EFFECT_VIBRATO          = 0x60
 | 
						|
EFFECT_VOLUME_SLIDE     = 0x70
 | 
						|
EFFECT_NOTE_CUT         = 0x80
 | 
						|
EFFECT_PATTERN_JUMP     = 0x90
 | 
						|
EFFECT_BREAK_SET_STEP   = 0xA0
 | 
						|
EFFECT_SPEED            = 0xB0
 | 
						|
EFFECT_EVENT            = 0xC0
 | 
						|
 | 
						|
# Returns (converted_num, converted_params) if there was a valid effect. If
 | 
						|
# there is none, it returns (None, None). Note that it is needed to pass the
 | 
						|
# channel to this function because some effects behave differently depending on
 | 
						|
# the channel (like panning).
 | 
						|
def effect_s3m_to_gb(channel, effectnum, effectparams):
 | 
						|
 | 
						|
    if effectnum == 'A': # Set Speed
 | 
						|
        if effectparams == 0:
 | 
						|
            raise RowConversionError("Speed must not be zero")
 | 
						|
 | 
						|
        return (EFFECT_SPEED, effectparams)
 | 
						|
 | 
						|
    if effectnum == 'B': # Pattern jump
 | 
						|
        # TODO: Fail if this jumps out of bounds
 | 
						|
        return (EFFECT_PATTERN_JUMP, effectparams)
 | 
						|
 | 
						|
    elif effectnum == 'C': # Break + Set row
 | 
						|
        # Effect value is BCD, convert to integer
 | 
						|
        val = (((effectparams & 0xF0) >> 4) * 10) + (effectparams & 0x0F)
 | 
						|
        return (EFFECT_BREAK_SET_STEP, val)
 | 
						|
 | 
						|
    elif effectnum == 'D': # Volume Slide
 | 
						|
        if channel == 3:
 | 
						|
            raise RowConversionError("Volume slide not supported in channel 3")
 | 
						|
 | 
						|
        if effectparams == 0:
 | 
						|
            # Ignore volume slide commands that just continue the effect,
 | 
						|
            # they are only needed for the S3M player.
 | 
						|
            return (None, None)
 | 
						|
 | 
						|
        upper = (effectparams >> 4) & 0xF
 | 
						|
        lower = effectparams & 0xF
 | 
						|
 | 
						|
        if upper == 0xF or lower == 0xF:
 | 
						|
            raise RowConversionError("Fine volume slide not supported")
 | 
						|
 | 
						|
        elif lower == 0: # Volume goes up
 | 
						|
            params = 1 << 3 # Increase
 | 
						|
            delay = 7 - upper + 1
 | 
						|
            if delay <= 0:
 | 
						|
                raise RowConversionError("Volume slide too steep")
 | 
						|
            params |= delay
 | 
						|
            return (EFFECT_VOLUME_SLIDE, params)
 | 
						|
        elif upper == 0: # Volume goes down
 | 
						|
            params = 0 << 3 # Decrease
 | 
						|
            delay = 7 - lower + 1
 | 
						|
            if delay <= 0:
 | 
						|
                raise RowConversionError("Volume slide too steep")
 | 
						|
            params = delay
 | 
						|
            return (EFFECT_VOLUME_SLIDE, params)
 | 
						|
        else:
 | 
						|
            raise RowConversionError("Invalid volume slide arguments")
 | 
						|
 | 
						|
        return (EFFECT_VOLUME_SLIDE, effectparams)
 | 
						|
 | 
						|
    elif effectnum == 'H': # Vibrato
 | 
						|
        return (EFFECT_VIBRATO, effectparams)
 | 
						|
 | 
						|
    elif effectnum == 'J': # Arpeggio
 | 
						|
        return (EFFECT_ARPEGGIO, effectparams)
 | 
						|
 | 
						|
    elif effectnum == 'S': # This effect is subdivided into many
 | 
						|
 | 
						|
        subeffectnum = (effectparams & 0xF0) >> 4
 | 
						|
        subeffectparams = effectparams & 0x0F
 | 
						|
 | 
						|
        if subeffectnum == 0x8: # Pan position
 | 
						|
            val = s3m_pan_to_gb(subeffectparams, channel)
 | 
						|
            return (EFFECT_PAN, val)
 | 
						|
 | 
						|
        elif subeffectnum == 0xC: # Notecut
 | 
						|
            print(f"found ay note cut! with params {subeffectparams}")
 | 
						|
            return (EFFECT_NOTE_CUT, subeffectparams)
 | 
						|
 | 
						|
        elif subeffectnum == 0xF: # Funkrepeat? Set active macro?
 | 
						|
            # This effect is either unused, or it's the "set active macro"
 | 
						|
            # command, which doesn't have any effect if you don't use the macro
 | 
						|
            # afterwards. It can safely be overloaded for event callbacks.
 | 
						|
            return (EFFECT_EVENT, subeffectparams)
 | 
						|
 | 
						|
    raise RowConversionError(f"Unsupported effect: {effectnum}{effectparams:02X}")
 | 
						|
 | 
						|
def convert_channel(channel, note_index, samplenum, volume, effectnum, effectparams):
 | 
						|
    commands = []
 | 
						|
 | 
						|
    # Check if it's needed to add a note
 | 
						|
    if note_index != -1 and note_index != 0xFF and note_index != 0xFE and note_index != 169:
 | 
						|
        note_index = s3m_note_to_gb(note_index)
 | 
						|
        commands.append([0x10 | channel, note_index])
 | 
						|
    if note_index == 0xFF or note_index == 0xFE or note_index == 169:
 | 
						|
        print(f"found a note cut by note value: {note_index}")
 | 
						|
        commands.append([0x80 | channel, 0])
 | 
						|
 | 
						|
    if volume > -1:
 | 
						|
        print(channel)
 | 
						|
        print(f"converting volume {volume} to {s3m_volume_to_gb(volume) & 0x0F}")
 | 
						|
        commands.append([0x20 | channel, s3m_volume_to_gb(volume) & 0x0F])
 | 
						|
        print(commands)
 | 
						|
 | 
						|
    # Check if there is a sample defined
 | 
						|
    if samplenum > 0:
 | 
						|
        if channel == 1 or channel == 2:
 | 
						|
            commands.append([0x30 | channel, (samplenum & 3) << 6])
 | 
						|
        if channel == 3:
 | 
						|
            commands.append([0x30 | channel, samplenum & 7])
 | 
						|
        if channel == 4:
 | 
						|
            commands.append([0x30 | channel, samplenum & 15])
 | 
						|
 | 
						|
    if effectnum is not None:
 | 
						|
        [num, params] = effect_s3m_to_gb(1, effectnum, effectparams)
 | 
						|
 | 
						|
        if num is not None:
 | 
						|
            commands.append([num | channel, params])
 | 
						|
 | 
						|
    return commands
 | 
						|
 | 
						|
def convert_file(module_path, song_name, output_path, export_instruments):
 | 
						|
 | 
						|
    with open(module_path, "rb") as file:
 | 
						|
        file_byte_array = bytearray(file.read())
 | 
						|
 | 
						|
    s3m = S3MFile(file_byte_array)
 | 
						|
 | 
						|
    if output_path == None:
 | 
						|
        output_path = song_name + ".inc"
 | 
						|
 | 
						|
    with open(output_path, "w") as fileout:
 | 
						|
 | 
						|
        fileout.write("; File created by shoofle's s3m2gbt edit\n\n")
 | 
						|
 | 
						|
        # Export patterns
 | 
						|
        # ---------------
 | 
						|
 | 
						|
        print(f"Exporting patterns...")
 | 
						|
 | 
						|
        pattern = -1
 | 
						|
        for p in s3m.patterns:
 | 
						|
            pattern += 1
 | 
						|
 | 
						|
            # Check if pattern is actually used in the order list. If it isn't
 | 
						|
            # used, don't export it.
 | 
						|
            if pattern not in s3m.song_orders:
 | 
						|
                print(f"Pattern {pattern} not exported: Not in the order list")
 | 
						|
                continue
 | 
						|
 | 
						|
            fileout.write(f"{song_name}_{pattern}:\n")
 | 
						|
 | 
						|
            row = 0
 | 
						|
 | 
						|
            commands = [[0x00, 0x00]]
 | 
						|
 | 
						|
            for c in p.cells:
 | 
						|
 | 
						|
 | 
						|
                # If an end of row marker is reached, print the previous row.
 | 
						|
                # Trust that the S3M file is generated in a valid way and it
 | 
						|
                # doesn't have markers at weird positions, and that there is one
 | 
						|
                # marker right at the end of each pattern.
 | 
						|
                if c.empty:
 | 
						|
 | 
						|
                    # Write row
 | 
						|
                    fileout.write("    ")
 | 
						|
 | 
						|
                    for cmd in commands:
 | 
						|
                        fileout.write("db ")
 | 
						|
                        for b in cmd:
 | 
						|
                            fileout.write(f"${b:02X}, ")
 | 
						|
                        fileout.write("\n")
 | 
						|
 | 
						|
                    fileout.write("\n")
 | 
						|
 | 
						|
                    row = row + 1
 | 
						|
 | 
						|
                    commands = [[0x0F, row & 0xFF]]
 | 
						|
 | 
						|
                    # Next iteration
 | 
						|
                    continue
 | 
						|
 | 
						|
                volume = -1
 | 
						|
                if c.has_volume:
 | 
						|
                    volume = c.volume
 | 
						|
 | 
						|
                note = -1
 | 
						|
                instrument = 0
 | 
						|
                if c.has_note_and_instrument:
 | 
						|
                    note = c.note
 | 
						|
                    instrument = c.instrument
 | 
						|
 | 
						|
                    # Rows with note and instrument but no volume use the
 | 
						|
                    # default volume of the sample.
 | 
						|
                    if instrument > 0 and volume == -1:
 | 
						|
                        this_instr = s3m.instruments[instrument - 1]
 | 
						|
                        volume = this_instr.default_volume
 | 
						|
 | 
						|
                effectnum = None
 | 
						|
                effectparams = None
 | 
						|
                if c.has_effect:
 | 
						|
                    # Convert type to ASCII to match the documentation
 | 
						|
                    effectnum = chr(c.effect + ord('A') - 1)
 | 
						|
                    effectparams = c.effect_args
 | 
						|
 | 
						|
                channel = c.channel + 1
 | 
						|
 | 
						|
                try:
 | 
						|
                    commands.extend(convert_channel(channel, 
 | 
						|
                        note, instrument, volume,
 | 
						|
                        effectnum, effectparams))
 | 
						|
                    
 | 
						|
                    if channel > 4:
 | 
						|
                        raise S3MFormatError(f"Too many channels: {channel}")
 | 
						|
                except RowConversionError as e:
 | 
						|
                    e.row = row
 | 
						|
                    e.pattern = pattern
 | 
						|
                    e.channel = channel
 | 
						|
                    raise e
 | 
						|
 | 
						|
 | 
						|
        # Export initial state
 | 
						|
        # --------------------
 | 
						|
 | 
						|
        print(f"Exporting initial state... or not...")
 | 
						|
 | 
						|
        
 | 
						|
        # Export orders
 | 
						|
        # -------------
 | 
						|
 | 
						|
        print(f"Exporting orders...")
 | 
						|
 | 
						|
        fileout.write(f"{song_name}:\n")
 | 
						|
 | 
						|
        fileout.write(f"\tdb {len(s3m.song_orders)-1}\n")
 | 
						|
        for o in s3m.song_orders:
 | 
						|
            pattern = int(o)
 | 
						|
            if pattern >= s3m.num_patterns:
 | 
						|
                # TODO: Warn if the pattern goes over the limit?
 | 
						|
                continue
 | 
						|
            fileout.write(f"\tdw {song_name}_{pattern}\n")
 | 
						|
 | 
						|
if __name__ == "__main__":
 | 
						|
 | 
						|
    import argparse
 | 
						|
    import sys
 | 
						|
 | 
						|
    print("s3m2gbt v4.4.1 (part of GBT Player)")
 | 
						|
    print("Copyright (c) 2022 Antonio Niño Díaz <antonio_nd@outlook.com>")
 | 
						|
    print("All rights reserved")
 | 
						|
    print("")
 | 
						|
 | 
						|
    parser = argparse.ArgumentParser(description='Convert S3M files into GBT format.')
 | 
						|
    parser.add_argument("--input", default=None, required=True,
 | 
						|
                        help="input file")
 | 
						|
    parser.add_argument("--name", default=None, required=True,
 | 
						|
                        help="output song name")
 | 
						|
    parser.add_argument("--output", default=None, required=False,
 | 
						|
                        help="output file")
 | 
						|
    parser.add_argument("--instruments", default=False, required=False,
 | 
						|
                        action='store_true', help="export channel 3 instruments")
 | 
						|
 | 
						|
    args = parser.parse_args()
 | 
						|
 | 
						|
    try:
 | 
						|
        convert_file(args.input, args.name, args.output, args.instruments)
 | 
						|
    except RowConversionError as e:
 | 
						|
        print("ERROR: " + str(e))
 | 
						|
        sys.exit(1)
 | 
						|
    except S3MFormatError as e:
 | 
						|
        print("ERROR: Invalid S3M file: " + str(e))
 | 
						|
        sys.exit(1)
 | 
						|
 | 
						|
    print("Done!")
 | 
						|
 | 
						|
    sys.exit(0)
 |