summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGlenn Kasten <gkasten@google.com>2016-08-04 15:18:50 -0700
committerGlenn Kasten <gkasten@google.com>2016-08-04 15:36:48 -0700
commit144dd00dcb045631df54ec7c755288a1dadeddc3 (patch)
treec0514070d3a01ca0f11e26d208f91250202b65c1
parentc58bf2f1d8871c89e1d32b237193997cd55ec7d1 (diff)
downloaddrrickorang-144dd00dcb045631df54ec7c755288a1dadeddc3.tar.gz
Version 15
Snap to commit 1265b7e83b43ebc8227d9243591914ea0721cec4 Allow enabling systrace and bugreport captures separately in LoopbackApp. Capturing a bugreport takes a lot of time and CPU power. The CPU usage may cause another glitch, or an unrelated glitch may be missed because the app is waiting for a bugreport to complete. Capture systrace/bugreport on late callbacks in LoopbackApp. Remove obsolete method for systrace from native code in LoopbackApp. The code for this was unused. Add more atomic methods to audio_utils in LoopbackApp. Note that these methods are temporary until we can move to the standard atomic library for C++. Discard player callbacks if recorder is not yet running. Display channelIndex in LoopbackApp as MONO when appropriate. Iteratively calculate variance in LoopbackApp for Java. Iteratively calculate variance in LoopbackApp for native only. It is converted into standard deviation and reported by the app. Refactor statistics recording into a new function in LoopbackApp. This will make it easier to add new metrics and adjust the existing ones. Deduplicate code dealing with buffer period stats in LoopbackApp. Previously, the code for the recorder and player was copied and pasted with minimal changes. Fix build warning. Refactor LoopbackApp buffer stats into one struct. By unifying the player and recorder stats, copy-pasted code can be eliminated and adding new statistics is made much easier. Fix how late buffer callbacks are counted in LoopbackApp. The previous calculation id not take rounding into account which causes it to incorrectly label callbacks which occured only 1 ms late as over the threshold of 2 ms late. This results in the metric being oversensitive and flaky. Add setting to LoopbackApp to ignore beginning of recording. This is useful to work around hardware bugs that cause pops or other noise at the beginning of a recording.
-rw-r--r--LoopbackApp/app/src/main/AndroidManifest.xml4
-rw-r--r--LoopbackApp/app/src/main/java/org/drrickorang/loopback/BufferPeriod.java29
-rw-r--r--LoopbackApp/app/src/main/java/org/drrickorang/loopback/CaptureHolder.java74
-rw-r--r--LoopbackApp/app/src/main/java/org/drrickorang/loopback/Constant.java8
-rw-r--r--LoopbackApp/app/src/main/java/org/drrickorang/loopback/GlitchDetectionThread.java6
-rw-r--r--LoopbackApp/app/src/main/java/org/drrickorang/loopback/LoopbackActivity.java87
-rw-r--r--LoopbackApp/app/src/main/java/org/drrickorang/loopback/LoopbackApplication.java23
-rw-r--r--LoopbackApp/app/src/main/java/org/drrickorang/loopback/NativeAudioThread.java38
-rw-r--r--LoopbackApp/app/src/main/java/org/drrickorang/loopback/PerformanceMeasurement.java13
-rw-r--r--LoopbackApp/app/src/main/java/org/drrickorang/loopback/SettingsActivity.java33
-rw-r--r--LoopbackApp/app/src/main/jni/audio_utils/atomic.c11
-rw-r--r--LoopbackApp/app/src/main/jni/audio_utils/atomic.h5
-rw-r--r--LoopbackApp/app/src/main/jni/jni_sles.c45
-rw-r--r--LoopbackApp/app/src/main/jni/jni_sles.h17
-rw-r--r--LoopbackApp/app/src/main/jni/sles.cpp304
-rw-r--r--LoopbackApp/app/src/main/jni/sles.h65
-rw-r--r--LoopbackApp/app/src/main/res/layout/settings_activity.xml55
-rw-r--r--LoopbackApp/app/src/main/res/raw/loopback_listener22
-rw-r--r--LoopbackApp/app/src/main/res/values/strings.xml5
19 files changed, 548 insertions, 296 deletions
diff --git a/LoopbackApp/app/src/main/AndroidManifest.xml b/LoopbackApp/app/src/main/AndroidManifest.xml
index fbe8ba6..1b2f5f3 100644
--- a/LoopbackApp/app/src/main/AndroidManifest.xml
+++ b/LoopbackApp/app/src/main/AndroidManifest.xml
@@ -23,8 +23,8 @@
xmlns:android="http://schemas.android.com/apk/res/android"
package="org.drrickorang.loopback"
- android:versionCode="14"
- android:versionName="0.9.71">
+ android:versionCode="15"
+ android:versionName="0.9.72">
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
diff --git a/LoopbackApp/app/src/main/java/org/drrickorang/loopback/BufferPeriod.java b/LoopbackApp/app/src/main/java/org/drrickorang/loopback/BufferPeriod.java
index b4cd87e..97ab6ad 100644
--- a/LoopbackApp/app/src/main/java/org/drrickorang/loopback/BufferPeriod.java
+++ b/LoopbackApp/app/src/main/java/org/drrickorang/loopback/BufferPeriod.java
@@ -16,13 +16,13 @@
package org.drrickorang.loopback;
-import java.util.Arrays;
-
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.Log;
+import java.util.Arrays;
+
/**
* This class records the buffer period of the audio player or recorder when in Java mode.
@@ -37,9 +37,14 @@ public class BufferPeriod implements Parcelable {
private long mPreviousTimeNs = 0;
private long mCurrentTimeNs = 0;
+ private int mMeasurements = 0;
+ private long mVar; // variance in nanoseconds^2
+ private long mSDM = 0; // sum of squares of deviations from the expected mean
private int mMaxBufferPeriod = 0;
+
private int mCount = 0;
private final int range = 1002; // store counts for 0ms to 1000ms, and for > 1000ms
+ private int mExpectedBufferPeriod = 0;
private int[] mBufferPeriod = new int[range];
private BufferCallbackTimes mCallbackTimes;
@@ -62,8 +67,9 @@ public class BufferPeriod implements Parcelable {
mStartTimeNs = mCurrentTimeNs;
}
- final int discard = 10; // discard the first few buffer period values
- if (mPreviousTimeNs != 0 && mCount > discard) {
+ if (mPreviousTimeNs != 0 && mCount > Constant.BUFFER_PERIOD_DISCARD) {
+ mMeasurements++;
+
long diffInNano = mCurrentTimeNs - mPreviousTimeNs;
// diffInMilli is rounded up
int diffInMilli = (int) ((diffInNano + Constant.NANOS_PER_MILLI - 1) /
@@ -86,6 +92,12 @@ public class BufferPeriod implements Parcelable {
log("Having negative BufferPeriod.");
}
+ long delta = diffInNano - (long) mExpectedBufferPeriod * Constant.NANOS_PER_MILLI;
+ mSDM += delta * delta;
+ if (mCount > 1) {
+ mVar = mSDM / mMeasurements;
+ }
+
mCallbackTimes.recordCallbackTime(timeStampInMilli, (short) diffInMilli);
// If diagnosing specific Java thread callback behavior set a conditional here and use
@@ -102,6 +114,8 @@ public class BufferPeriod implements Parcelable {
mCurrentTimeNs = 0;
Arrays.fill(mBufferPeriod, 0);
mMaxBufferPeriod = 0;
+ mMeasurements = 0;
+ mExpectedBufferPeriod = 0;
mCount = 0;
mCallbackTimes = null;
}
@@ -110,12 +124,17 @@ public class BufferPeriod implements Parcelable {
CaptureHolder captureHolder){
mCallbackTimes = new BufferCallbackTimes(maxRecords, expectedBufferPeriod);
mCaptureHolder = captureHolder;
+ mExpectedBufferPeriod = expectedBufferPeriod;
}
public int[] getBufferPeriodArray() {
return mBufferPeriod;
}
+ public double getStdDevBufferPeriod() {
+ return Math.sqrt(mVar) / (double) Constant.NANOS_PER_MILLI;
+ }
+
public int getMaxBufferPeriod() {
return mMaxBufferPeriod;
}
@@ -136,6 +155,7 @@ public class BufferPeriod implements Parcelable {
Bundle out = new Bundle();
out.putInt("mMaxBufferPeriod", mMaxBufferPeriod);
out.putIntArray("mBufferPeriod", mBufferPeriod);
+ out.putInt("mExpectedBufferPeriod", mExpectedBufferPeriod);
out.putParcelable("mCallbackTimes", mCallbackTimes);
dest.writeBundle(out);
}
@@ -144,6 +164,7 @@ public class BufferPeriod implements Parcelable {
Bundle in = source.readBundle(getClass().getClassLoader());
mMaxBufferPeriod = in.getInt("mMaxBufferPeriod");
mBufferPeriod = in.getIntArray("mBufferPeriod");
+ mExpectedBufferPeriod = in.getInt("mExpectedBufferPeriod");
mCallbackTimes = in.getParcelable("mCallbackTimes");
}
diff --git a/LoopbackApp/app/src/main/java/org/drrickorang/loopback/CaptureHolder.java b/LoopbackApp/app/src/main/java/org/drrickorang/loopback/CaptureHolder.java
index fecacfe..774310b 100644
--- a/LoopbackApp/app/src/main/java/org/drrickorang/loopback/CaptureHolder.java
+++ b/LoopbackApp/app/src/main/java/org/drrickorang/loopback/CaptureHolder.java
@@ -24,6 +24,7 @@ import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
+import java.io.PrintWriter;
import java.util.concurrent.TimeUnit;
/**
@@ -36,8 +37,11 @@ public class CaptureHolder {
public static final String STORAGE = "/sdcard/";
public static final String DIRECTORY = STORAGE + "Loopback";
private static final String SIGNAL_FILE = DIRECTORY + "/loopback_signal";
+ // These suffixes are used to tell the listener script what types of data to collect.
+ // They MUST match the definitions in the script file.
private static final String SYSTRACE_SUFFIX = ".trace";
private static final String BUGREPORT_SUFFIX = "_bugreport.txt.gz";
+
private static final String WAV_SUFFIX = ".wav";
private static final String TERMINATE_SIGNAL = "QUIT";
@@ -51,8 +55,9 @@ public class CaptureHolder {
private final long mStartTimeMS;
private final boolean mIsCapturingWavs;
private final boolean mIsCapturingSystraces;
+ private final boolean mIsCapturingBugreports;
private final int mCaptureCapacity;
- private Thread mCaptureThread;
+ private CaptureThread mCaptureThread;
private volatile CapturedState mCapturedStates[];
private WaveDataRingBuffer mWaveDataBuffer;
@@ -61,11 +66,13 @@ public class CaptureHolder {
private final int mSamplingRate;
public CaptureHolder(int captureCapacity, String fileNamePrefix, boolean captureWavs,
- boolean captureSystraces, Context context, int samplingRate) {
+ boolean captureSystraces, boolean captureBugreports, Context context,
+ int samplingRate) {
mCaptureCapacity = captureCapacity;
mFileNamePrefix = fileNamePrefix;
mIsCapturingWavs = captureWavs;
mIsCapturingSystraces = captureSystraces;
+ mIsCapturingBugreports = captureBugreports;
mStartTimeMS = System.currentTimeMillis();
mCapturedStates = new CapturedState[mCaptureCapacity];
mContext = context;
@@ -94,14 +101,15 @@ public class CaptureHolder {
*/
public synchronized int captureState(int rank) {
- if (!mIsCapturingWavs && !mIsCapturingSystraces) {
- Log.d(TAG, "captureState: Capturing wavs or systraces not enabled");
+ if (!isCapturing()) {
+ Log.d(TAG, "captureState: Capturing state not enabled");
return CAPTURING_DISABLED;
}
if (mCaptureThread != null && mCaptureThread.getState() != Thread.State.TERMINATED) {
// Capture already in progress
Log.d(TAG, "captureState: Capture thread already running");
+ mCaptureThread.updateRank(rank);
return CAPTURE_ALREADY_IN_PROGRESS;
}
@@ -113,8 +121,8 @@ public class CaptureHolder {
TimeUnit.MINUTES.toSeconds(TimeUnit.MILLISECONDS.toMinutes(timeFromTestStartMS));
String timeString = String.format("%02dh%02dm%02ds", hours, minutes, seconds);
- String fullFileNamePrefix = STORAGE + mFileNamePrefix + '_' + timeString;
- CapturedState cs = new CapturedState(fullFileNamePrefix, timeFromTestStartMS, rank);
+ String fileNameBase = STORAGE + mFileNamePrefix + '_' + timeString;
+ CapturedState cs = new CapturedState(fileNameBase, timeFromTestStartMS, rank);
int indexOfLeastInteresting = getIndexOfLeastInterestingCapture(cs);
if (indexOfLeastInteresting == NEW_CAPTURE_IS_LEAST_INTERESTING) {
@@ -167,12 +175,8 @@ public class CaptureHolder {
return index;
}
- public boolean isCapturingWavs() {
- return mIsCapturingWavs;
- }
-
- public boolean isCapturingSysTraces() {
- return mIsCapturingSystraces;
+ public boolean isCapturing() {
+ return mIsCapturingWavs || mIsCapturingSystraces || mIsCapturingBugreports;
}
/**
@@ -180,19 +184,19 @@ public class CaptureHolder {
* for determining position in rolling queue
*/
private class CapturedState {
- public final String fileNamePrefix;
+ public final String fileNameBase;
public final long timeFromStartOfTestMS;
- public final int rank;
+ public int rank;
- public CapturedState(String fileNamePrefix, long timeFromStartOfTestMS, int rank) {
- this.fileNamePrefix = fileNamePrefix;
+ public CapturedState(String fileNameBase, long timeFromStartOfTestMS, int rank) {
+ this.fileNameBase = fileNameBase;
this.timeFromStartOfTestMS = timeFromStartOfTestMS;
this.rank = rank;
}
@Override
public String toString() {
- return "CapturedState { fileName:" + fileNamePrefix + ", Rank:" + rank + "}";
+ return "CapturedState { fileName:" + fileNameBase + ", Rank:" + rank + "}";
}
}
@@ -214,17 +218,22 @@ public class CaptureHolder {
@Override
public void run() {
- // Create two empty files and write that file name prefix to signal file, signalling
- // the listener script to write systrace and bugreport to those files
- if (mIsCapturingSystraces) {
+ // Write names of desired captures to signal file, signalling
+ // the listener script to write systrace and/or bugreport to those files
+ if (mIsCapturingSystraces || mIsCapturingBugreports) {
Log.d(TAG, "CaptureThread: signaling listener to write to:" +
- mNewCapturedState.fileNamePrefix);
+ mNewCapturedState.fileNameBase + "*");
try {
- new File(mNewCapturedState.fileNamePrefix + SYSTRACE_SUFFIX).createNewFile();
- new File(mNewCapturedState.fileNamePrefix + BUGREPORT_SUFFIX).createNewFile();
- OutputStream outputStream = new FileOutputStream(SIGNAL_FILE);
- outputStream.write(mNewCapturedState.fileNamePrefix.getBytes());
- outputStream.close();
+ PrintWriter writer = new PrintWriter(SIGNAL_FILE);
+ // mNewCapturedState.fileNameBase is the path and basename of the state files.
+ // Each suffix is used to tell the listener script to record that type of data.
+ if (mIsCapturingSystraces) {
+ writer.println(mNewCapturedState.fileNameBase + SYSTRACE_SUFFIX);
+ }
+ if (mIsCapturingBugreports) {
+ writer.println(mNewCapturedState.fileNameBase + BUGREPORT_SUFFIX);
+ }
+ writer.close();
} catch (IOException e) {
e.printStackTrace();
}
@@ -236,7 +245,7 @@ public class CaptureHolder {
WaveDataRingBuffer.ReadableWaveDeck deck = mWaveDataBuffer.getWaveDeck();
if (deck != null) {
AudioFileOutput audioFile = new AudioFileOutput(mContext,
- Uri.parse("file://mnt" + mNewCapturedState.fileNamePrefix
+ Uri.parse("file://mnt" + mNewCapturedState.fileNameBase
+ WAV_SUFFIX),
mSamplingRate);
boolean success = deck.writeToFile(audioFile);
@@ -246,11 +255,11 @@ public class CaptureHolder {
// Check for sys and bug finished
// loopback listener script signals completion by deleting signal file
- if (mIsCapturingSystraces) {
+ if (mIsCapturingSystraces || mIsCapturingBugreports) {
File signalFile = new File(SIGNAL_FILE);
while (signalFile.exists()) {
try {
- sleep(1);
+ sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
@@ -262,7 +271,7 @@ public class CaptureHolder {
if (mCapturedStates[mIndexToPlace] != null) {
Log.d(TAG, "Deleting capture: " + mCapturedStates[mIndexToPlace]);
for (String suffix : suffixes) {
- File oldFile = new File(mCapturedStates[mIndexToPlace].fileNamePrefix + suffix);
+ File oldFile = new File(mCapturedStates[mIndexToPlace].fileNameBase + suffix);
boolean deleted = oldFile.delete();
if (!deleted) {
Log.d(TAG, "Delete old capture: " + oldFile.toString() +
@@ -280,5 +289,10 @@ public class CaptureHolder {
Log.d(TAG, "Completed capture thread terminating");
}
+
+ // Sets the rank of the current capture to rank if it is greater than the current value
+ public synchronized void updateRank(int rank) {
+ mNewCapturedState.rank = Math.max(mNewCapturedState.rank, rank);
+ }
}
}
diff --git a/LoopbackApp/app/src/main/java/org/drrickorang/loopback/Constant.java b/LoopbackApp/app/src/main/java/org/drrickorang/loopback/Constant.java
index d015352..988bc08 100644
--- a/LoopbackApp/app/src/main/java/org/drrickorang/loopback/Constant.java
+++ b/LoopbackApp/app/src/main/java/org/drrickorang/loopback/Constant.java
@@ -35,6 +35,7 @@ public class Constant {
public static final int BYTES_PER_SHORT = 2;
public static final int SHORTS_PER_INT = 2;
+ // FIXME Assumes 16-bit and mono, will not work for other bit depths or multi-channel.
public static final int BYTES_PER_FRAME = 2; // bytes per sample
// prime numbers that don't overlap with FFT frequencies
@@ -75,7 +76,14 @@ public class Constant {
public static final int MIN_NUM_CAPTURES = 1;
public static final int MAX_NUM_CAPTURES = 100;
public static final int DEFAULT_NUM_CAPTURES = 5;
+ public static final int MIN_IGNORE_FIRST_FRAMES = 0;
+ // impulse happens after 300 ms and shouldn't be ignored
+ public static final int MAX_IGNORE_FIRST_FRAMES = SAMPLING_RATE_MAX * 3 / 10;
+ public static final int DEFAULT_IGNORE_FIRST_FRAMES = 0;
+
// Controls size of pre allocated timestamp arrays
public static final int MAX_RECORDED_LATE_CALLBACKS_PER_SECOND = 2;
+ // Ignore first few buffer callback periods
+ public static final int BUFFER_PERIOD_DISCARD = 10;
}
diff --git a/LoopbackApp/app/src/main/java/org/drrickorang/loopback/GlitchDetectionThread.java b/LoopbackApp/app/src/main/java/org/drrickorang/loopback/GlitchDetectionThread.java
index d88b065..e52c116 100644
--- a/LoopbackApp/app/src/main/java/org/drrickorang/loopback/GlitchDetectionThread.java
+++ b/LoopbackApp/app/src/main/java/org/drrickorang/loopback/GlitchDetectionThread.java
@@ -16,10 +16,10 @@
package org.drrickorang.loopback;
-import java.util.Arrays;
-
import android.util.Log;
+import java.util.Arrays;
+
/**
* This thread is responsible for detecting glitches in the samples.
@@ -213,7 +213,7 @@ public class GlitchDetectionThread extends Thread {
// Glitch Detected
mGlitches[mGlitchesIndex] = mFFTCount;
mGlitchesIndex++;
- if (mCaptureHolder.isCapturingSysTraces() || mCaptureHolder.isCapturingWavs()) {
+ if (mCaptureHolder.isCapturing()) {
checkGlitchConcentration();
}
}
diff --git a/LoopbackApp/app/src/main/java/org/drrickorang/loopback/LoopbackActivity.java b/LoopbackApp/app/src/main/java/org/drrickorang/loopback/LoopbackActivity.java
index 3ae11d0..17b3bf8 100644
--- a/LoopbackApp/app/src/main/java/org/drrickorang/loopback/LoopbackActivity.java
+++ b/LoopbackApp/app/src/main/java/org/drrickorang/loopback/LoopbackActivity.java
@@ -16,11 +16,6 @@
package org.drrickorang.loopback;
-import java.io.File;
-import java.io.FileDescriptor;
-import java.io.FileOutputStream;
-import java.util.Arrays;
-
import android.Manifest;
import android.app.Activity;
import android.app.DialogFragment;
@@ -34,8 +29,8 @@ import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.media.AudioManager;
import android.net.Uri;
-import android.os.Bundle;
import android.os.Build;
+import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
@@ -55,8 +50,14 @@ import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.PopupWindow;
import android.widget.SeekBar;
-import android.widget.Toast;
import android.widget.TextView;
+import android.widget.Toast;
+
+import java.io.File;
+import java.io.FileDescriptor;
+import java.io.FileOutputStream;
+import java.util.Arrays;
+import java.util.Locale;
/**
@@ -121,10 +122,12 @@ public class LoopbackActivity extends Activity
private BufferPeriod mPlayerBufferPeriod = new BufferPeriod();
// for native buffer period
- private int[] mNativeRecorderBufferPeriodArray;
- private int mNativeRecorderMaxBufferPeriod;
- private int[] mNativePlayerBufferPeriodArray;
- private int mNativePlayerMaxBufferPeriod;
+ private int[] mNativeRecorderBufferPeriodArray;
+ private int mNativeRecorderMaxBufferPeriod;
+ private double mNativeRecorderStdDevBufferPeriod;
+ private int[] mNativePlayerBufferPeriodArray;
+ private int mNativePlayerMaxBufferPeriod;
+ private double mNativePlayerStdDevBufferPeriod;
private BufferCallbackTimes mRecorderCallbackTimes;
private BufferCallbackTimes mPlayerCallbackTimes;
@@ -136,6 +139,7 @@ public class LoopbackActivity extends Activity
private static final String INTENT_AUDIO_THREAD = "AudioThread";
private static final String INTENT_MIC_SOURCE = "MicSource";
private static final String INTENT_AUDIO_LEVEL = "AudioLevel";
+ private static final String INTENT_IGNORE_FIRST_FRAMES = "IgnoreFirstFrames";
private static final String INTENT_TEST_TYPE = "TestType";
private static final String INTENT_BUFFER_TEST_DURATION = "BufferTestDuration";
private static final String INTENT_NUMBER_LOAD_THREADS = "NumLoadThreads";
@@ -156,6 +160,7 @@ public class LoopbackActivity extends Activity
private int mSoundLevel;
private int mPlayerBufferSizeInBytes;
private int mRecorderBufferSizeInBytes;
+ private int mIgnoreFirstFrames; // TODO: this only applies to native mode
// for buffer test
private int[] mGlitchesData;
@@ -272,7 +277,7 @@ public class LoopbackActivity extends Activity
showToast("Java Buffer Test Completed");
break;
}
- if (getApp().isCaptureSysTraceEnabled()) {
+ if (getApp().isCaptureEnabled()) {
CaptureHolder.stopLoopbackListenerScript();
}
stopAudioTestThreads();
@@ -325,10 +330,14 @@ public class LoopbackActivity extends Activity
mFFTOverlapSamples = mNativeAudioThread.getNativeFFTOverlapSamples();
mWaveData = mNativeAudioThread.getWaveData();
mNativeRecorderBufferPeriodArray = mNativeAudioThread.getRecorderBufferPeriod();
- mNativeRecorderMaxBufferPeriod = mNativeAudioThread.
- getRecorderMaxBufferPeriod();
+ mNativeRecorderMaxBufferPeriod =
+ mNativeAudioThread.getRecorderMaxBufferPeriod();
+ mNativeRecorderStdDevBufferPeriod =
+ mNativeAudioThread.getRecorderStdDevBufferPeriod();
mNativePlayerBufferPeriodArray = mNativeAudioThread.getPlayerBufferPeriod();
mNativePlayerMaxBufferPeriod = mNativeAudioThread.getPlayerMaxBufferPeriod();
+ mNativePlayerStdDevBufferPeriod =
+ mNativeAudioThread.getPlayerStdDevBufferPeriod();
mRecorderCallbackTimes = mNativeAudioThread.getRecorderCallbackTimes();
mPlayerCallbackTimes = mNativeAudioThread.getPlayerCallbackTimes();
@@ -370,7 +379,7 @@ public class LoopbackActivity extends Activity
}
- if (getApp().isCaptureSysTraceEnabled()) {
+ if (getApp().isCaptureEnabled()) {
CaptureHolder.stopLoopbackListenerScript();
}
refreshSoundLevelBar();
@@ -577,6 +586,11 @@ public class LoopbackActivity extends Activity
mIntentRunning = true;
}
+ if (b.containsKey(INTENT_IGNORE_FIRST_FRAMES)) {
+ getApp().setIgnoreFirstFrames(b.getInt(INTENT_IGNORE_FIRST_FRAMES));
+ mIntentRunning = true;
+ }
+
if (b.containsKey(INTENT_AUDIO_LEVEL)) {
int audioLevel = b.getInt(INTENT_AUDIO_LEVEL);
if (audioLevel >= 0) {
@@ -749,6 +763,7 @@ public class LoopbackActivity extends Activity
mTestStartTimeString = (String) DateFormat.format("MMddkkmmss",
System.currentTimeMillis());
mMicSource = getApp().getMicSource();
+ mIgnoreFirstFrames = getApp().getIgnoreFirstFrames();
AudioManager am = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
mSoundLevel = am.getStreamVolume(AudioManager.STREAM_MUSIC);
mBufferTestDurationInSeconds = getApp().getBufferTestDuration();
@@ -756,7 +771,8 @@ public class LoopbackActivity extends Activity
CaptureHolder captureHolder = new CaptureHolder(getApp().getNumStateCaptures(),
getFileNamePrefix(), getApp().isCaptureWavSnippetsEnabled(),
- getApp().isCaptureSysTraceEnabled(), this, mSamplingRate);
+ getApp().isCaptureSysTraceEnabled(), getApp().isCaptureBugreportEnabled(),
+ this, mSamplingRate);
log(" current sampling rate: " + mSamplingRate);
stopAudioTestThreads();
@@ -797,7 +813,7 @@ public class LoopbackActivity extends Activity
mNativeAudioThread = new NativeAudioThread(mSamplingRate, mPlayerBufferSizeInBytes,
mRecorderBufferSizeInBytes, micSourceMapped, mTestType,
mBufferTestDurationInSeconds, mBufferTestWavePlotDurationInSeconds,
- captureHolder);
+ mIgnoreFirstFrames, captureHolder);
mNativeAudioThread.setMessageHandler(mMessageHandler);
mNativeAudioThread.mSessionId = sessionId;
mNativeAudioThread.start();
@@ -998,7 +1014,7 @@ public class LoopbackActivity extends Activity
/** Stop the ongoing test. */
- public void stopTests() throws InterruptedException{
+ public void stopTests() throws InterruptedException {
if (mAudioThread != null) {
mAudioThread.requestStopTest();
}
@@ -1509,7 +1525,7 @@ public class LoopbackActivity extends Activity
s.append("):\n");
s.append("SR: " + mSamplingRate + " Hz");
- s.append(" ChannelIndex: " + mChannelIndex);
+ s.append(" ChannelIndex: " + (mChannelIndex < 0 ? "MONO" : mChannelIndex));
switch (mAudioThreadType) {
case Constant.AUDIO_THREAD_TYPE_JAVA:
s.append(" Play Frames: " + playerFrames);
@@ -1532,14 +1548,15 @@ public class LoopbackActivity extends Activity
s.append(String.format(" Sound Level: %d/%d", mSoundLevel,
mBarMasterLevel.getMax()));
- String info = getApp().getSystemInfo();
- s.append(" " + info);
-
// Show short summary of results, round trip latency or number of glitches
- if (mTestType == Constant.LOOPBACK_PLUG_AUDIO_THREAD_TEST_TYPE_LATENCY &&
- mCorrelation.isValid()) {
- mTextViewResultSummary.setText(String.format("Latency: %.2f ms Confidence: %.2f",
- mCorrelation.mEstimatedLatencyMs, mCorrelation.mEstimatedLatencyConfidence));
+ if (mTestType == Constant.LOOPBACK_PLUG_AUDIO_THREAD_TEST_TYPE_LATENCY) {
+ if (mIgnoreFirstFrames > 0) {
+ s.append(" First Frames Ignored: " + mIgnoreFirstFrames);
+ }
+ if (mCorrelation.isValid()) {
+ mTextViewResultSummary.setText(String.format("Latency: %.2f ms Confidence: %.2f",
+ mCorrelation.mEstimatedLatencyMs, mCorrelation.mEstimatedLatencyConfidence));
+ }
} else if (mTestType == Constant.LOOPBACK_PLUG_AUDIO_THREAD_TEST_TYPE_BUFFER_PERIOD &&
mGlitchesData != null) {
// show buffer test duration
@@ -1555,6 +1572,9 @@ public class LoopbackActivity extends Activity
mTextViewResultSummary.setText("");
}
+ String info = getApp().getSystemInfo();
+ s.append(" " + info);
+
mTextInfo.setText(s.toString());
}
@@ -1807,6 +1827,7 @@ public class LoopbackActivity extends Activity
switch (mTestType) {
case Constant.LOOPBACK_PLUG_AUDIO_THREAD_TEST_TYPE_LATENCY:
+ sb.append(INTENT_IGNORE_FIRST_FRAMES + " = " + mIgnoreFirstFrames);
if (mCorrelation.isValid()) {
sb.append(String.format("LatencyMs = %.2f", mCorrelation.mEstimatedLatencyMs)
+ endline);
@@ -1823,14 +1844,17 @@ public class LoopbackActivity extends Activity
// report recorder results
int[] recorderBufferData = null;
int recorderBufferDataMax = 0;
+ double recorderBufferDataStdDev = 0.0;
switch (mAudioThreadType) {
case Constant.AUDIO_THREAD_TYPE_JAVA:
recorderBufferData = mRecorderBufferPeriod.getBufferPeriodArray();
recorderBufferDataMax = mRecorderBufferPeriod.getMaxBufferPeriod();
+ recorderBufferDataStdDev = mRecorderBufferPeriod.getStdDevBufferPeriod();
break;
case Constant.AUDIO_THREAD_TYPE_NATIVE:
recorderBufferData = mNativeRecorderBufferPeriodArray;
recorderBufferDataMax = mNativeRecorderMaxBufferPeriod;
+ recorderBufferDataStdDev = mNativeRecorderStdDevBufferPeriod;
break;
}
// report expected recorder buffer period
@@ -1851,6 +1875,10 @@ public class LoopbackActivity extends Activity
sb.append("Recorder Buffer Periods At Expected = " +
String.format("%.5f%%", recorderPercentAtExpected * 100) + endline);
+ sb.append("Recorder Buffer Period Std Dev = "
+ + String.format(Locale.US, "%.5f ms", recorderBufferDataStdDev)
+ + endline);
+
// output thousandths of a percent not at expected buffer period
sb.append("kth% Late Recorder Buffer Callbacks = "
+ String.format("%.5f", (1 - recorderPercentAtExpected) * 100000)
@@ -1864,14 +1892,17 @@ public class LoopbackActivity extends Activity
// report player results
int[] playerBufferData = null;
int playerBufferDataMax = 0;
+ double playerBufferDataStdDev = 0.0;
switch (mAudioThreadType) {
case Constant.AUDIO_THREAD_TYPE_JAVA:
playerBufferData = mPlayerBufferPeriod.getBufferPeriodArray();
playerBufferDataMax = mPlayerBufferPeriod.getMaxBufferPeriod();
+ playerBufferDataStdDev = mPlayerBufferPeriod.getStdDevBufferPeriod();
break;
case Constant.AUDIO_THREAD_TYPE_NATIVE:
playerBufferData = mNativePlayerBufferPeriodArray;
playerBufferDataMax = mNativePlayerMaxBufferPeriod;
+ playerBufferDataStdDev = mNativePlayerStdDevBufferPeriod;
break;
}
// report expected player buffer period
@@ -1891,6 +1922,10 @@ public class LoopbackActivity extends Activity
sb.append("Player Buffer Periods At Expected = "
+ String.format("%.5f%%", playerPercentAtExpected * 100) + endline);
+ sb.append("Player Buffer Period Std Dev = "
+ + String.format(Locale.US, "%.5f ms", playerBufferDataStdDev)
+ + endline);
+
// output thousandths of a percent not at expected buffer period
sb.append("kth% Late Player Buffer Callbacks = "
+ String.format("%.5f", (1 - playerPercentAtExpected) * 100000)
diff --git a/LoopbackApp/app/src/main/java/org/drrickorang/loopback/LoopbackApplication.java b/LoopbackApp/app/src/main/java/org/drrickorang/loopback/LoopbackApplication.java
index e779777..a0c4b61 100644
--- a/LoopbackApp/app/src/main/java/org/drrickorang/loopback/LoopbackApplication.java
+++ b/LoopbackApp/app/src/main/java/org/drrickorang/loopback/LoopbackApplication.java
@@ -44,10 +44,12 @@ public class LoopbackApplication extends Application {
private int mRecorderBuffSizeInBytes = 0; // for both native and java
private int mAudioThreadType = Constant.AUDIO_THREAD_TYPE_JAVA; //0:Java, 1:Native (JNI)
private int mMicSource = 3; //maps to MediaRecorder.AudioSource.VOICE_RECOGNITION;
+ private int mIgnoreFirstFrames = 0;
private int mBufferTestDurationInSeconds = 5;
private int mBufferTestWavePlotDurationInSeconds = 7;
private int mNumberOfLoadThreads = 4;
private boolean mCaptureSysTraceEnabled = false;
+ private boolean mCaptureBugreportEnabled = false;
private boolean mCaptureWavSnippetsEnabled = false;
private int mNumStateCaptures = Constant.DEFAULT_NUM_CAPTURES;
@@ -175,6 +177,13 @@ public class LoopbackApplication extends Application {
void setMicSource(int micSource) { mMicSource = micSource; }
+ int getIgnoreFirstFrames() {
+ return mIgnoreFirstFrames;
+ }
+
+ void setIgnoreFirstFrames(int ignoreFirstFrames) {
+ mIgnoreFirstFrames = ignoreFirstFrames;
+ }
int getPlayerBufferSizeInBytes() {
return mPlayerBufferSizeInBytes;
@@ -238,14 +247,26 @@ public class LoopbackApplication extends Application {
mCaptureSysTraceEnabled = enabled;
}
+ public void setCaptureBugreportEnabled(boolean enabled) {
+ mCaptureBugreportEnabled = enabled;
+ }
+
public void setCaptureWavsEnabled (boolean enabled){
mCaptureWavSnippetsEnabled = enabled;
}
- public boolean isCaptureSysTraceEnabled () {
+ public boolean isCaptureEnabled() {
+ return isCaptureSysTraceEnabled() || isCaptureBugreportEnabled();
+ }
+
+ public boolean isCaptureSysTraceEnabled() {
return mCaptureSysTraceEnabled;
}
+ public boolean isCaptureBugreportEnabled() {
+ return mCaptureBugreportEnabled;
+ }
+
public int getNumStateCaptures() {
return mNumStateCaptures;
}
diff --git a/LoopbackApp/app/src/main/java/org/drrickorang/loopback/NativeAudioThread.java b/LoopbackApp/app/src/main/java/org/drrickorang/loopback/NativeAudioThread.java
index 48d9fdc..1bb2ae6 100644
--- a/LoopbackApp/app/src/main/java/org/drrickorang/loopback/NativeAudioThread.java
+++ b/LoopbackApp/app/src/main/java/org/drrickorang/loopback/NativeAudioThread.java
@@ -55,6 +55,7 @@ public class NativeAudioThread extends Thread {
private int mMinPlayerBufferSizeInBytes = 0;
private int mMinRecorderBuffSizeInBytes = 0; // currently not used
private int mMicSource;
+ private int mIgnoreFirstFrames;
private boolean mIsRequestStop = false;
private Handler mMessageHandler;
@@ -64,8 +65,10 @@ public class NativeAudioThread extends Thread {
// for buffer test
private int[] mRecorderBufferPeriod;
private int mRecorderMaxBufferPeriod;
+ private double mRecorderStdDevBufferPeriod;
private int[] mPlayerBufferPeriod;
private int mPlayerMaxBufferPeriod;
+ private double mPlayerStdDevBufferPeriod;
private BufferCallbackTimes mPlayerCallbackTimes;
private BufferCallbackTimes mRecorderCallbackTimes;
private int mBufferTestWavePlotDurationInSeconds;
@@ -84,7 +87,8 @@ public class NativeAudioThread extends Thread {
public NativeAudioThread(int samplingRate, int playerBufferInBytes, int recorderBufferInBytes,
int micSource, int testType, int bufferTestDurationInSeconds,
- int bufferTestWavePlotDurationInSeconds, CaptureHolder captureHolder) {
+ int bufferTestWavePlotDurationInSeconds, int ignoreFirstFrames,
+ CaptureHolder captureHolder) {
mSamplingRate = samplingRate;
mMinPlayerBufferSizeInBytes = playerBufferInBytes;
mMinRecorderBuffSizeInBytes = recorderBufferInBytes;
@@ -92,6 +96,7 @@ public class NativeAudioThread extends Thread {
mTestType = testType;
mBufferTestDurationInSeconds = bufferTestDurationInSeconds;
mBufferTestWavePlotDurationInSeconds = bufferTestWavePlotDurationInSeconds;
+ mIgnoreFirstFrames = ignoreFirstFrames;
mCaptureHolder = captureHolder;
setName("Loopback_NativeAudio");
}
@@ -113,18 +118,22 @@ public class NativeAudioThread extends Thread {
public native long slesInit(int samplingRate, int frameCount, int micSource,
int testType, double frequency1, ByteBuffer byteBuffer,
short[] sincTone, int maxRecordedLateCallbacks,
- CaptureHolder captureHolder);
+ int ignoreFirstFrames);
public native int slesProcessNext(long sles_data, double[] samples, long offset);
public native int slesDestroy(long sles_data);
// to get buffer period data
- public native int[] slesGetRecorderBufferPeriod(long sles_data);
- public native int slesGetRecorderMaxBufferPeriod(long sles_data);
- public native int[] slesGetPlayerBufferPeriod(long sles_data);
- public native int slesGetPlayerMaxBufferPeriod(long sles_data);
+ public native int[] slesGetRecorderBufferPeriod(long sles_data);
+ public native int slesGetRecorderMaxBufferPeriod(long sles_data);
+ public native double slesGetRecorderVarianceBufferPeriod(long sles_data);
+ public native int[] slesGetPlayerBufferPeriod(long sles_data);
+ public native int slesGetPlayerMaxBufferPeriod(long sles_data);
+ public native double slesGetPlayerVarianceBufferPeriod(long sles_data);
public native BufferCallbackTimes slesGetPlayerCallbackTimeStamps(long sles_data);
public native BufferCallbackTimes slesGetRecorderCallbackTimeStamps(long sles_data);
+ public native int slesGetCaptureRank(long sles_data);
+
public void run() {
setPriority(Thread.MAX_PRIORITY);
@@ -167,7 +176,7 @@ public class NativeAudioThread extends Thread {
mMinPlayerBufferSizeInBytes / Constant.BYTES_PER_FRAME, mMicSource, mTestType,
mFrequency1, mPipeByteBuffer.getByteBuffer(), loopbackTone,
mBufferTestDurationInSeconds * Constant.MAX_RECORDED_LATE_CALLBACKS_PER_SECOND,
- mCaptureHolder);
+ mIgnoreFirstFrames);
log(String.format("sles_data = 0x%X", sles_data));
if (sles_data == 0) {
@@ -228,6 +237,11 @@ public class NativeAudioThread extends Thread {
if (mIsRequestStop) {
break;
} else {
+ int rank = slesGetCaptureRank(sles_data);
+ if (rank > 0) {
+ //log("Late callback detected");
+ mCaptureHolder.captureState(rank);
+ }
try {
final int setUpTime = 100;
sleep(setUpTime); //just to let it start properly
@@ -246,8 +260,10 @@ public class NativeAudioThread extends Thread {
// collect buffer period data
mRecorderBufferPeriod = slesGetRecorderBufferPeriod(sles_data);
mRecorderMaxBufferPeriod = slesGetRecorderMaxBufferPeriod(sles_data);
+ mRecorderStdDevBufferPeriod = Math.sqrt(slesGetRecorderVarianceBufferPeriod(sles_data));
mPlayerBufferPeriod = slesGetPlayerBufferPeriod(sles_data);
mPlayerMaxBufferPeriod = slesGetPlayerMaxBufferPeriod(sles_data);
+ mPlayerStdDevBufferPeriod = Math.sqrt(slesGetPlayerVarianceBufferPeriod(sles_data));
mPlayerCallbackTimes = slesGetPlayerCallbackTimeStamps(sles_data);
mRecorderCallbackTimes = slesGetRecorderCallbackTimeStamps(sles_data);
@@ -427,21 +443,25 @@ public class NativeAudioThread extends Thread {
return mRecorderBufferPeriod;
}
-
public int getRecorderMaxBufferPeriod() {
return mRecorderMaxBufferPeriod;
}
+ public double getRecorderStdDevBufferPeriod() {
+ return mRecorderStdDevBufferPeriod;
+ }
public int[] getPlayerBufferPeriod() {
return mPlayerBufferPeriod;
}
-
public int getPlayerMaxBufferPeriod() {
return mPlayerMaxBufferPeriod;
}
+ public double getPlayerStdDevBufferPeriod() {
+ return mPlayerStdDevBufferPeriod;
+ }
public int[] getNativeAllGlitches() {
return mAllGlitches;
diff --git a/LoopbackApp/app/src/main/java/org/drrickorang/loopback/PerformanceMeasurement.java b/LoopbackApp/app/src/main/java/org/drrickorang/loopback/PerformanceMeasurement.java
index bd16707..35c5e18 100644
--- a/LoopbackApp/app/src/main/java/org/drrickorang/loopback/PerformanceMeasurement.java
+++ b/LoopbackApp/app/src/main/java/org/drrickorang/loopback/PerformanceMeasurement.java
@@ -107,17 +107,16 @@ public class PerformanceMeasurement {
/**
* Determine percent of Buffer Period Callbacks that occurred at the expected time
- * Note: due to current rounding in buffer sampling callbacks occurring at 1 ms after the
- * expected buffer period are also counted in the returned percentage
* Returns a value between 0 and 1
*/
public float percentBufferPeriodsAtExpected() {
int occurrenceNearExpectedBufferPeriod = 0;
- // indicate how many beams around mExpectedBufferPeriod do we want to add to the count
- int numberOfBeams = 2;
- int start = Math.max(0, mExpectedBufferPeriodMs - numberOfBeams);
- int end = Math.min(mBufferData.length, mExpectedBufferPeriodMs + numberOfBeams);
- for (int i = start; i < end; i++) {
+ // indicate how many buckets around mExpectedBufferPeriod do we want to add to the count
+ int acceptableOffset = 2;
+ int start = Math.max(0, mExpectedBufferPeriodMs - acceptableOffset);
+ int end = Math.min(mBufferData.length - 1, mExpectedBufferPeriodMs + acceptableOffset);
+ // include the next bucket too because the period is rounded up
+ for (int i = start; i <= end; i++) {
occurrenceNearExpectedBufferPeriod += mBufferData[i];
}
return ((float) occurrenceNearExpectedBufferPeriod) / mTotalOccurrence;
diff --git a/LoopbackApp/app/src/main/java/org/drrickorang/loopback/SettingsActivity.java b/LoopbackApp/app/src/main/java/org/drrickorang/loopback/SettingsActivity.java
index 8b5a2d7..9224abf 100644
--- a/LoopbackApp/app/src/main/java/org/drrickorang/loopback/SettingsActivity.java
+++ b/LoopbackApp/app/src/main/java/org/drrickorang/loopback/SettingsActivity.java
@@ -23,12 +23,12 @@ import android.util.Log;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemSelectedListener;
+import android.widget.ArrayAdapter;
import android.widget.CompoundButton;
import android.widget.PopupWindow;
import android.widget.Spinner;
-import android.widget.ArrayAdapter;
-import android.widget.AdapterView.OnItemSelectedListener;
-import android.widget.AdapterView;
import android.widget.TextView;
import android.widget.ToggleButton;
@@ -53,7 +53,9 @@ public class SettingsActivity extends Activity implements OnItemSelectedListener
private SettingsPicker mWavePlotDurationUI;
private SettingsPicker mLoadThreadUI;
private SettingsPicker mNumCapturesUI;
+ private SettingsPicker mIgnoreFirstFramesUI;
private ToggleButton mSystraceToggleButton;
+ private ToggleButton mBugreportToggleButton;
private ToggleButton mWavCaptureToggleButton;
ArrayAdapter<CharSequence> mAdapterSamplingRate;
@@ -220,10 +222,29 @@ public class SettingsActivity extends Activity implements OnItemSelectedListener
mWavCaptureToggleButton.setChecked(getApp().isCaptureWavSnippetsEnabled());
mWavCaptureToggleButton.setOnCheckedChangeListener(this);
+ mBugreportToggleButton = (ToggleButton) findViewById(R.id.BugreportEnabledToggle);
+ mBugreportToggleButton.setChecked(getApp().isCaptureBugreportEnabled());
+ mBugreportToggleButton.setOnCheckedChangeListener(this);
+
mSystraceToggleButton = (ToggleButton) findViewById(R.id.SystraceEnabledToggle);
mSystraceToggleButton.setChecked(getApp().isCaptureSysTraceEnabled());
mSystraceToggleButton.setOnCheckedChangeListener(this);
+ // Settings Picker for number of frames to ignore at the beginning
+ mIgnoreFirstFramesUI = (SettingsPicker) findViewById(R.id.ignoreFirstFramesSettingPicker);
+ mIgnoreFirstFramesUI.setMinMaxDefault(Constant.MIN_IGNORE_FIRST_FRAMES,
+ Constant.MAX_IGNORE_FIRST_FRAMES, getApp().getIgnoreFirstFrames());
+ mIgnoreFirstFramesUI.setTitle(getResources().getString(R.string.labelIgnoreFirstFrames,
+ Constant.MAX_IGNORE_FIRST_FRAMES));
+ mIgnoreFirstFramesUI.setSettingsChangeListener(new SettingsPicker.SettingChangeListener() {
+ @Override
+ public void settingChanged(int frames) {
+ log("new number of first frames to ignore: " + frames);
+ getApp().setIgnoreFirstFrames(frames);
+ setSettingsHaveChanged();
+ }
+ });
+
refresh();
}
@@ -266,7 +287,7 @@ public class SettingsActivity extends Activity implements OnItemSelectedListener
mSpinnerChannelIndex.setEnabled(false);
}
- mNumCapturesUI.setEnabled(getApp().isCaptureSysTraceEnabled() ||
+ mNumCapturesUI.setEnabled(getApp().isCaptureEnabled() ||
getApp().isCaptureWavSnippetsEnabled());
String info = getApp().getSystemInfo();
@@ -319,8 +340,10 @@ public class SettingsActivity extends Activity implements OnItemSelectedListener
getApp().setCaptureWavsEnabled(isChecked);
} else if (buttonView.getId() == mSystraceToggleButton.getId()) {
getApp().setCaptureSysTraceEnabled(isChecked);
+ } else if (buttonView.getId() == mBugreportToggleButton.getId()) {
+ getApp().setCaptureBugreportEnabled(isChecked);
}
- mNumCapturesUI.setEnabled(getApp().isCaptureSysTraceEnabled() ||
+ mNumCapturesUI.setEnabled(getApp().isCaptureEnabled() ||
getApp().isCaptureWavSnippetsEnabled());
}
diff --git a/LoopbackApp/app/src/main/jni/audio_utils/atomic.c b/LoopbackApp/app/src/main/jni/audio_utils/atomic.c
index a22ed15..b76b1f4 100644
--- a/LoopbackApp/app/src/main/jni/audio_utils/atomic.c
+++ b/LoopbackApp/app/src/main/jni/audio_utils/atomic.c
@@ -17,6 +17,7 @@
#include "atomic.h"
#include <stdatomic.h>
+#include <stdbool.h>
int32_t android_atomic_acquire_load(volatile const int32_t* addr) {
volatile atomic_int_least32_t* a = (volatile atomic_int_least32_t*) addr;
@@ -27,3 +28,13 @@ void android_atomic_release_store(int32_t value, volatile int32_t* addr) {
volatile atomic_int_least32_t* a = (volatile atomic_int_least32_t*) addr;
atomic_store_explicit(a, value, memory_order_release);
}
+
+int32_t android_atomic_exchange(int32_t value, volatile const int32_t* addr) {
+ volatile atomic_int_least32_t* a = (volatile atomic_int_least32_t*) addr;
+ return atomic_exchange(a, value);
+}
+
+bool android_atomic_compare_exchange(int32_t* expect, int32_t desire, volatile const int32_t* addr) {
+ volatile atomic_int_least32_t* a = (volatile atomic_int_least32_t*) addr;
+ return atomic_compare_exchange_weak(a, expect, desire);
+}
diff --git a/LoopbackApp/app/src/main/jni/audio_utils/atomic.h b/LoopbackApp/app/src/main/jni/audio_utils/atomic.h
index 535c926..164ad17 100644
--- a/LoopbackApp/app/src/main/jni/audio_utils/atomic.h
+++ b/LoopbackApp/app/src/main/jni/audio_utils/atomic.h
@@ -18,6 +18,7 @@
#define ANDROID_AUDIO_ATOMIC_H
#include <stdlib.h>
+#include <stdbool.h>
#ifdef __cplusplus
extern "C" {
@@ -26,6 +27,10 @@ extern "C" {
int32_t android_atomic_acquire_load(volatile const int32_t* addr);
void android_atomic_release_store(int32_t value, volatile int32_t* addr);
+// FIXME: use standard atomic library instead of these functions
+int32_t android_atomic_exchange(int32_t value, volatile const int32_t* addr);
+bool android_atomic_compare_exchange(int32_t* expect, int32_t desire, volatile const int32_t* addr);
+
#ifdef __cplusplus
}
#endif
diff --git a/LoopbackApp/app/src/main/jni/jni_sles.c b/LoopbackApp/app/src/main/jni/jni_sles.c
index 72c0884..f630086 100644
--- a/LoopbackApp/app/src/main/jni/jni_sles.c
+++ b/LoopbackApp/app/src/main/jni/jni_sles.c
@@ -18,13 +18,12 @@
#include "sles.h"
#include "jni_sles.h"
#include <stdio.h>
-#include <stddef.h>
JNIEXPORT jlong JNICALL Java_org_drrickorang_loopback_NativeAudioThread_slesInit
- (JNIEnv *env __unused, jobject obj __unused, jint samplingRate, jint frameCount, jint micSource,
+ (JNIEnv *env, jobject obj __unused, jint samplingRate, jint frameCount, jint micSource,
jint testType, jdouble frequency1, jobject byteBuffer, jshortArray loopbackTone,
- jint maxRecordedLateCallbacks, jobject captureHolder) {
+ jint maxRecordedLateCallbacks, jint ignoreFirstFrames) {
sles_data * pSles = NULL;
@@ -33,17 +32,9 @@ JNIEXPORT jlong JNICALL Java_org_drrickorang_loopback_NativeAudioThread_slesInit
short* loopbackToneArray = (*env)->GetShortArrayElements(env, loopbackTone, 0);
- const struct JNIInvokeInterface* *jvm;
- jint result = (*env)->GetJavaVM(env, &jvm);
- if (result != JNI_OK){
- jvm = NULL;
- __android_log_print(ANDROID_LOG_INFO, "CAPTURE", "failed to get vm");
- }
- captureHolder = (*env)->NewGlobalRef(env,captureHolder);
-
if (slesInit(&pSles, samplingRate, frameCount, micSource,
testType, frequency1, byteBufferPtr, byteBufferLength,
- loopbackToneArray, maxRecordedLateCallbacks, captureHolder, jvm) != SLES_FAIL) {
+ loopbackToneArray, maxRecordedLateCallbacks, ignoreFirstFrames) != SLES_FAIL) {
return (long) pSles;
}
@@ -103,6 +94,17 @@ JNIEXPORT jint JNICALL
}
+JNIEXPORT jdouble JNICALL
+ Java_org_drrickorang_loopback_NativeAudioThread_slesGetRecorderVarianceBufferPeriod
+ (JNIEnv *env __unused, jobject obj __unused, jlong sles) {
+ sles_data *pSles = (sles_data *) (size_t) sles;
+ int64_t result = slesGetRecorderVarianceBufferPeriod(pSles);
+ // variance has units ns^2 so we have to square the conversion factor
+ double scaled = (double) result / ((double) NANOS_PER_MILLI * (double) NANOS_PER_MILLI);
+ return scaled;
+}
+
+
JNIEXPORT jintArray
JNICALL Java_org_drrickorang_loopback_NativeAudioThread_slesGetPlayerBufferPeriod
(JNIEnv *env __unused, jobject obj __unused, jlong sles) {
@@ -125,6 +127,18 @@ JNIEXPORT jint JNICALL
return playerMaxBufferPeriod;
}
+
+JNIEXPORT jdouble JNICALL
+Java_org_drrickorang_loopback_NativeAudioThread_slesGetPlayerVarianceBufferPeriod
+ (JNIEnv *env __unused, jobject obj __unused, jlong sles) {
+ sles_data *pSles = (sles_data *) (size_t) sles;
+ int64_t result = slesGetPlayerVarianceBufferPeriod(pSles);
+ // variance has units ns^2 so we have to square the conversion factor
+ double scaled = (double) result / ((double) NANOS_PER_MILLI * (double) NANOS_PER_MILLI);
+ return scaled;
+}
+
+
jobject getCallbackTimes(JNIEnv *env, callbackTimeStamps *callbacks, short expectedBufferPeriod){
jintArray timeStamps = (*env)->NewIntArray(env, callbacks->index);
(*env)->SetIntArrayRegion(env, timeStamps, 0, callbacks->index, callbacks->timeStampsMs);
@@ -153,3 +167,10 @@ JNICALL Java_org_drrickorang_loopback_NativeAudioThread_slesGetRecorderCallbackT
sles_data * pSles = (sles_data*) (size_t) sles;
return getCallbackTimes(env, &(pSles->recorderTimeStamps), pSles->expectedBufferPeriod);
}
+
+JNIEXPORT jint
+JNICALL Java_org_drrickorang_loopback_NativeAudioThread_slesGetCaptureRank
+ (JNIEnv *env __unused, jobject obj __unused, jlong sles) {
+ sles_data * pSles = (sles_data*) (size_t) sles;
+ return slesGetCaptureRank(pSles);
+} \ No newline at end of file
diff --git a/LoopbackApp/app/src/main/jni/jni_sles.h b/LoopbackApp/app/src/main/jni/jni_sles.h
index 94b6a22..ee0f74d 100644
--- a/LoopbackApp/app/src/main/jni/jni_sles.h
+++ b/LoopbackApp/app/src/main/jni/jni_sles.h
@@ -27,7 +27,7 @@ extern "C" {
////SLE
JNIEXPORT jlong JNICALL Java_org_drrickorang_loopback_NativeAudioThread_slesInit
(JNIEnv *, jobject, jint, jint, jint, jint, jdouble, jobject byteBuffer,
- jshortArray loopbackTone, jint maxRecordedLateCallbacks, jobject captureHolder);
+ jshortArray loopbackTone, jint maxRecordedLateCallbacks, jint ignoreFirstFrames);
JNIEXPORT jint JNICALL Java_org_drrickorang_loopback_NativeAudioThread_slesProcessNext
(JNIEnv *, jobject, jlong, jdoubleArray, jlong);
@@ -43,11 +43,24 @@ JNIEXPORT jint JNICALL
Java_org_drrickorang_loopback_NativeAudioThread_slesGetRecorderMaxBufferPeriod
(JNIEnv *, jobject, jlong);
+JNIEXPORT jdouble JNICALL
+ Java_org_drrickorang_loopback_NativeAudioThread_slesGetRecorderVarianceBufferPeriod
+ (JNIEnv *, jobject, jlong);
+
JNIEXPORT jintArray JNICALL
Java_org_drrickorang_loopback_NativeAudioThread_slesGetPlayerBufferPeriod
(JNIEnv *, jobject, jlong);
-JNIEXPORT jint JNICALL Java_org_drrickorang_loopback_NativeAudioThread_slesGetPlayerMaxBufferPeriod
+JNIEXPORT jint JNICALL
+ Java_org_drrickorang_loopback_NativeAudioThread_slesGetPlayerMaxBufferPeriod
+ (JNIEnv *, jobject, jlong);
+
+JNIEXPORT jdouble JNICALL
+ Java_org_drrickorang_loopback_NativeAudioThread_slesGetPlayerVarianceBufferPeriod
+ (JNIEnv *, jobject, jlong);
+
+JNIEXPORT jint JNICALL
+ Java_org_drrickorang_loopback_NativeAudioThread_slesGetCaptureRank
(JNIEnv *, jobject, jlong);
#ifdef __cplusplus
diff --git a/LoopbackApp/app/src/main/jni/sles.cpp b/LoopbackApp/app/src/main/jni/sles.cpp
index 9b822a6..b49e3c8 100644
--- a/LoopbackApp/app/src/main/jni/sles.cpp
+++ b/LoopbackApp/app/src/main/jni/sles.cpp
@@ -27,15 +27,14 @@
#define _USE_MATH_DEFINES
#include <cmath>
#include "sles.h"
+#include "audio_utils/atomic.h"
#include <stdio.h>
#include <assert.h>
#include <unistd.h>
-#include <string.h>
int slesInit(sles_data ** ppSles, int samplingRate, int frameCount, int micSource,
int testType, double frequency1, char* byteBufferPtr, int byteBufferLength,
- short* loopbackTone, int maxRecordedLateCallbacks, jobject captureHolder,
- const struct JNIInvokeInterface* *jvm) {
+ short* loopbackTone, int maxRecordedLateCallbacks, int ignoreFirstFrames) {
int status = SLES_FAIL;
if (ppSles != NULL) {
sles_data * pSles = (sles_data*) malloc(sizeof(sles_data));
@@ -52,7 +51,7 @@ int slesInit(sles_data ** ppSles, int samplingRate, int frameCount, int micSourc
samplingRate, frameCount);
status = slesCreateServer(pSles, samplingRate, frameCount, micSource, testType,
frequency1, byteBufferPtr, byteBufferLength, loopbackTone,
- maxRecordedLateCallbacks, captureHolder, jvm);
+ maxRecordedLateCallbacks, ignoreFirstFrames);
SLES_PRINTF("slesCreateServer =%d", status);
}
}
@@ -77,12 +76,12 @@ int slesDestroy(sles_data ** ppSles) {
#define ASSERT_EQ(x, y) do { if ((x) == (y)) ; else { fprintf(stderr, "0x%x != 0x%x\n", \
(unsigned) (x), (unsigned) (y)); assert((x) == (y)); } } while (0)
-
// Called after audio recorder fills a buffer with data, then we can read from this filled buffer
static void recorderCallback(SLAndroidSimpleBufferQueueItf caller __unused, void *context) {
sles_data *pSles = (sles_data*) context;
if (pSles != NULL) {
- collectRecorderBufferPeriod(pSles);
+ collectBufferPeriod(&pSles->recorderBufferStats, NULL /*fdpStats*/, &pSles->recorderTimeStamps,
+ pSles->expectedBufferPeriod);
//__android_log_print(ANDROID_LOG_INFO, "sles_jni", "in recorderCallback");
SLresult result;
@@ -102,6 +101,16 @@ static void recorderCallback(SLAndroidSimpleBufferQueueItf caller __unused, void
}
if (pSles->testType == TEST_TYPE_LATENCY) {
+ // Throw out first frames
+ if (pSles->ignoreFirstFrames) {
+ int framesToErase = pSles->ignoreFirstFrames;
+ if (framesToErase > (int) pSles->bufSizeInFrames) {
+ framesToErase = pSles->bufSizeInFrames;
+ }
+ pSles->ignoreFirstFrames -= framesToErase;
+ memset(buffer, 0, framesToErase * pSles->channels * sizeof(short));
+ }
+
ssize_t actual = audio_utils_fifo_write(&(pSles->fifo), buffer,
(size_t) pSles->bufSizeInFrames);
@@ -194,108 +203,19 @@ ssize_t byteBuffer_write(sles_data *pSles, char *buffer, size_t count) {
return count;
}
-// Calculate millisecond difference between two timespec structs from clock_gettime(CLOCK_MONOTONIC)
+// Calculate nanosecond difference between two timespec structs from clock_gettime(CLOCK_MONOTONIC)
// tv_sec [0, max time_t] , tv_nsec [0, 999999999]
-int diffInMilli(struct timespec previousTime, struct timespec currentTime) {
- int diff_in_second = currentTime.tv_sec - previousTime.tv_sec;
- long diff_in_nano = currentTime.tv_nsec - previousTime.tv_nsec;
-
- // diff_in_milli is rounded up
- uint64_t total_diff_in_nano = (diff_in_second * (uint64_t) NANOS_PER_SECOND) + diff_in_nano;
- int diff_in_milli = (int) ((total_diff_in_nano + NANOS_PER_MILLI - 1) / NANOS_PER_MILLI);
-
- return diff_in_milli;
-}
-
-
-// Request CaptureHolder object to capture a systrace/bugreport and/or wav snippet
-// Uses cached JavaVM and Jobject to call a method on instantiated CaptureHolder object
-void captureState(sles_data *pSles, int rank) {
- // Retrieve JNIEnv from cached JavaVM
- if (pSles->jvm == NULL) {
- __android_log_print(ANDROID_LOG_DEBUG, "CAPTURE", "Java Virtual Machine unavailable");
- return;
- }
- C_JNIEnv *env;
- jint result = (*(pSles->jvm))->AttachCurrentThread((JavaVM *) pSles->jvm, (JNIEnv **) &env,
- NULL);
- if (result == JNI_OK && env != NULL) {
-
- // Get object class and method via reflection
- jclass captureHolderClass = (*env)->GetObjectClass((JNIEnv *) env, pSles->captureHolder);
- jmethodID javaCaptureMethod =
- (*env)->GetMethodID((JNIEnv *) env, captureHolderClass, "captureState", "(I)I");
-
- // Call CaptureHolder.captureState instance method
- int captureResult = (*env)->CallIntMethod((JNIEnv *) env, pSles->captureHolder,
- javaCaptureMethod, rank);
- __android_log_print(ANDROID_LOG_DEBUG, "CAPTURE",
- "instigated state capture from native SLES result: %d",captureResult);
-
- // Release local ref and attached thread
- (*env)->DeleteLocalRef((JNIEnv *) env, captureHolderClass);
- (*(pSles->jvm))->DetachCurrentThread((JavaVM *) pSles->jvm);
- } else {
- __android_log_print(ANDROID_LOG_DEBUG, "CAPTURE", "state capture from native SLES failed");
- }
+int64_t diffInNano(struct timespec previousTime, struct timespec currentTime) {
+ return (int64_t) (currentTime.tv_sec - previousTime.tv_sec) * (int64_t) NANOS_PER_SECOND +
+ currentTime.tv_nsec - previousTime.tv_nsec;
}
-// Called in the beginning of recorderCallback() to collect the interval between each
-// recorderCallback().
-void collectRecorderBufferPeriod(sles_data *pSles) {
- clock_gettime(CLOCK_MONOTONIC, &(pSles->recorder_current_time));
-
- if (pSles->recorder_buffer_count == 0){
- pSles->recorderTimeStamps.startTime = pSles->recorder_current_time;
- }
-
- (pSles->recorder_buffer_count)++;
-
- if (pSles->recorder_previous_time.tv_sec != 0 &&
- pSles->recorder_buffer_count > BUFFER_PERIOD_DISCARD){
- int diff_in_milli = diffInMilli(pSles->recorder_previous_time,
- pSles->recorder_current_time);
-
- if (diff_in_milli > pSles->recorder_max_buffer_period) {
- pSles->recorder_max_buffer_period = diff_in_milli;
- }
-
- // from 0 ms to 1000 ms, plus a sum of all instances > 1000ms
- if (diff_in_milli >= (RANGE - 1)) {
- (pSles->recorder_buffer_period)[RANGE-1]++;
- } else if (diff_in_milli >= 0) {
- (pSles->recorder_buffer_period)[diff_in_milli]++;
- } else { // for diff_in_milli < 0
- __android_log_print(ANDROID_LOG_INFO, "sles_recorder", "Having negative BufferPeriod.");
- }
-
- //recording timestamps of buffer periods not at expected buffer period
- if (!pSles->recorderTimeStamps.exceededCapacity
- && diff_in_milli != pSles->expectedBufferPeriod
- && diff_in_milli != pSles->expectedBufferPeriod+1) {
- //only marked as exceeded if attempting to record a late callback after arrays full
- if (pSles->recorderTimeStamps.index == pSles->recorderTimeStamps.capacity){
- pSles->recorderTimeStamps.exceededCapacity = true;
- } else {
- pSles->recorderTimeStamps.callbackDurations[pSles->recorderTimeStamps.index] =
- (short) diff_in_milli;
- pSles->recorderTimeStamps.timeStampsMs[pSles->recorderTimeStamps.index] =
- diffInMilli(pSles->recorderTimeStamps.startTime,
- pSles->recorder_current_time);
- pSles->recorderTimeStamps.index++;
- }
- }
- }
-
- pSles->recorder_previous_time = pSles->recorder_current_time;
-}
-
-
// Called after audio player empties a buffer of data
static void playerCallback(SLBufferQueueItf caller __unused, void *context) {
sles_data *pSles = (sles_data*) context;
if (pSles != NULL) {
- collectPlayerBufferPeriod(pSles);
+ collectBufferPeriod(&pSles->playerBufferStats, &pSles->recorderBufferStats /*fdpStats*/,
+ &pSles->playerTimeStamps, pSles->expectedBufferPeriod);
SLresult result;
//ee SLES_PRINTF("<P");
@@ -387,60 +307,118 @@ static void playerCallback(SLBufferQueueItf caller __unused, void *context) {
} //pSles not null
}
-// Called in the beginning of playerCallback() to collect the interval between each
-// playerCallback().
-void collectPlayerBufferPeriod(sles_data *pSles) {
- clock_gettime(CLOCK_MONOTONIC, &(pSles->player_current_time));
+// Used to set initial values for the bufferStats struct before values can be recorded.
+void initBufferStats(bufferStats *stats) {
+ stats->buffer_period = new int[RANGE](); // initialized to zeros
+ stats->previous_time = {0,0};
+ stats->current_time = {0,0};
- if (pSles->player_buffer_count == 0) {
- pSles->playerTimeStamps.startTime = pSles->player_current_time;
- }
+ stats->buffer_count = 0;
+ stats->max_buffer_period = 0;
- (pSles->player_buffer_count)++;
+ stats->measurement_count = 0;
+ stats->SDM = 0;
+ stats->var = 0;
+}
- if (pSles->player_previous_time.tv_sec != 0 &&
- pSles->player_buffer_count > BUFFER_PERIOD_DISCARD) {
+// Called in the beginning of playerCallback() to collect the interval between each callback.
+// fdpStats is either NULL or a pointer to the buffer statistics for the full-duplex partner.
+void collectBufferPeriod(bufferStats *stats, bufferStats *fdpStats, callbackTimeStamps *timeStamps,
+ short expectedBufferPeriod) {
+ clock_gettime(CLOCK_MONOTONIC, &(stats->current_time));
- int diff_in_milli = diffInMilli(pSles->player_previous_time, pSles->player_current_time);
+ if (timeStamps->startTime.tv_sec == 0 && timeStamps->startTime.tv_nsec == 0) {
+ timeStamps->startTime = stats->current_time;
+ }
- if (diff_in_milli > pSles->player_max_buffer_period) {
- pSles->player_max_buffer_period = diff_in_milli;
- }
+ (stats->buffer_count)++;
- // from 0 ms to 1000 ms, plus a sum of all instances > 1000ms
- if (diff_in_milli >= (RANGE - 1)) {
- (pSles->player_buffer_period)[RANGE-1]++;
- } else if (diff_in_milli >= 0) {
- (pSles->player_buffer_period)[diff_in_milli]++;
- } else { // for diff_in_milli < 0
- __android_log_print(ANDROID_LOG_INFO, "sles_player", "Having negative BufferPeriod.");
- }
+ if (stats->previous_time.tv_sec != 0 && stats->buffer_count > BUFFER_PERIOD_DISCARD &&
+ (fdpStats == NULL || fdpStats->buffer_count > BUFFER_PERIOD_DISCARD_FULL_DUPLEX_PARTNER)) {
+
+ int64_t callbackDuration = diffInNano(stats->previous_time, stats->current_time);
+
+ bool outlier = updateBufferStats(stats, callbackDuration, expectedBufferPeriod);
//recording timestamps of buffer periods not at expected buffer period
- if (!pSles->playerTimeStamps.exceededCapacity
- && diff_in_milli != pSles->expectedBufferPeriod
- && diff_in_milli != pSles->expectedBufferPeriod+1) {
- //only marked as exceeded if attempting to record a late callback after arrays full
- if (pSles->playerTimeStamps.index == pSles->playerTimeStamps.capacity){
- pSles->playerTimeStamps.exceededCapacity = true;
- } else {
- pSles->playerTimeStamps.callbackDurations[pSles->playerTimeStamps.index] =
- (short) diff_in_milli;
- pSles->playerTimeStamps.timeStampsMs[pSles->playerTimeStamps.index] =
- diffInMilli( pSles->playerTimeStamps.startTime, pSles->player_current_time);
- pSles->playerTimeStamps.index++;
- }
+ if (outlier) {
+ int64_t timeStamp = diffInNano(timeStamps->startTime, stats->current_time);
+ recordTimeStamp(timeStamps, callbackDuration, timeStamp);
}
}
- pSles->player_previous_time = pSles->player_current_time;
+ stats->previous_time = stats->current_time;
}
+// Records an outlier given the duration in nanoseconds and the number of nanoseconds
+// between it and the start of the test.
+void recordTimeStamp(callbackTimeStamps *timeStamps,
+ int64_t callbackDuration, int64_t timeStamp) {
+ if (timeStamps->exceededCapacity) {
+ return;
+ }
+
+ //only marked as exceeded if attempting to record a late callback after arrays full
+ if (timeStamps->index == timeStamps->capacity){
+ timeStamps->exceededCapacity = true;
+ } else {
+ timeStamps->callbackDurations[timeStamps->index] =
+ (short) ((callbackDuration + NANOS_PER_MILLI - 1) / NANOS_PER_MILLI);
+ timeStamps->timeStampsMs[timeStamps->index] =
+ (int) ((timeStamp + NANOS_PER_MILLI - 1) / NANOS_PER_MILLI);
+ timeStamps->index++;
+ }
+}
+
+void atomicSetIfGreater(volatile int32_t *addr, int32_t val) {
+ // TODO: rewrite this to avoid the need for unbounded spinning
+ int32_t old;
+ do {
+ old = *addr;
+ if (val < old) return;
+ } while(!android_atomic_compare_exchange(&old, val, addr));
+}
+
+// Updates the stats being collected about buffer periods. Returns true if this is an outlier.
+bool updateBufferStats(bufferStats *stats, int64_t diff_in_nano, int expectedBufferPeriod) {
+ stats->measurement_count++;
+
+ // round up to nearest millisecond
+ int diff_in_milli = (int) ((diff_in_nano + NANOS_PER_MILLI - 1) / NANOS_PER_MILLI);
+
+ if (diff_in_milli > stats->max_buffer_period) {
+ stats->max_buffer_period = diff_in_milli;
+ }
+
+ // from 0 ms to 1000 ms, plus a sum of all instances > 1000ms
+ if (diff_in_milli >= (RANGE - 1)) {
+ (stats->buffer_period)[RANGE-1]++;
+ } else if (diff_in_milli >= 0) {
+ (stats->buffer_period)[diff_in_milli]++;
+ } else { // for diff_in_milli < 0
+ __android_log_print(ANDROID_LOG_INFO, "sles_player", "Having negative BufferPeriod.");
+ }
+
+ int64_t delta = diff_in_nano - (int64_t) expectedBufferPeriod * NANOS_PER_MILLI;
+ stats->SDM += delta * delta;
+ if (stats->measurement_count > 1) {
+ stats->var = stats->SDM / stats->measurement_count;
+ }
+
+ // check if the lateness is so bad that a systrace should be captured
+ // TODO: replace static threshold of lateness with a dynamic determination
+ if (diff_in_milli > expectedBufferPeriod + LATE_CALLBACK_CAPTURE_THRESHOLD) {
+ // TODO: log in a non-blocking way
+ //__android_log_print(ANDROID_LOG_INFO, "buffer_stats", "Callback late by %d ms",
+ // diff_in_milli - expectedBufferPeriod);
+ atomicSetIfGreater(&(stats->captureRank), diff_in_milli - expectedBufferPeriod);
+ }
+ return diff_in_milli > expectedBufferPeriod + LATE_CALLBACK_OUTLIER_THRESHOLD;
+}
int slesCreateServer(sles_data *pSles, int samplingRate, int frameCount, int micSource,
int testType, double frequency1, char *byteBufferPtr, int byteBufferLength,
- short *loopbackTone, int maxRecordedLateCallbacks, jobject captureHolder,
- const struct JNIInvokeInterface* *jvm) {
+ short *loopbackTone, int maxRecordedLateCallbacks, int ignoreFirstFrames) {
int status = SLES_FAIL;
if (pSles != NULL) {
@@ -492,6 +470,7 @@ int slesCreateServer(sles_data *pSles, int samplingRate, int frameCount, int mic
pSles->freeBufCount = 0; // calculated
pSles->bufSizeInBytes = 0; // calculated
pSles->injectImpulse = 300; // -i#i
+ pSles->ignoreFirstFrames = ignoreFirstFrames;
// Storage area for the buffer queues
// char **rxBuffers;
@@ -563,19 +542,8 @@ int slesCreateServer(sles_data *pSles, int samplingRate, int frameCount, int mic
// sndfile = NULL;
// }
- //init recorder buffer period data
- pSles->recorder_buffer_period = new int[RANGE](); // initialized to zeros
- pSles->recorder_previous_time = {0,0};
- pSles->recorder_current_time = {0,0};
- pSles->recorder_buffer_count = 0;
- pSles->recorder_max_buffer_period = 0;
-
- //init player buffer period data
- pSles->player_buffer_period = new int[RANGE](); // initialized to zeros
- pSles->player_previous_time = {0,0};
- pSles->player_current_time = {0,0};
- pSles->player_buffer_count = 0;
- pSles->player_max_buffer_period = 0;
+ initBufferStats(&pSles->recorderBufferStats);
+ initBufferStats(&pSles->playerBufferStats);
// init other variables needed for buffer test
pSles->testType = testType;
@@ -609,9 +577,6 @@ int slesCreateServer(sles_data *pSles, int samplingRate, int frameCount, int mic
pSles->expectedBufferPeriod = (short) (
round(pSles->bufSizeInFrames * MILLIS_PER_SECOND / (float) pSles->sampleRate));
- pSles->captureHolder = captureHolder;
- pSles->jvm = jvm;
-
SLresult result;
// create engine
@@ -958,20 +923,37 @@ int slesDestroyServer(sles_data *pSles) {
int* slesGetRecorderBufferPeriod(sles_data *pSles) {
- return pSles->recorder_buffer_period;
+ return pSles->recorderBufferStats.buffer_period;
}
-
int slesGetRecorderMaxBufferPeriod(sles_data *pSles) {
- return pSles->recorder_max_buffer_period;
+ return pSles->recorderBufferStats.max_buffer_period;
}
+int64_t slesGetRecorderVarianceBufferPeriod(sles_data *pSles) {
+ return pSles->recorderBufferStats.var;
+}
int* slesGetPlayerBufferPeriod(sles_data *pSles) {
- return pSles->player_buffer_period;
+ return pSles->playerBufferStats.buffer_period;
}
-
int slesGetPlayerMaxBufferPeriod(sles_data *pSles) {
- return pSles->player_max_buffer_period;
+ return pSles->playerBufferStats.max_buffer_period;
+}
+
+int64_t slesGetPlayerVarianceBufferPeriod(sles_data *pSles) {
+ return pSles->playerBufferStats.var;
+}
+
+int slesGetCaptureRank(sles_data *pSles) {
+ // clear the capture flags since they're being handled now
+ int recorderRank = android_atomic_exchange(0, &pSles->recorderBufferStats.captureRank);
+ int playerRank = android_atomic_exchange(0, &pSles->playerBufferStats.captureRank);
+
+ if (recorderRank > playerRank) {
+ return recorderRank;
+ } else {
+ return playerRank;
+ }
}
diff --git a/LoopbackApp/app/src/main/jni/sles.h b/LoopbackApp/app/src/main/jni/sles.h
index ebf0342..b3b7e0e 100644
--- a/LoopbackApp/app/src/main/jni/sles.h
+++ b/LoopbackApp/app/src/main/jni/sles.h
@@ -42,6 +42,20 @@ typedef struct {
bool exceededCapacity; // Set only if late callbacks come after array is full
} callbackTimeStamps;
+typedef struct {
+ int* buffer_period;
+ struct timespec previous_time;
+ struct timespec current_time;
+ int buffer_count;
+ int max_buffer_period;
+
+ volatile int32_t captureRank; // Set > 0 when the callback requests a systrace/bug report
+
+ int measurement_count; // number of measurements which were actually recorded
+ int64_t SDM; // sum of squares of deviations from the expected mean
+ int64_t var; // variance in nanoseconds^2
+} bufferStats;
+
//TODO fix this
typedef struct {
SLuint32 rxBufCount; // -r#
@@ -53,6 +67,7 @@ typedef struct {
SLuint32 freeBufCount; // calculated
SLuint32 bufSizeInBytes; // calculated
int injectImpulse; // -i#i
+ int ignoreFirstFrames;
// Storage area for the buffer queues
char **rxBuffers;
@@ -80,17 +95,8 @@ typedef struct {
SLObjectItf outputmixObject;
SLObjectItf engineObject;
- int* recorder_buffer_period;
- struct timespec recorder_previous_time;
- struct timespec recorder_current_time;
- int recorder_buffer_count;
- int recorder_max_buffer_period;
-
- int* player_buffer_period;
- struct timespec player_previous_time;
- struct timespec player_current_time;
- int player_buffer_count;
- int player_max_buffer_period;
+ bufferStats recorderBufferStats;
+ bufferStats playerBufferStats;
int testType;
double frequency1;
@@ -104,27 +110,29 @@ typedef struct {
callbackTimeStamps recorderTimeStamps;
callbackTimeStamps playerTimeStamps;
short expectedBufferPeriod;
-
- jobject captureHolder;
- const struct JNIInvokeInterface* *jvm;
} sles_data;
+#define NANOS_PER_SECOND 1000000000
+#define NANOS_PER_MILLI 1000000
+#define MILLIS_PER_SECOND 1000
+
+// how late in ms a callback must be to trigger a systrace/bugreport
+#define LATE_CALLBACK_CAPTURE_THRESHOLD 4
+#define LATE_CALLBACK_OUTLIER_THRESHOLD 1
+#define BUFFER_PERIOD_DISCARD 10
+#define BUFFER_PERIOD_DISCARD_FULL_DUPLEX_PARTNER 2
+
enum {
SLES_SUCCESS = 0,
SLES_FAIL = 1,
- NANOS_PER_MILLI = 1000000,
- NANOS_PER_SECOND = 1000000000,
- MILLIS_PER_SECOND = 1000,
RANGE = 1002,
- BUFFER_PERIOD_DISCARD = 10,
TEST_TYPE_LATENCY = 222,
TEST_TYPE_BUFFER_PERIOD = 223
} SLES_STATUS_ENUM;
int slesInit(sles_data ** ppSles, int samplingRate, int frameCount, int micSource,
int testType, double frequency1, char* byteBufferPtr, int byteBufferLength,
- short* loopbackTone, int maxRecordedLateCallbacks, jobject captureHolder,
- const struct JNIInvokeInterface* *jvm);
+ short* loopbackTone, int maxRecordedLateCallbacks, int ignoreFirstFrames);
//note the double pointer to properly free the memory of the structure
int slesDestroy(sles_data ** ppSles);
@@ -135,18 +143,23 @@ int slesFull(sles_data *pSles);
int slesCreateServer(sles_data *pSles, int samplingRate, int frameCount, int micSource,
int testType, double frequency1, char* byteBufferPtr, int byteBufferLength,
- short* loopbackTone, int maxRecordedLateCallbacks, jobject captureHolder,
- const struct JNIInvokeInterface* *jvm);
+ short* loopbackTone, int maxRecordedLateCallbacks, int ignoreFirstFrames);
int slesProcessNext(sles_data *pSles, double *pSamples, long maxSamples);
int slesDestroyServer(sles_data *pSles);
int* slesGetRecorderBufferPeriod(sles_data *pSles);
int slesGetRecorderMaxBufferPeriod(sles_data *pSles);
+int64_t slesGetRecorderVarianceBufferPeriod(sles_data *pSles);
int* slesGetPlayerBufferPeriod(sles_data *pSles);
int slesGetPlayerMaxBufferPeriod(sles_data *pSles);
-
-void collectPlayerBufferPeriod(sles_data *pSles);
-void collectRecorderBufferPeriod(sles_data *pSles);
-void captureState(sles_data *pSles, int rank);
+int64_t slesGetPlayerVarianceBufferPeriod(sles_data *pSles);
+int slesGetCaptureRank(sles_data *pSles);
+
+void initBufferStats(bufferStats *stats);
+void collectBufferPeriod(bufferStats *stats, bufferStats *fdpStats, callbackTimeStamps *timeStamps,
+ short expectedBufferPeriod);
+bool updateBufferStats(bufferStats *stats, int64_t diff_in_nano, int expectedBufferPeriod);
+void recordTimeStamp(callbackTimeStamps *timeStamps,
+ int64_t callbackDuration, int64_t timeStamp);
ssize_t byteBuffer_write(sles_data *pSles, char *buffer, size_t count);
diff --git a/LoopbackApp/app/src/main/res/layout/settings_activity.xml b/LoopbackApp/app/src/main/res/layout/settings_activity.xml
index c76ce46..1ffbf84 100644
--- a/LoopbackApp/app/src/main/res/layout/settings_activity.xml
+++ b/LoopbackApp/app/src/main/res/layout/settings_activity.xml
@@ -223,6 +223,51 @@
android:layout_height="match_parent"
android:layout_weight="3">
<ToggleButton
+ android:id="@+id/BugreportEnabledToggle"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_marginRight="15dp"
+ android:background="@drawable/togglebutton_state_drawable"
+ android:textOn="Enabled"
+ android:textOff="Disabled"/>
+ </RelativeLayout>
+ <RelativeLayout
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="6">
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/enableBugreport"/>
+ </RelativeLayout>
+ <RelativeLayout
+ android:layout_width="0dip"
+ android:layout_height="match_parent"
+ android:layout_weight="1">
+ <ImageView
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:onClick="onButtonSysTraceHelp"
+ android:src="@drawable/ic_help_outline"/>
+ </RelativeLayout>
+ </LinearLayout>
+
+ <View
+ android:layout_width="fill_parent"
+ android:layout_height="1dp"
+ android:background="@android:color/darker_gray"/>
+
+ <LinearLayout
+ android:orientation="horizontal"
+ android:layout_width="match_parent"
+ android:layout_height="80dp"
+ android:padding="15dp">
+
+ <RelativeLayout
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="3">
+ <ToggleButton
android:id="@+id/wavSnippetsEnabledToggle"
android:layout_width="match_parent"
android:layout_height="match_parent"
@@ -258,6 +303,16 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
+ <View
+ android:layout_width="fill_parent"
+ android:layout_height="1dp"
+ android:background="@android:color/darker_gray"/>
+
+ <org.drrickorang.loopback.SettingsPicker
+ android:id="@+id/ignoreFirstFramesSettingPicker"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"/>
+
</LinearLayout>
</ScrollView>
</LinearLayout>
diff --git a/LoopbackApp/app/src/main/res/raw/loopback_listener b/LoopbackApp/app/src/main/res/raw/loopback_listener
index 22fdecd..bcbce2c 100644
--- a/LoopbackApp/app/src/main/res/raw/loopback_listener
+++ b/LoopbackApp/app/src/main/res/raw/loopback_listener
@@ -41,19 +41,27 @@ do
# Ensure that if more than one listener is running only one will consume signal
> $SIGNAL_FILE
- if [ $contents == $TERMINATE_SIGNAL ]
+ if [ "$contents" == $TERMINATE_SIGNAL ]
then
exitListener
else
- # write Systrace and bugreport to files
+ for filename in $contents
+ do
+ case $filename in
+ *$SYSTRACE_SUFFIX)
+ echo "LOOPBACK LISTENER: dumping systrace to file $filename"
+ atrace --async_dump -z -c -b $BUFFER_KB $TRACE_CATEGORIES > $filename
+ ;;
- echo "LOOPBACK LISTENER: dumping systrace to file $contents$SYSTRACE_SUFFIX"
- atrace --async_dump -z -c -b $BUFFER_KB $TRACE_CATEGORIES > $contents$SYSTRACE_SUFFIX
+ *$BUGREPORT_SUFFIX)
+ echo "LOOPBACK LISTENER: dumping bugreport to file $filename"
+ bugreport | gzip > $filename
+ ;;
- echo "LOOPBACK LISTENER: dumping bugreport to file $contents$BUGREPORT_SUFFIX"
- bugreport | gzip > $contents$BUGREPORT_SUFFIX
+ esac
+ done
- echo "LOOPBACK LISTENER: Finished systrace and bugreport"
+ echo "LOOPBACK LISTENER: Finished capture"
# Check for case that test has ended while capturing state and exit
if [ -e "$SIGNAL_FILE" ] && [ -s "$SIGNAL_FILE" ] \
diff --git a/LoopbackApp/app/src/main/res/values/strings.xml b/LoopbackApp/app/src/main/res/values/strings.xml
index c58469e..4c84d76 100644
--- a/LoopbackApp/app/src/main/res/values/strings.xml
+++ b/LoopbackApp/app/src/main/res/values/strings.xml
@@ -136,10 +136,13 @@
<string name="labelBufferTestWavePlotDuration">Buffer Test Wave Plot Duration (Seconds)
(Max: %1$d)</string>
<string name="loadThreadsLabel">Number of Simulated Load Threads</string>
- <string name="enableSystrace">Systrace and BugReport Captures During Test</string>
+ <string name="enableSystrace">Systrace Captures During Test</string>
+ <string name="enableBugreport">BugReport Captures During Test</string>
<string name="enableWavSnippets">Wav Snippet Captures During Test</string>
<string name="numCapturesSetting">Number of Systrace/BugReport and or Wav Snippets to Capture
</string>
+ <string name="labelIgnoreFirstFrames">
+ Frames to ignore at the start of the latency test (Max: %1$d)</string>
<string name="SaveFileDialogLabel">Save Files To:</string>
<string name="SaveFileDialogOK">//mnt/sdcard/</string>