mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-25 18:05:12 -04:00
f75159c8d6
Root cause: BIOS music engine reads voice register 6 (byte offset 0x0C, CURVOL / ADSR_LEVEL) to find a free voice; recomp returned the never- written spu_regs value (always 0), so the engine saw every voice as silent and over-recycled voices 0-3 instead of fanning across all 24. After fix, recomp KEYON-per-voice counts match Beetle exactly (e.g. v0 2/2, v18 2/2, v22 1/1). Also: - Full ADSR envelope (Attack/Decay/Sustain/Release) ported from Beetle's CalcVCDelta/RunEnvelope. KEYOFF now triggers Release-phase decay rather than immediate silencing. - End-without-loop triggers Release (matches RunDecoder), not active=0. - ENDX latch at 0x1F801D9C/9E now reports actual block-end-reached state instead of active-mask. - SPU event ring (KEYON/KEYOFF/END_STOP/END_LOOP) on both backends with frame-stamped per-voice payload; 1 M entries each (32 MB) for chime + game-scene headroom. Beetle-side env>0 filter so silent-loop noise doesn't bury the audible-voice timeline (matches recomp behavior where deactivated voices don't fire further events). - spu_voices / spu_events / spu_events_reset debug-server commands on both ports. send_fmt buffer kept single-shot to avoid extra newlines. - tools/_spu_ring_diff.py: frame-aligned event-ring diff with per-voice count summary; chunk-count cap removed (was aborting reads at 98 KB). - tools/_spu_capture.py, _spu_probe.py, _probe_one.py: targeted probes used during diagnosis. Local-only beetle-psx change (gitignored): same env>0 filter at mednafen/psx/spu.cpp around line 345/352. Rebuild beetle-psx static lib if reapplying. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
148 lines
5.9 KiB
Python
148 lines
5.9 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
SPU event-ring diff. Both backends maintain an always-on ring buffer of
|
|
KEYON / KEYOFF / END_STOP / END_LOOP events, frame-stamped by absolute
|
|
simulated frame count. This script dumps both rings AFTER the chime is
|
|
done playing and aligns events to find divergences.
|
|
|
|
Usage:
|
|
python tools/_spu_ring_diff.py [count] # default 1000
|
|
|
|
Run AFTER both backends have played through the boot chime fully.
|
|
"""
|
|
|
|
import json, socket, sys
|
|
from collections import defaultdict
|
|
from typing import Optional
|
|
|
|
|
|
class Cli:
|
|
def __init__(self, port: int):
|
|
self.s = socket.create_connection(("127.0.0.1", port), timeout=10)
|
|
self.s.settimeout(5.0)
|
|
self.buf = b""
|
|
self.id = 0
|
|
self.port = port
|
|
|
|
def call(self, cmd: str, **kw) -> Optional[dict]:
|
|
self.id += 1
|
|
msg = {"cmd": cmd, "id": self.id, **kw}
|
|
self.s.sendall((json.dumps(msg) + "\n").encode())
|
|
# Both backends emit newline-terminated JSON. Read until we see one,
|
|
# then parse. If parse fails on the first line, surface why.
|
|
# Read until we see a newline or hit a hard byte cap; the socket
|
|
# timeout is the real deadline. TCP loopback can fragment a 200KB
|
|
# JSON object into hundreds of small reads.
|
|
BYTE_CAP = 16 * 1024 * 1024
|
|
while True:
|
|
if b"\n" in self.buf:
|
|
line, _, self.buf = self.buf.partition(b"\n")
|
|
try:
|
|
return json.loads(line.decode())
|
|
except Exception as e:
|
|
print(f"[err] port {self.port} parse failed: {e}; "
|
|
f"line_len={len(line)} tail={line[-120:]!r}",
|
|
file=sys.stderr)
|
|
return None
|
|
try:
|
|
chunk = self.s.recv(1 << 20)
|
|
except socket.timeout:
|
|
print(f"[err] port {self.port} recv timeout after "
|
|
f"{len(self.buf)} bytes buffered", file=sys.stderr)
|
|
return None
|
|
if not chunk:
|
|
print(f"[err] port {self.port} EOF after {len(self.buf)} bytes",
|
|
file=sys.stderr)
|
|
return None
|
|
self.buf += chunk
|
|
if len(self.buf) > BYTE_CAP:
|
|
print(f"[err] port {self.port} response exceeds {BYTE_CAP} bytes "
|
|
f"without newline", file=sys.stderr)
|
|
return None
|
|
|
|
|
|
def main():
|
|
count = int(sys.argv[1]) if len(sys.argv) > 1 else 1000
|
|
|
|
rec = Cli(4370)
|
|
bee = Cli(4380)
|
|
|
|
r_evs = rec.call("spu_events", count=count)
|
|
b_evs = bee.call("spu_events", count=count)
|
|
|
|
if not (r_evs and r_evs.get("ok")):
|
|
print(f"[err] recomp spu_events failed: {r_evs}", file=sys.stderr)
|
|
return 1
|
|
if not (b_evs and b_evs.get("ok")):
|
|
print(f"[err] beetle spu_events failed: {b_evs}", file=sys.stderr)
|
|
return 1
|
|
|
|
r_list = r_evs["events"]
|
|
b_list = b_evs["events"]
|
|
print(f"[recomp] total={r_evs['total']} returned={len(r_list)} "
|
|
f"frames={r_list[0]['frame'] if r_list else '-'}..{r_list[-1]['frame'] if r_list else '-'}")
|
|
print(f"[beetle] total={b_evs['total']} returned={len(b_list)} "
|
|
f"frames={b_list[0]['frame'] if b_list else '-'}..{b_list[-1]['frame'] if b_list else '-'}")
|
|
|
|
# Find frame-offset between the two by aligning the first KEYON of each.
|
|
r_first_kon = next((e for e in r_list if e["kind"] == "KEYON"), None)
|
|
b_first_kon = next((e for e in b_list if e["kind"] == "KEYON"), None)
|
|
if not (r_first_kon and b_first_kon):
|
|
print("[err] no KEYON events found")
|
|
return 1
|
|
delta = r_first_kon["frame"] - b_first_kon["frame"]
|
|
print(f"\n[align] first KEYON: rec.frame={r_first_kon['frame']} "
|
|
f"bee.frame={b_first_kon['frame']} delta_rec_minus_bee={delta}")
|
|
|
|
# Emit aligned timeline: bee frame → list of events. Add rec events
|
|
# offset by -delta (so rec frames align with bee frames).
|
|
print("\n=== ALIGNED TIMELINE (bee_frame: rec_event | bee_event) ===")
|
|
print("'r/b': what each backend records at the equivalent frame.")
|
|
print("ENV col is post-event envelope level (0..0x7FFF).\n")
|
|
|
|
# Group events per bee-frame.
|
|
by_bf = defaultdict(lambda: {"r": [], "b": []})
|
|
for e in r_list:
|
|
bf = e["frame"] - delta
|
|
by_bf[bf]["r"].append(e)
|
|
for e in b_list:
|
|
by_bf[e["frame"]]["b"].append(e)
|
|
|
|
last_summary = None
|
|
for bf in sorted(by_bf):
|
|
slot = by_bf[bf]
|
|
r_kinds = sorted((e["kind"], e["v"]) for e in slot["r"])
|
|
b_kinds = sorted((e["kind"], e["v"]) for e in slot["b"])
|
|
if r_kinds == b_kinds: continue # match, skip
|
|
# Print divergence
|
|
rstr = ",".join(f"{k}.v{v}" for k, v in r_kinds) or "-"
|
|
bstr = ",".join(f"{k}.v{v}" for k, v in b_kinds) or "-"
|
|
only_r = set(r_kinds) - set(b_kinds)
|
|
only_b = set(b_kinds) - set(r_kinds)
|
|
marker = ""
|
|
if only_r and not only_b: marker = " <- rec-extra"
|
|
elif only_b and not only_r: marker = " <- rec-missing"
|
|
elif only_r and only_b: marker = " <- divergent"
|
|
print(f" bf=+{bf-b_first_kon['frame']:>4} r=[{rstr}] b=[{bstr}]{marker}")
|
|
|
|
# Per-voice keyon/keyoff count summary
|
|
print("\n=== PER-VOICE EVENT COUNTS ===")
|
|
print(" v rec_kon bee_kon rec_koff bee_koff rec_endloop bee_endloop rec_endstop bee_endstop")
|
|
counts = {v: defaultdict(int) for v in range(24)}
|
|
for e in r_list: counts[e["v"]][f"r_{e['kind']}"] += 1
|
|
for e in b_list: counts[e["v"]][f"b_{e['kind']}"] += 1
|
|
for v in range(24):
|
|
c = counts[v]
|
|
if not any(c.values()): continue
|
|
print(f" {v:2d} "
|
|
f"{c['r_KEYON']:>7} {c['b_KEYON']:>7} "
|
|
f"{c['r_KEYOFF']:>8} {c['b_KEYOFF']:>8} "
|
|
f"{c['r_END_LOOP']:>11} {c['b_END_LOOP']:>11} "
|
|
f"{c['r_END_STOP']:>11} {c['b_END_STOP']:>11}")
|
|
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|