import os
import string
import codecs
+import zipfile
+import StringIO
from gettext import gettext as _
"""
from rational import Rational
+# Store command-line options in a global variable, so we can access them everythwere
+options = None
def progress (str):
sys.stderr.write (str + '\n')
sys.stderr.write (str + '\n')
sys.stderr.flush ()
+needed_additional_definitions = []
+additional_definitions = {
+ "snappizzicato": """#(define-markup-command (snappizzicato layout props) ()
+ (interpret-markup layout props
+ (markup #:stencil
+ (ly:stencil-translate-axis
+ (ly:stencil-add
+ (make-circle-stencil 0.7 0.1 #f)
+ (ly:make-stencil
+ (list 'draw-line 0.1 0 0.1 0 1)
+ '(-0.1 . 0.1) '(0.1 . 1)
+ )
+ )
+ 0.7 X
+ )
+ )
+ )
+)
+"""
+}
+
+def round_to_two_digits (val):
+ return round (val * 100) / 100
+
+def extract_layout_information (tree):
+ paper = musicexp.Paper ()
+ defaults = tree.get_maybe_exist_named_child ('defaults')
+ if not defaults:
+ return None
+ tenths = -1
+ scaling = defaults.get_maybe_exist_named_child ('scaling')
+ if scaling:
+ mm = scaling.get_named_child ('millimeters')
+ mm = string.atof (mm.get_text ())
+ tn = scaling.get_maybe_exist_named_child ('tenths')
+ tn = string.atof (tn.get_text ())
+ tenths = mm / tn
+ paper.global_staff_size = mm * 72.27 / 25.4
+ # We need the scaling (i.e. the size of staff tenths for everything!
+ if tenths < 0:
+ return None
+
+ def from_tenths (txt):
+ return round_to_two_digits (string.atof (txt) * tenths / 10)
+ def set_paper_variable (varname, parent, element_name):
+ el = parent.get_maybe_exist_named_child (element_name)
+ if el: # Convert to cm from tenths
+ setattr (paper, varname, from_tenths (el.get_text ()))
+
+ pagelayout = defaults.get_maybe_exist_named_child ('page-layout')
+ if pagelayout:
+ # TODO: How can one have different margins for even and odd pages???
+ set_paper_variable ("page_height", pagelayout, 'page-height')
+ set_paper_variable ("page_width", pagelayout, 'page-width')
+
+ pmargins = pagelayout.get_named_children ('page-margins')
+ for pm in pmargins:
+ set_paper_variable ("left_margin", pm, 'left-margin')
+ set_paper_variable ("right_margin", pm, 'right-margin')
+ set_paper_variable ("bottom_margin", pm, 'bottom-margin')
+ set_paper_variable ("top_margin", pm, 'top-margin')
+
+ systemlayout = defaults.get_maybe_exist_named_child ('system-layout')
+ if systemlayout:
+ sl = systemlayout.get_maybe_exist_named_child ('system-margins')
+ if sl:
+ set_paper_variable ("system_left_margin", sl, 'left-margin')
+ set_paper_variable ("system_right_margin", sl, 'right-margin')
+ set_paper_variable ("system_distance", systemlayout, 'system-distance')
+ set_paper_variable ("top_system_distance", systemlayout, 'top-system-distance')
+
+ stafflayout = defaults.get_named_children ('staff-layout')
+ for sl in stafflayout:
+ nr = getattr (sl, 'number', 1)
+ dist = sl.get_named_child ('staff-distance')
+ #TODO: the staff distance needs to be set in the Staff context!!!
+
+ # TODO: Finish appearance?, music-font?, word-font?, lyric-font*, lyric-language*
+ appearance = defaults.get_named_child ('appearance')
+ if appearance:
+ lws = appearance.get_named_children ('line-width')
+ for lw in lws:
+ # Possible types are: 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
+ tp = lw.type
+ w = from_tenths (lw.get_text ())
+ # TODO: Do something with these values!
+ nss = appearance.get_named_children ('note-size')
+ for ns in nss:
+ # Possible types are: cue, grace and large
+ tp = ns.type
+ sz = from_tenths (ns.get_text ())
+ # TODO: Do something with these values!
+ # <other-appearance> elements have no specified meaning
+
+ rawmusicfont = defaults.get_named_child ('music-font')
+ if rawmusicfont:
+ # TODO: Convert the font
+ pass
+ rawwordfont = defaults.get_named_child ('word-font')
+ if rawwordfont:
+ # TODO: Convert the font
+ pass
+ rawlyricsfonts = defaults.get_named_children ('lyric-font')
+ for lyricsfont in rawlyricsfonts:
+ # TODO: Convert the font
+ pass
+
+ return paper
+
+
+
# score information is contained in the <work>, <identification> or <movement-title> tags
# extract those into a hash, indexed by proper lilypond header attributes
def extract_score_information (tree):
return ev
def musicxml_direction_to_indicator (direction):
- return { "above": 1, "upright": 1, "below": -1, "downright": -1 }.get (direction, '')
+ return { "above": 1, "upright": 1, "up":1, "below": -1, "downright": -1, "down": -1 }.get (direction, 0)
def musicxml_fermata_to_lily_event (mxl_event):
ev = musicexp.ArticulationEvent ()
ev.type = "fermata"
if hasattr (mxl_event, 'type'):
dir = musicxml_direction_to_indicator (mxl_event.type)
- if dir:
+ if dir and options.convert_directions:
ev.force_direction = dir
return ev
def musicxml_arpeggiate_to_lily_event (mxl_event):
ev = musicexp.ArpeggioEvent ()
- ev.direction = {"up": 1, "down": -1}.get (getattr (mxl_event, 'direction', None), 0)
+ ev.direction = musicxml_direction_to_indicator (getattr (mxl_event, 'direction', None))
return ev
ev.type = mxl_event.get_text ()
return ev
+def musicxml_snappizzicato_event (mxl_event):
+ needed_additional_definitions.append ("snappizzicato")
+ ev = musicexp.MarkupEvent ()
+ ev.contents = "\\snappizzicato"
+ return ev
+
def musicxml_string_event (mxl_event):
ev = musicexp.NoDirectionArticulationEvent ()
ev.type = mxl_event.get_text ()
#"schleifer": "?",
#"scoop": "",
#"shake": "?",
- #"snap-pizzicato": "",
+ "snap-pizzicato": musicxml_snappizzicato_event,
#"spiccato": "",
"staccatissimo": (musicexp.ShortArticulationEvent, "|"), # or "staccatissimo"
"staccato": (musicexp.ShortArticulationEvent, "."), # or "staccato"
# Some articulations use the type attribute, other the placement...
dir = None
- if hasattr (mxl_event, 'type'):
+ if hasattr (mxl_event, 'type') and options.convert_directions:
dir = musicxml_direction_to_indicator (mxl_event.type)
- if hasattr (mxl_event, 'placement'):
+ if hasattr (mxl_event, 'placement') and options.convert_directions:
dir = musicxml_direction_to_indicator (mxl_event.placement)
+ if dir:
+ ev.force_direction = dir
return ev
text = re.sub (' *\n? *$', '', text)
event.text = text
- if hasattr (words, 'default-y'):
+ if hasattr (words, 'default-y') and options.convert_directions:
offset = getattr (words, 'default-y')
try:
off = string.atoi (offset)
if diff > Rational (0) and not (self.ignore_skips and moment == 0):
skip = musicexp.SkipEvent()
- skip.duration.duration_log = 0
- skip.duration.factor = diff
+ duration_factor = 1
+ duration_log = {1: 0, 2: 1, 4:2, 8:3, 16:4, 32:5, 64:6, 128:7, 256:8, 512:9}.get (diff.denominator (), -1)
+ duration_dots = 0
+ if duration_log >= 0: # denominator is a power of 2...
+ if diff.numerator () == 3:
+ duration_log -= 1
+ duration_dots = 1
+ else:
+ duration_factor = Rational (diff.numerator ())
+ else:
+ duration_log = 0
+ duration_factor = diff
+ skip.duration.duration_log = duration_log
+ skip.duration.factor = duration_factor
+ skip.duration.dots = duration_dots
evc = musicexp.EventChord ()
evc.elements.append (skip)
lyrics = {}
return_value = VoiceData ()
return_value.voicedata = voice
+
+ # First pitch needed for relative mode (if selected in command-line options)
+ first_pitch = None
# Needed for melismata detection (ignore lyrics on those notes!):
inside_slur = False
voice_builder.add_bar_check (num)
main_event = musicxml_note_to_lily_main_event (n)
+ if main_event and not first_pitch:
+ first_pitch = main_event.pitch
ignore_lyrics = inside_slur or is_tied or is_chord
if hasattr (main_event, 'drum_type') and main_event.drum_type:
if len (modes_found) > 1:
error_message ('Too many modes found %s' % modes_found.keys ())
+
+ if options.relative:
+ v = musicexp.RelativeMusic ()
+ v.element = seq_music
+ v.basepitch = first_pitch
+ seq_music = v
return_value.ly_voice = seq_music
for mode in modes_found.keys ():
def option_parser ():
- p = ly.get_option_parser(usage=_ ("musicxml2ly FILE.xml"),
+ p = ly.get_option_parser(usage=_ ("musicxml2ly [options] FILE.xml"),
version=('''%prog (LilyPond) @TOPLEVEL_VERSION@\n\n'''
+
_ ("""This program is free software. It is covered by the GNU General Public
information.""") % 'lilypond'
+ """
Copyright (c) 2005--2007 by
- Han-Wen Nienhuys <hanwen@xs4all.nl> and
- Jan Nieuwenhuizen <janneke@gnu.org>
+ Han-Wen Nienhuys <hanwen@xs4all.nl>,
+ Jan Nieuwenhuizen <janneke@gnu.org> and
+ Reinhold Kainhofer <reinhold@kainhofer.com>
"""),
description=_ ("Convert %s to LilyPond input.") % 'MusicXML' + "\n")
p.add_option ('-v', '--verbose',
default=False,
dest="use_lxml",
help=_ ("Use lxml.etree; uses less memory and cpu time."))
-
+
+ p.add_option ('-z', '--compressed',
+ action = "store_true",
+ dest = 'compressed',
+ default = False,
+ help = _ ("Input file is a zip-compressed MusicXML file."))
+
+ p.add_option ('-r', '--relative',
+ action = "store_true",
+ dest = "relative",
+ help = _ ("Convert pitches in relative mode."))
+
+ p.add_option ('-l', '--language',
+ action = "store",
+ help = _ ("Use a different language file, e.g. 'deutsch' for deutsch.ly."))
+
+ p.add_option ('--no-articulation-directions', '--nd',
+ action = "store_false",
+ default = True,
+ dest = "convert_directions",
+ help = _ ("Do not convert directions (^, _ or -) for articulations."))
+
p.add_option ('-o', '--output',
metavar=_ ("FILE"),
action="store",
printer.dump_version ()
printer.print_verbatim ('%% automatically converted from %s\n' % filename)
-def read_musicxml (filename, use_lxml):
+def print_ly_additional_definitions (printer, filename):
+ if needed_additional_definitions:
+ printer.newline ()
+ printer.print_verbatim ('%% additional definitions required by the score:')
+ printer.newline ()
+ for a in set(needed_additional_definitions):
+ printer.print_verbatim (additional_definitions.get (a, ''))
+ printer.newline ()
+
+# Read in the tree from the given I/O object (either file or string) and
+# demarshall it using the classes from the musicxml.py file
+def read_xml (io_object, use_lxml):
if use_lxml:
import lxml.etree
-
- tree = lxml.etree.parse (filename)
+ tree = lxml.etree.parse (io_object)
mxl_tree = musicxml.lxml_demarshal_node (tree.getroot ())
return mxl_tree
else:
from xml.dom import minidom, Node
-
- doc = minidom.parse(filename)
+ doc = minidom.parse(io_object)
node = doc.documentElement
return musicxml.minidom_demarshal_node (node)
-
return None
+def read_musicxml (filename, compressed, use_lxml):
+ raw_string = None
+ if compressed:
+ progress ("Input file %s is compressed, extracting raw MusicXML data" % filename)
+ z = zipfile.ZipFile (filename, "r")
+ container_xml = z.read ("META-INF/container.xml")
+ if not container_xml:
+ return None
+ container = read_xml (StringIO.StringIO (container_xml), use_lxml)
+ if not container:
+ return None
+ rootfiles = container.get_maybe_exist_named_child ('rootfiles')
+ if not rootfiles:
+ return None
+ rootfile_list = rootfiles.get_named_children ('rootfile')
+ mxml_file = None
+ if len (rootfile_list) > 0:
+ mxml_file = getattr (rootfile_list[0], 'full-path', None)
+ if mxml_file:
+ raw_string = z.read (mxml_file)
+
+ io_object = filename
+ if raw_string:
+ io_object = StringIO.StringIO (raw_string)
+
+ return read_xml (io_object, use_lxml)
+
+
def convert (filename, options):
progress ("Reading MusicXML from %s ..." % filename)
- tree = read_musicxml (filename, options.use_lxml)
+ tree = read_musicxml (filename, options.compressed, options.use_lxml)
parts = tree.get_typed_children (musicxml.Part)
(voices, staff_info) = get_all_voices (parts)
# score information is contained in the <work>, <identification> or <movement-title> tags
score_information = extract_score_information (tree)
+ layout_information = extract_layout_information (tree)
update_score_setup (score_structure, part_list, voices)
if not options.output_name:
printer.set_file (codecs.open (defs_ly_name, 'wb', encoding='utf-8'))
print_ly_preamble (printer, filename)
- score_information.print_ly (printer)
+ print_ly_additional_definitions (printer, filename)
+ if score_information:
+ score_information.print_ly (printer)
+ if layout_information:
+ layout_information.print_ly (printer)
print_voice_definitions (printer, part_list, voices)
printer.close ()
def get_existing_filename_with_extension (filename, ext):
if os.path.exists (filename):
return filename
- newfilename = filename + ".xml"
+ newfilename = filename + "." + ext
if os.path.exists (newfilename):
return newfilename;
- newfilename = filename + "xml"
+ newfilename = filename + ext
if os.path.exists (newfilename):
return newfilename;
return ''
def main ():
opt_parser = option_parser()
+ global options
(options, args) = opt_parser.parse_args ()
if not args:
opt_parser.print_usage()
sys.exit (2)
-
+
+ if options.language:
+ musicexp.set_pitch_language (options.language)
+ needed_additional_definitions.append (options.language)
+ additional_definitions[options.language] = "\\include \"%s.ly\"\n" % options.language
+
# Allow the user to leave out the .xml or xml on the filename
filename = get_existing_filename_with_extension (args[0], "xml")
+ if not filename:
+ filename = get_existing_filename_with_extension (args[0], "mxl")
+ options.compressed = True
if filename and os.path.exists (filename):
voices = convert (filename, options)
else: