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.content.Context;
20 import android.media.AudioFormat;
21 import android.media.AudioManager;
22 import android.media.AudioRecord;
23 import android.os.Build;
24 import android.util.Log;
25 
26 /**
27  * This thread records incoming sound samples (uses AudioRecord).
28  */
29 
30 public class RecorderRunnable implements Runnable {
31     private static final String TAG = "RecorderRunnable";
32 
33     private AudioRecord         mRecorder;
34     private boolean             mIsRunning;
35     private boolean             mIsRecording = false;
36     private static final Object sRecordingLock = new Object();
37 
38     private final LoopbackAudioThread mAudioThread;
39     // This is the pipe that connects the player and the recorder in latency test.
40     private final PipeShort           mLatencyTestPipeShort;
41     // This is the pipe that is used in buffer test to send data to GlitchDetectionThread
42     private PipeShort                 mBufferTestPipeShort;
43 
44     private boolean   mIsRequestStop = false;
45     private final int mTestType;    // latency test or buffer test
46     private final int mSelectedRecordSource;
47     private final int mSamplingRate;
48 
49     private int       mChannelConfig = AudioFormat.CHANNEL_IN_MONO;
50     private int       mAudioFormat = AudioFormat.ENCODING_PCM_16BIT;
51     private int       mMinRecorderBuffSizeInBytes = 0;
52     private int       mMinRecorderBuffSizeInSamples = 0;
53 
54     private short[] mAudioShortArray;   // this array stores values from mAudioTone in read()
55     private short[] mBufferTestShortArray;
56     private short[] mAudioTone;
57 
58     // for glitch detection (buffer test)
59     private BufferPeriod          mRecorderBufferPeriodInRecorder;
60     private final int             mBufferTestWavePlotDurationInSeconds;
61     private final int             mChannelIndex;
62     private final double          mFrequency1;
63     private final double          mFrequency2; // not actually used
64     private int[]                 mAllGlitches; // value = 1 means there's a glitch in that interval
65     private boolean               mGlitchingIntervalTooLong;
66     private int                   mFFTSamplingSize; // the amount of samples used per FFT.
67     private int                   mFFTOverlapSamples; // overlap half the samples
68     private long                  mStartTimeMs;
69     private int                   mBufferTestDurationInSeconds;
70     private long                  mBufferTestDurationMs;
71     private final CaptureHolder   mCaptureHolder;
72     private final Context         mContext;
73     private AudioManager          mAudioManager;
74     private GlitchDetectionThread mGlitchDetectionThread;
75 
76     // for adjusting sound level in buffer test
77     private double[] mSoundLevelSamples;
78     private int      mSoundLevelSamplesIndex = 0;
79     private boolean  mIsAdjustingSoundLevel = true; // is true if still adjusting sound level
80     private double   mSoundBotLimit = 0.6;    // we want to keep the sound level high
81     private double   mSoundTopLimit = 0.8;    // but we also don't want to be close to saturation
82     private int      mAdjustSoundLevelCount = 0;
83     private int      mMaxVolume;   // max possible volume of the device
84 
85     private double[]  mSamples; // samples shown on WavePlotView
86     private int       mSamplesIndex;
87 
RecorderRunnable(PipeShort latencyPipe, int samplingRate, int channelConfig, int audioFormat, int recorderBufferInBytes, int micSource, LoopbackAudioThread audioThread, BufferPeriod recorderBufferPeriod, int testType, double frequency1, double frequency2, int bufferTestWavePlotDurationInSeconds, Context context, int channelIndex, CaptureHolder captureHolder)88     RecorderRunnable(PipeShort latencyPipe, int samplingRate, int channelConfig, int audioFormat,
89                      int recorderBufferInBytes, int micSource, LoopbackAudioThread audioThread,
90                      BufferPeriod recorderBufferPeriod, int testType, double frequency1,
91                      double frequency2, int bufferTestWavePlotDurationInSeconds,
92                      Context context, int channelIndex, CaptureHolder captureHolder) {
93         mLatencyTestPipeShort = latencyPipe;
94         mSamplingRate = samplingRate;
95         mChannelConfig = channelConfig;
96         mAudioFormat = audioFormat;
97         mMinRecorderBuffSizeInBytes = recorderBufferInBytes;
98         mSelectedRecordSource = micSource;
99         mAudioThread = audioThread;
100         mRecorderBufferPeriodInRecorder = recorderBufferPeriod;
101         mTestType = testType;
102         mFrequency1 = frequency1;
103         mFrequency2 = frequency2;
104         mBufferTestWavePlotDurationInSeconds = bufferTestWavePlotDurationInSeconds;
105         mContext = context;
106         mChannelIndex = channelIndex;
107         mCaptureHolder = captureHolder;
108     }
109 
110 
111     /** Initialize the recording device for latency test. */
initRecord()112     public boolean initRecord() {
113         log("Init Record");
114         if (mMinRecorderBuffSizeInBytes <= 0) {
115             mMinRecorderBuffSizeInBytes = AudioRecord.getMinBufferSize(mSamplingRate,
116                                           mChannelConfig, mAudioFormat);
117             log("RecorderRunnable: computing min buff size = " + mMinRecorderBuffSizeInBytes
118                 + " bytes");
119         } else {
120             log("RecorderRunnable: using min buff size = " + mMinRecorderBuffSizeInBytes +
121                 " bytes");
122         }
123 
124         if (mMinRecorderBuffSizeInBytes <= 0) {
125             return false;
126         }
127 
128         mMinRecorderBuffSizeInSamples = mMinRecorderBuffSizeInBytes / Constant.BYTES_PER_FRAME;
129         mAudioShortArray = new short[mMinRecorderBuffSizeInSamples];
130 
131         try {
132             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
133                 mRecorder = new AudioRecord.Builder()
134                         .setAudioFormat((mChannelIndex < 0 ?
135                                 new AudioFormat.Builder()
136                                         .setChannelMask(AudioFormat.CHANNEL_IN_MONO) :
137                                 new AudioFormat
138                                         .Builder().setChannelIndexMask(1 << mChannelIndex))
139                                 .setSampleRate(mSamplingRate)
140                                 .setEncoding(mAudioFormat)
141                                 .build())
142                         .setAudioSource(mSelectedRecordSource)
143                         .setBufferSizeInBytes(2 * mMinRecorderBuffSizeInBytes)
144                         .build();
145             } else {
146                 mRecorder = new AudioRecord(mSelectedRecordSource, mSamplingRate,
147                         mChannelConfig, mAudioFormat, 2 * mMinRecorderBuffSizeInBytes);
148             }
149         } catch (IllegalArgumentException | UnsupportedOperationException e) {
150             e.printStackTrace();
151             return false;
152         } finally {
153             if (mRecorder == null) {
154                 return false;
155             } else if (mRecorder.getState() != AudioRecord.STATE_INITIALIZED) {
156                 mRecorder.release();
157                 mRecorder = null;
158                 return false;
159             }
160         }
161 
162         //generate sinc wave for use in loopback test
163         ToneGeneration sincTone = new RampedSineTone(mSamplingRate, Constant.LOOPBACK_FREQUENCY);
164         mAudioTone = new short[Constant.LOOPBACK_SAMPLE_FRAMES];
165         sincTone.generateTone(mAudioTone, Constant.LOOPBACK_SAMPLE_FRAMES);
166 
167         return true;
168     }
169 
170 
171     /** Initialize the recording device for buffer test. */
initBufferRecord()172     boolean initBufferRecord() {
173         log("Init Record");
174         if (mMinRecorderBuffSizeInBytes <= 0) {
175 
176             mMinRecorderBuffSizeInBytes = AudioRecord.getMinBufferSize(mSamplingRate,
177                                           mChannelConfig, mAudioFormat);
178             log("RecorderRunnable: computing min buff size = " + mMinRecorderBuffSizeInBytes
179                 + " bytes");
180         } else {
181             log("RecorderRunnable: using min buff size = " + mMinRecorderBuffSizeInBytes +
182                 " bytes");
183         }
184 
185         if (mMinRecorderBuffSizeInBytes <= 0) {
186             return false;
187         }
188 
189         mMinRecorderBuffSizeInSamples = mMinRecorderBuffSizeInBytes / Constant.BYTES_PER_FRAME;
190         mBufferTestShortArray = new short[mMinRecorderBuffSizeInSamples];
191 
192         final int cycles = 100;
193         int soundLevelSamples =  (mSamplingRate / (int) mFrequency1) * cycles;
194         mSoundLevelSamples = new double[soundLevelSamples];
195         mAudioManager = (AudioManager) mContext.getSystemService(mContext.AUDIO_SERVICE);
196         mMaxVolume = mAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC);
197 
198         try {
199             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
200                 mRecorder = new AudioRecord.Builder()
201                         .setAudioFormat((mChannelIndex < 0 ?
202                                 new AudioFormat.Builder()
203                                         .setChannelMask(AudioFormat.CHANNEL_IN_MONO) :
204                                 new AudioFormat
205                                         .Builder().setChannelIndexMask(1 << mChannelIndex))
206                                 .setSampleRate(mSamplingRate)
207                                 .setEncoding(mAudioFormat)
208                                 .build())
209                         .setAudioSource(mSelectedRecordSource)
210                         .setBufferSizeInBytes(2 * mMinRecorderBuffSizeInBytes)
211                         .build();
212             } else {
213                 mRecorder = new AudioRecord(mSelectedRecordSource, mSamplingRate,
214                         mChannelConfig, mAudioFormat, 2 * mMinRecorderBuffSizeInBytes);
215             }
216         } catch (IllegalArgumentException | UnsupportedOperationException e) {
217             e.printStackTrace();
218             return false;
219         } finally {
220             if (mRecorder == null) {
221                 return false;
222             } else if (mRecorder.getState() != AudioRecord.STATE_INITIALIZED) {
223                 mRecorder.release();
224                 mRecorder = null;
225                 return false;
226             }
227         }
228 
229         final int targetFFTMs = 20; // we want each FFT to cover 20ms of samples
230         mFFTSamplingSize = targetFFTMs * mSamplingRate / Constant.MILLIS_PER_SECOND;
231         // round to the nearest power of 2
232         mFFTSamplingSize = (int) Math.pow(2, Math.round(Math.log(mFFTSamplingSize) / Math.log(2)));
233 
234         if (mFFTSamplingSize < 2) {
235             mFFTSamplingSize = 2; // mFFTSamplingSize should be at least 2
236         }
237         mFFTOverlapSamples = mFFTSamplingSize / 2; // mFFTOverlapSamples is half of mFFTSamplingSize
238 
239         return true;
240     }
241 
242 
startRecording()243     boolean startRecording() {
244         synchronized (sRecordingLock) {
245             mIsRecording = true;
246         }
247 
248         final int samplesDurationInSecond = 2;
249         int nNewSize = mSamplingRate * samplesDurationInSecond; // 2 seconds!
250         mSamples = new double[nNewSize];
251 
252         boolean status = initRecord();
253         if (status) {
254             log("Ready to go.");
255             startRecordingForReal();
256         } else {
257             log("Recorder initialization error.");
258             synchronized (sRecordingLock) {
259                 mIsRecording = false;
260             }
261         }
262 
263         return status;
264     }
265 
266 
startBufferRecording()267     boolean startBufferRecording() {
268         synchronized (sRecordingLock) {
269             mIsRecording = true;
270         }
271 
272         boolean status = initBufferRecord();
273         if (status) {
274             log("Ready to go.");
275             startBufferRecordingForReal();
276         } else {
277             log("Recorder initialization error.");
278             synchronized (sRecordingLock) {
279                 mIsRecording = false;
280             }
281         }
282 
283         return status;
284     }
285 
286 
startRecordingForReal()287     void startRecordingForReal() {
288         mLatencyTestPipeShort.flush();
289         mRecorder.startRecording();
290     }
291 
292 
startBufferRecordingForReal()293     void startBufferRecordingForReal() {
294         mBufferTestPipeShort = new PipeShort(Constant.MAX_SHORTS);
295         mGlitchDetectionThread = new GlitchDetectionThread(mFrequency1, mFrequency2, mSamplingRate,
296                 mFFTSamplingSize, mFFTOverlapSamples, mBufferTestDurationInSeconds,
297                 mBufferTestWavePlotDurationInSeconds, mBufferTestPipeShort, mCaptureHolder);
298         mGlitchDetectionThread.start();
299         mRecorder.startRecording();
300     }
301 
302 
stopRecording()303     void stopRecording() {
304         log("stop recording A");
305         synchronized (sRecordingLock) {
306             log("stop recording B");
307             mIsRecording = false;
308         }
309         stopRecordingForReal();
310     }
311 
312 
stopRecordingForReal()313     void stopRecordingForReal() {
314         log("stop recording for real");
315         if (mRecorder != null) {
316             mRecorder.stop();
317         }
318 
319         if (mRecorder != null) {
320             mRecorder.release();
321             mRecorder = null;
322         }
323     }
324 
325 
run()326     public void run() {
327         // keeps the total time elapsed since the start of the test. Only used in buffer test.
328         long elapsedTimeMs;
329         mIsRunning = true;
330         while (mIsRunning) {
331             boolean isRecording;
332 
333             synchronized (sRecordingLock) {
334                 isRecording = mIsRecording;
335             }
336 
337             if (isRecording && mRecorder != null) {
338                 int nSamplesRead;
339                 switch (mTestType) {
340                 case Constant.LOOPBACK_PLUG_AUDIO_THREAD_TEST_TYPE_LATENCY:
341                     nSamplesRead = mRecorder.read(mAudioShortArray, 0,
342                                    mMinRecorderBuffSizeInSamples);
343 
344                     if (nSamplesRead > 0) {
345                         mRecorderBufferPeriodInRecorder.collectBufferPeriod();
346                         { // inject the tone that will be looped-back
347                             int currentIndex = mSamplesIndex - 100; //offset
348                             for (int i = 0; i < nSamplesRead; i++) {
349                                 if (currentIndex >= 0 && currentIndex < mAudioTone.length) {
350                                     mAudioShortArray[i] = mAudioTone[currentIndex];
351                                 }
352                                 currentIndex++;
353                             }
354                         }
355 
356                         mLatencyTestPipeShort.write(mAudioShortArray, 0, nSamplesRead);
357                         if (isStillRoomToRecord()) { //record to vector
358                             for (int i = 0; i < nSamplesRead; i++) {
359                                 double value = mAudioShortArray[i];
360                                 value = value / Short.MAX_VALUE;
361                                 if (mSamplesIndex < mSamples.length) {
362                                     mSamples[mSamplesIndex++] = value;
363                                 }
364 
365                             }
366                         } else {
367                             mIsRunning = false;
368                         }
369                     }
370                     break;
371                 case Constant.LOOPBACK_PLUG_AUDIO_THREAD_TEST_TYPE_BUFFER_PERIOD:
372                     if (mIsRequestStop) {
373                         endBufferTest();
374                     } else {
375                         // before we start the test, first adjust sound level
376                         if (mIsAdjustingSoundLevel) {
377                             nSamplesRead = mRecorder.read(mBufferTestShortArray, 0,
378                                     mMinRecorderBuffSizeInSamples);
379                             if (nSamplesRead > 0) {
380                                 for (int i = 0; i < nSamplesRead; i++) {
381                                     double value = mBufferTestShortArray[i];
382                                     if (mSoundLevelSamplesIndex < mSoundLevelSamples.length) {
383                                         mSoundLevelSamples[mSoundLevelSamplesIndex++] = value;
384                                     } else {
385                                         // adjust the sound level to appropriate level
386                                         mIsAdjustingSoundLevel = AdjustSoundLevel();
387                                         mAdjustSoundLevelCount++;
388                                         mSoundLevelSamplesIndex = 0;
389                                         if (!mIsAdjustingSoundLevel) {
390                                             // end of sound level adjustment, notify AudioTrack
391                                             mAudioThread.setIsAdjustingSoundLevel(false);
392                                             mStartTimeMs = System.currentTimeMillis();
393                                             break;
394                                         }
395                                     }
396                                 }
397                             }
398                         } else {
399                             // the end of test is controlled here. Once we've run for the specified
400                             // test duration, end the test
401                             elapsedTimeMs = System.currentTimeMillis() - mStartTimeMs;
402                             if (elapsedTimeMs >= mBufferTestDurationMs) {
403                                 endBufferTest();
404                             } else {
405                                 nSamplesRead = mRecorder.read(mBufferTestShortArray, 0,
406                                         mMinRecorderBuffSizeInSamples);
407                                 if (nSamplesRead > 0) {
408                                     mRecorderBufferPeriodInRecorder.collectBufferPeriod();
409                                     mBufferTestPipeShort.write(mBufferTestShortArray, 0,
410                                             nSamplesRead);
411                                 }
412                             }
413                         }
414                     }
415                     break;
416                 }
417             }
418         } //synchronized
419         stopRecording(); //close this
420     }
421 
422 
423     /** Someone is requesting to stop the test, will stop the test even if the test is not done. */
requestStop()424     public void requestStop() {
425         switch (mTestType) {
426         case Constant.LOOPBACK_PLUG_AUDIO_THREAD_TEST_TYPE_BUFFER_PERIOD:
427             mIsRequestStop = true;
428             break;
429         case Constant.LOOPBACK_PLUG_AUDIO_THREAD_TEST_TYPE_LATENCY:
430             mIsRunning = false;
431             break;
432         }
433     }
434 
435 
436     /** Collect data then clean things up.*/
endBufferTest()437     private void endBufferTest() {
438         mIsRunning = false;
439         mAllGlitches = mGlitchDetectionThread.getGlitches();
440         mGlitchingIntervalTooLong = mGlitchDetectionThread.getGlitchingIntervalTooLong();
441         mSamples = mGlitchDetectionThread.getWaveData();
442         endDetecting();
443     }
444 
445 
446     /** Clean everything up. */
endDetecting()447     public void endDetecting() {
448         mBufferTestPipeShort.flush();
449         mBufferTestPipeShort = null;
450         mGlitchDetectionThread.requestStop();
451         GlitchDetectionThread tempThread = mGlitchDetectionThread;
452         mGlitchDetectionThread = null;
453         try {
454             tempThread.join(Constant.JOIN_WAIT_TIME_MS);
455         } catch (InterruptedException e) {
456             e.printStackTrace();
457         }
458     }
459 
460 
461     /**
462      * Adjust the sound level such that the buffer test can run with small noise disturbance.
463      * Return a boolean value to indicate whether or not the sound level has adjusted to an
464      * appropriate level.
465      */
AdjustSoundLevel()466     private boolean AdjustSoundLevel() {
467         // if after adjusting 20 times, we still cannot get into the volume we want, increase the
468         // limit range, so it's easier to get into the volume we want.
469         if (mAdjustSoundLevelCount != 0 && mAdjustSoundLevelCount % 20 == 0) {
470             mSoundTopLimit += 0.1;
471             mSoundBotLimit -= 0.1;
472         }
473 
474         double topThreshold = Short.MAX_VALUE * mSoundTopLimit;
475         double botThreshold = Short.MAX_VALUE * mSoundBotLimit;
476         double currentMax = mSoundLevelSamples[0];
477         int currentVolume = mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC);
478 
479         // since it's a sine wave, we are only checking max positive value
480         for (int i = 1; i < mSoundLevelSamples.length; i++) {
481             if (mSoundLevelSamples[i] > topThreshold) { // once a sample exceed, return
482                 // adjust sound level down
483                 currentVolume--;
484                 mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC, currentVolume, 0);
485                 return true;
486             }
487 
488             if (mSoundLevelSamples[i] > currentMax) {
489                 currentMax = mSoundLevelSamples[i];
490             }
491         }
492 
493         if (currentMax < botThreshold) {
494             // adjust sound level up
495             if (currentVolume < mMaxVolume) {
496                 currentVolume++;
497                 mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC,
498                         currentVolume, 0);
499                 return true;
500             } else {
501                 return false;
502             }
503         }
504 
505         return false;
506     }
507 
508 
509     /** Check if there's any room left in mSamples. */
isStillRoomToRecord()510     public boolean isStillRoomToRecord() {
511         boolean result = false;
512         if (mSamples != null) {
513             if (mSamplesIndex < mSamples.length) {
514                 result = true;
515             }
516         }
517 
518         return result;
519     }
520 
521 
setBufferTestDurationInSeconds(int bufferTestDurationInSeconds)522     public void setBufferTestDurationInSeconds(int bufferTestDurationInSeconds) {
523         mBufferTestDurationInSeconds = bufferTestDurationInSeconds;
524         mBufferTestDurationMs = Constant.MILLIS_PER_SECOND * mBufferTestDurationInSeconds;
525     }
526 
527 
getAllGlitches()528     public int[] getAllGlitches() {
529         return mAllGlitches;
530     }
531 
532 
getGlitchingIntervalTooLong()533     public boolean getGlitchingIntervalTooLong() {
534         return mGlitchingIntervalTooLong;
535     }
536 
537 
getWaveData()538     public double[] getWaveData() {
539         return mSamples;
540     }
541 
542 
getFFTSamplingSize()543     public int getFFTSamplingSize() {
544         return mFFTSamplingSize;
545     }
546 
547 
getFFTOverlapSamples()548     public int getFFTOverlapSamples() {
549         return mFFTOverlapSamples;
550     }
551 
552 
log(String msg)553     private static void log(String msg) {
554         Log.v(TAG, msg);
555     }
556 
557 }
558