From c1d676089870255ddcc5647ad037d753cad39546 Mon Sep 17 00:00:00 2001 From: Kevin J Walters Date: Wed, 27 Feb 2019 01:09:54 +0000 Subject: [PATCH 01/92] First implementation of reading MIDI messages using read() against _midi_in and buffering up that data in an internal buffer. Using a new MIDIMessage object with children which represent each MIDI event type as suggested in ticket. Send code left as is for now - must preserve interface as it is in use. #3 --- adafruit_midi.py | 169 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 166 insertions(+), 3 deletions(-) diff --git a/adafruit_midi.py b/adafruit_midi.py index 40094f0..9bb9f0a 100644 --- a/adafruit_midi.py +++ b/adafruit_midi.py @@ -1,6 +1,6 @@ # The MIT License (MIT) # -# Copyright (c) 2019 Limor Fried for Adafruit Industries +# Copyright (c) 2019 Limor Fried for Adafruit Industries, Kevin J. Walters # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -26,7 +26,7 @@ A CircuitPython helper for encoding/decoding MIDI packets over a MIDI or UART connection. -* Author(s): Limor Fried +* Author(s): Limor Fried, Kevin J. Walters Implementation Notes -------------------- @@ -47,6 +47,145 @@ __version__ = "0.0.0-auto.0" __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_MIDI.git" + +# TODO TBD: can relocate this class later to a separate file if recommended +class MIDIMessage: + """ + A MIDI message: + - Status - extracted from Status byte with channel replaced by 0s + (high bit always set) + - Channel - extracted from Status where present (0-15) + - 0 or more Data Byte(s) - high bit always not set for data + - _LENGTH is the fixed message length including status or -1 for variable length + - _ENDSTATUS is the EOM status byte if relevant + This is an abstract class. + """ + _STATUS = None + _STATUSMASK = None + _LENGTH = None + _ENDSTATUS = None + + ### Each element is ((status, mask), class) + _statusandmask_to_class = [] + + @classmethod + def register_message_type(cls): + """Register a new message by its status value and mask + """ + + ### TODO Why is not cls ? is this to avoid it ending up in subclass? + MIDIMessage._statusandmask_to_class.append(((cls._STATUS, cls._STATUSMASK), cls)) + + @classmethod + def from_bytes(cls, midibytes): + """Create an appropriate object of the correct class for the first message found in + some MIDI bytes. + Returns (messageobject, start, endplusone) or None for no message or partial message. + """ + + msg = None + startidx = 0 + endidx = len(midibytes) - 1 + + # Look for a status byte + # Second rule of the MIDI club is status bytes have MSB set + while startidx <= endidx and not (midibytes[startidx] & 0x80): + startidx += 1 + + # Either no message or a partial one + if startidx > endidx: + return None + + status = midibytes[startidx] + msgendidx = -1 + # Rummage through our list looking for variable bitness status match + for (sm, msgclass) in MIDIMessage._statusandmask_to_class: + maskedstatus = status & sm[1] + if sm[0] == maskedstatus: + # Check there's enough left to parse a complete message + if len(midibytes) - startidx >= msgclass._LENGTH: + if msgclass._LENGTH < 0: + # TODO code this properly + msgendidxplusone = endidx + 1 # TODO NOT CORRECT + else: + msgendidxplusone = startidx + msgclass._LENGTH + msg = msgclass.from_bytes(midibytes[startidx+1:msgendidxplusone]) + break + + ### TODO correct to handle a buffer with start of big SysEx + ### TODO correct to handle a buffer in middle of big SysEx + ### TODO correct to handle a buffer with end portion of big SysEx + if msg is not None: + return (msg, startidx, msgendidxplusone) + else: + return None + + +# TODO - do i omit Change word from these +class NoteOn(MIDIMessage): + _STATUS = 0x80 + _STATUSMASK = 0xf0 + _LENGTH = 3 + + def __init__(self, note, vel): + self.note = note + self.vel = vel + + @classmethod + def from_bytes(cls, databytes): + return cls(databytes[0], databytes[1]) + +NoteOn.register_message_type() + + +class NoteOff(MIDIMessage): + _STATUS = 0x90 + _STATUSMASK = 0xf0 + _LENGTH = 3 + + def __init__(self, note, vel): + self.note = note + self.vel = vel + + @classmethod + def from_bytes(cls, databytes): + return cls(databytes[0], databytes[1]) + +NoteOff.register_message_type() + + +class ControlChange(MIDIMessage): + _STATUS = 0xb0 + _STATUSMASK = 0xf0 + _LENGTH = 3 + + def __init__(self, control, value): + self.control = control + self.value = value + + @classmethod + def from_bytes(cls, databytes): + return cls(databytes[0], databytes[1]) + +ControlChange.register_message_type() + + +class PitchBendChange(MIDIMessage): + _STATUS = 0xe0 + _STATUSMASK = 0xf0 + _LENGTH = 3 + + def __init__(self, value): + self.value = value + + @classmethod + def from_bytes(cls, databytes): + return cls(databytes[1] << 7 | databytes[0]) + +PitchBendChange.register_message_type() + + + class MIDI: """MIDI helper class.""" @@ -56,12 +195,15 @@ class MIDI: CONTROL_CHANGE = 0xB0 def __init__(self, midi_in=usb_midi.ports[0], midi_out=usb_midi.ports[1], *, in_channel=None, - out_channel=0, debug=False): + out_channel=0, debug=False, in_buf_size=30): self._midi_in = midi_in self._midi_out = midi_out self._in_channel = in_channel self._out_channel = out_channel self._debug = debug + # This input buffer holds what has been read from midi_in + self._inbuf = bytearray(0) + self._inbuf_size = in_buf_size self._outbuf = bytearray(4) @property @@ -88,6 +230,26 @@ def out_channel(self, channel): raise RuntimeError("Invalid output channel") self._out_channel = channel + ### TODO - consider naming here and channel selection and omni mode + def read_in_port(self): + ### could check _midi_in is an object OR correct object OR correct interface here? + # If the buffer here is not full then read as much as we can fit from + # the input port + if len(self._inbuf) < self._inbuf_size: + self._inbuf.extend(self._midi_in.read(self._inbuf_size - len(self._inbuf))) + + msgse = MIDIMessage.from_bytes(self._inbuf) + if msgse is not None: + (msg, start, endplusone) = msgse + # This is not particularly efficient as it's copying most of bytearray + # and deleting old one + self._inbuf = self._inbuf[endplusone:] + # msg could still be None at this point, e.g. in middle of monster SysEx + return msg + else: + return None + + def note_on(self, note, vel, channel=None): """Sends a MIDI Note On message. @@ -130,6 +292,7 @@ def _generic_3(self, cmd, arg1, arg2, channel=None): raise RuntimeError("Argument 1 value %d invalid" % arg1) if not 0 <= arg2 <= 0x7F: raise RuntimeError("Argument 2 value %d invalid" % arg2) + ### TODO - change this to use is operator and range check or mask it if not channel: channel = self._out_channel self._outbuf[0] = (cmd & 0xF0) | channel From 4202499568df9f2fecd5ddf808437a45fe585717 Mon Sep 17 00:00:00 2001 From: Kevin J Walters Date: Wed, 27 Feb 2019 16:30:33 +0000 Subject: [PATCH 02/92] Added a MIDIUnknownEvent to deal with anything the module does not know about. Channel filtering NOT YET implemented. #3 --- adafruit_midi.py | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/adafruit_midi.py b/adafruit_midi.py index 9bb9f0a..66b8769 100644 --- a/adafruit_midi.py +++ b/adafruit_midi.py @@ -98,10 +98,12 @@ def from_bytes(cls, midibytes): status = midibytes[startidx] msgendidx = -1 + smfound = False # Rummage through our list looking for variable bitness status match for (sm, msgclass) in MIDIMessage._statusandmask_to_class: maskedstatus = status & sm[1] if sm[0] == maskedstatus: + smfound = True # Check there's enough left to parse a complete message if len(midibytes) - startidx >= msgclass._LENGTH: if msgclass._LENGTH < 0: @@ -112,6 +114,12 @@ def from_bytes(cls, midibytes): msg = msgclass.from_bytes(midibytes[startidx+1:msgendidxplusone]) break + if not smfound: + msg = MIDIUnknownEvent(status) + # length cannot be known + # next read will skip past leftover data bytes + msgendidxplusone = startidx + 1 + ### TODO correct to handle a buffer with start of big SysEx ### TODO correct to handle a buffer in middle of big SysEx ### TODO correct to handle a buffer with end portion of big SysEx @@ -185,6 +193,19 @@ def from_bytes(cls, databytes): PitchBendChange.register_message_type() +class MIDIUnknownEvent(MIDIMessage): + _LENGTH = -1 + + def __init__(self, status): + self.status = status + + @classmethod + def from_bytes(cls, status): + return cls(status) + + def register_message_type(cls): + return ValueError("DO NOT REGISTER THIS MESSAGE") + class MIDI: """MIDI helper class.""" @@ -238,6 +259,8 @@ def read_in_port(self): if len(self._inbuf) < self._inbuf_size: self._inbuf.extend(self._midi_in.read(self._inbuf_size - len(self._inbuf))) + # TODO - VERY IMPORTANT - WORK OUT HOW TO HANDLE CHANNEL FILTERING + # AND THINK ABOUT OMNI MODE msgse = MIDIMessage.from_bytes(self._inbuf) if msgse is not None: (msg, start, endplusone) = msgse @@ -247,7 +270,7 @@ def read_in_port(self): # msg could still be None at this point, e.g. in middle of monster SysEx return msg else: - return None + return None def note_on(self, note, vel, channel=None): From d585a51046b37fe06f3519e892a0c6e2dcee9e25 Mon Sep 17 00:00:00 2001 From: Kevin J Walters Date: Wed, 27 Feb 2019 16:31:47 +0000 Subject: [PATCH 03/92] midi_intest1 - simple loop reading data from MIDI Channel 1 but with some variable pauses thrown in to check buffering behaviour. #3 --- examples/midi_intest1.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 examples/midi_intest1.py diff --git a/examples/midi_intest1.py b/examples/midi_intest1.py new file mode 100644 index 0000000..ba88ca7 --- /dev/null +++ b/examples/midi_intest1.py @@ -0,0 +1,23 @@ +import time +import random +import adafruit_midi + +### 0 is MIDI channel 1 +midi = adafruit_midi.MIDI(in_channel=0) + +print("Midi test II") + +print("Input channel:", midi.in_channel) +print("Listening on input channel:", midi.in_channel) + +# play with the pause to simulate code doing other stuff +# in the loop +pauses = [0] * 10 + [.010] * 10 + [0.100] * 10 + [1.0] * 10 + +while True: + for pause in pauses: + msg = midi.read_in_port() + if msg is not None: + print(time.monotonic(), msg) + if pause: + time.sleep(pause) \ No newline at end of file From 0ce8bef0e02fed69fd9e8572b250f33f2a55234a Mon Sep 17 00:00:00 2001 From: Kevin J Walters Date: Thu, 28 Feb 2019 00:08:11 +0000 Subject: [PATCH 04/92] Fixing bug with _STATUS values transposed between NoteOn and NoteOff. #3 --- adafruit_midi.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/adafruit_midi.py b/adafruit_midi.py index 66b8769..fc3beb8 100644 --- a/adafruit_midi.py +++ b/adafruit_midi.py @@ -131,7 +131,7 @@ def from_bytes(cls, midibytes): # TODO - do i omit Change word from these class NoteOn(MIDIMessage): - _STATUS = 0x80 + _STATUS = 0x90 _STATUSMASK = 0xf0 _LENGTH = 3 @@ -147,7 +147,7 @@ def from_bytes(cls, databytes): class NoteOff(MIDIMessage): - _STATUS = 0x90 + _STATUS = 0x80 _STATUSMASK = 0xf0 _LENGTH = 3 From c9095870f91ac059f9158caf2f1e882b71d6b72e Mon Sep 17 00:00:00 2001 From: Kevin J Walters Date: Sat, 16 Mar 2019 00:07:55 +0000 Subject: [PATCH 05/92] register_message_type() now inserts in mask order to allow registration of messages in any order. Adding TimingClock, PolyphonicKeyPressure, ProgramChange and ChannelPressure messages. Removing some unnecessary methods in MIDIUnknownEvent. #3 --- adafruit_midi.py | 104 ++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 84 insertions(+), 20 deletions(-) diff --git a/adafruit_midi.py b/adafruit_midi.py index fc3beb8..4f0efd3 100644 --- a/adafruit_midi.py +++ b/adafruit_midi.py @@ -65,16 +65,23 @@ class MIDIMessage: _LENGTH = None _ENDSTATUS = None - ### Each element is ((status, mask), class) + # Each element is ((status, mask), class) + # order is more specific masks first _statusandmask_to_class = [] - + @classmethod def register_message_type(cls): - """Register a new message by its status value and mask + """Register a new message by its status value and mask. """ + ### These must be inserted with more specific masks first + insert_idx = len(MIDIMessage._statusandmask_to_class) + for idx, m_type in enumerate(MIDIMessage._statusandmask_to_class): + if cls._STATUSMASK > m_type[0][1]: + insert_idx = idx + break - ### TODO Why is not cls ? is this to avoid it ending up in subclass? - MIDIMessage._statusandmask_to_class.append(((cls._STATUS, cls._STATUSMASK), cls)) + MIDIMessage._statusandmask_to_class.insert(insert_idx, + ((cls._STATUS, cls._STATUSMASK), cls)) @classmethod def from_bytes(cls, midibytes): @@ -99,8 +106,8 @@ def from_bytes(cls, midibytes): status = midibytes[startidx] msgendidx = -1 smfound = False - # Rummage through our list looking for variable bitness status match - for (sm, msgclass) in MIDIMessage._statusandmask_to_class: + # Rummage through our list looking for a status match + for sm, msgclass in MIDIMessage._statusandmask_to_class: maskedstatus = status & sm[1] if sm[0] == maskedstatus: smfound = True @@ -129,7 +136,37 @@ def from_bytes(cls, midibytes): return None -# TODO - do i omit Change word from these +class TimingClock(MIDIMessage): + _STATUS = 0xf8 + _STATUSMASK = 0xff + _LENGTH = 1 + + def __init__(self): + pass + + @classmethod + def from_bytes(cls): + return cls() + +TimingClock.register_message_type() + + +class NoteOff(MIDIMessage): + _STATUS = 0x80 + _STATUSMASK = 0xf0 + _LENGTH = 3 + + def __init__(self, note, vel): + self.note = note + self.vel = vel + + @classmethod + def from_bytes(cls, databytes): + return cls(databytes[0], databytes[1]) + +NoteOff.register_message_type() + + class NoteOn(MIDIMessage): _STATUS = 0x90 _STATUSMASK = 0xf0 @@ -146,20 +183,20 @@ def from_bytes(cls, databytes): NoteOn.register_message_type() -class NoteOff(MIDIMessage): - _STATUS = 0x80 +class PolyphonicKeyPressure(MIDIMessage): + _STATUS = 0xa0 _STATUSMASK = 0xf0 _LENGTH = 3 - def __init__(self, note, vel): + def __init__(self, note, pressure): self.note = note - self.vel = vel + self.pressure = pressure @classmethod def from_bytes(cls, databytes): return cls(databytes[0], databytes[1]) -NoteOff.register_message_type() +PolyphonicKeyPressure.register_message_type() class ControlChange(MIDIMessage): @@ -178,6 +215,36 @@ def from_bytes(cls, databytes): ControlChange.register_message_type() +class ProgramChange(MIDIMessage): + _STATUS = 0xc0 + _STATUSMASK = 0xf0 + _LENGTH = 2 + + def __init__(self, patch): + self.patch = patch + + @classmethod + def from_bytes(cls, databytes): + return cls(databytes[0]) + +ProgramChange.register_message_type() + + +class ChannelPressure(MIDIMessage): + _STATUS = 0xd0 + _STATUSMASK = 0xf0 + _LENGTH = 2 + + def __init__(self, pressure): + self.pressure = pressure + + @classmethod + def from_bytes(cls, databytes): + return cls(databytes[0]) + +ChannelPressure.register_message_type() + + class PitchBendChange(MIDIMessage): _STATUS = 0xe0 _STATUSMASK = 0xf0 @@ -193,18 +260,12 @@ def from_bytes(cls, databytes): PitchBendChange.register_message_type() +# DO NOT try to register this message class MIDIUnknownEvent(MIDIMessage): _LENGTH = -1 def __init__(self, status): self.status = status - - @classmethod - def from_bytes(cls, status): - return cls(status) - - def register_message_type(cls): - return ValueError("DO NOT REGISTER THIS MESSAGE") class MIDI: @@ -261,6 +322,9 @@ def read_in_port(self): # TODO - VERY IMPORTANT - WORK OUT HOW TO HANDLE CHANNEL FILTERING # AND THINK ABOUT OMNI MODE + + ### TODO need to ensure code skips past unknown data/messages in buffer + ### aftertouch from Axiom 25 causes 6 in the buffer!! msgse = MIDIMessage.from_bytes(self._inbuf) if msgse is not None: (msg, start, endplusone) = msgse From fa3e5d49c9ce09f8bb65c768689ff96c07e5ec55 Mon Sep 17 00:00:00 2001 From: Kevin J Walters Date: Sat, 16 Mar 2019 01:14:18 +0000 Subject: [PATCH 06/92] Enhancing in_channel setter to accept ALL for a type of OMNI mode and a tuple of channels. Changing constructor to use setters to gain benefit of type/range checks. #3 --- adafruit_midi.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/adafruit_midi.py b/adafruit_midi.py index 4f0efd3..ad4fe6b 100644 --- a/adafruit_midi.py +++ b/adafruit_midi.py @@ -276,12 +276,14 @@ class MIDI: PITCH_BEND = 0xE0 CONTROL_CHANGE = 0xB0 + ALL_CHANNELS = -1 + def __init__(self, midi_in=usb_midi.ports[0], midi_out=usb_midi.ports[1], *, in_channel=None, out_channel=0, debug=False, in_buf_size=30): self._midi_in = midi_in self._midi_out = midi_out - self._in_channel = in_channel - self._out_channel = out_channel + self.in_channel = in_channel + self.out_channel = out_channel self._debug = debug # This input buffer holds what has been read from midi_in self._inbuf = bytearray(0) @@ -296,9 +298,14 @@ def in_channel(self): @in_channel.setter def in_channel(self, channel): - if channel is not None and not 0 <= channel <= 15: + if channel is None or (isinstance(channel, int) and 0 <= channel <= 15): + self._in_channel = channel + elif isinstance(channel, str) and channel == "ALL": + self._in_channel = self.ALL_CHANNELS + elif isinstance(channel, tuple) and all(0 <= c <= 15 for c in channel): + self._in_channel = channel + else: raise RuntimeError("Invalid input channel") - self._in_channel = channel @property def out_channel(self): From 6f8d51c33eb5c9a065c2e253d52deb8f1331f2f2 Mon Sep 17 00:00:00 2001 From: Kevin J Walters Date: Sat, 16 Mar 2019 18:37:43 +0000 Subject: [PATCH 07/92] Missed second arg to from_bytes() for data-less TimingClock which seemed superfluous but interface needs to be consistent. #3 --- adafruit_midi.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/adafruit_midi.py b/adafruit_midi.py index ad4fe6b..6eeb1f7 100644 --- a/adafruit_midi.py +++ b/adafruit_midi.py @@ -145,8 +145,8 @@ def __init__(self): pass @classmethod - def from_bytes(cls): - return cls() + def from_bytes(cls, databytes): + return cls() TimingClock.register_message_type() From e1cc85dd1c5130a875b08871f0944ecc7b91c240 Mon Sep 17 00:00:00 2001 From: Kevin J Walters Date: Sat, 16 Mar 2019 20:14:15 +0000 Subject: [PATCH 08/92] Renaming from_bytes() in MIDIMessage to from_message_bytes() as it is not the same thing. Adding Start and Stop messages. Punting the from_bytes for no data MIDI messages into parent and removing constructors for those as the default constructor does the job. #3 --- adafruit_midi.py | 36 +++++++++++++++++++++++++++--------- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/adafruit_midi.py b/adafruit_midi.py index 6eeb1f7..0de991d 100644 --- a/adafruit_midi.py +++ b/adafruit_midi.py @@ -84,7 +84,7 @@ def register_message_type(cls): ((cls._STATUS, cls._STATUSMASK), cls)) @classmethod - def from_bytes(cls, midibytes): + def from_message_bytes(cls, midibytes): """Create an appropriate object of the correct class for the first message found in some MIDI bytes. Returns (messageobject, start, endplusone) or None for no message or partial message. @@ -135,18 +135,36 @@ def from_bytes(cls, midibytes): else: return None + + @classmethod + def from_bytes(cls, databytes): + """A default method for constructing messages that have no data. + Returns the new object.""" + return cls() + + +class Start(MIDIMessage): + _STATUS = 0xfa + _STATUSMASK = 0xff + _LENGTH = 1 + +Start.register_message_type() + +class Stop(MIDIMessage): + _STATUS = 0xfc + _STATUSMASK = 0xff + _LENGTH = 1 + +Stop.register_message_type() + + +# Would be good to have this registered first as it occurs +# frequently when in-use class TimingClock(MIDIMessage): _STATUS = 0xf8 _STATUSMASK = 0xff _LENGTH = 1 - - def __init__(self): - pass - - @classmethod - def from_bytes(cls, databytes): - return cls() TimingClock.register_message_type() @@ -332,7 +350,7 @@ def read_in_port(self): ### TODO need to ensure code skips past unknown data/messages in buffer ### aftertouch from Axiom 25 causes 6 in the buffer!! - msgse = MIDIMessage.from_bytes(self._inbuf) + msgse = MIDIMessage.from_message_bytes(self._inbuf) if msgse is not None: (msg, start, endplusone) = msgse # This is not particularly efficient as it's copying most of bytearray From fb213ab89a2a02d640f976e81bd60b54110c04e8 Mon Sep 17 00:00:00 2001 From: Kevin J Walters Date: Sat, 16 Mar 2019 20:30:12 +0000 Subject: [PATCH 09/92] Giving PitchBendChange value a proper name - now pitch_bend. #3 --- adafruit_midi.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/adafruit_midi.py b/adafruit_midi.py index 0de991d..196f8f3 100644 --- a/adafruit_midi.py +++ b/adafruit_midi.py @@ -268,8 +268,8 @@ class PitchBendChange(MIDIMessage): _STATUSMASK = 0xf0 _LENGTH = 3 - def __init__(self, value): - self.value = value + def __init__(self, pitch_bend): + self.pitch_bend = pitch_bend @classmethod def from_bytes(cls, databytes): From 310e28ae5c029cdada436258a889239fde269a80 Mon Sep 17 00:00:00 2001 From: Kevin J Walters Date: Sun, 17 Mar 2019 00:54:46 +0000 Subject: [PATCH 10/92] Implementing the channel filtering with single input channel or "ALL" or tuple with multiple channels. read_in_port() now returns the message and actual channel (or None). Added SystemExclusive message but implementation of variable length messages is still incomplete. #3 --- adafruit_midi.py | 161 ++++++++++++++++++++++++++++++++--------------- 1 file changed, 109 insertions(+), 52 deletions(-) diff --git a/adafruit_midi.py b/adafruit_midi.py index 196f8f3..30eb6e4 100644 --- a/adafruit_midi.py +++ b/adafruit_midi.py @@ -48,7 +48,20 @@ __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_MIDI.git" +def channel_filter(channel, channel_spec): + if isinstance(channel_spec, int): + if channel_spec == MIDI.ALL_CHANNELS: + return True + else: + return channel == channel_spec + elif isinstance(channel_spec, tuple): + return channel in channel_spec + else: + raise ValueError("Incorrect type for channel_spec") + + # TODO TBD: can relocate this class later to a separate file if recommended +# need to work out what to do with channel_filter() and its use of MIDI.ALL_CHANNELS class MIDIMessage: """ A MIDI message: @@ -63,6 +76,7 @@ class MIDIMessage: _STATUS = None _STATUSMASK = None _LENGTH = None + _CHANNELMASK = None _ENDSTATUS = None # Each element is ((status, mask), class) @@ -82,66 +96,103 @@ def register_message_type(cls): MIDIMessage._statusandmask_to_class.insert(insert_idx, ((cls._STATUS, cls._STATUSMASK), cls)) - + + # TODO - this needs a lot of test cases to prove it actually works + # TODO - finish SysEx implementation and find something that sends one @classmethod - def from_message_bytes(cls, midibytes): - """Create an appropriate object of the correct class for the first message found in - some MIDI bytes. - Returns (messageobject, start, endplusone) or None for no message or partial message. + def from_message_bytes(cls, midibytes, channel_in): + """Create an appropriate object of the correct class for the + first message found in some MIDI bytes. + + Returns (messageobject, start, endplusone, channel) + or for no messages, partial messages or messages for other channels + (None, start, endplusone, None). """ msg = None startidx = 0 endidx = len(midibytes) - 1 - - # Look for a status byte - # Second rule of the MIDI club is status bytes have MSB set - while startidx <= endidx and not (midibytes[startidx] & 0x80): - startidx += 1 - # Either no message or a partial one - if startidx > endidx: - return None - - status = midibytes[startidx] - msgendidx = -1 - smfound = False - # Rummage through our list looking for a status match - for sm, msgclass in MIDIMessage._statusandmask_to_class: - maskedstatus = status & sm[1] - if sm[0] == maskedstatus: - smfound = True - # Check there's enough left to parse a complete message - if len(midibytes) - startidx >= msgclass._LENGTH: - if msgclass._LENGTH < 0: - # TODO code this properly - msgendidxplusone = endidx + 1 # TODO NOT CORRECT - else: - msgendidxplusone = startidx + msgclass._LENGTH - msg = msgclass.from_bytes(midibytes[startidx+1:msgendidxplusone]) + msgstartidx = startidx + while True: + # Look for a status byte + # Second rule of the MIDI club is status bytes have MSB set + while msgstartidx <= endidx and not (midibytes[msgstartidx] & 0x80): + msgstartidx += 1 + + # Either no message or a partial one + if msgstartidx > endidx: + return (None, startidx, endidx + 1, None) + + status = midibytes[msgstartidx] + msgendidxplusone = 0 + smfound = False + channel_match = True + channel = None + # Rummage through our list looking for a status match + for sm, msgclass in MIDIMessage._statusandmask_to_class: + masked_status = status & sm[1] + if sm[0] == masked_status: + smfound = True + # Check there's enough left to parse a complete message + if len(midibytes) - msgstartidx >= msgclass._LENGTH: + if msgclass._CHANNELMASK is not None: + channel = status & msgclass._CHANNELMASK + channel_match = channel_filter(channel, channel_in) + if msgclass._LENGTH < 0: + # TODO code this properly - THIS IS VARIABLE LENGTH MESSAGE + msgendidxplusone = endidx + 1 # TODO NOT CORRECT + else: + msgendidxplusone = msgstartidx + msgclass._LENGTH + + if channel_match: + msg = msgclass.from_bytes(midibytes[msgstartidx+1:msgendidxplusone]) + break # for + if smfound and channel_match: + break # while + elif not smfound: + msg = MIDIUnknownEvent(status) + # length cannot be known + # next read will skip past leftover data bytes + msgendidxplusone = msgstartidx + 1 break - - if not smfound: - msg = MIDIUnknownEvent(status) - # length cannot be known - # next read will skip past leftover data bytes - msgendidxplusone = startidx + 1 - + else: + msgstartidx = msgendidxplusone + ### TODO correct to handle a buffer with start of big SysEx ### TODO correct to handle a buffer in middle of big SysEx ### TODO correct to handle a buffer with end portion of big SysEx if msg is not None: - return (msg, startidx, msgendidxplusone) + return (msg, startidx, msgendidxplusone, channel) else: - return None + return (None, startidx, msgendidxplusone, None) - @classmethod def from_bytes(cls, databytes): """A default method for constructing messages that have no data. Returns the new object.""" return cls() - + + +class SystemExclusive(MIDIMessage): + _STATUS = 0xf0 + _STATUSMASK = 0xff + _LENGTH = -1 + _ENDSTATUS = 0xf7 + + def __init__(self, manufacturer_id, data): + self.manufacturer_id = manufacturer_id + self.data = data + + @classmethod + def from_bytes(cls, databytes): + if databytes[0] != 0: + return cls(databytes[0:1], databytes[1:]) + else: + return cls(databytes[0:3], databytes[3:]) + +SystemExclusive.register_message_type() + class Start(MIDIMessage): _STATUS = 0xfa @@ -173,6 +224,7 @@ class NoteOff(MIDIMessage): _STATUS = 0x80 _STATUSMASK = 0xf0 _LENGTH = 3 + _CHANNELMASK = 0x0f def __init__(self, note, vel): self.note = note @@ -189,6 +241,7 @@ class NoteOn(MIDIMessage): _STATUS = 0x90 _STATUSMASK = 0xf0 _LENGTH = 3 + _CHANNELMASK = 0x0f def __init__(self, note, vel): self.note = note @@ -205,6 +258,7 @@ class PolyphonicKeyPressure(MIDIMessage): _STATUS = 0xa0 _STATUSMASK = 0xf0 _LENGTH = 3 + _CHANNELMASK = 0x0f def __init__(self, note, pressure): self.note = note @@ -221,6 +275,7 @@ class ControlChange(MIDIMessage): _STATUS = 0xb0 _STATUSMASK = 0xf0 _LENGTH = 3 + _CHANNELMASK = 0x0f def __init__(self, control, value): self.control = control @@ -237,6 +292,7 @@ class ProgramChange(MIDIMessage): _STATUS = 0xc0 _STATUSMASK = 0xf0 _LENGTH = 2 + _CHANNELMASK = 0x0f def __init__(self, patch): self.patch = patch @@ -252,6 +308,7 @@ class ChannelPressure(MIDIMessage): _STATUS = 0xd0 _STATUSMASK = 0xf0 _LENGTH = 2 + _CHANNELMASK = 0x0f def __init__(self, pressure): self.pressure = pressure @@ -267,6 +324,7 @@ class PitchBendChange(MIDIMessage): _STATUS = 0xe0 _STATUSMASK = 0xf0 _LENGTH = 3 + _CHANNELMASK = 0x0f def __init__(self, pitch_bend): self.pitch_bend = pitch_bend @@ -339,28 +397,27 @@ def out_channel(self, channel): ### TODO - consider naming here and channel selection and omni mode def read_in_port(self): + """Read messages from MIDI port, store them in internal read buffer, then parse that data + and return the first MIDI message (event). + + Returns (MIDIMessage object, channel) or (None, None) for nothing. + """ ### could check _midi_in is an object OR correct object OR correct interface here? # If the buffer here is not full then read as much as we can fit from # the input port if len(self._inbuf) < self._inbuf_size: self._inbuf.extend(self._midi_in.read(self._inbuf_size - len(self._inbuf))) - - # TODO - VERY IMPORTANT - WORK OUT HOW TO HANDLE CHANNEL FILTERING - # AND THINK ABOUT OMNI MODE - + ### TODO need to ensure code skips past unknown data/messages in buffer ### aftertouch from Axiom 25 causes 6 in the buffer!! - msgse = MIDIMessage.from_message_bytes(self._inbuf) - if msgse is not None: - (msg, start, endplusone) = msgse + (msg, start, endplusone, channel) = MIDIMessage.from_message_bytes(self._inbuf, self._in_channel) + if endplusone != 0: # This is not particularly efficient as it's copying most of bytearray # and deleting old one self._inbuf = self._inbuf[endplusone:] - # msg could still be None at this point, e.g. in middle of monster SysEx - return msg - else: - return None + # msg could still be None at this point, e.g. in middle of monster SysEx + return (msg, channel) def note_on(self, note, vel, channel=None): """Sends a MIDI Note On message. From 83c6b8cf6d82c4b1696b8e95039bcb949a0795a6 Mon Sep 17 00:00:00 2001 From: Kevin J Walters Date: Sun, 17 Mar 2019 17:19:19 +0000 Subject: [PATCH 11/92] Returning number of skipped bytes in from_message_bytes() which is now counted by MIDI object. Making buffer append in read_in_port() dependent on receiving some data plus adding a debug print of data. #3 --- adafruit_midi.py | 29 ++++++++++++++++++++++------- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/adafruit_midi.py b/adafruit_midi.py index 30eb6e4..57b41f4 100644 --- a/adafruit_midi.py +++ b/adafruit_midi.py @@ -104,14 +104,15 @@ def from_message_bytes(cls, midibytes, channel_in): """Create an appropriate object of the correct class for the first message found in some MIDI bytes. - Returns (messageobject, start, endplusone, channel) + Returns (messageobject, start, endplusone, skipped, channel) or for no messages, partial messages or messages for other channels - (None, start, endplusone, None). + (None, start, endplusone, skipped, None). """ msg = None startidx = 0 endidx = len(midibytes) - 1 + skipped = 0 msgstartidx = startidx while True: @@ -119,10 +120,14 @@ def from_message_bytes(cls, midibytes, channel_in): # Second rule of the MIDI club is status bytes have MSB set while msgstartidx <= endidx and not (midibytes[msgstartidx] & 0x80): msgstartidx += 1 + skipped += 1 + print("Skipping past:", hex(midibytes[msgstartidx])) ### TODO REMOVE THIS # Either no message or a partial one if msgstartidx > endidx: - return (None, startidx, endidx + 1, None) + ### TODO review exactly when buffer should be discarded + ### must not discard the first half of a message + return (None, startidx, endidx + 1, skipped, None) status = midibytes[msgstartidx] msgendidxplusone = 0 @@ -159,13 +164,15 @@ def from_message_bytes(cls, midibytes, channel_in): else: msgstartidx = msgendidxplusone + ### TODO THIS IS NOW BUGGY DUE TO + ### TODO correct to handle a buffer with start of big SysEx ### TODO correct to handle a buffer in middle of big SysEx ### TODO correct to handle a buffer with end portion of big SysEx if msg is not None: - return (msg, startidx, msgendidxplusone, channel) + return (msg, startidx, msgendidxplusone, skipped, channel) else: - return (None, startidx, msgendidxplusone, None) + return (None, startidx, msgendidxplusone, skipped, None) @classmethod def from_bytes(cls, databytes): @@ -365,6 +372,7 @@ def __init__(self, midi_in=usb_midi.ports[0], midi_out=usb_midi.ports[1], *, in_ self._inbuf = bytearray(0) self._inbuf_size = in_buf_size self._outbuf = bytearray(4) + self._skipped_bytes = 0 @property def in_channel(self): @@ -406,16 +414,23 @@ def read_in_port(self): # If the buffer here is not full then read as much as we can fit from # the input port if len(self._inbuf) < self._inbuf_size: - self._inbuf.extend(self._midi_in.read(self._inbuf_size - len(self._inbuf))) + bytes_in = self._midi_in.read(self._inbuf_size - len(self._inbuf)) + if len(bytes_in) > 0: + if self._debug: + print("Receiving: ", [hex(i) for i in bytes_in]) + self._inbuf.extend(bytes_in) + del bytes_in ### TODO need to ensure code skips past unknown data/messages in buffer ### aftertouch from Axiom 25 causes 6 in the buffer!! - (msg, start, endplusone, channel) = MIDIMessage.from_message_bytes(self._inbuf, self._in_channel) + (msg, start, endplusone, skipped, channel) = MIDIMessage.from_message_bytes(self._inbuf, self._in_channel) if endplusone != 0: # This is not particularly efficient as it's copying most of bytearray # and deleting old one self._inbuf = self._inbuf[endplusone:] + self._skipped_bytes += skipped + # msg could still be None at this point, e.g. in middle of monster SysEx return (msg, channel) From 39fe07fa780946f4062280405df1cdafe4dfdea7 Mon Sep 17 00:00:00 2001 From: Kevin J Walters Date: Sun, 17 Mar 2019 17:22:36 +0000 Subject: [PATCH 12/92] Renaming vel to velocity in NoteOn / NoteOff for consistency with other (full) names in messages. #3 --- adafruit_midi.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/adafruit_midi.py b/adafruit_midi.py index 57b41f4..9812874 100644 --- a/adafruit_midi.py +++ b/adafruit_midi.py @@ -233,9 +233,9 @@ class NoteOff(MIDIMessage): _LENGTH = 3 _CHANNELMASK = 0x0f - def __init__(self, note, vel): + def __init__(self, note, velocity): self.note = note - self.vel = vel + self.vel = velocity @classmethod def from_bytes(cls, databytes): @@ -250,9 +250,9 @@ class NoteOn(MIDIMessage): _LENGTH = 3 _CHANNELMASK = 0x0f - def __init__(self, note, vel): + def __init__(self, note, velocity): self.note = note - self.vel = vel + self.velocity = velocity @classmethod def from_bytes(cls, databytes): From ba38b085ab17a36b41b77dbf0551b81d9f07ca78 Mon Sep 17 00:00:00 2001 From: Kevin J Walters Date: Sun, 17 Mar 2019 19:06:47 +0000 Subject: [PATCH 13/92] Some welcome unit tests for increasingly complex adafruit_midi.MIDIMessage.from_message_bytes(). #3 --- examples/MIDIMessage_unittests.py | 160 ++++++++++++++++++++++++++++++ 1 file changed, 160 insertions(+) create mode 100644 examples/MIDIMessage_unittests.py diff --git a/examples/MIDIMessage_unittests.py b/examples/MIDIMessage_unittests.py new file mode 100644 index 0000000..5c6713d --- /dev/null +++ b/examples/MIDIMessage_unittests.py @@ -0,0 +1,160 @@ +# The MIT License (MIT) +# +# Copyright (c) 2019 Kevin J. Walters +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import unittest +from unittest.mock import Mock, MagicMock + +import os +verbose = int(os.getenv('TESTVERBOSE',2)) + +# adafruit_midi has an import usb_midi +import sys +sys.modules['usb_midi'] = MagicMock() + +import adafruit_midi + + +### To incorporate into tests +# This is using running status in a rather sporadic manner +# +# Receiving: ['0xe0', '0x67', '0x40'] +# Receiving: ['0xe0', '0x72', '0x40'] +# Receiving: ['0x6d', '0x40', '0xe0'] +# Receiving: ['0x5', '0x41', '0xe0'] +# Receiving: ['0x17', '0x41', '0xe0'] +# Receiving: ['0x35', '0x41', '0xe0'] +# Receiving: ['0x40', '0x41', '0xe0'] + +### TODO - re work these when running status is implemented + +class MIDIMessage_from_message_byte_tests(unittest.TestCase): + def test_NoteOn_basic(self): + data = bytes([0x90, 0x30, 0x7f]) + ichannel = 0 + + (msg, startidx, msgendidxplusone, skipped, channel) = adafruit_midi.MIDIMessage.from_message_bytes(data, ichannel) + + self.assertIsInstance(msg, adafruit_midi.NoteOn) + self.assertEqual(msg.note, 0x30) + self.assertEqual(msg.velocity, 0x7f) + self.assertEqual(startidx, 0) + self.assertEqual(msgendidxplusone, 3) + self.assertEqual(skipped, 0) + self.assertEqual(channel, 0) + + def test_NoteOn_awaitingthirdbyte(self): + data = bytes([0x90, 0x30]) + ichannel = 0 + + (msg, startidx, msgendidxplusone, skipped, channel) = adafruit_midi.MIDIMessage.from_message_bytes(data, ichannel) + self.assertIsNone(msg) + self.assertEqual(msgendidxplusone, skipped, + "skipped must be 0 as it only indicates bytes before a status byte") + self.assertEqual(startidx, 0) + self.assertEqual(msgendidxplusone, 0, + "msgendidxplusone must be 0 as buffer must be lest as is for more data") + self.assertEqual(skipped, 0) + self.assertIsNone(channel) + + def test_NoteOn_predatajunk(self): + data = bytes([0x20, 0x64, 0x90, 0x30, 0x32]) + ichannel = 0 + + (msg, startidx, msgendidxplusone, skipped, channel) = adafruit_midi.MIDIMessage.from_message_bytes(data, ichannel) + + self.assertIsInstance(msg, adafruit_midi.NoteOn) + self.assertEqual(msg.note, 0x30) + self.assertEqual(msg.velocity, 0x32) + self.assertEqual(startidx, 0) + self.assertEqual(msgendidxplusone, 5) + self.assertEqual(skipped, 2) + self.assertEqual(channel, 0) + + def test_NoteOn_postNoteOn(self): + data = bytes([0x90 | 0x08, 0x30, 0x7f, 0x90 | 0x08, 0x37, 0x64]) + ichannel = 8 + + (msg, startidx, msgendidxplusone, skipped, channel) = adafruit_midi.MIDIMessage.from_message_bytes(data, ichannel) + + self.assertIsInstance(msg, adafruit_midi.NoteOn) + self.assertEqual(msg.note, 0x30) + self.assertEqual(msg.velocity, 0x7f) + self.assertEqual(startidx, 0) + self.assertEqual(msgendidxplusone, 3) + self.assertEqual(skipped, 0) + self.assertEqual(channel, 8) + + def test_NoteOn_postpartialNoteOn(self): + data = bytes([0x90, 0x30, 0x7f, 0x90, 0x37]) + ichannel = 0 + + (msg, startidx, msgendidxplusone, skipped, channel) = adafruit_midi.MIDIMessage.from_message_bytes(data, ichannel) + + self.assertIsInstance(msg, adafruit_midi.NoteOn) + self.assertEqual(msg.note, 0x30) + self.assertEqual(msg.velocity, 0x7f) + self.assertEqual(startidx, 0) + self.assertEqual(msgendidxplusone, 3) + self.assertEqual(skipped, 0) + self.assertEqual(channel, 0) + + def test_NoteOn_preotherchannel(self): + data = bytes([0x95, 0x30, 0x7f, 0x93, 0x37, 0x64]) + ichannel = 3 + + (msg, startidx, msgendidxplusone, skipped, channel) = adafruit_midi.MIDIMessage.from_message_bytes(data, ichannel) + + self.assertIsInstance(msg, adafruit_midi.NoteOn) + self.assertEqual(msg.note, 0x37) + self.assertEqual(msg.velocity, 0x64) + self.assertEqual(startidx, 0) + self.assertEqual(msgendidxplusone, 6) + self.assertEqual(skipped, 0) + self.assertEqual(channel, 3) + + def test_NoteOn_partialandpreotherchannel(self): + data = bytes([0x95, 0x30, 0x7f, 0x93, 0x37]) + ichannel = 3 + + (msg, startidx, msgendidxplusone, skipped, channel) = adafruit_midi.MIDIMessage.from_message_bytes(data, ichannel) + + self.assertIsNone(msg) + self.assertEqual(startidx, 0) + self.assertEqual(msgendidxplusone, 3) + self.assertEqual(skipped, 0) + self.assertIsNone(channel) + + def test_Unknown_Singlebyte(self): + data = bytes([0xfd]) + ichannel = 0 + + (msg, startidx, msgendidxplusone, skipped, channel) = adafruit_midi.MIDIMessage.from_message_bytes(data, ichannel) + + self.assertIsInstance(msg, adafruit_midi.MIDIUnknownEvent) + self.assertEqual(startidx, 0) + self.assertEqual(msgendidxplusone, 1) + self.assertEqual(skipped, 0) + self.assertIsNone(channel) + + +if __name__ == '__main__': + unittest.main(verbosity=verbose) From e389ff0bb73c435201e390e340a6bc659595e2ac Mon Sep 17 00:00:00 2001 From: Kevin J Walters Date: Mon, 18 Mar 2019 00:22:12 +0000 Subject: [PATCH 14/92] Adding some tests around SysEx which demonstrate implementation is not yet complete! #3 --- examples/MIDIMessage_unittests.py | 43 ++++++++++++++++++++++++++----- 1 file changed, 37 insertions(+), 6 deletions(-) diff --git a/examples/MIDIMessage_unittests.py b/examples/MIDIMessage_unittests.py index 5c6713d..d2ac32c 100644 --- a/examples/MIDIMessage_unittests.py +++ b/examples/MIDIMessage_unittests.py @@ -35,6 +35,8 @@ ### To incorporate into tests # This is using running status in a rather sporadic manner +# Acutally this now looks more like losing bytes due to being +# overwhelmed by "big" bursts of data # # Receiving: ['0xe0', '0x67', '0x40'] # Receiving: ['0xe0', '0x72', '0x40'] @@ -74,7 +76,7 @@ def test_NoteOn_awaitingthirdbyte(self): "msgendidxplusone must be 0 as buffer must be lest as is for more data") self.assertEqual(skipped, 0) self.assertIsNone(channel) - + def test_NoteOn_predatajunk(self): data = bytes([0x20, 0x64, 0x90, 0x30, 0x32]) ichannel = 0 @@ -89,6 +91,35 @@ def test_NoteOn_predatajunk(self): self.assertEqual(skipped, 2) self.assertEqual(channel, 0) + def test_NoteOn_prepartialsysex(self): + data = bytes([0x01, 0x02, 0x03, 0x04, 0xf7, 0x90, 0x30, 0x32]) + ichannel = 0 + + (msg, startidx, msgendidxplusone, skipped, channel) = adafruit_midi.MIDIMessage.from_message_bytes(data, ichannel) + + self.assertIsInstance(msg, adafruit_midi.NoteOn) + self.assertEqual(msg.note, 0x30) + self.assertEqual(msg.velocity, 0x32) + self.assertEqual(startidx, 0) + self.assertEqual(msgendidxplusone, 8) + self.assertEqual(skipped, 4, "skipped only counts data bytes so will be 4 here") + self.assertEqual(channel, 0) + + def test_NoteOn_predsysex(self): + data = bytes([0xf0, 0x42, 0x01, 0x02, 0x03, 0x04, 0xf7, 0x90, 0x30, 0x32]) + ichannel = 0 + + (msg, startidx, msgendidxplusone, skipped, channel) = adafruit_midi.MIDIMessage.from_message_bytes(data, ichannel) + + self.assertIsInstance(msg, adafruit_midi.SystemExclusive) + self.assertEqual(msg.manufacturer_id, bytes([0x42])) # Korg + self.assertEqual(msg.data, bytes([0x01, 0x02, 0x03, 0x04])) + self.assertEqual(startidx, 0) + self.assertEqual(msgendidxplusone, 7) + self.assertEqual(skipped, 4, "skipped only counts data bytes so will be 4 here") + self.assertEqual(channel, 0) + + def test_NoteOn_postNoteOn(self): data = bytes([0x90 | 0x08, 0x30, 0x7f, 0x90 | 0x08, 0x37, 0x64]) ichannel = 8 @@ -116,9 +147,9 @@ def test_NoteOn_postpartialNoteOn(self): self.assertEqual(msgendidxplusone, 3) self.assertEqual(skipped, 0) self.assertEqual(channel, 0) - + def test_NoteOn_preotherchannel(self): - data = bytes([0x95, 0x30, 0x7f, 0x93, 0x37, 0x64]) + data = bytes([0x90 | 0x05, 0x30, 0x7f, 0x90 | 0x03, 0x37, 0x64]) ichannel = 3 (msg, startidx, msgendidxplusone, skipped, channel) = adafruit_midi.MIDIMessage.from_message_bytes(data, ichannel) @@ -130,7 +161,7 @@ def test_NoteOn_preotherchannel(self): self.assertEqual(msgendidxplusone, 6) self.assertEqual(skipped, 0) self.assertEqual(channel, 3) - + def test_NoteOn_partialandpreotherchannel(self): data = bytes([0x95, 0x30, 0x7f, 0x93, 0x37]) ichannel = 3 @@ -154,7 +185,7 @@ def test_Unknown_Singlebyte(self): self.assertEqual(msgendidxplusone, 1) self.assertEqual(skipped, 0) self.assertIsNone(channel) - - + + if __name__ == '__main__': unittest.main(verbosity=verbose) From 0c70680718462d9efb36585f0491940f36b77ae5 Mon Sep 17 00:00:00 2001 From: Kevin J Walters Date: Mon, 18 Mar 2019 00:27:34 +0000 Subject: [PATCH 15/92] More consistent naming, self._inbuf now self._in_buf. Removing print statement used for temporary debug. #3 --- adafruit_midi.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/adafruit_midi.py b/adafruit_midi.py index 9812874..2005477 100644 --- a/adafruit_midi.py +++ b/adafruit_midi.py @@ -121,7 +121,6 @@ def from_message_bytes(cls, midibytes, channel_in): while msgstartidx <= endidx and not (midibytes[msgstartidx] & 0x80): msgstartidx += 1 skipped += 1 - print("Skipping past:", hex(midibytes[msgstartidx])) ### TODO REMOVE THIS # Either no message or a partial one if msgstartidx > endidx: @@ -350,7 +349,9 @@ class MIDIUnknownEvent(MIDIMessage): def __init__(self, status): self.status = status - +# TODO - implement running status +# some good tips at end of http://midi.teragonaudio.com/tech/midispec/run.htm +# only applies to voice category class MIDI: """MIDI helper class.""" @@ -369,8 +370,8 @@ def __init__(self, midi_in=usb_midi.ports[0], midi_out=usb_midi.ports[1], *, in_ self.out_channel = out_channel self._debug = debug # This input buffer holds what has been read from midi_in - self._inbuf = bytearray(0) - self._inbuf_size = in_buf_size + self._in_buf = bytearray(0) + self._in_buf_size = in_buf_size self._outbuf = bytearray(4) self._skipped_bytes = 0 @@ -413,21 +414,21 @@ def read_in_port(self): ### could check _midi_in is an object OR correct object OR correct interface here? # If the buffer here is not full then read as much as we can fit from # the input port - if len(self._inbuf) < self._inbuf_size: - bytes_in = self._midi_in.read(self._inbuf_size - len(self._inbuf)) + if len(self._in_buf) < self._in_buf_size: + bytes_in = self._midi_in.read(self._in_buf_size - len(self._in_buf)) if len(bytes_in) > 0: if self._debug: print("Receiving: ", [hex(i) for i in bytes_in]) - self._inbuf.extend(bytes_in) + self._in_buf.extend(bytes_in) del bytes_in ### TODO need to ensure code skips past unknown data/messages in buffer ### aftertouch from Axiom 25 causes 6 in the buffer!! - (msg, start, endplusone, skipped, channel) = MIDIMessage.from_message_bytes(self._inbuf, self._in_channel) + (msg, start, endplusone, skipped, channel) = MIDIMessage.from_message_bytes(self._in_buf, self._in_channel) if endplusone != 0: # This is not particularly efficient as it's copying most of bytearray # and deleting old one - self._inbuf = self._inbuf[endplusone:] + self._in_buf = self._in_buf[endplusone:] self._skipped_bytes += skipped From 8d103539ccdf65cd429e3715ead4ddd7a80fc25f Mon Sep 17 00:00:00 2001 From: Kevin J Walters Date: Thu, 21 Mar 2019 00:45:33 +0000 Subject: [PATCH 16/92] Aligning NoteOff naming with recent changes to NoteOn. #3 --- adafruit_midi.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/adafruit_midi.py b/adafruit_midi.py index 2005477..02fb24d 100644 --- a/adafruit_midi.py +++ b/adafruit_midi.py @@ -234,7 +234,7 @@ class NoteOff(MIDIMessage): def __init__(self, note, velocity): self.note = note - self.vel = velocity + self.velocity = velocity @classmethod def from_bytes(cls, databytes): From 46860500959b1c110150942037e5ec3863ddf85f Mon Sep 17 00:00:00 2001 From: Kevin J Walters Date: Thu, 21 Mar 2019 00:59:36 +0000 Subject: [PATCH 17/92] Move the MIDI class in adafruit_midi.py into adafruit_midi/__init__.py in anticipation of splitting out the MIDIMessage classes. #3 --- adafruit_midi.py => adafruit_midi/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename adafruit_midi.py => adafruit_midi/__init__.py (100%) diff --git a/adafruit_midi.py b/adafruit_midi/__init__.py similarity index 100% rename from adafruit_midi.py rename to adafruit_midi/__init__.py From 7c34ec704f1cff629b6d7038111472752cba8fec Mon Sep 17 00:00:00 2001 From: Kevin J Walters Date: Thu, 21 Mar 2019 15:55:14 +0000 Subject: [PATCH 18/92] Splitting MIDIMessage child messages into separate files to allow user to import only what is needed. Adjusting unit tests to match new scheme. #3 --- adafruit_midi/__init__.py | 308 +---------------------- adafruit_midi/channel_pressure.py | 64 +++++ adafruit_midi/control_change.py | 65 +++++ adafruit_midi/midi_message.py | 189 ++++++++++++++ adafruit_midi/note_off.py | 65 +++++ adafruit_midi/note_on.py | 65 +++++ adafruit_midi/pitch_bend_change.py | 64 +++++ adafruit_midi/polyphonic_key_pressure.py | 65 +++++ adafruit_midi/program_change.py | 64 +++++ adafruit_midi/start.py | 56 +++++ adafruit_midi/stop.py | 56 +++++ adafruit_midi/system_exclusive.py | 68 +++++ adafruit_midi/timing_clock.py | 58 +++++ examples/MIDIMessage_unittests.py | 52 ++-- 14 files changed, 913 insertions(+), 326 deletions(-) create mode 100644 adafruit_midi/channel_pressure.py create mode 100644 adafruit_midi/control_change.py create mode 100644 adafruit_midi/midi_message.py create mode 100644 adafruit_midi/note_off.py create mode 100644 adafruit_midi/note_on.py create mode 100644 adafruit_midi/pitch_bend_change.py create mode 100644 adafruit_midi/polyphonic_key_pressure.py create mode 100644 adafruit_midi/program_change.py create mode 100644 adafruit_midi/start.py create mode 100644 adafruit_midi/stop.py create mode 100644 adafruit_midi/system_exclusive.py create mode 100644 adafruit_midi/timing_clock.py diff --git a/adafruit_midi/__init__.py b/adafruit_midi/__init__.py index 02fb24d..98ee870 100644 --- a/adafruit_midi/__init__.py +++ b/adafruit_midi/__init__.py @@ -44,314 +44,16 @@ import usb_midi +from .midi_message import MIDIMessage + __version__ = "0.0.0-auto.0" __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_MIDI.git" -def channel_filter(channel, channel_spec): - if isinstance(channel_spec, int): - if channel_spec == MIDI.ALL_CHANNELS: - return True - else: - return channel == channel_spec - elif isinstance(channel_spec, tuple): - return channel in channel_spec - else: - raise ValueError("Incorrect type for channel_spec") - - -# TODO TBD: can relocate this class later to a separate file if recommended -# need to work out what to do with channel_filter() and its use of MIDI.ALL_CHANNELS -class MIDIMessage: - """ - A MIDI message: - - Status - extracted from Status byte with channel replaced by 0s - (high bit always set) - - Channel - extracted from Status where present (0-15) - - 0 or more Data Byte(s) - high bit always not set for data - - _LENGTH is the fixed message length including status or -1 for variable length - - _ENDSTATUS is the EOM status byte if relevant - This is an abstract class. - """ - _STATUS = None - _STATUSMASK = None - _LENGTH = None - _CHANNELMASK = None - _ENDSTATUS = None - - # Each element is ((status, mask), class) - # order is more specific masks first - _statusandmask_to_class = [] - - @classmethod - def register_message_type(cls): - """Register a new message by its status value and mask. - """ - ### These must be inserted with more specific masks first - insert_idx = len(MIDIMessage._statusandmask_to_class) - for idx, m_type in enumerate(MIDIMessage._statusandmask_to_class): - if cls._STATUSMASK > m_type[0][1]: - insert_idx = idx - break - - MIDIMessage._statusandmask_to_class.insert(insert_idx, - ((cls._STATUS, cls._STATUSMASK), cls)) - - # TODO - this needs a lot of test cases to prove it actually works - # TODO - finish SysEx implementation and find something that sends one - @classmethod - def from_message_bytes(cls, midibytes, channel_in): - """Create an appropriate object of the correct class for the - first message found in some MIDI bytes. - - Returns (messageobject, start, endplusone, skipped, channel) - or for no messages, partial messages or messages for other channels - (None, start, endplusone, skipped, None). - """ - - msg = None - startidx = 0 - endidx = len(midibytes) - 1 - skipped = 0 - - msgstartidx = startidx - while True: - # Look for a status byte - # Second rule of the MIDI club is status bytes have MSB set - while msgstartidx <= endidx and not (midibytes[msgstartidx] & 0x80): - msgstartidx += 1 - skipped += 1 - - # Either no message or a partial one - if msgstartidx > endidx: - ### TODO review exactly when buffer should be discarded - ### must not discard the first half of a message - return (None, startidx, endidx + 1, skipped, None) - - status = midibytes[msgstartidx] - msgendidxplusone = 0 - smfound = False - channel_match = True - channel = None - # Rummage through our list looking for a status match - for sm, msgclass in MIDIMessage._statusandmask_to_class: - masked_status = status & sm[1] - if sm[0] == masked_status: - smfound = True - # Check there's enough left to parse a complete message - if len(midibytes) - msgstartidx >= msgclass._LENGTH: - if msgclass._CHANNELMASK is not None: - channel = status & msgclass._CHANNELMASK - channel_match = channel_filter(channel, channel_in) - if msgclass._LENGTH < 0: - # TODO code this properly - THIS IS VARIABLE LENGTH MESSAGE - msgendidxplusone = endidx + 1 # TODO NOT CORRECT - else: - msgendidxplusone = msgstartidx + msgclass._LENGTH - - if channel_match: - msg = msgclass.from_bytes(midibytes[msgstartidx+1:msgendidxplusone]) - break # for - if smfound and channel_match: - break # while - elif not smfound: - msg = MIDIUnknownEvent(status) - # length cannot be known - # next read will skip past leftover data bytes - msgendidxplusone = msgstartidx + 1 - break - else: - msgstartidx = msgendidxplusone - - ### TODO THIS IS NOW BUGGY DUE TO - - ### TODO correct to handle a buffer with start of big SysEx - ### TODO correct to handle a buffer in middle of big SysEx - ### TODO correct to handle a buffer with end portion of big SysEx - if msg is not None: - return (msg, startidx, msgendidxplusone, skipped, channel) - else: - return (None, startidx, msgendidxplusone, skipped, None) - - @classmethod - def from_bytes(cls, databytes): - """A default method for constructing messages that have no data. - Returns the new object.""" - return cls() - - -class SystemExclusive(MIDIMessage): - _STATUS = 0xf0 - _STATUSMASK = 0xff - _LENGTH = -1 - _ENDSTATUS = 0xf7 - - def __init__(self, manufacturer_id, data): - self.manufacturer_id = manufacturer_id - self.data = data - - @classmethod - def from_bytes(cls, databytes): - if databytes[0] != 0: - return cls(databytes[0:1], databytes[1:]) - else: - return cls(databytes[0:3], databytes[3:]) - -SystemExclusive.register_message_type() - - -class Start(MIDIMessage): - _STATUS = 0xfa - _STATUSMASK = 0xff - _LENGTH = 1 - -Start.register_message_type() - - -class Stop(MIDIMessage): - _STATUS = 0xfc - _STATUSMASK = 0xff - _LENGTH = 1 - -Stop.register_message_type() - - -# Would be good to have this registered first as it occurs -# frequently when in-use -class TimingClock(MIDIMessage): - _STATUS = 0xf8 - _STATUSMASK = 0xff - _LENGTH = 1 - -TimingClock.register_message_type() - - -class NoteOff(MIDIMessage): - _STATUS = 0x80 - _STATUSMASK = 0xf0 - _LENGTH = 3 - _CHANNELMASK = 0x0f - - def __init__(self, note, velocity): - self.note = note - self.velocity = velocity - - @classmethod - def from_bytes(cls, databytes): - return cls(databytes[0], databytes[1]) - -NoteOff.register_message_type() - - -class NoteOn(MIDIMessage): - _STATUS = 0x90 - _STATUSMASK = 0xf0 - _LENGTH = 3 - _CHANNELMASK = 0x0f - - def __init__(self, note, velocity): - self.note = note - self.velocity = velocity - - @classmethod - def from_bytes(cls, databytes): - return cls(databytes[0], databytes[1]) - -NoteOn.register_message_type() - - -class PolyphonicKeyPressure(MIDIMessage): - _STATUS = 0xa0 - _STATUSMASK = 0xf0 - _LENGTH = 3 - _CHANNELMASK = 0x0f - - def __init__(self, note, pressure): - self.note = note - self.pressure = pressure - - @classmethod - def from_bytes(cls, databytes): - return cls(databytes[0], databytes[1]) - -PolyphonicKeyPressure.register_message_type() - - -class ControlChange(MIDIMessage): - _STATUS = 0xb0 - _STATUSMASK = 0xf0 - _LENGTH = 3 - _CHANNELMASK = 0x0f - - def __init__(self, control, value): - self.control = control - self.value = value - - @classmethod - def from_bytes(cls, databytes): - return cls(databytes[0], databytes[1]) - -ControlChange.register_message_type() - - -class ProgramChange(MIDIMessage): - _STATUS = 0xc0 - _STATUSMASK = 0xf0 - _LENGTH = 2 - _CHANNELMASK = 0x0f - - def __init__(self, patch): - self.patch = patch - - @classmethod - def from_bytes(cls, databytes): - return cls(databytes[0]) - -ProgramChange.register_message_type() - - -class ChannelPressure(MIDIMessage): - _STATUS = 0xd0 - _STATUSMASK = 0xf0 - _LENGTH = 2 - _CHANNELMASK = 0x0f - - def __init__(self, pressure): - self.pressure = pressure - - @classmethod - def from_bytes(cls, databytes): - return cls(databytes[0]) - -ChannelPressure.register_message_type() - - -class PitchBendChange(MIDIMessage): - _STATUS = 0xe0 - _STATUSMASK = 0xf0 - _LENGTH = 3 - _CHANNELMASK = 0x0f - - def __init__(self, pitch_bend): - self.pitch_bend = pitch_bend - - @classmethod - def from_bytes(cls, databytes): - return cls(databytes[1] << 7 | databytes[0]) - -PitchBendChange.register_message_type() - - -# DO NOT try to register this message -class MIDIUnknownEvent(MIDIMessage): - _LENGTH = -1 - - def __init__(self, status): - self.status = status - # TODO - implement running status # some good tips at end of http://midi.teragonaudio.com/tech/midispec/run.htm # only applies to voice category + class MIDI: """MIDI helper class.""" @@ -360,8 +62,6 @@ class MIDI: PITCH_BEND = 0xE0 CONTROL_CHANGE = 0xB0 - ALL_CHANNELS = -1 - def __init__(self, midi_in=usb_midi.ports[0], midi_out=usb_midi.ports[1], *, in_channel=None, out_channel=0, debug=False, in_buf_size=30): self._midi_in = midi_in @@ -386,7 +86,7 @@ def in_channel(self, channel): if channel is None or (isinstance(channel, int) and 0 <= channel <= 15): self._in_channel = channel elif isinstance(channel, str) and channel == "ALL": - self._in_channel = self.ALL_CHANNELS + self._in_channel = MIDIMessage.ALL_CHANNELS elif isinstance(channel, tuple) and all(0 <= c <= 15 for c in channel): self._in_channel = channel else: diff --git a/adafruit_midi/channel_pressure.py b/adafruit_midi/channel_pressure.py new file mode 100644 index 0000000..f46a4e5 --- /dev/null +++ b/adafruit_midi/channel_pressure.py @@ -0,0 +1,64 @@ +# The MIT License (MIT) +# +# Copyright (c) 2019 Kevin J. Walters +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +`adafruit_midi` +================================================================================ + +A CircuitPython helper for encoding/decoding MIDI packets over a MIDI or UART connection. + + +* Author(s): Kevin J. Walters + +Implementation Notes +-------------------- + +**Hardware:** + + + +**Software and Dependencies:** + +* Adafruit CircuitPython firmware for the supported boards: + https://github.com/adafruit/circuitpython/releases + +""" + +from .midi_message import MIDIMessage + +__version__ = "0.0.0-auto.0" +__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_MIDI.git" + + +class ChannelPressure(MIDIMessage): + _STATUS = 0xd0 + _STATUSMASK = 0xf0 + _LENGTH = 2 + _CHANNELMASK = 0x0f + + def __init__(self, pressure): + self.pressure = pressure + + @classmethod + def from_bytes(cls, databytes): + return cls(databytes[0]) + +ChannelPressure.register_message_type() diff --git a/adafruit_midi/control_change.py b/adafruit_midi/control_change.py new file mode 100644 index 0000000..59ade91 --- /dev/null +++ b/adafruit_midi/control_change.py @@ -0,0 +1,65 @@ +# The MIT License (MIT) +# +# Copyright (c) 2019 Kevin J. Walters +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +`adafruit_midi` +================================================================================ + +A CircuitPython helper for encoding/decoding MIDI packets over a MIDI or UART connection. + + +* Author(s): Kevin J. Walters + +Implementation Notes +-------------------- + +**Hardware:** + + + +**Software and Dependencies:** + +* Adafruit CircuitPython firmware for the supported boards: + https://github.com/adafruit/circuitpython/releases + +""" + +from .midi_message import MIDIMessage + +__version__ = "0.0.0-auto.0" +__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_MIDI.git" + + +class ControlChange(MIDIMessage): + _STATUS = 0xb0 + _STATUSMASK = 0xf0 + _LENGTH = 3 + _CHANNELMASK = 0x0f + + def __init__(self, control, value): + self.control = control + self.value = value + + @classmethod + def from_bytes(cls, databytes): + return cls(databytes[0], databytes[1]) + +ControlChange.register_message_type() diff --git a/adafruit_midi/midi_message.py b/adafruit_midi/midi_message.py new file mode 100644 index 0000000..a5caffe --- /dev/null +++ b/adafruit_midi/midi_message.py @@ -0,0 +1,189 @@ +# The MIT License (MIT) +# +# Copyright (c) 2019 Kevin J. Walters +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +`adafruit_midi` +================================================================================ + +A CircuitPython helper for encoding/decoding MIDI packets over a MIDI or UART connection. + + +* Author(s): Kevin J. Walters + +Implementation Notes +-------------------- + +**Hardware:** + + + +**Software and Dependencies:** + +* Adafruit CircuitPython firmware for the supported boards: + https://github.com/adafruit/circuitpython/releases + +""" + +__version__ = "0.0.0-auto.0" +__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_MIDI.git" + + +ALL_CHANNELS = -1 + + +def channel_filter(channel, channel_spec): + if isinstance(channel_spec, int): + if channel_spec == ALL_CHANNELS: + return True + else: + return channel == channel_spec + elif isinstance(channel_spec, tuple): + return channel in channel_spec + else: + raise ValueError("Incorrect type for channel_spec") + +# TODO TBD: can relocate this class later to a separate file if recommended +class MIDIMessage: + """ + A MIDI message: + - Status - extracted from Status byte with channel replaced by 0s + (high bit always set) + - Channel - extracted from Status where present (0-15) + - 0 or more Data Byte(s) - high bit always not set for data + - _LENGTH is the fixed message length including status or -1 for variable length + - _ENDSTATUS is the EOM status byte if relevant + This is an abstract class. + """ + _STATUS = None + _STATUSMASK = None + _LENGTH = None + _CHANNELMASK = None + _ENDSTATUS = None + + # Each element is ((status, mask), class) + # order is more specific masks first + _statusandmask_to_class = [] + + @classmethod + def register_message_type(cls): + """Register a new message by its status value and mask. + """ + ### These must be inserted with more specific masks first + insert_idx = len(MIDIMessage._statusandmask_to_class) + for idx, m_type in enumerate(MIDIMessage._statusandmask_to_class): + if cls._STATUSMASK > m_type[0][1]: + insert_idx = idx + break + + MIDIMessage._statusandmask_to_class.insert(insert_idx, + ((cls._STATUS, cls._STATUSMASK), cls)) + + # TODO - this needs a lot of test cases to prove it actually works + # TODO - finish SysEx implementation and find something that sends one + @classmethod + def from_message_bytes(cls, midibytes, channel_in): + """Create an appropriate object of the correct class for the + first message found in some MIDI bytes. + + Returns (messageobject, start, endplusone, skipped, channel) + or for no messages, partial messages or messages for other channels + (None, start, endplusone, skipped, None). + """ + + msg = None + startidx = 0 + endidx = len(midibytes) - 1 + skipped = 0 + + msgstartidx = startidx + while True: + # Look for a status byte + # Second rule of the MIDI club is status bytes have MSB set + while msgstartidx <= endidx and not (midibytes[msgstartidx] & 0x80): + msgstartidx += 1 + skipped += 1 + + # Either no message or a partial one + if msgstartidx > endidx: + ### TODO review exactly when buffer should be discarded + ### must not discard the first half of a message + return (None, startidx, endidx + 1, skipped, None) + + status = midibytes[msgstartidx] + msgendidxplusone = 0 + smfound = False + channel_match = True + channel = None + # Rummage through our list looking for a status match + for sm, msgclass in MIDIMessage._statusandmask_to_class: + masked_status = status & sm[1] + if sm[0] == masked_status: + smfound = True + # Check there's enough left to parse a complete message + if len(midibytes) - msgstartidx >= msgclass._LENGTH: + if msgclass._CHANNELMASK is not None: + channel = status & msgclass._CHANNELMASK + channel_match = channel_filter(channel, channel_in) + if msgclass._LENGTH < 0: + # TODO code this properly - THIS IS VARIABLE LENGTH MESSAGE + msgendidxplusone = endidx + 1 # TODO NOT CORRECT + else: + msgendidxplusone = msgstartidx + msgclass._LENGTH + + if channel_match: + msg = msgclass.from_bytes(midibytes[msgstartidx+1:msgendidxplusone]) + break # for + if smfound and channel_match: + break # while + elif not smfound: + msg = MIDIUnknownEvent(status) + # length cannot be known + # next read will skip past leftover data bytes + msgendidxplusone = msgstartidx + 1 + break + else: + msgstartidx = msgendidxplusone + + ### TODO THIS IS NOW BUGGY DUE TO + + ### TODO correct to handle a buffer with start of big SysEx + ### TODO correct to handle a buffer in middle of big SysEx + ### TODO correct to handle a buffer with end portion of big SysEx + if msg is not None: + return (msg, startidx, msgendidxplusone, skipped, channel) + else: + return (None, startidx, msgendidxplusone, skipped, None) + + @classmethod + def from_bytes(cls, databytes): + """A default method for constructing messages that have no data. + Returns the new object.""" + return cls() + + +# DO NOT try to register this message +class MIDIUnknownEvent(MIDIMessage): + _LENGTH = -1 + + def __init__(self, status): + self.status = status + + diff --git a/adafruit_midi/note_off.py b/adafruit_midi/note_off.py new file mode 100644 index 0000000..3764cd2 --- /dev/null +++ b/adafruit_midi/note_off.py @@ -0,0 +1,65 @@ +# The MIT License (MIT) +# +# Copyright (c) 2019 Kevin J. Walters +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +`adafruit_midi` +================================================================================ + +A CircuitPython helper for encoding/decoding MIDI packets over a MIDI or UART connection. + + +* Author(s): Kevin J. Walters + +Implementation Notes +-------------------- + +**Hardware:** + + + +**Software and Dependencies:** + +* Adafruit CircuitPython firmware for the supported boards: + https://github.com/adafruit/circuitpython/releases + +""" + +from .midi_message import MIDIMessage + +__version__ = "0.0.0-auto.0" +__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_MIDI.git" + + +class NoteOff(MIDIMessage): + _STATUS = 0x80 + _STATUSMASK = 0xf0 + _LENGTH = 3 + _CHANNELMASK = 0x0f + + def __init__(self, note, velocity): + self.note = note + self.velocity = velocity + + @classmethod + def from_bytes(cls, databytes): + return cls(databytes[0], databytes[1]) + +NoteOff.register_message_type() diff --git a/adafruit_midi/note_on.py b/adafruit_midi/note_on.py new file mode 100644 index 0000000..52838da --- /dev/null +++ b/adafruit_midi/note_on.py @@ -0,0 +1,65 @@ +# The MIT License (MIT) +# +# Copyright (c) 2019 Kevin J. Walters +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +`adafruit_midi` +================================================================================ + +A CircuitPython helper for encoding/decoding MIDI packets over a MIDI or UART connection. + + +* Author(s): Kevin J. Walters + +Implementation Notes +-------------------- + +**Hardware:** + + + +**Software and Dependencies:** + +* Adafruit CircuitPython firmware for the supported boards: + https://github.com/adafruit/circuitpython/releases + +""" + +from .midi_message import MIDIMessage + +__version__ = "0.0.0-auto.0" +__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_MIDI.git" + + +class NoteOn(MIDIMessage): + _STATUS = 0x90 + _STATUSMASK = 0xf0 + _LENGTH = 3 + _CHANNELMASK = 0x0f + + def __init__(self, note, velocity): + self.note = note + self.velocity = velocity + + @classmethod + def from_bytes(cls, databytes): + return cls(databytes[0], databytes[1]) + +NoteOn.register_message_type() diff --git a/adafruit_midi/pitch_bend_change.py b/adafruit_midi/pitch_bend_change.py new file mode 100644 index 0000000..7e0556c --- /dev/null +++ b/adafruit_midi/pitch_bend_change.py @@ -0,0 +1,64 @@ +# The MIT License (MIT) +# +# Copyright (c) 2019 Kevin J. Walters +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +`adafruit_midi` +================================================================================ + +A CircuitPython helper for encoding/decoding MIDI packets over a MIDI or UART connection. + + +* Author(s): Kevin J. Walters + +Implementation Notes +-------------------- + +**Hardware:** + + + +**Software and Dependencies:** + +* Adafruit CircuitPython firmware for the supported boards: + https://github.com/adafruit/circuitpython/releases + +""" + +from .midi_message import MIDIMessage + +__version__ = "0.0.0-auto.0" +__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_MIDI.git" + + +class PitchBendChange(MIDIMessage): + _STATUS = 0xe0 + _STATUSMASK = 0xf0 + _LENGTH = 3 + _CHANNELMASK = 0x0f + + def __init__(self, pitch_bend): + self.pitch_bend = pitch_bend + + @classmethod + def from_bytes(cls, databytes): + return cls(databytes[1] << 7 | databytes[0]) + +PitchBendChange.register_message_type() diff --git a/adafruit_midi/polyphonic_key_pressure.py b/adafruit_midi/polyphonic_key_pressure.py new file mode 100644 index 0000000..c472701 --- /dev/null +++ b/adafruit_midi/polyphonic_key_pressure.py @@ -0,0 +1,65 @@ +# The MIT License (MIT) +# +# Copyright (c) 2019 Kevin J. Walters +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +`adafruit_midi` +================================================================================ + +A CircuitPython helper for encoding/decoding MIDI packets over a MIDI or UART connection. + + +* Author(s): Kevin J. Walters + +Implementation Notes +-------------------- + +**Hardware:** + + + +**Software and Dependencies:** + +* Adafruit CircuitPython firmware for the supported boards: + https://github.com/adafruit/circuitpython/releases + +""" + +from .midi_message import MIDIMessage + +__version__ = "0.0.0-auto.0" +__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_MIDI.git" + + +class PolyphonicKeyPressure(MIDIMessage): + _STATUS = 0xa0 + _STATUSMASK = 0xf0 + _LENGTH = 3 + _CHANNELMASK = 0x0f + + def __init__(self, note, pressure): + self.note = note + self.pressure = pressure + + @classmethod + def from_bytes(cls, databytes): + return cls(databytes[0], databytes[1]) + +PolyphonicKeyPressure.register_message_type() diff --git a/adafruit_midi/program_change.py b/adafruit_midi/program_change.py new file mode 100644 index 0000000..79d18f6 --- /dev/null +++ b/adafruit_midi/program_change.py @@ -0,0 +1,64 @@ +# The MIT License (MIT) +# +# Copyright (c) 2019 Kevin J. Walters +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +`adafruit_midi` +================================================================================ + +A CircuitPython helper for encoding/decoding MIDI packets over a MIDI or UART connection. + + +* Author(s): Kevin J. Walters + +Implementation Notes +-------------------- + +**Hardware:** + + + +**Software and Dependencies:** + +* Adafruit CircuitPython firmware for the supported boards: + https://github.com/adafruit/circuitpython/releases + +""" + +from .midi_message import MIDIMessage + +__version__ = "0.0.0-auto.0" +__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_MIDI.git" + + +class ProgramChange(MIDIMessage): + _STATUS = 0xc0 + _STATUSMASK = 0xf0 + _LENGTH = 2 + _CHANNELMASK = 0x0f + + def __init__(self, patch): + self.patch = patch + + @classmethod + def from_bytes(cls, databytes): + return cls(databytes[0]) + +ProgramChange.register_message_type() diff --git a/adafruit_midi/start.py b/adafruit_midi/start.py new file mode 100644 index 0000000..d34f8e4 --- /dev/null +++ b/adafruit_midi/start.py @@ -0,0 +1,56 @@ +# The MIT License (MIT) +# +# Copyright (c) 2019 Kevin J. Walters +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +`adafruit_midi` +================================================================================ + +A CircuitPython helper for encoding/decoding MIDI packets over a MIDI or UART connection. + + +* Author(s): Kevin J. Walters + +Implementation Notes +-------------------- + +**Hardware:** + + + +**Software and Dependencies:** + +* Adafruit CircuitPython firmware for the supported boards: + https://github.com/adafruit/circuitpython/releases + +""" + +from .midi_message import MIDIMessage + +__version__ = "0.0.0-auto.0" +__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_MIDI.git" + + +class Start(MIDIMessage): + _STATUS = 0xfa + _STATUSMASK = 0xff + _LENGTH = 1 + +Start.register_message_type() diff --git a/adafruit_midi/stop.py b/adafruit_midi/stop.py new file mode 100644 index 0000000..81f2bd0 --- /dev/null +++ b/adafruit_midi/stop.py @@ -0,0 +1,56 @@ +# The MIT License (MIT) +# +# Copyright (c) 2019 Kevin J. Walters +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +`adafruit_midi` +================================================================================ + +A CircuitPython helper for encoding/decoding MIDI packets over a MIDI or UART connection. + + +* Author(s): Kevin J. Walters + +Implementation Notes +-------------------- + +**Hardware:** + + + +**Software and Dependencies:** + +* Adafruit CircuitPython firmware for the supported boards: + https://github.com/adafruit/circuitpython/releases + +""" + +from .midi_message import MIDIMessage + +__version__ = "0.0.0-auto.0" +__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_MIDI.git" + + +class Stop(MIDIMessage): + _STATUS = 0xfc + _STATUSMASK = 0xff + _LENGTH = 1 + +Stop.register_message_type() diff --git a/adafruit_midi/system_exclusive.py b/adafruit_midi/system_exclusive.py new file mode 100644 index 0000000..817e2e3 --- /dev/null +++ b/adafruit_midi/system_exclusive.py @@ -0,0 +1,68 @@ +# The MIT License (MIT) +# +# Copyright (c) 2019 Kevin J. Walters +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +`adafruit_midi` +================================================================================ + +A CircuitPython helper for encoding/decoding MIDI packets over a MIDI or UART connection. + + +* Author(s): Kevin J. Walters + +Implementation Notes +-------------------- + +**Hardware:** + + + +**Software and Dependencies:** + +* Adafruit CircuitPython firmware for the supported boards: + https://github.com/adafruit/circuitpython/releases + +""" + +from .midi_message import MIDIMessage + +__version__ = "0.0.0-auto.0" +__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_MIDI.git" + + +class SystemExclusive(MIDIMessage): + _STATUS = 0xf0 + _STATUSMASK = 0xff + _LENGTH = -1 + _ENDSTATUS = 0xf7 + + def __init__(self, manufacturer_id, data): + self.manufacturer_id = manufacturer_id + self.data = data + + @classmethod + def from_bytes(cls, databytes): + if databytes[0] != 0: + return cls(databytes[0:1], databytes[1:]) + else: + return cls(databytes[0:3], databytes[3:]) + +SystemExclusive.register_message_type() diff --git a/adafruit_midi/timing_clock.py b/adafruit_midi/timing_clock.py new file mode 100644 index 0000000..2467f22 --- /dev/null +++ b/adafruit_midi/timing_clock.py @@ -0,0 +1,58 @@ +# The MIT License (MIT) +# +# Copyright (c) 2019 Kevin J. Walters +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +`adafruit_midi` +================================================================================ + +A CircuitPython helper for encoding/decoding MIDI packets over a MIDI or UART connection. + + +* Author(s): Kevin J. Walters + +Implementation Notes +-------------------- + +**Hardware:** + + + +**Software and Dependencies:** + +* Adafruit CircuitPython firmware for the supported boards: + https://github.com/adafruit/circuitpython/releases + +""" + +from .midi_message import MIDIMessage + +__version__ = "0.0.0-auto.0" +__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_MIDI.git" + + +# Would be good to have this registered first as it occurs +# frequently when in-use +class TimingClock(MIDIMessage): + _STATUS = 0xf8 + _STATUSMASK = 0xff + _LENGTH = 1 + +TimingClock.register_message_type() diff --git a/examples/MIDIMessage_unittests.py b/examples/MIDIMessage_unittests.py index d2ac32c..a17b767 100644 --- a/examples/MIDIMessage_unittests.py +++ b/examples/MIDIMessage_unittests.py @@ -31,7 +31,8 @@ sys.modules['usb_midi'] = MagicMock() import adafruit_midi - +from adafruit_midi.note_on import NoteOn +from adafruit_midi.system_exclusive import SystemExclusive ### To incorporate into tests # This is using running status in a rather sporadic manner @@ -55,7 +56,7 @@ def test_NoteOn_basic(self): (msg, startidx, msgendidxplusone, skipped, channel) = adafruit_midi.MIDIMessage.from_message_bytes(data, ichannel) - self.assertIsInstance(msg, adafruit_midi.NoteOn) + self.assertIsInstance(msg, NoteOn) self.assertEqual(msg.note, 0x30) self.assertEqual(msg.velocity, 0x7f) self.assertEqual(startidx, 0) @@ -83,50 +84,54 @@ def test_NoteOn_predatajunk(self): (msg, startidx, msgendidxplusone, skipped, channel) = adafruit_midi.MIDIMessage.from_message_bytes(data, ichannel) - self.assertIsInstance(msg, adafruit_midi.NoteOn) + self.assertIsInstance(msg, NoteOn) self.assertEqual(msg.note, 0x30) self.assertEqual(msg.velocity, 0x32) self.assertEqual(startidx, 0) - self.assertEqual(msgendidxplusone, 5) + self.assertEqual(msgendidxplusone, 5, + "data bytes from partial message and messages are removed" ) self.assertEqual(skipped, 2) self.assertEqual(channel, 0) def test_NoteOn_prepartialsysex(self): - data = bytes([0x01, 0x02, 0x03, 0x04, 0xf7, 0x90, 0x30, 0x32]) + data = bytes([0x01, 0x02, 0x03, 0x04, 0xf7, 0x90, 0x30, 0x32]) ichannel = 0 (msg, startidx, msgendidxplusone, skipped, channel) = adafruit_midi.MIDIMessage.from_message_bytes(data, ichannel) - self.assertIsInstance(msg, adafruit_midi.NoteOn) + self.assertIsInstance(msg, NoteOn, + "NoteOn is expected if SystemExclusive is loaded otherwise it would be MIDIUnknownEvent") self.assertEqual(msg.note, 0x30) self.assertEqual(msg.velocity, 0x32) self.assertEqual(startidx, 0) - self.assertEqual(msgendidxplusone, 8) + self.assertEqual(msgendidxplusone, 8, + "end of partial SysEx and message are removed") self.assertEqual(skipped, 4, "skipped only counts data bytes so will be 4 here") self.assertEqual(channel, 0) def test_NoteOn_predsysex(self): - data = bytes([0xf0, 0x42, 0x01, 0x02, 0x03, 0x04, 0xf7, 0x90, 0x30, 0x32]) + data = bytes([0xf0, 0x42, 0x01, 0x02, 0x03, 0x04, 0xf7, 0x90, 0x30, 0x32]) ichannel = 0 (msg, startidx, msgendidxplusone, skipped, channel) = adafruit_midi.MIDIMessage.from_message_bytes(data, ichannel) - self.assertIsInstance(msg, adafruit_midi.SystemExclusive) + self.assertIsInstance(msg, SystemExclusive) self.assertEqual(msg.manufacturer_id, bytes([0x42])) # Korg self.assertEqual(msg.data, bytes([0x01, 0x02, 0x03, 0x04])) self.assertEqual(startidx, 0) self.assertEqual(msgendidxplusone, 7) - self.assertEqual(skipped, 4, "skipped only counts data bytes so will be 4 here") + self.assertEqual(skipped, 4, + "skipped only counts data bytes so will be 4 here") self.assertEqual(channel, 0) def test_NoteOn_postNoteOn(self): - data = bytes([0x90 | 0x08, 0x30, 0x7f, 0x90 | 0x08, 0x37, 0x64]) + data = bytes([0x90 | 0x08, 0x30, 0x7f, 0x90 | 0x08, 0x37, 0x64]) ichannel = 8 (msg, startidx, msgendidxplusone, skipped, channel) = adafruit_midi.MIDIMessage.from_message_bytes(data, ichannel) - self.assertIsInstance(msg, adafruit_midi.NoteOn) + self.assertIsInstance(msg, NoteOn) self.assertEqual(msg.note, 0x30) self.assertEqual(msg.velocity, 0x7f) self.assertEqual(startidx, 0) @@ -135,52 +140,55 @@ def test_NoteOn_postNoteOn(self): self.assertEqual(channel, 8) def test_NoteOn_postpartialNoteOn(self): - data = bytes([0x90, 0x30, 0x7f, 0x90, 0x37]) + data = bytes([0x90, 0x30, 0x7f, 0x90, 0x37]) ichannel = 0 (msg, startidx, msgendidxplusone, skipped, channel) = adafruit_midi.MIDIMessage.from_message_bytes(data, ichannel) - self.assertIsInstance(msg, adafruit_midi.NoteOn) + self.assertIsInstance(msg, NoteOn) self.assertEqual(msg.note, 0x30) self.assertEqual(msg.velocity, 0x7f) self.assertEqual(startidx, 0) - self.assertEqual(msgendidxplusone, 3) + self.assertEqual(msgendidxplusone, 3, + "Only first message is removed") self.assertEqual(skipped, 0) self.assertEqual(channel, 0) def test_NoteOn_preotherchannel(self): - data = bytes([0x90 | 0x05, 0x30, 0x7f, 0x90 | 0x03, 0x37, 0x64]) + data = bytes([0x90 | 0x05, 0x30, 0x7f, 0x90 | 0x03, 0x37, 0x64]) ichannel = 3 (msg, startidx, msgendidxplusone, skipped, channel) = adafruit_midi.MIDIMessage.from_message_bytes(data, ichannel) - self.assertIsInstance(msg, adafruit_midi.NoteOn) + self.assertIsInstance(msg, NoteOn) self.assertEqual(msg.note, 0x37) self.assertEqual(msg.velocity, 0x64) self.assertEqual(startidx, 0) - self.assertEqual(msgendidxplusone, 6) + self.assertEqual(msgendidxplusone, 6, + "Both messages are removed from buffer") self.assertEqual(skipped, 0) self.assertEqual(channel, 3) def test_NoteOn_partialandpreotherchannel(self): - data = bytes([0x95, 0x30, 0x7f, 0x93, 0x37]) + data = bytes([0x95, 0x30, 0x7f, 0x93, 0x37]) ichannel = 3 (msg, startidx, msgendidxplusone, skipped, channel) = adafruit_midi.MIDIMessage.from_message_bytes(data, ichannel) self.assertIsNone(msg) self.assertEqual(startidx, 0) - self.assertEqual(msgendidxplusone, 3) + self.assertEqual(msgendidxplusone, 3, + "first message removed, second partial left") self.assertEqual(skipped, 0) self.assertIsNone(channel) - def test_Unknown_Singlebyte(self): + def test_Unknown_SinglebyteStatus(self): data = bytes([0xfd]) ichannel = 0 (msg, startidx, msgendidxplusone, skipped, channel) = adafruit_midi.MIDIMessage.from_message_bytes(data, ichannel) - self.assertIsInstance(msg, adafruit_midi.MIDIUnknownEvent) + self.assertIsInstance(msg, adafruit_midi.midi_message.MIDIUnknownEvent) self.assertEqual(startidx, 0) self.assertEqual(msgendidxplusone, 1) self.assertEqual(skipped, 0) From d6efee7dfca730c78f6585142c00ad505ff3f92b Mon Sep 17 00:00:00 2001 From: Kevin J Walters Date: Thu, 21 Mar 2019 22:51:52 +0000 Subject: [PATCH 19/92] Adding a send method to MIDI to allow sending of the new MIDIMessage. Each MIDIMessage has to supply an as_bytes to support sending - this currently intentionally returns a bytearray for mutability but this is worth reviewing. Test cases for MIDI.send in new file MIDIMessage_unittests.py #3 --- adafruit_midi/__init__.py | 17 +++++ adafruit_midi/note_off.py | 6 +- adafruit_midi/note_on.py | 10 ++- examples/MIDI_unittests.py | 141 +++++++++++++++++++++++++++++++++++++ 4 files changed, 170 insertions(+), 4 deletions(-) create mode 100644 examples/MIDI_unittests.py diff --git a/adafruit_midi/__init__.py b/adafruit_midi/__init__.py index 98ee870..4636013 100644 --- a/adafruit_midi/__init__.py +++ b/adafruit_midi/__init__.py @@ -135,6 +135,23 @@ def read_in_port(self): # msg could still be None at this point, e.g. in middle of monster SysEx return (msg, channel) + def send(self, msg, channel=None): + """Sends a MIDI message. + + :param MIDIMessage msg: The midi message. + + """ + if channel is None: + channel = self.out_channel + if isinstance(msg, MIDIMessage): + data = msg.as_bytes(channel=channel) + else: + data = bytearray() + for each_msg in msg: + data.extend(each_msg.as_bytes(channel=channel)) + + self._send(data, len(data)) + def note_on(self, note, vel, channel=None): """Sends a MIDI Note On message. diff --git a/adafruit_midi/note_off.py b/adafruit_midi/note_off.py index 3764cd2..3eb316d 100644 --- a/adafruit_midi/note_off.py +++ b/adafruit_midi/note_off.py @@ -58,8 +58,12 @@ def __init__(self, note, velocity): self.note = note self.velocity = velocity + def as_bytes(self, channel=None): + return bytearray([self._STATUS | (channel & self._CHANNELMASK), + self.note, self.velocity]) + @classmethod def from_bytes(cls, databytes): return cls(databytes[0], databytes[1]) - + NoteOff.register_message_type() diff --git a/adafruit_midi/note_on.py b/adafruit_midi/note_on.py index 52838da..809a29b 100644 --- a/adafruit_midi/note_on.py +++ b/adafruit_midi/note_on.py @@ -53,13 +53,17 @@ class NoteOn(MIDIMessage): _STATUSMASK = 0xf0 _LENGTH = 3 _CHANNELMASK = 0x0f - + def __init__(self, note, velocity): self.note = note self.velocity = velocity - + + def as_bytes(self, channel=None): + return bytearray([self._STATUS | (channel & self._CHANNELMASK), + self.note, self.velocity]) + @classmethod def from_bytes(cls, databytes): return cls(databytes[0], databytes[1]) - + NoteOn.register_message_type() diff --git a/examples/MIDI_unittests.py b/examples/MIDI_unittests.py new file mode 100644 index 0000000..bc7f00a --- /dev/null +++ b/examples/MIDI_unittests.py @@ -0,0 +1,141 @@ +# The MIT License (MIT) +# +# Copyright (c) 2019 Kevin J. Walters +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import unittest +from unittest.mock import Mock, MagicMock, call + +import os +verbose = int(os.getenv('TESTVERBOSE',2)) + +# adafruit_midi has an import usb_midi +import sys +sys.modules['usb_midi'] = MagicMock() + +from adafruit_midi.note_on import NoteOn +from adafruit_midi.note_off import NoteOff + +import adafruit_midi + +# Need to test this with a stream of data +# including example below +# small sysex +# too large sysex + +### To incorporate into tests +# This is using running status in a rather sporadic manner +# Acutally this now looks more like losing bytes due to being +# overwhelmed by "big" bursts of data +# +# Receiving: ['0xe0', '0x67', '0x40'] +# Receiving: ['0xe0', '0x72', '0x40'] +# Receiving: ['0x6d', '0x40', '0xe0'] +# Receiving: ['0x5', '0x41', '0xe0'] +# Receiving: ['0x17', '0x41', '0xe0'] +# Receiving: ['0x35', '0x41', '0xe0'] +# Receiving: ['0x40', '0x41', '0xe0'] + +### TODO - re work these when running status is implemented + +class Test_MIDI(unittest.TestCase): + def test_goodmididatasmall(self): + self.assertEqual(TODO, TODO) + + def test_goodmididatasmall(self): + self.assertEqual(TODO, TODO) + + def test_gooddatarunningstatus(self): ### comment this out as it wont work + self.assertEqual(TODO, TODO) + + def test_somegoodsomemissingdatabytes(self): + self.assertEqual(TODO, TODO) + + def test_smallsysex(self): + self.assertEqual(TODO, TODO) + + def test_largerthanbuffersysex(self): + self.assertEqual(TODO, TODO) + + def test_send_basic(self): + #def printit(buffer, len): + # print(buffer[0:len]) + mockedPortIn = Mock() + #mockedPortIn.write = printit + + m = adafruit_midi.MIDI(midi_out=mockedPortIn, out_channel=2) + + # Test sending some NoteOn and NoteOff to various channels + next = 0 + m.send(NoteOn(0x60, 0x7f)) + self.assertEqual(mockedPortIn.write.mock_calls[next], + call(b'\x92\x60\x7f', 3)) + next += 1 + m.send(NoteOn(0x64, 0x3f)) + self.assertEqual(mockedPortIn.write.mock_calls[next], + call(b'\x92\x64\x3f', 3)) + next += 1 + m.send(NoteOn(0x67, 0x1f)) + self.assertEqual(mockedPortIn.write.mock_calls[next], + call(b'\x92\x67\x1f', 3)) + next += 1 + + m.send(NoteOn(0x60, 0x00)) # Alternative to NoteOff + self.assertEqual(mockedPortIn.write.mock_calls[next], + call(b'\x92\x60\x00', 3)) + next += 1 + m.send(NoteOff(0x64, 0x01)) + self.assertEqual(mockedPortIn.write.mock_calls[next], + call(b'\x82\x64\x01', 3)) + next += 1 + m.send(NoteOff(0x67, 0x02)) + self.assertEqual(mockedPortIn.write.mock_calls[next], + call(b'\x82\x67\x02', 3)) + next += 1 + + m.send(NoteOn(0x6c, 0x7f), channel=9) + self.assertEqual(mockedPortIn.write.mock_calls[next], + call(b'\x99\x6c\x7f', 3)) + next += 1 + + m.send(NoteOff(0x6c, 0x7f), channel=9) + self.assertEqual(mockedPortIn.write.mock_calls[next], + call(b'\x89\x6c\x7f', 3)) + next += 1 + + # Test the list syntax + note_list = [NoteOn(0x6c, 0x51), + NoteOn(0x70, 0x52), + NoteOn(0x73, 0x53)]; + note_tuple = tuple(note_list) + m.send(note_list, channel=10) + self.assertEqual(mockedPortIn.write.mock_calls[next], + call(b'\x9a\x6c\x51\x9a\x70\x52\x9a\x73\x53', 9), + "The implementation writes in one go, single 9 byte write expected") + next += 1 + m.send(note_tuple, channel=11) + self.assertEqual(mockedPortIn.write.mock_calls[next], + call(b'\x9b\x6c\x51\x9b\x70\x52\x9b\x73\x53', 9), + "The implementation writes in one go, single 9 byte write expected") + next += 1 + + +if __name__ == '__main__': + unittest.main(verbosity=verbose) From 5aa000bc41026411d6d4f5a79f8d2420becb1e23 Mon Sep 17 00:00:00 2001 From: Kevin J Walters Date: Thu, 21 Mar 2019 22:53:26 +0000 Subject: [PATCH 20/92] Prefixing all classes for testing with Test_. #3 --- examples/MIDIMessage_unittests.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/MIDIMessage_unittests.py b/examples/MIDIMessage_unittests.py index a17b767..c718c07 100644 --- a/examples/MIDIMessage_unittests.py +++ b/examples/MIDIMessage_unittests.py @@ -23,6 +23,7 @@ import unittest from unittest.mock import Mock, MagicMock + import os verbose = int(os.getenv('TESTVERBOSE',2)) @@ -49,7 +50,7 @@ ### TODO - re work these when running status is implemented -class MIDIMessage_from_message_byte_tests(unittest.TestCase): +class Test_MIDIMessage_from_message_byte_tests(unittest.TestCase): def test_NoteOn_basic(self): data = bytes([0x90, 0x30, 0x7f]) ichannel = 0 From f647a27325d0c00a0d63513bd5f05f38c81d3508 Mon Sep 17 00:00:00 2001 From: Kevin J Walters Date: Fri, 22 Mar 2019 21:42:26 +0000 Subject: [PATCH 21/92] Making test_NoteOn_partialandpreotherchannel two tests test_NoteOn_partialandpreotherchannel1 and test_NoteOn_partialandpreotherchannel2. Fixing test_NoteOn_partialandpreotherchannel with some tweaks to logic in complex MIDIMessage.from_message_bytes(). #3 --- adafruit_midi/midi_message.py | 44 ++++++++++++++++++----------- examples/MIDIMessage_unittests.py | 46 +++++++++++++++++++++++++++++-- 2 files changed, 72 insertions(+), 18 deletions(-) diff --git a/adafruit_midi/midi_message.py b/adafruit_midi/midi_message.py index a5caffe..dd17dd4 100644 --- a/adafruit_midi/midi_message.py +++ b/adafruit_midi/midi_message.py @@ -112,14 +112,19 @@ def from_message_bytes(cls, midibytes, channel_in): startidx = 0 endidx = len(midibytes) - 1 skipped = 0 + preamble = True msgstartidx = startidx + msgendidxplusone = 0 while True: # Look for a status byte # Second rule of the MIDI club is status bytes have MSB set while msgstartidx <= endidx and not (midibytes[msgstartidx] & 0x80): msgstartidx += 1 - skipped += 1 + if preamble: + skipped += 1 + + preamble = False # Either no message or a partial one if msgstartidx > endidx: @@ -128,39 +133,48 @@ def from_message_bytes(cls, midibytes, channel_in): return (None, startidx, endidx + 1, skipped, None) status = midibytes[msgstartidx] - msgendidxplusone = 0 - smfound = False - channel_match = True + known_message = False + complete_message = False + channel_match_orNA = True channel = None # Rummage through our list looking for a status match for sm, msgclass in MIDIMessage._statusandmask_to_class: masked_status = status & sm[1] if sm[0] == masked_status: - smfound = True + known_message = True # Check there's enough left to parse a complete message - if len(midibytes) - msgstartidx >= msgclass._LENGTH: + complete_message = len(midibytes) - msgstartidx >= msgclass._LENGTH + if complete_message: if msgclass._CHANNELMASK is not None: channel = status & msgclass._CHANNELMASK - channel_match = channel_filter(channel, channel_in) + channel_match_orNA = channel_filter(channel, channel_in) if msgclass._LENGTH < 0: # TODO code this properly - THIS IS VARIABLE LENGTH MESSAGE + complete_message = False msgendidxplusone = endidx + 1 # TODO NOT CORRECT else: msgendidxplusone = msgstartidx + msgclass._LENGTH - if channel_match: + if channel_match_orNA: msg = msgclass.from_bytes(midibytes[msgstartidx+1:msgendidxplusone]) - break # for - if smfound and channel_match: - break # while - elif not smfound: + break # for + + # break out of while loop if we have a complete message + # or we have one we do not know about + if known_message: + if complete_message: + if channel_match_orNA: + break + else: + msgstartidx = msgendidxplusone + else: + break + else: msg = MIDIUnknownEvent(status) # length cannot be known # next read will skip past leftover data bytes msgendidxplusone = msgstartidx + 1 break - else: - msgstartidx = msgendidxplusone ### TODO THIS IS NOW BUGGY DUE TO @@ -185,5 +199,3 @@ class MIDIUnknownEvent(MIDIMessage): def __init__(self, status): self.status = status - - diff --git a/examples/MIDIMessage_unittests.py b/examples/MIDIMessage_unittests.py index c718c07..8fd1826 100644 --- a/examples/MIDIMessage_unittests.py +++ b/examples/MIDIMessage_unittests.py @@ -49,6 +49,7 @@ # Receiving: ['0x40', '0x41', '0xe0'] ### TODO - re work these when running status is implemented +### TODO - consider fuzzing this to check it always terminates class Test_MIDIMessage_from_message_byte_tests(unittest.TestCase): def test_NoteOn_basic(self): @@ -170,7 +171,48 @@ def test_NoteOn_preotherchannel(self): self.assertEqual(skipped, 0) self.assertEqual(channel, 3) - def test_NoteOn_partialandpreotherchannel(self): + def test_NoteOn_preotherchannelplusintermediatejunk(self): + data = bytes([0x90 | 0x05, 0x30, 0x7f, 0x00, 0x00, 0x90 | 0x03, 0x37, 0x64]) + ichannel = 3 + + (msg, startidx, msgendidxplusone, skipped, channel) = adafruit_midi.MIDIMessage.from_message_bytes(data, ichannel) + + self.assertIsInstance(msg, NoteOn) + self.assertEqual(msg.note, 0x37) + self.assertEqual(msg.velocity, 0x64) + self.assertEqual(startidx, 0) + self.assertEqual(msgendidxplusone, 8, + "Both messages and junk are removed from buffer") + self.assertEqual(skipped, 0) + self.assertEqual(channel, 3) + + def test_NoteOn_wrongchannel(self): + data = bytes([0x95, 0x30, 0x7f]) + ichannel = 3 + + (msg, startidx, msgendidxplusone, skipped, channel) = adafruit_midi.MIDIMessage.from_message_bytes(data, ichannel) + + self.assertIsNone(msg) + self.assertEqual(startidx, 0) + self.assertEqual(msgendidxplusone, 3, + "wrong channel message discarded") + self.assertEqual(skipped, 0) + self.assertIsNone(channel) + + def test_NoteOn_partialandpreotherchannel1(self): + data = bytes([0x95, 0x30, 0x7f, 0x93]) + ichannel = 3 + + (msg, startidx, msgendidxplusone, skipped, channel) = adafruit_midi.MIDIMessage.from_message_bytes(data, ichannel) + + self.assertIsNone(msg) + self.assertEqual(startidx, 0) + self.assertEqual(msgendidxplusone, 3, + "first message discarded, second partial left") + self.assertEqual(skipped, 0) + self.assertIsNone(channel) + + def test_NoteOn_partialandpreotherchannel2(self): data = bytes([0x95, 0x30, 0x7f, 0x93, 0x37]) ichannel = 3 @@ -179,7 +221,7 @@ def test_NoteOn_partialandpreotherchannel(self): self.assertIsNone(msg) self.assertEqual(startidx, 0) self.assertEqual(msgendidxplusone, 3, - "first message removed, second partial left") + "first message discarded, second partial left") self.assertEqual(skipped, 0) self.assertIsNone(channel) From 55c98eed9eb4bf476f9c75e9c0a5b45df8d08e4c Mon Sep 17 00:00:00 2001 From: Kevin J Walters Date: Fri, 22 Mar 2019 22:15:03 +0000 Subject: [PATCH 22/92] Fixing indent level. #3 --- adafruit_midi/midi_message.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/adafruit_midi/midi_message.py b/adafruit_midi/midi_message.py index dd17dd4..9b26085 100644 --- a/adafruit_midi/midi_message.py +++ b/adafruit_midi/midi_message.py @@ -168,6 +168,8 @@ def from_message_bytes(cls, midibytes, channel_in): else: msgstartidx = msgendidxplusone else: + # Important case of a known message but one that is not + # yet complete - leave bytes in buffer and wait for more break else: msg = MIDIUnknownEvent(status) From 64cf79ed9ff891e36cf44a3c2a26f5dcc929c18a Mon Sep 17 00:00:00 2001 From: Kevin J Walters Date: Fri, 22 Mar 2019 23:08:42 +0000 Subject: [PATCH 23/92] Implemented the variable length message parsing to both handle SysEx and parse past SysEx larger than input buffer. Re-arranged some of the unit tests for SystemExclusive and added a mal-terminated test. Threw in an empty buffer test too. Adjusted SystemExclusive to handle terminating status byte being passed in databytes. #3 --- adafruit_midi/midi_message.py | 39 ++++++++++++--------- adafruit_midi/system_exclusive.py | 7 ++-- examples/MIDIMessage_unittests.py | 58 ++++++++++++++++++++++--------- 3 files changed, 69 insertions(+), 35 deletions(-) diff --git a/adafruit_midi/midi_message.py b/adafruit_midi/midi_message.py index 9b26085..b54429c 100644 --- a/adafruit_midi/midi_message.py +++ b/adafruit_midi/midi_message.py @@ -148,14 +148,26 @@ def from_message_bytes(cls, midibytes, channel_in): if msgclass._CHANNELMASK is not None: channel = status & msgclass._CHANNELMASK channel_match_orNA = channel_filter(channel, channel_in) - if msgclass._LENGTH < 0: - # TODO code this properly - THIS IS VARIABLE LENGTH MESSAGE - complete_message = False - msgendidxplusone = endidx + 1 # TODO NOT CORRECT + + bad_termination = False + if msgclass._LENGTH < 0: # indicator of variable length message + terminated_message = False + msgendidxplusone = msgstartidx + 1 + while msgendidxplusone <= endidx: + if midibytes[msgendidxplusone] & 0x80: + if midibytes[msgendidxplusone] == msgclass._ENDSTATUS: + terminated_message = True + else: + bad_termination = True + break + else: + msgendidxplusone += 1 + if terminated_message or bad_termination: + msgendidxplusone += 1 else: msgendidxplusone = msgstartidx + msgclass._LENGTH - - if channel_match_orNA: + + if not bad_termination and channel_match_orNA: msg = msgclass.from_bytes(midibytes[msgstartidx+1:msgendidxplusone]) break # for @@ -163,10 +175,10 @@ def from_message_bytes(cls, midibytes, channel_in): # or we have one we do not know about if known_message: if complete_message: - if channel_match_orNA: - break - else: - msgstartidx = msgendidxplusone + if channel_match_orNA: + break + else: + msgstartidx = msgendidxplusone else: # Important case of a known message but one that is not # yet complete - leave bytes in buffer and wait for more @@ -177,12 +189,7 @@ def from_message_bytes(cls, midibytes, channel_in): # next read will skip past leftover data bytes msgendidxplusone = msgstartidx + 1 break - - ### TODO THIS IS NOW BUGGY DUE TO - - ### TODO correct to handle a buffer with start of big SysEx - ### TODO correct to handle a buffer in middle of big SysEx - ### TODO correct to handle a buffer with end portion of big SysEx + if msg is not None: return (msg, startidx, msgendidxplusone, skipped, channel) else: diff --git a/adafruit_midi/system_exclusive.py b/adafruit_midi/system_exclusive.py index 817e2e3..04d235b 100644 --- a/adafruit_midi/system_exclusive.py +++ b/adafruit_midi/system_exclusive.py @@ -60,9 +60,10 @@ def __init__(self, manufacturer_id, data): @classmethod def from_bytes(cls, databytes): + # -1 on second arg is to avoid the _ENDSTATUS which is passed if databytes[0] != 0: - return cls(databytes[0:1], databytes[1:]) + return cls(databytes[0:1], databytes[1:-1]) else: - return cls(databytes[0:3], databytes[3:]) - + return cls(databytes[0:3], databytes[3:-1]) + SystemExclusive.register_message_type() diff --git a/examples/MIDIMessage_unittests.py b/examples/MIDIMessage_unittests.py index 8fd1826..8849ef1 100644 --- a/examples/MIDIMessage_unittests.py +++ b/examples/MIDIMessage_unittests.py @@ -110,22 +110,6 @@ def test_NoteOn_prepartialsysex(self): "end of partial SysEx and message are removed") self.assertEqual(skipped, 4, "skipped only counts data bytes so will be 4 here") self.assertEqual(channel, 0) - - def test_NoteOn_predsysex(self): - data = bytes([0xf0, 0x42, 0x01, 0x02, 0x03, 0x04, 0xf7, 0x90, 0x30, 0x32]) - ichannel = 0 - - (msg, startidx, msgendidxplusone, skipped, channel) = adafruit_midi.MIDIMessage.from_message_bytes(data, ichannel) - - self.assertIsInstance(msg, SystemExclusive) - self.assertEqual(msg.manufacturer_id, bytes([0x42])) # Korg - self.assertEqual(msg.data, bytes([0x01, 0x02, 0x03, 0x04])) - self.assertEqual(startidx, 0) - self.assertEqual(msgendidxplusone, 7) - self.assertEqual(skipped, 4, - "skipped only counts data bytes so will be 4 here") - self.assertEqual(channel, 0) - def test_NoteOn_postNoteOn(self): data = bytes([0x90 | 0x08, 0x30, 0x7f, 0x90 | 0x08, 0x37, 0x64]) @@ -225,6 +209,36 @@ def test_NoteOn_partialandpreotherchannel2(self): self.assertEqual(skipped, 0) self.assertIsNone(channel) + def test_SystemExclusive_NoteOn(self): + data = bytes([0xf0, 0x42, 0x01, 0x02, 0x03, 0x04, 0xf7, 0x90, 0x30, 0x32]) + ichannel = 0 + + (msg, startidx, msgendidxplusone, skipped, channel) = adafruit_midi.MIDIMessage.from_message_bytes(data, ichannel) + + self.assertIsInstance(msg, SystemExclusive) + self.assertEqual(msg.manufacturer_id, bytes([0x42])) # Korg + self.assertEqual(msg.data, bytes([0x01, 0x02, 0x03, 0x04])) + self.assertEqual(startidx, 0) + self.assertEqual(msgendidxplusone, 7) + self.assertEqual(skipped, 0, + "If SystemExclusive class is imported then this must be 0") + self.assertIsNone(channel) + ### TODO - call MIDIMessage.from_message_bytes for second part of buffer + + def test_SystemExclusive_NoteOn_premalterminatedsysex(self): + data = bytes([0xf0, 0x42, 0x01, 0x02, 0x03, 0x04, 0xf0, 0x90, 0x30, 0x32]) + ichannel = 0 + + # 0xf0 is incorrect status to mark end of this message, must be 0xf7 + (msg, startidx, msgendidxplusone, skipped, channel) = adafruit_midi.MIDIMessage.from_message_bytes(data, ichannel) + + self.assertIsNone(msg) + self.assertEqual(startidx, 0) + self.assertEqual(msgendidxplusone, 7) + self.assertEqual(skipped, 0, + "If SystemExclusive class is imported then this must be 0") + self.assertIsNone(channel, None) + def test_Unknown_SinglebyteStatus(self): data = bytes([0xfd]) ichannel = 0 @@ -237,6 +251,18 @@ def test_Unknown_SinglebyteStatus(self): self.assertEqual(skipped, 0) self.assertIsNone(channel) + def test_Empty(self): + data = bytes([]) + ichannel = 0 + + (msg, startidx, msgendidxplusone, skipped, channel) = adafruit_midi.MIDIMessage.from_message_bytes(data, ichannel) + + self.assertIsNone(msg) + self.assertEqual(startidx, 0) + self.assertEqual(msgendidxplusone, 0) + self.assertEqual(skipped, 0) + self.assertIsNone(channel) + if __name__ == '__main__': unittest.main(verbosity=verbose) From 3100fd47ac02b832a5e2bf830f486be05c9e4dba Mon Sep 17 00:00:00 2001 From: Kevin J Walters Date: Fri, 22 Mar 2019 23:42:36 +0000 Subject: [PATCH 24/92] Correcting and enhancing test_NoteOn_prepartialsysex() to match the current behaviour of MIDIMessage.from_message_bytes() which is reasonable here. #3 --- adafruit_midi/midi_message.py | 2 +- examples/MIDIMessage_unittests.py | 19 +++++++++++++++---- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/adafruit_midi/midi_message.py b/adafruit_midi/midi_message.py index b54429c..a5c06f9 100644 --- a/adafruit_midi/midi_message.py +++ b/adafruit_midi/midi_message.py @@ -171,7 +171,7 @@ def from_message_bytes(cls, midibytes, channel_in): msg = msgclass.from_bytes(midibytes[msgstartidx+1:msgendidxplusone]) break # for - # break out of while loop if we have a complete message + # break out of while loop for a complete message on good channel # or we have one we do not know about if known_message: if complete_message: diff --git a/examples/MIDIMessage_unittests.py b/examples/MIDIMessage_unittests.py index 8849ef1..09362f9 100644 --- a/examples/MIDIMessage_unittests.py +++ b/examples/MIDIMessage_unittests.py @@ -94,11 +94,23 @@ def test_NoteOn_predatajunk(self): "data bytes from partial message and messages are removed" ) self.assertEqual(skipped, 2) self.assertEqual(channel, 0) - + def test_NoteOn_prepartialsysex(self): data = bytes([0x01, 0x02, 0x03, 0x04, 0xf7, 0x90, 0x30, 0x32]) ichannel = 0 + + (msg, startidx, msgendidxplusone, skipped, channel) = adafruit_midi.MIDIMessage.from_message_bytes(data, ichannel) + # MIDIMessage parsing could be improved to return something that + # indicates its a truncated end of SysEx + self.assertIsInstance(msg, adafruit_midi.midi_message.MIDIUnknownEvent) + self.assertEqual(msg.status, 0xf7) + self.assertEqual(startidx, 0) + self.assertEqual(msgendidxplusone, 5, "removal of the end of the partial SysEx data and terminating status byte") + self.assertEqual(skipped, 4, "skipped only counts data bytes so will be 4 here") + self.assertIsNone(channel) + + data = data[msgendidxplusone:] (msg, startidx, msgendidxplusone, skipped, channel) = adafruit_midi.MIDIMessage.from_message_bytes(data, ichannel) self.assertIsInstance(msg, NoteOn, @@ -106,9 +118,8 @@ def test_NoteOn_prepartialsysex(self): self.assertEqual(msg.note, 0x30) self.assertEqual(msg.velocity, 0x32) self.assertEqual(startidx, 0) - self.assertEqual(msgendidxplusone, 8, - "end of partial SysEx and message are removed") - self.assertEqual(skipped, 4, "skipped only counts data bytes so will be 4 here") + self.assertEqual(msgendidxplusone, 3, "NoteOn message removed") + self.assertEqual(skipped, 0) self.assertEqual(channel, 0) def test_NoteOn_postNoteOn(self): From 30ccc47f212d2366f028c8668c2f1c7a6a304169 Mon Sep 17 00:00:00 2001 From: Kevin J Walters Date: Sun, 24 Mar 2019 09:45:20 +0000 Subject: [PATCH 25/92] Adding a note_parser() to MIDIMessage allowing NoteOn and NoteOff constructors to take strings like "C#4" with some unit tests. Adding range checks to data for NoteOn and Note Off with unit tests. #3 --- adafruit_midi/midi_message.py | 30 ++++++++++++++++++ adafruit_midi/note_off.py | 9 ++++-- adafruit_midi/note_on.py | 9 ++++-- examples/MIDIMessage_unittests.py | 51 +++++++++++++++++++++++++++++++ 4 files changed, 93 insertions(+), 6 deletions(-) diff --git a/adafruit_midi/midi_message.py b/adafruit_midi/midi_message.py index a5c06f9..8c78eb3 100644 --- a/adafruit_midi/midi_message.py +++ b/adafruit_midi/midi_message.py @@ -48,6 +48,9 @@ ALL_CHANNELS = -1 +# From C3 +# Semitones A B C D E F G +note_offset = [9, 11, 12, 14, 16, 17, 19] def channel_filter(channel, channel_spec): if isinstance(channel_spec, int): @@ -60,6 +63,33 @@ def channel_filter(channel, channel_spec): else: raise ValueError("Incorrect type for channel_spec") +# TODO - proper parameter typing and look up how this is done when different types are accepted +def note_parser(note): + """ + If note is a string then it will be parsed and converted to a MIDI note (key) number, e.g. + "C4" will return 60, "C#4" will return 61. If note is not a string it will simply be returned. + + Applies a range check to both string and integer inputs. + """ + midi_note = note + if isinstance(note, str): + if len(note) < 2: + raise ValueError("Bad note format") + noteidx = ord(note[0].upper()) - 65 # 65 os ord('A') + if not 0 <= noteidx <= 6: + raise ValueError("Bad note") + sharpen = 0 + if note[1] == '#': + sharpen = 1 + elif note[1] == 'b': + sharpen = -1 + # int may throw exception here + midi_note = (int(note[1 + abs(sharpen):]) * 12 + + note_offset[noteidx] + + sharpen) + + return midi_note + # TODO TBD: can relocate this class later to a separate file if recommended class MIDIMessage: """ diff --git a/adafruit_midi/note_off.py b/adafruit_midi/note_off.py index 3eb316d..7fbb1a3 100644 --- a/adafruit_midi/note_off.py +++ b/adafruit_midi/note_off.py @@ -42,7 +42,7 @@ """ -from .midi_message import MIDIMessage +from .midi_message import MIDIMessage, note_parser __version__ = "0.0.0-auto.0" __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_MIDI.git" @@ -55,9 +55,12 @@ class NoteOff(MIDIMessage): _CHANNELMASK = 0x0f def __init__(self, note, velocity): - self.note = note + self.note = note_parser(note) self.velocity = velocity - + if not 0 <= self.note <= 127 or not 0 <= self.velocity <= 127: + return ValueError("Out of range") + + # channel value is mandatory def as_bytes(self, channel=None): return bytearray([self._STATUS | (channel & self._CHANNELMASK), self.note, self.velocity]) diff --git a/adafruit_midi/note_on.py b/adafruit_midi/note_on.py index 809a29b..0661a6d 100644 --- a/adafruit_midi/note_on.py +++ b/adafruit_midi/note_on.py @@ -42,7 +42,7 @@ """ -from .midi_message import MIDIMessage +from .midi_message import MIDIMessage, note_parser __version__ = "0.0.0-auto.0" __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_MIDI.git" @@ -55,9 +55,12 @@ class NoteOn(MIDIMessage): _CHANNELMASK = 0x0f def __init__(self, note, velocity): - self.note = note + self.note = note_parser(note) self.velocity = velocity - + if not 0 <= self.note <= 127 or not 0 <= self.velocity <= 127: + raise ValueError("Out of range") + + # channel value is mandatory def as_bytes(self, channel=None): return bytearray([self._STATUS | (channel & self._CHANNELMASK), self.note, self.velocity]) diff --git a/examples/MIDIMessage_unittests.py b/examples/MIDIMessage_unittests.py index 09362f9..8bff530 100644 --- a/examples/MIDIMessage_unittests.py +++ b/examples/MIDIMessage_unittests.py @@ -220,6 +220,17 @@ def test_NoteOn_partialandpreotherchannel2(self): self.assertEqual(skipped, 0) self.assertIsNone(channel) + def test_NoteOn_constructor_int(self): + object1 = NoteOn(60, 0x7f) + + self.assertEqual(object1.note, 60) + self.assertEqual(object1.velocity, 0x7f) + + object2 = NoteOn(60, 0x00) # equivalent of NoteOff + + self.assertEqual(object2.note, 60) + self.assertEqual(object2.velocity, 0x00) + def test_SystemExclusive_NoteOn(self): data = bytes([0xf0, 0x42, 0x01, 0x02, 0x03, 0x04, 0xf7, 0x90, 0x30, 0x32]) ichannel = 0 @@ -275,5 +286,45 @@ def test_Empty(self): self.assertIsNone(channel) +class Test_MIDIMessage_NoteOn_constructor(unittest.TestCase): + def test_NoteOn_constructor_string(self): + object1 = NoteOn("C4", 0x64) + self.assertEqual(object1.note, 60) + self.assertEqual(object1.velocity, 0x64) + + object2 = NoteOn("C3", 0x7f) + self.assertEqual(object2.note, 48) + self.assertEqual(object2.velocity, 0x7f) + + object3 = NoteOn("C#4", 0x7f) + self.assertEqual(object3.note, 61) + self.assertEqual(object3.velocity, 0x7f) + + def test_NoteOn_constructor_valueerror1(self): + with self.assertRaises(ValueError): + object1 = NoteOn(60, 0x80) + + def test_NoteOn_constructor_valueerror2(self): + with self.assertRaises(ValueError): + object2 = NoteOn(-1, 0x7f) + + def test_NoteOn_constructor_valueerror3(self): + with self.assertRaises(ValueError): + object3 = NoteOn(128, 0x7f) + + def test_NoteOn_constructor_upperrange1(self): + object = NoteOn("G9", 0x7f) + self.assertEqual(object.note, 127) + self.assertEqual(object.velocity, 0x7f) + + def test_NoteOn_constructor_upperrange2(self): + with self.assertRaises(ValueError): + object = NoteOn("G#9", 0x7f) + + def test_NoteOn_constructor_bogusstring(self): + with self.assertRaises(ValueError): + object = NoteOn("CC4", 0x7f) + + if __name__ == '__main__': unittest.main(verbosity=verbose) From 67226e791bd02c1348a7a6cdd5efe478ad5714e6 Mon Sep 17 00:00:00 2001 From: Kevin J Walters Date: Sun, 24 Mar 2019 10:03:00 +0000 Subject: [PATCH 26/92] Splitting out the individual and sequence tests in Test_MIDI. #3 --- examples/MIDI_unittests.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/examples/MIDI_unittests.py b/examples/MIDI_unittests.py index bc7f00a..3f4c99f 100644 --- a/examples/MIDI_unittests.py +++ b/examples/MIDI_unittests.py @@ -74,7 +74,7 @@ def test_smallsysex(self): def test_largerthanbuffersysex(self): self.assertEqual(TODO, TODO) - def test_send_basic(self): + def test_send_basic_single(self): #def printit(buffer, len): # print(buffer[0:len]) mockedPortIn = Mock() @@ -120,6 +120,16 @@ def test_send_basic(self): call(b'\x89\x6c\x7f', 3)) next += 1 + def test_send_basic_sequences(self): + #def printit(buffer, len): + # print(buffer[0:len]) + mockedPortIn = Mock() + #mockedPortIn.write = printit + + m = adafruit_midi.MIDI(midi_out=mockedPortIn, out_channel=2) + + # Test sending some NoteOn and NoteOff to various channels + next = 0 # Test the list syntax note_list = [NoteOn(0x6c, 0x51), NoteOn(0x70, 0x52), @@ -135,7 +145,7 @@ def test_send_basic(self): call(b'\x9b\x6c\x51\x9b\x70\x52\x9b\x73\x53', 9), "The implementation writes in one go, single 9 byte write expected") next += 1 - + if __name__ == '__main__': unittest.main(verbosity=verbose) From ba091d4ac89308a7e1efa19de95079af747a1ba4 Mon Sep 17 00:00:00 2001 From: Kevin J Walters Date: Sun, 24 Mar 2019 10:16:21 +0000 Subject: [PATCH 27/92] Unit tests for exception behaviour for MIDIMessage constructed objects passing into MIDI.send(). #3 --- examples/MIDI_unittests.py | 30 +++++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/examples/MIDI_unittests.py b/examples/MIDI_unittests.py index 3f4c99f..c3d7406 100644 --- a/examples/MIDI_unittests.py +++ b/examples/MIDI_unittests.py @@ -110,6 +110,7 @@ def test_send_basic_single(self): call(b'\x82\x67\x02', 3)) next += 1 + # Setting channel to non default m.send(NoteOn(0x6c, 0x7f), channel=9) self.assertEqual(mockedPortIn.write.mock_calls[next], call(b'\x99\x6c\x7f', 3)) @@ -120,6 +121,29 @@ def test_send_basic_single(self): call(b'\x89\x6c\x7f', 3)) next += 1 + def test_send_badnotes(self): + mockedPortIn = Mock() + + m = adafruit_midi.MIDI(midi_out=mockedPortIn, out_channel=2) + + # Test sending some NoteOn and NoteOff to various channels + next = 0 + m.send(NoteOn(60, 0x7f)) + self.assertEqual(mockedPortIn.write.mock_calls[next], + call(b'\x92\x3c\x7f', 3)) + next += 1 + with self.assertRaises(ValueError): + m.send(NoteOn(64, 0x80)) # Velocity > 127 - illegal value + + with self.assertRaises(ValueError): + m.send(NoteOn(67, -1)) + + # test after exceptions to ensure sending is still ok + m.send(NoteOn(72, 0x7f)) + self.assertEqual(mockedPortIn.write.mock_calls[next], + call(b'\x92\x48\x7f', 3)) + next += 1 + def test_send_basic_sequences(self): #def printit(buffer, len): # print(buffer[0:len]) @@ -130,7 +154,7 @@ def test_send_basic_sequences(self): # Test sending some NoteOn and NoteOff to various channels next = 0 - # Test the list syntax + # Test sequences with list syntax and pass a tuple too note_list = [NoteOn(0x6c, 0x51), NoteOn(0x70, 0x52), NoteOn(0x73, 0x53)]; @@ -145,7 +169,7 @@ def test_send_basic_sequences(self): call(b'\x9b\x6c\x51\x9b\x70\x52\x9b\x73\x53', 9), "The implementation writes in one go, single 9 byte write expected") next += 1 - - + + if __name__ == '__main__': unittest.main(verbosity=verbose) From 4dace75fd93d79e34834da43e8aaf7629bddf217 Mon Sep 17 00:00:00 2001 From: Kevin J Walters Date: Sun, 24 Mar 2019 23:23:17 +0000 Subject: [PATCH 28/92] Adding as_bytes() method to all the MIDIMessage children apart from Start and Stop which inherit a new default dataless one from MIDIMessage. #3 --- adafruit_midi/channel_pressure.py | 5 +++++ adafruit_midi/control_change.py | 5 +++++ adafruit_midi/midi_message.py | 13 +++++++++++-- adafruit_midi/note_off.py | 2 +- adafruit_midi/pitch_bend_change.py | 6 ++++++ adafruit_midi/polyphonic_key_pressure.py | 9 +++++++-- adafruit_midi/program_change.py | 5 +++++ adafruit_midi/system_exclusive.py | 7 +++++++ adafruit_midi/timing_clock.py | 3 +-- 9 files changed, 48 insertions(+), 7 deletions(-) diff --git a/adafruit_midi/channel_pressure.py b/adafruit_midi/channel_pressure.py index f46a4e5..f9a0700 100644 --- a/adafruit_midi/channel_pressure.py +++ b/adafruit_midi/channel_pressure.py @@ -57,6 +57,11 @@ class ChannelPressure(MIDIMessage): def __init__(self, pressure): self.pressure = pressure + # channel value is mandatory + def as_bytes(self, channel=None): + return bytearray([self._STATUS | (channel & self._CHANNELMASK), + self.pressure]) + @classmethod def from_bytes(cls, databytes): return cls(databytes[0]) diff --git a/adafruit_midi/control_change.py b/adafruit_midi/control_change.py index 59ade91..5072a9f 100644 --- a/adafruit_midi/control_change.py +++ b/adafruit_midi/control_change.py @@ -58,6 +58,11 @@ def __init__(self, control, value): self.control = control self.value = value + # channel value is mandatory + def as_bytes(self, channel=None): + return bytearray([self._STATUS | (channel & self._CHANNELMASK), + self.control, self.value]) + @classmethod def from_bytes(cls, databytes): return cls(databytes[0], databytes[1]) diff --git a/adafruit_midi/midi_message.py b/adafruit_midi/midi_message.py index 8c78eb3..30c39b3 100644 --- a/adafruit_midi/midi_message.py +++ b/adafruit_midi/midi_message.py @@ -53,6 +53,9 @@ note_offset = [9, 11, 12, 14, 16, 17, 19] def channel_filter(channel, channel_spec): + """ + Utility function to return True iff the given channel matches channel_spec. + """ if isinstance(channel_spec, int): if channel_spec == ALL_CHANNELS: return True @@ -225,9 +228,15 @@ def from_message_bytes(cls, midibytes, channel_in): else: return (None, startidx, msgendidxplusone, skipped, None) + # channel value present to keep interface uniform but unused + def as_bytes(self, channel=None): + """A default method for constructing wire messages with no data. + Returns a (mutable) bytearray with just status code in.""" + return bytearray([self._STATUS]) + @classmethod def from_bytes(cls, databytes): - """A default method for constructing messages that have no data. + """A default method for constructing message objects with no data. Returns the new object.""" return cls() @@ -235,6 +244,6 @@ def from_bytes(cls, databytes): # DO NOT try to register this message class MIDIUnknownEvent(MIDIMessage): _LENGTH = -1 - + def __init__(self, status): self.status = status diff --git a/adafruit_midi/note_off.py b/adafruit_midi/note_off.py index 7fbb1a3..b1288c1 100644 --- a/adafruit_midi/note_off.py +++ b/adafruit_midi/note_off.py @@ -58,7 +58,7 @@ def __init__(self, note, velocity): self.note = note_parser(note) self.velocity = velocity if not 0 <= self.note <= 127 or not 0 <= self.velocity <= 127: - return ValueError("Out of range") + raise ValueError("Out of range") # channel value is mandatory def as_bytes(self, channel=None): diff --git a/adafruit_midi/pitch_bend_change.py b/adafruit_midi/pitch_bend_change.py index 7e0556c..e16c379 100644 --- a/adafruit_midi/pitch_bend_change.py +++ b/adafruit_midi/pitch_bend_change.py @@ -57,6 +57,12 @@ class PitchBendChange(MIDIMessage): def __init__(self, pitch_bend): self.pitch_bend = pitch_bend + # channel value is mandatory + def as_bytes(self, channel=None): + return bytearray([self._STATUS | (channel & self._CHANNELMASK), + self.pitch_bend & 0x7f, + (self.pitch_bend >> 7) & 0x7f]) + @classmethod def from_bytes(cls, databytes): return cls(databytes[1] << 7 | databytes[0]) diff --git a/adafruit_midi/polyphonic_key_pressure.py b/adafruit_midi/polyphonic_key_pressure.py index c472701..5415a34 100644 --- a/adafruit_midi/polyphonic_key_pressure.py +++ b/adafruit_midi/polyphonic_key_pressure.py @@ -57,9 +57,14 @@ class PolyphonicKeyPressure(MIDIMessage): def __init__(self, note, pressure): self.note = note self.pressure = pressure - + + # channel value is mandatory + def as_bytes(self, channel=None): + return bytearray([self._STATUS | (channel & self._CHANNELMASK), + self.note, self.pressure]) + @classmethod def from_bytes(cls, databytes): return cls(databytes[0], databytes[1]) - + PolyphonicKeyPressure.register_message_type() diff --git a/adafruit_midi/program_change.py b/adafruit_midi/program_change.py index 79d18f6..4bf81b5 100644 --- a/adafruit_midi/program_change.py +++ b/adafruit_midi/program_change.py @@ -57,6 +57,11 @@ class ProgramChange(MIDIMessage): def __init__(self, patch): self.patch = patch + # channel value is mandatory + def as_bytes(self, channel=None): + return bytearray([self._STATUS | (channel & self._CHANNELMASK), + self.patch]) + @classmethod def from_bytes(cls, databytes): return cls(databytes[0]) diff --git a/adafruit_midi/system_exclusive.py b/adafruit_midi/system_exclusive.py index 04d235b..482af38 100644 --- a/adafruit_midi/system_exclusive.py +++ b/adafruit_midi/system_exclusive.py @@ -58,6 +58,13 @@ def __init__(self, manufacturer_id, data): self.manufacturer_id = manufacturer_id self.data = data + # channel value present to keep interface uniform but unused + def as_bytes(self, channel=None): + return (bytearray([self._STATUS]) + + self.manufacturer_id + + self.data + + [self._ENDSTATUS]) + @classmethod def from_bytes(cls, databytes): # -1 on second arg is to avoid the _ENDSTATUS which is passed diff --git a/adafruit_midi/timing_clock.py b/adafruit_midi/timing_clock.py index 2467f22..dcd23e0 100644 --- a/adafruit_midi/timing_clock.py +++ b/adafruit_midi/timing_clock.py @@ -48,8 +48,7 @@ __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_MIDI.git" -# Would be good to have this registered first as it occurs -# frequently when in-use +# Good to have this registered first as it occurs frequently when present class TimingClock(MIDIMessage): _STATUS = 0xf8 _STATUSMASK = 0xff From 88b28c4b1c0f2aaa8643a8fa06b973fb9992de2c Mon Sep 17 00:00:00 2001 From: Kevin J Walters Date: Sun, 24 Mar 2019 23:24:37 +0000 Subject: [PATCH 29/92] Duplicating NoteOn constructor tests for NoteOff and tweaking first test to include one with velocity 0. Importing all the messages now. #3 --- examples/MIDIMessage_unittests.py | 63 ++++++++++++++++++++++++++++--- 1 file changed, 58 insertions(+), 5 deletions(-) diff --git a/examples/MIDIMessage_unittests.py b/examples/MIDIMessage_unittests.py index 8bff530..e93af41 100644 --- a/examples/MIDIMessage_unittests.py +++ b/examples/MIDIMessage_unittests.py @@ -32,8 +32,19 @@ sys.modules['usb_midi'] = MagicMock() import adafruit_midi -from adafruit_midi.note_on import NoteOn -from adafruit_midi.system_exclusive import SystemExclusive + +# Full monty +from adafruit_midi.channel_pressure import ChannelPressure +from adafruit_midi.control_change import ControlChange +from adafruit_midi.note_off import NoteOff +from adafruit_midi.note_on import NoteOn +from adafruit_midi.pitch_bend_change import PitchBendChange +from adafruit_midi.polyphonic_key_pressure import PolyphonicKeyPressure +from adafruit_midi.program_change import ProgramChange +from adafruit_midi.start import Start +from adafruit_midi.stop import Stop +from adafruit_midi.system_exclusive import SystemExclusive +from adafruit_midi.timing_clock import TimingClock ### To incorporate into tests # This is using running status in a rather sporadic manner @@ -296,9 +307,9 @@ def test_NoteOn_constructor_string(self): self.assertEqual(object2.note, 48) self.assertEqual(object2.velocity, 0x7f) - object3 = NoteOn("C#4", 0x7f) + object3 = NoteOn("C#4", 0x00) self.assertEqual(object3.note, 61) - self.assertEqual(object3.velocity, 0x7f) + self.assertEqual(object3.velocity, 0) def test_NoteOn_constructor_valueerror1(self): with self.assertRaises(ValueError): @@ -319,12 +330,54 @@ def test_NoteOn_constructor_upperrange1(self): def test_NoteOn_constructor_upperrange2(self): with self.assertRaises(ValueError): - object = NoteOn("G#9", 0x7f) + object = NoteOn("G#9", 0x7f) # just above max note def test_NoteOn_constructor_bogusstring(self): with self.assertRaises(ValueError): object = NoteOn("CC4", 0x7f) + +class Test_MIDIMessage_NoteOff_constructor(unittest.TestCase): + # mostly cut and paste from NoteOn above + def test_NoteOff_constructor_string(self): + object1 = NoteOff("C4", 0x64) + self.assertEqual(object1.note, 60) + self.assertEqual(object1.velocity, 0x64) + + object2 = NoteOff("C3", 0x7f) + self.assertEqual(object2.note, 48) + self.assertEqual(object2.velocity, 0x7f) + + object3 = NoteOff("C#4", 0x00) + self.assertEqual(object3.note, 61) + self.assertEqual(object3.velocity, 0) + + def test_NoteOff_constructor_valueerror1(self): + with self.assertRaises(ValueError): + object1 = NoteOff(60, 0x80) + + def test_NoteOff_constructor_valueerror2(self): + with self.assertRaises(ValueError): + object2 = NoteOff(-1, 0x7f) + + def test_NoteOff_constructor_valueerror3(self): + with self.assertRaises(ValueError): + object3 = NoteOff(128, 0x7f) + def test_NoteOff_constructor_upperrange1(self): + object = NoteOff("G9", 0x7f) + self.assertEqual(object.note, 127) + self.assertEqual(object.velocity, 0x7f) + + def test_NoteOff_constructor_upperrange2(self): + with self.assertRaises(ValueError): + object = NoteOff("G#9", 0x7f) # just above max note + + def test_NoteOff_constructor_bogusstring(self): + with self.assertRaises(ValueError): + object = NoteOff("CC4", 0x7f) + + + if __name__ == '__main__': unittest.main(verbosity=verbose) From 17eda5b76b8003c929dacbe7736ee193dfc90d83 Mon Sep 17 00:00:00 2001 From: Kevin J Walters Date: Tue, 26 Mar 2019 23:49:20 +0000 Subject: [PATCH 30/92] Whitespace adjustment. #3 --- examples/MIDIMessage_unittests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/MIDIMessage_unittests.py b/examples/MIDIMessage_unittests.py index e93af41..b47f134 100644 --- a/examples/MIDIMessage_unittests.py +++ b/examples/MIDIMessage_unittests.py @@ -336,7 +336,7 @@ def test_NoteOn_constructor_bogusstring(self): with self.assertRaises(ValueError): object = NoteOn("CC4", 0x7f) - + class Test_MIDIMessage_NoteOff_constructor(unittest.TestCase): # mostly cut and paste from NoteOn above def test_NoteOff_constructor_string(self): From 06247a91bf3e09727e7f075f508e11a3b5e2c306 Mon Sep 17 00:00:00 2001 From: Kevin J Walters Date: Tue, 26 Mar 2019 23:52:42 +0000 Subject: [PATCH 31/92] Adding first mocked test using send(), capturing data and feeding it back to read_in_port() for some data featuring notes either side of a small SysEx. Fixing bugs in SystemExclusive message - as_bytes had some wrong types and construtor should force things to bytearray. #3 --- adafruit_midi/system_exclusive.py | 6 +- examples/MIDI_unittests.py | 107 +++++++++++++++++++++++------- 2 files changed, 86 insertions(+), 27 deletions(-) diff --git a/adafruit_midi/system_exclusive.py b/adafruit_midi/system_exclusive.py index 482af38..6c6382d 100644 --- a/adafruit_midi/system_exclusive.py +++ b/adafruit_midi/system_exclusive.py @@ -55,15 +55,15 @@ class SystemExclusive(MIDIMessage): _ENDSTATUS = 0xf7 def __init__(self, manufacturer_id, data): - self.manufacturer_id = manufacturer_id - self.data = data + self.manufacturer_id = bytearray(manufacturer_id) + self.data = bytearray(data) # channel value present to keep interface uniform but unused def as_bytes(self, channel=None): return (bytearray([self._STATUS]) + self.manufacturer_id + self.data - + [self._ENDSTATUS]) + + bytearray([self._ENDSTATUS])) @classmethod def from_bytes(cls, databytes): diff --git a/examples/MIDI_unittests.py b/examples/MIDI_unittests.py index c3d7406..a533b8e 100644 --- a/examples/MIDI_unittests.py +++ b/examples/MIDI_unittests.py @@ -30,8 +30,19 @@ import sys sys.modules['usb_midi'] = MagicMock() -from adafruit_midi.note_on import NoteOn -from adafruit_midi.note_off import NoteOff +# Full monty +from adafruit_midi.channel_pressure import ChannelPressure +from adafruit_midi.control_change import ControlChange +from adafruit_midi.note_off import NoteOff +from adafruit_midi.note_on import NoteOn +from adafruit_midi.pitch_bend_change import PitchBendChange +from adafruit_midi.polyphonic_key_pressure import PolyphonicKeyPressure +from adafruit_midi.program_change import ProgramChange +from adafruit_midi.start import Start +from adafruit_midi.stop import Stop +from adafruit_midi.system_exclusive import SystemExclusive +from adafruit_midi.timing_clock import TimingClock + import adafruit_midi @@ -68,68 +79,112 @@ def test_gooddatarunningstatus(self): ### comment this out as it wont work def test_somegoodsomemissingdatabytes(self): self.assertEqual(TODO, TODO) - def test_smallsysex(self): - self.assertEqual(TODO, TODO) + def test_smallsysex_between_notes(self): + usb_data = bytearray() + def write(buffer, length): + nonlocal usb_data + usb_data.extend(buffer[0:length]) + + def read(length): + nonlocal usb_data + poppedbytes = usb_data[0:length] + usb_data = usb_data[len(poppedbytes):] + return bytes(poppedbytes) + + mockedPortIn = Mock() + mockedPortIn.read = read + mockedPortOut = Mock() + mockedPortOut.write = write + m = adafruit_midi.MIDI(midi_out=mockedPortOut, midi_in=mockedPortIn, + out_channel=3, in_channel=3) + + m.send([NoteOn("C4", 0x7f), + SystemExclusive([0x1f], [1, 2, 3, 4, 5, 6, 7, 8]), + NoteOff(60, 0x28)]) + + (msg1, channel1) = m.read_in_port() + self.assertIsInstance(msg1, NoteOn) + self.assertEqual(msg1.note, 60) + self.assertEqual(msg1.velocity, 0x7f) + self.assertEqual(channel1, 3) + + (msg2, channel2) = m.read_in_port() + self.assertIsInstance(msg2, SystemExclusive) + self.assertEqual(msg2.manufacturer_id, bytearray([0x1f])) + self.assertEqual(msg2.data, bytearray([1, 2, 3, 4, 5, 6, 7, 8])) + self.assertEqual(channel2, None) # SysEx does not have a channel + + (msg3, channel3) = m.read_in_port() + self.assertIsInstance(msg3, NoteOff) + self.assertEqual(msg3.note, 60) + self.assertEqual(msg3.velocity, 0x28) + self.assertEqual(channel3, 3) + + (msg4, channel4) = m.read_in_port() + self.assertIsNone(msg4) + self.assertIsNone(channel4) def test_largerthanbuffersysex(self): self.assertEqual(TODO, TODO) + +class Test_MIDI_send(unittest.TestCase): def test_send_basic_single(self): #def printit(buffer, len): # print(buffer[0:len]) - mockedPortIn = Mock() - #mockedPortIn.write = printit + mockedPortOut = Mock() + #mockedPortOut.write = printit - m = adafruit_midi.MIDI(midi_out=mockedPortIn, out_channel=2) + m = adafruit_midi.MIDI(midi_out=mockedPortOut, out_channel=2) # Test sending some NoteOn and NoteOff to various channels next = 0 m.send(NoteOn(0x60, 0x7f)) - self.assertEqual(mockedPortIn.write.mock_calls[next], + self.assertEqual(mockedPortOut.write.mock_calls[next], call(b'\x92\x60\x7f', 3)) next += 1 m.send(NoteOn(0x64, 0x3f)) - self.assertEqual(mockedPortIn.write.mock_calls[next], + self.assertEqual(mockedPortOut.write.mock_calls[next], call(b'\x92\x64\x3f', 3)) next += 1 m.send(NoteOn(0x67, 0x1f)) - self.assertEqual(mockedPortIn.write.mock_calls[next], + self.assertEqual(mockedPortOut.write.mock_calls[next], call(b'\x92\x67\x1f', 3)) next += 1 m.send(NoteOn(0x60, 0x00)) # Alternative to NoteOff - self.assertEqual(mockedPortIn.write.mock_calls[next], + self.assertEqual(mockedPortOut.write.mock_calls[next], call(b'\x92\x60\x00', 3)) next += 1 m.send(NoteOff(0x64, 0x01)) - self.assertEqual(mockedPortIn.write.mock_calls[next], + self.assertEqual(mockedPortOut.write.mock_calls[next], call(b'\x82\x64\x01', 3)) next += 1 m.send(NoteOff(0x67, 0x02)) - self.assertEqual(mockedPortIn.write.mock_calls[next], + self.assertEqual(mockedPortOut.write.mock_calls[next], call(b'\x82\x67\x02', 3)) next += 1 # Setting channel to non default m.send(NoteOn(0x6c, 0x7f), channel=9) - self.assertEqual(mockedPortIn.write.mock_calls[next], + self.assertEqual(mockedPortOut.write.mock_calls[next], call(b'\x99\x6c\x7f', 3)) next += 1 m.send(NoteOff(0x6c, 0x7f), channel=9) - self.assertEqual(mockedPortIn.write.mock_calls[next], + self.assertEqual(mockedPortOut.write.mock_calls[next], call(b'\x89\x6c\x7f', 3)) next += 1 def test_send_badnotes(self): - mockedPortIn = Mock() + mockedPortOut = Mock() - m = adafruit_midi.MIDI(midi_out=mockedPortIn, out_channel=2) + m = adafruit_midi.MIDI(midi_out=mockedPortOut, out_channel=2) # Test sending some NoteOn and NoteOff to various channels next = 0 m.send(NoteOn(60, 0x7f)) - self.assertEqual(mockedPortIn.write.mock_calls[next], + self.assertEqual(mockedPortOut.write.mock_calls[next], call(b'\x92\x3c\x7f', 3)) next += 1 with self.assertRaises(ValueError): @@ -140,17 +195,17 @@ def test_send_badnotes(self): # test after exceptions to ensure sending is still ok m.send(NoteOn(72, 0x7f)) - self.assertEqual(mockedPortIn.write.mock_calls[next], + self.assertEqual(mockedPortOut.write.mock_calls[next], call(b'\x92\x48\x7f', 3)) next += 1 def test_send_basic_sequences(self): #def printit(buffer, len): # print(buffer[0:len]) - mockedPortIn = Mock() - #mockedPortIn.write = printit + mockedPortOut = Mock() + #mockedPortOut.write = printit - m = adafruit_midi.MIDI(midi_out=mockedPortIn, out_channel=2) + m = adafruit_midi.MIDI(midi_out=mockedPortOut, out_channel=2) # Test sending some NoteOn and NoteOff to various channels next = 0 @@ -160,16 +215,20 @@ def test_send_basic_sequences(self): NoteOn(0x73, 0x53)]; note_tuple = tuple(note_list) m.send(note_list, channel=10) - self.assertEqual(mockedPortIn.write.mock_calls[next], + self.assertEqual(mockedPortOut.write.mock_calls[next], call(b'\x9a\x6c\x51\x9a\x70\x52\x9a\x73\x53', 9), "The implementation writes in one go, single 9 byte write expected") next += 1 m.send(note_tuple, channel=11) - self.assertEqual(mockedPortIn.write.mock_calls[next], + self.assertEqual(mockedPortOut.write.mock_calls[next], call(b'\x9b\x6c\x51\x9b\x70\x52\x9b\x73\x53', 9), "The implementation writes in one go, single 9 byte write expected") next += 1 +class Test_MIDI_send_receive_loop(unittest.TestCase): + def test_do_something_that_collects_sent_data_then_parses_it(self): + self.assertEqual(TODO, TODO) + if __name__ == '__main__': unittest.main(verbosity=verbose) From 6b4cb0ccb0ef92034670d428db0e8f0c06dd3cea Mon Sep 17 00:00:00 2001 From: Kevin J Walters Date: Wed, 27 Mar 2019 15:02:40 +0000 Subject: [PATCH 32/92] Whitespace tabbing correction. #3 --- adafruit_midi/note_off.py | 2 +- adafruit_midi/note_on.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/adafruit_midi/note_off.py b/adafruit_midi/note_off.py index b1288c1..9f1accf 100644 --- a/adafruit_midi/note_off.py +++ b/adafruit_midi/note_off.py @@ -58,7 +58,7 @@ def __init__(self, note, velocity): self.note = note_parser(note) self.velocity = velocity if not 0 <= self.note <= 127 or not 0 <= self.velocity <= 127: - raise ValueError("Out of range") + raise ValueError("Out of range") # channel value is mandatory def as_bytes(self, channel=None): diff --git a/adafruit_midi/note_on.py b/adafruit_midi/note_on.py index 0661a6d..1d34ba9 100644 --- a/adafruit_midi/note_on.py +++ b/adafruit_midi/note_on.py @@ -58,7 +58,7 @@ def __init__(self, note, velocity): self.note = note_parser(note) self.velocity = velocity if not 0 <= self.note <= 127 or not 0 <= self.velocity <= 127: - raise ValueError("Out of range") + raise ValueError("Out of range") # channel value is mandatory def as_bytes(self, channel=None): From 8705e688182876e7136de7a867f07e05fe9bfc46 Mon Sep 17 00:00:00 2001 From: Kevin J Walters Date: Wed, 27 Mar 2019 15:51:27 +0000 Subject: [PATCH 33/92] Adding range checking to rest of non-dataless MIDI messages following the unusual post assignment positioning in NoteOn/NoteOff. #3 --- adafruit_midi/channel_pressure.py | 6 ++++-- adafruit_midi/control_change.py | 4 +++- adafruit_midi/pitch_bend_change.py | 4 +++- adafruit_midi/polyphonic_key_pressure.py | 2 ++ adafruit_midi/program_change.py | 4 +++- adafruit_midi/system_exclusive.py | 2 +- 6 files changed, 16 insertions(+), 6 deletions(-) diff --git a/adafruit_midi/channel_pressure.py b/adafruit_midi/channel_pressure.py index f9a0700..65aaefb 100644 --- a/adafruit_midi/channel_pressure.py +++ b/adafruit_midi/channel_pressure.py @@ -53,10 +53,12 @@ class ChannelPressure(MIDIMessage): _STATUSMASK = 0xf0 _LENGTH = 2 _CHANNELMASK = 0x0f - + def __init__(self, pressure): self.pressure = pressure - + if not 0 <= self.pressure <= 127: + raise ValueError("Out of range") + # channel value is mandatory def as_bytes(self, channel=None): return bytearray([self._STATUS | (channel & self._CHANNELMASK), diff --git a/adafruit_midi/control_change.py b/adafruit_midi/control_change.py index 5072a9f..e401a16 100644 --- a/adafruit_midi/control_change.py +++ b/adafruit_midi/control_change.py @@ -57,7 +57,9 @@ class ControlChange(MIDIMessage): def __init__(self, control, value): self.control = control self.value = value - + if not 0 <= self.control <= 127 or not 0 <= self.value <= 127: + raise ValueError("Out of range") + # channel value is mandatory def as_bytes(self, channel=None): return bytearray([self._STATUS | (channel & self._CHANNELMASK), diff --git a/adafruit_midi/pitch_bend_change.py b/adafruit_midi/pitch_bend_change.py index e16c379..eecc162 100644 --- a/adafruit_midi/pitch_bend_change.py +++ b/adafruit_midi/pitch_bend_change.py @@ -56,7 +56,9 @@ class PitchBendChange(MIDIMessage): def __init__(self, pitch_bend): self.pitch_bend = pitch_bend - + if not 0 <= self.pitch_bend <= 16383: + raise ValueError("Out of range") + # channel value is mandatory def as_bytes(self, channel=None): return bytearray([self._STATUS | (channel & self._CHANNELMASK), diff --git a/adafruit_midi/polyphonic_key_pressure.py b/adafruit_midi/polyphonic_key_pressure.py index 5415a34..b70e649 100644 --- a/adafruit_midi/polyphonic_key_pressure.py +++ b/adafruit_midi/polyphonic_key_pressure.py @@ -57,6 +57,8 @@ class PolyphonicKeyPressure(MIDIMessage): def __init__(self, note, pressure): self.note = note self.pressure = pressure + if not 0 <= self.note <= 127 or not 0 <= self.pressure <= 127: + raise ValueError("Out of range") # channel value is mandatory def as_bytes(self, channel=None): diff --git a/adafruit_midi/program_change.py b/adafruit_midi/program_change.py index 4bf81b5..8824dbc 100644 --- a/adafruit_midi/program_change.py +++ b/adafruit_midi/program_change.py @@ -56,7 +56,9 @@ class ProgramChange(MIDIMessage): def __init__(self, patch): self.patch = patch - + if not 0 <= self.patch <= 127: + raise ValueError("Out of range") + # channel value is mandatory def as_bytes(self, channel=None): return bytearray([self._STATUS | (channel & self._CHANNELMASK), diff --git a/adafruit_midi/system_exclusive.py b/adafruit_midi/system_exclusive.py index 6c6382d..2b9f02f 100644 --- a/adafruit_midi/system_exclusive.py +++ b/adafruit_midi/system_exclusive.py @@ -64,7 +64,7 @@ def as_bytes(self, channel=None): + self.manufacturer_id + self.data + bytearray([self._ENDSTATUS])) - + @classmethod def from_bytes(cls, databytes): # -1 on second arg is to avoid the _ENDSTATUS which is passed From d3b0ece71409784d92fbc5a0a936d33027bda198 Mon Sep 17 00:00:00 2001 From: Kevin J Walters Date: Thu, 28 Mar 2019 20:47:38 +0000 Subject: [PATCH 34/92] Making a function out of code to setup the loopback/echo send/receive for MIDI. #3 --- examples/MIDI_unittests.py | 42 ++++++++++++++++++++++---------------- 1 file changed, 24 insertions(+), 18 deletions(-) diff --git a/examples/MIDI_unittests.py b/examples/MIDI_unittests.py index a533b8e..36bc23b 100644 --- a/examples/MIDI_unittests.py +++ b/examples/MIDI_unittests.py @@ -66,6 +66,27 @@ ### TODO - re work these when running status is implemented +# For loopback/echo tests +def MIDI_mocked_both_loopback(in_c, out_c): + usb_data = bytearray() + def write(buffer, length): + nonlocal usb_data + usb_data.extend(buffer[0:length]) + + def read(length): + nonlocal usb_data + poppedbytes = usb_data[0:length] + usb_data = usb_data[len(poppedbytes):] + return bytes(poppedbytes) + + mockedPortIn = Mock() + mockedPortIn.read = read + mockedPortOut = Mock() + mockedPortOut.write = write + m = adafruit_midi.MIDI(midi_out=mockedPortOut, midi_in=mockedPortIn, + out_channel=out_c, in_channel=in_c) + return m + class Test_MIDI(unittest.TestCase): def test_goodmididatasmall(self): self.assertEqual(TODO, TODO) @@ -76,27 +97,12 @@ def test_goodmididatasmall(self): def test_gooddatarunningstatus(self): ### comment this out as it wont work self.assertEqual(TODO, TODO) - def test_somegoodsomemissingdatabytes(self): + def test_somegood_somemissing_databytes(self): + m = MIDI_mocked_both_loopback(8, 8) self.assertEqual(TODO, TODO) def test_smallsysex_between_notes(self): - usb_data = bytearray() - def write(buffer, length): - nonlocal usb_data - usb_data.extend(buffer[0:length]) - - def read(length): - nonlocal usb_data - poppedbytes = usb_data[0:length] - usb_data = usb_data[len(poppedbytes):] - return bytes(poppedbytes) - - mockedPortIn = Mock() - mockedPortIn.read = read - mockedPortOut = Mock() - mockedPortOut.write = write - m = adafruit_midi.MIDI(midi_out=mockedPortOut, midi_in=mockedPortIn, - out_channel=3, in_channel=3) + m = MIDI_mocked_both_loopback(3, 3) m.send([NoteOn("C4", 0x7f), SystemExclusive([0x1f], [1, 2, 3, 4, 5, 6, 7, 8]), From cc4642fc0c227d87643fc2e620ad2e9c8bf82194 Mon Sep 17 00:00:00 2001 From: Kevin J Walters Date: Thu, 28 Mar 2019 22:01:19 +0000 Subject: [PATCH 35/92] Added test test_somegood_somemissing_databytes which uses new MIDI_mocked_receive(). #3 --- examples/MIDI_unittests.py | 68 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 65 insertions(+), 3 deletions(-) diff --git a/examples/MIDI_unittests.py b/examples/MIDI_unittests.py index 36bc23b..73dcd4b 100644 --- a/examples/MIDI_unittests.py +++ b/examples/MIDI_unittests.py @@ -87,6 +87,29 @@ def read(length): out_channel=out_c, in_channel=in_c) return m +def MIDI_mocked_receive(in_c, data, read_sizes): + usb_data = bytearray(data) + chunks = read_sizes + chunk_idx = 0 + + def read(length): + nonlocal usb_data, chunks, chunk_idx + if chunk_idx < len(chunks): + poppedbytes = usb_data[0:chunks[chunk_idx]] + usb_data = usb_data[len(poppedbytes):] + chunk_idx += 1 + return bytes(poppedbytes) + else: + return bytes() + + mockedPortIn = Mock() + mockedPortIn.read = read + + m = adafruit_midi.MIDI(midi_out=None, midi_in=mockedPortIn, + out_channel=in_c, in_channel=in_c) + return m + + class Test_MIDI(unittest.TestCase): def test_goodmididatasmall(self): self.assertEqual(TODO, TODO) @@ -98,8 +121,47 @@ def test_gooddatarunningstatus(self): ### comment this out as it wont work self.assertEqual(TODO, TODO) def test_somegood_somemissing_databytes(self): - m = MIDI_mocked_both_loopback(8, 8) - self.assertEqual(TODO, TODO) + c = 8 + raw_data = (NoteOn("C5", 0x7f,).as_bytes(channel=c) + + bytearray([0xe8, 0x72, 0x40] + + [0xe8, 0x6d ] # Missing last data byte + + [0xe8, 0x5, 0x41 ]) + + NoteOn("D5", 0x7f).as_bytes(channel=c)) + m = MIDI_mocked_receive(c, raw_data, [3 + 3 + 2 + 3 + 3]) + + (msg1, channel1) = m.read_in_port() + self.assertIsInstance(msg1, NoteOn) + self.assertEqual(msg1.note, 72) + self.assertEqual(msg1.velocity, 0x7f) + self.assertEqual(channel1, c) + + (msg2, channel2) = m.read_in_port() + self.assertIsInstance(msg2, PitchBendChange) + self.assertEqual(msg2.pitch_bend, 8306) + self.assertEqual(channel2, c) + + # The current implementation will read status bytes for data + # In most cases it would be a faster recovery with fewer messages + # lost if status byte wasn't consumed and parsing restart from that + (msg3, channel3) = m.read_in_port() + self.assertIsInstance(msg3, adafruit_midi.midi_message.MIDIBadEvent) + self.assertEqual(msg3.data, bytearray([0x6d, 0xe8])) + self.assertEqual(channel3, c) + + #(msg4, channel4) = m.read_in_port() + #self.assertIsInstance(msg4, PitchBendChange) + #self.assertEqual(msg4.pitch_bend, 72) + #self.assertEqual(channel4, c) + + (msg5, channel5) = m.read_in_port() + self.assertIsInstance(msg5, NoteOn) + self.assertEqual(msg5.note, 74) + self.assertEqual(msg5.velocity, 0x7f) + self.assertEqual(channel5, c) + + (msg6, channel6) = m.read_in_port() + self.assertIsNone(msg6) + self.assertIsNone(channel6) def test_smallsysex_between_notes(self): m = MIDI_mocked_both_loopback(3, 3) @@ -235,6 +297,6 @@ class Test_MIDI_send_receive_loop(unittest.TestCase): def test_do_something_that_collects_sent_data_then_parses_it(self): self.assertEqual(TODO, TODO) - + if __name__ == '__main__': unittest.main(verbosity=verbose) From f74992f1e4416c44e639d1acde3389ada92e3078 Mon Sep 17 00:00:00 2001 From: Kevin J Walters Date: Thu, 28 Mar 2019 23:18:17 +0000 Subject: [PATCH 36/92] Added test test_larger_than_buffer_sysex which tests a SystemExclusive message which cannot fit in the default input buffer size and therefore cannot be parsed. Fixed a bug in variable length parsing where object creation was still attempted for an unterminated variable length message. #3 --- adafruit_midi/midi_message.py | 24 +++++++++++--- examples/MIDI_unittests.py | 61 +++++++++++++++++++++++++++++++---- 2 files changed, 73 insertions(+), 12 deletions(-) diff --git a/adafruit_midi/midi_message.py b/adafruit_midi/midi_message.py index 30c39b3..45cb888 100644 --- a/adafruit_midi/midi_message.py +++ b/adafruit_midi/midi_message.py @@ -80,7 +80,7 @@ def note_parser(note): raise ValueError("Bad note format") noteidx = ord(note[0].upper()) - 65 # 65 os ord('A') if not 0 <= noteidx <= 6: - raise ValueError("Bad note") + raise ValueError("Bad note") sharpen = 0 if note[1] == '#': sharpen = 1 @@ -176,6 +176,7 @@ def from_message_bytes(cls, midibytes, channel_in): if sm[0] == masked_status: known_message = True # Check there's enough left to parse a complete message + # this value can be changed later for a var. length msgs complete_message = len(midibytes) - msgstartidx >= msgclass._LENGTH if complete_message: if msgclass._CHANNELMASK is not None: @@ -196,12 +197,18 @@ def from_message_bytes(cls, midibytes, channel_in): else: msgendidxplusone += 1 if terminated_message or bad_termination: - msgendidxplusone += 1 + msgendidxplusone += 1 + if not terminated_message: + complete_message = False else: msgendidxplusone = msgstartidx + msgclass._LENGTH - if not bad_termination and channel_match_orNA: - msg = msgclass.from_bytes(midibytes[msgstartidx+1:msgendidxplusone]) + if complete_message and not bad_termination and channel_match_orNA: + try: + msg = msgclass.from_bytes(midibytes[msgstartidx+1:msgendidxplusone]) + except(ValueError, TypeError) as e: + msg = MIDIBadEvent(midibytes[msgstartidx+1:msgendidxplusone], e) + break # for # break out of while loop for a complete message on good channel @@ -241,9 +248,16 @@ def from_bytes(cls, databytes): return cls() -# DO NOT try to register this message +# DO NOT try to register these messages class MIDIUnknownEvent(MIDIMessage): _LENGTH = -1 def __init__(self, status): self.status = status + +class MIDIBadEvent(MIDIMessage): + _LENGTH = -1 + + def __init__(self, data, exception): + self.data = bytearray(data) + self.exception_text = repr(exception) diff --git a/examples/MIDI_unittests.py b/examples/MIDI_unittests.py index 73dcd4b..9be33c9 100644 --- a/examples/MIDI_unittests.py +++ b/examples/MIDI_unittests.py @@ -94,14 +94,18 @@ def MIDI_mocked_receive(in_c, data, read_sizes): def read(length): nonlocal usb_data, chunks, chunk_idx - if chunk_idx < len(chunks): - poppedbytes = usb_data[0:chunks[chunk_idx]] + if length != 0 and chunk_idx < len(chunks): + # min() to ensure we only read what's asked for and present + poppedbytes = usb_data[0:min(length, chunks[chunk_idx])] usb_data = usb_data[len(poppedbytes):] - chunk_idx += 1 + if length >= chunks[chunk_idx]: + chunk_idx += 1 + else: + chunks[chunk_idx] -= length return bytes(poppedbytes) else: return bytes() - + mockedPortIn = Mock() mockedPortIn.read = read @@ -192,9 +196,52 @@ def test_smallsysex_between_notes(self): self.assertIsNone(msg4) self.assertIsNone(channel4) - def test_largerthanbuffersysex(self): - self.assertEqual(TODO, TODO) + def test_larger_than_buffer_sysex(self): + c = 0 + monster_data_len = 500 + raw_data = (NoteOn("C5", 0x7f,).as_bytes(channel=c) + + SystemExclusive([0x02], + [d & 0x7f for d in range(monster_data_len)]).as_bytes(channel=c) + + NoteOn("D5", 0x7f).as_bytes(channel=c)) + m = MIDI_mocked_receive(c, raw_data, [len(raw_data)]) + buffer_len = m._in_buf_size + + self.assertTrue(monster_data_len > buffer_len, + "checking our SysEx truly is a monster") + + (msg1, channel1) = m.read_in_port() + self.assertIsInstance(msg1, NoteOn) + self.assertEqual(msg1.note, 72) + self.assertEqual(msg1.velocity, 0x7f) + self.assertEqual(channel1, c) + # (Ab)using python's rounding down for negative division + for n in range(-(-(1 + 1 + monster_data_len + 1) // buffer_len) - 1): + (msg2, channel2) = m.read_in_port() + self.assertIsNone(msg2) + self.assertIsNone(channel2) + + # The current implementation will read SysEx end status byte + # and report it as an unknown + (msg3, channel3) = m.read_in_port() + self.assertIsInstance(msg3, adafruit_midi.midi_message.MIDIUnknownEvent) + self.assertEqual(msg3.status, 0xf7) + self.assertIsNone(channel3) + + #(msg4, channel4) = m.read_in_port() + #self.assertIsInstance(msg4, PitchBendChange) + #self.assertEqual(msg4.pitch_bend, 72) + #self.assertEqual(channel4, c) + + (msg5, channel5) = m.read_in_port() + self.assertIsInstance(msg5, NoteOn) + self.assertEqual(msg5.note, 74) + self.assertEqual(msg5.velocity, 0x7f) + self.assertEqual(channel5, c) + + (msg6, channel6) = m.read_in_port() + self.assertIsNone(msg6) + self.assertIsNone(channel6) class Test_MIDI_send(unittest.TestCase): def test_send_basic_single(self): @@ -266,7 +313,7 @@ def test_send_badnotes(self): self.assertEqual(mockedPortOut.write.mock_calls[next], call(b'\x92\x48\x7f', 3)) next += 1 - + def test_send_basic_sequences(self): #def printit(buffer, len): # print(buffer[0:len]) From 4c68e6ebaef5113314e91ac4aefaab3c9d25c3de Mon Sep 17 00:00:00 2001 From: Kevin J Walters Date: Thu, 28 Mar 2019 23:31:26 +0000 Subject: [PATCH 37/92] Adjusting travis script so it runs tests and pylints them and relocating tests from examples to new tests directory. #3 --- .travis.yml | 6 +++++- {examples => tests}/MIDIMessage_unittests.py | 0 {examples => tests}/MIDI_unittests.py | 0 3 files changed, 5 insertions(+), 1 deletion(-) rename {examples => tests}/MIDIMessage_unittests.py (100%) rename {examples => tests}/MIDI_unittests.py (100%) diff --git a/.travis.yml b/.travis.yml index c60d5ad..720032c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,6 +3,8 @@ # See https://github.com/adafruit/circuitpython-build-tools for detailed setup # instructions. +# CUSTOMISED to run tests +# dist: xenial language: python python: @@ -42,7 +44,9 @@ install: - pip install --force-reinstall pylint==1.9.2 script: - - pylint adafruit_midi.py + - ([[ -d "tests" ]] && py.test) + - pylint adafruit_midi/*.py + - ([[ -d "tests" ]] && pylint --disable=missing-docstring,invalid-name,bad-whitespace tests/*.py) - ([[ ! -d "examples" ]] || pylint --disable=missing-docstring,invalid-name,bad-whitespace examples/*.py) - circuitpython-build-bundles --filename_prefix adafruit-circuitpython-midi --library_location . - cd docs && sphinx-build -E -W -b html . _build/html && cd .. diff --git a/examples/MIDIMessage_unittests.py b/tests/MIDIMessage_unittests.py similarity index 100% rename from examples/MIDIMessage_unittests.py rename to tests/MIDIMessage_unittests.py diff --git a/examples/MIDI_unittests.py b/tests/MIDI_unittests.py similarity index 100% rename from examples/MIDI_unittests.py rename to tests/MIDI_unittests.py From 0a4c7d715f50756418091fc0d3773e93ba0e51af Mon Sep 17 00:00:00 2001 From: Kevin J Walters Date: Thu, 28 Mar 2019 23:38:05 +0000 Subject: [PATCH 38/92] Adjusting MIDI channel printing in examples to present the channel numbers as the ones musicians know, not wire protocol ones. #3 --- examples/midi_intest1.py | 6 +++--- examples/midi_simpletest.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/examples/midi_intest1.py b/examples/midi_intest1.py index ba88ca7..8c618e7 100644 --- a/examples/midi_intest1.py +++ b/examples/midi_intest1.py @@ -7,8 +7,8 @@ print("Midi test II") -print("Input channel:", midi.in_channel) -print("Listening on input channel:", midi.in_channel) +print("Input channel:", midi.in_channel + 1 ) +print("Listening on input channel:", midi.in_channel + 1) # play with the pause to simulate code doing other stuff # in the loop @@ -20,4 +20,4 @@ if msg is not None: print(time.monotonic(), msg) if pause: - time.sleep(pause) \ No newline at end of file + time.sleep(pause) diff --git a/examples/midi_simpletest.py b/examples/midi_simpletest.py index 1dea1c3..3cc562b 100644 --- a/examples/midi_simpletest.py +++ b/examples/midi_simpletest.py @@ -6,8 +6,8 @@ print("Midi test") -print("Default output channel:", midi.out_channel) -print("Listening on input channel:", midi.in_channel) +print("Default output channel:", midi.out_channel + 1) +print("Listening on input channel:", midi.in_channel + 1) while True: midi.note_on(44, 120) From 5a778e461f7a6290df844beed1d8f84de9b91b61 Mon Sep 17 00:00:00 2001 From: Kevin J Walters Date: Fri, 29 Mar 2019 00:42:37 +0000 Subject: [PATCH 39/92] Showing equivalent of new interface in new examples/midi_simpletest2.py. #3 --- examples/midi_intest1.py | 1 + examples/midi_simpletest.py | 4 +++- examples/midi_simpletest2.py | 35 +++++++++++++++++++++++++++++++++++ 3 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 examples/midi_simpletest2.py diff --git a/examples/midi_intest1.py b/examples/midi_intest1.py index 8c618e7..4277865 100644 --- a/examples/midi_intest1.py +++ b/examples/midi_intest1.py @@ -7,6 +7,7 @@ print("Midi test II") +# Convert channel numbers at the presentation layer to the ones musicians use print("Input channel:", midi.in_channel + 1 ) print("Listening on input channel:", midi.in_channel + 1) diff --git a/examples/midi_simpletest.py b/examples/midi_simpletest.py index 3cc562b..7bc07df 100644 --- a/examples/midi_simpletest.py +++ b/examples/midi_simpletest.py @@ -6,8 +6,10 @@ print("Midi test") +# Convert channel numbers at the presentation layer to the ones musicians use print("Default output channel:", midi.out_channel + 1) -print("Listening on input channel:", midi.in_channel + 1) +print("Listening on input channel:", + midi.in_channel + 1 if midi.in_channel is not None else None) while True: midi.note_on(44, 120) diff --git a/examples/midi_simpletest2.py b/examples/midi_simpletest2.py new file mode 100644 index 0000000..bada06d --- /dev/null +++ b/examples/midi_simpletest2.py @@ -0,0 +1,35 @@ +# simple_test demonstrating both interfaces +import time +import random +import adafruit_midi +from adafruit_midi.control_change import ControlChange +from adafruit_midi.note_off import NoteOff +from adafruit_midi.note_on import NoteOn +from adafruit_midi.pitch_bend_change import PitchBendChange + +midi = adafruit_midi.MIDI(out_channel=0) + +print("Midi test") + +# Convert channel numbers at the presentation layer to the ones musicians use +print("Default output channel:", midi.out_channel + 1) +print("Listening on input channel:", + midi.in_channel + 1 if midi.in_channel is not None else None) + +while True: + # method per message interface + midi.note_on(44, 120) + time.sleep(0.25) + midi.pitch_bend(random.randint(0, 16383)) + time.sleep(0.25) + midi.note_off(44, 120) + midi.control_change(3, 44) + time.sleep(0.5) + # send message(s) interface + midi.send(NoteOn(44, 120)) + time.sleep(0.25) + midi.send(PitchBendChange(random.randint(0, 16383))) + time.sleep(0.25) + midi.send([NoteOff("G#2", 120), + ControlChange(3, 44)]) + time.sleep(0.5) From 47f0a7ed73be0f8b968059bd46eae1bca99d7156 Mon Sep 17 00:00:00 2001 From: Kevin J Walters Date: Fri, 29 Mar 2019 14:32:26 +0000 Subject: [PATCH 40/92] Minor tweak to MIDI_mocked_receive.read() to deal with case of chunk sizes being incorrect. #3 --- tests/MIDI_unittests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/MIDI_unittests.py b/tests/MIDI_unittests.py index 9be33c9..d1e5e49 100644 --- a/tests/MIDI_unittests.py +++ b/tests/MIDI_unittests.py @@ -101,7 +101,7 @@ def read(length): if length >= chunks[chunk_idx]: chunk_idx += 1 else: - chunks[chunk_idx] -= length + chunks[chunk_idx] -= len(poppedbytes) return bytes(poppedbytes) else: return bytes() From 053e9c013b5eecb6548e98bcd72785bbcb1858fb Mon Sep 17 00:00:00 2001 From: Kevin J Walters Date: Fri, 29 Mar 2019 14:56:36 +0000 Subject: [PATCH 41/92] Putting the data into the renamed test_running_status_when_implemented test for when it is eventually implemented. #8 --- tests/MIDI_unittests.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/tests/MIDI_unittests.py b/tests/MIDI_unittests.py index d1e5e49..6160b2c 100644 --- a/tests/MIDI_unittests.py +++ b/tests/MIDI_unittests.py @@ -121,9 +121,18 @@ def test_goodmididatasmall(self): def test_goodmididatasmall(self): self.assertEqual(TODO, TODO) - def test_gooddatarunningstatus(self): ### comment this out as it wont work - self.assertEqual(TODO, TODO) + # See https://github.com/adafruit/Adafruit_CircuitPython_MIDI/issues/8 + def test_running_status_when_implemented(self): + c = 8 + raw_data = (NoteOn("C5", 0x7f,).as_bytes(channel=c) + + bytearray([0xe8, 0x72, 0x40] + + [0x6d, 0x40] + + [0x05, 0x41]) + + NoteOn("D5", 0x7f).as_bytes(channel=c)) + m = MIDI_mocked_receive(c, raw_data, [3 + 3 + 2 + 3 + 3]) + #self.assertEqual(TOFINISH, WHENIMPLEMENTED) + def test_somegood_somemissing_databytes(self): c = 8 raw_data = (NoteOn("C5", 0x7f,).as_bytes(channel=c) From 7a37669f2c776929ed98c8e13c77cdea782a044d Mon Sep 17 00:00:00 2001 From: Kevin J Walters Date: Fri, 29 Mar 2019 15:30:19 +0000 Subject: [PATCH 42/92] New test test_captured_data_one_byte_reads() working from some slightly data modified MIDI data from an Axiom controller with atypical reads of 1 byte. #3 --- tests/MIDI_unittests.py | 79 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 70 insertions(+), 9 deletions(-) diff --git a/tests/MIDI_unittests.py b/tests/MIDI_unittests.py index 6160b2c..9334629 100644 --- a/tests/MIDI_unittests.py +++ b/tests/MIDI_unittests.py @@ -115,11 +115,76 @@ def read(length): class Test_MIDI(unittest.TestCase): - def test_goodmididatasmall(self): - self.assertEqual(TODO, TODO) - - def test_goodmididatasmall(self): - self.assertEqual(TODO, TODO) + def test_captured_data_one_byte_reads(self): + c = 0 + # From an M-Audio AXIOM controller + raw_data = bytearray([0x90, 0x3e, 0x5f] + + [ 0xd0, 0x10] + + [ 0x90, 0x40, 0x66 ] + + [ 0xb0, 0x1, 0x08 ] + + [ 0x90, 0x41, 0x74 ] + + [ 0xe0, 0x03, 0x40 ]) + m = MIDI_mocked_receive(c, raw_data, [1] * len(raw_data)) + + for read in range(100): + (msg, channel) = m.read_in_port() + if msg is not None: + break + self.assertIsInstance(msg, NoteOn) + self.assertEqual(msg.note, 0x3e) + self.assertEqual(msg.velocity, 0x5f) + self.assertEqual(channel, c) + + # for loops currently absorb any Nones but could + # be set to read precisely the expected number... + for read in range(100): + (msg, channel) = m.read_in_port() + if msg is not None: + break + self.assertIsInstance(msg, ChannelPressure) + self.assertEqual(msg.pressure, 0x10) + self.assertEqual(channel, c) + + for read in range(100): + (msg, channel) = m.read_in_port() + if msg is not None: + break + self.assertIsInstance(msg, NoteOn) + self.assertEqual(msg.note, 0x40) + self.assertEqual(msg.velocity, 0x66) + self.assertEqual(channel, c) + + for read in range(100): + (msg, channel) = m.read_in_port() + if msg is not None: + break + self.assertIsInstance(msg, ControlChange) + self.assertEqual(msg.control, 0x01) + self.assertEqual(msg.value, 0x08) + self.assertEqual(channel, c) + + for read in range(100): + (msg, channel) = m.read_in_port() + if msg is not None: + break + self.assertIsInstance(msg, NoteOn) + self.assertEqual(msg.note, 0x41) + self.assertEqual(msg.velocity, 0x74) + self.assertEqual(channel, c) + + for read in range(100): + (msg, channel) = m.read_in_port() + if msg is not None: + break + self.assertIsInstance(msg, PitchBendChange) + self.assertEqual(msg.pitch_bend, 8195) + self.assertEqual(channel, c) + + for read in range(100): + (msg, channel) = m.read_in_port() + self.assertIsNone(msg) + self.assertIsNone(channel) + # See https://github.com/adafruit/Adafruit_CircuitPython_MIDI/issues/8 def test_running_status_when_implemented(self): @@ -349,10 +414,6 @@ def test_send_basic_sequences(self): "The implementation writes in one go, single 9 byte write expected") next += 1 -class Test_MIDI_send_receive_loop(unittest.TestCase): - def test_do_something_that_collects_sent_data_then_parses_it(self): - self.assertEqual(TODO, TODO) - if __name__ == '__main__': unittest.main(verbosity=verbose) From a5e0ceda54d4ee325b6c60a62f1f82015cf811ce Mon Sep 17 00:00:00 2001 From: Kevin J Walters Date: Sat, 30 Mar 2019 13:13:26 +0000 Subject: [PATCH 43/92] Renaming MIDI.read_in_port() to receive to match the send method. #3 --- adafruit_midi/__init__.py | 4 ++- examples/midi_intest1.py | 2 +- tests/MIDIMessage_unittests.py | 2 +- tests/MIDI_unittests.py | 46 +++++++++++++++++----------------- 4 files changed, 28 insertions(+), 26 deletions(-) diff --git a/adafruit_midi/__init__.py b/adafruit_midi/__init__.py index 6fd1fb5..953b94b 100644 --- a/adafruit_midi/__init__.py +++ b/adafruit_midi/__init__.py @@ -105,7 +105,7 @@ def out_channel(self, channel): self._out_channel = channel ### TODO - consider naming here and channel selection and omni mode - def read_in_port(self): + def receive(self): """Read messages from MIDI port, store them in internal read buffer, then parse that data and return the first MIDI message (event). @@ -139,6 +139,8 @@ def send(self, msg, channel=None): """Sends a MIDI message. :param MIDIMessage msg: The midi message. + TODO - finish this and work out how to do types that differ, e.g. msg vs [msg] + - do i want a return on this? """ if channel is None: diff --git a/examples/midi_intest1.py b/examples/midi_intest1.py index 4277865..9a829cb 100644 --- a/examples/midi_intest1.py +++ b/examples/midi_intest1.py @@ -17,7 +17,7 @@ while True: for pause in pauses: - msg = midi.read_in_port() + msg = midi.receive() if msg is not None: print(time.monotonic(), msg) if pause: diff --git a/tests/MIDIMessage_unittests.py b/tests/MIDIMessage_unittests.py index b47f134..e9fc989 100644 --- a/tests/MIDIMessage_unittests.py +++ b/tests/MIDIMessage_unittests.py @@ -25,7 +25,7 @@ import os -verbose = int(os.getenv('TESTVERBOSE',2)) +verbose = int(os.getenv('TESTVERBOSE', 2)) # adafruit_midi has an import usb_midi import sys diff --git a/tests/MIDI_unittests.py b/tests/MIDI_unittests.py index 9334629..8332441 100644 --- a/tests/MIDI_unittests.py +++ b/tests/MIDI_unittests.py @@ -127,7 +127,7 @@ def test_captured_data_one_byte_reads(self): m = MIDI_mocked_receive(c, raw_data, [1] * len(raw_data)) for read in range(100): - (msg, channel) = m.read_in_port() + (msg, channel) = m.receive() if msg is not None: break self.assertIsInstance(msg, NoteOn) @@ -138,7 +138,7 @@ def test_captured_data_one_byte_reads(self): # for loops currently absorb any Nones but could # be set to read precisely the expected number... for read in range(100): - (msg, channel) = m.read_in_port() + (msg, channel) = m.receive() if msg is not None: break self.assertIsInstance(msg, ChannelPressure) @@ -146,7 +146,7 @@ def test_captured_data_one_byte_reads(self): self.assertEqual(channel, c) for read in range(100): - (msg, channel) = m.read_in_port() + (msg, channel) = m.receive() if msg is not None: break self.assertIsInstance(msg, NoteOn) @@ -155,7 +155,7 @@ def test_captured_data_one_byte_reads(self): self.assertEqual(channel, c) for read in range(100): - (msg, channel) = m.read_in_port() + (msg, channel) = m.receive() if msg is not None: break self.assertIsInstance(msg, ControlChange) @@ -164,7 +164,7 @@ def test_captured_data_one_byte_reads(self): self.assertEqual(channel, c) for read in range(100): - (msg, channel) = m.read_in_port() + (msg, channel) = m.receive() if msg is not None: break self.assertIsInstance(msg, NoteOn) @@ -173,7 +173,7 @@ def test_captured_data_one_byte_reads(self): self.assertEqual(channel, c) for read in range(100): - (msg, channel) = m.read_in_port() + (msg, channel) = m.receive() if msg is not None: break self.assertIsInstance(msg, PitchBendChange) @@ -181,7 +181,7 @@ def test_captured_data_one_byte_reads(self): self.assertEqual(channel, c) for read in range(100): - (msg, channel) = m.read_in_port() + (msg, channel) = m.receive() self.assertIsNone(msg) self.assertIsNone(channel) @@ -207,13 +207,13 @@ def test_somegood_somemissing_databytes(self): + NoteOn("D5", 0x7f).as_bytes(channel=c)) m = MIDI_mocked_receive(c, raw_data, [3 + 3 + 2 + 3 + 3]) - (msg1, channel1) = m.read_in_port() + (msg1, channel1) = m.receive() self.assertIsInstance(msg1, NoteOn) self.assertEqual(msg1.note, 72) self.assertEqual(msg1.velocity, 0x7f) self.assertEqual(channel1, c) - (msg2, channel2) = m.read_in_port() + (msg2, channel2) = m.receive() self.assertIsInstance(msg2, PitchBendChange) self.assertEqual(msg2.pitch_bend, 8306) self.assertEqual(channel2, c) @@ -221,23 +221,23 @@ def test_somegood_somemissing_databytes(self): # The current implementation will read status bytes for data # In most cases it would be a faster recovery with fewer messages # lost if status byte wasn't consumed and parsing restart from that - (msg3, channel3) = m.read_in_port() + (msg3, channel3) = m.receive() self.assertIsInstance(msg3, adafruit_midi.midi_message.MIDIBadEvent) self.assertEqual(msg3.data, bytearray([0x6d, 0xe8])) self.assertEqual(channel3, c) - #(msg4, channel4) = m.read_in_port() + #(msg4, channel4) = m.receive() #self.assertIsInstance(msg4, PitchBendChange) #self.assertEqual(msg4.pitch_bend, 72) #self.assertEqual(channel4, c) - (msg5, channel5) = m.read_in_port() + (msg5, channel5) = m.receive() self.assertIsInstance(msg5, NoteOn) self.assertEqual(msg5.note, 74) self.assertEqual(msg5.velocity, 0x7f) self.assertEqual(channel5, c) - (msg6, channel6) = m.read_in_port() + (msg6, channel6) = m.receive() self.assertIsNone(msg6) self.assertIsNone(channel6) @@ -248,25 +248,25 @@ def test_smallsysex_between_notes(self): SystemExclusive([0x1f], [1, 2, 3, 4, 5, 6, 7, 8]), NoteOff(60, 0x28)]) - (msg1, channel1) = m.read_in_port() + (msg1, channel1) = m.receive() self.assertIsInstance(msg1, NoteOn) self.assertEqual(msg1.note, 60) self.assertEqual(msg1.velocity, 0x7f) self.assertEqual(channel1, 3) - (msg2, channel2) = m.read_in_port() + (msg2, channel2) = m.receive() self.assertIsInstance(msg2, SystemExclusive) self.assertEqual(msg2.manufacturer_id, bytearray([0x1f])) self.assertEqual(msg2.data, bytearray([1, 2, 3, 4, 5, 6, 7, 8])) self.assertEqual(channel2, None) # SysEx does not have a channel - (msg3, channel3) = m.read_in_port() + (msg3, channel3) = m.receive() self.assertIsInstance(msg3, NoteOff) self.assertEqual(msg3.note, 60) self.assertEqual(msg3.velocity, 0x28) self.assertEqual(channel3, 3) - (msg4, channel4) = m.read_in_port() + (msg4, channel4) = m.receive() self.assertIsNone(msg4) self.assertIsNone(channel4) @@ -283,7 +283,7 @@ def test_larger_than_buffer_sysex(self): self.assertTrue(monster_data_len > buffer_len, "checking our SysEx truly is a monster") - (msg1, channel1) = m.read_in_port() + (msg1, channel1) = m.receive() self.assertIsInstance(msg1, NoteOn) self.assertEqual(msg1.note, 72) self.assertEqual(msg1.velocity, 0x7f) @@ -291,29 +291,29 @@ def test_larger_than_buffer_sysex(self): # (Ab)using python's rounding down for negative division for n in range(-(-(1 + 1 + monster_data_len + 1) // buffer_len) - 1): - (msg2, channel2) = m.read_in_port() + (msg2, channel2) = m.receive() self.assertIsNone(msg2) self.assertIsNone(channel2) # The current implementation will read SysEx end status byte # and report it as an unknown - (msg3, channel3) = m.read_in_port() + (msg3, channel3) = m.receive() self.assertIsInstance(msg3, adafruit_midi.midi_message.MIDIUnknownEvent) self.assertEqual(msg3.status, 0xf7) self.assertIsNone(channel3) - #(msg4, channel4) = m.read_in_port() + #(msg4, channel4) = m.receive() #self.assertIsInstance(msg4, PitchBendChange) #self.assertEqual(msg4.pitch_bend, 72) #self.assertEqual(channel4, c) - (msg5, channel5) = m.read_in_port() + (msg5, channel5) = m.receive() self.assertIsInstance(msg5, NoteOn) self.assertEqual(msg5.note, 74) self.assertEqual(msg5.velocity, 0x7f) self.assertEqual(channel5, c) - (msg6, channel6) = m.read_in_port() + (msg6, channel6) = m.receive() self.assertIsNone(msg6) self.assertIsNone(channel6) From 6b1509e03b76713d44f1afc8bce18b6ed7f7bfb2 Mon Sep 17 00:00:00 2001 From: Kevin J Walters Date: Sat, 30 Mar 2019 14:23:17 +0000 Subject: [PATCH 44/92] Adding test_termination_with_random_data() to ensure that a large random stream of bytes can be parsed in some way, a fuzzing approach to look for non termination. Scraping away some comments with old examples of corrupt MIDI data streams. #3 --- tests/MIDIMessage_unittests.py | 15 -------------- tests/MIDI_unittests.py | 37 ++++++++++++++++------------------ 2 files changed, 17 insertions(+), 35 deletions(-) diff --git a/tests/MIDIMessage_unittests.py b/tests/MIDIMessage_unittests.py index e9fc989..c9509a9 100644 --- a/tests/MIDIMessage_unittests.py +++ b/tests/MIDIMessage_unittests.py @@ -46,21 +46,6 @@ from adafruit_midi.system_exclusive import SystemExclusive from adafruit_midi.timing_clock import TimingClock -### To incorporate into tests -# This is using running status in a rather sporadic manner -# Acutally this now looks more like losing bytes due to being -# overwhelmed by "big" bursts of data -# -# Receiving: ['0xe0', '0x67', '0x40'] -# Receiving: ['0xe0', '0x72', '0x40'] -# Receiving: ['0x6d', '0x40', '0xe0'] -# Receiving: ['0x5', '0x41', '0xe0'] -# Receiving: ['0x17', '0x41', '0xe0'] -# Receiving: ['0x35', '0x41', '0xe0'] -# Receiving: ['0x40', '0x41', '0xe0'] - -### TODO - re work these when running status is implemented -### TODO - consider fuzzing this to check it always terminates class Test_MIDIMessage_from_message_byte_tests(unittest.TestCase): def test_NoteOn_basic(self): diff --git a/tests/MIDI_unittests.py b/tests/MIDI_unittests.py index 8332441..ca73266 100644 --- a/tests/MIDI_unittests.py +++ b/tests/MIDI_unittests.py @@ -23,6 +23,7 @@ import unittest from unittest.mock import Mock, MagicMock, call +import random import os verbose = int(os.getenv('TESTVERBOSE',2)) @@ -43,28 +44,8 @@ from adafruit_midi.system_exclusive import SystemExclusive from adafruit_midi.timing_clock import TimingClock - import adafruit_midi -# Need to test this with a stream of data -# including example below -# small sysex -# too large sysex - -### To incorporate into tests -# This is using running status in a rather sporadic manner -# Acutally this now looks more like losing bytes due to being -# overwhelmed by "big" bursts of data -# -# Receiving: ['0xe0', '0x67', '0x40'] -# Receiving: ['0xe0', '0x72', '0x40'] -# Receiving: ['0x6d', '0x40', '0xe0'] -# Receiving: ['0x5', '0x41', '0xe0'] -# Receiving: ['0x17', '0x41', '0xe0'] -# Receiving: ['0x35', '0x41', '0xe0'] -# Receiving: ['0x40', '0x41', '0xe0'] - -### TODO - re work these when running status is implemented # For loopback/echo tests def MIDI_mocked_both_loopback(in_c, out_c): @@ -414,6 +395,22 @@ def test_send_basic_sequences(self): "The implementation writes in one go, single 9 byte write expected") next += 1 + def test_termination_with_random_data(self): + """Test with a random stream of bytes to ensure that the parsing code + termates and returns, i.e. does not go into any infinite loops. + """ + c = 0 + random.seed(303808) + raw_data = bytearray([random.randint(0, 255) for i in range(50000)]) + m = MIDI_mocked_receive(c, raw_data, [len(raw_data)]) + + noinfiniteloops = False + for read in range(len(raw_data)): + (msg, channel) = m.receive() # not interested in return values + + noinfiniteloops = True # interested in getting to here + self.assertTrue(noinfiniteloops) + if __name__ == '__main__': unittest.main(verbosity=verbose) From b8c839afdc90f8cb775adeefd62d64b0a915edfb Mon Sep 17 00:00:00 2001 From: Kevin J Walters Date: Sat, 30 Mar 2019 14:34:40 +0000 Subject: [PATCH 45/92] Finished checks in test_SystemExclusive_NoteOn(). #3 --- tests/MIDIMessage_unittests.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/tests/MIDIMessage_unittests.py b/tests/MIDIMessage_unittests.py index c9509a9..ded74cf 100644 --- a/tests/MIDIMessage_unittests.py +++ b/tests/MIDIMessage_unittests.py @@ -228,10 +228,10 @@ def test_NoteOn_constructor_int(self): self.assertEqual(object2.velocity, 0x00) def test_SystemExclusive_NoteOn(self): - data = bytes([0xf0, 0x42, 0x01, 0x02, 0x03, 0x04, 0xf7, 0x90, 0x30, 0x32]) - ichannel = 0 + data = bytes([0xf0, 0x42, 0x01, 0x02, 0x03, 0x04, 0xf7, 0x90 | 14, 0x30, 0x60]) + ichannel = 14 - (msg, startidx, msgendidxplusone, skipped, channel) = adafruit_midi.MIDIMessage.from_message_bytes(data, ichannel) + (msg, startidx, msgendidxplusone, skipped, channel) = adafruit_midi.MIDIMessage.from_message_bytes(data, ichannel) self.assertIsInstance(msg, SystemExclusive) self.assertEqual(msg.manufacturer_id, bytes([0x42])) # Korg @@ -241,7 +241,16 @@ def test_SystemExclusive_NoteOn(self): self.assertEqual(skipped, 0, "If SystemExclusive class is imported then this must be 0") self.assertIsNone(channel) - ### TODO - call MIDIMessage.from_message_bytes for second part of buffer + + (msg, startidx, msgendidxplusone, skipped, channel) = adafruit_midi.MIDIMessage.from_message_bytes(data[msgendidxplusone:], ichannel) + + self.assertIsInstance(msg, NoteOn) + self.assertEqual(msg.note, 48) + self.assertEqual(msg.velocity, 0x60) + self.assertEqual(startidx, 0) + self.assertEqual(msgendidxplusone, 3) + self.assertEqual(skipped, 0) + self.assertEqual(channel, 14) def test_SystemExclusive_NoteOn_premalterminatedsysex(self): data = bytes([0xf0, 0x42, 0x01, 0x02, 0x03, 0x04, 0xf0, 0x90, 0x30, 0x32]) From 767451a068ad55a67c3c9640afd7108cb00c7451 Mon Sep 17 00:00:00 2001 From: Kevin J Walters Date: Sat, 30 Mar 2019 15:21:12 +0000 Subject: [PATCH 46/92] Documentation and TODO tidying. #3 --- adafruit_midi/midi_message.py | 27 ++++++++++++--------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/adafruit_midi/midi_message.py b/adafruit_midi/midi_message.py index 45cb888..ff1ffd5 100644 --- a/adafruit_midi/midi_message.py +++ b/adafruit_midi/midi_message.py @@ -66,13 +66,12 @@ def channel_filter(channel, channel_spec): else: raise ValueError("Incorrect type for channel_spec") -# TODO - proper parameter typing and look up how this is done when different types are accepted + def note_parser(note): - """ - If note is a string then it will be parsed and converted to a MIDI note (key) number, e.g. + """If note is a string then it will be parsed and converted to a MIDI note (key) number, e.g. "C4" will return 60, "C#4" will return 61. If note is not a string it will simply be returned. - Applies a range check to both string and integer inputs. + :param note: Either 0-127 int or a str representing the note, e.g. "C#4" """ midi_note = note if isinstance(note, str): @@ -93,16 +92,17 @@ def note_parser(note): return midi_note -# TODO TBD: can relocate this class later to a separate file if recommended + class MIDIMessage: """ A MIDI message: - - Status - extracted from Status byte with channel replaced by 0s - (high bit always set) - - Channel - extracted from Status where present (0-15) - - 0 or more Data Byte(s) - high bit always not set for data - - _LENGTH is the fixed message length including status or -1 for variable length - - _ENDSTATUS is the EOM status byte if relevant + - _STATUS - extracted from Status byte with channel replaced by 0s + (high bit always set). + - _STATUSMASK - mask used to compared a status byte with _STATUS value + - _LENGTH - length for a fixed size message including status + or -1 for variable length. + - _CHANNELMASK - mask use to apply a (wire protocol) channel number. + - _ENDSTATUS - the EOM status byte, only set for variable length. This is an abstract class. """ _STATUS = None @@ -129,8 +129,6 @@ def register_message_type(cls): MIDIMessage._statusandmask_to_class.insert(insert_idx, ((cls._STATUS, cls._STATUSMASK), cls)) - # TODO - this needs a lot of test cases to prove it actually works - # TODO - finish SysEx implementation and find something that sends one @classmethod def from_message_bytes(cls, midibytes, channel_in): """Create an appropriate object of the correct class for the @@ -161,8 +159,6 @@ def from_message_bytes(cls, midibytes, channel_in): # Either no message or a partial one if msgstartidx > endidx: - ### TODO review exactly when buffer should be discarded - ### must not discard the first half of a message return (None, startidx, endidx + 1, skipped, None) status = midibytes[msgstartidx] @@ -255,6 +251,7 @@ class MIDIUnknownEvent(MIDIMessage): def __init__(self, status): self.status = status + class MIDIBadEvent(MIDIMessage): _LENGTH = -1 From 39e72cb9c3a0f8f26f1c21c935b366d1d9c72f17 Mon Sep 17 00:00:00 2001 From: Kevin J Walters Date: Sat, 30 Mar 2019 15:41:13 +0000 Subject: [PATCH 47/92] Fixing a bug unrelated to this feature, _generic_3() test against channel was incorrect and was inconsistent on applying range checks/limits. #3 --- adafruit_midi/__init__.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/adafruit_midi/__init__.py b/adafruit_midi/__init__.py index 953b94b..b72b264 100644 --- a/adafruit_midi/__init__.py +++ b/adafruit_midi/__init__.py @@ -196,10 +196,9 @@ def _generic_3(self, cmd, arg1, arg2, channel=None): raise RuntimeError("Argument 1 value %d invalid" % arg1) if not 0 <= arg2 <= 0x7F: raise RuntimeError("Argument 2 value %d invalid" % arg2) - ### TODO - change this to use is operator and range check or mask it - if not channel: + if channel is None: channel = self._out_channel - self._outbuf[0] = (cmd & 0xF0) | channel + self._outbuf[0] = (cmd & 0xF0) | (channel & 0x0f) self._outbuf[1] = arg1 self._outbuf[2] = arg2 self._send(self._outbuf, 3) From 6fc5f286c5884b9fcefe5f7c6746ab4cc461cec2 Mon Sep 17 00:00:00 2001 From: Kevin J Walters Date: Sat, 30 Mar 2019 15:47:12 +0000 Subject: [PATCH 48/92] Completing docs for in_channel() and correcting default (from construction). #3 --- adafruit_midi/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/adafruit_midi/__init__.py b/adafruit_midi/__init__.py index b72b264..ec6a17b 100644 --- a/adafruit_midi/__init__.py +++ b/adafruit_midi/__init__.py @@ -78,7 +78,10 @@ def __init__(self, midi_in=usb_midi.ports[0], midi_out=usb_midi.ports[1], *, in_ @property def in_channel(self): """The incoming MIDI channel. Must be 0-15. Correlates to MIDI channels 1-16, e.g. - ``in_channel(3)`` will listen on MIDI channel 4. Default is 0.""" + ``in_channel(3)`` will listen on MIDI channel 4. + Can also listen on multiple channels, e.g. ``in_channel((0,1,2))`` + will listen on MIDI channels 1-3 or ``in_channel("ALL")`` for every channel. + Default is None.""" return self._in_channel @in_channel.setter From 44b77e02643654452be9211bdb5ea9d1b0d1560f Mon Sep 17 00:00:00 2001 From: Kevin J Walters Date: Sat, 30 Mar 2019 15:54:52 +0000 Subject: [PATCH 49/92] Completing docs for MIDI.send(). #3 --- adafruit_midi/__init__.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/adafruit_midi/__init__.py b/adafruit_midi/__init__.py index ec6a17b..d5aa0c2 100644 --- a/adafruit_midi/__init__.py +++ b/adafruit_midi/__init__.py @@ -50,10 +50,6 @@ __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_MIDI.git" -# TODO - implement running status -# some good tips at end of http://midi.teragonaudio.com/tech/midispec/run.htm -# only applies to voice category - class MIDI: """MIDI helper class.""" @@ -62,8 +58,8 @@ class MIDI: PITCH_BEND = 0xE0 CONTROL_CHANGE = 0xB0 - def __init__(self, midi_in=usb_midi.ports[0], midi_out=usb_midi.ports[1], *, in_channel=None, - out_channel=0, debug=False, in_buf_size=30): + def __init__(self, midi_in=usb_midi.ports[0], midi_out=usb_midi.ports[1], *, + in_channel=None, out_channel=0, debug=False, in_buf_size=30): self._midi_in = midi_in self._midi_out = midi_out self.in_channel = in_channel @@ -141,9 +137,8 @@ def receive(self): def send(self, msg, channel=None): """Sends a MIDI message. - :param MIDIMessage msg: The midi message. - TODO - finish this and work out how to do types that differ, e.g. msg vs [msg] - - do i want a return on this? + :param msg: Either a MIDIMessage object or a sequence (list) of MIDIMessage objects. + :param int channel: Channel number, if not set the ``out_channel`` will be used. """ if channel is None: From f17e89a4a2284af99c83d03f5b13767fab1c9e94 Mon Sep 17 00:00:00 2001 From: Kevin J Walters Date: Sat, 30 Mar 2019 16:13:06 +0000 Subject: [PATCH 50/92] Adding another test for 6 bytes of valid but unknown to module MIDI messages before a NoteOn. #3 --- adafruit_midi/__init__.py | 5 +---- tests/MIDI_unittests.py | 21 ++++++++++++++++++++- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/adafruit_midi/__init__.py b/adafruit_midi/__init__.py index d5aa0c2..dcd2923 100644 --- a/adafruit_midi/__init__.py +++ b/adafruit_midi/__init__.py @@ -103,7 +103,6 @@ def out_channel(self, channel): raise RuntimeError("Invalid output channel") self._out_channel = channel - ### TODO - consider naming here and channel selection and omni mode def receive(self): """Read messages from MIDI port, store them in internal read buffer, then parse that data and return the first MIDI message (event). @@ -120,9 +119,7 @@ def receive(self): print("Receiving: ", [hex(i) for i in bytes_in]) self._in_buf.extend(bytes_in) del bytes_in - - ### TODO need to ensure code skips past unknown data/messages in buffer - ### aftertouch from Axiom 25 causes 6 in the buffer!! + (msg, start, endplusone, skipped, channel) = MIDIMessage.from_message_bytes(self._in_buf, self._in_channel) if endplusone != 0: # This is not particularly efficient as it's copying most of bytearray diff --git a/tests/MIDI_unittests.py b/tests/MIDI_unittests.py index ca73266..8523a7c 100644 --- a/tests/MIDI_unittests.py +++ b/tests/MIDI_unittests.py @@ -165,7 +165,26 @@ def test_captured_data_one_byte_reads(self): (msg, channel) = m.receive() self.assertIsNone(msg) self.assertIsNone(channel) - + + def test_unknown_before_NoteOn(self): + c = 0 + # From an M-Audio AXIOM controller + raw_data = (bytearray([0b11110011, 0x10] # Song Select (not yet implemented) + + [ 0b11110011, 0x20] + + [ 0b11110100 ] + + [ 0b11110101 ]) + + NoteOn("C5", 0x7f).as_bytes(channel=c)) + m = MIDI_mocked_receive(c, raw_data, [2, 2, 1, 1, 3]) + + for read in range(4): + (msg, channel) = m.receive() + self.assertIsInstance(msg, adafruit_midi.midi_message.MIDIUnknownEvent) + + (msg, channel) = m.receive() + self.assertIsInstance(msg, NoteOn) + self.assertEqual(msg.note, 0x48) + self.assertEqual(msg.velocity, 0x7f) + self.assertEqual(channel, c) # See https://github.com/adafruit/Adafruit_CircuitPython_MIDI/issues/8 def test_running_status_when_implemented(self): From 50f0fcb01b5659ce7a17a5a52e0e98f8106eb2c6 Mon Sep 17 00:00:00 2001 From: Kevin J Walters Date: Sat, 30 Mar 2019 17:23:16 +0000 Subject: [PATCH 51/92] Adding an example of input from multiple channels, a little bit of processing and output to a channel. #3 --- examples/midi_inoutdemo.py | 59 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 examples/midi_inoutdemo.py diff --git a/examples/midi_inoutdemo.py b/examples/midi_inoutdemo.py new file mode 100644 index 0000000..a69db42 --- /dev/null +++ b/examples/midi_inoutdemo.py @@ -0,0 +1,59 @@ +# midi_inoutdemo - demonstrates receiving and sending MIDI events + +import time +import random + +import adafruit_midi + +# TimingClock is worth importing first if present as it +# will make parsing more efficient for this high frequency event +# Only importing what is used will save a little bit of memory +from adafruit_midi.timing_clock import TimingClock +from adafruit_midi.channel_pressure import ChannelPressure +from adafruit_midi.control_change import ControlChange +from adafruit_midi.note_off import NoteOff +from adafruit_midi.note_on import NoteOn +from adafruit_midi.pitch_bend_change import PitchBendChange +from adafruit_midi.polyphonic_key_pressure import PolyphonicKeyPressure +from adafruit_midi.program_change import ProgramChange +from adafruit_midi.start import Start +from adafruit_midi.stop import Stop +from adafruit_midi.system_exclusive import SystemExclusive + +from adafruit_midi.midi_message import MIDIUnknownEvent + +midi = adafruit_midi.MIDI(in_channel=(1,2,3), out_channel=0) + +print("Midi Demo in and out") + +# Convert channel numbers at the presentation layer to the ones musicians use +print("Default output channel:", midi.out_channel + 1) +print("Listening on input channels:", tuple([c + 1 for c in midi.in_channel])) + +major_chord = [0, 4, 7] +while True: + while True: + (msg_in, channel_in) = midi.receive() # non-blocking read + # For a Note On or Note Off play a major chord + # For any other known event just forward it + if isinstance(msg_in, NoteOn) and msg_in.velocity != 0: + print("Playing major chord with root", msg_in.note, + "from channel", channel_in + 1) + for offset in major_chord: + new_note = msg_in.note + offset + if 0 <= new_note <= 127: + midi.send(NoteOn(new_note, msg_in.velocity)) + + elif (isinstance(msg_in, NoteOff) or + isinstance(msg_in, NoteOn) and msg_in.velocity == 0): + for offset in major_chord: + new_note = msg_in.note + offset + if 0 <= new_note <= 127: + midi.send(NoteOff(new_note, 0x00)) + + elif isinstance(msg_in, MIDIUnknownEvent): + # Message are only known if they are imported + print("Unknown MIDI event status ", msg_in.status) + + elif msg_in is not None: + midi.send(msg_in) From caac32dd57c65db36c56aa74e6b6350962af028e Mon Sep 17 00:00:00 2001 From: Kevin J Walters Date: Sat, 30 Mar 2019 21:34:49 +0000 Subject: [PATCH 52/92] Renaming test files to match what pytest expects. Cutting back on pylint checks of those test files. #3 --- .travis.yml | 2 +- .../{MIDIMessage_unittests.py => test_MIDIMessage_unittests.py} | 0 tests/{MIDI_unittests.py => test_MIDI_unittests.py} | 0 3 files changed, 1 insertion(+), 1 deletion(-) rename tests/{MIDIMessage_unittests.py => test_MIDIMessage_unittests.py} (100%) rename tests/{MIDI_unittests.py => test_MIDI_unittests.py} (100%) diff --git a/.travis.yml b/.travis.yml index 720032c..e8069ad 100644 --- a/.travis.yml +++ b/.travis.yml @@ -46,7 +46,7 @@ install: script: - ([[ -d "tests" ]] && py.test) - pylint adafruit_midi/*.py - - ([[ -d "tests" ]] && pylint --disable=missing-docstring,invalid-name,bad-whitespace tests/*.py) + - ([[ -d "tests" ]] && pylint --disable=missing-docstring,invalid-name,bad-whitespace,trailing-whitespace,line-too-long,wrong-import-position tests/*.py) - ([[ ! -d "examples" ]] || pylint --disable=missing-docstring,invalid-name,bad-whitespace examples/*.py) - circuitpython-build-bundles --filename_prefix adafruit-circuitpython-midi --library_location . - cd docs && sphinx-build -E -W -b html . _build/html && cd .. diff --git a/tests/MIDIMessage_unittests.py b/tests/test_MIDIMessage_unittests.py similarity index 100% rename from tests/MIDIMessage_unittests.py rename to tests/test_MIDIMessage_unittests.py diff --git a/tests/MIDI_unittests.py b/tests/test_MIDI_unittests.py similarity index 100% rename from tests/MIDI_unittests.py rename to tests/test_MIDI_unittests.py From 142ab8652fe8f008453bcd09328252af897e1139 Mon Sep 17 00:00:00 2001 From: Kevin J Walters Date: Sat, 30 Mar 2019 22:56:41 +0000 Subject: [PATCH 53/92] Judicious sprinkle of pylint disables in appropriate places in tests to keep pylint happy and useful. Disabling a few globally across tests via command line. Adding python path modification to allow tests to find adafruit_midi. #3 --- .travis.yml | 2 +- tests/test_MIDIMessage_unittests.py | 40 ++++++----- tests/test_MIDI_unittests.py | 108 +++++++++++++++------------- 3 files changed, 82 insertions(+), 68 deletions(-) diff --git a/.travis.yml b/.travis.yml index e8069ad..87d4926 100644 --- a/.travis.yml +++ b/.travis.yml @@ -46,7 +46,7 @@ install: script: - ([[ -d "tests" ]] && py.test) - pylint adafruit_midi/*.py - - ([[ -d "tests" ]] && pylint --disable=missing-docstring,invalid-name,bad-whitespace,trailing-whitespace,line-too-long,wrong-import-position tests/*.py) + - ([[ -d "tests" ]] && pylint --disable=missing-docstring,invalid-name,bad-whitespace,trailing-whitespace,line-too-long,wrong-import-position,unused-import tests/*.py) - ([[ ! -d "examples" ]] || pylint --disable=missing-docstring,invalid-name,bad-whitespace examples/*.py) - circuitpython-build-bundles --filename_prefix adafruit-circuitpython-midi --library_location . - cd docs && sphinx-build -E -W -b html . _build/html && cd .. diff --git a/tests/test_MIDIMessage_unittests.py b/tests/test_MIDIMessage_unittests.py index ded74cf..35736df 100644 --- a/tests/test_MIDIMessage_unittests.py +++ b/tests/test_MIDIMessage_unittests.py @@ -25,12 +25,16 @@ import os -verbose = int(os.getenv('TESTVERBOSE', 2)) +verbose = int(os.getenv('TESTVERBOSE', '2')) # adafruit_midi has an import usb_midi import sys sys.modules['usb_midi'] = MagicMock() +# Borrowing the dhlalbert/tannewt technique from adafruit/Adafruit_CircuitPython_Motor +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) + +# Import before messages - opposite to other test file import adafruit_midi # Full monty @@ -306,29 +310,29 @@ def test_NoteOn_constructor_string(self): self.assertEqual(object3.velocity, 0) def test_NoteOn_constructor_valueerror1(self): - with self.assertRaises(ValueError): - object1 = NoteOn(60, 0x80) + with self.assertRaises(ValueError): + NoteOn(60, 0x80) # pylint is happier if return value not stored def test_NoteOn_constructor_valueerror2(self): with self.assertRaises(ValueError): - object2 = NoteOn(-1, 0x7f) + NoteOn(-1, 0x7f) def test_NoteOn_constructor_valueerror3(self): with self.assertRaises(ValueError): - object3 = NoteOn(128, 0x7f) + NoteOn(128, 0x7f) def test_NoteOn_constructor_upperrange1(self): - object = NoteOn("G9", 0x7f) - self.assertEqual(object.note, 127) - self.assertEqual(object.velocity, 0x7f) + object1 = NoteOn("G9", 0x7f) + self.assertEqual(object1.note, 127) + self.assertEqual(object1.velocity, 0x7f) def test_NoteOn_constructor_upperrange2(self): with self.assertRaises(ValueError): - object = NoteOn("G#9", 0x7f) # just above max note + NoteOn("G#9", 0x7f) # just above max note def test_NoteOn_constructor_bogusstring(self): with self.assertRaises(ValueError): - object = NoteOn("CC4", 0x7f) + NoteOn("CC4", 0x7f) class Test_MIDIMessage_NoteOff_constructor(unittest.TestCase): @@ -348,28 +352,28 @@ def test_NoteOff_constructor_string(self): def test_NoteOff_constructor_valueerror1(self): with self.assertRaises(ValueError): - object1 = NoteOff(60, 0x80) + NoteOff(60, 0x80) def test_NoteOff_constructor_valueerror2(self): with self.assertRaises(ValueError): - object2 = NoteOff(-1, 0x7f) + NoteOff(-1, 0x7f) def test_NoteOff_constructor_valueerror3(self): with self.assertRaises(ValueError): - object3 = NoteOff(128, 0x7f) + NoteOff(128, 0x7f) def test_NoteOff_constructor_upperrange1(self): - object = NoteOff("G9", 0x7f) - self.assertEqual(object.note, 127) - self.assertEqual(object.velocity, 0x7f) + object1 = NoteOff("G9", 0x7f) + self.assertEqual(object1.note, 127) + self.assertEqual(object1.velocity, 0x7f) def test_NoteOff_constructor_upperrange2(self): with self.assertRaises(ValueError): - object = NoteOff("G#9", 0x7f) # just above max note + NoteOff("G#9", 0x7f) # just above max note def test_NoteOff_constructor_bogusstring(self): with self.assertRaises(ValueError): - object = NoteOff("CC4", 0x7f) + NoteOff("CC4", 0x7f) diff --git a/tests/test_MIDI_unittests.py b/tests/test_MIDI_unittests.py index 8523a7c..1974790 100644 --- a/tests/test_MIDI_unittests.py +++ b/tests/test_MIDI_unittests.py @@ -25,12 +25,15 @@ import random import os -verbose = int(os.getenv('TESTVERBOSE',2)) +verbose = int(os.getenv('TESTVERBOSE', '2')) # adafruit_midi has an import usb_midi import sys sys.modules['usb_midi'] = MagicMock() +# Borrowing the dhlalbert/tannewt technique from adafruit/Adafruit_CircuitPython_Motor +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) + # Full monty from adafruit_midi.channel_pressure import ChannelPressure from adafruit_midi.control_change import ControlChange @@ -44,6 +47,7 @@ from adafruit_midi.system_exclusive import SystemExclusive from adafruit_midi.timing_clock import TimingClock +# Import after messages - opposite to other test file import adafruit_midi @@ -75,6 +79,7 @@ def MIDI_mocked_receive(in_c, data, read_sizes): def read(length): nonlocal usb_data, chunks, chunk_idx + # pylint: disable=no-else-return if length != 0 and chunk_idx < len(chunks): # min() to ensure we only read what's asked for and present poppedbytes = usb_data[0:min(length, chunks[chunk_idx])] @@ -96,6 +101,7 @@ def read(length): class Test_MIDI(unittest.TestCase): + # pylint: disable=too-many-branches def test_captured_data_one_byte_reads(self): c = 0 # From an M-Audio AXIOM controller @@ -107,7 +113,7 @@ def test_captured_data_one_byte_reads(self): + [ 0xe0, 0x03, 0x40 ]) m = MIDI_mocked_receive(c, raw_data, [1] * len(raw_data)) - for read in range(100): + for unused in range(100): # pylint: disable=unused-variable (msg, channel) = m.receive() if msg is not None: break @@ -118,7 +124,7 @@ def test_captured_data_one_byte_reads(self): # for loops currently absorb any Nones but could # be set to read precisely the expected number... - for read in range(100): + for unused in range(100): # pylint: disable=unused-variable (msg, channel) = m.receive() if msg is not None: break @@ -126,7 +132,7 @@ def test_captured_data_one_byte_reads(self): self.assertEqual(msg.pressure, 0x10) self.assertEqual(channel, c) - for read in range(100): + for unused in range(100): # pylint: disable=unused-variable (msg, channel) = m.receive() if msg is not None: break @@ -135,7 +141,7 @@ def test_captured_data_one_byte_reads(self): self.assertEqual(msg.velocity, 0x66) self.assertEqual(channel, c) - for read in range(100): + for unused in range(100): # pylint: disable=unused-variable (msg, channel) = m.receive() if msg is not None: break @@ -144,7 +150,7 @@ def test_captured_data_one_byte_reads(self): self.assertEqual(msg.value, 0x08) self.assertEqual(channel, c) - for read in range(100): + for unused in range(100): # pylint: disable=unused-variable (msg, channel) = m.receive() if msg is not None: break @@ -153,7 +159,7 @@ def test_captured_data_one_byte_reads(self): self.assertEqual(msg.velocity, 0x74) self.assertEqual(channel, c) - for read in range(100): + for unused in range(100): # pylint: disable=unused-variable (msg, channel) = m.receive() if msg is not None: break @@ -161,7 +167,7 @@ def test_captured_data_one_byte_reads(self): self.assertEqual(msg.pitch_bend, 8195) self.assertEqual(channel, c) - for read in range(100): + for unused in range(100): # pylint: disable=unused-variable (msg, channel) = m.receive() self.assertIsNone(msg) self.assertIsNone(channel) @@ -173,10 +179,10 @@ def test_unknown_before_NoteOn(self): + [ 0b11110011, 0x20] + [ 0b11110100 ] + [ 0b11110101 ]) - + NoteOn("C5", 0x7f).as_bytes(channel=c)) + + NoteOn("C5", 0x7f).as_bytes(channel=c)) m = MIDI_mocked_receive(c, raw_data, [2, 2, 1, 1, 3]) - for read in range(4): + for unused in range(4): # pylint: disable=unused-variable (msg, channel) = m.receive() self.assertIsInstance(msg, adafruit_midi.midi_message.MIDIUnknownEvent) @@ -191,19 +197,20 @@ def test_running_status_when_implemented(self): c = 8 raw_data = (NoteOn("C5", 0x7f,).as_bytes(channel=c) + bytearray([0xe8, 0x72, 0x40] - + [0x6d, 0x40] - + [0x05, 0x41]) + + [0x6d, 0x40] + + [0x05, 0x41]) + NoteOn("D5", 0x7f).as_bytes(channel=c)) - m = MIDI_mocked_receive(c, raw_data, [3 + 3 + 2 + 3 + 3]) + m = MIDI_mocked_receive(c, raw_data, [3 + 3 + 2 + 3 + 3]) + self.assertIsInstance(m, adafruit_midi.MIDI) # silence pylint! #self.assertEqual(TOFINISH, WHENIMPLEMENTED) def test_somegood_somemissing_databytes(self): c = 8 raw_data = (NoteOn("C5", 0x7f,).as_bytes(channel=c) + bytearray([0xe8, 0x72, 0x40] - + [0xe8, 0x6d ] # Missing last data byte - + [0xe8, 0x5, 0x41 ]) + + [0xe8, 0x6d ] # Missing last data byte + + [0xe8, 0x5, 0x41 ]) + NoteOn("D5", 0x7f).as_bytes(channel=c)) m = MIDI_mocked_receive(c, raw_data, [3 + 3 + 2 + 3 + 3]) @@ -270,6 +277,7 @@ def test_smallsysex_between_notes(self): self.assertIsNone(msg4) self.assertIsNone(channel4) + # pylint: disable=too-many-locals def test_larger_than_buffer_sysex(self): c = 0 monster_data_len = 500 @@ -278,10 +286,9 @@ def test_larger_than_buffer_sysex(self): [d & 0x7f for d in range(monster_data_len)]).as_bytes(channel=c) + NoteOn("D5", 0x7f).as_bytes(channel=c)) m = MIDI_mocked_receive(c, raw_data, [len(raw_data)]) - buffer_len = m._in_buf_size - + buffer_len = m._in_buf_size # pylint: disable=protected-access self.assertTrue(monster_data_len > buffer_len, - "checking our SysEx truly is a monster") + "checking our SysEx truly is larger than buffer") (msg1, channel1) = m.receive() self.assertIsInstance(msg1, NoteOn) @@ -290,7 +297,8 @@ def test_larger_than_buffer_sysex(self): self.assertEqual(channel1, c) # (Ab)using python's rounding down for negative division - for n in range(-(-(1 + 1 + monster_data_len + 1) // buffer_len) - 1): + # pylint: disable=unused-variable + for unused in range(-(-(1 + 1 + monster_data_len + 1) // buffer_len) - 1): (msg2, channel2) = m.receive() self.assertIsNone(msg2) self.assertIsNone(channel2) @@ -317,6 +325,8 @@ def test_larger_than_buffer_sysex(self): self.assertIsNone(msg6) self.assertIsNone(channel6) +# pylint does not like mock_calls - must be a better way to handle this? +# pylint: disable=no-member class Test_MIDI_send(unittest.TestCase): def test_send_basic_single(self): #def printit(buffer, len): @@ -327,43 +337,43 @@ def test_send_basic_single(self): m = adafruit_midi.MIDI(midi_out=mockedPortOut, out_channel=2) # Test sending some NoteOn and NoteOff to various channels - next = 0 + nextcall = 0 m.send(NoteOn(0x60, 0x7f)) - self.assertEqual(mockedPortOut.write.mock_calls[next], + self.assertEqual(mockedPortOut.write.mock_calls[nextcall], call(b'\x92\x60\x7f', 3)) - next += 1 + nextcall += 1 m.send(NoteOn(0x64, 0x3f)) - self.assertEqual(mockedPortOut.write.mock_calls[next], + self.assertEqual(mockedPortOut.write.mock_calls[nextcall], call(b'\x92\x64\x3f', 3)) - next += 1 + nextcall += 1 m.send(NoteOn(0x67, 0x1f)) - self.assertEqual(mockedPortOut.write.mock_calls[next], + self.assertEqual(mockedPortOut.write.mock_calls[nextcall], call(b'\x92\x67\x1f', 3)) - next += 1 + nextcall += 1 m.send(NoteOn(0x60, 0x00)) # Alternative to NoteOff - self.assertEqual(mockedPortOut.write.mock_calls[next], + self.assertEqual(mockedPortOut.write.mock_calls[nextcall], call(b'\x92\x60\x00', 3)) - next += 1 + nextcall += 1 m.send(NoteOff(0x64, 0x01)) - self.assertEqual(mockedPortOut.write.mock_calls[next], + self.assertEqual(mockedPortOut.write.mock_calls[nextcall], call(b'\x82\x64\x01', 3)) - next += 1 + nextcall += 1 m.send(NoteOff(0x67, 0x02)) - self.assertEqual(mockedPortOut.write.mock_calls[next], + self.assertEqual(mockedPortOut.write.mock_calls[nextcall], call(b'\x82\x67\x02', 3)) - next += 1 + nextcall += 1 # Setting channel to non default m.send(NoteOn(0x6c, 0x7f), channel=9) - self.assertEqual(mockedPortOut.write.mock_calls[next], + self.assertEqual(mockedPortOut.write.mock_calls[nextcall], call(b'\x99\x6c\x7f', 3)) - next += 1 + nextcall += 1 m.send(NoteOff(0x6c, 0x7f), channel=9) - self.assertEqual(mockedPortOut.write.mock_calls[next], + self.assertEqual(mockedPortOut.write.mock_calls[nextcall], call(b'\x89\x6c\x7f', 3)) - next += 1 + nextcall += 1 def test_send_badnotes(self): mockedPortOut = Mock() @@ -371,11 +381,11 @@ def test_send_badnotes(self): m = adafruit_midi.MIDI(midi_out=mockedPortOut, out_channel=2) # Test sending some NoteOn and NoteOff to various channels - next = 0 + nextcall = 0 m.send(NoteOn(60, 0x7f)) - self.assertEqual(mockedPortOut.write.mock_calls[next], + self.assertEqual(mockedPortOut.write.mock_calls[nextcall], call(b'\x92\x3c\x7f', 3)) - next += 1 + nextcall += 1 with self.assertRaises(ValueError): m.send(NoteOn(64, 0x80)) # Velocity > 127 - illegal value @@ -384,9 +394,9 @@ def test_send_badnotes(self): # test after exceptions to ensure sending is still ok m.send(NoteOn(72, 0x7f)) - self.assertEqual(mockedPortOut.write.mock_calls[next], + self.assertEqual(mockedPortOut.write.mock_calls[nextcall], call(b'\x92\x48\x7f', 3)) - next += 1 + nextcall += 1 def test_send_basic_sequences(self): #def printit(buffer, len): @@ -397,22 +407,22 @@ def test_send_basic_sequences(self): m = adafruit_midi.MIDI(midi_out=mockedPortOut, out_channel=2) # Test sending some NoteOn and NoteOff to various channels - next = 0 + nextcall = 0 # Test sequences with list syntax and pass a tuple too note_list = [NoteOn(0x6c, 0x51), NoteOn(0x70, 0x52), - NoteOn(0x73, 0x53)]; + NoteOn(0x73, 0x53)] note_tuple = tuple(note_list) m.send(note_list, channel=10) - self.assertEqual(mockedPortOut.write.mock_calls[next], + self.assertEqual(mockedPortOut.write.mock_calls[nextcall], call(b'\x9a\x6c\x51\x9a\x70\x52\x9a\x73\x53', 9), "The implementation writes in one go, single 9 byte write expected") - next += 1 + nextcall += 1 m.send(note_tuple, channel=11) - self.assertEqual(mockedPortOut.write.mock_calls[next], + self.assertEqual(mockedPortOut.write.mock_calls[nextcall], call(b'\x9b\x6c\x51\x9b\x70\x52\x9b\x73\x53', 9), "The implementation writes in one go, single 9 byte write expected") - next += 1 + nextcall += 1 def test_termination_with_random_data(self): """Test with a random stream of bytes to ensure that the parsing code @@ -424,8 +434,8 @@ def test_termination_with_random_data(self): m = MIDI_mocked_receive(c, raw_data, [len(raw_data)]) noinfiniteloops = False - for read in range(len(raw_data)): - (msg, channel) = m.receive() # not interested in return values + for unused in range(len(raw_data)): # pylint: disable=unused-variable + m.receive() # not interested in returned tuple noinfiniteloops = True # interested in getting to here self.assertTrue(noinfiniteloops) From 804a94969a7f3500ee574568366b6a62724c6bfe Mon Sep 17 00:00:00 2001 From: Kevin J Walters Date: Sat, 30 Mar 2019 23:12:59 +0000 Subject: [PATCH 54/92] MIDIMessage.from_message_bytes() start return value was always 0 (highlighted by pylint), removing as there appears to be no need to remove buffer data starting anywhere else. #3 --- adafruit_midi/__init__.py | 4 +-- adafruit_midi/midi_message.py | 13 ++++---- tests/test_MIDIMessage_unittests.py | 51 ++++++++++------------------- 3 files changed, 25 insertions(+), 43 deletions(-) diff --git a/adafruit_midi/__init__.py b/adafruit_midi/__init__.py index dcd2923..724f950 100644 --- a/adafruit_midi/__init__.py +++ b/adafruit_midi/__init__.py @@ -106,7 +106,7 @@ def out_channel(self, channel): def receive(self): """Read messages from MIDI port, store them in internal read buffer, then parse that data and return the first MIDI message (event). - + Returns (MIDIMessage object, channel) or (None, None) for nothing. """ ### could check _midi_in is an object OR correct object OR correct interface here? @@ -120,7 +120,7 @@ def receive(self): self._in_buf.extend(bytes_in) del bytes_in - (msg, start, endplusone, skipped, channel) = MIDIMessage.from_message_bytes(self._in_buf, self._in_channel) + (msg, endplusone, skipped, channel) = MIDIMessage.from_message_bytes(self._in_buf, self._in_channel) if endplusone != 0: # This is not particularly efficient as it's copying most of bytearray # and deleting old one diff --git a/adafruit_midi/midi_message.py b/adafruit_midi/midi_message.py index ff1ffd5..d2bc7f9 100644 --- a/adafruit_midi/midi_message.py +++ b/adafruit_midi/midi_message.py @@ -134,18 +134,17 @@ def from_message_bytes(cls, midibytes, channel_in): """Create an appropriate object of the correct class for the first message found in some MIDI bytes. - Returns (messageobject, start, endplusone, skipped, channel) + Returns (messageobject, endplusone, skipped, channel) or for no messages, partial messages or messages for other channels - (None, start, endplusone, skipped, None). + (None, endplusone, skipped, None). """ msg = None - startidx = 0 endidx = len(midibytes) - 1 skipped = 0 preamble = True - msgstartidx = startidx + msgstartidx = 0 msgendidxplusone = 0 while True: # Look for a status byte @@ -159,7 +158,7 @@ def from_message_bytes(cls, midibytes, channel_in): # Either no message or a partial one if msgstartidx > endidx: - return (None, startidx, endidx + 1, skipped, None) + return (None, endidx + 1, skipped, None) status = midibytes[msgstartidx] known_message = False @@ -227,9 +226,9 @@ def from_message_bytes(cls, midibytes, channel_in): break if msg is not None: - return (msg, startidx, msgendidxplusone, skipped, channel) + return (msg, msgendidxplusone, skipped, channel) else: - return (None, startidx, msgendidxplusone, skipped, None) + return (None, msgendidxplusone, skipped, None) # channel value present to keep interface uniform but unused def as_bytes(self, channel=None): diff --git a/tests/test_MIDIMessage_unittests.py b/tests/test_MIDIMessage_unittests.py index 35736df..493c76c 100644 --- a/tests/test_MIDIMessage_unittests.py +++ b/tests/test_MIDIMessage_unittests.py @@ -56,12 +56,11 @@ def test_NoteOn_basic(self): data = bytes([0x90, 0x30, 0x7f]) ichannel = 0 - (msg, startidx, msgendidxplusone, skipped, channel) = adafruit_midi.MIDIMessage.from_message_bytes(data, ichannel) + (msg, msgendidxplusone, skipped, channel) = adafruit_midi.MIDIMessage.from_message_bytes(data, ichannel) self.assertIsInstance(msg, NoteOn) self.assertEqual(msg.note, 0x30) self.assertEqual(msg.velocity, 0x7f) - self.assertEqual(startidx, 0) self.assertEqual(msgendidxplusone, 3) self.assertEqual(skipped, 0) self.assertEqual(channel, 0) @@ -70,11 +69,10 @@ def test_NoteOn_awaitingthirdbyte(self): data = bytes([0x90, 0x30]) ichannel = 0 - (msg, startidx, msgendidxplusone, skipped, channel) = adafruit_midi.MIDIMessage.from_message_bytes(data, ichannel) + (msg, msgendidxplusone, skipped, channel) = adafruit_midi.MIDIMessage.from_message_bytes(data, ichannel) self.assertIsNone(msg) self.assertEqual(msgendidxplusone, skipped, "skipped must be 0 as it only indicates bytes before a status byte") - self.assertEqual(startidx, 0) self.assertEqual(msgendidxplusone, 0, "msgendidxplusone must be 0 as buffer must be lest as is for more data") self.assertEqual(skipped, 0) @@ -84,12 +82,11 @@ def test_NoteOn_predatajunk(self): data = bytes([0x20, 0x64, 0x90, 0x30, 0x32]) ichannel = 0 - (msg, startidx, msgendidxplusone, skipped, channel) = adafruit_midi.MIDIMessage.from_message_bytes(data, ichannel) + (msg, msgendidxplusone, skipped, channel) = adafruit_midi.MIDIMessage.from_message_bytes(data, ichannel) self.assertIsInstance(msg, NoteOn) self.assertEqual(msg.note, 0x30) self.assertEqual(msg.velocity, 0x32) - self.assertEqual(startidx, 0) self.assertEqual(msgendidxplusone, 5, "data bytes from partial message and messages are removed" ) self.assertEqual(skipped, 2) @@ -99,25 +96,23 @@ def test_NoteOn_prepartialsysex(self): data = bytes([0x01, 0x02, 0x03, 0x04, 0xf7, 0x90, 0x30, 0x32]) ichannel = 0 - (msg, startidx, msgendidxplusone, skipped, channel) = adafruit_midi.MIDIMessage.from_message_bytes(data, ichannel) + (msg, msgendidxplusone, skipped, channel) = adafruit_midi.MIDIMessage.from_message_bytes(data, ichannel) # MIDIMessage parsing could be improved to return something that # indicates its a truncated end of SysEx self.assertIsInstance(msg, adafruit_midi.midi_message.MIDIUnknownEvent) self.assertEqual(msg.status, 0xf7) - self.assertEqual(startidx, 0) self.assertEqual(msgendidxplusone, 5, "removal of the end of the partial SysEx data and terminating status byte") self.assertEqual(skipped, 4, "skipped only counts data bytes so will be 4 here") self.assertIsNone(channel) data = data[msgendidxplusone:] - (msg, startidx, msgendidxplusone, skipped, channel) = adafruit_midi.MIDIMessage.from_message_bytes(data, ichannel) + (msg, msgendidxplusone, skipped, channel) = adafruit_midi.MIDIMessage.from_message_bytes(data, ichannel) self.assertIsInstance(msg, NoteOn, "NoteOn is expected if SystemExclusive is loaded otherwise it would be MIDIUnknownEvent") self.assertEqual(msg.note, 0x30) self.assertEqual(msg.velocity, 0x32) - self.assertEqual(startidx, 0) self.assertEqual(msgendidxplusone, 3, "NoteOn message removed") self.assertEqual(skipped, 0) self.assertEqual(channel, 0) @@ -126,12 +121,11 @@ def test_NoteOn_postNoteOn(self): data = bytes([0x90 | 0x08, 0x30, 0x7f, 0x90 | 0x08, 0x37, 0x64]) ichannel = 8 - (msg, startidx, msgendidxplusone, skipped, channel) = adafruit_midi.MIDIMessage.from_message_bytes(data, ichannel) + (msg, msgendidxplusone, skipped, channel) = adafruit_midi.MIDIMessage.from_message_bytes(data, ichannel) self.assertIsInstance(msg, NoteOn) self.assertEqual(msg.note, 0x30) self.assertEqual(msg.velocity, 0x7f) - self.assertEqual(startidx, 0) self.assertEqual(msgendidxplusone, 3) self.assertEqual(skipped, 0) self.assertEqual(channel, 8) @@ -140,12 +134,11 @@ def test_NoteOn_postpartialNoteOn(self): data = bytes([0x90, 0x30, 0x7f, 0x90, 0x37]) ichannel = 0 - (msg, startidx, msgendidxplusone, skipped, channel) = adafruit_midi.MIDIMessage.from_message_bytes(data, ichannel) + (msg, msgendidxplusone, skipped, channel) = adafruit_midi.MIDIMessage.from_message_bytes(data, ichannel) self.assertIsInstance(msg, NoteOn) self.assertEqual(msg.note, 0x30) self.assertEqual(msg.velocity, 0x7f) - self.assertEqual(startidx, 0) self.assertEqual(msgendidxplusone, 3, "Only first message is removed") self.assertEqual(skipped, 0) @@ -155,12 +148,11 @@ def test_NoteOn_preotherchannel(self): data = bytes([0x90 | 0x05, 0x30, 0x7f, 0x90 | 0x03, 0x37, 0x64]) ichannel = 3 - (msg, startidx, msgendidxplusone, skipped, channel) = adafruit_midi.MIDIMessage.from_message_bytes(data, ichannel) + (msg, msgendidxplusone, skipped, channel) = adafruit_midi.MIDIMessage.from_message_bytes(data, ichannel) self.assertIsInstance(msg, NoteOn) self.assertEqual(msg.note, 0x37) self.assertEqual(msg.velocity, 0x64) - self.assertEqual(startidx, 0) self.assertEqual(msgendidxplusone, 6, "Both messages are removed from buffer") self.assertEqual(skipped, 0) @@ -170,12 +162,11 @@ def test_NoteOn_preotherchannelplusintermediatejunk(self): data = bytes([0x90 | 0x05, 0x30, 0x7f, 0x00, 0x00, 0x90 | 0x03, 0x37, 0x64]) ichannel = 3 - (msg, startidx, msgendidxplusone, skipped, channel) = adafruit_midi.MIDIMessage.from_message_bytes(data, ichannel) + (msg, msgendidxplusone, skipped, channel) = adafruit_midi.MIDIMessage.from_message_bytes(data, ichannel) self.assertIsInstance(msg, NoteOn) self.assertEqual(msg.note, 0x37) self.assertEqual(msg.velocity, 0x64) - self.assertEqual(startidx, 0) self.assertEqual(msgendidxplusone, 8, "Both messages and junk are removed from buffer") self.assertEqual(skipped, 0) @@ -185,10 +176,9 @@ def test_NoteOn_wrongchannel(self): data = bytes([0x95, 0x30, 0x7f]) ichannel = 3 - (msg, startidx, msgendidxplusone, skipped, channel) = adafruit_midi.MIDIMessage.from_message_bytes(data, ichannel) + (msg, msgendidxplusone, skipped, channel) = adafruit_midi.MIDIMessage.from_message_bytes(data, ichannel) self.assertIsNone(msg) - self.assertEqual(startidx, 0) self.assertEqual(msgendidxplusone, 3, "wrong channel message discarded") self.assertEqual(skipped, 0) @@ -198,10 +188,9 @@ def test_NoteOn_partialandpreotherchannel1(self): data = bytes([0x95, 0x30, 0x7f, 0x93]) ichannel = 3 - (msg, startidx, msgendidxplusone, skipped, channel) = adafruit_midi.MIDIMessage.from_message_bytes(data, ichannel) + (msg, msgendidxplusone, skipped, channel) = adafruit_midi.MIDIMessage.from_message_bytes(data, ichannel) self.assertIsNone(msg) - self.assertEqual(startidx, 0) self.assertEqual(msgendidxplusone, 3, "first message discarded, second partial left") self.assertEqual(skipped, 0) @@ -211,10 +200,9 @@ def test_NoteOn_partialandpreotherchannel2(self): data = bytes([0x95, 0x30, 0x7f, 0x93, 0x37]) ichannel = 3 - (msg, startidx, msgendidxplusone, skipped, channel) = adafruit_midi.MIDIMessage.from_message_bytes(data, ichannel) + (msg, msgendidxplusone, skipped, channel) = adafruit_midi.MIDIMessage.from_message_bytes(data, ichannel) self.assertIsNone(msg) - self.assertEqual(startidx, 0) self.assertEqual(msgendidxplusone, 3, "first message discarded, second partial left") self.assertEqual(skipped, 0) @@ -235,23 +223,21 @@ def test_SystemExclusive_NoteOn(self): data = bytes([0xf0, 0x42, 0x01, 0x02, 0x03, 0x04, 0xf7, 0x90 | 14, 0x30, 0x60]) ichannel = 14 - (msg, startidx, msgendidxplusone, skipped, channel) = adafruit_midi.MIDIMessage.from_message_bytes(data, ichannel) + (msg, msgendidxplusone, skipped, channel) = adafruit_midi.MIDIMessage.from_message_bytes(data, ichannel) self.assertIsInstance(msg, SystemExclusive) self.assertEqual(msg.manufacturer_id, bytes([0x42])) # Korg self.assertEqual(msg.data, bytes([0x01, 0x02, 0x03, 0x04])) - self.assertEqual(startidx, 0) self.assertEqual(msgendidxplusone, 7) self.assertEqual(skipped, 0, "If SystemExclusive class is imported then this must be 0") self.assertIsNone(channel) - (msg, startidx, msgendidxplusone, skipped, channel) = adafruit_midi.MIDIMessage.from_message_bytes(data[msgendidxplusone:], ichannel) + (msg, msgendidxplusone, skipped, channel) = adafruit_midi.MIDIMessage.from_message_bytes(data[msgendidxplusone:], ichannel) self.assertIsInstance(msg, NoteOn) self.assertEqual(msg.note, 48) self.assertEqual(msg.velocity, 0x60) - self.assertEqual(startidx, 0) self.assertEqual(msgendidxplusone, 3) self.assertEqual(skipped, 0) self.assertEqual(channel, 14) @@ -261,10 +247,9 @@ def test_SystemExclusive_NoteOn_premalterminatedsysex(self): ichannel = 0 # 0xf0 is incorrect status to mark end of this message, must be 0xf7 - (msg, startidx, msgendidxplusone, skipped, channel) = adafruit_midi.MIDIMessage.from_message_bytes(data, ichannel) + (msg, msgendidxplusone, skipped, channel) = adafruit_midi.MIDIMessage.from_message_bytes(data, ichannel) self.assertIsNone(msg) - self.assertEqual(startidx, 0) self.assertEqual(msgendidxplusone, 7) self.assertEqual(skipped, 0, "If SystemExclusive class is imported then this must be 0") @@ -274,10 +259,9 @@ def test_Unknown_SinglebyteStatus(self): data = bytes([0xfd]) ichannel = 0 - (msg, startidx, msgendidxplusone, skipped, channel) = adafruit_midi.MIDIMessage.from_message_bytes(data, ichannel) + (msg, msgendidxplusone, skipped, channel) = adafruit_midi.MIDIMessage.from_message_bytes(data, ichannel) self.assertIsInstance(msg, adafruit_midi.midi_message.MIDIUnknownEvent) - self.assertEqual(startidx, 0) self.assertEqual(msgendidxplusone, 1) self.assertEqual(skipped, 0) self.assertIsNone(channel) @@ -286,10 +270,9 @@ def test_Empty(self): data = bytes([]) ichannel = 0 - (msg, startidx, msgendidxplusone, skipped, channel) = adafruit_midi.MIDIMessage.from_message_bytes(data, ichannel) + (msg, msgendidxplusone, skipped, channel) = adafruit_midi.MIDIMessage.from_message_bytes(data, ichannel) self.assertIsNone(msg) - self.assertEqual(startidx, 0) self.assertEqual(msgendidxplusone, 0) self.assertEqual(skipped, 0) self.assertIsNone(channel) From b93dc7cac5b450378328d309a1e36d4d1ab74756 Mon Sep 17 00:00:00 2001 From: Kevin J Walters Date: Sat, 30 Mar 2019 23:27:23 +0000 Subject: [PATCH 55/92] Fixing a bug with use of ALL_CHANNELS constant. Various pylint tidying. #3 --- adafruit_midi/__init__.py | 17 ++++++++++------- adafruit_midi/midi_message.py | 3 ++- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/adafruit_midi/__init__.py b/adafruit_midi/__init__.py index 724f950..e086cf4 100644 --- a/adafruit_midi/__init__.py +++ b/adafruit_midi/__init__.py @@ -44,7 +44,7 @@ import usb_midi -from .midi_message import MIDIMessage +from .midi_message import MIDIMessage, ALL_CHANNELS __version__ = "0.0.0-auto.0" __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_MIDI.git" @@ -80,12 +80,13 @@ def in_channel(self): Default is None.""" return self._in_channel + # pylint: disable=attribute-defined-outside-init @in_channel.setter def in_channel(self, channel): if channel is None or (isinstance(channel, int) and 0 <= channel <= 15): self._in_channel = channel elif isinstance(channel, str) and channel == "ALL": - self._in_channel = MIDIMessage.ALL_CHANNELS + self._in_channel = ALL_CHANNELS elif isinstance(channel, tuple) and all(0 <= c <= 15 for c in channel): self._in_channel = channel else: @@ -97,6 +98,7 @@ def out_channel(self): ``out_channel(3)`` will send to MIDI channel 4. Default is 0.""" return self._out_channel + # pylint: disable=attribute-defined-outside-init @out_channel.setter def out_channel(self, channel): if not 0 <= channel <= 15: @@ -114,20 +116,21 @@ def receive(self): # the input port if len(self._in_buf) < self._in_buf_size: bytes_in = self._midi_in.read(self._in_buf_size - len(self._in_buf)) - if len(bytes_in) > 0: + if bytes_in: if self._debug: print("Receiving: ", [hex(i) for i in bytes_in]) self._in_buf.extend(bytes_in) del bytes_in - (msg, endplusone, skipped, channel) = MIDIMessage.from_message_bytes(self._in_buf, self._in_channel) + (msg, endplusone, + skipped, channel) = MIDIMessage.from_message_bytes(self._in_buf, self._in_channel) if endplusone != 0: # This is not particularly efficient as it's copying most of bytearray # and deleting old one self._in_buf = self._in_buf[endplusone:] self._skipped_bytes += skipped - + # msg could still be None at this point, e.g. in middle of monster SysEx return (msg, channel) @@ -146,9 +149,9 @@ def send(self, msg, channel=None): data = bytearray() for each_msg in msg: data.extend(each_msg.as_bytes(channel=channel)) - + self._send(data, len(data)) - + def note_on(self, note, vel, channel=None): """Sends a MIDI Note On message. diff --git a/adafruit_midi/midi_message.py b/adafruit_midi/midi_message.py index d2bc7f9..39657c6 100644 --- a/adafruit_midi/midi_message.py +++ b/adafruit_midi/midi_message.py @@ -45,7 +45,8 @@ __version__ = "0.0.0-auto.0" __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_MIDI.git" - +# This is a special channel value outside of wire protocol range used to +# represent all of the sixteen channels ALL_CHANNELS = -1 # From C3 From ac6b56846d7d8d1a321db55424986da509b6dc9b Mon Sep 17 00:00:00 2001 From: Kevin J Walters Date: Sat, 30 Mar 2019 23:59:41 +0000 Subject: [PATCH 56/92] More pylint tidying including documenting the two MIDIMessage classes in here. #3 --- adafruit_midi/midi_message.py | 56 +++++++++++++++++++++++------------ 1 file changed, 37 insertions(+), 19 deletions(-) diff --git a/adafruit_midi/midi_message.py b/adafruit_midi/midi_message.py index 39657c6..b97dee0 100644 --- a/adafruit_midi/midi_message.py +++ b/adafruit_midi/midi_message.py @@ -51,8 +51,9 @@ # From C3 # Semitones A B C D E F G -note_offset = [9, 11, 12, 14, 16, 17, 19] +NOTE_OFFSET = [9, 11, 12, 14, 16, 17, 19] +# pylint: disable=no-else-return def channel_filter(channel, channel_spec): """ Utility function to return True iff the given channel matches channel_spec. @@ -67,11 +68,11 @@ def channel_filter(channel, channel_spec): else: raise ValueError("Incorrect type for channel_spec") - + def note_parser(note): """If note is a string then it will be parsed and converted to a MIDI note (key) number, e.g. "C4" will return 60, "C#4" will return 61. If note is not a string it will simply be returned. - + :param note: Either 0-127 int or a str representing the note, e.g. "C#4" """ midi_note = note @@ -88,11 +89,11 @@ def note_parser(note): sharpen = -1 # int may throw exception here midi_note = (int(note[1 + abs(sharpen):]) * 12 - + note_offset[noteidx] + + NOTE_OFFSET[noteidx] + sharpen) return midi_note - + class MIDIMessage: """ @@ -111,7 +112,7 @@ class MIDIMessage: _LENGTH = None _CHANNELMASK = None _ENDSTATUS = None - + # Each element is ((status, mask), class) # order is more specific masks first _statusandmask_to_class = [] @@ -129,7 +130,7 @@ def register_message_type(cls): MIDIMessage._statusandmask_to_class.insert(insert_idx, ((cls._STATUS, cls._STATUSMASK), cls)) - + @classmethod def from_message_bytes(cls, midibytes, channel_in): """Create an appropriate object of the correct class for the @@ -144,17 +145,17 @@ def from_message_bytes(cls, midibytes, channel_in): endidx = len(midibytes) - 1 skipped = 0 preamble = True - + msgstartidx = 0 msgendidxplusone = 0 while True: # Look for a status byte # Second rule of the MIDI club is status bytes have MSB set - while msgstartidx <= endidx and not (midibytes[msgstartidx] & 0x80): + while msgstartidx <= endidx and not midibytes[msgstartidx] & 0x80: msgstartidx += 1 if preamble: skipped += 1 - + preamble = False # Either no message or a partial one @@ -164,12 +165,12 @@ def from_message_bytes(cls, midibytes, channel_in): status = midibytes[msgstartidx] known_message = False complete_message = False - channel_match_orNA = True + channel_match_orna = True channel = None # Rummage through our list looking for a status match - for sm, msgclass in MIDIMessage._statusandmask_to_class: - masked_status = status & sm[1] - if sm[0] == masked_status: + for status_mask, msgclass in MIDIMessage._statusandmask_to_class: + masked_status = status & status_mask[1] + if status_mask[0] == masked_status: known_message = True # Check there's enough left to parse a complete message # this value can be changed later for a var. length msgs @@ -177,7 +178,7 @@ def from_message_bytes(cls, midibytes, channel_in): if complete_message: if msgclass._CHANNELMASK is not None: channel = status & msgclass._CHANNELMASK - channel_match_orNA = channel_filter(channel, channel_in) + channel_match_orna = channel_filter(channel, channel_in) bad_termination = False if msgclass._LENGTH < 0: # indicator of variable length message @@ -199,11 +200,11 @@ def from_message_bytes(cls, midibytes, channel_in): else: msgendidxplusone = msgstartidx + msgclass._LENGTH - if complete_message and not bad_termination and channel_match_orNA: + if complete_message and not bad_termination and channel_match_orna: try: msg = msgclass.from_bytes(midibytes[msgstartidx+1:msgendidxplusone]) - except(ValueError, TypeError) as e: - msg = MIDIBadEvent(midibytes[msgstartidx+1:msgendidxplusone], e) + except(ValueError, TypeError) as ex: + msg = MIDIBadEvent(midibytes[msgstartidx+1:msgendidxplusone], ex) break # for @@ -211,7 +212,7 @@ def from_message_bytes(cls, midibytes, channel_in): # or we have one we do not know about if known_message: if complete_message: - if channel_match_orNA: + if channel_match_orna: break else: msgstartidx = msgendidxplusone @@ -232,11 +233,14 @@ def from_message_bytes(cls, midibytes, channel_in): return (None, msgendidxplusone, skipped, None) # channel value present to keep interface uniform but unused + # pylint: disable=unused-argument def as_bytes(self, channel=None): """A default method for constructing wire messages with no data. Returns a (mutable) bytearray with just status code in.""" return bytearray([self._STATUS]) + # databytes value present to keep interface uniform but unused + # pylint: disable=unused-argument @classmethod def from_bytes(cls, databytes): """A default method for constructing message objects with no data. @@ -246,6 +250,13 @@ def from_bytes(cls, databytes): # DO NOT try to register these messages class MIDIUnknownEvent(MIDIMessage): + """An unknown MIDI message. + + :param int status: The MIDI status number. + + This can either occur because there is no class representing the message + or because it is not imported. + """ _LENGTH = -1 def __init__(self, status): @@ -253,6 +264,13 @@ def __init__(self, status): class MIDIBadEvent(MIDIMessage): + """A bad MIDI message, one that could not be parsed/constructed. + + :param list data: The MIDI status number. + :param Exception exception: The exception used to store the repr() text representation. + + This could be due to status bytes appearing where data bytes are expected. + """ _LENGTH = -1 def __init__(self, data, exception): From fe674f2d02cf706361bcc5b42b2f071af11fa249 Mon Sep 17 00:00:00 2001 From: Kevin J Walters Date: Sun, 31 Mar 2019 00:36:46 +0000 Subject: [PATCH 57/92] pylint does not like _LENGTH due to access to the value outside of classes, renaming this and _ENDSTATUS to underscoreless version. #3 --- adafruit_midi/channel_pressure.py | 2 +- adafruit_midi/control_change.py | 2 +- adafruit_midi/midi_message.py | 20 ++++++++++---------- adafruit_midi/note_off.py | 2 +- adafruit_midi/note_on.py | 2 +- adafruit_midi/pitch_bend_change.py | 2 +- adafruit_midi/polyphonic_key_pressure.py | 2 +- adafruit_midi/program_change.py | 2 +- adafruit_midi/start.py | 2 +- adafruit_midi/stop.py | 2 +- adafruit_midi/system_exclusive.py | 8 ++++---- adafruit_midi/timing_clock.py | 2 +- 12 files changed, 24 insertions(+), 24 deletions(-) diff --git a/adafruit_midi/channel_pressure.py b/adafruit_midi/channel_pressure.py index 65aaefb..64bd47c 100644 --- a/adafruit_midi/channel_pressure.py +++ b/adafruit_midi/channel_pressure.py @@ -51,7 +51,7 @@ class ChannelPressure(MIDIMessage): _STATUS = 0xd0 _STATUSMASK = 0xf0 - _LENGTH = 2 + LENGTH = 2 _CHANNELMASK = 0x0f def __init__(self, pressure): diff --git a/adafruit_midi/control_change.py b/adafruit_midi/control_change.py index e401a16..7f68757 100644 --- a/adafruit_midi/control_change.py +++ b/adafruit_midi/control_change.py @@ -51,7 +51,7 @@ class ControlChange(MIDIMessage): _STATUS = 0xb0 _STATUSMASK = 0xf0 - _LENGTH = 3 + LENGTH = 3 _CHANNELMASK = 0x0f def __init__(self, control, value): diff --git a/adafruit_midi/midi_message.py b/adafruit_midi/midi_message.py index b97dee0..3b8ed71 100644 --- a/adafruit_midi/midi_message.py +++ b/adafruit_midi/midi_message.py @@ -101,17 +101,17 @@ class MIDIMessage: - _STATUS - extracted from Status byte with channel replaced by 0s (high bit always set). - _STATUSMASK - mask used to compared a status byte with _STATUS value - - _LENGTH - length for a fixed size message including status + - LENGTH - length for a fixed size message including status or -1 for variable length. - _CHANNELMASK - mask use to apply a (wire protocol) channel number. - - _ENDSTATUS - the EOM status byte, only set for variable length. + - ENDSTATUS - the EOM status byte, only set for variable length. This is an abstract class. """ _STATUS = None _STATUSMASK = None - _LENGTH = None + LENGTH = None _CHANNELMASK = None - _ENDSTATUS = None + ENDSTATUS = None # Each element is ((status, mask), class) # order is more specific masks first @@ -174,19 +174,19 @@ def from_message_bytes(cls, midibytes, channel_in): known_message = True # Check there's enough left to parse a complete message # this value can be changed later for a var. length msgs - complete_message = len(midibytes) - msgstartidx >= msgclass._LENGTH + complete_message = len(midibytes) - msgstartidx >= msgclass.LENGTH if complete_message: if msgclass._CHANNELMASK is not None: channel = status & msgclass._CHANNELMASK channel_match_orna = channel_filter(channel, channel_in) bad_termination = False - if msgclass._LENGTH < 0: # indicator of variable length message + if msgclass.LENGTH < 0: # indicator of variable length message terminated_message = False msgendidxplusone = msgstartidx + 1 while msgendidxplusone <= endidx: if midibytes[msgendidxplusone] & 0x80: - if midibytes[msgendidxplusone] == msgclass._ENDSTATUS: + if midibytes[msgendidxplusone] == msgclass.ENDSTATUS: terminated_message = True else: bad_termination = True @@ -198,7 +198,7 @@ def from_message_bytes(cls, midibytes, channel_in): if not terminated_message: complete_message = False else: - msgendidxplusone = msgstartidx + msgclass._LENGTH + msgendidxplusone = msgstartidx + msgclass.LENGTH if complete_message and not bad_termination and channel_match_orna: try: @@ -257,7 +257,7 @@ class MIDIUnknownEvent(MIDIMessage): This can either occur because there is no class representing the message or because it is not imported. """ - _LENGTH = -1 + LENGTH = -1 def __init__(self, status): self.status = status @@ -271,7 +271,7 @@ class MIDIBadEvent(MIDIMessage): This could be due to status bytes appearing where data bytes are expected. """ - _LENGTH = -1 + LENGTH = -1 def __init__(self, data, exception): self.data = bytearray(data) diff --git a/adafruit_midi/note_off.py b/adafruit_midi/note_off.py index 9f1accf..043ee97 100644 --- a/adafruit_midi/note_off.py +++ b/adafruit_midi/note_off.py @@ -51,7 +51,7 @@ class NoteOff(MIDIMessage): _STATUS = 0x80 _STATUSMASK = 0xf0 - _LENGTH = 3 + LENGTH = 3 _CHANNELMASK = 0x0f def __init__(self, note, velocity): diff --git a/adafruit_midi/note_on.py b/adafruit_midi/note_on.py index 1d34ba9..427c7cf 100644 --- a/adafruit_midi/note_on.py +++ b/adafruit_midi/note_on.py @@ -51,7 +51,7 @@ class NoteOn(MIDIMessage): _STATUS = 0x90 _STATUSMASK = 0xf0 - _LENGTH = 3 + LENGTH = 3 _CHANNELMASK = 0x0f def __init__(self, note, velocity): diff --git a/adafruit_midi/pitch_bend_change.py b/adafruit_midi/pitch_bend_change.py index eecc162..ddf97ca 100644 --- a/adafruit_midi/pitch_bend_change.py +++ b/adafruit_midi/pitch_bend_change.py @@ -51,7 +51,7 @@ class PitchBendChange(MIDIMessage): _STATUS = 0xe0 _STATUSMASK = 0xf0 - _LENGTH = 3 + LENGTH = 3 _CHANNELMASK = 0x0f def __init__(self, pitch_bend): diff --git a/adafruit_midi/polyphonic_key_pressure.py b/adafruit_midi/polyphonic_key_pressure.py index b70e649..e787682 100644 --- a/adafruit_midi/polyphonic_key_pressure.py +++ b/adafruit_midi/polyphonic_key_pressure.py @@ -51,7 +51,7 @@ class PolyphonicKeyPressure(MIDIMessage): _STATUS = 0xa0 _STATUSMASK = 0xf0 - _LENGTH = 3 + LENGTH = 3 _CHANNELMASK = 0x0f def __init__(self, note, pressure): diff --git a/adafruit_midi/program_change.py b/adafruit_midi/program_change.py index 8824dbc..a600e69 100644 --- a/adafruit_midi/program_change.py +++ b/adafruit_midi/program_change.py @@ -51,7 +51,7 @@ class ProgramChange(MIDIMessage): _STATUS = 0xc0 _STATUSMASK = 0xf0 - _LENGTH = 2 + LENGTH = 2 _CHANNELMASK = 0x0f def __init__(self, patch): diff --git a/adafruit_midi/start.py b/adafruit_midi/start.py index d34f8e4..75d47d5 100644 --- a/adafruit_midi/start.py +++ b/adafruit_midi/start.py @@ -51,6 +51,6 @@ class Start(MIDIMessage): _STATUS = 0xfa _STATUSMASK = 0xff - _LENGTH = 1 + LENGTH = 1 Start.register_message_type() diff --git a/adafruit_midi/stop.py b/adafruit_midi/stop.py index 81f2bd0..e26d2ae 100644 --- a/adafruit_midi/stop.py +++ b/adafruit_midi/stop.py @@ -51,6 +51,6 @@ class Stop(MIDIMessage): _STATUS = 0xfc _STATUSMASK = 0xff - _LENGTH = 1 + LENGTH = 1 Stop.register_message_type() diff --git a/adafruit_midi/system_exclusive.py b/adafruit_midi/system_exclusive.py index 2b9f02f..9a12a49 100644 --- a/adafruit_midi/system_exclusive.py +++ b/adafruit_midi/system_exclusive.py @@ -51,8 +51,8 @@ class SystemExclusive(MIDIMessage): _STATUS = 0xf0 _STATUSMASK = 0xff - _LENGTH = -1 - _ENDSTATUS = 0xf7 + LENGTH = -1 + ENDSTATUS = 0xf7 def __init__(self, manufacturer_id, data): self.manufacturer_id = bytearray(manufacturer_id) @@ -63,11 +63,11 @@ def as_bytes(self, channel=None): return (bytearray([self._STATUS]) + self.manufacturer_id + self.data - + bytearray([self._ENDSTATUS])) + + bytearray([self.ENDSTATUS])) @classmethod def from_bytes(cls, databytes): - # -1 on second arg is to avoid the _ENDSTATUS which is passed + # -1 on second arg is to avoid the ENDSTATUS which is passed if databytes[0] != 0: return cls(databytes[0:1], databytes[1:-1]) else: diff --git a/adafruit_midi/timing_clock.py b/adafruit_midi/timing_clock.py index dcd23e0..70a5822 100644 --- a/adafruit_midi/timing_clock.py +++ b/adafruit_midi/timing_clock.py @@ -52,6 +52,6 @@ class TimingClock(MIDIMessage): _STATUS = 0xf8 _STATUSMASK = 0xff - _LENGTH = 1 + LENGTH = 1 TimingClock.register_message_type() From 6e1257ee609116f95f30681f7219f8509adcaf0c Mon Sep 17 00:00:00 2001 From: Kevin J Walters Date: Sun, 31 Mar 2019 00:39:37 +0000 Subject: [PATCH 58/92] pylint does not like _CHANNELMASK due to access to the value outside of classes, pondered scrapping this for a constant but it is used to mark message types that have channels so renaming to CHANNELMASK. #3 --- adafruit_midi/channel_pressure.py | 4 ++-- adafruit_midi/control_change.py | 4 ++-- adafruit_midi/midi_message.py | 8 ++++---- adafruit_midi/note_off.py | 4 ++-- adafruit_midi/note_on.py | 4 ++-- adafruit_midi/pitch_bend_change.py | 4 ++-- adafruit_midi/polyphonic_key_pressure.py | 4 ++-- adafruit_midi/program_change.py | 4 ++-- 8 files changed, 18 insertions(+), 18 deletions(-) diff --git a/adafruit_midi/channel_pressure.py b/adafruit_midi/channel_pressure.py index 64bd47c..8a5011f 100644 --- a/adafruit_midi/channel_pressure.py +++ b/adafruit_midi/channel_pressure.py @@ -52,7 +52,7 @@ class ChannelPressure(MIDIMessage): _STATUS = 0xd0 _STATUSMASK = 0xf0 LENGTH = 2 - _CHANNELMASK = 0x0f + CHANNELMASK = 0x0f def __init__(self, pressure): self.pressure = pressure @@ -61,7 +61,7 @@ def __init__(self, pressure): # channel value is mandatory def as_bytes(self, channel=None): - return bytearray([self._STATUS | (channel & self._CHANNELMASK), + return bytearray([self._STATUS | (channel & self.CHANNELMASK), self.pressure]) @classmethod diff --git a/adafruit_midi/control_change.py b/adafruit_midi/control_change.py index 7f68757..7c0ecf3 100644 --- a/adafruit_midi/control_change.py +++ b/adafruit_midi/control_change.py @@ -52,7 +52,7 @@ class ControlChange(MIDIMessage): _STATUS = 0xb0 _STATUSMASK = 0xf0 LENGTH = 3 - _CHANNELMASK = 0x0f + CHANNELMASK = 0x0f def __init__(self, control, value): self.control = control @@ -62,7 +62,7 @@ def __init__(self, control, value): # channel value is mandatory def as_bytes(self, channel=None): - return bytearray([self._STATUS | (channel & self._CHANNELMASK), + return bytearray([self._STATUS | (channel & self.CHANNELMASK), self.control, self.value]) @classmethod diff --git a/adafruit_midi/midi_message.py b/adafruit_midi/midi_message.py index 3b8ed71..f5f00af 100644 --- a/adafruit_midi/midi_message.py +++ b/adafruit_midi/midi_message.py @@ -103,14 +103,14 @@ class MIDIMessage: - _STATUSMASK - mask used to compared a status byte with _STATUS value - LENGTH - length for a fixed size message including status or -1 for variable length. - - _CHANNELMASK - mask use to apply a (wire protocol) channel number. + - CHANNELMASK - mask use to apply a (wire protocol) channel number. - ENDSTATUS - the EOM status byte, only set for variable length. This is an abstract class. """ _STATUS = None _STATUSMASK = None LENGTH = None - _CHANNELMASK = None + CHANNELMASK = None ENDSTATUS = None # Each element is ((status, mask), class) @@ -176,8 +176,8 @@ def from_message_bytes(cls, midibytes, channel_in): # this value can be changed later for a var. length msgs complete_message = len(midibytes) - msgstartidx >= msgclass.LENGTH if complete_message: - if msgclass._CHANNELMASK is not None: - channel = status & msgclass._CHANNELMASK + if msgclass.CHANNELMASK is not None: + channel = status & msgclass.CHANNELMASK channel_match_orna = channel_filter(channel, channel_in) bad_termination = False diff --git a/adafruit_midi/note_off.py b/adafruit_midi/note_off.py index 043ee97..3b2b36a 100644 --- a/adafruit_midi/note_off.py +++ b/adafruit_midi/note_off.py @@ -52,7 +52,7 @@ class NoteOff(MIDIMessage): _STATUS = 0x80 _STATUSMASK = 0xf0 LENGTH = 3 - _CHANNELMASK = 0x0f + CHANNELMASK = 0x0f def __init__(self, note, velocity): self.note = note_parser(note) @@ -62,7 +62,7 @@ def __init__(self, note, velocity): # channel value is mandatory def as_bytes(self, channel=None): - return bytearray([self._STATUS | (channel & self._CHANNELMASK), + return bytearray([self._STATUS | (channel & self.CHANNELMASK), self.note, self.velocity]) @classmethod diff --git a/adafruit_midi/note_on.py b/adafruit_midi/note_on.py index 427c7cf..d296e29 100644 --- a/adafruit_midi/note_on.py +++ b/adafruit_midi/note_on.py @@ -52,7 +52,7 @@ class NoteOn(MIDIMessage): _STATUS = 0x90 _STATUSMASK = 0xf0 LENGTH = 3 - _CHANNELMASK = 0x0f + CHANNELMASK = 0x0f def __init__(self, note, velocity): self.note = note_parser(note) @@ -62,7 +62,7 @@ def __init__(self, note, velocity): # channel value is mandatory def as_bytes(self, channel=None): - return bytearray([self._STATUS | (channel & self._CHANNELMASK), + return bytearray([self._STATUS | (channel & self.CHANNELMASK), self.note, self.velocity]) @classmethod diff --git a/adafruit_midi/pitch_bend_change.py b/adafruit_midi/pitch_bend_change.py index ddf97ca..ee0e15f 100644 --- a/adafruit_midi/pitch_bend_change.py +++ b/adafruit_midi/pitch_bend_change.py @@ -52,7 +52,7 @@ class PitchBendChange(MIDIMessage): _STATUS = 0xe0 _STATUSMASK = 0xf0 LENGTH = 3 - _CHANNELMASK = 0x0f + CHANNELMASK = 0x0f def __init__(self, pitch_bend): self.pitch_bend = pitch_bend @@ -61,7 +61,7 @@ def __init__(self, pitch_bend): # channel value is mandatory def as_bytes(self, channel=None): - return bytearray([self._STATUS | (channel & self._CHANNELMASK), + return bytearray([self._STATUS | (channel & self.CHANNELMASK), self.pitch_bend & 0x7f, (self.pitch_bend >> 7) & 0x7f]) diff --git a/adafruit_midi/polyphonic_key_pressure.py b/adafruit_midi/polyphonic_key_pressure.py index e787682..8f91557 100644 --- a/adafruit_midi/polyphonic_key_pressure.py +++ b/adafruit_midi/polyphonic_key_pressure.py @@ -52,7 +52,7 @@ class PolyphonicKeyPressure(MIDIMessage): _STATUS = 0xa0 _STATUSMASK = 0xf0 LENGTH = 3 - _CHANNELMASK = 0x0f + CHANNELMASK = 0x0f def __init__(self, note, pressure): self.note = note @@ -62,7 +62,7 @@ def __init__(self, note, pressure): # channel value is mandatory def as_bytes(self, channel=None): - return bytearray([self._STATUS | (channel & self._CHANNELMASK), + return bytearray([self._STATUS | (channel & self.CHANNELMASK), self.note, self.pressure]) @classmethod diff --git a/adafruit_midi/program_change.py b/adafruit_midi/program_change.py index a600e69..524b959 100644 --- a/adafruit_midi/program_change.py +++ b/adafruit_midi/program_change.py @@ -52,7 +52,7 @@ class ProgramChange(MIDIMessage): _STATUS = 0xc0 _STATUSMASK = 0xf0 LENGTH = 2 - _CHANNELMASK = 0x0f + CHANNELMASK = 0x0f def __init__(self, patch): self.patch = patch @@ -61,7 +61,7 @@ def __init__(self, patch): # channel value is mandatory def as_bytes(self, channel=None): - return bytearray([self._STATUS | (channel & self._CHANNELMASK), + return bytearray([self._STATUS | (channel & self.CHANNELMASK), self.patch]) @classmethod From 0ae13eff64d7569a7d0be675a8100caaeff6e39f Mon Sep 17 00:00:00 2001 From: Kevin J Walters Date: Sun, 31 Mar 2019 13:31:16 +0100 Subject: [PATCH 59/92] Chopping up the unwieldy code in MIDIMessage.from_message_bytes(), some code has gone into two new class methods, _match_message_status() and _search_eom_status(). Also simplified the final condition around return. #3 --- adafruit_midi/midi_message.py | 143 +++++++++++++++++++++------------- 1 file changed, 91 insertions(+), 52 deletions(-) diff --git a/adafruit_midi/midi_message.py b/adafruit_midi/midi_message.py index f5f00af..3634185 100644 --- a/adafruit_midi/midi_message.py +++ b/adafruit_midi/midi_message.py @@ -131,6 +131,75 @@ def register_message_type(cls): MIDIMessage._statusandmask_to_class.insert(insert_idx, ((cls._STATUS, cls._STATUSMASK), cls)) + + # pylint: disable=too-many-arguments + @classmethod + def _search_eom_status(cls, buf, eom_status, msgstartidx, msgendidxplusone, endidx): + good_termination = False + bad_termination = False + + msgendidxplusone = msgstartidx + 1 + while msgendidxplusone <= endidx: + # Look for a status byte + # Second rule of the MIDI club is status bytes have MSB set + if buf[msgendidxplusone] & 0x80: + if buf[msgendidxplusone] == eom_status: + good_termination = True + else: + bad_termination = True + break + else: + msgendidxplusone += 1 + + if good_termination or bad_termination: + msgendidxplusone += 1 + + return (msgendidxplusone, good_termination, bad_termination) + + # pylint: disable=too-many-arguments,too-many-locals + @classmethod + def _match_message_status(cls, buf, channel_in, msgstartidx, msgendidxplusone, endidx): + msgclass = None + status = buf[msgstartidx] + known_msg = False + complete_msg = False + bad_termination = False + channel_match_orna = True + channel = None + + # Rummage through our list looking for a status match + for status_mask, msgclass in MIDIMessage._statusandmask_to_class: + masked_status = status & status_mask[1] + if status_mask[0] == masked_status: + known_msg = True + # Check there's enough left to parse a complete message + # this value can be changed later for a var. length msgs + complete_msg = len(buf) - msgstartidx >= msgclass.LENGTH + if not complete_msg: + break + + if msgclass.CHANNELMASK is not None: + channel = status & msgclass.CHANNELMASK + channel_match_orna = channel_filter(channel, channel_in) + + if msgclass.LENGTH < 0: # indicator of variable length message + (msgendidxplusone, + terminated_msg, + bad_termination) = cls._search_eom_status(buf, + msgclass.ENDSTATUS, + msgstartidx, + msgendidxplusone, + endidx) + if not terminated_msg: + complete_msg = False + else: # fixed length message + msgendidxplusone = msgstartidx + msgclass.LENGTH + break + + return (msgclass, status, + known_msg, complete_msg, bad_termination, + channel_match_orna, channel, msgendidxplusone) + @classmethod def from_message_bytes(cls, midibytes, channel_in): """Create an appropriate object of the correct class for the @@ -140,11 +209,11 @@ def from_message_bytes(cls, midibytes, channel_in): or for no messages, partial messages or messages for other channels (None, endplusone, skipped, None). """ - msg = None endidx = len(midibytes) - 1 skipped = 0 preamble = True + channel = None msgstartidx = 0 msgendidxplusone = 0 @@ -155,58 +224,31 @@ def from_message_bytes(cls, midibytes, channel_in): msgstartidx += 1 if preamble: skipped += 1 - preamble = False # Either no message or a partial one if msgstartidx > endidx: return (None, endidx + 1, skipped, None) - status = midibytes[msgstartidx] - known_message = False - complete_message = False - channel_match_orna = True - channel = None - # Rummage through our list looking for a status match - for status_mask, msgclass in MIDIMessage._statusandmask_to_class: - masked_status = status & status_mask[1] - if status_mask[0] == masked_status: - known_message = True - # Check there's enough left to parse a complete message - # this value can be changed later for a var. length msgs - complete_message = len(midibytes) - msgstartidx >= msgclass.LENGTH - if complete_message: - if msgclass.CHANNELMASK is not None: - channel = status & msgclass.CHANNELMASK - channel_match_orna = channel_filter(channel, channel_in) - - bad_termination = False - if msgclass.LENGTH < 0: # indicator of variable length message - terminated_message = False - msgendidxplusone = msgstartidx + 1 - while msgendidxplusone <= endidx: - if midibytes[msgendidxplusone] & 0x80: - if midibytes[msgendidxplusone] == msgclass.ENDSTATUS: - terminated_message = True - else: - bad_termination = True - break - else: - msgendidxplusone += 1 - if terminated_message or bad_termination: - msgendidxplusone += 1 - if not terminated_message: - complete_message = False - else: - msgendidxplusone = msgstartidx + msgclass.LENGTH - - if complete_message and not bad_termination and channel_match_orna: - try: - msg = msgclass.from_bytes(midibytes[msgstartidx+1:msgendidxplusone]) - except(ValueError, TypeError) as ex: - msg = MIDIBadEvent(midibytes[msgstartidx+1:msgendidxplusone], ex) - - break # for + # Try and match the status byte found in midibytes + (msgclass, + status, + known_message, + complete_message, + bad_termination, + channel_match_orna, + channel, + msgendidxplusone) = cls._match_message_status(midibytes, + channel_in, + msgstartidx, + msgendidxplusone, + endidx) + + if complete_message and not bad_termination and channel_match_orna: + try: + msg = msgclass.from_bytes(midibytes[msgstartidx+1:msgendidxplusone]) + except(ValueError, TypeError) as ex: + msg = MIDIBadEvent(midibytes[msgstartidx+1:msgendidxplusone], ex) # break out of while loop for a complete message on good channel # or we have one we do not know about @@ -214,7 +256,7 @@ def from_message_bytes(cls, midibytes, channel_in): if complete_message: if channel_match_orna: break - else: + else: # advance to next message msgstartidx = msgendidxplusone else: # Important case of a known message but one that is not @@ -227,10 +269,7 @@ def from_message_bytes(cls, midibytes, channel_in): msgendidxplusone = msgstartidx + 1 break - if msg is not None: - return (msg, msgendidxplusone, skipped, channel) - else: - return (None, msgendidxplusone, skipped, None) + return (msg, msgendidxplusone, skipped, channel) # channel value present to keep interface uniform but unused # pylint: disable=unused-argument From 1811478998af5e9e41753ed3a08831f3fc77bd08 Mon Sep 17 00:00:00 2001 From: Kevin J Walters Date: Sun, 31 Mar 2019 14:50:58 +0100 Subject: [PATCH 60/92] Adding documentation to each of the MIDI message classes. Adding use of note_parser() in constuctor for PolyphonicKeyPressure. #3 --- adafruit_midi/channel_pressure.py | 5 +++++ adafruit_midi/control_change.py | 7 +++++++ adafruit_midi/note_off.py | 7 +++++++ adafruit_midi/note_on.py | 6 ++++++ adafruit_midi/pitch_bend_change.py | 5 +++++ adafruit_midi/polyphonic_key_pressure.py | 10 ++++++++-- adafruit_midi/program_change.py | 5 +++++ adafruit_midi/start.py | 3 +++ adafruit_midi/stop.py | 2 ++ adafruit_midi/system_exclusive.py | 6 ++++++ adafruit_midi/timing_clock.py | 7 +++++++ 11 files changed, 61 insertions(+), 2 deletions(-) diff --git a/adafruit_midi/channel_pressure.py b/adafruit_midi/channel_pressure.py index 8a5011f..eab5297 100644 --- a/adafruit_midi/channel_pressure.py +++ b/adafruit_midi/channel_pressure.py @@ -49,6 +49,11 @@ class ChannelPressure(MIDIMessage): + """Channel Pressure MIDI message. + + :param int pressure: The pressure, 0-127. + """ + _STATUS = 0xd0 _STATUSMASK = 0xf0 LENGTH = 2 diff --git a/adafruit_midi/control_change.py b/adafruit_midi/control_change.py index 7c0ecf3..f5446f3 100644 --- a/adafruit_midi/control_change.py +++ b/adafruit_midi/control_change.py @@ -49,6 +49,13 @@ class ControlChange(MIDIMessage): + """Control Change MIDI message. + + :param int control: The control number, 0-127. + :param int value: The 7bit value of the control, 0-127. + + """ + _STATUS = 0xb0 _STATUSMASK = 0xf0 LENGTH = 3 diff --git a/adafruit_midi/note_off.py b/adafruit_midi/note_off.py index 3b2b36a..2ac9956 100644 --- a/adafruit_midi/note_off.py +++ b/adafruit_midi/note_off.py @@ -49,6 +49,13 @@ class NoteOff(MIDIMessage): + """Note Off Change MIDI message. + + :param note: The note (key) number either as an int (0-127) or a str which is parsed, e.g. "C4" (middle C) is 60, "A4" is 69. + :param int velocity: The release velocity, 0-127. + + """ + _STATUS = 0x80 _STATUSMASK = 0xf0 LENGTH = 3 diff --git a/adafruit_midi/note_on.py b/adafruit_midi/note_on.py index d296e29..d96f952 100644 --- a/adafruit_midi/note_on.py +++ b/adafruit_midi/note_on.py @@ -49,6 +49,12 @@ class NoteOn(MIDIMessage): + """Note On Change MIDI message. + + :param note: The note (key) number either as an int (0-127) or a str which is parsed, e.g. "C4" (middle C) is 60, "A4" is 69. + :param int velocity: The strike velocity, 0-127, 0 is equivalent to a Note Off. + """ + _STATUS = 0x90 _STATUSMASK = 0xf0 LENGTH = 3 diff --git a/adafruit_midi/pitch_bend_change.py b/adafruit_midi/pitch_bend_change.py index ee0e15f..dc3dcd4 100644 --- a/adafruit_midi/pitch_bend_change.py +++ b/adafruit_midi/pitch_bend_change.py @@ -49,6 +49,11 @@ class PitchBendChange(MIDIMessage): + """Pitch Bend Change MIDI message. + + :param int pitch_bend: A 14bit unsigned int representing the degree of bend from 0 through 8192 (midpoint, no bend) to 16383. + """ + _STATUS = 0xe0 _STATUSMASK = 0xf0 LENGTH = 3 diff --git a/adafruit_midi/polyphonic_key_pressure.py b/adafruit_midi/polyphonic_key_pressure.py index 8f91557..514fa8f 100644 --- a/adafruit_midi/polyphonic_key_pressure.py +++ b/adafruit_midi/polyphonic_key_pressure.py @@ -42,20 +42,26 @@ """ -from .midi_message import MIDIMessage +from .midi_message import MIDIMessage, note_parser __version__ = "0.0.0-auto.0" __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_MIDI.git" class PolyphonicKeyPressure(MIDIMessage): + """Polyphonic Key Pressure MIDI message. + + :param note: The note (key) number either as an int (0-127) or a str which is parsed, e.g. "C4" (middle C) is 60, "A4" is 69. + :param int pressure: The pressure, 0-127. + """ + _STATUS = 0xa0 _STATUSMASK = 0xf0 LENGTH = 3 CHANNELMASK = 0x0f def __init__(self, note, pressure): - self.note = note + self.note = note_parser(note) self.pressure = pressure if not 0 <= self.note <= 127 or not 0 <= self.pressure <= 127: raise ValueError("Out of range") diff --git a/adafruit_midi/program_change.py b/adafruit_midi/program_change.py index 524b959..d15d906 100644 --- a/adafruit_midi/program_change.py +++ b/adafruit_midi/program_change.py @@ -49,6 +49,11 @@ class ProgramChange(MIDIMessage): + """Program Change MIDI message. + + :param int patch: The new program/patch number to use, 0-127. + """ + _STATUS = 0xc0 _STATUSMASK = 0xf0 LENGTH = 2 diff --git a/adafruit_midi/start.py b/adafruit_midi/start.py index 75d47d5..dfcc8f1 100644 --- a/adafruit_midi/start.py +++ b/adafruit_midi/start.py @@ -49,6 +49,9 @@ class Start(MIDIMessage): + """Start MIDI message. + """ + _STATUS = 0xfa _STATUSMASK = 0xff LENGTH = 1 diff --git a/adafruit_midi/stop.py b/adafruit_midi/stop.py index e26d2ae..75b8b41 100644 --- a/adafruit_midi/stop.py +++ b/adafruit_midi/stop.py @@ -49,6 +49,8 @@ class Stop(MIDIMessage): + """Stop MIDI message. + """ _STATUS = 0xfc _STATUSMASK = 0xff LENGTH = 1 diff --git a/adafruit_midi/system_exclusive.py b/adafruit_midi/system_exclusive.py index 9a12a49..9868acd 100644 --- a/adafruit_midi/system_exclusive.py +++ b/adafruit_midi/system_exclusive.py @@ -49,6 +49,12 @@ class SystemExclusive(MIDIMessage): + """System Exclusive MIDI message. + + :param list manufacturer_id: The single byte or three byte manufacturer's id as a list or bytearray of numbers between 0-127. + :param list data: The 7bit data as a list or bytearray of numbers between 0-127. + """ + _STATUS = 0xf0 _STATUSMASK = 0xff LENGTH = -1 diff --git a/adafruit_midi/timing_clock.py b/adafruit_midi/timing_clock.py index 70a5822..3650def 100644 --- a/adafruit_midi/timing_clock.py +++ b/adafruit_midi/timing_clock.py @@ -50,6 +50,13 @@ # Good to have this registered first as it occurs frequently when present class TimingClock(MIDIMessage): + """Timing Clock MIDI message. + + This occurs 24 times per quarter note when synchronization is in use. + If this is not needed it's best to avoid this sending this high frequency + message to a CircuitPython device to reduce the amount of message processing. + """ + _STATUS = 0xf8 _STATUSMASK = 0xff LENGTH = 1 From c1740cbf42be2b0f15c4e5b3bcf8f6b849e712ef Mon Sep 17 00:00:00 2001 From: Kevin J Walters Date: Mon, 1 Apr 2019 00:05:38 +0100 Subject: [PATCH 61/92] More pylint happiness and minor reST formatting corrections. #3 --- adafruit_midi/channel_pressure.py | 4 ++-- adafruit_midi/control_change.py | 6 +++--- adafruit_midi/note_off.py | 9 +++++---- adafruit_midi/note_on.py | 12 +++++++----- adafruit_midi/pitch_bend_change.py | 9 +++++---- adafruit_midi/polyphonic_key_pressure.py | 7 ++++--- adafruit_midi/program_change.py | 6 +++--- adafruit_midi/system_exclusive.py | 5 +++-- 8 files changed, 32 insertions(+), 26 deletions(-) diff --git a/adafruit_midi/channel_pressure.py b/adafruit_midi/channel_pressure.py index eab5297..c767786 100644 --- a/adafruit_midi/channel_pressure.py +++ b/adafruit_midi/channel_pressure.py @@ -68,9 +68,9 @@ def __init__(self, pressure): def as_bytes(self, channel=None): return bytearray([self._STATUS | (channel & self.CHANNELMASK), self.pressure]) - + @classmethod def from_bytes(cls, databytes): - return cls(databytes[0]) + return cls(databytes[0]) ChannelPressure.register_message_type() diff --git a/adafruit_midi/control_change.py b/adafruit_midi/control_change.py index f5446f3..21ef7b6 100644 --- a/adafruit_midi/control_change.py +++ b/adafruit_midi/control_change.py @@ -60,7 +60,7 @@ class ControlChange(MIDIMessage): _STATUSMASK = 0xf0 LENGTH = 3 CHANNELMASK = 0x0f - + def __init__(self, control, value): self.control = control self.value = value @@ -74,6 +74,6 @@ def as_bytes(self, channel=None): @classmethod def from_bytes(cls, databytes): - return cls(databytes[0], databytes[1]) - + return cls(databytes[0], databytes[1]) + ControlChange.register_message_type() diff --git a/adafruit_midi/note_off.py b/adafruit_midi/note_off.py index 2ac9956..75d3707 100644 --- a/adafruit_midi/note_off.py +++ b/adafruit_midi/note_off.py @@ -51,7 +51,8 @@ class NoteOff(MIDIMessage): """Note Off Change MIDI message. - :param note: The note (key) number either as an int (0-127) or a str which is parsed, e.g. "C4" (middle C) is 60, "A4" is 69. + :param note: The note (key) number either as an ``int`` (0-127) or a + ``str`` which is parsed, e.g. "C4" (middle C) is 60, "A4" is 69. :param int velocity: The release velocity, 0-127. """ @@ -60,7 +61,7 @@ class NoteOff(MIDIMessage): _STATUSMASK = 0xf0 LENGTH = 3 CHANNELMASK = 0x0f - + def __init__(self, note, velocity): self.note = note_parser(note) self.velocity = velocity @@ -70,10 +71,10 @@ def __init__(self, note, velocity): # channel value is mandatory def as_bytes(self, channel=None): return bytearray([self._STATUS | (channel & self.CHANNELMASK), - self.note, self.velocity]) + self.note, self.velocity]) @classmethod def from_bytes(cls, databytes): - return cls(databytes[0], databytes[1]) + return cls(databytes[0], databytes[1]) NoteOff.register_message_type() diff --git a/adafruit_midi/note_on.py b/adafruit_midi/note_on.py index d96f952..7ffb049 100644 --- a/adafruit_midi/note_on.py +++ b/adafruit_midi/note_on.py @@ -51,8 +51,10 @@ class NoteOn(MIDIMessage): """Note On Change MIDI message. - :param note: The note (key) number either as an int (0-127) or a str which is parsed, e.g. "C4" (middle C) is 60, "A4" is 69. - :param int velocity: The strike velocity, 0-127, 0 is equivalent to a Note Off. + :param note: The note (key) number either as an ``int`` (0-127) or a + ``str`` which is parsed, e.g. "C4" (middle C) is 60, "A4" is 69. + :param int velocity: The strike velocity, 0-127, 0 is equivalent + to a Note Off. """ _STATUS = 0x90 @@ -65,14 +67,14 @@ def __init__(self, note, velocity): self.velocity = velocity if not 0 <= self.note <= 127 or not 0 <= self.velocity <= 127: raise ValueError("Out of range") - + # channel value is mandatory def as_bytes(self, channel=None): return bytearray([self._STATUS | (channel & self.CHANNELMASK), - self.note, self.velocity]) + self.note, self.velocity]) @classmethod def from_bytes(cls, databytes): - return cls(databytes[0], databytes[1]) + return cls(databytes[0], databytes[1]) NoteOn.register_message_type() diff --git a/adafruit_midi/pitch_bend_change.py b/adafruit_midi/pitch_bend_change.py index dc3dcd4..ddc013f 100644 --- a/adafruit_midi/pitch_bend_change.py +++ b/adafruit_midi/pitch_bend_change.py @@ -51,14 +51,15 @@ class PitchBendChange(MIDIMessage): """Pitch Bend Change MIDI message. - :param int pitch_bend: A 14bit unsigned int representing the degree of bend from 0 through 8192 (midpoint, no bend) to 16383. + :param int pitch_bend: A 14bit unsigned int representing the degree of + bend from 0 through 8192 (midpoint, no bend) to 16383. """ _STATUS = 0xe0 _STATUSMASK = 0xf0 LENGTH = 3 CHANNELMASK = 0x0f - + def __init__(self, pitch_bend): self.pitch_bend = pitch_bend if not 0 <= self.pitch_bend <= 16383: @@ -69,9 +70,9 @@ def as_bytes(self, channel=None): return bytearray([self._STATUS | (channel & self.CHANNELMASK), self.pitch_bend & 0x7f, (self.pitch_bend >> 7) & 0x7f]) - + @classmethod def from_bytes(cls, databytes): - return cls(databytes[1] << 7 | databytes[0]) + return cls(databytes[1] << 7 | databytes[0]) PitchBendChange.register_message_type() diff --git a/adafruit_midi/polyphonic_key_pressure.py b/adafruit_midi/polyphonic_key_pressure.py index 514fa8f..f6279e7 100644 --- a/adafruit_midi/polyphonic_key_pressure.py +++ b/adafruit_midi/polyphonic_key_pressure.py @@ -51,7 +51,8 @@ class PolyphonicKeyPressure(MIDIMessage): """Polyphonic Key Pressure MIDI message. - :param note: The note (key) number either as an int (0-127) or a str which is parsed, e.g. "C4" (middle C) is 60, "A4" is 69. + :param note: The note (key) number either as an ``int`` (0-127) or a + ``str`` which is parsed, e.g. "C4" (middle C) is 60, "A4" is 69. :param int pressure: The pressure, 0-127. """ @@ -59,7 +60,7 @@ class PolyphonicKeyPressure(MIDIMessage): _STATUSMASK = 0xf0 LENGTH = 3 CHANNELMASK = 0x0f - + def __init__(self, note, pressure): self.note = note_parser(note) self.pressure = pressure @@ -73,6 +74,6 @@ def as_bytes(self, channel=None): @classmethod def from_bytes(cls, databytes): - return cls(databytes[0], databytes[1]) + return cls(databytes[0], databytes[1]) PolyphonicKeyPressure.register_message_type() diff --git a/adafruit_midi/program_change.py b/adafruit_midi/program_change.py index d15d906..8767f12 100644 --- a/adafruit_midi/program_change.py +++ b/adafruit_midi/program_change.py @@ -58,7 +58,7 @@ class ProgramChange(MIDIMessage): _STATUSMASK = 0xf0 LENGTH = 2 CHANNELMASK = 0x0f - + def __init__(self, patch): self.patch = patch if not 0 <= self.patch <= 127: @@ -68,9 +68,9 @@ def __init__(self, patch): def as_bytes(self, channel=None): return bytearray([self._STATUS | (channel & self.CHANNELMASK), self.patch]) - + @classmethod def from_bytes(cls, databytes): - return cls(databytes[0]) + return cls(databytes[0]) ProgramChange.register_message_type() diff --git a/adafruit_midi/system_exclusive.py b/adafruit_midi/system_exclusive.py index 9868acd..8f4290b 100644 --- a/adafruit_midi/system_exclusive.py +++ b/adafruit_midi/system_exclusive.py @@ -51,7 +51,8 @@ class SystemExclusive(MIDIMessage): """System Exclusive MIDI message. - :param list manufacturer_id: The single byte or three byte manufacturer's id as a list or bytearray of numbers between 0-127. + :param list manufacturer_id: The single byte or three byte + manufacturer's id as a list or bytearray of numbers between 0-127. :param list data: The 7bit data as a list or bytearray of numbers between 0-127. """ @@ -74,7 +75,7 @@ def as_bytes(self, channel=None): @classmethod def from_bytes(cls, databytes): # -1 on second arg is to avoid the ENDSTATUS which is passed - if databytes[0] != 0: + if databytes[0] != 0: # pylint: disable=no-else-return return cls(databytes[0:1], databytes[1:-1]) else: return cls(databytes[0:3], databytes[3:-1]) From 0fba8ffdd7f771f09bf8b87bab8a615854d56432 Mon Sep 17 00:00:00 2001 From: Kevin J Walters Date: Mon, 1 Apr 2019 00:06:47 +0100 Subject: [PATCH 62/92] More pylint happiness for examples. #3 --- examples/midi_inoutdemo.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/examples/midi_inoutdemo.py b/examples/midi_inoutdemo.py index a69db42..fdbf776 100644 --- a/examples/midi_inoutdemo.py +++ b/examples/midi_inoutdemo.py @@ -1,13 +1,12 @@ # midi_inoutdemo - demonstrates receiving and sending MIDI events -import time -import random - import adafruit_midi # TimingClock is worth importing first if present as it # will make parsing more efficient for this high frequency event # Only importing what is used will save a little bit of memory + +# pylint: disable=unused-import from adafruit_midi.timing_clock import TimingClock from adafruit_midi.channel_pressure import ChannelPressure from adafruit_midi.control_change import ControlChange @@ -22,7 +21,7 @@ from adafruit_midi.midi_message import MIDIUnknownEvent -midi = adafruit_midi.MIDI(in_channel=(1,2,3), out_channel=0) +midi = adafruit_midi.MIDI(in_channel=(1, 2, 3), out_channel=0) print("Midi Demo in and out") @@ -44,8 +43,8 @@ if 0 <= new_note <= 127: midi.send(NoteOn(new_note, msg_in.velocity)) - elif (isinstance(msg_in, NoteOff) or - isinstance(msg_in, NoteOn) and msg_in.velocity == 0): + elif (isinstance(msg_in, NoteOff) + or isinstance(msg_in, NoteOn) and msg_in.velocity == 0): for offset in major_chord: new_note = msg_in.note + offset if 0 <= new_note <= 127: From 0f963573639d2acf7c73f3e8b58acae85b420db7 Mon Sep 17 00:00:00 2001 From: Kevin J Walters Date: Mon, 1 Apr 2019 00:25:50 +0100 Subject: [PATCH 63/92] Saving a bit of memory by punting frequently used ValueError("Out of range") to MIDIMessage. #3 --- adafruit_midi/channel_pressure.py | 2 +- adafruit_midi/control_change.py | 2 +- adafruit_midi/midi_message.py | 3 +++ adafruit_midi/note_off.py | 2 +- adafruit_midi/note_on.py | 2 +- adafruit_midi/pitch_bend_change.py | 2 +- adafruit_midi/polyphonic_key_pressure.py | 2 +- adafruit_midi/program_change.py | 2 +- 8 files changed, 10 insertions(+), 7 deletions(-) diff --git a/adafruit_midi/channel_pressure.py b/adafruit_midi/channel_pressure.py index c767786..6e1f9d4 100644 --- a/adafruit_midi/channel_pressure.py +++ b/adafruit_midi/channel_pressure.py @@ -62,7 +62,7 @@ class ChannelPressure(MIDIMessage): def __init__(self, pressure): self.pressure = pressure if not 0 <= self.pressure <= 127: - raise ValueError("Out of range") + raise self._EX_VALUEERROR_OOR # channel value is mandatory def as_bytes(self, channel=None): diff --git a/adafruit_midi/control_change.py b/adafruit_midi/control_change.py index 21ef7b6..3894086 100644 --- a/adafruit_midi/control_change.py +++ b/adafruit_midi/control_change.py @@ -65,7 +65,7 @@ def __init__(self, control, value): self.control = control self.value = value if not 0 <= self.control <= 127 or not 0 <= self.value <= 127: - raise ValueError("Out of range") + raise self._EX_VALUEERROR_OOR # channel value is mandatory def as_bytes(self, channel=None): diff --git a/adafruit_midi/midi_message.py b/adafruit_midi/midi_message.py index 3634185..6735cd4 100644 --- a/adafruit_midi/midi_message.py +++ b/adafruit_midi/midi_message.py @@ -113,6 +113,9 @@ class MIDIMessage: CHANNELMASK = None ENDSTATUS = None + # Commonly used exceptions to save memory + _EX_VALUEERROR_OOR = ValueError("Out of range") + # Each element is ((status, mask), class) # order is more specific masks first _statusandmask_to_class = [] diff --git a/adafruit_midi/note_off.py b/adafruit_midi/note_off.py index 75d3707..26237ab 100644 --- a/adafruit_midi/note_off.py +++ b/adafruit_midi/note_off.py @@ -66,7 +66,7 @@ def __init__(self, note, velocity): self.note = note_parser(note) self.velocity = velocity if not 0 <= self.note <= 127 or not 0 <= self.velocity <= 127: - raise ValueError("Out of range") + raise self._EX_VALUEERROR_OOR # channel value is mandatory def as_bytes(self, channel=None): diff --git a/adafruit_midi/note_on.py b/adafruit_midi/note_on.py index 7ffb049..59ae70a 100644 --- a/adafruit_midi/note_on.py +++ b/adafruit_midi/note_on.py @@ -66,7 +66,7 @@ def __init__(self, note, velocity): self.note = note_parser(note) self.velocity = velocity if not 0 <= self.note <= 127 or not 0 <= self.velocity <= 127: - raise ValueError("Out of range") + raise self._EX_VALUEERROR_OOR # channel value is mandatory def as_bytes(self, channel=None): diff --git a/adafruit_midi/pitch_bend_change.py b/adafruit_midi/pitch_bend_change.py index ddc013f..4f23161 100644 --- a/adafruit_midi/pitch_bend_change.py +++ b/adafruit_midi/pitch_bend_change.py @@ -63,7 +63,7 @@ class PitchBendChange(MIDIMessage): def __init__(self, pitch_bend): self.pitch_bend = pitch_bend if not 0 <= self.pitch_bend <= 16383: - raise ValueError("Out of range") + raise self._EX_VALUEERROR_OOR # channel value is mandatory def as_bytes(self, channel=None): diff --git a/adafruit_midi/polyphonic_key_pressure.py b/adafruit_midi/polyphonic_key_pressure.py index f6279e7..3b746e9 100644 --- a/adafruit_midi/polyphonic_key_pressure.py +++ b/adafruit_midi/polyphonic_key_pressure.py @@ -65,7 +65,7 @@ def __init__(self, note, pressure): self.note = note_parser(note) self.pressure = pressure if not 0 <= self.note <= 127 or not 0 <= self.pressure <= 127: - raise ValueError("Out of range") + raise self._EX_VALUEERROR_OOR # channel value is mandatory def as_bytes(self, channel=None): diff --git a/adafruit_midi/program_change.py b/adafruit_midi/program_change.py index 8767f12..8d1babe 100644 --- a/adafruit_midi/program_change.py +++ b/adafruit_midi/program_change.py @@ -62,7 +62,7 @@ class ProgramChange(MIDIMessage): def __init__(self, patch): self.patch = patch if not 0 <= self.patch <= 127: - raise ValueError("Out of range") + raise self._EX_VALUEERROR_OOR # channel value is mandatory def as_bytes(self, channel=None): From 5d21aaddd6ed16d33ccfaedac1533ae5f01dee68 Mon Sep 17 00:00:00 2001 From: Kevin J Walters Date: Mon, 1 Apr 2019 00:39:33 +0100 Subject: [PATCH 64/92] Adding all the messages into API docs. #3 --- docs/api.rst | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/docs/api.rst b/docs/api.rst index a7e57e4..aa78784 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -6,3 +6,40 @@ .. automodule:: adafruit_midi :members: + +.. automodule:: adafruit_midi.channel_pressure + :members: + +.. automodule:: adafruit_midi.control_change + :members: + +.. automodule:: adafruit_midi.midi_message + :members: + +.. automodule:: adafruit_midi.note_off + :members: + +.. automodule:: adafruit_midi.note_on + :members: + +.. automodule:: adafruit_midi.pitch_bend_change + :members: + +.. automodule:: adafruit_midi.polyphonic_key_pressure + :members: + +.. automodule:: adafruit_midi.program_change + :members: + +.. automodule:: adafruit_midi.start + :members: + +.. automodule:: adafruit_midi.stop + :members: + +.. automodule:: adafruit_midi.system_exclusive + :members: + +.. automodule:: adafruit_midi.timing_clock + :members: + From 3d6682eb9786dc95f7f5f818df89581aeececff3 Mon Sep 17 00:00:00 2001 From: Kevin J Walters Date: Mon, 1 Apr 2019 00:52:45 +0100 Subject: [PATCH 65/92] Adding myself to author list. #3 --- docs/conf.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 00224f4..2d8c5f0 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -35,8 +35,8 @@ # General information about the project. project = u'Adafruit MIDI Library' -copyright = u'2019 Ladyada' -author = u'Ladyada' +copyright = u'2019 Ladyada & Kevin J. Walters' +author = u'Ladyada & Kevin J. Walters' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the From db01ea7a63dec9e0e3d30c589faf3ac41697e6d5 Mon Sep 17 00:00:00 2001 From: Kevin J Walters Date: Mon, 1 Apr 2019 19:56:46 +0100 Subject: [PATCH 66/92] More pylint merriment. #3 --- adafruit_midi/midi_message.py | 1 + examples/midi_inoutdemo.py | 2 +- examples/midi_intest1.py | 1 - 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/adafruit_midi/midi_message.py b/adafruit_midi/midi_message.py index 6735cd4..963e1b0 100644 --- a/adafruit_midi/midi_message.py +++ b/adafruit_midi/midi_message.py @@ -146,6 +146,7 @@ def _search_eom_status(cls, buf, eom_status, msgstartidx, msgendidxplusone, endi # Look for a status byte # Second rule of the MIDI club is status bytes have MSB set if buf[msgendidxplusone] & 0x80: + # pylint disable=simplifiable-if-statement # n/a for this technique if buf[msgendidxplusone] == eom_status: good_termination = True else: diff --git a/examples/midi_inoutdemo.py b/examples/midi_inoutdemo.py index fdbf776..deb3914 100644 --- a/examples/midi_inoutdemo.py +++ b/examples/midi_inoutdemo.py @@ -7,7 +7,7 @@ # Only importing what is used will save a little bit of memory # pylint: disable=unused-import -from adafruit_midi.timing_clock import TimingClock +from adafruit_midi.timing_clock import TimingClock from adafruit_midi.channel_pressure import ChannelPressure from adafruit_midi.control_change import ControlChange from adafruit_midi.note_off import NoteOff diff --git a/examples/midi_intest1.py b/examples/midi_intest1.py index 9a829cb..9b62da0 100644 --- a/examples/midi_intest1.py +++ b/examples/midi_intest1.py @@ -1,5 +1,4 @@ import time -import random import adafruit_midi ### 0 is MIDI channel 1 From 944cb4d5ac3753bce5ce9b21ec6b6b2868483025 Mon Sep 17 00:00:00 2001 From: Kevin J Walters Date: Mon, 1 Apr 2019 21:34:33 +0100 Subject: [PATCH 67/92] pylint does not like the comment on same line. #3 --- adafruit_midi/midi_message.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/adafruit_midi/midi_message.py b/adafruit_midi/midi_message.py index 963e1b0..45b7530 100644 --- a/adafruit_midi/midi_message.py +++ b/adafruit_midi/midi_message.py @@ -105,7 +105,8 @@ class MIDIMessage: or -1 for variable length. - CHANNELMASK - mask use to apply a (wire protocol) channel number. - ENDSTATUS - the EOM status byte, only set for variable length. - This is an abstract class. + + This is an abstract class. """ _STATUS = None _STATUSMASK = None @@ -146,7 +147,7 @@ def _search_eom_status(cls, buf, eom_status, msgstartidx, msgendidxplusone, endi # Look for a status byte # Second rule of the MIDI club is status bytes have MSB set if buf[msgendidxplusone] & 0x80: - # pylint disable=simplifiable-if-statement # n/a for this technique + # pylint disable=simplifiable-if-statement if buf[msgendidxplusone] == eom_status: good_termination = True else: From 5b813a3211675927e0be579e4f1ed2596d84335a Mon Sep 17 00:00:00 2001 From: Kevin J Walters Date: Mon, 1 Apr 2019 21:39:41 +0100 Subject: [PATCH 68/92] Cleaning up script lines - I think the strange looking negative directory tests are intentional and are there for required directories. Removing superfluous round brackets. #3 --- .travis.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 87d4926..c9b8e81 100644 --- a/.travis.yml +++ b/.travis.yml @@ -44,9 +44,9 @@ install: - pip install --force-reinstall pylint==1.9.2 script: - - ([[ -d "tests" ]] && py.test) + - [[ ! -d "tests" ]] || py.test - pylint adafruit_midi/*.py - - ([[ -d "tests" ]] && pylint --disable=missing-docstring,invalid-name,bad-whitespace,trailing-whitespace,line-too-long,wrong-import-position,unused-import tests/*.py) - - ([[ ! -d "examples" ]] || pylint --disable=missing-docstring,invalid-name,bad-whitespace examples/*.py) + - [[ ! -d "tests" ]] || pylint --disable=missing-docstring,invalid-name,bad-whitespace,trailing-whitespace,line-too-long,wrong-import-position,unused-import tests/*.py + - [[ ! -d "examples" ]] || pylint --disable=missing-docstring,invalid-name,bad-whitespace examples/*.py - circuitpython-build-bundles --filename_prefix adafruit-circuitpython-midi --library_location . - - cd docs && sphinx-build -E -W -b html . _build/html && cd .. + - ( cd docs && sphinx-build -E -W -b html . _build/html ) From 279345b8f3084c86efc8c95a2efa00243908f76b Mon Sep 17 00:00:00 2001 From: Kevin J Walters Date: Tue, 2 Apr 2019 00:01:58 +0100 Subject: [PATCH 69/92] Setting the package names correctly in the MIDI messages class files. Adding some warnings on SystemExclusive needing to fit into the input buffer to parse. Changing the reST docs for default class methods in MIDIMessage as those are inherited by documentation too and were not appropropriate for all. Documenting the MIDI constructor. #3 --- adafruit_midi/__init__.py | 25 +++++++++-- adafruit_midi/channel_pressure.py | 13 +----- adafruit_midi/control_change.py | 13 +----- adafruit_midi/midi_message.py | 53 +++++++++++++----------- adafruit_midi/note_off.py | 13 +----- adafruit_midi/note_on.py | 13 +----- adafruit_midi/pitch_bend_change.py | 13 +----- adafruit_midi/polyphonic_key_pressure.py | 13 +----- adafruit_midi/program_change.py | 13 +----- adafruit_midi/start.py | 13 +----- adafruit_midi/stop.py | 13 +----- adafruit_midi/system_exclusive.py | 15 ++----- adafruit_midi/timing_clock.py | 13 +----- 13 files changed, 74 insertions(+), 149 deletions(-) diff --git a/adafruit_midi/__init__.py b/adafruit_midi/__init__.py index e086cf4..924bc60 100644 --- a/adafruit_midi/__init__.py +++ b/adafruit_midi/__init__.py @@ -51,7 +51,24 @@ class MIDI: - """MIDI helper class.""" + """MIDI helper class. + + :param midi_in: an object which implements ``read(length)``, + defaults to ``usb_midi.ports[0]``. + :param midi_out: an object which implements ``write(buffer, length)``, + defaults to ``usb_midi.ports[1]``. + :param in_channel: The input channel(s). + This is used by ``receive`` to filter data. + This can either be an ``int`` for the wire protocol channel number (0-15) + a tuple of ``int`` to listen for multiple channels or ``"ALL"``. + Defaults to None. + :param int out_channel: The wire protocol output channel number (0-15) + used by ``send`` if no channel is specified, + defaults to 0 (MIDI Channel 1). + :param int in_buf_size: Size of input buffer in bytes, default 30. + :param bool debug: Debug mode, default False. + + """ NOTE_ON = 0x90 NOTE_OFF = 0x80 @@ -59,7 +76,7 @@ class MIDI: CONTROL_CHANGE = 0xB0 def __init__(self, midi_in=usb_midi.ports[0], midi_out=usb_midi.ports[1], *, - in_channel=None, out_channel=0, debug=False, in_buf_size=30): + in_channel=None, out_channel=0, in_buf_size=30, debug=False): self._midi_in = midi_in self._midi_out = midi_out self.in_channel = in_channel @@ -108,8 +125,10 @@ def out_channel(self, channel): def receive(self): """Read messages from MIDI port, store them in internal read buffer, then parse that data and return the first MIDI message (event). + This maintains the blocking characteristics of the midi_in port. - Returns (MIDIMessage object, channel) or (None, None) for nothing. + :returns (MIDIMessage object, int channel): Returns object and channel + or (None, None) for nothing. """ ### could check _midi_in is an object OR correct object OR correct interface here? # If the buffer here is not full then read as much as we can fit from diff --git a/adafruit_midi/channel_pressure.py b/adafruit_midi/channel_pressure.py index 6e1f9d4..521d024 100644 --- a/adafruit_midi/channel_pressure.py +++ b/adafruit_midi/channel_pressure.py @@ -20,10 +20,10 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. """ -`adafruit_midi` +`adafruit_midi.channel_pressure` ================================================================================ -A CircuitPython helper for encoding/decoding MIDI packets over a MIDI or UART connection. +Channel Pressure MIDI message. * Author(s): Kevin J. Walters @@ -31,15 +31,6 @@ Implementation Notes -------------------- -**Hardware:** - - - -**Software and Dependencies:** - -* Adafruit CircuitPython firmware for the supported boards: - https://github.com/adafruit/circuitpython/releases - """ from .midi_message import MIDIMessage diff --git a/adafruit_midi/control_change.py b/adafruit_midi/control_change.py index 3894086..0212805 100644 --- a/adafruit_midi/control_change.py +++ b/adafruit_midi/control_change.py @@ -20,10 +20,10 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. """ -`adafruit_midi` +`adafruit_midi.control_change` ================================================================================ -A CircuitPython helper for encoding/decoding MIDI packets over a MIDI or UART connection. +Control Change MIDI message. * Author(s): Kevin J. Walters @@ -31,15 +31,6 @@ Implementation Notes -------------------- -**Hardware:** - - - -**Software and Dependencies:** - -* Adafruit CircuitPython firmware for the supported boards: - https://github.com/adafruit/circuitpython/releases - """ from .midi_message import MIDIMessage diff --git a/adafruit_midi/midi_message.py b/adafruit_midi/midi_message.py index 45b7530..32a6014 100644 --- a/adafruit_midi/midi_message.py +++ b/adafruit_midi/midi_message.py @@ -20,10 +20,16 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. """ -`adafruit_midi` +`adafruit_midi.midi_message.MIDIMessage` ================================================================================ -A CircuitPython helper for encoding/decoding MIDI packets over a MIDI or UART connection. +An abstract class for objects which represent MIDI messages (events). +When individual messages are imported they register themselves with +:func:register_message_type which makes them recognised +by the parser, :func:from_message_bytes. + +Large messages like :class:SystemExclusive can only be parsed if they fit +within the input buffer in :class:MIDI. * Author(s): Kevin J. Walters @@ -31,15 +37,6 @@ Implementation Notes -------------------- -**Hardware:** - - - -**Software and Dependencies:** - -* Adafruit CircuitPython firmware for the supported boards: - https://github.com/adafruit/circuitpython/releases - """ __version__ = "0.0.0-auto.0" @@ -97,16 +94,19 @@ def note_parser(note): class MIDIMessage: """ - A MIDI message: - - _STATUS - extracted from Status byte with channel replaced by 0s - (high bit always set). - - _STATUSMASK - mask used to compared a status byte with _STATUS value - - LENGTH - length for a fixed size message including status - or -1 for variable length. - - CHANNELMASK - mask use to apply a (wire protocol) channel number. - - ENDSTATUS - the EOM status byte, only set for variable length. - - This is an abstract class. + The parent class for MIDI messages. + + Class variables: + + * ``_STATUS`` - extracted from status byte with channel replaced by 0s + (high bit is always set by convention). + * ``_STATUSMASK`` - mask used to compared a status byte with ``_STATUS`` value. + * ``LENGTH`` - length for a fixed size message *including* status + or -1 for variable length. + * ``CHANNELMASK`` - mask used to apply a (wire protocol) channel number. + * ``ENDSTATUS`` - the end of message status byte, only set for variable length. + + This is an *abstract* class. """ _STATUS = None _STATUSMASK = None @@ -277,18 +277,21 @@ def from_message_bytes(cls, midibytes, channel_in): return (msg, msgendidxplusone, skipped, channel) # channel value present to keep interface uniform but unused + # A default method for constructing wire messages with no data. + # Returns a (mutable) bytearray with just the status code in. # pylint: disable=unused-argument def as_bytes(self, channel=None): - """A default method for constructing wire messages with no data. - Returns a (mutable) bytearray with just status code in.""" + """Return the ``bytearray`` wire protocol representation of the object.""" return bytearray([self._STATUS]) # databytes value present to keep interface uniform but unused + # A default method for constructing message objects with no data. + # Returns the new object. # pylint: disable=unused-argument @classmethod def from_bytes(cls, databytes): - """A default method for constructing message objects with no data. - Returns the new object.""" + """Creates an object from the byte stream of the wire protocol + (not including the first status byte).""" return cls() diff --git a/adafruit_midi/note_off.py b/adafruit_midi/note_off.py index 26237ab..5104a46 100644 --- a/adafruit_midi/note_off.py +++ b/adafruit_midi/note_off.py @@ -20,10 +20,10 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. """ -`adafruit_midi` +`adafruit_midi.note_off` ================================================================================ -A CircuitPython helper for encoding/decoding MIDI packets over a MIDI or UART connection. +Note Off Change MIDI message. * Author(s): Kevin J. Walters @@ -31,15 +31,6 @@ Implementation Notes -------------------- -**Hardware:** - - - -**Software and Dependencies:** - -* Adafruit CircuitPython firmware for the supported boards: - https://github.com/adafruit/circuitpython/releases - """ from .midi_message import MIDIMessage, note_parser diff --git a/adafruit_midi/note_on.py b/adafruit_midi/note_on.py index 59ae70a..7bd2de7 100644 --- a/adafruit_midi/note_on.py +++ b/adafruit_midi/note_on.py @@ -20,10 +20,10 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. """ -`adafruit_midi` +`adafruit_midi.note_on` ================================================================================ -A CircuitPython helper for encoding/decoding MIDI packets over a MIDI or UART connection. +Note On Change MIDI message. * Author(s): Kevin J. Walters @@ -31,15 +31,6 @@ Implementation Notes -------------------- -**Hardware:** - - - -**Software and Dependencies:** - -* Adafruit CircuitPython firmware for the supported boards: - https://github.com/adafruit/circuitpython/releases - """ from .midi_message import MIDIMessage, note_parser diff --git a/adafruit_midi/pitch_bend_change.py b/adafruit_midi/pitch_bend_change.py index 4f23161..a927725 100644 --- a/adafruit_midi/pitch_bend_change.py +++ b/adafruit_midi/pitch_bend_change.py @@ -20,10 +20,10 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. """ -`adafruit_midi` +`adafruit_midi.pitch_bend_change` ================================================================================ -A CircuitPython helper for encoding/decoding MIDI packets over a MIDI or UART connection. +Pitch Bend Change MIDI message. * Author(s): Kevin J. Walters @@ -31,15 +31,6 @@ Implementation Notes -------------------- -**Hardware:** - - - -**Software and Dependencies:** - -* Adafruit CircuitPython firmware for the supported boards: - https://github.com/adafruit/circuitpython/releases - """ from .midi_message import MIDIMessage diff --git a/adafruit_midi/polyphonic_key_pressure.py b/adafruit_midi/polyphonic_key_pressure.py index 3b746e9..58d76fe 100644 --- a/adafruit_midi/polyphonic_key_pressure.py +++ b/adafruit_midi/polyphonic_key_pressure.py @@ -20,10 +20,10 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. """ -`adafruit_midi` +`adafruit_midi.polyphonic_key_pressure` ================================================================================ -A CircuitPython helper for encoding/decoding MIDI packets over a MIDI or UART connection. +Polyphonic Key Pressure MIDI message. * Author(s): Kevin J. Walters @@ -31,15 +31,6 @@ Implementation Notes -------------------- -**Hardware:** - - - -**Software and Dependencies:** - -* Adafruit CircuitPython firmware for the supported boards: - https://github.com/adafruit/circuitpython/releases - """ from .midi_message import MIDIMessage, note_parser diff --git a/adafruit_midi/program_change.py b/adafruit_midi/program_change.py index 8d1babe..c639106 100644 --- a/adafruit_midi/program_change.py +++ b/adafruit_midi/program_change.py @@ -20,10 +20,10 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. """ -`adafruit_midi` +`adafruit_midi.program_change` ================================================================================ -A CircuitPython helper for encoding/decoding MIDI packets over a MIDI or UART connection. +Program Change MIDI message. * Author(s): Kevin J. Walters @@ -31,15 +31,6 @@ Implementation Notes -------------------- -**Hardware:** - - - -**Software and Dependencies:** - -* Adafruit CircuitPython firmware for the supported boards: - https://github.com/adafruit/circuitpython/releases - """ from .midi_message import MIDIMessage diff --git a/adafruit_midi/start.py b/adafruit_midi/start.py index dfcc8f1..df487c6 100644 --- a/adafruit_midi/start.py +++ b/adafruit_midi/start.py @@ -20,10 +20,10 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. """ -`adafruit_midi` +`adafruit_midi.start` ================================================================================ -A CircuitPython helper for encoding/decoding MIDI packets over a MIDI or UART connection. +Start MIDI message. * Author(s): Kevin J. Walters @@ -31,15 +31,6 @@ Implementation Notes -------------------- -**Hardware:** - - - -**Software and Dependencies:** - -* Adafruit CircuitPython firmware for the supported boards: - https://github.com/adafruit/circuitpython/releases - """ from .midi_message import MIDIMessage diff --git a/adafruit_midi/stop.py b/adafruit_midi/stop.py index 75b8b41..830a18a 100644 --- a/adafruit_midi/stop.py +++ b/adafruit_midi/stop.py @@ -20,10 +20,10 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. """ -`adafruit_midi` +`adafruit_midi.stop` ================================================================================ -A CircuitPython helper for encoding/decoding MIDI packets over a MIDI or UART connection. +Stop MIDI message. * Author(s): Kevin J. Walters @@ -31,15 +31,6 @@ Implementation Notes -------------------- -**Hardware:** - - - -**Software and Dependencies:** - -* Adafruit CircuitPython firmware for the supported boards: - https://github.com/adafruit/circuitpython/releases - """ from .midi_message import MIDIMessage diff --git a/adafruit_midi/system_exclusive.py b/adafruit_midi/system_exclusive.py index 8f4290b..b5457ae 100644 --- a/adafruit_midi/system_exclusive.py +++ b/adafruit_midi/system_exclusive.py @@ -20,10 +20,10 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. """ -`adafruit_midi` +`adafruit_midi.system_exclusive` ================================================================================ -A CircuitPython helper for encoding/decoding MIDI packets over a MIDI or UART connection. +System Exclusive MIDI message. * Author(s): Kevin J. Walters @@ -31,15 +31,6 @@ Implementation Notes -------------------- -**Hardware:** - - - -**Software and Dependencies:** - -* Adafruit CircuitPython firmware for the supported boards: - https://github.com/adafruit/circuitpython/releases - """ from .midi_message import MIDIMessage @@ -54,6 +45,8 @@ class SystemExclusive(MIDIMessage): :param list manufacturer_id: The single byte or three byte manufacturer's id as a list or bytearray of numbers between 0-127. :param list data: The 7bit data as a list or bytearray of numbers between 0-127. + + This message can only be parsed if it fits within the input buffer in :class:MIDI. """ _STATUS = 0xf0 diff --git a/adafruit_midi/timing_clock.py b/adafruit_midi/timing_clock.py index 3650def..f00ff19 100644 --- a/adafruit_midi/timing_clock.py +++ b/adafruit_midi/timing_clock.py @@ -20,10 +20,10 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. """ -`adafruit_midi` +`adafruit_midi.timing_clock` ================================================================================ -A CircuitPython helper for encoding/decoding MIDI packets over a MIDI or UART connection. +Timing Clock MIDI message. * Author(s): Kevin J. Walters @@ -31,15 +31,6 @@ Implementation Notes -------------------- -**Hardware:** - - - -**Software and Dependencies:** - -* Adafruit CircuitPython firmware for the supported boards: - https://github.com/adafruit/circuitpython/releases - """ from .midi_message import MIDIMessage From 9bfbd6cfa6251fd2a22b8deee9d7f28a110591aa Mon Sep 17 00:00:00 2001 From: Kevin J Walters Date: Tue, 2 Apr 2019 00:14:14 +0100 Subject: [PATCH 70/92] Missing one correction of the package names in Spinx/reST docs in midi_message.py. #3 --- adafruit_midi/midi_message.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/adafruit_midi/midi_message.py b/adafruit_midi/midi_message.py index 32a6014..3079aac 100644 --- a/adafruit_midi/midi_message.py +++ b/adafruit_midi/midi_message.py @@ -20,7 +20,7 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. """ -`adafruit_midi.midi_message.MIDIMessage` +`adafruit_midi.midi_message` ================================================================================ An abstract class for objects which represent MIDI messages (events). From d0af077a8809c069dee6dbd863bfbcdbda980e8b Mon Sep 17 00:00:00 2001 From: Kevin J Walters Date: Tue, 2 Apr 2019 00:16:14 +0100 Subject: [PATCH 71/92] Missing one correction of the package names in Spinx/reST docs in midi_message.py. #3 --- adafruit_midi/midi_message.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/adafruit_midi/midi_message.py b/adafruit_midi/midi_message.py index 32a6014..3079aac 100644 --- a/adafruit_midi/midi_message.py +++ b/adafruit_midi/midi_message.py @@ -20,7 +20,7 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. """ -`adafruit_midi.midi_message.MIDIMessage` +`adafruit_midi.midi_message` ================================================================================ An abstract class for objects which represent MIDI messages (events). From 27ada485b719a91b5067e5068faa0efec36aa971 Mon Sep 17 00:00:00 2001 From: Kevin J Walters Date: Tue, 2 Apr 2019 11:39:01 +0100 Subject: [PATCH 72/92] Correcting a username in comments. #3 --- tests/test_MIDIMessage_unittests.py | 2 +- tests/test_MIDI_unittests.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_MIDIMessage_unittests.py b/tests/test_MIDIMessage_unittests.py index 493c76c..16aa285 100644 --- a/tests/test_MIDIMessage_unittests.py +++ b/tests/test_MIDIMessage_unittests.py @@ -31,7 +31,7 @@ import sys sys.modules['usb_midi'] = MagicMock() -# Borrowing the dhlalbert/tannewt technique from adafruit/Adafruit_CircuitPython_Motor +# Borrowing the dhalbert/tannewt technique from adafruit/Adafruit_CircuitPython_Motor sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) # Import before messages - opposite to other test file diff --git a/tests/test_MIDI_unittests.py b/tests/test_MIDI_unittests.py index 1974790..f63d350 100644 --- a/tests/test_MIDI_unittests.py +++ b/tests/test_MIDI_unittests.py @@ -31,7 +31,7 @@ import sys sys.modules['usb_midi'] = MagicMock() -# Borrowing the dhlalbert/tannewt technique from adafruit/Adafruit_CircuitPython_Motor +# Borrowing the dhalbert/tannewt technique from adafruit/Adafruit_CircuitPython_Motor sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) # Full monty From 83d3a2aed4602111cbe2c216bcb0cd681660c1c7 Mon Sep 17 00:00:00 2001 From: Kevin J Walters Date: Tue, 2 Apr 2019 11:42:20 +0100 Subject: [PATCH 73/92] Adding unit tests specific to note_parser. Fixing bug with note_parser - A/B were in wrong octave!! #3 --- adafruit_midi/midi_message.py | 6 +-- tests/test_note_parser.py | 93 +++++++++++++++++++++++++++++++++++ 2 files changed, 96 insertions(+), 3 deletions(-) create mode 100644 tests/test_note_parser.py diff --git a/adafruit_midi/midi_message.py b/adafruit_midi/midi_message.py index 3079aac..a20b6ea 100644 --- a/adafruit_midi/midi_message.py +++ b/adafruit_midi/midi_message.py @@ -46,9 +46,9 @@ # represent all of the sixteen channels ALL_CHANNELS = -1 -# From C3 -# Semitones A B C D E F G -NOTE_OFFSET = [9, 11, 12, 14, 16, 17, 19] +# From C3 - A and B are above G +# Semitones A B C D E F G +NOTE_OFFSET = [21, 23, 12, 14, 16, 17, 19] # pylint: disable=no-else-return def channel_filter(channel, channel_spec): diff --git a/tests/test_note_parser.py b/tests/test_note_parser.py new file mode 100644 index 0000000..6384c56 --- /dev/null +++ b/tests/test_note_parser.py @@ -0,0 +1,93 @@ +# The MIT License (MIT) +# +# Copyright (c) 2019 Kevin J. Walters +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import unittest +from unittest.mock import Mock, MagicMock, call + +import random +import os +verbose = int(os.getenv('TESTVERBOSE', '2')) + +# adafruit_midi has an import usb_midi +import sys +sys.modules['usb_midi'] = MagicMock() + +# Borrowing the dhalbert/tannewt technique from adafruit/Adafruit_CircuitPython_Motor +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) + +from adafruit_midi.midi_message import note_parser + + +class Test_note_parser(unittest.TestCase): + def text_int_passthru(self): + self.assertEqual(note_parser(0), 0) + self.assertEqual(note_parser(70), 70) + self.assertEqual(note_parser(127), 127) + + # it does not range check so these should pass + self.assertEqual(note_parser(-303), -303) + self.assertEqual(note_parser(808), 808) + + def test_good_text(self): + note_prefix = { "Cb": 11, + "C" : 12, + "C#": 13, + "Db": 13, + "D" : 14, + "D#": 15, + "Eb": 15, + "E" : 16, + "Fb": 16, + "E#": 17, + "F" : 17, + "F#": 18, + "Gb": 18, + "G" : 19, + "G#": 20, + "Ab": 20, + "A" : 21, + "A#": 22, + "Bb": 22, + "B" : 23, + "B#": 24, + } + + # test from Cb0 to B#8 + for prefix, base_value in note_prefix.items(): + for octave in range(9): + note = prefix + str(octave) + expected_value = base_value + octave * 12 # 12 semitones in octave + self.assertEqual(note_parser(note), expected_value) + + # re-test with simple C4/A4 tests to catch any bugs in above + self.assertEqual(note_parser("C4"), 60) + self.assertEqual(note_parser("A4"), 69) + + def test_bad_text(self): + + for text_note in ["H", "H4", "asdfasdfasdf", "000", "999"]: + with self.assertRaises(ValueError): + note_parser(text_note) + + +if __name__ == '__main__': + unittest.main(verbosity=verbose) From 6f2aecbf91c27c94746efe74c368d34e7274286e Mon Sep 17 00:00:00 2001 From: Kevin J Walters Date: Tue, 2 Apr 2019 14:22:45 +0100 Subject: [PATCH 74/92] Going back to previous .travis.yml as it has broken. #3 --- .travis.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index c9b8e81..87d4926 100644 --- a/.travis.yml +++ b/.travis.yml @@ -44,9 +44,9 @@ install: - pip install --force-reinstall pylint==1.9.2 script: - - [[ ! -d "tests" ]] || py.test + - ([[ -d "tests" ]] && py.test) - pylint adafruit_midi/*.py - - [[ ! -d "tests" ]] || pylint --disable=missing-docstring,invalid-name,bad-whitespace,trailing-whitespace,line-too-long,wrong-import-position,unused-import tests/*.py - - [[ ! -d "examples" ]] || pylint --disable=missing-docstring,invalid-name,bad-whitespace examples/*.py + - ([[ -d "tests" ]] && pylint --disable=missing-docstring,invalid-name,bad-whitespace,trailing-whitespace,line-too-long,wrong-import-position,unused-import tests/*.py) + - ([[ ! -d "examples" ]] || pylint --disable=missing-docstring,invalid-name,bad-whitespace examples/*.py) - circuitpython-build-bundles --filename_prefix adafruit-circuitpython-midi --library_location . - - ( cd docs && sphinx-build -E -W -b html . _build/html ) + - cd docs && sphinx-build -E -W -b html . _build/html && cd .. From 6676f9f068eb2ba475291dd7348ff44ab54410a0 Mon Sep 17 00:00:00 2001 From: Kevin J Walters Date: Tue, 2 Apr 2019 14:27:58 +0100 Subject: [PATCH 75/92] Corrected the current example in README.rst. This does not yet show the new features. #3 --- README.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.rst b/README.rst index e1c9af3..bc5bdb7 100644 --- a/README.rst +++ b/README.rst @@ -68,11 +68,11 @@ Usage Example print("Listening on input channel:", midi.in_channel) while True: - midi.note_on(44, 120) - midi.note_off(44, 120) - midi.control_change(3, 44) - midi.pitch_bend(random.randint(0,16383)) - time.sleep(1) + midi.note_on(44, 120) + midi.note_off(44, 120) + midi.control_change(3, 44) + midi.pitch_bend(random.randint(0,16383)) + time.sleep(1) Contributing From 14d69d3808de70a1605952e67c1b2ae711e7947b Mon Sep 17 00:00:00 2001 From: Kevin J Walters Date: Tue, 2 Apr 2019 14:33:06 +0100 Subject: [PATCH 76/92] Correcting pylint inhibition (missing colon!) on a simplifiable-if-statement which is not quite what pylint thinks it is. #3 --- adafruit_midi/midi_message.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/adafruit_midi/midi_message.py b/adafruit_midi/midi_message.py index a20b6ea..411b60f 100644 --- a/adafruit_midi/midi_message.py +++ b/adafruit_midi/midi_message.py @@ -147,7 +147,7 @@ def _search_eom_status(cls, buf, eom_status, msgstartidx, msgendidxplusone, endi # Look for a status byte # Second rule of the MIDI club is status bytes have MSB set if buf[msgendidxplusone] & 0x80: - # pylint disable=simplifiable-if-statement + # pylint: disable=simplifiable-if-statement if buf[msgendidxplusone] == eom_status: good_termination = True else: From 047d1ceba39af813075606c0b68627db1d16f93e Mon Sep 17 00:00:00 2001 From: Kevin J Walters Date: Tue, 2 Apr 2019 16:08:46 +0100 Subject: [PATCH 77/92] Forcing the channel argument to as_bytes() in MIDIMessage and its children to be specified as a named (non-positional) arg. This is the variable that is ignored by some children. #3 --- adafruit_midi/channel_pressure.py | 2 +- adafruit_midi/control_change.py | 2 +- adafruit_midi/midi_message.py | 2 +- adafruit_midi/note_off.py | 2 +- adafruit_midi/note_on.py | 2 +- adafruit_midi/pitch_bend_change.py | 2 +- adafruit_midi/polyphonic_key_pressure.py | 2 +- adafruit_midi/program_change.py | 2 +- adafruit_midi/system_exclusive.py | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/adafruit_midi/channel_pressure.py b/adafruit_midi/channel_pressure.py index 521d024..5868c8c 100644 --- a/adafruit_midi/channel_pressure.py +++ b/adafruit_midi/channel_pressure.py @@ -56,7 +56,7 @@ def __init__(self, pressure): raise self._EX_VALUEERROR_OOR # channel value is mandatory - def as_bytes(self, channel=None): + def as_bytes(self, *, channel=None): return bytearray([self._STATUS | (channel & self.CHANNELMASK), self.pressure]) diff --git a/adafruit_midi/control_change.py b/adafruit_midi/control_change.py index 0212805..4c6eb38 100644 --- a/adafruit_midi/control_change.py +++ b/adafruit_midi/control_change.py @@ -59,7 +59,7 @@ def __init__(self, control, value): raise self._EX_VALUEERROR_OOR # channel value is mandatory - def as_bytes(self, channel=None): + def as_bytes(self, *, channel=None): return bytearray([self._STATUS | (channel & self.CHANNELMASK), self.control, self.value]) diff --git a/adafruit_midi/midi_message.py b/adafruit_midi/midi_message.py index 411b60f..e77447a 100644 --- a/adafruit_midi/midi_message.py +++ b/adafruit_midi/midi_message.py @@ -280,7 +280,7 @@ def from_message_bytes(cls, midibytes, channel_in): # A default method for constructing wire messages with no data. # Returns a (mutable) bytearray with just the status code in. # pylint: disable=unused-argument - def as_bytes(self, channel=None): + def as_bytes(self, *, channel=None): """Return the ``bytearray`` wire protocol representation of the object.""" return bytearray([self._STATUS]) diff --git a/adafruit_midi/note_off.py b/adafruit_midi/note_off.py index 5104a46..c3099a6 100644 --- a/adafruit_midi/note_off.py +++ b/adafruit_midi/note_off.py @@ -60,7 +60,7 @@ def __init__(self, note, velocity): raise self._EX_VALUEERROR_OOR # channel value is mandatory - def as_bytes(self, channel=None): + def as_bytes(self, *, channel=None): return bytearray([self._STATUS | (channel & self.CHANNELMASK), self.note, self.velocity]) diff --git a/adafruit_midi/note_on.py b/adafruit_midi/note_on.py index 7bd2de7..1172aab 100644 --- a/adafruit_midi/note_on.py +++ b/adafruit_midi/note_on.py @@ -60,7 +60,7 @@ def __init__(self, note, velocity): raise self._EX_VALUEERROR_OOR # channel value is mandatory - def as_bytes(self, channel=None): + def as_bytes(self, *, channel=None): return bytearray([self._STATUS | (channel & self.CHANNELMASK), self.note, self.velocity]) diff --git a/adafruit_midi/pitch_bend_change.py b/adafruit_midi/pitch_bend_change.py index a927725..50f1876 100644 --- a/adafruit_midi/pitch_bend_change.py +++ b/adafruit_midi/pitch_bend_change.py @@ -57,7 +57,7 @@ def __init__(self, pitch_bend): raise self._EX_VALUEERROR_OOR # channel value is mandatory - def as_bytes(self, channel=None): + def as_bytes(self, *, channel=None): return bytearray([self._STATUS | (channel & self.CHANNELMASK), self.pitch_bend & 0x7f, (self.pitch_bend >> 7) & 0x7f]) diff --git a/adafruit_midi/polyphonic_key_pressure.py b/adafruit_midi/polyphonic_key_pressure.py index 58d76fe..a26f46d 100644 --- a/adafruit_midi/polyphonic_key_pressure.py +++ b/adafruit_midi/polyphonic_key_pressure.py @@ -59,7 +59,7 @@ def __init__(self, note, pressure): raise self._EX_VALUEERROR_OOR # channel value is mandatory - def as_bytes(self, channel=None): + def as_bytes(self, *, channel=None): return bytearray([self._STATUS | (channel & self.CHANNELMASK), self.note, self.pressure]) diff --git a/adafruit_midi/program_change.py b/adafruit_midi/program_change.py index c639106..f4ae409 100644 --- a/adafruit_midi/program_change.py +++ b/adafruit_midi/program_change.py @@ -56,7 +56,7 @@ def __init__(self, patch): raise self._EX_VALUEERROR_OOR # channel value is mandatory - def as_bytes(self, channel=None): + def as_bytes(self, *, channel=None): return bytearray([self._STATUS | (channel & self.CHANNELMASK), self.patch]) diff --git a/adafruit_midi/system_exclusive.py b/adafruit_midi/system_exclusive.py index b5457ae..1d63be1 100644 --- a/adafruit_midi/system_exclusive.py +++ b/adafruit_midi/system_exclusive.py @@ -59,7 +59,7 @@ def __init__(self, manufacturer_id, data): self.data = bytearray(data) # channel value present to keep interface uniform but unused - def as_bytes(self, channel=None): + def as_bytes(self, *, channel=None): return (bytearray([self._STATUS]) + self.manufacturer_id + self.data From 5d0024de39cae9a926e1efca86a5eef46737d985 Mon Sep 17 00:00:00 2001 From: Kevin J Walters Date: Tue, 2 Apr 2019 23:15:33 +0100 Subject: [PATCH 78/92] Renaming PitchBendChange to PitchBend. #3 --- adafruit_midi/pitch_bend_change.py | 4 ++-- examples/midi_inoutdemo.py | 2 +- examples/midi_simpletest2.py | 4 ++-- tests/test_MIDIMessage_unittests.py | 2 +- tests/test_MIDI_unittests.py | 10 +++++----- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/adafruit_midi/pitch_bend_change.py b/adafruit_midi/pitch_bend_change.py index 50f1876..704e36f 100644 --- a/adafruit_midi/pitch_bend_change.py +++ b/adafruit_midi/pitch_bend_change.py @@ -39,7 +39,7 @@ __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_MIDI.git" -class PitchBendChange(MIDIMessage): +class PitchBend(MIDIMessage): """Pitch Bend Change MIDI message. :param int pitch_bend: A 14bit unsigned int representing the degree of @@ -66,4 +66,4 @@ def as_bytes(self, *, channel=None): def from_bytes(cls, databytes): return cls(databytes[1] << 7 | databytes[0]) -PitchBendChange.register_message_type() +PitchBend.register_message_type() diff --git a/examples/midi_inoutdemo.py b/examples/midi_inoutdemo.py index deb3914..2b19218 100644 --- a/examples/midi_inoutdemo.py +++ b/examples/midi_inoutdemo.py @@ -12,7 +12,7 @@ from adafruit_midi.control_change import ControlChange from adafruit_midi.note_off import NoteOff from adafruit_midi.note_on import NoteOn -from adafruit_midi.pitch_bend_change import PitchBendChange +from adafruit_midi.pitch_bend_change import PitchBend from adafruit_midi.polyphonic_key_pressure import PolyphonicKeyPressure from adafruit_midi.program_change import ProgramChange from adafruit_midi.start import Start diff --git a/examples/midi_simpletest2.py b/examples/midi_simpletest2.py index bada06d..2ce7b98 100644 --- a/examples/midi_simpletest2.py +++ b/examples/midi_simpletest2.py @@ -5,7 +5,7 @@ from adafruit_midi.control_change import ControlChange from adafruit_midi.note_off import NoteOff from adafruit_midi.note_on import NoteOn -from adafruit_midi.pitch_bend_change import PitchBendChange +from adafruit_midi.pitch_bend_change import PitchBend midi = adafruit_midi.MIDI(out_channel=0) @@ -28,7 +28,7 @@ # send message(s) interface midi.send(NoteOn(44, 120)) time.sleep(0.25) - midi.send(PitchBendChange(random.randint(0, 16383))) + midi.send(PitchBend(random.randint(0, 16383))) time.sleep(0.25) midi.send([NoteOff("G#2", 120), ControlChange(3, 44)]) diff --git a/tests/test_MIDIMessage_unittests.py b/tests/test_MIDIMessage_unittests.py index 16aa285..6d6d352 100644 --- a/tests/test_MIDIMessage_unittests.py +++ b/tests/test_MIDIMessage_unittests.py @@ -42,7 +42,7 @@ from adafruit_midi.control_change import ControlChange from adafruit_midi.note_off import NoteOff from adafruit_midi.note_on import NoteOn -from adafruit_midi.pitch_bend_change import PitchBendChange +from adafruit_midi.pitch_bend_change import PitchBend from adafruit_midi.polyphonic_key_pressure import PolyphonicKeyPressure from adafruit_midi.program_change import ProgramChange from adafruit_midi.start import Start diff --git a/tests/test_MIDI_unittests.py b/tests/test_MIDI_unittests.py index f63d350..007cde1 100644 --- a/tests/test_MIDI_unittests.py +++ b/tests/test_MIDI_unittests.py @@ -39,7 +39,7 @@ from adafruit_midi.control_change import ControlChange from adafruit_midi.note_off import NoteOff from adafruit_midi.note_on import NoteOn -from adafruit_midi.pitch_bend_change import PitchBendChange +from adafruit_midi.pitch_bend_change import PitchBend from adafruit_midi.polyphonic_key_pressure import PolyphonicKeyPressure from adafruit_midi.program_change import ProgramChange from adafruit_midi.start import Start @@ -163,7 +163,7 @@ def test_captured_data_one_byte_reads(self): (msg, channel) = m.receive() if msg is not None: break - self.assertIsInstance(msg, PitchBendChange) + self.assertIsInstance(msg, PitchBend) self.assertEqual(msg.pitch_bend, 8195) self.assertEqual(channel, c) @@ -221,7 +221,7 @@ def test_somegood_somemissing_databytes(self): self.assertEqual(channel1, c) (msg2, channel2) = m.receive() - self.assertIsInstance(msg2, PitchBendChange) + self.assertIsInstance(msg2, PitchBend) self.assertEqual(msg2.pitch_bend, 8306) self.assertEqual(channel2, c) @@ -234,7 +234,7 @@ def test_somegood_somemissing_databytes(self): self.assertEqual(channel3, c) #(msg4, channel4) = m.receive() - #self.assertIsInstance(msg4, PitchBendChange) + #self.assertIsInstance(msg4, PitchBend) #self.assertEqual(msg4.pitch_bend, 72) #self.assertEqual(channel4, c) @@ -311,7 +311,7 @@ def test_larger_than_buffer_sysex(self): self.assertIsNone(channel3) #(msg4, channel4) = m.receive() - #self.assertIsInstance(msg4, PitchBendChange) + #self.assertIsInstance(msg4, PitchBend) #self.assertEqual(msg4.pitch_bend, 72) #self.assertEqual(channel4, c) From e3a9ebea4153be568d4585df2e836c3d7f52b936 Mon Sep 17 00:00:00 2001 From: Kevin J Walters Date: Tue, 2 Apr 2019 23:21:01 +0100 Subject: [PATCH 79/92] Renaming pitch_bend_change file to pitch_bend (part II of previous commit). #3 --- adafruit_midi/{pitch_bend_change.py => pitch_bend.py} | 2 +- docs/api.rst | 2 +- examples/midi_inoutdemo.py | 2 +- examples/midi_simpletest2.py | 2 +- tests/test_MIDIMessage_unittests.py | 2 +- tests/test_MIDI_unittests.py | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) rename adafruit_midi/{pitch_bend_change.py => pitch_bend.py} (98%) diff --git a/adafruit_midi/pitch_bend_change.py b/adafruit_midi/pitch_bend.py similarity index 98% rename from adafruit_midi/pitch_bend_change.py rename to adafruit_midi/pitch_bend.py index 704e36f..7eb2850 100644 --- a/adafruit_midi/pitch_bend_change.py +++ b/adafruit_midi/pitch_bend.py @@ -20,7 +20,7 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. """ -`adafruit_midi.pitch_bend_change` +`adafruit_midi.pitch_bend` ================================================================================ Pitch Bend Change MIDI message. diff --git a/docs/api.rst b/docs/api.rst index aa78784..98b27ca 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -22,7 +22,7 @@ .. automodule:: adafruit_midi.note_on :members: -.. automodule:: adafruit_midi.pitch_bend_change +.. automodule:: adafruit_midi.pitch_bend :members: .. automodule:: adafruit_midi.polyphonic_key_pressure diff --git a/examples/midi_inoutdemo.py b/examples/midi_inoutdemo.py index 2b19218..3022cd0 100644 --- a/examples/midi_inoutdemo.py +++ b/examples/midi_inoutdemo.py @@ -12,7 +12,7 @@ from adafruit_midi.control_change import ControlChange from adafruit_midi.note_off import NoteOff from adafruit_midi.note_on import NoteOn -from adafruit_midi.pitch_bend_change import PitchBend +from adafruit_midi.pitch_bend import PitchBend from adafruit_midi.polyphonic_key_pressure import PolyphonicKeyPressure from adafruit_midi.program_change import ProgramChange from adafruit_midi.start import Start diff --git a/examples/midi_simpletest2.py b/examples/midi_simpletest2.py index 2ce7b98..0d73680 100644 --- a/examples/midi_simpletest2.py +++ b/examples/midi_simpletest2.py @@ -5,7 +5,7 @@ from adafruit_midi.control_change import ControlChange from adafruit_midi.note_off import NoteOff from adafruit_midi.note_on import NoteOn -from adafruit_midi.pitch_bend_change import PitchBend +from adafruit_midi.pitch_bend import PitchBend midi = adafruit_midi.MIDI(out_channel=0) diff --git a/tests/test_MIDIMessage_unittests.py b/tests/test_MIDIMessage_unittests.py index 6d6d352..f4e3a09 100644 --- a/tests/test_MIDIMessage_unittests.py +++ b/tests/test_MIDIMessage_unittests.py @@ -42,7 +42,7 @@ from adafruit_midi.control_change import ControlChange from adafruit_midi.note_off import NoteOff from adafruit_midi.note_on import NoteOn -from adafruit_midi.pitch_bend_change import PitchBend +from adafruit_midi.pitch_bend import PitchBend from adafruit_midi.polyphonic_key_pressure import PolyphonicKeyPressure from adafruit_midi.program_change import ProgramChange from adafruit_midi.start import Start diff --git a/tests/test_MIDI_unittests.py b/tests/test_MIDI_unittests.py index 007cde1..2dce29d 100644 --- a/tests/test_MIDI_unittests.py +++ b/tests/test_MIDI_unittests.py @@ -39,7 +39,7 @@ from adafruit_midi.control_change import ControlChange from adafruit_midi.note_off import NoteOff from adafruit_midi.note_on import NoteOn -from adafruit_midi.pitch_bend_change import PitchBend +from adafruit_midi.pitch_bend import PitchBend from adafruit_midi.polyphonic_key_pressure import PolyphonicKeyPressure from adafruit_midi.program_change import ProgramChange from adafruit_midi.start import Start From 4ce502cdafd001d4797597dd413d65be1506bb56 Mon Sep 17 00:00:00 2001 From: Kevin J Walters Date: Fri, 5 Apr 2019 15:10:27 +0100 Subject: [PATCH 80/92] Correcting the docs on two MIDI properties for channels based on @tannewt feedback from #9. #3. --- adafruit_midi/__init__.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/adafruit_midi/__init__.py b/adafruit_midi/__init__.py index 924bc60..0083f54 100644 --- a/adafruit_midi/__init__.py +++ b/adafruit_midi/__init__.py @@ -91,9 +91,9 @@ def __init__(self, midi_in=usb_midi.ports[0], midi_out=usb_midi.ports[1], *, @property def in_channel(self): """The incoming MIDI channel. Must be 0-15. Correlates to MIDI channels 1-16, e.g. - ``in_channel(3)`` will listen on MIDI channel 4. - Can also listen on multiple channels, e.g. ``in_channel((0,1,2))`` - will listen on MIDI channels 1-3 or ``in_channel("ALL")`` for every channel. + ``in_channel = 3`` will listen on MIDI channel 4. + Can also listen on multiple channels, e.g. ``in_channel = (0,1,2)`` + will listen on MIDI channels 1-3 or ``in_channel = "ALL"`` for every channel. Default is None.""" return self._in_channel @@ -112,7 +112,7 @@ def in_channel(self, channel): @property def out_channel(self): """The outgoing MIDI channel. Must be 0-15. Correlates to MIDI channels 1-16, e.g. - ``out_channel(3)`` will send to MIDI channel 4. Default is 0.""" + ``out_channel = 3`` will send to MIDI channel 4. Default is 0 (MIDI channel 1).""" return self._out_channel # pylint: disable=attribute-defined-outside-init From dfb307f8bdd3297009d6d35d56cd519156f57742 Mon Sep 17 00:00:00 2001 From: Kevin J Walters Date: Fri, 5 Apr 2019 15:16:46 +0100 Subject: [PATCH 81/92] Alternative method of shutting up pylint when instance variables are set with properties based on @tannewt feedback from #9. #3. --- adafruit_midi/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/adafruit_midi/__init__.py b/adafruit_midi/__init__.py index 0083f54..091a9be 100644 --- a/adafruit_midi/__init__.py +++ b/adafruit_midi/__init__.py @@ -79,7 +79,9 @@ def __init__(self, midi_in=usb_midi.ports[0], midi_out=usb_midi.ports[1], *, in_channel=None, out_channel=0, in_buf_size=30, debug=False): self._midi_in = midi_in self._midi_out = midi_out + self._in_channel = in_channel # dealing with pylint inadequacy self.in_channel = in_channel + self._out_channel = out_channel # dealing with pylint inadequacy self.out_channel = out_channel self._debug = debug # This input buffer holds what has been read from midi_in @@ -97,7 +99,6 @@ def in_channel(self): Default is None.""" return self._in_channel - # pylint: disable=attribute-defined-outside-init @in_channel.setter def in_channel(self, channel): if channel is None or (isinstance(channel, int) and 0 <= channel <= 15): @@ -115,7 +116,6 @@ def out_channel(self): ``out_channel = 3`` will send to MIDI channel 4. Default is 0 (MIDI channel 1).""" return self._out_channel - # pylint: disable=attribute-defined-outside-init @out_channel.setter def out_channel(self, channel): if not 0 <= channel <= 15: From d78b0f02da11896931839f358f4b65c5654271fa Mon Sep 17 00:00:00 2001 From: Kevin J Walters Date: Fri, 5 Apr 2019 22:36:09 +0100 Subject: [PATCH 82/92] Converting to a MIDI message format which stores channel number as a property based on @tannewt feedback from #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. #3 --- adafruit_midi/__init__.py | 14 +- adafruit_midi/channel_pressure.py | 15 +- adafruit_midi/control_change.py | 16 +- adafruit_midi/midi_message.py | 84 ++++++---- adafruit_midi/note_off.py | 16 +- adafruit_midi/note_on.py | 16 +- adafruit_midi/pitch_bend.py | 18 +-- adafruit_midi/polyphonic_key_pressure.py | 16 +- adafruit_midi/program_change.py | 15 +- adafruit_midi/system_exclusive.py | 20 +-- tests/test_MIDIMessage_unittests.py | 80 ++++----- tests/test_MIDI_unittests.py | 198 ++++++++++++----------- 12 files changed, 268 insertions(+), 240 deletions(-) diff --git a/adafruit_midi/__init__.py b/adafruit_midi/__init__.py index 091a9be..c20b56e 100644 --- a/adafruit_midi/__init__.py +++ b/adafruit_midi/__init__.py @@ -127,8 +127,7 @@ def receive(self): and return the first MIDI message (event). This maintains the blocking characteristics of the midi_in port. - :returns (MIDIMessage object, int channel): Returns object and channel - or (None, None) for nothing. + :returns MIDIMessage object: Returns object or None for nothing. """ ### could check _midi_in is an object OR correct object OR correct interface here? # If the buffer here is not full then read as much as we can fit from @@ -142,7 +141,7 @@ def receive(self): del bytes_in (msg, endplusone, - skipped, channel) = MIDIMessage.from_message_bytes(self._in_buf, self._in_channel) + skipped) = MIDIMessage.from_message_bytes(self._in_buf, self._in_channel) if endplusone != 0: # This is not particularly efficient as it's copying most of bytearray # and deleting old one @@ -151,23 +150,26 @@ def receive(self): self._skipped_bytes += skipped # msg could still be None at this point, e.g. in middle of monster SysEx - return (msg, channel) + return msg def send(self, msg, channel=None): """Sends a MIDI message. :param msg: Either a MIDIMessage object or a sequence (list) of MIDIMessage objects. + The channel property will be *updated* as a side-effect of sending message(s). :param int channel: Channel number, if not set the ``out_channel`` will be used. """ if channel is None: channel = self.out_channel if isinstance(msg, MIDIMessage): - data = msg.as_bytes(channel=channel) + msg.channel = channel + data = bytes(msg) else: data = bytearray() for each_msg in msg: - data.extend(each_msg.as_bytes(channel=channel)) + each_msg.channel = channel + data.extend(bytes(each_msg)) self._send(data, len(data)) diff --git a/adafruit_midi/channel_pressure.py b/adafruit_midi/channel_pressure.py index 5868c8c..927e987 100644 --- a/adafruit_midi/channel_pressure.py +++ b/adafruit_midi/channel_pressure.py @@ -48,20 +48,19 @@ class ChannelPressure(MIDIMessage): _STATUS = 0xd0 _STATUSMASK = 0xf0 LENGTH = 2 - CHANNELMASK = 0x0f - def __init__(self, pressure): + def __init__(self, pressure, *, channel=None): self.pressure = pressure + super().__init__(channel=channel) if not 0 <= self.pressure <= 127: raise self._EX_VALUEERROR_OOR - # channel value is mandatory - def as_bytes(self, *, channel=None): - return bytearray([self._STATUS | (channel & self.CHANNELMASK), - self.pressure]) + def __bytes__(self): + return bytes([self._STATUS | (self.channel & self.CHANNELMASK), + self.pressure]) @classmethod - def from_bytes(cls, databytes): - return cls(databytes[0]) + def from_bytes(cls, msg_bytes): + return cls(msg_bytes[1], channel=msg_bytes[0] & cls.CHANNELMASK) ChannelPressure.register_message_type() diff --git a/adafruit_midi/control_change.py b/adafruit_midi/control_change.py index 4c6eb38..3c42d0d 100644 --- a/adafruit_midi/control_change.py +++ b/adafruit_midi/control_change.py @@ -50,21 +50,21 @@ class ControlChange(MIDIMessage): _STATUS = 0xb0 _STATUSMASK = 0xf0 LENGTH = 3 - CHANNELMASK = 0x0f - def __init__(self, control, value): + def __init__(self, control, value, *, channel=None): self.control = control self.value = value + super().__init__(channel=channel) if not 0 <= self.control <= 127 or not 0 <= self.value <= 127: raise self._EX_VALUEERROR_OOR - # channel value is mandatory - def as_bytes(self, *, channel=None): - return bytearray([self._STATUS | (channel & self.CHANNELMASK), - self.control, self.value]) + def __bytes__(self): + return bytes([self._STATUS | (self.channel & self.CHANNELMASK), + self.control, self.value]) @classmethod - def from_bytes(cls, databytes): - return cls(databytes[0], databytes[1]) + def from_bytes(cls, msg_bytes): + return cls(msg_bytes[1], msg_bytes[2], + channel=msg_bytes[0] & cls.CHANNELMASK) ControlChange.register_message_type() diff --git a/adafruit_midi/midi_message.py b/adafruit_midi/midi_message.py index e77447a..7d4a1e0 100644 --- a/adafruit_midi/midi_message.py +++ b/adafruit_midi/midi_message.py @@ -111,7 +111,7 @@ class MIDIMessage: _STATUS = None _STATUSMASK = None LENGTH = None - CHANNELMASK = None + CHANNELMASK = 0x0f ENDSTATUS = None # Commonly used exceptions to save memory @@ -121,6 +121,24 @@ class MIDIMessage: # order is more specific masks first _statusandmask_to_class = [] + def __init__(self, *, channel=None): + ### TODO - can i kwargs this????? + self._channel = channel # dealing with pylint inadequacy + self.channel = channel + + @property + def channel(self): + """The channel number of the MIDI message where appropriate. + This is *updated* by MIDI.send() method. + """ + return self._channel + + @channel.setter + def channel(self, channel): + if channel is not None and not 0 <= channel <= 15: + raise "channel must be 0-15 or None" + self._channel = channel + @classmethod def register_message_type(cls): """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 return (msgendidxplusone, good_termination, bad_termination) - # pylint: disable=too-many-arguments,too-many-locals @classmethod - def _match_message_status(cls, buf, channel_in, msgstartidx, msgendidxplusone, endidx): + def _match_message_status(cls, buf, msgstartidx, msgendidxplusone, endidx): msgclass = None status = buf[msgstartidx] known_msg = False complete_msg = False bad_termination = False - channel_match_orna = True - channel = None # Rummage through our list looking for a status match for status_mask, msgclass in MIDIMessage._statusandmask_to_class: @@ -183,10 +198,6 @@ def _match_message_status(cls, buf, channel_in, msgstartidx, msgendidxplusone, e if not complete_msg: break - if msgclass.CHANNELMASK is not None: - channel = status & msgclass.CHANNELMASK - channel_match_orna = channel_filter(channel, channel_in) - if msgclass.LENGTH < 0: # indicator of variable length message (msgendidxplusone, terminated_msg, @@ -203,26 +214,26 @@ def _match_message_status(cls, buf, channel_in, msgstartidx, msgendidxplusone, e return (msgclass, status, known_msg, complete_msg, bad_termination, - channel_match_orna, channel, msgendidxplusone) + msgendidxplusone) + # pylint: disable=too-many-locals,too-many-branches @classmethod def from_message_bytes(cls, midibytes, channel_in): """Create an appropriate object of the correct class for the - first message found in some MIDI bytes. + first message found in some MIDI bytes filtered by channel_in. - Returns (messageobject, endplusone, skipped, channel) + Returns (messageobject, endplusone, skipped) or for no messages, partial messages or messages for other channels - (None, endplusone, skipped, None). + (None, endplusone, skipped). """ - msg = None endidx = len(midibytes) - 1 skipped = 0 preamble = True - channel = None msgstartidx = 0 msgendidxplusone = 0 while True: + msg = None # Look for a status byte # Second rule of the MIDI club is status bytes have MSB set while msgstartidx <= endidx and not midibytes[msgstartidx] & 0x80: @@ -233,7 +244,7 @@ def from_message_bytes(cls, midibytes, channel_in): # Either no message or a partial one if msgstartidx > endidx: - return (None, endidx + 1, skipped, None) + return (None, endidx + 1, skipped) # Try and match the status byte found in midibytes (msgclass, @@ -241,19 +252,19 @@ def from_message_bytes(cls, midibytes, channel_in): known_message, complete_message, bad_termination, - channel_match_orna, - channel, msgendidxplusone) = cls._match_message_status(midibytes, - channel_in, msgstartidx, msgendidxplusone, endidx) - - if complete_message and not bad_termination and channel_match_orna: + channel_match_orna = True + if complete_message and not bad_termination: try: - msg = msgclass.from_bytes(midibytes[msgstartidx+1:msgendidxplusone]) + msg = msgclass.from_bytes(midibytes[msgstartidx:msgendidxplusone]) + if msg.channel is not None: + channel_match_orna = channel_filter(msg.channel, channel_in) + except(ValueError, TypeError) as ex: - msg = MIDIBadEvent(midibytes[msgstartidx+1:msgendidxplusone], ex) + msg = MIDIBadEvent(midibytes[msgstartidx:msgendidxplusone], ex) # break out of while loop for a complete message on good channel # or we have one we do not know about @@ -274,24 +285,23 @@ def from_message_bytes(cls, midibytes, channel_in): msgendidxplusone = msgstartidx + 1 break - return (msg, msgendidxplusone, skipped, channel) + return (msg, msgendidxplusone, skipped) - # channel value present to keep interface uniform but unused # A default method for constructing wire messages with no data. - # Returns a (mutable) bytearray with just the status code in. - # pylint: disable=unused-argument - def as_bytes(self, *, channel=None): - """Return the ``bytearray`` wire protocol representation of the object.""" - return bytearray([self._STATUS]) + # Returns an (immutable) bytes with just the status code in. + def __bytes__(self): + """Return the ``bytes`` wire protocol representation of the object + with channel number applied where appropriate.""" + return bytes([self._STATUS]) # databytes value present to keep interface uniform but unused # A default method for constructing message objects with no data. # Returns the new object. # pylint: disable=unused-argument @classmethod - def from_bytes(cls, databytes): + def from_bytes(cls, msg_bytes): """Creates an object from the byte stream of the wire protocol - (not including the first status byte).""" + representation of the MIDI message.""" return cls() @@ -308,18 +318,22 @@ class MIDIUnknownEvent(MIDIMessage): def __init__(self, status): self.status = status + super().__init__() class MIDIBadEvent(MIDIMessage): """A bad MIDI message, one that could not be parsed/constructed. - :param list data: The MIDI status number. + :param list data: The MIDI status including any embedded channel number + and associated subsequent data bytes. :param Exception exception: The exception used to store the repr() text representation. This could be due to status bytes appearing where data bytes are expected. + The channel property will not be set. """ LENGTH = -1 - def __init__(self, data, exception): - self.data = bytearray(data) + def __init__(self, msg_bytes, exception): + self.data = bytes(msg_bytes) self.exception_text = repr(exception) + super().__init__() diff --git a/adafruit_midi/note_off.py b/adafruit_midi/note_off.py index c3099a6..1f1b0cd 100644 --- a/adafruit_midi/note_off.py +++ b/adafruit_midi/note_off.py @@ -51,21 +51,21 @@ class NoteOff(MIDIMessage): _STATUS = 0x80 _STATUSMASK = 0xf0 LENGTH = 3 - CHANNELMASK = 0x0f - def __init__(self, note, velocity): + def __init__(self, note, velocity, *, channel=None): self.note = note_parser(note) self.velocity = velocity + super().__init__(channel=channel) if not 0 <= self.note <= 127 or not 0 <= self.velocity <= 127: raise self._EX_VALUEERROR_OOR - # channel value is mandatory - def as_bytes(self, *, channel=None): - return bytearray([self._STATUS | (channel & self.CHANNELMASK), - self.note, self.velocity]) + def __bytes__(self): + return bytes([self._STATUS | (self.channel & self.CHANNELMASK), + self.note, self.velocity]) @classmethod - def from_bytes(cls, databytes): - return cls(databytes[0], databytes[1]) + def from_bytes(cls, msg_bytes): + return cls(msg_bytes[1], msg_bytes[2], + channel=msg_bytes[0] & cls.CHANNELMASK) NoteOff.register_message_type() diff --git a/adafruit_midi/note_on.py b/adafruit_midi/note_on.py index 1172aab..9c933a3 100644 --- a/adafruit_midi/note_on.py +++ b/adafruit_midi/note_on.py @@ -51,21 +51,21 @@ class NoteOn(MIDIMessage): _STATUS = 0x90 _STATUSMASK = 0xf0 LENGTH = 3 - CHANNELMASK = 0x0f - def __init__(self, note, velocity): + def __init__(self, note, velocity, *, channel=None): self.note = note_parser(note) self.velocity = velocity + super().__init__(channel=channel) if not 0 <= self.note <= 127 or not 0 <= self.velocity <= 127: raise self._EX_VALUEERROR_OOR - # channel value is mandatory - def as_bytes(self, *, channel=None): - return bytearray([self._STATUS | (channel & self.CHANNELMASK), - self.note, self.velocity]) + def __bytes__(self): + return bytes([self._STATUS | (self.channel & self.CHANNELMASK), + self.note, self.velocity]) @classmethod - def from_bytes(cls, databytes): - return cls(databytes[0], databytes[1]) + def from_bytes(cls, msg_bytes): + return cls(msg_bytes[1], msg_bytes[2], + channel=msg_bytes[0] & cls.CHANNELMASK) NoteOn.register_message_type() diff --git a/adafruit_midi/pitch_bend.py b/adafruit_midi/pitch_bend.py index 7eb2850..ea85af6 100644 --- a/adafruit_midi/pitch_bend.py +++ b/adafruit_midi/pitch_bend.py @@ -49,21 +49,21 @@ class PitchBend(MIDIMessage): _STATUS = 0xe0 _STATUSMASK = 0xf0 LENGTH = 3 - CHANNELMASK = 0x0f - def __init__(self, pitch_bend): + def __init__(self, pitch_bend, *, channel=None): self.pitch_bend = pitch_bend + super().__init__(channel=channel) if not 0 <= self.pitch_bend <= 16383: raise self._EX_VALUEERROR_OOR - # channel value is mandatory - def as_bytes(self, *, channel=None): - return bytearray([self._STATUS | (channel & self.CHANNELMASK), - self.pitch_bend & 0x7f, - (self.pitch_bend >> 7) & 0x7f]) + def __bytes__(self): + return bytes([self._STATUS | (self.channel & self.CHANNELMASK), + self.pitch_bend & 0x7f, + (self.pitch_bend >> 7) & 0x7f]) @classmethod - def from_bytes(cls, databytes): - return cls(databytes[1] << 7 | databytes[0]) + def from_bytes(cls, msg_bytes): + return cls(msg_bytes[2] << 7 | msg_bytes[1], + channel=msg_bytes[0] & cls.CHANNELMASK) PitchBend.register_message_type() diff --git a/adafruit_midi/polyphonic_key_pressure.py b/adafruit_midi/polyphonic_key_pressure.py index a26f46d..6519ae2 100644 --- a/adafruit_midi/polyphonic_key_pressure.py +++ b/adafruit_midi/polyphonic_key_pressure.py @@ -50,21 +50,21 @@ class PolyphonicKeyPressure(MIDIMessage): _STATUS = 0xa0 _STATUSMASK = 0xf0 LENGTH = 3 - CHANNELMASK = 0x0f - def __init__(self, note, pressure): + def __init__(self, note, pressure, *, channel=None): self.note = note_parser(note) self.pressure = pressure + super().__init__(channel=channel) if not 0 <= self.note <= 127 or not 0 <= self.pressure <= 127: raise self._EX_VALUEERROR_OOR - # channel value is mandatory - def as_bytes(self, *, channel=None): - return bytearray([self._STATUS | (channel & self.CHANNELMASK), - self.note, self.pressure]) + def __bytes__(self): + return bytes([self._STATUS | (self.channel & self.CHANNELMASK), + self.note, self.pressure]) @classmethod - def from_bytes(cls, databytes): - return cls(databytes[0], databytes[1]) + def from_bytes(cls, msg_bytes): + return cls(msg_bytes[1], msg_bytes[2], + channel=msg_bytes[0] & cls.CHANNELMASK) PolyphonicKeyPressure.register_message_type() diff --git a/adafruit_midi/program_change.py b/adafruit_midi/program_change.py index f4ae409..5e5d078 100644 --- a/adafruit_midi/program_change.py +++ b/adafruit_midi/program_change.py @@ -48,20 +48,19 @@ class ProgramChange(MIDIMessage): _STATUS = 0xc0 _STATUSMASK = 0xf0 LENGTH = 2 - CHANNELMASK = 0x0f - def __init__(self, patch): + def __init__(self, patch, *, channel=None): self.patch = patch + super().__init__(channel=channel) if not 0 <= self.patch <= 127: raise self._EX_VALUEERROR_OOR - # channel value is mandatory - def as_bytes(self, *, channel=None): - return bytearray([self._STATUS | (channel & self.CHANNELMASK), - self.patch]) + def __bytes__(self): + return bytes([self._STATUS | (self.channel & self.CHANNELMASK), + self.patch]) @classmethod - def from_bytes(cls, databytes): - return cls(databytes[0]) + def from_bytes(cls, msg_bytes): + return cls(msg_bytes[1], channel=msg_bytes[0] & cls.CHANNELMASK) ProgramChange.register_message_type() diff --git a/adafruit_midi/system_exclusive.py b/adafruit_midi/system_exclusive.py index 1d63be1..b77e885 100644 --- a/adafruit_midi/system_exclusive.py +++ b/adafruit_midi/system_exclusive.py @@ -55,22 +55,22 @@ class SystemExclusive(MIDIMessage): ENDSTATUS = 0xf7 def __init__(self, manufacturer_id, data): - self.manufacturer_id = bytearray(manufacturer_id) - self.data = bytearray(data) + self.manufacturer_id = bytes(manufacturer_id) + self.data = bytes(data) + super().__init__() - # channel value present to keep interface uniform but unused - def as_bytes(self, *, channel=None): - return (bytearray([self._STATUS]) + def __bytes__(self): + return (bytes([self._STATUS]) + self.manufacturer_id + self.data - + bytearray([self.ENDSTATUS])) + + bytes([self.ENDSTATUS])) @classmethod - def from_bytes(cls, databytes): + def from_bytes(cls, msg_bytes): # -1 on second arg is to avoid the ENDSTATUS which is passed - if databytes[0] != 0: # pylint: disable=no-else-return - return cls(databytes[0:1], databytes[1:-1]) + if msg_bytes[1] != 0: # pylint: disable=no-else-return + return cls(msg_bytes[1:2], msg_bytes[2:-1]) else: - return cls(databytes[0:3], databytes[3:-1]) + return cls(msg_bytes[1:4], msg_bytes[4:-1]) SystemExclusive.register_message_type() diff --git a/tests/test_MIDIMessage_unittests.py b/tests/test_MIDIMessage_unittests.py index f4e3a09..11d51a2 100644 --- a/tests/test_MIDIMessage_unittests.py +++ b/tests/test_MIDIMessage_unittests.py @@ -42,7 +42,7 @@ from adafruit_midi.control_change import ControlChange from adafruit_midi.note_off import NoteOff from adafruit_midi.note_on import NoteOn -from adafruit_midi.pitch_bend import PitchBend +from adafruit_midi.pitch_bend import PitchBend from adafruit_midi.polyphonic_key_pressure import PolyphonicKeyPressure from adafruit_midi.program_change import ProgramChange from adafruit_midi.start import Start @@ -56,33 +56,32 @@ def test_NoteOn_basic(self): data = bytes([0x90, 0x30, 0x7f]) ichannel = 0 - (msg, msgendidxplusone, skipped, channel) = adafruit_midi.MIDIMessage.from_message_bytes(data, ichannel) + (msg, msgendidxplusone, skipped) = adafruit_midi.MIDIMessage.from_message_bytes(data, ichannel) self.assertIsInstance(msg, NoteOn) self.assertEqual(msg.note, 0x30) self.assertEqual(msg.velocity, 0x7f) self.assertEqual(msgendidxplusone, 3) self.assertEqual(skipped, 0) - self.assertEqual(channel, 0) + self.assertEqual(msg.channel, 0) def test_NoteOn_awaitingthirdbyte(self): data = bytes([0x90, 0x30]) ichannel = 0 - (msg, msgendidxplusone, skipped, channel) = adafruit_midi.MIDIMessage.from_message_bytes(data, ichannel) + (msg, msgendidxplusone, skipped) = adafruit_midi.MIDIMessage.from_message_bytes(data, ichannel) self.assertIsNone(msg) self.assertEqual(msgendidxplusone, skipped, "skipped must be 0 as it only indicates bytes before a status byte") self.assertEqual(msgendidxplusone, 0, "msgendidxplusone must be 0 as buffer must be lest as is for more data") self.assertEqual(skipped, 0) - self.assertIsNone(channel) def test_NoteOn_predatajunk(self): data = bytes([0x20, 0x64, 0x90, 0x30, 0x32]) ichannel = 0 - (msg, msgendidxplusone, skipped, channel) = adafruit_midi.MIDIMessage.from_message_bytes(data, ichannel) + (msg, msgendidxplusone, skipped) = adafruit_midi.MIDIMessage.from_message_bytes(data, ichannel) self.assertIsInstance(msg, NoteOn) self.assertEqual(msg.note, 0x30) @@ -90,13 +89,13 @@ def test_NoteOn_predatajunk(self): self.assertEqual(msgendidxplusone, 5, "data bytes from partial message and messages are removed" ) self.assertEqual(skipped, 2) - self.assertEqual(channel, 0) + self.assertEqual(msg.channel, 0) def test_NoteOn_prepartialsysex(self): data = bytes([0x01, 0x02, 0x03, 0x04, 0xf7, 0x90, 0x30, 0x32]) ichannel = 0 - (msg, msgendidxplusone, skipped, channel) = adafruit_midi.MIDIMessage.from_message_bytes(data, ichannel) + (msg, msgendidxplusone, skipped) = adafruit_midi.MIDIMessage.from_message_bytes(data, ichannel) # MIDIMessage parsing could be improved to return something that # indicates its a truncated end of SysEx @@ -104,10 +103,10 @@ def test_NoteOn_prepartialsysex(self): self.assertEqual(msg.status, 0xf7) self.assertEqual(msgendidxplusone, 5, "removal of the end of the partial SysEx data and terminating status byte") self.assertEqual(skipped, 4, "skipped only counts data bytes so will be 4 here") - self.assertIsNone(channel) + self.assertIsNone(msg.channel) data = data[msgendidxplusone:] - (msg, msgendidxplusone, skipped, channel) = adafruit_midi.MIDIMessage.from_message_bytes(data, ichannel) + (msg, msgendidxplusone, skipped) = adafruit_midi.MIDIMessage.from_message_bytes(data, ichannel) self.assertIsInstance(msg, NoteOn, "NoteOn is expected if SystemExclusive is loaded otherwise it would be MIDIUnknownEvent") @@ -115,26 +114,26 @@ def test_NoteOn_prepartialsysex(self): self.assertEqual(msg.velocity, 0x32) self.assertEqual(msgendidxplusone, 3, "NoteOn message removed") self.assertEqual(skipped, 0) - self.assertEqual(channel, 0) + self.assertEqual(msg.channel, 0) def test_NoteOn_postNoteOn(self): data = bytes([0x90 | 0x08, 0x30, 0x7f, 0x90 | 0x08, 0x37, 0x64]) ichannel = 8 - (msg, msgendidxplusone, skipped, channel) = adafruit_midi.MIDIMessage.from_message_bytes(data, ichannel) + (msg, msgendidxplusone, skipped) = adafruit_midi.MIDIMessage.from_message_bytes(data, ichannel) self.assertIsInstance(msg, NoteOn) self.assertEqual(msg.note, 0x30) self.assertEqual(msg.velocity, 0x7f) self.assertEqual(msgendidxplusone, 3) self.assertEqual(skipped, 0) - self.assertEqual(channel, 8) + self.assertEqual(msg.channel, 8) def test_NoteOn_postpartialNoteOn(self): data = bytes([0x90, 0x30, 0x7f, 0x90, 0x37]) ichannel = 0 - (msg, msgendidxplusone, skipped, channel) = adafruit_midi.MIDIMessage.from_message_bytes(data, ichannel) + (msg, msgendidxplusone, skipped) = adafruit_midi.MIDIMessage.from_message_bytes(data, ichannel) self.assertIsInstance(msg, NoteOn) self.assertEqual(msg.note, 0x30) @@ -142,13 +141,13 @@ def test_NoteOn_postpartialNoteOn(self): self.assertEqual(msgendidxplusone, 3, "Only first message is removed") self.assertEqual(skipped, 0) - self.assertEqual(channel, 0) + self.assertEqual(msg.channel, 0) def test_NoteOn_preotherchannel(self): data = bytes([0x90 | 0x05, 0x30, 0x7f, 0x90 | 0x03, 0x37, 0x64]) ichannel = 3 - (msg, msgendidxplusone, skipped, channel) = adafruit_midi.MIDIMessage.from_message_bytes(data, ichannel) + (msg, msgendidxplusone, skipped) = adafruit_midi.MIDIMessage.from_message_bytes(data, ichannel) self.assertIsInstance(msg, NoteOn) self.assertEqual(msg.note, 0x37) @@ -156,13 +155,13 @@ def test_NoteOn_preotherchannel(self): self.assertEqual(msgendidxplusone, 6, "Both messages are removed from buffer") self.assertEqual(skipped, 0) - self.assertEqual(channel, 3) + self.assertEqual(msg.channel, 3) def test_NoteOn_preotherchannelplusintermediatejunk(self): data = bytes([0x90 | 0x05, 0x30, 0x7f, 0x00, 0x00, 0x90 | 0x03, 0x37, 0x64]) ichannel = 3 - (msg, msgendidxplusone, skipped, channel) = adafruit_midi.MIDIMessage.from_message_bytes(data, ichannel) + (msg, msgendidxplusone, skipped) = adafruit_midi.MIDIMessage.from_message_bytes(data, ichannel) self.assertIsInstance(msg, NoteOn) self.assertEqual(msg.note, 0x37) @@ -170,60 +169,65 @@ def test_NoteOn_preotherchannelplusintermediatejunk(self): self.assertEqual(msgendidxplusone, 8, "Both messages and junk are removed from buffer") self.assertEqual(skipped, 0) - self.assertEqual(channel, 3) + self.assertEqual(msg.channel, 3) def test_NoteOn_wrongchannel(self): data = bytes([0x95, 0x30, 0x7f]) ichannel = 3 - (msg, msgendidxplusone, skipped, channel) = adafruit_midi.MIDIMessage.from_message_bytes(data, ichannel) + (msg, msgendidxplusone, skipped) = adafruit_midi.MIDIMessage.from_message_bytes(data, ichannel) self.assertIsNone(msg) self.assertEqual(msgendidxplusone, 3, "wrong channel message discarded") self.assertEqual(skipped, 0) - self.assertIsNone(channel) def test_NoteOn_partialandpreotherchannel1(self): data = bytes([0x95, 0x30, 0x7f, 0x93]) ichannel = 3 - (msg, msgendidxplusone, skipped, channel) = adafruit_midi.MIDIMessage.from_message_bytes(data, ichannel) + (msg, msgendidxplusone, skipped) = adafruit_midi.MIDIMessage.from_message_bytes(data, ichannel) self.assertIsNone(msg) self.assertEqual(msgendidxplusone, 3, "first message discarded, second partial left") self.assertEqual(skipped, 0) - self.assertIsNone(channel) def test_NoteOn_partialandpreotherchannel2(self): data = bytes([0x95, 0x30, 0x7f, 0x93, 0x37]) ichannel = 3 - (msg, msgendidxplusone, skipped, channel) = adafruit_midi.MIDIMessage.from_message_bytes(data, ichannel) + (msg, msgendidxplusone, skipped) = adafruit_midi.MIDIMessage.from_message_bytes(data, ichannel) self.assertIsNone(msg) self.assertEqual(msgendidxplusone, 3, "first message discarded, second partial left") self.assertEqual(skipped, 0) - self.assertIsNone(channel) def test_NoteOn_constructor_int(self): object1 = NoteOn(60, 0x7f) self.assertEqual(object1.note, 60) self.assertEqual(object1.velocity, 0x7f) + self.assertIsNone(object1.channel) object2 = NoteOn(60, 0x00) # equivalent of NoteOff - + self.assertEqual(object2.note, 60) self.assertEqual(object2.velocity, 0x00) - + self.assertIsNone(object2.channel) + + object3 = NoteOn(60, 0x50, channel=7) + + self.assertEqual(object3.note, 60) + self.assertEqual(object3.velocity, 0x50) + self.assertEqual(object3.channel, 7) + def test_SystemExclusive_NoteOn(self): data = bytes([0xf0, 0x42, 0x01, 0x02, 0x03, 0x04, 0xf7, 0x90 | 14, 0x30, 0x60]) ichannel = 14 - (msg, msgendidxplusone, skipped, channel) = adafruit_midi.MIDIMessage.from_message_bytes(data, ichannel) + (msg, msgendidxplusone, skipped) = adafruit_midi.MIDIMessage.from_message_bytes(data, ichannel) self.assertIsInstance(msg, SystemExclusive) self.assertEqual(msg.manufacturer_id, bytes([0x42])) # Korg @@ -231,51 +235,49 @@ def test_SystemExclusive_NoteOn(self): self.assertEqual(msgendidxplusone, 7) self.assertEqual(skipped, 0, "If SystemExclusive class is imported then this must be 0") - self.assertIsNone(channel) + self.assertIsNone(msg.channel) - (msg, msgendidxplusone, skipped, channel) = adafruit_midi.MIDIMessage.from_message_bytes(data[msgendidxplusone:], ichannel) + (msg, msgendidxplusone, skipped) = adafruit_midi.MIDIMessage.from_message_bytes(data[msgendidxplusone:], ichannel) self.assertIsInstance(msg, NoteOn) self.assertEqual(msg.note, 48) self.assertEqual(msg.velocity, 0x60) self.assertEqual(msgendidxplusone, 3) self.assertEqual(skipped, 0) - self.assertEqual(channel, 14) + self.assertEqual(msg.channel, 14) def test_SystemExclusive_NoteOn_premalterminatedsysex(self): data = bytes([0xf0, 0x42, 0x01, 0x02, 0x03, 0x04, 0xf0, 0x90, 0x30, 0x32]) ichannel = 0 # 0xf0 is incorrect status to mark end of this message, must be 0xf7 - (msg, msgendidxplusone, skipped, channel) = adafruit_midi.MIDIMessage.from_message_bytes(data, ichannel) + (msg, msgendidxplusone, skipped) = adafruit_midi.MIDIMessage.from_message_bytes(data, ichannel) self.assertIsNone(msg) self.assertEqual(msgendidxplusone, 7) self.assertEqual(skipped, 0, "If SystemExclusive class is imported then this must be 0") - self.assertIsNone(channel, None) def test_Unknown_SinglebyteStatus(self): data = bytes([0xfd]) ichannel = 0 - (msg, msgendidxplusone, skipped, channel) = adafruit_midi.MIDIMessage.from_message_bytes(data, ichannel) + (msg, msgendidxplusone, skipped) = adafruit_midi.MIDIMessage.from_message_bytes(data, ichannel) self.assertIsInstance(msg, adafruit_midi.midi_message.MIDIUnknownEvent) self.assertEqual(msgendidxplusone, 1) self.assertEqual(skipped, 0) - self.assertIsNone(channel) + self.assertIsNone(msg.channel) def test_Empty(self): data = bytes([]) ichannel = 0 - (msg, msgendidxplusone, skipped, channel) = adafruit_midi.MIDIMessage.from_message_bytes(data, ichannel) + (msg, msgendidxplusone, skipped) = adafruit_midi.MIDIMessage.from_message_bytes(data, ichannel) self.assertIsNone(msg) self.assertEqual(msgendidxplusone, 0) - self.assertEqual(skipped, 0) - self.assertIsNone(channel) + self.assertEqual(skipped, 0) class Test_MIDIMessage_NoteOn_constructor(unittest.TestCase): @@ -359,6 +361,6 @@ def test_NoteOff_constructor_bogusstring(self): NoteOff("CC4", 0x7f) - + if __name__ == '__main__': unittest.main(verbosity=verbose) diff --git a/tests/test_MIDI_unittests.py b/tests/test_MIDI_unittests.py index 2dce29d..ab75282 100644 --- a/tests/test_MIDI_unittests.py +++ b/tests/test_MIDI_unittests.py @@ -39,7 +39,7 @@ from adafruit_midi.control_change import ControlChange from adafruit_midi.note_off import NoteOff from adafruit_midi.note_on import NoteOn -from adafruit_midi.pitch_bend import PitchBend +from adafruit_midi.pitch_bend import PitchBend from adafruit_midi.polyphonic_key_pressure import PolyphonicKeyPressure from adafruit_midi.program_change import ProgramChange from adafruit_midi.start import Start @@ -57,13 +57,13 @@ def MIDI_mocked_both_loopback(in_c, out_c): def write(buffer, length): nonlocal usb_data usb_data.extend(buffer[0:length]) - + def read(length): nonlocal usb_data poppedbytes = usb_data[0:length] usb_data = usb_data[len(poppedbytes):] return bytes(poppedbytes) - + mockedPortIn = Mock() mockedPortIn.read = read mockedPortOut = Mock() @@ -76,7 +76,7 @@ def MIDI_mocked_receive(in_c, data, read_sizes): usb_data = bytearray(data) chunks = read_sizes chunk_idx = 0 - + def read(length): nonlocal usb_data, chunks, chunk_idx # pylint: disable=no-else-return @@ -114,139 +114,140 @@ def test_captured_data_one_byte_reads(self): m = MIDI_mocked_receive(c, raw_data, [1] * len(raw_data)) for unused in range(100): # pylint: disable=unused-variable - (msg, channel) = m.receive() + msg = m.receive() if msg is not None: - break + break self.assertIsInstance(msg, NoteOn) self.assertEqual(msg.note, 0x3e) self.assertEqual(msg.velocity, 0x5f) - self.assertEqual(channel, c) - + self.assertEqual(msg.channel, c) + # for loops currently absorb any Nones but could # be set to read precisely the expected number... for unused in range(100): # pylint: disable=unused-variable - (msg, channel) = m.receive() + msg = m.receive() if msg is not None: - break + break self.assertIsInstance(msg, ChannelPressure) self.assertEqual(msg.pressure, 0x10) - self.assertEqual(channel, c) + self.assertEqual(msg.channel, c) for unused in range(100): # pylint: disable=unused-variable - (msg, channel) = m.receive() + msg = m.receive() if msg is not None: - break + break self.assertIsInstance(msg, NoteOn) self.assertEqual(msg.note, 0x40) self.assertEqual(msg.velocity, 0x66) - self.assertEqual(channel, c) - + self.assertEqual(msg.channel, c) + for unused in range(100): # pylint: disable=unused-variable - (msg, channel) = m.receive() + msg = m.receive() if msg is not None: break self.assertIsInstance(msg, ControlChange) self.assertEqual(msg.control, 0x01) self.assertEqual(msg.value, 0x08) - self.assertEqual(channel, c) - + self.assertEqual(msg.channel, c) + for unused in range(100): # pylint: disable=unused-variable - (msg, channel) = m.receive() + msg = m.receive() if msg is not None: break self.assertIsInstance(msg, NoteOn) self.assertEqual(msg.note, 0x41) self.assertEqual(msg.velocity, 0x74) - self.assertEqual(channel, c) - + self.assertEqual(msg.channel, c) + for unused in range(100): # pylint: disable=unused-variable - (msg, channel) = m.receive() + msg = m.receive() if msg is not None: break self.assertIsInstance(msg, PitchBend) self.assertEqual(msg.pitch_bend, 8195) - self.assertEqual(channel, c) - + self.assertEqual(msg.channel, c) + for unused in range(100): # pylint: disable=unused-variable - (msg, channel) = m.receive() + msg = m.receive() self.assertIsNone(msg) - self.assertIsNone(channel) def test_unknown_before_NoteOn(self): c = 0 # From an M-Audio AXIOM controller - raw_data = (bytearray([0b11110011, 0x10] # Song Select (not yet implemented) - + [ 0b11110011, 0x20] - + [ 0b11110100 ] - + [ 0b11110101 ]) - + NoteOn("C5", 0x7f).as_bytes(channel=c)) + raw_data = (bytes([0b11110011, 0x10] # Song Select (not yet implemented) + + [ 0b11110011, 0x20] + + [ 0b11110100 ] + + [ 0b11110101 ]) + + bytes(NoteOn("C5", 0x7f, channel=c))) m = MIDI_mocked_receive(c, raw_data, [2, 2, 1, 1, 3]) for unused in range(4): # pylint: disable=unused-variable - (msg, channel) = m.receive() + msg = m.receive() self.assertIsInstance(msg, adafruit_midi.midi_message.MIDIUnknownEvent) + self.assertIsNone(msg.channel) - (msg, channel) = m.receive() + msg = m.receive() self.assertIsInstance(msg, NoteOn) - self.assertEqual(msg.note, 0x48) + self.assertEqual(msg.note, 0x48) # 0x48 is C5 self.assertEqual(msg.velocity, 0x7f) - self.assertEqual(channel, c) + self.assertEqual(msg.channel, c) # See https://github.com/adafruit/Adafruit_CircuitPython_MIDI/issues/8 def test_running_status_when_implemented(self): c = 8 - raw_data = (NoteOn("C5", 0x7f,).as_bytes(channel=c) - + bytearray([0xe8, 0x72, 0x40] - + [0x6d, 0x40] - + [0x05, 0x41]) - + NoteOn("D5", 0x7f).as_bytes(channel=c)) + raw_data = (bytes(NoteOn("C5", 0x7f, channel=c)) + + bytes([0xe8, 0x72, 0x40] + + [0x6d, 0x40] + + [0x05, 0x41]) + + bytes(NoteOn("D5", 0x7f, channel=c))) m = MIDI_mocked_receive(c, raw_data, [3 + 3 + 2 + 3 + 3]) self.assertIsInstance(m, adafruit_midi.MIDI) # silence pylint! #self.assertEqual(TOFINISH, WHENIMPLEMENTED) - + def test_somegood_somemissing_databytes(self): c = 8 - raw_data = (NoteOn("C5", 0x7f,).as_bytes(channel=c) - + bytearray([0xe8, 0x72, 0x40] - + [0xe8, 0x6d ] # Missing last data byte - + [0xe8, 0x5, 0x41 ]) - + NoteOn("D5", 0x7f).as_bytes(channel=c)) + raw_data = (bytes(NoteOn("C5", 0x7f, channel=c)) + + bytes([0xe8, 0x72, 0x40] + + [0xe8, 0x6d ] # Missing last data byte + + [0xe8, 0x5, 0x41 ]) + + bytes(NoteOn("D5", 0x7f, channel=c))) m = MIDI_mocked_receive(c, raw_data, [3 + 3 + 2 + 3 + 3]) - (msg1, channel1) = m.receive() + msg1 = m.receive() self.assertIsInstance(msg1, NoteOn) self.assertEqual(msg1.note, 72) self.assertEqual(msg1.velocity, 0x7f) - self.assertEqual(channel1, c) + self.assertEqual(msg1.channel, c) - (msg2, channel2) = m.receive() + msg2 = m.receive() self.assertIsInstance(msg2, PitchBend) self.assertEqual(msg2.pitch_bend, 8306) - self.assertEqual(channel2, c) + self.assertEqual(msg2.channel, c) # The current implementation will read status bytes for data # In most cases it would be a faster recovery with fewer messages - # lost if status byte wasn't consumed and parsing restart from that - (msg3, channel3) = m.receive() + # lost if the next status byte wasn't consumed + # and parsing restarted from that byte + msg3 = m.receive() self.assertIsInstance(msg3, adafruit_midi.midi_message.MIDIBadEvent) - self.assertEqual(msg3.data, bytearray([0x6d, 0xe8])) - self.assertEqual(channel3, c) + self.assertIsInstance(msg3.data, bytes) + self.assertEqual(msg3.data, bytes([0xe8, 0x6d, 0xe8])) + self.assertIsNone(msg3.channel) #(msg4, channel4) = m.receive() #self.assertIsInstance(msg4, PitchBend) #self.assertEqual(msg4.pitch_bend, 72) #self.assertEqual(channel4, c) - (msg5, channel5) = m.receive() + msg5 = m.receive() self.assertIsInstance(msg5, NoteOn) self.assertEqual(msg5.note, 74) self.assertEqual(msg5.velocity, 0x7f) - self.assertEqual(channel5, c) + self.assertEqual(msg5.channel, c) - (msg6, channel6) = m.receive() + msg6 = m.receive() self.assertIsNone(msg6) - self.assertIsNone(channel6) def test_smallsysex_between_notes(self): m = MIDI_mocked_both_loopback(3, 3) @@ -255,75 +256,86 @@ def test_smallsysex_between_notes(self): SystemExclusive([0x1f], [1, 2, 3, 4, 5, 6, 7, 8]), NoteOff(60, 0x28)]) - (msg1, channel1) = m.receive() + msg1 = m.receive() self.assertIsInstance(msg1, NoteOn) self.assertEqual(msg1.note, 60) self.assertEqual(msg1.velocity, 0x7f) - self.assertEqual(channel1, 3) - - (msg2, channel2) = m.receive() + self.assertEqual(msg1.channel, 3) + + msg2 = m.receive() self.assertIsInstance(msg2, SystemExclusive) - self.assertEqual(msg2.manufacturer_id, bytearray([0x1f])) - self.assertEqual(msg2.data, bytearray([1, 2, 3, 4, 5, 6, 7, 8])) - self.assertEqual(channel2, None) # SysEx does not have a channel - - (msg3, channel3) = m.receive() + self.assertEqual(msg2.manufacturer_id, bytes([0x1f])) + self.assertEqual(msg2.data, bytes([1, 2, 3, 4, 5, 6, 7, 8])) + self.assertEqual(msg2.channel, None) # SysEx does not have a channel + + msg3 = m.receive() self.assertIsInstance(msg3, NoteOff) self.assertEqual(msg3.note, 60) self.assertEqual(msg3.velocity, 0x28) - self.assertEqual(channel3, 3) - - (msg4, channel4) = m.receive() + self.assertEqual(msg3.channel, 3) + + msg4 = m.receive() self.assertIsNone(msg4) - self.assertIsNone(channel4) + + def test_smallsysex_bytes_type(self): + s = SystemExclusive([0x1f], [100, 150, 200]) + + self.assertIsInstance(s, SystemExclusive) + self.assertEqual(s.manufacturer_id, bytes([0x1f])) + self.assertIsInstance(s.manufacturer_id, bytes) + + # check this really is immutable (pylint also picks this up!) + with self.assertRaises(TypeError): + s.data[0] = 0 # pylint: disable=unsupported-assignment-operation + + self.assertEqual(s.data, bytes([100, 150, 200])) + self.assertIsInstance(s.data, bytes) # pylint: disable=too-many-locals def test_larger_than_buffer_sysex(self): c = 0 monster_data_len = 500 - raw_data = (NoteOn("C5", 0x7f,).as_bytes(channel=c) - + SystemExclusive([0x02], - [d & 0x7f for d in range(monster_data_len)]).as_bytes(channel=c) - + NoteOn("D5", 0x7f).as_bytes(channel=c)) + raw_data = (bytes(NoteOn("C5", 0x7f, channel=c)) + + bytes(SystemExclusive([0x02], + [d & 0x7f for d in range(monster_data_len)])) + + bytes(NoteOn("D5", 0x7f, channel=c))) m = MIDI_mocked_receive(c, raw_data, [len(raw_data)]) - buffer_len = m._in_buf_size # pylint: disable=protected-access + buffer_len = m._in_buf_size # pylint: disable=protected-access self.assertTrue(monster_data_len > buffer_len, "checking our SysEx truly is larger than buffer") - - (msg1, channel1) = m.receive() + + msg1 = m.receive() self.assertIsInstance(msg1, NoteOn) self.assertEqual(msg1.note, 72) self.assertEqual(msg1.velocity, 0x7f) - self.assertEqual(channel1, c) + self.assertEqual(msg1.channel, c) # (Ab)using python's rounding down for negative division # pylint: disable=unused-variable for unused in range(-(-(1 + 1 + monster_data_len + 1) // buffer_len) - 1): - (msg2, channel2) = m.receive() + msg2 = m.receive() self.assertIsNone(msg2) - self.assertIsNone(channel2) # The current implementation will read SysEx end status byte # and report it as an unknown - (msg3, channel3) = m.receive() + msg3 = m.receive() self.assertIsInstance(msg3, adafruit_midi.midi_message.MIDIUnknownEvent) self.assertEqual(msg3.status, 0xf7) - self.assertIsNone(channel3) + self.assertIsNone(msg3.channel) #(msg4, channel4) = m.receive() #self.assertIsInstance(msg4, PitchBend) #self.assertEqual(msg4.pitch_bend, 72) #self.assertEqual(channel4, c) - (msg5, channel5) = m.receive() + msg5 = m.receive() self.assertIsInstance(msg5, NoteOn) self.assertEqual(msg5.note, 74) self.assertEqual(msg5.velocity, 0x7f) - self.assertEqual(channel5, c) + self.assertEqual(msg5.channel, c) - (msg6, channel6) = m.receive() + msg6 = m.receive() self.assertIsNone(msg6) - self.assertIsNone(channel6) # pylint does not like mock_calls - must be a better way to handle this? # pylint: disable=no-member @@ -333,7 +345,7 @@ def test_send_basic_single(self): # print(buffer[0:len]) mockedPortOut = Mock() #mockedPortOut.write = printit - + m = adafruit_midi.MIDI(midi_out=mockedPortOut, out_channel=2) # Test sending some NoteOn and NoteOff to various channels @@ -350,7 +362,7 @@ def test_send_basic_single(self): self.assertEqual(mockedPortOut.write.mock_calls[nextcall], call(b'\x92\x67\x1f', 3)) nextcall += 1 - + m.send(NoteOn(0x60, 0x00)) # Alternative to NoteOff self.assertEqual(mockedPortOut.write.mock_calls[nextcall], call(b'\x92\x60\x00', 3)) @@ -363,13 +375,13 @@ def test_send_basic_single(self): self.assertEqual(mockedPortOut.write.mock_calls[nextcall], call(b'\x82\x67\x02', 3)) nextcall += 1 - + # Setting channel to non default m.send(NoteOn(0x6c, 0x7f), channel=9) self.assertEqual(mockedPortOut.write.mock_calls[nextcall], call(b'\x99\x6c\x7f', 3)) nextcall += 1 - + m.send(NoteOff(0x6c, 0x7f), channel=9) self.assertEqual(mockedPortOut.write.mock_calls[nextcall], call(b'\x89\x6c\x7f', 3)) @@ -377,7 +389,7 @@ def test_send_basic_single(self): def test_send_badnotes(self): mockedPortOut = Mock() - + m = adafruit_midi.MIDI(midi_out=mockedPortOut, out_channel=2) # Test sending some NoteOn and NoteOff to various channels @@ -403,7 +415,7 @@ def test_send_basic_sequences(self): # print(buffer[0:len]) mockedPortOut = Mock() #mockedPortOut.write = printit - + m = adafruit_midi.MIDI(midi_out=mockedPortOut, out_channel=2) # Test sending some NoteOn and NoteOff to various channels From 5cd820d304b9a59df352ae314d9613a6caac15ef Mon Sep 17 00:00:00 2001 From: Kevin J Walters Date: Sun, 7 Apr 2019 00:08:44 +0100 Subject: [PATCH 83/92] Removing default values in MIDI constructor for midi_in and midi_out based on @tannewt advice. Updating all the examples to explicitly set midi_in or midi_out to the USB ports. Adding test case as constructor now raises an error if midi_in and midi_out are both omitted. This will make sphinx-build work but that appears to be a separate bug. #13 --- README.rst | 6 +++--- adafruit_midi/__init__.py | 14 +++++++------- docs/conf.py | 2 +- examples/midi_inoutdemo.py | 5 ++++- examples/midi_intest1.py | 3 ++- examples/midi_simpletest.py | 3 ++- examples/midi_simpletest2.py | 10 ++++++---- tests/test_MIDIMessage_unittests.py | 4 ++-- tests/test_MIDI_unittests.py | 10 ++++++++-- tests/test_note_parser.py | 4 ++-- 10 files changed, 37 insertions(+), 24 deletions(-) diff --git a/README.rst b/README.rst index bc5bdb7..0ce3d20 100644 --- a/README.rst +++ b/README.rst @@ -58,14 +58,14 @@ Usage Example import time import random + import usb_midi import adafruit_midi - midi = adafruit_midi.MIDI(out_channel=0) + midi = adafruit_midi.MIDI(midi_out=usb_midi.ports[1], out_channel=0) print("Midi test") - print("Default output channel:", midi.out_channel) - print("Listening on input channel:", midi.in_channel) + print("Default output MIDI channel:", midi.out_channel + 1) while True: midi.note_on(44, 120) diff --git a/adafruit_midi/__init__.py b/adafruit_midi/__init__.py index c20b56e..baf3086 100644 --- a/adafruit_midi/__init__.py +++ b/adafruit_midi/__init__.py @@ -42,8 +42,6 @@ """ -import usb_midi - from .midi_message import MIDIMessage, ALL_CHANNELS __version__ = "0.0.0-auto.0" @@ -51,12 +49,12 @@ class MIDI: - """MIDI helper class. + """MIDI helper class. ``midi_in`` or ``midi_out`` *must* be set or both together. :param midi_in: an object which implements ``read(length)``, - defaults to ``usb_midi.ports[0]``. + set to ``usb_midi.ports[0]`` for USB MIDI, default None. :param midi_out: an object which implements ``write(buffer, length)``, - defaults to ``usb_midi.ports[1]``. + set to ``usb_midi.ports[1]`` for USB MIDI, default None. :param in_channel: The input channel(s). This is used by ``receive`` to filter data. This can either be an ``int`` for the wire protocol channel number (0-15) @@ -65,7 +63,7 @@ class MIDI: :param int out_channel: The wire protocol output channel number (0-15) used by ``send`` if no channel is specified, defaults to 0 (MIDI Channel 1). - :param int in_buf_size: Size of input buffer in bytes, default 30. + :param int in_buf_size: Maximum size of input buffer in bytes, default 30. :param bool debug: Debug mode, default False. """ @@ -75,8 +73,10 @@ class MIDI: PITCH_BEND = 0xE0 CONTROL_CHANGE = 0xB0 - def __init__(self, midi_in=usb_midi.ports[0], midi_out=usb_midi.ports[1], *, + def __init__(self, midi_in=None, midi_out=None, *, in_channel=None, out_channel=0, in_buf_size=30, debug=False): + if midi_in is None and midi_out is None: + raise ValueError("No midi_in or midi_out provided") self._midi_in = midi_in self._midi_out = midi_out self._in_channel = in_channel # dealing with pylint inadequacy diff --git a/docs/conf.py b/docs/conf.py index 2d8c5f0..2c71034 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -20,7 +20,7 @@ # Uncomment the below if you use native CircuitPython modules such as # digitalio, micropython and busio. List the modules you use. Without it, the # autodoc module docs will fail to generate with a warning. -autodoc_mock_imports = ["usb_midi"] +autodoc_mock_imports = [] intersphinx_mapping = {'python': ('https://docs.python.org/3.4', None),'CircuitPython': ('https://circuitpython.readthedocs.io/en/latest/', None)} diff --git a/examples/midi_inoutdemo.py b/examples/midi_inoutdemo.py index 3022cd0..e14f5a9 100644 --- a/examples/midi_inoutdemo.py +++ b/examples/midi_inoutdemo.py @@ -1,5 +1,6 @@ # midi_inoutdemo - demonstrates receiving and sending MIDI events +import usb_midi import adafruit_midi # TimingClock is worth importing first if present as it @@ -21,7 +22,9 @@ from adafruit_midi.midi_message import MIDIUnknownEvent -midi = adafruit_midi.MIDI(in_channel=(1, 2, 3), out_channel=0) +midi = adafruit_midi.MIDI(midi_in=usb_midi.ports[0], + midi_out=usb_midi.ports[1], + in_channel=(1, 2, 3), out_channel=0) print("Midi Demo in and out") diff --git a/examples/midi_intest1.py b/examples/midi_intest1.py index 9b62da0..1bb72d6 100644 --- a/examples/midi_intest1.py +++ b/examples/midi_intest1.py @@ -1,8 +1,9 @@ import time +import usb_midi import adafruit_midi ### 0 is MIDI channel 1 -midi = adafruit_midi.MIDI(in_channel=0) +midi = adafruit_midi.MIDI(midi_in=usb_midi.ports[0], in_channel=0) print("Midi test II") diff --git a/examples/midi_simpletest.py b/examples/midi_simpletest.py index 7bc07df..c5c3282 100644 --- a/examples/midi_simpletest.py +++ b/examples/midi_simpletest.py @@ -1,8 +1,9 @@ import time import random +import usb_midi import adafruit_midi -midi = adafruit_midi.MIDI(out_channel=0) +midi = adafruit_midi.MIDI(midi_out=usb_midi.ports[1], out_channel=0) print("Midi test") diff --git a/examples/midi_simpletest2.py b/examples/midi_simpletest2.py index 0d73680..a0425d5 100644 --- a/examples/midi_simpletest2.py +++ b/examples/midi_simpletest2.py @@ -1,13 +1,14 @@ # simple_test demonstrating both interfaces import time import random +import usb_midi import adafruit_midi -from adafruit_midi.control_change import ControlChange -from adafruit_midi.note_off import NoteOff -from adafruit_midi.note_on import NoteOn +from adafruit_midi.control_change import ControlChange +from adafruit_midi.note_off import NoteOff +from adafruit_midi.note_on import NoteOn from adafruit_midi.pitch_bend import PitchBend -midi = adafruit_midi.MIDI(out_channel=0) +midi = adafruit_midi.MIDI(midi_out=usb_midi.ports[1], out_channel=0) print("Midi test") @@ -25,6 +26,7 @@ midi.note_off(44, 120) midi.control_change(3, 44) time.sleep(0.5) + # send message(s) interface midi.send(NoteOn(44, 120)) time.sleep(0.25) diff --git a/tests/test_MIDIMessage_unittests.py b/tests/test_MIDIMessage_unittests.py index 11d51a2..e0b4cc4 100644 --- a/tests/test_MIDIMessage_unittests.py +++ b/tests/test_MIDIMessage_unittests.py @@ -27,9 +27,9 @@ import os verbose = int(os.getenv('TESTVERBOSE', '2')) -# adafruit_midi has an import usb_midi +# adafruit_midi had an import usb_midi import sys -sys.modules['usb_midi'] = MagicMock() +#sys.modules['usb_midi'] = MagicMock() # Borrowing the dhalbert/tannewt technique from adafruit/Adafruit_CircuitPython_Motor sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) diff --git a/tests/test_MIDI_unittests.py b/tests/test_MIDI_unittests.py index ab75282..e04a437 100644 --- a/tests/test_MIDI_unittests.py +++ b/tests/test_MIDI_unittests.py @@ -27,9 +27,9 @@ import os verbose = int(os.getenv('TESTVERBOSE', '2')) -# adafruit_midi has an import usb_midi +# adafruit_midi had an import usb_midi import sys -sys.modules['usb_midi'] = MagicMock() +#sys.modules['usb_midi'] = MagicMock() # Borrowing the dhalbert/tannewt technique from adafruit/Adafruit_CircuitPython_Motor sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) @@ -99,6 +99,12 @@ def read(length): out_channel=in_c, in_channel=in_c) return m + +class Test_MIDI_constructor(unittest.TestCase): + def test_no_inout(self): + # constructor likes a bit of in out + with self.assertRaises(ValueError): + adafruit_midi.MIDI() class Test_MIDI(unittest.TestCase): # pylint: disable=too-many-branches diff --git a/tests/test_note_parser.py b/tests/test_note_parser.py index 6384c56..d632e53 100644 --- a/tests/test_note_parser.py +++ b/tests/test_note_parser.py @@ -27,9 +27,9 @@ import os verbose = int(os.getenv('TESTVERBOSE', '2')) -# adafruit_midi has an import usb_midi +# adafruit_midi had an import usb_midi import sys -sys.modules['usb_midi'] = MagicMock() +#sys.modules['usb_midi'] = MagicMock() # Borrowing the dhalbert/tannewt technique from adafruit/Adafruit_CircuitPython_Motor sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) From a764c7046119398784e03d17330fef5adb778db5 Mon Sep 17 00:00:00 2001 From: Kevin J Walters Date: Sun, 7 Apr 2019 00:23:14 +0100 Subject: [PATCH 84/92] Replacing MIDI.send() use of bytes(object) with object.__bytes__() due to a peculiar micropython-ism. Leaving test cases for now - these do not (currently) execute in CircuitPython. #3 #9 --- adafruit_midi/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/adafruit_midi/__init__.py b/adafruit_midi/__init__.py index baf3086..cd421c5 100644 --- a/adafruit_midi/__init__.py +++ b/adafruit_midi/__init__.py @@ -164,12 +164,12 @@ def send(self, msg, channel=None): channel = self.out_channel if isinstance(msg, MIDIMessage): msg.channel = channel - data = bytes(msg) + data = msg.__bytes__() # bytes(object) does not work in uPy else: data = bytearray() for each_msg in msg: each_msg.channel = channel - data.extend(bytes(each_msg)) + data.extend(each_msg.__bytes__()) self._send(data, len(data)) From b12f924a4a746941f063cf32fa1d1dcf610e09c0 Mon Sep 17 00:00:00 2001 From: Kevin J Walters Date: Sun, 7 Apr 2019 00:30:57 +0100 Subject: [PATCH 85/92] Missed update of midi_inoutdemo.py to new MIDI.receive() return value and channel extraction from messages. #3 #9 --- examples/midi_inoutdemo.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/midi_inoutdemo.py b/examples/midi_inoutdemo.py index e14f5a9..3921022 100644 --- a/examples/midi_inoutdemo.py +++ b/examples/midi_inoutdemo.py @@ -35,12 +35,12 @@ major_chord = [0, 4, 7] while True: while True: - (msg_in, channel_in) = midi.receive() # non-blocking read + msg_in = midi.receive() # non-blocking read # For a Note On or Note Off play a major chord # For any other known event just forward it if isinstance(msg_in, NoteOn) and msg_in.velocity != 0: print("Playing major chord with root", msg_in.note, - "from channel", channel_in + 1) + "from channel", msg.channel + 1) for offset in major_chord: new_note = msg_in.note + offset if 0 <= new_note <= 127: From 621fded7723075dd5b1d4e1cc766b92d9b2dfaed Mon Sep 17 00:00:00 2001 From: Kevin J Walters Date: Sun, 7 Apr 2019 00:49:11 +0100 Subject: [PATCH 86/92] New example to check memory use. CPX 4.0.0 beta 5 goes from 20128 to 4528 mem_free(). #3 --- examples/midi_memorycheck.py | 40 ++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 examples/midi_memorycheck.py diff --git a/examples/midi_memorycheck.py b/examples/midi_memorycheck.py new file mode 100644 index 0000000..059390c --- /dev/null +++ b/examples/midi_memorycheck.py @@ -0,0 +1,40 @@ +# Check memory usage +import time +import random +import gc +gc.collect() ; print(gc.mem_free()) +import usb_midi +gc.collect() ; print(gc.mem_free()) +import adafruit_midi +gc.collect() ; print(gc.mem_free()) + +# Full monty +from adafruit_midi.channel_pressure import ChannelPressure +gc.collect() ; print(gc.mem_free()) +from adafruit_midi.control_change import ControlChange +gc.collect() ; print(gc.mem_free()) +from adafruit_midi.note_off import NoteOff +gc.collect() ; print(gc.mem_free()) +from adafruit_midi.note_on import NoteOn +gc.collect() ; print(gc.mem_free()) +from adafruit_midi.pitch_bend import PitchBend +gc.collect() ; print(gc.mem_free()) +from adafruit_midi.polyphonic_key_pressure import PolyphonicKeyPressure +gc.collect() ; print(gc.mem_free()) +from adafruit_midi.program_change import ProgramChange +gc.collect() ; print(gc.mem_free()) +from adafruit_midi.start import Start +gc.collect() ; print(gc.mem_free()) +from adafruit_midi.stop import Stop +gc.collect() ; print(gc.mem_free()) +from adafruit_midi.system_exclusive import SystemExclusive +gc.collect() ; print(gc.mem_free()) +from adafruit_midi.timing_clock import TimingClock +gc.collect() ; print(gc.mem_free()) + +midi = adafruit_midi.MIDI(midi_in=usb_midi.ports[0], + midi_out=usb_midi.ports[1], + in_channel=0, out_channel=0) + +gc.collect() ; print(gc.mem_free()) + From 146d0fb1a6b904426e991131d60bab4429fca5b3 Mon Sep 17 00:00:00 2001 From: Kevin J Walters Date: Sun, 7 Apr 2019 12:15:25 +0100 Subject: [PATCH 87/92] midi_intest1.py refresh, needed messages imported. #3 --- examples/midi_intest1.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/examples/midi_intest1.py b/examples/midi_intest1.py index 1bb72d6..86bd971 100644 --- a/examples/midi_intest1.py +++ b/examples/midi_intest1.py @@ -2,14 +2,23 @@ import usb_midi import adafruit_midi -### 0 is MIDI channel 1 +# A subset of messages/events +# pylint: disable=unused-import +from adafruit_midi.timing_clock import TimingClock +#from adafruit_midi.channel_pressure import ChannelPressure +from adafruit_midi.control_change import ControlChange +from adafruit_midi.note_off import NoteOff +from adafruit_midi.note_on import NoteOn +from adafruit_midi.pitch_bend import PitchBend + + +# 0 is MIDI channel 1 midi = adafruit_midi.MIDI(midi_in=usb_midi.ports[0], in_channel=0) -print("Midi test II") +print("Midi input test with pauses") # Convert channel numbers at the presentation layer to the ones musicians use print("Input channel:", midi.in_channel + 1 ) -print("Listening on input channel:", midi.in_channel + 1) # play with the pause to simulate code doing other stuff # in the loop From d04b5c653345ab590ab67457d53a998c84580455 Mon Sep 17 00:00:00 2001 From: Kevin J Walters Date: Sun, 7 Apr 2019 12:19:56 +0100 Subject: [PATCH 88/92] pylint tweaks for midi_memorycheck.py. #3 --- examples/midi_memorycheck.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/examples/midi_memorycheck.py b/examples/midi_memorycheck.py index 059390c..cb46cef 100644 --- a/examples/midi_memorycheck.py +++ b/examples/midi_memorycheck.py @@ -1,4 +1,7 @@ # Check memory usage + +# pylint: disable=multiple-statements,unused-import,wrong-import-position + import time import random import gc @@ -37,4 +40,3 @@ in_channel=0, out_channel=0) gc.collect() ; print(gc.mem_free()) - From 45a55c490f062a226afd4b776527a84178eeff23 Mon Sep 17 00:00:00 2001 From: Kevin J Walters Date: Sun, 7 Apr 2019 12:33:11 +0100 Subject: [PATCH 89/92] midi_inoutdemo.py typo fix. #3 --- examples/midi_inoutdemo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/midi_inoutdemo.py b/examples/midi_inoutdemo.py index 3921022..4f78dc7 100644 --- a/examples/midi_inoutdemo.py +++ b/examples/midi_inoutdemo.py @@ -40,7 +40,7 @@ # For any other known event just forward it if isinstance(msg_in, NoteOn) and msg_in.velocity != 0: print("Playing major chord with root", msg_in.note, - "from channel", msg.channel + 1) + "from channel", msg_in.channel + 1) for offset in major_chord: new_note = msg_in.note + offset if 0 <= new_note <= 127: From e3dfd0ee0c968e11d34b5c22f18b7ce0ff4d402a Mon Sep 17 00:00:00 2001 From: Kevin J Walters Date: Sun, 7 Apr 2019 12:36:35 +0100 Subject: [PATCH 90/92] midi_memorycheck.py workaround for Travis pylint issue with false no-member reporting for gc.mem_free(). #3 --- examples/midi_memorycheck.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/examples/midi_memorycheck.py b/examples/midi_memorycheck.py index cb46cef..a86c3f4 100644 --- a/examples/midi_memorycheck.py +++ b/examples/midi_memorycheck.py @@ -1,6 +1,11 @@ # Check memory usage -# pylint: disable=multiple-statements,unused-import,wrong-import-position +# pylint: disable=multiple-statements,unused-import,wrong-import-position,no-member + +# The disable for no-member should not really be required +# probably a difference between Python 3 module and micropython +# +# E: 8,21: Module 'gc' has no 'mem_free' member (no-member) import time import random From bb44772b4a556d35a7cef99011e89f5f798cacc5 Mon Sep 17 00:00:00 2001 From: Kevin J Walters Date: Mon, 8 Apr 2019 23:19:38 +0100 Subject: [PATCH 91/92] Giving NoteOn and NoteOff some default velocity values as this is useful from REPL. #3 --- adafruit_midi/note_off.py | 4 ++-- adafruit_midi/note_on.py | 4 ++-- tests/test_MIDIMessage_unittests.py | 10 ++++++++++ 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/adafruit_midi/note_off.py b/adafruit_midi/note_off.py index 1f1b0cd..3e8382d 100644 --- a/adafruit_midi/note_off.py +++ b/adafruit_midi/note_off.py @@ -44,7 +44,7 @@ class NoteOff(MIDIMessage): :param note: The note (key) number either as an ``int`` (0-127) or a ``str`` which is parsed, e.g. "C4" (middle C) is 60, "A4" is 69. - :param int velocity: The release velocity, 0-127. + :param int velocity: The release velocity, 0-127, defaults to 0. """ @@ -52,7 +52,7 @@ class NoteOff(MIDIMessage): _STATUSMASK = 0xf0 LENGTH = 3 - def __init__(self, note, velocity, *, channel=None): + def __init__(self, note, velocity=0, *, channel=None): self.note = note_parser(note) self.velocity = velocity super().__init__(channel=channel) diff --git a/adafruit_midi/note_on.py b/adafruit_midi/note_on.py index 9c933a3..d0ef9fe 100644 --- a/adafruit_midi/note_on.py +++ b/adafruit_midi/note_on.py @@ -45,14 +45,14 @@ class NoteOn(MIDIMessage): :param note: The note (key) number either as an ``int`` (0-127) or a ``str`` which is parsed, e.g. "C4" (middle C) is 60, "A4" is 69. :param int velocity: The strike velocity, 0-127, 0 is equivalent - to a Note Off. + to a Note Off, defaults to 127. """ _STATUS = 0x90 _STATUSMASK = 0xf0 LENGTH = 3 - def __init__(self, note, velocity, *, channel=None): + def __init__(self, note, velocity=127, *, channel=None): self.note = note_parser(note) self.velocity = velocity super().__init__(channel=channel) diff --git a/tests/test_MIDIMessage_unittests.py b/tests/test_MIDIMessage_unittests.py index e0b4cc4..0126ead 100644 --- a/tests/test_MIDIMessage_unittests.py +++ b/tests/test_MIDIMessage_unittests.py @@ -223,6 +223,12 @@ def test_NoteOn_constructor_int(self): self.assertEqual(object3.velocity, 0x50) self.assertEqual(object3.channel, 7) + object4 = NoteOn(60) # velocity defaults to 127 + + self.assertEqual(object4.note, 60) + self.assertEqual(object4.velocity, 127) + self.assertIsNone(object4.channel) + def test_SystemExclusive_NoteOn(self): data = bytes([0xf0, 0x42, 0x01, 0x02, 0x03, 0x04, 0xf7, 0x90 | 14, 0x30, 0x60]) ichannel = 14 @@ -335,6 +341,10 @@ def test_NoteOff_constructor_string(self): self.assertEqual(object3.note, 61) self.assertEqual(object3.velocity, 0) + object4 = NoteOff("C#4") # velocity defaults to 0 + self.assertEqual(object4.note, 61) + self.assertEqual(object4.velocity, 0) + def test_NoteOff_constructor_valueerror1(self): with self.assertRaises(ValueError): NoteOff(60, 0x80) From d507b427784d65fea26e08c7c9a8ddddc4c9b508 Mon Sep 17 00:00:00 2001 From: Kevin J Walters Date: Mon, 8 Apr 2019 23:27:05 +0100 Subject: [PATCH 92/92] Removing old TODO, polishing docs for MIDIMessage.register_message_type() and upper case for exception from MIDIMessage.channel . #3 #9 --- adafruit_midi/midi_message.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/adafruit_midi/midi_message.py b/adafruit_midi/midi_message.py index 7d4a1e0..3fbe8fd 100644 --- a/adafruit_midi/midi_message.py +++ b/adafruit_midi/midi_message.py @@ -122,7 +122,6 @@ class MIDIMessage: _statusandmask_to_class = [] def __init__(self, *, channel=None): - ### TODO - can i kwargs this????? self._channel = channel # dealing with pylint inadequacy self.channel = channel @@ -136,12 +135,13 @@ def channel(self): @channel.setter def channel(self, channel): if channel is not None and not 0 <= channel <= 15: - raise "channel must be 0-15 or None" + raise "Channel must be 0-15 or None" self._channel = channel @classmethod def register_message_type(cls): """Register a new message by its status value and mask. + This is called automagically at ``import`` time for each message. """ ### These must be inserted with more specific masks first insert_idx = len(MIDIMessage._statusandmask_to_class)