PH-PB PXH-PXB Inazuma Eleven Sound Extractor.py
plain_text
11 hours ago
16.2 KB
9
Indexable
import os
import struct
import tkinter as tk
from tkinter import filedialog, messagebox, scrolledtext
VALID_EXT = (".SED", ".SWD", ".SMD")
# ============================================================
# Utilities
# ============================================================
def leer_u32(data, pos):
return struct.unpack_from("<I", data, pos)[0]
def nombre_valido(nombre):
if len(nombre) == 0 or len(nombre) > 24:
return False
if "." not in nombre:
return False
ext = os.path.splitext(nombre)[1].upper()
if ext not in VALID_EXT:
return False
for c in nombre:
o = ord(c)
if o < 32 or o > 126:
return False
return True
def nombre_imprimible(nombre):
if len(nombre) == 0 or len(nombre) > 24:
return False
for c in nombre:
o = ord(c)
if o < 32 or o > 126:
return False
return True
def leer_nombre_interno(data, offset):
"""Read internal name from SMD/SED/SWD file at data[offset+0x20]."""
name_off = offset + 0x20
if name_off + 24 > len(data):
return None
raw = data[name_off:name_off+24].split(b"\x00")[0]
try:
candidate = raw.decode("ascii")
if candidate and len(candidate) > 4 and "." in candidate:
cext = os.path.splitext(candidate)[1].upper()
if cext in [".SMD",".SED",".SWD",".SAD"]:
return candidate
except:
pass
return None
def make_output_folder(index_path):
"""Build output folder name including the index extension to avoid IE1/IE2/IE3 mixing."""
base = os.path.splitext(index_path)[0]
ext = os.path.splitext(index_path)[1].lstrip(".").lower()
return base + "_" + ext + "_extracted"
# ============================================================
# PH/PB (Inazuma Eleven 1)
# ============================================================
def buscar_tabla_ph(ph):
limite = min(len(ph), 0x2000)
for pos in range(limite):
for largo in range(8, 25):
if pos + largo + 8 > len(ph):
continue
raw = ph[pos:pos+largo]
if b"\x00" not in raw:
continue
raw = raw.split(b"\x00")[0]
try:
nombre = raw.decode("ascii")
except:
continue
if not nombre_valido(nombre):
continue
base = pos
off = leer_u32(ph, base + 24)
size = leer_u32(ph, base + 28)
if off == 0 and size > 0:
return base
return None
def leer_tabla_ph(ph, pb_size, log=None):
def _log(msg):
if log: log(msg)
inicio = buscar_tabla_ph(ph)
if inicio is None:
raise Exception("PH table not found.")
entradas = []
omitidas = []
pos = inicio
while True:
if pos + 32 > len(ph):
break
nombre_raw = ph[pos:pos+24].split(b"\x00")[0]
try:
nombre = nombre_raw.decode("ascii")
except:
_log("[END] Non-decodable name at 0x%X, end of table." % pos)
break
if not nombre_imprimible(nombre):
_log("[END] Empty/non-printable name at 0x%X, end of table." % pos)
break
offset = leer_u32(ph, pos + 24)
size = leer_u32(ph, pos + 28)
if size == 0:
omitidas.append({"nombre": nombre, "offset": offset, "size": size, "motivo": "size==0"})
pos += 32
continue
if offset + size > pb_size:
omitidas.append({"nombre": nombre, "offset": offset, "size": size, "motivo": "out of range"})
pos += 32
continue
entradas.append({"nombre": nombre, "offset": offset, "size": size})
pos += 32
return inicio, pos, entradas, omitidas
# ============================================================
# PKH/PKB (Inazuma Eleven 2 / 3)
# ============================================================
def detectar_formato(ph_data):
"""Detect if index is PH (IE1) or PKH (IE2/IE3)."""
for pos in range(min(len(ph_data), 0x200)):
for largo in range(8, 25):
if pos + largo > len(ph_data):
continue
raw = ph_data[pos:pos+largo]
if b"\x00" not in raw:
continue
raw = raw.split(b"\x00")[0]
try:
nombre = raw.decode("ascii")
except:
continue
if nombre_valido(nombre):
return "PH"
return "PKH"
def leer_tabla_pkh(pkh, pkb_data, log=None):
"""Read PKH as 16-byte entries, extract SED/SMD + SWD pairs."""
def _log(msg):
if log: log(msg)
n16 = len(pkh) // 16
_log("PKH entries: %d" % n16)
# Scan magic bytes in PKB
magic_pos = []
for magic, ftype in [(b"smdl","SMD"),(b"sedl","SED"),(b"swdl","SWD")]:
start = 0
while True:
idx = pkb_data.find(magic, start)
if idx == -1:
break
magic_pos.append((idx, ftype))
start = idx + 1
magic_pos.sort()
entradas = []
errores = 0
for i in range(n16):
off = i * 16
f0, f1, f2, f3 = struct.unpack_from("<IIII", pkh, off)
seq_off = f1 + 0x10
if seq_off >= len(pkb_data):
errores += 1
continue
magic = pkb_data[seq_off:seq_off+4]
if magic not in (b"smdl", b"sedl"):
errores += 1
continue
# SED/SMD name
seq_name = leer_nombre_interno(pkb_data, seq_off)
if seq_name is None:
if magic == b"smdl": seq_name = "SMD_%03d.SMD" % i
else: seq_name = "SED_%03d.SED" % i
# Find paired SWD
swd_off = None
for pos, ft in magic_pos:
if pos > seq_off and ft == "SWD":
swd_off = pos
break
# SED/SMD size
if swd_off is not None:
seq_size = swd_off - seq_off
else:
seq_size = min(f2, len(pkb_data) - seq_off)
seq_size = max(seq_size, 0x50)
entradas.append({"nombre": seq_name, "offset": seq_off, "size": seq_size})
# Paired SWD
if swd_off is not None:
swd_name = leer_nombre_interno(pkb_data, swd_off)
if swd_name is None:
swd_name = os.path.splitext(seq_name)[0] + ".SWD"
next_after_swd = len(pkb_data)
for pos, ft in magic_pos:
if pos > swd_off:
next_after_swd = pos
break
pair_end = max(seq_off + f2, next_after_swd)
pair_end = min(pair_end, len(pkb_data))
swd_size = pair_end - swd_off
swd_size = max(swd_size, 0x50)
entradas.append({"nombre": swd_name, "offset": swd_off, "size": swd_size})
if errores > 0:
_log("PKH: %d entries with errors" % errores)
return entradas
# ============================================================
# Extract
# ============================================================
def extraer(ph_path, pb_path, log):
with open(ph_path, "rb") as f:
ph = f.read()
with open(pb_path, "rb") as f:
pb = f.read()
log("=" * 70)
log("Index : " + os.path.basename(ph_path))
log("Data : " + os.path.basename(pb_path))
log("Index size = %d" % len(ph))
log("Data size = %d" % len(pb))
log("=" * 70)
log("")
formato = detectar_formato(ph)
log("Detected format: %s" % formato)
log("")
if formato == "PH":
inicio, fin, entradas, omitidas = leer_tabla_ph(ph, len(pb), log=log)
log("Table start : %d" % inicio)
log("Table end : %d" % fin)
else:
entradas = leer_tabla_pkh(ph, pb, log=log)
omitidas = []
log("Entries : %d" % len(entradas))
log("Skipped : %d" % len(omitidas))
log("")
carpeta = make_output_folder(ph_path)
os.makedirs(carpeta, exist_ok=True)
log("Output folder: %s" % carpeta)
log("")
usados = {}
total = 0
conteo_ext = {}
for e in entradas:
nombre = e["nombre"]
if nombre in usados:
usados[nombre] += 1
base, ext = os.path.splitext(nombre)
nombre = "%s_%d%s" % (base, usados[nombre], ext)
else:
usados[nombre] = 0
ruta = os.path.join(carpeta, nombre)
with open(ruta, "wb") as f:
ini = e["offset"]
fin_ = ini + e["size"]
f.write(pb[ini:fin_])
log("%-24s off=%10d size=%d" % (nombre, e["offset"], e["size"]))
ext_key = os.path.splitext(nombre)[1].upper()
conteo_ext[ext_key] = conteo_ext.get(ext_key, 0) + 1
total += 1
log("")
log("Extracted %d files." % total)
log("Summary by extension: %s" % conteo_ext)
if omitidas:
log("")
log("Skipped entries (not extracted):")
for o in omitidas:
log(" %-20s off=%10d size=%d (%s)" % (o["nombre"], o["offset"], o["size"], o["motivo"]))
return total
# ============================================================
# GUI
# ============================================================
class App:
def __init__(self):
self.root = tk.Tk()
self.root.title("Extractor PH/PB + PKH/PKB v3.0 (IE1 + IE2 + IE3)")
self.root.geometry("900x700")
top = tk.Frame(self.root)
top.pack(fill="x", padx=10, pady=10)
tk.Button(top, text="Open PH / PKH", width=25, command=self.abrir).pack(side="left")
tk.Button(top, text="Clear", width=15, command=self.limpiar).pack(side="left", padx=5)
self.txt = scrolledtext.ScrolledText(self.root, width=120, height=40)
self.txt.pack(fill="both", expand=True, padx=10, pady=10)
self.log("Extractor PH/PB + PKH/PKB v3.0")
self.log("")
self.log("IE1: PH + PB (ASCII index, 32 bytes/entry)")
self.log("IE2: PKH + PKB (binary index, 16 bytes/entry, field1+0x10=offset)")
self.log("IE3: PKH + PKB (same format as IE2)")
self.log("")
self.log("Select a .PH or .PKH file")
self.root.mainloop()
def limpiar(self):
self.txt.delete("1.0", tk.END)
def log(self, texto):
self.txt.insert(tk.END, texto + "\n")
self.txt.see(tk.END)
self.root.update_idletasks()
def abrir(self):
ph = filedialog.askopenfilename(
title="Select PH or PKH file",
filetypes=[
("PH/PKH", "*.ph *.PH *.pkh *.PKH"),
("All files", "*.*")
]
)
if not ph:
return
base = os.path.splitext(ph)[0]
ext_idx = os.path.splitext(ph)[1].lower()
# Search for data file based on index extension
if ext_idx in (".pkh",):
candidatos = [base + ".PKB", base + ".pkb", base + ".Pkb", base + ".pKb"]
elif ext_idx in (".ph",):
candidatos = [base + ".PB", base + ".pb", base + ".Pb", base + ".pB"]
else:
candidatos = [base + ".PKB", base + ".pkb", base + ".PB", base + ".pb"]
pb = None
for f in candidatos:
if os.path.exists(f):
pb = f
break
if pb is None:
messagebox.showinfo("Data file", "Please select the PB/PKB data file.")
pb = filedialog.askopenfilename(
title="Select PB or PKB file",
filetypes=[
("PB/PKB", "*.pb *.PB *.pkb *.PKB"),
("All files", "*.*")
]
)
if not pb:
return
self.limpiar()
try:
total = extraer(ph, pb, self.log)
messagebox.showinfo("Done", "Extracted %d files." % total)
except Exception as e:
messagebox.showerror("Error", str(e))
self.log("")
self.log("ERROR:")
self.log(str(e))
def main():
App()
if __name__ == "__main__":
main()Editor is loading...
Leave a Comment