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 @@
|
|
|
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))
|