This commit is contained in:
khodowany
2026-10-06 21:28:42 -05:00
parent e15dc31568
commit c7f6a30a13
30 changed files with 2957 additions and 490 deletions
+80
View File
@@ -0,0 +1,80 @@
"""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:], [])