"""MuseScore input interface."""
import time
import xml.etree.ElementTree as ET
from collections import OrderedDict
from fractions import Fraction
from functools import reduce
from operator import attrgetter
from os.path import basename
from pathlib import Path
from typing import Dict, List, Optional, Tuple, TypeVar, Union
from xml.etree.ElementTree import Element
from zipfile import ZipFile
from re import sub
from copy import deepcopy
import numpy as np
import warnings
from ..classes import *
from ..annotations import *
from ..music import Music, DEFAULT_RESOLUTION
from ..utils import (
CIRCLE_OF_FIFTHS,
MODE_CENTERS,
NOTE_TYPE_MAP,
TONAL_PITCH_CLASSES
)
T = TypeVar("T")
# number of octaves to shift from an ottava
OTTAVA_OCTAVE_SHIFT_FACTORS = {8: 1, 15: 2, 22: 3}
# OTTAVA_N_OCTAVE_SHIFTS = {
# "22mb": -3, "15mb": -2, "8vb": -1,
# "8va": 1, "15ma": 2, "22ma": 3,
# }
# OTTAVA_PITCH_SHIFTS = {key: value * 12 for key, value in OTTAVA_N_OCTAVE_SHIFTS.items()} # switch from octave shifts to pitch shifts
# transposing to concert key for tuned instruments
TRANSPOSE_CHROMATIC_TO_CIRCLE_OF_FIFTHS_STEPS = {-i: ((-i) if (i % 2 == 0) else ((-i + 6) % -12)) for i in range(12)}
# subtypes of arpeggios
ARPEGGIO_SUBTYPES = ["default", "up", "down", "bracket", "up straight", "down straight"]
# subtypes of chord lines
CHORDLINE_SUBTYPES = ["fall", "doit", "plop", "scoop", "slide out down", "slide out up", "slide in above", "slide in below"]
[docs]class MuseScoreError(Exception):
"""A class for MuseScore related errors."""
class MuseScoreWarning(Warning):
"""A class for MuseScore related warnings."""
def _gcd(a: int, b: int) -> int:
"""Return greatest common divisor using Euclid's Algorithm.
Code copied from https://stackoverflow.com/a/147539.
"""
while b:
a, b = b, a % b
return a
def _lcm_two_args(a: int, b: int) -> int:
"""Return least common multiple.
Code copied from https://stackoverflow.com/a/147539.
"""
return a * b // _gcd(a, b)
def _lcm(*args: int) -> int:
"""Return lcm of args.
Code copied from https://stackoverflow.com/a/147539.
"""
return reduce(_lcm_two_args, args) # type: ignore
def prettify_ET(root: ET, output_filepath: str = None): # prettify an xml element tree
"""Return a pretty-printed (or outputted) XML string for the Element `root`.
"""
rough_string = ET.tostring(element = root, encoding = "utf-8") # get string for xml
rough_string = str(rough_string, "UTF-8") # convert from type bytes to str
if output_filepath:
with open(output_filepath, "w") as file:
file.write(rough_string)
else:
print(rough_string)
def _get_required(element: Element, path: str) -> Element:
"""Return a required child element of an element.
Raise a MuseScoreError if not found.
"""
elem = element.find(path = path)
if elem is None:
raise MuseScoreError(f"Element `{path}` is required for an '{element.tag}' element.")
return elem
def _get_required_attr(element: Element, attr: str) -> str:
"""Return a required attribute of an element.
Raise a MuseScoreError if not found.
"""
attribute = element.get(attr)
if attribute is None:
raise MuseScoreError(f"Attribute '{attr}' is required for an '{element.tag}' element.")
return attribute
def _get_required_text(element: Element, path: str, remove_newlines: bool = False) -> str:
"""Return a required text from a child element of an element.
Raise a MuseScoreError otherwise.
"""
elem = _get_required(element = element, path = path)
if elem.text is None:
return ""
# raise MuseScoreError(f"Text content '{path}' of an element '{element.tag}' must not be empty.")
if remove_newlines:
return " ".join(elem.text.splitlines())
return elem.text
def _get_raw_text(element: Element, path: str = "text") -> str:
"""Returns the raw XML-level text in an element"""
# find the element
elem = element.find(path = path)
# if that element is not found
if elem is None:
return None
# wrangle the raw string
text = str(ET.tostring(element = elem, encoding = "utf-8"), encoding = "UTF-8").strip()
path = basename(path) if "/" in path else path
text = sub(pattern = f"<{path}>|</{path}>|<font face=\"Edwin\" />", repl = "", string = text)
return text
def _get_root(path: Union[str, Path], compressed: bool = None):
"""Return root of the element tree."""
path = str(path) # ensure path is a string
if compressed is None:
compressed = path.endswith(".mscz")
if not compressed:
tree = ET.parse(path)
return tree.getroot()
# Find out the main MSCX file in the compressed ZIP archive
try:
zip_file = ZipFile(file = path)
except: # zipfile.BadZipFile: File is not a zip file
raise MuseScoreError(f"{path} is not a zip file")
if "META-INF/container.xml" not in zip_file.namelist():
raise MuseScoreError("Container file ('container.xml') not found.")
container = ET.fromstring(zip_file.read("META-INF/container.xml")) # read the container file as xml elementtree
rootfile = container.findall(path = "rootfiles/rootfile") # find all branches in the container file, look for .mscx
rootfile = [file for file in rootfile if "mscx" in file.get("full-path")]
if len(rootfile) == 0:
raise MuseScoreError("Element 'rootfile' tag not found in the container file ('container.xml').")
filename = _get_required_attr(element = rootfile[0], attr = "full-path")
if filename in zip_file.namelist():
root = ET.fromstring(zip_file.read(filename))
else:
try:
root = ET.fromstring(zip_file.read(tuple(path for path in zip_file.namelist() if path.endswith(".mscx"))[0]))
except (IndexError):
raise MuseScoreError("No .mscx file could be found in .mscz.")
return root
def _get_divisions(root: Element) -> List[int]:
"""Return a list of divisions."""
divisions = []
for division in root.findall(path = "Division"):
if division.text is None:
continue
if not float(division.text).is_integer():
raise MuseScoreError(
"Noninteger 'division' values are not supported."
)
divisions.append(int(division.text))
return divisions
def parse_repeats(elem: Element) -> Tuple[list, list]:
"""Read repeats."""
# Initialize with a start marker
start_repeats = [0]
end_repeats = [[],]
# Find all repeats in all measures
for i, measure in enumerate(elem.findall(path = "Measure")):
# check for startRepeat
if measure.find(path = "startRepeat") is not None:
start_repeats.append(i)
end_repeats.append([])
# check for endRepeat
if measure.find(path = "endRepeat") is not None:
repeat_times = int(_get_text(element = measure, path = "endRepeat", default = 2)) - 1 # default is 2 because by default, a repeat causes a section to be played twice
end_repeats[len(start_repeats) - 1].extend([i] * repeat_times)
# if there is an implied repeat at the end
if len(end_repeats[-1]) == 0 and len(start_repeats) >= 2:
end_repeats[-1] = [i]
# remove faulty repeats (start repeats with no corresponding end repeats)
start_repeats_filtered, end_repeats_filtered = [], []
for i in range(len(end_repeats)):
if len(end_repeats[i]) > 0:
start_repeats_filtered.append(start_repeats[i])
end_repeats_filtered.append(end_repeats[i])
return start_repeats_filtered, end_repeats_filtered
def parse_markers(elem: Element) -> Dict[str, int]:
"""Return a marker-measure map parsed from a staff element."""
# Initialize with a start marker
markers: Dict[str, int] = {"start": 0}
# Find all markers in all measures
for i, measure in enumerate(elem.findall(path = "Measure")):
for marker in measure.findall(path = "Marker"):
label = _get_text(element = marker, path = "label")
if label is not None:
markers[label] = i
return markers
def get_measure_ordering(elem: Element, timeout: int = None) -> List[int]:
"""Return a list of measure indices parsed from a staff element.
This function returns the ordering of measures, considering all
repeats and jumps.
"""
# create measure indices list
measure_indicies = []
# Get the markers and repeats
markers = parse_markers(elem = elem)
start_repeats, end_repeats = parse_repeats(elem = elem)
voltas_encountered = [] # voltas that have been past already
current_repeat_idx = -1
current_end_repeat_idx = 0
# Record start time to check for timeout
if timeout is not None:
start_time = time.time()
# boolean flags
before_jump = True
add_current_measure_idx = True
# jump related indicies
jump_to_idx = None
play_until_idx = None
continue_at_idx = None
# Iterate over all measures
measures = elem.findall(path = "Measure")
measure_idx = 0
while measure_idx < len(measures):
# Check for timeout
if timeout is not None and time.time() - start_time > timeout:
raise TimeoutError(f"Abort the process as it runned over {timeout} seconds.")
# Get the measure element
measure = measures[measure_idx]
# jump
# v
# ║----|----|----|----|----|----|----║
# ^ ^ ^
# jump-to play-until continue at
# set default next measure id
next_measure_idx = measure_idx + 1
# look for start repeat
if measure_idx in start_repeats: # if this measure is the start of a repeat section
previous_repeat_idx = current_repeat_idx
current_repeat_idx = start_repeats.index(measure_idx)
if previous_repeat_idx != current_repeat_idx:
current_end_repeat_idx = 0
# look for jump
jump = measure.find(path = "Jump") # search for jump element
if jump is not None and before_jump: # if jump is found
# set jump related indicies
jump_to_idx = markers.get(_get_text(element = jump, path = "jumpTo"))
play_until_idx = markers.get(_get_text(element = jump, path = "playUntil"))
continue_at_idx = markers.get(_get_text(element = jump, path = "continueAt"))
# set boolean flags
before_jump = False
# reset some variables
voltas_encountered = []
# go to the jump to measure
next_measure_idx = jump_to_idx
# look for end repeat (if there are any)
if len(end_repeats) > 0:
if current_end_repeat_idx < len(end_repeats[current_repeat_idx]) and measure_idx == end_repeats[current_repeat_idx][current_end_repeat_idx]: # if there is an end repeat to look out for (still an outstanding end_repeat) and we are at the end repeat
next_measure_idx = start_repeats[current_repeat_idx]
current_end_repeat_idx += 1
# look for first, second, etc. endings
volta = measure.find(path = "voice/Spanner/Volta/..") # select the parent element
if volta is not None:
if measure_idx not in (encounter[0] for encounter in voltas_encountered):
volta_duration = _get_text(element = volta, path = "next/location/measures")
if volta_duration is not None:
voltas_encountered.append((measure_idx, int(volta_duration)))
else: # if we have already seen this volta, skip volta_duration measures ahead
for volta_duration in (encounter[1] for encounter in voltas_encountered if encounter[0] >= measure_idx):
measure_idx += volta_duration
next_measure_idx = measure_idx
add_current_measure_idx = False
# look for Coda / Fine markings
if not before_jump and measure_idx == play_until_idx:
if continue_at_idx: # Coda
next_measure_idx = continue_at_idx
else: # Fine
next_measure_idx = len(measures) # end the while loop at the end of this iteration
# add the current measure index to measure indicies if I am allowed to
if add_current_measure_idx:
measure_indicies.append(measure_idx)
# for debugging
# print(measure_idx + 1, end = " " if measure_idx + 1 == next_measure_idx else "\n")
else: # flick off the switch
add_current_measure_idx = True
# Proceed to the next measure
measure_idx = next_measure_idx
return measure_indicies
def get_improved_measure_ordering(measures: List[Element], measure_indicies: List[int]) -> List[int]:
"""Return a list of measure indices parsed from a staff element.
This function returns the ordering of measures, considering all
repeats and jumps, as well as part-specific repeats.
"""
# scrape measures for repeat counts
measure_repeat_counts = [0] * len(measure_indicies)
for i, measure_idx in enumerate(measure_indicies): # MuseScore 3.x default
measure = measures[measure_idx] # get the measure element
measure_repeat_count = _get_text(element = measure, path = "measureRepeatCount") # check if this measure is a repeat
if measure_repeat_count is not None:
measure_repeat_counts[i] = int(measure_repeat_count)
if sum(measure_repeat_counts) == 0: # MuseScore 1.x and 2.x
for i, measure_idx in enumerate(measure_indicies): # MuseScore 3.x default
measure = measures[measure_idx] # get the measure element
if measure.find(path = "voice/RepeatMeasure") is not None:
measure_repeat_counts[i] = 1
# chunk into blocks
i = len(measure_repeat_counts) - 1
while i >= 0:
if measure_repeat_counts[i] > 1:
measure_repeat_counts[(i + 1 - measure_repeat_counts[i]):(i + 1)] = [measure_repeat_counts[i]] * measure_repeat_counts[i]
i -= measure_repeat_counts[i]
else:
i -= 1
del i
# get improved measure indicies
measure_indicies_improved = [0] * len(measure_indicies)
for i in range(len(measure_indicies)):
j = i
while measure_repeat_counts[j] > 0:
j -= measure_repeat_counts[j]
if j < 0: # if j becomes an invalid index
j = 0 # set j to 0
break # break out of while loop
measure_indicies_improved[i] = measure_indicies[j]
# return improved measure indicies
return measure_indicies_improved
def print_measure_indicies(measure_indicies: List[int]):
"""Print the measure indicies in a readable way (new line for every jump)"""
# convert into measure numbers as opposed to indicies
measure_indicies = [measure_idx + 1 for measure_idx in measure_indicies]
# create empty output
measure_indicies_formatted = ["",] * len(measure_indicies)
for i in range(len(measure_indicies) - 1):
if measure_indicies[i] + 1 == measure_indicies[i + 1]:
measure_indicies_formatted[i] = f"{measure_indicies[i]} "
else:
measure_indicies_formatted[i] = f"{measure_indicies[i]}\n"
measure_indicies_formatted[-1] = str(measure_indicies[-1])
# print output
print(*measure_indicies_formatted, sep = "", end = "\n")
def get_beats(downbeat_times: List[int], time_signatures: List[TimeSignature], resolution: int = DEFAULT_RESOLUTION, is_sorted: bool = False) -> List[Beat]:
"""Return beats given downbeat positions and time signatures.
Parameters
----------
downbeat_times : sequence of int
Positions of the downbeats.
time_signatures : sequence of :class:`muspy.TimeSignature`
Time signature objects.
resolution : int, default: `muspy.DEFAULT_RESOLUTION` (24)
Time steps per quarter note.
is_sorted : bool, default: False
Whether the downbeat times and time signatures are sorted.
Returns
-------
list of :class:`read_musescore.Beat`
Computed beats.
"""
# Return a list of downbeats if no time signatures is given
if not time_signatures:
return [Beat(time = int(round(time)), is_downbeat = True) for time in downbeat_times]
# Sort the downbeats and time signatures if necessary
if not is_sorted:
downbeat_times = sorted(downbeat_times)
time_signatures = sorted(time_signatures, key = attrgetter("time"))
# Compute the beats
beats: List[Beat] = []
sign_idx = 0
downbeat_idx = 0
while downbeat_idx < len(downbeat_times):
# Select the correct time signatures
if sign_idx + 1 < len(time_signatures) and downbeat_times[downbeat_idx] < time_signatures[sign_idx + 1].time:
sign_idx += 1
continue
# Set time signature
time_sign = time_signatures[sign_idx]
beat_resolution = resolution / (time_sign.denominator / 4)
# Get the next downbeat
if downbeat_idx < len(downbeat_times) - 1:
end: float = downbeat_times[downbeat_idx + 1]
else:
end = downbeat_times[downbeat_idx] + (beat_resolution * time_sign.numerator)
# Append the downbeat
start = int(round(downbeat_times[downbeat_idx]))
beats.append(Beat(time = start, is_downbeat = True))
# Append beats
beat_times = np.arange(start = start + beat_resolution, stop = end, step = beat_resolution)
for time in beat_times:
beats.append(Beat(time = int(round(time)), is_downbeat = False))
downbeat_idx += 1
return beats
def parse_metadata(root: Element) -> Metadata:
"""Return a Metadata object parsed from a MuseScore file."""
# creators and copyrights
title, subtitle = None, None
creators = []
copyrights = []
# iterate over meta tags
for meta_tag in root.findall(path = "Score/metaTag"):
name = _get_required_attr(element = meta_tag, attr = "name")
if name == "movementTitle":
title = meta_tag.text
if name == "subtitle":
subtitle = meta_tag.text
# Only use 'workTitle' when movementTitle is not found
if title is None and name == "workTitle":
title = meta_tag.text
if name in ("arranger", "composer", "lyricist"):
if meta_tag.text is not None:
creators.append(meta_tag.text)
if name == "copyright":
if meta_tag.text is not None:
copyrights.append(meta_tag.text)
return Metadata(title = title, subtitle = subtitle, creators = creators, copyright = " ".join(copyrights) if copyrights else None, source_format = "musescore")
def parse_part_info(elem: Element, musescore_version: int) -> Tuple[Optional[List[str]], OrderedDict]:
"""Return part information parsed from a score part element."""
part_info: OrderedDict = OrderedDict()
# Staff IDs
staffs = elem.findall(path = "Staff")
if musescore_version >= 2:
staff_ids = [_get_required_attr(element = staff, attr = "id") for staff in staffs]
else: # MuseScore 1
staff_ids = list(range(1, len(staffs) + 1)) # MuseScore 1.x
# Instrument
instrument = _get_required(element = elem, path = "Instrument")
part_info["id"] = _get_text(element = instrument, path = "instrumentId", remove_newlines = True)
part_info["name"] = _get_text(element = elem, path = "trackName", remove_newlines = True)
part_info["transposeChromatic"] = int(_get_text(element = instrument, path = "transposeChromatic", remove_newlines = True, default = "0"))
# MIDI program and channel
program = instrument.find(path = "Channel/program")
if program is not None:
program = program.get("value")
part_info["program"] = int(program) if program is not None else 0
else:
part_info["program"] = 0
part_info["is_drum"] = ((int(_get_text(element = instrument, path = "Channel/midiChannel", default = 0)) == 9) or
(_get_text(element = instrument, path = "clef", default = "") == "PERC") or
("drum" in str(part_info["name"]).lower()) or
(len(instrument.findall(path = "Drum")) > 0))
return staff_ids, part_info
def get_part_staff_info(elem: Element, musescore_version: int) -> Tuple[List[OrderedDict], OrderedDict]:
"""Return part information and the mappings between staff and parts from a list of all the parts elements."""
# initialize return collections
parts_info: List[OrderedDict] = [] # for storing info on each part
staff_part_map: OrderedDict = OrderedDict() # for connecting staff ids (keys) to the part they belong to (values)
# iterate through the parts
for part_id, part in enumerate(elem):
# get part info
staff_ids, current_part_info = parse_part_info(elem = part, musescore_version = musescore_version) # get the part info
parts_info.append(current_part_info) # add the current part info to parts_info
# Deal with quirks of MuseScore 1
if musescore_version < 2:
current_max_staff_id = max((int(staff_id) for staff_id in staff_part_map.keys())) if len(staff_part_map.keys()) > 0 else 0 # get the current largest staff id
staff_ids = tuple(str(staff_id + current_max_staff_id) for staff_id in staff_ids) # adjust staff ids to total scale, not just within each part (essentially, convert MuseScore 1's lack of staff ids within parts to MuseScore>1)
# assign each staff id to a part
for staff_id in staff_ids:
staff_part_map[staff_id] = part_id
# return a value or raise an error
if not parts_info: # Raise an error if there is no Part information
raise MuseScoreError("Part information is missing (i.e. there are no parts).")
else:
return parts_info, staff_part_map
def get_musescore_version(path: Union[str, Path], compressed: bool = None) -> str:
"""Determine the version of a MuseScore file.
Parameters
----------
path : str or Path
Path to the MuseScore file to read.
compressed : bool, optional
Whether it is a compressed MuseScore file. Defaults to infer from the filename.
Returns
-------
:str:
Version of MuseScore
"""
# get element tree root
root = _get_root(path = path, compressed = compressed)
# detect MuseScore version
musescore_version = root.get("version")
return musescore_version
def _get_text(element: Element, path: str, default: T = None, remove_newlines: bool = False) -> Union[str, T]:
"""Return the text of the first matching element."""
elem = element.find(path = path)
if elem is not None and elem.text is not None:
if remove_newlines:
return " ".join(elem.text.splitlines())
return elem.text
return default # type: ignore
def parse_metronome(elem: Element) -> Optional[float]:
"""Return a qpm value parsed from a metronome element."""
beat_unit = _get_text(element = elem, path = "beat-unit")
if beat_unit is not None:
per_minute = _get_text(element = elem, path = "per-minute")
if per_minute is not None and beat_unit in NOTE_TYPE_MAP:
qpm = NOTE_TYPE_MAP[beat_unit] * float(per_minute)
if elem.find(path = "beat-unit-dot") is not None:
qpm *= 1.5
return qpm
return None
def parse_time(elem: Element) -> Tuple[int, int]:
"""Return the numerator and denominator of a time element."""
# Numerator
beats = _get_text(element = elem, path = "sigN")
if beats is None:
beats = _get_text(element = elem, path = "nom1")
if beats is None:
raise MuseScoreError("Neither 'sigN' nor 'nom1' element is found for a TimeSig element.")
if "+" in beats:
numerator = sum(int(beat) for beat in beats.split("+"))
else:
numerator = int(beats)
# Denominator
beat_type = _get_text(element = elem, path = "sigD")
if beat_type is None:
beat_type = _get_text(element = elem, path = "den")
if beat_type is None:
raise MuseScoreError("Neither 'sigD' nor 'den' element is found for a TimeSig element.")
if "+" in beat_type:
raise RuntimeError("Compound time signatures with separate fractions are not supported.")
denominator = int(beat_type)
return numerator, denominator
def parse_key(elem: Element) -> Tuple[int, str, int, str]:
"""Return the key parsed from a key element."""
mode = _get_text(element = elem, path = "mode")
fifths_text = _get_text(element = elem, path = "accidental") # MuseScore 2.x and 3.x
if fifths_text is None:
fifths_text = _get_text(element = elem, path = "subtype") # MuseScore 1.x
if fifths_text is None:
fifths_text = _get_text(element = elem, path = "concertKey") # last
if fifths_text is None:
return None, None, None, None
# raise MuseScoreError("'accidental', 'subtype', or 'concertKey' subelement not found for KeySig element.")
fifths = int(fifths_text)
if mode is None:
return None, None, fifths, None
idx = MODE_CENTERS[mode] + fifths
if idx < 0 or idx > 20:
return None, mode, fifths, None # type: ignore
root, root_str = CIRCLE_OF_FIFTHS[MODE_CENTERS[mode] + fifths]
return root, mode, fifths, root_str
def parse_lyric(elem: Element) -> str:
"""Return the lyric text parsed from a lyric element."""
text = _get_required_text(element = elem, path = "text")
syllabic = elem.find(path = "syllabic")
if syllabic is not None:
if syllabic.text == "begin":
text = f"{text} -"
elif syllabic.text == "middle":
text = f"- {text} -"
elif syllabic.text == "end":
text = f"- {text}"
return text
def get_spanner_duration(spanner: Element, measure_len: int) -> int:
"""Returns the duration (in universal time) of a spanner element."""
# get the duration (in measures) of the spanner
spanner_duration = _get_text(element = spanner, path = "next/location/measures") # the duration of the spanner
if spanner_duration is None: # if next/location/measures is not found
return 0
spanner_duration = float(spanner_duration) # convert to float
fractions = _get_text(element = spanner, path = "next/location/fractions")
if fractions is not None:
spanner_duration += float(Fraction(fractions))
# convert spanner to int
# spanner_duration = round(measure_len * spanner_duration) if spanner_duration > 0 else measure_len
spanner_duration = round(measure_len * spanner_duration)
return spanner_duration
def parse_constant_features(
staff: Element,
resolution: int,
measure_indicies: List[int],
timeout: int = None
) -> Tuple[List[Tempo], List[KeySignature], List[TimeSignature], List[Barline], List[Beat], List[Annotation]]:
"""Return data parsed from a meta staff element.
This function only parses the tempos, key and time signatures. Use `parse_staff` to parse the notes and lyrics.
"""
# initialize lists
tempos: List[Tempo] = []
key_signatures: List[KeySignature] = []
time_signatures: List[TimeSignature] = []
barlines: List[Barline] = []
annotations: List[Annotation] = []
# Initialize variables
time_ = 0
measure_len = round(resolution * 4)
measure_len_fraction = 1.0
is_tuple = False
notes_left_in_tuple = 0
downbeat_times: List[int] = []
# record start time to check for timeout
if timeout is not None:
start_time = time.time()
# Iterate over all elements
measures = staff.findall(path = "Measure")
measure_indicies_improved = get_improved_measure_ordering(measures = measures, measure_indicies = measure_indicies)
for measure_idx, measure_idx_to_actually_read in zip(measure_indicies, measure_indicies_improved):
# Check for timeout
if timeout is not None and time.time() - start_time > timeout:
raise TimeoutError(f"Abort the process as it runned over {timeout} seconds.")
# Get the measure element
measure = measures[measure_idx_to_actually_read]
is_measure_written_out = (measure_idx == measure_idx_to_actually_read)
# Barlines, check for special types
barline_subtype = _get_text(element = measure, path = "voice/BarLine/subtype")
barline = Barline(time = time_, subtype = barline_subtype)
barlines.append(barline)
# Collect the measure start times
downbeat_times.append(time_)
# Get measure duration, but we don't want to recalculate every measure
measure_len_text = measure.get("len")
if measure_len_text is not None:
measure_len_fraction = float(Fraction(measure_len_text))
# get voice elements
voices = list(measure.findall(path = "voice")) # MuseScore 3.x, 4.x
if not voices:
voices = [measure] # MuseScore 1.x and 2.x
# Initialize position
# max_position = float("-inf")
# Iterate over voice elements
for voice in voices:
# Reset position
position = 0
# Iterate over child elements
for elem in voice:
# Key signatures
if is_measure_written_out and elem.tag == "KeySig":
root, mode, fifths, root_str = parse_key(elem = elem)
if fifths is not None:
key_signatures.append(KeySignature(time = time_ + position, root = root, mode = mode, fifths = fifths, root_str = root_str))
# Time signatures
elif is_measure_written_out and elem.tag == "TimeSig":
numerator, denominator = parse_time(elem = elem)
measure_len = round((resolution / (denominator / 4)) * numerator)
time_signatures.append(TimeSignature(time = time_ + position, numerator = numerator, denominator = denominator))
del numerator, denominator
# Tempo elements
elif is_measure_written_out and elem.tag == "Tempo":
tempo_qpm = 60 * float(_get_required_text(element = elem, path = "tempo"))
tempo_text = _get_raw_text(element = elem)
tempos.append(Tempo(time = time_ + position, qpm = tempo_qpm, text = tempo_text))
# Tempo spanner elements
elif is_measure_written_out and elem.tag == "Spanner" and elem.get("type") == "GradualTempoChange" and elem.find(path = "next/location/measures") is not None:
tempo_spanner_duration = get_spanner_duration(spanner = elem, measure_len = measure_len)
if tempo_spanner_duration > 0:
annotations.append(Annotation(time = time_ + position, annotation = TempoSpanner(duration = tempo_spanner_duration, subtype = _get_raw_text(element = elem, path = "GradualTempoChange/tempoChangeType"))))
del tempo_spanner_duration
# Text spanner elements (system)
elif is_measure_written_out and elem.tag == "Spanner" and elem.get("type") == "TextLine" and elem.find(path = "next/location/measures") is not None:
text_line_is_system = "system" in elem.find(path = "TextLine").attrib.keys()
if text_line_is_system: # only append the text spanner if it is system text
text_spanner_duration = get_spanner_duration(spanner = elem, measure_len = measure_len)
if text_spanner_duration > 0:
annotations.append(Annotation(time = time_ + position, annotation = TextSpanner(duration = text_spanner_duration, text = _get_required_text(element = elem, path = "TextLine/beginText"), is_system = text_line_is_system)))
del text_spanner_duration
# System Text
elif is_measure_written_out and elem.tag == "SystemText": # save staff text for other function
annotations.append(Annotation(time = time_ + position, annotation = Text(text = _get_raw_text(element = elem, path = "text"), is_system = True, style = _get_raw_text(element = elem, path = "style"))))
# Rehearsal Mark
elif is_measure_written_out and elem.tag == "RehearsalMark":
annotations.append(Annotation(time = time_ + position, annotation = RehearsalMark(text = _get_raw_text(element = elem))))
# Fermatas
elif elem.tag == "Fermata":
annotations.append(Annotation(time = time_ + position, annotation = Fermata(is_fermata_above = (_get_text(element = elem, path = "subtype") == "fermataAbove"))))
# Location elements
elif elem.tag == "location":
duration = resolution * 4 * float(Fraction(_get_required_text(element = elem, path = "fractions")))
position += duration
# Tuplet elements
elif elem.tag == "Tuplet":
is_tuple = True
normal_notes = int(_get_required_text(element = elem, path = "normalNotes"))
actual_notes = int(_get_required_text(element = elem, path = "actualNotes"))
tuple_ratio = normal_notes / actual_notes
notes_left_in_tuple = actual_notes
# Handle last tuplet note
elif elem.tag == "endTuplet" or (is_tuple and notes_left_in_tuple == 0):
old_duration = round(NOTE_TYPE_MAP[duration_type] * resolution)
new_duration = normal_notes * old_duration - (actual_notes - 1) * round(old_duration * tuple_ratio)
if duration != new_duration:
position += int(new_duration - duration)
is_tuple = False
# Rest elements
elif elem.tag == "Rest":
# move time position forward if it is a rest
duration_type = _get_required_text(element = elem, path = "durationType")
if duration_type == "measure":
duration_text = _get_text(element = elem, path = "duration")
if duration_text is not None:
duration = (resolution * 4 * float(Fraction(duration_text)))
else:
duration = measure_len
position += round(duration)
# get duration, taking into account dots and tuples
else:
duration = NOTE_TYPE_MAP[duration_type] * resolution
dots = elem.find(path = "dots")
if dots is not None and dots.text:
duration *= 2 - 0.5 ** int(dots.text)
if is_tuple:
duration *= tuple_ratio
position += round(duration)
# Chord elements
elif elem.tag == "Chord":
# Compute duration
duration_type = _get_required_text(element = elem, path = "durationType")
duration = NOTE_TYPE_MAP[duration_type] * resolution
# Handle tuplets
if is_tuple:
duration *= tuple_ratio
notes_left_in_tuple -= 1
# Handle dots
dots = elem.find(path = "dots")
if dots is not None and dots.text:
duration *= 2 - 0.5 ** int(dots.text)
# Round the duration
duration = round(duration)
# Grace notes
is_grace = False
for child in elem:
if "grace" in child.tag or child.tag in ("appoggiatura", "acciaccatura"):
is_grace = True
# update position
if not is_grace:
position += duration
# update positions
# if position > max_position:
# max_position = position
# update time
# time_ += max_position # get the maximum position (don't want some unfinished voice)
time_ += round(measure_len_fraction * measure_len)
# Sort tempos, key and time signatures
tempos.sort(key = attrgetter("time"))
key_signatures.sort(key = attrgetter("time"))
time_signatures.sort(key = attrgetter("time"))
annotations.sort(key = attrgetter("time"))
# Get the beats
beats = get_beats(downbeat_times = downbeat_times, time_signatures = time_signatures, resolution = resolution, is_sorted = True)
return tempos, key_signatures, time_signatures, barlines, beats, annotations
def parse_staff(
staff: Element,
resolution: int,
measure_indicies: List[int],
timeout: int = None,
part_info: OrderedDict = OrderedDict([("transposeChromatic", 0)])
) -> Tuple[List[Note], List[Chord], List[Lyric], List[Annotation]]:
"""Return notes and lyrics parsed from a staff element.
This function only parses the notes and lyrics. Use
`parse_constant_features` to parse the tempos, key and time
signatures.
"""
# Initialize lists
notes: List[Note] = []
chords: List[Chord] = []
lyrics: List[Lyric] = []
annotations: List[Annotation] = []
# Initialize variables
time_ = 0
velocity = 64
measure_len = round(resolution * 4)
measure_len_fraction = 1.0
is_tuple = False
notes_left_in_tuple = 0
ottava_shift = 0
ottava_end = float("inf")
transpose_circle_of_fifths = TRANSPOSE_CHROMATIC_TO_CIRCLE_OF_FIFTHS_STEPS[part_info["transposeChromatic"] % -12] # number of semitones to transpose so that chord symbols are concert pitch
# Record start time to check for timeout
if timeout is not None:
start_time = time.time()
# Create a dictionary to handle ties
ties: Dict[int, int] = {}
# Iterate over all elements
measures = staff.findall(path = "Measure")
measure_indicies_improved = get_improved_measure_ordering(measures = measures, measure_indicies = measure_indicies)
for measure_idx, measure_idx_to_actually_read in zip(measure_indicies, measure_indicies_improved):
# Check for timeout
if timeout is not None and time.time() - start_time > timeout:
raise TimeoutError(f"Abort the process as it runned over {timeout} seconds.")
# Get the measure element
measure = measures[measure_idx_to_actually_read]
is_measure_written_out = (measure_idx == measure_idx_to_actually_read)
# Get measure duration, but we don't want to recalculate every measure
measure_len_text = measure.get("len")
if measure_len_text is not None:
measure_len_fraction = float(Fraction(measure_len_text))
# Get voice elements
voices = list(measure.findall(path = "voice")) # MuseScore 3.x
if not voices:
voices = [measure] # MuseScore 1.x and 2.x
# Initialize position
# max_position = float("-inf")
# Iterate over voice elements
for voice in voices:
# Initialize position
position = 0
# Iterate over child elements
for elem in voice:
# check to see if we reset ottava shift
if (ottava_shift != 0) and ((time_ + position) >= ottava_end):
ottava_shift = 0
# Dynamic elements
if elem.tag == "Dynamic":
velocity = int(round(float(_get_text(element = elem, path = "velocity", default = velocity))))
annotations.append(Annotation(time = time_ + position, annotation = Dynamic(subtype = _get_required_text(element = elem, path = "subtype"), velocity = velocity)))
# Hairpin elements
elif elem.tag == "Spanner" and elem.get("type") == "HairPin" and elem.find(path = "next/location/measures") is not None:
hairpin_duration = get_spanner_duration(spanner = elem, measure_len = measure_len)
if hairpin_duration > 0:
annotations.append(Annotation(time = time_ + position, annotation = HairPinSpanner(duration = hairpin_duration, subtype = _get_text(element = elem, path = "HairPin/beginText"), hairpin_type = int(_get_required_text(element = elem, path = "HairPin/subtype")))))
del hairpin_duration
# Text spanner elements (staff)
elif elem.tag == "Spanner" and elem.get("type") == "TextLine" and elem.find(path = "next/location/measures") is not None:
text_line_is_system = "system" in elem.find(path = "TextLine").attrib.keys()
if not text_line_is_system: # only append the text spanner if it is staff text
text_spanner_duration = get_spanner_duration(spanner = elem, measure_len = measure_len)
if text_spanner_duration > 0:
annotations.append(Annotation(time = time_ + position, annotation = TextSpanner(duration = text_spanner_duration, text = _get_raw_text(element = elem, path = "TextLine/beginText"), is_system = text_line_is_system)))
del text_spanner_duration
# Staff Text
elif elem.tag == "StaffText":
annotations.append(Annotation(time = time_ + position, annotation = Text(text = _get_raw_text(element = elem), is_system = False, style = _get_raw_text(element = elem, path = "style"))))
# Pedals
elif elem.tag == "Spanner" and elem.get("type") == "Pedal" and elem.find(path = "next/location/measures") is not None:
pedal_duration = get_spanner_duration(spanner = elem, measure_len = measure_len)
if pedal_duration > 0:
annotations.append(Annotation(time = time_ + position, annotation = PedalSpanner(duration = pedal_duration)))
del pedal_duration
# Trill Spanners
elif elem.tag == "Spanner" and elem.get("type") == "Trill" and elem.find(path = "next/location/measures") is not None:
trill_duration = get_spanner_duration(spanner = elem, measure_len = measure_len)
if trill_duration > 0:
annotations.append(Annotation(time = time_ + position, annotation = TrillSpanner(duration = trill_duration, subtype = _get_text(element = elem, path = "Trill/subtype"), ornament = _get_text(element = elem, path = "Trill/Ornament/subtype"))))
del trill_duration
# Vibrato Spanners
elif elem.tag == "Spanner" and elem.get("type") == "Vibrato" and elem.find(path = "next/location/measures") is not None:
vibrato_duration = get_spanner_duration(spanner = elem, measure_len = measure_len)
if vibrato_duration > 0:
annotations.append(Annotation(time = time_ + position, annotation = VibratoSpanner(duration = vibrato_duration, subtype = _get_text(element = elem, path = "Vibrato/subtype"))))
del vibrato_duration
# Glissando Spanners
elif elem.tag == "Spanner" and elem.get("type") == "Glissando" and elem.find(path = "next/location/measures") is not None:
glissando_duration = get_spanner_duration(spanner = elem, measure_len = measure_len)
if glissando_duration > 0:
annotations.append(Annotation(time = time_ + position, annotation = GlissandoSpanner(duration = glissando_duration, is_wavy = bool(_get_text(element = elem, path = "Trill/subtype")))))
del glissando_duration
# Ottava Spanners
elif elem.tag == "Spanner" and elem.get("type") == "Ottava" and elem.find(path = "next/location/measures") is not None:
ottava_time = time_ + position
ottava_duration = get_spanner_duration(spanner = elem, measure_len = measure_len)
ottava_end = ottava_time + ottava_duration
ottava_subtype = _get_text(element = elem, path = "Ottava/subtype")
ottava_shift = 12 * OTTAVA_OCTAVE_SHIFT_FACTORS[int(sub(pattern = "[^0-9]", repl = "", string = ottava_subtype))] * (-1 if "b" in ottava_subtype else 1)
if ottava_duration > 0:
annotations.append(Annotation(time = ottava_time, annotation = OttavaSpanner(duration = ottava_duration, subtype = ottava_subtype)))
del ottava_time, ottava_duration, ottava_subtype
# Technical Annotation
elif elem.tag == "PlayTechAnnotation":
annotations.append(Annotation(time = time_ + position, annotation = TechAnnotation(text = _get_raw_text(element = elem), tech_type = _get_raw_text(element = elem, path = "playTechType"), is_system = False)))
# Tremolo Bar
elif elem.tag == "TremoloBar":
annotations.append(Annotation(time = time_ + position, annotation = TremoloBar(points = [Point(time = int(point.get("time")), pitch = int(point.get("pitch")), vibrato = int(point.get("vibrato"))) for point in elem.findall("point")])))
# Chord Symbol
elif elem.tag == "Harmony":
root = _get_text(element = elem, path = "root")
if root is not None:
root_str = TONAL_PITCH_CLASSES[int(root) + transpose_circle_of_fifths]
name = _get_text(element = elem, path = "name")
name = name if name is not None else "Maj"
annotations.append(Annotation(time = time_ + position, annotation = ChordSymbol(root_str = root_str, name = name)))
del root_str, name
del root
# Time signatures to keep track of for incrementing position
elif is_measure_written_out and elem.tag == "TimeSig":
numerator, denominator = parse_time(elem = elem)
measure_len = round((resolution / (denominator / 4)) * numerator)
del numerator, denominator
# Location elements
elif elem.tag == "location":
duration = resolution * 4 * float(Fraction(_get_required_text(element = elem, path = "fractions")))
position += duration
# Tuplet elements
elif elem.tag == "Tuplet":
is_tuple = True
normal_notes = int(_get_required_text(element = elem, path = "normalNotes"))
actual_notes = int(_get_required_text(element = elem, path = "actualNotes"))
tuple_ratio = normal_notes / actual_notes
notes_left_in_tuple = actual_notes
# Handle last tuplet note
elif elem.tag == "endTuplet" or (is_tuple and notes_left_in_tuple == 0):
old_duration = round(NOTE_TYPE_MAP[duration_type] * resolution)
new_duration = normal_notes * old_duration - (actual_notes - 1) * round(old_duration * tuple_ratio)
if notes[-1].duration != new_duration:
notes[-1].duration = new_duration
position += int(new_duration - duration)
is_tuple = False
# Rest elements
elif elem.tag == "Rest":
# move time position forward if it is a rest
duration_type = _get_required_text(element = elem, path = "durationType")
if duration_type == "measure":
duration_text = _get_text(element = elem, path = "duration")
if duration_text is not None:
duration = (resolution * 4 * float(Fraction(duration_text)))
else:
duration = measure_len
position += round(duration)
# get duration, taking into account dots and tuples
else:
duration = NOTE_TYPE_MAP[duration_type] * resolution
dots = elem.find(path = "dots")
if dots is not None and dots.text:
duration *= 2 - 0.5 ** int(dots.text)
if is_tuple:
duration *= tuple_ratio
position += round(duration)
# Chord elements
elif elem.tag == "Chord":
# Compute duration
duration_type = _get_required_text(element = elem, path = "durationType")
duration = NOTE_TYPE_MAP[duration_type] * resolution
# store notations on the chord
chord_notations = []
# Handle tuplets
if is_tuple:
duration *= tuple_ratio
notes_left_in_tuple -= 1
# Handle dots
dots = elem.find(path = "dots")
if dots is not None and dots.text:
duration *= 2 - 0.5 ** int(dots.text)
# Round the duration
duration = round(duration)
# Grace notes
is_grace = False
for child in elem:
if "grace" in child.tag or child.tag in ("appoggiatura", "acciaccatura"):
is_grace = True
# check for slurs and ties
is_outgoing_tie = False
for spanner in elem.findall(path = "Spanner"):
# Check if it is a tied chord
if spanner.get("type") == "Tie" and ((spanner.find(path = "next/location/measures") is not None) or (spanner.find(path = "next/location/fractions") is not None)):
is_outgoing_tie = True
# Check for any slurs
elif spanner.get("type") == "Slur" and spanner.find(path = "next/location/measures") is not None:
slur_duration = get_spanner_duration(spanner = spanner, measure_len = measure_len)
if slur_duration > 0:
annotations.append(Annotation(time = time_ + position, annotation = SlurSpanner(duration = slur_duration, is_slur = True)))
del slur_duration
# Check for ornament
if elem.find(path = "Ornament") is not None:
chord_notations.append(Ornament(subtype = _get_text(element = elem, path = "Ornament/subtype")))
# Check for arpeggio
if elem.find(path = "Arpeggio") is not None:
arpeggio_subtype = int(_get_text(element = elem, path = "Arpeggio/subtype"))
arpeggio_subtype = ARPEGGIO_SUBTYPES[arpeggio_subtype if (arpeggio_subtype > 0 and arpeggio_subtype < len(ARPEGGIO_SUBTYPES)) else 0] # convert to stsring
annotations.append(Annotation(time = time_ + position, annotation = Arpeggio(subtype = arpeggio_subtype)))
# Check for tremolo
if elem.find(path = "Tremolo") is not None:
annotations.append(Annotation(time = time_ + position, annotation = Tremolo(subtype = _get_text(element = elem, path = "Tremolo/subtype"))))
# Check for articulation
if elem.find(path = "Articulation") is not None:
for articulation_subtype in elem.findall(path = "Articulation/subtype"): # in the case of multiple articulations
chord_notations.append(Articulation(subtype = articulation_subtype.text))
# Lyrics
for lyric in elem.findall(path = "Lyrics"):
lyric_text = parse_lyric(elem = lyric)
lyrics.append(Lyric(time = time_ + position, lyric = lyric_text))
# Collect notes
# chord = Chord(time = time_ + position, pitches = [], duration = duration, velocity = velocity, pitches_str = [], is_grace = is_grace)
for note in elem.findall(path = "Note"):
# notations on this specific note
note_notations = deepcopy(chord_notations)
# Get pitch
pitch = int(_get_required_text(element = note, path = "pitch")) + ottava_shift
pitch_str = TONAL_PITCH_CLASSES[int(_get_required_text(element = note, path = "tpc"))]
# Check for bend
if note.find(path = "Bend") is not None:
note_notations.append(Bend(points = [Point(time = int(point.get("time")), pitch = int(point.get("pitch")), vibrato = int(point.get("vibrato"))) for point in note.findall("Bend/point")]))
# Check for ChordLines (falls, doits, scoops, etc.)
if note.find(path = "ChordLine") is not None:
subtype = int(_get_required_text(element = note, path = "ChordLine/subtype"))
subtype = CHORDLINE_SUBTYPES[subtype if subtype in range(len(CHORDLINE_SUBTYPES)) else 0]
note_notations.append(ChordLine(subtype = subtype, is_straight = bool(_get_text(element = note, path = "ChordLine/straight"))))
del subtype
# get notehead
if note.find(path = "head") is not None:
note_notations.append(Notehead(subtype = _get_required_text(element = note, path = "head")))
# get symbol(s)
if note.find(path = "Symbol/name") is not None:
for symbol_name in note.findall(path = "Symbol/name"): # in the case of multiple articulations
note_notations.append(Symbol(subtype = symbol_name.text))
# Handle grace note
if is_grace:
notes.append(Note(time = time_ + position, pitch = pitch, duration = duration, velocity = velocity, pitch_str = pitch_str, is_grace = True, notations = note_notations if len(note_notations) > 0 else None))
# chord.pitches.append(pitch)
# chord.pitches_str.append(pitch_str)
continue
# Check if it is a tied note
for spanner in note.findall(path = "Spanner"): # MuseScore 3.x
if spanner.get("type") == "Tie" and ((spanner.find(path = "next/location/measures") is not None) or (spanner.find(path = "next/location/fractions") is not None)):
is_outgoing_tie = True
if note.find(path = "Tie") is not None: # MuseScore 1.x and 2.x
is_outgoing_tie = True
# Check if it is an incoming tied note
if pitch in ties.keys():
note_idx = ties[pitch]
notes[note_idx].duration += duration
if is_outgoing_tie: # if the tie continues
ties[pitch] = note_idx
else: # if the tie ended
del ties[pitch]
# Append a new note to the note list
else:
notes.append(Note(time = time_ + position, pitch = pitch, duration = duration, velocity = velocity, pitch_str = pitch_str, is_grace = False, notations = note_notations if len(note_notations) > 0 else None))
# chord.pitches.append(pitch)
# chord.pitches_str.append(pitch_str)
if is_outgoing_tie: # start of a tie, make note of it
ties[pitch] = len(notes) - 1
# update position
if not is_grace: # is a normal note that time acts on normally
position += duration
# add chord if there is stuff to add
# if len(chord.pitches) > 0:
# chords.append(chord)
# del chord
# update positions
# if position > max_position:
# max_position = position
# update time
# time_ += max_position # get the maximum position (don't want some unfinished voice)
time_ += round(measure_len_fraction * measure_len)
# Sort notes
notes.sort(key = attrgetter("time", "pitch", "duration", "velocity"))
# Sort chords
chords.sort(key = attrgetter("time", "duration", "velocity"))
# Sort lyrics
lyrics.sort(key = attrgetter("time"))
# Sort annotations
annotations.sort(key = attrgetter("time"))
return notes, chords, lyrics, annotations
[docs]def read_musescore(path: Union[str, Path], resolution: int = None, compressed: bool = None, timeout: int = None) -> Music:
"""Read the MuseScore file into a Music object, paying close attention to details such as articulation and expressive features.
Parameters
----------
path : str or Path
Path to the MuseScore file to read.
resolution : int, optional
Time steps per quarter note. Defaults to the least common
multiple of all divisions.
compressed : bool, optional
Whether it is a compressed MuseScore file. Defaults to infer
from the filename.
Returns
-------
:class:`Music`
Converted Music object.
"""
# get element tree for MuseScore file
path = str(path)
root = _get_root(path = path, compressed = compressed)
# ET.ElementTree(root).write(f"{dirname(path)}/mscx.xml")
# detect MuseScore version
musescore_version = int(float(root.get("version"))) # the file format differs slightly between musescore 1 and the rest
if musescore_version < 3:
warnings.warn(
f"Detected a legacy MuseScore version of {musescore_version}. "
"Data might not be loaded correctly.",
MuseScoreWarning,
)
# get the score element
if musescore_version >= 2:
score = root.find(path = "Score")
else: # MuseScore 1
score = root # No "Score" tree
# metadata
metadata = parse_metadata(root = root)
metadata.source_filename = basename(path)
# find the resolution
if resolution is None:
divisions = _get_divisions(root = score)
resolution = max(divisions, key = divisions.count) if len(divisions) > 0 else muspy.DEFAULT_RESOLUTION
# staff/part information
parts = score.findall(path = "Part") # get all the parts
parts_info, staff_part_map = get_part_staff_info(elem = parts, musescore_version = musescore_version)
# get all the staff elements
staffs = score.findall(path = "Staff")
if len(staffs) == 0: # Return empty music object with metadata if no staff is found
return Music(metadata = metadata, resolution = resolution)
# parse measure ordering from the meta staff, expanding all repeats and jumps
measure_indicies = get_measure_ordering(elem = staffs[0], timeout = timeout) # feed in the first staff, since measure ordering are constant across all staffs
# parse the part element
(tempos, key_signatures, time_signatures, barlines, beats, annotations) = parse_constant_features(staff = staffs[0], resolution = resolution, measure_indicies = measure_indicies, timeout = timeout) # feed in the first staff to extract features constant across all parts
# initialize lists
tracks: List[Track] = []
# record start time to check for timeout
start_time = time.time()
# iterate over all staffs
part_track_map: Dict[int, int] = {} # keeps track of parts we have already looked at
for staff in staffs:
# check for timeout
if timeout is not None and time.time() - start_time > timeout:
raise TimeoutError(f"Abort the process as it runned over {timeout} seconds.")
# get the staff ID
staff_id = staff.get("id")
if staff_id is None:
if len(score.findall(path = "Staff")) > 1:
continue
staff_id = next(iter(staff_part_map))
if staff_id not in staff_part_map:
continue
# parse the staff, extend/append to lists
part_id = staff_part_map[staff_id]
(notes, chords, lyrics, annotations_staff) = parse_staff(staff = staff, resolution = resolution, measure_indicies = measure_indicies, timeout = timeout, part_info = parts_info[part_id])
if part_id in part_track_map:
track_id = part_track_map[part_id]
tracks[track_id].notes.extend(notes)
tracks[track_id].chords.extend(chords)
tracks[track_id].lyrics.extend(lyrics)
tracks[track_id].annotations.extend(annotations_staff)
else:
part_track_map[part_id] = len(tracks)
tracks.append(Track(program = parts_info[part_id]["program"], is_drum = parts_info[part_id]["is_drum"], name = parts_info[part_id]["name"], notes = notes, chords = chords, lyrics = lyrics, annotations = annotations_staff))
# make sure everything is sorted
tempos.sort(key = attrgetter("time"))
key_signatures.sort(key = attrgetter("time"))
time_signatures.sort(key = attrgetter("time"))
annotations.sort(key = attrgetter("time"))
for track in tracks:
track.notes.sort(key = attrgetter("time", "pitch", "duration", "velocity"))
track.chords.sort(key = attrgetter("time", "duration", "velocity"))
track.lyrics.sort(key = attrgetter("time"))
track.annotations.sort(key = attrgetter("time"))
return Music(metadata = metadata, resolution = resolution, tempos = tempos, key_signatures = key_signatures, time_signatures = time_signatures, barlines = barlines, beats = beats, tracks = tracks, annotations = annotations)