diff --git a/music21/midi/base.py b/music21/midi/base.py index 4198decf2..c364448f3 100644 --- a/music21/midi/base.py +++ b/music21/midi/base.py @@ -44,6 +44,9 @@ from typing import overload import typing as t +if t.TYPE_CHECKING: + import pathlib + from music21.common.enums import ContainsEnum from music21 import defaults from music21 import environment @@ -491,7 +494,7 @@ def __init__(self, track: MidiTrack|None = None, type: MidiEventTypes = MetaEvents.UNKNOWN, time: int = 0, - channel: int = 1): + channel: int = 1) -> None: self.track: MidiTrack|None = track # a MidiTrack object self.type: MidiEventTypes = type self.time: int = time @@ -585,7 +588,7 @@ def pitch(self) -> int|None: return None @pitch.setter - def pitch(self, value: int|None): + def pitch(self, value: int|None) -> None: self.parameter1 = value @property @@ -595,7 +598,7 @@ def velocity(self) -> int|None: return self.parameter2 @velocity.setter - def velocity(self, value: int|None): + def velocity(self, value: int|None) -> None: self.parameter2 = value # store generic data in parameter 1 @@ -614,7 +617,7 @@ def data(self) -> int|bytes|None: return self.parameter1 @data.setter - def data(self, value: int|str|bytes|bool|None): + def data(self, value: int|str|bytes|bool|None) -> None: if value is not None and not isinstance(value, bytes): if isinstance(value, str): value = value.encode('utf-8') @@ -635,7 +638,7 @@ def isChannelEvent(self) -> bool: ''' return self.type in ChannelVoiceMessages or self.type in ChannelModeMessages - def setPitchBend(self, cents: int|float, bendRange=2) -> None: + def setPitchBend(self, cents: int|float, bendRange: int = 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. @@ -1244,7 +1247,7 @@ def __init__( track: MidiTrack|None = None, time: int = 0, channel: int = 1, - ): + ) -> None: super().__init__(track, time=time, channel=channel) self.type = 'DeltaTime' @@ -1388,7 +1391,7 @@ class MidiTrack(prebase.ProtoM21Object): ''' headerId = b'MTrk' - def __init__(self, index: int = 0): + def __init__(self, index: int = 0) -> None: self.index = index self.events: list[MidiEvent] = [] # or DeltaTime subclass self.data = b'' @@ -1550,7 +1553,7 @@ def getBytes(self) -> bytes: bytes_out = b''.join(midiBytes) return self.headerId + putNumber(len(bytes_out), 4) + bytes_out - def _reprInternal(self): + def _reprInternal(self) -> str: r = f'{self.index} -- {len(self.events)} events' return r @@ -1713,7 +1716,7 @@ def __init__(self) -> None: self.ticksPerQuarterNote: int = defaults.ticksPerQuarter self.ticksPerSecond: int|None = None - def open(self, filename, attrib='rb') -> None: + def open(self, filename: str|pathlib.Path, attrib: str = 'rb') -> None: ''' Open a MIDI file path for reading or writing. diff --git a/music21/midi/percussion.py b/music21/midi/percussion.py index 6e70f85e4..3cb11ec5e 100644 --- a/music21/midi/percussion.py +++ b/music21/midi/percussion.py @@ -10,6 +10,7 @@ # ------------------------------------------------------------------------------ from __future__ import annotations +import typing as t import unittest from music21 import pitch @@ -94,7 +95,7 @@ class PercussionMapper: # formerly at: # https://www.midi.org/specifications/item/gm-level-1-sound-set - def midiPitchToInstrument(self, midiPitch): + def midiPitchToInstrument(self, midiPitch: int|pitch.Pitch) -> instrument.Instrument: ''' Takes a pitch.Pitch object or int ranging from 0-127 and returns the corresponding instrument in the GM Percussion Map. @@ -157,13 +158,15 @@ def midiPitchToInstrument(self, midiPitch): midiInstrumentObject = midiInstrument() if (midiInstrumentObject.inGMPercMap is True and hasattr(midiInstrumentObject, '_percMapPitchToModifier')): - if midiNumber in midiInstrumentObject._percMapPitchToModifier: - modifier = midiInstrumentObject._percMapPitchToModifier[midiNumber] - midiInstrumentObject.modifier = modifier + midiUnpitchedPercussion = t.cast( + instrument.UnpitchedPercussion, midiInstrumentObject) + if midiNumber in midiUnpitchedPercussion._percMapPitchToModifier: + modifier = midiUnpitchedPercussion._percMapPitchToModifier[midiNumber] + midiUnpitchedPercussion.modifier = modifier return midiInstrumentObject - def midiInstrumentToPitch(self, midiInstrument): + def midiInstrumentToPitch(self, midiInstrument: instrument.Instrument) -> pitch.Pitch: ''' Takes an instrument.Instrument object and returns a pitch object with the corresponding 1-indexed MIDI note, according to the GM Percussion Map. @@ -191,7 +194,8 @@ def midiInstrumentToPitch(self, midiInstrument): ''' if not hasattr(midiInstrument, 'inGMPercMap') or midiInstrument.inGMPercMap is False: raise MIDIPercussionException(f'{midiInstrument!r} is not in the GM Percussion Map!') - midiPitch = midiInstrument.percMapPitch + percInstrument = t.cast(instrument.Percussion, midiInstrument) + midiPitch = t.cast(int, percInstrument.percMapPitch) pitchObject = pitch.Pitch() pitchObject.midi = midiPitch return pitchObject @@ -201,7 +205,7 @@ def midiInstrumentToPitch(self, midiInstrument): class Test(unittest.TestCase): - def testCopyAndDeepcopy(self): + def testCopyAndDeepcopy(self) -> None: from music21.test.commonTest import testCopyAll testCopyAll(self, globals()) diff --git a/music21/midi/realtime.py b/music21/midi/realtime.py index 206ec2e81..d84247bf6 100644 --- a/music21/midi/realtime.py +++ b/music21/midi/realtime.py @@ -21,8 +21,10 @@ ''' from __future__ import annotations +from collections.abc import Callable from importlib.util import find_spec from io import BytesIO +import typing as t import unittest from music21 import defaults @@ -80,7 +82,7 @@ def __init__( mixerBitSize: int = -16, mixerChannels: int = 2, mixerBuffer: int = 1024, - ): + ) -> None: try: # noinspection PyPackageRequirements import pygame # type: ignore @@ -93,14 +95,14 @@ def __init__( self.streamIn = streamIn def play(self, - busyFunction=None, - busyArgs=None, - endFunction=None, - endArgs=None, - busyWaitMilliseconds=50, + busyFunction: Callable[[t.Any], t.Any]|None = None, + busyArgs: t.Any = None, + endFunction: Callable[[t.Any], t.Any]|None = None, + endArgs: t.Any = None, + busyWaitMilliseconds: int = 50, *, - playForMilliseconds=float('inf'), - blocked=True): + playForMilliseconds: float = float('inf'), + blocked: bool = True) -> None: ''' busyFunction is a function that is called with busyArgs when the music is busy every busyWaitMilliseconds. @@ -123,15 +125,21 @@ def play(self, playForMilliseconds=playForMilliseconds, blocked=blocked) - def getStringOrBytesIOFile(self): + def getStringOrBytesIOFile(self) -> BytesIO: streamMidiFile = midiTranslate.streamToMidiFile(self.streamIn) streamMidiWritten = streamMidiFile.writestr() return BytesIO(streamMidiWritten) - def playStringIOFile(self, stringIOFile, busyFunction=None, busyArgs=None, - endFunction=None, endArgs=None, busyWaitMilliseconds=50, + def playStringIOFile(self, + stringIOFile: BytesIO, + busyFunction: Callable[[t.Any], t.Any]|None = None, + busyArgs: t.Any = None, + endFunction: Callable[[t.Any], t.Any]|None = None, + endArgs: t.Any = None, + busyWaitMilliseconds: int = 50, *, - playForMilliseconds=float('inf'), blocked=True): + playForMilliseconds: float = float('inf'), + blocked: bool = True) -> None: ''' busyFunction is a function that is called with busyArgs when the music is busy every busyWaitMilliseconds. @@ -167,7 +175,7 @@ def playStringIOFile(self, stringIOFile, busyFunction=None, busyArgs=None, if endFunction is not None: endFunction(endArgs) - def stop(self): + def stop(self) -> None: self.pygame.mixer.music.stop() @@ -183,7 +191,7 @@ class TestExternal(unittest.TestCase): # pragma: no cover pygame_installed = False @unittest.skipUnless(pygame_installed, 'pygame is not installed') - def testBachDetune(self): + def testBachDetune(self) -> None: from music21 import corpus import random b = corpus.parse('bwv66.6') @@ -232,7 +240,7 @@ class Mock: sp = StreamPlayer(b) sp.play(busyFunction=busyCounter, busyArgs=[timeCounter], busyWaitMilliseconds=500) - def x_testPlayOneMeasureAtATime(self): + def x_testPlayOneMeasureAtATime(self) -> None: from music21 import corpus defaults.ticksAtStart = 0 b = corpus.parse('bwv66.6') diff --git a/music21/midi/translate.py b/music21/midi/translate.py index 82a7b6dec..867f9422e 100644 --- a/music21/midi/translate.py +++ b/music21/midi/translate.py @@ -21,7 +21,6 @@ import warnings from music21 import chord -from music21 import common from music21.common.numberTools import opFrac from music21 import defaults from music21 import duration @@ -48,6 +47,7 @@ if t.TYPE_CHECKING: + import pathlib from music21 import base from music21.common.types import OffsetQLIn @@ -300,8 +300,8 @@ def getEndEvents( def music21ObjectToMidiFile( music21Object: base.Music21Object, *, - addStartDelay=False, - addEndDelay=True, + addStartDelay: bool = False, + addEndDelay: bool = True, encoding: str = 'utf-8', ) -> MidiFile: ''' @@ -450,8 +450,9 @@ def midiEventsToNote( if isinstance(nr, note.Note): nr.pitch.midi = midiOnEvent.pitch elif isinstance(nr, note.Unpitched): + onPitch = t.cast(int, midiOnEvent.pitch) try: - i = PERCUSSION_MAPPER.midiPitchToInstrument(midiOnEvent.pitch) + i = PERCUSSION_MAPPER.midiPitchToInstrument(onPitch) except MIDIPercussionException: i = instrument.UnpitchedPercussion() nr.storedInstrument = i @@ -845,7 +846,7 @@ def instrumentToMidiEvents( includeDeltaTime: bool = True, midiTrack: MidiTrack|None = None, channel: int = 1, -): +) -> list[MidiEvent|DeltaTime]: ''' Converts a :class:`~music21.instrument.Instrument` object to a list of MidiEvents @@ -962,7 +963,9 @@ def midiEventToInstrument( return i -def midiEventsToTimeSignature(eventList): +def midiEventsToTimeSignature( + eventList: MidiEvent|Sequence[MidiEvent] +) -> meter.TimeSignature: # noinspection PyShadowingNames ''' Convert a single MIDI event into a music21 TimeSignature object. @@ -1003,13 +1006,14 @@ def midiEventsToTimeSignature(eventList): # Time Signature Event should appear in the first track chunk (or all track # chunks in a Type 2 file) before any non-zero delta time events. If one # is not specified 4/4, 24, 8 should be assumed. - if not common.isListLike(eventList): + if isinstance(eventList, MidiEvent): event = eventList else: # get the second event; first is delta time event = eventList[1] # time signature is 4 byte encoding - post = list(event.data) + eventData = t.cast(bytes, event.data) + post = list(eventData) n = post[0] d = pow(2, post[1]) @@ -1017,7 +1021,10 @@ def midiEventsToTimeSignature(eventList): return ts -def timeSignatureToMidiEvents(ts, includeDeltaTime=True) -> list[DeltaTime|MidiEvent]: +def timeSignatureToMidiEvents( + ts: meter.TimeSignature, + includeDeltaTime: bool = True +) -> list[DeltaTime|MidiEvent]: # noinspection PyShadowingNames ''' Translate a :class:`~music21.meter.TimeSignature` to a pair of events: a DeltaTime and @@ -1076,7 +1083,7 @@ def timeSignatureToMidiEvents(ts, includeDeltaTime=True) -> list[DeltaTime|MidiE return eventList -def midiEventsToKey(eventList) -> key.Key: +def midiEventsToKey(eventList: MidiEvent|Sequence[MidiEvent]) -> key.Key: # noinspection PyShadowingNames r''' Convert a single MIDI event into a :class:`~music21.key.KeySignature` object. @@ -1111,11 +1118,12 @@ def midiEventsToKey(eventList) -> key.Key: # the key specifies the number of sharps and a negative value specifies # the number of flats. A value of 0 for the scale specifies a major key # and a value of 1 specifies a minor key. - if not common.isListLike(eventList): + if isinstance(eventList, MidiEvent): event = eventList else: # get the second event; first is delta time event = eventList[1] - post = list(event.data) + eventData = t.cast(bytes, event.data) + post = list(eventData) # first value is number of sharp, or neg for number of flat if post[0] > 12: @@ -1137,7 +1145,7 @@ def midiEventsToKey(eventList) -> key.Key: def keySignatureToMidiEvents( ks: key.KeySignature, - includeDeltaTime=True + includeDeltaTime: bool = True ) -> list[DeltaTime|MidiEvent]: # noinspection PyShadowingNames r''' @@ -1198,18 +1206,19 @@ def keySignatureToMidiEvents( return eventList -def midiEventsToTempo(eventList): +def midiEventsToTempo(eventList: MidiEvent|Sequence[MidiEvent]) -> tempo.MetronomeMark: ''' Convert a single MIDI event into a music21 Tempo object. TODO: Need Tests ''' - if not common.isListLike(eventList): + if isinstance(eventList, MidiEvent): event = eventList else: # get the second event; first is delta time event = eventList[1] # get microseconds per quarter - mspq = getNumber(event.data, 3)[0] # first data is number + eventData = t.cast(bytes, event.data) + mspq = getNumber(eventData, 3)[0] # first data is number bpm = round(60_000_000 / mspq, 2) # post = list(event.data) # environLocal.printDebug(['midiEventsToTempo, got bpm', bpm]) @@ -1219,7 +1228,7 @@ def midiEventsToTempo(eventList): def tempoToMidiEvents( tempoIndication: tempo.MetronomeMark, - includeDeltaTime=True, + includeDeltaTime: bool = True, ) -> list[DeltaTime|MidiEvent]|None: # noinspection PyShadowingNames r''' @@ -1490,11 +1499,11 @@ def streamToPackets( def assignPacketsToChannels( - packets, - channelByInstrument=None, - channelsDynamic=None, - initTrackIdToChannelMap=None, -): + packets: list[dict[str, t.Any]], + channelByInstrument: dict[int|None, int]|None = None, + channelsDynamic: list[int]|None = None, + initTrackIdToChannelMap: dict[int, int|None]|None = None, +) -> list[dict[str, t.Any]]: ''' Given a list of packets, assign each to a channel. @@ -1521,9 +1530,10 @@ def assignPacketsToChannels( if initTrackIdToChannelMap is None: initTrackIdToChannelMap = {} - uniqueChannelEvents = {} # dict of (start, stop, usedChannel) : channel - post = [] - usedTracks = [] + # dict of (start, stop, usedChannel) : list of cent shifts + uniqueChannelEvents: dict[tuple[int, int, int], list[int]] = {} + post: list[dict[str, t.Any]] = [] + usedTracks: list[int] = [] for p in packets: # environLocal.printDebug(['assignPacketsToChannels', p['midiEvent'].track, p['trackId']]) @@ -1690,8 +1700,10 @@ def assignPacketsToChannels( if trackId == 0: continue # Conductor track: do not add pitch bend ch = initTrackIdToChannelMap[trackId] - # use None for track; will get updated later - me = MidiEvent(track=trackId, + if ch is None: + continue # no channel assigned: do not add pitch bend + # use None for track; will get updated later by MidiTrack.updateEvents() + me = MidiEvent(track=None, type=ChannelVoiceMessages.PITCH_BEND, channel=ch) me.setPitchBend(0) @@ -1783,7 +1795,14 @@ def packetsToDeltaSeparatedEvents( return events -def packetsToMidiTrack(packets, trackId=1, channel=1, instrumentObj=None, *, addEndDelay=True): +def packetsToMidiTrack( + packets: list[dict[str, t.Any]], + trackId: int = 1, + channel: int = 1, + instrumentObj: instrument.Instrument|None = None, + *, + addEndDelay: bool = True, +) -> MidiTrack: ''' Given packets already allocated with channel and/or instrument assignments, place these in a MidiTrack. @@ -2016,7 +2035,7 @@ def insertConductorEvents( target: stream.Part, *, isFirst: bool = False, -): +) -> None: ''' Insert a deepcopy of any TimeSignature, KeySignature, or MetronomeMark found in the `conductorPart` into the `target` Part at the same offset. @@ -2034,16 +2053,16 @@ def insertConductorEvents( target.insert(conductorPart.elementOffset(e), eventCopy) def midiTrackToStream( - mt, + mt: MidiTrack, *, ticksPerQuarter: int = defaults.ticksPerQuarter, - quantizePost=True, - inputM21=None, + quantizePost: bool = True, + inputM21: stream.Part|None = None, conductorPart: stream.Part|None = None, isFirst: bool = False, quarterLengthDivisors: Sequence[int] = (), encoding: str = 'utf-8', - **keywords + **keywords: t.Any ) -> stream.Part: # noinspection PyShadowingNames ''' @@ -2245,7 +2264,7 @@ def midiTrackToStream( return s -def prepareStreamForMidi(s) -> stream.Stream: +def prepareStreamForMidi(s: stream.Stream) -> stream.Stream: # noinspection PyShadowingNames ''' Given a score, prepare it for MIDI processing, and return a new Stream: @@ -2497,9 +2516,9 @@ def channelInstrumentData( def packetStorageFromSubstreamList( - substreamList: list[stream.Part], + substreamList: Sequence[stream.Stream], *, - addStartDelay=False, + addStartDelay: bool = False, encoding: str = 'utf-8', ) -> dict[int, dict[str, t.Any]]: # noinspection PyShadowingNames @@ -2600,7 +2619,7 @@ def packetStorageFromSubstreamList( def updatePacketStorageWithChannelInfo( packetStorage: dict[int, dict[str, t.Any]], - channelByInstrument: dict[int|None, int|None], + channelByInstrument: dict[int|None, int], ) -> None: ''' Take the packetStorage dictionary and using information @@ -2631,13 +2650,13 @@ def updatePacketStorageWithChannelInfo( def streamHierarchyToMidiTracks( - inputM21, + inputM21: stream.Stream, *, - acceptableChannelList=None, - addStartDelay=False, - addEndDelay=True, - encoding='utf-8', -): + acceptableChannelList: list[int]|None = None, + addStartDelay: bool = False, + addEndDelay: bool = True, + encoding: str = 'utf-8', +) -> list[MidiTrack]: ''' Given a Stream, Score, Part, etc., that may have substreams (i.e., a hierarchy), return a list of :class:`~music21.midi.MidiTrack` objects. @@ -2675,7 +2694,7 @@ def streamHierarchyToMidiTracks( # Streams that do not start at same time # store streams in uniform list: prepareStreamForMidi() ensures there are substreams - substreamList = [] + substreamList: list[stream.Stream] = [] for obj in s.getElementsByClass(stream.Stream): # prepareStreamForMidi() supplies defaults for these if obj.getElementsByClass(('MetronomeMark', 'TimeSignature')): @@ -2728,11 +2747,11 @@ def streamHierarchyToMidiTracks( def midiTracksToStreams( midiTracks: list[MidiTrack], ticksPerQuarter: int = defaults.ticksPerQuarter, - quantizePost=True, + quantizePost: bool = True, inputM21: stream.Score|None = None, *, encoding: str = 'utf-8', - **keywords + **keywords: t.Any ) -> stream.Score: ''' Given a list of midiTracks, populate either a new stream.Score or inputM21 @@ -2828,12 +2847,12 @@ def streamToMidiFile( def midiFilePathToStream( - filePath, + filePath: str|pathlib.Path, *, - inputM21=None, + inputM21: stream.Score|None = None, encoding: str = 'utf-8', - **keywords, -): + **keywords: t.Any, +) -> stream.Score: ''' Used by music21.converter: @@ -2864,9 +2883,9 @@ def midiFilePathToStream( def midiAsciiStringToBinaryString( - midiFormat=1, - ticksPerQuarterNote=960, - tracksEventsList=None + midiFormat: int = 1, + ticksPerQuarterNote: int = 960, + tracksEventsList: list[list[str]]|None = None ) -> bytes: r''' Convert Ascii midi data to a bytes object (formerly binary midi string). @@ -2897,7 +2916,7 @@ def midiAsciiStringToBinaryString( ''' mf = MidiFile() - numTracks = len(tracksEventsList) + numTracks = len(tracksEventsList) if tracksEventsList is not None else 0 if numTracks == 1: mf.format = 1 @@ -2947,7 +2966,7 @@ def midiAsciiStringToBinaryString( return midiBinStr -def midiStringToStream(strData, **keywords): +def midiStringToStream(strData: bytes, **keywords: t.Any) -> stream.Score: r''' Convert a string of binary midi data to a Music21 stream.Score object. @@ -2981,11 +3000,11 @@ def midiStringToStream(strData, **keywords): def midiFileToStream( mf: MidiFile, *, - inputM21=None, - quantizePost=True, + inputM21: stream.Score|None = None, + quantizePost: bool = True, encoding: str = 'utf-8', - **keywords -): + **keywords: t.Any +) -> stream.Score: # noinspection PyShadowingNames ''' Note: this is NOT the normal way to read a MIDI file. The best way is generally: