Version 3.4.0: Edit tab (edit list, large subtitle, replacements with effects and presets), Sounds tab only browses, ackit fx (pedalboard + own reverb) and replace --fx, first Linux build, catalog without a mod package

This commit is contained in:
2026-10-28 13:54:12 +01:00
parent a706dae419
commit 6de470174e
36 changed files with 2872 additions and 278 deletions

View File

@@ -16,6 +16,8 @@ of the editor and the CLI), read back from the game files, reverted, and the fil
7. mod package made with the language's voices and imported into another voice language (INT; FRA for INT):
matched by the game's line identity, so the same line (not the same WAD position) is replaced; a text key
names its language, so the edited subtitle stays that language's subtitle (the other language's is untouched)
8. 5-channel speech line replaced with effects (the Edit tab's chain: EQ, echo, PA channel layout): the
decoded game file has the line's channels in the game's order, the surrounds carry the delayed voice
Uses temporary originals stores (the user's store and settings are not touched), deleted at the end when every
change was reverted (kept, with a warning, otherwise). Whatever happens (a failed check, an exception), every
change is reverted before the tool exits; a sound that is not in its original state is never written.
@@ -32,7 +34,7 @@ sys.path.insert(0, ROOT)
import numpy as np # noqa: E402
from ackit import banks, settings, subtitles # noqa: E402
from ackit import audio, banks, fx, settings, subtitles # noqa: E402
from ackit.game import Game, default_game_dir # noqa: E402
from ackit.modpack import export_mod, import_mod, match_mod # noqa: E402
from ackit.wem import fill # noqa: E402
@@ -93,6 +95,39 @@ def sound_round_trip(ws, s, label):
check(label, ok, "(%s, %d files, %d copies/heads)" % (s.name, len(files), len(ws.locations(s))))
def effects_round_trip(ws, voices, lang):
"""8. A replacement through the effects into a 5-channel line (L R C SL SR), read back and decoded."""
label = "8 %s 5-channel line replaced with effects" % lang
s = next((x for x in ws.catalog.sounds if x.archive == voices and x.group == "Speech" and x.channels == 5
and 1.5 < x.seconds < 4 and ws.state(x) == ORIGINAL), None)
if s is None:
check(label, False, "no original 5-channel speech line found")
return
new, sr = tone(1.0, 400)
new[int(0.6 * sr):] = 0 # a pause, so the echo can be measured after the voice stops
st = fx.defaults()
st["eq"].update(on=True, low_cut=300, high_cut=3400)
st["echo"].update(on=True, time=120, feedback=0.2, mix=0.3)
st["channels"].update(on=True, front=-12, surround=-6, surround_delay=160)
x = fx.render(new, sr, st, s.channels)
wem, _ = ws.prepare(s, x, sr)
files = ws.files(s)
before = snapshot(files)
ws.apply([(s, wem, {"source": "e2e", "effects": fx.summary(st)})])
y, ysr = audio.decode_wem(ws.current_bytes(s))
ok = ws.state(s) == MODIFIED and y.shape[1] == 5
lag = -1.0
if ok:
def onset(c): # first sample above half of the channel's peak
a = np.abs(y[:, c])
return int(np.argmax(a > 0.5 * a.max())) / ysr
lag = onset(3) - onset(2) # SL vs C: the PA echo delay
ok &= abs(lag - 0.16) < 0.02 and np.abs(y[:, 2]).max() > 2 * np.abs(y[:, 0]).max()
ws.revert([s])
ok &= ws.state(s) == ORIGINAL and snapshot(files) == before and not ws.store.entries
check(label, ok, "(%s, surround delay %.3f s)" % (s.name, lag))
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--game")
@@ -202,6 +237,8 @@ def run_checks(game, lang, voices, td, ws, first):
else:
check("6 %s sound + subtitle in one package" % lang, False, "no such package found")
effects_round_trip(ws, voices, lang)
# 7. cross-language mod: the same game line, not the same WAD position
from ackit.catalog import read_csv
from ackit.paths import catalog_dir