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