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:
258
app/editor.py
258
app/editor.py
@@ -1,14 +1,16 @@
|
||||
"""Single-sound editor: listen, substitute (file or recording), process, apply / revert."""
|
||||
"""Single-sound editor of the Edit tab: listen, replace (file or recording), process with effects (`effects`,
|
||||
ackit/fx.py, between the trim and the loudness steps), apply / revert."""
|
||||
import json
|
||||
import os
|
||||
|
||||
import numpy as np
|
||||
from PySide6.QtCore import Qt, QTimer, Signal
|
||||
from PySide6.QtGui import QFont
|
||||
from PySide6.QtWidgets import (QCheckBox, QComboBox, QDoubleSpinBox, QFileDialog, QGridLayout, QGroupBox, QSplitter,
|
||||
QHBoxLayout, QLabel, QMessageBox, QProgressBar, QPushButton, QSlider,
|
||||
from PySide6.QtWidgets import (QCheckBox, QComboBox, QDoubleSpinBox, QFileDialog, QGridLayout, QGroupBox,
|
||||
QHBoxLayout, QLabel, QMessageBox, QProgressBar, QPushButton, QSizePolicy, QSlider,
|
||||
QVBoxLayout, QWidget)
|
||||
|
||||
from ackit import audio, workspace as W
|
||||
from ackit import audio, fx, workspace as W
|
||||
from . import icons, theme
|
||||
from .audioio import Recorder, input_devices, find_device
|
||||
from .spinner import Spinner
|
||||
@@ -29,6 +31,8 @@ class EditorPanel(QWidget):
|
||||
applied = Signal(str) # sound key
|
||||
reverted = Signal(str)
|
||||
message = Signal(str)
|
||||
source_changed = Signal() # a new replacement source (file, recording)
|
||||
edited = Signal() # trim / gain / normalize / loudness changed
|
||||
|
||||
def __init__(self, ctx, parent=None):
|
||||
super().__init__(parent)
|
||||
@@ -39,6 +43,10 @@ class EditorPanel(QWidget):
|
||||
self.src = None # replacement source (x, rate) before processing
|
||||
self.src_name = ""
|
||||
self.playing_what = None
|
||||
self.effects = None # effects settings (ackit.fx) or None
|
||||
self._src_gen = 0 # identity of the current source (effects cache key)
|
||||
self._fx_cache = (None, None) # (key, rendered audio)
|
||||
self._fx_pending = None
|
||||
self.recorder = Recorder(self)
|
||||
self._build()
|
||||
self.set_sound(None)
|
||||
@@ -66,14 +74,17 @@ class EditorPanel(QWidget):
|
||||
def _wave(self, kind, selectable=False):
|
||||
w = Waveform(selectable=selectable)
|
||||
w.accent, w.bg = theme.WAVE[kind], theme.WAVE_BG
|
||||
w.setMinimumHeight(44) # three rows + the rest fit the Edit tab without scrolling
|
||||
w.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) # and grow with the window
|
||||
return w
|
||||
|
||||
def _build(self):
|
||||
"""Header on top; below, side by side (resizable): LISTEN | REPLACE (with the apply buttons) | SUBTITLE."""
|
||||
"""Top to bottom: header, SUBTITLE (when the sound has one), LISTEN (original, installed and replacement,
|
||||
each with its play button), REPLACEMENT (record / load, trim, level), then the action bar."""
|
||||
self.setObjectName("EditorPanel")
|
||||
lay = QVBoxLayout(self)
|
||||
lay.setContentsMargins(8, 6, 8, 6)
|
||||
lay.setSpacing(4)
|
||||
lay.setContentsMargins(8, 4, 8, 2)
|
||||
lay.setSpacing(3)
|
||||
self.title = QLabel()
|
||||
self.title.setObjectName("EditorTitle")
|
||||
self.title.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse)
|
||||
@@ -93,25 +104,41 @@ class EditorPanel(QWidget):
|
||||
self.meta.setObjectName("Muted")
|
||||
lay.addWidget(self.meta)
|
||||
|
||||
# listen ----------------------------------------------------------------------------------
|
||||
# the subtitle, large ------------------------------------------------------------------------
|
||||
from .textpanel import SubtitlePanel
|
||||
self.subtitle_box = QGroupBox("SUBTITLE")
|
||||
sl = QVBoxLayout(self.subtitle_box)
|
||||
sl.setContentsMargins(8, 8, 8, 8)
|
||||
self.subtitle = SubtitlePanel(self.ctx, self.subtitle_box)
|
||||
sl.addWidget(self.subtitle)
|
||||
lay.addWidget(self.subtitle_box)
|
||||
|
||||
# listen: one row per version, its play button on the left -----------------------------------
|
||||
g = QGroupBox("LISTEN")
|
||||
gl = QGridLayout(g)
|
||||
gl.setContentsMargins(8, 8, 8, 8)
|
||||
gl.setVerticalSpacing(4)
|
||||
gl.setHorizontalSpacing(8)
|
||||
self.wf_orig = self._wave("orig")
|
||||
self.wf_inst = self._wave("inst")
|
||||
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.b_play_orig = self._btn("Original", "play", lambda: self.play("orig"),
|
||||
"Play the original (double-click the row; Space until a replacement is loaded)",
|
||||
icons.STEEL)
|
||||
self.b_play_inst = self._btn("Installed", "play", lambda: self.play("inst"), "Play what the game has now", "#8f4a4e")
|
||||
"Play the game's original (Space plays the replacement once there is one)", icons.STEEL)
|
||||
self.b_play_inst = self._btn("Installed", "play", lambda: self.play("inst"), "Play what the game has now",
|
||||
"#8f4a4e")
|
||||
self.b_play_new = self._btn("Replacement", "play", lambda: self.play("new"),
|
||||
"Play the replacement as it will be applied (trim, effects, level)", icons.ACCENT)
|
||||
for i, (b, w) in enumerate(((self.b_play_orig, self.wf_orig), (self.b_play_inst, self.wf_inst),
|
||||
(self.b_play_new, self.wf_new))):
|
||||
b.setMinimumWidth(110)
|
||||
gl.addWidget(b, i, 0)
|
||||
gl.addWidget(w, i, 1)
|
||||
self.b_stop = self._btn("Stop", "stop", self.stop, "Stop (Esc)")
|
||||
# playback options next to the volume: icons with tooltips
|
||||
self.b_ab = self._btn("", "ab", self.ab_switch, "A/B: switch between the original and the replacement at "
|
||||
"the same position", icons.ACCENT)
|
||||
self.cb_loop = self._btn("", "loop", None, "Loop: repeat playback")
|
||||
self.b_ab = self._btn("A/B", "ab", self.ab_switch, "Switch between the original and the replacement at the same "
|
||||
"position, to compare", icons.ACCENT)
|
||||
self.cb_loop = self._btn("Loop", "loop", None, "Repeat playback")
|
||||
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.setRange(0, 100)
|
||||
self.sl_vol.setValue(80)
|
||||
@@ -122,71 +149,65 @@ class EditorPanel(QWidget):
|
||||
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(self.wf_orig, 0, 1)
|
||||
self.lbl_inst = QLabel("Installed")
|
||||
gl.addWidget(self.lbl_inst, 1, 0)
|
||||
gl.addWidget(self.wf_inst, 1, 1)
|
||||
gl.setRowStretch(2, 1) # controls at the bottom, level with the other columns' actions
|
||||
gl.addLayout(self._fill((self.b_play_orig, 1), (self.b_play_inst, 1), (self.b_stop, 1)), 4, 0, 1, 2)
|
||||
vol = QHBoxLayout() # playback options: A/B, loop, volume, position
|
||||
vol.setSpacing(6)
|
||||
vol.addWidget(self.b_ab)
|
||||
vol.addWidget(self.cb_loop)
|
||||
vol.addSpacing(6)
|
||||
vol.addWidget(QLabel("Volume"))
|
||||
vol.addWidget(self.sl_vol, 1)
|
||||
vol.addWidget(self.lbl_time)
|
||||
gl.addLayout(vol, 3, 0, 1, 2)
|
||||
tr = QHBoxLayout()
|
||||
tr.setSpacing(6)
|
||||
for w in (self.b_stop, self.b_ab, self.cb_loop):
|
||||
tr.addWidget(w)
|
||||
tr.addSpacing(10)
|
||||
tr.addWidget(QLabel("Volume"))
|
||||
tr.addWidget(self.sl_vol, 1)
|
||||
tr.addWidget(self.lbl_time)
|
||||
gl.addLayout(tr, 3, 0, 1, 2)
|
||||
gl.setColumnStretch(1, 1)
|
||||
self.wf_orig.seekRequested.connect(lambda t: self.play("orig", t))
|
||||
self.wf_inst.seekRequested.connect(lambda t: self.play("inst", t))
|
||||
listen = g
|
||||
self.wf_new.seekRequested.connect(lambda t: self.play("new", t))
|
||||
self.wf_new.selectionChanged.connect(self._selection)
|
||||
for r in range(3):
|
||||
gl.setRowStretch(r, 1) # the waveforms take the free height of the window
|
||||
lay.addWidget(g, 1)
|
||||
|
||||
# replace ---------------------------------------------------------------------------------
|
||||
g = QGroupBox("REPLACE")
|
||||
# replacement: source, trim, level -----------------------------------------------------------
|
||||
g = QGroupBox("REPLACEMENT")
|
||||
rl = QVBoxLayout(g)
|
||||
rl.setContentsMargins(8, 8, 8, 8)
|
||||
rl.setSpacing(6)
|
||||
src = QHBoxLayout()
|
||||
src.setSpacing(6)
|
||||
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)")
|
||||
self.b_rec = self._btn("Record", "record", self.toggle_record, "Record from the selected input")
|
||||
self.b_rec.setObjectName("Primary")
|
||||
self.b_rec.setMinimumWidth(150)
|
||||
self.cmb_in = QComboBox()
|
||||
self.cmb_in.setMinimumWidth(110)
|
||||
self.cmb_in.setToolTip("Recording device")
|
||||
self.b_rec = self._btn("Record", "record", self.toggle_record, "Record from the selected input")
|
||||
self.meter = QProgressBar()
|
||||
self.meter.setRange(0, 100)
|
||||
self.meter.setTextVisible(False)
|
||||
self.meter.setFixedWidth(60)
|
||||
self.meter.setFixedWidth(80)
|
||||
self.meter.setToolTip("Input level")
|
||||
self.lbl_rec = QLabel("")
|
||||
self.lbl_rec.setObjectName("Muted")
|
||||
src.addWidget(self.b_load, 1) # the row fills the column: file | microphone, level
|
||||
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)")
|
||||
src.addWidget(self.b_rec)
|
||||
src.addWidget(self.cmb_in, 1)
|
||||
src.addWidget(self.meter)
|
||||
src.addWidget(self.lbl_rec)
|
||||
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(self.b_load)
|
||||
rl.addLayout(src)
|
||||
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.seekRequested.connect(lambda t: self.play("new", t))
|
||||
self.wf_new.selectionChanged.connect(self._selection)
|
||||
rl.addWidget(self.wf_new)
|
||||
self.sp_a = QDoubleSpinBox()
|
||||
self.sp_b = QDoubleSpinBox()
|
||||
for s_ in (self.sp_a, self.sp_b):
|
||||
s_.setDecimals(2)
|
||||
s_.setSuffix(" s")
|
||||
s_.setRange(0, 36000)
|
||||
s_.setMaximumWidth(80)
|
||||
s_.setMaximumWidth(90)
|
||||
s_.valueChanged.connect(self._changed)
|
||||
self.sp_gain = QDoubleSpinBox()
|
||||
self.sp_gain.setRange(-40, 40)
|
||||
self.sp_gain.setSuffix(" dB")
|
||||
self.sp_gain.setMaximumWidth(80)
|
||||
self.sp_gain.setMaximumWidth(90)
|
||||
self.sp_gain.valueChanged.connect(self._changed)
|
||||
self.cb_norm = QCheckBox("Normalize")
|
||||
self.cb_match = QCheckBox("Match loudness")
|
||||
@@ -194,17 +215,14 @@ class EditorPanel(QWidget):
|
||||
self.cb_norm.toggled.connect(self._changed)
|
||||
self.cb_match.toggled.connect(self._changed)
|
||||
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",
|
||||
icons.ACCENT)
|
||||
# edit controls in a grid: labels in one column, the fields aligned, playback on the right
|
||||
proc = QGridLayout()
|
||||
proc = QGridLayout() # labels in one column, the fields aligned
|
||||
proc.setHorizontalSpacing(6)
|
||||
proc.setVerticalSpacing(6)
|
||||
proc.addWidget(QLabel("Keep"), 0, 0)
|
||||
proc.addWidget(self.sp_a, 0, 1)
|
||||
proc.addWidget(QLabel("to"), 0, 2, Qt.AlignmentFlag.AlignCenter)
|
||||
proc.addWidget(self.sp_b, 0, 3)
|
||||
proc.addLayout(self._fill((self.b_silence, 1), (self.b_play_new, 1)), 0, 4) # the rest of the width
|
||||
proc.addWidget(self.b_silence, 0, 4)
|
||||
proc.addWidget(QLabel("Gain"), 1, 0)
|
||||
proc.addWidget(self.sp_gain, 1, 1)
|
||||
checks = QHBoxLayout()
|
||||
@@ -213,7 +231,7 @@ class EditorPanel(QWidget):
|
||||
checks.addWidget(self.cb_match)
|
||||
checks.addStretch(1)
|
||||
proc.addLayout(checks, 1, 2, 1, 3)
|
||||
proc.setColumnStretch(4, 1)
|
||||
proc.setColumnStretch(5, 1)
|
||||
rl.addLayout(proc)
|
||||
info = QHBoxLayout()
|
||||
self.lbl_new = QLabel()
|
||||
@@ -229,37 +247,33 @@ class EditorPanel(QWidget):
|
||||
info.addWidget(self.warn_icon, 0, Qt.AlignmentFlag.AlignTop)
|
||||
info.addWidget(self.lbl_warn, 1)
|
||||
rl.addLayout(info)
|
||||
# apply (belongs to the replacement) ------------------------------------------------------
|
||||
lay.addWidget(g)
|
||||
|
||||
# actions -----------------------------------------------------------------------------------
|
||||
self.b_prev = self._btn("Previous", "prev", None, "Previous sound of the edit list (Alt+Left)")
|
||||
self.b_next = self._btn("Next", "next", None, "Next sound of the edit list (Alt+Right)")
|
||||
self.b_apply = self._btn("Apply to game", "apply", self.apply,
|
||||
"Encode the replacement and install it in the game files (Ctrl+S)", "#ffffff")
|
||||
self.b_apply.setObjectName("Primary")
|
||||
self.b_revert = self._btn("Revert", "revert", self.revert, "Put the game's original back")
|
||||
self.b_export = self._btn("Save WAV…", "save", self.export_wav,
|
||||
"Save the original (or the replacement) as a WAV file")
|
||||
"Save the original (or the replacement as applied) as a WAV file")
|
||||
self.busy = Spinner(16)
|
||||
self.lbl_busy = QLabel()
|
||||
info.addWidget(self.busy, 0, Qt.AlignmentFlag.AlignTop) # encoding/writing: next to the status
|
||||
info.addWidget(self.lbl_busy, 0, Qt.AlignmentFlag.AlignTop)
|
||||
act = self._fill((self.b_apply, 2), (self.b_revert, 1), (self.b_export, 1))
|
||||
rl.addStretch(1)
|
||||
rl.addLayout(act)
|
||||
replace = g
|
||||
|
||||
# subtitle of this sound (edited side by side with the audio) -------------------------------
|
||||
from .textpanel import SubtitleBox
|
||||
gs = QGroupBox("SUBTITLE")
|
||||
gsl = QVBoxLayout(gs)
|
||||
gsl.setContentsMargins(8, 8, 8, 8)
|
||||
self.subtitle = SubtitleBox(self.ctx, gs)
|
||||
gsl.addWidget(self.subtitle, 1)
|
||||
|
||||
self.columns = QSplitter(Qt.Orientation.Horizontal)
|
||||
self.columns.setChildrenCollapsible(False)
|
||||
self.columns.setHandleWidth(4)
|
||||
for w, stretch in ((listen, 3), (replace, 4), (gs, 3)):
|
||||
self.columns.addWidget(w)
|
||||
self.columns.setStretchFactor(self.columns.count() - 1, stretch)
|
||||
lay.addWidget(self.columns, 1)
|
||||
self.action_bar = QWidget() # the Edit tab keeps it below its scroll area (always visible)
|
||||
act = QHBoxLayout(self.action_bar)
|
||||
act.setContentsMargins(8, 4, 8, 6)
|
||||
act.setSpacing(6)
|
||||
act.addWidget(self.b_prev, 1)
|
||||
act.addSpacing(10)
|
||||
act.addWidget(self.b_apply, 3)
|
||||
act.addWidget(self.b_revert, 1)
|
||||
act.addWidget(self.b_export, 1)
|
||||
act.addWidget(self.busy)
|
||||
act.addWidget(self.lbl_busy)
|
||||
act.addSpacing(10)
|
||||
act.addWidget(self.b_next, 1)
|
||||
lay.addWidget(self.action_bar)
|
||||
|
||||
self.recorder.level.connect(lambda v: self.meter.setValue(int(min(1.0, v) * 100)))
|
||||
self.recorder.elapsed.connect(lambda t: self.lbl_rec.setText("recording %s" % fmt_s(t)))
|
||||
@@ -305,13 +319,16 @@ class EditorPanel(QWidget):
|
||||
self.recorder.stop()
|
||||
self.sound, self.orig, self.installed, self.src = s, None, None, None
|
||||
enabled = s is not None
|
||||
for w in self.findChildren(QPushButton) + [self.sp_a, self.sp_b, self.sp_gain, self.cb_norm, self.cb_match]:
|
||||
# the action bar may live outside this widget (the Edit tab keeps it below its scroll area)
|
||||
buttons = set(self.findChildren(QPushButton)) | set(self.action_bar.findChildren(QPushButton))
|
||||
for w in list(buttons) + [self.sp_a, self.sp_b, self.sp_gain, self.cb_norm, self.cb_match]:
|
||||
w.setEnabled(enabled)
|
||||
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, "")
|
||||
self.wf_new.set_audio(None, 1, "load a file or record a replacement")
|
||||
self.lbl_new.setText("")
|
||||
self.subtitle.set_sound(s)
|
||||
self.subtitle_box.setVisible(bool(self.subtitle.keys))
|
||||
if s is None:
|
||||
self.title.setText("No sound selected")
|
||||
self.meta.setText("Search and select a sound in the list.")
|
||||
@@ -380,8 +397,9 @@ class EditorPanel(QWidget):
|
||||
self.ctx.runner.start(lambda job: audio.load(path), done, failed)
|
||||
return result[0] if result else True
|
||||
|
||||
def set_source(self, x, sr, name):
|
||||
def set_source(self, x, sr, name, restoring=False):
|
||||
self.src, self.src_name = (np.asarray(x, np.float32), sr), name
|
||||
self._src_gen += 1
|
||||
dur = audio.duration(x, sr)
|
||||
self.sp_a.blockSignals(True)
|
||||
self.sp_b.blockSignals(True)
|
||||
@@ -392,6 +410,8 @@ class EditorPanel(QWidget):
|
||||
self.sp_a.blockSignals(False)
|
||||
self.sp_b.blockSignals(False)
|
||||
self._update_new()
|
||||
if not restoring:
|
||||
self.source_changed.emit()
|
||||
|
||||
def toggle_record(self):
|
||||
if self.recorder.is_recording():
|
||||
@@ -427,15 +447,65 @@ class EditorPanel(QWidget):
|
||||
|
||||
def _changed(self, *_):
|
||||
self._reproc.start()
|
||||
self.edited.emit()
|
||||
|
||||
def processed(self):
|
||||
"""Replacement audio after trim / gain / normalize / loudness matching -> (x, rate) or None."""
|
||||
if not self.src:
|
||||
return None
|
||||
def set_effects(self, settings):
|
||||
self.effects = settings
|
||||
self._changed()
|
||||
|
||||
def _fx_key(self):
|
||||
return (self._src_gen, self.sp_a.value(), self.sp_b.value(), json.dumps(self.effects, sort_keys=True),
|
||||
self.sound.channels if self.sound else None)
|
||||
|
||||
def _effected(self, render=True):
|
||||
"""Trimmed replacement with the effects (cached). With render=False: None while not rendered yet."""
|
||||
x, sr = self.src
|
||||
a, b = self.sp_a.value(), self.sp_b.value()
|
||||
if b > a:
|
||||
x = audio.trim(x, sr, a, b)
|
||||
if not fx.active(self.effects):
|
||||
return x
|
||||
key = self._fx_key()
|
||||
if self._fx_cache[0] == key:
|
||||
return self._fx_cache[1]
|
||||
if not render:
|
||||
return None
|
||||
y = fx.render(x, sr, self.effects, key[-1])
|
||||
self._fx_cache = (key, y)
|
||||
return y
|
||||
|
||||
def _render_effects(self):
|
||||
"""Render the effects in the background (spinner in the replacement waveform), then show the result."""
|
||||
key = self._fx_key()
|
||||
if self._fx_pending == key:
|
||||
return
|
||||
self._fx_pending = key
|
||||
x, sr = self.src
|
||||
a, b = self.sp_a.value(), self.sp_b.value()
|
||||
if b > a:
|
||||
x = audio.trim(x, sr, a, b)
|
||||
settings = self.effects
|
||||
self.wf_new.set_audio(None, 1, "applying effects…", busy=True)
|
||||
|
||||
def done(y):
|
||||
if self._fx_pending == key:
|
||||
self._fx_pending = None
|
||||
self._fx_cache = (key, y)
|
||||
if self.src and self._fx_key() == key:
|
||||
self._update_new()
|
||||
|
||||
def failed(e, tb):
|
||||
self._fx_pending = None
|
||||
self._failed("The effects failed", e)
|
||||
|
||||
self.ctx.runner.start(lambda job: fx.render(x, sr, settings, key[-1]), done, failed)
|
||||
|
||||
def processed(self):
|
||||
"""Replacement audio after trim / effects / gain / normalize / loudness matching -> (x, rate) or None."""
|
||||
if not self.src:
|
||||
return None
|
||||
sr = self.src[1]
|
||||
x = self._effected()
|
||||
if self.cb_match.isChecked() and self.orig is not None:
|
||||
x = audio.match_loudness(x, self.orig[0])
|
||||
if self.cb_norm.isChecked():
|
||||
@@ -445,6 +515,9 @@ class EditorPanel(QWidget):
|
||||
return x, sr
|
||||
|
||||
def _update_new(self):
|
||||
if self.src and self._effected(render=False) is None:
|
||||
self._render_effects()
|
||||
return
|
||||
p = self.processed()
|
||||
if p is None:
|
||||
self.wf_new.set_audio(None, 1, "load a file or record a replacement")
|
||||
@@ -455,7 +528,9 @@ class EditorPanel(QWidget):
|
||||
self.wf_new.set_audio(x, sr)
|
||||
dur = audio.duration(x, sr)
|
||||
s = self.sound
|
||||
self.lbl_new.setText("%s: %.2f s, peak %.1f dBFS" % (self.src_name, dur, audio.peak_db(x)))
|
||||
on = fx.summary(self.effects)
|
||||
self.lbl_new.setText("%s%s: %.2f s, peak %.1f dBFS" % (self.src_name, " + " + on if on else "", dur,
|
||||
audio.peak_db(x)))
|
||||
warnings = []
|
||||
if s and not s.is_pack_media and dur > s.seconds * 1.15 + 0.25:
|
||||
warnings.append("Longer than the original (%.2f s): it will be squeezed into the original's size "
|
||||
@@ -477,6 +552,7 @@ class EditorPanel(QWidget):
|
||||
if not a:
|
||||
return
|
||||
self.playing_what = what
|
||||
self.ctx.player.owner = self # two editors share the player: only this one shows playheads
|
||||
self.ctx.player.play(a[0], a[1], start, loop=self.cb_loop.isChecked(), label=what)
|
||||
|
||||
def toggle_play(self):
|
||||
@@ -500,6 +576,8 @@ class EditorPanel(QWidget):
|
||||
w.set_playhead(None)
|
||||
|
||||
def _pos(self, t):
|
||||
if getattr(self.ctx.player, "owner", self) is not self:
|
||||
return
|
||||
self.lbl_time.setText(fmt_s(t))
|
||||
wf = {"orig": self.wf_orig, "inst": self.wf_inst, "new": self.wf_new}.get(self.playing_what)
|
||||
if wf:
|
||||
@@ -535,6 +613,8 @@ class EditorPanel(QWidget):
|
||||
return
|
||||
ws = self.ctx.ws
|
||||
meta = {"source": self.src_name}
|
||||
if fx.active(self.effects):
|
||||
meta["effects"] = fx.summary(self.effects)
|
||||
|
||||
def work(job):
|
||||
job.report("encoding…")
|
||||
|
||||
Reference in New Issue
Block a user