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