112 lines
4.8 KiB
Python
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']
|