One language setting; localized in-memory speech per language
This commit is contained in:
@@ -89,11 +89,12 @@ def pack(media):
|
||||
return bytes(hdr) + bank, bytes(payload)
|
||||
|
||||
|
||||
def bank(bank_id, media, name=None, extra_sources=(), prefetch=(), events=()):
|
||||
def bank(bank_id, media, name=None, extra_sources=(), prefetch=(), events=(), mem_events=()):
|
||||
"""A v56 soundbank holding `media` {id: wem} in memory (DIDX/DATA, 16-byte aligned) with one HIRC
|
||||
Sound object per media; extra_sources: [(media id, bank id, off, size)] for sources of other banks;
|
||||
media ids in `prefetch` get a prefetch source (their DIDX data is the head of a streamed WEM);
|
||||
events: [(event name, streamed media id)] -> event -> action -> streamed Sound object."""
|
||||
events: [(event name, streamed media id)] -> event -> action -> streamed Sound object;
|
||||
mem_events: [(event name, media id of this bank)] -> event -> action -> that media's in-memory Sound."""
|
||||
data = bytearray()
|
||||
didx = b""
|
||||
for m in sorted(media):
|
||||
@@ -110,14 +111,19 @@ def bank(bank_id, media, name=None, extra_sources=(), prefetch=(), events=()):
|
||||
2 if m in prefetch else 0))
|
||||
objs += [x + (0,) for x in extra_sources]
|
||||
from ackit.bnk import fnv1
|
||||
obj = (bank_id & 0xFFFFF000) + 0x100 # HIRC object ids: unique per bank, like real Wwise ids
|
||||
hirc_objs = []
|
||||
for i, (m, b, off, size, stream) in enumerate(objs):
|
||||
hirc_objs.append((2, struct.pack("<7I", 0x100 + i, 0x00040001, stream, m, b, off, size) + bytes(6)))
|
||||
hirc_objs.append((2, struct.pack("<7I", obj + i, 0x00040001, stream, m, b, off, size) + bytes(6)))
|
||||
for i, (ev, m) in enumerate(events):
|
||||
sid, aid = 0x300 + i, 0x400 + i
|
||||
hirc_objs.append((2, struct.pack("<7I", sid, 0x00040001, 1, m, 0, 0, 0) + bytes(6)))
|
||||
hirc_objs.append((3, struct.pack("<III", aid, 0x4011, sid) + bytes(8)))
|
||||
hirc_objs.append((4, struct.pack("<III", fnv1(ev), 1, aid)))
|
||||
for i, (ev, m) in enumerate(mem_events):
|
||||
aid = 0x500 + i
|
||||
hirc_objs.append((3, struct.pack("<III", aid, 0x4011, obj + sorted(media).index(m)) + bytes(8)))
|
||||
hirc_objs.append((4, struct.pack("<III", fnv1(ev), 1, aid)))
|
||||
hirc = struct.pack("<I", len(hirc_objs))
|
||||
for t, body in hirc_objs:
|
||||
hirc += bytes([t]) + struct.pack("<I", len(body)) + body
|
||||
@@ -294,16 +300,36 @@ def make_banks(cooked, speech_wems):
|
||||
ev_bank = bank(fnv1("NPC_SynthDialogue"), {DLG_MEDIA: dlg_wem}, name="NPC_SynthDialogue",
|
||||
events=[(SPEECH_EVENT, LINE_MEDIA[1])])
|
||||
objs = [("AkEvent", SPEECH_EVENT, bytes(8)), ("RDialogueEvent", "Radio_02", dlg)]
|
||||
ue_package(os.path.join(cooked, "Synth_Dialogue.upk"), objs + [("AkBank", "NPC_SynthDialogue", ev_bank)])
|
||||
# a localized bank (one AkBank export: language list + one bank per language) with an in-memory line
|
||||
loc = localized_bank(GRUNT_BANK, {lang: {m: wem(0.4, 500 + 100 * i)} for i, (lang, m) in enumerate(GRUNT_MEDIA.items())},
|
||||
GRUNT_EVENT)
|
||||
grunt = [("AkEvent", GRUNT_EVENT, bytes(8)), ("RDialogueEvent", "Grunt_01", dialogue(GRUNT_EVENT, GRUNT_SUBS)),
|
||||
("AkBank", GRUNT_BANK, loc)]
|
||||
ue_package(os.path.join(cooked, "Synth_Dialogue.upk"),
|
||||
objs + [("AkBank", "NPC_SynthDialogue", ev_bank)] + grunt)
|
||||
ue_package(os.path.join(cooked, "Synth_Dialogue2.upk"), objs, prefix=bytes(3000))
|
||||
with open(os.path.join(cooked, "CookedBanks.txt"), "a", newline="\r\n") as f:
|
||||
f.write("NPC_SynthDialogue\n")
|
||||
f.write("NPC_SynthDialogue\n%s\n" % GRUNT_BANK)
|
||||
out = {"%08X" % m: w for m, w in list(foot.items()) + list(foley.items()) + list(gui.items())}
|
||||
out["%08X" % DLG_MEDIA] = dlg_wem
|
||||
return out
|
||||
|
||||
|
||||
SPEECH_EVENT = "Speech_Synth_Radio_02"
|
||||
GRUNT_BANK, GRUNT_EVENT = "Speech_Synth_Grunt", "Speech_Synth_Grunt_01"
|
||||
GRUNT_MEDIA = {"INT": 0x0F000001, "ITA": 0x0F000002} # one media per language (localized bank)
|
||||
GRUNT_SUBS = {"INT": ["Hey you!"], "ITA": ["Ehi, tu!"]}
|
||||
|
||||
|
||||
def localized_bank(name, per_lang, event):
|
||||
"""AkBank export data of a localized bank: language list, then one bank per language (same id)."""
|
||||
from ackit.bnk import fnv1
|
||||
langs = list(per_lang)
|
||||
lst = struct.pack("<I", len(langs)) + b"".join(_fstr(l) for l in langs)
|
||||
head = bytes(4) + struct.pack("<HHiiii", 9, 0x84, 0, 0, len(lst), 0) + lst
|
||||
blobs = b"".join(bank(fnv1(name), media, name=name, mem_events=[(event, next(iter(media)))]) + bytes(16)
|
||||
for media in per_lang.values())
|
||||
return head + blobs
|
||||
SUBTITLES = {"INT": ["Scanner line two.", "Second part."], "ITA": ["Riga due dello scanner."],
|
||||
"RUS": ["Строка два"]}
|
||||
DLG_MEDIA = 0x0E000001
|
||||
|
||||
@@ -97,7 +97,7 @@ def test_bank_sound_apply_revert_in_editor(win, tmp_path):
|
||||
w, root, originals = win
|
||||
before = files(root)
|
||||
w.cmb_arch.setCurrentText("BANK")
|
||||
assert w.proxy.rowCount() == 6
|
||||
assert w.proxy.rowCount() == 7 # 6 neutral + the INT copy of a localized line
|
||||
assert w.select_name("CHR_SynthFoley_1")
|
||||
ed = w.editor
|
||||
assert "bank CHR_SynthFoley (2 copies)" in ed.meta.text()
|
||||
@@ -116,7 +116,7 @@ def test_subtitles_tab_edit_and_link(win):
|
||||
before = files(root)
|
||||
w.tabs.setCurrentWidget(w.subs)
|
||||
p = w.subs
|
||||
assert p.lang == "INT" and p.model.rowCount() == 2
|
||||
assert p.lang == "INT" and p.model.rowCount() == 3
|
||||
p.search.setText("scanner")
|
||||
p._filter()
|
||||
assert p.proxy.rowCount() == 1
|
||||
@@ -141,6 +141,22 @@ def test_subtitles_tab_edit_and_link(win):
|
||||
assert files(root) == before and not w.ws.texts.records()
|
||||
|
||||
|
||||
def test_one_language_for_everything(win):
|
||||
w, root, _ = win
|
||||
assert w.language == "INT" and w.cmb_lang.currentText() == "INT" and not w.lbl_voices.text()
|
||||
assert {"INT", "ITA", "RUS"} <= {w.cmb_lang.itemText(i) for i in range(w.cmb_lang.count())}
|
||||
w.load_game(root, "RUS") # no Russian voices: INT speech, RUS texts
|
||||
assert w.language == "RUS" and "voices: INT" in w.lbl_voices.text()
|
||||
assert "INT" in w.ws.catalog.archives
|
||||
w.tabs.setCurrentWidget(w.subs)
|
||||
assert w.subs.lang == "RUS" and "RUS" in w.subs.lbl_lang.text()
|
||||
w.tabs.setCurrentIndex(0)
|
||||
w.select_name("CHR_RadioScanner_2")
|
||||
assert [e.text() for e in w.editor.subtitle.edits] == ["Строка два"]
|
||||
w.tabs.setCurrentWidget(w.menu)
|
||||
assert w.menu.lang is None and w.menu.model.rowCount() == 0 # no RUS menu files in the synthetic game
|
||||
|
||||
|
||||
def test_subtitles_find_replace(win):
|
||||
w, root, _ = win
|
||||
before = files(root)
|
||||
|
||||
@@ -32,7 +32,12 @@ def test_bank_catalog(synth_game, tmp_path):
|
||||
root, originals = synth_game
|
||||
ws = _ws(root, tmp_path)
|
||||
bank = {s.target: s for s in ws.catalog.sounds if s.archive == "BANK"}
|
||||
assert set(bank) == {"%08X" % m for m in synth.FOOT + synth.FOLEY + synth.GUI + (synth.DLG_MEDIA,)}
|
||||
neutral = synth.FOOT + synth.FOLEY + synth.GUI + (synth.DLG_MEDIA,)
|
||||
assert set(bank) == {"%08X" % m for m in neutral + (synth.GRUNT_MEDIA["INT"],)} # INT catalog: no ITA copy
|
||||
grunt = bank["%08X" % synth.GRUNT_MEDIA["INT"]]
|
||||
assert grunt.language == "INT" and grunt.name == synth.GRUNT_EVENT and grunt.group == "Speech"
|
||||
ita = Workspace(root, store_root=str(tmp_path / "s2"), archives=["BANK"]).catalog # no language: all
|
||||
assert {s.language for s in ita.sounds if s.bank == synth.GRUNT_BANK} == {"INT", "ITA"}
|
||||
foot = bank["%08X" % synth.FOOT[0]]
|
||||
assert foot.bank == "CHR_SynthFootsteps_Media" and foot.name == "CHR_SynthFootsteps_Media_1"
|
||||
assert (foot.group, foot.category) == ("SFX", "Footsteps")
|
||||
@@ -41,6 +46,8 @@ def test_bank_catalog(synth_game, tmp_path):
|
||||
assert foley.rate == 32000 and foley.size == len(originals[foley.target])
|
||||
assert sorted(l.file for l in ws.locations(foley)) == ["Synth_A.upk", "Synth_B.upk"]
|
||||
for s in bank.values():
|
||||
if s.language:
|
||||
continue
|
||||
assert ws.current_bytes(s) == originals[s.target]
|
||||
assert all(_copy(ws, l) == originals[s.target] for l in ws.locations(s))
|
||||
# the prefetch head belongs to INT variation 1 of the speech entry, not to the BANK archive
|
||||
|
||||
@@ -69,7 +69,7 @@ def test_shipped_csvs_are_consistent():
|
||||
keys = {r.key for r in rows}
|
||||
assert len(keys) == len(rows), f
|
||||
assert all(r.group in classify.GROUPS for r in rows)
|
||||
assert len({r.name.lower() for r in rows}) == len(rows), f
|
||||
assert len({(r.language, r.name.lower()) for r in rows}) == len(rows), f # per language
|
||||
# no checksum only where the copies of a BANK sound differ (1C898731)
|
||||
assert sum(len(r.sha1) != 16 for r in rows) <= (1 if f == "BANK.csv" else 0), f
|
||||
hdr = next(csv.reader(open(os.path.join(catalog_dir(), "ITA.csv"), encoding="utf-8")))
|
||||
@@ -84,7 +84,9 @@ def test_shipped_csvs_are_consistent():
|
||||
def test_real_game_uses_shipped_catalog(real_game_dir):
|
||||
g = Game(real_game_dir)
|
||||
cat = Catalog(g, ["ITA", "SFX", "BANK"])
|
||||
assert len(cat.sounds) == 10816 + 651 + 15404
|
||||
bank = read_csv(os.path.join(catalog_dir(), "BANK.csv"))
|
||||
assert len(cat.sounds) == 10816 + 651 + sum(1 for s in bank if s.language in ("", "ITA"))
|
||||
assert {s.language for s in cat.sounds if s.archive == "BANK"} == {"", "ITA"}
|
||||
s = cat.resolve("Speech_TAPE_JOKER_1_4")
|
||||
assert s.category == "Interview tapes" and s.size == 44985
|
||||
assert len(cat.heads) == 396 and sum(len(v) for v in cat.heads.values()) == 2101
|
||||
|
||||
@@ -12,7 +12,7 @@ def test_clean_synthetic_catalog_passes(synth_game):
|
||||
root, _ = synth_game
|
||||
rep = catalogcheck.run(Game(root), log=lambda *a: None)
|
||||
assert rep.ok, rep.issues
|
||||
assert rep.checked["BANK"] == 6 and rep.checked["INT"] == 4
|
||||
assert rep.checked["BANK"] == 8 and rep.checked["INT"] == 4 # all languages of the localized line
|
||||
|
||||
|
||||
def test_corrupted_catalog_is_reported(synth_game):
|
||||
|
||||
@@ -56,11 +56,17 @@ def test_subtitle_catalog_and_link(synth_game, tmp_path):
|
||||
root, _ = synth_game
|
||||
ws = _ws(root, tmp_path)
|
||||
lines = ws.texts.subtitles("INT")
|
||||
assert [(s.speech, s.line, s.text) for s in lines] == [(synth.SPEECH_EVENT, 0, "Scanner line two."),
|
||||
assert [(s.speech, s.line, s.text) for s in lines] == [(synth.GRUNT_EVENT, 0, "Hey you!"),
|
||||
(synth.SPEECH_EVENT, 0, "Scanner line two."),
|
||||
(synth.SPEECH_EVENT, 1, "Second part.")]
|
||||
# a line kept in memory: each language's subtitle links to that language's BANK sound
|
||||
grunt = ws.catalog.by_key["BANK:%08X" % synth.GRUNT_MEDIA["INT"]]
|
||||
assert [s.text for s in ws.texts.for_sound(grunt)] == ["Hey you!"]
|
||||
ita = {s.speech: s.sound for s in ws.texts.subtitles("ITA")}
|
||||
assert ita[synth.GRUNT_EVENT] == "BANK:%08X" % synth.GRUNT_MEDIA["ITA"]
|
||||
line = ws.catalog.by_key["INT:%08X:1" % synth.SPEECH_ENTRY]
|
||||
assert [s.text for s in ws.texts.for_sound(line)] == ["Scanner line two.", "Second part."]
|
||||
assert ws.texts.subtitles("ITA")[0].sound == line.key # no ITA speech WAD here: INT line
|
||||
assert ita[synth.SPEECH_EVENT] == line.key # no ITA speech WAD here: INT line
|
||||
assert ws.texts.subtitles("RUS")[0].enc == "w"
|
||||
assert sorted(os.path.basename(p) for p in ws.texts.files(SUB_KEY)) == ["Synth_Dialogue.upk", "Synth_Dialogue2.upk"]
|
||||
assert ws.texts.limit(SUB_KEY) == len("Scanner line two.")
|
||||
|
||||
Reference in New Issue
Block a user