1 package com.mobileer.oboetester; 2 3 import android.app.Activity; 4 import android.widget.TextView; 5 6 import java.io.IOException; 7 8 /** 9 * Measure tap-to-tone latency by and update the waveform display. 10 */ 11 public class TapToToneTester { 12 13 private static final float MAX_TOUCH_LATENCY = 0.200f; 14 private static final float MAX_OUTPUT_LATENCY = 1.200f; 15 private static final float ANALYSIS_TIME_MARGIN = 0.500f; 16 17 private static final float ANALYSIS_TIME_DELAY = MAX_OUTPUT_LATENCY; 18 private static final float ANALYSIS_TIME_TOTAL = MAX_TOUCH_LATENCY + MAX_OUTPUT_LATENCY; 19 private static final int ANALYSIS_SAMPLE_RATE = 48000; // need not match output rate 20 21 private final boolean mRecordEnabled = true; 22 private final AudioRecordThread mRecorder; 23 private final TapLatencyAnalyser mTapLatencyAnalyser; 24 25 private final Activity mActivity; 26 private final WaveformView mWaveformView; 27 private final TextView mResultView; 28 29 private final String mTapInstructions; 30 private float mAnalysisTimeMargin = ANALYSIS_TIME_MARGIN; 31 32 private boolean mArmed = true; 33 34 // Stats for latency 35 private int mMeasurementCount; 36 private int mLatencySumSamples; 37 private int mLatencyMin; 38 private int mLatencyMax; 39 40 public static class TestResult { 41 public float[] samples; 42 public float[] filtered; 43 public int frameRate; 44 public TapLatencyAnalyser.TapLatencyEvent[] events; 45 } 46 TapToToneTester(Activity activity, String tapInstructions)47 public TapToToneTester(Activity activity, String tapInstructions) { 48 mActivity = activity; 49 mTapInstructions = tapInstructions; 50 mResultView = (TextView) activity.findViewById(R.id.resultView); 51 mWaveformView = (WaveformView) activity.findViewById(R.id.waveview_audio); 52 mWaveformView.setEnabled(false); 53 54 if (mRecordEnabled) { 55 float analysisTimeMax = ANALYSIS_TIME_TOTAL + mAnalysisTimeMargin; 56 mRecorder = new AudioRecordThread(ANALYSIS_SAMPLE_RATE, 57 1, 58 (int) (analysisTimeMax * ANALYSIS_SAMPLE_RATE)); 59 } 60 mTapLatencyAnalyser = new TapLatencyAnalyser(); 61 } 62 start()63 public void start() throws IOException { 64 if (mRecordEnabled) { 65 mRecorder.startAudio(); 66 mWaveformView.setEnabled(true); 67 } 68 } 69 stop()70 public void stop() { 71 if (mRecordEnabled) { 72 mRecorder.stopAudio(); 73 mWaveformView.setEnabled(false); 74 } 75 } 76 77 /** 78 * @return true if ready to process a tap, false if already triggered 79 */ isArmed()80 public boolean isArmed() { 81 return mArmed; 82 } 83 setArmed(boolean armed)84 public void setArmed(boolean armed) { 85 this.mArmed = armed; 86 } 87 analyzeLater(String message)88 public void analyzeLater(String message) { 89 showPendingStatus(message); 90 Runnable task = this::analyseAndShowResults; 91 scheduleTaskWhenDone(task); 92 mArmed = false; 93 } 94 showPendingStatus(final String message)95 private void showPendingStatus(final String message) { 96 mWaveformView.post(() -> { 97 mWaveformView.setMessage(message); 98 mWaveformView.clearSampleData(); 99 mWaveformView.invalidate(); 100 }); 101 } 102 scheduleTaskWhenDone(Runnable task)103 private void scheduleTaskWhenDone(Runnable task) { 104 if (mRecordEnabled) { 105 // schedule an analysis to start in the near future 106 int numSamples = (int) (mRecorder.getSampleRate() * ANALYSIS_TIME_DELAY); 107 mRecorder.scheduleTask(numSamples, task); 108 } 109 } 110 analyseAndShowResults()111 private void analyseAndShowResults() { 112 TestResult result = analyzeCapturedAudio(); 113 if (result != null) { 114 showTestResults(result); 115 } 116 } 117 analyzeCapturedAudio()118 public TestResult analyzeCapturedAudio() { 119 if (!mRecordEnabled) return null; 120 int numSamples = (int) (mRecorder.getSampleRate() * ANALYSIS_TIME_TOTAL); 121 float[] buffer = new float[numSamples]; 122 mRecorder.setCaptureEnabled(false); // TODO wait for it to settle 123 int numRead = mRecorder.readMostRecent(buffer); 124 125 TestResult result = new TestResult(); 126 result.samples = buffer; 127 result.frameRate = mRecorder.getSampleRate(); 128 result.events = mTapLatencyAnalyser.analyze(buffer, 0, numRead); 129 result.filtered = mTapLatencyAnalyser.getFilteredBuffer(); 130 mRecorder.setCaptureEnabled(true); 131 return result; 132 } 133 resetLatency()134 public void resetLatency() { 135 mMeasurementCount = 0; 136 mLatencySumSamples = 0; 137 mLatencyMin = Integer.MAX_VALUE; 138 mLatencyMax = 0; 139 showTestResults(null); 140 } 141 142 // Runs on UI thread. showTestResults(TestResult result)143 public void showTestResults(TestResult result) { 144 String text; 145 mWaveformView.setMessage(null); 146 if (result == null) { 147 text = mTapInstructions; 148 mWaveformView.clearSampleData(); 149 } else { 150 // Show edges detected. 151 if (result.events.length == 0) { 152 mWaveformView.setCursorData(null); 153 } else { 154 int numEdges = Math.min(8, result.events.length); 155 int[] cursors = new int[numEdges]; 156 for (int i = 0; i < numEdges; i++) { 157 cursors[i] = result.events[i].sampleIndex; 158 } 159 mWaveformView.setCursorData(cursors); 160 } 161 // Did we get a good measurement? 162 if (result.events.length < 2) { 163 text = "Not enough edges. Use fingernail.\n"; 164 } else if (result.events.length > 2) { 165 text = "Too many edges.\n"; 166 } else { 167 int latencySamples = result.events[1].sampleIndex - result.events[0].sampleIndex; 168 mLatencySumSamples += latencySamples; 169 mMeasurementCount++; 170 171 int latencyMillis = 1000 * latencySamples / result.frameRate; 172 if (mLatencyMin > latencyMillis) { 173 mLatencyMin = latencyMillis; 174 } 175 if (mLatencyMax < latencyMillis) { 176 mLatencyMax = latencyMillis; 177 } 178 179 text = String.format("tap-to-tone latency = %3d msec\n", latencyMillis); 180 } 181 mWaveformView.setSampleData(result.filtered); 182 } 183 184 if (mMeasurementCount > 0) { 185 int averageLatencySamples = mLatencySumSamples / mMeasurementCount; 186 int averageLatencyMillis = 1000 * averageLatencySamples / result.frameRate; 187 final String plural = (mMeasurementCount == 1) ? "test" : "tests"; 188 text = text + String.format("min = %3d, avg = %3d, max = %3d, %d %s", 189 mLatencyMin, averageLatencyMillis, mLatencyMax, mMeasurementCount, plural); 190 } 191 final String postText = text; 192 mWaveformView.post(new Runnable() { 193 public void run() { 194 mResultView.setText(postText); 195 mWaveformView.postInvalidate(); 196 } 197 }); 198 199 mArmed = true; 200 } 201 } 202