Cleanup
This commit is contained in:
@@ -3,7 +3,7 @@ from __future__ import annotations
|
||||
import dataclasses
|
||||
from fractions import Fraction as F
|
||||
from generator import harmony as chord_model, model, pitch as spelling, vocabulary
|
||||
from generator.solo import types, drafts, enclosures, placement, policy
|
||||
from generator.solo import types
|
||||
|
||||
|
||||
def quality(chord):
|
||||
@@ -158,177 +158,3 @@ def validate_frontier(notes,contracts,harmony,until,complete):
|
||||
if incomplete:pending.append(c)
|
||||
approved,error=validate(tuple(sorted(future.values(),key=lambda n:(n.start,n.event_id))),contracts,harmony)
|
||||
return {key:c for key,c in approved.items() if key in actual},tuple(pending),error
|
||||
|
||||
|
||||
def proposals(phrase,harmony):
|
||||
draft=phrase.draft
|
||||
if not draft or any(n.continuation for n in draft.notes):return
|
||||
notes=types.sounding_notes(draft.notes)
|
||||
for target,role in enclosures.declared_targets(phrase,harmony):
|
||||
for rule in vocabulary.data('lead_ins')['patterns']:
|
||||
start=target.start-F(rule['gap'])-sum((F(d) for d in rule['durations']),F())
|
||||
if start<phrase.plan.start or not enclosures.editable(draft,start,target.start):continue
|
||||
cursor=start;events=[]
|
||||
for i,(step,duration) in enumerate(zip(rule['semitones'],rule['durations'])):
|
||||
events.append(types.Note(cursor,cursor+F(duration),chromatic(target.pitch,step),target.sources,rule['kind'],event_id=target.event_id+':'+rule['id']+':'+str(i)))
|
||||
cursor+=F(duration)
|
||||
actual=enclosures.replace_window(notes,start,target.start,tuple(events))
|
||||
if actual is None:continue
|
||||
prior=next((n for n in reversed(actual) if n.end<=start),None)
|
||||
contract=types.ExpressionContract(target.event_id+':'+rule['id'],rule['kind'],rule['id'],tuple(events),target,harmony.at(target.start).chord,preparation=prior)
|
||||
yield actual,contract
|
||||
for i,n in enumerate(notes[:-1]):
|
||||
target=notes[i+1]
|
||||
if not enclosures.editable(draft,n.start,n.end):continue
|
||||
span=harmony.at(n.start)
|
||||
if not span or not span.scale:continue
|
||||
for rule in vocabulary.data('harmony')['colors']:
|
||||
if not eligible_color(rule,span,harmony) or n.end-n.start>F(rule['max_beats']):continue
|
||||
p=degree_pitch(chord_model.parse_symbol(span.chord),rule['degree'],rule['alter'],n.pitch.number())
|
||||
if abs(p.number()-n.pitch.number())>4 or p==n.pitch:continue
|
||||
color=dataclasses.replace(n,pitch=p,role='color')
|
||||
resolution=target if rule.get('resolve') else color
|
||||
contract=types.ExpressionContract(n.event_id+':'+rule['id'],'color',rule['id'],(color,),resolution,harmony.at(resolution.start).chord)
|
||||
yield (*notes[:i],color,*notes[i+1:]),contract
|
||||
for rule in vocabulary.data('lead_ins')['blues']:
|
||||
if i==0 or n.end-n.start>F(1,2):continue
|
||||
p=degree_pitch(chord_model.parse_symbol(span.chord),rule['degree'],rule['alter'],n.pitch.number())
|
||||
changed=dataclasses.replace(n,pitch=p,role='blues')
|
||||
yield (*notes[:i],changed,*notes[i+1:]),types.ExpressionContract(n.event_id+':'+rule['id'],'blues',rule['id'],(changed,),target,harmony.at(target.start).chord,preparation=notes[i-1])
|
||||
for i,target in enumerate(notes):
|
||||
if i==0 or target.start not in {s.start for s in harmony.spans}:continue
|
||||
start=target.start-F(1,2)
|
||||
if not enclosures.editable(draft,start,target.start):continue
|
||||
n=dataclasses.replace(target,start=start,role='anticipation')
|
||||
actual=enclosures.replace_window(notes,start,target.start,())
|
||||
if actual:
|
||||
actual=tuple(n if a.event_id==target.event_id else a for a in actual)
|
||||
yield actual,types.ExpressionContract(target.event_id+':anticipate','anticipation','early_chord_tone',(n,),n,harmony.at(target.start).chord,boundary=target.start)
|
||||
for i,(before,target) in enumerate(zip(notes,notes[1:])):
|
||||
boundary=target.start
|
||||
if before.end!=boundary or boundary not in {s.start for s in harmony.spans}:continue
|
||||
if not enclosures.editable(draft,before.start,target.end):continue
|
||||
for rule in vocabulary.data('harmony').get('suspensions',[]):
|
||||
delay=min(F(1,2),F(rule['max_delay']))
|
||||
if target.end<=boundary+delay:continue
|
||||
held=dataclasses.replace(before,end=boundary+delay,role='suspension')
|
||||
resolved=dataclasses.replace(target,start=boundary+delay)
|
||||
yield (*notes[:i],held,resolved,*notes[i+2:]),types.ExpressionContract(before.event_id+':suspend',
|
||||
'suspension',rule['id'],(held,),resolved,harmony.at(boundary).chord,boundary=boundary)
|
||||
|
||||
|
||||
def joint_candidates(phrases,incoming,harmony,tempo,context,*,bar=F(4)):
|
||||
"""Expression alternatives compete BEFORE complete-phrase selection.
|
||||
|
||||
This rewrites provisional gestures, never the selected chorus. The direct
|
||||
realization remains available and every alternative uses the same evaluator.
|
||||
"""
|
||||
from generator.solo import realization, sequences, phrasing, groups
|
||||
result=list(phrases)
|
||||
trace=context.diagnostics.setdefault('expression_operations',{})
|
||||
for phrase in phrases:
|
||||
proposed={}
|
||||
for actual,c in proposals(phrase,harmony):
|
||||
counts=trace.setdefault(c.kind,dict(proposed=0,valid=0,retained=0,rejected_by_rule=0))
|
||||
counts['proposed']+=1
|
||||
old_pattern=tuple((n.start-phrase.plan.start,n.end-phrase.plan.start) for n in phrase.draft.notes)
|
||||
new_pattern=tuple((n.start-phrase.plan.start,n.end-phrase.plan.start) for n in actual)
|
||||
if groups.valid(old_pattern) and not groups.valid(new_pattern):
|
||||
counts['rejected_by_rule']+=1;continue
|
||||
if not drafts.contract_valid(phrase.draft,actual)[0]:
|
||||
counts['rejected_by_rule']+=1;continue
|
||||
checked=harmony.evaluate(actual,until=phrase.plan.end,complete=True,expressions=(*phrase.draft.expressions,c))
|
||||
if not checked.valid:
|
||||
counts['rejected_by_rule']+=1;continue
|
||||
counts['valid']+=1
|
||||
prior=next((n for n in reversed(actual) if n.end<=c.events[0].start),None)
|
||||
join=abs(prior.pitch.number()-c.events[0].pitch.number()) if prior else 0
|
||||
rank=max(0,join-5)+bool(prior and prior.pitch==c.events[0].pitch)
|
||||
proposed.setdefault(c.kind,[]).append((rank,actual,c))
|
||||
# One representative per treatment kind, then a bounded second sweep.
|
||||
options=sequences.diverse([v for values in proposed.values() for v in values],
|
||||
lambda v:(v[0],policy.stable_rank(phrase.plan.seed,v[2].id)),lambda v:v[2].kind,8)
|
||||
for _,actual,c in options:
|
||||
transforms=tuple(dataclasses.replace(t,
|
||||
events=tuple(n.event_id for n in actual if t.start<=n.start<t.end),
|
||||
rhythm=tuple((n.start-t.start,n.end-t.start) for n in actual if t.start<=n.start<t.end))
|
||||
for t in phrase.draft.transformations)
|
||||
changed=dataclasses.replace(phrase.draft,id=phrase.draft.id+':'+c.rule,
|
||||
notes=actual,expressions=(*phrase.draft.expressions,c),transformations=transforms)
|
||||
candidate=realization.checked_phrase(phrase.plan,changed,actual,incoming.treatments,incoming,harmony,tempo,phrase.edits)
|
||||
if candidate is None:continue
|
||||
trace[c.kind]['retained']+=1
|
||||
state=dataclasses.replace(phrase.state,notes=candidate.state.notes,last=candidate.state.last,
|
||||
motifs=candidate.state.motifs,travel=candidate.state.travel)
|
||||
history=incoming.history
|
||||
for t in transforms:
|
||||
ns=tuple(n for n in actual if n.event_id in t.events)
|
||||
if ns:history=phrasing.remember(history,phrasing.heard(ns,t.operation,bar=bar),bar)
|
||||
state=dataclasses.replace(state,history=history)
|
||||
kept_routes=tuple(r for r in phrase.routes if all(any((n.event_id,n.start,n.end,n.pitch)==
|
||||
(a.event_id,a.start,a.end,a.pitch) for a in actual) for n in r.notes))
|
||||
original=dataclasses.replace(phrase.draft,notes=tuple(n for d in phrase.decisions for n in d.notes))
|
||||
result.append(dataclasses.replace(candidate,state=state,initial=phrase.initial,
|
||||
refined=phrase.refined,before_rhythm=phrase.before_rhythm,before_enclosure=original,routes=kept_routes))
|
||||
return result
|
||||
|
||||
|
||||
def refine(solo,enabled=True):
|
||||
from generator.solo import harmonic
|
||||
harmony=harmonic.Harmony(solo.choices,solo.profile);result=[];history=[];attempts=0
|
||||
seed=vocabulary.role(solo.part+'_solo').get('choice_seed',solo.part)
|
||||
for phrase in solo.phrases:
|
||||
draft=dataclasses.replace(phrase.draft,notes=tuple(n for d in phrase.decisions for n in d.notes))
|
||||
phrase=dataclasses.replace(phrase,draft=draft,before_enclosure=draft)
|
||||
options=[(0.,phrase,None)]
|
||||
if enabled:
|
||||
original=draft.notes
|
||||
for actual,c in proposals(phrase,harmony):
|
||||
attempts+=1
|
||||
if not drafts.contract_valid(draft,actual)[0]:continue
|
||||
if not harmony.evaluate(actual,until=phrase.plan.end,complete=True,expressions=(c,)).valid:continue
|
||||
if not harmonic.anchors_valid(phrase.anchors,actual,phrase.plan.end,complete=True,closure=phrase.plan.closure)[0]:continue
|
||||
placed=placement.place(actual,solo.tempo,profile=solo.profile)
|
||||
if not placed:continue
|
||||
# Context, repetition, and motion determine utility; no chromatic count target.
|
||||
prior=next((n for n in reversed(actual) if n.end<=c.events[0].start),None)
|
||||
join=abs(prior.pitch.number()-c.events[0].pitch.number()) if prior else 0
|
||||
duplicated=bool(prior and prior.pitch==c.events[0].pitch)
|
||||
destination=c.target.pitch.number()
|
||||
old_approach=next((n for n in reversed(original) if n.end<=c.target.start),None)
|
||||
prepares=bool(old_approach and abs(old_approach.pitch.number()-destination)>2 and join<=3)
|
||||
recent=sum(kind==c.kind for kind in history[-2:])
|
||||
cost=.08*max(0,join-3)+.18*duplicated+.12*recent+.3*(enclosures.movement(actual)-enclosures.movement(original))-.12*prepares
|
||||
if c.kind=='anticipation':cost-=.05
|
||||
decisions=tuple(dataclasses.replace(d,notes=tuple(n for n in actual if d.start<=n.start<d.end)) for d in phrase.decisions)
|
||||
if tuple(n for d in decisions for n in d.notes)!=actual:continue
|
||||
# Rebase editable development spans onto the completed gesture. The
|
||||
# feature contract is unchanged, but stale event IDs/rhythm would make
|
||||
# reports falsely describe notes that the expression replaced.
|
||||
transforms=tuple(dataclasses.replace(t,
|
||||
events=tuple(n.event_id for n in actual if t.start<=n.start<t.end),
|
||||
rhythm=tuple((n.start-t.start,n.end-t.start) for n in actual if t.start<=n.start<t.end))
|
||||
for t in draft.transformations)
|
||||
changed=dataclasses.replace(draft,notes=actual,expressions=(*phrase.draft.expressions,c),transformations=transforms)
|
||||
routes=tuple(r for r in phrase.routes if all(any((n.event_id,n.start,n.end,n.pitch)==(a.event_id,a.start,a.end,a.pitch) for a in actual) for n in r.notes))
|
||||
options.append((cost,dataclasses.replace(phrase,draft=changed,decisions=decisions,routes=routes),c))
|
||||
_,chosen,c=min(options,key=lambda v:(round(v[0],8),policy.stable_rank(seed,(phrase.plan.id,v[2]))))
|
||||
if c:history.append(c.kind)
|
||||
result.append(chosen)
|
||||
raw=tuple(n for p in result for d in p.decisions for n in d.notes)
|
||||
placed=placement.place(raw,solo.tempo,profile=solo.profile)
|
||||
if raw and placed is None:raise ValueError('Expression realization has no complete guitar path')
|
||||
by_id={n.event_id:n for n in placed[1]} if placed else {}
|
||||
final=[]
|
||||
for p in result:
|
||||
decisions=tuple(dataclasses.replace(d,notes=tuple(by_id[n.event_id] for n in d.notes)) for d in p.decisions)
|
||||
notes=tuple(n for d in decisions for n in d.notes)
|
||||
state=dataclasses.replace(p.state,notes=notes,last=notes[-1] if notes else p.state.last) if p.state else None
|
||||
# A legal timing revision preserves the complete route with its new times.
|
||||
routes=[]
|
||||
for route in p.routes:
|
||||
if all(n.event_id in by_id and by_id[n.event_id].pitch==n.pitch for n in route.notes):
|
||||
route_notes=tuple(by_id[n.event_id] for n in route.notes)
|
||||
routes.append(dataclasses.replace(route,notes=route_notes,destination=route_notes[-1]))
|
||||
final.append(dataclasses.replace(p,decisions=decisions,draft=dataclasses.replace(p.draft,notes=notes),state=state,routes=tuple(routes)))
|
||||
return dataclasses.replace(solo,phrases=tuple(final),diagnostics={**solo.diagnostics,'expression_candidates':attempts,'expressions_selected':history,'enclosures_enabled':enabled})
|
||||
|
||||
Reference in New Issue
Block a user