Prise en charge des pauses / soupirs etc : on augmente la durée de la note précédente.
[minwii.git] / src / songs / musicxmltosong.py
index 3de33ca..826363e 100755 (executable)
@@ -9,7 +9,8 @@ import sys
 from types import StringTypes
 from xml.dom.minidom import parse
 from optparse import OptionParser
-from Song import Song
+from itertools import cycle
+#from Song import Song
 
 # Do4 <=> midi 60
 OCTAVE_REF = 4
@@ -20,18 +21,146 @@ DIATO_SCALE = {'C' : 60,
                'G' : 67,
                'A' : 69,
                'B' : 71}
+
+FR_NOTES = {'C' : u'Do',
+            'D' : u'Ré',
+            'E' : u'Mi',
+            'F' : u'Fa',
+            'G' : u'Sol',
+            'A' : u'La',
+            'B' : u'Si'}
+
 _marker = []
 
+class Part(object) :
+    
+    requiresExtendedScale = False
+    scale = [55, 57, 59, 60, 62, 64, 65, 67, 69, 71, 72]
+    quarterNoteLength = 400
+    
+    def __init__(self, node, autoDetectChorus=True) :
+        self.node = node
+        self.notes = []
+        self._parseMusic()
+        self.verses = [[]]
+        self.chorus = []
+        if autoDetectChorus :
+            self._findChorus()
+        self._findVersesLoops()
+    
+    def _parseMusic(self) :
+        divisions = 0
+        noteIndex = 0
+        next = previous = None
+        for measureNode in self.node.getElementsByTagName('measure') :
+            # divisions de la noire
+            divisions = int(_getNodeValue(measureNode, 'attributes/divisions', divisions))
+            for noteNode in measureNode.getElementsByTagName('note') :
+                note = Note(noteNode, divisions, previous)
+                if not note.isRest :
+                    self.notes.append(note)
+                    try :
+                        self.notes[noteIndex-1].next = note
+                    except IndexError:
+                        pass
+                else :
+                    previous.addDuration(note)
+                    continue
+                    
+                previous = note
+                noteIndex += 1
+
+    def _findChorus(self):
+        """ le refrain correspond aux notes pour lesquelles
+            il n'existe q'une seule syllable attachée.
+        """
+        start = stop = None
+        for i, note in enumerate(self.notes) :
+            ll = len(note.lyrics)
+            if start is None and ll == 1 :
+                start = i
+            elif start is not None and ll > 1 :
+                stop = i
+                break
+        self.chorus = self.notes[start:stop]
+    
+    def _findVersesLoops(self) :
+        "recherche des couplets / boucles"
+        verse = self.verses[0]
+        for note in self.notes[:-1] :
+            verse.append(note)
+            ll = len(note.lyrics)
+            nll = len(note.next.lyrics)
+            if ll != nll :
+                verse = []
+                self.verses.append(verse)
+        verse.append(self.notes[-1])
+        
+    
+    def iterNotes(self) :
+        "exécution de la chanson avec l'alternance couplets / refrains"
+        for verse in self.verses :
+            print "---partie---"
+            repeats = len(verse[0].lyrics)
+            if repeats > 1 :
+                for i in range(repeats) :
+                    # couplet
+                    print "---couplet%d---" % i
+                    for note in verse :
+                        yield note, i
+                    # refrain
+                    print "---refrain---"
+                    for note in self.chorus :
+                        yield note, 0
+            else :
+                for note in verse :
+                    yield note, 0
+        
+    def pprint(self) :
+        for note, verseIndex in self.iterNotes() :
+            print note.nom, note.name, note.midi, note.duration, note.lyrics[verseIndex]
+
+
+    def assignNotesFromMidiNoteNumbers(self):
+        # TODO faire le mapping bande hauteur midi
+        for i in range(len(self.midiNoteNumbers)):
+            noteInExtendedScale = 0
+            while self.midiNoteNumbers[i] > self.scale[noteInExtendedScale] and noteInExtendedScale < len(self.scale)-1:
+                noteInExtendedScale += 1
+            if self.midiNoteNumbers[i]<self.scale[noteInExtendedScale]:
+                noteInExtendedScale -= 1
+            self.notes.append(noteInExtendedScale)
+
+        
+        
 
 class Note(object) :
-    def __init__(self, node, divisions) :
-        self.step = _getNodeValue(node, 'pitch/step')
-        self.octave = int(_getNodeValue(node, 'pitch/octave'))
-        self.alter = int(_getNodeValue(node, 'pitch/alter', 0))
+    scale = [55, 57, 59, 60, 62, 64, 65, 67, 69, 71, 72]
+    
+    def __init__(self, node, divisions, previous) :
+        self.node = node
+        self.isRest = False
+        self.step = _getNodeValue(node, 'pitch/step', None)
+        if self.step is not None :
+            self.octave = int(_getNodeValue(node, 'pitch/octave'))
+            self.alter = int(_getNodeValue(node, 'pitch/alter', 0))
+        elif self.node.getElementsByTagName('rest') :
+            self.isRest = True
+        else :
+            NotImplementedError(self.node.toxml('utf-8'))
+            
         self._duration = float(_getNodeValue(node, 'duration'))
-        self.lyric = _getNodeValue(node, 'lyric/text', '')
+        self.lyrics = []
+        for ly in node.getElementsByTagName('lyric') :
+            self.lyrics.append(Lyric(ly))
 
         self.divisions = divisions
+        self.previous = previous
+        self.next = None
+    
+    def addDuration(self, note) :
+        self._duration = self.duration + note.duration
+        self.divisions = 1
     
     @property
     def midi(self) :
@@ -53,6 +182,40 @@ class Note(object) :
             alterext = '#'
         name = '%s%s' % (name, abs(self.alter) * alterext)
         return name
+    
+    @property
+    def nom(self) :
+        name = FR_NOTES[self.step]
+        if self.alter < 0 :
+            alterext = 'b'
+        else :
+            alterext = '#'
+        name = '%s%s' % (name, abs(self.alter) * alterext)
+        return name
+    
+    @property
+    def column(self):
+        return self.scale.index(self.midi)
+    
+
+class Lyric(object) :
+    
+    _syllabicModifiers = {
+        'single' : '%s',
+        'begin'  : '%s -',
+        'middle' : '- %s -',
+        'end'    : '- %s'
+        }
+    
+    def __init__(self, node) :
+        self.node = node
+        self.syllabic = _getNodeValue(node, 'syllabic', 'single')
+        self.text = _getNodeValue(node, 'text')
+    
+    def __str__(self) :
+        text = self._syllabicModifiers[self.syllabic] % self.text
+        return text.encode('utf-8')
+    __repr__  = __str__
         
         
 
@@ -68,7 +231,7 @@ def _getNodeValue(node, path, default=_marker) :
         else :
             return default
 
-def musicXml2Song(input, output, partIndex=0, printNotes=False) :
+def musicXml2Song(input, partIndex=0, printNotes=False) :
     if isinstance(input, StringTypes) :
         input = open(input, 'r')
     
@@ -81,26 +244,34 @@ def musicXml2Song(input, output, partIndex=0, printNotes=False) :
     parts = doc.getElementsByTagName('part')
     leadPart = parts[partIndex]
     
+    part = Part(leadPart)
+    
+    if printNotes :
+        part.pprint()
+
+    return part
+
+    
     # divisions de la noire
-    divisions = 0
-    midiNotes, durations, lyrics = [], [], []
-
-    for measureNode in leadPart.getElementsByTagName('measure') :
-        divisions = int(_getNodeValue(measureNode, 'attributes/divisions', divisions))
-        for noteNode in measureNode.getElementsByTagName('note') :
-            note = Note(noteNode, divisions)
-            if printNotes :
-                print note.name, note.midi, note.duration, note.lyric
-            midiNotes.append(note.midi)
-            durations.append(note.duration)
-            lyrics.append(note.lyric)
-    
-    song = Song(None,
-                midiNoteNumbers = midiNotes,
-                noteLengths = durations,
-                lyrics = lyrics,
-                notesInExtendedScale=None)
-    song.save(output)
+#    divisions = 0
+#    midiNotes, durations, lyrics = [], [], []
+#
+#    for measureNode in leadPart.getElementsByTagName('measure') :
+#        divisions = int(_getNodeValue(measureNode, 'attributes/divisions', divisions))
+#        for noteNode in measureNode.getElementsByTagName('note') :
+#            note = Note(noteNode, divisions)
+#            if printNotes :
+#                print note.name, note.midi, note.duration, note.lyric
+#            midiNotes.append(note.midi)
+#            durations.append(note.duration)
+#            lyrics.append(note.lyric)
+#    
+#    song = Song(None,
+#                midiNoteNumbers = midiNotes,
+#                noteLengths = durations,
+#                lyrics = lyrics,
+#                notesInExtendedScale=None)
+#    song.save(output)
     
     
 def main() :
@@ -116,10 +287,10 @@ def main() :
     
     options, args = op.parse_args()
     
-    if len(args) != 2 :
+    if len(args) != 1 :
         raise SystemExit(op.format_help())
     
-    musicXml2Song(args[0], args[1], partIndex=options.partIndex, printNotes=options.printNotes)
+    musicXml2Song(args[0], partIndex=options.partIndex, printNotes=options.printNotes)