From 4cf7227a6cfbee26c22d7863a6775f11afb251dc Mon Sep 17 00:00:00 2001 From: Honney OC Date: Mon, 14 Sep 2026 01:38:17 +0000 Subject: [PATCH] fix: apply_sync ffmpeg streamcpy and filter --- apply_sync.py | 28 +++++++++++++++++++++++++--- sync_audio_test.py | 30 ++++++++++++++++++++++++++---- 2 files changed, 51 insertions(+), 7 deletions(-) diff --git a/apply_sync.py b/apply_sync.py index 18aa97e..2667c72 100644 --- a/apply_sync.py +++ b/apply_sync.py @@ -57,6 +57,23 @@ def probe_audio_streams(path: str) -> list[dict]: 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]: """Build ffmpeg command to mux A video + A audio + delayed B audio streams.""" 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", "")) # Build filter_complex for B audio streams + # Negative adelay trims the start (B ahead of A), positive adds silence (B behind A) filters = [] for i, s in enumerate(b_streams): 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)): cmd += ["-map", f"[d{i}]"] - # Codecs: copy everything (lossless) - cmd += ["-c:v", "copy", "-c:a", "copy"] + # Codecs: copy video + A's audio, re-encode B's filtered audio with same codec + 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) - n_a_audio = len(probe_audio_streams(file_a)) for i, s in enumerate(b_streams): out_audio_idx = n_a_audio + i # 0-based index in output audio streams if set_german and i == 0: diff --git a/sync_audio_test.py b/sync_audio_test.py index 21deee1..15747e6 100644 --- a/sync_audio_test.py +++ b/sync_audio_test.py @@ -111,7 +111,8 @@ def find_wavs_from_config(cfg: dict) -> tuple[str, str, int, float, float, int]: class SyncWindow(QtWidgets.QMainWindow): 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__() self.setWindowTitle(f"movie_sync — audio sync test — {device_info()}") self.resize(1400, 900) @@ -121,6 +122,7 @@ class SyncWindow(QtWidgets.QMainWindow): self.video_b = video_b self.out_dir = out_dir self.sr = sr + self.duration = duration self.method = method self.device_pref = device self._updating = False @@ -193,6 +195,10 @@ class SyncWindow(QtWidgets.QMainWindow): self.p3.addItem(self.peak_line) 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 ctr = QtWidgets.QHBoxLayout() lay.addLayout(ctr) @@ -232,7 +238,7 @@ class SyncWindow(QtWidgets.QMainWindow): btn_export = QtWidgets.QPushButton("Export data") ctr.addWidget(btn_export) - btn_verify = QtWidgets.QPushButton("Verify (30s)") + btn_verify = QtWidgets.QPushButton(f"Verify ({duration:g}s)") ctr.addWidget(btn_verify) btn_apply = QtWidgets.QPushButton("Apply sync") @@ -294,6 +300,7 @@ class SyncWindow(QtWidgets.QMainWindow): self.btn_stop.clicked.connect(self.stop_play) self.update_labels() + self._update_xcorr_label() def _set_lag(self, lag_sec: float, source: str = ""): if self._updating: @@ -314,9 +321,23 @@ class SyncWindow(QtWidgets.QMainWindow): # pyqtgraph setPos shifts items; we recreate x # Instead shift data: plot t_b + lag self.curve_b.setData(self.t_b + shift, self.b_plot) + self._update_xcorr_label() self.update_labels() 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): lag_ms = self.lag_sec * 1000 stem = os.path.splitext(os.path.basename(self.video_a))[0] @@ -498,7 +519,7 @@ def main(): args = ap.parse_args() 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 b_path = args.b or cfg_b method = args.method or cfg.get("sync", {}).get("method", "phat") @@ -546,7 +567,8 @@ def main(): app.setPalette(dark) pg.setConfigOptions(antialias=True, background="#1e1e1e", foreground="#ddd") 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() sys.exit(app.exec())