/ tests / test_MIDIMessage_unittests.py
test_MIDIMessage_unittests.py
  1  # The MIT License (MIT)
  2  #
  3  # Copyright (c) 2019 Kevin J. Walters
  4  #
  5  # Permission is hereby granted, free of charge, to any person obtaining a copy
  6  # of this software and associated documentation files (the "Software"), to deal
  7  # in the Software without restriction, including without limitation the rights
  8  # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  9  # copies of the Software, and to permit persons to whom the Software is
 10  # furnished to do so, subject to the following conditions:
 11  #
 12  # The above copyright notice and this permission notice shall be included in
 13  # all copies or substantial portions of the Software.
 14  #
 15  # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 16  # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 17  # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 18  # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 19  # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 20  # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 21  # THE SOFTWARE.
 22  
 23  import unittest
 24  from unittest.mock import Mock, MagicMock
 25  
 26  
 27  import os
 28  
 29  verbose = int(os.getenv("TESTVERBOSE", "2"))
 30  
 31  # adafruit_midi had an import usb_midi
 32  import sys
 33  
 34  # sys.modules['usb_midi'] = MagicMock()
 35  
 36  # Borrowing the dhalbert/tannewt technique from adafruit/Adafruit_CircuitPython_Motor
 37  sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
 38  
 39  # Import before messages - opposite to other test file
 40  import adafruit_midi
 41  
 42  # Full monty
 43  from adafruit_midi.channel_pressure import ChannelPressure
 44  from adafruit_midi.control_change import ControlChange
 45  from adafruit_midi.note_off import NoteOff
 46  from adafruit_midi.note_on import NoteOn
 47  from adafruit_midi.pitch_bend import PitchBend
 48  from adafruit_midi.polyphonic_key_pressure import PolyphonicKeyPressure
 49  from adafruit_midi.program_change import ProgramChange
 50  from adafruit_midi.start import Start
 51  from adafruit_midi.stop import Stop
 52  from adafruit_midi.system_exclusive import SystemExclusive
 53  from adafruit_midi.timing_clock import TimingClock
 54  
 55  
 56  class Test_MIDIMessage_from_message_byte_tests(unittest.TestCase):
 57      def test_NoteOn_basic(self):
 58          data = bytes([0x90, 0x30, 0x7F])
 59          ichannel = 0
 60  
 61          (msg, msgendidxplusone, skipped) = adafruit_midi.MIDIMessage.from_message_bytes(
 62              data, ichannel
 63          )
 64  
 65          self.assertIsInstance(msg, NoteOn)
 66          self.assertEqual(msg.note, 0x30)
 67          self.assertEqual(msg.velocity, 0x7F)
 68          self.assertEqual(msgendidxplusone, 3)
 69          self.assertEqual(skipped, 0)
 70          self.assertEqual(msg.channel, 0)
 71  
 72      def test_NoteOn_awaitingthirdbyte(self):
 73          data = bytes([0x90, 0x30])
 74          ichannel = 0
 75  
 76          (msg, msgendidxplusone, skipped) = adafruit_midi.MIDIMessage.from_message_bytes(
 77              data, ichannel
 78          )
 79          self.assertIsNone(msg)
 80          self.assertEqual(
 81              msgendidxplusone,
 82              skipped,
 83              "skipped must be 0 as it only indicates bytes before a status byte",
 84          )
 85          self.assertEqual(
 86              msgendidxplusone,
 87              0,
 88              "msgendidxplusone must be 0 as buffer must be lest as is for more data",
 89          )
 90          self.assertEqual(skipped, 0)
 91  
 92      def test_NoteOn_predatajunk(self):
 93          data = bytes([0x20, 0x64, 0x90, 0x30, 0x32])
 94          ichannel = 0
 95  
 96          (msg, msgendidxplusone, skipped) = adafruit_midi.MIDIMessage.from_message_bytes(
 97              data, ichannel
 98          )
 99  
100          self.assertIsInstance(msg, NoteOn)
101          self.assertEqual(msg.note, 0x30)
102          self.assertEqual(msg.velocity, 0x32)
103          self.assertEqual(
104              msgendidxplusone,
105              5,
106              "data bytes from partial message and messages are removed",
107          )
108          self.assertEqual(skipped, 2)
109          self.assertEqual(msg.channel, 0)
110  
111      def test_NoteOn_prepartialsysex(self):
112          data = bytes([0x01, 0x02, 0x03, 0x04, 0xF7, 0x90, 0x30, 0x32])
113          ichannel = 0
114  
115          (msg, msgendidxplusone, skipped) = adafruit_midi.MIDIMessage.from_message_bytes(
116              data, ichannel
117          )
118  
119          # MIDIMessage parsing could be improved to return something that
120          # indicates its a truncated end of SysEx
121          self.assertIsInstance(msg, adafruit_midi.midi_message.MIDIUnknownEvent)
122          self.assertEqual(msg.status, 0xF7)
123          self.assertEqual(
124              msgendidxplusone,
125              5,
126              "removal of the end of the partial SysEx data and terminating status byte",
127          )
128          self.assertEqual(skipped, 4, "skipped only counts data bytes so will be 4 here")
129          self.assertIsNone(msg.channel)
130  
131          data = data[msgendidxplusone:]
132          (msg, msgendidxplusone, skipped) = adafruit_midi.MIDIMessage.from_message_bytes(
133              data, ichannel
134          )
135  
136          self.assertIsInstance(
137              msg,
138              NoteOn,
139              "NoteOn is expected if SystemExclusive is loaded otherwise it would be MIDIUnknownEvent",
140          )
141          self.assertEqual(msg.note, 0x30)
142          self.assertEqual(msg.velocity, 0x32)
143          self.assertEqual(msgendidxplusone, 3, "NoteOn message removed")
144          self.assertEqual(skipped, 0)
145          self.assertEqual(msg.channel, 0)
146  
147      def test_NoteOn_postNoteOn(self):
148          data = bytes([0x90 | 0x08, 0x30, 0x7F, 0x90 | 0x08, 0x37, 0x64])
149          ichannel = 8
150  
151          (msg, msgendidxplusone, skipped) = adafruit_midi.MIDIMessage.from_message_bytes(
152              data, ichannel
153          )
154  
155          self.assertIsInstance(msg, NoteOn)
156          self.assertEqual(msg.note, 0x30)
157          self.assertEqual(msg.velocity, 0x7F)
158          self.assertEqual(msgendidxplusone, 3)
159          self.assertEqual(skipped, 0)
160          self.assertEqual(msg.channel, 8)
161  
162      def test_NoteOn_postpartialNoteOn(self):
163          data = bytes([0x90, 0x30, 0x7F, 0x90, 0x37])
164          ichannel = 0
165  
166          (msg, msgendidxplusone, skipped) = adafruit_midi.MIDIMessage.from_message_bytes(
167              data, ichannel
168          )
169  
170          self.assertIsInstance(msg, NoteOn)
171          self.assertEqual(msg.note, 0x30)
172          self.assertEqual(msg.velocity, 0x7F)
173          self.assertEqual(msgendidxplusone, 3, "Only first message is removed")
174          self.assertEqual(skipped, 0)
175          self.assertEqual(msg.channel, 0)
176  
177      def test_NoteOn_preotherchannel(self):
178          data = bytes([0x90 | 0x05, 0x30, 0x7F, 0x90 | 0x03, 0x37, 0x64])
179          ichannel = 3
180  
181          (msg, msgendidxplusone, skipped) = adafruit_midi.MIDIMessage.from_message_bytes(
182              data, ichannel
183          )
184  
185          self.assertIsInstance(msg, NoteOn)
186          self.assertEqual(msg.note, 0x37)
187          self.assertEqual(msg.velocity, 0x64)
188          self.assertEqual(msgendidxplusone, 6, "Both messages are removed from buffer")
189          self.assertEqual(skipped, 0)
190          self.assertEqual(msg.channel, 3)
191  
192      def test_NoteOn_preotherchannelplusintermediatejunk(self):
193          data = bytes([0x90 | 0x05, 0x30, 0x7F, 0x00, 0x00, 0x90 | 0x03, 0x37, 0x64])
194          ichannel = 3
195  
196          (msg, msgendidxplusone, skipped) = adafruit_midi.MIDIMessage.from_message_bytes(
197              data, ichannel
198          )
199  
200          self.assertIsInstance(msg, NoteOn)
201          self.assertEqual(msg.note, 0x37)
202          self.assertEqual(msg.velocity, 0x64)
203          self.assertEqual(
204              msgendidxplusone, 8, "Both messages and junk are removed from buffer"
205          )
206          self.assertEqual(skipped, 0)
207          self.assertEqual(msg.channel, 3)
208  
209      def test_NoteOn_wrongchannel(self):
210          data = bytes([0x95, 0x30, 0x7F])
211          ichannel = 3
212  
213          (msg, msgendidxplusone, skipped) = adafruit_midi.MIDIMessage.from_message_bytes(
214              data, ichannel
215          )
216  
217          self.assertIsNone(msg)
218          self.assertEqual(msgendidxplusone, 3, "wrong channel message discarded")
219          self.assertEqual(skipped, 0)
220  
221      def test_NoteOn_partialandpreotherchannel1(self):
222          data = bytes([0x95, 0x30, 0x7F, 0x93])
223          ichannel = 3
224  
225          (msg, msgendidxplusone, skipped) = adafruit_midi.MIDIMessage.from_message_bytes(
226              data, ichannel
227          )
228  
229          self.assertIsNone(msg)
230          self.assertEqual(
231              msgendidxplusone, 3, "first message discarded, second partial left"
232          )
233          self.assertEqual(skipped, 0)
234  
235      def test_NoteOn_partialandpreotherchannel2(self):
236          data = bytes([0x95, 0x30, 0x7F, 0x93, 0x37])
237          ichannel = 3
238  
239          (msg, msgendidxplusone, skipped) = adafruit_midi.MIDIMessage.from_message_bytes(
240              data, ichannel
241          )
242  
243          self.assertIsNone(msg)
244          self.assertEqual(
245              msgendidxplusone, 3, "first message discarded, second partial left"
246          )
247          self.assertEqual(skipped, 0)
248  
249      def test_NoteOn_constructor_int(self):
250          object1 = NoteOn(60, 0x7F)
251  
252          self.assertEqual(object1.note, 60)
253          self.assertEqual(object1.velocity, 0x7F)
254          self.assertIsNone(object1.channel)
255  
256          object2 = NoteOn(60, 0x00)  # equivalent of NoteOff
257  
258          self.assertEqual(object2.note, 60)
259          self.assertEqual(object2.velocity, 0x00)
260          self.assertIsNone(object2.channel)
261  
262          object3 = NoteOn(60, 0x50, channel=7)
263  
264          self.assertEqual(object3.note, 60)
265          self.assertEqual(object3.velocity, 0x50)
266          self.assertEqual(object3.channel, 7)
267  
268          object4 = NoteOn(60)  # velocity defaults to 127
269  
270          self.assertEqual(object4.note, 60)
271          self.assertEqual(object4.velocity, 127)
272          self.assertIsNone(object4.channel)
273  
274      def test_SystemExclusive_NoteOn(self):
275          data = bytes([0xF0, 0x42, 0x01, 0x02, 0x03, 0x04, 0xF7, 0x90 | 14, 0x30, 0x60])
276          ichannel = 14
277  
278          (msg, msgendidxplusone, skipped) = adafruit_midi.MIDIMessage.from_message_bytes(
279              data, ichannel
280          )
281  
282          self.assertIsInstance(msg, SystemExclusive)
283          self.assertEqual(msg.manufacturer_id, bytes([0x42]))  # Korg
284          self.assertEqual(msg.data, bytes([0x01, 0x02, 0x03, 0x04]))
285          self.assertEqual(msgendidxplusone, 7)
286          self.assertEqual(
287              skipped, 0, "If SystemExclusive class is imported then this must be 0"
288          )
289          self.assertIsNone(msg.channel)
290  
291          (msg, msgendidxplusone, skipped) = adafruit_midi.MIDIMessage.from_message_bytes(
292              data[msgendidxplusone:], ichannel
293          )
294  
295          self.assertIsInstance(msg, NoteOn)
296          self.assertEqual(msg.note, 48)
297          self.assertEqual(msg.velocity, 0x60)
298          self.assertEqual(msgendidxplusone, 3)
299          self.assertEqual(skipped, 0)
300          self.assertEqual(msg.channel, 14)
301  
302      def test_SystemExclusive_NoteOn_premalterminatedsysex(self):
303          data = bytes([0xF0, 0x42, 0x01, 0x02, 0x03, 0x04, 0xF0, 0x90, 0x30, 0x32])
304          ichannel = 0
305  
306          # 0xf0 is incorrect status to mark end of this message, must be 0xf7
307          (msg, msgendidxplusone, skipped) = adafruit_midi.MIDIMessage.from_message_bytes(
308              data, ichannel
309          )
310  
311          self.assertIsNone(msg)
312          self.assertEqual(msgendidxplusone, 7)
313          self.assertEqual(
314              skipped, 0, "If SystemExclusive class is imported then this must be 0"
315          )
316  
317      def test_Unknown_SinglebyteStatus(self):
318          data = bytes([0xFD])
319          ichannel = 0
320  
321          (msg, msgendidxplusone, skipped) = adafruit_midi.MIDIMessage.from_message_bytes(
322              data, ichannel
323          )
324  
325          self.assertIsInstance(msg, adafruit_midi.midi_message.MIDIUnknownEvent)
326          self.assertEqual(msgendidxplusone, 1)
327          self.assertEqual(skipped, 0)
328          self.assertIsNone(msg.channel)
329  
330      def test_Empty(self):
331          data = bytes([])
332          ichannel = 0
333  
334          (msg, msgendidxplusone, skipped) = adafruit_midi.MIDIMessage.from_message_bytes(
335              data, ichannel
336          )
337  
338          self.assertIsNone(msg)
339          self.assertEqual(msgendidxplusone, 0)
340          self.assertEqual(skipped, 0)
341  
342  
343  class Test_MIDIMessage_NoteOn_constructor(unittest.TestCase):
344      def test_NoteOn_constructor_string(self):
345          object1 = NoteOn("C4", 0x64)
346          self.assertEqual(object1.note, 60)
347          self.assertEqual(object1.velocity, 0x64)
348  
349          object2 = NoteOn("C3", 0x7F)
350          self.assertEqual(object2.note, 48)
351          self.assertEqual(object2.velocity, 0x7F)
352  
353          object3 = NoteOn("C#4", 0x00)
354          self.assertEqual(object3.note, 61)
355          self.assertEqual(object3.velocity, 0)
356  
357      def test_NoteOn_constructor_valueerror1(self):
358          with self.assertRaises(ValueError):
359              NoteOn(60, 0x80)  # pylint is happier if return value not stored
360  
361      def test_NoteOn_constructor_valueerror2(self):
362          with self.assertRaises(ValueError):
363              NoteOn(-1, 0x7F)
364  
365      def test_NoteOn_constructor_valueerror3(self):
366          with self.assertRaises(ValueError):
367              NoteOn(128, 0x7F)
368  
369      def test_NoteOn_constructor_upperrange1(self):
370          object1 = NoteOn("G9", 0x7F)
371          self.assertEqual(object1.note, 127)
372          self.assertEqual(object1.velocity, 0x7F)
373  
374      def test_NoteOn_constructor_upperrange2(self):
375          with self.assertRaises(ValueError):
376              NoteOn("G#9", 0x7F)  # just above max note
377  
378      def test_NoteOn_constructor_bogusstring(self):
379          with self.assertRaises(ValueError):
380              NoteOn("CC4", 0x7F)
381  
382  
383  class Test_MIDIMessage_NoteOff_constructor(unittest.TestCase):
384      # mostly cut and paste from NoteOn above
385      def test_NoteOff_constructor_string(self):
386          object1 = NoteOff("C4", 0x64)
387          self.assertEqual(object1.note, 60)
388          self.assertEqual(object1.velocity, 0x64)
389  
390          object2 = NoteOff("C3", 0x7F)
391          self.assertEqual(object2.note, 48)
392          self.assertEqual(object2.velocity, 0x7F)
393  
394          object3 = NoteOff("C#4", 0x00)
395          self.assertEqual(object3.note, 61)
396          self.assertEqual(object3.velocity, 0)
397  
398          object4 = NoteOff("C#4")  # velocity defaults to 0
399          self.assertEqual(object4.note, 61)
400          self.assertEqual(object4.velocity, 0)
401  
402      def test_NoteOff_constructor_valueerror1(self):
403          with self.assertRaises(ValueError):
404              NoteOff(60, 0x80)
405  
406      def test_NoteOff_constructor_valueerror2(self):
407          with self.assertRaises(ValueError):
408              NoteOff(-1, 0x7F)
409  
410      def test_NoteOff_constructor_valueerror3(self):
411          with self.assertRaises(ValueError):
412              NoteOff(128, 0x7F)
413  
414      def test_NoteOff_constructor_upperrange1(self):
415          object1 = NoteOff("G9", 0x7F)
416          self.assertEqual(object1.note, 127)
417          self.assertEqual(object1.velocity, 0x7F)
418  
419      def test_NoteOff_constructor_upperrange2(self):
420          with self.assertRaises(ValueError):
421              NoteOff("G#9", 0x7F)  # just above max note
422  
423      def test_NoteOff_constructor_bogusstring(self):
424          with self.assertRaises(ValueError):
425              NoteOff("CC4", 0x7F)
426  
427  
428  if __name__ == "__main__":
429      unittest.main(verbosity=verbose)