1 /*
2  * Copyright (C) 2015 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package org.drrickorang.loopback;
18 
19 import android.util.Log;
20 
21 import java.util.Arrays;
22 
23 
24 /**
25  * This thread is responsible for detecting glitches in the samples.
26  */
27 
28 public class GlitchDetectionThread extends Thread {
29     private static final String TAG = "GlitchDetectionThread";
30     // the acceptable difference between the expected center of mass and what we actually get
31     private static final double mAcceptablePercentDifference = 0.02; // change this if necessary
32 
33     // Measured in FFT samples
34     private static final int GLITCH_CONCENTRATION_WINDOW_SIZE = 1500; // approx 30 seconds at 48kHz
35     private static final int COOLDOWN_WINDOW = 4500; // approx 90 seconds at 48kHz
36 
37     private boolean mIsRunning; // condition must be true for the thread to run
38     private short   mShortBuffer[]; // keep the data read from Pipe
39     private int     mShortBufferIndex = 0;
40     private Pipe    mPipe;
41     private static int mThreadSleepDurationMs;
42 
43     private double  mDoubleBuffer[]; // keep the data used for FFT calculation
44     private boolean mIsFirstFFT = true; // whether or not it's the first FFT calculation
45 
46     private WaveDataRingBuffer mWaveDataRing; // Record last n seconds of wave data
47 
48     private final double  mFrequency1;
49     private final double  mFrequency2; //currently not used
50     private final int     mSamplingRate;
51     private final int     mFFTSamplingSize;   // amount of samples used to perform a FFT
52     private final int     mFFTOverlapSamples; // amount of overlapped samples used between two FFTs
53     private final int     mNewSamplesPerFFT;  // amount of new samples (not from last FFT) in a FFT
54     private double  mCenterOfMass;  // expected center of mass of samples
55 
56     private final int[]   mGlitches;  // for every value = n, n is nth FFT where a glitch is found
57     private int     mGlitchesIndex;
58     private int     mFFTCount; // store the current number of FFT performed
59     private FFT     mFFT;
60     private boolean mGlitchingIntervalTooLong = false; // true if mGlitches is full
61 
62     // Pre-Allocated buffers for glitch detection process
63     private final double[] mFFTResult;
64     private final double[] mCurrentSamples;
65     private final double[] mImagArray;
66 
67     // Used for captured SysTrace dumps
68     private CaptureHolder mCaptureHolder;
69     private int mLastGlitchCaptureAttempt = 0;
70 
GlitchDetectionThread(double frequency1, double frequency2, int samplingRate, int FFTSamplingSize, int FFTOverlapSamples, int bufferTestDurationInSeconds, int bufferTestWavePlotDurationInSeconds, Pipe pipe, CaptureHolder captureHolder)71     GlitchDetectionThread(double frequency1, double frequency2, int samplingRate,
72           int FFTSamplingSize, int FFTOverlapSamples, int bufferTestDurationInSeconds,
73           int bufferTestWavePlotDurationInSeconds, Pipe pipe, CaptureHolder captureHolder) {
74         mPipe = pipe;
75         mFrequency1 = frequency1;
76         mFrequency2 = frequency2;
77         mFFTSamplingSize = FFTSamplingSize;
78         mFFTOverlapSamples = FFTOverlapSamples;
79         mNewSamplesPerFFT = mFFTSamplingSize - mFFTOverlapSamples;
80         mSamplingRate = samplingRate;
81         mIsRunning = true;
82 
83         mShortBuffer = new short[mFFTSamplingSize];
84         mDoubleBuffer = new double[mFFTSamplingSize];
85         mWaveDataRing = new WaveDataRingBuffer(mSamplingRate * bufferTestWavePlotDurationInSeconds);
86 
87         final int acceptableGlitchingIntervalsPerSecond = 10;
88         mGlitches = new int[bufferTestDurationInSeconds * acceptableGlitchingIntervalsPerSecond];
89         mGlitchesIndex = 0;
90         mFFTCount = 0;
91 
92         mFFTResult = new double[mFFTSamplingSize/2];
93         mCurrentSamples = new double[mFFTSamplingSize];
94         mImagArray = new double[mFFTSamplingSize];
95 
96         mFFT = new FFT(mFFTSamplingSize);
97         computeExpectedCenterOfMass();
98 
99         setName("Loopback_GlitchDetection");
100 
101         mCaptureHolder = captureHolder;
102         mCaptureHolder.setWaveDataBuffer(mWaveDataRing);
103 
104         mThreadSleepDurationMs = FFTOverlapSamples * Constant.MILLIS_PER_SECOND / mSamplingRate;
105         if (mThreadSleepDurationMs < 1) {
106             mThreadSleepDurationMs = 1; // sleeps at least 1ms
107         }
108     }
109 
110 
run()111     public void run() {
112         while (mIsRunning) {
113             int requiredRead;
114             int actualRead;
115 
116             requiredRead = mFFTSamplingSize - mShortBufferIndex;
117             actualRead = mPipe.read(mShortBuffer, mShortBufferIndex, requiredRead);
118 
119             if (actualRead > 0) {
120                 mShortBufferIndex += actualRead;
121             }
122 
123             if (actualRead == Pipe.OVERRUN) {
124                 log("There's an overrun");
125             }
126 
127             // Once we have enough data, we can do a FFT on it. Note that between two FFTs, part of
128             // the samples (of size mFFTOverlapSamples) are used in both FFTs .
129             if (mShortBufferIndex == mFFTSamplingSize) {
130                 bufferShortToDouble(mShortBuffer, mDoubleBuffer);
131 
132                 // copy data in mDoubleBuffer to mWaveData
133                 if (mIsFirstFFT) {
134                     // if it's the first FFT, copy the whole "mNativeBuffer" to mWaveData
135                     mWaveDataRing.writeWaveData(mDoubleBuffer, 0, mFFTSamplingSize);
136                     mIsFirstFFT = false;
137                 } else {
138                     mWaveDataRing.writeWaveData(mDoubleBuffer, mFFTOverlapSamples,
139                             mNewSamplesPerFFT);
140                 }
141 
142                 detectGlitches();
143                 // move new samples to the beginning of the array as they will be reused in next fft
144                 System.arraycopy(mShortBuffer, mNewSamplesPerFFT, mShortBuffer,
145                                  0, mFFTOverlapSamples);
146                 mShortBufferIndex = mFFTOverlapSamples;
147             } else {
148                 try {
149                     sleep(mThreadSleepDurationMs);
150                 } catch (InterruptedException e) {
151                     e.printStackTrace();
152                 }
153             }
154         }
155 
156     }
157 
158 
159     /** convert samples in shortBuffer to double, then copy into doubleBuffer. */
160     // TODO move to audio_utils
bufferShortToDouble(short[] shortBuffer, double[] doubleBuffer)161     private void bufferShortToDouble(short[] shortBuffer, double[] doubleBuffer) {
162         double temp;
163         for (int i = 0; i < shortBuffer.length; i++) {
164             temp = (double) shortBuffer[i];
165             temp *= (1.0 / Short.MAX_VALUE);
166             doubleBuffer[i] = temp;
167         }
168     }
169 
170 
171     /** Should be called by other thread to stop this thread */
requestStop()172     public void requestStop() {
173         mIsRunning = false;
174         interrupt();
175     }
176 
177 
178     /**
179      * Use the data in mDoubleBuffer to do glitch detection since we know what
180      * data we are expecting.
181      */
detectGlitches()182     private void detectGlitches() {
183         double centerOfMass;
184 
185         // retrieve a copy of recorded wave data for manipulating and analyzing
186         System.arraycopy(mDoubleBuffer, 0, mCurrentSamples, 0, mDoubleBuffer.length);
187 
188         Utilities.hanningWindow(mCurrentSamples);
189 
190         double width = (double) mSamplingRate / mCurrentSamples.length;
191         computeFFT(mCurrentSamples, mFFTResult);     // gives an array of sampleSize / 2
192         final double threshold = 0.1;
193 
194         // for all elements in the FFT result that are smaller than threshold,
195         // eliminate them as they are probably noise
196         for (int j = 0; j < mFFTResult.length; j++) {
197             if (mFFTResult[j] < threshold) {
198                 mFFTResult[j] = 0;
199             }
200         }
201 
202         // calculate the center of mass of sample's FFT
203         centerOfMass = computeCenterOfMass(mFFTResult, width);
204         double difference = (Math.abs(centerOfMass - mCenterOfMass) / mCenterOfMass);
205         if (mGlitchesIndex >= mGlitches.length) {
206             // we just want to show this log once and set the flag once.
207             if (!mGlitchingIntervalTooLong) {
208                 log("Not enough room to store glitches!");
209                 mGlitchingIntervalTooLong = true;
210             }
211         } else {
212             // centerOfMass == -1 if the wave we get is silence.
213             if (difference > mAcceptablePercentDifference || centerOfMass == -1) {
214                 // Glitch Detected
215                 mGlitches[mGlitchesIndex] = mFFTCount;
216                 mGlitchesIndex++;
217                 if (mCaptureHolder.isCapturing()) {
218                     checkGlitchConcentration();
219                 }
220             }
221         }
222         mFFTCount++;
223     }
224 
checkGlitchConcentration()225     private void checkGlitchConcentration() {
226 
227         final int recordedGlitch = mGlitches[mGlitchesIndex-1];
228         if (recordedGlitch - mLastGlitchCaptureAttempt <= COOLDOWN_WINDOW) {
229             return;
230         }
231 
232         final int windowBegin = recordedGlitch - GLITCH_CONCENTRATION_WINDOW_SIZE;
233 
234         int numGlitches = 0;
235         for (int index = mGlitchesIndex-1; index >= 0 && mGlitches[index] >= windowBegin; --index) {
236             ++numGlitches;
237         }
238 
239         int captureResponse = mCaptureHolder.captureState(numGlitches);
240         if (captureResponse != CaptureHolder.NEW_CAPTURE_IS_LEAST_INTERESTING) {
241             mLastGlitchCaptureAttempt = recordedGlitch;
242         }
243 
244     }
245 
246     /** Compute the center of mass of fftResults. Width is the width of each beam. */
computeCenterOfMass(double[] fftResult, double width)247     private double computeCenterOfMass(double[] fftResult, double width) {
248         int length = fftResult.length;
249         double weightedSum = 0;
250         double totalWeight = 0;
251         for (int i = 0; i < length; i++) {
252             weightedSum += fftResult[i] * i;
253             totalWeight += fftResult[i];
254         }
255 
256         // this may happen since we are eliminating the noises. So if the wave we got is silence,
257         // totalWeight might == 0.
258         if (totalWeight == 0) {
259             return -1;
260         }
261 
262         return (weightedSum * width) / totalWeight;
263     }
264 
265 
266     /** Compute FFT of a set of data "samples". */
computeFFT(double[] src, double[] dst)267     private void computeFFT(double[] src, double[] dst) {
268         Arrays.fill(mImagArray, 0);
269         mFFT.fft(src, mImagArray, 1);    // here src array and imagArray get set
270 
271 
272         for (int i = 0; i < (src.length / 2); i++) {
273             dst[i] = Math.sqrt(src[i] * src[i] + mImagArray[i] * mImagArray[i]);
274         }
275 
276     }
277 
278 
279     /** Compute the center of mass if the samples have no glitches. */
computeExpectedCenterOfMass()280     private void computeExpectedCenterOfMass() {
281         SineWaveTone sineWaveTone = new SineWaveTone(mSamplingRate, mFrequency1);
282         double[] sineWave = new double[mFFTSamplingSize];
283         double centerOfMass;
284         double[] sineFFTResult = new double[mFFTSamplingSize/2];
285 
286         sineWaveTone.generateTone(sineWave, mFFTSamplingSize);
287         Utilities.hanningWindow(sineWave);
288         double width = (double) mSamplingRate / sineWave.length;
289 
290         computeFFT(sineWave, sineFFTResult);     // gives an array of sample sizes / 2
291         centerOfMass = computeCenterOfMass(sineFFTResult, width);  // return center of mass
292         mCenterOfMass = centerOfMass;
293         log("the expected center of mass:" + Double.toString(mCenterOfMass));
294     }
295 
296 
getWaveData()297     public double[] getWaveData() {
298         return mWaveDataRing.getWaveRecord();
299     }
300 
301 
getGlitchingIntervalTooLong()302     public boolean getGlitchingIntervalTooLong() {
303         return mGlitchingIntervalTooLong;
304     }
305 
306 
getGlitches()307     public int[] getGlitches() {
308         //return a copy of recorded glitches in an array sized to hold only recorded glitches
309         int[] output = new int[mGlitchesIndex];
310         System.arraycopy(mGlitches, 0, output, 0, mGlitchesIndex);
311         return output;
312     }
313 
314 
log(String msg)315     private static void log(String msg) {
316         Log.v(TAG, msg);
317     }
318 
319 }
320