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)