100 lines
5.8 KiB
Python
100 lines
5.8 KiB
Python
"""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 0<i<len(hint):
|
|
step=hint[i].pitch.number()-hint[i-1].pitch.number()
|
|
if purpose=='answer':step=-step
|
|
desired=(step>0)-(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)
|