Files
kennethreitz 00fc35de19 Refactor all tracks to be more pythonic — audio verified bit-identical
Every track: tuple-unpacked scale degrees, small local helpers
(rest_bars, chord_bars, play_phrase), data-driven drum patterns and
phrase tuples, sparse-event dicts, explicit velocity lists for fades,
dead code removed. Net -1,415 lines across 25 files.

Adds .fingerprint.py, a verification harness that hashes every audible
parameter of a score (notes, voicings, velocities, bends, drum hits,
LFO automation, part settings). All 25 tracks fingerprint identical to
their pre-refactor baselines, stored in .fingerprints/.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 00:32:38 -04:00

108 lines
2.9 KiB
Python

#!/usr/bin/env python3
"""Fingerprint a track's score — every audible parameter, deterministically.
Usage:
uv run python .fingerprint.py tracks/foo.py # print sha256
uv run python .fingerprint.py tracks/foo.py --dump # print full JSON
Used to verify refactors are audio-identical: fingerprint before and after.
"""
import hashlib
import json
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent))
from play import load_score
def _scalar(v):
if isinstance(v, float):
return round(v, 10)
return v
def _tone(t):
"""Serialize a Tone / Chord / _DrumTone / None to plain data."""
if t is None:
return None
if hasattr(t, "tones"): # Chord
return {"chord": [_tone(x) for x in t.tones]}
if hasattr(t, "sound"): # _DrumTone
return {"drum": str(t.sound)}
d = {"name": t.name, "octave": t.octave}
if getattr(t, "system_name", None):
d["system_name"] = t.system_name
freq = getattr(t, "_frequency", None)
if freq is not None:
d["frequency"] = _scalar(freq)
sys_ = getattr(t, "_system", None)
if sys_ is not None:
d["system"] = repr(sys_)
return d
def fingerprint(path):
score, mod = load_score(path)
data = {
"time_signature": str(score.time_signature),
"bpm": score.bpm,
"system": score.system,
"temperament": score.temperament,
"reference_pitch": score.reference_pitch,
"parts": {},
}
for name, part in score.parts.items():
pd = {}
for k, v in sorted(vars(part).items()):
if k in ("notes", "_drum_hits", "_automation", "_fretboard"):
continue
if k == "synth_kw":
pd[k] = {kk: _scalar(vv) for kk, vv in sorted(v.items())} if v else {}
continue
pd[k] = _scalar(v)
pd["notes"] = [
{
"tone": _tone(n.tone),
"duration": _scalar(float(getattr(n.duration, "value", n.duration))),
"velocity": n.velocity,
"bend": _scalar(n.bend),
"bend_type": n.bend_type,
"lyric": n.lyric,
"articulation": n.articulation,
"hold": n._hold,
}
for n in part.notes
]
pd["drum_hits"] = [
json.loads(json.dumps(h, default=repr)) for h in part._drum_hits
]
pd["automation"] = [
[_scalar(beat), {k: _scalar(v) for k, v in sorted(vals.items())}]
for beat, vals in part._automation
]
data["parts"][name] = pd
return data
def main():
path = sys.argv[1]
data = fingerprint(path)
blob = json.dumps(data, sort_keys=True, default=repr)
if "--dump" in sys.argv:
print(json.dumps(data, indent=1, sort_keys=True, default=repr))
else:
print(hashlib.sha256(blob.encode()).hexdigest())
if __name__ == "__main__":
main()