+# -*- coding: utf-8 -*-
"""
-converstion d'un fichier musicxml en objet song minwii.
+conversion d'un fichier musicxml en objet song minwii.
$Id$
$URL$
"""
+import sys
+from types import StringTypes
+from xml.dom.minidom import parse
+from optparse import OptionParser
+from itertools import cycle
+#from Song import Song
+# Do4 <=> midi 60
+OCTAVE_REF = 4
+DIATO_SCALE = {'C' : 60,
+ 'D' : 62,
+ 'E' : 64,
+ 'F' : 65,
+ '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.repeats = []
+ self._parseMusic()
+ self.verses = [[]]
+ self.chorus = []
+ if autoDetectChorus :
+ self._findChorus()
+ self._findVersesLoops()
+
+ def _parseMusic(self) :
+ divisions = 0
+ previous = None
+
+ 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)
+ if not note.isRest :
+ measureNotes.append(note)
+ if previous :
+ previous.next = note
+ else :
+ previous.addDuration(note)
+ continue
+ previous = note
+ 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
+ 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, indefinitely=True) :
+ "exécution de la chanson avec l'alternance couplets / refrains"
+ 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
+ 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(indefinitely=False) :
+ print note, 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 Barline(object) :
+
+ def __init__(self, node, measureNotes) :
+ self.node = node
+ 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 '|'
+
+ __repr__ = __str__
+
+
+class Note(object) :
+ 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) :
+
+ _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 syllabus(self, encoding='utf-8'):
+ text = self._syllabicModifiers[self.syllabic] % self.text
+ return text.encode(encoding)
+
+ def __str__(self) :
+ return self.syllabus()
+ __repr__ = __str__
+
+
+
+
+def _getNodeValue(node, path, default=_marker) :
+ try :
+ for name in path.split('/') :
+ node = node.getElementsByTagName(name)[0]
+ return node.firstChild.nodeValue
+ except :
+ if default is _marker :
+ raise
+ else :
+ return default
+
+def musicXml2Song(input, partIndex=0, printNotes=False) :
+ if isinstance(input, StringTypes) :
+ input = open(input, 'r')
+
+ d = parse(input)
+ doc = d.documentElement
+
+ # TODO conversion préalable score-timewise -> score-partwise
+ assert doc.nodeName == u'score-partwise'
+
+ parts = doc.getElementsByTagName('part')
+ leadPart = parts[partIndex]
+
+ part = Part(leadPart)
+
+ if printNotes :
+ part.pprint()
+
+ return part
+
+
def main() :
- pass
+ usage = "%prog musicXmlFile.xml [options]"
+ op = OptionParser(usage)
+ op.add_option("-i", "--part-index", dest="partIndex"
+ , default = 0
+ , help = "Index de la partie qui contient le champ.")
+ op.add_option("-p", '--print', dest='printNotes'
+ , action="store_true"
+ , default = False
+ , help = "Affiche les notes sur la sortie standard (debug)")
+
+ options, args = op.parse_args()
+
+ if len(args) != 1 :
+ raise SystemExit(op.format_help())
+
+ musicXml2Song(args[0], partIndex=options.partIndex, printNotes=options.printNotes)
+
if __name__ == '__main__' :
- main()
\ No newline at end of file
+ sys.exit(main())