Rhythm Patterns

This commit is contained in:
khodowany
2026-10-06 23:55:58 -05:00
parent d2f493c17b
commit 23b606e47f
11 changed files with 673 additions and 109 deletions
+14 -7
View File
@@ -23,14 +23,17 @@ def _load(contents):
result = {}
for name, text in contents:
value = json.loads(text)
if value.get('version') != (4 if name == 'accompaniment' else 1) or not value.get('provenance') or not value.get('license'):
if value.get('version') != (5 if name == 'accompaniment' else 1) or not value.get('provenance') or not value.get('license'):
raise ValueError(f'{name}: unsupported or unprovenanced production vocabulary')
if name == 'accompaniment':
signatures = {}
for part in ('electric', 'acoustic'):
policy = value['instruments'][part]
if part == 'acoustic' and policy.get('first_attack') != 'bass_or_combined':
raise ValueError('acoustic patterns require a bass-or-combined first attack policy')
if part == 'acoustic' and policy.get('first_attack') != 'bass_per_harmony':
raise ValueError('acoustic patterns require bass at the first attack of each harmony')
groove = policy['groove']
if any(type(groove[k]) is not int for k in ('cycle_patterns', 'slot_candidates', 'fill_spacing_measures')) or not 1 <= groove['cycle_patterns'] <= 4 or not 4 <= groove['slot_candidates'] <= 8 or groove['fill_spacing_measures'] < 8:
raise ValueError('invalid bounded accompaniment groove policy')
identifiers = set()
signatures[part] = set()
for pattern in policy['patterns']:
@@ -41,7 +44,7 @@ def _load(contents):
if not 1 <= len(lengths) <= 2 or any(n <= 0 for n in lengths):
raise ValueError('patterns require one or two positive measure lengths')
# Validate musical events before any measure projection or ties.
previous = fractions.Fraction()
previous, releases = fractions.Fraction(), {}
onsets = []
for row in pattern['events']:
onset, duration = fractions.Fraction(row['onset']), fractions.Fraction(row['duration'])
@@ -51,15 +54,19 @@ def _load(contents):
raise ValueError('comping vocabulary uses a quarter/eighth grid')
if part == 'acoustic':
if row['layer'] == 'combined':
if duration <= 1:
raise ValueError('combined acoustic sustain must exceed one beat')
if duration < 1 and duration != fractions.Fraction(1, 2):
raise ValueError('combined acoustic attacks require quarter/eighth notes or sustains')
elif row['layer'] not in ('bass', 'upper') or duration not in (fractions.Fraction(1, 2), 1):
raise ValueError('acoustic short attacks require bass/upper quarter/eighth notes')
elif row['layer'] != 'electric':
raise ValueError('electric pattern must contain electric chord attacks')
if row.get('anticipate') and row['layer'] not in ('upper', 'electric'):
raise ValueError('only upper/electric chords can anticipate')
previous = onset + duration
lanes = ('bass', 'upper') if row['layer'] == 'combined' else (row['layer'],)
if any(releases.get(lane, fractions.Fraction()) > onset for lane in lanes):
raise ValueError('overlapping accompaniment attacks on the same strings')
releases.update({lane: onset + duration for lane in lanes})
previous = onset
onsets.append(onset)
if len(lengths) == 2:
signatures[part].add((lengths, tuple(onsets)))