ALTTP: Add "oof" sound customization option (#709)
Co-authored-by: Fabian Dill <Berserker66@users.noreply.github.com> Co-authored-by: Fabian Dill <fabian.dill@web.de> Co-authored-by: Zach Parks <zach@alliware.com>
This commit is contained in:
parent
c711d803f8
commit
8b7ffaf671
|
@ -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()
|
||||
|
@ -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):
|
||||
|
@ -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)
|
||||
|
@ -227,6 +240,7 @@ def adjustGUI():
|
|||
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)
|
||||
|
@ -265,6 +279,7 @@ def adjustGUI():
|
|||
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")
|
||||
|
||||
|
@ -481,6 +496,36 @@ class BackgroundTaskProgressNullWindow(BackgroundTask):
|
|||
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:
|
||||
|
@ -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',
|
||||
|
@ -598,12 +644,50 @@ def get_rom_options_frame(parent=None):
|
|||
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()
|
||||
|
@ -1056,7 +1140,6 @@ class SpriteSelector():
|
|||
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
|
||||
|
|
|
@ -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:
|
||||
|
@ -1775,8 +1775,57 @@ 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])
|
||||
|
||||
#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]
|
||||
|
@ -1918,6 +1967,10 @@ def apply_rom_settings(rom, beep, color, quickswap, menuspeed, music: bool, spri
|
|||
|
||||
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()
|
||||
|
||||
|
|
|
@ -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):
|
||||
|
@ -485,6 +494,7 @@ class ALTTPWorld(World):
|
|||
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,
|
||||
|
|
|
@ -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.
|
Loading…
Reference in New Issue