104 lines
5.3 KiB
Python
104 lines
5.3 KiB
Python
from __future__ import annotations
|
|
|
|
import copy
|
|
import fractions
|
|
|
|
import generator.accompaniment
|
|
import generator.model
|
|
import generator.score
|
|
|
|
def enters(section, source: list[dict]) -> bool:
|
|
return section is not None and section.section_index in (1, 2, 5) and section.source_measure_index == 1 and generator.score.leading_pickup_measure_index(source) == 0
|
|
|
|
def offset(destination: dict, pickup: dict) -> fractions.Fraction:
|
|
pad = generator.model.fraction(destination['duration']) - generator.model.fraction(pickup['duration'])
|
|
if pad < 0:
|
|
raise ValueError(f'measure {destination["number"]}: pickup is longer than the preceding arrangement measure')
|
|
return pad
|
|
|
|
def harmonies(destination: dict, pickup: dict) -> None:
|
|
pad = offset(destination, pickup)
|
|
if pickup['harmonies']:
|
|
first = pad + generator.model.fraction(pickup['harmonies'][0]['onset'])
|
|
destination['harmonies'] = [h for h in destination['harmonies'] if generator.model.fraction(h['onset']) < first]
|
|
destination['harmonies'].extend({**copy.deepcopy(h), 'onset': generator.model.rational(pad + generator.model.fraction(h['onset']))} for h in pickup['harmonies'])
|
|
|
|
def truncate(note: dict, end: fractions.Fraction) -> None:
|
|
duration = end - generator.model.fraction(note['onset'])
|
|
if generator.model.fraction(note['duration']) <= duration:
|
|
return
|
|
note.update(duration=generator.model.rational(duration), written=generator.model.written_for(duration))
|
|
for field in ('measure_rest', 'beams'):
|
|
note.pop(field, None)
|
|
for pitch in note['pitches']:
|
|
if generator.model.fraction(pitch['duration']) > duration:
|
|
pitch.update(duration=note['duration'], written=copy.deepcopy(note['written']))
|
|
pitch.pop('tie_to', None)
|
|
|
|
def splice(destination: dict, pickup: dict) -> fractions.Fraction:
|
|
"""Keep outgoing music until the pickup's first sounding event, then hand over."""
|
|
pad = offset(destination, pickup)
|
|
sounding = [generator.model.fraction(n['onset']) for n in generator.score.measure_notes(pickup) if n['pitches']]
|
|
if not sounding:
|
|
return generator.model.fraction(destination['duration'])
|
|
start = pad + min(sounding)
|
|
voices = {v['id']: {'id': v['id'], 'notes': []} for v in pickup['voices']}
|
|
primary = pickup['voices'][0]['id']
|
|
for voice in destination['voices']:
|
|
identifier = primary if len(destination['voices']) == 1 and voice['id'] not in voices else voice['id']
|
|
notes = voices.setdefault(identifier, {'id': identifier, 'notes': []})['notes']
|
|
for note in voice['notes']:
|
|
if generator.model.fraction(note['onset']) < start:
|
|
truncate(note, start)
|
|
note['voice'] = identifier
|
|
notes.append(note)
|
|
for voice in pickup['voices']:
|
|
notes = voices[voice['id']]['notes']
|
|
for note in voice['notes']:
|
|
onset = pad + generator.model.fraction(note['onset'])
|
|
end = onset + generator.model.fraction(note['duration'])
|
|
if onset < start:
|
|
if end <= start:
|
|
continue
|
|
note.update(duration=generator.model.rational(end - start), written=generator.model.written_for(end - start))
|
|
note.pop('measure_rest', None)
|
|
onset = start
|
|
note['onset'] = generator.model.rational(onset)
|
|
notes.append(note)
|
|
generator.accompaniment.normalize_beams(notes)
|
|
destination['voices'] = [voice for voice in voices.values() if voice['notes']]
|
|
state = {k: copy.deepcopy(pickup[k]) for k in ('key', 'clef', 'meter', 'transpose', 'staff_details')}
|
|
for change in pickup.get('attributes', []):
|
|
if pad + generator.model.fraction(change['onset']) <= start:
|
|
state.update({k: copy.deepcopy(v) for k, v in change.items() if k != 'onset'})
|
|
destination['attributes'] = [a for a in destination.get('attributes', []) if generator.model.fraction(a['onset']) < start]
|
|
previous = dict(destination)
|
|
for change in destination['attributes']:
|
|
previous.update(change)
|
|
state = {k: v for k, v in state.items() if previous.get(k) != v}
|
|
if start and state:
|
|
destination['attributes'].append({'onset': generator.model.rational(start), **state})
|
|
elif not start:
|
|
destination.update(state)
|
|
destination['key_signature'] = generator.model.key_label(destination['key'])
|
|
destination['time_signature'] = generator.model.meter_label(destination['meter'])
|
|
destination['attributes'].extend({**copy.deepcopy(a), 'onset': generator.model.rational(pad + generator.model.fraction(a['onset']))} for a in pickup.get('attributes', []) if pad + generator.model.fraction(a['onset']) > start)
|
|
return start
|
|
|
|
def retain_links(measures: list[dict], tuplets: list[dict]) -> list[dict]:
|
|
notes = [n for m in measures for n in generator.score.measure_notes(m)]
|
|
tones = {p['id']: p for n in notes for p in n['pitches']}
|
|
for pitch in tones.values():
|
|
for field, reverse in (('tie_to', 'tie_from'), ('tie_from', 'tie_to')):
|
|
if field in pitch and tones.get(pitch[field], {}).get(reverse) != pitch['id']:
|
|
pitch.pop(field)
|
|
members = {}
|
|
for note in notes:
|
|
for identifier in note.get('tuplet_ids', []):
|
|
members.setdefault(identifier, []).append(note['id'])
|
|
result = []
|
|
for span in tuplets:
|
|
if events := members.get(span['id']):
|
|
result.append({**span, 'start': events[0], 'end': events[-1], 'continued_start': span.get('continued_start', False) or span['start'] != events[0], 'continued_end': span.get('continued_end', False) or span['end'] != events[-1]})
|
|
return result
|