/ 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__()