Création d'une classe Tone dont hérite Note.
[minwii.git] / src / songs / musicxmltosong.py
index 1724fc4..7eedf53 100755 (executable)
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 """
 # -*- coding: utf-8 -*-
 """
-converstion d'un fichier musicxml en objet song minwii.
+conversion d'un fichier musicxml en objet song minwii.
 
 $Id$
 $URL$
 
 $Id$
 $URL$
@@ -9,7 +9,8 @@ import sys
 from types import StringTypes
 from xml.dom.minidom import parse
 from optparse import OptionParser
 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
 
 # Do4 <=> midi 60
 OCTAVE_REF = 4
@@ -20,13 +21,41 @@ DIATO_SCALE = {'C' : 60,
                'G' : 67,
                'A' : 69,
                'B' : 71}
                'G' : 67,
                'A' : 69,
                'B' : 71}
+
+CHROM_SCALE = {  0 : ('C',  0),
+                 1 : ('C',  1),
+                 2 : ('D',  0),
+                 3 : ('E', -1),
+                 4 : ('E',  0),
+                 5 : ('F',  0),
+                 6 : ('F',  1),
+                 7 : ('G',  0),
+                 8 : ('G',  1),
+                 9 : ('A',  0),
+                10 : ('B', -1),
+                11 : ('B',  0)}
+
+
+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) :
     
 _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 = []
     def __init__(self, node, autoDetectChorus=True) :
         self.node = node
         self.notes = []
+        self.repeats = []
         self._parseMusic()
         self.verses = [[]]
         self.chorus = []
         self._parseMusic()
         self.verses = [[]]
         self.chorus = []
@@ -36,20 +65,35 @@ class Part(object) :
     
     def _parseMusic(self) :
         divisions = 0
     
     def _parseMusic(self) :
         divisions = 0
-        noteIndex = 0
-        next = previous = None
+        previous = None
+
         for measureNode in self.node.getElementsByTagName('measure') :
         for measureNode in self.node.getElementsByTagName('measure') :
+            measureNotes = []
+            
+            # iteration sur les notes
             # divisions de la noire
             divisions = int(_getNodeValue(measureNode, 'attributes/divisions', divisions))
             for noteNode in measureNode.getElementsByTagName('note') :
                 note = Note(noteNode, divisions, previous)
             # divisions de la noire
             divisions = int(_getNodeValue(measureNode, 'attributes/divisions', divisions))
             for noteNode in measureNode.getElementsByTagName('note') :
                 note = Note(noteNode, divisions, previous)
-                self.notes.append(note)
-                try :
-                    self.notes[noteIndex-1].next = note
-                except IndexError:
-                    pass
+                if not note.isRest :
+                    measureNotes.append(note)
+                    if previous :
+                        previous.next = note
+                else :
+                    previous.addDuration(note)
+                    continue
                 previous = note
                 previous = note
-                noteIndex += 1
+            self.notes.extend(measureNotes)
+            
+            # barres de reprises
+            try :
+                barlineNode = measureNode.getElementsByTagName('barline')[0]
+            except IndexError :
+                continue
+            
+            barline = Barline(barlineNode, measureNotes)
+            if barline.repeat :
+                self.repeats.append(barline)
 
     def _findChorus(self):
         """ le refrain correspond aux notes pour lesquelles
 
     def _findChorus(self):
         """ le refrain correspond aux notes pour lesquelles
@@ -78,16 +122,24 @@ class Part(object) :
         verse.append(self.notes[-1])
         
     
         verse.append(self.notes[-1])
         
     
-    def iterNotes(self) :
+    def iterNotes(self, indefinitely=True) :
         "exécution de la chanson avec l'alternance couplets / refrains"
         "exécution de la chanson avec l'alternance couplets / refrains"
-        for verse in self.verses :
+        print 'indefinitely', indefinitely
+        if indefinitely == False :
+            iterable = self.verses
+        else :
+            iterable = cycle(self.verses)
+        for verse in iterable :
+            print "---partie---"
             repeats = len(verse[0].lyrics)
             if repeats > 1 :
                 for i in range(repeats) :
                     # couplet
             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
                     for note in verse :
                         yield note, i
                     # refrain
+                    print "---refrain---"
                     for note in self.chorus :
                         yield note, 0
             else :
                     for note in self.chorus :
                         yield note, 0
             else :
@@ -95,36 +147,75 @@ class Part(object) :
                     yield note, 0
         
     def pprint(self) :
                     yield note, 0
         
     def pprint(self) :
-        for note, verseIndex in self.iterNotes() :
-            print note.name, note.midi, note.duration, note.lyrics[verseIndex]
-        
-        
+        for note, verseIndex in self.iterNotes(indefinitely=False) :
+            print note, note.lyrics[verseIndex]
 
 
-class Note(object) :
-    def __init__(self, node, divisions, previous) :
+
+    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 Barline(object) :
+
+    def __init__(self, node, measureNotes) :
         self.node = node
         self.node = node
-        self.step = _getNodeValue(node, 'pitch/step')
-        self.octave = int(_getNodeValue(node, 'pitch/octave'))
-        self.alter = int(_getNodeValue(node, 'pitch/alter', 0))
-        self._duration = float(_getNodeValue(node, 'duration'))
-        self.lyrics = []
-        for ly in node.getElementsByTagName('lyric') :
-            self.lyrics.append(Lyric(ly))
+        location = self.location = node.getAttribute('location') or 'right'
+        try :
+            repeatN = node.getElementsByTagName('repeat')[0]
+            repeat = {'direction' : repeatN.getAttribute('direction'),
+                      'times' : int(repeatN.getAttribute('times') or 1)}
+            if location == 'left' :
+                repeat['note'] = measureNotes[0]
+            elif location == 'right' :
+                repeat['note'] = measureNotes[-1]
+            else :
+                raise ValueError(location)
+            self.repeat = repeat
+        except IndexError :
+            self.repeat = None
+    
+    def __str__(self)  :
+        if self.repeat :
+            if self.location == 'left' :
+                return '|:'
+            elif self.location == 'right' :
+                return ':|'
+        return '|'
 
 
-        self.divisions = divisions
-        self.previous = previous
-        self.next = None
+    __repr__ = __str__
+
+
+class Tone(object) :
+    
+    @staticmethod
+    def midi_to_step_alter_octave(midi):
+        stepIndex = midi % 12
+        step, alter = CHROM_SCALE[stepIndex]
+        octave = midi / 12 - 1
+        return step, alter, octave
+    
     
     
+    def __init__(self, *args) :
+        if len(args) == 3 :
+            self.step, self.alter, self.octave = args
+        elif len(args) == 1 :
+            midi = args[0]
+            self.step, self.alter, self.octave = Tone.midi_to_step_alter_octave(midi)
+
     @property
     def midi(self) :
         mid = DIATO_SCALE[self.step]
         mid = mid + (self.octave - OCTAVE_REF) * 12
         mid = mid + self.alter
         return mid
     @property
     def midi(self) :
         mid = DIATO_SCALE[self.step]
         mid = mid + (self.octave - OCTAVE_REF) * 12
         mid = mid + self.alter
         return mid
-    
-    @property
-    def duration(self) :
-        return self._duration / self.divisions
+
     
     @property
     def name(self) :
     
     @property
     def name(self) :
@@ -135,6 +226,87 @@ class Note(object) :
             alterext = '#'
         name = '%s%s' % (name, abs(self.alter) * alterext)
         return name
             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
+        
+        
+
+class Note(Tone) :
+    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.lyrics = []
+        for ly in node.getElementsByTagName('lyric') :
+            self.lyrics.append(Lyric(ly))
+
+        self.divisions = divisions
+        self.previous = previous
+        self.next = None
+    
+    def __str__(self) :
+        return (u'%5s %2s %2d %4s' % (self.nom, self.name, self.midi, round(self.duration, 2))).encode('utf-8')
+    
+    def __repr__(self) :
+        return self.name.encode('utf-8')
+    
+    def addDuration(self, note) :
+        self._duration = self.duration + note.duration
+        self.divisions = 1
+    
+#    @property
+#    def midi(self) :
+#        mid = DIATO_SCALE[self.step]
+#        mid = mid + (self.octave - OCTAVE_REF) * 12
+#        mid = mid + self.alter
+#        return mid
+    
+    @property
+    def duration(self) :
+        return self._duration / self.divisions
+    
+#    @property
+#    def name(self) :
+#        name = '%s%d' % (self.step, self.octave)
+#        if self.alter < 0 :
+#            alterext = 'b'
+#        else :
+#            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) :
     
 
 class Lyric(object) :
@@ -151,9 +323,12 @@ class Lyric(object) :
         self.syllabic = _getNodeValue(node, 'syllabic', 'single')
         self.text = _getNodeValue(node, 'text')
     
         self.syllabic = _getNodeValue(node, 'syllabic', 'single')
         self.text = _getNodeValue(node, 'text')
     
-    def __str__(self) :
+    def syllabus(self, encoding='utf-8'):
         text = self._syllabicModifiers[self.syllabic] % self.text
         text = self._syllabicModifiers[self.syllabic] % self.text
-        return text.encode('utf-8')
+        return text.encode(encoding)
+    
+    def __str__(self) :
+        return self.syllabus()
     __repr__  = __str__
         
         
     __repr__  = __str__
         
         
@@ -170,7 +345,7 @@ def _getNodeValue(node, path, default=_marker) :
         else :
             return default
 
         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')
     
     if isinstance(input, StringTypes) :
         input = open(input, 'r')
     
@@ -187,31 +362,13 @@ def musicXml2Song(input, output, partIndex=0, printNotes=False) :
     
     if printNotes :
         part.pprint()
     
     if printNotes :
         part.pprint()
-    
-    # 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)
+
+    return part
+
     
     
 def main() :
     
     
 def main() :
-    usage = "%prog musicXmlFile.xml outputSongFile.smwi [options]"
+    usage = "%prog musicXmlFile.xml [options]"
     op = OptionParser(usage)
     op.add_option("-i", "--part-index", dest="partIndex"
                  , default = 0
     op = OptionParser(usage)
     op.add_option("-i", "--part-index", dest="partIndex"
                  , default = 0
@@ -223,10 +380,10 @@ def main() :
     
     options, args = op.parse_args()
     
     
     options, args = op.parse_args()
     
-    if len(args) != 2 :
+    if len(args) != 1 :
         raise SystemExit(op.format_help())
     
         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)