277 lines
9.0 KiB
Python
277 lines
9.0 KiB
Python
|
|
#!/usr/bin/env python3
|
||
|
|
"""Probe 1.bin (rhythm) and 3.bin (universal) for bass ch8 + plan compliance.
|
||
|
|
|
||
|
|
Requires FW with: tone bin1 / tone bin3 / tone start / chord key / chord xpose / PITCH ch8(bass)
|
||
|
|
"""
|
||
|
|
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import argparse
|
||
|
|
import os
|
||
|
|
import re
|
||
|
|
import sys
|
||
|
|
import time
|
||
|
|
from collections import Counter, defaultdict
|
||
|
|
from dataclasses import dataclass, field
|
||
|
|
from datetime import datetime
|
||
|
|
|
||
|
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||
|
|
from rtt_pitch_reg_test import ( # noqa: E402
|
||
|
|
RttSession,
|
||
|
|
analyze_lines,
|
||
|
|
connect_jlink,
|
||
|
|
expect_for_event,
|
||
|
|
find_rtt_control_block,
|
||
|
|
import_deps,
|
||
|
|
parse_pitch_line,
|
||
|
|
wait_rtt_ready,
|
||
|
|
)
|
||
|
|
|
||
|
|
PITCH_RE_CH = re.compile(r"\[PITCH\].*?ch(\d+)\((\w+)\)")
|
||
|
|
CH_SEEN_RE = re.compile(r"ch-seen ch(\d+) role=(\w+) key=(-?\d+)")
|
||
|
|
TONE_OK_RE = re.compile(r"TONE_BIN([13]).*name=(\S+).*count=(\d+)")
|
||
|
|
|
||
|
|
|
||
|
|
@dataclass
|
||
|
|
class PresetResult:
|
||
|
|
bin_id: str
|
||
|
|
idx: int
|
||
|
|
name: str = ""
|
||
|
|
channels: Counter = field(default_factory=Counter)
|
||
|
|
roles: Counter = field(default_factory=Counter)
|
||
|
|
bass_n: int = 0
|
||
|
|
drum_n: int = 0
|
||
|
|
chord_n: int = 0
|
||
|
|
pass_ok: int = 0
|
||
|
|
pass_fail: int = 0
|
||
|
|
iii_ok: int = 0
|
||
|
|
iii_fail: int = 0
|
||
|
|
xf_ok: int = 0
|
||
|
|
xf_fail: int = 0
|
||
|
|
bass_ok: int = 0
|
||
|
|
bass_fail: int = 0
|
||
|
|
samples: list[str] = field(default_factory=list)
|
||
|
|
|
||
|
|
|
||
|
|
def run_preset(sess: RttSession, bin_id: str, idx: int, hold: float) -> PresetResult:
|
||
|
|
res = PresetResult(bin_id=bin_id, idx=idx)
|
||
|
|
mark = len(sess.lines)
|
||
|
|
|
||
|
|
if bin_id == "1":
|
||
|
|
ok = sess.cmd_ack(f"tone bin1 {idx}", "TONE_BIN1", timeout_s=3.0, retries=4)
|
||
|
|
else:
|
||
|
|
ok = sess.cmd_ack(f"tone bin3 {idx}", "TONE_BIN3", timeout_s=3.0, retries=4)
|
||
|
|
if not ok:
|
||
|
|
res.samples.append("FAIL load ack")
|
||
|
|
return res
|
||
|
|
|
||
|
|
# parse name from recent lines
|
||
|
|
for line in sess.lines[-8:]:
|
||
|
|
m = TONE_OK_RE.search(line)
|
||
|
|
if m and m.group(1) == bin_id:
|
||
|
|
res.name = m.group(2)
|
||
|
|
break
|
||
|
|
|
||
|
|
sess.set_xpose(2)
|
||
|
|
sess.cmd("tone start", settle=0.7)
|
||
|
|
|
||
|
|
# I/II
|
||
|
|
sess.set_key(0)
|
||
|
|
sess.set_key(2)
|
||
|
|
sess.pump(hold)
|
||
|
|
|
||
|
|
# III
|
||
|
|
sess.set_key(0)
|
||
|
|
sess.set_key(8)
|
||
|
|
sess.pump(hold)
|
||
|
|
|
||
|
|
# VII + #F
|
||
|
|
sess.set_xpose(6)
|
||
|
|
sess.set_key(0)
|
||
|
|
sess.set_key(20)
|
||
|
|
sess.pump(hold)
|
||
|
|
|
||
|
|
sess.set_key(23) # stop
|
||
|
|
sess.pump(0.3)
|
||
|
|
|
||
|
|
chunk = sess.lines[mark:]
|
||
|
|
for line in chunk:
|
||
|
|
m = CH_SEEN_RE.search(line)
|
||
|
|
if m:
|
||
|
|
ch, role = int(m.group(1)), m.group(2)
|
||
|
|
res.channels[ch] += 1
|
||
|
|
res.roles[role] += 1
|
||
|
|
res.samples.append(line.strip())
|
||
|
|
ev = parse_pitch_line(line)
|
||
|
|
if not ev or not ev.is_on:
|
||
|
|
continue
|
||
|
|
res.channels[ev.ch] += 1
|
||
|
|
res.roles[ev.role] += 1
|
||
|
|
if ev.role == "bass" or ev.ch == 8:
|
||
|
|
res.bass_n += 1
|
||
|
|
elif ev.role == "drum" or ev.ch == 9:
|
||
|
|
res.drum_n += 1
|
||
|
|
else:
|
||
|
|
res.chord_n += 1
|
||
|
|
|
||
|
|
detail, ok = expect_for_event(ev)
|
||
|
|
tag = f"ch{ev.ch}({ev.role}) {ev.key_in}->{ev.key_out} chord={ev.chord} | {detail}"
|
||
|
|
if ev.chord <= 6:
|
||
|
|
if ok:
|
||
|
|
res.pass_ok += 1
|
||
|
|
else:
|
||
|
|
res.pass_fail += 1
|
||
|
|
res.samples.append("FAIL " + tag)
|
||
|
|
elif ev.xf:
|
||
|
|
if ok:
|
||
|
|
res.xf_ok += 1
|
||
|
|
else:
|
||
|
|
res.xf_fail += 1
|
||
|
|
res.samples.append("FAIL " + tag)
|
||
|
|
elif ev.role == "bass" or ev.ch == 8:
|
||
|
|
if ok:
|
||
|
|
res.bass_ok += 1
|
||
|
|
else:
|
||
|
|
res.bass_fail += 1
|
||
|
|
res.samples.append("FAIL " + tag)
|
||
|
|
else:
|
||
|
|
if ok:
|
||
|
|
res.iii_ok += 1
|
||
|
|
else:
|
||
|
|
res.iii_fail += 1
|
||
|
|
res.samples.append("FAIL " + tag)
|
||
|
|
|
||
|
|
return res
|
||
|
|
|
||
|
|
|
||
|
|
def main() -> int:
|
||
|
|
if hasattr(sys.stdout, "reconfigure"):
|
||
|
|
try:
|
||
|
|
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
||
|
|
except Exception:
|
||
|
|
pass
|
||
|
|
|
||
|
|
ap = argparse.ArgumentParser()
|
||
|
|
ap.add_argument("--hold", type=float, default=2.2)
|
||
|
|
ap.add_argument("--bin1", default="0,1,2,3,4,5,8,12,16,20,24,28", help="1.bin indices")
|
||
|
|
ap.add_argument("--bin3", default="0,1,2", help="3.bin indices")
|
||
|
|
ap.add_argument("--out", default="")
|
||
|
|
args = ap.parse_args()
|
||
|
|
|
||
|
|
bin1_idxs = [int(x) for x in args.bin1.split(",") if x.strip() != ""]
|
||
|
|
bin3_idxs = [int(x) for x in args.bin3.split(",") if x.strip() != ""]
|
||
|
|
|
||
|
|
import_deps()
|
||
|
|
jlink = connect_jlink("AT32F403AC")
|
||
|
|
results: list[PresetResult] = []
|
||
|
|
try:
|
||
|
|
cb = find_rtt_control_block(jlink)
|
||
|
|
if cb is None:
|
||
|
|
raise SystemExit("RTT CB not found")
|
||
|
|
print(f"RTT CB @ 0x{cb:08X}", flush=True)
|
||
|
|
jlink.rtt_start(cb)
|
||
|
|
wait_rtt_ready(jlink)
|
||
|
|
sess = RttSession(jlink)
|
||
|
|
sess.pump(0.3)
|
||
|
|
sess.cmd("log clear", 0.3)
|
||
|
|
|
||
|
|
print("\n######## BIN1 / 1.bin rhythms ########", flush=True)
|
||
|
|
for idx in bin1_idxs:
|
||
|
|
print(f"\n--- BIN1 idx={idx} ---", flush=True)
|
||
|
|
r = run_preset(sess, "1", idx, args.hold)
|
||
|
|
results.append(r)
|
||
|
|
print(
|
||
|
|
f"name={r.name} ch={dict(r.channels)} roles={dict(r.roles)} "
|
||
|
|
f"bass={r.bass_n} drum={r.drum_n} chord={r.chord_n} "
|
||
|
|
f"I/II {r.pass_ok}/{r.pass_fail} III {r.iii_ok}/{r.iii_fail} "
|
||
|
|
f"xf {r.xf_ok}/{r.xf_fail} bassMap {r.bass_ok}/{r.bass_fail}",
|
||
|
|
flush=True,
|
||
|
|
)
|
||
|
|
|
||
|
|
print("\n######## BIN3 / 3.bin universal ########", flush=True)
|
||
|
|
for idx in bin3_idxs:
|
||
|
|
print(f"\n--- BIN3 idx={idx} ---", flush=True)
|
||
|
|
r = run_preset(sess, "3", idx, args.hold)
|
||
|
|
results.append(r)
|
||
|
|
print(
|
||
|
|
f"name={r.name} ch={dict(r.channels)} roles={dict(r.roles)} "
|
||
|
|
f"bass={r.bass_n} drum={r.drum_n} chord={r.chord_n} "
|
||
|
|
f"I/II {r.pass_ok}/{r.pass_fail} III {r.iii_ok}/{r.iii_fail} "
|
||
|
|
f"xf {r.xf_ok}/{r.xf_fail} bassMap {r.bass_ok}/{r.bass_fail}",
|
||
|
|
flush=True,
|
||
|
|
)
|
||
|
|
|
||
|
|
# Summary vs plan
|
||
|
|
print("\n======== PLAN CHECK (bass和弦分通道八度) ========", flush=True)
|
||
|
|
with_bass = [r for r in results if r.bass_n > 0]
|
||
|
|
print(f"Presets tested: {len(results)}; with ch8(bass) NoteOn: {len(with_bass)}", flush=True)
|
||
|
|
if with_bass:
|
||
|
|
for r in with_bass:
|
||
|
|
print(
|
||
|
|
f" BASS HIT {r.bin_id}.bin[{r.idx}] {r.name}: "
|
||
|
|
f"bass_n={r.bass_n} map_ok={r.bass_ok} fail={r.bass_fail}",
|
||
|
|
flush=True,
|
||
|
|
)
|
||
|
|
else:
|
||
|
|
print(" NO preset produced ch8(bass). Plan bass rule NOT verified on-device.", flush=True)
|
||
|
|
|
||
|
|
# Aggregate chord rules
|
||
|
|
pass_ok = sum(r.pass_ok for r in results)
|
||
|
|
pass_fail = sum(r.pass_fail for r in results)
|
||
|
|
iii_ok = sum(r.iii_ok for r in results)
|
||
|
|
iii_fail = sum(r.iii_fail for r in results)
|
||
|
|
xf_ok = sum(r.xf_ok for r in results)
|
||
|
|
xf_fail = sum(r.xf_fail for r in results)
|
||
|
|
bass_ok = sum(r.bass_ok for r in results)
|
||
|
|
bass_fail = sum(r.bass_fail for r in results)
|
||
|
|
|
||
|
|
def gate(name, ok, fail, need=True):
|
||
|
|
status = "PASS" if fail == 0 and (ok > 0 or not need) else ("FAIL" if fail else "SKIP")
|
||
|
|
print(f" [{status}] {name}: ok={ok} fail={fail}", flush=True)
|
||
|
|
return status != "FAIL"
|
||
|
|
|
||
|
|
all_ok = True
|
||
|
|
all_ok &= gate("I/II PASS (keys1-6)", pass_ok, pass_fail)
|
||
|
|
all_ok &= gate("III+ chord map", iii_ok, iii_fail)
|
||
|
|
all_ok &= gate("xpose>=#F extra-12", xf_ok, xf_fail)
|
||
|
|
all_ok &= gate("bass ch8 map", bass_ok, bass_fail, need=False)
|
||
|
|
if not with_bass:
|
||
|
|
all_ok = False
|
||
|
|
print(" [FAIL] plan requires bass on code ch8 — never seen across 1.bin/3.bin samples", flush=True)
|
||
|
|
|
||
|
|
# channel histogram
|
||
|
|
ch_all: Counter = Counter()
|
||
|
|
for r in results:
|
||
|
|
ch_all.update(r.channels)
|
||
|
|
print(f"Channel histogram: {dict(sorted(ch_all.items()))}", flush=True)
|
||
|
|
|
||
|
|
stamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||
|
|
out = args.out or os.path.join(
|
||
|
|
os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
|
||
|
|
f"bin13_reg_{stamp}.log",
|
||
|
|
)
|
||
|
|
with open(out, "w", encoding="utf-8", newline="\n") as f:
|
||
|
|
f.write(f"# bin1/bin3 plan check {datetime.now().isoformat(timespec='seconds')}\n")
|
||
|
|
for line in sess.lines:
|
||
|
|
f.write(line + "\n")
|
||
|
|
f.write("\n# SUMMARY\n")
|
||
|
|
for r in results:
|
||
|
|
f.write(
|
||
|
|
f"# {r.bin_id}[{r.idx}] {r.name} ch={dict(r.channels)} "
|
||
|
|
f"bass={r.bass_n} I={r.pass_ok}/{r.pass_fail} "
|
||
|
|
f"III={r.iii_ok}/{r.iii_fail} xf={r.xf_ok}/{r.xf_fail} "
|
||
|
|
f"bassMap={r.bass_ok}/{r.bass_fail}\n"
|
||
|
|
)
|
||
|
|
print(f"Log: {out}", flush=True)
|
||
|
|
return 0 if all_ok else 1
|
||
|
|
finally:
|
||
|
|
try:
|
||
|
|
jlink.rtt_stop()
|
||
|
|
except Exception:
|
||
|
|
pass
|
||
|
|
jlink.close()
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
raise SystemExit(main())
|