summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTharo <17233964+Thar0@users.noreply.github.com>2024-08-08 05:11:39 +0100
committerGitHub <noreply@github.com>2024-08-08 00:11:39 -0400
commit29acf96db2fe30f0d194cc4298a3ceef367bf0e5 (patch)
tree014fa2efbc4e23bbe4594c7e127d600109caf624
parent01865243003bdeab66a079e29ba9489552ce6956 (diff)
[Audio 1/?] Extract Samplebanks and Soundfonts to XML (#2008)
* [Audio 1/?] Extract Samplebanks and Soundfonts to XML * Remove config.py and use the version yamls for addresses, other suggested changes * Adjust setup-audio * Remove some commented out dead code (MM review)
-rw-r--r--Makefile8
-rw-r--r--assets/xml/audio/samplebanks/SampleBank_0.xml433
-rw-r--r--assets/xml/audio/samplebanks/SampleBank_2.xml4
-rw-r--r--assets/xml/audio/samplebanks/SampleBank_3.xml8
-rw-r--r--assets/xml/audio/samplebanks/SampleBank_4.xml8
-rw-r--r--assets/xml/audio/samplebanks/SampleBank_5.xml9
-rw-r--r--assets/xml/audio/samplebanks/SampleBank_6.xml10
-rw-r--r--assets/xml/audio/soundfonts/Soundfont_0.xml250
-rw-r--r--assets/xml/audio/soundfonts/Soundfont_1.xml102
-rw-r--r--assets/xml/audio/soundfonts/Soundfont_10.xml23
-rw-r--r--assets/xml/audio/soundfonts/Soundfont_11.xml15
-rw-r--r--assets/xml/audio/soundfonts/Soundfont_12.xml13
-rw-r--r--assets/xml/audio/soundfonts/Soundfont_13.xml24
-rw-r--r--assets/xml/audio/soundfonts/Soundfont_14.xml15
-rw-r--r--assets/xml/audio/soundfonts/Soundfont_15.xml26
-rw-r--r--assets/xml/audio/soundfonts/Soundfont_16.xml23
-rw-r--r--assets/xml/audio/soundfonts/Soundfont_17.xml18
-rw-r--r--assets/xml/audio/soundfonts/Soundfont_18.xml29
-rw-r--r--assets/xml/audio/soundfonts/Soundfont_19.xml14
-rw-r--r--assets/xml/audio/soundfonts/Soundfont_2.xml28
-rw-r--r--assets/xml/audio/soundfonts/Soundfont_20.xml23
-rw-r--r--assets/xml/audio/soundfonts/Soundfont_21.xml20
-rw-r--r--assets/xml/audio/soundfonts/Soundfont_22.xml25
-rw-r--r--assets/xml/audio/soundfonts/Soundfont_23.xml14
-rw-r--r--assets/xml/audio/soundfonts/Soundfont_24.xml23
-rw-r--r--assets/xml/audio/soundfonts/Soundfont_25.xml19
-rw-r--r--assets/xml/audio/soundfonts/Soundfont_26.xml10
-rw-r--r--assets/xml/audio/soundfonts/Soundfont_27.xml21
-rw-r--r--assets/xml/audio/soundfonts/Soundfont_28.xml15
-rw-r--r--assets/xml/audio/soundfonts/Soundfont_29.xml22
-rw-r--r--assets/xml/audio/soundfonts/Soundfont_3.xml37
-rw-r--r--assets/xml/audio/soundfonts/Soundfont_30.xml13
-rw-r--r--assets/xml/audio/soundfonts/Soundfont_31.xml19
-rw-r--r--assets/xml/audio/soundfonts/Soundfont_32.xml32
-rw-r--r--assets/xml/audio/soundfonts/Soundfont_33.xml37
-rw-r--r--assets/xml/audio/soundfonts/Soundfont_34.xml42
-rw-r--r--assets/xml/audio/soundfonts/Soundfont_35.xml26
-rw-r--r--assets/xml/audio/soundfonts/Soundfont_36.xml17
-rw-r--r--assets/xml/audio/soundfonts/Soundfont_37.xml11
-rw-r--r--assets/xml/audio/soundfonts/Soundfont_4.xml11
-rw-r--r--assets/xml/audio/soundfonts/Soundfont_5.xml22
-rw-r--r--assets/xml/audio/soundfonts/Soundfont_6.xml16
-rw-r--r--assets/xml/audio/soundfonts/Soundfont_7.xml17
-rw-r--r--assets/xml/audio/soundfonts/Soundfont_8.xml26
-rw-r--r--assets/xml/audio/soundfonts/Soundfont_9.xml34
-rw-r--r--baseroms/gc-eu-mq-dbg/config.yml4
-rw-r--r--baseroms/gc-eu-mq/config.yml4
-rw-r--r--baseroms/gc-eu/config.yml4
-rw-r--r--baseroms/gc-us/config.yml4
-rw-r--r--tools/audio/extraction/audio_extract.py269
-rw-r--r--tools/audio/extraction/audio_tables.py107
-rw-r--r--tools/audio/extraction/audiobank_file.py956
-rw-r--r--tools/audio/extraction/audiobank_structs.py406
-rw-r--r--tools/audio/extraction/audiotable.py690
-rw-r--r--tools/audio/extraction/envelope.py119
-rw-r--r--tools/audio/extraction/tuning.py211
-rw-r--r--tools/audio/extraction/util.py126
-rw-r--r--tools/audio_extraction.py166
58 files changed, 4678 insertions, 0 deletions
diff --git a/Makefile b/Makefile
index adf690705..fa0ede70a 100644
--- a/Makefile
+++ b/Makefile
@@ -183,6 +183,9 @@ PYTHON ?= $(VENV)/bin/python3
# preprocessor for this because it won't substitute inside string literals.
SPEC_REPLACE_VARS := sed -e 's|$$(BUILD_DIR)|$(BUILD_DIR)|g'
+# Audio tools
+AUDIO_EXTRACT := $(PYTHON) tools/audio_extraction.py
+
CFLAGS += $(CPP_DEFINES)
CPPFLAGS += $(CPP_DEFINES)
@@ -427,6 +430,10 @@ venv:
$(PYTHON) -m pip install -U pip
$(PYTHON) -m pip install -U -r requirements.txt
+# TODO this is a temporary rule for testing audio, to be removed
+setup-audio:
+ $(AUDIO_EXTRACT) -o $(EXTRACTED_DIR) -v $(VERSION) --read-xml
+
setup: venv
$(MAKE) -C tools
$(PYTHON) tools/decompress_baserom.py $(VERSION)
@@ -434,6 +441,7 @@ setup: venv
$(PYTHON) tools/extract_incbins.py $(EXTRACTED_DIR)/baserom --oot-version $(VERSION) -o $(EXTRACTED_DIR)/incbin
$(PYTHON) tools/msgdis.py $(VERSION)
$(PYTHON) extract_assets.py -v $(VERSION) -j$(N_THREADS)
+ $(AUDIO_EXTRACT) -o $(EXTRACTED_DIR) -v $(VERSION) --read-xml
disasm:
$(RM) -r $(EXPECTED_DIR)
diff --git a/assets/xml/audio/samplebanks/SampleBank_0.xml b/assets/xml/audio/samplebanks/SampleBank_0.xml
new file mode 100644
index 000000000..336d17028
--- /dev/null
+++ b/assets/xml/audio/samplebanks/SampleBank_0.xml
@@ -0,0 +1,433 @@
+<!-- This file is only for extraction of vanilla data. For other purposes see assets/audio/samplebanks/ -->
+<SampleBank Name="SampleBank_0" Index="0">
+ <Sample Name="SAMPLE_0_0" FileName="Sample0" Offset="0x000000" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_1" FileName="Sample1" Offset="0x0005B0" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_2" FileName="Sample2" Offset="0x000780" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_3" FileName="Sample3" Offset="0x000C70" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_4" FileName="Sample4" Offset="0x001730" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_5" FileName="Sample5" Offset="0x001ED0" SampleRate="22050" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_6" FileName="Sample6" Offset="0x002D10" SampleRate="32000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_7" FileName="Sample7" Offset="0x004250" SampleRate="32000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_8" FileName="Sample8" Offset="0x005480" SampleRate="32000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_9" FileName="Sample9" Offset="0x0077D0" SampleRate="32000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_10" FileName="Sample10" Offset="0x0097E0" SampleRate="32000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_11" FileName="Sample11" Offset="0x00B660" SampleRate="32000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_12" FileName="Sample12" Offset="0x00BC80" SampleRate="32000" BaseNote="F3"/>
+ <Sample Name="SAMPLE_0_13" FileName="Sample13" Offset="0x00E4B0" SampleRate="16000" BaseNote="C0"/>
+ <Sample Name="SAMPLE_0_14" FileName="Sample14" Offset="0x011D80" SampleRate="24000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_15" FileName="Sample15" Offset="0x01C4A0" SampleRate="22050" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_16" FileName="Sample16" Offset="0x027DA0" SampleRate="22050" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_17" FileName="Sample17" Offset="0x033800" SampleRate="22050" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_18" FileName="Sample18" Offset="0x0393F0" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_19" FileName="Sample19" Offset="0x03D090" SampleRate="32000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_20" FileName="Sample20" Offset="0x03E360" SampleRate="22050" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_21" FileName="Sample21" Offset="0x03ECE0" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_22" FileName="Sample22" Offset="0x03F770" SampleRate="16000" BaseNote="G4"/>
+ <Sample Name="SAMPLE_0_23" FileName="Sample23" Offset="0x040000" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_24" FileName="Sample24" Offset="0x040AA0" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_25" FileName="Sample25" Offset="0x0414F0" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_26" FileName="Sample26" Offset="0x041A20" SampleRate="8000" BaseNote="G3"/>
+ <Sample Name="SAMPLE_0_27" FileName="Sample27" Offset="0x041CD0" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_28" FileName="Sample28" Offset="0x0439A0" SampleRate="22050" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_29" FileName="Sample29" Offset="0x046810" SampleRate="22050" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_30" FileName="Sample30" Offset="0x048840" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_31" FileName="Sample31" Offset="0x04A510" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_32" FileName="Sample32" Offset="0x04A6A0" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_33" FileName="Sample33" Offset="0x04F790" SampleRate="23220" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_34" FileName="Sample34" Offset="0x053210" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_35" FileName="Sample35" Offset="0x055BA0" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_36" FileName="Sample36" Offset="0x057D10" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_37" FileName="Sample37" Offset="0x05B590" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_38" FileName="Sample38" Offset="0x05CBA0" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_39" FileName="Sample39" Offset="0x060240" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_40" FileName="Sample40" Offset="0x061B90" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_41" FileName="Sample41" Offset="0x063350" SampleRate="16000" BaseNote="AF4"/>
+ <Sample Name="SAMPLE_0_42" FileName="Sample42" Offset="0x063B70" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_43" FileName="Sample43" Offset="0x064600" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_44" FileName="Sample44" Offset="0x064F70" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_45" FileName="Sample45" Offset="0x065580" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_46" FileName="Sample46" Offset="0x067FA0" SampleRate="22050" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_47" FileName="Sample47" Offset="0x068CC0" SampleRate="22050" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_48" FileName="Sample48" Offset="0x069130" SampleRate="22050" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_49" FileName="Sample49" Offset="0x06A040" SampleRate="22050" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_50" FileName="Sample50" Offset="0x06BA00" SampleRate="22050" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_51" FileName="Sample51" Offset="0x06C3C0" SampleRate="22050" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_52" FileName="Sample52" Offset="0x06D5B0" SampleRate="22050" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_53" FileName="Sample53" Offset="0x071C70" SampleRate="22050" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_54" FileName="Sample54" Offset="0x0732A0" SampleRate="22050" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_55" FileName="Sample55" Offset="0x074790" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_56" FileName="Sample56" Offset="0x077010" SampleRate="22050" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_57" FileName="Sample57" Offset="0x077BA0" SampleRate="22050" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_58" FileName="Sample58" Offset="0x07AFD0" SampleRate="22050" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_59" FileName="Sample59" Offset="0x07D290" SampleRate="22050" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_60" FileName="Sample60" Offset="0x07F3F0" SampleRate="22050" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_61" FileName="Sample61" Offset="0x07FC90" SampleRate="22050" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_62" FileName="Sample62" Offset="0x080E00" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_63" FileName="Sample63" Offset="0x082000" SampleRate="22050" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_64" FileName="Sample64" Offset="0x082ED0" SampleRate="22050" BaseNote="C0"/>
+ <Sample Name="SAMPLE_0_65" FileName="Sample65" Offset="0x084380" SampleRate="22050" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_66" FileName="Sample66" Offset="0x087030" SampleRate="22050" BaseNote="C0"/>
+ <Sample Name="SAMPLE_0_67" FileName="Sample67" Offset="0x087440" SampleRate="16000" BaseNote="C0"/>
+ <Sample Name="SAMPLE_0_68" FileName="Sample68" Offset="0x088620" SampleRate="22050" BaseNote="C0"/>
+ <Sample Name="SAMPLE_0_69" FileName="Sample69" Offset="0x088A50" SampleRate="45530" BaseNote="F1"/>
+ <Sample Name="SAMPLE_0_70" FileName="Sample70" Offset="0x08A4B0" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_71" FileName="Sample71" Offset="0x08E160" SampleRate="16000" BaseNote="C0"/>
+ <Sample Name="SAMPLE_0_72" FileName="Sample72" Offset="0x08F6F0" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_73" FileName="Sample73" Offset="0x093DB0" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_74" FileName="Sample74" Offset="0x094B10" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_75" FileName="Sample75" Offset="0x098B00" SampleRate="22050" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_76" FileName="Sample76" Offset="0x09D5F0" SampleRate="16000" BaseNote="C0"/>
+ <Sample Name="SAMPLE_0_77" FileName="Sample77" Offset="0x0A0260" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_78" FileName="Sample78" Offset="0x0A14A0" SampleRate="22050" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_79" FileName="Sample79" Offset="0x0A2590" SampleRate="22050" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_80" FileName="Sample80" Offset="0x0A9EF0" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_81" FileName="Sample81" Offset="0x0AB9E0" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_82" FileName="Sample82" Offset="0x0ADBA0" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_83" FileName="Sample83" Offset="0x0AF0A0" SampleRate="16000" BaseNote="C0"/>
+ <Sample Name="SAMPLE_0_84" FileName="Sample84" Offset="0x0B0960" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_85" FileName="Sample85" Offset="0x0B3600" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_86" FileName="Sample86" Offset="0x0B3B10" SampleRate="24000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_87" FileName="Sample87" Offset="0x0B4B90" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_88" FileName="Sample88" Offset="0x0B5A80" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_89" FileName="Sample89" Offset="0x0B8690" SampleRate="16000" BaseNote="C0"/>
+ <Sample Name="SAMPLE_0_90" FileName="Sample90" Offset="0x0BA0D0" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_91" FileName="Sample91" Offset="0x0BBB00" SampleRate="32000" BaseNote="B1"/>
+ <Sample Name="SAMPLE_0_92" FileName="Sample92" Offset="0x0C42B0" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_93" FileName="Sample93" Offset="0x0C5140" SampleRate="22050" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_94" FileName="Sample94" Offset="0x0C88C0" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_95" FileName="Sample95" Offset="0x0CAF60" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_96" FileName="Sample96" Offset="0x0D16F0" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_97" FileName="Sample97" Offset="0x0D2110" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_98" FileName="Sample98" Offset="0x0D3DC0" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_99" FileName="Sample99" Offset="0x0D57A0" SampleRate="22050" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_100" FileName="Sample100" Offset="0x0DE0A0" SampleRate="22050" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_101" FileName="Sample101" Offset="0x0E01F0" SampleRate="16000" BaseNote="C0"/>
+ <Sample Name="SAMPLE_0_102" FileName="Sample102" Offset="0x0E2510" SampleRate="22050" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_103" FileName="Sample103" Offset="0x0E44A0" SampleRate="22050" BaseNote="C0"/>
+ <Sample Name="SAMPLE_0_104" FileName="Sample104" Offset="0x0E4B00" SampleRate="16000" BaseNote="C0"/>
+ <Sample Name="SAMPLE_0_105" FileName="Sample105" Offset="0x0E5B70" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_106" FileName="Sample106" Offset="0x0EA760" SampleRate="8000" BaseNote="A0"/>
+ <Sample Name="SAMPLE_0_107" FileName="Sample107" Offset="0x0EAFC0" SampleRate="16000" BaseNote="C0"/>
+ <Sample Name="SAMPLE_0_108" FileName="Sample108" Offset="0x0EB5B0" SampleRate="16000" BaseNote="C0"/>
+ <Sample Name="SAMPLE_0_109" FileName="Sample109" Offset="0x0ECF40" SampleRate="16000" BaseNote="C0"/>
+ <Sample Name="SAMPLE_0_110" FileName="Sample110" Offset="0x0EEB80" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_111" FileName="Sample111" Offset="0x0F2FE0" SampleRate="22050" BaseNote="GF3"/>
+ <Sample Name="SAMPLE_0_112" FileName="Sample112" Offset="0x0F5350" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_113" FileName="Sample113" Offset="0x0F5A90" SampleRate="22050" BaseNote="C0"/>
+ <Sample Name="SAMPLE_0_114" FileName="Sample114" Offset="0x0F72A0" SampleRate="22050" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_115" FileName="Sample115" Offset="0x0FAD40" SampleRate="32000" BaseNote="E3"/>
+ <Sample Name="SAMPLE_0_116" FileName="Sample116" Offset="0x0FDFC0" SampleRate="32000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_117" FileName="Sample117" Offset="0x1026F0" SampleRate="24000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_118" FileName="Sample118" Offset="0x106C20" SampleRate="22050" BaseNote="E3"/>
+ <Sample Name="SAMPLE_0_119" FileName="Sample119" Offset="0x108690" SampleRate="32000" BaseNote="F2"/>
+ <Sample Name="SAMPLE_0_120" FileName="Sample120" Offset="0x10CD20" SampleRate="32000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_121" FileName="Sample121" Offset="0x10EFF0" SampleRate="32000" BaseNote="D4"/>
+ <Sample Name="SAMPLE_0_122" FileName="Sample122" Offset="0x111520" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_123" FileName="Sample123" Offset="0x1129E0" SampleRate="16000" BaseNote="C0"/>
+ <Sample Name="SAMPLE_0_124" FileName="Sample124" Offset="0x114D70" SampleRate="20000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_125" FileName="Sample125" Offset="0x1165E0" SampleRate="20000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_126" FileName="Sample126" Offset="0x1176B0" SampleRate="20000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_127" FileName="Sample127" Offset="0x118910" SampleRate="20000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_128" FileName="Sample128" Offset="0x119870" SampleRate="20000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_129" FileName="Sample129" Offset="0x11A270" SampleRate="20000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_130" FileName="Sample130" Offset="0x11B0E0" SampleRate="20000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_131" FileName="Sample131" Offset="0x11B790" SampleRate="20000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_132" FileName="Sample132" Offset="0x11BE20" SampleRate="20000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_133" FileName="Sample133" Offset="0x11D6D0" SampleRate="20000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_134" FileName="Sample134" Offset="0x11EE50" SampleRate="20000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_135" FileName="Sample135" Offset="0x11FB00" SampleRate="20000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_136" FileName="Sample136" Offset="0x120D60" SampleRate="20000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_137" FileName="Sample137" Offset="0x121BF0" SampleRate="20000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_138" FileName="Sample138" Offset="0x122C10" SampleRate="20000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_139" FileName="Sample139" Offset="0x1243F0" SampleRate="20000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_140" FileName="Sample140" Offset="0x125A90" SampleRate="20000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_141" FileName="Sample141" Offset="0x126ED0" SampleRate="20000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_142" FileName="Sample142" Offset="0x128DA0" SampleRate="20000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_143" FileName="Sample143" Offset="0x12AC80" SampleRate="20000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_144" FileName="Sample144" Offset="0x12CD30" SampleRate="20000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_145" FileName="Sample145" Offset="0x133AB0" SampleRate="20000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_146" FileName="Sample146" Offset="0x139D70" SampleRate="20000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_147" FileName="Sample147" Offset="0x13AF50" SampleRate="20000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_148" FileName="Sample148" Offset="0x13C040" SampleRate="20000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_149" FileName="Sample149" Offset="0x13D3B0" SampleRate="20000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_150" FileName="Sample150" Offset="0x13E870" SampleRate="20000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_151" FileName="Sample151" Offset="0x13F6B0" SampleRate="20000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_152" FileName="Sample152" Offset="0x140FC0" SampleRate="20000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_153" FileName="Sample153" Offset="0x141960" SampleRate="20000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_154" FileName="Sample154" Offset="0x143690" SampleRate="20000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_155" FileName="Sample155" Offset="0x146490" SampleRate="20000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_156" FileName="Sample156" Offset="0x1478F0" SampleRate="20000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_157" FileName="Sample157" Offset="0x148390" SampleRate="20000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_158" FileName="Sample158" Offset="0x1493C0" SampleRate="20000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_159" FileName="Sample159" Offset="0x14B440" SampleRate="20000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_160" FileName="Sample160" Offset="0x14C250" SampleRate="20000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_161" FileName="Sample161" Offset="0x151520" SampleRate="20000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_162" FileName="Sample162" Offset="0x15A6A0" SampleRate="20000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_163" FileName="Sample163" Offset="0x15BE50" SampleRate="20000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_164" FileName="Sample164" Offset="0x15DCB0" SampleRate="20000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_165" FileName="Sample165" Offset="0x15F4B0" SampleRate="20000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_166" FileName="Sample166" Offset="0x1617F0" SampleRate="20000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_167" FileName="Sample167" Offset="0x163280" SampleRate="20000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_168" FileName="Sample168" Offset="0x166790" SampleRate="20000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_169" FileName="Sample169" Offset="0x167800" SampleRate="20000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_170" FileName="Sample170" Offset="0x168650" SampleRate="20000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_171" FileName="Sample171" Offset="0x1698E0" SampleRate="20000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_172" FileName="Sample172" Offset="0x16A450" SampleRate="20000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_173" FileName="Sample173" Offset="0x16AEA0" SampleRate="20000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_174" FileName="Sample174" Offset="0x16B3E0" SampleRate="20000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_175" FileName="Sample175" Offset="0x16CE20" SampleRate="20000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_176" FileName="Sample176" Offset="0x16EEA0" SampleRate="20000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_177" FileName="Sample177" Offset="0x16FE60" SampleRate="20000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_178" FileName="Sample178" Offset="0x170E80" SampleRate="20000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_179" FileName="Sample179" Offset="0x171DA0" SampleRate="20000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_180" FileName="Sample180" Offset="0x173320" SampleRate="20000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_181" FileName="Sample181" Offset="0x174240" SampleRate="20000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_182" FileName="Sample182" Offset="0x175140" SampleRate="20000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_183" FileName="Sample183" Offset="0x175C30" SampleRate="20000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_184" FileName="Sample184" Offset="0x177480" SampleRate="20000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_185" FileName="Sample185" Offset="0x178D40" SampleRate="20000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_186" FileName="Sample186" Offset="0x1795F0" SampleRate="20000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_187" FileName="Sample187" Offset="0x17E6C0" SampleRate="20000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_188" FileName="Sample188" Offset="0x182EC0" SampleRate="20000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_189" FileName="Sample189" Offset="0x1844E0" SampleRate="20000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_190" FileName="Sample190" Offset="0x185BA0" SampleRate="20000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_191" FileName="Sample191" Offset="0x188170" SampleRate="20000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_192" FileName="Sample192" Offset="0x18A560" SampleRate="20000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_193" FileName="Sample193" Offset="0x18B650" SampleRate="20000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_194" FileName="Sample194" Offset="0x18D090" SampleRate="20000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_195" FileName="Sample195" Offset="0x18E480" SampleRate="20000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_196" FileName="Sample196" Offset="0x190200" SampleRate="20000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_197" FileName="Sample197" Offset="0x193410" SampleRate="20000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_198" FileName="Sample198" Offset="0x196950" SampleRate="20000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_199" FileName="Sample199" Offset="0x1971A0" SampleRate="20000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_200" FileName="Sample200" Offset="0x197500" SampleRate="20000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_201" FileName="Sample201" Offset="0x198F30" SampleRate="20000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_202" FileName="Sample202" Offset="0x19F6D0" SampleRate="20000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_203" FileName="Sample203" Offset="0x1A1670" SampleRate="20000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_204" FileName="Sample204" Offset="0x1A2580" SampleRate="20000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_205" FileName="Sample205" Offset="0x1A71E0" SampleRate="20000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_206" FileName="Sample206" Offset="0x1A79C0" SampleRate="20000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_207" FileName="Sample207" Offset="0x1AF2E0" SampleRate="20000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_208" FileName="Sample208" Offset="0x1AFEE0" SampleRate="20000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_209" FileName="Sample209" Offset="0x1B3F50" SampleRate="20000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_210" FileName="Sample210" Offset="0x1B6530" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_211" FileName="Sample211" Offset="0x1B7B40" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_212" FileName="Sample212" Offset="0x1B8DB0" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_213" FileName="Sample213" Offset="0x1B9D70" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_214" FileName="Sample214" Offset="0x1BB410" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_215" FileName="Sample215" Offset="0x1BC380" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_216" FileName="Sample216" Offset="0x1BFA40" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_217" FileName="Sample217" Offset="0x1C0F80" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_218" FileName="Sample218" Offset="0x1C2510" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_219" FileName="Sample219" Offset="0x1C6870" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_220" FileName="Sample220" Offset="0x1CC560" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_221" FileName="Sample221" Offset="0x1CF9C0" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_222" FileName="Sample222" Offset="0x1D25C0" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_223" FileName="Sample223" Offset="0x1D58F0" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_224" FileName="Sample224" Offset="0x1D74B0" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_225" FileName="Sample225" Offset="0x1D8420" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_226" FileName="Sample226" Offset="0x1D9880" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_227" FileName="Sample227" Offset="0x1DDA40" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_228" FileName="Sample228" Offset="0x1DE3C0" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_229" FileName="Sample229" Offset="0x1DF040" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_230" FileName="Sample230" Offset="0x1E0140" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_231" FileName="Sample231" Offset="0x1E3160" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_232" FileName="Sample232" Offset="0x1E48E0" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_233" FileName="Sample233" Offset="0x1E55F0" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_234" FileName="Sample234" Offset="0x1E6000" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_235" FileName="Sample235" Offset="0x1E7E90" SampleRate="16000" BaseNote="BF3"/>
+ <Sample Name="SAMPLE_0_236" FileName="Sample236" Offset="0x1E9960" SampleRate="16000" BaseNote="BF3"/>
+ <Sample Name="SAMPLE_0_237" FileName="Sample237" Offset="0x1EB6B0" SampleRate="16000" BaseNote="BF3"/>
+ <Sample Name="SAMPLE_0_238" FileName="Sample238" Offset="0x1EC2B0" SampleRate="16000" BaseNote="BF3"/>
+ <Sample Name="SAMPLE_0_239" FileName="Sample239" Offset="0x1EC990" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_240" FileName="Sample240" Offset="0x1EE390" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_241" FileName="Sample241" Offset="0x1EF1B0" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_242" FileName="Sample242" Offset="0x1EFCE0" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_243" FileName="Sample243" Offset="0x1F1600" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_244" FileName="Sample244" Offset="0x1F3020" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_245" FileName="Sample245" Offset="0x1F4010" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_246" FileName="Sample246" Offset="0x1F4EB0" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_247" FileName="Sample247" Offset="0x1F5E50" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_248" FileName="Sample248" Offset="0x1F7220" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_249" FileName="Sample249" Offset="0x1F9490" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_250" FileName="Sample250" Offset="0x1FA230" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_251" FileName="Sample251" Offset="0x1FB850" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_252" FileName="Sample252" Offset="0x1FEA30" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_253" FileName="Sample253" Offset="0x1FFD80" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_254" FileName="Sample254" Offset="0x200840" SampleRate="22050" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_255" FileName="Sample255" Offset="0x206D10" SampleRate="22050" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_256" FileName="Sample256" Offset="0x209E40" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_257" FileName="Sample257" Offset="0x20B580" SampleRate="16000" BaseNote="C0"/>
+ <Sample Name="SAMPLE_0_258" FileName="Sample258" Offset="0x20BDA0" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_259" FileName="Sample259" Offset="0x20C9E0" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_260" FileName="Sample260" Offset="0x20DE70" SampleRate="16000" BaseNote="C0"/>
+ <Sample Name="SAMPLE_0_261" FileName="Sample261" Offset="0x20F440" SampleRate="16000" BaseNote="C0"/>
+ <Sample Name="SAMPLE_0_262" FileName="Sample262" Offset="0x2115A0" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_263" FileName="Sample263" Offset="0x212040" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_264" FileName="Sample264" Offset="0x2147A0" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_265" FileName="Sample265" Offset="0x214E10" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_266" FileName="Sample266" Offset="0x215810" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_267" FileName="Sample267" Offset="0x216210" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_268" FileName="Sample268" Offset="0x216D00" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_269" FileName="Sample269" Offset="0x2187D0" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_270" FileName="Sample270" Offset="0x2193E0" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_271" FileName="Sample271" Offset="0x219F10" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_272" FileName="Sample272" Offset="0x21A810" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_273" FileName="Sample273" Offset="0x21AA80" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_274" FileName="Sample274" Offset="0x21ACF0" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_275" FileName="Sample275" Offset="0x21B150" SampleRate="16000" BaseNote="C0"/>
+ <Sample Name="SAMPLE_0_276" FileName="Sample276" Offset="0x21B370" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_277" FileName="Sample277" Offset="0x21BBC0" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_278" FileName="Sample278" Offset="0x21C330" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_279" FileName="Sample279" Offset="0x21CF00" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_280" FileName="Sample280" Offset="0x21DDB0" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_281" FileName="Sample281" Offset="0x220770" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_282" FileName="Sample282" Offset="0x2222F0" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_283" FileName="Sample283" Offset="0x222BA0" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_284" FileName="Sample284" Offset="0x2230B0" SampleRate="16000" BaseNote="C0"/>
+ <Sample Name="SAMPLE_0_285" FileName="Sample285" Offset="0x223C90" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_286" FileName="Sample286" Offset="0x224FD0" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_287" FileName="Sample287" Offset="0x228A70" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_288" FileName="Sample288" Offset="0x22C380" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_289" FileName="Sample289" Offset="0x22DD50" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_290" FileName="Sample290" Offset="0x233280" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_291" FileName="Sample291" Offset="0x236F80" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_292" FileName="Sample292" Offset="0x237BA0" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_293" FileName="Sample293" Offset="0x23AD60" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_294" FileName="Sample294" Offset="0x23D490" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_295" FileName="Sample295" Offset="0x240600" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_296" FileName="Sample296" Offset="0x241690" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_297" FileName="Sample297" Offset="0x241C20" SampleRate="16000" BaseNote="C0"/>
+ <Sample Name="SAMPLE_0_298" FileName="Sample298" Offset="0x243440" SampleRate="16000" BaseNote="C0"/>
+ <Sample Name="SAMPLE_0_299" FileName="Sample299" Offset="0x244DD0" SampleRate="16000" BaseNote="BF3"/>
+ <Sample Name="SAMPLE_0_300" FileName="Sample300" Offset="0x247290" SampleRate="16000" BaseNote="BF3"/>
+ <Sample Name="SAMPLE_0_301" FileName="Sample301" Offset="0x249810" SampleRate="16000" BaseNote="BF3"/>
+ <Sample Name="SAMPLE_0_302" FileName="Sample302" Offset="0x24AB70" SampleRate="16000" BaseNote="BF3"/>
+ <Sample Name="SAMPLE_0_303" FileName="Sample303" Offset="0x24BBE0" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_304" FileName="Sample304" Offset="0x24C2C0" SampleRate="16000" BaseNote="BF3"/>
+ <Sample Name="SAMPLE_0_305" FileName="Sample305" Offset="0x24E060" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_306" FileName="Sample306" Offset="0x24F440" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_307" FileName="Sample307" Offset="0x24FED0" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_308" FileName="Sample308" Offset="0x250E20" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_309" FileName="Sample309" Offset="0x2516D0" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_310" FileName="Sample310" Offset="0x252B10" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_311" FileName="Sample311" Offset="0x255720" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_312" FileName="Sample312" Offset="0x2564A0" SampleRate="16000" BaseNote="C0"/>
+ <Sample Name="SAMPLE_0_313" FileName="Sample313" Offset="0x2581C0" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_314" FileName="Sample314" Offset="0x258360" SampleRate="16000" BaseNote="BF3"/>
+ <Sample Name="SAMPLE_0_315" FileName="Sample315" Offset="0x259120" SampleRate="16000" BaseNote="BF3"/>
+ <Sample Name="SAMPLE_0_316" FileName="Sample316" Offset="0x259BF0" SampleRate="16000" BaseNote="BF3"/>
+ <Sample Name="SAMPLE_0_317" FileName="Sample317" Offset="0x25AAC0" SampleRate="16000" BaseNote="BF3"/>
+ <Sample Name="SAMPLE_0_318" FileName="Sample318" Offset="0x25C140" SampleRate="16000" BaseNote="BF3"/>
+ <Sample Name="SAMPLE_0_319" FileName="Sample319" Offset="0x25CD60" SampleRate="16000" BaseNote="AF3"/>
+ <Sample Name="SAMPLE_0_320" FileName="Sample320" Offset="0x25DB50" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_321" FileName="Sample321" Offset="0x25E9E0" SampleRate="16000" BaseNote="C0"/>
+ <Sample Name="SAMPLE_0_322" FileName="Sample322" Offset="0x25FC50" SampleRate="16000" BaseNote="BF3"/>
+ <Sample Name="SAMPLE_0_323" FileName="Sample323" Offset="0x260420" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_324" FileName="Sample324" Offset="0x261790" SampleRate="16000" BaseNote="AF3"/>
+ <Sample Name="SAMPLE_0_325" FileName="Sample325" Offset="0x262170" SampleRate="16000" BaseNote="AF3"/>
+ <Sample Name="SAMPLE_0_326" FileName="Sample326" Offset="0x263270" SampleRate="22050" BaseNote="BF3"/>
+ <Sample Name="SAMPLE_0_327" FileName="Sample327" Offset="0x264B60" SampleRate="16000" BaseNote="AF3"/>
+ <Sample Name="SAMPLE_0_328" FileName="Sample328" Offset="0x26A060" SampleRate="16000" BaseNote="BF3"/>
+ <Sample Name="SAMPLE_0_329" FileName="Sample329" Offset="0x26AAD0" SampleRate="16000" BaseNote="BF3"/>
+ <Sample Name="SAMPLE_0_330" FileName="Sample330" Offset="0x26B960" SampleRate="16000" BaseNote="BF3"/>
+ <Sample Name="SAMPLE_0_331" FileName="Sample331" Offset="0x26BEF0" SampleRate="16000" BaseNote="BF3"/>
+ <Sample Name="SAMPLE_0_332" FileName="Sample332" Offset="0x26CE70" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_333" FileName="Sample333" Offset="0x26DF40" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_334" FileName="Sample334" Offset="0x26FC60" SampleRate="16000" BaseNote="BF3"/>
+ <Sample Name="SAMPLE_0_335" FileName="Sample335" Offset="0x270E40" SampleRate="16000" BaseNote="BF3"/>
+ <Sample Name="SAMPLE_0_336" FileName="Sample336" Offset="0x271A70" SampleRate="16000" BaseNote="AF3"/>
+ <Sample Name="SAMPLE_0_337" FileName="Sample337" Offset="0x273860" SampleRate="16000" BaseNote="AF3"/>
+ <Sample Name="SAMPLE_0_338" FileName="Sample338" Offset="0x274EA0" SampleRate="16000" BaseNote="BF3"/>
+ <Sample Name="SAMPLE_0_339" FileName="Sample339" Offset="0x2765B0" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_340" FileName="Sample340" Offset="0x27C0A0" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_341" FileName="Sample341" Offset="0x27E260" SampleRate="16000" BaseNote="AF3"/>
+ <Sample Name="SAMPLE_0_342" FileName="Sample342" Offset="0x280C70" SampleRate="16000" BaseNote="G3"/>
+ <Sample Name="SAMPLE_0_343" FileName="Sample343" Offset="0x283440" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_344" FileName="Sample344" Offset="0x2869C0" SampleRate="22050" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_345" FileName="Sample345" Offset="0x28BF50" SampleRate="22050" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_346" FileName="Sample346" Offset="0x28CE90" SampleRate="22050" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_347" FileName="Sample347" Offset="0x2938F0" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_348" FileName="Sample348" Offset="0x29B480" SampleRate="16000" BaseNote="C0"/>
+ <Sample Name="SAMPLE_0_349" FileName="Sample349" Offset="0x29EB20" SampleRate="32000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_350" FileName="Sample350" Offset="0x29FE30" SampleRate="32000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_351" FileName="Sample351" Offset="0x2A4D40" SampleRate="32000" BaseNote="G4"/>
+ <Sample Name="SAMPLE_0_352" FileName="Sample352" Offset="0x2A8500" SampleRate="32000" BaseNote="C2"/>
+ <Sample Name="SAMPLE_0_353" FileName="Sample353" Offset="0x2AF020" SampleRate="22050" BaseNote="GF4"/>
+ <Sample Name="SAMPLE_0_354" FileName="Sample354" Offset="0x2B43B0" SampleRate="22050" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_355" FileName="Sample355" Offset="0x2B9E60" SampleRate="22050" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_356" FileName="Sample356" Offset="0x2C8510" SampleRate="22050" BaseNote="A5"/>
+ <Sample Name="SAMPLE_0_357" FileName="Sample357" Offset="0x2CFEE0" SampleRate="32000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_358" FileName="Sample358" Offset="0x2D96A0" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_359" FileName="Sample359" Offset="0x2DFF40" SampleRate="22050" BaseNote="G3"/>
+ <Sample Name="SAMPLE_0_360" FileName="Sample360" Offset="0x2E7410" SampleRate="22050" BaseNote="DF3"/>
+ <Sample Name="SAMPLE_0_361" FileName="Sample361" Offset="0x2EF650" SampleRate="24000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_362" FileName="Sample362" Offset="0x2F3300" SampleRate="32000" BaseNote="DF3"/>
+ <Sample Name="SAMPLE_0_363" FileName="Sample363" Offset="0x2F8690" SampleRate="32000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_364" FileName="Sample364" Offset="0x2F9A90" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_365" FileName="Sample365" Offset="0x2FD270" SampleRate="32000" BaseNote="F3"/>
+ <Sample Name="SAMPLE_0_366" FileName="Sample366" Offset="0x301EC0" SampleRate="32000" BaseNote="AF5"/>
+ <Sample Name="SAMPLE_0_367" FileName="Sample367" Offset="0x304E30" SampleRate="22000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_368" FileName="Sample368" Offset="0x309BD0" SampleRate="32000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_369" FileName="Sample369" Offset="0x30C360" SampleRate="32000" BaseNote="D4"/>
+ <Sample Name="SAMPLE_0_370" FileName="Sample370" Offset="0x30EAF0" SampleRate="32000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_371" FileName="Sample371" Offset="0x317260" SampleRate="32000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_372" FileName="Sample372" Offset="0x31F640" SampleRate="32000" BaseNote="C6"/>
+ <Sample Name="SAMPLE_0_373" FileName="Sample373" Offset="0x324660" SampleRate="32000" BaseNote="E4"/>
+ <Sample Name="SAMPLE_0_374" FileName="Sample374" Offset="0x328D10" SampleRate="32000" BaseNote="E3"/>
+ <Sample Name="SAMPLE_0_375" FileName="Sample375" Offset="0x32CA00" SampleRate="32000" BaseNote="E3"/>
+ <Sample Name="SAMPLE_0_376" FileName="Sample376" Offset="0x32ECB0" SampleRate="32000" BaseNote="E4"/>
+ <Sample Name="SAMPLE_0_377" FileName="Sample377" Offset="0x332D10" SampleRate="32000" BaseNote="A2"/>
+ <Sample Name="SAMPLE_0_378" FileName="Sample378" Offset="0x335740" SampleRate="32000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_379" FileName="Sample379" Offset="0x337A10" SampleRate="32000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_380" FileName="Sample380" Offset="0x338450" SampleRate="32000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_381" FileName="Sample381" Offset="0x33A980" SampleRate="32000" BaseNote="AF2"/>
+ <Sample Name="SAMPLE_0_382" FileName="Sample382" Offset="0x33BC30" SampleRate="32000" BaseNote="EF5"/>
+ <Sample Name="SAMPLE_0_383" FileName="Sample383" Offset="0x33D130" SampleRate="32000" BaseNote="DF5"/>
+ <Sample Name="SAMPLE_0_384" FileName="Sample384" Offset="0x3403F0" SampleRate="32000" BaseNote="D3"/>
+ <Sample Name="SAMPLE_0_385" FileName="Sample385" Offset="0x343870" SampleRate="32000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_386" FileName="Sample386" Offset="0x34D670" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_387" FileName="Sample387" Offset="0x351810" SampleRate="32000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_388" FileName="Sample388" Offset="0x359170" SampleRate="24000" BaseNote="AF5"/>
+ <Sample Name="SAMPLE_0_389" FileName="Sample389" Offset="0x35C900" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_390" FileName="Sample390" Offset="0x35D100" SampleRate="22050" BaseNote="A3"/>
+ <Sample Name="SAMPLE_0_391" FileName="Sample391" Offset="0x364580" SampleRate="22050" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_392" FileName="Sample392" Offset="0x36FE80" SampleRate="27777" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_393" FileName="Sample393" Offset="0x374C20" SampleRate="32000" BaseNote="D3"/>
+ <Sample Name="SAMPLE_0_394" FileName="Sample394" Offset="0x378360" SampleRate="32000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_395" FileName="Sample395" Offset="0x37A6B0" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_396" FileName="Sample396" Offset="0x37B2A0" SampleRate="32000" BaseNote="F4"/>
+ <Sample Name="SAMPLE_0_397" FileName="Sample397" Offset="0x382830" SampleRate="32000" BaseNote="BF3"/>
+ <Sample Name="SAMPLE_0_398" FileName="Sample398" Offset="0x384ED0" SampleRate="32000" BaseNote="BF2"/>
+ <Sample Name="SAMPLE_0_399" FileName="Sample399" Offset="0x387060" SampleRate="48000" BaseNote="D3"/>
+ <Sample Name="SAMPLE_0_400" FileName="Sample400" Offset="0x38F000" SampleRate="32000" BaseNote="BF4"/>
+ <Sample Name="SAMPLE_0_401" FileName="Sample401" Offset="0x3926D0" SampleRate="32000" BaseNote="BF3"/>
+ <Sample Name="SAMPLE_0_402" FileName="Sample402" Offset="0x396F80" SampleRate="32000" BaseNote="C6"/>
+ <Sample Name="SAMPLE_0_403" FileName="Sample403" Offset="0x397FA0" SampleRate="32000" BaseNote="G5"/>
+ <Sample Name="SAMPLE_0_404" FileName="Sample404" Offset="0x399790" SampleRate="24000" BaseNote="GF5"/>
+ <Sample Name="SAMPLE_0_405" FileName="Sample405" Offset="0x39A6B0" SampleRate="32000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_406" FileName="Sample406" Offset="0x3A4BE0" SampleRate="24000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_407" FileName="Sample407" Offset="0x3AA6E0" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_408" FileName="Sample408" Offset="0x3ACC20" SampleRate="32000" BaseNote="DF4"/>
+ <Sample Name="SAMPLE_0_409" FileName="Sample409" Offset="0x3AE1C0" SampleRate="24000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_410" FileName="Sample410" Offset="0x3B45D0" SampleRate="32000" BaseNote="AF5"/>
+ <Sample Name="SAMPLE_0_411" FileName="Sample411" Offset="0x3B5490" SampleRate="32000" BaseNote="E4"/>
+ <Sample Name="SAMPLE_0_412" FileName="Sample412" Offset="0x3B63F0" SampleRate="32000" BaseNote="E3"/>
+ <Sample Name="SAMPLE_0_413" FileName="Sample413" Offset="0x3B71F0" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_414" FileName="Sample414" Offset="0x3B94B0" SampleRate="32000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_415" FileName="Sample415" Offset="0x3BBC80" SampleRate="22050" BaseNote="F6"/>
+ <Sample Name="SAMPLE_0_416" FileName="Sample416" Offset="0x3BE750" SampleRate="32000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_417" FileName="Sample417" Offset="0x3BF6A0" SampleRate="32000" BaseNote="A1"/>
+ <Sample Name="SAMPLE_0_418" FileName="Sample418" Offset="0x3C0360" SampleRate="24000" BaseNote="AF5"/>
+ <Sample Name="SAMPLE_0_419" FileName="Sample419" Offset="0x3C2B40" SampleRate="24000" BaseNote="F4"/>
+ <Sample Name="SAMPLE_0_420" FileName="Sample420" Offset="0x3C4D00" SampleRate="24000" BaseNote="BF2"/>
+ <Sample Name="SAMPLE_0_421" FileName="Sample421" Offset="0x3C6BD0" SampleRate="32000" BaseNote="EF3"/>
+ <Sample Name="SAMPLE_0_422" FileName="Sample422" Offset="0x3CAC50" SampleRate="22050" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_423" FileName="Sample423" Offset="0x3CF8A0" SampleRate="16000" BaseNote="DF6"/>
+ <Sample Name="SAMPLE_0_424" FileName="Sample424" Offset="0x3D0140" SampleRate="22050" BaseNote="B5"/>
+ <Sample Name="SAMPLE_0_425" FileName="Sample425" Offset="0x3D0C00" SampleRate="32000" BaseNote="BF4"/>
+ <Sample Name="SAMPLE_0_426" FileName="Sample426" Offset="0x3D3A40" SampleRate="32000" BaseNote="EF4"/>
+ <Sample Name="SAMPLE_0_427" FileName="Sample427" Offset="0x3D62E0" SampleRate="16000" BaseNote="C2"/>
+ <Sample Name="SAMPLE_0_428" FileName="Sample428" Offset="0x3E3F50" SampleRate="32000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_0_429" FileName="Sample429" Offset="0x3E8BB0" SampleRate="32000" BaseNote="BF2"/>
+</SampleBank>
diff --git a/assets/xml/audio/samplebanks/SampleBank_2.xml b/assets/xml/audio/samplebanks/SampleBank_2.xml
new file mode 100644
index 000000000..21e76424e
--- /dev/null
+++ b/assets/xml/audio/samplebanks/SampleBank_2.xml
@@ -0,0 +1,4 @@
+<!-- This file is only for extraction of vanilla data. For other purposes see assets/audio/samplebanks/ -->
+<SampleBank Name="SampleBank_2" Index="2">
+ <Sample Name="SAMPLE_2_0" FileName="Sample0" Offset="0x000000" SampleRate="32000" BaseNote="F4"/>
+</SampleBank>
diff --git a/assets/xml/audio/samplebanks/SampleBank_3.xml b/assets/xml/audio/samplebanks/SampleBank_3.xml
new file mode 100644
index 000000000..e6738f8b3
--- /dev/null
+++ b/assets/xml/audio/samplebanks/SampleBank_3.xml
@@ -0,0 +1,8 @@
+<!-- This file is only for extraction of vanilla data. For other purposes see assets/audio/samplebanks/ -->
+<SampleBank Name="SampleBank_3" Index="3">
+ <Sample Name="SAMPLE_3_0" FileName="Sample0" Offset="0x000000" SampleRate="32000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_3_1" FileName="Sample1" Offset="0x008BC0" SampleRate="32000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_3_2" FileName="Sample2" Offset="0x00A590" SampleRate="32000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_3_3" FileName="Sample3" Offset="0x00B3B0" SampleRate="32000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_3_4" FileName="Sample4" Offset="0x016480" SampleRate="32000" BaseNote="C4"/>
+</SampleBank>
diff --git a/assets/xml/audio/samplebanks/SampleBank_4.xml b/assets/xml/audio/samplebanks/SampleBank_4.xml
new file mode 100644
index 000000000..8d68e285f
--- /dev/null
+++ b/assets/xml/audio/samplebanks/SampleBank_4.xml
@@ -0,0 +1,8 @@
+<!-- This file is only for extraction of vanilla data. For other purposes see assets/audio/samplebanks/ -->
+<SampleBank Name="SampleBank_4" Index="4">
+ <Sample Name="SAMPLE_4_0" FileName="Sample0" Offset="0x000000" SampleRate="24000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_4_1" FileName="Sample1" Offset="0x006410" SampleRate="32000" BaseNote="AF5"/>
+ <Sample Name="SAMPLE_4_2" FileName="Sample2" Offset="0x0072D0" SampleRate="32000" BaseNote="E4"/>
+ <Sample Name="SAMPLE_4_3" FileName="Sample3" Offset="0x008230" SampleRate="32000" BaseNote="E3"/>
+ <Sample Name="SAMPLE_4_4" FileName="Sample4" Offset="0x009030" SampleRate="32000" BaseNote="DF4"/>
+</SampleBank>
diff --git a/assets/xml/audio/samplebanks/SampleBank_5.xml b/assets/xml/audio/samplebanks/SampleBank_5.xml
new file mode 100644
index 000000000..6eb735693
--- /dev/null
+++ b/assets/xml/audio/samplebanks/SampleBank_5.xml
@@ -0,0 +1,9 @@
+<!-- This file is only for extraction of vanilla data. For other purposes see assets/audio/samplebanks/ -->
+<SampleBank Name="SampleBank_5" Index="5">
+ <Sample Name="SAMPLE_5_0" FileName="Sample0" Offset="0x000000" SampleRate="16000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_5_1" FileName="Sample1" Offset="0x002540" SampleRate="32000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_5_2" FileName="Sample2" Offset="0x004800" SampleRate="22050" BaseNote="BF4"/>
+ <Sample Name="SAMPLE_5_3" FileName="Sample3" Offset="0x0072D0" SampleRate="16000" BaseNote="DF6"/>
+ <Sample Name="SAMPLE_5_4" FileName="Sample4" Offset="0x007B70" SampleRate="22050" BaseNote="B5"/>
+ <Sample Name="SAMPLE_5_5" FileName="Sample5" Offset="0x008630" SampleRate="22050" BaseNote="A3"/>
+</SampleBank>
diff --git a/assets/xml/audio/samplebanks/SampleBank_6.xml b/assets/xml/audio/samplebanks/SampleBank_6.xml
new file mode 100644
index 000000000..e6971659b
--- /dev/null
+++ b/assets/xml/audio/samplebanks/SampleBank_6.xml
@@ -0,0 +1,10 @@
+<!-- This file is only for extraction of vanilla data. For other purposes see assets/audio/samplebanks/ -->
+<SampleBank Name="SampleBank_6" Index="6">
+ <Sample Name="SAMPLE_6_0" FileName="Sample0" Offset="0x000000" SampleRate="32000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_6_1" FileName="Sample1" Offset="0x00B0D0" SampleRate="32000" BaseNote="BF2"/>
+ <Sample Name="SAMPLE_6_2" FileName="Sample2" Offset="0x00E120" SampleRate="32000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_6_3" FileName="Sample3" Offset="0x0103E0" SampleRate="22050" BaseNote="BF4"/>
+ <Sample Name="SAMPLE_6_4" FileName="Sample4" Offset="0x012EB0" SampleRate="32000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_6_5" FileName="Sample5" Offset="0x01D3E0" SampleRate="24000" BaseNote="C4"/>
+ <Sample Name="SAMPLE_6_6" FileName="Sample6" Offset="0x022EE0" SampleRate="24000" BaseNote="C4"/>
+</SampleBank>
diff --git a/assets/xml/audio/soundfonts/Soundfont_0.xml b/assets/xml/audio/soundfonts/Soundfont_0.xml
new file mode 100644
index 000000000..02f75704f
--- /dev/null
+++ b/assets/xml/audio/soundfonts/Soundfont_0.xml
@@ -0,0 +1,250 @@
+<!-- This file is only for extraction of vanilla data. For other purposes see assets/audio/soundfonts/ -->
+<SoundFont Name="Soundfont_0" Index="0">
+ <Envelopes>
+ <Envelope Name="Env0"/>
+ <Envelope Name="Env1"/>
+ <Envelope Name="Env2"/>
+ <Envelope Name="Env3"/>
+ <Envelope Name="Env4"/>
+ <Envelope Name="Env5"/>
+ <Envelope Name="Env6"/>
+ <Envelope Name="Env7"/>
+ </Envelopes>
+ <Instruments>
+ <Instrument ProgramNumber="0" Name="INST_0"/>
+ <Instrument ProgramNumber="1" Name="INST_1"/>
+ <Instrument ProgramNumber="2" Name="INST_2"/>
+ <Instrument ProgramNumber="3" Name="INST_3"/>
+ <Instrument ProgramNumber="4" Name="INST_4"/>
+ <Instrument ProgramNumber="5" Name="INST_5"/>
+ <Instrument ProgramNumber="6" Name="INST_6"/>
+ <Instrument ProgramNumber="7" Name="INST_7"/>
+ <Instrument ProgramNumber="11" Name="INST_11"/>
+ <Instrument ProgramNumber="8" Name="INST_8"/>
+ <Instrument ProgramNumber="9" Name="INST_9"/>
+ <Instrument ProgramNumber="10" Name="INST_10"/>
+ <Instrument ProgramNumber="12" Name="INST_12"/>
+ <Instrument ProgramNumber="13" Name="INST_13"/>
+ <Instrument ProgramNumber="14" Name="INST_14"/>
+ <Instrument ProgramNumber="16" Name="INST_16"/>
+ <Instrument ProgramNumber="17" Name="INST_17"/>
+ <Instrument ProgramNumber="18" Name="INST_18"/>
+ <Instrument ProgramNumber="19" Name="INST_19"/>
+ <Instrument ProgramNumber="20" Name="INST_20"/>
+ <Instrument ProgramNumber="21" Name="INST_21"/>
+ <Instrument ProgramNumber="22" Name="INST_22"/>
+ <Instrument ProgramNumber="23" Name="INST_23"/>
+ <Instrument ProgramNumber="24" Name="INST_24"/>
+ <Instrument ProgramNumber="25" Name="INST_25"/>
+ <Instrument ProgramNumber="15" Name="INST_15"/>
+ <Instrument ProgramNumber="26" Name="INST_26"/>
+ <Instrument ProgramNumber="27" Name="INST_27"/>
+ <Instrument ProgramNumber="28" Name="INST_28"/>
+ <Instrument ProgramNumber="29" Name="INST_29"/>
+ <Instrument ProgramNumber="30" Name="INST_30"/>
+ <Instrument ProgramNumber="31" Name="INST_31"/>
+ <Instrument ProgramNumber="33" Name="INST_33"/>
+ <Instrument ProgramNumber="32" Name="INST_32"/>
+ <Instrument ProgramNumber="34" Name="INST_34"/>
+ <Instrument ProgramNumber="35" Name="INST_35"/>
+ <Instrument ProgramNumber="36" Name="INST_36"/>
+ <Instrument ProgramNumber="91" Name="INST_91"/>
+ <Instrument ProgramNumber="52" Name="INST_52"/>
+ <Instrument ProgramNumber="53" Name="INST_53"/>
+ <Instrument ProgramNumber="37" Name="INST_37"/>
+ <Instrument ProgramNumber="38" Name="INST_38"/>
+ <Instrument ProgramNumber="39" Name="INST_39"/>
+ <Instrument ProgramNumber="40" Name="INST_40"/>
+ <Instrument ProgramNumber="41" Name="INST_41"/>
+ <Instrument ProgramNumber="42" Name="INST_42"/>
+ <Instrument ProgramNumber="43" Name="INST_43"/>
+ <Instrument ProgramNumber="44" Name="INST_44"/>
+ <Instrument ProgramNumber="45" Name="INST_45"/>
+ <Instrument ProgramNumber="47" Name="INST_47"/>
+ <Instrument ProgramNumber="48" Name="INST_48"/>
+ <Instrument ProgramNumber="50" Name="INST_50"/>
+ <Instrument ProgramNumber="51" Name="INST_51"/>
+ <Instrument ProgramNumber="54" Name="INST_54"/>
+ <Instrument ProgramNumber="55" Name="INST_55"/>
+ <Instrument ProgramNumber="56" Name="INST_56"/>
+ <Instrument ProgramNumber="57" Name="INST_57"/>
+ <Instrument ProgramNumber="58" Name="INST_58"/>
+ <Instrument ProgramNumber="59" Name="INST_59"/>
+ <Instrument ProgramNumber="61" Name="INST_61"/>
+ <Instrument ProgramNumber="62" Name="INST_62"/>
+ <Instrument ProgramNumber="63" Name="INST_63"/>
+ <Instrument ProgramNumber="64" Name="INST_64"/>
+ <Instrument ProgramNumber="65" Name="INST_65"/>
+ <Instrument ProgramNumber="66" Name="INST_66"/>
+ <Instrument ProgramNumber="67" Name="INST_67"/>
+ <Instrument ProgramNumber="68" Name="INST_68"/>
+ <Instrument ProgramNumber="69" Name="INST_69"/>
+ <Instrument ProgramNumber="70" Name="INST_70"/>
+ <Instrument ProgramNumber="71" Name="INST_71"/>
+ <Instrument ProgramNumber="72" Name="INST_72"/>
+ <Instrument ProgramNumber="73" Name="INST_73"/>
+ <Instrument ProgramNumber="74" Name="INST_74"/>
+ <Instrument ProgramNumber="75" Name="INST_75"/>
+ <Instrument ProgramNumber="76" Name="INST_76"/>
+ <Instrument ProgramNumber="77" Name="INST_77"/>
+ <Instrument ProgramNumber="78" Name="INST_78"/>
+ <Instrument ProgramNumber="60" Name="INST_60"/>
+ <Instrument ProgramNumber="79" Name="INST_79"/>
+ <Instrument ProgramNumber="80" Name="INST_80"/>
+ <Instrument ProgramNumber="81" Name="INST_81"/>
+ <Instrument ProgramNumber="46" Name="INST_46"/>
+ <Instrument ProgramNumber="84" Name="INST_84"/>
+ <Instrument ProgramNumber="87" Name="INST_87"/>
+ <Instrument ProgramNumber="88" Name="INST_88"/>
+ <Instrument ProgramNumber="89" Name="INST_89"/>
+ <Instrument ProgramNumber="90" Name="INST_90"/>
+ <Instrument ProgramNumber="85" Name="INST_85"/>
+ <Instrument ProgramNumber="86" Name="INST_86"/>
+ <Instrument ProgramNumber="83" Name="INST_83"/>
+ <Instrument ProgramNumber="82" Name="INST_82"/>
+ </Instruments>
+ <Drums>
+ <Drum Name="DRUM_0"/>
+ <Drum Name="DRUM_1"/>
+ <Drum Name="DRUM_2"/>
+ <Drum Name="DRUM_3"/>
+ </Drums>
+ <Effects>
+ <Effect Name="EFFECT_0"/>
+ <Effect Name="EFFECT_1"/>
+ <Effect Name="EFFECT_2"/>
+ <Effect Name="EFFECT_3"/>
+ <Effect Name="EFFECT_4"/>
+ <Effect Name="EFFECT_5"/>
+ <Effect Name="EFFECT_6"/>
+ <Effect Name="EFFECT_7"/>
+ <Effect Name="EFFECT_8"/>
+ <Effect Name="EFFECT_9"/>
+ <Effect Name="EFFECT_10"/>
+ <Effect Name="EFFECT_11"/>
+ <Effect Name="EFFECT_12"/>
+ <Effect Name="EFFECT_13"/>
+ <Effect Name="EFFECT_14"/>
+ <Effect Name="EFFECT_15"/>
+ <Effect Name="EFFECT_16"/>
+ <Effect Name="EFFECT_17"/>
+ <Effect Name="EFFECT_18"/>
+ <Effect Name="EFFECT_19"/>
+ <Effect Name="EFFECT_20"/>
+ <Effect Name="EFFECT_21"/>
+ <Effect Name="EFFECT_22"/>
+ <Effect Name="EFFECT_23"/>
+ <Effect Name="EFFECT_24"/>
+ <Effect Name="EFFECT_25"/>
+ <Effect Name="EFFECT_26"/>
+ <Effect Name="EFFECT_27"/>
+ <Effect Name="EFFECT_28"/>
+ <Effect Name="EFFECT_29"/>
+ <Effect Name="EFFECT_30"/>
+ <Effect Name="EFFECT_31"/>
+ <Effect Name="EFFECT_32"/>
+ <Effect Name="EFFECT_33"/>
+ <Effect Name="EFFECT_34"/>
+ <Effect Name="EFFECT_35"/>
+ <Effect Name="EFFECT_36"/>
+ <Effect Name="EFFECT_37"/>
+ <Effect Name="EFFECT_38"/>
+ <Effect Name="EFFECT_39"/>
+ <Effect Name="EFFECT_40"/>
+ <Effect Name="EFFECT_41"/>
+ <Effect Name="EFFECT_42"/>
+ <Effect Name="EFFECT_43"/>
+ <Effect Name="EFFECT_44"/>
+ <Effect Name="EFFECT_45"/>
+ <Effect Name="EFFECT_46"/>
+ <Effect Name="EFFECT_47"/>
+ <Effect Name="EFFECT_48"/>
+ <Effect Name="EFFECT_49"/>
+ <Effect Name="EFFECT_50"/>
+ <Effect Name="EFFECT_51"/>
+ <Effect Name="EFFECT_52"/>
+ <Effect Name="EFFECT_53"/>
+ <Effect Name="EFFECT_54"/>
+ <Effect Name="EFFECT_55"/>
+ <Effect Name="EFFECT_56"/>
+ <Effect Name="EFFECT_57"/>
+ <Effect Name="EFFECT_58"/>
+ <Effect Name="EFFECT_59"/>
+ <Effect Name="EFFECT_60"/>
+ <Effect Name="EFFECT_61"/>
+ <Effect Name="EFFECT_62"/>
+ <Effect Name="EFFECT_63"/>
+ <Effect Name="EFFECT_64"/>
+ <Effect Name="EFFECT_65"/>
+ <Effect Name="EFFECT_66"/>
+ <Effect Name="EFFECT_67"/>
+ <Effect Name="EFFECT_68"/>
+ <Effect Name="EFFECT_69"/>
+ <Effect Name="EFFECT_70"/>
+ <Effect Name="EFFECT_71"/>
+ <Effect Name="EFFECT_72"/>
+ <Effect Name="EFFECT_73"/>
+ <Effect Name="EFFECT_74"/>
+ <Effect Name="EFFECT_75"/>
+ <Effect Name="EFFECT_76"/>
+ <Effect Name="EFFECT_77"/>
+ <Effect Name="EFFECT_78"/>
+ <Effect Name="EFFECT_79"/>
+ <Effect Name="EFFECT_80"/>
+ <Effect Name="EFFECT_81"/>
+ <Effect Name="EFFECT_82"/>
+ <Effect Name="EFFECT_83"/>
+ <Effect Name="EFFECT_84"/>
+ <Effect Name="EFFECT_85"/>
+ <Effect Name="EFFECT_86"/>
+ <Effect Name="EFFECT_87"/>
+ <Effect Name="EFFECT_88"/>
+ <Effect Name="EFFECT_89"/>
+ <Effect Name="EFFECT_90"/>
+ <Effect Name="EFFECT_91"/>
+ <Effect Name="EFFECT_92"/>
+ <Effect Name="EFFECT_93"/>
+ <Effect Name="EFFECT_94"/>
+ <Effect Name="EFFECT_95"/>
+ <Effect Name="EFFECT_96"/>
+ <Effect Name="EFFECT_97"/>
+ <Effect Name="EFFECT_98"/>
+ <Effect Name="EFFECT_99"/>
+ <Effect Name="EFFECT_100"/>
+ <Effect Name="EFFECT_101"/>
+ <Effect Name="EFFECT_102"/>
+ <Effect Name="EFFECT_103"/>
+ <Effect Name="EFFECT_104"/>
+ <Effect Name="EFFECT_105"/>
+ <Effect Name="EFFECT_106"/>
+ <Effect Name="EFFECT_107"/>
+ <Effect Name="EFFECT_108"/>
+ <Effect Name="EFFECT_109"/>
+ <Effect Name="EFFECT_110"/>
+ <Effect Name="EFFECT_111"/>
+ <Effect Name="EFFECT_112"/>
+ <Effect Name="EFFECT_113"/>
+ <Effect Name="EFFECT_114"/>
+ <Effect Name="EFFECT_115"/>
+ <Effect Name="EFFECT_116"/>
+ <Effect Name="EFFECT_117"/>
+ <Effect Name="EFFECT_118"/>
+ <Effect Name="EFFECT_119"/>
+ <Effect Name="EFFECT_120"/>
+ <Effect Name="EFFECT_121"/>
+ <Effect Name="EFFECT_122"/>
+ <Effect Name="EFFECT_123"/>
+ <Effect Name="EFFECT_124"/>
+ <Effect Name="EFFECT_125"/>
+ <Effect Name="EFFECT_126"/>
+ <Effect Name="EFFECT_127"/>
+ <Effect Name="EFFECT_128"/>
+ <Effect Name="EFFECT_129"/>
+ <Effect Name="EFFECT_130"/>
+ <Effect Name="EFFECT_131"/>
+ <Effect Name="EFFECT_132"/>
+ <Effect Name="EFFECT_133"/>
+ <Effect Name="EFFECT_134"/>
+ <Effect Name="EFFECT_135"/>
+ </Effects>
+</SoundFont>
diff --git a/assets/xml/audio/soundfonts/Soundfont_1.xml b/assets/xml/audio/soundfonts/Soundfont_1.xml
new file mode 100644
index 000000000..6376f6d11
--- /dev/null
+++ b/assets/xml/audio/soundfonts/Soundfont_1.xml
@@ -0,0 +1,102 @@
+<!-- This file is only for extraction of vanilla data. For other purposes see assets/audio/soundfonts/ -->
+<SoundFont Name="Soundfont_1" Index="1">
+ <Envelopes>
+ <Envelope Name="Env0"/>
+ </Envelopes>
+ <Instruments>
+ <Instrument ProgramNumber="0" Name="INST_0"/>
+ <Instrument ProgramNumber="1" Name="INST_1"/>
+ <Instrument ProgramNumber="2" Name="INST_2"/>
+ <Instrument ProgramNumber="18" Name="INST_18"/>
+ <Instrument ProgramNumber="19" Name="INST_19"/>
+ <Instrument ProgramNumber="20" Name="INST_20"/>
+ <Instrument ProgramNumber="3" Name="INST_3"/>
+ <Instrument ProgramNumber="4" Name="INST_4"/>
+ <Instrument ProgramNumber="5" Name="INST_5"/>
+ <Instrument ProgramNumber="6" Name="INST_6"/>
+ <Instrument ProgramNumber="7" Name="INST_7"/>
+ <Instrument ProgramNumber="11" Name="INST_11"/>
+ <Instrument ProgramNumber="8" Name="INST_8"/>
+ <Instrument ProgramNumber="9" Name="INST_9"/>
+ <Instrument ProgramNumber="10" Name="INST_10"/>
+ <Instrument ProgramNumber="23" Name="INST_23"/>
+ <Instrument ProgramNumber="12" Name="INST_12"/>
+ <Instrument ProgramNumber="13" Name="INST_13"/>
+ <Instrument ProgramNumber="15" Name="INST_15"/>
+ <Instrument ProgramNumber="14" Name="INST_14"/>
+ <Instrument ProgramNumber="16" Name="INST_16"/>
+ <Instrument ProgramNumber="17" Name="INST_17"/>
+ <Instrument ProgramNumber="25" Name="INST_25"/>
+ <Instrument ProgramNumber="26" Name="INST_26"/>
+ <Instrument ProgramNumber="27" Name="INST_27"/>
+ <Instrument ProgramNumber="28" Name="INST_28"/>
+ <Instrument ProgramNumber="21" Name="INST_21"/>
+ <Instrument ProgramNumber="24" Name="INST_24"/>
+ <Instrument ProgramNumber="29" Name="INST_29"/>
+ <Instrument ProgramNumber="30" Name="INST_30"/>
+ <Instrument ProgramNumber="31" Name="INST_31"/>
+ <Instrument ProgramNumber="32" Name="INST_32"/>
+ <Instrument ProgramNumber="33" Name="INST_33"/>
+ <Instrument ProgramNumber="34" Name="INST_34"/>
+ <Instrument ProgramNumber="35" Name="INST_35"/>
+ <Instrument ProgramNumber="36" Name="INST_36"/>
+ <Instrument ProgramNumber="37" Name="INST_37"/>
+ <Instrument ProgramNumber="41" Name="INST_41"/>
+ <Instrument ProgramNumber="42" Name="INST_42"/>
+ <Instrument ProgramNumber="43" Name="INST_43"/>
+ <Instrument ProgramNumber="45" Name="INST_45"/>
+ <Instrument ProgramNumber="46" Name="INST_46"/>
+ <Instrument ProgramNumber="48" Name="INST_48"/>
+ <Instrument ProgramNumber="50" Name="INST_50"/>
+ <Instrument ProgramNumber="38" Name="INST_38"/>
+ <Instrument ProgramNumber="39" Name="INST_39"/>
+ <Instrument ProgramNumber="40" Name="INST_40"/>
+ <Instrument ProgramNumber="44" Name="INST_44"/>
+ <Instrument ProgramNumber="22" Name="INST_22"/>
+ <Instrument ProgramNumber="47" Name="INST_47"/>
+ <Instrument ProgramNumber="49" Name="INST_49"/>
+ </Instruments>
+ <Effects>
+ <Effect Name="EFFECT_0"/>
+ <Effect Name="EFFECT_1"/>
+ <Effect Name="EFFECT_2"/>
+ <Effect Name="EFFECT_3"/>
+ <Effect Name="EFFECT_4"/>
+ <Effect Name="EFFECT_5"/>
+ <Effect Name="EFFECT_6"/>
+ <Effect Name="EFFECT_7"/>
+ <Effect Name="EFFECT_8"/>
+ <Effect Name="EFFECT_9"/>
+ <Effect Name="EFFECT_10"/>
+ <Effect Name="EFFECT_11"/>
+ <Effect Name="EFFECT_12"/>
+ <Effect Name="EFFECT_13"/>
+ <Effect Name="EFFECT_14"/>
+ <Effect Name="EFFECT_15"/>
+ <Effect Name="EFFECT_16"/>
+ <Effect Name="EFFECT_17"/>
+ <Effect Name="EFFECT_18"/>
+ <Effect Name="EFFECT_19"/>
+ <Effect Name="EFFECT_20"/>
+ <Effect Name="EFFECT_21"/>
+ <Effect Name="EFFECT_22"/>
+ <Effect Name="EFFECT_23"/>
+ <Effect Name="EFFECT_24"/>
+ <Effect Name="EFFECT_25"/>
+ <Effect Name="EFFECT_26"/>
+ <Effect Name="EFFECT_27"/>
+ <Effect Name="EFFECT_28"/>
+ <Effect Name="EFFECT_29"/>
+ <Effect Name="EFFECT_30"/>
+ <Effect Name="EFFECT_31"/>
+ <Effect Name="EFFECT_32"/>
+ <Effect Name="EFFECT_33"/>
+ <Effect Name="EFFECT_34"/>
+ <Effect Name="EFFECT_35"/>
+ <Effect Name="EFFECT_36"/>
+ <Effect Name="EFFECT_37"/>
+ <Effect Name="EFFECT_38"/>
+ <Effect Name="EFFECT_39"/>
+ <Effect Name="EFFECT_40"/>
+ </Effects>
+</SoundFont>
diff --git a/assets/xml/audio/soundfonts/Soundfont_10.xml b/assets/xml/audio/soundfonts/Soundfont_10.xml
new file mode 100644
index 000000000..578101fd4
--- /dev/null
+++ b/assets/xml/audio/soundfonts/Soundfont_10.xml
@@ -0,0 +1,23 @@
+<!-- This file is only for extraction of vanilla data. For other purposes see assets/audio/soundfonts/ -->
+<SoundFont Name="Soundfont_10" Index="10">
+ <Envelopes>
+ <Envelope Name="Env0"/>
+ <Envelope Name="Env1"/>
+ <Envelope Name="Env2"/>
+ <Envelope Name="Env3"/>
+ <Envelope Name="Env4"/>
+ </Envelopes>
+ <Instruments>
+ <Instrument ProgramNumber="3" Name="INST_3"/>
+ <Instrument ProgramNumber="5" Name="INST_5"/>
+ <Instrument ProgramNumber="6" Name="INST_6"/>
+ <Instrument ProgramNumber="10" Name="INST_10"/>
+ <Instrument ProgramNumber="11" Name="INST_11"/>
+ <Instrument ProgramNumber="12" Name="INST_12"/>
+ <Instrument ProgramNumber="13" Name="INST_13"/>
+ </Instruments>
+ <Drums>
+ <Drum Name="DRUM_0"/>
+ <Drum Name="DRUM_1"/>
+ </Drums>
+</SoundFont>
diff --git a/assets/xml/audio/soundfonts/Soundfont_11.xml b/assets/xml/audio/soundfonts/Soundfont_11.xml
new file mode 100644
index 000000000..72b4f77fd
--- /dev/null
+++ b/assets/xml/audio/soundfonts/Soundfont_11.xml
@@ -0,0 +1,15 @@
+<!-- This file is only for extraction of vanilla data. For other purposes see assets/audio/soundfonts/ -->
+<SoundFont Name="Soundfont_11" Index="11">
+ <Envelopes>
+ <Envelope Name="Env0"/>
+ <Envelope Name="Env1"/>
+ <Envelope Name="Env2"/>
+ <Envelope Name="Env3"/>
+ </Envelopes>
+ <Instruments>
+ <Instrument ProgramNumber="0" Name="INST_0"/>
+ <Instrument ProgramNumber="1" Name="INST_1"/>
+ <Instrument ProgramNumber="2" Name="INST_2"/>
+ <Instrument ProgramNumber="3" Name="INST_3"/>
+ </Instruments>
+</SoundFont>
diff --git a/assets/xml/audio/soundfonts/Soundfont_12.xml b/assets/xml/audio/soundfonts/Soundfont_12.xml
new file mode 100644
index 000000000..4800feced
--- /dev/null
+++ b/assets/xml/audio/soundfonts/Soundfont_12.xml
@@ -0,0 +1,13 @@
+<!-- This file is only for extraction of vanilla data. For other purposes see assets/audio/soundfonts/ -->
+<SoundFont Name="Soundfont_12" Index="12">
+ <Envelopes>
+ <Envelope Name="Env0"/>
+ <Envelope Name="Env1"/>
+ <Envelope Name="Env2"/>
+ </Envelopes>
+ <Instruments>
+ <Instrument ProgramNumber="0" Name="INST_0"/>
+ <Instrument ProgramNumber="1" Name="INST_1"/>
+ <Instrument ProgramNumber="2" Name="INST_2"/>
+ </Instruments>
+</SoundFont>
diff --git a/assets/xml/audio/soundfonts/Soundfont_13.xml b/assets/xml/audio/soundfonts/Soundfont_13.xml
new file mode 100644
index 000000000..b399905de
--- /dev/null
+++ b/assets/xml/audio/soundfonts/Soundfont_13.xml
@@ -0,0 +1,24 @@
+<!-- This file is only for extraction of vanilla data. For other purposes see assets/audio/soundfonts/ -->
+<SoundFont Name="Soundfont_13" Index="13">
+ <Envelopes>
+ <Envelope Name="Env0"/>
+ <Envelope Name="Env1"/>
+ <Envelope Name="Env2"/>
+ <Envelope Name="Env3"/>
+ <Envelope Name="Env4"/>
+ <Envelope Name="Env5"/>
+ <Envelope Name="Env6"/>
+ </Envelopes>
+ <Instruments>
+ <Instrument ProgramNumber="0" Name="INST_0"/>
+ <Instrument ProgramNumber="1" Name="INST_1"/>
+ <Instrument ProgramNumber="2" Name="INST_2"/>
+ <Instrument ProgramNumber="3" Name="INST_3"/>
+ <Instrument ProgramNumber="4" Name="INST_4"/>
+ <Instrument ProgramNumber="11" Name="INST_11"/>
+ <Instrument ProgramNumber="12" Name="INST_12"/>
+ <Instrument ProgramNumber="13" Name="INST_13"/>
+ <Instrument ProgramNumber="14" Name="INST_14"/>
+ <Instrument ProgramNumber="15" Name="INST_15"/>
+ </Instruments>
+</SoundFont>
diff --git a/assets/xml/audio/soundfonts/Soundfont_14.xml b/assets/xml/audio/soundfonts/Soundfont_14.xml
new file mode 100644
index 000000000..2345061c9
--- /dev/null
+++ b/assets/xml/audio/soundfonts/Soundfont_14.xml
@@ -0,0 +1,15 @@
+<!-- This file is only for extraction of vanilla data. For other purposes see assets/audio/soundfonts/ -->
+<SoundFont Name="Soundfont_14" Index="14">
+ <Envelopes>
+ <Envelope Name="Env0"/>
+ <Envelope Name="Env1"/>
+ <Envelope Name="Env2"/>
+ </Envelopes>
+ <Instruments>
+ <Instrument ProgramNumber="0" Name="INST_0"/>
+ <Instrument ProgramNumber="1" Name="INST_1"/>
+ <Instrument ProgramNumber="2" Name="INST_2"/>
+ <Instrument ProgramNumber="3" Name="INST_3"/>
+ <Instrument ProgramNumber="4" Name="INST_4"/>
+ </Instruments>
+</SoundFont>
diff --git a/assets/xml/audio/soundfonts/Soundfont_15.xml b/assets/xml/audio/soundfonts/Soundfont_15.xml
new file mode 100644
index 000000000..bf32ecd43
--- /dev/null
+++ b/assets/xml/audio/soundfonts/Soundfont_15.xml
@@ -0,0 +1,26 @@
+<!-- This file is only for extraction of vanilla data. For other purposes see assets/audio/soundfonts/ -->
+<SoundFont Name="Soundfont_15" Index="15">
+ <Envelopes>
+ <Envelope Name="Env0"/>
+ <Envelope Name="Env1"/>
+ <Envelope Name="Env2"/>
+ <Envelope Name="Env3"/>
+ <Envelope Name="Env4"/>
+ <Envelope Name="Env5"/>
+ <Envelope Name="Env6"/>
+ </Envelopes>
+ <Instruments>
+ <Instrument ProgramNumber="0" Name="INST_0"/>
+ <Instrument ProgramNumber="1" Name="INST_1"/>
+ <Instrument ProgramNumber="2" Name="INST_2"/>
+ <Instrument ProgramNumber="3" Name="INST_3"/>
+ <Instrument ProgramNumber="4" Name="INST_4"/>
+ <Instrument ProgramNumber="8" Name="INST_8"/>
+ <Instrument ProgramNumber="10" Name="INST_10"/>
+ <Instrument ProgramNumber="11" Name="INST_11"/>
+ <Instrument ProgramNumber="12" Name="INST_12"/>
+ <Instrument ProgramNumber="13" Name="INST_13"/>
+ <Instrument ProgramNumber="14" Name="INST_14"/>
+ <Instrument ProgramNumber="15" Name="INST_15"/>
+ </Instruments>
+</SoundFont>
diff --git a/assets/xml/audio/soundfonts/Soundfont_16.xml b/assets/xml/audio/soundfonts/Soundfont_16.xml
new file mode 100644
index 000000000..c1c6d1d2b
--- /dev/null
+++ b/assets/xml/audio/soundfonts/Soundfont_16.xml
@@ -0,0 +1,23 @@
+<!-- This file is only for extraction of vanilla data. For other purposes see assets/audio/soundfonts/ -->
+<SoundFont Name="Soundfont_16" Index="16">
+ <Envelopes>
+ <Envelope Name="Env0"/>
+ <Envelope Name="Env1"/>
+ <Envelope Name="Env2"/>
+ <Envelope Name="Env3"/>
+ <Envelope Name="Env4"/>
+ <Envelope Name="Env5"/>
+ </Envelopes>
+ <Instruments>
+ <Instrument ProgramNumber="1" Name="INST_1"/>
+ <Instrument ProgramNumber="2" Name="INST_2"/>
+ <Instrument ProgramNumber="3" Name="INST_3"/>
+ <Instrument ProgramNumber="4" Name="INST_4"/>
+ <Instrument ProgramNumber="5" Name="INST_5"/>
+ <Instrument ProgramNumber="0" Name="INST_0"/>
+ </Instruments>
+ <Drums>
+ <Drum Name="DRUM_0"/>
+ <Drum Name="DRUM_1"/>
+ </Drums>
+</SoundFont>
diff --git a/assets/xml/audio/soundfonts/Soundfont_17.xml b/assets/xml/audio/soundfonts/Soundfont_17.xml
new file mode 100644
index 000000000..a16e9752f
--- /dev/null
+++ b/assets/xml/audio/soundfonts/Soundfont_17.xml
@@ -0,0 +1,18 @@
+<!-- This file is only for extraction of vanilla data. For other purposes see assets/audio/soundfonts/ -->
+<SoundFont Name="Soundfont_17" Index="17">
+ <Envelopes>
+ <Envelope Name="Env0"/>
+ <Envelope Name="Env1"/>
+ <Envelope Name="Env2"/>
+ <Envelope Name="Env3"/>
+ </Envelopes>
+ <Instruments>
+ <Instrument ProgramNumber="0" Name="INST_0"/>
+ <Instrument ProgramNumber="1" Name="INST_1"/>
+ <Instrument ProgramNumber="5" Name="INST_5"/>
+ <Instrument ProgramNumber="6" Name="INST_6"/>
+ <Instrument ProgramNumber="7" Name="INST_7"/>
+ <Instrument ProgramNumber="13" Name="INST_13"/>
+ <Instrument ProgramNumber="14" Name="INST_14"/>
+ </Instruments>
+</SoundFont>
diff --git a/assets/xml/audio/soundfonts/Soundfont_18.xml b/assets/xml/audio/soundfonts/Soundfont_18.xml
new file mode 100644
index 000000000..76807539a
--- /dev/null
+++ b/assets/xml/audio/soundfonts/Soundfont_18.xml
@@ -0,0 +1,29 @@
+<!-- This file is only for extraction of vanilla data. For other purposes see assets/audio/soundfonts/ -->
+<SoundFont Name="Soundfont_18" Index="18">
+ <Envelopes>
+ <Envelope Name="Env0"/>
+ <Envelope Name="Env1"/>
+ <Envelope Name="Env2"/>
+ <Envelope Name="Env3"/>
+ <Envelope Name="Env4"/>
+ <Envelope Name="Env5"/>
+ <Envelope Name="Env6"/>
+ </Envelopes>
+ <Instruments>
+ <Instrument ProgramNumber="0" Name="INST_0"/>
+ <Instrument ProgramNumber="1" Name="INST_1"/>
+ <Instrument ProgramNumber="4" Name="INST_4"/>
+ <Instrument ProgramNumber="5" Name="INST_5"/>
+ <Instrument ProgramNumber="8" Name="INST_8"/>
+ <Instrument ProgramNumber="10" Name="INST_10"/>
+ <Instrument ProgramNumber="11" Name="INST_11"/>
+ <Instrument ProgramNumber="12" Name="INST_12"/>
+ </Instruments>
+ <Drums>
+ <Drum Name="DRUM_0"/>
+ <Drum Name="DRUM_1"/>
+ <Drum Name="DRUM_2"/>
+ <Drum Name="DRUM_3"/>
+ <Drum Name="DRUM_4"/>
+ </Drums>
+</SoundFont>
diff --git a/assets/xml/audio/soundfonts/Soundfont_19.xml b/assets/xml/audio/soundfonts/Soundfont_19.xml
new file mode 100644
index 000000000..9f2ba9286
--- /dev/null
+++ b/assets/xml/audio/soundfonts/Soundfont_19.xml
@@ -0,0 +1,14 @@
+<!-- This file is only for extraction of vanilla data. For other purposes see assets/audio/soundfonts/ -->
+<SoundFont Name="Soundfont_19" Index="19">
+ <Envelopes>
+ <Envelope Name="Env0"/>
+ <Envelope Name="Env1"/>
+ <Envelope Name="Env2"/>
+ </Envelopes>
+ <Instruments>
+ <Instrument ProgramNumber="0" Name="INST_0"/>
+ <Instrument ProgramNumber="1" Name="INST_1"/>
+ <Instrument ProgramNumber="2" Name="INST_2"/>
+ <Instrument ProgramNumber="3" Name="INST_3"/>
+ </Instruments>
+</SoundFont>
diff --git a/assets/xml/audio/soundfonts/Soundfont_2.xml b/assets/xml/audio/soundfonts/Soundfont_2.xml
new file mode 100644
index 000000000..a297ce850
--- /dev/null
+++ b/assets/xml/audio/soundfonts/Soundfont_2.xml
@@ -0,0 +1,28 @@
+<!-- This file is only for extraction of vanilla data. For other purposes see assets/audio/soundfonts/ -->
+<SoundFont Name="Soundfont_2" Index="2">
+ <Envelopes>
+ <Envelope Name="Env0"/>
+ </Envelopes>
+ <Instruments>
+ <Instrument ProgramNumber="1" Name="INST_1"/>
+ <Instrument ProgramNumber="2" Name="INST_2"/>
+ <Instrument ProgramNumber="3" Name="INST_3"/>
+ <Instrument ProgramNumber="4" Name="INST_4"/>
+ <Instrument ProgramNumber="5" Name="INST_5"/>
+ <Instrument ProgramNumber="6" Name="INST_6"/>
+ <Instrument ProgramNumber="7" Name="INST_7"/>
+ <Instrument ProgramNumber="8" Name="INST_8"/>
+ <Instrument ProgramNumber="9" Name="INST_9"/>
+ <Instrument ProgramNumber="10" Name="INST_10"/>
+ <Instrument ProgramNumber="11" Name="INST_11"/>
+ <Instrument ProgramNumber="12" Name="INST_12"/>
+ <Instrument ProgramNumber="13" Name="INST_13"/>
+ <Instrument ProgramNumber="14" Name="INST_14"/>
+ <Instrument ProgramNumber="15" Name="INST_15"/>
+ <Instrument ProgramNumber="16" Name="INST_16"/>
+ <Instrument ProgramNumber="17" Name="INST_17"/>
+ <Instrument ProgramNumber="18" Name="INST_18"/>
+ <Instrument ProgramNumber="19" Name="INST_19"/>
+ <Instrument ProgramNumber="20" Name="INST_20"/>
+ </Instruments>
+</SoundFont>
diff --git a/assets/xml/audio/soundfonts/Soundfont_20.xml b/assets/xml/audio/soundfonts/Soundfont_20.xml
new file mode 100644
index 000000000..5ce632d47
--- /dev/null
+++ b/assets/xml/audio/soundfonts/Soundfont_20.xml
@@ -0,0 +1,23 @@
+<!-- This file is only for extraction of vanilla data. For other purposes see assets/audio/soundfonts/ -->
+<SoundFont Name="Soundfont_20" Index="20">
+ <Envelopes>
+ <Envelope Name="Env0"/>
+ <Envelope Name="Env1"/>
+ <Envelope Name="Env2"/>
+ <Envelope Name="Env3"/>
+ </Envelopes>
+ <Instruments>
+ <Instrument ProgramNumber="0" Name="INST_0"/>
+ <Instrument ProgramNumber="1" Name="INST_1"/>
+ <Instrument ProgramNumber="2" Name="INST_2"/>
+ <Instrument ProgramNumber="3" Name="INST_3"/>
+ <Instrument ProgramNumber="4" Name="INST_4"/>
+ </Instruments>
+ <Drums>
+ <Drum Name="DRUM_0"/>
+ <Drum Name="DRUM_1"/>
+ <Drum Name="DRUM_2"/>
+ <Drum Name="DRUM_3"/>
+ <Drum Name="DRUM_4"/>
+ </Drums>
+</SoundFont>
diff --git a/assets/xml/audio/soundfonts/Soundfont_21.xml b/assets/xml/audio/soundfonts/Soundfont_21.xml
new file mode 100644
index 000000000..a1bc07be8
--- /dev/null
+++ b/assets/xml/audio/soundfonts/Soundfont_21.xml
@@ -0,0 +1,20 @@
+<!-- This file is only for extraction of vanilla data. For other purposes see assets/audio/soundfonts/ -->
+<SoundFont Name="Soundfont_21" Index="21">
+ <Envelopes>
+ <Envelope Name="Env0"/>
+ <Envelope Name="Env1"/>
+ <Envelope Name="Env2"/>
+ <Envelope Name="Env3"/>
+ </Envelopes>
+ <Instruments>
+ <Instrument ProgramNumber="0" Name="INST_0"/>
+ <Instrument ProgramNumber="1" Name="INST_1"/>
+ <Instrument ProgramNumber="2" Name="INST_2"/>
+ </Instruments>
+ <Drums>
+ <Drum Name="DRUM_0"/>
+ <Drum Name="DRUM_1"/>
+ <Drum Name="DRUM_2"/>
+ <Drum Name="DRUM_3"/>
+ </Drums>
+</SoundFont>
diff --git a/assets/xml/audio/soundfonts/Soundfont_22.xml b/assets/xml/audio/soundfonts/Soundfont_22.xml
new file mode 100644
index 000000000..fe0099beb
--- /dev/null
+++ b/assets/xml/audio/soundfonts/Soundfont_22.xml
@@ -0,0 +1,25 @@
+<!-- This file is only for extraction of vanilla data. For other purposes see assets/audio/soundfonts/ -->
+<SoundFont Name="Soundfont_22" Index="22">
+ <Envelopes>
+ <Envelope Name="Env0"/>
+ <Envelope Name="Env1"/>
+ <Envelope Name="Env2"/>
+ <Envelope Name="Env3"/>
+ <Envelope Name="Env4"/>
+ <Envelope Name="Env5"/>
+ </Envelopes>
+ <Instruments>
+ <Instrument ProgramNumber="0" Name="INST_0"/>
+ <Instrument ProgramNumber="1" Name="INST_1"/>
+ <Instrument ProgramNumber="2" Name="INST_2"/>
+ <Instrument ProgramNumber="3" Name="INST_3"/>
+ <Instrument ProgramNumber="4" Name="INST_4"/>
+ <Instrument ProgramNumber="10" Name="INST_10"/>
+ </Instruments>
+ <Drums>
+ <Drum Name="DRUM_0"/>
+ <Drum Name="DRUM_1"/>
+ <Drum Name="DRUM_2"/>
+ <Drum Name="DRUM_3"/>
+ </Drums>
+</SoundFont>
diff --git a/assets/xml/audio/soundfonts/Soundfont_23.xml b/assets/xml/audio/soundfonts/Soundfont_23.xml
new file mode 100644
index 000000000..582c638a2
--- /dev/null
+++ b/assets/xml/audio/soundfonts/Soundfont_23.xml
@@ -0,0 +1,14 @@
+<!-- This file is only for extraction of vanilla data. For other purposes see assets/audio/soundfonts/ -->
+<SoundFont Name="Soundfont_23" Index="23">
+ <Envelopes>
+ <Envelope Name="Env0"/>
+ <Envelope Name="Env1"/>
+ <Envelope Name="Env2"/>
+ </Envelopes>
+ <Instruments>
+ <Instrument ProgramNumber="0" Name="INST_0"/>
+ <Instrument ProgramNumber="1" Name="INST_1"/>
+ <Instrument ProgramNumber="2" Name="INST_2"/>
+ <Instrument ProgramNumber="3" Name="INST_3"/>
+ </Instruments>
+</SoundFont>
diff --git a/assets/xml/audio/soundfonts/Soundfont_24.xml b/assets/xml/audio/soundfonts/Soundfont_24.xml
new file mode 100644
index 000000000..aee3907f9
--- /dev/null
+++ b/assets/xml/audio/soundfonts/Soundfont_24.xml
@@ -0,0 +1,23 @@
+<!-- This file is only for extraction of vanilla data. For other purposes see assets/audio/soundfonts/ -->
+<SoundFont Name="Soundfont_24" Index="24">
+ <Envelopes>
+ <Envelope Name="Env0"/>
+ <Envelope Name="Env1"/>
+ <Envelope Name="Env2"/>
+ <Envelope Name="Env3"/>
+ <Envelope Name="Env4"/>
+ <Envelope Name="Env5"/>
+ <Envelope Name="Env6"/>
+ <Envelope Name="Env7"/>
+ </Envelopes>
+ <Instruments>
+ <Instrument ProgramNumber="0" Name="INST_0"/>
+ <Instrument ProgramNumber="1" Name="INST_1"/>
+ <Instrument ProgramNumber="2" Name="INST_2"/>
+ <Instrument ProgramNumber="3" Name="INST_3"/>
+ <Instrument ProgramNumber="4" Name="INST_4"/>
+ <Instrument ProgramNumber="6" Name="INST_6"/>
+ <Instrument ProgramNumber="7" Name="INST_7"/>
+ <Instrument ProgramNumber="8" Name="INST_8"/>
+ </Instruments>
+</SoundFont>
diff --git a/assets/xml/audio/soundfonts/Soundfont_25.xml b/assets/xml/audio/soundfonts/Soundfont_25.xml
new file mode 100644
index 000000000..5ad7d22f2
--- /dev/null
+++ b/assets/xml/audio/soundfonts/Soundfont_25.xml
@@ -0,0 +1,19 @@
+<!-- This file is only for extraction of vanilla data. For other purposes see assets/audio/soundfonts/ -->
+<SoundFont Name="Soundfont_25" Index="25">
+ <Envelopes>
+ <Envelope Name="Env0"/>
+ <Envelope Name="Env1"/>
+ <Envelope Name="Env2"/>
+ <Envelope Name="Env3"/>
+ <Envelope Name="Env4"/>
+ <Envelope Name="Env5"/>
+ </Envelopes>
+ <Instruments>
+ <Instrument ProgramNumber="0" Name="INST_0"/>
+ <Instrument ProgramNumber="4" Name="INST_4"/>
+ <Instrument ProgramNumber="5" Name="INST_5"/>
+ <Instrument ProgramNumber="6" Name="INST_6"/>
+ <Instrument ProgramNumber="7" Name="INST_7"/>
+ <Instrument ProgramNumber="1" Name="INST_1"/>
+ </Instruments>
+</SoundFont>
diff --git a/assets/xml/audio/soundfonts/Soundfont_26.xml b/assets/xml/audio/soundfonts/Soundfont_26.xml
new file mode 100644
index 000000000..ec20f4f56
--- /dev/null
+++ b/assets/xml/audio/soundfonts/Soundfont_26.xml
@@ -0,0 +1,10 @@
+<!-- This file is only for extraction of vanilla data. For other purposes see assets/audio/soundfonts/ -->
+<SoundFont Name="Soundfont_26" Index="26">
+ <Envelopes>
+ <Envelope Name="Env0"/>
+ </Envelopes>
+ <Instruments>
+ <Instrument ProgramNumber="14" Name="INST_14"/>
+ <Instrument ProgramNumber="13" Name="INST_13"/>
+ </Instruments>
+</SoundFont>
diff --git a/assets/xml/audio/soundfonts/Soundfont_27.xml b/assets/xml/audio/soundfonts/Soundfont_27.xml
new file mode 100644
index 000000000..1bc1c9d88
--- /dev/null
+++ b/assets/xml/audio/soundfonts/Soundfont_27.xml
@@ -0,0 +1,21 @@
+<!-- This file is only for extraction of vanilla data. For other purposes see assets/audio/soundfonts/ -->
+<SoundFont Name="Soundfont_27" Index="27">
+ <Envelopes>
+ <Envelope Name="Env0"/>
+ <Envelope Name="Env1"/>
+ <Envelope Name="Env2"/>
+ <Envelope Name="Env3"/>
+ <Envelope Name="Env4"/>
+ <Envelope Name="Env5"/>
+ </Envelopes>
+ <Instruments>
+ <Instrument ProgramNumber="0" Name="INST_0"/>
+ <Instrument ProgramNumber="1" Name="INST_1"/>
+ <Instrument ProgramNumber="2" Name="INST_2"/>
+ <Instrument ProgramNumber="3" Name="INST_3"/>
+ <Instrument ProgramNumber="5" Name="INST_5"/>
+ <Instrument ProgramNumber="7" Name="INST_7"/>
+ <Instrument ProgramNumber="11" Name="INST_11"/>
+ <Instrument ProgramNumber="10" Name="INST_10"/>
+ </Instruments>
+</SoundFont>
diff --git a/assets/xml/audio/soundfonts/Soundfont_28.xml b/assets/xml/audio/soundfonts/Soundfont_28.xml
new file mode 100644
index 000000000..c0292b0d7
--- /dev/null
+++ b/assets/xml/audio/soundfonts/Soundfont_28.xml
@@ -0,0 +1,15 @@
+<!-- This file is only for extraction of vanilla data. For other purposes see assets/audio/soundfonts/ -->
+<SoundFont Name="Soundfont_28" Index="28">
+ <Envelopes>
+ <Envelope Name="Env0"/>
+ <Envelope Name="Env1"/>
+ <Envelope Name="Env2"/>
+ <Envelope Name="Env3"/>
+ </Envelopes>
+ <Instruments>
+ <Instrument ProgramNumber="0" Name="INST_0"/>
+ <Instrument ProgramNumber="1" Name="INST_1"/>
+ <Instrument ProgramNumber="4" Name="INST_4"/>
+ <Instrument ProgramNumber="3" Name="INST_3"/>
+ </Instruments>
+</SoundFont>
diff --git a/assets/xml/audio/soundfonts/Soundfont_29.xml b/assets/xml/audio/soundfonts/Soundfont_29.xml
new file mode 100644
index 000000000..39e03a9a2
--- /dev/null
+++ b/assets/xml/audio/soundfonts/Soundfont_29.xml
@@ -0,0 +1,22 @@
+<!-- This file is only for extraction of vanilla data. For other purposes see assets/audio/soundfonts/ -->
+<SoundFont Name="Soundfont_29" Index="29">
+ <Envelopes>
+ <Envelope Name="Env0"/>
+ <Envelope Name="Env1"/>
+ <Envelope Name="Env2"/>
+ <Envelope Name="Env3"/>
+ <Envelope Name="Env4"/>
+ </Envelopes>
+ <Instruments>
+ <Instrument ProgramNumber="0" Name="INST_0"/>
+ <Instrument ProgramNumber="1" Name="INST_1"/>
+ <Instrument ProgramNumber="5" Name="INST_5"/>
+ <Instrument ProgramNumber="7" Name="INST_7"/>
+ <Instrument ProgramNumber="10" Name="INST_10"/>
+ <Instrument ProgramNumber="11" Name="INST_11"/>
+ </Instruments>
+ <Drums>
+ <Drum Name="DRUM_0"/>
+ <Drum Name="DRUM_1"/>
+ </Drums>
+</SoundFont>
diff --git a/assets/xml/audio/soundfonts/Soundfont_3.xml b/assets/xml/audio/soundfonts/Soundfont_3.xml
new file mode 100644
index 000000000..bb510aaef
--- /dev/null
+++ b/assets/xml/audio/soundfonts/Soundfont_3.xml
@@ -0,0 +1,37 @@
+<!-- This file is only for extraction of vanilla data. For other purposes see assets/audio/soundfonts/ -->
+<SoundFont Name="Soundfont_3" Index="3">
+ <Envelopes>
+ <Envelope Name="Env0"/>
+ <Envelope Name="Env1"/>
+ <Envelope Name="Env2"/>
+ <Envelope Name="Env3"/>
+ <Envelope Name="Env4"/>
+ <Envelope Name="Env5"/>
+ <Envelope Name="Env6"/>
+ <Envelope Name="Env7"/>
+ </Envelopes>
+ <Instruments>
+ <Instrument ProgramNumber="0" Name="INST_0"/>
+ <Instrument ProgramNumber="1" Name="INST_1"/>
+ <Instrument ProgramNumber="2" Name="INST_2"/>
+ <Instrument ProgramNumber="3" Name="INST_3"/>
+ <Instrument ProgramNumber="4" Name="INST_4"/>
+ <Instrument ProgramNumber="5" Name="INST_5"/>
+ <Instrument ProgramNumber="6" Name="INST_6"/>
+ <Instrument ProgramNumber="7" Name="INST_7"/>
+ <Instrument ProgramNumber="8" Name="INST_8"/>
+ <Instrument ProgramNumber="10" Name="INST_10"/>
+ <Instrument ProgramNumber="11" Name="INST_11"/>
+ <Instrument ProgramNumber="12" Name="INST_12"/>
+ <Instrument ProgramNumber="13" Name="INST_13"/>
+ <Instrument ProgramNumber="14" Name="INST_14"/>
+ <Instrument ProgramNumber="15" Name="INST_15"/>
+ </Instruments>
+ <Drums>
+ <Drum Name="DRUM_0"/>
+ <Drum Name="DRUM_1"/>
+ <Drum Name="DRUM_2"/>
+ <Drum Name="DRUM_3"/>
+ <Drum Name="DRUM_4"/>
+ </Drums>
+</SoundFont>
diff --git a/assets/xml/audio/soundfonts/Soundfont_30.xml b/assets/xml/audio/soundfonts/Soundfont_30.xml
new file mode 100644
index 000000000..e9a1d093e
--- /dev/null
+++ b/assets/xml/audio/soundfonts/Soundfont_30.xml
@@ -0,0 +1,13 @@
+<!-- This file is only for extraction of vanilla data. For other purposes see assets/audio/soundfonts/ -->
+<SoundFont Name="Soundfont_30" Index="30">
+ <Envelopes>
+ <Envelope Name="Env0"/>
+ <Envelope Name="Env1"/>
+ </Envelopes>
+ <Instruments>
+ <Instrument ProgramNumber="0" Name="INST_0"/>
+ <Instrument ProgramNumber="1" Name="INST_1"/>
+ <Instrument ProgramNumber="2" Name="INST_2"/>
+ <Instrument ProgramNumber="4" Name="INST_4"/>
+ </Instruments>
+</SoundFont>
diff --git a/assets/xml/audio/soundfonts/Soundfont_31.xml b/assets/xml/audio/soundfonts/Soundfont_31.xml
new file mode 100644
index 000000000..d246fff00
--- /dev/null
+++ b/assets/xml/audio/soundfonts/Soundfont_31.xml
@@ -0,0 +1,19 @@
+<!-- This file is only for extraction of vanilla data. For other purposes see assets/audio/soundfonts/ -->
+<SoundFont Name="Soundfont_31" Index="31">
+ <Envelopes>
+ <Envelope Name="Env0"/>
+ <Envelope Name="Env1"/>
+ <Envelope Name="Env2"/>
+ <Envelope Name="Env3"/>
+ <Envelope Name="Env4"/>
+ <Envelope Name="Env5"/>
+ </Envelopes>
+ <Instruments>
+ <Instrument ProgramNumber="1" Name="INST_1"/>
+ <Instrument ProgramNumber="2" Name="INST_2"/>
+ <Instrument ProgramNumber="3" Name="INST_3"/>
+ <Instrument ProgramNumber="7" Name="INST_7"/>
+ <Instrument ProgramNumber="4" Name="INST_4"/>
+ <Instrument ProgramNumber="5" Name="INST_5"/>
+ </Instruments>
+</SoundFont>
diff --git a/assets/xml/audio/soundfonts/Soundfont_32.xml b/assets/xml/audio/soundfonts/Soundfont_32.xml
new file mode 100644
index 000000000..7508ee8b5
--- /dev/null
+++ b/assets/xml/audio/soundfonts/Soundfont_32.xml
@@ -0,0 +1,32 @@
+<!-- This file is only for extraction of vanilla data. For other purposes see assets/audio/soundfonts/ -->
+<SoundFont Name="Soundfont_32" Index="32">
+ <Envelopes>
+ <Envelope Name="Env0"/>
+ <Envelope Name="Env1"/>
+ <Envelope Name="Env2"/>
+ <Envelope Name="Env3"/>
+ <Envelope Name="Env4"/>
+ <Envelope Name="Env5"/>
+ <Envelope Name="Env6"/>
+ </Envelopes>
+ <Instruments>
+ <Instrument ProgramNumber="0" Name="INST_0"/>
+ <Instrument ProgramNumber="1" Name="INST_1"/>
+ <Instrument ProgramNumber="2" Name="INST_2"/>
+ <Instrument ProgramNumber="4" Name="INST_4"/>
+ <Instrument ProgramNumber="5" Name="INST_5"/>
+ <Instrument ProgramNumber="6" Name="INST_6"/>
+ <Instrument ProgramNumber="7" Name="INST_7"/>
+ <Instrument ProgramNumber="10" Name="INST_10"/>
+ <Instrument ProgramNumber="11" Name="INST_11"/>
+ <Instrument ProgramNumber="13" Name="INST_13"/>
+ <Instrument ProgramNumber="15" Name="INST_15"/>
+ </Instruments>
+ <Drums>
+ <Drum Name="DRUM_0"/>
+ <Drum Name="DRUM_1"/>
+ <Drum Name="DRUM_2"/>
+ <Drum Name="DRUM_3"/>
+ <Drum Name="DRUM_4"/>
+ </Drums>
+</SoundFont>
diff --git a/assets/xml/audio/soundfonts/Soundfont_33.xml b/assets/xml/audio/soundfonts/Soundfont_33.xml
new file mode 100644
index 000000000..93116ad1c
--- /dev/null
+++ b/assets/xml/audio/soundfonts/Soundfont_33.xml
@@ -0,0 +1,37 @@
+<!-- This file is only for extraction of vanilla data. For other purposes see assets/audio/soundfonts/ -->
+<SoundFont Name="Soundfont_33" Index="33">
+ <Envelopes>
+ <Envelope Name="Env0"/>
+ <Envelope Name="Env1"/>
+ <Envelope Name="Env2"/>
+ <Envelope Name="Env3"/>
+ <Envelope Name="Env4"/>
+ <Envelope Name="Env5"/>
+ <Envelope Name="Env6"/>
+ <Envelope Name="Env7"/>
+ <Envelope Name="Env8"/>
+ <Envelope Name="Env9"/>
+ </Envelopes>
+ <Instruments>
+ <Instrument ProgramNumber="0" Name="INST_0"/>
+ <Instrument ProgramNumber="1" Name="INST_1"/>
+ <Instrument ProgramNumber="2" Name="INST_2"/>
+ <Instrument ProgramNumber="4" Name="INST_4"/>
+ <Instrument ProgramNumber="5" Name="INST_5"/>
+ <Instrument ProgramNumber="6" Name="INST_6"/>
+ <Instrument ProgramNumber="7" Name="INST_7"/>
+ <Instrument ProgramNumber="8" Name="INST_8"/>
+ <Instrument ProgramNumber="10" Name="INST_10"/>
+ <Instrument ProgramNumber="11" Name="INST_11"/>
+ <Instrument ProgramNumber="14" Name="INST_14"/>
+ <Instrument ProgramNumber="15" Name="INST_15"/>
+ <Instrument ProgramNumber="13" Name="INST_13"/>
+ </Instruments>
+ <Drums>
+ <Drum Name="DRUM_0"/>
+ <Drum Name="DRUM_1"/>
+ <Drum Name="DRUM_2"/>
+ <Drum Name="DRUM_3"/>
+ <Drum Name="DRUM_4"/>
+ </Drums>
+</SoundFont>
diff --git a/assets/xml/audio/soundfonts/Soundfont_34.xml b/assets/xml/audio/soundfonts/Soundfont_34.xml
new file mode 100644
index 000000000..20437b048
--- /dev/null
+++ b/assets/xml/audio/soundfonts/Soundfont_34.xml
@@ -0,0 +1,42 @@
+<!-- This file is only for extraction of vanilla data. For other purposes see assets/audio/soundfonts/ -->
+<SoundFont Name="Soundfont_34" Index="34">
+ <Envelopes>
+ <Envelope Name="Env0"/>
+ <Envelope Name="Env1"/>
+ <Envelope Name="Env2"/>
+ <Envelope Name="Env3"/>
+ <Envelope Name="Env4"/>
+ <Envelope Name="Env5"/>
+ <Envelope Name="Env6"/>
+ <Envelope Name="Env7"/>
+ <Envelope Name="Env8"/>
+ <Envelope Name="Env9"/>
+ <Envelope Name="Env10"/>
+ <Envelope Name="Env11"/>
+ <Envelope Name="Env12"/>
+ </Envelopes>
+ <Instruments>
+ <Instrument ProgramNumber="0" Name="INST_0"/>
+ <Instrument ProgramNumber="1" Name="INST_1"/>
+ <Instrument ProgramNumber="2" Name="INST_2"/>
+ <Instrument ProgramNumber="3" Name="INST_3"/>
+ <Instrument ProgramNumber="4" Name="INST_4"/>
+ <Instrument ProgramNumber="5" Name="INST_5"/>
+ <Instrument ProgramNumber="6" Name="INST_6"/>
+ <Instrument ProgramNumber="7" Name="INST_7"/>
+ <Instrument ProgramNumber="8" Name="INST_8"/>
+ <Instrument ProgramNumber="10" Name="INST_10"/>
+ <Instrument ProgramNumber="11" Name="INST_11"/>
+ <Instrument ProgramNumber="12" Name="INST_12"/>
+ <Instrument ProgramNumber="13" Name="INST_13"/>
+ <Instrument ProgramNumber="14" Name="INST_14"/>
+ <Instrument ProgramNumber="15" Name="INST_15"/>
+ </Instruments>
+ <Drums>
+ <Drum Name="DRUM_0"/>
+ <Drum Name="DRUM_1"/>
+ <Drum Name="DRUM_2"/>
+ <Drum Name="DRUM_3"/>
+ <Drum Name="DRUM_4"/>
+ </Drums>
+</SoundFont>
diff --git a/assets/xml/audio/soundfonts/Soundfont_35.xml b/assets/xml/audio/soundfonts/Soundfont_35.xml
new file mode 100644
index 000000000..fa9209a72
--- /dev/null
+++ b/assets/xml/audio/soundfonts/Soundfont_35.xml
@@ -0,0 +1,26 @@
+<!-- This file is only for extraction of vanilla data. For other purposes see assets/audio/soundfonts/ -->
+<SoundFont Name="Soundfont_35" Index="35">
+ <Envelopes>
+ <Envelope Name="Env0"/>
+ <Envelope Name="Env1"/>
+ <Envelope Name="Env2"/>
+ <Envelope Name="Env3"/>
+ <Envelope Name="Env4"/>
+ </Envelopes>
+ <Instruments>
+ <Instrument ProgramNumber="5" Name="INST_5"/>
+ <Instrument ProgramNumber="6" Name="INST_6"/>
+ <Instrument ProgramNumber="7" Name="INST_7"/>
+ <Instrument ProgramNumber="8" Name="INST_8"/>
+ <Instrument ProgramNumber="10" Name="INST_10"/>
+ <Instrument ProgramNumber="11" Name="INST_11"/>
+ <Instrument ProgramNumber="14" Name="INST_14"/>
+ </Instruments>
+ <Drums>
+ <Drum Name="DRUM_0"/>
+ <Drum Name="DRUM_1"/>
+ <Drum Name="DRUM_2"/>
+ <Drum Name="DRUM_3"/>
+ <Drum Name="DRUM_4"/>
+ </Drums>
+</SoundFont>
diff --git a/assets/xml/audio/soundfonts/Soundfont_36.xml b/assets/xml/audio/soundfonts/Soundfont_36.xml
new file mode 100644
index 000000000..fe49eda57
--- /dev/null
+++ b/assets/xml/audio/soundfonts/Soundfont_36.xml
@@ -0,0 +1,17 @@
+<!-- This file is only for extraction of vanilla data. For other purposes see assets/audio/soundfonts/ -->
+<SoundFont Name="Soundfont_36" Index="36">
+ <Envelopes>
+ <Envelope Name="Env0"/>
+ <Envelope Name="Env1"/>
+ <Envelope Name="Env2"/>
+ <Envelope Name="Env3"/>
+ </Envelopes>
+ <Instruments>
+ <Instrument ProgramNumber="1" Name="INST_1"/>
+ <Instrument ProgramNumber="3" Name="INST_3"/>
+ <Instrument ProgramNumber="10" Name="INST_10"/>
+ <Instrument ProgramNumber="11" Name="INST_11"/>
+ <Instrument ProgramNumber="12" Name="INST_12"/>
+ <Instrument ProgramNumber="14" Name="INST_14"/>
+ </Instruments>
+</SoundFont>
diff --git a/assets/xml/audio/soundfonts/Soundfont_37.xml b/assets/xml/audio/soundfonts/Soundfont_37.xml
new file mode 100644
index 000000000..67fa255b2
--- /dev/null
+++ b/assets/xml/audio/soundfonts/Soundfont_37.xml
@@ -0,0 +1,11 @@
+<!-- This file is only for extraction of vanilla data. For other purposes see assets/audio/soundfonts/ -->
+<SoundFont Name="Soundfont_37" Index="37">
+ <Envelopes>
+ <Envelope Name="Env0"/>
+ <Envelope Name="Env1"/>
+ </Envelopes>
+ <Instruments>
+ <Instrument ProgramNumber="0" Name="INST_0"/>
+ <Instrument ProgramNumber="1" Name="INST_1"/>
+ </Instruments>
+</SoundFont>
diff --git a/assets/xml/audio/soundfonts/Soundfont_4.xml b/assets/xml/audio/soundfonts/Soundfont_4.xml
new file mode 100644
index 000000000..4e023974e
--- /dev/null
+++ b/assets/xml/audio/soundfonts/Soundfont_4.xml
@@ -0,0 +1,11 @@
+<!-- This file is only for extraction of vanilla data. For other purposes see assets/audio/soundfonts/ -->
+<SoundFont Name="Soundfont_4" Index="4">
+ <Envelopes>
+ <Envelope Name="Env0"/>
+ <Envelope Name="Env1"/>
+ </Envelopes>
+ <Instruments>
+ <Instrument ProgramNumber="0" Name="INST_0"/>
+ <Instrument ProgramNumber="1" Name="INST_1"/>
+ </Instruments>
+</SoundFont>
diff --git a/assets/xml/audio/soundfonts/Soundfont_5.xml b/assets/xml/audio/soundfonts/Soundfont_5.xml
new file mode 100644
index 000000000..465b554ee
--- /dev/null
+++ b/assets/xml/audio/soundfonts/Soundfont_5.xml
@@ -0,0 +1,22 @@
+<!-- This file is only for extraction of vanilla data. For other purposes see assets/audio/soundfonts/ -->
+<SoundFont Name="Soundfont_5" Index="5">
+ <Envelopes>
+ <Envelope Name="Env0"/>
+ <Envelope Name="Env1"/>
+ <Envelope Name="Env2"/>
+ <Envelope Name="Env3"/>
+ <Envelope Name="Env4"/>
+ </Envelopes>
+ <Instruments>
+ <Instrument ProgramNumber="0" Name="INST_0"/>
+ <Instrument ProgramNumber="1" Name="INST_1"/>
+ <Instrument ProgramNumber="2" Name="INST_2"/>
+ <Instrument ProgramNumber="3" Name="INST_3"/>
+ <Instrument ProgramNumber="12" Name="INST_12"/>
+ </Instruments>
+ <Drums>
+ <Drum Name="DRUM_0"/>
+ <Drum Name="DRUM_1"/>
+ <Drum Name="DRUM_2"/>
+ </Drums>
+</SoundFont>
diff --git a/assets/xml/audio/soundfonts/Soundfont_6.xml b/assets/xml/audio/soundfonts/Soundfont_6.xml
new file mode 100644
index 000000000..f4c239da8
--- /dev/null
+++ b/assets/xml/audio/soundfonts/Soundfont_6.xml
@@ -0,0 +1,16 @@
+<!-- This file is only for extraction of vanilla data. For other purposes see assets/audio/soundfonts/ -->
+<SoundFont Name="Soundfont_6" Index="6">
+ <Envelopes>
+ <Envelope Name="Env0"/>
+ <Envelope Name="Env1"/>
+ <Envelope Name="Env2"/>
+ <Envelope Name="Env3"/>
+ </Envelopes>
+ <Instruments>
+ <Instrument ProgramNumber="1" Name="INST_1"/>
+ <Instrument ProgramNumber="10" Name="INST_10"/>
+ <Instrument ProgramNumber="11" Name="INST_11"/>
+ <Instrument ProgramNumber="13" Name="INST_13"/>
+ <Instrument ProgramNumber="14" Name="INST_14"/>
+ </Instruments>
+</SoundFont>
diff --git a/assets/xml/audio/soundfonts/Soundfont_7.xml b/assets/xml/audio/soundfonts/Soundfont_7.xml
new file mode 100644
index 000000000..aa61f7d25
--- /dev/null
+++ b/assets/xml/audio/soundfonts/Soundfont_7.xml
@@ -0,0 +1,17 @@
+<!-- This file is only for extraction of vanilla data. For other purposes see assets/audio/soundfonts/ -->
+<SoundFont Name="Soundfont_7" Index="7">
+ <Envelopes>
+ <Envelope Name="Env0"/>
+ <Envelope Name="Env1"/>
+ </Envelopes>
+ <Instruments>
+ <Instrument ProgramNumber="0" Name="INST_0"/>
+ <Instrument ProgramNumber="2" Name="INST_2"/>
+ <Instrument ProgramNumber="3" Name="INST_3"/>
+ <Instrument ProgramNumber="4" Name="INST_4"/>
+ </Instruments>
+ <Drums>
+ <Drum Name="DRUM_0"/>
+ <Drum Name="DRUM_1"/>
+ </Drums>
+</SoundFont>
diff --git a/assets/xml/audio/soundfonts/Soundfont_8.xml b/assets/xml/audio/soundfonts/Soundfont_8.xml
new file mode 100644
index 000000000..06cc14a1c
--- /dev/null
+++ b/assets/xml/audio/soundfonts/Soundfont_8.xml
@@ -0,0 +1,26 @@
+<!-- This file is only for extraction of vanilla data. For other purposes see assets/audio/soundfonts/ -->
+<SoundFont Name="Soundfont_8" Index="8">
+ <Envelopes>
+ <Envelope Name="Env0"/>
+ <Envelope Name="Env1"/>
+ <Envelope Name="Env2"/>
+ <Envelope Name="Env3"/>
+ <Envelope Name="Env4"/>
+ <Envelope Name="Env5"/>
+ <Envelope Name="Env6"/>
+ </Envelopes>
+ <Instruments>
+ <Instrument ProgramNumber="0" Name="INST_0"/>
+ <Instrument ProgramNumber="1" Name="INST_1"/>
+ <Instrument ProgramNumber="2" Name="INST_2"/>
+ <Instrument ProgramNumber="3" Name="INST_3"/>
+ <Instrument ProgramNumber="4" Name="INST_4"/>
+ <Instrument ProgramNumber="5" Name="INST_5"/>
+ <Instrument ProgramNumber="6" Name="INST_6"/>
+ </Instruments>
+ <Drums>
+ <Drum Name="DRUM_0"/>
+ <Drum Name="DRUM_1"/>
+ <Drum Name="DRUM_2"/>
+ </Drums>
+</SoundFont>
diff --git a/assets/xml/audio/soundfonts/Soundfont_9.xml b/assets/xml/audio/soundfonts/Soundfont_9.xml
new file mode 100644
index 000000000..97384ba3c
--- /dev/null
+++ b/assets/xml/audio/soundfonts/Soundfont_9.xml
@@ -0,0 +1,34 @@
+<!-- This file is only for extraction of vanilla data. For other purposes see assets/audio/soundfonts/ -->
+<SoundFont Name="Soundfont_9" Index="9">
+ <Envelopes>
+ <Envelope Name="Env0"/>
+ <Envelope Name="Env1"/>
+ <Envelope Name="Env2"/>
+ <Envelope Name="Env3"/>
+ <Envelope Name="Env4"/>
+ <Envelope Name="Env5"/>
+ <Envelope Name="Env6"/>
+ <Envelope Name="Env7"/>
+ </Envelopes>
+ <Instruments>
+ <Instrument ProgramNumber="0" Name="INST_0"/>
+ <Instrument ProgramNumber="1" Name="INST_1"/>
+ <Instrument ProgramNumber="2" Name="INST_2"/>
+ <Instrument ProgramNumber="3" Name="INST_3"/>
+ <Instrument ProgramNumber="4" Name="INST_4"/>
+ <Instrument ProgramNumber="5" Name="INST_5"/>
+ <Instrument ProgramNumber="6" Name="INST_6"/>
+ <Instrument ProgramNumber="7" Name="INST_7"/>
+ <Instrument ProgramNumber="8" Name="INST_8"/>
+ <Instrument ProgramNumber="10" Name="INST_10"/>
+ <Instrument ProgramNumber="11" Name="INST_11"/>
+ <Instrument ProgramNumber="12" Name="INST_12"/>
+ </Instruments>
+ <Drums>
+ <Drum Name="DRUM_0"/>
+ <Drum Name="DRUM_1"/>
+ <Drum Name="DRUM_2"/>
+ <Drum Name="DRUM_3"/>
+ <Drum Name="DRUM_4"/>
+ </Drums>
+</SoundFont>
diff --git a/baseroms/gc-eu-mq-dbg/config.yml b/baseroms/gc-eu-mq-dbg/config.yml
index c5e301afa..115ee5c05 100644
--- a/baseroms/gc-eu-mq-dbg/config.yml
+++ b/baseroms/gc-eu-mq-dbg/config.yml
@@ -72,6 +72,10 @@ variables:
sFraMessageEntryTable: 0x80151658
sStaffMessageEntryTable: 0x80153768
sShadowTex: 0x80A8E610
+ gSoundFontTable: 0x801550D0
+ gSequenceFontTable: 0x80155340
+ gSequenceTable: 0x80155500
+ gSampleBankTable: 0x80155BF0
assets:
- name: code/fbdemo_circle
xml_path: assets/xml/code/fbdemo_circle.xml
diff --git a/baseroms/gc-eu-mq/config.yml b/baseroms/gc-eu-mq/config.yml
index 34c5cbe9c..59b1802e4 100644
--- a/baseroms/gc-eu-mq/config.yml
+++ b/baseroms/gc-eu-mq/config.yml
@@ -64,6 +64,10 @@ variables:
sFraMessageEntryTable: 0x8010DB28
sStaffMessageEntryTable: 0x8010FC38
sShadowTex: 0x80A72FA0
+ gSoundFontTable: 0x80110470
+ gSequenceFontTable: 0x801106E0
+ gSequenceTable: 0x801108A0
+ gSampleBankTable: 0x80110F90
assets:
- name: code/fbdemo_circle
xml_path: assets/xml/code/fbdemo_circle.xml
diff --git a/baseroms/gc-eu/config.yml b/baseroms/gc-eu/config.yml
index 90712dc3f..7d54a0259 100644
--- a/baseroms/gc-eu/config.yml
+++ b/baseroms/gc-eu/config.yml
@@ -64,6 +64,10 @@ variables:
sFraMessageEntryTable: 0x8010DB48
sStaffMessageEntryTable: 0x8010FC58
sShadowTex: 0x80A73020
+ gSoundFontTable: 0x80110490
+ gSequenceFontTable: 0x80110700
+ gSequenceTable: 0x801108C0
+ gSampleBankTable: 0x80110FB0
assets:
- name: code/fbdemo_circle
xml_path: assets/xml/code/fbdemo_circle.xml
diff --git a/baseroms/gc-us/config.yml b/baseroms/gc-us/config.yml
index 54498f9a2..c66c4322e 100644
--- a/baseroms/gc-us/config.yml
+++ b/baseroms/gc-us/config.yml
@@ -63,6 +63,10 @@ variables:
sNesMessageEntryTable: 0x8010DFCC
sStaffMessageEntryTable: 0x801121EC
sShadowTex: 0x80A74130
+ gSoundFontTable: 0x80112C80
+ gSequenceFontTable: 0x80112EF0
+ gSequenceTable: 0x801130B0
+ gSampleBankTable: 0x801137A0
assets:
- name: code/fbdemo_circle
xml_path: assets/xml/code/fbdemo_circle.xml
diff --git a/tools/audio/extraction/audio_extract.py b/tools/audio/extraction/audio_extract.py
new file mode 100644
index 000000000..ac51d2e0e
--- /dev/null
+++ b/tools/audio/extraction/audio_extract.py
@@ -0,0 +1,269 @@
+# SPDX-FileCopyrightText: © 2024 ZeldaRET
+# SPDX-License-Identifier: CC0-1.0
+#
+# Extract audio files
+#
+
+import os
+from dataclasses import dataclass
+from enum import auto, Enum
+from typing import Dict, List, Tuple, Union
+from xml.etree import ElementTree
+from xml.etree.ElementTree import Element
+
+from .audio_tables import AudioCodeTable, AudioCodeTableEntry, AudioStorageMedium
+from .audiotable import AudioTableFile
+from .audiobank_file import AudiobankFile
+from .util import align, debugm, error, incbin
+
+class MMLVersion(Enum):
+ OOT = auto()
+ MM = auto()
+
+@dataclass
+class GameVersionInfo:
+ # Music Macro Language Version
+ mml_version : MMLVersion
+ # Soundfont table code offset
+ soundfont_table : int
+ # Sequence font table code offset
+ seq_font_table : int
+ # Sequence table code offset
+ seq_table : int
+ # Sample bank table code offset
+ sample_bank_table : int
+ # Sequence enum names
+ seq_enum_names : Tuple[str]
+ # List of indices corresponding to handwritten sequences
+ handwritten_sequences : Tuple[int]
+ # Some soundfonts report the wrong samplebank, map them to the correct samplebank for proper sample discovery
+ fake_banks : Dict[int, int]
+ # Contains audiotable indices that suffer from a buffer clearing bug
+ audiotable_buffer_bugs : Tuple[int]
+
+SAMPLECONV_PATH = f"{os.path.dirname(os.path.realpath(__file__))}/../sampleconv/sampleconv"
+
+BASEROM_DEBUG = False
+
+# ======================================================================================================================
+# Run
+# ======================================================================================================================
+
+def collect_sample_banks(audiotable_seg : memoryview, extracted_dir : str, version_info : GameVersionInfo,
+ table : AudioCodeTable, samplebank_xmls : Dict[int, Tuple[str, Element]]):
+ sample_banks : List[Union[AudioTableFile, int]] = []
+
+ for i,entry in enumerate(table):
+ entry : AudioCodeTableEntry
+
+ assert entry.short_data1 == 0 and entry.short_data2 == 0 and entry.short_data3 == 0, \
+ "Bad data for Sample Bank entry, all short data should be 0"
+ assert entry.medium == AudioStorageMedium.MEDIUM_CART , \
+ "Bad data for Sample Bank entry, medium should be CART"
+
+ if entry.size == 0:
+ # Pointer to other entry, in this case the rom address is a table index
+
+ entry_dst = table.entries[entry.rom_addr]
+ sample_banks[entry.rom_addr].register_ptr(i)
+ sample_banks.append(entry_dst.rom_addr)
+ else:
+ # Check whether this samplebank suffers from the buffer bug
+ # TODO it should be possible to detect this automatically by checking padding following sample discovery
+ bug = i in version_info.audiotable_buffer_bugs
+
+ bank = AudioTableFile(i, audiotable_seg, entry, table.rom_addr, buffer_bug=bug,
+ extraction_xml=samplebank_xmls.get(i, None))
+
+ if BASEROM_DEBUG:
+ bank.dump_bin(f"{extracted_dir}/baserom_audiotest/audiotable_files/{bank.file_name}.bin")
+
+ sample_banks.append(bank)
+
+ return sample_banks
+
+def bank_data_lookup(sample_banks : List[Union[AudioTableFile, int]], e : Union[AudioTableFile, int]) -> AudioTableFile:
+ if isinstance(e, int):
+ if e == 255:
+ return None
+ return bank_data_lookup(sample_banks, sample_banks[e])
+ else:
+ return e
+
+def collect_soundfonts(audiobank_seg : memoryview, extracted_dir : str, version_info : GameVersionInfo,
+ sound_font_table : AudioCodeTable, soundfont_xmls : Dict[int, Tuple[str, Element]],
+ sample_banks : List[Union[AudioTableFile, int]]):
+ soundfonts = []
+
+ for i,entry in enumerate(sound_font_table):
+ entry : AudioCodeTableEntry
+
+ # Lookup the samplebanks used by this soundfont
+ bank1 = bank_data_lookup(sample_banks, version_info.fake_banks.get(i, entry.sample_bank_id_1))
+ bank2 = bank_data_lookup(sample_banks, entry.sample_bank_id_2)
+
+ # Read the data
+ soundfont = AudiobankFile(audiobank_seg, i, entry, sound_font_table.rom_addr, bank1, bank2,
+ entry.sample_bank_id_1, entry.sample_bank_id_2,
+ extraction_xml=soundfont_xmls.get(i, None))
+ soundfonts.append(soundfont)
+
+ if BASEROM_DEBUG:
+ # Write the individual file for debugging and comparison
+ soundfont.dump_bin(f"{extracted_dir}/baserom_audiotest/audiobank_files/{soundfont.file_name}.bin")
+
+ return soundfonts
+
+def extract_samplebank(extracted_dir : str, sample_banks : List[Union[AudioTableFile, int]], bank : AudioTableFile,
+ write_xml : bool):
+ # deal with remaining gaps, have to blob them unless we can find an exact match in another bank
+ bank.finalize_coverage(sample_banks)
+ # assign names
+ bank.assign_names()
+
+ # write xml
+ with open(f"{extracted_dir}/assets/audio/samplebanks/{bank.file_name}.xml", "w") as outfile:
+ outfile.write(bank.to_xml(f"assets/audio/samples/{bank.name}"))
+
+ # write the extraction xml if specified
+ if write_xml:
+ bank.write_extraction_xml(f"assets/xml/audio/samplebanks/{bank.file_name}.xml")
+
+def extract_audio_for_version(version_info : GameVersionInfo, extracted_dir : str, read_xml : bool, write_xml : bool):
+ print("Setting up...")
+
+ # Open baserom segments
+
+ code_seg = None
+ audiotable_seg = None
+ audiobank_seg = None
+
+ with open(f"{extracted_dir}/baserom/code", "rb") as infile:
+ code_seg = memoryview(infile.read())
+
+ with open(f"{extracted_dir}/baserom/Audiotable", "rb") as infile:
+ audiotable_seg = memoryview(infile.read())
+
+ with open(f"{extracted_dir}/baserom/Audiobank", "rb") as infile:
+ audiobank_seg = memoryview(infile.read())
+
+ # ==================================================================================================================
+ # Collect audio tables
+ # ==================================================================================================================
+
+ seq_font_tbl_len = version_info.seq_table - version_info.seq_font_table
+
+ sound_font_table = AudioCodeTable(code_seg, version_info.soundfont_table)
+ sample_bank_table = AudioCodeTable(code_seg, version_info.sample_bank_table)
+ sequence_table = AudioCodeTable(code_seg, version_info.seq_table)
+ sequence_font_table = incbin(code_seg, version_info.seq_font_table, seq_font_tbl_len)
+
+ if BASEROM_DEBUG:
+ # Extract Table Binaries
+
+ os.makedirs(f"{extracted_dir}/baserom_audiotest/audio_code_tables/", exist_ok=True)
+
+ with open(f"{extracted_dir}/baserom_audiotest/audio_code_tables/samplebank_table.bin", "wb") as outfile:
+ outfile.write(sample_bank_table.data)
+
+ with open(f"{extracted_dir}/baserom_audiotest/audio_code_tables/soundfont_table.bin", "wb") as outfile:
+ outfile.write(sound_font_table.data)
+
+ with open(f"{extracted_dir}/baserom_audiotest/audio_code_tables/sequence_table.bin", "wb") as outfile:
+ outfile.write(sequence_table.data)
+
+ with open(f"{extracted_dir}/baserom_audiotest/audio_code_tables/sequence_font_table.bin", "wb") as outfile:
+ outfile.write(sequence_font_table)
+
+ # ==================================================================================================================
+ # Collect extraction xmls
+ # ==================================================================================================================
+
+ samplebank_xmls : Dict[int, Tuple[str, Element]] = {}
+ soundfont_xmls : Dict[int, Tuple[str, Element]] = {}
+ sequence_xmls : Dict[int, Tuple[str, Element]] = {}
+
+ if read_xml:
+ # Read all present xmls
+
+ def walk_xmls(out_dict : Dict[int, Tuple[str, Element]], path : str, typename : str):
+ for root,_,files in os.walk(path):
+ for f in files:
+ fullpath = os.path.join(root, f)
+ xml = ElementTree.parse(fullpath)
+ xml_root = xml.getroot()
+
+ if xml_root.tag != typename or "Name" not in xml_root.attrib or "Index" not in xml_root.attrib:
+ error(f"Malformed {typename} extraction xml: \"{fullpath}\"")
+ out_dict[int(xml_root.attrib["Index"])] = (f.replace(".xml", ""), xml_root)
+
+ walk_xmls(samplebank_xmls, f"assets/xml/audio/samplebanks", "SampleBank")
+ walk_xmls(soundfont_xmls, f"assets/xml/audio/soundfonts", "SoundFont")
+ walk_xmls(sequence_xmls, f"assets/xml/audio/sequences", "Sequence")
+
+ # TODO warn about any missing xmls or xmls with a bad index
+
+ # ==================================================================================================================
+ # Collect samplebanks
+ # ==================================================================================================================
+
+ if BASEROM_DEBUG:
+ os.makedirs(f"{extracted_dir}/baserom_audiotest/audiotable_files", exist_ok=True)
+ sample_banks = collect_sample_banks(audiotable_seg, extracted_dir, version_info, sample_bank_table, samplebank_xmls)
+
+ # ==================================================================================================================
+ # Collect soundfonts
+ # ==================================================================================================================
+
+ if BASEROM_DEBUG:
+ os.makedirs(f"{extracted_dir}/baserom_audiotest/audiobank_files", exist_ok=True)
+ soundfonts = collect_soundfonts(audiobank_seg, extracted_dir, version_info, sound_font_table, soundfont_xmls,
+ sample_banks)
+
+ # ==================================================================================================================
+ # Finalize samplebanks
+ # ==================================================================================================================
+
+ for i,bank in enumerate(sample_banks):
+ if isinstance(bank, AudioTableFile):
+ bank.finalize_samples()
+
+ # ==================================================================================================================
+ # Extract samplebank contents
+ # ==================================================================================================================
+
+ print("Extracting samplebanks...")
+
+ os.makedirs(f"{extracted_dir}/assets/audio/samplebanks", exist_ok=True)
+ if write_xml:
+ os.makedirs(f"assets/xml/audio/samplebanks", exist_ok=True)
+
+ for bank in sample_banks:
+ if isinstance(bank, AudioTableFile):
+ extract_samplebank(extracted_dir, sample_banks, bank, write_xml)
+
+ # ==================================================================================================================
+ # Extract soundfonts
+ # ==================================================================================================================
+
+ print("Extracting soundfonts...")
+
+ os.makedirs(f"{extracted_dir}/assets/audio/soundfonts", exist_ok=True)
+ if write_xml:
+ os.makedirs(f"assets/xml/audio/soundfonts", exist_ok=True)
+
+ for i,sf in enumerate(soundfonts):
+ sf : AudiobankFile
+
+ # Finalize instruments/drums/etc.
+ # This step includes assigning the final samplerate and basenote for the instruments, which may be different
+ # from the samplerate and basenote assigned to their sample prior.
+ sf.finalize()
+
+ # write the soundfont xml itself
+ with open(f"{extracted_dir}/assets/audio/soundfonts/{sf.file_name}.xml", "w") as outfile:
+ outfile.write(sf.to_xml(f"Soundfont_{i}", "assets/audio/samplebanks"))
+
+ # write the extraction xml if specified
+ if write_xml:
+ sf.write_extraction_xml(f"assets/xml/audio/soundfonts/{sf.file_name}.xml")
diff --git a/tools/audio/extraction/audio_tables.py b/tools/audio/extraction/audio_tables.py
new file mode 100644
index 000000000..d288b9262
--- /dev/null
+++ b/tools/audio/extraction/audio_tables.py
@@ -0,0 +1,107 @@
+# SPDX-FileCopyrightText: © 2024 ZeldaRET
+# SPDX-License-Identifier: CC0-1.0
+#
+# Implements code tables structure and related enums
+#
+
+import struct
+from enum import IntEnum
+
+from .util import incbin
+
+class AudioStorageMedium(IntEnum):
+ MEDIUM_RAM = 0
+ MEDIUM_UNK = 1
+ MEDIUM_CART = 2
+ MEDIUM_DISK_DRIVE = 3
+
+class AudioCachePolicy(IntEnum):
+ CACHE_LOAD_PERMANENT = 0
+ CACHE_LOAD_PERSISTENT = 1
+ CACHE_LOAD_TEMPORARY = 2
+ CACHE_LOAD_EITHER = 3
+ CACHE_LOAD_EITHER_NOSYNC = 4
+
+class AudioCodeTableEntry:
+ """
+ typedef struct {
+ /* 0x00 */ u32 romAddr;
+ /* 0x04 */ u32 size;
+ /* 0x08 */ s8 medium;
+ /* 0x09 */ s8 cachePolicy;
+ /* 0x0A */ s16 shortData1;
+ /* 0x0C */ s16 shortData2;
+ /* 0x0E */ s16 shortData3;
+ } AudioTableEntry; // size = 0x10
+ """
+ def __init__(self, data):
+ self.rom_addr, self.size, self.medium, self.cache_policy, self.short_data1, self.short_data2, \
+ self.short_data3 = struct.unpack(">IIbbhhh", data[:0x10])
+
+ self.medium = AudioStorageMedium(self.medium)
+ self.cache_policy = AudioCachePolicy(self.cache_policy)
+
+ self.sample_bank_id_1 = (self.short_data1 >> 8) & 0xFF
+ self.sample_bank_id_2 = (self.short_data1 >> 0) & 0xFF
+
+ self.num_instruments = (self.short_data2 >> 8) & 0xFF
+ self.num_drums = (self.short_data2 >> 0) & 0xFF
+
+ self.num_sfx = self.short_data3
+
+ def __str__(self):
+ out = "{\n"
+ out += f" .romAddr = 0x{self.rom_addr:X}\n"
+ out += f" .size = 0x{self.size:X}\n"
+ out += f" .medium = {self.medium.name}\n"
+ out += f" .cachePolicy = {self.cache_policy.name}\n"
+ out += f" .shortData1 = ({self.sample_bank_id_1} << 8) | {self.sample_bank_id_2}\n"
+ out += f" .shortData2 = ({self.num_instruments} << 8) | {self.num_drums}\n"
+ out += f" .shortData3 = {self.num_sfx}\n"
+ out += "}\n"
+ return out
+
+ def data(self, segment_data : memoryview, segment_offset : int) -> memoryview:
+ return incbin(segment_data, self.rom_addr + segment_offset, self.size)
+
+class AudioCodeTable:
+ """
+ typedef struct {
+ /* 0x00 */ s16 numEntries;
+ /* 0x02 */ s16 unkMediumParam;
+ /* 0x04 */ u32 romAddr;
+ /* 0x08 */ char pad[0x8];
+ /* 0x10 */ AudioTableEntry entries[1/* numEntries */];
+ } AudioTable; // size = 0x10 + 0x10 * numEntries
+ """
+
+ def __init__(self, rom_image : memoryview, rom_start : int):
+ header = incbin(rom_image, rom_start, 0x10)
+
+ self.num_entries, self.unk_medium_param, self.rom_addr = struct.unpack(">hhI", header[:8])
+ assert all([b == 0 for b in header[8:]])
+
+ self.data = incbin(rom_image, rom_start, 0x10 + 0x10 * self.num_entries)
+
+ self.entries = []
+ for i in range(self.num_entries):
+ self.entries.append(AudioCodeTableEntry(self.data[0x10 + 0x10 * i:][:0x10]))
+
+ def __iter__(self) -> AudioCodeTableEntry:
+ for e in self.entries:
+ yield e
+
+ def __len__(self):
+ return len(self.entries)
+
+ def __str__(self):
+ out = "{\n"
+ out += f" .numEntries = {self.num_entries}\n"
+ out += f" .unkMediumParam = {self.unk_medium_param}\n"
+ out += f" .romAddr = 0x{self.rom_addr:X}\n"
+ out += " .entries = {\n"
+ for entry in self.entries:
+ out += str(entry) + "\n"
+ out += " }\n"
+ out += "}\n"
+ return out
diff --git a/tools/audio/extraction/audiobank_file.py b/tools/audio/extraction/audiobank_file.py
new file mode 100644
index 000000000..8c6bd58b9
--- /dev/null
+++ b/tools/audio/extraction/audiobank_file.py
@@ -0,0 +1,956 @@
+# SPDX-FileCopyrightText: © 2024 ZeldaRET
+# SPDX-License-Identifier: CC0-1.0
+#
+# Implements audiobank file
+#
+
+import struct
+from typing import Optional, Tuple
+from xml.etree.ElementTree import Element
+
+from .audio_tables import AudioCodeTableEntry
+from .audiobank_structs import AdpcmBook, AdpcmLoop, Drum, Instrument, SoundFontSample, SoundFontSound
+from .envelope import Envelope
+from .audiotable import AudioTableFile, AudioTableSample
+from .tuning import pitch_names
+from .util import XMLWriter, align, debugm, merge_like_ranges, merge_ranges
+
+# Debug settings
+PLOT_DRUM_TUNING = False
+LOG_COVERAGE = False
+
+def coverage_log(str):
+ if LOG_COVERAGE: debugm(str)
+
+if PLOT_DRUM_TUNING:
+ import matplotlib.pyplot as plt
+
+
+
+# dummy types for coverage labeling
+
+class Padding:
+ pass
+
+class SfxListPtr:
+ SIZE = 4
+
+class DrumsListPtr:
+ SIZE = 4
+
+class InstrumentPtr:
+ SIZE = 4
+
+class DrumPtr:
+ SIZE = 4
+
+
+
+
+
+class DrumGroup:
+
+ def __init__(self):
+ self.drums = []
+ self.start = None
+ self.end = None
+ self.sample_header_offset = None
+ self.sample = None
+
+ # Filled in at finalize
+ self.envelope_offset = None
+ self.envelope = None
+ self.release_rate = None
+ self.pan = None
+ self.sample_header_offset = None
+ self.sample_rate = None
+ self.base_note = None
+ self.needs_rate_override = None
+ self.needs_note_override = None
+
+ def __len__(self):
+ return len(self.drums)
+
+ def __iter__(self):
+ for drum in self.drums:
+ yield drum
+
+ def append(self, drum):
+ self.drums.append(drum)
+
+ def set_range(self, start, end):
+ self.start, self.end = start, end
+
+ def finalize(self, envelopes, sample_lookup_fn):
+ # A drum group should use the same envelope for all entries
+ env_offsets = set(drum.envelope for drum in self.drums)
+ assert len(env_offsets) == 1
+ self.envelope_offset = env_offsets.pop()
+ self.envelope : Envelope = envelopes[self.envelope_offset]
+
+ # A drum group should use the same release rate
+ release_rates = set(drum.release_rate for drum in self.drums)
+ assert len(release_rates) == 1
+ self.release_rate = release_rates.pop()
+
+ # The release rate used should belong to the envelope used
+ assert self.release_rate in self.envelope.release_rates
+
+ # A drum group should always contain a single pan value
+ pans = set(drum.pan for drum in self.drums)
+ assert len(pans) == 1
+ self.pan = pans.pop()
+
+ # A drum group should be the same sample repeated
+ sample_header_offsets = set(drum.sample for drum in self.drums)
+ assert len(sample_header_offsets) == 1
+ sample_header_offset = sample_header_offsets.pop()
+
+ # Fetch sample header
+ self.sample_header_offset = sample_header_offset
+ sample = sample_lookup_fn(sample_header_offset)
+ sample : AudioTableSample
+
+ # Collect final samplerate and basenotes for each drum in the group
+ final_rate = None
+ notes = []
+ for drum in self:
+ drum : Drum
+
+ tuning = drum.tuning
+ assert tuning in sample.tuning_map
+ # Get from sample
+ rate, note = sample.tuning_map[tuning]
+
+ if final_rate is None:
+ final_rate = rate
+ # This should never occur as drum groups are split when the samplerate changes
+ assert final_rate == rate
+
+ notes.append(note)
+
+ # Note values should increase monotonically in a drum group
+ note_indices = [pitch_names.index(note) + 21 for note in notes]
+ assert all(v == note_indices[0] + i for i,v in enumerate(note_indices))
+
+ # Assign final rate and note.
+ # Use first note in the group as the basenote for the whole group, the rest will be filled in during build.
+ self.sample_rate = final_rate
+ self.base_note = notes[0]
+
+ assert sample.sample_rate is not None
+ assert sample.base_note is not None
+
+ # Needs override if they do not agree with the final values in the sample
+ self.needs_rate_override = sample.sample_rate != self.sample_rate
+ self.needs_note_override = sample.base_note != self.base_note
+
+ def to_xml(self, xml : XMLWriter, name : str, sample_name_func, envelope_name_func):
+ attributes = {
+ "Name" : name,
+ "Envelope" : envelope_name_func(self.envelope_offset),
+ }
+
+ if self.release_rate != self.envelope.release_rate():
+ attributes["Release"] = self.release_rate
+
+ attributes["Pan"] = self.pan
+
+ if self.start == self.end:
+ attributes["Note"] = pitch_names[self.start]
+ else:
+ attributes["NoteStart"] = pitch_names[self.start]
+ attributes["NoteEnd"] = pitch_names[self.end]
+
+ attributes["Sample"] = sample_name_func(self.sample_header_offset)
+
+ if self.needs_rate_override:
+ attributes["SampleRate"] = self.sample_rate
+ if self.needs_note_override:
+ attributes["BaseNote"] = self.base_note
+
+ xml.write_element("Drum", attributes)
+
+
+
+
+
+
+
+class AudiobankFile:
+ """
+ """
+
+ def __init__(self, audiobank_seg : memoryview, index : int, table_entry : AudioCodeTableEntry,
+ seg_offset : int, bank1 : AudioTableFile, bank2 : AudioTableFile, bank1_num : int, bank2_num : int,
+ extraction_xml : Tuple[str, Element] = None):
+ self.bank_num = index
+ self.table_entry : AudioCodeTableEntry = table_entry
+ self.num_instruments = self.table_entry.num_instruments
+ self.data = self.table_entry.data(audiobank_seg, seg_offset)
+ self.bank1 : AudioTableFile = bank1
+ self.bank2 : AudioTableFile = bank2
+ self.bank1_num = bank1_num
+ self.bank2_num = bank2_num
+
+ if extraction_xml is None:
+ self.file_name = f"Soundfont_{self.bank_num}"
+ self.name = f"Soundfont_{self.bank_num}"
+
+ self.extraction_envelopes_info = None
+ self.extraction_instruments_info = None
+ self.extraction_drums_info = None
+ self.extraction_effects_info = None
+ else:
+ self.file_name = extraction_xml[0]
+ self.name = extraction_xml[1].attrib["Name"]
+
+ self.extraction_envelopes_info = []
+ self.extraction_instruments_info = {}
+ self.extraction_drums_info = []
+ self.extraction_effects_info = []
+
+ for item in extraction_xml[1]:
+ if item.tag == "Envelopes":
+ for env in item:
+ assert env.tag == "Envelope"
+ self.extraction_envelopes_info.append(env.attrib["Name"])
+ elif item.tag == "Instruments":
+ for instr in item:
+ assert instr.tag == "Instrument"
+ self.extraction_instruments_info[int(instr.attrib["ProgramNumber"])] = instr.attrib["Name"]
+ elif item.tag == "Drums":
+ for drum in item:
+ self.extraction_drums_info.append(drum.attrib["Name"])
+ elif item.tag == "Effects":
+ for effect in item:
+ self.extraction_effects_info.append(effect.attrib["Name"])
+ else:
+ assert False, item.tag
+
+ # Coverage consists of a list of itervals of the form [[start,type],[end,type]]
+ self.coverage = []
+ self.envelopes = {}
+ self.sample_headers = {}
+ self.books = {}
+ self.loops = {}
+ self.loops_have_frames = False
+
+ # Read Drums
+
+ self.collect_drums()
+ self.group_drums()
+
+ # Read Sfx
+
+ self.collect_sfx()
+
+ # Read Instruments
+
+ self.collect_instruments()
+
+
+ # Check Coverage
+
+ self.cvg_log()
+ self.coverage = merge_ranges(self.coverage)
+
+ self.resolve_cvg_gaps()
+ self.coverage = merge_ranges(self.coverage)
+
+ coverage_log("Final Coverage:")
+ coverage_log([[[interval[0][0], interval[0][1].__name__], [interval[1][0], interval[1][1].__name__]] for interval in self.coverage])
+ coverage_log(f"[[{0}, {len(self.data)}]]")
+ assert len(self.coverage) == 1
+ coverage_log("OK")
+
+ # Check End of File
+
+ self.check_end()
+
+ def collect_drums(self):
+ # Read structures
+
+ self.drums_ptr_list_ptr = self.read_pointer(0, DrumsListPtr)
+ assert self.drums_ptr_list_ptr % 16 == 0
+ self.drums_ptr_list = self.read_pointer_list(self.drums_ptr_list_ptr, self.table_entry.num_drums, DrumPtr)
+ self.drums = self.read_list_from_offset_list(self.drums_ptr_list, Drum)
+
+ # Process structures
+
+ for drum in self.drums:
+ if drum is None:
+ # NULL pointer in drums pointer list
+ continue
+
+ # Read envelope
+ self.read_envelope(drum.envelope, drum.release_rate)
+
+ # Read sample if it exists
+ if drum.tuning != 0 and drum.sample != 0:
+ self.read_sample_header(drum.sample, drum.tuning, drum)
+
+ def group_drums(self):
+ self.drum_groups = []
+
+ first = True
+ last_drum = None
+ for drum in self.drums:
+ if drum is None:
+ if last_drum is None and not first:
+ self.drum_groups[-1].append(None)
+ else:
+ self.drum_groups.append([None])
+ last_drum = None
+ else:
+ drum : Drum
+
+ if not drum.group_continuation(last_drum):
+ # group changed
+ self.drum_groups.append(DrumGroup())
+
+ self.drum_groups[-1].append(drum)
+ last_drum = drum
+
+ first = False
+
+ note_start = 0
+ for drum_grp in self.drum_groups:
+ note_end = note_start + len(drum_grp) - 1
+
+ if any(d is not None for d in drum_grp):
+ drum_grp : DrumGroup
+ drum_grp.set_range(note_start, note_end)
+
+ note_start = note_end + 1
+
+ def collect_sfx(self):
+ # Read structures
+
+ self.sfx_list_ptr = self.read_pointer(4, SfxListPtr)
+ assert self.sfx_list_ptr % 16 == 0
+ self.sfx = self.read_list(self.sfx_list_ptr, self.table_entry.num_sfx, SoundFontSound)
+
+ # Process structures
+
+ for sfx in self.sfx:
+ # Read sample if it exists
+ if sfx.tuning != 0 and sfx.sample != 0:
+ self.read_sample_header(sfx.sample, sfx.tuning, sfx)
+
+ def collect_instruments(self):
+ # Read structures
+ self.instrument_offset_list = self.read_pointer_list(8, self.table_entry.num_instruments, InstrumentPtr)
+ self.instruments = self.read_list_from_offset_list(self.instrument_offset_list, Instrument)
+
+ # Record order information
+ for i,instr in enumerate(self.instruments):
+ if instr is None:
+ # NULL entry in pointer list
+ continue
+ instr.program_number = i
+ instr.offset = self.instrument_offset_list[i]
+
+ # Get rid of NULL entries, these correspond to program numbers with no assigned instrument.
+ self.instruments = [instr for instr in self.instruments if instr is not None]
+
+ # Build index map for sequence checking
+ self.instrument_index_map = { instr.program_number : instr for instr in self.instruments }
+
+ # The struct index records the order of the instrument structures themselves. This is often different than the
+ # order they appear in the pointer table, since the pointer table is indexed by program number. We want to emit
+ # xml entries in struct order with a property stating their program number as this seems most user-friendly.
+ for i,instr in enumerate(sorted(self.instruments, key=lambda instr : instr.offset)):
+ instr : Instrument
+ instr.struct_index = i
+
+ # Read data that this structure references
+
+ for i,instr in enumerate(self.instruments):
+ # Read the envelope
+ self.read_envelope(instr.envelope, instr.release_rate)
+
+ # Read the samples, if they exist
+ if instr.low_notes_tuning != 0 and instr.low_notes_sample != 0:
+ self.read_sample_header(instr.low_notes_sample, instr.low_notes_tuning, instr)
+
+ if instr.normal_notes_tuning != 0 and instr.normal_notes_sample != 0:
+ self.read_sample_header(instr.normal_notes_sample, instr.normal_notes_tuning, instr)
+
+ if instr.high_notes_tuning != 0 and instr.high_notes_sample != 0:
+ self.read_sample_header(instr.high_notes_sample, instr.high_notes_tuning, instr)
+
+ def cvg_log(self):
+ if not LOG_COVERAGE:
+ return
+
+ types_ranges = merge_like_ranges(self.coverage)
+
+ for type_range in types_ranges:
+ interval_start, interval_start_type = type_range[0]
+ interval_end, _ = type_range[1]
+
+ if interval_start == interval_end:
+ continue
+
+ interval_length = interval_end - interval_start
+
+ if interval_start_type == int:
+ sizeof_type = 4
+ elif interval_start_type == Padding:
+ sizeof_type = interval_end - interval_start
+ elif interval_start_type == AdpcmBook:
+ sizeof_type = self.read_book_size(interval_start)
+ elif interval_start_type == AdpcmLoop:
+ sizeof_type = self.read_loop_size(interval_start)
+ elif interval_start_type == Envelope.EnvelopePoint:
+ sizeof_type = 4
+ else:
+ sizeof_type = interval_start_type.SIZE
+
+ array_size = interval_length // sizeof_type
+
+ output_str = f"0x{interval_start:04X} - 0x{interval_end:04X} : {interval_start_type.__name__}"
+ if array_size != 1 or interval_start_type == Envelope.EnvelopePoint:
+ output_str += f"[{array_size}]"
+
+ coverage_log(output_str)
+
+ def resolve_cvg_gaps(self):
+ if len(self.coverage) < 2:
+ # There are already no gaps, nothing to do
+ return
+
+ # Resolve gaps in coverage with heuristics
+
+ for i in range(len(self.coverage) - 1):
+ prev_interval = self.coverage[i]
+ next_interval = self.coverage[i + 1]
+
+ unref_start_offset, unref_start_type = prev_interval[1]
+ unref_end_offset, unref_end_type = next_interval[0]
+
+ unaccounted_data = self.data[unref_start_offset:unref_end_offset]
+
+ if unref_end_type in [AdpcmBook, AdpcmLoop] and all(b == 0 for b in unaccounted_data) and \
+ unref_end_offset - unref_start_offset < 16 and (unref_end_offset % 16) == 0:
+ # Book and Loop structures are aligned to 16 byte boundaries, silently mark padding
+ self.coverage.append([[unref_start_offset, Padding], [unref_end_offset, Padding]])
+ continue
+
+ coverage_log(f"Unaccounted: 0x{unref_start_offset:X}({unref_start_type.__name__}) " + \
+ f"to 0x{unref_end_offset:X}({unref_end_type.__name__})")
+ coverage_log([f"0x{b:02X}" for b in unaccounted_data])
+
+ try:
+ if unref_start_type == Envelope.EnvelopePoint:
+ # Assume it is an envelope if it follows an envelope
+ assert unref_start_offset not in self.envelopes
+ coverage_log("Unaccounted follows an envelope, assume it is an envelope")
+ st = self.read_envelope(unref_start_offset, None, is_zero=all(b == 0 for b in unaccounted_data))
+
+ elif unref_start_type in [SoundFontSample, AdpcmLoop]:
+ # Orphaned loops are unlikely, it's more likely a SoundFontSample
+ coverage_log("Unaccounted follows a SoundFontSample or AdpcmLoop, assuming SoundFontSample")
+ st = self.read_sample_header(unref_start_offset, None, None)
+
+ elif unref_start_type == Instrument:
+ coverage_log("Unaccounted follows an Instrument, assume it is an Instrument")
+ st : Instrument = self.read_structure(unref_start_offset, unref_start_type)
+ # Check that we already saw the sample header this instrument wants
+ assert st.normal_notes_sample in self.sample_headers
+ assert st.normal_range_hi == 127 or st.high_notes_sample in self.sample_headers
+ assert st.normal_range_lo == 0 or st.low_notes_sample in self.sample_headers
+ # Insert into instrument list in the appropriate location, mark it as unused so that sfc knows not
+ # to add it to the instrument pointer list when recompiling
+ st.offset = unref_start_offset
+ st.unused = True
+
+ # Assign struct index for this unreferenced instrument
+ new_index = -1
+ for instr in sorted(self.instruments, key= lambda instr : instr.struct_index):
+ instr : Instrument
+
+ if instr.offset > unref_start_offset:
+ if new_index == -1:
+ # Record struct index for the unused instrument
+ new_index = instr.struct_index
+ # Increment struct indices for every structure that occurs after this one
+ instr.struct_index += 1
+ else:
+ # Give it a new index at the end
+ if new_index == -1:
+ new_index = len(self.instruments)
+
+ st.struct_index = new_index
+ self.instruments.append(st)
+ else:
+ st = self.read_structure(unref_start_offset, unref_start_type)
+ coverage_log(st)
+ assert False, "Unhandled coverage case" # handle more structures if they appear
+
+ coverage_log(st)
+ except Exception as e:
+ coverage_log("FAILED")
+ if all(b == 0 for b in unaccounted_data):
+ coverage_log("Probably padding or an empty file?")
+ raise e
+
+ def check_end(self):
+ self.pad_to_size = None
+
+ end = self.coverage[-1][1][0]
+ end_aligned = align(end, 16)
+ if end_aligned != len(self.data):
+ print(f"[Soundfont {self.bank_num:2}] Did not reach end of the file?",
+ f"0x{end_aligned:X} vs 0x{len(self.data):X}")
+ assert all(b == 0 for b in self.data[end_aligned:])
+ self.pad_to_size = len(self.data)
+
+ self.file_padding = None
+
+ if not all(b == 0 for b in self.data[end:]):
+ print(f"[Soundfont {self.bank_num:2}] Non-zero unaccounted data at the end of the file?",
+ f"From 0x{end:X} to 0x{len(self.data):X}")
+ self.file_padding = self.data[end:]
+
+ def dump_bin(self, path):
+ with open(path, "wb") as outfile:
+ outfile.write(self.data)
+
+ def read_loop_size(self, offset):
+ loop_count, = struct.unpack(">I", self.data[offset+8:offset+0xC])
+ return 0x30 if loop_count != 0 else 0x10
+
+ def read_loop_struct(self, offset):
+ return AdpcmLoop(self.logged_read(offset, self.read_loop_size(offset), AdpcmLoop))
+
+ def read_book_size(self, offset):
+ order, npredictors = struct.unpack(">ii", self.data[offset:offset+8])
+ return 8 + 2 * 8 * order * npredictors
+
+ def read_sample_header(self, offset, tuning, ob):
+ assert offset % 16 == 0
+
+ if offset in self.sample_headers:
+ # Don't re-read a sample header structure if it was already read
+ sample_header = self.sample_headers[offset]
+ sample_header : SoundFontSample
+ else:
+ # Read the new sample header and cache it
+ sample_header = self.read_structure(offset, SoundFontSample)
+ self.sample_headers[offset] = sample_header
+
+ # Samples must always have an associated book
+ assert sample_header.book != 0
+
+ if sample_header.book in self.books:
+ # Lookup the book, samples may share books if they are identical
+ book = self.books[sample_header.book]
+ else:
+ # Read the new book
+ book_size = self.read_book_size(sample_header.book)
+ book = AdpcmBook(self.logged_read(sample_header.book, book_size, AdpcmBook))
+
+ # Books are `8 + 16 * n` bytes large and should start on an 0x10 byte boundary.
+ # Check that we get 8 bytes of padding following the book.
+ book_end = sample_header.book + book_size
+ assert sample_header.book % 16 == 0
+ assert book_end % 16 == 8
+ assert all(b == 0 for b in self.logged_read(book_end, 8, Padding))
+
+ # Cache it
+ self.books[sample_header.book] = book
+
+ # Read the loop, if there is one
+ if sample_header.loop == 0:
+ # No loop
+ loop = None
+ elif sample_header.loop in self.loops:
+ # Already seen, look it up
+ loop = self.loops[sample_header.loop]
+ else:
+ # Read new loop structure
+ loop = self.read_loop_struct(sample_header.loop)
+
+ # If loops were determined to store the sample's total frame count, require that all loops with nonzero
+ # count all have the same behavior within the same soundfont
+ if self.loops_have_frames and loop.count != 0:
+ assert loop.num_frames != 0, loop
+
+ # If the numFrames field is nonzero anywhere, record this
+ # TODO this may miss some checks, fix?
+ if loop.num_frames != 0:
+ self.loops_have_frames = True
+
+ # Add the sample to the appropriate samplebank
+ bank = self.bank1 if sample_header.medium == 0 else self.bank2
+ if tuning is not None:
+ bank.add_sample(sample_header, book, loop, tuning, ob)
+ else:
+ # If we found unreferenced sample data that was not discovered elsewhere there is no tuning value to recover
+ # the samplerate from. These need to be handled manually, but this is currently unsupported as this does not
+ # occur in zelda64 audio banks.
+ assert sample_header.sample_addr in bank.samples , \
+ "Unreferenced sample header refers to sample that was not otherwise discovered, cannot " + \
+ "automatically recover sample rate"
+
+ return sample_header
+
+ def read_envelope_points(self, offset, is_zero=False):
+ size = 0
+
+ if not is_zero:
+ points = []
+
+ while True:
+ point = Envelope.EnvelopePoint(*struct.unpack(">hh", self.data[offset + size:][:4]))
+ assert point.delay >= -3 # TODO this could be used to determine whether data is really an envelope
+ points.append(point)
+ size += 4
+ if point.delay < 0:
+ break
+
+ # pad to 0x10 byte boundary
+ while (size % 16) != 0:
+ point = Envelope.EnvelopePoint(*struct.unpack(">hh", self.data[offset + size:][:4]))
+ assert point.delay == 0 and point.arg == 0
+ points.append(point)
+ size += 4
+ else:
+ size = 16
+ points = [Envelope.EnvelopePoint(0, 0), Envelope.EnvelopePoint(0, 0),
+ Envelope.EnvelopePoint(0, 0), Envelope.EnvelopePoint(0, 0)]
+
+ return points, size
+
+ def read_envelope(self, offset, release_rate, is_zero=False):
+ assert offset % 16 == 0
+
+ if offset in self.envelopes:
+ # Look it up if it was already seen
+ env = self.envelopes[offset]
+ else:
+ # Read new
+ points, size = self.read_envelope_points(offset, is_zero)
+ env = Envelope(points, is_zero=is_zero)
+
+ # Cache it
+ self.envelopes[offset] = env
+ # Mark coverage
+ self.coverage.append([[offset, Envelope.EnvelopePoint], [offset + size, Envelope.EnvelopePoint]])
+
+ # Add release rate if there was one
+ if release_rate is not None:
+ env.release_rates.append(release_rate)
+
+ return env
+
+ def logged_read(self, start, length, dtype):
+ """
+ Read data while also recording coverage information
+ """
+ end = start + length
+ self.coverage.append([[start, dtype], [end, dtype]])
+ return self.data[start:end]
+
+ def read_structure(self, offset, dtype):
+ return dtype(self.logged_read(offset, dtype.SIZE, dtype))
+
+ def read_list(self, offset, num, dtype):
+ return [dtype(i, self.logged_read(offset + i * dtype.SIZE, dtype.SIZE, dtype)) for i in range(num)]
+
+ def read_pointer(self, offset, ptr_type):
+ return struct.unpack('>I', self.logged_read(offset, 4, ptr_type))[0]
+
+ def read_list_from_offset_list(self, offset_list, dtype):
+ assert all([b % 0x10 == 0 for b in offset_list])
+ return [dtype(self.logged_read(offset, dtype.SIZE, dtype)) if offset != 0 else None for offset in offset_list]
+
+ def read_pointer_list(self, offset, count, ptr_type):
+ # May be NULL, but only if the count is 0
+ assert (count == 0 and offset == 0) or offset != 0
+
+ if count == 0:
+ # No data
+ return []
+
+ # Read pointer list contents
+ ptr_list = [i[0] for i in struct.iter_unpack('>I', self.logged_read(offset, 4 * count, ptr_type))]
+ assert len(ptr_list) == count
+
+ # Pointer lists seem to always pad to the next 0x10 byte boundary
+ pointers_end = offset + 4 * count
+ possible_pad = self.logged_read(pointers_end, align(pointers_end, 16) - pointers_end, Padding)
+ assert all(b == 0 for b in possible_pad)
+
+ return ptr_list
+
+ def sorted_envelopes(self):
+ # sort by offset
+ for i,(offset,env) in enumerate(sorted(self.envelopes.items(), key=lambda x : x[0])):
+ yield i,(offset,env)
+
+ def envelope_name_func(self, offset):
+ return self.envelopes[offset].name
+
+ def sorted_sample_headers(self):
+ for i,offset in enumerate(sorted(self.sample_headers)):
+ yield i,(offset,self.sample_headers[offset])
+
+ def lookup_sample(self, header_offset : int) -> Optional[AudioTableSample]:
+ if header_offset == 0:
+ return None
+ header : SoundFontSample = self.sample_headers[header_offset]
+ bank = self.bank1 if header.medium == 0 else self.bank2
+ return bank.lookup_sample(header.sample_addr)
+
+ def lookup_sample_name(self, sample_header : SoundFontSample):
+ bank = self.bank1 if sample_header.medium == 0 else self.bank2
+ name = bank.lookup_sample(sample_header.sample_addr).name
+ assert name is not None
+ return name
+
+ def sample_name_func(self, offset):
+ return self.lookup_sample_name(self.sample_headers[offset])
+
+ def finalize(self):
+ # Assign envelope names
+ for i,(offset,env) in self.sorted_envelopes():
+ env : Envelope
+ env.name = self.envelope_name(i)
+
+ # Link Instruments
+ for instr in self.instruments:
+ instr.finalize(self.lookup_sample)
+
+ # Final Drum Groups
+
+ if PLOT_DRUM_TUNING:
+ plt.clf()
+ plt.cla()
+ plt.title(f"Drums in soundfont {self.bank_num}")
+ plt.xlabel("Drum index")
+ plt.ylabel("Tuning value")
+
+ for drum_grp in self.drum_groups:
+ if all(d is None for d in drum_grp):
+ continue
+
+ if PLOT_DRUM_TUNING:
+ plt.plot( range(drum_grp.start,drum_grp.end), [drum.tuning for drum in drum_grp])
+ plt.scatter(range(drum_grp.start,drum_grp.end), [drum.tuning for drum in drum_grp])
+
+ drum_grp : DrumGroup
+ drum_grp.finalize(self.envelopes, self.lookup_sample)
+
+ if PLOT_DRUM_TUNING:
+ if len(self.drum_groups) != 0:
+ plt.savefig(f"figures/drums_{self.bank_num}.png")
+
+ # Link SFX
+ for sfx in self.sfx:
+ sfx.finalize(self.lookup_sample)
+
+ # TODO resolve decay/release index overrides?
+
+ def envelope_name(self, index):
+ if self.extraction_envelopes_info is not None:
+ return self.extraction_envelopes_info[index]
+ else:
+ return f"Env{index}"
+
+ def instrument_name(self, program_number):
+ if self.extraction_instruments_info is not None:
+ return self.extraction_instruments_info[program_number]
+ else:
+ return f"INST_{program_number}"
+
+ def drum_grp_name(self, index):
+ if self.extraction_drums_info is not None:
+ return self.extraction_drums_info[index]
+ else:
+ return f"DRUM_{index}"
+
+ def effect_name(self, index):
+ if self.extraction_effects_info is not None:
+ return self.extraction_effects_info[index]
+ else:
+ return f"EFFECT_{index}"
+
+ def envelopes_to_xml(self, xml : XMLWriter):
+ if len(self.envelopes) == 0:
+ return
+
+ xml.write_start_tag("Envelopes")
+
+ for i,(offset,env) in self.sorted_envelopes():
+ env : Envelope
+ env.to_xml(xml, self.envelope_name(i))
+
+ xml.write_end_tag()
+
+ def samples_to_xml(self, xml : XMLWriter):
+ if len(self.sample_headers) == 0:
+ return
+
+ xml.write_start_tag("Samples")
+
+ # Emit these in the order the sample headers appear in the soundfont
+ for i,(offset,sample_header) in self.sorted_sample_headers():
+ sample_header : SoundFontSample
+ sample_header.to_xml(xml, self.lookup_sample_name(sample_header))
+
+ xml.write_end_tag()
+
+ def sfx_to_xml(self, xml : XMLWriter):
+ if len(self.sfx) == 0:
+ return
+
+ xml.write_start_tag("Effects")
+
+ for i,sfx in enumerate(self.sfx):
+ sfx.to_xml(xml, self.effect_name(i), self.sample_name_func)
+
+ xml.write_end_tag()
+
+ def drums_to_xml(self, xml : XMLWriter):
+ if len(self.drums) == 0:
+ return
+
+ xml.write_start_tag("Drums")
+
+ for i,drum_grp in enumerate(self.drum_groups):
+ if isinstance(drum_grp, list):
+ for _ in range(len(drum_grp)):
+ xml.write_element("Drum")
+ else:
+ drum_grp : DrumGroup
+ drum_grp.to_xml(xml, self.drum_grp_name(i), self.sample_name_func, self.envelope_name_func)
+
+ xml.write_end_tag()
+
+ def instruments_to_xml(self, xml : XMLWriter):
+ if len(self.instruments) == 0:
+ return
+
+ xml.write_start_tag("Instruments")
+
+ # Write in struct order
+ for instr in sorted(self.instruments, key=lambda instr : instr.struct_index):
+ instr : Instrument
+ name = self.instrument_name(instr.program_number) if not instr.unused else None
+ instr.to_xml(xml, name, self.sample_name_func, self.envelope_name_func)
+
+ xml.write_end_tag()
+
+ def to_xml(self, name, samplebanks_base):
+ xml = XMLWriter()
+
+ start = {
+ "Name" : name,
+ "Index" : self.bank_num,
+ "Medium" : self.table_entry.medium.name,
+ "CachePolicy" : self.table_entry.cache_policy.name,
+ "SampleBank" : f"$(BUILD_DIR)/{samplebanks_base}/{self.bank1.file_name}.xml",
+ }
+
+ # If the samplebank1 index is not the true index (that is it's a pointer), write an Indirect
+ if self.bank1_num != self.bank1.bank_num:
+ start["Indirect"] = self.bank1_num
+
+ if self.bank2_num != 255: # bank2 is not None if bank2_num != 255
+ start["SampleBankDD"] = f"$(BUILD_DIR)/{samplebanks_base}/{self.bank2.file_name}.xml",
+ # TODO we should really write an indirect for DD banks too if bank2_num != bank2.bank_num
+
+ if self.loops_have_frames:
+ # Some MM banks have sample frame counts embedded in loop headers, but not all soundfonts do this
+ start["LoopsHaveFrames"] = "true"
+
+ if max(instr.program_number or 0 for instr in self.instruments) + 1 != self.table_entry.num_instruments:
+ # Some banks have trailing NULLs in their instrument pointer tables, record the max length for matching
+ start["NumInstruments"] = self.table_entry.num_instruments
+
+ if self.pad_to_size is not None:
+ # The final soundfont typically has extra zeros at the end
+ start["PadToSize"] = f"0x{self.pad_to_size:X}"
+
+ xml.write_start_tag("Soundfont", start)
+
+ self.envelopes_to_xml(xml)
+ self.samples_to_xml(xml)
+
+ self.sfx_to_xml(xml)
+ self.drums_to_xml(xml)
+ self.instruments_to_xml(xml)
+
+ if self.file_padding is not None:
+ # Some soundfonts may have garbage data in the final 16-byte file padding
+ xml.write_start_tag("MatchPadding")
+ xml.write_raw(", ".join(f"0x{b:02X}" for b in self.file_padding))
+ xml.write_end_tag()
+
+ xml.write_end_tag()
+ return str(xml)
+
+ def write_extraction_xml(self, path):
+ xml = XMLWriter()
+
+ xml.write_comment("This file is only for extraction of vanilla data. For other purposes see assets/audio/soundfonts/")
+
+ xml.write_start_tag("SoundFont", {
+ "Name" : self.name,
+ "Index" : self.bank_num,
+ })
+
+ # add contents for names
+
+ if len(self.envelopes) != 0:
+ xml.write_start_tag("Envelopes")
+
+ for i in range(len(self.envelopes)):
+ xml.write_element("Envelope", {
+ "Name" : self.envelope_name(i)
+ })
+
+ xml.write_end_tag()
+
+ if len(self.instruments) != 0:
+ xml.write_start_tag("Instruments")
+
+ # Write in struct order
+ for instr in sorted(self.instruments, key=lambda instr : instr.struct_index):
+ instr : Instrument
+ if not instr.unused:
+ xml.write_element("Instrument", {
+ "ProgramNumber" : instr.program_number,
+ "Name" : self.instrument_name(instr.program_number),
+ })
+
+ xml.write_end_tag()
+
+ if any(isinstance(dg, DrumGroup) for dg in self.drum_groups):
+ xml.write_start_tag("Drums")
+
+ for i,drum_grp in enumerate(self.drum_groups):
+ if isinstance(drum_grp, DrumGroup):
+ xml.write_element("Drum", {
+ "Name" : self.drum_grp_name(i)
+ })
+
+ xml.write_end_tag()
+
+ if len(self.sfx) != 0:
+ xml.write_start_tag("Effects")
+
+ for i,sfx in enumerate(self.sfx):
+ xml.write_element("Effect", {
+ "Name" : self.effect_name(i)
+ })
+
+ xml.write_end_tag()
+
+ xml.write_end_tag()
+
+ with open(path, "w") as outfile:
+ outfile.write(str(xml))
diff --git a/tools/audio/extraction/audiobank_structs.py b/tools/audio/extraction/audiobank_structs.py
new file mode 100644
index 000000000..fa6afa37d
--- /dev/null
+++ b/tools/audio/extraction/audiobank_structs.py
@@ -0,0 +1,406 @@
+# SPDX-FileCopyrightText: © 2024 ZeldaRET
+# SPDX-License-Identifier: CC0-1.0
+#
+# This file implements reading various structures resident to the Audiobank files.
+# Additionally handles:
+# - Linking with finalized samples
+# - Writing xml elements representing these structures in soundfont xmls
+#
+
+import struct
+from enum import IntEnum
+
+from .audio_tables import AudioStorageMedium
+from .tuning import rate_from_tuning, pitch_names
+from .util import XMLWriter
+
+VADPCM_VERSTAMP = 1
+
+class AudioSampleCodec(IntEnum):
+ CODEC_ADPCM = 0
+ CODEC_S8 = 1
+ CODEC_S16_INMEMORY = 2
+ CODEC_SMALL_ADPCM = 3
+ CODEC_REVERB = 4
+ CODEC_S16 = 5
+
+
+
+class SoundFontSample: # SampleHeader ?
+ """
+ typedef struct {
+ /* 0x00 */ u32 codec : 4;
+ /* 0x00 */ u32 medium : 2; // storage medium determines which of the two sample bank ids to use when relocating sampleAddr
+ /* 0x00 */ u32 cached : 1;
+ /* 0x00 */ u32 isRelocated : 1;
+ /* 0x01 */ u32 size : 24;
+ /* 0x04 */ u8* sampleAddr; // offset into the sample bank associated with this soundfont
+ /* 0x08 */ AdpcmLoop* loop;
+ /* 0x0C */ AdpcmBook* book;
+ } SoundFontSample; // size = 0x10
+ """
+ SIZE = 0x10
+
+ def __init__(self, data):
+ bits, self.sample_addr, self.loop, self.book = struct.unpack(">IIII", data[:0x10])
+
+ self.codec = AudioSampleCodec((bits >> 28) & 0b1111)
+ self.medium = AudioStorageMedium((bits >> 26) & 0b11)
+ self.cached = bool((bits >> 25) & 1)
+ self.is_relocated = bool((bits >> 24) & 1)
+ self.size = (bits >> 0) & 0b111111111111111111111111
+
+ assert self.book != 0
+ assert self.loop != 0
+ assert self.codec in [AudioSampleCodec.CODEC_ADPCM, AudioSampleCodec.CODEC_SMALL_ADPCM]
+ assert self.medium == 0
+ assert not self.is_relocated # Not relocated in ROM
+
+ def to_xml(self, xml : XMLWriter, name : str, rate_override = None, note_override = None):
+ # Example xml output:
+ # <Sample Name="SAMPLE_NAME" SampleRate="32000" BaseNote="C4" IsDD="false" Cached="false">
+
+ attrs = { "Name" : name }
+ if rate_override is not None:
+ attrs["SampleRate"] = rate_override
+ if note_override is not None:
+ attrs["BaseNote"] = note_override
+ if self.medium != 0:
+ attrs["IsDD"] = "true"
+ if self.cached:
+ attrs["Cached"] = str(self.cached).lower()
+
+ xml.write_element("Sample", attrs)
+
+ def __str__(self):
+ out = "(SoundFontSample){\n"
+ out += f" .codec = {self.codec.name}\n"
+ out += f" .medium = {self.medium.name}\n"
+ out += f" .cached = {self.cached}\n"
+ out += f" .is_relocated = {self.is_relocated}\n"
+ out += f" .size = 0x{self.size:X}\n"
+ out += f" .sampleAddr = 0x{self.sample_addr:X}\n"
+ out += f" .loop = 0x{self.loop:X}\n"
+ out += f" .book = 0x{self.book:X}\n"
+ out += "}\n"
+ return out
+
+
+
+class AdpcmLoop:
+ """
+ typedef struct {
+ /* 0x00 */ u32 start;
+ /* 0x04 */ u32 end;
+ /* 0x08 */ u32 count;
+ /* 0x0C */ u32 numFrames;
+ /* 0x10 */ s16 state[16]; // only exists if count != 0. 8-byte aligned
+ } AdpcmLoop; // size = 0x30 (or 0x10)
+ """
+
+ def __init__(self, data):
+ self.start, self.end, self.count, self.num_frames = struct.unpack(">IIII", data[:0x10])
+
+ # We expect loops to be either "no loop" or "infinite", as these are all that vadpcm_enc could handle.
+ assert self.count in (0,0xFFFFFFFF)
+
+ if self.count != 0:
+ self.state = tuple(s[0] for s in struct.iter_unpack(">h", data[0x10:0x30]))
+ else:
+ # A count of 0 indicates "no loop", but a loop structure is mandatory for all samples so something had to
+ # be emitted. Ensure the start is at 0, later we will ensure that the end is at the last frame of the sample
+ # once we have the sample data.
+ assert self.start == 0
+ self.state = tuple([0] * 16)
+ assert len(self.state) == 16
+
+ def serialize(self):
+ """
+ Creates VADPCMLOOPS section data for aifc files
+ """
+ NUM_LOOPS = 1
+
+ return struct.pack(">HHIII16h",
+ VADPCM_VERSTAMP, NUM_LOOPS,
+ self.start, self.end, self.count,
+ *self.state)
+
+ def __eq__(self, other):
+ if not isinstance(other, AdpcmLoop):
+ return False
+ other : AdpcmLoop
+
+ start_matches = self.start == other.start
+ end_matches = self.end == other.end
+ count_matches = self.count == other.count
+ # We don't check num_frames in loop equality since loops in different soundfonts referring to the same
+ # sample data may not have this field filled out
+ return start_matches and end_matches and count_matches and self.state == other.state
+
+ def __str__(self):
+ out = "(AdpcmLoop){\n"
+ out += f" .start = {self.start},\n"
+ out += f" .end = {self.end},\n"
+ out += f" .count = {self.count},\n"
+ out += f" .numFrames = {self.num_frames},\n"
+ out += f" .state = {self.state},\n"
+ out += "}\n"
+ return out
+
+class AdpcmBook:
+ """
+ typedef struct {
+ /* 0x00 */ s32 order;
+ /* 0x04 */ s32 npredictors;
+ /* 0x08 */ s16 book[1]; // size 8 * order * npredictors. 8-byte aligned
+ } AdpcmBook; // size >= 0x8
+ """
+
+ def __init__(self, data):
+ self.order, self.n_predictors = struct.unpack(">ii", data[:8])
+ self.book = tuple(s[0] for s in struct.iter_unpack(">h", data[8:][:2 * 8 * self.order * self.n_predictors]))
+ assert len(self.book) == 8 * self.order * self.n_predictors , (len(self.book), 8 * self.order * self.n_predictors)
+
+ def serialize(self):
+ header = struct.pack(">hhh", VADPCM_VERSTAMP, self.order, self.n_predictors)
+ data = b"".join(struct.pack(">h", x) for x in self.book)
+ return header + data
+
+ def __eq__(self, other):
+ if not isinstance(other, AdpcmBook):
+ return False
+ other : AdpcmBook
+
+ order_matches = self.order == other.order
+ npredictors_matches = self.n_predictors == other.n_predictors
+ return order_matches and npredictors_matches and self.book == other.book
+
+ def __str__(self):
+ out = "(AdpcmBook){\n"
+ out += f" .order = {self.order},\n"
+ out += f" .npredictors = {self.n_predictors},\n"
+ out += f" .book = {self.book},\n"
+ out += "}\n"
+ return out
+
+
+
+class SoundFontSound:
+ """
+ typedef struct {
+ /* 0x00 */ SoundFontSample* sample;
+ /* 0x04 */ f32 tuning; // frequency scale factor
+ } SoundFontSound; // size = 0x8
+ """
+ SIZE = 8
+
+ def __init__(self, index, data):
+ self.index = index
+ self.sample, self.tuning = struct.unpack(">If", data[:8])
+
+ def finalize(self, sample_lookup_fn):
+ from .audiotable import AudioTableSample
+
+ sample = sample_lookup_fn(self.sample)
+ if sample is None:
+ return
+
+ assert isinstance(sample, AudioTableSample)
+ sample : AudioTableSample
+
+ assert self.tuning in sample.tuning_map
+ rate,note = sample.tuning_map[self.tuning]
+
+ self.sample_rate = rate
+ self.needs_rate_override = self.sample_rate != sample.sample_rate
+
+ self.base_note = note
+ self.needs_note_override = self.base_note != sample.base_note
+
+ def __str__(self) -> str:
+ out = "(SoundFontSound}{\n"
+ out += f" .sample = 0x{self.sample:X}\n"
+ out += f" .tuning = {self.tuning:.7f}f\n"
+ out += "}\n"
+ return out
+
+ def to_xml(self, xml : XMLWriter, name : str, sample_name_func):
+ if self.sample == 0 and self.tuning == 0:
+ xml.write_element("Effect")
+ else:
+ attrs = {
+ "Name" : name,
+ "Sample" : sample_name_func(self.sample),
+ }
+ if self.needs_rate_override:
+ attrs["SampleRate"] = self.sample_rate
+ if self.needs_note_override:
+ attrs["BaseNote"] = self.base_note
+
+ xml.write_element("Effect", attrs)
+
+
+
+class Drum:
+ """
+ typedef struct {
+ /* 0x00 */ u8 releaseRate;
+ /* 0x01 */ u8 pan;
+ /* 0x02 */ u8 isRelocated;
+ /* 0x04 */ SoundFontSound sound;
+ /* 0x0C */ AdsrEnvelope* envelope;
+ } Drum; // size = 0x10
+ """
+ SIZE = 0x10
+
+ def __init__(self, data):
+ self.release_rate, self.pan, self.is_relocated, self.sample, self.tuning, self.envelope = \
+ struct.unpack(">BBBxIfI", data[:0x10])
+
+ assert self.is_relocated == 0
+
+ def group_continuation(self, other):
+ """
+ Determine if self is a continuation of the drum group containing other, the last drum added.
+ """
+ # If there is no previous drum or the previous drum was an empty entry, always begin a new group
+ if other is None:
+ return False
+
+ assert isinstance(other, Drum)
+
+ # Check general agreement, if these attributes do not match it is certainly not part of the same group
+ if self.sample == other.sample and self.pan == other.pan and self.envelope == other.envelope and \
+ self.release_rate == other.release_rate:
+ # If there is any intersection in the samplerates, assume these are in the same drum group
+ samplerates1 = set(rate for _,rate in rate_from_tuning(self.tuning))
+ samplerates2 = set(rate for _,rate in rate_from_tuning(other.tuning))
+ return len(samplerates1.intersection(samplerates2)) != 0
+
+ return False
+
+ def __str__(self):
+ out = "(Drum){\n"
+ out += f" .releaseRate = {self.release_rate},\n"
+ out += f" .pan = {self.pan},\n"
+ out += f" .isRelocated = {self.is_relocated},\n"
+ out += f" .sound.sample = 0x{self.sample:X},\n"
+ out += f" .sound.tuning = {self.tuning:.7f}f,\n"
+ out += f" .envelope = 0x{self.envelope:X},\n"
+ out += "}\n"
+ return out
+
+
+
+class Instrument:
+ """
+ typedef struct {
+ /* 0x00 */ u8 isRelocated;
+ /* 0x01 */ u8 normalRangeLo;
+ /* 0x02 */ u8 normalRangeHi;
+ /* 0x03 */ u8 releaseRate;
+ /* 0x04 */ AdsrEnvelope* envelope;
+ /* 0x08 */ SoundFontSound lowNotesSound;
+ /* 0x10 */ SoundFontSound normalNotesSound;
+ /* 0x18 */ SoundFontSound highNotesSound;
+ } Instrument; // size = 0x20
+ """
+ SIZE = 0x20
+
+ def __init__(self, data):
+ self.is_relocated, self.normal_range_lo, self.normal_range_hi, self.release_rate, self.envelope, \
+ self.low_notes_sample, self.low_notes_tuning, \
+ self.normal_notes_sample, self.normal_notes_tuning, \
+ self.high_notes_sample, self.high_notes_tuning = struct.unpack(">BBBBIIfIfIf", data[:0x20])
+
+ self.program_number = None
+ self.offset = None
+ self.struct_index = None
+ self.unused = False
+
+ assert self.is_relocated == 0
+
+ # Sample is either present or the split point is at the start/end
+ assert not (self.low_notes_sample == 0 and self.low_notes_tuning == 0.0) or self.normal_range_lo == 0
+ assert not (self.high_notes_sample == 0 and self.high_notes_tuning == 0.0) or self.normal_range_hi == 127
+
+ def __str__(self):
+ out = "(Instrument){\n"
+ out += f" .isRelocated = {self.is_relocated},\n"
+ out += f" .normalRangeLo = {self.normal_range_lo},\n"
+ out += f" .normalRangeHi = {self.normal_range_hi},\n"
+ out += f" .releaseRate = {self.release_rate},\n"
+ out += f" .envelope = 0x{self.envelope:X},\n"
+ out += f" .lowNotesSound.sample = {self.low_notes_sample},\n"
+ out += f" .lowNotesSound.tuning = {self.low_notes_tuning},\n"
+ out += f" .normalNotesSound.sample = {self.normal_notes_sample},\n"
+ out += f" .normalNotesSound.tuning = {self.normal_notes_tuning},\n"
+ out += f" .highNotesSound.sample = {self.high_notes_sample},\n"
+ out += f" .highNotesSound.tuning = {self.high_notes_tuning},\n"
+ out += "}\n"
+ return out
+
+ def finalize(self, sample_lookup_fn):
+ from .audiotable import AudioTableSample
+
+ self.sample_rate = [None] * 3
+ self.base_note = [None] * 3
+ self.needs_rate_override = [False] * 3
+ self.needs_note_override = [False] * 3
+
+ sample_offsets = (self.low_notes_sample, self.normal_notes_sample, self.high_notes_sample)
+ tunings = (self.low_notes_tuning, self.normal_notes_tuning, self.high_notes_tuning)
+ for i,(sample_offset,tuning) in enumerate(zip(sample_offsets, tunings)):
+ sample = sample_lookup_fn(sample_offset)
+ if sample is None:
+ continue
+ assert isinstance(sample, AudioTableSample)
+ sample : AudioTableSample
+
+ assert tuning in sample.tuning_map
+ rate,note = sample.tuning_map[tuning]
+
+ self.sample_rate[i] = rate
+ self.needs_rate_override[i] = self.sample_rate[i] != sample.sample_rate
+
+ self.base_note[i] = note
+ self.needs_note_override[i] = self.base_note[i] != sample.base_note
+
+ def to_xml(self, xml : XMLWriter, name : str, sample_names_func, envelope_name_func):
+ attributes = {}
+
+ if not self.unused:
+ attributes["ProgramNumber"] = self.program_number
+ attributes["Name"] = name
+
+ # TODO release rate overrides?
+ attributes.update({
+ "Envelope" : envelope_name_func(self.envelope),
+ #"Release" : self.release_rate,
+ "Sample" : sample_names_func(self.normal_notes_sample),
+ })
+
+ if self.needs_rate_override[1]:
+ attributes["SampleRate"] = self.sample_rate[1]
+ if self.needs_note_override[1]:
+ attributes["BaseNote"] = self.base_note[1]
+
+ if self.normal_range_lo != 0:
+ attributes["RangeLo"] = pitch_names[self.normal_range_lo]
+ attributes["SampleLo"] = sample_names_func(self.low_notes_sample)
+
+ if self.needs_rate_override[0]:
+ attributes["SampleRateLo"] = self.sample_rate[0]
+ if self.needs_note_override[0]:
+ attributes["BaseNoteLo"] = self.base_note[0]
+
+ if self.normal_range_hi != 127:
+ attributes["RangeHi"] = pitch_names[self.normal_range_hi]
+ attributes["SampleHi"] = sample_names_func(self.high_notes_sample)
+
+ if self.needs_rate_override[2]:
+ attributes["SampleRateHi"] = self.sample_rate[2]
+ if self.needs_note_override[2]:
+ attributes["BaseNoteHi"] = self.base_note[2]
+
+ xml.write_element("Instrument" if not self.unused else "InstrumentUnused", attributes)
diff --git a/tools/audio/extraction/audiotable.py b/tools/audio/extraction/audiotable.py
new file mode 100644
index 000000000..e02a6f285
--- /dev/null
+++ b/tools/audio/extraction/audiotable.py
@@ -0,0 +1,690 @@
+# SPDX-FileCopyrightText: © 2024 ZeldaRET
+# SPDX-License-Identifier: CC0-1.0
+#
+#
+#
+
+import struct
+from typing import Dict, Tuple
+from xml.etree.ElementTree import Element
+
+from .audio_tables import AudioCodeTableEntry
+from .audiobank_structs import AudioSampleCodec, SoundFontSample, AdpcmBook, AdpcmLoop
+from .tuning import pitch_names, note_z64_to_midi, recalc_tuning, rate_from_tuning, rank_rates_notes, BAD_FLOATS
+from .util import align, error, XMLWriter, f32_to_u32
+
+class AIFCFile:
+
+ def __init__(self):
+ self.sections = []
+ self.total_size = 0
+
+ @staticmethod
+ def pstring(data):
+ return bytes([len(data)]) + data + (b"" if len(data) % 2 else b"\0")
+
+ @staticmethod
+ def serialize_f80(num):
+ """
+ Convert num to 80-bit float. Does not accept denormal/infinity/nan but these should never appear anyway.
+ """
+ num = float(num)
+ if num == 0.0:
+ return b"\0" * 10
+ elif num == -0.0:
+ return b"\x80" + b"\0" * 9
+
+ f64_bits, = struct.unpack(">Q", struct.pack(">d", num))
+
+ f64_sign_bit = f64_bits & (2 ** 63)
+
+ f64_exponent = (f64_bits ^ f64_sign_bit) >> 52
+ assert f64_exponent != 0, "can't handle denormals"
+ assert f64_exponent != 0x7FF, "can't handle infinity/nan"
+ f64_exponent -= 1023
+
+ f64_mantissa = f64_bits & (2 ** 52 - 1)
+
+ f80_sign_bit = f64_sign_bit << (80 - 64)
+ f80_exponent = (f64_exponent + 0x3FFF) << 64
+ f80_mantissa = (2 ** 63) | (f64_mantissa << (63 - 52))
+
+ f80 = f80_sign_bit | f80_exponent | f80_mantissa
+
+ return struct.pack(">HQ", f80 >> 64, f80 & (2 ** 64 - 1))
+
+ def add_section(self, tp, data):
+ assert isinstance(tp, bytes)
+ assert isinstance(data, bytes)
+
+ self.sections.append((tp, data))
+ self.total_size += align(len(data),2) + 8
+
+ def add_custom_section(self, tp, data):
+ self.add_section(b"APPL", b"stoc" + self.pstring(tp) + data)
+
+ def remove_section(self, tp):
+ assert isinstance(tp, bytes)
+
+ for s_tp, s_data in self.sections:
+ if s_tp == tp:
+ self.sections.remove((s_tp, s_data))
+ self.total_size -= align(len(s_data),2) + 8
+ return
+
+ def commit(self, outpath):
+ self.total_size += 4
+
+ with open(outpath, "wb") as outfile:
+ outfile.write(b"FORM" + struct.pack(">I", self.total_size) + b"AIFC")
+
+ for tp, data in self.sections:
+ outfile.write(tp + struct.pack(">I", len(data)))
+ outfile.write(data)
+
+ if len(data) % 2:
+ outfile.write(b"\0")
+
+class AudioTableData:
+ """
+ Unaccounted data in the Audiotable
+ """
+
+ def __init__(self, start, end, data):
+ self.start : int = start
+ self.end : int = end
+ self.data = data
+ assert len(self.data) % 2 == 0
+
+ self.name : str = None
+ self.filename : str = None
+
+ def __len__(self):
+ return len(self.data)
+
+ def to_asm(self, name):
+ out = f"# {name} [0x{self.start:X}:0x{self.end:X}](0x{self.end-self.start:X})\n\n"
+ out += " .byte "
+ for i,b in enumerate(self.data):
+ if i != 0 and i % 32 == 0:
+ out = out[:-2] + "\n .byte "
+ out += f"0x{b:02X}, "
+ out = out[:-2] + "\n\n"
+ return out
+
+ def to_file(self, outpath : str):
+ # Output as binary blob
+
+ with open(outpath, "wb") as outfile:
+ outfile.write(self.data)
+
+
+
+PCM16_SAMPLE_SIZE = 16
+
+class AudioTableSample(AudioTableData):
+ """
+ Sample in the Audiotable
+ """
+
+ def __init__(self, start : int, end : int, header : SoundFontSample, data, book : AdpcmBook, loop : AdpcmLoop, padding=None):
+ super().__init__(start, end, data)
+
+ self.header : SoundFontSample = header
+ self.book : AdpcmBook = book
+ self.loop : AdpcmLoop = loop
+ self.padding = padding
+
+ self.notes_rates = set()
+ self.sample_rate = None
+ self.base_note = None
+ self.tuning_map = None
+
+ if self.loop.count == 0:
+ # If a count is 0 the loop end must be the (bugged, vadpcm_enc computed it wrong originally) frame count
+ num_frames_bugged = (len(self.data) * PCM16_SAMPLE_SIZE) // self.frame_size()
+ assert self.loop.end == num_frames_bugged, f"{self.loop.end}, {num_frames_bugged}"
+
+ def clone(self, start, end, padding):
+ new_sample = AudioTableSample(start, end, self.header, self.data, self.book, self.loop, padding)
+ new_sample.notes_rates = self.notes_rates
+ return new_sample
+
+ def frame_size(self):
+ return {
+ AudioSampleCodec.CODEC_ADPCM : 9,
+ AudioSampleCodec.CODEC_S8 : 16,
+ AudioSampleCodec.CODEC_S16_INMEMORY : 32,
+ AudioSampleCodec.CODEC_SMALL_ADPCM : 5,
+ AudioSampleCodec.CODEC_REVERB : 0,
+ AudioSampleCodec.CODEC_S16 : 32
+ }[self.header.codec]
+
+ def codec_id(self):
+ return {
+ AudioSampleCodec.CODEC_ADPCM : b'ADP9',
+ AudioSampleCodec.CODEC_S8 : b'HPCM',
+ AudioSampleCodec.CODEC_S16_INMEMORY : b'NONE',
+ AudioSampleCodec.CODEC_SMALL_ADPCM : b'ADP5',
+ AudioSampleCodec.CODEC_REVERB : b'RVRB',
+ AudioSampleCodec.CODEC_S16 : b'NONE',
+ }[self.header.codec]
+
+ def codec_name(self):
+ return {
+ AudioSampleCodec.CODEC_ADPCM : b"Nintendo/SGI VADPCM 9-bytes/frame",
+ AudioSampleCodec.CODEC_S8 : b"Half-frame PCM",
+ AudioSampleCodec.CODEC_S16_INMEMORY : b"Uncompressed",
+ AudioSampleCodec.CODEC_SMALL_ADPCM : b"Nintendo/SGI VADPCM 5-bytes/frame",
+ AudioSampleCodec.CODEC_REVERB : b"Nintendo Reverb format",
+ AudioSampleCodec.CODEC_S16 : b"Uncompressed"
+ }[self.header.codec]
+
+ def codec_file_extension_compressed(self):
+ ext = {
+ AudioSampleCodec.CODEC_ADPCM : ".aifc",
+ AudioSampleCodec.CODEC_S8 : None,
+ AudioSampleCodec.CODEC_S16_INMEMORY : None,
+ AudioSampleCodec.CODEC_SMALL_ADPCM : ".half.aifc",
+ AudioSampleCodec.CODEC_REVERB : None,
+ AudioSampleCodec.CODEC_S16 : ".aiff",
+ }[self.header.codec]
+ assert ext is not None
+ return ext
+
+ def codec_file_extension_decompressed(self):
+ ext = {
+ AudioSampleCodec.CODEC_ADPCM : ".wav",
+ AudioSampleCodec.CODEC_S8 : None,
+ AudioSampleCodec.CODEC_S16_INMEMORY : None,
+ AudioSampleCodec.CODEC_SMALL_ADPCM : ".half.wav",
+ AudioSampleCodec.CODEC_REVERB : None,
+ AudioSampleCodec.CODEC_S16 : ".wav",
+ }[self.header.codec]
+ assert ext is not None
+ return ext
+
+ def base_note_number(self):
+ return note_z64_to_midi(pitch_names.index(self.base_note))
+
+ def resolve_basenote_rate(self, extraction_sample_info : Dict[int, Dict[str,str]]):
+ assert len(self.notes_rates) != 0
+
+ # rate_3ds = None
+ # if SAMPLERATES_3DS is not None:
+ # rate_3ds = SAMPLERATES_3DS[self.bank_num].get(i, None)
+
+ tuning_map = {}
+ def update_tuning_map(tuning, rate, note):
+ tuning_map.update({ tuning : (rate, note) })
+
+ # check
+ tuning_bits = f32_to_u32(tuning)
+ ntuning = recalc_tuning(rate, note)
+ assert ntuning == tuning or tuning_bits in BAD_FLOATS, \
+ f"Got: {ntuning}(0x{f32_to_u32(ntuning):X}), Expected: {tuning}(0x{f32_to_u32(tuning):X})"
+
+ if len(self.notes_rates) == 1:
+ # only need to match one tuning value
+
+ notes_rates,tuning = self.notes_rates.pop()
+
+ # if rate_3ds is not None and rate_3ds not in [rate for _,rate in notes_rates]:
+ # print(f"NONMATCHING: 3DS={rate_3ds} N64={[rate for _,rate in notes_rates]}")
+
+ if len(notes_rates) == 1:
+ # only one possible combination of samplerate and basenote
+ final_note,final_rate = notes_rates[0]
+ else:
+ # Several possible combinations of samplerate and basenote that result in the same tuning value,
+ # choose just one by arbitrary ranking
+ final_rate,(final_note,) = rank_rates_notes(tuple((rate, (note,)) for note,rate in notes_rates))
+
+ update_tuning_map(tuning, final_rate, final_note)
+ else:
+ # need to match for multiple tuning values
+
+ # produce a list of samplerates that are common to all entries, the correct samplerate is most likely in
+ # this intersection
+ rate_cands = set.intersection(*(set(rate for note,rate in nrs) for nrs,t in self.notes_rates))
+
+ # if rate_3ds is not None and rate_3ds not in rate_cands:
+ # print(f"NONMATCHING: 3DS={rate_3ds} N64={rate_cands}")
+
+ if len(rate_cands) == 0:
+ # no common samplerates, arbitrarily rank each separately to get best candidate for each tuning, then
+ # rank those again to find the one we should associate with the sample itself
+
+ finalists = []
+ for all_layout,tuning in self.notes_rates:
+ best_rate,(best_note,) = rank_rates_notes([(rate, (note,)) for note, rate in all_layout])
+
+ update_tuning_map(tuning, best_rate, best_note)
+
+ finalists.append((best_rate,(best_note,)))
+
+ final_rate,(final_note,) = rank_rates_notes(finalists)
+ else:
+ tunings = [t for nrs,t in self.notes_rates]
+ # Found one or more common samplerate, select just one by arbitrary ranking
+
+ # build a map from samplerate -> note value for each entry
+ dicts = tuple(dict((rate,note) for note,rate in nrs) for nrs,t in self.notes_rates)
+
+ # list of tuples (rate, (notes for each entry)) for each candidate samplerate
+ final_rate,final_notes = rank_rates_notes([(rate, tuple(D[rate] for D in dicts)) for rate in rate_cands])
+
+ finalists = []
+
+ # map the result of this stage to the tunings
+ for tuning,note in zip(tunings,final_notes):
+ update_tuning_map(tuning, final_rate, note)
+ finalists.append((final_rate,(note,)))
+
+ # select best note to go in the sample
+ final_rate,(final_note,) = rank_rates_notes(finalists)
+
+ if extraction_sample_info is not None:
+ if self.start in extraction_sample_info:
+ entry = extraction_sample_info[self.start]
+ if "SampleRate" in entry and "BaseNote" in entry:
+ final_rate = int(entry["SampleRate"])
+ final_note = entry["BaseNote"]
+ else:
+ print(f"WARNING: Missing extraction xml entry for sample at offset=0x{self.start:X}")
+
+ # print(" ",len(FINAL_NOTES_RATES), FINAL_NOTES_RATES)
+ # if rate_3ds is not None and len(FINAL_NOTES_RATES) == 1:
+ # print(f"3DS : {rate_3ds} N64 : {FINAL_NOTES_RATES[0][0]}")
+ # if rate_3ds != FINAL_NOTES_RATES[0][0]:
+ # print("NONMATCHING AFTER RANKING")
+ # else:
+ # print("No 3DS comparison")
+
+ self.notes_rates = None
+ self.sample_rate = final_rate
+ self.base_note = final_note
+ self.tuning_map = tuning_map
+
+ def to_file(self, outpath : str):
+ assert self.sample_rate is not None and self.base_note is not None,\
+ f"The sample must have been assigned a samplerate and basenote to be extracted to AIFC: [0x{self.start:X}:0x{self.end:X}]\n{self.header}"
+
+ NUM_CHANNELS = 1
+
+ # Note this computes the correct number of frames, The original sdk tool vadpcm_enc contained a bug where aifc
+ # files would sometimes be 1-off in the reported number of frames. We do not reproduce this.
+ num_frames = (len(self.data) // self.frame_size()) * PCM16_SAMPLE_SIZE
+
+ aifc = AIFCFile()
+
+ aifc.add_section(b"COMM",
+ struct.pack(">hIh", NUM_CHANNELS, num_frames, PCM16_SAMPLE_SIZE)
+ + AIFCFile.serialize_f80(self.sample_rate)
+ + self.codec_id()
+ + AIFCFile.pstring(self.codec_name())
+ )
+
+ aifc.add_section(b"INST",
+ struct.pack(">bbbbbbhhhhhhh",
+ self.base_note_number(),
+ 0, # detune
+ # TODO fill in the rest? with what?
+ 0, # lownote
+ 0, # highnote
+ 0, # lowvel
+ 0, # highvel
+ 0, # gain
+ 0,0,0, # sustain(mode,start,end)
+ 0,0,0, # release(mode,start,end)
+ )
+ )
+
+ aifc.add_custom_section(b"VADPCMCODES", self.book.serialize())
+ if self.loop.count != 0:
+ # We don't need to write a VADPCMLOOPS chunk if the count is 0 as we can represent these by the absence of
+ # a VADPCMLOOPS chunk; a count of 0 indicates the sample has no loop, the start and end of a loop with
+ # count=0 are always 0 and the end of the sample respectively.
+ aifc.add_custom_section(b"VADPCMLOOPS", self.loop.serialize())
+
+ aifc.add_section(b"SSND", struct.pack(">II", 0, 0) + bytes(self.data))
+
+ aifc.commit(outpath)
+
+ def to_asm(self, name):
+ out = f"# {name} [0x{self.start:X}:0x{self.end:X}](0x{self.end-self.start:X})\n"
+ out += "\n"
+ out += f".global {name}\n"
+ out += f"{name}:\n"
+ out += f".global {name}_OFF\n"
+ out += f".set {name}_OFF, . - $start\n"
+ out += "\n"
+ out += " .byte "
+ for i,b in enumerate(self.data):
+ if i != 0 and i % 32 == 0:
+ out = out[:-2] + "\n .byte "
+ out += f"0x{b:02X}, "
+ out = out[:-2] + "\n"
+ if len(self.padding) == 0 or all(b == 0 for b in self.padding):
+ out += " .balign 16\n"
+ else:
+ out += f"# PADDING\n"
+ out += " .byte " + ", ".join(f"0x{b:02X}" for b in self.padding) + "\n"
+ out += "\n"
+ return out
+
+
+
+
+
+
+
+class AudioTableFile:
+ """
+ Single sample bank in the Audiotable
+ """
+
+ def __init__(self, bank_num : int, audiotable_seg : memoryview, table_entry : AudioCodeTableEntry,
+ seg_offset : int, buffer_bug : bool = False, extraction_xml : Tuple[str, Element] = None):
+ self.bank_num = bank_num
+ self.table_entry : AudioCodeTableEntry = table_entry
+ self.data = self.table_entry.data(audiotable_seg, seg_offset)
+ self.buffer_bug = buffer_bug
+
+ self.samples_final = None
+
+ if extraction_xml is None:
+ self.file_name = f"SampleBank_{self.bank_num}"
+ self.name = f"SampleBank_{self.bank_num}"
+ self.extraction_sample_info = None
+ self.extraction_blob_info = None
+ else:
+ self.file_name = extraction_xml[0]
+ self.name = extraction_xml[1].attrib["Name"]
+
+ self.extraction_sample_info = {}
+ self.extraction_blob_info = {}
+ for item in extraction_xml[1]:
+ if item.tag == "Sample":
+ self.extraction_sample_info[int(item.attrib["Offset"], 16)] = item.attrib
+ elif item.tag == "Blob":
+ self.extraction_blob_info[int(item.attrib["Offset"], 16)] = item.attrib
+ else:
+ assert False
+
+ self.pointer_indices = []
+
+ self.samples = {}
+ self.coverage = set()
+
+ def register_ptr(self, index):
+ self.pointer_indices.append(index)
+
+ def dump_bin(self, path):
+ with open(path, "wb") as outfile:
+ outfile.write(self.data)
+
+ def __len__(self):
+ return len(self.data)
+
+ def add_sample(self, sample_header : SoundFontSample, book : AdpcmBook, loop : AdpcmLoop, tuning : float, ob):
+ # collect sample data
+ sample_start = sample_header.sample_addr
+ sample_end = sample_header.sample_addr + sample_header.size
+ sample_end_aligned = align(sample_end, 16)
+ sample_data = self.data[sample_start:sample_end]
+ sample_padding = self.data[sample_end:sample_end_aligned]
+ notes_rates = rate_from_tuning(tuning)
+
+ # update coverage
+ self.coverage.add((sample_start, sample_end_aligned, sample_end))
+
+ if sample_start in self.samples:
+ # if this sample start was already recorded, compare with previous
+ prev_sample : AudioTableSample = self.samples[sample_start]
+
+ # check data integrity, these should not change if the same is the same
+ assert prev_sample.end == sample_end
+ assert prev_sample.header.codec == sample_header.codec
+ assert prev_sample.book == book
+ assert prev_sample.loop == loop
+
+ # add notes/rates candidates
+ prev_sample.notes_rates.add((notes_rates, tuning))
+ else:
+ # if this sample start was not recorded, add it
+ new_sample = AudioTableSample(sample_start, sample_end, sample_header, sample_data, book, loop, sample_padding)
+ new_sample.notes_rates.add((notes_rates, tuning))
+ self.samples[sample_start] = new_sample
+
+ def lookup_sample(self, offset : int) -> AudioTableSample:
+ return self.samples[offset]
+
+ def sample_name(self, sample : AudioTableSample, index : int):
+ if self.extraction_sample_info is not None:
+ if sample.start in self.extraction_sample_info:
+ return self.extraction_sample_info[sample.start]["Name"]
+ print(f"WARNING: Missing extraction xml entry for sample at offset=0x{sample.start:X}")
+ return f"SAMPLE_{self.bank_num}_{index}"
+
+ def sample_filename(self, sample : AudioTableSample, index : int):
+ ext = sample.codec_file_extension_compressed()
+
+ if self.extraction_sample_info is not None:
+ if sample.start in self.extraction_sample_info:
+ return self.extraction_sample_info[sample.start]["FileName"] + ext
+ print(f"WARNING: Missing extraction xml entry for sample at offset=0x{sample.start:X}")
+ return f"Sample{index}{ext}"
+
+ def blob_filename(self, start, end):
+ if self.extraction_blob_info is not None:
+ if start in self.extraction_blob_info:
+ return self.extraction_blob_info[start]["Name"]
+ print(f"WARNING: Missing extraction xml entry for blob at offset=0x{start:X}")
+ return f"UNACCOUNTED_{start:X}_{end:X}"
+
+ def finalize_samples(self):
+ self.samples_final = list(sorted(self.samples.values(), key = lambda sample : sample.start))
+
+ for i,sample in enumerate(self.samples_final):
+ sample : AudioTableSample
+ sample.resolve_basenote_rate(self.extraction_sample_info)
+
+ def finalize_coverage(self, all_sample_banks):
+ if len(self.coverage) != 0:
+ # merge ranges if there are any
+ self.coverage = list(sorted(self.coverage))
+
+ merged = [list(self.coverage.pop(0))]
+
+ while len(self.coverage) != 0:
+ next = self.coverage.pop(0)
+ if merged[-1][1] == next[0]:
+ merged[-1][1] = next[1]
+ merged[-1][2] = next[2]
+ else:
+ merged.append(list(next))
+
+ self.coverage = merged
+
+ # check fully covered
+ if len(self.coverage) == 1 and self.coverage[0][0] == 0 and self.coverage[0][1] == len(self.data):
+ return # all accounted
+
+ # not fully covered, determine ranges of unaccounted data
+ if len(self.coverage) == 0:
+ # absolutely nothing is accounted for
+ unaccounted_ranges = [(0, len(self))]
+ else:
+ unaccounted_ranges = []
+ # deal with gap at the start
+ if self.coverage[0][0] != 0:
+ unaccounted_ranges.append((0, self.coverage[0][0]))
+ # deal with gaps in the middle
+ for j,cvg in enumerate(self.coverage[:-1]):
+ start = cvg[1]
+ end = self.coverage[j + 1][0]
+ if start != end:
+ unaccounted_ranges.append((start, end))
+ # deal with gap at the end
+ if self.coverage[-1][1] != len(self):
+ unaccounted_ranges.append((self.coverage[-1][1], len(self)))
+
+ # TODO if an unaccounted range is in the extraction xml, trust it before searching other banks
+
+ unaccounted_str = "[" + ", ".join(f"(0x{start:06X}, 0x{end:06X})" for start,end in unaccounted_ranges) + "]"
+ print(f"Sample Bank {self.bank_num} has incomplete coverage. Unaccounted: {unaccounted_str}")
+
+ # search other banks for matches
+ for start,end in unaccounted_ranges:
+ while start != end:
+ found = False
+
+ for j,bank in enumerate(all_sample_banks):
+ if not isinstance(bank, AudioTableFile):
+ # Ignore pointer entries
+ continue
+
+ for sample in bank.samples_final:
+ sample : AudioTableSample
+
+ sample_end = start + len(sample)
+ sample_end_aligned = align(sample_end, 16)
+
+ if self.data[start:sample_end] == sample.data:
+ print(f" Located match for range [0x{start:X}:0x{sample_end:X}] in bank {j} at 0x{sample.start:X}")
+ new_sample = sample.clone(start, sample_end, self.data[sample_end:sample_end_aligned])
+ new_sample.start = start
+ new_sample.end = sample_end
+ new_sample.sample_rate = sample.sample_rate
+ new_sample.base_note = sample.base_note
+ self.samples_final.append(new_sample)
+ found = True
+ start = sample_end_aligned
+ break
+ if found:
+ break
+ else:
+ # found no matches, blob it
+ print(f" No match found in other banks for range [0x{start:X}:0x{end:X}], leaving as binary blob")
+ self.samples_final.append(AudioTableData(start, end, self.data[start:end]))
+ break
+
+ # Final sort
+ self.samples_final.sort(key = lambda sample : sample.start)
+
+ def assign_names(self):
+ i = 0
+ for sample in self.samples_final:
+ if isinstance(sample, AudioTableSample):
+ sample : AudioTableSample
+
+ sample.name = self.sample_name(sample, i)
+ sample.filename = self.sample_filename(sample, i)
+ i += 1
+ else:
+ sample : AudioTableData
+
+ name = self.blob_filename(sample.start, sample.end)
+ sample.name = name
+ sample.filename = f"{name}.bin"
+
+ def to_xml(self, base_path):
+ xml = XMLWriter()
+
+ start = {
+ "Name" : self.name,
+ "Index" : self.bank_num,
+ "Medium" : self.table_entry.medium.name,
+ "CachePolicy" : self.table_entry.cache_policy.name,
+ }
+ if self.buffer_bug:
+ start["BufferBug"] = "true"
+
+ xml.write_start_tag("SampleBank", start)
+
+ # write pointers
+ for index in self.pointer_indices:
+ xml.write_element("Pointer", { "Index" : index })
+
+ # write samples/blobs
+ for sample in self.samples_final:
+ if isinstance(sample, AudioTableSample):
+ sample : AudioTableSample
+
+ xml.write_element("Sample", {
+ "Name" : sample.name,
+ "Path" : f"$(BUILD_DIR)/{base_path}/{sample.filename}",
+ })
+ else:
+ sample : AudioTableData
+
+ xml.write_element("Blob", {
+ "Name" : sample.name,
+ "Path" : f"$(BUILD_DIR)/{base_path}/{sample.filename}",
+ })
+
+ xml.write_end_tag()
+
+ return str(xml)
+
+ def write_extraction_xml(self, path):
+ xml = XMLWriter()
+
+ xml.write_comment("This file is only for extraction of vanilla data. For other purposes see assets/audio/samplebanks/")
+
+ start = {
+ "Name" : self.name,
+ "Index" : self.bank_num,
+ }
+ xml.write_start_tag("SampleBank", start)
+
+ i = 0
+ for sample in self.samples_final:
+ if isinstance(sample, AudioTableSample):
+ sample : AudioTableSample
+
+ xml.write_element("Sample", {
+ "Name" : sample.name,
+ "FileName" : sample.filename.replace(sample.codec_file_extension_compressed(), ""),
+ "Offset" : f"0x{sample.start:06X}",
+ "SampleRate" : sample.sample_rate,
+ "BaseNote" : sample.base_note,
+ })
+ i += 1
+ else:
+ sample : AudioTableData
+
+ xml.write_element("Blob", {
+ "Name" : sample.name,
+ "Offset" : f"0x{sample.start:06X}",
+ "Size" : f"0x{sample.end - sample.start:X}",
+ })
+
+ xml.write_end_tag()
+
+ with open(path, "w") as outfile:
+ outfile.write(str(xml))
+
+ def write_s_file(self, name, path):
+ with open(path, "w") as outfile:
+ out = ".rdata\n"
+ out += "\n"
+ out += ".balign 16\n"
+ out += "\n"
+ out += f".global {name}\n"
+ out += f"{name}_Start:\n"
+ out += "$start:\n"
+ out += "\n"
+
+ outfile.write(out)
+
+ i = 0
+ for sample in self.samples:
+ if isinstance(sample, AudioTableSample):
+ sample : AudioTableSample
+ outfile.write(sample.to_asm(self.sample_name(i)))
+ i += 1
+ else:
+ sample : AudioTableData
+ outfile.write(sample.to_asm("__UNACCOUNTED__"))
diff --git a/tools/audio/extraction/envelope.py b/tools/audio/extraction/envelope.py
new file mode 100644
index 000000000..0dab8c8d9
--- /dev/null
+++ b/tools/audio/extraction/envelope.py
@@ -0,0 +1,119 @@
+# SPDX-FileCopyrightText: © 2024 ZeldaRET
+# SPDX-License-Identifier: CC0-1.0
+#
+# Implements envelopes and envelope point structures
+#
+
+import collections
+
+from .util import XMLWriter
+
+class EnvDelay(int):
+ def __str__(self):
+ return {
+ 0 : "ADSR_DISABLE",
+ -1 : "ADSR_HANG",
+ -2 : "ADSR_GOTO",
+ -3 : "ADSR_RESTART",
+ }.get(self, super().__str__())
+
+class Envelope:
+ """
+ Array of envelope points
+ """
+
+ class EnvelopePoint:
+ """
+ typedef struct {
+ /* 0x0 */ s16 delay;
+ /* 0x2 */ s16 arg;
+ } EnvelopePoint; // size = 0x4
+ """
+
+ def __init__(self, delay, arg):
+ self.delay = EnvDelay(delay)
+ self.arg = arg
+
+ def __repr__(self):
+ return str(self)
+
+ def __str__(self):
+ return f"{{ {self.delay}, {self.arg} }}"
+
+ def is_disable(self):
+ return self.delay == 0 and self.arg == 0
+
+ def is_hang(self):
+ return self.delay == -1 and self.arg == 0
+
+ def to_xml(self, xml : XMLWriter):
+ if self.delay == 0: # Disable
+ assert self.arg == 0
+ xml.write_element("Disable")
+ elif self.delay == -1: # Hang
+ assert self.arg == 0
+ xml.write_element("Hang")
+ elif self.delay == -2: # Goto
+ xml.write_element("Goto",
+ { "Arg" : self.arg }
+ )
+ elif self.delay == -3: # Restart
+ assert self.arg == 0
+ xml.write_element("Restart")
+ else:
+ assert self.delay >= 0
+ xml.write_element("Point",
+ {
+ "Delay" : self.delay,
+ "Arg" : self.arg,
+ }
+ )
+
+ def __init__(self, points, is_zero=False):
+ self.name = None # Assigned when bank is finalized
+
+ self.is_zero = is_zero
+ self.release_rates = []
+ self._release_rate = None # cached
+
+ assert len(points) != 0
+ assert type(points[0]) == Envelope.EnvelopePoint
+ self.points = points
+
+ if not self.is_zero:
+ while self.points[-1].is_disable():
+ self.points.pop()
+
+ assert self.points[-1].is_hang()
+
+ def __str__(self):
+ out = "{\n"
+ out += " " + ", ".join([str(point) for point in self.points]) + "\n"
+ out += "}\n"
+ return out
+
+ def release_rate(self):
+ if self._release_rate is not None:
+ return self._release_rate
+
+ rates = collections.Counter(self.release_rates).most_common()
+ assert len(rates) in [0, 1], rates # TODO handle ties?
+
+ self._release_rate = 0 if len(rates) == 0 else rates[0][0]
+ return self._release_rate
+
+ def to_xml(self, xml : XMLWriter, name : str):
+ if self.is_zero:
+ return xml.write_element("Envelope")
+
+ xml.write_start_tag("Envelope",
+ {
+ "Name" : name,
+ "Release" : self.release_rate(),
+ }
+ )
+
+ for point in self.points[:-1]: # exclude final hang command, will be added by the soundfont compiler on build
+ point.to_xml(xml)
+
+ xml.write_end_tag()
diff --git a/tools/audio/extraction/tuning.py b/tools/audio/extraction/tuning.py
new file mode 100644
index 000000000..b538c68e8
--- /dev/null
+++ b/tools/audio/extraction/tuning.py
@@ -0,0 +1,211 @@
+#!/usr/bin/env python3
+# SPDX-FileCopyrightText: © 2024 ZeldaRET
+# SPDX-License-Identifier: CC0-1.0
+#
+# Estimate (samplerate, basenote) from tuning
+#
+# tuning = samplerate * 2 ** basenote
+#
+
+from typing import List, Tuple
+
+from .util import f32, u32_to_f32, f32_to_u32
+
+# Mirrors gPitchFrequencies in audio driver source.
+# Indexed by z64 note numbers, g_pitch_frequencies[C4] = 1.0 (0x3F800000)
+# Converted to their IEEE-754 binary representation to avoid any string -> float parser trouble as we need exact values.
+g_pitch_frequencies = (
+ 0x3DD744F6, 0x3DE411C3, 0x3DF1A198, 0x3E000000, 0x3E079C84, 0x3E0FACE6, 0x3E1837F8, 0x3E21450F,
+ 0x3E2ADC0A, 0x3E350508, 0x3E3FC86D, 0x3E4B2FEC, 0x3E5744F6, 0x3E641206, 0x3E71A1DC, 0x3E800000,
+ 0x3E879C84, 0x3E8FACE6, 0x3E9837F8, 0x3EA1450F, 0x3EAADC0A, 0x3EB504E6, 0x3EBFC88E, 0x3ECB2FEC,
+ 0x3ED744F6, 0x3EE411E4, 0x3EF1A1BA, 0x3F000000, 0x3F079C84, 0x3F0FACD6, 0x3F1837F8, 0x3F214520,
+ 0x3F2ADC0A, 0x3F3504F7, 0x3F3FC88E, 0x3F4B2FFD, 0x3F574507, 0x3F6411F5, 0x3F71A1CB, 0x3F800000,
+ 0x3F879C7C, 0x3F8FACD6, 0x3F9837EF, 0x3FA14517, 0x3FAADC0A, 0x3FB504F7, 0x3FBFC886, 0x3FCB2FF5,
+ 0x3FD744FE, 0x3FE411F5, 0x3FF1A1C2, 0x40000000, 0x40079C7C, 0x400FACD6, 0x401837EF, 0x40214517,
+ 0x402ADC0A, 0x403504F7, 0x403FC88A, 0x404B2FF9, 0x405744FE, 0x406411F5, 0x4071A1C2, 0x40800000,
+ 0x40879C7E, 0x408FACD8, 0x409837F1, 0x40A14519, 0x40AADC0A, 0x40B504F5, 0x40BFC888, 0x40CB2FF9,
+ 0x40D74500, 0x40E411F5, 0x40F1A1C2, 0x41000000, 0x41079C7D, 0x410FACD7, 0x411837F1, 0x41214519,
+ 0x412ADC0A, 0x413504F5, 0x413FC889, 0x414B2FF8, 0x41574500, 0x416411F4, 0x4171A1C3, 0x41800000,
+ 0x41879C7D, 0x418FACD7, 0x419837F1, 0x41A14519, 0x41AADC0A, 0x41B504F5, 0x41BFC889, 0x41CB2FF8,
+ 0x41D74500, 0x41E411F4, 0x41F1A1C3, 0x42000000, 0x42079C7D, 0x420FACD7, 0x421837F1, 0x42214519,
+ 0x422ADC0A, 0x423504F5, 0x423FC889, 0x424B2FF8, 0x42574500, 0x426411F4, 0x4271A1C3, 0x42800000,
+ 0x42879C7D, 0x428FACD7, 0x429837F1, 0x42A14519, 0x42AADC0A, 0x3D6411C3, 0x3D71A198, 0x3D800000,
+ 0x3D879C41, 0x3D8FACE6, 0x3D9837B5, 0x3DA1450F, 0x3DAADBC6, 0x3DB504C5, 0x3DBFC86D, 0x3DCB302F,
+)
+
+# Names for pitch values indexed by z64 note numbers, pitch_names[39] = C4
+pitch_names = (
+ "A0", "BF0", "B0",
+ "C1", "DF1", "D1", "EF1", "E1", "F1", "GF1", "G1", "AF1", "A1", "BF1", "B1",
+ "C2", "DF2", "D2", "EF2", "E2", "F2", "GF2", "G2", "AF2", "A2", "BF2", "B2",
+ "C3", "DF3", "D3", "EF3", "E3", "F3", "GF3", "G3", "AF3", "A3", "BF3", "B3",
+ "C4", "DF4", "D4", "EF4", "E4", "F4", "GF4", "G4", "AF4", "A4", "BF4", "B4",
+ "C5", "DF5", "D5", "EF5", "E5", "F5", "GF5", "G5", "AF5", "A5", "BF5", "B5",
+ "C6", "DF6", "D6", "EF6", "E6", "F6", "GF6", "G6", "AF6", "A6", "BF6", "B6",
+ "C7", "DF7", "D7", "EF7", "E7", "F7", "GF7", "G7", "AF7", "A7", "BF7", "B7",
+ "C8", "DF8", "D8", "EF8", "E8", "F8", "GF8", "G8", "AF8", "A8", "BF8", "B8",
+ "C9", "DF9", "D9", "EF9", "E9", "F9", "GF9", "G9", "AF9", "A9", "BF9", "B9",
+ "C10", "DF10", "D10", "EF10", "E10", "F10",
+ "BFNEG1", "BNEG1",
+ "C0", "DF0", "D0", "EF0", "E0", "F0", "GF0", "G0", "AF0",
+)
+
+# Floats that are encountered in extraction but cannot be resolved to a match.
+BAD_FLOATS = [0x3E7319E3]
+
+def note_z64_to_midi(note : int) -> int:
+ """
+ Convert a z64 note number to MIDI note number.
+
+ Middle C is 39 in z64, while it is 60 in MIDI.
+ We want MIDI note numbers to store in the extracted sample files (aiff or wav)
+ """
+ return (21 + note) % 128
+
+def recalc_tuning(rate : int, note : str) -> float:
+ return f32(f32(rate / 32000.0) * u32_to_f32(g_pitch_frequencies[pitch_names.index(note)]))
+
+def rate_from_tuning(tuning : float) -> Tuple[Tuple[str,int]]:
+ """
+ Decompose a tuning value into a pair (samplerate, basenote) that round-trips when ran through `recalc_tuning`
+ """
+ matches : List[Tuple[str,int]] = []
+ diffs : List[Tuple[int, Tuple[str,int]]] = []
+
+ tuning_bits : int = f32_to_u32(tuning)
+
+ def test_value(note_val : int, nominal_rate : int, freq : float):
+ if nominal_rate > 48000:
+ # reject samplerate if too high
+ return
+
+ # recalc tuning and compare to original
+
+ tuning2 : float = f32(f32(nominal_rate / 32000.0) * freq)
+
+ diff : int = abs(f32_to_u32(tuning2) - tuning_bits)
+
+ if diff == 0:
+ matches.append((pitch_names[note_val], nominal_rate))
+ else:
+ diffs.append((diff, (pitch_names[note_val], nominal_rate)))
+
+ # search gPitchFrequencies LUT one by one. We don't exit as soon as a match is found as in general this procedure
+ # only recovers the correct (rate,note) pair up to multiples of 2, to get the final value we want to select the
+ # "best" of these pairs by an essentially arbitrary ranking (cf `rank_rates_notes`)
+ for note_val,freq_bits in enumerate(g_pitch_frequencies):
+ freq : float = u32_to_f32(freq_bits)
+
+ # compute the "nominal" samplerate for a given basenote by R = 32000 * (t / f)
+ nominal_rate : int = int(f32(tuning / freq) * 32000.0)
+
+ # test nominal value and +/-1
+ test_value(note_val, nominal_rate, freq)
+ test_value(note_val, nominal_rate + 1, freq)
+ test_value(note_val, nominal_rate - 1, freq)
+
+ if len(matches) != 0:
+ return tuple(matches)
+
+ # no matches found... check if we expected this, otherwise flag it for special handling
+ assert tuning_bits in BAD_FLOATS , f"0x{tuning_bits:08X}"
+
+ # just take the closest match and hack it in the soundfont compiler
+ hack_rate = sorted(diffs, key=lambda e : e[0])[0]
+ return (hack_rate[1],)
+
+def rank_rates_notes(layouts):
+
+ def rank_rate_note(rate, notes):
+ """
+ Arbitrarily rank the input samplerate + note numbers, based on what is most likely.
+ """
+ rank = 0
+
+ if 'C4' in notes and rate > 10000:
+ rank += 10000
+ elif 'C2' in notes and rate > 10000:
+ rank += 9500
+ elif 'D3' in notes and rate > 10000:
+ rank += 8500
+ elif 'D4' in notes and rate > 10000:
+ rank += 8000
+ elif 'G3' in notes:
+ rank += 2000
+ elif 'F3' in notes:
+ rank += 25
+ elif 'C0' in notes:
+ rank += 50
+ elif 'BF2' in notes:
+ rank += 30
+ elif 'B3' in notes:
+ rank += 25
+ elif 'BF1' in notes:
+ rank += 25
+ elif 'E2' in notes:
+ rank += 20
+ elif 'F6' in notes:
+ rank += 15
+ elif 'GF2' in notes:
+ rank += 10
+
+ rank += {
+ 32000 : 200,
+ 16000 : 100,
+ 24000 : 50,
+ 22050 : 30,
+ 20000 : 28,
+ 44100 : 25,
+ 12000 : 15,
+ 8000 : 10,
+ 15950 : 5,
+ 20050 : 5,
+ 31800 : 5,
+ }.get(rate, 0)
+
+ return rank
+
+ # Input should not be empty
+ assert len(layouts) != 0
+
+ if len(layouts) == 1:
+ # No ranking needed, there is only one possible option
+ return layouts[0]
+
+ # Ranking is needed, rank each layout
+ ranked = list(sorted(layouts, key=lambda L : rank_rate_note(*L), reverse=True))
+
+ # Ensure the ranking produced a unique best option
+ assert rank_rate_note(*ranked[0]) != rank_rate_note(*ranked[1]) , ranked
+
+ # Output best
+ return ranked[0]
+
+if __name__ == '__main__':
+ import argparse
+
+ parser = argparse.ArgumentParser(description="Given either a (rate,note) or a tuning, compute all matching rates/notes.")
+ parser.add_argument("-t", dest="tuning", required=False, default=None, type=float, help="Tuning value (float)")
+ parser.add_argument("-r", dest="rate", required=False, default=None, type=int, help="Sample rate (integer)")
+ parser.add_argument("-n", dest="note", required=False, default=None, type=str, help="Base note (note name)")
+ parser.add_argument("--show-result", required=False, default=False, action="store_true", help="Show recalculated tuning value")
+ args = parser.parse_args()
+
+ if args.tuning is not None:
+ # Take input tuning
+ tuning = args.tuning
+ elif args.rate is not None and args.note is not None:
+ # Calculate target tuning from input rate and note
+ tuning : float = recalc_tuning(args.rate, args.note)
+ else:
+ # Insufficient arguments
+ parser.print_help()
+ raise SystemExit("Must specify either -t or both -r and -n.")
+
+ notes_rates : Tuple[Tuple[str,int]] = rate_from_tuning(tuning)
+
+ for note,rate in notes_rates:
+ if args.show_result:
+ print(rate, note, "->", recalc_tuning(rate, note))
+ else:
+ print(rate, note)
diff --git a/tools/audio/extraction/util.py b/tools/audio/extraction/util.py
new file mode 100644
index 000000000..20891810c
--- /dev/null
+++ b/tools/audio/extraction/util.py
@@ -0,0 +1,126 @@
+# SPDX-FileCopyrightText: © 2024 ZeldaRET
+# SPDX-License-Identifier: CC0-1.0
+#
+# Misc utilities
+#
+
+import struct, subprocess, sys
+
+def debugm(msg):
+ """
+ Debug message on stderr
+ """
+ print(msg, file=sys.stderr)
+
+def error(msg):
+ """
+ Debug message + exit
+ """
+ debugm(msg)
+ sys.exit(1)
+
+def incbin(rom, offset, size):
+ return rom[offset:offset+size]
+
+def f32(f):
+ """
+ Reduces precision of f to a 32-bit float for correct intermediate calculations
+ """
+ return struct.unpack("f", struct.pack("f", f))[0]
+
+def u32_to_f32(u):
+ """
+ Convert IEEE-754 binary rep to float
+ """
+ return struct.unpack(">f", struct.pack(">I", u))[0]
+
+def f32_to_u32(f):
+ """
+ Convert float to IEEE-754 binary rep
+ """
+ return struct.unpack(">I", struct.pack(">f", f))[0]
+
+def align(x, n):
+ """
+ Align to next n (power of 2)
+ """
+ return (x + (n - 1)) & ~(n - 1)
+
+def merge_ranges(intervals):
+ if len(intervals) == 0:
+ return []
+
+ intervals = sorted(intervals, key=lambda x: x[0][0])
+
+ stack = [intervals[0]]
+ for i in range(1, len(intervals)):
+ last_element = stack[len(stack) - 1]
+ if last_element[1][0] >= intervals[i][0][0]:
+ last_element[1] = max(intervals[i][1], last_element[1], key=lambda x: x[0])
+ stack.pop(len(stack) - 1)
+ stack.append(last_element)
+ else:
+ stack.append(intervals[i])
+ return stack
+
+def merge_like_ranges(intervals):
+ if len(intervals) == 0:
+ return []
+
+ intervals = sorted(intervals, key=lambda x: x[0][0])
+
+ stack = [intervals[0]]
+ for i in range(1, len(intervals)):
+ last_element = stack[len(stack) - 1]
+ if last_element[1][0] >= intervals[i][0][0] and last_element[1][1] == intervals[i][1][1]:
+ last_element[1] = max(intervals[i][1], last_element[1], key=lambda x: x[0])
+ stack.pop(len(stack) - 1)
+ stack.append(last_element)
+ else:
+ stack.append(intervals[i])
+ return stack
+
+def list_is_in_order(l):
+ return all(l[i] <= l[i + 1] for i in range(len(l) - 1))
+
+def program_call(cmd):
+ subprocess.check_call(cmd, shell=True)
+
+def program_get(cmd):
+ return subprocess.check_output(cmd, shell=True).decode("ascii")
+
+class XMLWriter:
+ """
+ Simple XML builder for writing with desired formatting characteristics (no tabs, 4 space indent)
+ """
+
+ def __init__(self):
+ self.contents = ""
+ self.tag_stack = []
+
+ def __str__(self):
+ return self.contents
+
+ def write_line(self, name, open, close, attributes):
+ indent = " " * len(self.tag_stack)
+ if attributes is None:
+ self.contents += f"{indent}{open}{name}{close}\n"
+ else:
+ attributes_str = " ".join(f"{k}=\"{v}\"" for k,v in attributes.items())
+ self.contents += f"{indent}{open}{name} {attributes_str}{close}\n"
+
+ def write_comment(self, comment):
+ self.write_line(comment, "<!-- ", " -->", None)
+
+ def write_start_tag(self, name, attributes=None):
+ self.write_line(name, "<", ">", attributes)
+ self.tag_stack.append(name)
+
+ def write_end_tag(self):
+ self.write_line(self.tag_stack.pop(), "</", ">", None)
+
+ def write_element(self, name, attributes=None):
+ self.write_line(name, "<", "/>", attributes)
+
+ def write_raw(self, contents):
+ self.write_line(contents, "", "", None)
diff --git a/tools/audio_extraction.py b/tools/audio_extraction.py
new file mode 100644
index 000000000..da30273b7
--- /dev/null
+++ b/tools/audio_extraction.py
@@ -0,0 +1,166 @@
+#!/usr/bin/env python3
+# SPDX-FileCopyrightText: © 2024 ZeldaRET
+# SPDX-License-Identifier: CC0-1.0
+#
+# Configures and runs baserom audio extraction
+#
+
+import argparse
+
+import version_config
+
+from audio.extraction.audio_extract import extract_audio_for_version, GameVersionInfo, MMLVersion
+
+if __name__ == '__main__':
+ parser = argparse.ArgumentParser(description="baserom audio asset extractor")
+ parser.add_argument("-o", "--extracted-dir", required=True, help="path to extracted directory")
+ parser.add_argument("-v", "--version", required=True, help="version name")
+ parser.add_argument("--read-xml", required=False, action="store_true", help="Read extraction xml files")
+ parser.add_argument("--write-xml", required=False, action="store_true", help="Write extraction xml files")
+ args = parser.parse_args()
+
+ version = args.version
+
+ config = version_config.load_version_config(version)
+
+ code_vram = config.dmadata_segments["code"].vram
+ soundfont_table_code_offset = config.variables["gSoundFontTable"] - code_vram
+ seq_font_table_code_offset = config.variables["gSequenceFontTable"] - code_vram
+ seq_table_code_offset = config.variables["gSequenceTable"] - code_vram
+ sample_bank_table_code_offset = config.variables["gSampleBankTable"] - code_vram
+
+ # List any sequences that are "handwritten", we don't extract these by
+ # default as we want these checked in for documentation.
+ handwritten_sequences = (0, 1, 2, 109)
+
+ # Sequence enum names for extraction purposes.
+ seq_enum_names = (
+ "NA_BGM_GENERAL_SFX",
+ "NA_BGM_NATURE_AMBIENCE",
+ "NA_BGM_FIELD_LOGIC",
+ "NA_BGM_FIELD_INIT",
+ "NA_BGM_FIELD_DEFAULT_1",
+ "NA_BGM_FIELD_DEFAULT_2",
+ "NA_BGM_FIELD_DEFAULT_3",
+ "NA_BGM_FIELD_DEFAULT_4",
+ "NA_BGM_FIELD_DEFAULT_5",
+ "NA_BGM_FIELD_DEFAULT_6",
+ "NA_BGM_FIELD_DEFAULT_7",
+ "NA_BGM_FIELD_DEFAULT_8",
+ "NA_BGM_FIELD_DEFAULT_9",
+ "NA_BGM_FIELD_DEFAULT_A",
+ "NA_BGM_FIELD_DEFAULT_B",
+ "NA_BGM_FIELD_ENEMY_INIT",
+ "NA_BGM_FIELD_ENEMY_1",
+ "NA_BGM_FIELD_ENEMY_2",
+ "NA_BGM_FIELD_ENEMY_3",
+ "NA_BGM_FIELD_ENEMY_4",
+ "NA_BGM_FIELD_STILL_1",
+ "NA_BGM_FIELD_STILL_2",
+ "NA_BGM_FIELD_STILL_3",
+ "NA_BGM_FIELD_STILL_4",
+ "NA_BGM_DUNGEON",
+ "NA_BGM_KAKARIKO_ADULT",
+ "NA_BGM_ENEMY",
+ "NA_BGM_BOSS",
+ "NA_BGM_INSIDE_DEKU_TREE",
+ "NA_BGM_MARKET",
+ "NA_BGM_TITLE",
+ "NA_BGM_LINK_HOUSE",
+ "NA_BGM_GAME_OVER",
+ "NA_BGM_BOSS_CLEAR",
+ "NA_BGM_ITEM_GET",
+ "NA_BGM_OPENING_GANON",
+ "NA_BGM_HEART_GET",
+ "NA_BGM_OCA_LIGHT",
+ "NA_BGM_JABU_JABU",
+ "NA_BGM_KAKARIKO_KID",
+ "NA_BGM_GREAT_FAIRY",
+ "NA_BGM_ZELDA_THEME",
+ "NA_BGM_FIRE_TEMPLE",
+ "NA_BGM_OPEN_TRE_BOX",
+ "NA_BGM_FOREST_TEMPLE",
+ "NA_BGM_COURTYARD",
+ "NA_BGM_GANON_TOWER",
+ "NA_BGM_LONLON",
+ "NA_BGM_GORON_CITY",
+ "NA_BGM_FIELD_MORNING",
+ "NA_BGM_SPIRITUAL_STONE",
+ "NA_BGM_OCA_BOLERO",
+ "NA_BGM_OCA_MINUET",
+ "NA_BGM_OCA_SERENADE",
+ "NA_BGM_OCA_REQUIEM",
+ "NA_BGM_OCA_NOCTURNE",
+ "NA_BGM_MINI_BOSS",
+ "NA_BGM_SMALL_ITEM_GET",
+ "NA_BGM_TEMPLE_OF_TIME",
+ "NA_BGM_EVENT_CLEAR",
+ "NA_BGM_KOKIRI",
+ "NA_BGM_OCA_FAIRY_GET",
+ "NA_BGM_SARIA_THEME",
+ "NA_BGM_SPIRIT_TEMPLE",
+ "NA_BGM_HORSE",
+ "NA_BGM_HORSE_GOAL",
+ "NA_BGM_INGO",
+ "NA_BGM_MEDALLION_GET",
+ "NA_BGM_OCA_SARIA",
+ "NA_BGM_OCA_EPONA",
+ "NA_BGM_OCA_ZELDA",
+ "NA_BGM_OCA_SUNS",
+ "NA_BGM_OCA_TIME",
+ "NA_BGM_OCA_STORM",
+ "NA_BGM_NAVI_OPENING",
+ "NA_BGM_DEKU_TREE_CS",
+ "NA_BGM_WINDMILL",
+ "NA_BGM_HYRULE_CS",
+ "NA_BGM_MINI_GAME",
+ "NA_BGM_SHEIK",
+ "NA_BGM_ZORA_DOMAIN",
+ "NA_BGM_APPEAR",
+ "NA_BGM_ADULT_LINK",
+ "NA_BGM_MASTER_SWORD",
+ "NA_BGM_INTRO_GANON",
+ "NA_BGM_SHOP",
+ "NA_BGM_CHAMBER_OF_SAGES",
+ "NA_BGM_FILE_SELECT",
+ "NA_BGM_ICE_CAVERN",
+ "NA_BGM_DOOR_OF_TIME",
+ "NA_BGM_OWL",
+ "NA_BGM_SHADOW_TEMPLE",
+ "NA_BGM_WATER_TEMPLE",
+ "NA_BGM_BRIDGE_TO_GANONS",
+ "NA_BGM_OCARINA_OF_TIME",
+ "NA_BGM_GERUDO_VALLEY",
+ "NA_BGM_POTION_SHOP",
+ "NA_BGM_KOTAKE_KOUME",
+ "NA_BGM_ESCAPE",
+ "NA_BGM_UNDERGROUND",
+ "NA_BGM_GANONDORF_BOSS",
+ "NA_BGM_GANON_BOSS",
+ "NA_BGM_END_DEMO",
+ "NA_BGM_STAFF_1",
+ "NA_BGM_STAFF_2",
+ "NA_BGM_STAFF_3",
+ "NA_BGM_STAFF_4",
+ "NA_BGM_FIRE_BOSS",
+ "NA_BGM_TIMED_MINI_GAME",
+ "NA_BGM_CUTSCENE_EFFECTS",
+ )
+
+ # Some bugged soundfonts report the wrong samplebank. Map them to the correct samplebank for proper sample discovery.
+ fake_banks = { 37 : 2 }
+
+ # Some audiotable banks have a buffer clearing bug. Indicate which banks suffer from this.
+ audiotable_buffer_bugs = (0,)
+
+ version_info = GameVersionInfo(MMLVersion.OOT,
+ soundfont_table_code_offset,
+ seq_font_table_code_offset,
+ seq_table_code_offset,
+ sample_bank_table_code_offset,
+ seq_enum_names,
+ handwritten_sequences,
+ fake_banks,
+ audiotable_buffer_bugs)
+
+ extract_audio_for_version(version_info, args.extracted_dir, args.read_xml, args.write_xml)