mirror of
https://github.com/zeldaret/oot.git
synced 2025-07-13 03:14:38 +00:00
[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)
This commit is contained in:
parent
0186524300
commit
29acf96db2
58 changed files with 4678 additions and 0 deletions
269
tools/audio/extraction/audio_extract.py
Normal file
269
tools/audio/extraction/audio_extract.py
Normal file
|
@ -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")
|
Loading…
Add table
Add a link
Reference in a new issue