Comping
This commit is contained in:
@@ -50,7 +50,15 @@ def audit(source, documents):
|
||||
exceptions.append({'measure': m['number'], 'reason': 'source-defined solo pickup allocation'})
|
||||
continue
|
||||
position = section.source_measure_index + j
|
||||
key = (section.source_measure_index + j, repr(harmonic_key(m, carried[offset + j])))
|
||||
# A complete figure depends on both measures' effective harmony and
|
||||
# source-defined participation. Changed final turnarounds/pickups are
|
||||
# explicit contexts, not incidental changes between iterations.
|
||||
first = generator.score.body_start_measure_index(authored)
|
||||
block_start = first + 2 * ((position - first) // 2)
|
||||
neighbors = range(max(section.source_measure_index, block_start), min(section.source_measure_index + section.measure_count, block_start + 2))
|
||||
context = tuple(repr(harmonic_key(measures[offset + k - section.source_measure_index], carried[offset + k - section.source_measure_index])) for k in neighbors)
|
||||
boundary = following.role if following and block_start + 2 >= section.source_measure_index + section.measure_count and generator.score.body_start_measure_index(authored) and following.role in ('slash', 'melody') else ''
|
||||
key = (position, context, boundary)
|
||||
signature = tuple((v['id'], generator.model.fraction(n['onset']), generator.model.fraction(n['duration']),
|
||||
tuple((generator.model.pitch_number(p), generator.model.fraction(p['duration']), p['tablature']['string'], p['tablature']['fret'], bool(p.get('tie_from')) if position not in entry_positions or generator.model.fraction(n['onset']) else False) for p in n['pitches']))
|
||||
for v in m['voices'] for n in v['notes'] if n['pitches'])
|
||||
@@ -60,7 +68,7 @@ def audit(source, documents):
|
||||
reused += 1
|
||||
else:
|
||||
if any(k[0] == key[0] for k in seen):
|
||||
exceptions.append({'measure': m['number'], 'reason': 'changed effective harmony or duration'})
|
||||
exceptions.append({'measure': m['number'], 'reason': 'changed two-bar harmony, duration, or protected participation'})
|
||||
seen[key] = signature
|
||||
offset += section.measure_count
|
||||
result[part] = {'reused_bars': reused, 'distinct_form_bars': len(seen), 'exceptions': exceptions}
|
||||
|
||||
@@ -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:], [])
|
||||
@@ -8,6 +8,7 @@ import generator.accompaniment
|
||||
import generator.arrange.accompaniment
|
||||
import generator.acoustic
|
||||
import generator.acoustic_voicing
|
||||
import generator.arrange.ending
|
||||
import generator.arrange.form
|
||||
import generator.arrange.pickup
|
||||
import generator.electric
|
||||
@@ -61,10 +62,20 @@ def _arrange_document(document: dict, part_type: str, plans=None, *, owned=None,
|
||||
section_measures = section_source(source, section, sections[index + 1] if index + 1 < len(sections) else None)
|
||||
previous = generator.accompaniment.carried_chords(section_measures)
|
||||
segment = []
|
||||
stop = None
|
||||
next_section = sections[index + 1] if index + 1 < len(sections) else None
|
||||
if next_section and generator.score.body_start_measure_index(source):
|
||||
last = section.source_measure_index + section.measure_count - 1
|
||||
if next_section.role == 'slash':
|
||||
stop = (last, generator.model.fraction(section_measures[last]['duration']) - generator.model.fraction(source[0]['duration']))
|
||||
elif next_section.role == 'melody' and generator.arrange.pickup.enters(next_section, source):
|
||||
attacks = [generator.model.fraction(n['onset']) for n in generator.score.measure_notes(source[0]) if n['pitches']]
|
||||
if attacks:
|
||||
stop = (last, generator.model.fraction(section_measures[last]['duration']) - generator.model.fraction(source[0]['duration']) + min(attacks))
|
||||
for si in range(section.source_measure_index, section.source_measure_index + section.measure_count):
|
||||
m = section_measures[si]
|
||||
if role == 'accompaniment' and accompaniment is not None:
|
||||
result = accompaniment(m, previous[si], si)
|
||||
result = accompaniment(m, previous[si], si, sources=section_measures, bounds=(section.source_measure_index, section.source_measure_index + section.measure_count), stop=stop)
|
||||
result['id'] = f'measure_{len(output) + len(segment) + 1}'
|
||||
result['section_label'] = section.label if not segment else ''
|
||||
else:
|
||||
@@ -97,6 +108,10 @@ def _arrange_document(document: dict, part_type: str, plans=None, *, owned=None,
|
||||
octave_shifts.append({**shift, 'id': f'pickup_{section.section_index}_{shift["id"]}', 'start': generator.model.rational(offset + low), 'end': generator.model.rational(offset + high)})
|
||||
output.extend(segment)
|
||||
absolute += segment_length
|
||||
if sections and sections[-1].section_index == FINAL_SECTION_INDEX:
|
||||
generator.arrange.ending.append(source, output, part_type,
|
||||
sounding=part_type != 'vocal' and (owned is None or 'accompaniment' in owned),
|
||||
center=generator.vocabulary.role(part_type + '_accompaniment').get('center', 55))
|
||||
tuplets = generator.arrange.pickup.retain_links(output, tuplets)
|
||||
result = generator.score.make_arranged_document(document, staff, staff_id='staff_arranged_' + part_type, staff_name=part_type.capitalize(), role='vocal' if part_type == 'vocal' else 'guitar', arrangement_part_type=part_type, measures=output, tuplets=tuplets, octave_shifts=octave_shifts)
|
||||
result['metadata'].setdefault('tempo', 124)
|
||||
@@ -104,6 +119,7 @@ def _arrange_document(document: dict, part_type: str, plans=None, *, owned=None,
|
||||
if accompaniment is None and (owned is None or 'accompaniment' in owned):
|
||||
generator.acoustic.prepare(result)
|
||||
generator.acoustic_voicing.place(result)
|
||||
generator.acoustic.validate_comping(result)
|
||||
generator.acoustic.combine(result)
|
||||
elif part_type == 'electric' and (owned is None or 'melody' in owned):
|
||||
generator.melody.harmonize(result)
|
||||
|
||||
Reference in New Issue
Block a user