blob: 69207688e9a2d34bf218e96b6ff571110be88501 [file] [log] [blame]
/*
* Copyright 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.mobileer.oboetester;
import static com.mobileer.oboetester.IntentBasedTestSupport.configureStreamsFromBundle;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.support.annotation.NonNull;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import java.io.IOException;
/**
* Activity to measure latency on a full duplex stream.
*/
public class RoundTripLatencyActivity extends AnalyzerActivity {
// STATEs defined in LatencyAnalyzer.h
private static final int STATE_MEASURE_BACKGROUND = 0;
private static final int STATE_IN_PULSE = 1;
private static final int STATE_GOT_DATA = 2;
private final static String LATENCY_FORMAT = "%4.2f";
// When I use 5.3g I only get one digit after the decimal point!
private final static String CONFIDENCE_FORMAT = "%5.3f";
private TextView mAnalyzerView;
private Button mMeasureButton;
private Button mAverageButton;
private Button mCancelButton;
private Button mShareButton;
private boolean mHasRecording = false;
private int mBufferBursts = -1;
private Handler mHandler = new Handler(Looper.getMainLooper()); // UI thread
DoubleStatistics mTimestampLatencyStats = new DoubleStatistics(); // for single measurement
// Run the test several times and report the average latency.
protected class AverageLatencyTestRunner {
private final static int AVERAGE_TEST_DELAY_MSEC = 1000; // arbitrary
private static final int GOOD_RUNS_REQUIRED = 5; // arbitrary
private static final int MAX_BAD_RUNS_ALLOWED = 5; // arbitrary
private int mBadCount = 0; // number of bad measurements
DoubleStatistics mLatencies = new DoubleStatistics();
DoubleStatistics mConfidences = new DoubleStatistics();
DoubleStatistics mTimestampLatencies = new DoubleStatistics(); // for multiple measurements
private boolean mActive;
private String mLastReport = "";
private int getGoodCount() {
return mLatencies.count();
}
// Called on UI thread.
String onAnalyserDone() {
String message;
boolean reschedule = false;
if (!mActive) {
message = "";
} else if (getMeasuredResult() != 0) {
mBadCount++;
if (mBadCount > MAX_BAD_RUNS_ALLOWED) {
cancel();
updateButtons(false);
message = "averaging cancelled due to error\n";
} else {
message = "skipping this bad run, "
+ mBadCount + " of " + MAX_BAD_RUNS_ALLOWED + " max\n";
reschedule = true;
}
} else {
double latency = getMeasuredLatencyMillis();
mLatencies.add(latency);
double confidence = getMeasuredConfidence();
mConfidences.add(confidence);
double timestampLatency = getTimestampLatencyMillis();
if (timestampLatency > 0.0) {
mTimestampLatencies.add(timestampLatency);
}
if (getGoodCount() < GOOD_RUNS_REQUIRED) {
reschedule = true;
} else {
mActive = false;
updateButtons(false);
}
message = reportAverage();
}
if (reschedule) {
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
measureSingleLatency();
}
}, AVERAGE_TEST_DELAY_MSEC);
}
return message;
}
private String reportAverage() {
String message;
if (getGoodCount() == 0 || mConfidences.getSum() == 0.0) {
message = "num.iterations = " + getGoodCount() + "\n";
} else {
final double mAverageConfidence = mConfidences.calculateMean();
double meanLatency = mLatencies.calculateMean();
double meanAbsoluteDeviation = mLatencies.calculateMeanAbsoluteDeviation(meanLatency);
double timestampLatencyMean = -1;
double timestampLatencyMAD = 0.0;
if (mTimestampLatencies.count() > 0) {
timestampLatencyMean = mTimestampLatencies.calculateMean();
timestampLatencyMAD =
mTimestampLatencies.calculateMeanAbsoluteDeviation(timestampLatencyMean);
}
message = "average.latency.msec = "
+ String.format(LATENCY_FORMAT, meanLatency) + "\n"
+ "mean.absolute.deviation = "
+ String.format(LATENCY_FORMAT, meanAbsoluteDeviation) + "\n"
+ "average.confidence = "
+ String.format(CONFIDENCE_FORMAT, mAverageConfidence) + "\n"
+ "min.latency.msec = " + String.format(LATENCY_FORMAT, mLatencies.getMin()) + "\n"
+ "max.latency.msec = " + String.format(LATENCY_FORMAT, mLatencies.getMax()) + "\n"
+ "num.iterations = " + mLatencies.count() + "\n"
+ "timestamp.latency.msec = "
+ String.format(LATENCY_FORMAT, timestampLatencyMean) + "\n"
+ "timestamp.latency.mad = "
+ String.format(LATENCY_FORMAT, timestampLatencyMAD) + "\n";
}
message += "num.failed = " + mBadCount + "\n";
message += "\n"; // mark end of average report
mLastReport = message;
return message;
}
// Called on UI thread.
public void start() {
mLatencies = new DoubleStatistics();
mConfidences = new DoubleStatistics();
mTimestampLatencies = new DoubleStatistics();
mBadCount = 0;
mActive = true;
mLastReport = "";
measureSingleLatency();
}
public void clear() {
mActive = false;
mLastReport = "";
}
public void cancel() {
mActive = false;
}
public boolean isActive() {
return mActive;
}
public String getLastReport() {
return mLastReport;
}
}
AverageLatencyTestRunner mAverageLatencyTestRunner = new AverageLatencyTestRunner();
// Periodically query the status of the stream.
protected class LatencySniffer {
private int counter = 0;
public static final int SNIFFER_UPDATE_PERIOD_MSEC = 150;
public static final int SNIFFER_UPDATE_DELAY_MSEC = 300;
// Display status info for the stream.
private Runnable runnableCode = new Runnable() {
@Override
public void run() {
double timestampLatency = -1.0;
int state = getAnalyzerState();
if (state == STATE_MEASURE_BACKGROUND || state == STATE_IN_PULSE) {
timestampLatency = measureTimestampLatency();
// Some configurations do not support input timestamps.
if (timestampLatency > 0) {
mTimestampLatencyStats.add(timestampLatency);
}
}
String message;
if (isAnalyzerDone()) {
if (mAverageLatencyTestRunner.isActive()) {
message = mAverageLatencyTestRunner.onAnalyserDone();
} else {
message = getResultString();
}
onAnalyzerDone();
} else {
message = getProgressText();
message += "please wait... " + counter + "\n";
message += convertStateToString(getAnalyzerState()) + "\n";
// Repeat this runnable code block again.
mHandler.postDelayed(runnableCode, SNIFFER_UPDATE_PERIOD_MSEC);
}
setAnalyzerText(message);
counter++;
}
};
private void startSniffer() {
counter = 0;
// Start the initial runnable task by posting through the handler
mHandler.postDelayed(runnableCode, SNIFFER_UPDATE_DELAY_MSEC);
}
private void stopSniffer() {
if (mHandler != null) {
mHandler.removeCallbacks(runnableCode);
}
}
}
static String convertStateToString(int state) {
switch (state) {
case STATE_MEASURE_BACKGROUND: return "BACKGROUND";
case STATE_IN_PULSE: return "RECORDING";
case STATE_GOT_DATA: return "ANALYZING";
default: return "DONE";
}
}
private String getProgressText() {
int progress = getAnalyzerProgress();
int state = getAnalyzerState();
int resetCount = getResetCount();
String message = String.format("progress = %d\nstate = %d\n#resets = %d\n",
progress, state, resetCount);
message += mAverageLatencyTestRunner.getLastReport();
return message;
}
private void onAnalyzerDone() {
if (mTestRunningByIntent) {
String report = getCommonTestReport();
report += getResultString();
maybeWriteTestResult(report);
}
mTestRunningByIntent = false;
mHasRecording = true;
stopAudioTest();
}
@NonNull
private String getResultString() {
int result = getMeasuredResult();
int resetCount = getResetCount();
double confidence = getMeasuredConfidence();
String message = "";
message += String.format("confidence = " + CONFIDENCE_FORMAT + "\n", confidence);
message += String.format("result.text = %s\n", resultCodeToString(result));
// Only report valid latencies.
if (result == 0) {
int latencyFrames = getMeasuredLatency();
double latencyMillis = getMeasuredLatencyMillis();
int bufferSize = mAudioOutTester.getCurrentAudioStream().getBufferSizeInFrames();
int latencyEmptyFrames = latencyFrames - bufferSize;
double latencyEmptyMillis = latencyEmptyFrames * 1000.0 / getSampleRate();
message += String.format("latency.msec = " + LATENCY_FORMAT + "\n", latencyMillis);
message += String.format("latency.frames = %d\n", latencyFrames);
message += String.format("latency.empty.msec = " + LATENCY_FORMAT + "\n", latencyEmptyMillis);
message += String.format("latency.empty.frames = %d\n", latencyEmptyFrames);
}
message += String.format("rms.signal = %7.5f\n", getSignalRMS());
message += String.format("rms.noise = %7.5f\n", getBackgroundRMS());
message += String.format("correlation = " + CONFIDENCE_FORMAT + "\n",
getMeasuredCorrelation());
double timestampLatency = getTimestampLatencyMillis();
message += String.format("timestamp.latency.msec = " + LATENCY_FORMAT + "\n",
timestampLatency);
if (mTimestampLatencyStats.count() > 0) {
message += String.format("timestamp.latency.mad = " + LATENCY_FORMAT + "\n",
mTimestampLatencyStats.calculateMeanAbsoluteDeviation(timestampLatency));
}
message += "timestamp.latency.count = " + mTimestampLatencyStats.count() + "\n";
message += String.format("reset.count = %d\n", resetCount);
message += String.format("result = %d\n", result);
return message;
}
private LatencySniffer mLatencySniffer = new LatencySniffer();
double getMeasuredLatencyMillis() {
return getMeasuredLatency() * 1000.0 / getSampleRate();
}
double getTimestampLatencyMillis() {
if (mTimestampLatencyStats.count() == 0) return -1.0;
else return mTimestampLatencyStats.calculateMean();
}
native int getAnalyzerProgress();
native int getMeasuredLatency();
native double measureTimestampLatency();
native double getMeasuredConfidence();
native double getMeasuredCorrelation();
native double getBackgroundRMS();
native double getSignalRMS();
private void setAnalyzerText(String s) {
mAnalyzerView.setText(s);
}
@Override
protected void inflateActivity() {
setContentView(R.layout.activity_rt_latency);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mMeasureButton = (Button) findViewById(R.id.button_measure);
mAverageButton = (Button) findViewById(R.id.button_average);
mCancelButton = (Button) findViewById(R.id.button_cancel);
mShareButton = (Button) findViewById(R.id.button_share);
mShareButton.setEnabled(false);
mAnalyzerView = (TextView) findViewById(R.id.text_status);
updateEnabledWidgets();
hideSettingsViews();
mBufferSizeView.setFaderNormalizedProgress(0.0); // for lowest latency
}
@Override
int getActivityType() {
return ACTIVITY_RT_LATENCY;
}
@Override
protected void onStart() {
super.onStart();
mHasRecording = false;
updateButtons(false);
}
@Override
public void startTestUsingBundle() {
try {
StreamConfiguration requestedInConfig = mAudioInputTester.requestedConfiguration;
StreamConfiguration requestedOutConfig = mAudioOutTester.requestedConfiguration;
configureStreamsFromBundle(mBundleFromIntent, requestedInConfig, requestedOutConfig);
mBufferBursts = mBundleFromIntent.getInt(IntentBasedTestSupport.KEY_BUFFER_BURSTS, mBufferBursts);
onMeasure(null);
} finally {
mBundleFromIntent = null;
}
}
@Override
protected void onStop() {
mLatencySniffer.stopSniffer();
super.onStop();
}
public void onMeasure(View view) {
mAverageLatencyTestRunner.clear();
measureSingleLatency();
}
void updateButtons(boolean running) {
boolean busy = running || mAverageLatencyTestRunner.isActive();
mMeasureButton.setEnabled(!busy);
mAverageButton.setEnabled(!busy);
mCancelButton.setEnabled(running);
mShareButton.setEnabled(!busy && mHasRecording);
}
private void measureSingleLatency() {
try {
openAudio();
if (mBufferBursts >= 0) {
AudioStreamBase stream = mAudioOutTester.getCurrentAudioStream();
int framesPerBurst = stream.getFramesPerBurst();
stream.setBufferSizeInFrames(framesPerBurst * mBufferBursts);
// override buffer size fader
mBufferSizeView.setEnabled(false);
mBufferBursts = -1;
}
startAudio();
mTimestampLatencyStats = new DoubleStatistics();
mLatencySniffer.startSniffer();
updateButtons(true);
} catch (IOException e) {
showErrorToast(e.getMessage());
}
}
public void onAverage(View view) {
mAverageLatencyTestRunner.start();
}
public void onCancel(View view) {
mAverageLatencyTestRunner.cancel();
stopAudioTest();
}
// Call on UI thread
public void stopAudioTest() {
mLatencySniffer.stopSniffer();
stopAudio();
closeAudio();
updateButtons(false);
}
@Override
String getWaveTag() {
return "rtlatency";
}
@Override
boolean isOutput() {
return false;
}
}