/ adafruit_midi / midi_message.py
midi_message.py
1 # The MIT License (MIT) 2 # 3 # Copyright (c) 2019 Kevin J. Walters 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 13 # all 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 21 # THE SOFTWARE. 22 """ 23 `adafruit_midi.midi_message` 24 ================================================================================ 25 26 An abstract class for objects which represent MIDI messages (events). 27 When individual messages are imported they register themselves with 28 :func:register_message_type which makes them recognised 29 by the parser, :func:from_message_bytes. 30 31 Large messages like :class:SystemExclusive can only be parsed if they fit 32 within the input buffer in :class:MIDI. 33 34 35 * Author(s): Kevin J. Walters 36 37 Implementation Notes 38 -------------------- 39 40 """ 41 42 __version__ = "0.0.0-auto.0" 43 __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_MIDI.git" 44 45 # From C3 - A and B are above G 46 # Semitones A B C D E F G 47 NOTE_OFFSET = [21, 23, 12, 14, 16, 17, 19] 48 49 50 def channel_filter(channel, channel_spec): 51 """ 52 Utility function to return True iff the given channel matches channel_spec. 53 """ 54 if isinstance(channel_spec, int): 55 return channel == channel_spec 56 if isinstance(channel_spec, tuple): 57 return channel in channel_spec 58 raise ValueError("Incorrect type for channel_spec" + str(type(channel_spec))) 59 60 61 def note_parser(note): 62 """If note is a string then it will be parsed and converted to a MIDI note (key) number, e.g. 63 "C4" will return 60, "C#4" will return 61. If note is not a string it will simply be returned. 64 65 :param note: Either 0-127 int or a str representing the note, e.g. "C#4" 66 """ 67 midi_note = note 68 if isinstance(note, str): 69 if len(note) < 2: 70 raise ValueError("Bad note format") 71 noteidx = ord(note[0].upper()) - 65 # 65 os ord('A') 72 if not 0 <= noteidx <= 6: 73 raise ValueError("Bad note") 74 sharpen = 0 75 if note[1] == "#": 76 sharpen = 1 77 elif note[1] == "b": 78 sharpen = -1 79 # int may throw exception here 80 midi_note = int(note[1 + abs(sharpen) :]) * 12 + NOTE_OFFSET[noteidx] + sharpen 81 82 return midi_note 83 84 85 class MIDIMessage: 86 """ 87 The parent class for MIDI messages. 88 89 Class variables: 90 91 * ``_STATUS`` - extracted from status byte with channel replaced by 0s 92 (high bit is always set by convention). 93 * ``_STATUSMASK`` - mask used to compared a status byte with ``_STATUS`` value. 94 * ``LENGTH`` - length for a fixed size message *including* status 95 or -1 for variable length. 96 * ``CHANNELMASK`` - mask used to apply a (wire protocol) channel number. 97 * ``ENDSTATUS`` - the end of message status byte, only set for variable length. 98 99 This is an *abstract* class. 100 """ 101 102 _STATUS = None 103 _STATUSMASK = None 104 LENGTH = None 105 CHANNELMASK = 0x0F 106 ENDSTATUS = None 107 108 # Commonly used exceptions to save memory 109 _EX_VALUEERROR_OOR = ValueError("Out of range") 110 111 # Each element is ((status, mask), class) 112 # order is more specific masks first 113 _statusandmask_to_class = [] 114 115 def __init__(self, *, channel=None): 116 self._channel = channel # dealing with pylint inadequacy 117 self.channel = channel 118 119 @property 120 def channel(self): 121 """The channel number of the MIDI message where appropriate. 122 This is *updated* by MIDI.send() method. 123 """ 124 return self._channel 125 126 @channel.setter 127 def channel(self, channel): 128 if channel is not None and not 0 <= channel <= 15: 129 raise "Channel must be 0-15 or None" 130 self._channel = channel 131 132 @classmethod 133 def register_message_type(cls): 134 """Register a new message by its status value and mask. 135 This is called automagically at ``import`` time for each message. 136 """ 137 ### These must be inserted with more specific masks first 138 insert_idx = len(MIDIMessage._statusandmask_to_class) 139 for idx, m_type in enumerate(MIDIMessage._statusandmask_to_class): 140 if cls._STATUSMASK > m_type[0][1]: 141 insert_idx = idx 142 break 143 144 MIDIMessage._statusandmask_to_class.insert( 145 insert_idx, ((cls._STATUS, cls._STATUSMASK), cls) 146 ) 147 148 # pylint: disable=too-many-arguments 149 @classmethod 150 def _search_eom_status(cls, buf, eom_status, msgstartidx, msgendidxplusone, endidx): 151 good_termination = False 152 bad_termination = False 153 154 msgendidxplusone = msgstartidx + 1 155 while msgendidxplusone <= endidx: 156 # Look for a status byte 157 # Second rule of the MIDI club is status bytes have MSB set 158 if buf[msgendidxplusone] & 0x80: 159 # pylint: disable=simplifiable-if-statement 160 if buf[msgendidxplusone] == eom_status: 161 good_termination = True 162 else: 163 bad_termination = True 164 break 165 msgendidxplusone += 1 166 167 if good_termination or bad_termination: 168 msgendidxplusone += 1 169 170 return (msgendidxplusone, good_termination, bad_termination) 171 172 @classmethod 173 def _match_message_status(cls, buf, msgstartidx, msgendidxplusone, endidx): 174 msgclass = None 175 status = buf[msgstartidx] 176 known_msg = False 177 complete_msg = False 178 bad_termination = False 179 180 # Rummage through our list looking for a status match 181 for status_mask, msgclass in MIDIMessage._statusandmask_to_class: 182 masked_status = status & status_mask[1] 183 if status_mask[0] == masked_status: 184 known_msg = True 185 # Check there's enough left to parse a complete message 186 # this value can be changed later for a var. length msgs 187 complete_msg = len(buf) - msgstartidx >= msgclass.LENGTH 188 if not complete_msg: 189 break 190 191 if msgclass.LENGTH < 0: # indicator of variable length message 192 ( 193 msgendidxplusone, 194 terminated_msg, 195 bad_termination, 196 ) = cls._search_eom_status( 197 buf, msgclass.ENDSTATUS, msgstartidx, msgendidxplusone, endidx 198 ) 199 if not terminated_msg: 200 complete_msg = False 201 else: # fixed length message 202 msgendidxplusone = msgstartidx + msgclass.LENGTH 203 break 204 205 return ( 206 msgclass, 207 status, 208 known_msg, 209 complete_msg, 210 bad_termination, 211 msgendidxplusone, 212 ) 213 214 # pylint: disable=too-many-locals,too-many-branches 215 @classmethod 216 def from_message_bytes(cls, midibytes, channel_in): 217 """Create an appropriate object of the correct class for the 218 first message found in some MIDI bytes filtered by channel_in. 219 220 Returns (messageobject, endplusone, skipped) 221 or for no messages, partial messages or messages for other channels 222 (None, endplusone, skipped). 223 """ 224 endidx = len(midibytes) - 1 225 skipped = 0 226 preamble = True 227 228 msgstartidx = 0 229 msgendidxplusone = 0 230 while True: 231 msg = None 232 # Look for a status byte 233 # Second rule of the MIDI club is status bytes have MSB set 234 while msgstartidx <= endidx and not midibytes[msgstartidx] & 0x80: 235 msgstartidx += 1 236 if preamble: 237 skipped += 1 238 preamble = False 239 240 # Either no message or a partial one 241 if msgstartidx > endidx: 242 return (None, endidx + 1, skipped) 243 244 # Try and match the status byte found in midibytes 245 ( 246 msgclass, 247 status, 248 known_message, 249 complete_message, 250 bad_termination, 251 msgendidxplusone, 252 ) = cls._match_message_status( 253 midibytes, msgstartidx, msgendidxplusone, endidx 254 ) 255 channel_match_orna = True 256 if complete_message and not bad_termination: 257 try: 258 msg = msgclass.from_bytes(midibytes[msgstartidx:msgendidxplusone]) 259 if msg.channel is not None: 260 channel_match_orna = channel_filter(msg.channel, channel_in) 261 262 except (ValueError, TypeError) as ex: 263 msg = MIDIBadEvent(midibytes[msgstartidx:msgendidxplusone], ex) 264 265 # break out of while loop for a complete message on good channel 266 # or we have one we do not know about 267 if known_message: 268 if complete_message: 269 if channel_match_orna: 270 break 271 # advance to next message 272 msgstartidx = msgendidxplusone 273 else: 274 # Important case of a known message but one that is not 275 # yet complete - leave bytes in buffer and wait for more 276 break 277 else: 278 msg = MIDIUnknownEvent(status) 279 # length cannot be known 280 # next read will skip past leftover data bytes 281 msgendidxplusone = msgstartidx + 1 282 break 283 284 return (msg, msgendidxplusone, skipped) 285 286 # A default method for constructing wire messages with no data. 287 # Returns an (immutable) bytes with just the status code in. 288 def __bytes__(self): 289 """Return the ``bytes`` wire protocol representation of the object 290 with channel number applied where appropriate.""" 291 return bytes([self._STATUS]) 292 293 # databytes value present to keep interface uniform but unused 294 # A default method for constructing message objects with no data. 295 # Returns the new object. 296 # pylint: disable=unused-argument 297 @classmethod 298 def from_bytes(cls, msg_bytes): 299 """Creates an object from the byte stream of the wire protocol 300 representation of the MIDI message.""" 301 return cls() 302 303 304 # DO NOT try to register these messages 305 class MIDIUnknownEvent(MIDIMessage): 306 """An unknown MIDI message. 307 308 :param int status: The MIDI status number. 309 310 This can either occur because there is no class representing the message 311 or because it is not imported. 312 """ 313 314 LENGTH = -1 315 316 def __init__(self, status): 317 self.status = status 318 super().__init__() 319 320 321 class MIDIBadEvent(MIDIMessage): 322 """A bad MIDI message, one that could not be parsed/constructed. 323 324 :param list data: The MIDI status including any embedded channel number 325 and associated subsequent data bytes. 326 :param Exception exception: The exception used to store the repr() text representation. 327 328 This could be due to status bytes appearing where data bytes are expected. 329 The channel property will not be set. 330 """ 331 332 LENGTH = -1 333 334 def __init__(self, msg_bytes, exception): 335 self.data = bytes(msg_bytes) 336 self.exception_text = repr(exception) 337 super().__init__()