Skip to content

Commit

Permalink
ALTTP: Add "oof" sound customization option (ArchipelagoMW#709)
Browse files Browse the repository at this point in the history
Co-authored-by: Fabian Dill <[email protected]>
Co-authored-by: Fabian Dill <[email protected]>
Co-authored-by: Zach Parks <[email protected]>
  • Loading branch information
4 people authored and FlySniper committed Nov 14, 2023
1 parent c10f17d commit c9ec779
Show file tree
Hide file tree
Showing 4 changed files with 173 additions and 7 deletions.
89 changes: 86 additions & 3 deletions LttPAdjuster.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,12 @@ def main():
Alternatively, can be a ALttP Rom patched with a Link
sprite that will be extracted.
''')
parser.add_argument('--oof', help='''\
Path to a sound effect to replace Link's "oof" sound.
Needs to be in a .brr format and have a length of no
more than 2673 bytes, created from a 16-bit signed PCM
.wav at 12khz. https://github.com/boldowa/snesbrr
''')
parser.add_argument('--names', default='', type=str)
parser.add_argument('--update_sprites', action='store_true', help='Update Sprite Database, then exit.')
args = parser.parse_args()
Expand All @@ -126,6 +132,13 @@ def main():
if args.sprite is not None and not os.path.isfile(args.sprite) and not Sprite.get_sprite_from_name(args.sprite):
input('Could not find link sprite sheet at given location. \nPress Enter to exit.')
sys.exit(1)
if args.oof is not None and not os.path.isfile(args.oof):
input('Could not find oof sound effect at given location. \nPress Enter to exit.')
sys.exit(1)
if args.oof is not None and os.path.getsize(args.oof) > 2673:
input('"oof" sound effect cannot exceed 2673 bytes. \nPress Enter to exit.')
sys.exit(1)


args, path = adjust(args=args)
if isinstance(args.sprite, Sprite):
Expand Down Expand Up @@ -165,7 +178,7 @@ def adjust(args):
world = getattr(args, "world")

apply_rom_settings(rom, args.heartbeep, args.heartcolor, args.quickswap, args.menuspeed, args.music,
args.sprite, palettes_options, reduceflashing=args.reduceflashing or racerom, world=world,
args.sprite, args.oof, palettes_options, reduceflashing=args.reduceflashing or racerom, world=world,
deathlink=args.deathlink, allowcollect=args.allowcollect)
path = output_path(f'{os.path.basename(args.rom)[:-4]}_adjusted.sfc')
rom.write_to_file(path)
Expand Down Expand Up @@ -227,6 +240,7 @@ def adjustRom():
guiargs.sprite = rom_vars.sprite
if rom_vars.sprite_pool:
guiargs.world = AdjusterWorld(rom_vars.sprite_pool)
guiargs.oof = rom_vars.oof

try:
guiargs, path = adjust(args=guiargs)
Expand Down Expand Up @@ -265,6 +279,7 @@ def saveGUISettings():
else:
guiargs.sprite = rom_vars.sprite
guiargs.sprite_pool = rom_vars.sprite_pool
guiargs.oof = rom_vars.oof
persistent_store("adjuster", GAME_ALTTP, guiargs)
messagebox.showinfo(title="Success", message="Settings saved to persistent storage")

Expand Down Expand Up @@ -481,6 +496,36 @@ def close_window(self):
self.stop()


class AttachTooltip(object):

def __init__(self, parent, text):
self._parent = parent
self._text = text
self._window = None
parent.bind('<Enter>', lambda event : self.show())
parent.bind('<Leave>', lambda event : self.hide())

def show(self):
if self._window or not self._text:
return
self._window = Toplevel(self._parent)
#remove window bar controls
self._window.wm_overrideredirect(1)
#adjust positioning
x, y, *_ = self._parent.bbox("insert")
x = x + self._parent.winfo_rootx() + 20
y = y + self._parent.winfo_rooty() + 20
self._window.wm_geometry("+{0}+{1}".format(x,y))
#show text
label = Label(self._window, text=self._text, justify=LEFT)
label.pack(ipadx=1)

def hide(self):
if self._window:
self._window.destroy()
self._window = None


def get_rom_frame(parent=None):
adjuster_settings = get_adjuster_settings(GAME_ALTTP)
if not adjuster_settings:
Expand Down Expand Up @@ -522,6 +567,7 @@ def get_rom_options_frame(parent=None):
"reduceflashing": True,
"deathlink": False,
"sprite": None,
"oof": None,
"quickswap": True,
"menuspeed": 'normal',
"heartcolor": 'red',
Expand Down Expand Up @@ -598,12 +644,50 @@ def SpriteSelect():
spriteEntry.pack(side=LEFT)
spriteSelectButton.pack(side=LEFT)

oofDialogFrame = Frame(romOptionsFrame)
oofDialogFrame.grid(row=1, column=1)
baseOofLabel = Label(oofDialogFrame, text='"OOF" Sound:')

vars.oofNameVar = StringVar()
vars.oof = adjuster_settings.oof

def set_oof(oof_param):
nonlocal vars
if isinstance(oof_param, str) and os.path.isfile(oof_param) and os.path.getsize(oof_param) <= 2673:
vars.oof = oof_param
vars.oofNameVar.set(oof_param.rsplit('/',1)[-1])
else:
vars.oof = None
vars.oofNameVar.set('(unchanged)')

set_oof(adjuster_settings.oof)
oofEntry = Label(oofDialogFrame, textvariable=vars.oofNameVar)

def OofSelect():
nonlocal vars
oof_file = filedialog.askopenfilename(
filetypes=[("BRR files", ".brr"),
("All Files", "*")])
try:
set_oof(oof_file)
except Exception:
set_oof(None)

oofSelectButton = Button(oofDialogFrame, text='...', command=OofSelect)
AttachTooltip(oofSelectButton,
text="Select a .brr file no more than 2673 bytes.\n" + \
"This can be created from a <=0.394s 16-bit signed PCM .wav file at 12khz using snesbrr.")

baseOofLabel.pack(side=LEFT)
oofEntry.pack(side=LEFT)
oofSelectButton.pack(side=LEFT)

vars.quickSwapVar = IntVar(value=adjuster_settings.quickswap)
quickSwapCheckbutton = Checkbutton(romOptionsFrame, text="L/R Quickswapping", variable=vars.quickSwapVar)
quickSwapCheckbutton.grid(row=1, column=0, sticky=E)

menuspeedFrame = Frame(romOptionsFrame)
menuspeedFrame.grid(row=1, column=1, sticky=E)
menuspeedFrame.grid(row=6, column=1, sticky=E)
menuspeedLabel = Label(menuspeedFrame, text='Menu speed')
menuspeedLabel.pack(side=LEFT)
vars.menuspeedVar = StringVar()
Expand Down Expand Up @@ -1056,7 +1140,6 @@ def alttpr_sprite_dir(self):
def custom_sprite_dir(self):
return user_path("data", "sprites", "custom")


def get_image_for_sprite(sprite, gif_only: bool = False):
if not sprite.valid:
return None
Expand Down
59 changes: 56 additions & 3 deletions worlds/alttp/Rom.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ def check_enemizer(enemizercli):
# some time may have passed since the lock was acquired, as such a quick re-check doesn't hurt
if getattr(check_enemizer, "done", None):
return
wanted_version = (7, 0, 1)
wanted_version = (7, 1, 0)
# version info is saved on the lib, for some reason
library_info = os.path.join(os.path.dirname(enemizercli), "EnemizerCLI.Core.deps.json")
with open(library_info) as f:
Expand Down Expand Up @@ -1775,8 +1775,57 @@ def hud_format_text(text):
output += b'\x7f\x00'
return output[:32]


def apply_rom_settings(rom, beep, color, quickswap, menuspeed, music: bool, sprite: str, palettes_options,
def apply_oof_sfx(rom, oof: str):
with open(oof, 'rb') as stream:
oof_bytes = bytearray(stream.read())

oof_len_bytes = len(oof_bytes).to_bytes(2, byteorder='little')

# Credit to kan for this method, and Nyx for initial C# implementation
# this is ported from, with both of their permission for use by AP
# Original C# implementation:
# https://github.com/Nyx-Edelstein/The-Unachievable-Ideal-of-Chibi-Elf-Grunting-Noises-When-They-Get-Punched-A-Z3-Rom-Patcher

# Jump execution from the SPC load routine to new code
rom.write_bytes(0x8CF, [0x5C, 0x00, 0x80, 0x25])

# Change the pointer for instrument 9 in SPC memory to point to the new data we'll be inserting:
rom.write_bytes(0x1A006C, [0x88, 0x31, 0x00, 0x00])

# Insert a sigil so we can branch on it later
# We will recover the value it overwrites after we're done with insertion
rom.write_bytes(0x1AD38C, [0xBE, 0xBE])

# Change the "oof" sound effect to use instrument 9:
rom.write_byte(0x1A9C4E, 0x09)

# Correct the pitch shift value:
rom.write_byte(0x1A9C51, 0xB6)

# Modify parameters of instrument 9
# (I don't actually understand this part, they're just magic values to me)
rom.write_bytes(0x1A9CAE, [0x7F, 0x7F, 0x00, 0x10, 0x1A, 0x00, 0x00, 0x7F, 0x01])

# Hook from SPC load routine:
# * Check for the read of the sigil
# * Once we find it, change the SPC load routine's data pointer to read from the location containing the new sample
# * Note: XXXX in the string below is a placeholder for the number of bytes in the .brr sample (little endian)
# * Another sigil "$EBEB" is inserted at the end of the data
# * When the second sigil is read, we know we're done inserting our data so we can change the data pointer back
# * Effect: The new data gets loaded into SPC memory without having to relocate the SPC load routine
# Slight variation from VT-compatible algorithm: We need to change the data pointer to $00 00 35 and load 538E into Y to pick back up where we left off
rom.write_bytes(0x128000, [0xB7, 0x00, 0xC8, 0xC8, 0xC9, 0xBE, 0xBE, 0xF0, 0x09, 0xC9, 0xEB, 0xEB, 0xF0, 0x1B, 0x5C, 0xD3, 0x88, 0x00, 0xA2, oof_len_bytes[0], oof_len_bytes[1], 0xA9, 0x80, 0x25, 0x85, 0x01, 0xA9, 0x3A, 0x80, 0x85, 0x00, 0xA0, 0x00, 0x00, 0xA9, 0x88, 0x31, 0x5C, 0xD8, 0x88, 0x00, 0xA9, 0x80, 0x35, 0x64, 0x00, 0x85, 0x01, 0xA2, 0x00, 0x00, 0xA0, 0x8E, 0x53, 0x5C, 0xD4, 0x88, 0x00])

# The new sample data
# (We need to insert the second sigil at the end)
rom.write_bytes(0x12803A, oof_bytes)
rom.write_bytes(0x12803A + len(oof_bytes), [0xEB, 0xEB])

#Enemizer patch: prevent Enemizer from overwriting $3188 in SPC memory with an unused sound effect ("WHAT")
rom.write_bytes(0x13000D, [0x00, 0x00, 0x00, 0x08])


def apply_rom_settings(rom, beep, color, quickswap, menuspeed, music: bool, sprite: str, oof: str, palettes_options,
world=None, player=1, allow_random_on_event=False, reduceflashing=False,
triforcehud: str = None, deathlink: bool = False, allowcollect: bool = False):
local_random = random if not world else world.per_slot_randoms[player]
Expand Down Expand Up @@ -1918,6 +1967,10 @@ def next_color_generator():

apply_random_sprite_on_event(rom, sprite, local_random, allow_random_on_event,
world.sprite_pool[player] if world else [])

if oof is not None:
apply_oof_sfx(rom, oof)

if isinstance(rom, LocalRom):
rom.write_crc()

Expand Down
12 changes: 11 additions & 1 deletion worlds/alttp/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,16 @@ class ALTTPWeb(WebWorld):
["Berserker"]
)

tutorials = [setup_en, setup_de, setup_es, setup_fr, msu, msu_es, msu_fr, plando]
oof_sound = Tutorial(
"'OOF' Sound Replacement",
"A guide to customizing Link's 'oof' sound",
"English",
"oof_sound_en.md",
"oof_sound/en",
["Nyx Edelstein"]
)

tutorials = [setup_en, setup_de, setup_es, setup_fr, msu, msu_es, msu_fr, plando, oof_sound]


class ALTTPWorld(World):
Expand Down Expand Up @@ -485,6 +494,7 @@ def generate_output(self, output_directory: str):
world.menuspeed[player].current_key,
world.music[player],
world.sprite[player],
None,
palettes_options, world, player, True,
reduceflashing=world.reduceflashing[player] or world.is_race,
triforcehud=world.triforcehud[player].current_key,
Expand Down
20 changes: 20 additions & 0 deletions worlds/alttp/docs/oof_sound_en.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# "OOF" sound customization guide

## What does this feature do?

It replaces the sound effect when Link takes damage. The intended use case for this is custom sprites, but you can use it with any sprite, including the default one.

Due to technical restrictions resulting from limited available memory, there is a limit to how long the sound can be. Using the current method, this limit is **0.394 seconds**. This means that many ideas won't work, and any intelligible speech or anything other than a grunt or simple noise will be too long.

Some examples of what is possible: https://www.youtube.com/watch?v=TYs322kHlc0

## How do I create my own custom sound?

1. Obtain a .wav file with the following specifications: 16-bit signed PCM at 12khz, no longer than 0.394 seconds. You can do this by editing an existing sample using a program like Audacity, or by recording your own. Note that samples can be shrinked or truncated to meet the length requirement, at the expense of sound quality.
2. Use the `--encode` function of the snesbrr tool (https://github.com/boldowa/snesbrr) to encode your .wav file in the proper format (.brr). The .brr file **cannot** exceed 2673 bytes. As long as the input file meets the above specifications, the .brr file should be this size or smaller. If your file is too large, go back to step 1 and make the sample shorter.
3. When running the adjuster GUI, simply select the .brr file you wish to use after clicking the `"OOF" Sound` menu option.
4. You can also do the patch via command line: `python .\LttPAdjuster.py --baserom .\baserom.sfc --oof .\oof.brr .\romtobeadjusted.sfc`, replacing the file names with your files.

## Can I use multiple sounds for composite sprites?

No, this is not technically feasible. You can only use one sound.

0 comments on commit c9ec779

Please sign in to comment.