]> git.donarmstrong.com Git - lilypond.git/blobdiff - python/musicexp.py
MusicXML: Correctly convert nested staff/part groups
[lilypond.git] / python / musicexp.py
index abe3125955437bd3e7352372c3a5f518aac9c95e..050ae66c03daed3ebeaa162d38ae673e53ffe505 100644 (file)
@@ -5,6 +5,19 @@ import re
 
 from rational import Rational
 
+
+def escape_instrument_string (input_string):
+    retstring = string.replace (input_string, "\"", "\\\"")
+    if re.match ('.*\n.*', retstring):
+        strings = retstring.split ('\r\n')
+        retstring = "\\markup { \\column { "
+        for s in strings:
+            retstring += "\\line {\"" + s + "\"} "
+        retstring += "} }"
+    else:
+        retstring = "\"" + retstring + "\""
+    return retstring
+
 class Output_stack_element:
     def __init__ (self):
         self.factor = Rational (1)
@@ -62,8 +75,14 @@ class Output_printer:
         self._line += str
 
     def unformatted_output (self, str):
-        self._nesting += str.count ('<') + str.count ('{')
-        self._nesting -= str.count ('>') + str.count ('}')
+        # don't indent on \< and indent only once on <<
+        self._nesting += ( str.count ('<') 
+                         - str.count ('\<') - str.count ('<<') 
+                         + str.count ('{') )
+        self._nesting -= ( str.count ('>') - str.count ('\>') - str.count ('>>')
+                                           - str.count ('->') - str.count ('_>')
+                                           - str.count ('^>')
+                         + str.count ('}') )
         self.print_verbatim (str)
         
     def print_duration_string (self, str):
@@ -129,7 +148,10 @@ class Duration:
         str = '%d%s' % (1 << self.duration_log, '.'*self.dots)
 
         if factor <> Rational (1,1):
-            str += '*%d/%d' % (factor.numerator (), factor.denominator ())
+            if factor.denominator () <> 1:
+                str += '*%d/%d' % (factor.numerator (), factor.denominator ())
+            else:
+                str += '*%d' % factor.numerator ()
 
         return str
     
@@ -389,17 +411,31 @@ class NestedMusic(Music):
         return None
         
 class SequentialMusic (NestedMusic):
-    def print_ly (self, printer):
+    def get_last_event_chord (self):
+        value = None
+        at = len( self.elements ) - 1
+        while (at >= 0 and
+               not isinstance (self.elements[at], EventChord) and
+               not isinstance (self.elements[at], BarLine)):
+            at -= 1
+
+        if (at >= 0 and isinstance (self.elements[at], EventChord)):
+            value = self.elements[at]
+        return value
+
+    def print_ly (self, printer, newline = True):
         printer ('{')
         if self.comment:
             self.print_comment (printer)
 
-        printer.newline()
+        if newline:
+            printer.newline()
         for e in self.elements:
             e.print_ly (printer)
 
         printer ('}')
-        printer.newline()
+        if newline:
+            printer.newline()
             
     def lisp_sub_expression (self, pred):
         name = self.name()
@@ -414,6 +450,36 @@ class SequentialMusic (NestedMusic):
             e.set_start (start)
             start += e.get_length()
 
+class RepeatedMusic:
+    def __init__ (self):
+        self.repeat_type = "volta"
+        self.repeat_count = 2
+        self.endings = []
+        self.music = None
+    def set_music (self, music):
+        if isinstance (music, Music):
+            self.music = music
+        elif isinstance (music, list):
+            self.music = SequentialMusic ()
+            self.music.elements = music
+        else:
+            sys.stderr.write ("WARNING: Unable to set the music %s for the repeat %s" % (music, self))
+    def add_ending (self, music):
+        self.endings.append (music)
+    def print_ly (self, printer):
+        printer.dump ('\\repeat %s %s' % (self.repeat_type, self.repeat_count))
+        if self.music:
+            self.music.print_ly (printer)
+        else:
+            sys.stderr.write ("WARNING: Encountered repeat without body\n")
+            printer.dump ('{}')
+        if self.endings:
+            printer.dump ('\\alternative {')
+            for e in self.endings:
+                e.print_ly (printer)
+            printer.dump ('}')
+
+
 class Lyrics:
     def __init__ (self):
         self.lyrics_syllables = []
@@ -432,7 +498,37 @@ class Lyrics:
         return lstr
 
 
-class EventChord(NestedMusic):
+class Header:
+    def __init__ (self):
+        self.header_fields = {}
+    def set_field (self, field, value):
+        self.header_fields[field] = value
+
+    def print_ly (self, printer):
+        printer.dump ("\header {")
+        printer.newline ()
+        for (k,v) in self.header_fields.items ():
+            if v:
+                printer.dump ('%s = %s' % (k,v))
+                printer.newline ()
+        printer.dump ("}")
+        printer.newline ()
+        printer.newline ()
+
+
+
+
+class EventChord (NestedMusic):
+    def __init__ (self):
+        NestedMusic.__init__ (self)
+        self.grace_elements = None
+        self.grace_type = None
+    def append_grace (self, element):
+        if element:
+            if not self.grace_elements:
+                self.grace_elements = SequentialMusic ()
+            self.grace_elements.append (element)
+
     def get_length (self):
         l = Rational (0)
         for e in self.elements:
@@ -450,6 +546,14 @@ class EventChord(NestedMusic):
         other_events = [e for e in self.elements if
                 not isinstance (e, RhythmicEvent)]
 
+        if self.grace_elements and self.elements:
+            if self.grace_type:
+                printer ('\\%s' % self.grace_type)
+            else:
+                printer ('\\grace')
+            # don't print newlines after the { and } braces
+            self.grace_elements.print_ly (printer, False)
+
         if rest_events:
             rest_events[0].print_ly (printer)
         elif len (note_events) == 1:
@@ -466,21 +570,35 @@ class EventChord(NestedMusic):
 
         self.print_comment (printer)
             
+class Partial (Music):
+    def __init__ (self):
+        Music.__init__ (self)
+        self.partial = None
+    def print_ly (self, printer):
+        if self.partial:
+            printer.dump ("\\partial %s" % self.partial.ly_expression ())
 
-class BarCheck (Music):
+class BarLine (Music):
     def __init__ (self):
         Music.__init__ (self)
         self.bar_number = 0
+        self.type = None
         
     def print_ly (self, printer):
+        bar_symbol = { 'regular': "|", 'dotted': ":", 'dashed': ":",
+                       'heavy': "|", 'light-light': "||", 'light-heavy': "|.",
+                       'heavy-light': ".|", 'heavy-heavy': ".|.", 'tick': "'",
+                       'short': "'", 'none': "" }.get (self.type, None)
+        if bar_symbol <> None:
+            printer.dump ('\\bar "%s"' % bar_symbol)
+        else:
+            printer.dump ("|")
+
         if self.bar_number > 0 and (self.bar_number % 10) == 0:
-            printer.dump ("|  \\barNumberCheck #%d " % self.bar_number)
-            printer.newline ()
+            printer.dump ("\\barNumberCheck #%d " % self.bar_number)
         else:
-            printer.dump ("| ")
             printer.print_verbatim (' %% %d' % self.bar_number)
-            printer.newline ()
+        printer.newline ()
 
     def ly_expression (self):
         return " | "
@@ -489,25 +607,85 @@ class Event(Music):
     pass
 
 class SpanEvent (Event):
-    def __init__(self):
+    def __init__ (self):
         Event.__init__ (self)
-        self.span_direction = 0
+        self.span_direction = 0 # start/stop
+        self.line_type = 'solid'
+        self.span_type = 0 # e.g. cres/decrescendo, ottava up/down
+        self.size = 0 # size of e.g. ocrave shift
+    def wait_for_note (self):
+        return True
     def get_properties(self):
         return "'span-direction  %d" % self.span_direction
-    
+    def set_span_type (self, type):
+        self.span_type = type
+
 class SlurEvent (SpanEvent):
     def ly_expression (self):
-        return {-1: '(',
-            0:'',
+        before = ''
+        after = ''
+        # TODO: setting dashed/dotted line style does not work, because that
+        #       command needs to be written before the note, not when the
+        #       event is observed after the note!
+        #before = {'dotted': '\\slurDotted', 
+        #          'dashed' : '\\slurDashed'}.get (self.line_type, '')
+        #if before:
+            #after = '\\slurSolid'
+
+        return {-1: before + '(' + after,
             1:')'}.get (self.span_direction, '')
 
 class BeamEvent (SpanEvent):
     def ly_expression (self):
         return {-1: '[',
-            0:'',
             1:']'}.get (self.span_direction, '')
 
+class PedalEvent (SpanEvent):
+    def ly_expression (self):
+        return {-1: '\\sustainDown',
+            1:'\\sustainUp'}.get (self.span_direction, '')
+
+# type==-1 means octave up, type==-2 means octave down
+class OctaveShiftEvent (SpanEvent):
+    def wait_for_note (self):
+        return False;
+    def set_span_type (self, type):
+        self.span_type = {'up': 1, 'down': -1}.get (type, 0)
+    def ly_octave_shift_indicator (self):
+        # convert 8/15 to lilypond indicators (+-1/+-2)
+        value = {8: 1, 15: 2}.get (self.size, 0)
+        # negative values go up!
+        value *= -1*self.span_type
+        return value
+    def ly_expression (self):
+        dir = self.ly_octave_shift_indicator ()
+        value = ''
+        if dir:
+            value = '#(set-octavation %s)' % dir
+        return { 
+            -1: value,
+            1: '#(set-octavation 0)'}.get (self.span_direction, '')
+
+class TrillSpanEvent (SpanEvent):
+    def ly_expression (self):
+        return {-1: '\\startTrillSpan',
+            0: '', # no need to write out anything for type='continue'
+            1:'\\stopTrillSpan'}.get (self.span_direction, '')
+
+class GlissandoEvent (SpanEvent):
+    def ly_expression (self):
+        style = ''
+        # TODO: wavy-line glissandos don't work, becasue the style has to be
+        #       set before the note, at the \glissando it's already too late!
+        #if self.line_type == 'wavy':
+            #style = "\once\override Glissando #'style = #'zigzag"
+        # In lilypond, glissando is NOT a spanner, unlike MusicXML.
+        return {-1: style + '\\glissando',
+            1:''}.get (self.span_direction, '')
+
 class ArpeggioEvent(Event):
+    def wait_for_note (self):
+        return True;
     def ly_expression (self):
         return ('\\arpeggio')
 
@@ -517,11 +695,14 @@ class TieEvent(Event):
         return '~'
 
 
-class HairpinEvent (Event):
-    def __init__ (self, type):
-        self.type = type
+class HairpinEvent (SpanEvent):
+    def set_span_type (self, type):
+        self.span_type = {'crescendo' : 1, 'decrescendo' : -1, 'diminuendo' : -1 }.get (type, 0)
     def hairpin_to_ly (self):
-        return { 0: '\!', 1: '\<', -1: '\>' }.get (self.type, '')
+        if self.span_direction == 1:
+            return '\!'
+        else:
+            return {1: '\<', -1: '\>'}.get (self.span_type, '')
     
     def ly_expression (self):
         return self.hairpin_to_ly ()
@@ -540,13 +721,15 @@ class DynamicsEvent (Event):
                                     "mp", "mf", 
                                     "f", "ff", "fff", "ffff", 
                                     "fp", "sf", "sff", "sp", "spp", "sfz", "rfz" ];
+    def wait_for_note (self):
+        return True;
     def ly_expression (self):
         if self.type == None:
             return;
         elif self.type in self.available_commands:
             return '\%s' % self.type
         else:
-            return '\markup{ \dynamic %s }' % self.type
+            return '-\markup{ \dynamic %s }' % self.type
         
     def print_ly (self, printer):
         if self.type == None:
@@ -554,13 +737,32 @@ class DynamicsEvent (Event):
         elif self.type in self.available_commands:
             printer.dump ("\\%s" % self.type)
         else:
-            printer.dump ("\\markup{ \\dynamic %s }" % self.type)
+            printer.dump ("-\\markup{ \\dynamic %s }" % self.type)
 
 
+class TextEvent (Event):
+    def __init__ (self):
+        self.Text = None
+        self.force_direction = None
+        self.markup = ''
+    def wait_for_note (self):
+        return True
+
+    def direction_mod (self):
+        return { 1: '^', -1: '_', 0: '-' }.get (self.force_direction, '-')
+
+    def ly_expression (self):
+        base_string = '%s\"%s\"'
+        if self.markup:
+            base_string = '%s\markup{ ' + self.markup + ' {%s} }'
+        return base_string % (self.direction_mod (), self.text)
+
 class ArticulationEvent (Event):
     def __init__ (self):
         self.type = None
         self.force_direction = None
+    def wait_for_note (self):
+        return True;
 
     def direction_mod (self):
         return { 1: '^', -1: '_', 0: '-' }.get (self.force_direction, '')
@@ -568,17 +770,78 @@ class ArticulationEvent (Event):
     def ly_expression (self):
         return '%s\\%s' % (self.direction_mod (), self.type)
 
+class ShortArticulationEvent (ArticulationEvent):
+    def direction_mod (self):
+        # default is -
+        return { 1: '^', -1: '_', 0: '-' }.get (self.force_direction, '-')
+    def ly_expression (self):
+        return '%s%s' % (self.direction_mod (), self.type)
+
+class NoDirectionArticulationEvent (ArticulationEvent):
+    def ly_expression (self):
+        return '\\%s' % self.type
+
+class MarkupEvent (ShortArticulationEvent):
+    def __init__ (self):
+        ArticulationEvent.__init__ (self)
+        self.contents = None
+    def ly_expression (self):
+        if self.contents:
+            return "%s\\markup { %s }" % (self.direction_mod (), self.contents)
+        else:
+            return ''
 
-class TremoloEvent (Event):
+class FretEvent (MarkupEvent):
     def __init__ (self):
-        self.bars = 0;
+        MarkupEvent.__init__ (self)
+        self.force_direction = 1
+        self.strings = 6
+        self.frets = 4
+        self.barre = None
+        self.elements = []
+    def ly_expression (self):
+        val = ""
+        if self.strings <> 6:
+            val += "w:%s;" % self.strings
+        if self.frets <> 4:
+            val += "h:%s;" % self.frets
+        if self.barre and len (self.barre) >= 3:
+            val += "c:%s-%s-%s;" % (self.barre[0], self.barre[1], self.barre[2])
+        have_fingering = False
+        for i in self.elements:
+            if len (i) > 1:
+                val += "%s-%s" % (i[0], i[1])
+            if len (i) > 2:
+                have_fingering = True
+                val += "-%s" % i[2]
+            val += ";"
+        if have_fingering:
+            val = "f:1;" + val
+        if val:
+            return "%s\\markup { \\fret-diagram #\"%s\" }" % (self.direction_mod (), val)
+        else:
+            return ''
+
+class TremoloEvent (ArticulationEvent):
+    def __init__ (self):
+        Event.__init__ (self)
+        self.bars = 0
 
     def ly_expression (self):
         str=''
-        if self.bars > 0:
+        if self.bars and self.bars > 0:
             str += ':%s' % (2 ** (2 + string.atoi (self.bars)))
         return str
 
+class BendEvent (ArticulationEvent):
+    def __init__ (self):
+        Event.__init__ (self)
+        self.alter = 0
+    def ly_expression (self):
+        if self.alter:
+            return "-\\bendAfter #%s" % self.alter
+        else:
+            return ''
 
 class RhythmicEvent(Event):
     def __init__ (self):
@@ -593,12 +856,23 @@ class RhythmicEvent(Event):
                 % self.duration.lisp_expression ())
     
 class RestEvent (RhythmicEvent):
+    def __init__ (self):
+        RhythmicEvent.__init__ (self)
+        self.pitch = None
     def ly_expression (self):
-        return 'r%s' % self.duration.ly_expression ()
+        if self.pitch:
+            return "%s%s\\rest" % (self.pitch.ly_expression (), self.duration.ly_expression ())
+        else:
+            return 'r%s' % self.duration.ly_expression ()
     
     def print_ly (self, printer):
-        printer('r')
-        self.duration.print_ly (printer)
+        if self.pitch:
+            self.pitch.print_ly (printer)
+            self.duration.print_ly (printer)
+            printer ('\\rest')
+        else:
+            printer('r')
+            self.duration.print_ly (printer)
 
 class SkipEvent (RhythmicEvent):
     def ly_expression (self):
@@ -613,7 +887,7 @@ class NoteEvent(RhythmicEvent):
         self.forced_accidental = False
         
     def get_properties (self):
-        str = RhythmicEvent.get_properties ()
+        str = RhythmicEvent.get_properties (self)
         
         if self.pitch:
             str += self.pitch.lisp_expression ()
@@ -678,10 +952,27 @@ class ClefChange (Music):
     def __init__ (self):
         Music.__init__ (self)
         self.type = 'G'
-        
-    
+        self.position = 2
+        self.octave = 0
+
+    def octave_modifier (self):
+        return {1: "^8", 2: "^15", -1: "_8", -2: "_15"}.get (self.octave, '')
+    def clef_name (self):
+        return {('G', 2): "treble",
+                ('G', 1): "french",
+                ('C', 1): "soprano",
+                ('C', 2): "mezzosoprano",
+                ('C', 3): "alto",
+                ('C', 4): "tenor",
+                ('C', 5): "baritone",
+                ('F', 3): "varbaritone",
+                ('F', 4): "bass",
+                ('F', 5): "subbass",
+                ("percussion", 2): "percussion",
+                ("TAB", 5): "tab"}.get ((self.type, self.position), None)
     def ly_expression (self):
-        return '\\clef "%s"' % self.type
+        return '\\clef "%s%s"' % (self.clef_name (), self.octave_modifier ())
+
     clef_dict = {
         "G": ("clefs.G", -2, -6),
         "C": ("clefs.C", 0, 0),
@@ -689,7 +980,10 @@ class ClefChange (Music):
         }
     
     def lisp_expression (self):
-        (glyph, pos, c0) = self.clef_dict.get (self.type)
+        try:
+            (glyph, pos, c0) = self.clef_dict[self.type]
+        except KeyError:
+            return ""
         clefsetting = """
         (make-music 'SequentialMusic
         'elements (list
@@ -703,6 +997,17 @@ class ClefChange (Music):
         return clefsetting
 
 
+class StaffChange (Music):
+    def __init__ (self, staff):
+        Music.__init__ (self)
+        self.staff = staff
+    def ly_expression (self):
+        if self.staff:
+            return "\\change Staff=\"%s\"" % self.staff
+        else:
+            return ''
+
+
 class MultiMeasureRest(Music):
 
     def lisp_expression (self):
@@ -725,6 +1030,116 @@ class MultiMeasureRest(Music):
         return 'R%s' % self.duration.ly_expression ()
 
 
+class StaffGroup:
+    def __init__ (self, command = "StaffGroup"):
+        self.stafftype = command
+        self.id = None
+        self.instrument_name = None
+        self.short_instrument_name = None
+        self.symbol = None
+        self.spanbar = None
+        self.children = []
+        self.is_group = True
+        # part_information is a list with entries of the form
+        #     [staffid, voicelist]
+        # where voicelist is a list with entries of the form
+        #     [voiceid1, [lyricsid11, lyricsid12,...] ]
+        self.part_information = None
+
+    def append_staff (self, staff):
+        self.children.append (staff)
+
+    def set_part_information (self, part_name, staves_info):
+        if part_name == self.id:
+            self.part_information = staves_info
+        else:
+            for c in self.children:
+                c.set_part_information (part_name, staves_info)
+
+    def print_ly_contents (self, printer):
+        for c in self.children:
+            if c:
+                c.print_ly (printer)
+    def print_ly_overrides (self, printer):
+        needs_with = False
+        needs_with |= self.spanbar == "no"
+        needs_with |= self.instrument_name != None
+        needs_with |= self.short_instrument_name != None
+        needs_with |= (self.symbol != None) and (self.symbol != "bracket")
+        if needs_with:
+            printer.dump ("\\with {")
+            if self.instrument_name or self.short_instrument_name:
+                printer.dump ("\\consists \"Instrument_name_engraver\"")
+            if self.spanbar == "no":
+                printer.dump ("\\override SpanBar #'transparent = ##t")
+            brack = {"brace": "SystemStartBrace",
+                     "none": "f",
+                     "line": "SystemStartSquare"}.get (self.symbol, None)
+            if brack:
+                printer.dump ("systemStartDelimiter = #'%s" % brack)
+            printer.dump ("}")
+
+    def print_ly (self, printer):
+        if self.stafftype:
+            printer.dump ("\\new %s" % self.stafftype)
+        self.print_ly_overrides (printer)
+        printer.dump ("<<")
+        printer.newline ()
+        if self.stafftype and self.instrument_name:
+            printer.dump ("\\set %s.instrumentName = %s" % (self.stafftype, 
+                    escape_instrument_string (self.instrument_name)))
+            printer.newline ()
+        if self.stafftype and self.short_instrument_name:
+            printer.dump ("\\set %s.shortInstrumentName = %s" % (self.stafftype,
+                    escape_instrument_string (self.short_instrument_name)))
+            printer.newline ()
+        self.print_ly_contents (printer)
+        printer.newline ()
+        printer.dump (">>")
+        printer.newline ()
+
+
+class Staff (StaffGroup):
+    def __init__ (self):
+        StaffGroup.__init__ (self, "Staff")
+        self.is_group = False
+        self.part = None
+
+    def print_ly_overrides (self, printer):
+        pass
+
+    def print_ly_contents (self, printer):
+        if not self.id or not self.part_information:
+            return
+
+        for [staff_id, voices] in self.part_information:
+            if staff_id:
+                printer ('\\context Staff = "%s" << ' % staff_id)
+            else:
+                printer ('\\context Staff << ')
+            printer.newline ()
+            n = 0
+            nr_voices = len (voices)
+            for [v, lyrics] in voices:
+                n += 1
+                voice_count_text = ''
+                if nr_voices > 1:
+                    voice_count_text = {1: ' \\voiceOne', 2: ' \\voiceTwo',
+                                        3: ' \\voiceThree'}.get (n, ' \\voiceFour')
+                printer ('\\context Voice = "%s" {%s \\%s }' % (v,voice_count_text,v))
+                printer.newline ()
+
+                for l in lyrics:
+                    printer ('\\new Lyrics \\lyricsto "%s" \\%s' % (v,l))
+                    printer.newline()
+            printer ('>>')
+
+    def print_ly (self, printer):
+        if self.part_information and len (self.part_information) > 1:
+            self.stafftype = "PianoStaff"
+        StaffGroup.print_ly (self, printer)
+
+
 def test_pitch ():
     bflat = Pitch()
     bflat.alteration = -1