X-Git-Url: https://svn.cri.ensmp.fr/git/minwii.git/blobdiff_plain/719344253552583b33d14ea672d9909b012d78bd..1711aaae1c2b3397d739a0669df124e0534711b0:/src/songs/musicxmltosong.py diff --git a/src/songs/musicxmltosong.py b/src/songs/musicxmltosong.py index de8e26a..1724fc4 100755 --- a/src/songs/musicxmltosong.py +++ b/src/songs/musicxmltosong.py @@ -1,14 +1,234 @@ +# -*- coding: utf-8 -*- """ converstion 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 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} +_marker = [] +class Part(object) : + + 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) + self.notes.append(note) + try : + self.notes[noteIndex-1].next = note + except IndexError: + pass + 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 : + repeats = len(verse[0].lyrics) + if repeats > 1 : + for i in range(repeats) : + # couplet + for note in verse : + yield note, i + # 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.name, note.midi, note.duration, note.lyrics[verseIndex] + + + +class Note(object) : + def __init__(self, node, divisions, previous) : + 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)) + + self.divisions = divisions + self.previous = previous + self.next = None + + @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 + + +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__ + + + + +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, output, 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() + + # 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) + + def main() : - pass + usage = "%prog musicXmlFile.xml outputSongFile.smwi [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) != 2 : + raise SystemExit(op.format_help()) + + musicXml2Song(args[0], args[1], partIndex=options.partIndex, printNotes=options.printNotes) + if __name__ == '__main__' : - main() \ No newline at end of file + sys.exit(main())