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