Comping
This commit is contained in:
+156
-110
@@ -135,91 +135,157 @@ class HarmonyChoice:
|
||||
def key(self):
|
||||
return (self.index, self.start, self.end, semantic_key(self.chord), self.events)
|
||||
|
||||
def split_measures(flat, inputs, part):
|
||||
"""Project a selected sounding figure into notation; barlines create ties only."""
|
||||
output, fragments, original = [], {}, {}
|
||||
cursor = fractions.Fraction()
|
||||
for m, prior, index in inputs:
|
||||
length = generator.model.fraction(m['duration'])
|
||||
result = generator.parts.build_measure(m, f'comp_{part}_{index}', 'rest', part, prior)
|
||||
voices = []
|
||||
for voice in flat['voices']:
|
||||
notes = []
|
||||
for n in voice['notes']:
|
||||
start = generator.model.fraction(n['onset'])
|
||||
end = start + generator.model.fraction(n['duration'])
|
||||
a, b = max(cursor, start), min(cursor + length, end)
|
||||
if a >= b:
|
||||
continue
|
||||
for duration, written in generator.accompaniment.written_durations(b - a):
|
||||
pitches = [{k: v for k, v in p.items() if k not in ('tie_from', 'tie_to')} for p in n['pitches']]
|
||||
event = generator.model.make_event(f'comp_{part}_{index}_{voice["id"]}_{len(notes)}', a - cursor, duration, 'note' if pitches else 'rest', pitches, written)
|
||||
event['voice'] = voice['id']
|
||||
for old, new in zip(n['pitches'], event['pitches']):
|
||||
fragments.setdefault(old['id'], []).append(new)
|
||||
original[old['id']] = old
|
||||
notes.append(event)
|
||||
a += duration
|
||||
voices.append({'id': voice['id'], 'notes': notes})
|
||||
result['voices'] = voices
|
||||
for chord in result['harmonies']:
|
||||
chord.pop('chord_optional', None)
|
||||
output.append(result)
|
||||
cursor += length
|
||||
for pieces in fragments.values():
|
||||
for a, b in zip(pieces, pieces[1:]):
|
||||
a['tie_to'], b['tie_from'] = b['id'], a['id']
|
||||
for identifier, old in original.items():
|
||||
if old.get('tie_to') in fragments:
|
||||
a, b = fragments[identifier][-1], fragments[old['tie_to']][0]
|
||||
a['tie_to'], b['tie_from'] = b['id'], a['id']
|
||||
return tuple(output)
|
||||
|
||||
class Canonical:
|
||||
def __init__(self, document, part, seed):
|
||||
self.part, self.seed = part, seed
|
||||
self.policy = generator.vocabulary.role(part + '_accompaniment')
|
||||
self.cache, self._reports, self._rhythm_reports = {}, {}, {}
|
||||
measures = generator.score.source_staff(document)['measures']
|
||||
self.measures = generator.score.source_staff(document)['measures']
|
||||
self.melody = generator.comping_rhythm.melody_contexts(document)
|
||||
first_index = generator.score.body_start_measure_index(measures)
|
||||
sections = generator.arrange.form.full_arrangement_sections(measures, part)
|
||||
self.closed = any(a.role == b.role == 'accompaniment' and a.source_measure_index + a.measure_count == len(measures)
|
||||
and b.source_measure_index == first_index for a, b in zip(sections, sections[1:]))
|
||||
carried = generator.accompaniment.carried_chords(measures)
|
||||
inputs = []
|
||||
for index in range(first_index, len(measures)):
|
||||
source = copy.deepcopy(measures[index])
|
||||
for chord in source['harmonies']:
|
||||
chord.pop('chord_optional', None)
|
||||
inputs.append((source, carried[index], index))
|
||||
layers = tuple(generator.comping_rhythm.candidates(m, prior, part, self.melody[i], seed + '/' + str(i)) for m, prior, i in inputs)
|
||||
rhythms = generator.comping_rhythm.select(layers, seed)
|
||||
self.rhythms = {i: r for (_, _, i), r in zip(inputs, rhythms)}
|
||||
prepared = [(m, prior, i, self.prepare(m, prior, i, r)) for (m, prior, i), r in zip(inputs, rhythms)]
|
||||
active = tuple(s for _, _, _, spans in prepared for s in spans if s.events)
|
||||
self.first = generator.score.body_start_measure_index(self.measures)
|
||||
sections = generator.arrange.form.full_arrangement_sections(self.measures, part)
|
||||
self.closed = any(a.role == b.role == 'accompaniment' and a.source_measure_index + a.measure_count == len(self.measures)
|
||||
and b.source_measure_index == self.first for a, b in zip(sections, sections[1:]))
|
||||
carried = generator.accompaniment.carried_chords(self.measures)
|
||||
self.groups = []
|
||||
index = self.first
|
||||
while index < len(self.measures):
|
||||
size = 1
|
||||
if index + 1 < len(self.measures):
|
||||
lengths = [generator.model.fraction(m['duration']) for m in self.measures[index:index + 2]]
|
||||
if any(tuple(map(fractions.Fraction, p['lengths'])) == tuple(lengths) for p in generator.vocabulary.accompaniment(part)['patterns']):
|
||||
size = 2
|
||||
self.groups.append(tuple((self.measures[i], carried[i], i) for i in range(index, index + size)))
|
||||
index += size
|
||||
layers = [self.candidates(group) for group in self.groups]
|
||||
selected = generator.comping_rhythm.select(layers, seed)
|
||||
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)
|
||||
path, self.path_cost = self.select(active, close=self.closed)
|
||||
self.base = dict(zip((s.key for s in active), path))
|
||||
self.order = tuple((s.index, s.start, v) for s, v in zip(active, path))
|
||||
self.rhythms = {}
|
||||
previous = path[-1] if self.closed and path else ()
|
||||
for source, prior, index, spans in prepared:
|
||||
selected = tuple(self.base[s.key] for s in spans if s.events)
|
||||
self.cache[self.key(source, prior, index)] = self.render(source, prior, index, spans, selected, previous, False)
|
||||
if selected:
|
||||
previous = selected[-1]
|
||||
if generator.vocabulary.accompaniment(part)['connect_holds']:
|
||||
self.connect_holds(inputs)
|
||||
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)
|
||||
cursor = fractions.Fraction()
|
||||
for m, _, i in group:
|
||||
length = generator.model.fraction(m['duration'])
|
||||
self.rhythms[i] = dataclasses.replace(rhythm, events=tuple(dataclasses.replace(e, start=max(e.start, cursor) - cursor, end=min(e.end, cursor + length) - cursor, harmonic_time=e.harmonic_time - cursor if e.harmonic_time is not None else None) for e in rhythm.events if e.start < cursor + length and e.end > cursor))
|
||||
cursor += length
|
||||
if grips:
|
||||
previous = grips[-1]
|
||||
@property
|
||||
def report(self):
|
||||
return [row for key in sorted(self._reports, key=lambda k: (k[0], repr(k))) for row in self._reports[key]]
|
||||
return [row for key in sorted(self._reports, key=repr) for row in self._reports[key]]
|
||||
@property
|
||||
def rhythm_report(self):
|
||||
return [self._rhythm_reports[key] for key in sorted(self._rhythm_reports, key=lambda k: (k[0], repr(k)))]
|
||||
return [row for key in sorted(self._rhythm_reports, key=repr) for row in self._rhythm_reports[key]]
|
||||
@staticmethod
|
||||
def key(source, prior, index):
|
||||
return (index, tuple((a, b, semantic_key(c) if c else '') for a, b, c in generator.comping_rhythm.spans(source, prior)),
|
||||
source['duration'].__repr__())
|
||||
def prepare(self, source, prior, index, rhythm):
|
||||
def key(inputs, limit=None):
|
||||
return (tuple((i, str(generator.model.fraction(m['duration'])), tuple((str(a), str(b), semantic_key(c) if c else '') for a, b, c in generator.comping_rhythm.spans(m, p))) for m, p, i in inputs), str(limit))
|
||||
def candidates(self, inputs, limit=None):
|
||||
flat = generator.comping_rhythm.flatten(inputs)
|
||||
melody, cursor, lengths = [], fractions.Fraction(), []
|
||||
for m, _, i in inputs:
|
||||
melody.extend((a + cursor, b + cursor) for a, b in self.melody[i])
|
||||
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 prepare(self, inputs, rhythm):
|
||||
flat = generator.comping_rhythm.flatten(inputs)
|
||||
spans = []
|
||||
for start, end, chord in generator.comping_rhythm.spans(source, prior):
|
||||
offsets, cursor = [], fractions.Fraction()
|
||||
for m, _, i in inputs:
|
||||
offsets.append((cursor, i))
|
||||
cursor += generator.model.fraction(m['duration'])
|
||||
for start, end, chord in generator.comping_rhythm.spans(flat, None):
|
||||
if not chord:
|
||||
continue
|
||||
events = tuple(e for e in rhythm.events if start <= e.start < end)
|
||||
offset, index = next((a, i) for a, i in reversed(offsets) if a <= start)
|
||||
events = tuple(e for e in rhythm.events if start <= (e.harmonic_time if e.harmonic_time is not None else e.start) < end)
|
||||
try:
|
||||
options = voicings(semantic_key(chord), self.part)
|
||||
except ValueError as error:
|
||||
raise ValueError(f'source measure {source["number"]}, beat {start + 1}: {error}') from error
|
||||
raise ValueError(f'source measure {inputs[index - inputs[0][2]][0]["number"]}, beat {start - offset + 1}: {error}') from error
|
||||
spans.append(HarmonyChoice(index, start, end, chord, events, dict(rhythm.families), options))
|
||||
self._rhythm_reports[self.key(source, prior, index)] = {
|
||||
'source_index': index, 'families': dict(rhythm.families), 'source_melody': [(str(a), str(b)) for a, b in self.melody[index]],
|
||||
'contributions': dict(rhythm.contributions), 'selection_cost': rhythm.cost, 'exceptions': rhythm.exceptions,
|
||||
'layers': {lane: {'attacks': sum(e.lane == lane for e in rhythm.events),
|
||||
'events': [{'onset': str(e.start), 'duration': str(e.end - e.start)} for e in rhythm.events if e.lane == lane], 'tied_continuations': 0} for lane, _ in rhythm.families},
|
||||
'overlap_beats': str(sum((max(0, min(a.end, b.end) - max(a.start, b.start)) for a in rhythm.events for b in rhythm.events if a.lane == 'bass' and b.lane == 'upper'), fractions.Fraction()))}
|
||||
return tuple(spans)
|
||||
def select(self, spans, *, left=(), right=(), close=False, fixed=None):
|
||||
layers = tuple((fixed[s.key],) if fixed and s.key in fixed else s.options for s in spans)
|
||||
costs = tuple({v: local_cost(self.part, semantic_key(s.chord), v, self.policy.get('center', 55)) for v in layer}
|
||||
for s, layer in zip(spans, layers))
|
||||
def select(self, spans, *, left=(), right=(), close=False):
|
||||
layers = tuple(s.options for s in spans)
|
||||
costs = tuple({v: local_cost(self.part, semantic_key(s.chord), v, self.policy.get('center', 55)) for v in layer} for s, layer in zip(spans, layers))
|
||||
return select_grips(layers, costs, self.part, self.seed, left=left, right=right, close=close)
|
||||
def __call__(self, source, prior, index):
|
||||
key = self.key(source, prior, index)
|
||||
def __call__(self, source, prior, index, *, sources=None, bounds=None, stop=None):
|
||||
group = next((g for g in self.groups if any(i == index for _, _, i in g)), ((source, prior, index),))
|
||||
sources = sources or self.measures
|
||||
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)
|
||||
limit = None
|
||||
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)
|
||||
if key not in self.cache:
|
||||
# Effective pickup/turnaround harmony may differ from the main form.
|
||||
# Solve that measure between fixed canonical neighbors. Request order
|
||||
# and previously generated arrangement sections cannot change it.
|
||||
choices = generator.comping_rhythm.candidates(source, prior, self.part, self.melody[index], self.seed + '/' + str(index))
|
||||
rhythm = generator.comping_rhythm.select((choices,), self.seed, self.rhythms.get(index - 1), self.rhythms.get(index + 1))[0]
|
||||
spans = self.prepare(source, prior, index, rhythm)
|
||||
before = [v for i, _, v in self.order if i < index]
|
||||
after = [v for i, _, v in self.order if i > index]
|
||||
choices = self.candidates(group, limit)
|
||||
start, end = group[0][2], group[-1][2]
|
||||
# Preserve the canonical whole figure where still legal under changed
|
||||
# effective harmony. Recompute only genuinely different source context.
|
||||
original = self.block_rhythms.get(start)
|
||||
matches = [r for r in choices if original and r.pattern_id == original.pattern_id]
|
||||
rhythm = matches[0] if matches else generator.comping_rhythm.select((choices,), self.seed, self.block_rhythms.get(start - 2), self.block_rhythms.get(end + 1))[0]
|
||||
spans = self.prepare(group, rhythm)
|
||||
before = [v for i, _, v in self.order if i < start]
|
||||
after = [v for i, _, v in self.order if i > end]
|
||||
left = before[-1] if before else self.order[-1][2] if self.closed and self.order else ()
|
||||
right = after[0] if after else self.order[0][2] if self.closed and self.order else ()
|
||||
selected, _ = self.select(tuple(s for s in spans if s.events), left=left, right=right, fixed=self.base)
|
||||
self.cache[key] = self.render(source, prior, index, spans, selected, left, True)
|
||||
return copy.deepcopy(self.cache[key])
|
||||
def render(self, source, prior, index, spans, selected, previous, variant):
|
||||
length = generator.model.fraction(source['duration'])
|
||||
selected, _ = self.select(tuple(s for s in spans if s.events), left=left, right=right)
|
||||
self.cache[key] = self.render(group, rhythm, spans, selected, left, True, limit)
|
||||
return copy.deepcopy(self.cache[key][index - group[0][2]])
|
||||
def render(self, inputs, rhythm, spans, selected, previous, variant, limit=None):
|
||||
flat = generator.comping_rhythm.flatten(inputs)
|
||||
length = generator.model.fraction(flat['duration'])
|
||||
index = inputs[0][2]
|
||||
attacks, rows = [], []
|
||||
choices = iter(selected)
|
||||
for span in spans:
|
||||
@@ -227,8 +293,8 @@ class Canonical:
|
||||
bass = grip[0] if grip and self.part == 'acoustic' else None
|
||||
degrees = generator.harmony.degrees(span.chord)
|
||||
root = int(generator.model.NATURAL[span.chord['root']['letter']] + generator.model.alteration(span.chord['root']['alter']))
|
||||
rows.append({'source_index': index, 'onset': str(span.start), 'chord': generator.harmony.symbol(span.chord),
|
||||
'figure': span.figure, 'tones': len(grip), 'bass': bool(bass), 'variant': variant,
|
||||
rows.append({'source_index': span.index, 'onset': str(span.start), 'chord': generator.harmony.symbol(span.chord),
|
||||
'figure': span.figure, 'pattern_id': rhythm.pattern_id, 'tones': len(grip), 'bass': bool(bass), 'variant': variant,
|
||||
'family': (generator.chord_families.acoustic(span.chord, grip) if self.part == 'acoustic' else generator.chord_families.electric(grip)) if grip else None,
|
||||
'family_coverage': generator.chord_families.coverage(span.chord, span.options, self.part),
|
||||
'fret_span': generator.fretboard.fret_span(grip),
|
||||
@@ -239,66 +305,46 @@ class Canonical:
|
||||
'bass_string': bass.string_number if bass else None,
|
||||
'movement': movement(self.part, previous, grip), 'voicing_candidates': len(span.options)})
|
||||
for event in span.events:
|
||||
positions = (grip[0],) if event.lane == 'bass' else grip[1:] if event.lane == 'upper' else grip
|
||||
attacks.append((event.lane, event.start, event.end, positions, span.chord))
|
||||
lanes = ['bass', 'upper'] if event.lane == 'combined' else [event.lane]
|
||||
for lane in lanes:
|
||||
positions = (grip[0],) if lane == 'bass' else grip[1:] if lane == 'upper' else grip
|
||||
attacks.append((lane, event.start, event.end, positions, span.chord))
|
||||
if grip:
|
||||
previous = grip
|
||||
self._reports[self.key(source, prior, index)] = rows
|
||||
key = self.key(inputs, limit)
|
||||
self._reports[key] = rows
|
||||
voices = []
|
||||
notes, cursor, lane = [], fractions.Fraction(), ''
|
||||
def emit(a, b, positions=(), chord=None):
|
||||
nonlocal cursor
|
||||
previous = None
|
||||
for duration, written in generator.accompaniment.written_durations(b - a):
|
||||
pitches = generator.fretboard.chord_pitches(chord, positions) if positions else []
|
||||
n = generator.model.make_event(f'comp_{self.part}_{index}_{lane}_{len(notes)}', a, duration, 'note' if pitches else 'rest', pitches, written)
|
||||
n['voice'] = lane
|
||||
if self.part == 'acoustic':
|
||||
n['stem'] = 'down' if lane == 'bass' else 'up'
|
||||
if previous and pitches:
|
||||
for old, new in zip(previous['pitches'], n['pitches']):
|
||||
old['tie_to'], new['tie_from'] = new['id'], old['id']
|
||||
notes.append(n)
|
||||
previous = n
|
||||
a += duration
|
||||
cursor = b
|
||||
for lane in ['upper', 'bass'] if self.part == 'acoustic' else ['electric']:
|
||||
notes, cursor = [], fractions.Fraction()
|
||||
def emit(a, b, positions=(), chord=None):
|
||||
previous = None
|
||||
for duration, written in generator.accompaniment.written_durations(b - a):
|
||||
pitches = generator.fretboard.chord_pitches(chord, positions) if positions else []
|
||||
n = generator.model.make_event(f'comp_{self.part}_{index}_{lane}_{len(notes)}', a, duration, 'note' if pitches else 'rest', pitches, written)
|
||||
n['voice'] = lane
|
||||
if previous and pitches:
|
||||
for old, new in zip(previous['pitches'], n['pitches']):
|
||||
old['tie_to'], new['tie_from'] = new['id'], old['id']
|
||||
notes.append(n)
|
||||
previous = n
|
||||
a += duration
|
||||
for _, start, end, positions, chord in sorted((e for e in attacks if e[0] == lane), key=lambda e: e[1]):
|
||||
if cursor < start:
|
||||
emit(cursor, start)
|
||||
emit(start, end, positions, chord)
|
||||
cursor = end
|
||||
if cursor < length:
|
||||
emit(cursor, length)
|
||||
voices.append({'id': lane, 'notes': notes})
|
||||
result = generator.parts.build_measure(source, 'canonical', 'rest', self.part, prior)
|
||||
result['voices'] = voices
|
||||
result = split_measures({'voices': voices}, inputs, self.part)
|
||||
reports, cursor = [], fractions.Fraction()
|
||||
for m, _, i in inputs:
|
||||
duration = generator.model.fraction(m['duration'])
|
||||
reports.append({'source_index': i, 'pattern_id': rhythm.pattern_id, 'pattern_start': index,
|
||||
'families': dict(rhythm.families), 'contributions': dict(rhythm.contributions), 'selection_cost': rhythm.cost,
|
||||
'exceptions': rhythm.exceptions, 'source_melody': [(str(a), str(b)) for a, b in self.melody[i]],
|
||||
'events': [{'layer': e.lane, 'onset': str(max(e.start, cursor) - cursor), 'duration': str(min(e.end, cursor + duration) - max(e.start, cursor)),
|
||||
'continuation': e.start < cursor, 'harmonic_target': str(e.harmonic_time - cursor) if e.harmonic_time is not None else None} for e in rhythm.events if e.start < cursor + duration and e.end > cursor]})
|
||||
cursor += duration
|
||||
self._rhythm_reports[key] = reports
|
||||
return result
|
||||
def connect_holds(self, inputs):
|
||||
"""Only explicit sustained neighbors can share an attack across a barline.
|
||||
|
||||
These are canonical internal joins, never links across role boundaries.
|
||||
The direct reattack remains when a grip or required attack count differs.
|
||||
"""
|
||||
for left, right in zip(inputs, inputs[1:]):
|
||||
before = self.cache[self.key(*left)]
|
||||
after = self.cache[self.key(*right)]
|
||||
previous_chord = generator.accompaniment.active_chord(left[0], generator.model.fraction(left[0]['duration']), left[1])
|
||||
next_chord = generator.accompaniment.active_chord(right[0], fractions.Fraction(), right[1])
|
||||
if not generator.accompaniment.same_chord(previous_chord, next_chord):
|
||||
continue
|
||||
for a, b in zip(before['voices'], after['voices']):
|
||||
if a['id'] == 'bass':
|
||||
continue
|
||||
old, new = a['notes'][-1], b['notes'][0]
|
||||
minimum = 1 if a['id'] == 'upper' else 0
|
||||
attacks = sum(bool(n['pitches']) and not any('tie_from' in p for p in n['pitches']) for n in b['notes'])
|
||||
if not old['pitches'] or not new['pitches'] or attacks <= minimum or min(generator.model.fraction(n['duration']) for n in (old, new)) < 1:
|
||||
continue
|
||||
if [p['tablature'] for p in old['pitches']] != [p['tablature'] for p in new['pitches']]:
|
||||
continue
|
||||
for p, q in zip(old['pitches'], new['pitches']):
|
||||
p['tie_to'], q['tie_from'] = q['id'], p['id']
|
||||
row = self._rhythm_reports[self.key(*right)]['layers'][a['id']]
|
||||
row['attacks'] -= 1
|
||||
row['tied_continuations'] += 1
|
||||
|
||||
Reference in New Issue
Block a user