import json,pathlib,datetime,hashlib,subprocess,wave,array,shutil,sys
sys.stdout.reconfigure(encoding='utf-8')
D=pathlib.Path(__file__).parent;E=pathlib.Path(sys.argv[1]) if len(sys.argv)>1 else D/'evidence';E.mkdir(parents=True,exist_ok=True);R=pathlib.Path('D:/AudioToMidiNow');G=pathlib.Path('D:/JOBS/2026-11-07-guitar-to-midi');P=pathlib.Path('D:/JOBS/2026-11-08-piano-to-midi');O=pathlib.Path('D:/JOBS/2026-11-14-audio-to-midi/evidence')
FF=R/'ops/research/daily-2026-10-29/tools/node_modules/@ffmpeg-installer/win32-x64/ffmpeg.exe'
def save(p,x):p.write_text(json.dumps(x,ensure_ascii=False,indent=2)+'\n',encoding='utf-8')
def read(p):return json.loads(p.read_text(encoding='utf-8-sig'))
def sha(p):return hashlib.sha256(p.read_bytes()).hexdigest()
planPath=D/'experiment-plan.json'
if not planPath.exists():
 save(planPath,{'frozenAt':datetime.datetime.now(datetime.timezone.utc).isoformat(),'dateSeoul':'2026-11-15','keyword':'m4a to midi','status':'FROZEN_BEFORE_OUTPUT','mode':2,'sampleRate':44100,'monoRule':'Arithmetic channel average, nearest integer PCM16, then common0.5 gain and nearest integer; no normalization/denoising/parameter tuning','gain':0.5,'activeSeconds':5,'zeroTailSeconds':0.5,'sourceWindows':[{'id':'guitar15','source':'guitar','start':15,'end':20},{'id':'piano36','source':'piano','start':36,'end':41},{'id':'piano61','source':'piano','start':61,'end':66}],'selectionRule':'Three windows fixed before codec outputs or converter results; previously archived human performances reused and explicitly disclosed, not new independent performers or an iPhone capture. No best-case selection.','sourcePolicy':'Existing verified GuitarSet1.1.0 and Vienna4x22 CC BY4.0 originals; controlled exported codec files, not genuine iOS Voice Memo files','audioMinusPianoMidiSeconds':-0.63375,'codecPaths':[{'id':'raw','kind':'Original mono PCM16 WAV baseline'},{'id':'aac','kind':'AAC128kbps M4A, decoded with existing FFmpeg to monoPCM16 WAV'},{'id':'alac','kind':'ALAC M4A, decoded with same FFmpeg to monoPCM16 WAV'}],'matching':{'pitch':'Exact integer MIDI pitch','onsetToleranceSeconds':0.05,'algorithm':'Reference onset order then pitch; nearest unused exact-pitch output; no fitted global shift, octave credit or widened tolerance','headlineReference':'Only new annotated attacks within original5s crop; carried-in notes retained separately for context, not attacks','offset':'Duration and ending errors of actual matched pairs reported separately; acoustic pedal decay not hardware key duration'},'runtimeScope':'Offline Node exact production detector after local FFmpeg decoding. Does NOT validate native Chrome M4A decoder, actual UI downloads, mobile browser, private file handling or rankings.','browserRequired':'12actual UI attempts: each3clip x AAC M4A, same AAC-decoded WAV, ALAC M4A, same ALAC-decoded WAV; actual downloaded MIDI independently parsed, never relabel offline as browser. Browser differences trigger explicit analysis.','noveltyBoundary':'Competitor audiotomidiconverter.com/m4a-to-midi already measures seven encodings and bit-identical outputs; no claim codec/hash comparison itself is unique. Only new useful annotated-human-performance findings can count after proper top10 and own-intent review.','sourceConfounders':['Same archived recordings, not3independent participants','Guitar per-string annotation timing/pitch-rounding uncertainty','Piano inherited fixed timing calibration, not refitted; hardware key duration differs from acoustic sustain','Carried-in held notes and real overlapping notes remain contextual, monophonic detector not promoted to polyphony']})
plan=read(planPath);shutil.copyfile(planPath,E/'experiment-plan.json')
for n in ['detector-snapshot.js','convert.mjs','midi_reader.py','score-quality.py']:shutil.copyfile(O/n,E/n)
assert (E/'detector-snapshot.js').read_text(encoding='utf-8').strip() in (R/'dist/script.js').read_text(encoding='utf-8')
sources=[(G/'00_BN1-129-Eb_solo_mic.wav',G/'source-provenance.json'),(G/'00_BN1-129-Eb_solo.jams',G/'source-provenance.json'),(P/'Mozart_K331_1st-mov_p01.wav',P/'source-provenance.json'),(P/'Mozart_K331_1st-mov_p01.mid',P/'source-provenance.json')]
for f,p in sources:assert sha(f)==next(x['sha256']for x in read(p)['files']if x['saved']==f.name)
for src,name in [(G/'source-provenance.json','guitar-source-provenance.json'),(P/'source-provenance.json','piano-source-provenance.json'),(G/'00_BN1-129-Eb_solo.jams','guitar-original.jams'),(P/'Mozart_K331_1st-mov_p01.mid','piano-original-reference.mid')]:shutil.copyfile(src,E/name)
g=read(G/'00_BN1-129-Eb_solo.jams');gnotes=[{'pitch':round(n['value']),'annotatedPitch':n['value'],'start':n['time'],'duration':n['duration'],'source':'guitar'}for a in g['annotations']if a['namespace']=='note_midi'for n in a['data']]
pnotes=[{**n,'start':n['start']+plan['audioMinusPianoMidiSeconds'],'source':'piano'}for n in read(P/'Mozart_K331_1st-mov_p01-reference-all.json')['notes']]
inputs=[];files=[];browser=[];refs=[]
def encode(args):
 p=subprocess.run([str(FF),'-hide_banner','-loglevel','error','-nostdin','-y',*map(str,args)],capture_output=True);assert p.returncode==0,p.stderr.decode(errors='replace')[:300]
for w in plan['sourceWindows']:
 cid=w['id'];src=G/'00_BN1-129-Eb_solo_mic.wav'if w['source']=='guitar'else P/'Mozart_K331_1st-mov_p01.wav'
 with wave.open(str(src),'rb')as inp:
  sr=inp.getframerate();ch=inp.getnchannels();assert sr==44100 and inp.getsampwidth()==2;inp.setpos(round(w['start']*sr));v=array.array('h',inp.readframes(5*sr))
 values=[round(round(sum(v[i:i+ch])/ch)*plan['gain'])for i in range(0,len(v),ch)]+[0]*round(.5*sr);assert max(abs(v)for v in values)<32767
 raw=E/(cid+'-raw.wav')
 with wave.open(str(raw),'wb')as out:out.setparams((1,2,sr,len(values),'NONE','not compressed'));out.writeframes(array.array('h',values).tobytes())
 ns=gnotes if w['source']=='guitar'else pnotes;attack=[];carried=[]
 for n in ns:
  a=n['start'];end=a+n['duration'];s=a-w['start'];duration=min(end,w['end'])-max(a,w['start'])
  if duration<=0:continue
  rec={**n,'originalStart':a,'start':max(s,0),'duration':duration}
  if w['start']<=a<w['end']:attack.append(rec)
  elif a<w['start']<end:carried.append(rec)
 attack.sort(key=lambda n:(n['start'],n['pitch']));refs.append({'id':cid,'attacks':len(attack),'carried':len(carried),'crop':[w['start'],w['end']]})
 for path in ['raw','aac','alac']:
  name=cid+'-'+path
  if path!='raw':
   m4a=E/(name+'.m4a');decoded=E/(name+'.wav');codec=['-c:a','aac','-b:a','128k']if path=='aac'else['-c:a','alac'];encode(['-i',raw,*codec,m4a]);encode(['-i',m4a,'-c:a','pcm_s16le','-ac','1','-ar','44100',decoded]);browser.extend([{'id':name+'-m4a','input':m4a.name,'status':'NOT_RUN','actualAttempt':False},{'id':name+'-wav','input':decoded.name,'status':'NOT_RUN','actualAttempt':False}])
  else:decoded=raw
  save(E/(name+'-reference.json'),{'condition':name,'notes':attack,'carriedInNotesNotScoredAsAttacks':carried,'annotationMeaning':'Per-string guitar notes or hardware piano keys; no claim of infallible acoustic ground truth','protocol':'experiment-plan.json'})
  inputs.append({'id':name,'input':decoded.name,'mode':2})
  with wave.open(str(decoded),'rb')as a:pcm=a.readframes(a.getnframes());samples=a.getnframes();rate=a.getframerate()
  files.append({'id':name,'input':decoded.name,'inputSha256':sha(decoded),'decodedPCMsha256':hashlib.sha256(pcm).hexdigest(),'samples':samples,'sampleRate':rate,'durationSeconds':samples/rate,'samePCMAsOriginal':pcm==array.array('h',values).tobytes(),'encodedM4A':None if path=='raw'else name+'.m4a','m4aSha256':None if path=='raw'else sha(E/(name+'.m4a'))})
save(E/'inputs.json',{'cases':inputs});save(E/'input-generation.json',{'scope':'Measured inputs only, no converter/browsers success implied','windows':refs,'files':files,'sourceFiles':[{'file':str(f),'sha256':sha(f)}for f,p in sources],'ffmpegExecutableSHA256':sha(FF)})
if not (D/'browser-conversion-checklist.json').exists():save(D/'browser-conversion-checklist.json',{'result':'NOT_RUN','cases':browser,'actualDownloads':0,'browserDecoderUnknown':True})
(E/'ATTRIBUTION.md').write_text('''# Real recordings and modifications

GuitarSet1.1.0, Qingyang Xi and colleagues, https://doi.org/10.5281/zenodo.3371780, CC BY4.0: archived00_BN1-129-Eb_solo microphone human guitar with per-string JAMS note labels. Original hash/provenance retained. New declared crop15–20s reuses the old performance, not a new performer.

Vienna4x22 Piano Corpus, Werner Goebl, https://doi.org/10.21939/4X22, CC BY4.0: archived human Mozart_K331_1st-mov_p01 grand-piano recording and hardware key MIDI. New declared crops36–41s and61–66s reuse the old performance. Fixed audio-minus-MIDI offset-0.63375s inherited from prior source calibration, not fitted to these outputs. Key duration is not pedal/acoustic decay.

Modifications2026-11-15: declared5s crops; arithmetic stereo-to-mono average, common0.5gain, half-second zero tail, controlled AAC128kbps or ALAC encoding intoM4A with the existing local FFmpeg, and local PCM16 WAV decode. These are controlled exported human-recording copies, not authentic iPhone Voice Memo exports; mobile/Safari and real phone microphone behavior are untested. No source-author endorsement; no synthesized instrument replacement. Carried-in notes are separately retained rather than scored as new attacks.
''',encoding='utf-8')
print(json.dumps({'status':'PREPARED_BEFORE_CONVERTER_OUTPUT','referenceWindows':refs,'offlineInputs':len(inputs),'browserCasesRequired':len(browser)}))
