Skip to content

Commit d78b0f0

Browse files
author
Kevin J Walters
committed
Converting to a MIDI message format which stores channel number as a property based on @tannewt feedback from adafruit#9.
This changes MIDI.receive() to return just the object and MIDI.send() now has the side-effect of setting the channel on each message. Pushing CHANNELMASK into the parent MIDIMessage as it is no longer used as an indicator of presence/use of channel. MIDIBadEvent now includes the status number in data. Objects are now constructed on messages regardless of matching channel number as input channel test comes afterwards now. Lots of associated changes to unit tests plus a few doc updates. adafruit#3
1 parent dfb307f commit d78b0f0

12 files changed

+268
-240
lines changed

adafruit_midi/__init__.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -127,8 +127,7 @@ def receive(self):
127127
and return the first MIDI message (event).
128128
This maintains the blocking characteristics of the midi_in port.
129129
130-
:returns (MIDIMessage object, int channel): Returns object and channel
131-
or (None, None) for nothing.
130+
:returns MIDIMessage object: Returns object or None for nothing.
132131
"""
133132
### could check _midi_in is an object OR correct object OR correct interface here?
134133
# If the buffer here is not full then read as much as we can fit from
@@ -142,7 +141,7 @@ def receive(self):
142141
del bytes_in
143142

144143
(msg, endplusone,
145-
skipped, channel) = MIDIMessage.from_message_bytes(self._in_buf, self._in_channel)
144+
skipped) = MIDIMessage.from_message_bytes(self._in_buf, self._in_channel)
146145
if endplusone != 0:
147146
# This is not particularly efficient as it's copying most of bytearray
148147
# and deleting old one
@@ -151,23 +150,26 @@ def receive(self):
151150
self._skipped_bytes += skipped
152151

153152
# msg could still be None at this point, e.g. in middle of monster SysEx
154-
return (msg, channel)
153+
return msg
155154

156155
def send(self, msg, channel=None):
157156
"""Sends a MIDI message.
158157
159158
:param msg: Either a MIDIMessage object or a sequence (list) of MIDIMessage objects.
159+
The channel property will be *updated* as a side-effect of sending message(s).
160160
:param int channel: Channel number, if not set the ``out_channel`` will be used.
161161
162162
"""
163163
if channel is None:
164164
channel = self.out_channel
165165
if isinstance(msg, MIDIMessage):
166-
data = msg.as_bytes(channel=channel)
166+
msg.channel = channel
167+
data = bytes(msg)
167168
else:
168169
data = bytearray()
169170
for each_msg in msg:
170-
data.extend(each_msg.as_bytes(channel=channel))
171+
each_msg.channel = channel
172+
data.extend(bytes(each_msg))
171173

172174
self._send(data, len(data))
173175

adafruit_midi/channel_pressure.py

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -48,20 +48,19 @@ class ChannelPressure(MIDIMessage):
4848
_STATUS = 0xd0
4949
_STATUSMASK = 0xf0
5050
LENGTH = 2
51-
CHANNELMASK = 0x0f
5251

53-
def __init__(self, pressure):
52+
def __init__(self, pressure, *, channel=None):
5453
self.pressure = pressure
54+
super().__init__(channel=channel)
5555
if not 0 <= self.pressure <= 127:
5656
raise self._EX_VALUEERROR_OOR
5757

58-
# channel value is mandatory
59-
def as_bytes(self, *, channel=None):
60-
return bytearray([self._STATUS | (channel & self.CHANNELMASK),
61-
self.pressure])
58+
def __bytes__(self):
59+
return bytes([self._STATUS | (self.channel & self.CHANNELMASK),
60+
self.pressure])
6261

6362
@classmethod
64-
def from_bytes(cls, databytes):
65-
return cls(databytes[0])
63+
def from_bytes(cls, msg_bytes):
64+
return cls(msg_bytes[1], channel=msg_bytes[0] & cls.CHANNELMASK)
6665

6766
ChannelPressure.register_message_type()

adafruit_midi/control_change.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -50,21 +50,21 @@ class ControlChange(MIDIMessage):
5050
_STATUS = 0xb0
5151
_STATUSMASK = 0xf0
5252
LENGTH = 3
53-
CHANNELMASK = 0x0f
5453

55-
def __init__(self, control, value):
54+
def __init__(self, control, value, *, channel=None):
5655
self.control = control
5756
self.value = value
57+
super().__init__(channel=channel)
5858
if not 0 <= self.control <= 127 or not 0 <= self.value <= 127:
5959
raise self._EX_VALUEERROR_OOR
6060

61-
# channel value is mandatory
62-
def as_bytes(self, *, channel=None):
63-
return bytearray([self._STATUS | (channel & self.CHANNELMASK),
64-
self.control, self.value])
61+
def __bytes__(self):
62+
return bytes([self._STATUS | (self.channel & self.CHANNELMASK),
63+
self.control, self.value])
6564

6665
@classmethod
67-
def from_bytes(cls, databytes):
68-
return cls(databytes[0], databytes[1])
66+
def from_bytes(cls, msg_bytes):
67+
return cls(msg_bytes[1], msg_bytes[2],
68+
channel=msg_bytes[0] & cls.CHANNELMASK)
6969

7070
ControlChange.register_message_type()

adafruit_midi/midi_message.py

Lines changed: 49 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ class MIDIMessage:
111111
_STATUS = None
112112
_STATUSMASK = None
113113
LENGTH = None
114-
CHANNELMASK = None
114+
CHANNELMASK = 0x0f
115115
ENDSTATUS = None
116116

117117
# Commonly used exceptions to save memory
@@ -121,6 +121,24 @@ class MIDIMessage:
121121
# order is more specific masks first
122122
_statusandmask_to_class = []
123123

124+
def __init__(self, *, channel=None):
125+
### TODO - can i kwargs this?????
126+
self._channel = channel # dealing with pylint inadequacy
127+
self.channel = channel
128+
129+
@property
130+
def channel(self):
131+
"""The channel number of the MIDI message where appropriate.
132+
This is *updated* by MIDI.send() method.
133+
"""
134+
return self._channel
135+
136+
@channel.setter
137+
def channel(self, channel):
138+
if channel is not None and not 0 <= channel <= 15:
139+
raise "channel must be 0-15 or None"
140+
self._channel = channel
141+
124142
@classmethod
125143
def register_message_type(cls):
126144
"""Register a new message by its status value and mask.
@@ -161,16 +179,13 @@ def _search_eom_status(cls, buf, eom_status, msgstartidx, msgendidxplusone, endi
161179

162180
return (msgendidxplusone, good_termination, bad_termination)
163181

164-
# pylint: disable=too-many-arguments,too-many-locals
165182
@classmethod
166-
def _match_message_status(cls, buf, channel_in, msgstartidx, msgendidxplusone, endidx):
183+
def _match_message_status(cls, buf, msgstartidx, msgendidxplusone, endidx):
167184
msgclass = None
168185
status = buf[msgstartidx]
169186
known_msg = False
170187
complete_msg = False
171188
bad_termination = False
172-
channel_match_orna = True
173-
channel = None
174189

175190
# Rummage through our list looking for a status match
176191
for status_mask, msgclass in MIDIMessage._statusandmask_to_class:
@@ -183,10 +198,6 @@ def _match_message_status(cls, buf, channel_in, msgstartidx, msgendidxplusone, e
183198
if not complete_msg:
184199
break
185200

186-
if msgclass.CHANNELMASK is not None:
187-
channel = status & msgclass.CHANNELMASK
188-
channel_match_orna = channel_filter(channel, channel_in)
189-
190201
if msgclass.LENGTH < 0: # indicator of variable length message
191202
(msgendidxplusone,
192203
terminated_msg,
@@ -203,26 +214,26 @@ def _match_message_status(cls, buf, channel_in, msgstartidx, msgendidxplusone, e
203214

204215
return (msgclass, status,
205216
known_msg, complete_msg, bad_termination,
206-
channel_match_orna, channel, msgendidxplusone)
217+
msgendidxplusone)
207218

219+
# pylint: disable=too-many-locals,too-many-branches
208220
@classmethod
209221
def from_message_bytes(cls, midibytes, channel_in):
210222
"""Create an appropriate object of the correct class for the
211-
first message found in some MIDI bytes.
223+
first message found in some MIDI bytes filtered by channel_in.
212224
213-
Returns (messageobject, endplusone, skipped, channel)
225+
Returns (messageobject, endplusone, skipped)
214226
or for no messages, partial messages or messages for other channels
215-
(None, endplusone, skipped, None).
227+
(None, endplusone, skipped).
216228
"""
217-
msg = None
218229
endidx = len(midibytes) - 1
219230
skipped = 0
220231
preamble = True
221-
channel = None
222232

223233
msgstartidx = 0
224234
msgendidxplusone = 0
225235
while True:
236+
msg = None
226237
# Look for a status byte
227238
# Second rule of the MIDI club is status bytes have MSB set
228239
while msgstartidx <= endidx and not midibytes[msgstartidx] & 0x80:
@@ -233,27 +244,27 @@ def from_message_bytes(cls, midibytes, channel_in):
233244

234245
# Either no message or a partial one
235246
if msgstartidx > endidx:
236-
return (None, endidx + 1, skipped, None)
247+
return (None, endidx + 1, skipped)
237248

238249
# Try and match the status byte found in midibytes
239250
(msgclass,
240251
status,
241252
known_message,
242253
complete_message,
243254
bad_termination,
244-
channel_match_orna,
245-
channel,
246255
msgendidxplusone) = cls._match_message_status(midibytes,
247-
channel_in,
248256
msgstartidx,
249257
msgendidxplusone,
250258
endidx)
251-
252-
if complete_message and not bad_termination and channel_match_orna:
259+
channel_match_orna = True
260+
if complete_message and not bad_termination:
253261
try:
254-
msg = msgclass.from_bytes(midibytes[msgstartidx+1:msgendidxplusone])
262+
msg = msgclass.from_bytes(midibytes[msgstartidx:msgendidxplusone])
263+
if msg.channel is not None:
264+
channel_match_orna = channel_filter(msg.channel, channel_in)
265+
255266
except(ValueError, TypeError) as ex:
256-
msg = MIDIBadEvent(midibytes[msgstartidx+1:msgendidxplusone], ex)
267+
msg = MIDIBadEvent(midibytes[msgstartidx:msgendidxplusone], ex)
257268

258269
# break out of while loop for a complete message on good channel
259270
# or we have one we do not know about
@@ -274,24 +285,23 @@ def from_message_bytes(cls, midibytes, channel_in):
274285
msgendidxplusone = msgstartidx + 1
275286
break
276287

277-
return (msg, msgendidxplusone, skipped, channel)
288+
return (msg, msgendidxplusone, skipped)
278289

279-
# channel value present to keep interface uniform but unused
280290
# A default method for constructing wire messages with no data.
281-
# Returns a (mutable) bytearray with just the status code in.
282-
# pylint: disable=unused-argument
283-
def as_bytes(self, *, channel=None):
284-
"""Return the ``bytearray`` wire protocol representation of the object."""
285-
return bytearray([self._STATUS])
291+
# Returns an (immutable) bytes with just the status code in.
292+
def __bytes__(self):
293+
"""Return the ``bytes`` wire protocol representation of the object
294+
with channel number applied where appropriate."""
295+
return bytes([self._STATUS])
286296

287297
# databytes value present to keep interface uniform but unused
288298
# A default method for constructing message objects with no data.
289299
# Returns the new object.
290300
# pylint: disable=unused-argument
291301
@classmethod
292-
def from_bytes(cls, databytes):
302+
def from_bytes(cls, msg_bytes):
293303
"""Creates an object from the byte stream of the wire protocol
294-
(not including the first status byte)."""
304+
representation of the MIDI message."""
295305
return cls()
296306

297307

@@ -308,18 +318,22 @@ class MIDIUnknownEvent(MIDIMessage):
308318

309319
def __init__(self, status):
310320
self.status = status
321+
super().__init__()
311322

312323

313324
class MIDIBadEvent(MIDIMessage):
314325
"""A bad MIDI message, one that could not be parsed/constructed.
315326
316-
:param list data: The MIDI status number.
327+
:param list data: The MIDI status including any embedded channel number
328+
and associated subsequent data bytes.
317329
:param Exception exception: The exception used to store the repr() text representation.
318330
319331
This could be due to status bytes appearing where data bytes are expected.
332+
The channel property will not be set.
320333
"""
321334
LENGTH = -1
322335

323-
def __init__(self, data, exception):
324-
self.data = bytearray(data)
336+
def __init__(self, msg_bytes, exception):
337+
self.data = bytes(msg_bytes)
325338
self.exception_text = repr(exception)
339+
super().__init__()

adafruit_midi/note_off.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -51,21 +51,21 @@ class NoteOff(MIDIMessage):
5151
_STATUS = 0x80
5252
_STATUSMASK = 0xf0
5353
LENGTH = 3
54-
CHANNELMASK = 0x0f
5554

56-
def __init__(self, note, velocity):
55+
def __init__(self, note, velocity, *, channel=None):
5756
self.note = note_parser(note)
5857
self.velocity = velocity
58+
super().__init__(channel=channel)
5959
if not 0 <= self.note <= 127 or not 0 <= self.velocity <= 127:
6060
raise self._EX_VALUEERROR_OOR
6161

62-
# channel value is mandatory
63-
def as_bytes(self, *, channel=None):
64-
return bytearray([self._STATUS | (channel & self.CHANNELMASK),
65-
self.note, self.velocity])
62+
def __bytes__(self):
63+
return bytes([self._STATUS | (self.channel & self.CHANNELMASK),
64+
self.note, self.velocity])
6665

6766
@classmethod
68-
def from_bytes(cls, databytes):
69-
return cls(databytes[0], databytes[1])
67+
def from_bytes(cls, msg_bytes):
68+
return cls(msg_bytes[1], msg_bytes[2],
69+
channel=msg_bytes[0] & cls.CHANNELMASK)
7070

7171
NoteOff.register_message_type()

adafruit_midi/note_on.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -51,21 +51,21 @@ class NoteOn(MIDIMessage):
5151
_STATUS = 0x90
5252
_STATUSMASK = 0xf0
5353
LENGTH = 3
54-
CHANNELMASK = 0x0f
5554

56-
def __init__(self, note, velocity):
55+
def __init__(self, note, velocity, *, channel=None):
5756
self.note = note_parser(note)
5857
self.velocity = velocity
58+
super().__init__(channel=channel)
5959
if not 0 <= self.note <= 127 or not 0 <= self.velocity <= 127:
6060
raise self._EX_VALUEERROR_OOR
6161

62-
# channel value is mandatory
63-
def as_bytes(self, *, channel=None):
64-
return bytearray([self._STATUS | (channel & self.CHANNELMASK),
65-
self.note, self.velocity])
62+
def __bytes__(self):
63+
return bytes([self._STATUS | (self.channel & self.CHANNELMASK),
64+
self.note, self.velocity])
6665

6766
@classmethod
68-
def from_bytes(cls, databytes):
69-
return cls(databytes[0], databytes[1])
67+
def from_bytes(cls, msg_bytes):
68+
return cls(msg_bytes[1], msg_bytes[2],
69+
channel=msg_bytes[0] & cls.CHANNELMASK)
7070

7171
NoteOn.register_message_type()

adafruit_midi/pitch_bend.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -49,21 +49,21 @@ class PitchBend(MIDIMessage):
4949
_STATUS = 0xe0
5050
_STATUSMASK = 0xf0
5151
LENGTH = 3
52-
CHANNELMASK = 0x0f
5352

54-
def __init__(self, pitch_bend):
53+
def __init__(self, pitch_bend, *, channel=None):
5554
self.pitch_bend = pitch_bend
55+
super().__init__(channel=channel)
5656
if not 0 <= self.pitch_bend <= 16383:
5757
raise self._EX_VALUEERROR_OOR
5858

59-
# channel value is mandatory
60-
def as_bytes(self, *, channel=None):
61-
return bytearray([self._STATUS | (channel & self.CHANNELMASK),
62-
self.pitch_bend & 0x7f,
63-
(self.pitch_bend >> 7) & 0x7f])
59+
def __bytes__(self):
60+
return bytes([self._STATUS | (self.channel & self.CHANNELMASK),
61+
self.pitch_bend & 0x7f,
62+
(self.pitch_bend >> 7) & 0x7f])
6463

6564
@classmethod
66-
def from_bytes(cls, databytes):
67-
return cls(databytes[1] << 7 | databytes[0])
65+
def from_bytes(cls, msg_bytes):
66+
return cls(msg_bytes[2] << 7 | msg_bytes[1],
67+
channel=msg_bytes[0] & cls.CHANNELMASK)
6868

6969
PitchBend.register_message_type()

0 commit comments

Comments
 (0)