@@ -1,12 +1,13 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import replace
|
||||
from bisect import bisect_left, bisect_right
|
||||
from fractions import Fraction
|
||||
import math
|
||||
|
||||
from generator import harmony
|
||||
from generator.engraving import tablature
|
||||
from generator.model import fraction, validate_document
|
||||
from generator.model import validate_document
|
||||
from generator.engraving.attributes import barline, staff_attributes, tempo
|
||||
from generator.engraving.fonts import Fonts
|
||||
from generator.engraving.notation import ScoreIndex
|
||||
@@ -19,7 +20,7 @@ def lyric_text(verse: dict) -> str:
|
||||
|
||||
def column(index: ScoreIndex, time: Fraction, start: Fraction, end: Fraction, fonts: Fonts, style: Style) -> tuple[list[Primitive], list[Anchor], list[Fragment]]:
|
||||
items, anchors, fragments = [], [], []
|
||||
events = [e for e in index.events if e.start == time or (time == start and e.start < start < e.end)]
|
||||
events = index.events_at(time, include_sustained=time == start)
|
||||
grace_width = 0
|
||||
occupied = []
|
||||
for event in events:
|
||||
@@ -47,48 +48,45 @@ def column(index: ScoreIndex, time: Fraction, start: Fraction, end: Fraction, fo
|
||||
text = lyric_text(verse)
|
||||
if text:
|
||||
items.append(fonts.text(text, x + style.space / 2, lane * 23, 18, 'lyric', (event.value['id'] + ':lyric:' + verse['number'], event.value['id']), True))
|
||||
for measure_start, measure_end, measure in index.measures:
|
||||
if not measure_start <= time < measure_end:
|
||||
continue
|
||||
for item in measure['harmonies']:
|
||||
if measure_start + fraction(item['onset']) == time:
|
||||
label = harmony.symbol(item)
|
||||
items.append(fonts.text(label, 0, 0, 21, 'harmony', (item['id'],)))
|
||||
for i, value in enumerate(measure.get('tempos', [])):
|
||||
if measure_start + fraction(value['onset']) == time:
|
||||
items.extend(tempo(value, fonts, 0, 0, style.space, measure['id'] + f':tempo:{i}'))
|
||||
if time == measure_start:
|
||||
items.append(fonts.text(str(measure['number']), 0, -style.space * (index.state(time)['staff_details']['lines'] + 0.5), 12, 'measure-number', (measure['id'],)))
|
||||
if label := measure.get('section_label'):
|
||||
items.append(fonts.text(label, 0, 0, 19, 'section', (measure['id'] + ':section',)))
|
||||
measure_start, _, measure = index.measure_at(time)
|
||||
for owner, _, item in index.harmonies_by_onset.get(time, []):
|
||||
if owner is measure:
|
||||
items.append(fonts.text(harmony.symbol(item), 0, 0, 21, 'harmony', (item['id'],)))
|
||||
for owner, i, value in index.tempos_by_onset.get(time, []):
|
||||
if owner is measure:
|
||||
items.extend(tempo(value, fonts, 0, 0, style.space, measure['id'] + f':tempo:{i}'))
|
||||
if time == measure_start:
|
||||
items.append(fonts.text(str(measure['number']), 0, -style.space * (index.state(time)['staff_details']['lines'] + 0.5), 12, 'measure-number', (measure['id'],)))
|
||||
if label := measure.get('section_label'):
|
||||
items.append(fonts.text(label, 0, 0, 19, 'section', (measure['id'] + ':section',)))
|
||||
return items, anchors, fragments
|
||||
|
||||
def make_system(index: ScoreIndex, start: Fraction, end: Fraction, fonts: Fonts, style: Style, justify: bool = False) -> System:
|
||||
items, anchors, fragments = [], [], []
|
||||
state = index.state(start)
|
||||
measure = next(m for a, b, m in index.measures if a <= start < b)
|
||||
measure = index.measure_at(start)[2]
|
||||
header = staff_attributes(index, state, start, fonts, style.space, measure['id'] + ':initial', ('clef', 'key', 'meter', 'staff_details'))
|
||||
items.extend(header)
|
||||
cursor = max((p.box.right for p in header), default=0) + 22
|
||||
content_left = cursor - 9
|
||||
times = sorted({start} | {t for t in index.columns if start < t < end})
|
||||
times = [start] + index.column_times[bisect_right(index.column_times, start):bisect_left(index.column_times, end)]
|
||||
placements = []
|
||||
groups = []
|
||||
for ti, time in enumerate(times):
|
||||
item_start, anchor_start = len(items), len(anchors)
|
||||
drawn, placed, pieces = column(index, time, start, end, fonts, style)
|
||||
leading = []
|
||||
for measure_start, measure_end, m in index.measures:
|
||||
if time == measure_start and time > start:
|
||||
previous = next(previous for a, b, previous in index.measures if b == time)
|
||||
leading.extend(barline(previous.get('barlines', {}).get('right', 'regular'), 0, -style.space * (index.state(time)['staff_details']['lines'] - 1), previous['id']))
|
||||
old_state = index.state(time, before=True)
|
||||
fields = tuple(k for k in ('clef', 'key', 'meter', 'staff_details') if old_state[k] != index.state(time)[k])
|
||||
leading.extend(p.moved(12, 0) for p in staff_attributes(index, index.state(time), time, fonts, style.space, m['id'] + ':initial', fields))
|
||||
for ci, change in enumerate(m.get('attributes', [])):
|
||||
if measure_start + fraction(change['onset']) == time and time > start:
|
||||
extra = staff_attributes(index, index.state(time), time, fonts, style.space, m['id'] + f':attributes:{ci}', tuple(change))
|
||||
leading.extend(p.moved(max((p.box.right for p in leading), default=0) + 10, 0) for p in extra)
|
||||
measure_start, _, m = index.measure_at(time)
|
||||
if time == measure_start and time > start:
|
||||
previous = index.measure_at(time, before=True)[2]
|
||||
leading.extend(barline(previous.get('barlines', {}).get('right', 'regular'), 0, -style.space * (index.state(time)['staff_details']['lines'] - 1), previous['id']))
|
||||
old_state = index.state(time, before=True)
|
||||
fields = tuple(k for k in ('clef', 'key', 'meter', 'staff_details') if old_state[k] != index.state(time)[k])
|
||||
leading.extend(p.moved(12, 0) for p in staff_attributes(index, index.state(time), time, fonts, style.space, m['id'] + ':initial', fields))
|
||||
if time > start:
|
||||
for owner, ci, change in index.attributes_by_onset.get(time, []):
|
||||
extra = staff_attributes(index, index.state(time), time, fonts, style.space, owner['id'] + f':attributes:{ci}', tuple(change))
|
||||
leading.extend(p.moved(max((p.box.right for p in leading), default=0) + 10, 0) for p in extra)
|
||||
if leading:
|
||||
width = bounds(leading).right - bounds(leading).left + 14
|
||||
items.extend(p.moved(cursor - bounds(leading).left, 0) for p in leading)
|
||||
@@ -113,7 +111,7 @@ def make_system(index: ScoreIndex, start: Fraction, end: Fraction, fonts: Fonts,
|
||||
placements = [(time, x + i * stretch) for i, (time, x) in enumerate(placements)]
|
||||
width = available
|
||||
right = width - 5
|
||||
last_measure = next(m for a, b, m in index.measures if a < end <= b)
|
||||
last_measure = index.measure_at(end, before=True)[2]
|
||||
tab_items = [p for p in items if p.role.startswith('tab')]
|
||||
items = [p for p in items if not p.role.startswith('tab')]
|
||||
items = draw_beams(index, anchors, items, start, end, content_left, right, style)
|
||||
@@ -200,7 +198,7 @@ def layout_score(document: dict, style: Style, fonts: Fonts) -> LayoutDocument:
|
||||
start = Fraction()
|
||||
available = style.width - style.margin_x * 2
|
||||
while start < index.end:
|
||||
candidates = sorted(b for b in index.boundaries if b > start)
|
||||
candidates = index.boundary_times[bisect_right(index.boundary_times, start):]
|
||||
selected = None
|
||||
# Reserve the same title/credit and footer bands when considering a split,
|
||||
# so a wide pitch range can break vertically as well as horizontally.
|
||||
@@ -212,7 +210,7 @@ def layout_score(document: dict, style: Style, fonts: Fonts) -> LayoutDocument:
|
||||
selected = trial
|
||||
if selected is None:
|
||||
measure_end = candidates[0]
|
||||
for end in sorted(t for t in index.columns if start < t < measure_end):
|
||||
for end in index.column_times[bisect_right(index.column_times, start):bisect_left(index.column_times, measure_end)]:
|
||||
trial = make_system(index, start, end, fonts, style)
|
||||
if trial.box.right - trial.box.left <= available and trial.box.bottom - trial.box.top <= height_limit:
|
||||
selected = trial
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user