Home | History | Annotate | Download | only in walt
      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 
     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 
     72     MidiTest(Context context, AutoRunFragment.ResultHandler resultHandler) {
     73         this(context);
     74         this.resultHandler = resultHandler;
     75     }
     76 
     77     void setInputRepetitions(int repetitions) {
     78         inputRepetitions = repetitions;
     79     }
     80 
     81     void setOutputRepetitions(int repetitions) {
     82         outputRepetitions = repetitions;
     83     }
     84 
     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 
    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 
    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 
    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 
    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 
    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 
    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 {
    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 
    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 
    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