fix: made sure the highest value are shown in cross correlation

This commit is contained in:
Honney OC committed 2026-09-13 21:35:45 +00:00
1 parent b76b7394cf
commit a01cf79447
1 file changed
+77 -5
+77 -5
View File
@@ -59,6 +59,25 @@ def decimate_for_plot(arr: np.ndarray, max_pts: int = 120_000) -> tuple[np.ndarr
stride = int(math.ceil(len(arr) / max_pts)) stride = int(math.ceil(len(arr) / max_pts))
return arr[::stride], stride return arr[::stride], stride
def decimate_with_peak(arr: np.ndarray, peak_idx: int, max_pts: int = 200_000) -> tuple[np.ndarray, np.ndarray]:
"""Decimate array for plotting using max-pooling to preserve all peaks."""
n = len(arr)
if n <= max_pts:
return arr, np.arange(n, dtype=np.float64)
stride = int(math.ceil(n / max_pts))
abs_arr = np.abs(arr)
n_blocks = (n + stride - 1) // stride
out = np.empty(n_blocks, dtype=arr.dtype)
idx_out = np.empty(n_blocks, dtype=np.float64)
for i in range(n_blocks):
start = i * stride
end = min(start + stride, n)
block_abs = abs_arr[start:end]
local_peak = int(np.argmax(block_abs))
out[i] = arr[start + local_peak]
idx_out[i] = float(start + local_peak)
return out, idx_out
def find_wavs_from_config(cfg: dict) -> tuple[str, str, int, float, float]: def find_wavs_from_config(cfg: dict) -> tuple[str, str, int, float, float]:
audio_cfg = cfg.get("audio", {}) audio_cfg = cfg.get("audio", {})
@@ -128,13 +147,14 @@ class SyncWindow(QtWidgets.QMainWindow):
# xcorr display — corr is already fftshifted # xcorr display — corr is already fftshifted
N = len(self.corr) N = len(self.corr)
# lags in seconds for display # lags in seconds for display
# raw corr was shifted: center at N//2, range [- (len(b)-1) .. len(a)-1]
# for display we map indices to lag
total = len(self.a) + len(self.b) - 1 total = len(self.a) + len(self.b) - 1
n_pow2 = N n_pow2 = N
# shifted lags: -N//2 .. N//2-1 # shifted lags: -N//2 .. N//2-1
self.corr_lags = np.arange(N) - N//2 self.corr_lags = np.arange(N) - N//2
self.corr_lags_sec = self.corr_lags / sr self.corr_lags_sec = self.corr_lags / sr
# find peak index in raw (unshifted) for decimation
peak_raw_idx = int(np.argmax(np.abs(self.corr))) # this is fftshifted, peak near center
# for decimation we need the index in the shifted array
central = QtWidgets.QWidget() central = QtWidgets.QWidget()
self.setCentralWidget(central) self.setCentralWidget(central)
@@ -169,9 +189,10 @@ class SyncWindow(QtWidgets.QMainWindow):
self.p3 = self.gw.addPlot(row=2, col=0, title="Cross-correlation (GCC-PHAT)" if method=="phat" else "Cross-correlation") self.p3 = self.gw.addPlot(row=2, col=0, title="Cross-correlation (GCC-PHAT)" if method=="phat" else "Cross-correlation")
self.p3.showGrid(x=True, y=True) self.p3.showGrid(x=True, y=True)
self.p3.setLabel("bottom", "Lag", "s") self.p3.setLabel("bottom", "Lag", "s")
# decimate corr for perf if huge # decimate corr with peak awareness
corr_dec, corr_stride = decimate_for_plot(self.corr, max_pts=200_000) peak_in_shifted = int(np.argmax(np.abs(self.corr)))
corr_lags_dec = self.corr_lags_sec[::corr_stride] if corr_stride>1 else self.corr_lags_sec corr_dec, corr_idx = decimate_with_peak(self.corr, peak_in_shifted, max_pts=200_000)
corr_lags_dec = self.corr_lags_sec[corr_idx.astype(int)]
self.curve_corr = self.p3.plot(corr_lags_dec, np.abs(corr_dec), pen=pg.mkPen("#ffd166", width=1)) self.curve_corr = self.p3.plot(corr_lags_dec, np.abs(corr_dec), pen=pg.mkPen("#ffd166", width=1))
self.peak_line = pg.InfiniteLine(pos=self.lag_sec, angle=90, pen=pg.mkPen("#06d6a0", width=2, style=QtCore.Qt.PenStyle.DashLine)) self.peak_line = pg.InfiniteLine(pos=self.lag_sec, angle=90, pen=pg.mkPen("#06d6a0", width=2, style=QtCore.Qt.PenStyle.DashLine))
self.p3.addItem(self.peak_line) self.p3.addItem(self.peak_line)
@@ -213,6 +234,9 @@ class SyncWindow(QtWidgets.QMainWindow):
btn_swap = QtWidgets.QPushButton("Swap A/B") btn_swap = QtWidgets.QPushButton("Swap A/B")
ctr.addWidget(btn_swap) ctr.addWidget(btn_swap)
btn_export = QtWidgets.QPushButton("Export data")
ctr.addWidget(btn_export)
# volume + playback # volume + playback
ctr2 = QtWidgets.QHBoxLayout() ctr2 = QtWidgets.QHBoxLayout()
lay.addLayout(ctr2) lay.addLayout(ctr2)
@@ -249,6 +273,7 @@ class SyncWindow(QtWidgets.QMainWindow):
self.vol.valueChanged.connect(lambda v: self.vol_label.setText(f"{v}%")) self.vol.valueChanged.connect(lambda v: self.vol_label.setText(f"{v}%"))
btn_auto.clicked.connect(self.on_auto) btn_auto.clicked.connect(self.on_auto)
btn_swap.clicked.connect(self.on_swap) btn_swap.clicked.connect(self.on_swap)
btn_export.clicked.connect(self.on_export)
self.btn_play_a.clicked.connect(lambda: self.play("a")) self.btn_play_a.clicked.connect(lambda: self.play("a"))
self.btn_play_b.clicked.connect(lambda: self.play("b")) self.btn_play_b.clicked.connect(lambda: self.play("b"))
self.btn_play_mix.clicked.connect(lambda: self.play("mix")) self.btn_play_mix.clicked.connect(lambda: self.play("mix"))
@@ -321,6 +346,53 @@ class SyncWindow(QtWidgets.QMainWindow):
self.corr = corr self.corr = corr
self._set_lag(lag/self.sr, "swap") self._set_lag(lag/self.sr, "swap")
def on_export(self):
import json
path, _ = QtWidgets.QFileDialog.getSaveFileName(
self, "Export sync data", "sync_data.json", "JSON (*.json)")
if not path:
return
# export decimated correlation (full res is too large)
peak_in_shifted = int(np.argmax(np.abs(self.corr)))
corr_dec, corr_idx = decimate_with_peak(self.corr, peak_in_shifted, max_pts=200_000)
corr_lags_sec = self.corr_lags_sec[corr_idx.astype(int)]
export = {
"file_a": self.a_path,
"file_b": self.b_path,
"sr": self.sr,
"method": self.method,
"device": self.device_pref,
"lag_samples": self.lag_samples,
"lag_sec": self.lag_sec,
"peak_value": float(self.peak),
"a_samples": len(self.a_orig),
"a_duration_sec": len(self.a_orig) / self.sr,
"b_samples": len(self.b_orig),
"b_duration_sec": len(self.b_orig) / self.sr,
"corr_peak_index": peak_in_shifted,
"corr_length": len(self.corr),
# decimated correlation for plotting (not full 4M points)
"corr_lags_sec": corr_lags_sec.tolist(),
"corr_magnitude": np.abs(corr_dec).tolist(),
# waveform stats (full res waveforms too large for JSON)
"a_stats": {
"mean": float(self.a_orig.mean()),
"std": float(self.a_orig.std()),
"min": float(self.a_orig.min()),
"max": float(self.a_orig.max()),
},
"b_stats": {
"mean": float(self.b_orig.mean()),
"std": float(self.b_orig.std()),
"min": float(self.b_orig.min()),
"max": float(self.b_orig.max()),
},
}
with open(path, "w") as f:
json.dump(export, f, indent=2)
print(f"exported to {path}")
self.setWindowTitle(f"movie_sync — exported to {os.path.basename(path)}")
def play(self, which: str): def play(self, which: str):
if not HAS_SD: if not HAS_SD:
return return