+59
-36
@@ -131,11 +131,8 @@ def validate_events(events, part, lengths=None):
|
||||
if duration <= 0:
|
||||
raise ValueError('comping event must have positive sounding duration')
|
||||
if part == 'acoustic':
|
||||
if e.lane == 'combined':
|
||||
if duration < 1 and duration != fractions.Fraction(1, 2):
|
||||
raise ValueError('combined acoustic attacks require quarter/eighth notes or sustains')
|
||||
elif e.lane not in ('bass', 'upper') or duration not in (fractions.Fraction(1, 2), 1):
|
||||
raise ValueError('ordinary acoustic attacks require separate quarter/eighth notes')
|
||||
if e.lane not in ('bass', 'upper', 'combined') or (duration < 1 and duration != fractions.Fraction(1, 2)):
|
||||
raise ValueError('acoustic attacks require quarter/eighth notes or authored sustains')
|
||||
elif e.lane != 'electric':
|
||||
raise ValueError('electric comping requires chord events')
|
||||
for i, a in enumerate(ordered):
|
||||
@@ -143,7 +140,50 @@ def validate_events(events, part, lengths=None):
|
||||
if a.end > b.start and (part != 'acoustic' or a.lane == b.lane or 'combined' in (a.lane, b.lane)):
|
||||
raise ValueError('comping attacks cannot overlap on the same strings')
|
||||
|
||||
def fit(pattern, measure, prior, part, *, anticipate=False, limit=None, initial_bass=True):
|
||||
def assign_acoustic(events, harmony, breaks=()):
|
||||
"""Assign attacks, not notation fragments, across a complete source span.
|
||||
|
||||
Pair left to right within an effective harmony. An anticipated attack is
|
||||
already playing its target harmony. Form/participation boundaries break
|
||||
pairs; ordinary barlines and computational blocks do not.
|
||||
"""
|
||||
ordered = sorted(events, key=lambda e: e.start)
|
||||
if len({e.start for e in ordered}) != len(ordered):
|
||||
raise ValueError('acoustic rhythm must have one event per attack position')
|
||||
|
||||
def region(event):
|
||||
time = event.harmonic_time if event.harmonic_time is not None else event.start
|
||||
return next(i for i, (a, b, _) in enumerate(harmony) if a <= time < b)
|
||||
|
||||
lanes, i = ['combined'] * len(ordered), 0
|
||||
while i + 1 < len(ordered):
|
||||
a, b = ordered[i:i + 2]
|
||||
if b.start - a.start == fractions.Fraction(1, 2) and region(a) == region(b) and not any(a.start < t <= b.start for t in breaks):
|
||||
lanes[i:i + 2] = ['bass', 'upper']
|
||||
i += 2
|
||||
else:
|
||||
i += 1
|
||||
result = [dataclasses.replace(e, lane=lane) for e, lane in zip(ordered, lanes)]
|
||||
# Different strings may ring together, but never overlap a fresh attack on
|
||||
# the same strings. Preserve the authored release whenever it is playable.
|
||||
for i, a in enumerate(result):
|
||||
following = next((b.start for b in result[i + 1:] if a.lane == b.lane or 'combined' in (a.lane, b.lane)), a.end)
|
||||
result[i] = dataclasses.replace(a, end=min(a.end, following))
|
||||
# Shortening must not strand an anticipation before its promised arrival.
|
||||
# Rebind that event to the current chord, then reconsider its pair once.
|
||||
expired = {e.start for e in result if e.harmonic_time is not None and e.end <= e.harmonic_time}
|
||||
if expired:
|
||||
rebound = []
|
||||
for e in ordered:
|
||||
if e.start in expired:
|
||||
boundary = next(b for a, b, _ in harmony if a <= e.start < b)
|
||||
e = dataclasses.replace(e, harmonic_time=None, end=min(e.end, boundary))
|
||||
rebound.append(e)
|
||||
return assign_acoustic(rebound, harmony, breaks)
|
||||
validate_events(result, 'acoustic')
|
||||
return tuple(result)
|
||||
|
||||
def fit(pattern, measure, prior, part, *, anticipate=False, limit=None, assign=True):
|
||||
"""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)
|
||||
@@ -163,36 +203,20 @@ def fit(pattern, measure, prior, part, *, anticipate=False, limit=None, initial_
|
||||
for row, start, end, active in available:
|
||||
_, boundary, chord = active
|
||||
target = None
|
||||
if anticipate and row.get('anticipate') and row['layer'] in ('upper', 'electric'):
|
||||
if anticipate and row.get('anticipate'):
|
||||
future = next(((a, b, c) for a, b, c in harmony if a == start + fractions.Fraction(1, 2) and c), None)
|
||||
if future and future[0] < end and not generator.accompaniment.same_chord(chord, future[2]):
|
||||
target, boundary = future[0], future[1]
|
||||
end = min(end, boundary)
|
||||
events.append(Attack(row['layer'], start, end, target))
|
||||
if part == 'acoustic':
|
||||
for a, b, chord in harmony:
|
||||
if chord is None or (a == 0 and not initial_bass):
|
||||
continue
|
||||
local = [e for e in events if a <= (e.harmonic_time if e.harmonic_time is not None else e.start) < b]
|
||||
if not local:
|
||||
continue
|
||||
first = min(local, key=lambda e: e.start)
|
||||
if any(e.start == first.start and e.lane in ('bass', 'combined') for e in local):
|
||||
continue
|
||||
# Preserve the comping rhythm and upper voice. Add bass at the same
|
||||
# attack, allowing different releases when the next bass enters early.
|
||||
stop = min([first.end, first.start + 1, *(e.start for e in events if e.start > first.start and e.lane in ('bass', 'combined'))])
|
||||
duration = next((d for d in (fractions.Fraction(1), fractions.Fraction(1, 2)) if d <= stop - first.start), None)
|
||||
if duration is None:
|
||||
return None
|
||||
if first.end - first.start == duration:
|
||||
events[events.index(first)] = dataclasses.replace(first, lane='combined')
|
||||
else:
|
||||
events.append(Attack('bass', first.start, first.start + duration, first.harmonic_time))
|
||||
exceptions.append(('layer_assignment', f'bass included at first attack for harmony {a}: {first.start}'))
|
||||
events.sort(key=lambda e: (e.start, e.lane))
|
||||
events.append(Attack('acoustic' if part == 'acoustic' else row['layer'], start, end, target))
|
||||
try:
|
||||
validate_events(events, part, lengths)
|
||||
if part == 'acoustic' and assign:
|
||||
events = assign_acoustic(events, harmony)
|
||||
exceptions.append(('layer_assignment', 'combined attacks; same-harmony eighth pairs use bass then upper'))
|
||||
elif part == 'acoustic' and any(e.end <= e.start or (e.end - e.start < 1 and e.end - e.start != fractions.Fraction(1, 2)) for e in events):
|
||||
raise ValueError('acoustic articulation cannot fit this boundary')
|
||||
if part != 'acoustic' or assign:
|
||||
validate_events(events, part, lengths)
|
||||
except ValueError:
|
||||
return None
|
||||
return tuple(events), tuple(exceptions)
|
||||
@@ -267,11 +291,10 @@ def project(pattern, offset, lengths):
|
||||
rows.append({**row, 'onset': str(max(a, offset) - offset), 'duration': str(min(b, end) - max(a, offset))})
|
||||
return {**pattern, 'lengths': list(map(str, lengths)), 'events': rows}
|
||||
|
||||
def realize(pattern, inputs, part, melody, *, anticipate=False, limit=None, boundary=False):
|
||||
def realize(pattern, inputs, part, melody, *, anticipate=False, limit=None, boundary=False, assign=True):
|
||||
flat = flatten(inputs)
|
||||
lengths = tuple(generator.model.fraction(m['duration']) for m, _, _ in inputs)
|
||||
initial_bass = not generator.accompaniment.same_chord(inputs[0][1], generator.accompaniment.active_chord(inputs[0][0], fractions.Fraction(), inputs[0][1]))
|
||||
fitted = fit(pattern, flat, None, part, anticipate=anticipate, limit=limit, initial_bass=initial_bass)
|
||||
fitted = fit(pattern, flat, None, part, anticipate=anticipate, limit=limit, assign=assign)
|
||||
if fitted is None and boundary:
|
||||
# Keep the selected onset/layers at a protected handoff, using the longest
|
||||
# permitted short articulation that fits rather than another groove.
|
||||
@@ -281,11 +304,11 @@ def realize(pattern, inputs, part, melody, *, anticipate=False, limit=None, boun
|
||||
a, duration = fractions.Fraction(row['onset']), fractions.Fraction(row['duration'])
|
||||
_, b, _ = next(s for s in spans(flat, None) if s[0] <= a < s[1])
|
||||
duration = min(duration, b - a)
|
||||
if part == 'acoustic' and (row['layer'] != 'combined' or duration <= 1):
|
||||
if part == 'acoustic' and duration <= 1:
|
||||
duration = next((d for d in (fractions.Fraction(1), fractions.Fraction(1, 2)) if d <= duration), fractions.Fraction())
|
||||
if duration:
|
||||
rows.append({**row, 'duration': str(duration)})
|
||||
fitted = fit({**pattern, 'events': rows}, flat, None, part, anticipate=False, limit=limit, initial_bass=initial_bass)
|
||||
fitted = fit({**pattern, 'events': rows}, flat, None, part, anticipate=False, limit=limit, assign=assign)
|
||||
if fitted is not None:
|
||||
fitted = fitted[0], (*fitted[1], ('release', 'shortened articulation at source-defined boundary'))
|
||||
if fitted is None:
|
||||
|
||||
Reference in New Issue
Block a user