From 35bb4dd60b16d12bdd263b4556535e785faed618 Mon Sep 17 00:00:00 2001 From: shoofle Date: Thu, 3 Apr 2025 10:59:18 -0400 Subject: [PATCH] attempt to write my own audio engine. didnt work well. --- .gitignore | 1 + Audio.inc | 347 +++++++++++++++++++++++++ ScreenShuffle.inc | 99 ++----- gb_tarot_theme.s3m | Bin 0 -> 55512 bytes gbtarottheme.inc | 203 +++++++++++++++ main.asm | 12 +- s3m2gbt.py | 627 +++++++++++++++++++++++++++++++++++++++++++++ source.zip | Bin 419454 -> 427157 bytes theme.inc | 203 +++++++++++++++ 9 files changed, 1403 insertions(+), 89 deletions(-) create mode 100644 Audio.inc create mode 100644 gb_tarot_theme.s3m create mode 100644 gbtarottheme.inc create mode 100644 s3m2gbt.py create mode 100644 theme.inc diff --git a/.gitignore b/.gitignore index 6a4dae1..43f4fa1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ .DS_Store __pycache__ +*.s3m~ diff --git a/Audio.inc b/Audio.inc new file mode 100644 index 0000000..150fa24 --- /dev/null +++ b/Audio.inc @@ -0,0 +1,347 @@ +PUSHS "Sound Variables", WRAM0[AUDIO_VARS_START] +ordersStart: dw +println "orders start is ", ordersStart +ordersLength: db + + +; audio player state +currentOrderNumber: db +currentRowNumber: db +println "currentRowNumber is ", currentRowNumber +currentTickNumber: db +currentPattern: dw +println "current pattern is ", currentPattern +audioReadHead: dw + +AUDIO_VARIABLES_BEGIN: + +speed: db + +macro channel ; macro to define all the channell data in parallel +channel_\1_note: db +channel_\1_duty: db +channel_\1_volume: db +channel_\1_slide: db +channel_\1_arp: db +channel_\1_pan: db +channel_\1_vibrato: db +channel_\1_trigger: db +endm + +channel 1 +channel 2 +channel 3 +channel 4 + +println "channel 1 note is ", channel_1_note +AUDIO_VARIABLES_END: +POPS + +SoundSetup: + ld a, LOW(gbtarottheme) + ld [ordersStart], a + ld a, HIGH(gbtarottheme) + ld [ordersStart+1], a + + ld a, 0 + ld [currentOrderNumber], a + ld [currentRowNumber], a + ld [currentTickNumber], a + + ld a, $FF + ld [currentRowNumber], a + + ld a, [ordersStart] + ld l, a + ld a, [ordersStart+1] + ld h, a + inc hl + + ld a, [hl+] + ld [currentPattern], a + ld [audioReadHead], a + ld a, [hl+] + ld [currentPattern+1], a + ld [audioReadHead+1], a + + ld hl, ZEROES + ld de, AUDIO_VARIABLES_BEGIN + ld bc, AUDIO_VARIABLES_END - AUDIO_VARIABLES_BEGIN + call CopyRange + + ld a, 6 + ld [speed], a + + ld a, %1000_1111 + ld [rNR52], a + ld a, $ff + ld [rNR51], a + ld a, $11 + ld [rNR50], a + + ret +SoundUpdate: + ld a, [speed] + ld b, a + ld a, [currentTickNumber] + inc a + call ArrayClampLoopingB + ld [currentTickNumber], a + + cp a, 0 + jp nz, .doneAllPackets ; if we're not zero, then just update the things + + ld b, 64 + ld a, [currentRowNumber] + inc a + call ArrayClampLoopingB + ld [currentRowNumber], a + + cp a, 0 + jp nz, :+ ; if the new row number is zero, then we go to the next pattern. + ld a, AUDHIGH_RESTART + ld [rNR14], a + + + ; TODO: update for song orders + ld a, [currentPattern] + ld [audioReadHead], a + ld a, [currentPattern+1] + ld [audioReadHead+1], a +: + ld a, [audioReadHead] + ld l, a + ld a, [audioReadHead+1] + ld h, a + + ld a, [currentRowNumber] + ld b, a + ld a, [hl+] + and a, $F0 + cp a, 0 + ret nz + ld a, [hl+] + cp a, b + jp nz, .doneAllPackets ; return if the current row doesn't equal the row we're looking at + + ; else we're looking at packets +.packetExamine + ld a, l + ld [audioReadHead], a + ld a, h + ld [audioReadHead+1], a + + ld d, h + ld e, l + + ld hl, channel_1_note + + ld a, [de] + and a, $0F + cp a, 1 + jp nz, :+ + ld hl, channel_1_note +: cp a, 2 + jp nz, :+ + ld hl, channel_2_note +: cp a, 3 + jp nz, :+ + ld hl, channel_3_note +: cp a, 4 + jp nz, :+ + ld hl, channel_4_note +: + + ld a, [de] + and a, $F0 ; grab the top nibble of [hl], which holds the effect. + + ; if nibble is zero, then update our read head (already done) and return. + cp a, $00 + jp z, .doneAllPackets + + ld bc, channel_1_note - channel_1_note + cp a, $10 + jp z, .updateValue + + ld bc, channel_1_volume - channel_1_note + cp a, $20 + jp z, .updateVolume + + ld bc, channel_1_duty - channel_1_note + cp a, $30 + jp z, .updateValue + + ld bc, channel_1_pan - channel_1_note + cp a, $40 + jp z, .updateValue + + ld bc, channel_1_arp - channel_1_note + cp a, $50 + jp z, .updateValue + + ld bc, channel_1_vibrato - channel_1_note + cp a, $60 + jp z, .updateValue + + ld bc, channel_1_slide - channel_1_note + cp a, $70 + jp z, .updateValue + + ld bc, channel_1_volume - channel_1_note + cp a, $80 + jp z, .noteCut + + cp a, $90 + jp z, .patternJump + + cp a, $a0 + jp z, .breakSetStep + + cp a, $b0 + jp z, .setSpeed + + cp a, $c0 + jp z, .event + ; and now a bunch of code for handling all those cases! + +.updateVolume: + inc de + add hl, bc + ld a, 0 + ld [channel_1_trigger], a + ld a, [hl] + cp a, 0 + jp nz, :+ + ; if the volume is zero to start with, + ; and the new volume is nonzero, + ld a, [de] + ; set the trigger flag + cp a, 0 + jp z, :+ + ld a, AUDHIGH_RESTART + ld [channel_1_trigger], a +: + + ld a, [de] + ld [hl], a + + inc de + ld h, d + ld l, e + jp .packetExamine + +.updateValue: + inc de + + add hl, bc +: ld a, [de] + ld [hl], a + + inc de + ld h, d + ld l, e + + jp .packetExamine + +.noteCut: + inc de + add hl, bc + + ld [hl], 0 + + ld a, AUDHIGH_RESTART + ld [channel_1_trigger], a + + inc de + ld h, d + ld l, e + jp .packetExamine +.patternJump: + ; TODO + inc de + inc de + ld h, d + ld l, e + jp .packetExamine +.breakSetStep: + ; TODO + inc de + inc de + ld h, d + ld l, e + jp .packetExamine +.setSpeed: + inc de + ld a, [de] + ld [speed], a + + inc de + ld h, d + ld l, e + jp .packetExamine +.event: + ; TODO + inc de + inc de + ld h, d + ld l, e + jp .packetExamine + + +.doneAllPackets: + call Channel_1_Update + call Channel_2_Update + call Channel_3_Update + call Channel_4_Update + ret + + +Channel_1_Update: + ld a, [channel_1_duty] + or a, $0a + ld [rNR11], a + + ld a, [channel_1_volume] + sla a + sla a + sla a + sla a + and a, $F0 + ld [rNR12], a + + ld a, [channel_1_note] + ld hl, note_periods + ld b, 0 + ld c, a + add hl, bc + add hl, bc ; double width values + + ld a, [hl+] + ld [rNR13], a + + + ld a, [channel_1_trigger] + or a, [hl] + or a, AUDHIGH_LENGTH_OFF + ld [rNR14], a + + ld a, 0 + ld [channel_1_trigger], a + ret + +Channel_2_Update: +Channel_3_Update: +Channel_4_Update: + ret + +note_periods: + dw 44, 156, 262, 363, 457, 547, 631, 710, 786, 854, 923, 986, ; C3 to B3 + dw 1046,1102,1155,1205,1253,1297,1339,1379,1417,1452,1486,1517, ; C4 to B4 + dw 1546,1575,1602,1627,1650,1673,1694,1714,1732,1750,1767,1783, ; C5 to B5 + dw 1798,1812,1825,1837,1849,1860,1871,1881,1890,1899,1907,1915, ; C6 to B6 + dw 1923,1930,1936,1943,1949,1954,1959,1964,1969,1974,1978,1982, ; C7 to B7 + dw 1985,1988,1992,1995,1998,2001,2004,2006,2009,2011,2013,2015 ; C8 to B8 + +INCLUDE "theme.inc" + + diff --git a/ScreenShuffle.inc b/ScreenShuffle.inc index 5320628..ee95ef1 100644 --- a/ScreenShuffle.inc +++ b/ScreenShuffle.inc @@ -3,7 +3,6 @@ ;def vBlocked equ vPreviousCardIndex + 1 def vAnimationFrame EQU SCREEN_VARS_START def vState EQU vAnimationFrame+1 -println "vState is ", vState def vCurrentAnimation EQU vState+1 ; 2 bytes def vShuffleIndex equ vCurrentAnimation+2 def vShuffleTime equ vShuffleIndex+1 ; 2 bytes @@ -40,11 +39,6 @@ ShuffleSetup: ld a, S_Center ld [vState], a - ld a, LOW(ShuffleAnimationRight) - ld [vCurrentAnimation], a - ld a, HIGH(ShuffleAnimationRight) - ld [vCurrentAnimation+1], a - ld hl, .asyncTask call Async_Spawn_HL @@ -60,84 +54,14 @@ ShuffleSetup: ld c, 20 ; width call CopyTilesToMap - ; manually drawing the Big Card -.drawBigCard - ld hl, _SCRN0 + 32*5 + 8 - ld a, VARIABLE_TILES_START - ld [hl+], a - inc a - ld [hl+], a - ld [hl+], a - inc a - ld [hl+], a - ld b, 0 - ld c, 32 - 4 - add hl, bc + ld hl, Shuffle.BigCard + ld de, _SCRN0 + 32*5 + 8 + ld b, 8 + ld c, 4 + call CopyTilesToMap - inc a - ld [hl+], a - inc a - ld [hl+], a - ld [hl+], a - inc a - ld [hl+], a - add hl, bc - sub a, 2 - - ld [hl+], a - inc a - ld [hl+], a - ld [hl+], a - inc a - ld [hl+], a - add hl, bc - sub a, 2 - - ld [hl+], a - inc a - ld [hl+], a - ld [hl+], a - inc a - ld [hl+], a - add hl, bc - sub a, 2 - - ld [hl+], a - inc a - ld [hl+], a - ld [hl+], a - inc a - ld [hl+], a - add hl, bc - sub a, 2 - - ld [hl+], a - inc a - ld [hl+], a - ld [hl+], a - inc a - ld [hl+], a - add hl, bc - sub a, 2 - - ld [hl+], a - inc a - ld [hl+], a - ld [hl+], a - inc a - ld [hl+], a - add hl, bc - inc a - - ld [hl+], a - inc a - ld [hl+], a - ld [hl+], a - inc a - ld [hl+], a - ld hl, Shuffle.UITileData - ld de, $9000 + VARIABLE_TILES_START*16 + ld de, _VRAM + $1000 + VARIABLE_TILES_START*16 ld bc, Shuffle.UITileDataEnd - Shuffle.UITileData call CopyRange @@ -554,3 +478,14 @@ Shuffle.UITileData: db $aa,$55,$55,$aa,$aa,$55,$ff,$ff,$00,$ff,$ff,$ff,$ff,$ff,$00,$ff ; bottom-middle db $b6,$5f,$56,$bf,$b6,$5f,$f6,$ff,$06,$ff,$fe,$ff,$fe,$ff,$00,$ff ; bottom-right Shuffle.UITileDataEnd: + +Shuffle.BigCard: +def VTS = VARIABLE_TILES_START + db VTS, VTS+1, VTS+1, VTS+2 + db VTS+3, VTS+4, VTS+4, VTS+5 + db VTS+3, VTS+4, VTS+4, VTS+5 + db VTS+3, VTS+4, VTS+4, VTS+5 + db VTS+3, VTS+4, VTS+4, VTS+5 + db VTS+3, VTS+4, VTS+4, VTS+5 + db VTS+3, VTS+4, VTS+4, VTS+5 + db VTS+6, VTS+7, VTS+7, VTS+8 \ No newline at end of file diff --git a/gb_tarot_theme.s3m b/gb_tarot_theme.s3m new file mode 100644 index 0000000000000000000000000000000000000000..2d06bb520d9dead1846a32c0c8ed7d9bff2b4d6b GIT binary patch literal 55512 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 + +""" + 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: + 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 # 0% + elif vol >= 4 and vol <= 6: + return 3 # 25% + elif vol >= 7 and vol <= 9: + return 2 # 50% + elif vol >= 10 and vol <= 12: + return 4 # 75% + elif vol >= 13 and vol <= 15: + return 1 # 100% + else: + return 0 + +def s3m_note_to_gb(note): + # Note cut with ^^ + if note == 0xFE: + return 0xFE + if note == 0xFF: + return 0xFF + + # Note off and ^^ note cut should be handled before reaching this point + #note = note & 0x7F + if note > 0x7F: + 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 + 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: + note_index = s3m_note_to_gb(note_index) + commands.append([0x10 | channel, note_index]) + if note_index == 0xFF or note_index == 0xFE: + commands.append([0x80 | channel, note_index]) + + if volume > -1: + commands.append([0x20 | channel, s3m_volume_to_gb(volume) & 0x0F]) + + # Check if there is a sample defined + if samplenum > 0: + instrument = samplenum & 3 + commands.append([0x30 | channel, instrument << 6]) + + 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)}\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 ") + 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) diff --git a/source.zip b/source.zip index d586910a64bcb47ef7bd7f8bc6a7273b7fb0434b..2f25319eced3bc4dd68468b8103e540044892066 100644 GIT binary patch delta 9089 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 delta 1771 zcmeH{?@wD*7{^aVw&MK)NIRx=KaDzv4r!W;rq3PH*s4%;##Q{k?+gJ$J2N(-8n z{(xk{UThkNN0+%J`hxf)i*qhFi)QZ4OiZSR?1l5qvKWm@vdtwL4bkeo&%MP}WBdoa zy{Gqcp7VUq^W5j;K1++X{1vNtpwGygkb1p1ojz-BOc<5txsJqYEkOOL8$N)t?+*5; zAgA4i-@7!58BWy!Rh~3#tO0UgGECM3y>Zjb16J_6_!%#j0okwfWdx|@8~#(PSqVFIX75)^lRktd z<s zF51Cfwc^jY?Tu{IhO5g16A4KiJR}P7aI1;ZSU7Hy7i@UdAs_C>HUlK_=3_Wg4s`Dj zwg4^Hj^iU>a{hv^c}mp`bG;=igARlS9c+_>+`4}~ndv}W*c|6GcroMgqUb@tF=;QFo8tIqAofd=<+|_ zNS(CF-G0~JNvtbgT4LTf#E>V>kqPlcd^AFup7rQWj~rQW_MmNZdsZ( zmR2@-XgzIK)Pg8db?5yd^_7%6B88~%52aHJv~MixEWCcPaw@T(D%J2Pe*cS=QrG@h z!Jy8CYO(9baWx!~_3t`ut{f4oY9N-Ex6akbc{h2xs<=bR-#$SWHkb`u1zPV~>{62K zR6h!~mL$+r*v=Gr7%WpM-SS>a_gPI5=m5+QL^xuC=Ey3WQ?eu%v#H{;(jrl_MPJD=PWWj+u+WtBVM!qgY?gyz`_AGC9z{`|Pp@ zA$3^p3ZcwcZl2h|>96*M(551%JM8j2p`B{FJZV33a05c>(9XT!nFtc(7bJ0FTuT!Z Qb!D6K8!17k