SHARE
TWEET

Untitled

a guest Oct 23rd, 2019 74 Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. #!/usr/bin/env python
  2.  
  3. """
  4. Convert additional staves in musicxml scores into ossias. This is useful for making scores
  5. searchable by search.py.
  6.  
  7. In particular, this script takes a 2-staff file and adds the following code to it:
  8. <staff-details number="1">
  9.   <staff-type>ossia</staff-type>
  10. </staff-details>
  11.  
  12. This marks staff #1 as an ossia, so that it is recognized as a variant by search.py, so search.py
  13. can find substrings in and around the variant.
  14.  
  15. Installation:
  16. * Clone the repo
  17. * Make a virtualenv with python3 and pip install lxml
  18. """
  19.  
  20.  
  21. from argparse import ArgumentParser
  22. from io import BytesIO
  23. from zipfile import ZipFile
  24.  
  25. # There's no way to preserve xml and doctype declarations with xml.etree
  26. from lxml import etree as ET
  27.  
  28.  
  29. def parse_args():
  30.     parser = ArgumentParser(description="""
  31.         Convert a 2-staff musicxml score into a score with a single main staff and an ossia staff,
  32.         as annotated by the <staff-details> element. The convention followed by this script is
  33.         that the first staff is the ossia staff and the second is the main staff.
  34.     """)
  35.  
  36.     parser.add_argument('file', help="File to modify")
  37.     parser.add_argument('-t', '--test', action='store_true',
  38.         help="Show the modified musicxml instead of saving it")
  39.    
  40.     return parser.parse_args()
  41.  
  42.  
  43. def add_ossia_marker(tree):
  44.     attributes = tree.getroot().find('./part/measure/attributes')
  45.    
  46.     num_staves = int(attributes.find('staves').text)
  47.     if num_staves != 2:
  48.         raise ValueError(f"Expected only 2 staves, got {num_staves}")
  49.  
  50.     last_clef_idx = [i for i, el in enumerate(attributes) if el.tag == 'clef'][-1]
  51.     attributes.insert(last_clef_idx + 1, ET.fromstring(
  52.         '<staff-details number="1"><staff-type>ossia</staff-type></staff-details>'
  53.     ))
  54.  
  55.  
  56. def tree2str(tree):
  57.     # This appears to be the only way to preserve the xml and doctype declarations
  58.     buffer = BytesIO()
  59.     tree.write(buffer, xml_declaration=True, encoding=tree.docinfo.encoding)
  60.     return buffer.getvalue().decode('utf8')
  61.  
  62.  
  63. def main():
  64.     args = parse_args()
  65.    
  66.     with ZipFile(args.file) as zipf:
  67.         maybe_fnames = [f for f in zipf.namelist() if not f.startswith('META-INF/')]
  68.         assert len(maybe_fnames) == 1, f"More than one relevant file found in {args.file}! Malformed mxl file?"
  69.         fname = maybe_fnames[0]
  70.        
  71.         with zipf.open(fname) as f:
  72.             tree = ET.parse(f)
  73.  
  74.     add_ossia_marker(tree)
  75.  
  76.     if args.test:
  77.         print(tree2str(tree))
  78.     else:
  79.         # NB: this drops the META-INF member of the zip archive. Nobody cares.
  80.         with ZipFile(args.file, 'w') as zipf:
  81.             zipf.writestr(fname, tree2str(tree))
  82.  
  83.  
  84.  
  85. if __name__ == '__main__':
  86.     main()
RAW Paste Data
We use cookies for various purposes including analytics. By continuing to use Pastebin, you agree to our use of cookies as described in the Cookies Policy. OK, I Understand
 
Top