Repository: cuthbertLab/music21
Pull request #997 (2021-05-02 17:58:10)

v7 `Stream.makeNotation(inPlace=True)` returns None
This will now match `Score.makeNotation()` and `Measure.makeNotation()`.

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

Coverage increased (+9.0e-05%) to 92.251% when pulling **b93e71235f6bbcfc0a02bd7d6f0661b5348249e0 on jacobtylerwalls:makeNotation-inPlace** into **af666853a104933fe425eb0a354128f144f417f9 on cuthbertLab:master**.


Patch:
diff --git a/.github/scripts/install_ubuntu_deps.sh b/.github/scripts/install_ubuntu_deps.sh
index b2447b473..0575cf971 100755
--- a/.github/scripts/install_ubuntu_deps.sh
+++ b/.github/scripts/install_ubuntu_deps.sh
@@ -5,7 +5,7 @@ set -e  # error if anything returns non-zero exit code
 mkdir ~/Desktop
 
 sudo apt-get install -y libpng-dev
-# sudo apt-get install -y python-qt4
+# sudo apt-get install -y python3-pyqt5
 wget -q https://lilypond.org/download/binaries/linux-64/lilypond-2.22.0-1.linux-64.sh
 sh lilypond-2.22.0-1.linux-64.sh --batch
 export PATH=/home/runner/bin:$PATH
@@ -15,5 +15,4 @@ pip3 install scipy
 pip3 install python-Levenshtein
 pip3 install setuptools
 pip3 install coverage
-cd music21
-python -m compileall music21
+python3 -m compileall music21
diff --git a/.gitignore b/.gitignore
index 6cc846870..d1c020241 100644
--- a/.gitignore
+++ b/.gitignore
@@ -60,6 +60,9 @@ nosetests.xml
 .pydevproject
 /.settings
 
+# VS Code
+/.vscode
+
 # remove all from autogenerated except conf.py
 documentation/autogenerated/**/*
 documentation/autogenerated/**/*.png
diff --git a/README.md b/README.md
index 1ddda9911..c43588b2f 100644
--- a/README.md
+++ b/README.md
@@ -10,7 +10,7 @@ http://web.mit.edu/music21 or http://music21.readthedocs.org/en/latest/index.htm
 And to install, see:
 http://web.mit.edu/music21/doc/usersGuide/usersGuide_01_installing.html
 
-Music21 runs on Python 3.6+.  User version 4 on Python 2 or Py3.4, version 5 
+Music21 runs on Python 3.6+.  Use version 4 on Python 2 or Py3.4, version 5
 on Py3.5.
 
 Released under the BSD (3-clause) license. Music21 may also be used
diff --git a/documentation/source/usersGuide/usersGuide_15_key.ipynb b/documentation/source/usersGuide/usersGuide_15_key.ipynb
index bf1167ec1..078f001c6 100644
--- a/documentation/source/usersGuide/usersGuide_15_key.ipynb
+++ b/documentation/source/usersGuide/usersGuide_15_key.ipynb
@@ -1344,7 +1344,7 @@
     }
    ],
    "source": [
-    "bach.plot('key')"
+    "bach.flat.plot('key')"
    ]
   },
   {
diff --git a/documentation/source/usersGuide/usersGuide_32_articulations.ipynb b/documentation/source/usersGuide/usersGuide_32_articulations.ipynb
index 31317651a..c3a05bca5 100644
--- a/documentation/source/usersGuide/usersGuide_32_articulations.ipynb
+++ b/documentation/source/usersGuide/usersGuide_32_articulations.ipynb
@@ -471,7 +471,7 @@
    "cell_type": "markdown",
    "metadata": {},
    "source": [
-    "Again, unfortuantely, some MusicXML readers do not respect this value.\n",
+    "Again, unfortunately, some MusicXML readers do not respect this value.\n",
     "\n",
     "MusicXML distinguishes articulations from technical indicates, but in `music21` these are all in the `articulations` module and stored in the `.articulations` list.  They do, however, have `TechnicalIndication` in their classes: "
    ]
diff --git a/documentation/source/usersGuide/usersGuide_53_advancedCorpus.ipynb b/documentation/source/usersGuide/usersGuide_53_advancedCorpus.ipynb
index 891651eea..b4033592c 100644
--- a/documentation/source/usersGuide/usersGuide_53_advancedCorpus.ipynb
+++ b/documentation/source/usersGuide/usersGuide_53_advancedCorpus.ipynb
@@ -437,7 +437,7 @@
    "cell_type": "markdown",
    "metadata": {},
    "source": [
-    "Consult the API for class:`~music21.metadata.bundles.MetadataBundle` for a more\n",
+    "Consult the API for :class:`~music21.metadata.bundles.MetadataBundle` for a more\n",
     "in depth look at how this works."
    ]
   },
@@ -693,7 +693,7 @@
    "metadata": {},
    "source": [
     "You can delete, rebuild and save a metadata bundle in one go with the\n",
-    "``rebuild()`` method:"
+    "``rebuildMetadataCache()`` method:"
    ]
   },
   {
@@ -703,7 +703,7 @@
    "outputs": [],
    "source": [
     "localBundle = corpus.corpora.LocalCorpus().metadataBundle\n",
-    "#_DOCS_SHOW localBundle.rebuild()"
+    "#_DOCS_SHOW localBundle.rebuildMetadataCache()"
    ]
   },
   {
diff --git a/music21/analysis/discrete.py b/music21/analysis/discrete.py
index ed01a56f8..afec97193 100644
--- a/music21/analysis/discrete.py
+++ b/music21/analysis/discrete.py
@@ -453,6 +453,7 @@ class KeyWeightKeyAnalysis(DiscreteAnalysis):
             'B-', 'B',
         ]
 
+        colorsUsed = []
         if compress:
             colorsUsed = self.getColorsUsed()
             solutionsUsed = self.getSolutionsUsed()
@@ -471,7 +472,6 @@ class KeyWeightKeyAnalysis(DiscreteAnalysis):
 
         data = []
         valid = None
-        colorsUsed = []
 
         for yLabel in ['Major', 'Minor']:
             if yLabel == 'Major':
@@ -983,7 +983,7 @@ class Ambitus(DiscreteAnalysis):
         else:  # create minPitch maxPitch
             maxPitch = numColors
 
-        valueRange = maxPitch
+        valueRange = maxPitch - minPitch
         if valueRange == 0:
             valueRange = 1  # avoid float division by zero
         step = 0
diff --git a/music21/audioSearch/recording.py b/music21/audioSearch/recording.py
index 2bfbf4d04..d270d54c0 100644
--- a/music21/audioSearch/recording.py
+++ b/music21/audioSearch/recording.py
@@ -53,14 +53,9 @@ def samplesFromRecording(seconds=10.0, storeFile=True,
 
     Returns a list of samples.
     '''
-    try:
-        # noinspection PyPackageRequirements
-        import pyaudio  # @UnresolvedImport
-        recordFormatDefault = pyaudio.paInt16
-    except (ImportError, SystemExit):
-        pyaudio = None
-        environLocal.warn("No Pyaudio found. Recording will probably not work.")
-        recordFormatDefault = 8  # pyaudio.paInt16
+    # noinspection PyPackageRequirements
+    import pyaudio  # @UnresolvedImport  # pylint: disable=import-error
+    recordFormatDefault = pyaudio.paInt16
 
     if recordFormat is None:
         recordFormat = recordFormatDefault
diff --git a/music21/audioSearch/transcriber.py b/music21/audioSearch/transcriber.py
index 65d1abf13..127be3dde 100644
--- a/music21/audioSearch/transcriber.py
+++ b/music21/audioSearch/transcriber.py
@@ -48,13 +48,12 @@ def runTranscribe(show=True, plot=True, useMic=True,
     if useScale is None:
         useScale = scale.ChromaticScale('C4')
     # beginning - recording or not
-    if saveFile is not False:
-        if saveFile:
-            waveFilename = environLocal.getRootTempDir() / 'ex.wav'
-        else:
-            waveFilename = saveFile
-    else:
+    if saveFile is True:
+        waveFilename = environLocal.getRootTempDir() / 'ex.wav'
+    elif saveFile is False:
         waveFilename = False
+    else:
+        waveFilename = saveFile
 
     # the rest of the score
     if useMic is True:
diff --git a/music21/graph/plot.py b/music21/graph/plot.py
index 298a5928b..a71eb4816 100644
--- a/music21/graph/plot.py
+++ b/music21/graph/plot.py
@@ -1665,6 +1665,20 @@ class Test(unittest.TestCase):
                         minWindow=1, windowStep=windowStep,
                         doneAction=doneAction, dpi=300)
         b.run()
+        self.assertEqual(b.graphLegend.data,
+            [
+                ['Major',
+                    [('C#', '#f0727a'), ('D', '#ffd752'), ('E', '#eeff9a'),
+                     ('F#', '#b9f0ff'), ('A', '#bb9aff'), ('B', '#ffb5ff')
+                     ]
+                 ],
+                ['Minor',
+                    [('c#', '#8c0e16'), ('', '#ffffff'), ('', '#ffffff'),
+                     ('f#', '#558caa'), ('', '#ffffff'), ('b', '#9b519b')
+                     ]
+                 ]
+            ]
+        )
 
     def testFeatures(self):
         streamList = ['bach/bwv66.6', 'schoenberg/opus19/movement2', 'corelli/opus3no1/1grave']
diff --git a/music21/metadata/bundles.py b/music21/metadata/bundles.py
index 53c735502..2471e5660 100644
--- a/music21/metadata/bundles.py
+++ b/music21/metadata/bundles.py
@@ -815,7 +815,7 @@ class MetadataBundle(prebase.ProtoM21Object):
             accumulatedErrors.extend(result['errors'])
             for metadataEntry in result['metadataEntries']:
                 self._metadataEntries[metadataEntry.corpusPath] = metadataEntry
-            if (currentIteration % 50) and (storeOnDisk is True) == 0:
+            if (currentIteration % 50 == 0) and storeOnDisk is True:
                 self.write()
         self.validate()
         if storeOnDisk is True:
diff --git a/music21/midi/__init__.py b/music21/midi/__init__.py
index efb2f7482..14897e7c0 100644
--- a/music21/midi/__init__.py
+++ b/music21/midi/__init__.py
@@ -29,7 +29,6 @@ __all__ = [
     'DeltaTime',
     'MetaEvents', 'ChannelVoiceMessages', 'ChannelModeMessages',
     'SysExEvents',
-    'EnumerationException',
 ]
 
 import io
@@ -59,10 +58,6 @@ environLocal = environment.Environment(_MOD)
 # good midi reference:
 # http://www.sonicspot.com/guide/midifiles.html
 # ------------------------------------------------------------------------------
-class EnumerationException(exceptions21.Music21Exception):
-    pass
-
-
 class MidiException(exceptions21.Music21Exception):
     pass
 
diff --git a/music21/midi/translate.py b/music21/midi/translate.py
index 06f3e4174..5e12e2ab8 100644
--- a/music21/midi/translate.py
+++ b/music21/midi/translate.py
@@ -1064,8 +1064,16 @@ def tempoToMidiEvents(tempoIndication, includeDeltaTime=True):
 
     >>> midi.translate.midiEventsToTempo(events)
     <music21.tempo.MetronomeMark maestoso Quarter=90.0>
+
+    `None` is returned if the MetronomeMark lacks a number, which can
+    happen with metric modulation marks.
+
+    >>> midi.translate.tempoToMidiEvents(tempo.MetronomeMark(number=None)) is None
+    True
     '''
     from music21 import midi as midiModule
+    if tempoIndication.number is None:
+        return
     mt = None  # use a midi track set to None
     eventList = []
     if includeDeltaTime:
diff --git a/music21/musicxml/m21ToXml.py b/music21/musicxml/m21ToXml.py
index eb264f75b..599fd6774 100644
--- a/music21/musicxml/m21ToXml.py
+++ b/music21/musicxml/m21ToXml.py
@@ -861,7 +861,7 @@ class XMLExporterBase:
         '''
         Sets mxObject['color'] to a normalized version of m21Object.style.color
         '''
-        self.setStyleAttributes(mxObject, m21Object, 'color')
+        self.setStyleAttributes(mxObject, m21Object, 'color', 'color')
         if 'color' in mxObject.attrib:  # set
             mxObject.attrib['color'] = normalizeColor(mxObject.attrib['color'])
 
@@ -2239,8 +2239,7 @@ class ScoreExporter(XMLExporterBase, PartStaffExporterMixin):
         mxEncodingDate.text = str(datetime.date.today())  # right format...
         # TODO: encoder
 
-        if self.scoreMetadata is not None and hasattr(self.scoreMetadata, 'software'):
-            # TODO: Remove hasattr after all caches are cleared after v5 release.
+        if self.scoreMetadata is not None:
             for software in self.scoreMetadata.software:
                 mxSoftware = SubElement(mxEncoding, 'software')
                 mxSoftware.text = software
@@ -2796,8 +2795,6 @@ class MeasureExporter(XMLExporterBase):
         Note that if the .id is high enough to be an id(x) memory location, then a small
         voice number is used instead.
         '''
-        # TODO: fix mid-measure clef change with voices and part-staff in Schoenberg op. 19 no 2
-        # staff 2, m. 6... -- placed at the beginning of the measure not at the appropriate place.
         root = self.xmlRoot
         divisions = self.currentDivisions
         self.offsetInMeasure = 0.0
@@ -4059,6 +4056,8 @@ class MeasureExporter(XMLExporterBase):
         for artObj in applicableArticulations:
             if 'Pizzicato' in artObj.classes:
                 continue
+            if 'StringIndication' in artObj.classes and artObj.number < 1:
+                continue
             if 'TechnicalIndication' in artObj.classes:
                 if mxTechnicalMark is None:
                     mxTechnicalMark = Element('technical')
@@ -5500,7 +5499,7 @@ class MeasureExporter(XMLExporterBase):
 
         # not to be done: repeater (deprecated)
         self.setColor(mxBeam, beamObject)
-        self.setStyleAttributes(mxBeam, beamObject, 'fan')
+        self.setStyleAttributes(mxBeam, beamObject, 'fan', 'fan')
 
         return mxBeam
 
@@ -6509,6 +6508,20 @@ class Test(unittest.TestCase):
         rest = tree.find('.//rest')
         self.assertEqual(rest.get('measure'), 'yes')
 
+    def testArticulationSpecialCases(self):
+        from music21 import articulations
+
+        n = note.Note()
+        a = articulations.StringIndication()
+        n.articulations.append(a)
+
+        # Legal values for StringIndication begin at 1
+        self.assertEqual(a.number, 0)
+        # Use GEX to go through wellformed object conversion
+        gex = GeneralObjectExporter(n)
+        tree = ET.fromstring(gex.parse().decode('utf-8'))
+        self.assertIsNone(tree.find('.//string'))
+
     def testMeasurePadding(self):
         from music21 import converter
         s = stream.Score([converter.parse('tinyNotation: 4/4 c4')])
diff --git a/music21/musicxml/xmlToM21.py b/music21/musicxml/xmlToM21.py
index 35faa2ea5..0691d6f3d 100644
--- a/music21/musicxml/xmlToM21.py
+++ b/music21/musicxml/xmlToM21.py
@@ -474,7 +474,7 @@ class XMLParserBase:
         '''
         Sets m21Object.style.color to be the same as color...
         '''
-        self.setStyleAttributes(mxObject, m21Object, 'color')
+        self.setStyleAttributes(mxObject, m21Object, 'color', 'color')
 
     def setFont(self, mxObject, m21Object):
         '''
@@ -2804,7 +2804,7 @@ class MeasureParser(XMLParserBase):
         # TODO: get number to preserve
         # not to-do: repeater; is deprecated.
         self.setColor(mxBeam, beamOut)
-        self.setStyleAttributes(mxBeam, beamOut, 'fan')
+        self.setStyleAttributes(mxBeam, beamOut, 'fan', 'fan')
 
         try:
             mxType = mxBeam.text.strip()
diff --git a/music21/stream/base.py b/music21/stream/base.py
index 044d7e6a1..65ac8ff80 100644
--- a/music21/stream/base.py
+++ b/music21/stream/base.py
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 # -----------------------------------------------------------------------------
-# Name:         stream/__init__.py
+# Name:         stream/base.py
 # Purpose:      base classes for dealing with groups of positioned objects
 #
 # Authors:      Michael Scott Cuthbert
@@ -6623,7 +6623,7 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
         notation, including creating voices for overlapped regions, Measures
         if necessary, creating ties, beams, accidentals, and tuplet brackets.
 
-        If `inPlace` is True, this is done in-place;
+        If `inPlace` is True, this is done in-place (changed in v7 -- returns None);
         if `inPlace` is False, this returns a modified deep copy.
 
         makeAccidentalsKeywords can be a dict specifying additional
@@ -6729,7 +6729,8 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
             if m.streamStatus.haveTupletBracketsBeenMade() is False:
                 makeNotation.makeTupletBrackets(m, inPlace=True)
 
-        return returnStream
+        if not inPlace:
+            return returnStream
 
     def extendDuration(self, objName, *, inPlace=False):
         '''
@@ -7719,7 +7720,7 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
         if s1.containerInHierarchy(n) is called, it will return m1,
         the Measure that contains the note.
 
-        Unless `setActiveSite` is False, n's activeStie will be set to m1, and
+        Unless `setActiveSite` is False, n's activeSite will be set to m1, and
         its `.offset` will be the offset in `m1`.
 
         >>> s1 = stream.Score(id='s1')
@@ -9798,7 +9799,6 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
         findConsecutiveNotes.
 
 
-
         >>> s1 = converter.parse("tinynotation: 3/4 c4 d' r b b'", makeNotation=False)
         >>> #_DOCS_SHOW s1.show()
 
@@ -9834,33 +9834,26 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
         for i in range(len(returnList) - 1):
             firstNote = returnList[i]
             secondNote = returnList[i + 1]
-            firstPitch = None
-            secondPitch = None
-            if firstNote is not None and secondNote is not None:
-                startIsChord = False
-                endIsChord = False
-                if hasattr(firstNote, 'pitch') and firstNote.pitch is not None:
-                    firstPitch = firstNote.pitch
-                elif hasattr(firstNote, 'pitches') and firstNote.pitches:
-                    firstPitch = firstNote.pitches[0]
-                    startIsChord = True
-                if hasattr(secondNote, 'pitch') and secondNote.pitch is not None:
-                    secondPitch = secondNote.pitch
-                elif hasattr(secondNote, 'pitches') and secondNote.pitches:
-                    secondPitch = secondNote.pitches[0]
-                    endIsChord = True
-                if firstPitch is not None and secondPitch is not None:
-                    returnInterval = interval.notesToInterval(firstPitch,
-                                                              secondPitch)
-                    if startIsChord is False:
-                        returnInterval.noteStart = firstNote
-                    if endIsChord is False:
-                        returnInterval.noteEnd = secondNote
-                    returnInterval.offset = opFrac(firstNote.offset
-                                                   + firstNote.duration.quarterLength)
-                    returnInterval.duration = duration.Duration(opFrac(
-                        secondNote.offset - returnInterval.offset))
-                    returnStream.insert(returnInterval)
+            # returnList could contain None to represent a rest
+            if firstNote is None or secondNote is None:
+                continue
+            # Protect against empty chords
+            if not (firstNote.pitches and secondNote.pitches):
+                continue
+            if chord.Chord in firstNote.classSet:
+                noteStart = firstNote.notes[0]
+            else:
+                noteStart = firstNote
+            if chord.Chord in secondNote.classSet:
+                noteEnd = secondNote.notes[0]
+            else:
+                noteEnd = secondNote
+            # Prefer Note objects over Pitch objects so that noteStart is set correctly
+            returnInterval = interval.Interval(noteStart, noteEnd)
+            returnInterval.offset = opFrac(firstNote.offset + firstNote.quarterLength)
+            returnInterval.duration = duration.Duration(opFrac(
+                secondNote.offset - returnInterval.offset))
+            returnStream.insert(returnInterval)
 
         return returnStream
 
diff --git a/music21/stream/core.py b/music21/stream/core.py
index 62e974091..f3adddbd4 100644
--- a/music21/stream/core.py
+++ b/music21/stream/core.py
@@ -669,7 +669,7 @@ class StreamCoreMixin:
         that spannerBundle are added.  This can be useful, for instance, in restoring
         spanners from an excerpt that might already have spanners removed.  In
         Jacob Tyler Walls's brilliant phrasing, it prevents regrowing zombie spanners
-        the you thought you had killed.
+        that you thought you had killed.
 
         Here we will constrain only to spanners also present in another Stream:
 
diff --git a/music21/stream/filters.py b/music21/stream/filters.py
index 338bf83b5..1d29feef5 100644
--- a/music21/stream/filters.py
+++ b/music21/stream/filters.py
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 # -----------------------------------------------------------------------------
-# Name:         stream/filter.py
+# Name:         stream/filters.py
 # Purpose:      classes for filtering iterators of  streams...
 #
 # Authors:      Michael Scott Cuthbert
diff --git a/music21/stream/iterator.py b/music21/stream/iterator.py
index f57177b7e..9d9b0507c 100644
--- a/music21/stream/iterator.py
+++ b/music21/stream/iterator.py
@@ -637,7 +637,7 @@ class StreamIterator(prebase.ProtoM21Object):
          <music21.note.Note E>, <music21.note.Note F>, <music21.note.Note G>,
          <music21.note.Note A>]
 
-        If restoreActiveSties is False then the elements will not have
+        If restoreActiveSites is False then the elements will not have
         their activeSites changed (callers should use it when they do not plan to actually
         expose the elements to users, such as in `__len__`).
 
diff --git a/music21/vexflow/toMusic21j.py b/music21/vexflow/toMusic21j.py
index 0b18a29b5..9ea1fe79c 100644
--- a/music21/vexflow/toMusic21j.py
+++ b/music21/vexflow/toMusic21j.py
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 # ------------------------------------------------------------------------------
-# Name:         vexflow/toM21p.py
+# Name:         vexflow/toM21j.py
 # Purpose:      music21 classes for converting music21 objects to music21j
 #
 # Authors:      Michael Scott Cuthbert


Test patch:
diff --git a/documentation/source/developerReference/testing.ipynb b/documentation/source/developerReference/testing.ipynb
index dcaffd2fe..e95d5f713 100644
--- a/documentation/source/developerReference/testing.ipynb
+++ b/documentation/source/developerReference/testing.ipynb
@@ -210,7 +210,7 @@
     "\n",
     "    https://coveralls.io/github/cuthbertLab/music21\n",
     "\n",
-    "While 100% code coverage is impossible, increasing the coverage percentage with each new contribution is essential.  We're at 91% code coverage and get the green badge of pride on our README.md file, and we like that!\n",
+    "While 100% code coverage is impossible, increasing the coverage percentage with each new contribution is essential.  We're at 92% code coverage and get the green badge of pride on our README.md file, and we like that!\n",
     "\n",
     "If something should be impossible to trigger, but the code is being maintained for some reason, you can add \"`# pragma: no cover`\" to the line or to the fork, but this is generally a no-no."
    ]
diff --git a/music21/stream/tests.py b/music21/stream/tests.py
index a2254a571..657ac4eef 100644
--- a/music21/stream/tests.py
+++ b/music21/stream/tests.py
@@ -994,6 +994,18 @@ class Test(unittest.TestCase):
         self.assertEqual(len(intS1), 2)
         M9 = intS1[0]
         self.assertEqual(M9.niceName, 'Major Ninth')
+
+        # Simple chord example
+        ch1 = chord.Chord('C4 E4 G4')
+        ch2 = chord.Chord('D4 F4 A4')
+        s2 = Stream([ch1, ch2])
+        intS2 = s2.melodicIntervals()
+        self.assertEqual(len(intS2), 1)
+        major_second = intS2.first()
+        self.assertEqual(major_second.niceName, 'Major Second')
+        self.assertIs(major_second.noteStart, ch1.notes[0])
+        self.assertIs(major_second.noteEnd, ch2.notes[0])
+
         # TODO: Many more tests
 
     def testMelodicIntervalsB(self):