Cleanup
This commit is contained in:
+130
-217
@@ -1,230 +1,143 @@
|
||||
"""Construct complete phrases directly from harmonic/rhythmic purposes.
|
||||
"""Bounded search of joint musical actions; one normal production path.
|
||||
|
||||
The head is optional material, never a compulsory prefix or rhythmic template.
|
||||
No reference score, completed part, or previous published output enters here.
|
||||
Selection compares complete actions by cost per quarter. Actions are indivisible;
|
||||
cross-bar paths and resolution contracts never restart at search boundaries.
|
||||
"""
|
||||
import dataclasses
|
||||
import time
|
||||
from __future__ import annotations
|
||||
from collections import Counter
|
||||
import dataclasses
|
||||
from fractions import Fraction as F
|
||||
from generator import vocabulary, harmony as chords, model
|
||||
from generator.solo import types, expressions, groups, routes, rhythm, scoring, placement, policy
|
||||
import time
|
||||
|
||||
from generator import vocabulary
|
||||
from generator.solo import types, groups, operators, selection, sequences, scoring
|
||||
|
||||
class SearchExhausted(ValueError):pass
|
||||
|
||||
def choose(values,seed,domain):
|
||||
return min(values,key=lambda v:policy.stable_rank(seed,(domain,v)))
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class State:
|
||||
total: F = F()
|
||||
recent: tuple = ()
|
||||
home: tuple = ()
|
||||
last_kind: str = ''
|
||||
parent: object = None
|
||||
action: object = None
|
||||
evaluation: object = None
|
||||
tie: tuple = ()
|
||||
pending: tuple = ()
|
||||
alternatives: tuple = ()
|
||||
|
||||
class Builder:
|
||||
def __init__(self,analysis,measures,harmony,sequences,rhythms,end,seed,enabled=True):
|
||||
self.analysis=analysis;self.harmony=harmony;self.end=end;self.seed=seed;self.enabled=enabled
|
||||
self.rules=vocabulary.data('solo');self.budgets=self.rules['search'];self.routes=routes.Routes(harmony,sequences)
|
||||
self.rhythms=rhythm.Rhythms(rhythms,analysis.tempo);self.windows=[];cursor=F()
|
||||
for m in measures:
|
||||
length=model.fraction(m['duration']);self.windows.append((cursor,cursor+length,m['time_signature']));cursor+=length
|
||||
self.coverage={a:Counter() for a in self.rules['actions']};self.failures=Counter();self.cache={};self.decisions=[];self.timings=Counter()
|
||||
def __init__(self,analysis,measures,harmony,sequences,rhythms,end,seed,enabled=True,*,start=F()):
|
||||
self.analysis=analysis;self.harmony=harmony;self.start=start;self.end=end
|
||||
self.operators=operators.Operators(harmony,analysis,end,start,enabled,sequences,rhythms)
|
||||
self.selector=selection.Selector(harmony,analysis,harmony.profile.reference or 60)
|
||||
self.rules=vocabulary.data('solo')['operators'];self.decisions=[];self.failures=Counter();self.coverage={};self.timings=Counter()
|
||||
|
||||
def at(self,time):return next((m for m in self.windows if m[0]<=time<m[1]),self.windows[-1])
|
||||
def candidates(self,at,state):
|
||||
candidates=self.operators.at(at)
|
||||
if state.home and state.last_kind!='return':
|
||||
candidates+=self.operators.related(at,state.home,'return')+self.operators.related(at,state.home,'answer')
|
||||
evaluated=[]
|
||||
for a in candidates:
|
||||
if state.pending and self.advance(state,None,a) is None:continue
|
||||
cost=self.selector.cost(a,state.recent,state.home,state.last_kind)
|
||||
evaluated.append((cost,a))
|
||||
# Rank complete actions by cost per quarter before bounded repair.
|
||||
# Accumulated prefix totals are diagnostic, not a global search objective.
|
||||
retained=sequences.diverse(evaluated,
|
||||
lambda v:(v[0],v[1].key),
|
||||
lambda v:(v[1].kind,v[1].end-v[1].start),self.rules['per_state'])
|
||||
for _,a in retained:self.operators.coverage['retained:'+a.kind]+=1
|
||||
return tuple((self.selector.evaluate(a,state.recent,state.home,state.last_kind),a) for _,a in retained)
|
||||
|
||||
def actions(self,history):
|
||||
result=['arpeggio','scalar','neighbor']
|
||||
if history:
|
||||
result+=['develop','answer']
|
||||
if len(history)>1 and history[-1].purpose!='return':result.append('return')
|
||||
if self.analysis.melody and len(history)>2:result.append('head')
|
||||
preferred=vocabulary.role().get('preferred_actions',[])
|
||||
return sorted(result,key=lambda a:(a not in preferred,preferred.index(a) if a in preferred else 0,a))
|
||||
|
||||
def propose(self,start,history,variant,batch=0):
|
||||
incoming=next((p.notes[-1] for p in reversed(history) if p.notes),None)
|
||||
home=next((p for p in history if len(p.notes)>=4),None)
|
||||
seed=policy.stable_rank(self.seed,('phrase',start,tuple(p.id for p in history[-2:]),variant,batch))
|
||||
purpose=self.actions(history)[variant%len(self.actions(history))]
|
||||
self.coverage[purpose]['proposed']+=1
|
||||
_,_,meter=self.at(start);bar=F(meter.split('/')[0])*4/int(meter.split('/')[1])
|
||||
span=self.harmony.at(start)
|
||||
if not span or not span.scale:return None
|
||||
limit=min(self.end,start+self.rules['max_phrase_bars']*bar)
|
||||
# Source harmony boundaries and available time supply opportunities, not mandatory closes.
|
||||
opportunities=sorted({min(self.end,start+2*bar),*(s.start for s in self.harmony.spans if start+2<=s.start<limit),*(s.end for s in self.harmony.spans if start+2<=s.end<limit),self.end if self.end<=limit else limit})
|
||||
boundary=choose(opportunities,seed,'destination time')
|
||||
ending=choose(('open','settle','complete'),seed,'ending')
|
||||
if boundary==self.end:ending='handoff'
|
||||
elif history and history[-1].ending=='open':ending=choose(('settle','complete'),seed,'complete continuation')
|
||||
offset=choose(tuple(map(F,self.rules['arrival_offsets'])) if self.enabled else (F(),F(1,2)),seed,'arrival')
|
||||
arrival=boundary+offset
|
||||
if ending=='open':arrival=min(boundary,self.end)-F(1,2)
|
||||
if ending=='handoff':arrival=self.end-choose((F(2),F(5,2),F(3)),seed,'handoff hold')
|
||||
arrival=max(start+(F(1) if ending=='handoff' else F(3,2)),min(arrival,self.end-F(1,2)))
|
||||
if arrival<=start:return None
|
||||
anticipated=arrival<boundary and ending!='open' and any(s.start==boundary for s in self.harmony.spans) and boundary-arrival<=F(vocabulary.data('harmony')['anticipation_max'])
|
||||
harmonic_time=boundary if anticipated else arrival
|
||||
target_span=self.harmony.at(harmonic_time)
|
||||
if not target_span or not target_span.scale:return None
|
||||
hold=F(1,2) if ending=='open' else choose(tuple(map(F,self.rules['holds'])),seed,'hold')
|
||||
if ending in ('complete','handoff'):hold=max(F(2),hold)
|
||||
release=min(self.end,arrival+hold)
|
||||
if anticipated and release<=boundary:return None
|
||||
if ending=='handoff':release=self.end
|
||||
breath=F() if ending=='open' else choose(tuple(map(F,self.rules['breaths'])),seed,'breath')
|
||||
finish=min(self.end,release+breath)
|
||||
if 0<self.end-finish<3:return None
|
||||
if finish==self.end and ending!='open':
|
||||
if release-arrival<min(F(2),self.end-start-1):return None
|
||||
ending='handoff'
|
||||
center=self.harmony.profile.reference or 60
|
||||
# A coherent excursion and return; no per-note octave folding or string quota.
|
||||
shift=choose(tuple(vocabulary.role().get('register_alternatives',(-7,0,7))),seed,'register')
|
||||
if incoming and incoming.pitch.number()>center+7:shift=min(0,shift)
|
||||
if incoming and incoming.pitch.number()<center-7:shift=max(0,shift)
|
||||
goal=center+shift
|
||||
permitted=types.permitted(self.harmony.choices,harmonic_time,release)
|
||||
members=self.harmony.members(harmonic_time,release)
|
||||
targets=[types.scale_pitch(target_span.scale,i) for i in range(-21,22)]
|
||||
targets=[p for p in targets if p.number()%12 in permitted and (ending=='open' or p.number()%12 in members) and self.harmony.profile.permits(p.number()) and abs(p.number()-goal)<=7]
|
||||
if not targets:return None
|
||||
# Choose among actual chord degrees and registers, without always picking the nearest fifth/root.
|
||||
target_pitch=choose(targets,seed,'destination pitch')
|
||||
target=types.Note(arrival,release,target_pitch,role='arrival' if ending!='open' else 'motion',event_id=seed[:16]+':target')
|
||||
contracts=[]
|
||||
if anticipated:contracts.append(types.ExpressionContract(seed[:16]+':anticipation','anticipation','early_chord_tone',(target,),target,target_span.chord,boundary=boundary))
|
||||
lead=();movement_end=arrival
|
||||
# Shape-changing expression is planned before either rhythm or route is realized.
|
||||
treatment=choose(('direct','approach','color','blues','suspension'),seed,'expression') if self.enabled and ending!='open' else 'direct'
|
||||
if treatment=='approach':
|
||||
rule=choose(vocabulary.data('lead_ins')['patterns'],seed,'approach rule')
|
||||
movement_end=arrival-F(rule['gap'])-sum(map(F,rule['durations']),F());cursor=movement_end;events=[]
|
||||
for i,(step,duration) in enumerate(zip(rule['semitones'],rule['durations'])):
|
||||
events.append(types.Note(cursor,cursor+F(duration),expressions.chromatic(target_pitch,step),role=rule['kind'],event_id=seed[:16]+':approach:'+str(i)));cursor+=F(duration)
|
||||
lead=tuple(events)
|
||||
contracts.append(types.ExpressionContract(seed[:16]+':approach',rule['kind'],rule['id'],lead,target,target_span.chord,boundary=boundary if anticipated else None))
|
||||
elif treatment=='color' and not anticipated:
|
||||
region=self.harmony.at(arrival-1)
|
||||
eligible=[r for r in vocabulary.data('harmony')['colors'] if region and expressions.eligible_color(r,region,self.harmony)]
|
||||
if eligible:
|
||||
rule=choose(eligible,seed,'color rule');p=expressions.degree_pitch(chords.parse_symbol(region.chord),rule['degree'],rule['alter'],target_pitch.number())
|
||||
if not rule.get('resolve') and choose((True,False),seed,'sustained color'):
|
||||
target=dataclasses.replace(target,pitch=p,role='color');target_pitch=p
|
||||
contracts.append(types.ExpressionContract(seed[:16]+':color','color',rule['id'],(target,),target,target_span.chord))
|
||||
else:
|
||||
color=types.Note(arrival-1,arrival,p,role='color',event_id=seed[:16]+':color')
|
||||
if rule.get('resolve') and not 0<abs(target_pitch.number()-p.number())<=2:return None
|
||||
lead=(color,);movement_end=color.start
|
||||
contracts.append(types.ExpressionContract(seed[:16]+':color','color',rule['id'],lead,target if rule.get('resolve') else color,target_span.chord if rule.get('resolve') else region.chord))
|
||||
elif treatment=='blues' and not anticipated:
|
||||
eligible=[r for r in vocabulary.data('lead_ins')['blues'] if expressions.degree_pitch(chords.parse_symbol(target_span.chord),r['target_degree'],0,target_pitch.number())==target_pitch]
|
||||
if eligible:
|
||||
rule=choose(eligible,seed,'blues rule');p=expressions.degree_pitch(chords.parse_symbol(target_span.chord),rule['degree'],rule['alter'],target_pitch.number())
|
||||
lead=(types.Note(arrival-F(1,2),arrival,p,role='blues',event_id=seed[:16]+':blues'),);movement_end=lead[0].start
|
||||
contracts.append(types.ExpressionContract(seed[:16]+':blues','blues',rule['id'],lead,target,target_span.chord))
|
||||
elif treatment=='suspension' and offset>0 and arrival==boundary+offset:
|
||||
previous=self.harmony.at(boundary-F(1,2));rule=vocabulary.data('harmony')['suspensions'][0]
|
||||
if previous and previous.scale:
|
||||
candidates=[p for p in self.routes.pitches(boundary-F(1,2),arrival) if p.number()%12 in previous.roles.basic and target_pitch.number()-p.number() in rule['steps']]
|
||||
if candidates:
|
||||
p=choose(candidates,seed,'suspension pitch');lead=(types.Note(boundary-F(1,2),arrival,p,role='suspension',event_id=seed[:16]+':suspension'),);movement_end=lead[0].start
|
||||
contracts.append(types.ExpressionContract(seed[:16]+':suspension','suspension',rule['id'],lead,target,target_span.chord,boundary=boundary))
|
||||
entrance=F() if history and history[-1].ending=='open' else choose(tuple(map(F,self.rules['entrances'])),seed,'entrance')
|
||||
entry=start+entrance
|
||||
if movement_end-entry<F(1):return None
|
||||
hint=home.notes[:4] if home else ()
|
||||
if purpose=='head':
|
||||
candidates=[self.analysis.melody[i:i+4] for i in range(max(0,len(self.analysis.melody)-3))]
|
||||
candidates=[ns for ns in candidates if ns[-1].end-ns[0].start<=movement_end-entry]
|
||||
if not candidates:return None
|
||||
hint=choose(candidates,seed,'head reference')
|
||||
rhythm_purpose=purpose if purpose in ('arpeggio','scalar','neighbor','develop','answer') else 'develop'
|
||||
rhythm_began=time.monotonic()
|
||||
timing,rhythm_groups,sources=self.rhythms.realize(entry,movement_end,rhythm_purpose,seed,meter)
|
||||
self.timings['rhythm_seconds']+=time.monotonic()-rhythm_began
|
||||
if not timing:return None
|
||||
if purpose in ('return','head'):
|
||||
if len(hint)<3:return None
|
||||
duration=hint[-1].end-hint[0].start
|
||||
if entry+duration>movement_end:return None
|
||||
pattern=tuple((entry+n.start-hint[0].start,entry+n.end-hint[0].start) for n in hint)
|
||||
tail,tail_groups,tail_sources=self.rhythms.realize(entry+duration,movement_end,'develop',seed,meter)
|
||||
timing=(*pattern,*tail)
|
||||
if not groups.valid(tuple((a-entry,b-entry) for a,b in timing)):return None
|
||||
rhythm_groups=(*groups.describe(tuple((a-entry,b-entry) for a,b in pattern),origin=entry,source='heard_theme' if purpose=='return' else 'head'),*tail_groups)
|
||||
sources=(purpose,*tail_sources)
|
||||
self.coverage[purpose]['rhythm_valid']+=1
|
||||
first_span=self.harmony.at(entry)
|
||||
starting=[p for p in self.routes.pitches(*timing[0]) if p.number()%12 in first_span.roles.basic and abs(p.number()-(incoming.pitch.number() if incoming else goal))<=7]
|
||||
if not starting:return None
|
||||
first=choose(starting,seed,'entry pitch')
|
||||
notes=tuple(types.Note(a,b,first,role=purpose,event_id=seed[:16]+':'+str(i)) for i,(a,b) in enumerate(timing))
|
||||
all_notes=(*notes,*lead,target)
|
||||
fixed={0,*range(len(notes),len(all_notes))}
|
||||
direction=choose(self.rules['directions'],seed,'contour')
|
||||
route_began=time.monotonic()
|
||||
paths=self.routes.realize(all_notes,fixed,purpose,direction,seed,goal,incoming,hint)
|
||||
self.timings['route_seconds']+=time.monotonic()-route_began
|
||||
self.coverage[purpose]['complete_routes']+=len(paths)
|
||||
for actual in paths:
|
||||
bound=[]
|
||||
for c in contracts:
|
||||
prior=next((n for n in reversed(actual) if n.end<=c.events[0].start),None)
|
||||
bound.append(dataclasses.replace(c,preparation=prior if c.kind in ('connection','blues') else c.preparation))
|
||||
validation_began=time.monotonic()
|
||||
checked=self.harmony.evaluate(actual,until=finish,complete=True,expressions=tuple(bound))
|
||||
self.timings['harmonic_seconds']+=time.monotonic()-validation_began
|
||||
if not checked.valid:self.failures[checked.error.split(':')[-1]]+=1;continue
|
||||
anchor=None if ending=='open' else types.Anchor(seed[:16]+':destination',arrival,arrival,target_pitch,members|frozenset({target_pitch.number()%12}),self.harmony.profile.register,release-arrival,'terminal' if ending in ('complete','handoff') else 'intermediate',target_span.chord,False,release,ending)
|
||||
phrase=types.Phrase(seed[:16],start,finish,actual,purpose,direction,ending,anchor,tuple(bound),rhythm_groups,home.id if home else seed[:16],sources=sources)
|
||||
evaluation_began=time.monotonic()
|
||||
evaluation=scoring.evaluate(phrase,incoming,history,center)
|
||||
self.timings['evaluation_seconds']+=time.monotonic()-evaluation_began
|
||||
self.coverage[purpose]['valid']+=1
|
||||
return dataclasses.replace(phrase,score=evaluation.total,evaluation=evaluation)
|
||||
return None
|
||||
|
||||
def candidates(self,start,history):
|
||||
key=(start,len(history),next(((p.id,p.notes) for p in history if len(p.notes)>=4),None),tuple((p.id,p.purpose,p.ending,p.notes,p.expressions) for p in history[-4:]))
|
||||
if key in self.cache:return self.cache[key]
|
||||
span=self.harmony.at(start)
|
||||
if not span or not span.scale:
|
||||
finish=min(self.end,span.end if span else self.end)
|
||||
result=(types.Phrase('space:'+str(start),start,finish,(),'space','none','open'),)
|
||||
self.cache[key]=result;return result
|
||||
for action in self.actions(history):self.coverage[action]['eligible_contexts']+=1
|
||||
proposals=[]
|
||||
for batch in range(self.budgets['replans']+1):
|
||||
for variant in range(self.budgets['proposals']):
|
||||
p=self.propose(start,history,variant,batch)
|
||||
if p:proposals.append(p)
|
||||
if proposals:break
|
||||
if not proposals:raise SearchExhausted(f'at beat {start}: no complete phrase within deterministic budget; rejections {self.failures.most_common(3)}; not proof of harmonic incompatibility')
|
||||
from generator.solo.sequences import diverse
|
||||
finals=diverse(proposals,lambda p:(p.score,policy.stable_rank(self.seed,p.id)),lambda p:(p.purpose,p.ending,p.direction),self.budgets['finalists'])
|
||||
placed=[];incoming=next((p.notes[-1] for p in reversed(history) if p.notes),None)
|
||||
for p in finals:
|
||||
self.coverage[p.purpose]['finalists']+=1
|
||||
placement_began=time.monotonic()
|
||||
options=placement.alternatives(p.notes,self.analysis.tempo,incoming,self.budgets['fingerings'],self.harmony.profile)
|
||||
self.timings['placement_seconds']+=time.monotonic()-placement_began
|
||||
if options:
|
||||
# Physical economy selects a fingering of this same melody, not the tune itself.
|
||||
placed.append(dataclasses.replace(p,notes=options[0][1]));self.coverage[p.purpose]['placed']+=1
|
||||
self.cache[key]=tuple(placed)
|
||||
return self.cache[key]
|
||||
def advance(self,state,evaluation,action):
|
||||
for contract in state.pending:
|
||||
target=contract.target
|
||||
if action.start<=target.start<action.end:
|
||||
# Reject a missed committed attack/hold before revalidating all the
|
||||
# harmonic relationships. Partial target evidence remains possible.
|
||||
if not any(n.event_id==target.event_id and n.start==target.start
|
||||
and n.end==min(action.end,target.end) and n.pitch==target.pitch for n in action.notes):return None
|
||||
obligations=(*state.pending,*action.contracts);pending=()
|
||||
if not state.pending:
|
||||
pending=tuple(c for c in action.contracts if c.target.end>action.end)
|
||||
elif obligations:
|
||||
heard={n.event_id:n for c in state.pending for n in c.events if n.start<action.start}
|
||||
heard.update({n.event_id:n for n in action.notes})
|
||||
check=self.harmony.evaluate(tuple(sorted(heard.values(),key=lambda n:n.start)),
|
||||
expressions=obligations,until=action.end,complete=action.end==self.end)
|
||||
if not check.valid:return None
|
||||
pending=check.pending
|
||||
recent=(*state.recent,*action.notes)[-8:];home=state.home
|
||||
if not home and len(recent)>=4:
|
||||
proposed=recent[:4]
|
||||
if proposed[-1].end-proposed[0].start<=8:home=proposed
|
||||
return State(state.total+(evaluation.total if evaluation else 0),recent,home,action.kind,state,action,evaluation,(),pending)
|
||||
|
||||
def compose(self):
|
||||
history=[];start=F()
|
||||
while start<self.end:
|
||||
options=self.candidates(start,tuple(history));ranked=[]
|
||||
for p in options[:self.budgets['frontier']]:
|
||||
future=()
|
||||
if p.end<self.end:
|
||||
try:future=self.candidates(p.end,(*history,p))
|
||||
except SearchExhausted:continue
|
||||
if not future:continue
|
||||
next_score=min((f.score for f in future),default=p.score)
|
||||
total=(p.score+next_score)/2
|
||||
ranked.append((total,policy.stable_rank(self.seed,p.id),p))
|
||||
if p.purpose in self.coverage:self.coverage[p.purpose]['lookahead_valid']+=1
|
||||
if not ranked:raise SearchExhausted(f'at beat {start}: no legal following phrase within lookahead budget')
|
||||
_,_,chosen=min(ranked,key=lambda v:v[:2]);history.append(chosen);start=chosen.end
|
||||
if chosen.purpose in self.coverage:self.coverage[chosen.purpose]['selected']+=1
|
||||
self.decisions.append({'start':str(chosen.start),'selected':chosen.id,'candidates':[{'id':p.id,'purpose':p.purpose,'end':str(p.end),'evaluation':dataclasses.asdict(p.evaluation) if p.evaluation else {},'lookahead_total':next((v[0] for v in ranked if v[2].id==p.id),None)} for p in options]})
|
||||
return tuple(history)
|
||||
self.decisions=[];began=time.monotonic();backtracks=0
|
||||
# Compare full actions by cost per exact quarter, with a bounded repair
|
||||
# frontier. This is a local policy, not a claim of globally optimal melody.
|
||||
stack=[(self.start,State(),None)];rankings={}
|
||||
while stack:
|
||||
at,state,iterator=stack[-1]
|
||||
if at==self.end:final=state;break
|
||||
if iterator is None:
|
||||
options=sorted(self.candidates(at,state),key=lambda v:(v[0].total,v[1].key))
|
||||
rankings[id(state)]=tuple({'kind':a.kind,'start':str(a.start),'end':str(a.end),
|
||||
'notes':[(str(n.start),str(n.end),n.pitch.number()) for n in a.notes],
|
||||
'evaluation':dataclasses.asdict(e)} for e,a in options[:4])
|
||||
iterator=iter(options);stack[-1]=(at,state,iterator)
|
||||
for evaluation,action in iterator:
|
||||
next_state=self.advance(state,evaluation,action)
|
||||
if next_state is not None:
|
||||
next_state=dataclasses.replace(next_state,alternatives=rankings[id(state)])
|
||||
stack.append((action.end,next_state,None));break
|
||||
else:
|
||||
stack.pop();backtracks+=1
|
||||
if backtracks>self.rules['backtracks']:raise SearchExhausted(f'at {at}: action repair budget exhausted')
|
||||
else:raise SearchExhausted('no complete action path')
|
||||
self.timings['backtracks']=backtracks
|
||||
self.timings['action_search_seconds']=time.monotonic()-began
|
||||
path=[];state=final
|
||||
while state.action:
|
||||
path.append(state);state=state.parent
|
||||
path.reverse();phrases=[];pending=[]
|
||||
self.selected_actions=tuple(s.action for s in path)
|
||||
for state in path:
|
||||
action=state.action;pending.append(state)
|
||||
self.operators.coverage['selected:'+action.kind]+=1
|
||||
self.decisions.append({'start':str(action.start),'end':str(action.end),'selected':action.kind,
|
||||
'evaluation':dataclasses.asdict(state.evaluation),'prefix_total':state.total,'alternatives':state.alternatives})
|
||||
# Reporting groups the selected thought; it does not insert events.
|
||||
held=bool(action.notes and action.notes[-1].end-action.notes[-1].start>=2)
|
||||
breath=not action.notes and action.end-action.start>=1
|
||||
if held or breath or action.end==self.end:
|
||||
phrases.append(self.phrase(pending,len(phrases)));pending=[]
|
||||
for name,count in self.operators.coverage.items():
|
||||
stage,kind=name.split(':',1);self.coverage.setdefault(kind,{})[stage]=count
|
||||
self.timings['selected_cost']=final.total
|
||||
return tuple(phrases)
|
||||
|
||||
def phrase(self,states,index):
|
||||
actions=tuple(s.action for s in states);ns=tuple(n for a in actions for n in a.notes)
|
||||
start,end=actions[0].start,actions[-1].end
|
||||
last=ns[-1] if ns else None
|
||||
held=last and last.end-last.start>=2
|
||||
ending='handoff' if end==self.end else 'settle' if held else 'open'
|
||||
anchor=None
|
||||
if held:
|
||||
region=self.harmony.at(last.start)
|
||||
anchor=types.Anchor(f'gesture_{index}:arrival',last.start,last.start,last.pitch,
|
||||
frozenset({last.pitch.number()%12}),self.harmony.profile.register,last.end-last.start,
|
||||
'terminal' if ending=='handoff' else 'intermediate',region.chord,False,last.end,ending)
|
||||
raw=Counter();contributions=Counter();weights={}
|
||||
for s in states:
|
||||
raw.update(s.evaluation.raw);contributions.update(s.evaluation.contributions);weights.update(s.evaluation.weights)
|
||||
evaluation=scoring.Evaluation(selection.VERSION,True,dict(raw),weights,dict(contributions),sum(s.evaluation.total for s in states),
|
||||
{'span':str(end-start),'attacks':len(ns),'ticks_per_quarter':12})
|
||||
direction='up' if len(ns)>1 and ns[-1].pitch.number()>ns[0].pitch.number() else 'down'
|
||||
rhythm=tuple(g for a in actions for g in groups.describe(tuple((n.start-a.start,n.end-a.start) for n in a.notes),origin=a.start,source=a.kind))
|
||||
return types.Phrase(f'gesture_{index}',start,end,ns,actions[0].kind,direction,ending,anchor,
|
||||
tuple(c for a in actions for c in a.contracts),rhythm,'home',evaluation.total,evaluation,tuple(a.source or a.kind for a in actions))
|
||||
|
||||
Reference in New Issue
Block a user