This commit is contained in:
khodowany
2026-10-03 10:12:12 -05:00
parent 5fff5a165e
commit 59afd78205
14 changed files with 666 additions and 41 deletions
+61 -3
View File
@@ -123,10 +123,10 @@ def endings(source, incoming, part, harmony, bar, *, limit=8):
harmony.members(arrival,arrival+hold),(41,76),hold,'terminal',harmony.at(arrival).chord,
hold_until=arrival+hold)
cadence=sum(h.ending==(pitch.number()%12,arrival%4,hold) for h in incoming.history)
rank=abs(pitch.number()-goal)/24+.16*min(2,cadence)+.035*abs(float(hold)-3)+.015*float(release)
rank=destination_cost(pitch,arrival,arrival+hold,harmony,incoming,goal,True)+.16*min(2,cadence)+.035*abs(float(hold)-3)+.015*float(release)
values.append((rank,anchor))
selected=sequences.diverse(values,lambda x:(x[0],policy.stable_rank(source.id,x[1])),
lambda x:(x[1].duration,x[1].earliest%bar,x[1].pitch.number()>goal),limit)
selected=destination_options(values,lambda x:(x[0],policy.stable_rank(source.id,x[1])),
lambda x:x[1].pitch,lambda x:x[1].earliest,harmony,limit)
return tuple(a for _,a in selected)
@@ -159,3 +159,61 @@ def evaluate(notes, incoming, harmony, start, end, bar):
leaps=sum(max(0,abs(b.pitch.number()-a.pitch.number())-5)/12 for a,b in zip(notes,notes[1:]))/max(1,len(notes)-1)
history_cost=repetition(heard(notes,'develop'),incoming.history,bar)
return 2*checked.exposure+entry+leaps+.35*max(0,4-density,density-6)+.8*history_cost
@functools.lru_cache(maxsize=512)
def defining_members(symbol):
"""Written chord identity, without inventing omitted thirds or sevenths."""
chord=generator.harmony.parse_symbol(symbol)
degrees=generator.harmony.degrees(chord)
root=generator.model.NATURAL[chord['root']['letter']]+generator.model.alteration(chord['root']['alter'])
selected={d for d in (3,6,7) if d in degrees}
if chord['kind'] in ('suspended-second','suspended-fourth'):
selected.update(d for d in (2,4) if d in degrees)
if degrees.get(5,0):selected.add(5)
return frozenset(int(root+generator.harmony.degree_semitones(d,degrees[d]))%12 for d in selected)
def destination_cost(pitch, start, end, harmony, incoming=None, goal=None, terminal=False):
"""Shared bounded function/trajectory cost at retrieval and final selection."""
span=harmony.at(start)
if not span or not span.scale:return float('inf')
number=pitch.number(); pc=number%12
if pc not in harmony.members(start,end):return float('inf')
tonic=generator.model.parse_pitch_name(span.scale.root)
tonic_pc=int(generator.model.NATURAL[tonic['letter']]+generator.model.alteration(tonic['alter']))%12
function=0. if pc in defining_members(span.chord) or terminal and pc==tonic_pc else .25
register=min(.15,abs(number-goal)/80) if goal is not None else 0.
entry=min(.15,max(0,abs(number-incoming.last.pitch.number())-7)/80) if incoming and incoming.last else 0.
previous=next((h.ending[0] for h in reversed(incoming.history) if h.ending),None) if incoming else None
recurrence=.04 if previous==pc else 0.
following=next((s for s in harmony.spans if s.start>=end and s.scale and s.roles.basic),None)
lookahead=0.
if following:
future=[p for p in range(max(41,number-12),min(76,number+12)+1) if p%12 in following.roles.basic and p%12 in following.scale.pitches]
if future:lookahead=.04*min(1,min(abs(p-number) for p in future)/5)
# Contour comes from the heard theme, not a universal upward/nearest rule.
direction=0.
if incoming and incoming.home and len(incoming.home.notes)>1 and incoming.last:
motion=incoming.home.notes[-1].pitch.number()-incoming.home.notes[0].pitch.number()
direction=.025 if (number-incoming.last.pitch.number())*motion<0 else 0.
return function+register+entry+recurrence+lookahead+direction
def destination_family(pitch, time, harmony):
return next((degrees for pc,degrees in harmony.at(time).roles.degrees if pc==pitch.number()%12),())
def destination_options(values, rank, pitch, time, harmony, limit):
"""Keep chord-degree representatives before register/timing variants."""
ordered=sorted(values,key=rank)
selected=[];seen=set()
for value in ordered:
family=destination_family(pitch(value),time(value),harmony)
if family not in seen:
selected.append(value);seen.add(family)
if len(selected)==limit:return tuple(selected)
for value in ordered:
if value not in selected:selected.append(value)
if len(selected)==limit:break
return tuple(selected)