• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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