Cleanup
This commit is contained in:
+51
-21
@@ -33,8 +33,9 @@ class Operators:
|
||||
def __init__(self,harmony,analysis,end,start=F(),enabled=True,sequences=None,rhythms=None):
|
||||
self.harmony=harmony;self.analysis=analysis;self.start=start;self.end=end;self.enabled=enabled
|
||||
self.rules=vocabulary.data('solo')['operators'];self.cache={};self.pitch_cache={};self.coverage=Counter()
|
||||
self.ordinary_cache={}
|
||||
self.ordinary_cache={};self.validity_cache={}
|
||||
self.sequences=sequences
|
||||
self.licensed=frozenset(s.steps for values in sequences.paths.values() for s in values) if sequences else None
|
||||
self.patterns=[]
|
||||
if rhythms is not None:
|
||||
library=rhythm.Rhythms(rhythms,analysis.tempo)
|
||||
@@ -57,6 +58,7 @@ class Operators:
|
||||
if self.harmony.profile.permits((p:=types.scale_pitch(span.scale,i)).number()))
|
||||
return self.pitch_cache[span.scale]
|
||||
|
||||
@functools.lru_cache(maxsize=32768)
|
||||
def note(self,start,end,pitch,role):
|
||||
# Identity follows exact musical content, not an output filename or hash.
|
||||
return types.Note(start,end,pitch,role=role,event_id=f'{start}:{end}:{pitch.number()}')
|
||||
@@ -68,7 +70,7 @@ class Operators:
|
||||
return tuple(p for octave in range(1,6)
|
||||
if self.harmony.profile.permits((p:=dataclasses.replace(spelling,octave=octave)).number()))
|
||||
|
||||
def _accepted(self,action):
|
||||
def _accepted(self,action,validate=True):
|
||||
self.coverage['proposed:'+action.kind]+=1
|
||||
if action.end>self.end or action.end<=action.start:
|
||||
self.coverage['rejected_by_allocation:'+action.kind]+=1;return False
|
||||
@@ -76,40 +78,51 @@ class Operators:
|
||||
self.coverage['rejected_by_physical_domain:'+action.kind]+=1;return False
|
||||
if not groups.valid(tuple((n.start-action.start,n.end-action.start) for n in action.notes)):
|
||||
self.coverage['rejected_by_rhythm:'+action.kind]+=1;return False
|
||||
self.coverage['feasible:'+action.kind]+=1
|
||||
return self.is_valid(action) if validate else True
|
||||
|
||||
def is_valid(self,action):
|
||||
key=action.key
|
||||
if key in self.validity_cache:return self.validity_cache[key]
|
||||
if action.contracts:
|
||||
valid=self.harmony.evaluate(action.notes,expressions=action.contracts,complete=False,until=action.end).valid
|
||||
else:
|
||||
valid=True
|
||||
for n in action.notes:
|
||||
key=(n.start,n.end,n.pitch.number())
|
||||
if key not in self.ordinary_cache:self.ordinary_cache[key]=self.harmony.evaluate((n,),complete=True,until=n.end).valid
|
||||
if not self.ordinary_cache[key]:valid=False;break
|
||||
if not valid:
|
||||
self.coverage['rejected_by_rule:'+action.kind]+=1
|
||||
return False
|
||||
self.coverage['valid:'+action.kind]+=1
|
||||
return True
|
||||
note_key=(n.start,n.end,n.pitch.number())
|
||||
if note_key not in self.ordinary_cache:self.ordinary_cache[note_key]=self.harmony.evaluate((n,),complete=True,until=n.end).valid
|
||||
if not self.ordinary_cache[note_key]:valid=False;break
|
||||
self.coverage[('valid:' if valid else 'rejected_by_rule:')+action.kind]+=1
|
||||
self.validity_cache[key]=valid
|
||||
if len(self.validity_cache)>8192:self.validity_cache.pop(next(iter(self.validity_cache)))
|
||||
return valid
|
||||
|
||||
@functools.lru_cache(maxsize=8192)
|
||||
def _color(self,n,span):
|
||||
result=[]
|
||||
for rule in vocabulary.data('harmony')['colors']:
|
||||
if rule.get('resolve') or not expressions.eligible_color(rule,span,self.harmony):continue
|
||||
expected=expressions.degree_pitch(expressions.parsed_chord(span.chord),rule['degree'],rule['alter'],n.pitch.number())
|
||||
if expected.number()!=n.pitch.number():continue
|
||||
c=types.ExpressionContract(n.event_id+':'+rule['id'],'color',rule['id'],(n,),n,span.chord)
|
||||
yield Action(n.start,n.end,(n,),'color',(c,),rule['id'])
|
||||
result.append(Action(n.start,n.end,(n,),'color',(c,),rule['id']))
|
||||
return tuple(result)
|
||||
|
||||
def at(self,time,eligible=None,first=None):
|
||||
def proposals(self,time):
|
||||
return self.at(time,validate=False)
|
||||
|
||||
def at(self,time,eligible=None,first=None,*,validate=True):
|
||||
"""Unpruned legal actions. Incoming-state ranking is a separate operation."""
|
||||
if eligible is None and first is None and time in self.cache:return self.cache[time]
|
||||
if eligible is None and first is None and (time,validate) in self.cache:return self.cache[(time,validate)]
|
||||
span=self.harmony.at(time);values=[]
|
||||
if not span or not span.scale:
|
||||
end=min(self.end,span.end if span else self.end)
|
||||
result=(Action(time,end,(),'space'),)
|
||||
if eligible is not None:return tuple(a for a in result if eligible(a))
|
||||
self.cache[time]=result;return result
|
||||
self.cache[(time,validate)]=result;return result
|
||||
def add(action):
|
||||
if eligible is not None and not eligible(action):return
|
||||
if self._accepted(action):values.append(action)
|
||||
if self._accepted(action,validate=validate):values.append(action)
|
||||
def entry(a,b,pitch):
|
||||
return first is None or first==(a,b,pitch.number())
|
||||
durations=tuple(map(F,self.rules['durations']))
|
||||
@@ -122,7 +135,7 @@ class Operators:
|
||||
for pitch in self.pitches(time):
|
||||
if not entry(time,time+duration,pitch):continue
|
||||
n=self.note(time,time+duration,pitch,'motion')
|
||||
if pitch.number()%12 in types.permitted(self.harmony.choices,time,n.end):
|
||||
if pitch.number()%12 in self.harmony.permitted(time,n.end):
|
||||
add(Action(time,n.end,(n,),'tone'))
|
||||
if self.enabled:
|
||||
delayed=vocabulary.data('lead_ins').get('delayed_approach')
|
||||
@@ -139,7 +152,7 @@ class Operators:
|
||||
for step in delayed['steps']:
|
||||
n=self.note(time,time+F(delayed['max_note']),expressions.chromatic(p,-step),'delayed_approach')
|
||||
if not entry(n.start,n.end,n.pitch):continue
|
||||
if n.pitch.number()%12 in types.permitted(self.harmony.choices,n.start,n.end):continue
|
||||
if n.pitch.number()%12 in self.harmony.permitted(n.start,n.end):continue
|
||||
c=types.ExpressionContract(n.event_id+':delayed','delayed_approach',delayed['id'],(n,),target,region.chord)
|
||||
add(Action(time,n.end,(n,),'delayed_approach',(c,),delayed['id']))
|
||||
# Optional colors retain functional degree identity independently of
|
||||
@@ -254,7 +267,7 @@ class Operators:
|
||||
if first is not None and (time+pattern[0][0],time+pattern[0][1])!=first[:2]:continue
|
||||
for stride in (-2,-1,1,2):
|
||||
# Catalog licenses relative paths, but never sets family probability.
|
||||
if self.sequences and not any(s.steps==(stride,)*(len(pattern)-1) for s in self.sequences.paths.get(len(pattern),())):continue
|
||||
if self.licensed is not None and (stride,)*(len(pattern)-1) not in self.licensed:continue
|
||||
for initial in self.pitches(time):
|
||||
if not entry(time+pattern[0][0],time+pattern[0][1],initial):continue
|
||||
cursor=time;p=initial;notes=[]
|
||||
@@ -265,8 +278,24 @@ class Operators:
|
||||
if index:
|
||||
try:p=types.scale_pitch(region.scale,types.scale_index(region.scale,p.number())+stride)
|
||||
except ValueError:break
|
||||
notes.append(self.note(cursor,time+b,p,'scalar' if abs(stride)==1 else 'arpeggio'))
|
||||
if len(notes)==len(pattern):add(Action(time,time+length,tuple(notes),'scalar' if abs(stride)==1 else 'arpeggio',source=f'{name}:degree_stride:{stride}'))
|
||||
notes.append(self.note(cursor,time+b,p,'scalar' if abs(stride)==1 else 'scale_thirds'))
|
||||
if len(notes)==len(pattern):add(Action(time,time+length,tuple(notes),'scalar' if abs(stride)==1 else 'scale_thirds',source=f'{name}:degree_stride:{stride}'))
|
||||
# A real arpeggio walks successive members of the WRITTEN chord, not
|
||||
# alternate degrees of its selected seven-note collection.
|
||||
for direction in (-1,1):
|
||||
for initial in self.pitches(time):
|
||||
if initial.number()%12 not in span.roles.basic:continue
|
||||
if not entry(time+pattern[0][0],time+pattern[0][1],initial):continue
|
||||
p=initial;notes=[]
|
||||
for index,(a,b) in enumerate(pattern):
|
||||
region=self.harmony.at(time+a)
|
||||
if not region or not region.scale:break
|
||||
if index:
|
||||
choices=[q for q in self.pitches(time+a) if q.number()%12 in region.roles.basic and (q.number()-p.number())*direction>0]
|
||||
if not choices:break
|
||||
p=min(choices,key=lambda q:abs(q.number()-p.number()))
|
||||
notes.append(self.note(time+a,time+b,p,'arpeggio'))
|
||||
if len(notes)==len(pattern):add(Action(time,time+length,tuple(notes),'arpeggio',source=f'{name}:written_chord:{direction}'))
|
||||
# Equivalent musical actions need one stable derivation, not extra votes.
|
||||
unique={}
|
||||
for a in values:unique.setdefault(a.key,a)
|
||||
@@ -274,9 +303,10 @@ class Operators:
|
||||
if eligible is not None or first is not None:return result
|
||||
if len(self.cache)>=32:self.cache.pop(next(iter(self.cache)))
|
||||
if len(self.ordinary_cache)>8192:self.ordinary_cache.clear()
|
||||
self.cache[time]=result
|
||||
self.cache[(time,validate)]=result
|
||||
return result
|
||||
|
||||
@functools.lru_cache(maxsize=512)
|
||||
def related(self,time,home,kind):
|
||||
"""Construct a heard-theme return or answer as a whole group, never a prefix."""
|
||||
if len(home)<3:return ()
|
||||
|
||||
Reference in New Issue
Block a user