"""Pure page coverage and performance timing.""" from __future__ import annotations import fractions import math import generator.model TEMPOS = tuple(range(60, 121, 10)) def boundaries(document): result = [fractions.Fraction()] for measure in document['staves'][0]['measures']: result.append(result[-1] + generator.model.fraction(measure['duration'])) return result def page_timing(document, page): times = boundaries(document) start, end = page.systems[0].start, page.systems[-1].end first, last = times.index(start), times.index(end) measures = document['staves'][0]['measures'] systems = [] for system in page.systems: a, b = times.index(system.start), times.index(system.end) labels = {p.sources[0]: p for p in system.primitives if p.role == 'measure_number'} boxes = [] for i in range(a, b): box = labels[measures[i]['id']].box boxes.append(dict(measure=i + 1, bounds=[box.left, box.top, box.right, box.bottom])) systems.append(dict(first_measure=a + 1, measure_count=b - a, start_beat=generator.model.rational(system.start), end_beat=generator.model.rational(system.end), labels=boxes)) return dict(first_measure=first + 1, measure_count=last - first, start_beat=generator.model.rational(start), end_beat=generator.model.rational(end), systems=systems) def validate_views(document, pages, playback_pages): validate_pages(document, pages) times = boundaries(document) canonical = [] def system_range(system): a, count = system['first_measure'], system['measure_count'] if type(a) is not int or type(count) is not int or a < 1 or count < 1 or a + count > len(times): raise ValueError('invalid system measure coverage') if generator.model.fraction(system['start_beat']) != times[a - 1] or generator.model.fraction(system['end_beat']) != times[a + count - 1]: raise ValueError('invalid system timing') if [label['measure'] for label in system['labels']] != list(range(a, a + count)): raise ValueError('invalid measure label coverage') for label in system['labels']: box = label['bounds'] if not isinstance(box, list) or len(box) != 4 or any(type(v) not in (int, float) or not math.isfinite(v) for v in box) or not (4 <= box[0] < box[2] <= 972 and 4 <= box[1] < box[3] <= 480): raise ValueError('invalid measure label bounds') return a, count for page in pages: systems = page.get('systems', []) if len(systems) != 2 and (page is not pages[-1] or len(systems) != 1): raise ValueError('invalid page systems') cursor = page['first_measure'] for system in systems: a, count = system_range(system) if a != cursor: raise ValueError('system gap or overlap') cursor += count canonical.append(system) if cursor != page['first_measure'] + page['measure_count']: raise ValueError('systems disagree with page coverage') if not isinstance(playback_pages, list) or len(playback_pages) != len(canonical): raise ValueError('missing playback pages; regenerate score assets') for i, page in enumerate(playback_pages): expected = canonical[i:i + 2] systems = page.get('systems', []) if len(systems) != len(expected): raise ValueError('invalid playback systems') for actual, source in zip(systems, expected): system_range(actual) if any(actual[k] != source[k] for k in ('first_measure', 'measure_count', 'start_beat', 'end_beat')): raise ValueError('playback changes system boundaries') if any(page[k] != expected[0][k] for k in ('first_measure', 'start_beat')) or page['end_beat'] != expected[-1]['end_beat'] or page['measure_count'] != sum(s['measure_count'] for s in expected): raise ValueError('invalid playback page timing') def validate_pages(document, pages): times = boundaries(document) cursor = 0 for page in pages: if not isinstance(page, dict): raise ValueError('invalid page descriptor') count = page.get('measure_count') if type(count) is not int or count <= 0 or page.get('first_measure') != cursor + 1 or cursor + count >= len(times): raise ValueError('invalid page measure coverage') if generator.model.fraction(page.get('start_beat')) != times[cursor] or generator.model.fraction(page.get('end_beat')) != times[cursor + count]: raise ValueError('page timing does not match its measures') cursor += count if cursor != len(times) - 1: raise ValueError('incomplete page measure coverage') def timing(document): times = boundaries(document) measures = document['staves'][0]['measures'] ending = [i for i, m in enumerate(measures) if m.get('ending_sustain')] if ending and ending != list(range(ending[0], len(measures))): raise ValueError('held ending must be a contiguous terminal section') sustain_start = times[ending[0]] if ending else times[-1] # The written held ending participates in page time, not metronome time. clicks = [] for start, measure in zip(times, measures): if start >= sustain_start: break duration = generator.model.fraction(measure['duration']) beat = fractions.Fraction() while beat < duration: clicks.append(dict(beat=generator.model.rational(start + beat), downbeat=beat == 0)) beat += 1 return dict(song_id=document['metadata'].get('song_id', ''), duration=generator.model.rational(times[-1]), sustain_start=generator.model.rational(sustain_start), count_in_beats=4, clicks=clicks)