circuitpython-tmidi 2.0.0__py3-none-any.whl → 3.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {circuitpython_tmidi-2.0.0.dist-info → circuitpython_tmidi-3.0.0.dist-info}/METADATA +97 -27
- circuitpython_tmidi-3.0.0.dist-info/RECORD +10 -0
- {circuitpython_tmidi-2.0.0.dist-info → circuitpython_tmidi-3.0.0.dist-info}/WHEEL +1 -1
- tmidi/__init__.py +643 -0
- tmidi/tmidi_kernel.armv6m.mpy +0 -0
- tmidi/tmidi_kernel.armv7emsp.mpy +0 -0
- tmidi/tmidi_kernel.rv32imc.mpy +0 -0
- tmidi/tmidi_kernel.xtensawin.mpy +0 -0
- circuitpython_tmidi-2.0.0.dist-info/RECORD +0 -6
- tmidi.py +0 -423
- {circuitpython_tmidi-2.0.0.dist-info → circuitpython_tmidi-3.0.0.dist-info}/licenses/LICENSE +0 -0
- {circuitpython_tmidi-2.0.0.dist-info → circuitpython_tmidi-3.0.0.dist-info}/top_level.txt +0 -0
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: circuitpython-tmidi
|
|
3
|
-
Version:
|
|
3
|
+
Version: 3.0.0
|
|
4
4
|
Summary: MIDI library for CircuitPython
|
|
5
5
|
Author-email: Tod Kurt <tod@todbot.com>
|
|
6
|
-
License: MIT
|
|
6
|
+
License-Expression: MIT
|
|
7
7
|
Project-URL: Homepage, https://github.com/todbot/CircuitPython_TMIDI
|
|
8
8
|
Keywords: adafruit,blinka,circuitpython,micropython,tmidi,midi
|
|
9
9
|
Classifier: Intended Audience :: Developers
|
|
10
10
|
Classifier: Topic :: Software Development :: Libraries
|
|
11
11
|
Classifier: Topic :: Software Development :: Embedded Systems
|
|
12
12
|
Classifier: Topic :: System :: Hardware
|
|
13
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
14
13
|
Classifier: Programming Language :: Python :: 3
|
|
15
14
|
Description-Content-Type: text/x-rst
|
|
16
15
|
License-File: LICENSE
|
|
@@ -37,6 +36,100 @@ Introduction
|
|
|
37
36
|
|
|
38
37
|
MIDI library for CircuitPython
|
|
39
38
|
|
|
39
|
+
TMIDI is a MIDI library for CircuitPython / MicroPython. It attempts to
|
|
40
|
+
be a provide a simpler API and perform faster, especially for MIDI parsing,
|
|
41
|
+
compared to ``adafruit_midi`` (`adafruit_midi github <https://github.com/adafruit/Adafruit_CircuitPython_MIDI/>`_)
|
|
42
|
+
TMIDI is derived from ``winterbloom_smolmidi``
|
|
43
|
+
(`smolmidi github <https://github.com/wntrblm/Winterbloom_SmolMIDI>`_)
|
|
44
|
+
by Thea Flowers for Winterbloom.
|
|
45
|
+
|
|
46
|
+
Like ``adafruit_midi``, TMIDI works on CircuitPython's ``usb_midi`` and ``busio.UART``,
|
|
47
|
+
or any stream-like object that supports ``.readinto()`` and ``.write()``
|
|
48
|
+
|
|
49
|
+
Usage Example
|
|
50
|
+
=============
|
|
51
|
+
|
|
52
|
+
Sending MIDI over USB. Channels are 0-indexed, so ``channel=0`` is
|
|
53
|
+
MIDI channel 1:
|
|
54
|
+
|
|
55
|
+
.. code-block:: python
|
|
56
|
+
|
|
57
|
+
import time
|
|
58
|
+
import usb_midi
|
|
59
|
+
import tmidi
|
|
60
|
+
|
|
61
|
+
midi = tmidi.MIDI(midi_out=usb_midi.ports[1])
|
|
62
|
+
|
|
63
|
+
while True:
|
|
64
|
+
midi.send(tmidi.Message(tmidi.NOTE_ON, 60, 100, channel=0))
|
|
65
|
+
time.sleep(0.5)
|
|
66
|
+
midi.send(tmidi.Message(tmidi.NOTE_OFF, 60, 0, channel=0))
|
|
67
|
+
time.sleep(0.5)
|
|
68
|
+
|
|
69
|
+
Receiving MIDI over USB. ``receive()`` returns ``None`` when nothing
|
|
70
|
+
has arrived:
|
|
71
|
+
|
|
72
|
+
.. code-block:: python
|
|
73
|
+
|
|
74
|
+
import usb_midi
|
|
75
|
+
import tmidi
|
|
76
|
+
|
|
77
|
+
midi = tmidi.MIDI(midi_in=usb_midi.ports[0])
|
|
78
|
+
|
|
79
|
+
while True:
|
|
80
|
+
if msg := midi.receive():
|
|
81
|
+
if msg.type == tmidi.NOTE_ON:
|
|
82
|
+
print("note on:", msg.note, msg.velocity, msg.channel)
|
|
83
|
+
else:
|
|
84
|
+
print(msg)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
Callbacks
|
|
88
|
+
=========
|
|
89
|
+
|
|
90
|
+
Instead of ``receive()``, register functions for the message types you want
|
|
91
|
+
and call ``poll()``. No ``Message`` objects are allocated.
|
|
92
|
+
|
|
93
|
+
.. code-block:: python
|
|
94
|
+
|
|
95
|
+
def note_on(channel, note, velocity):
|
|
96
|
+
print("note on:", channel, note, velocity)
|
|
97
|
+
|
|
98
|
+
midi.on(tmidi.NOTE_ON, note_on)
|
|
99
|
+
while True:
|
|
100
|
+
midi.poll()
|
|
101
|
+
|
|
102
|
+
See ``examples/tmidi_simple_poll.py`` for a full example.
|
|
103
|
+
|
|
104
|
+
Turbo parser
|
|
105
|
+
============
|
|
106
|
+
|
|
107
|
+
TMIDI runs entirely in Python, but uses a faster ``@micropython.viper``
|
|
108
|
+
parser when your firmware can load native code. The parsers ship inside the
|
|
109
|
+
``tmidi/`` library folder as ``tmidi_kernel.<arch>.mpy``, and CircuitPython
|
|
110
|
+
picks the one for your board. ``tmidi.ENGINE`` says which parser is in use.
|
|
111
|
+
|
|
112
|
+
TMIDI 3.0 is a folder, ``lib/tmidi/``, instead of a single ``lib/tmidi.mpy``.
|
|
113
|
+
Delete an old ``lib/tmidi.mpy`` when upgrading.
|
|
114
|
+
|
|
115
|
+
====================== =====================================
|
|
116
|
+
``tmidi_kernel`` file Boards
|
|
117
|
+
====================== =====================================
|
|
118
|
+
``armv6m`` RP2040
|
|
119
|
+
``armv7emsp`` SAMD51, nRF52840, RP2350, STM32F4
|
|
120
|
+
``xtensawin`` ESP32-S2, ESP32-S3
|
|
121
|
+
``rv32imc`` ESP32-C3, C5, C6
|
|
122
|
+
====================== =====================================
|
|
123
|
+
|
|
124
|
+
To rebuild them from ``src/tmidi_turbo.py``, use an ``mpy-cross`` from the
|
|
125
|
+
CircuitPython version they will run on. The files land in ``tmidi/``:
|
|
126
|
+
|
|
127
|
+
.. code-block:: shell
|
|
128
|
+
|
|
129
|
+
cd src
|
|
130
|
+
make MPY_CROSS=path/to/circuitpython/mpy-cross/build/mpy-cross
|
|
131
|
+
|
|
132
|
+
|
|
40
133
|
|
|
41
134
|
Dependencies
|
|
42
135
|
=============
|
|
@@ -99,28 +192,6 @@ Or the following command to update an existing version:
|
|
|
99
192
|
|
|
100
193
|
circup update
|
|
101
194
|
|
|
102
|
-
Usage Example
|
|
103
|
-
=============
|
|
104
|
-
|
|
105
|
-
.. code-block:: python
|
|
106
|
-
|
|
107
|
-
import usb_midi
|
|
108
|
-
import tmidi
|
|
109
|
-
|
|
110
|
-
midi_usb = tmidi.MIDI(midi_in=usb_midi.ports[0], midi_out=usb_midi.ports[1])
|
|
111
|
-
|
|
112
|
-
def send_midi_panic(midi_channel=1):
|
|
113
|
-
ch = midi_channel-1
|
|
114
|
-
msg = tmidi.Message(tmidi.NOTE_OFF, channel=ch) # channels are 0-indexed
|
|
115
|
-
for notenum in range(128):
|
|
116
|
-
msg.note = notenum
|
|
117
|
-
msg.velocity = 0
|
|
118
|
-
midi_usb.send(msg)
|
|
119
|
-
time.sleep(0.01)
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
send_midi_panic()
|
|
123
|
-
|
|
124
195
|
|
|
125
196
|
Documentation
|
|
126
197
|
=============
|
|
@@ -132,8 +203,7 @@ For information on building library documentation, please check out
|
|
|
132
203
|
Testing
|
|
133
204
|
=======
|
|
134
205
|
|
|
135
|
-
Install ``pytest`` with ``pip3 install pytest --upgrade`` and run ``pytest -v``
|
|
136
|
-
To build docs:
|
|
206
|
+
Install ``pytest`` with ``pip3 install pytest circuitpython-mocks --upgrade`` and run ``pytest -v``
|
|
137
207
|
|
|
138
208
|
Contributing
|
|
139
209
|
============
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
circuitpython_tmidi-3.0.0.dist-info/licenses/LICENSE,sha256=jhhRyxpqyQxyF_D6SzY2-jCA5v8Z8JXfezCtxBS85CM,1075
|
|
2
|
+
tmidi/__init__.py,sha256=zYkXV7fPwddPiDMCsSXOANIgH9TCFKcXzatkBQoc9x4,19833
|
|
3
|
+
tmidi/tmidi_kernel.armv6m.mpy,sha256=OCmHancSoUi1NzRiPc7gUmBJlMsdlNh0rWwp-oSh88U,1238
|
|
4
|
+
tmidi/tmidi_kernel.armv7emsp.mpy,sha256=X-Go1gkD--_t3ZldGvl8i9HUJ-W3r5IKijI-lZZLLRA,992
|
|
5
|
+
tmidi/tmidi_kernel.rv32imc.mpy,sha256=_XAqmSPDTpmEngrJ5m7rorq_mh5snhellBiPYIgVCds,1344
|
|
6
|
+
tmidi/tmidi_kernel.xtensawin.mpy,sha256=KMWvzgLML75mXVZ05PeWsjlKqVNagJOLj2kqBEHnljY,1145
|
|
7
|
+
circuitpython_tmidi-3.0.0.dist-info/METADATA,sha256=UCS6X4Xa_DRFq5PwPHxiwlLi_gOo00Q-UNx5xd9pnFE,6575
|
|
8
|
+
circuitpython_tmidi-3.0.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
|
|
9
|
+
circuitpython_tmidi-3.0.0.dist-info/top_level.txt,sha256=ey35b95sYIkXnGcYlLMoLxC4R4Z_waH1rnvn3ntCW5o,6
|
|
10
|
+
circuitpython_tmidi-3.0.0.dist-info/RECORD,,
|
tmidi/__init__.py
ADDED
|
@@ -0,0 +1,643 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: Copyright (c) 2019 Alethea Flowers for Winterbloom
|
|
2
|
+
# SPDX-FileCopyrightText: Copyright (c) 2026 Tod Kurt
|
|
3
|
+
#
|
|
4
|
+
# SPDX-License-Identifier: MIT
|
|
5
|
+
"""
|
|
6
|
+
`tmidi`
|
|
7
|
+
================================================================================
|
|
8
|
+
|
|
9
|
+
MIDI library for CircuitPython
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
* Author(s): Tod Kurt, with code from Alethea Flowers for Winterbloom
|
|
13
|
+
|
|
14
|
+
* Portions of this library come from Winterbloom_SmolMIDI:
|
|
15
|
+
https://github.com/wntrblm/Winterbloom_SmolMIDI
|
|
16
|
+
|
|
17
|
+
Implementation Notes
|
|
18
|
+
--------------------
|
|
19
|
+
|
|
20
|
+
**Hardware:**
|
|
21
|
+
|
|
22
|
+
* Native USB for USB MIDI
|
|
23
|
+
* UART for Serial MIDI
|
|
24
|
+
* PIO-USB or MAX3421E USB Host for USB Host MIDI
|
|
25
|
+
|
|
26
|
+
**Software and Dependencies:**
|
|
27
|
+
|
|
28
|
+
* Adafruit CircuitPython firmware for the supported boards:
|
|
29
|
+
https://circuitpython.org/downloads
|
|
30
|
+
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
# imports
|
|
34
|
+
|
|
35
|
+
__version__ = "3.0.0"
|
|
36
|
+
__repo__ = "https://github.com/todbot/CircuitPython_TMIDI.git"
|
|
37
|
+
|
|
38
|
+
# Portions of this library come from Winterbloom_SmolMIDI:
|
|
39
|
+
#
|
|
40
|
+
# The MIT License (MIT)
|
|
41
|
+
#
|
|
42
|
+
# Copyright (c) 2019 Alethea Flowers for Winterbloom
|
|
43
|
+
#
|
|
44
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
45
|
+
# of this software and associated documentation files (the "Software"), to deal
|
|
46
|
+
# in the Software without restriction, including without limitation the rights
|
|
47
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
48
|
+
# copies of the Software, and to permit persons to whom the Software is
|
|
49
|
+
# furnished to do so, subject to the following conditions:
|
|
50
|
+
#
|
|
51
|
+
# The above copyright notice and this permission notice shall be included in
|
|
52
|
+
# all copies or substantial portions of the Software.
|
|
53
|
+
#
|
|
54
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
55
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
56
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
57
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
58
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
59
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
60
|
+
# THE SOFTWARE.
|
|
61
|
+
|
|
62
|
+
from array import array
|
|
63
|
+
|
|
64
|
+
from micropython import const
|
|
65
|
+
|
|
66
|
+
# Prebuilt per chip as tmidi_kernel.<arch>.mpy from src/tmidi_turbo.py.
|
|
67
|
+
# Missing on Blinka and on firmware without native code.
|
|
68
|
+
try:
|
|
69
|
+
from tmidi.tmidi_kernel import parse as _kernel_parse
|
|
70
|
+
except (ImportError, ValueError):
|
|
71
|
+
_kernel_parse = None
|
|
72
|
+
|
|
73
|
+
ENGINE = "Python" if _kernel_parse is None else "viper"
|
|
74
|
+
"""``"viper"`` when the turbo parser loaded, else ``"Python"``."""
|
|
75
|
+
|
|
76
|
+
# Message type constants.
|
|
77
|
+
NOTE_OFF = const(0x80)
|
|
78
|
+
"""Note Off"""
|
|
79
|
+
NOTE_ON = const(0x90)
|
|
80
|
+
"""Note On"""
|
|
81
|
+
AFTERTOUCH = const(0xA0)
|
|
82
|
+
"""Aftertouch"""
|
|
83
|
+
CONTROLLER_CHANGE = CC = const(0xB0)
|
|
84
|
+
"""Controller Change"""
|
|
85
|
+
PROGRAM_CHANGE = const(0xC0)
|
|
86
|
+
"""Program Change"""
|
|
87
|
+
CHANNEL_PRESSURE = const(0xD0)
|
|
88
|
+
"""Channel Pressure"""
|
|
89
|
+
PITCH_BEND = const(0xE0)
|
|
90
|
+
"""Pitch Bend"""
|
|
91
|
+
SYSTEM_EXCLUSIVE = SYSEX = const(0xF0)
|
|
92
|
+
"""Sysex"""
|
|
93
|
+
SONG_POSITION = const(0xF2)
|
|
94
|
+
"""Song Position"""
|
|
95
|
+
SONG_SELECT = const(0xF3)
|
|
96
|
+
"""Song Select"""
|
|
97
|
+
BUS_SELECT = const(0xF5)
|
|
98
|
+
"""BUS Select"""
|
|
99
|
+
TUNE_REQUEST = const(0xF6)
|
|
100
|
+
"""Tune Request"""
|
|
101
|
+
SYSEX_END = const(0xF7)
|
|
102
|
+
"""Sysex End"""
|
|
103
|
+
CLOCK = const(0xF8)
|
|
104
|
+
"""Clock"""
|
|
105
|
+
TICK = const(0xF9)
|
|
106
|
+
"""Tick"""
|
|
107
|
+
START = const(0xFA)
|
|
108
|
+
"""Start"""
|
|
109
|
+
CONTINUE = const(0xFB)
|
|
110
|
+
"""Continue"""
|
|
111
|
+
STOP = const(0xFC)
|
|
112
|
+
"""Stop"""
|
|
113
|
+
ACTIVE_SENSING = const(0xFE)
|
|
114
|
+
"""Active Sensing"""
|
|
115
|
+
SYSTEM_RESET = const(0xFF)
|
|
116
|
+
"""System Reset"""
|
|
117
|
+
|
|
118
|
+
_MSG_DATA_LEN = {
|
|
119
|
+
NOTE_OFF: 2,
|
|
120
|
+
NOTE_ON: 2,
|
|
121
|
+
AFTERTOUCH: 2,
|
|
122
|
+
CC: 2,
|
|
123
|
+
PITCH_BEND: 2,
|
|
124
|
+
SONG_POSITION: 2,
|
|
125
|
+
PROGRAM_CHANGE: 1,
|
|
126
|
+
CHANNEL_PRESSURE: 1,
|
|
127
|
+
SONG_SELECT: 1,
|
|
128
|
+
BUS_SELECT: 1,
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
# Pre-allocated scratch buffer for __bytes__() — avoids per-call list allocation.
|
|
132
|
+
# Safe because CircuitPython is single-threaded.
|
|
133
|
+
_msg_buf = bytearray(3)
|
|
134
|
+
|
|
135
|
+
_MSG_TYPE_NAMES = {
|
|
136
|
+
NOTE_OFF: "NoteOff",
|
|
137
|
+
NOTE_ON: "NoteOn",
|
|
138
|
+
AFTERTOUCH: "Aftertouch",
|
|
139
|
+
CC: "CC",
|
|
140
|
+
PROGRAM_CHANGE: "ProgramChange",
|
|
141
|
+
CHANNEL_PRESSURE: "ChannelPressure",
|
|
142
|
+
PITCH_BEND: "PitchBend",
|
|
143
|
+
SYSEX: "Sysex",
|
|
144
|
+
SONG_POSITION: "SongPosition",
|
|
145
|
+
SONG_SELECT: "SongSelect",
|
|
146
|
+
BUS_SELECT: "BusSelect",
|
|
147
|
+
TUNE_REQUEST: "TuneRequest",
|
|
148
|
+
SYSEX_END: "SysexEnd",
|
|
149
|
+
CLOCK: "Clock",
|
|
150
|
+
TICK: "Tick",
|
|
151
|
+
START: "Start",
|
|
152
|
+
CONTINUE: "Continue",
|
|
153
|
+
STOP: "Stop",
|
|
154
|
+
ACTIVE_SENSING: "ActiveSensing",
|
|
155
|
+
SYSTEM_RESET: "SystemReset",
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _is_channel_message(status_byte):
|
|
160
|
+
return status_byte >= NOTE_OFF and status_byte < SYSEX
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
# Keep in sync with the _ST_ slots in src/tmidi_turbo.py
|
|
164
|
+
_ST_RD = const(0)
|
|
165
|
+
_ST_WR = const(1)
|
|
166
|
+
_ST_STATUS = const(2)
|
|
167
|
+
_ST_NEED = const(3)
|
|
168
|
+
_ST_DATA0 = const(4)
|
|
169
|
+
_ST_SX_LEN = const(5)
|
|
170
|
+
_ST_SX_CAP = const(6)
|
|
171
|
+
_ST_ERRORS = const(7)
|
|
172
|
+
_ST_RUNNING = const(8)
|
|
173
|
+
_ST_RUNNING_ON = const(9)
|
|
174
|
+
_ST_SIZE = const(10)
|
|
175
|
+
|
|
176
|
+
_NEED_ONE = const(1)
|
|
177
|
+
_NEED_FIRST = const(2)
|
|
178
|
+
_NEED_SECOND = const(3)
|
|
179
|
+
|
|
180
|
+
# status bytes the parser tests for, keep in sync with src/tmidi_turbo.py
|
|
181
|
+
_B_STATUS = const(0x80) # lowest status byte, data bytes are below
|
|
182
|
+
_B_SYSTEM = const(0xF0) # first system message, channel messages are below
|
|
183
|
+
_B_SYSEX = const(0xF0)
|
|
184
|
+
_B_SONG_POSITION = const(0xF2)
|
|
185
|
+
_B_SONG_SELECT = const(0xF3)
|
|
186
|
+
_B_BUS_SELECT = const(0xF5)
|
|
187
|
+
_B_SYSEX_END = const(0xF7)
|
|
188
|
+
_B_REALTIME = const(0xF8) # first real-time message
|
|
189
|
+
_B_ONE_DATA_MASK = const(0xE0)
|
|
190
|
+
_B_ONE_DATA = const(0xC0) # PROGRAM_CHANGE and CHANNEL_PRESSURE, masked
|
|
191
|
+
|
|
192
|
+
_READ_SIZE = const(64)
|
|
193
|
+
_MAX_SYSEX = const(65535)
|
|
194
|
+
|
|
195
|
+
_NO_SYSEX = bytearray(0)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _parse_py(buf, st, sx):
|
|
199
|
+
"""Return next message from buf as status<<16|data0<<8|data1, 0 if drained.
|
|
200
|
+
|
|
201
|
+
Keep in sync with the viper version in src/tmidi_turbo.py.
|
|
202
|
+
"""
|
|
203
|
+
rd = st[_ST_RD]
|
|
204
|
+
wr = st[_ST_WR]
|
|
205
|
+
status = st[_ST_STATUS]
|
|
206
|
+
need = st[_ST_NEED]
|
|
207
|
+
|
|
208
|
+
while rd < wr:
|
|
209
|
+
b = buf[rd]
|
|
210
|
+
rd += 1
|
|
211
|
+
|
|
212
|
+
# real-time can land inside any message and must not disturb it
|
|
213
|
+
if b >= _B_REALTIME:
|
|
214
|
+
st[_ST_RD] = rd
|
|
215
|
+
st[_ST_STATUS] = status
|
|
216
|
+
st[_ST_NEED] = need
|
|
217
|
+
return b << 16
|
|
218
|
+
|
|
219
|
+
if b >= _B_STATUS:
|
|
220
|
+
if b == _B_SYSEX_END and status == _B_SYSEX:
|
|
221
|
+
st[_ST_RD] = rd
|
|
222
|
+
st[_ST_STATUS] = 0
|
|
223
|
+
st[_ST_NEED] = 0
|
|
224
|
+
return _B_SYSEX << 16 | st[_ST_SX_LEN]
|
|
225
|
+
|
|
226
|
+
if status != 0:
|
|
227
|
+
st[_ST_ERRORS] = (st[_ST_ERRORS] + 1) & 0xFFFF
|
|
228
|
+
|
|
229
|
+
status = b
|
|
230
|
+
if b < _B_SYSTEM:
|
|
231
|
+
if st[_ST_RUNNING_ON] != 0:
|
|
232
|
+
st[_ST_RUNNING] = b
|
|
233
|
+
if (b & _B_ONE_DATA_MASK) == _B_ONE_DATA:
|
|
234
|
+
need = _NEED_ONE
|
|
235
|
+
else:
|
|
236
|
+
need = _NEED_FIRST
|
|
237
|
+
continue
|
|
238
|
+
|
|
239
|
+
st[_ST_RUNNING] = 0
|
|
240
|
+
if b == _B_SYSEX:
|
|
241
|
+
need = 0
|
|
242
|
+
st[_ST_SX_LEN] = 0
|
|
243
|
+
continue
|
|
244
|
+
if b == _B_SONG_POSITION:
|
|
245
|
+
need = _NEED_FIRST
|
|
246
|
+
continue
|
|
247
|
+
if b == _B_SONG_SELECT or b == _B_BUS_SELECT:
|
|
248
|
+
need = _NEED_ONE
|
|
249
|
+
continue
|
|
250
|
+
|
|
251
|
+
st[_ST_RD] = rd
|
|
252
|
+
st[_ST_STATUS] = 0
|
|
253
|
+
st[_ST_NEED] = 0
|
|
254
|
+
return b << 16
|
|
255
|
+
|
|
256
|
+
if status == _B_SYSEX:
|
|
257
|
+
n = st[_ST_SX_LEN]
|
|
258
|
+
if n < st[_ST_SX_CAP]:
|
|
259
|
+
sx[n] = b
|
|
260
|
+
st[_ST_SX_LEN] = n + 1
|
|
261
|
+
continue
|
|
262
|
+
|
|
263
|
+
if status == 0:
|
|
264
|
+
status = st[_ST_RUNNING]
|
|
265
|
+
if status == 0:
|
|
266
|
+
st[_ST_ERRORS] = (st[_ST_ERRORS] + 1) & 0xFFFF
|
|
267
|
+
continue
|
|
268
|
+
if (status & _B_ONE_DATA_MASK) == _B_ONE_DATA:
|
|
269
|
+
need = _NEED_ONE
|
|
270
|
+
else:
|
|
271
|
+
need = _NEED_FIRST
|
|
272
|
+
|
|
273
|
+
if need == _NEED_FIRST:
|
|
274
|
+
st[_ST_DATA0] = b
|
|
275
|
+
need = _NEED_SECOND
|
|
276
|
+
continue
|
|
277
|
+
|
|
278
|
+
st[_ST_RD] = rd
|
|
279
|
+
st[_ST_STATUS] = 0
|
|
280
|
+
st[_ST_NEED] = 0
|
|
281
|
+
if need == _NEED_SECOND:
|
|
282
|
+
return status << 16 | st[_ST_DATA0] << 8 | b
|
|
283
|
+
return status << 16 | b << 8
|
|
284
|
+
|
|
285
|
+
st[_ST_RD] = rd
|
|
286
|
+
st[_ST_STATUS] = status
|
|
287
|
+
st[_ST_NEED] = need
|
|
288
|
+
return 0
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
class Message:
|
|
292
|
+
"""
|
|
293
|
+
MIDI Message.
|
|
294
|
+
|
|
295
|
+
:param mtype: The type of message, e.g. tmidi.NOTE_ON.
|
|
296
|
+
:param data0: The first data byte for this message,
|
|
297
|
+
e.g. the note number as an ``int`` (0-127) for NOTE_ON messages.
|
|
298
|
+
:param data1: The second data byte for this message,
|
|
299
|
+
e.g. the velocity (0-127) for NOTE_ON messages.
|
|
300
|
+
:param channel: The MIDI channel for this message, if applicable (0-15)
|
|
301
|
+
|
|
302
|
+
Example of creating Messages:
|
|
303
|
+
|
|
304
|
+
.. code-block:: python
|
|
305
|
+
|
|
306
|
+
# create Note On middle-C message on ch 1 (0-indexed)
|
|
307
|
+
m = Message(tmidi.NOTE_ON, 60, 127, channel=0)
|
|
308
|
+
# create CC 74 with val 63 on ch 4
|
|
309
|
+
m = Message(tmidi.CC, 74, 63, channel=4-1)
|
|
310
|
+
# create a pitch bend full up on ch 1
|
|
311
|
+
m = Message(tmidi.PITCH_BEND, 8191)
|
|
312
|
+
|
|
313
|
+
Example of creating accessing Message attributes:
|
|
314
|
+
|
|
315
|
+
.. code-block:: python
|
|
316
|
+
|
|
317
|
+
if msg := midi.receive():
|
|
318
|
+
if msg.type == tmidi.NOTE_ON:
|
|
319
|
+
print("note on:", msg.note, msg.velocity)
|
|
320
|
+
elif msg.type == tmidi.NOTE_OFF:
|
|
321
|
+
print("note off:", msg.note, msg.velocity)
|
|
322
|
+
elif msg.type == tmidi.PROGRAM_CHANGE:
|
|
323
|
+
print("program change:", msg.value)
|
|
324
|
+
"""
|
|
325
|
+
|
|
326
|
+
def __init__(self, mtype=SYSTEM_RESET, data0=0, data1=0, channel=0):
|
|
327
|
+
self.type = mtype
|
|
328
|
+
self.channel = channel
|
|
329
|
+
self.data0 = data0
|
|
330
|
+
self.data1 = data1
|
|
331
|
+
if mtype == PITCH_BEND and data1 == 0 and not (0 <= data0 <= 0x7F):
|
|
332
|
+
self.pitch_bend = data0
|
|
333
|
+
|
|
334
|
+
def __bytes__(self):
|
|
335
|
+
status_byte = self.type
|
|
336
|
+
if _is_channel_message(status_byte):
|
|
337
|
+
status_byte |= self.channel
|
|
338
|
+
_msg_buf[0] = status_byte
|
|
339
|
+
n = _MSG_DATA_LEN.get(self.type, 0)
|
|
340
|
+
if n == 2:
|
|
341
|
+
_msg_buf[1] = self.data0
|
|
342
|
+
_msg_buf[2] = self.data1
|
|
343
|
+
return bytes(_msg_buf)
|
|
344
|
+
if n == 1:
|
|
345
|
+
_msg_buf[1] = self.data0
|
|
346
|
+
return bytes(_msg_buf[:2])
|
|
347
|
+
return bytes(_msg_buf[:1])
|
|
348
|
+
|
|
349
|
+
def __repr__(self):
|
|
350
|
+
return self.__str__()
|
|
351
|
+
|
|
352
|
+
def __str__(self):
|
|
353
|
+
mtype = self.type
|
|
354
|
+
type_str = "Message(" + _MSG_TYPE_NAMES.get(mtype, "Unknown")
|
|
355
|
+
ch_str = "ch:%d" % self.channel if _is_channel_message(mtype) else "-"
|
|
356
|
+
if mtype == PITCH_BEND:
|
|
357
|
+
return "%s %s %d)" % (type_str, ch_str, self.pitch_bend)
|
|
358
|
+
if mtype == SYSEX:
|
|
359
|
+
if self.data0:
|
|
360
|
+
return "%s %d bytes)" % (type_str, self.data0)
|
|
361
|
+
return "%s)" % type_str
|
|
362
|
+
n = _MSG_DATA_LEN.get(mtype, 0)
|
|
363
|
+
if n == 2:
|
|
364
|
+
return "%s %s %d %d)" % (type_str, ch_str, self.data0, self.data1)
|
|
365
|
+
if n == 1:
|
|
366
|
+
return "%s %s %d)" % (type_str, ch_str, self.data0)
|
|
367
|
+
return "%s)" % type_str
|
|
368
|
+
|
|
369
|
+
@property
|
|
370
|
+
def note(self):
|
|
371
|
+
"""MIDI note number of message (only valid for NOTE_ON/NOTE_OFF msgs)"""
|
|
372
|
+
return self.data0
|
|
373
|
+
|
|
374
|
+
@note.setter
|
|
375
|
+
def note(self, notenum):
|
|
376
|
+
self.data0 = notenum
|
|
377
|
+
|
|
378
|
+
@property
|
|
379
|
+
def velocity(self):
|
|
380
|
+
"""MIDI velocity of message (only valid for NOTE_ON/NOTE_OFF msgs)"""
|
|
381
|
+
return self.data1
|
|
382
|
+
|
|
383
|
+
@velocity.setter
|
|
384
|
+
def velocity(self, vel):
|
|
385
|
+
self.data1 = vel
|
|
386
|
+
|
|
387
|
+
@property
|
|
388
|
+
def pitch_bend(self):
|
|
389
|
+
"""Pitch bend value of message (only valid for PITCH_BEND msgs)"""
|
|
390
|
+
return (self.data1 << 7 | self.data0) - 8192
|
|
391
|
+
|
|
392
|
+
@pitch_bend.setter
|
|
393
|
+
def pitch_bend(self, pbval):
|
|
394
|
+
pbval += 8192
|
|
395
|
+
self.data0 = pbval & 0x7F
|
|
396
|
+
self.data1 = pbval >> 7
|
|
397
|
+
|
|
398
|
+
@property
|
|
399
|
+
def value(self):
|
|
400
|
+
"""Message value, only valid for len1 messages (Program Change, etc)"""
|
|
401
|
+
return self.data0
|
|
402
|
+
|
|
403
|
+
@value.setter
|
|
404
|
+
def value(self, val):
|
|
405
|
+
self.data0 = val
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
class MIDI:
|
|
409
|
+
"""
|
|
410
|
+
MIDI Parser, receiver and sender
|
|
411
|
+
``midi_in`` or ``midi_out`` *must* be set or both together.
|
|
412
|
+
|
|
413
|
+
:param midi_in: an object which implements ``read(length)``,
|
|
414
|
+
set to ``usb_midi.ports[0]`` for USB MIDI, default None.
|
|
415
|
+
:param midi_out: an object which implements ``write(buffer, length)``,
|
|
416
|
+
set to ``usb_midi.ports[1]`` for USB MIDI, default None.
|
|
417
|
+
:param bool enable_running_status: Allow running status messages to work, default False.
|
|
418
|
+
:param bytearray sysex_buffer: Optional pre-allocated buffer for capturing SysEx payloads.
|
|
419
|
+
When provided, received SysEx payload bytes are written into this buffer and
|
|
420
|
+
``msg.data0`` on the returned message holds the number of bytes written.
|
|
421
|
+
If the payload exceeds the buffer length, the overflow is consumed and discarded
|
|
422
|
+
so the stream stays in sync. When ``None`` (default), SysEx payloads are discarded
|
|
423
|
+
and ``msg.data0`` is 0.
|
|
424
|
+
:param bool turbo: Use the viper parser if it loaded (see ``ENGINE``), default True.
|
|
425
|
+
|
|
426
|
+
Example of receiving with callbacks, which avoids allocating ``Message`` objects:
|
|
427
|
+
|
|
428
|
+
.. code-block:: python
|
|
429
|
+
|
|
430
|
+
import usb_midi
|
|
431
|
+
import tmidi
|
|
432
|
+
|
|
433
|
+
def note_on(channel, note, velocity):
|
|
434
|
+
print("note on:", channel, note, velocity)
|
|
435
|
+
|
|
436
|
+
midi = tmidi.MIDI(midi_in=usb_midi.ports[0])
|
|
437
|
+
midi.on(tmidi.NOTE_ON, note_on)
|
|
438
|
+
|
|
439
|
+
while True:
|
|
440
|
+
midi.poll()
|
|
441
|
+
|
|
442
|
+
Example of receiving SysEx with payload capture:
|
|
443
|
+
|
|
444
|
+
.. code-block:: python
|
|
445
|
+
|
|
446
|
+
import usb_midi
|
|
447
|
+
import tmidi
|
|
448
|
+
sysex_buf = bytearray(128)
|
|
449
|
+
midi = tmidi.MIDI(midi_in=usb_midi.ports[0], sysex_buffer=sysex_buf)
|
|
450
|
+
|
|
451
|
+
while True:
|
|
452
|
+
if msg := midi.receive():
|
|
453
|
+
if msg.type == tmidi.SYSEX:
|
|
454
|
+
payload = memoryview(sysex_buf)[: msg.data0]
|
|
455
|
+
print("SysEx:", list(payload))
|
|
456
|
+
|
|
457
|
+
Example of sending MIDI over USB:
|
|
458
|
+
|
|
459
|
+
.. code-block:: python
|
|
460
|
+
|
|
461
|
+
import usb_midi
|
|
462
|
+
import tmidi
|
|
463
|
+
midi_usb = tmidi.MIDI(midi_out=usb_midi.ports[1])
|
|
464
|
+
msg_on = tmidi.Message(tmidi.NOTE_ON, notenum, velocity)
|
|
465
|
+
midi_usb.send(msg_on)
|
|
466
|
+
|
|
467
|
+
Example of sending MIDI over UART (TRS or 5-pin):
|
|
468
|
+
|
|
469
|
+
.. code-block:: python
|
|
470
|
+
|
|
471
|
+
import board
|
|
472
|
+
import busio
|
|
473
|
+
import tmidi
|
|
474
|
+
uart = busio.UART(tx=board.TX, rx=board.RX, timeout=0.001)
|
|
475
|
+
midi_uart = tmidi.MIDI(midi_out=uart)
|
|
476
|
+
msg_on = tmidi.Message(tmidi.NOTE_ON, notenum, velocity)
|
|
477
|
+
midi_uart.send(msg_on)
|
|
478
|
+
|
|
479
|
+
Example of receiving MIDI:
|
|
480
|
+
|
|
481
|
+
.. code-block:: python
|
|
482
|
+
|
|
483
|
+
import board
|
|
484
|
+
import busio
|
|
485
|
+
import usb_midi
|
|
486
|
+
import tmidi
|
|
487
|
+
uart = busio.UART(tx=board.TX, rx=board.RX, timeout=0.001)
|
|
488
|
+
midi_uart = tmidi.MIDI(midi_in=uart, midi_out=uart)
|
|
489
|
+
midi_usb = tmidi.MIDI(midi_in=usb_midi.ports[0], midi_out=usb_midi.ports[1])
|
|
490
|
+
|
|
491
|
+
while True:
|
|
492
|
+
if msg := midi_usb.receive():
|
|
493
|
+
print("usb midi:", msg)
|
|
494
|
+
if msg := midi_uart.receive():
|
|
495
|
+
print("uart midi:", msg)
|
|
496
|
+
"""
|
|
497
|
+
|
|
498
|
+
def __init__(
|
|
499
|
+
self,
|
|
500
|
+
midi_in=None,
|
|
501
|
+
midi_out=None,
|
|
502
|
+
enable_running_status=False,
|
|
503
|
+
sysex_buffer=None,
|
|
504
|
+
turbo=True,
|
|
505
|
+
):
|
|
506
|
+
self._in_port = midi_in
|
|
507
|
+
self._out_port = midi_out
|
|
508
|
+
self._has_in_waiting = hasattr(midi_in, "in_waiting")
|
|
509
|
+
self._parse = _kernel_parse if turbo and _kernel_parse else _parse_py
|
|
510
|
+
self._callbacks = {}
|
|
511
|
+
|
|
512
|
+
self._bulk_read = True
|
|
513
|
+
self._one = bytearray(1)
|
|
514
|
+
self._buf = bytearray(_READ_SIZE)
|
|
515
|
+
self._sx = _NO_SYSEX if sysex_buffer is None else sysex_buffer
|
|
516
|
+
self._st = array("H", [0] * _ST_SIZE)
|
|
517
|
+
self._st[_ST_SX_CAP] = min(len(self._sx), _MAX_SYSEX)
|
|
518
|
+
self._st[_ST_RUNNING_ON] = 1 if enable_running_status else 0
|
|
519
|
+
|
|
520
|
+
@property
|
|
521
|
+
def error_count(self):
|
|
522
|
+
"""Number of errors encountered when parsing received messages"""
|
|
523
|
+
return self._st[_ST_ERRORS]
|
|
524
|
+
|
|
525
|
+
def _fill(self):
|
|
526
|
+
n = _READ_SIZE
|
|
527
|
+
if self._has_in_waiting:
|
|
528
|
+
# a large readinto on UART would block toward its timeout
|
|
529
|
+
n = min(self._in_port.in_waiting, _READ_SIZE)
|
|
530
|
+
if not n:
|
|
531
|
+
return 0
|
|
532
|
+
if self._bulk_read:
|
|
533
|
+
try:
|
|
534
|
+
n = self._in_port.readinto(self._buf, n) or 0
|
|
535
|
+
except TypeError:
|
|
536
|
+
# adafruit_usb_host_midi has readinto(buf), which must be filled exactly
|
|
537
|
+
self._bulk_read = False
|
|
538
|
+
return self._fill()
|
|
539
|
+
else:
|
|
540
|
+
n = self._in_port.readinto(self._one) or 0
|
|
541
|
+
if n:
|
|
542
|
+
self._buf[0] = self._one[0]
|
|
543
|
+
self._st[_ST_RD] = 0
|
|
544
|
+
self._st[_ST_WR] = n
|
|
545
|
+
return n
|
|
546
|
+
|
|
547
|
+
def receive(self):
|
|
548
|
+
"""Read from MIDI port, parse that data and return the next MIDI message.
|
|
549
|
+
Does not wait: a partly received message is kept until the rest arrives.
|
|
550
|
+
|
|
551
|
+
:returns Message object: Returns object or None for nothing.
|
|
552
|
+
"""
|
|
553
|
+
parse = self._parse
|
|
554
|
+
buf = self._buf
|
|
555
|
+
st = self._st
|
|
556
|
+
sx = self._sx
|
|
557
|
+
|
|
558
|
+
r = parse(buf, st, sx)
|
|
559
|
+
while not r:
|
|
560
|
+
if not self._fill():
|
|
561
|
+
return None
|
|
562
|
+
r = parse(buf, st, sx)
|
|
563
|
+
|
|
564
|
+
status = r >> 16
|
|
565
|
+
if status < SYSEX:
|
|
566
|
+
return Message(status & 0xF0, (r >> 8) & 0xFF, r & 0xFF, status & 0x0F)
|
|
567
|
+
if status == SYSEX:
|
|
568
|
+
return Message(SYSEX, r & 0xFFFF)
|
|
569
|
+
return Message(status, (r >> 8) & 0xFF, r & 0xFF)
|
|
570
|
+
|
|
571
|
+
def on(self, mtype, func):
|
|
572
|
+
"""Register a function to be called by :meth:`poll` for a type of message.
|
|
573
|
+
There is one function per message type.
|
|
574
|
+
|
|
575
|
+
:param int mtype: The type of message, e.g. ``tmidi.NOTE_ON``.
|
|
576
|
+
:param func: Called as ``func(channel, data0, data1)`` with the raw data bytes.
|
|
577
|
+
``channel`` is 0 for system messages. For ``tmidi.SYSEX``, ``data0`` is
|
|
578
|
+
the number of bytes written to ``sysex_buffer``.
|
|
579
|
+
Use ``None`` to unregister.
|
|
580
|
+
"""
|
|
581
|
+
if func is None:
|
|
582
|
+
self._callbacks.pop(mtype, None)
|
|
583
|
+
else:
|
|
584
|
+
self._callbacks[mtype] = func
|
|
585
|
+
|
|
586
|
+
def poll(self, limit=32):
|
|
587
|
+
"""Parse waiting MIDI data, calling functions registered with :meth:`on`.
|
|
588
|
+
|
|
589
|
+
:param int limit: Most messages to handle, so a busy port can't starve
|
|
590
|
+
the main loop.
|
|
591
|
+
:returns int: Number of messages parsed
|
|
592
|
+
"""
|
|
593
|
+
parse = self._parse
|
|
594
|
+
buf = self._buf
|
|
595
|
+
st = self._st
|
|
596
|
+
sx = self._sx
|
|
597
|
+
callbacks = self._callbacks
|
|
598
|
+
|
|
599
|
+
count = 0
|
|
600
|
+
while count < limit:
|
|
601
|
+
r = parse(buf, st, sx)
|
|
602
|
+
while not r:
|
|
603
|
+
if not self._fill():
|
|
604
|
+
return count
|
|
605
|
+
r = parse(buf, st, sx)
|
|
606
|
+
count += 1
|
|
607
|
+
|
|
608
|
+
status = r >> 16
|
|
609
|
+
if status < SYSEX:
|
|
610
|
+
func = callbacks.get(status & 0xF0)
|
|
611
|
+
if func:
|
|
612
|
+
func(status & 0x0F, (r >> 8) & 0xFF, r & 0xFF)
|
|
613
|
+
elif status == SYSEX:
|
|
614
|
+
func = callbacks.get(SYSEX)
|
|
615
|
+
if func:
|
|
616
|
+
func(0, r & 0xFFFF, 0)
|
|
617
|
+
else:
|
|
618
|
+
func = callbacks.get(status)
|
|
619
|
+
if func:
|
|
620
|
+
func(0, (r >> 8) & 0xFF, r & 0xFF)
|
|
621
|
+
return count
|
|
622
|
+
|
|
623
|
+
def send(self, msg, channel=None):
|
|
624
|
+
"""Send a MIDI message.
|
|
625
|
+
|
|
626
|
+
:param msg: Either a Message object or a sequence (list) of Message objects.
|
|
627
|
+
The channel property will be *updated* as a side-effect of sending message(s).
|
|
628
|
+
:param int channel: Channel number, if not set, the msg's channel will be used.
|
|
629
|
+
"""
|
|
630
|
+
|
|
631
|
+
if isinstance(msg, Message):
|
|
632
|
+
if channel is not None:
|
|
633
|
+
msg.channel = channel
|
|
634
|
+
# bytes(object) does not work in uPy
|
|
635
|
+
data = msg.__bytes__()
|
|
636
|
+
else:
|
|
637
|
+
data = bytearray()
|
|
638
|
+
for each_msg in msg:
|
|
639
|
+
if channel is not None:
|
|
640
|
+
each_msg.channel = channel
|
|
641
|
+
data.extend(each_msg.__bytes__())
|
|
642
|
+
|
|
643
|
+
self._out_port.write(data, len(data))
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
tmidi.py,sha256=o0wX5I5xUPLpD9K6eAgSaewBZGPqSn8YT4eAukeeYU8,13086
|
|
2
|
-
circuitpython_tmidi-2.0.0.dist-info/licenses/LICENSE,sha256=jhhRyxpqyQxyF_D6SzY2-jCA5v8Z8JXfezCtxBS85CM,1075
|
|
3
|
-
circuitpython_tmidi-2.0.0.dist-info/METADATA,sha256=-X6m4y1dqA5NvLbJxIgQp2Dp20w9_1NByUMUSZBoD0w,4192
|
|
4
|
-
circuitpython_tmidi-2.0.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
5
|
-
circuitpython_tmidi-2.0.0.dist-info/top_level.txt,sha256=ey35b95sYIkXnGcYlLMoLxC4R4Z_waH1rnvn3ntCW5o,6
|
|
6
|
-
circuitpython_tmidi-2.0.0.dist-info/RECORD,,
|
tmidi.py
DELETED
|
@@ -1,423 +0,0 @@
|
|
|
1
|
-
# SPDX-FileCopyrightText: Copyright (c) 2019 Alethea Flowers for Winterbloom
|
|
2
|
-
# SPDX-FileCopyrightText: Copyright (c) 2024 Tod Kurt
|
|
3
|
-
#
|
|
4
|
-
# SPDX-License-Identifier: MIT
|
|
5
|
-
"""
|
|
6
|
-
`tmidi`
|
|
7
|
-
================================================================================
|
|
8
|
-
|
|
9
|
-
MIDI library for CircuitPython
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
* Author(s): Tod Kurt, with code from Alethea Flowers for Winterbloom
|
|
13
|
-
|
|
14
|
-
* Portions of this library come from Winterbloom_SmolMIDI:
|
|
15
|
-
https://github.com/wntrblm/Winterbloom_SmolMIDI
|
|
16
|
-
|
|
17
|
-
Implementation Notes
|
|
18
|
-
--------------------
|
|
19
|
-
|
|
20
|
-
**Hardware:**
|
|
21
|
-
|
|
22
|
-
* Native USB for USB MIDI
|
|
23
|
-
* UART for Serial MIDI
|
|
24
|
-
* PIO-USB or MAX3421E USB Host for USB Host MIDI
|
|
25
|
-
|
|
26
|
-
**Software and Dependencies:**
|
|
27
|
-
|
|
28
|
-
* Adafruit CircuitPython firmware for the supported boards:
|
|
29
|
-
https://circuitpython.org/downloads
|
|
30
|
-
|
|
31
|
-
"""
|
|
32
|
-
|
|
33
|
-
# imports
|
|
34
|
-
|
|
35
|
-
__version__ = "2.0.0"
|
|
36
|
-
__repo__ = "https://github.com/todbot/CircuitPython_TMIDI.git"
|
|
37
|
-
|
|
38
|
-
# Portions of this library come from Winterbloom_SmolMIDI:
|
|
39
|
-
#
|
|
40
|
-
# The MIT License (MIT)
|
|
41
|
-
#
|
|
42
|
-
# Copyright (c) 2019 Alethea Flowers for Winterbloom
|
|
43
|
-
#
|
|
44
|
-
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
45
|
-
# of this software and associated documentation files (the "Software"), to deal
|
|
46
|
-
# in the Software without restriction, including without limitation the rights
|
|
47
|
-
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
48
|
-
# copies of the Software, and to permit persons to whom the Software is
|
|
49
|
-
# furnished to do so, subject to the following conditions:
|
|
50
|
-
#
|
|
51
|
-
# The above copyright notice and this permission notice shall be included in
|
|
52
|
-
# all copies or substantial portions of the Software.
|
|
53
|
-
#
|
|
54
|
-
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
55
|
-
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
56
|
-
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
57
|
-
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
58
|
-
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
59
|
-
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
60
|
-
# THE SOFTWARE.
|
|
61
|
-
|
|
62
|
-
from micropython import const
|
|
63
|
-
|
|
64
|
-
# Message type constants.
|
|
65
|
-
NOTE_OFF = const(0x80)
|
|
66
|
-
"""Note Off"""
|
|
67
|
-
NOTE_ON = const(0x90)
|
|
68
|
-
"""Note On"""
|
|
69
|
-
AFTERTOUCH = const(0xA0)
|
|
70
|
-
"""Aftertouch"""
|
|
71
|
-
CONTROLLER_CHANGE = CC = const(0xB0)
|
|
72
|
-
"""Controller Change"""
|
|
73
|
-
PROGRAM_CHANGE = const(0xC0)
|
|
74
|
-
"""Program Change"""
|
|
75
|
-
CHANNEL_PRESSURE = const(0xD0)
|
|
76
|
-
"""Channel Pressure"""
|
|
77
|
-
PITCH_BEND = const(0xE0)
|
|
78
|
-
"""Pitch Bend"""
|
|
79
|
-
SYSTEM_EXCLUSIVE = SYSEX = const(0xF0)
|
|
80
|
-
"""Sysex"""
|
|
81
|
-
SONG_POSITION = const(0xF2)
|
|
82
|
-
"""Song Position"""
|
|
83
|
-
SONG_SELECT = const(0xF3)
|
|
84
|
-
"""Song Select"""
|
|
85
|
-
BUS_SELECT = const(0xF5)
|
|
86
|
-
"""BUS Select"""
|
|
87
|
-
TUNE_REQUEST = const(0xF6)
|
|
88
|
-
"""Tune Request"""
|
|
89
|
-
SYSEX_END = const(0xF7)
|
|
90
|
-
"""Sysex End"""
|
|
91
|
-
CLOCK = const(0xF8)
|
|
92
|
-
"""Clock"""
|
|
93
|
-
TICK = const(0xF9)
|
|
94
|
-
"""Tick"""
|
|
95
|
-
START = const(0xFA)
|
|
96
|
-
"""Start"""
|
|
97
|
-
CONTINUE = const(0xFB)
|
|
98
|
-
"""Continue"""
|
|
99
|
-
STOP = const(0xFC)
|
|
100
|
-
"""Stop"""
|
|
101
|
-
ACTIVE_SENSING = const(0xFE)
|
|
102
|
-
"""Active Sensing"""
|
|
103
|
-
SYSTEM_RESET = const(0xFF)
|
|
104
|
-
"""System Reset"""
|
|
105
|
-
|
|
106
|
-
_LEN_0_MESSAGES = set(
|
|
107
|
-
[
|
|
108
|
-
TUNE_REQUEST,
|
|
109
|
-
SYSEX,
|
|
110
|
-
SYSEX_END,
|
|
111
|
-
CLOCK,
|
|
112
|
-
TICK,
|
|
113
|
-
START,
|
|
114
|
-
CONTINUE,
|
|
115
|
-
STOP,
|
|
116
|
-
ACTIVE_SENSING,
|
|
117
|
-
SYSTEM_RESET,
|
|
118
|
-
]
|
|
119
|
-
)
|
|
120
|
-
_LEN_1_MESSAGES = set([PROGRAM_CHANGE, CHANNEL_PRESSURE, SONG_SELECT, BUS_SELECT])
|
|
121
|
-
_LEN_2_MESSAGES = set([NOTE_OFF, NOTE_ON, AFTERTOUCH, CC, PITCH_BEND, SONG_POSITION])
|
|
122
|
-
|
|
123
|
-
_MSG_TYPE_NAMES = {
|
|
124
|
-
NOTE_OFF: "NoteOff",
|
|
125
|
-
NOTE_ON: "NoteOn",
|
|
126
|
-
AFTERTOUCH: "Aftertouch",
|
|
127
|
-
CC: "CC",
|
|
128
|
-
PROGRAM_CHANGE: "ProgramChange",
|
|
129
|
-
CHANNEL_PRESSURE: "ChannelPressure",
|
|
130
|
-
PITCH_BEND: "PitchBend",
|
|
131
|
-
SYSEX: "Sysex",
|
|
132
|
-
SONG_POSITION: "SongPosition",
|
|
133
|
-
SONG_SELECT: "SongSelect",
|
|
134
|
-
BUS_SELECT: "BusSelect",
|
|
135
|
-
TUNE_REQUEST: "TuneRequest",
|
|
136
|
-
SYSEX_END: "SysexEnd",
|
|
137
|
-
CLOCK: "Clock",
|
|
138
|
-
TICK: "Tick",
|
|
139
|
-
START: "Start",
|
|
140
|
-
CONTINUE: "Continue",
|
|
141
|
-
STOP: "Stop",
|
|
142
|
-
ACTIVE_SENSING: "ActiveSensing",
|
|
143
|
-
SYSTEM_RESET: "SystemReset",
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
def _is_channel_message(status_byte):
|
|
148
|
-
return status_byte >= NOTE_OFF and status_byte < SYSEX
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
def _read_byte(port):
|
|
152
|
-
while not (buf := port.read(1)):
|
|
153
|
-
pass
|
|
154
|
-
return buf[0]
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
# def _read_n_bytes(port, buf, dest, num_bytes):
|
|
158
|
-
# while num_bytes:
|
|
159
|
-
# if port.readinto(buf):
|
|
160
|
-
# dest.append(buf[0])
|
|
161
|
-
# num_bytes -= 1
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
# def _read_byte_works(port):
|
|
165
|
-
# while True:
|
|
166
|
-
# buf = port.read(1)
|
|
167
|
-
# if buf:
|
|
168
|
-
# return buf[0]
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
class Message:
|
|
172
|
-
"""
|
|
173
|
-
MIDI Message.
|
|
174
|
-
|
|
175
|
-
:param mtype: The type of message, e.g. tmidi.NOTE_ON.
|
|
176
|
-
:param data0: The first data byte for this message,
|
|
177
|
-
e.g. the note number as an ``int`` (0-127) for NOTE_ON messages.
|
|
178
|
-
:param data1: The second data byte for this message,
|
|
179
|
-
e.g. the velocity (0-127) for NOTE_ON messages.
|
|
180
|
-
:param channel: The MIDI channel for this message, if applicable (0-15)
|
|
181
|
-
|
|
182
|
-
Example of creating Messages:
|
|
183
|
-
|
|
184
|
-
.. code-block:: python
|
|
185
|
-
|
|
186
|
-
# create Note On middle-C message on ch 1 (0-indexed)
|
|
187
|
-
m = Message(tmidi.NOTE_ON, 60, 127, channel=0)
|
|
188
|
-
# create CC 74 with val 63 on ch 4
|
|
189
|
-
m = Message(tmidi.CC, 74, 63, channel=4-1)
|
|
190
|
-
# create a pitch bend full up on ch 1
|
|
191
|
-
m = Message(tmidi.PITCH_BEND, 8191)
|
|
192
|
-
|
|
193
|
-
Example of creating accessing Message attributes:
|
|
194
|
-
|
|
195
|
-
.. code-block:: python
|
|
196
|
-
|
|
197
|
-
if msg := midi.receive():
|
|
198
|
-
if msg.type == tmidi.NOTE_ON:
|
|
199
|
-
print("note on:", msg.note, msg.velocity)
|
|
200
|
-
elif msg.type == tmidi.NOTE_OFF:
|
|
201
|
-
print("note off:", msg.note, msg.velocity)
|
|
202
|
-
elif msg.type == tmidi.PROGRAM_CHANGE:
|
|
203
|
-
print("program change:", msg.value)
|
|
204
|
-
"""
|
|
205
|
-
|
|
206
|
-
def __init__(self, mtype=SYSTEM_RESET, data0=0, data1=0, channel=0):
|
|
207
|
-
self.type = mtype
|
|
208
|
-
self.channel = channel
|
|
209
|
-
self.data0 = data0
|
|
210
|
-
self.data1 = data1
|
|
211
|
-
if mtype == PITCH_BEND and data1 == 0:
|
|
212
|
-
self.pitch_bend = data0
|
|
213
|
-
|
|
214
|
-
def __bytes__(self):
|
|
215
|
-
status_byte = self.type
|
|
216
|
-
if _is_channel_message(status_byte):
|
|
217
|
-
status_byte |= self.channel
|
|
218
|
-
if self.type in _LEN_2_MESSAGES:
|
|
219
|
-
return bytes([status_byte, self.data0, self.data1])
|
|
220
|
-
elif self.type in _LEN_1_MESSAGES:
|
|
221
|
-
return bytes([status_byte, self.data0])
|
|
222
|
-
return bytes([status_byte])
|
|
223
|
-
|
|
224
|
-
def __repr__(self):
|
|
225
|
-
return self.__str__()
|
|
226
|
-
|
|
227
|
-
def __str__(self):
|
|
228
|
-
mtype = self.type
|
|
229
|
-
type_str = "Message(" + _MSG_TYPE_NAMES.get(mtype, "Unknown")
|
|
230
|
-
ch_str = "ch:%d" % self.channel if _is_channel_message(mtype) else "-"
|
|
231
|
-
if mtype == PITCH_BEND:
|
|
232
|
-
return "%s %s %d)" % (type_str, ch_str, self.pitch_bend)
|
|
233
|
-
if mtype in _LEN_2_MESSAGES:
|
|
234
|
-
return "%s %s %d %d)" % (type_str, ch_str, self.data0, self.data1)
|
|
235
|
-
if mtype in _LEN_1_MESSAGES:
|
|
236
|
-
return "%s %s %d)" % (type_str, ch_str, self.data0)
|
|
237
|
-
return "%s)" % type_str
|
|
238
|
-
|
|
239
|
-
@property
|
|
240
|
-
def note(self):
|
|
241
|
-
"""MIDI note number of message (only valid for NOTE_ON/NOTE_OFF msgs)"""
|
|
242
|
-
return self.data0
|
|
243
|
-
|
|
244
|
-
@note.setter
|
|
245
|
-
def note(self, notenum):
|
|
246
|
-
self.data0 = notenum
|
|
247
|
-
|
|
248
|
-
@property
|
|
249
|
-
def velocity(self):
|
|
250
|
-
"""MIDI velocity of message (only valid for NOTE_ON/NOTE_OFF msgs)"""
|
|
251
|
-
return self.data1
|
|
252
|
-
|
|
253
|
-
@velocity.setter
|
|
254
|
-
def velocity(self, vel):
|
|
255
|
-
self.data1 = vel
|
|
256
|
-
|
|
257
|
-
@property
|
|
258
|
-
def pitch_bend(self):
|
|
259
|
-
"""Pitch bend value of message (only valid for PITCH_BEND msgs)"""
|
|
260
|
-
return (self.data1 << 7 | self.data0) - 8192
|
|
261
|
-
|
|
262
|
-
@pitch_bend.setter
|
|
263
|
-
def pitch_bend(self, pbval):
|
|
264
|
-
pbval += 8192
|
|
265
|
-
self.data0 = pbval & 0x7F
|
|
266
|
-
self.data1 = pbval >> 7
|
|
267
|
-
|
|
268
|
-
@property
|
|
269
|
-
def value(self):
|
|
270
|
-
"""Message value, only valid for len1 messages (Program Change, etc)"""
|
|
271
|
-
return self.data0
|
|
272
|
-
|
|
273
|
-
@value.setter
|
|
274
|
-
def value(self, val):
|
|
275
|
-
self.data0 = val
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
class MIDI:
|
|
279
|
-
"""
|
|
280
|
-
MIDI Parser, receiver and sender
|
|
281
|
-
``midi_in`` or ``midi_out`` *must* be set or both together.
|
|
282
|
-
|
|
283
|
-
:param midi_in: an object which implements ``read(length)``,
|
|
284
|
-
set to ``usb_midi.ports[0]`` for USB MIDI, default None.
|
|
285
|
-
:param midi_out: an object which implements ``write(buffer, length)``,
|
|
286
|
-
set to ``usb_midi.ports[1]`` for USB MIDI, default None.
|
|
287
|
-
:param bool enable_running_status: Allow running status messages to work, default False.
|
|
288
|
-
|
|
289
|
-
Example of sending MIDI over USB:
|
|
290
|
-
|
|
291
|
-
.. code-block:: python
|
|
292
|
-
|
|
293
|
-
import usb_midi
|
|
294
|
-
import tmidi
|
|
295
|
-
midi_usb = tmidi.MIDI(midi_out=usb_midi.ports[1])
|
|
296
|
-
msg_on = tmidi.Message(tmidi.NOTE_ON, notenum, velocity)
|
|
297
|
-
midi_usb.send(msg_on)
|
|
298
|
-
|
|
299
|
-
Example of sending MIDI over UART (TRS or 5-pin):
|
|
300
|
-
|
|
301
|
-
.. code-block:: python
|
|
302
|
-
|
|
303
|
-
import board
|
|
304
|
-
import busio
|
|
305
|
-
import tmidi
|
|
306
|
-
uart = busio.UART(tx=board.TX, rx=board.RX, timeout=0.001)
|
|
307
|
-
midi_uart = tmidi.MIDI(midi_out=uart)
|
|
308
|
-
msg_on = tmidi.Message(tmidi.NOTE_ON, notenum, velocity)
|
|
309
|
-
midi_uart.send(msg_on)
|
|
310
|
-
|
|
311
|
-
Example of receiving MIDI:
|
|
312
|
-
|
|
313
|
-
.. code-block:: python
|
|
314
|
-
|
|
315
|
-
import board
|
|
316
|
-
import busio
|
|
317
|
-
import usb_midi
|
|
318
|
-
import tmidi
|
|
319
|
-
uart = busio.UART(tx=board.TX, rx=board.RX, timeout=0.001)
|
|
320
|
-
midi_uart = tmidi.MIDI(midi_in=uart, midi_out=uart)
|
|
321
|
-
midi_usb = tmidi.MIDI(midi_in=usb_midi.ports[0], midi_out=usb_midi.ports[1])
|
|
322
|
-
|
|
323
|
-
while True:
|
|
324
|
-
if msg := midi_usb.receive():
|
|
325
|
-
print("usb midi:", msg)
|
|
326
|
-
if msg := midi_uart.receive():
|
|
327
|
-
print("uart midi:", msg)
|
|
328
|
-
"""
|
|
329
|
-
|
|
330
|
-
def __init__(self, midi_in=None, midi_out=None, enable_running_status=False):
|
|
331
|
-
self._in_port = midi_in
|
|
332
|
-
self._out_port = midi_out
|
|
333
|
-
self._running_status_enabled = enable_running_status
|
|
334
|
-
self._running_status = None
|
|
335
|
-
self._error_count = 0
|
|
336
|
-
|
|
337
|
-
# This input buffer holds what has been read from midi_in
|
|
338
|
-
self._read_buf = bytearray(1)
|
|
339
|
-
|
|
340
|
-
@property
|
|
341
|
-
def error_count(self):
|
|
342
|
-
"""Number of errors encountered when parsing received messages"""
|
|
343
|
-
return self._error_count
|
|
344
|
-
|
|
345
|
-
def receive(self):
|
|
346
|
-
"""Read message from MIDI port, parse that data and
|
|
347
|
-
return the first MIDI message (event).
|
|
348
|
-
This maintains the blocking characteristics of the midi_in port.
|
|
349
|
-
Relies on the port to buffer the incoming unread MIDI messages.
|
|
350
|
-
|
|
351
|
-
:returns Message object: Returns object or None for nothing.
|
|
352
|
-
"""
|
|
353
|
-
|
|
354
|
-
# Read the status byte for the next message.
|
|
355
|
-
# note: this will block if the port is set to have a timeout
|
|
356
|
-
status_byte_buf = self._in_port.read(1)
|
|
357
|
-
|
|
358
|
-
# No message ready.
|
|
359
|
-
if not status_byte_buf:
|
|
360
|
-
return None
|
|
361
|
-
|
|
362
|
-
# Is this actually a status byte?
|
|
363
|
-
status_byte = status_byte_buf[0]
|
|
364
|
-
is_status = status_byte & 0x80
|
|
365
|
-
|
|
366
|
-
# If not, see if we have a running status byte.
|
|
367
|
-
if not is_status:
|
|
368
|
-
if self._running_status_enabled and self._running_status:
|
|
369
|
-
status_byte = self._running_status
|
|
370
|
-
# If not a status byte and no running status, this is invalid data.
|
|
371
|
-
else:
|
|
372
|
-
self._error_count += 1
|
|
373
|
-
return None
|
|
374
|
-
|
|
375
|
-
message = Message(status_byte)
|
|
376
|
-
|
|
377
|
-
# Is this a channel message, if so, let's figure out the right
|
|
378
|
-
# message type and set the message's channel property.
|
|
379
|
-
if _is_channel_message(status_byte):
|
|
380
|
-
# Only set the running status byte for channel messages.
|
|
381
|
-
self._running_status = status_byte
|
|
382
|
-
# Mask off the channel nibble.
|
|
383
|
-
message.type = status_byte & 0xF0
|
|
384
|
-
message.channel = status_byte & 0x0F
|
|
385
|
-
|
|
386
|
-
# Read the appropriate number of bytes for each message type.
|
|
387
|
-
if message.type in _LEN_2_MESSAGES:
|
|
388
|
-
message.data0 = _read_byte(self._in_port)
|
|
389
|
-
message.data1 = _read_byte(self._in_port)
|
|
390
|
-
elif message.type in _LEN_1_MESSAGES:
|
|
391
|
-
message.data0 = _read_byte(self._in_port)
|
|
392
|
-
|
|
393
|
-
# Check the data bytes for corruption. status bytes in data
|
|
394
|
-
# means we're out of sync, so discard.
|
|
395
|
-
# TODO: Figure out a better way to detect and deal with this upstream.
|
|
396
|
-
for b in (message.data0 or 0, message.data1 or 0):
|
|
397
|
-
if b & 0x80:
|
|
398
|
-
self._error_count += 1
|
|
399
|
-
return None
|
|
400
|
-
|
|
401
|
-
return message
|
|
402
|
-
|
|
403
|
-
def send(self, msg, channel=None):
|
|
404
|
-
"""Send a MIDI message.
|
|
405
|
-
|
|
406
|
-
:param msg: Either a Message object or a sequence (list) of Message objects.
|
|
407
|
-
The channel property will be *updated* as a side-effect of sending message(s).
|
|
408
|
-
:param int channel: Channel number, if not set, the msg's channel will be used.
|
|
409
|
-
"""
|
|
410
|
-
|
|
411
|
-
if isinstance(msg, Message):
|
|
412
|
-
if channel:
|
|
413
|
-
msg.channel = channel
|
|
414
|
-
# bytes(object) does not work in uPy
|
|
415
|
-
data = msg.__bytes__()
|
|
416
|
-
else:
|
|
417
|
-
data = bytearray()
|
|
418
|
-
for each_msg in msg:
|
|
419
|
-
if channel:
|
|
420
|
-
each_msg.channel = channel
|
|
421
|
-
data.extend(each_msg.__bytes__())
|
|
422
|
-
|
|
423
|
-
self._out_port.write(data, len(data))
|
{circuitpython_tmidi-2.0.0.dist-info → circuitpython_tmidi-3.0.0.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|
|
File without changes
|