Save
This commit is contained in:
@@ -89,6 +89,17 @@ def validate(notes,contracts,harmony):
|
||||
last=c.events[-1]
|
||||
if rule.get('resolve'):
|
||||
if target is last or not 0<=target.start-last.end<=F(vocabulary.data('harmony')['resolution_gap']) or not 0<abs(target.pitch.number()-last.pitch.number())<=2 or target.pitch.number()%12 not in harmony.members(target.start,target.end):return {},c.id+': unresolved color'
|
||||
elif c.kind=='suspension':
|
||||
rule=next((r for r in vocabulary.data('harmony').get('suspensions',[]) if r['id']==c.rule),None)
|
||||
if not rule or len(c.events)!=1 or c.boundary is None:return {},c.id+': unsupported suspension'
|
||||
held,=c.events
|
||||
if not held.start<=c.boundary-F(rule['preparation'])<c.boundary<held.end:
|
||||
return {},c.id+': insufficient sounding preparation'
|
||||
if held.pitch.number()%12 not in harmony.members(held.start,c.boundary):return {},c.id+': nonchordal preparation'
|
||||
if held.end-c.boundary>F(rule['max_delay']) or target.start!=held.end or target.pitch.number()-held.pitch.number() not in rule['steps']:
|
||||
return {},c.id+': missed suspension resolution'
|
||||
if target.pitch.number()%12 not in harmony.members(target.start,target.end):return {},c.id+': invalid suspension target'
|
||||
if held.pitch.number()%12 not in types.permitted(harmony.choices,held.start,held.end):return {},c.id+': suspension outside mapped collections'
|
||||
elif c.kind=='anticipation':
|
||||
if c.rule!='early_chord_tone' or len(c.events)!=1 or c.target!=c.events[0] or span.start!=c.boundary:
|
||||
return {},c.id+': unknown anticipation or non-boundary target'
|
||||
@@ -116,6 +127,34 @@ def validate(notes,contracts,harmony):
|
||||
return approved,''
|
||||
|
||||
|
||||
def validate_frontier(notes,contracts,harmony,until,complete):
|
||||
"""Validate heard prefixes and bounded future obligations separately.
|
||||
|
||||
Future contract events are used ONLY to establish rule feasibility. They do
|
||||
not count as heard resolution; the contract stays pending until its entire
|
||||
target hold is present. Missing past events and expired deadlines fail.
|
||||
"""
|
||||
if complete:
|
||||
approved,error=validate(notes,contracts,harmony)
|
||||
return approved,(),error
|
||||
actual={n.event_id:n for n in notes};future=dict(actual);pending=[]
|
||||
for c in contracts:
|
||||
required=tuple(dict.fromkeys((*c.events,c.target,*((c.preparation,) if c.preparation else ()))))
|
||||
incomplete=False
|
||||
for expected in required:
|
||||
heard=actual.get(expected.event_id)
|
||||
if expected.start>=until:
|
||||
future[expected.event_id]=expected;incomplete=True
|
||||
elif heard and heard.pitch==expected.pitch and heard.start==expected.start and heard.end==min(until,expected.end):
|
||||
future[expected.event_id]=expected
|
||||
incomplete |= heard.end<expected.end
|
||||
elif not heard or (heard.start,heard.end,heard.pitch)!=(expected.start,expected.end,expected.pitch):
|
||||
return {},(),c.id+': missing/changed event before resolution deadline'
|
||||
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
|
||||
@@ -160,6 +199,73 @@ def proposals(phrase,harmony):
|
||||
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=(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=(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):
|
||||
|
||||
Reference in New Issue
Block a user