1 /* 2 * Copyright 2015 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 package com.google.sample.oboe.manualtest; 17 18 19 import android.media.midi.MidiDeviceService; 20 import android.media.midi.MidiReceiver; 21 import android.util.Log; 22 23 import com.mobileer.miditools.MidiConstants; 24 import com.mobileer.miditools.MidiFramer; 25 26 import java.io.IOException; 27 import java.util.ArrayList; 28 29 /** 30 * Measure the latency of various output paths by playing a blip. 31 * Report the results back to the TestListeners. 32 */ 33 public class AudioMidiTester extends MidiDeviceService { 34 35 // Sometimes the service can be run without the MainActivity being run! 36 static { 37 // Must match name in CMakeLists.txt 38 System.loadLibrary("oboetester"); 39 } 40 41 private static final float MAX_TOUCH_LATENCY = 0.200f; 42 private static final float MAX_OUTPUT_LATENCY = 0.600f; 43 private static final float ANALYSIS_TIME_MARGIN = 0.250f; 44 45 private static final float ANALYSIS_TIME_DELAY = MAX_OUTPUT_LATENCY; 46 private static final float ANALYSIS_TIME_TOTAL = MAX_TOUCH_LATENCY + MAX_OUTPUT_LATENCY; 47 private static final float ANALYSIS_TIME_MAX = ANALYSIS_TIME_TOTAL + ANALYSIS_TIME_MARGIN; 48 private static final int ANALYSIS_SAMPLE_RATE = 48000; // need not match output rate 49 50 private ArrayList<TestListener> mListeners = new ArrayList<TestListener>(); 51 private MyMidiReceiver mReceiver = new MyMidiReceiver(); 52 private MidiFramer mMidiFramer = new MidiFramer(mReceiver); 53 private boolean mRecordEnabled = true; 54 55 private static AudioMidiTester mInstance; 56 private AudioRecordThread mRecorder; 57 private TapLatencyAnalyser mTapLatencyAnalyser; 58 59 private AudioOutputTester mAudioOutputTester; 60 61 public static class TestResult { 62 public float[] samples; 63 public float[] filtered; 64 public int frameRate; 65 public TapLatencyAnalyser.TapLatencyEvent[] events; 66 } 67 68 public static interface TestListener { onTestFinished(TestResult result)69 public void onTestFinished(TestResult result); 70 onNoteOn(int pitch)71 public void onNoteOn(int pitch); 72 } 73 74 /** 75 * This is a Service so it is only created when a client requests the service. 76 */ AudioMidiTester()77 public AudioMidiTester() { 78 mInstance = this; 79 } 80 addTestListener(TestListener listener)81 public void addTestListener(TestListener listener) { 82 mListeners.add(listener); 83 } 84 removeTestListener(TestListener listener)85 public void removeTestListener(TestListener listener) { 86 mListeners.remove(listener); 87 } 88 89 @Override onCreate()90 public void onCreate() { 91 super.onCreate(); 92 if (mRecordEnabled) { 93 mRecorder = new AudioRecordThread(ANALYSIS_SAMPLE_RATE, 94 1, 95 (int) (ANALYSIS_TIME_MAX * ANALYSIS_SAMPLE_RATE)); 96 } 97 98 mAudioOutputTester = AudioOutputTester.getInstance(); 99 100 mTapLatencyAnalyser = new TapLatencyAnalyser(); 101 } 102 103 @Override onDestroy()104 public void onDestroy() { 105 // do stuff here 106 super.onDestroy(); 107 } 108 getInstance()109 public static AudioMidiTester getInstance() { 110 return mInstance; 111 } 112 113 class MyMidiReceiver extends MidiReceiver { onSend(byte[] data, int offset, int count, long timestamp)114 public void onSend(byte[] data, int offset, 115 int count, long timestamp) throws IOException { 116 // parse MIDI 117 byte command = (byte) (data[0] & 0x0F0); 118 if (command == MidiConstants.STATUS_NOTE_ON) { 119 if (data[2] == 0) { 120 noteOff(data[1]); 121 } else { 122 noteOn(data[1]); 123 } 124 } else if (command == MidiConstants.STATUS_NOTE_OFF) { 125 noteOff(data[1]); 126 } 127 Log.i(TapToToneActivity.TAG, "MIDI command = " + command); 128 } 129 } 130 noteOn(byte b)131 private void noteOn(byte b) { 132 trigger(); 133 fireNoteOn(b); 134 } 135 fireNoteOn(byte pitch)136 private void fireNoteOn(byte pitch) { 137 for (TestListener listener : mListeners) { 138 listener.onNoteOn(pitch); 139 } 140 } 141 noteOff(byte b)142 private void noteOff(byte b) {} 143 144 @Override onGetInputPortReceivers()145 public MidiReceiver[] onGetInputPortReceivers() { 146 return new MidiReceiver[]{mMidiFramer}; 147 } 148 149 start()150 public void start() throws IOException { 151 if (mRecordEnabled) { 152 mRecorder.startAudio(); 153 } 154 } 155 trigger()156 public void trigger() { 157 mAudioOutputTester.trigger(); 158 if (mRecordEnabled) { 159 // schedule an analysis to start in the near future 160 int numSamples = (int) (mRecorder.getSampleRate() * ANALYSIS_TIME_DELAY); 161 Runnable task = new Runnable() { 162 public void run() { 163 new Thread() { 164 public void run() { 165 analyzeCapturedAudio(); 166 } 167 }.start(); 168 } 169 }; 170 171 mRecorder.scheduleTask(numSamples, task); 172 } 173 } 174 analyzeCapturedAudio()175 private void analyzeCapturedAudio() { 176 if (!mRecordEnabled) return; 177 int numSamples = (int) (mRecorder.getSampleRate() * ANALYSIS_TIME_TOTAL); 178 float[] buffer = new float[numSamples]; 179 mRecorder.setCaptureEnabled(false); // TODO wait for it to settle 180 int numRead = mRecorder.readMostRecent(buffer); 181 182 TestResult result = new TestResult(); 183 result.samples = buffer; 184 result.frameRate = mRecorder.getSampleRate(); 185 result.events = mTapLatencyAnalyser.analyze(buffer, 0, numRead); 186 result.filtered = mTapLatencyAnalyser.getFilteredBuffer(); 187 mRecorder.setCaptureEnabled(true); 188 // notify listeners 189 for (TestListener listener : mListeners) { 190 listener.onTestFinished(result); 191 } 192 } 193 194 stop()195 public void stop() { 196 if (mRecordEnabled) { 197 mRecorder.stopAudio(); 198 } 199 } 200 201 } 202