71 lines
2.6 KiB
Python
71 lines
2.6 KiB
Python
"""Exact rhythmic groups for generated material; imported music is not quantized.
|
|
|
|
Binary boundaries may be displaced together. Tuplet runs must return to their
|
|
entry subdivision phase before binary material resumes. A search boundary is
|
|
not a rhythmic boundary and must not be used to clip a group.
|
|
"""
|
|
from __future__ import annotations
|
|
import dataclasses
|
|
import functools
|
|
from fractions import Fraction as F
|
|
|
|
|
|
@dataclasses.dataclass(frozen=True)
|
|
class RhythmGroup:
|
|
start: F
|
|
end: F
|
|
family: str
|
|
events: tuple[tuple[F, F], ...]
|
|
displacement: F = F()
|
|
source: str = ''
|
|
|
|
|
|
def describe(pattern, *, origin=F(), source='', imported=False):
|
|
if not pattern:return ()
|
|
if imported:
|
|
return (RhythmGroup(origin, origin+pattern[-1][1], 'source', pattern, source=source),)
|
|
result=[]; i=0
|
|
while i<len(pattern):
|
|
a,b=pattern[i]
|
|
if b<=a or i and a<pattern[i-1][1]:raise ValueError('overlapping rhythm group')
|
|
# A complete ternary figure can be intentionally displaced on an eighth.
|
|
if (b-a).denominator%3==0:
|
|
if (a*2).denominator!=1:raise ValueError('unexplained tuplet entry phase')
|
|
start=i; cursor=a
|
|
while i<len(pattern):
|
|
x,y=pattern[i]
|
|
if x!=cursor or (y-x).denominator not in (3,6):break
|
|
cursor=y;i+=1
|
|
if (cursor-a).denominator==1:break
|
|
if i==start or (cursor-a).denominator!=1:
|
|
raise ValueError('incomplete tuplet group')
|
|
group=pattern[start:i]
|
|
result.append(RhythmGroup(origin+a,origin+cursor,'triplet',group,a%1,source))
|
|
else:
|
|
if (a*4).denominator!=1 or (b*4).denominator!=1:
|
|
raise ValueError('unexplained binary subdivision phase')
|
|
family='sixteenth' if (a*2).denominator!=1 or (b*2).denominator!=1 else 'binary'
|
|
result.append(RhythmGroup(origin+a,origin+b,family,((a,b),),a%1,source));i+=1
|
|
return tuple(result)
|
|
|
|
|
|
@functools.lru_cache(maxsize=8192)
|
|
def valid(pattern):
|
|
try:describe(pattern);return True
|
|
except ValueError:return False
|
|
|
|
|
|
def signature(notes):
|
|
"""Translation-invariant duration, inter-onset and rest vocabulary."""
|
|
return tuple((n.end-n.start, notes[i+1].start-n.end if i+1<len(notes) else F())
|
|
for i,n in enumerate(notes))
|
|
|
|
|
|
def broaden(pattern, arrival, release):
|
|
"""Reopen an editable binary tail; never slice a tuplet or inherit its phase."""
|
|
if (arrival*2).denominator!=1 or (release*2).denominator!=1:return ()
|
|
for group in describe(pattern):
|
|
if group.family=='triplet' and group.start<arrival<group.end:return ()
|
|
result=tuple((a,min(b,arrival)) for a,b in pattern if a<arrival)+((arrival,release),)
|
|
return result if valid(result) else ()
|