@@ -0,0 +1,103 @@
|
||||
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
|
||||
Reference in New Issue
Block a user