diff --git a/.gitlab-ci.yaml b/.gitlab-ci.yaml new file mode 100644 index 00000000..1ef10aaa --- /dev/null +++ b/.gitlab-ci.yaml @@ -0,0 +1,42 @@ +.test_template: &test_definition + stage: test + tags: + - docker + script: + - pip3 install -r requirements.txt + - pip3 install -r dev-only-reqs.txt + - pycodestyle --ignore=E501,W503,E121,E123,E126,E133,E226,E241,E242,E704 . + - pytest tests --cov=ly --cov-report term --cov-report html + +test_python3.5: + image: python:3.5 + <<: *test_definition +test_python3.6: + image: python:3.6 + <<: *test_definition +test_python3.7: + image: python:3.7 + <<: *test_definition +test_python3.8: + image: python:3.8 + <<: *test_definition + +test: + image: python + <<: *test_definition + artifacts: + paths: + - htmlcov/ + expire_in: 1 day + +docs: + stage: deploy + tags: + - static-server + dependencies: + - test + script: + - rm -rf ~/coverage/htdocs/python-ly + - mv htmlcov/ ~/coverage/htdocs/python-ly + only: + - stable diff --git a/README.rst b/README.rst index eafcb9ad..7790948b 100644 --- a/README.rst +++ b/README.rst @@ -47,7 +47,7 @@ This will behave like running the `ly` command when the package is installed. The `ly` Python module ---------------------- -The `ly` module supports both Python2 and Python3. This is a short description +The `ly` module supports only Python3. This is a short description of some modules: * ``ly.slexer``: generic tools to build parsers using regular expressions @@ -72,3 +72,21 @@ by typing ``make html`` in the doc directory. You can also read the docs online at http://python-ly.readthedocs.org/. +`Core Understanding Test (CUT)`_ +-------------------------------- + +.. _Core Understanding Test (CUT): //gitlab.ccel.org/drupal/shared-modules/wikis + /Core-Understanding-Test + +- When should branches be merged to ``master``, ``ccel-specific``, + and ``stable`` respectively? +- If you were going to be a developer for python-ly's ``ly2xml`` conversion, + where would be a good directory of test files to use in order to become + familiar with ``Lilypond`` and ``MusicXML``? +- What software should you use to render ``Lilypond`` and ``MusicXML`` files, + respectively? +- Within the ``/ly/musicxml`` directory, what is the general flow of control + between files? +- What are two important differences and one similarity between ``Lilypond`` and + ``MusicXML`` files? + diff --git a/dev-only-reqs.txt b/dev-only-reqs.txt new file mode 100644 index 00000000..2c770a23 --- /dev/null +++ b/dev-only-reqs.txt @@ -0,0 +1,4 @@ +coverage==4.5.4 +pycodestyle==2.5.0 +pytest==5.1.2 +pytest-cov==2.7.1 diff --git a/doc/conf.py b/doc/conf.py index 3c2e79f7..1153e846 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -12,21 +12,21 @@ # All configuration values have a default; values that are commented out # serve to show the default. +import ly.pkginfo import sys import os # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -#sys.path.insert(0, os.path.abspath('.')) +# sys.path.insert(0, os.path.abspath('.')) sys.path.insert(0, os.path.abspath('..')) -import ly.pkginfo # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. -#needs_sphinx = '1.0' +# needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom @@ -43,7 +43,7 @@ source_suffix = '.rst' # The encoding of source files. -#source_encoding = 'utf-8-sig' +# source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' @@ -63,13 +63,13 @@ # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. -#language = None +# language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: -#today = '' +# today = '' # Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' +# today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. @@ -77,27 +77,27 @@ # The reST default role (used for this markup: `text`) to use for all # documents. -#default_role = None +# default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True +# add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). -#add_module_names = True +# add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. -#show_authors = False +# show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] +# modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. -#keep_warnings = False +# keep_warnings = False # -- Options for HTML output ---------------------------------------------- @@ -109,77 +109,77 @@ # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. -#html_theme_options = {} +# html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. -#html_theme_path = [] +# html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". -#html_title = None +# html_title = None # A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None +# html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. -#html_logo = None +# html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. -#html_favicon = None +# html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -#html_static_path = ['_static'] +# html_static_path = ['_static'] # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied # directly to the root of the documentation. -#html_extra_path = [] +# html_extra_path = [] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. -#html_last_updated_fmt = '%b %d, %Y' +# html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. -#html_use_smartypants = True +# html_use_smartypants = True # Custom sidebar templates, maps document names to template names. -#html_sidebars = {} +# html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. -#html_additional_pages = {} +# html_additional_pages = {} # If false, no module index is generated. -#html_domain_indices = True +# html_domain_indices = True # If false, no index is generated. -#html_use_index = True +# html_use_index = True # If true, the index is split into individual pages for each letter. -#html_split_index = False +# html_split_index = False # If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True +# html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -#html_show_sphinx = True +# html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -#html_show_copyright = True +# html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. -#html_use_opensearch = '' +# html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = None +# html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = 'lydoc' @@ -188,14 +188,14 @@ # -- Options for LaTeX output --------------------------------------------- latex_elements = { -# The paper size ('letterpaper' or 'a4paper'). -#'papersize': 'letterpaper', + # The paper size ('letterpaper' or 'a4paper'). + # 'papersize': 'letterpaper', -# The font size ('10pt', '11pt' or '12pt'). -#'pointsize': '10pt', + # The font size ('10pt', '11pt' or '12pt'). + # 'pointsize': '10pt', -# Additional stuff for the LaTeX preamble. -#'preamble': '', + # Additional stuff for the LaTeX preamble. + # 'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples @@ -208,23 +208,23 @@ # The name of an image file (relative to this directory) to place at the top of # the title page. -#latex_logo = None +# latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. -#latex_use_parts = False +# latex_use_parts = False # If true, show page references after internal links. -#latex_show_pagerefs = False +# latex_show_pagerefs = False # If true, show URL addresses after external links. -#latex_show_urls = False +# latex_show_urls = False # Documents to append as an appendix to all manuals. -#latex_appendices = [] +# latex_appendices = [] # If false, no module index is generated. -#latex_domain_indices = True +# latex_domain_indices = True # -- Options for manual page output --------------------------------------- @@ -239,7 +239,7 @@ ] # If true, show URL addresses after external links. -#man_show_urls = False +# man_show_urls = False # -- Options for Texinfo output ------------------------------------------- @@ -254,16 +254,16 @@ ] # Documents to append as an appendix to all manuals. -#texinfo_appendices = [] +# texinfo_appendices = [] # If false, no module index is generated. -#texinfo_domain_indices = True +# texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. -#texinfo_show_urls = 'footnote' +# texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. -#texinfo_no_detailmenu = False +# texinfo_no_detailmenu = False # -- Options for Epub output ---------------------------------------------- @@ -275,62 +275,62 @@ epub_copyright = u'2015, ' + ly.pkginfo.maintainer # The basename for the epub file. It defaults to the project name. -#epub_basename = u'ly' +# epub_basename = u'ly' # The HTML theme for the epub output. Since the default themes are not optimized # for small screen space, using the same theme for HTML and epub output is # usually not wise. This defaults to 'epub', a theme designed to save visual # space. -#epub_theme = 'epub' +# epub_theme = 'epub' # The language of the text. It defaults to the language option # or en if the language is not set. -#epub_language = '' +# epub_language = '' # The scheme of the identifier. Typical schemes are ISBN or URL. -#epub_scheme = '' +# epub_scheme = '' # The unique identifier of the text. This can be a ISBN number # or the project homepage. -#epub_identifier = '' +# epub_identifier = '' # A unique identification for the text. -#epub_uid = '' +# epub_uid = '' # A tuple containing the cover image and cover page html template filenames. -#epub_cover = () +# epub_cover = () # A sequence of (type, uri, title) tuples for the guide element of content.opf. -#epub_guide = () +# epub_guide = () # HTML files that should be inserted before the pages created by sphinx. # The format is a list of tuples containing the path and title. -#epub_pre_files = [] +# epub_pre_files = [] # HTML files shat should be inserted after the pages created by sphinx. # The format is a list of tuples containing the path and title. -#epub_post_files = [] +# epub_post_files = [] # A list of files that should not be packed into the epub file. epub_exclude_files = ['search.html'] # The depth of the table of contents in toc.ncx. -#epub_tocdepth = 3 +# epub_tocdepth = 3 # Allow duplicate toc entries. -#epub_tocdup = True +# epub_tocdup = True # Choose between 'default' and 'includehidden'. -#epub_tocscope = 'default' +# epub_tocscope = 'default' # Fix unsupported image types using the PIL. -#epub_fix_images = False +# epub_fix_images = False # Scale large images. -#epub_max_image_width = 0 +# epub_max_image_width = 0 # How to display URL addresses: 'footnote', 'no', or 'inline'. -#epub_show_urls = 'inline' +# epub_show_urls = 'inline' # If false, no index is generated. -#epub_use_index = True +# epub_use_index = True diff --git a/ly/__init__.py b/ly/__init__.py index ce54701d..04606e74 100644 --- a/ly/__init__.py +++ b/ly/__init__.py @@ -43,7 +43,7 @@ Document class automatically parses and tokenizes the text, also when its contents are changed. -A music tree can be built from a document using the ly.music module. +A music tree can be built from a document using the ly.music module. In the near future, music trees can be built from scratch and also generate LilyPond output from scratch. At that moment, ly.dom is deprecated. @@ -52,4 +52,3 @@ """ - diff --git a/ly/barcheck.py b/ly/barcheck.py index 49e20d0f..e3fddf34 100644 --- a/ly/barcheck.py +++ b/ly/barcheck.py @@ -47,7 +47,7 @@ def remove(cursor): else: del d[cur.pos:cur.end] elif isinstance(nxt, ly.lex.Space): - # delete if followed by a space + # delete if followed by a space del d[cur.pos:cur.end] else: # replace "|" with a space @@ -57,13 +57,14 @@ def remove(cursor): class event(object): """A limited event type at a certain time.""" + def __init__(self): self._nodes = [] self.cadenza = None self.barcheck = False self.timesig = None self.partial = None - + def append(self, node): self._nodes.append(node) @@ -87,10 +88,10 @@ def insert(cursor, music=None): if music is None: import ly.music music = ly.music.document(cursor.document) - + if len(music) == 0: return - + if cursor.start: n = music.node(cursor.start, 1) nodes = itertools.chain((n,), n.forward()) @@ -99,19 +100,20 @@ def insert(cursor, music=None): if cursor.end is None: iter_nodes = iter else: - predicate = lambda node: node.position < cursor.end + def predicate(node): return node.position < cursor.end + def iter_nodes(it): return itertools.takewhile(predicate, it) - + # make time-based lists of events event_lists = [] - + def do_topnode(node): if not isinstance(node, ly.music.items.Music): for n in node: do_topnode(n) return - + def do_node(node, time, scaling): if isinstance(node, (ly.music.items.Durable, ly.music.items.UserCommand)): if node.position >= cursor.start: @@ -140,20 +142,18 @@ def do_node(node, time, scaling): else: do_topnode(node) return time - + events = collections.defaultdict(event) do_node(node, 0, 1) event_lists.append(sorted(events.items())) - + do_topnode(nodes) - + for event_list in event_lists: - + # default to 4/4 without pickup measure_length = 1 measure_pos = 0 - + for time, evt in event_list: print(time, evt) - - diff --git a/ly/cli/__init__.py b/ly/cli/__init__.py index 1b923cdd..decbfacc 100644 --- a/ly/cli/__init__.py +++ b/ly/cli/__init__.py @@ -20,4 +20,3 @@ """ The commandline interface of the 'ly' command. """ - diff --git a/ly/cli/command.py b/ly/cli/command.py index 9e630c35..89164be2 100644 --- a/ly/cli/command.py +++ b/ly/cli/command.py @@ -44,6 +44,7 @@ class _command(object): yourself. """ + def __init__(self): pass @@ -54,7 +55,7 @@ def run(self, opts, cursor, output): def get_absolute(opts, cursor): """Utility function to determine whether the first pitch in a relative should be regarded as absolute (LilyPond 2.18+ behaviour). - + """ if opts.rel_absolute is None: return ly.docinfo.DocInfo(cursor.document).version() >= (2, 18) @@ -64,6 +65,7 @@ def get_absolute(opts, cursor): class set_variable(_command): """set a variable to a value""" + def __init__(self, arg): self.name, self.value = arg.split('=', 1) @@ -73,6 +75,7 @@ def run(self, opts, cursor, output): class _info_command(_command): """base class for commands that print some output to stdout.""" + def run(self, opts, cursor, output): info = ly.docinfo.DocInfo(cursor.document) text = self.get_info(info) @@ -92,18 +95,21 @@ def get_info(self, info): class mode(_info_command): """print mode to stdout""" + def get_info(self, info): return info.mode() class version(_info_command): """print version to stdout""" + def get_info(self, info): return info.version_string() class language(_info_command): """print language to stdout""" + def get_info(self, info): return info.language() @@ -115,6 +121,7 @@ class _edit_command(_command): class indent(_edit_command): """run the indenter""" + def indenter(self, opts): """Get a ly.indent.Indenter initialized with our options.""" i = ly.indent.Indenter() @@ -128,12 +135,14 @@ def run(self, opts, cursor, output): class reformat(indent): """reformat the document""" + def run(self, opts, cursor, output): ly.reformat.reformat(cursor, self.indenter(opts)) class translate(_edit_command): """translate pitch names""" + def __init__(self, language): if language not in ly.pitch.pitchInfo: raise ValueError() @@ -155,6 +164,7 @@ def run(self, opts, cursor, output): class transpose(_edit_command): """transpose music""" + def __init__(self, arg): result = [] for pitch, octave in re.findall(r"([a-z]+)([,']*)", arg): @@ -178,6 +188,7 @@ def run(self, opts, cursor, output): class rel2abs(_edit_command): """convert relative music to absolute""" + def run(self, opts, cursor, output): absolute = self.get_absolute(opts, cursor) import ly.pitch.rel2abs @@ -186,6 +197,7 @@ def run(self, opts, cursor, output): class abs2rel(_edit_command): """convert absolute music to relative""" + def run(self, opts, cursor, output): absolute = self.get_absolute(opts, cursor) import ly.pitch.abs2rel @@ -194,6 +206,7 @@ def run(self, opts, cursor, output): class simplify_accidentals(_edit_command): """replace notes with accidentals as much as possible with their natural neighbors""" + def run(self, opts, cursor, output): absolute = self.get_absolute(opts, cursor) import ly.pitch.transpose @@ -209,6 +222,7 @@ def run(self, opts, cursor, output): class _export_command(_command): """Command that exports to a file.""" + def __init__(self, output=None): self.output = output @@ -231,6 +245,7 @@ def run(self, opts, cursor, output): class write(_command): """write the source file.""" + def __init__(self, output=None): self.output = output @@ -251,6 +266,7 @@ def run(self, opts, cursor, output): class highlight(_export_command): """write syntax colored HTML.""" + def run(self, opts, cursor, output): import ly.colorize w = ly.colorize.HtmlWriter() @@ -276,4 +292,3 @@ def run(self, opts, cursor, output): hl = highlight - diff --git a/ly/cli/doc.py b/ly/cli/doc.py index dcadd342..d579da0b 100644 --- a/ly/cli/doc.py +++ b/ly/cli/doc.py @@ -48,12 +48,12 @@ The command is one argument with semicolon-separated commands. In most cases you'll quote the command so that it is seen as one argument. -You can specify more than one LilyPond file. If you want to process many -files and write the results of the operations on each file to a separate -output file, you can use two special characters in the output filename: a -'*' will be replaced with the full path name of the current input file -(without extension), and a '?' will be replaced with the input filename -(without path and extension). If you don't want to have '*' or '?' replaced +You can specify more than one LilyPond file. If you want to process many +files and write the results of the operations on each file to a separate +output file, you can use two special characters in the output filename: a +'*' will be replaced with the full path name of the current input file +(without extension), and a '?' will be replaced with the input filename +(without path and extension). If you don't want to have '*' or '?' replaced in the output filename, you can set ``-d replace-pattern=false``. If you don't specify input or output filenames, standard input is read and @@ -62,24 +62,24 @@ Commands -------- - + Informative commands that write information to standard output and do not change the file: ``mode`` print the mode (guessing if not given) of the document - + ``version`` print the LilyPond version, if set in the document ``language`` print the pitch name language, if set in the document - + Commands that change the file: ``indent`` re-indent the file - + ``reformat`` reformat the file @@ -136,7 +136,7 @@ ``mode`` mode of the file to read (default automatic) can be one of: lilypond, scheme, latex, html, docbook, texinfo. - + ``output`` [-] the output filename (also set by -o argument) @@ -163,7 +163,7 @@ ``rel-startpitch`` [``true``] whether to write relative music with a startpitch - + ``rel-absolute`` whether to assume that the first pitch in a relative expression without specified startpitch is absolute. If ``false``, it is assumed to be @@ -180,7 +180,7 @@ ``full-html`` [``True``] if set to True a full document with syntax-highlighted HTML will be exported, otherwise only the bare content wrapped in an - element configured by the ``wrapper-`` variables. + element configured by the ``wrapper-`` variables. ``stylesheet`` filename to reference as an external stylesheet for diff --git a/ly/cli/main.py b/ly/cli/main.py index b96577e6..9263ed0f 100644 --- a/ly/cli/main.py +++ b/ly/cli/main.py @@ -39,6 +39,7 @@ def usage(): from . import doc sys.stdout.write(doc.__doc__) + def usage_short(): """Print short usage info.""" sys.stdout.write("""\ @@ -49,19 +50,23 @@ def usage_short(): See ly -h for a full list of commands and options. """) + def version(): """Print version info.""" sys.stdout.write("ly {0}\n".format(ly.pkginfo.version)) + def die(message): """Exit with message to STDERR.""" sys.stderr.write("error: " + message + '\n') sys.stderr.write( "See ly -h for a full list of commands and options.\n") sys.exit(1) - + + class Options(object): """Store all the startup options and their defaults.""" + def __init__(self): self.mode = None self.in_place = False @@ -74,11 +79,11 @@ def __init__(self): self.default_language = "nederlands" self.rel_startpitch = True self.rel_absolute = None - + self.indent_width = 2 self.indent_tabs = False self.tab_width = 8 - + self.full_html = True self.inline_style = False self.stylesheet = None @@ -99,27 +104,29 @@ def set_variable(self, name, value): except ValueError as e: die(format(e)) setattr(self, name, value) - + + class Output(object): """Object living for a whole file/command operation, handling the output. - + When opening a file it has already opened earlier, the file is appended to (like awk). - + """ + def __init__(self): self._seen_filenames = set() - + def get_filename(self, opts, filename): """Queries the output attribute from the Options and returns it. - - If replace_pattern is True (by default) and the attribute contains a - '*', it is replaced with the full path of the specified filename, - but without extension. It the attribute contains a '?', it is + + If replace_pattern is True (by default) and the attribute contains a + '*', it is replaced with the full path of the specified filename, + but without extension. It the attribute contains a '?', it is replaced with the filename without path and extension. - + If '-' is returned, it denotes standard output. - + """ if not opts.output: return '-' @@ -129,14 +136,14 @@ def get_filename(self, opts, filename): return opts.output.replace('?', name).replace('*', path) else: return opts.output - + @contextlib.contextmanager def file(self, opts, filename, encoding): """Return a context manager for writing to. - + If you set encoding to "binary" or False, the file is opened in binary mode and you should encode the data you write yourself. - + """ if not filename or filename == '-': filename, mode = sys.stdout.fileno(), 'w' @@ -157,20 +164,21 @@ def file(self, opts, filename, encoding): finally: f.close() + def parse_command_line(): """Return a three-tuple(options, commands, files). - + options is an Options instance with all the command-line options commands is a list of command.command instances files is the list of filename arguments - + Also performs error handling and may exit on certain circumstances. - + """ if len(sys.argv) < 2: usage_short() sys.exit(2) - + if isinstance(sys.argv[0], type('')): # python 3 - arguments are unicode strings args = iter(sys.argv[1:]) @@ -178,18 +186,18 @@ def parse_command_line(): # python 2 - arguments are bytes, decode them fsenc = sys.getfilesystemencoding() or 'latin1' args = (a.decode(fsenc) for a in sys.argv[1:]) - + opts = Options() commands = [] files = [] - + def next_arg(message): """Get the next argument, if missing, die with message.""" try: return next(args) except StopIteration: die(message) - + for arg in args: if arg in ('-h', '--help'): usage() @@ -232,16 +240,17 @@ def next_arg(message): opts.with_filename = len(files) > 1 return opts, commands, files + def parse_command(arg): """Parse the command string, returning a list of command.command instances. - + Exits when a command is invalid. - + """ from . import command result = [] - + for c in arg.split(';'): args = c.split(None, 1) if args: @@ -260,6 +269,7 @@ def parse_command(arg): die("invalid arguments: " + c) return result + def load(filename, encoding, mode): """Load a file, returning a ly.document.Document""" import ly.document @@ -270,6 +280,7 @@ def load(filename, encoding, mode): doc = ly.document.Document.load(filename, encoding, mode) return doc + def main(): opts, commands, files = parse_command_line() import ly.document @@ -287,4 +298,3 @@ def main(): for c in commands: c.run(options, cursor, output) return exit_code - diff --git a/ly/cli/setvar.py b/ly/cli/setvar.py index 3da656fb..cc13da87 100644 --- a/ly/cli/setvar.py +++ b/ly/cli/setvar.py @@ -144,19 +144,19 @@ def number_lines(arg): def wrapper_tag(arg): - if not arg in ['div', 'pre', 'code', 'id']: + if arg not in ['div', 'pre', 'code', 'id']: raise ValueError("unknown wrapper tag: {tag}".format( tag=arg)) return arg def wrapper_attribute(arg): - if not arg in ['id', 'class']: + if arg not in ['id', 'class']: raise ValueError("wrapper attribute must be 'id' or 'class', found {attr}".format( attr=arg)) return arg - + def document_id(arg): return arg or None diff --git a/ly/colorize.py b/ly/colorize.py index b3c236c9..6d104212 100644 --- a/ly/colorize.py +++ b/ly/colorize.py @@ -69,6 +69,7 @@ class found in the keys is returned. The class is also cached to speed up requests for other tokens. """ + def __getitem__(self, token): cls = type(token) try: @@ -92,8 +93,8 @@ def default_mapping(): from ly.lex import scheme from ly.lex import html from ly.lex import texinfo - #from ly.lex import latex - #from ly.lex import docbook + # from ly.lex import latex + # from ly.lex import docbook from ly.lex import mup return ( @@ -169,7 +170,7 @@ def default_mapping(): style('macro', 'variable', (mup.Macro,)), style('preprocessor', 'keyword', (mup.Preprocessor,)), )), - ) # end of mapping + ) # end of mapping default_scheme = { @@ -250,7 +251,7 @@ def default_mapping(): }, 'mup': { }, -} # end of default_css_styles +} # end of default_css_styles def get_tokens(cursor): @@ -322,9 +323,9 @@ def css_mapper(mapping=None): if mapping is None: mapping = default_mapping() return Mapper((cls, css_class(mode, style.name, style.base)) - for mode, styles in mapping - for style in styles - for cls in style.classes) + for mode, styles in mapping + for style in styles + for cls in style.classes) def css_dict(css_style, scheme=default_scheme): @@ -378,6 +379,7 @@ def format_css_span_class(css_style): class css_style_attribute_formatter(object): """Return the inline style attribute for a specified style.""" + def __init__(self, scheme=default_scheme): self.scheme = scheme @@ -390,7 +392,7 @@ def __call__(self, css_style): def format_stylesheet(scheme=default_scheme): """Return a formatted stylesheet for the stylesheet scheme dictionary.""" sheet = [] - key = lambda i: '' if i[0] is None else i[0] + def key(i): return '' if i[0] is None else i[0] for mode, styles in sorted(scheme.items(), key=key): if styles: sheet.append('/* {0} */'.format( @@ -509,10 +511,10 @@ def format_html_document(body, title="", stylesheet=None, stylesheet_ref=None, e '{css}' '\n' '\n{body}\n\n').format( - title = html_escape(title), - encoding = encoding, - body = body, - css = css, + title=html_escape(title), + encoding=encoding, + body=body, + css=css, ) diff --git a/ly/cursortools.py b/ly/cursortools.py index 714eb9ca..f54f25f2 100644 --- a/ly/cursortools.py +++ b/ly/cursortools.py @@ -31,9 +31,9 @@ def find_indent(iterable): """Yield (token, is_indent, nest) for every occurring indent/dedent token. - + The tokens are yielded from the specified iterable. - + """ nest = 0 for token in iterable: @@ -47,12 +47,12 @@ def find_indent(iterable): def select_block(cursor): """Try to select a meaningful block. - - Searches backwards for an indenting token, then selects up to the - corresponding dedenting token. If needed searches an extra level back to - always extend the selection. Returns True if the cursor's selection has + + Searches backwards for an indenting token, then selects up to the + corresponding dedenting token. If needed searches an extra level back to + always extend the selection. Returns True if the cursor's selection has changed. - + """ end = cursor.end if cursor.end is not None else cursor.document.size() tokens = ly.document.Runner.at(cursor, after_token=True) @@ -75,4 +75,3 @@ def select_block(cursor): cursor.start, cursor.end = pos1, pos2 return True return - diff --git a/ly/data/__init__.py b/ly/data/__init__.py index 4b640a82..e9347ce3 100644 --- a/ly/data/__init__.py +++ b/ly/data/__init__.py @@ -21,12 +21,14 @@ Query functions to get data from the LilyPond-generated _data.py module. """ + def grob_properties(grob): """Returns the list of properties the named grob supports.""" from . import _data return sorted(set(prop - for iface in _data.grobs.get(grob, []) - for prop in _data.interfaces[iface])) + for iface in _data.grobs.get(grob, []) + for prop in _data.interfaces[iface])) + def grob_properties_with_interface(grob): """Returns a list of two-tuples (property, interface).""" @@ -36,11 +38,12 @@ def grob_properties_with_interface(grob): for iface in _data.grobs.get(grob, []) for prop in _data.interfaces[iface]) + def grob_interfaces(grob, prop=None): """Returns the list of interfaces a grob supports. - + If prop is given, only returns the interfaces that define prop. - + """ from . import _data ifaces = _data.grobs.get(grob, []) @@ -49,67 +52,79 @@ def grob_interfaces(grob, prop=None): return [iface for iface in ifaces if prop in grob_interface_properties(iface)] + def grob_interface_properties(iface): """Returns the list of properties an interface supports.""" from . import _data return _data.interfaces.get(iface, []) + def grob_interfaces_for_property(prop): """Returns the list of interfaces that define the property. - + Most times returns one, but several interface names may be returned. - + """ from . import _data return [iface - for iface, props in _data.interfaces.items() - if prop in props] + for iface, props in _data.interfaces.items() + if prop in props] + def grobs(): """Returns the sorted list of all grob names.""" from . import _data return sorted(_data.grobs.keys()) - + + def all_grob_properties(): """Returns the list of all properties.""" from . import _data return sorted(set(sum(_data.interfaces.values(), []))) + def context_properties(): """Returns the list of context properties.""" from . import _data return _data.contextproperties + def engravers(): """Returns the list of engravers and performers.""" from . import _data return _data.engravers + def music_glyphs(): """Returns the list of glyphs in the emmentaler font.""" from . import _data return _data.musicglyphs + def scheme_keywords(): """Returns the list of guile keywords.""" from . import _data return _data.scheme_keywords + def scheme_functions(): """Returns the list of scheme functions.""" from . import _data return _data.scheme_functions + def scheme_variables(): """Returns the list of scheme variables.""" from . import _data return _data.scheme_variables + def scheme_constants(): """Returns the list of scheme constants.""" from . import _data return _data.scheme_constants + def all_scheme_words(): """Returns the list of all scheme words.""" from . import _data diff --git a/ly/data/_data.py b/ly/data/_data.py index cf3abe4e..b9f29c20 100644 --- a/ly/data/_data.py +++ b/ly/data/_data.py @@ -27,5 +27,3 @@ # BarLine #'bar-extent if "bar-extent" not in interfaces["bar-line-interface"]: interfaces["bar-line-interface"].insert(1, "bar-extent") - - diff --git a/ly/data/_lilypond_data.py b/ly/data/_lilypond_data.py index f1f736d8..337384f7 100644 --- a/ly/data/_lilypond_data.py +++ b/ly/data/_lilypond_data.py @@ -2608,4 +2608,3 @@ "z", "zero", ] - diff --git a/ly/data/_scheme_data.py b/ly/data/_scheme_data.py index 7fb635e0..1eaad09d 100644 --- a/ly/data/_scheme_data.py +++ b/ly/data/_scheme_data.py @@ -1,6 +1,6 @@ -#generated by makeschemedata.py +# generated by makeschemedata.py -version="2.18" +version = "2.18" scheme_keywords = [ '*', @@ -2813,4 +2813,3 @@ 'Y', 'ZERO-MOMENT', ] - diff --git a/ly/data/makeschemedata.py b/ly/data/makeschemedata.py index 4476f827..44f950bd 100644 --- a/ly/data/makeschemedata.py +++ b/ly/data/makeschemedata.py @@ -27,7 +27,7 @@ import re try: from urllib.request import urlopen -except: +except BaseException: from urllib import urlopen @@ -69,17 +69,20 @@ GUILE_OTHER_WORDS = ['else', 'set!'] + def writeList(file_, name, lst): file_.write("{} = [\n".format(name)) for word in sorted(set(lst)): file_.write(" '{0}',\n".format(word)) file_.write("]\n\n") + def replace(word): word = word.replace('<', '<') word = word.replace('>', '>') return word + guilePage = urlopen(GUILE_URL).read() guilePat = re.compile(r"([a-z\d\+\?\!\*&;/=:-]+): (.+)") @@ -95,10 +98,11 @@ def replace(word): schemeDataPath = os.path.join(os.path.split(__file__)[0], '_scheme_data.py') + def main(): with open(schemeDataPath, "w") as f: f.write("#generated by makeschemedata.py\n\nversion=\"{}\"\n\n".format(VERSION)) - + guileWords = [] for m in guilePat.finditer(guilePage.decode('utf-8')): if m.group(2) not in GUILE_IGNONED_TYPE: @@ -106,46 +110,44 @@ def main(): if not proc.startswith('gds-'): guileWords.append(replace(proc)) guileWords += GUILE_OTHER_WORDS - - + lyFunctions = [] lyVars = [] lyCons = [] for word in lyFunctionPat.finditer(lyFunctionPage.decode('utf-8')): lyFunctions.append(replace(word.group(1))) - + for filename in os.listdir(SCM_PATH): path = os.path.join(SCM_PATH, filename) with open(path) as inp: text = inp.read() for m in defineFunc.finditer(text): lyFunctions.append(m.group(1)) - + for m in defineMacro.finditer(text): lyFunctions.append(m.group(1)) - + for m in startWithLy.finditer(text): lyFunctions.append(m.group(1)) - + for m in defineVar.finditer(text): word = m.group(1) if word.isupper(): lyCons.append(word) else: lyVars.append(word) - + for word in lyFunctions: if word in lyVars: lyVars.remove(word) - + lyVars.remove('parser') - - writeList(f, 'scheme_keywords', guileWords) + + writeList(f, 'scheme_keywords', guileWords) writeList(f, 'scheme_functions', lyFunctions) writeList(f, 'scheme_variables', lyVars) writeList(f, 'scheme_constants', lyCons) - + if __name__ == "__main__": main() - diff --git a/ly/docinfo.py b/ly/docinfo.py index d32784c8..f79c4939 100644 --- a/ly/docinfo.py +++ b/ly/docinfo.py @@ -51,19 +51,20 @@ def wrapper(self): class DocInfo(object): """Harvest information from a ly.document.DocumentBase instance. - - All tokens are saved in the tokens attribute as a tuple. Newline tokens - are added between all lines. All corresponding classes are in the + + All tokens are saved in the tokens attribute as a tuple. Newline tokens + are added between all lines. All corresponding classes are in the classes attribute as a tuple. This makes quick search and access possible. - - The tokens are requested from the document using the - tokens_with_position() method, so you can always locate them back in the + + The tokens are requested from the document using the + tokens_with_position() method, so you can always locate them back in the original document using their pos attribute. - - DocInfo does not update when the document changes, you should just + + DocInfo does not update when the document changes, you should just instantiate a new one. - + """ + def __init__(self, doc): """Initialize with ly.document.DocumentBase instance.""" self._d = doc @@ -71,26 +72,26 @@ def __init__(self, doc): for b in blocks: tokens = doc.tokens_with_position(b) self.tokens = sum(map( - lambda b: ((ly.lex.Newline('\n', doc.position(b) - 1),) + - doc.tokens_with_position(b)), + lambda b: ((ly.lex.Newline('\n', doc.position(b) - 1),) + + doc.tokens_with_position(b)), blocks), tokens) self.classes = tuple(map(type, self.tokens)) - + @property def document(self): return self._d - + def range(self, start=0, end=None): """Return a new instance of the DocInfo class for the selected range. - - Only the tokens completely contained within the range start..end are - added to the new instance. This can be used to perform fast searches + + Only the tokens completely contained within the range start..end are + added to the new instance. This can be used to perform fast searches on a subset of a document. - + """ if start == 0 and end is None: return self - + lo = 0 hi = len(self.tokens) while lo < hi: @@ -109,27 +110,27 @@ def range(self, start=0, end=None): hi = mid else: lo = mid + 1 - end = lo - 1 + end = lo - 1 s = slice(start, end) n = type(self).__new__(type(self)) n._d = self._d n.tokens = self.tokens[s] n.classes = self.classes[s] return n - + @_cache def mode(self): """Return the mode, e.g. "lilypond".""" return self._d.initial_state().mode() - + def find(self, token=None, cls=None, pos=0, endpos=-1): """Return the index of the first specified token and/or class after pos. - - If token is None, the cls should be specified. If cls is given, the - token should be an instance of the specified class. If endpos is - given, never searches beyond endpos. Returns -1 if the token is not + + If token is None, the cls should be specified. If cls is given, the + token should be an instance of the specified class. If endpos is + given, never searches beyond endpos. Returns -1 if the token is not found. - + """ if token is None: try: @@ -150,15 +151,15 @@ def find(self, token=None, cls=None, pos=0, endpos=-1): if cls == self.classes[i]: return i pos = i + 1 - + def find_all(self, token=None, cls=None, pos=0, endpos=-1): """Yield all indices of the first specified token and/or class after pos. - - If token is None, the cls should be specified. If cls is given, the - token should be an instance of the specified class. If endpos is - given, never searches beyond endpos. Returns -1 if the token is not + + If token is None, the cls should be specified. If cls is given, the + token should be an instance of the specified class. If endpos is + given, never searches beyond endpos. Returns -1 if the token is not found. - + """ while True: i = self.find(token, cls, pos, endpos) @@ -166,14 +167,14 @@ def find_all(self, token=None, cls=None, pos=0, endpos=-1): break yield i pos = i + 1 - + @_cache def version_string(self): r"""Return the version as a string, e.g. "2.19.8". - + Looks for the \version LilyPond command. The string is returned without quotes. Returns None if there was no \version command found. - + """ i = self.find("\\version", ly.lex.lilypond.Keyword) if i != -1: @@ -181,9 +182,9 @@ def version_string(self): for t in tokens: if not isinstance(t, (ly.lex.Space, ly.lex.Comment)): if t == '"': - pred = lambda t: t != '"' + def pred(t): return t != '"' else: - pred = lambda t: not isinstance(t, (ly.lex.Space, ly.lex.Comment)) + def pred(t): return not isinstance(t, (ly.lex.Space, ly.lex.Comment)) return ''.join(itertools.takewhile(pred, tokens)) @_cache @@ -206,7 +207,7 @@ def include_args(self): result.append(''.join(itertools.takewhile(lambda t: t != '"', tokens))) break return result - + @_cache def scheme_load_args(self): """The list of scheme (load) command arguments.""" @@ -219,17 +220,17 @@ def scheme_load_args(self): result.append(''.join(itertools.takewhile(lambda t: t != '"', tokens))) break return result - + @_cache def output_args(self): r"""The list of arguments of constructs defining the name of output documents. - + This looks at the \bookOutputName, \bookOutputSuffix and define output-suffix commands. - - Every argument is a two tuple(type, argument) where type is either + + Every argument is a two tuple(type, argument) where type is either "suffix" or "name". - + """ result = [] for arg_type, cmd, cls in ( @@ -247,12 +248,12 @@ def output_args(self): elif isinstance(t, ly.lex.lilypond.Name): result.append((arg_type, format(t))) elif isinstance(t, (ly.lex.lilypond.SchemeStart, - ly.lex.Space, - ly.lex.Comment)): + ly.lex.Space, + ly.lex.Comment)): continue break return result - + @_cache def definitions(self): """The list of LilyPond identifiers the document defines.""" @@ -261,7 +262,7 @@ def definitions(self): if i == 0 or self.tokens[i-1] == '\n': result.append(self.tokens[i]) return result - + @_cache def markup_definitions(self): """The list of markup command definitions in the document.""" @@ -300,7 +301,7 @@ def language(self): lang = n.rsplit('.', 1)[0] if lang in languages: return lang - + @_cache def global_staff_size(self): """The global-staff-size, if set, else None.""" @@ -310,52 +311,50 @@ def global_staff_size(self): return int(self.tokens[i+2]) except (IndexError, ValueError): pass - + @_cache def token_hash(self): """Return an integer hash for all non-whitespace and non-comment tokens. - + This hash does not change when only comments or whitespace are changed. - + """ return hash(tuple(t for t in self.tokens if not isinstance(t, (ly.lex.Space, ly.lex.Comment)))) - + @_cache def complete(self): """Return whether the document is probably complete and could be compilable.""" return self._d.state_end(self._d[len(self._d)-1]).depth() == 1 - + @_cache def has_output(self): """Return True when the document probably generates output. - + I.e. has notes, rests, markup or other output-generating commands. - + """ for t, c in ( - (None, ly.lex.lilypond.MarkupStart), - (None, ly.lex.lilypond.Note), - (None, ly.lex.lilypond.Rest), - ('\\include', ly.lex.lilypond.Keyword), - (None, ly.lex.lilypond.LyricMode), - ): + (None, ly.lex.lilypond.MarkupStart), + (None, ly.lex.lilypond.Note), + (None, ly.lex.lilypond.Rest), + ('\\include', ly.lex.lilypond.Keyword), + (None, ly.lex.lilypond.LyricMode), + ): for i in self.find_all(t, c): return True return False - + def count_tokens(self, cls): """Return the number of tokens that are (a subclass) of the specified class. - - If you only want the number of instances of the exact class (not a - subclass of) you can use info.classes.count(cls), where info is a + + If you only want the number of instances of the exact class (not a + subclass of) you can use info.classes.count(cls), where info is a DocInfo instance. - + """ return sum([issubclass(c, cls) for c in self.classes], False) def counted_tokens(self): """Return a dictionary mapping classes to the number of instances of that class.""" return collections.Counter(self.classes) - - diff --git a/ly/document.py b/ly/document.py index 3c238fd5..a0766fcf 100644 --- a/ly/document.py +++ b/ly/document.py @@ -23,8 +23,8 @@ Represents a LilyPond source document (the text contents). -The Document implementation keeps the document in a (unicode) text string, -but you can inherit from the DocumentBase class to support other +The Document implementation keeps the document in a (unicode) text string, +but you can inherit from the DocumentBase class to support other representations of the text content. Modifying is preferably done inside a context (the with statement), e.g.: @@ -43,10 +43,10 @@ re-tokenized immediately. This is much slower however when performing multiple changes after each other. -The tokens(block) method returns a tuple of tokens for the specified block. -Depending on the implementation, a block describes a line in the LilyPond -source document. It is not expected to have any methods, except that the -'==' operator is supported between two blocks, and returns True if both +The tokens(block) method returns a tuple of tokens for the specified block. +Depending on the implementation, a block describes a line in the LilyPond +source document. It is not expected to have any methods, except that the +'==' operator is supported between two blocks, and returns True if both refer to the same line of text in the source document. @@ -84,9 +84,9 @@ class DocumentBase(object): """Abstract base class for Document instances. - + You should inherit the following methods: - + setplaintext __len__ __getitem__ @@ -99,50 +99,49 @@ class DocumentBase(object): initial_state state_end apply_changes - + You may inherit (e.g. to get speed improvements): - + plaintext next_block previous_block blocks_forward blocks_backward state - + You may use the following attributes: - + filename (None) # can represent the filename of the document on disk encoding (None) # can represent the encoding of the document when reading/writing to disk - + """ - + filename = None encoding = None - - + def __init__(self): """Constructor""" self._writing = 0 self._changes = collections.defaultdict(list) self._cursors = weakref.WeakSet() - + def __bool__(self): return True - + __nonzero__ = __bool__ # py2 compat - + def __iter__(self): """Iter over all blocks.""" return self.blocks_forward(self[0]) - + def __len__(self): """Return the number of blocks""" raise NotImplementedError() - + def __getitem__(self, index): """Return the block at the specified index.""" raise NotImplementedError() - + def plaintext(self): """The document contents as a plain text string.""" return '\n'.join(map(self.text, self)) @@ -158,19 +157,19 @@ def size(self): def block(self, position): """Return the text block at the specified character position. - + The text block itself has no methods, but it can be used as an argument to other methods of this class. - + (Blocks do have to support the '==' operator.) - + """ raise NotImplementedError() - + def index(self, block): """Return the linenumber of the block (starting with 0).""" raise NotImplementedError() - + def blocks_forward(self, block): """Iter forward starting with the specified block.""" while self.isvalid(block): @@ -190,23 +189,23 @@ def position(self, block): def text(self, block): """Return the text of the specified block.""" raise NotImplementedError() - + def next_block(self, block): """Return the next block, which may be invalid.""" index = self.index(block) if index < len(self) - 1: return self[index + 1] - + def previous_block(self, block): """Return the previous block, which may be invalid.""" index = self.index(block) if index > 0: return self[index - 1] - + def isvalid(self, block): """Return True if the block is a valid block.""" raise NotImplementedError() - + def isblank(self, block): """Return True if the block is empty or blank.""" t = self.text(block) @@ -216,7 +215,7 @@ def __enter__(self): """Start the context for modifying the document.""" self._writing += 1 return self - + def __exit__(self, exc_type, exc_val, exc_tb): """Exit the context for modifying.""" if exc_type is not None: @@ -232,16 +231,16 @@ def __exit__(self, exc_type, exc_val, exc_tb): self._writing = 0 elif self._writing > 1: self._writing -= 1 - + def _register_cursor(self, cursor): """Make a weak reference to the cursor. - + This is called by the constructor of the Cursor. The Cursor gets updated when the document is changed. - + """ self._cursors.add(cursor) - + def check_changes(self): """Debugging method that checks for overlapping edits.""" pos = self.size() @@ -251,15 +250,15 @@ def check_changes(self): text = text[:10] + '...' raise ValueError("overlapping edit: {0}-{1}: {2}".format(start, end, text)) pos = start - + def _sort_changes(self): """Sort all the changes and put them in the _changes_list.""" self._changes_list = [(start, end, text) - for start, items in sorted(self._changes.items(), reverse=True) - for end, text in reversed(sorted(items, - key=lambda i: (i[0] is None, i[0])))] + for start, items in sorted(self._changes.items(), reverse=True) + for end, text in reversed(sorted(items, + key=lambda i: (i[0] is None, i[0])))] self._changes.clear() - + def update_cursors(self): """Updates the position of the registered Cursor instances.""" for start, end, text in self._changes_list: @@ -274,54 +273,54 @@ def update_cursors(self): c.end = start + len(text) else: c.end += start + len(text) - end - + def apply_changes(self): """Apply the changes and update the tokens.""" raise NotImplementedError() - + def tokens(self, block): """Return the tuple of tokens of the specified block. - + The pos and end attributes of every token point to the position - of the token in the block. - + of the token in the block. + """ raise NotImplementedError() - + def tokens_with_position(self, block): """Return a tuple of tokens of the specified block. - + The pos and end attributes of every token point to the position in the Document, instead of to the position in the current block. - + This makes it easier to iterate over tokens and change the document. - + """ pos = self.position(block) return tuple(type(t)(t, pos + t.pos) for t in self.tokens(block)) - + def initial_state(self): """Return the state at the beginning of the document.""" raise NotImplementedError() - + def state(self, block): """Return the state at the start of the specified block.""" prev = self.previous_block(block) if self.isvalid(prev): return self.state_end(prev) return self.initial_state() - + def state_end(self, block): """Return the state at the end of the specified block.""" raise NotImplementedError() - + def __setitem__(self, key, text): """Change the text pointed to in key (integer or slice). - + If start > stop in the slice (and stop is not None), start and stop are swapped. (This is different than usual Python behaviour, where stop is set to start if it was lower.) - + """ if isinstance(key, slice): start = key.start or 0 @@ -348,20 +347,20 @@ def __delitem__(self, key): class Document(DocumentBase): """A plain text LilyPond source document that auto-updates the tokens. - + The modified attribute is set to True as soon as the document is changed, but the setplaintext() method sets it to False. - + """ modified = False - + def __init__(self, text='', mode=None): super(Document, self).__init__() self._fridge = ly.lex.Fridge() self._mode = mode self._guessed_mode = None self.setplaintext(text) - + @classmethod def load(cls, filename, encoding='utf-8', mode=None): """Load the document from a file, using the specified encoding and mode.""" @@ -369,7 +368,7 @@ def load(cls, filename, encoding='utf-8', mode=None): doc = cls(f.read(), mode) doc.filename = filename return doc - + def copy(self): """Return a full copy of the document.""" doc = Document(self.plaintext(), self.mode()) @@ -377,20 +376,20 @@ def copy(self): doc.encoding = self.encoding doc.modified = self.modified return doc - + def __len__(self): """Return the number of blocks""" return len(self._blocks) - + def __getitem__(self, index): """Return the block at the specified index.""" return self._blocks[index] - + def setmode(self, mode): """Sets the mode to one of the ly.lex modes. - + Use None to auto-determine the mode. - + """ if mode not in ly.lex.modes: mode = None @@ -405,11 +404,11 @@ def setmode(self, mode): if mode == self._guessed_mode: return self._update_all_tokens() - + def mode(self): """Return the mode (lilypond, html, etc). None means automatic mode.""" return self._mode - + def setplaintext(self, text): """Set the text of the document, sets modified to False.""" text = text.replace('\r', '') @@ -423,21 +422,21 @@ def setplaintext(self, text): self._guessed_mode = ly.lex.guessMode(text) self._update_all_tokens() self.modified = False - + def _update_all_tokens(self): state = self.initial_state() for b in self._blocks: b.tokens = tuple(state.tokens(b.text)) b.state = self._fridge.freeze(state) - + def initial_state(self): """Return the state at the beginning of the document.""" return ly.lex.state(self._mode or self._guessed_mode) - + def state_end(self, block): """Return the state at the end of the specified block.""" return self._fridge.thaw(block.state) - + def block(self, position): """Return the text block at the specified character position.""" if 0 <= position <= self._blocks[-1].position + len(self._blocks[-1].text): @@ -450,7 +449,7 @@ def block(self, position): else: lo = mid + 1 return self._blocks[lo-1] - + def index(self, block): """Return the linenumber of the block (starting with 0).""" return block.index @@ -462,15 +461,15 @@ def position(self, block): def text(self, block): """Return the text of the specified block.""" return block.text - + def isvalid(self, block): """Return True if the block is a valid block.""" return bool(block) - + def tokens(self, block): """Return the tuple of tokens of the specified block.""" return block.tokens - + def apply_changes(self): for start, end, text in self._changes_list: s = self.block(start) @@ -492,16 +491,16 @@ def apply_changes(self): self._blocks[s.index+1:s.index+1] = map(_Block, lines[1:]) # make sure this line gets reparsed s.tokens = None - + # update the position of all the new blocks pos = s.position for i, b in enumerate(self._blocks[s.index:], s.index): b.index = i b.position = pos pos += len(b.text) + 1 - + self.modified = True - + # if the initial state has changed, reparse everything if not self._mode: mode = ly.lex.guessMode(self.plaintext()) @@ -509,7 +508,7 @@ def apply_changes(self): self._guessed_mode = mode self._update_all_tokens() return - + # update the tokens starting at block s state = self.state(s) reparse = False @@ -525,15 +524,15 @@ def apply_changes(self): class _Block(object): """A line of text. - + This class is only used by the Document implementation. - + """ - + position = sys.maxsize # prevent picking those blocks before updating pos - state = None - tokens = None - + state = None + tokens = None + def __init__(self, text="", index=-1): self.text = text self.index = index @@ -541,53 +540,54 @@ def __init__(self, text="", index=-1): class Cursor(object): """Defines a certain range (selection) in a Document. - - You may change the start and end attributes yourself. Both must be an + + You may change the start and end attributes yourself. Both must be an integer, end may also be None, denoting the end of the document. - - As long as you keep a reference to the Cursor, its positions are updated - when the document changes. When text is inserted at the start position, - it remains the same. But when text is inserted at the end of a cursor, + + As long as you keep a reference to the Cursor, its positions are updated + when the document changes. When text is inserted at the start position, + it remains the same. But when text is inserted at the end of a cursor, the end position moves along with the new text. E.g.: - + .. code-block:: python - + d = Document('hi there, folks!') c = Cursor(d, 8, 8) with d: d[8:8] = 'new text' c.start, c.end --> (8, 16) - + Many tools in the ly module use this object to describe (part of) a document. - + """ + def __init__(self, doc, start=0, end=None): self._d = doc self.start = start self.end = end doc._register_cursor(self) - + @property def document(self): return self._d - + def start_block(self): """Return the block the start attribute points at.""" return self._d.block(self.start) - + def end_block(self): """Return the block the end attribute points at.""" if self.end is None: return self._d[len(self._d)-1] return self._d.block(self.end) - + def blocks(self): """Iterate over the selected blocks. - - If there are multiple blocks and the cursor ends on the first + + If there are multiple blocks and the cursor ends on the first position of the last selected block, that block is not included. - + """ if self.end == self.start: yield self.start_block() @@ -596,17 +596,17 @@ def blocks(self): if self.end is not None and self._d.position(b) >= self.end: break yield b - + def text(self): """Convenience method to return the selected text.""" return self._d.plaintext()[self.start:self.end] - + def text_before(self): """Return text before the cursor in it's start block.""" b = self.start_block() pos = self.start - self._d.position(b) return self._d.text(b)[:pos] - + def text_after(self): """Return text after the cursor in it's end block.""" if self.end is None: @@ -614,35 +614,35 @@ def text_after(self): b = self.end_block() pos = self.end - self._d.position(b) return self._d.text(b)[pos:] - + def has_selection(self): """Return True when there is some text selected.""" end = self.end if end is None: end = self._d.size() return self.start != end - + def select_all(self): """Select all text.""" self.start, self.end = 0, None - + def select_end_of_block(self): """Move end to the end of the block.""" if self.end is not None: end = self.end_block() self.end = self._d.position(end) + len(self._d.text(end)) - + def select_start_of_block(self): """Move start to the start of the block.""" start = self.start_block() self.start = self._d.position(start) - + def lstrip(self, chars=None): """Move start to the right, like Python's lstrip() string method.""" if self.has_selection(): text = self.text() self.start += len(text) - len(text.lstrip(chars)) - + def rstrip(self, chars=None): """Move end to the left, like Python's lstrip() string method.""" if self.has_selection(): @@ -651,7 +651,7 @@ def rstrip(self, chars=None): end -= len(text) - len(text.rstrip(chars)) if end < self._d.size(): self.end = end - + def strip(self, chars=None): """Strip chars from the selection, like Python's strip() method.""" self.rstrip(chars) @@ -660,56 +660,57 @@ def strip(self, chars=None): class Runner(object): """Iterates back and forth over tokens. - + A Runner can stop anywhere and remembers its current token. - + """ + def __init__(self, doc, tokens_with_position=False): """Create and init with Document. - - If tokens_with_position is True, uses the tokens_with_position() - method to get the tokens, else (by default), the tokens() method is + + If tokens_with_position is True, uses the tokens_with_position() + method to get the tokens, else (by default), the tokens() method is used. - - The Runner is initialized at position 0. Alternatively, you can use - the 'at' classmethod to construct a Runner at a specific cursor + + The Runner is initialized at position 0. Alternatively, you can use + the 'at' classmethod to construct a Runner at a specific cursor position. - + """ self._doc = doc self._wp = tokens_with_position self.move_to_block(doc[0]) - + @classmethod def at(cls, cursor, after_token=False, tokens_with_position=False): """Create and init from a Cursor. - + The Runner is positioned so that yielding forward starts with the first complete token after the cursor's start position. - + Set after_token to True if you want to position the cursor after the token, so that it gets yielded when you go backward. - - If tokens_with_position is True, uses the tokens_with_position() - method to get the tokens, else (by default), the tokens() method is + + If tokens_with_position is True, uses the tokens_with_position() + method to get the tokens, else (by default), the tokens() method is used. - + """ runner = cls(cursor.document, tokens_with_position) runner.set_position(cursor.start, after_token) return runner - + @property def document(self): """Return our Document.""" return self._doc - + def set_position(self, position, after_token=False): """Positions the Runner at the specified position. - + Set after_token to True if you want to position the cursor after the token, so that it gets yielded when you go backward. - + """ block = self._doc.block(position) self.move_to_block(block) @@ -723,12 +724,12 @@ def set_position(self, position, after_token=False): if self.position() + len(t) > position: self._index -= 1 break - + def move_to_block(self, block, at_end=False): """Positions the Runner at the start of the given text block. - + If at_end == True, the iterator is positioned past the end of the block. - + """ if self._doc.isvalid(block): self.block = block @@ -736,14 +737,14 @@ def move_to_block(self, block, at_end=False): self._tokens = method(block) self._index = len(self._tokens) if at_end else -1 return True - + def _newline(self): """(Internal) Create a Newline token at the end of the current block.""" pos = len(self._doc.text(self.block)) if self._wp: pos += self._doc.position(self.block) return ly.lex.Newline('\n', pos) - + def forward_line(self): """Yields tokens in forward direction in the current block.""" end = len(self._tokens) @@ -753,7 +754,7 @@ def forward_line(self): if self._index == end: break yield self._tokens[self._index] - + def forward(self): """Yields tokens in forward direction across blocks.""" while True: @@ -772,7 +773,7 @@ def backward_line(self): if self._index == -1: break yield self._tokens[self._index] - + def backward(self): """Yields tokens in backward direction across blocks.""" while True: @@ -781,23 +782,23 @@ def backward(self): if not self.previous_block(): break yield self._newline() - + def previous_block(self, at_end=True): """Go to the previous block, positioning the cursor at the end by default. - + Returns False if there was no previous block, else True. - + """ return self.move_to_block(self._doc.previous_block(self.block), at_end) - + def next_block(self, at_end=False): """Go to the next block, positioning the cursor at the start by default. - + Returns False if there was no next block, else True. - + """ return self.move_to_block(self._doc.next_block(self.block), at_end) - + def token(self): """Re-returns the last yielded token.""" if self._tokens: @@ -807,7 +808,7 @@ def token(self): elif index >= len(self._tokens): index = len(self._tokens) - 1 return self._tokens[index] - + def position(self): """Returns the position of the current token.""" if self._tokens: @@ -817,7 +818,7 @@ def position(self): return pos else: return self._d.position(self.block) - + def copy(self): """Return a new Runner at the current position.""" obj = type(self)(self._doc, self._wp) @@ -829,47 +830,47 @@ def copy(self): OUTSIDE = -1 PARTIAL = 0 -INSIDE = 1 +INSIDE = 1 class Source(object): """Helper iterator. - - Iterates over the (block, tokens) tuples from a Document (or a part - thereof). Stores the current block in the block attribute and the tokens + + Iterates over the (block, tokens) tuples from a Document (or a part + thereof). Stores the current block in the block attribute and the tokens (which also is a generator) in the tokens attribute. - + Iterating over the source object itself just yields the tokens, while the block attribute contains the current block. - + You can also iterate over the tokens attribute, which will yield the remaining tokens of the current block and then stop. - + If you specify a state, the tokens will update the state. If you specify state = True, the state will be taken from the document. - + """ - + def __init__(self, cursor, state=None, partial=INSIDE, tokens_with_position=False): """Initialize the iterator. - + cursor is a Cursor instance, describing a Document and a selected range state is, if given, a ly.lex.State instance or True (in which case the state is taken from the document). - + The following keyword arguments can be used: - + partial is either OUTSIDE, PARTIAL, or INSIDE: OUTSIDE: tokens that touch the selected range are also yielded PARTIAL: tokens that overlap the start or end positions are yielded INSIDE: (default) yield only tokens fully contained in the range The partial argument only makes sense if start or end are specified. - + If tokens_with_position is True, uses the document.tokens_with_position() method to get the tokens from the cursor's document, else (by default), the document.tokens() method is used. - + """ self._pushback = False self._last = None @@ -877,7 +878,7 @@ def __init__(self, cursor, state=None, partial=INSIDE, tokens_with_position=Fals start_block = document.block(cursor.start) self._wp = tokens_with_position tokens_method = document.tokens_with_position if tokens_with_position else document.tokens - + # start, end predicates start_pred, end_pred = { OUTSIDE: ( @@ -893,11 +894,12 @@ def __init__(self, cursor, state=None, partial=INSIDE, tokens_with_position=Fals lambda t: t.end > end_pos, ), }[partial] - + # if a state is given, use it (True: pick state from doc) if state: if state is True: state = document.state(start_block) + def token_source(block): for t in tokens_method(block): state.follow(t) @@ -906,13 +908,14 @@ def token_source(block): def token_source(block): return iter(tokens_method(block)) self.state = state - + # where to start if cursor.start: start_pos = cursor.start if not tokens_with_position: start_pos -= document.position(start_block) # token source for first block + def source_start(block): source = token_source(block) for t in source: @@ -922,19 +925,20 @@ def source_start(block): yield t else: source_start = token_source - + # where to end if cursor.end is not None: end_block = cursor.end_block() - end_pos = cursor.end + end_pos = cursor.end if not tokens_with_position: end_pos -= document.position(end_block) + def source_end(source): for t in source: if end_pred(t): break yield t - + # generate the tokens def generator(): source = source_start @@ -950,7 +954,7 @@ def generator(): yield block, source(block) source = token_source gen = generator() - + if tokens_with_position: def newline(): pos = document.position(self.block) - 1 @@ -959,11 +963,12 @@ def newline(): def newline(): pos = len(document.text(document.previous_block(self.block))) return ly.lex.Newline('\n', pos) - + # initialize block and tokens for self.block, self.tokens in gen: break # keep them going after the first line + def g(): for t in self.tokens: yield t @@ -972,69 +977,69 @@ def g(): for t in self.tokens: yield t self._gen = g() - + def __iter__(self): return self - + def __next__(self): if self._pushback: self._pushback = False return self._last i = self._last = next(self._gen) return i - + next = __next__ - + def pushback(self, pushback=True): """Yields the last yielded token again on the next request. - - This can be called multiple times, but only the last token will be - yielded again. You can also undo a call to pushback() using + + This can be called multiple times, but only the last token will be + yielded again. You can also undo a call to pushback() using pushback(False). - + """ self._pushback = pushback - + def token(self): """Re-returns the last yielded token.""" return self._last - + @property def document(self): """Return our Document.""" return self._doc - + def position(self, token): """Returns the position of the token in the current block. - - If the iterator was instantiated with tokens_with_position == True, + + If the iterator was instantiated with tokens_with_position == True, this position is the same as the token.pos attribute, and the current - block does not matter. (In that case you'll probably not use this + block does not matter. (In that case you'll probably not use this method.) - + """ pos = token.pos if not self._wp: pos += self._doc.position(self.block) return pos - + def until_parser_end(self): """Yield the tokens until the current parser is quit. - + You can only use this method if you have a State enabled. - + """ depth = self.state.depth() for t in self: yield t if self.state.depth() < depth and not self._pushback: break - + def consume(self, iterable, position): """Consumes iterable (supposed to be reading from us) until position. - + Returns the last token if that overlaps position. - + """ if self._doc.position(self.block) < position: for t in iterable: @@ -1044,5 +1049,3 @@ def consume(self, iterable, position): return elif end > position: return t - - diff --git a/ly/dom.py b/ly/dom.py index f50ba5b2..6769c7a6 100644 --- a/ly/dom.py +++ b/ly/dom.py @@ -36,7 +36,7 @@ """ from __future__ import unicode_literals -from __future__ import absolute_import # prevent picking old stale node.py from package +from __future__ import absolute_import # prevent picking old stale node.py from package try: string_types = basestring @@ -56,20 +56,20 @@ class LyNode(WeakNode): Base class for LilyPond objects, based on Node, which takes care of the tree structure. """ - + ## # True if this element is single LilyPond atom, word, note, etc. # When it is the only element inside { }, the brackets can be removed. isAtom = False - + ## # The number of newlines this object wants before it. before = 0 - + ## # The number of newlines this object wants after it. after = 0 - + def ly(self, printer): """ Returns printable output for this object. @@ -88,7 +88,7 @@ def concat(self, other): ## # Leaf and Container are the two base classes the rest of the LilyPond # element classes is based on. -# +# class Leaf(LyNode): """ A leaf node without children """ pass @@ -96,11 +96,11 @@ class Leaf(LyNode): class Container(LyNode): """ A node that concatenates its children on output """ - + ## # default character to concatenate children with defaultSpace = " " - + @property def before(self): if len(self): @@ -114,7 +114,7 @@ def after(self): return self[-1].after else: return 0 - + def ly(self, printer): if len(self) == 0: return '' @@ -136,18 +136,18 @@ class Printer(object): Performs certain operations on behalf of a LyNode tree, like quoting strings or translating pitch names, etc. """ - + # You may change these primary_quote_left = '\u2018' primary_quote_right = '\u2019' secondary_quote_left = '\u201C' secondary_quote_right = '\u201D' - + def __init__(self): self.typographicalQuotes = True self.language = "nederlands" self.indentString = ' ' - + def quoteString(self, text): if self.typographicalQuotes: text = re.sub(r'"(.*?)"', self.primary_quote_left + r'\1' + self.primary_quote_right, text) @@ -158,7 +158,7 @@ def quoteString(self, text): # quote the string return '"{0}"'.format(text) - def indentGen(self, node, startIndent = 0): + def indentGen(self, node, startIndent=0): """ A generator that walks on the output of the given node, and returns properly indented LilyPond code. @@ -185,9 +185,10 @@ class Reference(object): to display, and on all places in the document the name will show up. """ + def __init__(self, name=""): self.name = name - + def __format__(self, format_spec): return self.name @@ -199,11 +200,11 @@ class Named(object): be a Reference. """ name = "" - + def ly(self, printer): return "\\{0} {1}".format(self.name, super(Named, self).ly(printer)) - - + + class HandleVars(object): """ A powerful mixin class to facilitate handling unique variable assignments @@ -215,7 +216,7 @@ class HandleVars(object): that node again gets an autogenerated subnode of type QuotedString (if the argument wasn't already a Node). """ - childClass = None # To be filled in later + childClass = None # To be filled in later def ifbasestring(func): """ @@ -266,6 +267,7 @@ def importNode(self, obj): class AddDuration(object): """ Mixin to add a duration (as child). """ + def ly(self, printer): s = super(AddDuration, self).ly(printer) dur = self.find_child(Duration, 1) @@ -275,7 +277,7 @@ def ly(self, printer): class Block(Container): - """ + """ A vertical container type that puts everything on a new line. """ defaultSpace = "\n" @@ -283,7 +285,7 @@ class Block(Container): class Document(Container): - """ + """ A container type that puts everything on a new line. To be used as a full LilyPond document. """ @@ -296,10 +298,11 @@ class Document(Container): # class Text(Leaf): """ A leaf node with arbitrary text """ + def __init__(self, text="", parent=None): super(Text, self).__init__(parent) self.text = text - + def ly(self, printer): return self.text @@ -312,8 +315,8 @@ class TextDur(AddDuration, Text): class Line(Text): """ A text node that claims its own line. """ before, after = 1, 1 - - + + class Comment(Text): """ A LilyPond comment at the end of a line """ after = 1 @@ -325,31 +328,32 @@ def ly(self, printer): class LineComment(Comment): """ A LilyPond comment that takes a full line """ before = 1 - + class BlockComment(Comment): """ A block comment between %{ and %} """ @property def before(self): return '\n' in self.text and 1 or 0 - + @property def after(self): return '\n' in self.text and 1 or 0 - + def ly(self, printer): text = self.text.replace('%}', '') f = "%{{\n{0}\n%}}" if '\n' in text else "%{{ {0} %}}" return f.format(text) - + class QuotedString(Text): """ A string that is output inside double quotes. """ isAtom = True + def ly(self, printer): # we call format(), since self.text MIGHT be a Reference... return printer.quoteString(format(self.text)) - + class Newline(LyNode): """ A newline. """ @@ -359,24 +363,26 @@ class Newline(LyNode): class BlankLine(Newline): """ A blank line. """ before = 1 - + class Scheme(Text): """ A Scheme expression, without the extra # prepended """ isAtom = True - + def ly(self, printer): return '#' + self.text class Version(Line): """ a LilyPond version instruction """ + def ly(self, printer): return r'\version "{0}"'.format(self.text) class Include(Line): r""" a LilyPond \include statement """ + def ly(self, printer): return r'\include "{0}"'.format(self.text) @@ -388,13 +394,13 @@ class Assignment(Container): where this varname is referenced, the name is the same. """ before, after = 1, 1 - + def __init__(self, name=None, parent=None, valueObj=None): super(Assignment, self).__init__(parent) self.name = name if valueObj: self.append(valueObj) - + # Convenience methods: def setValue(self, obj): if len(self): @@ -419,11 +425,11 @@ class Identifier(Leaf): Name may be a string or a Reference object. """ isAtom = True - + def __init__(self, name=None, parent=None): super(Identifier, self).__init__(parent) self.name = name - + def ly(self, printer): return "\\{0}".format(self.name) @@ -433,7 +439,7 @@ class Statement(Named, Container): Base class for statements with arguments. The statement is read in the name attribute, the arguments are the children. """ - before = 0 # do not read property from container + before = 0 # do not read property from container isAtom = True @@ -442,11 +448,12 @@ class Command(Statement): Use this to create a LilyPond command supplying the name (or a Reference) when instantiating. """ + def __init__(self, name, parent=None): super(Command, self).__init__(parent) self.name = name - + class Enclosed(Container): """ Encloses all children between brackets: { ... } @@ -458,7 +465,7 @@ class Enclosed(Container): pre, post = "{", "}" before, after = 0, 0 isAtom = True - + def ly(self, printer): if len(self) == 0: return " ".join((self.pre, self.post)) @@ -476,16 +483,20 @@ def ly(self, printer): class Seq(Enclosed): """ An SequentialMusic expression between { } """ pre, post = "{", "}" - + class Sim(Enclosed): """ An SimultaneousMusic expression between << >> """ pre, post = "<<", ">>" -class Seqr(Seq): may_remove_brackets = True -class Simr(Sim): may_remove_brackets = True - +class Seqr(Seq): + may_remove_brackets = True + + +class Simr(Sim): + may_remove_brackets = True + class SchemeLily(Enclosed): """ A LilyPond expression between #{ #} (inside scheme) """ @@ -495,7 +506,7 @@ class SchemeLily(Enclosed): class SchemeList(Enclosed): """ A list of items enclosed in parentheses """ pre, post = "(", ")" - + def ly(self, printer): return self.pre + Container.ly(self, printer) + self.post @@ -510,15 +521,16 @@ class StatementEnclosed(Named, Enclosed): class CommandEnclosed(StatementEnclosed): """ - Use this to print LilyPond commands that have a single + Use this to print LilyPond commands that have a single bracket-enclosed list of arguments. The command name is supplied to the constructor. """ + def __init__(self, name, parent=None): super(CommandEnclosed, self).__init__(parent) self.name = name - - + + class Section(StatementEnclosed): """ Very much like a Statement. Use as base class for \\book { }, \\score { } @@ -528,20 +540,39 @@ class Section(StatementEnclosed): before, after = 1, 1 -class Book(Section): name = 'book' -class BookPart(Section): name = 'bookpart' -class Score(Section): name = 'score' -class Paper(HandleVars, Section): name = 'paper' -class Layout(HandleVars, Section): name = 'layout' -class Midi(HandleVars, Section): name = 'midi' -class Header(HandleVars, Section): name = 'header' +class Book(Section): + name = 'book' + + +class BookPart(Section): + name = 'bookpart' + + +class Score(Section): + name = 'score' + + +class Paper(HandleVars, Section): + name = 'paper' + + +class Layout(HandleVars, Section): + name = 'layout' + + +class Midi(HandleVars, Section): + name = 'midi' + + +class Header(HandleVars, Section): + name = 'header' class With(HandleVars, Section): """ If this item has no children, it prints nothing. """ name = 'with' before, after = 0, 0 - + def ly(self, printer): if len(self): return super(With, self).ly(printer) @@ -553,6 +584,7 @@ class ContextName(Text): """ Used to print a context name, like \\Score. """ + def ly(self, printer): return "\\" + self.text @@ -562,12 +594,12 @@ class Context(HandleVars, Section): A \context section for use inside Layout or Midi sections. """ name = 'context' - + def __init__(self, contextName="", parent=None): super(Context, self).__init__(parent) if contextName: ContextName(contextName, self) - + class ContextType(Container): r""" @@ -581,12 +613,12 @@ class ContextType(Container): before, after = 1, 1 isAtom = True ctype = None - + def __init__(self, cid=None, new=True, parent=None): super(ContextType, self).__init__(parent) self.new = new self.cid = cid - + def ly(self, printer): res = [] res.append(self.new and "\\new" or "\\context") @@ -596,7 +628,7 @@ def ly(self, printer): res.append(printer.quoteString(format(self.cid))) res.append(super(ContextType, self).ly(printer)) return " ".join(res) - + def getWith(self): """ Gets the attached with clause. Creates it if not there. @@ -613,31 +645,94 @@ def addInstrumentNameEngraverIfNecessary(self): to print instrument names. """ if not isinstance(self, - (Staff, RhythmicStaff, PianoStaff, Lyrics, FretBoards)): + (Staff, RhythmicStaff, PianoStaff, Lyrics, FretBoards)): Line('\\consists "Instrument_name_engraver"', self.getWith()) -class ChoirStaff(ContextType): pass -class ChordNames(ContextType): pass -class CueVoice(ContextType): pass -class Devnull(ContextType): pass -class DrumStaff(ContextType): pass -class DrumVoice(ContextType): pass -class Dynamics(ContextType): pass -class FiguredBass(ContextType): pass -class FretBoards(ContextType): pass -class Global(ContextType): pass -class GrandStaff(ContextType): pass -class GregorianTranscriptionStaff(ContextType): pass -class GregorianTranscriptionVoice(ContextType): pass -class InnerChoirStaff(ContextType): pass -class InnerStaffGroup(ContextType): pass -class Lyrics(ContextType): pass -class MensuralStaff(ContextType): pass -class MensuralVoice(ContextType): pass -class NoteNames(ContextType): pass -class PianoStaff(ContextType): pass -class RhythmicStaff(ContextType): pass +class ChoirStaff(ContextType): + pass + + +class ChordNames(ContextType): + pass + + +class CueVoice(ContextType): + pass + + +class Devnull(ContextType): + pass + + +class DrumStaff(ContextType): + pass + + +class DrumVoice(ContextType): + pass + + +class Dynamics(ContextType): + pass + + +class FiguredBass(ContextType): + pass + + +class FretBoards(ContextType): + pass + + +class Global(ContextType): + pass + + +class GrandStaff(ContextType): + pass + + +class GregorianTranscriptionStaff(ContextType): + pass + + +class GregorianTranscriptionVoice(ContextType): + pass + + +class InnerChoirStaff(ContextType): + pass + + +class InnerStaffGroup(ContextType): + pass + + +class Lyrics(ContextType): + pass + + +class MensuralStaff(ContextType): + pass + + +class MensuralVoice(ContextType): + pass + + +class NoteNames(ContextType): + pass + + +class PianoStaff(ContextType): + pass + + +class RhythmicStaff(ContextType): + pass + + class ScoreContext(ContextType): r""" Represents the Score context in LilyPond, but the name would @@ -648,13 +743,33 @@ class ScoreContext(ContextType): """ ctype = 'Score' -class Staff(ContextType): pass -class StaffGroup(ContextType): pass -class TabStaff(ContextType): pass -class TabVoice(ContextType): pass -class VaticanaStaff(ContextType): pass -class VaticanaVoice(ContextType): pass -class Voice(ContextType): pass + +class Staff(ContextType): + pass + + +class StaffGroup(ContextType): + pass + + +class TabStaff(ContextType): + pass + + +class TabVoice(ContextType): + pass + + +class VaticanaStaff(ContextType): + pass + + +class VaticanaVoice(ContextType): + pass + + +class Voice(ContextType): + pass class UserContext(ContextType): @@ -662,6 +777,7 @@ class UserContext(ContextType): Represents a context the user creates. e.g. \new MyStaff = cid << music >> """ + def __init__(self, ctype, cid=None, new=True, parent=None): super(UserContext, self).__init__(cid, new, parent) self.ctype = ctype @@ -672,6 +788,7 @@ class ContextProperty(Leaf): A Context.property or Context.layoutObject construct. Call e.g. ContextProperty('aDueText', 'Staff') to get 'Staff.aDueText'. """ + def __init__(self, prop, context=None, parent=None): self.prop = prop self.context = context @@ -697,19 +814,47 @@ class InputMode(StatementEnclosed): pass -class ChordMode(InputMode): name = 'chordmode' -class InputChords(ChordMode): name = 'chords' -class LyricMode(InputMode): name = 'lyricmode' -class InputLyrics(LyricMode): name = 'lyrics' -class NoteMode(InputMode): name = 'notemode' -class InputNotes(NoteMode): name = 'notes' -class FigureMode(InputMode): name = 'figuremode' -class InputFigures(FigureMode): name = 'figures' -class DrumMode(InputMode): name = 'drummode' -class InputDrums(DrumMode): name = 'drums' +class ChordMode(InputMode): + name = 'chordmode' + + +class InputChords(ChordMode): + name = 'chords' + + +class LyricMode(InputMode): + name = 'lyricmode' + + +class InputLyrics(LyricMode): + name = 'lyrics' + + +class NoteMode(InputMode): + name = 'notemode' + + +class InputNotes(NoteMode): + name = 'notes' + + +class FigureMode(InputMode): + name = 'figuremode' + +class InputFigures(FigureMode): + name = 'figures' -class AddLyrics(InputLyrics): + +class DrumMode(InputMode): + name = 'drummode' + + +class InputDrums(DrumMode): + name = 'drums' + + +class AddLyrics(InputLyrics): name = 'addlyrics' may_remove_brackets = False before, after = 1, 1 @@ -717,17 +862,17 @@ class AddLyrics(InputLyrics): class LyricsTo(LyricMode): name = 'lyricsto' - + def __init__(self, cid, parent=None): super(LyricsTo, self).__init__(parent) self.cid = cid - + def ly(self, printer): res = ["\\" + self.name] res.append(printer.quoteString(format(self.cid))) res.append(Enclosed.ly(self, printer)) return " ".join(res) - + class Pitch(Leaf): """ @@ -763,9 +908,10 @@ class Duration(Leaf): dots (number of dots), factor (Fraction giving the scaling of the duration). """ + def __init__(self, dur, dots=0, factor=1, parent=None): super(Duration, self).__init__(parent) - self.dur = dur # log + self.dur = dur # log self.dots = dots self.factor = fractions.Fraction(factor) @@ -778,6 +924,7 @@ class Chord(Container): A chord containing one of more Pitches and optionally one Duration. This is a bit of a hack, awaiting real music object support. """ + def ly(self, printer): pitches = list(self.find_children(Pitch, 1)) if len(pitches) == 1: @@ -816,6 +963,7 @@ class KeySignature(Leaf): The pitch should be given in the arguments note and alter and is written out in the document's language. """ + def __init__(self, note=0, alter=0, mode="major", parent=None): super(KeySignature, self).__init__(parent) self.note = note @@ -831,6 +979,7 @@ class TimeSignature(Leaf): r""" A time signature, like: \time 4/4 """ + def __init__(self, num, beat, parent=None): super(TimeSignature, self).__init__(parent) self.num = num @@ -847,20 +996,20 @@ class Partial(Named, Duration): """ name = "partial" before, after = 1, 1 - - + + class Tempo(Container): r""" A tempo setting, like: \tempo 4 = 100 May have a child markup or quoted string. """ before, after = 1, 1 - + def __init__(self, duration, value, parent=None): super(Tempo, self).__init__(parent) self.duration = duration self.value = value - + def ly(self, printer): result = ['\\tempo'] if len(self) > 0: @@ -868,12 +1017,13 @@ def ly(self, printer): if self.value: result.append("{0}={1}".format(self.duration, self.value)) return ' '.join(result) - - + + class Clef(Leaf): """ A clef. """ + def __init__(self, clef, parent=None): super(Clef, self).__init__(parent) self.clef = clef @@ -887,6 +1037,7 @@ class VoiceSeparator(Leaf): r""" A Voice Separator: \\ """ + def ly(self, printer): return r'\\' @@ -924,6 +1075,3 @@ class MarkupCommand(Command): If one argument can be a markup list, use a Enclosed() construct for that. """ pass - - - diff --git a/ly/duration.py b/ly/duration.py index e1bd11a3..1ddeb7ed 100644 --- a/ly/duration.py +++ b/ly/duration.py @@ -35,12 +35,12 @@ def tostring(dur, dots=0, factor=1): r"""Returns the LilyPond string representation of a given logarithmic duration. - + Supports values from -3 up to and including 11. -2 = '\longa', 0 = '1' (whole note), etc. - + Adds the number of dots (defaults to 0) and the fraction factor if given. - + """ s = durations[dur + 3] + '.' * dots if factor != 1: @@ -97,5 +97,3 @@ def format_fraction(value): return "{0}/{1}".format(value.numerator, value.denominator) else: return "{0}/1".format(value) - - diff --git a/ly/etreeutil.py b/ly/etreeutil.py index d9ca1b2b..2a11a474 100644 --- a/ly/etreeutil.py +++ b/ly/etreeutil.py @@ -31,9 +31,9 @@ def isblank(s): def indent(elem, indent_string=" ", level=0): """Indent the XML in element. - + Text content that is already non-whitespace is not changed. - + """ # based on http://effbot.org/zone/element-lib.htm#prettyprint i = "\n" + indent_string * level @@ -49,5 +49,3 @@ def indent(elem, indent_string=" ", level=0): else: if level and isblank(elem.tail): elem.tail = i - - diff --git a/ly/indent.py b/ly/indent.py index d46787a2..87f33055 100644 --- a/ly/indent.py +++ b/ly/indent.py @@ -28,21 +28,21 @@ class Indenter(object): - + # variables indent_tabs = False # use tabs for indent indent_width = 2 # amount of spaces if indent_tabs == False - + def __init__(self): pass - + def indent(self, cursor, indent_blank_lines=False): """Indent all lines in the cursor's range. - - If indent_blank_lines is True, the indent of blank lines is made - larger if necessary. If False (the default), the indent of blank + + If indent_blank_lines is True, the indent of blank lines is made + larger if necessary. If False (the default), the indent of blank lines if not changed if it is shorter than it should be. - + """ indents = [''] start_block, end_block = cursor.start_block(), cursor.end_block() @@ -53,9 +53,9 @@ def indent(self, cursor, indent_blank_lines=False): for b in cursor.document: if b == start_block: in_range = True - + line = Line(d, b) - + # handle indents of prev line if pline: if pline.indent is not False: @@ -70,22 +70,22 @@ def indent(self, cursor, indent_blank_lines=False): new_indent += '\t' if self.indent_tabs else ' ' * self.indent_width indents.append(new_indent) del indents[max(1, len(indents) - line.dedenters_start):] - + # if we may not change the indent just remember the current if line.indent is not False: if not in_range: indents[-1] = line.indent elif not indent_blank_lines and line.isblank and indents[-1].startswith(line.indent): - pass # don't make shorter indents longer on blank lines + pass # don't make shorter indents longer on blank lines elif line.indent != indents[-1]: d[d.position(b):d.position(b)+len(line.indent)] = indents[-1] del indents[max(1, len(indents) - line.dedenters_end):] - + if b == end_block: break - + pline = line - + def increase_indent(self, cursor): """Manually add indent to all lines of cursor.""" indent = '\t' if self.indent_tabs else ' ' * self.indent_width @@ -100,7 +100,7 @@ def increase_indent(self, cursor): else: ins += tokens[0].end d[ins:ins] = indent - + def decrease_indent(self, cursor): """Manually remove one level of indent from all lines of cursor.""" with cursor.document as d: @@ -116,7 +116,7 @@ def decrease_indent(self, cursor): end = pos + len(space) if '\t' in space and space.endswith(' '): # strip alignment - del d[pos + space.rfind('\t') + 1 : end] + del d[pos + space.rfind('\t') + 1: end] elif space.endswith('\t'): # just strip one tab del d[end - 1] @@ -124,30 +124,30 @@ def decrease_indent(self, cursor): del d[end - self.indent_width: end] else: del d[pos:end] - + def get_indent(self, document, block): """Return the indent the block currently has. - + Returns False if the block is not indentable, e.g. when it is part of a multiline string. - + """ return Line(document, block).indent - + def compute_indent(self, document, block): """Return the indent the specified block should have. - + Returns False if the block is not indentable, e.g. when it is part of a multiline string. - - This method is used to determine the indent of one line, and just - looks to previous lines, copying the indent of the line where the - current indent depth starts, and/or adding a level of indent or + + This method is used to determine the indent of one line, and just + looks to previous lines, copying the indent of the line where the + current indent depth starts, and/or adding a level of indent or alignment space. - - Use this method only for one line or the first of a group you're + + Use this method only for one line or the first of a group you're indenting. - + """ line = Line(document, block) if line.indent is False: @@ -171,7 +171,7 @@ def compute_indent(self, document, block): depth += line.dedenters_start else: return "" - + # here we arrive after 'break' i = line.indent if i is False: @@ -190,63 +190,64 @@ def compute_indent(self, document, block): class Line(object): """Brings together all relevant information about a line (block).""" + def __init__(self, document, block): """Initialize with a block (line) of the document. - + After init, the following attributes are set: - + indent - - The indent the current line has. This is a string containing - whitespace (i.e. spaces and/or tabs) which can be empty. A special - case is False, which means the current line is not indentable, e.g. - it is a multiline string and should never be automatically be + + The indent the current line has. This is a string containing + whitespace (i.e. spaces and/or tabs) which can be empty. A special + case is False, which means the current line is not indentable, e.g. + it is a multiline string and should never be automatically be re-indented. - - + + isblank - - True if the line is empty or white-space only. It is False when the - indent attribute is also False (e.g. when the line is part of a + + True if the line is empty or white-space only. It is False when the + indent attribute is also False (e.g. when the line is part of a multiline string). - - + + dedenters_start - - The number of dedent tokens that should cause the indenter to go a + + The number of dedent tokens that should cause the indenter to go a level up. - - + + dedenters_end - - The number of dedent tokens that should cause the next line to go a + + The number of dedent tokens that should cause the next line to go a level up. - - + + indenters - - A list of tuples (align, indent). Each item corresponds with an - indent that starts on the line. The align value (integer) determines - the position the next line should be padded to with spaces, 0 or - None means no alignment. The indent value (bool) specifies if there + + A list of tuples (align, indent). Each item corresponds with an + indent that starts on the line. The align value (integer) determines + the position the next line should be padded to with spaces, 0 or + None means no alignment. The indent value (bool) specifies if there should a new indent level be added (a tab or some amount of spaces). - + """ state = document.state(block) tokens = document.tokens(block) - + # are we in a multi-line string? if isinstance(state.parser(), ( - ly.lex.lilypond.ParseString, - ly.lex.scheme.ParseString, - )): + ly.lex.lilypond.ParseString, + ly.lex.scheme.ParseString, + )): self.indent = False self.isblank = False # or a multi-line comment? elif isinstance(state.parser(), ( - ly.lex.lilypond.ParseBlockComment, - ly.lex.scheme.ParseBlockComment, - )): + ly.lex.lilypond.ParseBlockComment, + ly.lex.scheme.ParseBlockComment, + )): # do allow indent the last line of a block comment if it only # contains space if tokens and isinstance(tokens[0], ly.lex.BlockCommentEnd): @@ -269,8 +270,8 @@ def __init__(self, document, block): find_dedenters = True self.dedenters_start = 0 self.dedenters_end = 0 - - # quickly iter over the tokens, collecting the indent tokens and + + # quickly iter over the tokens, collecting the indent tokens and # possible stuff to align to after the indent tokens indenters = [] for t in tokens: @@ -292,7 +293,7 @@ def __init__(self, document, block): find_dedenters = False if indenters: indenters[-1].append(t) - + # now analyse the indent tokens that are not closed on the same line # and determine how the next line should be indented self.indenters = [] @@ -310,17 +311,16 @@ def __init__(self, document, block): else: align, indent = None, True self.indenters.append((align, indent)) - + def is_alignable_scheme_keyword(self, token): """Return True if token is an alignable Scheme word like "if", etc.""" return isinstance(token, ly.lex.scheme.Word) and token in ( - # Scheme commands that can have one argument on the same line and - # then want the next arguments on the next lines at the same + # Scheme commands that can have one argument on the same line and + # then want the next arguments on the next lines at the same # position. 'if', 'and', 'or', 'set!', '=', '<', '<=', '>', '>=', 'eq?', 'eqv?', 'equal?', 'filter', ) - diff --git a/ly/lex/__init__.py b/ly/lex/__init__.py index e3254c5b..760060e4 100644 --- a/ly/lex/__init__.py +++ b/ly/lex/__init__.py @@ -123,8 +123,8 @@ class Parser(slexer.Parser): argcount = 0 default = Unparsed mode = None - - def __init__(self, argcount = None): + + def __init__(self, argcount=None): if argcount is not None: self.argcount = argcount @@ -147,7 +147,7 @@ def endArgument(self): if p.argcount > 0: p.argcount -= 1 return - + def mode(self): """Returns the mode attribute of the first parser (from current parser) that has it.""" for parser in self.state[::-1]: @@ -156,7 +156,7 @@ def mode(self): class Fridge(slexer.Fridge): - def __init__(self, stateClass = State): + def __init__(self, stateClass=State): super(Fridge, self).__init__(stateClass) @@ -168,5 +168,3 @@ def state(mode): def guessState(text): """Returns a State instance, guessing the type of text.""" return State(modes[guessMode(text)]()) - - diff --git a/ly/lex/_mode.py b/ly/lex/_mode.py index 4801f762..f948caa4 100644 --- a/ly/lex/_mode.py +++ b/ly/lex/_mode.py @@ -28,12 +28,12 @@ This maps a mode name to a function returning the base parser class for that mode. (This way the corresponding module only needs to be imported when the mode is really needed.) - + 2. the guessMode function. This tries to guess the type of the given text and returns a mode name. - - + + You can easily add more modes in separate modules and mention them here, Don't use this module directly! modes and guessMode are imported in the main @@ -48,42 +48,42 @@ def _modes(): """Returns a dictionary mapping a mode name to a function. - + The function should return the initial Parser instance for that mode. - + """ - + def lilypond(): from . import lilypond return lilypond.ParseGlobal - + def scheme(): from . import scheme return scheme.ParseScheme - + def docbook(): from . import docbook return docbook.ParseDocBook - + def latex(): from . import latex return latex.ParseLaTeX - + def texinfo(): from . import texinfo return texinfo.ParseTexinfo - + def html(): from . import html return html.ParseHTML - + def mup(): from . import mup return mup.ParseMup - + # more modes can be added here return locals() - + # dictionary mapping mode name to a function returning initial parser instance # for that mode. Can also be used to test the existence of a mode @@ -93,9 +93,9 @@ def mup(): def guessMode(text): """Tries to guess the type of the input text, using a quite fast heuristic. - + Returns one of the strings also present as key in the modes dictionary. - + """ text = text.lstrip() if text.startswith(('%', '\\')): @@ -123,7 +123,6 @@ def guessMode(text): return "lilypond" - # dictionary mapping mode name to a default extension for a file of that mode. extensions = { 'lilypond': '.ly', @@ -134,4 +133,3 @@ def guessMode(text): 'docbook': '.docbook', 'mup': '.mup', } - diff --git a/ly/lex/_token.py b/ly/lex/_token.py index 713fb1d0..ca878048 100644 --- a/ly/lex/_token.py +++ b/ly/lex/_token.py @@ -48,18 +48,19 @@ def __repr__(self): class patternproperty(object): """Property that caches the return value of its function and returns that next time. - + Use this if the rx attribute (the pattern string to match tokens for) of a Token subclass is already costly to create and you want it created lazily (i.e. only when parsing starts): - + @patternproperty def rx(): ...complicated function returning the regular expression string... - + """ + def __init__(self, func): self.func = func - + def __get__(self, instance, owner): try: return self.rx @@ -75,12 +76,14 @@ class Unparsed(Token): # some token types with special behaviour: class Item(Token): """A token that decreases the argument count of the current parser.""" + def update_state(self, state): state.endArgument() class Leaver(Token): """A token that leaves the current parser.""" + def update_state(self, state): state.leave() @@ -103,8 +106,8 @@ class Comment(Token): class LineComment(Comment): """Base class for items that are a whole line comment.""" - - + + class BlockComment(Comment): """Base class for tokens that belong to a block/multiline comment.""" @@ -119,24 +122,24 @@ class BlockCommentEnd(BlockComment): class String(Token): """Base class for tokens that belong to a quote-delimited string.""" - - + + class StringStart(String): """Base class for tokens that start a quote-delimited string.""" - - + + class StringEnd(String): """Base class for tokens that end a quote-delimited string.""" - - + + class Character(Token): """Base class for tokens that are an (escaped) character.""" - - + + class Numeric(Token): """Base class for tokens that are a numerical value.""" - - + + class Error(Token): """Base class for tokens that represent erroneous input.""" @@ -148,18 +151,18 @@ class Error(Token): # of the way other types could be nested. class MatchStart(object): """Mixin class for tokens that have a matching token forward in the text. - + The matchname attribute should give a unique name. - + """ matchname = "" class MatchEnd(object): """Mixin class for tokens that have a matching token backward in the text. - + The matchname attribute should give a unique name. - + """ matchname = "" @@ -171,5 +174,3 @@ class Indent(object): class Dedent(object): """Mixin class for tokens that have the text on the next line indent less.""" - - diff --git a/ly/lex/docbook.py b/ly/lex/docbook.py index 16406d21..2ba9da68 100644 --- a/ly/lex/docbook.py +++ b/ly/lex/docbook.py @@ -32,4 +32,3 @@ class ParseDocBook(Parser): items = ( _token.Space, ) - diff --git a/ly/lex/html.py b/ly/lex/html.py index d708006f..745f6676 100644 --- a/ly/lex/html.py +++ b/ly/lex/html.py @@ -35,10 +35,11 @@ class Comment(_token.Comment): class CommentStart(Comment, _token.BlockCommentStart): rx = r"" @@ -53,43 +54,47 @@ class Tag(_token.Token): class TagStart(Tag): rx = r"" - + class AttrName(_token.Token): rx = r"\w+([-_:]\w+)?" - - + + class EqualSign(_token.Token): rx = "=" + def update_state(self, state): state.enter(ParseValue()) class Value(_token.Leaver): rx = r"\w+" - + class StringDQStart(String, _token.StringStart): rx = r'"' + def update_state(self, state): state.enter(ParseStringDQ()) class StringSQStart(String, _token.StringStart): rx = r"'" + def update_state(self, state): state.enter(ParseStringSQ()) - + class StringDQEnd(String, _token.StringEnd, _token.Leaver): rx = r'"' - + class StringSQEnd(String, _token.StringEnd, _token.Leaver): rx = r"'" @@ -109,6 +114,7 @@ class LilyPondVersionTag(LilyPondTag): class LilyPondFileTag(LilyPondTag): rx = r"" - - + + class LilyPondTagEnd(LilyPondTag): rx = r">" + def update_state(self, state): state.replace(ParseLilyPond()) class LilyPondInlineTagEnd(LilyPondTag, _token.Leaver): rx = r"/?>" - + class SemiColon(_token.Token): rx = r":" + def update_state(self, state): state.replace(ParseLilyPondInline()) - # Parsers: class ParseHTML(Parser): @@ -176,7 +184,7 @@ class ParseStringDQ(Parser): StringDQEnd, EntityRef, ) - + class ParseStringSQ(Parser): default = String @@ -184,7 +192,7 @@ class ParseStringSQ(Parser): StringSQEnd, EntityRef, ) - + class ParseComment(Parser): default = Comment @@ -199,6 +207,7 @@ class ParseValue(FallthroughParser): _token.Space, Value, ) + def fallthrough(self, state): state.leave() @@ -213,7 +222,7 @@ class ParseLilyPondAttr(Parser): LilyPondTagEnd, SemiColon, ) - + class ParseLilyPondFileOptions(Parser): items = ( @@ -230,11 +239,9 @@ class ParseLilyPond(lilypond.ParseGlobal): items = ( LilyPondCloseTag, ) + lilypond.ParseGlobal.items - + class ParseLilyPondInline(lilypond.ParseMusic): items = ( LilyPondInlineTagEnd, ) + lilypond.ParseMusic.items - - diff --git a/ly/lex/latex.py b/ly/lex/latex.py index bc2a1e4e..6a04bb09 100644 --- a/ly/lex/latex.py +++ b/ly/lex/latex.py @@ -32,5 +32,3 @@ class ParseLaTeX(Parser): items = ( _token.Space, ) - - diff --git a/ly/lex/lilypond.py b/ly/lex/lilypond.py index a432b8aa..77ed23df 100644 --- a/ly/lex/lilypond.py +++ b/ly/lex/lilypond.py @@ -80,8 +80,8 @@ class IntegerValue(DecimalValue): class Fraction(Value): rx = r"\d+/\d+" - - + + class Delimiter(_token.Token): pass @@ -101,6 +101,7 @@ class Comment(_token.Comment): class BlockCommentStart(Comment, _token.BlockCommentStart): rx = r"%{" + def update_state(self, state): state.enter(ParseBlockComment()) @@ -115,7 +116,7 @@ class BlockComment(Comment, _token.BlockComment): class LineComment(Comment, _token.LineComment): rx = r"%.*$" - + class String(_token.String): pass @@ -123,12 +124,14 @@ class String(_token.String): class StringQuotedStart(String, _token.StringStart): rx = r'"' + def update_state(self, state): state.enter(ParseString()) - + class StringQuotedEnd(String, _token.StringEnd): rx = r'"' + def update_state(self, state): state.leave() state.endArgument() @@ -141,19 +144,19 @@ class StringQuoteEscape(_token.Character): class MusicItem(_token.Token): r"""A note, rest, spacer, ``\skip`` or ``q``.""" - + class Skip(MusicItem): rx = r"\\skip" + re_identifier_end class Spacer(MusicItem): rx = r"s(?![A-Za-z])" - - + + class Rest(MusicItem): rx = r"[Rr](?![A-Za-z])" - - + + class Note(MusicItem): rx = r"[a-x]+(?![A-Za-z])" @@ -192,18 +195,19 @@ class Duration(_token.Token): class Length(Duration): rx = re_duration + def update_state(self, state): state.enter(ParseDuration()) class Dot(Duration): rx = re_dot - - + + class Scaling(Duration): rx = re_scaling - - + + class OpenBracket(Delimiter, _token.MatchStart, _token.Indent): """An open bracket, does not enter different parser, subclass or reimplement Parser.update_state().""" rx = r"\{" @@ -213,10 +217,11 @@ class OpenBracket(Delimiter, _token.MatchStart, _token.Indent): class CloseBracket(Delimiter, _token.MatchEnd, _token.Dedent): rx = r"\}" matchname = "bracket" + def update_state(self, state): state.leave() - state.endArgument() - + state.endArgument() + class OpenSimultaneous(Delimiter, _token.MatchStart, _token.Indent): """An open double French quote, does not enter different parser, subclass or reimplement Parser.update_state().""" @@ -227,10 +232,11 @@ class OpenSimultaneous(Delimiter, _token.MatchStart, _token.Indent): class CloseSimultaneous(Delimiter, _token.MatchEnd, _token.Dedent): rx = r">>" matchname = "simultaneous" + def update_state(self, state): state.leave() state.endArgument() - + class SequentialStart(OpenBracket): def update_state(self, state): @@ -275,10 +281,11 @@ def test_match(cls, match): if s in l: return True return False - - + + class Direction(_token.Token): rx = r"[-_^]" + def update_state(self, state): state.enter(ParseScriptAbbreviationOrFingering()) @@ -302,22 +309,22 @@ class Slur(_token.Token): class SlurStart(Slur, _token.MatchStart): rx = r"\(" matchname = "slur" - + class SlurEnd(Slur, _token.MatchEnd): rx = r"\)" matchname = "slur" - + class PhrasingSlurStart(SlurStart): rx = r"\\\(" matchname = "phrasingslur" - - + + class PhrasingSlurEnd(SlurEnd): rx = r"\\\)" matchname = "phrasingslur" - + class Tie(Slur): rx = r"~" @@ -344,19 +351,20 @@ class Ligature(_token.Token): class LigatureStart(Ligature, _token.MatchStart): rx = r"\\\[" matchname = "ligature" - - + + class LigatureEnd(Ligature, _token.MatchEnd): rx = r"\\\]" matchname = "ligature" - - + + class Tremolo(_token.Token): pass class TremoloColon(Tremolo): rx = r":" + def update_state(self, state): state.enter(ParseTremolo()) @@ -387,7 +395,7 @@ class DotChord(ChordItem): class VoiceSeparator(Delimiter): rx = r"\\\\" - + class Dynamic(_token.Token): rx = re_dynamic @@ -401,7 +409,7 @@ def test_match(cls, match): from .. import words return s in words.lilypond_music_commands return False - + class Keyword(_token.Item, IdentifierRef): @classmethod @@ -420,54 +428,63 @@ class Specifier(_token.Token): class Score(Keyword): rx = r"\\score\b" + def update_state(self, state): state.enter(ExpectScore()) - + class Book(Keyword): rx = r"\\book\b" + def update_state(self, state): state.enter(ExpectBook()) - - + + class BookPart(Keyword): rx = r"\\bookpart\b" + def update_state(self, state): state.enter(ExpectBookPart()) class Paper(Keyword): rx = r"\\paper\b" + def update_state(self, state): state.enter(ExpectPaper()) class Header(Keyword): rx = r"\\header\b" + def update_state(self, state): state.enter(ExpectHeader()) class Layout(Keyword): rx = r"\\layout\b" + def update_state(self, state): state.enter(ExpectLayout()) class Midi(Keyword): rx = r"\\midi\b" + def update_state(self, state): state.enter(ExpectMidi()) class With(Keyword): rx = r"\\with\b" + def update_state(self, state): state.enter(ExpectWith()) class LayoutContext(Keyword): rx = r"\\context\b" + def update_state(self, state): state.enter(ExpectContext()) @@ -478,18 +495,21 @@ class Markup(_token.Item): class MarkupStart(Markup, Command): rx = r"\\markup" + re_identifier_end + def update_state(self, state): state.enter(ParseMarkup(1)) class MarkupLines(Markup): rx = r"\\markuplines" + re_identifier_end + def update_state(self, state): state.enter(ParseMarkup(1)) class MarkupList(Markup): rx = r"\\markuplist" + re_identifier_end + def update_state(self, state): state.enter(ParseMarkup(1)) @@ -500,7 +520,7 @@ class MarkupCommand(Markup, IdentifierRef): def test_match(cls, match): from .. import words return match.group()[1:] in words.markupcommands - + def update_state(self, state): from .. import words command = self[1:] @@ -517,12 +537,14 @@ def update_state(self, state): class MarkupScore(Markup): rx = r"\\score\b" + def update_state(self, state): state.enter(ExpectScore()) class MarkupUserCommand(Markup, IdentifierRef): """A user-defined markup (i.e. not in the words markupcommands list).""" + def update_state(self, state): state.endArgument() @@ -543,21 +565,22 @@ def update_state(self, state): while state.parser().argcount > 0: state.leave() state.leave() - state.endArgument() + state.endArgument() class Repeat(Command): rx = r"\\repeat(?![A-Za-z])" + def update_state(self, state): state.enter(ParseRepeat()) - - + + class RepeatSpecifier(Specifier): @_token.patternproperty def rx(): from .. import words return r"\b({0})(?![A-Za-z])".format("|".join(words.repeat_types)) - + class RepeatCount(IntegerValue, _token.Leaver): pass @@ -565,6 +588,7 @@ class RepeatCount(IntegerValue, _token.Leaver): class Tempo(Command): rx = r"\\tempo\b" + def update_state(self, state): state.enter(ParseTempo()) @@ -579,30 +603,35 @@ class Partial(Command): class Override(Keyword): rx = r"\\override\b" + def update_state(self, state): state.enter(ParseOverride()) class Set(Override): rx = r"\\set\b" + def update_state(self, state): state.enter(ParseSet()) - + class Revert(Override): rx = r"\\revert\b" + def update_state(self, state): state.enter(ParseRevert()) - + class Unset(Keyword): rx = r"\\unset\b" + def update_state(self, state): state.enter(ParseUnset()) class Tweak(Keyword): rx = r"\\tweak\b" + def update_state(self, state): state.enter(ParseTweak()) @@ -626,6 +655,7 @@ class Change(Translator): class AccidentalStyle(Command): rx = r"\\accidentalStyle\b" + def update_state(self, state): state.enter(ParseAccidentalStyle()) @@ -636,15 +666,17 @@ def rx(): from .. import words return r"\b({0})(?!-?\w)".format("|".join(words.accidentalstyles)) - + class AlterBroken(Command): rx = r"\\alterBroken\b" + def update_state(self, state): state.enter(ParseAlterBroken()) class Clef(Command): rx = r"\\clef\b" + def update_state(self, state): state.enter(ParseClef()) @@ -654,13 +686,14 @@ class ClefSpecifier(Specifier): def rx(): from .. import words return r"\b({0})\b".format("|".join(words.clefs_plain)) - + def update_state(self, state): state.leave() class PitchCommand(Command): rx = r"\\(relative|transpose|transposition|key|octaveCheck)\b" + def update_state(self, state): argcount = 2 if self == '\\transpose' else 1 state.enter(ParsePitchCommand(argcount)) @@ -672,22 +705,24 @@ def rx(): from .. import words return r"\\({0})(?![A-Za-z])".format("|".join(words.modes)) - + class Hide(Keyword): rx = r"\\hide\b" + def update_state(self, state): state.enter(ParseHideOmit()) class Omit(Keyword): rx = r"\\omit\b" + def update_state(self, state): state.enter(ParseHideOmit()) class Unit(Command): rx = r"\\(mm|cm|in|pt)\b" - + class InputMode(Command): pass @@ -695,6 +730,7 @@ class InputMode(Command): class LyricMode(InputMode): rx = r"\\(lyricmode|((old)?add)?lyrics|lyricsto)\b" + def update_state(self, state): state.enter(ExpectLyricMode()) @@ -709,15 +745,15 @@ class LyricText(Lyric): class LyricHyphen(Lyric): rx = r"--(?=($|[\s\\]))" - - + + class LyricExtender(Lyric): rx = r"__(?=($|[\s\\]))" - - + + class LyricSkip(Lyric): rx = r"_(?=($|[\s\\]))" - + class Figure(_token.Token): """Base class for Figure items.""" @@ -725,6 +761,7 @@ class Figure(_token.Token): class FigureStart(Figure): rx = r"<" + def update_state(self, state): state.enter(ParseFigure()) @@ -754,31 +791,35 @@ class FigureModifier(Figure): class NoteMode(InputMode): rx = r"\\(notes|notemode)\b" + def update_state(self, state): state.enter(ExpectNoteMode()) class ChordMode(InputMode): rx = r"\\(chords|chordmode)\b" + def update_state(self, state): state.enter(ExpectChordMode()) class DrumMode(InputMode): rx = r"\\(drums|drummode)\b" + def update_state(self, state): state.enter(ExpectDrumMode()) class FigureMode(InputMode): rx = r"\\(figures|figuremode)\b" + def update_state(self, state): state.enter(ExpectFigureMode()) class UserCommand(IdentifierRef): pass - + class SimultaneousOrSequentialCommand(Keyword): rx = r"\\(simultaneous|sequential)\b" @@ -786,6 +827,7 @@ class SimultaneousOrSequentialCommand(Keyword): class SchemeStart(_token.Item): rx = "[#$](?![{}])" + def update_state(self, state): from . import scheme state.enter(scheme.ParseScheme(1)) @@ -796,15 +838,15 @@ class ContextName(_token.Token): def rx(): from .. import words return r"\b({0})\b".format("|".join(words.contexts)) - + class BackSlashedContextName(ContextName): @_token.patternproperty def rx(): from .. import words return r"\\({0})\b".format("|".join(words.contexts)) - - + + class GrobName(_token.Token): @_token.patternproperty def rx(): @@ -854,51 +896,53 @@ class Chord(_token.Token): class ChordStart(Chord): rx = r"<" + def update_state(self, state): state.enter(ParseChord()) class ChordEnd(Chord, _token.Leaver): rx = r">" - + class DrumChordStart(ChordStart): def update_state(self, state): state.enter(ParseDrumChord()) - + class DrumChordEnd(ChordEnd): pass - + class ErrorInChord(Error): rx = "|".join(( - re_articulation, # articulation - r"<<|>>", # double french quotes - r"\\[\\\]\[\(\)()]", # slurs beams - re_duration, # duration - re_scaling, # scaling + re_articulation, # articulation + r"<<|>>", # double french quotes + r"\\[\\\]\[\(\)()]", # slurs beams + re_duration, # duration + re_scaling, # scaling )) - + class Name(UserVariable): r"""A variable name without \ prefix.""" - + class EqualSign(_token.Token): rx = r"=" - + # Parsers class ParseLilyPond(Parser): mode = 'lilypond' + # basic stuff that can appear everywhere space_items = ( _token.Space, BlockCommentStart, LineComment, -) +) base_items = space_items + ( @@ -972,7 +1016,7 @@ class ParseLilyPond(Parser): StringNumber, IntegerValue, ) + command_items - + # items that occur inside chords music_chord_items = ( @@ -981,7 +1025,6 @@ class ParseLilyPond(Parser): ) + music_items - class ParseGlobal(ParseLilyPond): """Parses LilyPond from the toplevel of a file.""" items = ( @@ -997,6 +1040,7 @@ class ParseGlobal(ParseLilyPond): Fraction, DecimalValue, ) + def update_state(self, state, token): if isinstance(token, EqualSign): state.enter(ParseGlobalAssignment()) @@ -1020,34 +1064,36 @@ class ParseGlobalAssignment(FallthroughParser, ParseLilyPond): class ExpectOpenBracket(FallthroughParser, ParseLilyPond): """Waits for an OpenBracket and then replaces the parser with the class set in the replace attribute. - + Subclass this to set the destination for the OpenBracket. - + """ default = Error items = space_items + ( OpenBracket, ) + def update_state(self, state, token): if isinstance(token, OpenBracket): state.replace(self.replace()) - + class ExpectMusicList(FallthroughParser, ParseLilyPond): """Waits for an OpenBracket or << and then replaces the parser with the class set in the replace attribute. - + Subclass this to set the destination for the OpenBracket. - + """ items = space_items + ( OpenBracket, OpenSimultaneous, SimultaneousOrSequentialCommand, ) + def update_state(self, state, token): if isinstance(token, (OpenBracket, OpenSimultaneous)): state.replace(self.replace()) - + class ParseScore(ParseLilyPond): r"""Parses the expression after ``\score {``, leaving at ``}`` """ @@ -1059,7 +1105,7 @@ class ParseScore(ParseLilyPond): class ExpectScore(ExpectOpenBracket): replace = ParseScore - + class ParseBook(ParseLilyPond): r"""Parses the expression after ``\book {``, leaving at ``}`` """ @@ -1072,7 +1118,6 @@ class ParseBook(ParseLilyPond): ) + toplevel_base_items - class ExpectBook(ExpectOpenBracket): replace = ParseBook @@ -1123,7 +1168,7 @@ class ParseHeader(ParseLilyPond): class ExpectHeader(ExpectOpenBracket): replace = ParseHeader - + class ParseLayout(ParseLilyPond): r"""Parses the expression after ``\layout {``, leaving at ``}`` """ @@ -1143,7 +1188,7 @@ class ParseLayout(ParseLilyPond): class ExpectLayout(ExpectOpenBracket): replace = ParseLayout - + class ParseMidi(ParseLilyPond): r"""Parses the expression after ``\midi {``, leaving at ``}`` """ @@ -1179,7 +1224,7 @@ class ParseWith(ParseLilyPond): class ExpectWith(ExpectOpenBracket): replace = ParseWith - + class ParseContext(ParseLilyPond): r"""Parses the expression after (``\layout {``) ``\context {``, leaving at ``}`` """ @@ -1194,14 +1239,14 @@ class ParseContext(ParseLilyPond): class ExpectContext(ExpectOpenBracket): replace = ParseContext - + class ParseMusic(ParseLilyPond): """Parses LilyPond music expressions.""" items = music_items + ( TremoloColon, ) - + class ParseChord(ParseMusic): """LilyPond inside chords ``< >``""" @@ -1214,7 +1259,7 @@ class ParseString(Parser): StringQuotedEnd, StringQuoteEscape, ) - + class ParseBlockComment(Parser): default = BlockComment @@ -1224,7 +1269,7 @@ class ParseBlockComment(Parser): class ParseMarkup(Parser): - items = ( + items = ( MarkupScore, MarkupCommand, MarkupUserCommand, @@ -1250,6 +1295,7 @@ class ParseTempo(FallthroughParser): Length, EqualSign, ) + def update_state(self, state, token): if isinstance(token, EqualSign): state.replace(ParseTempoAfterEqualSign()) @@ -1266,14 +1312,16 @@ class ParseDuration(FallthroughParser): items = space_items + ( Dot, ) + def fallthrough(self, state): state.replace(ParseDurationScaling()) - - + + class ParseDurationScaling(ParseDuration): items = space_items + ( Scaling, ) + def fallthrough(self, state): state.leave() @@ -1287,6 +1335,7 @@ class ParseOverride(ParseLilyPond): GrobProperty, EqualSign, ) + base_items + def update_state(self, state, token): if isinstance(token, EqualSign): state.replace(ParseDecimalValue()) @@ -1306,15 +1355,17 @@ class ParseRevert(FallthroughParser): GrobName, GrobProperty, ) + def update_state(self, state, token): if isinstance(token, GrobProperty): state.replace(ParseGrobPropertyPath()) - + class ParseGrobPropertyPath(FallthroughParser): items = space_items + ( DotPath, ) + def update_state(self, state, token): if isinstance(token, DotPath): state.enter(ExpectGrobProperty()) @@ -1324,6 +1375,7 @@ class ExpectGrobProperty(FallthroughParser): items = space_items + ( GrobProperty, ) + def update_state(self, state, token): if isinstance(token, GrobProperty): state.leave() @@ -1338,11 +1390,12 @@ class ParseSet(ParseLilyPond): EqualSign, Name, ) + base_items + def update_state(self, state, token): if isinstance(token, EqualSign): state.replace(ParseDecimalValue()) - + class ParseUnset(FallthroughParser): items = space_items + ( ContextName, @@ -1350,6 +1403,7 @@ class ParseUnset(FallthroughParser): ContextProperty, Name, ) + def update_state(self, state, token): if isinstance(token, ContextProperty) or token[:1].islower(): state.leave() @@ -1361,6 +1415,7 @@ class ParseTweak(FallthroughParser): DotPath, GrobProperty, ) + def update_state(self, state, token): if isinstance(token, GrobProperty): state.replace(ParseTweakGrobProperty()) @@ -1371,6 +1426,7 @@ class ParseTweakGrobProperty(FallthroughParser): DotPath, DecimalValue, ) + def update_state(self, state, token): if isinstance(token, DotPath): state.enter(ExpectGrobProperty()) @@ -1383,7 +1439,7 @@ class ParseTranslator(FallthroughParser): ContextName, Name, ) - + def update_state(self, state, token): if isinstance(token, (Name, ContextName)): state.replace(ExpectTranslatorId()) @@ -1393,7 +1449,7 @@ class ExpectTranslatorId(FallthroughParser): items = space_items + ( EqualSign, ) - + def update_state(self, state, token): if token == '=': state.replace(ParseTranslatorId()) @@ -1405,7 +1461,7 @@ class ParseTranslatorId(FallthroughParser): Name, StringQuotedStart, ) - + def update_state(self, state, token): if isinstance(token, Name): state.leave() @@ -1425,6 +1481,7 @@ class ParseHideOmit(FallthroughParser): DotPath, GrobName, ) + def update_state(self, state, token): if isinstance(token, GrobName): state.leave() @@ -1436,6 +1493,7 @@ class ParseAccidentalStyle(FallthroughParser): DotPath, AccidentalStyleSpecifier, ) + def update_state(self, state, token): if isinstance(token, AccidentalStyleSpecifier): state.leave() @@ -1445,6 +1503,7 @@ class ParseAlterBroken(FallthroughParser): items = space_items + ( GrobProperty, ) + def update_state(self, state, token): if isinstance(token, GrobProperty): state.replace(ParseGrobPropertyPath()) @@ -1464,8 +1523,8 @@ class ParseInputMode(ParseLilyPond): def update_state(cls, state, token): if isinstance(token, (OpenSimultaneous, OpenBracket)): state.enter(cls()) - - + + class ParseLyricMode(ParseInputMode): r"""Parser for ``\lyrics``, ``\lyricmode``, ``\addlyrics``, etc.""" items = base_items + ( @@ -1502,9 +1561,10 @@ class ParseChordMode(ParseInputMode, ParseMusic): items = ( OpenBracket, OpenSimultaneous, - ) + music_items + ( # TODO: specify items exactly, e.g. < > is not allowed + ) + music_items + ( # TODO: specify items exactly, e.g. < > is not allowed ChordSeparator, ) + def update_state(self, state, token): if isinstance(token, ChordSeparator): state.enter(ParseChordItems()) @@ -1514,7 +1574,7 @@ def update_state(self, state, token): class ExpectChordMode(ExpectMusicList): replace = ParseChordMode - + class ParseNoteMode(ParseMusic): r"""Parser for ``\notes`` and ``\notemode``. Same as Music itself.""" @@ -1522,7 +1582,7 @@ class ParseNoteMode(ParseMusic): class ExpectNoteMode(ExpectMusicList): replace = ParseNoteMode - + class ParseDrumChord(ParseMusic): """LilyPond inside chords in drummode ``< >``""" @@ -1589,7 +1649,7 @@ class ParseDrumMode(ParseInputMode, ParseMusic): class ExpectDrumMode(ExpectMusicList): replace = ParseDrumMode - + class ParseFigureMode(ParseInputMode, ParseMusic): r"""Parser for ``\figures`` and ``\figuremode``.""" @@ -1619,7 +1679,7 @@ class ParseFigure(Parser): class ExpectFigureMode(ExpectMusicList): replace = ParseFigureMode - + class ParsePitchCommand(FallthroughParser): argcount = 1 @@ -1627,6 +1687,7 @@ class ParsePitchCommand(FallthroughParser): Note, Octave, ) + def update_state(self, state, token): if isinstance(token, Note): self.argcount -= 1 @@ -1654,5 +1715,3 @@ class ParseDecimalValue(FallthroughParser): Fraction, DecimalValue, ) - - diff --git a/ly/lex/mup.py b/ly/lex/mup.py index 3df377de..e8a74b30 100644 --- a/ly/lex/mup.py +++ b/ly/lex/mup.py @@ -47,12 +47,14 @@ class String(_token.String): class StringQuotedStart(String, _token.StringStart): rx = r'"' + def update_state(self, state): state.enter(ParseString()) - + class StringQuotedEnd(String, _token.StringEnd): rx = r'"' + def update_state(self, state): state.leave() state.endArgument() @@ -68,8 +70,8 @@ class Macro(_token.Token): class Preprocessor(_token.Token): rx = (r'\b(' - 'if|then|else|endif|define|undef|ifdef|ifndef' - r')\b|@') + 'if|then|else|endif|define|undef|ifdef|ifndef' + r')\b|@') class ParseMup(Parser): @@ -88,4 +90,3 @@ class ParseString(Parser): StringQuotedEnd, StringQuoteEscape, ) - diff --git a/ly/lex/scheme.py b/ly/lex/scheme.py index 2cbd1cc0..57d1561f 100644 --- a/ly/lex/scheme.py +++ b/ly/lex/scheme.py @@ -23,6 +23,7 @@ from __future__ import unicode_literals +from . import lilypond from . import _token from . import Parser, FallthroughParser @@ -38,16 +39,18 @@ class String(_token.String): class StringQuotedStart(String, _token.StringStart): rx = r'"' + def update_state(self, state): state.enter(ParseString()) - + class StringQuotedEnd(String, _token.StringEnd): rx = r'"' + def update_state(self, state): state.leave() state.endArgument() - + class StringQuoteEscape(_token.Character): rx = r'\\[\\"]' @@ -59,13 +62,14 @@ class Comment(_token.Comment): class LineComment(Comment, _token.LineComment): rx = r";.*$" - + class BlockCommentStart(Comment, _token.BlockCommentStart): rx = r"#!" + def update_state(self, state): state.enter(ParseBlockComment()) - + class BlockCommentEnd(Comment, _token.BlockCommentEnd, _token.Leaver): rx = "!#" @@ -78,6 +82,7 @@ class BlockComment(Comment, _token.BlockComment): class OpenParen(Scheme, _token.MatchStart, _token.Indent): rx = r"\(" matchname = "schemeparen" + def update_state(self, state): state.enter(ParseScheme()) @@ -85,23 +90,24 @@ def update_state(self, state): class CloseParen(Scheme, _token.MatchEnd, _token.Dedent): rx = r"\)" matchname = "schemeparen" + def update_state(self, state): state.leave() state.endArgument() - + class Quote(Scheme): rx = r"['`,]" - - + + class Dot(Scheme): rx = r"\.(?!\S)" class Bool(Scheme, _token.Item): rx = r"#[tf]\b" - - + + class Char(Scheme, _token.Item): rx = r"#\\([a-z]+|.)" @@ -165,9 +171,10 @@ class LilyPond(_token.Token): class LilyPondStart(LilyPond, _token.MatchStart, _token.Indent): rx = r"#{" matchname = "schemelily" + def update_state(self, state): state.enter(ParseLilyPond()) - + class LilyPondEnd(LilyPond, _token.Leaver, _token.MatchEnd, _token.Dedent): rx = r"#}" @@ -200,7 +207,7 @@ class ParseScheme(Parser): Word, StringQuotedStart, ) - + class ParseString(Parser): default = String @@ -208,7 +215,7 @@ class ParseString(Parser): StringQuotedEnd, StringQuoteEscape, ) - + class ParseBlockComment(Parser): default = BlockComment @@ -217,8 +224,5 @@ class ParseBlockComment(Parser): ) -from . import lilypond - class ParseLilyPond(lilypond.ParseMusic): items = (LilyPondEnd,) + lilypond.ParseMusic.items - diff --git a/ly/lex/texinfo.py b/ly/lex/texinfo.py index 5f5faa1e..9d35ee1c 100644 --- a/ly/lex/texinfo.py +++ b/ly/lex/texinfo.py @@ -23,6 +23,7 @@ from __future__ import unicode_literals +from . import lilypond from . import _token from . import Parser, FallthroughParser @@ -37,10 +38,11 @@ class LineComment(Comment, _token.LineComment): class BlockCommentStart(Comment, _token.BlockCommentStart): rx = r"@ignore\b" + def update_state(self, state): state.enter(ParseComment()) - - + + class BlockCommentEnd(Comment, _token.Leaver, _token.BlockCommentEnd): rx = r"@end\s+ignore\b" @@ -59,6 +61,7 @@ class Block(_token.Token): class BlockStart(Block): rx = r"@[a-zA-Z]+\{" + def update_state(self, state): state.enter(ParseBlock()) @@ -69,7 +72,7 @@ class BlockEnd(Block, _token.Leaver): class EscapeChar(_token.Character): rx = r"@[@{}]" - + class Accent(EscapeChar): rx = "@['\"',=^`~](\\{[a-zA-Z]\\}|[a-zA-Z]\\b)" @@ -81,58 +84,65 @@ class Verbatim(_token.Token): class VerbatimStart(Keyword): rx = r"@verbatim\b" + def update_state(self, state): state.enter(ParseVerbatim()) class VerbatimEnd(Keyword, _token.Leaver): rx = r"@end\s+verbatim\b" - - + + class LilyPondBlockStart(Block): rx = r"@lilypond(?=(\[[a-zA-Z,=0-9\\\s]+\])?\{)" + def update_state(self, state): state.enter(ParseLilyPondBlockAttr()) class LilyPondBlockStartBrace(Block): rx = r"\{" + def update_state(self, state): state.replace(ParseLilyPondBlock()) class LilyPondBlockEnd(Block, _token.Leaver): rx = r"\}" - - + + class LilyPondEnvStart(Keyword): rx = r"@lilypond\b" + def update_state(self, state): state.enter(ParseLilyPondEnvAttr()) - - + + class LilyPondEnvEnd(Keyword, _token.Leaver): rx = r"@end\s+lilypond\b" class LilyPondFileStart(Block): rx = r"@lilypondfile\b" + def update_state(self, state): state.enter(ParseLilyPondFile()) class LilyPondFileStartBrace(Block): rx = r"\{" + def update_state(self, state): state.replace(ParseBlock()) class LilyPondAttrStart(Attribute): rx = r"\[" + def update_state(self, state): state.enter(ParseLilyPondAttr()) - - + + class LilyPondAttrEnd(Attribute, _token.Leaver): rx = r"\]" @@ -190,6 +200,7 @@ class ParseLilyPondEnvAttr(FallthroughParser): items = ( LilyPondAttrStart, ) + def fallthrough(self, state): state.replace(ParseLilyPondEnv()) @@ -208,8 +219,6 @@ class ParseLilyPondFile(Parser): ) -from . import lilypond - class ParseLilyPondBlock(lilypond.ParseGlobal): items = ( LilyPondBlockEnd, @@ -220,5 +229,3 @@ class ParseLilyPondEnv(lilypond.ParseGlobal): items = ( LilyPondEnvEnd, ) + lilypond.ParseGlobal.items - - diff --git a/ly/music/__init__.py b/ly/music/__init__.py index 69bccc68..d2eea13d 100644 --- a/ly/music/__init__.py +++ b/ly/music/__init__.py @@ -107,5 +107,3 @@ def document(doc): """Return a music.items.Document instance for the ly.document.Document.""" from . import items return items.Document(doc) - - diff --git a/ly/music/event.py b/ly/music/event.py index 0083e509..b7b09eb7 100644 --- a/ly/music/event.py +++ b/ly/music/event.py @@ -28,13 +28,11 @@ class Events(object): """Traverses a music tree and records music events from it.""" unfold_repeats = False - + def read(self, node, time=0, scaling=1): """Read events from the node and all its child nodes; return time.""" return self.traverse(node, time, scaling) - + def traverse(self, node, time, scaling): """Traverse node and call event handlers; record and return the time.""" return node.events(self, time, scaling) - - diff --git a/ly/music/items.py b/ly/music/items.py index 27a544cc..af154283 100644 --- a/ly/music/items.py +++ b/ly/music/items.py @@ -22,12 +22,12 @@ Whitespace and comments are left out. -All nodes (instances of Item) have a 'position' attribute that indicates -where the item starts in the source text. Almost all items have the token -that starts the expression in the 'token' attribute and possibly other -tokens in the 'tokens' attribute, as a tuple. +All nodes (instances of Item) have a 'position' attribute that indicates +where the item starts in the source text. Almost all items have the token +that starts the expression in the 'token' attribute and possibly other +tokens in the 'tokens' attribute, as a tuple. -The 'end_position()' method returns the position where the node (including +The 'end_position()' method returns the position where the node (including its child nodes) ends. You can get the whole tree structure of a LilyPond document by instantiating @@ -53,15 +53,15 @@ class Item(ly.node.WeakNode): """Represents any item in the music of a document. - + This can be just a token, or an interpreted construct such as a note, rest or sequential or simultaneous construct , etc. - + Some Item instances just have one responsible token, but others have a list or tuple to tokens. - + An Item also has a pointer to the Document it originates from. - + """ document = None tokens = () @@ -71,16 +71,16 @@ class Item(ly.node.WeakNode): def __repr__(self): s = ' ' + repr(self.token[:]) if self.token else '' return '<{0}{1}>'.format(self.__class__.__name__, s) - + def plaintext(self): """Return a plaintext value for this node. - + This only makes sense for items like Markup or String. For other types, an empty string is returned - + """ return "" - + def end_position(self): """Return the end position of this node.""" def ends(): @@ -100,21 +100,21 @@ def ends(): elif isinstance(i, lex.Token): yield i.end return max(ends()) - + def events(self, e, time, scaling): """Let the event.Events instance handle the events. Return the time.""" return time - + def length(self): """Return the musical duration.""" return 0 - + def iter_toplevel_items(self): """Yield the toplevel items of our Document node in backward direction. - - Iteration starts with the node just before the node "self" is a + + Iteration starts with the node just before the node "self" is a descendant of. - + """ node = self for doc in self.ancestors(): @@ -123,12 +123,12 @@ def iter_toplevel_items(self): node = doc else: return - + # now, doc is the Document node, and node is the child of the Document # node we are a (far) descendant of for i in node.backward(): yield i - + # look in parent Document before the place we were included while doc.include_node: p = doc.include_node.parent() @@ -138,7 +138,7 @@ def iter_toplevel_items(self): doc = p else: break - + def iter_toplevel_items_include(self): r"""Same as iter_toplevel_items(), but follows \include commands.""" def follow(it): @@ -151,13 +151,13 @@ def follow(it): else: yield i return follow(self.iter_toplevel_items()) - + def music_parent(self): """Walk up the parent tree until Music is found; return the outermost Music node. - + Returns None is the node does not belong to any music expression (e.g. a toplevel Markup or Scheme object). - + """ node = self mus = isinstance(node, Music) @@ -167,12 +167,12 @@ def music_parent(self): return node mus = pmus node = p - + def music_children(self, depth=-1): """Yield all the children that are new music expressions - + (i.e. that are inside other constructions). - + """ def find(node, depth): if depth != 0: @@ -188,16 +188,16 @@ def find(node, depth): for i in find(i, depth-1): yield i return find(self, depth) - + def has_output(self, _seen_docs=None): """Return True if this node has toplevel music, markup, book etc. - + I.e. returns True when LilyPond would likely generate output. Usually you'll call this method on a Document, Score, BookPart or Book node. - - You should not supply the _seen_docs argument; it is used internally + + You should not supply the _seen_docs argument; it is used internally to avoid traversing recursively nested include files. - + """ if _seen_docs is None: _seen_docs = set() @@ -216,7 +216,7 @@ def has_output(self, _seen_docs=None): class Document(Item): """A toplevel item representing a ly.document.Document.""" - + def __init__(self, doc): super(Document, self).__init__() self.document = doc @@ -229,7 +229,7 @@ def __init__(self, doc): from .read import Reader r = Reader(s) self.extend(r.read()) - + def node(self, position, depth=-1): """Return the node at or just before the specified position.""" def bisect(n, depth): @@ -250,60 +250,60 @@ def bisect(n, depth): return n return bisect(n[pos], depth - 1) return bisect(self, depth) - + def music_events_til_position(self, position): """Return a list of tuples. - - Every tuple is a (parent, nodes, scaling). If an empty list is + + Every tuple is a (parent, nodes, scaling). If an empty list is returned, there is no music expression at this position. - + """ node = self.node(position) # be nice and allow including an assignment if (isinstance(node, Assignment) and node.parent() is self - and isinstance(node.value(), Music)): + and isinstance(node.value(), Music)): return [(node, [], 1)] - + if isinstance(node.parent(), Chord): node = node.parent() - - l = [] + + lis = [] mus = isinstance(node, (Music, Durable)) if mus: - l.append((node, [], 1)) + lis.append((node, [], 1)) for p in node.ancestors(): pmus = isinstance(p, Music) end = node.end_position() if pmus: if position > end: preceding, s = p.preceding(node.next_sibling()) - l = [(p, preceding, s)] + lis = [(p, preceding, s)] elif position == end: preceding, s = p.preceding(node) - l = [(p, preceding + [node], s)] + lis = [(p, preceding + [node], s)] else: preceding, s = p.preceding(node) - l.append((p, preceding, s)) + lis.append((p, preceding, s)) elif mus: # we are at the musical top if position > end: return [] elif position == end: - l = [(p, [node], 1)] + lis = [(p, [node], 1)] else: - l.append((p, [], 1)) + lis.append((p, [], 1)) break node = p mus = pmus - l.reverse() - return l - + lis.reverse() + return lis + def time_position(self, position): """Return the time position in the music at the specified cursor position. - - The value is a fraction. If None is returned, we are not in a music + + The value is a fraction. If None is returned, we are not in a music expression. - + """ events = self.music_events_til_position(position) if events: @@ -316,26 +316,26 @@ def time_position(self, position): for n in nodes: time = e.traverse(n, time, scaling) return time - + def time_length(self, start, end): """Return the length of the music between start and end positions. - + Returns None if start and end are not in the same expression. - + """ def mk_list(evts): """Make a flat list of all the events.""" - l = [] + lis = [] scaling = 1 for p, nodes, s in evts: scaling *= s for n in nodes: - l.append((n, scaling)) - return l - + lis.append((n, scaling)) + return lis + if start > end: start, end = end, start - + start_evts = self.music_events_til_position(start) if start_evts: end_evts = self.music_events_til_position(end) @@ -361,15 +361,15 @@ def mk_list(evts): for evt, s in end_evts[i::]: end_time = e.traverse(evt, end_time, s) return end_time - time - + def substitute_for_node(self, node): """Returns a node that replaces the specified node (e.g. in music). - + For example: a variable reference returns its value. Returns nothing if the node is not substitutable. Returns the node itself if it was substitutable, but the substitution failed. - + """ if isinstance(node, UserCommand): value = node.value() @@ -378,9 +378,9 @@ def substitute_for_node(self, node): return node elif isinstance(node, Include): return self.get_included_document_node(node) or node - + # maybe other substitutions - + def iter_music(self, node=None): """Iter over the music, following references to other assignments.""" for n in node or self: @@ -388,7 +388,7 @@ def iter_music(self, node=None): yield n for n in self.iter_music(n): yield n - + def get_included_document_node(self, node): """Return a Document for the Include node. May return None.""" try: @@ -404,7 +404,7 @@ def get_included_document_node(self, node): docnode.include_path = self.include_path node._document = docnode return node._document - + def resolve_filename(self, filename): """Resolve filename against our document and include_path.""" import os @@ -422,21 +422,21 @@ def resolve_filename(self, filename): fullpath = os.path.join(p, filename) if os.path.exists(fullpath): return fullpath - + def get_music(self, filename): """Return the music Document for the specified filename. - - This implementation loads a ly.document.Document using utf-8 - encoding. Inherit from this class to implement other loading + + This implementation loads a ly.document.Document using utf-8 + encoding. Inherit from this class to implement other loading mechanisms or caching. - + """ import ly.document return type(self)(ly.document.Document.load(filename)) class Token(Item): - """Any token that is not otherwise recognized""" + """Any token that is not otherwise recognized""" class Container(Item): @@ -449,13 +449,13 @@ class Duration(Item): class Durable(Item): """An Item that has a musical duration, in the duration attribute.""" - duration = 0, 1 # two Fractions: (base, scaling) - + duration = 0, 1 # two Fractions: (base, scaling) + def length(self): """Return the musical duration (our base * our scaling).""" base, scaling = self.duration return base * scaling - + def events(self, e, time, scaling): """Let the event.Events instance handle the events. Return the time.""" return time + self.duration[0] * self.duration[1] * scaling @@ -496,26 +496,27 @@ class DrumNote(Durable): class Music(Container): """Any music expression, to be inherited of.""" + def events(self, e, time, scaling): """Let the event.Events instance handle the events. Return the time.""" for node in self: time = e.traverse(node, time, scaling) return time - + def length(self): """Return the musical duration.""" from . import event return event.Events().read(self) - + def preceding(self, node=None): """Return a two-tuple (nodes, scaling). - + The nodes are the nodes in time before the node (which must be a child), and the scaling is the scaling this node applies (normally 1). - + If node is None, all nodes that would precede a fictive node at the end are returned. - + """ i = self.index(node) if node else None return self[:i:], 1 @@ -524,7 +525,7 @@ def preceding(self, node=None): class MusicList(Music): """A music expression, either << >> or { }.""" simultaneous = False - + def events(self, e, time, scaling): """Let the event.Events instance handle the events. Return the time.""" if self.simultaneous: @@ -536,13 +537,13 @@ def events(self, e, time, scaling): def preceding(self, node=None): """Return a two-tuple (nodes, scaling). - + The nodes are the nodes in time before the node (which must be a child), and the scaling is the scaling this node applies (normally 1). - + If node is None, all nodes that would precede a fictive node at the end are returned. - + """ if self.simultaneous: return [], 1 @@ -551,55 +552,55 @@ def preceding(self, node=None): class Tag(Music): r"""A \tag, \keepWithTag or \removeWithTag command.""" - + def events(self, e, time, scaling): """Let the event.Events instance handle the events. Return the time.""" for node in self[-1:]: time = e.traverse(node, time, scaling) return time - + def preceding(self, node=None): """Return a two-tuple (nodes, scaling). - + The nodes are the nodes in time before the node (which must be a child), and the scaling is the scaling this node applies (normally 1). - + If node is None, all nodes that would precede a fictive node at the end are returned. - + """ return [], 1 class Scaler(Music): r"""A music construct that scales the duration of its contents. - + In the numerator and denominator attributes the values specified for LilyPond are stored, e.g. with \times 3/2 { c d e }, the numerator is integer 3 and the denominator is integer 2. Note that for \tuplet and \times the meaning of these numbers is reversed. - + The algebraic scaling is stored in the scaling attribute. - + """ scaling = 1 - + numerator = 0 denominator = 0 - + def events(self, e, time, scaling): """Let the event.Events instance handle the events. Return the time.""" return super(Scaler, self).events(e, time, scaling * self.scaling) - + def preceding(self, node=None): """Return a two-tuple (nodes, scaling). - + The nodes are the nodes in time before the node (which must be a child), and the scaling is the scaling this node applies. - + If node is None, all nodes that would precede a fictive node at the end are returned. - + """ i = self.index(node) if node else None return self[:i:], self.scaling @@ -607,20 +608,20 @@ def preceding(self, node=None): class Grace(Music): """Music that has grace timing, i.e. 0 as far as computation is concerned.""" - + def events(self, e, time, scaling): """Let the event.Events instance handle the events. Return the time.""" return super(Grace, self).events(e, time, 0) - + def preceding(self, node=None): """Return a two-tuple (nodes, scaling). - + The nodes are the nodes in time before the node (which must be a child), and the scaling is 0 for (because we have grace notes). - + If node is None, all nodes that would precede a fictive node at the end are returned. - + """ i = self.index(node) if node else None return self[:i:], 0 @@ -628,29 +629,30 @@ def preceding(self, node=None): class AfterGrace(Music): r"""The \afterGrace function with its two arguments. - + Only the duration of the first is counted. - + """ class PartCombine(Music): r"""The \partcombine command with 2 music arguments.""" + def events(self, e, time, scaling): """Let the event.Events instance handle the events. Return the time.""" if len(self): time = max(e.traverse(node, time, scaling) for node in self) return time - + def preceding(self, node=None): """Return a two-tuple (nodes, scaling). - + The nodes are the nodes in time before the node (which must be a child), and the scaling is the scaling this node applies (normally 1). - + If node is None, all nodes that would precede a fictive node at the end are returned. - + """ return [], 1 @@ -671,13 +673,14 @@ class Transpose(Music): class Repeat(Music): r"""A \repeat expression.""" + def specifier(self): if isinstance(self._specifier, Scheme): return self._specifier.get_string() elif isinstance(self._specifier, String): return self._specifier.value() return self._specifier - + def repeat_count(self): if isinstance(self._repeat_count, Scheme): return self._repeat_count.get_int() or 1 @@ -691,7 +694,7 @@ def events(self, e, time, scaling): else: alt = None children = self[:] - + if e.unfold_repeats or self.specifier() != "volta": count = self.repeat_count() if alt and len(alt) and len(alt[0]): @@ -744,7 +747,7 @@ class LyricMode(InputMode): class LyricsTo(InputMode): r"""A \lyricsto expression.""" _context_id = None - + def context_id(self): if isinstance(self._context_id, String): return self._context_id.value() @@ -763,9 +766,9 @@ class LyricItem(Item): class ChordSpecifier(Item): """Chord specifications after a note in chord mode. - + Has children of Note or ChordItem class. - + """ @@ -782,10 +785,10 @@ class Translator(Item): r"""Base class for a \change, \new, or \context music expression.""" _context = None _context_id = None - + def context(self): return self._context - + def context_id(self): """The context id, if specified after an equal sign.""" if isinstance(self._context_id, String): @@ -803,19 +806,19 @@ class Change(Translator): class Tempo(Item): duration = 0, 1 - + def fraction(self): """Return the note value as a fraction given before the equal sign.""" base, scaling = self.duration # (scaling will normally be 1) return base * scaling - + def text(self): """Return the text, if set. Can be Markup, Scheme, or String.""" for i in self: if isinstance(i, (Markup, Scheme, String)): return i return - + def tempo(self): """Return a list of integer values describing the tempo or range.""" nodes = iter(self) @@ -841,15 +844,15 @@ class TimeSignature(Item): def measure_length(self): """The length of one measure in this time signature as a Fraction.""" return self._num * self._fraction - + def numerator(self): """The upper number (e.g. for 3/2 it returns 3).""" return self._num - + def fraction(self): """The lower number as a Fraction (e.g. for 3/2 it returns 1/2).""" return self._fraction - + def beatstructure(self): """The scheme expressions denoting the beat structure, if specified.""" return self._beatstructure @@ -868,20 +871,21 @@ def partial_length(self): class Clef(Item): r"""A \clef item.""" _specifier = None - + def specifier(self): if isinstance(self._specifier, String): return self._specifier.value() return self._specifier - + class KeySignature(Item): r"""A \key pitch \mode command.""" + def pitch(self): """The ly.pitch.Pitch that denotes the pitch.""" for i in self.find(Note): return i.pitch - + def mode(self): """The mode, e.g. "major", "minor", etc.""" for i in self.find(Command): @@ -941,16 +945,17 @@ class Command(Item): class UserCommand(Music): """A user command, most probably referring to music.""" + def name(self): """Return the name of this user command (without the leading backslash).""" return self.token[1:] - + def value(self): """Find the value assigned to this variable.""" for i in self.iter_toplevel_items_include(): if isinstance(i, Assignment) and i.name() == self.name(): return i.value() - + def events(self, e, time, scaling): """Let the event.Events instance handle the events. Return the time.""" value = self.value() @@ -961,6 +966,7 @@ def events(self, e, time, scaling): class Version(Item): r"""A \version command.""" + def version_string(self): """The version as a string.""" for i in self: @@ -977,6 +983,7 @@ def version(self): class Include(Item): r"""An \include command (not changing the language).""" + def filename(self): """Returns the filename.""" for i in self: @@ -993,6 +1000,7 @@ class Language(Item): class Markup(Item): r"""A command starting markup (\markup, -lines and -list).""" + def plaintext(self): """Return the plain text value of this node.""" return ' '.join(n.plaintext() for n in self) @@ -1000,12 +1008,13 @@ def plaintext(self): class MarkupCommand(Item): r"""A markup command, such as \italic etc.""" + def plaintext(self): """Return the plain text value of this node.""" if self.token == '\\concat': joiner = '' - #elif 'column' in self.token: - #joiner = '\n' + # elif 'column' in self.token: + # joiner = '\n' else: joiner = ' ' if len(self) == 1 and isinstance(self[0], MarkupList): @@ -1017,10 +1026,11 @@ def plaintext(self): class MarkupUserCommand(Item): """A user-defined markup command""" + def name(self): """Return the name of this user command (without the leading backslash).""" return self.token[1:] - + def value(self): """Find the value assigned to this variable.""" for i in self.iter_toplevel_items_include(): @@ -1048,6 +1058,7 @@ class MarkupScore(Item): class MarkupList(Item): r"""The group of markup items inside { and }. NOTE: *not* a \markuplist.""" + def plaintext(self): """Return the plain text value of this node.""" return ' '.join(n.plaintext() for n in self) @@ -1055,16 +1066,18 @@ def plaintext(self): class MarkupWord(Item): """A MarkupWord token.""" + def plaintext(self): return self.token class Assignment(Item): """A variable = value construct.""" + def name(self): """The variable name.""" return self.token - + def value(self): """The assigned value.""" if len(self): @@ -1109,12 +1122,13 @@ class With(Container): class Set(Item): r"""A \set command.""" + def context(self): """The context, if specified.""" for t in self.tokens: if isinstance(t, lilypond.ContextName): return t - + def property(self): """The property.""" for t in self.tokens: @@ -1123,7 +1137,7 @@ def property(self): for t in self.tokens[::-1]: if isinstance(t, lilypond.Name): return t - + def value(self): """The value, given as argument. This is simply the child element.""" for i in self: @@ -1132,12 +1146,13 @@ def value(self): class Unset(Item): """An \\unset command.""" + def context(self): """The context, if specified.""" for t in self.tokens: if isinstance(t, lilypond.ContextName): return t - + def property(self): """The property.""" for t in self.tokens: @@ -1150,11 +1165,12 @@ def property(self): class Override(Item): r"""An \override command.""" + def context(self): for i in self: if isinstance(i.token, lilypond.ContextName): return i.token - + def grob(self): for i in self: if isinstance(i.token, lilypond.GrobName): @@ -1163,11 +1179,12 @@ def grob(self): class Revert(Item): r"""A \revert command.""" + def context(self): for i in self: if isinstance(i.token, lilypond.ContextName): return i.token - + def grob(self): for i in self: if isinstance(i.token, lilypond.GrobName): @@ -1184,12 +1201,12 @@ class PathItem(Item): class String(Item): """A double-quoted string.""" - + def plaintext(self): """Return the plaintext value of this string, without escapes and quotes.""" # TEMP use the value(), must become token independent. return self.value() - + def value(self): return ''.join( t[1:] if isinstance(t, lex.Character) and t.startswith('\\') else t @@ -1198,6 +1215,7 @@ def value(self): class Number(Item): """A numerical value, directly entered.""" + def value(self): if isinstance(self.token, lilypond.IntegerValue): return int(self.token) @@ -1211,27 +1229,28 @@ def value(self): class Scheme(Item): """A Scheme expression inside LilyPond.""" + def plaintext(self): """A crude way to get the plain text in this node.""" # TEMP use get_string() return self.get_string() - + def get_pair_ints(self): """Very basic way to get two integers specified as a pair.""" result = [int(i.token) for i in self.find(SchemeItem) if i.token.isdigit()] if len(result) >= 2: return tuple(result[:2]) - + def get_list_ints(self): """A basic way to get a list of integer values.""" return [int(i.token) for i in self.find(SchemeItem) if i.token.isdigit()] - + def get_int(self): """A basic way to get one integer value.""" for i in self.find(SchemeItem): if i.token.isdigit(): return int(i.token) - + def get_fraction(self): """A basic way to get one (may be fractional) numerical value.""" for i in self.find(SchemeItem): @@ -1239,7 +1258,7 @@ def get_fraction(self): return int(i.token) elif isinstance(i.token, scheme.Fraction): return Fraction(i.token) - + def get_string(self): """A basic way to get a quoted string value (without the quotes).""" return ''.join(i.value() for i in self.find(String)) @@ -1251,6 +1270,14 @@ def get_ly_make_moment(self): if tokens[1].isdigit() and tokens[2].isdigit(): return Fraction(int(tokens[1]), int(tokens[2])) + def get_bool(self): + """A basic way to get a boolean.""" + for i in self.find(SchemeItem): + if i.token == "#f": + return False + elif i.token == "#t": + return True + class SchemeItem(Item): """Any scheme token.""" @@ -1266,6 +1293,3 @@ class SchemeQuote(Item): class SchemeLily(Container): """A music expression inside #{ and #}.""" - - - diff --git a/ly/music/read.py b/ly/music/read.py index 50b3526d..1044ab4a 100644 --- a/ly/music/read.py +++ b/ly/music/read.py @@ -27,12 +27,12 @@ Whitespace and comments are left out. -All nodes (instances of Item) have a 'position' attribute that indicates -where the item starts in the source text. Almost all items have the token -that starts the expression in the 'token' attribute and possibly other -tokens in the 'tokens' attribute, as a tuple. +All nodes (instances of Item) have a 'position' attribute that indicates +where the item starts in the source text. Almost all items have the token +that starts the expression in the 'token' attribute and possibly other +tokens in the 'tokens' attribute, as a tuple. -The 'end_position()' method returns the position where the node (including +The 'end_position()' method returns the position where the node (including its child nodes) ends. @@ -56,9 +56,9 @@ def skip(source, what=(lex.Space, lex.Comment)): """Yield tokens from source, skipping items of classes specified in what. - + By default, comments and whitespace are skipped. - + """ for t in source: if not isinstance(t, what): @@ -67,14 +67,16 @@ def skip(source, what=(lex.Space, lex.Comment)): class dispatcher(object): """Decorator creator to dispatch commands, keywords, etc. to a method.""" + def __init__(self): self.d = {} - + def read_arg(self, a): return a - + def __call__(self, *args): d = self.d + def wrapper(func): for a in args: d[a] = func @@ -82,7 +84,7 @@ def wrapper(func): func.__doc__ = doc if not func.__doc__ else func.__doc__ + '\n\n' + doc return func return wrapper - + def method(self, token): """The registered method to call for the token. May return None.""" return self.d.get(token) @@ -90,9 +92,10 @@ def method(self, token): class dispatcher_class(dispatcher): """Decorator creator to dispatch the class of a token to a method.""" + def read_arg(self, a): return a.__name__ - + def method(self, token): """The registered method to call for the token's class. May return None.""" cls = token.__class__ @@ -112,32 +115,34 @@ def method(self, token): class Reader(object): """Reads tokens from a Source and builds a meaningful tree structure.""" - + _commands = dispatcher() _keywords = dispatcher() _tokencls = dispatcher_class() _markup = dispatcher_class() _scheme = dispatcher_class() - + def __init__(self, source): """Initialize with a ly.document.Source. - + The language is set to "nederlands". - + """ self.source = source self.language = "nederlands" self.in_chord = False self.prev_duration = Fraction(1, 4), 1 - + self.lyric_mode = False + self.volta = False + def set_language(self, lang): r"""Changes the pitch name language to use. - + Called internally when \language or \include tokens are encountered with a valid language name/file. - + Sets the language attribute to the language name. - + """ if lang in ly.pitch.pitchInfo: self.language = lang @@ -145,9 +150,9 @@ def set_language(self, lang): def add_duration(self, item, token=None, source=None): """Add a duration attribute to the item. - + When there are tokens, a Duration item is also appended to the item. - + """ source = source or self.source tokens = [] @@ -164,18 +169,19 @@ def add_duration(self, item, token=None, source=None): self.source.pushback() break if tokens: - d = self.factory(Duration, tokens[0]) - d.tokens = tuple(tokens[1:]) - item.append(d) + if not isinstance(item, ly.music.items.Partial): # prevents \partial commands from changing the duration of notes + d = self.factory(Duration, tokens[0]) + d.tokens = tuple(tokens[1:]) + item.append(d) item.duration = self.prev_duration = ly.duration.base_scaling(tokens) else: item.duration = self.prev_duration - + def consume(self, last_token=None): """Yield the tokens from our source until a parser is exit. - + If last_token is given, it is called with the last token that is read. - + """ t = None for t in self.source.until_parser_end(): @@ -185,10 +191,10 @@ def consume(self, last_token=None): def factory(self, cls, token=None, consume=False, position=None): """Create Item instance for token. - + If consume is True, consume()s the source into item.tokens. If you don't specify a token, you must specify the position (>= 0). - + """ item = cls() item.document = self.source.document @@ -204,12 +210,12 @@ def factory(self, cls, token=None, consume=False, position=None): if not token and item.tokens: item.position = item.tokens[0].pos return item - + def add_bracketed(self, item, source): """Append the arguments between brackets to the item. - + Returns True if that succeeded, else False. - + """ for t in source: if isinstance(t, lilypond.OpenBracket): @@ -221,32 +227,36 @@ def add_bracketed(self, item, source): self.source.pushback() break return False - + def read(self, source=None): """Yield Item instances reading from source.""" source = source or self.source for t in skip(source): - item = self.read_item(t, source) + # Handle case where lyrics are within a volta repeat + if self.lyric_mode and self.volta: + item = self.read_lyric_item(t) + else: + item = self.read_item(t, source) if item: yield item - + def read_item(self, t, source=None): """Return one Item that starts with token t. May return None.""" meth = self._tokencls.method(t) if meth: return meth(self, t, source or self.source) - + @_tokencls(lilypond.SchemeStart) @_markup(lilypond.SchemeStart) def handle_scheme_start(self, t, source=None): return self.read_scheme_item(t) - + @_tokencls(lex.StringStart) @_markup(lex.StringStart) @_scheme(lex.StringStart) def handle_string_start(self, t, source=None): return self.factory(String, t, True) - + @_tokencls( lilypond.DecimalValue, lilypond.IntegerValue, @@ -254,17 +264,17 @@ def handle_string_start(self, t, source=None): ) def handle_number_class(self, t, source=None): return self.factory(Number, t) - + @_tokencls(lilypond.MusicItem) def handle_music_item(self, t, source): return self.read_music_item(t, source) - + @_tokencls(lilypond.Length) def handle_length(self, t, source): item = self.factory(Unpitched, position=t.pos) self.add_duration(item, t, source) return item - + @_tokencls(lilypond.ChordStart) def handle_chord_start(self, t, source): if not self.in_chord: @@ -275,7 +285,7 @@ def last(t): chord.tokens += (t,) self.in_chord = False self.add_duration(chord, None, source) return chord - + @_tokencls( lilypond.OpenBracket, lilypond.OpenSimultaneous, lilypond.SimultaneousOrSequentialCommand, @@ -286,7 +296,7 @@ def handle_music_list(self, t, source): if it: item.extend(self.read(it)) return item - + @_tokencls(lilypond.Command) def read_command(self, t, source): """Read the rest of a command given in t from the source.""" @@ -294,7 +304,7 @@ def read_command(self, t, source): if meth: return meth(self, t, source) return self.factory(Command, t) - + @_tokencls(lilypond.Keyword) def read_keyword(self, t, source): """Read the rest of a keyword given in t from the source.""" @@ -302,18 +312,20 @@ def read_keyword(self, t, source): if meth: return meth(self, t, source) return self.factory(Keyword, t) - + @_tokencls(lilypond.UserCommand) def read_user_command(self, t, source): """Read a user command, this can be a variable reference.""" return self.factory(UserCommand, t) - + @_tokencls(lilypond.ChordSeparator) def read_chord_specifier(self, t, source=None): """Read stuff behind notes in chordmode.""" item = self.factory(ChordSpecifier, position=t.pos) item.append(self.factory(ChordItem, t)) for t in self.consume(): + if t == '\n': # line breaks should separate chords + break if isinstance(t, lilypond.ChordItem): item.append(self.factory(ChordItem, t)) elif isinstance(t, lilypond.Note): @@ -323,7 +335,7 @@ def read_chord_specifier(self, t, source=None): note.pitch = ly.pitch.Pitch(*r) item.append(note) return item - + @_tokencls(lilypond.TremoloColon) def read_tremolo(self, t, source=None): """Read a tremolo.""" @@ -336,11 +348,11 @@ def read_tremolo(self, t, source=None): self.source.pushback() break return item - + @_tokencls(lilypond.Name, lilypond.ContextProperty) def handle_name(self, t, source): return self.read_assignment(t) - + @_tokencls( lilypond.PaperVariable, lilypond.LayoutVariable, @@ -358,7 +370,7 @@ def handle_variable_assignment(self, t, source): self.source.pushback() break return item - + _direct_items = { lilypond.VoiceSeparator: VoiceSeparator, lilypond.PipeSymbol: PipeSymbol, @@ -369,20 +381,20 @@ def handle_variable_assignment(self, t, source): def handle_direct_items(self, t, source): """Tokens that directly translate to an Item.""" return self.factory(self._direct_items[t.__class__], t) - + @_tokencls(lilypond.Direction) def handle_direction(self, t, source): item = self.factory(Postfix, t) item.direction = '_-^'.index(t) - 1 for t in skip(source): if isinstance(t, ( - lex.StringStart, - lilypond.MarkupStart, - lilypond.Articulation, - lilypond.Slur, - lilypond.Beam, - lilypond.Dynamic, - )): + lex.StringStart, + lilypond.MarkupStart, + lilypond.Articulation, + lilypond.Slur, + lilypond.Beam, + lilypond.Dynamic, + )): item.append(self.read_item(t)) elif isinstance(t, lilypond.Command) and t in ('\\tag'): item.append(self.read_item(t)) @@ -392,24 +404,24 @@ def handle_direction(self, t, source): self.source.pushback() break return item - + @_tokencls(lilypond.Slur) def handle_slurs(self, t, source=None): cls = PhrasingSlur if t.startswith('\\') else Slur item = self.factory(cls, t) item.event = 'start' if t.endswith('(') else 'stop' return item - + @_tokencls(lilypond.Beam) def handle_beam(self, t, source=None): item = self.factory(Beam, t) item.event = 'start' if t == '[' else 'stop' return item - + @_tokencls(lilypond.Articulation) def handle_articulation(self, t, source=None): return self.factory(Articulation, t) - + def read_assignment(self, t): """Read an assignment from the variable name. May return None.""" item = self.factory(Assignment, t) @@ -433,26 +445,26 @@ def read_assignment(self, t): else: self.source.pushback() return - + def test_music_list(self, t): r"""Test whether a music list ({ ... }, << ... >>, starts here. - + Also handles \simultaneous { ... } and \sequential { ... } - correctly. These obscure commands are not even highlighted by + correctly. These obscure commands are not even highlighted by lex, but they exist in LilyPond... \simultaneous { ... } is like << ... >> but \sequential << ... >> just behaves like << ... >> - Returns a two-tuple(item; iterable), both may be None. If - item is not None, it can be either a UserCommand or a MusicList. If + Returns a two-tuple(item; iterable), both may be None. If + item is not None, it can be either a UserCommand or a MusicList. If iterable is None, the item is a UserCommand (namely \simultaneous or \sequential, but not followed by a { or <<); else the item is a - MusicList, and the iterable should be read fully to get all the - tokens inside the MusicList. If item is None, there is no MusicList + MusicList, and the iterable should be read fully to get all the + tokens inside the MusicList. If item is None, there is no MusicList and no token is read. - + This way you can handle the { ... } and << ... >> transparently in every input mode. - + """ def make_music_list(t, simultaneous, tokens=()): """Make the MusicList item.""" @@ -461,7 +473,7 @@ def make_music_list(t, simultaneous, tokens=()): item.tokens = tokens def last(t): item.tokens += (t,) return item, self.consume(last) - + if isinstance(t, (lilypond.OpenBracket, lilypond.OpenSimultaneous)): return make_music_list(t, t == '<<') elif isinstance(t, lilypond.SimultaneousOrSequentialCommand): @@ -472,7 +484,7 @@ def last(t): item.tokens += (t,) self.source.pushback() return self.factory(Keyword, t), None return None, None - + def read_music_item(self, t, source): r"""Read one music item (note, rest, s, \skip, or q) from t and source.""" item = None @@ -508,7 +520,7 @@ def read_music_item(self, t, source): if not self.in_chord and not in_pitch_command: self.add_duration(item, None, source) return item - + @_commands('\\relative') def handle_relative(self, t, source): item = self.factory(Relative, t) @@ -521,7 +533,7 @@ def handle_relative(self, t, source): continue break return item - + @_commands('\\absolute') def handle_absolute(self, t, source): item = self.factory(Absolute, t) @@ -529,7 +541,7 @@ def handle_absolute(self, t, source): item.append(i) break return item - + @_commands('\\transpose') def handle_transpose(self, t, source): item = self.factory(Transpose, t) @@ -542,7 +554,7 @@ def handle_transpose(self, t, source): continue break return item - + @_commands('\\clef') def handle_clef(self, t, source): item = self.factory(Clef, t) @@ -553,13 +565,13 @@ def handle_clef(self, t, source): item._specifier = self.factory(String, t, True) break return item - + @_commands('\\key') def handle_key(self, t, source): item = self.factory(KeySignature, t) item.extend(itertools.islice(self.read(source), 2)) return item - + @_commands('\\times', '\\tuplet', '\\scaleDurations') def handle_scaler(self, t, source): item = self.factory(Scaler, t) @@ -593,7 +605,7 @@ def handle_scaler(self, t, source): elif not isinstance(t, lex.Space): self.source.pushback() break - else: # t == '\\times' + else: # t == '\\times' for t in source: if isinstance(t, lilypond.Fraction): item.append(self.factory(Number, t)) @@ -607,14 +619,14 @@ def handle_scaler(self, t, source): item.append(i) break return item - + @_commands('\\tag', '\\keepWithTag', '\\removeWithTag', '\\appendToTag', '\\pushToTag') def handle_tag(self, t, source): item = self.factory(Tag, t) argcount = 3 if t in ('\\appendToTag', '\\pushToTag') else 2 item.extend(itertools.islice(self.read(), argcount)) return item - + @_commands('\\grace', '\\acciaccatura', '\\appoggiatura', '\\slashedGrace') def handle_grace(self, t, source): item = self.factory(Grace, t) @@ -622,7 +634,7 @@ def handle_grace(self, t, source): item.append(i) break return item - + @_commands('\\afterGrace') def handle_after_grace(self, t, source): item = self.factory(AfterGrace, t) @@ -634,7 +646,7 @@ def handle_after_grace(self, t, source): i.append(item[-1]) item.append(i) return item - + @_commands('\\repeat') def handle_repeat(self, t, source): item = self.factory(Repeat, t) @@ -643,6 +655,8 @@ def handle_repeat(self, t, source): for t in skip(source): if isinstance(t, lilypond.RepeatSpecifier): item._specifier = t + if t == 'volta': + self.volta = True elif not item.specifier and isinstance(t, lex.StringStart): item._specifier = self.factory(String, t, True) elif isinstance(t, lilypond.RepeatCount): @@ -669,8 +683,9 @@ def handle_repeat(self, t, source): self.source.pushback() break break + self.volta = False return item - + @_commands('\\alternative') def handle_alternative(self, t, source): item = self.factory(Alternative, t) @@ -678,7 +693,7 @@ def handle_alternative(self, t, source): item.append(i) break return item - + @_commands('\\tempo') def handle_tempo(self, t, source): item = self.factory(Tempo, t) @@ -689,7 +704,7 @@ def handle_tempo(self, t, source): for t in source: if not equal_sign_seen: if (not text_seen and isinstance(t, - (lilypond.SchemeStart, lex.StringStart, lilypond.Markup))): + (lilypond.SchemeStart, lex.StringStart, lilypond.Markup))): item.append(self.read_item(t)) t = None text_seen = True @@ -704,12 +719,12 @@ def handle_tempo(self, t, source): item.append(self.read_item(t)) elif t == "-": item.tokens += (t,) - ## if the last token does not belong to the \\tempo expression anymore, - ## push it back + # if the last token does not belong to the \\tempo expression anymore, + # push it back if t and not isinstance(t, (lex.Space, lex.Comment)): self.source.pushback() return item - + @_commands('\\time') def handle_time(self, t, source): item = self.factory(TimeSignature, t) @@ -724,16 +739,16 @@ def handle_time(self, t, source): self.source.pushback() break return item - + @_commands('\\partial') def handle_partial(self, t, source): item = self.factory(Partial, t) self.add_duration(item, None, source) return item - + @_commands('\\new', '\\context', '\\change') def handle_translator(self, t, source): - cls = Change if t == '\\change' else Context + cls = Change if t == '\\change' else Context item = self.factory(cls, t) isource = self.consume() for t in skip(isource): @@ -763,7 +778,7 @@ def handle_translator(self, t, source): if not isinstance(i, With): break return item - + _inputmode_commands = { '\\notemode': NoteMode, '\\notes': NoteMode, @@ -782,7 +797,7 @@ def handle_inputmode(self, t, source): item.append(i) break return item - + _lyricmode_commands = { '\\lyricmode': LyricMode, '\\lyrics': LyricMode, @@ -792,6 +807,7 @@ def handle_inputmode(self, t, source): } @_commands(*_lyricmode_commands) def handle_lyricmode(self, t, source): + self.lyric_mode = True cls = self._lyricmode_commands[t] item = self.factory(cls, t) if cls is LyricsTo: @@ -808,8 +824,9 @@ def handle_lyricmode(self, t, source): if i: item.append(i) break + self.lyric_mode = False return item - + def read_lyric_item(self, t): """Read one lyric item. Returns None for tokens it does not handle.""" if isinstance(t, (lex.StringStart, lilypond.MarkupStart)): @@ -832,7 +849,7 @@ def read_lyric_item(self, t): if i: item.append(i) return item - + @_commands('\\stringTuning') def handle_string_tuning(self, t, source): item = self.factory(StringTuning, t) @@ -840,13 +857,13 @@ def handle_string_tuning(self, t, source): item.append(arg) break return item - + @_commands('\\partcombine') def handle_partcombine(self, t, source=None): item = self.factory(PartCombine, t) item.extend(itertools.islice(self.read(), 2)) return item - + @_keywords('\\language') def handle_language(self, t, source): item = self.factory(Language, t) @@ -858,7 +875,7 @@ def handle_language(self, t, source): self.language = value break return item - + @_keywords('\\include') def handle_include(self, t, source): item = None @@ -876,7 +893,7 @@ def handle_include(self, t, source): if name: item.append(name) return item - + @_keywords('\\version') def handle_version(self, t, source): item = self.factory(Version, t) @@ -884,7 +901,7 @@ def handle_version(self, t, source): item.append(arg) break return item - + _bracketed_keywords = { '\\header': Header, '\\score': Score, @@ -906,7 +923,7 @@ def handle_bracketed(self, t, source): item.append(i) break return item - + @_keywords('\\set') def handle_set(self, t, source): item = self.factory(Set, t) @@ -920,7 +937,7 @@ def handle_set(self, t, source): break break return item - + @_keywords('\\unset') def handle_unset(self, t, source): item = self.factory(Unset, t) @@ -932,7 +949,7 @@ def handle_unset(self, t, source): tokens.append(t) item.tokens = tuple(tokens) return item - + @_keywords('\\override') def handle_override(self, t, source): item = self.factory(Override, t) @@ -948,7 +965,7 @@ def handle_override(self, t, source): else: item.append(self.factory(PathItem, t)) return item - + @_keywords('\\revert') def handle_revert(self, t, source): item = self.factory(Revert, t) @@ -964,7 +981,7 @@ def handle_revert(self, t, source): else: self.source.pushback() return item - + @_keywords('\\tweak') def handle_tweak(self, t, source): item = self.factory(Tweak, t) @@ -981,19 +998,19 @@ def handle_tweak(self, t, source): item.append(i) break return item - + @_commands('\\markup', '\\markuplist', '\\markuplines') def handle_markup(self, t, source=None): item = self.factory(Markup, t) self.add_markup_arguments(item) return item - + def read_markup(self, t): """Read LilyPond markup (recursively).""" meth = self._markup.method(t) if meth: return meth(self, t) - + @_markup(lilypond.MarkupScore) def handle_markup_score(self, t): item = self.factory(MarkupScore, t) @@ -1007,28 +1024,28 @@ def last(t): item.tokens += (t,) self.source.pushback() break return item - + @_markup(lilypond.MarkupCommand) def handle_markup_command(self, t): item = self.factory(MarkupCommand, t) self.add_markup_arguments(item) return item - + @_markup(lilypond.MarkupUserCommand) def handle_markup_user_command(self, t): item = self.factory(MarkupUserCommand, t) return item - + @_markup(lilypond.OpenBracketMarkup) def handle_markup_open_bracket(self, t): item = self.factory(MarkupList, t) self.add_markup_arguments(item) return item - + @_markup(lilypond.MarkupWord) def handle_markup_word(self, t): return self.factory(MarkupWord, t) - + def add_markup_arguments(self, item): """Add markup arguments to the item.""" for t in self.consume(): @@ -1038,7 +1055,7 @@ def add_markup_arguments(self, item): elif isinstance(item, MarkupList) and isinstance(t, lilypond.CloseBracketMarkup): item.tokens = (t,) return item - + def read_scheme_item(self, t): """Reads a Scheme expression (just after the # in LilyPond mode).""" item = self.factory(Scheme, t) @@ -1055,7 +1072,7 @@ def read_scheme(self, t): meth = self._scheme.method(t) if meth: return meth(self, t) - + @_scheme(scheme.Quote) def handle_scheme_quote(self, t): item = self.factory(SchemeQuote, t) @@ -1066,7 +1083,7 @@ def handle_scheme_quote(self, t): item.append(i) break return item - + @_scheme(scheme.OpenParen) def handle_scheme_open_parenthesis(self, t): item = self.factory(SchemeList, t) @@ -1077,7 +1094,7 @@ def last(t): item.tokens = (t,) if i: item.append(i) return item - + @_scheme( scheme.Dot, scheme.Bool, @@ -1089,12 +1106,10 @@ def last(t): item.tokens = (t,) ) def handle_scheme_token(self, t): return self.factory(SchemeItem, t) - + @_scheme(scheme.LilyPondStart) def handle_scheme_lilypond_start(self, t): item = self.factory(SchemeLily, t) def last(t): item.tokens = (t,) item.extend(self.read(self.consume(last))) return item - - diff --git a/ly/musicxml/__init__.py b/ly/musicxml/__init__.py index 7c215eba..049c1a69 100644 --- a/ly/musicxml/__init__.py +++ b/ly/musicxml/__init__.py @@ -27,6 +27,7 @@ from __future__ import unicode_literals + def writer(): """Convert LilyPond text to MusicXML @@ -43,5 +44,3 @@ def writer(): from . import lymus2musxml return lymus2musxml.ParseSource() - - diff --git a/ly/musicxml/create_musicxml.py b/ly/musicxml/create_musicxml.py index 4e425a31..2587127c 100644 --- a/ly/musicxml/create_musicxml.py +++ b/ly/musicxml/create_musicxml.py @@ -44,6 +44,14 @@ import ly.pkginfo +def duration(dictionary): + """ + Return the 'dur' element of a dictionary (used for `sort(key=duration)`) + Example from https://www.geeksforgeeks.org/python-list-sort/ + """ + return dictionary['dur'] + + class CreateMusicXML(): """ Creates the XML nodes according to the Music XML standard.""" @@ -60,6 +68,13 @@ def __init__(self): encoding_date.text = str(datetime.date.today()) self.partlist = etree.SubElement(self.root, "part-list") self.part_count = 1 + # Dictionary to keep track of all accidentals (or lack thereof) applied to each note name and octave in the current measure in reverse chronological order + # Keys look like 'N#' where N is a note letter and # is an octave number, for example 'A3' + # Entries of lists look like: {'dur': duration since bar at time of accidental, 'alt': accidental alter, + # 'note': note with the accidental, 'type': accidental type ('normal', '?', '!', None)} + # Includes notes which do not have an accidental applied because after a measure is complete, it may be necessary to add an accidental + self.accidentals = {} + self.current_dur = 0 ## # Building the basic Elements @@ -71,14 +86,26 @@ def create_title(self, title): mov_title.text = title self.root.insert(0, mov_title) + def create_subtitle(self, subtitle): + """Create score subtitle (using ).""" + # TODO: Better option than ? + mov_number = etree.Element("movement-number") + mov_number.text = subtitle + self.root.insert(0, mov_number) + def create_score_info(self, tag, info, attr={}): """Create score info.""" - info_node = etree.SubElement(self.score_info, tag, attr) + # modeled after https://kite.com/python/examples/3596/xml-insert-a-subelement-into-an-xml-element + info_node = etree.Element(tag, attr) info_node.text = info + # put info_node before data, prevents invalid xml + self.score_info.insert(0, info_node) def create_partgroup(self, gr_type, num, name=None, abbr=None, symbol=None): """Create a new part group.""" - attr_dict = {"type": gr_type, "number": str(num)} + attr_dict = {} + attr_dict["number"] = str(num) + attr_dict["type"] = gr_type partgroup = etree.SubElement(self.partlist, "part-group", attr_dict) if name: group_name = etree.SubElement(partgroup, "group-name") @@ -112,10 +139,32 @@ def create_part(self, name="unnamed", abbr=False, midi=False): self.part_count += 1 self.bar_nr = 1 + def correct_accidentals(self): + """ Remove and add accidentals to the previous measure which are made redundant or necessary by other voices """ + for note_name, acc_list in self.accidentals.items(): + acc_list.sort(key=duration) + current_alter = 0 + acc_count = 0 + for acc in acc_list: + # Add necessary accidentals not applied previously (as a result of multiple voices) + if acc['type'] is None and current_alter != acc['alt'] and acc_count: + self.add_accidental(acc['alt'], note=acc['note']) + acc_count += 1 + # Remove unnecessary accidentals + elif acc['type'] == 'normal' and acc['alt'] == current_alter and acc_count: + acc['note'].remove(acc['note'].find('accidental')) + acc_count -= 1 + elif acc['type'] is not None: + acc_count += 1 + current_alter = acc['alt'] + self.accidentals = {} + self.current_dur = 0 + def create_measure(self, **bar_attrs): """Create new measure """ + self.correct_accidentals() self.current_bar = etree.SubElement(self.current_part, "measure", number=str(self.bar_nr)) - self.bar_nr +=1 + self.bar_nr += 1 if bar_attrs: self.new_bar_attr(**bar_attrs) @@ -124,9 +173,13 @@ def create_measure(self, **bar_attrs): ## def new_note(self, step, octave, durtype, divdur, alter=0, - acc_token=0, voice=1, dot=0, chord=0, grace=(0, 0)): + acc_token=None, voice=1, dot=0, chord=0, grace=(0, 0), staff=0, beam=False): """Create all nodes needed for a normal note. """ self.create_note() + key = step + str(octave) + if key not in self.accidentals: + self.accidentals[key] = [] + self.accidentals[key].append({'dur': self.current_dur, 'alt': alter, 'note': self.current_note, 'type': acc_token}) if grace[0]: self.add_grace(grace[1]) if chord: @@ -139,16 +192,20 @@ def new_note(self, step, octave, durtype, divdur, alter=0, if dot: for i in range(dot): self.add_dot() - if alter or acc_token: - if acc_token == '!': # cautionary + if acc_token is not None: + if acc_token == '!': # cautionary self.add_accidental(alter, caut=True) - elif acc_token == '?': # parentheses + elif acc_token == '?': # parentheses self.add_accidental(alter, parenth=True) - else: + elif acc_token == 'normal': self.add_accidental(alter) + if staff: + self.add_staff(staff) + if beam: + self.add_beam(1, beam) def new_unpitched_note(self, step, octave, durtype, divdur, voice=1, - dot=0, chord=0, grace=(0, 0)): + dot=0, chord=0, grace=(0, 0), staff=0): """Create all nodes needed for an unpitched note. """ self.create_note() if grace[0]: @@ -163,14 +220,12 @@ def new_unpitched_note(self, step, octave, durtype, divdur, voice=1, if dot: for i in range(dot): self.add_dot() + if staff: + self.add_staff(staff) def tuplet_note(self, fraction, bs, ttype, nr, divs, atyp='', ntyp=''): """Convert current note to tuplet """ - base = self.mult * bs[0] - scaling = bs[1] - a = divs*4*fraction[1] - b = (1/base)*fraction[0] - duration = (a/b)*scaling + duration = bs[0] * bs[1] * divs * 4 self.change_div_duration(duration) from fractions import Fraction self.mult = Fraction(fraction[1], fraction[0]) @@ -186,12 +241,12 @@ def tuplet_note(self, fraction, bs, ttype, nr, divs, atyp='', ntyp=''): else: self.add_tuplet_type(nr, ttype) - def tie_note(self, tie_type): + def tie_note(self, tie_type, line): self.add_tie(tie_type) self.add_notations() - self.add_tied(tie_type) + self.add_tied(tie_type, line) - def new_rest(self, duration, durtype, pos, dot, voice): + def new_rest(self, duration, durtype, pos, dot, voice, staff): """Create all nodes needed for a rest. """ self.create_note() if pos: @@ -205,6 +260,8 @@ def new_rest(self, duration, durtype, pos, dot, voice): if dot: for i in range(dot): self.add_dot() + if staff: + self.add_staff(staff) def new_articulation(self, artic): """Add specified articulation. """ @@ -226,9 +283,11 @@ def new_adv_ornament(self, ornament, args): if ornament == "wavy-line": self.add_wavyline(args['type']) - def new_bar_attr(self, clef=0, mustime=0, key=None, mode=0, divs=0): + def new_bar_attr(self, clef=0, mustime=0, key=None, mode=0, divs=0, br=False): """Create all bar attributes set. """ self.create_bar_attr() + if br: + self.add_break() if divs: self.add_divisions(divs) if key is not None: @@ -257,14 +316,14 @@ def create_new_node(self, parentnode, nodename, txt): new_node = etree.SubElement(parentnode, nodename) new_node.text = str(txt) - ## # Low-level node creation ## def add_creator(self, creator, name): """Add creator to score info.""" - attr = {'type': creator } + attr = {} + attr["type"] = creator self.create_score_info("creator", name, attr) def add_rights(self, rights): @@ -298,20 +357,23 @@ def add_unpitched(self, step, octave): octnode = etree.SubElement(unpitched, "display-octave") octnode.text = str(octave) - def add_accidental(self, alter, caut=False, parenth=False): + def add_accidental(self, alter, caut=False, parenth=False, note=None): """Create accidental.""" + selected_note = self.current_note + if note is not None: + selected_note = note attrib = {} if caut: attrib['cautionary'] = 'yes' if parenth: attrib['parentheses'] = 'yes' - acc = etree.SubElement(self.current_note, "accidental", attrib) + acc = etree.SubElement(selected_note, "accidental", attrib) acc_dict = { - 0: 'natural', - 1: 'sharp', -1: 'flat', - 2: 'sharp-sharp', -2: 'flat-flat', - 0.5: 'natural-up', -0.5: 'natural-down', - 1.5: 'sharp-up', -1.5: 'flat-down' + 0: 'natural', + 1: 'sharp', -1: 'flat', + 2: 'sharp-sharp', -2: 'flat-flat', + 0.5: 'natural-up', -0.5: 'natural-down', + 1.5: 'sharp-up', -1.5: 'flat-down' } acc.text = acc_dict[alter] @@ -329,20 +391,27 @@ def add_rest_w_pos(self, step, octave): def add_skip(self, duration, forward=True): if forward: + self.current_dur += duration skip = etree.SubElement(self.current_bar, "forward") else: + self.current_dur -= duration + if self.current_dur < 0: + self.current_dur = 0 skip = etree.SubElement(self.current_bar, "backward") dura_node = etree.SubElement(skip, "duration") dura_node.text = str(duration) def add_div_duration(self, divdur): """Create new duration """ + self.current_dur += divdur self.duration = etree.SubElement(self.current_note, "duration") self.duration.text = str(divdur) self.mult = 1 def change_div_duration(self, newdura): """Set new duration when tuplet """ + self.current_dur -= int(self.duration.text) + self.current_dur += newdura self.duration.text = str(newdura) def add_duration_type(self, durtype): @@ -356,7 +425,7 @@ def add_dot(self): def add_beam(self, nr, beam_type): """Add beam. """ - beam_node = etree.SubElement(self.current_notation, "beam", number=str(nr)) + beam_node = etree.SubElement(self.current_note, "beam", number=str(nr)) beam_node.text = beam_type def add_tie(self, tie_type): @@ -377,9 +446,13 @@ def add_notations(self): if not self.current_notation: self.current_notation = etree.SubElement(self.current_note, "notations") - def add_tied(self, tie_type): + def add_tied(self, tie_type, line): """Create node tied (used for notation of tie) """ - etree.SubElement(self.current_notation, "tied", type=tie_type) + attr = {} + if line != "solid": + attr["line-type"] = line + attr["type"] = tie_type + etree.SubElement(self.current_notation, "tied", attr) def add_time_modify(self, fraction): """Create time modification """ @@ -410,8 +483,10 @@ def adjust_time_modify(self, timemod_node, fraction): def add_tuplet_type(self, nr, ttype, actnr=0, acttype='', normnr=0, normtype=''): """Create tuplet with type attribute """ - tuplnode = etree.SubElement(self.current_notation, "tuplet", - {'number': str(nr), 'type': ttype }) + attr = {} + attr["number"] = str(nr) + attr["type"] = ttype + tuplnode = etree.SubElement(self.current_notation, "tuplet", attr) if actnr: actnode = etree.SubElement(tuplnode, "tuplet-actual") atn = etree.SubElement(actnode, "tuplet-number") @@ -429,10 +504,15 @@ def add_tuplet_type(self, nr, ttype, actnr=0, acttype='', normnr=0, normtype='') normtype = self.current_note.find("type").text ntt.text = normtype - def add_slur(self, nr, sl_type): + def add_slur(self, nr, sl_type, line): """Add slur. """ self.add_notations() - etree.SubElement(self.current_notation, "slur", {'number': str(nr), 'type': sl_type }) + attr = {} + if line != "solid": + attr["line-type"] = line + attr["number"] = str(nr) + attr["type"] = sl_type + etree.SubElement(self.current_notation, "slur", attr) def add_named_notation(self, notate): """Fermata, etc. """ @@ -455,7 +535,10 @@ def add_ornaments(self): def add_tremolo(self, trem_type, lines): self.add_ornaments() - trem_node = etree.SubElement(self.current_ornament, "tremolo", type=trem_type) + if trem_type is not None: + trem_node = etree.SubElement(self.current_ornament, "tremolo", type=trem_type) + else: + trem_node = etree.SubElement(self.current_ornament, "tremolo") trem_node.text = str(lines) def add_trill(self): @@ -475,7 +558,10 @@ def add_wavyline(self, end_type): etree.SubElement(self.current_ornament, "wavy-line", type=end_type) def add_gliss(self, linetype, endtype, nr): - nodedict = { "line-type": linetype, "number": str(nr), "type": endtype } + nodedict = {} + nodedict["line-type"] = linetype + nodedict["number"] = str(nr) + nodedict["type"] = endtype self.add_notations() etree.SubElement(self.current_notation, "glissando", nodedict) @@ -493,6 +579,46 @@ def create_bar_attr(self): """Create node attributes """ self.bar_attr = etree.SubElement(self.current_bar, "attributes") + def add_harmony(self, rt, rt_a=0, bs=False, bs_a=0, txt="", ofst=0): + """Create harmony element""" + CHORD_DICT = {"": "major", "m": "minor", "aug": "augmented", "dim": "diminished", + "7": "dominant", "maj7": "major-seventh", "m7": "minor-seventh", + "dim7": "diminished-seventh", "aug7": "augmented-seventh", "m7.5-": "half-diminished", + "m7+": "major-minor", "6": "major-sixth", "m6": "minor-sixth", "9": "dominant-ninth", + "maj9": "major-ninth", "m9": "minor-ninth", "11": "dominant-11th", "maj11": "major-11th", + "m11": "minor-11th", "13": "dominant-13th", "maj13": "major-13th", "m13": "minor-13th", + "sus2": "suspended-second", "sus4": "suspended-fourth", "m5": "minor", "5": "major", + "maj": "major-seventh", "13.11": "dominant-thirteenth", "maj13.11": "major-13th", + "m13.11": "minor-13th"} + harmony = etree.SubElement(self.current_bar, "harmony") + root = etree.SubElement(harmony, "root") + root_step = etree.SubElement(root, "root-step") + root_step.text = rt + if rt_a != 0: + root_alter = etree.SubElement(root, "root-alter") + root_alter.text = str(rt_a) + if txt in CHORD_DICT: + kind = etree.SubElement(harmony, "kind", text=txt) + kind.text = CHORD_DICT[txt] + else: + kind = etree.SubElement(harmony, "kind", text=txt) + kind.text = "major" # this is just a placeholder in order to make this a valid xml + if bs: + bass = etree.SubElement(harmony, "bass") + bass_step = etree.SubElement(bass, "bass-step") + bass_step.text = bs + if bs_a != 0: + bass_alter = etree.SubElement(bass, "bass-alter") + bass_alter.text = str(bs_a) + if ofst: + offset = etree.SubElement(harmony, "offset") + offset.text = str(ofst) + + def add_break(self): + attr_dict = {} + attr_dict["new-system"] = "yes" + sys_break = etree.SubElement(self.current_bar, "print", attr_dict) + def add_divisions(self, div): division = etree.SubElement(self.bar_attr, "divisions") division.text = str(div) @@ -505,7 +631,7 @@ def add_key(self, key, mode): modenode.text = str(mode) def add_time(self, timesign): - if len(timesign)==3: + if len(timesign) == 3: timenode = etree.SubElement(self.bar_attr, "time", symbol=timesign[2]) else: timenode = etree.SubElement(self.bar_attr, "time") @@ -528,16 +654,30 @@ def add_clef(self, sign, line, nr=0, oct_ch=0): octchnode = etree.SubElement(clefnode, "clef-octave-change") octchnode.text = str(oct_ch) - def add_barline(self, bl_type, repeat=None): - barnode = etree.SubElement(self.current_bar, "barline", location="right") - barstyle = etree.SubElement(barnode, "bar-style") - barstyle.text = bl_type + def add_barline(self, bl_type=None, ending=None, repeat=None): + # Do not place repeat if this is the first measure + if repeat == "forward" and "number" in self.current_bar.attrib and self.current_bar.attrib["number"] == "1": + return + if repeat == "forward" or ending and ending.etype == "start": # Forward repeats and ending starts should be at the start of the measure + barnode = etree.SubElement(self.current_bar, "barline", location="left") + else: # All other barlines should be at the end of the measure + barnode = etree.SubElement(self.current_bar, "barline", location="right") + if bl_type: + barstyle = etree.SubElement(barnode, "bar-style") + barstyle.text = bl_type + if ending: + endingnode = etree.SubElement(barnode, "ending", number=ending.get_number(), type=ending.etype) + if ending.etype == "start": + endingnode.text = ending.get_text() if repeat: repeatnode = etree.SubElement(barnode, "repeat", direction=repeat) def add_backup(self, duration): if duration <= 0: return + self.current_dur -= duration + if self.current_dur < 0: + self.current_dur = 0 backupnode = etree.SubElement(self.current_bar, "backup") durnode = etree.SubElement(backupnode, "duration") durnode.text = str(duration) @@ -591,7 +731,9 @@ def add_dynamic_dashes(self, text): def add_octave_shift(self, plac, octdir, size): """Add octave shift.""" - oct_dict = {"type": octdir, "size": str(size) } + oct_dict = {} + oct_dict["type"] = octdir + oct_dict["size"] = str(size) direction = etree.SubElement(self.current_bar, "direction", placement=plac) dirtypenode = etree.SubElement(direction, "direction-type") dyn_node = etree.SubElement(dirtypenode, "octave-shift", oct_dict) @@ -616,17 +758,22 @@ def add_metron_dir(self, unit, beats, dots): def add_sound_dir(self, midi_tempo): soundnode = etree.SubElement(self.direction, "sound", tempo=str(midi_tempo)) - def add_lyric(self, txt, syll, nr, ext=False): + def add_lyric(self, txt, syll, nr, args=[]): """ Add lyric element. """ lyricnode = etree.SubElement(self.current_note, "lyric", number=str(nr)) syllnode = etree.SubElement(lyricnode, "syllabic") syllnode.text = syll - txtnode = etree.SubElement(lyricnode, "text") + arg_dict = {} + if "italic" in args: + arg_dict["font-style"] = "italic" + txtnode = etree.SubElement(lyricnode, "text", arg_dict) txtnode.text = txt - if ext: + # For lyrics which were not parsed correctly (https://lxml.de/tutorial.html) + if txt == "ERROR": + lyricnode.insert(0, etree.Comment("Lyric text was not parsed correctly, so it was marked with ERROR")) + if "extend" in args: etree.SubElement(lyricnode, "extend") - ## # Create the XML document ## @@ -640,6 +787,7 @@ def musicxml(self, prettyprint=True): class MusicXML(object): """Represent a generated MusicXML tree.""" + def __init__(self, tree): self.tree = tree self.root = tree.getroot() diff --git a/ly/musicxml/ly2xml_mediator.py b/ly/musicxml/ly2xml_mediator.py index a9ec6951..dac433cf 100644 --- a/ly/musicxml/ly2xml_mediator.py +++ b/ly/musicxml/ly2xml_mediator.py @@ -28,23 +28,39 @@ from fractions import Fraction +import sys import ly.duration import ly.pitch from . import xml_objs +def eprint(*args, **kwargs): + """ + From https://stackoverflow.com/questions/5574702/how-to-print-to-stderr-in-python + Prints to stderr + """ + print(*args, file=sys.stderr, **kwargs) + + class Mediator(): """Help class that acts as mediator between the ly source parser and the XML creating modules.""" def __init__(self): """ create global lists """ + # the current number of flats/sharps in key (ex: -3 indicates 3 flats in the key) + self.num_accidentals_in_key = 0 + # keeps track of the most recent number of flats/sharps applied to all notes (ex: 'B':-2 is double flat B) + self.current_accidentals_dict = {'C': 0, 'D': 0, 'E': 0, 'F': 0, 'G': 0, 'A': 0, 'B': 0} self.score = xml_objs.Score() self.sections = [] + self.permanent_sections = [] """ default and initial values """ self.insert_into = None + self.current_attr = None self.current_note = None + self.prev_note = None self.current_lynote = None self.current_is_rest = False self.action_onnext = [] @@ -52,8 +68,11 @@ def __init__(self): self.dur_token = "4" self.dur_tokens = () self.dots = 0 - self.tied = False + self.tie_list = [] + self.tie_line = 'solid' self.voice = 1 + self.voice_sep_sections = 0 + self.voice_name = None self.staff = 0 self.part = None self.group = None @@ -62,6 +81,7 @@ def __init__(self): self.q_chord = [] self.prev_pitch = None self.prev_chord_pitch = None + self.prev_bar = None self.store_voicenr = 0 self.staff_id_dict = {} self.store_unset_staff = False @@ -76,12 +96,16 @@ def __init__(self): self.prev_tremolo = 8 self.tupl_dur = 0 self.tupl_sum = 0 + self.ly_to_xml_oct = 3 # xml octave values are 3 higher than lilypond + self.current_chord_or_note = [] def new_header_assignment(self, name, value): """Distributing header information.""" creators = ['composer', 'arranger', 'poet', 'lyricist'] if name == 'title': self.score.title = value + elif name == 'subtitle': + self.score.subtitle = value elif name == 'copyright': self.score.rights = value elif name in creators: @@ -94,6 +118,7 @@ def new_section(self, name, glob=False): section = xml_objs.ScoreSection(name, glob) self.insert_into = section self.sections.append(section) + self.permanent_sections.append(section) self.bar = None def new_snippet(self, name): @@ -101,6 +126,7 @@ def new_snippet(self, name): snippet = xml_objs.Snippet(name, self.insert_into) self.insert_into = snippet self.sections.append(snippet) + self.permanent_sections.append(snippet) self.bar = None def new_lyric_section(self, name, voice_id): @@ -117,7 +143,7 @@ def check_name(self, name, nr=1): return name def get_var_byname(self, name): - for n in self.sections: + for n in self.permanent_sections: if n.name == name: return n @@ -125,7 +151,7 @@ def new_group(self): parent = self.group self.group_num += 1 self.group = xml_objs.ScorePartGroup(self.group_num, "bracket") - if parent: #nested group + if parent: # nested group self.group.parent = parent parent.partlist.append(self.group) else: @@ -170,20 +196,14 @@ def get_part_by_id(self, pid, partholder=None): def set_voicenr(self, command=None, add=False, nr=0, piano=0): if add: - if not self.store_voicenr: - self.store_voicenr = self.voice self.voice += 1 elif nr: self.voice = nr else: self.voice = get_voice(command) - if piano>2: + if piano > 2: self.voice += piano+1 - def revert_voicenr(self): - self.voice = self.store_voicenr - self.store_voicenr = 0 - def set_staffnr(self, staffnr, staff_id=None): self.store_unset_staff = False if staffnr: @@ -212,6 +232,7 @@ def add_snippet(self, snippet_name): def continue_barlist(insert_into): self.insert_into = insert_into if insert_into.barlist: + self.prev_bar = self.bar self.bar = insert_into.barlist[-1] else: self.new_bar(False) @@ -230,7 +251,7 @@ def check_voices(self): if self.sections[-1].glob: self.score.merge_globally(self.sections[-1]) self.score.glob_section.merge_voice(self.section[-1]) - if len(self.sections)>2: + if len(self.sections) > 2: if not self.sections[-2].barlist: self.sections.pop(-2) self.check_voices() @@ -245,31 +266,33 @@ def check_voices_by_nr(self): """ Used for snippets. Merges all active snippets created after the stored voice number.""" sect_len = len(self.sections) - if sect_len>2: - if self.voice>1: - for n in range(self.store_voicenr, self.voice): + if sect_len > 2: + if self.voice > 1: + voices_skipped = (self.voice - self.store_voicenr) - (self.voice_sep_sections - 1) + for n in range(self.store_voicenr, self.voice - voices_skipped): self.check_voices() if isinstance(self.sections[-1], xml_objs.Snippet): self.add_snippet(self.sections[-1].name) self.sections.pop() else: - print("WARNING: problem adding snippet!") + eprint("WARNING: problem adding snippet!") def check_lyrics(self, voice_id): """Check the finished lyrics section and merge it into the referenced voice.""" - if self.lyric[1] == 'middle': + if self.lyric is not None and self.lyric[1] == 'middle': self.lyric[1] = 'end' lyrics_section = self.lyric_sections['lyricsto'+voice_id] - voice_section = self.get_var_byname(lyrics_section.voice_id) + voice_section = self.sections[-1] if voice_section: voice_section.merge_lyrics(lyrics_section) else: - print("Warning can't merge in lyrics!", voice_section) + eprint("Warning can't merge in lyrics!", voice_section) + self.lyric = None # Clear final lyric def check_part(self): """Adds the latest active section to the part.""" - if len(self.sections)>1: + if len(self.sections) > 1: if self.score.is_empty(): self.new_part() if self.sections[-1].glob: @@ -288,9 +311,10 @@ def check_simultan(self): """Check done after simultanoues (<< >>) section.""" if self.part: self.part.merge_voice(self.sections[-1]) - elif len(self.sections)>1: - self.sections[-2].merge_voice(self.sections[-1]) - self.sections.pop() + elif len(self.sections) > 1: + self.sections[-2].merge_voice(self.sections[-1]) + if len(self.sections) > 0: + self.sections.pop() def check_score(self): """ @@ -303,16 +327,60 @@ def check_score(self): if self.score.is_empty(): self.new_part() self.part.barlist.extend(self.get_first_var()) - self.score.merge_globally(self.score.glob_section, override=True) + self.score.merge_globally(self.score.glob_section, False) + + def reset_current_accidentals_dict(self, fifths): + """Resets self.current_accidentals_dict to only include the accidentals in the current key""" + ORDER_OF_FIFTHS = ('B', 'E', 'A', 'D', 'G', 'C', 'F') + # Reset all notes to natural, and remove any notes with octaves + self.current_accidentals_dict = {'C': 0, 'D': 0, 'E': 0, 'F': 0, 'G': 0, 'A': 0, 'B': 0} + if fifths < 0: # flat key + for f in ORDER_OF_FIFTHS[:abs(fifths)]: + self.current_accidentals_dict[f] = -1 + elif fifths > 0: # sharp key + for s in ORDER_OF_FIFTHS[-fifths:]: + self.current_accidentals_dict[s] = 1 + + def is_acc_needed(self, name, octave, alter): + """ + Check if a given note needs an accidental + given the current state of accidentals + + name is the name of the note ('A', 'B', 'C', 'D', 'E', 'F', 'G') + alter is a number representing the flat/sharp status of the note (-1 is flat, +1 is sharp, 0 is natural) + """ + abs_note = name + str(octave) + if self.check_for_tie_end(name, octave, alter) is not None: # subsequent tied notes never need accidentals + return False + if abs_note in self.current_accidentals_dict: + # Same note in same octave had same alter + if self.current_accidentals_dict[abs_note] == alter: + return False + # Same note in same octave had different alter + else: + self.current_accidentals_dict[abs_note] = alter + return True + # No previous same notes in same octave, but key matches alter + elif self.current_accidentals_dict[name] == alter: + self.current_accidentals_dict[abs_note] = alter + return False + # No previous same notes in same octave, and key has different alter + else: + self.current_accidentals_dict[abs_note] = alter + return True + eprint("Warning: Invalid note checked for accidental!") + return False def get_first_var(self): if self.sections: return self.sections[0].barlist def new_bar(self, fill_prev=True): + self.reset_current_accidentals_dict(self.num_accidentals_in_key) if self.bar and fill_prev: self.bar.list_full = True self.current_attr = xml_objs.BarAttr() + self.prev_bar = self.bar self.bar = xml_objs.Bar() self.bar.obj_list = [self.current_attr] self.insert_into.barlist.append(self.bar) @@ -322,21 +390,127 @@ def add_to_bar(self, obj): self.new_bar() self.bar.add(obj) + def change_end_type(self, bar, from_type, to_type): + """ Changes ending in bar of from_type to to_type. """ + obj = bar.obj_list[0] + if isinstance(obj, xml_objs.BarAttr): + for end in obj.endings: + if end.etype == from_type: + end.etype = to_type + return + + def update_ending(self, bl, bar): + """ Updates whether an ending has a downward jog based on what type of barline is added (bl). """ + if bl in ['|.', '.|']: # 'light-heavy' or 'heavy-light' + # Add downward jog to ending + self.change_end_type(bar, 'discontinue', 'stop') + else: + # Remove downward jog from ending + self.change_end_type(bar, 'stop', 'discontinue') + def create_barline(self, bl): + self.update_ending(bl, self.bar) barline = xml_objs.BarAttr() barline.set_barline(bl) self.bar.add(barline) self.new_bar() - def new_repeat(self, rep): - barline = xml_objs.BarAttr() - barline.set_barline(rep) - barline.repeat = rep + def new_repeat(self, rep, prev=False): + """ Create a repeat sign (forward or backward). """ + # Start a new bar when None or forward repeat in middle of measure + if self.bar is None or (rep == 'forward' and self.bar.has_music()): + self.new_bar() + # Determine whether the repeat should be in this measure or the previous + bar = None + if prev or (rep == 'backward' and not self.bar.has_music()): + bar = self.prev_bar + else: + bar = self.bar + # Determine whether a new attribute is needed + attr = None + if rep == 'backward': + if isinstance(bar.obj_list[-1], xml_objs.BarAttr): + attr = bar.obj_list[-1] + else: + attr = xml_objs.BarAttr() + bar.add(attr) + else: + attr = bar.obj_list[0] + # Set the barline (or left barline for forward repeats), but do not overwrite existing special barlines + existing_barline = False + if rep == 'forward': + if self.prev_bar is not None: + for obj in self.prev_bar.obj_list: + if isinstance(obj, xml_objs.BarAttr): + # Backward repeats with 'light-heavy' barlines don't count as special barlines + if obj.repeat == 'backward': + if obj.barline != 'light-heavy': + existing_barline = True + break + elif obj.barline is not None: + existing_barline = True + break + if not existing_barline: + attr.set_left_barline(rep) + else: + for obj in bar.obj_list: + if isinstance(obj, xml_objs.BarAttr): + if obj.barline is not None: + existing_barline = True + break + if not existing_barline: + attr.set_barline(rep) + # Create the repeat + attr.repeat = rep + # Start a new bar if necessary + if not prev and rep == 'backward' and self.bar.has_music(): + self.new_bar() + + def new_ending(self, start, end, etype, staff_nr): + """ Create an alternate ending to a repeat. """ if self.bar is None: self.new_bar() - self.bar.add(barline) + # Choose the appropriate bar, create necessary repeats between endings, + # and create empty barlines at the start and end of the alternate endings (if they occur in the middle of a measure) + bar = None + if not self.bar.has_music(): + if etype == 'start': + bar = self.bar + else: + bar = self.prev_bar + if etype == 'stop': + self.new_repeat('backward', prev=True) + else: + if etype == 'start': + self.create_barline('') + bar = self.bar + elif etype == 'discontinue': + bar = self.bar + self.create_barline('') + elif etype == 'stop': + bar = self.bar + self.new_repeat('backward') + # If the final ending in a set of alternate endings ends on a 'light-heavy' barline, give it a "downward jog" + if etype == 'discontinue': + for obj in bar.obj_list: + if isinstance(obj, xml_objs.BarAttr): + if obj.barline in ['light-heavy', 'heavy-light']: + etype = 'stop' + break + elif etype == 'stop': + for obj in bar.obj_list: + if isinstance(obj, xml_objs.BarAttr): + if obj.barline is not None and obj.barline not in ['light-heavy', 'heavy-light']: + etype = 'discontinue' + break + # Mark the ending only if this is the first staff + if staff_nr == 1: + attr = bar.obj_list[0] + attr.add_ending(start, end, etype) def new_key(self, key_name, mode): + self.num_accidentals_in_key = get_fifths(key_name, mode) + self.reset_current_accidentals_dict(self.num_accidentals_in_key) if self.bar is None: self.new_bar() if self.bar.has_music(): @@ -349,7 +523,7 @@ def new_key(self, key_name, mode): def new_time(self, num, den, numeric=False): if self.bar is None: self.new_bar() - self.current_attr.set_time([num, den.denominator], numeric) + self.current_attr.set_time([num, den], numeric) def new_clef(self, clefname): self.clef = clefname2clef(clefname) @@ -365,6 +539,17 @@ def new_clef(self, clefname): else: self.current_attr.set_clef(self.clef) + def set_sys_break(self): + if self.bar is None: + self.new_bar() + if self.bar.has_music(): + self.new_bar() + new_bar_attr = xml_objs.BarAttr() + new_bar_attr.sys_break = True + self.add_to_bar(new_bar_attr) + else: + self.current_attr.sys_break = True + def set_relative(self, note): self.prev_pitch = note.pitch @@ -372,12 +557,15 @@ def new_note(self, note, rel=False, is_unpitched=False): self.current_is_rest = False self.clear_chord() if is_unpitched: + self.prev_note = self.current_note self.current_note = self.create_unpitched(note) self.check_current_note(is_unpitched=True) else: - self.current_note = self.create_barnote_from_note(note) + self.prev_note = self.current_note + self.current_note = self.create_barnote_from_note(note, rel) self.current_lynote = note self.check_current_note(rel) + self.current_chord_or_note = [self.current_note] self.do_action_onnext(self.current_note) self.action_onnext = [] @@ -401,27 +589,43 @@ def new_iso_dura(self, note, rel=False, is_unpitched=False): def create_unpitched(self, unpitched): """Create a xml_objs.Unpitched from ly.music.items.Unpitched.""" dura = unpitched.duration - return xml_objs.Unpitched(dura) + return xml_objs.Unpitched(dura, voice=self.voice, voice_name=self.voice_name) - def create_barnote_from_note(self, note): + def create_barnote_from_note(self, note, relative): """Create a xml_objs.BarNote from ly.music.items.Note.""" + p_copy = note.pitch.copy() + if relative: + p_copy.makeAbsolute(self.prev_pitch) + octave = p_copy.octave + self.ly_to_xml_oct p = getNoteName(note.pitch.note) alt = get_xml_alter(note.pitch.alter) try: - acc = note.accidental_token + acc = note.accidental_token # special accidentals (?, !) except AttributeError: - acc = "" + acc = None + if acc is None and self.is_acc_needed(p, octave, alt): # check if a normal accidental should be printed + acc = 'normal' dura = note.duration - return xml_objs.BarNote(p, alt, acc, dura, self.voice) + return xml_objs.BarNote(p, alt, acc, dura, self.voice, self.voice_name) def copy_barnote_basics(self, bar_note): """Create a copy of a xml_objs.BarNote.""" + octave = bar_note.octave p = bar_note.base_note alt = bar_note.alter - acc = bar_note.accidental_token + try: + if bar_note.accidental_token != 'normal': + acc = bar_note.accidental_token # special accidentals (?, !) + else: + acc = None + except AttributeError: + acc = None + if acc is None and self.is_acc_needed(p, octave, alt): # check if a normal accidental should be printed + acc = 'normal' dura = bar_note.duration voc = bar_note.voice - copy = xml_objs.BarNote(p, alt, acc, dura, voc) + voc_name = bar_note.voice_name + copy = xml_objs.BarNote(p, alt, acc, dura, voc, voc_name) copy.octave = bar_note.octave copy.chord = bar_note.chord return copy @@ -436,9 +640,11 @@ def check_current_note(self, rel=False, rest=False, is_unpitched=False): if not rest and not is_unpitched: self.set_octave(rel) if not rest: - if self.tied: - self.current_note.set_tie('stop') - self.tied = False + note = self.current_note + tie_idx = self.check_for_tie_end(note.base_note, note.octave, note.alter) + if tie_idx is not None: + self.tie_list.pop(tie_idx) + note.set_tie('stop', self.tie_line) self.check_duration(rest) self.check_divs() if self.staff: @@ -450,13 +656,17 @@ def check_current_note(self, rel=False, rest=False, is_unpitched=False): self.staff_unset_notes[self.staff] = [self.current_note] self.add_to_bar(self.current_note) + def check_chord_note_for_staff(self, chord_note): + if self.staff: + chord_note.set_staff(self.staff) + def set_octave(self, relative): - """Set octave by getting the octave of an absolute note + 3.""" + """Set octave by getting the octave of an absolute note + self.ly_to_xml_oct (3).""" p = self.current_lynote.pitch.copy() if relative: p.makeAbsolute(self.prev_pitch) self.prev_pitch = p - self.current_note.set_octave(p.octave + 3) + self.current_note.set_octave(p.octave + self.ly_to_xml_oct) def do_action_onnext(self, note): """Perform the stored action on the next note.""" @@ -467,7 +677,7 @@ def do_action_onnext(self, note): def check_duration(self, rest): """Check the duration for the current note.""" dots, rs = self.duration_from_tokens(self.dur_tokens) - if rest and rs: # special case of multibar rest + if rest and rs: # special case of multibar rest if not self.current_note.show_type or self.current_note.skip: bs = self.current_note.duration if rs == bs[1]: @@ -491,30 +701,38 @@ def new_chord(self, note, duration=None, rel=False, chord_base=True): self.do_action_onnext(self.current_chord[-1]) def new_chordbase(self, note, duration, rel=False): - self.current_note = self.create_barnote_from_note(note) + self.prev_note = self.current_note + self.current_note = self.create_barnote_from_note(note, rel) self.current_note.set_duration(duration) self.current_lynote = note + self.current_chord_or_note = [self.current_note] self.check_current_note(rel) def new_chordnote(self, note, rel): - chord_note = self.create_barnote_from_note(note) + chord_note = self.create_barnote_from_note(note, rel) chord_note.set_duration(self.current_note.duration) chord_note.set_durtype(durval2type(self.dur_token)) chord_note.dots = self.dots - chord_note.tie = self.current_note.tie chord_note.tuplet = self.current_note.tuplet if not self.prev_chord_pitch: self.prev_chord_pitch = self.prev_pitch p = note.pitch.copy() if(rel): p.makeAbsolute(self.prev_chord_pitch) - chord_note.set_octave(p.octave + 3) + chord_note.set_octave(p.octave + self.ly_to_xml_oct) self.prev_chord_pitch = p chord_note.chord = True + self.current_chord_or_note.append(chord_note) self.bar.add(chord_note) + self.check_chord_note_for_staff(chord_note) + tie_idx = self.check_for_tie_end(chord_note.base_note, chord_note.octave, chord_note.alter) + if tie_idx is not None: + self.tie_list.pop(tie_idx) + chord_note.set_tie('stop', self.tie_line) return chord_note def copy_prev_chord(self, duration): + self.current_chord_or_note = [] if self.current_chord: prev_chord = self.current_chord self.clear_chord() @@ -522,15 +740,26 @@ def copy_prev_chord(self, duration): prev_chord = self.q_chord for i, pc in enumerate(prev_chord): cn = self.copy_barnote_basics(pc) - cn.set_duration(duration) + cn.set_staff(pc.staff) + pc_dot = 0 + if duration == pc.duration: # Carry over dots from prev chord if this copy has same duration + pc_dot = pc.dot + cn.set_duration(duration, dot=pc_dot) cn.set_durtype(durval2type(self.dur_token)) if i == 0: + self.prev_note = self.current_note self.current_note = cn self.current_chord.append(cn) - if self.tied: - cn.set_tie('stop') + self.current_chord_or_note.append(cn) + tie_idx = self.check_for_tie_end(cn.base_note, cn.octave, cn.alter) + if tie_idx is not None: + self.tie_list.pop(tie_idx) + cn.set_tie('stop', self.tie_line) self.bar.add(cn) - self.tied = False + if i == 0: # On base note of chord, update divisions + self.check_duration(False) + self.check_divs() + self.current_chord_or_note.append('end') def clear_chord(self): self.q_chord = self.current_chord @@ -539,6 +768,7 @@ def clear_chord(self): def chord_end(self): """Actions when chord is parsed.""" + self.current_chord_or_note.append('end') self.action_onnext = [] def new_rest(self, rest): @@ -547,19 +777,24 @@ def new_rest(self, rest): rtype = rest.token dur = rest.duration if rtype == 'r': - self.current_note = xml_objs.BarRest(dur, self.voice) + self.prev_note = self.current_note + self.current_note = xml_objs.BarRest(dur, self.voice, self.voice_name) elif rtype == 'R': - self.current_note = xml_objs.BarRest(dur, self.voice, show_type=False) - elif rtype == 's' or rtype == '\\skip': - self.current_note = xml_objs.BarRest(dur, self.voice, skip=True) + self.prev_note = self.current_note + self.current_note = xml_objs.BarRest(dur, self.voice, self.voice_name, show_type=False) + elif rtype == 's' or rtype == '\\skip' or rtype == '_': + self.prev_note = self.current_note + self.current_note = xml_objs.BarRest(dur, self.voice, self.voice_name, skip=True) self.check_current_note(rest=True) def note2rest(self): """Note used as rest position transformed to rest.""" dur = self.current_note.duration - voice = self.current_note.voice + voc = self.current_note.voice + voc_name = self.current_note.voice_name pos = [self.current_note.base_note, self.current_note.octave] - self.current_note = xml_objs.BarRest(dur, voice, pos=pos) + self.prev_note = self.current_note + self.current_note = xml_objs.BarRest(dur, voice=voc, voice_name=voc_name, pos=pos) self.check_duration(rest=True) self.bar.obj_list.pop() self.bar.add(self.current_note) @@ -568,11 +803,12 @@ def scale_rest(self, multp): """ create multiple whole bar rests """ dur = self.current_note.duration voc = self.current_note.voice + voc_name = self.current_note.voice_name st = self.current_note.show_type sk = self.current_note.skip for i in range(1, int(multp)): self.new_bar() - rest_copy = xml_objs.BarRest(dur, voice=voc, show_type=st, skip=sk) + rest_copy = xml_objs.BarRest(dur, voice=voc, voice_name=voc_name, show_type=st, skip=sk) self.add_to_bar(rest_copy) def change_to_tuplet(self, tfraction, ttype, nr, length=None): @@ -617,15 +853,42 @@ def calc_tupl_den(self, tfraction, length): fraction and duration of tuplet.""" return tfraction[1] / length - def tie_to_next(self): + def tie_to_next(self, line): + """ Begin a tie on an entire chord or an individual note. """ tie_type = 'start' - self.tied = True - self.current_note.set_tie(tie_type) - - def set_slur(self, nr, slur_type): + self.tie_line = line + if self.current_chord_or_note: + if self.current_chord_or_note[-1] == 'end': + # Apply tie to entire chord if it comes after + for note in self.current_chord_or_note: + if note != 'end': + note.set_tie(tie_type, line) + self.tie_list.append(note) + else: + # Apply tie to only most recent note + note = self.current_chord_or_note[-1] + note.set_tie(tie_type, line) + self.tie_list.append(note) + else: + eprint("Warning: No proper note/chord found for tie!") + note = self.current_note + note.set_tie(tie_type, line) + self.tie_list.append(note) + + def check_for_tie_end(self, name, octave, alter): + """ If there is an unfinished tie which matches the input note information, then return its list index else return None. """ + if self.tie_list: + count = 0 + for tie in self.tie_list: + if tie.base_note == name and tie.octave == octave and tie.alter == alter: + return count + count += 1 + return None + + def set_slur(self, nr, slur_type, phrasing=False, line='solid', grace=False): """ Set the slur start or stop for the current note. """ - self.current_note.set_slur(nr, slur_type) + self.current_note.set_slur(nr, slur_type, phrasing, line, grace) def new_articulation(self, art_token): """ @@ -696,15 +959,19 @@ def end_gliss(self, note, line): n = 1 note.set_gliss(line, endtype="stop", nr=n) - def set_tremolo(self, trem_type='single', duration=0, repeats=0): - if self.current_note.tremolo[1]: #tremolo already set + def set_breathe(self): + self.current_note.add_articulation('breath-mark') + + def set_tremolo(self, trem_type='single', duration=0, repeats=0, note_count=1): + if self.current_note.tremolo[1]: # tremolo already set self.current_note.set_tremolo(trem_type) else: if repeats: duration = int(self.dur_token) - bs, durtype = calc_trem_dur(repeats, self.current_note.duration, duration) + bs, durtype, dot_num = calc_trem_dur(repeats, self.current_note.duration, duration, note_count) self.current_note.duration = bs self.current_note.type = durtype + self.current_note.dot = dot_num elif not duration: duration = self.prev_tremolo else: @@ -755,9 +1022,7 @@ def new_tempo(self, unit, dur_tokens, tempo, string): tempo = xml_objs.BarAttr() unittype = durval2type(unit) if unit else '' tempo.set_tempo(unit, unittype, beats, dots, text) - if self.bar is None: - self.new_bar() - self.bar.add(tempo) + self.add_to_bar(tempo) def set_by_property(self, prprty, value, group=False): """Generic setter for different properties.""" @@ -804,7 +1069,10 @@ def set_partmidi(self, midi): def new_lyric_nr(self, num): self.lyric_nr = num - def new_lyrics_text(self, txt): + def new_lyrics_text(self, txt, italic=False): + if not txt: + txt = "ERROR" + eprint("Warning: Lyric text not readable, marked with ERROR!") if self.lyric: if self.lyric_syll: if self.lyric[1] in ['begin', 'middle']: @@ -815,6 +1083,8 @@ def new_lyrics_text(self, txt): self.lyric = [txt, 'single', self.lyric_nr] else: self.lyric = [txt, 'single', self.lyric_nr] + if italic: + self.lyric.append('italic') self.insert_into.barlist.append(self.lyric) self.lyric_syll = False @@ -826,8 +1096,12 @@ def new_lyrics_item(self, item): self.lyric_syll = True elif item == '__': self.lyric.append("extend") - elif item == '\\skip': + elif item == '\\skip' or item == '_': self.insert_into.barlist.append("skip") + elif isinstance(item, list) and item[-1] == "command": + self.insert_into.barlist.append(item) # Item should be of form ["commandName", args, "command"] + else: + eprint("Warning: Lyric item", str(item), "not implemented!") def duration_from_tokens(self, tokens): """Calculate dots and multibar rests from tokens.""" @@ -846,21 +1120,12 @@ def check_divs(self): scaling = self.current_note.duration[1] divs = self.divisions tupl = self.current_note.tuplet - if not tupl: - a = 4 - if base: - b = 1/base - else: - b = 1 - print("Warning problem checking duration!") + a = 4 + if base: + b = 1/base else: - num = 1 - den = 1 - for t in tupl: - num *= t.fraction[0] - den *= t.fraction[1] - a = 4*den - b = (1/base)*num + b = 1 + eprint("Warning problem checking duration!") c = a * divs * scaling predur, mod = divmod(c, b) if mod > 0: @@ -868,7 +1133,6 @@ def check_divs(self): self.divisions = divs*mult - ## # Translation functions ## @@ -877,6 +1141,7 @@ def getNoteName(index): noteNames = ['C', 'D', 'E', 'F', 'G', 'A', 'B'] return noteNames[index] + def get_xml_alter(alter): """ Convert alter to the specified format, i e int if it's int and float otherwise. @@ -887,6 +1152,7 @@ def get_xml_alter(alter): else: return float(alter) + def durval2type(durval): """Convert LilyPond duration to MusicXML duration type.""" xml_types = [ @@ -894,28 +1160,33 @@ def durval2type(durval): "half", "quarter", "eighth", "16th", "32nd", "64th", "128th", "256th", "512th", "1024th", "2048th" - ] # Note: 2048 is supported by ly but not by MusicXML! + ] # Note: 2048 is supported by ly but not by MusicXML! try: type_index = ly.duration.durations.index(str(durval)) except ValueError: type_index = 5 return xml_types[type_index] + def get_fifths(key, mode): + """Returns current number of sharps/flats in the key (negative = flats, positive = sharps)""" fifths = 0 sharpkeys = ['c', 'g', 'd', 'a', 'e', 'b', 'fis', 'cis', 'gis', - 'dis', 'ais', 'eis', 'bis', 'fisis', 'cisis'] + 'dis', 'ais', 'eis', 'bis', 'fisis', 'cisis'] flatkeys = ['c', 'f', 'bes', 'es', 'as', 'des', 'ges', 'ces', 'fes', - 'beses', 'eses', 'ases'] + 'beses', 'eses', 'ases'] if key in sharpkeys: fifths = sharpkeys.index(key) elif key in flatkeys: fifths = -flatkeys.index(key) - if mode=='minor': + if mode == 'minor': return fifths-3 - elif mode=='major': + elif mode == 'dorian': + return fifths-2 + elif mode == 'major': return fifths + def clefname2clef(clefname): """ To add a clef look up the clef name in LilyPond @@ -923,38 +1194,38 @@ def clefname2clef(clefname): Add it to the python dictionary below. """ clef_dict = { - "treble": ('G', 2, 0), - "violin": ('G', 2, 0), - "G": ('G', 2, 0), - "bass": ('F', 4, 0), - "F": ('F', 4, 0), - "alto": ('C', 3, 0), - "C": ('C', 3, 0), - "tenor": ('C', 4, 0), - "treble_8": ('G', 2, -1), - "treble_15": ('G', 2, -2), - "bass_8": ('F', 4, -1), - "bass_15": ('F', 4, -2), - "treble^8": ('G', 2, 1), - "treble^15": ('G', 2, 2), - "bass^8": ('F', 4, 1), - "bass^15": ('F', 4, 2), - "percussion": ('percussion', 0, 0), - "tab": ('TAB', 5, 0), - "soprano": ('C', 1, 0), - "mezzosoprano": ('C', 2, 0), - "baritone": ('C', 5, 0), - "varbaritone": ('F', 3, 0), - "baritonevarF": ('F', 3, 0), - "french": ('G', 1, 0), - "subbass": ('F', 5, 0), - # From here on the clefs will end up with wrong symbols - "GG": ('G', 2, -1), - "tenorG": ('G', 2, -1), - "varC": ('C', 3, 0), - "altovarC": ('C', 3, 0), - "tenorvarC": ('C', 4, 0), - "baritonevarC": ('C', 5, 0), + "treble": ('G', 2, 0), + "violin": ('G', 2, 0), + "G": ('G', 2, 0), + "bass": ('F', 4, 0), + "F": ('F', 4, 0), + "alto": ('C', 3, 0), + "C": ('C', 3, 0), + "tenor": ('C', 4, 0), + "treble_8": ('G', 2, -1), + "treble_15": ('G', 2, -2), + "bass_8": ('F', 4, -1), + "bass_15": ('F', 4, -2), + "treble^8": ('G', 2, 1), + "treble^15": ('G', 2, 2), + "bass^8": ('F', 4, 1), + "bass^15": ('F', 4, 2), + "percussion": ('percussion', 0, 0), + "tab": ('TAB', 5, 0), + "soprano": ('C', 1, 0), + "mezzosoprano": ('C', 2, 0), + "baritone": ('C', 5, 0), + "varbaritone": ('F', 3, 0), + "baritonevarF": ('F', 3, 0), + "french": ('G', 1, 0), + "subbass": ('F', 5, 0), + # From here on the clefs will end up with wrong symbols + "GG": ('G', 2, -1), + "tenorG": ('G', 2, -1), + "varC": ('C', 3, 0), + "altovarC": ('C', 3, 0), + "tenorvarC": ('C', 4, 0), + "baritonevarC": ('C', 5, 0), } try: @@ -963,14 +1234,17 @@ def clefname2clef(clefname): clef = 0 return clef + def get_mult(num, den): simple = Fraction(num, den) return simple.denominator + def get_voice(c): voices = ["voiceOne", "voiceTwo", "voiceThree", "voiceFour"] return voices.index(c)+1 + def artic_token2xml_name(art_token): """ From Articulations in ly.music.items. @@ -981,9 +1255,9 @@ def artic_token2xml_name(art_token): Add it to the python dictionary below. """ artic_dict = { - ".": "staccato", "-": "tenuto", ">": "accent", - "_": "detached-legato", "!": "staccatissimo", - "\\staccatissimo": "staccatissimo" + ".": "staccato", "-": "tenuto", ">": "accent", + "_": "detached-legato", "!": "staccatissimo", + "\\staccatissimo": "staccatissimo" } ornaments = ['\\trill', '\\prall', '\\mordent', '\\turn'] others = ['\\fermata'] @@ -997,32 +1271,67 @@ def artic_token2xml_name(art_token): else: return False -def calc_trem_dur(repeats, base_scaling, duration): - """ Calculate tremolo duration from number of - repeats and initial duration. """ + +def length_to_duration(length): + """ + Convert a note length fraction (such as 3/4) into a lilypond duration (str) and a number of dots + lilypond duration could be: '\\maxima' (8), '\\longa' (4), '\\breve' (2), or '1', '2', '4', ..., '2048' for fractions (1/#) + + Ex: length of 3/4 -> ('2', 1) because 3/4 is a dotted half note + length of 7/2 -> ('\\breve', 2) because 7/2 is a double dotted breve + """ + durations = [ + 8, 4, 2, 1, Fraction(1, 2), Fraction(1, 4), Fraction(1, 8), Fraction(1, 16), Fraction(1, 32), + Fraction(1, 64), Fraction(1, 128), Fraction(1, 256), Fraction(1, 512), Fraction(1, 1024), Fraction(1, 2048) + ] # Note: 2048 is supported by ly but not by MusicXML! + index = durations.index(Fraction(1, 2048)) + dots = dur = add = 0 + # Calculate the index of the first duration shorter than length + for i in range(len(durations)): + if length >= durations[i]: + if length < 16: # Prevents infinite recursion + dur = durations[i] + dot_length = dur * Fraction(1, 2) + # Calculate number of needed dots + while(length > dur): # Limit of dur is durations[i] * 2 + dots += 1 + dur += dot_length + dot_length *= Fraction(1, 2) + else: + eprint("Warning: Length of note is too long!") + index = i + break + if index == durations.index(Fraction(1, 2048)): + eprint("Warning: Length of note is too short for MusicXML!") + return ly.duration.durations[index], dots + + +def calc_trem_dur(repeats, base_scaling, duration, note_count): + """ + Calculate tremolo duration, note type, and number of dots from: + number of repeats, initial duration, and number of notes in the tremolo. + """ base = base_scaling[0] scale = base_scaling[1] + duration, dots = length_to_duration(base * scale * repeats * note_count) new_base = base * repeats - if repeats > duration: - import ly.duration - trem_length = ly.duration.tostring(int((repeats // duration) * -0.5)) - else: - trem_length = str(duration // repeats) - new_type = xml_objs.durval2type(trem_length) - return (new_base, scale), new_type + new_type = durval2type(duration) + return (new_base, scale), new_type, dots + def get_line_style(style): style_dict = { - "dashed-line": "dashed", - "dotted-line": "dotted", - "trill": "wavy", - "zigzag": "wavy" + "dashed-line": "dashed", + "dotted-line": "dotted", + "trill": "wavy", + "zigzag": "wavy" } try: return style_dict[style] except KeyError: return False + def get_group_symbol(lily_sys_start): symbol_dict = { "SystemStartBrace": "brace", diff --git a/ly/musicxml/lymus2musxml.py b/ly/musicxml/lymus2musxml.py index 13dfb733..e579afae 100644 --- a/ly/musicxml/lymus2musxml.py +++ b/ly/musicxml/lymus2musxml.py @@ -29,14 +29,17 @@ from __future__ import unicode_literals from __future__ import print_function +import sys import ly.document import ly.music +from ly.pitch.transpose import Transposer +from fractions import Fraction from . import create_musicxml from . import ly2xml_mediator from . import xml_objs -#excluded from parsing +# excluded from parsing excl_list = ['Version', 'Midi', 'Layout'] @@ -46,14 +49,23 @@ pno_contexts = ['PianoStaff', 'GrandStaff'] staff_contexts = ['Staff', 'RhythmicStaff', 'TabStaff', - 'DrumStaff', 'VaticanaStaff', 'MensuralStaff'] + 'DrumStaff', 'VaticanaStaff', 'MensuralStaff'] part_contexts = pno_contexts + staff_contexts +def eprint(*args, **kwargs): + """ + From https://stackoverflow.com/questions/5574702/how-to-print-to-stderr-in-python + Prints to stderr + """ + print(*args, file=sys.stderr, **kwargs) + + class End(): """ Extra class that gives information about the end of Container elements in the node list. """ + def __init__(self, node): self.node = node @@ -71,12 +83,30 @@ def __init__(self): self.tuplet = [] self.scale = '' self.grace_seq = False + self.grace_slash = False + self.grace_slur = False + self.prev_was_grace_slur = False self.trem_rep = 0 + self.trem_note_count = 0 + self.trem_is_started = False self.piano_staff = 0 + self.staves_in_piano_staff = False + self.staff = 0 + self.voice_count = 0 self.numericTime = False self.voice_sep = False + self.voice_sep_start_time_sig = Fraction(1, 1) + self.voice_sep_start_total_time = 0 + self.voice_sep_start_time_since_bar = 0 + self.voice_sep_start_voice_name = None + self.voice_sep_length = 0 + self.voice_sep_first_meas = False self.sims_and_seqs = [] + self.override_key = '' + self.override_key_word_count = 0 self.override_dict = {} + self.revert_key = '' + self.revert_key_word_count = 0 self.ottava = False self.with_contxt = None self.schm_assignm = None @@ -86,16 +116,40 @@ def __init__(self): self.unset_tuplspan = False self.alt_mode = None self.rel_pitch_isset = False + self.tie_types = [] + self.phr_slur_types = [] + self.slur_types = [] self.slurcount = 0 self.slurnr = 0 self.phrslurnr = 0 + self.auto_beam = True + self.beam = None # Values include None, "Normal", and "Manual" (for beams made with []) + self.prev_beam_type = None + self.shortest_length_in_beam = Fraction(1, 4) + self.beam_ends = [] + self.beam_exceptions = [] + self.base_moment = Fraction(1, 4) + self.transposer = None + self.volta_counts = [] + self.alt_endings = [] + # Variables to keep track of place in music, and place of \bar barlines and chord symbols + self.time_sig = Fraction(1, 1) + self.time_sig_locations = {} + self.partial = 0 + self.first_meas = False + self.prev_note_dur = 0 + self.total_time = 0 + self.time_since_bar = 0 + self.barline_locations = {} + self.chord_locations = {} + self.prev_len_before_tuplet = 0 def parse_text(self, ly_text, filename=None): """Parse the LilyPond source specified as text. - + If you specify a filename, it can be used to resolve \\include commands correctly. - + """ doc = ly.document.Document(ly_text) doc.filename = filename @@ -103,11 +157,11 @@ def parse_text(self, ly_text, filename=None): def parse_document(self, ly_doc, relative_first_pitch_absolute=False): """Parse the LilyPond source specified as a ly.document document. - + If relative_first_pitch_absolute is set to True, the first pitch in a \relative expression without startpitch is considered to be absolute (LilyPond 2.18+ behaviour). - + """ # The document is copied and the copy is converted to absolute mode to # facilitate the export. The original document is unchanged. @@ -120,7 +174,7 @@ def parse_document(self, ly_doc, relative_first_pitch_absolute=False): def parse_tree(self, mustree): """Parse the LilyPond source as a ly.music node tree.""" - # print(mustree.dump()) + # eprint(mustree.dump()) header_nodes = self.iter_header(mustree) if header_nodes: self.parse_nodes(header_nodes) @@ -129,26 +183,221 @@ def parse_tree(self, mustree): mus_nodes = self.iter_score(score, mustree) else: mus_nodes = self.find_score_sub(mustree) - self.mediator.new_section("fallback") #fallback/default section + self.generate_location_dicts(mus_nodes) + self.mediator.new_section("fallback") # fallback/default section + if score: + mus_nodes = self.iter_score(score, mustree) + else: + mus_nodes = self.find_score_sub(mustree) self.parse_nodes(mus_nodes) + def generate_location_dicts(self, nodes): + r""" + Work through all nodes in order to preemptively formulate dictionaries containing + positions of `\bar`s and time signature changes (self.barline_locations and self.time_sig_locations) + They will look something like {3/4: "||"} for `\bar`s + and {3/2: {'numerator': 4, 'denominator': 4, 'length': 1, 'numeric': {1: True, 2: False}}} for time sig changes + (The 'numeric' key contains a dictionary which indicates whether a staff number's time sig should be numeric or not) + (The last seen '\bar' type or time signature in a given location will be prioritized over the others) + + Note: this function should be updated with any elements which affect the passage of time in the music + """ + if nodes: + class_name = '' + dur = (0, 1) + total_time = 0 + chord_mode = False + tuplet = [] + trem_rep = 0 + grace_seq = False + slashed_grace = False + voice_sep = False + voice_sep_start_total_time = 0 + voice_sep_length = 0 + prev = False + numeric_time = False + piano_staff = False + staves_in_piano_staff = False + staff = 0 + note_locations = [[(0, False)]] + for m in nodes: + class_name = m.__class__.__name__ + # Notes should advance total_time by their length (but not chord or grace notes) + if (isinstance(m, ly.music.items.Durable) and not isinstance(m, ly.music.items.LyricText) + and not chord_mode and not isinstance(m.parent(), ly.music.items.Chord)): + if not grace_seq: + dur = m.duration + # In the instance of a tuplet or tremolo repeat, change the duration + if len(tuplet) != 0: + dur = (Fraction((dur[0] * tuplet[-1][1]) / tuplet[-1][0]), dur[1]) + if trem_rep != 0: + dur = (dur[0] * trem_rep, dur[1]) + total_time += dur[0] * dur[1] + # Log the end time of this Durable and whether it is a Skip + if isinstance(m, ly.music.items.Skip): + note_locations[-1].append((total_time, True)) + else: + note_locations[-1].append((total_time, False)) + # End slashed grace note + # TODO: Handle multiple/chord \slashedGrace notes + elif slashed_grace: + grace_seq = False + slashed_grace = False + # Indicate chord mode (ignore notes in this mode) + elif isinstance(m, (ly.music.items.ChordMode)): + chord_mode = True + # Store scalar info (things like \times or \tuplet) + elif isinstance(m, ly.music.items.Scaler): + if m.token == '\\tuplet': + tuplet.append((m.numerator, m.denominator)) + else: # \scaleDurations or \times + tuplet.append((m.denominator, m.numerator)) + # Store the number of repeats in a tremolo + elif class_name == 'Repeat' and m.specifier() == 'tremolo': + trem_rep = m.repeat_count() + # Indicate grace note(s) + elif class_name == 'Grace': + grace_seq = True + # Store info at beginning of voice separator section + elif class_name == 'MusicList' and m.token == '<<' and self.look_ahead(m, ly.music.items.VoiceSeparator): + voice_sep = True + voice_sep_start_total_time = total_time + # Reset time at a voice separator (only do so for final \\ if multiple in a row) + elif class_name == 'VoiceSeparator' and voice_sep and self.get_next_node(m) and not self.get_next_node(m).token == r'\\': + note_locations.append([(voice_sep_start_total_time, False)]) + total_time = total_time - voice_sep_length + # A string could be a \bar type, so record its position and type if so + elif class_name == 'String': + prev = self.get_previous_node(m) + if prev and prev.token == '\\bar': + self.barline_locations[total_time] = m.value() + # Handle numeric vs default time sig commands + elif class_name == 'Command': + if m.token == '\\numericTimeSignature': + numeric_time = True + elif m.token == '\\defaultTimeSignature': + numeric_time = False + if total_time in self.time_sig_locations: + self.time_sig_locations[total_time]['numeric'][staff] = numeric_time + # Handle user commands (like slashedGrace notes) + elif class_name == 'UserCommand': + if m.token == '\\slashedGrace': + grace_seq = True + slashed_grace = True + # Store the location of time signature changes + elif class_name == 'TimeSignature': + if total_time not in self.time_sig_locations: + self.time_sig_locations[total_time] = {'numerator': m.numerator(), 'denominator': m.fraction().denominator, + 'length': m.measure_length(), 'numeric': {staff: numeric_time}, + 'setMeasLen': False} + else: + self.time_sig_locations[total_time]['numerator'] = m.numerator() + self.time_sig_locations[total_time]['denominator'] = m.fraction().denominator + self.time_sig_locations[total_time]['length'] = m.measure_length() + self.time_sig_locations[total_time]['setMeasLen'] = False + # Store the location of set measure length changes (same location as time sig changes) + elif class_name == 'Set' and m.property() == 'measureLength': + num = m.value().get_list_ints()[0] + den = m.value().get_list_ints()[1] + if total_time not in self.time_sig_locations: + self.time_sig_locations[total_time] = {'numerator': num, 'denominator': den, + 'length': Fraction(num, den), 'numeric': {staff: numeric_time}, + 'setMeasLen': True} + else: + self.time_sig_locations[total_time]['numerator'] = num + self.time_sig_locations[total_time]['denominator'] = den + self.time_sig_locations[total_time]['length'] = Fraction(num, den) + self.time_sig_locations[total_time]['setMeasLen'] = True + # Handle piano staff beginning + elif class_name == 'Context' and m.context() in pno_contexts: + piano_staff = True + # Reset total_time to 0 at new voices and staves + elif class_name == 'Context' and (m.context() == 'Voice' or m.context() in staff_contexts): + note_locations.append([(0, False)]) + total_time = 0 + if m.context() in staff_contexts and piano_staff: + staves_in_piano_staff = True + # With staves or new voices without staves in a piano staff, indicate the start of a new staff + if m.context() in staff_contexts or (m.context() == 'Voice' and piano_staff and not staves_in_piano_staff): + staff += 1 + numeric_time = False + # End currently running nodes + elif class_name == 'End': + if m.node.token == '{': + # End chord mode + if m.node.parent().token in ['\\chordmode', '\\chords']: + chord_mode = False + # End voice separator music list (record its length) + elif voice_sep: + voice_sep_length = total_time - voice_sep_start_total_time + # End Voice separator section + elif m.node.token == '<<': + voice_sep = False + voice_sep_length = 0 + # End scaler (\tuplet, \times, etc.) + elif isinstance(m.node, ly.music.items.Scaler): + tuplet.pop() + # End tremolo repeat + elif m.node.token == '\\repeat' and m.node.specifier() == 'tremolo': + trem_rep = 0 + # End grace note(s) + elif isinstance(m.node, ly.music.items.Grace): + grace_seq = False + # End piano staff + elif isinstance(m.node, ly.music.items.Context) and m.node.context() in pno_contexts: + piano_staff = False + staves_in_piano_staff = False + # Find any (\bar) barlines which occur during some note + barline_locations_to_skip = [] + barline_conflict = False + # Iterate through all found barlines + for bar_loc in self.barline_locations.keys(): + # Iterate through sections of note locations + for part in note_locations: + # Iterate through the note end locations themselves + for note_num in range(len(part)): + note_loc = part[note_num][0] + skip = part[note_num][1] + # Note ends at barline is ok + if note_loc == bar_loc: + break + # First note to end after barline + elif note_loc > bar_loc: + # If the note is the first in the part or is a skip then ok + if note_num == 0 or skip: + break + # Barline occurs during some note + else: + eprint("Warning: Barline found during note, skipping barline!") + barline_locations_to_skip.append(bar_loc) + barline_conflict = True + break + # If current barline location has a conflict, go on to check next barline location + if barline_conflict: + barline_conflict = False + break + # Clear the conflicting barlines + for loc in barline_locations_to_skip: + if loc in self.barline_locations: + self.barline_locations.pop(loc) + def parse_nodes(self, nodes): """Work through all nodes by calling the function with the same name as the nodes class.""" if nodes: for m in nodes: - # print(m) - func_name = m.__class__.__name__ #get instance name + # eprint(m) + func_name = m.__class__.__name__ # get instance name if func_name not in excl_list: try: func_call = getattr(self, func_name) func_call(m) except AttributeError as ae: - print("Warning:", func_name, "not implemented!") - print(ae) + eprint("Warning:", func_name, "not implemented!") + eprint(ae) pass else: - print("Warning! Couldn't parse source!") + eprint("Warning! Couldn't parse source!") def musicxml(self, prettyprint=True): self.mediator.check_score() @@ -190,17 +439,42 @@ def MusicList(self, musicList): if self.look_ahead(musicList, ly.music.items.VoiceSeparator): self.mediator.new_snippet('sim-snip') self.voice_sep = True + # Store information at beginning of voice separator + self.voice_sep_start_time_sig = self.time_sig + self.voice_sep_start_total_time = self.total_time + self.voice_sep_start_time_since_bar = self.time_since_bar + self.voice_sep_first_meas = self.first_meas + self.voice_sep_start_voice_name = self.mediator.voice_name + self.mediator.store_voicenr = self.mediator.voice + self.mediator.voice_name = '1' else: self.mediator.new_section('simultan') self.sims_and_seqs.append('sim') elif musicList.token == '{': + self.tie_types.append('solid') + self.phr_slur_types.append('solid') + self.slur_types.append('solid') self.sims_and_seqs.append('seq') + if self.is_volta_ending(musicList): + end = self.alt_endings[0] + self.mediator.new_ending(end[0], end[1], 'start', self.staff) + + def is_volta_ending(self, music_list): + """ Returns whether a MusicList in an alternate ending of a volta repeat (not in lyrics or chord symbols) """ + return (music_list.parent().parent().token == '\\alternative' + and music_list.parent().parent().parent().specifier() == 'volta' + and self.alt_mode not in ['lyric', 'chord']) def Chord(self, chord): self.mediator.clear_chord() def Q(self, q): + """ q, A copy of the previous chord's pitches (has its own length) """ + self.adjust_tuplet_length(q) self.mediator.copy_prev_chord(q.duration) + self.check_note(q) + self.update_beams(q) + self.update_time_and_check(q) def Context(self, context): r""" \context """ @@ -222,29 +496,65 @@ def check_context(self, context, context_id=None, token=""): self.mediator.new_group() elif context in staff_contexts: if self.piano_staff: - if self.piano_staff > 1: - self.mediator.set_voicenr(nr=self.piano_staff+3) + self.mediator.set_voicenr(nr=1) self.mediator.new_section('piano-staff'+str(self.piano_staff)) self.mediator.set_staffnr(self.piano_staff) self.piano_staff += 1 + self.staves_in_piano_staff = True else: if token != '\\context' or self.mediator.part_not_empty(): self.mediator.new_part(context_id) + self.mediator.set_voicenr(nr=1) self.mediator.add_staff_id(context_id) + self.total_time = 0 + self.time_since_bar = 0 + self.first_meas = True + self.staff += 1 + self.update_time_sig() elif context == 'Voice': + # Treat voices without staves in a piano staff as staves + if self.piano_staff and not self.staves_in_piano_staff: + self.mediator.set_voicenr(nr=1) + self.mediator.new_section('piano-staff'+str(self.piano_staff)) + self.mediator.set_staffnr(self.piano_staff) + self.piano_staff += 1 + self.staff += 1 + self.total_time = 0 + self.time_since_bar = 0 self.sims_and_seqs.append('voice') if context_id: + self.mediator.voice_name = context_id self.mediator.new_section(context_id) else: + self.mediator.voice_name = None self.mediator.new_section('voice') + self.first_meas = True + self.voice_count += 1 + self.update_time_sig() elif context == 'Devnull': self.mediator.new_section('devnull', True) + elif context == 'Lyrics': # The way lyrics are implemented, they don't need a new section here (prevents irrelevant warning) + if self.alt_mode == 'lyric': + eprint("Warning: Nested lyric sections are not supported!") # TODO: Support nested lyrics + elif context == 'ChordNames': + pass # Without using ChordMode to write actual chords, ChordNames doesn't need a new section else: - print("Context not implemented:", context) + eprint("Context not implemented:", context) def VoiceSeparator(self, voice_sep): - self.mediator.new_snippet('sim') - self.mediator.set_voicenr(add=True) + # Increment voice name no matter what (for lyric assignment) + self.mediator.voice_name = str(int(self.mediator.voice_name) + 1) + # Prevent << \\ {... from starting on a different voice + if self.mediator.voice_sep_sections > 0: + self.mediator.new_snippet('sim') + self.mediator.set_voicenr(add=True) + if self.voice_sep: + # Reset time information after last \\ (voice separator) + if self.get_next_node(voice_sep).token != r"\\": # Last \\ + self.time_sig = self.voice_sep_start_time_sig + self.total_time = self.total_time - self.voice_sep_length + self.time_since_bar = self.voice_sep_start_time_since_bar + self.first_meas = self.voice_sep_first_meas def Change(self, change): r""" A \change music expression. Changes the staff number. """ @@ -253,66 +563,356 @@ def Change(self, change): def PipeSymbol(self, barcheck): """ PipeSymbol = | """ - self.mediator.new_bar() + pass # Barlines are automatically generated at correct locations by check_for_barline() def Clef(self, clef): r""" Clef \clef""" self.mediator.new_clef(clef.specifier()) def KeySignature(self, key): - self.mediator.new_key(key.pitch().output(), key.mode()) + r""" + A new key signature (\key and \major, \minor, etc.) + + The key's pitch is transposed if the key is after a \transpose + using a copy of the pitch (to prevent a reused key from being modified multiple times) + """ + pitch_copy = key.pitch().copy() + if self.transposer is not None: + self.transposer.transpose(pitch_copy) + self.mediator.new_key(pitch_copy.output(), key.mode()) def Relative(self, relative): r"""A \relative music expression.""" self.relative = True - def Note(self, note): - """ notename, e.g. c, cis, a bes ... """ - #print(note.token) - if note.length(): - if self.relative and not self.rel_pitch_isset: - self.mediator.new_note(note, False) - self.mediator.set_relative(note) - self.rel_pitch_isset = True + def Transpose(self, transpose): + r""" A \transpose music expression. """ + self.transposer = Transposer(transpose[0].pitch, transpose[1].pitch) + + def transpose_note(self, note): + """ If music should be transposed, adjust note pitch accordingly, key changes handled in KeySignature() """ + if not isinstance(note.parent(), ly.music.items.KeySignature) and self.transposer is not None: + self.transposer.transpose(note.pitch) + + def ChordSpecifier(self, specifier): + """ + A ChordSpecifier occurs with : or / in ChordMode + + Function modifies current chord symbol in self.chord_locations + to include additional text and bass note + """ + for item in specifier: + if isinstance(item, ly.music.items.Note): + self.chord_locations[self.total_time - self.prev_note_dur]["bass"] = item.token.capitalize()[0] + self.chord_locations[self.total_time - self.prev_note_dur]["bass-alter"] = int(item.pitch.alter * 2) + elif item.token != ":" and item.token != "/": + self.chord_locations[self.total_time - self.prev_note_dur]["text"] += item.token + + def ChordItem(self, item): + """A ChordItem :, /, m, maj, etc.""" + pass + + def check_for_chord(self, note): + """ + Checks the current note to see if any chord symbols are needed above + Adds one to current note if needed + Based on whether a chord symbol was defined during the duration of the note in the music + Removes any used chord symbols from dictionary of chord symbols to ensure the symbol is only placed once + """ + pos_to_pop = [] # Stores any chord symbol positions used during this note + if isinstance(note.parent(), ly.music.items.Chord): + length = note.parent().duration[0] + else: + length = note.length() + for pos, chord_dict in self.chord_locations.items(): + # Check if the chord symbol at pos, is at the beginning of the note + if pos == self.total_time - length: + pos_to_pop.append(pos) + self.mediator.current_note.add_harmony(chord_dict["root"], chord_dict["root-alter"], + chord_dict["bass"], chord_dict["bass-alter"], + chord_dict["text"]) + # Check if the chord symbol at pos, is anytime else during the note + # (adds offset equal to time from beginning of note to pos) + elif pos > self.total_time - length and pos < self.total_time: + pos_to_pop.append(pos) + self.mediator.current_note.add_harmony(chord_dict["root"], chord_dict["root-alter"], + chord_dict["bass"], chord_dict["bass-alter"], + chord_dict["text"], pos - (self.total_time - length)) + # Remove any used chord symbols from self.chord_locations + for pos in pos_to_pop: + self.chord_locations.pop(pos) + + def check_for_barline(self): + """ + Checks at the current location in music to see if a barline is needed + Creates a barline if needed + """ + # Create a \bar if found in same location in earlier part + if self.total_time in self.barline_locations and self.time_since_bar != 0: + self.mediator.create_barline(self.barline_locations[self.total_time]) + # Reset time since bar if the \bar coincides with actual end of measure + if self.first_meas and self.partial != 0: + if self.time_since_bar == self.partial: + self.time_since_bar = 0 + self.first_meas = False else: - self.mediator.new_note(note, self.relative) - self.check_note(note) + if self.time_since_bar == self.time_sig: + self.time_since_bar = 0 + self.end_beam(current=True) + return True + # Create regular measure if there has been enough time since prev bar + if self.first_meas and self.partial != 0: + if self.time_since_bar == self.partial: + self.time_since_bar = 0 + self.mediator.new_bar() + self.first_meas = False + self.end_beam(current=True) + return True + else: + if self.time_since_bar == self.time_sig: + self.time_since_bar = 0 + self.mediator.new_bar() + self.end_beam(current=True) + return True + return False + + def update_time_sig(self): + """ If time sig changes here (according to self.time_sig_locations), update current time sig accordingly """ + if self.total_time in self.time_sig_locations: + ts = self.time_sig_locations[self.total_time] + self.time_sig = ts['length'] + # Only change beaming and place a time sig if this was + # an actual `\time #/#`, not a `\set Timing.measureLength...` + if not ts['setMeasLen']: + self.get_beat_structure_from_time_sig(ts['numerator'], ts['denominator']) + numeric = False + if self.staff in ts['numeric']: + numeric = ts['numeric'][self.staff] + else: + numeric = self.numericTime + self.mediator.new_time(ts['numerator'], ts['denominator'], numeric) + + def update_time_and_check(self, mus_obj, skip_len=0, final_skip=False): + """ + Takes a note, rest, skip, etc. and updates total_time and time_since_bar accordingly + Also records chord symbol locations when in 'chord' alt_mode + After updating times, checks for barlines, time sigs, and/or chord symbols when applicable + """ + if not self.grace_seq: + length = 0 + # Skip lengths are given manually, otherwise use the object's length + if skip_len: + length = skip_len + else: + length = mus_obj.length() + if length: + self.prev_note_dur = length + # When in chord alt_mode, record chord symb locations + if self.alt_mode == "chord": + if not isinstance(mus_obj, ly.music.items.Skip): + self.chord_locations[self.total_time] = {"root": mus_obj.token.capitalize()[0], + "root-alter": int(mus_obj.pitch.alter * 2), + "bass": False, "bass-alter": 0, "text": ""} + self.total_time += length + # When not in chord alt_mode, treat notes as notes + else: + self.time_since_bar += length + self.total_time += length + # Check for bar/time sigs unless final note in voice separator (in which case, wait until after) + if not self.voice_sep or (skip_len and not final_skip): + self.check_for_barline() + self.update_time_sig() + else: + node = mus_obj + while True: + if not self.get_next_node(node): + break + elif isinstance(self.get_next_node(node), ly.music.items.Durable): + self.check_for_barline() + self.update_time_sig() + break + else: + node = self.get_next_node(node) + if not skip_len: + self.check_for_chord(mus_obj) + # First note of chord is used to update time and check for chord symbols, + # but must wait for end of chord to check for barlines/time sigs (see End()) + elif mus_obj is not None and isinstance(mus_obj.parent(), ly.music.items.Chord): + self.prev_note_dur = mus_obj.parent().duration[0] + self.time_since_bar += mus_obj.parent().duration[0] + self.total_time += mus_obj.parent().duration[0] + self.check_for_chord(mus_obj) + else: + # Handle end of \slashedGrace (since it is not a Grace object) + # TODO: Handle multiple/chord \slashedGrace notes + if self.grace_slash and not self.grace_slur: + self.grace_slash = False + self.grace_slur = False + self.grace_seq = False + + def adjust_tuplet_length(self, obj): + r""" Adjusts the length of notes within a \tuplet """ + self.prev_len_before_tuplet = obj.length() + if isinstance(obj.parent(), ly.music.items.Chord): # Adjust length of total chord not singular note in chord + obj = obj.parent() + if len(self.tuplet) != 0: + obj.duration = ((Fraction(obj.duration[0] * self.tuplet[-1]["fraction"][1]) / self.tuplet[-1]["fraction"][0]), obj.duration[1]) + + def end_beam(self, current=False): + """ Ends an ongoing beam not started with [ (prev_note if current is False, current_note otherwise) """ + if self.prev_beam_type != "Manual": + if current: + note = self.mediator.current_note + else: + note = self.mediator.prev_note + if hasattr(note, "beam"): + if note.beam == "continue": + note.set_beam("end") + elif note.beam == "begin": + note.set_beam(False) + self.beam = None + self.shortest_length_in_beam = Fraction(1, 4) + + def note_ends_on_beam_end(self, time_after_note): + """ Return True/False based on whether time_after_note is a beam end (exception or otherwise) """ + for exception in self.beam_exceptions: # In order from least to greatest fractions + if exception['fraction'] >= self.shortest_length_in_beam: + return time_after_note in exception['ends'] + return time_after_note in self.beam_ends + + def update_beams(self, note): + """ + Based on lilypond automatic beaming rules found at `/scm/time-signature-settings.scm` within the lilypond repository + See also: http://lilypond.org/doc/v2.19/Documentation/notation/beams#setting-automatic-beam-behavior + + Given a note: + Begins beam when not currently beamed, auto beam is on, and note is not on specified beam end + Continues exisiting beam when note doesn't end on a specified beam end + Ends beam when current note is a quarter or longer or note ends on specified beam end + """ + time_after_note = self.time_since_bar + note.length() + # Only beam notes shorter than a quarter + # Note: quarter note tuplets should not be beamed + if (note.length() < Fraction(1, 4) and not self.tuplet) or (self.prev_len_before_tuplet < Fraction(1, 4) and self.tuplet): + # Beams started without [ + if self.beam == "Normal": + if self.shortest_length_in_beam > note.length(): + self.shortest_length_in_beam = note.length() + if self.note_ends_on_beam_end(time_after_note): + self.mediator.current_note.set_beam("end") + self.beam = None + self.shortest_length_in_beam = Fraction(1, 4) + else: + self.mediator.current_note.set_beam("continue") + # Beams started with [ + elif self.beam == "Manual": + if not self.mediator.current_note.beam: + self.mediator.current_note.set_beam("continue") + # No ongoing beams + elif self.beam is None and self.auto_beam: + self.shortest_length_in_beam = note.length() + if not self.note_ends_on_beam_end(time_after_note): + self.mediator.current_note.set_beam("begin") + self.beam = "Normal" + self.prev_beam_type = "Normal" + else: + self.shortest_length_in_beam = Fraction(1, 4) + # Quarter note or longer ends ongoing beam else: - if isinstance(note.parent(), ly.music.items.Relative): - self.mediator.set_relative(note) - self.rel_pitch_isset = True - elif isinstance(note.parent(), ly.music.items.Chord): - if self.mediator.current_chord: - self.mediator.new_chord(note, chord_base=False) + self.end_beam() + + def Note(self, note): + """ notename, e.g. c, cis, a bes ... """ + # eprint(note.token) + self.adjust_tuplet_length(note) + self.transpose_note(note) + # if the note is a bass note in a chord symbol, break out of function + if not isinstance(note.parent(), ly.music.items.ChordSpecifier): + if note.length() and self.alt_mode != "chord": + if self.relative and not self.rel_pitch_isset: + self.mediator.new_note(note, False) + self.mediator.set_relative(note) + self.rel_pitch_isset = True else: - self.mediator.new_chord(note, note.parent().duration, self.relative) - self.check_tuplet() - # chord as grace note - if self.grace_seq: - self.mediator.new_chord_grace() + self.mediator.new_note(note, self.relative) + self.check_note(note) + self.update_beams(note) + self.update_time_and_check(note) + else: + if self.alt_mode == "chord": # if chord symbols are being written, record location of chord + self.update_time_and_check(note) + elif isinstance(note.parent(), ly.music.items.Relative): + self.mediator.set_relative(note) + self.rel_pitch_isset = True + elif isinstance(note.parent(), ly.music.items.Chord): + # Secondary note in chord + if self.mediator.current_chord: + self.mediator.new_chord(note, chord_base=False) + # First note in chord + else: + self.mediator.new_chord(note, note.parent().duration, self.relative) + self.check_tuplet() + self.update_beams(note.parent()) + self.update_time_and_check(note) + # chord as grace note + if self.grace_seq: + self.mediator.new_chord_grace(self.grace_slash) def Unpitched(self, unpitched): """A note without pitch, just a standalone duration.""" + self.adjust_tuplet_length(unpitched) if unpitched.length(): if self.alt_mode == 'drum': self.mediator.new_iso_dura(unpitched, self.relative, True) else: self.mediator.new_iso_dura(unpitched, self.relative) self.check_note(unpitched) + self.update_beams(unpitched) + self.update_time_and_check(unpitched) def DrumNote(self, drumnote): """A note in DrumMode.""" + self.adjust_tuplet_length(drumnote) if drumnote.length(): self.mediator.new_note(drumnote, is_unpitched=True) self.check_note(drumnote) + self.update_beams(drumnote) + self.update_time_and_check(drumnote) + + def handle_tremolo_start_stop(self): + """ Set multinote tremolos. The first note gets the 'start' type, subsequent notes get no type. """ + if not self.trem_is_started: + ttype = 'start' + self.trem_is_started = True + else: + ttype = None + self.mediator.set_tremolo(trem_type=ttype, repeats=self.trem_rep, note_count=self.trem_note_count) def check_note(self, note): """Generic check for all notes, both pitched and unpitched.""" self.check_tuplet() + # Handle grace notes and their slurs (marked as grace note slurs to avoid lyrics skipping notes) if self.grace_seq: - self.mediator.new_grace() - if self.trem_rep and not self.look_ahead(note, ly.music.items.Duration): - self.mediator.set_tremolo(trem_type='start', repeats=self.trem_rep) + self.mediator.new_grace(self.grace_slash) + if self.grace_slur: + if not self.prev_was_grace_slur: + self.slurcount += 1 + self.slurnr = self.slurcount + self.mediator.set_slur(self.slurnr, "start", grace=True) + self.prev_was_grace_slur = True + else: + self.mediator.set_slur(self.slurnr, "continue", grace=True) + self.slurcount -= 1 + else: + if self.prev_was_grace_slur: + self.mediator.set_slur(self.slurnr, "stop", grace=True) + self.prev_was_grace_slur = False + if self.trem_rep: + # Update the duration of the note + note.duration = (note.duration[0] * self.trem_rep, note.duration[1]) + # Set tremolo if there isn't a duration change + if not self.look_ahead(note, ly.music.items.Duration): + self.handle_tremolo_start_stop() def check_tuplet(self): """Generic tuplet check.""" @@ -322,10 +922,10 @@ def check_tuplet(self): for td in self.tuplet: if nested: self.mediator.change_to_tuplet(td['fraction'], td['ttype'], - td['nr'], td['length']) + td['nr'], td['length']) else: self.mediator.change_to_tuplet(td['fraction'], td['ttype'], - td['nr']) + td['nr']) td['ttype'] = "" self.mediator.check_divs() @@ -341,9 +941,10 @@ def Duration(self, duration): self.mediator.set_tuplspan_dur(duration.token, duration.tokens) self.tupl_span = False else: - self.mediator.new_duration_token(duration.token, duration.tokens) - if self.trem_rep: - self.mediator.set_tremolo(trem_type='start', repeats=self.trem_rep) + if self.alt_mode not in ["chord", "lyric"]: # Avoids \skip # in lyrics + self.mediator.new_duration_token(duration.token, duration.tokens) + if self.trem_rep: + self.handle_tremolo_start_stop() def Tempo(self, tempo): """ Tempo direction, e g '4 = 80' """ @@ -354,20 +955,87 @@ def Tempo(self, tempo): def Tie(self, tie): """ tie ~ """ - self.mediator.tie_to_next() + self.mediator.tie_to_next(self.tie_types[-1]) def Rest(self, rest): r""" rest, r or R. Note: NOT by command, i.e. \rest """ + self.adjust_tuplet_length(rest) if rest.token == 'R': self.scale = 'R' self.mediator.new_rest(rest) + self.check_note(rest) + self.update_time_and_check(rest) + self.end_beam() def Skip(self, skip): r""" invisible rest/spacer rest (s or command \skip)""" - if 'lyrics' in self.sims_and_seqs: + self.adjust_tuplet_length(skip) + if self.alt_mode == 'lyric': self.mediator.new_lyrics_item(skip.token) + elif self.alt_mode == "chord": + self.total_time += skip.length() + else: + self.break_skip_at_barline(skip) + + def place_skip(self, length, final=False, node=None): + """ Manually place a skip of specified length, updating relevant information """ + if length != 0: + self.mediator.current_is_rest = True + self.mediator.clear_chord() + self.mediator.prev_note = self.mediator.current_note + self.mediator.current_note = xml_objs.BarRest((length, Fraction(1, 1)), voice=self.mediator.voice, voice_name=self.mediator.voice_name, skip=True) + self.mediator.check_current_note(rest=True) + self.update_time_and_check(node, skip_len=length, final_skip=final) # node used to determine whether skip ends voice sep section + self.end_beam() + + def break_skip_at_barline(self, skip): + r""" + When a skip goes over any measure breaks (\bar, regular bar, pickup bar), break skip into measure length pieces + Handle any remaining skip length after last break + """ + break_locations = [] + remaining_length = skip.length() + time_since_bar = self.time_since_bar + partial = 0 + prev_break = 0 + # Get break locations from \bar positions + for bar_location in self.barline_locations: + if bar_location > self.total_time and bar_location <= self.total_time + remaining_length: + break_locations.append(bar_location - self.total_time) + # Get break location from pickup measure if needed + if self.first_meas and self.partial != 0 and remaining_length >= self.partial - time_since_bar: + partial = self.partial - time_since_bar + break_locations.append(partial) + remaining_length -= partial + time_since_bar = 0 + # Get break location from measure already started + if time_since_bar and remaining_length >= self.time_sig - time_since_bar: + partial = self.time_sig - time_since_bar + break_locations.append(partial) + remaining_length -= partial + time_since_bar = 0 + # Get break locations from full measure lengths + if remaining_length >= self.time_sig: + for i in range(remaining_length // self.time_sig): + break_locations.append(partial + self.time_sig * (i + 1)) + break_locations.sort() + # Determine length of skip for any remaining length after the last break + if len(break_locations) > 0: + final_skip_length = skip.length() - break_locations[-1] else: - self.mediator.new_rest(skip) + final_skip_length = skip.length() # Skip was not broken, so entire skip still needs placement + # Place skips for each (unique) break location + for i in range(len(break_locations)): + break_loc = break_locations[i] + skip_length = break_loc - prev_break + # If there isn't any remaining length after this skip, indicate that it is the final one + if i == len(break_locations) - 1 and final_skip_length == 0: + self.place_skip(skip_length, final=True, node=skip) + else: + self.place_skip(skip_length) + prev_break = break_loc + # Place last skip (if its length isn't 0) + self.place_skip(final_skip_length, final=True, node=skip) def Scaler(self, scaler): r""" @@ -404,26 +1072,44 @@ def Postfix(self, postfix): pass def Beam(self, beam): - pass + """ Beam, "[" = begin, "]" = end. """ + if beam.token == "[": + self.mediator.current_note.set_beam("begin") + self.end_beam() + if self.beam == "Manual": # Should never be True + self.mediator.current_note.set_beam("continue") + eprint("Warning: Beam start does not have corresponding beam end!") + self.beam = "Manual" + self.prev_beam_type = "Manual" + elif beam.token == "]": + if self.beam == "Manual": + self.mediator.current_note.set_beam("end") + self.beam = None + else: # Should never be True + eprint("Warning: Beam end does not have corresponding beam start!") + + def Partial(self, partial): + r""" \partial # """ + self.partial = partial.partial_length() def Slur(self, slur): """ Slur, '(' = start, ')' = stop. """ if slur.token == '(': self.slurcount += 1 self.slurnr = self.slurcount - self.mediator.set_slur(self.slurnr, "start") + self.mediator.set_slur(self.slurnr, "start", False, self.slur_types[-1]) elif slur.token == ')': - self.mediator.set_slur(self.slurnr, "stop") + self.mediator.set_slur(self.slurnr, "stop", False, self.slur_types[-1]) self.slurcount -= 1 def PhrasingSlur(self, phrslur): r"""A \( or \).""" - if phrslur.token == '\(': + if phrslur.token == r'\(': self.slurcount += 1 self.phrslurnr = self.slurcount - self.mediator.set_slur(self.phrslurnr, "start") - elif phrslur.token == '\)': - self.mediator.set_slur(self.phrslurnr, "stop") + self.mediator.set_slur(self.phrslurnr, "start", True, self.phr_slur_types[-1]) + elif phrslur.token == r'\)': + self.mediator.set_slur(self.phrslurnr, "stop", True, self.phr_slur_types[-1]) self.slurcount -= 1 def Dynamic(self, dynamic): @@ -431,16 +1117,111 @@ def Dynamic(self, dynamic): self.mediator.new_dynamics(dynamic.token[1:]) def Grace(self, grace): + if grace.token == '\\acciaccatura': + self.grace_slash = True + self.grace_slur = True + elif grace.token == '\\appoggiatura': + self.grace_slash = False + self.grace_slur = True + else: + self.grace_slash = False + self.grace_slur = False self.grace_seq = True + def generate_beam_ends(self, fraction, beat_pattern): + """ Given a denominator and a pattern of beats to count by (array), return an array of the intended beam end locations """ + beam_ends = [] + prev_beam_end = 0 + for beat in beat_pattern: + beam_end = prev_beam_end + beat * fraction + beam_ends.append(beam_end) + prev_beam_end = beam_end + return beam_ends + + def get_beat_structure_from_time_sig(self, numerator, denominator): + """ + Get the beat structure and exceptions from a given key signature (numerator and denominator) + + Rules derived from Lilypond's `/scm/time-signature-settings.scm` see http://lilypond.org/doc/v2.19/Documentation/notation/beams#setting-automatic-beam-behavior + """ + # Generate beam_ends (default) (an array of beam end locations (fractions)) + self.beam_ends = [] + fraction = Fraction(1, denominator) + beat_pattern = [] + if numerator > 3 and numerator % 3 == 0: # numerators like 6, 9, 12,... + for i in range(numerator // 3): + beat_pattern.append(3) + self.beam_ends = self.generate_beam_ends(Fraction(1, denominator), beat_pattern) + elif numerator == 4 and denominator == 8: # 4/8 + beat_pattern = [2, 2] + elif numerator == 5 and denominator == 8: # 5/8 + beat_pattern = [3, 2] + elif numerator == 8 and denominator == 8: # 8/8 + beat_pattern = [3, 3, 2] + else: # all other time signatures + for i in range(numerator): + beat_pattern.append(1) + self.beam_ends = self.generate_beam_ends(fraction, beat_pattern) + # Generate beam_exceptions (an array of dictionaries containing a lowest fraction and its associated beam ends array) sorted by fraction + self.beam_exceptions = [] + exception_rules = {} + if numerator == 2 and denominator == 2: # 2/2 + exception_rules[Fraction(1, 32)] = [8, 8, 8, 8] + elif numerator == 3 and denominator == 2: # 3/2 + exception_rules[Fraction(1, 32)] = [8, 8, 8, 8, 8, 8] + elif numerator == 3 and denominator == 4: # 3/4 + exception_rules[Fraction(1, 8)] = [6] + exception_rules[Fraction(1, 12)] = [3, 3, 3] + elif numerator == 3 and denominator == 8: # 3/8 + exception_rules[Fraction(1, 8)] = [3] + elif numerator == 4 and denominator == 2: # 4/2 + exception_rules[Fraction(1, 16)] = [4, 4, 4, 4, 4, 4, 4, 4] + elif numerator == 4 and denominator == 4: # 4/4 + exception_rules[Fraction(1, 8)] = [4, 4] + exception_rules[Fraction(1, 12)] = [3, 3, 3, 3] + elif numerator == 6 and denominator == 4: # 6/4 + exception_rules[Fraction(1, 16)] = [4, 4, 4, 4, 4, 4] + elif numerator == 9 and denominator == 4: # 9/4 + exception_rules[Fraction(1, 32)] = [8, 8, 8, 8, 8, 8, 8, 8] + elif numerator == 12 and denominator == 4: # 12/4 + exception_rules[Fraction(1, 32)] = [8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8] + for fract in sorted(exception_rules.keys()): + self.beam_exceptions.append({"fraction": fract, "ends": self.generate_beam_ends(fract, exception_rules[fract])}) + def TimeSignature(self, timeSign): - self.mediator.new_time(timeSign.numerator(), timeSign.fraction(), self.numericTime) + r""" See update_time_sig() and generate_location_dicts() for more on how `\time` is implemented """ + pass def Repeat(self, repeat): + r""" A \repeat specifier num {...} provides various types of repetition depending on specifier. """ if repeat.specifier() == 'volta': - self.mediator.new_repeat('forward') + if self.alt_mode not in ['lyric', 'chord']: + self.volta_counts.append(repeat.repeat_count()) + self.mediator.new_repeat('forward') elif repeat.specifier() == 'tremolo': self.trem_rep = repeat.repeat_count() + if isinstance(repeat[0], ly.music.items.MusicList): # List of notes + self.trem_note_count = len(repeat[0]) + else: # Singular note + self.trem_note_count = 1 + else: # TODO: Support percent repeats + eprint("Warning: Repeat", repeat.specifier(), "is not supported!") + + def Alternative(self, alt): + r""" A \alternative {...} provides alternate endings. """ + if self.alt_mode not in ['lyric', 'chord']: + num_endings = len(alt[0]) + num_repeats = self.volta_counts.pop() + if num_endings > num_repeats: + eprint('Warning: More alternate endings than repeats!') + # Create a stack of ending lists of form [starting_repeat_num, ending_repeat_num, is_last] + for ending_num in range(1, num_endings + 1): + if ending_num == 1: # First ending + self.alt_endings.append([ending_num, num_repeats - num_endings + ending_num, False]) + else: # All other endings + self.alt_endings.append([num_repeats - num_endings + ending_num, num_repeats - num_endings + ending_num, False]) + if ending_num == num_endings: # If last ending, indicate so + self.alt_endings[-1][-1] = True def Tremolo(self, tremolo): """A tremolo item ":".""" @@ -453,6 +1234,23 @@ def With(self, cont_with): r"""A \with ... construct.""" self.with_contxt = cont_with.parent().context() + def set_base_moment(self, numer_denom): + self.base_moment = Fraction(numer_denom[0], numer_denom[1]) + + def set_beat_structure(self, beat_list): + self.beam_ends = [] + prev_end = 0 + for num in beat_list: + prev_end += self.base_moment * num + self.beam_ends.append(prev_end) + + def set_ignore_melismata(self, ignore): + """ Sends command within lyrics to ignore beams (True) or skip over them (False). """ + if ignore: + self.mediator.new_lyrics_item(["ignoreMelismata", True, "command"]) + else: + self.mediator.new_lyrics_item(["ignoreMelismata", False, "command"]) + def Set(self, cont_set): r"""A \set command.""" if isinstance(cont_set.value(), ly.music.items.Scheme): @@ -470,16 +1268,50 @@ def Set(self, cont_set): self.mediator.set_by_property(cont_set.property(), val) elif cont_set.context() in group_contexts: self.mediator.set_by_property(cont_set.property(), val, group=True) + if cont_set.property() == 'baseMoment': + self.set_base_moment(cont_set.value().get_list_ints()) + elif cont_set.property() == 'beatStructure': + self.beam_ends = self.generate_beam_ends(self.base_moment, cont_set.value().get_list_ints()) + # TODO: Add functionality for setting custom beam exceptions (currently just clears beam_exceptions, as this is common) + elif cont_set.property() == 'beamExceptions': + self.beam_exceptions = [] + elif cont_set.property() == 'associatedVoice': + self.mediator.new_lyrics_item(["switchVoice", cont_set.value().value(), "command"]) + elif cont_set.property() == 'ignoreMelismata': + self.set_ignore_melismata(cont_set.value().get_bool()) + elif cont_set.property() == 'measureLength': + pass # See generate_location_dicts() and update_time_sig() for implementation of measureLength + else: + eprint("Warning: Set", cont_set.property(), "failed!") + + def Unset(self, cont_unset): + r""" A \unset command. """ + if cont_unset.property() == 'ignoreMelismata': + self.mediator.new_lyrics_item(["ignoreMelismata", False, "command"]) + else: + eprint("Warning: Unset", cont_unset.property(), "failed!") def Command(self, command): r""" \bar, \rest etc """ - excls = ['\\major', '\\minor', '\\bar'] + excls = ['\\major', '\\dorian', '\\minor', '\\bar'] if command.token == '\\rest': self.mediator.note2rest() elif command.token == '\\numericTimeSignature': self.numericTime = True + # If the current bar has no music and is in common or cut time, then change it to numeric time + if self.mediator.bar and not self.mediator.bar.has_music() and self.mediator.current_attr.time and self.mediator.current_attr.time[-1] in ['common', 'cut']: + self.mediator.current_attr.time.pop() elif command.token == '\\defaultTimeSignature': self.numericTime = False + # If the current bar has no music and is 2/2 or 4/4, then change it to cut or common time respectively + # Don't add 'cut' or 'common' tag if one already exists + if self.mediator.bar and not self.mediator.bar.has_music() and self.mediator.current_attr.time and len(self.mediator.current_attr.time) == 2: + num = self.mediator.current_attr.time[0] + den = self.mediator.current_attr.time[1] + if num == 2 and den == 2: + self.mediator.current_attr.time.append('cut') + elif num == 4 and den == 4: + self.mediator.current_attr.time.append('common') elif command.token.find('voice') == 1: self.mediator.set_voicenr(command.token[1:], piano=self.piano_staff) elif command.token == '\\glissando': @@ -497,19 +1329,60 @@ def Command(self, command): if self.tupl_span: self.mediator.unset_tuplspan_dur() self.tupl_span = False + elif command.token == '\\noBeam': + self.end_beam() + if self.prev_beam_type == "Normal": # noBeam does not apply to [] beams + self.mediator.current_note.set_beam(False) + elif command.token == '\\autoBeamOn': + self.auto_beam = True + elif command.token == '\\autoBeamOff': + self.auto_beam = False + elif command.token == '\\slurSolid': + self.slur_types[-1] = 'solid' + elif command.token == '\\slurDashed': + self.slur_types[-1] = 'dashed' + elif command.token == '\\slurDotted': + self.slur_types[-1] = 'dotted' + elif command.token == '\\tieSolid': + self.tie_types[-1] = 'solid' + elif command.token == '\\tieDashed': + self.tie_types[-1] = 'dashed' + elif command.token == '\\tieDotted': + self.tie_types[-1] = 'dotted' + elif command.token == '\\phrasingSlurSolid': + self.phr_slur_types[-1] = 'solid' + elif command.token == '\\phrasingSlurDashed': + self.phr_slur_types[-1] = 'dashed' + elif command.token == '\\phrasingSlurDotted': + self.phr_slur_types[-1] = 'dotted' + elif command.token == '\\break': + self.mediator.set_sys_break() + elif command.token == '\\breathe': + self.mediator.set_breathe() else: if command.token not in excls: - print("Unknown command:", command.token) + eprint("Unknown command:", command.token) def UserCommand(self, usercommand): """Music variables are substituted so this must be something else.""" if usercommand.name() == 'tupletSpan': self.tupl_span = True + elif usercommand.name() == 'slashedGrace': + self.grace_slash = True + self.grace_slur = False + self.grace_seq = True def String(self, string): - prev = self.get_previous_node(string) - if prev and prev.token == '\\bar': - self.mediator.create_barline(string.value()) + r""" + Handles case where multiple words are to be placed on one note (ex: "all these words") + + See check_for_barline() and generate_location_dicts() for more on how `\bar "..."` is implemented + """ + if self.alt_mode == 'lyric' and isinstance(string.parent(), ly.music.items.LyricText): + if 'LyricText_font-shape' in self.override_dict and self.override_dict['LyricText_font-shape'] == 'italic': + self.mediator.new_lyrics_text(string.tokens[0], True) + else: + self.mediator.new_lyrics_text(string.tokens[0]) def LyricsTo(self, lyrics_to): r"""A \lyricsto expression. """ @@ -517,8 +1390,17 @@ def LyricsTo(self, lyrics_to): self.sims_and_seqs.append('lyrics') def LyricText(self, lyrics_text): - """A lyric text (word, markup or string), with a Duration.""" - self.mediator.new_lyrics_text(lyrics_text.token) + """ + A lyric text (word, markup or string), with a Duration + + Create a lyric text if there is text or the next node is not a String + in which case, wait to allow that String to be the lyric text (see String() above) + """ + if lyrics_text.token or not isinstance(lyrics_text[0], ly.music.items.String): + if 'LyricText_font-shape' in self.override_dict and self.override_dict['LyricText_font-shape'] == 'italic': + self.mediator.new_lyrics_text(lyrics_text.token, True) + else: + self.mediator.new_lyrics_text(lyrics_text.token) def LyricItem(self, lyrics_item): """Another lyric item (skip, extender, hyphen or tie).""" @@ -553,10 +1435,53 @@ def LyricMode(self, lyricmode): def Override(self, override): r"""An \override command.""" self.override_key = '' + self.override_key_word_count = len(override) - 1 + + def Revert(self, revert): + r"""A \revert command.""" + self.revert_key = '' + self.revert_key_word_count = len(revert) + + def handle_override_revert_items(self, item): + r""" + Handle items which come after \override or \revert. + + For override: + If the item is not the final one, it should be appended to the key. + The final item should be the value assigned to the key in override_dict. + + For revert: + All items compose the key, and once complete, that key should be popped from override_dict. + """ + # Override item + if self.look_behind(item, ly.music.items.Override): + # First word in key + if len(self.override_key) == 0: + self.override_key += item.token + # Secondary word in key (separated by _) + elif self.override_key.count('_') < self.override_key_word_count - 1: + self.override_key += '_' + item.token + # Key is complete, value is assigned in dictionary + else: + self.override_dict[self.override_key] = str(item.token) + # Revert item + elif self.look_behind(item, ly.music.items.Revert): + # First word in key + if len(self.revert_key) == 0: + self.revert_key += item.token + # Secondary word in key (separated by _) + elif self.revert_key.count('_') < self.revert_key_word_count - 1: + self.revert_key += '_' + item.token + # Key is complete, value with revert_key is popped from dictionary + if self.revert_key.count('_') == self.revert_key_word_count - 1: + try: + self.override_dict.pop(self.revert_key) + except KeyError as ke: + eprint("Warning: Revert could not find override to remove!") def PathItem(self, item): r"""An item in the path of an \override or \revert command.""" - self.override_key += item.token + self.handle_override_revert_items(item) def Scheme(self, scheme): """A Scheme expression inside LilyPond.""" @@ -567,17 +1492,23 @@ def SchemeItem(self, item): if self.ottava: self.mediator.new_ottava(item.token) self.ottava = False - elif self.look_behind(item, ly.music.items.Override): - self.override_dict[self.override_key] = item.token + elif self.look_behind(item, ly.music.items.Override) or self.look_behind(item, ly.music.items.Revert): + self.handle_override_revert_items(item) elif self.schm_assignm: self.mediator.set_by_property(self.schm_assignm, item.token) + elif self.look_behind(item, ly.music.items.Set): + pass # See Set() else: - print("SchemeItem not implemented:", item.token) + eprint("SchemeItem not implemented:", item.token) def SchemeQuote(self, quote): """A ' in scheme.""" pass + def SchemeList(self, slist): + """ A (...) inside scheme (handled eslewhere on a case-by-case basis). """ + pass + def End(self, end): if isinstance(end.node, ly.music.items.Scaler): if self.unset_tuplspan: @@ -587,16 +1518,24 @@ def End(self, end): self.mediator.change_tuplet_type(len(self.tuplet) - 1, "stop") self.tuplet.pop() self.fraction = None - elif isinstance(end.node, ly.music.items.Grace): #Grace + elif isinstance(end.node, ly.music.items.Grace): # Grace notes (except \slashedGrace) + self.grace_slash = False + self.grace_slur = False self.grace_seq = False elif end.node.token == '\\repeat': - if end.node.specifier() == 'volta': - self.mediator.new_repeat('backward') + if end.node.specifier() == 'volta' and self.alt_mode not in ['lyric', 'chord']: + # Create an ending repeat barline if there are no alternate endings (otherwise the repeat signs are already between endings) + if not self.look_ahead(end.node, ly.music.items.Alternative): + self.mediator.new_repeat('backward') + elif not self.look_ahead(end.node[-1][0], ly.music.items.MusicList): + self.mediator.new_repeat('backward') + eprint("Warning: Alternate ending has no music lists!") elif end.node.specifier() == 'tremolo': if self.look_ahead(end.node, ly.music.items.MusicList): self.mediator.set_tremolo(trem_type="stop") else: self.mediator.set_tremolo(trem_type="single") + self.trem_is_started = False self.trem_rep = 0 elif isinstance(end.node, ly.music.items.Context): self.in_context = False @@ -606,32 +1545,76 @@ def End(self, end): elif end.node.context() in group_contexts: self.mediator.close_group() elif end.node.context() in staff_contexts: + self.numericTime = False if not self.piano_staff: self.mediator.check_part() elif end.node.context() in pno_contexts: self.mediator.check_voices() self.mediator.check_part() self.piano_staff = 0 + self.staves_in_piano_staff = False self.mediator.set_voicenr(nr=1) elif end.node.context() == 'Devnull': self.mediator.check_voices() + elif end.node.context() == 'Lyrics': + self.mediator.check_voices() elif end.node.token == '<<': if self.voice_sep: self.mediator.check_voices_by_nr() - self.mediator.revert_voicenr() + self.mediator.set_voicenr(nr=self.mediator.store_voicenr) self.voice_sep = False - elif not self.piano_staff: + self.voice_sep_length = 0 + self.check_for_barline() + self.update_time_sig() + self.mediator.voice_sep_sections = 0 + self.mediator.voice_name = self.voice_sep_start_voice_name + self.voice_sep_start_voice_name = None + elif not self.piano_staff and not self.alt_mode == 'lyric': # Simultaneous lyric sections not currently supported self.mediator.check_simultan() if self.sims_and_seqs: self.sims_and_seqs.pop() elif end.node.token == '{': + self.tie_types.pop() + self.phr_slur_types.pop() + self.slur_types.pop() if self.sims_and_seqs: self.sims_and_seqs.pop() - elif end.node.token == '<': #chord + if end.node.parent().token == '<<': + self.mediator.voice_sep_sections += 1 + self.voice_sep_length = self.total_time - self.voice_sep_start_total_time + if end.node.parent().token in ["\\notemode", "\\notes", "\\chordmode", "\\chords", + "\\drummode", "\\drums", "\\figuremode", "\\figures", + "\\lyricmode", "\\lyrics", "\\addlyrics"]: + self.alt_mode = None + elif end.node.parent().token == '\\transpose': + self.transposer = None + if self.is_volta_ending(end.node): + end = self.alt_endings.pop(0) + if end[2]: # Final ending + self.mediator.new_ending(end[0], end[1], 'discontinue', self.staff) + else: # All others + self.mediator.new_ending(end[0], end[1], 'stop', self.staff) + elif end.node.token == '<': # chord self.mediator.chord_end() + # Check for bar unless final note in voice separator (in which case, wait until after) + if not self.voice_sep: + self.check_for_barline() + self.update_time_sig() + else: + node = end.node + while True: + if not self.get_next_node(node): + break + elif isinstance(self.get_next_node(node), ly.music.items.Durable): + self.check_for_barline() + self.update_time_sig() + break + else: + node = self.get_next_node(node) elif end.node.token == '\\lyricsto': self.mediator.check_lyrics(end.node.context_id()) self.sims_and_seqs.pop() + self.mediator.new_lyric_nr(self.mediator.lyric_nr + 1) elif end.node.token == '\\with': self.with_contxt = None elif end.node.token == '\\drums': @@ -640,7 +1623,7 @@ def End(self, end): self.relative = False self.rel_pitch_isset = False else: - # print("end:", end.node.token) + # eprint("end:", end.node.token) pass ## @@ -648,7 +1631,7 @@ def End(self, end): ## def get_previous_node(self, node): - """ Returns the nodes previous node + """ Returns the node's previous node or false if the node is first in its branch. """ parent = node.parent() i = parent.index(node) @@ -657,18 +1640,32 @@ def get_previous_node(self, node): else: return False - def simple_node_gen(self, node): + def get_next_node(self, node): + """ Returns the node's next node + or false if the node is last in its branch. """ + parent = node.parent() + i = parent.index(node) + try: + return parent[i+1] + except IndexError: + return False + + def simple_node_gen(self, nodes): """Unlike iter_score are the subnodes yielded without substitution.""" - for n in node: - yield n - for s in self.simple_node_gen(n): - yield s + for starting_node in nodes: + for n in starting_node: + yield n + for s in self.simple_node_gen([n]): + yield s def iter_header(self, tree): - """Iter only over header nodes.""" + """Iter only over header nodes (could be more than one header).""" + headers = [] for t in tree: if isinstance(t, ly.music.items.Header): - return self.simple_node_gen(t) + headers.append(t) + if headers: + return self.simple_node_gen(headers) def get_score(self, node): """ Returns (first) Score node or false if no Score is found. """ @@ -702,10 +1699,35 @@ def unfold_repeat(self, repeat_node, repeat_count, doc): Iter over node which represent a \repeat unfold expression and do the unfolding directly. """ + # Create list of repeat_count alternate endings (in reverse order) if there are any + # (the first ending is repeated to fill in the difference between the number of endings and repeat_count) + num_endings = 0 + endings = [] + for node in repeat_node: + if isinstance(node, ly.music.items.Alternative): + num_endings = len(node[0]) + if num_endings > repeat_count: + num_endings = repeat_count + eprint("Warning: More alternate endings than unfold repeats (removing extras)!") + if num_endings != 0: + # Append all endings from the last ending until the second ending (reverse order) + for i in range(num_endings - 1, 0, -1): + endings.append(node[0][i]) + # Repeat first ending + for i in range(repeat_count - num_endings + 1): + endings.append(node[0][0]) + break + # Duplicate the nodes in the repeat for r in range(repeat_count): + # Duplicate nodes in the body of the repeat for n in repeat_node: - for c in self.iter_score(n, doc): - yield c + if not isinstance(n, ly.music.items.Alternative): + for c in self.iter_score(n, doc): + yield c + # Produce one ending (if any) after every time the body of the repeat has been duplicated + if endings: + for alt in self.iter_score(endings.pop(), doc): + yield alt def find_score_sub(self, doc): """Find substitute for scorenode. Takes first music node that isn't diff --git a/ly/musicxml/xml_objs.py b/ly/musicxml/xml_objs.py index bad1e057..428f19c7 100644 --- a/ly/musicxml/xml_objs.py +++ b/ly/musicxml/xml_objs.py @@ -53,12 +53,24 @@ from fractions import Fraction +import sys + + +def eprint(*args, **kwargs): + """ + From https://stackoverflow.com/questions/5574702/how-to-print-to-stderr-in-python + Prints to stderr + """ + print(*args, file=sys.stderr, **kwargs) + + class IterateXmlObjs(): """ A ly.musicxml.xml_objs.Score object is iterated and the Music XML node tree is constructed. """ + def __init__(self, score, musxml, div): """Create the basic score information, and initiate the iteration of the parts.""" @@ -67,9 +79,11 @@ def __init__(self, score, musxml, div): self.divisions = div if score.title: self.musxml.create_title(score.title) - for ctag in score.creators: + if score.subtitle: + self.musxml.create_subtitle(score.subtitle) + for ctag in sorted(score.creators.keys()): self.musxml.add_creator(ctag, score.creators[ctag]) - for itag in score.info: + for itag in sorted(score.info.keys()): self.musxml.create_score_info(itag, score.info[itag]) if score.rights: self.musxml.add_rights(score.rights) @@ -78,6 +92,8 @@ def __init__(self, score, musxml, div): self.iterate_part(p) elif isinstance(p, ScorePartGroup): self.iterate_partgroup(p) + # Correct the accidentals from the last measure + self.musxml.correct_accidentals() def iterate_partgroup(self, group): """Loop through a group, recursively if nested.""" @@ -98,34 +114,56 @@ def iterate_part(self, part): for bar in part.barlist: self.iterate_bar(bar) else: - print("Warning: empty part:", part.name) + eprint("Warning: empty part:", part.name) def iterate_bar(self, bar): """The objects in the bar are output to the xml-file.""" - self.musxml.create_measure() - for obj in bar.obj_list: - if isinstance(obj, BarAttr): - self.new_xml_bar_attr(obj) - elif isinstance(obj, BarMus): - self.before_note(obj) - if isinstance(obj, BarNote): - self.new_xml_note(obj) - elif isinstance(obj, BarRest): - self.new_xml_rest(obj) - self.gener_xml_mus(obj) - self.after_note(obj) - elif isinstance(obj, BarBackup): - divdur = self.count_duration(obj.duration, self.divisions) - self.musxml.add_backup(divdur) + if len(bar.obj_list) > 1: # Prevents empty measures from being made + self.musxml.create_measure() + for obj in bar.obj_list: + if isinstance(obj, BarAttr): + self.new_xml_bar_attr(obj) + elif isinstance(obj, BarMus): + self.before_note(obj) + if isinstance(obj, BarNote): + self.new_xml_note(obj) + elif isinstance(obj, BarRest): + self.new_xml_rest(obj) + self.after_note(obj) + elif isinstance(obj, BarBackup): + divdur = self.count_duration(obj.duration, self.divisions) + self.musxml.add_backup(divdur) def new_xml_bar_attr(self, obj): """Create bar attribute xml-nodes.""" if obj.has_attr(): - self.musxml.new_bar_attr(obj.clef, obj.time, obj.key, obj.mode, obj.divs) - if obj.repeat: - self.musxml.add_barline(obj.barline, obj.repeat) + self.musxml.new_bar_attr(obj.clef, obj.time, obj.key, obj.mode, obj.divs, obj.sys_break) + # Place repeats, alternate endings, and their associated barlines + if obj.endings or obj.repeat is not None: + # Get repeat + forward_rep = None + backward_rep = None + if obj.repeat == 'forward': + forward_rep = obj.repeat + elif obj.repeat == 'backward': + backward_rep = obj.repeat + # Get endings + ending_start = None + ending_end = None + for end in obj.endings: + if end.etype == 'start': + ending_start = end + else: + ending_end = end + # Left barline + if forward_rep is not None or ending_start is not None or obj.left_barline is not None: + self.musxml.add_barline(obj.left_barline, ending_start, forward_rep) + # Right barline + if backward_rep is not None or ending_end is not None or obj.barline is not None: + self.musxml.add_barline(obj.barline, ending_end, backward_rep) + # Place barline without any repeats or alternate endings elif obj.barline: - self.musxml.add_barline(obj.barline) + self.musxml.add_barline(obj.barline, None, None) if obj.staves: self.musxml.add_staves(obj.staves) if obj.multiclef: @@ -140,6 +178,10 @@ def before_note(self, obj): self._add_dynamics([d for d in obj.dynamic if d.before]) if obj.oct_shift and not obj.oct_shift.octdir == 'stop': self.musxml.add_octave_shift(obj.oct_shift.plac, obj.oct_shift.octdir, obj.oct_shift.size) + if len(obj.harmony) != 0: + for h in obj.harmony: + self.musxml.add_harmony(h.root, h.root_alter, h.bass, h.bass_alter, h.text, + self.count_duration((h.offset, 1), self.divisions)) def after_note(self, obj): """Xml-nodes after note.""" @@ -165,8 +207,6 @@ def gener_xml_mus(self, obj): for t in obj.tuplet: self.musxml.tuplet_note(t.fraction, obj.duration, t.ttype, t.nr, self.divisions, t.acttype, t.normtype) - if obj.staff and not obj.skip: - self.musxml.add_staff(obj.staff) if obj.other_notation: self.musxml.add_named_notation(obj.other_notation) @@ -175,15 +215,15 @@ def new_xml_note(self, obj): divdur = self.count_duration(obj.duration, self.divisions) if isinstance(obj, Unpitched): self.musxml.new_unpitched_note(obj.base_note, obj.octave, obj.type, divdur, - obj.voice, obj.dot, obj.chord, obj.grace) + obj.voice, obj.dot, obj.chord, obj.grace, obj.staff) else: self.musxml.new_note(obj.base_note, obj.octave, obj.type, divdur, - obj.alter, obj.accidental_token, obj.voice, obj.dot, obj.chord, - obj.grace) + obj.alter, obj.accidental_token, obj.voice, obj.dot, obj.chord, + obj.grace, obj.staff, obj.beam) for t in obj.tie: - self.musxml.tie_note(t) + self.musxml.tie_note(t[0], t[1]) for s in obj.slur: - self.musxml.add_slur(s.nr, s.slurtype) + self.musxml.add_slur(s.nr, s.slurtype, s.line) for a in obj.artic: self.musxml.new_articulation(a) if obj.ornament: @@ -196,12 +236,17 @@ def new_xml_note(self, obj): self.musxml.add_gliss(obj.gliss[0], obj.gliss[1], obj.gliss[2]) if obj.fingering: self.musxml.add_fingering(obj.fingering) + self.gener_xml_mus(obj) # Notations must be added before lyrics to have a valid XML if obj.lyric: for l in obj.lyric: - try: - self.musxml.add_lyric(l[0], l[1], l[2], l[3]) - except IndexError: - self.musxml.add_lyric(l[0], l[1], l[2]) + args = [] + # Allows a lyric to be italicized + if "italic" in l: + args.append("italic") + # Allows a lyric to have the extend tag + if "extend" in l: + args.append("extend") + self.musxml.add_lyric(l[0], l[1], l[2], args) def new_xml_rest(self, obj): """Create rest specific xml-nodes.""" @@ -210,7 +255,8 @@ def new_xml_rest(self, obj): self.musxml.add_skip(divdur) else: self.musxml.new_rest(divdur, obj.type, obj.pos, - obj.dot, obj.voice) + obj.dot, obj.voice, obj.staff) + self.gener_xml_mus(obj) def count_duration(self, base_scaling, divs): base = base_scaling[0] @@ -222,9 +268,11 @@ def count_duration(self, base_scaling, divs): class Score(): """Object that keep track of a whole score.""" + def __init__(self): self.partlist = [] self.title = None + self.subtitle = None self.creators = {} self.info = {} self.rights = None @@ -251,15 +299,16 @@ def debug_score(self, attr=[]): """ ind = " " + def debug_part(p): - print("Score part:"+p.name) + eprint("Score part:"+p.name) for n, b in enumerate(p.barlist): - print(ind+"Bar nr: "+str(n+1)) + eprint(ind+"Bar nr: "+str(n+1)) for obj in b.obj_list: - print(ind+ind+repr(obj)) + eprint(ind+ind+repr(obj)) for a in attr: try: - print(ind+ind+ind+a+':'+repr(getattr(obj, a))) + eprint(ind+ind+ind+a+':'+repr(getattr(obj, a))) except AttributeError: pass @@ -267,7 +316,7 @@ def debug_group(g): if hasattr(g, 'barlist'): debug_part(g) else: - print("Score group:"+g.name) + eprint("Score group:"+g.name) for pg in g.partlist: debug_group(pg) @@ -277,6 +326,7 @@ def debug_group(g): class ScorePartGroup(): """Object to keep track of part group.""" + def __init__(self, num, bracket): self.bracket = bracket self.partlist = [] @@ -296,6 +346,7 @@ def merge_voice(self, voice, override=False): class ScoreSection(): """ object to keep track of music section """ + def __init__(self, name, glob=False): self.name = name self.barlist = [] @@ -314,32 +365,148 @@ def merge_voice(self, voice, override=False): def merge_lyrics(self, lyrics): """Merge in lyrics in music section.""" - i = 0 - ext = False + current_voice = lyrics.voice_id # Indicates which voice the notes should be assigned to + voices = {} # Stores the notes in each voice (name (ex: "SopranoVoice") or number (ex: "3")) + indices = {} # Stores the current index in each voice's note list + objects = {} # Stores the current object (containing the note and time) in each voice + chords = {} # Stores the secondary chord notes (containing the note indexed by time) in each voice + time = 0 # Stores the current time in the music + lyrics_idx = 0 # Stores the current index in the lyrics list + ignore_slur = False # Indicates whether subsequent slurred/tied notes should have lyrics (no if False) + slurs = {} # Indicates whether the current note is slurred for each voice {"voice": bool} + ties = {} # Indicates the number of ties not yet completed {"voice": int} + prev_time = -1 # Stores the start time of the previously placed lyric + note_used = True # Indicates whether the current note has had a lyric (or skip) assigned + voice_count = 0 # Stores how many voices there are (not counting the "None" voice) + # Create dictionary of empty lists for each voice's notes + for bar in self.barlist: + for obj in bar.obj_list: + if isinstance(obj, BarMus) and not obj.chord and (not isinstance(obj, BarNote) or obj.grace == (0, 0)): + # Get the name of the voice, if there is no voice name, then use "None" + voc_name = obj.voice_name + if voc_name is None: + voc_name = "None" + eprint("Warning: Voice name for an object is None!") + if voc_name not in voices: + voices[voc_name] = [] + if voc_name not in chords: + chords[voc_name] = {} + voice_count = len(voices) + if "None" in voices: + voice_count -= 1 + # Create dictionary of lists for each voice's notes for bar in self.barlist: for obj in bar.obj_list: - if isinstance(obj, BarNote): - if ext: - if obj.slur: - ext = False + # Handle notes and rests which are not secondary chord notes (placed in the voices list, advances time) + if isinstance(obj, BarMus) and not obj.chord and (not isinstance(obj, BarNote) or obj.grace == (0, 0)): + # Get the name of the voice, if there is no voice name, then use "None" + voc_name = obj.voice_name + if voc_name is None: + voc_name = "None" + voices[voc_name].append({"note": obj, "time": time}) + time += obj.duration[0] * obj.duration[1] + # Handle bar backups which send current time backwards + elif isinstance(obj, BarBackup) and voice_count > 1: + time -= obj.duration[0] * obj.duration[1] + # Handle secondary chord notes (placed in the chords dictionary) + elif isinstance(obj, BarMus) and obj.chord: + # Get the name of the voice, if there is no voice name, then use "None" + voc_name = obj.voice_name + if voc_name is None: + voc_name = "None" + chord_time = time - obj.duration[0] * obj.duration[1] + if chord_time in chords[voc_name]: + chords[voc_name][chord_time].append(obj) else: - try: - l = lyrics.barlist[i] - except IndexError: - break - if l != 'skip': - try: - if l[3] == "extend" and obj.slur: - ext = True - except IndexError: - pass - obj.add_lyric(l) - i += 1 + chords[voc_name][chord_time] = [obj] + # Initialize the needed keys for indices and objects + for voice in voices: + voice_count += 1 + if voice not in indices: + indices[voice] = 0 + objects[voice] = None + slurs[voice] = False + ties[voice] = 0 + while(True): + # Update position and slur/tie status in all voice's note lists (break if the necessary list (current voice) ends) + if note_used: + for voice, notes in voices.items(): + try: + while(not notes[indices[voice]]["time"] > prev_time): + # Adjust the status of slurs and ties + if isinstance(notes[indices[voice]]["note"], BarNote): + # For loops cover the case where a note has multiple slur objects + # ex: the note `(a)` has an opening and closing slur, meaning slur should remain False + for s in notes[indices[voice]]["note"].slur: + if not s.phrasing and not s.grace: # Phrasing/grace slurs don't affect lyric placement + slurs[voice] = not slurs[voice] + # Keep track of the number of unfinished ties (normal notes) + for t in notes[indices[voice]]["note"].tie: + if t[0] == "start": + ties[voice] += 1 + else: # "stop" + ties[voice] -= 1 + # Keep track of the number of unfinished ties (chord notes) + if notes[indices[voice]]["time"] in chords[voice]: + for ch in chords[voice][notes[indices[voice]]["time"]]: + for t in ch.tie: + if t[0] == "start": + ties[voice] += 1 + else: # "stop" + ties[voice] -= 1 + indices[voice] += 1 + # Store current note in voice + objects[voice] = notes[indices[voice]] + except IndexError: + objects[voice] = None + # Choose needed note + obj = objects[current_voice] + if obj is None: + break + prev_time = obj["time"] + note_used = False + # After finding the proper note, add the lyric and update status of voice, slurs, and ties + if isinstance(obj["note"], BarNote): + if ignore_slur or (not slurs[current_voice] and ties[current_voice] < 1): + if ties[current_voice] < 0: + eprint("Warning: Tie endings found without corresponding start!") + # Handles normal lyrics + try: + lyr = lyrics.barlist[lyrics_idx] + except IndexError: + break + if lyr[-1] != "command": + if lyr != 'skip': + lyr[0] = lyr[0].replace('~', chr(0x203f)) # Turns ~ into undertie + lyr[0] = lyr[0].replace('_', ' ') # Ex: Hello_I -> Hello I (but on one note) + obj["note"].add_lyric(lyr) + note_used = True + # Handles case where previous lyric is a command (ex: ["switchVoice", "Alto", "command"]) + try: + prev_lyr = lyrics.barlist[lyrics_idx-1] + except IndexError: + prev_lyr = None + if prev_lyr is not None and prev_lyr[-1] == "command": + if "ignoreMelismata" == prev_lyr[0]: + ignore_slur = prev_lyr[1] + elif "switchVoice" == prev_lyr[0]: + if prev_lyr[1] in voices: + current_voice = prev_lyr[1] + else: + eprint("Warning: Voice", prev_lyr[1], "is not a valid voice for lyric assignment!") + else: + eprint("Warning: Unknown voice command!") + lyrics_idx += 1 + else: + note_used = True # No lyric needed for slurred/tied note + else: + note_used = True # No lyric needed for non-note objects class Snippet(ScoreSection): """ Short section intended to be merged. Holds reference to the barlist to be merged into.""" + def __init__(self, name, merge_into): ScoreSection.__init__(self, name) self.merge_barlist = merge_into @@ -348,6 +515,7 @@ def __init__(self, name, merge_into): class LyricsSection(ScoreSection): """ Holds the lyrics information. Will eventually be merged to the corresponding note in the section set by the voice id. """ + def __init__(self, name, voice_id): ScoreSection.__init__(self, name) self.voice_id = voice_id @@ -355,6 +523,7 @@ def __init__(self, name, voice_id): class ScorePart(ScoreSection): """ object to keep track of part """ + def __init__(self, staves=0, part_id=None, to_part=None, name=''): ScoreSection.__init__(self, name) self.part_id = part_id @@ -391,12 +560,12 @@ def check_clef(bar): try: self.barlist[0].obj_list[0].set_time(initime, False) except AttributeError: - print("Warning can't set initial time sign!") + eprint("Warning can't set initial time sign!") if not check_clef(self.barlist[0]): try: self.barlist[0].obj_list[0].set_clef(iniclef) except AttributeError: - print("Warning can't set initial clef sign!") + eprint("Warning can't set initial clef sign!") self.barlist[0].obj_list[0].divs = divisions if self.staves: self.barlist[0].obj_list[0].staves = self.staves @@ -430,6 +599,7 @@ def extract_global_to_section(self, name): class Bar(): """ Representing the bar/measure. Contains also information about how complete it is.""" + def __init__(self): self.obj_list = [] self.list_full = False @@ -452,13 +622,14 @@ def create_backup(self): b = 0 s = 1 for obj in self.obj_list: - if isinstance(obj, BarMus): - if not obj.chord: - b += obj.duration[0] - s *= obj.duration[1] + if isinstance(obj, BarMus) and not obj.chord and (not isinstance(obj, BarNote) or obj.grace == (0, 0)): + b += obj.duration[0] + s *= obj.duration[1] elif isinstance(obj, BarBackup): - break - self.add(BarBackup((b, s))) + b -= obj.duration[0] + s /= obj.duration[1] + if b > 0: # prevents the pickup measure from already having a blank BarBackup + self.add(BarBackup((b, s))) def is_skip(self, obj_list=None): """ Check if bar has nothing but skips. """ @@ -478,7 +649,7 @@ def inject_voice(self, new_voice, override=False): """ Adding new voice to bar. Omitting double or conflicting bar attributes as long as override is false. Omitting also bars with only skips.""" - if new_voice.obj_list[0].has_attr(): + if new_voice.obj_list and new_voice.obj_list[0].has_attr(): if self.obj_list[0].has_attr(): self.obj_list[0].merge_attr(new_voice.obj_list[0], override) else: @@ -489,7 +660,7 @@ def inject_voice(self, new_voice, override=False): try: if self.obj_list[-1].barline and new_voice.obj_list[-1].barline: self.obj_list.pop() - except AttributeError: + except (AttributeError, IndexError): pass if not self.is_skip(backup_list): self.create_backup() @@ -499,17 +670,20 @@ def inject_voice(self, new_voice, override=False): class BarMus(): """ Common class for notes and rests. """ - def __init__(self, duration, voice=1): + + def __init__(self, duration, voice=1, voice_name=None): self.duration = duration self.type = None self.tuplet = [] self.dot = 0 self.voice = voice + self.voice_name = voice_name self.staff = 0 self.chord = False self.other_notation = None self.dynamic = [] self.oct_shift = None + self.harmony = [] def __repr__(self): return '<{0} {1}>'.format(self.__class__.__name__, self.duration) @@ -541,6 +715,9 @@ def set_dynamics_dashes(self, sign, before=True): def set_oct_shift(self, plac, octdir, size): self.oct_shift = OctaveShift(plac, octdir, size) + def add_harmony(self, root, root_alter=0, bass=False, bass_alter=0, text="", offset=0): + self.harmony.append(Harmony(root, root_alter, bass, bass_alter, text, offset)) + def has_attr(self): return False @@ -550,8 +727,20 @@ def has_attr(self): ## +class Harmony(): + """Class for harmony objects (chord name representations)""" + def __init__(self, root, root_alter=0, bass=False, bass_alter=0, text="", offset=0): + self.root = root + self.root_alter = root_alter + self.bass = bass + self.bass_alter = bass_alter + self.text = text + self.offset = offset + + class OctaveShift(): """Class for octave shifts.""" + def __init__(self, plac, octdir, size): self.plac = plac self.octdir = octdir @@ -560,6 +749,7 @@ def __init__(self, plac, octdir, size): class Dynamics(): """Stores information about dynamics. """ + def __init__(self, sign, before=True): self.before = before self.sign = sign @@ -587,6 +777,7 @@ class DynamicsDashes(Dynamics): class Tuplet(): """Stores information about tuplet.""" + def __init__(self, fraction, ttype, nr, acttype, normtype): self.fraction = fraction self.ttype = ttype @@ -594,11 +785,16 @@ def __init__(self, fraction, ttype, nr, acttype, normtype): self.acttype = acttype self.normtype = normtype + class Slur(): """Stores information about slur.""" - def __init__(self, nr, slurtype): + + def __init__(self, nr, slurtype, phrasing, line, grace=False): self.nr = nr self.slurtype = slurtype + self.phrasing = phrasing + self.line = line + self.grace = grace ## @@ -608,8 +804,9 @@ def __init__(self, nr, slurtype): class BarNote(BarMus): """ object to keep track of note parameters """ - def __init__(self, pitch_note, alter, accidental, duration, voice=1): - BarMus.__init__(self, duration, voice) + + def __init__(self, pitch_note, alter, accidental, duration, voice=1, voice_name=None): + BarMus.__init__(self, duration, voice, voice_name) self.base_note = pitch_note.upper() self.alter = alter self.octave = None @@ -625,10 +822,11 @@ def __init__(self, pitch_note, alter, accidental, duration, voice=1): self.adv_ornament = None self.fingering = None self.lyric = None + self.beam = False - def set_duration(self, duration, durtype=''): + def set_duration(self, duration, durtype='', dot=0): self.duration = duration - self.dot = 0 + self.dot = dot if durtype: self.type = durtype @@ -638,11 +836,11 @@ def set_durtype(self, durtype): def set_octave(self, octave): self.octave = octave - def set_tie(self, tie_type): - self.tie.append(tie_type) + def set_tie(self, tie_type, line): + self.tie.append((tie_type, line)) - def set_slur(self, nr, slur_type): - self.slur.append(Slur(nr, slur_type)) + def set_slur(self, nr, slur_type, phrasing, line, grace): + self.slur.append(Slur(nr, slur_type, phrasing, line, grace)) def add_articulation(self, art_name): self.artic.append(art_name) @@ -656,7 +854,7 @@ def add_adv_ornament(self, ornament, end_type="start"): def set_grace(self, slash): self.grace = (1, slash) - def set_gliss(self, line, endtype = "start", nr=1): + def set_gliss(self, line, endtype="start", nr=1): if not line: line = "solid" self.gliss = (line, endtype, nr) @@ -681,11 +879,15 @@ def change_lyric_syll(self, index, syll): def change_lyric_nr(self, index, nr): self.lyric[index][2] = nr + def set_beam(self, beam): + self.beam = beam + class Unpitched(BarNote): """Object to keep track of unpitched notes.""" - def __init__(self, duration, step=None, voice=1): - BarNote.__init__(self, 'B', 0, "", duration, voice=1) + + def __init__(self, duration, step=None, voice=1, voice_name=None): + BarNote.__init__(self, 'B', 0, "", duration, voice, voice_name) self.octave = 4 if step: self.base_note = step.upper() @@ -693,8 +895,9 @@ def __init__(self, duration, step=None, voice=1): class BarRest(BarMus): """ object to keep track of different rests and skips """ - def __init__(self, duration, voice=1, show_type=True, skip=False, pos=0): - BarMus.__init__(self, duration, voice) + + def __init__(self, duration, voice=1, voice_name=None, show_type=True, skip=False, pos=0): + BarMus.__init__(self, duration, voice, voice_name) self.show_type = show_type self.type = None self.skip = skip @@ -713,8 +916,33 @@ def set_durtype(self, durtype): self.type = durtype +class Ending(): + """ object that keeps track of alternate repeat endings """ + + def __init__(self, start, end, etype): + self.start = start + self.end = end + self.etype = etype + + def get_number(self): + """ A string of comma separated integers from start to end """ + number = '' + for i in range(self.start, self.end): + number += str(i) + ', ' + return number + str(self.end) + + def get_text(self): + """ 'S.' if start == end, else 'S.-E.' (where S and E are the start and end numbers) """ + text = '' + text += str(self.start) + '.' + if self.start != self.end: + text += '-' + str(self.end) + '.' + return text + + class BarAttr(): """ object that keep track of bar attributes, e.g. time sign, clef, key etc """ + def __init__(self): self.key = None self.time = 0 @@ -722,10 +950,13 @@ def __init__(self): self.mode = '' self.divs = 0 self.barline = None + self.left_barline = None self.repeat = None + self.endings = [] self.staves = 0 self.multiclef = [] self.tempo = None + self.sys_break = False def __repr__(self): return '<{0} {1}>'.format(self.__class__.__name__, self.time) @@ -736,8 +967,11 @@ def set_key(self, muskey, mode): def set_time(self, fractlist, numeric=True): self.time = fractlist - if not numeric and (fractlist == [2, 2] or fractlist == [4, 4]): - self.time.append('common') + if not numeric: + if fractlist == [4, 4]: + self.time.append('common') + elif fractlist == [2, 2]: + self.time.append('cut') def set_clef(self, clef): self.clef = clef @@ -745,9 +979,15 @@ def set_clef(self, clef): def set_barline(self, bl): self.barline = convert_barl(bl) + def set_left_barline(self, bl): + self.left_barline = convert_barl(bl) + def set_tempo(self, unit=0, unittype='', beats=0, dots=0, text=""): self.tempo = TempoDir(unit, unittype, beats, dots, text) + def add_ending(self, start, end, etype): + self.endings.append(Ending(start, end, etype)) + def has_attr(self): check = False if self.key is not None: @@ -760,6 +1000,18 @@ def has_attr(self): check = True elif self.divs != 0: check = True + elif self.barline is not None: + check = True + elif self.tempo is not None: + check = True + elif self.staves != 0: + check = True + elif self.repeat is not None: + check = True + elif self.endings: + check = True + elif self.sys_break: + check = True return check def merge_attr(self, barattr, override=False): @@ -777,16 +1029,23 @@ def merge_attr(self, barattr, override=False): self.multiclef += barattr.multiclef if barattr.tempo is not None and (override or self.tempo is None): self.tempo = barattr.tempo + if barattr.sys_break and (override or not self.sys_break): + self.sys_break = barattr.sys_break class BarBackup(): """ Object that stores duration for backup """ + def __init__(self, duration): self.duration = duration + def has_attr(self): + return False + class TempoDir(): """ Object that stores tempo direction information """ + def __init__(self, unit, unittype, beats, dots, text): if unittype: self.metr = unittype, beats @@ -822,22 +1081,25 @@ def dur2lines(dur): else: return 0 + def convert_barl(bl): if bl == '|': return 'regular' - elif bl == ':': + elif bl == ':' or bl == ';': return 'dotted' - elif bl == 'dashed': - return bl + elif bl == 'dashed' or bl == '!': + return 'dashed' elif bl == '.': return 'heavy' elif bl == '||': return 'light-light' elif bl == '.|' or bl == 'forward': return 'heavy-light' - elif bl == '.|.': + elif bl == '.|.' or bl == '..': return 'heavy-heavy' elif bl == '|.' or bl == 'backward': return 'light-heavy' elif bl == "'": return 'tick' + elif bl == "": + return 'none' diff --git a/ly/node.py b/ly/node.py index bfa19a84..ea16f0a2 100644 --- a/ly/node.py +++ b/ly/node.py @@ -58,7 +58,7 @@ class Node(object): """A list-like class to build tree structures with.""" __slots__ = ('__weakref__', '_parent', '_children') - + def __init__(self, parent=None): self._parent = None self._children = [] @@ -71,11 +71,11 @@ def _own(self, node): if parent: parent.remove(node) node._set_parent(self) - + def _set_parent(self, node): """(Internal) Set the Node (or None) as our parent.""" self._parent = node - + def parent(self): """The parent, or None if the node has no parent.""" return self._parent @@ -86,30 +86,30 @@ def index(self, node): def append(self, node): """Append a node to the current node. - + It will be reparented, that means it will be removed from it's former parent if it had one. - + """ self._own(node) self._children.append(node) - + def extend(self, iterable): """Append every Node from iterable.""" for node in iterable: self.append(node) - + def insert(self, index, node): """Insert a node at the specified index.""" self._own(node) self._children.insert(index, node) - + def insert_before(self, other, node): """Insert a node before the other node.""" i = self.index(other) self._own(node) self._children.insert(i, node) - + def remove(self, node): """Remove the given child node.""" self._children.remove(node) @@ -118,9 +118,9 @@ def remove(self, node): def __bool__(self): """We are always true.""" return True - + __nonzero__ = __bool__ # py2 compat - + def __len__(self): """Return the number of children.""" return len(self._children) @@ -172,7 +172,7 @@ def unlink(self): for node in self: node.unlink() del self._children[:] - + def replace(self, old, new): """Replace a child node with another node.""" i = self.index(old) @@ -180,13 +180,13 @@ def replace(self, old, new): def sort(self, key=None, reverse=False): """Sorts the children, optionally using the key function. - + Using a key function is recommended, or you must add comparison methods to your Node subclass. - + """ self._children.sort(key, reverse=reverse) - + def copy(self): """Return a deep copy of the node and its children """ obj = self.__class__.__new__(self.__class__) @@ -196,12 +196,12 @@ def copy(self): for n in self: obj.append(n.copy()) return obj - + def _copy_attrs(self, node): """Called by copy(); copy attributes not starting with '_'.""" for name, value in vars(self).items(): name.startswith("_") or setattr(node, name, value) - + def ancestors(self): """Climb the tree up over the parents.""" node = self.parent() @@ -211,18 +211,18 @@ def ancestors(self): def previous_sibling(self): """Return the sibling object just before us in our parents list. - + Returns None if this is the first child, or if we have no parent. - + """ for i in self.backward(): return i def next_sibling(self): """Return the sibling object just after us in our parents list. - + Returns None if this is the last child, or if we have no parent. - + """ for i in self.forward(): return i @@ -256,29 +256,29 @@ def toplevel(self): node = parent parent = node.parent() return node - - def descendants(self, depth = -1): + + def descendants(self, depth=-1): """Yield all the descendants, in tree order. Same as iter_depth().""" return self.iter_depth(depth) - - def iter_depth(self, depth = -1): + + def iter_depth(self, depth=-1): """Iterate over all the children, and their children, etc. - + Set depth to restrict the search to a certain depth, -1 is unrestricted. - + """ if depth != 0: for i in self: yield i for j in i.iter_depth(depth - 1): yield j - - def iter_rings(self, depth = -1): + + def iter_rings(self, depth=-1): """Iterate over the children in rings, depth last. - + This method returns the closest descendants first. Set depth to restrict the search to a certain depth, -1 is unrestricted. - + """ children = list(self) while children and depth: @@ -289,46 +289,46 @@ def iter_rings(self, depth = -1): newchildren.extend(i) children = newchildren - def find(self, cls, depth = -1): + def find(self, cls, depth=-1): """Yield all descendants if they are an instance of cls. - + cls may also be a tuple of classes. This method uses iter_depth(). - + """ for node in self.iter_depth(depth): if isinstance(node, cls): yield node - - def find_children(self, cls, depth = -1): + + def find_children(self, cls, depth=-1): """Yield all descendants if they are an instance of cls. - + cls may also be a tuple of classes. This method uses iter_rings(). - + """ for node in self.iter_rings(depth): if isinstance(node, cls): yield node - def find_child(self, cls, depth = -1): + def find_child(self, cls, depth=-1): """Return the first descendant that's an instance of cls. - + cls may also be a tuple of classes. This method uses iter_rings(). - + """ for node in self.iter_rings(depth): if isinstance(node, cls): return node - + def find_parent(self, cls): """Find an ancestor that's an instance of the given class. - + cls may also be a tuple of classes. - + """ for node in self.ancestors(): if isinstance(node, cls): return node - + def dump(self): """Return a string representation of the tree.""" def line(obj, indent): @@ -339,16 +339,14 @@ def line(obj, indent): return '\n'.join(line(self, 0)) - class WeakNode(Node): """A Node type using a weak reference to the parent.""" __slots__ = () + def _set_parent(self, node): self._parent = None if node is None else weakref.ref(node) - + def parent(self): """The parent, or None if the node has no parent.""" if self._parent is not None: return self._parent() - - diff --git a/ly/pitch/__init__.py b/ly/pitch/__init__.py index 48335beb..d261619b 100644 --- a/ly/pitch/__init__.py +++ b/ly/pitch/__init__.py @@ -73,11 +73,12 @@ class PitchNameNotAvailable(Exception): """Exception raised when there is no name for a pitch. - + Can occur when translating pitch names, if the target language e.g. does not have quarter-tone names. - + """ + def __init__(self, language): super(PitchNameNotAvailable, self).__init__() self.language = language @@ -85,21 +86,22 @@ def __init__(self, language): class Pitch(object): """A pitch with note, alter and octave attributes. - + Attributes may be manipulated directly. - + """ + def __init__(self, note=0, alter=0, octave=0, accidental="", octavecheck=None): self.note = note # base note (c, d, e, f, g, a, b) - # as integer (0 to 6) + # as integer (0 to 6) self.alter = alter # # = .5; b = -.5; natural = 0 self.octave = octave # '' = 2; ,, = -2 self.accidental = accidental # "", "?" or "!" self.octavecheck = octavecheck # a number is an octave check - + def __repr__(self): return ''.format(self.output()) - + def output(self, language="nederlands"): """Returns our string representation.""" res = [] @@ -110,7 +112,7 @@ def output(self, language="nederlands"): res.append('=') res.append(octaveToString(self.octavecheck)) return ''.join(res) - + @classmethod def c1(cls): """Returns a pitch c'.""" @@ -129,11 +131,11 @@ def f0(cls): def copy(self): """Returns a new instance with our attributes.""" return self.__class__(self.note, self.alter, self.octave) - + def makeAbsolute(self, lastPitch): """Makes ourselves absolute, i.e. sets our octave from lastPitch.""" self.octave += lastPitch.octave - (self.note - lastPitch.note + 3) // 7 - + def makeRelative(self, lastPitch): """Makes ourselves relative, i.e. changes our octave from lastPitch.""" self.octave -= lastPitch.octave - (self.note - lastPitch.note + 3) // 7 @@ -141,12 +143,13 @@ def makeRelative(self, lastPitch): class PitchWriter(object): language = "unknown" + def __init__(self, names, accs, replacements=()): self.names = names self.accs = accs self.replacements = replacements - def __call__(self, note, alter = 0): + def __call__(self, note, alter=0): """ Returns a string representing the pitch in our language. Raises PitchNameNotAvailable if the requested pitch @@ -171,7 +174,7 @@ def __init__(self, names, accs, replacements=()): self.accs = list(accs) self.replacements = replacements self.rx = re.compile("({0})({1})?$".format("|".join(names), - "|".join(acc for acc in accs if acc))) + "|".join(acc for acc in accs if acc))) def __call__(self, text): for s, r in self.replacements: @@ -189,22 +192,22 @@ def __call__(self, text): # HACK: were we using (rarely used) long english syntax? text = text.replace('flat', 'f').replace('sharp', 's') return False - - + + def octaveToString(octave): """Converts numeric octave to a string with apostrophes or commas. - + 0 -> "" ; 1 -> "'" ; -1 -> "," ; etc. - + """ return octave < 0 and ',' * -octave or "'" * octave def octaveToNum(octave): """Converts string octave to an integer: - + "" -> 0 ; "," -> -1 ; "'''" -> 3 ; etc. - + """ return octave.count("'") - octave.count(",") @@ -234,30 +237,30 @@ def pitchWriter(language): class PitchIterator(object): """Iterate over notes or pitches in a source.""" - + def __init__(self, source, language="nederlands"): """Initialize with a ly.document.Source. - + The language is by default set to "nederlands". - + """ self.source = source self.setLanguage(language) - + def setLanguage(self, lang): r"""Changes the pitch name language to use. - + Called internally when \language or \include tokens are encountered with a valid language name/file. - + Sets the language attribute to the language name and the read attribute to an instance of ly.pitch.PitchReader. - + """ if lang in pitchInfo.keys(): self.language = lang return True - + def tokens(self): """Yield all the tokens from the source, following the language.""" for t in self.source: @@ -271,17 +274,17 @@ def tokens(self): yield LanguageName(lang, t.pos) break yield t - + def read(self, token): """Reads the token and returns (note, alter) or None.""" return pitchReader(self.language)(token) - + def pitches(self): """Yields all tokens, but collects Note and Octave tokens. - + When a Note is encountered, also reads octave and octave check and then a Pitch is yielded instead of the tokens. - + """ tokens = self.tokens() for t in tokens: @@ -290,13 +293,13 @@ def pitches(self): if not p: break p = Pitch(*p) - + p.note_token = t p.octave_token = None p.accidental_token = None p.octavecheck_token = None - - t = None # prevent hang in this loop + + t = None # prevent hang in this loop for t in tokens: if isinstance(t, ly.lex.lilypond.Octave): p.octave = octaveToNum(t) @@ -314,21 +317,21 @@ def pitches(self): break else: yield t - + def position(self, t): """Returns the cursor position for the given token or Pitch.""" if isinstance(t, Pitch): t = t.note_token return self.source.position(t) - + def write(self, pitch, language=None): """Output a changed Pitch. - + The Pitch is written in the Source's document. - + To use this method reliably, you must instantiate the PitchIterator with a ly.document.Source that has tokens_with_position set to True. - + """ document = self.source.document pwriter = pitchWriter(language or self.language) @@ -364,5 +367,3 @@ def write(self, pitch, language=None): class LanguageName(ly.lex.Token): """A Token that denotes a language name.""" pass - - diff --git a/ly/pitch/abs2rel.py b/ly/pitch/abs2rel.py index d68b913e..9cd4f4ca 100644 --- a/ly/pitch/abs2rel.py +++ b/ly/pitch/abs2rel.py @@ -30,44 +30,44 @@ def abs2rel(cursor, language="nederlands", startpitch=True, first_pitch_absolute=False): """Converts pitches from absolute to relative. - + language: language to start reading pitch names in - + startpitch: if True, write a starting pitch before the opening bracket of a relative expression. - + first_pitch_absolute: this option only makes sense when startpitch is False. If first_pitch_absolute is True, the first pitch of a \\relative expression is written as absolute. This mimics the behaviour of LilyPond >= 2.18. (In fact, the starting pitch is then assumed to be f.) - + If False, the first pitch is written as relative to c' (LilyPond < 2.18 behaviour). - + Existing \\relative expressions are not changed. - + """ start = cursor.start cursor.start = 0 - + source = ly.document.Source(cursor, True, tokens_with_position=True) pitches = ly.pitch.PitchIterator(source, language) psource = pitches.pitches() - + if start > 0: # consume tokens before the selection, following the language t = source.consume(pitches.tokens(), start) if t: psource = itertools.chain((t,), psource) - + # this class dispatches the tokens. we can't use a generator function # as that doesn't like to be called again while there is already a body # running. class gen(object): def __iter__(self): return self - + def __next__(self): t = next(psource) while isinstance(t, (ly.lex.Space, ly.lex.Comment)): @@ -76,15 +76,15 @@ def __next__(self): relative() t = next(psource) elif isinstance(t, ly.lex.lilypond.ChordMode): - consume() # do not change chords + consume() # do not change chords t = next(psource) elif isinstance(t, ly.lex.lilypond.MarkupScore): consume() t = next(psource) return t - + next = __next__ - + tsource = gen() def getpitches(iterable): @@ -100,21 +100,21 @@ def context(): yield t if source.state.depth() < depth: return - + def consume(): """Consume tokens from context() returning the last token, if any.""" t = None for t in context(): pass return t - + def relative(): r"""Consume the whole \relative expression without doing anything. """ # skip pitch argument t = next(tsource) if isinstance(t, ly.pitch.Pitch): t = next(tsource) - + while True: # eat stuff like \new Staff == "bla" \new Voice \notes etc. if isinstance(source.state.parser(), ly.lex.lilypond.ParseTranslator): @@ -123,10 +123,10 @@ def relative(): t = next(tsource) else: break - + if t in ('{', '<<', '<'): consume() - + # Do it! with cursor.document as document: for t in tsource: @@ -169,5 +169,3 @@ def relative(): # remember the first pitch of a chord if chord == []: chord.append(p) - - diff --git a/ly/pitch/rel2abs.py b/ly/pitch/rel2abs.py index 119720d5..275ccbad 100644 --- a/ly/pitch/rel2abs.py +++ b/ly/pitch/rel2abs.py @@ -30,39 +30,39 @@ def rel2abs(cursor, language="nederlands", first_pitch_absolute=False): """Converts pitches from relative to absolute. - + language: language to start reading pitch names in - + first_pitch_absolute: if True, the first pitch of a \\relative expression is regarded as absolute, when no starting pitch was given. This mimics the behaviour of LilyPond >= 2.18. (In fact, the starting pitch is then assumed to be f.) - + If False, the starting pitch, when not given, is assumed to be c' (LilyPond < 2.18 behaviour). - + """ start = cursor.start cursor.start = 0 - + source = ly.document.Source(cursor, True, tokens_with_position=True) pitches = ly.pitch.PitchIterator(source, language) psource = pitches.pitches() - + if start > 0: # consume tokens before the selection, following the language t = source.consume(pitches.tokens(), start) if t: psource = itertools.chain((t,), psource) - + # this class dispatches the tokens. we can't use a generator function # as that doesn't like to be called again while there is already a body # running. class gen(object): def __iter__(self): return self - + def __next__(self): t = next(psource) while isinstance(t, (ly.lex.Space, ly.lex.Comment)): @@ -74,11 +74,11 @@ def __next__(self): consume() t = next(psource) return t - + next = __next__ - + tsource = gen() - + def makeAbsolute(p, lastPitch): """Makes pitch absolute (honoring and removing possible octaveCheck).""" if p.octavecheck is not None: @@ -87,7 +87,7 @@ def makeAbsolute(p, lastPitch): else: p.makeAbsolute(lastPitch) pitches.write(p) - + def getpitches(iterable): """Consumes iterable but only yields Pitch instances.""" for p in iterable: @@ -101,18 +101,18 @@ def context(): yield t if source.state.depth() < depth: return - + def consume(): """Consume tokens from context() returning the last token, if any.""" t = None for t in context(): pass return t - + def relative(t): pos = t.pos lastPitch = None - + t = next(tsource) if isinstance(t, ly.pitch.Pitch): lastPitch = t @@ -121,10 +121,10 @@ def relative(t): lastPitch = ly.pitch.Pitch.f0() else: lastPitch = ly.pitch.Pitch.c1() - + # remove the \relative tokens del document[pos:t.pos] - + while True: # eat stuff like \new Staff == "bla" \new Voice \notes etc. if isinstance(source.state.parser(), ly.lex.lilypond.ParseTranslator): @@ -133,7 +133,7 @@ def relative(t): t = next(tsource) else: break - + # now convert the relative expression to absolute if t in ('{', '<<'): # Handle full music expression { ... } or << ... >> @@ -156,7 +156,7 @@ def relative(t): for p in getpitches(context()): makeAbsolute(p, chord[-1]) chord.append(p) - lastPitch = chord[:2][-1] # same or first + lastPitch = chord[:2][-1] # same or first elif isinstance(t, ly.pitch.Pitch): makeAbsolute(t, lastPitch) lastPitch = t @@ -168,10 +168,8 @@ def relative(t): elif isinstance(t, ly.pitch.Pitch): # Handle just one pitch makeAbsolute(t, lastPitch) - + # Do it! with cursor.document as document: for t in tsource: pass - - diff --git a/ly/pitch/transform.py b/ly/pitch/transform.py index b3a44fb0..c1b8bcfd 100644 --- a/ly/pitch/transform.py +++ b/ly/pitch/transform.py @@ -27,7 +27,7 @@ def retrograde(cursor, language="nederlands"): - """Reverses pitches.""" + """Reverses pitches.""" source = ly.document.Source(cursor, True, tokens_with_position=True) pitches = ly.pitch.PitchIterator(source, language) @@ -35,18 +35,19 @@ def retrograde(cursor, language="nederlands"): plist = [p for p in psource if isinstance(p, ly.pitch.Pitch)] rlist = [r.copy() for r in reversed(plist)] - + with cursor.document as d: for p, r in zip(plist, rlist): - p.note = r.note - p.alter = r.alter - p.octave = r.octave + p.note = r.note + p.alter = r.alter + p.octave = r.octave pitches.write(p) + def inversion(cursor, language="nederlands"): """Inversion of the intervals between pitches.""" import ly.pitch.transpose - + source = ly.document.Source(cursor, True, tokens_with_position=True) pitches = ly.pitch.PitchIterator(source, language) @@ -64,9 +65,7 @@ def inversion(cursor, language="nederlands"): prev_note = p.copy() p.note = refp.note p.alter = refp.alter - p.octave = refp.octave + p.octave = refp.octave transposer.transpose(p) refp = p pitches.write(p) - - diff --git a/ly/pitch/translate.py b/ly/pitch/translate.py index 50e23804..13d8cfe8 100644 --- a/ly/pitch/translate.py +++ b/ly/pitch/translate.py @@ -29,32 +29,32 @@ def translate(cursor, language, default_language="nederlands"): r"""Changes the language of the pitch names. - + May raise ly.pitch.PitchNameNotAvailable if the current pitch language has no quarter tones. - - Returns True if there also was a \language or \include language command - that was changed. If not and the cursor specified only a part of the + + Returns True if there also was a \language or \include language command + that was changed. If not and the cursor specified only a part of the document, you could warn the user that a language or include command - should be added to the document. Or you could call insert_language to + should be added to the document. Or you could call insert_language to add a language command to the top of the document. - + """ start = cursor.start cursor.start = 0 - + source = ly.document.Source(cursor, tokens_with_position=True) pitches = ly.pitch.PitchIterator(source, default_language) tokens = pitches.tokens() writer = ly.pitch.pitchWriter(language) - + if start > 0: # consume tokens before the selection, following the language source.consume(tokens, start) cursor.start = start - - changed = False # track change of \language or \include language command + + changed = False # track change of \language or \include language command with cursor.document as d: for t in tokens: if isinstance(t, ly.lex.lilypond.Note): @@ -74,12 +74,12 @@ def translate(cursor, language, default_language="nederlands"): def insert_language(document, language, version=None): r"""Inserts a language command in the document. - + The command is inserted at the top or just below the version line. - + If the LilyPond version specified < (2, 13, 38), the \include command is used, otherwise the newer \language command. - + """ # maybe TODO: determine version automatically from document if version and version < (2, 13, 38): @@ -97,5 +97,3 @@ def insert_language(document, language, version=None): else: pos = document.size() document[pos:pos] = '\n\n' + text - - diff --git a/ly/pitch/transpose.py b/ly/pitch/transpose.py index b3b0c76a..f441ac20 100644 --- a/ly/pitch/transpose.py +++ b/ly/pitch/transpose.py @@ -30,28 +30,28 @@ class Transposer(object): """Transpose pitches. - + Instantiate with a from- and to-Pitch, and optionally a scale. The scale is a list with the pitch height of the unaltered step (0 .. 6). The default scale is the normal scale: C, D, E, F, G, A, B. - + """ scale = (0, 1, 2, Fraction(5, 2), Fraction(7, 2), Fraction(9, 2), Fraction(11, 2)) - - def __init__(self, fromPitch, toPitch, scale = None): + + def __init__(self, fromPitch, toPitch, scale=None): if scale is not None: self.scale = scale - + # the number of octaves we need to transpose self.octave = toPitch.octave - fromPitch.octave - + # the number of base note steps (c->d == 1, e->f == 1, etc.) self.steps = toPitch.note - fromPitch.note - + # the number (fraction) of real whole steps - self.alter = (self.scale[toPitch.note] + toPitch.alter - - self.scale[fromPitch.note] - fromPitch.alter) - + self.alter = (self.scale[toPitch.note] + toPitch.alter + - self.scale[fromPitch.note] - fromPitch.alter) + def transpose(self, pitch): doct, note = divmod(pitch.note + self.steps, 7) pitch.alter += self.alter - doct * 6 - self.scale[note] + self.scale[pitch.note] @@ -68,16 +68,17 @@ def transpose(self, pitch): pitch.alter += doct * -6 + self.scale[pitch.note] - self.scale[note] pitch.octave += doct pitch.note = note - - + + class Simplifier(Transposer): """Make complicated accidentals simpler by substituting naturals where possible. - + """ + def __init__(self, scale=None): if scale is not None: self.scale = scale - + def transpose(self, pitch): if pitch.alter == 1: doct, note = divmod(pitch.note + 1, 7) @@ -108,12 +109,13 @@ def transpose(self, pitch): class ModeShifter(Transposer): """ Shift pitches to optional mode/scale. - + The scale should be formatted in analogy to the scale in the Transposer parent class. - - The key should be an instance of ly.pitch.Pitch. + + The key should be an instance of ly.pitch.Pitch. """ + def __init__(self, key, scale): """ Create scale of pitches from given scale definition. @@ -123,18 +125,18 @@ def __init__(self, key, scale): self.modpitches = [0] * 7 for s, a in scale: p = key.copy() - self.steps = s + self.steps = s self.alter = a super(ModeShifter, self).transpose(p) if self.modpitches[p.note]: self.modpitches[p.note].append(p) else: self.modpitches[p.note] = [p] - + def closestPitch(self, pitch): """ - Get closest pitch from scale. - + Get closest pitch from scale. + If only one scale note with the same base step exist that is returned. Otherwise the closest is calculated. """ @@ -148,26 +150,25 @@ def getNextPitch(step, up=True): else: step = (step - 1) % 7 return getNextPitch(step, up) - + def comparePitch(pitch, uppitch, dwnpitch): upnum = self.scale[uppitch.note] + uppitch.alter dwnnum = self.scale[dwnpitch.note] + dwnpitch.alter pnum = self.scale[pitch.note] + pitch.alter if upnum - pnum < pnum - dwnnum: return uppitch - else: + else: return dwnpitch - + step = pitch.note modepitch = self.modpitches[step] if modepitch and len(modepitch) == 2: return comparePitch(pitch, modepitch[0], modepitch[1]) else: - uppitch = getNextPitch(step)[0] + uppitch = getNextPitch(step)[0] dwnpitch = getNextPitch(step, False)[-1] return comparePitch(pitch, uppitch, dwnpitch) - - + def transpose(self, pitch): """ Shift to closest scale pitch if not already in scale. @@ -185,38 +186,39 @@ def transpose(self, pitch): self.octave = 1 else: self.octave = 0 - self.alter = (self.scale[clp.note] + clp.alter - - self.scale[pitch.note] - pitch.alter) + self.alter = (self.scale[clp.note] + clp.alter + - self.scale[pitch.note] - pitch.alter) super(ModeShifter, self).transpose(pitch) class ModalTransposer(object): """Transpose pitches by number of steps within a given scale. - + Instantiate with the number of steps (+/-) in the scale to transpose by, and a mode index. The mode index is the index of the major scale in the circle of fifths (C Major = 0). - """ - def __init__(self, numSteps = 1, scaleIndex = 0): + """ + + def __init__(self, numSteps=1, scaleIndex=0): self.numSteps = numSteps self.notes = [0, 1, 2, 3, 4, 5, 6] self.alter = [-0.5, -0.5, -0.5, -0.5, -0.5, -0.5, -0.5] # Initialize to Db, then update to desired mode - + for i in range(0, scaleIndex): - keyNameIndex = ((i+1)*4)%len(self.notes) - accidentalIndex = (keyNameIndex-1)%len(self.notes) + keyNameIndex = ((i+1)*4) % len(self.notes) + accidentalIndex = (keyNameIndex-1) % len(self.notes) self.alter[accidentalIndex] += .5 - + @staticmethod def getKeyIndex(text): """Get the index of the key in the circle of fifths. - + 'Cb' returns 0, 'C' returns 7, 'B#' returns 14. """ circleOfFifths = ['Cb', 'Gb', 'Db', 'Ab', 'Eb', 'Bb', 'F', 'C', 'G', 'D', 'A', 'E', 'B', 'F#', 'C#'] return circleOfFifths.index(text.capitalize()) - + def transpose(self, pitch): # Look for an exact match: otherwise, # look for the letter name and save the accidental @@ -228,7 +230,7 @@ def transpose(self, pitch): else: fromScaleDeg = self.notes.index(pitch.note) accidental = pitch.alter - self.alter[fromScaleDeg] - + toOctaveMod, toScaleDeg = divmod(fromScaleDeg + self.numSteps, 7) pitch.note = self.notes[toScaleDeg] pitch.alter = self.alter[toScaleDeg] + accidental @@ -237,21 +239,21 @@ def transpose(self, pitch): def transpose(cursor, transposer, language="nederlands", relative_first_pitch_absolute=False): """Transpose pitches using the specified transposer. - + If relative_first_pitch_absolute is True, the first pitch in a \\relative expression is considered to be absolute, when a startpitch is not given. This is LilyPond >= 2.18 behaviour. - + If relative_first_pitch_absolute is False, the first pitch in a \\relative expression is considered to be relative to c', is no startpitch is given. This is LilyPond < 2.18 behaviour. - + Currently, relative_first_pitch_absolute defaults to False. - + """ start = cursor.start cursor.start = 0 - + source = ly.document.Source(cursor, True, tokens_with_position=True) pitches = ly.pitch.PitchIterator(source, language) @@ -260,7 +262,7 @@ def transpose(cursor, transposer, language="nederlands", relative_first_pitch_ab class gen(object): def __iter__(self): return self - + def __next__(self): while True: t = next(psource) @@ -277,7 +279,7 @@ def __next__(self): string_tuning() elif isinstance(t, ly.lex.lilypond.PitchCommand): if t == "\\transposition": - next(psource) # skip pitch + next(psource) # skip pitch elif t == "\\transpose": for p in getpitches(context()): transpose(p) @@ -288,15 +290,15 @@ def __next__(self): return t else: return t - + next = __next__ - + tsource = gen() - + def in_selection(p): """Return True if the pitch or token p may be replaced, i.e. was selected.""" return start == 0 or pitches.position(p) >= start - + def getpitches(iterable): """Consumes iterable but only yields Pitch instances.""" for p in iterable: @@ -310,15 +312,15 @@ def context(): yield t if source.state.depth() < depth: return - + def consume(): """Consume tokens from context() returning the last token, if any.""" t = None for t in context(): pass return t - - def transpose(p, resetOctave = None): + + def transpose(p, resetOctave=None): """Transpose absolute pitch, using octave if given.""" transposer.transpose(p) if resetOctave is not None: @@ -330,19 +332,19 @@ def chordmode(): r"""Called inside \chordmode or \chords.""" for p in getpitches(context()): transpose(p, 0) - + def string_tuning(): r"""Called after \stringTuning. Ignores the following chord expression.""" for t in tsource: if isinstance(t, ly.lex.lilypond.ChordStart): consume() break - + def absolute(tokens): r"""Called when outside a possible \relative environment.""" for p in getpitches(tokens): transpose(p) - + def relative(): r"""Called when \relative is encountered.""" def transposeRelative(p, lastPitch): @@ -377,8 +379,8 @@ def transposeRelative(p, lastPitch): return newLastPitch lastPitch = None - relPitch = [] # we use a list so it can be changed from inside functions - + relPitch = [] # we use a list so it can be changed from inside functions + # find the pitch after the \relative command t = next(tsource) if isinstance(t, ly.pitch.Pitch): @@ -390,7 +392,7 @@ def transposeRelative(p, lastPitch): lastPitch = ly.pitch.Pitch.f0() else: lastPitch = ly.pitch.Pitch.c1() - + while True: # eat stuff like \new Staff == "bla" \new Voice \notes etc. if isinstance(source.state.parser(), ly.lex.lilypond.ParseTranslator): @@ -399,7 +401,7 @@ def transposeRelative(p, lastPitch): t = next(tsource) else: break - + # now transpose the relative expression if t in ('{', '<<'): # Handle full music expression { ... } or << ... >> @@ -416,7 +418,7 @@ def transposeRelative(p, lastPitch): chord = [lastPitch] for p in getpitches(context()): chord.append(transposeRelative(p, chord[-1])) - lastPitch = chord[:2][-1] # same or first + lastPitch = chord[:2][-1] # same or first elif isinstance(t, ly.pitch.Pitch): lastPitch = transposeRelative(t, lastPitch) elif isinstance(t, ly.lex.lilypond.ChordStart): @@ -430,4 +432,3 @@ def transposeRelative(p, lastPitch): # Do it! with cursor.document as document: absolute(tsource) - diff --git a/ly/pkginfo.py b/ly/pkginfo.py index 02b5360c..73b1ed86 100644 --- a/ly/pkginfo.py +++ b/ly/pkginfo.py @@ -52,4 +52,3 @@ #: license license = "GPL" - diff --git a/ly/reformat.py b/ly/reformat.py index a2a6aee2..1cc1048f 100644 --- a/ly/reformat.py +++ b/ly/reformat.py @@ -21,7 +21,7 @@ Formatting tools to improve the readability of a ly.document.Document without changing the semantic meaning of the LilyPond source. -Basically the tools only change whitespace to make the source-code more +Basically the tools only change whitespace to make the source-code more readable. See also ly.indent. @@ -36,14 +36,14 @@ def break_indenters(cursor): """Add newlines around indent and dedent tokens where needed. - - If there is stuff after a { or << (that's not closed on the same line) - it is put on a new line, and if there if stuff before a } or >>, the } + + If there is stuff after a { or << (that's not closed on the same line) + it is put on a new line, and if there if stuff before a } or >>, the } or >> is put on a new line. - - It is necessary to run the indenter again over the same part of the + + It is necessary to run the indenter again over the same part of the document, as it will look garbled with the added newlines. - + """ with cursor.document as d: for b in cursor.blocks(): @@ -79,7 +79,7 @@ def move_long_comments(cursor): and isinstance(tokens[1], ( ly.lex.lilypond.LineComment, ly.lex.scheme.LineComment)) - and tokens[1][:3] in ('%%%', ';;;')): + and tokens[1][:3] in ('%%%', ';;;')): del d[d.position(b):d.position(b) + tokens[1].pos] @@ -105,6 +105,3 @@ def reformat(cursor, indenter): indenter.indent(cursor) move_long_comments(cursor) remove_trailing_whitespace(cursor) - - - diff --git a/ly/rests.py b/ly/rests.py index 1eb82bdd..10084610 100644 --- a/ly/rests.py +++ b/ly/rests.py @@ -29,6 +29,7 @@ import ly.document import ly.lex.lilypond + def replace_rest(cursor, replace_token): """Replace full rests (r) with optional token. """ source = ly.document.Source(cursor, True, tokens_with_position=True) @@ -38,6 +39,7 @@ def replace_rest(cursor, replace_token): if token == 'r': d[token.pos:token.end] = replace_token + def replace_fmrest(cursor, replace_token): """Replace full measure rests (R) with optional token. """ source = ly.document.Source(cursor, True, tokens_with_position=True) @@ -47,6 +49,7 @@ def replace_fmrest(cursor, replace_token): if token == 'R': d[token.pos:token.end] = replace_token + def replace_spacer(cursor, replace_token): """Replace spacer rests (s) with optional token. """ source = ly.document.Source(cursor, True, tokens_with_position=True) @@ -55,6 +58,7 @@ def replace_spacer(cursor, replace_token): if isinstance(token, ly.lex.lilypond.Spacer): d[token.pos:token.end] = replace_token + def replace_restcomm(cursor, replace_token): r"""Replace rests by rest command (\rest) with optional token. """ @@ -73,7 +77,7 @@ def get_comm_rests(source): rest_tokens.append(token) yield rest_tokens rest_tokens = None - + source = ly.document.Source(cursor, True, tokens_with_position=True) with cursor.document as d: for rt in get_comm_rests(source): @@ -83,4 +87,3 @@ def get_comm_rests(source): d[note.pos:note.end] = replace_token del d[space.pos:space.end] del d[comm.pos:comm.end] - diff --git a/ly/rhythm.py b/ly/rhythm.py index ec4cd861..ae6fe8d8 100644 --- a/ly/rhythm.py +++ b/ly/rhythm.py @@ -36,7 +36,7 @@ durations = ['\\maxima', '\\longa', '\\breve', - '1', '2', '4', '8', '16', '32', '64', '128', '256', '512', '1024', '2048'] + '1', '2', '4', '8', '16', '32', '64', '128', '256', '512', '1024', '2048'] def remove_dups(iterable): @@ -46,6 +46,7 @@ def remove_dups(iterable): yield '' if i == old else i old = i + # decribes a musical item that has a duration music_item = collections.namedtuple('music_item', ( 'tokens', # tokens of the item @@ -78,19 +79,20 @@ def remove_dups(iterable): ly.lex.lilypond.Tie, ) + def music_tokens(source, command=False, chord=False): r"""DEPRECATED. Yield lists of tokens describing rests, skips or pitches. - + source is a ly.document.Source instance following the state. - + The following keyword arguments can be used: - + - command: whether to allow pitches in \\relative, \\transpose, etc. - chord: whether to allow pitches inside chords. - + This function is deprecated and will be removed. You should use music_items() instead. - + """ skip_parsers = () if not command: @@ -111,41 +113,42 @@ def music_tokens(source, command=False, chord=False): break elif not isinstance(token, (ly.lex.Space, ly.lex.Numeric)): break - + while isinstance(token, _start): - l = [token] + lis = [token] for token in source: if isinstance(token, ly.lex.Space): continue if not isinstance(token, _stay): - yield l + yield lis break - l.append(token) + lis.append(token) else: - yield l + yield lis break + def music_items(cursor, command=False, chord=False, partial=ly.document.INSIDE): r"""Yield music_item instances describing rests, skips or pitches. - + cursor is a ly.document.Cursor instance. - + The following keyword arguments can be used: - + - command: whether to allow pitches in \\relative, \\transpose, etc. - chord: whether to allow pitches inside chords. - partial: ly.document.INSIDE (default), PARTIAL or OUTSIDE. See the documentation of ly.document.Source.__init__(). - + """ skip_parsers = () if not command: skip_parsers += (ly.lex.lilypond.ParsePitchCommand,) if not chord: skip_parsers += (ly.lex.lilypond.ParseChord,) - + source = ly.document.Source(cursor, True, partial=partial, tokens_with_position=True) - + def mk_item(l): """Convert a list of tokens to a music_item instance.""" tokens = [] @@ -166,36 +169,36 @@ def mk_item(l): break insert_pos = t.end return music_item(tokens, dur_tokens, may_remove, insert_pos, pos, end) - + for token in source: if isinstance(source.state.parser(), skip_parsers): continue # make sure to skip the duration tokens in a \tuplet command if token == '\\tuplet': - l = [token] + lis = [token] for token in source: if isinstance(token, ly.lex.lilypond.Duration): - l.append(token) + lis.append(token) for token in source: if not isinstance(token, ly.lex.lilypond.Duration): break - l.append(token) + lis.append(token) break elif isinstance(token, ly.lex.Numeric): - l.append(token) + lis.append(token) elif not isinstance(token, ly.lex.Space): break - yield mk_item(l) - + yield mk_item(lis) + length_seen = False while isinstance(token, _start): - l = [token] + lis = [token] if isinstance(token, ly.lex.lilypond.Length): length_seen = True for token in source: if isinstance(token, ly.lex.lilypond.Length): if length_seen is True: - yield mk_item(l) + yield mk_item(lis) length_seen = False break else: @@ -209,29 +212,31 @@ def mk_item(l): break continue elif not isinstance(token, _stay): - yield mk_item(l) + yield mk_item(lis) length_seen = False break - l.append(token) + lis.append(token) else: - yield mk_item(l) + yield mk_item(lis) break + def preceding_duration(cursor): """Return a preceding duration before the cursor, or an empty list.""" tokens = ly.document.Runner.at(cursor).backward() for t in tokens: if isinstance(t, ly.lex.lilypond.Duration): - l = [t] + lis = [t] for t in tokens: if isinstance(t, ly.lex.lilypond.Duration): - l.append(t) + lis.append(t) elif not isinstance(t, ly.lex.Space): break - l.reverse() - return l + lis.reverse() + return lis return [] + def rhythm_double(cursor): """Doubles all duration values.""" with cursor.document as d: @@ -247,6 +252,7 @@ def rhythm_double(cursor): d[token.pos:token.end] = durations[i - 1] break + def rhythm_halve(cursor): """Halves all duration values.""" with cursor.document as d: @@ -262,6 +268,7 @@ def rhythm_halve(cursor): d[token.pos:token.end] = durations[i + 1] break + def rhythm_dot(cursor): """Add a dot to all durations.""" with cursor.document as d: @@ -271,6 +278,7 @@ def rhythm_dot(cursor): d[token.end:token.end] = "." break + def rhythm_undot(cursor): """Remove one dot from all durations.""" with cursor.document as d: @@ -280,6 +288,7 @@ def rhythm_undot(cursor): del d[token.pos:token.end] break + def rhythm_remove_scaling(cursor): """Remove the scaling (like ``*3``, ``*1/3``) from all durations.""" with cursor.document as d: @@ -287,7 +296,8 @@ def rhythm_remove_scaling(cursor): for token in item.dur_tokens: if isinstance(token, ly.lex.lilypond.Scaling): del d[token.pos:token.end] - + + def rhythm_remove_fraction_scaling(cursor): """Remove the scaling containing fractions (like ``*1/3``) from all durations.""" with cursor.document as d: @@ -296,6 +306,7 @@ def rhythm_remove_fraction_scaling(cursor): if isinstance(token, ly.lex.lilypond.Scaling) and '/' in token: del d[token.pos:token.end] + def rhythm_remove(cursor): """Remove all durations.""" with cursor.document as d: @@ -303,6 +314,7 @@ def rhythm_remove(cursor): if item.dur_tokens and item.may_remove: del d[item.dur_tokens[0].pos:item.dur_tokens[-1].end] + def rhythm_implicit(cursor): """Remove reoccurring durations.""" items = music_items(cursor) @@ -319,6 +331,7 @@ def rhythm_implicit(cursor): del d[item.dur_tokens[0].pos:item.dur_tokens[-1].end] prev = item.dur_tokens + def rhythm_implicit_per_line(cursor): """Remove reoccurring durations, but always write one on a new line.""" items = music_items(cursor) @@ -331,7 +344,7 @@ def rhythm_implicit_per_line(cursor): with cursor.document as d: for item in items: if not set(item.tokens) & set(('\\tempo', '\\tuplet', '\\partial')): - block = d.block( (item.dur_tokens or item.tokens) [0].pos) + block = d.block((item.dur_tokens or item.tokens)[0].pos) if block != previous_block: if not item.dur_tokens: d[item.insert_pos:item.insert_pos] = ''.join(prev) @@ -343,6 +356,7 @@ def rhythm_implicit_per_line(cursor): del d[item.dur_tokens[0].pos:item.dur_tokens[-1].end] prev = item.dur_tokens + def rhythm_explicit(cursor): """Make all durations explicit.""" items = music_items(cursor) @@ -359,11 +373,12 @@ def rhythm_explicit(cursor): else: d[item.insert_pos:item.insert_pos] = ''.join(prev) + def rhythm_overwrite(cursor, durations): """Apply a list of durations to the cursor's range. - + The durations list looks like ["4", "8", "", "16.",] etc. - + """ durations_source = remove_dups(itertools.cycle(durations)) with cursor.document as d: @@ -372,6 +387,7 @@ def rhythm_overwrite(cursor, durations): end = item.dur_tokens[-1].end if item.dur_tokens else pos d[pos:end] = next(durations_source) + def rhythm_extract(cursor): """Return a list of the durations from the cursor's range.""" source = ly.document.Source(cursor, True) @@ -383,4 +399,3 @@ def rhythm_extract(cursor): if durations and not durations[0]: durations[0] = preceding_duration(cursor) or ['4'] return ["".join(tokens) for tokens in durations] - diff --git a/ly/server/__init__.py b/ly/server/__init__.py index 87f00ddc..3453acfa 100644 --- a/ly/server/__init__.py +++ b/ly/server/__init__.py @@ -20,4 +20,3 @@ """ A package implementing an HTTP server to process LilyPond input code. """ - diff --git a/ly/server/command.py b/ly/server/command.py index 238efed5..8347804d 100644 --- a/ly/server/command.py +++ b/ly/server/command.py @@ -42,6 +42,7 @@ 'highlight' ] + class _command(object): """Base class for commands. @@ -54,6 +55,7 @@ class _command(object): yourself. """ + def __init__(self): pass @@ -63,6 +65,7 @@ def run(self, opts, data): class set_variable(_command): """set a configuration variable to a value""" + def __init__(self, arg): self.name, self.value = arg.split('=', 1) @@ -71,7 +74,7 @@ def run(self, opts, data): ################################# -### Base classes for commands ### +# Base classes for commands # ################################# # The command classes have a run() method that serves as a common structure @@ -85,6 +88,7 @@ class _info_command(_command): The result is appended to the data['info'] array as a dict with 'command' and 'info' fields. """ + def run(self, opts, data): import ly.docinfo info = ly.docinfo.DocInfo(data['doc']['content'].document) @@ -109,10 +113,11 @@ class _edit_command(_command): added to the data['doc']['commands'] dict so it is possible to retrace which commands have been applied to the final result. """ + def run(self, opts, data): self.edit(opts, data['doc']['content']) data['doc']['commands'].append(self.__class__.__name__) - + def edit(self, opts, cursor): """Should edit the cursor in-place.""" raise NotImplementedError() @@ -124,48 +129,53 @@ class _export_command(_command): For each command an entry will be appended to data['exports'] field of the 'data' dict, leaving data['doc'] untouched. Each entry in data['exports'] has a ['doc'] and a ['command'] field, allowing the client to identify the - + """ + def run(self, opts, data): export = self.export(opts, data['doc']['content'], data['exports']) data['exports'].append({ 'command': self.__class__.__name__, 'doc': export }) - + def export(self, opts, cursor, exports): """Should return the converted document as string.""" raise NotImplementedError() ##################### -### Info commands ### +# Info commands # ##################### class mode(_info_command): """retrieve mode from document""" + def get_info(self, info): return info.mode() class version(_info_command): """retrieve version from document""" + def get_info(self, info): return info.version_string() class language(_info_command): """retrieve language from document""" + def get_info(self, info): return info.language() ##################### -### Edit commands ### +# Edit commands # ##################### class indent(_edit_command): """run the indenter""" + def indenter(self, opts): """Get a ly.indent.Indenter initialized with our options.""" import ly.indent @@ -173,13 +183,14 @@ def indenter(self, opts): i.indent_tabs = opts.indent_tabs i.indent_width = opts.indent_width return i - + def edit(self, opts, cursor): self.indenter(opts).indent(cursor) class reformat(indent): """reformat the document""" + def edit(self, opts, cursor): import ly.reformat ly.reformat.reformat(cursor, self.indenter(opts)) @@ -187,11 +198,12 @@ def edit(self, opts, cursor): class translate(_edit_command): """translate pitch names""" + def __init__(self, language): if language not in ly.pitch.pitchInfo: raise ValueError() self.language = language - + def edit(self, opts, cursor): import ly.pitch.translate try: @@ -201,10 +213,11 @@ def edit(self, opts, cursor): if not changed: version = ly.docinfo.DocInfo(cursor.document).version() ly.pitch.translate.insert_language(cursor.document, self.language, version) - + class transpose(_edit_command): """transpose music""" + def __init__(self, arg): import re result = [] @@ -226,6 +239,7 @@ def edit(self, opts, cursor): class rel2abs(_edit_command): """convert relative music to absolute""" + def edit(self, opts, cursor): import ly.pitch.rel2abs ly.pitch.rel2abs.rel2abs(cursor, opts.default_language) @@ -233,22 +247,24 @@ def edit(self, opts, cursor): class abs2rel(_edit_command): """convert absolute music to relative""" + def edit(self, opts, cursor): import ly.pitch.abs2rel ly.pitch.abs2rel.abs2rel(cursor, opts.default_language) ####################### -### Export commands ### +# Export commands # ####################### class musicxml(_export_command): """convert source to MusicXML""" + def export(self, opts, cursor, exports): import ly.musicxml writer = ly.musicxml.writer() writer.parse_document(cursor.document) - #TODO!!! + # TODO!!! # In Python3 this incorrectly escapes the \n characters, # but leaving out the str() conversion returns a Bytes object, # which will in turn trigger an "object is not JSON serializable" error @@ -257,10 +273,11 @@ def export(self, opts, cursor, exports): class highlight(_export_command): """convert source to syntax colored HTML.""" + def export(self, opts, cursor, exports): import ly.colorize w = ly.colorize.HtmlWriter() - + # set configuration options w.full_html = opts.full_html w.inline_style = opts.inline_style diff --git a/ly/server/doc.py b/ly/server/doc.py index e3c446c6..084f825e 100644 --- a/ly/server/doc.py +++ b/ly/server/doc.py @@ -36,7 +36,7 @@ -t, --timeout TIME If set, server shuts down automatically after -t seconds of inactivity NOT IMPLEMENTED YET! - + Command Options =============== @@ -67,19 +67,19 @@ As the request body it expects a single JSON string with the following elements: ``commands`` (mandatory) - An array with one or more commands to be executed subsequently. + An array with one or more commands to be executed subsequently. Each entry contains: - - ``command`` (mandatory): + + ``command`` (mandatory): A name for the command. It has to be one out of the list of available commands below. - + ``args`` (optional): If a command requires arguments (e.g. the ``transpose`` command) they are given as a single string value. - ``variables`` (optional): - A dictionary of variable assignments. Keys have to be from the + ``variables`` (optional): + A dictionary of variable assignments. Keys have to be from the list below, and proper value types are checked. If one or more variables are given they will be set *before* the command is executed. A variable may be modified again before the execution @@ -87,12 +87,12 @@ variable with a value of '' unsets the variable. ``options`` (optional) - A dictionary of option assignments. Keys have to be from the above list - of Command Options, taking the long name without the leading hyphens, + A dictionary of option assignments. Keys have to be from the above list + of Command Options, taking the long name without the leading hyphens, e.g. ``{ "encoding": "UTF-16" }``. If an option is given here it overrides the default option given on the command line, but only for the current command. - + ``data`` (mandatory) A single string containing the LilyPond input document. @@ -108,27 +108,27 @@ ``info`` An array of entries with the result of "info" commands (see below). Each entry has a ``command`` and an ``info`` field. - + ``doc`` An object with two fields: - + ``content`` A string with the content of the document with all "edit" commands applied consecutively. (If no edit commands have been specified this contains the original input. - + ``commands`` An array with the names of the commands that have been applied. - + ``exports`` An array with entries for each applied "export" command. Each entry has the following fields: - + ``doc`` A string with the content of the converted/exported document - + ``command`` - The name of the applied command + The name of the applied command Commands -------- @@ -141,23 +141,23 @@ - "export" commands that convert the input to another format. Subsequent commands are not affected by the result of export commands. - + Informative commands that return information and do not change the file: ``mode`` print the mode (guessing if not given) of the document - + ``version`` print the LilyPond version, if set in the document ``language`` print the pitch name language, if set in the document - + Commands that modify the input: ``indent`` re-indent the file - + ``reformat`` reformat the file @@ -191,10 +191,10 @@ The following variables can be set to influence the behaviour of commands. If there is a default value, it is written between brackets: - ``mode`` + ``mode`` mode of the input to read (default automatic) can be one of: lilypond, scheme, latex, html, docbook, texinfo. - + ``encoding`` [UTF-8] encoding to read (also set by -e argument) @@ -215,7 +215,7 @@ ``full-html`` [``True``] if set to True a full document with syntax-highlighted HTML will be exported, otherwise only the bare content wrapped in an - element configured by the ``wrapper-`` variables. + element configured by the ``wrapper-`` variables. ``stylesheet`` filename to reference as an external stylesheet for @@ -257,7 +257,7 @@ Specifying port and a timeout:: ly-server -p 4000 -t 5000 - + Sample Requests --------------- @@ -285,11 +285,11 @@ ], 'data' : "\\relative c' { c ( d e f ) }" } - + And a more complex example. This will first transpose the document and then convert the transposed version independently to highlighted HTML and MusicXML. Additionally it will retrieve the mode. This time the result will be in all -three places: the transposed document in ``doc.content``, the mode in +three places: the transposed document in ``doc.content``, the mode in ``info.info``, and HTML and MusicXML in ``exports[0].doc`` and ``exports[1].doc``.:: { diff --git a/ly/server/handler.py b/ly/server/handler.py index a33431ab..f2c230ee 100644 --- a/ly/server/handler.py +++ b/ly/server/handler.py @@ -33,8 +33,9 @@ # Prototype (in JavaScript sense) from which each command copies its options default_opts = None + class RequestHandler(BaseHTTPRequestHandler): - + def create_command(self, cmd): """ Parse one command from the JSON data, plus optionally some commands to @@ -42,23 +43,23 @@ def create_command(self, cmd): Raises exceptions upon faulty data. """ from . import command - + result = [] - + # set variables before executing the command if 'variables' in cmd: for v in cmd['variables']: result.append(command.set_variable( v + "=" + cmd['variables'][v])) - + # instantiate the command. - if not 'command' in cmd: - raise ValueError("Malformed JSON data in request body (missing 'command' field).\n" + - "Object:\n" + json.dumps(cmd)) + if 'command' not in cmd: + raise ValueError("Malformed JSON data in request body (missing 'command' field).\n" + + "Object:\n" + json.dumps(cmd)) cmd_name = cmd['command'].replace('-', '_') - if not cmd_name in command.known_commands: + if cmd_name not in command.known_commands: raise ValueError("unknown command: " + cmd_name) - + # add arguments to command if present. args = cmd.get('args', '') args = [args] if args else [] @@ -66,11 +67,10 @@ def create_command(self, cmd): result.append(getattr(command, cmd_name)(*args)) except TypeError as ae: raise ValueError("Error creating command {cmd} with args {args}.\n{msg}".format( - cmd = cmd_name, - args = ", ".join(args), - msg = str(ae))) + cmd=cmd_name, + args=", ".join(args), + msg=str(ae))) return result - def process_options(self, opts): """ @@ -85,8 +85,7 @@ def process_options(self, opts): else: result.set_variable(opt, opts[opt]) return result - - + def process_json_request(self, request): """ Configure the action(s) to be taken, based on the JSON object. @@ -97,12 +96,12 @@ def process_json_request(self, request): # set up an Options object and # override defaults with given options opts = self.process_options(request.get('options', [])) - + # set up commands commands = [] for c in request['commands']: - commands.extend(self.create_command(c)) - + commands.extend(self.create_command(c)) + # create document from passed data import ly.document doc = ly.document.Document(request['data'], opts.mode) @@ -117,31 +116,30 @@ def process_json_request(self, request): 'info': [], 'exports': [] } - + # run commands, which modify data in-place for c in commands: c.run(opts, data) - + data['doc']['content'] = data['doc']['content'].text() return data - def read_json_request(self): """ Returns the message body parsed to a dictionary - from JSON data. Raises + from JSON data. Raises - RuntimeWarning when no JSON data is present - ValueError when JSON parsing fails """ - + content_len = int(self.headers['content-length']) if content_len == 0: - #TODO: When testing is over remove (or comment out) + # TODO: When testing is over remove (or comment out) # the following two lines and raise the exception instead. from . import testjson return testjson.test_request raise RuntimeWarning("No JSON data in request body") - + req_body = self.rfile.read(content_len) # Python2 has string, Python3 has bytestream if not isinstance(req_body, str): @@ -152,21 +150,19 @@ def read_json_request(self): request = json.loads(req_body) except Exception as e: raise ValueError("Malformed JSON data in request body:\n" + str(e)) - if not 'commands' in request: + if 'commands' not in request: raise ValueError("Malformed JSON request. Missing 'commands' property") - if not 'data' in request: + if 'data' not in request: raise ValueError("Malformed JSON request. Missing 'data' property") return request - - ######################## - ### Request handlers ### + # Request handlers # ######################## def do_POST(self): """ - A POST request is expected to contain the task to be executed + A POST request is expected to contain the task to be executed as a JSON object in the request body. The POST handler (currently) ignores the URL. """ @@ -178,7 +174,7 @@ def do_POST(self): # use HTML templates self.send_error(400, format(e)) return - + # Send successful response self.send_response(200) self.send_header('Content-type', 'application/json') diff --git a/ly/server/main.py b/ly/server/main.py index 2cb6a3bf..2abf357e 100644 --- a/ly/server/main.py +++ b/ly/server/main.py @@ -72,15 +72,15 @@ def die(message): def parse_command_line(): """Returns a two-tuple(server_opts, cmd_opts) - + server_opts is a ServerOptions instance configuring the server behaviour cmd_opts is an Options instance with default options for future command executions triggered by HTTP requests. - + Also performs error handling and may exit on certain circumstances. - + """ - + if isinstance(sys.argv[0], type('')): # python 3 - arguments are unicode strings args = iter(sys.argv[1:]) @@ -88,17 +88,17 @@ def parse_command_line(): # python 2 - arguments are bytes, decode them fsenc = sys.getfilesystemencoding() or 'latin1' args = (a.decode(fsenc) for a in sys.argv[1:]) - + server_opts = options.ServerOptions() cmd_opts = options.Options() - + def next_arg(message): """Get the next argument, if missing, die with message.""" try: return next(args) except StopIteration: die(message) - + for arg in args: if arg in ('-h', '--help'): usage() @@ -106,13 +106,13 @@ def next_arg(message): elif arg in ('-v', '--version'): version() sys.exit(0) - + # Server Options elif arg in ('-p', '--port'): server_opts.port = int(next_arg("missing port number")) elif arg in ('-t', '--timeout'): server_opts.timeout = int(next_arg("missing timeout (in ms)")) - + # Command Options elif arg in ('-e', '--encoding'): cmd_opts.encoding = next_arg("missing encoding name") @@ -121,7 +121,7 @@ def next_arg(message): elif arg in ('-l', '--language'): s = next_arg("missing language name") cmd_opts.set_variable("default-language", s) - + # Command configuration Variables elif arg == '-d': s = next_arg("missing variable=value") @@ -130,10 +130,10 @@ def next_arg(message): except ValueError: die("missing '=' in variable set") cmd_opts.set_variable(name, value) - + elif arg.startswith('-'): die('unknown option: ' + arg) - + return server_opts, cmd_opts diff --git a/ly/server/options.py b/ly/server/options.py index ca94b273..9e14b0f5 100644 --- a/ly/server/options.py +++ b/ly/server/options.py @@ -25,18 +25,20 @@ from ly.cli import setvar + class ServerOptions(object): """ Options configuring the server itself, not the individual commands """ - + def __init__(self): self.port = 5432 self.timeout = 0 - + class Options(object): """Store all the startup options and their defaults.""" + def __init__(self): self.mode = None self.in_place = False @@ -47,11 +49,11 @@ def __init__(self): self.backup_suffix = '~' self.with_filename = None self.default_language = "nederlands" - + self.indent_width = 2 self.indent_tabs = False self.tab_width = 8 - + self.full_html = True self.inline_style = False self.stylesheet = None @@ -71,4 +73,4 @@ def set_variable(self, name, value): value = func(value) except ValueError as e: raise ValueError(format(e)) - setattr(self, name, value) + setattr(self, name, value) diff --git a/ly/server/testjson.py b/ly/server/testjson.py index c8b983b2..a57ffd7c 100644 --- a/ly/server/testjson.py +++ b/ly/server/testjson.py @@ -6,19 +6,19 @@ 'command': 'transpose', 'args': 'c d' }, - { 'command': 'reformat' }, + {'command': 'reformat'}, { 'command': 'highlight', - 'variables': { 'full-html': 'false' } + 'variables': {'full-html': 'false'} }, - { 'command': 'musicxml' }, - { 'command': 'mode' }, - { 'command': 'version' } + {'command': 'musicxml'}, + {'command': 'mode'}, + {'command': 'version'} ], 'options': { 'encoding': 'UTF-16', 'language': "deutsch" }, - 'data': "%No JSON data supplied. This is a test request.\n" + - "\\relative c' {\nc4 ( d e )\n}" + 'data': "%No JSON data supplied. This is a test request.\n" + + "\\relative c' {\nc4 ( d e )\n}" } diff --git a/ly/slexer.py b/ly/slexer.py index 4f4a5f70..1be534b8 100644 --- a/ly/slexer.py +++ b/ly/slexer.py @@ -134,45 +134,46 @@ class PString(Parser): class State(object): """Maintains state while parsing text. - + You instantiate a State object with an initial parser class. Then you use tokens(text) to start parsing for tokens. - + The state is basically a list of Parser instances; the last one is the active one. The enter() and leave() methods respectively enter a new parser or leave the current parser. - + You can't leave() the initial parser instance. - + """ + def __init__(self, initialParserClass): """Construct the State with an initial Parser instance.""" self.state = [initialParserClass()] - + def parser(self): """Return the currently active Parser instance.""" return self.state[-1] - + def parsers(self): """Return all active parsers, the most current one first.""" return self.state[::-1] - + def tokens(self, text, pos=0): """Parse a text string using our state info. - + Yields Token instances. All tokens are a subclass of str (or unicode in Python 2.x) and have a pos and an end attribute, describing their position in the original string. If the current parser defines a 'default' class attribute, it is the Token subclass to use for pieces of text that would otherwise be skipped. - + """ while True: parser = self.parser() m = parser.parse(text, pos) if m: if parser.default and pos < m.start(): - token = parser.default(text[pos:m.start()], pos) + token = parser.default(text[pos:m.start()], pos) token.update_state(self) yield token token = parser.token(m) @@ -185,72 +186,72 @@ def tokens(self, text, pos=0): token = parser.default(text[pos:], pos) token.update_state(self) yield token - + def enter(self, parser): """Enter a new parser. - + Note: 'parser' is an instantiated Parser subclass. Most times this method will be called from with the update_state() method of a Token subclass (or from a Parser subclass, which is also possible: the default implementation of Token.update_state() calls Parser.update_state(), which does nothing by default). - + E.g. in the Token subclass:: - + def update_state(self, state): state.enter(SomeDifferentParser()) - + """ self.state.append(parser) - + def leave(self): """Leave the current parser and pop back to the previous. - + The first parser (specified on instantiation) will never be left. - + """ if len(self.state) > 1: self.state.pop() - + def replace(self, parser): """Replace the current parser with a new one. - + Somewhat equivalent to:: - + state.leave() state.enter(SomeDifferentParser) - + But using this method you can also replace the first parser. - + """ self.state[-1] = parser - + def depth(self): """Return the number of parsers currently active (1 or more). - + You can use this e.g. to keep parsing until some context ends:: - + tokens = state.tokens(text) # iterator depth = state.depth() for token in tokens: if state.depth() < depth: break # do something - + """ return len(self.state) - + def follow(self, token): """Act as if the token has been instantiated with the current state. - + You need this when you already have the parsed tokens, (e.g. cached or saved somehow) but want to know which parser created them. - + This method changes state according to the token. Basically it calls the update_state() method of the token instance, but it does some more work behind the scenes to ensure that the FallthroughParser type (see below) also is handled correctly. - + """ while self.parser()._follow(token, self): pass @@ -259,80 +260,81 @@ def follow(self, token): def freeze(self): """Return the current state as a tuple (hashable object).""" return tuple((p.__class__, p.freeze()) for p in self.state) - + @classmethod def thaw(cls, frozen): """Reproduce a State object from the frozen state argument.""" state = cls.__new__(cls) state.state = [cls.thaw(attrs) for cls, attrs in frozen] return state - + class Token(str): """Represents a parsed piece of text. - + The subclass determines the type. - + You should put the regular expression string in the rx class attribute. In the rx string, you may not use named groups starting with "g\\_". - + To add token types to a Parser class, list the token class in the items attribute of the Parser class. - + """ __slots__ = ['pos', 'end'] - + rx = None - + @classmethod def test_match(cls, match): """Should return True if the match should indeed instantiate this class. - + This class method is only called if multiple Token classes in the Parser's items list have the same rx attribute. This method is then called for every matching Token subclass until one returns True. That token is then instantiated. (The method is not called for the last class in the list that have the same rx attribute, and also not if there is only one class with that rx attribute.) - + The default implementation always returns True. - + """ return True - + def __new__(cls, string, pos): token = str.__new__(cls, string) token.pos = pos token.end = pos + len(token) return token - + def update_state(self, state): """Lets the token update the state, e.g. enter a different parser. - + This method is called by the State upon instantiation of the tokens. - + Don't use it later on to have a State follow already instantiated Tokens because the FallthroughParser type can also change the state without generating a Token. Use State.follow() to have a State follow instantiated Tokens. - + The default implementation lets the Parser decide on state change. - + """ state.parser().update_state(state, self) class PatternProperty(object): """A descriptor that lazily generates a regular expression. - + The expression is based on the list of Token subclasses in the items attribute of a Parser class. Also creates an index attribute for the parser class that maps the lastindex property of a match object to the token class. - + When the pattern is requested for the first time, it is created and also written in the class, overwriting this descriptor. - + """ + def __get__(self, instance, owner): try: owner.pattern = self.pattern @@ -362,10 +364,10 @@ def __get__(self, instance, owner): class ParserMeta(type): """Metaclass for Parser subclasses. - + Adds a 'pattern' attribute with a PatternProperty() when the class also defines 'items'. - + """ def __new__(cls, name, bases, attrd): if attrd.get('items'): @@ -375,68 +377,68 @@ def __new__(cls, name, bases, attrd): class Parser(object): """Abstract base class for Parsers. - + When creating Parser subclasses, you should set the 'items' attribute to a tuple of Token subclasses. On class construction, a large regular expression pattern is built by combining the expressions from the 'rx' attributes of the Token subclasses. - + Additionally, you may implement the update_state() method which is called by the default implementation of update_state() in Token. - + """ re_flags = 0 # the re.compile flags to use - default = None # if not None, the default class for unparsed pieces of text - + default = None # if not None, the default class for unparsed pieces of text + # tuple of Token classes to look for in text items = () - + def parse(self, text, pos): """Parse text from position pos and returns a Match Object or None.""" return self.pattern.search(text, pos) - + def token(self, match): """Return a Token instance of the correct class. - + The match object is returned by the parse() method. - + """ clss = self.index[match.lastindex] for c in clss[:-1]: if c.test_match(match): return c(match.group(), match.start()) return clss[-1](match.group(), match.start()) - + def _follow(self, token, state): """(Internal) Called by State.follow(). Does nothing.""" pass - + def freeze(self): """Return our instance values as a hashable tuple.""" return () - + @classmethod def thaw(cls, attrs): return cls(*attrs) def fallthrough(self, state): """Called when no match is returned by parse(). - + If this function returns True, the tokenizer stops parsing, assuming all text has been consumed. If this function returns False or None, it should alter the state (switch parsers) and parsing continues using the new Parser. - + The default implementation simply returns True. - + """ return True def update_state(self, state, token): """Called by the default implementation of Token.update_state(). - + Does nothing by default. - + """ pass @@ -447,21 +449,22 @@ def update_state(self, state, token): class FallthroughParser(Parser): """Base class for parsers that 'match' instead of 'search' for a pattern. - + You can also implement the fallthrough() method to do something with the state if there is no match. The default is to leave the current parser. See Parser(). - + """ + def parse(self, text, pos): """Match text at position pos and returns a Match Object or None.""" return self.pattern.match(text, pos) - + def _follow(self, token, state): """(Internal) Called by State.follow(). - + Falls through if the token can't have been generated by this parser. - + """ if type(token) not in self.items: self.fallthrough(state) @@ -469,20 +472,21 @@ def _follow(self, token, state): def fallthrough(self, state): """Called when no match is returned by parse(). - + This implementation leaves the current parser and returns None (causing the State to continue parsing). - + """ state.leave() class Fridge(object): """Stores frozen States under an integer number.""" - def __init__(self, stateClass = State): + + def __init__(self, stateClass=State): self._stateClass = stateClass self._states = [] - + def freeze(self, state): """Stores a state and return an identifying integer.""" frozen = state.freeze() @@ -505,69 +509,69 @@ def count(self): def uniq(iterable): """Yields unique items from iterable.""" - seen, l = set(), 0 + seen, leng = set(), 0 for i in iterable: seen.add(i) - if len(seen) > l: + if len(seen) > leng: yield i - l = len(seen) + leng = len(seen) if __name__ == "__main__": # test class Word(Token): rx = r'\w+' - + class Number(Token): rx = r'\d+' - + class String(Token): pass - + class StringStart(String): rx = '"' + def update_state(self, state): state.enter(PString()) - + class StringEnd(String): rx = '"' + def update_state(self, state): state.leave() - + class PTest(Parser): items = ( Number, Word, StringStart, ) - + class PString(Parser): default = String items = ( StringEnd, ) - + s = State(PTest) print('test:') for t in s.tokens( 'een tekst met 7 woorden, ' 'een "tekst met 2 aanhalingstekens" ' - 'en 2 of 3 nummers'): + 'en 2 of 3 nummers'): print(t.__class__, t) print('test the Fridge:') s = State(PTest) f = Fridge() - + for t in s.tokens('text with "part of a '): print(t.__class__, t) - + n = f.freeze(s) - + # recover print('freeze and recover:') s = f.thaw(n) for t in s.tokens('quoted string" in the middle'): print(t.__class__, t) - - diff --git a/ly/util.py b/ly/util.py index 4ed12b66..5be41960 100644 --- a/ly/util.py +++ b/ly/util.py @@ -35,12 +35,13 @@ 'Twenty', 'Thirty', 'Forty', 'Fifty', 'Sixty', 'Seventy', 'Eighty', 'Ninety') + def int2text(number): """Converts an integer to the English language name of that integer. - + E.g. converts 1 to "One". Supports numbers 0 to 999999. This can be used in LilyPond identifiers (that do not support digits). - + """ result = [] if number >= 1000: @@ -60,16 +61,17 @@ def int2text(number): # Thanks: http://billmill.org/python_roman.html _roman_numerals = (("M", 1000), ("CM", 900), ("D", 500), ("CD", 400), -("C", 100), ("XC", 90), ("L", 50), ("XL", 40), ("X", 10), ("IX", 9), ("V", 5), -("IV", 4), ("I", 1)) + ("C", 100), ("XC", 90), ("L", 50), ("XL", 40), ("X", 10), ("IX", 9), ("V", 5), + ("IV", 4), ("I", 1)) + def int2roman(n): """Convert an integer value to a roman number string. - + E.g. 1 -> "I", 12 -> "XII", 2015 -> "MMXV" - + n has to be > 1. - + """ if n < 1: raise ValueError('Roman numerals must be positive integers, got %s' % n) @@ -82,13 +84,13 @@ def int2roman(n): def int2letter(number, chars=string.ascii_uppercase): """Converts an integer to one or more letters. - + E.g. 1 -> A, 2 -> B, ... 26 -> Z, 27 -> AA, etc. Zero returns the empty string. - + chars is the string to pick characters from, defaulting to string.ascii_uppercase. - + """ mod = len(chars) result = [] @@ -100,16 +102,16 @@ def int2letter(number, chars=string.ascii_uppercase): def mkid(*args): """Makes a lower-camel-case identifier of the strings in args. - + All strings are concatenated with the first character of every string uppercased, except for the first character, which is lowercased. - + Examples:: - + mkid("Violin") ==> "violin" mkid("soprano", "verse") ==> "sopranoVerse" mkid("scoreOne", "choirII") ==> "scoreOneChoirII" - + """ result = [] for a in args[:1]: @@ -119,5 +121,3 @@ def mkid(*args): result.append(a[:1].upper()) result.append(a[1:]) return "".join(result) - - diff --git a/ly/words.py b/ly/words.py index 6ce2ba31..d23c456b 100644 --- a/ly/words.py +++ b/ly/words.py @@ -30,26 +30,26 @@ 'consists', 'defaultchild', 'denies', - #'description', - #'grobdescriptions', - 'hide', # since 2.18 + # 'description', + # 'grobdescriptions', + 'hide', # since 2.18 'include', - #'invalid', + # 'invalid', 'language', 'name', - #'objectid', - 'omit', # since 2.18 + # 'objectid', + 'omit', # since 2.18 'once', 'set', 'unset', 'override', 'revert', 'remove', - 'temporary', # since 2.18 - #'sequential', - #'simultaneous', - #'type', - 'undo', # since 2.18 (not mentioned in the command index) + 'temporary', # since 2.18 + # 'sequential', + # 'simultaneous', + # 'type', + 'undo', # since 2.18 (not mentioned in the command index) 'version', 'score', 'book', @@ -64,22 +64,22 @@ lilypond_music_commands = ( - 'absolute', # since 2.18 + 'absolute', # since 2.18 'acciaccatura', - 'accidentalStyle', # since 2.16 - 'addChordShape', # since 2.16 + 'accidentalStyle', # since 2.16 + 'addChordShape', # since 2.16 'addInstrumentDefinition', 'addlyrics', 'addQuote', 'afterGrace', - #'afterGraceFraction', # this is a parser variable + # 'afterGraceFraction', # this is a parser variable 'aikenHeads', 'aikenHeadsMinor', 'allowPageTurn', - 'alterBroken', # since 2.18 (?) + 'alterBroken', # since 2.18 (?) 'alternative', - #'AncientRemoveEmptyStaffContext', - 'appendToTag', # since 2.16 + # 'AncientRemoveEmptyStaffContext', + 'appendToTag', # since 2.16 'applyContext', 'applyMusic', 'applyOutput', @@ -123,21 +123,21 @@ 'cavum', 'change', 'chordmode', - #'chordNameSeparator', - #'chordPrefixSpacer', - #'chordRootNamer', - 'chordRepeats', # since 2.16 + # 'chordNameSeparator', + # 'chordPrefixSpacer', + # 'chordRootNamer', + 'chordRepeats', # since 2.16 'chords', 'clef', 'cm', - 'compoundMeter', # since 2.16 + 'compoundMeter', # since 2.16 'compressFullBarRests', 'context', 'cr', 'cresc', 'crescHairpin', 'crescTextCresc', - 'crossStaff', # since 2.16 + 'crossStaff', # since 2.16 'cueClef', # since 2.16 'cueClefUnset', # since 2.16 'cueDuring', @@ -154,7 +154,7 @@ 'default', 'defaultNoteHeads', # since 2.16 'defaultTimeSignature', - 'defineBarLine', # since 2.18 + 'defineBarLine', # since 2.18 'deminutum', 'denies', 'descendens', @@ -251,11 +251,11 @@ 'majorSevenSymbol', 'makeClusters', 'mark', - 'markLengthOff', # since 2.18 + 'markLengthOff', # since 2.18 'markLengthOn', # since 2.18 'markup', - 'markuplines', # deprecated, till 2.14 - 'markuplist', # from 2.16 + 'markuplines', # deprecated, till 2.14 + 'markuplist', # from 2.16 'maxima', 'melisma', 'melismaEnd', @@ -317,7 +317,7 @@ 'predefinedFretboardsOff', 'predefinedFretboardsOn', 'pt', - 'pushToTag', # since 2.16 + 'pushToTag', # since 2.16 'quilisma', 'quoteDuring', 'relative', @@ -342,14 +342,14 @@ 'sff', 'sfp', 'sfz', - 'shape', # since 2.16 + 'shape', # since 2.16 'shiftDurations', 'shiftOff', 'shiftOn', 'shiftOnn', 'shiftOnnn', 'showStaffSwitch', - 'single', # since 2.18 + 'single', # since 2.18 'skip', 'skipTypesetting', 'slurDashed', @@ -383,7 +383,7 @@ 'stopStaff', 'stopTextSpan', 'stopTrillSpan', - 'stringTuning', # since 2.16 + 'stringTuning', # since 2.16 'strokeFingerOrientations', 'stropha', 'sustainOff', @@ -409,12 +409,12 @@ 'times', 'timing', 'tiny', - 'tocItem', # since ? + 'tocItem', # since ? 'transpose', 'transposedCueDuring', 'transposition', 'treCorde', - 'tuplet', # since 2.18 + 'tuplet', # since 2.18 'tupletDown', 'tupletNeutral', 'tupletUp', @@ -470,7 +470,7 @@ 'reverseturn', 'trill', ) - + fermatas = ( 'shortfermata', @@ -513,171 +513,171 @@ modes = ( - 'major', - 'minor', - 'ionian', - 'dorian', - 'phrygian', - 'lydian', + 'major', + 'minor', + 'ionian', + 'dorian', + 'phrygian', + 'lydian', 'mixolydian', - 'aeolian', - 'locrian', + 'aeolian', + 'locrian', ) markupcommands_nargs = ( -# no arguments -( - 'doubleflat', - 'doublesharp', - 'eyeglasses', - 'flat', - 'natural', - 'null', - 'semiflat', - 'semisharp', - 'sesquiflat', - 'sesquisharp', - 'sharp', - 'strut', - 'table-of-contents' -), -# one argument -( - 'backslashed-digit', - 'bold', - 'box', - 'bracket', - 'caps', - 'center-align', - 'center-column', - 'char', - 'circle', - 'column', - 'concat', - 'dir-column', - 'draw-dashed-line', # since 2.18 - 'draw-dotted-line', # since 2.18 - 'draw-line', - 'dynamic', - 'fill-line', - 'finger', - 'fontCaps', - 'fret-diagram', - 'fret-diagram-terse', - 'fret-diagram-verbose', - 'fromproperty', - 'harp-pedal', - 'hbracket', - 'hspace', - 'huge', - 'italic', - 'justify', - 'justify-field', - 'justify-string', - 'large', - 'larger', - 'left-align', - 'left-brace', - 'left-column', - 'line', - 'lookup', - 'markalphabet', - 'markletter', - 'medium', - 'musicglyph', - 'normalsize', - 'normal-size-sub', - 'normal-size-super', - 'normal-text', - 'number', - 'oval', # since 2.18 - 'postscript', - 'right-align', - 'right-brace', - 'right-column', - 'roman', - 'rounded-box', - 'sans', - 'score', - 'simple', - 'slashed-digit', - 'small', - 'smallCaps', - 'smaller', - 'stencil', - 'sub', - 'super', - 'teeny', - 'text', - 'tied-lyric', - 'tiny', - 'transparent', - 'triangle', - 'typewriter', - 'underline', - 'upright', - 'vcenter', - 'vspace', - 'verbatim-file', - 'whiteout', - 'wordwrap', - 'wordwrap-field', - 'wordwrap-string', -), -# two arguments -( - 'abs-fontsize', - 'auto-footnote', # since 2.16 - 'combine', - 'customTabClef', - 'fontsize', - 'footnote', - 'fraction', - 'halign', - 'hcenter-in', - 'lower', - 'magnify', - 'note', - 'on-the-fly', - 'override', - 'pad-around', - 'pad-markup', - 'pad-x', - 'page-link', - 'path', # added in LP 2.13.31 - 'raise', - 'rotate', - 'scale', - 'translate', - 'translate-scaled', - 'with-color', - 'with-link', - 'with-url', - 'woodwind-diagram', -), -# three arguments -( - 'arrow-head', - 'beam', - 'draw-circle', - 'eps-file', - 'filled-box', - 'general-align', - 'note-by-number', - 'pad-to-box', - 'page-ref', - 'with-dimensions', -), -# four arguments -( - 'pattern', - 'put-adjacent', -), -# five arguments, -( - 'fill-with-pattern', -), + # no arguments + ( + 'doubleflat', + 'doublesharp', + 'eyeglasses', + 'flat', + 'natural', + 'null', + 'semiflat', + 'semisharp', + 'sesquiflat', + 'sesquisharp', + 'sharp', + 'strut', + 'table-of-contents' + ), + # one argument + ( + 'backslashed-digit', + 'bold', + 'box', + 'bracket', + 'caps', + 'center-align', + 'center-column', + 'char', + 'circle', + 'column', + 'concat', + 'dir-column', + 'draw-dashed-line', # since 2.18 + 'draw-dotted-line', # since 2.18 + 'draw-line', + 'dynamic', + 'fill-line', + 'finger', + 'fontCaps', + 'fret-diagram', + 'fret-diagram-terse', + 'fret-diagram-verbose', + 'fromproperty', + 'harp-pedal', + 'hbracket', + 'hspace', + 'huge', + 'italic', + 'justify', + 'justify-field', + 'justify-string', + 'large', + 'larger', + 'left-align', + 'left-brace', + 'left-column', + 'line', + 'lookup', + 'markalphabet', + 'markletter', + 'medium', + 'musicglyph', + 'normalsize', + 'normal-size-sub', + 'normal-size-super', + 'normal-text', + 'number', + 'oval', # since 2.18 + 'postscript', + 'right-align', + 'right-brace', + 'right-column', + 'roman', + 'rounded-box', + 'sans', + 'score', + 'simple', + 'slashed-digit', + 'small', + 'smallCaps', + 'smaller', + 'stencil', + 'sub', + 'super', + 'teeny', + 'text', + 'tied-lyric', + 'tiny', + 'transparent', + 'triangle', + 'typewriter', + 'underline', + 'upright', + 'vcenter', + 'vspace', + 'verbatim-file', + 'whiteout', + 'wordwrap', + 'wordwrap-field', + 'wordwrap-string', + ), + # two arguments + ( + 'abs-fontsize', + 'auto-footnote', # since 2.16 + 'combine', + 'customTabClef', + 'fontsize', + 'footnote', + 'fraction', + 'halign', + 'hcenter-in', + 'lower', + 'magnify', + 'note', + 'on-the-fly', + 'override', + 'pad-around', + 'pad-markup', + 'pad-x', + 'page-link', + 'path', # added in LP 2.13.31 + 'raise', + 'rotate', + 'scale', + 'translate', + 'translate-scaled', + 'with-color', + 'with-link', + 'with-url', + 'woodwind-diagram', + ), + # three arguments + ( + 'arrow-head', + 'beam', + 'draw-circle', + 'eps-file', + 'filled-box', + 'general-align', + 'note-by-number', + 'pad-to-box', + 'page-ref', + 'with-dimensions', + ), + # four arguments + ( + 'pattern', + 'put-adjacent', + ), + # five arguments, + ( + 'fill-with-pattern', + ), ) @@ -708,15 +708,15 @@ 'GrandStaff', 'GregorianTranscriptionStaff', 'GregorianTranscriptionVoice', - 'KievanStaff', # since 2.16 - 'KievanVoice', # since 2.16 + 'KievanStaff', # since 2.16 + 'KievanVoice', # since 2.16 'Lyrics', 'MensuralStaff', 'MensuralVoice', 'NoteNames', 'NullVoice', # since 2.18 - 'PetrucciStaff', # since 2.16 - 'PetrucciVoice', # since 2.16 + 'PetrucciStaff', # since 2.16 + 'PetrucciVoice', # since 2.16 'PianoStaff', 'RhythmicStaff', 'Score', @@ -784,7 +784,7 @@ 'contrabass', 'tremolo strings', 'pizzicato strings', - 'orchestral harp', # till LilyPond 2.12 was this erroneously called: 'orchestral strings' + 'orchestral harp', # till LilyPond 2.12 was this erroneously called: 'orchestral strings' 'timpani', # (49-56 ensemble) 'string ensemble 1', @@ -905,7 +905,7 @@ ) -#scheme_functions = ( +# scheme_functions = ( # 'set-accidental-style', # 'set-global-staff-size', # 'set-octavation', @@ -934,7 +934,7 @@ # 'color?', # 'rgb-color', # 'x11-color', -#) +# ) scheme_values = ( @@ -982,7 +982,7 @@ 'texidoc', 'footer', ) - + papervariables = ( # fixed vertical @@ -991,7 +991,7 @@ 'bottom-margin', 'ragged-bottom', 'ragged-last-bottom', - + # horizontal 'paper-width', 'line-width', @@ -1007,17 +1007,17 @@ 'horizontal-shift', 'indent', 'short-indent', - + # flex vertical - 'markup-system-spacing', # the distance between a (title or top-level) markup and the system that follows it. + 'markup-system-spacing', # the distance between a (title or top-level) markup and the system that follows it. 'score-markup-spacing', # the distance between the last system of a score and the (title or top-level) markup that follows it. 'score-system-spacing', # the distance between the last system of a score and the first system of the score that follows it, when no (title or top-level) markup exists between them. - 'system-system-spacing', # the distance between two systems in the same score. - 'markup-markup-spacing', # the distance between two (title or top-level) markups. + 'system-system-spacing', # the distance between two systems in the same score. + 'markup-markup-spacing', # the distance between two (title or top-level) markups. 'last-bottom-spacing', # the distance from the last system or top-level markup on a page to the bottom of the printable area (i.e. the top of the bottom margin). 'top-system-spacing', # the distance from the top of the printable area (i.e. the bottom of the top margin) to the first system on a page, when there is no (title or top-level) markup between the two. 'top-markup-spacing', # the distance from the top of the printable area (i.e. the bottom of the top margin) to the first (title or top-level) markup on a page, when there is no system between the two. - + # line breaking 'max-systems-per-page', 'min-systems-per-page', @@ -1029,23 +1029,23 @@ 'blank-last-page-force', # The penalty for ending the score on an odd-numbered page. 'blank-page-force', # The penalty for having a blank page in the middle of a score. This is not used by ly:optimal-breaking since it will never consider blank pages in the middle of a score. 'page-breaking', # The page-breaking algorithm to use. Choices are ly:minimal-breaking, ly:page-turn-breaking, and ly:optimal-breaking. - 'page-breaking-system-system-spacing', # Tricks the page breaker into thinking that system-system-spacing is set to something different than it really is. For example, if page-breaking-system-system-spacing #'padding is set to something substantially larger than system-system-spacing #'padding, then the page-breaker will put fewer systems on each page. Default: unset. + 'page-breaking-system-system-spacing', # Tricks the page breaker into thinking that system-system-spacing is set to something different than it really is. For example, if page-breaking-system-system-spacing #'padding is set to something substantially larger than system-system-spacing #'padding, then the page-breaker will put fewer systems on each page. Default: unset. 'page-count', # The number of pages to be used for a score, unset by default. - + # page numbering 'auto-first-page-number', 'first-page-number', 'print-first-page-number', 'print-page-number', - + # misc 'page-spacing-weight', 'print-all-headers', 'system-separator-markup', - + # debugging 'annotate-spacing', - + # different markups 'bookTitleMarkup', 'evenFooterMarkup', @@ -1055,15 +1055,15 @@ 'scoreTitleMarkup', 'tocItemMarkup', 'tocTitleMarkup', - + # fonts 'fonts', - + # undocumented? - #'blank-after-score-page-force', - #'force-assignment', - #'input-encoding', - #'output-scale', + # 'blank-after-score-page-force', + # 'force-assignment', + # 'input-encoding', + # 'output-scale', ) @@ -1131,7 +1131,7 @@ 'varpercussion', # since 2.19 'violin', ) - + clefs = clefs_plain + ( 'treble_8', @@ -1164,5 +1164,3 @@ 'format-mark-circle-letters', 'format-mark-circle-numbers', ) - - diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..e38a4b2e --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +lxml==4.3.3 diff --git a/setup.py b/setup.py index 6f782414..bf7d016d 100644 --- a/setup.py +++ b/setup.py @@ -6,12 +6,12 @@ except ImportError: from distutils.core import setup -### python-ly +# python-ly ### -### This setup script packages and/or installs: -### - the ly package -### - the ly script -### - the ly-server script +# This setup script packages and/or installs: +# - the ly package +# - the ly script +# - the ly-server script from ly import pkginfo @@ -21,8 +21,9 @@ def packagelist(directory): """Return a sorted list with package names for all packages under the given directory.""" folder, basename = os.path.split(directory) return list(sorted(root[len(folder)+1:].replace(os.sep, '.') - for root, dirs, files in os.walk(directory) - if '__init__.py' in files)) + for root, dirs, files in os.walk(directory) + if '__init__.py' in files)) + scripts = ['bin/ly', 'bin/ly-server'] packages = packagelist('./ly') @@ -35,13 +36,13 @@ def packagelist(directory): 'ly.xml': ['*.ily', '*.ly'], } -#data_files = [ +# data_files = [ # ('share/man/man1', ['ly.1']), -#] +# ] classifiers = [ 'Development Status :: 4 - Beta', - #'Development Status :: 5 - Production/Stable', + # 'Development Status :: 5 - Production/Stable', 'License :: OSI Approved :: GNU General Public License (GPL)', 'Operating System :: MacOS :: MacOS X', 'Operating System :: Microsoft :: Windows', @@ -53,21 +54,20 @@ def packagelist(directory): ] setup( - name = pkginfo.name, - version = pkginfo.version, - description = pkginfo.description, - long_description = long_description, - maintainer = pkginfo.maintainer, - maintainer_email = pkginfo.maintainer_email, - url = pkginfo.url, - license = pkginfo.license, - - scripts = scripts, - packages = packages, - package_data = package_data, - py_modules = py_modules, - classifiers = classifiers, - #data_files = data_files, - -) + name=pkginfo.name, + version=pkginfo.version, + description=pkginfo.description, + long_description=long_description, + maintainer=pkginfo.maintainer, + maintainer_email=pkginfo.maintainer_email, + url=pkginfo.url, + license=pkginfo.license, + scripts=scripts, + packages=packages, + package_data=package_data, + py_modules=py_modules, + classifiers=classifiers, + # data_files = data_files, + +) diff --git a/tests/musicxml.xsd b/tests/musicxml.xsd old mode 100644 new mode 100755 index 48116bf7..93d402ab --- a/tests/musicxml.xsd +++ b/tests/musicxml.xsd @@ -1,27 +1,28 @@ - + - MusicXML™ W3C XML schema (XSD) + MusicXML W3C XML schema (XSD) -Version 3.0 +Version 3.1 -Copyright © 2004-2011 MakeMusic, Inc. -http://www.makemusic.com/ +Copyright © 2004-2017 the Contributors to the MusicXML Specification, published by the W3C Music Notation Community Group under the W3C Community Final Specification Agreement (FSA): -This MusicXML™ work is being provided by the copyright holder under the MusicXML Public License Version 3.0, available from: + https://www.w3.org/community/about/agreements/final/ - http://www.musicxml.org/dtds/license.html - -This is the W3C XML Schema (XSD) version of the MusicXML 3.0 language. Validation is tightened by moving MusicXML definitions from comments into schema data types and definitions. Character entities and other entity usages that are not supported in W3C XML Schema have been removed. The features of W3C XML Schema make it easier to define variations of the MusicXML format, either via extension or restriction. +A human-readable summary is available: + + https://www.w3.org/community/about/agreements/fsa-deed/ -This file defines the MusicXML 3.0 XSD, including the score-partwise and score-timewise document elements. +This is the W3C XML Schema (XSD) version of the MusicXML 3.1 language. Validation is tightened by moving MusicXML definitions from comments into schema data types and definitions. Character entities and other entity usages that are not supported in W3C XML Schema have been removed. The features of W3C XML Schema make it easier to define variations of the MusicXML format, either via extension or restriction. + +This file defines the MusicXML 3.1 XSD, including the score-partwise and score-timewise document elements. - The MusicXML 3.0 DTD has no namespace, so for compatibility the MusicXML 3.0 XSD has no namespace either. Those who need to import the MusicXML XSD into another schema are advised to create a new version that uses "http://www.musicxml.org/xsd/MusicXML" as the namespace. + The MusicXML 3.1 DTD has no namespace, so for compatibility the MusicXML 3.1 XSD has no namespace either. Those who need to import the MusicXML XSD into another schema are advised to create a new version that uses "http://www.musicxml.org/xsd/MusicXML" as the namespace. - - + + @@ -34,7 +35,7 @@ This file defines the MusicXML 3.0 XSD, including the score-partwise and score-t - + The MusicXML format supports six levels of beaming, up to 1024th notes. Unlike the number-level type, the beam-level type identifies concurrent beams in a beam group. It does not distinguish overlapping beams such as grace notes within regular notes, or beams used in different voices. @@ -44,10 +45,10 @@ This file defines the MusicXML 3.0 XSD, including the score-partwise and score-t - + - The color type indicates the color of an element. Color may be represented as hexadecimal RGB triples, as in HTML, or as hexadecimal ARGB tuples, with the A indicating alpha of transparency. An alpha value of 00 is totally transparent; FF is totally opaque. If RGB is used, the A value is assumed to be FF. + The color type indicates the color of an element. Color may be represented as hexadecimal RGB triples, as in HTML, or as hexadecimal ARGB tuples, with the A indicating alpha of transparency. An alpha value of 00 is totally transparent; FF is totally opaque. If RGB is used, the A value is assumed to be FF. For instance, the RGB value "#800080" represents purple. An ARGB value of "#40800080" would be a transparent purple. @@ -57,7 +58,7 @@ As in SVG 1.1, colors are defined in terms of the sRGB color space (IEC 61966).< - + The comma-separated-text type is used to specify a comma-separated list of text elements, as is used by the font-family attribute. @@ -66,7 +67,7 @@ As in SVG 1.1, colors are defined in terms of the sRGB color space (IEC 61966).< - + The css-font-size type includes the CSS font sizes used as an alternative to a numeric point size. @@ -88,7 +89,7 @@ As in SVG 1.1, colors are defined in terms of the sRGB color space (IEC 61966).< - + The enclosure-shape type describes the shape and presence / absence of an enclosure around text or symbols. A bracket enclosure is similar to a rectangle with the bottom line missing, as is common in jazz notation. @@ -101,10 +102,16 @@ As in SVG 1.1, colors are defined in terms of the sRGB color space (IEC 61966).< + + + + + + - + The fermata-shape type represents the shape of the fermata sign. The empty value is equivalent to the normal value. @@ -113,17 +120,22 @@ As in SVG 1.1, colors are defined in terms of the sRGB color space (IEC 61966).< + + + + + - + The font-size can be one of the CSS font sizes or a numeric point size. - + The font-style type represents a simplified version of the CSS font-style property. @@ -133,7 +145,7 @@ As in SVG 1.1, colors are defined in terms of the sRGB color space (IEC 61966).< - + The font-weight type represents a simplified version of the CSS font-weight property. @@ -143,7 +155,7 @@ As in SVG 1.1, colors are defined in terms of the sRGB color space (IEC 61966).< - + The left-center-right type is used to define horizontal alignment and text justification. @@ -154,7 +166,7 @@ As in SVG 1.1, colors are defined in terms of the sRGB color space (IEC 61966).< - + The left-right type is used to indicate whether one element appears to the left or the right of another element. @@ -164,7 +176,18 @@ As in SVG 1.1, colors are defined in terms of the sRGB color space (IEC 61966).< - + + + + The line-length type distinguishes between different line lengths for doit, falloff, plop, and scoop articulations. + + + + + + + + The line-shape type distinguishes between straight and curved lines. @@ -174,7 +197,7 @@ As in SVG 1.1, colors are defined in terms of the sRGB color space (IEC 61966).< - + The line-type type distinguishes between solid, dashed, dotted, and wavy lines. @@ -186,7 +209,7 @@ As in SVG 1.1, colors are defined in terms of the sRGB color space (IEC 61966).< - + The midi-16 type is used to express MIDI 1.0 values that range from 1 to 16. @@ -196,7 +219,7 @@ As in SVG 1.1, colors are defined in terms of the sRGB color space (IEC 61966).< - + The midi-16 type is used to express MIDI 1.0 values that range from 1 to 128. @@ -206,7 +229,7 @@ As in SVG 1.1, colors are defined in terms of the sRGB color space (IEC 61966).< - + The midi-16 type is used to express MIDI 1.0 values that range from 1 to 16,384. @@ -216,7 +239,7 @@ As in SVG 1.1, colors are defined in terms of the sRGB color space (IEC 61966).< - + The mute type represents muting for different instruments, including brass, winds, and strings. The on and off values are used for undifferentiated mutes. The remaining values represent specific mutes. @@ -239,7 +262,7 @@ As in SVG 1.1, colors are defined in terms of the sRGB color space (IEC 61966).< - + The non-negative-decimal type specifies a non-negative decimal value. @@ -248,17 +271,17 @@ As in SVG 1.1, colors are defined in terms of the sRGB color space (IEC 61966).< - + - Slurs, tuplets, and many other features can be concurrent and overlapping within a single musical part. The number-level type distinguishes up to six concurrent objects of the same type. A reading program should be prepared to handle cases where the number-levels stop in an arbitrary order. Different numbers are needed when the features overlap in MusicXML document order. When a number-level value is implied, the value is 1 by default. + Slurs, tuplets, and many other features can be concurrent and overlapping within a single musical part. The number-level type distinguishes up to six concurrent objects of the same type. A reading program should be prepared to handle cases where the number-levels stop in an arbitrary order. Different numbers are needed when the features overlap in MusicXML document order. When a number-level value is optional, the value is 1 by default. - + The number-of-lines type is used to specify the number of lines in text decoration attributes. @@ -268,7 +291,7 @@ As in SVG 1.1, colors are defined in terms of the sRGB color space (IEC 61966).< - + The number-or-normal values can be either a decimal number or the string "normal". This is used by the line-height and letter-spacing attributes. @@ -281,7 +304,7 @@ As in SVG 1.1, colors are defined in terms of the sRGB color space (IEC 61966).< - + The over-under type is used to indicate whether the tips of curved lines such as slurs and ties are overhand (tips down) or underhand (tips up). @@ -291,7 +314,7 @@ As in SVG 1.1, colors are defined in terms of the sRGB color space (IEC 61966).< - + The percent type specifies a percentage from 0 to 100. @@ -301,7 +324,7 @@ As in SVG 1.1, colors are defined in terms of the sRGB color space (IEC 61966).< - + The positive-decimal type specifies a positive decimal value. @@ -310,7 +333,7 @@ As in SVG 1.1, colors are defined in terms of the sRGB color space (IEC 61966).< - + The positive-divisions type restricts divisions values to positive numbers. @@ -319,7 +342,7 @@ As in SVG 1.1, colors are defined in terms of the sRGB color space (IEC 61966).< - + The positive-integer-or-empty values can be either a positive integer or an empty string. @@ -332,7 +355,7 @@ As in SVG 1.1, colors are defined in terms of the sRGB color space (IEC 61966).< - + The rotation-degrees type specifies rotation, pan, and elevation values in degrees. Values range from -180 to 180. @@ -342,7 +365,7 @@ As in SVG 1.1, colors are defined in terms of the sRGB color space (IEC 61966).< - + The semi-pitched type represents categories of indefinite pitch for percussion instruments. @@ -356,7 +379,59 @@ As in SVG 1.1, colors are defined in terms of the sRGB color space (IEC 61966).< - + + + + The smufl-glyph-name type is used for attributes that reference a specific Standard Music Font Layout (SMuFL) character. The value is a SMuFL canonical glyph name, not a code point. For instance, the value for a standard piano pedal mark would be keyboardPedalPed, not U+E650. + + + + + + + The smufl-accidental-glyph-name type is used to reference a specific Standard Music Font Layout (SMuFL) accidental character. The value is a SMuFL canonical glyph name that starts with acc. + + + + + + + + + The smufl-coda-glyph-name type is used to reference a specific Standard Music Font Layout (SMuFL) coda character. The value is a SMuFL canonical glyph name that starts with coda. + + + + + + + + + The smufl-lyrics-glyph-name type is used to reference a specific Standard Music Font Layout (SMuFL) lyrics elision character. The value is a SMuFL canonical glyph name that starts with lyrics. + + + + + + + + + The smufl-pictogram-glyph-name type is used to reference a specific Standard Music Font Layout (SMuFL) percussion pictogram character. The value is a SMuFL canonical glyph name that starts with pict. + + + + + + + + + The smufl-segno-glyph-name type is used to reference a specific Standard Music Font Layout (SMuFL) segno character. The value is a SMuFL canonical glyph name that starts with segno. + + + + + + The start-note type describes the starting note of trills and mordents for playback, relative to the current note. @@ -367,11 +442,11 @@ As in SVG 1.1, colors are defined in terms of the sRGB color space (IEC 61966).< - + The start-stop type is used for an attribute of musical elements that can either start or stop, such as tuplets. - + The values of start and stop refer to how an element appears in musical score order, not in MusicXML document order. An element with a stop attribute may precede the corresponding element with a start attribute within a MusicXML document. This is particularly common in multi-staff music. For example, the stopping point for a tuplet may appear in staff 1 before the starting point for the tuplet appears in staff 2 later in the document. @@ -383,7 +458,7 @@ The values of start and stop refer to how an element appears in musical score or The start-stop-continue type is used for an attribute of musical elements that can either start or stop, but also need to refer to an intermediate point in the symbol, as for complex slurs or for formatting of symbols across system breaks. - + The values of start, stop, and continue refer to how an element appears in musical score order, not in MusicXML document order. An element with a stop attribute may precede the corresponding element with a start attribute within a MusicXML document. This is particularly common in multi-staff music. For example, the stopping point for a slur may appear in staff 1 before the starting point for the slur appears in staff 2 later in the document. @@ -395,7 +470,7 @@ The values of start, stop, and continue refer to how an element appears in music - The start-stop-single type is used for an attribute of musical elements that can be used for either multi-note or single-note musical elements, as for tremolos. + The start-stop-single type is used for an attribute of musical elements that can be used for either multi-note or single-note musical elements, as for groupings. @@ -403,21 +478,22 @@ The values of start, stop, and continue refer to how an element appears in music - + - The string-number type indicates a string number. Strings are numbered from high to low, with 1 being the highest pitched string. + The string-number type indicates a string number. Strings are numbered from high to low, with 1 being the highest pitched full-length string. - The symbol-size type is used to indicate full vs. cue-sized vs. oversized symbols. The large value for oversized symbols was added in version 1.1. + The symbol-size type is used to distinguish between full, cue sized, grace cue sized, and oversized symbols. + @@ -443,6 +519,20 @@ Distances in a MusicXML file are measured in tenths of staff space. Tenths are t + + + The tied-type type is used as an attribute of the tied element to specify where the visual representation of a tie begins and ends. A tied element which joins two notes of the same pitch can be specified with tied-type start on the first note and tied-type stop on the second note. To indicate a note should be undamped, use a single tied element with tied-type let-ring. For other ties that are visually attached to a single note, such as a tie leading into or out of a repeated section or coda, use two tied elements on the same note, one start and one stop. + +In start-stop cases, ties can add more elements using a continue type. This is typically used to specify the formatting of cross-system ties. + + + + + + + + + The time-only type is used to indicate that a particular playback-related element only applies particular times through a repeated section. The value is a comma-separated list of positive integers arranged in ascending order, indicating which times through the repeated section that the element applies. @@ -461,7 +551,19 @@ Distances in a MusicXML file are measured in tenths of staff space. Tenths are t - + + + + The tremolo-type is used to distinguish multi-note, single-note, and unmeasured tremolos. + + + + + + + + + The trill-beats type specifies the beats used in a trill-sound or bend-sound attribute group. It is a decimal value with a minimum value of 2. @@ -470,7 +572,7 @@ Distances in a MusicXML file are measured in tenths of staff space. Tenths are t - + The trill-step type describes the alternating note of trills and mordents for playback, relative to the current note. @@ -481,7 +583,7 @@ Distances in a MusicXML file are measured in tenths of staff space. Tenths are t - + The two-note-turn type describes the ending notes of trills and mordents for playback, relative to the current note. @@ -492,7 +594,7 @@ Distances in a MusicXML file are measured in tenths of staff space. Tenths are t - + The up-down type is used for the direction of arrows and other pointed symbols like vertical accents, indicating which way the tip is pointing. @@ -512,7 +614,7 @@ Distances in a MusicXML file are measured in tenths of staff space. Tenths are t - + The valign type is used to indicate vertical alignment to the top, middle, bottom, or baseline of the text. Defaults are implementation-dependent. @@ -524,7 +626,7 @@ Distances in a MusicXML file are measured in tenths of staff space. Tenths are t - + The valign-image type is used to indicate vertical alignment for images and graphics, so it does not include a baseline value. Defaults are implementation-dependent. @@ -535,7 +637,7 @@ Distances in a MusicXML file are measured in tenths of staff space. Tenths are t - + The yes-no type is used for boolean-like attributes. We cannot use W3C XML Schema booleans due to their restrictions on expression of boolean values. @@ -561,9 +663,9 @@ Distances in a MusicXML file are measured in tenths of staff space. Tenths are t - + - + The cancel-location type is used to indicate where a key signature cancellation appears relative to a new key signature: to the left, to the right, or before the barline and to the left. It is left by default. For mid-measure key elements, a cancel-location of before-barline should be treated like a cancel-location of left. @@ -574,7 +676,7 @@ Distances in a MusicXML file are measured in tenths of staff space. Tenths are t - + The clef-sign element represents the different clef symbols. The jianpu sign indicates that the music that follows should be in jianpu numbered notation, just as the TAB sign indicates that the music that follows should be in tablature notation. Unlike TAB, a jianpu sign does not correspond to a visual clef notation. @@ -589,21 +691,21 @@ Distances in a MusicXML file are measured in tenths of staff space. Tenths are t - + The fifths type represents the number of flats or sharps in a traditional key signature. Negative numbers are used for flats and positive numbers for sharps, reflecting the key's placement within the circle of fifths (hence the type name). - + The mode type is used to specify major/minor and other mode distinctions. Valid mode values include major, minor, dorian, phrygian, lydian, mixolydian, aeolian, ionian, locrian, and none. - + The show-frets type indicates whether to show tablature frets as numbers (0, 1, 2) or letters (a, b, c). The default choice is numbers. @@ -613,21 +715,21 @@ Distances in a MusicXML file are measured in tenths of staff space. Tenths are t - + The staff-line type indicates the line on a given staff. Staff lines are numbered from bottom to top, with 1 being the bottom line on a staff. Staff line values can be used to specify positions outside the staff, such as a C clef positioned in the middle of a grand staff. - + The staff-number type indicates staff numbers within a multi-staff part. Staves are numbered from top to bottom, with 1 being the top staff on a part. - + The staff-type value can be ossia, cue, editorial, regular, or alternate. An alternate staff indicates one that shares the same musical data as the prior staff, but displayed differently (e.g., treble and bass clef, standard notation and tab). @@ -640,7 +742,7 @@ Distances in a MusicXML file are measured in tenths of staff space. Tenths are t - + The time-relation type indicates the symbol used to represent the interchangeable aspect of dual time signatures. @@ -654,7 +756,7 @@ Distances in a MusicXML file are measured in tenths of staff space. Tenths are t - + The time-separator type indicates how to display the arrangement between the beats and beat-type values in a time signature. The default value is none. The horizontal, diagonal, and vertical values represent horizontal, diagonal lower-left to upper-right, and vertical lines respectively. For these values, the beats and beat-type values are arranged on either side of the separator line. The none value represents no separator with the beats and beat-type arranged vertically. The adjacent value represents no separator with the beats and beat-type arranged horizontally. @@ -667,7 +769,7 @@ Distances in a MusicXML file are measured in tenths of staff space. Tenths are t - + The time-symbol type indicates how to display a time signature. The normal value is the usual fractional display, and is the implied symbol type if none is specified. Other options are the common and cut time symbols, as well as a single number with an implied denominator. The note symbol indicates that the beat-type should be represented with the corresponding downstem note rather than a number. The dotted-note symbol indicates that the beat-type should be represented with a dotted downstem note that corresponds to three times the beat-type value, and a numerator that is one third the beats value. @@ -681,9 +783,9 @@ Distances in a MusicXML file are measured in tenths of staff space. Tenths are t - + - + The backward-forward type is used to specify repeat directions. The start of the repeat has a forward direction while the end of the repeat has a backward direction. @@ -693,7 +795,7 @@ Distances in a MusicXML file are measured in tenths of staff space. Tenths are t - + The bar-style type represents barline style information. Choices are regular, dotted, dashed, heavy, light-light, light-heavy, heavy-light, heavy-heavy, tick (a short stroke through the top line), short (a partial barline between the 2nd and 4th lines), and none. @@ -712,7 +814,7 @@ Distances in a MusicXML file are measured in tenths of staff space. Tenths are t - + The ending-number type is used to specify either a comma-separated list of positive integers without leading zeros, or a string of zero or more spaces. It is used for the number attribute of the ending element. The zero or more spaces version is used when software knows that an ending is present, but cannot determine the type of the ending. @@ -721,7 +823,7 @@ Distances in a MusicXML file are measured in tenths of staff space. Tenths are t - + The right-left-middle type is used to specify barline location. @@ -732,7 +834,7 @@ Distances in a MusicXML file are measured in tenths of staff space. Tenths are t - + The start-stop-discontinue type is used to specify ending types. Typically, the start type is associated with the left barline of the first measure in an ending. The stop and discontinue types are associated with the right barline of the last measure in an ending. Stop is used when the ending mark concludes with a downward jog, as is typical for first endings. Discontinue is used when there is no downward jog, as is typical for second endings that do not conclude a piece. @@ -743,7 +845,7 @@ Distances in a MusicXML file are measured in tenths of staff space. Tenths are t - + The winged attribute indicates whether the repeat has winged extensions that appear above and below the barline. The straight and curved values represent single wings, while the double-straight and double-curved values represent double wings. The none value indicates no wings and is the default. @@ -758,17 +860,17 @@ Distances in a MusicXML file are measured in tenths of staff space. Tenths are t - + - The accordion-middle type may have values of 1, 2, or 3, corresponding to having 1 to 3 dots in the middle section of the accordion registration symbol. + The accordion-middle type may have values of 1, 2, or 3, corresponding to having 1 to 3 dots in the middle section of the accordion registration symbol. This type is not used if no dots are present. - + The beater-value type represents pictograms for beaters, mallets, and sticks that do not have different materials represented in the pictogram. The finger and hammer values are in addition to Stone's list. @@ -777,6 +879,7 @@ Distances in a MusicXML file are measured in tenths of staff space. Tenths are t + @@ -786,14 +889,16 @@ Distances in a MusicXML file are measured in tenths of staff space. Tenths are t + + - + The degree-symbol-value type indicates indicates that a symbol should be used in specifying the degree. @@ -806,7 +911,7 @@ Distances in a MusicXML file are measured in tenths of staff space. Tenths are t - + The degree-type-value type indicates whether the current degree element is an addition, alteration, or subtraction to the kind of the current chord in the harmony element. @@ -817,10 +922,10 @@ Distances in a MusicXML file are measured in tenths of staff space. Tenths are t - + - The effect type represents pictograms for sound effect percussion instruments. The cannon value is in addition to Stone's list. + The effect type represents pictograms for sound effect percussion instruments. The cannon, lotus flute, and megaphone values are in addition to Stone's list. @@ -831,6 +936,8 @@ Distances in a MusicXML file are measured in tenths of staff space. Tenths are t + + @@ -839,16 +946,18 @@ Distances in a MusicXML file are measured in tenths of staff space. Tenths are t - - + + - The glass type represents pictograms for glass percussion instruments. + The glass-value type represents pictograms for glass percussion instruments. + + - + The harmony-type type differentiates different types of harmonies when alternate harmonies are possible. Explicit harmonies have all note present in the music; implied have some notes missing but implied; alternate represents alternate analyses. @@ -859,11 +968,11 @@ Distances in a MusicXML file are measured in tenths of staff space. Tenths are t - + A kind-value indicates the type of chord. Degree elements can then add, subtract, or alter from these starting points. Values include: - + Triads: major (major third, perfect fifth) minor (minor third, perfect fifth) @@ -904,7 +1013,7 @@ Other: pedal (pedal-point bass) power (perfect fifth) Tristan - + The "other" kind is used when the harmony is entirely composed of add elements. The "none" kind is used to explicitly encode absence of chords or functional harmony. @@ -943,7 +1052,7 @@ The "other" kind is used when the harmony is entirely composed of add elements. - + The line-end type specifies if there is a jog up or down (or both), an arrow, or nothing at the start or end of a bracket. @@ -956,7 +1065,7 @@ The "other" kind is used when the harmony is entirely composed of add elements. - + The measure-numbering-value type describes how measure numbers are displayed on this part: no numbers, numbers every measure, or numbers every system. @@ -967,36 +1076,45 @@ The "other" kind is used when the harmony is entirely composed of add elements. - + - The membrane type represents pictograms for membrane percussion instruments. The goblet drum value is in addition to Stone's list. + The membrane type represents pictograms for membrane percussion instruments. + + + + + - + The metal type represents pictograms for metal percussion instruments. The hi-hat value refers to a pictogram like Stone's high-hat cymbals but without the long vertical line at the bottom. + + + + @@ -1009,16 +1127,21 @@ The "other" kind is used when the harmony is entirely composed of add elements. + + + + + - + The on-off type is used for notation elements such as string mutes. @@ -1028,22 +1151,39 @@ The "other" kind is used when the harmony is entirely composed of add elements. + + + + The pedal-type simple type is used to distinguish types of pedal directions. The start value indicates the start of a damper pedal, while the sostenuto value indicates the start of a sostenuto pedal. The change, continue, and stop values can be used with either the damper or sostenuto pedal. The soft pedal is not included here because there is no special symbol or graphic used for it beyond what can be specified with words and bracket elements. + + + + + + + + + - + - The pitched type represents pictograms for pitched percussion instruments. The chimes and tubular chimes values distinguish the single-line and double-line versions of the pictogram. The mallet value is in addition to Stone's list. + The pitched-value type represents pictograms for pitched percussion instruments. The chimes and tubular chimes values distinguish the single-line and double-line versions of the pictogram. + + + + - + The principal-voice-symbol type represents the type of symbol used to indicate the start of a principal or secondary voice. The "plain" value represents a plain square bracket. The value of "none" is used for analysis markup when the principal-voice element does not have a corresponding appearance in the score. @@ -1055,7 +1195,18 @@ The "other" kind is used when the harmony is entirely composed of add elements. - + + + + The staff-divide-symbol type is used for staff division symbols. The down, up, and up-down values correspond to SMuFL code points U+E00B, U+E00C, and U+E00D respectively. + + + + + + + + The start-stop-change-continue type is used to distinguish types of pedal directions. @@ -1067,7 +1218,7 @@ The "other" kind is used when the harmony is entirely composed of add elements. - + The tip-direction type represents the direction in which the tip of a stick or beater points, using Unicode arrow terminology. @@ -1083,7 +1234,7 @@ The "other" kind is used when the harmony is entirely composed of add elements. - + The stick-location type represents pictograms for the location of sticks, beaters, or mallets on cymbals, gongs, drums, and other instruments. @@ -1095,7 +1246,7 @@ The "other" kind is used when the harmony is entirely composed of add elements. - + The stick-material type represents the material being displayed in a stick pictogram. @@ -1108,7 +1259,7 @@ The "other" kind is used when the harmony is entirely composed of add elements. - + The stick-type type represents the shape of pictograms where the material @@ -1117,12 +1268,17 @@ The "other" kind is used when the harmony is entirely composed of add elements. + + + + + - + The up-down-stop-continue type is used for octave-shift elements, indicating the direction of the shift from their true pitched values because of printing difficulty. @@ -1134,7 +1290,7 @@ The "other" kind is used when the harmony is entirely composed of add elements. - + The wedge type is crescendo for the start of a wedge that is closed at the left side, diminuendo for the start of a wedge that is closed on the right side, and stop for the end of a wedge. The continue type is used for formatting wedges over a system break, or for other situations where a single wedge is divided into multiple segments. @@ -1146,41 +1302,57 @@ The "other" kind is used when the harmony is entirely composed of add elements. - + - The wood type represents pictograms for wood percussion instruments. The maraca and maracas values distinguish the one- and two-maraca versions of the pictogram. The vibraslap and castanets values are in addition to Stone's list. + The wood type represents pictograms for wood percussion instruments. The maraca and maracas values distinguish the one- and two-maraca versions of the pictogram. + + + + + + + - + - + - The distance-type defines what type of distance is being defined in a distance element. Values include beam and hyphen. This is left as a string so that other application-specific types can be defined, but it is made a separate type so that it can be redefined more strictly. + The distance-type defines what type of distance is being defined in a distance element. Values include beam and hyphen. This is left as a string so that other application-specific types can be defined, but it is made a separate type so that it can be redefined more strictly. + + + + + + + The glyph-type defines what type of glyph is being defined in a glyph element. Values include quarter-rest, g-clef-ottava-bassa, c-clef, f-clef, percussion-clef, octave-shift-up-8, octave-shift-down-8, octave-shift-continue-8, octave-shift-down-15, octave-shift-up-15, octave-shift-continue-15, octave-shift-down-22, octave-shift-up-22, and octave-shift-continue-22. This is left as a string so that other application-specific types can be defined, but it is made a separate type so that it can be redefined more strictly. + +A quarter-rest type specifies the glyph to use when a note has a rest element and a type value of quarter. The c-clef, f-clef, and percussion-clef types specify the glyph to use when a clef sign element value is C, F, or percussion respectively. The g-clef-ottava-bassa type specifies the glyph to use when a clef sign element value is G and the clef-octave-change element value is -1. The octave-shift types specify the glyph to use when an octave-shift type attribute value is up, down, or continue and the octave-shift size attribute value is 8, 15, or 22. - The line-width-type defines what type of line is being defined in a line-width element. Values include beam, bracket, dashes, enclosure, ending, extend, heavy barline, leger, light barline, octave shift, pedal, slur middle, slur tip, staff, stem, tie middle, tie tip, tuplet bracket, and wedge. This is left as a string so that other application-specific types can be defined, but it is made a separate type so that it can be redefined more strictly. + The line-width-type defines what type of line is being defined in a line-width element. Values include beam, bracket, dashes, enclosure, ending, extend, heavy barline, leger, light barline, octave shift, pedal, slur middle, slur tip, staff, stem, tie middle, tie tip, tuplet bracket, and wedge. This is left as a string so that other application-specific types can be defined, but it is made a separate type so that it can be redefined more strictly. @@ -1202,23 +1374,24 @@ The "other" kind is used when the harmony is entirely composed of add elements. - + - The note-size-type type indicates the type of note being defined by a note-size element. The grace type is used for notes of cue size that that include a grace element. The cue type is used for all other notes with cue size, whether defined explicitly or implicitly via a cue element. The large type is used for notes of large size. + The note-size-type type indicates the type of note being defined by a note-size element. The grace-cue type is used for notes of grace-cue size. The grace type is used for notes of cue size that include a grace element. The cue type is used for all other notes with cue size, whether defined explicitly or implicitly via a cue element. The large type is used for notes of large size. + - + - The accidental-value type represents notated accidentals supported by MusicXML. In the MusicXML 2.0 DTD this was a string with values that could be included. The XSD strengthens the data typing to an enumerated list. The quarter- and three-quarters- accidentals are Tartini-style quarter-tone accidentals. The -down and -up accidentals are quarter-tone accidentals that include arrows pointing down or up. The slash- accidentals are used in Turkish classical music. The numbered sharp and flat accidentals are superscripted versions of the accidental signs, used in Turkish folk music. The sori and koron accidentals are microtonal sharp and flat accidentals used in Iranian and Persian music. + The accidental-value type represents notated accidentals supported by MusicXML. In the MusicXML 2.0 DTD this was a string with values that could be included. The XSD strengthens the data typing to an enumerated list. The quarter- and three-quarters- accidentals are Tartini-style quarter-tone accidentals. The -down and -up accidentals are quarter-tone accidentals that include arrows pointing down or up. The slash- accidentals are used in Turkish classical music. The numbered sharp and flat accidentals are superscripted versions of the accidental signs, used in Turkish folk music. The sori and koron accidentals are microtonal sharp and flat accidentals used in Iranian and Persian music. The other accidental covers accidentals other than those listed here. It is usually used in combination with the smufl attribute to specify a particular SMuFL accidental. The smufl attribute may be used with any accidental value to help specify the appearance of symbols that share the same MusicXML semantics. @@ -1239,6 +1412,12 @@ The "other" kind is used when the harmony is entirely composed of add elements. + + + + + + @@ -1255,9 +1434,10 @@ The "other" kind is used when the harmony is entirely composed of add elements. + - + The arrow-direction type represents the direction in which an arrow points, using Unicode arrow terminology. @@ -1278,7 +1458,7 @@ The "other" kind is used when the harmony is entirely composed of add elements. - + The arrow-style type represents the style of an arrow, using Unicode arrow terminology. Filled and hollow arrows indicate polygonal single arrows. Paired arrows are duplicate single arrows in the same direction. Combined arrows apply to double direction arrows like left right, indicating that an arrow in one direction should be combined with an arrow in the other direction. @@ -1293,7 +1473,7 @@ The "other" kind is used when the harmony is entirely composed of add elements. - + The beam-value type represents the type of beam associated with each of 8 beam levels (up to 1024th notes) available for each note. @@ -1306,7 +1486,7 @@ The "other" kind is used when the harmony is entirely composed of add elements. - + The breath-mark-value type represents the symbol used for a breath mark. @@ -1315,6 +1495,22 @@ The "other" kind is used when the harmony is entirely composed of add elements. + + + + + + + + The caesura-value type represents the shape of the caesura sign. + + + + + + + + @@ -1327,7 +1523,7 @@ The "other" kind is used when the harmony is entirely composed of add elements. - + The fan type represents the type of beam fanning present on a note, used to represent accelerandos and ritardandos. @@ -1338,12 +1534,13 @@ The "other" kind is used when the harmony is entirely composed of add elements. - + The handbell-value type represents the type of handbell technique being notated. + @@ -1357,7 +1554,30 @@ The "other" kind is used when the harmony is entirely composed of add elements. - + + + + The harmon-closed-location type indicates which portion of the symbol is filled in when the corresponding harmon-closed-value is half. + + + + + + + + + + + + The harmon-closed-value type represents whether the harmon mute is closed, open, or half-open. + + + + + + + + The hole-closed-location type indicates which portion of the hole is filled in when the corresponding hole-closed-value is half. @@ -1369,7 +1589,7 @@ The "other" kind is used when the harmony is entirely composed of add elements. - + The hole-closed-value type represents whether the hole is closed, open, or half-open. @@ -1380,7 +1600,7 @@ The "other" kind is used when the harmony is entirely composed of add elements. - + The note-type type is used for the MusicXML type element and represents the graphic note type, from 1024th (shortest) to maxima (longest). @@ -1402,13 +1622,17 @@ The "other" kind is used when the harmony is entirely composed of add elements. - + -The notehead type indicates shapes other than the open and closed ovals associated with note durations. The values do, re, mi, fa, fa up, so, la, and ti correspond to Aikin's 7-shape system. The fa up shape is typically used with upstems; the fa shape is typically used with downstems or no stems. +The notehead-value type indicates shapes other than the open and closed ovals associated with note durations. -The arrow shapes differ from triangle and inverted triangle by being centered on the stem. Slashed and back slashed notes include both the normal notehead and a slash. The triangle shape has the tip of the triangle pointing up; the inverted triangle shape has the tip of the triangle pointing down. The left triangle shape is a right triangle with the hypotenuse facing up and to the left. +The values do, re, mi, fa, fa up, so, la, and ti correspond to Aikin's 7-shape system. The fa up shape is typically used with upstems; the fa shape is typically used with downstems or no stems. + +The arrow shapes differ from triangle and inverted triangle by being centered on the stem. Slashed and back slashed notes include both the normal notehead and a slash. The triangle shape has the tip of the triangle pointing up; the inverted triangle shape has the tip of the triangle pointing down. The left triangle shape is a right triangle with the hypotenuse facing up and to the left. + +The other notehead covers noteheads other than those listed here. It is usually used in combination with the smufl attribute to specify a particular SMuFL notehead. The smufl attribute may be used with any notehead value to help specify the appearance of symbols that share the same MusicXML semantics. Noteheads in the SMuFL "Note name noteheads" range (U+E150–U+E1AF) should not use the smufl attribute or the "other" value, but instead use the notehead-text element. @@ -1421,6 +1645,7 @@ The arrow shapes differ from triangle and inverted triangle by being centered on + @@ -1437,9 +1662,10 @@ The arrow shapes differ from triangle and inverted triangle by being centered on + - + Octaves are represented by the numbers 0 to 9, where 4 indicates the octave started by middle C. @@ -1449,14 +1675,14 @@ The arrow shapes differ from triangle and inverted triangle by being centered on - + The semitones type is a number representing semitones, used for chromatic alteration. A value of -1 corresponds to a flat and a value of 1 to a sharp. Decimal values like 0.5 (quarter tone sharp) are used for microtones. - + The show-tuplet type indicates whether to show a part of a tuplet relating to the tuplet-actual element, both the tuplet-actual and tuplet-normal elements, or neither. @@ -1467,7 +1693,7 @@ The arrow shapes differ from triangle and inverted triangle by being centered on - + The stem type represents the notated stem direction. @@ -1479,7 +1705,7 @@ The arrow shapes differ from triangle and inverted triangle by being centered on - + The step type represents a step of the diatonic scale, represented using the English letters A through G. @@ -1494,7 +1720,7 @@ The arrow shapes differ from triangle and inverted triangle by being centered on - + Lyric hyphenation is indicated by the syllabic type. The single, begin, end, and middle values represent single-syllable words, word-beginning syllables, word-ending syllables, and mid-word syllables, respectively. @@ -1506,7 +1732,17 @@ The arrow shapes differ from triangle and inverted triangle by being centered on - + + + + The tap-hand type represents the symbol to use for a tap element. The left and right values refer to the SMuFL guitarLeftHandTapping and guitarRightHandTapping glyphs respectively. + + + + + + + The number of tremolo marks is represented by a number from 0 to 8: the same as beam-level with 0 added. @@ -1516,9 +1752,9 @@ The arrow shapes differ from triangle and inverted triangle by being centered on - + - + The group-barline-value type indicates if the group should have common barlines. @@ -1529,7 +1765,7 @@ The arrow shapes differ from triangle and inverted triangle by being centered on - + The group-symbol-value type indicates how the symbol for a group is indicated in the score. The default value is none. @@ -1542,13 +1778,22 @@ The arrow shapes differ from triangle and inverted triangle by being centered on - + + + + The measure-text type is used for the text attribute of measure elements. It has at least one character. The implicit attribute of the measure element should be set to "yes" rather than setting the text attribute to an empty string. + + + + + + - + The bend-sound type is used for bend and slide elements, and is similar to the trill-sound attribute group. Here the beats element refers to the number of discrete elements (like MIDI pitch bends) used to represent a continuous bend or slide. The first-beat indicates the percentage of the direction for starting a bend; the last-beat the percentage for ending it. The default choices are: - + accelerate = "no" beats = "4" first-beat = "25" @@ -1559,25 +1804,27 @@ The arrow shapes differ from triangle and inverted triangle by being centered on - + The bezier attribute group is used to indicate the curvature of slurs and ties, representing the control points for a cubic bezier curve. For ties, the bezier attribute group is used with the tied element. Normal slurs, S-shaped slurs, and ties need only two bezier points: one associated with the start of the slur or tie, the other with the stop. Complex slurs and slurs divided over system breaks can specify additional bezier data at slur elements with a continue type. - -The bezier-offset, bezier-x, and bezier-y attributes describe the outgoing bezier point for slurs and ties with a start type, and the incoming bezier point for slurs and ties with types of stop or continue. The attributes bezier-offset2, bezier-x2, and bezier-y2 are only valid with slurs of type continue, and describe the outgoing bezier point. - -The bezier-offset and bezier-offset2 attributes are measured in terms of musical divisions, like the offset element. These are the recommended attributes for specifying horizontal position. The other attributes are specified in tenths, relative to any position settings associated with the slur or tied element. + +The bezier-x, bezier-y, and bezier-offset attributes describe the outgoing bezier point for slurs and ties with a start type, and the incoming bezier point for slurs and ties with types of stop or continue. The bezier-x2, bezier-y2, and bezier-offset2 attributes are only valid with slurs of type continue, and describe the outgoing bezier point. + +The bezier-x, bezier-y, bezier-x2, and bezier-y2 attributes are specified in tenths, relative to any position settings associated with the slur or tied element. The bezier-offset and bezier-offset2 attributes are measured in terms of musical divisions, like the offset element. + +The bezier-offset and bezier-offset2 attributes are deprecated as of MusicXML 3.1. If both the bezier-x and bezier-offset attributes are present, the bezier-x attribute takes priority. Similarly, the bezier-x2 attribute takes priority over the bezier-offset2 attribute. The two types of bezier attributes are not additive. - - + + - + The color attribute group indicates the color of an element. @@ -1588,7 +1835,7 @@ The bezier-offset and bezier-offset2 attributes are measured in terms of musical The dashed-formatting entity represents the length of dashes and spaces in a dashed line. Both the dash-length and space-length attributes are represented in tenths. These attributes are ignored if the corresponding line-type attribute is not dashed. - + @@ -1599,7 +1846,7 @@ The bezier-offset and bezier-offset2 attributes are measured in terms of musical - + The document-attributes attribute group is used to specify the attributes for an entire MusicXML document. Currently this is used for the version attribute. @@ -1608,14 +1855,14 @@ The version attribute was added in Version 1.1 for the score-partwise and score- - + The enclosure attribute group is used to specify the formatting of an enclosure around text or symbols. - + The font attribute group gathers together attributes for determining the font within a credit or direction. They are based on the text styles for Cascading Style Sheets. The font-family is a comma-separated list of font names. These can be specific font styles such as Maestro or Opus, or one of several generic font styles: music, engraved, handwritten, text, serif, sans-serif, handwritten, cursive, fantasy, and monospace. The music, engraved, and handwritten values refer to music fonts; the rest refer to text fonts. The fantasy style refers to decorative text such as found in older German-style printing. The font-style can be normal or italic. The font-size can be one of the CSS sizes (xx-small, x-small, small, medium, large, x-large, xx-large) or a numeric point size. The font-weight can be normal or bold. The default is application-dependent, but is a text font vs. a music font. @@ -1625,7 +1872,7 @@ The version attribute was added in Version 1.1 for the score-partwise and score- - + In cases where text extends over more than one line, horizontal alignment and justify values can be different. The most typical case is for credits, such as: @@ -1639,21 +1886,21 @@ The halign attribute is used in these situations. If it is not present, its valu - + The justify attribute is used to indicate left, center, or right justification. The default value varies for different elements. For elements where the justify attribute is present but the halign attribute is not, the justify attribute indicates horizontal alignment as well as justification. - + The letter-spacing attribute specifies text tracking. Values are either "normal" or a number representing the number of ems to add between each letter. The number may be negative in order to subtract space. The default is normal, which allows flexibility of letter-spacing for purposes of text justification. - + The level-display attribute group specifies three common ways to indicate editorial indications: putting parentheses or square brackets around a symbol, or making the symbol a different size. If not specified, they are left to application defaults. It is used by the level and accidental elements. @@ -1662,45 +1909,59 @@ The halign attribute is used in these situations. If it is not present, its valu - + - The line-height attribute specifies text leading. Values are either "normal" or a number representing the percentage of the current font height to use for leading. The default is "normal". The exact normal value is implementation-dependent, but values between 100 and 120 are recommended. + The line-height attribute specifies text leading. Values are either "normal" or a number representing the percentage of the current font height to use for leading. The default is "normal". The exact normal value is implementation-dependent, but values between 100 and 120 are recommended. + + + + + + + The line-length attribute distinguishes between different line lengths for doit, falloff, plop, and scoop articulations. - + - + The line-shape attribute distinguishes between straight and curved lines. - + The line-type attribute distinguishes between solid, dashed, dotted, and wavy lines. - + + + + The optional-unique-id attribute group allows an element to optionally specify an ID that is unique to the entire document. This attribute group is not used for a required id attribute, or for an id attribute that specifies an id reference. + + + + The orientation attribute indicates whether slurs and ties are overhand (tips down) or underhand (tips up). This is distinct from the placement attribute used by any notation type. - + The placement attribute indicates whether something is above or below another element, such as a note or a notation. - + - The position attributes are based on MuseData print suggestions. For most elements, any program will compute a default x and y position. The position attributes let this be changed two ways. + The position attributes are based on MuseData print suggestions. For most elements, any program will compute a default x and y position. The position attributes let this be changed two ways. The default-x and default-y attributes change the computation of the default position. For most elements, the origin is changed relative to the left-hand side of the note or the musical position within the bar (x) and the top line of the staff (y). @@ -1724,7 +1985,7 @@ For the note, figured-bass, and harmony elements, the default-x value is conside Since the credit-words and credit-image elements are not related to a measure, in these cases the default-x and default-y attributes adjust the origin relative to the bottom left-hand corner of the specified page. The relative-x and relative-y attributes change the position relative to the default position, either as computed by the individual program, or as overridden by the default-x and default-y attributes. - + Positive x is right, negative x is left; positive y is up, negative y is down. All units are in tenths of interline space. For stems, positive relative-y lengthens a stem while negative relative-y shortens it. The default-x and default-y position attributes provide higher-resolution positioning data than related features such as the placement attribute and the offset element. Applications reading a MusicXML file that can understand both features should generally rely on the default-x and default-y attributes for their greater accuracy. For the relative-x and relative-y attributes, the offset element, placement attribute, and directive attribute provide context for the relative position information, so the two features should be interpreted together. @@ -1736,21 +1997,21 @@ As elsewhere in the MusicXML format, tenths are the global tenths defined by the - + The print-object attribute specifies whether or not to print an object (e.g. a note or a rest). It is yes by default. - + The print-spacing attribute controls whether or not spacing is left for an invisible note or object. It is used only if no note, dot, or lyric is being printed. The value is yes (leave spacing) by default. - + The print-style attribute group collects the most popular combination of printing attributes: position, font, and color. @@ -1759,7 +2020,7 @@ As elsewhere in the MusicXML format, tenths are the global tenths defined by the - + The print-style-align attribute group adds the halign and valign attributes to the position, font, and color attributes. @@ -1768,7 +2029,7 @@ As elsewhere in the MusicXML format, tenths are the global tenths defined by the - + The printout attribute group collects the different controls over printing an object (e.g. a note or rest) and its parts, including augmentation dots and lyrics. This is especially useful for notes that overlap in different voices, or for chord sheets that contain lyrics and chords but no melody. @@ -1780,6 +2041,27 @@ By default, all these attributes are set to yes. If print-object is set to no, t + + + + The smufl attribute group is used to indicate a particular Standard Music Font Layout (SMuFL) character. Sometimes this is a formatting choice, and sometimes this is a refinement of the semantic meaning of an element. + + + + + + + The symbol-formatting attribute group collects the common formatting attributes for musical symbols. Default values may differ across the elements that use this group. + + + + + + + + + + @@ -1789,14 +2071,14 @@ By default, all these attributes are set to yes. If print-object is set to no, t - + The text-direction attribute is used to adjust and override the Unicode bidirectional text algorithm, similar to the W3C Internationalization Tag Set recommendation. - + The text-formatting attribute group collects the common formatting attributes for text elements. Default values may differ across the elements that use this group. @@ -1812,28 +2094,28 @@ By default, all these attributes are set to yes. If print-object is set to no, t - + The rotation attribute is used to rotate text around the alignment point specified by the halign and valign attributes. Positive values are clockwise rotations, while negative values are counter-clockwise rotations. - + The trill-sound attribute group includes attributes used to guide the sound of trills, mordents, turns, shakes, and wavy lines, based on MuseData sound suggestions. The default choices are: - + start-note = "upper" trill-step = "whole" two-note-turn = "none" accelerate = "no" beats = "4". - + Second-beat and last-beat are percentages for landing on the indicated beat, with defaults of 25 and 75 respectively. - + For mordent and inverted-mordent elements, the defaults are different: - + The default start-note is "main", not "upper". The default for beats is "3", not "4". The default for second-beat is "12", not "25". @@ -1847,21 +2129,21 @@ For mordent and inverted-mordent elements, the defaults are different: - + The valign attribute is used to indicate vertical alignment to the top, middle, bottom, or baseline of the text. Defaults are implementation-dependent. - + The valign-image attribute is used to indicate vertical alignment for images and graphics, so it removes the baseline value. Defaults are implementation-dependent. - + The x-position attribute group is used for elements like notes where specifying x position is common, but specifying y position is rare. @@ -1871,7 +2153,7 @@ For mordent and inverted-mordent elements, the defaults are different: - + The y-position attribute group is used for elements like stems where specifying y position is common, but specifying x position is rare. @@ -1881,20 +2163,22 @@ For mordent and inverted-mordent elements, the defaults are different: - + - + - The image-attributes group is used to include graphical images in a score. The required source attribute is the URL for the image file. The required type attribute is the MIME type for the image file format. Typical choices include application/postscript, image/gif, image/jpeg, image/png, and image/tiff. + The image-attributes group is used to include graphical images in a score. The required source attribute is the URL for the image file. The required type attribute is the MIME type for the image file format. Typical choices include application/postscript, image/gif, image/jpeg, image/png, and image/tiff. The optional height and width attributes are used to size and scale an image. The image should be scaled independently in X and Y if both height and width are specified. If only one attribute is specified, the image should be scaled proportionally to fit in the specified dimension. + + - + The print-attributes group is used by the print element. The new-system and new-page attributes indicate whether to force a system or page break, or to force the current music onto the same system or page as the preceding music. Normally this is the first music data within a measure. If used in multi-part music, they should be placed in the same positions within each part, or the results are undefined. The page-number attribute sets the number of a new page; it is ignored if new-page is not "yes". Version 2.0 adds a blank-page attribute. This is a positive integer value that specifies the number of blank pages to insert before the current measure. It is ignored if new-page is not "yes". These blank pages have no music, but may have text or images specified by the credit element. This is used to allow a combination of pages that are all text, or all text and images, together with pages of music. @@ -1907,9 +2191,9 @@ The staff-spacing attribute specifies spacing between multiple staves in tenths - + - + The element and position attributes are new as of Version 2.0. They allow for bookmarks and links to be positioned at higher resolution than the level of music-data elements. When no element and position attributes are present, the bookmark or link element refers to the next sibling element in the MusicXML file. The element attribute specifies an element type for a descendant of the next sibling element that is not a link or bookmark. The position attribute specifies the position of this descendant element, where the first position is 1. The position attribute is ignored if the element attribute is not present. For instance, an element value of "beam" and a position value of "2" defines the link or bookmark to refer to the second beam descendant of the next sibling element that is not a link or bookmark. This is equivalent to an XPath test of [.//beam[2]] done in the context of the sibling element. @@ -1930,9 +2214,9 @@ The staff-spacing attribute specifies spacing between multiple staves in tenths - + - + The group-name-text attribute group is used by the group-name and group-abbreviation elements. The print-style and justify attribute groups are deprecated in MusicXML 2.0 in favor of the new group-name-display and group-abbreviation-display elements. @@ -1944,28 +2228,32 @@ The staff-spacing attribute specifies spacing between multiple staves in tenths The measure-attributes group is used by the measure element. Measures have a required number attribute (going from partwise to timewise, measures are grouped via the number). - + The implicit attribute is set to "yes" for measures where the measure number should never appear, such as pickup measures and the last half of mid-measure repeats. The value is "no" if not specified. - -The non-controlling attribute is intended for use in multimetric music like the Don Giovanni minuet. If set to "yes", the left barline in this measure does not coincide with the left barline of measures in other parts. The value is "no" if not specified. -In partwise files, the number attribute should be the same for measures in different parts that share the same left barline. While the number attribute is often numeric, it does not have to be. Non-numeric values are typically used together with the implicit or non-controlling attributes being set to "yes". For a pickup measure, the number attribute is typically set to "0" and the implicit attribute is typically set to "yes". Further details about measure numbering can be defined using the measure-numbering element. +The non-controlling attribute is intended for use in multimetric music like the Don Giovanni minuet. If set to "yes", the left barline in this measure does not coincide with the left barline of measures in other parts. The value is "no" if not specified. + +In partwise files, the number attribute should be the same for measures in different parts that share the same left barline. While the number attribute is often numeric, it does not have to be. Non-numeric values are typically used together with the implicit or non-controlling attributes being set to "yes". For a pickup measure, the number attribute is typically set to "0" and the implicit attribute is typically set to "yes". + +If measure numbers are not unique within a part, this can cause problems for conversions between partwise and timewise formats. The text attribute allows specification of displayed measure numbers that are different than what is used in the number attribute. This attribute is ignored for measures where the implicit attribute is set to "yes". Further details about measure numbering can be specified using the measure-numbering element. Measure width is specified in tenths. These are the global tenths specified in the scaling element, not local tenths as modified by the staff-size element. The width covers the entire measure from barline or system start to barline or system end. + + - + In either partwise or timewise format, the part element has an id attribute that is an IDREF back to a score-part in the part-list. - + The part-name-text attribute group is used by the part-name and part-abbreviation elements. The print-style and justify attribute groups are deprecated in MusicXML 2.0 in favor of the new part-name-display and part-abbreviation-display elements. @@ -1974,7 +2262,7 @@ Measure width is specified in tenths. These are the global tenths specified in t - + @@ -1984,14 +2272,24 @@ Measure width is specified in tenths. These are the global tenths specified in t + + + + The coda type is the visual indicator of a coda sign. The exact glyph can be specified with the smufl attribute. A sound element is also needed to guide playback applications reliably. + + + + + + Dynamics can be associated either with a note or a general musical direction. To avoid inconsistencies between and amongst the letter abbreviations for dynamics (what is sf vs. sfz, standing alone or with a trailing dynamic that is not always piano), we use the actual letters as the names of these dynamic elements. The other-dynamics element allows other dynamic marks that are not covered here, but many of those should perhaps be included in a more general musical direction element. Dynamics elements may also be combined to create marks not covered by a single element, such as sfmp. - + These letter dynamic symbols are separated from crescendo, decrescendo, and wedge indications. Dynamic representation is inconsistent in scores. Many things are assumed by the composer and left out, such as returns to original dynamics. Systematic representations are quite complex: for example, Humdrum has at least 3 representation formats related to dynamics. The MusicXML format captures what is in the score, but does not try to be optimal for analysis or synthesis of dynamics. @@ -2018,20 +2316,24 @@ These letter dynamic symbols are separated from crescendo, decrescendo, and wedg - + + + + + - + The empty type represents an empty element with no attributes. - + The empty-placement type represents an empty element with print-style and placement attributes. @@ -2039,21 +2341,38 @@ These letter dynamic symbols are separated from crescendo, decrescendo, and wedg - + + + + The empty-placement-smufl type represents an empty element with print-style, placement, and smufl attributes. + + + + + + The empty-print-style type represents an empty element with print-style attribute group. - + The empty-print-style-align type represents an empty element with print-style-align attribute group. - + + + + The empty-print-style-align-id type represents an empty element with print-style-align and optional-unique-id attribute groups. + + + + + The empty-print-style-align-object type represents an empty element with print-object and print-style-align attribute groups. @@ -2061,7 +2380,7 @@ These letter dynamic symbols are separated from crescendo, decrescendo, and wedg - + The empty-trill-sound type represents an empty element with print-style, placement, and trill-sound attributes. @@ -2070,7 +2389,7 @@ These letter dynamic symbols are separated from crescendo, decrescendo, and wedg - + The horizontal-turn type represents turn elements that are horizontal rather than vertical. These are empty elements with print-style, placement, trill-sound, and slash attributes. If the slash attribute is yes, then a vertical line is used to slash the turn; it is no by default. @@ -2080,7 +2399,7 @@ These letter dynamic symbols are separated from crescendo, decrescendo, and wedg - + The fermata text content represents the shape of the fermata sign. An empty fermata element represents a normal fermata. The fermata type is upright if not specified. @@ -2089,6 +2408,7 @@ These letter dynamic symbols are separated from crescendo, decrescendo, and wedg + @@ -2107,6 +2427,29 @@ These letter dynamic symbols are separated from crescendo, decrescendo, and wedg + + + The formatted-symbol type represents a SMuFL musical symbol element with formatting attributes. + + + + + + + + + + + The formatted-symbol-id type represents a SMuFL musical symbol element with formatting and id attributes. + + + + + + + + + The formatted-text type represents a text element with text-formatting attributes. @@ -2117,7 +2460,19 @@ These letter dynamic symbols are separated from crescendo, decrescendo, and wedg - + + + + The formatted-text-id type represents a text element with text-formatting and id attributes. + + + + + + + + + The fret element is used with tablature notation and chord diagrams. Fret numbers start with 0 for an open string and 1 for the first fret. @@ -2141,7 +2496,7 @@ These letter dynamic symbols are separated from crescendo, decrescendo, and wedg - + The midi-device type corresponds to the DeviceName meta event in Standard MIDI Files. The optional port attribute is a number from 1 to 16 that can be used with the unofficial MIDI port (or cable) meta event. Unlike the DeviceName meta event, there can be multiple midi-device elements per MusicXML part starting in MusicXML 3.0. The optional id attribute refers to the score-instrument assigned to this device. If missing, the device assignment affects all score-instrument elements in the score-part. @@ -2215,7 +2570,7 @@ These letter dynamic symbols are separated from crescendo, decrescendo, and wedg - + The other-play element represents other types of playback. The required type attribute indicates the type of playback to which the element content applies. @@ -2226,7 +2581,7 @@ These letter dynamic symbols are separated from crescendo, decrescendo, and wedg - + The play type, new in Version 3.0, specifies playback techniques to be used in conjunction with the instrument-sound element. When used as part of a sound element, it applies to all notes going forward in score order. In multi-instrument parts, the affected instrument should be specified using the id attribute. When used as part of a note element, it applies to the current note only. @@ -2245,10 +2600,19 @@ These letter dynamic symbols are separated from crescendo, decrescendo, and wedg - + + + + The segno type is the visual indicator of a segno sign. The exact glyph can be specified with the smufl attribute. A sound element is also needed to guide playback applications reliably. + + + + + + - The string type is used with tablature notation, regular notation (where it is often circled), and chord diagrams. String numbers start with 1 for the highest string. + The string type is used with tablature notation, regular notation (where it is often circled), and chord diagrams. String numbers start with 1 for the highest pitched full-length string. @@ -2257,7 +2621,7 @@ These letter dynamic symbols are separated from crescendo, decrescendo, and wedg - + The typed-text type represents a text element with a type attributes. @@ -2268,10 +2632,10 @@ These letter dynamic symbols are separated from crescendo, decrescendo, and wedg - + - Wavy lines are one way to indicate trills. When used with a measure element, they should always have type="continue" set. + Wavy lines are one way to indicate trills. When used with a barline element, they should always have type="continue" set. @@ -2282,7 +2646,7 @@ These letter dynamic symbols are separated from crescendo, decrescendo, and wedg - + The attributes element contains musical information that typically changes on measure boundaries. This includes key and time signatures, clefs, transpositions, and staving. When attributes are changed mid-measure, it affects the music in score order, not in MusicXML document order. @@ -2301,7 +2665,7 @@ These letter dynamic symbols are separated from crescendo, decrescendo, and wedg The key element represents a key signature. Both traditional and non-traditional key signatures are supported. The optional number attribute refers to staff numbers. If absent, the key signature applies to all staves in the part. - + Time signatures are represented by the beats element for the numerator and the beat-type element for the denominator. @@ -2370,7 +2734,7 @@ These letter dynamic symbols are separated from crescendo, decrescendo, and wedg The beat-repeat type is used to indicate that a single beat (but possibly many notes) is repeated. Both the start and stop of the beat being repeated should be specified. The slashes attribute specifies the number of slashes to use in the symbol. The use-dots attribute indicates whether or not to use dots as well (for instance, with mixed rhythm patterns). By default, the value for slashes is 1 and the value for use-dots is no. - + The beat-repeat element specifies a notation style for repetitions. The actual music being repeated needs to be repeated within the MusicXML file. This element specifies the notation that indicates the repeat. @@ -2381,7 +2745,7 @@ The beat-repeat element specifies a notation style for repetitions. The actual m - A cancel element indicates that the old key signature should be cancelled before the new one appears. This will always happen when changing to C major or A minor and need not be specified then. The cancel value matches the fifths value of the cancelled key signature (e.g., a cancel of -2 will provide an explicit cancellation for changing from B flat major to F major). The optional location attribute indicates whether the cancellation appears relative to the new key signature. + A cancel element indicates that the old key signature should be cancelled before the new one appears. This will always happen when changing to C major or A minor and need not be specified then. The cancel value matches the fifths value of the cancelled key signature (e.g., a cancel of -2 will provide an explicit cancellation for changing from B flat major to F major). The optional location attribute indicates where the cancellation appears relative to the new key signature. @@ -2423,11 +2787,12 @@ Clefs appear at the start of each system unless the print-object attribute has b + - The interchangeable type is used to represent the second in a pair of interchangeable dual time signatures, such as the 6/8 in 3/4 (6/8). A separate symbol attribute value is available compared to the time element's symbol attribute, which applies to the first of the dual time signatures. The parentheses attribute value is yes by default. + The interchangeable type is used to represent the second in a pair of interchangeable dual time signatures, such as the 6/8 in 3/4 (6/8). A separate symbol attribute value is available compared to the time element's symbol attribute, which applies to the first of the dual time signatures. @@ -2455,11 +2820,23 @@ Clefs appear at the start of each system unless the print-object attribute has b + - + + + + The key-accidental type indicates the accidental to be displayed in a non-traditional key signature, represented in the same manner as the accidental type without the formatting attributes. + + + + + + + + - The key-octave element specifies in which octave an element of a key signature appears. The content specifies the octave value using the same values as the display-octave element. The number attribute is a positive integer that refers to the key signature element in left-to-right order. If the cancel attribute is set to yes, then this number refers to an element specified by the cancel element. It is no by default. + The key-octave element specifies in which octave an element of a key signature appears. The content specifies the octave value using the same values as the display-octave element. The number attribute is a positive integer that refers to the key signature element in left-to-right order. If the cancel attribute is set to yes, then this number refers to the canceling key signature specified by the cancel element in the parent key element. The cancel attribute cannot be set to yes if there is no corresponding cancel element within the parent key element. It is no by default. @@ -2472,7 +2849,7 @@ Clefs appear at the start of each system unless the print-object attribute has b The measure-repeat type is used for both single and multiple measure repeats. The text of the element indicates the number of measures to be repeated in a single pattern. The slashes attribute specifies the number of slashes to use in the repeat sign. It is 1 if not specified. Both the start and the stop of the measure-repeat must be specified. The text of the element is ignored when the type is stop. - + The measure-repeat element specifies a notation style for repetitions. The actual music being repeated needs to be repeated within the MusicXML file. This element specifies the notation that indicates the repeat. @@ -2498,11 +2875,12 @@ The multiple-rest and measure-repeat symbols indicate the number of measures cov + - The text of the multiple-rest type indicates the number of measures in the multiple rest. Multiple rests may use the 1-bar / 2-bar / 4-bar rest symbols, or a single shape. The use-symbols attribute indicates which to use; it is no if not specified. The element text is ignored when the type is stop. + The text of the multiple-rest type indicates the number of measures in the multiple rest. Multiple rests may use the 1-bar / 2-bar / 4-bar rest symbols, or a single shape. The use-symbols attribute indicates which to use; it is no if not specified. @@ -2594,6 +2972,7 @@ The print-object attribute allows a time signature to be specified but not print + @@ -2623,8 +3002,9 @@ The print-object attribute allows a time signature to be specified but not print + - + @@ -2641,15 +3021,15 @@ The print-object attribute allows a time signature to be specified but not print If a barline is other than a normal single barline, it should be represented by a barline type that describes it. This includes information about repeats and multiple endings, as well as line style. Barline data is on the same level as the other musical data in a score - a child of a measure in a partwise score, or a part in a timewise score. This allows for barlines within measures, as in dotted barlines that subdivide measures in complex meters. The two fermata elements allow for fermatas on both sides of the barline (the lower one inverted). - + Barlines have a location attribute to make it easier to process barlines independently of the other musical data in a score. It is often easier to set up measures separately from entering notes. The location attribute must match where the barline element occurs within the rest of the musical data in the score. If location is left, it should be the first element in the measure, aside from the print, bookmark, and link elements. If location is right, it should be the last element, again with the possible exception of the print, bookmark, and link elements. If no location is specified, the right barline is the default. The segno, coda, and divisions attributes work the same way as in the sound element. They are used for playback when barline elements contain segno or coda child elements. - - + + @@ -2658,12 +3038,13 @@ Barlines have a location attribute to make it easier to process barlines indepen + - + The ending type represents multiple (e.g. first and second) endings. Typically, the start type is associated with the left barline of the first measure in an ending. The stop and discontinue types are associated with the right barline of the last measure in an ending. Stop is used when the ending mark concludes with a downward jog, as is typical for first endings. Discontinue is used when there is no downward jog, as is typical for second endings that do not conclude a piece. The length of the jog can be specified using the end-length attribute. The text-x and text-y attributes are offsets that specify where the baseline of the start of the ending text appears, relative to the start of the ending line. - + The number attribute reflects the numeric values of what is under the ending line. Single endings such as "1" or comma-separated multiple endings such as "1,2" may be used. The ending element text is used when the text displayed in the ending is different than what appears in the number attribute. The print-object element is used to indicate when an ending is present but not printed, as is often the case for many parts in a full score. @@ -2689,7 +3070,7 @@ The number attribute reflects the numeric values of what is under the ending lin - + The accord type represents the tuning of a single string in the scordatura element. It uses the same group of elements as the staff-tuning element. Strings are numbered from high to low. @@ -2705,21 +3086,22 @@ The number attribute reflects the numeric values of what is under the ending lin - The accordion-high element indicates the presence of a dot in the high (4') section of the registration symbol. + The accordion-high element indicates the presence of a dot in the high (4') section of the registration symbol. This element is omitted if no dot is present. - The accordion-middle element indicates the presence of 1 to 3 dots in the middle (8') section of the registration symbol. + The accordion-middle element indicates the presence of 1 to 3 dots in the middle (8') section of the registration symbol. This element is omitted if no dots are present. - The accordion-low element indicates the presence of a dot in the low (16') section of the registration symbol. + The accordion-low element indicates the presence of a dot in the low (16') section of the registration symbol. This element is omitted if no dot is present. + @@ -2729,7 +3111,7 @@ The number attribute reflects the numeric values of what is under the ending lin - + The bass type is used to indicate a bass note in popular music chord symbols, e.g. G/C. It is generally not used in functional harmony, as inversion is generally not used in pop chord symbols. As with root, it is divided into step and alter elements, similar to pitches. @@ -2776,6 +3158,13 @@ The number attribute reflects the numeric values of what is under the ending lin + + + The beat-unit-tied type indicates a beat-unit within a metronome mark that is tied to the preceding beat-unit. This allows or two or more tied notes to be associated with a per-minute value in a metronome mark, whereas the metronome-tied element is restricted to metric relationship marks. + + + + Brackets are combined with words in a variety of modern directions. The line-end attribute specifies if there is a jog up or down (or both), an arrow, or nothing at the start or end of the bracket. If the line-end is up or down, the length of the jog can be specified using the end-length attribute. The line-type is solid by default. @@ -2788,6 +3177,7 @@ The number attribute reflects the numeric values of what is under the ending lin + @@ -2799,12 +3189,13 @@ The number attribute reflects the numeric values of what is under the ending lin + The degree type is used to add, alter, or subtract individual notes in the chord. The print-object attribute can be used to keep the degree from printing separately when it has already taken into account in the text attribute of the kind element. The degree-value and degree-type text attributes specify how the value and type of the degree should be displayed. - + A harmony of kind "other" can be spelled explicitly by using a series of degree elements together with a root. @@ -2854,8 +3245,8 @@ A harmony of kind "other" can be spelled explicitly by using a series of degree - A direction is a musical indication that is not attached to a specific note. Two or more may be combined to indicate starts and stops of wedges, dashes, etc. - + A direction is a musical indication that is not necessarily attached to a specific note. Two or more may be combined to indicate starts and stops of wedges, dashes, etc. For applications where a specific direction is indeed attached to a specific note, the direction element can be associated with the note element that follows it in score order that is not in a different voice. + By default, a series of direction-type elements and a series of child elements of a direction-type within a single direction element follow one another in sequence visually. For a series of direction-type children, non-positional formatting attributes are carried over from the previous element by default. @@ -2867,33 +3258,33 @@ By default, a series of direction-type elements and a series of child elements o + - + Textual direction types may have more than 1 component due to multiple fonts. The dynamics element may also be used in the notations element. Attribute groups related to print suggestions apply to the individual direction-type, not to the overall direction. - + The rehearsal type specifies a rehearsal mark. Language is Italian ("it") by default. Enclosure is square by default. Left justification is assumed if not specified. - - - The segno element is the visual indicator of a segno sign. A sound element is needed to guide playback applications reliably. - - - - - The words element specifies a standard text direction. Left justification is assumed if not specified. Language is Italian ("it") by default. Enclosure is none by default. - - - - - The coda element is the visual indicator of a coda sign. A sound element is needed to guide playback applications reliably. - - + + + + + + The words element specifies a standard text direction. Left justification is assumed if not specified. Language is Italian ("it") by default. Enclosure is none by default. + + + + + The symbol element specifies a musical symbol using a canonical SMuFL glyph name. It is used when an occasional musical symbol is interspersed into text. It should not be used in place of semantic markup, such as metronome marks that mix text and symbols. Left justification is assumed if not specified. Enclosure is none by default. + + + @@ -2902,17 +3293,17 @@ By default, a series of direction-type elements and a series of child elements o - + The damp element specifies a harp damping mark. - + The damp-all element specifies a harp damping mark for all strings. - + The eyeglasses element specifies the eyeglasses symbol, common in commercial music. @@ -2921,10 +3312,12 @@ By default, a series of direction-type elements and a series of child elements o - + + + @@ -2975,6 +3368,7 @@ By default, a series of direction-type elements and a series of child elements o + @@ -2989,10 +3383,21 @@ By default, a series of direction-type elements and a series of child elements o + + + The glass type represents pictograms for glass percussion instruments. The smufl attribute is used to distinguish different SMuFL glyphs for wind chimes in the chimes pictograms range, including those made of materials other than glass. + + + + + + + + The grouping type is used for musical analysis. When the type attribute is "start" or "single", it usually contains one or more feature elements. The number attribute is used for distinguishing between overlapping and hierarchical groupings. The member-of attribute allows for easy distinguishing of what grouping elements are in what hierarchy. Feature elements contained within a "stop" type of grouping may be ignored. - + This element is flexible to allow for different types of analyses. Future versions of the MusicXML format may add elements that can represent more standardized categories of analysis data, allowing for easier data sharing. @@ -3001,14 +3406,15 @@ This element is flexible to allow for different types of analyses. Future versio + The harmony type is based on Humdrum's **harm encoding, extended to support chord symbols in popular music as well as functional harmony analysis in classical music. - -If there are alternate harmonies possible, this can be specified using multiple harmony elements differentiated by type. Explicit harmonies have all note present in the music; implied have some notes missing but implied; alternate represents alternate analyses. - + +If there are alternate harmonies possible, this can be specified using multiple harmony elements differentiated by type. Explicit harmonies have all note present in the music; implied have some notes missing but implied; alternate represents alternate analyses. + The harmony object may be used for analysis or for chord symbols. The print-object attribute controls whether or not anything is printed due to the harmony element. The print-frame attribute controls printing of a frame or fretboard diagram. The print-style attribute group sets the default for the harmony, but individual elements can override this with their own print-style values. @@ -3023,6 +3429,7 @@ The harmony object may be used for analysis or for chord symbols. The print-obje + @@ -3033,6 +3440,7 @@ The harmony object may be used for analysis or for chord symbols. The print-obje + @@ -3040,6 +3448,7 @@ The harmony object may be used for analysis or for chord symbols. The print-obje The image type is used to include graphical images in a score. + @@ -3056,17 +3465,17 @@ The harmony object may be used for analysis or for chord symbols. The print-obje Kind indicates the type of chord. Degree elements can then add, subtract, or alter from these starting points - + The attributes are used to indicate the formatting of the symbol. Since the kind element is the constant in all the harmony-chord groups that can make up a polychord, many formatting attributes are here. - + The use-symbols attribute is yes if the kind should be represented when possible with harmony symbols rather than letters and numbers. These symbols include: - + major: a triangle, like Unicode 25B3 minor: -, like Unicode 002D augmented: +, like Unicode 002B diminished: °, like Unicode 00B0 half-diminished: ø, like Unicode 00F8 - + For the major-minor kind, only the minor symbol is used when use-symbols is yes. The major symbol is set using the symbol attribute in the degree-value element. The corresponding degree-alter value will usually be 0 in this case. The text attribute describes how the kind should be spelled in a score. If use-symbols is yes, the value of the text attribute follows the symbol. The stack-degrees attribute is yes if the degree elements should be stacked above each other. The parentheses-degrees attribute is yes if all the degrees should be in parentheses. The bracket-degrees attribute is yes if all the degrees should be in a bracket. If not specified, these values are implementation-specific. The alignment attributes are for the entire harmony-chord group of which this kind element is a part. @@ -3098,17 +3507,26 @@ The text attribute describes how the kind should be spelled in a score. If use-s - The metronome type represents metronome marks and other metric relationships. The beat-unit group and per-minute element specify regular metronome marks. The metronome-note and metronome-relation elements allow for the specification of more complicated metric relationships, such as swing tempo marks where two eighths are equated to a quarter note / eighth note triplet. The parentheses attribute indicates whether or not to put the metronome mark in parentheses; its value is no if not specified. + The metronome type represents metronome marks and other metric relationships. The beat-unit group and per-minute element specify regular metronome marks. The metronome-note and metronome-relation elements allow for the specification of metric modulations and other metric relationships, such as swing tempo marks where two eighths are equated to a quarter note / eighth note triplet. Tied notes can be represented in both types of metronome marks by using the beat-unit-tied and metronome-tied elements. The parentheses attribute indicates whether or not to put the metronome mark in parentheses; its value is no if not specified. + - + + + + + + + If the metronome-arrows element is present, it indicates that metric modulation arrows are displayed on both sides of the metronome mark. + + @@ -3123,6 +3541,7 @@ The text attribute describes how the kind should be spelled in a score. If use-s + @@ -3152,10 +3571,18 @@ The text attribute describes how the kind should be spelled in a score. If use-s + - + + + + The metronome-tied indicates the presence of a tie within a metric relationship mark. As with the tied element, both the start and stop of the tie should be specified, in this case within separate metronome-note elements. + + + + The metronome-tuplet type uses the same element structure as the time-modification element along with some attributes from the tuplet element. @@ -3178,6 +3605,7 @@ The text attribute describes how the kind should be spelled in a score. If use-s + @@ -3193,24 +3621,31 @@ The text attribute describes how the kind should be spelled in a score. If use-s - The other-direction type is used to define any direction symbols not yet in the current version of the MusicXML format. This allows extended representation, though without application interoperability. + The other-direction type is used to define any direction symbols not yet in the MusicXML format. The smufl attribute can be used to specify a particular direction symbol, allowing application interoperability without requiring every SMuFL glyph to have a MusicXML element equivalent. Using the other-direction type without the smufl attribute allows for extended representation, though without application interoperability. + + - The pedal type represents piano pedal marks. The line attribute is yes if pedal lines are used. The sign attribute is yes if Ped and * signs are used. For MusicXML 2.0 compatibility, the sign attribute is yes by default if the line attribute is no, and is no by default if the line attribute is yes. The change and continue types are used when the line attribute is yes. The change type indicates a pedal lift and retake indicated with an inverted V marking. The continue type allows more precise formatting across system breaks and for more complex pedaling lines. The alignment attributes are ignored if the line attribute is yes. + The pedal type represents piano pedal marks. In MusicXML 3.1 this includes sostenuto as well as damper pedal marks. The line attribute is yes if pedal lines are used. The sign attribute is yes if Ped, Sost, and * signs are used. For MusicXML 2.0 compatibility, the sign attribute is yes by default if the line attribute is no, and is no by default if the line attribute is yes. If the sign attribute is set to yes and the type is start or sostenuto, the abbreviated attribute is yes if the short P and S signs are used, and no if the full Ped and Sost signs are used. It is no by default. Otherwise the abbreviated attribute is ignored. + +The change and continue types are used when the line attribute is yes. The change type indicates a pedal lift and retake indicated with an inverted V marking. The continue type allows more precise formatting across system breaks and for more complex pedaling lines. The alignment attributes are ignored if the line attribute is yes. - + + + + @@ -3257,10 +3692,22 @@ The text attribute describes how the kind should be spelled in a score. If use-s - + + + + + + + The pitched-value type represents pictograms for pitched percussion instruments. The smufl attribute is used to distinguish different SMuFL glyphs for a particular pictogram within the tuned mallet percussion pictograms range. + + + + + + @@ -3272,6 +3719,7 @@ The text attribute describes how the kind should be spelled in a score. If use-s + @@ -3279,7 +3727,7 @@ The text attribute describes how the kind should be spelled in a score. If use-s The print type contains general printing parameters, including the layout elements defined in the layout.mod file. The part-name-display and part-abbreviation-display elements used in the score.mod file may also be used here to change how a part name or abbreviation is displayed over the course of a piece. They take effect when the current measure or a succeeding measure starts a new system. - + Layout elements in a print statement only apply to the current page, system, staff, or measure. Music that follows continues to take the default values from the layout included in the defaults element. @@ -3290,6 +3738,7 @@ Layout elements in a print statement only apply to the current page, system, sta + @@ -3334,34 +3783,35 @@ Layout elements in a print statement only apply to the current page, system, sta + The sound element contains general playback parameters. They can stand alone within a part/measure, or be a component element within a direction. - + Tempo is expressed in quarter notes per minute. If 0, the sound-generating program should prompt the user at the time of compiling a sound (MIDI) file. - + Dynamics (or MIDI velocity) are expressed as a percentage of the default forte value (90 for MIDI 1.0). - + Dacapo indicates to go back to the beginning of the movement. When used it always has the value "yes". - + Segno and dalsegno are used for backwards jumps to a segno sign; coda and tocoda are used for forward jumps to a coda sign. If there are multiple jumps, the value of these parameters can be used to name and distinguish them. If segno or coda is used, the divisions attribute can also be used to indicate the number of divisions per quarter note. Otherwise sound and MIDI generating programs may have to recompute this. - + By default, a dalsegno or dacapo attribute indicates that the jump should occur the first time through, while a tocoda attribute indicates the jump should occur the second time through. The time that jumps occur can be changed by using the time-only attribute. - + Forward-repeat is used when a forward repeat sign is implied, and usually follows a bar line. When used it always has the value of "yes". - + The fine attribute follows the final note or rest in a movement with a da capo or dal segno direction. If numeric, the value represents the actual duration of the final note or rest, which can be ambiguous in written notation and different among parts and voices. The value may also be "yes" to indicate no change to the final duration. - + If the sound element applies only particular times through a repeat, the time-only attribute indicates which times to apply the sound element. - + Pizzicato in a sound element effects all following notes. Yes indicates pizzicato, no indicates arco. The pan and elevation attributes are deprecated in Version 2.0. The pan and elevation elements in the midi-instrument element should be used instead. The meaning of the pan and elevation attributes is the same as for the pan and elevation elements. If both are present, the mid-instrument elements take priority. - + The damper-pedal, soft-pedal, and sostenuto-pedal attributes effect playback of the three common piano pedals and their MIDI controller equivalents. The yes value indicates the pedal is depressed; no indicates the pedal is released. A numeric value from 0 to 100 may also be used for half pedaling. This value is the percentage that the pedal is depressed. A value of 0 is equivalent to no, and a value of 100 is equivalent to yes. - + MIDI devices, MIDI instruments, and playback techniques are changed using the midi-device, midi-instrument, and play elements. When there are multiple instances of these elements, they should be grouped together by instrument using the id attribute values. The offset element is used to indicate that the sound takes place offset from the current score position. If the sound element is a child of a direction element, the sound offset element overrides the direction offset element if both elements are present. Note that the offset reflects the intended musical position for the change in sound. It should not be used to compensate for latency issues in particular hardware configurations. @@ -3391,17 +3841,29 @@ The offset element is used to indicate that the sound takes place offset from th + - + + + + The staff-divide element represents the staff division arrow symbols found at SMuFL code points U+E00B, U+E00C, and U+E00D. + + + + + + - The stick type represents pictograms where the material of the stick, mallet, or beater is included. + The stick type represents pictograms where the material of the stick, mallet, or beater is included.The parentheses and dashed-circle attributes indicate the presence of these marks around the round beater part of a pictogram. Values for these attributes are "no" if not present. + + @@ -3410,8 +3872,9 @@ The offset element is used to indicate that the sound takes place offset from th + - + The wedge type represents crescendo and diminuendo wedge symbols. The type attribute is crescendo for the start of a wedge that is closed at the left side, and diminuendo for the start of a wedge that is closed on the right side. Spread values are measured in tenths; those at the start of a crescendo wedge or end of a diminuendo wedge are ignored. The niente attribute is yes if a circle appears at the point of the wedge, indicating a crescendo from nothing or diminuendo to nothing. It is no by default, and used only when the type is crescendo, or the type is stop for a wedge that began with a diminuendo type. The line-type is solid by default. @@ -3424,6 +3887,7 @@ The offset element is used to indicate that the sound takes place offset from th + @@ -3511,6 +3975,7 @@ The offset element is used to indicate that the sound takes place offset from th + @@ -3526,6 +3991,17 @@ The offset element is used to indicate that the sound takes place offset from th + + + The glyph element represents what SMuFL glyph should be used for different variations of symbols that are semantically identical. The type attribute specifies what type of glyph is being defined. The element value specifies what SMuFL glyph to use, including recommended stylistic alternates. The SMuFL glyph name should match the type. For instance, a type of quarter-rest would use values restQuarter, restQuarterOld, or restQuarterZ. A type of g-clef-ottava-bassa would use values gClef8vb, gClef8vbOld, or gClef8vbCClef. A type of octave-shift-up-8 would use values ottava, ottavaBassa, ottavaBassaBa, ottavaBassaVb, or octaveBassa. + + + + + + + + The line-width type indicates the width of a line type in tenths. The type attribute defines what type of line is being defined. Values include beam, bracket, dashes, enclosure, ending, extend, heavy barline, leger, light barline, octave shift, pedal, slur middle, slur tip, staff, stem, tie middle, tie tip, tuplet bracket, and wedge. The text content is expressed in tenths. @@ -3592,7 +4068,7 @@ The offset element is used to indicate that the sound takes place offset from th - + Margins, page sizes, and distances are all measured in tenths to keep MusicXML data in a consistent coordinate system as much as possible. The translation to absolute units is done with the scaling type, which specifies how many millimeters are equal to how many tenths. For a staff height of 7 mm, millimeters would be set to 7 while tenths is set to 40. The ability to set a formula rather than a single scaling factor helps avoid roundoff errors. @@ -3628,7 +4104,7 @@ When used in the print element, the system-dividers element affects the dividers A system is a group of staves that are read and played simultaneously. System layout includes left and right margins and the vertical distance from the previous system. The system distance is measured from the bottom line of the previous system to the top line of the current system. It is ignored for the first system on a page. The top system distance is measured from the page's top margin to the top line of the first system. It is ignored for all but the first system on a page. - + Sometimes the sum of measure widths in a system may not equal the system width specified by the layout elements due to roundoff or other errors. The behavior when reading MusicXML files in these cases is application-dependent. For instance, applications may find that the system layout data is more reliable than the sum of the measure widths, and adjust the measure widths accordingly. @@ -3668,7 +4144,7 @@ Sometimes the sum of measure widths in a system may not equal the system width s - + The accidental type represents actual notated accidentals. Editorial and cautionary indications are indicated by attributes. Values for these attributes are "no" if not present. Specific graphic display such as parentheses, brackets, and size are controlled by the level-display attribute group. @@ -3679,6 +4155,7 @@ Sometimes the sum of measure widths in a system may not equal the system width s + @@ -3689,8 +4166,11 @@ Sometimes the sum of measure widths in a system may not equal the system width s + + + @@ -3704,6 +4184,7 @@ Sometimes the sum of measure widths in a system may not equal the system width s + @@ -3763,15 +4244,11 @@ Sometimes the sum of measure widths in a system may not equal the system width s - The falloff element is an indeterminate slide attached to a single note. The falloff element appears before the main note and goes below the main pitch. + The falloff element is an indeterminate slide attached to a single note. The falloff element appears after the main note and goes below the main pitch. - - - The caesura element indicates a slight pause. It is notated using a "railroad tracks" symbol. - - + The stress element indicates a stressed note. @@ -3782,27 +4259,35 @@ Sometimes the sum of measure widths in a system may not equal the system width s The unstress element indicates an unstressed note. It is often notated using a u-shaped symbol. - + - The other-articulation element is used to define any articulations not yet in the MusicXML format. This allows extended representation, though without application interoperability. + The soft-accent element indicates a soft accent that is not as heavy as a normal accent. It is often notated as <>. It can be combined with other articulations to implement the entire SMuFL Articulation Supplement range. + + + + + The other-articulation element is used to define any articulations not yet in the MusicXML format. The smufl attribute can be used to specify a particular articulation, allowing application interoperability without requiring every SMuFL articulation to have a MusicXML element equivalent. Using the other-articulation element without the smufl attribute allows for extended representation, though without application interoperability. + - The arrow element represents an arrow used for a musical technical indication.. + The arrow element represents an arrow used for a musical technical indication. It can represent both Unicode and SMuFL arrows. The presence of an arrowhead element indicates that only the arrowhead is displayed, not the arrow stem. The smufl attribute distinguishes different SMuFL glyphs that have an arrow appearance such as arrowBlackUp, guitarStrumUp, or handbellsSwingUp. The specified glyph should match the descriptive representation. + + @@ -3822,7 +4307,7 @@ Sometimes the sum of measure widths in a system may not equal the system width s Note that the beam number does not distinguish sets of beams that overlap, as it does for slur and other elements. Beaming groups are distinguished by being in different voices and/or the presence or absence of grace and cue elements. Beams that have a begin value can also have a fan attribute to indicate accelerandos and ritardandos using fanned beams. The fan attribute may also be used with a continue value if the fanning direction changes on that note. The value is "none" if not specified. - + The repeater attribute has been deprecated in MusicXML 3.0. Formerly used for tremolos, it needs to be specified with a "yes" value for each beam using it. @@ -3831,6 +4316,7 @@ The repeater attribute has been deprecated in MusicXML 3.0. Formerly used for tr + @@ -3859,7 +4345,7 @@ The repeater attribute has been deprecated in MusicXML 3.0. Formerly used for tr - The with-bar element indicates that the bend is to be done at the bridge with a whammy or vibrato bar. The content of the element indicates how this should be notated. + The with-bar element indicates that the bend is to be done at the bridge with a whammy or vibrato bar. The content of the element indicates how this should be notated. Content values of "scoop" and "dip" refer to the SMuFL guitarVibratoBarScoop and guitarVibratoBarDip glyphs. @@ -3879,23 +4365,50 @@ The repeater attribute has been deprecated in MusicXML 3.0. Formerly used for tr + + + The caesura element indicates a slight pause. It is notated using a "railroad tracks" symbol or other variations specified in the element content. + + + + + + + + + + + + The elision type represents an elision between lyric syllables. The text content specifies the symbol used to display the elision. Common values are a no-break space (Unicode 00A0), an underscore (Unicode 005F), or an undertie (Unicode 203F). If the text content is empty, the smufl attribute is used to specify the symbol to use. Its value is a SMuFL canonical glyph name that starts with lyrics. The SMuFL attribute is ignored if the elision glyph is already specified by the text content. If neither text content nor a smufl attribute are present, the elision glyph is application-specific. + + + + + + + + + + - The empty-line type represents an empty element with line-shape, line-type, dashed-formatting, print-style and placement attributes. + The empty-line type represents an empty element with line-shape, line-type, line-length, dashed-formatting, print-style and placement attributes. + - + The extend type represents lyric word extension / melisma lines as well as figured bass extensions. The optional type and position attributes are added in Version 3.0 to provide better formatting control. - + + @@ -3905,7 +4418,7 @@ The repeater attribute has been deprecated in MusicXML 3.0. Formerly used for tr - Values for the prefix element include the accidental values sharp, flat, natural, double-sharp, flat-flat, and sharp-sharp. The prefix element may contain additional values for symbols specific to particular figured bass styles. + Values for the prefix element include plus and the accidental values sharp, flat, natural, double-sharp, flat-flat, and sharp-sharp. The prefix element may contain additional values for symbols specific to particular figured bass styles. @@ -3915,17 +4428,18 @@ The repeater attribute has been deprecated in MusicXML 3.0. Formerly used for tr - Values for the suffix element include the accidental values sharp, flat, natural, double-sharp, flat-flat, and sharp-sharp. Suffixes include both symbols that come after the figure number and those that overstrike the figure number. The suffix value slash is used for slashed numbers indicating chromatic alteration. The orientation and display of the slash usually depends on the figure number. The suffix element may contain additional values for symbols specific to particular figured bass styles. + Values for the suffix element include plus and the accidental values sharp, flat, natural, double-sharp, flat-flat, and sharp-sharp. Suffixes include both symbols that come after the figure number and those that overstrike the figure number. The suffix values slash, back-slash, and vertical are used for slashed numbers indicating chromatic alteration. The orientation and display of the slash usually depends on the figure number. The suffix element may contain additional values for symbols specific to particular figured bass styles. + The figured-bass element represents figured bass notation. Figured bass elements take their position from the first regular note (not a grace note or chord note) that follows in score order. The optional duration element is used to indicate changes of figures under a note. - + Figures are ordered from top to bottom. The value of parentheses is "no" if not present. @@ -3936,6 +4450,7 @@ Figures are ordered from top to bottom. The value of parentheses is "no" if not + @@ -3960,6 +4475,7 @@ Figures are ordered from top to bottom. The value of parentheses is "no" if not + @@ -4000,6 +4516,28 @@ Figures are ordered from top to bottom. The value of parentheses is "no" if not + + + The harmon-closed type represents whether the harmon mute is closed, open, or half-open. The optional location attribute indicates which portion of the symbol is filled in when the element value is half. + + + + + + + + + + + The harmon-mute type represents the symbols used for harmon mutes in brass notation. + + + + + + + + The harmonic type indicates natural and artificial harmonics. Allowing the type of pitch to be specified, combined with controls for appearance/playback differences, allows both the notation and the sound to be represented. Artificial harmonics can add a notated touching-pitch; artificial pinch harmonics will usually not notate a touching pitch. The attributes for the harmonic element refer to the use of the circular harmonic symbol, typically but not always used with natural harmonics. @@ -4071,7 +4609,7 @@ Figures are ordered from top to bottom. The value of parentheses is "no" if not - + The hole-closed type represents whether the hole is closed, open, or half-open. The optional location attribute indicates which portion of the hole is filled in when the element value is half. @@ -4082,7 +4620,7 @@ Figures are ordered from top to bottom. The value of parentheses is "no" if not - + The instrument type distinguishes between score-instrument elements in a score-part. The id attribute is an IDREF back to the score-instrument ID. If multiple score-instruments are specified on a score-part, there should be an instrument element for each note in the part. @@ -4092,7 +4630,9 @@ Figures are ordered from top to bottom. The value of parentheses is "no" if not - The lyric type represents text underlays for lyrics, based on Humdrum with support for other formats. Two text elements that are not separated by an elision element are part of the same syllable, but may have different text formatting. The MusicXML 2.0 XSD is more strict than the 2.0 DTD in enforcing this by disallowing a second syllabic element unless preceded by an elision element. The lyric number indicates multiple lines, though a name can be used as well (as in Finale's verse / chorus / section specification). Justification is center by default; placement is below by default. The content of the elision type is used to specify the symbol used to display the elision. Common values are a no-break space (Unicode 00A0), an underscore (Unicode 005F), or an undertie (Unicode 203F). + The lyric type represents text underlays for lyrics, based on Humdrum with support for other formats. Two text elements that are not separated by an elision element are part of the same syllable, but may have different text formatting. The MusicXML XSD is more strict than the DTD in enforcing this by disallowing a second syllabic element unless preceded by an elision element. The lyric number indicates multiple lines, though a name can be used as well (as in Finale's verse / chorus / section specification). + +Justification is center by default; placement is below by default. The print-object attribute can override a note's print-lyric attribute in cases where only some lyrics on a note are printed, as when lyrics for later verses are printed in a block of text rather than with each note. The time-only attribute precisely specifies which lyrics are to be sung which time through a repeated section. @@ -4101,7 +4641,7 @@ Figures are ordered from top to bottom. The value of parentheses is "no" if not - + @@ -4139,6 +4679,8 @@ Figures are ordered from top to bottom. The value of parentheses is "no" if not + + @@ -4163,6 +4705,7 @@ Figures are ordered from top to bottom. The value of parentheses is "no" if not + @@ -4189,25 +4732,42 @@ Figures are ordered from top to bottom. The value of parentheses is "no" if not + - Notes are the most common type of MusicXML data. The MusicXML format keeps the MuseData distinction between elements used for sound information and elements used for notation information (e.g., tie is used for sound, tied for notation). Thus grace notes do not have a duration element. Cue notes have a duration element, as do forward elements, but no tie elements. Having these two types of information available can make interchange considerably easier, as some programs handle one type of information much more readily than the other. - -The dynamics and end-dynamics attributes correspond to MIDI 1.0's Note On and Note Off velocities, respectively. They are expressed in terms of percentages of the default forte value (90 for MIDI 1.0). The attack and release attributes are used to alter the starting and stopping time of the note from when it would otherwise occur based on the flow of durations - information that is specific to a performance. They are expressed in terms of divisions, either positive or negative. A note that starts a tie should not have a release attribute, and a note that stops a tie should not have an attack attribute. If a note is played only particular times through a repeat, the time-only attribute shows which times to play the note. The pizzicato attribute is used when just this note is sounded pizzicato, vs. the pizzicato element which changes overall playback between pizzicato and arco. + Notes are the most common type of MusicXML data. The MusicXML format keeps the MuseData distinction between elements used for sound information and elements used for notation information (e.g., tie is used for sound, tied for notation). Thus grace notes do not have a duration element. Cue notes have a duration element, as do forward elements, but no tie elements. Having these two types of information available can make interchange considerably easier, as some programs handle one type of information much more readily than the other. + +The print-leger attribute is used to indicate whether leger lines are printed. Notes without leger lines are used to indicate indeterminate high and low notes. By default, it is set to yes. If print-object is set to no, print-leger is interpreted to also be set to no if not present. This attribute is ignored for rests. + +The dynamics and end-dynamics attributes correspond to MIDI 1.0's Note On and Note Off velocities, respectively. They are expressed in terms of percentages of the default forte value (90 for MIDI 1.0). + +The attack and release attributes are used to alter the starting and stopping time of the note from when it would otherwise occur based on the flow of durations - information that is specific to a performance. They are expressed in terms of divisions, either positive or negative. A note that starts a tie should not have a release attribute, and a note that stops a tie should not have an attack attribute. The attack and release attributes are independent of each other. The attack attribute only changes the starting time of a note, and the release attribute only changes the stopping time of a note. + +If a note is played only particular times through a repeat, the time-only attribute shows which times to play the note. + +The pizzicato attribute is used when just this note is sounded pizzicato, vs. the pizzicato element which changes overall playback between pizzicato and arco. - - + + + + + + + + + + - The cue element indicates the presence of a cue note. + The cue element indicates the presence of a cue note. In MusicXML, a cue note is a silent note with no playback. Normal notes that play can be specified as cue size using the type element. A cue note that is specified as full size using the type element will still remain silent. @@ -4242,17 +4802,19 @@ The dynamics and end-dynamics attributes correspond to MIDI 1.0's Note On and No + + - The note-type type indicates the graphic note type. Values range from 256th to long. The size attribute indicates full, cue, or large size, with full the default for regular notes and cue the default for cue and grace notes. + The note-type type indicates the graphic note type. Values range from 1024th to maxima. The size attribute indicates full, cue, grace-cue, or large size. The default is full for regular notes, grace-cue for notes that contain both grace and cue elements, and cue for notes that contain either a cue or a grace element, but not both. @@ -4263,10 +4825,12 @@ The dynamics and end-dynamics attributes correspond to MIDI 1.0's Note On and No - The notehead element indicates shapes other than the open and closed ovals associated with note durations. - + The notehead type indicates shapes other than the open and closed ovals associated with note durations. + +The smufl attribute can be used to specify a particular notehead, allowing application interoperability without requiring every SMuFL glyph to have a MusicXML element equivalent. This attribute can be used either with the "other" value, or to refine a specific notehead value such as "cluster". Noteheads in the SMuFL "Note name noteheads" range (U+E150–U+E1AF) should not use the smufl attribute or the "other" value, but instead use the notehead-text element. + For the enclosed shapes, the default is to be hollow for half notes and longer, and filled otherwise. The filled attribute can be set to change this if needed. - + If the parentheses attribute is set to yes, the notehead is parenthesized. It is no by default. @@ -4275,6 +4839,7 @@ If the parentheses attribute is set to yes, the notehead is parenthesized. It is + @@ -4290,7 +4855,7 @@ If the parentheses attribute is set to yes, the notehead is parenthesized. It is - + Ornaments can be any of several types, followed optionally by accidentals. The accidental-mark element's content is represented the same as an accidental element, but with a different name to reflect the different musical meaning. @@ -4327,6 +4892,11 @@ If the parentheses attribute is set to yes, the notehead is parenthesized. It is The vertical-turn element has the turn symbol shape arranged vertically going from upper left to lower right. + + + The inverted-vertical-turn element has the turn symbol shape arranged vertically going from upper right to lower left. + + The shake element has a similar appearance to an inverted-mordent element. @@ -4335,12 +4905,12 @@ If the parentheses attribute is set to yes, the notehead is parenthesized. It is - The mordent element represents the sign with the vertical line. The long attribute is "no" by default. + The mordent element represents the sign with the vertical line. The choice of which mordent sign is inverted differs between MusicXML and SMuFL. The long attribute is "no" by default. - The inverted-mordent element represents the sign without the vertical line. The long attribute is "no" by default. + The inverted-mordent element represents the sign without the vertical line. The choice of which mordent is inverted differs between MusicXML and SMuFL. The long attribute is "no" by default. @@ -4349,19 +4919,25 @@ If the parentheses attribute is set to yes, the notehead is parenthesized. It is - + + + The haydn element represents the Haydn ornament. This is defined in SMuFL as ornamentHaydn. + + + - The other-ornament element is used to define any ornaments not yet in the MusicXML format. This allows extended representation, though without application interoperability. + The other-ornament element is used to define any ornaments not yet in the MusicXML format. The smufl attribute can be used to specify a particular ornament, allowing application interoperability without requiring every SMuFL ornament to have a MusicXML element equivalent. Using the other-ornament element without the smufl attribute allows for extended representation, though without application interoperability. + - The other-notation type is used to define any notations not yet in the MusicXML format. This allows extended representation, though without application interoperability. It handles notations where more specific extension elements such as other-dynamics and other-technical are not appropriate. + The other-notation type is used to define any notations not yet in the MusicXML format. It handles notations where more specific extension elements such as other-dynamics and other-technical are not appropriate. The smufl attribute can be used to specify a particular notation, allowing application interoperability without requiring every SMuFL glyph to have a MusicXML element equivalent. Using the other-notation type without the smufl attribute allows for extended representation, though without application interoperability. @@ -4370,6 +4946,32 @@ If the parentheses attribute is set to yes, the notehead is parenthesized. It is + + + + + + + + + The other-placement-text type represents a text element with print-style, placement, and smufl attribute groups. This type is used by MusicXML notation extension elements to allow specification of specific SMuFL glyphs without needed to add every glyph as a MusicXML element. + + + + + + + + + + + + + The other-text type represents a text element with a smufl attribute group. This type is used by MusicXML direction extension elements to allow specification of specific SMuFL glyphs without needed to add every glyph as a MusicXML element. + + + + @@ -4396,7 +4998,7 @@ If the parentheses attribute is set to yes, the notehead is parenthesized. It is - + The rest element indicates notated rests or silences. Rest elements are usually empty, but placement on the staff can be specified using display-step and display-octave elements. If the measure attribute is set to yes, this indicates this is a complete measure rest. @@ -4406,7 +5008,7 @@ If the parentheses attribute is set to yes, the notehead is parenthesized. It is - + Glissando and slide types both indicate rapidly moving from one pitch to the other so that individual notes are not discerned. The distinction is similar to that between NIFF's glissando and portamento elements. A slide is continuous between two notes and defaults to a solid line. The optional text for a is printed alongside the line. @@ -4419,6 +5021,7 @@ If the parentheses attribute is set to yes, the notehead is parenthesized. It is + @@ -4436,6 +5039,7 @@ If the parentheses attribute is set to yes, the notehead is parenthesized. It is + @@ -4471,7 +5075,20 @@ If the parentheses attribute is set to yes, the notehead is parenthesized. It is - + + + + The tap type indicates a tap on the fretboard. The text content allows specification of the notation; + and T are common choices. If the element is empty, the hand attribute is used to specify the symbol to use. The hand attribute is ignored if the tap glyph is already specified by the text content. If neither text content nor the hand attribute are present, the display is application-specific. + + + + + + + + + + Technical indications give performance information for individual instruments. @@ -4514,14 +5131,14 @@ If the parentheses attribute is set to yes, the notehead is parenthesized. It is The triple-tongue element represents the triple tongue symbol (three dots arranged horizontally). - + - The stopped element represents the stopped symbol, which looks like a plus sign. + The stopped element represents the stopped symbol, which looks like a plus sign. The smufl attribute distinguishes different SMuFL glyphs that have a similar appearance such as handbellsMalletBellSuspended and guitarClosePedal. If not present, the default glyph is brassMuteClosed. - The snap-pizzicato element represents the snap pizzicato symbol. This is a circle with a line, where the line comes inside the circle. It is distinct from the thumb-position symbol, where the line does not come inside the circle. + The snap-pizzicato element represents the snap pizzicato symbol. This is a circle with a line, where the line comes inside the circle. It is distinct from the thumb-position symbol, where the line does not come inside the circle. @@ -4529,11 +5146,7 @@ If the parentheses attribute is set to yes, the notehead is parenthesized. It is - - - The tap element indicates a tap on the fretboard. The element content allows specification of the notation; + and T are common choices. If empty, the display is application-specific. - - + @@ -4544,12 +5157,44 @@ If the parentheses attribute is set to yes, the notehead is parenthesized. It is - + + + The brass-bend element represents the u-shaped bend symbol used in brass notation, distinct from the bend element used in guitar music. + + + + + The flip element represents the flip symbol used in brass notation. + + + + + The smear element represents the tilde-shaped smear symbol used in brass notation. + + + + + The open element represents the open symbol, which looks like a circle. The smufl attribute can be used to distinguish different SMuFL glyphs that have a similar appearance such as brassMuteOpen and guitarOpenPedal. If not present, the default glyph is brassMuteOpen. + + + + + The half-muted element represents the half-muted symbol, which looks like a circle with a plus sign inside. The smufl attribute can be used to distinguish different SMuFL glyphs that have a similar appearance such as brassMuteHalfClosed and guitarHalfOpenPedal. If not present, the default glyph is brassMuteHalfClosed. + + + + + + The golpe element represents the golpe symbol that is used for tapping the pick guard in guitar music. + + + - The other-technical element is used to define any technical indications not yet in the MusicXML format. This allows extended representation, though without application interoperability. + The other-technical element is used to define any technical indications not yet in the MusicXML format. The smufl attribute can be used to specify a particular glyph, allowing application interoperability without requiring every SMuFL technical indication to have a MusicXML element equivalent. Using the other-technical element without the smufl attribute allows for extended representation, though without application interoperability. + @@ -4569,23 +5214,6 @@ If the parentheses attribute is set to yes, the notehead is parenthesized. It is - - - The text-font-color type represents text with optional font and color information. It is used for the elision element. - - - - - - - - - - - - - - The tie element indicates that a tie begins or ends with this note. If the tie element applies only particular times through a repeat, the time-only attribute indicates which times to apply it. The tie element indicates sound; the tied element indicates notation. @@ -4596,11 +5224,17 @@ If the parentheses attribute is set to yes, the notehead is parenthesized. It is - The tied type represents the notated tie. The tie element represents the tie sound. + The tied element represents the notated tie. The tie element represents the tie sound. -The number attribute is rarely needed to disambiguate ties, since note pitches will usually suffice. The attribute is implied rather than defaulting to 1 as with most elements. It is available for use in more complex tied notation situations. +The number attribute is rarely needed to disambiguate ties, since note pitches will usually suffice. The attribute is implied rather than defaulting to 1 as with most elements. It is available for use in more complex tied notation situations. + +Ties that join two notes of the same pitch together should be represented with a tied element on the first note with type="start" and a tied element on the second note with type="stop". This can also be done if the two notes being tied are enharmonically equivalent, but have different step values. It is not recommended to use tied elements to join two notes with enharmonically inequivalent pitches. + +Ties that indicate that an instrument should be undamped are specified with a single tied element with type="let-ring". + +Ties that are visually attached to only one note, other than undamped ties, should be specified with two tied elements on the same note, first type="start" then type="stop". This can be used to represent ties into or out of repeated sections or codas. - + @@ -4609,6 +5243,7 @@ The number attribute is rarely needed to disambiguate ties, since note pitches w + @@ -4643,17 +5278,20 @@ The number attribute is rarely needed to disambiguate ties, since note pitches w - The tremolo ornament can be used to indicate either single-note or double-note tremolos. Single-note tremolos use the single type, while double-note tremolos use the start and stop types. The default is "single" for compatibility with Version 1.1. The text of the element indicates the number of tremolo marks and is an integer from 0 to 8. Note that the number of attached beams is not included in this value, but is represented separately using the beam element. - -When using double-note tremolos, the duration of each note in the tremolo should correspond to half of the notated type value. A time-modification element should also be added with an actual-notes value of 2 and a normal-notes value of 1. If used within a tuplet, this 2/1 ratio should be multiplied by the existing tuplet ratio. - -Using repeater beams for indicating tremolos is deprecated as of MusicXML 3.0. + The tremolo ornament can be used to indicate single-note, double-note, or unmeasured tremolos. Single-note tremolos use the single type, double-note tremolos use the start and stop types, and unmeasured tremolos use the unmeasured type. The default is "single" for compatibility with Version 1.1. The text of the element indicates the number of tremolo marks and is an integer from 0 to 8. Note that the number of attached beams is not included in this value, but is represented separately using the beam element. The value should be 0 for unmeasured tremolos. + + When using double-note tremolos, the duration of each note in the tremolo should correspond to half of the notated type value. A time-modification element should also be added with an actual-notes value of 2 and a normal-notes value of 1. If used within a tuplet, this 2/1 ratio should be multiplied by the existing tuplet ratio. + + The smufl attribute specifies the glyph to use from the SMuFL tremolos range for an unmeasured tremolo. It is ignored for other tremolo types. The SMuFL buzzRoll glyph is used by default if the attribute is missing. + + Using repeater beams for indicating tremolos is deprecated as of MusicXML 3.0. - + + @@ -4661,9 +5299,9 @@ Using repeater beams for indicating tremolos is deprecated as of MusicXML 3.0. A tuplet element is present when a tuplet is to be displayed graphically, in addition to the sound data provided by the time-modification elements. The number attribute is used to distinguish nested tuplets. The bracket attribute is used to indicate the presence of a bracket. If unspecified, the results are implementation-dependent. The line-shape attribute is used to specify whether the bracket is straight or in the older curved or slurred style. It is straight by default. - -Whereas a time-modification element shows how the cumulative, sounding effect of tuplets and double-note tremolos compare to the written note type, the tuplet element describes how this is displayed. The tuplet element also provides more detailed representation information than the time-modification element, and is needed to represent nested tuplets and other complex tuplets accurately. - + +Whereas a time-modification element shows how the cumulative, sounding effect of tuplets and double-note tremolos compare to the written note type, the tuplet element describes how this is displayed. The tuplet element also provides more detailed representation information than the time-modification element, and is needed to represent nested tuplets and other complex tuplets accurately. + The show-number attribute is used to display either the number of actual notes, the number of both actual and normal notes, or neither. It is actual by default. The show-type attribute is used to display either the actual type, both the actual and normal types, or neither. It is none by default. @@ -4686,6 +5324,7 @@ The show-number attribute is used to display either the number of actual notes, + @@ -4718,7 +5357,7 @@ The show-number attribute is used to display either the number of actual notes, - + The tuplet-type type indicates the graphical note type of the notes for this portion of the tuplet. @@ -4739,18 +5378,18 @@ The show-number attribute is used to display either the number of actual notes, - + - The credit type represents the appearance of the title, composer, arranger, lyricist, copyright, dedication, and other text and graphics that commonly appears on the first page of a score. The credit-words and credit-image elements are similar to the words and image elements for directions. However, since the credit is not part of a measure, the default-x and default-y attributes adjust the origin relative to the bottom left-hand corner of the first page. The enclosure for credit-words is none by default. - -By default, a series of credit-words elements within a single credit element follow one another in sequence visually. Non-positional formatting attributes are carried over from the previous element by default. - -The page attribute for the credit element, new in Version 2.0, specifies the page number where the credit should appear. This is an integer value that starts with 1 for the first page. Its value is 1 by default. Since credits occur before the music, these page numbers do not refer to the page numbering specified by the print element's page-number attribute. + The credit type represents the appearance of the title, composer, arranger, lyricist, copyright, dedication, and other text, symbols, and graphics that commonly appear on the first page of a score. The credit-words, credit-symbol, and credit-image elements are similar to the words, symbol, and image elements for directions. However, since the credit is not part of a measure, the default-x and default-y attributes adjust the origin relative to the bottom left-hand corner of the page. The enclosure for credit-words and credit-symbol is none by default. + +By default, a series of credit-words and credit-symbol elements within a single credit element follow one another in sequence visually. Non-positional formatting attributes are carried over from the previous element by default. -The credit-type element, new in Version 3.0, indicates the purpose behind a credit. Multiple types of data may be combined in a single credit, so multiple elements may be used. Standard values include page number, title, subtitle, composer, arranger, lyricist, and rights. +The page attribute for the credit element specifies the page number where the credit should appear. This is an integer value that starts with 1 for the first page. Its value is 1 by default. Since credits occur before the music, these page numbers do not refer to the page numbering specified by the print element's page-number attribute. + +The credit-type element indicates the purpose behind a credit. Multiple types of data may be combined in a single credit, so multiple elements may be used. Standard values include page number, title, subtitle, composer, arranger, lyricist, and rights. @@ -4760,16 +5399,23 @@ The credit-type element, new in Version 3.0, indicates the purpose behind a cred - + + + + - + + + + + @@ -4856,9 +5502,9 @@ The credit-type element, new in Version 3.0, indicates the purpose behind a cred The part-group element indicates groupings of parts in the score, usually indicated by braces and brackets. Braces that are used for multi-staff parts should be defined in the attributes element for that part. The part-group start element appears before the first score-part in the group. The part-group stop element appears after the last score-part in the group. - -The number attribute is used to distinguish overlapping and nested part-groups, not the sequence of groups. As with parts, groups can have a name and abbreviation. Values for the child elements are ignored at the stop of a group. - + +The number attribute is used to distinguish overlapping and nested part-groups, not the sequence of groups. As with parts, groups can have a name and abbreviation. Values for the child elements are ignored at the stop of a group. + A part-group element is not needed for a single multi-staff part. By default, multi-staff parts include a brace symbol and (if appropriate given the bar-style) common barlines. The symbol formatting for a multi-staff part can be more fully specified using the part-symbol element. @@ -4900,7 +5546,7 @@ A part-group element is not needed for a single multi-staff part. By default, mu - + The part-name type describes the name or abbreviation of a score-part element. Formatting attributes for the part-name element are deprecated in Version 2.0 in favor of the new part-name-display and part-abbreviation-display elements. @@ -4915,7 +5561,7 @@ A part-group element is not needed for a single multi-staff part. By default, mu The score-instrument type represents a single instrument within a score-part. As with the score-part type, each score-instrument has a required ID attribute, a name, and an optional abbreviation. - + A score-instrument type is also required if the score specifies MIDI 1.0 channels, banks, or programs. An initial midi-instrument assignment can also be made here. MusicXML software should be able to automatically assign reasonable channels and instruments without these elements in simple cases, such as where part names match General MIDI instrument names. @@ -5013,7 +5659,7 @@ A score-instrument type is also required if the score specifies MIDI 1.0 channel - + The editorial group specifies editorial information for a musical element. @@ -5126,7 +5772,7 @@ A score-instrument type is also required if the score specifies MIDI 1.0 channel Non-traditional key signatures can be represented using the Humdrum/Scot concept of a list of altered tones. The key-alter element represents the alteration for a given pitch step, represented with semitones in the same manner as the alter element. - + Non-traditional key signatures can be represented using the Humdrum/Scot concept of a list of altered tones. The key-accidental element indicates the accidental to be displayed in the key signature, represented in the same manner as the accidental element. It is used for disambiguating microtonal accidentals. @@ -5139,14 +5785,21 @@ A score-instrument type is also required if the score specifies MIDI 1.0 channel The slash group combines elements used for more complete specification of the slash and beat-repeat measure-style elements. They have the same values as the type and dot elements, and define what the beat is for the display of repetition marks. If not present, the beat is based on the current time signature. - - - The slash-type element indicates the graphical note type to use for the display of repetition marks. - - - + + + + The slash-type element indicates the graphical note type to use for the display of repetition marks. + + + + + The slash-dot element is used to specify any augmentation dots in the note type used to display repetition marks. + + + + - The slash-dot element is used to specify any augmentation dots in the note type used to display repetition marks. + The except-voice element is used to specify a combination of slash notation and regular notation. Any note elements that are in voices specified by the except-voice elements are displayed in normal notation, in addition to the slash notation that is always displayed. @@ -5204,7 +5857,7 @@ A score-instrument type is also required if the score specifies MIDI 1.0 channel A harmony element can contain many stacked chords (e.g. V of II). A sequence of harmony-chord groups is used for this type of secondary function, where V of II would be represented by a harmony-chord with a V function followed by a harmony-chord with a II function. - + A root is a pitch name like C, D, E, where a function is an indication like I, II, III. It is an either/or choice to avoid data inconsistency. @@ -5313,7 +5966,7 @@ A root is a pitch name like C, D, E, where a function is an indication like I, I - + diff --git a/tests/test_xml.py b/tests/test_xml.py index e25f6ae5..37f00c98 100644 --- a/tests/test_xml.py +++ b/tests/test_xml.py @@ -4,50 +4,41 @@ import ly.musicxml from lxml import etree import os -import io +import re +import ly.pkginfo +import glob -def test_glissando(): - compare_output('glissando') - - -def test_tie(): - compare_output('tie') - - -def test_merge_voice(): - compare_output('merge_voice') - - -def test_variable(): - compare_output('variable') - - -def test_dynamics(): - compare_output('dynamics') - - -def test_tuplet(): - compare_output('tuplet') +def test_all(): + """Test all files in test_xml_files""" + filebase = os.path.join(os.path.dirname(__file__), 'test_xml_files') + # Help from https://www.mkyong.com/python/python-how-to-list-all-files-in-a-directory/ + # Searches test_xml_files directory for all lilypond files and extracts their name (without .ly) + test_list = [re.search(r'([^/]*)\.ly', f).group(1) for f in glob.glob(filebase + "/*.ly")] + for test in test_list: + print("Testing {}.ly...".format(test)) + compare_output(test) + print(test + " test passed.\n") def ly_to_xml(filename): """Read Lilypond file and return XML string.""" writer = ly.musicxml.writer() - with open(filename, 'r') as lyfile: + with open(filename, 'r', encoding='utf-8') as lyfile: writer.parse_text(lyfile.read()) xml = writer.musicxml() - sio = io.StringIO() - xml.write(sio, "utf-8") - return sio.getvalue() + return (ly.musicxml.create_musicxml.xml_decl_txt.format(encoding='utf-8') + "\n" + + ly.musicxml.create_musicxml.doctype_txt + "\n" + + xml.tostring(encoding='unicode')) def read_expected_xml(filename): """Return string with expected XML from file.""" - with open(filename, 'r') as xmlfile: + with open(filename, 'r', encoding='utf-8') as xmlfile: output = xmlfile.read() - # Replace date in XML file with today's date - output = output.replace("2016-03-28", str(datetime.date.today())) + # Replace date and python-ly version in XML file with today's date and current version + output = re.sub(r'\d{4}-\d{2}-\d{2}', str(datetime.date.today()), output) + output = re.sub(r'python-ly \d*\.\d*\.\d*', "python-ly " + ly.pkginfo.version, output) return output @@ -57,7 +48,7 @@ def compare_output(filename): filename) output = ly_to_xml(filebase + '.ly') - expected_output = read_expected_xml(filebase + '.xml') + expected_output = read_expected_xml(filebase + '.musicxml') assert_multi_line_equal(expected_output, output) validate_xml(output) @@ -65,14 +56,15 @@ def compare_output(filename): def validate_xml(xml): """Validate XML against XSD file.""" + # see https://www.w3.org/2011/prov/track/issues/480 + # and https://stackoverflow.com/questions/49534700/how-to-use-xlink-data-types-in-xsd + # and https://stackoverflow.com/questions/15830421/xml-unicode-strings-with-encoding-declaration-are-not-supported + xml = xml.encode('utf-8') xsdname = os.path.join(os.path.dirname(__file__), 'musicxml.xsd') - xsdfile = open(xsdname, 'r') - xmlschema_doc = etree.parse(xsdfile) - xsdfile.close() - xmlschema = etree.XMLSchema(xmlschema_doc) - parser = etree.XMLParser(schema=xmlschema) + xmlschema = etree.XMLSchema(file=xsdname) + parser = etree.XMLParser(schema=xmlschema, encoding='utf-8') # Raises Exception if not valid: - etree.fromstring(xml, parser) + etree.fromstring(xml, parser=parser) def assert_multi_line_equal(first, second, msg=None): @@ -89,3 +81,8 @@ def assert_multi_line_equal(first, second, msg=None): if msg: message += " : " + msg assert False, "Multi-line strings are unequal:\n" + message + + +if __name__ == "__main__": + # sys.exit(main(sys.argv)) + test_all() diff --git a/tests/test_xml_files/accidentals.ly b/tests/test_xml_files/accidentals.ly new file mode 100644 index 00000000..376db2a8 --- /dev/null +++ b/tests/test_xml_files/accidentals.ly @@ -0,0 +1,31 @@ +\version "2.18.2" + +\language "english" + +keyTime = { + \time 4/4 +} + +Soprano = \relative c'' { + \voiceOne + \keyTime + gf4 gf \bar "||" gf g + | gss gss g gf~ + | gf~ gf gf gf + | \key a \major gs gss g gf + | \key c \minor af aff a as + | af?2 af! +} + +\score +{ + << + \new Staff = "treble" \with { + } + << + \clef "treble" + \new Voice = "SopranoVoice" \Soprano + >> + >> + +} diff --git a/tests/test_xml_files/accidentals.musicxml b/tests/test_xml_files/accidentals.musicxml new file mode 100644 index 00000000..8f01764e --- /dev/null +++ b/tests/test_xml_files/accidentals.musicxml @@ -0,0 +1,313 @@ + + + + + + python-ly 0.9.5 + 2019-06-17 + + + + + + + + + + + 1 + + + G + 2 + + + + + G + -1 + 4 + + 1 + 1 + quarter + flat + + + + G + -1 + 4 + + 1 + 1 + quarter + + + 2 + + + + light-light + + + + + + G + -1 + 4 + + 1 + 1 + quarter + flat + + + + G + 4 + + 1 + 1 + quarter + natural + + + + + + G + 2 + 4 + + 1 + 1 + quarter + sharp-sharp + + + + G + 2 + 4 + + 1 + 1 + quarter + + + + G + 4 + + 1 + 1 + quarter + natural + + + + G + -1 + 4 + + 1 + + 1 + quarter + flat + + + + + + + + + G + -1 + 4 + + 1 + + + 1 + quarter + + + + + + + + G + -1 + 4 + + 1 + + 1 + quarter + + + + + + + G + -1 + 4 + + 1 + 1 + quarter + flat + + + + G + -1 + 4 + + 1 + 1 + quarter + + + + + + 3 + major + + + + + G + 1 + 4 + + 1 + 1 + quarter + + + + G + 2 + 4 + + 1 + 1 + quarter + sharp-sharp + + + + G + 4 + + 1 + 1 + quarter + natural + + + + G + -1 + 4 + + 1 + 1 + quarter + flat + + + 4 + + + + + + -3 + minor + + + + + A + -1 + 4 + + 1 + 1 + quarter + + + + A + -2 + 4 + + 1 + 1 + quarter + flat-flat + + + + A + 4 + + 1 + 1 + quarter + natural + + + + A + 1 + 4 + + 1 + 1 + quarter + sharp + + + 4 + + + + + + A + -1 + 4 + + 2 + 1 + half + flat + + + + A + -1 + 4 + + 2 + 1 + half + flat + + + + diff --git a/tests/test_xml_files/accidentals2.ly b/tests/test_xml_files/accidentals2.ly new file mode 100644 index 00000000..13161228 --- /dev/null +++ b/tests/test_xml_files/accidentals2.ly @@ -0,0 +1,61 @@ +\version "2.18.2" + +\language "english" + +keyTime = { + \time 4/4 + \key f \major +} + +Soprano = \relative c'' { + \keyTime + \voiceOne + b4 b b' e, + e4 ef ef b + \times 4/3 {af4 af' af} +} + +Alto = \relative c'' { + \keyTime + \voiceTwo + e4 ef ef b + b4 b b e + c4 c c c +} + +Tenor = \relative c { + \keyTime + \voiceOne + b4 b b e + e4 ef ef b + \times 4/3 {af4 af' af} +} + +Bass = \relative c { + \keyTime + \voiceTwo + e4 ef ef b + b4 q e + c4 c,, c c +} + +\score +{ + << + \new Staff = "treble" \with { + } + << + \clef "treble" + \new Voice = "SopranoVoice" \Soprano + \new Voice = "AltoVoice" \Alto + >> + \new Staff = "bass" \with { + } + << + \clef "bass" + \new Voice = "TenorVoice" \Tenor + \new Voice = "BassVoice" \Bass + >> + >> + +} diff --git a/tests/test_xml_files/accidentals2.musicxml b/tests/test_xml_files/accidentals2.musicxml new file mode 100644 index 00000000..7b851dea --- /dev/null +++ b/tests/test_xml_files/accidentals2.musicxml @@ -0,0 +1,590 @@ + + + + + + python-ly 0.9.5 + 2019-10-31 + + + + + + + + + + + + + + 3 + + -1 + major + + + + G + 2 + + + + + B + 4 + + 3 + 1 + quarter + natural + + + + B + 4 + + 3 + 1 + quarter + + + + B + 5 + + 3 + 1 + quarter + natural + + + + E + 5 + + 3 + 1 + quarter + natural + + + 12 + + + + E + 5 + + 3 + 2 + quarter + + + + E + -1 + 5 + + 3 + 2 + quarter + flat + + + + E + -1 + 5 + + 3 + 2 + quarter + + + + B + 4 + + 3 + 2 + quarter + + + 12 + + + + + + E + 5 + + 3 + 1 + quarter + + + + E + -1 + 5 + + 3 + 1 + quarter + flat + + + + E + -1 + 5 + + 3 + 1 + quarter + + + + B + 4 + + 3 + 1 + quarter + + + 12 + + + + B + 4 + + 3 + 2 + quarter + natural + + + + B + 4 + + 3 + 2 + quarter + + + + B + 4 + + 3 + 2 + quarter + + + + E + 5 + + 3 + 2 + quarter + natural + + + + + + A + -1 + 4 + + 4 + 1 + quarter + flat + + 3 + 4 + + + + + + + + A + -1 + 5 + + 4 + 1 + quarter + flat + + 3 + 4 + + + + + A + -1 + 5 + + 4 + 1 + quarter + + 3 + 4 + + + + + + + 12 + + + + C + 5 + + 3 + 2 + quarter + + + + C + 5 + + 3 + 2 + quarter + + + + C + 5 + + 3 + 2 + quarter + + + + C + 5 + + 3 + 2 + quarter + + + + + + + 3 + + -1 + major + + + + F + 4 + + + + + B + 2 + + 3 + 1 + quarter + natural + + + + B + 2 + + 3 + 1 + quarter + + + + B + 2 + + 3 + 1 + quarter + + + + E + 3 + + 3 + 1 + quarter + natural + + + 12 + + + + E + 3 + + 3 + 2 + quarter + + + + E + -1 + 3 + + 3 + 2 + quarter + flat + + + + E + -1 + 3 + + 3 + 2 + quarter + + + + B + 2 + + 3 + 2 + quarter + + + 12 + + + + + + E + 3 + + 3 + 1 + quarter + + + + E + -1 + 3 + + 3 + 1 + quarter + flat + + + + E + -1 + 3 + + 3 + 1 + quarter + + + + B + 2 + + 3 + 1 + quarter + + + 12 + + + + B + 2 + + 3 + 2 + quarter + natural + + + + B + 2 + + 3 + 2 + quarter + + + + + G + 2 + + 3 + 2 + quarter + + + + B + 2 + + 3 + 2 + quarter + + + + + G + 2 + + 3 + 2 + quarter + + + + E + 3 + + 3 + 2 + quarter + natural + + + + + + A + -1 + 2 + + 4 + 1 + quarter + flat + + 3 + 4 + + + + + + + + A + -1 + 3 + + 4 + 1 + quarter + flat + + 3 + 4 + + + + + A + -1 + 3 + + 4 + 1 + quarter + + 3 + 4 + + + + + + + 12 + + + + C + 3 + + 3 + 2 + quarter + + + + C + 1 + + 3 + 2 + quarter + + + + C + 1 + + 3 + 2 + quarter + + + + C + 1 + + 3 + 2 + quarter + + + + diff --git a/tests/test_xml_files/barlines.ly b/tests/test_xml_files/barlines.ly new file mode 100644 index 00000000..bad34039 --- /dev/null +++ b/tests/test_xml_files/barlines.ly @@ -0,0 +1,71 @@ +\version "2.18.2" + +\language "english" + +keyTime = { + \time 4/4 +} + +Soprano = \relative c'' { + \voiceOne + \keyTime + r4 r \bar "'" g g \bar "||" + | g g \bar "" g g \bar "!" + | g g \bar "|" g g \bar "." + | g g \bar ".|" g g \bar ".." + | g g \bar ";" \bar ";" g g \bar ";" + \bar "|." +} + +Alto = \relative c' { + \voiceTwo + \keyTime + e4 e e e + | e e e e + | e e e e + | e e e e + | e e e e +} + +Tenor = \relative c { + \voiceOne + \keyTime + c4 c c c + | c c c c + | c c c c + | c c c c + | c c c c +} + +Bass = \relative c { + \voiceTwo + \keyTime + a4 a a a + | a a a a + | a a a a + | a a a a + | a a a a +} + +\score +{ + << + \new Staff = "treble" \with { + } + << + \clef "treble" + \new Voice = "SopranoVoice" \Soprano + \new Voice = "AltoVoice" \Alto + >> + + \new Staff = "bass" \with { + } + << + \clef "bass" + \new Voice = "TenorVoice" \Tenor + \new Voice = "BassVoice" \Bass + >> + + >> + +} diff --git a/tests/test_xml_files/barlines.musicxml b/tests/test_xml_files/barlines.musicxml new file mode 100644 index 00000000..188c2581 --- /dev/null +++ b/tests/test_xml_files/barlines.musicxml @@ -0,0 +1,999 @@ + + + + + + python-ly 0.9.5 + 2019-08-15 + + + + + + + + + + + + + + 1 + + + G + 2 + + + + + 1 + 1 + quarter + + + + 1 + 1 + quarter + + + 2 + + + + E + 4 + + 1 + 2 + quarter + + + + E + 4 + + 1 + 2 + quarter + + + 2 + + + + tick + + + + + + G + 4 + + 1 + 1 + quarter + + + + G + 4 + + 1 + 1 + quarter + + + 2 + + + + E + 4 + + 1 + 2 + quarter + + + + E + 4 + + 1 + 2 + quarter + + + 2 + + + + light-light + + + + + + G + 4 + + 1 + 1 + quarter + + + + G + 4 + + 1 + 1 + quarter + + + 2 + + + + E + 4 + + 1 + 2 + quarter + + + + E + 4 + + 1 + 2 + quarter + + + 2 + + + + none + + + + + + G + 4 + + 1 + 1 + quarter + + + + G + 4 + + 1 + 1 + quarter + + + 2 + + + + E + 4 + + 1 + 2 + quarter + + + + E + 4 + + 1 + 2 + quarter + + + 2 + + + + dashed + + + + + + G + 4 + + 1 + 1 + quarter + + + + G + 4 + + 1 + 1 + quarter + + + 2 + + + + E + 4 + + 1 + 2 + quarter + + + + E + 4 + + 1 + 2 + quarter + + + 2 + + + + regular + + + + + + G + 4 + + 1 + 1 + quarter + + + + G + 4 + + 1 + 1 + quarter + + + 2 + + + + E + 4 + + 1 + 2 + quarter + + + + E + 4 + + 1 + 2 + quarter + + + 2 + + + + heavy + + + + + + G + 4 + + 1 + 1 + quarter + + + + G + 4 + + 1 + 1 + quarter + + + 2 + + + + E + 4 + + 1 + 2 + quarter + + + + E + 4 + + 1 + 2 + quarter + + + 2 + + + + heavy-light + + + + + + G + 4 + + 1 + 1 + quarter + + + + G + 4 + + 1 + 1 + quarter + + + 2 + + + + E + 4 + + 1 + 2 + quarter + + + + E + 4 + + 1 + 2 + quarter + + + 2 + + + + heavy-heavy + + + + + + G + 4 + + 1 + 1 + quarter + + + + G + 4 + + 1 + 1 + quarter + + + 2 + + + + E + 4 + + 1 + 2 + quarter + + + + E + 4 + + 1 + 2 + quarter + + + 2 + + + + dotted + + + + + + G + 4 + + 1 + 1 + quarter + + + + G + 4 + + 1 + 1 + quarter + + + 2 + + + + E + 4 + + 1 + 2 + quarter + + + + E + 4 + + 1 + 2 + quarter + + + 2 + + + + light-heavy + + + + + + + 1 + + + F + 4 + + + + + C + 3 + + 1 + 1 + quarter + + + + C + 3 + + 1 + 1 + quarter + + + 2 + + + + A + 2 + + 1 + 2 + quarter + + + + A + 2 + + 1 + 2 + quarter + + + 2 + + + + tick + + + + + + C + 3 + + 1 + 1 + quarter + + + + C + 3 + + 1 + 1 + quarter + + + 2 + + + + A + 2 + + 1 + 2 + quarter + + + + A + 2 + + 1 + 2 + quarter + + + 2 + + + + light-light + + + + + + C + 3 + + 1 + 1 + quarter + + + + C + 3 + + 1 + 1 + quarter + + + 2 + + + + A + 2 + + 1 + 2 + quarter + + + + A + 2 + + 1 + 2 + quarter + + + 2 + + + + none + + + + + + C + 3 + + 1 + 1 + quarter + + + + C + 3 + + 1 + 1 + quarter + + + 2 + + + + A + 2 + + 1 + 2 + quarter + + + + A + 2 + + 1 + 2 + quarter + + + 2 + + + + dashed + + + + + + C + 3 + + 1 + 1 + quarter + + + + C + 3 + + 1 + 1 + quarter + + + 2 + + + + A + 2 + + 1 + 2 + quarter + + + + A + 2 + + 1 + 2 + quarter + + + 2 + + + + regular + + + + + + C + 3 + + 1 + 1 + quarter + + + + C + 3 + + 1 + 1 + quarter + + + 2 + + + + A + 2 + + 1 + 2 + quarter + + + + A + 2 + + 1 + 2 + quarter + + + 2 + + + + heavy + + + + + + C + 3 + + 1 + 1 + quarter + + + + C + 3 + + 1 + 1 + quarter + + + 2 + + + + A + 2 + + 1 + 2 + quarter + + + + A + 2 + + 1 + 2 + quarter + + + 2 + + + + heavy-light + + + + + + C + 3 + + 1 + 1 + quarter + + + + C + 3 + + 1 + 1 + quarter + + + 2 + + + + A + 2 + + 1 + 2 + quarter + + + + A + 2 + + 1 + 2 + quarter + + + 2 + + + + heavy-heavy + + + + + + C + 3 + + 1 + 1 + quarter + + + + C + 3 + + 1 + 1 + quarter + + + 2 + + + + A + 2 + + 1 + 2 + quarter + + + + A + 2 + + 1 + 2 + quarter + + + 2 + + + + dotted + + + + + + C + 3 + + 1 + 1 + quarter + + + + C + 3 + + 1 + 1 + quarter + + + 2 + + + + A + 2 + + 1 + 2 + quarter + + + + A + 2 + + 1 + 2 + quarter + + + 2 + + + + light-heavy + + + + diff --git a/tests/test_xml_files/barlines2.ly b/tests/test_xml_files/barlines2.ly new file mode 100644 index 00000000..595ec71f --- /dev/null +++ b/tests/test_xml_files/barlines2.ly @@ -0,0 +1,58 @@ +\version "2.18.2" + +\language "english" + +keyTime = { + \time 4/4 +} + +Soprano = \relative c'' { + \voiceOne + \keyTime + c4 c c c + c c c c + c c c c c + c c c c + c c c c + c c c c + c c c + c c c +} + +Alto = \relative c'' { + \voiceTwo + \keyTime + a4 s q + a a a \bar "|." a + \time 5/4 a8 a a a a a a a a a \bar "" + \time 4/4 a a a a a a a a \bar "|." + a1 + a1 + a2. + a2. +} + +Tenor = \relative c { + \voiceOne + \keyTime + \clef "bass" + a4 a \bar "||" a a + a a a a + a8 a a a a a a a a a + \time 8/8 a a a a a a a a \bar "||" + \time 4/4 \times 1/2 {a2 a} \tuplet 2/1 {a2 a} \bar "||" + << {c1 \time 3/4 c2. c2. \bar "|."} \\ {a1 \bar "" a2. a2.} >> +} + +\score +{ + << + \new Staff = "treble" \with {} + << + \clef "treble" + \new Voice = "SopranoVoice" \Soprano + \new Voice = "AltoVoice" \Alto + >> + \new Staff \Tenor + >> +} diff --git a/tests/test_xml_files/barlines2.musicxml b/tests/test_xml_files/barlines2.musicxml new file mode 100644 index 00000000..e1f0c0c5 --- /dev/null +++ b/tests/test_xml_files/barlines2.musicxml @@ -0,0 +1,1251 @@ + + + + + + python-ly 0.9.5 + 2019-08-27 + + + + + + + + + + + + + + 2 + + + G + 2 + + + + + C + 5 + + 2 + 1 + quarter + + + + C + 5 + + 2 + 1 + quarter + + + 4 + + + + A + 4 + + 2 + 2 + quarter + + + 2 + + + 4 + + + + light-light + + + + + + C + 5 + + 2 + 1 + quarter + + + + C + 5 + + 2 + 1 + quarter + + + 4 + + + + A + 4 + + 2 + 2 + quarter + + + + + B + 4 + + 2 + 2 + quarter + + + + A + 4 + + 2 + 2 + quarter + + + + + B + 4 + + 2 + 2 + quarter + + + + + + C + 5 + + 2 + 1 + quarter + + + + C + 5 + + 2 + 1 + quarter + + + + C + 5 + + 2 + 1 + quarter + + + 6 + + + + A + 4 + + 2 + 2 + quarter + + + + A + 4 + + 2 + 2 + quarter + + + + A + 4 + + 2 + 2 + quarter + + + 6 + + + + light-heavy + + + + + + C + 5 + + 2 + 1 + quarter + + + 2 + + + + A + 4 + + 2 + 2 + quarter + + + + + + + + + C + 5 + + 2 + 1 + quarter + + + + C + 5 + + 2 + 1 + quarter + + + + C + 5 + + 2 + 1 + quarter + + + + C + 5 + + 2 + 1 + quarter + + + + C + 5 + + 2 + 1 + quarter + + + 10 + + + + A + 4 + + 1 + 2 + eighth + begin + + + + A + 4 + + 1 + 2 + eighth + end + + + + A + 4 + + 1 + 2 + eighth + begin + + + + A + 4 + + 1 + 2 + eighth + end + + + + A + 4 + + 1 + 2 + eighth + begin + + + + A + 4 + + 1 + 2 + eighth + end + + + + A + 4 + + 1 + 2 + eighth + begin + + + + A + 4 + + 1 + 2 + eighth + end + + + + A + 4 + + 1 + 2 + eighth + begin + + + + A + 4 + + 1 + 2 + eighth + end + + + 10 + + + + none + + + + + + + + + C + 5 + + 2 + 1 + quarter + + + + C + 5 + + 2 + 1 + quarter + + + + C + 5 + + 2 + 1 + quarter + + + + C + 5 + + 2 + 1 + quarter + + + 8 + + + + A + 4 + + 1 + 2 + eighth + begin + + + + A + 4 + + 1 + 2 + eighth + continue + + + + A + 4 + + 1 + 2 + eighth + end + + + + A + 4 + + 1 + 2 + eighth + begin + + + + A + 4 + + 1 + 2 + eighth + continue + + + + A + 4 + + 1 + 2 + eighth + end + + + + A + 4 + + 1 + 2 + eighth + begin + + + + A + 4 + + 1 + 2 + eighth + end + + + 8 + + + + light-light + + + + + + + + + C + 5 + + 2 + 1 + quarter + + + + C + 5 + + 2 + 1 + quarter + + + + C + 5 + + 2 + 1 + quarter + + + + C + 5 + + 2 + 1 + quarter + + + 8 + + + + A + 4 + + 8 + 2 + whole + + + 8 + + + + light-light + + + + + + C + 5 + + 2 + 1 + quarter + + + + C + 5 + + 2 + 1 + quarter + + + + C + 5 + + 2 + 1 + quarter + + + + C + 5 + + 2 + 1 + quarter + + + 8 + + + + A + 4 + + 8 + 2 + whole + + + 8 + + + + none + + + + + + + + + C + 5 + + 2 + 1 + quarter + + + + C + 5 + + 2 + 1 + quarter + + + + C + 5 + + 2 + 1 + quarter + + + 6 + + + + A + 4 + + 6 + 2 + half + + + + 6 + + + + + + + + + C + 5 + + 2 + 1 + quarter + + + + C + 5 + + 2 + 1 + quarter + + + + C + 5 + + 2 + 1 + quarter + + + 6 + + + + A + 4 + + 6 + 2 + half + + + + 6 + + + + light-heavy + + + + + + + 2 + + + F + 4 + + + + + A + 2 + + 2 + 1 + quarter + + + + A + 2 + + 2 + 1 + quarter + + + 4 + + + + light-light + + + + + + A + 2 + + 2 + 1 + quarter + + + + A + 2 + + 2 + 1 + quarter + + + + + + A + 2 + + 2 + 1 + quarter + + + + A + 2 + + 2 + 1 + quarter + + + + A + 2 + + 2 + 1 + quarter + + + 6 + + + + light-heavy + + + + + + A + 2 + + 2 + 1 + quarter + + + + + + + + + A + 2 + + 1 + 1 + eighth + begin + + + + A + 2 + + 1 + 1 + eighth + end + + + + A + 2 + + 1 + 1 + eighth + begin + + + + A + 2 + + 1 + 1 + eighth + end + + + + A + 2 + + 1 + 1 + eighth + begin + + + + A + 2 + + 1 + 1 + eighth + end + + + + A + 2 + + 1 + 1 + eighth + begin + + + + A + 2 + + 1 + 1 + eighth + end + + + + A + 2 + + 1 + 1 + eighth + begin + + + + A + 2 + + 1 + 1 + eighth + end + + + 10 + + + + none + + + + + + + + + A + 2 + + 1 + 1 + eighth + begin + + + + A + 2 + + 1 + 1 + eighth + continue + + + + A + 2 + + 1 + 1 + eighth + end + + + + A + 2 + + 1 + 1 + eighth + begin + + + + A + 2 + + 1 + 1 + eighth + continue + + + + A + 2 + + 1 + 1 + eighth + end + + + + A + 2 + + 1 + 1 + eighth + begin + + + + A + 2 + + 1 + 1 + eighth + end + + + 8 + + + + light-light + + + + + + + + + A + 2 + + 2 + 1 + half + + 2 + 1 + + + + + + + + A + 2 + + 2 + 1 + half + + 2 + 1 + + + + + + + + A + 2 + + 2 + 1 + half + + 2 + 1 + + + + + + + + A + 2 + + 2 + 1 + half + + 2 + 1 + + + + + + + 8 + + + + light-light + + + + + + C + 3 + + 8 + 1 + whole + + + 8 + + + + A + 2 + + 8 + 2 + whole + + + 8 + + + + none + + + + + + + + + + + + C + 3 + + 6 + 1 + half + + + + 6 + + + + A + 2 + + 6 + 2 + half + + + + 6 + + + + + + + + + C + 3 + + 6 + 1 + half + + + + 6 + + + + A + 2 + + 6 + 2 + half + + + + 6 + + + + light-heavy + + + + diff --git a/tests/test_xml_files/barlines3.ly b/tests/test_xml_files/barlines3.ly new file mode 100644 index 00000000..7524e46d --- /dev/null +++ b/tests/test_xml_files/barlines3.ly @@ -0,0 +1,45 @@ +\version "2.18.2" + +\language "english" + +keyTime = { + \time 4/4 +} + +Soprano = \relative c'' { + \voiceOne + \keyTime + c1 + c2 \bar "||" c + c4 c \bar "||" c c +} + +Alto = \relative c'' { + \voiceTwo + \keyTime + a2 \bar "||" a + a1 + a4 a a a \bar "|." +} + +Tenor = \relative c { + \voiceOne + \keyTime + \clef "bass" + a2 a + a a + a a +} + +\score +{ + << + \new Staff = "treble" \with {} + << + \clef "treble" + \new Voice = "SopranoVoice" \Soprano + \new Voice = "AltoVoice" \Alto + >> + \new Staff \Tenor + >> +} diff --git a/tests/test_xml_files/barlines3.musicxml b/tests/test_xml_files/barlines3.musicxml new file mode 100644 index 00000000..1e82780c --- /dev/null +++ b/tests/test_xml_files/barlines3.musicxml @@ -0,0 +1,287 @@ + + + + + + python-ly 0.9.5 + 2019-11-07 + + + + + + + + + + + + + + 1 + + + G + 2 + + + + + C + 5 + + 4 + 1 + whole + + + 4 + + + + A + 4 + + 2 + 2 + half + + + + A + 4 + + 2 + 2 + half + + + 4 + + + + + + C + 5 + + 2 + 1 + half + + + + C + 5 + + 2 + 1 + half + + + 4 + + + + A + 4 + + 4 + 2 + whole + + + + + + C + 5 + + 1 + 1 + quarter + + + + C + 5 + + 1 + 1 + quarter + + + 2 + + + + A + 4 + + 1 + 2 + quarter + + + + A + 4 + + 1 + 2 + quarter + + + 2 + + + + light-light + + + + + + C + 5 + + 1 + 1 + quarter + + + + C + 5 + + 1 + 1 + quarter + + + 2 + + + + A + 4 + + 1 + 2 + quarter + + + + A + 4 + + 1 + 2 + quarter + + + 2 + + + + light-heavy + + + + + + + 1 + + + F + 4 + + + + + A + 2 + + 2 + 1 + half + + + + A + 2 + + 2 + 1 + half + + + 4 + + + + + + A + 2 + + 2 + 1 + half + + + + A + 2 + + 2 + 1 + half + + + + + + A + 2 + + 2 + 1 + half + + + 2 + + + + light-light + + + + + + A + 2 + + 2 + 1 + half + + + 2 + + + + light-heavy + + + + diff --git a/tests/test_xml_files/beams.ly b/tests/test_xml_files/beams.ly new file mode 100644 index 00000000..9a55d5b5 --- /dev/null +++ b/tests/test_xml_files/beams.ly @@ -0,0 +1,23 @@ +\version "2.18.2" + +keyTime = { + \time 4/4 +} + +Soprano = \relative c'' { + \voiceOne + \keyTime + g16[ g8. g8] g g[ g] g4 +} + +\score +{ + << + \new Staff = "treble" \with { + } + << + \clef "treble" + \new Voice = "SopranoVoice" \Soprano + >> + >> +} \ No newline at end of file diff --git a/tests/test_xml_files/beams.musicxml b/tests/test_xml_files/beams.musicxml new file mode 100644 index 00000000..ff8dfca2 --- /dev/null +++ b/tests/test_xml_files/beams.musicxml @@ -0,0 +1,103 @@ + + + + + + python-ly 0.9.5 + 2019-07-11 + + + + + + + + + + + 4 + + + G + 2 + + + + + G + 4 + + 1 + 1 + 16th + begin + + + + G + 4 + + 3 + 1 + eighth + + continue + + + + G + 4 + + 2 + 1 + eighth + end + + + + G + 4 + + 2 + 1 + eighth + + + + G + 4 + + 2 + 1 + eighth + begin + + + + G + 4 + + 2 + 1 + eighth + end + + + + G + 4 + + 4 + 1 + quarter + + + 16 + + + + diff --git a/tests/test_xml_files/beams2.ly b/tests/test_xml_files/beams2.ly new file mode 100644 index 00000000..71628444 --- /dev/null +++ b/tests/test_xml_files/beams2.ly @@ -0,0 +1,40 @@ +\version "2.18.2" + +keyTime = { + \time 3/4 + \key c \major +} + +Soprano = \relative c'' { + \voiceOne + \keyTime + g8 \noBeam g g \noBeam g g g + g8[ g g] g [g] g + g16 g[ 8 g] g g + g8 g16 g g g g[ g g g g] g + \time 4/4 \set Timing.baseMoment = #(ly:make-moment 1 4) \set Timing.beatStructure = #'(1 3) + g8 g g g g g g g + g16 g g g g g g g g g g g g g g g + \set Timing.beamExceptions = #'() + g8 g g g g4 g8 g + g16 g g g g g s g g[ g] \noBeam g g r g g g \noBeam + g16 g \autoBeamOff g g g g[ g] g g g[ \autoBeamOn g] g g8 g + \set Timing.baseMoment = #(ly:make-moment 1 1) \set Timing.beatStructure = #'(1) + g8 \autoBeamOff g r g g g g g \autoBeamOn + \set Timing.baseMoment = #(ly:make-moment 1 4) \set Timing.beatStructure = #'(2 3) + g8 g g g g g g g + \set Timing.beatStructure = #'(2 1) + g8 g g g g g g g +} + +\score +{ + << + \new Staff = "treble" \with { + } + << + \clef "treble" + \new Voice = "SopranoVoice" \Soprano + >> + >> +} diff --git a/tests/test_xml_files/beams2.musicxml b/tests/test_xml_files/beams2.musicxml new file mode 100644 index 00000000..18f94373 --- /dev/null +++ b/tests/test_xml_files/beams2.musicxml @@ -0,0 +1,1227 @@ + + + + + + python-ly 0.9.5 + 2019-08-15 + + + + + + + + + + + 8 + + 0 + major + + + + G + 2 + + + + + G + 4 + + 4 + 1 + eighth + + + + G + 4 + + 4 + 1 + eighth + + + + G + 4 + + 4 + 1 + eighth + + + + G + 4 + + 4 + 1 + eighth + begin + + + + G + 4 + + 4 + 1 + eighth + continue + + + + G + 4 + + 4 + 1 + eighth + end + + + 24 + + + + + + G + 4 + + 4 + 1 + eighth + begin + + + + G + 4 + + 4 + 1 + eighth + continue + + + + G + 4 + + 4 + 1 + eighth + end + + + + G + 4 + + 4 + 1 + eighth + begin + + + + G + 4 + + 4 + 1 + eighth + end + + + + G + 4 + + 4 + 1 + eighth + + + + + + G + 4 + + 2 + 1 + 16th + + + + G + 4 + + 2 + 1 + 16th + begin + + + + G + 4 + + 4 + 1 + eighth + continue + + + + + E + 4 + + 4 + 1 + eighth + + + + + F + 4 + + 4 + 1 + eighth + + + + G + 4 + + 4 + 1 + eighth + end + + + + G + 4 + + 4 + 1 + eighth + begin + + + + G + 4 + + 4 + 1 + eighth + continue + + + + G + 4 + + 4 + 1 + eighth + end + + + + + E + 4 + + 4 + 1 + eighth + + + + + F + 4 + + 4 + 1 + eighth + + + + + + G + 4 + + 4 + 1 + eighth + begin + + + + G + 4 + + 2 + 1 + 16th + continue + + + + G + 4 + + 2 + 1 + 16th + end + + + + G + 4 + + 2 + 1 + 16th + begin + + + + G + 4 + + 2 + 1 + 16th + end + + + + G + 4 + + 2 + 1 + 16th + begin + + + + G + 4 + + 2 + 1 + 16th + continue + + + + G + 4 + + 2 + 1 + 16th + continue + + + + G + 4 + + 2 + 1 + 16th + continue + + + + G + 4 + + 2 + 1 + 16th + end + + + + G + 4 + + 2 + 1 + 16th + + + + + + + + + G + 4 + + 4 + 1 + eighth + begin + + + + G + 4 + + 4 + 1 + eighth + continue + + + + G + 4 + + 4 + 1 + eighth + continue + + + + G + 4 + + 4 + 1 + eighth + end + + + + G + 4 + + 4 + 1 + eighth + begin + + + + G + 4 + + 4 + 1 + eighth + continue + + + + G + 4 + + 4 + 1 + eighth + continue + + + + G + 4 + + 4 + 1 + eighth + end + + + 32 + + + + + + G + 4 + + 2 + 1 + 16th + begin + + + + G + 4 + + 2 + 1 + 16th + continue + + + + G + 4 + + 2 + 1 + 16th + continue + + + + G + 4 + + 2 + 1 + 16th + end + + + + G + 4 + + 2 + 1 + 16th + begin + + + + G + 4 + + 2 + 1 + 16th + continue + + + + G + 4 + + 2 + 1 + 16th + continue + + + + G + 4 + + 2 + 1 + 16th + end + + + + G + 4 + + 2 + 1 + 16th + begin + + + + G + 4 + + 2 + 1 + 16th + continue + + + + G + 4 + + 2 + 1 + 16th + continue + + + + G + 4 + + 2 + 1 + 16th + end + + + + G + 4 + + 2 + 1 + 16th + begin + + + + G + 4 + + 2 + 1 + 16th + continue + + + + G + 4 + + 2 + 1 + 16th + continue + + + + G + 4 + + 2 + 1 + 16th + end + + + + + + G + 4 + + 4 + 1 + eighth + begin + + + + G + 4 + + 4 + 1 + eighth + end + + + + G + 4 + + 4 + 1 + eighth + begin + + + + G + 4 + + 4 + 1 + eighth + end + + + + G + 4 + + 8 + 1 + quarter + + + + G + 4 + + 4 + 1 + eighth + begin + + + + G + 4 + + 4 + 1 + eighth + end + + + + + + G + 4 + + 2 + 1 + 16th + begin + + + + G + 4 + + 2 + 1 + 16th + continue + + + + G + 4 + + 2 + 1 + 16th + continue + + + + G + 4 + + 2 + 1 + 16th + end + + + + G + 4 + + 2 + 1 + 16th + begin + + + + G + 4 + + 2 + 1 + 16th + end + + + 2 + + + + G + 4 + + 2 + 1 + 16th + + + + G + 4 + + 2 + 1 + 16th + begin + + + + G + 4 + + 2 + 1 + 16th + end + + + + G + 4 + + 2 + 1 + 16th + begin + + + + G + 4 + + 2 + 1 + 16th + end + + + + 2 + 1 + 16th + + + + G + 4 + + 2 + 1 + 16th + begin + + + + G + 4 + + 2 + 1 + 16th + end + + + + G + 4 + + 2 + 1 + 16th + + + + + + G + 4 + + 2 + 1 + 16th + begin + + + + G + 4 + + 2 + 1 + 16th + continue + + + + G + 4 + + 2 + 1 + 16th + continue + + + + G + 4 + + 2 + 1 + 16th + end + + + + G + 4 + + 2 + 1 + 16th + + + + G + 4 + + 2 + 1 + 16th + begin + + + + G + 4 + + 2 + 1 + 16th + end + + + + G + 4 + + 2 + 1 + 16th + + + + G + 4 + + 2 + 1 + 16th + + + + G + 4 + + 2 + 1 + 16th + begin + + + + G + 4 + + 2 + 1 + 16th + end + + + + G + 4 + + 2 + 1 + 16th + begin + + + + G + 4 + + 4 + 1 + eighth + continue + + + + G + 4 + + 4 + 1 + eighth + end + + + + + + G + 4 + + 4 + 1 + eighth + begin + + + + G + 4 + + 4 + 1 + eighth + end + + + + 4 + 1 + eighth + + + + G + 4 + + 4 + 1 + eighth + + + + G + 4 + + 4 + 1 + eighth + + + + G + 4 + + 4 + 1 + eighth + + + + G + 4 + + 4 + 1 + eighth + + + + G + 4 + + 4 + 1 + eighth + + + + + + G + 4 + + 4 + 1 + eighth + begin + + + + G + 4 + + 4 + 1 + eighth + continue + + + + G + 4 + + 4 + 1 + eighth + continue + + + + G + 4 + + 4 + 1 + eighth + end + + + + G + 4 + + 4 + 1 + eighth + begin + + + + G + 4 + + 4 + 1 + eighth + continue + + + + G + 4 + + 4 + 1 + eighth + continue + + + + G + 4 + + 4 + 1 + eighth + end + + + + + + G + 4 + + 4 + 1 + eighth + begin + + + + G + 4 + + 4 + 1 + eighth + continue + + + + G + 4 + + 4 + 1 + eighth + continue + + + + G + 4 + + 4 + 1 + eighth + end + + + + G + 4 + + 4 + 1 + eighth + begin + + + + G + 4 + + 4 + 1 + eighth + end + + + + G + 4 + + 4 + 1 + eighth + begin + + + + G + 4 + + 4 + 1 + eighth + end + + + + diff --git a/tests/test_xml_files/breaks.ly b/tests/test_xml_files/breaks.ly new file mode 100644 index 00000000..ee114dfc --- /dev/null +++ b/tests/test_xml_files/breaks.ly @@ -0,0 +1,67 @@ +\version "2.18.2" + +\language "english" + +keyTime = { + \time 4/4 +} + +Soprano = \relative c'' { + \voiceOne + \keyTime + g4 g \break \bar "" g g + | g g g g \break \bar "||" + | g g g g + | g g g g + \bar "|." +} + +Alto = \relative c' { + \voiceTwo + \keyTime + e4 e e e + | e e e e + | e e e e \bar "||" \break + | e e e e +} + +Tenor = \relative c { + \voiceOne + \keyTime + c4 c c c + | c c \bar "" \break c c + | c c c c + | c c c c +} + +Bass = \relative c { + \voiceTwo + \keyTime + a4 a a a \break + | a a a a + | a a a a + | a a a a +} + +\score +{ + << + \new Staff = "treble" \with { + } + << + \clef "treble" + \new Voice = "SopranoVoice" \Soprano + \new Voice = "AltoVoice" \Alto + >> + + \new Staff = "bass" \with { + } + << + \clef "bass" + \new Voice = "TenorVoice" \Tenor + \new Voice = "BassVoice" \Bass + >> + + >> + +} diff --git a/tests/test_xml_files/breaks.musicxml b/tests/test_xml_files/breaks.musicxml new file mode 100644 index 00000000..43475eba --- /dev/null +++ b/tests/test_xml_files/breaks.musicxml @@ -0,0 +1,761 @@ + + + + + + python-ly 0.9.5 + 2019-11-21 + + + + + + + + + + + + + + 1 + + + G + 2 + + + + + G + 4 + + 1 + 1 + quarter + + + + G + 4 + + 1 + 1 + quarter + + + 2 + + + + E + 4 + + 1 + 2 + quarter + + + + E + 4 + + 1 + 2 + quarter + + + 2 + + + + none + + + + + + + + G + 4 + + 1 + 1 + quarter + + + + G + 4 + + 1 + 1 + quarter + + + 2 + + + + E + 4 + + 1 + 2 + quarter + + + + E + 4 + + 1 + 2 + quarter + + + + + + G + 4 + + 1 + 1 + quarter + + + + G + 4 + + 1 + 1 + quarter + + + 2 + + + + E + 4 + + 1 + 2 + quarter + + + + E + 4 + + 1 + 2 + quarter + + + 2 + + + + none + + + + + + G + 4 + + 1 + 1 + quarter + + + + G + 4 + + 1 + 1 + quarter + + + 2 + + + + E + 4 + + 1 + 2 + quarter + + + + E + 4 + + 1 + 2 + quarter + + + 2 + + + + light-light + + + + + + + + G + 4 + + 1 + 1 + quarter + + + + G + 4 + + 1 + 1 + quarter + + + + G + 4 + + 1 + 1 + quarter + + + + G + 4 + + 1 + 1 + quarter + + + 4 + + + + E + 4 + + 1 + 2 + quarter + + + + E + 4 + + 1 + 2 + quarter + + + + E + 4 + + 1 + 2 + quarter + + + + E + 4 + + 1 + 2 + quarter + + + 4 + + + + light-light + + + + + + + + G + 4 + + 1 + 1 + quarter + + + + G + 4 + + 1 + 1 + quarter + + + + G + 4 + + 1 + 1 + quarter + + + + G + 4 + + 1 + 1 + quarter + + + 4 + + + + E + 4 + + 1 + 2 + quarter + + + + E + 4 + + 1 + 2 + quarter + + + + E + 4 + + 1 + 2 + quarter + + + + E + 4 + + 1 + 2 + quarter + + + 4 + + + + light-heavy + + + + + + + 1 + + + F + 4 + + + + + C + 3 + + 1 + 1 + quarter + + + + C + 3 + + 1 + 1 + quarter + + + 2 + + + + A + 2 + + 1 + 2 + quarter + + + + A + 2 + + 1 + 2 + quarter + + + 2 + + + + none + + + + + + C + 3 + + 1 + 1 + quarter + + + + C + 3 + + 1 + 1 + quarter + + + 2 + + + + A + 2 + + 1 + 2 + quarter + + + + A + 2 + + 1 + 2 + quarter + + + + + + + + C + 3 + + 1 + 1 + quarter + + + + C + 3 + + 1 + 1 + quarter + + + 2 + + + + A + 2 + + 1 + 2 + quarter + + + + A + 2 + + 1 + 2 + quarter + + + 2 + + + + none + + + + + + + + C + 3 + + 1 + 1 + quarter + + + + C + 3 + + 1 + 1 + quarter + + + 2 + + + + A + 2 + + 1 + 2 + quarter + + + + A + 2 + + 1 + 2 + quarter + + + 2 + + + + light-light + + + + + + C + 3 + + 1 + 1 + quarter + + + + C + 3 + + 1 + 1 + quarter + + + + C + 3 + + 1 + 1 + quarter + + + + C + 3 + + 1 + 1 + quarter + + + 4 + + + + A + 2 + + 1 + 2 + quarter + + + + A + 2 + + 1 + 2 + quarter + + + + A + 2 + + 1 + 2 + quarter + + + + A + 2 + + 1 + 2 + quarter + + + 4 + + + + light-light + + + + + + C + 3 + + 1 + 1 + quarter + + + + C + 3 + + 1 + 1 + quarter + + + + C + 3 + + 1 + 1 + quarter + + + + C + 3 + + 1 + 1 + quarter + + + 4 + + + + A + 2 + + 1 + 2 + quarter + + + + A + 2 + + 1 + 2 + quarter + + + + A + 2 + + 1 + 2 + quarter + + + + A + 2 + + 1 + 2 + quarter + + + 4 + + + + light-heavy + + + + diff --git a/tests/test_xml_files/breath_marks.ly b/tests/test_xml_files/breath_marks.ly new file mode 100644 index 00000000..08d4a9c7 --- /dev/null +++ b/tests/test_xml_files/breath_marks.ly @@ -0,0 +1,41 @@ +\version "2.18.2" + +\language "english" + +keyTime = { + \time 4/4 +} + +Soprano = \relative c'' { + \voiceOne + \keyTime + c4 c \breathe c c \breathe + c c c c +} + +Alto = \relative c'' { + \voiceTwo + \keyTime + a1 + a2 \breathe a +} + +Tenor = \relative c { + \keyTime + \clef "bass" + a4 \breathe a \breathe a + a1 \breathe \bar "|." +} + +\score +{ + << + \new Staff = "treble" \with {} + << + \clef "treble" + \new Voice = "SopranoVoice" \Soprano + \new Voice = "AltoVoice" \Alto + >> + \new Staff \Tenor + >> +} \ No newline at end of file diff --git a/tests/test_xml_files/breath_marks.musicxml b/tests/test_xml_files/breath_marks.musicxml new file mode 100644 index 00000000..028d68d1 --- /dev/null +++ b/tests/test_xml_files/breath_marks.musicxml @@ -0,0 +1,263 @@ + + + + + + python-ly 0.9.5 + 2019-12-17 + + + + + + + + + + + + + + 1 + + + G + 2 + + + + + C + 5 + + 1 + 1 + quarter + + + + C + 5 + + 1 + 1 + quarter + + + + + + + + + C + 5 + + 1 + 1 + quarter + + + + C + 5 + + 1 + 1 + quarter + + + + + + + + 4 + + + + A + 4 + + 4 + 2 + whole + + + 4 + + + + + + C + 5 + + 1 + 1 + quarter + + + + C + 5 + + 1 + 1 + quarter + + + + C + 5 + + 1 + 1 + quarter + + + + C + 5 + + 1 + 1 + quarter + + + 4 + + + + A + 4 + + 2 + 2 + half + + + + + + + + + A + 4 + + 2 + 2 + half + + + 4 + + + + light-heavy + + + + + + + 1 + + + F + 4 + + + + + A + 2 + + 1 + 1 + quarter + + + + + + + + + A + 2 + + 1 + 1 + quarter + + + + A + 2 + + 1 + 1 + quarter + + + + + + + + + + C + 3 + + 1 + 1 + quarter + + + + A + 2 + + 1 + 1 + quarter + + + 4 + + + + + + A + 2 + + 4 + 1 + whole + + + + + + + + 4 + + + + light-heavy + + + + diff --git a/tests/test_xml_files/chord_copy.ly b/tests/test_xml_files/chord_copy.ly new file mode 100644 index 00000000..d9314300 --- /dev/null +++ b/tests/test_xml_files/chord_copy.ly @@ -0,0 +1,23 @@ +\version "2.18.2" + +\language "english" + +Soprano = \relative c'' { + \time 4/4 \key c \major + 2 q + 2 q4 q + \tuplet 3/2 { q4 q } q2 + 8 q q q q q q q + q8. q q16 q( q q[ q q]) q q q q +} + +\score +{ + << + \new Staff = "treble" \with {} + << + \clef "treble" + \new Voice = "SopranoVoice" \Soprano + >> + >> +} diff --git a/tests/test_xml_files/chord_copy.musicxml b/tests/test_xml_files/chord_copy.musicxml new file mode 100644 index 00000000..34376507 --- /dev/null +++ b/tests/test_xml_files/chord_copy.musicxml @@ -0,0 +1,704 @@ + + + + + + python-ly 0.9.5 + 2019-08-16 + + + + + + + + + + + 24 + + 0 + major + + + + G + 2 + + + + + A + 4 + + 48 + 1 + half + + + + + C + 5 + + 48 + 1 + half + + + + A + 4 + + 48 + 1 + half + + + + + C + 5 + + 48 + 1 + half + + + 96 + + + + + + A + 1 + 4 + + 48 + 1 + half + sharp + + + + + C + -1 + 5 + + 48 + 1 + half + flat + + + + + E + 5 + + 48 + 1 + half + + + + A + 1 + 4 + + 24 + 1 + quarter + + + + + C + -1 + 5 + + 24 + 1 + quarter + + + + + E + 5 + + 24 + 1 + quarter + + + + A + 1 + 4 + + 24 + 1 + quarter + + + + + C + -1 + 5 + + 24 + 1 + quarter + + + + + E + 5 + + 24 + 1 + quarter + + + + + + A + 1 + 4 + + 16 + 1 + quarter + sharp + + 3 + 2 + + + + + + + + + C + -1 + 5 + + 16 + 1 + quarter + flat + + + + + E + 5 + + 16 + 1 + quarter + + + + A + 4 + + 16 + 1 + quarter + natural + + 3 + 2 + + + + + + C + 5 + + 16 + 1 + quarter + natural + + 3 + 2 + + + + + A + 4 + + 16 + 1 + quarter + natural + + 3 + 2 + + + + + + + + + C + 5 + + 16 + 1 + quarter + natural + + + + A + 4 + + 48 + 1 + half + natural + + + + + C + 5 + + 48 + 1 + half + natural + + + + + + A + 4 + + 12 + 1 + eighth + begin + + + + + C + 5 + + 12 + 1 + eighth + + + + A + 4 + + 12 + 1 + eighth + continue + + + + + C + 5 + + 12 + 1 + eighth + + + + A + 4 + + 12 + 1 + eighth + continue + + + + + C + 5 + + 12 + 1 + eighth + + + + A + 4 + + 12 + 1 + eighth + end + + + + + C + 5 + + 12 + 1 + eighth + + + + A + 4 + + 12 + 1 + eighth + begin + + + + + C + 5 + + 12 + 1 + eighth + + + + A + 4 + + 12 + 1 + eighth + continue + + + + + C + 5 + + 12 + 1 + eighth + + + + A + 4 + + 12 + 1 + eighth + continue + + + + + C + 5 + + 12 + 1 + eighth + + + + A + 4 + + 12 + 1 + eighth + end + + + + + C + 5 + + 12 + 1 + eighth + + + + + + A + 4 + + 18 + 1 + eighth + + begin + + + + + C + 5 + + 18 + 1 + eighth + + + + A + 4 + + 18 + 1 + eighth + + continue + + + + + C + 5 + + 18 + 1 + eighth + + + + A + 4 + + 6 + 1 + 16th + continue + + + + + C + 5 + + 6 + 1 + 16th + + + + A + 4 + + 6 + 1 + 16th + end + + + + + + + + C + 5 + + 6 + 1 + 16th + + + + A + 4 + + 6 + 1 + 16th + + + + + C + 5 + + 6 + 1 + 16th + + + + A + 4 + + 6 + 1 + 16th + begin + + + + + C + 5 + + 6 + 1 + 16th + + + + A + 4 + + 6 + 1 + 16th + continue + + + + + C + 5 + + 6 + 1 + 16th + + + + A + 4 + + 6 + 1 + 16th + end + + + + + + + + C + 5 + + 6 + 1 + 16th + + + + A + 4 + + 6 + 1 + 16th + begin + + + + + C + 5 + + 6 + 1 + 16th + + + + A + 4 + + 6 + 1 + 16th + continue + + + + + C + 5 + + 6 + 1 + 16th + + + + A + 4 + + 6 + 1 + 16th + continue + + + + + C + 5 + + 6 + 1 + 16th + + + + A + 4 + + 6 + 1 + 16th + end + + + + + C + 5 + + 6 + 1 + 16th + + + + diff --git a/tests/test_xml_files/chord_symbols.ly b/tests/test_xml_files/chord_symbols.ly new file mode 100644 index 00000000..7c10de9e --- /dev/null +++ b/tests/test_xml_files/chord_symbols.ly @@ -0,0 +1,110 @@ +\version "2.18.2" + +\language "english" + +keyTime = { + \time 4/4 + \key c \major +} + +Soprano = \relative c'' { + \voiceOne + \keyTime + + c1 +| c4 c4 c4 c4 +| c1 +| c4 c4 c4 c4 +| c4 c4 c4 c4 +| c4 c4 c4 c4 +| c4 c4 c4 c4 +| c4 c4 c4 c4 + +} + +Alto = \relative c'' { + \voiceTwo + \keyTime + + a1 +| a1 +| a1 +| a1 +| a1 +| a1 +| a1 +| a1 + +} + +Tenor = \relative c { + \voiceOne + \keyTime + + c1 +| c1 +| c4 c4 c4 c4 +| c1 +| c1 +| c1 +| c1 +| c1 + +} + +Bass = \relative c { + \voiceTwo + \keyTime + + a1 +| a1 +| a1 +| a1 +| a1 +| a1 +| a1 +| a1 + +} + +Chords = \relative c' { + \chordmode { + +s1 +b4:aug7 s cs2 +b4 b4:sus2 s2 +df1/es +f:maj7/g +g:m6 +a:sus4 +b:maj11 + + } +} + +\score +{ + << + << + \new ChordNames {\Chords} + >> + + \new Staff = "treble" \with { + } + << + \clef "treble" + \new Voice = "SopranoVoice" \Soprano + \new Voice = "AltoVoice" \Alto + >> + + \new Staff = "bass" \with { + } + << + \clef "bass" + \new Voice = "TenorVoice" \Tenor + \new Voice = "BassVoice" \Bass + >> + + >> + +} \ No newline at end of file diff --git a/tests/test_xml_files/chord_symbols.musicxml b/tests/test_xml_files/chord_symbols.musicxml new file mode 100644 index 00000000..a70668bc --- /dev/null +++ b/tests/test_xml_files/chord_symbols.musicxml @@ -0,0 +1,680 @@ + + + + + + python-ly 0.9.5 + 2019-08-15 + + + + + + + + + + + + + + 1 + + 0 + major + + + + G + 2 + + + + + C + 5 + + 4 + 1 + whole + + + 4 + + + + A + 4 + + 4 + 2 + whole + + + 4 + + + + + + B + + augmented-seventh + + + + C + 5 + + 1 + 1 + quarter + + + + C + 5 + + 1 + 1 + quarter + + + + C + 1 + + major + + + + C + 5 + + 1 + 1 + quarter + + + + C + 5 + + 1 + 1 + quarter + + + 4 + + + + A + 4 + + 4 + 2 + whole + + + + + + B + + major + + + + B + + suspended-second + 1 + + + + C + 5 + + 4 + 1 + whole + + + 4 + + + + A + 4 + + 4 + 2 + whole + + + + + + D + -1 + + major + + E + 1 + + + + + C + 5 + + 1 + 1 + quarter + + + + C + 5 + + 1 + 1 + quarter + + + + C + 5 + + 1 + 1 + quarter + + + + C + 5 + + 1 + 1 + quarter + + + 4 + + + + A + 4 + + 4 + 2 + whole + + + + + + F + + major-seventh + + G + + + + + C + 5 + + 1 + 1 + quarter + + + + C + 5 + + 1 + 1 + quarter + + + + C + 5 + + 1 + 1 + quarter + + + + C + 5 + + 1 + 1 + quarter + + + 4 + + + + A + 4 + + 4 + 2 + whole + + + + + + G + + minor-sixth + + + + C + 5 + + 1 + 1 + quarter + + + + C + 5 + + 1 + 1 + quarter + + + + C + 5 + + 1 + 1 + quarter + + + + C + 5 + + 1 + 1 + quarter + + + 4 + + + + A + 4 + + 4 + 2 + whole + + + + + + A + + suspended-fourth + + + + C + 5 + + 1 + 1 + quarter + + + + C + 5 + + 1 + 1 + quarter + + + + C + 5 + + 1 + 1 + quarter + + + + C + 5 + + 1 + 1 + quarter + + + 4 + + + + A + 4 + + 4 + 2 + whole + + + + + + B + + major-11th + + + + C + 5 + + 1 + 1 + quarter + + + + C + 5 + + 1 + 1 + quarter + + + + C + 5 + + 1 + 1 + quarter + + + + C + 5 + + 1 + 1 + quarter + + + 4 + + + + A + 4 + + 4 + 2 + whole + + + + + + + 1 + + 0 + major + + + + F + 4 + + + + + C + 3 + + 4 + 1 + whole + + + 4 + + + + A + 2 + + 4 + 2 + whole + + + 4 + + + + + + C + 3 + + 4 + 1 + whole + + + 4 + + + + A + 2 + + 4 + 2 + whole + + + + + + C + 3 + + 1 + 1 + quarter + + + + C + 3 + + 1 + 1 + quarter + + + + C + 3 + + 1 + 1 + quarter + + + + C + 3 + + 1 + 1 + quarter + + + 4 + + + + A + 2 + + 4 + 2 + whole + + + + + + C + 3 + + 4 + 1 + whole + + + 4 + + + + A + 2 + + 4 + 2 + whole + + + + + + C + 3 + + 4 + 1 + whole + + + 4 + + + + A + 2 + + 4 + 2 + whole + + + + + + C + 3 + + 4 + 1 + whole + + + 4 + + + + A + 2 + + 4 + 2 + whole + + + + + + C + 3 + + 4 + 1 + whole + + + 4 + + + + A + 2 + + 4 + 2 + whole + + + + + + C + 3 + + 4 + 1 + whole + + + 4 + + + + A + 2 + + 4 + 2 + whole + + + + diff --git a/tests/test_xml_files/chord_ties.ly b/tests/test_xml_files/chord_ties.ly new file mode 100644 index 00000000..71c44f65 --- /dev/null +++ b/tests/test_xml_files/chord_ties.ly @@ -0,0 +1,29 @@ +\version "2.18.2" + +\language "english" + +keyTime = { + \time 4/4 +} + +Stanza = \lyricmode { + La di la. +} + +Soprano = \relative c'' { + \keyTime + 4~ + q~ q a, a \bar "|." +} + +\score +{ + << + \new Staff = "treble" \with {} + << + \clef "treble" + \new Voice = "Soprano" \Soprano + \lyricsto Soprano \new Lyrics \Stanza + >> + >> +} \ No newline at end of file diff --git a/tests/test_xml_files/chord_ties.musicxml b/tests/test_xml_files/chord_ties.musicxml new file mode 100644 index 00000000..688f5065 --- /dev/null +++ b/tests/test_xml_files/chord_ties.musicxml @@ -0,0 +1,315 @@ + + + + + + python-ly 0.9.5 + 2019-12-18 + + + + + + + + + + + 1 + + + G + 2 + + + + + E + 5 + + 1 + + 1 + quarter + + + + + single + La + + + + + + C + 5 + + 1 + + 1 + quarter + + + + + + + + A + 4 + + 1 + + 1 + quarter + + + + + + + E + 5 + + 1 + + + 1 + quarter + + + + + + + + + C + 5 + + 1 + + 1 + quarter + + + + + + + + A + 4 + + 1 + + 1 + quarter + + + + + + + E + 5 + + 1 + + 1 + quarter + + + + + + + + C + 5 + + 1 + + 1 + quarter + + + + + + + + A + 4 + + 1 + 1 + quarter + + + + E + 5 + + 1 + 1 + quarter + + + + + C + 5 + + 1 + + 1 + quarter + + + + + + + + A + 4 + + 1 + + 1 + quarter + + + + + + 4 + + + + + + E + 5 + + 1 + + 1 + quarter + + + + + + + + C + 5 + + 1 + + 1 + quarter + + + + + + + + A + 4 + + 1 + + + 1 + quarter + + + + + + + + E + 5 + + 1 + + 1 + quarter + + + + + + + + C + 5 + + 1 + + 1 + quarter + + + + + + + + A + 4 + + 1 + + 1 + quarter + + + + + + + A + 4 + + 1 + 1 + quarter + + single + di + + + + + A + 4 + + 1 + 1 + quarter + + single + la. + + + + 4 + + + + light-heavy + + + + diff --git a/tests/test_xml_files/dynamics.xml b/tests/test_xml_files/dynamics.musicxml similarity index 96% rename from tests/test_xml_files/dynamics.xml rename to tests/test_xml_files/dynamics.musicxml index c39f8fd6..ef2e040a 100644 --- a/tests/test_xml_files/dynamics.xml +++ b/tests/test_xml_files/dynamics.musicxml @@ -4,8 +4,8 @@ - python-ly 0.9.4 - 2016-03-28 + python-ly 0.9.5 + 2019-06-26 @@ -87,6 +87,8 @@ + + diff --git a/tests/test_xml_files/empty_part.ly b/tests/test_xml_files/empty_part.ly new file mode 100644 index 00000000..e8875ece --- /dev/null +++ b/tests/test_xml_files/empty_part.ly @@ -0,0 +1,35 @@ +\version "2.18.2" + +\language "english" + +Stanza = \lyricmode { + La di la + di la di + la di la. +} + +keyTime = { + \time 4/4 +} + +Soprano = \relative c'' { + \keyTime + c4 c c2 + c c4 c + c c c2 \bar "|." +} + +Alto = \relative c'' { } + +\score +{ + << + \new Staff = "treble" \with {} + << + \clef "treble" + \new Voice = "SopranoVoice" \Soprano + \new Voice = "AltoVoice" \Alto + \lyricsto SopranoVoice \new Lyrics \Stanza + >> + >> +} diff --git a/tests/test_xml_files/empty_part.musicxml b/tests/test_xml_files/empty_part.musicxml new file mode 100644 index 00000000..28fb52c9 --- /dev/null +++ b/tests/test_xml_files/empty_part.musicxml @@ -0,0 +1,162 @@ + + + + + + python-ly 0.9.5 + 2019-12-17 + + + + + + + + + + + 1 + + + G + 2 + + + + + C + 5 + + 1 + 1 + quarter + + single + La + + + + + C + 5 + + 1 + 1 + quarter + + single + di + + + + + C + 5 + + 2 + 1 + half + + single + la + + + + 4 + + + + + + C + 5 + + 2 + 1 + half + + single + di + + + + + C + 5 + + 1 + 1 + quarter + + single + la + + + + + C + 5 + + 1 + 1 + quarter + + single + di + + + + + + + C + 5 + + 1 + 1 + quarter + + single + la + + + + + C + 5 + + 1 + 1 + quarter + + single + di + + + + + C + 5 + + 2 + 1 + half + + single + la. + + + + 4 + + + + light-heavy + + + + diff --git a/tests/test_xml_files/glissando.xml b/tests/test_xml_files/glissando.musicxml similarity index 100% rename from tests/test_xml_files/glissando.xml rename to tests/test_xml_files/glissando.musicxml diff --git a/tests/test_xml_files/grace.ly b/tests/test_xml_files/grace.ly new file mode 100644 index 00000000..2eed04ce --- /dev/null +++ b/tests/test_xml_files/grace.ly @@ -0,0 +1,33 @@ +\version "2.18.2" + +\language "english" + +keyTime = { + \time 4/4 +} + +Soprano = \relative c'' { + \voiceOne + \keyTime + c4 \grace {g8 a} c4 \acciaccatura g8 c4 \bar "" \slashedGrace g8 c4 \bar "||" + c4 \acciaccatura {g8 a} c4 \appoggiatura g8 c4 c \bar "|." +} + +Alto = \relative c'' { + \voiceTwo + \keyTime + a4 a a a + a a a a +} + +\score +{ + << + \new Staff = "treble" \with {} + << + \clef "treble" + \new Voice = "SopranoVoice" \Soprano + \new Voice = "AltoVoice" \Alto + >> + >> +} \ No newline at end of file diff --git a/tests/test_xml_files/grace.musicxml b/tests/test_xml_files/grace.musicxml new file mode 100644 index 00000000..2fd383f4 --- /dev/null +++ b/tests/test_xml_files/grace.musicxml @@ -0,0 +1,297 @@ + + + + + + python-ly 0.9.5 + 2019-12-05 + + + + + + + + + + + 2 + + + G + 2 + + + + + C + 5 + + 2 + 1 + quarter + + + + + G + 4 + + 1 + eighth + begin + + + + + A + 4 + + 1 + eighth + end + + + + C + 5 + + 2 + 1 + quarter + + + + + G + 4 + + 1 + eighth + + + + + + + C + 5 + + 2 + 1 + quarter + + + + + + 6 + + + + A + 4 + + 2 + 2 + quarter + + + + A + 4 + + 2 + 2 + quarter + + + + A + 4 + + 2 + 2 + quarter + + + 6 + + + + none + + + + + + + G + 4 + + 1 + eighth + + + + C + 5 + + 2 + 1 + quarter + + + 2 + + + + A + 4 + + 2 + 2 + quarter + + + 2 + + + + light-light + + + + + + C + 5 + + 2 + 1 + quarter + + + + + G + 4 + + 1 + eighth + begin + + + + + + + + A + 4 + + 1 + eighth + end + + + + + + + C + 5 + + 2 + 1 + quarter + + + + + + + + G + 4 + + 1 + eighth + + + + + + + C + 5 + + 2 + 1 + quarter + + + + + + + C + 5 + + 2 + 1 + quarter + + + 8 + + + + A + 4 + + 2 + 2 + quarter + + + + A + 4 + + 2 + 2 + quarter + + + + A + 4 + + 2 + 2 + quarter + + + + A + 4 + + 2 + 2 + quarter + + + 8 + + + + light-heavy + + + + diff --git a/tests/test_xml_files/headers.ly b/tests/test_xml_files/headers.ly new file mode 100644 index 00000000..d77ae775 --- /dev/null +++ b/tests/test_xml_files/headers.ly @@ -0,0 +1,32 @@ +\version "2.18.2" + +\language "english" + +\header { + title = "Headers Test" + composer = "John Doe" +} + +\header { + poet = "Jane Doe" +} + +Soprano = \relative c'' { + \voiceOne + \time 4/4 \key c \major + c1 \bar "|." +} + +\score +{ + << + \new Staff = "treble" \with { + } + << + \clef "treble" + \new Voice = "SopranoVoice" \Soprano + >> + + >> + +} diff --git a/tests/test_xml_files/headers.musicxml b/tests/test_xml_files/headers.musicxml new file mode 100644 index 00000000..738b3f15 --- /dev/null +++ b/tests/test_xml_files/headers.musicxml @@ -0,0 +1,54 @@ + + + + Headers Test + + Jane Doe + John Doe + + python-ly 0.9.5 + 2019-09-24 + + + + + + + + + + + 1 + + 0 + major + + + + G + 2 + + + + + C + 5 + + 4 + 1 + whole + + + 4 + + + + light-heavy + + + + diff --git a/tests/test_xml_files/italics.ly b/tests/test_xml_files/italics.ly new file mode 100644 index 00000000..86ff09d3 --- /dev/null +++ b/tests/test_xml_files/italics.ly @@ -0,0 +1,35 @@ +\version "2.18.2" + +Stanza = \lyricmode { + Font style + \override LyricText #'font-shape = #'italic + is su -- per cool + \revert LyricText #'font-shape + but not here +} + +\language "english" + +keyTime = { + \time 4/4 + \key c \major +} + +Soprano = \relative c'' { + \keyTime + c4 c c c + c c c c + c1 \bar "|." +} + +\score +{ + << + \new Staff = "treble" \with {} + << + \clef "treble" + \new Voice = "SopranoVoice" \Soprano + \lyricsto SopranoVoice \new Lyrics \Stanza + >> + >> +} \ No newline at end of file diff --git a/tests/test_xml_files/italics.musicxml b/tests/test_xml_files/italics.musicxml new file mode 100644 index 00000000..d168f2b4 --- /dev/null +++ b/tests/test_xml_files/italics.musicxml @@ -0,0 +1,166 @@ + + + + + + python-ly 0.9.5 + 2019-12-20 + + + + + + + + + + + 1 + + 0 + major + + + + G + 2 + + + + + C + 5 + + 1 + 1 + quarter + + single + Font + + + + + C + 5 + + 1 + 1 + quarter + + single + style + + + + + C + 5 + + 1 + 1 + quarter + + single + is + + + + + C + 5 + + 1 + 1 + quarter + + begin + su + + + + 4 + + + + + + C + 5 + + 1 + 1 + quarter + + end + per + + + + + C + 5 + + 1 + 1 + quarter + + single + cool + + + + + C + 5 + + 1 + 1 + quarter + + single + but + + + + + C + 5 + + 1 + 1 + quarter + + single + not + + + + + + + C + 5 + + 4 + 1 + whole + + single + here + + + + 4 + + + + light-heavy + + + + diff --git a/tests/test_xml_files/last_measure.ly b/tests/test_xml_files/last_measure.ly new file mode 100644 index 00000000..cadf7b98 --- /dev/null +++ b/tests/test_xml_files/last_measure.ly @@ -0,0 +1,57 @@ +\version "2.18.2" + +keyTime = { + \time 4/4 + \partial 4 +} + +Soprano = \relative c'' { + \voiceOne + \keyTime + g4 + | g2. \bar "|." +} + +Alto = \relative c' { + \voiceTwo + \keyTime + g4 + | g2. +} + +Tenor = \relative c' { + \voiceOne + \keyTime + g4 + | g2. +} + +Bass = \relative c { + \voiceTwo + \keyTime + g4 + | g2. +} + +\score +{ + << + \new Staff = "treble" \with { + } + << + \clef "treble" + \new Voice = "SopranoVoice" \Soprano + \new Voice = "AltoVoice" \Alto + >> + + \new Staff = "bass" \with { + } + << + \clef "bass" + \new Voice = "TenorVoice" \Tenor + \new Voice = "BassVoice" \Bass + >> + + >> + +} \ No newline at end of file diff --git a/tests/test_xml_files/last_measure.musicxml b/tests/test_xml_files/last_measure.musicxml new file mode 100644 index 00000000..d1a64a81 --- /dev/null +++ b/tests/test_xml_files/last_measure.musicxml @@ -0,0 +1,161 @@ + + + + + + python-ly 0.9.5 + 2019-08-15 + + + + + + + + + + + + + + 1 + + + G + 2 + + + + + G + 4 + + 1 + 1 + quarter + + + 1 + + + + G + 3 + + 1 + 2 + quarter + + + 1 + + + + + + G + 4 + + 3 + 1 + half + + + + 3 + + + + G + 3 + + 3 + 2 + half + + + + 3 + + + + light-heavy + + + + + + + 1 + + + F + 4 + + + + + G + 3 + + 1 + 1 + quarter + + + 1 + + + + G + 2 + + 1 + 2 + quarter + + + 1 + + + + + + G + 3 + + 3 + 1 + half + + + + 3 + + + + G + 2 + + 3 + 2 + half + + + + 3 + + + + light-heavy + + + + diff --git a/tests/test_xml_files/lyric_switch.ly b/tests/test_xml_files/lyric_switch.ly new file mode 100644 index 00000000..c711ebbc --- /dev/null +++ b/tests/test_xml_files/lyric_switch.ly @@ -0,0 +1,55 @@ +\version "2.18.2" + +switchSop = { \set associatedVoice = "SopranoVoice" } +switchOne = { \set associatedVoice = "1" } +switchThree = { \set associatedVoice = "3" } + +StanzaOne = \lyricmode { + La di la di + \set associatedVoice = "AltoVoice" la di la di + la \switchSop di la \switchOne di + \switchThree laaa da da da \switchSop da. + Err +} + +\language "english" + +keyTime = { + \time 4/4 + \key c \major +} + +Soprano = \relative c'' { + \voiceOne + \keyTime + g4 g g g + g1 + g2 g4 g + << {b1 b b} \\ \\ {g2 g g g4 g g1} >> +} + +Alto = \relative c' { + \voiceTwo + \keyTime + e1 + e4 e e e + e e2. + e4 e e2 + e1 e +} + +\score +{ + << + \new Staff = "treble" \with { + } + << + \clef "treble" + \new Voice = "SopranoVoice" \Soprano + \new Voice = "AltoVoice" \Alto + \lyricsto SopranoVoice \new Lyrics \StanzaOne + >> + + >> + +} \ No newline at end of file diff --git a/tests/test_xml_files/lyric_switch.musicxml b/tests/test_xml_files/lyric_switch.musicxml new file mode 100644 index 00000000..29d59c15 --- /dev/null +++ b/tests/test_xml_files/lyric_switch.musicxml @@ -0,0 +1,405 @@ + + + + + + python-ly 0.9.5 + 2019-08-13 + + + + + + + + + + + 1 + + 0 + major + + + + G + 2 + + + + + G + 4 + + 1 + 1 + quarter + + single + La + + + + + G + 4 + + 1 + 1 + quarter + + single + di + + + + + G + 4 + + 1 + 1 + quarter + + single + la + + + + + G + 4 + + 1 + 1 + quarter + + single + di + + + + 4 + + + + E + 4 + + 4 + 2 + whole + + + 4 + + + + + + G + 4 + + 4 + 1 + whole + + single + la + + + + 4 + + + + E + 4 + + 1 + 2 + quarter + + + + E + 4 + + 1 + 2 + quarter + + single + di + + + + + E + 4 + + 1 + 2 + quarter + + single + la + + + + + E + 4 + + 1 + 2 + quarter + + single + di + + + + + + + G + 4 + + 2 + 1 + half + + + + G + 4 + + 1 + 1 + quarter + + single + la + + + + + G + 4 + + 1 + 1 + quarter + + single + di + + + + 4 + + + + E + 4 + + 1 + 2 + quarter + + single + la + + + + + E + 4 + + 3 + 2 + half + + + single + di + + + + + + + B + 4 + + 4 + 1 + whole + + single + laaa + + + + 4 + + + + G + 4 + + 2 + 3 + half + + + + G + 4 + + 2 + 3 + half + + single + da + + + + 4 + + + + E + 4 + + 1 + 2 + quarter + + + + E + 4 + + 1 + 2 + quarter + + + + E + 4 + + 2 + 2 + half + + + + + + B + 4 + + 4 + 1 + whole + + + 4 + + + + G + 4 + + 2 + 3 + half + + single + da + + + + + G + 4 + + 1 + 3 + quarter + + single + da + + + + + G + 4 + + 1 + 3 + quarter + + single + da. + + + + 4 + + + + E + 4 + + 4 + 2 + whole + + + + + + B + 4 + + 4 + 1 + whole + + + 4 + + + + G + 4 + + 4 + 3 + whole + + + 4 + + + + E + 4 + + 4 + 2 + whole + + + + diff --git a/tests/test_xml_files/lyric_ties.ly b/tests/test_xml_files/lyric_ties.ly new file mode 100644 index 00000000..dac65f2b --- /dev/null +++ b/tests/test_xml_files/lyric_ties.ly @@ -0,0 +1,27 @@ +\version "2.18.2" + +StanzaOne = \lyricmode { + La di la~ah di~aa~ah +} + +\language "english" + +Soprano = \relative c'' { + \voiceOne + g2 g + | g2 g +} + +\score +{ + << + \new Staff = "treble" \with { + } + << + \clef "treble" + \new Voice = "SopranoVoice" \Soprano + \lyricsto SopranoVoice \new Lyrics \StanzaOne + >> + >> + +} \ No newline at end of file diff --git a/tests/test_xml_files/lyric_ties.musicxml b/tests/test_xml_files/lyric_ties.musicxml new file mode 100644 index 00000000..d077b160 --- /dev/null +++ b/tests/test_xml_files/lyric_ties.musicxml @@ -0,0 +1,85 @@ + + + + + + python-ly 0.9.5 + 2019-06-11 + + + + + + + + + + + 1 + + + G + 2 + + + + + G + 4 + + 2 + 1 + half + + single + La + + + + + G + 4 + + 2 + 1 + half + + single + di + + + + + + + G + 4 + + 2 + 1 + half + + single + la‿ah + + + + + G + 4 + + 2 + 1 + half + + single + di‿aa‿ah + + + + + diff --git a/tests/test_xml_files/lyrics.ly b/tests/test_xml_files/lyrics.ly new file mode 100644 index 00000000..f25da3a0 --- /dev/null +++ b/tests/test_xml_files/lyrics.ly @@ -0,0 +1,91 @@ +\version "2.18.2" + +slurOff = { \set ignoreMelismata = ##t } +slurOn = { \unset ignoreMelismata } + +StanzaOne = \lyricmode { + La _ di __ \slurOff la di \slurOn da + Ba dum dun + Li -- del -- li zap +} + +StanzaTwo = \lyricmode { + \slurOn This is a __ song for + cool peo -- ple + \slurOff Just a test for us \slurOn +} + +StanzaThree = \lyricmode { + \slurOn This is a \slurOff tune for \slurOn + some peo -- ple + Just a test for us +} + +\language "english" + +keyTime = { + \time 4/4 + \key c \major +} + +Soprano = \relative c'' { + \voiceOne + \keyTime + b4 c( d e) + | g( a) b( c) + | g2 a + | f8 g c a( c) b g f +} + +Alto = \relative c' { + \voiceTwo + \keyTime + g4 g e e + | f d e e + | g2 a + | f8 g c a( c) b g f +} + +Tenor = \relative c' { + \voiceOne + \keyTime + a4 b a b + | b a b a + | g2 a + | f8 g c a( c) b g f +} + +Bass = \relative c { + \voiceTwo + \keyTime + c4 g a8 g g g + | d4 b d b + | g2 a + | f8 g c a( c) b g f +} + +\score +{ + << + \new Staff = "treble" \with { + } + << + \clef "treble" + \new Voice = "SopranoVoice" \Soprano + \new Voice = "AltoVoice" \Alto + \lyricsto SopranoVoice \new Lyrics \StanzaOne + \lyricsto SopranoVoice \new Lyrics \StanzaTwo + \lyricsto SopranoVoice \new Lyrics \StanzaThree + >> + + \new Staff = "bass" \with { + } + << + \clef "bass" + \new Voice = "TenorVoice" \Tenor + \new Voice = "BassVoice" \Bass + >> + + >> + +} \ No newline at end of file diff --git a/tests/test_xml_files/lyrics.musicxml b/tests/test_xml_files/lyrics.musicxml new file mode 100644 index 00000000..347d2e02 --- /dev/null +++ b/tests/test_xml_files/lyrics.musicxml @@ -0,0 +1,997 @@ + + + + + + python-ly 0.9.5 + 2019-08-15 + + + + + + + + + + + + + + 2 + + 0 + major + + + + G + 2 + + + + + B + 4 + + 2 + 1 + quarter + + single + La + + + single + This + + + single + This + + + + + C + 5 + + 2 + 1 + quarter + + + + + single + is + + + single + is + + + + + D + 5 + + 2 + 1 + quarter + + + + E + 5 + + 2 + 1 + quarter + + + + + + 8 + + + + G + 3 + + 2 + 2 + quarter + + + + G + 3 + + 2 + 2 + quarter + + + + E + 3 + + 2 + 2 + quarter + + + + E + 3 + + 2 + 2 + quarter + + + 8 + + + + + + G + 5 + + 2 + 1 + quarter + + + + + single + di + + + + single + a + + + + single + a + + + + + A + 5 + + 2 + 1 + quarter + + + + + + + B + 5 + + 2 + 1 + quarter + + + + + single + la + + + single + song + + + single + tune + + + + + C + 6 + + 2 + 1 + quarter + + + + + single + di + + + single + for + + + + 8 + + + + F + 3 + + 2 + 2 + quarter + + + + D + 3 + + 2 + 2 + quarter + + + + E + 3 + + 2 + 2 + quarter + + + + E + 3 + + 2 + 2 + quarter + + + + + + G + 5 + + 4 + 1 + half + + single + da + + + single + for + + + single + some + + + + + A + 5 + + 4 + 1 + half + + single + Ba + + + single + cool + + + begin + peo + + + + 8 + + + + G + 3 + + 4 + 2 + half + + + + A + 3 + + 4 + 2 + half + + + + + + F + 5 + + 1 + 1 + eighth + begin + + single + dum + + + begin + peo + + + end + ple + + + + + G + 5 + + 1 + 1 + eighth + continue + + single + dun + + + end + ple + + + single + Just + + + + + C + 6 + + 1 + 1 + eighth + continue + + begin + Li + + + single + Just + + + single + a + + + + + A + 5 + + 1 + 1 + eighth + end + + + + + middle + del + + + single + a + + + single + test + + + + + C + 6 + + 1 + 1 + eighth + begin + + + + + single + test + + + + + B + 5 + + 1 + 1 + eighth + continue + + end + li + + + single + for + + + single + for + + + + + G + 5 + + 1 + 1 + eighth + continue + + single + zap + + + single + us + + + single + us + + + + + F + 5 + + 1 + 1 + eighth + end + + + 8 + + + + F + 3 + + 1 + 2 + eighth + begin + + + + G + 3 + + 1 + 2 + eighth + continue + + + + C + 4 + + 1 + 2 + eighth + continue + + + + A + 3 + + 1 + 2 + eighth + end + + + + + + + C + 4 + + 1 + 2 + eighth + begin + + + + + + + B + 3 + + 1 + 2 + eighth + continue + + + + G + 3 + + 1 + 2 + eighth + continue + + + + F + 3 + + 1 + 2 + eighth + end + + + + + + + 2 + + 0 + major + + + + F + 4 + + + + + A + 3 + + 2 + 1 + quarter + + + + B + 3 + + 2 + 1 + quarter + + + + A + 3 + + 2 + 1 + quarter + + + + B + 3 + + 2 + 1 + quarter + + + 8 + + + + C + 3 + + 2 + 2 + quarter + + + + G + 2 + + 2 + 2 + quarter + + + + A + 2 + + 1 + 2 + eighth + begin + + + + G + 2 + + 1 + 2 + eighth + continue + + + + G + 2 + + 1 + 2 + eighth + continue + + + + G + 2 + + 1 + 2 + eighth + end + + + 8 + + + + + + B + 3 + + 2 + 1 + quarter + + + + A + 3 + + 2 + 1 + quarter + + + + B + 3 + + 2 + 1 + quarter + + + + A + 3 + + 2 + 1 + quarter + + + 8 + + + + D + 2 + + 2 + 2 + quarter + + + + B + 1 + + 2 + 2 + quarter + + + + D + 2 + + 2 + 2 + quarter + + + + B + 1 + + 2 + 2 + quarter + + + + + + G + 3 + + 4 + 1 + half + + + + A + 3 + + 4 + 1 + half + + + 8 + + + + G + 1 + + 4 + 2 + half + + + + A + 1 + + 4 + 2 + half + + + + + + F + 3 + + 1 + 1 + eighth + begin + + + + G + 3 + + 1 + 1 + eighth + continue + + + + C + 4 + + 1 + 1 + eighth + continue + + + + A + 3 + + 1 + 1 + eighth + end + + + + + + + C + 4 + + 1 + 1 + eighth + begin + + + + + + + B + 3 + + 1 + 1 + eighth + continue + + + + G + 3 + + 1 + 1 + eighth + continue + + + + F + 3 + + 1 + 1 + eighth + end + + + 8 + + + + F + 1 + + 1 + 2 + eighth + begin + + + + G + 1 + + 1 + 2 + eighth + continue + + + + C + 2 + + 1 + 2 + eighth + continue + + + + A + 1 + + 1 + 2 + eighth + end + + + + + + + C + 2 + + 1 + 2 + eighth + begin + + + + + + + B + 1 + + 1 + 2 + eighth + continue + + + + G + 1 + + 1 + 2 + eighth + continue + + + + F + 1 + + 1 + 2 + eighth + end + + + + diff --git a/tests/test_xml_files/lyrics2.ly b/tests/test_xml_files/lyrics2.ly new file mode 100644 index 00000000..825e133a --- /dev/null +++ b/tests/test_xml_files/lyrics2.ly @@ -0,0 +1,34 @@ +\version "2.18.2" + +StanzaOne = \lyricmode { + La "di lo li" da doo loo + "ta da" this \markup \tiny {\musicglyph #"rests.2"} is rest +} + +\language "english" + +keyTime = { + \time 4/4 + \key c \major +} + +Soprano = \relative c' { + \keyTime + b4 c( d e) + | g( a) b( c) + | g2 a + | f4 a c b +} + +\score +{ + << + \new Staff = "treble" \with {} + << + \clef "treble" + \new Voice = "SopranoVoice" \Soprano + \lyricsto SopranoVoice \new Lyrics \StanzaOne + >> + >> + +} \ No newline at end of file diff --git a/tests/test_xml_files/lyrics2.musicxml b/tests/test_xml_files/lyrics2.musicxml new file mode 100644 index 00000000..62961da1 --- /dev/null +++ b/tests/test_xml_files/lyrics2.musicxml @@ -0,0 +1,229 @@ + + + + + + python-ly 0.9.5 + 2019-12-20 + + + + + + + + + + + 1 + + 0 + major + + + + G + 2 + + + + + B + 3 + + 1 + 1 + quarter + + single + La + + + + + C + 4 + + 1 + 1 + quarter + + + + + single + di lo li + + + + + D + 4 + + 1 + 1 + quarter + + + + E + 4 + + 1 + 1 + quarter + + + + + + 4 + + + + + + G + 4 + + 1 + 1 + quarter + + + + + single + da + + + + + A + 4 + + 1 + 1 + quarter + + + + + + + B + 4 + + 1 + 1 + quarter + + + + + single + doo + + + + + C + 5 + + 1 + 1 + quarter + + + + + + + + + G + 4 + + 2 + 1 + half + + single + loo + + + + + A + 4 + + 2 + 1 + half + + single + ta da + + + + + + + F + 4 + + 1 + 1 + quarter + + single + this + + + + + A + 4 + + 1 + 1 + quarter + + + single + ERROR + + + + + C + 5 + + 1 + 1 + quarter + + single + is + + + + + B + 4 + + 1 + 1 + quarter + + single + rest + + + + + diff --git a/tests/test_xml_files/measure_length.ly b/tests/test_xml_files/measure_length.ly new file mode 100644 index 00000000..68adafec --- /dev/null +++ b/tests/test_xml_files/measure_length.ly @@ -0,0 +1,84 @@ +\version "2.18.2" + +\language "english" + +keyTime = { + \time 4/4 +} + +Soprano = \relative c'' { + \voiceOne + \keyTime + c2 c + c c c + c c c + \set Timing.measureLength = #(ly:make-moment 2 4) + c + \set Timing.measureLength = #(ly:make-moment 3 8) + c4. + c + \set Timing.measureLength = #(ly:make-moment 2 4) + a8 a a a + \time 2/4 + a a a a + \bar "|." +} + +Alto = \relative c'' { + \voiceTwo + \keyTime + a2 a + a a a + a a a + a + a4. + a + s2 + s +} + +Tenor = \relative c { + \voiceOne + \keyTime + e2 e + e e e + e e e + e + e4. + e + c8 c c c + c c c c +} + +Bass = \relative c { + \voiceTwo + \keyTime + g2 g + \set Timing.measureLength = #(ly:make-moment 6 4) + g g g + g g g + g + \set Timing.measureLength = #(ly:make-moment 3 8) + g4. + g + s2 + s +} + +\score +{ + << + \new Staff = "treble" \with {} + << + \clef "treble" + \new Voice = "Soprano" \Soprano + \new Voice = "Alto" \Alto + >> + \new Staff = "bass" \with {} + << + \clef "bass" + \new Voice = "Tenor" \Tenor + \new Voice = "Bass" \Bass + >> + >> +} \ No newline at end of file diff --git a/tests/test_xml_files/measure_length.musicxml b/tests/test_xml_files/measure_length.musicxml new file mode 100644 index 00000000..bd9737b9 --- /dev/null +++ b/tests/test_xml_files/measure_length.musicxml @@ -0,0 +1,721 @@ + + + + + + python-ly 0.9.5 + 2019-12-18 + + + + + + + + + + + + + + 2 + + + G + 2 + + + + + C + 5 + + 4 + 1 + half + + + + C + 5 + + 4 + 1 + half + + + 8 + + + + A + 4 + + 4 + 2 + half + + + + A + 4 + + 4 + 2 + half + + + 8 + + + + + + C + 5 + + 4 + 1 + half + + + + C + 5 + + 4 + 1 + half + + + + C + 5 + + 4 + 1 + half + + + 12 + + + + A + 4 + + 4 + 2 + half + + + + A + 4 + + 4 + 2 + half + + + + A + 4 + + 4 + 2 + half + + + + + + C + 5 + + 4 + 1 + half + + + + C + 5 + + 4 + 1 + half + + + + C + 5 + + 4 + 1 + half + + + 12 + + + + A + 4 + + 4 + 2 + half + + + + A + 4 + + 4 + 2 + half + + + + A + 4 + + 4 + 2 + half + + + + + + C + 5 + + 4 + 1 + half + + + 4 + + + + A + 4 + + 4 + 2 + half + + + + + + C + 5 + + 3 + 1 + quarter + + + + 3 + + + + A + 4 + + 3 + 2 + quarter + + + + + + + C + 5 + + 3 + 1 + quarter + + + + 3 + + + + A + 4 + + 3 + 2 + quarter + + + + + + + A + 4 + + 1 + 1 + eighth + begin + + + + A + 4 + + 1 + 1 + eighth + continue + + + + A + 4 + + 1 + 1 + eighth + continue + + + + A + 4 + + 1 + 1 + eighth + end + + + + + + + + + A + 4 + + 1 + 1 + eighth + begin + + + + A + 4 + + 1 + 1 + eighth + end + + + + A + 4 + + 1 + 1 + eighth + begin + + + + A + 4 + + 1 + 1 + eighth + end + + + 4 + + + 4 + + + 4 + + + + light-heavy + + + + + + + 2 + + + F + 4 + + + + + E + 3 + + 4 + 1 + half + + + + E + 3 + + 4 + 1 + half + + + 8 + + + + G + 2 + + 4 + 2 + half + + + + G + 2 + + 4 + 2 + half + + + 8 + + + + + + E + 3 + + 4 + 1 + half + + + + E + 3 + + 4 + 1 + half + + + + E + 3 + + 4 + 1 + half + + + 12 + + + + G + 2 + + 4 + 2 + half + + + + G + 2 + + 4 + 2 + half + + + + G + 2 + + 4 + 2 + half + + + + + + E + 3 + + 4 + 1 + half + + + + E + 3 + + 4 + 1 + half + + + + E + 3 + + 4 + 1 + half + + + 12 + + + + G + 2 + + 4 + 2 + half + + + + G + 2 + + 4 + 2 + half + + + + G + 2 + + 4 + 2 + half + + + + + + E + 3 + + 4 + 1 + half + + + 4 + + + + G + 2 + + 4 + 2 + half + + + + + + E + 3 + + 3 + 1 + quarter + + + + 3 + + + + G + 2 + + 3 + 2 + quarter + + + + + + + E + 3 + + 3 + 1 + quarter + + + + 3 + + + + G + 2 + + 3 + 2 + quarter + + + + + + + C + 3 + + 1 + 1 + eighth + begin + + + + C + 3 + + 1 + 1 + eighth + continue + + + + C + 3 + + 1 + 1 + eighth + continue + + + + C + 3 + + 1 + 1 + eighth + end + + + + + + + + + C + 3 + + 1 + 1 + eighth + begin + + + + C + 3 + + 1 + 1 + eighth + end + + + + C + 3 + + 1 + 1 + eighth + begin + + + + C + 3 + + 1 + 1 + eighth + end + + + 4 + + + 4 + + + 4 + + + + light-heavy + + + + diff --git a/tests/test_xml_files/merge_voice.xml b/tests/test_xml_files/merge_voice.musicxml similarity index 91% rename from tests/test_xml_files/merge_voice.xml rename to tests/test_xml_files/merge_voice.musicxml index 4a638647..291caf59 100644 --- a/tests/test_xml_files/merge_voice.xml +++ b/tests/test_xml_files/merge_voice.musicxml @@ -5,8 +5,8 @@ Somebody to love - python-ly 0.9.4 - 2016-03-28 + python-ly 0.9.5 + 2019-06-13 @@ -51,7 +51,6 @@ 1 1 quarter - flat @@ -62,7 +61,6 @@ 1 1 quarter - flat @@ -113,7 +111,6 @@ 1 1 quarter - flat @@ -133,7 +130,6 @@ 1 1 quarter - flat @@ -144,14 +140,10 @@ 1 1 quarter - flat 4 - - 4 - diff --git a/tests/test_xml_files/piano_staff.ly b/tests/test_xml_files/piano_staff.ly new file mode 100644 index 00000000..4dfb6396 --- /dev/null +++ b/tests/test_xml_files/piano_staff.ly @@ -0,0 +1,33 @@ +\version "2.18.2" + +\language "english" + +keyTime = { + \time 4/4 +} + +Soprano = \relative c'' { + \voiceOne + \keyTime + r4 r g + | g g g g + \bar "|." +} + +Alto = \relative c' { + \voiceTwo + \keyTime + r1 + | e4 e e e +} + +\score +{ + << + \new PianoStaff + << + \new Staff = "SopranoVoice" \Soprano + \new Staff = "AltoVoice" \Alto + >> + >> +} \ No newline at end of file diff --git a/tests/test_xml_files/piano_staff.musicxml b/tests/test_xml_files/piano_staff.musicxml new file mode 100644 index 00000000..ca6f4b98 --- /dev/null +++ b/tests/test_xml_files/piano_staff.musicxml @@ -0,0 +1,182 @@ + + + + + + python-ly 0.9.5 + 2019-07-08 + + + + + + + + + + + 1 + + 2 + + G + 2 + + + + + 1 + 1 + quarter + 1 + + + + 1 + 1 + quarter + 1 + + + + G + 4 + + 1 + 1 + quarter + 1 + + + + + E + 4 + + 1 + 1 + quarter + 1 + + + + G + 4 + + 1 + 1 + quarter + 1 + + + 4 + + + + 4 + 6 + whole + 2 + + + 4 + + + + + + G + 4 + + 1 + 1 + quarter + 1 + + + + G + 4 + + 1 + 1 + quarter + 1 + + + + G + 4 + + 1 + 1 + quarter + 1 + + + + G + 4 + + 1 + 1 + quarter + 1 + + + 4 + + + + E + 4 + + 1 + 6 + quarter + 2 + + + + E + 4 + + 1 + 6 + quarter + 2 + + + + E + 4 + + 1 + 6 + quarter + 2 + + + + E + 4 + + 1 + 6 + quarter + 2 + + + 4 + + + + light-heavy + + + + diff --git a/tests/test_xml_files/piano_staff2.ly b/tests/test_xml_files/piano_staff2.ly new file mode 100644 index 00000000..c3b43677 --- /dev/null +++ b/tests/test_xml_files/piano_staff2.ly @@ -0,0 +1,24 @@ +\version "2.18.2" + +Soprano = \relative c'' { + \voiceOne + \time 4/4 + g1 \bar "|." +} + +Alto = \relative c' { + \voiceTwo + \time 4/4 + e1 +} + +\score +{ + << + \new PianoStaff + << + \new Voice \Soprano + \new Voice \Alto + >> + >> +} \ No newline at end of file diff --git a/tests/test_xml_files/piano_staff2.musicxml b/tests/test_xml_files/piano_staff2.musicxml new file mode 100644 index 00000000..574834d6 --- /dev/null +++ b/tests/test_xml_files/piano_staff2.musicxml @@ -0,0 +1,62 @@ + + + + + + python-ly 0.9.5 + 2019-09-05 + + + + + + + + + + + 1 + + 2 + + G + 2 + + + + + G + 4 + + 4 + 1 + whole + 1 + + + 4 + + + + E + 4 + + 4 + 6 + whole + 2 + + + 4 + + + + light-heavy + + + + diff --git a/tests/test_xml_files/piano_staff3.ly b/tests/test_xml_files/piano_staff3.ly new file mode 100644 index 00000000..8781d036 --- /dev/null +++ b/tests/test_xml_files/piano_staff3.ly @@ -0,0 +1,48 @@ +\version "2.18.2" + +\language "english" + +keyTime = { + \time 4/4 +} + +Stanza = \lyricmode { + La di la da! +} + +Melody = \relative c'' { + \voiceOne + \keyTime + g2 g + g g +} + +Right = \relative c'' { + \keyTime + 2 q + q q +} + +Left = \relative c { + \keyTime + \clef bass + 2 q + q q + \bar "|." +} + +\score +{ + << + \new Staff + << + \new Voice = "M" \Melody + \lyricsto M \new Lyrics \Stanza + >> + \new PianoStaff + << + \new Staff = "Right" \Right + \new Staff = "Left" \Left + >> + >> +} \ No newline at end of file diff --git a/tests/test_xml_files/piano_staff3.musicxml b/tests/test_xml_files/piano_staff3.musicxml new file mode 100644 index 00000000..f5860a90 --- /dev/null +++ b/tests/test_xml_files/piano_staff3.musicxml @@ -0,0 +1,300 @@ + + + + + + python-ly 0.9.5 + 2019-11-18 + + + + + + + + + + + + + + 1 + + + G + 2 + + + + + G + 4 + + 2 + 1 + half + + single + La + + + + + G + 4 + + 2 + 1 + half + + single + di + + + + 4 + + + + + + G + 4 + + 2 + 1 + half + + single + la + + + + + G + 4 + + 2 + 1 + half + + single + da! + + + + 4 + + + + light-heavy + + + + + + + 1 + + 2 + + F + 4 + + + + + B + 4 + + 2 + 1 + half + 1 + + + + + G + 4 + + 2 + 1 + half + 1 + + + + B + 4 + + 2 + 1 + half + 1 + + + + + G + 4 + + 2 + 1 + half + 1 + + + 4 + + + + A + 2 + + 2 + 1 + half + 2 + + + + + C + 3 + + 2 + 1 + half + 2 + + + + A + 2 + + 2 + 1 + half + 2 + + + + + C + 3 + + 2 + 1 + half + 2 + + + 4 + + + + + + B + 4 + + 2 + 1 + half + 1 + + + + + G + 4 + + 2 + 1 + half + 1 + + + + B + 4 + + 2 + 1 + half + 1 + + + + + G + 4 + + 2 + 1 + half + 1 + + + 4 + + + + A + 2 + + 2 + 1 + half + 2 + + + + + C + 3 + + 2 + 1 + half + 2 + + + + A + 2 + + 2 + 1 + half + 2 + + + + + C + 3 + + 2 + 1 + half + 2 + + + 4 + + + + light-heavy + + + + diff --git a/tests/test_xml_files/pickup.ly b/tests/test_xml_files/pickup.ly new file mode 100644 index 00000000..8cd5d712 --- /dev/null +++ b/tests/test_xml_files/pickup.ly @@ -0,0 +1,57 @@ +\version "2.18.2" + +keyTime = { + \time 4/4 + \partial 4 +} + +Soprano = \relative c'' { + \voiceOne + \keyTime + g4 + | g g g g +} + +Alto = \relative c' { + \voiceTwo + \keyTime + g4 + | g g g g +} + +Tenor = \relative c' { + \voiceOne + \keyTime + g4 + | g g g g +} + +Bass = \relative c { + \voiceTwo + \keyTime + g4 + | g g g g +} + +\score +{ + << + \new Staff = "treble" \with { + } + << + \clef "treble" + \new Voice = "SopranoVoice" \Soprano + \new Voice = "AltoVoice" \Alto + >> + + \new Staff = "bass" \with { + } + << + \clef "bass" + \new Voice = "TenorVoice" \Tenor + \new Voice = "BassVoice" \Bass + >> + + >> + +} \ No newline at end of file diff --git a/tests/test_xml_files/pickup.musicxml b/tests/test_xml_files/pickup.musicxml new file mode 100644 index 00000000..74b5ce9c --- /dev/null +++ b/tests/test_xml_files/pickup.musicxml @@ -0,0 +1,251 @@ + + + + + + python-ly 0.9.5 + 2019-08-15 + + + + + + + + + + + + + + 1 + + + G + 2 + + + + + G + 4 + + 1 + 1 + quarter + + + 1 + + + + G + 3 + + 1 + 2 + quarter + + + 1 + + + + + + G + 4 + + 1 + 1 + quarter + + + + G + 4 + + 1 + 1 + quarter + + + + G + 4 + + 1 + 1 + quarter + + + + G + 4 + + 1 + 1 + quarter + + + 4 + + + + G + 3 + + 1 + 2 + quarter + + + + G + 3 + + 1 + 2 + quarter + + + + G + 3 + + 1 + 2 + quarter + + + + G + 3 + + 1 + 2 + quarter + + + + + + + 1 + + + F + 4 + + + + + G + 3 + + 1 + 1 + quarter + + + 1 + + + + G + 2 + + 1 + 2 + quarter + + + 1 + + + + + + G + 3 + + 1 + 1 + quarter + + + + G + 3 + + 1 + 1 + quarter + + + + G + 3 + + 1 + 1 + quarter + + + + G + 3 + + 1 + 1 + quarter + + + 4 + + + + G + 2 + + 1 + 2 + quarter + + + + G + 2 + + 1 + 2 + quarter + + + + G + 2 + + 1 + 2 + quarter + + + + G + 2 + + 1 + 2 + quarter + + + + diff --git a/tests/test_xml_files/signatures.ly b/tests/test_xml_files/signatures.ly new file mode 100644 index 00000000..0199d2c3 --- /dev/null +++ b/tests/test_xml_files/signatures.ly @@ -0,0 +1,43 @@ +\version "2.18.2" + +\language "english" + +Soprano = \relative c'' { + \defaultTimeSignature + \time 4/4 \key a \major \numericTimeSignature a1 + \defaultTimeSignature \key ef \minor a1 + \time 2/2 \key gs \dorian a1 \numericTimeSignature + \time 4/4 a1 + \time 2/2 \defaultTimeSignature a1 +} + +Alto = \relative c'' { + \defaultTimeSignature \key a \major + c1 \key ef \minor + c1 \key gs \dorian + c1 + \time 4/4 c1 + c1 +} + +\score +{ + << + \new Staff = "treble" \with {} + << + \clef "treble" + \new Voice = "SopranoVoice1" \Soprano + >> + \new Staff = "treble2" \with {} + << + \clef "treble" + \new Voice = "SopranoVoice2" \Soprano + \new Voice = "AltoVoice1" \Alto + >> + \new Staff = "treble3" \with {} + << + \clef "treble" + \new Voice = "AltoVoice2" \Alto + >> + >> +} diff --git a/tests/test_xml_files/signatures.musicxml b/tests/test_xml_files/signatures.musicxml new file mode 100644 index 00000000..d25cd5b8 --- /dev/null +++ b/tests/test_xml_files/signatures.musicxml @@ -0,0 +1,445 @@ + + + + + + python-ly 0.9.5 + 2019-08-27 + + + + + + + + + + + + + + + + + 1 + + 3 + major + + + + G + 2 + + + + + A + 4 + + 4 + 1 + whole + + + 4 + + + + + + -6 + minor + + + + + A + 4 + + 4 + 1 + whole + natural + + + 4 + + + + + + 6 + dorian + + + + + + A + 4 + + 4 + 1 + whole + natural + + + 4 + + + + + + + + + A + 4 + + 4 + 1 + whole + natural + + + 4 + + + + + + + + + A + 4 + + 4 + 1 + whole + natural + + + 4 + + + + + + + 1 + + 3 + major + + + + G + 2 + + + + + A + 4 + + 4 + 1 + whole + + + 4 + + + + C + 5 + + 4 + 1 + whole + natural + + + 4 + + + + + + -6 + minor + + + + + A + 4 + + 4 + 1 + whole + natural + + + 4 + + + + C + 5 + + 4 + 1 + whole + natural + + + 4 + + + + + + 6 + dorian + + + + + + A + 4 + + 4 + 1 + whole + natural + + + 4 + + + + C + 5 + + 4 + 1 + whole + natural + + + 4 + + + + + + + + + A + 4 + + 4 + 1 + whole + natural + + + 4 + + + + C + 5 + + 4 + 1 + whole + natural + + + 4 + + + + + + + + + A + 4 + + 4 + 1 + whole + natural + + + 4 + + + + C + 5 + + 4 + 1 + whole + natural + + + 4 + + + + + + + 1 + + 3 + major + + + + G + 2 + + + + + C + 5 + + 4 + 1 + whole + natural + + + 4 + + + + + + -6 + minor + + + + + C + 5 + + 4 + 1 + whole + natural + + + 4 + + + + + + 6 + dorian + + + + + + C + 5 + + 4 + 1 + whole + natural + + + 4 + + + + + + + + + C + 5 + + 4 + 1 + whole + natural + + + 4 + + + + + + + + + C + 5 + + 4 + 1 + whole + natural + + + 4 + + + + diff --git a/tests/test_xml_files/skips.ly b/tests/test_xml_files/skips.ly new file mode 100644 index 00000000..9671d0b2 --- /dev/null +++ b/tests/test_xml_files/skips.ly @@ -0,0 +1,39 @@ +\version "2.18.2" + +\language "english" + +keyTime = { + \time 5/4 + \partial 4 +} + +Soprano = \relative c'' { + \voiceOne + \keyTime + c8 \bar "||" c8 + c4 c c \bar "||" c c + c1 c4 + c1 c4 + c1 c4 \bar "|." +} + +Alto = \relative c'' { + \voiceTwo + \keyTime + s1*7/4 + a2. a4 + s1*5/4 + a4 s2 a2 +} + +\score +{ + << + \new Staff = "treble" \with {} + << + \clef "treble" + \new Voice = "SopranoVoice" \Soprano + \new Voice = "AltoVoice" \Alto + >> + >> +} \ No newline at end of file diff --git a/tests/test_xml_files/skips.musicxml b/tests/test_xml_files/skips.musicxml new file mode 100644 index 00000000..cf7204c7 --- /dev/null +++ b/tests/test_xml_files/skips.musicxml @@ -0,0 +1,242 @@ + + + + + + python-ly 0.9.5 + 2019-08-06 + + + + + + + + + + + 2 + + + G + 2 + + + + + C + 5 + + 1 + 1 + eighth + + + 1 + + + 1 + + + 1 + + + + light-light + + + + + + C + 5 + + 1 + 1 + eighth + + + + + + C + 5 + + 2 + 1 + quarter + + + + C + 5 + + 2 + 1 + quarter + + + + C + 5 + + 2 + 1 + quarter + + + 6 + + + 6 + + + 6 + + + + light-light + + + + + + C + 5 + + 2 + 1 + quarter + + + + C + 5 + + 2 + 1 + quarter + + + + + + C + 5 + + 8 + 1 + whole + + + + C + 5 + + 2 + 1 + quarter + + + 10 + + + 2 + + + + A + 4 + + 6 + 2 + half + + + + + A + 4 + + 2 + 2 + quarter + + + + + + C + 5 + + 8 + 1 + whole + + + + C + 5 + + 2 + 1 + quarter + + + + + + C + 5 + + 8 + 1 + whole + + + + C + 5 + + 2 + 1 + quarter + + + 10 + + + + A + 4 + + 2 + 2 + quarter + + + 4 + + + + A + 4 + + 4 + 2 + half + + + 10 + + + + light-heavy + + + + diff --git a/tests/test_xml_files/skips2.ly b/tests/test_xml_files/skips2.ly new file mode 100644 index 00000000..6bd87012 --- /dev/null +++ b/tests/test_xml_files/skips2.ly @@ -0,0 +1,38 @@ +\version "2.18.2" + +\language "english" + +keyTime = { + \time 5/4 + \partial 4 +} + +Soprano = \relative c'' { + \voiceOne + \keyTime + c8 c + c1 \bar "||" c4 + c1 c4 + c1 \bar "||" c4 + c1 c4 +} + +Alto = \relative c'' { + \voiceTwo + \keyTime + a8 s8*12 + a8 s4*10 + a2. a4 +} + +\score +{ + << + \new Staff = "treble" \with {} + << + \clef "treble" + \new Voice = "SopranoVoice" \Soprano + \new Voice = "AltoVoice" \Alto + >> + >> +} diff --git a/tests/test_xml_files/skips2.musicxml b/tests/test_xml_files/skips2.musicxml new file mode 100644 index 00000000..4428fcb0 --- /dev/null +++ b/tests/test_xml_files/skips2.musicxml @@ -0,0 +1,222 @@ + + + + + + python-ly 0.9.5 + 2019-08-26 + + + + + + + + + + + 2 + + + G + 2 + + + + + C + 5 + + 1 + 1 + eighth + begin + + + + C + 5 + + 1 + 1 + eighth + end + + + 2 + + + + A + 4 + + 1 + 2 + eighth + + + 1 + + + 2 + + + + + + C + 5 + + 8 + 1 + whole + + + 8 + + + 8 + + + 8 + + + + light-light + + + + + + C + 5 + + 2 + 1 + quarter + + + + + + C + 5 + + 8 + 1 + whole + + + + C + 5 + + 2 + 1 + quarter + + + 10 + + + 1 + + + + A + 4 + + 1 + 2 + eighth + + + 8 + + + + + + C + 5 + + 8 + 1 + whole + + + 8 + + + 8 + + + 8 + + + + light-light + + + + + + C + 5 + + 2 + 1 + quarter + + + + + + C + 5 + + 8 + 1 + whole + + + + C + 5 + + 2 + 1 + quarter + + + 10 + + + 2 + + + + A + 4 + + 6 + 2 + half + + + + + A + 4 + + 2 + 2 + quarter + + + + diff --git a/tests/test_xml_files/slur_styles.ly b/tests/test_xml_files/slur_styles.ly new file mode 100644 index 00000000..562e6598 --- /dev/null +++ b/tests/test_xml_files/slur_styles.ly @@ -0,0 +1,60 @@ +\version "2.18.2" + +\language "english" + +keyTime = { + \time 4/4 +} + +Soprano = \relative c'' { + \keyTime + \voiceOne + c4~ c \tieDashed c~ c + \tieDotted c~ c << {d~ d} \\ {\tieDashed c~ c} >> + s2 c4~ c +} + +Alto = \relative c'' { + \keyTime + \voiceTwo + g4( a) \slurDashed g( a) + \slurDotted g( a) s2 + << {a4( b)} \\ {\slurDashed f( g)} >> +} + +Tenor = \relative c { + \keyTime + \voiceOne + f4\( g\) \phrasingSlurDashed f\( g\) + \phrasingSlurDotted f4\( g\) \phrasingSlurSolid f\( g\) + f4\( g\) f\( g\) +} + +Bass = \relative c { + \keyTime + \voiceTwo + c4\( d\) c\( d\) + c4\( d\) c\( d\) + c4\( d\) c\( d\) \bar "|." +} + +\score +{ + << + \new Staff = "treble" \with { + } + << + \clef "treble" + \new Voice = "SopranoVoice" \Soprano + \new Voice = "AltoVoice" \Alto + >> + \new Staff = "bass" \with { + } + << + \clef "bass" + \new Voice = "TenorVoice" \Tenor + \new Voice = "BassVoice" \Bass + >> + >> + +} \ No newline at end of file diff --git a/tests/test_xml_files/slur_styles.musicxml b/tests/test_xml_files/slur_styles.musicxml new file mode 100644 index 00000000..93d97785 --- /dev/null +++ b/tests/test_xml_files/slur_styles.musicxml @@ -0,0 +1,691 @@ + + + + + + python-ly 0.9.5 + 2019-10-28 + + + + + + + + + + + + + + 1 + + + G + 2 + + + + + C + 5 + + 1 + + 1 + quarter + + + + + + + C + 5 + + 1 + + 1 + quarter + + + + + + + C + 5 + + 1 + + 1 + quarter + + + + + + + C + 5 + + 1 + + 1 + quarter + + + + + + 4 + + + + G + 4 + + 1 + 2 + quarter + + + + + + + A + 4 + + 1 + 2 + quarter + + + + + + + G + 4 + + 1 + 2 + quarter + + + + + + + A + 4 + + 1 + 2 + quarter + + + + + + 4 + + + + + + C + 5 + + 1 + + 1 + quarter + + + + + + + C + 5 + + 1 + + 1 + quarter + + + + + + + D + 5 + + 1 + + 1 + quarter + + + + + + + D + 5 + + 1 + + 1 + quarter + + + + + + 2 + + + + C + 5 + + 1 + + 2 + quarter + + + + + + + C + 5 + + 1 + + 2 + quarter + + + + + + 4 + + + + G + 4 + + 1 + 2 + quarter + + + + + + + A + 4 + + 1 + 2 + quarter + + + + + + 2 + + + + + 2 + + + + C + 5 + + 1 + + 1 + quarter + + + + + + + C + 5 + + 1 + + 1 + quarter + + + + + + + light-heavy + + + 4 + + + + A + 4 + + 1 + 2 + quarter + + + + + + + B + 4 + + 1 + 2 + quarter + + + + + + 2 + + + + F + 4 + + 1 + 3 + quarter + + + + + + + G + 4 + + 1 + 3 + quarter + + + + + + 2 + + + + light-heavy + + + + light-heavy + + + + + + + 1 + + + F + 4 + + + + + F + 3 + + 1 + 1 + quarter + + + + + + + G + 3 + + 1 + 1 + quarter + + + + + + + F + 3 + + 1 + 1 + quarter + + + + + + + G + 3 + + 1 + 1 + quarter + + + + + + 4 + + + + C + 3 + + 1 + 2 + quarter + + + + + + + D + 3 + + 1 + 2 + quarter + + + + + + + C + 3 + + 1 + 2 + quarter + + + + + + + D + 3 + + 1 + 2 + quarter + + + + + + 4 + + + + + + F + 3 + + 1 + 1 + quarter + + + + + + + G + 3 + + 1 + 1 + quarter + + + + + + + F + 3 + + 1 + 1 + quarter + + + + + + + G + 3 + + 1 + 1 + quarter + + + + + + 4 + + + + C + 3 + + 1 + 2 + quarter + + + + + + + D + 3 + + 1 + 2 + quarter + + + + + + + C + 3 + + 1 + 2 + quarter + + + + + + + D + 3 + + 1 + 2 + quarter + + + + + + + + + F + 3 + + 1 + 1 + quarter + + + + + + + G + 3 + + 1 + 1 + quarter + + + + + + + F + 3 + + 1 + 1 + quarter + + + + + + + G + 3 + + 1 + 1 + quarter + + + + + + 4 + + + + C + 3 + + 1 + 2 + quarter + + + + + + + D + 3 + + 1 + 2 + quarter + + + + + + + C + 3 + + 1 + 2 + quarter + + + + + + + D + 3 + + 1 + 2 + quarter + + + + + + + light-heavy + + + 4 + + + + light-heavy + + + + light-heavy + + + + light-heavy + + + + diff --git a/tests/test_xml_files/subtitle.ly b/tests/test_xml_files/subtitle.ly new file mode 100644 index 00000000..65b1205a --- /dev/null +++ b/tests/test_xml_files/subtitle.ly @@ -0,0 +1,27 @@ +\version "2.18.2" + +\language "english" + +\header{ + title = "Title" + subtitle = "Subtitle" +} + +keyTime = { + \time 4/4 +} + +Soprano = \relative c'' { + \keyTime + c4 c c2 + c c4 c \bar "|." +} + +Alto = \relative c'' { } + +\score +{ + << + \new Staff \Soprano + >> +} \ No newline at end of file diff --git a/tests/test_xml_files/tuplet.xml b/tests/test_xml_files/subtitle.musicxml similarity index 54% rename from tests/test_xml_files/tuplet.xml rename to tests/test_xml_files/subtitle.musicxml index d483be55..82f226a3 100644 --- a/tests/test_xml_files/tuplet.xml +++ b/tests/test_xml_files/subtitle.musicxml @@ -2,10 +2,12 @@ + Subtitle + Title - python-ly 0.9.4 - 2016-03-28 + python-ly 0.9.5 + 2019-12-17 @@ -16,7 +18,7 @@ - 3 + 1 + - D - 3 + C + 5 2 1 + half + + + + C + 5 + + 1 + 1 + quarter + + + + C + 5 + + 1 + 1 quarter - - 3 - 2 - - - - + + 4 + + + + light-heavy + diff --git a/tests/test_xml_files/tie.xml b/tests/test_xml_files/tie.musicxml similarity index 100% rename from tests/test_xml_files/tie.xml rename to tests/test_xml_files/tie.musicxml diff --git a/tests/test_xml_files/transpose.ly b/tests/test_xml_files/transpose.ly new file mode 100644 index 00000000..f5524f55 --- /dev/null +++ b/tests/test_xml_files/transpose.ly @@ -0,0 +1,60 @@ +\version "2.18.2" + +\language "english" + +keyTime = { + \time 5/4 + \partial 4 + \key f \minor +} + +Soprano = \transpose f gs { \relative c'' { + \voiceOne + \keyTime + e4 + e ef es e + \bar "|." + } +} + +Alto = \transpose f gs { \relative c' { + \voiceTwo + \keyTime + d4 + d d d d + } +} + +Tenor = \transpose f gs { \relative c { + \voiceOne + \keyTime + c4 + c c c c + } +} + +Bass = \transpose f gs { \relative c { + \voiceTwo + \keyTime + a4 + a a a a + } +} + +\score +{ + << + \new Staff = "treble" \with {} + << + \clef "treble" + \new Voice = "SopranoVoice" \Soprano + \new Voice = "AltoVoice" \Alto + >> + \new Staff = "bass" \with {} + << + \clef "bass" + \new Voice = "TenorVoice" \Tenor + \new Voice = "BassVoice" \Bass + >> + >> +} diff --git a/tests/test_xml_files/transpose.musicxml b/tests/test_xml_files/transpose.musicxml new file mode 100644 index 00000000..2426d2ab --- /dev/null +++ b/tests/test_xml_files/transpose.musicxml @@ -0,0 +1,301 @@ + + + + + + python-ly 0.9.5 + 2019-08-01 + + + + + + + + + + + + + + 1 + + 5 + minor + + + + G + 2 + + + + + F + 2 + 5 + + 1 + 1 + quarter + sharp-sharp + + + 1 + + + + E + 1 + 4 + + 1 + 2 + quarter + sharp + + + 1 + + + + + + F + 2 + 5 + + 1 + 1 + quarter + sharp-sharp + + + + F + 1 + 5 + + 1 + 1 + quarter + sharp + + + + G + 1 + 5 + + 1 + 1 + quarter + + + + F + 2 + 5 + + 1 + 1 + quarter + sharp-sharp + + + 4 + + + + E + 1 + 4 + + 1 + 2 + quarter + sharp + + + + E + 1 + 4 + + 1 + 2 + quarter + + + + E + 1 + 4 + + 1 + 2 + quarter + + + + E + 1 + 4 + + 1 + 2 + quarter + + + 4 + + + + light-heavy + + + + + + + 1 + + 5 + minor + + + + F + 4 + + + + + D + 1 + 3 + + 1 + 1 + quarter + + + 1 + + + + B + 1 + 2 + + 1 + 2 + quarter + sharp + + + 1 + + + + + + D + 1 + 3 + + 1 + 1 + quarter + + + + D + 1 + 3 + + 1 + 1 + quarter + + + + D + 1 + 3 + + 1 + 1 + quarter + + + + D + 1 + 3 + + 1 + 1 + quarter + + + 4 + + + + B + 1 + 2 + + 1 + 2 + quarter + sharp + + + + B + 1 + 2 + + 1 + 2 + quarter + + + + B + 1 + 2 + + 1 + 2 + quarter + + + + B + 1 + 2 + + 1 + 2 + quarter + + + 4 + + + + light-heavy + + + + diff --git a/tests/test_xml_files/tremolo.ly b/tests/test_xml_files/tremolo.ly new file mode 100644 index 00000000..cccc6b38 --- /dev/null +++ b/tests/test_xml_files/tremolo.ly @@ -0,0 +1,26 @@ +\version "2.18.2" + +\language "english" + +Soprano = \relative c'' { + \time 4/4 \key c \major + c2.:8 c4:16 c1:32 c: + \repeat tremolo 6 c8 + \repeat tremolo 4 c16 + \repeat tremolo 32 c32 + \repeat tremolo 2 { c16 d8 c16} a2 + \repeat tremolo 6 { c16 d } + \repeat tremolo 2 { c16 d } + \repeat tremolo 4 { c16 d8 c16} +} + +\score +{ + << + \new Staff = "treble" \with {} + << + \clef "treble" + \new Voice = "SopranoVoice" \Soprano + >> + >> +} diff --git a/tests/test_xml_files/tremolo.musicxml b/tests/test_xml_files/tremolo.musicxml new file mode 100644 index 00000000..6af0444b --- /dev/null +++ b/tests/test_xml_files/tremolo.musicxml @@ -0,0 +1,311 @@ + + + + + + python-ly 0.9.5 + 2019-08-26 + + + + + + + + + + + 8 + + 0 + major + + + + G + 2 + + + + + C + 5 + + 24 + 1 + half + + + + 1 + + + + + + C + 5 + + 8 + 1 + quarter + + + 2 + + + + + 32 + + + + + + C + 5 + + 32 + 1 + whole + + + 3 + + + + + + + + C + 5 + + 32 + 1 + whole + + + 3 + + + + + + + + C + 5 + + 24 + 1 + half + + + + 1 + + + + + + C + 5 + + 8 + 1 + quarter + + + 2 + + + + + + + + C + 5 + + 32 + 1 + whole + + + 3 + + + + + + + + C + 5 + + 4 + 1 + quarter + + + + 2 + + + + + + D + 5 + + 8 + 1 + half + + + + 1 + + + + + + C + 5 + + 4 + 1 + quarter + + + + 2 + + + + + + A + 4 + + 16 + 1 + half + + + + + + C + 5 + + 12 + 1 + half + + + + 2 + + + + + + D + 5 + + 12 + 1 + half + + + + 2 + + + + + + C + 5 + + 4 + 1 + quarter + begin + + + 2 + + + + + + D + 5 + + 4 + 1 + quarter + end + + + 2 + + + + + + + + C + 5 + + 8 + 1 + half + + + + 2 + + + + + + D + 5 + + 16 + 1 + whole + + + + 1 + + + + + + C + 5 + + 8 + 1 + half + + + + 2 + + + + + + diff --git a/tests/test_xml_files/tuplet.ly b/tests/test_xml_files/tuplet.ly index 520e5a68..bfbe85fe 100644 --- a/tests/test_xml_files/tuplet.ly +++ b/tests/test_xml_files/tuplet.ly @@ -1,6 +1,43 @@ \version "2.18.2" -\score { - { c4~ \times 2/3 { c4 c4 d } } - \layout {} +StanzaOne = \lyricmode { + La di la di la + la di la di la di la di la di + la di la di la di la. +} + +\language "english" + +keyTime = { + \time 4/4 + \key c \major +} + +Soprano = \relative c'' { + \voiceOne + \keyTime + \tuplet 3/2 { bf4 bs a } a4 a + \tuplet 5/4 { a8 a a a a } \tuplet 5/2 { a4 a a a a } + \tuplet 7/4 { a4 a a a a a a } +} + +Alto = \relative c' { + \voiceTwo + \keyTime + e1 + e1 + e1 +} + +\score +{ + << + \new Staff = "treble" + << + \clef "treble" + \new Voice = "SopranoVoice" \Soprano + \new Voice = "AltoVoice" \Alto + \lyricsto SopranoVoice \new Lyrics \StanzaOne + >> + >> } diff --git a/tests/test_xml_files/tuplet.musicxml b/tests/test_xml_files/tuplet.musicxml new file mode 100644 index 00000000..35e4e7a5 --- /dev/null +++ b/tests/test_xml_files/tuplet.musicxml @@ -0,0 +1,477 @@ + + + + + + python-ly 0.9.5 + 2019-08-16 + + + + + + + + + + + 105 + + 0 + major + + + + G + 2 + + + + + B + -1 + 4 + + 70 + 1 + quarter + flat + + 3 + 2 + + + + + + single + La + + + + + B + 1 + 4 + + 70 + 1 + quarter + sharp + + 3 + 2 + + + single + di + + + + + A + 4 + + 70 + 1 + quarter + + 3 + 2 + + + + + + single + la + + + + + A + 4 + + 105 + 1 + quarter + + single + di + + + + + A + 4 + + 105 + 1 + quarter + + single + la + + + + 420 + + + + E + 4 + + 420 + 2 + whole + + + 420 + + + + + + A + 4 + + 42 + 1 + eighth + + 5 + 4 + + begin + + + + + single + la + + + + + A + 4 + + 42 + 1 + eighth + + 5 + 4 + + continue + + single + di + + + + + A + 4 + + 42 + 1 + eighth + + 5 + 4 + + continue + + single + la + + + + + A + 4 + + 42 + 1 + eighth + + 5 + 4 + + continue + + single + di + + + + + A + 4 + + 42 + 1 + eighth + + 5 + 4 + + end + + + + + single + la + + + + + A + 4 + + 42 + 1 + quarter + + 5 + 2 + + + + + + single + di + + + + + A + 4 + + 42 + 1 + quarter + + 5 + 2 + + + single + la + + + + + A + 4 + + 42 + 1 + quarter + + 5 + 2 + + + single + di + + + + + A + 4 + + 42 + 1 + quarter + + 5 + 2 + + + single + la + + + + + A + 4 + + 42 + 1 + quarter + + 5 + 2 + + + + + + single + di + + + + 420 + + + + E + 4 + + 420 + 2 + whole + + + + + + A + 4 + + 60 + 1 + quarter + + 7 + 4 + + + + + + single + la + + + + + A + 4 + + 60 + 1 + quarter + + 7 + 4 + + + single + di + + + + + A + 4 + + 60 + 1 + quarter + + 7 + 4 + + + single + la + + + + + A + 4 + + 60 + 1 + quarter + + 7 + 4 + + + single + di + + + + + A + 4 + + 60 + 1 + quarter + + 7 + 4 + + + single + la + + + + + A + 4 + + 60 + 1 + quarter + + 7 + 4 + + + single + di + + + + + A + 4 + + 60 + 1 + quarter + + 7 + 4 + + + + + + single + la. + + + + 420 + + + + E + 4 + + 420 + 2 + whole + + + + diff --git a/tests/test_xml_files/unfold.ly b/tests/test_xml_files/unfold.ly new file mode 100644 index 00000000..1f6b39f3 --- /dev/null +++ b/tests/test_xml_files/unfold.ly @@ -0,0 +1,29 @@ +\version "2.18.2" + +\language "english" + +Soprano = \relative c'' { + \time 4/4 \key c \major + \voiceOne + \repeat unfold 5 {a2} + \alternative {{b2 b1 \bar ""} {b2 c1} {d2 d1}} \bar "|." +} + +Alto = \relative c'' { + \time 4/4 \key c \major + \voiceTwo + \repeat unfold 5 {cs2} + \alternative {{cs2 d1} {e2 e1} {f2 f1}} +} + +\score +{ + << + \new Staff = "treble" \with {} + << + \clef "treble" + \new Voice = "SopranoVoice" \Soprano + \new Voice = "AltoVoice" \Alto + >> + >> +} diff --git a/tests/test_xml_files/unfold.musicxml b/tests/test_xml_files/unfold.musicxml new file mode 100644 index 00000000..52a4aff7 --- /dev/null +++ b/tests/test_xml_files/unfold.musicxml @@ -0,0 +1,397 @@ + + + + + + python-ly 0.9.5 + 2019-08-26 + + + + + + + + + + + 1 + + 0 + major + + + + G + 2 + + + + + A + 4 + + 2 + 1 + half + + + + B + 4 + + 2 + 1 + half + + + 4 + + + + C + 1 + 5 + + 2 + 2 + half + sharp + + + + C + 1 + 5 + + 2 + 2 + half + + + 4 + + + + + + B + 4 + + 4 + 1 + whole + + + 4 + + + + D + 5 + + 4 + 2 + whole + + + 4 + + + + none + + + + + + A + 4 + + 2 + 1 + half + + + + B + 4 + + 2 + 1 + half + + + 4 + + + + C + 1 + 5 + + 2 + 2 + half + sharp + + + + C + 1 + 5 + + 2 + 2 + half + + + + + + B + 4 + + 4 + 1 + whole + + + 4 + + + + D + 5 + + 4 + 2 + whole + + + 4 + + + + none + + + + + + A + 4 + + 2 + 1 + half + + + + B + 4 + + 2 + 1 + half + + + 4 + + + + C + 1 + 5 + + 2 + 2 + half + sharp + + + + C + 1 + 5 + + 2 + 2 + half + + + + + + B + 4 + + 4 + 1 + whole + + + 4 + + + + D + 5 + + 4 + 2 + whole + + + 4 + + + + none + + + + + + A + 4 + + 2 + 1 + half + + + + B + 4 + + 2 + 1 + half + + + 4 + + + + C + 1 + 5 + + 2 + 2 + half + sharp + + + + E + 5 + + 2 + 2 + half + + + + + + C + 5 + + 4 + 1 + whole + + + 4 + + + + E + 5 + + 4 + 2 + whole + + + + + + A + 4 + + 2 + 1 + half + + + + D + 5 + + 2 + 1 + half + + + 4 + + + + C + 1 + 5 + + 2 + 2 + half + sharp + + + + F + 5 + + 2 + 2 + half + + + + + + D + 5 + + 4 + 1 + whole + + + 4 + + + + F + 5 + + 4 + 2 + whole + + + 4 + + + + light-heavy + + + + diff --git a/tests/test_xml_files/variable.xml b/tests/test_xml_files/variable.musicxml similarity index 100% rename from tests/test_xml_files/variable.xml rename to tests/test_xml_files/variable.musicxml diff --git a/tests/test_xml_files/voice_separators.ly b/tests/test_xml_files/voice_separators.ly new file mode 100644 index 00000000..204220a0 --- /dev/null +++ b/tests/test_xml_files/voice_separators.ly @@ -0,0 +1,65 @@ +\version "2.18.2" + +\language "english" + +keyTime = { + \time 5/4 + \partial 4 +} + +Soprano = \relative c'' { + \voiceOne + \keyTime + << { c4 } \\ { a4 } >> + r4 r g g g + << { c1 4 c1 \bar "||" 4 } \\ { g1 4 g1 g8( g) } >> + \bar "|." +} + +Alto = \relative c' { + \voiceTwo + \keyTime + s4 + s1*5/2 +} + +Tenor = \relative c { + \voiceOne + \keyTime + c4 + c c c c c + c c c c c + c c c c c +} + +Bass = \relative c { + \voiceTwo + \keyTime + a4 + a a a a a + a a a a a + a a a a a +} + +\score +{ + << + \new Staff = "treble" \with { + } + << + \clef "treble" + \new Voice = "SopranoVoice" \Soprano + \new Voice = "AltoVoice" \Alto + >> + + \new Staff = "bass" \with { + } + << + \clef "bass" + \new Voice = "TenorVoice" \Tenor + \new Voice = "BassVoice" \Bass + >> + + >> + +} diff --git a/tests/test_xml_files/voice_separators.musicxml b/tests/test_xml_files/voice_separators.musicxml new file mode 100644 index 00000000..c4085950 --- /dev/null +++ b/tests/test_xml_files/voice_separators.musicxml @@ -0,0 +1,590 @@ + + + + + + python-ly 0.9.5 + 2019-08-08 + + + + + + + + + + + + + + 2 + + + G + 2 + + + + + C + 5 + + 2 + 1 + quarter + + + 2 + + + + A + 4 + + 2 + 2 + quarter + + + 2 + + + + + + 2 + 1 + quarter + + + + 2 + 1 + quarter + + + + G + 4 + + 2 + 1 + quarter + + + + G + 4 + + 2 + 1 + quarter + + + + G + 4 + + 2 + 1 + quarter + + + + + + C + 5 + + 8 + 1 + whole + + + + C + 5 + + 2 + 1 + quarter + + + + + A + 4 + + 2 + 1 + quarter + + + 10 + + + + G + 4 + + 8 + 2 + whole + + + + G + 4 + + 2 + 2 + quarter + + + + + E + 4 + + 2 + 2 + quarter + + + + + + C + 5 + + 8 + 1 + whole + + + 8 + + + + G + 4 + + 8 + 2 + whole + + + 8 + + + + light-light + + + + + + C + 5 + + 2 + 1 + quarter + + + + + A + 4 + + 2 + 1 + quarter + + + 2 + + + + G + 4 + + 1 + 2 + eighth + begin + + + + + + + G + 4 + + 1 + 2 + eighth + end + + + + + + 2 + + + + light-heavy + + + + + + + 2 + + + F + 4 + + + + + C + 3 + + 2 + 1 + quarter + + + 2 + + + + A + 2 + + 2 + 2 + quarter + + + 2 + + + + + + C + 3 + + 2 + 1 + quarter + + + + C + 3 + + 2 + 1 + quarter + + + + C + 3 + + 2 + 1 + quarter + + + + C + 3 + + 2 + 1 + quarter + + + + C + 3 + + 2 + 1 + quarter + + + 10 + + + + A + 2 + + 2 + 2 + quarter + + + + A + 2 + + 2 + 2 + quarter + + + + A + 2 + + 2 + 2 + quarter + + + + A + 2 + + 2 + 2 + quarter + + + + A + 2 + + 2 + 2 + quarter + + + + + + C + 3 + + 2 + 1 + quarter + + + + C + 3 + + 2 + 1 + quarter + + + + C + 3 + + 2 + 1 + quarter + + + + C + 3 + + 2 + 1 + quarter + + + + C + 3 + + 2 + 1 + quarter + + + 10 + + + + A + 2 + + 2 + 2 + quarter + + + + A + 2 + + 2 + 2 + quarter + + + + A + 2 + + 2 + 2 + quarter + + + + A + 2 + + 2 + 2 + quarter + + + + A + 2 + + 2 + 2 + quarter + + + + + + C + 3 + + 2 + 1 + quarter + + + + C + 3 + + 2 + 1 + quarter + + + + C + 3 + + 2 + 1 + quarter + + + + C + 3 + + 2 + 1 + quarter + + + 8 + + + + A + 2 + + 2 + 2 + quarter + + + + A + 2 + + 2 + 2 + quarter + + + + A + 2 + + 2 + 2 + quarter + + + + A + 2 + + 2 + 2 + quarter + + + 8 + + + + light-light + + + + + + C + 3 + + 2 + 1 + quarter + + + 2 + + + + A + 2 + + 2 + 2 + quarter + + + 2 + + + + light-heavy + + + + diff --git a/tests/test_xml_files/voice_separators2.ly b/tests/test_xml_files/voice_separators2.ly new file mode 100644 index 00000000..bde74809 --- /dev/null +++ b/tests/test_xml_files/voice_separators2.ly @@ -0,0 +1,54 @@ +\version "2.18.2" + +\language "english" + +keyTime = { + \time 4/4 +} + +Soprano = \relative c'' { + \voiceOne + \keyTime + d4 d << \\ {\voiceThree d2} \\ \\ {a4 a} >> + d d d d \bar "|." +} + +Alto = \relative c'' { + \voiceTwo + \keyTime + a4 a s2 + a4 a a a \bar "|." +} + +switchTwo = { + \set associatedVoice = "2" +} +switchFour = { + \set associatedVoice = "4" +} +switchSop = { + \set associatedVoice = "SopranoVoice" +} + +StanzaOne = \lyricmode { + La \switchTwo di \switchSop laaa + la di la di! +} + +StanzaTwo = \lyricmode { + Ba \switchFour dum ba \switchSop dum ba dum ba dum! +} + +\score +{ + << + \new Staff = "treble" \with {} + << + \clef "treble" + \new Voice = "SopranoVoice" \Soprano + \new Voice = "AltoVoice" \Alto + \lyricsto SopranoVoice \new Lyrics \StanzaOne + \lyricsto SopranoVoice \new Lyrics \StanzaTwo + >> + >> +} \ No newline at end of file diff --git a/tests/test_xml_files/voice_separators2.musicxml b/tests/test_xml_files/voice_separators2.musicxml new file mode 100644 index 00000000..9773cec8 --- /dev/null +++ b/tests/test_xml_files/voice_separators2.musicxml @@ -0,0 +1,247 @@ + + + + + + python-ly 0.9.5 + 2019-10-28 + + + + + + + + + + + 1 + + + G + 2 + + + + + D + 5 + + 1 + 1 + quarter + + single + La + + + single + Ba + + + + + D + 5 + + 1 + 1 + quarter + + single + di + + + single + dum + + + + + D + 5 + + 2 + 3 + half + + single + laaa + + + + 2 + + + + A + 4 + + 1 + 5 + quarter + + single + ba + + + + + A + 4 + + 1 + 5 + quarter + + single + dum + + + + 4 + + + + A + 4 + + 1 + 2 + quarter + + + + A + 4 + + 1 + 2 + quarter + + + 2 + + + + + + D + 5 + + 1 + 1 + quarter + + single + la + + + single + ba + + + + + D + 5 + + 1 + 1 + quarter + + single + di + + + single + dum + + + + + D + 5 + + 1 + 1 + quarter + + single + la + + + single + ba + + + + + D + 5 + + 1 + 1 + quarter + + single + di! + + + single + dum! + + + + 4 + + + + A + 4 + + 1 + 2 + quarter + + + + A + 4 + + 1 + 2 + quarter + + + + A + 4 + + 1 + 2 + quarter + + + + A + 4 + + 1 + 2 + quarter + + + 4 + + + + light-heavy + + + + diff --git a/tests/test_xml_files/volta.ly b/tests/test_xml_files/volta.ly new file mode 100644 index 00000000..cc3a7861 --- /dev/null +++ b/tests/test_xml_files/volta.ly @@ -0,0 +1,57 @@ +\version "2.18.2" + +\language "english" + +Soprano = \relative c'' { + \time 4/4 \key c \major + \voiceOne + \repeat volta 15 { a1 a2} + \alternative { + {\tuplet 3/2 {a4 a a}} + {b1} + {\time 8/8 \key f \major c2 c} + {d2} + {e2} + {f1 \bar "||"} + {g2} + } + a,2 \bar "" + \repeat volta 3 {a1 \bar ""} + a1 + \repeat volta 3 {a1} + \repeat volta 2 {a1} + \alternative {{b1}} \bar ".|" c1 +} + +Alto = \relative c'' { + \time 4/4 \key c \major + \voiceTwo + \repeat volta 15 { c1 c2} + \alternative { + {\tuplet 3/2 {c4 c c}} + {d1} + {\time 8/8 \key f \major e2 e} + {f2} + {g2} + {a1 \bar "||"} + {b2} + } + c,2 \bar "" + \repeat volta 3 {c1 \bar ""} + c1 + \repeat volta 3 {c1} + \repeat volta 2 {c1} + \alternative {{d1}} \bar ".|" e1 +} + +\score +{ + << + \new Staff = "treble" \with {} + << + \clef "treble" + \new Voice = "SopranoVoice" \Soprano + \new Voice = "AltoVoice" \Alto + >> + >> +} diff --git a/tests/test_xml_files/volta.musicxml b/tests/test_xml_files/volta.musicxml new file mode 100644 index 00000000..4cee0027 --- /dev/null +++ b/tests/test_xml_files/volta.musicxml @@ -0,0 +1,670 @@ + + + + + + python-ly 0.9.5 + 2019-08-21 + + + + + + + + + + + 3 + + 0 + major + + + + G + 2 + + + + + A + 4 + + 12 + 1 + whole + + + 12 + + + + C + 5 + + 12 + 2 + whole + + + 12 + + + + + + A + 4 + + 6 + 1 + half + + + 6 + + + + C + 5 + + 6 + 2 + half + + + 6 + + + + none + + + + + + 1.-9. + + + + + + + A + 4 + + 2 + 1 + quarter + + 3 + 2 + + + + + + + + A + 4 + + 2 + 1 + quarter + + 3 + 2 + + + + + A + 4 + + 2 + 1 + quarter + + 3 + 2 + + + + + + + 6 + + + + C + 5 + + 2 + 2 + quarter + + 3 + 2 + + + + + + + + C + 5 + + 2 + 2 + quarter + + 3 + 2 + + + + + C + 5 + + 2 + 2 + quarter + + 3 + 2 + + + + + + + 6 + + + + light-heavy + + + + + + + 10. + + + + + + + B + 4 + + 12 + 1 + whole + + + 12 + + + + D + 5 + + 12 + 2 + whole + + + 12 + + + + light-heavy + + + + + + + -1 + major + + + + + 11. + + + + + + + C + 5 + + 6 + 1 + half + + + + C + 5 + + 6 + 1 + half + + + 12 + + + + E + 5 + + 6 + 2 + half + + + + E + 5 + + 6 + 2 + half + + + 12 + + + + light-heavy + + + + + + + 12. + + + + + + + D + 5 + + 6 + 1 + half + + + 6 + + + + F + 5 + + 6 + 2 + half + + + 6 + + + + light-heavy + + + + + + + 13. + + + + + + + E + 5 + + 6 + 1 + half + + + 6 + + + + G + 5 + + 6 + 2 + half + + + 6 + + + + light-heavy + + + + + + + 14. + + + + + + + F + 5 + + 12 + 1 + whole + + + 12 + + + + A + 5 + + 12 + 2 + whole + + + 12 + + + + light-light + + + + + + + 15. + + + + + + + G + 5 + + 6 + 1 + half + + + 6 + + + + B + 5 + + 6 + 2 + half + natural + + + 6 + + + + none + + + + + + A + 4 + + 6 + 1 + half + + + 6 + + + + C + 5 + + 6 + 2 + half + + + 6 + + + + none + + + + + + + + + + A + 4 + + 12 + 1 + whole + + + 12 + + + + C + 5 + + 12 + 2 + whole + + + 12 + + + + none + + + + + + + A + 4 + + 12 + 1 + whole + + + 12 + + + + C + 5 + + 12 + 2 + whole + + + + + + heavy-light + + + + + A + 4 + + 12 + 1 + whole + + + 12 + + + + C + 5 + + 12 + 2 + whole + + + 12 + + + + light-heavy + + + + + + + heavy-light + + + + + A + 4 + + 12 + 1 + whole + + + 12 + + + + C + 5 + + 12 + 2 + whole + + + 12 + + + + + + 1.-2. + + + + + + + B + 4 + + 12 + 1 + whole + natural + + + 12 + + + + D + 5 + + 12 + 2 + whole + + + 12 + + + + heavy-light + + + + + + C + 5 + + 12 + 1 + whole + + + 12 + + + + E + 5 + + 12 + 2 + whole + + + + diff --git a/tests/test_xml_files/volta_lyrics.ly b/tests/test_xml_files/volta_lyrics.ly new file mode 100644 index 00000000..7e2f5a37 --- /dev/null +++ b/tests/test_xml_files/volta_lyrics.ly @@ -0,0 +1,24 @@ +\version "2.18.2" + +Stanza = \lyricmode { + \repeat volta 2 { Hi, you } + \alternative {{ are } { my best friend Joe! }} +} + +Soprano = \relative c'' { + \time 4/4 + \repeat volta 2 {g2 g} + \alternative {{g1} {g4 g g g}} \bar "|." +} + +\score +{ + << + \new Staff = "treble" + << + \clef "treble" + \new Voice = "SopranoVoice" \Soprano + \lyricsto SopranoVoice \new Lyrics \Stanza + >> + >> +} \ No newline at end of file diff --git a/tests/test_xml_files/volta_lyrics.musicxml b/tests/test_xml_files/volta_lyrics.musicxml new file mode 100644 index 00000000..a8973009 --- /dev/null +++ b/tests/test_xml_files/volta_lyrics.musicxml @@ -0,0 +1,158 @@ + + + + + + python-ly 0.9.5 + 2019-09-05 + + + + + + + + + + + 1 + + + G + 2 + + + + + G + 4 + + 2 + 1 + half + + single + Hi, + + + + + G + 4 + + 2 + 1 + half + + single + you + + + + 4 + + + + + + 1. + + + + + + + G + 4 + + 4 + 1 + whole + + single + are + + + + 4 + + + + light-heavy + + + + + + + 2. + + + + + + + G + 4 + + 1 + 1 + quarter + + single + my + + + + + G + 4 + + 1 + 1 + quarter + + single + best + + + + + G + 4 + + 1 + 1 + quarter + + single + friend + + + + + G + 4 + + 1 + 1 + quarter + + single + Joe! + + + + 4 + + + + light-heavy + + + +