@@ -9,6 +9,65 @@ import generator.solo.analysis
|
||||
import generator.solo.policy
|
||||
import generator.vocabulary
|
||||
|
||||
def meter_context(measure):
|
||||
"""Nominal meter and explicit additive grouping, independent of bar length.
|
||||
|
||||
In particular, a three-quarter-note pickup or a 6/8 bar is not 3/4.
|
||||
"""
|
||||
groups = measure['meter']['groups']
|
||||
if not groups or any(g['unit'] != 4 for g in groups):
|
||||
raise ValueError(f"source measure {measure['number']}: no comping vocabulary for {measure['time_signature']}")
|
||||
beats = tuple(b for g in groups for b in g['beats'])
|
||||
total = sum(beats)
|
||||
if total not in (3, 4, 5) or (len(beats) > 1 and beats not in ((3, 2), (2, 3))):
|
||||
raise ValueError(f"source measure {measure['number']}: unsupported comping grouping {measure['time_signature']}")
|
||||
return f'{total}/4', beats if len(beats) > 1 else ()
|
||||
|
||||
def grouping_for(run, melody):
|
||||
"""Choose one source-only quintuple feel, shared by independent roles.
|
||||
|
||||
Written grouping wins. Otherwise compare actual chord changes, entrances
|
||||
after an eighth-note breath, then all melodic attacks at the group boundary.
|
||||
Counts are evidence, not inferred accents or performance dynamics.
|
||||
"""
|
||||
meter, explicit = meter_context(run[0][0][0])
|
||||
if meter != '5/4':
|
||||
return (), {'reason': 'simple_meter'}
|
||||
if explicit:
|
||||
return explicit, {'reason': 'written'}
|
||||
evidence = {}
|
||||
for boundary in (3, 2):
|
||||
changes, entrances, attacks = 0, 0, 0
|
||||
for group in run:
|
||||
for m, prior, i in group:
|
||||
changes += sum(a == boundary and c is not None for a, _, c in spans(m, prior))
|
||||
context = melody[i]
|
||||
for a, _ in context:
|
||||
if a != boundary:
|
||||
continue
|
||||
attacks += 1
|
||||
previous = max((b for x, b in context if x < a), default=fractions.Fraction())
|
||||
entrances += a - previous >= fractions.Fraction(1, 2)
|
||||
evidence[boundary] = (changes, entrances, attacks)
|
||||
boundary = 2 if evidence[2] > evidence[3] else 3
|
||||
return (boundary, 5 - boundary), {'reason': 'source' if evidence[2] != evidence[3] else 'default_3+2',
|
||||
'boundary_evidence': {str(k): dict(zip(('chord_changes', 'melody_entrances', 'melody_attacks'), v)) for k, v in evidence.items()}}
|
||||
|
||||
def patterns_for(policy, measure, lengths, grouping=()):
|
||||
meter, explicit = meter_context(measure)
|
||||
grouping = explicit or grouping or ((3, 2) if meter == '5/4' else ())
|
||||
# Untagged in-memory figures are allowed for independently authored unit
|
||||
# tests; the resource loader requires metadata for every production entry.
|
||||
return [p for p in policy['patterns'] if tuple(map(fractions.Fraction, p['lengths'])) == tuple(lengths)
|
||||
and (p.get('meter') == 'partial' or (p.get('meter', meter) == meter and tuple(p.get('grouping', grouping)) == grouping))]
|
||||
|
||||
def relevant_policy(policy, measures):
|
||||
"""An unused meter's vocabulary must not influence this role's decisions."""
|
||||
meters = {meter_context(m)[0] for m in measures}
|
||||
lengths = {generator.model.fraction(m['duration']) for m in measures}
|
||||
return {**policy, 'patterns': [p for p in policy['patterns'] if p.get('meter') in meters
|
||||
or (p.get('meter') == 'partial' and fractions.Fraction(p['lengths'][0]) in lengths)]}
|
||||
|
||||
@dataclasses.dataclass(frozen=True, order=True)
|
||||
class Attack:
|
||||
lane: str
|
||||
@@ -164,13 +223,11 @@ def evaluate(events, part, melody, harmony, lengths):
|
||||
divisor = max(1, len(lengths))
|
||||
return tuple((k, v / divisor) for k, v in (('activity', activity), ('melody_support', support), ('harmonic_support', coverage), ('articulation', articulation)))
|
||||
|
||||
def candidates(measure, prior, part, melody, seed, *, lengths=None, limit=None):
|
||||
def candidates(measure, prior, part, melody, seed, *, lengths=None, limit=None, grouping=()):
|
||||
lengths = lengths or (generator.model.fraction(measure['duration']),)
|
||||
policy = generator.vocabulary.accompaniment(part)
|
||||
result = []
|
||||
for pattern in policy['patterns']:
|
||||
if tuple(map(fractions.Fraction, pattern['lengths'])) != tuple(lengths):
|
||||
continue
|
||||
for pattern in patterns_for(policy, measure, lengths, grouping):
|
||||
for anticipate in (False, True):
|
||||
fitted = fit(pattern, measure, prior, part, anticipate=anticipate, limit=limit)
|
||||
if fitted is None:
|
||||
@@ -257,14 +314,15 @@ def choose_cycles(groups, part, melody, seed):
|
||||
runs = []
|
||||
for group in groups:
|
||||
m = group[0][0]
|
||||
key = (m['time_signature'], generator.model.fraction(m['duration']))
|
||||
key = (meter_context(m), generator.model.fraction(m['duration']))
|
||||
if not runs or runs[-1][0] != key:
|
||||
runs.append((key, []))
|
||||
runs[-1][1].append(group)
|
||||
for (meter, length), run in runs:
|
||||
patterns = [p for p in policy['patterns'] if tuple(map(fractions.Fraction, p['lengths'])) == (length, length)]
|
||||
for ((meter, _), length), run in runs:
|
||||
grouping, grouping_evidence = grouping_for(run, melody)
|
||||
patterns = patterns_for(policy, run[0][0][0], (length, length), grouping)
|
||||
if not patterns:
|
||||
patterns = [p for p in policy['patterns'] if tuple(map(fractions.Fraction, p['lengths'])) == (length,)]
|
||||
patterns = patterns_for(policy, run[0][0][0], (length,), grouping)
|
||||
requested = settings['cycle_patterns']
|
||||
identities = {p['id']: identity(p) for p in patterns}
|
||||
available = len(set(identities.values()))
|
||||
@@ -311,6 +369,7 @@ def choose_cycles(groups, part, melody, seed):
|
||||
raise ValueError(f'source measure {run[0][0][0]["number"]}: no legal recurring {part} groove for {meter}; check harmonic releases and pattern vocabulary')
|
||||
cycle = winner[1]
|
||||
report = {'source_start': run[0][0][2], 'source_end': run[-1][-1][2], 'meter': meter,
|
||||
'grouping': list(grouping), 'grouping_evidence': grouping_evidence, 'eligible_patterns': len(patterns),
|
||||
'requested_patterns': requested, 'cycle_patterns': len(cycle), 'patterns': [o[1]['id'] for o in cycle],
|
||||
'anticipation': [o[2] for o in cycle], 'cost': winner[0][0], 'complete_cycles_compared': examined,
|
||||
'reduction': 'available distinct, legal patterns or source length' if len(cycle) < requested else None}
|
||||
@@ -347,7 +406,7 @@ def add_fills(groups, selected, locations, part, melody, seed):
|
||||
if any(e.start < middle < e.end for e in current.events):
|
||||
continue
|
||||
for variation in policy['patterns']:
|
||||
if variation['family'] != base['family'] or variation['lengths'] != base['lengths']:
|
||||
if variation['family'] != base['family'] or variation['lengths'] != base['lengths'] or variation.get('meter') != base.get('meter') or variation.get('grouping') != base.get('grouping'):
|
||||
continue
|
||||
hybrid = {**base, 'id': base['id'] + '.fill.' + variation['id'], 'events':
|
||||
[r for r in base['events'] if fractions.Fraction(r['onset']) < middle] +
|
||||
|
||||
Reference in New Issue
Block a user