Repository: cuthbertLab/music21
Pull request #1758 (2025-03-08 01:49:26)

Finish typing Midi.base
Deprecate many pre-Python3 (unicode vs. bytes) functions.

Hide channel information for non-channel events.


Patch:
diff --git a/music21/common/enums.py b/music21/common/enums.py
index 296ff54e5..4130e2701 100644
--- a/music21/common/enums.py
+++ b/music21/common/enums.py
@@ -13,6 +13,9 @@ from __future__ import annotations
 from enum import Enum, EnumMeta, IntEnum
 import re
 
+# When Python 3.11 is minimum, import EnumType instead of EnumMeta
+
+# when Python 3.12 is minimum, will not need StrEnumMeta at all -- contains will work.
 
 class StrEnumMeta(EnumMeta):
     def __contains__(cls, item):
@@ -27,7 +30,20 @@ class StrEnumMeta(EnumMeta):
             return False
 
 
-class ContainsEnum(IntEnum):
+class ContainsMeta(EnumMeta):
+    '''
+    This is a backport of the Python 3.12 `EnumType` class's contains method.
+    '''
+    def __contains__(cls, item):
+        try:
+            cls(item)  # pylint: disable=no-value-for-parameter
+            return True
+        except ValueError:
+            # Python 3.12 does some more subtle things but not backward compatible.
+            return False
+
+
+class ContainsEnum(IntEnum, metaclass=ContainsMeta):
     '''
     An IntEnum that allows "in" checks against the values of the enum.
     '''
diff --git a/music21/midi/base.py b/music21/midi/base.py
index 62d315ac9..e5e3fbb91 100644
--- a/music21/midi/base.py
+++ b/music21/midi/base.py
@@ -36,8 +36,10 @@ __all__ = [
     'METAEVENT_MARKER',
 ]
 
-import struct
+from collections.abc import Iterable
 import unicodedata
+from typing import overload
+import typing as t
 
 from music21 import common
 from music21.common.enums import ContainsEnum
@@ -54,51 +56,61 @@ environLocal = environment.Environment('midi.base')
 class MidiException(exceptions21.Music21Exception):
     pass
 
-
 # ------------------------------------------------------------------------------
-def charToBinary(char):
+@common.deprecated('v9.7', 'v10', 'use bin(ord(char)) instead.')
+def charToBinary(char: str):
     '''
+    DEPRECATED: just use bin(ord(char)) instead.
+
+    Or for the exact prior output (with left-padding)
+    use `f'{int(bin(ord(char))[2:]):08d}'`
+
+
     Convert a char into its binary representation. Useful for debugging.
 
-    >>> midi.charToBinary('a')
+    >>> #_DOCS_SHOW midi.charToBinary('a')
+    >>> f'{int(bin(ord("a"))[2:]):08d}'  #_DOCS_HIDE
     '01100001'
-    '''
-    asciiValue = ord(char)
-    binaryDigits = []
-    while asciiValue:
-        if (asciiValue & 1) == 1:
-            binaryDigits.append('1')
-        else:
-            binaryDigits.append('0')
-        asciiValue = asciiValue >> 1
 
-    binaryDigits.reverse()
-    binary = ''.join(binaryDigits)
+    Note: This function is deprecated and will be removed in v10.  Music21 actually
+    predates the bin() function in Python (added in Python 2.6).
+    '''
+    binary = bin(ord(char))[2:]
     zeroFix = (8 - len(binary)) * '0'
     return zeroFix + binary
 
-
-def intsToHexBytes(intList):
+@common.deprecated('v9.7', 'v10', 'Just use bytes(...) instead')
+def intsToHexBytes(intList: Iterable[int]) -> bytes:
     r'''
+    (Deprecated: just use bytes(...) instead)
+
     Convert a list of integers into hex bytes, suitable for testing MIDI encoding.
 
     Here we take NOTE_ON message, Middle C, 120 velocity and translate it to bytes
 
-    >>> midi.intsToHexBytes([0x90, 60, 120])
+    >>> #_DOCS_SHOW midi.intsToHexBytes([0x90, 60, 120])
+    >>> bytes([0x90, 60, 120])  #_DOCS_HIDE
     b'\x90<x'
+
+    Note: This function is deprecated and will be removed in v10.  This
+    function has not been needed since music21 became Python 3-only in v.5.
     '''
-    # note off are 128 (0x80) to 143 (0x8F)
-    # note on messages are decimal 144 (0x90) to 159 (0x9F)
-    post = b''
-    for i in intList:
-        # B is an unsigned char
-        # this forces values between 0 and 255
-        # the same as chr(int)
-        post += struct.pack('>B', i)
-    return post
+    return bytes(intList)
+
+
+@overload
+def getNumber(midiStr: int, length: int) -> tuple[int, int]:
+    ...
 
+@overload
+def getNumber(midiStr: str, length: int) -> tuple[int, str]:
+    ...
 
-def getNumber(midiStr, length):
+@overload
+def getNumber(midiStr: bytes, length: int) -> tuple[int, bytes]:
+    ...
+
+def getNumber(midiStr: str|bytes|int, length: int) -> tuple[int, str|bytes|int]:
     '''
     Return the value of a string byte or bytes if length > 1
     from an 8-bit string or bytes object
@@ -111,36 +123,59 @@ def getNumber(midiStr, length):
     Note that MIDI uses big-endian for everything.
     This is the inverse of Python's chr() function.
 
-    >>> midi.getNumber('test', 0)
-    (0, 'test')
+    Read first two bytes
+
+    >>> midi.getNumber(b'test', 2)
+    (29797, b'st')
+
+    That number comes from:
+
+    >>> ord('t') * 256 + ord('e')
+    29797
+
+    Demonstration of reading the whole length in:
+
+    >>> midi.getNumber(b'test', 4)
+    (1952805748, b'')
 
-    Given bytes, return bytes:
+    Reading in zero bytes leaves the midiStr unchanged:
 
     >>> midi.getNumber(b'test', 0)
     (0, b'test')
 
+
+    The method can also take in an integer and return an integer and the remainder part.
+    This usage might be deprecated in the future and has already been replaced within
+    music21 internal code.
+
+    >>> midi.getNumber(516, 1)   # = 2*256 + 4
+    (4, 512)
+
+    As of v9.7, this method can also take a string as input and return a string.
+    This usage is deprecated and will be removed in v10.
+
     >>> midi.getNumber('test', 2)
     (29797, 'st')
-    >>> midi.getNumber(b'test', 4)
-    (1952805748, b'')
     '''
     summation = 0
-    if not common.isNum(midiStr):
+    if not isinstance(midiStr, int):
         for i in range(length):
             midiStrOrNum = midiStr[i]
-            if common.isNum(midiStrOrNum):
+            if isinstance(midiStrOrNum, int):
                 summation = (summation << 8) + midiStrOrNum
-            else:
+            else:  # to remove
+                if t.TYPE_CHECKING:
+                    assert isinstance(midiStrOrNum, str)
                 summation = (summation << 8) + ord(midiStrOrNum)
         return summation, midiStr[length:]
-    else:  # midiStr is a number
+    else:  # midiStr is an int
         midNum = midiStr
         summation = midNum - ((midNum >> (8 * length)) << (8 * length))
         bigBytes = midNum - summation
         return summation, bigBytes
 
 
-def getVariableLengthNumber(midiBytes):
+def getVariableLengthNumber(midiBytes: bytes) -> tuple[int, bytes]:
     r'''
     Given a string or bytes of data, strip off the first character, or all high-byte characters
     terminating with one whose ord() function is < 0x80.  Thus, a variable number of bytes
@@ -156,7 +191,7 @@ def getVariableLengthNumber(midiBytes):
     (65, b'-u')
     >>> midi.getVariableLengthNumber(b'-u')
     (45, b'u')
-    >>> midi.getVariableLengthNumber('u')
+    >>> midi.getVariableLengthNumber(b'u')
     (117, b'')
 
     >>> midi.getVariableLengthNumber(b'test')
@@ -167,68 +202,91 @@ def getVariableLengthNumber(midiBytes):
     (64, b'-E')
     >>> midi.getVariableLengthNumber(b'-E')
     (45, b'E')
-    >>> midi.getVariableLengthNumber('E')
+    >>> midi.getVariableLengthNumber(b'E')
     (69, b'')
 
     Test that variable length characters work:
 
     >>> midi.getVariableLengthNumber(b'\xff\x7f')
     (16383, b'')
-    >>> midi.getVariableLengthNumber('中xy')
+    >>> midi.getVariableLengthNumber('中xy'.encode('utf-8'))
     (210638584, b'y')
 
     If no low-byte character is encoded, raises an IndexError
 
-    >>> midi.getVariableLengthNumber('中国')
+    >>> midi.getVariableLengthNumber('中国'.encode('utf-8'))
     Traceback (most recent call last):
     IndexError: index out of range
+
+    Up to v9.7, this method could also take a string as input and return a string.  This usage is
+    now removed.
     '''
     # from http://faydoc.tripod.com/formats/mid.htm
     # This allows the number to be read one byte at a time, and when you see
     # a msb of 0, you know that it was the last (least significant) byte of the number.
     summation = 0
     i = 0
-    if isinstance(midiBytes, str):
-        midiBytes = midiBytes.encode('utf-8')
-
     while i < 999:  # should return eventually (was "while True")
         x = midiBytes[i]
         # environLocal.printDebug(['getVariableLengthNumber: examined char:',
-        # charToBinary(midiBytes[i])])
+        #     bin(ord(midiBytes[i])))
         summation = (summation << 7) + (x & 0x7F)
         i += 1
         if not (x & 0x80):
-            # environLocal.printDebug(['getVariableLengthNumber: depth read into string: %s' % i])
+            # environLocal.printDebug([f' getVariableLengthNumber: depth read into string: {i}'])
             return summation, midiBytes[i:]
     raise MidiException('did not find the end of the number!')
 
 
-def getNumbersAsList(midiBytes):
+@common.deprecated('v9.7', 'v10', 'use list(midiBytes) instead')
+def getNumbersAsList(midiBytes: bytes|Iterable[int]) -> list[int]:
     r'''
+    Deprecated: this method existed in Python 2.6 and for Python 2 (no bytes)
+    compatibility.  Now use `list(midiBytes)` instead.
+
+    Ability to pass in a string will be removed in v10.
+
+    ----
     Translate each char into a number, return in a list.
     Used for reading data messages where each byte encodes
     a different discrete value.
 
-    >>> midi.getNumbersAsList(b'\x00\x00\x00\x03')
+    >>> #_DOCS_SHOW midi.getNumbersAsList(b'\x00\x00\x00\x03')
+    >>> list(b'\x00\x00\x00\x03') #_DOCS_HIDE
+    [0, 0, 0, 3]
+
+    Now just do:
+
+    >>> list(b'\x00\x00\x00\x03')
     [0, 0, 0, 3]
     '''
     post = []
     for midiByte in midiBytes:
-        if common.isNum(midiByte):
-            post.append(midiByte)
-        else:
+        if isinstance(midiByte, str):
             post.append(ord(midiByte))
+        else:
+            post.append(midiByte)
     return post
 
 
-def putNumber(num, length):
+def putNumber(num: int, length: int) -> bytes:
     r'''
-    Put a single number as a hex number at the end of a string `length` bytes long.
+    Put a single number as a hex number at the end of a bytes object `length` bytes long.
 
     >>> midi.putNumber(3, 4)
     b'\x00\x00\x00\x03'
     >>> midi.putNumber(0, 1)
     b'\x00'
+
+    >>> midi.putNumber(258, 2)
+    b'\x01\x02'
+
+    If the number is larger than the length currently only the least significant
+    bytes are returned. This behavior may change in the near future to raise an
+    exception instead.
+
+    >>> midi.putNumber(258, 1)
+    b'\x02'
     '''
     lst = bytearray()
 
@@ -240,19 +298,24 @@ def putNumber(num, length):
     return bytes(lst)
 
 
-def putVariableLengthNumber(x):
+def putVariableLengthNumber(x: int) -> bytes:
     r'''
-    Turn a number into the smallest bytes object that can hold it for MIDI
+    Turn an integer number into the smallest bytes object that can hold it for MIDI
+
+    Numbers < 128 are encoded as single bytes and are the same as bytes([x])
 
     >>> midi.putVariableLengthNumber(4)
     b'\x04'
     >>> midi.putVariableLengthNumber(127)
     b'\x7f'
+    >>> bytes([127])
+    b'\x7f'
     >>> midi.putVariableLengthNumber(0)
     b'\x00'
 
-    Numbers > 7bit but < 255 need two characters, with the first character to 0x80
-    + n // 128
+    Numbers > 7bit but < 16384 need two characters,
+    with the first character set as 0x80 + n // 128
+    and the second character set as n % 128:
 
     >>> midi.putVariableLengthNumber(128)
     b'\x81\x00'
@@ -260,16 +323,49 @@ def putVariableLengthNumber(x):
     b'\x81\x01'
     >>> midi.putVariableLengthNumber(255)
     b'\x81\x7f'
+
+    This differs from the 8-bit representation of
+    `bytes([x])`:
+
+    >>> bytes([128])
+    b'\x80'
+    >>> bytes([255])
+    b'\xff'
+
+
+    This method can also deal with numbers > 255, which `bytes` cannot.
+
     >>> midi.putVariableLengthNumber(256)
     b'\x82\x00'
+    >>> bytes([256])
+    Traceback (most recent call last):
+    ValueError: bytes must be in range(0, 256)
+
+    It also differs from the normal way of representing 256 in Python bytes:
+
+    >>> bytes([1, 0])
+    b'\x01\x00'
+
+    Here are MIDI representation of other numbers that are too large to fit in 8 bits
+    but stored in two bytes in MIDI.
+
     >>> midi.putVariableLengthNumber(1024)
     b'\x88\x00'
+
+    Notice that the least significant byte is second.
+
+    >>> midi.putVariableLengthNumber(8192)
+    b'\xc0\x00'
     >>> midi.putVariableLengthNumber(8193)
     b'\xc0\x01'
+
+    This is the maximum normal MIDI number that can be stored in 2 bytes.
+
     >>> midi.putVariableLengthNumber(16383)
     b'\xff\x7f'
 
-    Numbers > 16383 are not really MIDI numbers, but this is how they work:
+    Numbers >= 16384 are not used in 2-byte classic MIDI,
+    but this routine continues the basic principle from above:
 
     >>> midi.putVariableLengthNumber(16384)
     b'\x81\x80\x00'
@@ -280,6 +376,8 @@ def putVariableLengthNumber(x):
     >>> midi.putVariableLengthNumber(16384 ** 2)
     b'\x81\x80\x80\x80\x00'
 
+    Negative numbers raise MidiException
+
     >>> midi.putVariableLengthNumber(-1)
     Traceback (most recent call last):
     music21.midi.base.MidiException: cannot putVariableLengthNumber() when number is negative: -1
@@ -301,7 +399,7 @@ def putVariableLengthNumber(x):
     return bytes(lst)
 
 
-def putNumbersAsList(numList):
+def putNumbersAsList(numList: Iterable[int]) -> bytes:
     r'''
     Translate a list of numbers (0-255) into bytes.
     Used for encoding data messages where each byte encodes a different discrete value.
@@ -309,19 +407,29 @@ def putNumbersAsList(numList):
     >>> midi.putNumbersAsList([0, 0, 0, 3])
     b'\x00\x00\x00\x03'
 
-    If a number is < 0 then it wraps around from the top.
+    For positive numbers this method behaves the same as `bytes(numList)` and that
+    method should be used if you are sure all numbers are positive.
+
+    >>> bytes([0, 0, 0, 3])
+    b'\x00\x00\x00\x03'
+
+
+    If a number is < 0 then it wraps around from the top.  This is used in places
+    like MIDI key signatures where flats are represented by `256 - flats`.
 
     >>> midi.putNumbersAsList([0, 0, 0, -3])
     b'\x00\x00\x00\xfd'
     >>> midi.putNumbersAsList([0, 0, 0, -1])
     b'\x00\x00\x00\xff'
 
-    list can be of any length
+    The behavior with negative numbers is different from `bytes(numList)`
+
+    List can be of any length
 
     >>> midi.putNumbersAsList([1, 16, 255])
     b'\x01\x10\xff'
 
-    Any number > 255 raises an exception:
+    Any number > 255 (or less than -255) raises an exception:
 
     >>> midi.putNumbersAsList([256])
     Traceback (most recent call last):
@@ -339,6 +447,10 @@ def putNumbersAsList(numList):
 
 
 class ChannelVoiceMessages(ContainsEnum):
+    '''
+    ChannelVoiceMessages are the main MIDI messages for channel 1-16, such as
+    NOTE_ON, NOTE_OFF, etc.
+    '''
     NOTE_OFF = 0x80
     NOTE_ON = 0x90
     POLYPHONIC_KEY_PRESSURE = 0xA0
@@ -393,6 +505,15 @@ class SysExEvents(ContainsEnum):
 
 METAEVENT_MARKER = 0xFF
 
+
+MidiEventTypes = (
+    ChannelVoiceMessages
+    | ChannelModeMessages
+    | MetaEvents
+    | SysExEvents
+    | t.Literal['DeltaTime']
+)
+
 # ------------------------------------------------------------------------------
 
 
@@ -406,8 +527,9 @@ class MidiEvent(prebase.ProtoM21Object):
 
     The `track` argument must be a :class:`~music21.midi.MidiTrack` object.
 
-    The `type` attribute is an enumeration of a Midi event from the ChannelVoiceMessages
-    or metaEvents enums.
+    The `type` attribute is an enumeration of a Midi event from the ChannelVoiceMessages,
+    ChannelModeMessages, SysExEvents, or MetaEvents enums; if unspecified,
+    defaults to MetaEvents.UNKNOWN.
 
     The `channel` attribute is an integer channel id, from 1 to 16.
 
@@ -446,18 +568,22 @@ class MidiEvent(prebase.ProtoM21Object):
     >>> me2.type = midi.MetaEvents.SEQUENCE_TRACK_NAME
     >>> me2.data = 'guitar'
     >>> me2
-    <music21.midi.MidiEvent SEQUENCE_TRACK_NAME, track=1, channel=None, data=b'guitar'>
+    <music21.midi.MidiEvent SEQUENCE_TRACK_NAME, track=1, data=b'guitar'>
+
+    Changed in v9.7 - None is not a valid type anymore.  Use MetaEvents.UNKNOWN instead.
+        Channel defaults to 1.
     '''
+    # noinspection PyShadowingBuiltins
     # pylint: disable=redefined-builtin
     def __init__(self,
                  track: MidiTrack|None = None,
-                 type=None,
+                 type: MidiEventTypes = MetaEvents.UNKNOWN,
                  time: int = 0,
-                 channel: int|None = None):
+                 channel: int = 1):
         self.track: MidiTrack|None = track  # a MidiTrack object
-        self.type = type
+        self.type: MidiEventTypes = type
         self.time: int = time
-        self.channel: int|None = channel
+        self.channel: int = channel
 
         self.parameter1: int|bytes|None = None  # pitch or first data value
         self.parameter2: int|bytes|None = None  # velocity or second data value
@@ -488,9 +614,9 @@ class MidiEvent(prebase.ProtoM21Object):
         >>> pitchBend = midi.MidiEvent(type=CVM.PITCH_BEND)
 
         >>> sorted([noteOn, noteOff, pitchBend], key=lambda me: me.sortOrder)
-        [<music21.midi.MidiEvent NOTE_OFF, track=None, channel=None>,
-         <music21.midi.MidiEvent PITCH_BEND, track=None, channel=None>,
-         <music21.midi.MidiEvent NOTE_ON, track=None, channel=None>]
+        [<music21.midi.MidiEvent NOTE_OFF, track=None, channel=1>,
+         <music21.midi.MidiEvent PITCH_BEND, track=None, channel=1>,
+         <music21.midi.MidiEvent NOTE_ON, track=None, channel=1>]
         '''
         # update based on type; type may be set after init
         if self.type == ChannelVoiceMessages.NOTE_OFF:  # should come before pitch bend
@@ -500,23 +626,29 @@ class MidiEvent(prebase.ProtoM21Object):
         else:
             return 0
 
-    def _reprInternal(self):
+    def _reprInternal(self) -> str:
+        trackIndex: int|None
         if self.track is None:
             trackIndex = None
-        elif isinstance(self.track, int):
+        elif isinstance(self.track, int):  # should not happen anymore
             trackIndex = self.track
         else:
             trackIndex = self.track.index
 
+        printType: str
         if isinstance(self.type, ContainsEnum):
             printType = self.type.name
-        else:
+        elif self.type is None:
+            printType = 'None'
+        else:  # should not happen anymore
             printType = repr(self.type)
 
         r = f'{printType}, '
         if self.time != 0:
             r += f't={self.time!r}, '
-        r += f'track={trackIndex}, channel={self.channel!r}'
+        r += f'track={trackIndex}'
+        if self.type in ChannelVoiceMessages or self.type in ChannelModeMessages:
+            r += f', channel={self.channel!r}'
         if self.type in (ChannelVoiceMessages.NOTE_ON, ChannelVoiceMessages.NOTE_OFF):
             attrList = ['pitch', 'velocity']
         else:
@@ -531,41 +663,33 @@ class MidiEvent(prebase.ProtoM21Object):
         return r
 
     # provide parameter access to pitch and velocity
-    def _setPitch(self, value):
-        self.parameter1 = value
-
-    def _getPitch(self):
+    @property
+    def pitch(self) -> int|None:
         # only return pitch if this is note on /off
-        if self.type in (ChannelVoiceMessages.NOTE_ON, ChannelVoiceMessages.NOTE_OFF):
-            return self.parameter1
+        if (self.type in (ChannelVoiceMessages.NOTE_ON, ChannelVoiceMessages.NOTE_OFF)
+                and self.parameter1 is not None):
+            return int(self.parameter1)
         else:
             return None
 
-    pitch = property(_getPitch, _setPitch)
-
-    def _setVelocity(self, value):
-        self.parameter2 = value
+    @pitch.setter
+    def pitch(self, value: int|None):
+        self.parameter1 = value
 
-    def _getVelocity(self):
+    @property
+    def velocity(self) -> int|None:
+        if not isinstance(self.parameter2, int):
+            return None
         return self.parameter2
 
-    velocity = property(_getVelocity, _setVelocity)
+    @velocity.setter
+    def velocity(self, value: int|None):
+        self.parameter2 = value
 
     # store generic data in parameter 1
-
-    def _setData(self, value):
-        if value is not None and not isinstance(value, bytes):
-            if isinstance(value, str):
-                value = value.encode('utf-8')
-            elif isinstance(value, bool):
-                value = bytes([int(value)])
-
-        self.parameter1 = value
-
-    def _getData(self):
-        return self.parameter1
-
-    data = property(_getData, _setData, doc=r'''
+    @property
+    def data(self) -> int|bytes|None:
+        r'''
         Read or set the data (`.parameter1`) for the object
 
         Does some automatic conversions:
@@ -574,9 +698,20 @@ class MidiEvent(prebase.ProtoM21Object):
         >>> me.data = True
         >>> me.data
         b'\x01'
-    ''')
+        '''
+        return self.parameter1
+
+    @data.setter
+    def data(self, value: int|str|bytes|bool|None):
+        if value is not None and not isinstance(value, bytes):
+            if isinstance(value, str):
+                value = value.encode('utf-8')
+            elif isinstance(value, bool):
+                value = bytes([int(value)])
+        self.parameter1 = value
+
 
-    def setPitchBend(self, cents, bendRange=2):
+    def setPitchBend(self, cents: int|float, bendRange=2) -> None:
         '''
         Treat this event as a pitch bend value, and set the .parameter1 and
          .parameter2 fields appropriately given a specified bend value in cents.
@@ -585,9 +720,8 @@ class MidiEvent(prebase.ProtoM21Object):
 
         The `bendRange` parameter gives the number of half steps in the bend range.
 
-
         >>> mt = midi.MidiTrack(1)
-        >>> me1 = midi.MidiEvent(mt)
+        >>> me1 = midi.MidiEvent(mt, type=midi.ChannelVoiceMessages.PITCH_BEND)
         >>> me1.setPitchBend(50)
         >>> me1.parameter1, me1.parameter2
         (0, 80)
@@ -608,7 +742,7 @@ class MidiEvent(prebase.ProtoM21Object):
         >>> me1.parameter1, me1.parameter2
         (40, 96)
 
-        Exceeding maximum sets max
+        Exceeding maximum pitch bend sets the max (127, 127)
 
         >>> me1.setPitchBend(200)
         >>> me1.parameter1, me1.parameter2
@@ -647,7 +781,7 @@ class MidiEvent(prebase.ProtoM21Object):
 
         OMIT_FROM_DOCS
 
-        Pitch bends very close to 0 had problems
+        Pitch bends very close to 0 formerly had problems
 
         >>> me1.setPitchBend(-196)
         >>> me1.parameter1, me1.parameter2
@@ -678,20 +812,18 @@ class MidiEvent(prebase.ProtoM21Object):
             shift = 0
         target = center + shift
 
-        # produce a two-char value
+        # produce a 1-2-byte value with most significant byte first.
         charValue = putVariableLengthNumber(target)
-        d1, junk = getNumber(charValue[0], 1)  # d1 = parameter2
+        d1 = charValue[0]  # d1 = parameter2; bytes[i] returns an int
         # need to convert from 8 bit to 7, so using & 0x7F
         d1 = d1 & 0x7F
         if len(charValue) > 1:
-            d2, junk = getNumber(charValue[1], 1)
+            d2 = charValue[1]
             d2 = d2 & 0x7F
         else:
             d2 = d1
             d1 = 0
 
-
-
         # environLocal.printDebug(['got target char value', charValue,
         # 'getVariableLengthNumber(charValue)', getVariableLengthNumber(charValue)[0],
         # 'd1', d1, 'd2', d2,])
@@ -702,39 +834,46 @@ class MidiEvent(prebase.ProtoM21Object):
     def parseChannelVoiceMessage(self, midiBytes: bytes) -> bytes:
         r'''
         Take a set of bytes that represent a ChannelVoiceMessage and set the
-        appropriate enumeration value and data, returning the remaining bytes.
-
-        These are started with messages from 0x80 to 0xEF
+        appropriate event type and data, returning the remaining bytes.
 
+        Channel voice messages start with a one-byte message from 0x80 to 0xEF,
+        where the first nibble (8-E) is the message type and the second nibble (0-F)
+        is the channel represented in hex.
 
-        Demonstration.  First let's create a helper function and a MidiEvent:
+        First let's create a MidiEvent that does not know its type.
 
-        >>> to_bytes = midi.intsToHexBytes
-        >>> midBytes = to_bytes([0x90, 60, 120])
-        >>> midBytes
-        b'\x90<x'
-        >>> midBytes += b'hello'
         >>> mt = midi.MidiTrack(1)
         >>> me1 = midi.MidiEvent(mt)
         >>> me1
-        <music21.midi.MidiEvent None, track=1, channel=None>
+        <music21.midi.MidiEvent UNKNOWN, track=1>
+
+        Now create two messages -- a note on (at pitch = 60 or C4) at velocity 120
+
+        >>> midBytes = bytes([0x92, 60, 120])
+        >>> midBytes
+        b'\x92<x'
+
+        Add a second hypothetical message (would normally be a delta time)
+
+        >>> midBytes += b'hello'
+        >>> midBytes
+        b'\x92<xhello'
 
 
-        Now show how the midiBytes changes the event:
+        Now see how parsing this ChannelVoiceMessage changes the MidiEvent
 
         >>> remainder = me1.parseChannelVoiceMessage(midBytes)
         >>> me1
-        <music21.midi.MidiEvent NOTE_ON, track=1, channel=1, pitch=60, velocity=120>
+        <music21.midi.MidiEvent NOTE_ON, track=1, channel=3, pitch=60, velocity=120>
 
         The remainder would probably contain a delta time and following
-        events, but here we'll just show that it passes through.
+        events, but here we'll just show that it passes whatever is left through.
 
         >>> remainder
         b'hello'
 
-        We will ignore remainders from now on.
-
-        All attributes are set properly:
+        We will ignore remainders for the rest of this demo.  Note that all
+        attributes are set properly:
 
         >>> me1.type
         <ChannelVoiceMessages.NOTE_ON: 0x90>
@@ -742,30 +881,24 @@ class MidiEvent(prebase.ProtoM21Object):
         60
         >>> me1.velocity
         120
-        >>> me1.channel
-        1
-
 
-        Here we send the message for a note on another channel (0x91 = channel 2):
+        The channel is 3 because 0x90 is NOTE_ON for channel 1, 0x91 is 2, and 0x92 is 3, etc.
 
-        >>> rem = me1.parseChannelVoiceMessage(to_bytes([0x91, 60, 120]))
-        >>> me1
-        <music21.midi.MidiEvent NOTE_ON, track=1, channel=2, pitch=60, velocity=120>
         >>> me1.channel
-        2
+        3
 
-        Now let's make a program change
+        Now let's make a program change on channel 16:
 
         >>> me2 = midi.MidiEvent(mt)
-        >>> rem = me2.parseChannelVoiceMessage(to_bytes([0xC0, 71]))
+        >>> rem = me2.parseChannelVoiceMessage(bytes([0xCF, 71]))
         >>> me2
-        <music21.midi.MidiEvent PROGRAM_CHANGE, track=1, channel=1, data=71>
+        <music21.midi.MidiEvent PROGRAM_CHANGE, track=1, channel=16, data=71>
         >>> me2.data  # 71 = clarinet (0-127 indexed)
         71
 
-        Program change and channel pressure only go to 127.  More than that is an error:
+        Program changes and channel pressures only go to 127.  More than that signals an error:
 
-        >>> me2.parseChannelVoiceMessage(to_bytes([0xC0, 200]))
+        >>> me2.parseChannelVoiceMessage(bytes([0xCF, 200]))
         Traceback (most recent call last):
         music21.midi.base.MidiException: Cannot have a
             <ChannelVoiceMessages.PROGRAM_CHANGE: 0xC0> followed by a byte > 127: 200
@@ -867,16 +1000,16 @@ class MidiEvent(prebase.ProtoM21Object):
             # 'self.lastStatusByte:', self.lastStatusByte])
 
             if self.lastStatusByte is not None:
-                rsb = bytes([self.lastStatusByte])
+                runningStatusByte = bytes([self.lastStatusByte])
             else:  # provide a default
-                rsb = b'\x90'
+                runningStatusByte = b'\x90'
             # add the running status byte to the front of the string
             # and process as before
-            midiBytes = rsb + midiBytes
+            midiBytes = runningStatusByte + midiBytes
             byte0 = midiBytes[0]
-        elif midiBytes[0] != 0xff:
+        elif byte0 != 0xff:
             # store last status byte, unless it's a meta message
-            self.lastStatusByte = midiBytes[0]
+            self.lastStatusByte = byte0
 
         msgType: int = byte0 & 0xF0  # bitwise and to derive message type w/o channel
 
@@ -901,7 +1034,7 @@ class MidiEvent(prebase.ProtoM21Object):
             if MetaEvents.hasValue(byte1):
                 self.type = MetaEvents(byte1)
             else:
-                # environLocal.printDebug([f'unknown meta event: FF {byte1:02X}'])
+                # environLocal.printDebug([f' unknown meta event: FF {byte1:02X}'])
                 # sys.stdout.flush()
                 self.type = MetaEvents.UNKNOWN
             length, midiBytesAfterLength = getVariableLengthNumber(midiBytes[2:])
@@ -914,54 +1047,89 @@ class MidiEvent(prebase.ProtoM21Object):
                                      'hex(midiBytes[1])', hex(midiBytes[1])])
             raise MidiException(f'Unknown midi event type {hex(byte0)}')
 
-    def getBytes(self):
+    def getBytes(self) -> bytes:
         r'''
-        Return a set of bytes for this MIDI event.
+        Return a set of bytes for this MIDI event (used for translating from music21 to MIDI)
 
         >>> noteOn = midi.MidiEvent(type=midi.ChannelVoiceMessages.NOTE_ON, channel=10)
         >>> noteOn.pitch = 60
         >>> noteOn.velocity = 127
         >>> noteOn.getBytes()
         b'\x99<\x7f'
+
+        Changing the pitch changes the second byte (from less than to greater than):
+
         >>> noteOn.pitch = 62
         >>> noteOn.getBytes()
         b'\x99>\x7f'
+        >>> ord('>')
+        62
+
+        Changing the velocity changes the third byte:
+
+        >>> noteOn.velocity = 64
+        >>> noteOn.getBytes()
+        b'\x99>@'
+
+        The third byte is `\x40` but it is represented by '@'
+
+        >>> ord('@')
+        64
+        >>> hex(64)
+        '0x40'
+
+        And changing the channel changes the first byte:
+
+        >>> noteOn.channel = 11
+        >>> noteOn.getBytes()
+        b'\x9a>@'
         '''
+        if t.TYPE_CHECKING:
+            # no DeltaTime here.
+            assert isinstance(self.type, ContainsEnum)
+
         if self.type in ChannelVoiceMessages:
             # environLocal.printDebug(['writing channelVoiceMessages', self.type])
             bytes0 = bytes([self.channel - 1 + self.type.value])
             # for writing note-on/note-off
             if self.type not in (ChannelVoiceMessages.PROGRAM_CHANGE,
                                  ChannelVoiceMessages.CHANNEL_KEY_PRESSURE):
+                if self.parameter1 is None or self.parameter2 is None:
+                    raise MidiException(
+                        'Cannot write MIDI event without parameter1 and parameter2 set.'
+                    )
+
                 # this results in a two-part string, like '\x00\x00'
-                param1data = b''
+                param1data: bytes
                 if isinstance(self.parameter1, int):
                     param1data = bytes([self.parameter1])
                 else:
                     param1data = self.parameter1
 
-                param2data = b''
+                param2data: bytes
                 if isinstance(self.parameter2, int):
                     param2data = bytes([self.parameter2])
                 else:
                     param2data = self.parameter2
 
                 data = param1data + param2data
-            elif self.type == ChannelVoiceMessages.PROGRAM_CHANGE:
+            elif self.type == ChannelVoiceMessages.PROGRAM_CHANGE and isinstance(self.data, int):
                 data = bytes([self.data])
             else:  # all the other messages
                 try:
                     if isinstance(self.data, int):
                         data = bytes([self.data])
-                    else:
+                    elif isinstance(self.data, bytes):
                         data = self.data
+                    else:
+                        raise TypeError('data must be bytes or int')
                 except (TypeError, ValueError):
                     raise MidiException(
-                        f'Got incorrect data for {self} in .data: {self.data}, '
+                        f'Got incorrect data for {self} in .data: {self.data!r}, '
                         + 'cannot parse Miscellaneous Message')
             return bytes0 + data
 
-        elif self.type in ChannelModeMessages:
+        elif self.type in ChannelModeMessages and self.data is not None:
             channelMode = bytes([0xB0 + self.channel - 1])
             msgValue = bytes([self.type.value])
             if isinstance(self.data, int):
@@ -970,18 +1138,20 @@ class MidiEvent(prebase.ProtoM21Object):
                 data = self.data
             return channelMode + msgValue + data
 
-        elif self.type in SysExEvents:
+        elif self.type in SysExEvents and isinstance(self.data, bytes):
             s = bytes([self.type.value])
             s = s + putVariableLengthNumber(len(self.data))
-            return s + self.data
+            sys_data = t.cast(bytes, self.data)
+            return s + sys_data
 
-        elif self.type in MetaEvents:
+        elif self.type in MetaEvents and isinstance(self.data, (bytes, str)):
             s = bytes([METAEVENT_MARKER])
             s += bytes([self.type.value])
             s += putVariableLengthNumber(len(self.data))
 
             try:
-                return s + self.data
+                sys_data = t.cast(bytes, self.data)
+                return s + sys_data
             except (UnicodeDecodeError, TypeError):
                 # environLocal.printDebug(['cannot decode data', self.data])
 
@@ -989,13 +1159,13 @@ class MidiEvent(prebase.ProtoM21Object):
                 # noinspection PyTypeChecker
                 return s + unicodedata.normalize(
                     'NFKD',
-                    self.data
+                    t.cast(str, self.data)
                 ).encode('ascii', 'ignore')
         else:
             raise MidiException(f'unknown midi event type: {self.type!r}')
 
     # --------------------------------------------------------------------------
-    def isNoteOn(self):
+    def isNoteOn(self) -> bool:
         '''
         Return a boolean if this is a note-on message and velocity is not zero.
 
@@ -1007,17 +1177,32 @@ class MidiEvent(prebase.ProtoM21Object):
         True
         >>> me1.isNoteOff()
         False
+
+        A zero velocity note-on is treated as a note-off:
+
+        >>> me1.velocity = 0
+        >>> me1.isNoteOn()
+        False
+        >>> me1.isNoteOff()
+        True
+
+        A midi event can be neither a note on or a note off.
+
+        >>> me1.type = midi.ChannelVoiceMessages.PROGRAM_CHANGE
+        >>> me1.isNoteOn()
+        False
+        >>> me1.isNoteOff()
+        False
         '''
         if self.type == ChannelVoiceMessages.NOTE_ON and self.velocity != 0:
             return True
         return False
 
-    def isNoteOff(self):
+    def isNoteOff(self) -> bool:
         '''
         Return a boolean if this should be interpreted as a note-off message,
         either as a real note-off or as a note-on with zero velocity.
 
-
         >>> mt = midi.MidiTrack(1)
         >>> me1 = midi.MidiEvent(mt)
         >>> me1.type = midi.ChannelVoiceMessages.NOTE_OFF
@@ -1033,6 +1218,8 @@ class MidiEvent(prebase.ProtoM21Object):
         False
         >>> me2.isNoteOff()
         True
+
+        See :meth:`~music21.midi.MidiEvent.isNoteOn` for more examples.
         '''
         if self.type == ChannelVoiceMessages.NOTE_OFF:
             return True
@@ -1040,7 +1227,7 @@ class MidiEvent(prebase.ProtoM21Object):
             return True
         return False
 
-    def isDeltaTime(self):
+    def isDeltaTime(self) -> bool:
         '''
         Return a boolean if this is a DeltaTime subclass.
 
@@ -1053,14 +1240,13 @@ class MidiEvent(prebase.ProtoM21Object):
             return True
         return False
 
-    def matchedNoteOff(self, other):
+    def matchedNoteOff(self, other: MidiEvent) -> bool:
         '''
         Returns True if `other` is a MIDI event that specifies
         a note-off message for this message.  That is, this event
         is a NOTE_ON message, and the other is a NOTE_OFF message
         for this pitch on this channel.  Otherwise returns False
 
-
         >>> mt = midi.MidiTrack(1)
         >>> me1 = midi.MidiEvent(mt)
         >>> me1.type = midi.ChannelVoiceMessages.NOTE_ON
@@ -1098,8 +1284,7 @@ class MidiEvent(prebase.ProtoM21Object):
         >>> me1.matchedNoteOff(me2)
         False
         '''
-        if other.isNoteOff():
-            # might check velocity here too?
+        if self.isNoteOn() and other.isNoteOff():
             if self.pitch == other.pitch and self.channel == other.channel:
                 return True
         return False
@@ -1117,20 +1302,27 @@ class DeltaTime(MidiEvent):
     Time values are in integers, representing ticks.
 
     The `channel` attribute, inherited from MidiEvent is not used and set to None
-    unless overridden (don't!).
+    unless overridden (it does not do anything if set and is not written out to MIDI).
 
     >>> mt = midi.MidiTrack(1)
     >>> dt = midi.DeltaTime(mt)
     >>> dt.time = 1
     >>> dt
-    <music21.midi.DeltaTime t=1, track=1, channel=None>
-    '''
+    <music21.midi.DeltaTime t=1, track=1>
 
-    def __init__(self, track, time=0, channel=None):
+    Changed in v9.7 -- DeltaTime repr no longer lists its channel (to de-emphasize it).
+        Channel defaults to 1.
+    '''
+    def __init__(
+        self,
+        track: MidiTrack|None = None,
+        time: int = 0,
+        channel: int = 1,
+    ):
         super().__init__(track, time=time, channel=channel)
         self.type = 'DeltaTime'
 
-    def _reprInternal(self):
+    def _reprInternal(self) -> str:
         rep = super()._reprInternal()
         rep = rep.replace("'DeltaTime', ", '')
         if self.time == 0:
@@ -1140,12 +1332,20 @@ class DeltaTime(MidiEvent):
     def readUntilLowByte(self, oldBytes: bytes) -> tuple[int, bytes]:
         r'''
         Read a byte-string until hitting a character below 0x80
-        and return the converted number and the rest of the bytes
+        and return the converted number and the rest of the bytes.
+
+        The first number is also stored in `self.time`.
 
         >>> mt = midi.MidiTrack(1)
         >>> dt = midi.DeltaTime(mt)
+        >>> dt.time
+        0
+
         >>> dt.readUntilLowByte(b'\x20')
         (32, b'')
+        >>> dt.time
+        32
+
         >>> dt.readUntilLowByte(b'\x20hello')
         (32, b'hello')
 
@@ -1155,7 +1355,7 @@ class DeltaTime(MidiEvent):
         >>> dt.readUntilLowByte(b'\x82hello')
         (360, b'ello')
 
-        Changed in v9: was read() but had an incompatible signature with MidiEvent
+        Changed in v9: had an incompatible signature with MidiEvent
         '''
         self.time, newBytes = getVariableLengthNumber(oldBytes)
         return self.time, newBytes
@@ -1222,22 +1422,28 @@ class MidiTrack(prebase.ProtoM21Object):
 
     >>> mt.length
     22
+
+    New in v9.7: len(mt) returns the same as mt.length, but more Pythonic.
+
+    >>> len(mt)
+    22
+
     >>> mt.data[:8]
     b'\x00\xff\x03\x00\x00\xe0\x00@'
 
     Note that the '\x16' got translated to ascii '@'.
 
     >>> mt.events
-    [<music21.midi.DeltaTime (empty) track=3, channel=None>,
-     <music21.midi.MidiEvent SEQUENCE_TRACK_NAME, track=3, channel=None, data=b''>,
-     <music21.midi.DeltaTime (empty) track=3, channel=None>,
+    [<music21.midi.DeltaTime (empty) track=3>,
+     <music21.midi.MidiEvent SEQUENCE_TRACK_NAME, track=3, data=b''>,
+     <music21.midi.DeltaTime (empty) track=3>,
      <music21.midi.MidiEvent PITCH_BEND, track=3, channel=1, parameter1=0, parameter2=64>,
-     <music21.midi.DeltaTime (empty) track=3, channel=None>,
+     <music21.midi.DeltaTime (empty) track=3>,
      <music21.midi.MidiEvent NOTE_ON, track=3, channel=1, pitch=67, velocity=90>,
-     <music21.midi.DeltaTime t=1024, track=3, channel=None>,
+     <music21.midi.DeltaTime t=1024, track=3>,
      <music21.midi.MidiEvent NOTE_OFF, track=3, channel=1, pitch=67, velocity=0>,
-     <music21.midi.DeltaTime t=1024, track=3, channel=None>,
-     <music21.midi.MidiEvent END_OF_TRACK, track=3, channel=None, data=b''>]
+     <music21.midi.DeltaTime t=1024, track=3>,
+     <music21.midi.MidiEvent END_OF_TRACK, track=3, data=b''>]
 
     >>> mt
     <music21.midi.MidiTrack 3 -- 10 events>
@@ -1250,13 +1456,19 @@ class MidiTrack(prebase.ProtoM21Object):
     '''
     headerId = b'MTrk'
 
-    def __init__(self, index=0):
+    def __init__(self, index: int = 0):
         self.index = index
-        self.events = []
+        self.events: list[MidiEvent] = []  # or DeltaTime subclass
         self.data = b''
 
+    def __len__(self) -> int:
+        '''
+        New in v9.7
+        '''
+        return len(self.data)
+
     @property
-    def length(self):
+    def length(self) -> int:
         return len(self.data)
 
     def read(self, midiBytes: bytes) -> bytes:
@@ -1266,9 +1478,11 @@ class MidiTrack(prebase.ProtoM21Object):
 
         The string should begin with `MTrk`, specifying a Midi Track
 
-        Calls `processDataToEvents` which creates and stores
+        Stores the read data (after the header and length information) in self.data
+
+        Calls `processDataToEvents` which creates
         :class:`~music21.midi.DeltaTime`
-        and :class:`~music21.midi.MidiEvent` objects.
+        and :class:`~music21.midi.MidiEvent` objects and stores them in self.events
         '''
         if not midiBytes[:4] == self.headerId:
             raise MidiException('badly formed midi string: missing leading MTrk')
@@ -1288,8 +1502,13 @@ class MidiTrack(prebase.ProtoM21Object):
         '''
         Populate .events with trackData.  Called by .read()
         '''
-        time = 0  # a running counter of ticks
-        previousMidiEvent = None
+        time: int = 0  # a running counter of ticks
+        previousMidiEvent: MidiEvent|None = None
+
+        dt: int
+        trackDataCandidate: bytes
+        timeCandidate: int
+
         while trackData:
             # shave off the time stamp from the event
             delta_t = DeltaTime(track=self)
@@ -1300,70 +1519,111 @@ class MidiTrack(prebase.ProtoM21Object):
 
             # pass self to event, set this MidiTrack as the track for this event
             midiEvent = MidiEvent(track=self)
-            if previousMidiEvent is not None:  # set the last status byte
+            if previousMidiEvent is not None:  # synchronize the last status byte
                 midiEvent.lastStatusByte = previousMidiEvent.lastStatusByte
+
             # some midi events may raise errors; simply skip for now
             try:
-                trackDataCandidate = midiEvent.read(trackDataCandidate)
+                trackData = midiEvent.read(trackDataCandidate)
+                time = timeCandidate
             except MidiException:
                 # assume that trackData, after delta extraction, is still correct
                 # environLocal.printDebug(['forced to skip event; delta_t:', delta_t])
                 # set to result after taking delta time
                 trackData = trackDataCandidate
                 continue
-            # only set after trying to read, which may raise exception
-            time = timeCandidate
-            trackData = trackDataCandidate  # remainder bytes
+
             # only append if we get this far
             self.events.append(delta_t)
             self.events.append(midiEvent)
             previousMidiEvent = midiEvent
 
-    def getBytes(self):
+    def getBytes(self) -> bytes:
         r'''
-        returns bytes of midi-data from the `.events` in the object.
+        Returns bytes of midi-data from the `.events` in the object.
+        For conversion from music21 to MIDI.
+
+        For example: two events, one note-on and one note-off on C4 which appears as "<"
+        in the data.
 
         >>> mt = midi.MidiTrack(index=2)
-        >>> noteOn = midi.MidiEvent(mt, type=midi.ChannelVoiceMessages.NOTE_ON, channel=1)
+        >>> dt1 = midi.DeltaTime(mt, time=0)
+        >>> noteOn = midi.MidiEvent(mt, type=midi.ChannelVoiceMessages.NOTE_ON, channel=3)
         >>> noteOn.pitch = 60
         >>> noteOn.velocity = 20
-        >>> dt = midi.DeltaTime(mt, time=1024)
-        >>> noteOff = midi.MidiEvent(mt, type=midi.ChannelVoiceMessages.NOTE_OFF, channel=1)
+        >>> dt2 = midi.DeltaTime(mt, time=1030)
+        >>> noteOff = midi.MidiEvent(mt, type=midi.ChannelVoiceMessages.NOTE_OFF, channel=3)
         >>> noteOff.pitch = 60
         >>> noteOff.velocity = 0
 
-        >>> mt.events = [noteOn, dt, noteOff]
-        >>> mt.getBytes()
-        b'MTrk\x00\x00\x00\x08\x90<\x14\x88\x00\x80<\x00'
+        >>> mt.events = [dt1, noteOn, dt2, noteOff]
+        >>> bytes = mt.getBytes()
+        >>> bytes
+        b'MTrk\x00\x00\x00\t\x00\x92<\x14\x88\x06\x82<\x00'
+
+        Explanation:
+
+        The first four bytes `MTrk` are the header for any MIDI Track
+
+        >>> bytes[:4]
+        b'MTrk'
+
+        The next four bytes are the length of the data in bytes, the final `b'\t'`
+        indicates there are 9 bytes to follow.
+
+        >>> midi.base.getNumber(bytes[4:8], 4)
+        (9, b'')
+
+        The next byte is an empty delta time event.
+
+        >>> bytes[8]
+        0
 
-        The b'\x08' indicates that there are 8 bytes to follow.
+        The next three bytes are the note-on event.
+
+        >>> noteOn2 = midi.MidiEvent()
+        >>> _ = noteOn2.parseChannelVoiceMessage(bytes[9:12])
+        >>> noteOn2
+        <music21.midi.MidiEvent NOTE_ON, track=None, channel=3, pitch=60, velocity=20>
+
+        Followed by two bytes for the DeltaTime of 1030 ticks.
+
+        >>> dt3 = midi.DeltaTime()
+        >>> dt3.readUntilLowByte(bytes[12:14])
+        (1030, b'')
+
+        The 1030 is stored as b'\x88\x06' where the first byte is 136 and the second 6,
+        but MIDI stores each continuing digit as the last 7 bits of a 8-bit number with
+        first bit 1, so we remove the first bit by subtracting 128: 136 - 128 = 8.
+        Then we multiply 8 by 128 to get 1024 and add 6 to get 1030.
+
+        Finally, the last three bytes are the note-off event.
+
+        >>> noteOff2 = midi.MidiEvent()
+        >>> _ = noteOff2.parseChannelVoiceMessage(bytes[14:])
+        >>> noteOff2
+        <music21.midi.MidiEvent NOTE_OFF, track=None, channel=3, pitch=60, velocity=0>
         '''
         # set time to the first event
         # time = self.events[0].time
         # build str using MidiEvents
-        midiBytes = b''
+        midiBytes: list[bytes] = []
         for midiEvent in self.events:
             # this writes both delta time and message events
             try:
-                eventBytes = midiEvent.getBytes()
-                intArray = []
-                for x in eventBytes:
-                    if common.isNum(x):
-                        intArray.append(x)
-                    else:
-                        intArray.append(ord(x))
-                eventBytes = bytes(bytearray(intArray))
-                midiBytes = midiBytes + eventBytes
+                midiBytes.append(midiEvent.getBytes())
             except MidiException as ex:
                 environLocal.warn(f'Conversion error for {midiEvent}: {ex}; ignored.')
-        return self.headerId + putNumber(len(midiBytes), 4) + midiBytes
+
+        bytes_out = b''.join(midiBytes)
+        return self.headerId + putNumber(len(bytes_out), 4) + bytes_out
 
     def _reprInternal(self):
         r = f'{self.index} -- {len(self.events)} events'
         return r
 
     # --------------------------------------------------------------------------
-    def updateEvents(self):
+    def updateEvents(self) -> None:
         '''
         We may attach events to this track before setting their `track` parameter.
         This method will move through all events and set their track to this track.
@@ -1385,7 +1645,7 @@ class MidiTrack(prebase.ProtoM21Object):
         for e in self.events:
             e.track = self
 
-    def hasNotes(self):
+    def hasNotes(self) -> bool:
         '''
         Return True/False if this track has any note-ons defined.
 
@@ -1404,9 +1664,9 @@ class MidiTrack(prebase.ProtoM21Object):
                 return True
         return False
 
-    def setChannel(self, value):
+    def setChannel(self, value: int) -> None:
         '''
-        Set the channel of all events in this Track.
+        Set the one-indexed channel of all events in this Track.
 
         >>> mt = midi.MidiTrack(index=2)
         >>> noteOn = midi.MidiEvent(type=midi.ChannelVoiceMessages.NOTE_ON, channel=1)
@@ -1426,7 +1686,7 @@ class MidiTrack(prebase.ProtoM21Object):
         for e in self.events:
             e.channel = value
 
-    def getChannels(self):
+    def getChannels(self) -> list[int]:
         '''
         Get all channels (excluding None) used in this Track (sorted)
 
@@ -1441,13 +1701,13 @@ class MidiTrack(prebase.ProtoM21Object):
         >>> mt.getChannels()
         [5, 14]
         '''
-        post = []
+        post: set[int] = set()
         for e in self.events:
-            if e.channel not in post and e.channel is not None:
-                post.append(e.channel)
+            if e.channel is not None:
+                post.add(e.channel)
         return sorted(post)
 
-    def getProgramChanges(self):
+    def getProgramChanges(self) -> list[int]:
         '''
         Get all unique program changes used in this Track, in order they appear.
 
@@ -1462,11 +1722,12 @@ class MidiTrack(prebase.ProtoM21Object):
         >>> mt.getProgramChanges()
         [14, 1]
         '''
-        post = []
+        post: list[int] = []
         for e in self.events:
-            if e.type == ChannelVoiceMessages.PROGRAM_CHANGE:
-                if e.data not in post:
-                    post.append(e.data)
+            if (e.type == ChannelVoiceMessages.PROGRAM_CHANGE
+                    and isinstance(e.data, int)
+                    and e.data not in post):  # O(127) = O(1), faster than a set lookup + append
+                post.append(e.data)
         return post
 
 
@@ -1512,14 +1773,14 @@ class MidiFile(prebase.ProtoM21Object):
     '''
     headerId = b'MThd'
 
-    def __init__(self):
-        self.file = None
-        self.format = 1
-        self.tracks = []
-        self.ticksPerQuarterNote = defaults.ticksPerQuarter
-        self.ticksPerSecond = None
+    def __init__(self) -> None:
+        self.file: t.BinaryIO|None = None
+        self.format: int = 1
+        self.tracks: list[MidiTrack] = []
+        self.ticksPerQuarterNote: int = defaults.ticksPerQuarter
+        self.ticksPerSecond: int|None = None
 
-    def open(self, filename, attrib='rb'):
+    def open(self, filename, attrib='rb') -> None:
         '''
         Open a MIDI file path for reading or writing.
 
@@ -1528,9 +1789,9 @@ class MidiFile(prebase.ProtoM21Object):
         if attrib not in ['rb', 'wb']:
             raise MidiException('cannot read or write unless in binary mode, not:', attrib)
         # pylint: disable-next=consider-using-with, unspecified-encoding
-        self.file = open(filename, attrib)
+        self.file = t.cast(t.BinaryIO, open(filename, attrib))
 
-    def openFileLike(self, fileLike):
+    def openFileLike(self, fileLike: t.BinaryIO) -> None:
         '''
         Assign a file-like object, such as those provided by BytesIO, as an open file object.
 
@@ -1542,30 +1803,38 @@ class MidiFile(prebase.ProtoM21Object):
         '''
         self.file = fileLike
 
-    def _reprInternal(self):
+    def _reprInternal(self) -> str:
         lenTracks = len(self.tracks)
         plural = 's' if lenTracks != 1 else ''
         return f'{lenTracks} track{plural}'
 
-    def close(self):
+    def close(self) -> None:
         '''
         Close the file.
         '''
+        if not self.file:
+            # nothing to do
+            return
+
         self.file.close()
 
-    def read(self):
+    def read(self) -> None:
         '''
         Read and parse MIDI data stored in a file.
         '''
+        if not self.file:
+            raise TypeError('No file is open.')
+
         self.readstr(self.file.read())
 
-    def readstr(self, midiBytes):
+    def readstr(self, midiBytes: bytes) -> None:
         '''
         Read and parse MIDI data as a bytes, putting the
         data in `.ticksPerQuarterNote` and a list of
         `MidiTrack` objects in the attribute `.tracks`.
 
-        The name readstr is a carryover from Python 2.  It works on bytes objects, not strings
+        The name readstr is a carryover from Python 2.
+        It works on bytes objects, not strings
         '''
         if not midiBytes[:4] == b'MThd':
             raise MidiException(f'badly formatted midi bytes, got: {midiBytes[:20]!r}')
@@ -1605,10 +1874,13 @@ class MidiFile(prebase.ProtoM21Object):
             midiBytes = trk.read(midiBytes)  # pass all the remaining bytes, reassigning
             self.tracks.append(trk)
 
-    def write(self):
+    def write(self) -> None:
         '''
         Write MIDI data as a file to the file opened with `.open()`.
         '''
+        if not self.file:
+            raise TypeError('No file is open.')
+
         ws = self.writestr()
         self.file.write(ws)
 
diff --git a/music21/midi/realtime.py b/music21/midi/realtime.py
index 63911d26c..9fac020c9 100644
--- a/music21/midi/realtime.py
+++ b/music21/midi/realtime.py
@@ -87,6 +87,9 @@ class StreamPlayer:  # pragma: no cover
             # noinspection PyPackageRequirements
             import pygame  # type: ignore
             self.pygame = pygame
+            # noinspection PyPackageRequirements
+            import pygame.exceptions    # type: ignore
+            self.pygame_exceptions = pygame.exceptions  # type: ignore  # pylint: disable=no-member
         except ImportError:
             raise StreamPlayerException('StreamPlayer requires pygame.  Install first')
         if self.mixerInitialized is False or reinitMixer:
@@ -149,9 +152,11 @@ class StreamPlayer:  # pragma: no cover
         pygameClock = self.pygame.time.Clock()
         try:
             self.pygame.mixer.music.load(stringIOFile)
-        except self.pygame.error:
+        except self.pygame_exceptions.PygameError as pge:
             raise StreamPlayerException(
-                f'Could not play music file {stringIOFile} because: {self.pygame.get_error()}')
+                f'Could not play music file {stringIOFile} because: '
+                + f'{self.pygame_exceptions.get_error()}'
+            ) from pge
         self.pygame.mixer.music.play()
         if not blocked:
             return
diff --git a/music21/midi/translate.py b/music21/midi/translate.py
index 5ddaa6b5a..d5991c860 100644
--- a/music21/midi/translate.py
+++ b/music21/midi/translate.py
@@ -40,7 +40,7 @@ from music21 import tempo
 
 from music21.midi.base import (
     MidiTrack, DeltaTime, MidiFile, MidiEvent,
-    ChannelVoiceMessages, MetaEvents, getNumbersAsList, putNumbersAsList, getNumber, putNumber,
+    ChannelVoiceMessages, MetaEvents, putNumbersAsList, getNumber, putNumber,
 )
 from music21.midi.percussion import MIDIPercussionException, PercussionMapper
 
@@ -187,25 +187,35 @@ def ticksToDuration(ticks,
 
 
 def getStartEvents(
-    mt: MidiTrack|None = None,
+    midiTrack: MidiTrack|None = None,
     channel: int = 1,
     instrumentObj: instrument.Instrument|None = None,
+    *,
+    encoding_type: str = 'utf-8',
 ) -> list[DeltaTime|MidiEvent]:
     '''
     Returns a list of midi.MidiEvent objects found at the beginning of a track.
 
     A MidiTrack reference can be provided via the `mt` parameter.
 
-    >>> midi.translate.getStartEvents()
-    [<music21.midi.DeltaTime (empty) track=None, channel=1>,
-     <music21.midi.MidiEvent SEQUENCE_TRACK_NAME, track=None, channel=1, data=b''>]
+    >>> startEvents = midi.translate.getStartEvents()
+    >>> startEvents
+    [<music21.midi.DeltaTime (empty) track=None>,
+     <music21.midi.MidiEvent SEQUENCE_TRACK_NAME, track=None, data=b''>]
+
+    (Music21 DeltaTime events get assigned to the given channel, but this is not written out)
+
+    >>> startEvents[0].channel
+    1
 
-    >>> midi.translate.getStartEvents(channel=2, instrumentObj=instrument.Harpsichord())
-    [<music21.midi.DeltaTime (empty) track=None, channel=2>,
-     <music21.midi.MidiEvent SEQUENCE_TRACK_NAME, track=None, channel=2, data=b'Harpsichord'>,
-     <music21.midi.DeltaTime (empty) track=None, channel=2>,
-     <music21.midi.MidiEvent PROGRAM_CHANGE, track=None, channel=2, data=6>]
+    >>> mt = midi.MidiTrack(3)
+    >>> midi.translate.getStartEvents(mt, channel=2, instrumentObj=instrument.Harpsichord())
+    [<music21.midi.DeltaTime (empty) track=3>,
+     <music21.midi.MidiEvent SEQUENCE_TRACK_NAME, track=3, data=b'Harpsichord'>,
+     <music21.midi.DeltaTime (empty) track=3>,
+     <music21.midi.MidiEvent PROGRAM_CHANGE, track=3, channel=2, data=6>]
 
+    Bug fixed in v9.6 -- the program change and delta time are written to the given track.
     '''
     events: list[DeltaTime|MidiEvent] = []
     if isinstance(instrumentObj, Conductor):
@@ -215,12 +225,12 @@ def getStartEvents(
     else:
         partName = instrumentObj.bestName()
 
-    dt = DeltaTime(mt, channel=channel)
+    dt = DeltaTime(midiTrack, channel=channel)
     events.append(dt)
 
-    me = MidiEvent(mt, channel=channel)
+    me = MidiEvent(midiTrack, channel=channel)
     me.type = MetaEvents.SEQUENCE_TRACK_NAME
-    me.data = partName
+    me.data = partName.encode(encoding_type, 'ignore')
     events.append(me)
 
     # additional allocation of instruments may happen elsewhere
@@ -228,31 +238,41 @@ def getStartEvents(
     # however, this assures that the program change happens before
     # the clearing of the pitch bend data
     if instrumentObj is not None and instrumentObj.midiProgram is not None:
-        sub = instrumentToMidiEvents(instrumentObj, includeDeltaTime=True,
+        sub = instrumentToMidiEvents(instrumentObj,
+                                     includeDeltaTime=True,
+                                     midiTrack=midiTrack,
                                      channel=channel)
         events += sub
 
     return events
 
 
-def getEndEvents(mt=None, channel=1):
+def getEndEvents(
+    midiTrack: MidiTrack|None = None,
+    channel: int = 1
+) -> list[MidiEvent|DeltaTime]:
     '''
     Returns a list of midi.MidiEvent objects found at the end of a track.
 
-    >>> midi.translate.getEndEvents(channel=2)
-    [<music21.midi.DeltaTime t=10080, track=None, channel=2>,
-     <music21.midi.MidiEvent END_OF_TRACK, track=None, channel=2, data=b''>]
+    Note that the channel is basically ignored as no end events are channel
+    specific.  (Attribute will be removed in v10)
+
+    >>> midi.translate.getEndEvents()
+    [<music21.midi.DeltaTime t=10080, track=None>,
+     <music21.midi.MidiEvent END_OF_TRACK, track=None, data=b''>]
     '''
-    events = []
+    events: list[MidiEvent|DeltaTime] = []
 
-    dt = DeltaTime(track=mt, channel=channel)
+    dt = DeltaTime(track=midiTrack, channel=channel)
     dt.time = defaults.ticksAtStart
     events.append(dt)
 
-    me = MidiEvent(track=mt)
-    me.type = MetaEvents.END_OF_TRACK
-    me.channel = channel
-    me.data = ''  # must set data to empty string
+    me = MidiEvent(
+        track=midiTrack,
+        type=MetaEvents.END_OF_TRACK,
+        channel=channel,
+    )
+    me.data = b''  # must set data to empty bytes
     events.append(me)
 
     return events
@@ -262,7 +282,7 @@ def getEndEvents(mt=None, channel=1):
 
 
 def music21ObjectToMidiFile(
-    music21Object,
+    music21Object: base.Music21Object,
     *,
     addStartDelay=False,
 ) -> MidiFile:
@@ -272,12 +292,12 @@ def music21ObjectToMidiFile(
     not to change activeSites, etc.) and calls streamToMidiFile on
     that object.
     '''
-    classes = music21Object.classes
-    if 'Stream' in classes:
+    if isinstance(music21Object, stream.Stream):
         if music21Object.atSoundingPitch is False:
             music21Object = music21Object.toSoundingPitch()
 
-        return streamToMidiFile(music21Object, addStartDelay=addStartDelay)
+        return streamToMidiFile(t.cast(stream.Stream, music21Object),
+                                addStartDelay=addStartDelay)
     else:
         m21ObjectCopy = copy.deepcopy(music21Object)
         s: stream.Stream = stream.Stream()
@@ -455,17 +475,17 @@ def noteToMidiEvents(
     >>> n1 = note.Note('C#4')
     >>> eventList = midi.translate.noteToMidiEvents(n1)
     >>> eventList
-    [<music21.midi.DeltaTime (empty) track=None, channel=1>,
+    [<music21.midi.DeltaTime (empty) track=None>,
      <music21.midi.MidiEvent NOTE_ON, track=None, channel=1, pitch=61, velocity=90>,
-     <music21.midi.DeltaTime t=10080, track=None, channel=1>,
+     <music21.midi.DeltaTime t=10080, track=None>,
      <music21.midi.MidiEvent NOTE_OFF, track=None, channel=1, pitch=61, velocity=0>]
 
     >>> n1.duration.quarterLength = 2.5
     >>> eventList = midi.translate.noteToMidiEvents(n1)
     >>> eventList
-    [<music21.midi.DeltaTime (empty) track=None, channel=1>,
+    [<music21.midi.DeltaTime (empty) track=None>,
      <music21.midi.MidiEvent NOTE_ON, track=None, channel=1, pitch=61, velocity=90>,
-     <music21.midi.DeltaTime t=25200, track=None, channel=1>,
+     <music21.midi.DeltaTime t=25200, track=None>,
      <music21.midi.MidiEvent NOTE_OFF, track=None, channel=1, pitch=61, velocity=0>]
 
     Omitting DeltaTimes:
@@ -686,17 +706,17 @@ def chordToMidiEvents(
     >>> c.volume.velocityIsRelative = False
     >>> eventList = midi.translate.chordToMidiEvents(c)
     >>> eventList
-    [<music21.midi.DeltaTime (empty) track=None, channel=None>,
+    [<music21.midi.DeltaTime (empty) track=None>,
      <music21.midi.MidiEvent NOTE_ON, track=None, channel=1, pitch=48, velocity=90>,
-     <music21.midi.DeltaTime (empty) track=None, channel=None>,
+     <music21.midi.DeltaTime (empty) track=None>,
      <music21.midi.MidiEvent NOTE_ON, track=None, channel=1, pitch=68, velocity=90>,
-     <music21.midi.DeltaTime (empty) track=None, channel=None>,
+     <music21.midi.DeltaTime (empty) track=None>,
      <music21.midi.MidiEvent NOTE_ON, track=None, channel=1, pitch=83, velocity=90>,
-     <music21.midi.DeltaTime t=10080, track=None, channel=None>,
+     <music21.midi.DeltaTime t=10080, track=None>,
      <music21.midi.MidiEvent NOTE_OFF, track=None, channel=1, pitch=48, velocity=0>,
-     <music21.midi.DeltaTime (empty) track=None, channel=None>,
+     <music21.midi.DeltaTime (empty) track=None>,
      <music21.midi.MidiEvent NOTE_OFF, track=None, channel=1, pitch=68, velocity=0>,
-     <music21.midi.DeltaTime (empty) track=None, channel=None>,
+     <music21.midi.DeltaTime (empty) track=None>,
      <music21.midi.MidiEvent NOTE_OFF, track=None, channel=1, pitch=83, velocity=0>]
 
     * Changed in v7: made keyword-only.
@@ -867,14 +887,17 @@ def midiEventsToInstrument(
             decoded = event.data.decode('utf-8').split('\x00')[0]
             decoded = decoded.strip()
             i = instrument.fromString(decoded)
-        elif event.channel == 10:
+        elif event.channel == 10 and isinstance(event.data, int):
             # Can only get correct instruments from reading NOTE_ON
             i = instrument.UnpitchedPercussion()
             i.midiProgram = event.data
-        else:
+        elif isinstance(event.data, int):
             i = instrument.instrumentFromMidiProgram(event.data)
             # Instrument.midiProgram and event.data are both 0-indexed
             i.midiProgram = event.data
+        else:
+            raise instrument.InstrumentException(f'Could not get an instrument from {event}')
+
     except UnicodeDecodeError:
         warnings.warn(
             f'Unable to determine instrument from {event}; getting generic Instrument',
@@ -952,7 +975,7 @@ def midiEventsToTimeSignature(eventList):
         event = eventList[1]
 
     # time signature is 4 byte encoding
-    post = getNumbersAsList(event.data)
+    post = list(event.data)
 
     n = post[0]
     d = pow(2, post[1])
@@ -960,23 +983,36 @@ def midiEventsToTimeSignature(eventList):
     return ts
 
 
-def timeSignatureToMidiEvents(ts, includeDeltaTime=True):
+def timeSignatureToMidiEvents(ts, includeDeltaTime=True) -> list[DeltaTime|MidiEvent]:
     # noinspection PyShadowingNames
     '''
     Translate a :class:`~music21.meter.TimeSignature` to a pair of events: a DeltaTime and
     a MidiEvent TIME_SIGNATURE.
 
-    Returns a two-element list
+    Returns a two-element list unless there is an error.
 
     >>> ts = meter.TimeSignature('5/4')
     >>> eventList = midi.translate.timeSignatureToMidiEvents(ts)
     >>> eventList[0]
-    <music21.midi.DeltaTime (empty) track=None, channel=None>
+    <music21.midi.DeltaTime (empty) track=None>
     >>> eventList[1]
-    <music21.midi.MidiEvent TIME_SIGNATURE, track=None, channel=1, data=b'\\x05\\x02\\x18\\x08'>
+    <music21.midi.MidiEvent TIME_SIGNATURE, track=None, data=b'\\x05\\x02\\x18\\x08'>
+
+    Note that time signatures with numerators above 255 cannot be stored in MIDI.
+    (A feature?) They will not be stored and an empty list will be returned.
+    A warning will be issued.
+
+    >>> ts = meter.TimeSignature('267/32')  # found on music21 list
+    >>> import warnings  #_DOCS_HIDE
+    >>> with warnings.catch_warnings(): #_DOCS_HIDE
+    ...      warnings.simplefilter('ignore') #_DOCS_HIDE
+    ...      out = midi.translate.timeSignatureToMidiEvents(ts) #_DOCS_HIDE
+    >>> #_DOCS_SHOW out = midi.translate.timeSignatureToMidiEvents(ts)
+    >>> out
+    []
     '''
     mt = None  # use a midi track set to None
-    eventList = []
+    eventList: list[MidiEvent|DeltaTime] = []
     if includeDeltaTime:
         dt = DeltaTime(track=mt)
         # dt.time set to zero; will be shifted later as necessary
@@ -984,6 +1020,13 @@ def timeSignatureToMidiEvents(ts, includeDeltaTime=True):
         eventList.append(dt)
 
     n = ts.numerator
+    if n > 255:
+        warnings.warn(
+            f'TimeSignature with numerator > 255 cannot be stored in MIDI. Ignoring {ts}',
+            TranslateWarning
+        )
+        return []
+
     # need log base 2 to solve for exponent of 2
     # 1 is 0, 2 is 1, 4 is 2, 16 is 4, etc
     d = int(math.log2(ts.denominator))
@@ -1018,7 +1061,7 @@ def midiEventsToKey(eventList) -> key.Key:
     >>> me2.data = midi.putNumbersAsList([-2, 1])  # g minor
     >>> me2.data
     b'\xfe\x01'
-    >>> midi.getNumbersAsList(me2.data)
+    >>> list(me2.data)
     [254, 1]
     >>> ks = midi.translate.midiEventsToKey(me2)
     >>> ks
@@ -1037,7 +1080,7 @@ def midiEventsToKey(eventList) -> key.Key:
         event = eventList
     else:  # get the second event; first is delta time
         event = eventList[1]
-    post = getNumbersAsList(event.data)
+    post = list(event.data)
 
     # first value is number of sharp, or neg for number of flat
     if post[0] > 12:
@@ -1074,15 +1117,23 @@ def keySignatureToMidiEvents(
     <music21.key.KeySignature of 2 sharps>
     >>> eventList = midi.translate.keySignatureToMidiEvents(ks)
     >>> eventList
-    [<music21.midi.DeltaTime (empty) track=None, channel=None>,
-     <music21.midi.MidiEvent KEY_SIGNATURE, track=None, channel=1, data=b'\x02\x00'>]
+    [<music21.midi.DeltaTime (empty) track=None>,
+     <music21.midi.MidiEvent KEY_SIGNATURE, track=None, data=b'\x02\x00'>]
+
+    Note that MIDI Key Signatures are connected to tracks but not to channels.
+
+    MIDI Key Signatures store sharps or flats as the first number, and mode (0 = major
+    1 = minor) as the second number.
+
+    Flats are encoded as offsets at 0xff (=1 flat) or below (0xfe = 2 flats ... etc.,
+    through 0xfb = 5 flats, below).
 
     >>> k = key.Key('b-')
     >>> k
     <music21.key.Key of b- minor>
     >>> eventList = midi.translate.keySignatureToMidiEvents(k, includeDeltaTime=False)
     >>> eventList
-    [<music21.midi.MidiEvent KEY_SIGNATURE, track=None, channel=1, data=b'\xfb\x01'>]
+    [<music21.midi.MidiEvent KEY_SIGNATURE, track=None, data=b'\xfb\x01'>]
     '''
     mt = None  # use a midi track set to None
     eventList: list[DeltaTime|MidiEvent] = []
@@ -1091,14 +1142,22 @@ def keySignatureToMidiEvents(
         # leave dt.time set to zero; will be shifted later as necessary
         # add to track events
         eventList.append(dt)
+
+    # negative = flats.
     sharpCount = ks.sharps
+
     if isinstance(ks, key.Key) and ks.mode == 'minor':
         mode = 1
     else:  # major or None; must define one
         mode = 0
-    me = MidiEvent(track=mt)
-    me.type = MetaEvents.KEY_SIGNATURE
-    me.channel = 1
+
+    me = MidiEvent(
+        track=mt,
+        type=MetaEvents.KEY_SIGNATURE,
+    )
+
+    # this is, as far as I'm aware, the one place where we use the
+    # negative number aspect of putNumbersAsList.
     me.data = putNumbersAsList([sharpCount, mode])
     eventList.append(me)
     return eventList
@@ -1117,7 +1176,7 @@ def midiEventsToTempo(eventList):
     # get microseconds per quarter
     mspq = getNumber(event.data, 3)[0]  # first data is number
     bpm = round(60_000_000 / mspq, 2)
-    # post = getNumbersAsList(event.data)
+    # post = list(event.data)
     # environLocal.printDebug(['midiEventsToTempo, got bpm', bpm])
     mm = tempo.MetronomeMark(number=bpm)
     return mm
@@ -1140,11 +1199,11 @@ def tempoToMidiEvents(
     2
 
     >>> events[0]
-    <music21.midi.DeltaTime (empty) track=None, channel=None>
+    <music21.midi.DeltaTime (empty) track=None>
 
     >>> evt1 = events[1]
     >>> evt1
-    <music21.midi.MidiEvent SET_TEMPO, track=None, channel=1, data=b'\n,+'>
+    <music21.midi.MidiEvent SET_TEMPO, track=None, data=b'\n,+'>
     >>> evt1.data
     b'\n,+'
     >>> microSecondsPerQuarterNote = midi.getNumber(evt1.data, len(evt1.data))[0]
@@ -1453,8 +1512,8 @@ def assignPacketsToChannels(
                 if p['centShift']:
                     # do not set channel, as already set
                     me = MidiEvent(p['midiEvent'].track,
-                                              type=ChannelVoiceMessages.PITCH_BEND,
-                                              channel=p['midiEvent'].channel)
+                                   type=ChannelVoiceMessages.PITCH_BEND,
+                                   channel=p['midiEvent'].channel)
                     # note off stores a note on for each pitch; do not invert, simply
                     # set to zero
                     me.setPitchBend(0)
@@ -1551,8 +1610,8 @@ def assignPacketsToChannels(
         if centShift:
             # add pitch bend
             me = MidiEvent(p['midiEvent'].track,
-                                      type=ChannelVoiceMessages.PITCH_BEND,
-                                      channel=ch)
+                           type=ChannelVoiceMessages.PITCH_BEND,
+                           channel=ch)
             me.setPitchBend(centShift)
             pBendStart = getPacketFromMidiEvent(
                 trackId=p['trackId'],
@@ -1599,8 +1658,8 @@ def assignPacketsToChannels(
         ch = initTrackIdToChannelMap[trackId]
         # use None for track; will get updated later
         me = MidiEvent(track=trackId,
-                                  type=ChannelVoiceMessages.PITCH_BEND,
-                                  channel=ch)
+                       type=ChannelVoiceMessages.PITCH_BEND,
+                       channel=ch)
         me.setPitchBend(0)
         pBendEnd = getPacketFromMidiEvent(
             trackId=trackId,
@@ -1817,8 +1876,7 @@ def getMetaEvents(
 ) -> list[tuple[int, base.Music21Object]]:
     metaEvents: list[tuple[int, base.Music21Object]] = []  # store pairs of abs time, m21 object
     last_program: int = -1
-    for eventTuple in events:
-        timeEvent, e = eventTuple
+    for timeEvent, e in events:
         metaObj = None
         if e.type == MetaEvents.TIME_SIGNATURE:
             # time signature should be 4 bytes
@@ -1834,10 +1892,10 @@ def getMetaEvents(
             if last_program != -1:
                 # Only update if we have had an initial PROGRAM_CHANGE
                 metaObj.midiProgram = last_program
-        elif e.type == ChannelVoiceMessages.PROGRAM_CHANGE:
+        elif e.type == ChannelVoiceMessages.PROGRAM_CHANGE and isinstance(e.parameter1, int):
             # midiEventsToInstrument() WILL set the program on the instance
             metaObj = midiEventsToInstrument(e)
-            last_program = e.data
+            last_program = e.parameter1
         elif e.type == MetaEvents.MIDI_PORT:
             pass
         else:
@@ -2386,7 +2444,7 @@ def packetStorageFromSubstreamList(
          'rawPackets': [{'centShift': None,
                          'duration': 0,
                          'lastInstrument': <music21.instrument.Conductor 'Conductor'>,
-                         'midiEvent': <music21.midi.MidiEvent SET_TEMPO, ... channel=1, ...>,
+                         'midiEvent': <music21.midi.MidiEvent SET_TEMPO, ...>,
                          'obj': <music21.tempo.MetronomeMark Quarter=100>,
                          'offset': 0,
                          'trackId': 0},


Test patch:
diff --git a/music21/midi/tests.py b/music21/midi/tests.py
index 0569c056d..b8904bc0c 100644
--- a/music21/midi/tests.py
+++ b/music21/midi/tests.py
@@ -460,19 +460,16 @@ class Test(unittest.TestCase):
         # mtAlt = streamHierarchyToMidiTracks(s.getElementsByClass(meter.TimeSignature).stream())[0]
         conductorEvents = repr(mt.events)
 
-        match = r'''[<music21.midi.DeltaTime (empty) track=0, channel=None>,
-        <music21.midi.MidiEvent SET_TEMPO, track=0, channel=None, data=b'\x07\xa1 '>,
-        <music21.midi.DeltaTime (empty) track=0, channel=None>,
-        <music21.midi.MidiEvent TIME_SIGNATURE, track=0, channel=None,
-            data=b'\x03\x02\x18\x08'>,
-        <music21.midi.DeltaTime t=30240, track=0, channel=None>,
-        <music21.midi.MidiEvent TIME_SIGNATURE, track=0, channel=None,
-            data=b'\x05\x02\x18\x08'>,
-        <music21.midi.DeltaTime t=50400, track=0, channel=None>,
-        <music21.midi.MidiEvent TIME_SIGNATURE, track=0, channel=None,
-            data=b'\x02\x02\x18\x08'>,
-        <music21.midi.DeltaTime t=10080, track=0, channel=None>,
-        <music21.midi.MidiEvent END_OF_TRACK, track=0, channel=None, data=b''>]'''
+        match = r'''[<music21.midi.DeltaTime (empty) track=0>,
+        <music21.midi.MidiEvent SET_TEMPO, track=0, data=b'\x07\xa1 '>,
+        <music21.midi.DeltaTime (empty) track=0>,
+        <music21.midi.MidiEvent TIME_SIGNATURE, track=0, data=b'\x03\x02\x18\x08'>,
+        <music21.midi.DeltaTime t=30240, track=0>,
+        <music21.midi.MidiEvent TIME_SIGNATURE, track=0, data=b'\x05\x02\x18\x08'>,
+        <music21.midi.DeltaTime t=50400, track=0>,
+        <music21.midi.MidiEvent TIME_SIGNATURE, track=0, data=b'\x02\x02\x18\x08'>,
+        <music21.midi.DeltaTime t=10080, track=0>,
+        <music21.midi.MidiEvent END_OF_TRACK, track=0, data=b''>]'''
 
         self.assertTrue(common.whitespaceEqual(conductorEvents, match), conductorEvents)
 
@@ -609,11 +606,11 @@ class Test(unittest.TestCase):
 
         # first note-on is not delayed, even w anacrusis
         match = '''
-        [<music21.midi.DeltaTime (empty) track=1, channel=1>,
-         <music21.midi.MidiEvent SEQUENCE_TRACK_NAME, track=1, channel=1, data=b'Soprano'>,
-         <music21.midi.DeltaTime (empty) track=1, channel=1>,
+        [<music21.midi.DeltaTime (empty) track=1>,
+         <music21.midi.MidiEvent SEQUENCE_TRACK_NAME, track=1, data=b'Soprano'>,
+         <music21.midi.DeltaTime (empty) track=1>,
          <music21.midi.MidiEvent PITCH_BEND, track=1, channel=1, parameter1=0, parameter2=64>,
-         <music21.midi.DeltaTime (empty) track=1, channel=1>]'''
+         <music21.midi.DeltaTime (empty) track=1>]'''
 
         self.maxDiff = None
         found = str(mts.events[:5])
@@ -621,13 +618,13 @@ class Test(unittest.TestCase):
 
         # first note-on is not delayed, even w anacrusis
         match = '''
-        [<music21.midi.DeltaTime (empty) track=1, channel=1>,
-        <music21.midi.MidiEvent SEQUENCE_TRACK_NAME, track=1, channel=1, data=b'Alto'>,
-        <music21.midi.DeltaTime (empty) track=1, channel=1>,
+        [<music21.midi.DeltaTime (empty) track=1>,
+        <music21.midi.MidiEvent SEQUENCE_TRACK_NAME, track=1, data=b'Alto'>,
+        <music21.midi.DeltaTime (empty) track=1>,
         <music21.midi.MidiEvent PITCH_BEND, track=1, channel=1, parameter1=0, parameter2=64>,
-        <music21.midi.DeltaTime (empty) track=1, channel=1>,
+        <music21.midi.DeltaTime (empty) track=1>,
         <music21.midi.MidiEvent PROGRAM_CHANGE, track=1, channel=1, data=0>,
-        <music21.midi.DeltaTime (empty) track=1, channel=1>,
+        <music21.midi.DeltaTime (empty) track=1>,
         <music21.midi.MidiEvent NOTE_ON, track=1, channel=1, pitch=62, velocity=90>]'''
 
         alto = s.parts['#alto']
@@ -643,15 +640,15 @@ class Test(unittest.TestCase):
         self.assertEqual(len(mtList), 2)
 
         # it's the same as before
-        match = '''[<music21.midi.DeltaTime (empty) track=1, channel=1>,
-        <music21.midi.MidiEvent SEQUENCE_TRACK_NAME, track=1, channel=1, data=b'Soprano'>,
-        <music21.midi.DeltaTime (empty) track=1, channel=1>,
+        match = '''[<music21.midi.DeltaTime (empty) track=1>,
+        <music21.midi.MidiEvent SEQUENCE_TRACK_NAME, track=1, data=b'Soprano'>,
+        <music21.midi.DeltaTime (empty) track=1>,
         <music21.midi.MidiEvent PITCH_BEND, track=1, channel=1, parameter1=0, parameter2=64>,
-        <music21.midi.DeltaTime (empty) track=1, channel=1>,
+        <music21.midi.DeltaTime (empty) track=1>,
         <music21.midi.MidiEvent PROGRAM_CHANGE, track=1, channel=1, data=0>,
-        <music21.midi.DeltaTime (empty) track=1, channel=1>,
+        <music21.midi.DeltaTime (empty) track=1>,
         <music21.midi.MidiEvent NOTE_ON, track=1, channel=1, pitch=66, velocity=90>,
-        <music21.midi.DeltaTime t=5040, track=1, channel=1>,
+        <music21.midi.DeltaTime t=5040, track=1>,
         <music21.midi.MidiEvent NOTE_OFF, track=1, channel=1, pitch=66, velocity=0>]'''
         found = str(mtList[1].events[:10])
         self.assertTrue(common.whitespaceEqual(found, match), found)
@@ -712,22 +709,22 @@ class Test(unittest.TestCase):
 
         # it's the same as before
         match = '''[<music21.midi.MidiEvent NOTE_ON, track=1, channel=1, pitch=66, velocity=90>,
-        <music21.midi.DeltaTime (empty) track=1, channel=1>,
+        <music21.midi.DeltaTime (empty) track=1>,
         <music21.midi.MidiEvent NOTE_ON, track=1, channel=1, pitch=61, velocity=90>,
-        <music21.midi.DeltaTime (empty) track=1, channel=1>,
+        <music21.midi.DeltaTime (empty) track=1>,
         <music21.midi.MidiEvent NOTE_ON, track=1, channel=1, pitch=58, velocity=90>,
-        <music21.midi.DeltaTime (empty) track=1, channel=1>,
+        <music21.midi.DeltaTime (empty) track=1>,
         <music21.midi.MidiEvent NOTE_ON, track=1, channel=1, pitch=54, velocity=90>,
-        <music21.midi.DeltaTime t=10080, track=1, channel=1>,
+        <music21.midi.DeltaTime t=10080, track=1>,
         <music21.midi.MidiEvent NOTE_OFF, track=1, channel=1, pitch=66, velocity=0>,
-        <music21.midi.DeltaTime (empty) track=1, channel=1>,
+        <music21.midi.DeltaTime (empty) track=1>,
         <music21.midi.MidiEvent NOTE_OFF, track=1, channel=1, pitch=61, velocity=0>,
-        <music21.midi.DeltaTime (empty) track=1, channel=1>,
+        <music21.midi.DeltaTime (empty) track=1>,
         <music21.midi.MidiEvent NOTE_OFF, track=1, channel=1, pitch=58, velocity=0>,
-        <music21.midi.DeltaTime (empty) track=1, channel=1>,
+        <music21.midi.DeltaTime (empty) track=1>,
         <music21.midi.MidiEvent NOTE_OFF, track=1, channel=1, pitch=54, velocity=0>,
-        <music21.midi.DeltaTime t=10080, track=1, channel=1>,
-        <music21.midi.MidiEvent END_OF_TRACK, track=1, channel=1, data=b''>]'''
+        <music21.midi.DeltaTime t=10080, track=1>,
+        <music21.midi.MidiEvent END_OF_TRACK, track=1, data=b''>]'''
 
         results = str(mtList[1].events[-17:])
         self.assertTrue(common.whitespaceEqual(results, match), results)