Skip to content

Commit

Permalink
New custom voice pack support + ML64 pak support
Browse files Browse the repository at this point in the history
  • Loading branch information
rrealmuto committed Dec 9, 2024
1 parent b499369 commit 7c058cc
Show file tree
Hide file tree
Showing 760 changed files with 7,390 additions and 96 deletions.
2 changes: 1 addition & 1 deletion ASM/src/config.asm
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
COSMETIC_CONTEXT:

COSMETIC_FORMAT_VERSION:
.word 0x1F073FE2
.word 0x1F073FE3
CFG_MAGIC_COLOR:
.halfword 0x0000, 0x00C8, 0x0000
CFG_HEART_COLOR:
Expand Down
68 changes: 63 additions & 5 deletions Audiobank.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from __future__ import annotations
from io import FileIO
from Rom import Rom
import struct

# Container for storing Audiotable, Audiobank, Audiotable_index, Audiobank_index
class Audiobin:
Expand Down Expand Up @@ -33,21 +33,60 @@ def find_sample_in_audiobanks(self, sample_data: bytearray):
if instrument.normalNoteSample and instrument.normalNoteSample.data == sample_data:
return instrument.normalNoteSample
for sfx in audiobank.SFX:
if sfx and sfx.sample:
if sfx and sfx.sample and sfx.sample.data:
if sfx.sample.data == sample_data:
return sfx.sample
return None

class AdpcmLoop:
def __init__(self, bankdata: bytearray, loop_addr: int):
self.start = int.from_bytes(bankdata[loop_addr:loop_addr+4], 'big')
self.end = int.from_bytes(bankdata[loop_addr+4:loop_addr+8], 'big')
self.count = int.from_bytes(bankdata[loop_addr+8:loop_addr+12], 'big')
self.origSpls = int.from_bytes(bankdata[loop_addr+12:loop_addr+16], 'big')
self.state = []
if self.count:
for i in range(0,16):
index = loop_addr + 0x10 + 2*i
self.state.append(int.from_bytes(bankdata[index:index+2],'big'))

def get_bytes(self):
bytes = bytearray(0)
bytes += self.start.to_bytes(4,'big')
bytes += self.end.to_bytes(4, 'big')
bytes += self.count.to_bytes(4, 'big')
bytes += self.origSpls.to_bytes(4, 'big')
for short in self.state:
bytes += short.to_bytes(2, 'big')
return bytes

class AdpcmBook:
def __init__(self, bankdata: bytearray, book_addr: int):
self.order = int.from_bytes(bankdata[book_addr:book_addr+4], 'big')
self.npredictors = int.from_bytes(bankdata[book_addr+4:book_addr+8], 'big')
self.book = []
for i in range(0, 8 * self.order * self.npredictors):
index = book_addr + 8 + 2*i
self.book.append(int.from_bytes(bankdata[index:index+2], 'big'))

class Sample:
def __init__(self, bankdata: bytearray, audiotable_file: bytearray, audiotable_index: bytearray, sample_offset: int, audiotable_id: int, parent):
# Process the sample
if sample_offset == 0:
self.data = None
return
self.parent = parent
self.bank_offset = sample_offset
self.sample_header = bankdata[sample_offset:sample_offset + 0x10]
self.codec = (self.sample_header[0] & 0xF0) >> 4
self.medium = (self.sample_header[0] & 0x0C) >> 2
self.size = int.from_bytes(self.sample_header[1:4], 'big')
self.addr = int.from_bytes(self.sample_header[4:8], 'big')
if(sample_offset != 0):
self.loop_addr = int.from_bytes(self.sample_header[8:12], 'big')
self.book_addr = int.from_bytes(self.sample_header[12:16], 'big')
self.loop = AdpcmLoop(bankdata, self.loop_addr)
self.book = AdpcmBook(bankdata, self.book_addr)

if audiotable_file and self.addr > len(audiotable_file): # The offset is higher than the total size of audiotable so we'll assume it doesn't actually exist. # We'll need to get the sample data from ZSOUND files in the archive.
self.data = None
Expand All @@ -65,6 +104,15 @@ def __init__(self, bankdata: bytearray, audiotable_file: bytearray, audiotable_i
else:
self.audiotable_addr = -1
self.data = None

def get_bytes(self) -> bytearray:
bytes = bytearray()
bytes += (((self.codec & 0x0F) << 4) | ((self.medium & 0x03) << 2)).to_bytes(1, 'big')
bytes += self.size.to_bytes(3, 'big')
bytes += self.addr.to_bytes(4, 'big')
bytes += self.loop_addr.to_bytes(4, 'big')
bytes += self.book_addr.to_bytes(4, 'big')
return bytes

# Loads an audiobank and it's corresponding instrument/drum/sfxs
class AudioBank:
Expand Down Expand Up @@ -143,8 +191,12 @@ def get_all_samples(self) -> list[Sample]:
all_samples.append(sfx.sample)
return all_samples

def build_entry(self, offset: int) -> bytes:
bank_entry: bytes = offset.to_bytes(4, 'big')
def build_entry(self, offset: int = 0) -> bytes:
bank_entry: bytearray = bytearray(0)
if offset == 0:
bank_entry += self.bank_offset.to_bytes(4, 'big')
else:
bank_entry += offset.to_bytes(4, 'big')
bank_entry += len(self.bank_data).to_bytes(4, 'big')
bank_entry += self.table_entry[8:16]
return bank_entry
Expand All @@ -162,10 +214,16 @@ def __init__(self, drum_id: int, bankdata: bytearray, audiotable_file: bytearray
class SFX:
def __init__(self, sfx_id: int, bankdata: bytearray, audiotable_file: bytearray, audiotable_index: bytearray, sfx_offset: int, audiotable_id: int) -> None:
self.sfx_id = sfx_id
self.sfx_offset = sfx_offset
self.sampleOffset = int.from_bytes(bankdata[sfx_offset:sfx_offset+4], 'big')
self.sampleTuning = int.from_bytes(bankdata[sfx_offset+4:sfx_offset+8], 'big')
self.sample: Sample = Sample(bankdata, audiotable_file, audiotable_index, self.sampleOffset, audiotable_id, self)


def get_bytes(self):
bytes = bytearray(0)
bytes += self.sampleOffset.to_bytes(4,'big')
bytes += struct.pack(">f", self.sampleTuning)
return bytes

class Instrument:
def __init__(self, inst_id: int, bankdata: bytearray, audiotable_file: bytearray, audiotable_index: bytearray, instr_offset: int, audiotable_id: int) -> None:
Expand Down
Binary file added Book1.xlsx
Binary file not shown.
45 changes: 38 additions & 7 deletions Cosmetics.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from collections.abc import Iterable, Callable
from itertools import chain
from typing import TYPE_CHECKING, Optional, Any
from Audiobank import SFX

import Colors
import IconManip
Expand All @@ -15,6 +16,7 @@
from Plandomizer import InvalidFileException
from Utils import data_path
from version import __version__
from Voices import VOICE_PACK_AGE, _patch_voice_pack, child_link_sfx, adult_link_sfx

if TYPE_CHECKING:
from Rom import Rom
Expand Down Expand Up @@ -851,7 +853,7 @@ def read_default_voice_data(rom: Rom) -> dict[str, dict[str, int]]:
return soundbank_entries


def patch_silent_voice(rom: Rom, sfxidlist: Iterable[int], soundbank_entries: dict[str, dict[str, int]], log: CosmeticsLog) -> None:
def patch_silent_voice(rom: Rom, age: VOICE_PACK_AGE, log: CosmeticsLog) -> None:
binsfxfilename = os.path.join(data_path('Voices'), 'SilentVoiceSFX.bin')
if not os.path.isfile(binsfxfilename):
log.errors.append(f"Could not find silent voice sfx at {binsfxfilename}. Skipping voice patching")
Expand All @@ -861,12 +863,14 @@ def patch_silent_voice(rom: Rom, sfxidlist: Iterable[int], soundbank_entries: di
with open(binsfxfilename, 'rb') as binsfxin:
binsfx = bytearray() + binsfxin.read(-1)

sfxlist = adult_link_sfx if age == VOICE_PACK_AGE.ADULT else child_link_sfx

# Pad it to length and patch it into every id in sfxidlist
for decid in sfxidlist:
sfxid = f"00-00{decid:02x}"
injectme = binsfx.ljust(soundbank_entries[sfxid]["length"], b'\0')
for _, sfxid in sfxlist:
sfx: SFX = rom.audiobanks[0].SFX[sfxid]
injectme = binsfx.ljust(sfx.sample.size)
# Write the binary sfx to the rom
rom.write_bytes(soundbank_entries[sfxid]["romoffset"], injectme)
rom.audiotable[sfx.sample.audiotable_addr:sfx.sample.audiotable_addr + len(injectme)] = injectme


def apply_voice_patch(rom: Rom, voice_path: str, soundbank_entries: dict[str, dict[str, int]]) -> None:
Expand Down Expand Up @@ -933,6 +937,16 @@ def patch_voices(rom: Rom, settings: Settings, log: CosmeticsLog, symbols: dict[
# Write the setting to the log
log.sfx[log_key] = voice_setting

def patch_voice_pack(rom: Rom, settings: Settings, log: CosmeticsLog, symbols: dict[str, int]):
if settings.sfx_link_adult == 'Silent':
patch_silent_voice(rom, VOICE_PACK_AGE.ADULT, log)
elif settings.sfx_link_adult != 'Default':
_patch_voice_pack(rom, VOICE_PACK_AGE.ADULT, settings.sfx_link_adult)

if settings.sfx_link_child == 'Silent':
patch_silent_voice(rom, VOICE_PACK_AGE.ADULT, log)
elif settings.sfx_link_child != 'Default':
_patch_voice_pack(rom, VOICE_PACK_AGE.CHILD, settings.sfx_link_child)

def patch_music_changes(rom: Rom, settings: Settings, log: CosmeticsLog, symbols: dict[str, int]) -> None:
# Music tempo changes
Expand Down Expand Up @@ -1014,7 +1028,6 @@ def patch_song_names(rom: Rom, settings: Settings, log: CosmeticsLog, symbols: d
patch_sword_trails,
patch_gauntlet_colors,
patch_shield_frame_colors,
patch_voices,
patch_sfx,
patch_instrument,
]
Expand Down Expand Up @@ -1218,6 +1231,16 @@ def patch_song_names(rom: Rom, settings: Settings, log: CosmeticsLog, symbols: d
}
}

# 8.2.22
patch_sets[0x1F073FE3] = {
"patches": patch_sets[0x1F073FE0]["patches"] + [
patch_voice_pack,
],
"symbols": {
**patch_sets[0x1F073FE2]["symbols"]
}
}

def patch_cosmetics(settings: Settings, rom: Rom) -> CosmeticsLog:
# re-seed for aesthetic effects. They shouldn't be affected by the generation seed
random.seed()
Expand Down Expand Up @@ -1270,6 +1293,13 @@ def patch_cosmetics(settings: Settings, rom: Rom) -> CosmeticsLog:
# Unknown patch format
log.errors.append("Unable to patch some cosmetics. ROM uses unknown cosmetic patch format.")

audiotable_start = rom.write_audiotable()
log.symbols = cosmetic_context_symbols
log.symbols['audiotable_start'] = audiotable_start
if "CFG_AUDIOBANK_TABLE_EXTENDED_ADDR" in cosmetic_context_symbols.keys():
bank_index_base = (rom.read_int32(cosmetic_context_symbols['CFG_AUDIOBANK_TABLE_EXTENDED_ADDR']) - 0x80400000) + 0x3480000
rom.write_audiobanks(bank_index_base)

return log


Expand All @@ -1283,7 +1313,7 @@ def __init__(self, settings: Settings) -> None:
self.sfx: dict[str, str] = {}
self.bgm: dict[str, str] = {}
self.bgm_groups: dict[str, list | dict] = {}

self.symbols: dict[str, int] = {}
self.src_dict: dict = {}
self.errors: list[str] = []

Expand Down Expand Up @@ -1342,6 +1372,7 @@ def to_json(self) -> dict:
'sfx': self.sfx,
'bgm_groups': self.bgm_groups,
'bgm': self.bgm,
'symbols': self.symbols
}

if not self.settings.enable_cosmetic_file:
Expand Down
74 changes: 12 additions & 62 deletions Music.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,6 @@
from Settings import Settings

AUDIOSEQ_DMADATA_INDEX: int = 4
AUDIOBANK_DMADATA_INDEX: int = 3
AUDIOTABLE_DMADATA_INDEX: int = 5

# Format: (Title, Sequence ID)
bgm_sequence_ids: tuple[tuple[str, int], ...] = (
Expand Down Expand Up @@ -352,7 +350,8 @@ def rebuild_sequences(rom: Rom, sequences: list[Sequence], log: CosmeticsLog, sy

# If custom banks are supported, we're going to make copies of the banks to be used for fanfares
# In that case, need to update the fanfare sequence's bank to point to the new one
fanfare_bank_shift = 0x26 if CUSTOM_BANKS_SUPPORTED else 0
#fanfare_bank_shift = 0x26 if CUSTOM_BANKS_SUPPORTED else 0
fanfare_bank_shift = 0

# Update pointer table
for i in range(0x6E):
Expand Down Expand Up @@ -399,23 +398,18 @@ def rebuild_sequences(rom: Rom, sequences: list[Sequence], log: CosmeticsLog, sy
# Builds new audio bank entrys for fanfares to prevent fanfares killing bgm in areas like Goron City
bank_index_base = (rom.read_int32(symbols['CFG_AUDIOBANK_TABLE_EXTENDED_ADDR']) - 0x80400000) + 0x3480000
# Build new fanfare banks by copying each entry in audiobank_index
for i in range(0, 0x26):
bank_entry = rom.read_bytes(bank_index_base + 0x10 + 0x10 * i, 0x10) # Get the vanilla entry
bank_entry[9] = 1 # Update the cache type to 1
rom.write_bytes(bank_index_base + 0x270 + 0x10 * i, bank_entry) # Write the new entry at the end of the bank table.
rom.write_byte(bank_index_base + 0x01, 0x4C) # Updates AudioBank Index Header if no custom banks are present as this would be 0x26 which would crash the game if a fanfare was played
#for i in range(0, 0x26):
# bank_entry = rom.read_bytes(bank_index_base + 0x10 + (0x10*i), 0x10) # Get the vanilla entry
# bank_entry[9] = 1 # Update the cache type to 1
# rom.write_bytes(bank_index_base + 0x270 + 0x10*i, bank_entry) # Write the new entry at the end of the bank table.
#rom.write_byte(bank_index_base + 0x01, 0x4C) # Updates AudioBank Index Header if no custom banks are present as this would be 0x26 which would crash the game if a fanfare was played

added_banks: list[AudioBank] = [] # Store copies of all the banks we've added
added_samples: list[Sample] = [] # Store copies of all the samples we've added
new_bank_index = 0x4C
new_bank_index = 0x26
instr_data = bytearray(0) # Store all the new instrument data that will be added to the end of audiotable

audiobank_dma_entry = rom.dma[AUDIOBANK_DMADATA_INDEX]
audiotable_dma_entry = rom.dma[AUDIOTABLE_DMADATA_INDEX]
audiobank_start, audiobank_end, audiobank_size = audiobank_dma_entry.as_tuple()
audiotable_start, audiotable_end, audiotable_size = audiotable_dma_entry.as_tuple()

instr_offset_in_file = audiotable_size
instr_offset_in_file = len(rom.audiotable)
bank_table_base = 0

# Load OOT Audiobin
Expand Down Expand Up @@ -614,54 +608,10 @@ def rebuild_sequences(rom: Rom, sequences: list[Sequence], log: CosmeticsLog, sy
# Patch the new instrument data into the ROM in a new file.
# If there is any instrument data to add, move the entire audiotable file to a new location in the ROM.
if len(instr_data) > 0:
# Read the original audiotable data
audiotable_data = rom.read_bytes(audiotable_start, audiotable_size)
# Zeroize existing file
rom.write_bytes(audiotable_start, [0] * audiotable_size)
# Add the new data
audiotable_data += instr_data
# Get new address for the file
new_audiotable_start = rom.dma.free_space(len(audiotable_data))
# Write the file to the new address
rom.write_bytes(new_audiotable_start, audiotable_data)
# Update DMA
audiotable_dma_entry.update(new_audiotable_start, new_audiotable_start + len(audiotable_data))
log.instr_dma_index = audiotable_dma_entry.index

# Add new audio banks
new_bank_data = bytearray(0)
# Read the original audiobank data
audiobank_data = rom.read_bytes(audiobank_start, audiobank_size)
new_bank_offset = len(audiobank_data)
for bank in added_banks:
# Sample pointers should already be correct now
# bank.update_zsound_pointers()
bank.offset = new_bank_offset
#absolute_offset = new_audio_banks_addr + new_bank_offset
bank_entry = bank.build_entry(new_bank_offset)
rom.write_bytes(bank_table_base + 0x10 + bank.bank_index * 0x10, bank_entry)
for dupe_bank in bank.duplicate_banks:
bank_entry = bytearray(dupe_bank.build_entry(new_bank_offset))
bank_entry[4:8] = bank.size.to_bytes(4, 'big')
rom.write_bytes(bank_table_base + 0x10 + dupe_bank.bank_index * 0x10, bank_entry)
new_bank_data += bank.bank_data
new_bank_offset += len(bank.bank_data)

# If there is any audiobank data to add, move the entire audiobank file to a new place in ROM. Update the existing dmadata record
if len(new_bank_data) > 0:
# Zeroize existing file
rom.write_bytes(audiobank_start, [0] * audiobank_size)
# Add the new data
audiobank_data += new_bank_data
# Get new address for the file
new_audio_banks_addr = rom.dma.free_space(len(audiobank_data))
# Write the file to the new address
rom.write_bytes(new_audio_banks_addr, audiobank_data)
# Update DMA
audiobank_dma_entry.update(new_audio_banks_addr, new_audio_banks_addr + len(audiobank_data))
log.bank_dma_index = audiobank_dma_entry.index
# Update size of bank table in the Audiobank table header.
rom.write_bytes(bank_table_base, new_bank_index.to_bytes(2, 'big'))
rom.audiotable += instr_data

rom.audiobanks.extend(added_banks)

# Update the init heap size. This size is normally hardcoded based on the number of audio banks.
init_heap_size = rom.read_int32(0xB80118)
Expand Down
38 changes: 19 additions & 19 deletions OoTRandomizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,26 +38,26 @@ def start() -> None:
except VersionError as e:
logger.warning(str(e))

try:
if gui:
from Gui import gui_main
gui_main()
elif diff_rom:
diff_roms(settings, diff_rom)
elif settings.cosmetics_only:
cosmetic_patch(settings)
elif settings.patch_file != '':
from_patch_file(settings)
elif settings.count is not None and settings.count > 1:
orig_seed = settings.seed
for i in range(settings.count):
settings.update_seed(orig_seed + '-' + str(i))
main(settings)
else:
#try:
if gui:
from Gui import gui_main
gui_main()
elif diff_rom:
diff_roms(settings, diff_rom)
elif settings.cosmetics_only:
cosmetic_patch(settings)
elif settings.patch_file != '':
from_patch_file(settings)
elif settings.count is not None and settings.count > 1:
orig_seed = settings.seed
for i in range(settings.count):
settings.update_seed(orig_seed + '-' + str(i))
main(settings)
except Exception as ex:
logger.exception(ex)
sys.exit(1)
else:
main(settings)
#except Exception as ex:
# logger.exception(ex)
# sys.exit(1)


if __name__ == '__main__':
Expand Down
Loading

0 comments on commit 7c058cc

Please sign in to comment.