"""Complete pitch paths for joint phrase proposals, with explicit structural motion.""" import dataclasses from fractions import Fraction as F from generator import vocabulary from generator.solo import types, policy, sequences class Routes: def __init__(self,harmony,library): self.harmony=harmony;self.library=library;self.pitch_cache={};self.cache={} # Every short relative path is reachable by attack count and exact displacement. self.index={} self.steps=frozenset(s.steps[0] for s in library.paths.get(2,())) for values in library.paths.values(): for value in values:self.index.setdefault((len(value.steps)+1,value.offsets()[-1]),[]).append(value) def pitches(self,start,end): key=(start,end) if key not in self.pitch_cache: span=self.harmony.at(start) allowed=types.permitted(self.harmony.choices,start,end) if end-start>=2:allowed &= self.harmony.members(start,end)| (span.roles.specified if span else frozenset()) self.pitch_cache[key]=tuple(types.scale_pitch(span.scale,i) for i in range(-28,29) if types.scale_pitch(span.scale,i).number()%12 in allowed and self.harmony.profile.permits(types.scale_pitch(span.scale,i).number())) if span and span.scale else () return self.pitch_cache[key] def realize(self,notes,fixed,purpose,direction,seed,center,incoming=None,hint=()): """Only completed paths leave this function; internal frontier cuts have no cadence.""" if not notes:return () # Identical musical work is cached without event/provenance IDs. key=(tuple((n.start,n.end,n.pitch if i in fixed else None) for i,n in enumerate(notes)),tuple(sorted(fixed)),purpose,direction,seed,center,incoming.pitch if incoming else None,tuple((n.start,n.end,n.pitch) for n in hint)) if key in self.cache: return tuple(tuple(dataclasses.replace(n,pitch=p) for n,p in zip(notes,path)) for path in self.cache[key]) width=vocabulary.data('solo')['search']['route_frontier'];states=[(0.,())];count=len(notes) for i,n in enumerate(notes): options=(n.pitch,) if i in fixed else self.pitches(n.start,n.end) region=self.harmony.at(n.start) pitch_order={p:policy.stable_rank(seed,('pitch',i,p)) for p in options} following=[] for cost,path in states: prior=path[-1] if path else incoming.pitch if incoming else None phase=i/max(1,count-1) sign=1 if direction=='up' or direction=='arch' and phase<.5 or direction=='valley' and phase>=.5 else -1 target=next((notes[j].pitch for j in sorted(fixed) if j>i),None) choices=[] for pitch in options: number=pitch.number();motion=number-prior.number() if prior else 0 if not self.harmony.profile.permits(number) or prior and abs(motion)>12:continue if prior and i not in fixed: try: displacement=types.scale_index(region.scale,number)-types.scale_index(region.scale,prior.number()) if displacement and displacement not in self.steps:continue except ValueError: pass # Changed collection: retain the actual incoming pitch. local=max(0,abs(number-center)-10)/12 if i not in fixed: local+=max(0,abs(motion)-7)/3 if prior: span=region chordal=number%12 in span.roles.basic # Arpeggios expand during the gesture; the last portion connects to its goal. if purpose=='arpeggio' and phase<.65:local+=(0 if chordal else 1)+(.4 if abs(motion)<3 else 0) elif purpose=='neighbor':local+=max(0,abs(motion)-2)/2 else:local+=max(0,abs(motion)-3)/3 desired=sign if hint and 00)-(step<0) if purpose in ('return','head') and ((motion>0)-(motion<0))!=desired:continue local+=abs(abs(motion)-abs(step))/12 if purpose=='neighbor':desired=sign if i%2 else -sign local+=.35 if motion*desired<0 else .2 if motion==0 else 0 if len(path)>=2 and path[-2]==prior==pitch:local+=1 if target and phase>.6: remaining=max(1,next(j for j in sorted(fixed) if j>i)-i) local+=max(0,abs(target.number()-number)-remaining*2)/3 choices.append((local,pitch)) # Preserve distinct register/motion alternatives, not merely two nearest notes. for local,pitch in sorted(choices,key=lambda v:(v[0],pitch_order[v[1]]))[:width]: following.append((cost+local,(*path,pitch))) if not following:return () states=sequences.diverse(following,lambda v:(round(v[0],8),policy.stable_rank(seed,('path',v[1]))), lambda v:((v[1][-1].number()-v[1][0].number()>0),v[1][-1].number()),width) # Where applicable, add complete catalog routes as peers of the contextual route. if 2<=count<=4 and fixed=={0,count-1}: span=self.harmony.at(notes[0].start) if span and span.scale and all(self.harmony.at(n.start).scale==span.scale for n in notes): try: origin=types.scale_index(span.scale,notes[0].pitch.number());displacement=types.scale_index(span.scale,notes[-1].pitch.number())-origin for route in self.index.get((count,displacement),())[:32]: path=tuple(types.scale_pitch(span.scale,origin+x) for x in route.offsets()) if all(p in self.pitches(n.start,n.end) for p,n in zip(path,notes)):states=(*states,(0.,path)) except ValueError:pass paths=[] for _,path in sorted(states,key=lambda v:(v[0],policy.stable_rank(seed,v[1]))): if path not in paths:paths.append(path) if len(paths)==2:break self.cache[key]=tuple(paths) return tuple(tuple(dataclasses.replace(n,pitch=p) for n,p in zip(notes,path)) for path in paths)