test_MIDI_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, call 25 26 import random 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 # Full monty 40 from adafruit_midi.channel_pressure import ChannelPressure 41 from adafruit_midi.control_change import ControlChange 42 from adafruit_midi.note_off import NoteOff 43 from adafruit_midi.note_on import NoteOn 44 from adafruit_midi.pitch_bend import PitchBend 45 from adafruit_midi.polyphonic_key_pressure import PolyphonicKeyPressure 46 from adafruit_midi.program_change import ProgramChange 47 from adafruit_midi.start import Start 48 from adafruit_midi.stop import Stop 49 from adafruit_midi.system_exclusive import SystemExclusive 50 from adafruit_midi.timing_clock import TimingClock 51 52 # Import after messages - opposite to other test file 53 import adafruit_midi 54 55 56 # For loopback/echo tests 57 def MIDI_mocked_both_loopback(in_c, out_c): 58 usb_data = bytearray() 59 60 def write(buffer, length): 61 nonlocal usb_data 62 usb_data.extend(buffer[0:length]) 63 64 def read(length): 65 nonlocal usb_data 66 poppedbytes = usb_data[0:length] 67 usb_data = usb_data[len(poppedbytes) :] 68 return bytes(poppedbytes) 69 70 mockedPortIn = Mock() 71 mockedPortIn.read = read 72 mockedPortOut = Mock() 73 mockedPortOut.write = write 74 m = adafruit_midi.MIDI( 75 midi_out=mockedPortOut, midi_in=mockedPortIn, out_channel=out_c, in_channel=in_c 76 ) 77 return m 78 79 80 def MIDI_mocked_receive(in_c, data, read_sizes): 81 usb_data = bytearray(data) 82 chunks = read_sizes 83 chunk_idx = 0 84 85 def read(length): 86 nonlocal usb_data, chunks, chunk_idx 87 # pylint: disable=no-else-return 88 if length != 0 and chunk_idx < len(chunks): 89 # min() to ensure we only read what's asked for and present 90 poppedbytes = usb_data[0 : min(length, chunks[chunk_idx])] 91 usb_data = usb_data[len(poppedbytes) :] 92 if length >= chunks[chunk_idx]: 93 chunk_idx += 1 94 else: 95 chunks[chunk_idx] -= len(poppedbytes) 96 return bytes(poppedbytes) 97 else: 98 return bytes() 99 100 mockedPortIn = Mock() 101 mockedPortIn.read = read 102 103 m = adafruit_midi.MIDI( 104 midi_out=None, midi_in=mockedPortIn, out_channel=in_c, in_channel=in_c 105 ) 106 return m 107 108 109 class Test_MIDI_constructor(unittest.TestCase): 110 def test_no_inout(self): 111 # constructor likes a bit of in out 112 with self.assertRaises(ValueError): 113 adafruit_midi.MIDI() 114 115 116 class Test_MIDI(unittest.TestCase): 117 # pylint: disable=too-many-branches 118 def test_captured_data_one_byte_reads(self): 119 c = 0 120 # From an M-Audio AXIOM controller 121 raw_data = bytearray( 122 [0x90, 0x3E, 0x5F] 123 + [0xD0, 0x10] 124 + [0x90, 0x40, 0x66] 125 + [0xB0, 0x1, 0x08] 126 + [0x90, 0x41, 0x74] 127 + [0xE0, 0x03, 0x40] 128 ) 129 m = MIDI_mocked_receive(c, raw_data, [1] * len(raw_data)) 130 131 for unused in range(100): # pylint: disable=unused-variable 132 msg = m.receive() 133 if msg is not None: 134 break 135 self.assertIsInstance(msg, NoteOn) 136 self.assertEqual(msg.note, 0x3E) 137 self.assertEqual(msg.velocity, 0x5F) 138 self.assertEqual(msg.channel, c) 139 140 # for loops currently absorb any Nones but could 141 # be set to read precisely the expected number... 142 for unused in range(100): # pylint: disable=unused-variable 143 msg = m.receive() 144 if msg is not None: 145 break 146 self.assertIsInstance(msg, ChannelPressure) 147 self.assertEqual(msg.pressure, 0x10) 148 self.assertEqual(msg.channel, c) 149 150 for unused in range(100): # pylint: disable=unused-variable 151 msg = m.receive() 152 if msg is not None: 153 break 154 self.assertIsInstance(msg, NoteOn) 155 self.assertEqual(msg.note, 0x40) 156 self.assertEqual(msg.velocity, 0x66) 157 self.assertEqual(msg.channel, c) 158 159 for unused in range(100): # pylint: disable=unused-variable 160 msg = m.receive() 161 if msg is not None: 162 break 163 self.assertIsInstance(msg, ControlChange) 164 self.assertEqual(msg.control, 0x01) 165 self.assertEqual(msg.value, 0x08) 166 self.assertEqual(msg.channel, c) 167 168 for unused in range(100): # pylint: disable=unused-variable 169 msg = m.receive() 170 if msg is not None: 171 break 172 self.assertIsInstance(msg, NoteOn) 173 self.assertEqual(msg.note, 0x41) 174 self.assertEqual(msg.velocity, 0x74) 175 self.assertEqual(msg.channel, c) 176 177 for unused in range(100): # pylint: disable=unused-variable 178 msg = m.receive() 179 if msg is not None: 180 break 181 self.assertIsInstance(msg, PitchBend) 182 self.assertEqual(msg.pitch_bend, 8195) 183 self.assertEqual(msg.channel, c) 184 185 for unused in range(100): # pylint: disable=unused-variable 186 msg = m.receive() 187 self.assertIsNone(msg) 188 189 def test_unknown_before_NoteOn(self): 190 c = 0 191 # From an M-Audio AXIOM controller 192 raw_data = bytes( 193 [0b11110011, 0x10] # Song Select (not yet implemented) 194 + [0b11110011, 0x20] 195 + [0b11110100] 196 + [0b11110101] 197 ) + bytes(NoteOn("C5", 0x7F, channel=c)) 198 m = MIDI_mocked_receive(c, raw_data, [2, 2, 1, 1, 3]) 199 200 for unused in range(4): # pylint: disable=unused-variable 201 msg = m.receive() 202 self.assertIsInstance(msg, adafruit_midi.midi_message.MIDIUnknownEvent) 203 self.assertIsNone(msg.channel) 204 205 msg = m.receive() 206 self.assertIsInstance(msg, NoteOn) 207 self.assertEqual(msg.note, 0x48) # 0x48 is C5 208 self.assertEqual(msg.velocity, 0x7F) 209 self.assertEqual(msg.channel, c) 210 211 # See https://github.com/adafruit/Adafruit_CircuitPython_MIDI/issues/8 212 def test_running_status_when_implemented(self): 213 c = 8 214 raw_data = ( 215 bytes(NoteOn("C5", 0x7F, channel=c)) 216 + bytes([0xE8, 0x72, 0x40] + [0x6D, 0x40] + [0x05, 0x41]) 217 + bytes(NoteOn("D5", 0x7F, channel=c)) 218 ) 219 220 m = MIDI_mocked_receive(c, raw_data, [3 + 3 + 2 + 3 + 3]) 221 self.assertIsInstance(m, adafruit_midi.MIDI) # silence pylint! 222 # self.assertEqual(TOFINISH, WHENIMPLEMENTED) 223 224 def test_somegood_somemissing_databytes(self): 225 c = 8 226 raw_data = ( 227 bytes(NoteOn("C5", 0x7F, channel=c)) 228 + bytes( 229 [0xE8, 0x72, 0x40] 230 + [0xE8, 0x6D] # Missing last data byte 231 + [0xE8, 0x5, 0x41] 232 ) 233 + bytes(NoteOn("D5", 0x7F, channel=c)) 234 ) 235 m = MIDI_mocked_receive(c, raw_data, [3 + 3 + 2 + 3 + 3]) 236 237 msg1 = m.receive() 238 self.assertIsInstance(msg1, NoteOn) 239 self.assertEqual(msg1.note, 72) 240 self.assertEqual(msg1.velocity, 0x7F) 241 self.assertEqual(msg1.channel, c) 242 243 msg2 = m.receive() 244 self.assertIsInstance(msg2, PitchBend) 245 self.assertEqual(msg2.pitch_bend, 8306) 246 self.assertEqual(msg2.channel, c) 247 248 # The current implementation will read status bytes for data 249 # In most cases it would be a faster recovery with fewer messages 250 # lost if the next status byte wasn't consumed 251 # and parsing restarted from that byte 252 msg3 = m.receive() 253 self.assertIsInstance(msg3, adafruit_midi.midi_message.MIDIBadEvent) 254 self.assertIsInstance(msg3.data, bytes) 255 self.assertEqual(msg3.data, bytes([0xE8, 0x6D, 0xE8])) 256 self.assertIsNone(msg3.channel) 257 258 # (msg4, channel4) = m.receive() 259 # self.assertIsInstance(msg4, PitchBend) 260 # self.assertEqual(msg4.pitch_bend, 72) 261 # self.assertEqual(channel4, c) 262 263 msg5 = m.receive() 264 self.assertIsInstance(msg5, NoteOn) 265 self.assertEqual(msg5.note, 74) 266 self.assertEqual(msg5.velocity, 0x7F) 267 self.assertEqual(msg5.channel, c) 268 269 msg6 = m.receive() 270 self.assertIsNone(msg6) 271 272 def test_smallsysex_between_notes(self): 273 m = MIDI_mocked_both_loopback(3, 3) 274 275 m.send( 276 [ 277 NoteOn("C4", 0x7F), 278 SystemExclusive([0x1F], [1, 2, 3, 4, 5, 6, 7, 8]), 279 NoteOff(60, 0x28), 280 ] 281 ) 282 283 msg1 = m.receive() 284 self.assertIsInstance(msg1, NoteOn) 285 self.assertEqual(msg1.note, 60) 286 self.assertEqual(msg1.velocity, 0x7F) 287 self.assertEqual(msg1.channel, 3) 288 289 msg2 = m.receive() 290 self.assertIsInstance(msg2, SystemExclusive) 291 self.assertEqual(msg2.manufacturer_id, bytes([0x1F])) 292 self.assertEqual(msg2.data, bytes([1, 2, 3, 4, 5, 6, 7, 8])) 293 self.assertEqual(msg2.channel, None) # SysEx does not have a channel 294 295 msg3 = m.receive() 296 self.assertIsInstance(msg3, NoteOff) 297 self.assertEqual(msg3.note, 60) 298 self.assertEqual(msg3.velocity, 0x28) 299 self.assertEqual(msg3.channel, 3) 300 301 msg4 = m.receive() 302 self.assertIsNone(msg4) 303 304 def test_smallsysex_bytes_type(self): 305 s = SystemExclusive([0x1F], [100, 150, 200]) 306 307 self.assertIsInstance(s, SystemExclusive) 308 self.assertEqual(s.manufacturer_id, bytes([0x1F])) 309 self.assertIsInstance(s.manufacturer_id, bytes) 310 311 # check this really is immutable (pylint also picks this up!) 312 with self.assertRaises(TypeError): 313 s.data[0] = 0 # pylint: disable=unsupported-assignment-operation 314 315 self.assertEqual(s.data, bytes([100, 150, 200])) 316 self.assertIsInstance(s.data, bytes) 317 318 # pylint: disable=too-many-locals 319 def test_larger_than_buffer_sysex(self): 320 c = 0 321 monster_data_len = 500 322 raw_data = ( 323 bytes(NoteOn("C5", 0x7F, channel=c)) 324 + bytes( 325 SystemExclusive([0x02], [d & 0x7F for d in range(monster_data_len)]) 326 ) 327 + bytes(NoteOn("D5", 0x7F, channel=c)) 328 ) 329 m = MIDI_mocked_receive(c, raw_data, [len(raw_data)]) 330 buffer_len = m._in_buf_size # pylint: disable=protected-access 331 self.assertTrue( 332 monster_data_len > buffer_len, 333 "checking our SysEx truly is larger than buffer", 334 ) 335 336 msg1 = m.receive() 337 self.assertIsInstance(msg1, NoteOn) 338 self.assertEqual(msg1.note, 72) 339 self.assertEqual(msg1.velocity, 0x7F) 340 self.assertEqual(msg1.channel, c) 341 342 # (Ab)using python's rounding down for negative division 343 # pylint: disable=unused-variable 344 for unused in range(-(-(1 + 1 + monster_data_len + 1) // buffer_len) - 1): 345 msg2 = m.receive() 346 self.assertIsNone(msg2) 347 348 # The current implementation will read SysEx end status byte 349 # and report it as an unknown 350 msg3 = m.receive() 351 self.assertIsInstance(msg3, adafruit_midi.midi_message.MIDIUnknownEvent) 352 self.assertEqual(msg3.status, 0xF7) 353 self.assertIsNone(msg3.channel) 354 355 # (msg4, channel4) = m.receive() 356 # self.assertIsInstance(msg4, PitchBend) 357 # self.assertEqual(msg4.pitch_bend, 72) 358 # self.assertEqual(channel4, c) 359 360 msg5 = m.receive() 361 self.assertIsInstance(msg5, NoteOn) 362 self.assertEqual(msg5.note, 74) 363 self.assertEqual(msg5.velocity, 0x7F) 364 self.assertEqual(msg5.channel, c) 365 366 msg6 = m.receive() 367 self.assertIsNone(msg6) 368 369 370 # pylint does not like mock_calls - must be a better way to handle this? 371 # pylint: disable=no-member 372 class Test_MIDI_send(unittest.TestCase): 373 def test_send_basic_single(self): 374 # def printit(buffer, len): 375 # print(buffer[0:len]) 376 mockedPortOut = Mock() 377 # mockedPortOut.write = printit 378 379 m = adafruit_midi.MIDI(midi_out=mockedPortOut, out_channel=2) 380 381 # Test sending some NoteOn and NoteOff to various channels 382 nextcall = 0 383 m.send(NoteOn(0x60, 0x7F)) 384 self.assertEqual( 385 mockedPortOut.write.mock_calls[nextcall], call(b"\x92\x60\x7f", 3) 386 ) 387 nextcall += 1 388 m.send(NoteOn(0x64, 0x3F)) 389 self.assertEqual( 390 mockedPortOut.write.mock_calls[nextcall], call(b"\x92\x64\x3f", 3) 391 ) 392 nextcall += 1 393 m.send(NoteOn(0x67, 0x1F)) 394 self.assertEqual( 395 mockedPortOut.write.mock_calls[nextcall], call(b"\x92\x67\x1f", 3) 396 ) 397 nextcall += 1 398 399 m.send(NoteOn(0x60, 0x00)) # Alternative to NoteOff 400 self.assertEqual( 401 mockedPortOut.write.mock_calls[nextcall], call(b"\x92\x60\x00", 3) 402 ) 403 nextcall += 1 404 m.send(NoteOff(0x64, 0x01)) 405 self.assertEqual( 406 mockedPortOut.write.mock_calls[nextcall], call(b"\x82\x64\x01", 3) 407 ) 408 nextcall += 1 409 m.send(NoteOff(0x67, 0x02)) 410 self.assertEqual( 411 mockedPortOut.write.mock_calls[nextcall], call(b"\x82\x67\x02", 3) 412 ) 413 nextcall += 1 414 415 # Setting channel to non default 416 m.send(NoteOn(0x6C, 0x7F), channel=9) 417 self.assertEqual( 418 mockedPortOut.write.mock_calls[nextcall], call(b"\x99\x6c\x7f", 3) 419 ) 420 nextcall += 1 421 422 m.send(NoteOff(0x6C, 0x7F), channel=9) 423 self.assertEqual( 424 mockedPortOut.write.mock_calls[nextcall], call(b"\x89\x6c\x7f", 3) 425 ) 426 nextcall += 1 427 428 def test_send_badnotes(self): 429 mockedPortOut = Mock() 430 431 m = adafruit_midi.MIDI(midi_out=mockedPortOut, out_channel=2) 432 433 # Test sending some NoteOn and NoteOff to various channels 434 nextcall = 0 435 m.send(NoteOn(60, 0x7F)) 436 self.assertEqual( 437 mockedPortOut.write.mock_calls[nextcall], call(b"\x92\x3c\x7f", 3) 438 ) 439 nextcall += 1 440 with self.assertRaises(ValueError): 441 m.send(NoteOn(64, 0x80)) # Velocity > 127 - illegal value 442 443 with self.assertRaises(ValueError): 444 m.send(NoteOn(67, -1)) 445 446 # test after exceptions to ensure sending is still ok 447 m.send(NoteOn(72, 0x7F)) 448 self.assertEqual( 449 mockedPortOut.write.mock_calls[nextcall], call(b"\x92\x48\x7f", 3) 450 ) 451 nextcall += 1 452 453 def test_send_basic_sequences(self): 454 # def printit(buffer, len): 455 # print(buffer[0:len]) 456 mockedPortOut = Mock() 457 # mockedPortOut.write = printit 458 459 m = adafruit_midi.MIDI(midi_out=mockedPortOut, out_channel=2) 460 461 # Test sending some NoteOn and NoteOff to various channels 462 nextcall = 0 463 # Test sequences with list syntax and pass a tuple too 464 note_list = [NoteOn(0x6C, 0x51), NoteOn(0x70, 0x52), NoteOn(0x73, 0x53)] 465 note_tuple = tuple(note_list) 466 m.send(note_list, channel=10) 467 self.assertEqual( 468 mockedPortOut.write.mock_calls[nextcall], 469 call(b"\x9a\x6c\x51\x9a\x70\x52\x9a\x73\x53", 9), 470 "The implementation writes in one go, single 9 byte write expected", 471 ) 472 nextcall += 1 473 m.send(note_tuple, channel=11) 474 self.assertEqual( 475 mockedPortOut.write.mock_calls[nextcall], 476 call(b"\x9b\x6c\x51\x9b\x70\x52\x9b\x73\x53", 9), 477 "The implementation writes in one go, single 9 byte write expected", 478 ) 479 nextcall += 1 480 481 def test_termination_with_random_data(self): 482 """Test with a random stream of bytes to ensure that the parsing code 483 termates and returns, i.e. does not go into any infinite loops. 484 """ 485 c = 0 486 random.seed(303808) 487 raw_data = bytearray([random.randint(0, 255) for i in range(50000)]) 488 m = MIDI_mocked_receive(c, raw_data, [len(raw_data)]) 489 490 noinfiniteloops = False 491 for unused in range(len(raw_data)): # pylint: disable=unused-variable 492 m.receive() # not interested in returned tuple 493 494 noinfiniteloops = True # interested in getting to here 495 self.assertTrue(noinfiniteloops) 496 497 498 if __name__ == "__main__": 499 unittest.main(verbosity=verbose)