46 lines
2.8 KiB
JavaScript

// Pair already-recognized synthetic identities in a COMMON measured clock.
// Refuse multiple same-code candidates instead of choosing the smallest skew.
// Repeated palettes without an unambiguous time bound are not unique IDs.
const valid=e=>e&&Number.isInteger(e.code)&&e.code>=1&&e.code<=16&&
Number.isFinite(e.startMs)&&Number.isFinite(e.endMs)&&e.startMs>=0&&e.endMs>=e.startMs&&
e.endMs-e.startMs<=250;
const quantile=(values,p)=>values.length?[...values].sort((a,b)=>a-b)[Math.ceil(values.length*p)-1]:null;
export function pairedSummary(audio,video,{clockUncertaintyMs=null,maxSkewMs=2000}={}) {
const result={pairs:0,ambiguous:0,unpairedAudio:audio.length,unpairedVideo:video.length,
skewAbsP95UpperMs:null,skewAbsMaxUpperMs:null,medianSignedMs:null,estimatedMedianSignedMs:null,
clockUncertaintyMs:null,releaseEligible:false};
if(audio.length>256||video.length>256||!audio.every(valid)||!video.every(valid)||
!Number.isFinite(maxSkewMs)||maxSkewMs<=0||maxSkewMs>10000)return result;
for(const events of [audio,video]) {
if(events.some((e,i)=>i&&e.startMs<=events[i-1].startMs))return result;
}
const candidates=audio.map(a=>video.map((v,i)=>a.code===v.code&&
Math.max(a.startMs-v.endMs,v.startMs-a.endMs)<=maxSkewMs?i:-1).filter(i=>i>=0));
const reverse=video.map((_,i)=>candidates.reduce((n,c)=>n+Number(c.includes(i)),0));
const pairs=[];let lastVideo=-1;
for(let i=0;i<audio.length;i++) {
const match=candidates[i];
if(match.length!==1||reverse[match[0]]!==1){if(match.length)result.ambiguous++;continue;}
const index=match[0];
if(index<=lastVideo){result.ambiguous++;continue;}
lastVideo=index;pairs.push([audio[i],video[index]]);
}
result.pairs=pairs.length;result.unpairedAudio-=pairs.length;result.unpairedVideo-=pairs.length;
result.estimatedMedianSignedMs=quantile(pairs.map(([a,v])=>(a.startMs+a.endMs-v.startMs-v.endMs)/2),.5);
// Identity matches without independently bounded clock alignment must not
// be converted into a calibrated skew or accepted qualification result.
if(typeof clockUncertaintyMs!=='number'||!Number.isFinite(clockUncertaintyMs)||clockUncertaintyMs<0||clockUncertaintyMs>1000)return result;
result.clockUncertaintyMs=clockUncertaintyMs;
const signed=[],bounds=[];
for(const [a,v] of pairs) {
// Positive = audio later. Interval arithmetic includes detection error.
signed.push((a.startMs+a.endMs-v.startMs-v.endMs)/2);
bounds.push(Math.max(Math.abs(a.startMs-v.endMs),Math.abs(a.endMs-v.startMs))+clockUncertaintyMs);
}
result.medianSignedMs=quantile(signed,.5);
result.skewAbsP95UpperMs=quantile(bounds,.95);
result.skewAbsMaxUpperMs=bounds.length?Math.max(...bounds):null;
return result;
}