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