Files
product-vamp/generator/solo/notation.py
T
2026-10-04 09:05:25 -05:00

93 lines
4.3 KiB
Python

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