49 lines
1.8 KiB
Python
49 lines
1.8 KiB
Python
from __future__ import annotations
|
|
|
|
import dataclasses
|
|
import fractions
|
|
|
|
import generator.model
|
|
import generator.score
|
|
|
|
def solo_source(staff):
|
|
"""Source-form pickup context, shared by training and arranged choruses.
|
|
|
|
The arrangement repeats the source form before each solo. Only an unlabelled
|
|
pickup inherits its preceding source-form harmony. Scores without a pickup
|
|
retain their ordinary initial-harmony checks. Explicit pickup harmony
|
|
(including N.C.) still takes precedence in read().
|
|
"""
|
|
measures = staff['measures']
|
|
initial = None
|
|
if generator.score.body_start_measure_index(measures):
|
|
initial = next((h for m in reversed(measures) for h in reversed(m['harmonies'])), None)
|
|
return read(staff, initial=initial)
|
|
|
|
@dataclasses.dataclass(frozen=True)
|
|
class ChordSpan:
|
|
start: fractions.Fraction
|
|
end: fractions.Fraction
|
|
measure: int
|
|
source_measure: str
|
|
beat: fractions.Fraction
|
|
key: dict
|
|
chord: dict | None
|
|
|
|
def read(staff: dict, *, initial: dict | None = None) -> tuple[ChordSpan, ...]:
|
|
result = []
|
|
absolute = fractions.Fraction()
|
|
active = initial
|
|
for number, measure in enumerate(staff['measures'], 1):
|
|
length = generator.model.fraction(measure['duration'])
|
|
harmonies = {generator.model.fraction(h['onset']): h for h in measure['harmonies']}
|
|
changes = {generator.model.fraction(a['onset']): a['key'] for a in measure.get('attributes', []) if 'key' in a}
|
|
boundaries = sorted({fractions.Fraction(), length} | set(harmonies) | set(changes))
|
|
key = measure['key']
|
|
for start, end in zip(boundaries, boundaries[1:]):
|
|
key = changes.get(start, key)
|
|
active = harmonies.get(start, active)
|
|
result.append(ChordSpan(absolute + start, absolute + end, number, str(measure['number']), start + 1, key, active if active and active['kind'] != 'none' else None))
|
|
absolute += length
|
|
return tuple(result)
|