fix: apply_sync ffmpeg streamcpy and filter
This commit is contained in:
1 parent
708a256427
commit
38841b1ae4
2 files changed
+51
-7
No files matched your search
+25
-3
@@ -57,6 +57,23 @@ def probe_audio_streams(path: str) -> list[dict]:
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def _codec_args(codec: str, out_idx: int) -> list[str]:
|
||||||
|
"""Return ffmpeg codec args for a given audio codec string."""
|
||||||
|
lossless = {"flac", "alac", "pcm_s16le", "pcm_s24le", "pcm_s32le", "pcm_f32le", "pcm_f64le"}
|
||||||
|
if codec in lossless:
|
||||||
|
return [f"-c:a:{out_idx}", codec]
|
||||||
|
# lossy codecs: use original where possible
|
||||||
|
codec_map = {
|
||||||
|
"aac": "aac", "mp3": "libmp3lame", "opus": "libopus",
|
||||||
|
"vorbis": "libvorbis", "ac3": "ac3", "eac3": "eac3",
|
||||||
|
}
|
||||||
|
enc = codec_map.get(codec, "aac")
|
||||||
|
args = [f"-c:a:{out_idx}", enc]
|
||||||
|
if enc != codec or codec not in lossless:
|
||||||
|
args += [f"-b:a:{out_idx}", "192k"]
|
||||||
|
return args
|
||||||
|
|
||||||
|
|
||||||
def build_ffmpeg_cmd(file_a: str, file_b: str, lag_sec: float, out_path: str) -> list[str]:
|
def build_ffmpeg_cmd(file_a: str, file_b: str, lag_sec: float, out_path: str) -> list[str]:
|
||||||
"""Build ffmpeg command to mux A video + A audio + delayed B audio streams."""
|
"""Build ffmpeg command to mux A video + A audio + delayed B audio streams."""
|
||||||
b_streams = probe_audio_streams(file_b)
|
b_streams = probe_audio_streams(file_b)
|
||||||
@@ -67,6 +84,7 @@ def build_ffmpeg_cmd(file_a: str, file_b: str, lag_sec: float, out_path: str) ->
|
|||||||
set_german = (len(b_streams) == 1 and b_streams[0]["language"] in ("und", "unknown", ""))
|
set_german = (len(b_streams) == 1 and b_streams[0]["language"] in ("und", "unknown", ""))
|
||||||
|
|
||||||
# Build filter_complex for B audio streams
|
# Build filter_complex for B audio streams
|
||||||
|
# Negative adelay trims the start (B ahead of A), positive adds silence (B behind A)
|
||||||
filters = []
|
filters = []
|
||||||
for i, s in enumerate(b_streams):
|
for i, s in enumerate(b_streams):
|
||||||
filters.append(f"[1:a:{i}]adelay={lag_ms:.0f}|{lag_ms:.0f}:all=1[d{i}]")
|
filters.append(f"[1:a:{i}]adelay={lag_ms:.0f}|{lag_ms:.0f}:all=1[d{i}]")
|
||||||
@@ -81,11 +99,15 @@ def build_ffmpeg_cmd(file_a: str, file_b: str, lag_sec: float, out_path: str) ->
|
|||||||
for i in range(len(b_streams)):
|
for i in range(len(b_streams)):
|
||||||
cmd += ["-map", f"[d{i}]"]
|
cmd += ["-map", f"[d{i}]"]
|
||||||
|
|
||||||
# Codecs: copy everything (lossless)
|
# Codecs: copy video + A's audio, re-encode B's filtered audio with same codec
|
||||||
cmd += ["-c:v", "copy", "-c:a", "copy"]
|
n_a_audio = len(probe_audio_streams(file_a))
|
||||||
|
cmd += ["-c:v", "copy"]
|
||||||
|
for i in range(n_a_audio):
|
||||||
|
cmd += [f"-c:a:{i}", "copy"]
|
||||||
|
for i, s in enumerate(b_streams):
|
||||||
|
cmd += _codec_args(s["codec"], n_a_audio + i)
|
||||||
|
|
||||||
# Language metadata for B audio streams (they become the last N audio streams in output)
|
# Language metadata for B audio streams (they become the last N audio streams in output)
|
||||||
n_a_audio = len(probe_audio_streams(file_a))
|
|
||||||
for i, s in enumerate(b_streams):
|
for i, s in enumerate(b_streams):
|
||||||
out_audio_idx = n_a_audio + i # 0-based index in output audio streams
|
out_audio_idx = n_a_audio + i # 0-based index in output audio streams
|
||||||
if set_german and i == 0:
|
if set_german and i == 0:
|
||||||
|
|||||||
+26
-4
@@ -111,7 +111,8 @@ def find_wavs_from_config(cfg: dict) -> tuple[str, str, int, float, float, int]:
|
|||||||
|
|
||||||
class SyncWindow(QtWidgets.QMainWindow):
|
class SyncWindow(QtWidgets.QMainWindow):
|
||||||
def __init__(self, a_path: str, b_path: str, sr: int = 48000, method: str = "phat", device: str = "auto",
|
def __init__(self, a_path: str, b_path: str, sr: int = 48000, method: str = "phat", device: str = "auto",
|
||||||
video_a: str = "", video_b: str = "", out_dir: str = "output", volume: int = 30):
|
video_a: str = "", video_b: str = "", out_dir: str = "output", volume: int = 30,
|
||||||
|
duration: float = 90.0):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.setWindowTitle(f"movie_sync — audio sync test — {device_info()}")
|
self.setWindowTitle(f"movie_sync — audio sync test — {device_info()}")
|
||||||
self.resize(1400, 900)
|
self.resize(1400, 900)
|
||||||
@@ -121,6 +122,7 @@ class SyncWindow(QtWidgets.QMainWindow):
|
|||||||
self.video_b = video_b
|
self.video_b = video_b
|
||||||
self.out_dir = out_dir
|
self.out_dir = out_dir
|
||||||
self.sr = sr
|
self.sr = sr
|
||||||
|
self.duration = duration
|
||||||
self.method = method
|
self.method = method
|
||||||
self.device_pref = device
|
self.device_pref = device
|
||||||
self._updating = False
|
self._updating = False
|
||||||
@@ -193,6 +195,10 @@ class SyncWindow(QtWidgets.QMainWindow):
|
|||||||
self.p3.addItem(self.peak_line)
|
self.p3.addItem(self.peak_line)
|
||||||
self.p3.setXRange(self.lag_sec-2, self.lag_sec+2)
|
self.p3.setXRange(self.lag_sec-2, self.lag_sec+2)
|
||||||
|
|
||||||
|
self.xcorr_val_label = QtWidgets.QLabel()
|
||||||
|
self.xcorr_val_label.setStyleSheet("color: #ffd166; font-size: 12px;")
|
||||||
|
lay.addWidget(self.xcorr_val_label)
|
||||||
|
|
||||||
# controls
|
# controls
|
||||||
ctr = QtWidgets.QHBoxLayout()
|
ctr = QtWidgets.QHBoxLayout()
|
||||||
lay.addLayout(ctr)
|
lay.addLayout(ctr)
|
||||||
@@ -232,7 +238,7 @@ class SyncWindow(QtWidgets.QMainWindow):
|
|||||||
btn_export = QtWidgets.QPushButton("Export data")
|
btn_export = QtWidgets.QPushButton("Export data")
|
||||||
ctr.addWidget(btn_export)
|
ctr.addWidget(btn_export)
|
||||||
|
|
||||||
btn_verify = QtWidgets.QPushButton("Verify (30s)")
|
btn_verify = QtWidgets.QPushButton(f"Verify ({duration:g}s)")
|
||||||
ctr.addWidget(btn_verify)
|
ctr.addWidget(btn_verify)
|
||||||
|
|
||||||
btn_apply = QtWidgets.QPushButton("Apply sync")
|
btn_apply = QtWidgets.QPushButton("Apply sync")
|
||||||
@@ -294,6 +300,7 @@ class SyncWindow(QtWidgets.QMainWindow):
|
|||||||
self.btn_stop.clicked.connect(self.stop_play)
|
self.btn_stop.clicked.connect(self.stop_play)
|
||||||
|
|
||||||
self.update_labels()
|
self.update_labels()
|
||||||
|
self._update_xcorr_label()
|
||||||
|
|
||||||
def _set_lag(self, lag_sec: float, source: str = ""):
|
def _set_lag(self, lag_sec: float, source: str = ""):
|
||||||
if self._updating:
|
if self._updating:
|
||||||
@@ -314,9 +321,23 @@ class SyncWindow(QtWidgets.QMainWindow):
|
|||||||
# pyqtgraph setPos shifts items; we recreate x
|
# pyqtgraph setPos shifts items; we recreate x
|
||||||
# Instead shift data: plot t_b + lag
|
# Instead shift data: plot t_b + lag
|
||||||
self.curve_b.setData(self.t_b + shift, self.b_plot)
|
self.curve_b.setData(self.t_b + shift, self.b_plot)
|
||||||
|
self._update_xcorr_label()
|
||||||
self.update_labels()
|
self.update_labels()
|
||||||
self._updating = False
|
self._updating = False
|
||||||
|
|
||||||
|
def _update_xcorr_label(self):
|
||||||
|
N = len(self.corr)
|
||||||
|
idx = N // 2 + self.lag_samples
|
||||||
|
if 0 <= idx < N:
|
||||||
|
val = np.abs(self.corr[idx])
|
||||||
|
peak_val = np.abs(self.peak)
|
||||||
|
pct = (val / peak_val * 100) if peak_val > 0 else 0
|
||||||
|
self.xcorr_val_label.setText(
|
||||||
|
f"Xcorr at lag: {val:.6f} (peak: {peak_val:.6f} = {pct:.1f}%)"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.xcorr_val_label.setText("Xcorr at lag: (out of range)")
|
||||||
|
|
||||||
def update_labels(self):
|
def update_labels(self):
|
||||||
lag_ms = self.lag_sec * 1000
|
lag_ms = self.lag_sec * 1000
|
||||||
stem = os.path.splitext(os.path.basename(self.video_a))[0]
|
stem = os.path.splitext(os.path.basename(self.video_a))[0]
|
||||||
@@ -498,7 +519,7 @@ def main():
|
|||||||
args = ap.parse_args()
|
args = ap.parse_args()
|
||||||
|
|
||||||
cfg = load_config(args.config)
|
cfg = load_config(args.config)
|
||||||
cfg_a, cfg_b, sr, _, _, volume = find_wavs_from_config(cfg)
|
cfg_a, cfg_b, sr, _, duration, volume = find_wavs_from_config(cfg)
|
||||||
a_path = args.a or cfg_a
|
a_path = args.a or cfg_a
|
||||||
b_path = args.b or cfg_b
|
b_path = args.b or cfg_b
|
||||||
method = args.method or cfg.get("sync", {}).get("method", "phat")
|
method = args.method or cfg.get("sync", {}).get("method", "phat")
|
||||||
@@ -546,7 +567,8 @@ def main():
|
|||||||
app.setPalette(dark)
|
app.setPalette(dark)
|
||||||
pg.setConfigOptions(antialias=True, background="#1e1e1e", foreground="#ddd")
|
pg.setConfigOptions(antialias=True, background="#1e1e1e", foreground="#ddd")
|
||||||
w = SyncWindow(a_path, b_path, sr=sr, method=method, device=device,
|
w = SyncWindow(a_path, b_path, sr=sr, method=method, device=device,
|
||||||
video_a=video_a, video_b=video_b, out_dir=out_dir, volume=volume)
|
video_a=video_a, video_b=video_b, out_dir=out_dir, volume=volume,
|
||||||
|
duration=duration)
|
||||||
w.show()
|
w.show()
|
||||||
sys.exit(app.exec())
|
sys.exit(app.exec())
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user