Skip to content

Commit 8b7ffaf

Browse files
Nyx-EdelsteinBerserker66Zach Parks
authored
ALTTP: Add "oof" sound customization option (#709)
Co-authored-by: Fabian Dill <[email protected]> Co-authored-by: Fabian Dill <[email protected]> Co-authored-by: Zach Parks <[email protected]>
1 parent c711d80 commit 8b7ffaf

File tree

4 files changed

+173
-7
lines changed

4 files changed

+173
-7
lines changed

LttPAdjuster.py

+86-3
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,12 @@ def main():
107107
Alternatively, can be a ALttP Rom patched with a Link
108108
sprite that will be extracted.
109109
''')
110+
parser.add_argument('--oof', help='''\
111+
Path to a sound effect to replace Link's "oof" sound.
112+
Needs to be in a .brr format and have a length of no
113+
more than 2673 bytes, created from a 16-bit signed PCM
114+
.wav at 12khz. https://github.com/boldowa/snesbrr
115+
''')
110116
parser.add_argument('--names', default='', type=str)
111117
parser.add_argument('--update_sprites', action='store_true', help='Update Sprite Database, then exit.')
112118
args = parser.parse_args()
@@ -126,6 +132,13 @@ def main():
126132
if args.sprite is not None and not os.path.isfile(args.sprite) and not Sprite.get_sprite_from_name(args.sprite):
127133
input('Could not find link sprite sheet at given location. \nPress Enter to exit.')
128134
sys.exit(1)
135+
if args.oof is not None and not os.path.isfile(args.oof):
136+
input('Could not find oof sound effect at given location. \nPress Enter to exit.')
137+
sys.exit(1)
138+
if args.oof is not None and os.path.getsize(args.oof) > 2673:
139+
input('"oof" sound effect cannot exceed 2673 bytes. \nPress Enter to exit.')
140+
sys.exit(1)
141+
129142

130143
args, path = adjust(args=args)
131144
if isinstance(args.sprite, Sprite):
@@ -165,7 +178,7 @@ def adjust(args):
165178
world = getattr(args, "world")
166179

167180
apply_rom_settings(rom, args.heartbeep, args.heartcolor, args.quickswap, args.menuspeed, args.music,
168-
args.sprite, palettes_options, reduceflashing=args.reduceflashing or racerom, world=world,
181+
args.sprite, args.oof, palettes_options, reduceflashing=args.reduceflashing or racerom, world=world,
169182
deathlink=args.deathlink, allowcollect=args.allowcollect)
170183
path = output_path(f'{os.path.basename(args.rom)[:-4]}_adjusted.sfc')
171184
rom.write_to_file(path)
@@ -227,6 +240,7 @@ def adjustRom():
227240
guiargs.sprite = rom_vars.sprite
228241
if rom_vars.sprite_pool:
229242
guiargs.world = AdjusterWorld(rom_vars.sprite_pool)
243+
guiargs.oof = rom_vars.oof
230244

231245
try:
232246
guiargs, path = adjust(args=guiargs)
@@ -265,6 +279,7 @@ def saveGUISettings():
265279
else:
266280
guiargs.sprite = rom_vars.sprite
267281
guiargs.sprite_pool = rom_vars.sprite_pool
282+
guiargs.oof = rom_vars.oof
268283
persistent_store("adjuster", GAME_ALTTP, guiargs)
269284
messagebox.showinfo(title="Success", message="Settings saved to persistent storage")
270285

@@ -481,6 +496,36 @@ def close_window(self):
481496
self.stop()
482497

483498

499+
class AttachTooltip(object):
500+
501+
def __init__(self, parent, text):
502+
self._parent = parent
503+
self._text = text
504+
self._window = None
505+
parent.bind('<Enter>', lambda event : self.show())
506+
parent.bind('<Leave>', lambda event : self.hide())
507+
508+
def show(self):
509+
if self._window or not self._text:
510+
return
511+
self._window = Toplevel(self._parent)
512+
#remove window bar controls
513+
self._window.wm_overrideredirect(1)
514+
#adjust positioning
515+
x, y, *_ = self._parent.bbox("insert")
516+
x = x + self._parent.winfo_rootx() + 20
517+
y = y + self._parent.winfo_rooty() + 20
518+
self._window.wm_geometry("+{0}+{1}".format(x,y))
519+
#show text
520+
label = Label(self._window, text=self._text, justify=LEFT)
521+
label.pack(ipadx=1)
522+
523+
def hide(self):
524+
if self._window:
525+
self._window.destroy()
526+
self._window = None
527+
528+
484529
def get_rom_frame(parent=None):
485530
adjuster_settings = get_adjuster_settings(GAME_ALTTP)
486531
if not adjuster_settings:
@@ -522,6 +567,7 @@ def get_rom_options_frame(parent=None):
522567
"reduceflashing": True,
523568
"deathlink": False,
524569
"sprite": None,
570+
"oof": None,
525571
"quickswap": True,
526572
"menuspeed": 'normal',
527573
"heartcolor": 'red',
@@ -598,12 +644,50 @@ def SpriteSelect():
598644
spriteEntry.pack(side=LEFT)
599645
spriteSelectButton.pack(side=LEFT)
600646

647+
oofDialogFrame = Frame(romOptionsFrame)
648+
oofDialogFrame.grid(row=1, column=1)
649+
baseOofLabel = Label(oofDialogFrame, text='"OOF" Sound:')
650+
651+
vars.oofNameVar = StringVar()
652+
vars.oof = adjuster_settings.oof
653+
654+
def set_oof(oof_param):
655+
nonlocal vars
656+
if isinstance(oof_param, str) and os.path.isfile(oof_param) and os.path.getsize(oof_param) <= 2673:
657+
vars.oof = oof_param
658+
vars.oofNameVar.set(oof_param.rsplit('/',1)[-1])
659+
else:
660+
vars.oof = None
661+
vars.oofNameVar.set('(unchanged)')
662+
663+
set_oof(adjuster_settings.oof)
664+
oofEntry = Label(oofDialogFrame, textvariable=vars.oofNameVar)
665+
666+
def OofSelect():
667+
nonlocal vars
668+
oof_file = filedialog.askopenfilename(
669+
filetypes=[("BRR files", ".brr"),
670+
("All Files", "*")])
671+
try:
672+
set_oof(oof_file)
673+
except Exception:
674+
set_oof(None)
675+
676+
oofSelectButton = Button(oofDialogFrame, text='...', command=OofSelect)
677+
AttachTooltip(oofSelectButton,
678+
text="Select a .brr file no more than 2673 bytes.\n" + \
679+
"This can be created from a <=0.394s 16-bit signed PCM .wav file at 12khz using snesbrr.")
680+
681+
baseOofLabel.pack(side=LEFT)
682+
oofEntry.pack(side=LEFT)
683+
oofSelectButton.pack(side=LEFT)
684+
601685
vars.quickSwapVar = IntVar(value=adjuster_settings.quickswap)
602686
quickSwapCheckbutton = Checkbutton(romOptionsFrame, text="L/R Quickswapping", variable=vars.quickSwapVar)
603687
quickSwapCheckbutton.grid(row=1, column=0, sticky=E)
604688

605689
menuspeedFrame = Frame(romOptionsFrame)
606-
menuspeedFrame.grid(row=1, column=1, sticky=E)
690+
menuspeedFrame.grid(row=6, column=1, sticky=E)
607691
menuspeedLabel = Label(menuspeedFrame, text='Menu speed')
608692
menuspeedLabel.pack(side=LEFT)
609693
vars.menuspeedVar = StringVar()
@@ -1056,7 +1140,6 @@ def alttpr_sprite_dir(self):
10561140
def custom_sprite_dir(self):
10571141
return user_path("data", "sprites", "custom")
10581142

1059-
10601143
def get_image_for_sprite(sprite, gif_only: bool = False):
10611144
if not sprite.valid:
10621145
return None

worlds/alttp/Rom.py

+56-3
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@ def check_enemizer(enemizercli):
189189
# some time may have passed since the lock was acquired, as such a quick re-check doesn't hurt
190190
if getattr(check_enemizer, "done", None):
191191
return
192-
wanted_version = (7, 0, 1)
192+
wanted_version = (7, 1, 0)
193193
# version info is saved on the lib, for some reason
194194
library_info = os.path.join(os.path.dirname(enemizercli), "EnemizerCLI.Core.deps.json")
195195
with open(library_info) as f:
@@ -1775,8 +1775,57 @@ def hud_format_text(text):
17751775
output += b'\x7f\x00'
17761776
return output[:32]
17771777

1778-
1779-
def apply_rom_settings(rom, beep, color, quickswap, menuspeed, music: bool, sprite: str, palettes_options,
1778+
def apply_oof_sfx(rom, oof: str):
1779+
with open(oof, 'rb') as stream:
1780+
oof_bytes = bytearray(stream.read())
1781+
1782+
oof_len_bytes = len(oof_bytes).to_bytes(2, byteorder='little')
1783+
1784+
# Credit to kan for this method, and Nyx for initial C# implementation
1785+
# this is ported from, with both of their permission for use by AP
1786+
# Original C# implementation:
1787+
# https://github.com/Nyx-Edelstein/The-Unachievable-Ideal-of-Chibi-Elf-Grunting-Noises-When-They-Get-Punched-A-Z3-Rom-Patcher
1788+
1789+
# Jump execution from the SPC load routine to new code
1790+
rom.write_bytes(0x8CF, [0x5C, 0x00, 0x80, 0x25])
1791+
1792+
# Change the pointer for instrument 9 in SPC memory to point to the new data we'll be inserting:
1793+
rom.write_bytes(0x1A006C, [0x88, 0x31, 0x00, 0x00])
1794+
1795+
# Insert a sigil so we can branch on it later
1796+
# We will recover the value it overwrites after we're done with insertion
1797+
rom.write_bytes(0x1AD38C, [0xBE, 0xBE])
1798+
1799+
# Change the "oof" sound effect to use instrument 9:
1800+
rom.write_byte(0x1A9C4E, 0x09)
1801+
1802+
# Correct the pitch shift value:
1803+
rom.write_byte(0x1A9C51, 0xB6)
1804+
1805+
# Modify parameters of instrument 9
1806+
# (I don't actually understand this part, they're just magic values to me)
1807+
rom.write_bytes(0x1A9CAE, [0x7F, 0x7F, 0x00, 0x10, 0x1A, 0x00, 0x00, 0x7F, 0x01])
1808+
1809+
# Hook from SPC load routine:
1810+
# * Check for the read of the sigil
1811+
# * Once we find it, change the SPC load routine's data pointer to read from the location containing the new sample
1812+
# * Note: XXXX in the string below is a placeholder for the number of bytes in the .brr sample (little endian)
1813+
# * Another sigil "$EBEB" is inserted at the end of the data
1814+
# * When the second sigil is read, we know we're done inserting our data so we can change the data pointer back
1815+
# * Effect: The new data gets loaded into SPC memory without having to relocate the SPC load routine
1816+
# 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
1817+
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])
1818+
1819+
# The new sample data
1820+
# (We need to insert the second sigil at the end)
1821+
rom.write_bytes(0x12803A, oof_bytes)
1822+
rom.write_bytes(0x12803A + len(oof_bytes), [0xEB, 0xEB])
1823+
1824+
#Enemizer patch: prevent Enemizer from overwriting $3188 in SPC memory with an unused sound effect ("WHAT")
1825+
rom.write_bytes(0x13000D, [0x00, 0x00, 0x00, 0x08])
1826+
1827+
1828+
def apply_rom_settings(rom, beep, color, quickswap, menuspeed, music: bool, sprite: str, oof: str, palettes_options,
17801829
world=None, player=1, allow_random_on_event=False, reduceflashing=False,
17811830
triforcehud: str = None, deathlink: bool = False, allowcollect: bool = False):
17821831
local_random = random if not world else world.per_slot_randoms[player]
@@ -1918,6 +1967,10 @@ def next_color_generator():
19181967

19191968
apply_random_sprite_on_event(rom, sprite, local_random, allow_random_on_event,
19201969
world.sprite_pool[player] if world else [])
1970+
1971+
if oof is not None:
1972+
apply_oof_sfx(rom, oof)
1973+
19211974
if isinstance(rom, LocalRom):
19221975
rom.write_crc()
19231976

worlds/alttp/__init__.py

+11-1
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,16 @@ class ALTTPWeb(WebWorld):
103103
["Berserker"]
104104
)
105105

106-
tutorials = [setup_en, setup_de, setup_es, setup_fr, msu, msu_es, msu_fr, plando]
106+
oof_sound = Tutorial(
107+
"'OOF' Sound Replacement",
108+
"A guide to customizing Link's 'oof' sound",
109+
"English",
110+
"oof_sound_en.md",
111+
"oof_sound/en",
112+
["Nyx Edelstein"]
113+
)
114+
115+
tutorials = [setup_en, setup_de, setup_es, setup_fr, msu, msu_es, msu_fr, plando, oof_sound]
107116

108117

109118
class ALTTPWorld(World):
@@ -485,6 +494,7 @@ def generate_output(self, output_directory: str):
485494
world.menuspeed[player].current_key,
486495
world.music[player],
487496
world.sprite[player],
497+
None,
488498
palettes_options, world, player, True,
489499
reduceflashing=world.reduceflashing[player] or world.is_race,
490500
triforcehud=world.triforcehud[player].current_key,

worlds/alttp/docs/oof_sound_en.md

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# "OOF" sound customization guide
2+
3+
## What does this feature do?
4+
5+
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.
6+
7+
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.
8+
9+
Some examples of what is possible: https://www.youtube.com/watch?v=TYs322kHlc0
10+
11+
## How do I create my own custom sound?
12+
13+
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.
14+
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.
15+
3. When running the adjuster GUI, simply select the .brr file you wish to use after clicking the `"OOF" Sound` menu option.
16+
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.
17+
18+
## Can I use multiple sounds for composite sprites?
19+
20+
No, this is not technically feasible. You can only use one sound.

0 commit comments

Comments
 (0)