81 lines
3.8 KiB
Python
81 lines
3.8 KiB
Python
"""The recorded three-bar hold, expressed once as source-derived score time."""
|
|
import copy
|
|
import fractions
|
|
|
|
import generator.accompaniment
|
|
import generator.acoustic_voicing
|
|
import generator.comping
|
|
import generator.fretboard
|
|
import generator.model
|
|
import generator.parts
|
|
import generator.score
|
|
|
|
BARS = 3
|
|
|
|
def template(source):
|
|
result = copy.deepcopy(source[-1])
|
|
# An ending is three full bars in the final active meter, never a pickup.
|
|
for change in result.get('attributes', []):
|
|
for key in ('meter', 'key', 'clef', 'transpose', 'staff_details'):
|
|
if key in change:
|
|
result[key] = copy.deepcopy(change[key])
|
|
result.update(duration=generator.model.rational(generator.model.meter_length(result['meter'])), implicit=False,
|
|
attributes=[], harmonies=[], voices=[], tempos=[], section_label='')
|
|
result['time_signature'] = generator.model.meter_label(result['meter'])
|
|
return result
|
|
|
|
def duration(source):
|
|
return BARS * generator.model.fraction(template(source)['duration']) if source else fractions.Fraction()
|
|
|
|
def chord(source):
|
|
effective = [{**m, 'harmonies': [h for h in m['harmonies'] if not h.get('chord_optional')]} for m in source]
|
|
prior = generator.accompaniment.carried_chords(effective)[-1]
|
|
return generator.accompaniment.active_chord(effective[-1], generator.model.fraction(effective[-1]['duration']), prior)
|
|
|
|
def append(source, output, part, *, sounding=False, center=55):
|
|
if not source or not output:
|
|
return
|
|
model = template(source)
|
|
harmony = chord(source)
|
|
grip = ()
|
|
if sounding:
|
|
if harmony is None:
|
|
raise ValueError('Ending: final performed harmony is missing or N.C.; cannot invent a closing chord')
|
|
options = generator.comping.voicings(generator.comping.semantic_key(harmony), part)
|
|
recent = {}
|
|
for m in reversed(output):
|
|
for n in sorted(generator.score.measure_notes(m), key=lambda n: generator.model.fraction(n['onset']), reverse=True):
|
|
for p in n['pitches']:
|
|
if 'tablature' in p:
|
|
tab = p['tablature']
|
|
recent.setdefault(tab['string'], generator.fretboard.TabPosition(tab['string'], tab['fret'], int(generator.model.pitch_number(p)) - 12))
|
|
if len(recent) >= 4:
|
|
break
|
|
previous = tuple(recent[s] for s in sorted(recent, reverse=True))
|
|
if part == 'acoustic':
|
|
bass = next((recent[s] for s in (5, 6) if s in recent), None)
|
|
previous = generator.acoustic_voicing.Voicing(bass, *(recent[s] for s in (4, 3, 2))) if bass and all(s in recent for s in (4, 3, 2)) else ()
|
|
grip = min(options, key=lambda v: (generator.comping.local_cost(part, generator.comping.semantic_key(harmony), v, center).moved(generator.comping.movement(part, previous, v)), v))
|
|
length = generator.model.fraction(model['duration'])
|
|
previous = None
|
|
for index in range(BARS):
|
|
measure = generator.parts.build_measure(model, f'measure_{len(output) + 1}', 'rest', part, None, 'Ending' if index == 0 else '')
|
|
measure['ending_sustain'] = True
|
|
if index == 0 and harmony:
|
|
measure['harmonies'] = [{**copy.deepcopy(harmony), 'onset': generator.model.rational(0)}]
|
|
measure['harmonies'][0].pop('chord_optional', None)
|
|
if grip:
|
|
notes, cursor = [], fractions.Fraction()
|
|
for duration, written in generator.accompaniment.written_durations(length):
|
|
n = generator.model.make_event(f'ending_{part}_{index}_{len(notes)}', cursor, duration, 'note', generator.fretboard.chord_pitches(harmony, grip), written)
|
|
n['voice'] = 'voice_arranged'
|
|
if previous:
|
|
for a, b in zip(previous['pitches'], n['pitches']):
|
|
a['tie_to'], b['tie_from'] = b['id'], a['id']
|
|
notes.append(n)
|
|
previous = n
|
|
cursor += duration
|
|
measure['voices'] = [{'id': 'voice_arranged', 'notes': notes}]
|
|
output.append(measure)
|
|
generator.score.reidentify(output[-BARS:], [])
|