summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGlenn Kasten <gkasten@google.com>2016-03-18 11:17:33 -0700
committerGlenn Kasten <gkasten@google.com>2016-03-18 11:17:33 -0700
commitc2485a223ff13085337865dd81ccda7063431203 (patch)
tree973d1b2a7d9e3f9e5b75ff229b4d225bcf525a25
parent0ea71f7b39277601f41dfd7a600433c56db2466e (diff)
downloaddrrickorang-c2485a223ff13085337865dd81ccda7063431203.tar.gz
Snap to commit d5cc4bf4ebe772b0de598f8e456ac7a33d41fde7
Version 10
-rw-r--r--LoopbackApp/app/app.iml8
-rw-r--r--LoopbackApp/app/src/main/AndroidManifest.xml4
-rw-r--r--LoopbackApp/app/src/main/java/org/drrickorang/loopback/AtraceScriptsWriter.java88
-rw-r--r--LoopbackApp/app/src/main/java/org/drrickorang/loopback/AudioFileOutput.java33
-rw-r--r--LoopbackApp/app/src/main/java/org/drrickorang/loopback/BufferCallbackTimes.java129
-rw-r--r--LoopbackApp/app/src/main/java/org/drrickorang/loopback/BufferPeriod.java26
-rw-r--r--LoopbackApp/app/src/main/java/org/drrickorang/loopback/CaptureHolder.java284
-rw-r--r--LoopbackApp/app/src/main/java/org/drrickorang/loopback/CatchEventsEditText.java75
-rw-r--r--LoopbackApp/app/src/main/java/org/drrickorang/loopback/Constant.java5
-rw-r--r--LoopbackApp/app/src/main/java/org/drrickorang/loopback/GlitchAndCallbackHeatMapView.java498
-rw-r--r--LoopbackApp/app/src/main/java/org/drrickorang/loopback/GlitchDetectionThread.java38
-rw-r--r--LoopbackApp/app/src/main/java/org/drrickorang/loopback/GlitchesStringBuilder.java17
-rw-r--r--LoopbackApp/app/src/main/java/org/drrickorang/loopback/HistogramView.java51
-rw-r--r--LoopbackApp/app/src/main/java/org/drrickorang/loopback/LoopbackActivity.java339
-rw-r--r--LoopbackApp/app/src/main/java/org/drrickorang/loopback/LoopbackApplication.java28
-rw-r--r--LoopbackApp/app/src/main/java/org/drrickorang/loopback/LoopbackAudioThread.java7
-rw-r--r--LoopbackApp/app/src/main/java/org/drrickorang/loopback/NativeAudioThread.java32
-rw-r--r--LoopbackApp/app/src/main/java/org/drrickorang/loopback/PerformanceMeasurement.java4
-rw-r--r--LoopbackApp/app/src/main/java/org/drrickorang/loopback/PlayerBufferPeriodActivity.java16
-rw-r--r--LoopbackApp/app/src/main/java/org/drrickorang/loopback/RecorderBufferPeriodActivity.java14
-rw-r--r--LoopbackApp/app/src/main/java/org/drrickorang/loopback/RecorderRunnable.java7
-rw-r--r--LoopbackApp/app/src/main/java/org/drrickorang/loopback/SaveFilesDialogFragment.java4
-rw-r--r--LoopbackApp/app/src/main/java/org/drrickorang/loopback/SettingsActivity.java61
-rw-r--r--LoopbackApp/app/src/main/java/org/drrickorang/loopback/SettingsPicker.java55
-rw-r--r--LoopbackApp/app/src/main/java/org/drrickorang/loopback/WaveDataRingBuffer.java169
-rw-r--r--LoopbackApp/app/src/main/jni/jni_sles.c60
-rw-r--r--LoopbackApp/app/src/main/jni/jni_sles.h11
-rw-r--r--LoopbackApp/app/src/main/jni/sles.cpp174
-rw-r--r--LoopbackApp/app/src/main/jni/sles.h37
-rw-r--r--LoopbackApp/app/src/main/res/drawable-xxxhdpi/ic_help_outline.pngbin0 -> 1157 bytes
-rw-r--r--LoopbackApp/app/src/main/res/drawable-xxxhdpi/offtogglebutton.9.pngbin0 -> 2843 bytes
-rw-r--r--LoopbackApp/app/src/main/res/drawable-xxxhdpi/ontogglebutton.9.pngbin0 -> 3520 bytes
-rw-r--r--LoopbackApp/app/src/main/res/drawable-xxxhdpi/togglebutton_state_drawable.xml9
-rw-r--r--LoopbackApp/app/src/main/res/layout/heatmap_window.xml32
-rw-r--r--LoopbackApp/app/src/main/res/layout/main_activity.xml71
-rw-r--r--LoopbackApp/app/src/main/res/layout/report_window.xml1
-rw-r--r--LoopbackApp/app/src/main/res/layout/settings_activity.xml103
-rw-r--r--LoopbackApp/app/src/main/res/layout/settings_picker.xml27
-rw-r--r--LoopbackApp/app/src/main/res/raw/loopback_listener69
-rw-r--r--LoopbackApp/app/src/main/res/values/strings.xml37
-rw-r--r--LoopbackApp/proguard.cfg3
41 files changed, 2223 insertions, 403 deletions
diff --git a/LoopbackApp/app/app.iml b/LoopbackApp/app/app.iml
index a1e5607..d77f215 100644
--- a/LoopbackApp/app/app.iml
+++ b/LoopbackApp/app/app.iml
@@ -65,23 +65,15 @@
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/jni" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/renderscript" isTestSource="true" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/assets" />
- <excludeFolder url="file://$MODULE_DIR$/build/intermediates/binaries" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/blame" />
- <excludeFolder url="file://$MODULE_DIR$/build/intermediates/classes" />
- <excludeFolder url="file://$MODULE_DIR$/build/intermediates/debug" />
- <excludeFolder url="file://$MODULE_DIR$/build/intermediates/dependency-cache" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/exploded-aar/com.android.support/appcompat-v7/23.0.1/jars" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/exploded-aar/com.android.support/support-v4/23.0.1/jars" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/incremental" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/manifests" />
- <excludeFolder url="file://$MODULE_DIR$/build/intermediates/objectFiles" />
- <excludeFolder url="file://$MODULE_DIR$/build/intermediates/pre-dexed" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/res" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/rs" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/symbols" />
- <excludeFolder url="file://$MODULE_DIR$/build/intermediates/transforms" />
<excludeFolder url="file://$MODULE_DIR$/build/outputs" />
- <excludeFolder url="file://$MODULE_DIR$/build/tmp" />
</content>
<orderEntry type="jdk" jdkName="Android API 23 Platform" jdkType="Android SDK" />
<orderEntry type="sourceFolder" forTests="false" />
diff --git a/LoopbackApp/app/src/main/AndroidManifest.xml b/LoopbackApp/app/src/main/AndroidManifest.xml
index 488415b..2593afe 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="9"
- android:versionName="0.9">
+ android:versionCode="10"
+ android:versionName="0.9.5">
<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/AtraceScriptsWriter.java b/LoopbackApp/app/src/main/java/org/drrickorang/loopback/AtraceScriptsWriter.java
new file mode 100644
index 0000000..7cfb74a
--- /dev/null
+++ b/LoopbackApp/app/src/main/java/org/drrickorang/loopback/AtraceScriptsWriter.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2016 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 org.drrickorang.loopback;
+
+import android.content.Context;
+import android.util.Log;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+/**
+ * Places loopback_listener shell script on device storage
+ */
+public class AtraceScriptsWriter {
+
+ private static final String TAG = "AtraceScriptsWriter";
+ private static final String LISTENER_SCRIPT_LOCATION =
+ CaptureHolder.DIRECTORY + "/loopback_listener";
+
+ /** Writes scripts to device storage, return true on successful write **/
+ public static boolean writeScriptsToFile(Context ctx) {
+ try {
+ File file = new File(CaptureHolder.DIRECTORY);
+
+ // Create a directory for script and signal file
+ if (!file.exists()) {
+ if (file.mkdir()) {
+ Log.d(TAG, "writeScriptsToFile: Loopback folder created");
+ } else {
+ System.out.println("Failed to create folder!");
+ return false;
+ }
+ }
+ // Check for writable directory that already existed or after creating
+ if (!file.isDirectory() || !file.canWrite()) {
+ Log.d(TAG, "writeScriptsToFile: " + CaptureHolder.DIRECTORY
+ + (!file.isDirectory() ? "is not a directory " : "")
+ + (!file.canWrite() ? "is not writable" : ""));
+ return false;
+ }
+ copyResToFile(ctx, R.raw.loopback_listener, LISTENER_SCRIPT_LOCATION);
+ } catch (IOException e) {
+ Log.e(TAG, "Unable to write script to file", e);
+ return false;
+ }
+ return true;
+ }
+
+ private static void copyResToFile(Context ctx, int resId, String targetFile)
+ throws IOException {
+ InputStream inputStream = ctx.getResources().openRawResource(resId);
+ OutputStream outputStream = new FileOutputStream(targetFile);
+ copy(inputStream, outputStream);
+ outputStream.close();
+ inputStream.close();
+ }
+
+
+ private static int copy(InputStream input, OutputStream output) throws IOException {
+ final int BYTES_TO_READ = 2048;
+ byte[] buffer = new byte[BYTES_TO_READ];
+ int total = 0;
+ int n;
+ while ((n = input.read(buffer)) != -1) {
+ output.write(buffer, 0, n);
+ total = total + n;
+ }
+ return total;
+ }
+
+}
diff --git a/LoopbackApp/app/src/main/java/org/drrickorang/loopback/AudioFileOutput.java b/LoopbackApp/app/src/main/java/org/drrickorang/loopback/AudioFileOutput.java
index b78f04a..e8b44a7 100644
--- a/LoopbackApp/app/src/main/java/org/drrickorang/loopback/AudioFileOutput.java
+++ b/LoopbackApp/app/src/main/java/org/drrickorang/loopback/AudioFileOutput.java
@@ -48,6 +48,17 @@ public class AudioFileOutput {
public boolean writeData(double[] data) {
+ return writeRingBufferData(data, 0, data.length);
+ }
+
+ /**
+ * Writes recorded wav data to file
+ * endIndex <= startIndex: Writes [startIndex, data.length) then [0, endIndex)
+ * endIndex > startIndex : Writes [startIndex, endIndex)
+ * Returns true on successful write to file
+ */
+ public boolean writeRingBufferData(double[] data, int startIndex, int endIndex) {
+
boolean status = false;
ParcelFileDescriptor parcelFileDescriptor = null;
try {
@@ -56,9 +67,19 @@ public class AudioFileOutput {
FileDescriptor fileDescriptor = parcelFileDescriptor.getFileDescriptor();
mOutputStream = new FileOutputStream(fileDescriptor);
log("Done creating output stream");
- int sampleCount = data.length;
+ int sampleCount = endIndex - startIndex;
+ if (sampleCount <= 0) {
+ sampleCount += data.length;
+ }
writeHeader(sampleCount);
- writeDataBuffer(data);
+
+ if (endIndex > startIndex) {
+ writeDataBuffer(data, startIndex, endIndex);
+ } else {
+ writeDataBuffer(data, startIndex, data.length);
+ writeDataBuffer(data, 0, endIndex);
+ }
+
mOutputStream.close();
status = true;
parcelFileDescriptor.close();
@@ -78,7 +99,6 @@ public class AudioFileOutput {
return status;
}
-
private void writeHeader(int samples) {
if (mOutputStream != null) {
try {
@@ -124,20 +144,19 @@ public class AudioFileOutput {
}
- private void writeDataBuffer(double [] data) {
+ private void writeDataBuffer(double [] data, int startIndex, int end) {
if (mOutputStream != null) {
try {
- int sampleCount = data.length;
int bufferSize = 1024; //blocks of 1024 samples
byte [] buffer = new byte[bufferSize * 2];
- for (int ii = 0; ii < sampleCount; ii += bufferSize) {
+ for (int ii = startIndex; ii < end; ii += bufferSize) {
//clear buffer
Arrays.fill(buffer, (byte) 0);
int bytesUsed = 0;
for (int jj = 0; jj < bufferSize; jj++) {
int index = ii + jj;
- if (index >= sampleCount)
+ if (index >= end)
break;
int value = (int) Math.round(data[index] * Short.MAX_VALUE);
byte ba = (byte) (0xFF & (value >> 8)); //little-endian
diff --git a/LoopbackApp/app/src/main/java/org/drrickorang/loopback/BufferCallbackTimes.java b/LoopbackApp/app/src/main/java/org/drrickorang/loopback/BufferCallbackTimes.java
new file mode 100644
index 0000000..325459e
--- /dev/null
+++ b/LoopbackApp/app/src/main/java/org/drrickorang/loopback/BufferCallbackTimes.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright (C) 2016 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 org.drrickorang.loopback;
+
+import java.util.Iterator;
+
+/**
+ * Maintains and returns pairs of callback timestamps (in milliseconds since beginning of test) and
+ * lengths (milliseconds between a callback and the previous callback).
+ */
+public class BufferCallbackTimes implements Iterable<BufferCallbackTimes.BufferCallback> {
+ private final int[] mTimeStamps;
+ private final short[] mCallbackDurations;
+ private final short mExpectedBufferPeriod;
+ private boolean mExceededCapacity;
+ private int mIndex;
+
+ public BufferCallbackTimes(int maxRecords, int expectedBufferPeriod) {
+ mIndex = 0;
+ mTimeStamps = new int[maxRecords];
+ mCallbackDurations = new short[maxRecords];
+ mExceededCapacity = false;
+ mExpectedBufferPeriod = (short) expectedBufferPeriod;
+ }
+
+ /**
+ * Instantiates an iterable object with already recorded callback times and lengths
+ * used for callbacks recorded by native sles callback functions.
+ *
+ * exceededCapacity should be set to true only when there were late callbacks observed but
+ * unable to be recorded because allocated arrays were already at capacity
+ */
+ public BufferCallbackTimes(int[] timeStamps, short[] callbackDurations,
+ boolean exceededCapacity, short expectedBufferPeriod) {
+ mTimeStamps = timeStamps;
+ mCallbackDurations = callbackDurations;
+ mExceededCapacity = exceededCapacity;
+ mIndex = mTimeStamps.length;
+ mExpectedBufferPeriod = expectedBufferPeriod;
+ }
+
+ /** Record the length of a late/early callback and the time it occurred. Used by Java Thread. */
+ public void recordCallbackTime(int timeStamp, short callbackLength) {
+ if (!mExceededCapacity && callbackLength != mExpectedBufferPeriod
+ && callbackLength != mExpectedBufferPeriod + 1) {
+ //only marked as exceeded if attempting to record a late callback after arrays full
+ if (mIndex == mTimeStamps.length) {
+ mExceededCapacity = true;
+ return;
+ }
+ mTimeStamps[mIndex] = timeStamp;
+ mCallbackDurations[mIndex] = callbackLength;
+ mIndex++;
+ }
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ for (BufferCallback callback : this) {
+ sb.append(callback.timeStamp);
+ sb.append(",");
+ sb.append(callback.callbackDuration);
+ sb.append("\n");
+ }
+ return sb.toString();
+ }
+
+ // True only if arrays are full and recording more late or early callbacks is attempted.
+ public boolean isCapacityExceeded() {
+ return mExceededCapacity;
+ }
+
+ public int getNumLateOrEarlyCallbacks() {
+ return mIndex;
+ }
+
+ public short getExpectedBufferPeriod() {
+ return mExpectedBufferPeriod;
+ }
+
+ @Override
+ public Iterator<BufferCallback> iterator() {
+ return new Iterator<BufferCallback>() {
+ int mIteratorIndex = 0;
+
+ @Override
+ public boolean hasNext() {
+ return mIteratorIndex < mIndex;
+ }
+
+ @Override
+ public BufferCallback next() {
+ return new BufferCallback(mTimeStamps[mIteratorIndex],
+ mCallbackDurations[mIteratorIndex++]);
+ }
+
+ @Override
+ public void remove() {
+ throw new UnsupportedOperationException("Buffer Time Stamps are Immutable");
+ }
+ };
+ }
+
+ /** Wrapper for iteration over timestamp and length pairs */
+ public class BufferCallback {
+ public final int timeStamp;
+ public final short callbackDuration;
+
+ BufferCallback(final int ts, final short cd) {
+ timeStamp = ts;
+ callbackDuration = cd;
+ }
+ }
+}
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 ccfab52..7716d9f 100644
--- a/LoopbackApp/app/src/main/java/org/drrickorang/loopback/BufferPeriod.java
+++ b/LoopbackApp/app/src/main/java/org/drrickorang/loopback/BufferPeriod.java
@@ -39,8 +39,8 @@ public class BufferPeriod {
private final int range = 1002; // store counts for 0ms to 1000ms, and for > 1000ms
private int[] mBufferPeriod = new int[range];
- private int[] mBufferPeriodTimeStamp = new int[range];
-
+ private BufferCallbackTimes mCallbackTimes;
+ private CaptureHolder mCaptureHolder;
/**
* For player, this function is called before every AudioTrack.write().
@@ -73,14 +73,16 @@ public class BufferPeriod {
// from 0 ms to 1000 ms, plus a sum of all occurrences > 1000ms
if (diffInMilli >= (range - 1)) {
mBufferPeriod[range - 1]++;
- mBufferPeriodTimeStamp[range - 1] = timeStampInMilli;
} else if (diffInMilli >= 0) {
mBufferPeriod[diffInMilli]++;
- mBufferPeriodTimeStamp[diffInMilli] = timeStampInMilli;
} else { // for diffInMilli < 0
log("Having negative BufferPeriod.");
}
+ mCallbackTimes.recordCallbackTime(timeStampInMilli, (short) diffInMilli);
+
+ // If diagnosing specific Java thread callback behavior set a conditional here and use
+ // mCaptureHolder.captureState(rank); to capture systraces and bugreport and/or wav file
}
mPreviousTimeNs = mCurrentTimeNs;
@@ -91,27 +93,29 @@ public class BufferPeriod {
public void resetRecord() {
mPreviousTimeNs = 0;
mCurrentTimeNs = 0;
- Arrays.fill(mBufferPeriodTimeStamp, 0);
Arrays.fill(mBufferPeriod, 0);
mMaxBufferPeriod = 0;
mCount = 0;
+ mCallbackTimes = null;
}
+ public void prepareMemberObjects(int maxRecords, int expectedBufferPeriod,
+ CaptureHolder captureHolder){
+ mCallbackTimes = new BufferCallbackTimes(maxRecords, expectedBufferPeriod);
+ mCaptureHolder = captureHolder;
+ }
public int[] getBufferPeriodArray() {
return mBufferPeriod;
}
-
- public int[] getBufferPeriodTimeStampArray() {
- return mBufferPeriodTimeStamp;
- }
-
-
public int getMaxBufferPeriod() {
return mMaxBufferPeriod;
}
+ public BufferCallbackTimes getCallbackTimes(){
+ return mCallbackTimes;
+ }
private static void log(String msg) {
Log.v(TAG, msg);
diff --git a/LoopbackApp/app/src/main/java/org/drrickorang/loopback/CaptureHolder.java b/LoopbackApp/app/src/main/java/org/drrickorang/loopback/CaptureHolder.java
new file mode 100644
index 0000000..d5e38dd
--- /dev/null
+++ b/LoopbackApp/app/src/main/java/org/drrickorang/loopback/CaptureHolder.java
@@ -0,0 +1,284 @@
+/*
+ * Copyright (C) 2016 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 org.drrickorang.loopback;
+
+import android.content.Context;
+import android.net.Uri;
+import android.util.Log;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Captures systrace, bugreport, and wav snippets. Capable of relieving capture requests from
+ * multiple threads and maintains queue of most interesting records
+ */
+public class CaptureHolder {
+
+ private static final String TAG = "CAPTURE";
+ public static final String STORAGE = "/sdcard/";
+ public static final String DIRECTORY = STORAGE + "Loopback";
+ private static final String SIGNAL_FILE = DIRECTORY + "/loopback_signal";
+ 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";
+
+ // Status codes returned by captureState
+ public static final int NEW_CAPTURE_IS_LEAST_INTERESTING = -1;
+ public static final int CAPTURE_ALREADY_IN_PROGRESS = 0;
+ public static final int STATE_CAPTURED = 1;
+ public static final int CAPTURING_DISABLED = 2;
+
+ private final String mFileNamePrefix;
+ private final long mStartTimeMS;
+ private final boolean mIsCapturingWavs;
+ private final boolean mIsCapturingSystraces;
+ private final int mCaptureCapacity;
+ private Thread mCaptureThread;
+ private volatile CapturedState mCapturedStates[];
+ private WaveDataRingBuffer mWaveDataBuffer;
+
+ //for creating AudioFileOutput objects
+ private final Context mContext;
+ private final int mSamplingRate;
+
+ public CaptureHolder(int captureCapacity, String fileNamePrefix, boolean captureWavs,
+ boolean captureSystraces, Context context, int samplingRate) {
+ mCaptureCapacity = captureCapacity;
+ mFileNamePrefix = fileNamePrefix;
+ mIsCapturingWavs = captureWavs;
+ mIsCapturingSystraces = captureSystraces;
+ mStartTimeMS = System.currentTimeMillis();
+ mCapturedStates = new CapturedState[mCaptureCapacity];
+ mContext = context;
+ mSamplingRate = samplingRate;
+ }
+
+ public void setWaveDataBuffer(WaveDataRingBuffer waveDataBuffer) {
+ mWaveDataBuffer = waveDataBuffer;
+ }
+
+ /**
+ * Launch thread to capture a systrace/bugreport and/or wav snippets and insert into collection
+ * If capturing is not enabled or capture state thread is already running returns immediately
+ * If newly requested capture is determined to be less interesting than all previous captures
+ * returns without running capture thread
+ *
+ * Can be called from both GlitchDetectionThread and Sles/Java buffer callbacks.
+ * Rank parameter and time of capture can be used by getIndexOfLeastInterestingCapture to
+ * determine which records to delete when at capacity.
+ * Therefore rank could represent glitchiness or callback behaviour and comparisons will need to
+ * be adjusted based on testing priorities
+ *
+ * Please note if calling from audio thread could cause glitches to occur because of blocking on
+ * this synchronized method. Additionally capturing a systrace and bugreport and writing to
+ * disk will likely have an affect on audio performance.
+ */
+ public synchronized int captureState(int rank) {
+
+ if (!mIsCapturingWavs && !mIsCapturingSystraces) {
+ Log.d(TAG, "captureState: Capturing wavs or systraces 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");
+ return CAPTURE_ALREADY_IN_PROGRESS;
+ }
+
+ long timeFromTestStartMS = System.currentTimeMillis() - mStartTimeMS;
+ long hours = TimeUnit.MILLISECONDS.toHours(timeFromTestStartMS);
+ long minutes = TimeUnit.MILLISECONDS.toMinutes(timeFromTestStartMS) -
+ TimeUnit.HOURS.toMinutes(TimeUnit.MILLISECONDS.toHours(timeFromTestStartMS));
+ long seconds = TimeUnit.MILLISECONDS.toSeconds(timeFromTestStartMS) -
+ 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);
+
+ int indexOfLeastInteresting = getIndexOfLeastInterestingCapture(cs);
+ if (indexOfLeastInteresting == NEW_CAPTURE_IS_LEAST_INTERESTING) {
+ Log.d(TAG, "captureState: All Previously captured states were more interesting than" +
+ " requested capture");
+ return NEW_CAPTURE_IS_LEAST_INTERESTING;
+ }
+
+ mCaptureThread = new CaptureThread(cs, indexOfLeastInteresting);
+ mCaptureThread.start();
+
+ return STATE_CAPTURED;
+ }
+
+ /**
+ * Send signal to listener script to terminate and stop atrace
+ **/
+ public static void stopLoopbackListenerScript() {
+ try {
+ OutputStream outputStream = new FileOutputStream(SIGNAL_FILE);
+ outputStream.write(TERMINATE_SIGNAL.getBytes());
+ outputStream.close();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+
+ Log.d(TAG, "stopLoopbackListenerScript: Signaled Listener Script to exit");
+ }
+
+ /**
+ * Currently returns recorded state with lowest Glitch count
+ * Alternate criteria can be established here and in captureState rank parameter
+ *
+ * returns -1 (NEW_CAPTURE_IS_LEAST_INTERESTING) if candidate is least interesting, otherwise
+ * returns index of record to replace
+ */
+ private int getIndexOfLeastInterestingCapture(CapturedState candidateCS) {
+ CapturedState leastInteresting = candidateCS;
+ int index = NEW_CAPTURE_IS_LEAST_INTERESTING;
+ for (int i = 0; i < mCapturedStates.length; i++) {
+ if (mCapturedStates[i] == null) {
+ // Array is not yet at capacity, insert in next available position
+ return i;
+ }
+ if (mCapturedStates[i].rank < leastInteresting.rank) {
+ index = i;
+ leastInteresting = mCapturedStates[i];
+ }
+ }
+ return index;
+ }
+
+ public boolean isCapturingWavs() {
+ return mIsCapturingWavs;
+ }
+
+ public boolean isCapturingSysTraces() {
+ return mIsCapturingSystraces;
+ }
+
+ /**
+ * Data struct for filenames of previously captured results. Rank and time captured can be used
+ * for determining position in rolling queue
+ */
+ private class CapturedState {
+ public final String fileNamePrefix;
+ public final long timeFromStartOfTestMS;
+ public final int rank;
+
+ public CapturedState(String fileNamePrefix, long timeFromStartOfTestMS, int rank) {
+ this.fileNamePrefix = fileNamePrefix;
+ this.timeFromStartOfTestMS = timeFromStartOfTestMS;
+ this.rank = rank;
+ }
+
+ @Override
+ public String toString() {
+ return "CapturedState { fileName:" + fileNamePrefix + ", Rank:" + rank + "}";
+ }
+ }
+
+ private class CaptureThread extends Thread {
+
+ private CapturedState mNewCapturedState;
+ private int mIndexToPlace;
+
+ /**
+ * Create new thread with capture state sturct for captured systrace, bugreport and wav
+ **/
+ public CaptureThread(CapturedState cs, int indexToPlace) {
+ mNewCapturedState = cs;
+ mIndexToPlace = indexToPlace;
+ setName("CaptureThread");
+ setPriority(Thread.MIN_PRIORITY);
+ }
+
+ @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) {
+ Log.d(TAG, "CaptureThread: signaling listener to write to:" +
+ mNewCapturedState.fileNamePrefix);
+ 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();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ // Write wav if member mWaveDataBuffer has been set
+ if (mIsCapturingWavs && mWaveDataBuffer != null) {
+ Log.d(TAG, "CaptureThread: begin Writing wav data to file");
+ WaveDataRingBuffer.ReadableWaveDeck deck = mWaveDataBuffer.getWaveDeck();
+ if (deck != null) {
+ AudioFileOutput audioFile = new AudioFileOutput(mContext,
+ Uri.parse("file://mnt" + mNewCapturedState.fileNamePrefix
+ + WAV_SUFFIX),
+ mSamplingRate);
+ boolean success = deck.writeToFile(audioFile);
+ Log.d(TAG, "CaptureThread: wav data written successfully: " + success);
+ }
+ }
+
+ // Check for sys and bug finished
+ // loopback listener script signals completion by deleting signal file
+ if (mIsCapturingSystraces) {
+ File signalFile = new File(SIGNAL_FILE);
+ while (signalFile.exists()) {
+ try {
+ sleep(1);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ // Delete least interesting if necessary and insert new capture in list
+ String suffixes[] = {SYSTRACE_SUFFIX, BUGREPORT_SUFFIX, WAV_SUFFIX};
+ if (mCapturedStates[mIndexToPlace] != null) {
+ Log.d(TAG, "Deleting capture: " + mCapturedStates[mIndexToPlace]);
+ for (String suffix : suffixes) {
+ File oldFile = new File(mCapturedStates[mIndexToPlace].fileNamePrefix + suffix);
+ boolean deleted = oldFile.delete();
+ if (!deleted) {
+ Log.d(TAG, "Delete old capture: " + oldFile.toString() +
+ (oldFile.exists() ? " unable to delete" : " was not present"));
+ }
+ }
+ }
+ Log.d(TAG, "Adding capture to list: " + mNewCapturedState);
+ mCapturedStates[mIndexToPlace] = mNewCapturedState;
+
+ // Log captured states
+ String log = "Captured states:";
+ for (CapturedState cs:mCapturedStates) log += "\n...." + cs;
+ Log.d(TAG, log);
+
+ Log.d(TAG, "Completed capture thread terminating");
+ }
+ }
+}
diff --git a/LoopbackApp/app/src/main/java/org/drrickorang/loopback/CatchEventsEditText.java b/LoopbackApp/app/src/main/java/org/drrickorang/loopback/CatchEventsEditText.java
new file mode 100644
index 0000000..b8b3f8f
--- /dev/null
+++ b/LoopbackApp/app/src/main/java/org/drrickorang/loopback/CatchEventsEditText.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2016 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 org.drrickorang.loopback;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.KeyEvent;
+import android.view.inputmethod.EditorInfo;
+import android.widget.EditText;
+import android.widget.TextView;
+
+/**
+ * Provides a callback for both soft-keyboard dismissed or confirm submission button
+ */
+public class CatchEventsEditText extends EditText implements TextView.OnEditorActionListener {
+
+ public interface EditTextEventListener {
+ public void textEdited(EditText v);
+ }
+
+ private EditTextEventListener mEditListener;
+
+ public CatchEventsEditText(Context context) {
+ super(context);
+ setOnEditorActionListener(this);
+ }
+
+ public CatchEventsEditText(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ setOnEditorActionListener(this);
+ }
+
+ public CatchEventsEditText(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ setOnEditorActionListener(this);
+ }
+
+ public void setEditTextEvenListener(EditTextEventListener listener) {
+ mEditListener = listener;
+ }
+
+ @Override
+ public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
+ if ((event != null && (event.getKeyCode() == KeyEvent.KEYCODE_ENTER))
+ || (actionId == EditorInfo.IME_ACTION_DONE)) {
+ mEditListener.textEdited(this);
+ }
+ // Necessary to return false even when event handled for soft-keyboard to be dismissed
+ // Differs from on click listener chains where first listener to handle returns true
+ return false;
+ }
+
+ @Override
+ public boolean onKeyPreIme(int keyCode, KeyEvent event) {
+ if (event.getKeyCode() == KeyEvent.KEYCODE_BACK
+ && event.getAction() == KeyEvent.ACTION_UP) {
+ mEditListener.textEdited(this);
+ }
+ return super.onKeyPreIme(keyCode, event);
+ }
+} \ No newline at end of file
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 861c7fd..d015352 100644
--- a/LoopbackApp/app/src/main/java/org/drrickorang/loopback/Constant.java
+++ b/LoopbackApp/app/src/main/java/org/drrickorang/loopback/Constant.java
@@ -72,5 +72,10 @@ public class Constant {
public static final int BUFFER_TEST_WAVE_PLOT_DURATION_SECONDS_MIN = 1;
public static final int MAX_NUM_LOAD_THREADS = 20;
public static final int MIN_NUM_LOAD_THREADS = 0;
+ public static final int MIN_NUM_CAPTURES = 1;
+ public static final int MAX_NUM_CAPTURES = 100;
+ public static final int DEFAULT_NUM_CAPTURES = 5;
+ // Controls size of pre allocated timestamp arrays
+ public static final int MAX_RECORDED_LATE_CALLBACKS_PER_SECOND = 2;
}
diff --git a/LoopbackApp/app/src/main/java/org/drrickorang/loopback/GlitchAndCallbackHeatMapView.java b/LoopbackApp/app/src/main/java/org/drrickorang/loopback/GlitchAndCallbackHeatMapView.java
new file mode 100644
index 0000000..de24e81
--- /dev/null
+++ b/LoopbackApp/app/src/main/java/org/drrickorang/loopback/GlitchAndCallbackHeatMapView.java
@@ -0,0 +1,498 @@
+/*
+ * Copyright (C) 2016 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 org.drrickorang.loopback;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.view.View;
+import android.widget.LinearLayout.LayoutParams;
+
+/**
+ * Creates a heat map graphic for glitches and callback durations over the time period of the test
+ * Instantiated view is used for displaying heat map on android device, static methods can be used
+ * without an instantiated view to draw graph on a canvas for use in exporting an image file
+ */
+public class GlitchAndCallbackHeatMapView extends View {
+
+ private final BufferCallbackTimes mPlayerCallbackTimes;
+ private final BufferCallbackTimes mRecorderCallbackTimes;
+ private final int[] mGlitchTimes;
+ private boolean mGlitchesExceededCapacity;
+ private final int mTestDurationSeconds;
+ private final String mTitle;
+
+ private static final int MILLIS_PER_SECOND = 1000;
+ private static final int SECONDS_PER_MINUTE = 60;
+ private static final int MINUTES_PER_HOUR = 60;
+ private static final int SECONDS_PER_HOUR = 3600;
+
+ private static final int LABEL_SIZE = 36;
+ private static final int TITLE_SIZE = 80;
+ private static final int LINE_WIDTH = 5;
+ private static final int INNER_MARGIN = 20;
+ private static final int OUTER_MARGIN = 60;
+ private static final int COLOR_LEGEND_AREA_WIDTH = 250;
+ private static final int COLOR_LEGEND_WIDTH = 75;
+ private static final int EXCEEDED_LEGEND_WIDTH = 150;
+ private static final int MAX_DURATION_FOR_SECONDS_BUCKET = 240;
+ private static final int NUM_X_AXIS_TICKS = 9;
+ private static final int NUM_LEGEND_LABELS = 5;
+ private static final int TICK_SIZE = 30;
+
+ private static final int MAX_COLOR = 0xFF0D47A1; // Dark Blue
+ private static final int START_COLOR = Color.WHITE;
+ private static final float LOG_FACTOR = 2.0f; // >=1 Higher value creates a more linear curve
+
+ public GlitchAndCallbackHeatMapView(Context context, BufferCallbackTimes recorderCallbackTimes,
+ BufferCallbackTimes playerCallbackTimes, int[] glitchTimes,
+ boolean glitchesExceededCapacity, int testDurationSeconds,
+ String title) {
+ super(context);
+
+ mRecorderCallbackTimes = recorderCallbackTimes;
+ mPlayerCallbackTimes = playerCallbackTimes;
+ mGlitchTimes = glitchTimes;
+ mGlitchesExceededCapacity = glitchesExceededCapacity;
+ mTestDurationSeconds = testDurationSeconds;
+ mTitle = title;
+
+ setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
+ setWillNotDraw(false);
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+ Bitmap bmpResult = Bitmap.createBitmap(canvas.getHeight(), canvas.getWidth(),
+ Bitmap.Config.ARGB_8888);
+ // Provide rotated canvas to FillCanvas method
+ Canvas tmpCanvas = new Canvas(bmpResult);
+ fillCanvas(tmpCanvas, mRecorderCallbackTimes, mPlayerCallbackTimes, mGlitchTimes,
+ mGlitchesExceededCapacity, mTestDurationSeconds, mTitle);
+ tmpCanvas.translate(-1 * tmpCanvas.getWidth(), 0);
+ tmpCanvas.rotate(-90, tmpCanvas.getWidth(), 0);
+ // Display landscape oriented image on android device
+ canvas.drawBitmap(bmpResult, tmpCanvas.getMatrix(), new Paint(Paint.ANTI_ALIAS_FLAG));
+ }
+
+ /**
+ * Draw a heat map of callbacks and glitches for display on Android device or for export as png
+ */
+ public static void fillCanvas(final Canvas canvas,
+ final BufferCallbackTimes recorderCallbackTimes,
+ final BufferCallbackTimes playerCallbackTimes,
+ final int[] glitchTimes, final boolean glitchesExceededCapacity,
+ final int testDurationSeconds, final String title) {
+
+ final Paint heatPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+ heatPaint.setStyle(Paint.Style.FILL);
+
+ final Paint textPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+ textPaint.setColor(Color.BLACK);
+ textPaint.setTextSize(LABEL_SIZE);
+ textPaint.setTextAlign(Paint.Align.CENTER);
+
+ final Paint titlePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+ titlePaint.setColor(Color.BLACK);
+ titlePaint.setTextAlign(Paint.Align.CENTER);
+ titlePaint.setTextSize(TITLE_SIZE);
+
+ final Paint linePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+ linePaint.setColor(Color.BLACK);
+ linePaint.setStyle(Paint.Style.STROKE);
+ linePaint.setStrokeWidth(LINE_WIDTH);
+
+ final Paint colorPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+ colorPaint.setStyle(Paint.Style.STROKE);
+
+ ColorInterpolator colorInter = new ColorInterpolator(START_COLOR, MAX_COLOR);
+
+ Rect textBounds = new Rect();
+ titlePaint.getTextBounds(title, 0, title.length(), textBounds);
+ Rect titleArea = new Rect(0, OUTER_MARGIN, canvas.getWidth(),
+ OUTER_MARGIN + textBounds.height());
+
+ Rect bottomLegendArea = new Rect(0, canvas.getHeight() - LABEL_SIZE - OUTER_MARGIN,
+ canvas.getWidth(), canvas.getHeight() - OUTER_MARGIN);
+
+ int graphWidth = canvas.getWidth() - COLOR_LEGEND_AREA_WIDTH - OUTER_MARGIN * 3;
+ int graphHeight = (bottomLegendArea.top - titleArea.bottom - OUTER_MARGIN * 3) / 2;
+
+ Rect callbackHeatArea = new Rect(0, 0, graphWidth, graphHeight);
+ callbackHeatArea.offsetTo(OUTER_MARGIN, titleArea.bottom + OUTER_MARGIN);
+
+ Rect glitchHeatArea = new Rect(0, 0, graphWidth, graphHeight);
+ glitchHeatArea.offsetTo(OUTER_MARGIN, callbackHeatArea.bottom + OUTER_MARGIN);
+
+ final int bucketSize =
+ testDurationSeconds < MAX_DURATION_FOR_SECONDS_BUCKET ? 1 : SECONDS_PER_MINUTE;
+
+ String units = testDurationSeconds < MAX_DURATION_FOR_SECONDS_BUCKET ? "Second" : "Minute";
+ String glitchLabel = "Glitches Per " + units;
+ String callbackLabel = "Maximum Callback Duration(ms) Per " + units;
+
+ // Create White background
+ canvas.drawColor(Color.WHITE);
+
+ // Label Graph
+ canvas.drawText(title, titleArea.left + titleArea.width() / 2, titleArea.bottom,
+ titlePaint);
+
+ // Callback Graph /////////////
+ // label callback graph
+ Rect graphArea = new Rect(callbackHeatArea);
+ graphArea.left += LABEL_SIZE + INNER_MARGIN;
+ graphArea.bottom -= LABEL_SIZE;
+ graphArea.top += LABEL_SIZE + INNER_MARGIN;
+ canvas.drawText(callbackLabel, graphArea.left + graphArea.width() / 2,
+ graphArea.top - INNER_MARGIN, textPaint);
+
+ int labelX = graphArea.left - INNER_MARGIN;
+ int labelY = graphArea.top + graphArea.height() / 4;
+ canvas.save();
+ canvas.rotate(-90, labelX, labelY);
+ canvas.drawText("Recorder", labelX, labelY, textPaint);
+ canvas.restore();
+ labelY = graphArea.bottom - graphArea.height() / 4;
+ canvas.save();
+ canvas.rotate(-90, labelX, labelY);
+ canvas.drawText("Player", labelX, labelY, textPaint);
+ canvas.restore();
+
+ // draw callback heat graph
+ CallbackGraphData recorderData =
+ new CallbackGraphData(recorderCallbackTimes, bucketSize, testDurationSeconds);
+ CallbackGraphData playerData =
+ new CallbackGraphData(playerCallbackTimes, bucketSize, testDurationSeconds);
+ int maxCallbackValue = Math.max(recorderData.getMax(), playerData.getMax());
+
+ drawHeatMap(canvas, recorderData.getBucketedCallbacks(), maxCallbackValue, colorInter,
+ recorderCallbackTimes.isCapacityExceeded(), recorderData.getLastFilledIndex(),
+ new Rect(graphArea.left + LINE_WIDTH, graphArea.top,
+ graphArea.right - LINE_WIDTH, graphArea.centerY()));
+ drawHeatMap(canvas, playerData.getBucketedCallbacks(), maxCallbackValue, colorInter,
+ playerCallbackTimes.isCapacityExceeded(), playerData.getLastFilledIndex(),
+ new Rect(graphArea.left + LINE_WIDTH, graphArea.centerY(),
+ graphArea.right - LINE_WIDTH, graphArea.bottom));
+
+ drawTimeTicks(canvas, testDurationSeconds, bucketSize, callbackHeatArea.bottom,
+ graphArea.bottom, graphArea.left, graphArea.width(), textPaint, linePaint);
+
+ // draw graph boarder
+ canvas.drawRect(graphArea, linePaint);
+
+ // Callback Legend //////////////
+ if (maxCallbackValue > 0) {
+ Rect legendArea = new Rect(graphArea);
+ legendArea.left = graphArea.right + OUTER_MARGIN * 2;
+ legendArea.right = legendArea.left + COLOR_LEGEND_WIDTH;
+ drawColorLegend(canvas, maxCallbackValue, colorInter, linePaint, textPaint, legendArea);
+ }
+
+
+ // Glitch Graph /////////////
+ // label Glitch graph
+ graphArea.bottom = glitchHeatArea.bottom - LABEL_SIZE;
+ graphArea.top = glitchHeatArea.top + LABEL_SIZE + INNER_MARGIN;
+ canvas.drawText(glitchLabel, graphArea.left + graphArea.width() / 2,
+ graphArea.top - INNER_MARGIN, textPaint);
+
+ // draw glitch heat graph
+ int[] bucketedGlitches = new int[(testDurationSeconds + bucketSize - 1) / bucketSize];
+ int lastFilledGlitchBucket = bucketGlitches(glitchTimes, bucketSize * MILLIS_PER_SECOND,
+ bucketedGlitches);
+ int maxGlitchValue = 0;
+ for (int totalGlitch : bucketedGlitches) {
+ maxGlitchValue = Math.max(totalGlitch, maxGlitchValue);
+ }
+ drawHeatMap(canvas, bucketedGlitches, maxGlitchValue, colorInter,
+ glitchesExceededCapacity, lastFilledGlitchBucket,
+ new Rect(graphArea.left + LINE_WIDTH, graphArea.top,
+ graphArea.right - LINE_WIDTH, graphArea.bottom));
+
+ drawTimeTicks(canvas, testDurationSeconds, bucketSize,
+ graphArea.bottom + INNER_MARGIN + LABEL_SIZE, graphArea.bottom, graphArea.left,
+ graphArea.width(), textPaint, linePaint);
+
+ // draw graph border
+ canvas.drawRect(graphArea, linePaint);
+
+ // Callback Legend //////////////
+ if (maxGlitchValue > 0) {
+ Rect legendArea = new Rect(graphArea);
+ legendArea.left = graphArea.right + OUTER_MARGIN * 2;
+ legendArea.right = legendArea.left + COLOR_LEGEND_WIDTH;
+
+ drawColorLegend(canvas, maxGlitchValue, colorInter, linePaint, textPaint, legendArea);
+ }
+
+ // Draw legend for exceeded capacity
+ if (playerCallbackTimes.isCapacityExceeded() || recorderCallbackTimes.isCapacityExceeded()
+ || glitchesExceededCapacity) {
+ RectF exceededArea = new RectF(graphArea.left, bottomLegendArea.top,
+ graphArea.left + EXCEEDED_LEGEND_WIDTH, bottomLegendArea.bottom);
+ drawExceededMarks(canvas, exceededArea);
+ canvas.drawRect(exceededArea, linePaint);
+ textPaint.setTextAlign(Paint.Align.LEFT);
+ canvas.drawText(" = No Data Available, Recording Capacity Exceeded",
+ exceededArea.right + INNER_MARGIN, bottomLegendArea.bottom, textPaint);
+ textPaint.setTextAlign(Paint.Align.CENTER);
+ }
+
+ }
+
+ /**
+ * Find total number of glitches duration per minute or second
+ * Returns index of last minute or second bucket with a recorded glitches
+ */
+ private static int bucketGlitches(int[] glitchTimes, int bucketSizeMS, int[] bucketedGlitches) {
+ int bucketIndex = 0;
+
+ for (int glitchMS : glitchTimes) {
+ bucketIndex = glitchMS / bucketSizeMS;
+ bucketedGlitches[bucketIndex]++;
+ }
+
+ return bucketIndex;
+ }
+
+ private static void drawHeatMap(Canvas canvas, int[] bucketedValues, int maxValue,
+ ColorInterpolator colorInter, boolean capacityExceeded,
+ int lastFilledIndex, Rect graphArea) {
+ Paint colorPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+ colorPaint.setStyle(Paint.Style.FILL);
+ float rectWidth = (float) graphArea.width() / bucketedValues.length;
+ RectF colorRect = new RectF(graphArea.left, graphArea.top, graphArea.left + rectWidth,
+ graphArea.bottom);
+
+ // values are log scaled to a value between 0 and 1 using the following formula:
+ // (log(value + 1 ) / log(max + 1))^2
+ // Data is typically concentrated around the extreme high and low values, This log scale
+ // allows low values to still be visible and the exponent makes the curve slightly more
+ // linear in order that the color gradients are still distinguishable
+
+ float logMax = (float) Math.log(maxValue + 1);
+
+ for (int i = 0; i <= lastFilledIndex; ++i) {
+ colorPaint.setColor(colorInter.getInterColor(
+ (float) Math.pow((Math.log(bucketedValues[i] + 1) / logMax), LOG_FACTOR)));
+ canvas.drawRect(colorRect, colorPaint);
+ colorRect.offset(rectWidth, 0);
+ }
+
+ if (capacityExceeded) {
+ colorRect.right = graphArea.right;
+ drawExceededMarks(canvas, colorRect);
+ }
+ }
+
+ private static void drawColorLegend(Canvas canvas, int maxValue, ColorInterpolator colorInter,
+ Paint linePaint, Paint textPaint, Rect legendArea) {
+ Paint colorPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+ colorPaint.setStyle(Paint.Style.STROKE);
+ colorPaint.setStrokeWidth(1);
+ textPaint.setTextAlign(Paint.Align.LEFT);
+
+ float logMax = (float) Math.log(legendArea.height() + 1);
+ for (int y = legendArea.bottom; y >= legendArea.top; --y) {
+ float inter = (float) Math.pow(
+ (Math.log(legendArea.bottom - y + 1) / logMax), LOG_FACTOR);
+ colorPaint.setColor(colorInter.getInterColor(inter));
+ canvas.drawLine(legendArea.left, y, legendArea.right, y, colorPaint);
+ }
+
+ int tickSpacing = (maxValue + NUM_LEGEND_LABELS - 1) / NUM_LEGEND_LABELS;
+ for (int i = 0; i < maxValue; i += tickSpacing) {
+ float yPos = legendArea.bottom - (((float) i / maxValue) * legendArea.height());
+ canvas.drawText(Integer.toString(i), legendArea.right + INNER_MARGIN,
+ yPos + LABEL_SIZE / 2, textPaint);
+ canvas.drawLine(legendArea.right, yPos, legendArea.right - TICK_SIZE, yPos,
+ linePaint);
+ }
+ canvas.drawText(Integer.toString(maxValue), legendArea.right + INNER_MARGIN,
+ legendArea.top + LABEL_SIZE / 2, textPaint);
+
+ canvas.drawRect(legendArea, linePaint);
+ textPaint.setTextAlign(Paint.Align.CENTER);
+ }
+
+ private static void drawTimeTicks(Canvas canvas, int testDurationSeconds, int bucketSizeSeconds,
+ int textYPos, int tickYPos, int startXPos, int width,
+ Paint textPaint, Paint linePaint) {
+
+ int secondsPerTick;
+
+ if (bucketSizeSeconds == SECONDS_PER_MINUTE) {
+ secondsPerTick = (((testDurationSeconds / SECONDS_PER_MINUTE) + NUM_X_AXIS_TICKS - 1) /
+ NUM_X_AXIS_TICKS) * SECONDS_PER_MINUTE;
+ } else {
+ secondsPerTick = (testDurationSeconds + NUM_X_AXIS_TICKS - 1) / NUM_X_AXIS_TICKS;
+ }
+
+ for (int seconds = 0; seconds <= testDurationSeconds - secondsPerTick;
+ seconds += secondsPerTick) {
+ float xPos = startXPos + (((float) seconds / testDurationSeconds) * width);
+
+ if (bucketSizeSeconds == SECONDS_PER_MINUTE) {
+ canvas.drawText(String.format("%dh:%02dm", seconds / SECONDS_PER_HOUR,
+ (seconds / SECONDS_PER_MINUTE) % MINUTES_PER_HOUR),
+ xPos, textYPos, textPaint);
+ } else {
+ canvas.drawText(String.format("%dm:%02ds", seconds / SECONDS_PER_MINUTE,
+ seconds % SECONDS_PER_MINUTE),
+ xPos, textYPos, textPaint);
+ }
+
+ canvas.drawLine(xPos, tickYPos, xPos, tickYPos - TICK_SIZE, linePaint);
+ }
+
+ //Draw total duration marking on right side of graph
+ if (bucketSizeSeconds == SECONDS_PER_MINUTE) {
+ canvas.drawText(
+ String.format("%dh:%02dm", testDurationSeconds / SECONDS_PER_HOUR,
+ (testDurationSeconds / SECONDS_PER_MINUTE) % MINUTES_PER_HOUR),
+ startXPos + width, textYPos, textPaint);
+ } else {
+ canvas.drawText(
+ String.format("%dm:%02ds", testDurationSeconds / SECONDS_PER_MINUTE,
+ testDurationSeconds % SECONDS_PER_MINUTE),
+ startXPos + width, textYPos, textPaint);
+ }
+ }
+
+ /**
+ * Draw hash marks across a given rectangle, used to indicate no data available for that
+ * time period
+ */
+ private static void drawExceededMarks(Canvas canvas, RectF rect) {
+
+ final float LINE_WIDTH = 8;
+ final int STROKE_COLOR = Color.GRAY;
+ final float STROKE_OFFSET = LINE_WIDTH * 3; //space between lines
+
+ Paint strikePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+ strikePaint.setColor(STROKE_COLOR);
+ strikePaint.setStyle(Paint.Style.STROKE);
+ strikePaint.setStrokeWidth(LINE_WIDTH);
+
+ canvas.save();
+ canvas.clipRect(rect);
+
+ float startY = rect.bottom + STROKE_OFFSET;
+ float endY = rect.top - STROKE_OFFSET;
+ float startX = rect.left - rect.height(); //creates a 45 degree angle
+ float endX = rect.left;
+
+ for (; startX < rect.right; startX += STROKE_OFFSET, endX += STROKE_OFFSET) {
+ canvas.drawLine(startX, startY, endX, endY, strikePaint);
+ }
+
+ canvas.restore();
+ }
+
+ private static class CallbackGraphData {
+
+ private int[] mBucketedCallbacks;
+ private int mLastFilledIndex;
+
+ /**
+ * Fills buckets with maximum callback duration per minute or second
+ */
+ CallbackGraphData(BufferCallbackTimes callbackTimes, int bucketSizeSeconds,
+ int testDurationSeconds) {
+ mBucketedCallbacks =
+ new int[(testDurationSeconds + bucketSizeSeconds - 1) / bucketSizeSeconds];
+ int bucketSizeMS = bucketSizeSeconds * MILLIS_PER_SECOND;
+ int bucketIndex = 0;
+ for (BufferCallbackTimes.BufferCallback callback : callbackTimes) {
+
+ bucketIndex = callback.timeStamp / bucketSizeMS;
+ if (callback.callbackDuration > mBucketedCallbacks[bucketIndex]) {
+ mBucketedCallbacks[bucketIndex] = callback.callbackDuration;
+ }
+
+ // Original callback bucketing strategy, callbacks within a second/minute were added
+ // together in attempt to capture total amount of lateness within a time period.
+ // May become useful for debugging specific problems at some later date
+ /*if (callback.callbackDuration > callbackTimes.getExpectedBufferPeriod()) {
+ bucketedCallbacks[bucketIndex] += callback.callbackDuration;
+ }*/
+ }
+ mLastFilledIndex = bucketIndex;
+ }
+
+ public int getMax() {
+ int maxCallbackValue = 0;
+ for (int bucketValue : mBucketedCallbacks) {
+ maxCallbackValue = Math.max(maxCallbackValue, bucketValue);
+ }
+ return maxCallbackValue;
+ }
+
+ public int[] getBucketedCallbacks() {
+ return mBucketedCallbacks;
+ }
+
+ public int getLastFilledIndex() {
+ return mLastFilledIndex;
+ }
+ }
+
+ private static class ColorInterpolator {
+
+ private final int mAlphaStart;
+ private final int mAlphaRange;
+ private final int mRedStart;
+ private final int mRedRange;
+ private final int mGreenStart;
+ private final int mGreenRange;
+ private final int mBlueStart;
+ private final int mBlueRange;
+
+ public ColorInterpolator(int startColor, int endColor) {
+ mAlphaStart = Color.alpha(startColor);
+ mAlphaRange = Color.alpha(endColor) - mAlphaStart;
+
+ mRedStart = Color.red(startColor);
+ mRedRange = Color.red(endColor) - mRedStart;
+
+ mGreenStart = Color.green(startColor);
+ mGreenRange = Color.green(endColor) - mGreenStart;
+
+ mBlueStart = Color.blue(startColor);
+ mBlueRange = Color.blue(endColor) - mBlueStart;
+ }
+
+ /**
+ * Takes a float between 0 and 1 and returns a color int between mStartColor and mEndColor
+ **/
+ public int getInterColor(float input) {
+
+ return Color.argb(
+ mAlphaStart + (int) (input * mAlphaRange),
+ mRedStart + (int) (input * mRedRange),
+ mGreenStart + (int) (input * mGreenRange),
+ mBlueStart + (int) (input * mBlueRange)
+ );
+ }
+ }
+}
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 78cbc9d..d88b065 100644
--- a/LoopbackApp/app/src/main/java/org/drrickorang/loopback/GlitchDetectionThread.java
+++ b/LoopbackApp/app/src/main/java/org/drrickorang/loopback/GlitchDetectionThread.java
@@ -30,6 +30,9 @@ public class GlitchDetectionThread extends Thread {
// the acceptable difference between the expected center of mass and what we actually get
private static final double mAcceptablePercentDifference = 0.02; // change this if necessary
+ // Measured in FFT samples
+ private static final int GLITCH_CONCENTRATION_WINDOW_SIZE = 1500; // approx 30 seconds at 48kHz
+ private static final int COOLDOWN_WINDOW = 4500; // approx 90 seconds at 48kHz
private boolean mIsRunning; // condition must be true for the thread to run
private short mShortBuffer[]; // keep the data read from Pipe
@@ -56,14 +59,18 @@ public class GlitchDetectionThread extends Thread {
private FFT mFFT;
private boolean mGlitchingIntervalTooLong = false; // true if mGlitches is full
- //Pre-Allocated buffers for glitch detection process
+ // Pre-Allocated buffers for glitch detection process
private final double[] mFFTResult;
private final double[] mCurrentSamples;
private final double[] mImagArray;
+ // Used for captured SysTrace dumps
+ private CaptureHolder mCaptureHolder;
+ private int mLastGlitchCaptureAttempt = 0;
+
GlitchDetectionThread(double frequency1, double frequency2, int samplingRate,
int FFTSamplingSize, int FFTOverlapSamples, int bufferTestDurationInSeconds,
- int bufferTestWavePlotDurationInSeconds, Pipe pipe) {
+ int bufferTestWavePlotDurationInSeconds, Pipe pipe, CaptureHolder captureHolder) {
mPipe = pipe;
mFrequency1 = frequency1;
mFrequency2 = frequency2;
@@ -91,6 +98,9 @@ public class GlitchDetectionThread extends Thread {
setName("Loopback_GlitchDetection");
+ mCaptureHolder = captureHolder;
+ mCaptureHolder.setWaveDataBuffer(mWaveDataRing);
+
mThreadSleepDurationMs = FFTOverlapSamples * Constant.MILLIS_PER_SECOND / mSamplingRate;
if (mThreadSleepDurationMs < 1) {
mThreadSleepDurationMs = 1; // sleeps at least 1ms
@@ -200,13 +210,37 @@ public class GlitchDetectionThread extends Thread {
} else {
// centerOfMass == -1 if the wave we get is silence.
if (difference > mAcceptablePercentDifference || centerOfMass == -1) {
+ // Glitch Detected
mGlitches[mGlitchesIndex] = mFFTCount;
mGlitchesIndex++;
+ if (mCaptureHolder.isCapturingSysTraces() || mCaptureHolder.isCapturingWavs()) {
+ checkGlitchConcentration();
+ }
}
}
mFFTCount++;
}
+ private void checkGlitchConcentration(){
+
+ final int recordedGlitch = mGlitches[mGlitchesIndex-1];
+ if (recordedGlitch - mLastGlitchCaptureAttempt <= COOLDOWN_WINDOW){
+ return;
+ }
+
+ final int windowBegin = recordedGlitch - GLITCH_CONCENTRATION_WINDOW_SIZE;
+
+ int numGlitches = 0;
+ for (int index = mGlitchesIndex-1; index >= 0 && mGlitches[index] >= windowBegin; --index){
+ ++numGlitches;
+ }
+
+ int captureResponse = mCaptureHolder.captureState(numGlitches);
+ if (captureResponse != CaptureHolder.NEW_CAPTURE_IS_LEAST_INTERESTING){
+ mLastGlitchCaptureAttempt = recordedGlitch;
+ }
+
+ }
/** Compute the center of mass of fftResults. Width is the width of each beam. */
private double computeCenterOfMass(double[] fftResult, double width) {
diff --git a/LoopbackApp/app/src/main/java/org/drrickorang/loopback/GlitchesStringBuilder.java b/LoopbackApp/app/src/main/java/org/drrickorang/loopback/GlitchesStringBuilder.java
index a1770e3..535d991 100644
--- a/LoopbackApp/app/src/main/java/org/drrickorang/loopback/GlitchesStringBuilder.java
+++ b/LoopbackApp/app/src/main/java/org/drrickorang/loopback/GlitchesStringBuilder.java
@@ -83,6 +83,23 @@ public class GlitchesStringBuilder {
return listOfGlitches.toString();
}
+ /** Generate array of Glitch Times in ms */
+ public static int[] getGlitchMilliseconds(int fftSamplingSize, int FFTOverlapSamples,
+ int[] glitchesData, int samplingRate) {
+ int[] glitchMilliseconds = new int[glitchesData.length];
+ int newSamplesPerFFT = fftSamplingSize - FFTOverlapSamples;
+
+ // the time span of new samples for a single FFT in ms
+ double newSamplesInMs = ((double) newSamplesPerFFT / samplingRate) *
+ Constant.MILLIS_PER_SECOND;
+
+ for (int i = 0; i < glitchesData.length; i++) {
+ glitchMilliseconds[i] = (int) (glitchesData[i] * newSamplesInMs); // round down
+ }
+
+ return glitchMilliseconds;
+ }
+
private static void log(String msg) {
Log.v(TAG, msg);
}
diff --git a/LoopbackApp/app/src/main/java/org/drrickorang/loopback/HistogramView.java b/LoopbackApp/app/src/main/java/org/drrickorang/loopback/HistogramView.java
index 2a2d1fc..1055168 100644
--- a/LoopbackApp/app/src/main/java/org/drrickorang/loopback/HistogramView.java
+++ b/LoopbackApp/app/src/main/java/org/drrickorang/loopback/HistogramView.java
@@ -39,9 +39,6 @@ public class HistogramView extends View {
private Paint mLinePaint;
private Paint mXLabelPaint;
- private int[] mTimeStampData;
- private int[] mDisplayTimeStampData;
-
private int[] mData; // data for buffer period
private int[] mDisplayData; // modified data that is used to draw histogram
private int mMaxBufferPeriod = 0;
@@ -165,36 +162,8 @@ public class HistogramView extends View {
mDisplayData[range - 1] += mData[i];
}
}
-
- // for timestamp
- if (mTimeStampData != null) {
- mDisplayTimeStampData = new int[mMaxNumberOfBeams];
- mDisplayTimeStampData[0] = mTimeStampData[0];
- // find the max timestamp of each bucket.
- for (int i = 1; i < (range - 1); i++) {
- int maxTimeStamp = mTimeStampData[(((i - 1) * interval) + 1)];
- for (int j = (((i - 1) * interval) + 1); (j <= (i * interval)); j++) {
- if (mTimeStampData[j] > maxTimeStamp)
- maxTimeStamp = mTimeStampData[j];
- }
- mDisplayTimeStampData[i] = maxTimeStamp;
- }
- if (exceedBufferPeriodRange) {
- mDisplayTimeStampData[range - 1] = mTimeStampData[oldRange - 1];
- } else {
- int maxTimeStamp = mTimeStampData[(((range - 2) * interval) + 1)];
- for (int i = (((range - 2) * interval) + 1); i < oldRange; i++) {
- if (mTimeStampData[i] > maxTimeStamp) {
- maxTimeStamp = mTimeStampData[i];
- }
- }
- mDisplayTimeStampData[range - 1] = maxTimeStamp;
- }
- }
-
} else {
mDisplayData = mData;
- mDisplayTimeStampData = mTimeStampData;
}
// calculate the max frequency among all latencies
@@ -359,26 +328,6 @@ public class HistogramView extends View {
return width;
}
-
- /** Copy timestamp data into "mTimeStampData" */
- public void setBufferPeriodTimeStampArray(int[] timeStamp) {
- if (timeStamp == null) {
- return;
- }
-
- if (mTimeStampData == null || timeStamp.length != mTimeStampData.length) {
- mTimeStampData = new int[timeStamp.length];
- }
-
- System.arraycopy(timeStamp, 0, mTimeStampData, 0, timeStamp.length);
- }
-
-
- public int[] getBufferPeriodDisplayTimeStampArray() {
- return mDisplayTimeStampData;
- }
-
-
/** Copy buffer period data into "mData" */
public void setBufferPeriodArray(int[] data) {
if (data == null) {
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 5eb6d7b..36fbe71 100644
--- a/LoopbackApp/app/src/main/java/org/drrickorang/loopback/LoopbackActivity.java
+++ b/LoopbackApp/app/src/main/java/org/drrickorang/loopback/LoopbackActivity.java
@@ -32,9 +32,7 @@ import android.content.pm.PackageManager;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.Canvas;
-import android.media.AudioFormat;
import android.media.AudioManager;
-import android.media.MediaRecorder;
import android.net.Uri;
import android.os.Bundle;
import android.os.Build;
@@ -78,22 +76,35 @@ public class LoopbackActivity extends Activity
private static final int SAVE_PLAYER_BUFFER_PERIOD_TO_TXT_REQUEST = 46;
private static final int SAVE_RECORDER_BUFFER_PERIOD_TO_PNG_REQUEST = 47;
private static final int SAVE_PLAYER_BUFFER_PERIOD_TO_PNG_REQUEST = 48;
- private static final int SAVE_GLITCH_OCCURRENCES_TO_TEXT_REQUEST = 49;
- private static final int SETTINGS_ACTIVITY_REQUEST_CODE = 54;
+ private static final int SAVE_RECORDER_BUFFER_PERIOD_TIMES_TO_TXT_REQUEST = 49;
+ private static final int SAVE_PLAYER_BUFFER_PERIOD_TIMES_TO_TXT_REQUEST = 50;
+ private static final int SAVE_GLITCH_OCCURRENCES_TO_TEXT_REQUEST = 51;
+ private static final int SAVE_GLITCH_AND_CALLBACK_HEATMAP_REQUEST = 52;
+
+ private static final int SETTINGS_ACTIVITY_REQUEST = 54;
+
private static final int THREAD_SLEEP_DURATION_MS = 200;
- private static final int PERMISSIONS_REQUEST_RECORD_AUDIO = 201;
- private static final int PERMISSIONS_REQUEST_WRITE_EXTERNAL_STORAGE = 202;
+ private static final int PERMISSIONS_REQUEST_RECORD_AUDIO_LATENCY = 201;
+ private static final int PERMISSIONS_REQUEST_RECORD_AUDIO_BUFFER = 202;
+ private static final int PERMISSIONS_REQUEST_WRITE_EXTERNAL_STORAGE = 203;
private static final int LATENCY_TEST_STARTED = 300;
private static final int LATENCY_TEST_ENDED = 301;
private static final int BUFFER_TEST_STARTED = 302;
private static final int BUFFER_TEST_ENDED = 303;
+
+ // 0-100 controls compression rate, currently ignore because PNG format is being used
+ private static final int EXPORTED_IMAGE_QUALITY = 100;
+
private static final int HISTOGRAM_EXPORT_WIDTH = 2000;
private static final int HISTOGRAM_EXPORT_HEIGHT = 2000;
+ private static final int HEATMAP_DRAW_WIDTH = 2560;
+ private static final int HEATMAP_DRAW_HEIGHT = 1440;
+ private static final int HEATMAP_EXPORT_DIVISOR = 2;
LoopbackAudioThread mAudioThread = null;
NativeAudioThread mNativeAudioThread = null;
private WavePlotView mWavePlotView;
- private String mCurrentTime = "IncorrectTime"; // The time the plot is acquired
+ private String mTestStartTimeString = "IncorrectTime"; // The time the test begins
private static final String FILE_SAVE_PATH = "file://mnt/sdcard/";
private SeekBar mBarMasterLevel; // drag the volume
@@ -113,6 +124,8 @@ public class LoopbackActivity extends Activity
private int mNativeRecorderMaxBufferPeriod;
private int[] mNativePlayerBufferPeriodArray;
private int mNativePlayerMaxBufferPeriod;
+ private BufferCallbackTimes mRecorderCallbackTimes;
+ private BufferCallbackTimes mPlayerCallbackTimes;
private static final String INTENT_SAMPLING_FREQUENCY = "SF";
private static final String INTENT_CHANNEL_INDEX = "CI";
@@ -125,6 +138,10 @@ public class LoopbackActivity extends Activity
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";
+ private static final String INTENT_ENABLE_SYSTRACE = "CaptureSysTrace";
+ private static final String INTENT_ENABLE_WAVCAPTURE = "CaptureWavs";
+ private static final String INTENT_NUM_CAPTURES = "NumCaptures";
+ private static final String INTENT_WAV_DURATION = "WavDuration";
// for running the test using adb command
private boolean mIntentRunning = false; // if it is running triggered by intent with parameters
@@ -186,12 +203,12 @@ public class LoopbackActivity extends Activity
case LoopbackAudioThread.LOOPBACK_AUDIO_THREAD_MESSAGE_LATENCY_REC_COMPLETE:
if (mAudioThread != null) {
mWaveData = mAudioThread.getWaveData();
+ mRecorderCallbackTimes = mRecorderBufferPeriod.getCallbackTimes();
+ mPlayerCallbackTimes = mPlayerBufferPeriod.getCallbackTimes();
mCorrelation.computeCorrelation(mWaveData, mSamplingRate);
log("got message java latency rec complete!!");
refreshPlots();
refreshState();
- mCurrentTime = (String) DateFormat.format("MMddkkmmss",
- System.currentTimeMillis());
switch (msg.what) {
case LoopbackAudioThread.LOOPBACK_AUDIO_THREAD_MESSAGE_LATENCY_REC_STOP:
@@ -234,13 +251,14 @@ public class LoopbackActivity extends Activity
mGlitchingIntervalTooLong = mAudioThread.getGlitchingIntervalTooLong();
mFFTSamplingSize = mAudioThread.getFFTSamplingSize();
mFFTOverlapSamples = mAudioThread.getFFTOverlapSamples();
+ mRecorderCallbackTimes = mRecorderBufferPeriod.getCallbackTimes();
+ mPlayerCallbackTimes = mPlayerBufferPeriod.getCallbackTimes();
refreshPlots(); // only plot that last few seconds
refreshState();
- mCurrentTime = (String) DateFormat.format("MMddkkmmss",
- System.currentTimeMillis());
+ //rounded up number of seconds elapsed
mBufferTestElapsedSeconds =
- (int) ((System.currentTimeMillis() - mBufferTestStartTime)
- / Constant.MILLIS_PER_SECOND);
+ (int) ((System.currentTimeMillis() - mBufferTestStartTime +
+ Constant.MILLIS_PER_SECOND - 1) / Constant.MILLIS_PER_SECOND);
switch (msg.what) {
case LoopbackAudioThread.LOOPBACK_AUDIO_THREAD_MESSAGE_BUFFER_REC_STOP:
showToast("Java Buffer Test Stopped");
@@ -249,7 +267,9 @@ public class LoopbackActivity extends Activity
showToast("Java Buffer Test Completed");
break;
}
-
+ if (getApp().isCaptureSysTraceEnabled()) {
+ CaptureHolder.stopLoopbackListenerScript();
+ }
stopAudioTestThreads();
if (mIntentRunning && mIntentFileName != null && mIntentFileName.length() > 0) {
saveAllTo(mIntentFileName);
@@ -304,6 +324,8 @@ public class LoopbackActivity extends Activity
getRecorderMaxBufferPeriod();
mNativePlayerBufferPeriodArray = mNativeAudioThread.getPlayerBufferPeriod();
mNativePlayerMaxBufferPeriod = mNativeAudioThread.getPlayerMaxBufferPeriod();
+ mRecorderCallbackTimes = mNativeAudioThread.getRecorderCallbackTimes();
+ mPlayerCallbackTimes = mNativeAudioThread.getPlayerCallbackTimes();
if (msg.what != NativeAudioThread.
LOOPBACK_NATIVE_AUDIO_THREAD_MESSAGE_BUFFER_REC_COMPLETE) {
@@ -313,11 +335,10 @@ public class LoopbackActivity extends Activity
log("got message native buffer test rec complete!!");
refreshPlots();
refreshState();
- mCurrentTime = (String) DateFormat.format("MMddkkmmss",
- System.currentTimeMillis());
+ //rounded up number of seconds elapsed
mBufferTestElapsedSeconds =
- (int) ((System.currentTimeMillis() - mBufferTestStartTime)
- / Constant.MILLIS_PER_SECOND);
+ (int) ((System.currentTimeMillis() - mBufferTestStartTime +
+ Constant.MILLIS_PER_SECOND - 1) / Constant.MILLIS_PER_SECOND);
switch (msg.what) {
case NativeAudioThread.
LOOPBACK_NATIVE_AUDIO_THREAD_MESSAGE_BUFFER_REC_COMPLETE_ERRORS:
@@ -344,6 +365,9 @@ public class LoopbackActivity extends Activity
}
+ if (getApp().isCaptureSysTraceEnabled()) {
+ CaptureHolder.stopLoopbackListenerScript();
+ }
refreshSoundLevelBar();
break;
default:
@@ -401,6 +425,11 @@ public class LoopbackActivity extends Activity
View view = getLayoutInflater().inflate(R.layout.main_activity, null);
setContentView(view);
+ boolean successfulWrite = AtraceScriptsWriter.writeScriptsToFile(this);
+ if(!successfulWrite) {
+ showToast("Unable to write loopback_listener script to device");
+ }
+
mTextInfo = (TextView) findViewById(R.id.textInfo);
mBarMasterLevel = (SeekBar) findViewById(R.id.BarMasterLevel);
@@ -484,6 +513,8 @@ public class LoopbackActivity extends Activity
// --ei SF 48000 --es FileName test1 --ei RecorderBuffer 512 --ei PlayerBuffer 512
// --ei AudioThread 1 --ei MicSource 3 --ei AudioLevel 12
// --ei TestType 223 --ei BufferTestDuration 60 --ei NumLoadThreads 4
+ // --ei CI -1 --ez CaptureSysTrace true --ez CaptureWavs false --ei NumCaptures 5
+ // --ei WavDuration 15
// Note: for native mode, player and recorder buffer sizes are the same, and can only be
// set through player buffer size
@@ -547,6 +578,26 @@ public class LoopbackActivity extends Activity
mIntentRunning = true;
}
+ if (b.containsKey(INTENT_ENABLE_SYSTRACE)) {
+ getApp().setCaptureSysTraceEnabled(b.getBoolean(INTENT_ENABLE_SYSTRACE));
+ mIntentRunning = true;
+ }
+
+ if (b.containsKey(INTENT_ENABLE_WAVCAPTURE)) {
+ getApp().setCaptureWavsEnabled(b.getBoolean(INTENT_ENABLE_WAVCAPTURE));
+ mIntentRunning = true;
+ }
+
+ if (b.containsKey(INTENT_NUM_CAPTURES)) {
+ getApp().setNumberOfCaptures(b.getInt(INTENT_NUM_CAPTURES));
+ mIntentRunning = true;
+ }
+
+ if (b.containsKey(INTENT_WAV_DURATION)) {
+ getApp().setBufferTestWavePlotDuration(b.getInt(INTENT_WAV_DURATION));
+ mIntentRunning = true;
+ }
+
if (mIntentRunning || b.containsKey(INTENT_TEST_TYPE)) {
// run tests with provided or default parameters
refreshState();
@@ -648,7 +699,7 @@ public class LoopbackActivity extends Activity
// Launch settings activity
Intent mySettingsIntent = new Intent(this, SettingsActivity.class);
- startActivityForResult(mySettingsIntent, SETTINGS_ACTIVITY_REQUEST_CODE);
+ startActivityForResult(mySettingsIntent, SETTINGS_ACTIVITY_REQUEST);
} else {
showToast("Test in progress... please wait");
}
@@ -686,10 +737,16 @@ public class LoopbackActivity extends Activity
mChannelIndex = getApp().getChannelIndex();
mPlayerBufferSizeInBytes = getApp().getPlayerBufferSizeInBytes();
mRecorderBufferSizeInBytes = getApp().getRecorderBufferSizeInBytes();
+ mTestStartTimeString = (String) DateFormat.format("MMddkkmmss",
+ System.currentTimeMillis());
int micSource = getApp().getMicSource();
int bufferTestDurationInSeconds = getApp().getBufferTestDuration();
int bufferTestWavePlotDurationInSeconds = getApp().getBufferTestWavePlotDuration();
+ CaptureHolder captureHolder = new CaptureHolder(getApp().getNumStateCaptures(),
+ getFileNamePrefix(), getApp().isCaptureWavSnippetsEnabled(),
+ getApp().isCaptureSysTraceEnabled(), this, mSamplingRate);
+
log(" current sampling rate: " + mSamplingRate);
stopAudioTestThreads();
@@ -699,11 +756,25 @@ public class LoopbackActivity extends Activity
case Constant.AUDIO_THREAD_TYPE_JAVA:
micSourceMapped = getApp().mapMicSource(Constant.AUDIO_THREAD_TYPE_JAVA, micSource);
+ int expectedRecorderBufferPeriod = Math.round(
+ (float) (mRecorderBufferSizeInBytes * Constant.MILLIS_PER_SECOND)
+ / (Constant.BYTES_PER_FRAME * mSamplingRate));
+ mRecorderBufferPeriod.prepareMemberObjects(
+ Constant.MAX_RECORDED_LATE_CALLBACKS_PER_SECOND * bufferTestDurationInSeconds,
+ expectedRecorderBufferPeriod, captureHolder);
+
+ int expectedPlayerBufferPeriod = Math.round(
+ (float) (mPlayerBufferSizeInBytes * Constant.MILLIS_PER_SECOND)
+ / (Constant.BYTES_PER_FRAME * mSamplingRate));
+ mPlayerBufferPeriod.prepareMemberObjects(
+ Constant.MAX_RECORDED_LATE_CALLBACKS_PER_SECOND * bufferTestDurationInSeconds,
+ expectedPlayerBufferPeriod, captureHolder);
+
mAudioThread = new LoopbackAudioThread(mSamplingRate, mPlayerBufferSizeInBytes,
mRecorderBufferSizeInBytes, micSourceMapped, mRecorderBufferPeriod,
mPlayerBufferPeriod, mTestType, bufferTestDurationInSeconds,
bufferTestWavePlotDurationInSeconds, getApplicationContext(),
- mChannelIndex);
+ mChannelIndex, captureHolder);
mAudioThread.setMessageHandler(mMessageHandler);
mAudioThread.mSessionId = sessionId;
mAudioThread.start();
@@ -714,7 +785,8 @@ public class LoopbackActivity extends Activity
// size = player buffer size in native mode
mNativeAudioThread = new NativeAudioThread(mSamplingRate, mPlayerBufferSizeInBytes,
mRecorderBufferSizeInBytes, micSourceMapped, mTestType,
- bufferTestDurationInSeconds, bufferTestWavePlotDurationInSeconds);
+ bufferTestDurationInSeconds, bufferTestWavePlotDurationInSeconds,
+ captureHolder);
mNativeAudioThread.setMessageHandler(mMessageHandler);
mNativeAudioThread.mSessionId = sessionId;
mNativeAudioThread.start();
@@ -824,7 +896,7 @@ public class LoopbackActivity extends Activity
if (hasRecordAudioPermission()) {
startLatencyTest();
} else {
- requestRecordAudioPermission();
+ requestRecordAudioPermission(PERMISSIONS_REQUEST_RECORD_AUDIO_LATENCY);
}
}
@@ -869,7 +941,7 @@ public class LoopbackActivity extends Activity
if (hasRecordAudioPermission()) {
startBufferTest();
} else {
- requestRecordAudioPermission();
+ requestRecordAudioPermission(PERMISSIONS_REQUEST_RECORD_AUDIO_BUFFER);
}
}
@@ -946,7 +1018,7 @@ public class LoopbackActivity extends Activity
*/
private void SaveFilesWithDialog() {
- String fileName = "loopback_" + mCurrentTime;
+ String fileName = "loopback_" + mTestStartTimeString;
//Launch filename choosing activities if available, otherwise save without prompting
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
@@ -955,15 +1027,22 @@ public class LoopbackActivity extends Activity
launchFileNameChoosingActivity("audio/wav", fileName, ".wav", SAVE_TO_WAVE_REQUEST);
launchFileNameChoosingActivity("text/plain", fileName, "_recorderBufferPeriod.txt",
SAVE_RECORDER_BUFFER_PERIOD_TO_TXT_REQUEST);
+ launchFileNameChoosingActivity("text/plain", fileName, "_recorderBufferPeriodTimes.txt",
+ SAVE_RECORDER_BUFFER_PERIOD_TIMES_TO_TXT_REQUEST);
launchFileNameChoosingActivity("image/png", fileName, "_recorderBufferPeriod.png",
SAVE_RECORDER_BUFFER_PERIOD_TO_PNG_REQUEST);
launchFileNameChoosingActivity("text/plain", fileName, "_playerBufferPeriod.txt",
SAVE_PLAYER_BUFFER_PERIOD_TO_TXT_REQUEST);
+ launchFileNameChoosingActivity("text/plain", fileName, "_playerBufferPeriodTimes.txt",
+ SAVE_PLAYER_BUFFER_PERIOD_TIMES_TO_TXT_REQUEST);
launchFileNameChoosingActivity("image/png", fileName, "_playerBufferPeriod.png",
SAVE_PLAYER_BUFFER_PERIOD_TO_PNG_REQUEST);
+
if (mGlitchesData != null) {
launchFileNameChoosingActivity("text/plain", fileName, "_glitchMillis.txt",
SAVE_GLITCH_OCCURRENCES_TO_TEXT_REQUEST);
+ launchFileNameChoosingActivity("image/png", fileName, "_heatMap.png",
+ SAVE_GLITCH_AND_CALLBACK_HEATMAP_REQUEST);
}
} else {
saveAllTo(fileName);
@@ -982,6 +1061,13 @@ public class LoopbackActivity extends Activity
startActivityForResult(FilenameIntent, RequestCode);
}
+ private String getFileNamePrefix(){
+ if (mIntentFileName != null && !mIntentFileName.isEmpty()) {
+ return mIntentFileName;
+ } else {
+ return "loopback_" + mTestStartTimeString;
+ }
+ }
/** See the documentation on onButtonSave() */
public void saveAllTo(String fileName) {
@@ -998,7 +1084,7 @@ public class LoopbackActivity extends Activity
saveScreenShot(Uri.parse(FILE_SAVE_PATH + fileName + ".png"));
- saveReport(Uri.parse(FILE_SAVE_PATH + fileName + ".txt"));
+ saveTextToFile(Uri.parse(FILE_SAVE_PATH + fileName + ".txt"), getReport().toString());
int[] bufferPeriodArray = null;
int maxBufferPeriod = Constant.UNKNOWN;
@@ -1016,6 +1102,8 @@ public class LoopbackActivity extends Activity
bufferPeriodArray, maxBufferPeriod);
saveHistogram(Uri.parse(FILE_SAVE_PATH + fileName + "_recorderBufferPeriod.png"),
bufferPeriodArray, maxBufferPeriod);
+ saveTextToFile(Uri.parse(FILE_SAVE_PATH + fileName + "_recorderBufferPeriodTimes.txt"),
+ mRecorderCallbackTimes.toString());
bufferPeriodArray = null;
maxBufferPeriod = Constant.UNKNOWN;
@@ -1029,14 +1117,21 @@ public class LoopbackActivity extends Activity
maxBufferPeriod = mNativePlayerMaxBufferPeriod;
break;
}
- saveBufferPeriod(Uri.parse(FILE_SAVE_PATH + fileName + "_playerBufferPeriod.txt")
+ saveBufferPeriod(Uri.parse(FILE_SAVE_PATH + fileName + "_playerBufferPeriod.txt")
, bufferPeriodArray, maxBufferPeriod);
saveHistogram(Uri.parse(FILE_SAVE_PATH + fileName + "_playerBufferPeriod.png"),
bufferPeriodArray, maxBufferPeriod);
+ saveTextToFile(Uri.parse(FILE_SAVE_PATH + fileName + "_playerBufferPeriodTimes.txt"),
+ mPlayerCallbackTimes.toString());
if (mGlitchesData != null) {
saveGlitchOccurrences(Uri.parse(FILE_SAVE_PATH + fileName + "_glitchMillis.txt"),
mGlitchesData);
+ saveHeatMap(Uri.parse(FILE_SAVE_PATH + fileName + "_heatMap.png"),
+ mRecorderCallbackTimes, mPlayerCallbackTimes,
+ GlitchesStringBuilder.getGlitchMilliseconds(mFFTSamplingSize,
+ mFFTOverlapSamples, mGlitchesData, mSamplingRate),
+ mGlitchingIntervalTooLong, mBufferTestElapsedSeconds, fileName);
}
}
@@ -1062,7 +1157,7 @@ public class LoopbackActivity extends Activity
break;
case SAVE_TO_TXT_REQUEST:
if (resultData != null) {
- saveReport(resultData.getData());
+ saveTextToFile(resultData.getData(), getReport().toString());
}
break;
case SAVE_RECORDER_BUFFER_PERIOD_TO_TXT_REQUEST:
@@ -1133,12 +1228,33 @@ public class LoopbackActivity extends Activity
saveHistogram(resultData.getData(), bufferPeriodArray, maxBufferPeriod);
}
break;
+ case SAVE_PLAYER_BUFFER_PERIOD_TIMES_TO_TXT_REQUEST:
+ if (resultData != null) {
+ saveTextToFile(resultData.getData(),
+ mPlayerCallbackTimes.toString());
+ }
+ break;
+ case SAVE_RECORDER_BUFFER_PERIOD_TIMES_TO_TXT_REQUEST:
+ if (resultData != null) {
+ saveTextToFile(resultData.getData(),
+ mRecorderCallbackTimes.toString());
+ }
+ break;
case SAVE_GLITCH_OCCURRENCES_TO_TEXT_REQUEST:
if (resultData != null) {
saveGlitchOccurrences(resultData.getData(), mGlitchesData);
}
break;
- case SETTINGS_ACTIVITY_REQUEST_CODE:
+ case SAVE_GLITCH_AND_CALLBACK_HEATMAP_REQUEST:
+ if (resultData != null && mGlitchesData != null && mRecorderCallbackTimes != null
+ & mPlayerCallbackTimes != null){
+ saveHeatMap(resultData.getData(), mRecorderCallbackTimes, mPlayerCallbackTimes,
+ GlitchesStringBuilder.getGlitchMilliseconds(mFFTSamplingSize,
+ mFFTOverlapSamples, mGlitchesData, mSamplingRate),
+ mGlitchingIntervalTooLong, mBufferTestElapsedSeconds,
+ resultData.getData().toString());
+ }
+ case SETTINGS_ACTIVITY_REQUEST:
log("return from new settings!");
// here we wipe out all previous results, in order to avoid the condition where
@@ -1167,10 +1283,10 @@ public class LoopbackActivity extends Activity
/** Reset all results gathered from previous round of test (if any). */
private void resetResults() {
mCorrelation.invalidate();
- mRecorderBufferPeriod.resetRecord();
- mPlayerBufferPeriod.resetRecord();
mNativeRecorderBufferPeriodArray = null;
mNativePlayerBufferPeriodArray = null;
+ mPlayerCallbackTimes = null;
+ mRecorderCallbackTimes = null;
mGlitchesData = null;
mWaveData = null;
}
@@ -1228,15 +1344,12 @@ public class LoopbackActivity extends Activity
switch (mAudioThreadType) {
case Constant.AUDIO_THREAD_TYPE_JAVA:
- RecorderBufferPeriodIntent.putExtra("recorderBufferPeriodTimeStampArray",
- mRecorderBufferPeriod.getBufferPeriodTimeStampArray());
RecorderBufferPeriodIntent.putExtra("recorderBufferPeriodArray",
mRecorderBufferPeriod.getBufferPeriodArray());
RecorderBufferPeriodIntent.putExtra("recorderBufferPeriodMax",
mRecorderBufferPeriod.getMaxBufferPeriod());
break;
case Constant.AUDIO_THREAD_TYPE_NATIVE:
- // TODO change code in sles.cpp to collect timeStamp in native mode as well
RecorderBufferPeriodIntent.putExtra("recorderBufferPeriodArray",
mNativeRecorderBufferPeriodArray);
RecorderBufferPeriodIntent.putExtra("recorderBufferPeriodMax",
@@ -1260,15 +1373,12 @@ public class LoopbackActivity extends Activity
switch (mAudioThreadType) {
case Constant.AUDIO_THREAD_TYPE_JAVA:
- PlayerBufferPeriodIntent.putExtra("playerBufferPeriodTimeStampArray",
- mPlayerBufferPeriod.getBufferPeriodTimeStampArray());
PlayerBufferPeriodIntent.putExtra("playerBufferPeriodArray",
mPlayerBufferPeriod.getBufferPeriodArray());
PlayerBufferPeriodIntent.putExtra("playerBufferPeriodMax",
mPlayerBufferPeriod.getMaxBufferPeriod());
break;
case Constant.AUDIO_THREAD_TYPE_NATIVE:
- // TODO change code in sles.cpp to collect timeStamp in native mode as well
PlayerBufferPeriodIntent.putExtra("playerBufferPeriodArray",
mNativePlayerBufferPeriodArray);
PlayerBufferPeriodIntent.putExtra("playerBufferPeriodMax",
@@ -1339,6 +1449,37 @@ public class LoopbackActivity extends Activity
}
}
+ /** Display pop up window of recorded metrics and system information */
+ public void onButtonHeatMap(View view) {
+ if (!isBusy()) {
+ if (mTestType == Constant.LOOPBACK_PLUG_AUDIO_THREAD_TEST_TYPE_BUFFER_PERIOD
+ && mGlitchesData != null && mRecorderCallbackTimes != null
+ && mRecorderCallbackTimes != null) {
+
+ // Create a PopUpWindow with heatMap custom view
+ View puLayout = this.getLayoutInflater().inflate(R.layout.heatmap_window, null);
+ PopupWindow popUp = new PopupWindow(puLayout, ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.MATCH_PARENT, true);
+
+ ((LinearLayout) popUp.getContentView()).addView(
+ new GlitchAndCallbackHeatMapView(this, mRecorderCallbackTimes,
+ mPlayerCallbackTimes,
+ GlitchesStringBuilder.getGlitchMilliseconds(mFFTSamplingSize,
+ mFFTOverlapSamples, mGlitchesData, mSamplingRate),
+ mGlitchingIntervalTooLong, mBufferTestElapsedSeconds,
+ getResources().getString(R.string.heatTitle)));
+
+ popUp.showAtLocation(findViewById(R.id.linearLayoutMain), Gravity.TOP, 0, 0);
+
+ } else {
+ showToast("Please run the tests to get data");
+ }
+
+ } else {
+ showToast("Test in progress... please wait");
+ }
+ }
+
/** Redraw the plot according to mWaveData */
void refreshPlots() {
mWavePlotView.setData(mWaveData);
@@ -1502,8 +1643,35 @@ public class LoopbackActivity extends Activity
}
}
- /** Save a screenshot of the main activity. */
private void saveHistogram(Uri uri, int[] bufferPeriodArray, int maxBufferPeriod) {
+ // Create and histogram view bitmap
+ HistogramView recordHisto = new HistogramView(this,null);
+ recordHisto.setBufferPeriodArray(bufferPeriodArray);
+ recordHisto.setMaxBufferPeriod(maxBufferPeriod);
+
+ // Draw histogram on bitmap canvas
+ Bitmap histoBmp = Bitmap.createBitmap(HISTOGRAM_EXPORT_WIDTH,
+ HISTOGRAM_EXPORT_HEIGHT, Bitmap.Config.ARGB_8888); // creates a MUTABLE bitmap
+ recordHisto.fillCanvas(new Canvas(histoBmp), histoBmp.getWidth(), histoBmp.getHeight());
+
+ saveImage(uri, histoBmp);
+ }
+
+ private void saveHeatMap(Uri uri, BufferCallbackTimes recorderCallbackTimes,
+ BufferCallbackTimes playerCallbackTimes, int[] glitchMilliseconds,
+ boolean glitchesExceeded, int duration, String title) {
+ Bitmap heatBmp = Bitmap.createBitmap(HEATMAP_DRAW_WIDTH, HEATMAP_DRAW_HEIGHT,
+ Bitmap.Config.ARGB_8888);
+ GlitchAndCallbackHeatMapView.fillCanvas(new Canvas(heatBmp), recorderCallbackTimes,
+ playerCallbackTimes, glitchMilliseconds, glitchesExceeded, duration,
+ title);
+ saveImage(uri, Bitmap.createScaledBitmap(heatBmp,
+ HEATMAP_DRAW_WIDTH / HEATMAP_EXPORT_DIVISOR,
+ HEATMAP_DRAW_HEIGHT / HEATMAP_EXPORT_DIVISOR, false));
+ }
+
+ /** Save an image to file. */
+ private void saveImage(Uri uri, Bitmap bmp) {
ParcelFileDescriptor parcelFileDescriptor = null;
FileOutputStream outputStream;
try {
@@ -1515,19 +1683,8 @@ public class LoopbackActivity extends Activity
log("Done creating output stream");
- // Create and save histogram view
- HistogramView recordHisto = new HistogramView(this,null);
- recordHisto.setBufferPeriodArray(bufferPeriodArray);
- recordHisto.setMaxBufferPeriod(maxBufferPeriod);
-
- // Draw histogram on bitmap canvas
- Bitmap histoBmp = Bitmap.createBitmap(HISTOGRAM_EXPORT_WIDTH,
- HISTOGRAM_EXPORT_HEIGHT, Bitmap.Config.ARGB_8888); // creates a MUTABLE bitmap
- Canvas canvas = new Canvas(histoBmp);
- recordHisto.fillCanvas(canvas, histoBmp.getWidth(), histoBmp.getHeight());
-
// Save compressed bitmap to file
- histoBmp.compress(Bitmap.CompressFormat.PNG, 100, outputStream);
+ bmp.compress(Bitmap.CompressFormat.PNG, EXPORTED_IMAGE_QUALITY, outputStream);
parcelFileDescriptor.close();
} catch (Exception e) {
log("Failed to open png file " + e);
@@ -1564,10 +1721,10 @@ public class LoopbackActivity extends Activity
int[] usefulBufferData = Arrays.copyOfRange(bufferPeriodArray, 0, usefulDataRange);
String endline = "\n";
- String tab = "\t";
+ String delimiter = ",";
StringBuilder sb = new StringBuilder();
for (int i = 0; i < usefulBufferData.length; i++) {
- sb.append(i + tab + usefulBufferData[i] + endline);
+ sb.append(i + delimiter + usefulBufferData[i] + endline);
}
outputStream.write(sb.toString().getBytes());
@@ -1589,7 +1746,7 @@ public class LoopbackActivity extends Activity
}
/** Save a .txt file of various test results. */
- void saveReport(Uri uri) {
+ void saveTextToFile(Uri uri, String outputText) {
ParcelFileDescriptor parcelFileDescriptor = null;
FileOutputStream outputStream;
try {
@@ -1600,7 +1757,7 @@ public class LoopbackActivity extends Activity
outputStream = new FileOutputStream(fileDescriptor);
log("Done creating output stream");
- outputStream.write(getReport().toString().getBytes());
+ outputStream.write(outputText.getBytes());
parcelFileDescriptor.close();
} catch (Exception e) {
log("Failed to open text file " + e);
@@ -1621,7 +1778,7 @@ public class LoopbackActivity extends Activity
String endline = "\n";
final int stringLength = 300;
StringBuilder sb = new StringBuilder(stringLength);
- sb.append("DateTime = " + mCurrentTime + endline);
+ sb.append("DateTime = " + mTestStartTimeString + endline);
sb.append(INTENT_SAMPLING_FREQUENCY + " = " + getApp().getSamplingRate() + endline);
sb.append(INTENT_RECORDER_BUFFER + " = " + getApp().getRecorderBufferSizeInBytes() /
Constant.BYTES_PER_FRAME + endline);
@@ -1665,14 +1822,7 @@ public class LoopbackActivity extends Activity
case Constant.LOOPBACK_PLUG_AUDIO_THREAD_TEST_TYPE_BUFFER_PERIOD:
sb.append("Buffer Test Duration (s) = " + mBufferTestElapsedSeconds + endline);
- // report expected recorder buffer period
- int expectedRecorderBufferPeriod = mRecorderBufferSizeInBytes /
- Constant.BYTES_PER_FRAME * Constant.MILLIS_PER_SECOND / mSamplingRate;
- sb.append("Expected Recorder Buffer Period (ms) = " + expectedRecorderBufferPeriod +
- endline);
-
// report recorder results
- int recorderBufferSize = mRecorderBufferSizeInBytes / Constant.BYTES_PER_FRAME;
int[] recorderBufferData = null;
int recorderBufferDataMax = 0;
switch (mAudioThreadType) {
@@ -1685,6 +1835,7 @@ public class LoopbackActivity extends Activity
recorderBufferDataMax = mNativeRecorderMaxBufferPeriod;
break;
}
+ // report expected recorder buffer period
if (recorderBufferData != null) {
// this is the range of data that actually has values
int usefulDataRange = Math.min(recorderBufferDataMax + 1,
@@ -1692,11 +1843,13 @@ public class LoopbackActivity extends Activity
int[] usefulBufferData = Arrays.copyOfRange(recorderBufferData, 0,
usefulDataRange);
PerformanceMeasurement measurement = new PerformanceMeasurement(
- recorderBufferSize, mSamplingRate, usefulBufferData);
+ mRecorderCallbackTimes.getExpectedBufferPeriod(), usefulBufferData);
float recorderPercentAtExpected =
measurement.percentBufferPeriodsAtExpected();
double benchmark = measurement.computeWeightedBenchmark();
int outliers = measurement.countOutliers();
+ sb.append("Expected Recorder Buffer Period (ms) = " +
+ mRecorderCallbackTimes.getExpectedBufferPeriod() + endline);
sb.append("Recorder Buffer Periods At Expected = " +
String.format("%.5f%%", recorderPercentAtExpected * 100) + endline);
@@ -1711,7 +1864,6 @@ public class LoopbackActivity extends Activity
}
// report player results
- int playerBufferSize = mPlayerBufferSizeInBytes / Constant.BYTES_PER_FRAME;
int[] playerBufferData = null;
int playerBufferDataMax = 0;
switch (mAudioThreadType) {
@@ -1724,6 +1876,9 @@ public class LoopbackActivity extends Activity
playerBufferDataMax = mNativePlayerMaxBufferPeriod;
break;
}
+ // report expected player buffer period
+ sb.append("Expected Player Buffer Period (ms) = " +
+ mPlayerCallbackTimes.getExpectedBufferPeriod() + endline);
if (playerBufferData != null) {
// this is the range of data that actually has values
int usefulDataRange = Math.min(playerBufferDataMax + 1,
@@ -1731,7 +1886,7 @@ public class LoopbackActivity extends Activity
int[] usefulBufferData = Arrays.copyOfRange(playerBufferData, 0,
usefulDataRange);
PerformanceMeasurement measurement = new PerformanceMeasurement(
- playerBufferSize, mSamplingRate, usefulBufferData);
+ mPlayerCallbackTimes.getExpectedBufferPeriod(), usefulBufferData);
float playerPercentAtExpected = measurement.percentBufferPeriodsAtExpected();
double benchmark = measurement.computeWeightedBenchmark();
int outliers = measurement.countOutliers();
@@ -1748,18 +1903,6 @@ public class LoopbackActivity extends Activity
} else {
sb.append("Cannot Find Player Buffer Period Data!" + endline);
}
-
- // report expected player buffer period
- int expectedPlayerBufferPeriod = mPlayerBufferSizeInBytes / Constant.BYTES_PER_FRAME
- * Constant.MILLIS_PER_SECOND / mSamplingRate;
- if (audioType.equals("JAVA")) {
- // javaPlayerMultiple depends on the samples written per AudioTrack.write()
- int javaPlayerMultiple = 2;
- expectedPlayerBufferPeriod *= javaPlayerMultiple;
- }
- sb.append("Expected Player Buffer Period (ms) = " + expectedPlayerBufferPeriod +
- endline);
-
// report glitches per hour
int numberOfGlitches = estimateNumberOfGlitches(mGlitchesData);
float testDurationInHours = mBufferTestElapsedSeconds
@@ -1773,8 +1916,18 @@ public class LoopbackActivity extends Activity
sb.append("Total Number of Glitches = " + numberOfGlitches + endline);
// report if the total glitching interval is too long
- sb.append("Total glitching interval too long: " +
- mGlitchingIntervalTooLong + endline);
+ sb.append("Total glitching interval too long = " +
+ mGlitchingIntervalTooLong);
+
+ sb.append("\nLate Player Callbacks = ");
+ sb.append(mPlayerCallbackTimes.getNumLateOrEarlyCallbacks());
+ sb.append("\nLate Player Callbacks Exceeded Capacity = ");
+ sb.append(mPlayerCallbackTimes.isCapacityExceeded());
+ sb.append("\nLate Recorder Callbacks = ");
+ sb.append(mRecorderCallbackTimes.getNumLateOrEarlyCallbacks());
+ sb.append("\nLate Recorder Callbacks Exceeded Capacity = ");
+ sb.append(mRecorderCallbackTimes.isCapacityExceeded());
+ sb.append("\n");
}
@@ -1882,7 +2035,7 @@ public class LoopbackActivity extends Activity
/**
* Requests the RECORD_AUDIO permission from the user
*/
- private void requestRecordAudioPermission(){
+ private void requestRecordAudioPermission(int requestCode){
String requiredPermission = Manifest.permission.RECORD_AUDIO;
@@ -1891,30 +2044,26 @@ public class LoopbackActivity extends Activity
if (ActivityCompat.shouldShowRequestPermissionRationale(this,
requiredPermission)) {
- showToast("This app needs to record audio through the microphone to test the device's performance");
+ showToast("This app needs to record audio through the microphone to test the device's "+
+ "performance");
}
// request the permission.
- ActivityCompat.requestPermissions(this,
- new String[]{requiredPermission},
- PERMISSIONS_REQUEST_RECORD_AUDIO);
+ ActivityCompat.requestPermissions(this, new String[]{requiredPermission}, requestCode);
}
@Override
public void onRequestPermissionsResult(int requestCode,
String permissions[], int[] grantResults) {
- // We can ignore this call since we'll check for RECORD_AUDIO each time the
- // user does anything which requires that permission. We can't, however, delete
- // this method as this will cause ActivityCompat.requestPermissions to fail.
-
- // Save all files after being granted permissions
- if (requestCode == PERMISSIONS_REQUEST_WRITE_EXTERNAL_STORAGE && grantResults.length > 0
- && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
- if (mIntentFileName != null && !mIntentFileName.isEmpty()) {
- saveAllTo(mIntentFileName);
- } else {
- saveAllTo("loopback_" + mCurrentTime);
+ // Save all files or run requested test after being granted permissions
+ if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+ if (requestCode == PERMISSIONS_REQUEST_WRITE_EXTERNAL_STORAGE ) {
+ saveAllTo(getFileNamePrefix());
+ } else if (requestCode == PERMISSIONS_REQUEST_RECORD_AUDIO_BUFFER) {
+ startBufferTest();
+ } else if (requestCode == PERMISSIONS_REQUEST_RECORD_AUDIO_LATENCY) {
+ startLatencyTest();
}
}
}
@@ -1952,7 +2101,7 @@ public class LoopbackActivity extends Activity
@Override
public void onSaveDialogSelect(DialogFragment dialog, boolean saveWithoutDialog) {
if (saveWithoutDialog) {
- saveAllTo("loopback_" + mCurrentTime);
+ saveAllTo("loopback_" + mTestStartTimeString);
} else {
SaveFilesWithDialog();
}
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 02b91df..7844c70 100644
--- a/LoopbackApp/app/src/main/java/org/drrickorang/loopback/LoopbackApplication.java
+++ b/LoopbackApp/app/src/main/java/org/drrickorang/loopback/LoopbackApplication.java
@@ -47,6 +47,9 @@ public class LoopbackApplication extends Application {
private int mBufferTestDurationInSeconds = 5;
private int mBufferTestWavePlotDurationInSeconds = 7;
private int mNumberOfLoadThreads = 4;
+ private boolean mCaptureSysTraceEnabled = false;
+ private boolean mCaptureWavSnippetsEnabled = false;
+ private int mNumStateCaptures = Constant.DEFAULT_NUM_CAPTURES;
public void setDefaults() {
if (isSafeToUseSles()) {
@@ -210,6 +213,30 @@ public class LoopbackApplication extends Application {
Constant.MAX_NUM_LOAD_THREADS);
}
+ public void setNumberOfCaptures (int num){
+ mNumStateCaptures = clamp(num, Constant.MIN_NUM_CAPTURES, Constant.MAX_NUM_CAPTURES);
+ }
+
+ public void setCaptureSysTraceEnabled (boolean enabled){
+ mCaptureSysTraceEnabled = enabled;
+ }
+
+ public void setCaptureWavsEnabled (boolean enabled){
+ mCaptureWavSnippetsEnabled = enabled;
+ }
+
+ public boolean isCaptureSysTraceEnabled () {
+ return mCaptureSysTraceEnabled;
+ }
+
+ public int getNumStateCaptures() {
+ return mNumStateCaptures;
+ }
+
+ public boolean isCaptureWavSnippetsEnabled() {
+ return mCaptureWavSnippetsEnabled;
+ }
+
/**
* Returns value if value is within inclusive bounds min through max
* otherwise returns min or max according to if value is less than or greater than the range
@@ -315,5 +342,4 @@ public class LoopbackApplication extends Application {
private static void log(String msg) {
Log.v(TAG, msg);
}
-
}
diff --git a/LoopbackApp/app/src/main/java/org/drrickorang/loopback/LoopbackAudioThread.java b/LoopbackApp/app/src/main/java/org/drrickorang/loopback/LoopbackAudioThread.java
index 1ef64c4..ba68780 100644
--- a/LoopbackApp/app/src/main/java/org/drrickorang/loopback/LoopbackAudioThread.java
+++ b/LoopbackApp/app/src/main/java/org/drrickorang/loopback/LoopbackAudioThread.java
@@ -77,6 +77,7 @@ public class LoopbackAudioThread extends Thread {
private int mBufferTestDurationInSeconds; // Duration of actual buffer test
private Context mContext;
private int mBufferTestWavePlotDurationInSeconds;
+ private final CaptureHolder mCaptureHolder;
private boolean mIsAdjustingSoundLevel = true; // only used in buffer test
@@ -85,7 +86,7 @@ public class LoopbackAudioThread extends Thread {
BufferPeriod playerBufferPeriod, int testType,
int bufferTestDurationInSeconds,
int bufferTestWavePlotDurationInSeconds, Context context,
- int channelIndex) {
+ int channelIndex, CaptureHolder captureHolder) {
mSamplingRate = samplingRate;
mMinPlayerBufferSizeInBytes = playerBufferInBytes;
mMinRecorderBuffSizeInBytes = recorderBufferInBytes;
@@ -97,6 +98,7 @@ public class LoopbackAudioThread extends Thread {
mBufferTestWavePlotDurationInSeconds = bufferTestWavePlotDurationInSeconds;
mContext = context;
mChannelIndex = channelIndex;
+ mCaptureHolder = captureHolder;
setName("Loopback_LoopbackAudio");
}
@@ -126,11 +128,10 @@ public class LoopbackAudioThread extends Thread {
short[] bufferTestTone = new short[audioTrackWriteDataSize]; // used by AudioTrack.write()
ToneGeneration toneGeneration = new SineWaveTone(mSamplingRate, frequency1);
- //todo update recorderRunnable for channel index
mRecorderRunnable = new RecorderRunnable(mLatencyTestPipe, mSamplingRate, mChannelConfigIn,
mAudioFormat, mMinRecorderBuffSizeInBytes, MediaRecorder.AudioSource.MIC, this,
mRecorderBufferPeriod, mTestType, frequency1, frequency2,
- mBufferTestWavePlotDurationInSeconds, mContext, mChannelIndex);
+ mBufferTestWavePlotDurationInSeconds, mContext, mChannelIndex, mCaptureHolder);
mRecorderRunnable.setBufferTestDurationInSeconds(mBufferTestDurationInSeconds);
mRecorderThread = new Thread(mRecorderRunnable);
mRecorderThread.setName("Loopback_RecorderRunnable");
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 cf39efe..48d9fdc 100644
--- a/LoopbackApp/app/src/main/java/org/drrickorang/loopback/NativeAudioThread.java
+++ b/LoopbackApp/app/src/main/java/org/drrickorang/loopback/NativeAudioThread.java
@@ -66,6 +66,8 @@ public class NativeAudioThread extends Thread {
private int mRecorderMaxBufferPeriod;
private int[] mPlayerBufferPeriod;
private int mPlayerMaxBufferPeriod;
+ private BufferCallbackTimes mPlayerCallbackTimes;
+ private BufferCallbackTimes mRecorderCallbackTimes;
private int mBufferTestWavePlotDurationInSeconds;
private double mFrequency1 = Constant.PRIME_FREQUENCY_1;
private double mFrequency2 = Constant.PRIME_FREQUENCY_2; // not actually used
@@ -74,6 +76,7 @@ public class NativeAudioThread extends Thread {
private int mFFTOverlapSamples;
private int[] mAllGlitches;
private boolean mGlitchingIntervalTooLong;
+ private final CaptureHolder mCaptureHolder;
private PipeByteBuffer mPipeByteBuffer;
private GlitchDetectionThread mGlitchDetectionThread;
@@ -81,7 +84,7 @@ public class NativeAudioThread extends Thread {
public NativeAudioThread(int samplingRate, int playerBufferInBytes, int recorderBufferInBytes,
int micSource, int testType, int bufferTestDurationInSeconds,
- int bufferTestWavePlotDurationInSeconds) {
+ int bufferTestWavePlotDurationInSeconds, CaptureHolder captureHolder) {
mSamplingRate = samplingRate;
mMinPlayerBufferSizeInBytes = playerBufferInBytes;
mMinRecorderBuffSizeInBytes = recorderBufferInBytes;
@@ -89,7 +92,7 @@ public class NativeAudioThread extends Thread {
mTestType = testType;
mBufferTestDurationInSeconds = bufferTestDurationInSeconds;
mBufferTestWavePlotDurationInSeconds = bufferTestWavePlotDurationInSeconds;
-
+ mCaptureHolder = captureHolder;
setName("Loopback_NativeAudio");
}
@@ -109,7 +112,8 @@ public class NativeAudioThread extends Thread {
//jni calls
public native long slesInit(int samplingRate, int frameCount, int micSource,
int testType, double frequency1, ByteBuffer byteBuffer,
- short[] sincTone);
+ short[] sincTone, int maxRecordedLateCallbacks,
+ CaptureHolder captureHolder);
public native int slesProcessNext(long sles_data, double[] samples, long offset);
public native int slesDestroy(long sles_data);
@@ -118,6 +122,8 @@ public class NativeAudioThread extends Thread {
public native int slesGetRecorderMaxBufferPeriod(long sles_data);
public native int[] slesGetPlayerBufferPeriod(long sles_data);
public native int slesGetPlayerMaxBufferPeriod(long sles_data);
+ public native BufferCallbackTimes slesGetPlayerCallbackTimeStamps(long sles_data);
+ public native BufferCallbackTimes slesGetRecorderCallbackTimeStamps(long sles_data);
public void run() {
@@ -157,9 +163,11 @@ public class NativeAudioThread extends Thread {
// mPipeByteBuffer is only used in buffer test
mPipeByteBuffer = new PipeByteBuffer(Constant.MAX_SHORTS);
long startTimeMs = System.currentTimeMillis();
- long sles_data = slesInit(mSamplingRate, mMinPlayerBufferSizeInBytes /
- Constant.BYTES_PER_FRAME, mMicSource, mTestType, mFrequency1,
- mPipeByteBuffer.getByteBuffer(), loopbackTone);
+ long sles_data = slesInit(mSamplingRate,
+ mMinPlayerBufferSizeInBytes / Constant.BYTES_PER_FRAME, mMicSource, mTestType,
+ mFrequency1, mPipeByteBuffer.getByteBuffer(), loopbackTone,
+ mBufferTestDurationInSeconds * Constant.MAX_RECORDED_LATE_CALLBACKS_PER_SECOND,
+ mCaptureHolder);
log(String.format("sles_data = 0x%X", sles_data));
if (sles_data == 0) {
@@ -241,6 +249,9 @@ public class NativeAudioThread extends Thread {
mPlayerBufferPeriod = slesGetPlayerBufferPeriod(sles_data);
mPlayerMaxBufferPeriod = slesGetPlayerMaxBufferPeriod(sles_data);
+ mPlayerCallbackTimes = slesGetPlayerCallbackTimeStamps(sles_data);
+ mRecorderCallbackTimes = slesGetRecorderCallbackTimeStamps(sles_data);
+
// get glitches data only for buffer test
if (mTestType == Constant.LOOPBACK_PLUG_AUDIO_THREAD_TEST_TYPE_BUFFER_PERIOD) {
mAllGlitches = mGlitchDetectionThread.getGlitches();
@@ -302,7 +313,7 @@ public class NativeAudioThread extends Thread {
mGlitchDetectionThread = new GlitchDetectionThread(mFrequency1, mFrequency2, mSamplingRate,
mFFTSamplingSize, mFFTOverlapSamples, mBufferTestDurationInSeconds,
- mBufferTestWavePlotDurationInSeconds, mPipeByteBuffer);
+ mBufferTestWavePlotDurationInSeconds, mPipeByteBuffer, mCaptureHolder);
mGlitchDetectionThread.start();
}
@@ -456,4 +467,11 @@ public class NativeAudioThread extends Thread {
return mBufferTestDurationInSeconds;
}
+ public BufferCallbackTimes getPlayerCallbackTimes() {
+ return mPlayerCallbackTimes;
+ }
+
+ public BufferCallbackTimes getRecorderCallbackTimes() {
+ return mRecorderCallbackTimes;
+ }
}
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 8acb0bf..bd16707 100644
--- a/LoopbackApp/app/src/main/java/org/drrickorang/loopback/PerformanceMeasurement.java
+++ b/LoopbackApp/app/src/main/java/org/drrickorang/loopback/PerformanceMeasurement.java
@@ -45,7 +45,7 @@ public class PerformanceMeasurement {
* Note: if mBufferSize * Constant.MILLIS_PER_SECOND / mSamplingRate == Integer is satisfied,
* the measurement will be more accurate, but this is not necessary.
*/
- public PerformanceMeasurement(int bufferSize, int samplingRate, int[] bufferData) {
+ public PerformanceMeasurement(int expectedBufferPeriod, int[] bufferData) {
mBufferData = bufferData;
mTotalOccurrence = 0;
@@ -53,7 +53,7 @@ public class PerformanceMeasurement {
mTotalOccurrence += mBufferData[i];
}
- mExpectedBufferPeriodMs = bufferSize * Constant.MILLIS_PER_SECOND / samplingRate;
+ mExpectedBufferPeriodMs = expectedBufferPeriod;
}
diff --git a/LoopbackApp/app/src/main/java/org/drrickorang/loopback/PlayerBufferPeriodActivity.java b/LoopbackApp/app/src/main/java/org/drrickorang/loopback/PlayerBufferPeriodActivity.java
index b4d0978..6132a14 100644
--- a/LoopbackApp/app/src/main/java/org/drrickorang/loopback/PlayerBufferPeriodActivity.java
+++ b/LoopbackApp/app/src/main/java/org/drrickorang/loopback/PlayerBufferPeriodActivity.java
@@ -39,26 +39,10 @@ public class PlayerBufferPeriodActivity extends Activity {
Bundle bundle = getIntent().getExtras();
// setup the histogram
- int[] bufferTimeStampData = bundle.getIntArray("playerBufferPeriodTimeStampArray");
int[] bufferData = bundle.getIntArray("playerBufferPeriodArray");
int bufferDataMax = bundle.getInt("playerBufferPeriodMax");
- histogramView.setBufferPeriodTimeStampArray(bufferTimeStampData);
histogramView.setBufferPeriodArray(bufferData);
histogramView.setMaxBufferPeriod(bufferDataMax);
-
-
- // do performance measurement if there are buffer period data
- if (bufferData != null) {
- // this is the range of data that actually has values
- int usefulDataRange = Math.min(bufferDataMax + 1, bufferData.length);
- int[] usefulBufferData = Arrays.copyOfRange(bufferData, 0, usefulDataRange);
- int playerBufferSize = bundle.getInt("playerBufferSize");
- int samplingRate = bundle.getInt("samplingRate");
- PerformanceMeasurement measurement = new PerformanceMeasurement(playerBufferSize,
- samplingRate, usefulBufferData);
- measurement.measurePerformance();
- }
-
}
}
diff --git a/LoopbackApp/app/src/main/java/org/drrickorang/loopback/RecorderBufferPeriodActivity.java b/LoopbackApp/app/src/main/java/org/drrickorang/loopback/RecorderBufferPeriodActivity.java
index 9ecec65..f34dd4f 100644
--- a/LoopbackApp/app/src/main/java/org/drrickorang/loopback/RecorderBufferPeriodActivity.java
+++ b/LoopbackApp/app/src/main/java/org/drrickorang/loopback/RecorderBufferPeriodActivity.java
@@ -41,25 +41,11 @@ public class RecorderBufferPeriodActivity extends Activity {
Bundle bundle = getIntent().getExtras();
// setup the histogram
- int[] bufferTimeStampData = bundle.getIntArray("recorderBufferPeriodTimeStampArray");
int[] bufferData = bundle.getIntArray("recorderBufferPeriodArray");
int bufferDataMax = bundle.getInt("recorderBufferPeriodMax");
- histogramView.setBufferPeriodTimeStampArray(bufferTimeStampData);
histogramView.setBufferPeriodArray(bufferData);
histogramView.setMaxBufferPeriod(bufferDataMax);
- // do performance measurement if the there are buffer period data
- if (bufferData != null) {
- // this is the range of data that actually has values
- int usefulDataRange = Math.min(bufferDataMax + 1, bufferData.length);
- int[] usefulBufferData = Arrays.copyOfRange(bufferData, 0, usefulDataRange);
- int recorderBufferSize = bundle.getInt("recorderBufferSize");
- int samplingRate = bundle.getInt("samplingRate");
- PerformanceMeasurement measurement = new PerformanceMeasurement(recorderBufferSize,
- samplingRate, usefulBufferData);
- measurement.measurePerformance();
- }
-
}
diff --git a/LoopbackApp/app/src/main/java/org/drrickorang/loopback/RecorderRunnable.java b/LoopbackApp/app/src/main/java/org/drrickorang/loopback/RecorderRunnable.java
index d2c238b..8c3c7a1 100644
--- a/LoopbackApp/app/src/main/java/org/drrickorang/loopback/RecorderRunnable.java
+++ b/LoopbackApp/app/src/main/java/org/drrickorang/loopback/RecorderRunnable.java
@@ -68,6 +68,7 @@ public class RecorderRunnable implements Runnable {
private long mStartTimeMs;
private int mBufferTestDurationInSeconds;
private long mBufferTestDurationMs;
+ private final CaptureHolder mCaptureHolder;
private final Context mContext;
private AudioManager mAudioManager;
private GlitchDetectionThread mGlitchDetectionThread;
@@ -84,12 +85,11 @@ public class RecorderRunnable implements Runnable {
private double[] mSamples; // samples shown on WavePlotView
private int mSamplesIndex;
-
RecorderRunnable(PipeShort latencyPipe, int samplingRate, int channelConfig, int audioFormat,
int recorderBufferInBytes, int micSource, LoopbackAudioThread audioThread,
BufferPeriod recorderBufferPeriod, int testType, double frequency1,
double frequency2, int bufferTestWavePlotDurationInSeconds,
- Context context, int channelIndex) {
+ Context context, int channelIndex, CaptureHolder captureHolder) {
mLatencyTestPipeShort = latencyPipe;
mSamplingRate = samplingRate;
mChannelConfig = channelConfig;
@@ -104,6 +104,7 @@ public class RecorderRunnable implements Runnable {
mBufferTestWavePlotDurationInSeconds = bufferTestWavePlotDurationInSeconds;
mContext = context;
mChannelIndex = channelIndex;
+ mCaptureHolder = captureHolder;
}
@@ -293,7 +294,7 @@ public class RecorderRunnable implements Runnable {
mBufferTestPipeShort = new PipeShort(Constant.MAX_SHORTS);
mGlitchDetectionThread = new GlitchDetectionThread(mFrequency1, mFrequency2, mSamplingRate,
mFFTSamplingSize, mFFTOverlapSamples, mBufferTestDurationInSeconds,
- mBufferTestWavePlotDurationInSeconds, mBufferTestPipeShort);
+ mBufferTestWavePlotDurationInSeconds, mBufferTestPipeShort, mCaptureHolder);
mGlitchDetectionThread.start();
mRecorder.startRecording();
}
diff --git a/LoopbackApp/app/src/main/java/org/drrickorang/loopback/SaveFilesDialogFragment.java b/LoopbackApp/app/src/main/java/org/drrickorang/loopback/SaveFilesDialogFragment.java
index de8e575..4707b81 100644
--- a/LoopbackApp/app/src/main/java/org/drrickorang/loopback/SaveFilesDialogFragment.java
+++ b/LoopbackApp/app/src/main/java/org/drrickorang/loopback/SaveFilesDialogFragment.java
@@ -56,9 +56,9 @@ public class SaveFilesDialogFragment extends DialogFragment {
public Dialog onCreateDialog(Bundle savedInstanceState) {
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
- //todo make string resources 3x
builder.setMessage(R.string.SaveFileDialogLabel)
- .setPositiveButton(R.string.SaveFileDialogOK, new DialogInterface.OnClickListener() {
+ .setPositiveButton(R.string.SaveFileDialogOK,
+ new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id) {
dialog.dismiss();
mListener.onSaveDialogSelect(SaveFilesDialogFragment.this, true);
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 0f2e5ab..566dd18 100644
--- a/LoopbackApp/app/src/main/java/org/drrickorang/loopback/SettingsActivity.java
+++ b/LoopbackApp/app/src/main/java/org/drrickorang/loopback/SettingsActivity.java
@@ -20,19 +20,25 @@ import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
+import android.view.Gravity;
import android.view.View;
+import android.view.ViewGroup;
+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;
/**
* This activity displays all settings that can be adjusted by the user.
*/
-public class SettingsActivity extends Activity implements OnItemSelectedListener {
+public class SettingsActivity extends Activity implements OnItemSelectedListener,
+ ToggleButton.OnCheckedChangeListener {
private static final String TAG = "SettingsActivity";
@@ -46,6 +52,9 @@ public class SettingsActivity extends Activity implements OnItemSelectedListener
private SettingsPicker mBufferTestDurationUI;
private SettingsPicker mWavePlotDurationUI;
private SettingsPicker mLoadThreadUI;
+ private SettingsPicker mNumCapturesUI;
+ private ToggleButton mSystraceToggleButton;
+ private ToggleButton mWavCaptureToggleButton;
ArrayAdapter<CharSequence> mAdapterSamplingRate;
@@ -193,6 +202,28 @@ public class SettingsActivity extends Activity implements OnItemSelectedListener
}
});
+ // Settings Picker for Number of Captures
+ mNumCapturesUI = (SettingsPicker) findViewById(R.id.numCapturesSettingPicker);
+ mNumCapturesUI.setMinMaxDefault(Constant.MIN_NUM_CAPTURES, Constant.MAX_NUM_CAPTURES,
+ getApp().getNumStateCaptures());
+ mNumCapturesUI.setTitle(getResources().getString(R.string.numCapturesSetting));
+ mNumCapturesUI.setSettingsChangeListener(new SettingsPicker.SettingChangeListener() {
+ @Override
+ public void settingChanged(int value) {
+ log("new num captures:" + value);
+ getApp().setNumberOfCaptures(value);
+ setSettingsHaveChanged();
+ }
+ });
+
+ mWavCaptureToggleButton = (ToggleButton) findViewById(R.id.wavSnippetsEnabledToggle);
+ mWavCaptureToggleButton.setChecked(getApp().isCaptureWavSnippetsEnabled());
+ mWavCaptureToggleButton.setOnCheckedChangeListener(this);
+
+ mSystraceToggleButton = (ToggleButton) findViewById(R.id.SystraceEnabledToggle);
+ mSystraceToggleButton.setChecked(getApp().isCaptureSysTraceEnabled());
+ mSystraceToggleButton.setOnCheckedChangeListener(this);
+
refresh();
}
@@ -235,6 +266,9 @@ public class SettingsActivity extends Activity implements OnItemSelectedListener
mSpinnerChannelIndex.setEnabled(false);
}
+ mNumCapturesUI.setEnabled(getApp().isCaptureSysTraceEnabled() ||
+ getApp().isCaptureWavSnippetsEnabled());
+
String info = getApp().getSystemInfo();
mTextSettingsInfo.setText("SETTINGS - " + info);
}
@@ -280,6 +314,17 @@ public class SettingsActivity extends Activity implements OnItemSelectedListener
}
}
+ @Override
+ public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
+ if (buttonView.getId() == mWavCaptureToggleButton.getId()){
+ getApp().setCaptureWavsEnabled(isChecked);
+ } else if (buttonView.getId() == mSystraceToggleButton.getId()) {
+ getApp().setCaptureSysTraceEnabled(isChecked);
+ }
+ mNumCapturesUI.setEnabled(getApp().isCaptureSysTraceEnabled() ||
+ getApp().isCaptureWavSnippetsEnabled());
+ }
+
private void setSettingsHaveChanged() {
Intent intent = new Intent();
setResult(RESULT_OK, intent);
@@ -290,6 +335,20 @@ public class SettingsActivity extends Activity implements OnItemSelectedListener
// Another interface callback
}
+ public void onButtonSysTraceHelp(View view) {
+ // Create a PopUpWindow with scrollable TextView
+ View puLayout = this.getLayoutInflater().inflate(R.layout.report_window, null);
+ PopupWindow popUp = new PopupWindow(puLayout, ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.MATCH_PARENT, true);
+
+ // Generate report of glitch intervals and set pop up window text
+ TextView helpText =
+ (TextView) popUp.getContentView().findViewById(R.id.ReportInfo);
+ helpText.setText(getResources().getString(R.string.systraceHelp));
+
+ // display pop up window, dismissible with back button
+ popUp.showAtLocation(findViewById(R.id.settingsMainLayout), Gravity.TOP, 0, 0);
+ }
/** Called when the user clicks the button */
public void onButtonClick(View view) {
diff --git a/LoopbackApp/app/src/main/java/org/drrickorang/loopback/SettingsPicker.java b/LoopbackApp/app/src/main/java/org/drrickorang/loopback/SettingsPicker.java
index 969cc9a..f8a9e4a 100644
--- a/LoopbackApp/app/src/main/java/org/drrickorang/loopback/SettingsPicker.java
+++ b/LoopbackApp/app/src/main/java/org/drrickorang/loopback/SettingsPicker.java
@@ -18,18 +18,16 @@ package org.drrickorang.loopback;
import android.content.Context;
import android.util.AttributeSet;
-import android.view.KeyEvent;
-import android.view.inputmethod.EditorInfo;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.SeekBar;
import android.widget.TextView;
public class SettingsPicker extends LinearLayout implements SeekBar.OnSeekBarChangeListener,
- TextView.OnEditorActionListener {
+ CatchEventsEditText.EditTextEventListener {
protected TextView mTitleTextView;
- protected EditText mValueEditText;
+ protected CatchEventsEditText mValueEditText;
protected SeekBar mValueSeekBar;
protected SettingChangeListener mSettingsChangeListener;
@@ -45,11 +43,11 @@ public class SettingsPicker extends LinearLayout implements SeekBar.OnSeekBarCha
inflate(context, R.layout.settings_picker, this);
- mTitleTextView = (TextView) findViewWithTag("title");
- mValueEditText = (EditText) findViewWithTag("valueText");
- mValueSeekBar = (SeekBar) findViewWithTag("seekbar");
+ mTitleTextView = (TextView) findViewById(R.id.settings_title);
+ mValueEditText = (CatchEventsEditText) findViewById(R.id.settings_valueText);
+ mValueSeekBar = (SeekBar) findViewById(R.id.settings_seekbar);
- mValueEditText.setOnEditorActionListener(this);
+ mValueEditText.setEditTextEvenListener(this);
mValueSeekBar.setOnSeekBarChangeListener(this);
}
@@ -88,31 +86,26 @@ public class SettingsPicker extends LinearLayout implements SeekBar.OnSeekBarCha
}
@Override
- public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
- if ((event != null && (event.getKeyCode() == KeyEvent.KEYCODE_ENTER))
- || (actionId == EditorInfo.IME_ACTION_DONE)) {
- if (!v.getText().toString().isEmpty()) {
- int value;
- try {
- value = Integer.parseInt(v.getText().toString());
- } catch (NumberFormatException e) {
- value = mMinimumValue;
- v.setText(Integer.toString(value));
- }
- if (value < mMinimumValue) {
- value = mMinimumValue;
- v.setText(Integer.toString(value));
- } else if (value > mMaximumValue) {
- value = mMaximumValue;
- v.setText(Integer.toString(value));
- }
- textChanged(value);
- } else {
- sliderChanged(mMinimumValue + mValueSeekBar.getProgress(), false);
+ public void textEdited(EditText v) {
+ if (!v.getText().toString().isEmpty()) {
+ int value;
+ try {
+ value = Integer.parseInt(v.getText().toString());
+ } catch (NumberFormatException e) {
+ value = mMinimumValue;
+ v.setText(Integer.toString(value));
}
- return true;
+ if (value < mMinimumValue) {
+ value = mMinimumValue;
+ v.setText(Integer.toString(value));
+ } else if (value > mMaximumValue) {
+ value = mMaximumValue;
+ v.setText(Integer.toString(value));
+ }
+ textChanged(value);
+ } else {
+ sliderChanged(mMinimumValue + mValueSeekBar.getProgress(), false);
}
- return false;
}
@Override
diff --git a/LoopbackApp/app/src/main/java/org/drrickorang/loopback/WaveDataRingBuffer.java b/LoopbackApp/app/src/main/java/org/drrickorang/loopback/WaveDataRingBuffer.java
index 935c805..ee47238 100644
--- a/LoopbackApp/app/src/main/java/org/drrickorang/loopback/WaveDataRingBuffer.java
+++ b/LoopbackApp/app/src/main/java/org/drrickorang/loopback/WaveDataRingBuffer.java
@@ -16,14 +16,20 @@
package org.drrickorang.loopback;
+import java.util.Arrays;
+
/**
- * Maintains a recording of wave data of last n seconds
+ * Maintains two ring buffers for recording wav data
+ * At any one time one buffer is available for writing to file while one is recording incoming data
*/
public class WaveDataRingBuffer {
- private final double[] mWaveRecord;
- private volatile int index = 0; // between 0 and mWaveRecord.length - 1
- private boolean arrayFull = false; // true after index has wrapped
+ public interface ReadableWaveDeck {
+ boolean writeToFile(AudioFileOutput audioFile);
+ }
+
+ private WaveDeck mLoadedDeck;
+ private WaveDeck mShelvedDeck;
public WaveDataRingBuffer(int size) {
if (size < Constant.SAMPLING_RATE_MIN * Constant.BUFFER_TEST_DURATION_SECONDS_MIN) {
@@ -31,66 +37,137 @@ public class WaveDataRingBuffer {
} else if (size > Constant.SAMPLING_RATE_MAX * Constant.BUFFER_TEST_DURATION_SECONDS_MAX) {
size = Constant.SAMPLING_RATE_MAX * Constant.BUFFER_TEST_DURATION_SECONDS_MAX;
}
+ mLoadedDeck = new WaveDeck(size);
+ mShelvedDeck = new WaveDeck(size);
+ }
- mWaveRecord = new double[size];
+ public synchronized void writeWaveData(double[] data, int srcPos, int length) {
+ mLoadedDeck.writeWaveData(data, srcPos, length);
+ }
+
+ public synchronized double[] getWaveRecord() {
+ return mLoadedDeck.getWaveRecord();
+ }
+
+ private void SwapDecks() {
+ WaveDeck temp = mShelvedDeck;
+ mShelvedDeck = mLoadedDeck;
+ mLoadedDeck = temp;
+ }
+
+ /**
+ * Returns currently writing buffer as writeToFile interface, load erased shelved deck for write
+ * If shelved deck is still being read returns null
+ **/
+ public synchronized ReadableWaveDeck getWaveDeck() {
+ if (!mShelvedDeck.isBeingRead()) {
+ SwapDecks();
+ mShelvedDeck.readyForRead();
+ mLoadedDeck.reset();
+ return mShelvedDeck;
+ } else {
+ return null;
+ }
}
/**
- * Write length number of doubles from data into ring buffer from starting srcPos
+ * Maintains a recording of wave data of last n seconds
*/
- public synchronized void writeWaveData(double[] data, int srcPos, int length) {
- if (length > data.length - srcPos) {
- // requested to write more data than available
- // bad request leave data un-affected
- return;
+ public class WaveDeck implements ReadableWaveDeck {
+
+ private double[] mWaveRecord;
+ private volatile int mIndex = 0; // between 0 and mWaveRecord.length - 1
+ private boolean mArrayFull = false; // true after mIndex has wrapped
+ private boolean mIsBeingRead = false;
+
+ public WaveDeck(int size) {
+ mWaveRecord = new double[size];
}
- if (length >= mWaveRecord.length) {
- // requested write would fill or exceed ring buffer capacity
- // fill ring buffer with last segment of requested write
- System.arraycopy(data, srcPos + (length - mWaveRecord.length), mWaveRecord, 0,
- mWaveRecord.length);
- index = 0;
- } else if (mWaveRecord.length - index > length) {
- // write requested data from current offset
- System.arraycopy(data, srcPos, mWaveRecord, index, length);
- index += length;
- } else {
- // write to available buffer then wrap and overwrite previous records
- if (!arrayFull) {
- arrayFull = true;
+ /**
+ * Write length number of doubles from data into ring buffer from starting srcPos
+ */
+ public void writeWaveData(double[] data, int srcPos, int length) {
+ if (length > data.length - srcPos) {
+ // requested to write more data than available
+ // bad request leave data un-affected
+ return;
}
- int availBuff = mWaveRecord.length - index;
+ if (length >= mWaveRecord.length) {
+ // requested write would fill or exceed ring buffer capacity
+ // fill ring buffer with last segment of requested write
+ System.arraycopy(data, srcPos + (length - mWaveRecord.length), mWaveRecord, 0,
+ mWaveRecord.length);
+ mIndex = 0;
+ } else if (mWaveRecord.length - mIndex > length) {
+ // write requested data from current offset
+ System.arraycopy(data, srcPos, mWaveRecord, mIndex, length);
+ mIndex += length;
+ } else {
+ // write to available buffer then wrap and overwrite previous records
+ if (!mArrayFull) {
+ mArrayFull = true;
+ }
+
+ int availBuff = mWaveRecord.length - mIndex;
+
+ System.arraycopy(data, srcPos, mWaveRecord, mIndex, availBuff);
+ System.arraycopy(data, srcPos + availBuff, mWaveRecord, 0, length - availBuff);
+
+ mIndex = length - availBuff;
+
+ }
- System.arraycopy(data, srcPos, mWaveRecord, index, availBuff);
- System.arraycopy(data, srcPos + availBuff, mWaveRecord, 0, length - availBuff);
+ }
- index = length - availBuff;
+ /**
+ * Returns a private copy of recorded wave data
+ *
+ * @return double array of wave recording, rearranged with oldest sample at first index
+ */
+ public double[] getWaveRecord() {
+ double outputBuffer[] = new double[mWaveRecord.length];
+
+ if (!mArrayFull) {
+ //return partially filled sample with trailing zeroes
+ System.arraycopy(mWaveRecord, 0, outputBuffer, 0, mIndex);
+ Arrays.fill(outputBuffer, mIndex+1, outputBuffer.length-1, 0);
+ } else {
+ //copy buffer to contiguous sample and return unwrapped array
+ System.arraycopy(mWaveRecord, mIndex, outputBuffer, 0, mWaveRecord.length - mIndex);
+ System.arraycopy(mWaveRecord, 0, outputBuffer, mWaveRecord.length - mIndex, mIndex);
+ }
+ return outputBuffer;
}
- }
+ /** Make buffer available for new recording **/
+ public void reset() {
+ mIndex = 0;
+ mArrayFull = false;
+ }
- /**
- * Returns a private copy of recorded wave data
- *
- * @return double array of wave recording, rearranged with oldest sample at first index
- */
- public synchronized double[] getWaveRecord() {
- double outputBuffer[] = new double[mWaveRecord.length];
+ public boolean isBeingRead() {
+ return mIsBeingRead;
+ }
- if (!arrayFull) {
- //return partially filled sample with trailing zeroes
- System.arraycopy(mWaveRecord, 0, outputBuffer, 0, index);
- } else {
- //copy buffer to contiguous sample and return unwrapped array
- System.arraycopy(mWaveRecord, index, outputBuffer, 0, mWaveRecord.length - index);
- System.arraycopy(mWaveRecord, 0, outputBuffer, mWaveRecord.length - index, index);
+ private void readyForRead() {
+ mIsBeingRead = true;
}
- return outputBuffer;
+ @Override
+ public boolean writeToFile(AudioFileOutput audioFile) {
+ boolean successfulWrite;
+ if (mArrayFull) {
+ successfulWrite = audioFile.writeRingBufferData(mWaveRecord, mIndex, mIndex);
+ } else {
+ // Write only filled part of array to file
+ successfulWrite = audioFile.writeRingBufferData(mWaveRecord, 0, mIndex);
+ }
+ mIsBeingRead = false;
+ return successfulWrite;
+ }
}
-
}
diff --git a/LoopbackApp/app/src/main/jni/jni_sles.c b/LoopbackApp/app/src/main/jni/jni_sles.c
index 99c5543..72c0884 100644
--- a/LoopbackApp/app/src/main/jni/jni_sles.c
+++ b/LoopbackApp/app/src/main/jni/jni_sles.c
@@ -23,7 +23,8 @@
JNIEXPORT jlong JNICALL Java_org_drrickorang_loopback_NativeAudioThread_slesInit
(JNIEnv *env __unused, jobject obj __unused, jint samplingRate, jint frameCount, jint micSource,
- jint testType, jdouble frequency1, jobject byteBuffer, jshortArray loopbackTone) {
+ jint testType, jdouble frequency1, jobject byteBuffer, jshortArray loopbackTone,
+ jint maxRecordedLateCallbacks, jobject captureHolder) {
sles_data * pSles = NULL;
@@ -32,9 +33,17 @@ 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) != SLES_FAIL) {
+ loopbackToneArray, maxRecordedLateCallbacks, captureHolder, jvm) != SLES_FAIL) {
return (long) pSles;
}
@@ -54,7 +63,7 @@ JNIEXPORT jint JNICALL Java_org_drrickorang_loopback_NativeAudioThread_slesProce
long availableSamples = maxSamples-offset;
double *pCurrentSample = pSamples+offset;
- SLES_PRINTF("jni slesProcessNext pSles:%p, currentSample %p, availableSamples %d ",
+ SLES_PRINTF("jni slesProcessNext pSles:%p, currentSample %p, availableSamples %ld ",
pSles, pCurrentSample, availableSamples);
int samplesRead = slesProcessNext(pSles, pCurrentSample, availableSamples);
@@ -70,8 +79,9 @@ JNIEXPORT jint JNICALL Java_org_drrickorang_loopback_NativeAudioThread_slesDestr
}
-JNIEXPORT jintArray JNICALL Java_org_drrickorang_loopback_NativeAudioThread_slesGetRecorderBufferPeriod
- (JNIEnv *env, jobject obj, jlong sles) {
+JNIEXPORT jintArray JNICALL
+ Java_org_drrickorang_loopback_NativeAudioThread_slesGetRecorderBufferPeriod
+ (JNIEnv *env, jobject obj __unused, jlong sles) {
sles_data * pSles = (sles_data*) (size_t) sles;
int* recorderBufferPeriod = slesGetRecorderBufferPeriod(pSles);
@@ -83,16 +93,18 @@ JNIEXPORT jintArray JNICALL Java_org_drrickorang_loopback_NativeAudioThread_sles
}
-JNIEXPORT jint JNICALL Java_org_drrickorang_loopback_NativeAudioThread_slesGetRecorderMaxBufferPeriod
+JNIEXPORT jint JNICALL
+ Java_org_drrickorang_loopback_NativeAudioThread_slesGetRecorderMaxBufferPeriod
(JNIEnv *env __unused, jobject obj __unused, jlong sles) {
sles_data * pSles = (sles_data*) (size_t) sles;
- int* recorderMaxBufferPeriod = slesGetRecorderMaxBufferPeriod(pSles);
+ int recorderMaxBufferPeriod = slesGetRecorderMaxBufferPeriod(pSles);
return recorderMaxBufferPeriod;
}
-JNIEXPORT jintArray JNICALL Java_org_drrickorang_loopback_NativeAudioThread_slesGetPlayerBufferPeriod
+JNIEXPORT jintArray
+JNICALL Java_org_drrickorang_loopback_NativeAudioThread_slesGetPlayerBufferPeriod
(JNIEnv *env __unused, jobject obj __unused, jlong sles) {
sles_data * pSles = (sles_data*) (size_t) sles;
int* playerBufferPeriod = slesGetPlayerBufferPeriod(pSles);
@@ -104,10 +116,40 @@ JNIEXPORT jintArray JNICALL Java_org_drrickorang_loopback_NativeAudioThread_sles
}
-JNIEXPORT jint JNICALL Java_org_drrickorang_loopback_NativeAudioThread_slesGetPlayerMaxBufferPeriod
+JNIEXPORT jint JNICALL
+ Java_org_drrickorang_loopback_NativeAudioThread_slesGetPlayerMaxBufferPeriod
(JNIEnv *env __unused, jobject obj __unused, jlong sles) {
sles_data * pSles = (sles_data*) (size_t) sles;
int playerMaxBufferPeriod = slesGetPlayerMaxBufferPeriod(pSles);
return playerMaxBufferPeriod;
}
+
+jobject getCallbackTimes(JNIEnv *env, callbackTimeStamps *callbacks, short expectedBufferPeriod){
+ jintArray timeStamps = (*env)->NewIntArray(env, callbacks->index);
+ (*env)->SetIntArrayRegion(env, timeStamps, 0, callbacks->index, callbacks->timeStampsMs);
+
+ jshortArray callbackLengths = (*env)->NewShortArray(env, callbacks->index);
+ (*env)->SetShortArrayRegion(env, callbackLengths, 0, callbacks->index,
+ callbacks->callbackDurations);
+
+ jclass cls = (*env)->FindClass(env, "org/drrickorang/loopback/BufferCallbackTimes");
+ jmethodID methodID = (*env)->GetMethodID(env, cls, "<init>", "([I[SZS)V");
+ jobject callbackTimes=(*env)->NewObject(env,cls, methodID, timeStamps, callbackLengths,
+ callbacks->exceededCapacity, expectedBufferPeriod);
+ return callbackTimes;
+}
+
+JNIEXPORT jobject
+JNICALL Java_org_drrickorang_loopback_NativeAudioThread_slesGetPlayerCallbackTimeStamps
+ (JNIEnv *env, jobject obj __unused, jlong sles) {
+ sles_data * pSles = (sles_data*) (size_t) sles;
+ return getCallbackTimes(env, &(pSles->playerTimeStamps), pSles->expectedBufferPeriod);
+}
+
+JNIEXPORT jobject
+JNICALL Java_org_drrickorang_loopback_NativeAudioThread_slesGetRecorderCallbackTimeStamps
+ (JNIEnv *env, jobject obj __unused, jlong sles) {
+ sles_data * pSles = (sles_data*) (size_t) sles;
+ return getCallbackTimes(env, &(pSles->recorderTimeStamps), pSles->expectedBufferPeriod);
+}
diff --git a/LoopbackApp/app/src/main/jni/jni_sles.h b/LoopbackApp/app/src/main/jni/jni_sles.h
index de62948..94b6a22 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);
+ jshortArray loopbackTone, jint maxRecordedLateCallbacks, jobject captureHolder);
JNIEXPORT jint JNICALL Java_org_drrickorang_loopback_NativeAudioThread_slesProcessNext
(JNIEnv *, jobject, jlong, jdoubleArray, jlong);
@@ -35,13 +35,16 @@ JNIEXPORT jint JNICALL Java_org_drrickorang_loopback_NativeAudioThread_slesProce
JNIEXPORT jint JNICALL Java_org_drrickorang_loopback_NativeAudioThread_slesDestroy
(JNIEnv *, jobject, jlong);
-JNIEXPORT jintArray JNICALL Java_org_drrickorang_loopback_NativeAudioThread_slesGetRecorderBufferPeriod
+JNIEXPORT jintArray JNICALL
+ Java_org_drrickorang_loopback_NativeAudioThread_slesGetRecorderBufferPeriod
(JNIEnv *, jobject, jlong);
-JNIEXPORT jint JNICALL Java_org_drrickorang_loopback_NativeAudioThread_slesGetRecorderMaxBufferPeriod
+JNIEXPORT jint JNICALL
+ Java_org_drrickorang_loopback_NativeAudioThread_slesGetRecorderMaxBufferPeriod
(JNIEnv *, jobject, jlong);
-JNIEXPORT jintArray JNICALL Java_org_drrickorang_loopback_NativeAudioThread_slesGetPlayerBufferPeriod
+JNIEXPORT jintArray JNICALL
+ Java_org_drrickorang_loopback_NativeAudioThread_slesGetPlayerBufferPeriod
(JNIEnv *, jobject, jlong);
JNIEXPORT jint JNICALL Java_org_drrickorang_loopback_NativeAudioThread_slesGetPlayerMaxBufferPeriod
diff --git a/LoopbackApp/app/src/main/jni/sles.cpp b/LoopbackApp/app/src/main/jni/sles.cpp
index 6110a74..387aa4e 100644
--- a/LoopbackApp/app/src/main/jni/sles.cpp
+++ b/LoopbackApp/app/src/main/jni/sles.cpp
@@ -30,17 +30,19 @@
#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) {
+ short* loopbackTone, int maxRecordedLateCallbacks, jobject captureHolder,
+ const struct JNIInvokeInterface* *jvm) {
int status = SLES_FAIL;
if (ppSles != NULL) {
sles_data * pSles = (sles_data*) malloc(sizeof(sles_data));
memset(pSles, 0, sizeof(sles_data));
- SLES_PRINTF("malloc %d bytes at %p", sizeof(sles_data), pSles);
+ SLES_PRINTF("malloc %zu bytes at %p", sizeof(sles_data), pSles);
//__android_log_print(ANDROID_LOG_INFO, "sles_jni",
//"malloc %d bytes at %p", sizeof(sles_data), pSles);//Or ANDROID_LOG_INFO, ...
*ppSles = pSles;
@@ -49,7 +51,8 @@ int slesInit(sles_data ** ppSles, int samplingRate, int frameCount, int micSourc
SLES_PRINTF("creating server. Sampling rate =%d, frame count = %d",
samplingRate, frameCount);
status = slesCreateServer(pSles, samplingRate, frameCount, micSource, testType,
- frequency1, byteBufferPtr, byteBufferLength, loopbackTone);
+ frequency1, byteBufferPtr, byteBufferLength, loopbackTone,
+ maxRecordedLateCallbacks, captureHolder, jvm);
SLES_PRINTF("slesCreateServer =%d", status);
}
}
@@ -193,25 +196,67 @@ 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)
+// 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");
+ }
+}
// Called in the beginning of recorderCallback() to collect the interval between each
// recorderCallback().
void collectRecorderBufferPeriod(sles_data *pSles) {
- struct timespec recorder_time;
- clock_gettime(CLOCK_MONOTONIC, &recorder_time);
+ clock_gettime(CLOCK_MONOTONIC, &(pSles->recorder_current_time));
+
+ if (pSles->recorder_buffer_count == 0){
+ pSles->recorderTimeStamps.startTime = pSles->recorder_current_time;
+ }
- pSles->recorder_current_time_sec = recorder_time.tv_sec;
- pSles->recorder_current_time_nsec = recorder_time.tv_nsec;
(pSles->recorder_buffer_count)++;
- if (pSles->recorder_previous_time_sec != 0 &&
+ if (pSles->recorder_previous_time.tv_sec != 0 &&
pSles->recorder_buffer_count > BUFFER_PERIOD_DISCARD){
- int diff_in_second = pSles->recorder_current_time_sec - pSles->recorder_previous_time_sec;
- long diff_in_nano = pSles->recorder_current_time_nsec - pSles->recorder_previous_time_nsec;
-
- // diff_in_milli is rounded up
- long long total_diff_in_nano = (diff_in_second * NANOS_PER_SECOND) + diff_in_nano;
- int diff_in_milli = (int) ((total_diff_in_nano + NANOS_PER_MILLI - 1) / NANOS_PER_MILLI);
+ 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;
@@ -225,10 +270,26 @@ void collectRecorderBufferPeriod(sles_data *pSles) {
} 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_sec = pSles->recorder_current_time_sec;
- pSles->recorder_previous_time_nsec = pSles->recorder_current_time_nsec;
+ pSles->recorder_previous_time = pSles->recorder_current_time;
}
@@ -333,21 +394,18 @@ static void playerCallback(SLBufferQueueItf caller __unused, void *context) {
// Called in the beginning of playerCallback() to collect the interval between each
// playerCallback().
void collectPlayerBufferPeriod(sles_data *pSles) {
- struct timespec player_time;
- clock_gettime(CLOCK_MONOTONIC, &player_time);
+ clock_gettime(CLOCK_MONOTONIC, &(pSles->player_current_time));
+
+ if (pSles->player_buffer_count == 0) {
+ pSles->playerTimeStamps.startTime = pSles->player_current_time;
+ }
- pSles->player_current_time_sec = player_time.tv_sec;
- pSles->player_current_time_nsec = player_time.tv_nsec;
(pSles->player_buffer_count)++;
- if (pSles->player_previous_time_sec != 0 &&
+ if (pSles->player_previous_time.tv_sec != 0 &&
pSles->player_buffer_count > BUFFER_PERIOD_DISCARD) {
- int diff_in_second = pSles->player_current_time_sec - pSles->player_previous_time_sec;
- long diff_in_nano = pSles->player_current_time_nsec - pSles->player_previous_time_nsec;
- // diff_in_milli is rounded up
- long long total_diff_in_nano = (diff_in_second * NANOS_PER_SECOND) + diff_in_nano;
- int diff_in_milli = (int) ((total_diff_in_nano + NANOS_PER_MILLI - 1) / NANOS_PER_MILLI);
+ int diff_in_milli = diffInMilli(pSles->player_previous_time, pSles->player_current_time);
if (diff_in_milli > pSles->player_max_buffer_period) {
pSles->player_max_buffer_period = diff_in_milli;
@@ -361,16 +419,32 @@ void collectPlayerBufferPeriod(sles_data *pSles) {
} else { // for diff_in_milli < 0
__android_log_print(ANDROID_LOG_INFO, "sles_player", "Having negative BufferPeriod.");
}
+
+ //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++;
+ }
+ }
}
- pSles->player_previous_time_sec = pSles->player_current_time_sec;
- pSles->player_previous_time_nsec = pSles->player_current_time_nsec;
+ pSles->player_previous_time = pSles->player_current_time;
}
int slesCreateServer(sles_data *pSles, int samplingRate, int frameCount, int micSource,
int testType, double frequency1, char *byteBufferPtr, int byteBufferLength,
- short *loopbackTone) {
+ short *loopbackTone, int maxRecordedLateCallbacks, jobject captureHolder,
+ const struct JNIInvokeInterface* *jvm) {
int status = SLES_FAIL;
if (pSles != NULL) {
@@ -495,19 +569,15 @@ int slesCreateServer(sles_data *pSles, int samplingRate, int frameCount, int mic
//init recorder buffer period data
pSles->recorder_buffer_period = new int[RANGE](); // initialized to zeros
- pSles->recorder_previous_time_sec = 0;
- pSles->recorder_previous_time_nsec = 0;
- pSles->recorder_current_time_sec = 0;
- pSles->recorder_current_time_nsec = 0;
+ 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_sec = 0;
- pSles->player_previous_time_nsec = 0;
- pSles->player_current_time_sec = 0;
- pSles->player_current_time_nsec = 0;
+ pSles->player_previous_time = {0,0};
+ pSles->player_current_time = {0,0};
pSles->player_buffer_count = 0;
pSles->player_max_buffer_period = 0;
@@ -522,6 +592,30 @@ int slesCreateServer(sles_data *pSles, int samplingRate, int frameCount, int mic
//init loopback tone
pSles->loopbackTone = loopbackTone;
+ pSles->recorderTimeStamps = {
+ new int[maxRecordedLateCallbacks], //int* timeStampsMs
+ new short[maxRecordedLateCallbacks], //short* callbackDurations
+ 0, //short index
+ {0,0}, //struct timespec startTime;
+ maxRecordedLateCallbacks, //int capacity
+ false //bool exceededCapacity
+ };
+
+ pSles->playerTimeStamps = {
+ new int[maxRecordedLateCallbacks], //int* timeStampsMs
+ new short[maxRecordedLateCallbacks], //short* callbackDurations;
+ 0, //short index
+ {0,0}, //struct timespec startTime;
+ maxRecordedLateCallbacks, //int capacity
+ false //bool exceededCapacity
+ };
+
+ pSles->expectedBufferPeriod = (short) (
+ round(pSles->bufSizeInFrames * MILLIS_PER_SECOND / (float) pSles->sampleRate));
+
+ pSles->captureHolder = captureHolder;
+ pSles->jvm = jvm;
+
SLresult result;
// create engine
@@ -735,7 +829,7 @@ int slesCreateServer(sles_data *pSles, int samplingRate, int frameCount, int mic
int slesProcessNext(sles_data *pSles, double *pSamples, long maxSamples) {
//int status = SLES_FAIL;
- SLES_PRINTF("slesProcessNext: pSles = %p, currentSample: %p, maxSamples = %d",
+ SLES_PRINTF("slesProcessNext: pSles = %p, currentSample: %p, maxSamples = %ld",
pSles, pSamples, maxSamples);
int samplesRead = 0;
@@ -788,7 +882,7 @@ int slesProcessNext(sles_data *pSles, double *pSamples, long maxSamples) {
&recorderBQState);
ASSERT_EQ(SL_RESULT_SUCCESS, result);
- SLES_PRINTF("End of slesProcessNext: pSles = %p, samplesRead = %d, maxSamples = %d",
+ SLES_PRINTF("End of slesProcessNext: pSles = %p, samplesRead = %d, maxSamples = %ld",
pSles, samplesRead, maxSamples);
}
return samplesRead;
@@ -884,4 +978,4 @@ int* slesGetPlayerBufferPeriod(sles_data *pSles) {
int slesGetPlayerMaxBufferPeriod(sles_data *pSles) {
return pSles->player_max_buffer_period;
-} \ No newline at end of file
+}
diff --git a/LoopbackApp/app/src/main/jni/sles.h b/LoopbackApp/app/src/main/jni/sles.h
index ca1ad97..0c5d709 100644
--- a/LoopbackApp/app/src/main/jni/sles.h
+++ b/LoopbackApp/app/src/main/jni/sles.h
@@ -19,6 +19,7 @@
#include <pthread.h>
#include <android/log.h>
#include <jni.h>
+#include <stdbool.h>
#ifndef _Included_org_drrickorang_loopback_sles
#define _Included_org_drrickorang_loopback_sles
@@ -32,6 +33,15 @@ extern "C" {
#endif
#include <audio_utils/fifo.h>
+typedef struct {
+ int* timeStampsMs; // Array of milliseconds since first callback
+ short* callbackDurations; // Array of milliseconds between callback and previous callback
+ short index; // Current write position
+ struct timespec startTime; // Time of first callback {seconds,nanoseconds}
+ int capacity; // Total number of callback times/lengths that can be recorded
+ bool exceededCapacity; // Set only if late callbacks come after array is full
+} callbackTimeStamps;
+
//TODO fix this
typedef struct {
SLuint32 rxBufCount; // -r#
@@ -73,18 +83,14 @@ typedef struct {
SLObjectItf engineObject;
int* recorder_buffer_period;
- int recorder_previous_time_sec;
- int recorder_previous_time_nsec;
- int recorder_current_time_sec;
- int recorder_current_time_nsec;
+ struct timespec recorder_previous_time;
+ struct timespec recorder_current_time;
int recorder_buffer_count;
int recorder_max_buffer_period;
int* player_buffer_period;
- time_t player_previous_time_sec;
- long player_previous_time_nsec;
- time_t player_current_time_sec;
- long player_current_time_nsec;
+ struct timespec player_previous_time;
+ struct timespec player_current_time;
int player_buffer_count;
int player_max_buffer_period;
@@ -96,6 +102,13 @@ typedef struct {
int byteBufferLength;
short* loopbackTone;
+
+ callbackTimeStamps recorderTimeStamps;
+ callbackTimeStamps playerTimeStamps;
+ short expectedBufferPeriod;
+
+ jobject captureHolder;
+ const struct JNIInvokeInterface* *jvm;
} sles_data;
enum {
@@ -103,6 +116,7 @@ enum {
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,
@@ -111,7 +125,8 @@ enum {
int slesInit(sles_data ** ppSles, int samplingRate, int frameCount, int micSource,
int testType, double frequency1, char* byteBufferPtr, int byteBufferLength,
- short* loopbackTone);
+ short* loopbackTone, int maxRecordedLateCallbacks, jobject captureHolder,
+ const struct JNIInvokeInterface* *jvm);
//note the double pointer to properly free the memory of the structure
int slesDestroy(sles_data ** ppSles);
@@ -122,7 +137,8 @@ int slesFull(sles_data *pSles);
int slesCreateServer(sles_data *pSles, int samplingRate, int frameCount, int micSource,
int testType, double frequency1, char* qbyteBufferPtr, int byteBufferLength,
- short* loopbackTone);
+ short* loopbackTone, int maxRecordedLateCallbacks, jobject captureHolder,
+ const struct JNIInvokeInterface* *jvm);
int slesProcessNext(sles_data *pSles, double *pSamples, long maxSamples);
int slesDestroyServer(sles_data *pSles);
int* slesGetRecorderBufferPeriod(sles_data *pSles);
@@ -132,6 +148,7 @@ int slesGetPlayerMaxBufferPeriod(sles_data *pSles);
void collectPlayerBufferPeriod(sles_data *pSles);
void collectRecorderBufferPeriod(sles_data *pSles);
+void captureState(sles_data *pSles, int rank);
ssize_t byteBuffer_write(sles_data *pSles, char *buffer, size_t count);
diff --git a/LoopbackApp/app/src/main/res/drawable-xxxhdpi/ic_help_outline.png b/LoopbackApp/app/src/main/res/drawable-xxxhdpi/ic_help_outline.png
new file mode 100644
index 0000000..dc5cdee
--- /dev/null
+++ b/LoopbackApp/app/src/main/res/drawable-xxxhdpi/ic_help_outline.png
Binary files differ
diff --git a/LoopbackApp/app/src/main/res/drawable-xxxhdpi/offtogglebutton.9.png b/LoopbackApp/app/src/main/res/drawable-xxxhdpi/offtogglebutton.9.png
new file mode 100644
index 0000000..205e009
--- /dev/null
+++ b/LoopbackApp/app/src/main/res/drawable-xxxhdpi/offtogglebutton.9.png
Binary files differ
diff --git a/LoopbackApp/app/src/main/res/drawable-xxxhdpi/ontogglebutton.9.png b/LoopbackApp/app/src/main/res/drawable-xxxhdpi/ontogglebutton.9.png
new file mode 100644
index 0000000..3f7f8df
--- /dev/null
+++ b/LoopbackApp/app/src/main/res/drawable-xxxhdpi/ontogglebutton.9.png
Binary files differ
diff --git a/LoopbackApp/app/src/main/res/drawable-xxxhdpi/togglebutton_state_drawable.xml b/LoopbackApp/app/src/main/res/drawable-xxxhdpi/togglebutton_state_drawable.xml
new file mode 100644
index 0000000..37b7374
--- /dev/null
+++ b/LoopbackApp/app/src/main/res/drawable-xxxhdpi/togglebutton_state_drawable.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item
+ android:drawable="@drawable/ontogglebutton"
+ android:state_checked="true" />
+ <item
+ android:drawable="@drawable/offtogglebutton"
+ android:state_checked="false" />
+</selector> \ No newline at end of file
diff --git a/LoopbackApp/app/src/main/res/layout/heatmap_window.xml b/LoopbackApp/app/src/main/res/layout/heatmap_window.xml
new file mode 100644
index 0000000..d1190a6
--- /dev/null
+++ b/LoopbackApp/app/src/main/res/layout/heatmap_window.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2016 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.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="#FFFFFF">
+
+<!-- <ScrollView
+ android:id="@+id/heatMapScroll"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:scrollbars="vertical"
+ android:fadeScrollbars="false"
+ android:fillViewport="true">
+ </ScrollView>-->
+
+</LinearLayout> \ No newline at end of file
diff --git a/LoopbackApp/app/src/main/res/layout/main_activity.xml b/LoopbackApp/app/src/main/res/layout/main_activity.xml
index 04fe52e..50df2f7 100644
--- a/LoopbackApp/app/src/main/res/layout/main_activity.xml
+++ b/LoopbackApp/app/src/main/res/layout/main_activity.xml
@@ -94,37 +94,58 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:orientation="horizontal">
+ android:orientation="vertical">
- <Button
+ <LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
- android:id="@+id/buttonGlitches"
- android:layout_width="wrap_content"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:drawableLeft="@drawable/ic_description"
- style="@style/TextAppearance.AppCompat.Button"
- android:text="@string/buttonGlitches"
- android:onClick="onButtonGlitches"/>
+ android:orientation="horizontal">
+ <Button
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/buttonRecorderBufferPeriod"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:drawableLeft="@drawable/ic_assessment"
+ style="@style/TextAppearance.AppCompat.Button"
+ android:text="@string/buttonRecorderBufferPeriod"
+ android:onClick="onButtonRecorderBufferPeriod"/>
- <Button
+ <Button
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/buttonPlayerBufferPeriod"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:drawableLeft="@drawable/ic_assessment"
+ style="@style/TextAppearance.AppCompat.Button"
+ android:text="@string/buttonPlayerBufferPeriod"
+ android:onClick="onButtonPlayerBufferPeriod"/>
+ </LinearLayout>
+ <LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
- android:id="@+id/buttonRecorderBufferPeriod"
- android:layout_width="wrap_content"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:drawableLeft="@drawable/ic_assessment"
- style="@style/TextAppearance.AppCompat.Button"
- android:text="@string/buttonRecorderBufferPeriod"
- android:onClick="onButtonRecorderBufferPeriod"/>
-
- <Button
- xmlns:android="http://schemas.android.com/apk/res/android"
- android:id="@+id/buttonPlayerBufferPeriod"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:drawableLeft="@drawable/ic_assessment"
- style="@style/TextAppearance.AppCompat.Button"
- android:text="@string/buttonPlayerBufferPeriod"
- android:onClick="onButtonPlayerBufferPeriod"/>
+ android:orientation="horizontal">
+ <Button
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/buttonGlitches"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:drawableLeft="@drawable/ic_description"
+ style="@style/TextAppearance.AppCompat.Button"
+ android:text="@string/buttonGlitches"
+ android:onClick="onButtonGlitches"/>
+ <Button
+ android:id="@+id/buttonHeatMap"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:drawableLeft="@drawable/ic_assessment"
+ style="@style/TextAppearance.AppCompat.Button"
+ android:text="@string/compareAll"
+ android:onClick="onButtonHeatMap"/>
+ </LinearLayout>
</LinearLayout>
</HorizontalScrollView>
diff --git a/LoopbackApp/app/src/main/res/layout/report_window.xml b/LoopbackApp/app/src/main/res/layout/report_window.xml
index 36d6b8b..6039374 100644
--- a/LoopbackApp/app/src/main/res/layout/report_window.xml
+++ b/LoopbackApp/app/src/main/res/layout/report_window.xml
@@ -29,6 +29,7 @@
android:fillViewport="true">
<TextView
android:id="@+id/ReportInfo"
+ android:padding="10dp"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:textSize="15sp" />
diff --git a/LoopbackApp/app/src/main/res/layout/settings_activity.xml b/LoopbackApp/app/src/main/res/layout/settings_activity.xml
index e136ebc..c76ce46 100644
--- a/LoopbackApp/app/src/main/res/layout/settings_activity.xml
+++ b/LoopbackApp/app/src/main/res/layout/settings_activity.xml
@@ -17,6 +17,7 @@
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/settingsMainLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
@@ -28,7 +29,8 @@
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:orientation="vertical">
+ android:orientation="vertical"
+ android:paddingBottom="150dp">
<TextView
android:id="@+id/textSettingsInfo"
@@ -158,8 +160,103 @@
<org.drrickorang.loopback.SettingsPicker
android:id="@+id/numLoadThreadsSetting"
android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:paddingBottom="100dp"/>
+ android:layout_height="wrap_content" />
+
+ <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="0dip"
+ android:layout_height="match_parent"
+ android:layout_weight="3">
+ <ToggleButton
+ android:id="@+id/SystraceEnabledToggle"
+ 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="0dip"
+ android:layout_height="match_parent"
+ android:layout_weight="6">
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/enableSystrace"/>
+ </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"
+ 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/enableWavSnippets"/>
+ </RelativeLayout>
+ <RelativeLayout
+ android:layout_width="0dip"
+ android:layout_height="match_parent"
+ android:layout_weight="1">
+ <!-- This empty layout is to match spacing of SystraceEnabled layout -->
+ </RelativeLayout>
+ </LinearLayout>
+
+ <View
+ android:layout_width="fill_parent"
+ android:layout_height="1dp"
+ android:background="@android:color/darker_gray"/>
+
+ <org.drrickorang.loopback.SettingsPicker
+ android:id="@+id/numCapturesSettingPicker"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"/>
</LinearLayout>
</ScrollView>
diff --git a/LoopbackApp/app/src/main/res/layout/settings_picker.xml b/LoopbackApp/app/src/main/res/layout/settings_picker.xml
index 5423b70..e533228 100644
--- a/LoopbackApp/app/src/main/res/layout/settings_picker.xml
+++ b/LoopbackApp/app/src/main/res/layout/settings_picker.xml
@@ -1,11 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2016 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.
+-->
+
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
- android:tag="title"
+ android:id="@+id/settings_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingBottom="20dp"/>
@@ -17,12 +32,12 @@
android:orientation="horizontal">
<RelativeLayout
- android:layout_width="0dip"
+ android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="3">
- <EditText
- android:tag="valueText"
+ <org.drrickorang.loopback.CatchEventsEditText
+ android:id="@+id/settings_valueText"
android:inputType="number"
android:layout_width="match_parent"
android:layout_height="wrap_content"
@@ -33,12 +48,12 @@
</RelativeLayout>
<RelativeLayout
- android:layout_width="0dip"
+ android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="7">
<SeekBar
- android:tag="seekbar"
+ android:id="@+id/settings_seekbar"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</RelativeLayout>
diff --git a/LoopbackApp/app/src/main/res/raw/loopback_listener b/LoopbackApp/app/src/main/res/raw/loopback_listener
new file mode 100644
index 0000000..030f594
--- /dev/null
+++ b/LoopbackApp/app/src/main/res/raw/loopback_listener
@@ -0,0 +1,69 @@
+#!/system/bin/sh
+
+####
+# Continuously checks for presence of signal file in 1 second intervals
+# Reads either a filename prefix or termination signal from file when it exists
+# Writes output of atrace and bugreport to files with supplied prefix
+####
+
+SYSTRACE_SUFFIX=".trace";
+BUGREPORT_SUFFIX="_bugreport.txt.gz";
+TERMINATE_SIGNAL="QUIT";
+SIGNAL_FILE="/sdcard/Loopback/loopback_signal"
+TRACE_CATEGORIES="sched audio"
+BUFFER_KB="8000"
+
+function exitListener {
+ # Exit atrace, remove signal file, and exit
+
+ echo "LOOPBACK LISTENER: stopping trace before exiting"
+ rm $SIGNAL_FILE
+ atrace --async_stop -z > /dev/null
+ echo "LOOPBACK LISTENER: exiting"
+ exit 1
+}
+
+# Begin an asynchronous systrace writing into a circular buffer of size BUFFER_KB
+echo "LOOPBACK LISTENER: starting trace"
+atrace --async_start -z -c -b $BUFFER_KB $TRACE_CATEGORIES
+echo " "
+
+# Remove signal file erroneously left behind from previous tests
+if [ -e "$SIGNAL_FILE" ]; then rm $SIGNAL_FILE; fi
+
+while true
+do
+ #echo "LOOPBACK LISTENER: checking for file $SIGNAL_FILE"
+ if [ -e "$SIGNAL_FILE" ] && [ -s "$SIGNAL_FILE" ]
+ then
+ contents=$(cat $SIGNAL_FILE)
+
+ # Ensure that if more than one listener is running only one will consume signal
+ > $SIGNAL_FILE
+
+ if [ $contents == $TERMINATE_SIGNAL ]
+ then
+ exitListener
+ else
+ # write Systrace and bugreport to files
+
+ echo "LOOPBACK LISTENER: dumping systrace to file $contents$SYSTRACE_SUFFIX"
+ atrace --async_dump -z -c -b $BUFFER_KB $TRACE_CATEGORIES > $contents$SYSTRACE_SUFFIX
+
+ echo "LOOPBACK LISTENER: dumping bugreport to file $contents$BUGREPORT_SUFFIX"
+ bugreport | gzip > $contents$BUGREPORT_SUFFIX
+
+ echo "LOOPBACK LISTENER: Finished systrace and bugreport"
+
+ # Check for case that test has ended while capturing state and exit
+ if [ -e "$SIGNAL_FILE" ] && [ -s "$SIGNAL_FILE" ] \
+ && [ $(cat $SIGNAL_FILE) == $TERMINATE_SIGNAL ]
+ then
+ exitListener
+ fi
+
+ rm $SIGNAL_FILE
+ fi
+ fi
+ sleep 1
+done
diff --git a/LoopbackApp/app/src/main/res/values/strings.xml b/LoopbackApp/app/src/main/res/values/strings.xml
index 4da727f..13577fb 100644
--- a/LoopbackApp/app/src/main/res/values/strings.xml
+++ b/LoopbackApp/app/src/main/res/values/strings.xml
@@ -82,10 +82,17 @@
\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t 222: Latency Test\n
\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t 223: Buffer Test\n
-ei BufferTestDuration \t ####\t\t Buffer Test Duration \n
- -ei NumLoadThreads \t ####\t\t Number of Simulated Load Threads (0 - ~10)\n\n\n
- Example: adb shell am start -n org.drrickorang.loopback/.LoopbackActivity
+ -ei NumLoadThreads \t ####\t\t Number of Simulated Load Threads (0 - 20) \n
+ -ei CI \t ####\t\t Channel Index for USB Audio (0 - 8) \n
+ -ez CaptureSysTrace \t ####\t\t enable/disable systrace and bugreport capturing \n
+ -ez CaptureWavs \t ####\t\t enable/disable .wav file snippets capturing \n
+ -ei NumCaptures \t ####\t\t Number of systrace/bugreport and/or wav snippets to capture \n
+ -ei WaveDuration \t ####\t\t Duration in seconds of captured wave files
+ \n\n\n
+
+ Example: adb shell am start -n org.drrickorang.loopback/.LoopbackActivity \n
--ei SF 48000 --es FileName output --ei MicSource 3 --ei AudioThread 1 --ei AudioLevel 12
- --ei TestType 223 --ei BufferTestDuration 5
+ --ei TestType 223 --ei BufferTestDuration 5 --ez CaptureWavs true --ei NumCaptures 5
</string>
<!-- spinnerSamplingRate Options -->
@@ -128,8 +135,30 @@
<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="enableWavSnippets">Wav Snippet Captures During Test</string>
+ <string name="numCapturesSetting">Number of Systrace/BugReport and or Wav Snippets to Capture
+ </string>
+
<string name="SaveFileDialogLabel">Save Files To:</string>
<string name="SaveFileDialogOK">//mnt/sdcard/</string>
<string name="SaveFileDialogChooseFilenames">Choose Filenames \n and Location</string>
-
+ <string name="heatTitle">Glitches and Callbacks over Time</string>
+ <string name="compareAll">Compare All</string>
+
+ <string name="systraceHelp">
+ To use this feature it is necessary to launch a shell script on the Android device using ADB
+ shell. This script is responsible for launching an asynchronous systrace and writing its
+ buffer to file when signaled by the Loopback App.
+ \n\n
+ At the completion of the audio glitch/buffer test the Loopback App will signal the script to
+ exit so the script must be started before each test.
+ \n\n
+ The Loopback App places this script on the Android device when launched at
+ \n
+ /sdcard/Loopback/loopback_listener
+ \n\n
+ Example invocation:\n
+ adb shell \"sh /sdcard/Loopback/loopback_listener\"
+ </string>
</resources>
diff --git a/LoopbackApp/proguard.cfg b/LoopbackApp/proguard.cfg
index 6e43fd3..1c439e0 100644
--- a/LoopbackApp/proguard.cfg
+++ b/LoopbackApp/proguard.cfg
@@ -1,2 +1,5 @@
-keep class org.drrickorang.loopback.NativeAudioThread {
}
+-keep class org.drrickorang.loopback.BufferCallbackTimes.java {
+ public <init> (int[], short[], boolean, short);
+} \ No newline at end of file