nCoda Lychee Docs

Source code for lychee.converters.inbound.lilypond

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Program Name:           Lychee
# Program Description:    MEI document manager for formalized document control
# Filename:               lychee/converters/
# Purpose:                Converts a LilyPond document to a Lychee-MEI document.
# Copyright (C) 2016 Christopher Antila
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License along
# with this program.  If not, see <>.
Converts a LilyPond document to a Lychee-MEI document.

.. warning::
    This module is intended for internal *Lychee* use only, so the API may change without notice.
    If you wish to use this module outside *Lychee*, please contact us to discuss the best way.

.. tip::
    We recommend that you use the converters indirectly.
    Refer to :ref:`how-to-use-converters` for more information.

# NOTE for Lychee developers:
# The module "lychee.converters.lilypond_parser" is autogenerated from "lilypond.ebnf" using TatSu.
# Run this command in this directory to regenerate the "lilypond_parser" module after you update
# the EBNF grammar specification:
# $ python -m tatsu -c -o lilypond.ebnf

from __future__ import unicode_literals

import collections

from lxml import etree

from lychee import exceptions
from lychee.converters.inbound import lilypond_parser
from lychee.utils import lilypond_utils
from lychee.utils import music_utils
from lychee import exceptions
from lychee.logs import INBOUND_LOG as log
from lychee.namespaces import mei
from lychee.signals import inbound

    ",,": '1',
    ",": '2',
    "'''''": '8',
    "''''": '7',
    "'''": '6',
    "''": '5',
    "'": '4',
    None: '3'
    'major': {
        'cf': '7f',
        'gf': '6f',
        'df': '5f',
        'af': '4f',
        'ef': '3f',
        'bf': '2f',
        'f': '1f',
        'c': '0',
        'g': '1s',
        'd': '2s',
        'a': '3s',
        'e': '4s',
        'b': '5s',
        'fs': '6s',
        'cs': '7s',
    'minor': {
        'af': '7f',
        'ef': '6f',
        'bf': '5f',
        'f': '4f',
        'c': '3f',
        'g': '2f',
        'd': '1f',
        'a': '0',
        'e': '1s',
        'b': '2s',
        'fs': '3s',
        'cs': '4s',
        'gs': '5s',
        'ds': '6s',
        'as': '7s',
ClefSpec = collections.namedtuple('ClefSpec', ('shape', 'line'))
    'bass': ClefSpec('F', '4'),
    'tenor': ClefSpec('C', '4'),
    'alto': ClefSpec('C', '3'),
    'treble': ClefSpec('G', '2'),
# defined at end of file: _STAFF_SETTINGS_FUNCTIONS

[docs]def check(condition, message=None): """ Check that ``condition`` is ``True``. :param bool condition: This argument will be checked to be ``True``. :param str message: A failure message to use if the check does not pass. :raises: :exc:`exceptions.LilyPondError` when ``condition`` is anything other than ``True``. Use this function to guarantee that something is the case. This function replaces the ``assert`` statement but is always executed (not only in debug mode). **Example 1** >>> check(5 == 5) The ``5 == 5`` evaluates to ``True``, so the function returns just fine. **Example 2** >>> check(5 == 4) The ``5 == 4`` evaluates to ``False``, so the function raises an :exc:`exceptions.LilyPondError`. """ message = 'check failed' if message is None else message if condition is not True: raise exceptions.LilyPondError(message)
[docs]def convert(document, user_settings=None, **kwargs): ''' Convert a LilyPond document into an MEI document. This is the entry point for Lychee conversions. :param str document: The LilyPond document. :returns: The corresponding MEI document. :rtype: :class:`xml.etree.ElementTree.Element` or :class:`xml.etree.ElementTree.ElementTree` ''' inbound.CONVERSION_STARTED.emit() section = convert_no_signals(document, user_settings=user_settings) inbound.CONVERSION_FINISH.emit(converted=section) return section
[docs]def convert_no_signals(document, user_settings=None): ''' It's the convert() function that returns the converted document rather than emitting it with the CONVERSION_FINISHED signal. Mostly for testing. ''' # NOTE: this function has no tests because it will soon be changed; see T113 with'parse LilyPond') as action: parser = lilypond_parser.LilyPondParser(parseinfo=False) parsed = parser.parse(document, filename='file', trace=False) with'convert LilyPond') as action: converted = do_document(parsed, user_settings=user_settings) return converted
@log.wrap('info', 'process document') def do_document(l_document, user_settings): l_score = None if user_settings is None: user_settings = {} context = { 'language': 'nederlands', } for l_top_level_element in l_document: if isinstance(l_top_level_element, dict): ly_type = l_top_level_element['ly_type'] if ly_type == 'version': check_version(l_top_level_element) elif ly_type == 'language': context['language'] = l_top_level_element['language'] elif ly_type == 'score': l_score = l_top_level_element elif ly_type == 'staff': l_score = {'ly_type': 'score', 'staves': [l_top_level_element]} if l_score is None: raise exceptions.LilyPondError('Empty document') user_settings['lilyPondLanguage'] = context['language'] converted = do_score(l_score, context=context) return converted
[docs]@log.wrap('info', 'check syntax version', 'action') def check_version(ly_version, action): ''' Guarantees the version is at least somewhat compatible. If the major version is not '2', raises. If the minor version is other than '18' or '19', warns. ''' if ly_version['version']: ly_version_string = '.'.join(ly_version['version']) if ly_version['version'][0] != '2': raise RuntimeError( "inbound LilyPond parser expects version 2.18.x or 2.19.x, got '{}'" .format(ly_version_string)) elif ly_version['version'][1] not in ('18', '19'): action.failure( "inbound LilyPond parser expects version 2.18.x or 2.19.x, got '{}'" .format(ly_version_string)) else: action.failure('missing version info')
[docs]@log.wrap('info', 'convert score') def do_score(l_score, context=None): ''' Convert a LilyPond score to an LMEI <section>. :param dict context: Contains document-wide information such as language. :param dict l_score: The LilyPond score as parsed by TatSu. :returns: A converted Lychee-MEI <section> element. :rtype: :class:`lxml.etree.Element` ''' check(l_score['ly_type'] == 'score', 'did not receive a score') m_section = etree.Element(mei.SECTION) m_scoredef = etree.SubElement(m_section, mei.SCORE_DEF) m_staffgrp = etree.SubElement(m_scoredef, mei.STAFF_GRP) # Sometimes we only get one staff instead of simultaneous staves. staves = l_score['staves'] if isinstance(staves, dict): staves = [staves] for staff_n, l_staff in enumerate(staves): # we have to add one to staff_n or else the @n attributes would start at zero! m_staffdef = etree.SubElement(m_staffgrp, mei.STAFF_DEF, {'n': str(staff_n + 1), 'lines': '5'}) do_staff(l_staff, m_section, m_staffdef, context=context) return m_section
[docs]@log.wrap('debug', 'set clef', 'action') def set_clef(l_clef, m_staffdef, context=None, action=None): ''' Set the clef for a staff. :param dict l_time: The clef as parsed by TatSu. :param m_staffdef: The LMEI <staffDef> on which to set the clef. :type m_staffdef: :class:`lxml.etree.Element` :returns: ``None`` If the clef type is not recognized, :func:`set_initial_clef` emits a failure log message and does not set a clef. ''' check(l_clef['ly_type'] == 'clef', 'did not receive a clef') if l_clef['type'] in _CLEF_MAPPING: m_staffdef.set('clef.shape', _CLEF_MAPPING[l_clef['type']].shape) m_staffdef.set('clef.line', _CLEF_MAPPING[l_clef['type']].line) else: action.failure('unrecognized clef type: {clef_type}', clef_type=l_clef['type'])
[docs]@log.wrap('debug', 'set time signature') def set_time(l_time, m_staffdef, context=None): ''' Set the time signature for a staff. :param dict l_time: The time specification as parsed by TatSu. :param m_staffdef: The LMEI <staffDef> on which to set the time signature. :type m_staffdef: :class:`lxml.etree.Element` :returns: ``None`` ''' check(l_time['ly_type'] == 'time', 'did not receive a time specification') m_staffdef.set('meter.count', l_time['count']) m_staffdef.set('meter.unit', l_time['unit'])
[docs]@log.wrap('debug', 'set key signature') def set_key(l_key, m_staffdef, context=None): ''' Set the key signature for a staff. :param dict l_key: The key specification as parsed by TatSu. :param m_staffdef: The LMEI <staffDef> on which to set the key signature. :type m_staffdef: :class:`lxml.etree.Element` :returns: ``None`` ''' check(l_key['ly_type'] == 'key', 'did not receive a key specification') keynote = l_key['keynote'] language = 'nederlands' if context and 'language' in context: language = context['language'] pitch_and_accidental = lilypond_utils.parse_pitch_name(keynote, language) keynote = "".join(pitch_and_accidental) m_staffdef.set('key.sig', _KEY_MAPPING[l_key['mode']][keynote])
[docs]@log.wrap('debug', 'set instrument name') def set_instrument_name(l_name, m_staffdef, context=None): """ Set the instrument name for a staff. :param dict l_name: The instrument name as parsed by TatSu. :param m_staffdef: The LMEI <staffDef> on which to set the instrument name. :type m_staffdef: :class:`lxml.etree.Element` :returns: ``None`` """ check(l_name['ly_type'] == 'instr_name', 'did not receive an instrument name') m_staffdef.set('label', l_name['name'])
[docs]@log.wrap('info', 'postprocess staff') def postprocess_staff(m_staff): ''' Fixes @n in <staffDef> elements so it matches the containing <staff>. :param m_staff: The LMEI <staff>. ''' staff_number = m_staff.get('n') for m_staffdef in m_staff.iterfind('.//{}'.format(mei.STAFF_DEF)): m_staffdef.set('n', staff_number)
[docs]@log.wrap('info', 'convert staff', 'action') def do_staff(l_staff, m_section, m_staffdef, context=None, action=None): ''' :param dict l_staff: The LilyPond Staff context from TatSu. :param m_section: The LMEI <section> that will hold the staff. :type m_section: :class:`lxml.etree.Element` :param m_staffdef: The LMEI <staffDef> used to define this staff. :type m_staffdef: :class:`lxml.etree.Element` :returns: ``None`` :raises: :exc:`exceptions.LilyPondError` when the ``l_staff`` argument is not a staff. :raises: :exc:`exceptions.LilyPondError` when the ``m_staffdef`` argument does not have an @n attribute. .. note:: This function assumes that the ``m_staffdef`` argument is already present in the score. That is, the <staffDef> does not become a child element of the <staff> in this function. If the Staff context contains unknown staff settings, :func:`do_staff` emits a failure log message and continues processing the following settings and Voices in the Staff context. The @n attribute is taken from the ``m_staffdef`` argument. If the @n attribute is missing, :func:`do_staff` raises :exc:`exceptions.LilyPondError`. This function will use a single <staff> element whenever possible. However, each change between monophonic and polyphonic notation produces a new <staff> element. The following example shows a section with two staves; the second staff is split between two <staff> elements for technical reasons, but the @n attribute indicates they should be displayed as the same staff. .. sourcecode:: xml <section> <staff n="1"><!-- some content --></staff> <staff n="2"><!-- some monophonic content --></staff> <staff n="2"><!-- some polyphonic content --></staff> </section> ''' check(l_staff['ly_type'] == 'staff', 'did not receive a staff') check(m_staffdef.get('n') is not None, '<staffDef> is missing @n') # These initial settings -- treble clef, 4/4 time -- are the defaults in LilyPond, # but not necessarily in MEI. set_clef({'ly_type': 'clef', 'type': 'treble'}, m_staffdef, context=context) set_time({'ly_type': 'time', 'count': '4', 'unit': '4'}, m_staffdef, context=context) for l_setting in l_staff['initial_settings']: with log.debug('handle staff setting') as action: if l_setting['ly_type'] in _STAFF_SETTINGS_FUNCTIONS: _STAFF_SETTINGS_FUNCTIONS[l_setting['ly_type']]( l_setting, m_staffdef, context=context) action.success('converted {ly_type}', ly_type=l_setting['ly_type']) else: action.failure('unknown staff setting: {ly_type}', ly_type=l_setting['ly_type']) for i, l_each_staff in enumerate(l_staff['content']): with log.debug('convert staff @n={staff_n}:{i}', staff_n=m_staffdef.get('n'), i=i): m_each_staff = etree.SubElement(m_section, mei.STAFF, {'n': m_staffdef.get('n')}) for layer_n, l_layer in enumerate(l_each_staff['layers']): # we must add 1 to layer_n or else the @n would start at 0, not 1 do_layer(l_layer, m_each_staff, layer_n + 1, m_staffdef=m_staffdef, context=context) postprocess_staff(m_each_staff)
[docs]def note_pitch(m_note): ''' Return a tuple uniquely identifying the pitch of an LMEI node. ''' m_accid = m_note[0] return (m_note.get('oct'), m_note.get('pname'), m_accid.get('accid.ges', 'n'))
def _show_accidental(m_note): ''' Modify the <note> and <accid> elements to force the display of an accidental. ''' m_accid = m_note[0] accid = m_accid.get('accid.ges', 'n') m_accid.attrib['accid'] = accid if m_accid.get('accid.force') == '?': m_accid.attrib['func'] = 'caution' m_accid.attrib.pop('accid.ges', None) m_accid.attrib.pop('accid.force', None) def _hide_accidental(m_note): ''' Modify the <note> and <accid> elements to hide the accidental, but store the semantics in the note's @accid.ges. ''' m_accid = m_note[0] accid_ges = m_accid.attrib.pop('accid.ges', None) if accid_ges is not None: m_note.attrib['accid.ges'] = accid_ges m_note.remove(m_accid) def _render_accidental(m_note, accidentals, key_signature): ''' Decide whether to display or hide a note's accidental based on context. :param m_note: The LMEI <note> object to modify. It must contain an <accid> as its first child. :type m_note: :class:`lxml.etree.Element` :param dict accidentals: A map from tuples of the form (octave, pitch_name) to accidental values like 's' or 'ff', representing the current set of persistent accidentals in this measure but not including accidentals that are tied in from a previous measure. :param dict key_signature: A map from pitch names to accidental values like 's' or 'ff', representing the accidental settings specified by the current key signature. :returns: ``None`` ''' # Get some basic properties on the note pitch. pitch = note_pitch(m_note) pitch_name = pitch[1] pitch_name_and_octave = (pitch[0], pitch[1]) accidental = pitch[2] # The accidental value that the key signature sets as default. key_signature_accidental = key_signature[pitch_name] # The accidental MEI element. m_accid = m_note[0] # If this is set to true, then the next note on this same staff line/space will be forced to # display an accidental. force_next = False # Always display an accidental if it is forced or cautionary. if m_accid.get('accid.force'): _show_accidental(m_note) # If we are in the middle or end of a tie, hide the accidental. elif m_note.get('tie') in ('m', 't'): _hide_accidental(m_note) # Hoo boy. If we're at the end of a tie, we check to see whether there is a discrepancy # between our accidental and the current accidental setting in this measure. If so, that # means that accidental in the tie we're ending has persisted from a previous measure, and # has gone stale. So we set the accidental to a nonsense value so that the accidental for # the next note in this measure needs to be disambiguated. if (m_note.get('tie') == 't' and accidental != key_signature_accidental and pitch_name_and_octave not in accidentals): force_next = True # If the accidental does not match the current state of the accidentals in this measure # and key signature, then display it. elif accidentals.get(pitch_name_and_octave, key_signature_accidental) != accidental: _show_accidental(m_note) # Otherwise, hide it. else: _hide_accidental(m_note) if force_next: accidentals[pitch_name_and_octave] = "*** FORCE ***" else: accidentals[pitch_name_and_octave] = accidental
[docs]def fix_accidentals_in_layer(m_layer, m_staffdef): ''' Using a model of LilyPond's accidental rendering, fix the @accid/@accid.ges attributes and the temporary @accid.force attributes. Limitations: doesn't support key and time changes. Two different accidentals on the same note in the same chord may not be rendered correctly. :param m_layer: The LMEI <layer> object to fix. :type m_layer: :class:`lxml.etree.Element` :param m_staffdef: The initial <staffDef> settings for the containing staff. This is necessary to propoerly handle initial settings of key signatures and time signatures. :type m_layer: :class:`lxml.etree.Element` :returns: ``None`` ''' if m_staffdef is None: m_staffdef = {} key_signature = music_utils.KEY_SIGNATURES[m_staffdef.get("key.sig", "0")] accidentals = {} measure_length = music_utils.measure_duration(m_staffdef) phase = 0 for m_node in m_layer: # For all elements that occupy time, add their duration to the phase. if m_node.get('dur'): # If we have spilled over to a new measure, fix the phase, and reset the accidentals. if phase >= measure_length: accidentals = {} phase = phase % measure_length phase += music_utils.duration(m_node) if m_node.tag == mei.NOTE: _render_accidental(m_node, accidentals, key_signature) elif m_node.tag == mei.CHORD: for m_note in m_node: _render_accidental(m_note, accidentals, key_signature)
@log.wrap('debug', 'remove unterminated tie', 'action') def _maybe_remove_unterminated_tie(note, target_pitches, action): ''' :param note: The note to inspect. :param target_pitches: A set of pitches that the note could possibly tie to. If the given note tries to start a tie, then check to see if the tie is terminated by any of the target pitches or not. If it's unterminated, remove it. If it's terminated, make sure it is set to initial. ''' if note.get('tie') in ('i', 'm'): pitch = note_pitch(note) if pitch not in target_pitches: action.failure('unterminated tie') del note.attrib['tie'] else: note.attrib['tie'] = 'i' @log.wrap('debug', 'set medial or final tie attribute') def _fix_tie_target(note, pitch_map): ''' :param note: The note that might begin a tie. :param dict pitch_map: A dict mapping pitches to notes. If the note begins or continues a tie, find the note that it is tied to and set its @tie to either 'm' or 't'. ''' if note.get('tie') in ('i', 'm'): pitch = note_pitch(note) target_node = pitch_map[pitch] if target_node.get('tie') in ('i', 'm'): target_node.attrib['tie'] = 'm' else: target_node.attrib['tie'] = 't'
[docs]@log.wrap('debug', 'fix ties', 'action') def fix_ties_in_layer(m_layer, action): ''' Fix @tie attribute values in an LMEI <layer> element. ''' # In the first pass, we remove all unterminated ties and set all ties to initial. for i in range(len(m_layer)): node = m_layer[i] # next_pitches is the set of pitches that can be tied to in the next node. # If the next node is a rest, spacer, etc. then next_pitches is empty, # meaning that all ties from the current node are unterminated. next_pitches # is also empty on the last node in the layer, since it can't tie to anything. next_pitches = frozenset() if i != len(m_layer) - 1: next_node = m_layer[i + 1] if next_node.tag == mei.NOTE: next_pitches = frozenset([note_pitch(next_node)]) elif next_node.tag == mei.CHORD: next_pitches = frozenset([note_pitch(note) for note in next_node]) if node.tag == mei.NOTE: _maybe_remove_unterminated_tie(node, next_pitches) elif node.tag == mei.CHORD: for note in node: _maybe_remove_unterminated_tie(note, next_pitches) # In the second pass, we change some initial @ties to medial @ties, # and add some final @ties as necessary. for i in range(len(m_layer) - 1): node = m_layer[i] next_node = m_layer[i + 1] # In the first pass, we used a set of pitches. But this time, we need to # be able to get the node corresponding to each pitch, so we use a dict # mapping pitches to the next node. # This doesn't account for duplicate notes in a chord (e.g. <c c g>). # Whatever, I don't even know if MEI supports that. pitch_map = {} if next_node.tag == mei.NOTE: pitch_map[note_pitch(next_node)] = next_node elif next_node.tag == mei.CHORD: for note in next_node: pitch_map[note_pitch(note)] = note if node.tag == mei.NOTE: _fix_tie_target(node, pitch_map) elif node.tag == mei.CHORD: for note in node: _fix_tie_target(note, pitch_map)
[docs]@log.wrap('debug', 'fix slurs', 'action') def fix_slurs_in_layer(m_layer, action): ''' Fix @slur attribute values in an LMEI <layer> element. ''' current_slur = False for node in m_layer: if node.tag in (mei.NOTE, mei.CHORD): slur_attrib = node.get('slur') if current_slur: # If there is an ongoing tie, set all @ties to medial # until a terminal @tie shows up. if slur_attrib == 't1': current_slur = False else: node.attrib['slur'] = 'm1' else: # If there is no ongoing tie, unset all @ties until # an initial @tie shows up. if slur_attrib == 'i1': current_slur = True elif 'slur' in node.attrib: del node.attrib['slur']
[docs]@log.wrap('debug', 'convert voice/layer', 'action') def do_layer(l_layer, m_staff, layer_n, m_staffdef=None, context=None, action=None): ''' Convert a LilyPond Voice context into an LMEI <layer> element. :param l_layer: The LilyPond Voice context from TatSu. :type l_layer: list of dict :param m_staff: The MEI <staff> that will hold the layer. :type m_staff: :class:`lxml.etree.Element` :param int layer_n: The @n attribute value for this <layer>. :param m_staffdef: The initial <staffDef> for the containing staff. This is necessary to correctly handle key and time signatures. :returns: The new <layer> element. :rtype: :class:`lxml.etree.Element` If the Voice context contains an unknown node type, :func:`do_layer` emits a failure log message and continues processing the following nodes in the Voice context. ''' m_layer = etree.SubElement(m_staff, mei.LAYER, {'n': str(layer_n)}) node_converters = { 'chord': do_chord, 'note': do_note, 'rest': do_rest, 'measure_rest': do_measure_rest, 'spacer': do_spacer, } for obj in l_layer: with log.debug('node conversion') as action: if obj['ly_type'] in node_converters: node_converters[obj['ly_type']](obj, m_layer, context=context) action.success('converted {ly_type}', ly_type=obj['ly_type']) elif obj['ly_type'] in _STAFF_SETTINGS_FUNCTIONS: m_staffdef = etree.SubElement(m_layer, mei.STAFF_DEF) _STAFF_SETTINGS_FUNCTIONS[obj['ly_type']](obj, m_staffdef) # The <staffDef> has no "n" attribute, which we will fix in do_staff. else: action.failure('unknown node type: {ly_type}', ly_type=obj['ly_type']) fix_ties_in_layer(m_layer) fix_slurs_in_layer(m_layer) fix_accidentals_in_layer(m_layer, m_staffdef) music_utils.autobeam(m_layer, m_staffdef) return m_layer
[docs]@log.wrap('debug', 'process pitch') def process_pitch(l_note, note_attrib, accid_attrib, context=None): ''' Set the pitch of an LMEI note. :param l_note: The LilyPond note as provided by TatSu. This function reads the keys 'pitch_name' and 'accid_force'. :type l_note: dict :param dict note_attrib: The attributes for the MEI <note/> element *before* creation. :param dict accid_attrib: The attributes for the MEI <accid/> element *before* creation. :returns A tuple of the new note_attrib and accid_attrib. ''' language = 'nederlands' if context and 'language' in context: language = context['language'] pname, accid = lilypond_utils.parse_pitch_name(l_note['pitch_name'], language) note_attrib['pname'] = pname if accid: accid_attrib['accid.ges'] = accid if l_note.get('accid_force'): # This attribute is not valid MEI or LMEI, but it will be fixed in a post-processing step. accid_attrib['accid.force'] = l_note['accid_force'] return (note_attrib, accid_attrib)
[docs]@log.wrap('debug', 'add octave', 'action') def process_octave(l_oct, action): ''' Convert an octave specifier from the parsed LilyPond into an MEI @oct attribute. :param str l_oct: The "octave" part of the LilyPond note as provided by TatSu. May also be ``None``. :returns: The LMEI octave number. :rtype: str If the octave is not recognized, :func:`process_octave` emits a failure log message and returns the same value as if ``l_oct`` were ``None``. ''' if l_oct in _OCTAVE_MAPPING: return _OCTAVE_MAPPING[l_oct] else: action.failure('unknown octave: {octave}', octave=l_oct) return _OCTAVE_MAPPING[None]
[docs]@log.wrap('debug', 'process dots', 'action') def process_dots(l_node, attrib, action): """ Handle the @dots attribute for a chord, note, rest, or spacer rest. :param dict l_node: The LilyPond node from TatSu. :param dict attrib: The attribute dictionary that will be given to the :class:`Element` constructor. :returns: The ``attrib`` dictionary. Converts the "dots" member of ``l_node`` to the appropriate number in ``attrib``. If there is no "dots" member in ``l_node``, submit a "failure" log message and assume there are no dots. """ if 'dots' in l_node: if l_node['dots']: attrib['dots'] = str(len(l_node['dots'])) else: action.failure("missing 'dots' in the LilyPond node") return attrib
[docs]@log.wrap('debug', 'has tie') def has_tie(l_thing): ''' Given a parsed LilyPond note/chord, determine whether it has a tie. ''' post_events = l_thing.get('post_events', []) return any([post_event.get('ly_type') == 'tie' for post_event in post_events])
[docs]@log.wrap('debug', 'add tie') def add_tie(attrib): ''' Given an attribute dictionary for an LMEI element, add a tie to it. All tie attributes are initial at this stage of conversion. Medial and final tie attributes are fixed at the layer level. ''' attrib['tie'] = 'i'
[docs]@log.wrap('debug', 'process slur') def process_slur(l_thing, attrib): ''' Handle the @slur attribute for LilyPond nodes. Only emits initial and final slur attributes (medial is handled later), and does not process overlapping slurs. ''' post_events = l_thing.get('post_events', []) # Find the first slur post event for post_event in post_events: if post_event.get('ly_type') == 'slur': slur = post_event.get('slur') if slur == '(': attrib['slur'] = 'i1' break elif slur == ')': attrib['slur'] = 't1' break
[docs]@log.wrap('debug', 'convert chord', 'action') def do_chord(l_chord, m_layer, context=None, action=None): """ Convert a LilyPond chord to an LMEI <chord/>. :param dict l_chord: The LilyPond chord from TatSu. :param m_layer: The MEI <layer> that will hold the chord. :type m_layer: :class:`lxml.etree.Element` :returns: The new <chord/> element. :rtype: :class:`lxml.etree.Element` :raises: :exc:`exceptions.LilyPondError` if ``l_chord`` does not contain a TatSu chord """ check(l_chord['ly_type'] == 'chord', 'did not receive a chord') attrib = {'dur': l_chord['dur']} process_dots(l_chord, attrib) process_slur(l_chord, attrib) chord_has_tie = has_tie(l_chord) m_chord = etree.SubElement(m_layer, mei.CHORD, attrib) for l_note in l_chord['notes']: do_note(l_note, m_chord, context=context) return m_chord
[docs]@log.wrap('debug', 'convert note', 'action') def do_note(l_note, m_parent, context=None, action=None): """ Convert a LilyPond note to an LMEI <note/>. :param dict l_note: The LilyPond note from TatSu. :param m_parent: The parent element that will hold the note, either a <layer/> or a <chord/>. :type m_parent: :class:`lxml.etree.Element` :returns: The new <note/> element. :rtype: :class:`lxml.etree.Element` :raises: :exc:`exceptions.LilyPondError` if ``l_note`` does not contain a TatSu note """ check(l_note['ly_type'] == 'note', 'did not receive a note') attrib = {} accid_attrib = {} # This function is used for both notes in chords and standalone notes, so @dur may or may not # be present. if 'dur' in l_note: attrib['dur'] = l_note['dur'] process_pitch(l_note, attrib, accid_attrib, context=context) attrib['oct'] = process_octave(l_note['oct']) process_dots(l_note, attrib) if has_tie(l_note): add_tie(attrib) process_slur(l_note, attrib) m_note = etree.SubElement(m_parent, mei.NOTE, attrib) # NOTE: At least in this stage of conversion, <accid/> must always exist, and it must always be # the first child of the <note>. That way a note's accidental element can be retrieved as # m_note[0] rather than trying to search for the <accid/> element every time we need it. This # probably gives some efficiency savings, and more importantly, it's also easier to program. m_accid = etree.SubElement(m_note, mei.ACCID, accid_attrib) return m_note
[docs]@log.wrap('debug', 'convert rest', 'action') def do_rest(l_rest, m_layer, context=None, action=None): """ Convert a LilyPond rest to an LMEI <rest/>. :param dict l_rest: The LilyPond rest from TatSu. :param m_layer: The LMEI <layer> that will hold the rest. :type m_layer: :class:`lxml.etree.Element` :returns: The new <rest/> element. :rtype: :class:`lxml.etree.Element` :raises: :exc:`exceptions.LilyPondError` if ``l_rest`` does not contain a TatSu rest """ check(l_rest['ly_type'] == 'rest', 'did not receive a rest') attrib = { 'dur': l_rest['dur'], } process_dots(l_rest, attrib) m_rest = etree.SubElement(m_layer, mei.REST, attrib) return m_rest
@log.wrap('debug', 'convert measure rest') def do_measure_rest(l_measure_rest, m_layer, context=None): check(l_measure_rest['ly_type'] == 'measure_rest', 'did not receive a measure rest') attrib = { 'dur': l_measure_rest['dur'], } process_dots(l_measure_rest, attrib) m_measure_rest = etree.SubElement(m_layer, mei.M_REST, attrib) return m_measure_rest
[docs]@log.wrap('debug', 'convert spacer', 'action') def do_spacer(l_spacer, m_layer, context=None, action=None): """ Convert a LilyPond spacer rest to an LMEI <space/>. :param dict l_spacer: The LilyPond spacer rest from TatSu. :param m_layer: The LMEI <layer> that will hold the space. :type m_layer: :class:`lxml.etree.Element` :returns: The new <space/> element. :rtype: :class:`lxml.etree.Element` :raises: :exc:`exceptions.LilyPondError` if ``l_spacer`` does not contain a TatSu spacer rest """ check(l_spacer['ly_type'] == 'spacer', 'did not receive a spacer rest') attrib = { 'dur': l_spacer['dur'], } process_dots(l_spacer, attrib) m_space = etree.SubElement(m_layer, mei.SPACE, attrib) return m_space
# this is at the bottom so the functions will already be defined _STAFF_SETTINGS_FUNCTIONS = { 'clef': set_clef, 'key': set_key, 'instr_name': set_instrument_name, 'time': set_time, }