@@ -0,0 +1,76 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from fractions import Fraction
|
||||
|
||||
from generator.model import beat_lengths, fraction, rational
|
||||
|
||||
def pulse(measure: dict, onset: Fraction) -> tuple[Fraction | None, bool]:
|
||||
start, meter = Fraction(), measure['meter']
|
||||
for change in measure.get('attributes', []):
|
||||
if fraction(change['onset']) <= onset and 'meter' in change:
|
||||
start, meter = fraction(change['onset']), change['meter']
|
||||
beats = 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(Fraction(), sum(beats) - 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: Fraction, beat: 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 = 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 + fraction(note['onset'])
|
||||
for tone in note['pitches']:
|
||||
pitches[tone['id']] = (note, tone, onset)
|
||||
events.append((note, measure, onset))
|
||||
absolute += 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 + fraction(tone['duration'])
|
||||
chain = []
|
||||
while target := tone.get('tie_to'):
|
||||
continuation, tone, time = pitches[target]
|
||||
chain.append(continuation)
|
||||
end = time + 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, 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'] = 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': 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
|
||||
Reference in New Issue
Block a user