diff options
author | Glenn Kasten <gkasten@google.com> | 2016-02-09 15:59:53 -0800 |
---|---|---|
committer | Glenn Kasten <gkasten@google.com> | 2016-02-09 16:00:28 -0800 |
commit | 1be6921955b496665686edb1a96071f8f47c7409 (patch) | |
tree | 2c4cf810fdfab71388e202b139fb1d6e7eb896c0 /LoopbackApp | |
parent | eaacb9345a5043bbc7c4f908cae4fdbc02b5bff9 (diff) | |
download | drrickorang-1be6921955b496665686edb1a96071f8f47c7409.tar.gz |
Snap to commit 59d4f38aeaca9c4526b09b9fa9363ed7bcaf9fc9
Diffstat (limited to 'LoopbackApp')
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 Binary files differnew file mode 100644 index 0000000..47e6b52 --- /dev/null +++ b/LoopbackApp/app/src/main/res/drawable-xxxhdpi/ic_assessment.png diff --git a/LoopbackApp/app/src/main/res/drawable-xxxhdpi/ic_description.png b/LoopbackApp/app/src/main/res/drawable-xxxhdpi/ic_description.png Binary files differnew file mode 100644 index 0000000..687d5f8 --- /dev/null +++ b/LoopbackApp/app/src/main/res/drawable-xxxhdpi/ic_description.png diff --git a/LoopbackApp/app/src/main/res/drawable-xxxhdpi/ic_help.png b/LoopbackApp/app/src/main/res/drawable-xxxhdpi/ic_help.png Binary files differnew file mode 100644 index 0000000..c5b4f68 --- /dev/null +++ b/LoopbackApp/app/src/main/res/drawable-xxxhdpi/ic_help.png diff --git a/LoopbackApp/app/src/main/res/drawable-xxxhdpi/ic_launcher.png b/LoopbackApp/app/src/main/res/drawable-xxxhdpi/ic_launcher.png Binary files differindex 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 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 Binary files differnew file mode 100644 index 0000000..6f7a047 --- /dev/null +++ b/LoopbackApp/app/src/main/res/drawable-xxxhdpi/ic_play_arrow.png diff --git a/LoopbackApp/app/src/main/res/drawable-xxxhdpi/ic_report.png b/LoopbackApp/app/src/main/res/drawable-xxxhdpi/ic_report.png Binary files differnew file mode 100644 index 0000000..4c44889 --- /dev/null +++ b/LoopbackApp/app/src/main/res/drawable-xxxhdpi/ic_report.png diff --git a/LoopbackApp/app/src/main/res/drawable-xxxhdpi/ic_save.png b/LoopbackApp/app/src/main/res/drawable-xxxhdpi/ic_save.png Binary files differnew file mode 100644 index 0000000..fbc4acd --- /dev/null +++ b/LoopbackApp/app/src/main/res/drawable-xxxhdpi/ic_save.png diff --git a/LoopbackApp/app/src/main/res/drawable-xxxhdpi/ic_settings.png b/LoopbackApp/app/src/main/res/drawable-xxxhdpi/ic_settings.png Binary files differnew file mode 100644 index 0000000..8276847 --- /dev/null +++ b/LoopbackApp/app/src/main/res/drawable-xxxhdpi/ic_settings.png diff --git a/LoopbackApp/app/src/main/res/drawable-xxxhdpi/ic_stop.png b/LoopbackApp/app/src/main/res/drawable-xxxhdpi/ic_stop.png Binary files differnew file mode 100644 index 0000000..0255c0e --- /dev/null +++ b/LoopbackApp/app/src/main/res/drawable-xxxhdpi/ic_stop.png 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 Binary files differnew file mode 100644 index 0000000..cc66362 --- /dev/null +++ b/LoopbackApp/app/src/main/res/drawable-xxxhdpi/ic_zoom_in.png 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 Binary files differnew file mode 100644 index 0000000..6b72870 --- /dev/null +++ b/LoopbackApp/app/src/main/res/drawable-xxxhdpi/ic_zoom_out.png 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 Binary files differnew file mode 100644 index 0000000..79152d2 --- /dev/null +++ b/LoopbackApp/app/src/main/res/drawable-xxxhdpi/ic_zoom_out_full.png 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& 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> |