"""Synthetic MIDI timing comparison. Python standard library only. Run in an empty output directory."""
import json,struct,pathlib
notes=[dict(id='A',pitch=60,start=12,end=252,velocity=80),dict(id='B',pitch=62,start=247,end=463,velocity=90),dict(id='C',pitch=64,start=487,end=518,velocity=70),dict(id='D',pitch=65,start=726,end=1002,velocity=85)]
grid=120
def snap(t): return ((t+grid//2)//grid)*grid
def vlq(n):
    b=[n&127]; n>>=7
    while n: b.insert(0,(n&127)|128); n>>=7
    return bytes(b)
def midi(ns):
    events=[]
    for n in ns:
        events.extend([(n['start'],1,bytes([144,n['pitch'],n['velocity']])),(n['end'],0,bytes([128,n['pitch'],0]))])
    data=b'\x00\xff\x51\x03\x07\xa1\x20'; last=0
    for t,_,b in sorted(events): data+=vlq(t-last)+b; last=t
    data+=b'\x00\xff\x2f\x00'
    return b'MThd'+struct.pack('>IHHH',6,0,1,480)+b'MTrk'+struct.pack('>I',len(data))+data
variants={'raw':notes,'starts-preserve-duration':[{**n,'start':snap(n['start']),'end':snap(n['start'])+n['end']-n['start']} for n in notes],'starts-fixed-ends':[{**n,'start':snap(n['start'])} for n in notes],'both-edges-guarded':[{**n,'start':snap(n['start']),'end':max(snap(n['start'])+1,snap(n['end']))} for n in notes]}
result={'type':'constructed timing experiment, not DAW or transcription benchmark','ppq':480,'tempoBpm':120,'gridTicks':120,'tieRule':'nearest, exact halfway rounds later','minimumDurationTicks':1,'variants':{}}
for name,ns in variants.items():
    assert all(n['end']>n['start'] for n in ns)
    blob=midi(ns); pathlib.Path(name+'.mid').write_bytes(blob)
    result['variants'][name]={'bytes':len(blob),'notes':[{**n,'duration':n['end']-n['start']} for n in ns],'gaps':[ns[i+1]['start']-ns[i]['end'] for i in range(len(ns)-1)]}
pathlib.Path('raw-notes.json').write_text(json.dumps(notes,indent=2)+'\n')
pathlib.Path('results.json').write_text(json.dumps(result,indent=2)+'\n')
print(json.dumps(result,indent=2))
