Commit d1b21c94 by PLN (Algolia)

feat(gig-log): report where a control is PARKED, not just how far it travelled

The surface table said cc 51 moved 533 times somewhere between 1 and 127. That
is a biography, not a state, and it cannot answer the only question you ask a
log at 3am: "why is that orbit silent?". Only the LAST value can.

The data was already in the file — MidiReader.feed() has always coalesced to
count/first/last/min/max per control per second, so `v1` is the value the knob
was left at. The report simply never printed it. This is a formatting change to
capture that already happened.

Three additions:

* `report` gains a `left at` column, and for the three DJ filters it prints what
  that value MEANS in hertz. Transcribing BootTidal.hs:376 turned up something
  worth its own note: for v <= 0.5 the gDJF expression reduces to
  lpf = 180 + 39640*v, i.e. LINEAR IN HERTZ. Pitch perception is logarithmic, so
  that knob spends nearly all its travel in the top two octaves and crosses the
  entire audible bottom in the last ~2% — it feels inert, then collapses. That is
  one gesture producing both "it went quiet" and "it sounds lpf'd", which is
  exactly the pair of symptoms #79 was filed with.

* `gig-log.py controls` — the whole surface, by CC, with each value translated:
  filters to Hz, gMute/gMask to "mutes N% of cycles", panic to armed/clear. It
  ends with an explicit list of anything parked somewhere that silences or thins
  the sound, because a table you have to interpret under stage lights is a table
  you will misread.

* `report --from/--to`, wall clock or +M:SS. The recorder ran 10h48m of which
  ~90 minutes was playing and eight hours was tooling; every aggregate therefore
  described the wrong thing. Windowing has one non-obvious requirement, and it
  gets a test of its own: xrun and throttle are RUNNING TOTALS, so a naive slice
  reports the whole session's count inside the window — wrong, and wrong high.
  slice_session() rebases them to the window.

VALIDATION — it immediately paid for itself by killing two hypotheses:
    49  51  gF1  lpf 16098 Hz — open
    50  59  gF2  lpf 18595 Hz — open
    51  80  gF3  lpf 20000 Hz — open
    41   0  gMask   gates 0% of cycles
    73/74/75 0      mutes 0% of cycles
#79's two leading suspects were "a DJ filter parked below centre" and "a mask
left engaged". Both are now dead, from a file, with no rig and nobody's ears.

83 tests (was 71). The new ones pin the BootTidal arithmetic so it cannot drift
from the Haskell, the last-value ordering rule (latest TIMESTAMP wins, not file
order), the cumulative rebase, and midnight-crossing wall-clock parsing.
parent 55a38ba6
...@@ -13,6 +13,7 @@ import importlib.util ...@@ -13,6 +13,7 @@ import importlib.util
import json import json
import os import os
import time import time
from datetime import datetime
from pathlib import Path from pathlib import Path
import pytest import pytest
...@@ -558,3 +559,144 @@ def test_the_report_states_zero_xruns_as_a_positive_verdict(tmp_path, capsys): ...@@ -558,3 +559,144 @@ def test_the_report_states_zero_xruns_as_a_positive_verdict(tmp_path, capsys):
gl.cmd_report(r.path) gl.cmd_report(r.path)
out = capsys.readouterr().out out = capsys.readouterr().out
assert "ZERO" in out and "the audio path held" in out assert "ZERO" in out and "the audio path held" in out
# ---------------------------------------------------------------------------
# #82 — the LAST value, and what it MEANS
#
# Range and recency cannot answer "why is that orbit silent"; only the parked
# value can. These pin the arithmetic (which is copied from BootTidal.hs and
# must not drift) and the ordering rule.
# ---------------------------------------------------------------------------
def test_djf_lpf_matches_bootTidal_arithmetic():
# gDJF ch = (# lpf (range 180 20000 (fmap (\v -> 1 - 2*max 0 (0.5-v)) ...)))
# Centre and above is fully open; the bottom is linear in HERTZ.
assert round(gl.djf_lpf(0)) == 180 # hard left = 180 Hz = silence
assert round(gl.djf_lpf(127)) == 20000 # hard right = open
assert round(gl.djf_lpf(64)) == 20000 # centre = ALREADY open
assert round(gl.djf_lpf(96)) == 20000 # upper half does nothing
mid = gl.djf_lpf(32) # quarter-turn
assert 9000 < mid < 11000, mid
def test_djf_lpf_is_linear_in_hz_which_is_the_footgun():
# Equal knob steps = equal HERTZ steps, so the whole audible bottom lives in
# the last few percent of travel. If this ever becomes log-spaced the
# verdicts below must be re-derived, so assert the shape explicitly.
a, b, c = gl.djf_lpf(10), gl.djf_lpf(20), gl.djf_lpf(30)
assert abs((b - a) - (c - b)) < 1.0
def test_djf_verdict_flags_only_the_genuinely_inaudible():
assert "NEAR-SILENT" in gl.djf_verdict(gl.djf_lpf(0))
assert gl.djf_verdict(gl.djf_lpf(127)) == "open"
assert gl.djf_verdict(gl.djf_lpf(64)) == "open"
def test_surface_state_keeps_the_last_value_not_the_last_seen_record():
# Out-of-order records must not decide where a knob is parked.
ev = [
{"t": 100.0, "k": "cc", "cc": 49, "n": 3, "v0": 0, "v1": 10, "lo": 0, "hi": 10},
{"t": 130.0, "k": "cc", "cc": 49, "n": 2, "v0": 60, "v1": 64, "lo": 60, "hi": 64},
{"t": 110.0, "k": "cc", "cc": 49, "n": 1, "v0": 5, "v1": 5, "lo": 5, "hi": 5},
]
a = gl.surface_state(ev)[49]
assert a["v"] == 64, "the latest TIMESTAMP wins, not file order"
assert a["n"] == 6 and a["lo"] == 0 and a["hi"] == 64
def test_surface_state_survives_a_log_with_no_v1(tmp_path):
# Logs written before v1 existed must degrade to "?" rather than crash.
a = gl.surface_state([{"t": 1.0, "k": "cc", "cc": 7, "n": 1, "lo": 3, "hi": 3}])[7]
assert a["v"] is None
def test_slice_session_rebases_cumulative_counters():
# THE bug this guards: xrun/throttle are running totals, so a naive window
# reports the WHOLE session's count inside it — wrong, and wrong high.
s = [{"t": float(i), "k": "s", "xrun": i * 10,
"xrun_by": {"ardour": i * 7}, "thr_pkg": i, "thr_core": 0}
for i in range(11)]
sl, _, _ = gl.slice_session(s, [], [], 5.0, 9.0)
assert [r["t"] for r in sl] == [5.0, 6.0, 7.0, 8.0, 9.0]
assert sl[0]["xrun"] == 0, "the window must start at zero"
assert sl[-1]["xrun"] == 40, "5..9 is a delta of 40, not the total of 90"
assert sl[-1]["xrun_by"]["ardour"] == 28
assert sl[-1]["thr_pkg"] == 4
def test_slice_session_without_a_window_does_not_rebase():
s = [{"t": float(i), "k": "s", "xrun": i * 10} for i in range(4)]
sl, _, _ = gl.slice_session(s, [], [], None, None)
assert sl[-1]["xrun"] == 30, "an unwindowed report must be unchanged"
def test_parse_when_accepts_offsets_and_wall_clock():
t0 = datetime(2026, 7, 29, 0, 57, 8).timestamp()
assert gl.parse_when("+1:30", t0) == t0 + 90
assert gl.parse_when("+1:00:00", t0) == t0 + 3600
got = datetime.fromtimestamp(gl.parse_when("09:15", t0))
assert (got.hour, got.minute) == (9, 15)
def test_parse_when_wall_clock_before_start_crosses_midnight():
t0 = datetime(2026, 7, 29, 23, 30, 0).timestamp()
got = datetime.fromtimestamp(gl.parse_when("00:15", t0))
assert got.day == 30 and (got.hour, got.minute) == (0, 15)
def test_controls_report_names_the_dangerous_parking(tmp_path, capsys):
r = gl.Recorder(out_dir=tmp_path, hz=50.0, xruns=False, midi=False)
r._write(r.header())
t = time.time()
r._write({"t": t, "k": "cc", "p": "24:0", "ch": 0, "cc": 51,
"n": 4, "v0": 90, "v1": 0, "lo": 0, "hi": 90}) # parked hard left
r._write({"t": t, "k": "cc", "p": "24:0", "ch": 0, "cc": 74,
"n": 2, "v0": 0, "v1": 127, "lo": 0, "hi": 127}) # mute held on
r._write({"t": t, "k": "cc", "p": "24:0", "ch": 0, "cc": 49,
"n": 2, "v0": 0, "v1": 100, "lo": 0, "hi": 100}) # safe
r.f.close()
assert gl.cmd_controls(r.path) == 0
out = capsys.readouterr().out
assert "PARKED SOMEWHERE THAT SILENCES" in out
assert "cc 51" in out and "NEAR-SILENT" in out
assert "cc 74" in out and "100%" in out
risky = out.split("PARKED SOMEWHERE THAT SILENCES")[1]
assert "cc 49" not in risky, "an open filter must not be flagged"
def test_controls_report_is_quiet_when_the_surface_is_safe(tmp_path, capsys):
r = gl.Recorder(out_dir=tmp_path, hz=50.0, xruns=False, midi=False)
r._write(r.header())
r._write({"t": time.time(), "k": "cc", "p": "24:0", "ch": 0, "cc": 49,
"n": 1, "v0": 100, "v1": 100, "lo": 100, "hi": 100})
r.f.close()
assert gl.cmd_controls(r.path) == 0
assert "No control is parked in a known-dangerous zone" in capsys.readouterr().out
def test_report_window_narrows_the_xrun_total(tmp_path, capsys):
r = gl.Recorder(out_dir=tmp_path, hz=50.0, xruns=False, midi=False)
r._write(r.header())
hdr = json.loads(r.path.read_text().splitlines()[0])
hdr["xruns"] = True
t0 = hdr["t"]
for i in range(20):
r.f.write(json.dumps({"t": t0 + i, "k": "s", "pkg": 50, "core_max": 50,
"fan": [1700], "fmax": 2900, "favg": 2900,
"cpu_max": 10, "cpu_avg": 5, "thr_core": 0,
"thr_pkg": 0, "w": None, "gear": {},
"xrun": i * 100}) + "\n")
r.f.close()
lines = r.path.read_text().splitlines()
lines[0] = json.dumps(hdr)
r.path.write_text("\n".join(lines) + "\n")
gl.cmd_report(r.path)
assert "1900" in capsys.readouterr().out
gl.cmd_report(r.path, since="+0:10", until="+0:15")
out = capsys.readouterr().out
assert "WINDOW" in out and "rebased" in out
assert "500" in out and "1900" not in out
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment