from __future__ import annotations import dataclasses import fractions import generator.model @dataclasses.dataclass class SoundingTone: pitch: dict start: fractions.Fraction end: fractions.Fraction def written_durations(length: fractions.Fraction): while length: written = generator.model.written_for(length) duration = length # Binary spans such as five eighths need ties, not an invented tuplet. if 'ratio' in written and length.denominator & (length.denominator - 1) == 0: duration = next((size for size in generator.model.TYPE_LENGTHS.values() if size < length), length) written = generator.model.written_for(duration) yield duration, written length -= duration def combine(document: dict) -> None: """Consolidate generated acoustic lanes without changing their sounding notes.""" staff = document['staves'][0] if staff.get('arrangement_part_type') != 'acoustic': return measures, sounds, roots = [], {}, {} absolute = fractions.Fraction() for measure in staff['measures']: length = generator.model.fraction(measure['duration']) if {voice['id'] for voice in measure['voices']} == {'bass', 'upper'}: members = set() for voice in measure['voices']: for note in voice['notes']: start = absolute + generator.model.fraction(note['onset']) for pitch in note['pitches']: end = start + generator.model.fraction(pitch['duration']) if source := pitch.get('tie_from'): root = roots[source] sounds[root].end = end else: root = pitch['id'] sounds[root] = SoundingTone(pitch, start, end) roots[pitch['id']] = root members.add(root) measures.append((measure, absolute, members)) absolute += length previous = {} for measure, absolute, members in measures: length = generator.model.fraction(measure['duration']) spans = [(root, max(fractions.Fraction(), sounds[root].start - absolute), min(length, sounds[root].end - absolute)) for root in sorted(members)] boundaries = sorted({fractions.Fraction(), length} | {time for _, start, end in spans for time in (start, end)}) notes = [] for start, end in zip(boundaries, boundaries[1:]): active = sorted((root for root, low, high in spans if low <= start and end <= high), key=lambda root: (generator.model.pitch_number(sounds[root].pitch), sounds[root].pitch['tablature']['string'], root)) for duration, written in written_durations(end - start): pitches = [{key: value for key, value in sounds[root].pitch.items() if key not in ('tie_from', 'tie_to', 'stem')} for root in active] note = generator.model.make_event(f'{measure["id"]}-acoustic-{len(notes) + 1}', start, duration, 'note' if active else 'rest', pitches, written) note['voice'] = 'voice-arranged' for root, pitch in zip(active, note['pitches']): if root in previous: previous[root]['tie_to'], pitch['tie_from'] = pitch['id'], previous[root]['id'] previous[root] = pitch notes.append(note) start += duration if not members and length == generator.model.meter_length(measure['meter']): notes = [generator.model.make_event(f'{measure["id"]}-acoustic-1', fractions.Fraction(), length, 'rest', written={'type': 'whole', 'dots': 0}) | {'voice': 'voice-arranged', 'measure_rest': True}] measure['voices'] = [{'id': 'voice-arranged', 'notes': notes}]