summaryrefslogtreecommitdiff
path: root/LoopbackApp
diff options
context:
space:
mode:
authorGlenn Kasten <gkasten@google.com>2016-02-09 15:59:53 -0800
committerGlenn Kasten <gkasten@google.com>2016-02-09 16:00:28 -0800
commit1be6921955b496665686edb1a96071f8f47c7409 (patch)
tree2c4cf810fdfab71388e202b139fb1d6e7eb896c0 /LoopbackApp
parenteaacb9345a5043bbc7c4f908cae4fdbc02b5bff9 (diff)
downloaddrrickorang-1be6921955b496665686edb1a96071f8f47c7409.tar.gz
Snap to commit 59d4f38aeaca9c4526b09b9fa9363ed7bcaf9fc9
Diffstat (limited to 'LoopbackApp')
-rw-r--r--LoopbackApp/app/src/main/AndroidManifest.xml18
-rw-r--r--LoopbackApp/app/src/main/java/org/drrickorang/loopback/Constant.java21
-rw-r--r--LoopbackApp/app/src/main/java/org/drrickorang/loopback/Correlation.java17
-rw-r--r--LoopbackApp/app/src/main/java/org/drrickorang/loopback/GlitchDetectionThread.java101
-rw-r--r--LoopbackApp/app/src/main/java/org/drrickorang/loopback/GlitchesActivity.java89
-rw-r--r--LoopbackApp/app/src/main/java/org/drrickorang/loopback/GlitchesStringBuilder.java90
-rw-r--r--LoopbackApp/app/src/main/java/org/drrickorang/loopback/HistogramView.java10
-rw-r--r--LoopbackApp/app/src/main/java/org/drrickorang/loopback/LoadThread.java3
-rw-r--r--LoopbackApp/app/src/main/java/org/drrickorang/loopback/LoopbackActivity.java908
-rw-r--r--LoopbackApp/app/src/main/java/org/drrickorang/loopback/LoopbackApplication.java53
-rw-r--r--LoopbackApp/app/src/main/java/org/drrickorang/loopback/LoopbackAudioThread.java54
-rw-r--r--LoopbackApp/app/src/main/java/org/drrickorang/loopback/NativeAudioThread.java41
-rw-r--r--LoopbackApp/app/src/main/java/org/drrickorang/loopback/PerformanceMeasurement.java23
-rw-r--r--LoopbackApp/app/src/main/java/org/drrickorang/loopback/Pipe.java2
-rw-r--r--LoopbackApp/app/src/main/java/org/drrickorang/loopback/RampedSineTone.java70
-rw-r--r--LoopbackApp/app/src/main/java/org/drrickorang/loopback/RecorderRunnable.java110
-rw-r--r--LoopbackApp/app/src/main/java/org/drrickorang/loopback/SaveFilesDialogFragment.java77
-rw-r--r--LoopbackApp/app/src/main/java/org/drrickorang/loopback/SettingsActivity.java256
-rw-r--r--LoopbackApp/app/src/main/java/org/drrickorang/loopback/SettingsPicker.java140
-rw-r--r--LoopbackApp/app/src/main/java/org/drrickorang/loopback/SineWaveTone.java1
-rw-r--r--LoopbackApp/app/src/main/java/org/drrickorang/loopback/ToneGeneration.java5
-rw-r--r--LoopbackApp/app/src/main/java/org/drrickorang/loopback/TwoSineWavesTone.java1
-rw-r--r--LoopbackApp/app/src/main/java/org/drrickorang/loopback/Utilities.java3
-rw-r--r--LoopbackApp/app/src/main/java/org/drrickorang/loopback/WaveDataRingBuffer.java96
-rw-r--r--LoopbackApp/app/src/main/jni/jni_sles.c7
-rw-r--r--LoopbackApp/app/src/main/jni/jni_sles.h3
-rw-r--r--LoopbackApp/app/src/main/jni/sles.cpp38
-rw-r--r--LoopbackApp/app/src/main/jni/sles.h10
-rw-r--r--LoopbackApp/app/src/main/res/drawable-xxxhdpi/ic_assessment.pngbin0 -> 287 bytes
-rw-r--r--LoopbackApp/app/src/main/res/drawable-xxxhdpi/ic_description.pngbin0 -> 355 bytes
-rw-r--r--LoopbackApp/app/src/main/res/drawable-xxxhdpi/ic_help.pngbin0 -> 3196 bytes
-rw-r--r--LoopbackApp/app/src/main/res/drawable-xxxhdpi/ic_launcher.pngbin41702 -> 22271 bytes
-rw-r--r--LoopbackApp/app/src/main/res/drawable-xxxhdpi/ic_play_arrow.pngbin0 -> 1591 bytes
-rw-r--r--LoopbackApp/app/src/main/res/drawable-xxxhdpi/ic_report.pngbin0 -> 2213 bytes
-rw-r--r--LoopbackApp/app/src/main/res/drawable-xxxhdpi/ic_save.pngbin0 -> 2923 bytes
-rw-r--r--LoopbackApp/app/src/main/res/drawable-xxxhdpi/ic_settings.pngbin0 -> 5078 bytes
-rw-r--r--LoopbackApp/app/src/main/res/drawable-xxxhdpi/ic_stop.pngbin0 -> 1120 bytes
-rw-r--r--LoopbackApp/app/src/main/res/drawable-xxxhdpi/ic_zoom_in.pngbin0 -> 3528 bytes
-rw-r--r--LoopbackApp/app/src/main/res/drawable-xxxhdpi/ic_zoom_out.pngbin0 -> 3204 bytes
-rw-r--r--LoopbackApp/app/src/main/res/drawable-xxxhdpi/ic_zoom_out_full.pngbin0 -> 4009 bytes
-rw-r--r--LoopbackApp/app/src/main/res/layout/main_activity.xml240
-rw-r--r--LoopbackApp/app/src/main/res/layout/report_window.xml (renamed from LoopbackApp/app/src/main/res/layout/glitches_activity.xml)5
-rw-r--r--LoopbackApp/app/src/main/res/layout/settings_activity.xml73
-rw-r--r--LoopbackApp/app/src/main/res/layout/settings_picker.xml48
-rw-r--r--LoopbackApp/app/src/main/res/menu/tool_bar_menu.xml13
-rw-r--r--LoopbackApp/app/src/main/res/values/strings.xml36
46 files changed, 1789 insertions, 873 deletions
diff --git a/LoopbackApp/app/src/main/AndroidManifest.xml b/LoopbackApp/app/src/main/AndroidManifest.xml
index b3cecf7..488415b 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="8"
- android:versionName="0.8">
+ android:versionCode="9"
+ android:versionName="0.9">
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
@@ -55,7 +55,9 @@
android:parentActivityName="org.drrickorang.loopback.LoopbackActivity"
android:screenOrientation="sensorPortrait"
android:theme="@android:style/Theme.Holo.Light"
- android:configChanges="orientation|keyboardHidden|screenLayout">
+ android:configChanges="orientation|screenLayout"
+ android:windowSoftInputMode="stateAlwaysHidden"
+ >
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="org.drrickorang.loopback.LoopbackActivity"/>
@@ -91,15 +93,5 @@
android:value="org.drrickorang.loopback.LoopbackActivity" />
</activity>
- <activity
- android:name="org.drrickorang.loopback.GlitchesActivity"
- android:label="List of Glitches"
- android:parentActivityName="org.drrickorang.loopback.LoopbackActivity"
- android:theme="@android:style/Theme.Holo.Light">
- <meta-data
- android:name="android.support.PARENT_ACTIVITY"
- android:value="org.drrickorang.loopback.LoopbackActivity" />
- </activity>
-
</application>
</manifest>
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 de84b1f..861c7fd 100644
--- a/LoopbackApp/app/src/main/java/org/drrickorang/loopback/Constant.java
+++ b/LoopbackApp/app/src/main/java/org/drrickorang/loopback/Constant.java
@@ -25,6 +25,7 @@ public class Constant {
public static final double TWO_PI = 2.0 * Math.PI;
public static final long NANOS_PER_MILLI = 1000000;
public static final int MILLIS_PER_SECOND = 1000;
+ public static final int SECONDS_PER_HOUR = 3600;
public static final int LOOPBACK_PLUG_AUDIO_THREAD_TEST_TYPE_LATENCY = 222;
public static final int LOOPBACK_PLUG_AUDIO_THREAD_TEST_TYPE_BUFFER_PERIOD = 223;
@@ -52,4 +53,24 @@ public class Constant {
// used when joining a thread
public static final int JOIN_WAIT_TIME_MS = 1000;
+
+ // Loopback on Java thread test audio tone constants
+ public static final int LOOPBACK_SAMPLE_FRAMES = 300;
+ public static final double LOOPBACK_AMPLITUDE = 0.95;
+ public static final int LOOPBACK_FREQUENCY = 4000;
+
+ // Settings Activity and ADB constants
+ public static final int SAMPLING_RATE_MAX = 48000;
+ public static final int SAMPLING_RATE_MIN = 8000;
+ public static final int PLAYER_BUFFER_FRAMES_MAX = 8000;
+ public static final int PLAYER_BUFFER_FRAMES_MIN = 16;
+ public static final int RECORDER_BUFFER_FRAMES_MAX = 8000;
+ public static final int RECORDER_BUFFER_FRAMES_MIN = 16;
+ public static final int BUFFER_TEST_DURATION_SECONDS_MAX = 36000;
+ public static final int BUFFER_TEST_DURATION_SECONDS_MIN = 1;
+ public static final int BUFFER_TEST_WAVE_PLOT_DURATION_SECONDS_MAX = 120;
+ 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;
+
}
diff --git a/LoopbackApp/app/src/main/java/org/drrickorang/loopback/Correlation.java b/LoopbackApp/app/src/main/java/org/drrickorang/loopback/Correlation.java
index 1e92fca..1094fb0 100644
--- a/LoopbackApp/app/src/main/java/org/drrickorang/loopback/Correlation.java
+++ b/LoopbackApp/app/src/main/java/org/drrickorang/loopback/Correlation.java
@@ -37,6 +37,8 @@ public class Correlation {
private double mAmplitudeThreshold = 0.001; // 0.001 = -60 dB noise
+ private boolean mDataIsValid = false; // Used to mark computed latency information is available
+
public void init(int blockSize, int samplingRate) {
mBlockSize = blockSize;
@@ -44,8 +46,7 @@ public class Correlation {
}
- public boolean computeCorrelation(double [] data, int samplingRate) {
- boolean status;
+ public void computeCorrelation(double [] data, int samplingRate) {
log("Started Auto Correlation for data with " + data.length + " points");
mSamplingRate = samplingRate;
@@ -98,10 +99,18 @@ public class Correlation {
log(String.format(" latencySamples: %.2f %.2f ms", mEstimatedLatencySamples,
mEstimatedLatencyMs));
- status = true;
- return status;
+ mDataIsValid = mEstimatedLatencyMs > 0.0001;
}
+ // Called by LoopbackActivity before displaying latency test results
+ public boolean isValid() {
+ return mDataIsValid;
+ }
+
+ // Called at beginning of new test
+ public void invalidate() {
+ mDataIsValid = false;
+ }
private boolean downsampleData(double [] data, double [] dataDownsampled, double threshold) {
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 9ae5a93..78cbc9d 100644
--- a/LoopbackApp/app/src/main/java/org/drrickorang/loopback/GlitchDetectionThread.java
+++ b/LoopbackApp/app/src/main/java/org/drrickorang/loopback/GlitchDetectionThread.java
@@ -39,22 +39,27 @@ public class GlitchDetectionThread extends Thread {
private double mDoubleBuffer[]; // keep the data used for FFT calculation
private boolean mIsFirstFFT = true; // whether or not it's the first FFT calculation
- private double mWaveData[]; // data that will be plotted
- private int mWaveDataIndex = 0;
-
- private double mFrequency1;
- private double mFrequency2; //currently not used
- private int mSamplingRate;
- private int mFFTSamplingSize; // amount of samples used to perform a FFT
- private int mFFTOverlapSamples; // amount of overlapped samples used between two FFTs
- private int mNewSamplesPerFFT; // amount of new samples (not from last FFT) in a FFT
+
+ private WaveDataRingBuffer mWaveDataRing; // Record last n seconds of wave data
+
+ private final double mFrequency1;
+ private final double mFrequency2; //currently not used
+ private final int mSamplingRate;
+ private final int mFFTSamplingSize; // amount of samples used to perform a FFT
+ private final int mFFTOverlapSamples; // amount of overlapped samples used between two FFTs
+ private final int mNewSamplesPerFFT; // amount of new samples (not from last FFT) in a FFT
private double mCenterOfMass; // expected center of mass of samples
- private int[] mGlitches; // for every value = n, n is the nth FFT where a glitch is found
+
+ private final int[] mGlitches; // for every value = n, n is nth FFT where a glitch is found
private int mGlitchesIndex;
private int mFFTCount; // store the current number of FFT performed
private FFT mFFT;
private boolean mGlitchingIntervalTooLong = false; // true if mGlitches is full
+ //Pre-Allocated buffers for glitch detection process
+ private final double[] mFFTResult;
+ private final double[] mCurrentSamples;
+ private final double[] mImagArray;
GlitchDetectionThread(double frequency1, double frequency2, int samplingRate,
int FFTSamplingSize, int FFTOverlapSamples, int bufferTestDurationInSeconds,
@@ -70,17 +75,22 @@ public class GlitchDetectionThread extends Thread {
mShortBuffer = new short[mFFTSamplingSize];
mDoubleBuffer = new double[mFFTSamplingSize];
- mWaveData = new double[mSamplingRate * bufferTestWavePlotDurationInSeconds];
+ mWaveDataRing = new WaveDataRingBuffer(mSamplingRate * bufferTestWavePlotDurationInSeconds);
final int acceptableGlitchingIntervalsPerSecond = 10;
mGlitches = new int[bufferTestDurationInSeconds * acceptableGlitchingIntervalsPerSecond];
- Arrays.fill(mGlitches, 0);
mGlitchesIndex = 0;
- mFFTCount = 1;
+ mFFTCount = 0;
+
+ mFFTResult = new double[mFFTSamplingSize/2];
+ mCurrentSamples = new double[mFFTSamplingSize];
+ mImagArray = new double[mFFTSamplingSize];
mFFT = new FFT(mFFTSamplingSize);
computeExpectedCenterOfMass();
+ setName("Loopback_GlitchDetection");
+
mThreadSleepDurationMs = FFTOverlapSamples * Constant.MILLIS_PER_SECOND / mSamplingRate;
if (mThreadSleepDurationMs < 1) {
mThreadSleepDurationMs = 1; // sleeps at least 1ms
@@ -112,22 +122,11 @@ public class GlitchDetectionThread extends Thread {
// copy data in mDoubleBuffer to mWaveData
if (mIsFirstFFT) {
// if it's the first FFT, copy the whole "mNativeBuffer" to mWaveData
- System.arraycopy(mDoubleBuffer, 0, mWaveData,
- mWaveDataIndex, mFFTSamplingSize);
- mWaveDataIndex += mFFTSamplingSize;
+ mWaveDataRing.writeWaveData(mDoubleBuffer, 0, mFFTSamplingSize);
mIsFirstFFT = false;
} else {
- // if mWaveData is all filled, clear it then starting writing from beginning.
- //TODO make mWaveData into a circular buffer storing the last N seconds instead
- if ((mWaveDataIndex + mNewSamplesPerFFT) >= mWaveData.length) {
- Arrays.fill(mWaveData, 0);
- mWaveDataIndex = 0;
- }
-
- // if it's not the first FFT, copy the new data in "mNativeBuffer" to mWaveData
- System.arraycopy(mDoubleBuffer, mFFTOverlapSamples, mWaveData,
- mWaveDataIndex, mNewSamplesPerFFT);
- mWaveDataIndex += mFFTOverlapSamples;
+ mWaveDataRing.writeWaveData(mDoubleBuffer, mFFTOverlapSamples,
+ mNewSamplesPerFFT);
}
detectGlitches();
@@ -171,25 +170,26 @@ public class GlitchDetectionThread extends Thread {
*/
private void detectGlitches() {
double centerOfMass;
- double[] fftResult;
- double[] currentSamples;
- currentSamples = Arrays.copyOfRange(mDoubleBuffer, 0, mDoubleBuffer.length);
- currentSamples = Utilities.hanningWindow(currentSamples);
- double width = (double) mSamplingRate / currentSamples.length;
- fftResult = computeFFT(currentSamples); // gives an array of sampleSize / 2
+ // retrieve a copy of recorded wave data for manipulating and analyzing
+ System.arraycopy(mDoubleBuffer, 0, mCurrentSamples, 0, mDoubleBuffer.length);
+
+ Utilities.hanningWindow(mCurrentSamples);
+
+ double width = (double) mSamplingRate / mCurrentSamples.length;
+ computeFFT(mCurrentSamples, mFFTResult); // gives an array of sampleSize / 2
final double threshold = 0.1;
// for all elements in the FFT result that are smaller than threshold,
// eliminate them as they are probably noise
- for (int j = 0; j < fftResult.length; j++) {
- if (fftResult[j] < threshold) {
- fftResult[j] = 0;
+ for (int j = 0; j < mFFTResult.length; j++) {
+ if (mFFTResult[j] < threshold) {
+ mFFTResult[j] = 0;
}
}
// calculate the center of mass of sample's FFT
- centerOfMass = computeCenterOfMass(fftResult, width);
+ centerOfMass = computeCenterOfMass(mFFTResult, width);
double difference = (Math.abs(centerOfMass - mCenterOfMass) / mCenterOfMass);
if (mGlitchesIndex >= mGlitches.length) {
// we just want to show this log once and set the flag once.
@@ -229,19 +229,15 @@ public class GlitchDetectionThread extends Thread {
/** Compute FFT of a set of data "samples". */
- private double[] computeFFT(double[] realArray) {
- int length = realArray.length;
- double[] imagArray = new double[length]; // all zeros
- Arrays.fill(imagArray, 0);
- mFFT.fft(realArray, imagArray, 1); // here realArray and imagArray get set
+ private void computeFFT(double[] src, double[] dst) {
+ Arrays.fill(mImagArray, 0);
+ mFFT.fft(src, mImagArray, 1); // here src array and imagArray get set
- double[] absValue = new double[length / 2]; // don't use second portion of arrays
- for (int i = 0; i < (length / 2); i++) {
- absValue[i] = Math.sqrt(realArray[i] * realArray[i] + imagArray[i] * imagArray[i]);
+ for (int i = 0; i < (src.length / 2); i++) {
+ dst[i] = Math.sqrt(src[i] * src[i] + mImagArray[i] * mImagArray[i]);
}
- return absValue;
}
@@ -250,13 +246,13 @@ public class GlitchDetectionThread extends Thread {
SineWaveTone sineWaveTone = new SineWaveTone(mSamplingRate, mFrequency1);
double[] sineWave = new double[mFFTSamplingSize];
double centerOfMass;
- double[] sineFFTResult;
+ double[] sineFFTResult = new double[mFFTSamplingSize/2];
sineWaveTone.generateTone(sineWave, mFFTSamplingSize);
- sineWave = Utilities.hanningWindow(sineWave);
+ Utilities.hanningWindow(sineWave);
double width = (double) mSamplingRate / sineWave.length;
- sineFFTResult = computeFFT(sineWave); // gives an array of sample sizes / 2
+ computeFFT(sineWave, sineFFTResult); // gives an array of sample sizes / 2
centerOfMass = computeCenterOfMass(sineFFTResult, width); // return center of mass
mCenterOfMass = centerOfMass;
log("the expected center of mass:" + Double.toString(mCenterOfMass));
@@ -264,7 +260,7 @@ public class GlitchDetectionThread extends Thread {
public double[] getWaveData() {
- return mWaveData;
+ return mWaveDataRing.getWaveRecord();
}
@@ -274,7 +270,10 @@ public class GlitchDetectionThread extends Thread {
public int[] getGlitches() {
- return mGlitches;
+ //return a copy of recorded glitches in an array sized to hold only recorded glitches
+ int[] output = new int[mGlitchesIndex];
+ System.arraycopy(mGlitches, 0, output, 0, mGlitchesIndex);
+ return output;
}
diff --git a/LoopbackApp/app/src/main/java/org/drrickorang/loopback/GlitchesActivity.java b/LoopbackApp/app/src/main/java/org/drrickorang/loopback/GlitchesActivity.java
deleted file mode 100644
index 0c31289..0000000
--- a/LoopbackApp/app/src/main/java/org/drrickorang/loopback/GlitchesActivity.java
+++ /dev/null
@@ -1,89 +0,0 @@
-/*
- * Copyright (C) 2015 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.app.Activity;
-import android.os.Bundle;
-import android.util.Log;
-import android.view.View;
-import android.widget.TextView;
-
-
-/**
- * This activity shows a list of time intervals where a glitch occurs.
- */
-
-public class GlitchesActivity extends Activity {
- private static final String TAG = "GlitchesActivity";
-
-
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
-
- View view = getLayoutInflater().inflate(R.layout.glitches_activity, null);
- setContentView(view);
-
- Bundle bundle = getIntent().getExtras();
- int FFTSamplingSize = bundle.getInt("FFTSamplingSize");
- int FFTOverlapSamples = bundle.getInt("FFTOverlapSamples");
- int[] glitchesData = bundle.getIntArray("glitchesArray");
- int samplingRate = bundle.getInt("samplingRate");
- boolean glitchingIntervalTooLong = bundle.getBoolean("glitchingIntervalTooLong");
- int newSamplesPerFFT = FFTSamplingSize - FFTOverlapSamples;
- int numberOfGlitches = bundle.getInt("numberOfGlitches");
-
- // the time span of new samples for a single FFT in ms
- double newSamplesInMs = ((double) newSamplesPerFFT / samplingRate) *
- Constant.MILLIS_PER_SECOND;
- log("newSamplesInMs: " + Double.toString(newSamplesInMs));
-
- // the time span of all samples for a single FFT in ms
- double allSamplesInMs = ((double) FFTSamplingSize / samplingRate) *
- Constant.MILLIS_PER_SECOND;
- log("allSamplesInMs: " + Double.toString(allSamplesInMs));
-
- StringBuilder listOfGlitches = new StringBuilder();
- listOfGlitches.append("Total Glitching Interval too long: " +
- glitchingIntervalTooLong + "\n");
- listOfGlitches.append("Estimated number of glitches: " + numberOfGlitches + "\n");
- listOfGlitches.append("List of glitching intervals: \n");
-
- int timeInMs; // starting time of glitches
- for (int i = 0; i < glitchesData.length; i++) {
- //log("glitchesData" + i + " :" + glitchesData[i]);
- if (glitchesData[i] > 0) {
- //append the time of glitches to "listOfGlitches"
- timeInMs = (int) ((glitchesData[i] - 1) * newSamplesInMs); // round down
- listOfGlitches.append(Integer.toString(timeInMs) + "~" +
- Integer.toString(timeInMs + (int) allSamplesInMs) + "ms\n");
- }
- }
-
-
-
- // Set the textView
- TextView textView = (TextView) findViewById(R.id.GlitchesInfo);
- textView.setTextSize(12);
- textView.setText(listOfGlitches.toString());
- }
-
-
- private static void log(String msg) {
- Log.v(TAG, msg);
- }
-
-}
diff --git a/LoopbackApp/app/src/main/java/org/drrickorang/loopback/GlitchesStringBuilder.java b/LoopbackApp/app/src/main/java/org/drrickorang/loopback/GlitchesStringBuilder.java
new file mode 100644
index 0000000..a1770e3
--- /dev/null
+++ b/LoopbackApp/app/src/main/java/org/drrickorang/loopback/GlitchesStringBuilder.java
@@ -0,0 +1,90 @@
+/*
+ * 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.app.Activity;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.View;
+import android.widget.TextView;
+
+
+/**
+ * Creates a list of time intervals where glitches occurred.
+ */
+
+public class GlitchesStringBuilder {
+ private static final String TAG = "GlitchesStringBuilder";
+
+
+ public static String getGlitchString(int fftsamplingsize, int FFTOverlapSamples,
+ int[] glitchesData, int samplingRate,
+ boolean glitchingIntervalTooLong, int numberOfGlitches) {
+ 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;
+ log("newSamplesInMs: " + Double.toString(newSamplesInMs));
+
+ // the time span of all samples for a single FFT in ms
+ double allSamplesInMs = ((double) fftsamplingsize / samplingRate) *
+ Constant.MILLIS_PER_SECOND;
+ log("allSamplesInMs: " + Double.toString(allSamplesInMs));
+
+ StringBuilder listOfGlitches = new StringBuilder();
+ listOfGlitches.append("Total Glitching Interval too long: " +
+ glitchingIntervalTooLong + "\n");
+ listOfGlitches.append("Estimated number of glitches: " + numberOfGlitches + "\n");
+ listOfGlitches.append("List of glitching intervals: \n");
+
+ for (int i = 0; i < glitchesData.length; i++) {
+ int timeInMs; // starting time of glitches
+ //append the time of glitches to "listOfGlitches"
+ timeInMs = (int) (glitchesData[i] * newSamplesInMs); // round down
+ listOfGlitches.append(timeInMs + "~" + (timeInMs + (int) allSamplesInMs) + "ms\n");
+ }
+
+ return listOfGlitches.toString();
+ }
+
+ /** Generate String of Glitch Times in ms return separated. */
+ public static String getGlitchStringForFile(int fftSamplingSize, int FFTOverlapSamples,
+ int[] glitchesData, int samplingRate) {
+ 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;
+
+ StringBuilder listOfGlitches = new StringBuilder();
+
+ for (int i = 0; i < glitchesData.length; i++) {
+ int timeInMs; // starting time of glitches
+ //append the time of glitches to "listOfGlitches"
+ timeInMs = (int) (glitchesData[i] * newSamplesInMs); // round down
+ listOfGlitches.append(timeInMs + "\n");
+ }
+
+ return listOfGlitches.toString();
+ }
+
+ 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 4c99b39..2a2d1fc 100644
--- a/LoopbackApp/app/src/main/java/org/drrickorang/loopback/HistogramView.java
+++ b/LoopbackApp/app/src/main/java/org/drrickorang/loopback/HistogramView.java
@@ -84,10 +84,13 @@ public class HistogramView extends View {
mLinePaint.setStrokeWidth(mLineWidth);
}
-
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
+ fillCanvas(canvas, this.getRight(), this.getBottom());
+ }
+
+ public void fillCanvas(Canvas canvas, int right, int bottom){
canvas.drawColor(Color.GRAY);
if (mData == null || mData.length == 0) {
@@ -194,11 +197,6 @@ public class HistogramView extends View {
mDisplayTimeStampData = mTimeStampData;
}
-
- // coordinate starts at (0, 0), up to (right, bottom)
- int right = this.getRight();
- int bottom = this.getBottom();
-
// calculate the max frequency among all latencies
int maxBufferPeriodFreq = 0;
for (int i = 1; i < range; i++) {
diff --git a/LoopbackApp/app/src/main/java/org/drrickorang/loopback/LoadThread.java b/LoopbackApp/app/src/main/java/org/drrickorang/loopback/LoadThread.java
index 00b13ba..9c98c2e 100644
--- a/LoopbackApp/app/src/main/java/org/drrickorang/loopback/LoadThread.java
+++ b/LoopbackApp/app/src/main/java/org/drrickorang/loopback/LoadThread.java
@@ -28,6 +28,9 @@ public class LoadThread extends Thread {
private volatile boolean mIsRunning;
+ public LoadThread(String threadName) {
+ super(threadName);
+ }
public void run() {
log("Entering load thread");
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 fc1e001..5eb6d7b 100644
--- a/LoopbackApp/app/src/main/java/org/drrickorang/loopback/LoopbackActivity.java
+++ b/LoopbackApp/app/src/main/java/org/drrickorang/loopback/LoopbackActivity.java
@@ -23,6 +23,7 @@ import java.util.Arrays;
import android.Manifest;
import android.app.Activity;
+import android.app.DialogFragment;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
@@ -30,7 +31,10 @@ import android.content.ServiceConnection;
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;
@@ -44,8 +48,14 @@ import android.support.v4.content.ContextCompat;
import android.text.format.DateFormat;
import android.util.Log;
import android.view.Gravity;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
import android.widget.LinearLayout;
+import android.widget.PopupWindow;
import android.widget.SeekBar;
import android.widget.Toast;
import android.widget.TextView;
@@ -57,7 +67,8 @@ import android.widget.TextView;
* has two parts of result.
*/
-public class LoopbackActivity extends Activity {
+public class LoopbackActivity extends Activity
+ implements SaveFilesDialogFragment.NoticeDialogListener {
private static final String TAG = "LoopbackActivity";
private static final int SAVE_TO_WAVE_REQUEST = 42;
@@ -65,20 +76,30 @@ public class LoopbackActivity extends Activity {
private static final int SAVE_TO_TXT_REQUEST = 44;
private static final int SAVE_RECORDER_BUFFER_PERIOD_TO_TXT_REQUEST = 45;
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 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 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;
+ private static final int HISTOGRAM_EXPORT_WIDTH = 2000;
+ private static final int HISTOGRAM_EXPORT_HEIGHT = 2000;
LoopbackAudioThread mAudioThread = null;
NativeAudioThread mNativeAudioThread = null;
private WavePlotView mWavePlotView;
private String mCurrentTime = "IncorrectTime"; // The time the plot is acquired
- private String mWaveFilePath; // path of the wave file
+ private static final String FILE_SAVE_PATH = "file://mnt/sdcard/";
private SeekBar mBarMasterLevel; // drag the volume
private TextView mTextInfo;
private TextView mTextViewCurrentLevel;
- private TextView mTextViewEstimatedLatency;
+ private TextView mTextViewResultSummary;
private Toast mToast;
private int mTestType;
@@ -94,6 +115,7 @@ public class LoopbackActivity extends Activity {
private int mNativePlayerMaxBufferPeriod;
private static final String INTENT_SAMPLING_FREQUENCY = "SF";
+ private static final String INTENT_CHANNEL_INDEX = "CI";
private static final String INTENT_FILENAME = "FileName";
private static final String INTENT_RECORDER_BUFFER = "RecorderBuffer";
private static final String INTENT_PLAYER_BUFFER = "PlayerBuffer";
@@ -102,22 +124,16 @@ public class LoopbackActivity extends Activity {
private static final String INTENT_AUDIO_LEVEL = "AudioLevel";
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";
// for running the test using adb command
private boolean mIntentRunning = false; // if it is running triggered by intent with parameters
private String mIntentFileName;
- private int mIntentSamplingRate = 0;
- private int mIntentPlayerBuffer = 0;
- private int mIntentRecorderBuffer = 0;
- private int mIntentMicSource = -1;
- private int mIntentAudioThread = -1;
- private int mIntentAudioLevel = -1;
- private int mIntentTestType = -1;
- private int mIntentBufferTestDuration = 0; // in second
-
- // Note: these four values should only be assigned in restartAudioSystem()
+
+ // Note: these values should only be assigned in restartAudioSystem()
private int mAudioThreadType = Constant.UNKNOWN;
private int mSamplingRate;
+ private int mChannelIndex;
private int mPlayerBufferSizeInBytes;
private int mRecorderBufferSizeInBytes;
@@ -126,10 +142,10 @@ public class LoopbackActivity extends Activity {
private boolean mGlitchingIntervalTooLong;
private int mFFTSamplingSize;
private int mFFTOverlapSamples;
- private int mBufferTestDuration; //in second
+ private long mBufferTestStartTime;
+ private int mBufferTestElapsedSeconds;
// threads that load CPUs
- private static final int mNumberOfLoadThreads = 4;
private LoadThread[] mLoadThreads;
// for getting the Service
@@ -176,7 +192,6 @@ public class LoopbackActivity extends Activity {
refreshState();
mCurrentTime = (String) DateFormat.format("MMddkkmmss",
System.currentTimeMillis());
- mBufferTestDuration = mAudioThread.getDurationInSeconds();
switch (msg.what) {
case LoopbackAudioThread.LOOPBACK_AUDIO_THREAD_MESSAGE_LATENCY_REC_STOP:
@@ -201,6 +216,7 @@ public class LoopbackActivity extends Activity {
resetResults();
refreshState();
refreshPlots();
+ mBufferTestStartTime = System.currentTimeMillis();
break;
case LoopbackAudioThread.LOOPBACK_AUDIO_THREAD_MESSAGE_BUFFER_REC_ERROR:
log("got message java buffer test rec can't start!!");
@@ -222,6 +238,9 @@ public class LoopbackActivity extends Activity {
refreshState();
mCurrentTime = (String) DateFormat.format("MMddkkmmss",
System.currentTimeMillis());
+ mBufferTestElapsedSeconds =
+ (int) ((System.currentTimeMillis() - mBufferTestStartTime)
+ / Constant.MILLIS_PER_SECOND);
switch (msg.what) {
case LoopbackAudioThread.LOOPBACK_AUDIO_THREAD_MESSAGE_BUFFER_REC_STOP:
showToast("Java Buffer Test Stopped");
@@ -252,6 +271,7 @@ public class LoopbackActivity extends Activity {
resetResults();
refreshState();
refreshPlots();
+ mBufferTestStartTime = System.currentTimeMillis();
break;
case NativeAudioThread.LOOPBACK_NATIVE_AUDIO_THREAD_MESSAGE_LATENCY_REC_ERROR:
log("got message native latency test rec can't start!!");
@@ -267,18 +287,17 @@ public class LoopbackActivity extends Activity {
mIntentRunning = false;
refreshSoundLevelBar();
break;
- case NativeAudioThread.LOOPBACK_NATIVE_AUDIO_THREAD_MESSAGE_REC_STOP:
- case NativeAudioThread.
- LOOPBACK_NATIVE_AUDIO_THREAD_MESSAGE_BUFFER_REC_COMPLETE:
+ case NativeAudioThread.LOOPBACK_NATIVE_AUDIO_THREAD_MESSAGE_BUFFER_REC_STOP:
+ case NativeAudioThread.LOOPBACK_NATIVE_AUDIO_THREAD_MESSAGE_LATENCY_REC_STOP:
+ case NativeAudioThread.LOOPBACK_NATIVE_AUDIO_THREAD_MESSAGE_BUFFER_REC_COMPLETE:
case NativeAudioThread.LOOPBACK_NATIVE_AUDIO_THREAD_MESSAGE_LATENCY_REC_COMPLETE:
- case NativeAudioThread.
- LOOPBACK_NATIVE_AUDIO_THREAD_MESSAGE_REC_COMPLETE_ERRORS:
- if (mNativeAudioThread != null) {
+ case NativeAudioThread.LOOPBACK_NATIVE_AUDIO_THREAD_MESSAGE_BUFFER_REC_COMPLETE_ERRORS:
+ case NativeAudioThread.LOOPBACK_NATIVE_AUDIO_THREAD_MESSAGE_LATENCY_REC_COMPLETE_ERRORS:
+ if (mNativeAudioThread != null) {
mGlitchesData = mNativeAudioThread.getNativeAllGlitches();
mGlitchingIntervalTooLong = mNativeAudioThread.getGlitchingIntervalTooLong();
mFFTSamplingSize = mNativeAudioThread.getNativeFFTSamplingSize();
mFFTOverlapSamples = mNativeAudioThread.getNativeFFTOverlapSamples();
- mBufferTestDuration = mNativeAudioThread.getDurationInSeconds();
mWaveData = mNativeAudioThread.getWaveData();
mNativeRecorderBufferPeriodArray = mNativeAudioThread.getRecorderBufferPeriod();
mNativeRecorderMaxBufferPeriod = mNativeAudioThread.
@@ -296,12 +315,19 @@ public class LoopbackActivity extends Activity {
refreshState();
mCurrentTime = (String) DateFormat.format("MMddkkmmss",
System.currentTimeMillis());
+ mBufferTestElapsedSeconds =
+ (int) ((System.currentTimeMillis() - mBufferTestStartTime)
+ / Constant.MILLIS_PER_SECOND);
switch (msg.what) {
- case NativeAudioThread.
- LOOPBACK_NATIVE_AUDIO_THREAD_MESSAGE_REC_COMPLETE_ERRORS:
- showToast("Native Test Completed with Destroying Errors");
+ case NativeAudioThread.
+ LOOPBACK_NATIVE_AUDIO_THREAD_MESSAGE_BUFFER_REC_COMPLETE_ERRORS:
+ case NativeAudioThread.
+ LOOPBACK_NATIVE_AUDIO_THREAD_MESSAGE_LATENCY_REC_COMPLETE_ERRORS:
+ showToast("Native Test Completed with Fatal Errors");
break;
- case NativeAudioThread.LOOPBACK_NATIVE_AUDIO_THREAD_MESSAGE_REC_STOP:
+ case NativeAudioThread.LOOPBACK_NATIVE_AUDIO_THREAD_MESSAGE_BUFFER_REC_STOP:
+ case NativeAudioThread.
+ LOOPBACK_NATIVE_AUDIO_THREAD_MESSAGE_LATENCY_REC_STOP:
showToast("Native Test Stopped");
break;
default:
@@ -324,6 +350,45 @@ public class LoopbackActivity extends Activity {
log("Got message:" + msg.what);
break;
}
+
+ // Control UI elements visibility specific to latency or buffer/glitch test
+ switch (msg.what) {
+ // Latency test started
+ case LoopbackAudioThread.LOOPBACK_AUDIO_THREAD_MESSAGE_LATENCY_REC_STARTED:
+ case NativeAudioThread.LOOPBACK_NATIVE_AUDIO_THREAD_MESSAGE_LATENCY_REC_STARTED:
+ setTransportButtonsState(LATENCY_TEST_STARTED);
+ break;
+
+ // Latency test ended
+ case LoopbackAudioThread.LOOPBACK_AUDIO_THREAD_MESSAGE_LATENCY_REC_COMPLETE:
+ case LoopbackAudioThread.LOOPBACK_AUDIO_THREAD_MESSAGE_LATENCY_REC_ERROR:
+ case LoopbackAudioThread.LOOPBACK_AUDIO_THREAD_MESSAGE_LATENCY_REC_STOP:
+ case NativeAudioThread.LOOPBACK_NATIVE_AUDIO_THREAD_MESSAGE_LATENCY_REC_ERROR:
+ case NativeAudioThread.LOOPBACK_NATIVE_AUDIO_THREAD_MESSAGE_LATENCY_REC_COMPLETE:
+ case NativeAudioThread.LOOPBACK_NATIVE_AUDIO_THREAD_MESSAGE_LATENCY_REC_STOP:
+ case NativeAudioThread.
+ LOOPBACK_NATIVE_AUDIO_THREAD_MESSAGE_LATENCY_REC_COMPLETE_ERRORS:
+ setTransportButtonsState(LATENCY_TEST_ENDED);
+ break;
+
+ // Buffer test started
+ case LoopbackAudioThread.LOOPBACK_AUDIO_THREAD_MESSAGE_BUFFER_REC_STARTED:
+ case NativeAudioThread.LOOPBACK_NATIVE_AUDIO_THREAD_MESSAGE_BUFFER_REC_STARTED:
+ setTransportButtonsState(BUFFER_TEST_STARTED);
+ break;
+
+ // Buffer test ended
+ case LoopbackAudioThread.LOOPBACK_AUDIO_THREAD_MESSAGE_BUFFER_REC_COMPLETE:
+ case LoopbackAudioThread.LOOPBACK_AUDIO_THREAD_MESSAGE_BUFFER_REC_ERROR:
+ case LoopbackAudioThread.LOOPBACK_AUDIO_THREAD_MESSAGE_BUFFER_REC_STOP:
+ case NativeAudioThread.LOOPBACK_NATIVE_AUDIO_THREAD_MESSAGE_BUFFER_REC_ERROR:
+ case NativeAudioThread.LOOPBACK_NATIVE_AUDIO_THREAD_MESSAGE_BUFFER_REC_COMPLETE:
+ case NativeAudioThread.LOOPBACK_NATIVE_AUDIO_THREAD_MESSAGE_BUFFER_REC_STOP:
+ case NativeAudioThread.
+ LOOPBACK_NATIVE_AUDIO_THREAD_MESSAGE_BUFFER_REC_COMPLETE_ERRORS:
+ setTransportButtonsState(BUFFER_TEST_ENDED);
+ break;
+ }
}
};
@@ -366,7 +431,7 @@ public class LoopbackActivity extends Activity {
mTextViewCurrentLevel = (TextView) findViewById(R.id.textViewCurrentLevel);
mTextViewCurrentLevel.setTextSize(15);
- mTextViewEstimatedLatency = (TextView) findViewById(R.id.textViewEstimatedLatency);
+ mTextViewResultSummary = (TextView) findViewById(R.id.resultSummary);
refreshState();
applyIntent(getIntent());
@@ -418,22 +483,25 @@ public class LoopbackActivity extends Activity {
// adb shell am start -n org.drrickorang.loopback/.LoopbackActivity
// --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 TestType 223 --ei BufferTestDuration 60 --ei NumLoadThreads 4
// Note: for native mode, player and recorder buffer sizes are the same, and can only be
// set through player buffer size
- if (b.containsKey(INTENT_TEST_TYPE)) {
- mIntentTestType = b.getInt(INTENT_TEST_TYPE);
- mIntentRunning = true;
- }
+
if (b.containsKey(INTENT_BUFFER_TEST_DURATION)) {
- mIntentBufferTestDuration = b.getInt(INTENT_BUFFER_TEST_DURATION);
+ getApp().setBufferTestDuration(b.getInt(INTENT_BUFFER_TEST_DURATION));
mIntentRunning = true;
}
if (b.containsKey(INTENT_SAMPLING_FREQUENCY)) {
- mIntentSamplingRate = b.getInt(INTENT_SAMPLING_FREQUENCY);
+ getApp().setSamplingRate(b.getInt(INTENT_SAMPLING_FREQUENCY));
+ mIntentRunning = true;
+ }
+
+ if (b.containsKey(INTENT_CHANNEL_INDEX)) {
+ getApp().setChannelIndex(b.getInt(INTENT_CHANNEL_INDEX));
+ mChannelIndex = b.getInt(INTENT_CHANNEL_INDEX);
mIntentRunning = true;
}
@@ -443,110 +511,61 @@ public class LoopbackActivity extends Activity {
}
if (b.containsKey(INTENT_RECORDER_BUFFER)) {
- mIntentRecorderBuffer = b.getInt(INTENT_RECORDER_BUFFER);
+ getApp().setRecorderBufferSizeInBytes(
+ b.getInt(INTENT_RECORDER_BUFFER) * Constant.BYTES_PER_FRAME);
mIntentRunning = true;
}
if (b.containsKey(INTENT_PLAYER_BUFFER)) {
- mIntentPlayerBuffer = b.getInt(INTENT_PLAYER_BUFFER);
+ getApp().setPlayerBufferSizeInBytes(
+ b.getInt(INTENT_PLAYER_BUFFER) * Constant.BYTES_PER_FRAME);
mIntentRunning = true;
}
if (b.containsKey(INTENT_AUDIO_THREAD)) {
- mIntentAudioThread = b.getInt(INTENT_AUDIO_THREAD);
+ getApp().setAudioThreadType(b.getInt(INTENT_AUDIO_THREAD));
mIntentRunning = true;
}
if (b.containsKey(INTENT_MIC_SOURCE)) {
- mIntentMicSource = b.getInt(INTENT_MIC_SOURCE);
+ getApp().setMicSource(b.getInt(INTENT_MIC_SOURCE));
mIntentRunning = true;
}
if (b.containsKey(INTENT_AUDIO_LEVEL)) {
- mIntentAudioLevel = b.getInt(INTENT_AUDIO_LEVEL);
+ int audioLevel = b.getInt(INTENT_AUDIO_LEVEL);
+ if (audioLevel >= 0) {
+ AudioManager am = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
+ am.setStreamVolume(AudioManager.STREAM_MUSIC,
+ audioLevel, 0);
+ }
mIntentRunning = true;
}
- log("Intent " + INTENT_TEST_TYPE + ": " + mIntentTestType);
- log("Intent " + INTENT_BUFFER_TEST_DURATION + ": " + mIntentBufferTestDuration);
- log("Intent " + INTENT_SAMPLING_FREQUENCY + ": " + mIntentSamplingRate);
- log("Intent " + INTENT_FILENAME + ": " + mIntentFileName);
- log("Intent " + INTENT_RECORDER_BUFFER + ": " + mIntentRecorderBuffer);
- log("Intent " + INTENT_PLAYER_BUFFER + ": " + mIntentPlayerBuffer);
- log("Intent " + INTENT_AUDIO_THREAD + ":" + mIntentAudioThread);
- log("Intent " + INTENT_MIC_SOURCE + ": " + mIntentMicSource);
- log("Intent " + INTENT_AUDIO_LEVEL + ": " + mIntentAudioLevel);
-
- if (!mIntentRunning) {
- log("No info to actually run intent.");
- }
-
- runIntentTest();
- } else {
- log("warning: can't run this intent, system busy");
- showToast("System Busy. Stop sending intents!");
- }
- }
-
-
- /**
- * In the case where the test is started through adb command, this method will change the
- * settings if any parameter is specified.
- */
- private void runIntentTest() {
- // mIntentRunning == true if test is started through adb command.
- if (mIntentRunning) {
- if (mIntentBufferTestDuration > 0) {
- getApp().setBufferTestDuration(mIntentBufferTestDuration);
- }
-
- if (mIntentAudioLevel >= 0) {
- AudioManager am = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
- am.setStreamVolume(AudioManager.STREAM_MUSIC,
- mIntentAudioLevel, 0);
- }
-
- if (mIntentSamplingRate != 0) {
- getApp().setSamplingRate(mIntentSamplingRate);
- }
-
- if (mIntentMicSource >= 0) {
- getApp().setMicSource(mIntentMicSource);
- }
-
- if (mIntentAudioThread >= 0) {
- getApp().setAudioThreadType(mIntentAudioThread);
- getApp().computeDefaults();
- }
-
- int bytesPerFrame = Constant.BYTES_PER_FRAME;
-
- if (mIntentRecorderBuffer > 0) {
- getApp().setRecorderBufferSizeInBytes(mIntentRecorderBuffer * bytesPerFrame);
- }
-
- if (mIntentPlayerBuffer > 0) {
- getApp().setPlayerBufferSizeInBytes(mIntentPlayerBuffer * bytesPerFrame);
+ if (b.containsKey(INTENT_NUMBER_LOAD_THREADS)) {
+ getApp().setNumberOfLoadThreads(b.getInt(INTENT_NUMBER_LOAD_THREADS));
+ mIntentRunning = true;
}
- refreshState();
+ if (mIntentRunning || b.containsKey(INTENT_TEST_TYPE)) {
+ // run tests with provided or default parameters
+ refreshState();
- if (mIntentTestType >= 0) {
- switch (mIntentTestType) {
- case Constant.LOOPBACK_PLUG_AUDIO_THREAD_TEST_TYPE_LATENCY:
- startLatencyTest();
- break;
- case Constant.LOOPBACK_PLUG_AUDIO_THREAD_TEST_TYPE_BUFFER_PERIOD:
+ // if no test is specified then Latency Test will be run
+ if (b.containsKey(INTENT_TEST_TYPE)
+ && b.getInt(INTENT_TEST_TYPE) ==
+ Constant.LOOPBACK_PLUG_AUDIO_THREAD_TEST_TYPE_BUFFER_PERIOD) {
startBufferTest();
- break;
- default:
- assert(false);
+ } else {
+ startLatencyTest();
}
- } else {
- // if test type is not specified in command, just run latency test
- startLatencyTest();
}
+ } else {
+ if (mIntentRunning && b != null) {
+ log("Test already in progress");
+ showToast("Test already in progress");
+ }
}
}
@@ -598,6 +617,47 @@ public class LoopbackActivity extends Activity {
super.onPause();
}
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu){
+ MenuInflater inflater = getMenuInflater();
+ inflater.inflate(R.menu.tool_bar_menu, menu);
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ // Respond to user selecting action bar buttons
+ switch (item.getItemId()) {
+ case R.id.action_help:
+ if (!isBusy()) {
+ // Launch about Activity
+ Intent aboutIntent = new Intent(this, AboutActivity.class);
+ startActivity(aboutIntent);
+ } else {
+ showToast("Test in progress... please wait");
+ }
+
+ return true;
+
+ case R.id.action_settings:
+ if (!isBusy()) {
+ // Hide test result controls as results will be null on returning from settings
+ findViewById(R.id.glitchReportPanel).setVisibility(View.INVISIBLE);
+ findViewById(R.id.zoomAndSaveControlPanel).setVisibility(View.INVISIBLE);
+ findViewById(R.id.resultSummary).setVisibility(View.INVISIBLE);
+
+ // Launch settings activity
+ Intent mySettingsIntent = new Intent(this, SettingsActivity.class);
+ startActivityForResult(mySettingsIntent, SETTINGS_ACTIVITY_REQUEST_CODE);
+ } else {
+ showToast("Test in progress... please wait");
+ }
+ return true;
+ }
+
+ return super.onOptionsItemSelected(item);
+ }
+
/** Check if the app is busy (running test). */
public boolean isBusy() {
@@ -623,6 +683,7 @@ public class LoopbackActivity extends Activity {
mAudioThreadType = getApp().getAudioThreadType();
mSamplingRate = getApp().getSamplingRate();
+ mChannelIndex = getApp().getChannelIndex();
mPlayerBufferSizeInBytes = getApp().getPlayerBufferSizeInBytes();
mRecorderBufferSizeInBytes = getApp().getRecorderBufferSizeInBytes();
int micSource = getApp().getMicSource();
@@ -637,10 +698,12 @@ public class LoopbackActivity extends Activity {
switch (mAudioThreadType) {
case Constant.AUDIO_THREAD_TYPE_JAVA:
micSourceMapped = getApp().mapMicSource(Constant.AUDIO_THREAD_TYPE_JAVA, micSource);
+
mAudioThread = new LoopbackAudioThread(mSamplingRate, mPlayerBufferSizeInBytes,
mRecorderBufferSizeInBytes, micSourceMapped, mRecorderBufferPeriod,
mPlayerBufferPeriod, mTestType, bufferTestDurationInSeconds,
- bufferTestWavePlotDurationInSeconds, getApplicationContext());
+ bufferTestWavePlotDurationInSeconds, getApplicationContext(),
+ mChannelIndex);
mAudioThread.setMessageHandler(mMessageHandler);
mAudioThread.mSessionId = sessionId;
mAudioThread.start();
@@ -667,11 +730,15 @@ public class LoopbackActivity extends Activity {
/** Start all LoadThread. */
private void startLoadThreads() {
- mLoadThreads = new LoadThread[mNumberOfLoadThreads];
- for (int i = 0; i < mLoadThreads.length; i++) {
- mLoadThreads[i] = new LoadThread();
- mLoadThreads[i].start();
+ if (getApp().getNumberOfLoadThreads() > 0) {
+
+ mLoadThreads = new LoadThread[getApp().getNumberOfLoadThreads()];
+
+ for (int i = 0; i < mLoadThreads.length; i++) {
+ mLoadThreads[i] = new LoadThread("Loopback_LoadThread_" + i);
+ mLoadThreads[i].start();
+ }
}
}
@@ -702,12 +769,59 @@ public class LoopbackActivity extends Activity {
}
+ private void setTransportButtonsState(int state){
+ Button latencyStart = (Button) findViewById(R.id.buttonStartLatencyTest);
+ Button bufferStart = (Button) findViewById(R.id.buttonStartBufferTest);
+
+ switch (state) {
+ case LATENCY_TEST_STARTED:
+ findViewById(R.id.zoomAndSaveControlPanel).setVisibility(View.INVISIBLE);
+ findViewById(R.id.resultSummary).setVisibility(View.INVISIBLE);
+ findViewById(R.id.glitchReportPanel).setVisibility(View.INVISIBLE);
+ latencyStart.setCompoundDrawablesWithIntrinsicBounds(
+ R.drawable.ic_stop, 0, 0, 0);
+ bufferStart.setEnabled(false);
+ break;
+
+ case LATENCY_TEST_ENDED:
+ findViewById(R.id.zoomAndSaveControlPanel).setVisibility(View.VISIBLE);
+ findViewById(R.id.resultSummary).setVisibility(View.VISIBLE);
+ latencyStart.setCompoundDrawablesWithIntrinsicBounds(
+ R.drawable.ic_play_arrow, 0, 0, 0);
+ bufferStart.setEnabled(true);
+ break;
+
+ case BUFFER_TEST_STARTED:
+ findViewById(R.id.zoomAndSaveControlPanel).setVisibility(View.INVISIBLE);
+ findViewById(R.id.resultSummary).setVisibility(View.INVISIBLE);
+ findViewById(R.id.glitchReportPanel).setVisibility(View.INVISIBLE);
+ bufferStart.setCompoundDrawablesWithIntrinsicBounds(
+ R.drawable.ic_stop, 0, 0, 0);
+ latencyStart.setEnabled(false);
+ break;
+
+ case BUFFER_TEST_ENDED:
+ findViewById(R.id.zoomAndSaveControlPanel).setVisibility(View.VISIBLE);
+ findViewById(R.id.resultSummary).setVisibility(View.VISIBLE);
+ bufferStart.setCompoundDrawablesWithIntrinsicBounds(
+ R.drawable.ic_play_arrow, 0, 0, 0);
+ latencyStart.setEnabled(true);
+ findViewById(R.id.glitchReportPanel).setVisibility(View.VISIBLE);
+ break;
+ }
+ }
+
+
/** Start the latency test. */
- public void onButtonLatencyTest(View view) {
+ public void onButtonLatencyTest(View view) throws InterruptedException{
+ if (isBusy()) {
+ stopTests();
+ return;
+ }
// Ensure we have RECORD_AUDIO permissions
// On Android M (API 23) we must request dangerous permissions each time we use them
- if (hasRecordAudioPermission()){
+ if (hasRecordAudioPermission()) {
startLatencyTest();
} else {
requestRecordAudioPermission();
@@ -746,9 +860,13 @@ public class LoopbackActivity extends Activity {
/** Start the Buffer (Glitch Detection) Test. */
- public void onButtonBufferTest(View view) {
+ public void onButtonBufferTest(View view) throws InterruptedException {
+ if (isBusy()) {
+ stopTests();
+ return;
+ }
- if (hasRecordAudioPermission()){
+ if (hasRecordAudioPermission()) {
startBufferTest();
} else {
requestRecordAudioPermission();
@@ -798,7 +916,7 @@ public class LoopbackActivity extends Activity {
/** Stop the ongoing test. */
- public void onButtonStopTest(View view) throws InterruptedException{
+ public void stopTests() throws InterruptedException{
if (mAudioThread != null) {
mAudioThread.requestStopTest();
}
@@ -808,6 +926,17 @@ public class LoopbackActivity extends Activity {
}
}
+ /***
+ * Show dialog to choose to save files with filename dialog or not
+ */
+ public void onButtonSave(View view) {
+ if (!isBusy()) {
+ DialogFragment newFragment = new SaveFilesDialogFragment();
+ newFragment.show(getFragmentManager(), "saveFiles");
+ } else {
+ showToast("Test in progress... please wait");
+ }
+ }
/**
* Save five files: one .png file for a screenshot on the main activity, one .wav file for
@@ -815,80 +944,62 @@ public class LoopbackActivity extends Activity {
* .txt file for storing recorder buffer period data, and one .txt file for storing player
* buffer period data.
*/
- public void onButtonSave(View view) {
- if (!isBusy()) {
- //create filename with date
- String date = mCurrentTime; // the time the plot is acquired
- //String micSource = getApp().getMicSourceString(getApp().getMicSource());
- String fileName = "loopback_" + date;
-
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
- Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
- intent.addCategory(Intent.CATEGORY_OPENABLE);
- intent.setType("text/plain");
- intent.putExtra(Intent.EXTRA_TITLE, fileName + ".txt"); //suggested filename
- startActivityForResult(intent, SAVE_TO_TXT_REQUEST);
-
- Intent intent2 = new Intent(Intent.ACTION_CREATE_DOCUMENT);
- intent2.addCategory(Intent.CATEGORY_OPENABLE);
- intent2.setType("image/png");
- intent2.putExtra(Intent.EXTRA_TITLE, fileName + ".png"); //suggested filename
- startActivityForResult(intent2, SAVE_TO_PNG_REQUEST);
-
- //sometimes ".wav" will be added automatically, sometimes not
- Intent intent3 = new Intent(Intent.ACTION_CREATE_DOCUMENT);
- intent3.addCategory(Intent.CATEGORY_OPENABLE);
- intent3.setType("audio/wav");
- intent3.putExtra(Intent.EXTRA_TITLE, fileName + ".wav"); //suggested filename
- startActivityForResult(intent3, SAVE_TO_WAVE_REQUEST);
-
- fileName = "loopback_" + date + "_recorderBufferPeriod";
- Intent intent4 = new Intent(Intent.ACTION_CREATE_DOCUMENT);
- intent4.addCategory(Intent.CATEGORY_OPENABLE);
- intent4.setType("text/plain");
- intent4.putExtra(Intent.EXTRA_TITLE, fileName + ".txt");
- startActivityForResult(intent4, SAVE_RECORDER_BUFFER_PERIOD_TO_TXT_REQUEST);
-
- fileName = "loopback_" + date + "_playerBufferPeriod";
- Intent intent5 = new Intent(Intent.ACTION_CREATE_DOCUMENT);
- intent5.addCategory(Intent.CATEGORY_OPENABLE);
- intent5.setType("text/plain");
- intent5.putExtra(Intent.EXTRA_TITLE, fileName + ".txt");
- startActivityForResult(intent5, SAVE_PLAYER_BUFFER_PERIOD_TO_TXT_REQUEST);
- } else {
- saveAllTo(fileName);
+ private void SaveFilesWithDialog() {
+
+ String fileName = "loopback_" + mCurrentTime;
+
+ //Launch filename choosing activities if available, otherwise save without prompting
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
+ launchFileNameChoosingActivity("text/plain", fileName, ".txt", SAVE_TO_TXT_REQUEST);
+ launchFileNameChoosingActivity("image/png", fileName, ".png", SAVE_TO_PNG_REQUEST);
+ launchFileNameChoosingActivity("audio/wav", fileName, ".wav", SAVE_TO_WAVE_REQUEST);
+ launchFileNameChoosingActivity("text/plain", fileName, "_recorderBufferPeriod.txt",
+ SAVE_RECORDER_BUFFER_PERIOD_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("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);
}
} else {
- showToast("Test in progress... please wait");
+ saveAllTo(fileName);
}
}
+ /**
+ * Launches an activity for choosing the filename of the file to be saved
+ */
+ public void launchFileNameChoosingActivity(String type, String fileName, String suffix,
+ int RequestCode) {
+ Intent FilenameIntent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
+ FilenameIntent.addCategory(Intent.CATEGORY_OPENABLE);
+ FilenameIntent.setType(type);
+ FilenameIntent.putExtra(Intent.EXTRA_TITLE, fileName + suffix);
+ startActivityForResult(FilenameIntent, RequestCode);
+ }
+
/** See the documentation on onButtonSave() */
public void saveAllTo(String fileName) {
+
+ if (!hasWriteFilePermission()) {
+ requestWriteFilePermission();
+ return;
+ }
+
showToast("Saving files to: " + fileName + ".(wav,png,txt)");
//save to a given uri... local file?
- Uri uri = Uri.parse("file://mnt/sdcard/" + fileName + ".wav");
- String temp = getPath(uri);
-
- // for some devices it cannot find the path
- if (temp != null) {
- File file = new File(temp);
- mWaveFilePath = file.getAbsolutePath();
- } else {
- mWaveFilePath = "";
- }
+ saveToWaveFile(Uri.parse(FILE_SAVE_PATH + fileName + ".wav"));
- saveToWaveFile(uri);
- Uri uri2 = Uri.parse("file://mnt/sdcard/" + fileName + ".png");
- saveScreenShot(uri2);
+ saveScreenShot(Uri.parse(FILE_SAVE_PATH + fileName + ".png"));
- Uri uri3 = Uri.parse("file://mnt/sdcard/" + fileName + ".txt");
- saveReport(uri3);
+ saveReport(Uri.parse(FILE_SAVE_PATH + fileName + ".txt"));
- String fileName2 = fileName + "_recorderBufferPeriod";
- Uri uri4 = Uri.parse("file://mnt/sdcard/" + fileName2 + ".txt");
int[] bufferPeriodArray = null;
int maxBufferPeriod = Constant.UNKNOWN;
switch (mAudioThreadType) {
@@ -901,10 +1012,11 @@ public class LoopbackActivity extends Activity {
maxBufferPeriod = mNativeRecorderMaxBufferPeriod;
break;
}
- saveBufferPeriod(uri4, bufferPeriodArray, maxBufferPeriod);
+ saveBufferPeriod(Uri.parse(FILE_SAVE_PATH + fileName + "_recorderBufferPeriod.txt"),
+ bufferPeriodArray, maxBufferPeriod);
+ saveHistogram(Uri.parse(FILE_SAVE_PATH + fileName + "_recorderBufferPeriod.png"),
+ bufferPeriodArray, maxBufferPeriod);
- String fileName3 = fileName + "_playerBufferPeriod";
- Uri uri5 = Uri.parse("file://mnt/sdcard/" + fileName3 + ".txt");
bufferPeriodArray = null;
maxBufferPeriod = Constant.UNKNOWN;
switch (mAudioThreadType) {
@@ -917,46 +1029,44 @@ public class LoopbackActivity extends Activity {
maxBufferPeriod = mNativePlayerMaxBufferPeriod;
break;
}
- saveBufferPeriod(uri5, bufferPeriodArray, maxBufferPeriod);
+ saveBufferPeriod(Uri.parse(FILE_SAVE_PATH + fileName + "_playerBufferPeriod.txt")
+ , bufferPeriodArray, maxBufferPeriod);
+ saveHistogram(Uri.parse(FILE_SAVE_PATH + fileName + "_playerBufferPeriod.png"),
+ bufferPeriodArray, maxBufferPeriod);
+
+ if (mGlitchesData != null) {
+ saveGlitchOccurrences(Uri.parse(FILE_SAVE_PATH + fileName + "_glitchMillis.txt"),
+ mGlitchesData);
+ }
+
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent resultData) {
log("ActivityResult request: " + requestCode + " result:" + resultCode);
+
if (resultCode == Activity.RESULT_OK) {
- Uri uri;
switch (requestCode) {
case SAVE_TO_WAVE_REQUEST:
log("got SAVE TO WAV intent back!");
if (resultData != null) {
- uri = resultData.getData();
- String temp = getPath(uri);
- if (temp != null) {
- File file = new File(temp);
- mWaveFilePath = file.getAbsolutePath();
- } else {
- mWaveFilePath = "";
- }
- saveToWaveFile(uri);
+ saveToWaveFile(resultData.getData());
}
break;
case SAVE_TO_PNG_REQUEST:
log("got SAVE TO PNG intent back!");
if (resultData != null) {
- uri = resultData.getData();
- saveScreenShot(uri);
+ saveScreenShot(resultData.getData());
}
break;
case SAVE_TO_TXT_REQUEST:
if (resultData != null) {
- uri = resultData.getData();
- saveReport(uri);
+ saveReport(resultData.getData());
}
break;
case SAVE_RECORDER_BUFFER_PERIOD_TO_TXT_REQUEST:
if (resultData != null) {
- uri = resultData.getData();
int[] bufferPeriodArray = null;
int maxBufferPeriod = Constant.UNKNOWN;
switch (mAudioThreadType) {
@@ -969,12 +1079,11 @@ public class LoopbackActivity extends Activity {
maxBufferPeriod = mNativeRecorderMaxBufferPeriod;
break;
}
- saveBufferPeriod(uri, bufferPeriodArray, maxBufferPeriod);
+ saveBufferPeriod(resultData.getData(), bufferPeriodArray, maxBufferPeriod);
}
break;
case SAVE_PLAYER_BUFFER_PERIOD_TO_TXT_REQUEST:
if (resultData != null) {
- uri = resultData.getData();
int[] bufferPeriodArray = null;
int maxBufferPeriod = Constant.UNKNOWN;
switch (mAudioThreadType) {
@@ -987,7 +1096,46 @@ public class LoopbackActivity extends Activity {
maxBufferPeriod = mNativePlayerMaxBufferPeriod;
break;
}
- saveBufferPeriod(uri, bufferPeriodArray, maxBufferPeriod);
+ saveBufferPeriod(resultData.getData(), bufferPeriodArray, maxBufferPeriod);
+ }
+ break;
+ case SAVE_RECORDER_BUFFER_PERIOD_TO_PNG_REQUEST:
+ if (resultData != null) {
+ int[] bufferPeriodArray = null;
+ int maxBufferPeriod = Constant.UNKNOWN;
+ switch (mAudioThreadType) {
+ case Constant.AUDIO_THREAD_TYPE_JAVA:
+ bufferPeriodArray = mRecorderBufferPeriod.getBufferPeriodArray();
+ maxBufferPeriod = mRecorderBufferPeriod.getMaxBufferPeriod();
+ break;
+ case Constant.AUDIO_THREAD_TYPE_NATIVE:
+ bufferPeriodArray = mNativeRecorderBufferPeriodArray;
+ maxBufferPeriod = mNativeRecorderMaxBufferPeriod;
+ break;
+ }
+ saveHistogram(resultData.getData(), bufferPeriodArray, maxBufferPeriod);
+ }
+ break;
+ case SAVE_PLAYER_BUFFER_PERIOD_TO_PNG_REQUEST:
+ if (resultData != null) {
+ int[] bufferPeriodArray = null;
+ int maxBufferPeriod = Constant.UNKNOWN;
+ switch (mAudioThreadType) {
+ case Constant.AUDIO_THREAD_TYPE_JAVA:
+ bufferPeriodArray = mPlayerBufferPeriod.getBufferPeriodArray();
+ maxBufferPeriod = mPlayerBufferPeriod.getMaxBufferPeriod();
+ break;
+ case Constant.AUDIO_THREAD_TYPE_NATIVE:
+ bufferPeriodArray = mNativePlayerBufferPeriodArray;
+ maxBufferPeriod = mNativePlayerMaxBufferPeriod;
+ break;
+ }
+ saveHistogram(resultData.getData(), bufferPeriodArray, maxBufferPeriod);
+ }
+ break;
+ case SAVE_GLITCH_OCCURRENCES_TO_TEXT_REQUEST:
+ if (resultData != null) {
+ saveGlitchOccurrences(resultData.getData(), mGlitchesData);
}
break;
case SETTINGS_ACTIVITY_REQUEST_CODE:
@@ -1018,8 +1166,7 @@ public class LoopbackActivity extends Activity {
/** Reset all results gathered from previous round of test (if any). */
private void resetResults() {
- mCorrelation.mEstimatedLatencyMs = 0;
- mCorrelation.mEstimatedLatencyConfidence = 0;
+ mCorrelation.invalidate();
mRecorderBufferPeriod.resetRecord();
mPlayerBufferPeriod.resetRecord();
mNativeRecorderBufferPeriodArray = null;
@@ -1071,27 +1218,6 @@ public class LoopbackActivity extends Activity {
}
-/*
- public void onButtonZoomInFull(View view) {
-
- double minZoom = mWavePlotView.getMinZoomOut();
-
- mWavePlotView.setZoom(minZoom);
- mWavePlotView.refreshGraph();
- }
-*/
-
-
- /** Go to AboutActivity. */
- public void onButtonAbout(View view) {
- if (!isBusy()) {
- Intent aboutIntent = new Intent(this, AboutActivity.class);
- startActivity(aboutIntent);
- } else
- showToast("Test in progress... please wait");
- }
-
-
/** Go to RecorderBufferPeriodActivity */
public void onButtonRecorderBufferPeriod(View view) {
if (!isBusy()) {
@@ -1158,40 +1284,61 @@ public class LoopbackActivity extends Activity {
}
- /** Go to GlitchesActivity. */
+ /** Display pop up window of recorded glitches */
public void onButtonGlitches(View view) {
if (!isBusy()) {
if (mGlitchesData != null) {
- int numberOfGlitches = estimateNumberOfGlitches(mGlitchesData);
- Intent GlitchesIntent = new Intent(this, GlitchesActivity.class);
- GlitchesIntent.putExtra("glitchesArray", mGlitchesData);
- GlitchesIntent.putExtra("FFTSamplingSize", mFFTSamplingSize);
- GlitchesIntent.putExtra("FFTOverlapSamples", mFFTOverlapSamples);
- GlitchesIntent.putExtra("samplingRate", mSamplingRate);
- GlitchesIntent.putExtra("glitchingIntervalTooLong", mGlitchingIntervalTooLong);
- GlitchesIntent.putExtra("numberOfGlitches", numberOfGlitches);
- startActivity(GlitchesIntent);
+ // 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 GlitchText =
+ (TextView) popUp.getContentView().findViewById(R.id.ReportInfo);
+ GlitchText.setText(GlitchesStringBuilder.getGlitchString(mFFTSamplingSize,
+ mFFTOverlapSamples, mGlitchesData, mSamplingRate,
+ mGlitchingIntervalTooLong, estimateNumberOfGlitches(mGlitchesData)));
+
+ // display pop up window, dismissible with back button
+ popUp.showAtLocation(findViewById(R.id.linearLayoutMain), Gravity.TOP, 0, 0);
} else {
showToast("Please run the buffer test to get data");
}
- } else
+ } else {
showToast("Test in progress... please wait");
+ }
}
-
- /** Go to SettingsActivity. */
- public void onButtonSettings(View view) {
+ /** Display pop up window of recorded metrics and system information */
+ public void onButtonReport(View view) {
if (!isBusy()) {
- Intent mySettingsIntent = new Intent(this, SettingsActivity.class);
- //send settings
- startActivityForResult(mySettingsIntent, SETTINGS_ACTIVITY_REQUEST_CODE);
+ if ((mTestType == Constant.LOOPBACK_PLUG_AUDIO_THREAD_TEST_TYPE_BUFFER_PERIOD
+ && mGlitchesData != null)
+ || (mTestType == Constant.LOOPBACK_PLUG_AUDIO_THREAD_TEST_TYPE_LATENCY
+ && mCorrelation.isValid())) {
+ // 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 reportText =
+ (TextView) popUp.getContentView().findViewById(R.id.ReportInfo);
+ reportText.setText(getReport().toString());
+
+ // display pop up window, dismissible with back button
+ 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);
@@ -1215,10 +1362,12 @@ public class LoopbackActivity extends Activity {
// get info
int samplingRate = getApp().getSamplingRate();
+ int channelIndex = getApp().getChannelIndex();
int playerBuffer = getApp().getPlayerBufferSizeInBytes() / Constant.BYTES_PER_FRAME;
int recorderBuffer = getApp().getRecorderBufferSizeInBytes() / Constant.BYTES_PER_FRAME;
StringBuilder s = new StringBuilder(200);
s.append("SR: " + samplingRate + " Hz");
+ s.append(" ChannelIndex: " + channelIndex);
int audioThreadType = getApp().getAudioThreadType();
switch (audioThreadType) {
case Constant.AUDIO_THREAD_TYPE_JAVA:
@@ -1250,16 +1399,20 @@ public class LoopbackActivity extends Activity {
int bufferTestWavePlotDuration = getApp().getBufferTestWavePlotDuration();
s.append(" Buffer Test Wave Plot Duration: last " + bufferTestWavePlotDuration + "s");
+ // Show short summary of results, round trip latency or number of glitches
mTextInfo.setText(s.toString());
-
- String estimatedLatency = "----";
-
- if (mCorrelation.mEstimatedLatencyMs > 0.0001) {
- estimatedLatency = String.format("%.2f ms", mCorrelation.mEstimatedLatencyMs);
+ if (mTestType == Constant.LOOPBACK_PLUG_AUDIO_THREAD_TEST_TYPE_LATENCY &&
+ mCorrelation.isValid()) {
+ mTextViewResultSummary.setText(String.format("Latency: %s Confidence: %.2f",
+ String.format("%.2f ms", mCorrelation.mEstimatedLatencyMs),
+ mCorrelation.mEstimatedLatencyConfidence));
+ } else if (mTestType == Constant.LOOPBACK_PLUG_AUDIO_THREAD_TEST_TYPE_BUFFER_PERIOD &&
+ mGlitchesData != null) {
+ mTextViewResultSummary.setText(getResources().getString(R.string.numGlitches) + " " +
+ estimateNumberOfGlitches(mGlitchesData));
+ } else {
+ mTextViewResultSummary.setText("");
}
-
- mTextViewEstimatedLatency.setText(String.format("Latency: %s Confidence: %.2f",
- estimatedLatency, mCorrelation.mEstimatedLatencyConfidence));
}
@@ -1295,7 +1448,15 @@ public class LoopbackActivity extends Activity {
mSamplingRate);
boolean status = audioFileOutput.writeData(mWaveData);
if (status) {
- showToast("Finished exporting wave File " + mWaveFilePath);
+ String wavFileAbsolutePath = getPath(uri);
+ // for some devices getPath fails
+ if (wavFileAbsolutePath != null) {
+ File file = new File(wavFileAbsolutePath);
+ wavFileAbsolutePath = file.getAbsolutePath();
+ } else {
+ wavFileAbsolutePath = "";
+ }
+ showToast("Finished exporting wave File " + wavFileAbsolutePath);
} else {
showToast("Something failed saving wave file");
}
@@ -1341,6 +1502,47 @@ public class LoopbackActivity extends Activity {
}
}
+ /** Save a screenshot of the main activity. */
+ private void saveHistogram(Uri uri, int[] bufferPeriodArray, int maxBufferPeriod) {
+ ParcelFileDescriptor parcelFileDescriptor = null;
+ FileOutputStream outputStream;
+ try {
+ parcelFileDescriptor = getApplicationContext().getContentResolver().
+ openFileDescriptor(uri, "w");
+
+ FileDescriptor fileDescriptor = parcelFileDescriptor.getFileDescriptor();
+ outputStream = new FileOutputStream(fileDescriptor);
+
+ 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);
+ parcelFileDescriptor.close();
+ } catch (Exception e) {
+ log("Failed to open png file " + e);
+ } finally {
+ try {
+ if (parcelFileDescriptor != null) {
+ parcelFileDescriptor.close();
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ log("Error closing ParcelFile Descriptor");
+ }
+ }
+ }
+
/**
* Save a .txt file of the given buffer period's data.
@@ -1369,7 +1571,6 @@ public class LoopbackActivity extends Activity {
}
outputStream.write(sb.toString().getBytes());
- parcelFileDescriptor.close();
} catch (Exception e) {
log("Failed to open text file " + e);
@@ -1397,44 +1598,61 @@ public class LoopbackActivity extends Activity {
FileDescriptor fileDescriptor = parcelFileDescriptor.getFileDescriptor();
outputStream = new FileOutputStream(fileDescriptor);
-
log("Done creating output stream");
- String endline = "\n";
- final int stringLength = 300;
- StringBuilder sb = new StringBuilder(stringLength);
- sb.append("DateTime = " + mCurrentTime + endline);
- sb.append(INTENT_SAMPLING_FREQUENCY + " = " + getApp().getSamplingRate() + endline);
- sb.append(INTENT_RECORDER_BUFFER + " = " + getApp().getRecorderBufferSizeInBytes() /
- Constant.BYTES_PER_FRAME + endline);
- sb.append(INTENT_PLAYER_BUFFER + " = "
- + getApp().getPlayerBufferSizeInBytes() / Constant.BYTES_PER_FRAME + endline);
- sb.append(INTENT_AUDIO_THREAD + " = " + getApp().getAudioThreadType() + endline);
- int micSource = getApp().getMicSource();
-
-
- String audioType = "unknown";
- switch (getApp().getAudioThreadType()) {
+ outputStream.write(getReport().toString().getBytes());
+ parcelFileDescriptor.close();
+ } catch (Exception e) {
+ log("Failed to open text file " + e);
+ } finally {
+ try {
+ if (parcelFileDescriptor != null) {
+ parcelFileDescriptor.close();
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ log("Error closing ParcelFile Descriptor");
+ }
+ }
+
+ }
+
+ private StringBuilder getReport(){
+ String endline = "\n";
+ final int stringLength = 300;
+ StringBuilder sb = new StringBuilder(stringLength);
+ sb.append("DateTime = " + mCurrentTime + endline);
+ sb.append(INTENT_SAMPLING_FREQUENCY + " = " + getApp().getSamplingRate() + endline);
+ sb.append(INTENT_RECORDER_BUFFER + " = " + getApp().getRecorderBufferSizeInBytes() /
+ Constant.BYTES_PER_FRAME + endline);
+ sb.append(INTENT_PLAYER_BUFFER + " = "
+ + getApp().getPlayerBufferSizeInBytes() / Constant.BYTES_PER_FRAME + endline);
+ sb.append(INTENT_AUDIO_THREAD + " = " + getApp().getAudioThreadType() + endline);
+ int micSource = getApp().getMicSource();
+
+
+ String audioType = "unknown";
+ switch (getApp().getAudioThreadType()) {
case Constant.AUDIO_THREAD_TYPE_JAVA:
audioType = "JAVA";
break;
case Constant.AUDIO_THREAD_TYPE_NATIVE:
audioType = "NATIVE";
break;
- }
- sb.append(INTENT_AUDIO_THREAD + "_String = " + audioType + endline);
+ }
+ sb.append(INTENT_AUDIO_THREAD + "_String = " + audioType + endline);
- sb.append(INTENT_MIC_SOURCE + " = " + micSource + endline);
- sb.append(INTENT_MIC_SOURCE + "_String = " + getApp().getMicSourceString(micSource)
- + endline);
- AudioManager am = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
+ sb.append(INTENT_MIC_SOURCE + " = " + micSource + endline);
+ sb.append(INTENT_MIC_SOURCE + "_String = " + getApp().getMicSourceString(micSource)
+ + endline);
+ AudioManager am = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
- int currentVolume = am.getStreamVolume(AudioManager.STREAM_MUSIC);
- sb.append(INTENT_AUDIO_LEVEL + " = " + currentVolume + endline);
+ int currentVolume = am.getStreamVolume(AudioManager.STREAM_MUSIC);
+ sb.append(INTENT_AUDIO_LEVEL + " = " + currentVolume + endline);
- switch (mTestType) {
+ switch (mTestType) {
case Constant.LOOPBACK_PLUG_AUDIO_THREAD_TEST_TYPE_LATENCY:
- if (mCorrelation.mEstimatedLatencyMs > 0.0001) {
+ if (mCorrelation.isValid()) {
sb.append(String.format("LatencyMs = %.2f", mCorrelation.mEstimatedLatencyMs)
+ endline);
} else {
@@ -1445,7 +1663,7 @@ public class LoopbackActivity extends Activity {
mCorrelation.mEstimatedLatencyConfidence) + endline);
break;
case Constant.LOOPBACK_PLUG_AUDIO_THREAD_TEST_TYPE_BUFFER_PERIOD:
- sb.append("Buffer Test Duration (s) = " + mBufferTestDuration + endline);
+ sb.append("Buffer Test Duration (s) = " + mBufferTestElapsedSeconds + endline);
// report expected recorder buffer period
int expectedRecorderBufferPeriod = mRecorderBufferSizeInBytes /
@@ -1470,16 +1688,22 @@ public class LoopbackActivity extends Activity {
if (recorderBufferData != null) {
// this is the range of data that actually has values
int usefulDataRange = Math.min(recorderBufferDataMax + 1,
- recorderBufferData.length);
+ recorderBufferData.length);
int[] usefulBufferData = Arrays.copyOfRange(recorderBufferData, 0,
- usefulDataRange);
+ usefulDataRange);
PerformanceMeasurement measurement = new PerformanceMeasurement(
recorderBufferSize, mSamplingRate, usefulBufferData);
- boolean isBufferSizesMismatch = measurement.determineIsBufferSizesMatch();
+ float recorderPercentAtExpected =
+ measurement.percentBufferPeriodsAtExpected();
double benchmark = measurement.computeWeightedBenchmark();
int outliers = measurement.countOutliers();
- sb.append("Recorder Buffer Sizes Mismatch = " + isBufferSizesMismatch +
- endline);
+ sb.append("Recorder Buffer Periods At Expected = " +
+ String.format("%.5f%%", recorderPercentAtExpected * 100) + endline);
+
+ // output thousandths of a percent not at expected buffer period
+ sb.append("kth% Late Recorder Buffer Callbacks = "
+ + String.format("%.5f", (1 - recorderPercentAtExpected) * 100000)
+ + endline);
sb.append("Recorder Benchmark = " + benchmark + endline);
sb.append("Recorder Number of Outliers = " + outliers + endline);
} else {
@@ -1503,15 +1727,21 @@ public class LoopbackActivity extends Activity {
if (playerBufferData != null) {
// this is the range of data that actually has values
int usefulDataRange = Math.min(playerBufferDataMax + 1,
- playerBufferData.length);
+ playerBufferData.length);
int[] usefulBufferData = Arrays.copyOfRange(playerBufferData, 0,
- usefulDataRange);
+ usefulDataRange);
PerformanceMeasurement measurement = new PerformanceMeasurement(
playerBufferSize, mSamplingRate, usefulBufferData);
- boolean isBufferSizesMismatch = measurement.determineIsBufferSizesMatch();
+ float playerPercentAtExpected = measurement.percentBufferPeriodsAtExpected();
double benchmark = measurement.computeWeightedBenchmark();
int outliers = measurement.countOutliers();
- sb.append("Player Buffer Sizes Mismatch = " + isBufferSizesMismatch + endline);
+ sb.append("Player Buffer Periods At Expected = "
+ + String.format("%.5f%%", playerPercentAtExpected * 100) + endline);
+
+ // output thousandths of a percent not at expected buffer period
+ sb.append("kth% Late Player Buffer Callbacks = "
+ + String.format("%.5f", (1 - playerPercentAtExpected) * 100000)
+ + endline);
sb.append("Player Benchmark = " + benchmark + endline);
sb.append("Player Number of Outliers = " + outliers + endline);
@@ -1521,30 +1751,54 @@ public class LoopbackActivity extends Activity {
// report expected player buffer period
int expectedPlayerBufferPeriod = mPlayerBufferSizeInBytes / Constant.BYTES_PER_FRAME
- * Constant.MILLIS_PER_SECOND / mSamplingRate;
+ * 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);
+ endline);
- // report estimated number of glitches
+ // report glitches per hour
int numberOfGlitches = estimateNumberOfGlitches(mGlitchesData);
- sb.append("Estimated Number of Glitches = " + numberOfGlitches + endline);
+ float testDurationInHours = mBufferTestElapsedSeconds
+ / (float) Constant.SECONDS_PER_HOUR;
+
+ // Report Glitches Per Hour if sufficient data available, ie at least half an hour
+ if (testDurationInHours >= .5) {
+ int glitchesPerHour = (int) Math.ceil(numberOfGlitches/testDurationInHours);
+ sb.append("Glitches Per Hour = " + glitchesPerHour + endline);
+ }
+ 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);
- }
+ mGlitchingIntervalTooLong + endline);
+ }
- String info = getApp().getSystemInfo();
- sb.append("SystemInfo = " + info + endline);
+ String info = getApp().getSystemInfo();
+ sb.append("SystemInfo = " + info + endline);
- outputStream.write(sb.toString().getBytes());
- parcelFileDescriptor.close();
+ return sb;
+ }
+
+ /** Save a .txt file of of glitch occurrences in ms from beginning of test. */
+ private void saveGlitchOccurrences(Uri uri, int[] glitchesData) {
+ ParcelFileDescriptor parcelFileDescriptor = null;
+ FileOutputStream outputStream;
+ try {
+ parcelFileDescriptor = getApplicationContext().getContentResolver().
+ openFileDescriptor(uri, "w");
+
+ FileDescriptor fileDescriptor = parcelFileDescriptor.getFileDescriptor();
+ outputStream = new FileOutputStream(fileDescriptor);
+
+ log("Done creating output stream");
+
+ outputStream.write(GlitchesStringBuilder.getGlitchStringForFile(mFFTSamplingSize,
+ mFFTOverlapSamples, glitchesData, mSamplingRate).getBytes());
} catch (Exception e) {
log("Failed to open text file " + e);
} finally {
@@ -1557,10 +1811,8 @@ public class LoopbackActivity extends Activity {
log("Error closing ParcelFile Descriptor");
}
}
-
}
-
/**
* Estimate the number of glitches. This version of estimation will count two consecutive
* glitching intervals as one glitch. This is because two time intervals are partly overlapped.
@@ -1655,6 +1907,54 @@ public class LoopbackActivity extends Activity {
// 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);
+ }
+ }
+ }
+
+ /**
+ * Check whether we have the WRITE_EXTERNAL_STORAGE permission
+ *
+ * @return true if we do
+ */
+ private boolean hasWriteFilePermission() {
+ boolean hasPermission = (ContextCompat.checkSelfPermission(this,
+ Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED);
+
+ log("Has WRITE_EXTERNAL_STORAGE? " + hasPermission);
+ return hasPermission;
+ }
+
+ /**
+ * Requests the WRITE_EXTERNAL_STORAGE permission from the user
+ */
+ private void requestWriteFilePermission() {
+
+ String requiredPermission = Manifest.permission.WRITE_EXTERNAL_STORAGE;
+
+ // request the permission.
+ ActivityCompat.requestPermissions(this,
+ new String[]{requiredPermission},
+ PERMISSIONS_REQUEST_WRITE_EXTERNAL_STORAGE);
}
+ /**
+ * Receive results from save files DialogAlert and either save all files directly
+ * or use filename dialog
+ */
+ @Override
+ public void onSaveDialogSelect(DialogFragment dialog, boolean saveWithoutDialog) {
+ if (saveWithoutDialog) {
+ saveAllTo("loopback_" + mCurrentTime);
+ } 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 c57659f..02b91df 100644
--- a/LoopbackApp/app/src/main/java/org/drrickorang/loopback/LoopbackApplication.java
+++ b/LoopbackApp/app/src/main/java/org/drrickorang/loopback/LoopbackApplication.java
@@ -39,13 +39,14 @@ public class LoopbackApplication extends Application {
// here defines all the initial setting values, some get modified in ComputeDefaults()
private int mSamplingRate = 48000;
+ private int mChannelIndex = -1;
private int mPlayerBufferSizeInBytes = 0; // for both native and java
private int mRecorderBuffSizeInBytes = 0; // for both native and java
private int mAudioThreadType = Constant.AUDIO_THREAD_TYPE_JAVA; //0:Java, 1:Native (JNI)
private int mMicSource = 3; //maps to MediaRecorder.AudioSource.VOICE_RECOGNITION;
private int mBufferTestDurationInSeconds = 5;
private int mBufferTestWavePlotDurationInSeconds = 7;
-
+ private int mNumberOfLoadThreads = 4;
public void setDefaults() {
if (isSafeToUseSles()) {
@@ -57,16 +58,17 @@ public class LoopbackApplication extends Application {
computeDefaults();
}
-
int getSamplingRate() {
return mSamplingRate;
}
-
void setSamplingRate(int samplingRate) {
- mSamplingRate = samplingRate;
+ mSamplingRate = clamp(samplingRate, Constant.SAMPLING_RATE_MIN, Constant.SAMPLING_RATE_MAX);
}
+ int getChannelIndex() { return mChannelIndex; }
+
+ void setChannelIndex(int channelIndex) { mChannelIndex = channelIndex; }
int getAudioThreadType() {
return mAudioThreadType;
@@ -74,7 +76,12 @@ public class LoopbackApplication extends Application {
void setAudioThreadType(int audioThreadType) {
- mAudioThreadType = audioThreadType;
+ if (isSafeToUseSles() && audioThreadType != Constant.AUDIO_THREAD_TYPE_JAVA) {
+ //safe to use native and Java thread not selected
+ mAudioThreadType = Constant.AUDIO_THREAD_TYPE_NATIVE;
+ } else {
+ mAudioThreadType = Constant.AUDIO_THREAD_TYPE_JAVA;
+ }
}
@@ -155,7 +162,8 @@ public class LoopbackApplication extends Application {
void setPlayerBufferSizeInBytes(int playerBufferSizeInBytes) {
- mPlayerBufferSizeInBytes = playerBufferSizeInBytes;
+ mPlayerBufferSizeInBytes = clamp(playerBufferSizeInBytes, Constant.PLAYER_BUFFER_FRAMES_MIN,
+ Constant.PLAYER_BUFFER_FRAMES_MAX);
}
@@ -165,7 +173,8 @@ public class LoopbackApplication extends Application {
void setRecorderBufferSizeInBytes(int recorderBufferSizeInBytes) {
- mRecorderBuffSizeInBytes = recorderBufferSizeInBytes;
+ mRecorderBuffSizeInBytes = clamp(recorderBufferSizeInBytes,
+ Constant.RECORDER_BUFFER_FRAMES_MIN, Constant.RECORDER_BUFFER_FRAMES_MAX);
}
@@ -175,7 +184,9 @@ public class LoopbackApplication extends Application {
void setBufferTestDuration(int bufferTestDurationInSeconds) {
- mBufferTestDurationInSeconds = bufferTestDurationInSeconds;
+ mBufferTestDurationInSeconds = clamp(bufferTestDurationInSeconds,
+ Constant.BUFFER_TEST_DURATION_SECONDS_MIN,
+ Constant.BUFFER_TEST_DURATION_SECONDS_MAX);
}
@@ -185,7 +196,31 @@ public class LoopbackApplication extends Application {
void setBufferTestWavePlotDuration(int bufferTestWavePlotDurationInSeconds) {
- mBufferTestWavePlotDurationInSeconds = bufferTestWavePlotDurationInSeconds;
+ mBufferTestWavePlotDurationInSeconds = clamp(bufferTestWavePlotDurationInSeconds,
+ Constant.BUFFER_TEST_WAVE_PLOT_DURATION_SECONDS_MIN,
+ Constant.BUFFER_TEST_WAVE_PLOT_DURATION_SECONDS_MAX);
+ }
+
+ int getNumberOfLoadThreads() {
+ return mNumberOfLoadThreads;
+ }
+
+ void setNumberOfLoadThreads(int numberOfLoadThreads) {
+ mNumberOfLoadThreads = clamp(numberOfLoadThreads, Constant.MIN_NUM_LOAD_THREADS,
+ Constant.MAX_NUM_LOAD_THREADS);
+ }
+
+ /**
+ * 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
+ */
+ private int clamp(int value, int min, int max) {
+
+ if (max < min) throw new UnsupportedOperationException("min must be <= max");
+
+ if (value < min) return min;
+ else if (value > max) return max;
+ else return value;
}
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 6637bb6..1ef64c4 100644
--- a/LoopbackApp/app/src/main/java/org/drrickorang/loopback/LoopbackAudioThread.java
+++ b/LoopbackApp/app/src/main/java/org/drrickorang/loopback/LoopbackAudioThread.java
@@ -17,10 +17,12 @@
package org.drrickorang.loopback;
import android.content.Context;
+import android.media.AudioDeviceInfo;
import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.AudioTrack;
import android.media.MediaRecorder;
+import android.os.Build;
import android.util.Log;
import android.os.Handler;
import android.os.Message;
@@ -53,14 +55,15 @@ public class LoopbackAudioThread extends Thread {
private Thread mRecorderThread;
private RecorderRunnable mRecorderRunnable;
- private int mSamplingRate;
- private int mChannelConfigIn = AudioFormat.CHANNEL_IN_MONO;
- private int mAudioFormat = AudioFormat.ENCODING_PCM_16BIT;
+ private final int mSamplingRate;
+ private final int mChannelIndex;
+ private final int mChannelConfigIn = AudioFormat.CHANNEL_IN_MONO;
+ private final int mAudioFormat = AudioFormat.ENCODING_PCM_16BIT;
private int mMinPlayerBufferSizeInBytes = 0;
private int mMinRecorderBuffSizeInBytes = 0;
private int mMinPlayerBufferSizeSamples = 0;
- private int mMicSource;
- private int mChannelConfigOut = AudioFormat.CHANNEL_OUT_MONO;
+ private final int mMicSource;
+ private final int mChannelConfigOut = AudioFormat.CHANNEL_OUT_MONO;
private boolean mIsPlaying = false;
private boolean mIsRequestStop = false;
private Handler mMessageHandler;
@@ -81,7 +84,8 @@ public class LoopbackAudioThread extends Thread {
int micSource, BufferPeriod recorderBufferPeriod,
BufferPeriod playerBufferPeriod, int testType,
int bufferTestDurationInSeconds,
- int bufferTestWavePlotDurationInSeconds, Context context) {
+ int bufferTestWavePlotDurationInSeconds, Context context,
+ int channelIndex) {
mSamplingRate = samplingRate;
mMinPlayerBufferSizeInBytes = playerBufferInBytes;
mMinRecorderBuffSizeInBytes = recorderBufferInBytes;
@@ -92,6 +96,9 @@ public class LoopbackAudioThread extends Thread {
mBufferTestDurationInSeconds = bufferTestDurationInSeconds;
mBufferTestWavePlotDurationInSeconds = bufferTestWavePlotDurationInSeconds;
mContext = context;
+ mChannelIndex = channelIndex;
+
+ setName("Loopback_LoopbackAudio");
}
@@ -119,26 +126,41 @@ 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);
+ mBufferTestWavePlotDurationInSeconds, mContext, mChannelIndex);
mRecorderRunnable.setBufferTestDurationInSeconds(mBufferTestDurationInSeconds);
mRecorderThread = new Thread(mRecorderRunnable);
+ mRecorderThread.setName("Loopback_RecorderRunnable");
// both player and recorder run at max priority
mRecorderThread.setPriority(Thread.MAX_PRIORITY);
mRecorderThread.start();
- mAudioTrack = new AudioTrack(AudioManager.STREAM_MUSIC,
- mSamplingRate,
- mChannelConfigOut,
- mAudioFormat,
- mMinPlayerBufferSizeInBytes,
- AudioTrack.MODE_STREAM /* FIXME runtime test for API level 9,
- mSessionId */);
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ mAudioTrack = new AudioTrack.Builder()
+ .setAudioFormat((mChannelIndex < 0 ?
+ new AudioFormat.Builder().setChannelMask(AudioFormat.CHANNEL_OUT_MONO) :
+ new AudioFormat.Builder().setChannelIndexMask(1 << mChannelIndex))
+ .setSampleRate(mSamplingRate)
+ .setEncoding(mAudioFormat)
+ .build())
+ .setBufferSizeInBytes(mMinPlayerBufferSizeInBytes)
+ .setTransferMode(AudioTrack.MODE_STREAM)
+ .build();
+ } else {
+ mAudioTrack = new AudioTrack(AudioManager.STREAM_MUSIC,
+ mSamplingRate,
+ mChannelConfigOut,
+ mAudioFormat,
+ mMinPlayerBufferSizeInBytes,
+ AudioTrack.MODE_STREAM /* FIXME runtime test for API level 9,
+ mSessionId */);
+ }
- if (mRecorderRunnable != null && mAudioTrack != null) {
+ if (mRecorderRunnable != null && mAudioTrack.getState() == AudioTrack.STATE_INITIALIZED) {
mIsPlaying = false;
mIsRunning = true;
@@ -186,6 +208,8 @@ public class LoopbackAudioThread extends Thread {
} else {
log("Loopback Audio Thread couldn't run!");
+ mAudioTrack.release();
+ mAudioTrack = null;
if (mMessageHandler != null) {
Message msg = Message.obtain();
switch (mTestType) {
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 fcec9c2..cf39efe 100644
--- a/LoopbackApp/app/src/main/java/org/drrickorang/loopback/NativeAudioThread.java
+++ b/LoopbackApp/app/src/main/java/org/drrickorang/loopback/NativeAudioThread.java
@@ -35,15 +35,15 @@ public class NativeAudioThread extends Thread {
static final int LOOPBACK_NATIVE_AUDIO_THREAD_MESSAGE_LATENCY_REC_STARTED = 891;
static final int LOOPBACK_NATIVE_AUDIO_THREAD_MESSAGE_LATENCY_REC_ERROR = 892;
static final int LOOPBACK_NATIVE_AUDIO_THREAD_MESSAGE_LATENCY_REC_COMPLETE = 893;
+ static final int LOOPBACK_NATIVE_AUDIO_THREAD_MESSAGE_LATENCY_REC_COMPLETE_ERRORS = 894;
+ static final int LOOPBACK_NATIVE_AUDIO_THREAD_MESSAGE_LATENCY_REC_STOP = 895;
// for buffer test
static final int LOOPBACK_NATIVE_AUDIO_THREAD_MESSAGE_BUFFER_REC_STARTED = 896;
static final int LOOPBACK_NATIVE_AUDIO_THREAD_MESSAGE_BUFFER_REC_ERROR = 897;
static final int LOOPBACK_NATIVE_AUDIO_THREAD_MESSAGE_BUFFER_REC_COMPLETE = 898;
-
- // used by both latency test and buffer test
- static final int LOOPBACK_NATIVE_AUDIO_THREAD_MESSAGE_REC_COMPLETE_ERRORS = 894;
- static final int LOOPBACK_NATIVE_AUDIO_THREAD_MESSAGE_REC_STOP = 900;
+ static final int LOOPBACK_NATIVE_AUDIO_THREAD_MESSAGE_BUFFER_REC_COMPLETE_ERRORS = 899;
+ static final int LOOPBACK_NATIVE_AUDIO_THREAD_MESSAGE_BUFFER_REC_STOP = 900;
public boolean mIsRunning = false;
public int mSessionId;
@@ -89,6 +89,8 @@ public class NativeAudioThread extends Thread {
mTestType = testType;
mBufferTestDurationInSeconds = bufferTestDurationInSeconds;
mBufferTestWavePlotDurationInSeconds = bufferTestWavePlotDurationInSeconds;
+
+ setName("Loopback_NativeAudio");
}
@@ -106,7 +108,8 @@ public class NativeAudioThread extends Thread {
//jni calls
public native long slesInit(int samplingRate, int frameCount, int micSource,
- int testType, double frequency1, ByteBuffer byteBuffer);
+ int testType, double frequency1, ByteBuffer byteBuffer,
+ short[] sincTone);
public native int slesProcessNext(long sles_data, double[] samples, long offset);
public native int slesDestroy(long sles_data);
@@ -140,6 +143,14 @@ public class NativeAudioThread extends Thread {
mMessageHandler.sendMessage(msg);
}
+ //generate sinc tone use for loopback test
+ short loopbackTone[] = new short[mMinPlayerBufferSizeInBytes / Constant.BYTES_PER_FRAME];
+ if (mTestType == Constant.LOOPBACK_PLUG_AUDIO_THREAD_TEST_TYPE_LATENCY) {
+ ToneGeneration sincToneGen = new RampedSineTone(mSamplingRate,
+ Constant.LOOPBACK_FREQUENCY);
+ sincToneGen.generateTone(loopbackTone, loopbackTone.length);
+ }
+
log(String.format("about to init, sampling rate: %d, buffer:%d", mSamplingRate,
mMinPlayerBufferSizeInBytes / Constant.BYTES_PER_FRAME));
@@ -148,7 +159,7 @@ public class NativeAudioThread extends Thread {
long startTimeMs = System.currentTimeMillis();
long sles_data = slesInit(mSamplingRate, mMinPlayerBufferSizeInBytes /
Constant.BYTES_PER_FRAME, mMicSource, mTestType, mFrequency1,
- mPipeByteBuffer.getByteBuffer());
+ mPipeByteBuffer.getByteBuffer(), loopbackTone);
log(String.format("sles_data = 0x%X", sles_data));
if (sles_data == 0) {
@@ -353,9 +364,23 @@ public class NativeAudioThread extends Thread {
if (mMessageHandler != null) {
Message msg = Message.obtain();
if (hasDestroyingErrors) {
- msg.what = LOOPBACK_NATIVE_AUDIO_THREAD_MESSAGE_REC_COMPLETE_ERRORS;
+ switch (mTestType) {
+ case Constant.LOOPBACK_PLUG_AUDIO_THREAD_TEST_TYPE_LATENCY:
+ msg.what = LOOPBACK_NATIVE_AUDIO_THREAD_MESSAGE_LATENCY_REC_COMPLETE_ERRORS;
+ break;
+ case Constant.LOOPBACK_PLUG_AUDIO_THREAD_TEST_TYPE_BUFFER_PERIOD:
+ msg.what = LOOPBACK_NATIVE_AUDIO_THREAD_MESSAGE_BUFFER_REC_COMPLETE_ERRORS;
+ break;
+ }
} else if (mIsRequestStop) {
- msg.what = LOOPBACK_NATIVE_AUDIO_THREAD_MESSAGE_REC_STOP;
+ switch (mTestType) {
+ case Constant.LOOPBACK_PLUG_AUDIO_THREAD_TEST_TYPE_LATENCY:
+ msg.what = LOOPBACK_NATIVE_AUDIO_THREAD_MESSAGE_LATENCY_REC_STOP;
+ break;
+ case Constant.LOOPBACK_PLUG_AUDIO_THREAD_TEST_TYPE_BUFFER_PERIOD:
+ msg.what = LOOPBACK_NATIVE_AUDIO_THREAD_MESSAGE_BUFFER_REC_STOP;
+ break;
+ }
} else {
switch (mTestType) {
case Constant.LOOPBACK_PLUG_AUDIO_THREAD_TEST_TYPE_LATENCY:
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 1670b8b..8acb0bf 100644
--- a/LoopbackApp/app/src/main/java/org/drrickorang/loopback/PerformanceMeasurement.java
+++ b/LoopbackApp/app/src/main/java/org/drrickorang/loopback/PerformanceMeasurement.java
@@ -88,7 +88,8 @@ public class PerformanceMeasurement {
log("percent difference between two means: " + (Math.abs(meanAfterDiscard - mean) / mean));
// determine if there's a buffer sizes mismatch
- boolean isBufferSizesMismatch = determineIsBufferSizesMatch();
+ boolean isBufferSizesMismatch =
+ percentBufferPeriodsAtExpected() > mPercentOccurrenceThreshold;
// compute benchmark and count the number of outliers
double benchmark = computeWeightedBenchmark();
@@ -105,27 +106,21 @@ public class PerformanceMeasurement {
/**
- * Determine whether or not there is a buffer sizes mismatch by summing the counts around
- * mExpectedBufferPeriod. If the percent of this count over the total count is larger than
- * mPercentOccurrenceThreshold, then there is no mismatch. Else, there is mismatch.
- * Note: This method may not work in every case, but should work in most cases.
+ * Determine percent of Buffer Period Callbacks that occurred at the expected time
+ * Note: due to current rounding in buffer sampling callbacks occurring at 1 ms after the
+ * expected buffer period are also counted in the returned percentage
+ * Returns a value between 0 and 1
*/
- public boolean determineIsBufferSizesMatch() {
+ public float percentBufferPeriodsAtExpected() {
int occurrenceNearExpectedBufferPeriod = 0;
// indicate how many beams around mExpectedBufferPeriod do we want to add to the count
int numberOfBeams = 2;
int start = Math.max(0, mExpectedBufferPeriodMs - numberOfBeams);
- int end = Math.min(mBufferData.length, mExpectedBufferPeriodMs + numberOfBeams + 1);
+ int end = Math.min(mBufferData.length, mExpectedBufferPeriodMs + numberOfBeams);
for (int i = start; i < end; i++) {
occurrenceNearExpectedBufferPeriod += mBufferData[i];
}
- double percentOccurrence = ((double) occurrenceNearExpectedBufferPeriod) / mTotalOccurrence;
- log("percent occurrence near center: " + percentOccurrence);
- if (percentOccurrence > mPercentOccurrenceThreshold) {
- return false;
- } else {
- return true;
- }
+ return ((float) occurrenceNearExpectedBufferPeriod) / mTotalOccurrence;
}
diff --git a/LoopbackApp/app/src/main/java/org/drrickorang/loopback/Pipe.java b/LoopbackApp/app/src/main/java/org/drrickorang/loopback/Pipe.java
index fa5991f..8eb1214 100644
--- a/LoopbackApp/app/src/main/java/org/drrickorang/loopback/Pipe.java
+++ b/LoopbackApp/app/src/main/java/org/drrickorang/loopback/Pipe.java
@@ -37,7 +37,7 @@ public abstract class Pipe {
/**
* Read at most "count" number of samples into array "buffer", starting from index "offset".
- * Ff the available samples to read is smaller than count, just read as much as it can and
+ * If the available samples to read is smaller than count, just read as much as it can and
* return the amount of samples read (non-blocking). offset + count must be <= buffer.length.
*/
public abstract int read(short[] buffer, int offset, int count);
diff --git a/LoopbackApp/app/src/main/java/org/drrickorang/loopback/RampedSineTone.java b/LoopbackApp/app/src/main/java/org/drrickorang/loopback/RampedSineTone.java
new file mode 100644
index 0000000..dc0227f
--- /dev/null
+++ b/LoopbackApp/app/src/main/java/org/drrickorang/loopback/RampedSineTone.java
@@ -0,0 +1,70 @@
+/*
+ * 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;
+
+/**
+ * Creates a tone that can be injected (and then looped back) in the Latency test.
+ * The generated tone is a sine wave whose amplitude linearly increases than decreases
+ */
+public class RampedSineTone extends SineWaveTone {
+
+ public RampedSineTone(int samplingRate, double frequency) {
+ super(samplingRate, frequency);
+ mAmplitude = Constant.LOOPBACK_AMPLITUDE;
+ }
+
+ /**
+ * Modifies SineWaveTone by creating an ramp up in amplitude followed by an immediate ramp down
+ */
+ @Override
+ public void generateTone(short[] tone, int size) {
+ super.generateTone(tone, size);
+
+ for (int i = 0; i < size; i++) {
+ double factor; // applied to the amplitude of the sine wave
+
+ //for first half of sample amplitude is increasing hence i < size / 2
+ if (i < size / 2) {
+ factor = (i / (float) size) * 2;
+ } else {
+ factor = ((size - i) / (float) size) * 2;
+ }
+ tone[i] *= factor;
+ }
+ }
+
+ /**
+ * Modifies SineWaveTone by creating an ramp up in amplitude followed by an immediate ramp down
+ */
+ @Override
+ public void generateTone(double[] tone, int size) {
+ super.generateTone(tone, size);
+
+ for (int i = 0; i < size; i++) {
+ double factor; // applied to the amplitude of the sine wave
+
+ //for first half of sample amplitude is increasing hence i < size / 2
+ if (i < size / 2) {
+ factor = Constant.LOOPBACK_AMPLITUDE * i / size;
+ } else {
+ factor = Constant.LOOPBACK_AMPLITUDE * (size - i) / size;
+ }
+ tone[i] *= factor;
+ }
+ }
+
+}
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 d28719d..d2c238b 100644
--- a/LoopbackApp/app/src/main/java/org/drrickorang/loopback/RecorderRunnable.java
+++ b/LoopbackApp/app/src/main/java/org/drrickorang/loopback/RecorderRunnable.java
@@ -20,9 +20,9 @@ import android.content.Context;
import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.AudioRecord;
+import android.os.Build;
import android.util.Log;
-
/**
* This thread records incoming sound samples (uses AudioRecord).
*/
@@ -45,6 +45,7 @@ public class RecorderRunnable implements Runnable {
private final int mTestType; // latency test or buffer test
private final int mSelectedRecordSource;
private final int mSamplingRate;
+
private int mChannelConfig = AudioFormat.CHANNEL_IN_MONO;
private int mAudioFormat = AudioFormat.ENCODING_PCM_16BIT;
private int mMinRecorderBuffSizeInBytes = 0;
@@ -57,6 +58,7 @@ public class RecorderRunnable implements Runnable {
// for glitch detection (buffer test)
private BufferPeriod mRecorderBufferPeriodInRecorder;
private final int mBufferTestWavePlotDurationInSeconds;
+ private final int mChannelIndex;
private final double mFrequency1;
private final double mFrequency2; // not actually used
private int[] mAllGlitches; // value = 1 means there's a glitch in that interval
@@ -87,7 +89,7 @@ public class RecorderRunnable implements Runnable {
int recorderBufferInBytes, int micSource, LoopbackAudioThread audioThread,
BufferPeriod recorderBufferPeriod, int testType, double frequency1,
double frequency2, int bufferTestWavePlotDurationInSeconds,
- Context context) {
+ Context context, int channelIndex) {
mLatencyTestPipeShort = latencyPipe;
mSamplingRate = samplingRate;
mChannelConfig = channelConfig;
@@ -101,6 +103,7 @@ public class RecorderRunnable implements Runnable {
mFrequency2 = frequency2;
mBufferTestWavePlotDurationInSeconds = bufferTestWavePlotDurationInSeconds;
mContext = context;
+ mChannelIndex = channelIndex;
}
@@ -125,20 +128,40 @@ public class RecorderRunnable implements Runnable {
mAudioShortArray = new short[mMinRecorderBuffSizeInSamples];
try {
- mRecorder = new AudioRecord(mSelectedRecordSource, mSamplingRate,
- mChannelConfig, mAudioFormat, 2 * mMinRecorderBuffSizeInBytes);
- } catch (IllegalArgumentException e) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ mRecorder = new AudioRecord.Builder()
+ .setAudioFormat((mChannelIndex < 0 ?
+ new AudioFormat.Builder()
+ .setChannelMask(AudioFormat.CHANNEL_IN_MONO) :
+ new AudioFormat
+ .Builder().setChannelIndexMask(1 << mChannelIndex))
+ .setSampleRate(mSamplingRate)
+ .setEncoding(mAudioFormat)
+ .build())
+ .setAudioSource(mSelectedRecordSource)
+ .setBufferSizeInBytes(2 * mMinRecorderBuffSizeInBytes)
+ .build();
+ } else {
+ mRecorder = new AudioRecord(mSelectedRecordSource, mSamplingRate,
+ mChannelConfig, mAudioFormat, 2 * mMinRecorderBuffSizeInBytes);
+ }
+ } catch (IllegalArgumentException | UnsupportedOperationException e) {
e.printStackTrace();
return false;
+ } finally {
+ if (mRecorder == null){
+ return false;
+ } else if (mRecorder.getState() != AudioRecord.STATE_INITIALIZED) {
+ mRecorder.release();
+ mRecorder = null;
+ return false;
+ }
}
- if (mRecorder.getState() != AudioRecord.STATE_INITIALIZED) {
- mRecorder.release();
- mRecorder = null;
- return false;
- }
-
- createAudioTone(300, 1000, true);
+ //generate sinc wave for use in loopback test
+ ToneGeneration sincTone = new RampedSineTone(mSamplingRate, Constant.LOOPBACK_FREQUENCY);
+ mAudioTone = new short[Constant.LOOPBACK_SAMPLE_FRAMES];
+ sincTone.generateTone(mAudioTone, Constant.LOOPBACK_SAMPLE_FRAMES);
return true;
}
@@ -172,16 +195,34 @@ public class RecorderRunnable implements Runnable {
mMaxVolume = mAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC);
try {
- mRecorder = new AudioRecord(mSelectedRecordSource, mSamplingRate,
- mChannelConfig, mAudioFormat, 2 * mMinRecorderBuffSizeInBytes);
- } catch (IllegalArgumentException e) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ mRecorder = new AudioRecord.Builder()
+ .setAudioFormat((mChannelIndex < 0 ?
+ new AudioFormat.Builder()
+ .setChannelMask(AudioFormat.CHANNEL_IN_MONO) :
+ new AudioFormat
+ .Builder().setChannelIndexMask(1 << mChannelIndex))
+ .setSampleRate(mSamplingRate)
+ .setEncoding(mAudioFormat)
+ .build())
+ .setAudioSource(mSelectedRecordSource)
+ .setBufferSizeInBytes(2 * mMinRecorderBuffSizeInBytes)
+ .build();
+ } else {
+ mRecorder = new AudioRecord(mSelectedRecordSource, mSamplingRate,
+ mChannelConfig, mAudioFormat, 2 * mMinRecorderBuffSizeInBytes);
+ }
+ } catch (IllegalArgumentException | UnsupportedOperationException e) {
e.printStackTrace();
return false;
- }
- if (mRecorder.getState() != AudioRecord.STATE_INITIALIZED) {
- mRecorder.release();
- mRecorder = null;
- return false;
+ } finally {
+ if (mRecorder == null){
+ return false;
+ } else if (mRecorder.getState() != AudioRecord.STATE_INITIALIZED) {
+ mRecorder.release();
+ mRecorder = null;
+ return false;
+ }
}
final int targetFFTMs = 20; // we want each FFT to cover 20ms of samples
@@ -477,35 +518,6 @@ public class RecorderRunnable implements Runnable {
}
- /**
- * this function creates the tone that will be injected (and then loopback) in the Latency test.
- * It's a sine wave whose magnitude increases than decreases
- */
- //TODO make this a subclass of ToneGeneration
- private void createAudioTone(int sampleSize, int frequency, boolean taperEnds) {
- mAudioTone = new short[sampleSize];
- double phase = 0;
-
- for (int i = 0; i < sampleSize; i++) {
- double factor = 1.0; // decide the magnitude of the sine wave
- if (taperEnds) {
- if (i < sampleSize / 2) {
- factor = 2.0 * i / sampleSize;
- } else {
- factor = 2.0 * (sampleSize - i) / sampleSize;
- }
- }
-
- short value = (short) (factor * Math.sin(phase) * 10000);
- mAudioTone[i] = value;
- phase += Constant.TWO_PI * frequency / mSamplingRate;
- }
-
- while (phase > Constant.TWO_PI)
- phase -= Constant.TWO_PI;
- }
-
-
public void setBufferTestDurationInSeconds(int bufferTestDurationInSeconds) {
mBufferTestDurationInSeconds = bufferTestDurationInSeconds;
mBufferTestDurationMs = Constant.MILLIS_PER_SECOND * mBufferTestDurationInSeconds;
diff --git a/LoopbackApp/app/src/main/java/org/drrickorang/loopback/SaveFilesDialogFragment.java b/LoopbackApp/app/src/main/java/org/drrickorang/loopback/SaveFilesDialogFragment.java
new file mode 100644
index 0000000..de8e575
--- /dev/null
+++ b/LoopbackApp/app/src/main/java/org/drrickorang/loopback/SaveFilesDialogFragment.java
@@ -0,0 +1,77 @@
+/*
+ * 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.app.Activity;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.DialogFragment;
+import android.content.DialogInterface;
+import android.os.Bundle;
+
+/**
+ * Displays an option for saving all files to file://mnt/sdcard/ or choosing filenames
+ */
+public class SaveFilesDialogFragment extends DialogFragment {
+
+ /* The activity that creates an instance of this dialog fragment must
+ * implement this interface in order to receive event callbacks. */
+ public interface NoticeDialogListener {
+ public void onSaveDialogSelect(DialogFragment dialog, boolean saveWithoutDialog);
+ }
+
+ // Use this instance of the interface to deliver action events
+ NoticeDialogListener mListener;
+
+ // Override the Fragment.onAttach() method to instantiate the NoticeDialogListener
+ @Override
+ public void onAttach(Activity activity) {
+ super.onAttach(activity);
+ // Verify that the host activity implements the callback interface
+ try {
+ // Instantiate the NoticeDialogListener so we can send events to the host
+ mListener = (NoticeDialogListener) activity;
+ } catch (ClassCastException e) {
+ // The activity doesn't implement the interface, throw exception
+ throw new ClassCastException(activity.toString()
+ + " must implement NoticeDialogListener");
+ }
+ }
+
+ @Override
+ 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() {
+ public void onClick(DialogInterface dialog, int id) {
+ dialog.dismiss();
+ mListener.onSaveDialogSelect(SaveFilesDialogFragment.this, true);
+ }
+ })
+ .setNegativeButton(R.string.SaveFileDialogChooseFilenames,
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ dialog.dismiss();
+ mListener.onSaveDialogSelect(SaveFilesDialogFragment.this, false);
+ }
+ });
+ // Create the AlertDialog object and return it
+ return builder.create();
+ }
+}
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 fc26634..0f2e5ab 100644
--- a/LoopbackApp/app/src/main/java/org/drrickorang/loopback/SettingsActivity.java
+++ b/LoopbackApp/app/src/main/java/org/drrickorang/loopback/SettingsActivity.java
@@ -18,7 +18,6 @@ package org.drrickorang.loopback;
import android.app.Activity;
import android.content.Intent;
-import android.content.res.Resources;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
@@ -26,8 +25,6 @@ import android.widget.Spinner;
import android.widget.ArrayAdapter;
import android.widget.AdapterView.OnItemSelectedListener;
import android.widget.AdapterView;
-import android.widget.NumberPicker;
-import android.widget.NumberPicker.OnValueChangeListener;
import android.widget.TextView;
@@ -35,18 +32,20 @@ import android.widget.TextView;
* This activity displays all settings that can be adjusted by the user.
*/
-public class SettingsActivity extends Activity implements OnItemSelectedListener,
- OnValueChangeListener {
+public class SettingsActivity extends Activity implements OnItemSelectedListener {
+
private static final String TAG = "SettingsActivity";
private Spinner mSpinnerMicSource;
private Spinner mSpinnerSamplingRate;
private Spinner mSpinnerAudioThreadType;
- private NumberPicker mNumberPickerPlayerBuffer;
- private NumberPicker mNumberPickerRecorderBuffer;
- private NumberPicker mNumberPickerBufferTestDuration; // in seconds
- private NumberPicker mNumberPickerBufferTestWavePlotDuration; //in seconds
private TextView mTextSettingsInfo;
+ private Spinner mSpinnerChannelIndex;
+ private SettingsPicker mPlayerBufferUI;
+ private SettingsPicker mRecorderBufferUI;
+ private SettingsPicker mBufferTestDurationUI;
+ private SettingsPicker mWavePlotDurationUI;
+ private SettingsPicker mLoadThreadUI;
ArrayAdapter<CharSequence> mAdapterSamplingRate;
@@ -84,8 +83,8 @@ public class SettingsActivity extends Activity implements OnItemSelectedListener
String currentValue = String.valueOf(samplingRate);
int nPosition = mAdapterSamplingRate.getPosition(currentValue);
mSpinnerSamplingRate.setSelection(nPosition, false);
-
mSpinnerSamplingRate.setOnItemSelectedListener(this);
+
//spinner native
int audioThreadType = getApp().getAudioThreadType();
mSpinnerAudioThreadType = (Spinner) findViewById(R.id.spinnerAudioThreadType);
@@ -99,78 +98,100 @@ public class SettingsActivity extends Activity implements OnItemSelectedListener
mSpinnerAudioThreadType.setSelection(audioThreadType, false);
if (!getApp().isSafeToUseSles())
mSpinnerAudioThreadType.setEnabled(false);
-
mSpinnerAudioThreadType.setOnItemSelectedListener(this);
- // buffer test duration in seconds
- int bufferTestDurationMax = 36000;
- int bufferTestDurationMin = 1;
- mNumberPickerBufferTestDuration = (NumberPicker)
- findViewById(R.id.numberpickerBufferTestDuration);
- mNumberPickerBufferTestDuration.setMaxValue(bufferTestDurationMax);
- mNumberPickerBufferTestDuration.setMinValue(bufferTestDurationMin);
- mNumberPickerBufferTestDuration.setWrapSelectorWheel(false);
- mNumberPickerBufferTestDuration.setOnValueChangedListener(this);
- int bufferTestDuration = getApp().getBufferTestDuration();
- mNumberPickerBufferTestDuration.setValue(bufferTestDuration);
-
- // set the string to display bufferTestDurationMax
- Resources res = getResources();
- String string1 = res.getString(R.string.labelBufferTestDuration, bufferTestDurationMax);
- TextView textView = (TextView) findViewById(R.id.textBufferTestDuration);
- textView.setText(string1);
-
- // wave plot duration for buffer test in seconds
- int bufferTestWavePlotDurationMax = 120;
- int bufferTestWavePlotDurationMin = 1;
- mNumberPickerBufferTestWavePlotDuration = (NumberPicker)
- findViewById(R.id.numberPickerBufferTestWavePlotDuration);
- mNumberPickerBufferTestWavePlotDuration.setMaxValue(bufferTestWavePlotDurationMax);
- mNumberPickerBufferTestWavePlotDuration.setMinValue(bufferTestWavePlotDurationMin);
- mNumberPickerBufferTestWavePlotDuration.setWrapSelectorWheel(false);
- mNumberPickerBufferTestWavePlotDuration.setOnValueChangedListener(this);
- int bufferTestWavePlotDuration = getApp().getBufferTestWavePlotDuration();
- mNumberPickerBufferTestWavePlotDuration.setValue(bufferTestWavePlotDuration);
-
- // set the string to display bufferTestWavePlotDurationMax
- string1 = res.getString(R.string.labelBufferTestWavePlotDuration,
- bufferTestWavePlotDurationMax);
- textView = (TextView) findViewById(R.id.textBufferTestWavePlotDuration);
- textView.setText(string1);
-
- //player buffer
- int playerBufferMax = 8000;
- int playerBufferMin = 16;
- mNumberPickerPlayerBuffer = (NumberPicker) findViewById(R.id.numberpickerPlayerBuffer);
- mNumberPickerPlayerBuffer.setMaxValue(playerBufferMax);
- mNumberPickerPlayerBuffer.setMinValue(playerBufferMin);
- mNumberPickerPlayerBuffer.setWrapSelectorWheel(false);
- mNumberPickerPlayerBuffer.setOnValueChangedListener(this);
- int playerBuffer = getApp().getPlayerBufferSizeInBytes()/ Constant.BYTES_PER_FRAME;
- mNumberPickerPlayerBuffer.setValue(playerBuffer);
- log("playerbuffer = " + playerBuffer);
-
- // set the string to display playerBufferMax
- string1 = res.getString(R.string.labelPlayerBuffer, playerBufferMax);
- textView = (TextView) findViewById(R.id.textPlayerBuffer);
- textView.setText(string1);
-
- //record buffer
- int recorderBufferMax = 8000;
- int recorderBufferMin = 16;
- mNumberPickerRecorderBuffer = (NumberPicker) findViewById(R.id.numberpickerRecorderBuffer);
- mNumberPickerRecorderBuffer.setMaxValue(recorderBufferMax);
- mNumberPickerRecorderBuffer.setMinValue(recorderBufferMin);
- mNumberPickerRecorderBuffer.setWrapSelectorWheel(false);
- mNumberPickerRecorderBuffer.setOnValueChangedListener(this);
- int recorderBuffer = getApp().getRecorderBufferSizeInBytes()/ Constant.BYTES_PER_FRAME;
- mNumberPickerRecorderBuffer.setValue(recorderBuffer);
- log("recorderBuffer = " + recorderBuffer);
-
- // set the string to display playerBufferMax
- string1 = res.getString(R.string.labelRecorderBuffer, recorderBufferMax);
- textView = (TextView) findViewById(R.id.textRecorderBuffer);
- textView.setText(string1);
+ mSpinnerChannelIndex = (Spinner) findViewById(R.id.spinnerChannelIndex);
+ ArrayAdapter<CharSequence> adapter3 = ArrayAdapter.createFromResource(this,
+ R.array.channelIndex_array, android.R.layout.simple_spinner_item);
+ // Specify the layout to use when the list of choices appears
+ adapter3.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+ // Apply the adapter to the spinner
+ mSpinnerChannelIndex.setAdapter(adapter3);
+ mSpinnerChannelIndex.setOnItemSelectedListener(this);
+
+ // Settings Picker for Buffer Test Duration
+ mBufferTestDurationUI = (SettingsPicker) findViewById(R.id.bufferTestDurationSetting);
+ mBufferTestDurationUI.setMinMaxDefault(Constant.BUFFER_TEST_DURATION_SECONDS_MIN,
+ Constant.BUFFER_TEST_DURATION_SECONDS_MAX, getApp().getBufferTestDuration());
+ mBufferTestDurationUI.setTitle(getResources().getString(R.string.labelBufferTestDuration,
+ Constant.BUFFER_TEST_DURATION_SECONDS_MAX));
+ mBufferTestDurationUI.setSettingsChangeListener(new SettingsPicker.SettingChangeListener() {
+ @Override
+ public void settingChanged(int seconds) {
+ log("buffer test new duration: " + seconds);
+ getApp().setBufferTestDuration(seconds);
+ setSettingsHaveChanged();
+ }
+ });
+
+ // Settings Picker for Wave Plot Duration
+ mWavePlotDurationUI = (SettingsPicker) findViewById(R.id.wavePlotDurationSetting);
+ mWavePlotDurationUI.setMinMaxDefault(Constant.BUFFER_TEST_WAVE_PLOT_DURATION_SECONDS_MIN,
+ Constant.BUFFER_TEST_WAVE_PLOT_DURATION_SECONDS_MAX,
+ getApp().getBufferTestWavePlotDuration());
+ mWavePlotDurationUI.setTitle(getResources().getString(
+ R.string.labelBufferTestWavePlotDuration,
+ Constant.BUFFER_TEST_WAVE_PLOT_DURATION_SECONDS_MAX));
+ mWavePlotDurationUI.setSettingsChangeListener(new SettingsPicker.SettingChangeListener() {
+ @Override
+ public void settingChanged(int value) {
+ log("buffer test's wave plot new duration:" + value);
+ getApp().setBufferTestWavePlotDuration(value);
+ setSettingsHaveChanged();
+ }
+ });
+
+ // Settings Picker for Player Buffer Period
+ mPlayerBufferUI = (SettingsPicker) findViewById(R.id.playerBufferSetting);
+ mPlayerBufferUI.setMinMaxDefault(Constant.PLAYER_BUFFER_FRAMES_MIN,
+ Constant.PLAYER_BUFFER_FRAMES_MAX,
+ getApp().getPlayerBufferSizeInBytes() / Constant.BYTES_PER_FRAME);
+ mPlayerBufferUI.setTitle(getResources().getString(
+ R.string.labelPlayerBuffer, Constant.PLAYER_BUFFER_FRAMES_MAX));
+ mPlayerBufferUI.setSettingsChangeListener(new SettingsPicker.SettingChangeListener() {
+ @Override
+ public void settingChanged(int value) {
+ log("player buffer new size " + value);
+ getApp().setPlayerBufferSizeInBytes(value * Constant.BYTES_PER_FRAME);
+ int audioThreadType = mSpinnerAudioThreadType.getSelectedItemPosition();
+ // in native mode, recorder buffer size = player buffer size
+ if (audioThreadType == Constant.AUDIO_THREAD_TYPE_NATIVE) {
+ getApp().setRecorderBufferSizeInBytes(value * Constant.BYTES_PER_FRAME);
+ mRecorderBufferUI.setValue(value);
+ }
+ setSettingsHaveChanged();
+ }
+ });
+
+ // Settings Picker for Recorder Buffer Period
+ mRecorderBufferUI = (SettingsPicker) findViewById(R.id.recorderBufferSetting);
+ mRecorderBufferUI.setMinMaxDefault(Constant.RECORDER_BUFFER_FRAMES_MIN,
+ Constant.RECORDER_BUFFER_FRAMES_MAX,
+ getApp().getRecorderBufferSizeInBytes() / Constant.BYTES_PER_FRAME);
+ mRecorderBufferUI.setTitle(getResources().getString(R.string.labelRecorderBuffer,
+ Constant.RECORDER_BUFFER_FRAMES_MAX));
+ mRecorderBufferUI.setSettingsChangeListener(new SettingsPicker.SettingChangeListener() {
+ @Override
+ public void settingChanged(int value) {
+ log("recorder buffer new size:" + value);
+ getApp().setRecorderBufferSizeInBytes(value * Constant.BYTES_PER_FRAME);
+ setSettingsHaveChanged();
+ }
+ });
+
+ // Settings Picker for Number of Load Threads
+ mLoadThreadUI = (SettingsPicker) findViewById(R.id.numLoadThreadsSetting);
+ mLoadThreadUI.setMinMaxDefault(Constant.MIN_NUM_LOAD_THREADS, Constant.MAX_NUM_LOAD_THREADS,
+ getApp().getNumberOfLoadThreads());
+ mLoadThreadUI.setTitle(getResources().getString(R.string.loadThreadsLabel));
+ mLoadThreadUI.setSettingsChangeListener(new SettingsPicker.SettingChangeListener() {
+ @Override
+ public void settingChanged(int value) {
+ log("new num load threads:" + value);
+ getApp().setNumberOfLoadThreads(value);
+ setSettingsHaveChanged();
+ }
+ });
refresh();
}
@@ -184,34 +205,36 @@ public class SettingsActivity extends Activity implements OnItemSelectedListener
@Override
public void onBackPressed() {
log("on back pressed");
- settingsChanged();
+ setSettingsHaveChanged();
finish();
}
private void refresh() {
- int bufferTestDuration = getApp().getBufferTestDuration();
- mNumberPickerBufferTestDuration.setValue(bufferTestDuration);
+ mBufferTestDurationUI.setValue(getApp().getBufferTestDuration());
+ mWavePlotDurationUI.setValue(getApp().getBufferTestWavePlotDuration());
- int bufferTestWavePlotDuration = getApp().getBufferTestWavePlotDuration();
- mNumberPickerBufferTestWavePlotDuration.setValue(bufferTestWavePlotDuration);
+ mPlayerBufferUI.setValue(getApp().getPlayerBufferSizeInBytes() / Constant.BYTES_PER_FRAME);
+ mRecorderBufferUI.setValue(
+ getApp().getRecorderBufferSizeInBytes() / Constant.BYTES_PER_FRAME);
- int playerBuffer = getApp().getPlayerBufferSizeInBytes() / Constant.BYTES_PER_FRAME;
- mNumberPickerPlayerBuffer.setValue(playerBuffer);
- int recorderBuffer = getApp().getRecorderBufferSizeInBytes() / Constant.BYTES_PER_FRAME;
- mNumberPickerRecorderBuffer.setValue(recorderBuffer);
-
- if (getApp().getAudioThreadType() == Constant.AUDIO_THREAD_TYPE_JAVA) {
- mNumberPickerRecorderBuffer.setEnabled(true);
- } else {
- mNumberPickerRecorderBuffer.setEnabled(false);
- }
+ mRecorderBufferUI.setEnabled(
+ getApp().getAudioThreadType() == Constant.AUDIO_THREAD_TYPE_JAVA);
int samplingRate = getApp().getSamplingRate();
String currentValue = String.valueOf(samplingRate);
int nPosition = mAdapterSamplingRate.getPosition(currentValue);
mSpinnerSamplingRate.setSelection(nPosition);
+
+ if (getApp().getAudioThreadType() == Constant.AUDIO_THREAD_TYPE_JAVA) {
+ mSpinnerChannelIndex.setSelection(getApp().getChannelIndex() + 1, false);
+ mSpinnerChannelIndex.setEnabled(true);
+ } else {
+ mSpinnerChannelIndex.setSelection(0, false);
+ mSpinnerChannelIndex.setEnabled(false);
+ }
+
String info = getApp().getSystemInfo();
mTextSettingsInfo.setText("SETTINGS - " + info);
}
@@ -227,7 +250,7 @@ public class SettingsActivity extends Activity implements OnItemSelectedListener
String stringValue = mSpinnerSamplingRate.getSelectedItem().toString();
int samplingRate = Integer.parseInt(stringValue);
getApp().setSamplingRate(samplingRate);
- settingsChanged();
+ setSettingsHaveChanged();
log("Sampling Rate: " + stringValue);
refresh();
break;
@@ -235,46 +258,29 @@ public class SettingsActivity extends Activity implements OnItemSelectedListener
int audioThreadType = mSpinnerAudioThreadType.getSelectedItemPosition();
getApp().setAudioThreadType(audioThreadType);
getApp().computeDefaults();
- settingsChanged();
+ setSettingsHaveChanged();
log("AudioThreadType:" + audioThreadType);
refresh();
break;
+ case R.id.spinnerChannelIndex:
+ int channelIndex = mSpinnerChannelIndex.getSelectedItemPosition() - 1;
+ getApp().setChannelIndex(channelIndex);
+ getApp().computeDefaults();
+ setSettingsHaveChanged();
+ log("channelIndex:" + channelIndex);
+ refresh();
+ break;
case R.id.spinnerMicSource:
int micSource = mSpinnerMicSource.getSelectedItemPosition();
getApp().setMicSource(micSource);
- settingsChanged();
+ setSettingsHaveChanged();
log("mic Source:" + micSource);
refresh();
break;
}
}
-
- public void onValueChange(NumberPicker picker, int oldVal, int newVal) {
- if (picker == mNumberPickerPlayerBuffer) {
- log("player buffer new size " + oldVal + " -> " + newVal);
- getApp().setPlayerBufferSizeInBytes(newVal * Constant.BYTES_PER_FRAME);
- int audioThreadType = mSpinnerAudioThreadType.getSelectedItemPosition();
- // in native mode, recorder buffer size = player buffer size
- if (audioThreadType == Constant.AUDIO_THREAD_TYPE_NATIVE){
- getApp().setRecorderBufferSizeInBytes(newVal * Constant.BYTES_PER_FRAME);
- }
- } else if (picker == mNumberPickerRecorderBuffer) {
- log("recorder buffer new size " + oldVal + " -> " + newVal);
- getApp().setRecorderBufferSizeInBytes(newVal * Constant.BYTES_PER_FRAME);
- } else if (picker == mNumberPickerBufferTestDuration) {
- log("buffer test new duration: " + oldVal + " -> " + newVal);
- getApp().setBufferTestDuration(newVal);
- } else if (picker == mNumberPickerBufferTestWavePlotDuration) {
- log("buffer test's wave plot new duration: " + oldVal + " -> " + newVal);
- getApp().setBufferTestWavePlotDuration(newVal);
- }
- settingsChanged();
- refresh();
- }
-
-
- private void settingsChanged() {
+ private void setSettingsHaveChanged() {
Intent intent = new Intent();
setResult(RESULT_OK, intent);
}
@@ -327,7 +333,6 @@ public class SettingsActivity extends Activity implements OnItemSelectedListener
//
// }
-
private LoopbackApplication getApp() {
return (LoopbackApplication) this.getApplication();
}
@@ -337,4 +342,5 @@ public class SettingsActivity extends Activity implements OnItemSelectedListener
Log.v(TAG, msg);
}
+
}
diff --git a/LoopbackApp/app/src/main/java/org/drrickorang/loopback/SettingsPicker.java b/LoopbackApp/app/src/main/java/org/drrickorang/loopback/SettingsPicker.java
new file mode 100644
index 0000000..969cc9a
--- /dev/null
+++ b/LoopbackApp/app/src/main/java/org/drrickorang/loopback/SettingsPicker.java
@@ -0,0 +1,140 @@
+/*
+ * 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.LinearLayout;
+import android.widget.SeekBar;
+import android.widget.TextView;
+
+public class SettingsPicker extends LinearLayout implements SeekBar.OnSeekBarChangeListener,
+ TextView.OnEditorActionListener {
+
+ protected TextView mTitleTextView;
+ protected EditText mValueEditText;
+ protected SeekBar mValueSeekBar;
+ protected SettingChangeListener mSettingsChangeListener;
+
+ protected int mMinimumValue;
+ protected int mMaximumValue;
+
+ public interface SettingChangeListener {
+ public void settingChanged(int value);
+ }
+
+ public SettingsPicker(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ inflate(context, R.layout.settings_picker, this);
+
+ mTitleTextView = (TextView) findViewWithTag("title");
+ mValueEditText = (EditText) findViewWithTag("valueText");
+ mValueSeekBar = (SeekBar) findViewWithTag("seekbar");
+
+ mValueEditText.setOnEditorActionListener(this);
+ mValueSeekBar.setOnSeekBarChangeListener(this);
+ }
+
+ public void setMinMaxDefault(int min, int max, int def) {
+ mMinimumValue = min;
+ mMaximumValue = max;
+ mValueSeekBar.setMax(max - min);
+ setValue(def);
+ }
+
+ public void setTitle(String title) {
+ mTitleTextView.setText(title);
+ }
+
+ public void setValue(int value) {
+ mValueSeekBar.setProgress(value - mMinimumValue);
+ mValueEditText.setText(Integer.toString(value));
+ }
+
+ public void setSettingsChangeListener(SettingChangeListener settingsChangeListener) {
+ mSettingsChangeListener = settingsChangeListener;
+ }
+
+ protected void textChanged(int value) {
+ mValueSeekBar.setProgress(value - mMinimumValue);
+ if (mSettingsChangeListener != null) {
+ mSettingsChangeListener.settingChanged(value);
+ }
+ }
+
+ protected void sliderChanged(int value, boolean userInteractionFinished) {
+ mValueEditText.setText(Integer.toString(value));
+ if (userInteractionFinished && mSettingsChangeListener != null) {
+ mSettingsChangeListener.settingChanged(value);
+ }
+ }
+
+ @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);
+ }
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+ if (fromUser) {
+ sliderChanged(mMinimumValue + progress, false);
+ }
+ }
+
+ @Override
+ public void onStartTrackingTouch(SeekBar seekBar) {
+
+ }
+
+ @Override
+ public void onStopTrackingTouch(SeekBar seekBar) {
+ sliderChanged(mMinimumValue + seekBar.getProgress(), true);
+ }
+
+ @Override
+ public void setEnabled(boolean enabled) {
+ mValueEditText.setEnabled(enabled);
+ mValueSeekBar.setEnabled(enabled);
+ }
+}
diff --git a/LoopbackApp/app/src/main/java/org/drrickorang/loopback/SineWaveTone.java b/LoopbackApp/app/src/main/java/org/drrickorang/loopback/SineWaveTone.java
index a0b7fd9..186d847 100644
--- a/LoopbackApp/app/src/main/java/org/drrickorang/loopback/SineWaveTone.java
+++ b/LoopbackApp/app/src/main/java/org/drrickorang/loopback/SineWaveTone.java
@@ -26,7 +26,6 @@ package org.drrickorang.loopback;
public class SineWaveTone extends ToneGeneration {
private int mCount; // counts the total samples produced.
private double mPhase; // current phase
- private double mAmplitude; // this value should be from 0 to 1.0
private final double mPhaseIncrement; // phase incrementation associated with mFrequency
diff --git a/LoopbackApp/app/src/main/java/org/drrickorang/loopback/ToneGeneration.java b/LoopbackApp/app/src/main/java/org/drrickorang/loopback/ToneGeneration.java
index 0fde60a..176c7c1 100644
--- a/LoopbackApp/app/src/main/java/org/drrickorang/loopback/ToneGeneration.java
+++ b/LoopbackApp/app/src/main/java/org/drrickorang/loopback/ToneGeneration.java
@@ -23,6 +23,7 @@ package org.drrickorang.loopback;
public abstract class ToneGeneration {
protected int mSamplingRate;
+ protected double mAmplitude; // this value should be from 0 to 1.0
protected boolean mIsGlitchEnabled = false; // indicates we are inserting glitches or not
@@ -55,4 +56,8 @@ public abstract class ToneGeneration {
mIsGlitchEnabled = isGlitchEnabled;
}
+ public void setAmplitude(double amplitude) {
+ mAmplitude = amplitude;
+ }
+
}
diff --git a/LoopbackApp/app/src/main/java/org/drrickorang/loopback/TwoSineWavesTone.java b/LoopbackApp/app/src/main/java/org/drrickorang/loopback/TwoSineWavesTone.java
index 35874b4..27083cf 100644
--- a/LoopbackApp/app/src/main/java/org/drrickorang/loopback/TwoSineWavesTone.java
+++ b/LoopbackApp/app/src/main/java/org/drrickorang/loopback/TwoSineWavesTone.java
@@ -27,7 +27,6 @@ public class TwoSineWavesTone extends ToneGeneration {
private int mCount; // counts the total samples produced.
private double mPhase1; // current phase associated with mFrequency1
private double mPhase2; // current phase associated with mFrequency2
- private double mAmplitude; // this value should be from 0 to 1.0
private final double mPhaseIncrement1; // phase incrementation associated with mFrequency1
private final double mPhaseIncrement2; // phase incrementation associated with mFrequency2
diff --git a/LoopbackApp/app/src/main/java/org/drrickorang/loopback/Utilities.java b/LoopbackApp/app/src/main/java/org/drrickorang/loopback/Utilities.java
index 2d74b29..15928bf 100644
--- a/LoopbackApp/app/src/main/java/org/drrickorang/loopback/Utilities.java
+++ b/LoopbackApp/app/src/main/java/org/drrickorang/loopback/Utilities.java
@@ -25,7 +25,7 @@ public class Utilities {
/** Multiply the input array with a hanning window. */
- public static double[] hanningWindow(double[] samples) {
+ public static void hanningWindow(double[] samples) {
int length = samples.length;
final double alpha = 0.5;
final double beta = 0.5;
@@ -35,7 +35,6 @@ public class Utilities {
samples[i] *= alpha - beta * Math.cos(coefficient);
}
- return samples;
}
diff --git a/LoopbackApp/app/src/main/java/org/drrickorang/loopback/WaveDataRingBuffer.java b/LoopbackApp/app/src/main/java/org/drrickorang/loopback/WaveDataRingBuffer.java
new file mode 100644
index 0000000..935c805
--- /dev/null
+++ b/LoopbackApp/app/src/main/java/org/drrickorang/loopback/WaveDataRingBuffer.java
@@ -0,0 +1,96 @@
+/*
+ * 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;
+
+/**
+ * Maintains a recording of wave data of last n seconds
+ */
+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 WaveDataRingBuffer(int size) {
+ if (size < Constant.SAMPLING_RATE_MIN * Constant.BUFFER_TEST_DURATION_SECONDS_MIN) {
+ size = Constant.SAMPLING_RATE_MIN * Constant.BUFFER_TEST_DURATION_SECONDS_MIN;
+ } else if (size > Constant.SAMPLING_RATE_MAX * Constant.BUFFER_TEST_DURATION_SECONDS_MAX) {
+ size = Constant.SAMPLING_RATE_MAX * Constant.BUFFER_TEST_DURATION_SECONDS_MAX;
+ }
+
+ mWaveRecord = new double[size];
+ }
+
+ /**
+ * Write length number of doubles from data into ring buffer from starting srcPos
+ */
+ 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;
+ }
+
+ 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;
+ }
+
+ int availBuff = mWaveRecord.length - index;
+
+ 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 synchronized double[] getWaveRecord() {
+ double outputBuffer[] = new double[mWaveRecord.length];
+
+ 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);
+ }
+
+ return outputBuffer;
+
+ }
+
+}
diff --git a/LoopbackApp/app/src/main/jni/jni_sles.c b/LoopbackApp/app/src/main/jni/jni_sles.c
index 5a05d5c..99c5543 100644
--- a/LoopbackApp/app/src/main/jni/jni_sles.c
+++ b/LoopbackApp/app/src/main/jni/jni_sles.c
@@ -23,15 +23,18 @@
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) {
+ jint testType, jdouble frequency1, jobject byteBuffer, jshortArray loopbackTone) {
sles_data * pSles = NULL;
char* byteBufferPtr = (*env)->GetDirectBufferAddress(env, byteBuffer);
int byteBufferLength = (*env)->GetDirectBufferCapacity(env, byteBuffer);
+ short* loopbackToneArray = (*env)->GetShortArrayElements(env, loopbackTone, 0);
+
if (slesInit(&pSles, samplingRate, frameCount, micSource,
- testType, frequency1, byteBufferPtr, byteBufferLength) != SLES_FAIL) {
+ testType, frequency1, byteBufferPtr, byteBufferLength,
+ loopbackToneArray) != SLES_FAIL) {
return (long) pSles;
}
diff --git a/LoopbackApp/app/src/main/jni/jni_sles.h b/LoopbackApp/app/src/main/jni/jni_sles.h
index 33c792c..de62948 100644
--- a/LoopbackApp/app/src/main/jni/jni_sles.h
+++ b/LoopbackApp/app/src/main/jni/jni_sles.h
@@ -26,7 +26,8 @@ extern "C" {
////////////////////////
////SLE
JNIEXPORT jlong JNICALL Java_org_drrickorang_loopback_NativeAudioThread_slesInit
- (JNIEnv *, jobject, jint, jint, jint, jint, jdouble, jobject byteBuffer);
+ (JNIEnv *, jobject, jint, jint, jint, jint, jdouble, jobject byteBuffer,
+ jshortArray loopbackTone);
JNIEXPORT jint JNICALL Java_org_drrickorang_loopback_NativeAudioThread_slesProcessNext
(JNIEnv *, jobject, jlong, jdoubleArray, jlong);
diff --git a/LoopbackApp/app/src/main/jni/sles.cpp b/LoopbackApp/app/src/main/jni/sles.cpp
index 4e8f358..6110a74 100644
--- a/LoopbackApp/app/src/main/jni/sles.cpp
+++ b/LoopbackApp/app/src/main/jni/sles.cpp
@@ -26,23 +26,14 @@
#define _USE_MATH_DEFINES
#include <cmath>
-
#include "sles.h"
#include <stdio.h>
-#include <stdlib.h>
-#include <stddef.h>
-
#include <assert.h>
-#include <pthread.h>
-#include <stdio.h>
-#include <stdlib.h>
-#include <string.h>
#include <unistd.h>
-//#include <jni.h>
-#include <time.h>
int slesInit(sles_data ** ppSles, int samplingRate, int frameCount, int micSource,
- int testType, double frequency1, char* byteBufferPtr, int byteBufferLength) {
+ int testType, double frequency1, char* byteBufferPtr, int byteBufferLength,
+ short* loopbackTone) {
int status = SLES_FAIL;
if (ppSles != NULL) {
sles_data * pSles = (sles_data*) malloc(sizeof(sles_data));
@@ -57,8 +48,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);
+ status = slesCreateServer(pSles, samplingRate, frameCount, micSource, testType,
+ frequency1, byteBufferPtr, byteBufferLength, loopbackTone);
SLES_PRINTF("slesCreateServer =%d", status);
}
}
@@ -269,7 +260,8 @@ static void playerCallback(SLBufferQueueItf caller __unused, void *context) {
}
if (pSles->injectImpulse == -1) { // here we inject pulse
- // Experimentally, a single frame impulse was insufficient to trigger feedback.
+
+ /*// Experimentally, a single frame impulse was insufficient to trigger feedback.
// Also a Nyquist frequency signal was also insufficient, probably because
// the response of output and/or input path was not adequate at high frequencies.
// This short burst of a few cycles of square wave at Nyquist/4 found to work well.
@@ -280,7 +272,15 @@ static void playerCallback(SLBufferQueueItf caller __unused, void *context) {
j < 4 ? 0x7FFF : 0x8000;
}
}
+ }*/
+
+ //inject java generated tone
+ for (unsigned i = 0; i < pSles->bufSizeInFrames; ++i) {
+ for (unsigned k = 0; k < pSles->channels; ++k) {
+ ((short *) buffer)[i * pSles->channels + k] = pSles->loopbackTone[i];
+ }
}
+
pSles->injectImpulse = 0;
}
} else if (pSles->testType == TEST_TYPE_BUFFER_PERIOD) {
@@ -292,7 +292,9 @@ static void playerCallback(SLBufferQueueItf caller __unused, void *context) {
bool isGlitchEnabled = false;
for (unsigned i = 0; i < pSles->bufSizeInFrames; i++) {
value = (short) (sin(pSles->bufferTestPhase1) * maxShort * amplitude);
- ((short *) buffer)[i] = value;
+ for (unsigned k = 0; k < pSles->channels; ++k) {
+ ((short *) buffer)[i* pSles->channels + k] = value;
+ }
pSles->bufferTestPhase1 += twoPi * phaseIncrement;
// insert glitches if isGlitchEnabled == true, and insert it for every second
@@ -367,7 +369,8 @@ void collectPlayerBufferPeriod(sles_data *pSles) {
int slesCreateServer(sles_data *pSles, int samplingRate, int frameCount, int micSource,
- int testType, double frequency1, char* byteBufferPtr, int byteBufferLength) {
+ int testType, double frequency1, char *byteBufferPtr, int byteBufferLength,
+ short *loopbackTone) {
int status = SLES_FAIL;
if (pSles != NULL) {
@@ -516,6 +519,9 @@ int slesCreateServer(sles_data *pSles, int samplingRate, int frameCount, int mic
pSles->byteBufferPtr = byteBufferPtr;
pSles->byteBufferLength = byteBufferLength;
+ //init loopback tone
+ pSles->loopbackTone = loopbackTone;
+
SLresult result;
// create engine
diff --git a/LoopbackApp/app/src/main/jni/sles.h b/LoopbackApp/app/src/main/jni/sles.h
index 57690f1..ca1ad97 100644
--- a/LoopbackApp/app/src/main/jni/sles.h
+++ b/LoopbackApp/app/src/main/jni/sles.h
@@ -18,7 +18,7 @@
#include <SLES/OpenSLES_Android.h>
#include <pthread.h>
#include <android/log.h>
-
+#include <jni.h>
#ifndef _Included_org_drrickorang_loopback_sles
#define _Included_org_drrickorang_loopback_sles
@@ -94,6 +94,8 @@ typedef struct {
int count;
char* byteBufferPtr;
int byteBufferLength;
+
+ short* loopbackTone;
} sles_data;
enum {
@@ -108,7 +110,8 @@ enum {
} SLES_STATUS_ENUM;
int slesInit(sles_data ** ppSles, int samplingRate, int frameCount, int micSource,
- int testType, double frequency1, char* byteBufferPtr, int byteBufferLength);
+ int testType, double frequency1, char* byteBufferPtr, int byteBufferLength,
+ short* loopbackTone);
//note the double pointer to properly free the memory of the structure
int slesDestroy(sles_data ** ppSles);
@@ -118,7 +121,8 @@ int slesDestroy(sles_data ** ppSles);
int slesFull(sles_data *pSles);
int slesCreateServer(sles_data *pSles, int samplingRate, int frameCount, int micSource,
- int testType, double frequency1, char* byteBufferPtr, int byteBufferLength);
+ int testType, double frequency1, char* qbyteBufferPtr, int byteBufferLength,
+ short* loopbackTone);
int slesProcessNext(sles_data *pSles, double *pSamples, long maxSamples);
int slesDestroyServer(sles_data *pSles);
int* slesGetRecorderBufferPeriod(sles_data *pSles);
diff --git a/LoopbackApp/app/src/main/res/drawable-xxxhdpi/ic_assessment.png b/LoopbackApp/app/src/main/res/drawable-xxxhdpi/ic_assessment.png
new file mode 100644
index 0000000..47e6b52
--- /dev/null
+++ b/LoopbackApp/app/src/main/res/drawable-xxxhdpi/ic_assessment.png
Binary files differ
diff --git a/LoopbackApp/app/src/main/res/drawable-xxxhdpi/ic_description.png b/LoopbackApp/app/src/main/res/drawable-xxxhdpi/ic_description.png
new file mode 100644
index 0000000..687d5f8
--- /dev/null
+++ b/LoopbackApp/app/src/main/res/drawable-xxxhdpi/ic_description.png
Binary files differ
diff --git a/LoopbackApp/app/src/main/res/drawable-xxxhdpi/ic_help.png b/LoopbackApp/app/src/main/res/drawable-xxxhdpi/ic_help.png
new file mode 100644
index 0000000..c5b4f68
--- /dev/null
+++ b/LoopbackApp/app/src/main/res/drawable-xxxhdpi/ic_help.png
Binary files differ
diff --git a/LoopbackApp/app/src/main/res/drawable-xxxhdpi/ic_launcher.png b/LoopbackApp/app/src/main/res/drawable-xxxhdpi/ic_launcher.png
index 05a69ea..df5851e 100644
--- a/LoopbackApp/app/src/main/res/drawable-xxxhdpi/ic_launcher.png
+++ b/LoopbackApp/app/src/main/res/drawable-xxxhdpi/ic_launcher.png
Binary files differ
diff --git a/LoopbackApp/app/src/main/res/drawable-xxxhdpi/ic_play_arrow.png b/LoopbackApp/app/src/main/res/drawable-xxxhdpi/ic_play_arrow.png
new file mode 100644
index 0000000..6f7a047
--- /dev/null
+++ b/LoopbackApp/app/src/main/res/drawable-xxxhdpi/ic_play_arrow.png
Binary files differ
diff --git a/LoopbackApp/app/src/main/res/drawable-xxxhdpi/ic_report.png b/LoopbackApp/app/src/main/res/drawable-xxxhdpi/ic_report.png
new file mode 100644
index 0000000..4c44889
--- /dev/null
+++ b/LoopbackApp/app/src/main/res/drawable-xxxhdpi/ic_report.png
Binary files differ
diff --git a/LoopbackApp/app/src/main/res/drawable-xxxhdpi/ic_save.png b/LoopbackApp/app/src/main/res/drawable-xxxhdpi/ic_save.png
new file mode 100644
index 0000000..fbc4acd
--- /dev/null
+++ b/LoopbackApp/app/src/main/res/drawable-xxxhdpi/ic_save.png
Binary files differ
diff --git a/LoopbackApp/app/src/main/res/drawable-xxxhdpi/ic_settings.png b/LoopbackApp/app/src/main/res/drawable-xxxhdpi/ic_settings.png
new file mode 100644
index 0000000..8276847
--- /dev/null
+++ b/LoopbackApp/app/src/main/res/drawable-xxxhdpi/ic_settings.png
Binary files differ
diff --git a/LoopbackApp/app/src/main/res/drawable-xxxhdpi/ic_stop.png b/LoopbackApp/app/src/main/res/drawable-xxxhdpi/ic_stop.png
new file mode 100644
index 0000000..0255c0e
--- /dev/null
+++ b/LoopbackApp/app/src/main/res/drawable-xxxhdpi/ic_stop.png
Binary files differ
diff --git a/LoopbackApp/app/src/main/res/drawable-xxxhdpi/ic_zoom_in.png b/LoopbackApp/app/src/main/res/drawable-xxxhdpi/ic_zoom_in.png
new file mode 100644
index 0000000..cc66362
--- /dev/null
+++ b/LoopbackApp/app/src/main/res/drawable-xxxhdpi/ic_zoom_in.png
Binary files differ
diff --git a/LoopbackApp/app/src/main/res/drawable-xxxhdpi/ic_zoom_out.png b/LoopbackApp/app/src/main/res/drawable-xxxhdpi/ic_zoom_out.png
new file mode 100644
index 0000000..6b72870
--- /dev/null
+++ b/LoopbackApp/app/src/main/res/drawable-xxxhdpi/ic_zoom_out.png
Binary files differ
diff --git a/LoopbackApp/app/src/main/res/drawable-xxxhdpi/ic_zoom_out_full.png b/LoopbackApp/app/src/main/res/drawable-xxxhdpi/ic_zoom_out_full.png
new file mode 100644
index 0000000..79152d2
--- /dev/null
+++ b/LoopbackApp/app/src/main/res/drawable-xxxhdpi/ic_zoom_out_full.png
Binary files differ
diff --git a/LoopbackApp/app/src/main/res/layout/main_activity.xml b/LoopbackApp/app/src/main/res/layout/main_activity.xml
index 826e670..04fe52e 100644
--- a/LoopbackApp/app/src/main/res/layout/main_activity.xml
+++ b/LoopbackApp/app/src/main/res/layout/main_activity.xml
@@ -35,35 +35,24 @@
<Button
xmlns:android="http://schemas.android.com/apk/res/android"
- android:id="@+id/buttonTest"
+ android:id="@+id/buttonStartLatencyTest"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/buttonTest_enabled"
+ android:drawableLeft="@drawable/ic_play_arrow"
+ style="@style/TextAppearance.AppCompat.Button"
android:onClick="onButtonLatencyTest"/>
<Button
xmlns:android="http://schemas.android.com/apk/res/android"
- android:id="@+id/buttonStopTest"
+ android:id="@+id/buttonStartBufferTest"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:text="@string/buttonStopTest"
- android:onClick="onButtonStopTest"/>
-
- <Button
- xmlns:android="http://schemas.android.com/apk/res/android"
- android:id="@+id/buttonSave"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="@string/buttonSave"
- android:onClick="onButtonSave"/>
+ android:text="@string/buttonBufferTest"
+ android:drawableLeft="@drawable/ic_play_arrow"
+ style="@style/TextAppearance.AppCompat.Button"
+ android:onClick="onButtonBufferTest"/>
- <Button
- xmlns:android="http://schemas.android.com/apk/res/android"
- android:id="@+id/buttonSettings"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="@string/buttonSettings"
- android:onClick="onButtonSettings"/>
</LinearLayout>
</HorizontalScrollView>
@@ -74,54 +63,32 @@
android:textColor="#000000"
android:text="@string/labelInfo"/>
- <HorizontalScrollView
- android:id="@+id/ScrollView2"
- android:layout_width="wrap_content"
+ <LinearLayout
+ android:orientation="horizontal"
+ android:layout_width="fill_parent"
android:layout_height="wrap_content">
- <LinearLayout
- xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:tools="http://schemas.android.com/tools"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:orientation="horizontal">
- <Button
- xmlns:android="http://schemas.android.com/apk/res/android"
- android:id="@+id/buttonZoomIn"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="@string/buttonZoomIn"
- android:onClick="onButtonZoomIn"/>
-
- <Button
- xmlns:android="http://schemas.android.com/apk/res/android"
- android:id="@+id/buttonZoomOut"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="@string/buttonZoomOut"
- android:onClick="onButtonZoomOut"/>
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="Current Level"
+ android:id="@+id/textViewCurrentLevel"/>
- <Button
- xmlns:android="http://schemas.android.com/apk/res/android"
- android:id="@+id/buttonZoomOutFull"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="@string/buttonZoomOutFull"
- android:onClick="onButtonZoomOutFull"/>
- <Button
- xmlns:android="http://schemas.android.com/apk/res/android"
- android:id="@+id/buttonAbout"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="@string/buttonAbout"
- android:onClick="onButtonAbout"/>
- </LinearLayout>
- </HorizontalScrollView>
+ <SeekBar
+ android:id="@+id/BarMasterLevel"
+ android:indeterminate="false"
+ android:max="100"
+ android:progress="0"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ style="?android:attr/progressBarStyleHorizontal" />
+ </LinearLayout>
<HorizontalScrollView
- android:id="@+id/ScrollView3"
+ android:id="@+id/glitchReportPanel"
android:layout_width="wrap_content"
- android:layout_height="wrap_content">
+ android:layout_height="wrap_content"
+ android:visibility="invisible">
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
@@ -131,47 +98,21 @@
<Button
xmlns:android="http://schemas.android.com/apk/res/android"
- android:id="@+id/buttonBufferTest"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="@string/buttonBufferTest"
- android:onClick="onButtonBufferTest"/>
-
-
- <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"/>
- </LinearLayout>
- </HorizontalScrollView>
-
- <HorizontalScrollView
- android:id="@+id/ScrollView4"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content">
- <LinearLayout
- xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:tools="http://schemas.android.com/tools"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:orientation="horizontal">
-
- <TextView
- android:id="@+id/bufferPeriods"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:textColor="#000000"
- android:textSize="18sp"
- android:text="@string/showBufferPeriods"/>
<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"/>
@@ -180,6 +121,8 @@
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>
@@ -187,35 +130,15 @@
<LinearLayout
android:orientation="horizontal"
- android:layout_width="fill_parent"
- android:layout_height="wrap_content">
-
- <TextView
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="Current Level"
- android:id="@+id/textViewCurrentLevel"/>
-
- <SeekBar
- android:id="@+id/BarMasterLevel"
- android:indeterminate="false"
- android:max="100"
- android:progress="0"
- android:layout_width="fill_parent"
- android:layout_height="wrap_content"
- style="?android:attr/progressBarStyleHorizontal" />
- </LinearLayout>
-
- <LinearLayout
- android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:layout_width="250dp"
android:layout_height="wrap_content"
- android:text="latency"
- android:id="@+id/textViewEstimatedLatency"
+ android:text=""
+ android:id="@+id/resultSummary"
+ android:visibility="invisible"
android:textStyle="bold"/>
</LinearLayout>
@@ -235,4 +158,93 @@
android:layout_weight="1"/>
</LinearLayout>
+ <RelativeLayout
+ android:id="@+id/zoomAndSaveControlPanel"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="left"
+ android:padding="10dp"
+ android:visibility="invisible"
+ android:orientation="horizontal">
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentLeft="true">
+
+ <ImageButton
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/buttonZoomOutFull"
+ android:layout_width="40dp"
+ android:layout_height="40dp"
+ android:paddingEnd="5dp"
+ android:paddingRight="5dp"
+ android:paddingLeft="5dp"
+ android:text="@string/buttonZoomOutFull"
+ android:src="@drawable/ic_zoom_out_full"
+ android:background="@null"
+ android:onClick="onButtonZoomOutFull"/>
+
+ <ImageButton
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/buttonZoomOut"
+ android:layout_width="40dp"
+ android:layout_height="40dp"
+ android:paddingEnd="5dp"
+ android:paddingRight="5dp"
+ android:paddingLeft="5dp"
+ android:text="@string/buttonZoomOut"
+ android:src="@drawable/ic_zoom_out"
+ android:background="@null"
+ android:onClick="onButtonZoomOut"/>
+
+ <ImageButton
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/buttonZoomIn"
+ android:layout_width="40dp"
+ android:layout_height="40dp"
+ android:paddingEnd="5dp"
+ android:paddingRight="5dp"
+ android:paddingLeft="5dp"
+ android:text="@string/buttonZoomIn"
+ android:src="@drawable/ic_zoom_in"
+ android:background="@null"
+ android:onClick="onButtonZoomIn"/>
+
+ </LinearLayout>
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentRight="true">
+
+ <ImageButton
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/buttonReport"
+ android:layout_width="40dp"
+ android:layout_height="40dp"
+ android:paddingEnd="5dp"
+ android:paddingRight="5dp"
+ android:paddingLeft="5dp"
+ android:src="@drawable/ic_report"
+ android:text="@string/buttonSave"
+ android:background="@null"
+ android:onClick="onButtonReport"/>
+
+ <ImageButton
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/buttonSave"
+ android:layout_width="40dp"
+ android:layout_height="40dp"
+ android:paddingEnd="5dp"
+ android:paddingRight="5dp"
+ android:paddingLeft="5dp"
+ android:src="@drawable/ic_save"
+ android:text="@string/buttonSave"
+ android:background="@null"
+ android:onClick="onButtonSave"/>
+
+ </LinearLayout>
+
+ </RelativeLayout>
+
</LinearLayout>
diff --git a/LoopbackApp/app/src/main/res/layout/glitches_activity.xml b/LoopbackApp/app/src/main/res/layout/report_window.xml
index f69d7cb..36d6b8b 100644
--- a/LoopbackApp/app/src/main/res/layout/glitches_activity.xml
+++ b/LoopbackApp/app/src/main/res/layout/report_window.xml
@@ -21,13 +21,14 @@
android:background="#FFFFFF">
<ScrollView
- android:id="@+id/GlitchesScroll"
+ android:id="@+id/ReportScroll"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scrollbars="vertical"
+ android:fadeScrollbars="false"
android:fillViewport="true">
<TextView
- android:id="@+id/GlitchesInfo"
+ android:id="@+id/ReportInfo"
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 e5ed94d..e136ebc 100644
--- a/LoopbackApp/app/src/main/res/layout/settings_activity.xml
+++ b/LoopbackApp/app/src/main/res/layout/settings_activity.xml
@@ -72,6 +72,21 @@
android:layout_height="1dp"
android:background="@android:color/darker_gray"/>
+ <TextView
+ android:id="@+id/textChannelIndex"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/labelChannelIndex"/>
+ <Spinner
+ android:id="@+id/spinnerChannelIndex"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"/>
+
+ <View
+ android:layout_width="fill_parent"
+ android:layout_height="1dp"
+ android:background="@android:color/darker_gray"/>
+
<Button
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/buttonDefaultSettings"
@@ -96,14 +111,9 @@
android:layout_height="1dp"
android:background="@android:color/darker_gray"/>
- <TextView
- android:id="@+id/textPlayerBuffer"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="@string/labelPlayerBuffer"/>
- <NumberPicker
- android:id="@+id/numberpickerPlayerBuffer"
- android:layout_width="wrap_content"
+ <org.drrickorang.loopback.SettingsPicker
+ android:id="@+id/playerBufferSetting"
+ android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<View
@@ -111,51 +121,46 @@
android:layout_height="1dp"
android:background="@android:color/darker_gray"/>
- <TextView
- android:id="@+id/textRecorderBuffer"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="@string/labelRecorderBuffer"/>
- <NumberPicker
- android:id="@+id/numberpickerRecorderBuffer"
- android:layout_width="wrap_content"
+ <org.drrickorang.loopback.SettingsPicker
+ android:id="@+id/recorderBufferSetting"
+ android:layout_width="match_parent"
android:layout_height="wrap_content"/>
-
<View
android:layout_width="fill_parent"
android:layout_height="1dp"
android:background="@android:color/darker_gray"/>
- <TextView
- android:id="@+id/textBufferTestDuration"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="@string/labelBufferTestDuration"/>
- <NumberPicker
- android:id="@+id/numberpickerBufferTestDuration"
- android:layout_width="wrap_content"
+ <org.drrickorang.loopback.SettingsPicker
+ android:id="@+id/bufferTestDurationSetting"
+ android:layout_width="match_parent"
android:layout_height="wrap_content"/>
-
<View
android:layout_width="fill_parent"
android:layout_height="1dp"
android:background="@android:color/darker_gray"/>
- <TextView
- android:id="@+id/textBufferTestWavePlotDuration"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="@string/labelBufferTestWavePlotDuration"/>
- <NumberPicker
- android:id="@+id/numberPickerBufferTestWavePlotDuration"
- android:layout_width="wrap_content"
+ <org.drrickorang.loopback.SettingsPicker
+ android:id="@+id/wavePlotDurationSetting"
+ android:layout_width="match_parent"
android:layout_height="wrap_content"/>
+ <View
+ android:layout_width="fill_parent"
+ android:layout_height="1dp"
+ android:background="@android:color/darker_gray" />
+
+
+ <org.drrickorang.loopback.SettingsPicker
+ android:id="@+id/numLoadThreadsSetting"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingBottom="100dp"/>
+
</LinearLayout>
</ScrollView>
</LinearLayout>
diff --git a/LoopbackApp/app/src/main/res/layout/settings_picker.xml b/LoopbackApp/app/src/main/res/layout/settings_picker.xml
new file mode 100644
index 0000000..5423b70
--- /dev/null
+++ b/LoopbackApp/app/src/main/res/layout/settings_picker.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<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:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:paddingBottom="20dp"/>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingBottom="10dp"
+ android:orientation="horizontal">
+
+ <RelativeLayout
+ android:layout_width="0dip"
+ android:layout_height="match_parent"
+ android:layout_weight="3">
+
+ <EditText
+ android:tag="valueText"
+ android:inputType="number"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textSize="20sp"
+ android:imeOptions="actionDone"
+ android:selectAllOnFocus="true"
+ android:gravity="center" />
+ </RelativeLayout>
+
+ <RelativeLayout
+ android:layout_width="0dip"
+ android:layout_height="match_parent"
+ android:layout_weight="7">
+
+ <SeekBar
+ android:tag="seekbar"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content" />
+ </RelativeLayout>
+
+ </LinearLayout>
+
+</LinearLayout> \ No newline at end of file
diff --git a/LoopbackApp/app/src/main/res/menu/tool_bar_menu.xml b/LoopbackApp/app/src/main/res/menu/tool_bar_menu.xml
new file mode 100644
index 0000000..0d98cd9
--- /dev/null
+++ b/LoopbackApp/app/src/main/res/menu/tool_bar_menu.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+ <item
+ android:id="@+id/action_help"
+ android:icon="@drawable/ic_help"
+ android:title="@string/buttonAbout"
+ android:showAsAction="always"/>
+
+ <item android:id="@+id/action_settings"
+ android:icon="@drawable/ic_settings"
+ android:title="@string/buttonSettings"
+ android:showAsAction="always"/>
+</menu>
diff --git a/LoopbackApp/app/src/main/res/values/strings.xml b/LoopbackApp/app/src/main/res/values/strings.xml
index 0840188..4da727f 100644
--- a/LoopbackApp/app/src/main/res/values/strings.xml
+++ b/LoopbackApp/app/src/main/res/values/strings.xml
@@ -20,8 +20,7 @@
<string name="buttonPlay_play">Refresh Screen</string>
<string name="buttonPlay_pause">Pause</string>
- <string name="buttonTest_enabled">Latency Test</string>
- <string name="buttonStopTest">Stop Test</string>
+ <string name="buttonTest_enabled">Round-Trip\nLatency Test</string>
<string name="buttonTest_disabled">FX Disabled Loopback 2</string>
<string name="buttonSave">Save Results</string>
<string name="buttonZoomOutFull">Unzoom</string>
@@ -32,9 +31,9 @@
<string name="buttonPlayerBufferPeriod">Player</string>
<string name="ReadHistTitle">Frequency vs. Recorder Buffer Period (ms) Plot</string>
<string name="WriteHistTitle">Frequency vs. Player Buffer Period (ms) Plot</string>
- <string name="buttonBufferTest">Detect Glitch</string>
- <string name="buttonGlitches">Show Glitches</string>
- <string name="showBufferPeriods">Show Buffer Periods: </string>
+ <string name="buttonBufferTest">Buffer Period\n&amp; Glitch Test</string>
+ <string name="buttonGlitches">Glitches</string>
+ <string name="numGlitches">Total Number of Glitches:</string>
<!-- disabled -->
<string name="buttonZoomInFull">In Full</string>
@@ -58,7 +57,7 @@
<string name="labelSamplingRate">Sampling Rate</string>
<string name="AboutInfo">Round-trip audio latency testing app\n
using the Dr. Rick O\'Rang audio loopback dongle.\n
- Authors: Ricardo Garcia (rago) and Tzu-Yin Tai\n
+ Authors: Ricardo Garcia (rago), Tzu-Yin Tai, and Brandon Swanson\n
Open source project on:\n
https://github.com/gkasten/drrickorang\n
References:\n
@@ -82,10 +81,11 @@
-ei TestType \t\t\t\t\t\t\t\t ####\t\t Audio Test Type\n
\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\n\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 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
</string>
<!-- spinnerSamplingRate Options -->
@@ -105,6 +105,21 @@
<item>native (JNI)</item>
</string-array>
+ <string name="labelChannelIndex">Channel Index</string>
+
+ <!-- spinnerChannelIndex Options -->
+ <string-array name="channelIndex_array">
+ <item>Mono</item>
+ <item>0</item>
+ <item>1</item>
+ <item>2</item>
+ <item>3</item>
+ <item>4</item>
+ <item>5</item>
+ <item>6</item>
+ <item>7</item>
+ </string-array>
+
<string name="labelPlayerBuffer">Player Buffer (Frames) (Max: %1$d)</string>
<string name="labelRecorderBuffer">Recorder Buffer (Frames) (Max: %1$d)</string>
<string name="buttonDefaultSettings">Compute Default Settings</string>
@@ -112,6 +127,9 @@
<string name="labelBufferTestDuration">Buffer Test Duration (Seconds) (Max: %1$d)</string>
<string name="labelBufferTestWavePlotDuration">Buffer Test Wave Plot Duration (Seconds)
(Max: %1$d)</string>
-
+ <string name="loadThreadsLabel">Number of Simulated Load Threads</string>
+ <string name="SaveFileDialogLabel">Save Files To:</string>
+ <string name="SaveFileDialogOK">//mnt/sdcard/</string>
+ <string name="SaveFileDialogChooseFilenames">Choose Filenames \n and Location</string>
</resources>