66 lines
2.7 KiB
Python
66 lines
2.7 KiB
Python
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
|