Solo
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
"""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<len(left) and j<len(right):
|
||||
a,b=left[i],right[j]
|
||||
same = a[2]%12==b[2]%12 if modulo else a[2]==b[2]
|
||||
if same:
|
||||
total+=max(0.,min(a[1],b[1])-max(a[0],b[0]))
|
||||
if a[1]<=b[1]:i+=1
|
||||
else:j+=1
|
||||
return total
|
||||
|
||||
|
||||
def interval_match(left, right):
|
||||
"""Longest exact interval/rhythm fragment, allowing relocation/transposition."""
|
||||
def tokens(notes):
|
||||
return tuple((b[0]-a[0],a[1]-a[0],b[2]-a[2]) for a,b in zip(notes,notes[1:]))
|
||||
a,b=tokens(left),tokens(right); previous={}; best=(0.,0)
|
||||
for i,x in enumerate(a):
|
||||
current={}
|
||||
for j,y in enumerate(b):
|
||||
if x!=y:continue
|
||||
count=previous.get(j-1,0)+1
|
||||
current[j]=count
|
||||
# The last note's duration also belongs to the matching fragment.
|
||||
tail=min(left[i+1][1]-left[i+1][0],right[j+1][1]-right[j+1][0])
|
||||
duration=left[i+1][0]+tail-left[i+1-count][0]
|
||||
best=max(best,(duration,count+1))
|
||||
previous=current
|
||||
return best
|
||||
|
||||
|
||||
@functools.lru_cache(maxsize=8192)
|
||||
def compare_events(candidate, source):
|
||||
denominator=sum(b-a for a,b,_ in source)
|
||||
if not denominator:
|
||||
return dict(pitch_time=0.,octave_insensitive=0.,attack_agreement=0.,arrival_agreement=0.,
|
||||
interval_rhythm_coverage=0.,longest_fragment_beats=0.,longest_fragment_attacks=0)
|
||||
length,count=interval_match(candidate,source)
|
||||
attacks={(a,b,p%12) for a,b,p in candidate}
|
||||
arrivals=[n for n in source if n[1]-n[0]>=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+2 for b in source}
|
||||
for offset in sorted(offsets):
|
||||
window=tuple((max(left,a+offset),min(right,b+offset),p) for a,b,p in source if a+offset<right and b+offset>left)
|
||||
count_in=sum(left<=a+offset<right for a,_,_ in source)
|
||||
duration=sum(b-a for a,b,_ in window)
|
||||
if count_in<policy.DEFAULT.theme_copy_attacks or duration<minimum/2:
|
||||
continue
|
||||
score=occupancy(candidate,window,True)/duration
|
||||
if score>best: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))
|
||||
Reference in New Issue
Block a user