"""One learned accompaniment per instrument, reused at authored form positions.""" from __future__ import annotations import copy import fractions import generator.accompaniment import generator.acoustic import generator.acoustic_voicing import generator.electric import generator.guitar import generator.fretboard import generator.model import generator.parts import generator.score import generator.arrange.form import generator.arrange.pickup def harmonic_key(measure, previous): def key(chord): return repr({k: chord.get(k) for k in ('root', 'kind', 'degrees', 'bass', 'inversion')}) if chord else '' active = generator.accompaniment.active_chord(measure, fractions.Fraction(), previous) return (key(active), tuple((generator.model.fraction(h['onset']), key(h)) for h in measure['harmonies'] if generator.model.fraction(h['onset']) > 0), measure['duration'], measure['time_signature']) def audit(source, documents): """Compare music, not generated IDs, at every reused form position.""" authored = generator.score.source_staff(source)['measures'] result = {} for part, document in documents.items(): if part == 'vocal': continue sections = generator.arrange.form.full_arrangement_sections(authored, part) entry_positions = {s.source_measure_index for s in sections if s.role == 'accompaniment'} measures = document['staves'][0]['measures'] carried = generator.accompaniment.carried_chords(measures) seen, exceptions, offset, reused = {}, [], 0, 0 for si, section in enumerate(sections): following = sections[si + 1] if si + 1 < len(sections) else None for j in range(section.measure_count): if section.role != 'accompaniment': continue m = measures[offset + j] if j == section.measure_count - 1 and following and following.role == 'melody' and generator.arrange.pickup.enters(following, authored): exceptions.append({'measure': m['number'], 'reason': 'protected melody pickup'}) continue if j == section.measure_count - 1 and following and following.role == 'slash' and generator.score.body_start_measure_index(authored): exceptions.append({'measure': m['number'], 'reason': 'source-defined solo pickup allocation'}) continue position = section.source_measure_index + j # A complete figure depends on both measures' effective harmony and # source-defined participation. Changed final turnarounds/pickups are # explicit contexts, not incidental changes between iterations. first = generator.score.body_start_measure_index(authored) block_start = first + 2 * ((position - first) // 2) neighbors = range(max(section.source_measure_index, block_start), min(section.source_measure_index + section.measure_count, block_start + 2)) context = tuple(repr(harmonic_key(measures[offset + k - section.source_measure_index], carried[offset + k - section.source_measure_index])) for k in neighbors) boundary = following.role if following and block_start + 2 >= section.source_measure_index + section.measure_count and generator.score.body_start_measure_index(authored) and following.role in ('slash', 'melody') else '' key = (position, context, boundary) signature = tuple((v['id'], generator.model.fraction(n['onset']), generator.model.fraction(n['duration']), tuple((generator.model.pitch_number(p), generator.model.fraction(p['duration']), p['tablature']['string'], p['tablature']['fret'], bool(p.get('tie_from')) if position not in entry_positions or generator.model.fraction(n['onset']) else False) for p in n['pitches'])) for v in m['voices'] for n in v['notes'] if n['pitches']) if key in seen: if signature != seen[key]: raise ValueError(f'{part}, arrangement measure {m["number"]}: canonical accompaniment changed') reused += 1 else: if any(k[0] == key[0] for k in seen): exceptions.append({'measure': m['number'], 'reason': 'changed two-bar harmony, duration, or protected participation'}) seen[key] = signature offset += section.measure_count result[part] = {'reused_bars': reused, 'distinct_form_bars': len(seen), 'exceptions': exceptions} return result