/* * Copyright (C) 2016 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.chromium.latency.walt; import android.annotation.TargetApi; import android.content.Context; import android.media.midi.MidiDevice; import android.media.midi.MidiDeviceInfo; import android.media.midi.MidiInputPort; import android.media.midi.MidiManager; import android.media.midi.MidiOutputPort; import android.media.midi.MidiReceiver; import android.os.Handler; import java.io.IOException; import java.util.ArrayList; import java.util.Locale; import static org.chromium.latency.walt.Utils.getIntPreference; @TargetApi(23) class MidiTest extends BaseTest { private Handler handler = new Handler(); private static final String TEENSY_MIDI_NAME = "Teensyduino Teensy MIDI"; private static final byte[] noteMsg = {(byte) 0x90, (byte) 99, (byte) 0}; private MidiManager midiManager; private MidiDevice midiDevice; // Output and Input here are with respect to the MIDI device, not the Android device. private MidiOutputPort midiOutputPort; private MidiInputPort midiInputPort; private boolean isConnecting = false; private long last_tWalt = 0; private long last_tSys = 0; private long last_tJava = 0; private int inputSyncAfterRepetitions = 100; private int outputSyncAfterRepetitions = 20; // TODO: implement periodic clock sync for output private int inputRepetitions; private int outputRepetitions; private int repetitionsDone; private ArrayList<Double> deltasToSys = new ArrayList<>(); ArrayList<Double> deltasInputTotal = new ArrayList<>(); ArrayList<Double> deltasOutputTotal = new ArrayList<>(); private static final int noteDelay = 300; private static final int timeout = 1000; MidiTest(Context context) { super(context); inputRepetitions = getIntPreference(context, R.string.preference_midi_in_reps, 100); outputRepetitions = getIntPreference(context, R.string.preference_midi_out_reps, 10); midiManager = (MidiManager) context.getSystemService(Context.MIDI_SERVICE); findMidiDevice(); } MidiTest(Context context, AutoRunFragment.ResultHandler resultHandler) { this(context); this.resultHandler = resultHandler; } void setInputRepetitions(int repetitions) { inputRepetitions = repetitions; } void setOutputRepetitions(int repetitions) { outputRepetitions = repetitions; } void testMidiOut() { if (midiDevice == null) { if (isConnecting) { logger.log("Still connecting..."); handler.post(new Runnable() { @Override public void run() { testMidiOut(); } }); } else { logger.log("MIDI device is not open!"); if (testStateListener != null) testStateListener.onTestStoppedWithError(); } return; } try { setupMidiOut(); } catch (IOException e) { logger.log("Error setting up test: " + e.getMessage()); if (testStateListener != null) testStateListener.onTestStoppedWithError(); return; } handler.postDelayed(cancelMidiOutRunnable, noteDelay * inputRepetitions + timeout); } void testMidiIn() { if (midiDevice == null) { if (isConnecting) { logger.log("Still connecting..."); handler.post(new Runnable() { @Override public void run() { testMidiIn(); } }); } else { logger.log("MIDI device is not open!"); if (testStateListener != null) testStateListener.onTestStoppedWithError(); } return; } try { setupMidiIn(); } catch (IOException e) { logger.log("Error setting up test: " + e.getMessage()); if (testStateListener != null) testStateListener.onTestStoppedWithError(); return; } handler.postDelayed(requestNoteRunnable, noteDelay); } private void setupMidiOut() throws IOException { repetitionsDone = 0; deltasInputTotal.clear(); deltasOutputTotal.clear(); midiInputPort = midiDevice.openInputPort(0); waltDevice.syncClock(); waltDevice.command(WaltDevice.CMD_MIDI); waltDevice.startListener(); waltDevice.setTriggerHandler(triggerHandler); scheduleNotes(); } private void findMidiDevice() { MidiDeviceInfo[] infos = midiManager.getDevices(); for(MidiDeviceInfo info : infos) { String name = info.getProperties().getString(MidiDeviceInfo.PROPERTY_NAME); logger.log("Found MIDI device named " + name); if(TEENSY_MIDI_NAME.equals(name)) { logger.log("^^^ using this device ^^^"); isConnecting = true; midiManager.openDevice(info, new MidiManager.OnDeviceOpenedListener() { @Override public void onDeviceOpened(MidiDevice device) { if (device == null) { logger.log("Error, unable to open MIDI device"); } else { logger.log("Opened MIDI device successfully!"); midiDevice = device; } isConnecting = false; } }, null); break; } } } private WaltDevice.TriggerHandler triggerHandler = new WaltDevice.TriggerHandler() { @Override public void onReceive(WaltDevice.TriggerMessage tmsg) { last_tWalt = tmsg.t + waltDevice.clock.baseTime; double dt = (last_tWalt - last_tSys) / 1000.; deltasOutputTotal.add(dt); logger.log(String.format(Locale.US, "Note detected: latency of %.3f ms", dt)); if (testStateListener != null) testStateListener.onTestPartialResult(dt); if (traceLogger != null) { traceLogger.log(last_tSys, last_tWalt, "MIDI Output", "Bar starts when system sends audio and ends when WALT receives note"); } last_tSys += noteDelay * 1000; repetitionsDone++; if (repetitionsDone < outputRepetitions) { try { waltDevice.command(WaltDevice.CMD_MIDI); } catch (IOException e) { logger.log("Failed to send command CMD_MIDI: " + e.getMessage()); } } else { finishMidiOut(); } } }; private void scheduleNotes() { if(midiInputPort == null) { logger.log("midiInputPort is not open"); return; } long t = System.nanoTime() + ((long) noteDelay) * 1000000L; try { // TODO: only schedule some, then sync clock for (int i = 0; i < outputRepetitions; i++) { midiInputPort.send(noteMsg, 0, noteMsg.length, t + ((long) noteDelay) * 1000000L * i); } } catch(IOException e) { logger.log("Unable to schedule note: " + e.getMessage()); return; } last_tSys = t / 1000; } private void finishMidiOut() { logger.log("All notes detected"); logger.log(String.format( Locale.US, "Median total output latency %.1f ms", Utils.median(deltasOutputTotal))); handler.removeCallbacks(cancelMidiOutRunnable); if (resultHandler != null) { resultHandler.onResult(deltasOutputTotal); } if (testStateListener != null) testStateListener.onTestStopped(); if (traceLogger != null) traceLogger.flush(context); teardownMidiOut(); } private Runnable cancelMidiOutRunnable = new Runnable() { @Override public void run() { logger.log("Timed out waiting for notes to be detected by WALT"); if (testStateListener != null) testStateListener.onTestStoppedWithError(); teardownMidiOut(); } }; private void teardownMidiOut() { try { midiInputPort.close(); } catch(IOException e) { logger.log("Error, failed to close input port: " + e.getMessage()); } waltDevice.stopListener(); waltDevice.clearTriggerHandler(); waltDevice.checkDrift(); } private Runnable requestNoteRunnable = new Runnable() { @Override public void run() { logger.log("Requesting note from WALT..."); String s; try { s = waltDevice.command(WaltDevice.CMD_NOTE); } catch (IOException e) { logger.log("Error sending NOTE command: " + e.getMessage()); if (testStateListener != null) testStateListener.onTestStoppedWithError(); return; } last_tWalt = Integer.parseInt(s); handler.postDelayed(finishMidiInRunnable, timeout); } }; private Runnable finishMidiInRunnable = new Runnable() { @Override public void run() { waltDevice.checkDrift(); logger.log("deltas: " + deltasToSys.toString()); logger.log("MIDI Input Test Results:"); logger.log(String.format(Locale.US, "Median MIDI subsystem latency %.1f ms\nMedian total latency %.1f ms", Utils.median(deltasToSys), Utils.median(deltasInputTotal) )); if (resultHandler != null) { resultHandler.onResult(deltasToSys, deltasInputTotal); } if (testStateListener != null) testStateListener.onTestStopped(); if (traceLogger != null) traceLogger.flush(context); teardownMidiIn(); } }; private class WaltReceiver extends MidiReceiver { public void onSend(byte[] data, int offset, int count, long timestamp) throws IOException { if(count > 0 && data[offset] == (byte) 0x90) { // NoteOn message on channel 1 handler.removeCallbacks(finishMidiInRunnable); last_tJava = waltDevice.clock.micros(); last_tSys = timestamp / 1000 - waltDevice.clock.baseTime; final double d1 = (last_tSys - last_tWalt) / 1000.; final double d2 = (last_tJava - last_tSys) / 1000.; final double dt = (last_tJava - last_tWalt) / 1000.; logger.log(String.format(Locale.US, "Result: Time to MIDI subsystem = %.3f ms, Time to Java = %.3f ms, " + "Total = %.3f ms", d1, d2, dt)); deltasToSys.add(d1); deltasInputTotal.add(dt); if (testStateListener != null) { handler.post(new Runnable() { @Override public void run() { testStateListener.onTestPartialResult(dt); } }); } if (traceLogger != null) { traceLogger.log(last_tWalt + waltDevice.clock.baseTime, last_tSys + waltDevice.clock.baseTime, "MIDI Input Subsystem", "Bar starts when WALT sends note and ends when received by MIDI subsystem"); traceLogger.log(last_tSys + waltDevice.clock.baseTime, last_tJava + waltDevice.clock.baseTime, "MIDI Input Java", "Bar starts when note received by MIDI subsystem and ends when received by app"); } repetitionsDone++; if (repetitionsDone % inputSyncAfterRepetitions == 0) { try { waltDevice.syncClock(); } catch (IOException e) { logger.log("Error syncing clocks: " + e.getMessage()); handler.post(finishMidiInRunnable); return; } } if (repetitionsDone < inputRepetitions) { handler.post(requestNoteRunnable); } else { handler.post(finishMidiInRunnable); } } else { logger.log(String.format(Locale.US, "Expected 0x90, got 0x%x and count was %d", data[offset], count)); } } } private void setupMidiIn() throws IOException { repetitionsDone = 0; deltasInputTotal.clear(); deltasOutputTotal.clear(); midiOutputPort = midiDevice.openOutputPort(0); midiOutputPort.connect(new WaltReceiver()); waltDevice.syncClock(); } private void teardownMidiIn() { handler.removeCallbacks(requestNoteRunnable); handler.removeCallbacks(finishMidiInRunnable); try { midiOutputPort.close(); } catch (IOException e) { logger.log("Error, failed to close output port: " + e.getMessage()); } } }