Comping Gen

This commit is contained in:
khodowany
2026-10-06 22:13:50 -05:00
parent 2f21ec1f16
commit 125aaa1637
13 changed files with 213 additions and 44 deletions
+26 -3
View File
@@ -64,7 +64,7 @@ def flatten(inputs):
result['duration'] = generator.model.rational(cursor)
return result
def validate_events(events, part):
def validate_events(events, part, lengths=None):
ordered = sorted(events, key=lambda e: e.start)
for e in ordered:
duration = e.end - e.start
@@ -80,13 +80,20 @@ def validate_events(events, part):
raise ValueError('electric comping requires chord events')
if any(a.end > b.start for a, b in zip(ordered, ordered[1:])):
raise ValueError('separate comping events cannot overlap; use one combined sustain')
if part == 'acoustic' and lengths is not None:
cursor = fractions.Fraction()
for length in lengths:
first = next((e for e in ordered if cursor <= e.start < cursor + length), None)
if first and first.lane not in ('bass', 'combined'):
raise ValueError('first acoustic attack in each measure must include bass')
cursor += length
def fit(pattern, measure, prior, part, *, anticipate=False, limit=None):
"""Bind a complete figure to written harmony, with bounded early arrivals."""
length = generator.model.fraction(measure['duration'])
limit = length if limit is None else min(length, limit)
harmony = spans(measure, prior)
events, exceptions = [], []
events, exceptions, available = [], [], []
for row in pattern['events']:
start = fractions.Fraction(row['onset'])
end = min(start + fractions.Fraction(row['duration']), limit)
@@ -96,6 +103,22 @@ def fit(pattern, measure, prior, part, *, anticipate=False, limit=None):
if active is None or active[2] is None:
exceptions.append(('space', 'no written harmony at ' + str(start)))
continue
available.append((dict(row), start, end, active))
lengths = tuple(map(fractions.Fraction, pattern.get('lengths', (length,))))
if part == 'acoustic' and generator.vocabulary.accompaniment(part)['first_attack'] == 'bass_or_combined':
cursor = fractions.Fraction()
for span in lengths:
local = [(row, start) for row, start, _, _ in available if cursor <= start < cursor + span]
if local and local[0][0]['layer'] == 'upper':
first, onset = local[0]
later = next(((row, start) for row, start in local[1:] if row['layer'] == 'bass'), None)
first['layer'] = 'bass'
first.pop('anticipate', None)
if later:
later[0]['layer'] = 'upper'
exceptions.append(('layer_assignment', f'bass first at {onset}' + (f'; upper exchanged with bass at {later[1]}' if later else '; no later bass to exchange')))
cursor += span
for row, start, end, active in available:
_, boundary, chord = active
target = None
if anticipate and row.get('anticipate') and row['layer'] in ('upper', 'electric'):
@@ -105,7 +128,7 @@ def fit(pattern, measure, prior, part, *, anticipate=False, limit=None):
end = min(end, boundary)
events.append(Attack(row['layer'], start, end, target))
try:
validate_events(events, part)
validate_events(events, part, lengths)
except ValueError:
return None
return tuple(events), tuple(exceptions)