93 lines
4.3 KiB
Python
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
|