77 lines
3.6 KiB
Python
77 lines
3.6 KiB
Python
from __future__ import annotations
|
|
|
|
import fractions
|
|
|
|
import generator.model
|
|
|
|
def pulse(measure: dict, onset: fractions.Fraction) -> tuple[fractions.Fraction | None, bool]:
|
|
start, meter = fractions.Fraction(), measure['meter']
|
|
for change in measure.get('attributes', []):
|
|
if generator.model.fraction(change['onset']) <= onset and 'meter' in change:
|
|
start, meter = generator.model.fraction(change['onset']), change['meter']
|
|
beats = generator.model.beat_lengths(meter)
|
|
if not beats:
|
|
return None, False
|
|
# Pickups occupy the final beats of their meter.
|
|
phase = onset - start
|
|
if measure.get('implicit') and not start:
|
|
phase += max(fractions.Fraction(), sum(beats) - generator.model.fraction(measure['duration']))
|
|
phase %= sum(beats)
|
|
for beat in beats:
|
|
if phase < beat:
|
|
return beat, phase == 0
|
|
phase -= beat
|
|
raise ValueError('invalid meter pulse')
|
|
|
|
def syllable(duration: fractions.Fraction, beat: fractions.Fraction | None, onbeat: bool, final: bool, previous: str) -> str:
|
|
if duration >= (beat if beat is not None else 1):
|
|
return 'doo'
|
|
if final:
|
|
return 'dat'
|
|
result = 'dit' if beat is not None and not onbeat else 'ba'
|
|
return 'da' if result == previous else result
|
|
|
|
def add_scat(document: dict) -> None:
|
|
events, pitches = [], {}
|
|
absolute = fractions.Fraction()
|
|
boundaries = set()
|
|
for measure in document['staves'][0]['measures']:
|
|
if measure.get('section_label'):
|
|
boundaries.add(absolute)
|
|
for voice in measure['voices']:
|
|
for note in voice['notes']:
|
|
for field in ('lyrics', 'lyric', 'lyric_notation'):
|
|
note.pop(field, None)
|
|
onset = absolute + generator.model.fraction(note['onset'])
|
|
for tone in note['pitches']:
|
|
pitches[tone['id']] = (note, tone, onset)
|
|
events.append((note, measure, onset))
|
|
absolute += generator.model.fraction(measure['duration'])
|
|
attacks = [(n, m, t) for n, m, t in events if n['pitches'] and not n.get('cue') and not n['pitches'][0].get('cue') and not n['pitches'][0].get('tie_from')]
|
|
previous, previous_end = '', None
|
|
for index, (note, measure, onset) in enumerate(attacks):
|
|
tone = note['pitches'][0]
|
|
end = onset + generator.model.fraction(tone['duration'])
|
|
chain = []
|
|
while target := tone.get('tie_to'):
|
|
continuation, tone, time = pitches[target]
|
|
chain.append(continuation)
|
|
end = time + generator.model.fraction(tone['duration'])
|
|
following = attacks[index + 1][2] if index + 1 < len(attacks) else None
|
|
final = following is None or following > end or any(onset < b <= following for b in boundaries)
|
|
if previous_end is None or onset > previous_end or onset in boundaries:
|
|
previous = ''
|
|
beat, onbeat = pulse(measure, generator.model.fraction(note['onset']))
|
|
text = syllable(end - onset, beat, onbeat, final, previous)
|
|
values = [{'kind': 'syllabic', 'text': 'single', 'attributes': {}}, {'kind': 'text', 'text': text, 'attributes': {}}]
|
|
verse = {'number': '1', 'syllables': values}
|
|
if chain:
|
|
values.append({'kind': 'extend', 'text': '', 'attributes': {'type': 'start'}})
|
|
verse['extend_to'] = generator.model.rational(end)
|
|
for position, continuation in enumerate(chain):
|
|
continuation['lyrics'] = [{'number': '1', 'syllables': [{'kind': 'extend', 'text': '', 'attributes': {'type': 'stop' if position == len(chain) - 1 else 'continue'}}], 'extend_to': generator.model.rational(end)}]
|
|
note['lyrics'] = [verse]
|
|
note['lyric'] = text
|
|
note['lyric_notation'] = {'text': text, 'syllabic': 'single', 'extend': 'start' if chain else ''}
|
|
previous, previous_end = text, end
|