Add type stubs for mido MIDI library

- Created type stubs for various modules in the mido library including messages, midifiles, parser, ports, sockets, syx, tokenizer, and version.
- Implemented type hints for functions and classes to improve type checking and code clarity.
- Added support for MIDI over TCP/IP in sockets module.
- Included methods for reading and writing SYX files in syx module.
- Enhanced the parser functionality with a dedicated Parser class for MIDI byte streams.
- Established a structure for MIDI file handling with MidiFile and MidiTrack classes.
This commit is contained in:
2026-04-26 00:51:40 +08:00
parent e5c692e5d7
commit 9009a7c5bc
23 changed files with 1875 additions and 0 deletions
+237
View File
@@ -0,0 +1,237 @@
"""
MIDI Objects for Python
Mido is a library for working with MIDI messages and ports. It's
designed to be as straight forward and Pythonic as possible.
Creating messages:
Message(type, **parameters) -- create a new message
MetaMessage(type, **parameters) -- create a new meta message
UnknownMetaMessage(type_byte, data=None, time=0)
Ports:
open_input(name=None, virtual=False, callback=None) -- open an input port
open_output(name=None, virtual=False, -- open an output port
autoreset=False)
open_ioport(name=None, virtual=False, -- open an I/O port (capable
callback=None, autoreset=False) of both input and output)
get_input_names() -- return a list of names of available input ports
get_output_names() -- return a list of names of available output ports
get_ioport_names() -- return a list of names of available I/O ports
MIDI files:
MidiFile(filename, **kwargs) -- open a MIDI file
MidiTrack() -- a MIDI track
bpm2tempo() -- convert beats per minute to MIDI file tempo
tempo2bpm() -- convert MIDI file tempo to beats per minute
merge_tracks(tracks) -- merge tracks into one track
SYX files:
read_syx_file(filename) -- read a SYX file
write_syx_file(filename, messages,
plaintext=False) -- write a SYX file
Parsing MIDI streams:
parse(bytes) -- parse a single message bytes
(any iterable that generates integers in 0..127)
parse_all(bytes) -- parse all messages bytes
Parser -- MIDI parser class
Parsing objects serialized with str(message):
parse_string(string) -- parse a string containing a message
parse_string_stream(iterable) -- parse strings from an iterable and
generate messages
Sub modules:
ports -- useful tools for working with ports
For more on MIDI, see:
http://www.midi.org/
Getting started:
>>> import mido
>>> m = mido.Message('note_on', note=60, velocity=64)
>>> m
<message note_on channel=0, note=60, velocity=64, time=0>
>>> m.type
'note_on'
>>> m.channel = 6
>>> m.note = 19
>>> m.copy(velocity=120)
<message note_on channel=0, note=60, velocity=64, time=0>
>>> s = mido.Message('sysex', data=[byte for byte in range(5)])
>>> s.data
(0, 1, 2, 3, 4)
>>> s.hex()
'F0 00 01 02 03 04 F7'
>>> len(s)
7
>>> default_input = mido.open_input()
>>> default_input.name
'MPK mini MIDI 1'
>>> output = mido.open_output('SD-20 Part A')
>>>
>>> for message in default_input:
... output.send(message)
>>> get_input_names()
['MPK mini MIDI 1', 'SH-201']
"""
from collections.abc import Callable
from . import ports, sockets
from .backends.backend import Backend
from .messages import (
MAX_PITCHWHEEL,
MAX_SONGPOS,
MIN_PITCHWHEEL,
MIN_SONGPOS,
BaseMessage,
Message,
format_as_string,
parse_string,
parse_string_stream,
)
from .midifiles import (
KeySignatureError,
MetaMessage,
MidiFile,
MidiTrack,
UnknownMetaMessage,
bpm2tempo,
merge_tracks,
second2tick,
tempo2bpm,
tick2second,
)
from .parser import Parser, parse, parse_all
from .syx import read_syx_file, write_syx_file
from .version import version_info
__all__ = [
"MAX_PITCHWHEEL",
"MAX_SONGPOS",
"MIN_PITCHWHEEL",
"MIN_SONGPOS",
"KeySignatureError",
"Message",
"MetaMessage",
"MidiFile",
"MidiTrack",
"Parser",
"UnknownMetaMessage",
"bpm2tempo",
"format_as_string",
"merge_tracks",
"parse",
"parse_all",
"parse_string",
"parse_string_stream",
"ports",
"read_syx_file",
"second2tick",
"sockets",
"tempo2bpm",
"tick2second",
"version_info",
"write_syx_file",
]
def set_backend(name: str | Backend | None = ..., load: bool = False) -> None:
"""Set current backend.
name can be a module name like `'mido.backends.rtmidi'` or
a `Backend` object.
If no name is passed, the default backend will be used.
This will replace all the `open_*()` and `get_*_name()` functions
in top level mido module. The module will be loaded the first
time one of those functions is called."""
backend: Backend
def open_input(
name: str | None = ...,
virtual: bool = False,
callback: Callable[[BaseMessage], object] | None = ...,
**kwargs: object,
) -> ports.BaseInput:
"""Open an input port.
If the environment variable MIDO_DEFAULT_INPUT is set,
it will override the default port.
virtual=False
Passing True opens a new port that other applications can
connect to. Raises IOError if not supported by the backend.
callback=None
A callback function to be called when a new message arrives.
The function should take one argument (the message).
Raises IOError if not supported by the backend.
"""
def open_output(
name: str | None = ..., virtual: bool = False, autoreset: bool = False, **kwargs: object
) -> ports.BaseOutput:
"""Open an output port.
If the environment variable MIDO_DEFAULT_OUTPUT is set,
it will override the default port.
virtual=False
Passing True opens a new port that other applications can
connect to. Raises IOError if not supported by the backend.
autoreset=False
Automatically send all_notes_off and reset_all_controllers
on all channels. This is the same as calling `port.reset()`.
"""
def open_ioport[TIn: ports.BaseInput = ports.BaseInput, TOut: ports.BaseOutput = ports.BaseOutput](
name: str | None = ...,
virtual: bool = False,
callback: Callable[[BaseMessage], object] | None = ...,
autoreset: bool = False,
**kwargs: object,
) -> ports.BaseIOPort | ports.IOPort[TIn, TOut]:
"""Open a port for input and output.
If the environment variable MIDO_DEFAULT_IOPORT is set,
it will override the default port.
virtual=False
Passing True opens a new port that other applications can
connect to. Raises IOError if not supported by the backend.
callback=None
A callback function to be called when a new message arrives.
The function should take one argument (the message).
Raises IOError if not supported by the backend.
autoreset=False
Automatically send all_notes_off and reset_all_controllers
on all channels. This is the same as calling `port.reset()`.
"""
def get_input_names(**kwargs: object) -> list[str]:
"""Return a list of all input port names."""
def get_output_names(**kwargs: object) -> list[str]:
"""Return a list of all output port names."""
def get_ioport_names(**kwargs: object) -> list[str]:
"""Return a list of all I/O port names."""
View File
+119
View File
@@ -0,0 +1,119 @@
from collections.abc import Callable
from types import ModuleType
from ..messages import BaseMessage
from ..ports import BaseInput, BaseIOPort, BaseOutput, IOPort
DEFAULT_BACKEND: str = "mido.backends.rtmidi"
class Backend:
"""
Wrapper for backend module.
A backend module implements classes for input and output ports for
a specific MIDI library. The Backend object wraps around the
object and provides convenient 'open_*()' and 'get_*_names()'
functions.
"""
name: str
api: str | None
use_environ: bool
def __init__(
self, name: str | None = ..., api: str | None = ..., load: bool = False, use_environ: bool = True
) -> None: ...
@property
def module(self) -> ModuleType:
"""A reference module implementing the backend.
This will always be a valid reference to a module. Accessing
this property will load the module. Use .loaded to check if
the module is loaded.
"""
@property
def loaded(self) -> bool:
"""Return True if the module is loaded."""
def load(self) -> None:
"""Load the module.
Does nothing if the module is already loaded.
This function will be called if you access the 'module'
property."""
def open_input(
self,
name: str | None = ...,
virtual: bool = False,
callback: Callable[[BaseMessage], object] | None = ...,
**kwargs: object,
) -> BaseInput:
"""Open an input port.
If the environment variable MIDO_DEFAULT_INPUT is set,
it will override the default port.
virtual=False
Passing True opens a new port that other applications can
connect to. Raises IOError if not supported by the backend.
callback=None
A callback function to be called when a new message arrives.
The function should take one argument (the message).
Raises IOError if not supported by the backend.
"""
def open_output(
self, name: str | None = ..., virtual: bool = False, autoreset: bool = False, **kwargs: object
) -> BaseOutput:
"""Open an output port.
If the environment variable MIDO_DEFAULT_OUTPUT is set,
it will override the default port.
virtual=False
Passing True opens a new port that other applications can
connect to. Raises IOError if not supported by the backend.
autoreset=False
Automatically send all_notes_off and reset_all_controllers
on all channels. This is the same as calling `port.reset()`.
"""
def open_ioport[TIn: BaseInput = BaseInput, TOut: BaseOutput = BaseOutput](
self,
name: str | None = ...,
virtual: bool = False,
callback: Callable[[BaseMessage], object] | None = ...,
autoreset: bool = False,
**kwargs: object,
) -> BaseIOPort | IOPort[TIn, TOut]:
"""Open a port for input and output.
If the environment variable MIDO_DEFAULT_IOPORT is set,
it will override the default port.
virtual=False
Passing True opens a new port that other applications can
connect to. Raises IOError if not supported by the backend.
callback=None
A callback function to be called when a new message arrives.
The function should take one argument (the message).
Raises IOError if not supported by the backend.
autoreset=False
Automatically send all_notes_off and reset_all_controllers
on all channels. This is the same as calling `port.reset()`.
"""
def get_input_names(self, **kwargs: object) -> list[str]:
"""Return a list of all input port names."""
def get_output_names(self, **kwargs: object) -> list[str]:
"""Return a list of all output port names."""
def get_ioport_names(self, **kwargs: object) -> list[str]:
"""Return a list of all I/O port names."""
+67
View File
@@ -0,0 +1,67 @@
from typing import Never, TypeIs, overload, override
from .messages import Message
from .midifiles import MetaMessage, UnknownMetaMessage
from .midifiles.meta import _MetaMessage # pyright: ignore[reportPrivateUsage]
class Frozen:
@override
def __setattr__(self, *_) -> Never: ...
@override
def __hash__(self) -> int: ...
class FrozenMessage(Frozen, Message): ...
class FrozenMetaMessage(Frozen, _MetaMessage): ...
class FrozenUnknownMetaMessage(Frozen, UnknownMetaMessage): ...
@overload
def is_frozen(msg: UnknownMetaMessage) -> TypeIs[FrozenUnknownMetaMessage]: ... # pyright: ignore[reportOverlappingOverload]
@overload
def is_frozen(msg: _MetaMessage) -> TypeIs[FrozenMetaMessage]: ...
@overload
def is_frozen(msg: Message) -> TypeIs[FrozenMessage]: ...
@overload
def is_frozen(msg: object) -> TypeIs[Frozen]:
"""Return True if message is frozen, otherwise False."""
@overload
def freeze_message[T: Frozen](msg: T) -> T: ...
@overload
def freeze_message(msg: Message) -> FrozenMessage: ...
@overload
def freeze_message(msg: UnknownMetaMessage) -> FrozenUnknownMetaMessage: ...
@overload
def freeze_message(msg: MetaMessage) -> FrozenMetaMessage: ...
@overload
def freeze_message(msg: None) -> None: ...
@overload
def freeze_message[T: Frozen](
msg: T | Message | UnknownMetaMessage | MetaMessage | None,
) -> T | FrozenMessage | FrozenUnknownMetaMessage | FrozenMetaMessage | None:
"""Freeze message.
Returns a frozen version of the message. Frozen messages are
immutable, hashable and can be used as dictionary keys.
Will return None if called with None. This allows you to do things
like::
msg = freeze_message(port.poll())
"""
@overload
def thaw_message(msg: FrozenUnknownMetaMessage) -> UnknownMetaMessage: ...
@overload
def thaw_message(msg: FrozenMetaMessage) -> MetaMessage: ...
@overload
def thaw_message(msg: FrozenMessage) -> Message: ...
@overload
def thaw_message(
msg: FrozenMessage | FrozenUnknownMetaMessage | FrozenMetaMessage,
) -> Message | UnknownMetaMessage | MetaMessage:
"""Thaw message.
Returns a mutable version of a frozen message.
Will return None if called with None.
"""
+19
View File
@@ -0,0 +1,19 @@
from .checks import check_time
from .messages import BaseMessage, Message, format_as_string, parse_string, parse_string_stream
from .specs import MAX_PITCHWHEEL, MAX_SONGPOS, MIN_PITCHWHEEL, MIN_SONGPOS, SPEC_BY_STATUS, SPEC_BY_TYPE, SPEC_LOOKUP
__all__ = [
"MAX_PITCHWHEEL",
"MAX_SONGPOS",
"MIN_PITCHWHEEL",
"MIN_SONGPOS",
"SPEC_BY_STATUS",
"SPEC_BY_TYPE",
"SPEC_LOOKUP",
"BaseMessage",
"Message",
"check_time",
"format_as_string",
"parse_string",
"parse_string_stream",
]
+19
View File
@@ -0,0 +1,19 @@
from collections.abc import Callable, Iterable
from numbers import Integral, Real
from .specs import MsgDict
def check_type(type_: str) -> None: ...
def check_channel(channel: int | Integral) -> None: ...
def check_pos(pos: int | Integral) -> None: ...
def check_pitch(pitch: int | Integral) -> None: ...
def check_data(data_bytes: Iterable[int | Integral]) -> None: ...
def check_frame_type(value: int | Integral) -> None: ...
def check_frame_value(value: int | Integral) -> None: ...
def check_data_byte(value: int | Integral) -> None: ...
def check_time(time: float | Real) -> None: ...
_CHECKS: dict[str, Callable[[object], None]]
def check_value(name: str, value: object) -> None: ...
def check_msgdict(msgdict: MsgDict) -> None: ...
+15
View File
@@ -0,0 +1,15 @@
from collections.abc import Callable, Iterable
from numbers import Integral
from .specs import MsgDict
_SPECIAL_CASES: dict[int, Callable[[Iterable[int | Integral]], dict[str, object]]]
def decode_message(msg_bytes: Iterable[int | Integral], time: int = 0, check: bool = True) -> MsgDict:
"""Decode message bytes and return messages as a dictionary.
Raises ValueError if the bytes are out of range or the message is
invalid.
This is not a part of the public API.
"""
+14
View File
@@ -0,0 +1,14 @@
from collections.abc import Callable
from .specs import MsgDict
_SPECIAL_CASES: dict[str, Callable[[MsgDict], list[object]]]
def encode_message(msg: MsgDict) -> list[int]:
"""Encode msg dict as a list of bytes.
TODO: Add type and value checking.
(Can be turned off with keyword argument.)
This is not a part of the public API.
"""
+146
View File
@@ -0,0 +1,146 @@
from abc import ABCMeta, abstractmethod
from collections.abc import Generator, Iterable
from typing import Literal, Never, Self, Unpack, override
from mido.messages.specs import MsgDict, MsgDictOverride
class BaseMessage(metaclass=ABCMeta):
"""Abstract base class for messages."""
@property
@abstractmethod
def is_meta(self) -> bool: ... # a normal attribute in runtime
@abstractmethod
def copy(self) -> Self: ...
@abstractmethod
def bytes(self) -> list[int]: ...
def bin(self) -> bytearray:
"""Encode message and return as a bytearray.
This can be used to write the message to a file.
"""
def hex(self, sep: str = " ") -> str:
"""Encode message and return as a string of hex numbers,
Each number is separated by the string sep.
"""
def dict(self) -> MsgDict:
"""Returns a dictionary containing the attributes of the message.
Example: `{'type': 'sysex', 'data': [1, 2], 'time': 0}`
Sysex data will be returned as a list.
"""
@classmethod
def from_dict(cls, data: MsgDict) -> Self:
"""Create a message from a dictionary.
Only "type" is required. The other will be set to default
values.
"""
@property
def is_realtime(self) -> bool:
"""True if the message is a system realtime message."""
def is_cc(self, control: int | None = ...) -> bool:
"""Return True if the message is of type 'control_change'.
The optional control argument can be used to test for a specific
control number, for example:
>>> if msg.is_cc(7):
... # Message is control change 7 (channel volume).
"""
@override
def __delattr__(self, name: str) -> Never: ...
@override
def __setattr__(self, name: str, value: object) -> None: ...
@override
def __eq__(self, other: object) -> bool: ...
class SysexData(tuple[int, ...]):
"""Special kind of tuple accepts and converts any sequence in `+=`."""
def __iadd__(self, other: SysexData) -> Self: ...
class Message(BaseMessage):
type: str
time: int
@property
@override
def is_meta(self) -> Literal[False]: ... # a normal attribute in runtime
def __init__(self, type: str, skip_checks: bool = False, **args: Unpack[MsgDictOverride]) -> None: ...
@override
def copy(self, skip_checks: bool = False, **overrides: Unpack[MsgDictOverride]) -> Self:
"""Return a copy of the message.
Attributes will be overridden by the passed keyword arguments.
Only message specific attributes can be overridden. The message
type can not be changed.
The `skip_checks` arg can be used to bypass validation of message
attributes and should be used cautiously.
"""
@classmethod
def from_bytes(cls, data: Iterable[int], time: int = 0) -> Self:
"""Parse a byte encoded message.
Accepts a byte string or any iterable of integers.
This is the reverse of `msg.bytes()` or `msg.bin()`.
"""
@classmethod
def from_hex(cls, text: str, time: int = 0, sep: str | None = ...) -> Self:
"""Parse a hex encoded message.
This is the reverse of `msg.hex()`.
"""
@classmethod
def from_str(cls, text: str) -> Self:
"""Parse a string encoded message.
This is the reverse of `str(msg)`.
"""
def __len__(self) -> int: ...
@override
def __setattr__(self, name: str, value: object) -> None: ...
@override
def bytes(self) -> list[int]:
"""Encode message and return as a list of integers."""
def parse_string(text: str) -> Message:
"""Parse a string of text and return a message.
The string can span multiple lines, but must contain
one full message.
Raises ValueError if the string could not be parsed.
"""
def parse_string_stream(stream: Iterable[str]) -> Generator[tuple[Message, None] | tuple[None, str], Never]:
"""Parse a stream of messages and yield `(message, error_message)`
stream can be any iterable that generates text strings, where each
string is a string encoded message.
If a string can be parsed, `(message, None)` is returned. If it
can't be parsed, `(None, error_message)` is returned. The error
message contains the line number where the error occurred.
"""
def format_as_string(msg: Message, include_time: bool = True) -> str:
"""Format a message and return as a string.
This is equivalent to `str(message)`.
To leave out the time attribute, pass `include_time=False`.
"""
+179
View File
@@ -0,0 +1,179 @@
"""Definitions and lookup tables for MIDI messages.
TODO:
* add lookup functions for messages definitions by type and status
byte.
"""
from collections.abc import Sequence
from typing import Literal, NotRequired, TypedDict, final, overload, type_check_only
CHANNEL_MESSAGES: set[int]
COMMON_MESSAGES: set[int]
REALTIME_MESSAGES: set[int]
SYSEX_START: int = 0xF0
SYSEX_END: int = 0xF7
# Pitchwheel is a 14 bit signed integer
MIN_PITCHWHEEL: int = -8192
MAX_PITCHWHEEL: int = 8191
# Song pos is a 14 bit unsigned integer
MIN_SONGPOS: int = 0
MAX_SONGPOS: int = 16383
@type_check_only
class SpecsDict(TypedDict):
status_byte: int
type: str
value_names: tuple[str, ...]
attribute_names: set[str]
length: int | float
@type_check_only
class MsgDictOverride(TypedDict):
channel: NotRequired[int]
control: NotRequired[int]
data: NotRequired[Sequence[int]]
frame_type: NotRequired[int]
frame_value: NotRequired[int]
note: NotRequired[int]
pitch: NotRequired[int]
pos: NotRequired[int]
program: NotRequired[int]
song: NotRequired[int]
value: NotRequired[int]
velocity: NotRequired[int]
time: NotRequired[int | float]
@type_check_only
class MsgDictBase[T: str](TypedDict):
type: T
time: int | float
@type_check_only
class ChannelMsgDictBase[
T: Literal["note_off", "note_on", "polytouch", "control_change", "program_change", "aftertouch", "pitchwheel"]
](MsgDictBase[T]):
channel: int
@type_check_only
@final
class NoteMsgDict(ChannelMsgDictBase[Literal["note_off", "note_on"]]):
note: int
velocity: int
@type_check_only
@final
class PolytouchMsgDict(ChannelMsgDictBase[Literal["polytouch"]]):
note: int
value: int
@type_check_only
@final
class ControlChangeMsgDict(ChannelMsgDictBase[Literal["control_change"]]):
control: int
value: int
@type_check_only
@final
class ProgramChangeMsgDict(ChannelMsgDictBase[Literal["program_change"]]):
program: int
@type_check_only
@final
class AftertouchMsgDict(ChannelMsgDictBase[Literal["aftertouch"]]):
value: int
@type_check_only
@final
class PitchwheelMsgDict(ChannelMsgDictBase[Literal["pitchwheel"]]):
pitch: int
@type_check_only
@final
class SysExMsgDict(MsgDictBase[Literal["sysex"]]):
data: Sequence[int]
@type_check_only
@final
class QuarterFrameMsgDict(MsgDictBase[Literal["quarter_frame"]]):
frame_type: int
frame_value: int
@type_check_only
@final
class SongposMsgDict(MsgDictBase[Literal["songpos"]]):
pos: int
@type_check_only
@final
class SongSelectMsgDict(MsgDictBase[Literal["song_select"]]):
song: int
@type_check_only
@final
class RealTimeMsgDict(
MsgDictBase[Literal["tune_request", "clock", "start", "continue", "stop", "active_sensing", "reset"]]
): ...
type MsgDict = (
NoteMsgDict
| PolytouchMsgDict
| ControlChangeMsgDict
| ProgramChangeMsgDict
| AftertouchMsgDict
| PitchwheelMsgDict
| SysExMsgDict
| QuarterFrameMsgDict
| SongposMsgDict
| SongSelectMsgDict
| RealTimeMsgDict
)
SPECS: list[SpecsDict]
SPEC_LOOKUP: dict[str | int, SpecsDict]
SPEC_BY_STATUS: dict[int, SpecsDict]
SPEC_BY_TYPE: dict[str, SpecsDict]
REALTIME_TYPES: set[str]
DEFAULT_VALUES: MsgDict
@overload
def make_msgdict(type_: Literal["note_off", "note_on"], overrides: MsgDictOverride) -> NoteMsgDict: ...
@overload
def make_msgdict(type_: Literal["polytouch"], overrides: MsgDictOverride) -> PolytouchMsgDict: ...
@overload
def make_msgdict(type_: Literal["control_change"], overrides: MsgDictOverride) -> ControlChangeMsgDict: ...
@overload
def make_msgdict(type_: Literal["program_change"], overrides: MsgDictOverride) -> ProgramChangeMsgDict: ...
@overload
def make_msgdict(type_: Literal["aftertouch"], overrides: MsgDictOverride) -> AftertouchMsgDict: ...
@overload
def make_msgdict(type_: Literal["pitchwheel"], overrides: MsgDictOverride) -> PitchwheelMsgDict: ...
@overload
def make_msgdict(type_: Literal["sysex"], overrides: MsgDictOverride) -> SysExMsgDict: ...
@overload
def make_msgdict(type_: Literal["quarter_frame"], overrides: MsgDictOverride) -> QuarterFrameMsgDict: ...
@overload
def make_msgdict(type_: Literal["songpos"], overrides: MsgDictOverride) -> SongposMsgDict: ...
@overload
def make_msgdict(type_: Literal["song_select"], overrides: MsgDictOverride) -> SongSelectMsgDict: ...
@overload
def make_msgdict(
type_: Literal["tune_request", "clock", "start", "continue", "stop", "active_sensing", "reset"],
overrides: MsgDictOverride,
) -> RealTimeMsgDict: ...
@overload
def make_msgdict[T: str](type_: T, overrides: MsgDictOverride) -> MsgDictBase[T]: ...
def make_msgdict[T: str](type_: T, overrides: MsgDictOverride) -> MsgDictBase[T]:
"""Return a new message.
Returns a dictionary representing a message.
Message values can be overriden.
No type or value checking is done. The caller is responsible for
calling `check_msgdict()`.
"""
+15
View File
@@ -0,0 +1,15 @@
"""
This type stub file was generated by pyright.
"""
from .specs import MsgDict
def msg2str(msg: MsgDict, include_time: bool = True) -> str:
...
def str2msg(text: str) -> MsgDict:
"""Parse str format and return message dict.
No type or value checking is done. The caller is responsible for
calling check_msgdict().
"""
+21
View File
@@ -0,0 +1,21 @@
"""
This type stub file was generated by pyright.
"""
from .meta import KeySignatureError, MetaMessage, UnknownMetaMessage
from .midifiles import MidiFile
from .tracks import MidiTrack, merge_tracks
from .units import bpm2tempo, second2tick, tempo2bpm, tick2second
__all__ = [
"KeySignatureError",
"MetaMessage",
"MidiFile",
"MidiTrack",
"UnknownMetaMessage",
"bpm2tempo",
"merge_tracks",
"second2tick",
"tempo2bpm",
"tick2second",
]
+366
View File
@@ -0,0 +1,366 @@
"""
Meta messages for MIDI files.
TODO:
- what if an unknown meta message is implemented and someone depends on
the 'data' attribute?
- is 'type_byte' a good name?
- 'values' is not a good name for a dictionary.
- type and value safety?
- copy().
- expose _key_signature_encode/decode?
"""
from abc import ABCMeta, abstractmethod
from collections.abc import Generator, Iterable, Sequence
from contextlib import contextmanager
from numbers import Integral
from typing import ClassVar, Literal, Never, Self, SupportsIndex, override, type_check_only
from _typeshed import ReadableBuffer
from ..messages import BaseMessage
_charset: str = "latin1"
class KeySignatureError(Exception):
"""Raised when key cannot be converted from key/mode to key letter"""
_key_signature_decode: dict[tuple[int, int], str]
_key_signature_encode: dict[str, tuple[int, int]]
_smpte_framerate_decode: dict[int, int | float]
_smpte_framerate_encode: dict[int | float, int]
type _EncodeTypeNameSigned = Literal["byte", "short", "long"]
type _EncodeTypeNameUnsigned = Literal["ubyte", "ushort", "ulong"]
def signed(to_type: _EncodeTypeNameSigned | _EncodeTypeNameUnsigned, n: int) -> int: ...
def unsigned(to_type: _EncodeTypeNameSigned, n: int) -> int: ...
def encode_variable_int(value: int) -> list[int]:
"""Encode variable length integer.
Returns the integer as a list of bytes,
where the last byte is < 128.
This is used for delta times and meta message payload
length.
"""
def decode_variable_int(value: Iterable[int]) -> int:
"""Decode a list to a variable length integer.
Does the opposite of `encode_variable_int(value)`
"""
def encode_string(string: str) -> list[int]: ...
def decode_string(data: Iterable[SupportsIndex] | ReadableBuffer) -> str: ...
@contextmanager
def meta_charset(tmp_charset: str) -> Generator[None, Never]: ...
def check_int(value: int | Integral, low: int | Integral, high: int | Integral) -> None: ...
def check_str(value: str) -> None: ...
class MetaSpec(metaclass=ABCMeta): # not an ABC in runtime
type: str
@type_check_only
@abstractmethod
def decode(self, message: MetaMessage, data: Sequence[int]) -> None: ... # not exist in runtime
@type_check_only
@abstractmethod
def encode(self, message: MetaMessage) -> list[int]: ... # not exist in runtime
def check(self, name: Never, value: Never) -> None: ...
class MetaSpec_sequence_number(MetaSpec):
type: str = "sequence_number"
type_byte: int = 0x00
attributes: ClassVar[list[str]] = ["number"]
defaults: ClassVar[list[int]] = [0]
@override
def decode(self, message: MetaMessage, data: Sequence[int]) -> None: ...
@override
def encode(self, message: MetaMessage) -> list[int]: ...
@override
def check(self, name: Never, value: int | Integral) -> None: ...
class MetaSpec_text(MetaSpec):
type: str = "text"
type_byte: int = 0x01
attributes: ClassVar[list[str]] = ["text"]
defaults: ClassVar[list[str]] = [""]
@override
def decode(self, message: MetaMessage, data: Iterable[SupportsIndex] | ReadableBuffer) -> None: ...
@override
def encode(self, message: MetaMessage) -> list[int]: ...
@override
def check(self, name: Never, value: str) -> None: ...
class MetaSpec_copyright(MetaSpec_text):
type: str = "copyright"
type_byte: int = 0x02
class MetaSpec_track_name(MetaSpec_text):
type: str = "track_name"
type_byte: int = 0x03
attributes: ClassVar[list[str]] = ["name"]
defaults: ClassVar[list[str]] = [""]
@override
def decode(self, message: MetaMessage, data: Iterable[SupportsIndex] | ReadableBuffer) -> None: ...
@override
def encode(self, message: MetaMessage) -> list[int]: ...
class MetaSpec_instrument_name(MetaSpec_track_name):
type: str = "instrument_name"
type_byte: int = 0x04
class MetaSpec_lyrics(MetaSpec_text):
type: str = "lyrics"
type_byte: int = 0x05
class MetaSpec_marker(MetaSpec_text):
type: str = "marker"
type_byte: int = 0x06
class MetaSpec_cue_marker(MetaSpec_text):
type: str = "cue_marker"
type_byte: int = 0x07
class MetaSpec_device_name(MetaSpec_track_name):
type: str = "device_name"
type_byte: int = 0x09
class MetaSpec_channel_prefix(MetaSpec):
type: str = "channel_prefix"
type_byte: int = 0x20
attributes: ClassVar[list[str]] = ["channel"]
defaults: ClassVar[list[int]] = [0]
@override
def decode(self, message: MetaMessage, data: Sequence[int]) -> None: ...
@override
def encode(self, message: MetaMessage) -> list[int]: ...
@override
def check(self, name: Never, value: int) -> None: ...
class MetaSpec_midi_port(MetaSpec):
type: str = "midi_port"
type_byte: int = 0x21
attributes: ClassVar[list[str]] = ["port"]
defaults: ClassVar[list[int]] = [0]
@override
def decode(self, message: MetaMessage, data: Sequence[int]) -> None: ...
@override
def encode(self, message: MetaMessage) -> list[int]: ...
@override
def check(self, name: Never, value: int) -> None: ...
class MetaSpec_end_of_track(MetaSpec):
type: str = "end_of_track"
type_byte: int = 0x2F
attributes: ClassVar[list[Never]] = []
defaults: ClassVar[list[Never]] = []
@override
def decode(self, message: MetaMessage, data: Sequence[int]) -> None: ...
@override
def encode(self, message: MetaMessage) -> list[int]: ...
class MetaSpec_set_tempo(MetaSpec):
type: str = "set_tempo"
type_byte: int = 0x51
attributes: ClassVar[list[str]] = ["tempo"]
defaults: ClassVar[list[int]] = [500000]
@override
def decode(self, message: MetaMessage, data: Sequence[int]) -> None: ...
@override
def encode(self, message: MetaMessage) -> list[int]: ...
@override
def check(self, name: Never, value: int) -> None: ...
class MetaSpec_smpte_offset(MetaSpec):
type: str = "smpte_offset"
type_byte: int = 0x54
attributes: ClassVar[list[str]] = ["frame_rate", "hours", "minutes", "seconds", "frames", "sub_frames"]
# TODO: What are some good defaults?
defaults: ClassVar[list[int]] = [24, 0, 0, 0, 0, 0]
@override
def decode(self, message: MetaMessage, data: Sequence[int]) -> None: ...
@override
def encode(self, message: MetaMessage) -> list[int]: ...
@override
def check(self, name: str, value: int) -> None: ...
class MetaSpec_time_signature(MetaSpec):
type: str = "time_signature"
type_byte: int = 0x58
# TODO: these need more sensible names.
attributes: ClassVar[list[str]] = ["numerator", "denominator", "clocks_per_click", "notated_32nd_notes_per_beat"]
defaults: ClassVar[list[int]] = [4, 4, 24, 8]
@override
def decode(self, message: MetaMessage, data: Sequence[int]) -> None: ...
@override
def encode(self, message: MetaMessage) -> list[int]: ...
@override
def check(self, name: str, value: int) -> None: ...
class MetaSpec_key_signature(MetaSpec):
type: str = "key_signature"
type_byte: int = 0x59
attributes: ClassVar[list[str]] = ["key"]
defaults: ClassVar[list[str]] = ["C"]
@override
def decode(self, message: MetaMessage, data: Sequence[int]) -> None: ...
@override
def encode(self, message: MetaMessage) -> list[int]: ...
@override
def check(self, name: Never, value: str) -> None: ...
class MetaSpec_sequencer_specific(MetaSpec):
type: str = "sequencer_specific"
type_byte: int = 0x7F
attributes: ClassVar[list[str]] = ["data"]
defaults: ClassVar[list[Sequence[int]]]
@override
def decode(self, message: MetaMessage, data: Iterable[int]) -> None: ...
@override
def encode(self, message: MetaMessage) -> list[int]: ...
def add_meta_spec(klass: type[MetaSpec]) -> None: ...
_META_SPECS: dict[int | str, type[MetaSpec]]
_META_SPEC_BY_TYPE: dict[str, type[MetaSpec]]
def build_meta_message(
meta_type: int | str, data: Sequence[int], delta: int = 0
) -> MetaMessage | UnknownMetaMessage: ...
# NOTE: Generics not supported in runtime
class _MetaMessage[T: str = str](BaseMessage):
type: T
time: int
@property
@override
def is_meta(self) -> Literal[True]: ... # a normal attribute in runtime
def __init__(self, type: str, skip_checks: bool = False, **kwargs: object) -> None: ...
@override
def copy(self, **overrides: object) -> Self:
"""Return a copy of the message
Attributes will be overridden by the passed keyword arguments.
Only message specific attributes can be overridden. The message
type can not be changed.
"""
@override
def __setattr__(self, name: str, value: object) -> None: ...
@override
def bytes(self) -> list[int]: ...
@classmethod
def from_bytes(cls, msg_bytes: Sequence[int]) -> MetaMessage | UnknownMetaMessage: ...
@type_check_only
class SequenceNumberMetaMessage(_MetaMessage[Literal["sequence_number"]]):
number: int
@type_check_only
class TextMetaMessage(_MetaMessage[Literal["text"]]):
text: str
@type_check_only
class CopyrightMetaMessage(_MetaMessage[Literal["copyright"]]):
text: str
@type_check_only
class TrackNameMetaMessage(_MetaMessage[Literal["track_name"]]):
name: str
@type_check_only
class InstrumentNameMetaMessage(_MetaMessage[Literal["instrument_name"]]):
name: str
@type_check_only
class LyricsMetaMessage(_MetaMessage[Literal["lyrics"]]):
text: str
@type_check_only
class MarkerMetaMessage(_MetaMessage[Literal["marker"]]):
text: str
@type_check_only
class CueMarkerMetaMessage(_MetaMessage[Literal["cue_marker"]]):
text: str
@type_check_only
class DeviceNameMetaMessage(_MetaMessage[Literal["device_name"]]):
name: str
@type_check_only
class ChannelPrefixMetaMessage(_MetaMessage[Literal["channel_prefix"]]):
channel: int
@type_check_only
class MidiPortMetaMessage(_MetaMessage[Literal["midi_port"]]):
port: int
@type_check_only
class EndOfTrackMetaMessage(_MetaMessage[Literal["end_of_track"]]): ...
@type_check_only
class SetTempoMetaMessage(_MetaMessage[Literal["set_tempo"]]):
tempo: int
@type_check_only
class SmpteOffsetMetaMessage(_MetaMessage[Literal["smpte_offset"]]):
frame_rate: int
hours: int
minutes: int
seconds: int
frames: int
sub_frames: int
@type_check_only
class TimeSignatureMetaMessage(_MetaMessage[Literal["time_signature"]]):
numerator: int
denominator: int
clocks_per_click: int
notated_32nd_notes_per_beat: int
@type_check_only
class KeySignatureMetaMessage(_MetaMessage[Literal["key_signature"]]):
key: str
@type_check_only
class SequencerSpecificMetaMessage(_MetaMessage[Literal["sequencer_specific"]]):
data: Sequence[int]
class UnknownMetaMessage(_MetaMessage):
type_byte: Sequence[int]
data: tuple[int, ...]
def __init__(
self,
type_byte: Sequence[int],
data: Sequence[int] | None = ...,
time: int = 0,
type: str = "unknown_meta",
**kwargs: Never,
) -> None: ...
@override
def __setattr__(self, name: str, value: object) -> None: ...
@override
def bytes(self) -> list[int]: ...
# NOTE: `MetaMessage` is a concrete type (actually the `_MetaMessage` in this stub) in runtime
type MetaMessage = (
SequenceNumberMetaMessage
| TextMetaMessage
| CopyrightMetaMessage
| TrackNameMetaMessage
| InstrumentNameMetaMessage
| LyricsMetaMessage
| MarkerMetaMessage
| CueMarkerMetaMessage
| DeviceNameMetaMessage
| ChannelPrefixMetaMessage
| MidiPortMetaMessage
| EndOfTrackMetaMessage
| SetTempoMetaMessage
| SmpteOffsetMetaMessage
| TimeSignatureMetaMessage
| KeySignatureMetaMessage
| SequencerSpecificMetaMessage
)
+154
View File
@@ -0,0 +1,154 @@
"""
MIDI file reading and playback.
References:
http://home.roadrunner.com/~jgglatt/
http://home.roadrunner.com/~jgglatt/tech/miditech.htm
http://home.roadrunner.com/~jgglatt/tech/midifile.htm
http://www.sonicspot.com/guide/midifiles.html
http://www.ccarh.org/courses/253/assignment/midifile/
https://code.google.com/p/binasc/wiki/mainpage
http://stackoverflow.com/questions/2984608/midi-delta-time
http://www.recordingblogs.com/sa/tabid/82/EntryId/44/MIDI-Part-XIII-Delta-time-a
http://www.sonicspot.com/guide/midifiles.html
"""
from collections.abc import Callable, Generator, Iterable, Sequence
from numbers import Real
from typing import IO, Literal, Never, Self, overload
from ..messages import BaseMessage, Message
from .meta import MetaMessage, UnknownMetaMessage
from .tracks import MidiTrack
type _RealNumber = int | float | Real
# The default tempo is 120 BPM.
# (500000 microseconds per beat (quarter note).)
DEFAULT_TEMPO: int = 500000
DEFAULT_TICKS_PER_BEAT: int = 480
# Maximum message length to attempt to read.
MAX_MESSAGE_LENGTH: int = 1000000
def print_byte(byte: int, pos: int = 0) -> None: ...
class DebugFileWrapper:
def __init__(self, file: IO[bytes]) -> None: ...
def read(self, size: int) -> bytes: ...
def tell(self) -> int: ...
def read_byte(self: IO[bytes]) -> int: ...
def read_bytes(infile: IO[bytes], size: int) -> list[int]: ...
def read_chunk_header(infile: IO[bytes]) -> tuple[str, int]: ...
def read_file_header(infile: IO[bytes]) -> tuple[int, int, int]: ...
def read_message(
infile: IO[bytes], status_byte: int, peek_data: Sequence[int], delta: int, clip: bool = False
) -> Message: ...
def read_sysex(infile: IO[bytes], delta: int, clip: bool = False) -> Message: ...
def read_variable_int(infile: IO[bytes]) -> int: ...
def read_meta_message(infile: IO[bytes], delta: int) -> MetaMessage | UnknownMetaMessage: ...
def read_track(infile: IO[bytes], debug: bool = False, clip: bool = False) -> MidiTrack: ...
def write_chunk(outfile: IO[bytes], name: bytes, data: bytes) -> None:
"""Write an IFF chunk to the file.
`name` must be a bytestring."""
def write_track(outfile: IO[bytes], track: Iterable[BaseMessage]) -> None: ...
def get_seconds_per_tick(tempo: _RealNumber, ticks_per_beat: _RealNumber) -> float: ...
class MidiFile:
filename: str | None
type: Literal[0, 1, 2]
ticks_per_beat: int
charset: str
debug: bool
clip: bool
tracks: list[MidiTrack]
def __init__(
self,
filename: str | None = ...,
file: IO[bytes] | None = ...,
type: Literal[0, 1, 2] = 1,
ticks_per_beat: int = DEFAULT_TICKS_PER_BEAT, # noqa: PYI011
charset: str = "latin1",
debug: bool = False,
clip: bool = False,
tracks: list[MidiTrack] | None = ...,
) -> None: ...
@property
def merged_track(self) -> MidiTrack: ...
@merged_track.deleter
def merged_track(self) -> None: ...
def add_track(self, name: str | None = ...) -> MidiTrack:
"""Add a new track to the file.
This will create a new `MidiTrack` object and append it to the
track list.
"""
@property
def length(self) -> int | float:
"""Playback time in seconds.
This will be computed by going through every message in every
track and adding up delta times.
"""
def __iter__(self) -> Generator[Message | MetaMessage | UnknownMetaMessage, Never]: ...
@overload
def play(
self, meta_messages: Literal[False] = False, now: Callable[[], float] = ...
) -> Generator[Message, Never]: ...
@overload
def play(
self, meta_messages: bool, now: Callable[[], float] = ...
) -> Generator[Message | MetaMessage | UnknownMetaMessage, Never]: ...
def play(
self, meta_messages: bool = False, now: Callable[[], float] = ...
) -> Generator[Message | MetaMessage | UnknownMetaMessage, Never]:
"""Play back all tracks.
The generator will sleep between each message by
default. Messages are yielded with correct timing. The time
attribute is set to the number of seconds slept since the
previous message.
By default you will only get normal MIDI messages. Pass
`meta_messages=True` if you also want meta messages.
You will receive copies of the original messages, so you can
safely modify them without ruining the tracks.
By default the system clock is used for the timing of yielded
MIDI events. To use a different clock (e.g. to synchronize to
an audio stream), pass `now=time_fn` where `time_fn` is a zero
argument function that yields the current time in seconds.
"""
def save(self, filename: str | None = ..., file: IO[bytes] | None = ...) -> None:
"""Save to a file.
If file is passed the data will be saved to that file. This is
typically an in-memory file or and already open file like `sys.stdout`.
If filename is passed the data will be saved to that file.
Raises `ValueError` if both file and filename are None,
or if a type 0 file has != one track.
"""
def print_tracks(self, meta_only: bool = False) -> None:
"""Prints out all messages in a .midi file.
May take argument `meta_only` to show only meta messages.
Use:
`print_tracks()` -> will print all messages
`print_tracks(meta_only=True)` -> will print only MetaMessages
"""
def __enter__(self) -> Self: ...
def __exit__(self, type: object, value: object, traceback: object) -> Literal[False]: ...
+46
View File
@@ -0,0 +1,46 @@
from collections.abc import Generator, Iterable, Sequence
from typing import Never, Self, SupportsIndex, overload, override
from ..messages import BaseMessage
class MidiTrack(list[BaseMessage]):
@property
def name(self) -> str:
"""Name of the track.
This will return the name from the first `track_name` meta
message in the track, or `''` if there is no such message.
Setting this property will update the name field of the first
`track_name` message in the track. If no such message is found,
one will be added to the beginning of the track with a delta
time of 0."""
@name.setter
def name(self, name: str) -> None: ...
@override
def copy(self) -> Self: ...
@overload
def __getitem__(self, index_or_slice: SupportsIndex) -> BaseMessage: ...
@overload
def __getitem__(self, index_or_slice: slice[int, int, int]) -> Self: ...
@override
def __add__(self, other: Sequence[BaseMessage]) -> Self: ... # pyright: ignore[reportIncompatibleMethodOverride]
@override
def __mul__(self, other: SupportsIndex) -> Self: ...
def fix_end_of_track(messages: Iterable[BaseMessage], skip_checks: bool = False) -> Generator[BaseMessage, Never]:
"""Remove all end_of_track messages and add one at the end.
This is used by `merge_tracks()` and `MidiFile.save()`."""
def merge_tracks(tracks: Iterable[MidiTrack], skip_checks: bool = False) -> MidiTrack:
"""Returns a MidiTrack object with all messages from all tracks.
The messages are returned in playback order with delta times
as if they were all in one track.
Pass `skip_checks=True` to skip validation of messages before merging.
This should ONLY be used when the messages in tracks have already
been validated by mido.checks.
"""
+38
View File
@@ -0,0 +1,38 @@
from numbers import Real
type _RealNumber = int | float | Real
type _TimeSignature = tuple[int, int]
def tick2second(tick: _RealNumber, ticks_per_beat: _RealNumber, tempo: _RealNumber) -> float:
"""Convert absolute time in ticks to seconds.
Returns absolute time in seconds for a chosen MIDI file time resolution
(ticks/pulses per quarter note, also called PPQN) and tempo (microseconds
per quarter note).
"""
def second2tick(second: _RealNumber, ticks_per_beat: _RealNumber, tempo: _RealNumber) -> int:
"""Convert absolute time in seconds to ticks.
Returns absolute time in ticks for a chosen MIDI file time resolution
(ticks/pulses per quarter note, also called PPQN) and tempo (microseconds
per quarter note). Normal rounding applies.
"""
def bpm2tempo(bpm: _RealNumber, time_signature: _TimeSignature = (4, 4)) -> int:
"""Convert BPM (beats per minute) to MIDI file tempo (microseconds per
quarter note).
Depending on the chosen time signature a bar contains a different number of
beats. These beats are multiples/fractions of a quarter note, thus the
returned BPM depend on the time signature. Normal rounding applies.
"""
def tempo2bpm(tempo: _RealNumber, time_signature: _TimeSignature = (4, 4)) -> float:
"""Convert MIDI file tempo (microseconds per quarter note) to BPM (beats
per minute).
Depending on the chosen time signature a bar contains a different number of
beats. The beats are multiples/fractions of a quarter note, thus the
returned tempo depends on the time signature denominator.
"""
+81
View File
@@ -0,0 +1,81 @@
"""
MIDI Parser
There is no need to use this module directly. All you need is
available in the top level module.
"""
from collections import deque
from collections.abc import Generator, Iterable
from numbers import Integral
from typing import Never
from .messages import Message
class Parser:
"""
MIDI byte stream parser
Parses a stream of MIDI bytes and produces messages.
Data can be put into the parser in the form of
integers, byte arrays or byte strings.
"""
messages: deque[Message]
def __init__(self, data: Iterable[int] | None = ...) -> None: ...
def feed(self, data: Iterable[int]) -> None:
"""Feed MIDI data to the parser.
Accepts any object that produces a sequence of integers in
range 0..255, such as:
```
[0, 1, 2]
(0, 1, 2)
[for i in range(256)]
(for i in range(256)]
bytearray()
```
"""
def feed_byte(self, byte: int | Integral) -> None:
"""Feed one MIDI byte into the parser.
The byte must be an integer in range 0..255.
"""
def get_message(self) -> Message | None:
"""Get the first parsed message.
Returns None if there is no message yet. If you don't want to
deal with None, you can use `pending()` to see how many messages
you can get before you get None, or just iterate over the
parser.
"""
def pending(self) -> int:
"""Return the number of pending messages."""
def __len__(self) -> int:
"""Return the number of pending messages."""
def __iter__(self) -> Generator[Message, Never]:
"""Yield messages that have been parsed so far."""
def parse_all(data: Iterable[int] | None) -> list[Message]:
"""Parse MIDI data and return a list of all messages found.
This is typically used to parse a little bit of data with a few
messages in it. It's best to use a Parser object for larger
amounts of data. Also, tt's often easier to use `parse()` if you
know there is only one message in the data.
"""
def parse(data: Iterable[int] | None) -> Message | None:
"""Parse MIDI data and return the first message found.
Data after the first message is ignored. Use `parse_all()`
to parse more than one message.
"""
+234
View File
@@ -0,0 +1,234 @@
"""
Useful tools for working with ports
"""
import threading
from collections import deque
from collections.abc import Collection, Generator, Iterable
from typing import Any, Literal, Never, Self, overload
from .messages import Message
from .parser import Parser
# How many seconds to sleep before polling again.
_DEFAULT_SLEEP_TIME: float
_sleep_time: int | float
def sleep() -> None:
"""Sleep for N seconds.
This is used in ports when polling and waiting for messages. N can
be set with `set_sleep_time()`."""
def set_sleep_time(seconds: int | float = ...) -> None:
"""Set the number of seconds `sleep()` will sleep."""
def get_sleep_time() -> int | float:
"""Get number of seconds `sleep()` will sleep."""
def reset_messages() -> Generator[Message, Never]:
"""Yield "All Notes Off" and "Reset All Controllers" for all channels"""
def panic_messages() -> Generator[Message, Never]:
"""Yield "All Sounds Off" for all channels.
This will mute all sounding notes regardless of
envelopes. Useful when notes are hanging and nothing else
helps.
"""
class DummyLock:
def __enter__(self) -> Self: ...
def __exit__(self, *_: object) -> Literal[False]: ...
class BasePort:
"""
Abstract base class for Input and Output ports.
"""
is_input: bool = False
is_output: bool = False
_locking: bool = True
name: bool
closed: bool
_lock: DummyLock | threading.RLock
def __init__(self, name: str | None = None, **kwargs: object) -> None: ...
def _open(self, **kwargs: object) -> None: ...
def _close(self) -> None: ...
def close(self) -> None:
"""Close the port.
If the port is already closed, nothing will happen. The port
is automatically closed when the object goes out of scope or
is garbage collected.
"""
def __del__(self) -> None: ...
def __enter__(self) -> Self: ...
def __exit__(self, *_) -> Literal[False]: ...
class BaseInput(BasePort):
"""Base class for input port.
Subclass and override `_receive()` to create a new input port type.
(See `portmidi.py` for an example of how to do this.)
"""
is_input: bool = True
_parser: Parser
_messages: deque[Message]
def __init__(self, name: str = "", **kwargs: Any) -> None:
"""Create an input port.
name is the port name, as returned by `input_names()`. If
name is not passed, the default input is used instead.
"""
def _receive(self, block: bool = True) -> Message: ...
def iter_pending(self) -> Generator[Message, Never]:
"""Iterate through pending messages."""
@overload
def receive(self, block: Literal[True] = True) -> Message:
"""Return the next message.
This will block until a message arrives.
If you pass `block=False` it will not block and instead return
None if there is no available message.
If the port is closed and there are no pending messages IOError
will be raised. If the port closes while waiting inside `receive()`,
IOError will be raised. TODO: this seems a bit inconsistent. Should
different errors be raised? What's most useful here?
"""
@overload
def receive(self, block: bool) -> Message | None: ...
def poll(self) -> Message | None:
"""Receive the next pending message or None
This is the same as calling `receive(block=False)`."""
def __iter__(self) -> Generator[Message, Never]:
"""Iterate through messages until the port closes."""
class BaseOutput(BasePort):
"""
Base class for output port.
Subclass and override `_send()` to create a new port type. (See
`portmidi.py` for how to do this.)
"""
is_output: bool = True
autoreset: bool
def __init__(self, name: str = "", autoreset: bool = False, **kwargs: object) -> None:
"""Create an output port
name is the port name, as returned by `output_names()`. If
name is not passed, the default output is used instead.
"""
def _send(self, msg: Message) -> None: ...
def send(self, msg: Message) -> None:
"""Send a message on the port.
A copy of the message will be sent, so you can safely modify
the original message without any unexpected consequences.
"""
def reset(self) -> None:
"""Send "All Notes Off" and "Reset All Controllers" on all channels"""
def panic(self) -> None:
"""Send "All Sounds Off" on all channels.
This will mute all sounding notes regardless of
envelopes. Useful when notes are hanging and nothing else
helps.
"""
class BaseIOPort(BaseInput, BaseOutput): # pyright: ignore[reportUnsafeMultipleInheritance]
def __init__(self, name: str = "", **kwargs: object) -> None:
"""Create an IO port.
name is the port name, as returned by ioport_names().
"""
# NOTE: Generics not supported in runtime
class IOPort[InT: BaseInput = BaseInput, OutT: BaseOutput = BaseOutput](BaseIOPort):
"""Input / output port.
This is a convenient wrapper around an input port and an output
port which provides the functionality of both. Every method call
is forwarded to the appropriate port.
"""
_locking: bool = False
input: InT
output: OutT
def __init__(self, input: InT, output: OutT) -> None: ...
class EchoPort(BaseIOPort): ...
class MultiPort(BaseIOPort):
def __init__(self, ports: Iterable[BaseIOPort], yield_ports: bool = False) -> None: ...
@overload
def multi_receive[T: BaseInput](
ports: Collection[T], yield_ports: Literal[True], block: bool = True
) -> Generator[tuple[T, Message], Never]: ...
@overload
def multi_receive(
ports: Collection[BaseInput], yield_ports: Literal[False] = False, block: bool = True
) -> Generator[Message, Never]: ...
@overload
def multi_receive[T: BaseInput](
ports: Collection[T], yield_ports: bool, block: bool = True
) -> Generator[tuple[T, Message] | Message, Never]: ...
def multi_receive[T: BaseInput](
ports: Collection[T], yield_ports: bool, block: bool = True
) -> Generator[tuple[T, Message] | Message, Never]:
"""Receive messages from multiple ports.
Generates messages from ever input port. The ports are polled in
random order for fairness, and all messages from each port are
yielded before moving on to the next port.
If `yield_ports=True`, `(port, message)` is yielded instead of just
the message.
If `block=False` only pending messages will be yielded.
"""
@overload
def multi_iter_pending[T: BaseInput](
ports: Collection[T], yield_ports: Literal[True]
) -> Generator[tuple[T, Message], Never]: ...
@overload
def multi_iter_pending(
ports: Collection[BaseInput], yield_ports: Literal[False] = False
) -> Generator[Message, Never]: ...
@overload
def multi_iter_pending[T: BaseInput](
ports: Collection[T], yield_ports: bool
) -> Generator[tuple[T, Message] | Message, Never]: ...
def multi_iter_pending[T: BaseInput](
ports: Collection[T], yield_ports: bool
) -> Generator[tuple[T, Message] | Message, Never]:
"""Iterate through all pending messages in ports.
This is the same as calling `multi_receive(ports, block=False)`.
The function is kept around for backwards compatability.
"""
def multi_send(ports: Iterable[BaseOutput], msg: Message) -> None:
"""Send message on all ports."""
+4
View File
@@ -0,0 +1,4 @@
"""
This type stub file was generated by pyright.
"""
+43
View File
@@ -0,0 +1,43 @@
"""
MIDI over TCP/IP.
"""
import socket
from typing import Literal, overload
from .ports import BaseIOPort, MultiPort
class PortServer(MultiPort):
def __init__(self, host: str, portno: int, backlog: int = 1) -> None: ...
@overload
def accept(self, block: Literal[True] = True) -> SocketPort:
"""
Accept a connection from a client.
Will block until there is a new connection, and then return a
`SocketPort` object.
If `block=False`, `None` will be returned if there is no
new connection waiting.
"""
@overload
def accept(self, block: bool) -> SocketPort | None: ...
class SocketPort(BaseIOPort):
def __init__(self, host: str, portno: int, conn: socket.socket | None = ...) -> None: ...
def connect(host: str, portno: int) -> SocketPort:
"""Connect to a socket port server.
The return value is a `SocketPort` object connected to another
`SocketPort` object at the server end. Messages can be sent either way.
"""
def parse_address(address: str) -> tuple[str, int]:
"""Parse and address on the format host:port.
Returns a tuple (host, port). Raises ValueError if format is
invalid or port is not an integer or out of range.
"""
def format_address(host: str, portno: int) -> str: ...
+28
View File
@@ -0,0 +1,28 @@
"""
Read and write SYX file format
"""
from _typeshed import FileDescriptorOrPath
from .messages import Message
def read_syx_file(filename: FileDescriptorOrPath) -> list[Message]:
"""Read sysex messages from SYX file.
Returns a list of sysex messages.
This handles both the text (hexadecimal) and binary
formats. Messages other than sysex will be ignored. Raises
ValueError if file is plain text and byte is not a 2-digit hex
number.
"""
def write_syx_file(filename: FileDescriptorOrPath, messages: list[Message], plaintext: bool = False) -> None:
"""Write sysex messages to a SYX file.
Messages other than sysex will be skipped.
By default this will write the binary format. Pass
`plaintext=True` to write the plain text format (hex encoded
ASCII text).
"""
+26
View File
@@ -0,0 +1,26 @@
from collections.abc import Generator, Iterable
from numbers import Integral
from typing import Any
class Tokenizer:
"""
Splits a MIDI byte stream into messages.
"""
def __init__(self, data: Iterable[int] | None = ...) -> None:
"""Create a new decoder."""
def feed_byte(self, byte: int | Integral) -> None:
"""Feed MIDI byte to the decoder.
Takes an int in range [0..255].
"""
def feed(self, data: Iterable[int]) -> None:
"""Feed MIDI bytes to the decoder.
Takes an iterable of ints in in range [0..255].
"""
def __len__(self) -> int: ...
def __iter__(self) -> Generator[Any, Any]:
"""Yield messages that have been parsed so far."""
+4
View File
@@ -0,0 +1,4 @@
from packaging.version import Version
__version__: str
version_info: Version