Files
khodowany 09656a9d60
ci / checks (push) Successful in 48m34s
Package
2026-09-30 19:46:37 -05:00

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