"""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:], [])