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