Sepearate Generation

This commit is contained in:
khodowany
2026-10-04 09:05:25 -05:00
parent e8ac7b5edd
commit 8fdd736e13
56 changed files with 1739 additions and 341 deletions
+18 -17
View File
@@ -7,14 +7,15 @@ from fractions import Fraction as F
import generator.model
import generator.harmony
import generator.vocabulary
from generator.solo import types, policy, sequences, profiles
def heard(notes, operation, anchor=None):
def heard(notes, operation, anchor=None, bar=F(4)):
notes = types.sounding_notes(notes)
first = notes[0].start
events = tuple((n.start-first, n.end-n.start, n.pitch.number()) for n in notes)
ending = (anchor.pitch.number()%12, anchor.earliest%4, anchor.duration) if anchor and anchor.role == 'terminal' else ()
ending = (anchor.pitch.number()%12, anchor.earliest%bar, anchor.duration) if anchor and anchor.role == 'terminal' else ()
return types.HeardFigure(first, notes[-1].end, events, operation, ending)
@@ -56,7 +57,10 @@ 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))
trajectory=generator.vocabulary.role(part+'_solo').get('register_motion',[0,4,-3,0])
phase=min(len(trajectory)-1,incoming.closures%len(trajectory))
goal=profile.reference+trajectory[phase]
return max(profile.register[0]+3,min(profile.register[1]-3,round((center+goal)/2)))
recent = [p for h in incoming.history[-2:] for _,_,p in h.events]
if not recent:
return fallback
@@ -72,9 +76,7 @@ def register_goal(incoming, part, fallback, profile=None):
def endpoint_cost(finish, start, end, count, harmony, bar):
length = float((finish-start)/bar)
remaining = float((end-finish)/bar)
projected = count+1+remaining/(32/policy.DEFAULT.settling_target)
low, high = policy.DEFAULT.settling_range
pacing = .2*max(0,low-projected,projected-high)+.025*abs(projected-policy.DEFAULT.settling_target)
pacing = 0.
span = harmony.at(finish-F(1,100))
# A dominant may close an open question, but a nearby resolution is preferred.
degrees=generator.harmony.degrees(generator.harmony.parse_symbol(span.chord)) if span and span.scale else {}
@@ -94,7 +96,8 @@ def window_options(analysis, measures, start, end, harmony, incoming, *, limit=2
ends=(min(end,current.end if current else bounds[index+1]),)
else:
barrier=min((s.start for s in harmony.spans if not s.scale and s.start>start),default=end)
ends=tuple(sorted({min(end,barrier,bounds[min(index+n,len(measures))]) for n in (4,5,6,8)}))
authored=[n.end-analysis.body_offset for n in analysis.melody if n.end-n.start>=2]
ends=tuple(sorted({min(end,barrier,bounds[min(index+n,len(measures))]) for n in (3,4,5,6,8)} | {t for t in authored if start+3*bar<=t<=min(end,barrier,start+8*bar)}))
if barrier>=end:
# The returning pickup may shorten the last measure. Do not strand a
# one-beat "new phrase" after an otherwise completed sustained close.
@@ -116,9 +119,9 @@ def endings(source, incoming, part, harmony, bar, *, limit=8, final=False):
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),)
holds=tuple(F(v) for v in generator.vocabulary.data('grammar')['ending_holds']) if duration>=4 else (min(F(2),duration/2),)
for hold in holds:
for release in (F(),F(1,2),F(1),F(2)):
for release in map(F,generator.vocabulary.data('grammar')['ending_releases']):
arrival=source.end-release-hold
if arrival-source.start<F(3,2):continue
for pitch in available(harmony,arrival,arrival+hold,members=True):
@@ -126,8 +129,8 @@ def endings(source, incoming, part, harmony, bar, *, limit=8, final=False):
anchor=types.Anchor(f'{source.id}:close:{arrival}:{pitch.number()}',arrival,arrival,pitch,
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=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)
cadence=sum(h.ending==(pitch.number()%12,arrival%bar,hold) for h in incoming.history)
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)
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)
@@ -135,12 +138,10 @@ def endings(source, incoming, part, harmony, bar, *, limit=8, final=False):
def appearance_cost(operation, incoming, start, end):
expected=policy.DEFAULT.return_target*float(start/max(end,F(1)))
actual=len(incoming.returns)
gap=start-(incoming.returns[-1] if incoming.returns else F())
if operation=='return':
gap=start-(incoming.returns[-1] if incoming.returns else F())
return .65*max(0,actual+1-expected)+.5*max(0,1-float(gap)/24)
return .45*max(0,expected-actual)
return .5*max(0,1-float(gap)/16)
return .1*min(1,max(0,float(gap)/32-1))
def activity(source, ending):
@@ -162,7 +163,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 harmony.profile.cost(profiles.weighted_center(notes))+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+.8*history_cost
@functools.lru_cache(maxsize=512)