143 lines
5.0 KiB
Python
143 lines
5.0 KiB
Python
#!/usr/bin/env python3
|
|
"""Extract native sound-test-adjacent label candidates and build preview aliases.
|
|
|
|
This is static analysis of /games/pokemon_pro/game. The table at
|
|
0x325d940 contains localized label-list pointers paired with a numeric field.
|
|
Follow-up WAV/ASR checks showed that numeric field is not a reliable direct
|
|
Radium local sound ID for every row, so these labels are retained for audit
|
|
but are not promoted into the best-name export.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import csv
|
|
import os
|
|
import re
|
|
import struct
|
|
from pathlib import Path
|
|
|
|
|
|
SOUND_TEST_LABEL_TABLE_OFFSET = 0x325D940
|
|
SOUND_TEST_LABEL_COUNT = 4
|
|
LOCALIZED_STRING_COUNT = 5
|
|
|
|
|
|
def u64(data: bytes, offset: int) -> int:
|
|
return struct.unpack_from("<Q", data, offset)[0]
|
|
|
|
|
|
def cstring(data: bytes, offset: int) -> str:
|
|
if offset <= 0 or offset >= len(data):
|
|
return ""
|
|
end = data.find(b"\x00", offset)
|
|
if end < 0:
|
|
end = len(data)
|
|
return data[offset:end].decode("utf-8", errors="replace")
|
|
|
|
|
|
def slug(value: str) -> str:
|
|
return re.sub(r"[^A-Za-z0-9_]+", "_", value).strip("_") or "sound"
|
|
|
|
|
|
def localized_labels(data: bytes, list_offset: int) -> list[tuple[int, str]]:
|
|
labels: list[tuple[int, str]] = []
|
|
for index in range(LOCALIZED_STRING_COUNT):
|
|
ptr_offset = list_offset + index * 8
|
|
if ptr_offset + 8 > len(data):
|
|
break
|
|
string_offset = u64(data, ptr_offset)
|
|
if string_offset == 0:
|
|
break
|
|
labels.append((string_offset, cstring(data, string_offset)))
|
|
return labels
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("--game-binary", type=Path, default=Path("games/pokemon_pro/game"))
|
|
parser.add_argument("--sound-manifest", type=Path, default=Path("analysis/radium-image/sound_export_manifest.tsv"))
|
|
parser.add_argument("--alias-dir", type=Path, default=Path("analysis/radium-image/sounds-native-sound-test"))
|
|
parser.add_argument("--out", type=Path, default=Path("analysis/radium-image/native_sound_test_label_manifest.tsv"))
|
|
args = parser.parse_args()
|
|
|
|
data = args.game_binary.read_bytes()
|
|
manifest = list(csv.DictReader(args.sound_manifest.open(newline=""), delimiter="\t"))
|
|
by_object = {int(row["primary_object_local_id"]): row for row in manifest if row["primary_object_local_id"]}
|
|
args.alias_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
rows: list[dict[str, object]] = []
|
|
for index in range(SOUND_TEST_LABEL_COUNT):
|
|
entry_offset = SOUND_TEST_LABEL_TABLE_OFFSET + index * 16
|
|
label_list_offset = u64(data, entry_offset)
|
|
sound_id = u64(data, entry_offset + 8)
|
|
labels = localized_labels(data, label_list_offset)
|
|
if not labels:
|
|
continue
|
|
label_string_offset, label = labels[0]
|
|
source = by_object.get(sound_id)
|
|
if source is None:
|
|
continue
|
|
source_path = Path(source["wav_path"])
|
|
alias_name = (
|
|
f"{slug(label)}"
|
|
f"__obj{sound_id:04d}"
|
|
f"_s{int(source['section8_index']):04d}"
|
|
f"_{source['channels']}ch_{source['sample_rate']}hz.wav"
|
|
)
|
|
alias_path = args.alias_dir / alias_name
|
|
if not alias_path.exists():
|
|
try:
|
|
os.link(source_path, alias_path)
|
|
link_type = "hardlink"
|
|
except OSError:
|
|
alias_path.symlink_to(Path("..") / "sounds" / source_path.name)
|
|
link_type = "symlink"
|
|
else:
|
|
link_type = "existing"
|
|
rows.append(
|
|
{
|
|
"label": label,
|
|
"sound_id": sound_id,
|
|
"alias_wav_path": alias_path.as_posix(),
|
|
"source_wav_path": source_path.as_posix(),
|
|
"section8_index": source["section8_index"],
|
|
"duration_seconds": source["duration_seconds"],
|
|
"channels": source["channels"],
|
|
"sample_rate": source["sample_rate"],
|
|
"command_ref_count": source["command_ref_count"],
|
|
"table_entry_offset": f"0x{entry_offset:08x}",
|
|
"label_list_offset": f"0x{label_list_offset:08x}",
|
|
"label_string_offset": f"0x{label_string_offset:08x}",
|
|
"localized_labels": " | ".join(text for _, text in labels),
|
|
"link_type": link_type,
|
|
}
|
|
)
|
|
|
|
fields = [
|
|
"label",
|
|
"sound_id",
|
|
"alias_wav_path",
|
|
"source_wav_path",
|
|
"section8_index",
|
|
"duration_seconds",
|
|
"channels",
|
|
"sample_rate",
|
|
"command_ref_count",
|
|
"table_entry_offset",
|
|
"label_list_offset",
|
|
"label_string_offset",
|
|
"localized_labels",
|
|
"link_type",
|
|
]
|
|
with args.out.open("w", newline="") as f:
|
|
writer = csv.DictWriter(f, fieldnames=fields, delimiter="\t")
|
|
writer.writeheader()
|
|
writer.writerows(rows)
|
|
print(f"wrote {args.out} rows={len(rows)} alias_dir={args.alias_dir}")
|
|
return 0 if len(rows) == SOUND_TEST_LABEL_COUNT else 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|