Rhythm Patterns
This commit is contained in:
+23
-11
@@ -7,6 +7,7 @@ import itertools
|
||||
import generator.accompaniment
|
||||
import generator.model
|
||||
import generator.score
|
||||
import generator.harmonic_time
|
||||
|
||||
@dataclasses.dataclass
|
||||
class HarmonySpan:
|
||||
@@ -138,18 +139,29 @@ def validate_comping(document: dict) -> None:
|
||||
events = []
|
||||
for (start, end), lanes in groups.items():
|
||||
if len(lanes) == 2:
|
||||
if end - start <= 1:
|
||||
raise ValueError('combined acoustic sustain must exceed one beat')
|
||||
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')
|
||||
events.append((start, end))
|
||||
events.sort()
|
||||
if any(a[1] > b[0] for a, b in zip(events, events[1:])):
|
||||
raise ValueError('acoustic bass and upper must release together or remain separate')
|
||||
cursor = fractions.Fraction()
|
||||
for measure in document['staves'][0]['measures']:
|
||||
length = generator.model.fraction(measure['duration'])
|
||||
first = next((event for event in events if cursor <= event[0] < cursor + length), None)
|
||||
if first and 'bass' not in groups[first]:
|
||||
raise ValueError(f'measure {measure["number"]}: first acoustic attack must include bass')
|
||||
cursor += length
|
||||
# Distinct lanes can overlap. The physical validator still checks sounding
|
||||
# strings and grip span, including staggered releases after voice merging.
|
||||
for i, a in enumerate(events):
|
||||
for b in events[i + 1:]:
|
||||
if a[1] > b[0] and groups[a] & groups[b]:
|
||||
raise ValueError('acoustic attacks overlap within the same lane')
|
||||
spans = []
|
||||
for span in generator.harmonic_time.read(document['staves'][0]):
|
||||
if spans and generator.accompaniment.same_chord(spans[-1].chord, span.chord):
|
||||
spans[-1] = dataclasses.replace(spans[-1], end=span.end)
|
||||
else:
|
||||
spans.append(span)
|
||||
for span in spans:
|
||||
if span.chord is None:
|
||||
continue
|
||||
first = next((event for event in events if span.start <= event[0] < span.end), None)
|
||||
# A bounded anticipated bass can already be sounding into this harmony.
|
||||
anticipated = any(span.start - fractions.Fraction(1, 2) <= a < span.start < b and 'bass' in groups[a, b] for a, b in events)
|
||||
if first and not anticipated and not any('bass' in groups[e] for e in events if e[0] == first[0]):
|
||||
raise ValueError(f'measure {span.source_measure}, beat {span.beat}: first acoustic attack for a chord must include bass')
|
||||
|
||||
Reference in New Issue
Block a user