Skip to content

Commit

Permalink
Merge pull request #401 from leleogere/support_print-object
Browse files Browse the repository at this point in the history
Add a parameter to ignore invisible objects when parsing MusicXML files
  • Loading branch information
sildater authored Jan 21, 2025
2 parents 18cfd3d + aa582c9 commit 0122040
Show file tree
Hide file tree
Showing 4 changed files with 276 additions and 5 deletions.
38 changes: 33 additions & 5 deletions partitura/io/importmusicxml.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@ def load_musicxml(
filename: PathLike,
validate: bool = False,
force_note_ids: Optional[Union[bool, str]] = None,
ignore_invisible_objects: bool = False,
) -> score.Score:
"""Parse a MusicXML file and build a composite score ontology
structure from it (see also scoreontology.py).
Expand All @@ -213,6 +214,9 @@ def load_musicxml(
assigned unique id attribute. Existing note id attributes in
the MusicXML will be discarded. If 'keep', only notes without
a note id will be assigned one.
ignore_invisible_objects : bool, optional
When True, objects that with the attribute `print-object="no"`
will be ignored. Defaults to False.
Returns
-------
Expand Down Expand Up @@ -257,7 +261,7 @@ def load_musicxml(
partlist, part_dict = _parse_partlist(partlist_el)
# Go through each <part> to obtain the content of the parts.
# The Part instances will be modified in place
_parse_parts(document, part_dict)
_parse_parts(document, part_dict, ignore_invisible_objects=ignore_invisible_objects)
else:
partlist = []

Expand Down Expand Up @@ -346,7 +350,7 @@ def load_musicxml(
return scr


def _parse_parts(document, part_dict):
def _parse_parts(document, part_dict, ignore_invisible_objects=False):
"""
Populate the Part instances that are the values of `part_dict` with the
musical content in document.
Expand All @@ -358,6 +362,8 @@ def _parse_parts(document, part_dict):
part_dict : dict
A dictionary with key--value pairs (part_id, Part instance), as returned
by the _parse_partlist() function.
ignore_invisible_objects : bool, optional
When True, objects that with the attribute `print-object="no"` will be ignored.
"""

for part_el in document.findall("part"):
Expand All @@ -373,7 +379,7 @@ def _parse_parts(document, part_dict):

for mc, measure_el in enumerate(part_el.xpath("measure")):
position, doc_order = _handle_measure(
measure_el, position, part, ongoing, doc_order, mc + 1
measure_el, position, part, ongoing, doc_order, mc + 1, ignore_invisible_objects
)

# complete unfinished endings
Expand Down Expand Up @@ -497,7 +503,15 @@ def _parse_parts(document, part_dict):
# shift.applied = True


def _handle_measure(measure_el, position, part, ongoing, doc_order, measure_counter):
def _handle_measure(
measure_el,
position,
part,
ongoing,
doc_order,
measure_counter,
ignore_invisible_objects=False,
):
"""Parse a <measure>...</measure> element, adding it and its contents to the part.
Parameters
Expand All @@ -514,6 +528,8 @@ def _handle_measure(measure_el, position, part, ongoing, doc_order, measure_coun
The index of the first note element in the current measure in the xml file.
measure_counter : int
The index of the <measure> tag in the xml file, starting from 1
ignore_invisible_objects : bool, optional
When True, objects that with the attribute `print-object="no"` will be ignored.
Returns
-------
Expand Down Expand Up @@ -541,6 +557,19 @@ def _handle_measure(measure_el, position, part, ongoing, doc_order, measure_coun
measure_maxtime = measure_start
trailing_children = []
for i, e in enumerate(measure_el):
# If the object is invisible and the user wants it, skip the object
# Will probably not skip everything, but works at least for notes and rests
if ignore_invisible_objects:
print_obj = get_value_from_attribute(e, "print-object", str)
notehead = e.find("notehead") # Musescore mask notes with notehead="none"
if print_obj == "no" or (notehead is not None and notehead.text == "none"):
# Still update position for invisible notes (to avoid problems with backups)
if e.tag == "note":
duration = get_value_from_tag(e, "duration", int) or 0
position += duration
# Skip the object
continue

if e.tag == "backup":
# <xs:documentation>The backup and forward elements are required
# to coordinate multiple voices in one part, including music on
Expand Down Expand Up @@ -1368,7 +1397,6 @@ def _handle_note(e, position, part, ongoing, prev_note, doc_order, prev_beam=Non
technical=technical_notations,
doc_order=doc_order,
)

part.add(note, position, position + duration)

# After note is assigned to part we can assign the beam to the note if it exists
Expand Down
5 changes: 5 additions & 0 deletions tests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,11 @@
for fn1, fn2 in [("test_unfold_dacapo.xml", "test_unfold_dacapo_result.xml")]
]

MUSICXML_IGNORE_INVISIBLE_OBJECTS = [
os.path.join(MUSICXML_PATH, fn)
for fn in ["test_ignore_invisible_objects.musicxml"]
]

# This is a list of files for testing Chew and Wu's VOSA. (More files to come?)
VOSA_TESTFILES = [
os.path.join(MUSICXML_PATH, fn) for fn in ["test_chew_vosa_example.xml"]
Expand Down
211 changes: 211 additions & 0 deletions tests/data/musicxml/test_ignore_invisible_objects.musicxml
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE score-partwise PUBLIC "-//Recordare//DTD MusicXML 4.0 Partwise//EN" "http://www.musicxml.org/dtds/partwise.dtd">
<score-partwise version="4.0">
<work>
<work-title>Partition sans titre</work-title>
</work>
<identification>
<creator type="composer">Compositeur / Arrangeur</creator>
<encoding>
<software>MuseScore 4.4.4</software>
<encoding-date>2024-12-13</encoding-date>
<supports element="accidental" type="yes"/>
<supports element="beam" type="yes"/>
<supports element="print" attribute="new-page" type="no"/>
<supports element="print" attribute="new-system" type="no"/>
<supports element="stem" type="yes"/>
</encoding>
</identification>
<part-list>
<score-part id="P1">
<part-name>Piano</part-name>
<part-abbreviation>Pno.</part-abbreviation>
<score-instrument id="P1-I1">
<instrument-name>Piano</instrument-name>
<instrument-sound>keyboard.piano</instrument-sound>
</score-instrument>
<midi-device id="P1-I1" port="1"></midi-device>
<midi-instrument id="P1-I1">
<midi-channel>1</midi-channel>
<midi-program>1</midi-program>
<volume>78.7402</volume>
<pan>0</pan>
</midi-instrument>
</score-part>
</part-list>
<part id="P1">
<measure number="1">
<attributes>
<divisions>4</divisions>
<key>
<fifths>0</fifths>
</key>
<time>
<beats>4</beats>
<beat-type>4</beat-type>
</time>
<staves>2</staves>
<clef number="1">
<sign>G</sign>
<line>2</line>
</clef>
<clef number="2">
<sign>F</sign>
<line>4</line>
</clef>
</attributes>
<note>
<pitch>
<step>F</step>
<octave>4</octave>
</pitch>
<duration>4</duration>
<voice>1</voice>
<type>quarter</type>
<stem>up</stem>
<staff>1</staff>
</note>
<note>
<pitch>
<step>E</step>
<octave>4</octave>
</pitch>
<duration>4</duration>
<voice>1</voice>
<type>quarter</type>
<stem>up</stem>
<staff>1</staff>
</note>
<note>
<pitch>
<step>D</step>
<octave>4</octave>
</pitch>
<duration>4</duration>
<voice>1</voice>
<type>quarter</type>
<stem>up</stem>
<staff>1</staff>
</note>
<note>
<pitch>
<step>C</step>
<octave>4</octave>
</pitch>
<duration>4</duration>
<voice>1</voice>
<type>quarter</type>
<stem>up</stem>
<staff>1</staff>
</note>
<backup>
<duration>4</duration>
</backup>
<note>
<pitch>
<step>C</step>
<octave>4</octave>
</pitch>
<duration>1</duration>
<voice>2</voice>
<type>16th</type>
<stem>none</stem>
<notehead>none</notehead>
<staff>1</staff>
<beam number="1">begin</beam>
<beam number="2">begin</beam>
</note>
<note>
<pitch>
<step>D</step>
<octave>4</octave>
</pitch>
<duration>1</duration>
<voice>2</voice>
<type>16th</type>
<stem>none</stem>
<notehead>none</notehead>
<staff>1</staff>
<beam number="1">continue</beam>
<beam number="2">continue</beam>
</note>
<note>
<pitch>
<step>C</step>
<octave>4</octave>
</pitch>
<duration>1</duration>
<voice>2</voice>
<type>16th</type>
<stem>none</stem>
<notehead>none</notehead>
<staff>1</staff>
<beam number="1">continue</beam>
<beam number="2">continue</beam>
</note>
<note>
<pitch>
<step>D</step>
<octave>4</octave>
</pitch>
<duration>1</duration>
<voice>2</voice>
<type>16th</type>
<stem>none</stem>
<notehead>none</notehead>
<staff>1</staff>
<beam number="1">end</beam>
<beam number="2">end</beam>
</note>
<backup>
<duration>16</duration>
</backup>
<note>
<pitch>
<step>C</step>
<octave>3</octave>
</pitch>
<duration>4</duration>
<voice>5</voice>
<type>quarter</type>
<stem>up</stem>
<staff>2</staff>
</note>
<note>
<pitch>
<step>D</step>
<octave>3</octave>
</pitch>
<duration>4</duration>
<voice>5</voice>
<type>quarter</type>
<stem>none</stem>
<notehead>none</notehead>
<staff>2</staff>
</note>
<note print-object="no">
<rest/>
<duration>4</duration>
<voice>5</voice>
<type>quarter</type>
<staff>2</staff>
</note>
<note>
<pitch>
<step>B</step>
<alter>-1</alter>
<octave>2</octave>
</pitch>
<duration>4</duration>
<voice>5</voice>
<type>quarter</type>
<accidental>flat</accidental>
<stem>up</stem>
<staff>2</staff>
</note>
<barline location="right">
<bar-style>light-heavy</bar-style>
</barline>
</measure>
</part>
</score-partwise>
27 changes: 27 additions & 0 deletions tests/test_xml.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
MUSICXML_UNFOLD_COMPLEX,
MUSICXML_UNFOLD_VOLTA,
MUSICXML_UNFOLD_DACAPO,
MUSICXML_IGNORE_INVISIBLE_OBJECTS,
)

from partitura import load_musicxml, save_musicxml
Expand Down Expand Up @@ -256,6 +257,32 @@ def test_score_attribute(self):
self.assertTrue(score.work_title == test_work_title)
self.assertTrue(score.work_number == test_work_number)

def test_import_ignore_invisible_objects(self):
score_w_invisible = load_musicxml(MUSICXML_IGNORE_INVISIBLE_OBJECTS[0])[0]
score_wo_invisible = load_musicxml(MUSICXML_IGNORE_INVISIBLE_OBJECTS[0], ignore_invisible_objects=True)[0]

note_w_invisible_objs = score_w_invisible.note_array()
note_wo_invisible_objs = score_wo_invisible.note_array()

# Convert back from structured array to simple tuples as hash problems with set otherwise
note_w_invisible_objs = set(
[(n["pitch"], n["onset_beat"], n["duration_beat"]) for n in note_w_invisible_objs]
)
note_wo_invisible_objs = set(
[(n["pitch"], n["onset_beat"], n["duration_beat"]) for n in note_wo_invisible_objs]
)
# Make sure all notes in the filtered score are also in the unfiltered score
self.assertTrue(note_wo_invisible_objs.issubset(note_w_invisible_objs))

self.assertTrue(len(note_w_invisible_objs) == 11)
self.assertTrue(len(note_wo_invisible_objs) == 6)

self.assertTrue(len(score_w_invisible.rests) == 1)
self.assertTrue(len(score_wo_invisible.rests) == 0)

self.assertTrue(len(list(score_w_invisible.iter_all(cls=score.Beam))) == 1)
self.assertTrue(len(list(score_wo_invisible.iter_all(cls=score.Beam))) == 0)


def make_part_slur():
# create a part
Expand Down

0 comments on commit 0122040

Please sign in to comment.