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.google.sample.oboe.manualtest; 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