Repository: cuthbertLab/music21
Pull request #987 (2021-04-27 11:08:48)

Set `Interval.noteStart` when `melodicIntervals()` encounters Chords
I was using `melodicIntervals()` with the intent to find desired intervals and then extract the measure context by accessing .noteStart and then .derivation.origin, but I was encountering broken derivation chains because of the way `.noteStart` is set *after* the interval is created from `Pitch` objects:

```
>>> n = note.Note()
>>> n2 = note.Note()
>>> i = interval.notesToInterval(n.pitch, n2.pitch) # from pitches
>>> i.noteStart = n  # set the note afterward
>>> i.noteEnd = n2
>>> i.noteStart is n
False
>>> i.noteEnd is n2
True
```

The above example only happens in `melodicIntervals()` if chords are involved, so this PR just has melodicIntervals handle chords as it does notes and create intervals from Note objects, not Pitch objects. Took the opportunity to simplify a little bit, also.


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

Coverage increased (+0.006%) to 92.246% when pulling **86457027623ddae32c2014fd0c21c23bf95eb526 on jacobtylerwalls:melodicIntervals-derivation** into **06b2122b81ba86c777dae90fe707a4c0a3c3cba6 on cuthbertLab:master**.


Patch:
diff --git a/music21/_version.py b/music21/_version.py
index 333fa7463..57a202038 100644
--- a/music21/_version.py
+++ b/music21/_version.py
@@ -42,7 +42,7 @@ When changing, update the single test case in base.py.
 Changing this number invalidates old pickles -- do it if the old pickles create a problem.
 '''
 
-__version_info__ = (7, 0, 5)  # can be 4-tuple: (7, 0, 5, 'a2')
+__version_info__ = (7, 0, 6)  # can be 4-tuple: (7, 0, 5, 'a2')
 
 v = '.'.join(str(x) for x in __version_info__[0:3])
 if len(__version_info__) > 3 and __version_info__[3]:
diff --git a/music21/analysis/discrete.py b/music21/analysis/discrete.py
index 630ef6709..ed01a56f8 100644
--- a/music21/analysis/discrete.py
+++ b/music21/analysis/discrete.py
@@ -25,7 +25,7 @@ The :class:`music21.analysis.discrete.KrumhanslSchmuckler`
 #     range and key modules in analysis
 
 import unittest
-from typing import Union, List, Any, Tuple, Iterable
+from typing import Union, List, Any, Tuple, Iterable, Optional
 
 from collections import OrderedDict
 from music21 import exceptions21
@@ -935,7 +935,7 @@ keyWeightKeyAnalysisClasses = [KrumhanslSchmuckler,
 # -----------------------------------------------------------------------------
 class Ambitus(DiscreteAnalysis):
     '''
-    An basic analysis method for measuring register.
+    A basic analysis method for measuring register.
 
     >>> ambitusAnalysis = analysis.discrete.Ambitus()
     >>> ambitusAnalysis.identifiers[0]
@@ -945,10 +945,15 @@ class Ambitus(DiscreteAnalysis):
 
     name = 'Ambitus Analysis'
     # provide possible string matches for this processor
-    identifiers = ['ambitus', 'range', 'span']
+    identifiers = ['ambitus', 'span']
 
     def __init__(self, referenceStream=None):
         super().__init__(referenceStream=referenceStream)
+        # Store the min and max Pitch instances for referenceStream
+        # set by getPitchSpan(), which is called by _generateColors()
+        self.minPitchObj: Optional[pitch.Pitch] = None
+        self.maxPitchObj: Optional[pitch.Pitch] = None
+
         self._pitchSpanColors = OrderedDict()
         self._generateColors()
 
@@ -967,16 +972,18 @@ class Ambitus(DiscreteAnalysis):
         2 #16111d
         3 #16121e
         '''
+        minPitch = 0
         if numColors is None:
             if self._referenceStream is not None:
                 # get total range for entire piece
-                minPitch, maxPitch = self.getPitchRanges(self._referenceStream)
+                self.minPitchObj, self.maxPitchObj = self.getPitchSpan(self._referenceStream)
+                maxPitch = int(self.maxPitchObj.ps - self.minPitchObj.ps)
             else:
-                minPitch, maxPitch = 0, 130  # a large default
+                maxPitch = 130  # a large default
         else:  # create minPitch maxPitch
-            minPitch, maxPitch = 0, numColors
+            maxPitch = numColors
 
-        valueRange = maxPitch - minPitch
+        valueRange = maxPitch
         if valueRange == 0:
             valueRange = 1  # avoid float division by zero
         step = 0
@@ -990,10 +997,10 @@ class Ambitus(DiscreteAnalysis):
 
         # environLocal.printDebug([self._pitchSpanColors])
 
-    def getPitchSpan(self, subStream):
+    def getPitchSpan(self, subStream) -> Optional[Tuple[pitch.Pitch, pitch.Pitch]]:
         '''
-        For a given subStream, return the pitch with the minimum and
-        maximum pitch space value found.
+        For a given subStream, return a tuple consisting of the two pitches
+        with the minimum and maximum pitch space value.
 
         This public method may be used by other classes.
 
@@ -1012,7 +1019,25 @@ class Ambitus(DiscreteAnalysis):
         >>> s.append(c)
         >>> p.getPitchSpan(s)
         (<music21.pitch.Pitch A2>, <music21.pitch.Pitch C8>)
+
+        Returns None if the stream contains no pitches.
+
+        >>> s = stream.Stream(note.Rest())
+        >>> p.getPitchSpan(s) is None
+        True
+
+        OMIT_FROM_DOCS
+
+        And with only ChordSymbols:
+
+        >>> s.insert(4, harmony.ChordSymbol('C6'))
+        >>> p.getPitchSpan(s) is None
+        True
+
         '''
+        if subStream is self._referenceStream and self.minPitchObj and self.maxPitchObj:
+            return self.minPitchObj, self.maxPitchObj
+
         justNotes = subStream.recurse().notes
         if not justNotes:
             # need to handle case of no pitches
@@ -1030,65 +1055,21 @@ class Ambitus(DiscreteAnalysis):
                 pitches = [n.pitch]
             psFound += [p.ps for p in pitches]
             pitchesFound.extend(pitches)
-        # in some cases no pitch space values are found due to all rests
+        # in some cases there is stil nothing -- perhaps only ChordSymbols
         if not psFound:
             return None
         # use built-in functions
         minPitchIndex = psFound.index(min(psFound))
         maxPitchIndex = psFound.index(max(psFound))
 
-        return pitchesFound[minPitchIndex], pitchesFound[maxPitchIndex]
-
-    def getPitchRanges(self, subStream) -> Tuple[int, int]:
-        '''
-        For a given subStream, return the smallest .ps difference
-        between any two pitches and the largest difference
-        between any two pitches. This is used to get the
-        smallest and largest ambitus possible in a given work.
-
-        >>> ambitusAnalyzer = analysis.discrete.Ambitus()
-        >>> s = stream.Stream()
-        >>> c = chord.Chord(['a2', 'b4', 'c8'])
-        >>> s.append(c)
-        >>> [int(thisPitch.ps) for thisPitch in ambitusAnalyzer.getPitchSpan(s)]
-        [45, 108]
-        >>> ambitusAnalyzer.getPitchRanges(s)
-        (26, 63)
-
-        >>> s = corpus.parse('bach/bwv66.6')
-        >>> ambitusAnalyzer.getPitchRanges(s)
-        (0, 34)
+        minPitchObj = pitchesFound[minPitchIndex]
+        maxPitchObj = pitchesFound[maxPitchIndex]
 
-        An empty stream has pitch range (0, 0)
-
-        >>> s = stream.Stream()
-        >>> ambitusAnalyzer.getPitchRanges(s)
-        (0, 0)
-        '''
-        ssfn = subStream.flat.notes
+        if subStream is self._referenceStream:
+            self.minPitchObj = minPitchObj
+            self.maxPitchObj = maxPitchObj
 
-        psFound = []
-        for n in ssfn:
-            pitches = []
-            if 'Chord' in n.classes:
-                pitches = n.pitches
-            elif 'Note' in n.classes:
-                pitches = [n.pitch]
-            for p in pitches:
-                psFound.append(p.ps)
-        psFound.sort()
-        psRange = []
-        for i in range(len(psFound) - 1):
-            p1 = psFound[i]
-            for j in range(i + 1, len(psFound)):
-                p2 = psFound[j]
-                # p2 should always be equal or greater than p1
-                psRange.append(p2 - p1)
-
-        if not psRange:
-            return (0, 0)
-        else:
-            return (int(min(psRange)), int(max(psRange)))
+        return minPitchObj, maxPitchObj
 
     def solutionLegend(self, compress=False):
         '''
@@ -1330,14 +1311,14 @@ def analyzeStream(streamObj, *args, **keywords):
 
     >>> analysis.discrete.analyzeStream(s, 'key')
     <music21.key.Key of f# minor>
-    >>> analysis.discrete.analyzeStream(s, 'range')
+    >>> analysis.discrete.analyzeStream(s, 'span')
     <music21.interval.Interval m21>
 
 
     Note that the same results can be obtained by calling "analyze" directly on the stream object:
     >>> s.analyze('key')
     <music21.key.Key of f# minor>
-    >>> s.analyze('range')
+    >>> s.analyze('span')
     <music21.interval.Interval m21>
 
     '''
@@ -1348,6 +1329,11 @@ def analyzeStream(streamObj, *args, **keywords):
     if args:
         method = args[0]
 
+    if method == 'range':
+        # getPitchRanges() was removed in v7
+        # this synonym is being added for compatibility
+        method = 'span'
+
     match = analysisClassFromMethodName(method)
 
     if match is not None:
@@ -1370,7 +1356,7 @@ def analysisClassFromMethodName(method):
     >>> acfmn = analysis.discrete.analysisClassFromMethodName
     >>> acfmn('aarden')
     <class 'music21.analysis.discrete.AardenEssen'>
-    >>> acfmn('range')
+    >>> acfmn('span')
     <class 'music21.analysis.discrete.Ambitus'>
 
     This one is fundamentally important...
diff --git a/music21/base.py b/music21/base.py
index 262e04b42..3a5f13351 100644
--- a/music21/base.py
+++ b/music21/base.py
@@ -28,7 +28,7 @@ available after importing `music21`.
 <class 'music21.base.Music21Object'>
 
 >>> music21.VERSION_STR
-'7.0.5'
+'7.0.6'
 
 Alternatively, after doing a complete import, these classes are available
 under the module "base":
diff --git a/music21/metadata/__init__.py b/music21/metadata/__init__.py
index dddd4d146..38876453b 100644
--- a/music21/metadata/__init__.py
+++ b/music21/metadata/__init__.py
@@ -1266,12 +1266,11 @@ class RichMetadata(Metadata):
         self.pitchHighest = None
         self.pitchLowest = None
         analysisObject = discrete.Ambitus(streamObj)
-        psRange = analysisObject.getPitchSpan(streamObj)
-        if psRange is not None:
+        if analysisObject.minPitchObj is not None and analysisObject.maxPitchObj is not None:
             # may be none if no pitches are stored
             # presently, these are numbers; convert to pitches later
-            self.pitchLowest = psRange[0].nameWithOctave
-            self.pitchHighest = psRange[1].nameWithOctave
+            self.pitchLowest = analysisObject.minPitchObj.nameWithOctave
+            self.pitchHighest = analysisObject.maxPitchObj.nameWithOctave
         ambitusInterval = analysisObject.getSolution(streamObj)
         self.ambitus = AmbitusShort(semitones=ambitusInterval.semitones,
                                     diatonic=ambitusInterval.diatonic.simpleName,
diff --git a/music21/midi/translate.py b/music21/midi/translate.py
index 8d65a27c2..06f3e4174 100644
--- a/music21/midi/translate.py
+++ b/music21/midi/translate.py
@@ -768,7 +768,13 @@ def midiEventsToInstrument(eventList):
                 i = instrument.instrumentFromMidiProgram(event.data)
             # Instrument.midiProgram and event.data are both 0-indexed
             i.midiProgram = event.data
-    except (instrument.InstrumentException, UnicodeDecodeError):  # pragma: no cover
+    except (percussion.MIDIPercussionException,
+            UnicodeDecodeError):
+        warnings.warn(f'Unable to determine instrument from {event}', TranslateWarning)
+        i = instrument.Instrument()
+    except instrument.InstrumentException:
+        # Currently, we risk having an overwhelming number of warnings
+        # but consider consolidating to just one except one day
         i = instrument.Instrument()
 
     # Set MIDI channel
@@ -3784,7 +3790,7 @@ class Test(unittest.TestCase):
         i = midiEventsToInstrument(event)
         self.assertIsInstance(i, instrument.Flute)
 
-    def testLousyInstrumentName(self):
+    def testLousyInstrumentData(self):
         from music21 import midi as midiModule
 
         lousyNames = ('    ', 'Instrument 20', 'Instrument', 'Inst 2', 'instrument')
@@ -3796,6 +3802,19 @@ class Test(unittest.TestCase):
                 i = midiEventsToInstrument(event)
                 self.assertIsNone(i.instrumentName)
 
+        # lousy program change
+        # https://github.com/cuthbertLab/music21/issues/988
+        event = midiModule.MidiEvent()
+        event.data = 0
+        event.channel = 10
+        event.type = midiModule.ChannelVoiceMessages.PROGRAM_CHANGE
+
+        msg = 'Unable to determine instrument from '
+        msg += '<music21.midi.MidiEvent PROGRAM_CHANGE, track=0, channel=10, data=0>'
+        with self.assertWarns(TranslateWarning, msg=msg):
+            i = midiEventsToInstrument(event)
+            self.assertIsNone(i.instrumentName)
+
     def testConductorStream(self):
         s = stream.Stream()
         p = stream.Stream()
diff --git a/music21/musicxml/m21ToXml.py b/music21/musicxml/m21ToXml.py
index 0301cb43f..eb264f75b 100644
--- a/music21/musicxml/m21ToXml.py
+++ b/music21/musicxml/m21ToXml.py
@@ -1557,18 +1557,34 @@ class ScoreExporter(XMLExporterBase, PartStaffExporterMixin):
         called by .parse() if the score has individual parts.
 
         Calls makeRests() for the part, then creates a PartExporter for each part,
-        and runs .parse() on that part.  appends the PartExporter to self.partExporterList
+        and runs .parse() on that part.  appends the PartExporter to self.
+
+        Hide rests created at this late stage.
+
+        >>> v = stream.Voice(note.Note())
+        >>> m = stream.Measure([meter.TimeSignature(), v])
+        >>> GEX = musicxml.m21ToXml.GeneralObjectExporter(m)
+        >>> out = GEX.parse()  # out is bytes
+        >>> outStr = out.decode('utf-8')  # now is string
+        >>> '<note print-object="no" print-spacing="yes">' in outStr
+        True
         '''
-        # would like to do something like this but cannot
-        # replace object inside of the stream
-        sp = list(self.parts)
-        for innerStream in sp:
-            innerStream.makeRests(self.refStreamOrTimeRange, inPlace=True)
+        # self.parts is a stream of parts
+        # hide any rests created at this late stage, because we are
+        # merely trying to fill up MusicXML display, not impose things on users
+        self.parts.makeRests(refStreamOrTimeRange=self.refStreamOrTimeRange,
+                             inPlace=True,
+                             hideRests=True,
+                             timeRangeFromBarDuration=True,
+                             )
 
         count = 0
+        sp = list(self.parts)
         for innerStream in sp:
             count += 1
-            if count > len(sp):
+            # This guards against making an error in a future refactor
+            # Raises if editing while iterating instead of casting to list above
+            if count > len(sp):  # pragma: no cover
                 raise MusicXMLExportException('infinite stream encountered')
 
             pp = PartExporter(innerStream, parent=self)
@@ -6493,6 +6509,17 @@ class Test(unittest.TestCase):
         rest = tree.find('.//rest')
         self.assertEqual(rest.get('measure'), 'yes')
 
+    def testMeasurePadding(self):
+        from music21 import converter
+        s = stream.Score([converter.parse('tinyNotation: 4/4 c4')])
+        s[stream.Measure].first().paddingLeft = 2.0
+        s[stream.Measure].first().paddingRight = 1.0
+        tree = self.getET(s)
+        self.assertEqual(len(tree.findall('.//rest')), 0)
+        s[stream.Measure].first().paddingLeft = 1.0
+        tree = self.getET(s)
+        self.assertEqual(len(tree.findall('.//rest')), 1)
+
 
 class TestExternal(unittest.TestCase):  # pragma: no cover
 
diff --git a/music21/stream/base.py b/music21/stream/base.py
index c121c48cb..5bdba3260 100644
--- a/music21/stream/base.py
+++ b/music21/stream/base.py
@@ -9798,7 +9798,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 +9833,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
 
@@ -10526,6 +10518,9 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
         [<music21.pitch.Pitch C4>]
         >>> [str(n.pitch) for n in s.voices[1].notes]
         ['B-4', 'B-4', 'B-4', 'B-4', 'B-4', 'B-4', 'B-4', 'B-4']
+
+        Changed in v7 -- if `fillGaps=True` and called on an incomplete measure,
+        makes trailing rests in voices. This scenario occurs when parsing MIDI.
         '''
         # this method may not always
         # produce the optimal voice assignment based on context (register
@@ -10575,9 +10570,12 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
         # remove any unused voices (possible if overlap group has sus)
         for v in voices:
             if v:  # skip empty voices
-                if fillGaps:
-                    returnObj.makeRests(fillGaps=True, inPlace=True)
                 returnObj.insert(0, v)
+        if fillGaps:
+            returnObj.makeRests(fillGaps=True,
+                                inPlace=True,
+                                timeRangeFromBarDuration=True,
+                                )
         # remove rests in returnObj
         returnObj.removeByClass('Rest')
         # elements changed will already have been called
diff --git a/music21/stream/makeNotation.py b/music21/stream/makeNotation.py
index e8b268fe0..bded35368 100644
--- a/music21/stream/makeNotation.py
+++ b/music21/stream/makeNotation.py
@@ -90,6 +90,22 @@ def makeBeams(
     4 <music21.beam.Beams <music21.beam.Beam 1/continue>/<music21.beam.Beam 2/continue>>
     5 <music21.beam.Beams <music21.beam.Beam 1/stop>/<music21.beam.Beam 2/stop>>
 
+    Grace notes no longer interfere with beaming:
+
+    >>> m = stream.Measure()
+    >>> m.timeSignature = meter.TimeSignature('3/4')
+    >>> m.repeatAppend(note.Note(quarterLength=0.25), 4)
+    >>> m.repeatAppend(note.Rest(), 2)
+    >>> gn = note.Note(duration=duration.GraceDuration())
+    >>> m.insert(0.25, gn)
+    >>> m.makeBeams(inPlace=True)
+    >>> [n.beams for n in m.notes]
+    [<music21.beam.Beams <music21.beam.Beam 1/start>/<music21.beam.Beam 2/start>>,
+    <music21.beam.Beams>,
+    <music21.beam.Beams <music21.beam.Beam 1/continue>/<music21.beam.Beam 2/stop>>,
+    <music21.beam.Beams <music21.beam.Beam 1/continue>/<music21.beam.Beam 2/start>>,
+    <music21.beam.Beams <music21.beam.Beam 1/stop>/<music21.beam.Beam 2/stop>>]
+
     OMIT_FROM_DOCS
     TODO: inPlace=False does not work in many cases  ?? still an issue? 2017
     '''
@@ -139,6 +155,9 @@ def makeBeams(
                 continue  # nothing to beam
             durList = []
             for n in noteStream:
+                if n.duration.isGrace:
+                    noteStream.remove(n)
+                    continue
                 durList.append(n.duration)
             # environLocal.printDebug([
             #    'beaming with ts', lastTimeSignature, 'measure', m, durList,
@@ -692,7 +711,9 @@ def makeRests(
     fill with one Rest preceding this offset.
     This can be called on any Stream,
     a Measure alone, or a Measure that contains
-    Voices.
+    Voices. This method recurses into Parts, Measures, and Voices,
+    since users are unlikely to want "loose" rests outside
+    of sub-containers.
 
     If `refStreamOrTimeRange` is provided as a Stream, this
     Stream is used to get min and max offsets. If a list is provided,
@@ -703,8 +724,15 @@ def makeRests(
     time regions that have no active elements.
 
     If `timeRangeFromBarDuration` is True, and the calling Stream
-    is a Measure with a TimeSignature, the time range will be determined
-    based on the .barDuration property.
+    is a Measure with a TimeSignature (or a Part containing them),
+    the time range will be determined
+    by taking the :meth:`~music21.stream.Measure.barDuration` and subtracting
+    :attr:`~music21.stream.Measure.paddingLeft` and
+    :attr:`~music21.stream.Measure.paddingRight`.
+    This keyword takes priority over `refStreamOrTimeRange`.
+    If both are provided, `timeRangeFromBarDuration`
+    prevails, unless no TimeSignature can be found, in which case, the function
+    falls back to `refStreamOrTimeRange`.
 
     If `inPlace` is True, this is done in-place; if `inPlace` is False,
     this returns a modified deepcopy.
@@ -792,7 +820,11 @@ def makeRests(
 
     Changed in v6 -- all but first attribute are keyword only
 
-    Changed in v7 -- `inPlace` defaults False.
+    Changed in v7:
+
+      - `inPlace` defaults False
+      - Recurses into parts, measures, voices
+      - Gave priority to `timeRangeFromBarDuration` over `refStreamOrTimeRange`
     '''
     from music21 import stream
 
@@ -802,35 +834,57 @@ def makeRests(
     else:
         returnObj = s
 
-    oLowTarget = 0
-    oHighTarget = 0
+    if returnObj.iter.parts:
+        for inner_part in returnObj.iter.parts:
+            inner_part.makeRests(
+                inPlace=True,
+                fillGaps=fillGaps,
+                hideRests=hideRests,
+                refStreamOrTimeRange=refStreamOrTimeRange,
+                timeRangeFromBarDuration=timeRangeFromBarDuration,
+            )
+        return returnObj
 
-    # environLocal.printDebug([
-    #    'makeRests(): object lowestOffset, highestTime', oLow, oHigh])
-    if refStreamOrTimeRange is None:  # use local
-        oLowTarget = 0
-        if timeRangeFromBarDuration and returnObj.isMeasure:
-            # NOTE: this will raise an exception if no meter can be found
-            oHighTarget = returnObj.barDuration.quarterLength
-        elif timeRangeFromBarDuration and returnObj.hasMeasures():
+    def oHighTargetForMeasure(m: stream.Measure) -> float:
+        '''Needed for timeRangeFromBarDuration'''
+        # NOTE: this returns 0.0 if no meter can be found
+        post = m.barDuration.quarterLength
+        post -= m.paddingLeft
+        post -= m.paddingRight
+        return max(post, 0.0)
+
+    oLowTarget = 0.0
+    oHighTarget = 0.0
+    if timeRangeFromBarDuration:
+        if returnObj.isMeasure:
+            oHighTarget = oHighTargetForMeasure(returnObj)
+        elif (
+            stream.Voice in returnObj.classSet
+            and hasattr(refStreamOrTimeRange, 'isMeasure')
+            and refStreamOrTimeRange.isMeasure
+        ):
+            # Alternative to getting measure context would be to access .activeSite
+            # but for now, depend on getting measure context from refStreamOrTimeRange
+            # since we have not documented calling makeRests directly on a Voice and
+            # expecting to infer barDuration from the measure context.
+            # merely trying to support the recursive call for contained voices, below
+            oHighTarget = oHighTargetForMeasure(refStreamOrTimeRange)
+        elif returnObj.hasMeasures():
             oHighTarget = sum(
                 m.barDuration.quarterLength for m in returnObj.getElementsByClass(stream.Measure)
             )
-        else:
+    # If the above search didn't run or still yielded 0.0, use refStreamOrTimeRange
+    if oHighTarget == 0.0:
+        if refStreamOrTimeRange is None:  # use local
             oHighTarget = returnObj.highestTime
-    elif isinstance(refStreamOrTimeRange, stream.Stream):
-        oLowTarget = refStreamOrTimeRange.lowestOffset
-        oHighTarget = refStreamOrTimeRange.highestTime
-        # environLocal.printDebug([
-        #    'refStream used in makeRests', oLowTarget, oHighTarget,
-        #    len(refStreamOrTimeRange)])
-    # treat as a list
-    elif common.isIterable(refStreamOrTimeRange):
-        oLowTarget = min(refStreamOrTimeRange)
-        oHighTarget = max(refStreamOrTimeRange)
-        # environLocal.printDebug([
-        #    'offsets used in makeRests', oLowTarget, oHighTarget,
-        #    len(refStreamOrTimeRange)])
+        elif isinstance(refStreamOrTimeRange, stream.Stream):
+            oLowTarget = refStreamOrTimeRange.lowestOffset
+            oHighTarget = refStreamOrTimeRange.highestTime
+        # treat as a list
+        elif common.isIterable(refStreamOrTimeRange):
+            oLowTarget = min(refStreamOrTimeRange)
+            oHighTarget = max(refStreamOrTimeRange)
+
     if returnObj.hasVoices():
         bundle = list(returnObj.voices)
     elif returnObj.hasMeasures():
@@ -839,12 +893,25 @@ def makeRests(
         bundle = [returnObj]
 
     # bundle components may be voices, measures, or a flat Stream
-    for v in bundle:
-        oLow = v.lowestOffset
-        oHigh = v.highestTime
-        if returnObj.hasMeasures():
+    for component in bundle:
+        oLow = component.lowestOffset
+        oHigh = component.highestTime
+        if component.isMeasure:
+            if timeRangeFromBarDuration:
+                oHighTarget = oHighTargetForMeasure(component)
+            # process voices
+            for inner_voice in component.voices:
+                inner_voice.makeRests(inPlace=True,
+                                      fillGaps=fillGaps,
+                                      hideRests=hideRests,
+                                      refStreamOrTimeRange=component,
+                                      timeRangeFromBarDuration=timeRangeFromBarDuration,
+                                      )
+            # Refresh these variables given that inner voices were altered
+            oLow = component.lowestOffset
+            oHigh = component.highestTime
             # adjust oHigh to not exceed measure
-            oHighTarget = min(v.barDuration.quarterLength, oHighTarget)
+            oHighTarget = min(component.barDuration.quarterLength, oHighTarget)
 
         # create rest from start to end
         qLen = oLow - oLowTarget
@@ -854,27 +921,25 @@ def makeRests(
             r.style.hideObjectOnPrint = hideRests
             # environLocal.printDebug(['makeRests(): add rests', r, r.duration])
             # place at oLowTarget to reach to oLow
-            v.insert(oLowTarget, r)
+            component.insert(oLowTarget, r)
 
         # create rest from end to highest
         qLen = oHighTarget - oHigh
-        # environLocal.printDebug(['v', v, oHigh, oHighTarget, 'qLen', qLen])
         if qLen > 0:
             r = note.Rest()
             r.duration.quarterLength = qLen
             r.style.hideObjectOnPrint = hideRests
             # place at oHigh to reach to oHighTarget
-            v.insert(oHigh, r)
+            component.insert(oHigh, r)
 
         if fillGaps:
-            gapStream = v.findGaps()
+            gapStream = component.findGaps()
             if gapStream is not None:
                 for e in gapStream:
                     r = note.Rest()
                     r.duration.quarterLength = e.duration.quarterLength
                     r.style.hideObjectOnPrint = hideRests
-                    v.insert(e.offset, r)
-        # environLocal.printDebug(['post makeRests show()', v])
+                    component.insert(e.offset, r)
 
     if returnObj.hasMeasures():
         # split rests at measure boundaries
@@ -886,14 +951,6 @@ def makeRests(
             returnObj.setElementOffset(m, accumulatedTime)
             accumulatedTime += m.highestTime
 
-            # process voices
-            for v in m.voices:
-                v.makeRests(inPlace=True,
-                            fillGaps=fillGaps,
-                            hideRests=hideRests,
-                            refStreamOrTimeRange=m,
-                            )
-
     if inPlace is not True:
         return returnObj
 


Test patch:
diff --git a/music21/stream/tests.py b/music21/stream/tests.py
index d6cf35b51..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):
@@ -1989,6 +2001,44 @@ class Test(unittest.TestCase):
         p.makeRests(inPlace=True)
         self.assertEqual(p.duration.quarterLength, 8.0)
 
+    def testMakeRestsInMeasuresWithVoices(self):
+        p = Part()
+        m = Measure(meter.TimeSignature('4/4'), number=1)
+        v1 = Voice(note.Note(quarterLength=3.5))
+        v2 = Voice(note.Note(quarterLength=3.75))
+        m.insert(0, v1)
+        m.insert(0, v2)
+        p.insert(0, m)
+
+        post = p.makeRests(inPlace=False, timeRangeFromBarDuration=True)
+
+        # No loose rests outside voices
+        self.assertEqual(len(post.first().getElementsByClass(note.Rest)), 0)
+        # Total of two rests, one in each voice
+        self.assertEqual(len(post.recurse().getElementsByClass(note.Rest)), 2)
+
+        # Wrap into Score
+        sc = Score([p])
+        post = sc.makeRests(inPlace=False, timeRangeFromBarDuration=True)
+        # No loose rests outside parts
+        self.assertEqual(len(post.first().getElementsByClass(note.Rest)), 0)
+        # ... or outside measures
+        self.assertEqual(len(post.first().measure(1).getElementsByClass(note.Rest)), 0)
+        # Total of two rests, one in each voice
+        self.assertEqual(len(post.recurse().getElementsByClass(note.Rest)), 2)
+
+    def testMakeRestsByMakingVoices(self):
+        # Create incomplete measure with overlaps, like a MIDI file
+        m = Measure(meter.TimeSignature('4/4'), number=1)
+        m.insert(0, note.Note(quarterLength=3.5))
+        m.insert(0, note.Note(quarterLength=3.75))
+        m.makeVoices(inPlace=True)
+
+        # No loose rests outside voices
+        self.assertEqual(len(m.getElementsByClass(note.Rest)), 0)
+        # Total of two rests, one in each voice. Recursive search.
+        self.assertEqual(len(m[note.Rest]), 2)
+
     def testMakeMeasuresInPlace(self):
         sScr = Stream()
         sScr.insert(0, clef.TrebleClef())