circuitpython-tmidi 0.1__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.
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2024 Tod Kurt
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,138 @@
1
+ Metadata-Version: 2.1
2
+ Name: circuitpython-tmidi
3
+ Version: 0.1
4
+ Summary: MIDI library for CircuitPython
5
+ Author-email: Tod Kurt <tod@todbot.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/todbot/CircuitPython_TMIDI
8
+ Keywords: adafruit,blinka,circuitpython,micropython,tmidi,midi
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: Topic :: Software Development :: Libraries
11
+ Classifier: Topic :: Software Development :: Embedded Systems
12
+ Classifier: Topic :: System :: Hardware
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Description-Content-Type: text/x-rst
16
+ License-File: LICENSE
17
+ Requires-Dist: Adafruit-Blinka
18
+ Provides-Extra: optional
19
+
20
+ Introduction
21
+ ============
22
+
23
+
24
+ .. image:: https://readthedocs.org/projects/circuitpython-tmidi/badge/?version=latest
25
+ :target: https://circuitpython-tmidi.readthedocs.io/
26
+ :alt: Documentation Status
27
+ .. image:: https://img.shields.io/discord/327254708534116352.svg
28
+ :target: https://adafru.it/discord
29
+ :alt: Discord
30
+ .. image:: https://github.com/todbot/CircuitPython_TMIDI/workflows/Build%20CI/badge.svg
31
+ :target: https://github.com/todbot/CircuitPython_TMIDI/actions
32
+ :alt: Build Status
33
+ .. image:: https://img.shields.io/badge/code%20style-black-000000.svg
34
+ :target: https://github.com/psf/black
35
+ :alt: Code Style: Black
36
+
37
+ MIDI library for CircuitPython
38
+
39
+
40
+ Dependencies
41
+ =============
42
+ This library depends on:
43
+
44
+ * `Adafruit CircuitPython <https://github.com/adafruit/circuitpython>`_
45
+
46
+ Please ensure all dependencies are available on the CircuitPython filesystem.
47
+ This is easily achieved by downloading
48
+ `the Adafruit library and driver bundle <https://circuitpython.org/libraries>`_
49
+ or individual libraries can be installed using
50
+ `circup <https://github.com/adafruit/circup>`_.
51
+
52
+ Installing from PyPI
53
+ =====================
54
+ .. note:: This library is not available on PyPI yet. Install documentation is included
55
+ as a standard element. Stay tuned for PyPI availability!
56
+
57
+
58
+ On supported GNU/Linux systems like the Raspberry Pi, you can install the driver locally `from
59
+ PyPI <https://pypi.org/project/circuitpython-tmidi/>`_.
60
+ To install for current user:
61
+
62
+ .. code-block:: shell
63
+
64
+ pip3 install circuitpython-tmidi
65
+
66
+ To install system-wide (this may be required in some cases):
67
+
68
+ .. code-block:: shell
69
+
70
+ sudo pip3 install circuitpython-tmidi
71
+
72
+ To install in a virtual environment in your current project:
73
+
74
+ .. code-block:: shell
75
+
76
+ mkdir project-name && cd project-name
77
+ python3 -m venv .venv
78
+ source .env/bin/activate
79
+ pip3 install circuitpython-tmidi
80
+
81
+ Installing to a Connected CircuitPython Device with Circup
82
+ ==========================================================
83
+
84
+ Make sure that you have ``circup`` installed in your Python environment.
85
+ Install it with the following command if necessary:
86
+
87
+ .. code-block:: shell
88
+
89
+ pip3 install circup
90
+
91
+ With ``circup`` installed and your CircuitPython device connected use the
92
+ following command to install:
93
+
94
+ .. code-block:: shell
95
+
96
+ circup install tmidi
97
+
98
+ Or the following command to update an existing version:
99
+
100
+ .. code-block:: shell
101
+
102
+ circup update
103
+
104
+ Usage Example
105
+ =============
106
+
107
+ .. code-block:: python
108
+
109
+ import usb_midi
110
+ import tmidi
111
+
112
+ midi_usb = tmidi.MIDI(midi_in=usb_midi.ports[0], midi_out=usb_midi.ports[1])
113
+
114
+ def send_midi_panic(channel=1):
115
+ msg = tmidi.Message(tmidi.NOTE_OFF, channel-1) # channels are 0-indexed
116
+ for notenum in range(128):
117
+ msg.note = notenum
118
+ msg.velocity = 0
119
+ midi_usb.send(msg)
120
+ time.sleep(0.01)
121
+ }
122
+
123
+ send_midi_panic()
124
+
125
+
126
+ Documentation
127
+ =============
128
+ API documentation for this library can be found on `Read the Docs <https://circuitpython-tmidi.readthedocs.io/>`_.
129
+
130
+ For information on building library documentation, please check out
131
+ `this guide <https://learn.adafruit.com/creating-and-sharing-a-circuitpython-library/sharing-our-docs-on-readthedocs#sphinx-5-1>`_.
132
+
133
+ Contributing
134
+ ============
135
+
136
+ Contributions are welcome! Please read our `Code of Conduct
137
+ <https://github.com/todbot/CircuitPython_TMIDI/blob/HEAD/CODE_OF_CONDUCT.md>`_
138
+ before contributing to help this project stay welcoming.
@@ -0,0 +1,6 @@
1
+ tmidi.py,sha256=2CHDbFpDpTwuEXenmeIFmkDDzM5DvGUy-GyAVtgQI7Y,11029
2
+ circuitpython_tmidi-0.1.dist-info/LICENSE,sha256=jhhRyxpqyQxyF_D6SzY2-jCA5v8Z8JXfezCtxBS85CM,1075
3
+ circuitpython_tmidi-0.1.dist-info/METADATA,sha256=SeJ84vZeleQVW_Rk7lJfoOZ-wa7LO7Wb7dwpt5lDvsY,4169
4
+ circuitpython_tmidi-0.1.dist-info/WHEEL,sha256=R0nc6qTxuoLk7ShA2_Y-UWkN8ZdfDBG2B6Eqpz2WXbs,91
5
+ circuitpython_tmidi-0.1.dist-info/top_level.txt,sha256=ey35b95sYIkXnGcYlLMoLxC4R4Z_waH1rnvn3ntCW5o,6
6
+ circuitpython_tmidi-0.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (72.1.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ tmidi
tmidi.py ADDED
@@ -0,0 +1,359 @@
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__ = "0.1"
36
+ __repo__ = "https://github.com/todbot/CircuitPython_TMIDI.git"
37
+
38
+
39
+ # Portions of this library come from Winterbloom_SmolMIDI:
40
+ #
41
+ # The MIT License (MIT)
42
+ #
43
+ # Copyright (c) 2019 Alethea Flowers for Winterbloom
44
+ #
45
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
46
+ # of this software and associated documentation files (the "Software"), to deal
47
+ # in the Software without restriction, including without limitation the rights
48
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
49
+ # copies of the Software, and to permit persons to whom the Software is
50
+ # furnished to do so, subject to the following conditions:
51
+ #
52
+ # The above copyright notice and this permission notice shall be included in
53
+ # all copies or substantial portions of the Software.
54
+ #
55
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
56
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
57
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
58
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
59
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
60
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
61
+ # THE SOFTWARE.
62
+
63
+ # Message type constants.
64
+ NOTE_OFF = 0x80
65
+ """Note Off"""
66
+ NOTE_ON = 0x90
67
+ """Note On"""
68
+ AFTERTOUCH = 0xA0
69
+ """Aftertouch"""
70
+ CONTROLLER_CHANGE = CC = 0xB0
71
+ """Controller Change"""
72
+ PROGRAM_CHANGE = 0xC0
73
+ """Program Change"""
74
+ CHANNEL_PRESSURE = 0xD0
75
+ """Channel Pressure"""
76
+ PITCH_BEND = 0xE0
77
+ """Pitch Bend"""
78
+ SYSTEM_EXCLUSIVE = SYSEX = 0xF0
79
+ """Sysex"""
80
+ SONG_POSITION = 0xF2
81
+ """Song Position"""
82
+ SONG_SELECT = 0xF3
83
+ """Song Select"""
84
+ BUS_SELECT = 0xF5
85
+ """BUS Select"""
86
+ TUNE_REQUEST = 0xF6
87
+ """Tune Request"""
88
+ SYSEX_END = 0xF7
89
+ """Sysex End"""
90
+ CLOCK = 0xF8
91
+ """Clock"""
92
+ TICK = 0xF9
93
+ """Tick"""
94
+ START = 0xFA
95
+ """Start"""
96
+ CONTINUE = 0xFB
97
+ """Continue"""
98
+ STOP = 0xFC
99
+ """Stop"""
100
+ ACTIVE_SENSING = 0xFE
101
+ """Active Sensing"""
102
+ SYSTEM_RESET = 0xFF
103
+ """System Reset"""
104
+
105
+ _LEN_0_MESSAGES = set(
106
+ [
107
+ TUNE_REQUEST,
108
+ SYSEX,
109
+ SYSEX_END,
110
+ CLOCK,
111
+ TICK,
112
+ START,
113
+ CONTINUE,
114
+ STOP,
115
+ ACTIVE_SENSING,
116
+ SYSTEM_RESET,
117
+ ]
118
+ )
119
+ _LEN_1_MESSAGES = set([PROGRAM_CHANGE, CHANNEL_PRESSURE, SONG_SELECT, BUS_SELECT])
120
+ _LEN_2_MESSAGES = set([NOTE_OFF, NOTE_ON, AFTERTOUCH, CC, PITCH_BEND, SONG_POSITION])
121
+
122
+ _MSG_TYPE_NAMES = {
123
+ NOTE_OFF: "NoteOff",
124
+ NOTE_ON: "NoteOn",
125
+ AFTERTOUCH: "Aftertouch",
126
+ CC: "CC",
127
+ PROGRAM_CHANGE: "ProgramChange",
128
+ CHANNEL_PRESSURE: "ChannelPressure",
129
+ PITCH_BEND: "PitchBend",
130
+ SYSEX: "Sysex",
131
+ SONG_POSITION: "SongPosition",
132
+ SONG_SELECT: "SongSelect",
133
+ BUS_SELECT: "BusSelect",
134
+ TUNE_REQUEST: "TuneRequest",
135
+ SYSEX_END: "SysexEnd",
136
+ CLOCK: "Clock",
137
+ TICK: "Tick",
138
+ START: "Start",
139
+ CONTINUE: "Continue",
140
+ STOP: "Stop",
141
+ ACTIVE_SENSING: "ActiveSensing",
142
+ SYSTEM_RESET: "SystemReset",
143
+ }
144
+
145
+
146
+ # pylint: disable=chained-comparison
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 channel: The MIDI channel for this message, if applicable (0-15)
177
+ :param data0: The first data byte for this message,
178
+ e.g. the note number as an ``int`` (0-127) for NOTE_ON messages.
179
+ :param data1: The second data byte for this message,
180
+ e.g. the velocity (0-127) for NOTE_ON messages.
181
+ """
182
+
183
+ def __init__(self, mtype=SYSTEM_RESET, channel=None, data0=0, data1=0):
184
+ """
185
+ Create a MIDI Message.
186
+
187
+ Example::
188
+ # create Note On middle-C message on ch1 (0-indexed)
189
+ m = Message(tmidi.NOTE_ON, 0, 60, 127)
190
+ # create CC 74 with val 63 on ch 4
191
+ m = Message(tmidi.CC, 3, 74, 63)
192
+ """
193
+ self.type = mtype
194
+ self.channel = channel
195
+ self.data0 = data0
196
+ self.data1 = data1
197
+
198
+ def __bytes__(self):
199
+ status_byte = self.type
200
+ if _is_channel_message(status_byte):
201
+ status_byte |= self.channel
202
+ if status_byte in _LEN_2_MESSAGES:
203
+ return bytes([status_byte, self.data0, self.data1])
204
+ if status_byte in _LEN_1_MESSAGES:
205
+ return bytes([status_byte, self.data0])
206
+ return bytes([status_byte])
207
+
208
+ def __repr__(self):
209
+ return self.__str__()
210
+
211
+ # pylint: disable=consider-using-f-string
212
+ def __str__(self):
213
+ mtype = self.type
214
+ type_str = "Message(" + _MSG_TYPE_NAMES[mtype]
215
+ ch_str = "ch:%d" % self.channel if _is_channel_message(mtype) else "-"
216
+ if mtype == PITCH_BEND:
217
+ return "%s %s %d)" % (type_str, ch_str, self.pitch_bend)
218
+ if mtype in _LEN_2_MESSAGES:
219
+ return "%s %s %d %d)" % (type_str, ch_str, self.data0, self.data1)
220
+ if mtype in _LEN_1_MESSAGES:
221
+ return "%s %s %d)" % (type_str, ch_str, self.data0)
222
+ return "%s)" % type_str
223
+
224
+ @property
225
+ def note(self):
226
+ """MIDI note number of message (only valid for NOTE_ON/NOTE_OFF msgs)"""
227
+ return self.data0
228
+
229
+ @note.setter
230
+ def note(self, notenum):
231
+ self.data0 = notenum
232
+
233
+ @property
234
+ def velocity(self):
235
+ """MIDI velocity of message (only valid for NOTE_ON/NOTE_OFF msgs)"""
236
+ return self.data1
237
+
238
+ @velocity.setter
239
+ def velocity(self, vel):
240
+ self.data1 = vel
241
+
242
+ @property
243
+ def pitch_bend(self):
244
+ """Pitch bend value of message (only for PITCH_BEND msgs)"""
245
+ return (self.data1 << 7 | self.data0) - 8192
246
+
247
+ @pitch_bend.setter
248
+ def pitch_bend(self, pbval):
249
+ self.data0 = pbval & 0x7F
250
+ self.data1 = pbval >> 7
251
+
252
+
253
+ class MIDI:
254
+ """
255
+ MIDI Parser and sender
256
+ ``midi_in`` or ``midi_out`` *must* be set or both together.
257
+
258
+ :param midi_in: an object which implements ``read(length)``,
259
+ set to ``usb_midi.ports[0]`` for USB MIDI, default None.
260
+ :param midi_out: an object which implements ``write(buffer, length)``,
261
+ set to ``usb_midi.ports[1]`` for USB MIDI, default None.
262
+ :param bool enable_running_status: Allow running status messages to work, default False.
263
+ """
264
+
265
+ def __init__(self, midi_in=None, midi_out=None, enable_running_status=False):
266
+ self._in_port = midi_in
267
+ self._out_port = midi_out
268
+ self._running_status_enabled = enable_running_status
269
+ self._running_status = None
270
+ self._error_count = 0
271
+
272
+ # This input buffer holds what has been read from midi_in
273
+ self._read_buf = bytearray(1)
274
+
275
+ @property
276
+ def error_count(self):
277
+ """Number of errors encountered when parsing received messages"""
278
+ return self._error_count
279
+
280
+ def receive(self):
281
+ """Read message from MIDI port, parse that data and
282
+ return the first MIDI message (event).
283
+ This maintains the blocking characteristics of the midi_in port.
284
+ Relies on the port to buffer the incoming unread MIDI messages.
285
+
286
+ :returns Message object: Returns object or None for nothing.
287
+ """
288
+
289
+ # Read the status byte for the next message.
290
+ # note: this will block if the port is set to have a timeout
291
+ status_byte_buf = self._in_port.read(1)
292
+
293
+ # No message ready.
294
+ if not status_byte_buf:
295
+ return None
296
+
297
+ # Is this actually a status byte?
298
+ status_byte = status_byte_buf[0]
299
+ is_status = status_byte & 0x80
300
+
301
+ # If not, see if we have a running status byte.
302
+ if not is_status:
303
+ if self._running_status_enabled and self._running_status:
304
+ status_byte = self._running_status
305
+ # If not a status byte and no running status, this is invalid data.
306
+ else:
307
+ self._error_count += 1
308
+ return None
309
+
310
+ message = Message(status_byte)
311
+
312
+ # Is this a channel message, if so, let's figure out the right
313
+ # message type and set the message's channel property.
314
+ if _is_channel_message(status_byte):
315
+ # Only set the running status byte for channel messages.
316
+ self._running_status = status_byte
317
+ # Mask off the channel nibble.
318
+ message.type = status_byte & 0xF0
319
+ message.channel = status_byte & 0x0F
320
+
321
+ # Read the appropriate number of bytes for each message type.
322
+ if message.type in _LEN_2_MESSAGES:
323
+ message.data0 = _read_byte(self._in_port)
324
+ message.data1 = _read_byte(self._in_port)
325
+ elif message.type in _LEN_1_MESSAGES:
326
+ message.data0 = _read_byte(self._in_port)
327
+
328
+ # Check the data bytes for corruption. status bytes in data
329
+ # means we're out of sync, so discard.
330
+ # TODO: Figure out a better way to detect and deal with this upstream.
331
+ for b in (message.data0 or 0, message.data1 or 0):
332
+ if b & 0x80:
333
+ self._error_count += 1
334
+ return None
335
+
336
+ return message
337
+
338
+ # pylint: disable=unnecessary-dunder-call
339
+ def send(self, msg, channel=None):
340
+ """Send a MIDI message.
341
+
342
+ :param msg: Either a Message object or a sequence (list) of Message objects.
343
+ The channel property will be *updated* as a side-effect of sending message(s).
344
+ :param int channel: Channel number, if not set, the msg's channel will be used.
345
+ """
346
+
347
+ if isinstance(msg, Message):
348
+ if channel:
349
+ msg.channel = channel
350
+ # bytes(object) does not work in uPy
351
+ data = msg.__bytes__()
352
+ else:
353
+ data = bytearray()
354
+ for each_msg in msg:
355
+ if channel:
356
+ each_msg.channel = channel
357
+ data.extend(each_msg.__bytes__())
358
+
359
+ self._out_port.write(data, len(data))