1 /* 2 * Copyright (C) 2018 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.cts.verifier.audio; 18 19 import java.io.IOException; 20 import java.util.ArrayList; 21 22 import android.media.midi.MidiDevice; 23 import android.media.midi.MidiDeviceInfo; 24 import android.media.midi.MidiInputPort; 25 import android.media.midi.MidiManager; 26 import android.media.midi.MidiReceiver; 27 import android.os.Bundle; 28 import android.util.Log; 29 30 import com.android.cts.verifier.R; 31 32 import com.android.cts.verifier.audio.midilib.MidiIODevice; 33 34 /* 35 * A note about the USB MIDI device. 36 * Any USB MIDI peripheral with standard female DIN jacks can be used. A standard MIDI cable 37 * plugged into both input and output is required for the USB Loopback Test. A Bluetooth MIDI 38 * device like the Yamaha MD-BT01 plugged into both input and output is required for the 39 * Bluetooth Loopback test. 40 */ 41 42 /* 43 * A note about the "virtual MIDI" device... 44 * See file MidiEchoService for implementation of the echo server itself. 45 * This service is started by the main manifest file (AndroidManifest.xml). 46 */ 47 48 /* 49 * A note about Bluetooth MIDI devices... 50 * Any Bluetooth MIDI device needs to be paired with the DUT with the "MIDI+BTLE" application 51 * available in the Play Store: 52 * (https://play.google.com/store/apps/details?id=com.mobileer.example.midibtlepairing). 53 */ 54 55 /** 56 * CTS Verifier Activity for MIDI test 57 */ 58 public class MidiJavaTestActivity extends MidiTestActivityBase { 59 private static final String TAG = "MidiJavaTestActivity"; 60 private static final boolean DEBUG = false; 61 MidiJavaTestActivity()62 public MidiJavaTestActivity() { 63 super(); 64 initTestModules(new JavaMidiTestModule(MidiDeviceInfo.TYPE_USB), 65 new JavaMidiTestModule(MidiDeviceInfo.TYPE_VIRTUAL), 66 new BTMidiTestModule()); 67 } 68 69 @Override onCreate(Bundle savedInstanceState)70 protected void onCreate(Bundle savedInstanceState) { 71 if (DEBUG) { 72 Log.i(TAG, "---- onCreate()"); 73 } 74 75 setContentView(R.layout.midi_activity); 76 77 super.onCreate(savedInstanceState); 78 79 startMidiEchoServer(); 80 scanMidiDevices(); 81 82 connectDeviceListener(); 83 } 84 85 // 86 // MIDI Messages 87 // 88 // channel-oriented message (Commands) 89 public static final byte MIDICMD_NOTEON = 9; 90 public static final byte MIDICMD_NOTEOFF = 8; 91 public static final byte MIDICMD_POLYPRESS = 10; 92 public static final byte MIDICMD_CONTROL = 11; 93 public static final byte MIDICMD_PROGRAMCHANGE = 12; 94 public static final byte MIDICMD_CHANNELPRESS = 13; 95 public static final byte MIDICMD_PITCHWHEEL = 14; 96 public static final byte MIDICMD_SYSEX = (byte)0xF0; 97 public static final byte MIDICMD_EOSYSEX = (byte)0xF7; // (byte)0b11110111; // 0xF7 98 99 private class TestMessage { 100 public byte[] mMsgBytes; 101 matches(byte[] msg, int offset, int count)102 public boolean matches(byte[] msg, int offset, int count) { 103 // Length 104 if (DEBUG) { 105 Log.i(TAG, " count [" + count + " : " + mMsgBytes.length + "]"); 106 } 107 if (count != mMsgBytes.length) { 108 return false; 109 } 110 111 // Data 112 for(int index = 0; index < count; index++) { 113 if (DEBUG) { 114 Log.i(TAG, " [" + msg[offset + index] + " : " + mMsgBytes[index] + "]"); 115 } 116 if (msg[offset + index] != mMsgBytes[index]) { 117 return false; 118 } 119 } 120 return true; 121 } 122 } /* class TestMessage */ 123 makeMIDICmd(int cmd, int channel)124 private static byte makeMIDICmd(int cmd, int channel) { 125 return (byte)((cmd << 4) | (channel & 0x0F)); 126 } 127 128 // 129 // Logging Utility 130 // logByteArray(String prefix, byte[] value, int offset, int count)131 static void logByteArray(String prefix, byte[] value, int offset, int count) { 132 StringBuilder builder = new StringBuilder(prefix); 133 for (int i = 0; i < count; i++) { 134 builder.append(String.format("0x%02X", value[offset + i])); 135 if (i != value.length - 1) { 136 builder.append(", "); 137 } 138 } 139 Log.d(TAG, builder.toString()); 140 } 141 logByteArray(String prefix, ArrayList<Byte> value, int offset)142 static void logByteArray(String prefix, ArrayList<Byte> value, int offset) { 143 StringBuilder builder = new StringBuilder(prefix); 144 for (int i = 0; i < value.size(); i++) { 145 builder.append(String.format("0x%02X", value.get(offset + i))); 146 if (i != value.size() - 1) { 147 builder.append(", "); 148 } 149 } 150 Log.d(TAG, builder.toString()); 151 } 152 153 /** 154 * A class to control and represent the state of a given test. 155 * It hold the data needed for IO, and the logic for sending, receiving and matching 156 * the MIDI data stream. 157 */ 158 private class JavaMidiTestModule extends MidiTestModule { 159 private static final String TAG = "JavaMidiTestModule"; 160 161 protected boolean mTestMismatched; 162 163 // Test Data 164 // - The set of messages to send 165 private TestMessage[] mTestMessages; 166 167 // - The stream of message data to walk through when MIDI data is received. 168 // NOTE: To work on USB Audio Peripherals that drop the first message 169 // (AudioBoxUSB), have 2 streams to match against, one with the "warm-up" 170 // message in tact ("Nominal") and one where it is absent. 171 private ArrayList<Byte> mMatchStream = new ArrayList<Byte>(); 172 173 private int mReceiveStreamPos; 174 private static final int MESSAGE_MAX_BYTES = 1024; 175 176 // Some MIDI interfaces have been know to consistently drop the first message 177 // Send one to throw away. If it shows up, ignore it. If it doesn't then 178 // there is nothing there to ignore and the remainder should be legitimate. 179 // Use the MIDI CONTROL command to identify this "warm-up" message 180 private byte[] mWarmUpMsg = {makeMIDICmd(MIDICMD_CONTROL, 0), 0, 0}; 181 JavaMidiTestModule(int deviceType)182 public JavaMidiTestModule(int deviceType) { 183 super(deviceType); 184 setupTestMessages(); 185 } 186 187 @Override startLoopbackTest(int testId)188 void startLoopbackTest(int testId) { 189 synchronized (mTestLock) { 190 mTestRunning = true; 191 enableTestButtons(false); 192 } 193 194 if (DEBUG) { 195 Log.i(TAG, "---- startLoopbackTest()"); 196 } 197 198 mTestStatus = TESTSTATUS_NOTRUN; 199 mTestMismatched = false; 200 mReceiveStreamPos = 0; 201 202 mRunningTestID = testId; 203 204 // These might be left open due to a failing, previous test 205 // so just to be sure... 206 closePorts(); 207 208 if (mIODevice.mSendDevInfo != null) { 209 mMidiManager.openDevice(mIODevice.mSendDevInfo, new TestModuleOpenListener(), null); 210 } 211 212 startTimeoutHandler(); 213 } 214 openPorts(MidiDevice device)215 protected void openPorts(MidiDevice device) { 216 mIODevice.openPorts(device, new MidiMatchingReceiver()); 217 } 218 closePorts()219 protected void closePorts() { 220 mIODevice.closePorts(); 221 } 222 223 @Override hasTestPassed()224 boolean hasTestPassed() { 225 return mTestStatus == TESTSTATUS_PASSED; 226 } 227 228 // A little explanation here... It seems reasonable to send complete MIDI messages, i.e. 229 // as a set of discrete pakages. 230 // However the looped-back data may not be delivered in message-size packets, so it makes more 231 // sense to look at that as a stream of bytes. 232 // So we build a set of messages to send, and then create the equivalent stream of bytes 233 // from that to match against when received back in from the looped-back device. setupTestMessages()234 private void setupTestMessages() { 235 if (DEBUG) { 236 Log.i(TAG, "setupTestMessages()"); 237 } 238 int NUM_TEST_MESSAGES = 3; 239 mTestMessages = new TestMessage[NUM_TEST_MESSAGES]; 240 241 //TODO - Investgate using ByteArrayOutputStream for these data streams. 242 243 // 244 // Set up any set of messages you want 245 // Except for the command IDs, the data values are purely arbitrary and meaningless 246 // outside of being matched. 247 // KeyDown 248 mTestMessages[0] = new TestMessage(); 249 mTestMessages[0].mMsgBytes = new byte[]{makeMIDICmd(MIDICMD_NOTEON, 0), 64, 12}; 250 251 // KeyUp 252 mTestMessages[1] = new TestMessage(); 253 mTestMessages[1].mMsgBytes = new byte[]{makeMIDICmd(MIDICMD_NOTEOFF, 0), 73, 65}; 254 255 // SysEx 256 // NOTE: A sysex on the MT-BT01 seems to top out at sometimes as low as 40 bytes. 257 // It is not clear, but needs more research. For now choose a conservative size. 258 int sysExSize = 32; 259 byte[] sysExMsg = new byte[sysExSize]; 260 sysExMsg[0] = MIDICMD_SYSEX; 261 for(int index = 1; index < sysExSize-1; index++) { 262 sysExMsg[index] = (byte)index; 263 } 264 sysExMsg[sysExSize-1] = (byte) MIDICMD_EOSYSEX; 265 mTestMessages[2] = new TestMessage(); 266 mTestMessages[2].mMsgBytes = sysExMsg; 267 268 // 269 // Now build the stream to match against 270 // 271 mMatchStream.clear(); 272 for(int byteIndex = 0; byteIndex < mWarmUpMsg.length; byteIndex++) { 273 mMatchStream.add(mWarmUpMsg[byteIndex]); 274 } 275 for (int msgIndex = 0; msgIndex < mTestMessages.length; msgIndex++) { 276 for(int byteIndex = 0; byteIndex < mTestMessages[msgIndex].mMsgBytes.length; byteIndex++) { 277 mMatchStream.add(mTestMessages[msgIndex].mMsgBytes[byteIndex]); 278 } 279 } 280 281 mReceiveStreamPos = 0; 282 283 if (DEBUG) { 284 logByteArray("mMatchStream: ", mMatchStream, 0); 285 } 286 } 287 288 /** 289 * Compares the supplied bytes against the sent message stream at the current position 290 * and advances the stream position. 291 */ matchStream(byte[] bytes, int offset, int count)292 private boolean matchStream(byte[] bytes, int offset, int count) { 293 if (DEBUG) { 294 Log.i(TAG, "---- matchStream() offset:" + offset + " count:" + count); 295 } 296 // a little bit of checking here... 297 if (count < 0) { 298 Log.e(TAG, "Negative Byte Count in MidiActivity::matchStream()"); 299 return false; 300 } 301 302 if (count > MESSAGE_MAX_BYTES) { 303 Log.e(TAG, "Too Large Byte Count (" + count + ") in MidiActivity::matchStream()"); 304 return false; 305 } 306 307 boolean matches = true; 308 309 for (int index = 0; index < count; index++) { 310 // Avoid a buffer overrun. Still don't understand why it happens 311 if (mReceiveStreamPos >= mMatchStream.size()) { 312 // report an error here 313 Log.d(TAG, "matchStream buffer overrun @" + index + 314 " of " + mMatchStream.size()); 315 // Dump the bufer here 316 logByteArray("Expected: ", mMatchStream, 0); 317 matches = false; 318 break; // bail 319 } 320 321 if (bytes[offset + index] != mMatchStream.get(mReceiveStreamPos)) { 322 matches = false; 323 if (DEBUG) { 324 int gotValue = bytes[offset + index] & 0x000000FF; 325 int expectedValue = mMatchStream.get(mReceiveStreamPos) & 0x000000FF; 326 Log.i(TAG, "---- mismatch @" 327 + index 328 + " [0x" + Integer.toHexString(gotValue) 329 + " : 0x" + Integer.toHexString(expectedValue) 330 + "]"); 331 } 332 break; 333 } 334 mReceiveStreamPos++; 335 } 336 337 if (DEBUG) { 338 Log.i(TAG, " returns:" + matches); 339 } 340 341 return matches; 342 } 343 344 // In some instances, BlueTooth MIDI in particular, it is possible to overrun 345 // the bandwidth, resulting in lost data. In this case, slow the data stream 346 // down. 347 private static final int THROTTLE_PERIOD_MS = 10; portSend(MidiInputPort inputPort, byte[] bytes, int offset, int length, boolean throttle)348 private void portSend(MidiInputPort inputPort, byte[] bytes, int offset, int length, 349 boolean throttle) { 350 if (DEBUG) { 351 Log.i(TAG, "portSend() throttle:" + throttle); 352 } 353 try { 354 if (throttle) { 355 try { 356 for (int index = 0; index < length; index++) { 357 inputPort.send(bytes, offset + index, 1); 358 Thread.sleep(THROTTLE_PERIOD_MS); 359 } 360 } catch (InterruptedException ex) { 361 Log.i(TAG, "---- InterruptedException " + ex); 362 } 363 } else { 364 inputPort.send(bytes, offset, length); 365 } 366 } catch (IOException ex) { 367 Log.i(TAG, "---- IOException " + ex); 368 } 369 } 370 371 /** 372 * Writes out the list of MIDI messages to the output port. 373 */ sendMessages()374 private void sendMessages() { 375 if (DEBUG) { 376 Log.i(TAG, "---- sendMessages()..."); 377 } 378 379 synchronized (mTestLock) { 380 int totalSent = 0; 381 if (mIODevice.mSendPort != null) { 382 // Send a warm-up message... 383 logByteArray("warm-up: ", mWarmUpMsg, 0, mWarmUpMsg.length); 384 portSend(mIODevice.mSendPort, mWarmUpMsg, 0, mWarmUpMsg.length, 385 mRunningTestID == TESTID_BTLOOPBACK); 386 387 for (TestMessage msg : mTestMessages) { 388 if (DEBUG) { 389 logByteArray("send: ", msg.mMsgBytes, 0, msg.mMsgBytes.length); 390 } 391 portSend(mIODevice.mSendPort, msg.mMsgBytes, 0, msg.mMsgBytes.length, 392 mRunningTestID == TESTID_BTLOOPBACK); 393 totalSent += msg.mMsgBytes.length; 394 } 395 } 396 if (DEBUG) { 397 Log.i(TAG, "---- totalSent:" + totalSent); 398 } 399 } 400 } 401 402 /** 403 * Listens for MIDI device opens. Opens I/O ports and sends out the apriori 404 * setup messages. 405 */ 406 class TestModuleOpenListener implements MidiManager.OnDeviceOpenedListener { 407 @Override onDeviceOpened(MidiDevice device)408 public void onDeviceOpened(MidiDevice device) { 409 if (DEBUG) { 410 Log.i(TAG, "---- onDeviceOpened()"); 411 } 412 openPorts(device); 413 sendMessages(); 414 } 415 } 416 417 /** 418 * A MidiReceiver subclass whose job it is to monitor incomming messages 419 * and match them against the stream sent by the test. 420 */ 421 private class MidiMatchingReceiver extends MidiReceiver { 422 private static final String TAG = "MidiMatchingReceiver"; 423 424 @Override onSend(byte[] msg, int offset, int count, long timestamp)425 public void onSend(byte[] msg, int offset, int count, long timestamp) throws IOException { 426 if (DEBUG) { 427 Log.i(TAG, "---- onSend(offset:" + offset + " count:" + count); 428 logByteArray("bytes-received: ", msg, offset, count); 429 } 430 synchronized (mTestLock) { 431 if (!mTestRunning) { 432 return; 433 } 434 435 // Check for "Warm Up" message 436 if (mReceiveStreamPos == 0 && msg[offset] != makeMIDICmd(MIDICMD_CONTROL, 0)) { 437 // advance the match stream past the "warm-up" message 438 mReceiveStreamPos += mWarmUpMsg.length; 439 if (DEBUG) { 440 Log.i(TAG, "---- No Warm Up Message Detected."); 441 } 442 } 443 444 mTestMismatched = !matchStream(msg, offset, count); 445 446 if (mTestMismatched || mReceiveStreamPos == mMatchStream.size()) { 447 mTestRunning = false; 448 mRunningTestID = TESTID_NONE; 449 450 if (DEBUG) { 451 Log.i(TAG, "---- Test Complete"); 452 } 453 // defer closing the ports to outside of this callback. 454 new Thread(new Runnable() { 455 public void run() { 456 mTestStatus = mTestMismatched 457 ? TESTSTATUS_FAILED_MISMATCH : TESTSTATUS_PASSED; 458 closePorts(); 459 } 460 }).start(); 461 462 enableTestButtons(true); 463 updateTestStateUI(); 464 } 465 } 466 } 467 } /* class MidiMatchingReceiver */ 468 } /* class JavaMidiTestModule */ 469 470 /** 471 * Test Module for Bluetooth Loopback. 472 * This is a specialization of JavaMidiTestModule (which has the connections for the BL device 473 * itself) with and added MidiIODevice object for the USB audio device which does the 474 * "looping back". 475 */ 476 private class BTMidiTestModule extends JavaMidiTestModule { 477 private static final String TAG = "BTMidiTestModule"; 478 private MidiIODevice mUSBLoopbackDevice = new MidiIODevice(MidiDeviceInfo.TYPE_USB); 479 BTMidiTestModule()480 public BTMidiTestModule() { 481 super(MidiDeviceInfo.TYPE_BLUETOOTH ); 482 } 483 484 @Override scanDevices(MidiDeviceInfo[] devInfos)485 public void scanDevices(MidiDeviceInfo[] devInfos) { 486 // (normal) Scan for BT MIDI device 487 super.scanDevices(devInfos); 488 // Find a USB Loopback Device 489 mUSBLoopbackDevice.scanDevices(devInfos); 490 } 491 openUSBEchoDevice(MidiDevice device)492 private void openUSBEchoDevice(MidiDevice device) { 493 MidiDeviceInfo deviceInfo = device.getInfo(); 494 int numOutputs = deviceInfo.getOutputPortCount(); 495 if (numOutputs > 0) { 496 mUSBLoopbackDevice.mReceivePort = device.openOutputPort(0); 497 mUSBLoopbackDevice.mReceivePort.connect(new USBMidiEchoReceiver()); 498 } 499 500 int numInputs = deviceInfo.getInputPortCount(); 501 if (numInputs != 0) { 502 mUSBLoopbackDevice.mSendPort = device.openInputPort(0); 503 } 504 } 505 closePorts()506 protected void closePorts() { 507 super.closePorts(); 508 mUSBLoopbackDevice.closePorts(); 509 } 510 511 @Override startLoopbackTest(int testID)512 void startLoopbackTest(int testID) { 513 if (DEBUG) { 514 Log.i(TAG, "---- startLoopbackTest()"); 515 } 516 517 // Setup the USB Loopback Device 518 mUSBLoopbackDevice.closePorts(); 519 520 if (mIODevice.mSendDevInfo != null) { 521 mMidiManager.openDevice( 522 mUSBLoopbackDevice.mSendDevInfo, new USBLoopbackOpenListener(), null); 523 } 524 525 // Now start the test as usual 526 super.startLoopbackTest(testID); 527 } 528 529 /** 530 * We need this OnDeviceOpenedListener to open the USB-Loopback device 531 */ 532 private class USBLoopbackOpenListener implements MidiManager.OnDeviceOpenedListener { 533 @Override onDeviceOpened(MidiDevice device)534 public void onDeviceOpened(MidiDevice device) { 535 if (DEBUG) { 536 Log.i("USBLoopbackOpenListener", "---- onDeviceOpened()"); 537 } 538 mUSBLoopbackDevice.openPorts(device, new USBMidiEchoReceiver()); 539 } 540 } /* class USBLoopbackOpenListener */ 541 542 /** 543 * MidiReceiver subclass for BlueTooth Loopback Test 544 * 545 * This class receives bytes from the USB Interface (presumably coming from the 546 * Bluetooth MIDI peripheral) and echoes them back out (presumably to the Bluetooth 547 * MIDI peripheral). 548 */ 549 //TODO - This could be pulled out into a separate class and shared with the identical 550 // code in MidiNativeTestActivity if we pass in the USB Loopback Device object rather 551 // than accessing it from the enclosing BTMidiTestModule class. 552 private class USBMidiEchoReceiver extends MidiReceiver { 553 private int mTotalBytesEchoed; 554 555 @Override onSend(byte[] msg, int offset, int count, long timestamp)556 public void onSend(byte[] msg, int offset, int count, long timestamp) throws IOException { 557 mTotalBytesEchoed += count; 558 if (DEBUG) { 559 logByteArray("echo: ", msg, offset, count); 560 } 561 if (mUSBLoopbackDevice.mSendPort == null) { 562 Log.e(TAG, "(java) mUSBLoopbackDevice.mSendPort is null"); 563 } else { 564 mUSBLoopbackDevice.mSendPort.onSend(msg, offset, count, timestamp); 565 } 566 } 567 } /* class USBMidiEchoReceiver */ 568 } /* class BTMidiTestModule */ 569 } /* class MidiActivity */ 570