Version 3.2.3: editor layout with aligned controls and full-width button bars; docs on recordings shared between languages
This commit is contained in:
5
NOTES.md
5
NOTES.md
@@ -121,6 +121,11 @@
|
|||||||
plays that as silence; ackit now fills each in-memory copy exactly with a LIST/adtl/labl chunk (`wem.fill`,
|
plays that as silence; ackit now fills each in-memory copy exactly with a LIST/adtl/labl chunk (`wem.fill`,
|
||||||
`Workspace.fits/fill_sizes/prepare`). Verified by the user in game (a beep on every step), e2e_features 89/89.
|
`Workspace.fits/fill_sizes/prepare`). Verified by the user in game (a beep on every step), e2e_features 89/89.
|
||||||
Version 3.2.2. The footstep test was reverted (game original).
|
Version 3.2.2. The footstep test was reverted (game original).
|
||||||
|
- 2026-10-11 version 3.2.3: editor layout (user: controls "placed in line and go to new line, not pretty"):
|
||||||
|
aligned grid for the edit fields, every button row fills its column, each column ends with a full-width
|
||||||
|
action bar level with the others; A/B and Loop are icons next to the volume. Fits a 1000-px window
|
||||||
|
without horizontal scrolling (minimum 984 px). Docs: recordings the game shares between languages (user saw
|
||||||
|
English lines "leaking" into other languages: the game's own data, banks.md *Lines*).
|
||||||
- DONE by the user in game: replaced voice lines, edited subtitles and replaced footsteps (after the fill fix).
|
- DONE by the user in game: replaced voice lines, edited subtitles and replaced footsteps (after the fill fix).
|
||||||
- OPEN: channel order for >2 channels unverified; 19 speaker labels to check by listening; the Linux build was
|
- OPEN: channel order for >2 channels unverified; 19 speaker labels to check by listening; the Linux build was
|
||||||
never built or run (no Linux here).
|
never built or run (no Linux here).
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ editor Unheard City (mission 2).
|
|||||||
found in the game's memory, no crash); `tools/e2e_editor.py` PASS. Game files original at the end
|
found in the game's memory, no crash); `tools/e2e_editor.py` PASS. Game files original at the end
|
||||||
(`ackit status`). The catalog builder refuses to run on a modified game, so the catalog always describes the
|
(`ackit status`). The catalog builder refuses to run on a modified game, so the catalog always describes the
|
||||||
original files.
|
original files.
|
||||||
- **Package**: `dist/UnheardCity-3.2.2-windows-amd64.zip` (86 MB, catalogs included). With `PATH` reduced to
|
- **Package**: `dist/UnheardCity-3.2.3-windows-amd64.zip` (86 MB, catalogs included). With `PATH` reduced to
|
||||||
the Windows folders the packaged exe passes its smoke test (in the game's language - Italian on the test PC:
|
the Windows folders the packaged exe passes its smoke test (in the game's language - Italian on the test PC:
|
||||||
21,015 sounds loaded, encode + inject, 9,377 subtitles and 7,020 menu texts loaded, an in-memory sound decoded from a package) and the GUI starts and
|
21,015 sounds loaded, encode + inject, 9,377 subtitles and 7,020 menu texts loaded, an in-memory sound decoded from a package) and the GUI starts and
|
||||||
quits cleanly. The installer is unchanged since mission 2 (install/upgrade/uninstall verified then).
|
quits cleanly. The installer is unchanged since mission 2 (install/upgrade/uninstall verified then).
|
||||||
|
|||||||
@@ -8,4 +8,4 @@ Library layers (low -> high):
|
|||||||
inject (WAD writing), originals (store), workspace (apply/revert), modpack (share)
|
inject (WAD writing), originals (store), workspace (apply/revert), modpack (share)
|
||||||
tools cli (`ackit` command), verify/ (offline self-check, in-game test on Windows)
|
tools cli (`ackit` command), verify/ (offline self-check, in-game test on Windows)
|
||||||
"""
|
"""
|
||||||
__version__ = "3.2.2"
|
__version__ = "3.2.3"
|
||||||
|
|||||||
104
app/editor.py
104
app/editor.py
@@ -54,6 +54,15 @@ class EditorPanel(QWidget):
|
|||||||
b.setToolTip(tip)
|
b.setToolTip(tip)
|
||||||
return b
|
return b
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _fill(*items):
|
||||||
|
"""A row of buttons filling the column's width: [(widget, share)], equal edges in every column."""
|
||||||
|
row = QHBoxLayout()
|
||||||
|
row.setSpacing(6)
|
||||||
|
for w, share in items:
|
||||||
|
row.addWidget(w, share)
|
||||||
|
return row
|
||||||
|
|
||||||
def _wave(self, kind, selectable=False):
|
def _wave(self, kind, selectable=False):
|
||||||
w = Waveform(selectable=selectable)
|
w = Waveform(selectable=selectable)
|
||||||
w.accent, w.bg = theme.WAVE[kind], theme.WAVE_BG
|
w.accent, w.bg = theme.WAVE[kind], theme.WAVE_BG
|
||||||
@@ -63,7 +72,7 @@ class EditorPanel(QWidget):
|
|||||||
"""Header on top; below, side by side (resizable): LISTEN | REPLACE (with the apply buttons) | SUBTITLE."""
|
"""Header on top; below, side by side (resizable): LISTEN | REPLACE (with the apply buttons) | SUBTITLE."""
|
||||||
self.setObjectName("EditorPanel")
|
self.setObjectName("EditorPanel")
|
||||||
lay = QVBoxLayout(self)
|
lay = QVBoxLayout(self)
|
||||||
lay.setContentsMargins(10, 6, 10, 6)
|
lay.setContentsMargins(8, 6, 8, 6)
|
||||||
lay.setSpacing(4)
|
lay.setSpacing(4)
|
||||||
self.title = QLabel()
|
self.title = QLabel()
|
||||||
self.title.setObjectName("EditorTitle")
|
self.title.setObjectName("EditorTitle")
|
||||||
@@ -87,41 +96,47 @@ class EditorPanel(QWidget):
|
|||||||
# listen ----------------------------------------------------------------------------------
|
# listen ----------------------------------------------------------------------------------
|
||||||
g = QGroupBox("LISTEN")
|
g = QGroupBox("LISTEN")
|
||||||
gl = QGridLayout(g)
|
gl = QGridLayout(g)
|
||||||
|
gl.setContentsMargins(8, 8, 8, 8)
|
||||||
gl.setVerticalSpacing(4)
|
gl.setVerticalSpacing(4)
|
||||||
self.wf_orig = self._wave("orig")
|
self.wf_orig = self._wave("orig")
|
||||||
self.wf_inst = self._wave("inst")
|
self.wf_inst = self._wave("inst")
|
||||||
self.b_play_orig = self._btn("Original", "play", lambda: self.play("orig"), "Play the original (Space)", icons.STEEL)
|
self.b_play_orig = self._btn("Original", "play", lambda: self.play("orig"), "Play the original (Space)", icons.STEEL)
|
||||||
self.b_play_inst = self._btn("Installed", "play", lambda: self.play("inst"), "Play what the game has now", "#8f4a4e")
|
self.b_play_inst = self._btn("Installed", "play", lambda: self.play("inst"), "Play what the game has now", "#8f4a4e")
|
||||||
self.b_stop = self._btn("Stop", "stop", self.stop, "Stop (Esc)")
|
self.b_stop = self._btn("Stop", "stop", self.stop, "Stop (Esc)")
|
||||||
self.b_ab = self._btn("A/B", "ab", self.ab_switch, "Switch between original and replacement at the same position",
|
# playback options next to the volume: icons with tooltips
|
||||||
icons.ACCENT)
|
self.b_ab = self._btn("", "ab", self.ab_switch, "A/B: switch between the original and the replacement at "
|
||||||
self.cb_loop = self._btn("Loop", "loop", None, "Repeat playback")
|
"the same position", icons.ACCENT)
|
||||||
|
self.cb_loop = self._btn("", "loop", None, "Loop: repeat playback")
|
||||||
self.cb_loop.setCheckable(True)
|
self.cb_loop.setCheckable(True)
|
||||||
|
for b in (self.b_ab, self.cb_loop):
|
||||||
|
b.setFixedWidth(b.sizeHint().height() + 4)
|
||||||
self.sl_vol = QSlider(Qt.Orientation.Horizontal)
|
self.sl_vol = QSlider(Qt.Orientation.Horizontal)
|
||||||
self.sl_vol.setRange(0, 100)
|
self.sl_vol.setRange(0, 100)
|
||||||
self.sl_vol.setValue(80)
|
self.sl_vol.setValue(80)
|
||||||
self.sl_vol.setMaximumWidth(120)
|
self.sl_vol.setMinimumWidth(80)
|
||||||
self.sl_vol.setToolTip("Volume")
|
self.sl_vol.setToolTip("Volume")
|
||||||
self.sl_vol.valueChanged.connect(lambda v: self.ctx.player.set_volume(v / 100.0))
|
self.sl_vol.valueChanged.connect(lambda v: self.ctx.player.set_volume(v / 100.0))
|
||||||
self.lbl_time = QLabel("0:00.00")
|
self.lbl_time = QLabel("0:00.00")
|
||||||
self.lbl_time.setObjectName("Muted")
|
self.lbl_time.setObjectName("Muted")
|
||||||
|
self.lbl_time.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)
|
||||||
|
self.lbl_time.setMinimumWidth(self.lbl_time.fontMetrics().horizontalAdvance("00:00.00") + 4)
|
||||||
gl.addWidget(QLabel("Original"), 0, 0)
|
gl.addWidget(QLabel("Original"), 0, 0)
|
||||||
gl.addWidget(self.wf_orig, 0, 1)
|
gl.addWidget(self.wf_orig, 0, 1)
|
||||||
self.lbl_inst = QLabel("Installed")
|
self.lbl_inst = QLabel("Installed")
|
||||||
gl.addWidget(self.lbl_inst, 1, 0)
|
gl.addWidget(self.lbl_inst, 1, 0)
|
||||||
gl.addWidget(self.wf_inst, 1, 1)
|
gl.addWidget(self.wf_inst, 1, 1)
|
||||||
row = QHBoxLayout()
|
gl.setRowStretch(2, 1) # controls at the bottom, level with the other columns' actions
|
||||||
for w in (self.b_play_orig, self.b_play_inst, self.b_stop):
|
gl.addLayout(self._fill((self.b_play_orig, 1), (self.b_play_inst, 1), (self.b_stop, 1)), 4, 0, 1, 2)
|
||||||
row.addWidget(w)
|
vol = QHBoxLayout() # playback options: A/B, loop, volume, position
|
||||||
row.addStretch(1)
|
vol.setSpacing(6)
|
||||||
gl.addLayout(row, 2, 0, 1, 2)
|
vol.addWidget(self.b_ab)
|
||||||
row = QHBoxLayout()
|
vol.addWidget(self.cb_loop)
|
||||||
for w in (self.b_ab, self.cb_loop, QLabel("Volume"), self.sl_vol, self.lbl_time):
|
vol.addSpacing(6)
|
||||||
row.addWidget(w)
|
vol.addWidget(QLabel("Volume"))
|
||||||
row.addStretch(1)
|
vol.addWidget(self.sl_vol, 1)
|
||||||
gl.addLayout(row, 3, 0, 1, 2)
|
vol.addWidget(self.lbl_time)
|
||||||
|
gl.addLayout(vol, 3, 0, 1, 2)
|
||||||
gl.setColumnStretch(1, 1)
|
gl.setColumnStretch(1, 1)
|
||||||
gl.setRowStretch(4, 1)
|
|
||||||
self.wf_orig.seekRequested.connect(lambda t: self.play("orig", t))
|
self.wf_orig.seekRequested.connect(lambda t: self.play("orig", t))
|
||||||
self.wf_inst.seekRequested.connect(lambda t: self.play("inst", t))
|
self.wf_inst.seekRequested.connect(lambda t: self.play("inst", t))
|
||||||
listen = g
|
listen = g
|
||||||
@@ -129,25 +144,29 @@ class EditorPanel(QWidget):
|
|||||||
# replace ---------------------------------------------------------------------------------
|
# replace ---------------------------------------------------------------------------------
|
||||||
g = QGroupBox("REPLACE")
|
g = QGroupBox("REPLACE")
|
||||||
rl = QVBoxLayout(g)
|
rl = QVBoxLayout(g)
|
||||||
rl.setSpacing(4)
|
rl.setContentsMargins(8, 8, 8, 8)
|
||||||
|
rl.setSpacing(6)
|
||||||
src = QHBoxLayout()
|
src = QHBoxLayout()
|
||||||
|
src.setSpacing(6)
|
||||||
self.b_load = self._btn("Load file…", "open", self.load_file_dialog,
|
self.b_load = self._btn("Load file…", "open", self.load_file_dialog,
|
||||||
"WAV, FLAC, OGG, OPUS, MP3, AIFF (M4A/WMA need ffmpeg installed) (Ctrl+O)")
|
"WAV, FLAC, OGG, OPUS, MP3, AIFF (M4A/WMA need ffmpeg installed) (Ctrl+O)")
|
||||||
self.cmb_in = QComboBox()
|
self.cmb_in = QComboBox()
|
||||||
self.cmb_in.setMinimumWidth(110)
|
self.cmb_in.setMinimumWidth(110)
|
||||||
self.cmb_in.setMaximumWidth(200)
|
|
||||||
self.cmb_in.setToolTip("Recording device")
|
self.cmb_in.setToolTip("Recording device")
|
||||||
self.b_rec = self._btn("Record", "record", self.toggle_record, "Record from the selected input")
|
self.b_rec = self._btn("Record", "record", self.toggle_record, "Record from the selected input")
|
||||||
self.meter = QProgressBar()
|
self.meter = QProgressBar()
|
||||||
self.meter.setRange(0, 100)
|
self.meter.setRange(0, 100)
|
||||||
self.meter.setTextVisible(False)
|
self.meter.setTextVisible(False)
|
||||||
self.meter.setMaximumWidth(60)
|
self.meter.setFixedWidth(60)
|
||||||
self.meter.setToolTip("Input level")
|
self.meter.setToolTip("Input level")
|
||||||
self.lbl_rec = QLabel("")
|
self.lbl_rec = QLabel("")
|
||||||
self.lbl_rec.setObjectName("Muted")
|
self.lbl_rec.setObjectName("Muted")
|
||||||
for w in (self.b_load, self.cmb_in, self.b_rec, self.meter, self.lbl_rec):
|
src.addWidget(self.b_load, 1) # the row fills the column: file | microphone, level
|
||||||
|
src.addSpacing(10)
|
||||||
|
src.addWidget(self.cmb_in, 2)
|
||||||
|
src.addWidget(self.b_rec, 1)
|
||||||
|
for w in (self.meter, self.lbl_rec):
|
||||||
src.addWidget(w)
|
src.addWidget(w)
|
||||||
src.addStretch(1)
|
|
||||||
rl.addLayout(src)
|
rl.addLayout(src)
|
||||||
self.wf_new = self._wave("new", selectable=True)
|
self.wf_new = self._wave("new", selectable=True)
|
||||||
self.wf_new.setToolTip("Click: play from here. Right-drag (or Shift+drag): select the part to keep.")
|
self.wf_new.setToolTip("Click: play from here. Right-drag (or Shift+drag): select the part to keep.")
|
||||||
@@ -160,12 +179,12 @@ class EditorPanel(QWidget):
|
|||||||
s_.setDecimals(2)
|
s_.setDecimals(2)
|
||||||
s_.setSuffix(" s")
|
s_.setSuffix(" s")
|
||||||
s_.setRange(0, 36000)
|
s_.setRange(0, 36000)
|
||||||
s_.setMaximumWidth(92)
|
s_.setMaximumWidth(80)
|
||||||
s_.valueChanged.connect(self._changed)
|
s_.valueChanged.connect(self._changed)
|
||||||
self.sp_gain = QDoubleSpinBox()
|
self.sp_gain = QDoubleSpinBox()
|
||||||
self.sp_gain.setRange(-40, 40)
|
self.sp_gain.setRange(-40, 40)
|
||||||
self.sp_gain.setSuffix(" dB")
|
self.sp_gain.setSuffix(" dB")
|
||||||
self.sp_gain.setMaximumWidth(92)
|
self.sp_gain.setMaximumWidth(80)
|
||||||
self.sp_gain.valueChanged.connect(self._changed)
|
self.sp_gain.valueChanged.connect(self._changed)
|
||||||
self.cb_norm = QCheckBox("Normalize")
|
self.cb_norm = QCheckBox("Normalize")
|
||||||
self.cb_match = QCheckBox("Match loudness")
|
self.cb_match = QCheckBox("Match loudness")
|
||||||
@@ -175,15 +194,24 @@ class EditorPanel(QWidget):
|
|||||||
self.b_silence = self._btn("Trim silence", None, self.trim_silence, "Cut silence at the start and end")
|
self.b_silence = self._btn("Trim silence", None, self.trim_silence, "Cut silence at the start and end")
|
||||||
self.b_play_new = self._btn("Replacement", "play", lambda: self.play("new"), "Play the processed replacement",
|
self.b_play_new = self._btn("Replacement", "play", lambda: self.play("new"), "Play the processed replacement",
|
||||||
icons.ACCENT)
|
icons.ACCENT)
|
||||||
proc = QHBoxLayout()
|
# edit controls in a grid: labels in one column, the fields aligned, playback on the right
|
||||||
for w in (QLabel("Keep"), self.sp_a, QLabel("to"), self.sp_b, self.b_silence, self.b_play_new):
|
proc = QGridLayout()
|
||||||
proc.addWidget(w)
|
proc.setHorizontalSpacing(6)
|
||||||
proc.addStretch(1)
|
proc.setVerticalSpacing(6)
|
||||||
rl.addLayout(proc)
|
proc.addWidget(QLabel("Keep"), 0, 0)
|
||||||
proc = QHBoxLayout()
|
proc.addWidget(self.sp_a, 0, 1)
|
||||||
for w in (QLabel("Gain"), self.sp_gain, self.cb_norm, self.cb_match):
|
proc.addWidget(QLabel("to"), 0, 2, Qt.AlignmentFlag.AlignCenter)
|
||||||
proc.addWidget(w)
|
proc.addWidget(self.sp_b, 0, 3)
|
||||||
proc.addStretch(1)
|
proc.addLayout(self._fill((self.b_silence, 1), (self.b_play_new, 1)), 0, 4) # the rest of the width
|
||||||
|
proc.addWidget(QLabel("Gain"), 1, 0)
|
||||||
|
proc.addWidget(self.sp_gain, 1, 1)
|
||||||
|
checks = QHBoxLayout()
|
||||||
|
checks.setSpacing(12)
|
||||||
|
checks.addWidget(self.cb_norm)
|
||||||
|
checks.addWidget(self.cb_match)
|
||||||
|
checks.addStretch(1)
|
||||||
|
proc.addLayout(checks, 1, 2, 1, 3)
|
||||||
|
proc.setColumnStretch(4, 1)
|
||||||
rl.addLayout(proc)
|
rl.addLayout(proc)
|
||||||
info = QHBoxLayout()
|
info = QHBoxLayout()
|
||||||
self.lbl_new = QLabel()
|
self.lbl_new = QLabel()
|
||||||
@@ -200,7 +228,6 @@ class EditorPanel(QWidget):
|
|||||||
info.addWidget(self.lbl_warn, 1)
|
info.addWidget(self.lbl_warn, 1)
|
||||||
rl.addLayout(info)
|
rl.addLayout(info)
|
||||||
# apply (belongs to the replacement) ------------------------------------------------------
|
# apply (belongs to the replacement) ------------------------------------------------------
|
||||||
act = QHBoxLayout()
|
|
||||||
self.b_apply = self._btn("Apply to game", "apply", self.apply,
|
self.b_apply = self._btn("Apply to game", "apply", self.apply,
|
||||||
"Encode the replacement and install it in the game files (Ctrl+S)", "#ffffff")
|
"Encode the replacement and install it in the game files (Ctrl+S)", "#ffffff")
|
||||||
self.b_apply.setObjectName("Primary")
|
self.b_apply.setObjectName("Primary")
|
||||||
@@ -209,9 +236,9 @@ class EditorPanel(QWidget):
|
|||||||
"Save the original (or the replacement) as a WAV file")
|
"Save the original (or the replacement) as a WAV file")
|
||||||
self.busy = Spinner(16)
|
self.busy = Spinner(16)
|
||||||
self.lbl_busy = QLabel()
|
self.lbl_busy = QLabel()
|
||||||
for w in (self.b_apply, self.b_revert, self.b_export, self.busy, self.lbl_busy):
|
info.addWidget(self.busy, 0, Qt.AlignmentFlag.AlignTop) # encoding/writing: next to the status
|
||||||
act.addWidget(w)
|
info.addWidget(self.lbl_busy, 0, Qt.AlignmentFlag.AlignTop)
|
||||||
act.addStretch(1)
|
act = self._fill((self.b_apply, 2), (self.b_revert, 1), (self.b_export, 1))
|
||||||
rl.addStretch(1)
|
rl.addStretch(1)
|
||||||
rl.addLayout(act)
|
rl.addLayout(act)
|
||||||
replace = g
|
replace = g
|
||||||
@@ -220,12 +247,13 @@ class EditorPanel(QWidget):
|
|||||||
from .textpanel import SubtitleBox
|
from .textpanel import SubtitleBox
|
||||||
gs = QGroupBox("SUBTITLE")
|
gs = QGroupBox("SUBTITLE")
|
||||||
gsl = QVBoxLayout(gs)
|
gsl = QVBoxLayout(gs)
|
||||||
|
gsl.setContentsMargins(8, 8, 8, 8)
|
||||||
self.subtitle = SubtitleBox(self.ctx, gs)
|
self.subtitle = SubtitleBox(self.ctx, gs)
|
||||||
gsl.addWidget(self.subtitle)
|
gsl.addWidget(self.subtitle, 1)
|
||||||
gsl.addStretch(1)
|
|
||||||
|
|
||||||
self.columns = QSplitter(Qt.Orientation.Horizontal)
|
self.columns = QSplitter(Qt.Orientation.Horizontal)
|
||||||
self.columns.setChildrenCollapsible(False)
|
self.columns.setChildrenCollapsible(False)
|
||||||
|
self.columns.setHandleWidth(4)
|
||||||
for w, stretch in ((listen, 3), (replace, 4), (gs, 3)):
|
for w, stretch in ((listen, 3), (replace, 4), (gs, 3)):
|
||||||
self.columns.addWidget(w)
|
self.columns.addWidget(w)
|
||||||
self.columns.setStretchFactor(self.columns.count() - 1, stretch)
|
self.columns.setStretchFactor(self.columns.count() - 1, stretch)
|
||||||
|
|||||||
@@ -143,12 +143,16 @@ class SubtitleBox(QWidget):
|
|||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.win = win
|
self.win = win
|
||||||
self.keys = []
|
self.keys = []
|
||||||
self.lay = QFormLayout(self)
|
outer = QVBoxLayout(self)
|
||||||
|
outer.setContentsMargins(0, 0, 0, 0)
|
||||||
|
outer.setSpacing(6)
|
||||||
|
self.lay = QFormLayout() # one line edit per subtitle line
|
||||||
self.lay.setContentsMargins(0, 0, 0, 0)
|
self.lay.setContentsMargins(0, 0, 0, 0)
|
||||||
self.edits = []
|
self.edits = []
|
||||||
self.lbl = QLabel()
|
self.lbl = QLabel()
|
||||||
self.lbl.setObjectName("Muted")
|
self.lbl.setObjectName("Muted")
|
||||||
row = QHBoxLayout()
|
row = QHBoxLayout()
|
||||||
|
row.setSpacing(6)
|
||||||
self.b_apply = QPushButton(icons.icon("apply"), "Apply")
|
self.b_apply = QPushButton(icons.icon("apply"), "Apply")
|
||||||
self.b_revert = QPushButton(icons.icon("revert"), "Revert")
|
self.b_revert = QPushButton(icons.icon("revert"), "Revert")
|
||||||
self.b_open = QPushButton(icons.icon("search"), "Show in list")
|
self.b_open = QPushButton(icons.icon("search"), "Show in list")
|
||||||
@@ -158,16 +162,14 @@ class SubtitleBox(QWidget):
|
|||||||
self.b_apply.clicked.connect(self.apply)
|
self.b_apply.clicked.connect(self.apply)
|
||||||
self.b_revert.clicked.connect(self.revert)
|
self.b_revert.clicked.connect(self.revert)
|
||||||
self.b_open.clicked.connect(lambda: self.keys and self.open_in_list.emit(self.keys[0]))
|
self.b_open.clicked.connect(lambda: self.keys and self.open_in_list.emit(self.keys[0]))
|
||||||
for b in (self.b_apply, self.b_revert):
|
for b in (self.b_apply, self.b_revert): # two halves of the column, like the sound's action bar
|
||||||
row.addWidget(b)
|
row.addWidget(b, 1)
|
||||||
row.addStretch(1)
|
|
||||||
row2 = QHBoxLayout()
|
|
||||||
row2.addWidget(self.b_open)
|
|
||||||
row2.addStretch(1)
|
|
||||||
self.lbl.setWordWrap(True)
|
self.lbl.setWordWrap(True)
|
||||||
self.lay.addRow(self.lbl)
|
outer.addLayout(self.lay)
|
||||||
self.lay.addRow(row)
|
outer.addWidget(self.lbl) # room counter
|
||||||
self.lay.addRow(row2)
|
outer.addWidget(self.b_open) # the way to the Subtitles tab, across the column
|
||||||
|
outer.addStretch(1) # actions at the bottom, level with the sound's
|
||||||
|
outer.addLayout(row)
|
||||||
|
|
||||||
def set_sound(self, s):
|
def set_sound(self, s):
|
||||||
for e in self.edits:
|
for e in self.edits:
|
||||||
|
|||||||
@@ -149,8 +149,13 @@ copy per voice language) is filtered the same way, so the sound list never mixes
|
|||||||
|
|
||||||
## 7. Editor UX choices
|
## 7. Editor UX choices
|
||||||
- One window with three tabs. *Sounds*: filters (left) and sound table on top, the editor of the selected
|
- One window with three tabs. *Sounds*: filters (left) and sound table on top, the editor of the selected
|
||||||
sound below across the full width, in three resizable columns (Listen | Replace | Subtitle) so nothing has
|
sound below across the full width, in three resizable columns (Listen | Replace | Subtitle). Inside each
|
||||||
to scroll at usual window sizes. *Subtitles* and *Menu text*: search, filters, table, editor. The primary flow
|
column the controls are aligned, not packed in rows that wrap: Replace's edit fields in a grid with one
|
||||||
|
label column, and every column's actions in one full-width bar at its bottom, level across the three
|
||||||
|
columns (Listen: Original | Installed | Stop; Replace: Apply to game, twice as wide, | Revert | Save WAV;
|
||||||
|
Subtitle: Apply | Revert). Every row of buttons fills its column's width (Load file / device / Record; Trim
|
||||||
|
silence / Replacement; Show in list). A/B and Loop are icon buttons with tooltips next to the volume. The editor fits a 1000-pixel-wide window without horizontal scrolling (measured:
|
||||||
|
minimum 984 px with the window's vertical scroll bar). *Subtitles* and *Menu text*: search, filters, table, editor. The primary flow
|
||||||
(search -> select -> Space to listen -> Load file/Record -> Apply) needs no menus; power features (tags,
|
(search -> select -> Space to listen -> Load file/Record -> Apply) needs no menus; power features (tags,
|
||||||
favorites, multi-select revert, mod packages, settings) are in the toolbar and context menu.
|
favorites, multi-select revert, mod packages, settings) are in the toolbar and context menu.
|
||||||
- Long work (loading, encoding, writing 0.5 GB WADs) runs in background jobs with status messages; the
|
- Long work (loading, encoding, writing 0.5 GB WADs) runs in background jobs with status messages; the
|
||||||
|
|||||||
@@ -91,6 +91,12 @@ and the game ties the recordings together itself:
|
|||||||
identical, e.g. `Speech_STEEL_A1_CH2_Thug_behinddoormuffled` and `DIA_SteelA1Muffled`): the second place
|
identical, e.g. `Speech_STEEL_A1_CH2_Thug_behinddoormuffled` and `DIA_SteelA1Muffled`): the second place
|
||||||
is a copy of the line (same `line`, keeps its position name) and is written and reverted with it, because
|
is a copy of the line (same `line`, keeps its position name) and is written and reverted with it, because
|
||||||
the game plays whichever place the loaded package's table names.
|
the game plays whichever place the loaded package's table names.
|
||||||
|
- Not every line has its own recording in every language; the game shares some itself: in each voice WAD
|
||||||
|
616-651 lines are byte-identical to the INT recording (ITA 651: efforts and emotes 509, cinematics 117,
|
||||||
|
boss fights 10, story 8, radio/TV 4, character lines 2), 167 in-memory voice lines are stored once for every
|
||||||
|
language (efforts, radio chatter, crowds) and 6 DLC intro lines are one recording shared by INT/FRA/ITA(/ESN).
|
||||||
|
So some lines play the English recording in every language: that is the game's own data, which the catalog
|
||||||
|
shows as it is.
|
||||||
- Independent checks (no BKPK involved): every WAD recording is named as a source by a Sound object of a bank
|
- Independent checks (no BKPK involved): every WAD recording is named as a source by a Sound object of a bank
|
||||||
of its own language (54,055 of 54,055); every stored head or whole copy equals the start of the recording
|
of its own language (54,055 of 54,055); every stored head or whole copy equals the start of the recording
|
||||||
its line gives it (2,185 of 2,185); a line's recordings have similar lengths across languages (11,761 of 12,434
|
its line gives it (2,185 of 2,185); a line's recordings have similar lengths across languages (11,761 of 12,434
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 219 KiB After Width: | Height: | Size: 215 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 45 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 191 KiB After Width: | Height: | Size: 187 KiB |
@@ -25,6 +25,9 @@ game is set to (Steam > game properties > Language) and applies to everything yo
|
|||||||
editor shows those and says *voices: INT* next to the list.
|
editor shows those and says *voices: INT* next to the list.
|
||||||
- **Subtitles** and **Menu text**: the texts of that language.
|
- **Subtitles** and **Menu text**: the texts of that language.
|
||||||
Changing it reloads the lists; the next start is again in the game's language.
|
Changing it reloads the lists; the next start is again in the game's language.
|
||||||
|
Some voice lines sound English in every language: the game itself ships the English recording for most
|
||||||
|
efforts and grunts, many cinematics and a few other lines, and some in-memory lines exist only once for all
|
||||||
|
languages. The editor shows what each language's game files contain.
|
||||||
|
|
||||||
## Names
|
## Names
|
||||||
Every voice line has one name in every language, taken from the game itself (e.g. `Speech_TAPE_JOKER_1_04`, the
|
Every voice line has one name in every language, taken from the game itself (e.g. `Speech_TAPE_JOKER_1_04`, the
|
||||||
@@ -56,10 +59,11 @@ the same line and are changed and reverted together.
|
|||||||
## Listen
|
## Listen
|
||||||
The editor under the list shows the selected sound in three columns side by side (drag the dividers to
|
The editor under the list shows the selected sound in three columns side by side (drag the dividers to
|
||||||
resize them): **Listen**, **Replace** and **Subtitle**. Select a row and press **Space** (or double-click) to
|
resize them): **Listen**, **Replace** and **Subtitle**. Select a row and press **Space** (or double-click) to
|
||||||
play the original. In **Listen**:
|
play the original. Each column ends with its own buttons on one line. In **Listen**:
|
||||||
- **Original** / **Installed** play the game's original and what is currently in the game. Click on a
|
- **Original** / **Installed** play the game's original and what is currently in the game; **Stop** (Esc).
|
||||||
waveform to play from that point. **Loop**, **Volume** and **Stop** (Esc) work for everything.
|
Click on a waveform to play from that point.
|
||||||
- **A/B** switches between the original and your replacement at the same position, to compare.
|
- Above them: **A/B** (switch between the original and your replacement at the same position, to compare),
|
||||||
|
**Loop**, **Volume** and the playback position. The icon buttons show their names when you point at them.
|
||||||
|
|
||||||
## Replace
|
## Replace
|
||||||

|

|
||||||
|
|||||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "ackit"
|
name = "ackit"
|
||||||
version = "3.2.2"
|
version = "3.2.3"
|
||||||
description = "Batman: Arkham City (PC) audio toolkit and editor"
|
description = "Batman: Arkham City (PC) audio toolkit and editor"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.10"
|
requires-python = ">=3.10"
|
||||||
|
|||||||
@@ -137,6 +137,18 @@ def catalog_claims(cat):
|
|||||||
claim("REPORT.md", "recordings stored twice (%s per language) are written together" % rng(copies.values(), str))
|
claim("REPORT.md", "recordings stored twice (%s per language) are written together" % rng(copies.values(), str))
|
||||||
claim("docs/formats/banks.md", "- %s recordings per language are stored twice" % rng(copies.values(), str).capitalize())
|
claim("docs/formats/banks.md", "- %s recordings per language are stored twice" % rng(copies.values(), str).capitalize())
|
||||||
claim("docs/categories.md", "Coverage of the %s WAD voice lines" % n(WL))
|
claim("docs/categories.md", "Coverage of the %s WAD voice lines" % n(WL))
|
||||||
|
# recordings the game shares between languages (the same bytes as the INT recording of the line)
|
||||||
|
intl = {s.line: s.sha1 for s in rows["INT"] if s.line}
|
||||||
|
same = {L: [s for s in {x.line: x for x in rows[L] if x.line}.values() if intl.get(s.line) == s.sha1]
|
||||||
|
for L in ("FRA", "ITA", "DEU", "ESN")}
|
||||||
|
ita = collections.Counter(s.category for s in same["ITA"])
|
||||||
|
shared = {s.target: s.language for s in bank if ";" in s.language}
|
||||||
|
claim("docs/formats/banks.md", "in each voice WAD %s lines are byte-identical to the INT recording (ITA %d: efforts "
|
||||||
|
"and emotes %d, cinematics %d, boss fights %d, story %d, radio/TV %d, character lines %d), %d in-memory voice "
|
||||||
|
"lines are stored once for every language (efforts, radio chatter, crowds) and %d DLC intro lines"
|
||||||
|
% (rng([len(v) for v in same.values()]), len(same["ITA"]), ita["Efforts & emotes"], ita["Cinematics"],
|
||||||
|
ita["Boss fights"], ita["Story missions"], ita["Radio & TV"], ita["Character lines"],
|
||||||
|
sum(1 for s in bank if s.line and not s.language), len(shared)))
|
||||||
claim("docs/decisions.md", "of the %s WAD voice lines" % n(WL))
|
claim("docs/decisions.md", "of the %s WAD voice lines" % n(WL))
|
||||||
|
|
||||||
# positions differ between languages
|
# positions differ between languages
|
||||||
|
|||||||
Reference in New Issue
Block a user