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

112 lines
4.8 KiB
Python

from __future__ import annotations
import bisect
import copy
import fractions
import generator.accompaniment
import generator.model
def harmonize(document: dict) -> None:
"""Split long melody runs at harmony changes before choosing guitar positions."""
staff = document['staves'][0]
tones, events, changes = {}, [], []
absolute = fractions.Fraction()
previous = None
for measure in staff['measures']:
for chord in measure['harmonies']:
value = chord if chord['kind'] != 'none' else None
if value != previous and not generator.accompaniment.same_chord(value, previous):
time = absolute + generator.model.fraction(chord['onset'])
if changes and changes[-1][0] == time:
changes[-1] = (time, value)
else:
changes.append((time, value))
previous = value
for voice in measure['voices']:
for note in voice['notes']:
start = absolute + generator.model.fraction(note['onset'])
events.append((note, start))
for tone in note['pitches']:
tones[tone['id']] = tone
absolute += generator.model.fraction(measure['duration'])
eligible = set()
visited = set()
for note, _ in events:
if not note.get('_melody') or note.get('grace') or note.get('cue') or len(note['pitches']) != 1 or note['pitches'][0].get('cue'):
continue
tone = note['pitches'][0]
if tone['id'] in visited:
continue
while tone.get('tie_from'):
tone = tones[tone['tie_from']]
chain = []
while True:
chain.append(tone)
if not tone.get('tie_to'):
break
tone = tones[tone['tie_to']]
visited.update(p['id'] for p in chain)
if sum((generator.model.fraction(p['duration']) for p in chain), fractions.Fraction()) > 1:
eligible.update(p['id'] for p in chain)
times = [time for time, _ in changes]
reattacks = {time for time, chord in changes if chord}
replacements, tone_pieces = {}, {}
for note, start in events:
melodic = note.pop('_melody', False)
if not melodic or not note['pitches'] or note['pitches'][0]['id'] not in eligible:
continue
end = start + generator.model.fraction(note['duration'])
cuts = [start, *times[bisect.bisect_right(times, start):bisect.bisect_left(times, end)], end]
pieces = []
for index, (a, b) in enumerate(zip(cuts, cuts[1:])):
fragment = copy.deepcopy(note)
fragment['id'] = note['id'] if index == 0 else f'{note["id"]}_h{index}'
fragment['onset'] = generator.model.rational(generator.model.fraction(note['onset']) + a - start)
fragment['duration'] = generator.model.rational(b - a)
if len(cuts) > 2:
fragment['written'] = generator.model.written_for(b - a)
fragment.pop('beams', None)
for tone in fragment['pitches']:
original = tone['id']
tone['id'] = original if index == 0 else f'{original}_h{index}'
tone['duration'], tone['written'] = fragment['duration'], copy.deepcopy(fragment['written'])
tone.pop('tie_from', None)
tone.pop('tie_to', None)
tone_pieces.setdefault(original, []).append((tone, a))
if index:
fragment.pop('lyrics', None)
current = bisect.bisect_right(times, a) - 1
chord = changes[current][1] if current >= 0 else None
if chord:
fragment.update(_harmony=chord, _guitar_profile='electric', _harmonize=True)
pieces.append(fragment)
replacements[note['id']] = pieces
for identifier, pieces in tone_pieces.items():
original = tones[identifier]
links = list(zip(pieces, pieces[1:]))
if source := original.get('tie_from'):
previous_piece = tone_pieces[source][-1] if source in tone_pieces else (tones[source], None)
links.insert(0, (previous_piece, pieces[0]))
previous_piece[0].pop('tie_to', None)
if target := original.get('tie_to'):
if target not in tone_pieces:
pieces[-1][0]['tie_to'] = target
tones[target]['tie_from'] = pieces[-1][0]['id']
for (first, _), (second, start) in links:
if start not in reattacks:
first['tie_to'], second['tie_from'] = second['id'], first['id']
by_tone = {p['id']: note for original, _ in events for note in replacements.get(original['id'], [original]) for p in note['pitches']}
for measure in staff['measures']:
for voice in measure['voices']:
voice['notes'] = [piece for note in voice['notes'] for piece in replacements.get(note['id'], [note])]
for note in voice['notes']:
if note.get('_harmonize') and (source := note['pitches'][0].get('tie_from')):
earlier = by_tone[source]
if generator.accompaniment.same_chord(earlier.get('_harmony'), note['_harmony']):
note['_rhythm_continuation'] = True
generator.accompaniment.normalize_beams(voice['notes'])
for tuplet in staff.get('tuplets', []):
if tuplet['end'] in replacements:
tuplet['end'] = replacements[tuplet['end']][-1]['id']