Repository: cuthbertLab/music21
Pull request #805 (2021-02-03 20:08:07)

minor typo in usersGuide_17_derivations.ipynb
wnat > want

coveralls: 
[![Coverage Status](https://coveralls.io/builds/36842033/badge)](https://coveralls.io/builds/36842033)

Coverage remained the same at 91.475% when pulling **f15e5a40fa62ed5eec77407141550ac428329f4b on Meekohi:patch-1** into **2a3ba8d6ba759897bbd93879b70cae23ca111fd9 on cuthbertLab:master**.


Patch:
diff --git a/.gitignore b/.gitignore
index 5571c19ff..6cc846870 100644
--- a/.gitignore
+++ b/.gitignore
@@ -17,7 +17,7 @@ __pycache__/
 .idea/other.xml
 .idea/vcs.xml
 .idea/workspace.xml
-
+.idea/markdown-*.xml
 
 # OSX
 .DS_Store
diff --git a/documentation/source/usersGuide/usersGuide_17_derivations.ipynb b/documentation/source/usersGuide/usersGuide_17_derivations.ipynb
index e2ad7bb79..00a9a3228 100644
--- a/documentation/source/usersGuide/usersGuide_17_derivations.ipynb
+++ b/documentation/source/usersGuide/usersGuide_17_derivations.ipynb
@@ -351,7 +351,7 @@
    "cell_type": "markdown",
    "metadata": {},
    "source": [
-    "If we wnat to know the whole history of where this Stream comes from, we can use the `.chain()` method on the Derivation object:"
+    "If we want to know the whole history of where this Stream comes from, we can use the `.chain()` method on the Derivation object:"
    ]
   },
   {
diff --git a/music21/abcFormat/__init__.py b/music21/abcFormat/__init__.py
index d63796501..70f183eb5 100644
--- a/music21/abcFormat/__init__.py
+++ b/music21/abcFormat/__init__.py
@@ -1641,21 +1641,40 @@ class ABCChord(ABCNote):
 
     A subclass of ABCNote.
     '''
-    def __init__(self, src):
+
+    def __init__(self, src: str = ''):
         super().__init__(src)
         # store a list of component objects
         self.subTokens = []
 
     def parse(self, forceKeySignature=None, forceDefaultQuarterLength=None):
+        '''
+        Handles the following types of chords:
+
+        * Chord without length modifier: [ceg]
+
+        * Chords with outer length modifier: [ceg]2, [ceg]/2
+
+        * Chords with inner length modifier: [c2e2g2], [c2eg]
+
+        * Chords with inner and outer length modifier: [c2e2g2]/2, [c/2e/2g/2]2
+        '''
+
         self.chordSymbols, nonChordSymStr = self._splitChordSymbols(self.src)
 
-        tokenStr = nonChordSymStr[1:-1]  # remove outer brackets
-        # environLocal.printDebug(['ABCChord:', nonChordSymStr, 'tokenStr', tokenStr])
+        # position of the closing bracket
+        pos = nonChordSymStr.index(']')
+        # Length modifier string behind the chord brackets
+        outerLengthModifierStr = nonChordSymStr[pos + 1:]
+        # String in the chord brackets
+        tokenStr = nonChordSymStr[1:pos]
 
-        self.quarterLength = self.getQuarterLength(
-            nonChordSymStr,
-            forceDefaultQuarterLength=forceDefaultQuarterLength
-        )
+        # environLocal.printDebug(['ABCChord:', nonChordSymStr, 'tokenStr', tokenStr, '
+        # outerLengthModifierStr', outerLengthModifierStr])
+
+        # Get the outer chord length modifier if present
+        outer_lengthModifier = self.getQuarterLength(outerLengthModifierStr,
+                                                     forceDefaultQuarterLength=1.0)
 
         if forceKeySignature is not None:
             activeKeySignature = forceKeySignature
@@ -1669,7 +1688,7 @@ class ABCChord(ABCNote):
         # may need to supply key?
         ah.tokenize(tokenStr)
 
-        chordDurationPost = None
+        inner_quarterLength = 0
         # tokens contained here are each ABCNote instances
         for t in ah.tokens:
             # environLocal.printDebug(['ABCChord: subTokens', t])
@@ -1678,14 +1697,22 @@ class ABCChord(ABCNote):
                 t.parse(
                     forceDefaultQuarterLength=self.activeDefaultQuarterLength,
                     forceKeySignature=activeKeySignature)
+
+                if t.isRest:
+                    continue
+
                 # get the quarter length from the sub-tokens
-                # note: assuming these are the same
-                chordDurationPost = t.quarterLength
-            if isinstance(t, ABCNote) and not t.isRest:
+                # All the notes within a chord should normally have the same length,
+                # but if not, the chord duration is that of the first note.
+                if not inner_quarterLength:
+                    inner_quarterLength = t.quarterLength
+
                 self.subTokens.append(t)
 
-        if chordDurationPost is not None:
-            self.quarterLength = chordDurationPost
+
+        # When both inside and outside the chord length modifiers are used,
+        # they should be multiplied. Example: [C2E2G2]3 has the same meaning as [CEG]6.
+        self.quarterLength = outer_lengthModifier * inner_quarterLength
 
 
 # ------------------------------------------------------------------------------
@@ -1701,7 +1728,7 @@ class ABCHandler:
     define new phrases.  This is useful for parsing extra information from
     the Essen Folksong repertory
 
-    New in v6.2 -- lineBreaksDefinePhrases -- does not yet do anything
+    New in v6.3 -- lineBreaksDefinePhrases -- does not yet do anything
     '''
     def __init__(self, abcVersion=None, lineBreaksDefinePhrases=False):
         # tokens are ABC objects import n a linear stream
@@ -2193,9 +2220,17 @@ class ABCHandler:
             # get chords
             if c == '[':
                 j = self.pos + 1
+
+                # find closing chord bracket
                 while j < self.srcLen - 1 and self.strSrc[j] != ']':
                     j += 1
+
                 j += 1  # need character that caused break
+
+                # find outer chord length modifier
+                while j < self.srcLen and (self.strSrc[j].isdigit() or self.strSrc[j] in '/'):
+                    j += 1
+
                 # prepend chord symbol
                 if activeChordSymbol != '':
                     self.currentCollectStr = activeChordSymbol + self.strSrc[self.pos:j]
diff --git a/music21/abcFormat/translate.py b/music21/abcFormat/translate.py
index f559311af..5af2f8ceb 100644
--- a/music21/abcFormat/translate.py
+++ b/music21/abcFormat/translate.py
@@ -265,32 +265,6 @@ def parseTokens(mh, dst, p, useMeasures):
                 mmObj = t.getMetronomeMarkObject()
                 dst.coreAppend(mmObj)
 
-        # as ABCChord is subclass of ABCNote, handle first
-        elif isinstance(t, abcFormat.ABCChord):
-            # may have more than notes?
-            pitchNameList = []
-            accStatusList = []  # accidental display status list
-            for tSub in t.subTokens:
-                # notes are contained as subTokens are already parsed
-                if isinstance(tSub, abcFormat.ABCNote):
-                    pitchNameList.append(tSub.pitchName)
-                    accStatusList.append(tSub.accidentalDisplayStatus)
-            c = chord.Chord(pitchNameList)
-            c.duration.quarterLength = t.quarterLength
-            if t.activeTuplet:
-                thisTuplet = copy.deepcopy(t.activeTuplet)
-                if thisTuplet.durationNormal is None:
-                    thisTuplet.setDurationType(c.duration.type, c.duration.dots)
-                c.duration.appendTuplet(thisTuplet)
-            # adjust accidental display for each contained pitch
-            for pIndex in range(len(c.pitches)):
-                if c.pitches[pIndex].accidental is None:
-                    continue
-                c.pitches[pIndex].accidental.displayStatus = accStatusList[pIndex]
-            dst.coreAppend(c)
-
-            # ql += t.quarterLength
-
         elif isinstance(t, abcFormat.ABCNote):
             # add the attached chord symbol
             if t.chordSymbols:
@@ -307,46 +281,77 @@ def parseTokens(mh, dst, p, useMeasures):
                     dst.coreElementsChanged()
                 except ValueError:
                     pass  # Exclude malformed chord
-            if t.isRest:
-                n = note.Rest()
-            else:
-                n = note.Note(t.pitchName)
-                if n.pitch.accidental is not None:
-                    n.pitch.accidental.displayStatus = t.accidentalDisplayStatus
-
-            n.duration.quarterLength = t.quarterLength
-            if t.activeTuplet:
-                thisTuplet = copy.deepcopy(t.activeTuplet)
-                if thisTuplet.durationNormal is None:
-                    thisTuplet.setDurationType(n.duration.type, n.duration.dots)
-                n.duration.appendTuplet(thisTuplet)
-
-            # start or end a tie at note n
-            if t.tie is not None:
-                if t.tie in ('start', 'continue'):
-                    n.tie = tie.Tie(t.tie)
-                    n.tie.style = 'normal'
-                elif t.tie == 'stop':
-                    n.tie = tie.Tie(t.tie)
-            # Was: Extremely Slow for large Opus files... why?
-            # Answer: some pieces didn't close all their spanners, so
-            # everything was in a Slur/Diminuendo, etc.
-            for span in t.applicableSpanners:
-                span.addSpannedElements(n)
-
-            if t.inGrace:
-                n = n.getGrace()
-
-            n.articulations = []
-            while any(t.articulations):
-                tokenArticulationStr = t.articulations.pop()
-                if tokenArticulationStr not in _abcArticulationsToM21:
+
+            # as ABCChord is subclass of ABCNote, handle first
+            if isinstance(t, abcFormat.ABCChord):
+                # Skip an empty chord
+                if not t.subTokens:
                     continue
-                m21ArticulationClass = _abcArticulationsToM21[tokenArticulationStr]
-                m21ArticulationObj = m21ArticulationClass()
-                n.articulations.append(m21ArticulationObj)
 
-            dst.coreAppend(n, setActiveSite=False)
+                # may have more than notes?
+                pitchNameList = []
+                accStatusList = []  # accidental display status list
+                for tSub in t.subTokens:
+                    # notes are contained as subTokens are already parsed
+                    if isinstance(tSub, abcFormat.ABCNote):
+                        pitchNameList.append(tSub.pitchName)
+                        accStatusList.append(tSub.accidentalDisplayStatus)
+                c = chord.Chord(pitchNameList)
+                c.duration.quarterLength = t.quarterLength
+                if t.activeTuplet:
+                    thisTuplet = copy.deepcopy(t.activeTuplet)
+                    if thisTuplet.durationNormal is None:
+                        thisTuplet.setDurationType(c.duration.type, c.duration.dots)
+                    c.duration.appendTuplet(thisTuplet)
+                # adjust accidental display for each contained pitch
+                for pIndex in range(len(c.pitches)):
+                    if c.pitches[pIndex].accidental is None:
+                        continue
+                    c.pitches[pIndex].accidental.displayStatus = accStatusList[pIndex]
+                dst.coreAppend(c)
+
+                # ql += t.quarterLength
+            else:
+                if t.isRest:
+                    n = note.Rest()
+                else:
+                    n = note.Note(t.pitchName)
+                    if n.pitch.accidental is not None:
+                        n.pitch.accidental.displayStatus = t.accidentalDisplayStatus
+
+                n.duration.quarterLength = t.quarterLength
+                if t.activeTuplet:
+                    thisTuplet = copy.deepcopy(t.activeTuplet)
+                    if thisTuplet.durationNormal is None:
+                        thisTuplet.setDurationType(n.duration.type, n.duration.dots)
+                    n.duration.appendTuplet(thisTuplet)
+
+                # start or end a tie at note n
+                if t.tie is not None:
+                    if t.tie in ('start', 'continue'):
+                        n.tie = tie.Tie(t.tie)
+                        n.tie.style = 'normal'
+                    elif t.tie == 'stop':
+                        n.tie = tie.Tie(t.tie)
+                # Was: Extremely Slow for large Opus files... why?
+                # Answer: some pieces didn't close all their spanners, so
+                # everything was in a Slur/Diminuendo, etc.
+                for span in t.applicableSpanners:
+                    span.addSpannedElements(n)
+
+                if t.inGrace:
+                    n = n.getGrace()
+
+                n.articulations = []
+                while any(t.articulations):
+                    tokenArticulationStr = t.articulations.pop()
+                    if tokenArticulationStr not in _abcArticulationsToM21:
+                        continue
+                    m21ArticulationClass = _abcArticulationsToM21[tokenArticulationStr]
+                    m21ArticulationObj = m21ArticulationClass()
+                    n.articulations.append(m21ArticulationObj)
+
+                dst.coreAppend(n, setActiveSite=False)
 
         elif isinstance(t, abcFormat.ABCSlurStart):
             p.coreAppend(t.slurObj)


Test patch:
diff --git a/music21/abcFormat/testFiles.py b/music21/abcFormat/testFiles.py
index c519ac80c..27bc6dbb2 100644
--- a/music21/abcFormat/testFiles.py
+++ b/music21/abcFormat/testFiles.py
@@ -816,10 +816,130 @@ class Test(unittest.TestCase):
         self.assertEqual(notes[8].pitch.midi, 65, 'Natural is ignored')
         self.assertEqual(notes[12].pitch.midi, 72, 'Natural is ignored')
 
+    def testAbc21Chords(self):
+        '''
+        Translation of ABC Chord variations
+        '''
+        from music21 import abcFormat, chord
+        from music21.abcFormat import translate
+
+        af = abcFormat.ABCFile()
+        # default length of this test
+        abc_dl = 'L:1/8\n'
+
+        # Empty Chords should be skipped at all
+        for abc_chord in ['[]', '[z]']:
+            ah = af.readstr(abc_dl + '[]')
+            s = translate.abcToStreamScore(ah)
+            part = s.parts[0]
+            self.assertFalse(part.getElementsByClass(chord.Chord),
+                             'Empty chord "%s" in Score' % abc_chord)
+
+        # list of test abc chords and their quarter lengths at the default length of 1/8
+        # list[tuple(str, int)] = of abc chords and= [( abc_chord: str)]
+        abc_chords = [
+            ('[c_eg]', 0.5, ['C', 'E-', 'G']),
+            ('[ceg]', 0.5, 'CEG'),
+            ('[ceg]2', 1.0, 'CEG'),
+            ('[c2e2^g2]', 1.0, ['C', 'E', 'G#']),
+            ("[c'e2g]", 0.5, 'CEG'),
+            ('[ce^g2]', 0.5, ['C', 'E', 'G#']),
+            ('[c,2e2g2]/2', 0.5, 'CEG'),
+            ("[c/2e'/2=g/2]", 0.25, 'CEG'),
+            ('[c2_e,,/2g/2]/2', 0.5, ['C', 'E-', 'G']),
+            ('[c/2e/2g/2]2', 0.5, 'CEG'),
+            ('[^c/2e/2g/2]/2', 0.125, ['C#', 'E', 'G']),
+            ('[ceg]', 0.5, 'CEG'),
+        ]
+
+        for abc_chord, quarter_length, chord_pitches in abc_chords:
+            ah = af.readstr(abc_dl + abc_chord)
+            s = translate.abcToStreamScore(ah)
+            self.assertEqual(s.duration.quarterLength, quarter_length,
+                             'invalid duration of chord "%s"' % abc_chord)
+
+            notes = s.parts[0].notes
+            chord0 = notes[0]
+            self.assertEqual(len(notes), 1, 'Wrong number of chords found,')
+            self.assertIsInstance(chord0, chord.Chord, 'Not a Chord!')
+            for pitch_name in chord_pitches:
+                self.assertIn(pitch_name, chord0.pitchNames,
+                              'Pitch not in Chord "%s"' % abc_chord)
+
+    def testAbc21ChordSymbol(self):
+        # Test the chord symbol for note and chord
+        from music21 import abcFormat, harmony
+        from music21.abcFormat import translate
+
+        # default length of this test
+        abc_dl = 'L:1/8\n'
+
+        af = abcFormat.ABCFile()
+        for abc_text in ('"C"C', '"C"[ceg]'):
+            ah = af.readstr(abc_dl + abc_text)
+            part = translate.abcToStreamScore(ah).parts[0]
+            chord_symbol = part.getElementsByClass(harmony.ChordSymbol)
+            self.assertTrue(chord_symbol, 'No ChordSymbol found in abc: "%s"' % abc_text)
+            for pitch_name in 'CEG':
+                self.assertIn(pitch_name, chord_symbol[0].pitchNames,
+                              'Pitch not in ChordSymbol of abc: "%s"' % abc_text)
+
+    def testAbc21BrokenRhythm(self):
+        # Test the chord symbol for note and chord
+        from music21 import abcFormat, note
+        from music21.abcFormat import translate
+
+        # default length of this test
+        abc_dl = 'L:1/4\n'
+
+        # test abc strings of broken rhythm between 2 notes and/or chords and their
+        # quarter lengths at the default length of 1/4
+        # list[tuple(abc: str, value1: int, value2: int)]
+        data = [
+            ('[ceg]<f', 0.5, 1.5),
+            ('f<[ceg]', 0.5, 1.5),
+            ('c>g', 1.5, 0.5),
+            ('c<g', 0.5, 1.5),
+            ('c>>=g', 1.75, 0.25),
+            ('c<<g', 0.25, 1.75),
+            ('c>>>g', 1.875, 0.125),
+            ('c<<<_g', 0.125, 1.875),
+            ("[ceg]>^f", 1.5, 0.5),
+            ('[ce^g]>>f', 1.75, 0.25),
+            ("[ceg]<<f", 0.25, 1.75),
+            ('[ceg]>>>f', 1.875, 0.125),
+            ("[ceg]<<<f", 0.125, 1.875),
+            ('f>[ceg]', 1.5, 0.5),
+            ('f>>[_ceg]', 1.75, 0.25),
+            ("f'<<[ceg]", 0.25, 1.75),
+            ('f,>>>[ceg]', 1.875, 0.125),
+            ('f<<<[ce_g]', 0.125, 1.875),
+            ('f<<<[ceg]', 0.125, 1.875),
+            ('f2>[ceg]', 3, 0.5),
+            ('[ceg]>f2', 1.5, 1),
+            ('f>[c_eg]2', 1.5, 1),
+            ('[c^eg]2>f', 3, 0.5),
+            ('f2<[ceg]', 1.0, 1.5),
+            ('[ceg]<f2', 0.5, 3),
+            ('f<[ceg]2', 0.5, 3),
+            ('[ceg]2<f', 1.0, 1.5),
+        ]
+
+        af = abcFormat.ABCFile()
+        for abc, soll_left, soll_right in data:
+            ah = af.readstr(abc_dl + abc)
+            part = translate.abcToStreamScore(ah).parts[0]
+            general_notes = part.getElementsByClass(note.GeneralNote)
+            self.assertEqual(len(general_notes), 2,
+                             f'Wrong numbers of Notes found in abc: {abc}!')
+            ist_left, ist_right = general_notes
+            self.assertEqual(ist_left.duration.quarterLength, soll_left,
+                             f'Invalid left note/chord length of abc broken rhythm: {abc}')
+            self.assertEqual(ist_right.duration.quarterLength, soll_right,
+                             f'Invalid right note/chord length of abc broken rhythm: {abc}')
+
 
 if __name__ == '__main__':
     import music21
     # music21.converter.parse(reelsABC21, format='abc').scores[1].show()
     music21.mainTest(Test)
-
-