From 50be93de6195cc1240ea0608f01a4c933867d764 Mon Sep 17 00:00:00 2001 From: Reinhold Kainhofer Date: Fri, 11 Apr 2008 17:05:05 +0200 Subject: [PATCH] MusicXML: Add support for chord names Implemented it similar to FiguredBass (using its own voice builder). The duration of a chord is always the duration of the next following note (MusicXML does not have the concept of duration for a chord...) --- .../regression/musicxml/17f-AllChordTypes.xml | 444 ++++++++++++++++++ python/musicexp.py | 45 +- python/musicxml.py | 15 +- scripts/musicxml2ly.py | 147 +++++- 4 files changed, 635 insertions(+), 16 deletions(-) create mode 100644 input/regression/musicxml/17f-AllChordTypes.xml diff --git a/input/regression/musicxml/17f-AllChordTypes.xml b/input/regression/musicxml/17f-AllChordTypes.xml new file mode 100644 index 0000000000..18433bb1f8 --- /dev/null +++ b/input/regression/musicxml/17f-AllChordTypes.xml @@ -0,0 +1,444 @@ + + + + All MusicXM chord names/types + + + + MusicXML Part + + Acoustic Grand Piano + + + + + + + + 1 + + 0 + major + + + + G + 2 + + + + + C + + major + + + C4 + 1 + 1 + quarter + + + + C + + minor + + + C4 + 1 + 1 + quarter + + + + C + + augmented + + + C4 + 1 + 1 + quarter + + + + C + + diminished + + + C4 + 1 + 1 + quarter + + + + + + + C + + dominant + + + C4 + 1 + 1 + quarter + + + + C + + major-seventh + + + C4 + 1 + 1 + quarter + + + + C + + minor-seventh + + + C4 + 1 + 1 + quarter + + + + C + + diminished-seventh + + + C4 + 1 + 1 + quarter + + + + + + + C + + augmented-seventh + + + C4 + 1 + 1 + quarter + + + + C + + half-diminished + + + C4 + 1 + 1 + quarter + + + + C + + major-minor + + + C4 + 1 + 1 + quarter + + + + C + + major-sixth + + + C4 + 1 + 1 + quarter + + + + + + + C + + minor-sixth + + + C4 + 1 + 1 + quarter + + + + C + + dominant-ninth + + + C4 + 1 + 1 + quarter + + + + C + + major-ninth + + + C4 + 1 + 1 + quarter + + + + C + + minor-ninth + + + C4 + 1 + 1 + quarter + + + + + + + C + + dominant-11th + + + C4 + 1 + 1 + quarter + + + + C + + major-11th + + + C4 + 1 + 1 + quarter + + + + C + + minor-11th + + + C4 + 1 + 1 + quarter + + + + C + + dominant-13th + + + C4 + 1 + 1 + quarter + + + + + + + C + + major-13th + + + C4 + 1 + 1 + quarter + + + + C + + minor-13th + + + C4 + 1 + 1 + quarter + + + + C + + suspended-second + + + C4 + 1 + 1 + quarter + + + + C + + suspended-fourth + + + C4 + 1 + 1 + quarter + + + + + + + C + + Neapolitan + + + C4 + 1 + 1 + quarter + + + + C + + Italian + + + C4 + 1 + 1 + quarter + + + + C + + French + + + C4 + 1 + 1 + quarter + + + + C + + German + + + C4 + 1 + 1 + quarter + + + + + + + C + + pedal + + + C4 + 1 + 1 + quarter + + + + C + + power + + + C4 + 1 + 1 + quarter + + + + C + + Tristan + + + C4 + 1 + 1 + quarter + + + + C + + other + + + C4 + 1 + 1 + quarter + + + light-heavy + + + + + diff --git a/python/musicexp.py b/python/musicexp.py index 0346e91f4c..00b288176c 100644 --- a/python/musicexp.py +++ b/python/musicexp.py @@ -719,7 +719,15 @@ class ChordEvent (NestedMusic): for e in self.elements: l = max(l, e.get_length()) return l - + + def get_duration (self): + note_events = [e for e in self.elements if + isinstance (e, NoteEvent)] + if note_events: + return note_events[0].duration + else: + return None + def print_ly (self, printer): note_events = [e for e in self.elements if isinstance (e, NoteEvent)] @@ -757,7 +765,9 @@ class ChordEvent (NestedMusic): basepitch = previous_pitch printer ('<%s>' % string.join (pitches)) previous_pitch = basepitch - note_events[0].duration.print_ly (printer) + duration = self.get_duration () + if duration: + duration.print_ly (printer) else: pass @@ -1089,6 +1099,33 @@ class FretEvent (MarkupEvent): else: return '' +class ChordRoot: + def __init__ (self): + self.alteration = 0 + self.step = 0 + def __repr__(self): + return self.ly_expression() + def ly_expression (self): + return pitch_generating_function (self) + +class ChordNameEvent (Event): + def __init__ (self): + Event.__init__ (self) + self.root = None + self.kind = None + self.duration = None + def ly_expression (self): + if not self.root: + return '' + value = self.root.ly_expression () + if self.duration: + value += self.duration.ly_expression () + if self.kind: + value += ":" + value += self.kind + return value + + class TremoloEvent (ArticulationEvent): def __init__ (self): Event.__init__ (self) @@ -1494,8 +1531,10 @@ class Staff (StaffGroup): printer.newline () n = 0 nr_voices = len (voices) - for [v, lyrics, figuredbass] in voices: + for [v, lyrics, figuredbass, chordnames] in voices: n += 1 + if chordnames: + printer ('\context ChordNames = "%s" \\%s' % (chordnames, chordnames)) voice_count_text = '' if nr_voices > 1: voice_count_text = {1: ' \\voiceOne', 2: ' \\voiceTwo', diff --git a/python/musicxml.py b/python/musicxml.py index 194fffbd9c..c71bf2d0be 100644 --- a/python/musicxml.py +++ b/python/musicxml.py @@ -872,6 +872,17 @@ class Words (Music_xml_node): class Harmony (Music_xml_node): pass +class Root (Music_xml_node): + def get_step (self): + ch = self.get_unique_typed_child (get_class (u'root-step')) + return ch.get_text ().strip () + def get_alteration (self): + ch = self.get_maybe_exist_typed_child (get_class (u'root-alter')) + alter = 0 + if ch: + alter = int (ch.get_text ().strip ()) + return alter + class Frame (Music_xml_node): def get_frets (self): return self.get_named_child_value_number ('frame-frets', 4) @@ -879,6 +890,7 @@ class Frame (Music_xml_node): return self.get_named_child_value_number ('frame-strings', 6) def get_first_fret (self): return self.get_named_child_value_number ('first-fret', 1) + class Frame_Note (Music_xml_node): def get_string (self): return self.get_named_child_value_number ('string', 1) @@ -947,7 +959,8 @@ class_dict = { 'per-minute': PerMinute, 'pitch': Pitch, 'rest': Rest, - 'score-part': Score_part, + 'root': Root, + 'score-part': Score_part, 'slide': Slide, 'slur': Slur, 'staff': Staff, diff --git a/scripts/musicxml2ly.py b/scripts/musicxml2ly.py index a2b27e6aad..c7b1000349 100644 --- a/scripts/musicxml2ly.py +++ b/scripts/musicxml2ly.py @@ -1308,6 +1308,86 @@ def musicxml_harmony_to_lily (n): ev = musicxml_frame_to_lily_event (f) if ev: res.append (ev) + return res + + +def musicxml_chordroot_to_lily (mxl_root): + r = musicexp.ChordRoot () + r.alteration = mxl_root.get_alteration () + r.step = musicxml_step_to_lily (mxl_root.get_step ()) + return r + +chordkind_dict = { + 'major': '', + 'minor': 'm', + 'augmented': 'aug', + 'diminished': 'dim', + # Sevenths: + 'dominant': '7', + 'major-seventh': 'maj', + 'minor-seventh': 'm7', + 'diminished-seventh': 'dim7', + 'augmented-seventh': 'aug7', + #'half-diminished': '???', (diminished triad, minor seventh) + #'major-minor': '???', (minor triad, major seventh) + # Sixths: + 'major-sixth': '6', + 'minor-sixth': 'm6', + # Ninths: + 'dominant-ninth': '9', + 'major-ninth': 'maj9', + 'minor-ninth': 'm9', + # 11ths (usually as the basis for alteration): + 'dominant-11th': '11', + 'major-11th': 'maj11', + 'minor-11th': 'm11', + # 13ths (usually as the basis for alteration): + 'dominant-13th': '13', + 'major-13th': 'maj13', + 'minor-13th': 'm13', + # Suspended: + 'suspended-second': 'sus2', + 'suspended-fourth': 'sus4', + # Functional sixths: + #'Neapolitan': '???', + #'Italian': '???', + #'French': '???', + #'German': '???', + # Other: + #'pedal': '???',(pedal-point bass) + #'power': '???',(perfect fifth) + #'Tristan': '???', + #'other': '', + 'none': '', +} + +def musicxml_chordkind_to_lily (kind): + res = chordkind_dict.get (kind, None) + if not res: + error_message (_ ("Unable to convert chord type %s to lilypond.") % kind) + return res + +def musicxml_harmony_to_lily_chordname (n): + res = [] + root = n.get_maybe_exist_named_child ('root') + if root: + ev = musicexp.ChordNameEvent () + ev.root = musicxml_chordroot_to_lily (root) + kind = n.get_maybe_exist_named_child ('kind') + if kind: + ev.kind = musicxml_chordkind_to_lily (kind.get_text ()) + #TODO: convert the user-symbols attribute: + #major: a triangle, like Unicode 25B3 + #minor: -, like Unicode 002D + #augmented: +, like Unicode 002B + #diminished: (degree), like Unicode 00B0 + #half-diminished: (o with slash), like Unicode 00F8 + # TODO: Convert the inversion and bass children + for deg in n.get_named_children ('degree'): + # TODO: Convert the added/removed degrees to lilypond + pass + if ev and ev.root: + res.append (ev) return res @@ -1556,6 +1636,7 @@ class VoiceData: self.voicedata = None self.ly_voice = None self.figured_bass = None + self.chordnames = None self.lyrics_dict = {} self.lyrics_order = [] @@ -1585,6 +1666,7 @@ def musicxml_voice_to_lily_voice (voice): current_staff = None pending_figured_bass = [] + pending_chordnames = [] # Make sure that the keys in the dict don't get reordered, since # we need the correct ordering of the lyrics stanzas! By default, @@ -1595,6 +1677,7 @@ def musicxml_voice_to_lily_voice (voice): voice_builder = LilyPondVoiceBuilder () figured_bass_builder = LilyPondVoiceBuilder () + chordnames_builder = LilyPondVoiceBuilder () for n in voice._elements: if n.get_name () == 'forward': @@ -1626,8 +1709,10 @@ def musicxml_voice_to_lily_voice (voice): voice_builder.add_dynamics (a) else: voice_builder.add_command (a) + for a in musicxml_harmony_to_lily_chordname (n): + pending_chordnames.append (a) continue - + if isinstance (n, musicxml.FiguredBass): a = musicxml_figured_bass_to_lily (n) if a: @@ -1650,6 +1735,8 @@ def musicxml_voice_to_lily_voice (voice): number = 0 if number > 0: voice_builder.add_bar_check (number) + figured_bass_builder.add_bar_check (number) + chordnames_builder.add_bar_check (number) for a in musicxml_attributes_to_lily (n): voice_builder.add_command (a) @@ -1725,13 +1812,28 @@ def musicxml_voice_to_lily_voice (voice): # 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 = [] + try: + figured_bass_builder.jumpto (n._when) + except NegativeSkip, neg: + pass + for fb in pending_figured_bass: + # if a duration is given, use that, otherwise the one of the note + dur = fb.real_duration + if not dur: + dur = ev_chord.get_length () + figured_bass_builder.add_music (fb, dur) + pending_figured_bass = [] + + if pending_chordnames: + try: + chordnames_builder.jumpto (n._when) + except NegativeSkip, neg: + pass + for cn in pending_chordnames: + # Assign the duration of the EventChord + cn.duration = ev_chord.get_duration () + chordnames_builder.add_music (cn, ev_chord.get_length ()) + pending_chordnames = [] notations_children = n.get_typed_children (musicxml.Notations) @@ -1918,6 +2020,15 @@ def musicxml_voice_to_lily_voice (voice): v.element = fbass_music return_value.figured_bass = v + # create \chordmode { chords } + if chordnames_builder.elements: + cname_music = musicexp.SequentialMusic () + cname_music.elements = chordnames_builder.elements + v = musicexp.ModeChangingMusicWrapper() + v.mode = 'chordmode' + v.element = cname_music + return_value.chordnames = v + return return_value def musicxml_id_to_lily (id): @@ -2088,6 +2199,10 @@ def music_xml_figuredbass_name_to_lily_name (part_id, voicename): str = "Part%sVoice%sFiguredBass" % (part_id, voicename) return musicxml_id_to_lily (str) +def music_xml_chordnames_name_to_lily_name (part_id, voicename): + str = "Part%sVoice%sChords" % (part_id, voicename) + return musicxml_id_to_lily (str) + def print_voice_definitions (printer, part_list, voices): for part in part_list: part_id = part.id @@ -2097,6 +2212,11 @@ def print_voice_definitions (printer, part_list, voices): printer.dump ('%s = ' % k) voice.ly_voice.print_ly (printer) printer.newline() + if voice.chordnames: + cnname = music_xml_chordnames_name_to_lily_name (part_id, name) + printer.dump ('%s = ' % cnname ) + voice.chordnames.print_ly (printer) + printer.newline() for l in voice.lyrics_order: lname = music_xml_lyrics_name_to_lily_name (part_id, name, l) printer.dump ('%s = ' % lname ) @@ -2123,14 +2243,17 @@ def uniq_list (l): # raw_voices is of the form [(voicename, lyricsids, havefiguredbass)*] def format_staff_info (part_id, staff_id, raw_voices): voices = [] - for (v, lyricsids, figured_bass) in raw_voices: + for (v, lyricsids, figured_bass, chordnames) in raw_voices: voice_name = music_xml_voice_name_to_lily_name (part_id, v) voice_lyrics = [music_xml_lyrics_name_to_lily_name (part_id, v, l) for l in lyricsids] figured_bass_name = '' if figured_bass: figured_bass_name = music_xml_figuredbass_name_to_lily_name (part_id, v) - voices.append ([voice_name, voice_lyrics, figured_bass_name]) + chordnames_name = '' + if chordnames: + chordnames_name = music_xml_chordnames_name_to_lily_name (part_id, v) + voices.append ([voice_name, voice_lyrics, figured_bass_name, chordnames_name]) return [staff_id, voices] def update_score_setup (score_structure, part_list, voices): @@ -2152,12 +2275,12 @@ def update_score_setup (score_structure, part_list, voices): staves = uniq_list (staves) staves.sort () for s in staves: - thisstaff_raw_voices = [(voice_name, voice.lyrics_order, voice.figured_bass) + thisstaff_raw_voices = [(voice_name, voice.lyrics_order, voice.figured_bass, voice.chordnames) for (voice_name, voice) in nv_dict.items () if voice.voicedata._start_staff == s] staves_info.append (format_staff_info (part_id, s, thisstaff_raw_voices)) else: - thisstaff_raw_voices = [(voice_name, voice.lyrics_order, voice.figured_bass) + thisstaff_raw_voices = [(voice_name, voice.lyrics_order, voice.figured_bass, voice.chordnames) for (voice_name, voice) in nv_dict.items ()] staves_info.append (format_staff_info (part_id, None, thisstaff_raw_voices)) score_structure.set_part_information (part_id, staves_info) -- 2.39.2