diff options
author | Glenn Kasten <gkasten@google.com> | 2016-03-18 11:17:33 -0700 |
---|---|---|
committer | Glenn Kasten <gkasten@google.com> | 2016-03-18 11:17:33 -0700 |
commit | c2485a223ff13085337865dd81ccda7063431203 (patch) | |
tree | 973d1b2a7d9e3f9e5b75ff229b4d225bcf525a25 /LoopbackApp/app/src/main | |
parent | 0ea71f7b39277601f41dfd7a600433c56db2466e (diff) | |
download | drrickorang-c2485a223ff13085337865dd81ccda7063431203.tar.gz |
Snap to commit d5cc4bf4ebe772b0de598f8e456ac7a33d41fde7
Version 10
Diffstat (limited to 'LoopbackApp/app/src/main')
39 files changed, 2220 insertions, 395 deletions
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 Binary files differnew file mode 100644 index 0000000..dc5cdee --- /dev/null +++ b/LoopbackApp/app/src/main/res/drawable-xxxhdpi/ic_help_outline.png diff --git a/LoopbackApp/app/src/main/res/drawable-xxxhdpi/offtogglebutton.9.png b/LoopbackApp/app/src/main/res/drawable-xxxhdpi/offtogglebutton.9.png Binary files differnew file mode 100644 index 0000000..205e009 --- /dev/null +++ b/LoopbackApp/app/src/main/res/drawable-xxxhdpi/offtogglebutton.9.png diff --git a/LoopbackApp/app/src/main/res/drawable-xxxhdpi/ontogglebutton.9.png b/LoopbackApp/app/src/main/res/drawable-xxxhdpi/ontogglebutton.9.png Binary files differnew file mode 100644 index 0000000..3f7f8df --- /dev/null +++ b/LoopbackApp/app/src/main/res/drawable-xxxhdpi/ontogglebutton.9.png 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> |