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