Files
khodowany 7cad6db980
ci / checks (push) Successful in 47m22s
Sizing
2026-09-30 02:49:54 -05:00

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