"""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 import fractions @dataclasses.dataclass(frozen=True) class RhythmGroup: start: fractions.Fraction end: fractions.Fraction family: str events: tuple[tuple[fractions.Fraction, fractions.Fraction], ...] displacement: fractions.Fraction = fractions.Fraction() source: str = '' def describe(pattern, *, origin=fractions.Fraction(), 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 fractions.Fraction()) for i, n in enumerate(notes)) def descriptor(pattern, duration): """Retain ordered attacks, durations and space, including a trailing breath. Absolute placement belongs to the caller. Equal counts or Boolean labels do not make two different rhythmic sentences interchangeable. """ return (duration, tuple(pattern)) 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 ()