"""Duration-aware comparisons of realized music, independent of event labels.""" from __future__ import annotations import functools from generator.solo import types, policy def events(notes): return tuple((float(n.start),float(n.end),n.pitch.number()) for n in types.sounding_notes(notes)) def occupancy(left, right, modulo=False): """Intersection of pitch functions; repeated attacks cannot dilute a hold.""" i=j=0; total=0. while i=2] return dict(pitch_time=occupancy(candidate,source)/denominator, octave_insensitive=occupancy(candidate,source,True)/denominator, attack_agreement=sum((a,b,p%12) in attacks for a,b,p in source)/max(1,len(source)), arrival_agreement=sum((a,b,p%12) in attacks for a,b,p in arrivals)/max(1,len(arrivals)), interval_rhythm_coverage=min(1.,length/denominator),longest_fragment_beats=length,longest_fragment_attacks=count) def compare(candidate, source): return compare_events(events(candidate),events(source)) @functools.lru_cache(maxsize=8192) def copy_evidence(candidate, source, start, end): """Scan source-aligned windows, without an added-event denominator advantage.""" minimum=policy.DEFAULT.theme_copy_beats threshold=policy.DEFAULT.theme_copy_threshold fragment,count=interval_match(candidate,source) copied=fragment>=minimum and count>=policy.DEFAULT.theme_copy_attacks best=0.; best_offset=0. if end-start>=minimum and candidate: windows=sorted({start,*(a for a,_,_ in candidate if start<=a<=end-minimum)}) for left in windows: right=min(end,left+minimum) offsets={a[0]-b[0] for a in candidate if left<=a[0]left) count_in=sum(left<=a+offsetbest:best,best_offset=score,offset if score>=threshold:copied=True return {'near_copy':copied,'aligned_occupancy':best,'alignment_offset':best_offset, 'longest_fragment_beats':fragment,'longest_fragment_attacks':count} def near_copy(notes, source, start, end): return copy_evidence(events(notes),events(source),float(start),float(end))