Repository: cuthbertLab/music21
Pull request #688 (2020-12-02 00:23:52)

Retain instruments outside of voices when making measures
Refs #684  (Turned out to be easier than I thought!)

**Before:** elements outside of voices, such as Instruments, would vanish when running `.makeMeasures`.

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

Coverage increased (+0.0004%) to 90.97% when pulling **1651268945a25a77b81e7e4a976580fb65be4038 on jacobtylerwalls:vanishing-instrument** into **9a85ee5abc1161acb9de9179f3dea43a26dc5024 on cuthbertLab:master**.

mscuthbert: LGTM pending final check (Travis is SLOW these days) -- OffsetMap is a concept that should be changed in v.7 -- no reason to use srcObj, and probably just using the tree-object and getting the voice from .sites would be simpler.   

Patch:
diff --git a/.github/scripts/install_ubuntu_deps.sh b/.github/scripts/install_ubuntu_deps.sh
new file mode 100755
index 000000000..a62ac93a9
--- /dev/null
+++ b/.github/scripts/install_ubuntu_deps.sh
@@ -0,0 +1,19 @@
+#!/bin/bash
+set -e  # error if anything returns non-zero exit code
+
+# needed for some tests that add Desktop
+mkdir ~/Desktop
+
+sudo apt-get install -y libpng-dev
+sudo apt-get install -y python-qt4
+wget -q http://lilypond.org/downloads/binaries/linux-64/lilypond-2.19.83-1.linux-64.sh
+sh lilypond-2.19.83-1.linux-64.sh --batch
+export PATH=/home/runner/bin:$PATH
+pip3 install -r requirements.txt
+pip3 install coveralls
+pip3 install scipy
+pip3 install python-Levenshtein
+pip3 install setuptools
+pip3 install coverage
+cd music21
+python -m compileall music21
diff --git a/.github/workflows/maincheck.yml b/.github/workflows/maincheck.yml
new file mode 100644
index 000000000..af47ed88a
--- /dev/null
+++ b/.github/workflows/maincheck.yml
@@ -0,0 +1,27 @@
+name: maincheck
+
+on:
+    push:
+        branches:
+        - master
+        - '*CI*'
+    pull_request:
+        branches:
+        - '*'
+
+jobs:
+  check38:
+    runs-on: ubuntu-18.04
+    steps:
+    - uses: actions/setup-python@v1
+      with:
+        python-version: '3.8'
+        architecture: 'x64'
+    - uses: actions/checkout@v2
+    - name: Install Ubuntu dependencies
+      run: ./.github/scripts/install_ubuntu_deps.sh
+    - name: Setup Lilypond
+      run: python -c 'from music21 import environment; environment.UserSettings()["lilypondPath"] = "/home/runner/bin/lilypond"'
+    - name: Run Main script
+      run: python -c 'from music21.test.testSingleCoreAll import ciMain as ci; ci()'
+
diff --git a/.github/workflows/pythonpylint.yml b/.github/workflows/pythonpylint.yml
index c3a0bf25a..d9a958d84 100644
--- a/.github/workflows/pythonpylint.yml
+++ b/.github/workflows/pythonpylint.yml
@@ -8,14 +8,14 @@ on:
   pull_request:
     branches:
       - '*'
-        
+
 jobs:
   lint:
     runs-on: ubuntu-latest
     strategy:
       max-parallel: 4
       matrix:
-        python-version: [3.7]
+        python-version: [3.8]
 
     steps:
     - uses: actions/checkout@v1
@@ -38,7 +38,7 @@ jobs:
     strategy:
       max-parallel: 4
       matrix:
-        python-version: [3.7]
+        python-version: [3.8]
 
     steps:
     - uses: actions/checkout@v1
diff --git a/.travis.yml b/.travis.yml
index 37878ed73..e1d44cb22 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -38,7 +38,7 @@ install:
     - python -m compileall music21
 
 script:
-    - python -c 'from music21.test.testSingleCoreAll import travisMain as tm; tm()'
+    - python -c 'from music21.test.testSingleCoreAll import ciMain as ci; ci()'
 
 after_success:
     - if [ "$TRAVIS_PYTHON_VERSION" == "3.6" ]; then coveralls; fi
diff --git a/music21/key.py b/music21/key.py
index 30ece2582..2081cf155 100644
--- a/music21/key.py
+++ b/music21/key.py
@@ -1072,9 +1072,12 @@ class Key(KeySignature, scale.DiatonicScale):
                        method='correlationCoefficient',
                        *args,
                        **keywords):
-        '''Provide a measure of tonal ambiguity for Key determined with one of many methods.
+        '''
+        Provide a measure of tonal ambiguity for Key
+        determined with one of many methods.
 
-        The `correlationCoefficient` assumes that the alternateInterpretations list has
+        The `correlationCoefficient` assumes that the
+        alternateInterpretations list has
         been filled from the use of a KeyWeightKeyAnalysis subclass.
 
         >>> littlePiece = converter.parse('tinyNotation: 4/4 c4 d e f g a b cc ee gg ee cc')
diff --git a/music21/midi/translate.py b/music21/midi/translate.py
index fb7a4265c..ceb8206b6 100644
--- a/music21/midi/translate.py
+++ b/music21/midi/translate.py
@@ -689,22 +689,39 @@ def midiEventsToInstrument(eventList):
     '''
     Convert a single MIDI event into a music21 Instrument object.
     '''
+    from music21 import midi as midiModule
+
     if not common.isListLike(eventList):
         event = eventList
     else:  # get the second event; first is delta time
         event = eventList[1]
 
     from music21 import instrument
+    decoded: str = ''
     try:
         if isinstance(event.data, bytes):
             # MuseScore writes MIDI files with null-terminated
             # instrument names.  Thus stop before the byte-0x0
             decoded = event.data.decode('utf-8').split('\x00')[0]
+            decoded = decoded.strip()
             i = instrument.fromString(decoded)
         else:
             i = instrument.instrumentFromMidiProgram(event.data)
     except (instrument.InstrumentException, UnicodeDecodeError):  # pragma: no cover
         i = instrument.Instrument()
+    # Set partName or instrumentName with literal value from parsing
+    if decoded:
+        # Except for lousy instrument names
+        if (
+            decoded.lower() in ('instrument', 'inst')
+            or decoded.lower().replace('instrument ', '').isdigit()
+            or decoded.lower().replace('inst ', '').isdigit()
+        ):
+            return i
+        elif event.type == midiModule.MetaEvents.SEQUENCE_TRACK_NAME:
+            i.partName = decoded
+        elif event.type == midiModule.MetaEvents.INSTRUMENT_NAME:
+            i.instrumentName = decoded
     return i
 
 
@@ -3218,6 +3235,12 @@ class Test(unittest.TestCase):
         self.assertIsInstance(instruments[0], instrument.Oboe)
         self.assertEqual(instruments[0].quarterLength, 0)
 
+        # Unrecognized instrument 'a'
+        dirLib = common.getSourceFilePath() / 'midi' / 'testPrimitive'
+        fp = dirLib / 'test15.mid'
+        s2 = converter.parse(fp)
+        self.assertEqual(s2.parts[0].partName, 'a')
+
     def testImportZeroDurationNote(self):
         '''
         Musescore places zero duration notes in multiple voice scenarios
@@ -3261,6 +3284,18 @@ class Test(unittest.TestCase):
         i = midiEventsToInstrument(event)
         self.assertIsInstance(i, instrument.Flute)
 
+    def testLousyInstrumentName(self):
+        from music21 import midi as midiModule
+
+        lousyNames = ('    ', 'Instrument 20', 'Instrument', 'Inst 2', 'instrument')
+        for name in lousyNames:
+            with self.subTest(name=name):
+                event = midiModule.MidiEvent()
+                event.data = bytes(name, 'utf-8')
+                event.type = midiModule.MetaEvents.INSTRUMENT_NAME
+                i = midiEventsToInstrument(event)
+                self.assertIsNone(i.instrumentName)
+
 
 # ------------------------------------------------------------------------------
 _DOC_ORDER = [streamToMidiFile, midiFileToStream]
diff --git a/music21/musicxml/xmlToM21.py b/music21/musicxml/xmlToM21.py
index 467b8f70c..7e98cb4db 100644
--- a/music21/musicxml/xmlToM21.py
+++ b/music21/musicxml/xmlToM21.py
@@ -3950,6 +3950,8 @@ class MeasureParser(XMLParserBase):
                 showNumber = mxTuplet.get('show-number')
                 if showNumber is not None and showNumber == 'none':
                     t.tupletActualShow = None
+                    if bracketMaybe is None:
+                        t.bracket = False
                 elif showNumber is not None and showNumber == 'both':
                     t.tupletNormalShow = 'number'
 
@@ -6374,6 +6376,16 @@ class Test(unittest.TestCase):
         self.assertEqual(repr(nList[12].duration.tuplets),
                          '(<music21.duration.Tuplet 3/2/eighth>,)')
 
+    def testImpliedTuplet(self):
+        from music21 import converter
+        from music21.musicxml import testPrimitive
+
+        s = converter.parse(testPrimitive.tupletsImplied)
+        # First tuplet group of 3 is silent on bracket and show-number: draw bracket
+        # Second tuplet group of 3 is silent on bracket but show-number="none": don't draw bracket
+        tuplets = [n.duration.tuplets[0] for n in s.flat.notes]
+        self.assertEqual([tup.bracket for tup in tuplets], [True, True, True, False, False, False])
+
     def test34MeasureRestWithoutTag(self):
         from xml.etree.ElementTree import fromstring as EL
 
diff --git a/music21/stream/__init__.py b/music21/stream/__init__.py
index 39a0d2346..b6fd35309 100644
--- a/music21/stream/__init__.py
+++ b/music21/stream/__init__.py
@@ -5756,6 +5756,9 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
             groups = []
             for i, v in enumerate(srcObj.voices):
                 groups.append((v.flat, i))
+            elsNotOfVoice = srcObj.getElementsNotOfClass('Voice')
+            if len(elsNotOfVoice) > 0:
+                groups.insert(0, (elsNotOfVoice, None))
         else:  # create a single collection
             groups = [(srcObj, None)]
         # environLocal.printDebug(['offsetMap', groups])


Test patch:
diff --git a/music21/musicxml/testPrimitive.py b/music21/musicxml/testPrimitive.py
index 66ec96f6c..a1fa4dc7d 100644
--- a/music21/musicxml/testPrimitive.py
+++ b/music21/musicxml/testPrimitive.py
@@ -17932,6 +17932,146 @@ multiDigitEnding = '''<?xml version="1.0" encoding="UTF-8" standalone="no"?>
 </score-partwise>
 '''
 
+
+tupletsImplied = '''<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE score-partwise PUBLIC "-//Recordare//DTD MusicXML 3.1 Partwise//EN" "http://www.musicxml.org/dtds/partwise.dtd">
+<score-partwise version="3.1">
+  <part-list>
+    <score-part id="P1">
+      <part-name print-object="no">MusicXML Part</part-name>
+      <score-instrument id="P1-I1">
+        <instrument-name>None</instrument-name>
+      </score-instrument>
+    </score-part>
+  </part-list>
+  <part id="P1">
+    <measure number="1" width="983">
+      <attributes>
+        <divisions>6</divisions>
+        <key>
+          <fifths>0</fifths>
+          <mode>major</mode>
+        </key>
+        <time>
+          <beats>2</beats>
+          <beat-type>4</beat-type>
+        </time>
+        <clef>
+          <sign>G</sign>
+          <line>2</line>
+        </clef>
+      </attributes>
+      <note default-x="83">
+        <pitch>
+          <step>C</step>
+          <octave>5</octave>
+        </pitch>
+        <duration>2</duration>
+        <voice>1</voice>
+        <type>eighth</type>
+        <time-modification>
+          <actual-notes>3</actual-notes>
+          <normal-notes>2</normal-notes>
+        </time-modification>
+        <stem default-y="-50">down</stem>
+        <beam number="1">begin</beam>
+        <notations>
+          <tuplet number="1" placement="below" type="start"/>
+        </notations>
+      </note>
+      <note default-x="232">
+        <pitch>
+          <step>C</step>
+          <octave>5</octave>
+        </pitch>
+        <duration>2</duration>
+        <voice>1</voice>
+        <type>eighth</type>
+        <time-modification>
+          <actual-notes>3</actual-notes>
+          <normal-notes>2</normal-notes>
+        </time-modification>
+        <stem default-y="-50">down</stem>
+        <beam number="1">continue</beam>
+      </note>
+      <note default-x="380">
+        <pitch>
+          <step>C</step>
+          <octave>5</octave>
+        </pitch>
+        <duration>2</duration>
+        <voice>1</voice>
+        <type>eighth</type>
+        <time-modification>
+          <actual-notes>3</actual-notes>
+          <normal-notes>2</normal-notes>
+        </time-modification>
+        <stem default-y="-50">down</stem>
+        <beam number="1">end</beam>
+        <notations>
+          <tuplet number="1" type="stop"/>
+        </notations>
+      </note>
+      <note default-x="528">
+        <pitch>
+          <step>C</step>
+          <octave>5</octave>
+        </pitch>
+        <duration>2</duration>
+        <voice>1</voice>
+        <type>eighth</type>
+        <time-modification>
+          <actual-notes>3</actual-notes>
+          <normal-notes>2</normal-notes>
+        </time-modification>
+        <stem default-y="-50">down</stem>
+        <beam number="1">begin</beam>
+        <notations>
+          <tuplet number="1" placement="below" show-number="none" type="start"/>
+        </notations>
+      </note>
+      <note default-x="677">
+        <pitch>
+          <step>C</step>
+          <octave>5</octave>
+        </pitch>
+        <duration>2</duration>
+        <voice>1</voice>
+        <type>eighth</type>
+        <time-modification>
+          <actual-notes>3</actual-notes>
+          <normal-notes>2</normal-notes>
+        </time-modification>
+        <stem default-y="-50">down</stem>
+        <beam number="1">continue</beam>
+      </note>
+      <note default-x="825">
+        <pitch>
+          <step>C</step>
+          <octave>5</octave>
+        </pitch>
+        <duration>2</duration>
+        <voice>1</voice>
+        <type>eighth</type>
+        <time-modification>
+          <actual-notes>3</actual-notes>
+          <normal-notes>2</normal-notes>
+        </time-modification>
+        <stem default-y="-50">down</stem>
+        <beam number="1">end</beam>
+        <notations>
+          <tuplet number="1" type="stop"/>
+        </notations>
+      </note>
+      <barline location="right">
+        <bar-style>light-heavy</bar-style>
+      </barline>
+    </measure>
+  </part>
+</score-partwise>
+'''
+
+
 ALL = [
     articulations01, pitches01a, directions31a, lyricsMelisma61d, notations32a,  # 0
     restsDurations02a, rhythmDurations03a, chordsThreeNotesDuration21c,  # 5
@@ -17947,7 +18087,7 @@ ALL = [
     mixedVoices1a, mixedVoices1b, mixedVoices2,  # 37
     colors01, triplets01, textBoxes01, octaveShifts33d,  # 40
     unicodeStrNoNonAscii, unicodeStrWithNonAscii,  # 44
-    tremoloTest, hiddenRests, multiDigitEnding  # 46
+    tremoloTest, hiddenRests, multiDigitEnding, tupletsImplied  # 46
 ]
 
 
diff --git a/music21/test/coverageM21.py b/music21/test/coverageM21.py
index 138329114..3e0651de1 100644
--- a/music21/test/coverageM21.py
+++ b/music21/test/coverageM21.py
@@ -35,7 +35,7 @@ exclude_lines = [
 
 def getCoverage(overrideVersion=False):
     if overrideVersion or sys.version_info.minor == 6:
-        # run on Py 3.6 -- to get Py 3.5/3.7 timing...
+        # run on Py 3.6 -- to get Py 3.7/3.8 timing...
         try:
             import coverage
             cov = coverage.Coverage(omit=omit_modules)
diff --git a/music21/test/testSingleCoreAll.py b/music21/test/testSingleCoreAll.py
index 9b6ae5300..30f31d011 100644
--- a/music21/test/testSingleCoreAll.py
+++ b/music21/test/testSingleCoreAll.py
@@ -119,8 +119,8 @@ def main(testGroup=('test',), restoreEnvironmentDefaults=False, limit=None, verb
     return returnCode
 
 
-def travisMain():
-    # the main call for travis-ci tests.
+def ciMain():
+    # the main call for ci tests.
     # exits with the returnCode
     returnCode = main(verbosity=1)
     sys.exit(returnCode)
diff --git a/music21/test/testStream.py b/music21/test/testStream.py
index b468682d0..9d6416a8a 100644
--- a/music21/test/testStream.py
+++ b/music21/test/testStream.py
@@ -3893,7 +3893,9 @@ class Test(unittest.TestCase):
         '''Testing voices making routines within make notation
         '''
         from music21 import stream
+        from music21.instrument import Xylophone
         s = stream.Stream()
+        s.insert(0, Xylophone())
         s.insert(0, note.Note('C4', quarterLength=8))
         s.repeatInsert(note.Note('b-4', quarterLength=0.5), [x * 0.5 for x in range(16)])
         s.repeatInsert(note.Note('f#5', quarterLength=2), [0, 2, 4, 6])
@@ -3912,6 +3914,9 @@ class Test(unittest.TestCase):
             for n in m.voices[1].notes:  # middle voice has beams
                 self.assertGreater(len(n.beams), 0)
 
+        # check instruments
+        self.assertIsInstance(sPost.getInstruments(recurse=True)[0], Xylophone)
+
     def testMakeNotationC(self):
         '''Test creating diverse, overlapping durations and notes
         '''