feat: add zero-offset functionality and reposition stats panels in latency measurement GUI
This commit is contained in:
@@ -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')
|
||||
|
||||
Reference in New Issue
Block a user