Skip to content

Commit 4d3826f

Browse files
authored
Merge pull request #115 from liebharc/fix/note-y-position
MuseScore allows to change the y-position of a note
2 parents b164bec + f4251b9 commit 4d3826f

1 file changed

Lines changed: 96 additions & 2 deletions

File tree

training/omr_datasets/convert_lieder.py

Lines changed: 96 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# ruff: noqa: E402
22

3+
import hashlib
34
import json
45
import multiprocessing
56
import os
@@ -119,7 +120,9 @@ def copy_all_mscx_files(working_dir: str, dest: str) -> None:
119120
shutil.copyfile(source, os.path.join(dest, file))
120121

121122

122-
def create_formats(source_file: str, formats: list[str]) -> list[dict[str, str]]:
123+
def create_formats(
124+
source_file: str, formats: list[str], style_file: str | None = None
125+
) -> list[dict[str, str]]:
123126
jobs: list[dict[str, str]] = []
124127

125128
# sq8940236: MuseScore seems to hang up
@@ -152,10 +155,66 @@ def create_formats(source_file: str, formats: list[str]) -> list[dict[str, str]]
152155
"in": source_file,
153156
"out": out_name,
154157
}
158+
if style_file is not None:
159+
job["style"] = style_file
155160
jobs.append(job)
156161
return jobs
157162

158163

164+
# Every Lieder page is rendered by MuseScore with its default "Leland" engraving font,
165+
# so the model only ever sees one glyph vocabulary for noteheads/clefs/accidentals/etc.
166+
# MuseScore ships several other SMuFL-compliant engraving fonts (selectable in the app
167+
# under Format > Style > Score > Musical Symbols, backed by a swappable <musicalSymbolFont>
168+
# style setting); rotating through them per piece costs nothing at render time and gives
169+
# the model exposure to multiple glyph "handwritings" without needing a different dataset
170+
# or renderer. This only varies glyph shapes, not MuseScore's own layout/spacing engine -
171+
# so it doesn't substitute for training on genuinely different renderers (Primus,
172+
# grandstaff), just cheaply widens the glyph diversity within Lieder itself.
173+
_MUSIC_FONTS = ["Leland", "Bravura", "Petaluma", "MuseJazz", "Gonville"]
174+
_music_font_style_dir = os.path.join(dataset_root, "MuseScoreStyles")
175+
176+
177+
def _music_font_style_file(font: str) -> str:
178+
return os.path.join(_music_font_style_dir, f"{font.replace(' ', '_')}.mss")
179+
180+
181+
def _ensure_music_font_style_files() -> None:
182+
"""
183+
Writes one minimal .mss style file per font in _MUSIC_FONTS (skipping ones that
184+
already exist), each just pointing MuseScore's musical-symbol and musical-text
185+
fonts at a single named font pair - MuseScore fills in every other style default.
186+
The "<Font> Text" naming for the paired text font mirrors the font-pair names
187+
MuseScore itself uses for its bundled fonts.
188+
"""
189+
os.makedirs(_music_font_style_dir, exist_ok=True)
190+
for font in _MUSIC_FONTS:
191+
path = _music_font_style_file(font)
192+
if os.path.exists(path):
193+
continue
194+
content = (
195+
'<?xml version="1.0" encoding="UTF-8"?>\n'
196+
'<museScore version="4.00">\n'
197+
" <Style>\n"
198+
f" <musicalSymbolFont>{font}</musicalSymbolFont>\n"
199+
f" <musicalTextFont>{font} Text</musicalTextFont>\n"
200+
" </Style>\n"
201+
"</museScore>\n"
202+
)
203+
with open(path, "w", encoding="utf-8") as f:
204+
f.write(content)
205+
206+
207+
def _music_font_for_file(source_file: str) -> str:
208+
"""
209+
Deterministic per-piece font choice (stable across reruns/partial recreates, so a
210+
piece doesn't silently re-render with a different font the next time this is run)
211+
based on a hash of the piece's own filename, not path or run order.
212+
"""
213+
stem = os.path.basename(source_file).split(".")[0]
214+
index = int(hashlib.sha256(stem.encode()).hexdigest(), 16) % len(_MUSIC_FONTS)
215+
return _MUSIC_FONTS[index]
216+
217+
159218
"""
160219
In mscx file, tuplet can be set invisible via the following ways:
161220
1. <Tuplet( id="...")>
@@ -243,6 +302,37 @@ def _make_staff_visible(mscx_file: str) -> None:
243302
f.write(new_content)
244303

245304

305+
def _reset_note_positions(mscx_file: str) -> None:
306+
"""
307+
A manually-dragged notehead in MuseScore leaves a <pos x=".." y=".."/> override
308+
as the first child of its <Note> element, which shifts where it renders without
309+
touching its <pitch>. This is rare (~0.015% of notes dataset-wide) but when it
310+
happens, the rendered position can silently disagree with the note's own pitch -
311+
e.g. in lc4926375 a repeated E5 renders on the D5 line because of a y="0.5" (one
312+
diatonic step) override, so the exported image shows a different note than the
313+
label says.
314+
315+
<pos> is also used pervasively elsewhere in this format for unrelated, legitimate
316+
purposes - slur/tie curve control points (nested under <Note><Tie><SlurSegment>),
317+
augmentation-dot offsets (<Note><NoteDot>), staff text, tempo marks, stems - so
318+
this only strips a <pos> that is directly the first thing inside <Note>, never one
319+
nested deeper. MuseScore also serializes an empty element as either a self-closing
320+
tag or a separate open/close pair depending on context, so both forms are matched.
321+
"""
322+
with open(mscx_file, encoding="utf-8") as f:
323+
content = f.read()
324+
325+
new_content = re.sub(
326+
r"(<Note>\s*)<pos\b[^>]*(?:/>|>\s*</pos>)\s*",
327+
r"\1",
328+
content,
329+
)
330+
331+
if new_content != content:
332+
with open(mscx_file, "w", encoding="utf-8") as f:
333+
f.write(new_content)
334+
335+
246336
def _create_musicxml_and_svg_files() -> None:
247337
dest = os.path.join(lieder, "flat")
248338
os.makedirs(dest, exist_ok=True)
@@ -252,12 +342,16 @@ def _create_musicxml_and_svg_files() -> None:
252342

253343
MuseScore = os.path.join(dataset_root, "MuseScore")
254344

345+
_ensure_music_font_style_files()
346+
255347
all_jobs = []
256348

257349
for file in mscx_files:
258350
_make_tuplet_visible(str(file))
259351
_make_staff_visible(str(file))
260-
jobs = create_formats(str(file), ["musicxml", "svg"])
352+
_reset_note_positions(str(file))
353+
style_file = _music_font_style_file(_music_font_for_file(str(file)))
354+
jobs = create_formats(str(file), ["musicxml", "svg"], style_file)
261355
all_jobs.extend(jobs)
262356

263357
if len(all_jobs) == 0:

0 commit comments

Comments
 (0)