# This file is part of python-ly, https://pypi.python.org/pypi/python-ly
#
# Copyright (c) 2008 - 2015 by Wilbert Berendsen
#
# 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 2
# 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
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# 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, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
# See http://www.gnu.org/licenses/ for more information.
"""
Using the tree structure from ly.music to initiate the conversion to MusicXML.
Uses functions similar to ly.music.items.Document.iter_music() to iter through
the node tree. But information about where a node branch ends
is also added. During the iteration the information needed for the conversion
is captured.
"""
from __future__ import unicode_literals
from __future__ import print_function
import ly.document
import ly.music
from . import create_musicxml
from . import ly2xml_mediator
from . import xml_objs
#excluded from parsing
excl_list = ['Version', 'Midi', 'Layout']
# Defining contexts in relation to musicXML
group_contexts = ['StaffGroup', 'ChoirStaff']
pno_contexts = ['PianoStaff', 'GrandStaff']
staff_contexts = ['Staff', 'RhythmicStaff', 'TabStaff',
'DrumStaff', 'VaticanaStaff', 'MensuralStaff']
part_contexts = pno_contexts + staff_contexts
[docs]class End():
""" Extra class that gives information about the end of Container
elements in the node list. """
def __init__(self, node):
self.node = node
def __repr__(self):
return '<{0} {1}>'.format(self.__class__.__name__, self.node)
[docs]class ParseSource():
""" creates the XML-file from the source code according to the Music XML standard """
def __init__(self):
self.musxml = create_musicxml.CreateMusicXML()
self.mediator = ly2xml_mediator.Mediator()
self.relative = False
self.tuplet = []
self.scale = ''
self.grace_seq = False
self.trem_rep = 0
self.piano_staff = 0
self.numericTime = False
self.voice_sep = False
self.sims_and_seqs = []
self.override_dict = {}
self.ottava = False
self.with_contxt = None
self.schm_assignm = None
self.tempo = ()
self.tremolo = False
self.tupl_span = False
self.unset_tuplspan = False
self.alt_mode = None
self.rel_pitch_isset = False
self.slurcount = 0
self.slurnr = 0
self.phrslurnr = 0
self.mark = False
self.pickup = False
[docs] 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
self.parse_document(doc)
[docs] 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.
doc = ly_doc.copy()
import ly.pitch.rel2abs
cursor = ly.document.Cursor(doc)
ly.pitch.rel2abs.rel2abs(cursor, first_pitch_absolute=relative_first_pitch_absolute)
mustree = ly.music.document(doc)
self.parse_tree(mustree)
[docs] def parse_tree(self, mustree):
"""Parse the LilyPond source as a ly.music node tree."""
# print(mustree.dump())
header_nodes = self.iter_header(mustree)
if header_nodes:
self.parse_nodes(header_nodes)
score = self.get_score(mustree)
if score:
mus_nodes = self.iter_score(score, mustree)
else:
mus_nodes = self.find_score_sub(mustree)
self.mediator.new_section("fallback") #fallback/default section
self.parse_nodes(mus_nodes)
[docs] 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
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)
pass
else:
print("Warning! Couldn't parse source!")
[docs] def musicxml(self, prettyprint=True):
self.mediator.check_score()
xml_objs.IterateXmlObjs(
self.mediator.score, self.musxml, self.mediator.divisions)
xml = self.musxml.musicxml(prettyprint)
return xml
##
# The different source types from ly.music are here sent to translation.
##
[docs] def Assignment(self, a):
"""
Variables should already have been substituted
so this need only cover other types of assignments.
"""
if isinstance(a.value(), ly.music.items.Markup):
val = a.value().plaintext()
elif isinstance(a.value(), ly.music.items.String):
val = a.value().value()
elif isinstance(a.value(), ly.music.items.Scheme):
val = a.value().get_string()
if not val:
self.schm_assignm = a.name()
elif isinstance(a.value(), ly.music.items.UserCommand):
# Don't know what to do with this:
return
if self.look_behind(a, ly.music.items.With):
if self.with_contxt in group_contexts:
self.mediator.set_by_property(a.name(), val, True)
else:
self.mediator.set_by_property(a.name(), val)
else:
self.mediator.new_header_assignment(a.name(), val)
[docs] def MusicList(self, musicList):
if musicList.token == '<<':
if self.look_ahead(musicList, ly.music.items.VoiceSeparator):
self.mediator.new_snippet('sim-snip')
self.voice_sep = True
else:
self.mediator.new_section('simultan')
self.sims_and_seqs.append('sim')
elif musicList.token == '{':
self.sims_and_seqs.append('seq')
[docs] def Chord(self, chord):
self.mediator.clear_chord()
[docs] def Q(self, q):
self.mediator.copy_prev_chord(q.duration)
[docs] def Context(self, context):
r""" \context """
self.in_context = True
self.check_context(context.context(), context.context_id(), context.token)
[docs] def check_context(self, context, context_id=None, token=""):
"""Check context and do appropriate action (e.g. create new part)."""
# Check first if part already exists
if context_id:
match = self.mediator.get_part_by_id(context_id)
if match:
self.mediator.new_part(to_part=match)
return
if context in pno_contexts:
self.mediator.new_part(context_id, piano=True)
self.piano_staff = 1
elif context in group_contexts:
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.new_section('piano-staff'+str(self.piano_staff))
self.mediator.set_staffnr(self.piano_staff)
self.piano_staff += 1
else:
if token != '\\context' or self.mediator.part_not_empty():
self.mediator.new_part(context_id)
self.mediator.add_staff_id(context_id)
elif context == 'Voice':
self.sims_and_seqs.append('voice')
if context_id:
self.mediator.new_section(context_id)
else:
self.mediator.new_section('voice')
elif context == 'Devnull':
self.mediator.new_section('devnull', True)
else:
print("Context not implemented:", context)
[docs] def VoiceSeparator(self, voice_sep):
self.mediator.new_snippet('sim')
self.mediator.set_voicenr(add=True)
[docs] def Change(self, change):
r""" A \change music expression. Changes the staff number. """
if change.context() == 'Staff':
self.mediator.set_staffnr(0, staff_id=change.context_id())
[docs] def PipeSymbol(self, barcheck):
""" PipeSymbol = | """
pass
[docs] def Clef(self, clef):
r""" Clef \clef"""
self.mediator.new_clef(clef.specifier())
[docs] def KeySignature(self, key):
self.mediator.new_key(key.pitch().output(), key.mode())
[docs] def Relative(self, relative):
r"""A \relative music expression."""
self.relative = True
[docs] def Partial(self, partial):
self.pickup = True
[docs] 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
else:
self.mediator.new_note(note, self.relative)
self.check_note(note)
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)
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()
[docs] def Unpitched(self, unpitched):
"""A note without pitch, just a standalone duration."""
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)
[docs] def DrumNote(self, drumnote):
"""A note in DrumMode."""
if drumnote.length():
self.mediator.new_note(drumnote, is_unpitched=True)
self.check_note(drumnote)
[docs] def check_note(self, note):
"""Generic check for all notes, both pitched and unpitched."""
self.check_tuplet()
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)
[docs] def check_tuplet(self):
"""Generic tuplet check."""
if self.tuplet:
tlevels = len(self.tuplet)
nested = True if tlevels > 1 else False
for td in self.tuplet:
if nested:
self.mediator.change_to_tuplet(td['fraction'], td['ttype'],
td['nr'], td['length'])
else:
self.mediator.change_to_tuplet(td['fraction'], td['ttype'],
td['nr'])
td['ttype'] = ""
self.mediator.check_divs()
[docs] def Duration(self, duration):
"""A written duration"""
if self.tempo:
self.mediator.new_tempo(duration.token, duration.tokens, *self.tempo)
self.tempo = ()
elif self.tremolo:
self.mediator.set_tremolo(duration=int(duration.token))
self.tremolo = False
elif self.tupl_span:
self.mediator.set_tuplspan_dur(duration.token, duration.tokens)
self.tupl_span = False
elif self.pickup:
self.mediator.set_pickup()
self.pickup = 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)
[docs] def Tempo(self, tempo):
""" Tempo direction, e g '4 = 80' """
if self.look_ahead(tempo, ly.music.items.Duration):
self.tempo = (tempo.tempo(), tempo.text())
else:
self.mediator.new_tempo(0, (), tempo.tempo(), tempo.text())
[docs] def Tie(self, tie):
""" tie ~ """
self.mediator.tie_to_next()
[docs] def Rest(self, rest):
r""" rest, r or R. Note: NOT by command, i.e. \rest """
if rest.token == 'R':
self.scale = 'R'
self.mediator.new_rest(rest)
[docs] def Skip(self, skip):
r""" invisible rest/spacer rest (s or command \skip)"""
if 'lyrics' in self.sims_and_seqs:
self.mediator.new_lyrics_item(skip.token)
else:
self.mediator.new_rest(skip)
[docs] def Scaler(self, scaler):
r"""
\times \tuplet \scaleDurations
"""
if scaler.token == '\\scaleDurations':
ttype = ""
fraction = (scaler.denominator, scaler.numerator)
elif scaler.token == '\\times':
ttype = "start"
fraction = (scaler.denominator, scaler.numerator)
elif scaler.token == '\\tuplet':
ttype = "start"
fraction = (scaler.numerator, scaler.denominator)
nr = len(self.tuplet) + 1
self.tuplet.append({'set': False,
'fraction': fraction,
'ttype': ttype,
'length': scaler.length(),
'nr': nr})
if self.look_ahead(scaler, ly.music.items.Duration):
self.tupl_span = True
self.unset_tuplspan = True
[docs] def Number(self, number):
pass
[docs] def Articulation(self, art):
"""An articulation, fingering, string number, or other symbol."""
self.mediator.new_articulation(art.token)
[docs] def Postfix(self, postfix):
pass
[docs] def Beam(self, beam):
pass
[docs] def Slur(self, slur):
""" Slur, '(' = start, ')' = stop. """
if slur.token == '(':
self.slurcount += 1
self.slurnr = self.slurcount
self.mediator.set_slur(self.slurnr, "start")
elif slur.token == ')':
self.mediator.set_slur(self.slurnr, "stop")
self.slurcount -= 1
[docs] def PhrasingSlur(self, phrslur):
r"""A \( or \)."""
if phrslur.token == '\(':
self.slurcount += 1
self.phrslurnr = self.slurcount
self.mediator.set_slur(self.phrslurnr, "start", phrasing=True)
elif phrslur.token == '\)':
self.mediator.set_slur(self.phrslurnr, "stop", phrasing=True)
self.slurcount -= 1
[docs] def Dynamic(self, dynamic):
"""Any dynamic symbol."""
self.mediator.new_dynamics(dynamic.token[1:])
[docs] def Grace(self, grace):
self.grace_seq = True
[docs] def TimeSignature(self, timeSign):
self.mediator.new_time(timeSign.numerator(), timeSign.fraction(), self.numericTime)
[docs] def Repeat(self, repeat):
if repeat.specifier() == 'volta':
self.mediator.new_repeat('forward')
elif repeat.specifier() == 'tremolo':
self.trem_rep = repeat.repeat_count()
[docs] def Tremolo(self, tremolo):
"""A tremolo item ":"."""
if self.look_ahead(tremolo, ly.music.items.Duration):
self.tremolo = True
else:
self.mediator.set_tremolo()
[docs] def With(self, cont_with):
r"""A \with ... construct."""
self.with_contxt = cont_with.parent().context()
[docs] def Set(self, cont_set):
r"""A \set command."""
if isinstance(cont_set.value(), ly.music.items.Scheme):
if cont_set.property() == 'tupletSpannerDuration':
moment = cont_set.value().get_ly_make_moment()
if moment:
self.mediator.set_tuplspan_dur(fraction=moment)
else:
self.mediator.unset_tuplspan_dur()
return
val = cont_set.value().get_string()
else:
val = cont_set.value().value()
if cont_set.context() in part_contexts:
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)
[docs] def Command(self, command):
r""" \bar, \rest etc """
excls = ['\\major', '\\minor', '\\dorian', '\\bar']
if command.token == '\\rest':
self.mediator.note2rest()
elif command.token == '\\numericTimeSignature':
self.numericTime = True
elif command.token == '\\defaultTimeSignature':
self.numericTime = False
elif command.token.find('voice') == 1:
self.mediator.set_voicenr(command.token[1:], piano=self.piano_staff)
elif command.token == '\\glissando':
try:
self.mediator.new_gliss(self.override_dict["Glissando.style"])
except KeyError:
self.mediator.new_gliss()
elif command.token == '\\startTrillSpan':
self.mediator.new_trill_spanner()
elif command.token == '\\stopTrillSpan':
self.mediator.new_trill_spanner("stop")
elif command.token == '\\ottava':
self.ottava = True
elif command.token == '\\mark':
self.mark = True
self.mediator.new_mark()
elif command.token == '\\breathe':
art = type('',(object,),{"token": "\\breathe"})()
self.Articulation(art)
elif command.token == '\\stemUp' or command.token == '\\stemDown' or command.token == '\\stemNeutral':
self.mediator.stem_direction(command.token)
elif command.token == '\\default':
if self.tupl_span:
self.mediator.unset_tuplspan_dur()
self.tupl_span = False
elif self.mark:
self.mark = False
elif command.token == '\\compressFullBarRests':
self.mediator.set_mult_rest()
elif command.token == '\\break':
self.mediator.add_break()
else:
if command.token not in excls:
print("Unknown command:", command.token)
[docs] def UserCommand(self, usercommand):
"""Music variables are substituted so this must be something else."""
if usercommand.name() == 'tupletSpan':
self.tupl_span = True
[docs] def Markup(self, markup):
pass
[docs] def MarkupWord(self, markupWord):
self.mediator.new_word(markupWord.token)
[docs] def MarkupList(self, markuplist):
pass
[docs] def String(self, string):
prev = self.get_previous_node(string)
if prev and prev.token == '\\bar':
self.mediator.create_barline(string.value())
[docs] def LyricsTo(self, lyrics_to):
r"""A \lyricsto expression. """
self.mediator.new_lyric_section('lyricsto'+lyrics_to.context_id(), lyrics_to.context_id())
self.sims_and_seqs.append('lyrics')
[docs] def LyricText(self, lyrics_text):
"""A lyric text (word, markup or string), with a Duration."""
self.mediator.new_lyrics_text(lyrics_text.token)
[docs] def LyricItem(self, lyrics_item):
"""Another lyric item (skip, extender, hyphen or tie)."""
self.mediator.new_lyrics_item(lyrics_item.token)
[docs] def NoteMode(self, notemode):
r"""A \notemode or \notes expression."""
self.alt_mode = 'note'
[docs] def ChordMode(self, chordmode):
r"""A \chordmode or \chords expression."""
self.alt_mode = 'chord'
[docs] def DrumMode(self, drummode):
r"""A \drummode or \drums expression.
If the shorthand form \drums is found, DrumStaff is implicit.
"""
if drummode.token == '\\drums':
self.check_context('DrumStaff')
self.alt_mode = 'drum'
[docs] def LyricMode(self, lyricmode):
r"""A \lyricmode, \lyrics or \addlyrics expression."""
self.alt_mode = 'lyric'
[docs] def Override(self, override):
r"""An \override command."""
self.override_key = ''
[docs] def PathItem(self, item):
r"""An item in the path of an \override or \revert command."""
self.override_key += item.token
[docs] def Scheme(self, scheme):
"""A Scheme expression inside LilyPond."""
pass
[docs] def SchemeItem(self, item):
"""Any scheme token."""
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.schm_assignm:
self.mediator.set_by_property(self.schm_assignm, item.token)
elif self.mark:
self.mediator.new_mark(int(item.token))
else:
print("SchemeItem not implemented:", item.token)
[docs] def SchemeQuote(self, quote):
"""A ' in scheme."""
pass
[docs] def End(self, end):
if isinstance(end.node, ly.music.items.Scaler):
if self.unset_tuplspan:
self.mediator.unset_tuplspan_dur()
self.unset_tuplspan = False
if end.node.token != '\\scaleDurations':
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
self.grace_seq = False
elif end.node.token == '\\repeat':
if end.node.specifier() == 'volta':
self.mediator.new_repeat('backward')
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_rep = 0
elif isinstance(end.node, ly.music.items.Context):
self.in_context = False
if end.node.context() == 'Voice':
self.mediator.check_voices()
self.sims_and_seqs.pop()
elif end.node.context() in group_contexts:
self.mediator.close_group()
elif end.node.context() in staff_contexts:
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.mediator.set_voicenr(nr=1)
elif end.node.context() == 'Devnull':
self.mediator.check_voices()
elif end.node.token == '<<':
if self.voice_sep:
self.mediator.check_voices_by_nr()
self.mediator.revert_voicenr()
self.voice_sep = False
elif not self.piano_staff:
self.mediator.check_simultan()
if self.sims_and_seqs:
self.sims_and_seqs.pop()
elif end.node.token == '{':
if self.sims_and_seqs:
self.sims_and_seqs.pop()
elif end.node.token == '<': #chord
self.mediator.chord_end()
elif end.node.token == '\\lyricsto':
self.mediator.check_lyrics(end.node.context_id())
self.sims_and_seqs.pop()
elif end.node.token == '\\with':
self.with_contxt = None
elif end.node.token == '\\drums':
self.mediator.check_part()
elif isinstance(end.node, ly.music.items.Relative):
self.relative = False
self.rel_pitch_isset = False
else:
# print("end:", end.node.token)
pass
##
# Additional node manipulation
##
[docs] def get_previous_node(self, node):
""" Returns the nodes previous node
or false if the node is first in its branch. """
parent = node.parent()
i = parent.index(node)
if i > 0:
return parent[i-1]
else:
return False
[docs] def simple_node_gen(self, node):
"""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
[docs] def get_score(self, node):
""" Returns (first) Score node or false if no Score is found. """
for n in node:
if isinstance(n, ly.music.items.Score) or isinstance(n, ly.music.items.Book):
return n
return False
[docs] def iter_score(self, scorenode, doc):
r"""
Iter over score.
Similarly to items.Document.iter_music user commands are substituted.
Furthermore \repeat unfold expressions are unfolded.
"""
for s in scorenode:
if isinstance(s, ly.music.items.Repeat) and s.specifier() == 'unfold':
for u in self.unfold_repeat(s, s.repeat_count(), doc):
yield u
else:
n = doc.substitute_for_node(s) or s
yield n
for c in self.iter_score(n, doc):
yield c
if isinstance(s, ly.music.items.Container):
yield End(s)
[docs] def unfold_repeat(self, repeat_node, repeat_count, doc):
r"""
Iter over node which represent a \repeat unfold expression
and do the unfolding directly.
"""
for r in range(repeat_count):
for n in repeat_node:
for c in self.iter_score(n, doc):
yield c
[docs] def find_score_sub(self, doc):
"""Find substitute for scorenode. Takes first music node that isn't
an assignment."""
for n in doc:
if not isinstance(n, ly.music.items.Assignment):
if isinstance(n, ly.music.items.Music):
return self.iter_score(n, doc)
[docs] def look_ahead(self, node, find_node):
"""Looks ahead in a container node and returns True
if the search is successful."""
for n in node:
if isinstance(n, find_node):
return True
return False
[docs] def look_behind(self, node, find_node):
"""Looks behind on the parent node(s) and returns True
if the search is successful."""
parent = node.parent()
if parent:
if isinstance(parent, find_node):
ret = True
else:
ret = self.look_behind(parent, find_node)
return ret
else:
return False
##
# Other functions
##
[docs] def gen_med_caller(self, func_name, *args):
"""Call any function in the mediator object."""
func_call = getattr(self.mediator, func_name)
func_call(*args)