This commit is contained in:
khodowany
2026-10-03 12:44:53 -05:00
parent 59afd78205
commit 04c6ea2e00
25 changed files with 673 additions and 185 deletions
+38 -10
View File
@@ -7,7 +7,7 @@ from fractions import Fraction as F
import generator.model
import generator.harmony
from generator.solo import types, policy, sequences
from generator.solo import types, policy, sequences, profiles
def heard(notes, operation, anchor=None):
@@ -52,7 +52,11 @@ def repetition(figure, history, bar):
return min(1., discount*overuse/2) + .3*oscillation + .3*min(1.,cadence/2)
def register_goal(incoming, part, fallback):
def register_goal(incoming, part, fallback, profile=None):
if profile and profile.reference is not None:
recent = [p for h in incoming.history[-2:] for _,_,p in h.events]
center = round(sum(recent)/len(recent)) if recent else profile.reference
return max(profile.reference-4,min(profile.reference+4,center))
recent = [p for h in incoming.history[-2:] for _,_,p in h.events]
if not recent:
return fallback
@@ -76,7 +80,7 @@ def endpoint_cost(finish, start, end, count, harmony, bar):
degrees=generator.harmony.degrees(generator.harmony.parse_symbol(span.chord)) if span and span.scale else {}
dominant=degrees.get(3)==0 and degrees.get(7)==-1
short_remainder=.2*max(0,4-remaining) if remaining else 0.
return pacing+.035*abs(length-5)+.16*dominant+.1*(length==8)+short_remainder
return pacing+(.08 if ending_effect(finish-F(1,100),harmony,False)=='local_settle' else 0)+.035*abs(length-5)+.16*dominant+.1*(length==8)+short_remainder
def window_options(analysis, measures, start, end, harmony, incoming, *, limit=2):
@@ -105,11 +109,11 @@ def window_options(analysis, measures, start, end, harmony, incoming, *, limit=2
return tuple(sorted(values,key=lambda s:(endpoint_cost(s.end,start,end,incoming.closures,harmony,bar),s.end))[:limit])
def endings(source, incoming, part, harmony, bar, *, limit=8):
def endings(source, incoming, part, harmony, bar, *, limit=8, final=False):
"""Choose pitch AND sounding interval before generating the approach."""
from generator.solo.leads import available
base=incoming.last.pitch.number() if incoming.last else source.notes[0].pitch.number() if source.notes else 55
goal=register_goal(incoming,part,base)
goal=register_goal(incoming,part,base,harmony.profile)
values=[]
duration=source.end-source.start
holds=(F(2),F(5,2),F(3),F(7,2),F(4)) if duration>=4 else (min(F(2),duration/2),)
@@ -120,10 +124,10 @@ def endings(source, incoming, part, harmony, bar, *, limit=8):
for pitch in available(harmony,arrival,arrival+hold,members=True):
if abs(pitch.number()-goal)>12:continue
anchor=types.Anchor(f'{source.id}:close:{arrival}:{pitch.number()}',arrival,arrival,pitch,
harmony.members(arrival,arrival+hold),(41,76),hold,'terminal',harmony.at(arrival).chord,
hold_until=arrival+hold)
harmony.members(arrival,arrival+hold),harmony.profile.register,hold,'terminal',harmony.at(arrival).chord,
hold_until=arrival+hold,effect=ending_effect(arrival,harmony,final))
cadence=sum(h.ending==(pitch.number()%12,arrival%4,hold) for h in incoming.history)
rank=destination_cost(pitch,arrival,arrival+hold,harmony,incoming,goal,True)+.16*min(2,cadence)+.035*abs(float(hold)-3)+.015*float(release)
rank=harmony.profile.cost(pitch.number())+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=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)
@@ -158,7 +162,7 @@ def evaluate(notes, incoming, harmony, start, end, bar):
entry=max(0,abs(notes[0].pitch.number()-incoming.last.pitch.number())-5)/12 if incoming.last else 0.
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
return harmony.profile.cost(profiles.weighted_center(notes))+2*checked.exposure+entry+leaps+.35*max(0,4-density,density-6)+.8*history_cost
@functools.lru_cache(maxsize=512)
@@ -197,7 +201,8 @@ def destination_cost(pitch, start, end, harmony, incoming=None, goal=None, termi
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
pending=.08 if incoming and incoming.next_destination and pc not in incoming.next_destination else 0.
return function+register+entry+recurrence+lookahead+direction+pending
def destination_family(pitch, time, harmony):
@@ -217,3 +222,26 @@ def destination_options(values, rank, pitch, time, harmony, limit):
if value not in selected:selected.append(value)
if len(selected)==limit:break
return tuple(selected)
def ending_effect(time, harmony, final=False):
if final: return 'handoff'
current=harmony.at(time)
previous=next((s for s in reversed(harmony.spans) if current and s.end<=current.start and s.chord!=current.chord and s.scale),None)
if not current or not current.scale or not previous: return 'local_settle'
old=generator.harmony.parse_symbol(previous.chord)
new=generator.harmony.parse_symbol(current.chord)
degrees=generator.harmony.degrees(old)
def root(chord):
p=chord['root'];return int(generator.model.NATURAL[p['letter']]+generator.model.alteration(p['alter']))%12
if degrees.get(3)==0 and degrees.get(7)==-1 and (root(old)-root(new))%12==7:
return 'cadential_arrival'
return 'local_settle'
def next_destination(anchor,harmony):
if anchor.effect!='local_settle': return ()
following=[s for s in harmony.spans if s.start>=anchor.hold_until and s.scale]
resolution=next((s for s in following[:2] if ending_effect(s.start,harmony)=='cadential_arrival'),None)
selected=resolution or next(iter(following),None)
return tuple(sorted(selected.roles.basic & selected.scale.pitches)) if selected else ()