From c58bf2f1d8871c89e1d32b237193997cd55ec7d1 Mon Sep 17 00:00:00 2001 From: Glenn Kasten Date: Thu, 2 Jun 2016 08:09:36 -0700 Subject: Version 14 Snap to commit 27ed08872221cd6291777cab07bcc275e2c88106 --- LoopbackApp/app/src/main/AndroidManifest.xml | 7 +- .../drrickorang/loopback/BufferCallbackTimes.java | 42 +++- .../org/drrickorang/loopback/BufferPeriod.java | 43 +++- .../java/org/drrickorang/loopback/Correlation.java | 47 +++- .../org/drrickorang/loopback/LoopbackActivity.java | 233 +++++++++++++------- .../org/drrickorang/loopback/SettingsActivity.java | 1 - .../org/drrickorang/loopback/WavePlotView.java | 242 +++++++++------------ LoopbackApp/app/src/main/res/values/strings.xml | 2 +- 8 files changed, 384 insertions(+), 233 deletions(-) (limited to 'LoopbackApp/app/src/main') diff --git a/LoopbackApp/app/src/main/AndroidManifest.xml b/LoopbackApp/app/src/main/AndroidManifest.xml index 1608a12..fbe8ba6 100644 --- a/LoopbackApp/app/src/main/AndroidManifest.xml +++ b/LoopbackApp/app/src/main/AndroidManifest.xml @@ -23,14 +23,15 @@ xmlns:android="http://schemas.android.com/apk/res/android" package="org.drrickorang.loopback" - android:versionCode="12" - android:versionName="0.9.7"> + android:versionCode="14" + android:versionName="0.9.71"> + @@ -53,7 +53,6 @@ { +public class BufferCallbackTimes implements Iterable, Parcelable { private final int[] mTimeStamps; private final short[] mCallbackDurations; private final short mExpectedBufferPeriod; @@ -116,6 +120,42 @@ public class BufferCallbackTimes implements Iterable CREATOR + = new Parcelable.Creator() { + public BufferCallbackTimes createFromParcel(Parcel in) { + return new BufferCallbackTimes(in); + } + + public BufferCallbackTimes[] newArray(int size) { + return new BufferCallbackTimes[size]; + } + }; + /** Wrapper for iteration over timestamp and length pairs */ public class BufferCallback { public final int timeStamp; diff --git a/LoopbackApp/app/src/main/java/org/drrickorang/loopback/BufferPeriod.java b/LoopbackApp/app/src/main/java/org/drrickorang/loopback/BufferPeriod.java index 7716d9f..b4cd87e 100644 --- a/LoopbackApp/app/src/main/java/org/drrickorang/loopback/BufferPeriod.java +++ b/LoopbackApp/app/src/main/java/org/drrickorang/loopback/BufferPeriod.java @@ -18,6 +18,9 @@ package org.drrickorang.loopback; import java.util.Arrays; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; import android.util.Log; @@ -27,7 +30,7 @@ import android.util.Log; */ //TODO for native mode, should use a scale more accurate than the current 1ms -public class BufferPeriod { +public class BufferPeriod implements Parcelable { private static final String TAG = "BufferPeriod"; private long mStartTimeNs = 0; // first time collectBufferPeriod() is called @@ -42,6 +45,10 @@ public class BufferPeriod { private BufferCallbackTimes mCallbackTimes; private CaptureHolder mCaptureHolder; + public BufferPeriod() { + // Default constructor for when no data will be restored + } + /** * For player, this function is called before every AudioTrack.write(). * For recorder, this function is called after every AudioRecord.read() with read > 0. @@ -117,6 +124,40 @@ public class BufferPeriod { return mCallbackTimes; } + @Override + public int describeContents() { + return 0; + } + + // Only save values which represent the results. Any ongoing timing would not give useful + // results after a save/restore. + @Override + public void writeToParcel(Parcel dest, int flags) { + Bundle out = new Bundle(); + out.putInt("mMaxBufferPeriod", mMaxBufferPeriod); + out.putIntArray("mBufferPeriod", mBufferPeriod); + out.putParcelable("mCallbackTimes", mCallbackTimes); + dest.writeBundle(out); + } + + private BufferPeriod(Parcel source) { + Bundle in = source.readBundle(getClass().getClassLoader()); + mMaxBufferPeriod = in.getInt("mMaxBufferPeriod"); + mBufferPeriod = in.getIntArray("mBufferPeriod"); + mCallbackTimes = in.getParcelable("mCallbackTimes"); + } + + public static final Parcelable.Creator CREATOR + = new Parcelable.Creator() { + public BufferPeriod createFromParcel(Parcel in) { + return new BufferPeriod(in); + } + + public BufferPeriod[] newArray(int size) { + return new BufferPeriod[size]; + } + }; + private static void log(String msg) { Log.v(TAG, msg); } 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 1094fb0..d12cd7b 100644 --- a/LoopbackApp/app/src/main/java/org/drrickorang/loopback/Correlation.java +++ b/LoopbackApp/app/src/main/java/org/drrickorang/loopback/Correlation.java @@ -16,6 +16,9 @@ package org.drrickorang.loopback; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; import android.util.Log; @@ -23,7 +26,7 @@ import android.util.Log; * This class is used to automatically estimate latency and its confidence. */ -public class Correlation { +public class Correlation implements Parcelable { private static final String TAG = "Correlation"; private int mBlockSize = 4096; @@ -39,6 +42,9 @@ public class Correlation { private boolean mDataIsValid = false; // Used to mark computed latency information is available + public Correlation() { + // Default constructor for when no data will be restored + } public void init(int blockSize, int samplingRate) { mBlockSize = blockSize; @@ -179,6 +185,45 @@ public class Correlation { return status; } + @Override + public int describeContents() { + return 0; + } + + // Store the results before this object is destroyed + @Override + public void writeToParcel(Parcel dest, int flags) { + Bundle bundle = new Bundle(); + bundle.putBoolean("mDataIsValid", mDataIsValid); + if(mDataIsValid) { + bundle.putDouble("mEstimatedLatencySamples", mEstimatedLatencySamples); + bundle.putDouble("mEstimatedLatencyMs", mEstimatedLatencyMs); + bundle.putDouble("mEstimatedLatencyConfidence", mEstimatedLatencyConfidence); + } + dest.writeBundle(bundle); + } + + // Restore the results which were previously calculated + private Correlation(Parcel in) { + Bundle bundle = in.readBundle(getClass().getClassLoader()); + mDataIsValid = bundle.getBoolean("mDataIsValid"); + if(mDataIsValid) { + mEstimatedLatencySamples = bundle.getDouble("mEstimatedLatencySamples"); + mEstimatedLatencyMs = bundle.getDouble("mEstimatedLatencyMs"); + mEstimatedLatencyConfidence = bundle.getDouble("mEstimatedLatencyConfidence"); + } + } + + public static final Parcelable.Creator CREATOR + = new Parcelable.Creator() { + public Correlation createFromParcel(Parcel in) { + return new Correlation(in); + } + + public Correlation[] newArray(int size) { + return new Correlation[size]; + } + }; private static void log(String msg) { Log.v(TAG, msg); 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 c0f4bcb..3ae11d0 100644 --- a/LoopbackApp/app/src/main/java/org/drrickorang/loopback/LoopbackActivity.java +++ b/LoopbackApp/app/src/main/java/org/drrickorang/loopback/LoopbackActivity.java @@ -150,8 +150,10 @@ public class LoopbackActivity extends Activity // Note: these values should only be assigned in restartAudioSystem() private int mAudioThreadType = Constant.UNKNOWN; + private int mMicSource; private int mSamplingRate; private int mChannelIndex; + private int mSoundLevel; private int mPlayerBufferSizeInBytes; private int mRecorderBufferSizeInBytes; @@ -162,6 +164,8 @@ public class LoopbackActivity extends Activity private int mFFTOverlapSamples; private long mBufferTestStartTime; private int mBufferTestElapsedSeconds; + private int mBufferTestDurationInSeconds; + private int mBufferTestWavePlotDurationInSeconds; // threads that load CPUs private LoadThread[] mLoadThreads; @@ -459,7 +463,7 @@ public class LoopbackActivity extends Activity AudioManager am = (AudioManager) getSystemService(Context.AUDIO_SERVICE); am.setStreamVolume(AudioManager.STREAM_MUSIC, progress, 0); - refreshState(); + refreshSoundLevelBar(); log("Changed stream volume to: " + progress); } }); @@ -469,12 +473,15 @@ public class LoopbackActivity extends Activity mTextViewCurrentLevel.setTextSize(15); mTextViewResultSummary = (TextView) findViewById(R.id.resultSummary); - refreshState(); + refreshSoundLevelBar(); + + if(savedInstanceState != null) { + restoreInstanceState(savedInstanceState); + } applyIntent(getIntent()); } - @Override protected void onStart() { super.onStart(); @@ -501,7 +508,6 @@ public class LoopbackActivity extends Activity } } - @Override public void onNewIntent(Intent intent) { log("On New Intent called!"); @@ -700,11 +706,6 @@ public class LoopbackActivity extends Activity 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); @@ -747,9 +748,11 @@ public class LoopbackActivity extends Activity mRecorderBufferSizeInBytes = getApp().getRecorderBufferSizeInBytes(); mTestStartTimeString = (String) DateFormat.format("MMddkkmmss", System.currentTimeMillis()); - int micSource = getApp().getMicSource(); - int bufferTestDurationInSeconds = getApp().getBufferTestDuration(); - int bufferTestWavePlotDurationInSeconds = getApp().getBufferTestWavePlotDuration(); + mMicSource = getApp().getMicSource(); + AudioManager am = (AudioManager) getSystemService(Context.AUDIO_SERVICE); + mSoundLevel = am.getStreamVolume(AudioManager.STREAM_MUSIC); + mBufferTestDurationInSeconds = getApp().getBufferTestDuration(); + mBufferTestWavePlotDurationInSeconds = getApp().getBufferTestWavePlotDuration(); CaptureHolder captureHolder = new CaptureHolder(getApp().getNumStateCaptures(), getFileNamePrefix(), getApp().isCaptureWavSnippetsEnabled(), @@ -762,38 +765,38 @@ public class LoopbackActivity extends Activity int micSourceMapped; switch (mAudioThreadType) { case Constant.AUDIO_THREAD_TYPE_JAVA: - micSourceMapped = getApp().mapMicSource(Constant.AUDIO_THREAD_TYPE_JAVA, micSource); + micSourceMapped = getApp().mapMicSource(Constant.AUDIO_THREAD_TYPE_JAVA, mMicSource); int expectedRecorderBufferPeriod = Math.round( (float) (mRecorderBufferSizeInBytes * Constant.MILLIS_PER_SECOND) / (Constant.BYTES_PER_FRAME * mSamplingRate)); mRecorderBufferPeriod.prepareMemberObjects( - Constant.MAX_RECORDED_LATE_CALLBACKS_PER_SECOND * bufferTestDurationInSeconds, + Constant.MAX_RECORDED_LATE_CALLBACKS_PER_SECOND * mBufferTestDurationInSeconds, expectedRecorderBufferPeriod, captureHolder); int expectedPlayerBufferPeriod = Math.round( (float) (mPlayerBufferSizeInBytes * Constant.MILLIS_PER_SECOND) / (Constant.BYTES_PER_FRAME * mSamplingRate)); mPlayerBufferPeriod.prepareMemberObjects( - Constant.MAX_RECORDED_LATE_CALLBACKS_PER_SECOND * bufferTestDurationInSeconds, + Constant.MAX_RECORDED_LATE_CALLBACKS_PER_SECOND * mBufferTestDurationInSeconds, expectedPlayerBufferPeriod, captureHolder); mAudioThread = new LoopbackAudioThread(mSamplingRate, mPlayerBufferSizeInBytes, mRecorderBufferSizeInBytes, micSourceMapped, mRecorderBufferPeriod, - mPlayerBufferPeriod, mTestType, bufferTestDurationInSeconds, - bufferTestWavePlotDurationInSeconds, getApplicationContext(), + mPlayerBufferPeriod, mTestType, mBufferTestDurationInSeconds, + mBufferTestWavePlotDurationInSeconds, getApplicationContext(), mChannelIndex, captureHolder); mAudioThread.setMessageHandler(mMessageHandler); mAudioThread.mSessionId = sessionId; mAudioThread.start(); break; case Constant.AUDIO_THREAD_TYPE_NATIVE: - micSourceMapped = getApp().mapMicSource(Constant.AUDIO_THREAD_TYPE_NATIVE, micSource); + micSourceMapped = getApp().mapMicSource(Constant.AUDIO_THREAD_TYPE_NATIVE, mMicSource); // Note: mRecorderBufferSizeInBytes will not actually be used, since recorder buffer // size = player buffer size in native mode mNativeAudioThread = new NativeAudioThread(mSamplingRate, mPlayerBufferSizeInBytes, mRecorderBufferSizeInBytes, micSourceMapped, mTestType, - bufferTestDurationInSeconds, bufferTestWavePlotDurationInSeconds, + mBufferTestDurationInSeconds, mBufferTestWavePlotDurationInSeconds, captureHolder); mNativeAudioThread.setMessageHandler(mMessageHandler); mNativeAudioThread.mSessionId = sessionId; @@ -803,7 +806,6 @@ public class LoopbackActivity extends Activity startLoadThreads(); - mWavePlotView.setSamplingRate(mSamplingRate); refreshState(); } @@ -893,7 +895,7 @@ public class LoopbackActivity extends Activity /** Start the latency test. */ - public void onButtonLatencyTest(View view) throws InterruptedException{ + public void onButtonLatencyTest(View view) throws InterruptedException { if (isBusy()) { stopTests(); return; @@ -1265,11 +1267,6 @@ public class LoopbackActivity extends Activity case SETTINGS_ACTIVITY_REQUEST: log("return from new settings!"); - // here we wipe out all previous results, in order to avoid the condition where - // previous results does not match the new settings - resetResults(); - refreshState(); - refreshPlots(); break; } } @@ -1285,6 +1282,9 @@ public class LoopbackActivity extends Activity AudioManager am = (AudioManager) getSystemService(Context.AUDIO_SERVICE); int currentVolume = am.getStreamVolume(AudioManager.STREAM_MUSIC); mBarMasterLevel.setProgress(currentVolume); + + mTextViewCurrentLevel.setText(String.format("Current Sound Level: %d/%d", currentVolume, + mBarMasterLevel.getMax())); } @@ -1490,78 +1490,72 @@ public class LoopbackActivity extends Activity /** Redraw the plot according to mWaveData */ void refreshPlots() { - mWavePlotView.setData(mWaveData); + mWavePlotView.setData(mWaveData, mSamplingRate); mWavePlotView.redraw(); } - /** Refresh the text on the main activity that shows the app states and audio settings. */ void refreshState() { log("refreshState!"); - - //get current audio level - AudioManager am = (AudioManager) getSystemService(Context.AUDIO_SERVICE); - int currentVolume = am.getStreamVolume(AudioManager.STREAM_MUSIC); - mBarMasterLevel.setProgress(currentVolume); - - mTextViewCurrentLevel.setText(String.format("Sound Level: %d/%d", currentVolume, - mBarMasterLevel.getMax())); - - log("refreshState 2b"); + refreshSoundLevelBar(); // 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; + int playerFrames = mPlayerBufferSizeInBytes / Constant.BYTES_PER_FRAME; + int recorderFrames = mRecorderBufferSizeInBytes / Constant.BYTES_PER_FRAME; StringBuilder s = new StringBuilder(200); - s.append("SR: " + samplingRate + " Hz"); - s.append(" ChannelIndex: " + channelIndex); - int audioThreadType = getApp().getAudioThreadType(); - switch (audioThreadType) { + + s.append("Settings from most recent run (at "); + s.append(mTestStartTimeString); + s.append("):\n"); + + s.append("SR: " + mSamplingRate + " Hz"); + s.append(" ChannelIndex: " + mChannelIndex); + switch (mAudioThreadType) { case Constant.AUDIO_THREAD_TYPE_JAVA: - s.append(" Play Frames: " + playerBuffer); - s.append(" Record Frames: " + recorderBuffer); + s.append(" Play Frames: " + playerFrames); + s.append(" Record Frames: " + recorderFrames); s.append(" Audio: JAVA"); break; case Constant.AUDIO_THREAD_TYPE_NATIVE: - s.append(" Frames: " + playerBuffer); + s.append(" Frames: " + playerFrames); s.append(" Audio: NATIVE"); break; } // mic source - int micSource = getApp().getMicSource(); - String micSourceName = getApp().getMicSourceString(micSource); + String micSourceName = getApp().getMicSourceString(mMicSource); if (micSourceName != null) { s.append(String.format(" Mic: %s", micSourceName)); } + // sound level at start of test + s.append(String.format(" Sound Level: %d/%d", mSoundLevel, + mBarMasterLevel.getMax())); + String info = getApp().getSystemInfo(); s.append(" " + info); - // show buffer test duration - int bufferTestDuration = getApp().getBufferTestDuration(); - s.append("\nBuffer Test Duration: " + bufferTestDuration + "s"); - - // show buffer test wave plot duration - 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()); 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)); + mTextViewResultSummary.setText(String.format("Latency: %.2f ms Confidence: %.2f", + mCorrelation.mEstimatedLatencyMs, mCorrelation.mEstimatedLatencyConfidence)); } else if (mTestType == Constant.LOOPBACK_PLUG_AUDIO_THREAD_TEST_TYPE_BUFFER_PERIOD && mGlitchesData != null) { + // show buffer test duration + s.append("\nBuffer Test Duration: " + mBufferTestDurationInSeconds + " s"); + + // show buffer test wave plot duration + s.append(" Buffer Test Wave Plot Duration: last " + + mBufferTestWavePlotDurationInSeconds + " s"); + mTextViewResultSummary.setText(getResources().getString(R.string.numGlitches) + " " + estimateNumberOfGlitches(mGlitchesData)); } else { mTextViewResultSummary.setText(""); } + + mTextInfo.setText(s.toString()); } @@ -1782,22 +1776,21 @@ public class LoopbackActivity extends Activity } - private StringBuilder getReport(){ + private StringBuilder getReport() { String endline = "\n"; final int stringLength = 300; StringBuilder sb = new StringBuilder(stringLength); sb.append("DateTime = " + mTestStartTimeString + endline); - sb.append(INTENT_SAMPLING_FREQUENCY + " = " + getApp().getSamplingRate() + endline); - sb.append(INTENT_RECORDER_BUFFER + " = " + getApp().getRecorderBufferSizeInBytes() / + sb.append(INTENT_SAMPLING_FREQUENCY + " = " + mSamplingRate + endline); + sb.append(INTENT_CHANNEL_INDEX + " = " + mChannelIndex + endline); + sb.append(INTENT_RECORDER_BUFFER + " = " + mRecorderBufferSizeInBytes / 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(); - + sb.append(INTENT_PLAYER_BUFFER + " = " + mPlayerBufferSizeInBytes / + Constant.BYTES_PER_FRAME + endline); + sb.append(INTENT_AUDIO_THREAD + " = " + mAudioThreadType + endline); String audioType = "unknown"; - switch (getApp().getAudioThreadType()) { + switch (mAudioThreadType) { case Constant.AUDIO_THREAD_TYPE_JAVA: audioType = "JAVA"; break; @@ -1807,13 +1800,10 @@ public class LoopbackActivity extends Activity } sb.append(INTENT_AUDIO_THREAD + "_String = " + audioType + endline); - sb.append(INTENT_MIC_SOURCE + " = " + micSource + endline); - sb.append(INTENT_MIC_SOURCE + "_String = " + getApp().getMicSourceString(micSource) + sb.append(INTENT_MIC_SOURCE + " = " + mMicSource + endline); + sb.append(INTENT_MIC_SOURCE + "_String = " + getApp().getMicSourceString(mMicSource) + endline); - AudioManager am = (AudioManager) getSystemService(Context.AUDIO_SERVICE); - - int currentVolume = am.getStreamVolume(AudioManager.STREAM_MUSIC); - sb.append(INTENT_AUDIO_LEVEL + " = " + currentVolume + endline); + sb.append(INTENT_AUDIO_LEVEL + " = " + mSoundLevel + endline); switch (mTestType) { case Constant.LOOPBACK_PLUG_AUDIO_THREAD_TEST_TYPE_LATENCY: @@ -1828,7 +1818,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) = " + mBufferTestElapsedSeconds + endline); + sb.append("Buffer Test Duration (s) = " + mBufferTestDurationInSeconds + endline); // report recorder results int[] recorderBufferData = null; @@ -2114,4 +2104,89 @@ public class LoopbackActivity extends Activity SaveFilesWithDialog(); } } + + private void restoreInstanceState(Bundle in) { + mWaveData = in.getDoubleArray("mWaveData"); + + mTestType = in.getInt("mTestType"); + mMicSource = in.getInt("mMicSource"); + mAudioThreadType = in.getInt("mAudioThreadType"); + mSamplingRate = in.getInt("mSamplingRate"); + mChannelIndex = in.getInt("mChannelIndex"); + mSoundLevel = in.getInt("mSoundLevel"); + mPlayerBufferSizeInBytes = in.getInt("mPlayerBufferSizeInBytes"); + mRecorderBufferSizeInBytes = in.getInt("mRecorderBufferSizeInBytes"); + + mTestStartTimeString = in.getString("mTestStartTimeString"); + + mGlitchesData = in.getIntArray("mGlitchesData"); + if(mGlitchesData != null) { + mGlitchingIntervalTooLong = in.getBoolean("mGlitchingIntervalTooLong"); + mFFTSamplingSize = in.getInt("mFFTSamplingSize"); + mFFTOverlapSamples = in.getInt("mFFTOverlapSamples"); + mBufferTestStartTime = in.getLong("mBufferTestStartTime"); + mBufferTestElapsedSeconds = in.getInt("mBufferTestElapsedSeconds"); + mBufferTestDurationInSeconds = in.getInt("mBufferTestDurationInSeconds"); + mBufferTestWavePlotDurationInSeconds = + in.getInt("mBufferTestWavePlotDurationInSeconds"); + + findViewById(R.id.glitchReportPanel).setVisibility(View.VISIBLE); + } + + if(mWaveData != null) { + mCorrelation = in.getParcelable("mCorrelation"); + mPlayerBufferPeriod = in.getParcelable("mPlayerBufferPeriod"); + mRecorderBufferPeriod = in.getParcelable("mRecorderBufferPeriod"); + mPlayerCallbackTimes = in.getParcelable("mPlayerCallbackTimes"); + mRecorderCallbackTimes = in.getParcelable("mRecorderCallbackTimes"); + + mNativePlayerBufferPeriodArray = in.getIntArray("mNativePlayerBufferPeriodArray"); + mNativePlayerMaxBufferPeriod = in.getInt("mNativePlayerMaxBufferPeriod"); + mNativeRecorderBufferPeriodArray = in.getIntArray("mNativeRecorderBufferPeriodArray"); + mNativeRecorderMaxBufferPeriod = in.getInt("mNativeRecorderMaxBufferPeriod"); + + mWavePlotView.setData(mWaveData, mSamplingRate); + refreshState(); + findViewById(R.id.zoomAndSaveControlPanel).setVisibility(View.VISIBLE); + findViewById(R.id.resultSummary).setVisibility(View.VISIBLE); + } + } + + @Override + protected void onSaveInstanceState(Bundle out) { + super.onSaveInstanceState(out); + // TODO: keep larger pieces of data in a fragment to speed up response to rotation + out.putDoubleArray("mWaveData", mWaveData); + + out.putInt("mTestType", mTestType); + out.putInt("mMicSource", mMicSource); + out.putInt("mAudioThreadType", mAudioThreadType); + out.putInt("mSamplingRate", mSamplingRate); + out.putInt("mChannelIndex", mChannelIndex); + out.putInt("mSoundLevel", mSoundLevel); + out.putInt("mPlayerBufferSizeInBytes", mPlayerBufferSizeInBytes); + out.putInt("mRecorderBufferSizeInBytes", mRecorderBufferSizeInBytes); + out.putString("mTestStartTimeString", mTestStartTimeString); + + out.putParcelable("mCorrelation", mCorrelation); + out.putParcelable("mPlayerBufferPeriod", mPlayerBufferPeriod); + out.putParcelable("mRecorderBufferPeriod", mRecorderBufferPeriod); + out.putParcelable("mPlayerCallbackTimes", mPlayerCallbackTimes); + out.putParcelable("mRecorderCallbackTimes", mRecorderCallbackTimes); + + out.putIntArray("mNativePlayerBufferPeriodArray", mNativePlayerBufferPeriodArray); + out.putInt("mNativePlayerMaxBufferPeriod", mNativePlayerMaxBufferPeriod); + out.putIntArray("mNativeRecorderBufferPeriodArray", mNativeRecorderBufferPeriodArray); + out.putInt("mNativeRecorderMaxBufferPeriod", mNativeRecorderMaxBufferPeriod); + + // buffer test values + out.putIntArray("mGlitchesData", mGlitchesData); + out.putBoolean("mGlitchingIntervalTooLong", mGlitchingIntervalTooLong); + out.putInt("mFFTSamplingSize", mFFTSamplingSize); + out.putInt("mFFTOverlapSamples", mFFTOverlapSamples); + out.putLong("mBufferTestStartTime", mBufferTestStartTime); + out.putInt("mBufferTestElapsedSeconds", mBufferTestElapsedSeconds); + out.putInt("mBufferTestDurationInSeconds", mBufferTestDurationInSeconds); + out.putInt("mBufferTestWavePlotDurationInSeconds", mBufferTestWavePlotDurationInSeconds); + } } 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 566dd18..8b5a2d7 100644 --- a/LoopbackApp/app/src/main/java/org/drrickorang/loopback/SettingsActivity.java +++ b/LoopbackApp/app/src/main/java/org/drrickorang/loopback/SettingsActivity.java @@ -299,7 +299,6 @@ public class SettingsActivity extends Activity implements OnItemSelectedListener case R.id.spinnerChannelIndex: int channelIndex = mSpinnerChannelIndex.getSelectedItemPosition() - 1; getApp().setChannelIndex(channelIndex); - getApp().computeDefaults(); setSettingsHaveChanged(); log("channelIndex:" + channelIndex); refresh(); diff --git a/LoopbackApp/app/src/main/java/org/drrickorang/loopback/WavePlotView.java b/LoopbackApp/app/src/main/java/org/drrickorang/loopback/WavePlotView.java index eaf068a..71b31c5 100644 --- a/LoopbackApp/app/src/main/java/org/drrickorang/loopback/WavePlotView.java +++ b/LoopbackApp/app/src/main/java/org/drrickorang/loopback/WavePlotView.java @@ -23,12 +23,15 @@ import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Path; import android.graphics.Paint.Style; +import android.os.Vibrator; import android.util.AttributeSet; import android.util.Log; import android.view.GestureDetector; import android.view.MotionEvent; import android.view.ScaleGestureDetector; import android.view.View; +import android.view.animation.LinearInterpolator; +import android.widget.Scroller; /** @@ -54,9 +57,11 @@ public class WavePlotView extends View { private GestureDetector mDetector; private ScaleGestureDetector mSGDetector; private MyScaleGestureListener mSGDListener; + private Scroller mScroller; private int mWidth; private int mHeight; + private boolean mHasDimensions; private Paint mMyPaint; private Paint mPaintZoomBox; @@ -66,12 +71,23 @@ public class WavePlotView extends View { private Paint mPaintGrid; private Paint mPaintGridText; + // Default values used when we don't have a valid waveform to display. + // This saves us having to add multiple special cases to handle null waveforms. + private int mDefaultSampleRate = 48000; // chosen because it is common in real world devices + private double[] mDefaultDataVector = new double[mDefaultSampleRate]; // 1 second of fake audio + public WavePlotView(Context context, AttributeSet attrs) { super(context, attrs); mSGDListener = new MyScaleGestureListener(); mDetector = new GestureDetector(context, new MyGestureListener()); mSGDetector = new ScaleGestureDetector(context, mSGDListener); + mScroller = new Scroller(context, new LinearInterpolator(), true); initPaints(); + + // Initialize the value array to 1s silence + mSamplingRate = mDefaultSampleRate; + mBigDataArray = new double[mSamplingRate]; + Arrays.fill(mDefaultDataVector, 0); } @@ -123,13 +139,6 @@ public class WavePlotView extends View { mPaintGridText.setTextSize(textSize); } - - /** Must call this function to set mSamplingRate before plotting the wave. */ - public void setSamplingRate(int samplingRate) { - mSamplingRate = samplingRate; - } - - public double getZoom() { return mZoomFactorX; } @@ -226,7 +235,9 @@ public class WavePlotView extends View { mWidth = w; mHeight = h; log("New w: " + mWidth + " h: " + mHeight); + mHasDimensions = true; initView(); + refreshView(); } @@ -236,12 +247,8 @@ public class WavePlotView extends View { mInsetSize = mWidth / 5; mValuesArray = new double[mArraySize]; mValuesArray2 = new double[mArraySize]; - int i; - - for (i = 0; i < mArraySize; i++) { - mValuesArray[i] = 0; - mValuesArray2[i] = 0; - } + Arrays.fill(mValuesArray, 0); + Arrays.fill(mValuesArray2, 0); //inset mInsetArray = new double[mInsetSize]; @@ -254,7 +261,6 @@ public class WavePlotView extends View { @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); - boolean showZoomBox = mSGDListener.mIsScaling; boolean showGrid = true; boolean showInset = true; @@ -338,33 +344,23 @@ public class WavePlotView extends View { myPath.moveTo(0, h / 2); //start if (mBigDataArray != null) { - for (i = 0; i < mArraySize; i++) { - double value = mValuesArray[i]; - double valueScaled = (valueMax - value) / valueRange; - float newX = i * deltaX; - float newY = (float) (valueScaled * h); - myPath.lineTo(newX, newY); - } - - //bottom - for (i = mArraySize - 1; i >= 0; i--) { - double value = mValuesArray2[i]; - double valueScaled = (valueMax - value) / valueRange; - float newX = i * deltaX; - float newY = (float) (valueScaled * h); - myPath.lineTo(newX, newY); + if (getZoom() >= 2) { + for (i = 0; i < mArraySize; ++i) { + float top = (float) ((valueMax - mValuesArray[i]) / valueRange) * h; + float bottom = (float) ((valueMax - mValuesArray2[i]) / valueRange) * h + 1; + float left = i * deltaX; + canvas.drawRect(left, top, left + deltaX, bottom, mMyPaint); + } + } else { + for (i = 0; i < (mArraySize - 1); ++i) { + float first = (float) ((valueMax - mValuesArray[i]) / valueRange) * h; + float second = (float) ((valueMax - mValuesArray[i + 1]) / valueRange) * h; + float left = i * deltaX; + canvas.drawLine(left, first, left + deltaX, second, mMyPaint); + } } - //close - myPath.close(); - canvas.drawPath(myPath, mMyPaint); - if (showZoomBox) { - float x1 = (float) mSGDListener.mX1; - float x2 = (float) mSGDListener.mX2; - canvas.drawRect(x1, 0, x2, h, mPaintZoomBox); - } - if (showInset) { float iW = (float) (w * 0.2); float iH = (float) (h * 0.2); @@ -376,31 +372,14 @@ public class WavePlotView extends View { //paintInset float iDeltaX = (float) iW / mInsetSize; - //top - Path iPath = new Path(); - iPath.moveTo(iX, iY + (iH / 2)); //start - - for (i = 0; i < mInsetSize; i++) { - double value = mInsetArray[i]; - double valueScaled = (valueMax - value) / valueRange; - float newX = iX + (i * iDeltaX); - float newY = iY + (float) (valueScaled * iH); - iPath.lineTo(newX, newY); - } - - //bottom - for (i = mInsetSize - 1; i >= 0; i--) { - double value = mInsetArray2[i]; - double valueScaled = (valueMax - value) / valueRange; - float newX = iX + i * iDeltaX; - float newY = iY + (float) (valueScaled * iH); - iPath.lineTo(newX, newY); + for (i = 0; i < mInsetSize; ++i) { + float top = iY + (float) ((valueMax - mInsetArray[i]) / valueRange) * iH; + float bottom = iY + + (float) ((valueMax - mInsetArray2[i]) / valueRange) * iH + 1; + float left = iX + i * iDeltaX; + canvas.drawRect(left, top, left + deltaX, bottom, mPaintInset); } - //close - iPath.close(); - canvas.drawPath(iPath, mPaintInset); - if (mBigDataArray != null) { //paint current region of zoom int offsetSamples = getOffset(); @@ -416,6 +395,10 @@ public class WavePlotView extends View { } } } + if (mScroller.computeScrollOffset()) { + setOffset(mScroller.getCurrX(), false); + refreshGraph(); + } } @@ -424,6 +407,13 @@ public class WavePlotView extends View { Arrays.fill(mValuesArray2, 0); } + void refreshView() { + double maxZoom = getMaxZoomOut(); + setZoom(maxZoom); + setOffset(0, false); + computeInset(); + refreshGraph(); + } void computeInset() { if (mBigDataArray != null) { @@ -529,21 +519,22 @@ public class WavePlotView extends View { } - void setData(double [] dataVector) { - mBigDataArray = dataVector; - double maxZoom = getMaxZoomOut(); - setZoom(maxZoom); - setOffset(0, false); - computeInset(); - refreshGraph(); - } + void setData(double[] dataVector, int sampleRate) { + if (sampleRate < 1) + throw new IllegalArgumentException("sampleRate must be a positive integer"); + + mSamplingRate = sampleRate; + mBigDataArray = (dataVector != null ? dataVector : mDefaultDataVector); + if (mHasDimensions) { // only refresh the view if it has been initialized already + refreshView(); + } + } void redraw() { invalidate(); } - @Override public boolean onTouchEvent(MotionEvent event) { mDetector.onTouchEvent(event); @@ -552,14 +543,17 @@ public class WavePlotView extends View { return true; } - class MyGestureListener extends GestureDetector.SimpleOnGestureListener { private static final String DEBUG_TAG = "MyGestureListener"; - + private boolean mInDrag = false; @Override public boolean onDown(MotionEvent event) { Log.d(DEBUG_TAG, "onDown: " + event.toString() + " " + TAG); + if(!mScroller.isFinished()) { + mScroller.forceFinished(true); + refreshGraph(); + } return true; } @@ -569,110 +563,68 @@ public class WavePlotView extends View { float velocityX, float velocityY) { Log.d(DEBUG_TAG, "onFling: VelocityX: " + velocityX + " velocityY: " + velocityY); - //velocityX positive left to right - // negative: right to left - //double offset = getZoom() - - double samplesPerWindow = mArraySize * getZoom(); - int maxPixelsPerWindow = 8000; - double offsetFactor = -(double) (velocityX / maxPixelsPerWindow); - double offset = (samplesPerWindow * offsetFactor / 3.0); - Log.d(DEBUG_TAG, " VELOCITY: " + velocityX + " samples/window = " + samplesPerWindow + - " offsetFactor = " + offsetFactor + " offset: " + offset); - - setOffset((int) offset, true); + mScroller.fling(mCurrentOffset, 0, + (int) (-velocityX * getZoom()), + 0, 0, mBigDataArray.length, 0, 0); refreshGraph(); return true; } + @Override + public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { + setOffset((int) (distanceX * getZoom()), true); + refreshGraph(); + return super.onScroll(e1, e2, distanceX, distanceY); + } + @Override public boolean onDoubleTap(MotionEvent event) { Log.d(DEBUG_TAG, "onDoubleTap: " + event.toString()); - setZoom(100000); - setOffset(0, false); + int tappedSample = (int) (event.getX() * getZoom()); + setZoom(getZoom() / 2); + setOffset(tappedSample / 2, true); + refreshGraph(); return true; } - } + @Override + public void onLongPress(MotionEvent e) { + Vibrator vibe = (Vibrator) getContext().getSystemService(Context.VIBRATOR_SERVICE); + if (vibe.hasVibrator()) { + vibe.vibrate(20); + } + setZoom(getMaxZoomOut()); + setOffset(0, false); + refreshGraph(); + } + } private class MyScaleGestureListener extends ScaleGestureDetector.SimpleOnScaleGestureListener { private static final String DEBUG_TAG = "MyScaleGestureListener"; - public boolean mIsScaling = false; - public double mX1 = 0; - public double mX2 = 0; + int focusSample = 0; @Override public boolean onScaleBegin(ScaleGestureDetector detector) { - mIsScaling = true; + focusSample = (int) (detector.getFocusX() * getZoom()) + mCurrentOffset; return super.onScaleBegin(detector); } - - @Override - public void onScaleEnd(ScaleGestureDetector detector) { - mIsScaling = false; - //now zoom - { - int w = getWidth(); - //int h = getHeight(); - - //double currentSpan = detector.getCurrentSpan(); - double currentSpanX = detector.getCurrentSpanX(); - //double currentSpanY = detector.getCurrentSpanY(); - double focusX = detector.getFocusX(); - //double focusY = detector.getFocusY(); - //double scaleFactor = detector.getScaleFactor(); - - //estimated X1, X2 - double x1 = focusX - (currentSpanX / 2); - double x2 = focusX + (currentSpanX / 2); - //double x1clip = x1 < 0 ? 0 : (x1 > w ? w : x1); - //double x2clip = x2 < 0 ? 0 : (x2 > w ? w : x2); - - //int originalOffset = getOffset(); - double windowSamplesOriginal = getWindowSamples(); //samples in current window - double currentZoom = getZoom(); - double windowFactor = Math.abs(mX2 - mX1) / w; - - double newZoom = currentZoom * windowFactor; - setZoom(newZoom); - int newOffset = (int) (windowSamplesOriginal * mX1 / w); - setOffset(newOffset, true); //relative - - } - refreshGraph(); - } - - @Override public boolean onScale(ScaleGestureDetector detector) { + setZoom(getZoom() / detector.getScaleFactor()); - int w = getWidth(); - //int h = getHeight(); - //double currentSpan = detector.getCurrentSpan(); - double currentSpanX = detector.getCurrentSpanX(); - //double currentSpanY = detector.getCurrentSpanY(); - double focusX = detector.getFocusX(); - //double focusY = detector.getFocusY(); - //double scaleFactor = detector.getScaleFactor(); - - //estimated X1, X2 - double x1 = focusX - (currentSpanX / 2); - double x2 = focusX + (currentSpanX / 2); - double x1clip = x1 < 0 ? 0 : (x1 > w ? w : x1); - double x2clip = x2 < 0 ? 0 : (x2 > w ? w : x2); - mX1 = x1clip; - mX2 = x2clip; + int newFocusSample = (int) (detector.getFocusX() * getZoom()) + mCurrentOffset; + int sampleDelta = (int) (focusSample - newFocusSample); + setOffset(sampleDelta, true); refreshGraph(); return true; } } - private static void log(String msg) { Log.v(TAG, msg); } diff --git a/LoopbackApp/app/src/main/res/values/strings.xml b/LoopbackApp/app/src/main/res/values/strings.xml index 49fe37d..c58469e 100644 --- a/LoopbackApp/app/src/main/res/values/strings.xml +++ b/LoopbackApp/app/src/main/res/values/strings.xml @@ -52,7 +52,7 @@ UNPROCESSED (N or later) - Info... + Test settings will appear here after the first test is run SETTINGS About Sampling Rate -- cgit v1.2.3