from __future__ import annotations import bisect import copy import dataclasses import fractions import generator.accompaniment import generator.guitar import generator.model @dataclasses.dataclass(frozen=True) class MeasureOrigin: onset: fractions.Fraction role: str measure: dict start: fractions.Fraction = fractions.Fraction() end: fractions.Fraction | None = None @dataclasses.dataclass(frozen=True) class VoicingLink: target: dict references: tuple[dict, ...] def link_voicings(source: list[dict], origins: list[MeasureOrigin]) -> list[VoicingLink]: """Link accompaniment to melody attacks in the same authored harmony span.""" boundaries, previous, start = [], None, fractions.Fraction() for measure in source: for harmony in measure['harmonies']: if not generator.accompaniment.same_chord(previous, harmony): boundaries.append(start + generator.model.fraction(harmony['onset'])) previous = harmony start += generator.model.fraction(measure['duration']) melody, targets = {}, [] for origin in origins: for voice in origin.measure['voices']: for note in voice['notes']: local = generator.model.fraction(note['onset']) if '_harmony' not in note or local < origin.start or (origin.end is not None and local >= origin.end): continue onset = origin.onset + local span = bisect.bisect_right(boundaries, onset) if origin.role == 'melody' and note.get('_harmonize'): melody.setdefault(span, []).append((onset, note['_harmony'], note)) elif origin.role == 'accompaniment': targets.append((span, onset, note)) links = [] for span, onset, note in targets: matches = [(time, reference) for time, harmony, reference in melody.get(span, []) if generator.accompaniment.same_chord(harmony, note['_harmony'])] # Before the first melody chord, use the first grip in this harmony span. matches.sort(key=lambda item: (item[0] > onset, abs(item[0] - onset), item[1]['id'])) if matches: links.append(VoicingLink(note, tuple(reference for _, reference in matches))) return links def reuse_voicings(links: list[VoicingLink]) -> None: """Copy placed melody grips without changing either part's rhythm or ties.""" for link in links: for reference in link.references: strings = tuple(sorted((p['tablature']['string'] for p in reference['pitches']), reverse=True)) if strings not in generator.guitar.ELECTRIC_STRINGS: continue note = link.target note['pitches'] = [copy.deepcopy({**{k: p[k] for k in ('letter', 'alter', 'octave', 'tablature')}, 'id': f'{note["id"]}_s{p["tablature"]["string"]}', 'duration': note['duration'], 'written': note['written']}) for p in reference['pitches']] break