Files
product-vamp/generator/scat.py
T
khodowany cf7c0c010c
ci / checks (push) Successful in 1h19m14s
Tab Generation
2026-09-20 16:20:43 -05:00

77 lines
3.4 KiB
Python

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