File size: 3,516 Bytes
11330a4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#!/usr/bin/env node
// Offline rival audit for the existing audible G#2 trace. No browser, no writes
// to production defaults, and no counterfactual is applied here.
import fs from 'node:fs';
import { dequantiseSal } from '../static/salience.js';
import { ScoreFollower, FOLLOWER_DEFAULTS } from '../static/follower.js';

const tracePath = process.argv[2] || '/tmp/notepilot-left-loop/mic-follower-hold-diagnostics-20260918-v2.json';
const tr = JSON.parse(fs.readFileSync(tracePath, 'utf8'));
const ev = tr.events, meta = tr.meta || {}, opt = meta.follower?.options || FOLLOWER_DEFAULTS;
const sr = meta.follower?.sr || 48000, frames = ev.filter(e => e.type === 'follow.frame' && Array.isArray(e.sal));
const attempts = ev.filter(e => e.type === 'input.testEvent' && e.scoreEventId === 1);
const nextTimes = attempts.map((a,i) => ev.find(e => e.type === 'input.testEvent' && e.t > a.t)?.t || a.t + 3000);
const input = e => ({seq:e.k, at:e.at, sr, win:meta.follower?.win||2048, hop:meta.follower?.hop||512,
  db:e.db, floorDb:e.fl, gated:!!e.g, attack:!!e.a, onset:e.on, sal:dequantiseSal(Uint8Array.from(e.sal), e.pk)});
const name = m => { const n=['C','C#','D','D#','E','F','F#','G','G#','A','A#','B']; return `${n[((m%12)+12)%12]}${Math.floor(m/12)-1}`; };
const finite = x => Number.isFinite(x) ? +x.toFixed(4) : null;
const hOffsets = [2,3,4,5,6,7,8,9,10].map(h => [h, Math.round(12*Math.log2(h))]);
const harmonic = q => { const d=q-44; const h=hOffsets.find(([_,o])=>Math.abs(d-o)<=1); return h ? `h${h[0]} (Δ${d-h[1]})` : null; };
const all = [];
const diag=[]; const sf=new ScoreFollower({solo:meta.rh, sampleRate:sr, win:2048, hop:meta.follower?.hop||512,
  position:0, options:{...opt, diagnostic:d=>diag.push(d)}});
const fired=new Set();
for (const f of frames) {
  for (let fi=0; fi<ev.length; fi++) { const af=ev[fi];
    if (af.type==='accomp.fire' && af.t<=f.t && !fired.has(fi)) { fired.add(fi); sf.accompanimentFired({pitches:af.pitches,atMs:af.t,durMs:500,beat:af.beat}); }
  }
  const before=diag.length;
  const out=sf.observe(input(f), f.t), d=diag[before];
  const ai=attempts.findIndex((a,i)=>f.t>=a.t && f.t<=nextTimes[i]);
  if (ai < 0) continue;
    const c=d?.hyps?.find(h=>h.i===1); if (!c || !d?.wrong) continue;
    const p=out.posterior?.find(x=>x.i===1)?.p ?? 0;
    all.push({attempt:ai, t:f.t, role:'all', p, ev:c.ev, distinct:c.distinct, ell:c.ell, rss:c.fit?.rss ?? null,
      explained:c.explained, gNw:c.fit?.gNw ?? null, gPv:c.fit?.gPv ?? null, ageMs: f.t-c.tOn,
      onsetDriven:c.onsetDriven, pWrong:out.pWrong, wrongEll:d.wrong.ell, q:d.wrong.q, path:d.wrong.path,
      feedQ:d.wrong.feed?.q ?? null, feedEll:d.wrong.feed?.ell ?? null, wrongGain:d.wrong.gain ?? null, wrongRss:d.wrong.rss ?? null, gHeld:d.wrong.gHeld || null});
}
for (let ai=0; ai<attempts.length; ai++) {
  const rows=all.filter(r=>r.attempt===ai); if(!rows.length) continue;
  const picks=[['peak-posterior',rows.reduce((x,y)=>y.p>x.p?y:x)],['peak-evidence',rows.reduce((x,y)=>y.ev>x.ev?y:x)]];
  if(ai===2) { const cross=rows.filter(r=>r.ev>=2.5).reduce((x,y)=>Math.abs(y.p-0.5)<Math.abs(x.p-0.5)?y:x,rows[0]); picks.push(['crossover',cross]); }
  for(const [role,r] of picks) console.log(JSON.stringify({...r,role,qName:r.q==null?null:name(r.q),harmonicOfGsharp:r.q==null?null:harmonic(r.q),deltaEll:finite(r.ell-r.wrongEll)}));
}
fs.writeFileSync(`${tracePath}.wrong-audit.json`, JSON.stringify({source:tracePath,expected:{midi:44,name:'G#2'},harmonicOffsets:hOffsets,rows:all},null,2)+'\n');