from __future__ import annotations import dataclasses import fractions import generator.model @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)