from __future__ import annotations import copy import fractions import itertools import generator.accompaniment import generator.model import generator.score import generator.solo.types def durations(length): if length.denominator & (length.denominator - 1) == 0: yield from generator.accompaniment.written_durations(length) else: kind, base = next((kind, value) for kind, value in reversed(generator.model.TYPE_LENGTHS.items()) if value >= length) yield length, {'type': kind, 'dots': 0, 'ratio': generator.model.rational(length / base)} def measures(sources: list[dict], notes: tuple[generator.solo.types.Note, ...], prefix: str) -> list[dict]: """Write attack spans as conventional durations; only continuations get ties.""" result, previous = [], {} absolute = fractions.Fraction() for index, source in enumerate(sources): length = generator.model.fraction(source['duration']) events, cursor = [], absolute def emit(start, end, tone=None, identity=None): for duration, written in durations(end - start): identifier = f'{prefix}_{index + 1}_{len(events) + 1}' pitches = [tone.pitch.written() | {'tablature': {'string': tone.position.string_number, 'fret': tone.position.fret}}] if tone else [] event = generator.model.make_event(identifier, start - absolute, duration, 'note' if tone else 'rest', pitches, written) if tone: pitch = event['pitches'][0] if identity in previous: prior = previous[identity] prior['tie_to'] = pitch['id'] pitch['tie_from'] = prior['id'] previous[identity] = pitch events.append(event) start += duration for identity, note in enumerate(notes): start, end = max(absolute, note.start), min(absolute + length, note.end) if start >= end: continue if start > cursor: emit(cursor, start) emit(start, end, note, identity) cursor = end if cursor < absolute + length: emit(cursor, absolute + length) measure = generator.score.make_measure(source['id'], source, events, section_label=source.get('section_label', '')) measure['transpose'] = {'diatonic': 0, 'chromatic': 0, 'octave_change': -1, 'explicit': True} measure['clef'] = {'sign': 'G', 'line': 2, 'octave_change': 0} for change in measure.get('attributes', []): change.pop('transpose', None) change.pop('clef', None) result.append(measure) absolute += length return result def tuplets(measures): # Group by sounding duration, including notation fragments across barlines. spans = [] events = [n for m in measures for n in generator.score.measure_notes(m)] for ratio, values in itertools.groupby(events, key=lambda n: generator.model.fraction(n['written'].get('ratio', generator.model.rational(1)))): if ratio == 1: continue members = [] elapsed = fractions.Fraction() length = None for note in values: if not members: length = generator.model.TYPE_LENGTHS[note['written']['type']] * ratio.numerator members.append(note) elapsed += generator.model.fraction(note['duration']) if elapsed >= length: identifier = members[0]['id'] + '_tuplet' spans.append({'id': identifier, 'voice': 'voice_arranged', 'start': members[0]['id'], 'end': members[-1]['id'], 'note_count': ratio.denominator, 'space_count': ratio.numerator, 'depth': 0, 'bracket': 'auto', 'show_number': 'actual'}) for n in members: n['tuplet_ids'] = [identifier] members = [] elapsed = fractions.Fraction() if members: identifier = members[0]['id'] + '_tuplet' spans.append({'id': identifier, 'voice': 'voice_arranged', 'start': members[0]['id'], 'end': members[-1]['id'], 'note_count': ratio.denominator, 'space_count': ratio.numerator, 'depth': 0, 'bracket': 'auto', 'show_number': 'actual'}) for n in members: n['tuplet_ids'] = [identifier] return spans def replace_region(document: dict, first: int, replacement: list[dict]) -> None: staff = document['staves'][0] replacement = copy.deepcopy(replacement) spans = tuplets(replacement) staff['measures'][first:first + len(replacement)] = replacement staff['tablature'] = True existing = {n['id'] for m in staff['measures'] for n in generator.score.measure_notes(m)} staff['tuplets'] = [t for t in staff.get('tuplets', []) if t['start'] in existing and t['end'] in existing] + spans