1 /* 2 * Copyright (C) 2016 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 org.chromium.latency.walt; 18 19 import android.annotation.TargetApi; 20 import android.content.Context; 21 import android.media.midi.MidiDevice; 22 import android.media.midi.MidiDeviceInfo; 23 import android.media.midi.MidiInputPort; 24 import android.media.midi.MidiManager; 25 import android.media.midi.MidiOutputPort; 26 import android.media.midi.MidiReceiver; 27 import android.os.Handler; 28 29 import java.io.IOException; 30 import java.util.ArrayList; 31 import java.util.Locale; 32 33 import static org.chromium.latency.walt.Utils.getIntPreference; 34 35 @TargetApi(23) 36 class MidiTest extends BaseTest { 37 38 private Handler handler = new Handler(); 39 40 private static final String TEENSY_MIDI_NAME = "Teensyduino Teensy MIDI"; 41 private static final byte[] noteMsg = {(byte) 0x90, (byte) 99, (byte) 0}; 42 43 private MidiManager midiManager; 44 private MidiDevice midiDevice; 45 // Output and Input here are with respect to the MIDI device, not the Android device. 46 private MidiOutputPort midiOutputPort; 47 private MidiInputPort midiInputPort; 48 private boolean isConnecting = false; 49 private long last_tWalt = 0; 50 private long last_tSys = 0; 51 private long last_tJava = 0; 52 private int inputSyncAfterRepetitions = 100; 53 private int outputSyncAfterRepetitions = 20; // TODO: implement periodic clock sync for output 54 private int inputRepetitions; 55 private int outputRepetitions; 56 private int repetitionsDone; 57 private ArrayList<Double> deltasToSys = new ArrayList<>(); 58 ArrayList<Double> deltasInputTotal = new ArrayList<>(); 59 ArrayList<Double> deltasOutputTotal = new ArrayList<>(); 60 61 private static final int noteDelay = 300; 62 private static final int timeout = 1000; 63 MidiTest(Context context)64 MidiTest(Context context) { 65 super(context); 66 inputRepetitions = getIntPreference(context, R.string.preference_midi_in_reps, 100); 67 outputRepetitions = getIntPreference(context, R.string.preference_midi_out_reps, 10); 68 midiManager = (MidiManager) context.getSystemService(Context.MIDI_SERVICE); 69 findMidiDevice(); 70 } 71 MidiTest(Context context, AutoRunFragment.ResultHandler resultHandler)72 MidiTest(Context context, AutoRunFragment.ResultHandler resultHandler) { 73 this(context); 74 this.resultHandler = resultHandler; 75 } 76 setInputRepetitions(int repetitions)77 void setInputRepetitions(int repetitions) { 78 inputRepetitions = repetitions; 79 } 80 setOutputRepetitions(int repetitions)81 void setOutputRepetitions(int repetitions) { 82 outputRepetitions = repetitions; 83 } 84 testMidiOut()85 void testMidiOut() { 86 if (midiDevice == null) { 87 if (isConnecting) { 88 logger.log("Still connecting..."); 89 handler.post(new Runnable() { 90 @Override 91 public void run() { 92 testMidiOut(); 93 } 94 }); 95 } else { 96 logger.log("MIDI device is not open!"); 97 if (testStateListener != null) testStateListener.onTestStoppedWithError(); 98 } 99 return; 100 } 101 try { 102 setupMidiOut(); 103 } catch (IOException e) { 104 logger.log("Error setting up test: " + e.getMessage()); 105 if (testStateListener != null) testStateListener.onTestStoppedWithError(); 106 return; 107 } 108 handler.postDelayed(cancelMidiOutRunnable, noteDelay * inputRepetitions + timeout); 109 } 110 testMidiIn()111 void testMidiIn() { 112 if (midiDevice == null) { 113 if (isConnecting) { 114 logger.log("Still connecting..."); 115 handler.post(new Runnable() { 116 @Override 117 public void run() { 118 testMidiIn(); 119 } 120 }); 121 } else { 122 logger.log("MIDI device is not open!"); 123 if (testStateListener != null) testStateListener.onTestStoppedWithError(); 124 } 125 return; 126 } 127 try { 128 setupMidiIn(); 129 } catch (IOException e) { 130 logger.log("Error setting up test: " + e.getMessage()); 131 if (testStateListener != null) testStateListener.onTestStoppedWithError(); 132 return; 133 } 134 handler.postDelayed(requestNoteRunnable, noteDelay); 135 } 136 setupMidiOut()137 private void setupMidiOut() throws IOException { 138 repetitionsDone = 0; 139 deltasInputTotal.clear(); 140 deltasOutputTotal.clear(); 141 142 midiInputPort = midiDevice.openInputPort(0); 143 144 waltDevice.syncClock(); 145 waltDevice.command(WaltDevice.CMD_MIDI); 146 waltDevice.startListener(); 147 waltDevice.setTriggerHandler(triggerHandler); 148 149 scheduleNotes(); 150 } 151 findMidiDevice()152 private void findMidiDevice() { 153 MidiDeviceInfo[] infos = midiManager.getDevices(); 154 for(MidiDeviceInfo info : infos) { 155 String name = info.getProperties().getString(MidiDeviceInfo.PROPERTY_NAME); 156 logger.log("Found MIDI device named " + name); 157 if(TEENSY_MIDI_NAME.equals(name)) { 158 logger.log("^^^ using this device ^^^"); 159 isConnecting = true; 160 midiManager.openDevice(info, new MidiManager.OnDeviceOpenedListener() { 161 @Override 162 public void onDeviceOpened(MidiDevice device) { 163 if (device == null) { 164 logger.log("Error, unable to open MIDI device"); 165 } else { 166 logger.log("Opened MIDI device successfully!"); 167 midiDevice = device; 168 } 169 isConnecting = false; 170 } 171 }, null); 172 break; 173 } 174 } 175 } 176 177 private WaltDevice.TriggerHandler triggerHandler = new WaltDevice.TriggerHandler() { 178 @Override 179 public void onReceive(WaltDevice.TriggerMessage tmsg) { 180 last_tWalt = tmsg.t + waltDevice.clock.baseTime; 181 double dt = (last_tWalt - last_tSys) / 1000.; 182 183 deltasOutputTotal.add(dt); 184 logger.log(String.format(Locale.US, "Note detected: latency of %.3f ms", dt)); 185 if (testStateListener != null) testStateListener.onTestPartialResult(dt); 186 if (traceLogger != null) { 187 traceLogger.log(last_tSys, last_tWalt, "MIDI Output", 188 "Bar starts when system sends audio and ends when WALT receives note"); 189 } 190 191 last_tSys += noteDelay * 1000; 192 repetitionsDone++; 193 194 if (repetitionsDone < outputRepetitions) { 195 try { 196 waltDevice.command(WaltDevice.CMD_MIDI); 197 } catch (IOException e) { 198 logger.log("Failed to send command CMD_MIDI: " + e.getMessage()); 199 } 200 } else { 201 finishMidiOut(); 202 } 203 } 204 }; 205 scheduleNotes()206 private void scheduleNotes() { 207 if(midiInputPort == null) { 208 logger.log("midiInputPort is not open"); 209 return; 210 } 211 long t = System.nanoTime() + ((long) noteDelay) * 1000000L; 212 try { 213 // TODO: only schedule some, then sync clock 214 for (int i = 0; i < outputRepetitions; i++) { 215 midiInputPort.send(noteMsg, 0, noteMsg.length, t + ((long) noteDelay) * 1000000L * i); 216 } 217 } catch(IOException e) { 218 logger.log("Unable to schedule note: " + e.getMessage()); 219 return; 220 } 221 last_tSys = t / 1000; 222 } 223 finishMidiOut()224 private void finishMidiOut() { 225 logger.log("All notes detected"); 226 logger.log(String.format( 227 Locale.US, "Median total output latency %.1f ms", Utils.median(deltasOutputTotal))); 228 229 handler.removeCallbacks(cancelMidiOutRunnable); 230 231 if (resultHandler != null) { 232 resultHandler.onResult(deltasOutputTotal); 233 } 234 if (testStateListener != null) testStateListener.onTestStopped(); 235 if (traceLogger != null) traceLogger.flush(context); 236 teardownMidiOut(); 237 } 238 239 private Runnable cancelMidiOutRunnable = new Runnable() { 240 @Override 241 public void run() { 242 logger.log("Timed out waiting for notes to be detected by WALT"); 243 if (testStateListener != null) testStateListener.onTestStoppedWithError(); 244 teardownMidiOut(); 245 } 246 }; 247 teardownMidiOut()248 private void teardownMidiOut() { 249 try { 250 midiInputPort.close(); 251 } catch(IOException e) { 252 logger.log("Error, failed to close input port: " + e.getMessage()); 253 } 254 255 waltDevice.stopListener(); 256 waltDevice.clearTriggerHandler(); 257 waltDevice.checkDrift(); 258 } 259 260 private Runnable requestNoteRunnable = new Runnable() { 261 @Override 262 public void run() { 263 logger.log("Requesting note from WALT..."); 264 String s; 265 try { 266 s = waltDevice.command(WaltDevice.CMD_NOTE); 267 } catch (IOException e) { 268 logger.log("Error sending NOTE command: " + e.getMessage()); 269 if (testStateListener != null) testStateListener.onTestStoppedWithError(); 270 return; 271 } 272 last_tWalt = Integer.parseInt(s); 273 handler.postDelayed(finishMidiInRunnable, timeout); 274 } 275 }; 276 277 private Runnable finishMidiInRunnable = new Runnable() { 278 @Override 279 public void run() { 280 waltDevice.checkDrift(); 281 282 logger.log("deltas: " + deltasToSys.toString()); 283 logger.log("MIDI Input Test Results:"); 284 logger.log(String.format(Locale.US, 285 "Median MIDI subsystem latency %.1f ms\nMedian total latency %.1f ms", 286 Utils.median(deltasToSys), Utils.median(deltasInputTotal) 287 )); 288 289 if (resultHandler != null) { 290 resultHandler.onResult(deltasToSys, deltasInputTotal); 291 } 292 if (testStateListener != null) testStateListener.onTestStopped(); 293 if (traceLogger != null) traceLogger.flush(context); 294 teardownMidiIn(); 295 } 296 }; 297 298 private class WaltReceiver extends MidiReceiver { onSend(byte[] data, int offset, int count, long timestamp)299 public void onSend(byte[] data, int offset, 300 int count, long timestamp) throws IOException { 301 if(count > 0 && data[offset] == (byte) 0x90) { // NoteOn message on channel 1 302 handler.removeCallbacks(finishMidiInRunnable); 303 last_tJava = waltDevice.clock.micros(); 304 last_tSys = timestamp / 1000 - waltDevice.clock.baseTime; 305 306 final double d1 = (last_tSys - last_tWalt) / 1000.; 307 final double d2 = (last_tJava - last_tSys) / 1000.; 308 final double dt = (last_tJava - last_tWalt) / 1000.; 309 logger.log(String.format(Locale.US, 310 "Result: Time to MIDI subsystem = %.3f ms, Time to Java = %.3f ms, " + 311 "Total = %.3f ms", 312 d1, d2, dt)); 313 deltasToSys.add(d1); 314 deltasInputTotal.add(dt); 315 if (testStateListener != null) { 316 handler.post(new Runnable() { 317 @Override 318 public void run() { 319 testStateListener.onTestPartialResult(dt); 320 } 321 }); 322 } 323 if (traceLogger != null) { 324 traceLogger.log(last_tWalt + waltDevice.clock.baseTime, 325 last_tSys + waltDevice.clock.baseTime, "MIDI Input Subsystem", 326 "Bar starts when WALT sends note and ends when received by MIDI subsystem"); 327 traceLogger.log(last_tSys + waltDevice.clock.baseTime, 328 last_tJava + waltDevice.clock.baseTime, "MIDI Input Java", 329 "Bar starts when note received by MIDI subsystem and ends when received by app"); 330 } 331 332 repetitionsDone++; 333 if (repetitionsDone % inputSyncAfterRepetitions == 0) { 334 try { 335 waltDevice.syncClock(); 336 } catch (IOException e) { 337 logger.log("Error syncing clocks: " + e.getMessage()); 338 handler.post(finishMidiInRunnable); 339 return; 340 } 341 } 342 if (repetitionsDone < inputRepetitions) { 343 handler.post(requestNoteRunnable); 344 } else { 345 handler.post(finishMidiInRunnable); 346 } 347 } else { 348 logger.log(String.format(Locale.US, "Expected 0x90, got 0x%x and count was %d", 349 data[offset], count)); 350 } 351 } 352 } 353 setupMidiIn()354 private void setupMidiIn() throws IOException { 355 repetitionsDone = 0; 356 deltasInputTotal.clear(); 357 deltasOutputTotal.clear(); 358 midiOutputPort = midiDevice.openOutputPort(0); 359 midiOutputPort.connect(new WaltReceiver()); 360 waltDevice.syncClock(); 361 } 362 teardownMidiIn()363 private void teardownMidiIn() { 364 handler.removeCallbacks(requestNoteRunnable); 365 handler.removeCallbacks(finishMidiInRunnable); 366 try { 367 midiOutputPort.close(); 368 } catch (IOException e) { 369 logger.log("Error, failed to close output port: " + e.getMessage()); 370 } 371 } 372 } 373