Cleanup
This commit is contained in:
+49
-15
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user