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
import json
import os
import time
from datetime import datetime
from pathlib import Path
import pytest
......@@ -558,3 +559,144 @@ def test_the_report_states_zero_xruns_as_a_positive_verdict(tmp_path, capsys):
gl.cmd_report(r.path)
out = capsys.readouterr().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