blob: e52c1169a355acfdc337f912728aa120a9f8d77e [file] [log] [blame]
/*
* 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.util.Log;
import java.util.Arrays;
/**
* This thread is responsible for detecting glitches in the samples.
*/
public class GlitchDetectionThread extends Thread {
private static final String TAG = "GlitchDetectionThread";
// the acceptable difference between the expected center of mass and what we actually get
private static final double mAcceptablePercentDifference = 0.02; // change this if necessary
// Measured in FFT samples
private static final int GLITCH_CONCENTRATION_WINDOW_SIZE = 1500; // approx 30 seconds at 48kHz
private static final int COOLDOWN_WINDOW = 4500; // approx 90 seconds at 48kHz
private boolean mIsRunning; // condition must be true for the thread to run
private short mShortBuffer[]; // keep the data read from Pipe
private int mShortBufferIndex = 0;
private Pipe mPipe;
private static int mThreadSleepDurationMs;
private double mDoubleBuffer[]; // keep the data used for FFT calculation
private boolean mIsFirstFFT = true; // whether or not it's the first FFT calculation
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 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;
// Used for captured SysTrace dumps
private CaptureHolder mCaptureHolder;
private int mLastGlitchCaptureAttempt = 0;
GlitchDetectionThread(double frequency1, double frequency2, int samplingRate,
int FFTSamplingSize, int FFTOverlapSamples, int bufferTestDurationInSeconds,
int bufferTestWavePlotDurationInSeconds, Pipe pipe, CaptureHolder captureHolder) {
mPipe = pipe;
mFrequency1 = frequency1;
mFrequency2 = frequency2;
mFFTSamplingSize = FFTSamplingSize;
mFFTOverlapSamples = FFTOverlapSamples;
mNewSamplesPerFFT = mFFTSamplingSize - mFFTOverlapSamples;
mSamplingRate = samplingRate;
mIsRunning = true;
mShortBuffer = new short[mFFTSamplingSize];
mDoubleBuffer = new double[mFFTSamplingSize];
mWaveDataRing = new WaveDataRingBuffer(mSamplingRate * bufferTestWavePlotDurationInSeconds);
final int acceptableGlitchingIntervalsPerSecond = 10;
mGlitches = new int[bufferTestDurationInSeconds * acceptableGlitchingIntervalsPerSecond];
mGlitchesIndex = 0;
mFFTCount = 0;
mFFTResult = new double[mFFTSamplingSize/2];
mCurrentSamples = new double[mFFTSamplingSize];
mImagArray = new double[mFFTSamplingSize];
mFFT = new FFT(mFFTSamplingSize);
computeExpectedCenterOfMass();
setName("Loopback_GlitchDetection");
mCaptureHolder = captureHolder;
mCaptureHolder.setWaveDataBuffer(mWaveDataRing);
mThreadSleepDurationMs = FFTOverlapSamples * Constant.MILLIS_PER_SECOND / mSamplingRate;
if (mThreadSleepDurationMs < 1) {
mThreadSleepDurationMs = 1; // sleeps at least 1ms
}
}
public void run() {
while (mIsRunning) {
int requiredRead;
int actualRead;
requiredRead = mFFTSamplingSize - mShortBufferIndex;
actualRead = mPipe.read(mShortBuffer, mShortBufferIndex, requiredRead);
if (actualRead > 0) {
mShortBufferIndex += actualRead;
}
if (actualRead == Pipe.OVERRUN) {
log("There's an overrun");
}
// Once we have enough data, we can do a FFT on it. Note that between two FFTs, part of
// the samples (of size mFFTOverlapSamples) are used in both FFTs .
if (mShortBufferIndex == mFFTSamplingSize) {
bufferShortToDouble(mShortBuffer, mDoubleBuffer);
// copy data in mDoubleBuffer to mWaveData
if (mIsFirstFFT) {
// if it's the first FFT, copy the whole "mNativeBuffer" to mWaveData
mWaveDataRing.writeWaveData(mDoubleBuffer, 0, mFFTSamplingSize);
mIsFirstFFT = false;
} else {
mWaveDataRing.writeWaveData(mDoubleBuffer, mFFTOverlapSamples,
mNewSamplesPerFFT);
}
detectGlitches();
// move new samples to the beginning of the array as they will be reused in next fft
System.arraycopy(mShortBuffer, mNewSamplesPerFFT, mShortBuffer,
0, mFFTOverlapSamples);
mShortBufferIndex = mFFTOverlapSamples;
} else {
try {
sleep(mThreadSleepDurationMs);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
/** convert samples in shortBuffer to double, then copy into doubleBuffer. */
private void bufferShortToDouble(short[] shortBuffer, double[] doubleBuffer) {
double temp;
for (int i = 0; i < shortBuffer.length; i++) {
temp = (double) shortBuffer[i];
temp *= (1.0 / Short.MAX_VALUE);
doubleBuffer[i] = temp;
}
}
/** Should be called by other thread to stop this thread */
public void requestStop() {
mIsRunning = false;
interrupt();
}
/**
* Use the data in mDoubleBuffer to do glitch detection since we know what
* data we are expecting.
*/
private void detectGlitches() {
double centerOfMass;
// 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 < mFFTResult.length; j++) {
if (mFFTResult[j] < threshold) {
mFFTResult[j] = 0;
}
}
// calculate the center of mass of sample's FFT
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.
if (!mGlitchingIntervalTooLong) {
log("Not enough room to store glitches!");
mGlitchingIntervalTooLong = true;
}
} else {
// centerOfMass == -1 if the wave we get is silence.
if (difference > mAcceptablePercentDifference || centerOfMass == -1) {
// Glitch Detected
mGlitches[mGlitchesIndex] = mFFTCount;
mGlitchesIndex++;
if (mCaptureHolder.isCapturing()) {
checkGlitchConcentration();
}
}
}
mFFTCount++;
}
private void checkGlitchConcentration(){
final int recordedGlitch = mGlitches[mGlitchesIndex-1];
if (recordedGlitch - mLastGlitchCaptureAttempt <= COOLDOWN_WINDOW){
return;
}
final int windowBegin = recordedGlitch - GLITCH_CONCENTRATION_WINDOW_SIZE;
int numGlitches = 0;
for (int index = mGlitchesIndex-1; index >= 0 && mGlitches[index] >= windowBegin; --index){
++numGlitches;
}
int captureResponse = mCaptureHolder.captureState(numGlitches);
if (captureResponse != CaptureHolder.NEW_CAPTURE_IS_LEAST_INTERESTING){
mLastGlitchCaptureAttempt = recordedGlitch;
}
}
/** Compute the center of mass of fftResults. Width is the width of each beam. */
private double computeCenterOfMass(double[] fftResult, double width) {
int length = fftResult.length;
double weightedSum = 0;
double totalWeight = 0;
for (int i = 0; i < length; i++) {
weightedSum += fftResult[i] * i;
totalWeight += fftResult[i];
}
// this may happen since we are eliminating the noises. So if the wave we got is silence,
// totalWeight might == 0.
if (totalWeight == 0) {
return -1;
}
return (weightedSum * width) / totalWeight;
}
/** Compute FFT of a set of data "samples". */
private void computeFFT(double[] src, double[] dst) {
Arrays.fill(mImagArray, 0);
mFFT.fft(src, mImagArray, 1); // here src array and imagArray get set
for (int i = 0; i < (src.length / 2); i++) {
dst[i] = Math.sqrt(src[i] * src[i] + mImagArray[i] * mImagArray[i]);
}
}
/** Compute the center of mass if the samples have no glitches. */
private void computeExpectedCenterOfMass() {
SineWaveTone sineWaveTone = new SineWaveTone(mSamplingRate, mFrequency1);
double[] sineWave = new double[mFFTSamplingSize];
double centerOfMass;
double[] sineFFTResult = new double[mFFTSamplingSize/2];
sineWaveTone.generateTone(sineWave, mFFTSamplingSize);
Utilities.hanningWindow(sineWave);
double width = (double) mSamplingRate / sineWave.length;
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));
}
public double[] getWaveData() {
return mWaveDataRing.getWaveRecord();
}
public boolean getGlitchingIntervalTooLong() {
return mGlitchingIntervalTooLong;
}
public int[] getGlitches() {
//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;
}
private static void log(String msg) {
Log.v(TAG, msg);
}
}