Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ALTTP: Add "oof" sound customization option #709

Merged
merged 35 commits into from
Apr 11, 2023
Merged
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
7bd9f24
Adding CLI Oof Sounds from Krebel's branch
Nyx-Edelstein Jun 19, 2022
1ae878e
GUI code for selecting "OOF" sounds
Nyx-Edelstein Jun 19, 2022
2a44092
Merge branch 'ArchipelagoMW:main' into main
Nyx-Edelstein Jun 19, 2022
ec69a3f
Merge branch 'ArchipelagoMW:main' into main
Nyx-Edelstein Jun 25, 2022
3b03ee6
Update __init__.py
Nyx-Edelstein Jun 28, 2022
f03c97a
Update LttPAdjuster.py
Nyx-Edelstein Jun 29, 2022
1228fcb
Update Rom.py
Nyx-Edelstein Jun 29, 2022
379641e
Update LttPAdjuster.py
Nyx-Edelstein Jun 29, 2022
8150162
Update LttPAdjuster.py
Nyx-Edelstein Jun 29, 2022
40d8f19
Update LttPAdjuster.py
Nyx-Edelstein Jun 29, 2022
634455a
Create tutorial for replacing oof sounds
Nyx-Edelstein Jun 29, 2022
ee68c6c
Merge branch 'main' into main
Nyx-Edelstein Jun 29, 2022
64f050d
Update LttPAdjuster.py
Nyx-Edelstein Jun 29, 2022
312e7aa
Merge branch 'main' of https://github.com/Nyx-Edelstein/ArchipelagoMW…
Nyx-Edelstein Jun 29, 2022
d6da67d
Update LttPAdjuster.py
Nyx-Edelstein Jun 29, 2022
00ca456
Merge branch 'main' into main
Nyx-Edelstein Jun 30, 2022
5d33e82
Merge branch 'main' into main-
Nyx-Edelstein Jul 6, 2022
512c0d4
Update LttPAdjuster.py
Nyx-Edelstein Jul 6, 2022
d55b4f9
Update LttPAdjuster.py
Nyx-Edelstein Jul 6, 2022
418bffb
Merge branch 'main' into main
Nyx-Edelstein Jul 6, 2022
8570f68
Merge branch 'main' into main
Nyx-Edelstein Jul 7, 2022
3966fd0
Update Rom.py
Nyx-Edelstein Jul 11, 2022
d90cb2a
Merge branch 'main' into main
Nyx-Edelstein Jul 11, 2022
9e0eeec
Update Rom.py
Nyx-Edelstein Jul 15, 2022
e670ac7
Merge branch 'main' into main
Nyx-Edelstein Jul 15, 2022
5fe21e6
Merge branch 'main' into main
Berserker66 Sep 17, 2022
a32a965
Check for Enemizer version
Berserker66 Sep 17, 2022
882e5ef
Merge branch 'main' into main
Nyx-Edelstein Oct 5, 2022
a334e09
Update __init__.py
Nyx-Edelstein Oct 5, 2022
a86f352
Merge branch 'main' into main
ThePhar Oct 28, 2022
883008e
Fix missing argument in apply_rom_settings preventing generation.
ThePhar Oct 28, 2022
3946a1d
Merge branch 'main' into main
Nyx-Edelstein Oct 29, 2022
27415db
Merge branch 'main' into main
Nyx-Edelstein Feb 13, 2023
26c286c
Update Rom.py
Nyx-Edelstein Feb 13, 2023
50187a3
Merge branch 'main' into main
ThePhar Apr 11, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 86 additions & 3 deletions LttPAdjuster.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,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 @@ -124,6 +130,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 @@ -163,7 +176,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 @@ -225,6 +238,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 @@ -263,6 +277,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 @@ -479,6 +494,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 @@ -520,6 +565,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 @@ -596,12 +642,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 @@ -1054,7 +1138,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
53 changes: 51 additions & 2 deletions worlds/alttp/Rom.py
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,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 @@ -1772,8 +1772,53 @@ def hud_format_text(text):
output += b'\x7f\x00'
return output[:32]

def apply_oof_sfx(rom, oof: str):
with open(oof, 'rb') as stream:
oof_bytes = bytearray(stream.read())

def apply_rom_settings(rom, beep, color, quickswap, menuspeed, music: bool, sprite: str, palettes_options,
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])

def apply_rom_settings(rom, beep, color, quickswap, menuspeed, music: bool, sprite: str, oof: str, palettes_options,
Nyx-Edelstein marked this conversation as resolved.
Show resolved Hide resolved
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.slot_seeds[player]
Expand Down Expand Up @@ -1915,6 +1960,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 @@ -97,7 +97,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 @@ -388,6 +397,7 @@ def generate_output(self, output_directory: str):
world.menuspeed[player].current_key,
world.music[player],
world.sprite[player],
world.oof[player],
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

actually just had a generation issue since this attribute doesn't exist at run time. is this used by the adjuster or do we rip this out until implementing as a generation option?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it does. I just removed it.

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.