Files
product-vamp/generator/solo/notation.py
T
khodowany eae432aa83
ci / checks (push) Successful in 50m39s
Soloing
2026-10-01 06:31:14 -05:00

85 lines
3.7 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):
spans = []
for measure in measures:
for ratio, values in itertools.groupby(generator.score.measure_notes(measure), key=lambda n: generator.model.fraction(n['written'].get('ratio', generator.model.rational(1)))):
notes = list(values)
if ratio == 1:
continue
for first in range(0, len(notes), ratio.denominator):
members = notes[first:first + ratio.denominator]
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 note in members:
note['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