"""Constructed SMF state experiment, not a synthesizer or audio test. Python 3."""
import struct,json,csv,pathlib
root=pathlib.Path(__file__).parent
def vlq(n):
 out=[n&127];n>>=7
 while n:out.insert(0,(n&127)|128);n>>=7
 return bytes(out)
def make(pc):
 track=b'\x00\xff\x51\x03\x07\xa1\x20'
 if pc is not None:track+=b'\x00'+bytes(pc)
 track+=b'\x00\x90\x3c\x50'+vlq(480)+b'\x80\x3c\x00\x00\xff\x2f\x00'
 return b'MThd'+struct.pack('>IHHH',6,0,1,480)+b'MTrk'+struct.pack('>I',len(track))+track
def parse(data,initial):
 assert data[:4]==b'MThd' and data[14:18]==b'MTrk'
 p=22;tick=0;programs=[initial]*16;notes=[];changes=[]
 def readvlq():
  nonlocal p
  n=0
  while True:
   b=data[p];p+=1;n=(n<<7)|(b&127)
   if b<128:return n
 while p<len(data):
  tick+=readvlq();s=data[p];p+=1
  if s==255:
   kind=data[p];p+=1;size=readvlq();p+=size
   if kind==47:break
   continue
  kind=s>>4;ch=s&15;size=1 if kind in (12,13) else 2
  vals=list(data[p:p+size]);p+=size
  if kind==12:programs[ch]=vals[0];changes.append([tick,ch+1,vals[0]])
  elif kind in (8,9):notes.append({'tick':tick,'channel':ch+1,'kind':'on' if kind==9 and vals[1]>0 else 'off','key':vals[0],'velocity':vals[1],**({'programAtOn':programs[ch]} if kind==9 and vals[1]>0 else {})})
 return {'programChanges':changes,'notes':notes}
cases={'program-0.mid':(192,0),'program-40.mid':(192,40),'wrong-channel.mid':(193,40),'no-program.mid':None}
rows=[];results=[]
for name,pc in cases.items():
 data=make(pc);(root/name).write_bytes(data)
 for initial in ([0,40] if pc is None else [0]):
  r=parse(data,initial);on=r['notes'][0];off=r['notes'][1]
  assert [(e['tick'],e['kind'],e['key'],e['velocity']) for e in r['notes']]==[(0,'on',60,80),(480,'off',60,0)]
  rows.append({'file':name,'initialProgram':initial,'programAtNoteOn':on['programAtOn'],'key':60,'onTick':0,'offTick':480,'velocity':80})
  results.append({'file':name,'initialProgram':initial,**r})
assert [r['programAtNoteOn'] for r in rows]==[0,40,0,0,40]
(root/'results.json').write_text(json.dumps({'scope':'Constructed channel-program state; no audio rendering, DAW or transcription test','division':480,'tempoMicrosecondsPerQuarter':500000,'cases':results},indent=2)+'\n',encoding='utf-8')
with (root/'observations.csv').open('w',newline='',encoding='utf-8') as f:
 w=csv.DictWriter(f,fieldnames=list(rows[0]));w.writeheader();w.writerows(rows)
print(json.dumps(rows))
