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