Cleanupf
ci / checks (push) Successful in 1h15m45s

This commit is contained in:
khodowany
2026-09-23 23:54:19 -05:00
parent a0faa9b565
commit da5640291a
24 changed files with 366 additions and 233 deletions
+38 -6
View File
@@ -26,6 +26,10 @@ class ScoreIndex:
self.staff = document['staves'][0]
self.measures = []
self.events = []
self.events_by_onset = {}
self.attributes_by_onset = {}
self.harmonies_by_onset = {}
self.tempos_by_onset = {}
self.by_id = {}
self.tones = {}
self.boundaries = {Fraction()}
@@ -48,18 +52,46 @@ class ScoreIndex:
for tone in note['pitches']:
self.tones[tone['id']] = (event, tone)
for field in ('attributes', 'harmonies', 'tempos'):
self.columns.update(time + fraction(item['onset']) for item in measure.get(field, []))
bucket = {'attributes': self.attributes_by_onset, 'harmonies': self.harmonies_by_onset, 'tempos': self.tempos_by_onset}[field]
for index, item in enumerate(measure.get(field, [])):
onset = time + fraction(item['onset'])
self.columns.add(onset)
bucket.setdefault(onset, []).append((measure, index, item))
time = end
self.end = time
self.measure_starts = [item[0] for item in self.measures]
self.events.sort(key=lambda e: (e.start, not bool(e.value.get('grace')), e.lane))
self.event_starts = []
self.event_max_ends = []
for event in self.events:
self.events_by_onset.setdefault(event.start, []).append(event)
self.event_starts.append(event.start)
self.event_max_ends.append(max(event.end, self.event_max_ends[-1] if self.event_max_ends else event.end))
self.onsets = list(self.events_by_onset)
self.column_times = sorted(self.columns)
self.boundary_times = sorted(self.boundaries)
self.lyric_lanes = list(dict.fromkeys((e.voice, verse['number']) for e in self.events for verse in e.value.get('lyrics', [])))
self.accidentals = self.accidental_state()
def measure_at(self, time: Fraction, before: bool = False) -> tuple:
position = bisect_left(self.measure_starts, time) if before else bisect_right(self.measure_starts, time)
return self.measures[max(0, position - 1)]
def events_at(self, time: Fraction, include_sustained: bool = False) -> list[Event]:
attacks = self.events_by_onset.get(time, [])
if not include_sustained:
return attacks
sustained = []
position = bisect_left(self.event_starts, time) - 1
# Prefix maxima let system continuations stop searching once every earlier note has ended.
while position >= 0 and self.event_max_ends[position] > time:
event = self.events[position]
if event.end > time:
sustained.append(event)
position -= 1
return list(reversed(sustained)) + attacks
def state(self, time: Fraction, before: bool = False) -> dict:
if (time, before) in self._states:
return self._states[time, before]
position = bisect_left(self.measure_starts, time) if before else bisect_right(self.measure_starts, time)
start, _, measure = self.measures[max(0, position - 1)]
start, _, measure = self.measure_at(time, before)
state = {key: measure[key] for key in ('key', 'meter', 'clef', 'transpose', 'staff_details')}
for change in measure.get('attributes', []):
onset = start + fraction(change['onset'])
@@ -82,13 +114,13 @@ class ScoreIndex:
for start, end, measure in self.measures:
memory = {}
key = None
onsets = sorted({e.start for e in self.events if start <= e.start < end})
onsets = self.onsets[bisect_left(self.onsets, start):bisect_left(self.onsets, end)]
for onset in onsets:
state = self.state(onset)
if key != state['key']:
key, memory = state['key'], {}
simultaneous = {}
for event in (e for e in self.events if e.start == onset):
for event in self.events_by_onset[onset]:
for tone in event.value['pitches']:
hidden_head = isinstance(tone.get('notehead'), dict) and tone['notehead'].get('name') == 'none'
if event.value.get('hidden') or tone.get('print', {}).get('print-object') == 'no' or hidden_head:
@@ -108,7 +140,7 @@ class ScoreIndex:
for identity, values in simultaneous.items():
memory[identity] = next(iter(values)) if len(values) == 1 else None
if any(len(values) > 1 for values in simultaneous.values()):
for event in (e for e in self.events if e.start == onset):
for event in self.events_by_onset[onset]:
for tone in event.value['pitches']:
if 'letter' in tone and len(simultaneous.get((tone['letter'], tone['octave']), ())) > 1:
result[tone['id']] = True