feat: add zero-offset functionality and reposition stats panels in latency measurement GUI

This commit is contained in:
2025-10-24 09:40:44 +02:00
parent 18d1f961d3
commit d4756cbb03

View File

@@ -262,13 +262,13 @@ def run_gui(fs: int, dur: float, vol: float, indev: int | None, outdev: int | No
log_text = ax_log.text(0.0, 1.0, "", va="top", ha="left", family="monospace", fontsize=8)
LOG_WINDOW = 10 # show last 10 lines; start scrolling after 10
# Stats panel (immediately below terminal)
ax_stats = fig.add_axes([0.73, 0.32, 0.23, 0.02])
# Stats panel (move higher and slightly to the right)
ax_stats = fig.add_axes([0.78, 0.60, 0.18, 0.04])
ax_stats.axis("off")
stats_text = ax_stats.text(0.0, 1.0, "", va="top", ha="left", family="monospace", fontsize=8)
# Hardware/Status panel (directly below stats)
ax_hw = fig.add_axes([0.73, 0.28, 0.23, 0.02])
# Hardware/Status panel (just below the moved stats)
ax_hw = fig.add_axes([0.80, 0.46, 0.18, 0.04])
ax_hw.axis("off")
hw_text = ax_hw.text(0.0, 1.0, "", va="top", ha="left", family="monospace", fontsize=8)
@@ -279,6 +279,7 @@ def run_gui(fs: int, dur: float, vol: float, indev: int | None, outdev: int | No
current_conf_min = [float(conf_min)]
include_low = [False]
zero_offset = [0.0]
# status/xrun counter shared with stream callback
xrun_counter = {"count": 0}
# input RMS meter shared
@@ -338,6 +339,8 @@ def run_gui(fs: int, dur: float, vol: float, indev: int | None, outdev: int | No
start = max(0, n - SCATTER_WINDOW[0])
idx = np.arange(start, n)
y = np.array(latencies[start:n], dtype=float)
# Apply display zero-offset
y = y - zero_offset[0]
c = np.array(confidences[start:n], dtype=float)
finite = np.isfinite(y)
thr = current_conf_min[0]
@@ -345,9 +348,7 @@ def run_gui(fs: int, dur: float, vol: float, indev: int | None, outdev: int | No
is_low = finite & ~is_valid
y_plot = y.copy()
y_plot[~finite] = np.nan
# Visual floor epsilon to avoid points collapsing exactly at 0
eps = 0.1 # ms
y_plot = np.maximum(y_plot, eps)
# No artificial floor; allow negatives when zero-offset is applied
# Build display mask respecting include_low and conf_min
if include_low[0]:
disp_mask = (np.isfinite(y_plot))
@@ -371,7 +372,7 @@ def run_gui(fs: int, dur: float, vol: float, indev: int | None, outdev: int | No
# Clear legacy series to avoid double plotting
sc_valid.set_data([], [])
sc_low.set_data([], [])
# Y-axis starts at 0 with small padding above max
# Y-axis: include zero and any negatives (when offset applied)
# Guard all-NaN window: if no finite data, show default axes and clear rolling overlays
if not np.any(np.isfinite(y_plot)):
ax_sc.set_ylim(0.0, 1.0)
@@ -387,7 +388,8 @@ def run_gui(fs: int, dur: float, vol: float, indev: int | None, outdev: int | No
ann_last.set_text("")
else:
y_max = float(np.nanmax(y_plot))
y_low = 0.0
y_min = float(np.nanmin(y_plot))
y_low = min(0.0, y_min)
y_high = max(1.0, y_max)
pad = 0.05 * (y_high - y_low)
ax_sc.set_ylim(y_low, y_high + pad)
@@ -419,7 +421,7 @@ def run_gui(fs: int, dur: float, vol: float, indev: int | None, outdev: int | No
except Exception:
pass
upper = m + s
lower = np.maximum(0.0, m - s)
lower = m - s
band_poly[0] = ax_sc.fill_between(idx, lower, upper, color="#1f77b4", alpha=0.15, linewidth=0)
# Latest sample highlight
@@ -445,7 +447,7 @@ def run_gui(fs: int, dur: float, vol: float, indev: int | None, outdev: int | No
lines.append(f"{i:04d} | {lat_str} ms | conf={conf_str} | {flag}")
log_text.set_text("\n".join(lines[-LOG_WINDOW:]))
# Update stats panel (respect filters like the scatter)
# Update stats panel (respect filters like the scatter). Stats use zero-offset adjusted values.
data_all = np.array(latencies, dtype=float)
conf_all = np.array(confidences, dtype=float)
thr = current_conf_min[0]
@@ -457,8 +459,9 @@ def run_gui(fs: int, dur: float, vol: float, indev: int | None, outdev: int | No
msk &= np.isfinite(conf_all) & (conf_all >= thr)
n_sel = int(np.sum(msk))
if n_sel >= 1:
mean_val = float(np.nanmean(data_all[msk]))
std_val = float(np.nanstd(data_all[msk], ddof=1)) if n_sel >= 2 else 0.0
adj = data_all - zero_offset[0]
mean_val = float(np.nanmean(adj[msk]))
std_val = float(np.nanstd(adj[msk], ddof=1)) if n_sel >= 2 else 0.0
stats_text.set_text(f"N={n_sel} mean={mean_val:.2f} ms std={std_val:.2f} ms")
else:
stats_text.set_text("N=0 mean=-- std=--")
@@ -475,7 +478,8 @@ def run_gui(fs: int, dur: float, vol: float, indev: int | None, outdev: int | No
f"pre={pre_silence:.2f}s, post={post_silence:.2f}s",
f"xruns={xrun_counter['count']}",
f"inRMS={rms_info['rms_dbfs']:.1f} dBFS, clip={'YES' if rms_info['clip'] else 'no'}",
f"bandpass={'on' if bandpass else 'off'}"
f"bandpass={'on' if bandpass else 'off'}",
f"zero_offset={zero_offset[0]:.2f} ms"
]
hw_text.set_text("\n".join(hw_lines))
fig.canvas.draw_idle()
@@ -534,10 +538,14 @@ def run_gui(fs: int, dur: float, vol: float, indev: int | None, outdev: int | No
stop_ax = fig.add_axes([0.69, 0.02, 0.13, 0.06])
reset_ax = fig.add_axes([0.84, 0.02, 0.06, 0.06])
save_ax = fig.add_axes([0.92, 0.02, 0.06, 0.06])
zero_ax = fig.add_axes([0.84, 0.10, 0.06, 0.06])
zero_clr_ax = fig.add_axes([0.92, 0.10, 0.06, 0.06])
btn_start = Button(start_ax, "Start")
btn_stop = Button(stop_ax, "Stop")
btn_reset = Button(reset_ax, "Clr")
btn_save = Button(save_ax, "Save")
btn_zero = Button(zero_ax, "Zero")
btn_zero_clr = Button(zero_clr_ax, "ZeroClr")
btn_start.on_clicked(on_start)
btn_stop.on_clicked(on_stop)
def on_reset(event):
@@ -545,9 +553,33 @@ def run_gui(fs: int, dur: float, vol: float, indev: int | None, outdev: int | No
latencies.clear()
confidences.clear()
latest_conf["value"] = float("nan")
zero_offset[0] = 0.0
update_plot()
btn_reset.on_clicked(on_reset)
def on_zero(event):
# Set zero_offset to current mean of selected (masked) samples
data_all = np.array(latencies, dtype=float)
conf_all = np.array(confidences, dtype=float)
thr = current_conf_min[0]
if include_low[0]:
msk = np.isfinite(data_all)
else:
msk = np.isfinite(data_all) & (data_all >= 0.0)
if conf_all.size == data_all.size:
msk &= np.isfinite(conf_all) & (conf_all >= thr)
if np.any(msk):
zero_offset[0] = float(np.nanmean(data_all[msk]))
else:
zero_offset[0] = 0.0
update_plot()
btn_zero.on_clicked(on_zero)
def on_zero_clr(event):
zero_offset[0] = 0.0
update_plot()
btn_zero_clr.on_clicked(on_zero_clr)
def on_save(event):
# Save current measurements to CSV with timestamped filename
ts = datetime.now().strftime('%Y%m%d_%H%M%S')