Cleanup
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
"""Complete generated rhythm groups, indexed once, independent of source rhythms."""
|
||||
from fractions import Fraction as F
|
||||
from generator import vocabulary
|
||||
from generator.solo import groups, policy
|
||||
|
||||
class Rhythms:
|
||||
def __init__(self, library, tempo):
|
||||
self.groups=[]; limits=vocabulary.data('roles')['difficulty']
|
||||
for item in vocabulary.data('solo')['groups']:
|
||||
difficulty=item.get('difficulty')
|
||||
if difficulty and tempo>limits[difficulty+'_max_bpm']:continue
|
||||
cursor=F();pattern=[]
|
||||
for value in item['values']:
|
||||
duration=F(value.removeprefix('r:'))
|
||||
if not value.startswith('r:'):pattern.append((cursor,cursor+duration))
|
||||
cursor+=duration
|
||||
self.groups.append((item['id'],tuple(pattern),cursor,tuple(item['purpose'])))
|
||||
for cell in vocabulary.rhythm_cells(tempo):
|
||||
# Imported authored cells remain whole. They are never sliced to fit.
|
||||
if groups.valid(cell.pattern):self.groups.append((cell.id,cell.pattern,cell.duration,('develop','answer') if cell.purpose=='answer' else ('arpeggio','scalar','develop','neighbor')))
|
||||
# Meter-indexed catalog representatives: file counts do not set style frequency.
|
||||
self.catalog={}
|
||||
for meter,items in library.snippets.items():
|
||||
representatives={}
|
||||
for item in items:
|
||||
for bar in item.bars:
|
||||
pattern=tuple((e.onset,e.onset+e.duration) for e in bar.events if e.sounding)
|
||||
if any(e.continuation for e in bar.events) or not pattern or not groups.valid(pattern):continue
|
||||
if any(b-a<F(1,2) for a,b in pattern):continue
|
||||
descriptor=(len(pattern),pattern[0][0]>0,any(a[1]<b[0] for a,b in zip(pattern,pattern[1:])),len(set(b-a for a,b in pattern)))
|
||||
representatives.setdefault(descriptor,(item.id,pattern,bar.length,('develop','answer')))
|
||||
self.catalog[meter]=tuple(representatives.values())
|
||||
self.cache={}
|
||||
|
||||
def realize(self,start,end,purpose,seed,meter):
|
||||
key=(start,end,purpose,seed,meter)
|
||||
if key in self.cache:return self.cache[key]
|
||||
cursor=start;notes=[];sources=[];chosen=[];home=None;uses=0
|
||||
while cursor<end:
|
||||
available=[g for g in self.groups if purpose in g[3] and g[2]<=end-cursor]
|
||||
# Catalog is a separate family, not thousands of duplicate votes.
|
||||
if purpose in ('develop','answer'):
|
||||
available += sorted((g for g in self.catalog.get(meter,()) if g[2]<=end-cursor),key=lambda g:policy.stable_rank(seed,('catalog',cursor,g[0])))[:1]
|
||||
available=[g for g in available if ((end-cursor-g[2])*2).denominator==1]
|
||||
if not available:return (),(),()
|
||||
# Prefer complete multi-event groups; single values finish an exact remainder.
|
||||
large=[g for g in available if g[2]>=1]
|
||||
if home is not None and uses<2 and home in available:
|
||||
selected=home
|
||||
else:
|
||||
# Establish one rhythmic idea before reshaping it. A complete phrase
|
||||
# is not a fresh unrelated catalog draw at every short boundary.
|
||||
selected=min(large or available,key=lambda g:policy.stable_rank(seed,('rhythm',len(chosen)//4,g[0])))
|
||||
home=selected;uses=0
|
||||
uses+=1
|
||||
name,pattern,length,_=selected
|
||||
events=tuple((cursor+a,cursor+b) for a,b in pattern)
|
||||
notes.extend(events);sources.append(name)
|
||||
chosen.extend(groups.describe(pattern,origin=cursor,source=name))
|
||||
cursor+=length
|
||||
result=(tuple(notes),tuple(chosen),tuple(sources));self.cache[key]=result
|
||||
return result
|
||||
Reference in New Issue
Block a user