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

181
app/preview.py Normal file
View File

@@ -0,0 +1,181 @@
"""Sounds tab: preview of the selected sound (listen, read its subtitle) with the way into the Edit tab. No
editing here: editing is the Edit tab's job."""
from PySide6.QtCore import Qt, Signal
from PySide6.QtWidgets import QGridLayout, QHBoxLayout, QLabel, QPushButton, QVBoxLayout, QWidget
from ackit import audio, subtitles, workspace as W
from . import icons, theme
from .editor import STATE_TEXT
from .waveform import Waveform
class PreviewPanel(QWidget):
edit_requested = Signal() # open the selected sounds in the Edit tab
add_requested = Signal() # add them to the edit list (stay here)
def __init__(self, ctx, parent=None):
super().__init__(parent)
self.ctx = ctx # main window: .ws, .runner, .player, .language
self.sound = None
self.orig = self.installed = None
self.playing_what = None
self._build()
self.set_sound(None)
def _build(self):
self.setObjectName("EditorPanel")
lay = QHBoxLayout(self)
lay.setContentsMargins(8, 6, 8, 6)
lay.setSpacing(12)
info = QVBoxLayout()
info.setSpacing(3)
self.title = QLabel()
self.title.setObjectName("EditorTitle")
self.title.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse)
f = self.title.font()
f.setPointSizeF(f.pointSizeF() * 1.2)
f.setBold(True)
self.title.setFont(f)
self.meta = QLabel()
self.meta.setObjectName("Muted")
self.meta.setWordWrap(True)
self.state = QLabel()
self.sub = QLabel()
self.sub.setWordWrap(True)
self.sub.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse)
for w in (self.title, self.meta, self.state, self.sub):
info.addWidget(w)
info.addStretch(1)
lay.addLayout(info, 3)
# player: the transport buttons together, the waveforms under them (click = play from there)
player = QVBoxLayout()
player.setSpacing(4)
tr = QHBoxLayout()
tr.setSpacing(6)
self.b_orig = self._btn("Play original", "play", lambda: self.play("orig"), "Play the game's original (Space)",
icons.STEEL)
self.b_inst = self._btn("Play installed", "play", lambda: self.play("inst"),
"Play what the game has now (your replacement)", "#8f4a4e")
self.b_stop = self._btn("Stop", "stop", self.stop, "Stop (Esc)")
for b in (self.b_orig, self.b_inst, self.b_stop):
tr.addWidget(b)
tr.addStretch(1)
player.addLayout(tr)
waves = QGridLayout()
waves.setVerticalSpacing(3)
waves.setHorizontalSpacing(6)
self.wf_orig, self.wf_inst = Waveform(), Waveform()
for i, (w, kind, name) in enumerate(((self.wf_orig, "orig", "Original"), (self.wf_inst, "inst", "Installed"))):
w.accent, w.bg = theme.WAVE[kind], theme.WAVE_BG
w.setMinimumHeight(40)
lab = QLabel(name)
lab.setObjectName("Muted")
waves.addWidget(lab, i, 0)
waves.addWidget(w, i, 1)
waves.setColumnStretch(1, 1)
player.addLayout(waves)
self.wf_orig.seekRequested.connect(lambda t: self.play("orig", t))
self.wf_inst.seekRequested.connect(lambda t: self.play("inst", t))
lay.addLayout(player, 4)
# editing: into the Edit tab (on the right, apart from the player)
acts = QVBoxLayout()
acts.setSpacing(6)
self.b_edit = self._btn("Edit", "edit", self.edit_requested.emit,
"Open the selected sound(s) in the Edit tab (Enter)", "#ffffff")
self.b_edit.setObjectName("Primary")
self.b_add = self._btn("Add to edit list", "add", self.add_requested.emit,
"Put the selected sounds on the Edit tab's list and stay here")
for b in (self.b_edit, self.b_add):
acts.addWidget(b)
acts.addStretch(1)
lay.addSpacing(8)
lay.addLayout(acts, 1)
self.ctx.player.position.connect(self._pos)
self.ctx.player.playing.connect(lambda on: None if on else self._clear_playheads())
@staticmethod
def _btn(text, icon, slot, tip, color=None):
b = QPushButton(text)
b.setIcon(icons.icon(icon, color or icons.TEXT))
b.clicked.connect(lambda _=False: slot())
b.setToolTip(tip)
return b
def set_sound(self, s):
self.stop()
self.sound, self.orig, self.installed = s, None, None
for b in (self.b_orig, self.b_inst, self.b_edit, self.b_add, self.b_stop):
b.setEnabled(s is not None)
self.wf_orig.set_audio(None, 1, "select a sound" if s is None else "loading…", busy=s is not None)
self.wf_inst.set_audio(None, 1, "")
if s is None:
self.title.setText("No sound selected")
self.meta.setText("Select a sound in the list to listen to it; Edit opens it for changing.")
self.state.setText("")
self.sub.setText("")
return
self.title.setText(s.name)
who = s.character or (("mentions " + s.characters_inferred) if s.characters_inferred else "—")
self.meta.setText("%s / %s • %s • %.2f s, %d Hz, %s • %s"
% (s.group, s.category, who, s.seconds, s.rate,
{1: "mono", 2: "stereo"}.get(s.channels, "%d ch" % s.channels), s.key))
ws = self.ctx.ws
subs = ws.texts.for_sound(s, self.ctx.language) if ws else []
self.sub.setText("“%s”" % " / ".join(ws.texts.current(_key(x)) or x.text for x in subs) if subs else "")
key = s.key
def work(job):
cur = ws.current_bytes(s)
st = ws.state(s, cur)
o = audio.decode_wem(ws.original_bytes(s))
return key, st, o, audio.decode_wem(cur) if st != W.ORIGINAL else None
def failed(e, tb):
self.wf_orig.set_audio(None, 1, "could not load: %s" % e)
self.ctx.runner.start(work, self._loaded, failed)
def _loaded(self, r):
key, st, o, inst = r
if not self.sound or self.sound.key != key:
return
self.orig, self.installed = o, inst
self.wf_orig.set_audio(*o)
self.wf_inst.set_audio(*(inst or (None, 1)), placeholder="same as the original")
self.b_inst.setEnabled(inst is not None)
self.state.setText(STATE_TEXT.get(st, st))
def play(self, what, start=0.0):
a = {"orig": self.orig, "inst": self.installed}.get(what)
if not a:
return
self.playing_what = what
self.ctx.player.owner = self
self.ctx.player.play(a[0], a[1], start, label=what)
def toggle_play(self):
if self.ctx.player.is_playing():
self.stop()
else:
self.play("orig")
def stop(self):
self.ctx.player.stop()
self._clear_playheads()
def _clear_playheads(self):
for w in (self.wf_orig, self.wf_inst):
w.set_playhead(None)
def _pos(self, t):
if getattr(self.ctx.player, "owner", None) is not self:
return
wf = {"orig": self.wf_orig, "inst": self.wf_inst}.get(self.playing_what)
if wf:
wf.set_playhead(t)
def _key(sub):
return subtitles.key(sub.lang, sub.speech, sub.line)