Source code for ly.musicxml.xml_objs

# 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.

"""
Classes that holds information about a musical score,
suitable for converting to musicXML.

When the score structure is built, it can easily be used to create a musicXML.

Example::

    from ly.musicxml import create_musicxml, xml_objs

    musxml = create_musicxml.CreateMusicXML()

    score = xml_objs.Score()
    part = xml_objs.ScorePart()
    score.partlist.append(part)
    bar = xml_objs.Bar()
    part.barlist.append(bar)
    ba = xml_objs.BarAttr()
    ba.set_time([4,4])
    bar.obj_list.append(ba)
    c = xml_objs.BarNote('c', 0, 0, (1,1))
    c.set_octave(4)
    c.set_durtype(1)
    bar.obj_list.append(c)

    xml_objs.IterateXmlObjs(score, musxml, 1)
    xml = musxml.musicxml()
    xml.write(filename)

"""

from __future__ import unicode_literals
from __future__ import print_function

from fractions import Fraction

[docs]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.""" # score.debug_score([]) self.musxml = musxml self.divisions = div if score.title: self.musxml.create_title(score.title) for ctag in score.creators: self.musxml.add_creator(ctag, score.creators[ctag]) for itag in score.info: self.musxml.create_score_info(itag, score.info[itag]) if score.rights: self.musxml.add_rights(score.rights) for p in score.partlist: if isinstance(p, ScorePart): self.iterate_part(p) elif isinstance(p, ScorePartGroup): self.iterate_partgroup(p)
[docs] def iterate_partgroup(self, group): """Loop through a group, recursively if nested.""" self.musxml.create_partgroup( 'start', group.num, group.name, group.abbr, group.bracket) for p in group.partlist: if isinstance(p, ScorePart): self.iterate_part(p) elif isinstance(p, ScorePartGroup): self.iterate_partgroup(p) self.musxml.create_partgroup('stop', group.num)
[docs] def iterate_part(self, part): """The part is iterated.""" if part.barlist: part.set_first_bar(self.divisions) self.musxml.create_part(part.name, part.abbr, part.midi) for bar in part.barlist: self.iterate_bar(bar) else: print("Warning: empty part:", part.name)
[docs] 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)
[docs] 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) elif obj.barline: self.musxml.add_barline(obj.barline) if obj.staves: self.musxml.add_staves(obj.staves) if obj.multiclef: for mc in obj.multiclef: self.musxml.add_clef(sign=mc[0][0], line=mc[0][1], nr=mc[1], oct_ch=mc[0][2]) if obj.tempo: self.musxml.create_tempo(obj.tempo.text, obj.tempo.metr, obj.tempo.midi, obj.tempo.dots)
[docs] def before_note(self, obj): """Xml-nodes before note.""" 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)
[docs] def after_note(self, obj): """Xml-nodes after note.""" self._add_dynamics([d for d in obj.dynamic if not d.before]) if obj.oct_shift and obj.oct_shift.octdir == 'stop': self.musxml.add_octave_shift(obj.oct_shift.plac, obj.oct_shift.octdir, obj.oct_shift.size)
def _add_dynamics(self, dyns): """Add XML nodes for list of Dynamics objects.""" for d in dyns: if isinstance(d, DynamicsMark): self.musxml.add_dynamic_mark(d.sign) elif isinstance(d, DynamicsWedge): self.musxml.add_dynamic_wedge(d.sign) elif isinstance(d, DynamicsText): self.musxml.add_dynamic_text(d.sign) elif isinstance(d, DynamicsDashes): self.musxml.add_dynamic_dashes(d.sign)
[docs] def gener_xml_mus(self, obj): """Nodes generic for both notes and rests.""" if obj.tuplet: 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)
[docs] def new_xml_note(self, obj): """Create note specific xml-nodes.""" 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) 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) for t in obj.tie: self.musxml.tie_note(t) for s in obj.slur: self.musxml.add_slur(s.nr, s.slurtype) for a in obj.artic: self.musxml.new_articulation(a) if obj.ornament: self.musxml.new_simple_ornament(obj.ornament) if obj.adv_ornament: self.musxml.new_adv_ornament(obj.adv_ornament[0], obj.adv_ornament[1]) if obj.tremolo[1]: self.musxml.add_tremolo(obj.tremolo[0], obj.tremolo[1]) if obj.gliss: self.musxml.add_gliss(obj.gliss[0], obj.gliss[1], obj.gliss[2]) if obj.fingering: self.musxml.add_fingering(obj.fingering) 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])
[docs] def new_xml_rest(self, obj): """Create rest specific xml-nodes.""" divdur = self.count_duration(obj.duration, self.divisions) if obj.skip: self.musxml.add_skip(divdur) else: self.musxml.new_rest(divdur, obj.type, obj.pos, obj.dot, obj.voice)
[docs] def count_duration(self, base_scaling, divs): base = base_scaling[0] scaling = base_scaling[1] duration = divs*4*base duration = duration * scaling return int(duration)
[docs]class Score(): """Object that keep track of a whole score.""" def __init__(self): self.partlist = [] self.title = None self.creators = {} self.info = {} self.rights = None self.glob_section = ScoreSection('global', True)
[docs] def is_empty(self): """Check if score is empty.""" if self.partlist: return False else: return True
[docs] def merge_globally(self, section, override=False): """Merge section to all parts.""" for p in self.partlist: p.merge_voice(section, override)
[docs] def debug_score(self, attr=[]): """ Loop through score and print all elements for debugging purposes. Additionally print element attributes by adding them to the argument 'attr' list. """ ind = " " def debug_part(p): print("Score part:"+p.name) for n, b in enumerate(p.barlist): print(ind+"Bar nr: "+str(n+1)) for obj in b.obj_list: print(ind+ind+repr(obj)) for a in attr: try: print(ind+ind+ind+a+':'+repr(getattr(obj, a))) except AttributeError: pass def debug_group(g): if hasattr(g, 'barlist'): debug_part(g) else: print("Score group:"+g.name) for pg in g.partlist: debug_group(pg) for i in self.partlist: debug_group(i)
[docs]class ScorePartGroup(): """Object to keep track of part group.""" def __init__(self, num, bracket): self.bracket = bracket self.partlist = [] self.name = '' self.abbr = '' self.parent = None self.num = num
[docs] def set_bracket(self, bracket): self.bracket = bracket
[docs] def merge_voice(self, voice, override=False): """Merge in a ScoreSection into all parts.""" for part in self.partlist: part.merge_voice(voice, override)
[docs]class ScoreSection(): """ object to keep track of music section """ def __init__(self, name, glob=False): self.name = name self.barlist = [] self.glob = glob def __repr__(self): return '<{0} {1}>'.format(self.__class__.__name__, self.name)
[docs] def merge_voice(self, voice, override=False): """Merge in other ScoreSection.""" for org_v, add_v in zip(self.barlist, voice.barlist): org_v.inject_voice(add_v, override) bl_len = len(self.barlist) if len(voice.barlist) > bl_len: self.barlist += voice.barlist[bl_len:]
[docs] def merge_lyrics(self, lyrics): """Merge in lyrics in music section.""" i = 0 ext = False for bar in self.barlist: for obj in bar.obj_list: if isinstance(obj, BarNote): if ext: if obj.slur: ext = False 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
[docs]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
[docs]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
[docs]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 self.to_part = to_part self.abbr = '' self.midi = '' self.staves = staves def __repr__(self): return '<{0} {1} {2}>'.format( self.__class__.__name__, self.name, self.part_id)
[docs] def set_first_bar(self, divisions): initime = [4, 4] iniclef = ('G', 2, 0) def check_time(bar): for obj in bar.obj_list: if isinstance(obj, BarAttr): if obj.time: return True if isinstance(obj, BarMus): return False def check_clef(bar): for obj in bar.obj_list: if isinstance(obj, BarAttr): if obj.clef or obj.multiclef: return True if isinstance(obj, BarMus): return False if not check_time(self.barlist[0]): try: self.barlist[0].obj_list[0].set_time(initime, False) except AttributeError: print("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!") self.barlist[0].obj_list[0].divs = divisions if self.staves: self.barlist[0].obj_list[0].staves = self.staves
[docs] def merge_part_to_part(self): """Merge the part with the one indicated.""" if self.to_part.barlist: self.to_part.merge_voice(self) else: self.to_part.barlist.extend(self.barlist)
[docs] def extract_global_to_section(self, name): """Extract only elements that is relevant for the score globally into a given section.""" section = ScoreSection(name, True) for bar in self.barlist: section_bar = Bar() for obj in bar.obj_list: if isinstance(obj, BarAttr): glob_barattr = BarAttr() glob_barattr.key = obj.key glob_barattr.time = obj.time glob_barattr.mode = obj.mode glob_barattr.barline = obj.barline glob_barattr.repeat = obj.repeat glob_barattr.tempo = obj.tempo section_bar.obj_list.append(glob_barattr) section.barlist.append(section_bar) return section
[docs]class Bar(): """ Representing the bar/measure. Contains also information about how complete it is.""" def __init__(self): self.obj_list = [] self.list_full = False def __repr__(self): return '<{0} {1}>'.format(self.__class__.__name__, self.obj_list)
[docs] def add(self, obj): self.obj_list.append(obj)
[docs] def has_music(self): """ Check if bar contains music. """ for obj in self.obj_list: if isinstance(obj, BarMus): return True return False
[docs] def create_backup(self): """ Calculate and create backup object.""" 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] elif isinstance(obj, BarBackup): break self.add(BarBackup((b, s)))
[docs] def is_skip(self, obj_list=None): """ Check if bar has nothing but skips. """ if not obj_list: obj_list = self.obj_list for obj in obj_list: if obj.has_attr(): return False if isinstance(obj, BarNote): return False elif isinstance(obj, BarRest): if not obj.skip: return False return True
[docs] 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 self.obj_list[0].has_attr(): self.obj_list[0].merge_attr(new_voice.obj_list[0], override) else: self.obj_list.insert(0, new_voice.obj_list[0]) backup_list = new_voice.obj_list[1:] else: backup_list = new_voice.obj_list try: if self.obj_list[-1].barline and new_voice.obj_list[-1].barline: self.obj_list.pop() except AttributeError: pass if not self.is_skip(backup_list): self.create_backup() for bl in backup_list: self.add(bl)
[docs]class BarMus(): """ Common class for notes and rests. """ def __init__(self, duration, voice=1): self.duration = duration self.type = None self.tuplet = [] self.dot = 0 self.voice = voice self.staff = 0 self.chord = False self.other_notation = None self.dynamic = [] self.oct_shift = None def __repr__(self): return '<{0} {1}>'.format(self.__class__.__name__, self.duration)
[docs] def set_tuplet(self, fraction, ttype, nr, acttype='', normtype=''): self.tuplet.append(Tuplet(fraction, ttype, nr, acttype, normtype))
[docs] def set_staff(self, staff): self.staff = staff
[docs] def add_dot(self): self.dot += 1
[docs] def add_other_notation(self, other): self.other_notation = other
[docs] def set_dynamics_mark(self, sign, before=True): self.dynamic.append(DynamicsMark(sign, before))
[docs] def set_dynamics_wedge(self, sign, before=True): self.dynamic.append(DynamicsWedge(sign, before))
[docs] def set_dynamics_text(self, sign, before=True): self.dynamic.append(DynamicsText(sign, before))
[docs] def set_dynamics_dashes(self, sign, before=True): self.dynamic.append(DynamicsDashes(sign, before))
[docs] def set_oct_shift(self, plac, octdir, size): self.oct_shift = OctaveShift(plac, octdir, size)
[docs] def has_attr(self): return False
## # Classes that are used by BarMus ##
[docs]class OctaveShift(): """Class for octave shifts.""" def __init__(self, plac, octdir, size): self.plac = plac self.octdir = octdir self.size = size
[docs]class Dynamics(): """Stores information about dynamics. """ def __init__(self, sign, before=True): self.before = before self.sign = sign
[docs]class DynamicsMark(Dynamics): """A dynamics mark.""" pass
[docs]class DynamicsWedge(Dynamics): """A dynamics wedge/hairpin.""" pass
[docs]class DynamicsText(Dynamics): """A dynamics text.""" pass
[docs]class DynamicsDashes(Dynamics): """Dynamics dashes.""" pass
[docs]class Tuplet(): """Stores information about tuplet.""" def __init__(self, fraction, ttype, nr, acttype, normtype): self.fraction = fraction self.ttype = ttype self.nr = nr self.acttype = acttype self.normtype = normtype
[docs]class Slur(): """Stores information about slur.""" def __init__(self, nr, slurtype): self.nr = nr self.slurtype = slurtype
## # Subclasses of BarMus ##
[docs]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) self.base_note = pitch_note.upper() self.alter = alter self.octave = None self.accidental_token = accidental self.tie = [] self.grace = (0, 0) self.gliss = None self.tremolo = ('', 0) self.skip = False self.slur = [] self.artic = [] self.ornament = None self.adv_ornament = None self.fingering = None self.lyric = None
[docs] def set_duration(self, duration, durtype=''): self.duration = duration self.dot = 0 if durtype: self.type = durtype
[docs] def set_durtype(self, durtype): self.type = durtype
[docs] def set_octave(self, octave): self.octave = octave
[docs] def set_tie(self, tie_type): self.tie.append(tie_type)
[docs] def set_slur(self, nr, slur_type): self.slur.append(Slur(nr, slur_type))
[docs] def add_articulation(self, art_name): self.artic.append(art_name)
[docs] def add_ornament(self, ornament): self.ornament = ornament
[docs] def add_adv_ornament(self, ornament, end_type="start"): self.adv_ornament = (ornament, {"type": end_type})
[docs] def set_grace(self, slash): self.grace = (1, slash)
[docs] def set_gliss(self, line, endtype = "start", nr=1): if not line: line = "solid" self.gliss = (line, endtype, nr)
[docs] def set_tremolo(self, trem_type, duration=False): if duration: self.tremolo = (trem_type, dur2lines(duration)) else: self.tremolo = (trem_type, self.tremolo[1])
[docs] def add_fingering(self, finger_nr): self.fingering = finger_nr
[docs] def add_lyric(self, lyric_list): if not self.lyric: self.lyric = [] self.lyric.append(lyric_list)
[docs] def change_lyric_syll(self, index, syll): self.lyric[index][1] = syll
[docs] def change_lyric_nr(self, index, nr): self.lyric[index][2] = nr
[docs]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) self.octave = 4 if step: self.base_note = step.upper()
[docs]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) self.show_type = show_type self.type = None self.skip = skip self.pos = pos
[docs] def set_duration(self, duration, durtype=''): self.duration = duration if durtype: if self.show_type: self.type = durtype else: self.type = None
[docs] def set_durtype(self, durtype): if self.show_type: self.type = durtype
[docs]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 self.clef = 0 self.mode = '' self.divs = 0 self.barline = None self.repeat = None self.staves = 0 self.multiclef = [] self.tempo = None def __repr__(self): return '<{0} {1}>'.format(self.__class__.__name__, self.time)
[docs] def set_key(self, muskey, mode): self.key = muskey self.mode = mode
[docs] 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')
[docs] def set_clef(self, clef): self.clef = clef
[docs] def set_barline(self, bl): self.barline = convert_barl(bl)
[docs] def set_tempo(self, unit=0, unittype='', beats=0, dots=0, text=""): self.tempo = TempoDir(unit, unittype, beats, dots, text)
[docs] def has_attr(self): check = False if self.key is not None: check = True elif self.time != 0: check = True elif self.clef != 0: check = True elif self.multiclef: check = True elif self.divs != 0: check = True return check
[docs] def merge_attr(self, barattr, override=False): """Merge in attributes (from another bar). Existing attributes will only be replaced when override is set to true. """ if barattr.key is not None and (override or self.key is None): self.key = barattr.key self.mode = barattr.mode if barattr.time != 0 and (override or self.time == 0): self.time = barattr.time if barattr.clef != 0 and (override or self.clef == 0): self.clef = barattr.clef if barattr.multiclef: self.multiclef += barattr.multiclef if barattr.tempo is not None and (override or self.tempo is None): self.tempo = barattr.tempo
[docs]class BarBackup(): """ Object that stores duration for backup """ def __init__(self, duration): self.duration = duration
[docs]class TempoDir(): """ Object that stores tempo direction information """ def __init__(self, unit, unittype, beats, dots, text): if unittype: self.metr = unittype, beats self.midi = self.set_midi_tempo(unit, beats, dots) else: self.metr = 0 self.midi = 0 self.dots = dots self.text = text
[docs] def set_midi_tempo(self, unit, beats, dots): u = Fraction(1, int(unit)) if dots: import math den = int(math.pow(2, dots)) num = int(math.pow(2, dots+1)-1) u *= Fraction(num, den) mult = 4*u return float(Fraction(beats)*mult)
## # Translation functions ##
[docs]def dur2lines(dur): if dur == 8: return 1 elif dur == 16: return 2 elif dur == 32: return 3 else: return 0
[docs]def convert_barl(bl): if bl == '|': return 'regular' elif bl == ':': return 'dotted' elif bl == 'dashed': return bl elif bl == '.': return 'heavy' elif bl == '||': return 'light-light' elif bl == '.|' or bl == 'forward': return 'heavy-light' elif bl == '.|.': return 'heavy-heavy' elif bl == '|.' or bl == 'backward': return 'light-heavy' elif bl == "'": return 'tick'