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.
@@ -1,16 +1,15 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: circuitpython-tmidi
3
- Version: 2.0.0
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,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.9.0)
2
+ Generator: setuptools (84.2.1)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
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))