Sizing
ci / checks (push) Successful in 47m22s

This commit is contained in:
khodowany
2026-09-30 02:49:54 -05:00
parent 923b7f14e2
commit 7cad6db980
17 changed files with 1443 additions and 1298 deletions
+23 -36
View File
@@ -7,6 +7,7 @@ import pathlib
import generator.accompaniment
import generator.acoustic
import generator.arrange.form
import generator.arrange.pickup
import generator.electric
import generator.guitar
import generator.melody
@@ -23,21 +24,25 @@ def arrange_document(document: dict, part_type: str, *, rhythms: generator.rhyth
plans = plan_arrangement(document, rhythms) if part_type != 'vocal' else {}
return _arrange_document(document, part_type, plans)
def section_source(source: list[dict], final: bool) -> list[dict]:
def section_source(source: list[dict], section, next_section=None) -> list[dict]:
result = copy.deepcopy(source)
for measure in result:
measure['harmonies'] = [h for h in measure['harmonies'] if not final or not h.get('chord_optional')]
measure['harmonies'] = [h for h in measure['harmonies'] if section.section_index != FINAL_SECTION_INDEX or not h.get('chord_optional')]
for chord in measure['harmonies']:
chord.pop('chord_optional', None)
if generator.arrange.pickup.enters(next_section, source):
destination = result[section.source_measure_index + section.measure_count - 1]
generator.arrange.pickup.harmonies(destination, result[0])
return result
def plan_arrangement(document: dict, rhythms: generator.rhythm.Library) -> dict[int, generator.accompaniment.Plan]:
source = generator.score.source_staff(document)['measures']
result = {}
for section in generator.arrange.form.full_arrangement_sections(source, 'acoustic'):
sections = generator.arrange.form.full_arrangement_sections(source, 'acoustic')
for index, section in enumerate(sections):
if section.role == 'slash':
continue
measures = section_source(source, section.section_index == FINAL_SECTION_INDEX)
measures = section_source(source, section, sections[index + 1] if index + 1 < len(sections) else None)
previous = generator.accompaniment.carried_chords(measures)
start = section.source_measure_index
result[section.section_index] = generator.accompaniment.plan_section(measures[start:start + section.measure_count], rhythms, initial=previous[start], electric=section.section_index in (1, 5, 6))
@@ -56,8 +61,8 @@ def _arrange_document(document: dict, part_type: str, plans: dict[int, generator
for m in source:
source_starts.append(source_time)
source_time += generator.model.fraction(m['duration'])
for section in sections:
section_measures = section_source(source, section.section_index == FINAL_SECTION_INDEX)
for index, section in enumerate(sections):
section_measures = section_source(source, section, sections[index + 1] if index + 1 < len(sections) else None)
previous = generator.accompaniment.carried_chords(section_measures)
segment = []
for si in range(section.source_measure_index, section.source_measure_index + section.measure_count):
@@ -70,7 +75,6 @@ def _arrange_document(document: dict, part_type: str, plans: dict[int, generator
segment.append(result)
if part_type == 'electric' and section.role in ('melody', 'accompaniment'):
voicing_origins.append(generator.electric.MeasureOrigin(source_starts[si], section.role, result))
identity_segment = segment
segment_length = sum((generator.model.fraction(m['duration']) for m in segment), fractions.Fraction())
if section.role in ('vocal', 'melody'):
start = source_starts[section.source_measure_index]
@@ -78,42 +82,25 @@ def _arrange_document(document: dict, part_type: str, plans: dict[int, generator
low, high = max(start, generator.model.fraction(shift['start'])), min(start + segment_length, generator.model.fraction(shift['end']))
if low < high:
octave_shifts.append({**shift, 'id': f'section-{section.section_index}-{shift["id"]}', 'start': generator.model.rational(absolute + low - start), 'end': generator.model.rational(absolute + high - start)})
if part_type == 'vocal' and section.section_index in (1, 5) and section.source_measure_index == 1 and source[0].get('implicit') and output:
pickup = generator.parts.build_measure(source[0], source[0]['id'], 'vocal', part_type, None)
pickup = None
if section.role in ('vocal', 'melody') and generator.arrange.pickup.enters(section, source) and output:
pickup = generator.parts.build_measure(source[0], f'pickup-{section.section_index}', section.role, part_type, None)
tuplets.extend(generator.score.reidentify(([pickup] if pickup else []) + segment, staff.get('tuplets', [])))
if pickup:
destination = output[-1]
pad = generator.model.fraction(destination['duration']) - generator.model.fraction(pickup['duration'])
if pad < 0:
raise ValueError('pickup is longer than the preceding arrangement measure')
voices = generator.score.copy_source_voices(pickup, strip_lyrics=False)
for voice in voices:
for n in voice['notes']:
n['onset'] = generator.model.rational(generator.model.fraction(n['onset']) + pad)
if pad:
voice['notes'].insert(0, generator.model.make_event(f'pickup-rest-{voice["id"]}', fractions.Fraction(), pad, 'rest') | {'voice': voice['id']})
destination['voices'] = voices
identity_segment = [destination, *segment]
if pickup['harmonies']:
destination['harmonies'] = [h for h in destination['harmonies'] if generator.model.fraction(h['onset']) < pad]
for h in pickup['harmonies']:
h['onset'] = generator.model.rational(generator.model.fraction(h['onset']) + pad)
destination['harmonies'].append(h)
state = {k: copy.deepcopy(pickup[k]) for k in ('key', 'clef', 'meter', 'transpose')}
if pad:
destination['attributes'].append({'onset': generator.model.rational(pad), **state})
else:
destination.update(state)
destination['key_signature'] = generator.model.key_label(state['key'])
destination['time_signature'] = generator.model.meter_label(state['meter'])
for change in pickup.get('attributes', []):
destination['attributes'].append({**change, 'onset': generator.model.rational(generator.model.fraction(change['onset']) + pad)})
pad = generator.arrange.pickup.offset(destination, pickup)
onset = generator.arrange.pickup.splice(destination, pickup)
if part_type == 'electric':
voicing_origins = [generator.electric.MeasureOrigin(o.onset, o.role, o.measure, o.start, onset if o.measure is destination else o.end) for o in voicing_origins]
voicing_origins.append(generator.electric.MeasureOrigin(-pad, 'melody', destination, onset))
for shift in staff.get('octave_shifts', []):
low, high = generator.model.fraction(shift['start']), min(generator.model.fraction(pickup['duration']), generator.model.fraction(shift['end']))
low, high = max(onset - pad, generator.model.fraction(shift['start'])), min(generator.model.fraction(pickup['duration']), generator.model.fraction(shift['end']))
if low < high:
offset = absolute - generator.model.fraction(pickup['duration'])
octave_shifts.append({**shift, 'id': f'pickup-{section.section_index}-{shift["id"]}', 'start': generator.model.rational(offset + low), 'end': generator.model.rational(offset + high)})
tuplets.extend(generator.score.reidentify(identity_segment, staff.get('tuplets', [])))
output.extend(segment)
absolute += segment_length
tuplets = generator.arrange.pickup.retain_links(output, tuplets)
result = generator.score.make_arranged_document(document, staff, staff_id='staff-arranged-' + part_type, staff_name=part_type.capitalize(), role='vocal' if part_type == 'vocal' else 'guitar', arrangement_part_type=part_type, measures=output, tuplets=tuplets, octave_shifts=octave_shifts)
result['metadata'].setdefault('tempo', 124)
if part_type != 'vocal':
+103
View File
@@ -0,0 +1,103 @@
from __future__ import annotations
import copy
import fractions
import generator.accompaniment
import generator.model
import generator.score
def enters(section, source: list[dict]) -> bool:
return section is not None and section.section_index in (1, 2, 5) and section.source_measure_index == 1 and generator.score.leading_pickup_measure_index(source) == 0
def offset(destination: dict, pickup: dict) -> fractions.Fraction:
pad = generator.model.fraction(destination['duration']) - generator.model.fraction(pickup['duration'])
if pad < 0:
raise ValueError(f'measure {destination["number"]}: pickup is longer than the preceding arrangement measure')
return pad
def harmonies(destination: dict, pickup: dict) -> None:
pad = offset(destination, pickup)
if pickup['harmonies']:
first = pad + generator.model.fraction(pickup['harmonies'][0]['onset'])
destination['harmonies'] = [h for h in destination['harmonies'] if generator.model.fraction(h['onset']) < first]
destination['harmonies'].extend({**copy.deepcopy(h), 'onset': generator.model.rational(pad + generator.model.fraction(h['onset']))} for h in pickup['harmonies'])
def truncate(note: dict, end: fractions.Fraction) -> None:
duration = end - generator.model.fraction(note['onset'])
if generator.model.fraction(note['duration']) <= duration:
return
note.update(duration=generator.model.rational(duration), written=generator.model.written_for(duration))
for field in ('measure_rest', 'beams'):
note.pop(field, None)
for pitch in note['pitches']:
if generator.model.fraction(pitch['duration']) > duration:
pitch.update(duration=note['duration'], written=copy.deepcopy(note['written']))
pitch.pop('tie_to', None)
def splice(destination: dict, pickup: dict) -> fractions.Fraction:
"""Keep outgoing music until the pickup's first sounding event, then hand over."""
pad = offset(destination, pickup)
sounding = [generator.model.fraction(n['onset']) for n in generator.score.measure_notes(pickup) if n['pitches']]
if not sounding:
return generator.model.fraction(destination['duration'])
start = pad + min(sounding)
voices = {v['id']: {'id': v['id'], 'notes': []} for v in pickup['voices']}
primary = pickup['voices'][0]['id']
for voice in destination['voices']:
identifier = primary if len(destination['voices']) == 1 and voice['id'] not in voices else voice['id']
notes = voices.setdefault(identifier, {'id': identifier, 'notes': []})['notes']
for note in voice['notes']:
if generator.model.fraction(note['onset']) < start:
truncate(note, start)
note['voice'] = identifier
notes.append(note)
for voice in pickup['voices']:
notes = voices[voice['id']]['notes']
for note in voice['notes']:
onset = pad + generator.model.fraction(note['onset'])
end = onset + generator.model.fraction(note['duration'])
if onset < start:
if end <= start:
continue
note.update(duration=generator.model.rational(end - start), written=generator.model.written_for(end - start))
note.pop('measure_rest', None)
onset = start
note['onset'] = generator.model.rational(onset)
notes.append(note)
generator.accompaniment.normalize_beams(notes)
destination['voices'] = [voice for voice in voices.values() if voice['notes']]
state = {k: copy.deepcopy(pickup[k]) for k in ('key', 'clef', 'meter', 'transpose', 'staff_details')}
for change in pickup.get('attributes', []):
if pad + generator.model.fraction(change['onset']) <= start:
state.update({k: copy.deepcopy(v) for k, v in change.items() if k != 'onset'})
destination['attributes'] = [a for a in destination.get('attributes', []) if generator.model.fraction(a['onset']) < start]
previous = dict(destination)
for change in destination['attributes']:
previous.update(change)
state = {k: v for k, v in state.items() if previous.get(k) != v}
if start and state:
destination['attributes'].append({'onset': generator.model.rational(start), **state})
elif not start:
destination.update(state)
destination['key_signature'] = generator.model.key_label(destination['key'])
destination['time_signature'] = generator.model.meter_label(destination['meter'])
destination['attributes'].extend({**copy.deepcopy(a), 'onset': generator.model.rational(pad + generator.model.fraction(a['onset']))} for a in pickup.get('attributes', []) if pad + generator.model.fraction(a['onset']) > start)
return start
def retain_links(measures: list[dict], tuplets: list[dict]) -> list[dict]:
notes = [n for m in measures for n in generator.score.measure_notes(m)]
tones = {p['id']: p for n in notes for p in n['pitches']}
for pitch in tones.values():
for field, reverse in (('tie_to', 'tie_from'), ('tie_from', 'tie_to')):
if field in pitch and tones.get(pitch[field], {}).get(reverse) != pitch['id']:
pitch.pop(field)
members = {}
for note in notes:
for identifier in note.get('tuplet_ids', []):
members.setdefault(identifier, []).append(note['id'])
result = []
for span in tuplets:
if events := members.get(span['id']):
result.append({**span, 'start': events[0], 'end': events[-1], 'continued_start': span.get('continued_start', False) or span['start'] != events[0], 'continued_end': span.get('continued_end', False) or span['end'] != events[-1]})
return result
+5 -2
View File
@@ -14,6 +14,8 @@ class MeasureOrigin:
onset: fractions.Fraction
role: str
measure: dict
start: fractions.Fraction = fractions.Fraction()
end: fractions.Fraction | None = None
@dataclasses.dataclass(frozen=True)
class VoicingLink:
@@ -33,9 +35,10 @@ def link_voicings(source: list[dict], origins: list[MeasureOrigin]) -> list[Voic
for origin in origins:
for voice in origin.measure['voices']:
for note in voice['notes']:
if '_harmony' not in note:
local = generator.model.fraction(note['onset'])
if '_harmony' not in note or local < origin.start or (origin.end is not None and local >= origin.end):
continue
onset = origin.onset + generator.model.fraction(note['onset'])
onset = origin.onset + local
span = bisect.bisect_right(boundaries, onset)
if origin.role == 'melody' and note.get('_harmonize'):
melody.setdefault(span, []).append((onset, note['_harmony'], note))
-19
View File
@@ -101,22 +101,3 @@ def barline(style: str, x: float, top: float, source: str) -> list[generator.eng
return [generator.engraving.types.line('barline', references, ((x, top - 4 if style == 'tick' else top * 0.75), (x, top + 5 if style == 'tick' else top * 0.25)))]
weights = {'regular': [1.4], 'heavy': [4], 'light-light': [1.4, 1.4], 'light-heavy': [1.4, 4], 'heavy-light': [4, 1.4], 'heavy-heavy': [4, 4]}[style]
return [generator.engraving.types.line('barline', references, ((x - (len(weights) - 1 - i) * 5, top), (x - (len(weights) - 1 - i) * 5, 0)), width) for i, width in enumerate(weights)]
def tempo(value: dict, fonts: generator.engraving.fonts.Fonts, x: float, y: float, space: float, source: str) -> list[generator.engraving.types.Primitive]:
written = value['written']
references = (source,)
glyph = fonts.glyph(generator.engraving.notation.head_glyph(written, None), x, y, space * 0.7, 'tempo', references)
items = [glyph]
stem_x = glyph.box.right - 0.5
if written['type'] not in ('whole', 'breve', 'long', 'maxima'):
items.append(generator.engraving.types.line('tempo', references, ((stem_x, y), (stem_x, y - 23)), 1))
if generator.engraving.notation.flags(written):
name = 'flag' + ('8th' if written['type'] == 'eighth' else written['type']) + 'Up'
items.append(fonts.glyph(name, stem_x, y - 23, space * 0.7, 'tempo', references))
cursor = max(p.box.right for p in items) + 3
for _ in range(written.get('dots', 0)):
dot = fonts.glyph('augmentationDot', cursor, y - 3, space * 0.7, 'tempo', references)
items.append(dot)
cursor = dot.box.right + 3
items.append(fonts.text(' = ' + str(generator.model.fraction(value['bpm'])), cursor, y + 3, 16, 'tempo', references))
return items
+7 -3
View File
@@ -23,16 +23,23 @@ def audit_layout(document: dict, layout: generator.engraving.types.LayoutDocumen
index = generator.engraving.notation.ScoreIndex(document)
fragments, primitives = {}, {}
previous_end = fractions.Fraction()
scale = layout.pages[0].systems[0].scale
for page in layout.pages:
if len(page.systems) != 2 and (page is not layout.pages[-1] or len(page.systems) != 1):
raise generator.engraving.types.EngravingError('pages must contain two music systems, except an odd final system')
if len(page.systems) == 2 and page.systems[0].box.bottom + layout.style.system_gap > page.systems[1].box.top + 0.01:
raise generator.engraving.types.EngravingError('music systems overlap their page rows')
audit_primitives(page.primitives, page.width, page.height, fonts)
header = generator.engraving.types.bounds([p for p in page.primitives if p.role in ('title', 'subtitle')])
footer = generator.engraving.types.bounds([p for p in page.primitives if p.role == 'page-number'])
if page.systems[0].box.top < header.bottom + layout.style.page_clearance - 0.01 or page.systems[-1].box.bottom > footer.top - layout.style.page_clearance + 0.01:
raise generator.engraving.types.EngravingError('music overlaps the page header or footer')
for item in page.primitives:
for source in item.sources:
primitives.setdefault(source, []).append(item)
for system in page.systems:
if system.scale != scale:
raise generator.engraving.types.EngravingError('notation size must remain consistent throughout a part')
for role in ('staff', 'tab-staff'):
lines = [p for p in system.primitives if p.role == role]
if lines:
@@ -123,9 +130,6 @@ def audit_layout(document: dict, layout: generator.engraving.types.LayoutDocumen
label = generator.harmony.symbol(value)
if not any(p.role == 'harmony' and p.value == label for p in primitives.get(value['id'], [])):
raise generator.engraving.types.EngravingError(f'{value["id"]}: harmony was omitted')
for i, _ in enumerate(measure.get('tempos', [])):
if not any(p.role == 'tempo' for p in primitives.get(measure['id'] + f':tempo:{i}', [])):
raise generator.engraving.types.EngravingError(f'{measure["id"]}: tempo was omitted')
for span in index.staff.get('tuplets', []):
if span.get('show_number', 'actual') != 'none' or span.get('bracket', 'auto') != 'no':
if not any(p.role.startswith('tuplet-') for p in primitives.get(span['id'], [])):
+65 -38
View File
@@ -54,9 +54,6 @@ def column(index: generator.engraving.notation.ScoreIndex, time: fractions.Fract
for owner, _, item in index.harmonies_by_onset.get(time, []):
if owner is measure:
items.append(fonts.text(generator.harmony.symbol(item), 0, 0, 18 if index.staff.get('tablature') else 21, 'harmony', (item['id'],)))
for owner, i, value in index.tempos_by_onset.get(time, []):
if owner is measure:
items.extend(generator.engraving.attributes.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'):
@@ -159,54 +156,84 @@ def make_system(index: generator.engraving.notation.ScoreIndex, start: fractions
items = generator.engraving.tablature.staff(index, items + tab_items, start, end, content_left, width - 1, fonts, style)
return generator.engraving.types.System(start, end, tuple(items), tuple(anchors), tuple(fragments), generator.engraving.types.bounds(items))
def fit_page_systems(systems, index, row_height, fonts, style):
scale = min(1, row_height / max(s.box.bottom - s.box.top for s in systems))
def fit_part_systems(systems, index, available_height, fonts, style):
def fitting_scale(items):
scale = 1
for first in range(0, len(items), 2):
pair = items[first:first + 2]
height = sum(s.box.bottom - s.box.top for s in pair)
scale = min(scale, (available_height - style.system_gap * (len(pair) - 1)) / height)
return scale
scale = fitting_scale(systems)
columns = collections.OrderedDict()
# Re-space columns at the final physical width; scale glyphs uniformly, not horizontally.
# Fit the entire part together; only spacing, never page-specific glyph sizes, varies.
for _ in range(8):
width = (style.width - 2 * style.margin_x - 2) / scale
fitted = [make_system(index, s.start, s.end, fonts, style, justify=s.end < index.end, columns=columns, staff_width=width) for s in systems]
height = max(s.box.bottom - s.box.top for s in fitted)
if height * scale <= row_height + 0.001:
next_scale = fitting_scale(fitted)
if scale <= next_scale + 0.000001:
return [s.scaled(scale) for s in fitted]
scale = row_height / height
raise generator.engraving.types.EngravingError(f'at musical offset {systems[0].start}: page spacing did not converge')
scale = next_scale
raise generator.engraving.types.EngravingError('part spacing did not converge')
def part_header(document, fonts, style):
size = 16
title = document['metadata']['title']
item = fonts.text(title, style.margin_x, style.margin_y + size, size, 'title', ())
while item.box.right > style.width - style.margin_x and size > 12:
size -= 1
item = fonts.text(title, style.margin_x, style.margin_y + size, size, 'title', ())
if item.box.right > style.width - style.margin_x:
raise generator.engraving.types.EngravingError('score title cannot fit the page without clipping')
result = [item]
subtitle = ' - '.join(str(value) for value in (document['metadata'].get('composer'), document['staves'][0].get('name')) if value)
if not subtitle:
return result
credit = fonts.text(subtitle, 0, style.margin_y + size, 12, 'subtitle', ())
width = credit.box.right - credit.box.left
if item.box.right + 20 + width <= style.width - style.margin_x:
result.append(credit.moved(style.width - style.margin_x - credit.box.right, 0))
return result
line = ''
baseline = item.box.bottom + 15
for word in subtitle.split():
text = ' '.join(filter(None, (line, word)))
candidate = fonts.text(text, style.margin_x, baseline, 12, 'subtitle', ())
if candidate.box.right > style.width - style.margin_x and line:
result.append(fonts.text(line, style.margin_x, baseline, 12, 'subtitle', ()))
baseline = result[-1].box.bottom + 15
text = word
candidate = fonts.text(text, style.margin_x, baseline, 12, 'subtitle', ())
if candidate.box.right > style.width - style.margin_x:
raise generator.engraving.types.EngravingError('score credits cannot fit the page without clipping')
line = text
result.append(fonts.text(line, style.margin_x, baseline, 12, 'subtitle', ()))
return result
def paginate(systems: list[generator.engraving.types.System], document: dict, fonts: generator.engraving.fonts.Fonts, style: generator.engraving.types.Style, *, index=None) -> tuple[generator.engraving.types.Page, ...]:
pages = []
index = index or generator.engraving.notation.ScoreIndex(document)
guitar = document['staves'][0].get('tablature', False)
header_gap = 14 if guitar else 24
def page_header(number):
title = document['metadata']['title']
size = (20 if number == 1 else 16) if guitar else (26 if number == 1 else 17)
item = fonts.text(title, style.margin_x, style.margin_y + size, size, 'title', ())
while item.box.right > style.width - style.margin_x and size > 12:
size -= 1
item = fonts.text(title, style.margin_x, style.margin_y + size, size, 'title', ())
if item.box.right > style.width - style.margin_x:
raise generator.engraving.types.EngravingError('score title cannot fit the page without clipping')
result = [item]
if number == 1:
subtitle = ' - '.join(str(value) for value in (document['metadata'].get('composer'), document['staves'][0].get('name')) if value)
if subtitle:
result.append(fonts.text(subtitle, style.margin_x, item.box.bottom + (15 if guitar else 22), 12 if guitar else 16, 'subtitle', ()))
return result
header = part_header(document, fonts, style)
top = generator.engraving.types.bounds(header).bottom + style.page_clearance
footers = [fonts.text(str(number), style.width / 2, style.height - style.margin_y, 12, 'page-number', (), True) for number in range(1, (len(systems) + 1) // 2 + 1)]
bottom = min(p.box.top for p in footers) - style.page_clearance
if bottom - top <= style.system_gap:
raise generator.engraving.types.EngravingError('page header leaves no room for two music systems')
fitted = fit_part_systems(systems, index, bottom - top, fonts, style)
for first in range(0, len(systems), 2):
number = len(pages) + 1
header = page_header(number)
top = generator.engraving.types.bounds(header).bottom + header_gap
row_height = (style.height - style.margin_y - 24 - top - style.system_gap) / 2
if row_height <= 0:
raise generator.engraving.types.EngravingError('page header leaves no room for two music systems')
pair = fit_page_systems(systems[first:first + 2], index, row_height, fonts, style)
pair = fitted[first:first + 2]
slack = max(0, bottom - top - sum(s.box.bottom - s.box.top for s in pair) - style.system_gap * (len(pair) - 1))
spacing = slack / 3 if len(pair) == 2 else 0
cursor = top + spacing
placed = []
for row, system in enumerate(pair):
dx, dy = style.margin_x + 1, top + row * (row_height + style.system_gap) - system.box.top
for system in pair:
dx, dy = style.margin_x + 1, cursor - system.box.top
placed.append(dataclasses.replace(system, primitives=tuple(p.moved(dx, dy) for p in system.primitives), anchors=tuple(a.moved(dx, dy) for a in system.anchors), box=system.box.moved(dx, dy)))
footer = fonts.text(str(number), style.width / 2, style.height - style.margin_y, 12, 'page-number', (), True)
cursor = placed[-1].box.bottom + style.system_gap + spacing
footer = footers[first // 2]
pages.append(generator.engraving.types.Page(number, style.width, style.height, style.supersampling, tuple(placed), tuple(header + [p for s in placed for p in s.primitives] + [footer])))
return tuple(pages)
+1 -3
View File
@@ -29,7 +29,6 @@ class ScoreIndex:
self.events_by_onset = {}
self.attributes_by_onset = {}
self.harmonies_by_onset = {}
self.tempos_by_onset = {}
self.by_id = {}
self.tones = {}
self.boundaries = {fractions.Fraction()}
@@ -51,8 +50,7 @@ class ScoreIndex:
self.columns.add(onset)
for tone in note['pitches']:
self.tones[tone['id']] = (event, tone)
for field in ('attributes', 'harmonies', 'tempos'):
bucket = {'attributes': self.attributes_by_onset, 'harmonies': self.harmonies_by_onset, 'tempos': self.tempos_by_onset}[field]
for field, bucket in (('attributes', self.attributes_by_onset), ('harmonies', self.harmonies_by_onset)):
for index, item in enumerate(measure.get(field, [])):
onset = time + generator.model.fraction(item['onset'])
self.columns.add(onset)
+2 -2
View File
@@ -203,11 +203,11 @@ def draw_spans(index: generator.engraving.notation.ScoreIndex, anchors: list[gen
return items + result
def place_text_lanes(items: list[generator.engraving.types.Primitive], fonts: generator.engraving.fonts.Fonts, style: generator.engraving.types.Style) -> list[generator.engraving.types.Primitive]:
music = [p for p in items if p.role not in ('harmony', 'lyric', 'section', 'tempo')]
music = [p for p in items if p.role not in ('harmony', 'lyric', 'section')]
box = generator.engraving.types.bounds(music)
result = list(music)
top = box.top - 12
for role in ('harmony', 'tempo', 'section'):
for role in ('harmony', 'section'):
lane = [p for p in items if p.role == role]
if lane:
extent = generator.engraving.types.bounds(lane)
+1
View File
@@ -14,6 +14,7 @@ class Style:
space: float = 12
margin_x: float = 28
margin_y: float = 18
page_clearance: float = 14
supersampling: int = 4
column_gap: float = 10
system_gap: float = 26