Tone Generation
ci / checks (push) Has been cancelled

This commit is contained in:
khodowany
2026-09-18 01:17:36 -05:00
parent 64eac2f11e
commit 80873a0896
39 changed files with 1624 additions and 52 deletions
+3 -1
View File
@@ -52,8 +52,10 @@ def engrave_tree(root: Path, fonts: Path) -> None:
rasterize_page(page, family).save(target, format='PNG')
check_page(target)
entries.append({'path': destination.name + '/' + name, 'sha256': hashlib.sha256(target.read_bytes()).hexdigest()})
music = {k: v for k, v in document.items() if k != 'schema_version'}
music = {k: v for k, v in document.items() if k not in ('schema_version', 'playback')}
bundle = {'schema_version': 3, 'music': music, 'presentation': {'engine': ENGINE_NAME, 'engine_version': ENGINE_VERSION, 'style_sha256': layout.fingerprint, 'width': PAGE_WIDTH, 'height': PAGE_HEIGHT, 'pages': entries}}
if 'playback' in document:
bundle['playback'] = document['playback']
path.write_text(json.dumps(bundle, indent=2, ensure_ascii=False) + '\n', encoding='utf-8')
except ValueError as error:
raise EngravingError(f'{path.relative_to(root)}: {error}') from error
+17 -1
View File
@@ -58,6 +58,22 @@ def audit_layout(document: dict, layout: LayoutDocument, fonts: Fonts) -> None:
raise EngravingError(f'{item.sources} and {other.sources}: colliding {item.role}/{other.role}')
if previous_end != index.end:
raise EngravingError('layout omits the end of the score')
from generator.repeats import analyze
_, endings = analyze(index.staff['measures'])
for ending in endings:
if not ending.visible:
continue
identifier = index.measures[ending.start][2]['id'] + ':ending'
if not any(p.role == 'ending-label' and p.value == ending.text for p in primitives.get(identifier, [])):
raise EngravingError(f'{identifier}: numbered ending was omitted')
low, high = index.measures[ending.start][0], index.measures[ending.end - 1][1]
for page in layout.pages:
for system in page.systems:
if system.start < high and system.end > low and not any(p.role == 'ending' and identifier in p.sources for p in system.primitives):
raise EngravingError(f'{identifier}: ending continuation was omitted')
for _, _, measure in index.measures:
if measure.get('repeats') and not any(p.role == 'repeat-dot' for p in primitives.get(measure['id'], [])):
raise EngravingError(f'{measure["id"]}: repeat dots were omitted')
for event in index.events:
note = event.value
for source in note['pitches'] or [note]:
@@ -85,7 +101,7 @@ def audit_layout(document: dict, layout: LayoutDocument, fonts: Fonts) -> None:
if any(f.visible for f in pieces) and identifier in index.tones and index.accidentals[identifier]:
if not any(p.role == 'accidental' for p in primitives.get(identifier, [])):
raise EngravingError(f'{identifier}: required accidental was omitted')
if identifier in index.tones and source.get('tie_to') and any(f.visible for f in pieces):
if identifier in index.tones and (source.get('tie_to') or source.get('printed_ties')) and any(f.visible for f in pieces):
if not any(p.role == 'tie' for p in primitives.get(identifier, [])):
raise EngravingError(f'{identifier}: tie was omitted')
if not note.get('hidden'):
+22 -6
View File
@@ -10,6 +10,7 @@ from generator.engraving.attributes import barline, staff_attributes, tempo
from generator.engraving.fonts import Fonts
from generator.engraving.notation import ScoreIndex
from generator.engraving.notes import draw_event
from generator.engraving.repeats import draw_boundary, draw_endings
from generator.engraving.spans import draw_beams, draw_lyric_spans, draw_spans, place_text_lanes
from generator.engraving.types import Anchor, EngravingError, Fragment, LayoutDocument, Page, Primitive, Style, System, bounds, line
@@ -70,6 +71,12 @@ def make_system(index: ScoreIndex, start: Fraction, end: Fraction, fonts: Fonts,
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
boundary_positions = {start: cursor - 9}
if start in index.boundaries and measure.get('repeats', {}).get('left'):
initial = draw_boundary(None, measure, cursor, -style.space * (state['staff_details']['lines'] - 1), fonts, style)
boundary_positions[start] = cursor
items.extend(initial)
cursor = max(p.box.right for p in initial) + 18
content_left = cursor - 9
times = sorted({start} | {t for t in index.columns if start < t < end})
placements = []
@@ -81,15 +88,18 @@ def make_system(index: ScoreIndex, start: Fraction, end: Fraction, fonts: Fonts,
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']))
leading.extend(draw_boundary(previous, m, 0, -style.space * (index.state(time)['staff_details']['lines'] - 1), fonts, style))
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))
offset = max((p.box.right for p in leading), default=0) + 12 if previous.get('repeats') or m.get('repeats') else 12
leading.extend(p.moved(offset, 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)
if leading:
if time in index.boundaries:
boundary_positions[time] = cursor - bounds(leading).left
width = bounds(leading).right - bounds(leading).left + 14
items.extend(p.moved(cursor - bounds(leading).left, 0) for p in leading)
cursor += width
@@ -111,12 +121,18 @@ def make_system(index: ScoreIndex, start: Fraction, end: Fraction, fonts: Fonts,
items[first_item:last_item] = [p.moved(i * stretch, 0) for p in items[first_item:last_item]]
anchors[first_anchor:last_anchor] = [a.moved(i * stretch, 0) for a in anchors[first_anchor:last_anchor]]
placements = [(time, x + i * stretch) for i, (time, x) in enumerate(placements)]
boundary_positions = {time: x + times.index(time) * stretch for time, x in boundary_positions.items()}
width = available
right = width - 5
boundary_positions[end] = width - 3
last_measure = next(m for a, b, m in index.measures if a < end <= b)
if end in index.boundaries and last_measure.get('repeats', {}).get('right'):
items.extend(draw_boundary(last_measure, None, width - 3, -style.space * (index.state(end, before=True)['staff_details']['lines'] - 1), fonts, style))
items = draw_beams(index, anchors, items, start, end, content_left, right, style)
items = draw_spans(index, anchors, items, start, end, content_left, right, fonts, style)
items = place_text_lanes(items, fonts, style)
items = draw_lyric_spans(index, items, anchors, start, end, content_left, right, fonts)
items = draw_endings(index, items, start, end, boundary_positions, fonts, style)
# Staff lines are interrupted behind TAB numerals, not painted through them.
changes = [(0, state['staff_details']['lines'])]
for time, x in placements:
@@ -135,11 +151,11 @@ def make_system(index: ScoreIndex, start: Fraction, end: Fraction, fonts: Fonts,
staff_lines.append(line('staff', (), ((cursor, y), (a, y)), style.space * 0.13))
cursor = max(cursor, b)
items = staff_lines + items
last_measure = next(m for a, b, m in index.measures if a < end <= b)
if end in index.boundaries:
items.extend(barline(last_measure.get('barlines', {}).get('right', 'regular'), width - 1, -style.space * (index.state(end, before=True)['staff_details']['lines'] - 1), last_measure['id']))
if end in index.boundaries and not last_measure.get('repeats', {}).get('right'):
items.extend(draw_boundary(last_measure, None, width - (3 if last_measure.get('repeats') else 1), -style.space * (index.state(end, before=True)['staff_details']['lines'] - 1), fonts, style))
if start in index.boundaries:
items.extend(barline(measure.get('barlines', {}).get('left', 'none'), 0, -style.space * (state['staff_details']['lines'] - 1), measure['id']))
if not measure.get('repeats', {}).get('left'):
items.extend(barline(measure.get('barlines', {}).get('left', 'none'), 0, -style.space * (state['staff_details']['lines'] - 1), measure['id']))
else:
items.append(fonts.text(str(measure['number']) + ' (cont.)', 0, min(p.box.top for p in items) - 6, 12, 'continuation', (measure['id'],)))
return System(start, end, tuple(items), tuple(anchors), tuple(fragments), bounds(items))
+42
View File
@@ -0,0 +1,42 @@
from generator.engraving.attributes import barline
from generator.engraving.types import line
from generator.repeats import analyze
def draw_boundary(previous, following, x, top, fonts, style):
backward = previous.get('repeats', {}).get('right') if previous else None
forward = following.get('repeats', {}).get('left') if following else None
source = previous or following
if not backward and not forward:
value = previous.get('barlines', {}).get('right', 'regular') if previous else following.get('barlines', {}).get('left', 'none')
return barline(value, x, top, source['id'])
refs = tuple(m['id'] for m in (previous, following) if m)
result = [line('repeat-bar', refs, ((x, top), (x, 0)), 4)]
for marker, direction in ((backward, -1), (forward, 1)):
if not marker:
continue
result.append(line('repeat-bar', refs, ((x + direction * 6, top), (x + direction * 6, 0)), 1.4))
for offset in (-0.5, 0.5):
result.append(fonts.glyph('augmentationDot', x + direction * 13 - 2, top / 2 + style.space * offset, style.space, 'repeat-dot', refs))
if backward and backward.get('times', 2) != 2:
result.append(fonts.text(f'{backward["times"]} times', x - 10, top - 12, 14, 'repeat-count', refs, True))
return result
def draw_endings(index, items, start, end, positions, fonts, style):
_, endings = analyze(index.staff['measures'])
result = list(items)
boundaries = [a for a, _, _ in index.measures] + [index.end]
top = min((p.box.top for p in items), default=0) - 25
for ending in endings:
low, high = boundaries[ending.start], boundaries[ending.end]
if not ending.visible or high <= start or low >= end:
continue
x1 = positions[max(low, start)]
x2 = positions[min(high, end)]
refs = (index.measures[ending.start][2]['id'] + ':ending',)
result.append(line('ending', refs, ((x1, top), (x2, top))))
if low >= start:
result.append(line('ending', refs, ((x1, top), (x1, top + 10))))
result.append(fonts.text(ending.text, x1 + 5, top - 4, 16, 'ending-label', refs))
if high <= end and ending.closed:
result.append(line('ending', refs, ((x2, top), (x2, top + 10))))
return result
+14
View File
@@ -101,6 +101,20 @@ def draw_spans(index: ScoreIndex, anchors: list[Anchor], items: list[Primitive],
target = tone.get('tie_to')
tone_end = event.start + fraction(tone['duration'])
anchor = by_tone.get(identifier)
if tone.get('printed_ties'):
for action in tone['printed_ties']:
candidates = [(other_id, other) for other_id, (other_event, other) in index.tones.items() if other_event.voice == event.voice and other_event.start == tone_end and 'stop' in other.get('printed_ties', []) and all(other.get(k) == tone.get(k) for k in ('letter', 'alter', 'octave', 'unpitched'))]
if action == 'start' and candidates:
target = candidates[0][0]
continue
if action == 'stop' and any(other_event.voice == event.voice and other_event.start + fraction(other['duration']) == event.start and 'start' in other.get('printed_ties', []) and all(other.get(k) == tone.get(k) for k in ('letter', 'alter', 'octave', 'unpitched')) for other_event, other in index.tones.values()):
continue
if anchor is None:
continue
x1, x2 = (anchor.x + style.space * 1.4, min(right, anchor.x + 36)) if action == 'start' else (max(left, anchor.x - 24), anchor.x - 2)
y = anchor.y - anchor.direction * 8
if x2 > x1:
result.append(curve('tie', (identifier,), ((x1, y), (x1 + (x2 - x1) / 3, y - anchor.direction * 9), (x2 - (x2 - x1) / 3, y - anchor.direction * 9), (x2, y)), 1.6))
if anchor and anchor.fragment.end < tone_end:
target = identifier
if target: