From 45ca20d569861bd33ea67de72dae8e3ed2b87c3b Mon Sep 17 00:00:00 2001 From: shoofle Date: Mon, 7 Apr 2025 12:20:57 -0400 Subject: [PATCH] got frustrated, wrote an audio engine --- Audio.inc | 1129 +++++++++++++++++++++++-------------------- gb_tarot_theme.inc | 335 +++++++++++++ gb_tarot_theme.s3m | Bin 55512 -> 55720 bytes gb_tarot_theme1.inc | 262 ++++++++++ main.asm | 26 +- range_test.s3m | Bin 50160 -> 55800 bytes rangetest.inc | 672 +++++++++++++++++++++++++ s3m2gbt.py | 255 ++++------ s3m2shoofmt.py | 36 +- source.zip | Bin 475958 -> 434658 bytes theme.inc | 192 ++++++-- 11 files changed, 2170 insertions(+), 737 deletions(-) create mode 100644 gb_tarot_theme.inc create mode 100644 gb_tarot_theme1.inc create mode 100644 rangetest.inc diff --git a/Audio.inc b/Audio.inc index 07db592..849e369 100644 --- a/Audio.inc +++ b/Audio.inc @@ -1,534 +1,629 @@ +SECTION "Audio Interface", ROM0 SoundSetup: - ld a, BANK(gbt_play) + ld a, BANK(AudioEngineInit) ld [rROMB0], a - ld de,pythonrangetest - ld bc,BANK(instr_test_data) - ld a,$05 - call gbt_play ; Play song - - ld a, 1 - call gbt_loop + ld de, gb_tarot_theme + ld c, BANK(gb_tarot_theme) + call AudioEngineInit ; Play song ld a, [cvCardBank] ld [rROMB0], a ret SoundUpdate: - ld a, BANK(gbt_play) - ld [rROMB0], a - - call gbt_update + call AudioEngineUpdate ld a, [cvCardBank] ld [rROMB0], a ret + +SECTION "Audio Variables", WRAM0[AUDIO_VARS_START] +ordersBank: db +orders: dw + +speed: db + +readHead: dw + +tick: db +println "ticks are at ", tick +row: db +println "rows are at ", row +order: db +println "order index at ", order + +; i've chosen to pack the variables for each instrument sequentially in memory +; so all of channel 1's vars, then all of channel 2's vars, etc. +; gbt-player instead packs all four notes together, then all four volumes, +; then all four instruments. having thought about it more, gbt-player's approach +; may be better - it makes indexing to a channel's value simpler. but i've +; already written GetChannelAttributeAddress so it's neither here nor there +; at this point. +macro channel_vars + channel_\1: + ; inherents + channel_\1_note: db + channel_\1_volume: db + channel_\1_instrument: db + channel_\1_pan: db + ; special features! + channel_\1_arpeggio: db + channel_\1_vibrato: db + channel_\1_volume_slide: db + channel_\1_note_cut: db -; File created by mod2gbt -; s3m2gbt modified by shoofle to output rgbds-compatible asm files -SECTION "pythonrangetest_0", ROMX -pythonrangetest_0: - db $BF,$00,$30,$10,$10,$10, - db $00,$00,$00,$00, - db $BF,$01,$30,$00,$00,$00, - db $00,$00,$00,$00, - db $BF,$02,$30,$00,$00,$00, - db $00,$00,$00,$00, - db $BF,$03,$30,$00,$00,$00, - db $00,$00,$00,$00, - db $BF,$04,$30,$00,$00,$00, - db $00,$00,$00,$00, - db $BF,$05,$30,$00,$00,$00, - db $00,$00,$00,$00, - db $BF,$06,$30,$00,$00,$00, - db $00,$00,$00,$00, - db $BF,$07,$30,$00,$00,$00, - db $00,$00,$00,$00, - db $BF,$08,$30,$00,$00,$00, - db $00,$00,$00,$00, - db $BF,$09,$30,$00,$00,$00, - db $00,$00,$00,$00, - db $BF,$0A,$30,$00,$00,$00, - db $00,$00,$00,$00, - db $BF,$0B,$30,$00,$00,$00, - db $00,$00,$00,$00, - db $BF,$0C,$30,$00,$00,$00, - db $00,$00,$00,$00, - db $BF,$0D,$30,$00,$00,$00, - db $00,$00,$00,$00, - db $BF,$0E,$30,$00,$00,$00, - db $00,$00,$00,$00, - db $BF,$0F,$30,$00,$00,$00, - db $00,$00,$00,$00, - db $BF,$10,$30,$00,$00,$00, - db $00,$00,$00,$00, - db $BF,$11,$30,$00,$00,$00, - db $00,$00,$00,$00, - db $BF,$12,$30,$00,$00,$00, - db $00,$00,$00,$00, - db $BF,$13,$30,$00,$00,$00, - db $00,$00,$00,$00, - db $BF,$14,$30,$00,$00,$00, - db $00,$00,$00,$00, - db $BF,$15,$30,$00,$00,$00, - db $00,$00,$00,$00, - db $BF,$16,$30,$00,$00,$00, - db $00,$00,$00,$00, - db $BF,$17,$30,$00,$00,$00, - db $00,$00,$00,$00, - db $BF,$18,$30,$00,$00,$00, - db $00,$00,$00,$00, - db $BF,$19,$30,$00,$00,$00, - db $00,$00,$00,$00, - db $BF,$1A,$30,$00,$00,$00, - db $00,$00,$00,$00, - db $BF,$1B,$30,$00,$00,$00, - db $00,$00,$00,$00, - db $BF,$1C,$30,$00,$00,$00, - db $00,$00,$00,$00, - db $BF,$1D,$30,$00,$00,$00, - db $00,$00,$00,$00, - db $BF,$1E,$30,$00,$00,$00, - db $00,$00,$00,$00, - db $BF,$1F,$30,$00,$00,$00, - db $00,$00,$00,$00, - - -SECTION "pythonrangetest_1", ROMX -pythonrangetest_1: - db $BF,$20,$30,$10,$10,$10, - db $00,$00,$00,$00, - db $BF,$21,$30,$00,$00,$00, - db $00,$00,$00,$00, - db $BF,$22,$30,$00,$00,$00, - db $00,$00,$00,$00, - db $BF,$23,$30,$00,$00,$00, - db $00,$00,$00,$00, - db $BF,$24,$30,$00,$00,$00, - db $00,$00,$00,$00, - db $BF,$25,$30,$00,$00,$00, - db $00,$00,$00,$00, - db $BF,$26,$30,$00,$00,$00, - db $00,$00,$00,$00, - db $BF,$27,$30,$00,$00,$00, - db $00,$00,$00,$00, - db $BF,$28,$30,$00,$00,$00, - db $00,$00,$00,$00, - db $BF,$29,$30,$00,$00,$00, - db $00,$00,$00,$00, - db $BF,$2A,$30,$00,$00,$00, - db $00,$00,$00,$00, - db $BF,$2B,$30,$00,$00,$00, - db $00,$00,$00,$00, - db $BF,$2C,$30,$00,$00,$00, - db $00,$00,$00,$00, - db $BF,$2D,$30,$00,$00,$00, - db $00,$00,$00,$00, - db $BF,$2E,$30,$00,$00,$00, - db $00,$00,$00,$00, - db $BF,$2F,$30,$00,$00,$00, - db $00,$00,$00,$00, - db $BF,$30,$30,$00,$00,$00, - db $00,$00,$00,$00, - db $BF,$31,$30,$00,$00,$00, - db $00,$00,$00,$00, - db $BF,$32,$30,$00,$00,$00, - db $00,$00,$00,$00, - db $BF,$33,$30,$00,$00,$00, - db $00,$00,$00,$00, - db $BF,$34,$30,$00,$00,$00, - db $00,$00,$00,$00, - db $BF,$35,$30,$00,$00,$00, - db $00,$00,$00,$00, - db $BF,$36,$30,$00,$00,$00, - db $00,$00,$00,$00, - db $BF,$37,$30,$00,$00,$00, - db $00,$00,$00,$00, - db $BF,$38,$30,$00,$00,$00, - db $00,$00,$00,$00, - db $BF,$39,$30,$00,$00,$00, - db $00,$00,$00,$00, - db $BF,$3A,$30,$00,$00,$00, - db $00,$00,$00,$00, - db $BF,$3B,$30,$00,$00,$00, - db $00,$00,$00,$00, - db $BF,$3C,$30,$00,$00,$00, - db $00,$00,$00,$00, - db $BF,$3D,$30,$00,$00,$00, - db $00,$00,$00,$00, - db $BF,$3E,$30,$00,$00,$00, - db $00,$00,$00,$00, - db $BF,$3F,$30,$00,$00,$00, - db $00,$00,$00,$00, - - -SECTION "pythonrangetest_2", ROMX -pythonrangetest_2: - db $BF,$40,$30,$10,$10,$10, - db $00,$00,$00,$00, - db $BF,$41,$30,$00,$00,$00, - db $00,$00,$00,$00, - db $BF,$42,$30,$00,$00,$00, - db $00,$00,$00,$00, - db $BF,$43,$30,$00,$00,$00, - db $00,$00,$00,$00, - db $BF,$44,$30,$00,$00,$00, - db $00,$00,$00,$00, - db $BF,$45,$30,$00,$00,$00, - db $00,$00,$00,$00, - db $BF,$46,$30,$00,$00,$00, - db $00,$00,$00,$00, - db $BF,$47,$30,$00,$00,$00, - db $00,$00,$00,$00, - db $00,$00,$00,$00, - db $00,$00,$00,$00, - db $00,$00,$00,$00, - db $00,$00,$00,$00, - db $00,$00,$00,$00, - db $00,$00,$00,$00, - db $00,$00,$00,$00, - db $00,$00,$00,$00, - db $00,$00,$00,$00, - db $00,$00,$00,$00, - db $00,$00,$00,$00, - db $00,$00,$00,$00, - db $00,$00,$00,$00, - db $00,$00,$00,$00, - db $00,$00,$00,$00, - db $00,$00,$00,$00, - db $00,$00,$00,$00, - db $00,$00,$00,$00, - db $00,$00,$00,$00, - db $00,$00,$00,$00, - db $00,$00,$00,$00, - db $00,$00,$00,$00, - db $00,$00,$00,$00, - db $00,$00,$00,$00, - db $00,$00,$00,$00, - db $00,$00,$00,$00, - db $00,$00,$00,$00, - db $00,$00,$00,$00, - db $00,$00,$00,$00, - db $00,$00,$00,$00, - db $00,$00,$00,$00, - db $00,$00,$00,$00, - db $00,$00,$00,$00, - db $00,$00,$00,$00, - db $00,$00,$00,$00, - db $00,$00,$00,$00, - db $00,$00,$00,$00, - db $00,$00,$00,$00, - db $00,$00,$00,$00, - db $00,$00,$00,$00, - db $00,$00,$00,$00, - db $00,$00,$00,$00, - db $00,$00,$00,$00, - db $00,$00,$00,$00, - db $00,$00,$00,$00, - db $00,$00,$00,$00, - db $00,$00,$00,$00, - db $00,$00,$00,$00, - - -pythonrangetest_init_state: - db 0x01,0x06,0x02,0x11,0x22,0x44,0x88,0x00, - -SECTION "pythonrangetest", ROMX -pythonrangetest: - db BANK(pythonrangetest_0) - dw pythonrangetest_0 - - db BANK(pythonrangetest_1) - dw pythonrangetest_1 - - db BANK(pythonrangetest_2) - dw pythonrangetest_2 - - db $00 - dw $0000 - - - SECTION "instr_test_0", ROMX -instr_test_0: - DB $98, $1F, $20, $20, $4A, $07 - DB $2F, $00, $00, $20 - DB $2F, $00, $00, $00 - DB $2F, $00, $00, $00 - DB $2F, $00, $00, $00 - DB $2F, $00, $00, $00 - DB $2F, $00, $00, $00 - DB $2F, $00, $00, $00 - DB $98, $2F, $00, $00, $00 - DB $2F, $00, $00, $00 - DB $2F, $00, $00, $00 - DB $2F, $00, $00, $00 - DB $2F, $00, $00, $00 - DB $2F, $00, $00, $00 - DB $2F, $00, $00, $00 - DB $2F, $00, $00, $00 - DB $98, $3F, $00, $00, $00 - DB $2F, $00, $00, $00 - DB $2F, $00, $00, $00 - DB $2F, $00, $00, $00 - DB $2F, $00, $00, $00 - DB $2F, $00, $00, $00 - DB $2F, $00, $00, $00 - DB $2F, $00, $00, $00 - DB $98, $0F, $00, $00, $00 - DB $2F, $00, $00, $00 - DB $2F, $00, $00, $00 - DB $2F, $00, $00, $00 - DB $2F, $00, $00, $00 - DB $2F, $00, $00, $00 - DB $2F, $00, $00, $00 - DB $2F, $00, $00, $00 - DB $20, $00, $98, $10, $00 - DB $00, $00, $21, $00 - DB $00, $00, $21, $00 - DB $00, $00, $21, $00 - DB $00, $00, $21, $00 - DB $00, $00, $21, $00 - DB $00, $00, $21, $00 - DB $00, $00, $21, $00 - DB $00, $00, $98, $11, $00 - DB $00, $00, $21, $00 - DB $00, $00, $21, $00 - DB $00, $00, $21, $00 - DB $00, $00, $21, $00 - DB $00, $00, $21, $00 - DB $00, $00, $21, $00 - DB $00, $00, $21, $00 - DB $00, $00, $98, $12, $00 - DB $00, $00, $21, $00 - DB $00, $00, $21, $00 - DB $00, $00, $21, $00 - DB $00, $00, $21, $00 - DB $00, $00, $21, $00 - DB $00, $00, $21, $00 - DB $00, $00, $21, $00 - DB $00, $00, $98, $13, $00 - DB $00, $00, $21, $00 - DB $00, $00, $21, $00 - DB $00, $00, $21, $00 - DB $00, $00, $21, $00 - DB $00, $00, $21, $00 - DB $00, $00, $21, $00 - DB $00, $00, $21, $00 - - SECTION "instr_test_1", ROMX -instr_test_1: - DB $00, $00, $98, $14, $00 - DB $00, $00, $21, $00 - DB $00, $00, $21, $00 - DB $00, $00, $21, $00 - DB $00, $00, $21, $00 - DB $00, $00, $21, $00 - DB $00, $00, $21, $00 - DB $00, $00, $21, $00 - DB $00, $00, $98, $15, $00 - DB $00, $00, $21, $00 - DB $00, $00, $21, $00 - DB $00, $00, $21, $00 - DB $00, $00, $21, $00 - DB $00, $00, $21, $00 - DB $00, $00, $21, $00 - DB $00, $00, $21, $00 - DB $00, $00, $98, $16, $00 - DB $00, $00, $21, $00 - DB $00, $00, $21, $00 - DB $00, $00, $21, $00 - DB $00, $00, $21, $00 - DB $00, $00, $21, $00 - DB $00, $00, $21, $00 - DB $00, $00, $21, $00 - DB $00, $00, $98, $17, $00 - DB $00, $00, $21, $00 - DB $00, $00, $21, $00 - DB $00, $00, $21, $00 - DB $00, $00, $21, $00 - DB $00, $00, $21, $00 - DB $00, $00, $21, $00 - DB $00, $00, $21, $00 - DB $00, $00, $20, $80, $0F - DB $00, $00, $00, $2F - DB $00, $00, $00, $2F - DB $00, $00, $00, $2F - DB $00, $00, $00, $2F - DB $00, $00, $00, $2F - DB $00, $00, $00, $2F - DB $00, $00, $00, $2F - DB $00, $00, $00, $81, $0F - DB $00, $00, $00, $2F - DB $00, $00, $00, $2F - DB $00, $00, $00, $2F - DB $00, $00, $00, $2F - DB $00, $00, $00, $2F - DB $00, $00, $00, $2F - DB $00, $00, $00, $2F - DB $00, $00, $00, $82, $0F - DB $00, $00, $00, $2F - DB $00, $00, $00, $2F - DB $00, $00, $00, $2F - DB $00, $00, $00, $2F - DB $00, $00, $00, $2F - DB $00, $00, $00, $2F - DB $00, $00, $00, $2F - DB $00, $00, $00, $83, $0F - DB $00, $00, $00, $2F - DB $00, $00, $00, $2F - DB $00, $00, $00, $2F - DB $00, $00, $00, $2F - DB $00, $00, $00, $2F - DB $00, $00, $00, $2F - DB $00, $00, $00, $2F - - SECTION "instr_test_2", ROMX -instr_test_2: - DB $00, $00, $00, $84, $0F - DB $00, $00, $00, $2F - DB $00, $00, $00, $2F - DB $00, $00, $00, $2F - DB $00, $00, $00, $2F - DB $00, $00, $00, $2F - DB $00, $00, $00, $2F - DB $00, $00, $00, $2F - DB $00, $00, $00, $85, $0F - DB $00, $00, $00, $2F - DB $00, $00, $00, $2F - DB $00, $00, $00, $2F - DB $00, $00, $00, $2F - DB $00, $00, $00, $2F - DB $00, $00, $00, $2F - DB $00, $00, $00, $2F - DB $00, $00, $00, $86, $0F - DB $00, $00, $00, $2F - DB $00, $00, $00, $2F - DB $00, $00, $00, $2F - DB $00, $00, $00, $2F - DB $00, $00, $00, $2F - DB $00, $00, $00, $2F - DB $00, $00, $00, $2F - DB $00, $00, $00, $87, $0F - DB $00, $00, $00, $2F - DB $00, $00, $00, $2F - DB $00, $00, $00, $2F - DB $00, $00, $00, $2F - DB $00, $00, $00, $2F - DB $00, $00, $00, $2F - DB $00, $00, $00, $2F - DB $00, $00, $00, $88, $0F - DB $00, $00, $00, $2F - DB $00, $00, $00, $2F - DB $00, $00, $00, $2F - DB $00, $00, $00, $2F - DB $00, $00, $00, $2F - DB $00, $00, $00, $2F - DB $00, $00, $00, $2F - DB $00, $00, $00, $89, $0F - DB $00, $00, $00, $2F - DB $00, $00, $00, $2F - DB $00, $00, $00, $2F - DB $00, $00, $00, $2F - DB $00, $00, $00, $2F - DB $00, $00, $00, $2F - DB $00, $00, $00, $2F - DB $00, $00, $00, $8A, $0F - DB $00, $00, $00, $2F - DB $00, $00, $00, $2F - DB $00, $00, $00, $2F - DB $00, $00, $00, $2F - DB $00, $00, $00, $2F - DB $00, $00, $00, $2F - DB $00, $00, $00, $2F - DB $00, $00, $00, $8B, $0F - DB $00, $00, $00, $2F - DB $00, $00, $00, $2F - DB $00, $00, $00, $2F - DB $00, $00, $00, $2F - DB $00, $00, $00, $2F - DB $00, $00, $00, $2F - DB $00, $00, $00, $2F - - SECTION "instr_test_3", ROMX -instr_test_3: - DB $00, $00, $00, $8C, $0F - DB $00, $00, $00, $2F - DB $00, $00, $00, $2F - DB $00, $00, $00, $2F - DB $00, $00, $00, $2F - DB $00, $00, $00, $2F - DB $00, $00, $00, $2F - DB $00, $00, $00, $2F - DB $00, $00, $00, $8D, $0F - DB $00, $00, $00, $2F - DB $00, $00, $00, $2F - DB $00, $00, $00, $2F - DB $00, $00, $00, $2F - DB $00, $00, $00, $2F - DB $00, $00, $00, $2F - DB $00, $00, $00, $2F - DB $00, $00, $00, $8E, $0F - DB $00, $00, $00, $2F - DB $00, $00, $00, $2F - DB $00, $00, $00, $2F - DB $00, $00, $00, $2F - DB $00, $00, $00, $2F - DB $00, $00, $00, $2F - DB $00, $00, $00, $2F - DB $00, $00, $00, $8F, $0F - DB $00, $00, $00, $2F - DB $00, $00, $00, $2F - DB $00, $00, $00, $2F - DB $00, $00, $00, $2F - DB $00, $00, $00, $2F - DB $00, $00, $00, $2F - DB $00, $00, $00, $2F - DB $00, $00, $00, $20 - DB $00, $00, $00, $00 - DB $00, $00, $00, $00 - DB $00, $00, $00, $00 - DB $00, $00, $00, $00 - DB $00, $00, $00, $00 - DB $00, $00, $00, $00 - DB $00, $00, $00, $00 - DB $00, $00, $00, $00 - DB $00, $00, $00, $00 - DB $00, $00, $00, $00 - DB $00, $00, $00, $00 - DB $00, $00, $00, $00 - DB $00, $00, $00, $00 - DB $00, $00, $00, $00 - DB $00, $00, $00, $00 - DB $00, $00, $00, $00 - DB $00, $00, $00, $00 - DB $00, $00, $00, $00 - DB $00, $00, $00, $00 - DB $00, $00, $00, $00 - DB $00, $00, $00, $00 - DB $00, $00, $00, $00 - DB $00, $00, $00, $00 - DB $00, $00, $00, $00 - DB $00, $00, $00, $00 - DB $00, $00, $00, $00 - DB $00, $00, $00, $00 - DB $00, $00, $00, $00 - DB $00, $00, $00, $00 - DB $00, $00, $00, $00 - DB $00, $00, $00, $00 - - SECTION "instr_test_data", ROM0 -instr_test_data:: - DB BANK(instr_test_0) - DW instr_test_0 - DB BANK(instr_test_1) - DW instr_test_1 - DB BANK(instr_test_2) - DW instr_test_2 - DB BANK(instr_test_3) - DW instr_test_3 - DB $00 - DW $0000 - + channel_\1_trigger: db +endm + +channel_vars 1 +channel_vars 2 +channel_vars 3 +channel_3_loaded_instrument: db +channel_vars 4 + +SECTION "Audio Engine", ROM0 + +Periods: + DW 44, 156, 262, 363, 457, 547, 631, 710, 786, 854, 923, 986 + DW 1046, 1102, 1155, 1205, 1253, 1297, 1339, 1379, 1417, 1452, 1486, 1517 + DW 1546, 1575, 1602, 1627, 1650, 1673, 1694, 1714, 1732, 1750, 1767, 1783 + DW 1798, 1812, 1825, 1837, 1849, 1860, 1871, 1881, 1890, 1899, 1907, 1915 + DW 1923, 1930, 1936, 1943, 1949, 1954, 1959, 1964, 1969, 1974, 1978, 1982 + DW 1985, 1988, 1992, 1995, 1998, 2001, 2004, 2006, 2009, 2011, 2013, 2015 +WaveSamples: ; 8 sounds, pilfered from gbt like so many things + DB $A5,$D7,$C9,$E1,$BC,$9A,$76,$31,$0C,$BA,$DE,$60,$1B,$CA,$03,$93 ; random + DB $F0,$E1,$D2,$C3,$B4,$A5,$96,$87,$78,$69,$5A,$4B,$3C,$2D,$1E,$0F + DB $FD,$EC,$DB,$CA,$B9,$A8,$97,$86,$79,$68,$57,$46,$35,$24,$13,$02 ; up-downs + DB $DE,$FE,$DC,$BA,$9A,$A9,$87,$77,$88,$87,$65,$56,$54,$32,$10,$12 + DB $AB,$CD,$EF,$ED,$CB,$A0,$12,$3E,$DC,$BA,$BC,$DE,$FE,$DC,$32,$10 ; tri. broken + DB $FF,$EE,$DD,$CC,$BB,$AA,$99,$88,$77,$66,$55,$44,$33,$22,$11,$00 ; triangular + DB $FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$00,$00,$00,$00,$00,$00,$00,$00 ; square + DB $79,$BC,$DE,$EF,$FF,$EE,$DC,$B9,$75,$43,$21,$10,$00,$11,$23,$45 ; sine +NoiseOptions: ; 16 different ways to configure NR43 for different sounds +; once again this method is from gbt. if you use the gbt sample# as an index into +; this, it will match the gbt s3m template. + DB $5F,$5B,$4B,$2F,$3B,$58,$1F,$0F ; 7 bit + DB $90,$80,$70,$50,$00 ; 15 bit + DB $67,$63,$53 +AudioEngineInit: + ld a, c + ld [ordersBank], a + + ld a, e + ld [orders], a + ld a, d + ld [orders+1], a + + ld a, 6 + ld [speed], a + + ld a, $ff + ld [tick], a + ld [row], a + ld [order], a + + ld a, AUDENA_ON + ld [rNR52], a ; master audio enable + ld a, $ff + ld [rNR51], a ; panning + ld a, $ff + ld [rNR50], a ; stereo volume + + ld a, $ff + ld [channel_1_note_cut], a + ld [channel_2_note_cut], a + ld [channel_3_note_cut], a + ld [channel_4_note_cut], a + ld [channel_3_loaded_instrument], a + ret + +AudioEngineUpdate: + ld a, [ordersBank] + ld [rROMB0], a + + call IncrementTick + ld a, [tick] + cp a, 0 + jp nz, .updateRegisters + call z, IncrementRow + + ld a, [row] + cp a, 0 + call z, IncrementOrder + + ld a, [row] + cp a, 0 + call z, hitit + + ld a, [row] + cp a, 0 + call z, FreshOrder + + call FreshRow +.updateRegisters + + call UpdateRegisters1 + call UpdateRegisters2 + call UpdateRegisters3 + call UpdateRegisters4 + + nop ; just throwiing this in here so i can break on it for timing + + ret +hitit: + ret +IncrementTick: + ld a, [speed] + ld b, a + ld a, [tick] + inc a + call ArrayClampLoopingB + ld [tick], a + ret +IncrementRow: + ld b, 64 + ld a, [row] + inc a + call ArrayClampLoopingB + ld [row], a + ret +IncrementOrder: + ld a, [orders] + ld l, a + ld a, [orders+1] + ld h, a + ld a, [order] + inc a + call ArrayClampLooping + ld [order], a + ret + +FreshOrder: + ld a, [orders] + ld l, a + ld a, [orders+1] + ld h, a + inc hl + ld a, [order] + ld c, a + ld b, 0 + + add hl, bc + add hl, bc + ld b, h + ld c, l + + ld a, [bc] + ld [readHead], a + inc bc + ld a, [bc] + ld [readHead+1], a + + ret + +FreshRow: + ld a, [readHead] + ld l, a + ld a, [readHead+1] + ld h, a + + ld a, [hl] + and a, $F0 + cp a, 0 + ; this should always be zero. + + inc hl + ld a, [row] + cp a, [hl] + + ret nz ; not ready for a fresh row until the current row matches the upcoming row + inc hl + + ld e, l + ld a, l + ld [readHead], a + ld d, h + ld a, h + ld [readHead+1], a + +.examinePacket + ; de is our read head for the time being so we don't have to write + ; so many loads in and out of memory + ld a, [de] + and a, $F0 + swap a + ld hl, .packetJumpTable ; this all jumps based on what the top nibble is + ld c, a + ld b, 0 + add hl, bc + add hl, bc + ld c, [hl] + inc hl + ld b, [hl] + ld l, c + ld h, b + + jp hl +.packetJumpTable + dw .newRow ;$0x + dw .setNote ;$1x + dw .setVolume ;$2x + dw .setInstrument ;$3x + dw .setPan ;$4x + dw .setArpeggio ;$5x + dw .setVibrato ;$6x + dw .setVolumeSlide;$7x + dw .setNoteCut ;$8x + dw .jumpOrder ;$90 + dw .breakAndSetRow;$A0 + dw .setSpeed ;$B0 + dw .callbackEvent ;$C0 +.newRow + ; if we find a new row, set the read head to that value and return + ld a, e + ld [readHead], a + ld a, d + ld [readHead+1], a + ret +.setNote + call TriggerIt ; always trigger on notes + ld hl, channel_1_note - channel_1 + jp .WriteAttributeButDoNotTrigger +.setVolume + ld hl, channel_1_volume - channel_1 + jp .WriteAttributeAndTriggerIfDifferent +.setInstrument + ld hl, channel_1_instrument - channel_1 + jp .WriteAttributeAndTriggerIfDifferent +.setPan + ld hl, channel_1_pan - channel_1 + jp .WriteAttributeButDoNotTrigger +.setArpeggio + ld hl, channel_1_arpeggio - channel_1 + jp .WriteAttributeAndTriggerIfDifferent +.setVibrato + ld hl, channel_1_vibrato - channel_1 + jp .WriteAttributeAndTriggerIfDifferent +.setVolumeSlide + ld hl, channel_1_volume_slide - channel_1 + jp .WriteAttributeAndTriggerIfDifferent +.setNoteCut + ld hl, channel_1_note_cut - channel_1 + jp .WriteAttributeButDoNotTrigger +.jumpOrder + inc de + ld a, [de] + ld [order], a + + ld a, 0 + ld [row], a + ld [tick], a + + call FreshOrder + call FreshRow + ret +.breakAndSetRow + inc de + + call IncrementOrder + + call FreshOrder + call FreshRow + ret +.setSpeed + inc de + ld a, [de] + ld [speed], a + inc de + jp .examinePacket +.callbackEvent + ; todo, fall through +.skipIt + inc de + inc de + jp .examinePacket + +.WriteAttributeAndTriggerIfDifferent: + ; de points to a packet + ; hl holds an offset to an attribute (e.g. channel_1_volume - channel_1) + ld a, [de] + and a, $0F + call GetChannelAttributeAddress + push hl + + inc de + ld a, [de] + cp a, [hl] + call nz, TriggerBack + ld a, [de] + pop hl + ld [hl], a + + inc de + jp .examinePacket + +.WriteAttributeButDoNotTrigger: + ld a, [de] + and a, $0F + call GetChannelAttributeAddress + + inc de + ld a, [de] + ld [hl], a + + inc de + jp .examinePacket + +GetChannelAttributeAddress: + ; this is a weird one. + ; a should have the channel number (1, 2, 3, 4) + ; hl shoulld have an offset into a channel attribute (like channel_1_note - channel_1) + ; htis will fetch into hl that attribute's address on that channel. + push hl + ld hl, .beginningsOfNoteData + ld b, 0 + ld c, a + add hl, bc + add hl, bc + ld c, [hl] + inc hl + ld b, [hl] + pop hl + add hl, bc + ret + +.beginningsOfNoteData + dw 0 + dw channel_1 + dw channel_2 + dw channel_3 + dw channel_4 + +TriggerBack: + ; de points to an address. get the low nibble one address previous, + ; take as a channel number, and write 1 to the corresponding channel_1_trigger + ; address + dec de + call TriggerIt + inc de + ret + +TriggerIt: + ld a, [de] + and a, $0F + ld hl, channel_1_trigger - channel_1 + call GetChannelAttributeAddress + ld [hl], 1 + ret + +UpdateRegisters1: + ld a, [tick] + ld b, a + ld a, [channel_1_note_cut] + cp a, b + jp nz, :+ + ld a, 0 + ld [channel_1_volume], a + + cpl + ld [channel_1_trigger], a + + ld a, $ff + ld [channel_1_note_cut], a +: + ld a, [channel_1_trigger] + cp a, 0 + ret z + + ld a, 0 + ld [rNR10], a + + + ld a, [channel_1_instrument] + ld [rNR11], a + + + ld a, [channel_1_volume] + swap a + and a, $70 + ld [rNR12], a + + ld a, [channel_1_note] + ld b, 0 + ld c, a + ld hl, Periods + add hl, bc + add hl, bc + ld a, [hl+] + ld [rNR13], a + + ld a, [hl] + or a, AUDHIGH_RESTART + ld [rNR14], a + + ld a, 0 + ld [channel_1_trigger], a + ret +UpdateRegisters2: + ld a, [tick] + ld b, a + ld a, [channel_2_note_cut] + cp a, b + jp nz, :+ + ld a, 0 + ld [channel_2_volume], a + + cpl + ld [channel_2_trigger], a + + ld a, $ff + ld [channel_2_note_cut], a +: + ld a, [channel_2_trigger] + cp a, 0 + ret z + + ld a, [channel_2_instrument] + ld [rNR21], a + + + ld a, [channel_2_volume] + swap a + and a, $f0 + ld [rNR22], a + + ld a, [channel_2_note] + ld b, 0 + ld c, a + ld hl, Periods + add hl, bc + add hl, bc + ld a, [hl+] + ld [rNR23], a + + ld a, [hl] + or a, AUDHIGH_RESTART + ld [rNR24], a + + ld a, 0 + ld [channel_2_trigger], a + ret +UpdateRegisters3: + ld a, [tick] + ld b, a + ld a, [channel_3_note_cut] + cp a, b + jp nz, :+ + ld a, 0 + ld [rNR30], a ; turn off channel 3 if we hit a note cut. + + ld a, 0 + ld [channel_3_volume], a + + ; we don't need to retrigger because we already turned off the dac. + ;ld a, 1 + ;ld [channel_3_trigger], a + + ; we do want to reset the note cut tho. + ld a, $ff + ld [channel_3_note_cut], a +: + ld a, [channel_3_trigger] + cp a, 0 + ret z ; if we don't have a trigger, then don't do anything else. + + ; instrument has a special meaning for channel 3; it refers to an index into + ; an array of 16 (could be more!) samples, defined at the top of this file. + ; (each "sample" is a 16-byte sequence of 4-bit values blah blah read the + ; pandoc for that) + ld a, [channel_3_instrument] + ld hl, channel_3_loaded_instrument + cp a, [hl] + call nz, ChangeLoadedWave + + ld a, 0 ; length should always be zero + ld [rNR31], a + + + ; volumes are pre-processed by the export script. + ld a, [channel_3_volume] + swap a + and a, $f0 + ld [rNR32], a + cp a, 0 + call nz, .turnOnDAC + ld a, [channel_3_trigger] + cp a, 0 + call nz, .turnOnDAC + ; periods don't seem to be different for channel 3, so leave it as is + ld a, [channel_3_note] + ld b, 0 + ld c, a + ld hl, Periods + add hl, bc + add hl, bc + ld a, [hl+] + ld [rNR33], a + + ld a, [hl] + or a, AUDHIGH_RESTART + ld [rNR34], a + + ld a, 0 + ld [channel_3_trigger], a + ret +.turnOnDAC: + ld a, $80 + ld [rNR30], a + ret +ChangeLoadedWave: + ; a has the nidex of the wave in the wave table at the top of the file + ld b, a + ld a, [rNR30] + push af ; save thsi for later so we can restore it. + ld a, 0 + ld [rNR30], a ; turn off the DAC for channel 3 + ld a, b + ld [channel_3_loaded_instrument], a ; mark that we've switched to it + + and a, $0F + swap a ; fast multiply by 16 + + ld hl, WaveSamples + ld b, 0 + ld c, a + add hl, bc ; hl shoulld now ponit to the appropriate wave + ld de, _AUD3WAVERAM + ld bc, 16 / 8 ; using by8s for unnecessary optimization, so divide length by 8 + call CopyRangeBy8s + + pop af + ld [rNR30], a + ret + +UpdateRegisters4: + ld a, [tick] + ld b, a + ld a, [channel_4_note_cut] + cp a, b + jp nz, :+ + ld a, 0 + ld [channel_4_volume], a + + cpl + ld [channel_4_trigger], a + + ld a, $ff + ld [channel_4_note_cut], a +: + ld a, [channel_4_trigger] + cp a, 0 + ret z + + ld a, 0; [channel_2_instrument] + ld [rNR41], a + + + ld a, [channel_4_volume] + swap a + and a, $f0 + ld [rNR42], a + ld a, [channel_4_instrument] + ld b, 0 + ld c, a + ld hl, NoiseOptions + add hl, bc + ld a, [hl] + ld [rNR43], a + + ld a, AUDHIGH_RESTART + ld [rNR44], a + + ld a, 0 + ld [channel_4_trigger], a + ret + ret \ No newline at end of file diff --git a/gb_tarot_theme.inc b/gb_tarot_theme.inc new file mode 100644 index 0000000..ae0abff --- /dev/null +++ b/gb_tarot_theme.inc @@ -0,0 +1,335 @@ +; File created by shoofle's s3m2gbt edit + +gb_tarot_theme_0: + db $00, $00, +db $11, $00, +db $21, $0F, +db $31, $40, +db $12, $18, +db $22, $04, +db $32, $40, +db $23, $00, +db $14, $18, +db $24, $0F, +db $34, $08, + + db $0F, $01, +db $84, $00, + + db $0F, $02, +db $11, $00, +db $21, $0F, +db $31, $80, +db $12, $1C, +db $22, $04, +db $32, $40, + + db $0F, $03, + + db $0F, $04, +db $11, $00, +db $21, $0F, +db $31, $C0, +db $12, $1F, +db $22, $04, +db $32, $40, +db $14, $18, +db $24, $0F, +db $34, $0B, + + db $0F, $05, +db $84, $00, + + db $0F, $06, +db $81, $00, +db $12, $18, +db $22, $04, +db $32, $40, + + db $0F, $07, + + db $0F, $08, +db $12, $1C, +db $22, $04, +db $32, $40, +db $13, $26, +db $23, $0F, +db $33, $01, +db $14, $18, +db $24, $0F, +db $34, $0B, + + db $0F, $09, +db $84, $00, + + db $0F, $0A, +db $12, $1F, +db $22, $04, +db $32, $40, + + db $0F, $0B, + + db $0F, $0C, +db $11, $00, +db $21, $0F, +db $31, $00, +db $12, $18, +db $22, $04, +db $32, $40, +db $13, $24, +db $23, $0F, +db $33, $00, +db $14, $18, +db $24, $0F, +db $34, $0B, + + db $0F, $0D, +db $84, $00, + + db $0F, $0E, +db $11, $00, +db $21, $0F, +db $31, $00, +db $12, $1C, +db $22, $04, +db $32, $40, + + db $0F, $0F, + + db $0F, $10, +db $11, $04, +db $21, $0F, +db $31, $40, +db $12, $1C, +db $22, $04, +db $32, $40, +db $13, $21, +db $23, $0F, +db $33, $00, +db $14, $18, +db $24, $0F, +db $34, $08, + + db $0F, $11, +db $84, $00, + + db $0F, $12, +db $11, $04, +db $21, $0F, +db $31, $80, +db $12, $1F, +db $22, $04, +db $32, $40, + + db $0F, $13, + + db $0F, $14, +db $11, $04, +db $21, $0F, +db $31, $C0, +db $12, $23, +db $22, $04, +db $32, $40, +db $14, $18, +db $24, $0F, +db $34, $0B, + + db $0F, $15, +db $84, $00, + + db $0F, $16, +db $81, $00, +db $12, $1C, +db $22, $04, +db $32, $40, + + db $0F, $17, + + db $0F, $18, +db $12, $1F, +db $22, $04, +db $32, $40, +db $14, $18, +db $24, $0F, +db $34, $0B, + + db $0F, $19, +db $84, $00, + + db $0F, $1A, +db $12, $23, +db $22, $04, +db $32, $40, + + db $0F, $1B, + + db $0F, $1C, +db $11, $04, +db $21, $0F, +db $31, $00, +db $12, $1C, +db $22, $04, +db $32, $40, +db $14, $18, +db $24, $0F, +db $34, $0B, + + db $0F, $1D, +db $84, $00, + + db $0F, $1E, +db $11, $04, +db $21, $0F, +db $31, $00, +db $12, $1F, +db $22, $04, +db $32, $40, + + db $0F, $1F, + + db $0F, $20, +db $11, $06, +db $21, $0F, +db $31, $40, +db $12, $1F, +db $22, $03, +db $32, $40, +db $13, $28, +db $23, $0F, +db $33, $01, +db $14, $18, +db $24, $0F, +db $34, $08, + + db $0F, $21, +db $84, $00, + + db $0F, $22, +db $11, $06, +db $21, $0F, +db $31, $80, + + db $0F, $23, + + db $0F, $24, +db $11, $06, +db $21, $0F, +db $31, $C0, +db $22, $03, +db $14, $18, +db $24, $0F, +db $34, $0B, + + db $0F, $25, +db $84, $00, + + db $0F, $26, +db $81, $00, + + db $0F, $27, + + db $0F, $28, +db $22, $02, +db $14, $18, +db $24, $0F, +db $34, $0B, + + db $0F, $29, +db $84, $00, + + db $0F, $2A, + + db $0F, $2B, + + db $0F, $2C, +db $11, $06, +db $21, $0F, +db $31, $00, +db $22, $02, +db $14, $18, +db $24, $0F, +db $34, $0B, + + db $0F, $2D, +db $84, $00, + + db $0F, $2E, +db $11, $06, +db $21, $0F, +db $31, $00, + + db $0F, $2F, + + db $0F, $30, +db $11, $05, +db $21, $0F, +db $31, $40, +db $22, $01, +db $14, $18, +db $24, $0F, +db $34, $08, + + db $0F, $31, +db $84, $00, + + db $0F, $32, +db $11, $05, +db $21, $0F, +db $31, $80, + + db $0F, $33, + + db $0F, $34, +db $11, $05, +db $21, $0F, +db $31, $C0, +db $22, $01, +db $14, $18, +db $24, $0F, +db $34, $0B, + + db $0F, $35, +db $84, $00, + + db $0F, $36, +db $81, $00, + + db $0F, $37, + + db $0F, $38, +db $22, $00, +db $14, $18, +db $24, $0F, +db $34, $0B, + + db $0F, $39, +db $84, $00, + + db $0F, $3A, + + db $0F, $3B, + + db $0F, $3C, +db $11, $11, +db $21, $0F, +db $31, $00, +db $22, $00, +db $14, $18, +db $24, $0F, +db $34, $0B, + + db $0F, $3D, +db $84, $00, + + db $0F, $3E, +db $11, $11, +db $21, $0F, +db $31, $00, + + db $0F, $3F, + +gb_tarot_theme: + db 3 + dw gb_tarot_theme_0 + dw gb_tarot_theme_0 + dw gb_tarot_theme_0 diff --git a/gb_tarot_theme.s3m b/gb_tarot_theme.s3m index 2d06bb520d9dead1846a32c0c8ed7d9bff2b4d6b..7c378f8e8ceafa406abf15aca60613bbc702282b 100644 GIT binary patch delta 574 zcmZ9Hu}i~16vpr6Vr^qvyC~Iy37AwwY$;MiM4CuJ#Nmp#N}Ki%=;j(61)-L=3N8+U zNR^1V^*^xSA-J$heS`+k!ANTJ2-uLc(Jn=i9UoCROFuF^6^5Q+qf&&WgRzG9Q zN~(}5U}mXnad&G4d6E}RJQ~)-y!vz#^NGxGd6|jXKv5W)aHtI71cCby{ z#XfNl=VJoSuukk?k9dt^;w_3<0rz-Ae8ex}GgjgPUa&*V;gEQW#e{%w^oW-@AU>e3 z$+R|6_xsxUf>(!z0k&&HW}mg}tj$(Oj2VXFRv{xV+&zFy%g#-O5tCNNq})7C*r6hA z(yvOrS&K)Ce4B)s5VRdiaXQH}Ol8_?a`))psG#l72c(6@m_cMXT-KBMd7w+Wc_M%I zyG&l8jy!jgk)L*Cjz)E3vX?ZdPCPQ?ReG|(iA6D%jIp?*My?=632V~Js>8yXD(1f# G+UpM|U4az< delta 365 zcmY+YU6vy#+b|Q^b8;Fg)jGV-U|2Y!yWFq#+cE5u-R(v@WiP~ZFKux*>VC7E;rif zNnM)gizLCXIyEal$Y_QceMaExYXiDnO#S=aY-qwruFSQ$ z;WA<5C|ol9E4R~|wS~EWKr98ZD(bc%Bq|7kChCEON+L#qC?6_Oh$P@a z6WcL*&_g+Bf|}S+O+*tV%0UkrqX#``f|BS#W9mU;)C6bSZmDzf;;rxS=KX&=Z+GVH zO!C6R^hbzRR(l%dhLRe&vGq{r!RD@3SvLU4mH-Uk1f?!z{0$0K+YPv9vW#xv-{3wQ|wcm=QFO}vHUcpo3(V|wfDaDBo}=5M zS4|t)BC?6e#Q4w9AffgRM^Q^#JI@CvVG`YvqJ^b|sj;w(uwX275x*h&d>vtRv9O%5 zKrGxySU46cgegXMnzk<8z(d$Mk@XQaC9?B;rzt}22;a8IMtKcH79>obBjnyDtW#v; zgiVO-9^b!7$dwID6#!^p|8Ei|s}!w3@`NOBNBk}%55s>s403-asxm5W-e`CgNrA!i2~c=LqAsH@yCTi% zTgIQl@kY*;-r{bIB`-_e;k!i14pG4bRw_jEt6bE zz52T3H5zcy_=L5ZUKiKuddiosDpb*DsO|{yB#?>-sniMIuDUsbs&q8L+^N$}QmECW zLMuf#DdfH<;(H_hXv80j_~Q}ZM+$kbvx;{tsiAY0`9|K?g#rtP(9B;DGv-KtIA0gh z(QiF05H?_K5mDIc5s_kZneLF37aTCxj$rGK<%H)wP{22oov0l*?kxNRX(RI? delta 1177 zcmaLW%WD%+6bA6`&LmBn)EcC;HU^wj?JJ;@)@rQ_ClB8?wx&oCq-jf0p=vAIs$Fz8 zM%0BdM+7&e;6fKJf}!Bf5?v@FRzyTZtoRRP>B5CGClA9FJc}Rae)rxpGlBfdsfC+r z>|kVAOY{tA!;|Nyr^YT%qN;pez=6$BtxO+MkcgfbFlruTy}zxvTtbL@F>PTTHlP`P z1ki>cbo8Ph`w+un#F4-VPGcNrFohY+Vjd=L;5P2!J|1EbkMIP`$fLum+W(YUDlu_3 zG&(mnI~f}7?!_Bu08NOa%3DxY#Fcl*C~@UIXiy1Lw`UVQRdk2-?gg0gaEh>T0RdPvZqoE;amBxl2d3UW3g$f%X(of4Fmvlj}j$(eAF zUMJmMbdo7&x{FrjY{*5LTe|D3qiH#N>82I8vG5v6O*O)ZAZq=J$$)BDwz6B;t?V|o zjcsQ;*iN>K4YE2LV!PQMb|>4*?qYXaMv&~|#U8ew-OCzmn2oSeHpcE}4_MMr;HUMV z$|qdmomqB{y~= 16 or channel_settings[1] >= 16 or \ channel_settings[2] >= 16 or channel_settings[3] >= 16: @@ -207,7 +209,7 @@ class S3MFile(S3MFormatReader): # Read orders - self.read_ptr = 0x60 + self.read_ptr = 0x60 # jump forward to uint8[orderCount] orderList self.song_orders = self.read_string(self.song_length) if self.song_length % 2 == 1: @@ -258,14 +260,14 @@ class S3MFile(S3MFormatReader): self.data = [] # Channels 1, 2, 4 -def s3m_volume_to_gb(s3m_vol): +def s3m_volume_to_gb(s3m_vol): # this is identical to mod files. if s3m_vol >= 64: return 15 else: return s3m_vol >> 2; # Channel 3 -def s3m_volume_to_gb_ch3(s3m_vol): +def s3m_volume_to_gb_ch3(s3m_vol): # also identicll to mod files vol = s3m_volume_to_gb(s3m_vol) if vol >= 0 and vol <= 3: @@ -281,7 +283,7 @@ def s3m_volume_to_gb_ch3(s3m_vol): else: return 0 -def s3m_note_to_gb(note): +def s3m_note_to_gb(note): # Note cut with ^^ if note == 0xFE: return 0xFE @@ -334,22 +336,25 @@ EFFECT_EVENT = 15 # the channel (like panning). def effect_s3m_to_gb(channel, effectnum, effectparams): - if effectnum == 'A': # Set Speed + if effectnum == 'A': # Set Speed. compatible + if effectparams > 0x1F: + raise RowConversionError("Unsupported BPM speed effect") + if effectparams == 0: raise RowConversionError("Speed must not be zero") - return (EFFECT_SPEED, effectparams) + return (EFFECT_SPEED, effectparams) - if effectnum == 'B': # Pattern jump + if effectnum == 'B': # Pattern jump compatible # TODO: Fail if this jumps out of bounds return (EFFECT_PATTERN_JUMP, effectparams) - elif effectnum == 'C': # Break + Set row + elif effectnum == 'C': # Break + Set row compatiblle # 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 + elif effectnum == 'D': # Volume Slide not present in .mod if channel == 3: raise RowConversionError("Volume slide not supported in channel 3") @@ -394,14 +399,15 @@ def effect_s3m_to_gb(channel, effectnum, effectparams): subeffectnum = (effectparams & 0xF0) >> 4 subeffectparams = effectparams & 0x0F - if subeffectnum == 0x8: # Pan position + if subeffectnum == 0x8: # Pan position compatible val = s3m_pan_to_gb(subeffectparams, channel) return (EFFECT_PAN, val) - elif subeffectnum == 0xC: # Notecut + elif subeffectnum == 0xC: # Notecut compatible + print(f"fofund a note cut, {effectnum}, {subeffectnum}, {subeffectparams}") return (EFFECT_NOTE_CUT, subeffectparams) - elif subeffectnum == 0xF: # Funkrepeat? Set active macro? + elif subeffectnum == 0xF: # Funkrepeat? Set active macro? not present in .mod # 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. @@ -409,167 +415,86 @@ def effect_s3m_to_gb(channel, effectnum, effectparams): raise RowConversionError(f"Unsupported effect: {effectnum}{effectparams:02X}") -HAS_VOLUME = 1 << 4 -HAS_INSTRUMENT = 1 << 5 -HAS_EFFECT = 1 << 6 +HAS_VOLUME = 1 << 5 # changing these to be compatible with old gbt +HAS_DRUM_EFFECT = 1 << 6 +HAS_EFFECT = 1 << 7 HAS_NOTE = 1 << 7 HAS_KIT = 1 << 7 -def convert_channel1(note_index, samplenum, volume, effectnum, effectparams): - command = [ 0, 0, 0, 0 ] # NOP - command_ptr = 1 +def convert_channel(channel, note_index, samplenum, volume, effectnum, effectparams): + #print(f"{channel}: {note_index}, using {samplenum} @ volume {volume}") + if channel == 4: + return convert_channel4(note_index, samplenum, volume, effectnum, effectparams) + + command = [ 0, 0, 0 ] # NOP + command_len = 1 + + if channel == 3: + shifted_instrument = (samplenum) << 7 + converted_volume = s3m_volume_to_gb_ch3(volume) + else: + shifted_instrument = (samplenum & 3) << 4 + converted_volume = s3m_volume_to_gb(volume) # Check if it's needed to add a note if note_index != -1: - note_index = s3m_note_to_gb(note_index) - command[0] |= HAS_NOTE - command[command_ptr] = note_index - command_ptr = command_ptr + 1 + command[0] = HAS_NOTE | note_index + command[1] = shifted_instrument | converted_volume + command_len = 2 - # Check if there is a sample defined - if samplenum > 0: - instrument = samplenum & 3 + if effectnum is not None: + [num, params] = effect_s3m_to_gb(channel, effectnum, effectparams) + command[1] = HAS_EFFECT | shifted_instrument | num + command[2] = params + command_len = 3 + else: + command[0] = HAS_VOLUME | converted_volume + command_len = 1 - command[0] |= HAS_INSTRUMENT - command[command_ptr] = (instrument << 4) & 0x30 + if effectnum is not None: + [num, params] = effect_s3m_to_gb(channel, effectnum, effectparams) + command[1] = HAS_EFFECT | shifted_instrument | num + command[2] = params + command_len = 3 + + return command[:command_len] - if effectnum is not None: - [num, params] = effect_s3m_to_gb(1, effectnum, effectparams) - - if num is not None: - command[0] |= HAS_EFFECT - command[command_ptr] |= num & 0x0F - command_ptr += 1 - command[command_ptr] = params & 0xFF - - # Check if it's needed to add a volume - if volume > -1: - command[0] |= HAS_VOLUME - command[0] |= s3m_volume_to_gb(volume) & 0x0F - - # Note: The volume bit doesn't affect the final size. - sizes = [ 1, 2, 3, 3, 2, 3, 4, 4 ] - command_size = sizes[command[0] >> 5] - - return command[:command_size] - -def convert_channel2(note_index, samplenum, volume, effectnum, effectparams): - command = [ 0, 0, 0, 0 ] # NOP - command_ptr = 1 - - # Check if it's needed to add a note - if note_index != -1: - note_index = s3m_note_to_gb(note_index) - command[0] |= HAS_NOTE - command[command_ptr] = note_index - command_ptr = command_ptr + 1 - - # Check if there is a sample defined - if samplenum > 0: - instrument = samplenum & 3 - - command[0] |= HAS_INSTRUMENT - command[command_ptr] = (instrument << 4) & 0x30 - - if effectnum is not None: - [num, params] = effect_s3m_to_gb(2, effectnum, effectparams) - - if num is not None: - command[0] |= HAS_EFFECT - command[command_ptr] |= num & 0x0F - command_ptr += 1 - command[command_ptr] = params & 0xFF - - # Check if it's needed to add a volume - if volume > -1: - command[0] |= HAS_VOLUME - command[0] |= s3m_volume_to_gb(volume) & 0x0F - - # Note: The volume bit doesn't affect the final size. - sizes = [ 1, 2, 3, 3, 2, 3, 4, 4 ] - command_size = sizes[command[0] >> 5] - - return command[:command_size] - -def convert_channel3(note_index, samplenum, volume, effectnum, effectparams): - command = [ 0, 0, 0, 0 ] # NOP - command_ptr = 1 - - # Check if it's needed to add a note - if note_index != -1: - note_index = s3m_note_to_gb(note_index) - command[0] |= HAS_NOTE - command[command_ptr] = note_index - command_ptr = command_ptr + 1 - - # Check if there is a sample defined - if samplenum > 0: - instrument = samplenum & 7 - - command[0] |= HAS_INSTRUMENT - command[command_ptr] = (instrument << 4) & 0xF0 - - if effectnum is not None: - [num, params] = effect_s3m_to_gb(3, effectnum, effectparams) - - if num is not None: - command[0] |= HAS_EFFECT - command[command_ptr] |= num & 0x0F - command_ptr += 1 - command[command_ptr] = params & 0xFF - - # Check if it's needed to add a volume - if volume > -1: - command[0] |= HAS_VOLUME - command[0] |= s3m_volume_to_gb_ch3(volume) & 0x0F - - # Note: The volume bit doesn't affect the final size. - sizes = [ 1, 2, 3, 3, 2, 3, 4, 4 ] - command_size = sizes[command[0] >> 5] - - return command[:command_size] def convert_channel4(note_index, samplenum, volume, effectnum, effectparams): - command = [ 0, 0, 0, 0 ] # NOP - command_ptr = 1 + command = [ 0, 0, 0 ] # NOP + command_len = 1 + #print(f"we found a note with index {note_index}") # Note cut using ^^ as note - if note_index == 0xFE: + if note_index == 169 or note_index == -1: if samplenum > 0: # This limitation is only for channel 4. It should never happen in a # regular song. raise("Note cut + Sample in same row: Not supported in channel 4") samplenum = 0xFE - # Check if there is a sample defined - if samplenum > 0: - if samplenum == 0xFE: - kit = 0xFE; - else: - kit = samplenum & 0xF; - command[0] |= HAS_KIT - command[command_ptr] = kit - command_ptr += 1 + if note_index != 169 and note_index != -1 and samplenum > 0: + command[0] = HAS_KIT | (samplenum & 0xF) + command[1] = s3m_volume_to_gb(volume) & 0xF + command_len = 2 + else: + if note_index == 169: + command[0] = HAS_DRUM_EFFECT | EFFECT_NOTE_CUT + command[1] = 1 # 0 ticks + command_len = 2 + elif effectnum is not None: + [num, params] = effect_s3m_to_gb(4, effectnum, effectparams) - if effectnum is not None: - [num, params] = effect_s3m_to_gb(4, effectnum, effectparams) + if num is not None: + command[0] = HAS_DRUM_EFFECT | (num & 0x0F) + command[1] = (params & 0xFF) | 1 + command_len = 2 + elif volume > -1: + #command[0] = HAS_VOLUME | (s3m_volume_to_gb(volume) & 0xF) + command[0] = 0 + command_len = 1 - if num is not None: - command[0] |= HAS_EFFECT - command[command_ptr] |= num & 0x0F - command_ptr += 1 - command[command_ptr] = params & 0xFF - - # Check if it's needed to add a volume - if volume > -1: - command[0] |= HAS_VOLUME - command[0] |= s3m_volume_to_gb(volume) & 0x0F - - # Note: The volume bit doesn't affect the final size. - sizes = [ 1, 2, 3, 3, 2, 3, 4, 4 ] - command_size = sizes[command[0] >> 5] - - return command[:command_size] + return command[:command_len] STARTUP_CMD_DONE = 0 STARTUP_CMD_SPEED = 1 @@ -685,7 +610,8 @@ def convert_file(module_path, song_name, output_path, export_instruments): cmd = cmd1 + cmd2 + cmd3 + cmd4 for b in cmd: - fileout.write(f"${b:02X},") + if b == -1: fileout.write(f"$FF, ") + else: fileout.write(f"${b:02X},") fileout.write("\n") @@ -707,7 +633,10 @@ def convert_file(module_path, song_name, output_path, export_instruments): note = -1 instrument = 0 if c.has_note_and_instrument: - note = c.note + note = c.note - 0b0010_0001 # note indices should start from 0=C3 + note = note + 1 # s3m note values go 00..0A, 0F, 10..1A, 1F, etc. + note = (note // 16) * 12 + (note % 16) #these expressions compensate for it. + note = note - 1 instrument = c.instrument # Rows with note and instrument but no volume use the @@ -727,17 +656,13 @@ def convert_file(module_path, song_name, output_path, export_instruments): try: if channel == 1: - cmd1 = convert_channel1(note, instrument, volume, - effectnum, effectparams) + cmd1 = convert_channel(channel, note, instrument, volume, effectnum, effectparams) elif channel == 2: - cmd2 = convert_channel2(note, instrument, volume, - effectnum, effectparams) + cmd2 = convert_channel(channel, note, instrument, volume, effectnum, effectparams) elif channel == 3: - cmd3 = convert_channel3(note, instrument, volume, - effectnum, effectparams) + cmd3 = convert_channel(channel, note, instrument, volume, effectnum, effectparams) elif channel == 4: - cmd4 = convert_channel4(note, instrument, volume, - effectnum, effectparams) + cmd4 = convert_channel(channel, note, instrument, volume, effectnum, effectparams) else: raise S3MFormatError(f"Too many channels: {channel}") except RowConversionError as e: diff --git a/s3m2shoofmt.py b/s3m2shoofmt.py index d7f03a8..3c6cb48 100644 --- a/s3m2shoofmt.py +++ b/s3m2shoofmt.py @@ -290,6 +290,7 @@ 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 @@ -297,28 +298,30 @@ def s3m_volume_to_gb_ch3(s3m_vol): vol = s3m_volume_to_gb(s3m_vol) if vol >= 0 and vol <= 3: - return 0 # 0% + return 0<<1 # 0% elif vol >= 4 and vol <= 6: - return 3 # 25% + return 3<<1 # 25% elif vol >= 7 and vol <= 9: - return 2 # 50% + return 2<<1 # 50% elif vol >= 10 and vol <= 12: - return 4 # 75% + return 1<<1 # 75% elif vol >= 13 and vol <= 15: - return 1 # 100% + 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: + if note > 0x7F or note < 0: print(note) assert note <= 0x7F @@ -433,6 +436,7 @@ def effect_s3m_to_gb(channel, effectnum, effectparams): return (EFFECT_PAN, val) elif subeffectnum == 0xC: # Notecut + print(f"found a note cut! with params {subeffectparams}") return (EFFECT_NOTE_CUT, subeffectparams) elif subeffectnum == 0xF: # Funkrepeat? Set active macro? @@ -447,19 +451,27 @@ def convert_channel(channel, note_index, samplenum, volume, effectnum, effectpar commands = [] # Check if it's needed to add a note - if note_index != -1 and note_index != 0xFF and note_index != 0xFE: + 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: - commands.append([0x80 | 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: - instrument = samplenum & 3 - commands.append([0x30 | channel, instrument << 6]) + 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) @@ -583,7 +595,7 @@ def convert_file(module_path, song_name, output_path, export_instruments): fileout.write(f"{song_name}:\n") - fileout.write(f"\tdb {len(s3m.song_orders)}\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: diff --git a/source.zip b/source.zip index dde3c625ba4dd74687e7620b3607928ae1352605..64ecc98eaa85c91cf9b0a3b4c1cc78abe9325d8a 100644 GIT binary patch delta 14873 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 RBZR~{{C^qlBk}+}{y%&V8=C+C delta 54906 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