From 8209aff7f2ebbbe437faa05ea1b7d92a27a15806 Mon Sep 17 00:00:00 2001 From: Reinhold Kainhofer Date: Wed, 2 Apr 2008 23:51:55 +0200 Subject: [PATCH] MusicXML: Add basic support for figured bass The figured bass definitionis correctly generated and written to the -defs.ly file, but not yet integrated into the score itself. For this, I'm keeping a second voice builder while going through the voices' children and insert the figured bass there and at the end extract the definition from it. --- python/musicexp.py | 52 +++++++++++++++++ python/musicxml.py | 20 +++++-- python/rational.py | 13 +++-- scripts/musicxml2ly.py | 130 ++++++++++++++++++++++++++++++++++++++--- 4 files changed, 199 insertions(+), 16 deletions(-) diff --git a/python/musicexp.py b/python/musicexp.py index 2ae8a6784f..3219c689d2 100644 --- a/python/musicexp.py +++ b/python/musicexp.py @@ -1270,6 +1270,58 @@ class StaffChange (Music): else: return '' +class FiguredBassNote (Music): + def __init__ (self): + Music.__init__ (self) + self.number = '' + self.prefix = '' + self.suffix = '' + def set_prefix (self, prefix): + self.prefix = prefix + def set_suffix (self, suffix): + self.prefix = suffix + def set_number (self, number): + self.number = number + def ly_expression (self): + res = '' + if self.number: + res += self.number + else: + res += '_' + if self.prefix: + res += self.prefix + if self.suffix: + res += self.suffix + return res + + +class FiguredBassEvent (NestedMusic): + def __init__ (self): + NestedMusic.__init__ (self) + self.duration = None + self.real_duration = 0 + self.parentheses = False + return + def set_duration (self, dur): + self.duration = dur + def set_parentheses (self, par): + self.parentheses = par + def set_real_duration (self, dur): + self.real_duration = dur + + def print_ly (self, printer): + figured_bass_events = [e for e in self.elements if + isinstance (e, FiguredBassNote)] + if figured_bass_events: + notes = [] + for x in figured_bass_events: + notes.append (x.ly_expression ()) + contents = string.join (notes) + if self.parentheses: + contents = '[%]' % contents + printer ('<%s>' % contents) + self.duration.print_ly (printer) + class MultiMeasureRest(Music): diff --git a/python/musicxml.py b/python/musicxml.py index e0612802b7..adfd1027cb 100644 --- a/python/musicxml.py +++ b/python/musicxml.py @@ -529,6 +529,12 @@ class Part (Music_xml_node): measure_position = Rational (0) for n in m.get_all_children (): + # figured bass has a duration, but applies to the next note + # and should not change the current measure position! + if isinstance (n, FiguredBass): + n._divisions = factor.denominator () + continue + if isinstance (n, Hash_text): continue dur = Rational (0) @@ -656,7 +662,8 @@ class Part (Music_xml_node): if not (voice_id or isinstance (n, Attributes) or isinstance (n, Direction) or isinstance (n, Partial) or - isinstance (n, Barline) or isinstance (n, Harmony) ): + isinstance (n, Barline) or isinstance (n, Harmony) or + isinstance (n, FiguredBass) ): continue if isinstance (n, Attributes) and not start_attr: @@ -688,9 +695,9 @@ class Part (Music_xml_node): voices[v].add_element (n) continue - if isinstance (n, Harmony): - # store the harmony element until we encounter the next note - # and assign it only to that one voice. + if isinstance (n, Harmony) or isinstance (n, FiguredBass): + # store the harmony or figured bass element until we encounter + # the next note and assign it only to that one voice. assign_to_next_note.append (n) continue @@ -880,6 +887,10 @@ class Frame_Note (Music_xml_node): else: return '' +class FiguredBass (Music_xml_node): + pass + + ## need this, not all classes are instantiated ## for every input file. Only add those classes, that are either directly @@ -902,6 +913,7 @@ class_dict = { 'duration': Duration, 'frame': Frame, 'frame-note': Frame_Note, + 'figured-bass': FiguredBass, 'glissando': Glissando, 'grace': Grace, 'harmony': Harmony, diff --git a/python/rational.py b/python/rational.py index 705829e120..5a9ee629b3 100644 --- a/python/rational.py +++ b/python/rational.py @@ -38,13 +38,18 @@ class Rational(object): raise TypeError('denominator must have integer type') if not denominator: raise ZeroDivisionError('rational construction') - # Store the fraction in reduced form as _n/_d - factor = _gcf(numerator, denominator) - self._n = numerator // factor - self._d = denominator // factor + self._d = denominator + self._n = numerator + self.normalize_self() + # Cancel the fraction to reduced form + def normalize_self(self): + factor = _gcf(self._n, self._d) + self._n = self._n // factor + self._d = self._d // factor if self._d < 0: self._n = -self._n self._d = -self._d + def numerator(self): return self._n diff --git a/scripts/musicxml2ly.py b/scripts/musicxml2ly.py index a429b75251..bfb9ab2b0e 100644 --- a/scripts/musicxml2ly.py +++ b/scripts/musicxml2ly.py @@ -426,7 +426,6 @@ def extract_score_structure (part_list, staffinfo): return structure - def musicxml_duration_to_lily (mxl_note): d = musicexp.Duration () # if the note has no Type child, then that method spits out a warning and @@ -443,16 +442,27 @@ def musicxml_duration_to_lily (mxl_note): def rational_to_lily_duration (rational_len): d = musicexp.Duration () - d.duration_log = {1: 0, 2: 1, 4:2, 8:3, 16:4, 32:5, 64:6, 128:7, 256:8, 512:9}.get (rational_len.denominator (), -1) - d.factor = Rational (rational_len.numerator ()) - if d.duration_log < 0: + + rational_len.normalize_self () + d_log = {1: 0, 2: 1, 4:2, 8:3, 16:4, 32:5, 64:6, 128:7, 256:8, 512:9}.get (rational_len.denominator (), -1) + print "d_log: %s, rational_len: %s\n" % (d_log, rational_len) + + # Duration of the form 1/2^n or 3/2^n can be converted to a simple lilypond duration + if (d_log >= 0 and rational_len.numerator() in (1,3,5,7) ): + # account for the dots! + d.dots = (rational_len.numerator()-1)/2 + d.duration_log = d_log - d.dots + elif (d_log >= 0): + d.duration_log = d_log + d.factor = Rational (rational_len.numerator ()) + else: error_message (_ ("Encountered rational duration with denominator %s, " "unable to convert to lilypond duration") % rational_len.denominator ()) # TODO: Test the above error message return None - else: - return d + + return d def musicxml_partial_to_lily (partial_len): if partial_len > 0: @@ -1233,6 +1243,59 @@ def musicxml_harmony_to_lily (n): return res +def musicxml_figured_bass_note_to_lily (n): + res = musicexp.FiguredBassNote () + suffix_dict = { 'sharp' : "+", + 'flat' : "-", + 'natural' : "!", + 'double-sharp' : "++", + 'flat-flat' : "--", + 'sharp-sharp' : "++", + 'slash' : "/" } + prefix = n.get_maybe_exist_named_child ('prefix') + if prefix: + res.set_prefix (suffix_dict.get (prefix.get_text (), "")) + fnumber = n.get_maybe_exist_typed_child ('figure-number') + if fnumber: + print "figure-number: %s, text: %s" % (fnumber, fnumber.get_text ()) + res.set_number (fnumber.get_text ()) + suffix = n.get_maybe_exist_named_child ('suffix') + if suffix: + res.set_suffix (suffix_dict.get (suffix.get_text (), "")) + if n.get_maybe_exist_named_child ('extend'): + # TODO: Implement extender lines (unfortunately, in lilypond you have + # to use \set useBassFigureExtenders = ##t, which turns them on + # globally, while MusicXML has a property for each note... + # I'm not sure there is a proper way to implement this cleanly + #n.extend + pass + return res + + + +def musicxml_figured_bass_to_lily (n): + if not isinstance (n, musicxml.FiguredBass): + return + res = musicexp.FiguredBassEvent () + for i in n.get_named_children ('figure'): + note = musicxml_figured_bass_note_to_lily (i) + if note: + res.append (note) + dur = n.get_maybe_exist_named_child ('duration') + if dur: + # TODO: implement duration (given in base steps!) + # apply the duration to res + print "duration: %s, divisions: %s" % (dur.get_text(), n._divisions) + length = Rational(int(dur.get_text()), n._divisions)*Rational(1,4) + res.set_real_duration (length) + duration = rational_to_lily_duration (length) + if duration: + res.set_duration (duration) + print "Converted to duration: %s" % duration.ly_expression () + if hasattr (n, 'parentheses') and n.parentheses == "yes": + res.set_parentheses (True) + return res + instrument_drumtype_dict = { 'Acoustic Snare Drum': 'acousticsnare', 'Side Stick': 'sidestick', @@ -1427,6 +1490,7 @@ class VoiceData: def __init__ (self): self.voicedata = None self.ly_voice = None + self.figured_bass = None self.lyrics_dict = {} self.lyrics_order = [] @@ -1454,6 +1518,8 @@ def musicxml_voice_to_lily_voice (voice): ignore_lyrics = False current_staff = None + + pending_figured_bass = [] # Make sure that the keys in the dict don't get reordered, since # we need the correct ordering of the lyrics stanzas! By default, @@ -1462,7 +1528,8 @@ def musicxml_voice_to_lily_voice (voice): for k in return_value.lyrics_order: lyrics[k] = [] - voice_builder = LilyPondVoiceBuilder() + voice_builder = LilyPondVoiceBuilder () + figured_bass_builder = LilyPondVoiceBuilder () for n in voice._elements: if n.get_name () == 'forward': @@ -1495,6 +1562,13 @@ def musicxml_voice_to_lily_voice (voice): else: voice_builder.add_command (a) continue + + if isinstance (n, musicxml.FiguredBass): + a = musicxml_figured_bass_to_lily (n) + print "Figured bass element: %s\n" % a + if a: + pending_figured_bass.append (a) + continue is_chord = n.get_maybe_exist_named_child ('chord') if not is_chord: @@ -1584,6 +1658,18 @@ def musicxml_voice_to_lily_voice (voice): if voice_builder.current_duration () == 0 and n._duration > 0: voice_builder.set_duration (n._duration) + # if we have a figured bass, set its voice builder to the correct position + # and insert the pending figures + if pending_figured_bass: + try: + figured_bass_builder.jumpto (n._when) + except NegativeSkip, neg: + pass + for fb in pending_figured_bass: + figured_bass_builder.add_music (fb, fb.real_duration) + pending_figured_bass = [] + + notations_children = n.get_typed_children (musicxml.Notations) tuplet_event = None span_events = [] @@ -1759,6 +1845,25 @@ def musicxml_voice_to_lily_voice (voice): v.mode = mode return_value.ly_voice = v + # create \figuremode { figured bass elements } + if figured_bass_builder.elements: + fbass_music = musicexp.SequentialMusic () + fbass_music.elements = figured_bass_builder.elements + v = musicexp.ModeChangingMusicWrapper() + v.mode = 'figuremode' + v.element = fbass_music + + #printer = musicexp.Output_printer() + #debug_file = "debug.out" + #progress (_ ("FiguredBass debug output to `%s'") % debug_file) + #printer.set_file (codecs.open (debug_file, 'wb', encoding='utf-8')) + #v.print_ly( printer ) + #printer.close () + + return_value.figured_bass = v + + + print figured_bass_builder.elements return return_value def musicxml_id_to_lily (id): @@ -1925,6 +2030,10 @@ def music_xml_lyrics_name_to_lily_name (part_id, name, lyricsnr): str = "Part%sVoice%sLyrics%s" % (part_id, name, lyricsnr) return musicxml_id_to_lily (str) +def music_xml_figuredbass_name_to_lily_name (part_id, name): + str = "Part%sVoice%sFiguredBass" % (part_id, name) + return musicxml_id_to_lily (str) + def print_voice_definitions (printer, part_list, voices): for part in part_list: part_id = part.id @@ -1936,9 +2045,14 @@ def print_voice_definitions (printer, part_list, voices): printer.newline() for l in voice.lyrics_order: lname = music_xml_lyrics_name_to_lily_name (part_id, name, l) - printer.dump ('%s = ' %lname ) + printer.dump ('%s = ' % lname ) voice.lyrics_dict[l].print_ly (printer) printer.newline() + if voice.figured_bass: + fbname = music_xml_figuredbass_name_to_lily_name (part_id, name) + printer.dump ('%s = ' % fbname ) + voice.figured_bass.print_ly (printer) + printer.newline() def uniq_list (l): -- 2.39.5