+26
-9
@@ -110,10 +110,10 @@ def combine(document: dict) -> None:
|
||||
b = fragments[end][0] if end in fragments else originals[end]
|
||||
a['tie_to'], b['tie_from'] = b['id'], a['id']
|
||||
|
||||
def validate_comping(document: dict) -> None:
|
||||
"""Check complete sounding events, not barline or notation fragments."""
|
||||
def sounding_lanes(measures):
|
||||
"""Recover complete string-lane events, preserving ties versus reattacks."""
|
||||
tones, starts, cursor = {}, {}, fractions.Fraction()
|
||||
for measure in document['staves'][0]['measures']:
|
||||
for measure in measures:
|
||||
for note in generator.score.measure_notes(measure):
|
||||
for pitch in note['pitches']:
|
||||
tones[pitch['id']] = pitch
|
||||
@@ -122,6 +122,8 @@ def validate_comping(document: dict) -> None:
|
||||
groups = {}
|
||||
for identifier, pitch in tones.items():
|
||||
if pitch.get('tie_from') in tones:
|
||||
if tones[pitch['tie_from']].get('tie_to') != identifier:
|
||||
raise ValueError('broken acoustic sustain link')
|
||||
continue
|
||||
start, tail = starts[identifier], pitch
|
||||
seen = {identifier}
|
||||
@@ -129,20 +131,35 @@ def validate_comping(document: dict) -> None:
|
||||
target = tail['tie_to']
|
||||
if target in seen:
|
||||
raise ValueError('cyclic acoustic sustain')
|
||||
continuation = tones[target]
|
||||
if continuation.get('tie_from') != tail['id'] or starts[target] != starts[tail['id']] + generator.model.fraction(tail['duration']):
|
||||
raise ValueError('broken acoustic sustain link or timing')
|
||||
if generator.model.pitch_number(continuation) != generator.model.pitch_number(tail) or continuation.get('tablature') != tail.get('tablature'):
|
||||
raise ValueError('acoustic sustain changes pitch or position')
|
||||
seen.add(target)
|
||||
tail = tones[target]
|
||||
tail = continuation
|
||||
end = starts[tail['id']] + generator.model.fraction(tail['duration'])
|
||||
string = pitch.get('tablature', {}).get('string')
|
||||
if string is None:
|
||||
raise ValueError('acoustic comping requires placed notes')
|
||||
groups.setdefault((start, end), set()).add('bass' if string >= 5 else 'upper')
|
||||
return groups
|
||||
|
||||
def validate_assignment(measures, events):
|
||||
expected = {}
|
||||
for e in events:
|
||||
lanes = ['bass', 'upper'] if e.lane == 'combined' else [e.lane]
|
||||
expected.setdefault((e.start, e.end), set()).update(lanes)
|
||||
if sounding_lanes(measures) != expected:
|
||||
raise ValueError('rendered acoustic attacks disagree with the selected bass/upper assignment')
|
||||
|
||||
def validate_comping(document: dict) -> None:
|
||||
"""Check complete sounding events, not barline or notation fragments."""
|
||||
groups = sounding_lanes(document['staves'][0]['measures'])
|
||||
events = []
|
||||
for (start, end), lanes in groups.items():
|
||||
if len(lanes) == 2:
|
||||
if end - start < 1 and end - start != fractions.Fraction(1, 2):
|
||||
raise ValueError('combined acoustic attacks require quarter/eighth notes or sustains')
|
||||
elif end - start not in (fractions.Fraction(1, 2), 1):
|
||||
raise ValueError('separate acoustic comping requires quarter/eighth notes')
|
||||
if end <= start or (end - start < 1 and end - start != fractions.Fraction(1, 2)):
|
||||
raise ValueError('acoustic comping requires quarter/eighth notes or authored sustains')
|
||||
events.append((start, end))
|
||||
events.sort()
|
||||
# Distinct lanes can overlap. The physical validator still checks sounding
|
||||
|
||||
+92
-4
@@ -8,10 +8,12 @@ import fractions
|
||||
import typing
|
||||
|
||||
import generator.accompaniment
|
||||
import generator.acoustic
|
||||
import generator.comping_rhythm
|
||||
import generator.chord_families
|
||||
import generator.acoustic_voicing
|
||||
import generator.arrange.form
|
||||
import generator.arrange.pickup
|
||||
import generator.fretboard
|
||||
import generator.guitar
|
||||
import generator.harmony
|
||||
@@ -210,6 +212,30 @@ class Canonical:
|
||||
self.patterns = {g[0][2]: (p, a) for g, (p, a, _) in zip(self.groups, choices)}
|
||||
self.locations = {g[0][2]: location for g, location in zip(self.groups, locations)}
|
||||
selected = [r for _, _, r in choices]
|
||||
self.acoustic_cache = {}
|
||||
self.pair_breaks = {self.first, len(self.measures)}
|
||||
self.pair_stops = []
|
||||
for section in sections:
|
||||
if section.role == 'accompaniment':
|
||||
self.pair_breaks.update((section.source_measure_index, section.source_measure_index + section.measure_count))
|
||||
for section, following in zip(sections, sections[1:]):
|
||||
if section.role != 'accompaniment' or not self.first:
|
||||
continue
|
||||
last = section.source_measure_index + section.measure_count - 1
|
||||
pad = generator.arrange.pickup.offset(self.measures[last], self.measures[0])
|
||||
if following.role == 'slash':
|
||||
self.pair_stops.append((last, pad))
|
||||
elif following.role == 'melody' and generator.arrange.pickup.enters(following, self.measures):
|
||||
attacks = [generator.model.fraction(n['onset']) for n in generator.score.measure_notes(self.measures[0]) if n['pitches']]
|
||||
if attacks:
|
||||
self.pair_stops.append((last, pad + min(attacks)))
|
||||
if part == 'acoustic':
|
||||
assigned = self.acoustic_realizations(self.measures)
|
||||
selected = [assigned[g[0][2]] for g in self.groups]
|
||||
for report in self.groove_report:
|
||||
report['retrieval_cost'] = report['cost']
|
||||
report['cost'] = sum(r.cost * len(g) for g, r in zip(self.groups, selected) if report['source_start'] <= g[0][2] <= report['source_end'])
|
||||
report['attack_assignment'] = 'combined_except_eighth_pairs'
|
||||
self.block_rhythms = {group[0][2]: rhythm for group, rhythm in zip(self.groups, selected)}
|
||||
prepared = [self.prepare(group, rhythm) for group, rhythm in zip(self.groups, selected)]
|
||||
active = tuple(span for spans in prepared for span in spans if span.events)
|
||||
@@ -221,7 +247,8 @@ class Canonical:
|
||||
previous = path[-1] if self.closed and path else ()
|
||||
for group, rhythm, spans in zip(self.groups, selected, prepared):
|
||||
grips = tuple(self.base[s.key] for s in spans if s.events)
|
||||
self.cache[self.key(group)] = self.render(group, rhythm, spans, grips, previous, False)
|
||||
key = (self.key(group), rhythm.signature) if part == 'acoustic' else self.key(group)
|
||||
self.cache[key] = self.render(group, rhythm, spans, grips, previous, False)
|
||||
cursor = fractions.Fraction()
|
||||
for m, _, i in group:
|
||||
length = generator.model.fraction(m['duration'])
|
||||
@@ -246,6 +273,52 @@ class Canonical:
|
||||
lengths.append(generator.model.fraction(m['duration']))
|
||||
cursor += lengths[-1]
|
||||
return generator.comping_rhythm.candidates(flat, None, self.part, tuple(melody), self.seed + '/' + str(inputs[0][2]), lengths=tuple(lengths), limit=limit)
|
||||
def acoustic_realizations(self, sources, bounds=None, stop=None):
|
||||
"""Assign the canonical attack stream once, before splitting it into blocks.
|
||||
|
||||
A variant uses the same patterns with the source's actual harmony and
|
||||
participation boundaries. No selected notes from another role enter here.
|
||||
"""
|
||||
carried = generator.accompaniment.carried_chords(sources)
|
||||
full = tuple((m, carried[i], i) for i, m in enumerate(sources))
|
||||
key = (self.key(full), bounds, stop)
|
||||
if key in self.acoustic_cache:
|
||||
return self.acoustic_cache[key]
|
||||
starts = [fractions.Fraction()]
|
||||
for m in sources:
|
||||
starts.append(starts[-1] + generator.model.fraction(m['duration']))
|
||||
breaks = {starts[i] for i in self.pair_breaks | set(bounds or ())}
|
||||
breaks.update(starts[i] + t for i, t in self.pair_stops)
|
||||
if stop:
|
||||
breaks.add(starts[stop[0]] + stop[1])
|
||||
events, raw = [], {}
|
||||
for original in self.groups:
|
||||
group = tuple(full[i] for _, _, i in original)
|
||||
index = group[0][2]
|
||||
pattern, anticipate = self.patterns[index]
|
||||
limit = starts[stop[0]] + stop[1] - starts[index] if stop and any(i == stop[0] for _, _, i in group) else None
|
||||
rhythm = generator.comping_rhythm.realize(pattern, group, 'acoustic', self.melody, anticipate=anticipate, limit=limit, boundary=True, assign=False)
|
||||
if rhythm is None:
|
||||
raise ValueError(f'source measure {sources[index]["number"]}: selected acoustic rhythm cannot fit written harmony')
|
||||
raw[index] = rhythm
|
||||
events.extend(dataclasses.replace(e, start=e.start + starts[index], end=e.end + starts[index],
|
||||
harmonic_time=e.harmonic_time + starts[index] if e.harmonic_time is not None else None) for e in rhythm.events)
|
||||
harmony = generator.comping_rhythm.spans(generator.comping_rhythm.flatten(full), None)
|
||||
assigned = generator.comping_rhythm.assign_acoustic(events, harmony, breaks)
|
||||
result = {}
|
||||
for original in self.groups:
|
||||
group = tuple(full[i] for _, _, i in original)
|
||||
index, end = group[0][2], group[-1][2] + 1
|
||||
local = tuple(dataclasses.replace(e, start=e.start - starts[index], end=e.end - starts[index],
|
||||
harmonic_time=e.harmonic_time - starts[index] if e.harmonic_time is not None else None)
|
||||
for e in assigned if starts[index] <= e.start < starts[end])
|
||||
melody = tuple((a + starts[i] - starts[index], b + starts[i] - starts[index]) for _, _, i in group for a, b in self.melody[i])
|
||||
flat = generator.comping_rhythm.flatten(group)
|
||||
contributions = generator.comping_rhythm.evaluate(local, 'acoustic', melody, generator.comping_rhythm.spans(flat, None), tuple(starts[i + 1] - starts[i] for _, _, i in group))
|
||||
result[index] = dataclasses.replace(raw[index], events=local, contributions=contributions,
|
||||
exceptions=(*raw[index].exceptions, ('layer_assignment', 'source-form combined attacks with non-overlapping same-harmony eighth pairs')))
|
||||
self.acoustic_cache[key] = result
|
||||
return result
|
||||
def prepare(self, inputs, rhythm):
|
||||
flat = generator.comping_rhythm.flatten(inputs)
|
||||
spans = []
|
||||
@@ -272,6 +345,8 @@ class Canonical:
|
||||
group = next((g for g in self.groups if any(i == index for _, _, i in g)), ((source, prior, index),))
|
||||
original_group = group
|
||||
sources = sources or self.measures
|
||||
if self.part == 'acoustic' and source != sources[index]:
|
||||
sources = [source if i == index else m for i, m in enumerate(sources)]
|
||||
carried = generator.accompaniment.carried_chords(sources)
|
||||
low, high = bounds or (self.first, len(sources))
|
||||
group = tuple((source if i == index else sources[i], prior if i == index else carried[i], i) for _, _, i in group if low <= i < high)
|
||||
@@ -279,12 +354,20 @@ class Canonical:
|
||||
if stop and any(i == stop[0] for _, _, i in group):
|
||||
limit = sum((generator.model.fraction(m['duration']) for m, _, i in group if i < stop[0]), fractions.Fraction()) + stop[1]
|
||||
key = self.key(group, limit)
|
||||
complete = None
|
||||
if self.part == 'acoustic' and original_group[0][2] in self.patterns:
|
||||
complete = self.acoustic_realizations(sources, (low, high), stop)[original_group[0][2]]
|
||||
key = (key, complete.signature)
|
||||
if key not in self.cache:
|
||||
start, end = group[0][2], group[-1][2]
|
||||
offset = sum((generator.model.fraction(m['duration']) for m, _, i in original_group if i < start), fractions.Fraction())
|
||||
if original_group[0][2] in self.patterns:
|
||||
pattern, anticipate = self.patterns[original_group[0][2]]
|
||||
if offset:
|
||||
if self.part == 'acoustic':
|
||||
end_time = offset + (limit if limit is not None else sum(generator.model.fraction(m['duration']) for m, _, _ in group))
|
||||
rhythm = dataclasses.replace(complete, events=tuple(dataclasses.replace(e, start=max(e.start, offset) - offset, end=min(e.end, end_time) - offset, harmonic_time=e.harmonic_time - offset if e.harmonic_time is not None and e.harmonic_time >= offset else None)
|
||||
for e in complete.events if e.start < end_time and e.end > offset))
|
||||
elif offset:
|
||||
# Fit the incoming context before slicing. A raw template sustain
|
||||
# may already have ended at a chord change; projecting it first
|
||||
# would resurrect that note as an unintended new downbeat attack.
|
||||
@@ -295,8 +378,9 @@ class Canonical:
|
||||
raise ValueError(f'source measure {source["number"]}: selected groove cannot fit incoming harmony')
|
||||
pattern = {**pattern, 'events': [{'onset': str(e.start), 'duration': str(e.end - e.start),
|
||||
'layer': e.lane, 'anticipate': e.harmonic_time is not None and e.lane in ('upper', 'electric')} for e in incoming.events]}
|
||||
pattern = generator.comping_rhythm.project(pattern, offset, tuple(generator.model.fraction(m['duration']) for m, _, _ in group))
|
||||
rhythm = generator.comping_rhythm.realize(pattern, group, self.part, self.melody, anticipate=anticipate, limit=limit, boundary=True)
|
||||
if self.part != 'acoustic':
|
||||
pattern = generator.comping_rhythm.project(pattern, offset, tuple(generator.model.fraction(m['duration']) for m, _, _ in group))
|
||||
rhythm = generator.comping_rhythm.realize(pattern, group, self.part, self.melody, anticipate=anticipate, limit=limit, boundary=True)
|
||||
if rhythm is None:
|
||||
raise ValueError(f'source measure {source["number"]}: selected groove cannot fit protected boundary')
|
||||
else:
|
||||
@@ -354,6 +438,8 @@ class Canonical:
|
||||
if grip:
|
||||
previous = grip
|
||||
key = self.key(inputs, limit)
|
||||
if self.part == 'acoustic':
|
||||
key = (key, rhythm.signature)
|
||||
self._reports[key] = rows
|
||||
voices = []
|
||||
for lane in ['upper', 'bass'] if self.part == 'acoustic' else ['electric']:
|
||||
@@ -380,6 +466,8 @@ class Canonical:
|
||||
emit(cursor, length)
|
||||
voices.append({'id': voice_id, 'notes': notes})
|
||||
result = split_measures({'voices': voices}, inputs, self.part)
|
||||
if self.part == 'acoustic':
|
||||
generator.acoustic.validate_assignment(result, rhythm.events)
|
||||
reports, cursor = [], fractions.Fraction()
|
||||
for m, _, i in inputs:
|
||||
duration = generator.model.fraction(m['duration'])
|
||||
|
||||
+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:
|
||||
|
||||
@@ -31,6 +31,8 @@ def _load(contents):
|
||||
policy = value['instruments'][part]
|
||||
if part == 'acoustic' and policy.get('first_attack') != 'bass_per_harmony':
|
||||
raise ValueError('acoustic patterns require bass at the first attack of each harmony')
|
||||
if part == 'acoustic' and policy.get('attack_assignment') != {'version': 1, 'mode': 'combined_except_eighth_pairs'}:
|
||||
raise ValueError('acoustic patterns require automatic eighth-pair assignment')
|
||||
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')
|
||||
@@ -62,16 +64,15 @@ def _load(contents):
|
||||
if onset.denominator > 2 or duration.denominator > 2:
|
||||
raise ValueError('comping vocabulary uses a quarter/eighth grid')
|
||||
if part == 'acoustic':
|
||||
if row['layer'] == 'combined':
|
||||
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')
|
||||
if row['layer'] != 'acoustic':
|
||||
raise ValueError('acoustic pattern layers are assigned from the complete rhythm')
|
||||
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')
|
||||
lanes = ('bass', 'upper') if row['layer'] == 'combined' else (row['layer'],)
|
||||
if row.get('anticipate') and row['layer'] not in ('acoustic', 'electric'):
|
||||
raise ValueError('only acoustic/electric chords can anticipate')
|
||||
# Acoustic overlaps are checked after deterministic string-lane
|
||||
# assignment, including neighboring figures in the source form.
|
||||
lanes = () if part == 'acoustic' 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})
|
||||
|
||||
Reference in New Issue
Block a user