diff --git a/.vscode/settings.json b/.vscode/settings.json index fa93d8b..72ff9a5 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,7 +1,7 @@ { "[markdown]": { "editor.codeActionsOnSave": { - "source.fixAll.markdownlint": true + "source.fixAll.markdownlint": "explicit" }, "editor.minimap.enabled": false, "editor.wordWrap": "wordWrapColumn", @@ -9,7 +9,7 @@ }, "[python]": { "editor.codeActionsOnSave": { - "source.organizeImports": true + "source.organizeImports": "explicit" }, "editor.formatOnSave": true }, diff --git a/muspy/classes.py b/muspy/classes.py index 8995070..a126a5a 100644 --- a/muspy/classes.py +++ b/muspy/classes.py @@ -33,6 +33,7 @@ DEFAULT_VELOCITY = 64 NoteT = TypeVar("NoteT", bound="Note") ChordT = TypeVar("ChordT", bound="Chord") +ChordSymbolT = TypeVar("ChordSymbolT", bound="Chord") TrackT = TypeVar("TrackT", bound="Track") __all__ = [ @@ -52,6 +53,29 @@ # pylint: disable=super-init-not-called +import mir_eval +import numpy as np +from copy import deepcopy +note_symbols_dict = { + 0: 'C', + 1: 'Db', + 2: 'D', + 3: 'Eb', + 4: 'E', + 5: 'F', + 6: 'Gb', + 7: 'G', + 8: 'Ab', + 9: 'A', + 10: 'Bb', + 11: 'B' +} + +MIR_QUALITIES = mir_eval.chord.QUALITIES +EXT_MIR_QUALITIES = deepcopy( MIR_QUALITIES ) +for k in list(EXT_MIR_QUALITIES.keys()) + ['7(b9)', '7(#9)', '7(#11)', '7(b13)']: + _, semitone_bitmap, _ = mir_eval.chord.encode( 'C' + (len(k) > 0)*':' + k, reduce_extended_chords=True ) + EXT_MIR_QUALITIES[k] = semitone_bitmap def get_end_time(list_: List, is_sorted: bool = False, attr: str = "time"): """Return the end time of a list of objects. @@ -631,6 +655,147 @@ def clip(self: ChordT, lower: int = 0, upper: int = 127) -> ChordT: self.velocity = lower return self +# https://www.w3.org/2021/06/musicxml40/musicxml-reference/examples/tutorial-chord-symbols/ +# https://github.com/DCMLab/pitchplots/blob/master/modified_musicxml_parser.py#L908 +# https://lilypond.org/doc/v2.25/input/regression/musicxml/collated-files#g_t71-_002e_002e_002e-guitar-notation +# https://www.w3.org/2021/06/musicxml40/tutorial/chord-symbols-and-diagrams/ +# https://www.w3.org/2021/06/musicxml40/musicxml-reference/data-types/ +# https://www.w3.org/2021/06/musicxml40/musicxml-reference/data-types/kind-value/ +# https://www.w3.org/2021/06/musicxml40/musicxml-reference/data-types/degree-symbol-value/ +# https://www.w3.org/2021/06/musicxml40/musicxml-reference/data-types/degree-type-value/ +class ChordSymbol(Base): + """A container for chord symbols. + + Attributes + ---------- + time : int + Start time of the chord symbol, in time steps. + root : str + Root note symbol in string format as returned by utils:ChordSymbolParser. + kind : str + Root kind symbol in string format as returned by utils:ChordSymbolParser. + degrees : List[str] + Root degrees as list of symbols in string format as returned by utils:ChordSymbolParser. + bass : str + Bass note symbol in string format as returned by utils:ChordSymbolParser. + binary_xml : np.array + Binary 12D representation of chord quality pitch classes, as produced by utils:ChordSymbolParses. + chord_symbol_xml : str + Chord symbol in string format as constructed from XML information. + chord_symbol_mir_eval : str + Chord symbol that can be parsed by mir_eval.chord.encode. + chord_symbol_mir_eval : np.array + Binary 12D representation of chord quality pitch classes that best matches chord qualities in mir_eval.chord. + root_pc : int + Pitch class of the root note. + """ + + _attributes = OrderedDict( + [ + ("time", int), + ("root", str), + ("kind", str), + ("degrees", List[str]), + ("bass", str), + ("binary_xml", List[int]), + ("chord_symbol_xml", str), + ("chord_symbol_mir_eval", str), + ("binary_mir_eval", List[int]), + ("root_pc", int) + ] + ) + _optional_attributes = [] + + def __init__( + self, + parsed_chord_symbol + ): + self.time = parsed_chord_symbol.time + self.root = parsed_chord_symbol.root + self.kind = parsed_chord_symbol.kind + self.degrees = parsed_chord_symbol.degrees + self.bass = parsed_chord_symbol.bass + self.binary_xml = parsed_chord_symbol.binary + self.construct_xml_symbol() + self.get_closest_mir_eval_symbol() + + def construct_xml_symbol(self): + self.chord_symbol_xml = self.root + self.kind + for d in self.degrees: + self.chord_symbol_xml += d + if self.bass: + self.chord_symbol_xml += '/' + self.bass + + def get_closest_mir_eval_symbol(self): + similarity_max = -1 + key_max = None + for k in EXT_MIR_QUALITIES.keys(): + tmp_similarity = np.sum(self.binary_xml == EXT_MIR_QUALITIES[k]) + if similarity_max < tmp_similarity: + similarity_max = tmp_similarity + key_max = k + self.chord_symbol_mir_eval = self.root + ':' + key_max + self.binary_mir_eval = EXT_MIR_QUALITIES[key_max] + self.root_pc, _, _ = mir_eval.chord.encode( self.chord_symbol_mir_eval ) + + @property + def start(self): + """Start time of the chord symbol.""" + return self.time + + @start.setter + def start(self, start): + """Setter for start time.""" + self.time = start + + def adjust_time( + self: ChordSymbolT, + func: Callable[[int], int], + attr: str = None, + recursive: bool = True, + ) -> ChordSymbolT: + """Adjust the timing of the chord. + + Parameters + ---------- + func : callable + The function used to compute the new timing from the old + timing, i.e., `new_time = func(old_time)`. + attr : str, optional + Attribute to adjust. Defaults to adjust all attributes. + recursive : bool, default: True + Whether to apply recursively. + + Returns + ------- + Object itself. + + """ + if attr is not None and attr != "time": + raise AttributeError(f"'Note' object has no attribute '{attr}'") + + self.time = func(self.time) + return self + + def transpose(self: ChordSymbolT, semitone: int) -> ChordT: + """Transpose the notes by a number of semitones. + + Parameters + ---------- + semitone : int + Number of semitones to transpose the notes. A positive value + raises the pitches, while a negative value lowers the + pitches. + + Returns + ------- + Object itself. + + """ + self.root_pc = (self.root_pc + semitone)%12 + self.pitch_classes += [(pitch + semitone)%12 for pitch in self.pitch_classes] + self.root = note_symbols_dict[self.root_pc] + return self class Track(ComplexBase): """A container for music track. @@ -652,6 +817,8 @@ class Track(ComplexBase): Annotations. lyrics : list of :class:`muspy.Lyric`, default: [] Lyrics. + harmony: list of :class:`muspy.ChordSymbol`, default: [] + Harmony. Note ---- @@ -674,11 +841,12 @@ class Track(ComplexBase): ("notes", Note), ("chords", Chord), ("lyrics", Lyric), + ("harmony", ChordSymbol), ("annotations", Annotation), ] ) - _optional_attributes = ["name", "notes", "chords", "lyrics", "annotations"] - _list_attributes = ["notes", "chords", "lyrics", "annotations"] + _optional_attributes = ["name", "notes", "chords", "lyrics", "harmony", "annotations"] + _list_attributes = ["notes", "chords", "lyrics", "harmony", "annotations"] def __init__( self, @@ -688,6 +856,7 @@ def __init__( notes: List[Note] = None, chords: List[Chord] = None, lyrics: List[Lyric] = None, + harmony: List[ChordSymbol] = None, annotations: List[Annotation] = None, ): self.program = program if program is not None else 0 @@ -696,6 +865,7 @@ def __init__( self.notes = notes if notes is not None else [] self.chords = chords if chords is not None else [] self.lyrics = lyrics if lyrics is not None else [] + self.harmony = harmony if harmony is not None else [] self.annotations = annotations if annotations is not None else [] def __len__(self) -> int: @@ -769,6 +939,8 @@ def transpose(self: TrackT, semitone: int) -> TrackT: """ for note in self.notes: note.transpose(semitone) + for chordsymbol in self.harmony: + chordsymbol.transpose(semitone) return self def trim(self: TrackT, end: int) -> TrackT: diff --git a/muspy/inputs/musicxml.py b/muspy/inputs/musicxml.py index b487d33..9a69f07 100644 --- a/muspy/inputs/musicxml.py +++ b/muspy/inputs/musicxml.py @@ -20,9 +20,10 @@ Tempo, TimeSignature, Track, + ChordSymbol ) from ..music import DEFAULT_RESOLUTION, Music -from ..utils import CIRCLE_OF_FIFTHS, MODE_CENTERS, NOTE_MAP, NOTE_TYPE_MAP +from ..utils import CIRCLE_OF_FIFTHS, MODE_CENTERS, NOTE_MAP, NOTE_TYPE_MAP, ChordSymbolParser T = TypeVar("T") @@ -648,6 +649,7 @@ def parse_part_elem( instrument_id: [] for instrument_id in instrument_info } lyrics: List[Lyric] = [] + chord_symbols: List[ChordSymbol] = [] # Initialize variables time = 0 @@ -712,6 +714,12 @@ def parse_part_elem( if dynamics is not None: velocity = round(float(dynamics)) + # Harmony - chord symbols + elif elem.tag == 'harmony': + chord_symbol_parsed = ChordSymbolParser(elem, time+position) + chord_symbol = ChordSymbol(chord_symbol_parsed) + chord_symbols.append( chord_symbol ) + # Note elements elif elem.tag == "note": # TODO: Handle voice information @@ -847,7 +855,10 @@ def parse_part_elem( # Sort lyrics lyrics.sort(key=attrgetter("time")) - return notes, lyrics + # Sort chord_symbols + chord_symbols.sort(key=attrgetter("time")) + + return notes, lyrics, chord_symbols def parse_metadata(root: Element) -> Metadata: @@ -1040,11 +1051,11 @@ def read_musicxml( ) part_elem = _get_required(root, "part") instrument_info = {"": {"program": 0, "is_drum": False}} - notes, lyrics = parse_part_elem( + notes, lyrics, chord_symbols = parse_part_elem( part_elem, resolution, instrument_info, measure_indices ) tracks.append( - Track(program=0, is_drum=False, notes=notes[""], lyrics=lyrics) + Track(program=0, is_drum=False, notes=notes[""], lyrics=lyrics, harmony=chord_symbols) ) else: @@ -1059,7 +1070,7 @@ def read_musicxml( continue # Parse part - notes, lyrics = parse_part_elem( + notes, lyrics, chord_symbols = parse_part_elem( part_elem, resolution, part_info[part_id], measure_indices ) @@ -1071,6 +1082,7 @@ def read_musicxml( name=part_info[part_id][instrument_id]["name"], notes=instrument_notes, lyrics=lyrics, + harmony=chord_symbols ) tracks.append(track) diff --git a/muspy/utils.py b/muspy/utils.py index c9e631c..33b53ba 100644 --- a/muspy/utils.py +++ b/muspy/utils.py @@ -1,6 +1,8 @@ """Utility functions.""" from collections import OrderedDict from typing import Dict, List, Tuple +import numpy as np +import warnings import yaml @@ -169,3 +171,348 @@ def yaml_dump( return yaml.dump( data, Dumper=Dumper, allow_unicode=allow_unicode, **kwargs ) + +class ChordSymbolParser(object): + # Modified version of: + # https://github.com/DCMLab/pitchplots/blob/master/modified_musicxml_parser.py#L908 + # Renders to mir_eval compatible chords. + # Time representation is taken from caller and transition is + # implemented in the classes.ChordSymbol class, so state has been removed. + """Internal representation of a MusicXML chord symbol element. + This represents a chord symbol with four components: + 1) Root: a string representing the chord root pitch class, e.g. "C#". + 2) Kind: a string representing the chord kind, e.g. "m7" for minor-seventh, + "9" for dominant-ninth, or the empty string for major triad. + 3) Scale degree modifications: a list of strings representing scale degree + modifications for the chord, e.g. "add9" to add an unaltered ninth scale + degree (without the seventh), "b5" to flatten the fifth scale degree, + "no3" to remove the third scale degree, etc. + 4) Bass: a string representing the chord bass pitch class, or None if the bass + pitch class is the same as the root pitch class. + 5) Binary_xml: 12D binary representation of chord pitch classes. This is used + to find the proper matching quality in mir_eval.chord. + There's also a special chord kind "N.C." representing no harmony, for which + all other fields should be None. + Use the `get_figure_string` method to get a string representation of the chord + symbol as might appear in a lead sheet. This string representation is what we + use to represent chord symbols in NoteSequence protos, as text annotations. + While the MusicXML representation has more structure, using an unstructured + string provides more flexibility and allows us to ingest chords from other + sources, e.g. guitar tabs on the web. + """ + + # The below dictionary maps chord kinds to an abbreviated string as would + # appear in a chord symbol in a standard lead sheet. There are often multiple + # standard abbreviations for the same chord type, e.g. "+" and "aug" both + # refer to an augmented chord, and "maj7", "M7", and a Delta character all + # refer to a major-seventh chord; this dictionary attempts to be consistent + # but the choice of abbreviation is somewhat arbitrary. + # + # The MusicXML-defined chord kinds are listed here: + # http://usermanuals.musicxml.com/MusicXML/Content/ST-MusicXML-kind-value.htm + + CHORD_KIND_ABBREVIATIONS = { + # These chord kinds are in the MusicXML spec. + 'major': '', + 'minor': 'm', + 'augmented': 'aug', + 'augmented-major-seventh': 'aug', + 'diminished': 'dim', + 'dominant': '7', + 'major-seventh': 'maj7', + 'minor-seventh': 'm7', + 'seventh-flat-five': '7(b5)', + 'diminished-seventh': 'dim7', + 'augmented-seventh': 'aug7', + 'half-diminished': 'm7b5', + 'major-minor': 'm(maj7)', + 'major-sixth': '6', + 'minor-sixth': 'm6', + 'dominant-ninth': '9', + 'major-ninth': 'maj9', + 'minor-ninth': 'm9', + 'dominant-11th': '11', + 'major-11th': 'maj11', + 'minor-11th': 'm11', + 'dominant-13th': '13', + 'major-13th': 'maj13', + 'minor-13th': 'm13', + 'suspended-second': 'sus2', + 'suspended-fourth': 'sus', + 'suspended-fourth-seventh': 'sus', + 'pedal': 'ped', + 'power': '5', + 'none': 'N.C.', + + # These are not in the spec, but show up frequently in the wild. + 'dominant-seventh': '7', + 'augmented-ninth': 'aug9', + 'minor-major': 'm(maj7)', + + # Some abbreviated kinds also show up frequently in the wild. + '': '', + 'min': 'm', + 'aug': 'aug', + 'dim': 'dim', + '7': '7', + 'maj7': 'maj7', + 'min7': 'm7', + 'dim7': 'dim7', + 'm7b5': 'm7b5', + 'minMaj7': 'm(maj7)', + '6': '6', + 'min6': 'm6', + 'maj69': '6(add9)', + '9': '9', + 'maj9': 'maj9', + 'min9': 'm9', + 'sus47': 'sus7', + + # added from + # https://lilypond.org/doc/v2.25/input/regression/musicxml/collated-files#g_t71-_002e_002e_002e-guitar-notation + 'Neapolitan': '', + 'Italian': '7', + 'French': '7', + 'German': '7', + 'Tristan': '9', + 'other': 'ped' + } + + KIND_TO_BINARY = { + '': [1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0], + 'm': [1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0], + 'aug': [1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0], + 'dim': [1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0], + '7': [1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0], + 'maj7': [1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1], + 'm7': [1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0], + 'dim7': [1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0], + 'aug7': [1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0], + 'm7b5': [1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0], + 'm(maj7)': [1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1], + '6': [1, 0, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0], + 'm6': [1, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0], + '9': [1, 0, 1, 0, 1, 0, 0, 1, 0, 0, 1, 0], + 'maj9': [1, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 1], + 'm9': [1, 0, 1, 1, 0, 0, 0, 1, 0, 0, 1, 0], + '11': [1, 0, 1, 0, 1, 1, 0, 1, 0, 0, 1, 0], + 'maj11': [1, 0, 1, 0, 1, 1, 0, 1, 0, 0, 0, 1], + 'm11': [1, 0, 1, 1, 0, 1, 0, 1, 0, 0, 1, 0], + '13': [1, 0, 1, 0, 1, 1, 0, 1, 1, 0, 1, 0], + 'maj13': [1, 0, 1, 0, 1, 1, 0, 1, 1, 0, 0, 1], + 'm13': [1, 0, 1, 1, 0, 1, 0, 1, 1, 0, 1, 0], + 'sus2': [1, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0], + 'sus': [1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0], + 'ped': [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + '5': [1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0], + 'N.C.': [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + 'aug9': [1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0], + 'sus7': [1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1, 0], + '6(add9)': [1, 0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 0], + } + + def __init__(self, xml_harmony, time): + self.xml_harmony = xml_harmony + self.root = None + self.kind = '' + self.degrees = [] + self.binary = np.zeros(12) + self.bass = None + self.time = time + self._parse() + + def _alter_to_string(self, alter_text): + """Parse alter text to a string of one or two sharps/flats. + Args: + alter_text: A string representation of an integer number of semitones. + Returns: + A string, one of 'bb', 'b', '#', '##', or the empty string. + Raises: + ChordSymbolParseError: If `alter_text` cannot be parsed to an integer, + or if the integer is not a valid number of semitones between -2 and 2 + inclusive. + """ + # Parse alter text to an integer number of semitones. + try: + alter_semitones = int(alter_text) + except ValueError: + warnings.warn('Non-integer alter: ' + str(alter_text)) + + # Visual alter representation + if alter_semitones == -2: + alter_string = 'bb' + elif alter_semitones == -1: + alter_string = 'b' + elif alter_semitones == 0: + alter_string = '' + elif alter_semitones == 1: + alter_string = '#' + elif alter_semitones == 2: + alter_string = '##' + else: + warnings.warn('Invalid alter: ' + str(alter_semitones)) + + return alter_string + + def _parse(self): + """Parse the MusicXML element.""" + for child in self.xml_harmony: + if child.tag == 'root': + self._parse_root(child) + elif child.tag == 'kind': + if child.text is None: + # Seems like this shouldn't happen but frequently does in the wild... + continue + kind_text = str(child.text).strip() + if kind_text not in self.CHORD_KIND_ABBREVIATIONS: + warnings.warn('Unknown chord kind: ' + kind_text) + self.kind = self.CHORD_KIND_ABBREVIATIONS[kind_text] + self.binary = np.array( self.KIND_TO_BINARY[self.kind] ) + elif child.tag == 'degree': + tmp_degree = self._parse_degree(child) + self.apply_degree(tmp_degree) + self.degrees.append( tmp_degree ) + elif child.tag == 'bass': + self._parse_bass(child) + elif child.tag == 'offset': + # Offset tag moves chord symbol time position. + try: + offset = int(child.text) + except ValueError: + warnings.warn('Non-integer offset: ' + str(child.text)) + else: + # Ignore other tag types because they are not relevant to Magenta. + pass + + if self.root is None and self.kind != 'N.C.': + warnings.warn('Chord symbol must have a root') + + def apply_degree(self, d): + if 'no' in d: + d_num = d.split('no')[1] + if d_num == '5': + if self.binary[7] == 0: + self.binary[6] = 0 + else: + self.binary[7] = 0 + elif d_num == '3': + if self.binary[4] == 0: + self.binary[3] = 0 + else: + self.binary[4] = 0 + elif d_num == '7': + if self.binary[10] == 0: + self.binary[11] = 0 + else: + self.binary[10] = 0 + else: + if 'add' in d: + # just remove 'add' + d = d.split('add')[1] + modifier = 0 + if '#' in d: + modifier = 1 + if 'b' in d: + modifier = -1 + if '11' in d: + self.binary[ 5 + modifier] = 0 + if '9' in d or '2' in d: + self.binary[ 2 + modifier] = 0 + if '13' in d or '6' in d: + self.binary[ 9 + modifier] = 0 + + + + + def _parse_pitch(self, xml_pitch, step_tag, alter_tag): + """Parse and return the pitch-like or element.""" + if xml_pitch.find(step_tag) is None: + warnings.warn('Missing pitch step') + step = xml_pitch.find(step_tag).text + + alter_string = '' + if xml_pitch.find(alter_tag) is not None: + alter_text = xml_pitch.find(alter_tag).text + alter_string = self._alter_to_string(alter_text) + + return step + alter_string + + def _parse_root(self, xml_root): + """Parse the tag for a chord symbol.""" + self.root = self._parse_pitch(xml_root, step_tag='root-step', alter_tag='root-alter') + + def _parse_bass(self, xml_bass): + """Parse the tag for a chord symbol.""" + self.bass = self._parse_pitch(xml_bass, step_tag='bass-step', alter_tag='bass-alter') + + def _parse_degree(self, xml_degree): + """Parse and return the scale degree modification element.""" + if xml_degree.find('degree-value') is None: + warnings.warn('Missing scale degree value in harmony') + value_text = xml_degree.find('degree-value').text + if value_text is None: + warnings.warn('Missing scale degree') + try: + value = int(value_text) + except ValueError: + warnings.warn( + 'Non-integer scale degree: ' + str(value_text)) + + alter_string = '' + if xml_degree.find('degree-alter') is not None: + alter_text = xml_degree.find('degree-alter').text + alter_string = self._alter_to_string(alter_text) + + if xml_degree.find('degree-type') is None: + warnings.warn('Missing degree modification type') + type_text = xml_degree.find('degree-type').text + + if type_text == 'add': + if not alter_string: + # When adding unaltered scale degree, use "add" string. + type_string = 'add' + else: + # When adding altered scale degree, "add" not necessary. + type_string = '' + elif type_text == 'subtract': + type_string = 'no' + # Alter should be irrelevant when removing scale degree. + alter_string = '' + elif type_text == 'alter': + if not alter_string: + warnings.warn('Degree alteration by zero semitones') + # No type string necessary as merely appending e.g. "#9" suffices. + type_string = '' + else: + warnings.warn( + 'Invalid degree modification type: ' + str(type_text)) + + # Return a scale degree modification string that can be appended to a chord + # symbol figure string. + return type_string + alter_string + str(value) + + def __str__(self): + if self.kind == 'N.C.': + note_string = '{kind: ' + self.kind + '} ' + else: + note_string = '{root: ' + self.root + note_string += ', kind: ' + self.kind + note_string += ', degrees: [%s]' % ', '.join(degree for degree in self.degrees) + note_string += ', binary: ' + repr(self.binary) + if self.bass: + note_string += ', bass: ' + self.bass + '} ' + note_string += '} ' + note_string += '(@time: ' + str(self.time) + ')' + return note_string + + def get_figure_string(self): + """Return a chord symbol figure string.""" + if self.kind == 'N.C.': + return self.kind + else: + degrees_string = ''.join('(%s)' % degree for degree in self.degrees) + figure = self.root + self.kind + degrees_string + if self.bass: + figure += '/' + self.bass + return figure +# end class ChordSymbolParser diff --git a/test_xml.ipynb b/test_xml.ipynb new file mode 100644 index 0000000..b1253e0 --- /dev/null +++ b/test_xml.ipynb @@ -0,0 +1,62 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'maj': array([1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0]), 'min': array([1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0]), 'aug': array([1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0]), 'dim': array([1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0]), 'sus4': array([1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0]), 'sus2': array([1, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0]), '7': array([1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0]), 'maj7': array([1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1]), 'min7': array([1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0]), 'minmaj7': array([1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1]), 'maj6': array([1, 0, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0]), 'min6': array([1, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0]), 'dim7': array([1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0]), 'hdim7': array([1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0]), 'maj9': array([1, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 1]), 'min9': array([1, 0, 1, 1, 0, 0, 0, 1, 0, 0, 1, 0]), '9': array([1, 0, 1, 0, 1, 0, 0, 1, 0, 0, 1, 0]), 'b9': array([1, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0]), '#9': array([1, 0, 0, 1, 1, 0, 0, 1, 0, 0, 1, 0]), 'min11': array([1, 0, 1, 1, 0, 1, 0, 1, 0, 0, 1, 0]), '11': array([1, 0, 1, 0, 1, 1, 0, 1, 0, 0, 1, 0]), '#11': array([1, 0, 1, 0, 1, 0, 1, 1, 0, 0, 1, 0]), 'maj13': array([1, 0, 1, 0, 1, 1, 0, 1, 0, 1, 0, 1]), 'min13': array([1, 0, 1, 1, 0, 1, 0, 1, 0, 1, 1, 0]), '13': array([1, 0, 1, 0, 1, 1, 0, 1, 0, 1, 1, 0]), 'b13': array([1, 0, 1, 0, 1, 1, 0, 1, 1, 0, 1, 0]), '1': array([1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), '5': array([1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0]), '': array([1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0]), '7(b9)': array([1, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0]), '7(#9)': array([1, 0, 0, 1, 1, 0, 0, 1, 0, 0, 1, 0]), '7(#11)': array([1, 0, 0, 0, 1, 0, 1, 1, 0, 0, 1, 0]), '7(b13)': array([1, 0, 0, 0, 1, 0, 0, 1, 1, 0, 1, 0])}\n" + ] + } + ], + "source": [ + "import muspy\n", + "from tests.test_musicxml import test_chord_symbols" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Music(metadata=Metadata(schema_version='0.2', title='All MusicXML chord names/types with ', source_filename='71a_f-Guitar_notation.xml', source_format='musicxml'), resolution=2, key_signatures=[KeySignature(time=0, fifths=0)], time_signatures=[TimeSignature(time=0, numerator=4, denominator=4)], barlines=[Barline(time=0), Barline(time=8), Barline(time=16), ...], beats=[Beat(time=0), Beat(time=2), Beat(time=4), ...], tracks=[Track(program=1, is_drum=False, name='MusicXML Part', notes=[Note(time=0, pitch=60, duration=2, velocity=64, pitch_str='C4'), Note(time=2, pitch=60, duration=2, velocity=64, pitch_str='C4'), Note(time=4, pitch=60, duration=2, velocity=64, pitch_str='C4'), ...], lyrics=[Lyric(time=0, lyric='major'), Lyric(time=2, lyric='minor'), Lyric(time=4, lyric='augmented'), ...], harmony=[ChordSymbol(time=0, root='C', kind='', degrees=[], binary_xml=array([1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0]), chord_symbol_xml='C', chord_symbol_mir_eval='C:maj', binary_mir_eval=array([1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0]), root_pc=0), ChordSymbol(time=2, root='C', kind='m', degrees=[], binary_xml=array([1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0]), chord_symbol_xml='Cm', chord_symbol_mir_eval='C:min', binary_mir_eval=array([1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0]), root_pc=0), ChordSymbol(time=4, root='C', kind='aug', degrees=[], binary_xml=array([1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0]), chord_symbol_xml='Caug', chord_symbol_mir_eval='C:aug', binary_mir_eval=array([1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0]), root_pc=0), ...])])\n", + "[ChordSymbol(time=0, root='C', kind='', degrees=[], binary_xml=array([1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0]), chord_symbol_xml='C', chord_symbol_mir_eval='C:maj', binary_mir_eval=array([1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0]), root_pc=0), ChordSymbol(time=2, root='C', kind='m', degrees=[], binary_xml=array([1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0]), chord_symbol_xml='Cm', chord_symbol_mir_eval='C:min', binary_mir_eval=array([1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0]), root_pc=0), ChordSymbol(time=4, root='C', kind='aug', degrees=[], binary_xml=array([1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0]), chord_symbol_xml='Caug', chord_symbol_mir_eval='C:aug', binary_mir_eval=array([1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0]), root_pc=0), ChordSymbol(time=6, root='C', kind='dim', degrees=[], binary_xml=array([1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0]), chord_symbol_xml='Cdim', chord_symbol_mir_eval='C:dim', binary_mir_eval=array([1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0]), root_pc=0), ChordSymbol(time=8, root='C', kind='7', degrees=[], binary_xml=array([1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0]), chord_symbol_xml='C7', chord_symbol_mir_eval='C:7', binary_mir_eval=array([1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0]), root_pc=0), ChordSymbol(time=10, root='C', kind='maj7', degrees=[], binary_xml=array([1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1]), chord_symbol_xml='Cmaj7', chord_symbol_mir_eval='C:maj7', binary_mir_eval=array([1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1]), root_pc=0), ChordSymbol(time=12, root='C', kind='m7', degrees=[], binary_xml=array([1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0]), chord_symbol_xml='Cm7', chord_symbol_mir_eval='C:min7', binary_mir_eval=array([1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0]), root_pc=0), ChordSymbol(time=14, root='C', kind='dim7', degrees=[], binary_xml=array([1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0]), chord_symbol_xml='Cdim7', chord_symbol_mir_eval='C:dim7', binary_mir_eval=array([1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0]), root_pc=0), ChordSymbol(time=16, root='C', kind='aug7', degrees=[], binary_xml=array([1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0]), chord_symbol_xml='Caug7', chord_symbol_mir_eval='C:aug', binary_mir_eval=array([1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0]), root_pc=0), ChordSymbol(time=18, root='C', kind='m7b5', degrees=[], binary_xml=array([1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0]), chord_symbol_xml='Cm7b5', chord_symbol_mir_eval='C:hdim7', binary_mir_eval=array([1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0]), root_pc=0), ChordSymbol(time=20, root='C', kind='m(maj7)', degrees=[], binary_xml=array([1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1]), chord_symbol_xml='Cm(maj7)', chord_symbol_mir_eval='C:minmaj7', binary_mir_eval=array([1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1]), root_pc=0), ChordSymbol(time=22, root='C', kind='6', degrees=[], binary_xml=array([1, 0, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0]), chord_symbol_xml='C6', chord_symbol_mir_eval='C:maj6', binary_mir_eval=array([1, 0, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0]), root_pc=0), ChordSymbol(time=24, root='C', kind='m6', degrees=[], binary_xml=array([1, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0]), chord_symbol_xml='Cm6', chord_symbol_mir_eval='C:min6', binary_mir_eval=array([1, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0]), root_pc=0), ChordSymbol(time=26, root='C', kind='9', degrees=[], binary_xml=array([1, 0, 1, 0, 1, 0, 0, 1, 0, 0, 1, 0]), chord_symbol_xml='C9', chord_symbol_mir_eval='C:9', binary_mir_eval=array([1, 0, 1, 0, 1, 0, 0, 1, 0, 0, 1, 0]), root_pc=0), ChordSymbol(time=28, root='C', kind='maj9', degrees=[], binary_xml=array([1, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 1]), chord_symbol_xml='Cmaj9', chord_symbol_mir_eval='C:maj9', binary_mir_eval=array([1, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 1]), root_pc=0), ChordSymbol(time=30, root='C', kind='m9', degrees=[], binary_xml=array([1, 0, 1, 1, 0, 0, 0, 1, 0, 0, 1, 0]), chord_symbol_xml='Cm9', chord_symbol_mir_eval='C:min9', binary_mir_eval=array([1, 0, 1, 1, 0, 0, 0, 1, 0, 0, 1, 0]), root_pc=0), ChordSymbol(time=32, root='C', kind='11', degrees=[], binary_xml=array([1, 0, 1, 0, 1, 1, 0, 1, 0, 0, 1, 0]), chord_symbol_xml='C11', chord_symbol_mir_eval='C:11', binary_mir_eval=array([1, 0, 1, 0, 1, 1, 0, 1, 0, 0, 1, 0]), root_pc=0), ChordSymbol(time=34, root='C', kind='maj11', degrees=[], binary_xml=array([1, 0, 1, 0, 1, 1, 0, 1, 0, 0, 0, 1]), chord_symbol_xml='Cmaj11', chord_symbol_mir_eval='C:maj9', binary_mir_eval=array([1, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 1]), root_pc=0), ChordSymbol(time=36, root='C', kind='m11', degrees=[], binary_xml=array([1, 0, 1, 1, 0, 1, 0, 1, 0, 0, 1, 0]), chord_symbol_xml='Cm11', chord_symbol_mir_eval='C:min11', binary_mir_eval=array([1, 0, 1, 1, 0, 1, 0, 1, 0, 0, 1, 0]), root_pc=0), ChordSymbol(time=38, root='C', kind='13', degrees=[], binary_xml=array([1, 0, 1, 0, 1, 1, 0, 1, 1, 0, 1, 0]), chord_symbol_xml='C13', chord_symbol_mir_eval='C:b13', binary_mir_eval=array([1, 0, 1, 0, 1, 1, 0, 1, 1, 0, 1, 0]), root_pc=0), ChordSymbol(time=40, root='C', kind='maj13', degrees=[], binary_xml=array([1, 0, 1, 0, 1, 1, 0, 1, 1, 0, 0, 1]), chord_symbol_xml='Cmaj13', chord_symbol_mir_eval='C:maj9', binary_mir_eval=array([1, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 1]), root_pc=0), ChordSymbol(time=42, root='C', kind='m13', degrees=[], binary_xml=array([1, 0, 1, 1, 0, 1, 0, 1, 1, 0, 1, 0]), chord_symbol_xml='Cm13', chord_symbol_mir_eval='C:min11', binary_mir_eval=array([1, 0, 1, 1, 0, 1, 0, 1, 0, 0, 1, 0]), root_pc=0), ChordSymbol(time=44, root='C', kind='sus2', degrees=[], binary_xml=array([1, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0]), chord_symbol_xml='Csus2', chord_symbol_mir_eval='C:sus2', binary_mir_eval=array([1, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0]), root_pc=0), ChordSymbol(time=46, root='C', kind='sus', degrees=[], binary_xml=array([1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0]), chord_symbol_xml='Csus', chord_symbol_mir_eval='C:sus4', binary_mir_eval=array([1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0]), root_pc=0), ChordSymbol(time=48, root='C', kind='', degrees=[], binary_xml=array([1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0]), chord_symbol_xml='C', chord_symbol_mir_eval='C:maj', binary_mir_eval=array([1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0]), root_pc=0), ChordSymbol(time=50, root='C', kind='7', degrees=[], binary_xml=array([1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0]), chord_symbol_xml='C7', chord_symbol_mir_eval='C:7', binary_mir_eval=array([1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0]), root_pc=0), ChordSymbol(time=52, root='C', kind='7', degrees=[], binary_xml=array([1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0]), chord_symbol_xml='C7', chord_symbol_mir_eval='C:7', binary_mir_eval=array([1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0]), root_pc=0), ChordSymbol(time=54, root='C', kind='7', degrees=[], binary_xml=array([1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0]), chord_symbol_xml='C7', chord_symbol_mir_eval='C:7', binary_mir_eval=array([1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0]), root_pc=0), ChordSymbol(time=56, root='C', kind='ped', degrees=[], binary_xml=array([1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), chord_symbol_xml='Cped', chord_symbol_mir_eval='C:1', binary_mir_eval=array([1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), root_pc=0), ChordSymbol(time=58, root='C', kind='5', degrees=[], binary_xml=array([1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0]), chord_symbol_xml='C5', chord_symbol_mir_eval='C:5', binary_mir_eval=array([1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0]), root_pc=0), ChordSymbol(time=60, root='C', kind='9', degrees=[], binary_xml=array([1, 0, 1, 0, 1, 0, 0, 1, 0, 0, 1, 0]), chord_symbol_xml='C9', chord_symbol_mir_eval='C:9', binary_mir_eval=array([1, 0, 1, 0, 1, 0, 0, 1, 0, 0, 1, 0]), root_pc=0), ChordSymbol(time=62, root='C', kind='ped', degrees=[], binary_xml=array([1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), chord_symbol_xml='Cped', chord_symbol_mir_eval='C:1', binary_mir_eval=array([1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), root_pc=0), ChordSymbol(time=64, root='F#', kind='', degrees=[], binary_xml=array([1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0]), chord_symbol_xml='F#', chord_symbol_mir_eval='F#:maj', binary_mir_eval=array([1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0]), root_pc=6), ChordSymbol(time=68, root='Fbb', kind='', degrees=[], bass='C', binary_xml=array([1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0]), chord_symbol_xml='Fbb/C', chord_symbol_mir_eval='Fbb:maj', binary_mir_eval=array([1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0]), root_pc=3), ChordSymbol(time=69, root='G#', kind='', degrees=[], bass='D#', binary_xml=array([1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0]), chord_symbol_xml='G#/D#', chord_symbol_mir_eval='G#:maj', binary_mir_eval=array([1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0]), root_pc=8), ChordSymbol(time=70, root='C', kind='', degrees=[], binary_xml=array([1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0]), chord_symbol_xml='C', chord_symbol_mir_eval='C:maj', binary_mir_eval=array([1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0]), root_pc=0), ChordSymbol(time=72, root='C', kind='', degrees=['no3', 'b5'], binary_xml=array([1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0]), chord_symbol_xml='Cno3b5', chord_symbol_mir_eval='C:5', binary_mir_eval=array([1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0]), root_pc=0), ChordSymbol(time=74, root='C', kind='', degrees=['no1', 'b6'], binary_xml=array([1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0]), chord_symbol_xml='Cno1b6', chord_symbol_mir_eval='C:maj', binary_mir_eval=array([1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0]), root_pc=0), ChordSymbol(time=76, root='C', kind='', degrees=[], binary_xml=array([1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0]), chord_symbol_xml='C', chord_symbol_mir_eval='C:maj', binary_mir_eval=array([1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0]), root_pc=0), ChordSymbol(time=78, root='C', kind='maj7', degrees=['#11'], binary_xml=array([1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1]), chord_symbol_xml='Cmaj7#11', chord_symbol_mir_eval='C:maj7', binary_mir_eval=array([1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1]), root_pc=0), ChordSymbol(time=80, root='B', kind='7', degrees=['#5', '#9'], binary_xml=array([1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0]), chord_symbol_xml='B7#5#9', chord_symbol_mir_eval='B:7', binary_mir_eval=array([1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0]), root_pc=11), ChordSymbol(time=82, root='Eb', kind='', degrees=['add2'], binary_xml=array([1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0]), chord_symbol_xml='Ebadd2', chord_symbol_mir_eval='Eb:maj', binary_mir_eval=array([1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0]), root_pc=3), ChordSymbol(time=84, root='G', kind='m', degrees=[], binary_xml=array([1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0]), chord_symbol_xml='Gm', chord_symbol_mir_eval='G:min', binary_mir_eval=array([1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0]), root_pc=7), ChordSymbol(time=86, root='D#', kind='maj7', degrees=[], binary_xml=array([1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1]), chord_symbol_xml='D#maj7', chord_symbol_mir_eval='D#:maj7', binary_mir_eval=array([1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1]), root_pc=3), ChordSymbol(time=88, root='A', kind='dim7', degrees=[], binary_xml=array([1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0]), chord_symbol_xml='Adim7', chord_symbol_mir_eval='A:dim7', binary_mir_eval=array([1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0]), root_pc=9), ChordSymbol(time=90, root='A', kind='aug', degrees=[], binary_xml=array([1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0]), chord_symbol_xml='Aaug', chord_symbol_mir_eval='A:aug', binary_mir_eval=array([1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0]), root_pc=9)]\n" + ] + } + ], + "source": [ + "x = test_chord_symbols()\n", + "print(x.tracks[0].harmony)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "muspy", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.6.15" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/tests/data/musicxml-lilypond/71a_f-Guitar_notation.xml b/tests/data/musicxml-lilypond/71a_f-Guitar_notation.xml new file mode 100644 index 0000000..f2f05ee --- /dev/null +++ b/tests/data/musicxml-lilypond/71a_f-Guitar_notation.xml @@ -0,0 +1,1017 @@ + + + + All MusicXML chord names/types with <root> + + + MuseScore 4.1.1 + 2024-11-27 + + + + + + + + + + MusicXML Part + + + keyboard.piano.grand + + + + 1 + 1 + 78.7402 + 0 + + + + + + + 2 + + 0 + + + + G + 2 + + + + + C + + major + + + + C + 4 + + 2 + 1 + quarter + up + + single + major + + + + + C + + minor + + + + C + 4 + + 2 + 1 + quarter + up + + single + minor + + + + + C + + augmented + + + + C + 4 + + 2 + 1 + quarter + up + + single + augmented + + + + + C + + diminished + + + + C + 4 + + 2 + 1 + quarter + up + + single + diminished + + + + + + + C + + dominant + + + + C + 4 + + 2 + 1 + quarter + up + + single + dominant + + + + + C + + major-seventh + + + + C + 4 + + 2 + 1 + quarter + up + + single + major-seventh + + + + + C + + minor-seventh + + + + C + 4 + + 2 + 1 + quarter + up + + single + minor-seventh + + + + + C + + diminished-seventh + + + + C + 4 + + 2 + 1 + quarter + up + + single + diminished-seventh + + + + + + + + C + + augmented-seventh + + + + C + 4 + + 2 + 1 + quarter + up + + single + augmented-seventh + + + + + C + + half-diminished + + + + C + 4 + + 2 + 1 + quarter + up + + single + half-diminished + + + + + C + + major-minor + + + + C + 4 + + 2 + 1 + quarter + up + + single + major-minor + + + + + C + + major-sixth + + + + C + 4 + + 2 + 1 + quarter + up + + single + major-sixth + + + + + + + + C + + minor-sixth + + + + C + 4 + + 2 + 1 + quarter + up + + single + minor-sixth + + + + + C + + dominant-ninth + + + + C + 4 + + 2 + 1 + quarter + up + + single + dominant-ninth + + + + + C + + major-ninth + + + + C + 4 + + 2 + 1 + quarter + up + + single + major-ninth + + + + + C + + minor-ninth + + + + C + 4 + + 2 + 1 + quarter + up + + single + minor-ninth + + + + + + + C + + dominant-11th + + + + C + 4 + + 2 + 1 + quarter + up + + single + dominant-11th + + + + + C + + major-11th + + + + C + 4 + + 2 + 1 + quarter + up + + single + major-11th + + + + + C + + minor-11th + + + + C + 4 + + 2 + 1 + quarter + up + + single + minor-11th + + + + + C + + dominant-13th + + + + C + 4 + + 2 + 1 + quarter + up + + single + dominant-13th + + + + + + + + C + + major-13th + + + + C + 4 + + 2 + 1 + quarter + up + + single + major-13th + + + + + C + + minor-13th + + + + C + 4 + + 2 + 1 + quarter + up + + single + minor-13th + + + + + C + + suspended-second + + + + C + 4 + + 2 + 1 + quarter + up + + single + suspended-second + + + + + C + + suspended-fourth + + + + C + 4 + + 2 + 1 + quarter + up + + single + suspended-fourth + + + + + + + C + + Neapolitan + + + + C + 4 + + 2 + 1 + quarter + up + + single + Neapolitan + + + + + C + + Italian + + + + C + 4 + + 2 + 1 + quarter + up + + single + Italians + + + + + C + + French + + + + C + 4 + + 2 + 1 + quarter + up + + single + French + + + + + C + + German + + + + C + 4 + + 2 + 1 + quarter + up + + single + German + + + + + + + + C + + pedal + + + + C + 4 + + 2 + 1 + quarter + up + + single + pedal + + + + + C + + power + + + + C + 4 + + 2 + 1 + quarter + up + + single + power + + + + + C + + Tristan + + + + C + 4 + + 2 + 1 + quarter + up + + single + Tristan + + + + + C + + other + + + + C + 4 + + 2 + 1 + quarter + up + + single + other + + + + + + + F + 1 + + major + + + + C + 4 + + 4 + 1 + half + up + + single + Inversion + + + + + F + -2 + + major + + C + + + + + C + 4 + + 1 + 1 + eighth + up + begin + + single + Fbb/C + + + + + G + 1 + + major + + D + 1 + + + + + C + 4 + + 1 + 1 + eighth + up + end + + single + G#/D# + + + + + C + + major + + + + 2 + 1 + quarter + + + + + + C + + major + + 3 + 0 + subtract + + + 5 + -1 + alter + + + + + C + 4 + + 2 + 1 + quarter + up + + single + C-3+5b + + + + + C + + major + + 1 + 0 + subtract + + + 6 + -1 + add + + + + + C + 4 + + 2 + 1 + quarter + up + + single + C-1+6b + + + + light-heavy + + + + + + C + + major + + + + A + 4 + + 2 + 1 + quarter + up + + + + C + + major-seventh + + 11 + 1 + add + + + + + A + 4 + + 2 + 1 + quarter + up + + + + B + + dominant + + 5 + 1 + alter + + + 9 + 1 + add + + + + + A + 4 + + 2 + 1 + quarter + up + + + + E + -1 + + major + + 2 + 0 + add + + + + + A + 4 + + 2 + 1 + quarter + up + + + + + + + G + + minor + + + + A + 4 + + 2 + 1 + quarter + up + + + + D + 1 + + major-seventh + + + + A + 4 + + 2 + 1 + quarter + up + + + + A + + diminished-seventh + + + + A + 4 + + 2 + 1 + quarter + up + + + + A + + augmented + + + + A + 4 + + 2 + 1 + quarter + up + + + light-heavy + + + + diff --git a/tests/data/musicxml-lilypond/A_Beautiful_Friendship.mxl b/tests/data/musicxml-lilypond/A_Beautiful_Friendship.mxl new file mode 100644 index 0000000..e42e0e8 Binary files /dev/null and b/tests/data/musicxml-lilypond/A_Beautiful_Friendship.mxl differ diff --git a/tests/data/musicxml-lilypond/A_Beautiful_Friendship.xml b/tests/data/musicxml-lilypond/A_Beautiful_Friendship.xml new file mode 100644 index 0000000..715f867 --- /dev/null +++ b/tests/data/musicxml-lilypond/A_Beautiful_Friendship.xml @@ -0,0 +1,1345 @@ + + + + + + MuseScore 3.6.2 + 2022-12-10 + + + + + + + + + + 7 + 40 + + + 1697.14 + 1200 + + 57.1429 + 57.1429 + 57.1429 + 114.286 + + + 57.1429 + 57.1429 + 57.1429 + 114.286 + + + + + + + title + A Beautiful Friendship + + + composer + Donald Kahn + + + + Lead sheet + + + + + + 1 + 1 + 78.7402 + 0 + + + + + + + + + 50.00 + -0.00 + + 244.33 + + + + heavy-light + + + + 6 + + 0 + + + + G + 2 + + + + + C + + major-seventh + + + + A + + + + + Swing + + + + + 140_C + + + + + E + 4 + + 12 + + 1 + half + up + + + + + + + E + 4 + + 3 + + 1 + eighth + up + + + + + + + E + 4 + + 6 + 1 + quarter + up + + + + D + 1 + 4 + + 3 + 1 + eighth + sharp + up + + + + + + G + -1 + + dominant + + 11 + 1 + add + + + + + E + 4 + + 12 + + 1 + half + up + + + + + + + E + 4 + + 3 + + 1 + eighth + up + + + + + + + E + 4 + + 6 + 1 + quarter + up + + + + D + 1 + 4 + + 3 + 1 + eighth + sharp + up + + + + + + F + + major-seventh + + + + E + 4 + + 12 + + 1 + half + up + + + + + + + E + 4 + + 4 + + 1 + quarter + + 3 + 2 + + up + + + + + + + + C + 4 + + 4 + 1 + quarter + + 3 + 2 + + up + + + + D + 4 + + 4 + 1 + quarter + + 3 + 2 + + up + + + + + + + + + E + + half-diminished + + + + E + 4 + + 6 + 1 + quarter + up + + + + A + + dominant + + 9 + -1 + add + + 6 + + + + F + 4 + + 12 + 1 + half + up + + + + E + 4 + + 6 + 1 + quarter + up + + + + + + + -0.00 + -0.00 + + 150.00 + + + + + A + + minor-seventh + + + + D + 4 + + 12 + + 1 + half + up + + + + + + + D + 4 + + 4 + + 1 + quarter + + 3 + 2 + + up + + + + + + + + B + 3 + + 4 + 1 + quarter + + 3 + 2 + + up + + + + C + 4 + + 4 + 1 + quarter + + 3 + 2 + + up + + + + + + + + + D + + dominant + + + + D + 4 + + 6 + 1 + quarter + up + + + + E + 4 + + 12 + 1 + half + up + + + + C + 4 + + 6 + 1 + quarter + up + + + + + + D + + minor-seventh + + + + D + 4 + + 24 + 1 + whole + + + + + + G + + dominant + + + + D + 1 + 4 + + 24 + 1 + whole + sharp + + + light-heavy + + + + + + + + -0.00 + -0.00 + + 150.00 + + + + + G + + minor-seventh + + + + B + + + + + A + 4 + + 12 + + 1 + half + up + + + + + + + A + 4 + + 4 + + 1 + quarter + + 3 + 2 + + up + + + + + + + + A + 4 + + 4 + 1 + quarter + + 3 + 2 + + up + + + + G + 4 + + 4 + 1 + quarter + + 3 + 2 + + up + + + + + + + + + C + + dominant + + + + A + 4 + + 18 + 1 + half + + up + + + + E + 4 + + 6 + 1 + quarter + up + + + + + + F + + major-seventh + + + + G + 4 + + 12 + + 1 + half + up + + + + + + + G + 4 + + 4 + + 1 + quarter + + 3 + 2 + + up + + + + + + + + G + 4 + + 4 + 1 + quarter + + 3 + 2 + + up + + + + F + 4 + + 4 + 1 + quarter + + 3 + 2 + + up + + + + + + + + + B + -1 + + dominant + + + + G + 4 + + 6 + 1 + quarter + up + + + + G + 1 + 4 + + 18 + 1 + half + + sharp + up + + + + + + + -0.00 + -0.00 + + 150.00 + + + + + E + + minor-seventh + + + + A + 4 + + 12 + + 1 + half + up + + + + + + + A + 4 + + 4 + + 1 + quarter + + 3 + 2 + + up + + + + + + + + A + 4 + + 4 + 1 + quarter + + 3 + 2 + + up + + + + G + 4 + + 4 + 1 + quarter + + 3 + 2 + + up + + + + + + + + + A + + dominant + + + + A + 4 + + 6 + 1 + quarter + up + + + + G + 4 + + 12 + 1 + half + up + + + + E + 4 + + 6 + 1 + quarter + up + + + + + + D + + dominant + + + + D + 4 + + 12 + + 1 + half + up + + + + + + + D + 4 + + 4 + + 1 + quarter + + 3 + 2 + + up + + + + + + + + D + 4 + + 4 + 1 + quarter + + 3 + 2 + + up + + + + C + 4 + + 4 + 1 + quarter + + 3 + 2 + + up + + + + + + + + + G + + dominant + + 5 + 1 + alter + + + + + D + 4 + + 6 + 1 + quarter + up + + + + D + 1 + 4 + + 12 + 1 + half + sharp + up + + + + 6 + 1 + quarter + + + light-light + + + + + + + -0.00 + -0.00 + + 150.00 + + + + + C + + major-seventh + + + + A + + + + + E + 4 + + 12 + + 1 + half + up + + + + + + + E + 4 + + 3 + + 1 + eighth + up + + + + + + + E + 4 + + 6 + 1 + quarter + up + + + + D + 1 + 4 + + 3 + 1 + eighth + sharp + up + + + + + + G + -1 + + dominant + + 11 + 1 + add + + + + + E + 4 + + 12 + + 1 + half + up + + + + + + + E + 4 + + 3 + + 1 + eighth + up + + + + + + + E + 4 + + 6 + 1 + quarter + up + + + + D + 1 + 4 + + 3 + 1 + eighth + sharp + up + + + + + + F + + major-seventh + + + + E + 4 + + 12 + 1 + half + up + + + + E + 4 + + 4 + 1 + quarter + + 3 + 2 + + up + + + + + + + C + 4 + + 4 + 1 + quarter + + 3 + 2 + + up + + + + D + 4 + + 4 + 1 + quarter + + 3 + 2 + + up + + + + + + + + + E + + half-diminished + + + + E + 4 + + 6 + 1 + quarter + up + + + + A + + dominant + + 9 + -1 + add + + 6 + + + + A + 4 + + 12 + 1 + half + up + + + + B + 4 + + 6 + 1 + quarter + down + + + + + + + -0.00 + 0.00 + + 150.00 + + + + + D + + minor-seventh + + + + C + 5 + + 12 + 1 + half + down + + + + D + 5 + + 6 + 1 + quarter + down + + + + B + 4 + + 6 + 1 + quarter + down + + + + + + G + + dominant + + + + C + 5 + + 6 + 1 + quarter + down + + + + D + 5 + + 12 + 1 + half + down + + + + B + 4 + + 6 + 1 + quarter + down + + + + + + C + + major-sixth + + + + C + 5 + + 24 + + 1 + whole + + + + + + + + + C + + major-sixth + + + + C + 5 + + 24 + + 1 + whole + + + + + + light-heavy + + + + diff --git a/tests/data/musicxml-lilypond/bass_test.mxl b/tests/data/musicxml-lilypond/bass_test.mxl new file mode 100644 index 0000000..cc75b57 Binary files /dev/null and b/tests/data/musicxml-lilypond/bass_test.mxl differ diff --git a/tests/test_musicxml.py b/tests/test_musicxml.py index 59200e2..ee74415 100644 --- a/tests/test_musicxml.py +++ b/tests/test_musicxml.py @@ -684,3 +684,9 @@ def test_write_compressed(): check_time_signatures(loaded.time_signatures) check_tracks(loaded.tracks, 10080) # TODO: Check lyrics and annotations + +def test_chord_symbols(): + loaded = muspy.read('tests/data/musicxml-lilypond/71a_f-Guitar_notation.xml') + # loaded = muspy.read('tests/data/musicxml-lilypond/bass_test.mxl') + print(loaded) + return loaded