From 6dc25f04195169860be7f336031ed8baa98cdc40 Mon Sep 17 00:00:00 2001 From: Han-Wen Nienhuys Date: Mon, 5 Dec 2005 12:25:26 +0000 Subject: [PATCH] * scripts/musicxml2ly.py (NonDentedHeadingFormatter.format_headi): option formatting, lilypond style. * python/musicexp.py: grab from Ikebana: a library for composing ly music expressions. (Output_printer): class for advanced .ly printing. (eg. tupletting) * python/musicxml.py: new file. Read MusicXML MiniDOM tree, and convert to pythonesque structure. * Documentation/user/converters.itely (Invoking musicxml2ly): new node. --- ChangeLog | 17 ++ Documentation/topdocs/NEWS.tely | 2 + Documentation/user/converters.itely | 21 ++ make/lilypond.fedora.spec.in | 2 + python/musicexp.py | 351 +++++++++++++++++++--------- python/musicxml.py | 168 +++++++------ scripts/GNUmakefile | 2 +- scripts/musicxml2ly.py | 258 ++++++++++++++++++-- 8 files changed, 618 insertions(+), 203 deletions(-) diff --git a/ChangeLog b/ChangeLog index 7ae44f8c7e..1ef20720a9 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,20 @@ +2005-12-05 Han-Wen Nienhuys + + * scripts/musicxml2ly.py (NonDentedHeadingFormatter.format_headi): + option formatting, lilypond style. + + * python/musicexp.py: grab from Ikebana: a library for composing + ly music expressions. + (Output_printer): class for advanced .ly printing. + (eg. tupletting) + + * python/musicxml.py: new file. Read MusicXML MiniDOM tree, and + convert to pythonesque structure. + + * python/rational.py: PD rational number class. + + * Documentation/user/converters.itely (Invoking musicxml2ly): new node. + 2005-12-04 Erik Sandberg * lily/part-combine-iterator.cc: Minor bugfix diff --git a/Documentation/topdocs/NEWS.tely b/Documentation/topdocs/NEWS.tely index efc05d37ff..069dbd931e 100644 --- a/Documentation/topdocs/NEWS.tely +++ b/Documentation/topdocs/NEWS.tely @@ -45,6 +45,8 @@ This document is also available in @uref{NEWS.pdf,PDF}. @itemize @bullet +@item A MusicXML importer is included now. + @item Texts set in a TrueType font are now kerned. This requires CVS Pango or Pango 1.12. diff --git a/Documentation/user/converters.itely b/Documentation/user/converters.itely index b3172d1774..9d46c6d97c 100644 --- a/Documentation/user/converters.itely +++ b/Documentation/user/converters.itely @@ -140,6 +140,27 @@ version information The list of articulation scripts is incomplete. Empty measures confuse @command{etf2ly}. Sequences of grace notes are ended improperly. +@node Invoking musicxml2ly +@section Invoking @code{musicxml2ly} + +@uref{http://@/www.@/recordarde@/.com/xml/,MusicXML} is a XML dialect +for representing music notation. + +@command{musicxml2ly} extracts the notes from part-wise MusicXML +files, and writes it to a .ly file. + + +The following options are supported by @command{musicxml2ly}: + + +@table @code +@item -h,--help +print usage and option summary. +@item -o,--output=@var{file} +set output filename to @var{file}. (default: print to stdout) +@item -v,--version +print version information. +@end table @node Invoking abc2ly @section Invoking @code{abc2ly} diff --git a/make/lilypond.fedora.spec.in b/make/lilypond.fedora.spec.in index 4b671b4ab4..fafd9cd376 100644 --- a/make/lilypond.fedora.spec.in +++ b/make/lilypond.fedora.spec.in @@ -158,6 +158,7 @@ scrollkeeper-update %{_bindir}/midi2ly %{_bindir}/lilypond-book %{_bindir}/mup2ly +%{_bindir}/musicxml2ly %{_bindir}/lilypond-invoke-editor %doc THANKS @@ -173,6 +174,7 @@ scrollkeeper-update %endif %{_mandir}/man1/abc2ly.1.gz +%{_mandir}/man1/musicxml2ly.1.gz %{_mandir}/man1/convert-ly.1.gz %{_mandir}/man1/etf2ly.1.gz %{_mandir}/man1/lilypond.1.gz diff --git a/python/musicexp.py b/python/musicexp.py index 76f9078a0b..717a9a8a41 100644 --- a/python/musicexp.py +++ b/python/musicexp.py @@ -1,53 +1,95 @@ import inspect import sys import string -from rational import Rational - -def flatten_list (fl): - if type(fl) == type((1,)): - return - - flattened = [] - for f in fl: - flattened += flatten_list (fl) - -def is_derived (deriv_class, maybe_base): - if deriv_class == maybe_base: - return True +import re - for c in deriv_class.__bases__: - if is_derived (c, maybe_base): - return True - - return False +from rational import Rational +class Output_stack_element: + def __init__ (self): + self.factor = Rational (1) + def copy (self): + o = Output_stack_element() + o.factor = self.factor + return o class Output_printer: + + ## TODO: support for \relative. + def __init__ (self): self.line = '' - self.indent = 0 + self.indent = 4 + self.nesting = 0 self.file = sys.stdout self.line_len = 72 + self.output_state_stack = [Output_stack_element()] + self._skipspace = False + self.last_duration = None + + def get_indent (self): + return self.nesting * self.indent + + def override (self): + last = self.output_state_stack[-1] + self.output_state_stack.append (last.copy()) + def add_factor (self, factor): + self.override() + self.output_state_stack[-1].factor *= factor + + def revert (self): + del self.output_state_stack[-1] + if not self.output_state_stack: + raise 'empty' + + def duration_factor (self): + return self.output_state_stack[-1].factor + + def print_verbatim (self, str): + self.line += str + + def print_duration_string (self, str): + if self.last_duration == str: + return + + self.print_verbatim (str) + def add_word (self, str): if (len (str) + 1 + len (self.line) > self.line_len): self.newline() + self._skipspace = True + + self.nesting += str.count ('<') + str.count ('{') + self.nesting -= str.count ('>') + str.count ('}') - self.indent += str.count ('<') + str.count ('{') - self.indent -= str.count ('>') + str.count ('}') - self.line += ' ' + str + if not self._skipspace: + self.line += ' ' + self.line += str + self._skipspace = False def newline (self): self.file.write (self.line + '\n') - self.line = ' ' * self.indent + self.line = ' ' * self.indent * self.nesting + self._skipspace = True + + def skipspace (self): + self._skipspace = True + def __call__(self, arg): + self.dump (arg) + def dump (self, str): - words = string.split (str) - for w in words: - self.add_word (w) - + if self._skipspace: + self._skipspace = False + self.print_verbatim (str) + else: + words = string.split (str) + for w in words: + self.add_word (w) + class Duration: def __init__ (self): - self.duration_log = 2 + self.duration_log = 0 self.dots = 0 self.factor = Rational (1) @@ -57,14 +99,28 @@ class Duration: self.factor.numerator (), self.factor.denominator ()) - def ly_expression (self): + + def ly_expression (self, factor = None): + if not factor: + factor = self.factor + str = '%d%s' % (1 << self.duration_log, '.'*self.dots) - if self.factor <> Rational (1,1): - str += '*%d/%d' % (self.factor.numerator (),self.factor.denominator ()) + if factor <> Rational (1,1): + str += '*%d/%d' % (factor.numerator (), factor.denominator ()) return str - + + def print_ly (self, outputter): + if isinstance (outputter, Output_printer): + str = self.ly_expression (self.factor / outputter.duration_factor ()) + outputter.print_duration_string (str) + else: + outputter (self.ly_expression ()) + + def __repr__(self): + return self.ly_expression() + def copy (self): d = Duration () d.dots = self.dots @@ -84,6 +140,7 @@ class Duration: base = Rational (1, dur) return base * dot_fact * self.factor + class Pitch: def __init__ (self): @@ -91,6 +148,28 @@ class Pitch: self.step = 0 self.octave = 0 + def __repr__(self): + return self.ly_expression() + + def transposed (self, interval): + c = self.copy () + c.alteration += interval.alteration + c.step += interval.step + c.octave += interval.octave + c.normalize () + + target_st = self.semitones() + interval.semitones() + c.alteration += target_st - c.semitones() + return c + + def normalize (c): + while c.step < 0: + c.step += 7 + c.octave -= 1 + c.octave += c.step / 7 + c.step = c.step % 7 + + def lisp_expression (self): return '(ly:make-pitch %d %d %d)' % (self.octave, self.step, @@ -104,7 +183,10 @@ class Pitch: return p def steps (self): - return self.step + self.octave * 7 + return self.step + self.octave *7 + + def semitones (self): + return self.octave * 12 + [0,2,4,5,7,9,11][self.step] + self.alteration def ly_step_expression (self): str = 'cdefgab'[self.step] @@ -123,10 +205,11 @@ class Pitch: str += "," * (-self.octave - 1) return str - + def print_ly (self, outputter): + outputter (self.ly_expression()) + class Music: def __init__ (self): - self.tag = None self.parent = None self.start = Rational (0) pass @@ -134,10 +217,6 @@ class Music: def get_length(self): return Rational (0) - def set_tag (self, counter, tag_dict): - self.tag = counter - tag_dict [counter] = self - return counter + 1 def get_properties (self): return '' @@ -150,17 +229,16 @@ class Music: return self.parent.elements.index (self) else: return None - + def name (self): + return self.__class__.__name__ + def lisp_expression (self): name = self.name() - tag = '' - if self.tag: - tag = "'input-tag %d" % self.tag props = self.get_properties () # props += 'start %f ' % self.start - return "(make-music '%s %s %s)" % (name, tag, props) + return "(make-music '%s %s)" % (name, props) def set_start (self, start): self.start = start @@ -172,29 +250,50 @@ class Music: def print_ly (self, printer): printer (self.ly_expression ()) - -class Music_document: + + +class Comment (Music): + def __name__ (self): + self.text = '' + def print_ly (self, printer): + if isinstance (printer, Output_printer): + lines = string.split (self.text, '\n') + for l in lines: + if l: + printer.print_verbatim ('% ' + l) + printer.newline () + else: + printer ('% ' + re.sub ('\n', '\n% ', self.text)) + printer ('\n') + + + +class MusicWrapper (Music): def __init__ (self): - self.music = test_expr () - self.tag_dict = {} - self.touched = True - - def recompute (self): - self.tag_dict = {} - self.music.set_tag (0, self.tag_dict) - self.music.set_start (Rational (0)) - + Music.__init__(self) + self.element = None + def print_ly (self, func): + self.element.print_ly (func) + +class TimeScaledMusic (MusicWrapper): + def print_ly (self, func): + if isinstance(func, Output_printer): + func ('\\times %d/%d ' % + (self.numerator, self.denominator)) + func.add_factor (Rational (self.numerator, self.denominator)) + MusicWrapper.print_ly (self, func) + func.revert () + else: + func (r'\times 1/1 ') + MusicWrapper.print_ly (self, func) + class NestedMusic(Music): def __init__ (self): Music.__init__ (self) self.elements = [] def has_children (self): return self.elements - def set_tag (self, counter, dict): - counter = Music.set_tag (self, counter, dict) - for e in self.elements : - counter = e.set_tag (counter, dict) - return counter + def insert_around (self, succ, elt, dir): assert elt.parent == None @@ -256,9 +355,6 @@ class NestedMusic(Music): return None class SequentialMusic (NestedMusic): - def name(self): - return 'SequentialMusic' - def print_ly (self, printer): printer ('{') for e in self.elements: @@ -267,14 +363,11 @@ class SequentialMusic (NestedMusic): def lisp_sub_expression (self, pred): name = self.name() - tag = '' - if self.tag: - tag = "'input-tag %d" % self.tag props = self.get_subset_properties (pred) - return "(make-music '%s %s %s)" % (name, tag, props) + return "(make-music '%s %s)" % (name, props) def set_start (self, start): for e in self.elements: @@ -282,9 +375,6 @@ class SequentialMusic (NestedMusic): start += e.get_length() class EventChord(NestedMusic): - def name(self): - return "EventChord" - def get_length (self): l = Rational (0) for e in self.elements: @@ -293,41 +383,47 @@ class EventChord(NestedMusic): def print_ly (self, printer): note_events = [e for e in self.elements if - is_derived (e.__class__, NoteEvent)] + isinstance (e, NoteEvent)] + rest_events = [e for e in self.elements if - is_derived (e.__class__, RhythmicEvent) - and not is_derived (e.__class__, NoteEvent)] + isinstance (e, RhythmicEvent) + and not isinstance (e, NoteEvent)] other_events = [e for e in self.elements if - not is_derived (e.__class__, RhythmicEvent)] + not isinstance (e, RhythmicEvent)] if rest_events: - printer (rest_events[0].ly_expression ()) + rest_events[0].print_ly (printer) elif len (note_events) == 1: - printer (note_events[0].ly_expression ()) + note_events[0].print_ly (printer) elif note_events: pitches = [x.pitch.ly_expression () for x in note_events] - printer ('<%s>' % string.join (pitches) - + note_events[0].duration.ly_expression ()) + printer ('<%s>' % string.join (pitches)) + note_events[0].duration.print_ly (printer) else: pass + # print 'huh', rest_events, note_events, other_events - - for e in other_events: + for e in other_events: e.print_ly (printer) class Event(Music): - def __init__ (self): - Music.__init__ (self) + pass - def name (self): - return "Event" +class SpanEvent (Event): + def __init__(self): + Event.__init__ (self) + self.span_direction = 0 + def get_properties(self): + return "'span-direction %d" % self.span_direction +class SlurEvent (SpanEvent): + def ly_expression (self): + return {-1: '(', + 0:'', + 1:')'}[self.span_direction] class ArpeggioEvent(Music): - def name (self): - return 'ArpeggioEvent' - def ly_expression (self): return ('\\arpeggio') @@ -343,18 +439,17 @@ class RhythmicEvent(Event): return ("'duration %s" % self.duration.lisp_expression ()) - def name (self): - return 'RhythmicEvent' - class RestEvent (RhythmicEvent): - def name (self): - return 'RestEvent' def ly_expression (self): return 'r%s' % self.duration.ly_expression () + + def print_ly (self, printer): + printer('r') + if isinstance(printer, Output_printer): + printer.skipspace() + self.duration.print_ly (printer) class SkipEvent (RhythmicEvent): - def name (self): - return 'SkipEvent' def ly_expression (self): return 's%s' % self.duration.ly_expression () @@ -363,9 +458,6 @@ class NoteEvent(RhythmicEvent): RhythmicEvent.__init__ (self) self.pitch = Pitch() - def name (self): - return 'NoteEvent' - def get_properties (self): return ("'pitch %s\n 'duration %s" % (self.pitch.lisp_expression (), @@ -375,17 +467,20 @@ class NoteEvent(RhythmicEvent): return '%s%s' % (self.pitch.ly_expression (), self.duration.ly_expression ()) + def print_ly (self, printer): + self.pitch.print_ly (printer) + self.duration.print_ly (printer) - -class KeySignatureEvent (Event): - def __init__ (self, tonic, scale): - Event.__init__ (self) - self.scale = scale - self.tonic = tonic - def name (self): - return 'KeySignatureEvent' +class KeySignatureChange (Music): + def __init__ (self): + Music.__init__ (self) + self.scale = [] + self.tonic = Pitch() + self.mode = 'major' + def ly_expression (self): - return '\\key %s \\major' % self.tonic.ly_step_expression () + return '\\key %s \\%s' % (self.tonic.ly_step_expression (), + self.mode) def lisp_expression (self): pairs = ['(%d . %d)' % (i , self.scale[i]) for i in range (0,7)] @@ -394,13 +489,19 @@ class KeySignatureEvent (Event): return """ (make-music 'KeyChangeEvent 'pitch-alist %s) """ % scale_str -class ClefEvent (Event): - def __init__ (self, t): - Event.__init__ (self) - self.type = t +class TimeSignatureChange (Music): + def __init__ (self): + Music.__init__ (self) + self.fraction = (4,4) + def ly_expression (self): + return '\\time %d/%d ' % self.fraction + +class ClefChange (Music): + def __init__ (self): + Music.__init__ (self) + self.type = 'G' - def name (self): - return 'ClefEvent' + def ly_expression (self): return '\\clef "%s"' % self.type clef_dict = { @@ -423,6 +524,28 @@ class ClefEvent (Event): """ % (glyph, pos, c0) return clefsetting + +def test_pitch (): + bflat = Pitch() + bflat.alteration = -1 + bflat.step = 6 + bflat.octave = -1 + fifth = Pitch() + fifth.step = 4 + down = Pitch () + down.step = -4 + down.normalize () + + + print bflat.semitones() + print bflat.transposed (fifth), bflat.transposed (fifth).transposed (fifth) + print bflat.transposed (fifth).transposed (fifth).transposed (fifth) + + print bflat.semitones(), 'down' + print bflat.transposed (down) + print bflat.transposed (down).transposed (down) + print bflat.transposed (down).transposed (down).transposed (down) + def test_expr (): m = SequentialMusic() l = 2 @@ -447,7 +570,7 @@ def test_expr (): evc.insert_around (None, n, 0) m.insert_around (None, evc, 0) - evc = ClefEvent("G") + evc = ClefChange("G") m.insert_around (None, evc, 0) evc = EventChord() @@ -462,6 +585,8 @@ def test_expr (): if __name__ == '__main__': + test_pitch() + raise 1 expr = test_expr() expr.set_start (Rational (0)) print expr.ly_expression() diff --git a/python/musicxml.py b/python/musicxml.py index 87240cbb16..34de577a7c 100644 --- a/python/musicxml.py +++ b/python/musicxml.py @@ -13,6 +13,10 @@ class Xml_node: self._data = None self._original = None self._name = 'xml_node' + self._parent = None + + def is_first (self): + return self._parent.get_typed_children (self.__class__)[0] == self def original (self): return self._original @@ -29,7 +33,10 @@ class Xml_node: return ''.join ([c.get_text () for c in self._children]) def get_typed_children (self, klass): - return [c for c in self._children if c.__class__ == klass] + return [c for c in self._children if isinstance(c, klass)] + + def get_named_children (self, nm): + return self.get_typed_children (class_dict[nm]) def get_children (self, predicate): return [c for c in self._children if predicate(c)] @@ -37,6 +44,9 @@ class Xml_node: def get_all_children (self): return self._children + def get_maybe_exist_named_child (self, name): + return self.get_maybe_exist_typed_child (class_dict[name]) + def get_maybe_exist_typed_child (self, klass): cn = self.get_typed_children (klass) if len (cn)==0: @@ -59,28 +69,15 @@ class Music_xml_node (Xml_node): Xml_node.__init__ (self) self.duration = Rational (0) self.start = Rational (0) - -class Attributes (Music_xml_node): - def __init__ (self): - self._dict = {} - def set_attributes_from_previous (self, dict): - self._dict.update (dict) - def read_self (self): - for c in self.get_all_children (): - self._dict[c.get_name()] = c - def get_named_attribute (self, name): - return self._dict[name] - class Duration (Music_xml_node): def get_length (self): dur = string.atoi (self.get_text ()) * Rational (1,4) return dur class Hash_comment (Music_xml_node): - def to_ly (self, output_func): - output_func ('%% %s\n ' % self.get_text()) + pass class Pitch (Music_xml_node): def get_step (self): @@ -100,24 +97,34 @@ class Pitch (Music_xml_node): alter = string.atoi (ch.get_text ().strip ()) return alter - def to_ly (self, output_func): - oct = (self.get_octave () - 4) - oct_str = '' - if oct > 0: - oct_str = "'" * oct - elif oct < 0: - oct_str = "," * -oct - - alt = self.get_alteration () - alt_str = '' - if alt > 0: - alt_str = 'is' * alt - elif alt < 0: - alt_str = 'es' * alt +class Measure_element (Music_xml_node): + def get_voice_id (self): + voice_id = self.get_maybe_exist_named_child ('voice') + if voice_id: + return voice_id.get_text () + else: + return None - output_func ('%s%s%s' % (self.get_step ().lower(), alt_str, oct_str)) - -class Note (Music_xml_node): + def is_first (self): + cn = self._parent.get_typed_children (self.__class__) + cn = [c for c in cn if c.get_voice_id () == self.get_voice_id ()] + return cn[0] == self + +class Attributes (Measure_element): + def __init__ (self): + Measure_element.__init__ (self) + self._dict = {} + + def set_attributes_from_previous (self, dict): + self._dict.update (dict) + def read_self (self): + for c in self.get_all_children (): + self._dict[c.get_name()] = c + + def get_named_attribute (self, name): + return self._dict[name] + +class Note (Measure_element): def get_duration_log (self): ch = self.get_maybe_exist_typed_child (class_dict[u'type']) @@ -140,38 +147,13 @@ class Note (Music_xml_node): def get_pitches (self): return self.get_typed_children (class_dict[u'pitch']) - def to_ly (self, func): - ps = self.get_pitches () - - if len (ps) == 0: - func ('r') - else: - func ('<') - for p in ps: - p.to_ly (func) - func ('>') - - func ('%d ' % (1 << self.get_duration_log ())) - - class Measure(Music_xml_node): def get_notes (self): return self.get_typed_children (class_dict[u'note']) - def to_ly (self, func): - func (' { % measure \n ') - for c in self._children: - c.to_ly (func) - func (' } \n ') class Part (Music_xml_node): - def to_ly (self, func): - func (' { %% part %s \n ' % self.name) - for c in self._children: - c.to_ly (func) - func (' } \n ') - def interpret (self): """Set durations and starting points.""" @@ -210,12 +192,22 @@ class Part (Music_xml_node): measures = part.get_typed_children (Measure) elements = [] for m in measures: - elements.extend (m.get_typed_children (Note)) + elements.extend (m.get_all_children ()) + start_attr = None for n in elements: voice_id = n.get_maybe_exist_typed_child (class_dict['voice']) - if not voice_id: + if not (voice_id or isinstance (n, Attributes)): + continue + + if isinstance (n, Attributes) and not start_attr: + start_attr = n + continue + + if isinstance (n, Attributes): + for v in voices.values (): + v.append (n) continue id = voice_id.get_text () @@ -223,11 +215,42 @@ class Part (Music_xml_node): voices[id] = [] voices[id].append (n) - + + if start_attr: + for (k,v) in voices.items (): + v.insert (0, start_attr) + part._voices = voices def get_voices (self): return self._voices - + +class Notations (Music_xml_node): + def get_tuplet (self): + return self.get_maybe_exist_typed_child (Tuplet) + def get_slur (self): + slurs = self.get_typed_children (Slur) + + if not slurs: + return None + + if len (slurs) > 1: + print "More than one slur?!" + + return slurs[0] + +class Time_modification(Music_xml_node): + def get_fraction (self): + b = self.get_maybe_exist_typed_child (class_dict['actual-notes']) + a = self.get_maybe_exist_typed_child (class_dict['normal-notes']) + return (string.atoi(a.get_text ()), string.atoi (b.get_text ())) + + + +class Tuplet(Music_xml_node): + pass +class Slur (Music_xml_node): + pass + class Chord (Music_xml_node): pass class Dot (Music_xml_node): @@ -244,6 +267,8 @@ class Grace (Music_xml_node): pass class_dict = { + 'notations': Notations, + 'time-modification': Time_modification, 'alter': Alter, 'grace': Grace, 'rest':Rest, @@ -256,6 +281,8 @@ class_dict = { 'part': Part, 'measure': Measure, 'type': Type, + 'slur': Slur, + 'tuplet': Tuplet, '#comment': Hash_comment, } @@ -287,9 +314,13 @@ def demarshal_node (node): py_node = klass() py_node._name = name py_node._children = [demarshal_node (cn) for cn in node.childNodes] + for c in py_node._children: + c._parent = py_node + if node.attributes: - for (name, value) in node.attributes.items(): - py_node.name = value + + for (nm, value) in node.attributes.items(): + py_node.__dict__[nm] = value py_node._data = None if node.nodeType == node.TEXT_NODE and node.data: @@ -314,21 +345,11 @@ def create_tree (name): create_classes (names, class_dict) return demarshal_node (node) - -def oldtest (): - n = tree._children[-2]._children[-1]._children[0] - print n - print n.get_duration_log() - print n.get_pitches() - print n.get_pitches()[0].get_alteration() - def test_musicxml (tree): m = tree._children[-2] print m - m.to_ly (lambda str: sys.stdout.write (str)) - def read_musicxml (name): tree = create_tree (name) strip_white_space (tree) @@ -338,3 +359,4 @@ if __name__ == '__main__': tree = read_musicxml ('BeetAnGeSample.xml') test_musicxml (tree) + diff --git a/scripts/GNUmakefile b/scripts/GNUmakefile index 009d4c46b8..cfa46960c6 100644 --- a/scripts/GNUmakefile +++ b/scripts/GNUmakefile @@ -1,6 +1,6 @@ depth = .. -SEXECUTABLES=convert-ly lilypond-book abc2ly etf2ly mup2ly midi2ly lilypond-invoke-editor +SEXECUTABLES=convert-ly lilypond-book abc2ly etf2ly mup2ly midi2ly lilypond-invoke-editor musicxml2ly STEPMAKE_TEMPLATES=script help2man po LOCALSTEPMAKE_TEMPLATES = lilypond diff --git a/scripts/musicxml2ly.py b/scripts/musicxml2ly.py index a1a1def9b0..2be5653651 100644 --- a/scripts/musicxml2ly.py +++ b/scripts/musicxml2ly.py @@ -1,6 +1,11 @@ +#!@PYTHON@ + +import optparse import sys import re import os +import string +from gettext import gettext as _ datadir = '@local_lilypond_datadir@' if not os.path.isdir (datadir): @@ -17,6 +22,7 @@ elif os.path.exists (os.path.join (datadir, 'share/lilypond/current/')): sys.path.insert (0, os.path.join (datadir, 'python')) + import musicxml import musicexp from rational import Rational @@ -26,29 +32,153 @@ def musicxml_duration_to_lily (mxl_note): if mxl_note.get_maybe_exist_typed_child (musicxml.Type): d.duration_log = mxl_note.get_duration_log () else: - d.factor = mxl_note._duration d.duration_log = 0 d.dots = len (mxl_note.get_typed_children (musicxml.Dot)) d.factor = mxl_note._duration / d.get_length () return d + +span_event_dict = { + 'start': -1, + 'stop': 1 +} + +def group_tuplets (music_list, events): + indices = [] + + j = 0 + for (ev_chord, tuplet_elt, fraction) in events: + while (j < len (music_list)): + if music_list[j]== ev_chord: + break + j += 1 + if tuplet_elt.type == 'start': + indices.append ((j, None, fraction)) + elif tuplet_elt.type == 'stop': + indices[-1] = (indices[-1][0], j, indices[-1][2]) + + new_list = [] + last = 0 + for (i1, i2, frac) in indices: + if i1 >= i2: + continue + + new_list.extend (music_list[last:i1]) + seq = musicexp.SequentialMusic () + last = i2 + 1 + seq.elements = music_list[i1:last] + + tsm = musicexp.TimeScaledMusic () + tsm.element = seq + + tsm.numerator = frac[0] + tsm.denominator = frac[1] + + new_list.append (tsm) + + new_list.extend (music_list[last:]) + return new_list + +def musicxml_clef_to_lily (mxl): + sign = mxl.get_maybe_exist_named_child ('sign') + change = musicexp.ClefChange () + if sign: + change.type = sign.get_text () + return change + + +def musicxml_time_to_lily (mxl): + beats = mxl.get_maybe_exist_named_child ('beats') + type = mxl.get_maybe_exist_named_child ('beat-type') + change = musicexp.TimeSignatureChange() + change.fraction = (string.atoi(beats.get_text ()), + string.atoi(type.get_text ())) + + return change + +def musicxml_key_to_lily (mxl): + mode = mxl.get_maybe_exist_named_child ('mode').get_text () + fifths = string.atoi (mxl.get_maybe_exist_named_child ('fifths').get_text ()) + + fifth = musicexp.Pitch() + fifth.step = 4 + if fifths < 0: + fifths *= -1 + fifth.step *= -1 + fifth.normalize () + + c = musicexp.Pitch() + for x in range (fifths): + c = c.transposed (fifth) + + c.octave = 0 + + change = musicexp.KeySignatureChange() + change.mode = mode + change.tonic = c + return change + +def musicxml_attributes_to_lily (attrs): + elts = [] + attr_dispatch = { + 'clef': musicxml_clef_to_lily, + 'time': musicxml_time_to_lily, + 'key': musicxml_key_to_lily + } + for (k, func) in attr_dispatch.items (): + childs = attrs.get_named_children (k) + + ## ugh: you get clefs spread over staves for piano + if childs: + elts.append (func (childs[0])) + + return elts + +def insert_measure_start_comments (ly_voice, indices): + idxs = indices[:] + idxs.reverse () + for i in idxs: + c = musicexp.Comment() + c.text = '' + ly_voice.insert (i, c) + + return ly_voice def musicxml_voice_to_lily_voice (voice): ly_voice = [] ly_now = Rational (0) + + tuplet_events = [] + + measure_start_indices = [] for n in voice: + if n.is_first (): + measure_start_indices.append (len (ly_voice)) + + if isinstance (n, musicxml.Attributes): + ly_voice.extend (musicxml_attributes_to_lily (n)) + continue + if not n.__class__.__name__ == 'Note': - print 'not a note?' + print 'not a Note or Attributes?' continue - + + pitch = None duration = None mxl_pitch = n.get_maybe_exist_typed_child (musicxml.Pitch) event = None - + + notations = n.get_maybe_exist_typed_child (musicxml.Notations) + tuplet_event = None + slur_event = None + if notations: + tuplet_event = notations.get_tuplet () + slur_event = notations.get_slur () + if mxl_pitch: pitch = musicxml_pitch_to_lily (mxl_pitch) event = musicexp.NoteEvent() @@ -67,7 +197,6 @@ def musicxml_voice_to_lily_voice (voice): if diff < Rational (0): print 'huh: negative skip', n._when, ly_now, n._duration diff = Rational (1,314159265) - skip = musicexp.SkipEvent() skip.duration.duration_log = 0 @@ -81,13 +210,30 @@ def musicxml_voice_to_lily_voice (voice): ly_voice.append (musicexp.EventChord()) else: pass - #print 'append' + ev_chord = ly_voice[-1] ev_chord.elements.append (event) + if tuplet_event: + mod = n.get_maybe_exist_typed_child (musicxml.Time_modification) + frac = (1,1) + if mod: + frac = mod.get_fraction () + + tuplet_events.append ((ev_chord, tuplet_event, frac)) + + if slur_event: + sp = musicexp.SlurEvent() + try: + sp.span_direction = span_event_dict[slur_event.type] + ev_chord.elements.append (sp) + except KeyError: + pass + + ly_voice = insert_measure_start_comments (ly_voice, measure_start_indices) + ly_voice = group_tuplets (ly_voice, tuplet_events) seq_music = musicexp.SequentialMusic() - seq_music.elements = ly_voice return seq_music @@ -122,22 +268,102 @@ def get_all_voices (parts): for (id, voice) in voice_dict.items (): m = musicxml_voice_to_lily_voice (voice) - m_name = 'Part' + p.name + 'Voice' + id + m_name = 'Part' + p.id + 'Voice' + id m_name = musicxml_id_to_lily (m_name) all_voices[m_name] = m return all_voices -printer = musicexp.Output_printer() +class NonDentedHeadingFormatter (optparse.IndentedHelpFormatter): + def format_heading(self, heading): + if heading: + return heading[0].upper() + heading[1:] + ':\n' + return '' + def format_option_strings(self, option): + sep = ' ' + if option._short_opts and option._long_opts: + sep = ',' + + metavar = '' + if option.takes_value(): + metavar = '=' + option.metavar or option.dest.upper() + + return "%3s%s %s%s" % (" ".join (option._short_opts), + sep, + " ".join (option._long_opts), + metavar) + + def format_usage(self, usage): + return _("Usage: %s\n") % usage + + def format_description(self, description): + return description + +def option_parser (): + p = optparse.OptionParser(usage='musicxml2ly FILE.xml', + version = """%prog (LilyPond) @TOPLEVEL_VERSION@ -tree = musicxml.read_musicxml (sys.argv[1]) -parts = tree.get_typed_children (musicxml.Part) +This program is free software. It is covered by the GNU General Public +License and you are welcome to change it and/or distribute copies of it +under certain conditions. Invoke as `lilypond --warranty' for more +information. -voices = get_all_voices (parts) -for (k,v) in voices.items(): - print '%s = \n' % k - v.print_ly (printer.dump) - printer.newline() +Copyright (c) 2005 by + Han-Wen Nienhuys and + Jan Nieuwenhuizen +""", + + description = + """Convert MusicXML file to LilyPond input. +""" + ) + p.add_option ('-v', '--verbose', + action = "store_true", + dest = 'verbose', + help = 'be verbose') + p.add_option ('-o', '--output', + metavar = 'FILE', + action = "store", + default = None, + type = 'string', + dest = 'output', + help = 'set output file') + + p.add_option_group ('', description = '''Report bugs via http://post.gmane.org/post.php?group=gmane.comp.gnu.lilypond.bugs +''') + p.formatter = NonDentedHeadingFormatter () + return p + + +def convert (filename, output_name): + printer = musicexp.Output_printer() + tree = musicxml.read_musicxml (filename) + parts = tree.get_typed_children (musicxml.Part) + + voices = get_all_voices (parts) + + + if output_name: + printer.file = open (output_name,'w') + + for (k,v) in voices.items(): + printer.dump ('%s = ' % k) + v.print_ly (printer) + printer.newline() + + return voices + + +opt_parser = option_parser() + +(options, args) = opt_parser.parse_args () +if options.version: + opt_parser.print_version() + sys.exit (0) +if not args: + opt_parser.print_usage() + sys.exit (2) +voices = convert (args[0], options.output) -- 2.39.2