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
+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.
"""