Generation
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
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']
|
||||
Reference in New Issue
Block a user