+105
-18
@@ -46,7 +46,7 @@ class Decoder:
|
||||
self.durations=tuple(d for d in model.durations if 0<d<=4 and d.denominator in (1,2,4) and
|
||||
(d.denominator!=4 or analysis.tempo<=limits['sixteenth_max_bpm']))
|
||||
self.allow_triplets=analysis.tempo<=limits['triplet_max_bpm']
|
||||
self._actions={};self._context={};self._pitches={};self.trace=[];self.counts=collections.Counter()
|
||||
self._actions={};self._context={};self._pitches={};self._colors={};self._permissions={};self.trace=[];self.counts=collections.Counter()
|
||||
|
||||
def context(self,time):
|
||||
if time not in self._context:
|
||||
@@ -80,7 +80,37 @@ class Decoder:
|
||||
pitch=p if isinstance(p,types.Pitch) else self.pitch(p,a)
|
||||
return types.Note(a,b,pitch,role=role,event_id=f'{a}:{b}:{pitch.number()}')
|
||||
|
||||
def color_bases(self,n):
|
||||
key=(n.start,n.end,n.pitch.number())
|
||||
if key in self._colors:return self._colors[key]
|
||||
bases=[]
|
||||
regions=[(None,n)]
|
||||
regions.extend((s.start,dataclasses.replace(n,start=s.start)) for s in self.harmony.spans
|
||||
if n.start<s.start<n.end and s.start-n.start<=F(vocabulary.data('harmony')['anticipation_max']))
|
||||
for boundary,arrived in regions:
|
||||
exposed=[s for s,_,_ in self.harmony.segments(arrived) if n.pitch.number()%12 not in s.roles.basic|s.roles.specified]
|
||||
if not exposed:continue
|
||||
for rule in vocabulary.data('harmony')['colors']:
|
||||
if arrived.end-arrived.start>F(rule['max_beats']):continue
|
||||
if any(s.chord=='N.C.' or not expressions.eligible_color(rule,s,self.harmony) or
|
||||
expressions.degree_pitch(expressions.parsed_chord(s.chord),rule['degree'],rule['alter'],n.pitch.number()).number()!=n.pitch.number()
|
||||
for s in exposed):continue
|
||||
bases.append((rule,boundary))
|
||||
self._colors[key]=tuple(bases)
|
||||
return self._colors[key]
|
||||
|
||||
def color_contract(self,n,target,rule,boundary):
|
||||
span=self.harmony.at(boundary if boundary is not None else n.start)
|
||||
if not span:return None
|
||||
kind='anticipated_color' if boundary is not None else 'color'
|
||||
return types.ExpressionContract(n.event_id+':'+rule['id']+':'+str(boundary),kind,rule['id'],(n,),target,span.chord,boundary)
|
||||
|
||||
def permission(self,n):
|
||||
key=(n.start,n.end,n.pitch.number())
|
||||
if key not in self._permissions:self._permissions[key]=self._permission(n)
|
||||
return self._permissions[key]
|
||||
|
||||
def _permission(self,n):
|
||||
"""Find justified color/anticipation support, never authorize by label."""
|
||||
# Source spans are contiguous. These are precisely the ordinary-event
|
||||
# branches of Harmony.evaluate, without rebuilding an empty contract map.
|
||||
@@ -90,23 +120,64 @@ class Decoder:
|
||||
if not self.expressions_enabled:return None
|
||||
span=self.harmony.at(n.start)
|
||||
if not span or span.chord=='N.C.':return None
|
||||
exposed=tuple(region for region,_,_ in self.harmony.segments(n)
|
||||
if n.pitch.number()%12 not in region.roles.basic|region.roles.specified)
|
||||
for rule in vocabulary.data('harmony')['colors']:
|
||||
if rule.get('resolve') or n.end-n.start>F(rule['max_beats']):continue
|
||||
# A held chord member can become a color only after a chord change.
|
||||
# Test the regions needing permission, not just the attack's harmony.
|
||||
if any(region.chord=='N.C.' or not expressions.eligible_color(rule,region,self.harmony) or
|
||||
expressions.degree_pitch(expressions.parsed_chord(region.chord),rule['degree'],rule['alter'],n.pitch.number()).number()!=n.pitch.number()
|
||||
for region in exposed):continue
|
||||
c=types.ExpressionContract(n.event_id+':color','color',rule['id'],(n,),n,span.chord)
|
||||
if self.harmony.evaluate((n,),complete=True,expressions=(c,)).valid:return (c,)
|
||||
for rule,boundary in self.color_bases(n):
|
||||
if rule.get('resolve'):continue
|
||||
c=self.color_contract(n,n,rule,boundary)
|
||||
if self.harmony.evaluate((n,),complete=True,expressions=(c,),infer=False).valid:return (c,)
|
||||
for arriving in self.harmony.spans:
|
||||
if n.start<arriving.start<n.end and arriving.start-n.start<=F(vocabulary.data('harmony')['anticipation_max']):
|
||||
c=types.ExpressionContract(n.event_id+':anticipation','anticipation','early_chord_tone',(n,),n,arriving.chord,arriving.start)
|
||||
if self.harmony.evaluate((n,),complete=True,expressions=(c,)).valid:return (c,)
|
||||
if self.harmony.evaluate((n,),complete=True,expressions=(c,),infer=False).valid:return (c,)
|
||||
return None
|
||||
|
||||
def tails(self,n,target):
|
||||
"""Finite resolution timing: direct, real space, or one valid intervening note."""
|
||||
gap=target.start-n.end
|
||||
if gap<0:return
|
||||
if gap==0:
|
||||
yield ((n.start,n.end,n.pitch.number()),(target.start,target.end,target.pitch.number())),(n,target)
|
||||
return
|
||||
yield ((n.start,n.end,n.pitch.number()),(n.end,target.start,None),(target.start,target.end,target.pitch.number())),(n,target)
|
||||
windows={(n.end,target.start)} if gap<=1 else set()
|
||||
if gap>=1:windows|={(n.end,n.end+F(1,2)),(target.start-F(1,2),target.start)}
|
||||
for a,b in sorted(windows):
|
||||
for number in range(max(self.low,n.pitch.number()-3),min(self.high,n.pitch.number()+3)+1):
|
||||
mid=self.note(a,b,number)
|
||||
if number in (n.pitch.number(),target.pitch.number()) or not self.harmony.ordinary(mid):continue
|
||||
events=[(n.start,n.end,n.pitch.number())]
|
||||
if a>n.end:events.append((n.end,a,None))
|
||||
events.append((a,b,number))
|
||||
if b<target.start:events.append((b,target.start,None))
|
||||
events.append((target.start,target.end,target.pitch.number()))
|
||||
yield tuple(events),(n,mid,target)
|
||||
|
||||
def resolutions(self,n):
|
||||
"""Joint color/delayed-approach actions; the destination is never optional."""
|
||||
requests=[]
|
||||
for rule,boundary in self.color_bases(n):
|
||||
if rule.get('resolve'):
|
||||
for gap in (F(),F(1,2),F(1)):
|
||||
if gap<=F(vocabulary.data('harmony')['resolution_gap']):requests.append(('color',rule,boundary,n.end+gap,(-2,-1,1,2)))
|
||||
delay=vocabulary.data('lead_ins')['delayed_approach']
|
||||
if n.end-n.start<=F(delay['max_note']) and n.pitch.number()%12 not in self.harmony.permitted(n.start,n.end):
|
||||
for arrival in map(F,delay['arrivals']):
|
||||
if n.end<=n.start+arrival<=n.start+F(delay['deadline']):requests.append(('delayed',delay,None,n.start+arrival,delay['steps']))
|
||||
for kind,rule,boundary,arrival,steps in requests:
|
||||
for delta in steps:
|
||||
number=n.pitch.number()+delta
|
||||
if not self.low<=number<=self.high:continue
|
||||
for hold in (F(1,2),F(1),F(2)):
|
||||
if arrival+hold>self.end or number%12 not in self.harmony.members(arrival,arrival+hold):continue
|
||||
target=self.note(arrival,arrival+hold,number,'destination')
|
||||
c=self.color_contract(n,target,rule,boundary) if kind=='color' else types.ExpressionContract(n.event_id+':delayed','delayed_approach',rule['id'],(n,),target,self.harmony.at(arrival).chord)
|
||||
# Color and delayed-approach contracts permit this intervening
|
||||
# window. Validate the invariant endpoints once; tails() checks
|
||||
# every inserted note's ordinary full-duration permission itself.
|
||||
if not self.harmony.evaluate((n,target),complete=True,expressions=(c,),infer=False).valid:continue
|
||||
for events,ns in self.tails(n,target):
|
||||
if not groups.valid(tuple((a,b) for a,b,_ in events)):continue
|
||||
yield events,ns,(c,)
|
||||
|
||||
def actions(self,time):
|
||||
if time in self._actions:return self._actions[time]
|
||||
result=[]
|
||||
@@ -118,6 +189,19 @@ class Decoder:
|
||||
for number in range(self.low,self.high+1):
|
||||
n=self.note(time,end,number);support=self.permission(n)
|
||||
if support is not None:add(((time,end,number),),(n,),support)
|
||||
if self.expressions_enabled and not self.harmony.ordinary(n):
|
||||
for events,ns,contracts in self.resolutions(n):add(events,ns,contracts)
|
||||
rule=vocabulary.data('lead_ins')['passing']
|
||||
if duration<=F(rule['max_note']):
|
||||
for step in rule['steps']:
|
||||
p=number+step
|
||||
if not self.low<=p<=self.high:continue
|
||||
for hold in (F(1,2),F(1),F(2)):
|
||||
if end+hold>self.end:continue
|
||||
target=self.note(end,end+hold,p);extra=self.permission(target)
|
||||
if extra is None or any(c.kind not in ('color','anticipated_color') for c in extra):continue
|
||||
c=types.ExpressionContract(n.event_id+':passing','passing',rule['id'],(n,),target,self.harmony.at(end).chord)
|
||||
add(((time,end,number),(end,end+hold,p)),(n,target),(c,*extra))
|
||||
if self.expressions_enabled:
|
||||
limits=vocabulary.data('roles')['difficulty']
|
||||
for rule in vocabulary.data('lead_ins')['patterns']:
|
||||
@@ -147,7 +231,7 @@ class Decoder:
|
||||
if rule['kind']=='connection':
|
||||
# Validation waits for the actual incoming event.
|
||||
add((*events,(target_time,end,number)),ns,(contract,))
|
||||
elif self.harmony.evaluate(ns,complete=True,expressions=(contract,)).valid:
|
||||
elif self.harmony.evaluate(ns,complete=True,expressions=(contract,),infer=False).valid:
|
||||
add((*events,(target_time,end,number)),ns,(contract,))
|
||||
# Complete ordinary triplet groups; do not leave a third-beat residual.
|
||||
if self.allow_triplets and time+1<=self.end:
|
||||
@@ -161,7 +245,7 @@ class Decoder:
|
||||
numbers.append(nxt);cursor+=F(1,3)
|
||||
if len(numbers)!=3:continue
|
||||
ns=tuple(self.note(time+i*F(1,3),time+(i+1)*F(1,3),p) for i,p in enumerate(numbers))
|
||||
if self.harmony.evaluate(ns,complete=True).valid:add(tuple((n.start,n.end,n.pitch.number()) for n in ns),ns)
|
||||
if self.harmony.evaluate(ns,complete=True,infer=False).valid:add(tuple((n.start,n.end,n.pitch.number()) for n in ns),ns)
|
||||
self._actions[time]=tuple(result);self.counts['valid_structures']+=len(result)
|
||||
return self._actions[time]
|
||||
|
||||
@@ -171,10 +255,13 @@ class Decoder:
|
||||
# rest-to-rest transition absent from that event representation.
|
||||
if state.events and state.events[-1][2] is None and events[0][2] is None:return None
|
||||
if notes and state.previous is not None and abs(notes[0].pitch.number()-state.previous)>12:return None
|
||||
if contracts and contracts[0].kind=='connection':
|
||||
if any(c.kind in ('connection','passing') and c.preparation is None for c in contracts):
|
||||
if not state.notes:return None
|
||||
c=dataclasses.replace(contracts[0],preparation=state.notes[-1]);contracts=(c,)
|
||||
_,error=expressions.validate((state.notes[-1],*notes),contracts,self.harmony)
|
||||
contracts=tuple(dataclasses.replace(c,preparation=state.notes[-1]) if c.kind in ('connection','passing') and c.preparation is None else c for c in contracts)
|
||||
if self.harmony.ordinary(state.notes[-1]):
|
||||
_,error=expressions.validate((state.notes[-1],*notes),contracts,self.harmony)
|
||||
else:
|
||||
_,error=expressions.validate((*state.notes,*notes),(*state.contracts,*contracts),self.harmony)
|
||||
if error:return None
|
||||
history=state.history;previous=state.previous;cost=state.cost
|
||||
for a,b,p in events:
|
||||
|
||||
Reference in New Issue
Block a user