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 0000000..2d06bb5 Binary files /dev/null and b/gb_tarot_theme.s3m differ diff --git a/gbtarottheme.inc b/gbtarottheme.inc new file mode 100644 index 0000000..5a995e8 --- /dev/null +++ b/gbtarottheme.inc @@ -0,0 +1,203 @@ +; File created by shoofle's s3m2gbt edit + +gbtarottheme_0: + db $00, $00, +db $11, $0C, +db $21, $0F, +db $31, $40, +db $22, $00, +db $23, $00, +db $24, $00, + + db $0F, $01, + + db $0F, $02, +db $11, $0C, +db $21, $0F, +db $31, $80, + + db $0F, $03, + + db $0F, $04, +db $11, $0C, +db $21, $0F, +db $31, $C0, + + db $0F, $05, + + db $0F, $06, +db $81, $FE, + + db $0F, $07, + + db $0F, $08, + + db $0F, $09, + + db $0F, $0A, + + db $0F, $0B, + + db $0F, $0C, +db $11, $0C, +db $21, $0F, +db $31, $00, + + db $0F, $0D, + + db $0F, $0E, +db $11, $0C, +db $21, $0F, +db $31, $00, + + db $0F, $0F, + + db $0F, $10, +db $11, $10, +db $21, $0F, +db $31, $40, + + db $0F, $11, + + db $0F, $12, +db $11, $10, +db $21, $0F, +db $31, $80, + + db $0F, $13, + + db $0F, $14, +db $11, $10, +db $21, $0F, +db $31, $C0, + + db $0F, $15, + + db $0F, $16, +db $81, $FE, + + db $0F, $17, + + db $0F, $18, + + db $0F, $19, + + db $0F, $1A, + + db $0F, $1B, + + db $0F, $1C, +db $11, $10, +db $21, $0F, +db $31, $00, + + db $0F, $1D, + + db $0F, $1E, +db $11, $10, +db $21, $0F, +db $31, $00, + + db $0F, $1F, + + db $0F, $20, +db $11, $12, +db $21, $0F, +db $31, $40, + + db $0F, $21, + + db $0F, $22, +db $11, $12, +db $21, $0F, +db $31, $80, + + db $0F, $23, + + db $0F, $24, +db $11, $12, +db $21, $0F, +db $31, $C0, + + db $0F, $25, + + db $0F, $26, +db $81, $FE, + + db $0F, $27, + + db $0F, $28, + + db $0F, $29, + + db $0F, $2A, + + db $0F, $2B, + + db $0F, $2C, +db $11, $12, +db $21, $0F, +db $31, $00, + + db $0F, $2D, + + db $0F, $2E, +db $11, $12, +db $21, $0F, +db $31, $00, + + db $0F, $2F, + + db $0F, $30, +db $11, $11, +db $21, $0F, +db $31, $40, + + db $0F, $31, + + db $0F, $32, +db $11, $11, +db $21, $0F, +db $31, $80, + + db $0F, $33, + + db $0F, $34, +db $11, $11, +db $21, $0F, +db $31, $C0, + + db $0F, $35, + + db $0F, $36, +db $81, $FE, + + db $0F, $37, + + db $0F, $38, + + db $0F, $39, + + db $0F, $3A, + + db $0F, $3B, + + db $0F, $3C, +db $11, $11, +db $21, $0F, +db $31, $00, + + db $0F, $3D, + + db $0F, $3E, +db $11, $11, +db $21, $0F, +db $31, $00, + + db $0F, $3F, + +gbtarottheme: + db 4 + dw gbtarottheme_0 + dw gbtarottheme_0 + dw gbtarottheme_0 diff --git a/main.asm b/main.asm index be80b5b..2cef85f 100644 --- a/main.asm +++ b/main.asm @@ -36,6 +36,7 @@ def CARD_HELPER_VARS_START equ $c600 def CARD_VARS_START equ $c700 ; variables for animation of individual cards def CVS equ CARD_VARS_START def SHUFFLED_DECK equ $c800 ; location for the shuffled deck +def AUDIO_VARS_START equ $c900 def ZEROES equ $D000 def ONES equ $D200 @@ -63,8 +64,6 @@ SECTION "Interrupts", ROM0[$0] call INTERRUPT_LCD - 1 ret - - SECTION "Header", ROM0[$100] jp EntryPoint @@ -225,6 +224,8 @@ EntryPoint: ld hl, ScreenMainMenu call ChangeScene + call SoundSetup + Loop: ; okay this is kinda sketchy. we want a delta time variable. ; we've got two eight-bit counters, one at 4096hz and one at 16384hz @@ -286,18 +287,14 @@ println "scene update is ", SCENE_UPDATE - 1 println "scene draw is ", SCENE_DRAW - 1 call SCENE_DRAW - 1 ; hope this takes fewer than 9 scanlines! - println "scenee draw call is at ", @ ; either way it's going to eat into the update timing ; at this point we want to make sure that scanline 153 has passed ; we should check if we're past there and skip this await if necessary - ld b, 10 + ld b, 5 call AwaitLine jp Loop -SoundUpdate: - ret - ChangeScene: ; hl should be a pointer to, in sequence, setup update draw teardown ;call SCENE_TEARDOWN - 1 @@ -559,6 +556,7 @@ LetterTiles: .end INCLUDE "Async.inc" +INCLUDE "Audio.inc" INCLUDE "Random.inc" INCLUDE "CopyRangeSafe.inc" INCLUDE "CopyTilesSafe.inc" diff --git a/s3m2gbt.py b/s3m2gbt.py new file mode 100644 index 0000000..d7f03a8 --- /dev/null +++ b/s3m2gbt.py @@ -0,0 +1,627 @@ +#!/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 + +""" + 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 d586910..2f25319 100644 Binary files a/source.zip and b/source.zip differ diff --git a/theme.inc b/theme.inc new file mode 100644 index 0000000..5a995e8 --- /dev/null +++ b/theme.inc @@ -0,0 +1,203 @@ +; File created by shoofle's s3m2gbt edit + +gbtarottheme_0: + db $00, $00, +db $11, $0C, +db $21, $0F, +db $31, $40, +db $22, $00, +db $23, $00, +db $24, $00, + + db $0F, $01, + + db $0F, $02, +db $11, $0C, +db $21, $0F, +db $31, $80, + + db $0F, $03, + + db $0F, $04, +db $11, $0C, +db $21, $0F, +db $31, $C0, + + db $0F, $05, + + db $0F, $06, +db $81, $FE, + + db $0F, $07, + + db $0F, $08, + + db $0F, $09, + + db $0F, $0A, + + db $0F, $0B, + + db $0F, $0C, +db $11, $0C, +db $21, $0F, +db $31, $00, + + db $0F, $0D, + + db $0F, $0E, +db $11, $0C, +db $21, $0F, +db $31, $00, + + db $0F, $0F, + + db $0F, $10, +db $11, $10, +db $21, $0F, +db $31, $40, + + db $0F, $11, + + db $0F, $12, +db $11, $10, +db $21, $0F, +db $31, $80, + + db $0F, $13, + + db $0F, $14, +db $11, $10, +db $21, $0F, +db $31, $C0, + + db $0F, $15, + + db $0F, $16, +db $81, $FE, + + db $0F, $17, + + db $0F, $18, + + db $0F, $19, + + db $0F, $1A, + + db $0F, $1B, + + db $0F, $1C, +db $11, $10, +db $21, $0F, +db $31, $00, + + db $0F, $1D, + + db $0F, $1E, +db $11, $10, +db $21, $0F, +db $31, $00, + + db $0F, $1F, + + db $0F, $20, +db $11, $12, +db $21, $0F, +db $31, $40, + + db $0F, $21, + + db $0F, $22, +db $11, $12, +db $21, $0F, +db $31, $80, + + db $0F, $23, + + db $0F, $24, +db $11, $12, +db $21, $0F, +db $31, $C0, + + db $0F, $25, + + db $0F, $26, +db $81, $FE, + + db $0F, $27, + + db $0F, $28, + + db $0F, $29, + + db $0F, $2A, + + db $0F, $2B, + + db $0F, $2C, +db $11, $12, +db $21, $0F, +db $31, $00, + + db $0F, $2D, + + db $0F, $2E, +db $11, $12, +db $21, $0F, +db $31, $00, + + db $0F, $2F, + + db $0F, $30, +db $11, $11, +db $21, $0F, +db $31, $40, + + db $0F, $31, + + db $0F, $32, +db $11, $11, +db $21, $0F, +db $31, $80, + + db $0F, $33, + + db $0F, $34, +db $11, $11, +db $21, $0F, +db $31, $C0, + + db $0F, $35, + + db $0F, $36, +db $81, $FE, + + db $0F, $37, + + db $0F, $38, + + db $0F, $39, + + db $0F, $3A, + + db $0F, $3B, + + db $0F, $3C, +db $11, $11, +db $21, $0F, +db $31, $00, + + db $0F, $3D, + + db $0F, $3E, +db $11, $11, +db $21, $0F, +db $31, $00, + + db $0F, $3F, + +gbtarottheme: + db 4 + dw gbtarottheme_0 + dw gbtarottheme_0 + dw gbtarottheme_0