• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright 2018 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 com.mobileer.oboetester;
18 
19 import static com.mobileer.oboetester.IntentBasedTestSupport.configureStreamsFromBundle;
20 
21 import android.os.Bundle;
22 import android.os.Handler;
23 import android.os.Looper;
24 import android.util.Log;
25 import android.view.View;
26 import android.widget.Button;
27 import android.widget.TextView;
28 import androidx.annotation.NonNull;
29 
30 import java.io.File;
31 import java.io.IOException;
32 import java.util.Locale;
33 
34 /**
35  * Activity to measure latency on a full duplex stream.
36  */
37 public class RoundTripLatencyActivity extends AnalyzerActivity {
38 
39     // STATEs defined in LatencyAnalyzer.h
40     private static final int STATE_MEASURE_BACKGROUND = 0;
41     private static final int STATE_IN_PULSE = 1;
42     private static final int STATE_GOT_DATA = 2;
43     private final static String LATENCY_FORMAT = "%4.2f";
44     // When I use 5.3g I only get one digit after the decimal point!
45     private final static String CONFIDENCE_FORMAT = "%5.3f";
46 
47     private TextView mAnalyzerView;
48     private Button   mMeasureButton;
49     private Button   mAverageButton;
50     private Button   mCancelButton;
51     private Button   mShareButton;
52     private boolean  mHasRecording = false;
53 
54     private int     mBufferBursts = -1;
55     private Handler mHandler = new Handler(Looper.getMainLooper()); // UI thread
56 
57     DoubleStatistics mTimestampLatencyStats = new DoubleStatistics(); // for single measurement
58 
59     // Run the test several times and report the average latency.
60     protected class AverageLatencyTestRunner {
61         private final static int AVERAGE_TEST_DELAY_MSEC = 1000; // arbitrary
62         private static final int GOOD_RUNS_REQUIRED = 5; // arbitrary
63         private static final int MAX_BAD_RUNS_ALLOWED = 5; // arbitrary
64         private int mBadCount = 0; // number of bad measurements
65 
66         DoubleStatistics mLatencies = new DoubleStatistics();
67         DoubleStatistics mConfidences = new DoubleStatistics();
68         DoubleStatistics mTimestampLatencies = new DoubleStatistics(); // for multiple measurements
69         private boolean mActive;
70         private String  mLastReport = "";
71 
getGoodCount()72         private int getGoodCount() {
73             return mLatencies.count();
74         }
75 
76         // Called on UI thread.
onAnalyserDone()77         String onAnalyserDone() {
78             String message;
79             boolean reschedule = false;
80             if (!mActive) {
81                 message = "";
82             } else if (getMeasuredResult() != 0) {
83                 mBadCount++;
84                 if (mBadCount > MAX_BAD_RUNS_ALLOWED) {
85                     cancel();
86                     updateButtons(false);
87                     message = "averaging cancelled due to error\n";
88                 } else {
89                     message = "skipping this bad run, "
90                             + mBadCount + " of " + MAX_BAD_RUNS_ALLOWED + " max\n";
91                     reschedule = true;
92                 }
93             } else {
94                 double latency = getMeasuredLatencyMillis();
95                 mLatencies.add(latency);
96                 double confidence = getMeasuredConfidence();
97                 mConfidences.add(confidence);
98 
99                 double timestampLatency = getTimestampLatencyMillis();
100                 if (timestampLatency > 0.0) {
101                     mTimestampLatencies.add(timestampLatency);
102                 }
103                 if (getGoodCount() < GOOD_RUNS_REQUIRED) {
104                     reschedule = true;
105                 } else {
106                     mActive = false;
107                     updateButtons(false);
108                 }
109                 message = reportAverage();
110             }
111             if (reschedule) {
112                 mHandler.postDelayed(new Runnable() {
113                     @Override
114                     public void run() {
115                         measureSingleLatency();
116                     }
117                 }, AVERAGE_TEST_DELAY_MSEC);
118             }
119             return message;
120         }
121 
reportAverage()122         private String reportAverage() {
123             String message;
124             if (getGoodCount() == 0 || mConfidences.getSum() == 0.0) {
125                 message = "num.iterations = " + getGoodCount() + "\n";
126             } else {
127                 final double mAverageConfidence = mConfidences.calculateMean();
128                 double meanLatency = mLatencies.calculateMean();
129                 double meanAbsoluteDeviation = mLatencies.calculateMeanAbsoluteDeviation(meanLatency);
130                 double timestampLatencyMean = -1;
131                 double timestampLatencyMAD = 0.0;
132                 if (mTimestampLatencies.count() > 0) {
133                     timestampLatencyMean = mTimestampLatencies.calculateMean();
134                     timestampLatencyMAD =
135                             mTimestampLatencies.calculateMeanAbsoluteDeviation(timestampLatencyMean);
136                 }
137                 message = "average.latency.msec = "
138                         + String.format(Locale.getDefault(), LATENCY_FORMAT, meanLatency) + "\n"
139                         + "mean.absolute.deviation = "
140                         + String.format(Locale.getDefault(), LATENCY_FORMAT, meanAbsoluteDeviation) + "\n"
141                         + "average.confidence = "
142                         + String.format(Locale.getDefault(), CONFIDENCE_FORMAT, mAverageConfidence) + "\n"
143                         + "min.latency.msec = " + String.format(Locale.getDefault(), LATENCY_FORMAT, mLatencies.getMin()) + "\n"
144                         + "max.latency.msec = " + String.format(Locale.getDefault(), LATENCY_FORMAT, mLatencies.getMax()) + "\n"
145                         + "num.iterations = " + mLatencies.count() + "\n"
146                         + "timestamp.latency.msec = "
147                         + String.format(Locale.getDefault(), LATENCY_FORMAT, timestampLatencyMean) + "\n"
148                         + "timestamp.latency.mad = "
149                         + String.format(Locale.getDefault(), LATENCY_FORMAT, timestampLatencyMAD) + "\n";
150             }
151             message += "num.failed = " + mBadCount + "\n";
152             message += "\n"; // mark end of average report
153             mLastReport = message;
154             return message;
155         }
156 
157         // Called on UI thread.
start()158         public void start() {
159             mLatencies = new DoubleStatistics();
160             mConfidences = new DoubleStatistics();
161             mTimestampLatencies = new DoubleStatistics();
162             mBadCount = 0;
163             mActive = true;
164             mLastReport = "";
165             measureSingleLatency();
166         }
167 
clear()168         public void clear() {
169             mActive = false;
170             mLastReport = "";
171         }
172 
cancel()173         public void cancel() {
174             mActive = false;
175         }
176 
isActive()177         public boolean isActive() {
178             return mActive;
179         }
180 
getLastReport()181         public String getLastReport() {
182             return mLastReport;
183         }
184     }
185     AverageLatencyTestRunner mAverageLatencyTestRunner = new AverageLatencyTestRunner();
186 
187     // Periodically query the status of the stream.
188     protected class LatencySniffer {
189         private int counter = 0;
190         public static final int SNIFFER_UPDATE_PERIOD_MSEC = 150;
191         public static final int SNIFFER_UPDATE_DELAY_MSEC = 300;
192 
193         // Display status info for the stream.
194         private Runnable runnableCode = new Runnable() {
195             @Override
196             public void run() {
197                 double timestampLatency = -1.0;
198                 int state = getAnalyzerState();
199                 if (state == STATE_MEASURE_BACKGROUND || state == STATE_IN_PULSE) {
200                     timestampLatency = measureTimestampLatency();
201                     // Some configurations do not support input timestamps.
202                     if (timestampLatency > 0) {
203                         mTimestampLatencyStats.add(timestampLatency);
204                     }
205                 }
206 
207                 String message;
208                 if (isAnalyzerDone()) {
209                     if (mAverageLatencyTestRunner.isActive()) {
210                         message = mAverageLatencyTestRunner.onAnalyserDone();
211                     } else {
212                         message = getResultString();
213                     }
214                     File resultFile = onAnalyzerDone();
215                     if (resultFile != null) {
216                         message = "result.file = " + resultFile.getAbsolutePath() + "\n" + message;
217                     }
218                 } else {
219                     message = getProgressText();
220                     message += "please wait... " + counter + "\n";
221                     message += convertStateToString(getAnalyzerState()) + "\n";
222 
223                     // Repeat this runnable code block again.
224                     mHandler.postDelayed(runnableCode, SNIFFER_UPDATE_PERIOD_MSEC);
225                 }
226                 setAnalyzerText(message);
227                 counter++;
228             }
229         };
230 
startSniffer()231         private void startSniffer() {
232             counter = 0;
233             // Start the initial runnable task by posting through the handler
234             mHandler.postDelayed(runnableCode, SNIFFER_UPDATE_DELAY_MSEC);
235         }
236 
stopSniffer()237         private void stopSniffer() {
238             if (mHandler != null) {
239                 mHandler.removeCallbacks(runnableCode);
240             }
241         }
242     }
243 
convertStateToString(int state)244     static String convertStateToString(int state) {
245         switch (state) {
246             case STATE_MEASURE_BACKGROUND: return "BACKGROUND";
247             case STATE_IN_PULSE: return "RECORDING";
248             case STATE_GOT_DATA: return "ANALYZING";
249             default: return "DONE";
250         }
251     }
252 
getProgressText()253     private String getProgressText() {
254         int progress = getAnalyzerProgress();
255         int state = getAnalyzerState();
256         int resetCount = getResetCount();
257         String message = String.format(Locale.getDefault(), "progress = %d\nstate = %d\n#resets = %d\n",
258                 progress, state, resetCount);
259         message += mAverageLatencyTestRunner.getLastReport();
260         return message;
261     }
262 
onAnalyzerDone()263     private File onAnalyzerDone() {
264         File resultFile = null;
265         if (mTestRunningByIntent) {
266             String report = getCommonTestReport();
267             report += getResultString();
268             resultFile = maybeWriteTestResult(report);
269         }
270         mTestRunningByIntent = false;
271         mHasRecording = true;
272         stopAudioTest();
273         return resultFile;
274     }
275 
276     @NonNull
getResultString()277     private String getResultString() {
278         int result = getMeasuredResult();
279         int resetCount = getResetCount();
280         double confidence = getMeasuredConfidence();
281         String message = "";
282 
283         message += String.format(Locale.getDefault(), "confidence = " + CONFIDENCE_FORMAT + "\n", confidence);
284         message += String.format(Locale.getDefault(), "result.text = %s\n", resultCodeToString(result));
285 
286         // Only report valid latencies.
287         if (result == 0) {
288             int latencyFrames = getMeasuredLatency();
289             double latencyMillis = getMeasuredLatencyMillis();
290             int bufferSize = mAudioOutTester.getCurrentAudioStream().getBufferSizeInFrames();
291             int latencyEmptyFrames = latencyFrames - bufferSize;
292             double latencyEmptyMillis = latencyEmptyFrames * 1000.0 / getSampleRate();
293             message += String.format(Locale.getDefault(), "latency.msec = " + LATENCY_FORMAT + "\n", latencyMillis);
294             message += String.format(Locale.getDefault(), "latency.frames = %d\n", latencyFrames);
295             message += String.format(Locale.getDefault(), "latency.empty.msec = " + LATENCY_FORMAT + "\n", latencyEmptyMillis);
296             message += String.format(Locale.getDefault(), "latency.empty.frames = %d\n", latencyEmptyFrames);
297         }
298 
299         message += String.format(Locale.getDefault(), "rms.signal = %7.5f\n", getSignalRMS());
300         message += String.format(Locale.getDefault(), "rms.noise = %7.5f\n", getBackgroundRMS());
301         message += String.format(Locale.getDefault(), "correlation = " + CONFIDENCE_FORMAT + "\n",
302                 getMeasuredCorrelation());
303         double timestampLatency = getTimestampLatencyMillis();
304         message += String.format(Locale.getDefault(), "timestamp.latency.msec = " + LATENCY_FORMAT + "\n",
305                 timestampLatency);
306         if (mTimestampLatencyStats.count() > 0) {
307             message += String.format(Locale.getDefault(), "timestamp.latency.mad = " + LATENCY_FORMAT + "\n",
308                     mTimestampLatencyStats.calculateMeanAbsoluteDeviation(timestampLatency));
309         }
310         message +=  "timestamp.latency.count = " + mTimestampLatencyStats.count() + "\n";
311         message += String.format(Locale.getDefault(), "reset.count = %d\n", resetCount);
312         message += String.format(Locale.getDefault(), "result = %d\n", result);
313 
314         return message;
315     }
316 
317     private LatencySniffer mLatencySniffer = new LatencySniffer();
318 
getMeasuredLatencyMillis()319     double getMeasuredLatencyMillis() {
320         return getMeasuredLatency() * 1000.0 / getSampleRate();
321     }
322 
getTimestampLatencyMillis()323     double getTimestampLatencyMillis() {
324         if (mTimestampLatencyStats.count() == 0) return -1.0;
325         else return mTimestampLatencyStats.calculateMean();
326     }
327 
getAnalyzerProgress()328     native int getAnalyzerProgress();
getMeasuredLatency()329     native int getMeasuredLatency();
measureTimestampLatency()330     native double measureTimestampLatency();
getMeasuredConfidence()331     native double getMeasuredConfidence();
getMeasuredCorrelation()332     native double getMeasuredCorrelation();
getBackgroundRMS()333     native double getBackgroundRMS();
getSignalRMS()334     native double getSignalRMS();
335 
setAnalyzerText(String s)336     private void setAnalyzerText(String s) {
337         mAnalyzerView.setText(s);
338     }
339 
340     @Override
inflateActivity()341     protected void inflateActivity() {
342         setContentView(R.layout.activity_rt_latency);
343     }
344 
345     @Override
onCreate(Bundle savedInstanceState)346     protected void onCreate(Bundle savedInstanceState) {
347         super.onCreate(savedInstanceState);
348         mMeasureButton = (Button) findViewById(R.id.button_measure);
349         mAverageButton = (Button) findViewById(R.id.button_average);
350         mCancelButton = (Button) findViewById(R.id.button_cancel);
351         mShareButton = (Button) findViewById(R.id.button_share);
352         mShareButton.setEnabled(false);
353         mAnalyzerView = (TextView) findViewById(R.id.text_status);
354         updateEnabledWidgets();
355 
356         hideSettingsViews();
357 
358         mBufferSizeView.setFaderNormalizedProgress(0.0); // for lowest latency
359 
360         mCommunicationDeviceView = (CommunicationDeviceView) findViewById(R.id.comm_device_view);
361 
362     }
363 
364     @Override
getActivityType()365     int getActivityType() {
366         return ACTIVITY_RT_LATENCY;
367     }
368 
369     @Override
onStart()370     protected void onStart() {
371         super.onStart();
372         mHasRecording = false;
373         updateButtons(false);
374     }
375 
376     @Override
startTestUsingBundle()377     public void startTestUsingBundle() {
378         try {
379             StreamConfiguration requestedInConfig = mAudioInputTester.requestedConfiguration;
380             StreamConfiguration requestedOutConfig = mAudioOutTester.requestedConfiguration;
381             configureStreamsFromBundle(mBundleFromIntent, requestedInConfig, requestedOutConfig);
382 
383             mBufferBursts = mBundleFromIntent.getInt(IntentBasedTestSupport.KEY_BUFFER_BURSTS, mBufferBursts);
384 
385             onMeasure(null);
386         } finally {
387             mBundleFromIntent = null;
388         }
389     }
390 
391     @Override
onStop()392     protected void onStop() {
393         mLatencySniffer.stopSniffer();
394         super.onStop();
395     }
396 
onMeasure(View view)397     public void onMeasure(View view) {
398         mAverageLatencyTestRunner.clear();
399         measureSingleLatency();
400     }
401 
updateButtons(boolean running)402     void updateButtons(boolean running) {
403         boolean busy = running || mAverageLatencyTestRunner.isActive();
404         mMeasureButton.setEnabled(!busy);
405         mAverageButton.setEnabled(!busy);
406         mCancelButton.setEnabled(running);
407         mShareButton.setEnabled(!busy && mHasRecording);
408     }
409 
measureSingleLatency()410     private void measureSingleLatency() {
411         try {
412             openAudio();
413             if (mBufferBursts >= 0) {
414                 AudioStreamBase stream = mAudioOutTester.getCurrentAudioStream();
415                 int framesPerBurst = stream.getFramesPerBurst();
416                 stream.setBufferSizeInFrames(framesPerBurst * mBufferBursts);
417                 // override buffer size fader
418                 mBufferSizeView.setEnabled(false);
419                 mBufferBursts = -1;
420             }
421             startAudio();
422             mTimestampLatencyStats  = new DoubleStatistics();
423             mLatencySniffer.startSniffer();
424             updateButtons(true);
425         } catch (IOException e) {
426             showErrorToast(e.getMessage());
427         }
428     }
429 
onAverage(View view)430     public void onAverage(View view) {
431         mAverageLatencyTestRunner.start();
432     }
433 
onCancel(View view)434     public void onCancel(View view) {
435         mAverageLatencyTestRunner.cancel();
436         stopAudioTest();
437     }
438 
439     // Call on UI thread
stopAudioTest()440     public void stopAudioTest() {
441         mLatencySniffer.stopSniffer();
442         stopAudio();
443         closeAudio();
444         updateButtons(false);
445     }
446 
447     @Override
getWaveTag()448     String getWaveTag() {
449         return "rtlatency";
450     }
451 
452     @Override
isOutput()453     boolean isOutput() {
454         return false;
455     }
456 }
457