import pathlib,wave,array,json,math
from midi_reader import parse,max_polyphony
D=pathlib.Path(__file__).parent
name='Mozart_K331_1st-mov_p01';m=parse((D/(name+'.mid')).read_bytes())
with wave.open(str(D/(name+'.wav'))) as w:
    assert w.getparams()[:3]==(2,2,44100);raw=array.array('h',w.readframes(w.getnframes()))
rate=44100
# Calibrate a single clock offset from first audible attack, before any detected
# pitch is examined. Fixed 10ms stereo RMS bins and fixed threshold 0.004.
rms=[math.sqrt(sum(x*x for x in raw[i:i+882])/882)/32768 for i in range(0,len(raw)-882,882)]
first=next(i for i in range(len(rms)-1) if rms[i]>=.004 and rms[i+1]>=.004)
audio_first=first/100;offset=audio_first-m['notes'][0]['start']
start,end=2.20,8.20;ia,ib=round(start*rate),round(end*rate)
stereo=raw[ia*2:ib*2];mono=array.array('h',(round((stereo[i]+stereo[i+1])/2) for i in range(0,len(stereo),2)))
def save(fn,a):
    with wave.open(str(D/fn),'wb') as w:w.setparams((1,2,rate,len(a),'NONE','not compressed'));w.writeframes(a.tobytes())
save('piano.wav',mono);save('piano-half-gain.wav',array.array('h',(round(x/2) for x in mono)))
notes=[]
for n in m['notes']:
    a=n['start']+offset-start;b=a+n['duration']
    if a<end-start and b>0:notes.append({**n,'sourceMidiStart':n['start'],'start':max(0,a),'duration':min(end-start,b)-max(0,a)})
controls=[{**n,'time':n['time']+offset-start} for n in m['controls'] if start<=n['time']+offset<end]
clusters=[]
for n in m['notes']:
    if not clusters or n['start']-clusters[-1][0]>.10:clusters.append([n['start']])
    else:clusters[-1].append(n['start'])
checks=[]
for group in clusters[1:9]:
    expected=group[0]+offset;k=round(expected*100)
    # Validation only: maximum RMS rise in +/-50ms, no offset refit.
    peak=max(range(max(1,k-5),min(len(rms),k+6)),key=lambda j:rms[j]-rms[j-1])
    checks.append({'expectedAudioOnset':expected,'peakRmsRiseBin':peak/100,'residualSeconds':peak/100-expected})
reference={'source':name,'realRecording':True,'sampleRate':rate,'channelsOriginal':2,'channelsCrop':1,'monoRule':'round((left+right)/2), signed PCM16; no normalization','cropSourceSeconds':[start,end],'durationSeconds':end-start,'alignment':{'method':'First sustained10ms stereo RMS>=0.004 minus first MIDI key-on; offset fixed before evaluating output pitches; subsequent8groups validate only','audioFirstRmsBinSeconds':audio_first,'firstMidiKeyOnSeconds':m['notes'][0]['start'],'audioMinusMidiSeconds':offset,'validation':checks,'uncertainty':'10ms bin resolution plus threshold/attack uncertainty; nominal hardware key-held notes, not human audio annotation; no per-note alignment optimization'},'referenceNotesMeaning':'Hardware key-on/key-off from original SE290 MIDI, shifted by one disclosed onset-derived offset; excludes pedal-held/acoustic decay; not count of all sounding frequencies','notes':notes,'controls':controls,'maxKeyHeldPolyphony':max_polyphony(notes)}
(D/'reference.json').write_text(json.dumps(reference,indent=2),encoding='utf-8');print(json.dumps({'notes':len(notes),'pitches':sorted(set(n['pitch'] for n in notes)),'maxKeyHeldPolyphony':max_polyphony(notes),'alignment':reference['alignment']}))
