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