mirror of
https://github.com/kennethreitz/interpretations.git
synced 2026-07-21 18:49:29 +00:00
00fc35de19
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>
108 lines
2.9 KiB
Python
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()
|