This commit is contained in:
khodowany
2026-10-04 17:14:13 -05:00
parent f234dd6d51
commit 68df7292ee
33 changed files with 990 additions and 1872 deletions
+49 -15
View File
@@ -116,7 +116,9 @@ def rhythm_cost(pattern,length,purpose):
isolated=sum(b-a<=F(1,2) and prev[1]<a and b<nxt[0]
for prev,(a,b),nxt in zip(pattern,pattern[1:],pattern[2:]))
values={b-a for a,b in pattern}
uniform=.15 if len(pattern)>=4 and len(values)==1 else 0.
# Equal eighths can be a single directional run. Uniformity alone is not a
# musical defect; aimless long-value packets are addressed by construction.
uniform=.15 if len(pattern)>=4 and len(values)==1 and next(iter(values))>=1 else 0.
if purpose in ('continue','connect'):
# Silence may articulate an idea; disconnected tiny islands do not carry
# unfinished motion. No minimum occupancy or chorus attack quota.
@@ -194,6 +196,13 @@ def patterns(ref, start, finish, rhythms, meter, context, *, home=None, operatio
# Develop the heard timing, preserving total duration. Eighth/quarter/dotted
# alternatives are actual contracts, not failed copies of a rhythmic seed.
for original,label in tuple(values.items()):
movement=generator.vocabulary.data('melodic_vocabulary')['motion_subdivision']
for i,(a,b) in enumerate(original[:-1]):
pulse=F(movement['pulse'])
if b-a<F(movement['minimum_value']) or (a/pulse).denominator!=1 or ((b-a)/pulse).denominator!=1:continue
moving=tuple((a+j*pulse,a+(j+1)*pulse) for j in range(int((b-a)/pulse)))
revised=(*original[:i],*moving,*original[i+1:])
if len(revised)<=movement['max_attacks']:values[revised]=label+'; moving continuation'
for i,(a,b) in enumerate(original):
if b-a==1:
values[(*original[:i],(a,a+F(1,2)),(a+F(1,2),b),*original[i+1:])]=label+'; diminution'
@@ -252,9 +261,10 @@ class Unit:
routes: tuple[types.RouteCandidate,...] = ()
start: F = F()
end: F = F()
expressions: tuple[types.ExpressionContract,...] = ()
def unit_candidates(ref,start,finish,incoming,harmony,library,rhythms,meter,context,*,home=None,operation='introduce',terminal=False,batch=0,ending=None,goal=None):
def unit_candidates(ref,start,finish,incoming,harmony,library,rhythms,meter,context,*,home=None,operation='introduce',terminal=False,batch=0,ending=None,goal=None,allow_expressions=True):
"""Joint whole-gesture pitch/rhythm actions, conditioned on the actual entry.
A complete route is proposed before thematic evidence is checked. No motif
@@ -263,7 +273,7 @@ def unit_candidates(ref,start,finish,incoming,harmony,library,rhythms,meter,cont
"""
from generator.solo import leads
key=('joint-gesture-1',ref,start,finish,incoming.last,incoming.direction,incoming.treatments,
home,operation,ending,goal,batch,id(harmony),id(library),id(rhythms))
home,operation,ending,goal,batch,allow_expressions,getattr(context,'expressions_enabled',True),id(harmony),id(library),id(rhythms))
if key in context.results:
context.diagnostics['cache_hits']+=1;return context.results[key]
trace=context.diagnostics.setdefault('operations',{}).setdefault(operation,
@@ -298,13 +308,31 @@ def unit_candidates(ref,start,finish,incoming,harmony,library,rhythms,meter,cont
(j[4].number()>j[3].number())-(j[4].number()<j[3].number())),policy.DEFAULT.theme_route_jobs*(batch+1))
trace['jobs']+=len(jobs);trace['pruned_jobs']+=len(jobs)-len(selected)
result=[]
for _,templates,label,first,target in selected:
from generator.solo import gestures
scheduled=[]
for rank,templates,label,first,target in selected:
timed=(dataclasses.replace(templates[0],pitch=first),*templates[1:-1],dataclasses.replace(templates[-1],pitch=target))
proposed=list(routes.candidates(timed,{timed[-1].event_id},harmony,library,harmony.profile.register,
operation+' '+ref.id,purpose=rhythmic_purpose(operation,terminal)))
scheduled.append((rank,gestures.Proposal(timed,(),'direct'),label))
if allow_expressions and getattr(context,'expressions_enabled',True):
for rank,plain,label in tuple(scheduled[:4]):
scheduled.extend((rank,p,label+'; '+p.purpose) for p in gestures.select(plain.notes,harmony,start,finish,ending,limit=4))
funnel=context.diagnostics.setdefault('joint_gestures',{})
def record(purpose,stage,count=1):
values=funnel.setdefault(purpose,dict(proposed=0,retained_jobs=0,valid_routes=0,retained_units=0))
values[stage]+=count
for _,p,_ in scheduled:record(p.purpose,'proposed')
scheduled=sequences.diverse(scheduled,lambda v:(v[0],policy.stable_rank(ref.id,(v[1].purpose,tuple((n.start,n.end,n.pitch) for n in v[1].notes)))),
lambda v:tuple(c.kind for c in v[1].contracts) or ('direct',),policy.DEFAULT.theme_route_jobs*(batch+1))
for _,proposal,label in scheduled:
record(proposal.purpose,'retained_jobs')
timed=proposal.notes;first=timed[0].pitch;target=timed[-1].pitch;templates=timed
if not max(2,count)<=len(timed)<=16:continue
fixed={timed[-1].event_id}|{n.event_id for n in timed if n.role=='arrival'}
proposed=list(routes.candidates(timed,fixed,harmony,library,harmony.profile.register,
operation+' '+ref.id,purpose=rhythmic_purpose(operation,terminal),expressions=proposal.contracts))
# A substantial literal return is a musical action, not merely the hope
# that a general interval search will accidentally reproduce the theme.
if require_feature and len(templates)==len(ref.notes):
if proposal.purpose=='direct' and require_feature and len(templates)==len(ref.notes):
delta=first.number()-ref.notes[0].pitch.number()
actual=[]
for template,n in zip(templates,ref.notes):
@@ -320,18 +348,22 @@ def unit_candidates(ref,start,finish,incoming,harmony,library,rhythms,meter,cont
contract=feature(ref,actual[:count],treatment=operation if operation!='introduce' else None) if count else None
if contract and references.feature_error(contract,actual):
trace['rejected_by_rule']+=1;continue
checked=harmony.evaluate(actual,until=finish,complete=True)
contracts=proposal.bind(actual)
checked=harmony.evaluate(actual,until=finish,complete=True,expressions=contracts)
if not checked.valid:trace['rejected_by_rule']+=1;continue
trace['valid']+=1
record(proposal.purpose,'valid_routes')
entry=max(0,abs(first.number()-incoming.last.pitch.number())-7)/12 if incoming.last else 0.
score=2*checked.exposure+entry+.1*routes.path_cost(actual)
score+=.25*rhythm_cost(tuple((n.start-start,n.end-start) for n in actual),finish-start,rhythmic_purpose(operation,terminal))
result.append(Unit(actual,contract,ref,operation,ending,score,label,(route,),start,finish))
result.append(Unit(actual,contract,ref,operation,ending,score,label,(route,),start,finish,contracts))
unique={tuple((n.start,n.end,n.pitch) for n in u.notes):u for u in result}
chosen=sequences.diverse(tuple(unique.values()),lambda u:(u.score,policy.stable_rank(ref.id,u.notes)),
lambda u:(routes.path_family(u.notes,harmony),u.notes[0].pitch.number()//12,
tuple(n.end-n.start for n in u.notes)),policy.DEFAULT.theme_unit_alternatives)
tuple(n.end-n.start for n in u.notes),tuple(c.kind for c in u.expressions)),policy.DEFAULT.theme_unit_alternatives)
trace['retained']+=len(chosen)
for u in chosen:
record(u.source.split('; ')[-1] if '; ' in u.source else 'direct','retained_units')
context.diagnostics['generated_candidates']+=len(chosen)
context.results[key]=chosen
return chosen
@@ -437,7 +469,7 @@ def phrase_candidates(source,analysis,incoming,part,harmony,library,rhythms,mete
destination=ending.pitch.number() if ending else phrasing.register_goal(state,part,base,harmony.profile)
goal=destination if finish==source.end else round(base+(destination-base)*float((finish-cursor)/(source.end-cursor)))
options=unit_candidates(ref,cursor,finish,state,harmony,library,rhythms,meter,context,home=home,operation=operation,
terminal=terminal,batch=batch,ending=ending if terminal else None,goal=goal)
terminal=terminal,batch=batch,ending=ending if terminal else None,goal=goal,allow_expressions=stage=='elaborated')
for unit in options:
if introducing and similarity.near_copy(unit.notes,head,cursor,finish)['near_copy']:
context.diagnostics['near_copy_rejections']=context.diagnostics.get('near_copy_rejections',0)+1;continue
@@ -466,21 +498,23 @@ def phrase_candidates(source,analysis,incoming,part,harmony,library,rhythms,mete
# Cell tails are not compulsory melodic arrivals. Only the selected close
# has a sounding contract; intermediate turns remain editable.
anchors=(ending,) if ending else ()
tails=(types.SustainedTail(ending.earliest,ending.hold_until,ending.pitch.number()),) if ending else ()
tails=tuple(types.SustainedTail(n.start,n.end,n.pitch.number()) for n in plain if n.role=='arrival')
if ending:tails=(*tails,types.SustainedTail(ending.earliest,ending.hold_until,ending.pitch.number()))
options=[(plain,(),())]
for notes,edits,selected_routes in options:
identifier=part+'_'+source.id+'_'+policy.stable_rank(seed,leads.signature(notes))[:12]
transforms=tuple(types.Transformation(identifier+f':{i}',u.reference.id,home.id,u.operation,u.notes[0].start,u.notes[-1].end,
tuple(n.event_id for n in notes if u.notes[0].start<=n.start<u.notes[-1].end),tuple((n.start-u.notes[0].start,n.end-u.notes[0].start) for n in notes if u.notes[0].start<=n.start<u.notes[-1].end),preserves=(),source=u.reference.id+'; rhythm '+u.source) for i,u in enumerate(units))
source_ref=state.source_reference
draft=types.MelodicDraft(identifier,source.start,source.end,notes,transforms,(),(),anchors,home,'introduce' if not incoming.home else 'develop',
draft=types.MelodicDraft(identifier,source.start,source.end,notes,transforms,tuple(n.event_id for n in notes if n.role=='arrival'),(),anchors,home,'introduce' if not incoming.home else 'develop',
features=features,reference=source_ref.id,reference_motif=home,source_phrase=source,source_reference=source_ref,
source_links=tuple((n.event_id,n.sources) for n in notes),sustained_tails=tails)
source_links=tuple((n.event_id,n.sources) for n in notes),sustained_tails=tails,
expressions=tuple(c for u in units for c in u.expressions))
motif=types.Motif(home.id,home.notes,home.duration,(), 'realized solo theme from '+source_ref.id)
landmarks=(types.Landmark(ending.earliest,ending.latest,ending.pitch.number(),ending.pitch.number(),0,'sounding destination'),) if ending else ()
closed=bool(ending and ending.role=='terminal')
plan=types.PhrasePlan(identifier,source.start,source.end,motif,draft.relationship,'thematic_recomposition',None,harmony.profile.register,F(5,4),
source.end-ending.hold_until if ending else F(),phrasing.activity(source,ending),landmarks,
source.end-ending.hold_until if ending else F(),phrasing.activity(source,ending,tuple(n for n in notes if n.role=='arrival')),landmarks,
'sustain' if ending else 'connected','closed' if closed else 'open',seed,
source.id,source.start,source.end,(anchors,))
before_rhythm=draft